Keep finished terminals visible as attention

This commit is contained in:
Mohamed Boudra
2026-06-15 14:06:48 +07:00
parent 71b5c35d9b
commit a4c4418164
12 changed files with 147 additions and 55 deletions
@@ -1,5 +1,10 @@
import { describe, expect, it } from "vitest";
import { TERMINAL_ACTIVITY_STATES, TerminalActivitySchema } from "./terminal-activity.js";
import {
TERMINAL_ACTIVITY_ATTENTION_REASONS,
TERMINAL_ACTIVITY_STATES,
TerminalActivitySchema,
deriveTerminalActivityStatusBucket,
} from "./terminal-activity.js";
describe("TerminalActivitySchema", () => {
it("parses the known activity states", () => {
@@ -16,4 +21,32 @@ describe("TerminalActivitySchema", () => {
expect(parsed.state).toBe("idle");
expect(parsed.changedAt).toBe(1718000000000);
});
it("parses known attention reasons", () => {
for (const attentionReason of TERMINAL_ACTIVITY_ATTENTION_REASONS) {
expect(
TerminalActivitySchema.parse({ state: "attention", attentionReason, changedAt: 1 })
.attentionReason,
).toBe(attentionReason);
}
});
it("maps terminal activity to workspace status buckets", () => {
expect(deriveTerminalActivityStatusBucket({ state: "working", changedAt: 1 })).toBe("running");
expect(
deriveTerminalActivityStatusBucket({
state: "idle",
attentionReason: "finished",
changedAt: 1,
}),
).toBe("attention");
expect(
deriveTerminalActivityStatusBucket({
state: "idle",
attentionReason: "needs_input",
changedAt: 1,
}),
).toBe("needs_input");
expect(deriveTerminalActivityStatusBucket({ state: "idle", changedAt: 1 })).toBeNull();
});
});
@@ -1,15 +1,31 @@
import { z } from "zod";
export const TERMINAL_ACTIVITY_STATES = ["idle", "working", "attention"] as const;
export const TERMINAL_ACTIVITY_ATTENTION_REASONS = ["finished", "needs_input"] as const;
export type TerminalActivityState = (typeof TERMINAL_ACTIVITY_STATES)[number];
export type TerminalActivityAttentionReason = (typeof TERMINAL_ACTIVITY_ATTENTION_REASONS)[number];
export const TerminalActivitySchema = z.object({
// Forward-compat: a newer daemon may send a state this client doesn't know.
// Degrade unknown states to "idle" (no indicator, no notification) so the
// message still parses, instead of a strict enum rejecting the whole payload.
state: z.enum(TERMINAL_ACTIVITY_STATES).catch("idle"),
attentionReason: z.enum(TERMINAL_ACTIVITY_ATTENTION_REASONS).nullable().optional().catch(null),
changedAt: z.number(),
});
export type TerminalActivity = z.infer<typeof TerminalActivitySchema>;
export type TerminalActivityStatusBucket = "running" | "needs_input" | "attention";
export function deriveTerminalActivityStatusBucket(
activity: TerminalActivity | null | undefined,
): TerminalActivityStatusBucket | null {
if (!activity) return null;
if (activity.attentionReason === "needs_input") return "needs_input";
if (activity.attentionReason === "finished") return "attention";
if (activity.state === "working") return "running";
if (activity.state === "attention") return "needs_input";
return null;
}