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
20 changes: 18 additions & 2 deletions src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,8 @@ export type SessionStatus =
| "waiting_for_user"
| "completed"
| "interrupted"
| "ask_permission";
| "ask_permission"
| "permission_denied";

export type ModelUsage = {
prompt_tokens: number;
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -2715,7 +2730,8 @@ ${skillMd}
status === "waiting_for_user" ||
status === "completed" ||
status === "interrupted" ||
status === "ask_permission"
status === "ask_permission" ||
status === "permission_denied"
) {
return status;
}
Expand Down
51 changes: 51 additions & 0 deletions src/tests/session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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-");
Expand Down
2 changes: 2 additions & 0 deletions src/tests/sessionList.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});

Expand Down
3 changes: 3 additions & 0 deletions src/ui/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -686,6 +688,7 @@ function App({ projectRoot, initialPrompt, onRestart }: AppProps): React.ReactEl
sessionManager.interruptActiveSession();
setActiveStatus("interrupted");
setActiveAskPermissions(undefined);
setPromptDraft(null);
refreshSessionsList();
}, [refreshSessionsList, sessionManager]);

Expand Down
4 changes: 4 additions & 0 deletions src/ui/SessionList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Loading