Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 68 additions & 1 deletion apps/desktop/src/main/services/chat/agentChatService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ type PersistedChatState = {
preferredExecutionLaneId?: string | null;
selectedExecutionLaneId?: string | null;
lastLaneDirectiveKey?: string | null;
manuallyNamed?: boolean;
updatedAt: string;
};

Expand Down Expand Up @@ -566,6 +567,7 @@ type ManagedChatSession = {
autoTitleSeed: string | null;
autoTitleStage: "none" | "initial" | "final";
autoTitleInFlight: boolean;
manuallyNamed: boolean;
summaryInFlight: boolean;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
activeAssistantMessageId: string | null;
lastActivitySignature: string | null;
Expand Down Expand Up @@ -2844,6 +2846,7 @@ export function createAgentChatService(args: {
): Promise<void> => {
const config = resolveChatConfig();
if (!config.autoTitleEnabled) return;
if (managed.manuallyNamed) return;
if (managed.autoTitleInFlight) return;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if (args.stage === "initial" && managed.autoTitleStage !== "none") return;
if (args.stage === "final") {
Expand Down Expand Up @@ -2909,6 +2912,8 @@ export function createAgentChatService(args: {
titleContext.join("\n"),
].join("\n\n"),
});
// Re-check after async — user may have manually renamed while the request was in flight.
if (managed.manuallyNamed) return;
const nextTitle = setManagedSessionTitle(managed, result.text);
if (!nextTitle) return;
managed.autoTitleStage = args.stage;
Expand Down Expand Up @@ -3178,6 +3183,7 @@ export function createAgentChatService(args: {
...(managed.preferredExecutionLaneId ? { preferredExecutionLaneId: managed.preferredExecutionLaneId } : {}),
...(managed.selectedExecutionLaneId ? { selectedExecutionLaneId: managed.selectedExecutionLaneId } : {}),
...(managed.lastLaneDirectiveKey ? { lastLaneDirectiveKey: managed.lastLaneDirectiveKey } : {}),
manuallyNamed: Boolean(managed.manuallyNamed),
updatedAt: nowIso()
};

Expand Down Expand Up @@ -3293,6 +3299,7 @@ export function createAgentChatService(args: {
...(typeof record.lastLaneDirectiveKey === "string" && record.lastLaneDirectiveKey.trim().length
? { lastLaneDirectiveKey: record.lastLaneDirectiveKey.trim() }
: {}),
...(record.manuallyNamed === true ? { manuallyNamed: true } : {}),
updatedAt: typeof record.updatedAt === "string" && record.updatedAt.trim().length ? record.updatedAt : nowIso()
};
hydrateNativePermissionControls(hydrated as Parameters<typeof hydrateNativePermissionControls>[0]);
Expand Down Expand Up @@ -4121,6 +4128,7 @@ export function createAgentChatService(args: {
autoTitleSeed: null,
autoTitleStage: hasCustomChatSessionTitle(row.title, provider) ? "initial" : "none",
autoTitleInFlight: false,
manuallyNamed: persisted?.manuallyNamed === true,
summaryInFlight: false,
continuitySummary: persisted?.continuitySummary ?? null,
continuitySummaryUpdatedAt: persisted?.continuitySummaryUpdatedAt ?? null,
Expand Down Expand Up @@ -7303,6 +7311,13 @@ export function createAgentChatService(args: {
return;
}

// Apply permission mode before the first interaction so the session
// starts with the correct approval behaviour selected in the rebase tab.
const initialPermissionMode = resolveClaudeTurnPermissionMode(managed);
if (typeof runtime.v2Session.setPermissionMode === "function") {
await runtime.v2Session.setPermissionMode(initialPermissionMode);
}

await runtime.v2Session.send("System initialization check. Respond with only the word READY.");
for await (const msg of runtime.v2Session.stream()) {
if (runtime.v2WarmupCancelled) break;
Expand Down Expand Up @@ -7456,6 +7471,7 @@ export function createAgentChatService(args: {
autoTitleSeed: null,
autoTitleStage: "none",
autoTitleInFlight: false,
manuallyNamed: false,
summaryInFlight: false,
continuitySummary: null,
continuitySummaryUpdatedAt: null,
Expand Down Expand Up @@ -7613,11 +7629,13 @@ export function createAgentChatService(args: {
automationId,
automationRunId,
computerUse,
requestedCwd,
}: AgentChatCreateArgs): Promise<AgentChatSession> => {
const launchContext = resolveLaneLaunchContext({
laneService,
laneId,
purpose: "start this chat",
requestedCwd,
});
const sessionId = randomUUID();
const startedAt = nowIso();
Expand Down Expand Up @@ -7763,6 +7781,7 @@ export function createAgentChatService(args: {
autoTitleSeed: null,
autoTitleStage: "none",
autoTitleInFlight: false,
manuallyNamed: false,
summaryInFlight: false,
continuitySummary: null,
continuitySummaryUpdatedAt: null,
Expand Down Expand Up @@ -8113,6 +8132,14 @@ export function createAgentChatService(args: {
if (reasoningEffort) {
managed.session.reasoningEffort = normalizeReasoningEffort(reasoningEffort);
}
// Re-sync permission mode so mid-session changes take effect on this turn.
if (managed.runtime?.kind === "unified") {
const chatConfig = resolveChatConfig();
managed.runtime.permissionMode = resolveSessionUnifiedPermissionMode(
managed.session,
chatConfig.unifiedPermissionMode,
);
}
Comment thread
arul28 marked this conversation as resolved.
await runTurn(managed, {
promptText,
displayText: visibleText,
Expand All @@ -8133,6 +8160,20 @@ export function createAgentChatService(args: {
managed.session.reasoningEffort = DEFAULT_REASONING_EFFORT;
}

// Re-sync codex approval policy so mid-session changes take effect on this turn.
if (runtime.threadResumed) {
const prevApproval = managed.session.codexApprovalPolicy;
const prevSandbox = managed.session.codexSandbox;
resolveCodexThreadParams(managed);
if (
managed.session.codexApprovalPolicy !== prevApproval
|| managed.session.codexSandbox !== prevSandbox
) {
// Policy drifted — force a re-resume so the codex server picks up the new settings.
runtime.threadResumed = false;
}
}

if (!runtime.threadResumed) {
const threadIdToResume = managed.session.threadId || readPersistedState(sessionId)?.threadId;
const { codexPolicy, mcpServers } = resolveCodexThreadParams(managed);
Expand Down Expand Up @@ -8487,6 +8528,11 @@ export function createAgentChatService(args: {
await startFreshCodexThread(managed, runtime, codexPolicy, mcpServers);
}
}
// Re-sync codex approval policy from persisted/config settings
managed.session.codexApprovalPolicy = persisted?.codexApprovalPolicy ?? managed.session.codexApprovalPolicy;
managed.session.codexSandbox = persisted?.codexSandbox ?? managed.session.codexSandbox;
managed.session.codexConfigSource = persisted?.codexConfigSource ?? managed.session.codexConfigSource;
managed.session.permissionMode = syncLegacyPermissionMode(managed.session) ?? managed.session.permissionMode;
} else if (managed.runtime?.kind === "unified" || (managed.session.modelId && !providerResolver.isModelCliWrapped(managed.session.modelId))) {
// Unified runtime resume — re-resolve the model
const result = await startUnifiedSession(managed);
Expand All @@ -8509,11 +8555,21 @@ export function createAgentChatService(args: {
}
// Fallthrough to Claude — SDK manages history via sdkSessionId
ensureClaudeSessionRuntime(managed);
// Re-sync permission mode from persisted/config settings
const fallbackPermMode = resolveClaudeTurnPermissionMode(managed);
if (managed.runtime?.kind === "claude" && managed.runtime.v2Session && typeof managed.runtime.v2Session.setPermissionMode === "function") {
await managed.runtime.v2Session.setPermissionMode(fallbackPermMode);
}
sessionService.setResumeCommand(sessionId, `chat:claude:${sessionId}`);
}
} else {
// Claude — SDK manages history via sdkSessionId
ensureClaudeSessionRuntime(managed);
// Re-sync permission mode from persisted/config settings
const claudePermMode = resolveClaudeTurnPermissionMode(managed);
if (managed.runtime?.kind === "claude" && managed.runtime.v2Session && typeof managed.runtime.v2Session.setPermissionMode === "function") {
await managed.runtime.v2Session.setPermissionMode(claudePermMode);
}
sessionService.setResumeCommand(sessionId, `chat:claude:${sessionId}`);
}

Expand Down Expand Up @@ -8948,6 +9004,7 @@ export function createAgentChatService(args: {
const updateSession = async ({
sessionId,
title,
manuallyNamed,
modelId,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
reasoningEffort,
interactionMode,
Expand Down Expand Up @@ -9157,10 +9214,20 @@ export function createAgentChatService(args: {

if (title !== undefined) {
const normalizedTitle = String(title ?? "").trim();
const hasExplicitTitle = normalizedTitle.length > 0;
sessionService.updateMeta({
sessionId,
title: normalizedTitle.length ? normalizedTitle : defaultChatSessionTitle(managed.session.provider),
title: hasExplicitTitle ? normalizedTitle : defaultChatSessionTitle(managed.session.provider),
});
if (manuallyNamed !== undefined) {
managed.manuallyNamed = manuallyNamed && hasExplicitTitle;
} else if (!hasExplicitTitle) {
managed.manuallyNamed = false;
}
}
// Allow resetting manuallyNamed independently when no title change is provided
if (manuallyNamed !== undefined && title === undefined) {
managed.manuallyNamed = manuallyNamed;
}

persistChatState(managed);
Expand Down
13 changes: 12 additions & 1 deletion apps/desktop/src/main/services/git/gitOperationsService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -608,7 +608,18 @@ export function createGitOperationsService({
} else if (normalizedBehind > 0 && normalizedAhead === 0) {
recommendedAction = "pull";
} else if (diverged) {
recommendedAction = "pull";
// Check if local HEAD contains the upstream tip. When both sides have
// unique commits, the upstream tip is almost never an ancestor of HEAD
// (that only happens after a local rebase that replayed upstream), so
// the safest default for genuine divergence is "pull" (merge upstream
// into local). We only suggest force-push when we can confirm the
// upstream tip IS already an ancestor — meaning the local branch was
// rebased on top of upstream and only needs a force-push to publish.
const mergeBaseRes = await runGit(["merge-base", "--is-ancestor", upstreamRef, "HEAD"], {
cwd: lane.worktreePath,
timeoutMs: 5_000
});
recommendedAction = mergeBaseRes.exitCode === 0 ? "force_push_lease" : "pull";
}

return {
Expand Down
Loading
Loading