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
4 changes: 3 additions & 1 deletion src/hooks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,16 @@ describe("installHooks", () => {
assert.ok(settings.hooks);
});

it("creates all 5 hook events including PreToolUse", () => {
it("creates all 7 hook events including PreToolUse, PermissionRequest, and PostToolUse", () => {
installHooks(8377, tmpDir);
const settings = readSettings() as { hooks: Record<string, unknown[]> };
assert.ok(settings.hooks.SessionStart);
assert.ok(settings.hooks.UserPromptSubmit);
assert.ok(settings.hooks.Stop);
assert.ok(settings.hooks.SessionEnd);
assert.ok(settings.hooks.PreToolUse);
assert.ok(settings.hooks.PermissionRequest);
assert.ok(settings.hooks.PostToolUse);
});

it("PreToolUse hook captures all tools without matcher", () => {
Expand Down
9 changes: 8 additions & 1 deletion src/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,14 @@ const MARKER_QUICK = "__claude_code_dashboard_quick__";
const MARKER_INSTALL = "__claude_code_dashboard_install__";
const MARKER_LEGACY = "__claude_code_dashboard__";

const HOOK_EVENTS = ["SessionStart", "UserPromptSubmit", "Stop", "SessionEnd"] as const;
const HOOK_EVENTS = [
"SessionStart",
"UserPromptSubmit",
"Stop",
"SessionEnd",
"PermissionRequest",
"PostToolUse",
] as const;

interface HookEntry {
type: string;
Expand Down
108 changes: 106 additions & 2 deletions src/state.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ describe("createStore", () => {
assert.equal(session?.lastEvent, "Read");
});

it("PreToolUse with Bash transitions to waiting", () => {
it("PreToolUse with Bash stays running", () => {
const store = createStore();
store.handleEvent({ session_id: "s1", hook_event_name: "SessionStart" });
store.handleEvent({
Expand All @@ -176,7 +176,7 @@ describe("createStore", () => {
hook_event_name: "PreToolUse",
tool_name: "Bash",
});
assert.equal(session?.status, "waiting");
assert.equal(session?.status, "running");
assert.equal(session?.lastEvent, "Bash");
});

Expand Down Expand Up @@ -260,6 +260,110 @@ describe("createStore", () => {
assert.equal(session?.status, "waiting");
});

it("PermissionRequest transitions to waiting with tool name", () => {
const store = createStore();
store.handleEvent({ session_id: "s1", hook_event_name: "SessionStart" });
store.handleEvent({ session_id: "s1", hook_event_name: "UserPromptSubmit" });
store.handleEvent({ session_id: "s1", hook_event_name: "PreToolUse", tool_name: "Bash" });
assert.equal(store.getSession("s1")?.status, "running");
const session = store.handleEvent({
session_id: "s1",
hook_event_name: "PermissionRequest",
tool_name: "Bash",
});
assert.equal(session?.status, "waiting");
assert.equal(session?.lastEvent, "Bash");
});

it("PostToolUse transitions to running with tool name", () => {
const store = createStore();
store.handleEvent({ session_id: "s1", hook_event_name: "SessionStart" });
store.handleEvent({ session_id: "s1", hook_event_name: "UserPromptSubmit" });
store.handleEvent({ session_id: "s1", hook_event_name: "PreToolUse", tool_name: "Bash" });
store.handleEvent({
session_id: "s1",
hook_event_name: "PermissionRequest",
tool_name: "Bash",
});
assert.equal(store.getSession("s1")?.status, "waiting");
const session = store.handleEvent({
session_id: "s1",
hook_event_name: "PostToolUse",
tool_name: "Bash",
});
assert.equal(session?.status, "running");
assert.equal(session?.lastEvent, "Bash");
});

it("Full approval flow: PreToolUse → PermissionRequest → PostToolUse", () => {
const store = createStore();
store.handleEvent({ session_id: "s1", hook_event_name: "SessionStart" });
store.handleEvent({ session_id: "s1", hook_event_name: "UserPromptSubmit" });

const afterPre = store.handleEvent({
session_id: "s1",
hook_event_name: "PreToolUse",
tool_name: "Bash",
});
assert.equal(afterPre?.status, "running");

const afterPerm = store.handleEvent({
session_id: "s1",
hook_event_name: "PermissionRequest",
tool_name: "Bash",
});
assert.equal(afterPerm?.status, "waiting");

const afterPost = store.handleEvent({
session_id: "s1",
hook_event_name: "PostToolUse",
tool_name: "Bash",
});
assert.equal(afterPost?.status, "running");
});

it("Auto-approved flow: PreToolUse → PostToolUse (no PermissionRequest)", () => {
const store = createStore();
store.handleEvent({ session_id: "s1", hook_event_name: "SessionStart" });
store.handleEvent({ session_id: "s1", hook_event_name: "UserPromptSubmit" });

const afterPre = store.handleEvent({
session_id: "s1",
hook_event_name: "PreToolUse",
tool_name: "Bash",
});
assert.equal(afterPre?.status, "running");

const afterPost = store.handleEvent({
session_id: "s1",
hook_event_name: "PostToolUse",
tool_name: "Bash",
});
assert.equal(afterPost?.status, "running");
});

it("PermissionRequest without tool_name falls back to event name", () => {
const store = createStore();
store.handleEvent({ session_id: "s1", hook_event_name: "SessionStart" });
const session = store.handleEvent({
session_id: "s1",
hook_event_name: "PermissionRequest",
});
assert.equal(session?.status, "waiting");
assert.equal(session?.lastEvent, "PermissionRequest");
});

it("PostToolUse without tool_name falls back to event name", () => {
const store = createStore();
store.handleEvent({ session_id: "s1", hook_event_name: "SessionStart" });
const session = store.handleEvent({
session_id: "s1",
hook_event_name: "PostToolUse",
});
assert.equal(session?.status, "running");
assert.equal(session?.lastEvent, "PostToolUse");
});

it("removeSession deletes an existing session", () => {
const store = createStore();
store.handleEvent({ session_id: "s1", hook_event_name: "SessionStart" });
Expand Down
9 changes: 8 additions & 1 deletion src/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ const EVENT_TO_STATUS: Record<string, SessionStatus> = {
const INTERACTIVE_TOOLS = new Set([
"ExitPlanMode",
"AskUserQuestion",
"Bash",
"Write",
"Edit",
"NotebookEdit",
Expand Down Expand Up @@ -62,6 +61,14 @@ export function createStore(): Store {
const toolName = typeof payload.tool_name === "string" ? payload.tool_name : "";
displayEvent = toolName || hook_event_name;
status = INTERACTIVE_TOOLS.has(toolName) ? "waiting" : "running";
} else if (hook_event_name === "PermissionRequest") {
const toolName = typeof payload.tool_name === "string" ? payload.tool_name : "";
displayEvent = toolName || hook_event_name;
status = "waiting";
} else if (hook_event_name === "PostToolUse") {
const toolName = typeof payload.tool_name === "string" ? payload.tool_name : "";
displayEvent = toolName || hook_event_name;
status = "running";
} else {
const mapped = EVENT_TO_STATUS[hook_event_name];
if (!mapped) {
Expand Down