diff --git a/src/session.ts b/src/session.ts index c81ae4a..a9fc39e 100644 --- a/src/session.ts +++ b/src/session.ts @@ -159,7 +159,8 @@ export type SessionStatus = | "waiting_for_user" | "completed" | "interrupted" - | "ask_permission"; + | "ask_permission" + | "permission_denied"; export type ModelUsage = { prompt_tokens: number; @@ -1532,6 +1533,20 @@ ${skillMd} return !this.sessionControllers.has(sessionId); } + /** + * Mark a session's permission as denied by the user. + * Updates the session entry status and failReason so the denial is visible in the session list. + */ + denySessionPermission(sessionId: string, reason?: string): void { + const now = new Date().toISOString(); + this.updateSessionEntry(sessionId, (entry) => ({ + ...entry, + status: "permission_denied", + failReason: reason ?? "Permission denied by user", + updateTime: now, + })); + } + adjustActiveBashTimeout(deltaMs: number): BashTimeoutAdjustment | null { const sessionId = this.activeSessionId; if (!sessionId || !Number.isFinite(deltaMs)) { @@ -2715,7 +2730,8 @@ ${skillMd} status === "waiting_for_user" || status === "completed" || status === "interrupted" || - status === "ask_permission" + status === "ask_permission" || + status === "permission_denied" ) { return status; } diff --git a/src/tests/session.test.ts b/src/tests/session.test.ts index e0a863e..95de8e3 100644 --- a/src/tests/session.test.ts +++ b/src/tests/session.test.ts @@ -1313,6 +1313,57 @@ test("activateSession pauses for permission when a tool call requires ask", asyn ); }); +test("SessionManager preserves permission_denied status when sessions are reloaded", async () => { + const workspace = createTempDir("deepcode-permission-denied-workspace-"); + const home = createTempDir("deepcode-permission-denied-home-"); + setHomeDir(home); + + const permissions = { + allow: [], + deny: [], + ask: [], + defaultMode: "askAll" as const, + }; + const manager = createPermissionSessionManager( + workspace, + [ + { + choices: [ + { + message: { + content: "", + tool_calls: [ + { + id: "call-bash", + type: "function", + function: { + name: "bash", + arguments: JSON.stringify({ + command: "rg TODO src", + description: "Search TODO markers", + sideEffects: ["read-in-cwd"], + }), + }, + }, + ], + }, + }, + ], + }, + ], + permissions + ); + + const sessionId = await manager.createSession({ text: "search todos" }); + manager.denySessionPermission(sessionId); + + const reloadedManager = createPermissionSessionManager(workspace, [], permissions); + const reloadedSession = reloadedManager.getSession(sessionId); + + assert.equal(reloadedSession?.status, "permission_denied"); + assert.equal(reloadedSession?.failReason, "Permission denied by user"); +}); + test("replySession applies permission replies, runs pending tools, and stores always allow scopes", async () => { const workspace = createTempDir("deepcode-permission-allow-workspace-"); const home = createTempDir("deepcode-permission-allow-home-"); diff --git a/src/tests/sessionList.test.ts b/src/tests/sessionList.test.ts index 3dfda33..6fe41c7 100644 --- a/src/tests/sessionList.test.ts +++ b/src/tests/sessionList.test.ts @@ -18,6 +18,8 @@ test("formatSessionStatus maps status values to display labels", () => { assert.equal(formatSessionStatus("waiting_for_user"), "waiting"); assert.equal(formatSessionStatus("failed"), "failed"); assert.equal(formatSessionStatus("interrupted"), "stopped"); + assert.equal(formatSessionStatus("ask_permission"), "waiting"); + assert.equal(formatSessionStatus("permission_denied"), "denied"); assert.equal(formatSessionStatus("unknown_status" as any), "unknown_status"); }); diff --git a/src/ui/App.tsx b/src/ui/App.tsx index 71e9ca3..ae94fa0 100644 --- a/src/ui/App.tsx +++ b/src/ui/App.tsx @@ -669,6 +669,8 @@ function App({ projectRoot, initialPrompt, onRestart }: AppProps): React.ReactEl alwaysAllows: result.alwaysAllows, }); setStatusLine("Permission denied. Add a reply, then press Enter to continue."); + setPromptDraft(null); + sessionManager.denySessionPermission(sessionId); return; } void handlePrompt({ @@ -686,6 +688,7 @@ function App({ projectRoot, initialPrompt, onRestart }: AppProps): React.ReactEl sessionManager.interruptActiveSession(); setActiveStatus("interrupted"); setActiveAskPermissions(undefined); + setPromptDraft(null); refreshSessionsList(); }, [refreshSessionsList, sessionManager]); diff --git a/src/ui/SessionList.tsx b/src/ui/SessionList.tsx index 82ca797..2d83b84 100644 --- a/src/ui/SessionList.tsx +++ b/src/ui/SessionList.tsx @@ -332,6 +332,10 @@ export function formatSessionStatus(status: SessionStatus): string { return "failed"; case "interrupted": return "stopped"; + case "ask_permission": + return "waiting"; + case "permission_denied": + return "denied"; default: return status; }