Clear terminal attention when the user views the terminal
A finished or input-waiting terminal shows an attention indicator. The
focus heartbeat already asked the daemon to clear it, but the clear was a
no-op: it guarded on activity state === "attention", while the real
tracker encodes attention as state "idle" plus an attentionReason
("finished" / "needs_input"). The state is never literally "attention",
so the indicator only ever cleared when the agent started a new turn.
The worker proxy also had no way to reach the tracker's clearAttention():
it only spoke setActivity, and setActivity("idle") is short-circuited by
the tracker while a "finished" reason is set. Add a clearAttention worker
message that calls the existing manager.clearTerminalAttention(), and
guard both clear paths on attentionReason instead of state.
This commit is contained in:
@@ -258,6 +258,12 @@ async function handleRequest(message: TerminalWorkerRequest): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
case "clearAttention": {
|
||||
await manager.clearTerminalAttention(message.terminalId);
|
||||
sendToParent({ type: "response", requestId: message.requestId, ok: true });
|
||||
return;
|
||||
}
|
||||
|
||||
case "killTerminal": {
|
||||
const session = manager.getTerminal(message.terminalId);
|
||||
const cwd = session?.cwd;
|
||||
|
||||
@@ -54,6 +54,11 @@ export type TerminalWorkerRequest =
|
||||
terminalId: string;
|
||||
state: TerminalActivityState;
|
||||
}
|
||||
| {
|
||||
type: "clearAttention";
|
||||
requestId: string;
|
||||
terminalId: string;
|
||||
}
|
||||
| {
|
||||
type: "killTerminal";
|
||||
requestId: string;
|
||||
|
||||
@@ -665,7 +665,7 @@ it("sets terminal activity through a worker request", async () => {
|
||||
await expect(result).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it("clears terminal attention through a worker activity update", async () => {
|
||||
it("clears terminal attention through a worker request", async () => {
|
||||
const worker = new FakeTerminalWorker();
|
||||
manager = createWorkerTerminalManager({
|
||||
requestTimeoutMs: 50,
|
||||
@@ -677,19 +677,16 @@ it("clears terminal attention through a worker activity update", async () => {
|
||||
id: "terminal-a",
|
||||
name: "Shell",
|
||||
cwd: "/workspace",
|
||||
activity: { state: "attention", changedAt: 1000 },
|
||||
activity: { state: "idle", attentionReason: "finished", changedAt: 1000 },
|
||||
},
|
||||
state: createTerminalState(),
|
||||
});
|
||||
|
||||
const result = manager.clearTerminalAttention("terminal-a");
|
||||
const request = worker.sentMessages.find(
|
||||
(message) => message.type === "setActivity" && message.state === "idle",
|
||||
);
|
||||
const request = worker.sentMessages.find((message) => message.type === "clearAttention");
|
||||
expect(request).toMatchObject({
|
||||
type: "setActivity",
|
||||
type: "clearAttention",
|
||||
terminalId: "terminal-a",
|
||||
state: "idle",
|
||||
});
|
||||
if (!request) {
|
||||
throw new Error("attention clear request not sent");
|
||||
@@ -699,6 +696,41 @@ it("clears terminal attention through a worker activity update", async () => {
|
||||
await expect(result).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it("clears finished attention on a real terminal", async () => {
|
||||
const cwd = mkdtempSync(join(tmpdir(), "worker-terminal-manager-attention-"));
|
||||
temporaryDirs.push(cwd);
|
||||
manager = createWorkerTerminalManager();
|
||||
const session = trackTerminal(
|
||||
await manager.createTerminal({
|
||||
cwd,
|
||||
...nodeTerminalCommand("setInterval(() => {}, 1000);"),
|
||||
}),
|
||||
);
|
||||
|
||||
// A working -> idle transition is how the real tracker records a "finished"
|
||||
// attention: { state: "idle", attentionReason: "finished" }. The state is
|
||||
// never literally "attention", so a clear that checks state === "attention"
|
||||
// would never fire — the bug this reproduces.
|
||||
await manager.setTerminalActivity(session.id, "working");
|
||||
await manager.setTerminalActivity(session.id, "idle");
|
||||
await waitForCondition(
|
||||
() => manager?.getTerminal(session.id)?.getActivity()?.attentionReason === "finished",
|
||||
5000,
|
||||
);
|
||||
|
||||
const cleared = await manager.clearTerminalAttention(session.id);
|
||||
|
||||
expect(cleared).toBe(true);
|
||||
await waitForCondition(
|
||||
() => manager?.getTerminal(session.id)?.getActivity()?.attentionReason == null,
|
||||
5000,
|
||||
);
|
||||
expect(manager.getTerminal(session.id)?.getActivity()).toEqual({
|
||||
state: "idle",
|
||||
changedAt: expect.any(Number),
|
||||
});
|
||||
});
|
||||
|
||||
it("removes worker terminals after killAndWait", async () => {
|
||||
const cwd = mkdtempSync(join(tmpdir(), "worker-terminal-manager-kill-"));
|
||||
temporaryDirs.push(cwd);
|
||||
|
||||
@@ -294,11 +294,11 @@ export function createWorkerTerminalManager(
|
||||
sendBestEffortRequest({ type: "setActivity", terminalId: record.info.id, state });
|
||||
},
|
||||
clearActivityAttention(): boolean {
|
||||
if (record.activity?.state !== "attention") {
|
||||
if (record.activity?.attentionReason == null) {
|
||||
return false;
|
||||
}
|
||||
record.activity = { state: "idle", changedAt: Date.now() };
|
||||
sendBestEffortRequest({ type: "setActivity", terminalId: record.info.id, state: "idle" });
|
||||
record.activity = { state: record.activity.state, changedAt: Date.now() };
|
||||
sendBestEffortRequest({ type: "clearAttention", terminalId: record.info.id });
|
||||
return true;
|
||||
},
|
||||
getSize(): { rows: number; cols: number } {
|
||||
@@ -753,10 +753,10 @@ export function createWorkerTerminalManager(
|
||||
|
||||
async clearTerminalAttention(id: string): Promise<boolean> {
|
||||
const record = recordsById.get(id);
|
||||
if (!record || record.activity?.state !== "attention") {
|
||||
if (!record || record.activity?.attentionReason == null) {
|
||||
return false;
|
||||
}
|
||||
await sendRequest({ type: "setActivity", terminalId: id, state: "idle" });
|
||||
await sendRequest({ type: "clearAttention", terminalId: id });
|
||||
return true;
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user