Mark completed hook runs as completed

This commit is contained in:
murashit 2026-06-16 15:01:17 +09:00
parent 6a5fcc67df
commit b68f2d1fb8
4 changed files with 40 additions and 1 deletions

View file

@ -1,5 +1,5 @@
import { definedProp } from "../../../../../utils";
import type { HookMessageStreamItem } from "../../../domain/message-stream/items";
import type { ExecutionState, HookMessageStreamItem } from "../../../domain/message-stream/items";
interface MessageStreamHookRun {
id: string;
@ -10,6 +10,17 @@ interface MessageStreamHookRun {
entries: readonly { kind: string; text: string }[];
}
type MessageStreamExecutionState = Exclude<ExecutionState, null>;
type ExecutionStateByStatus = Readonly<Record<string, MessageStreamExecutionState>>;
const HOOK_RUN_STATES = {
running: "running",
completed: "completed",
failed: "failed",
blocked: "failed",
stopped: "failed",
} as const satisfies ExecutionStateByStatus;
export function hookRunMessageStreamItem(run: MessageStreamHookRun, turnId: string | null, status: string): HookMessageStreamItem | null {
if (run.id.length === 0) return null;
const eventName = hookEventName(run.eventName);
@ -25,6 +36,7 @@ export function hookRunMessageStreamItem(run: MessageStreamHookRun, turnId: stri
sourceItemId: displayId,
provenance: { source: "appServer", channel: "notification", event: "hookRun", sourceItemId: displayId },
status,
executionState: hookRunExecutionState(status),
hookRun: {
eventName,
...definedProp("statusMessage", run.statusMessage ?? undefined),
@ -35,6 +47,14 @@ export function hookRunMessageStreamItem(run: MessageStreamHookRun, turnId: stri
};
}
function hookRunExecutionState(status: string): ExecutionState {
return executionStateFromStatus(status, HOOK_RUN_STATES);
}
function executionStateFromStatus(status: string, states: ExecutionStateByStatus): ExecutionState {
return states[status] ?? null;
}
function hookRunDisplayId(run: MessageStreamHookRun): string {
return `hook-${run.id}-${run.startedAt.toString()}`;
}

View file

@ -12,6 +12,7 @@ import {
createAutoReviewResultItem,
createReviewResultItem,
} from "../../../../src/features/chat/app-server/mappers/message-stream/review-result-items";
import { hookRunMessageStreamItem } from "../../../../src/features/chat/app-server/mappers/message-stream/hook-run-items";
import {
messageStreamItemFromTurnItem,
messageStreamItemsFromTurns,
@ -1437,6 +1438,8 @@ describe("execution state uses typed status adapters before rendered text", () =
).toMatchObject({ executionState: "running" });
expect(autoReviewItem("approved")).toMatchObject({ executionState: "completed" });
expect(autoReviewItem("timedOut")).toMatchObject({ executionState: "failed" });
expect(hookRunMessageStreamItem(hookRun(), "turn", "completed")).toMatchObject({ executionState: "completed" });
expect(hookRunMessageStreamItem(hookRun(), "turn", "stopped")).toMatchObject({ executionState: "failed" });
expect(collabAgentStateExecutionState("inProgress")).toBe("running");
expect(collabAgentStateExecutionState("failed")).toBe("failed");
});
@ -1569,3 +1572,14 @@ function autoReviewItem(status: string): MessageStreamItem {
action: { type: "applyPatch", cwd: "/vault", files: ["/vault/src/main.ts"] },
});
}
function hookRun() {
return {
id: "hook-1",
eventName: "postToolUse",
statusMessage: "Formatted 1 file.",
startedAt: 1n,
durationMs: 1n,
entries: [{ kind: "feedback", text: "ok" }],
};
}

View file

@ -268,6 +268,7 @@ describe("ChatInboundController", () => {
operation: "postToolUse",
primaryTarget: { kind: "value", value: "Formatted 1 file." },
status: "completed",
executionState: "completed",
output: "",
hookRun: {
eventName: "postToolUse",

View file

@ -259,6 +259,7 @@ describe("work log renderer decisions", () => {
toolName: "hook",
turnId: "turn",
status: "completed",
executionState: "completed",
hookRun: {
eventName: "postToolUse",
statusMessage: "Formatted 1 file.",
@ -276,6 +277,7 @@ describe("work log renderer decisions", () => {
const element = renderMessageBlockElement(block);
expect(topLevelDetailsSummaries(element)).toEqual(["hook"]);
expect(element.classList.contains("codex-panel__execution--completed")).toBe(true);
expect([...element.querySelectorAll("details summary")].map((summary) => summary.textContent)).toEqual(["hook"]);
expect(element.querySelector(".codex-panel__tool-summary")?.textContent).toBe("postToolUse: Formatted 1 file.");
expect(element.textContent).not.toContain("Details");
@ -302,6 +304,7 @@ describe("work log renderer decisions", () => {
toolName: "hook",
turnId: "turn",
status: "completed",
executionState: "completed",
hookRun: {
eventName: "postToolUse",
statusMessage: "Formatted 1 file.",
@ -328,6 +331,7 @@ describe("work log renderer decisions", () => {
expect(element).toBeDefined();
expect(element.querySelector(":scope > summary")?.textContent).toBe("Work details: hook");
expect(element.querySelector(".codex-panel__tool-item--hook")?.classList.contains("codex-panel__execution--completed")).toBe(true);
expect(element.querySelector(".codex-panel__tool-summary")?.textContent).toBe("postToolUse: Formatted 1 file.");
expect(element.querySelector(".codex-panel__meta-grid")?.textContent).toContain("statuscompleted");
expect(element.querySelector(".codex-panel__meta-grid")?.textContent).toContain("eventpostToolUse");