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
@@ -18,6 +18,31 @@ describe("TerminalActivityTracker — set", () => {
expect(tracker.getSnapshot().state).toBe("working");
});
it("marks a working to idle transition as finished attention", () => {
const tracker = new TerminalActivityTracker();
tracker.set("working");
tracker.set("idle");
expect(tracker.getSnapshot()).toMatchObject({
state: "idle",
attentionReason: "finished",
});
});
it("keeps finished attention across repeated idle reports", () => {
const tracker = new TerminalActivityTracker();
tracker.set("working");
tracker.set("idle");
tracker.set("idle");
expect(tracker.getSnapshot()).toMatchObject({
state: "idle",
attentionReason: "finished",
});
});
});
describe("TerminalActivityTracker — clearAttention", () => {
@@ -27,7 +52,7 @@ describe("TerminalActivityTracker — clearAttention", () => {
tracker.set("attention");
expect(tracker.clearAttention()).toBe(true);
expect(tracker.getSnapshot().state).toBe("idle");
expect(tracker.getSnapshot()).toMatchObject({ state: "idle", attentionReason: null });
});
it("leaves non-attention states unchanged", () => {
@@ -102,7 +127,10 @@ describe("TerminalActivityTracker — onChange listener", () => {
expect(transitions[0].previous.state).toBeNull();
expect(transitions[0].snapshot.state).toBe("working");
expect(transitions[1].previous).toEqual(transitions[0].snapshot);
expect(transitions[1].snapshot.state).toBe("idle");
expect(transitions[1].snapshot).toMatchObject({
state: "idle",
attentionReason: "finished",
});
});
});
@@ -1,13 +1,18 @@
import type { TerminalActivityState } from "@getpaseo/protocol/terminal-activity";
import type {
TerminalActivityAttentionReason,
TerminalActivityState,
} from "@getpaseo/protocol/terminal-activity";
export interface TerminalActivitySnapshot {
state: TerminalActivityState | null;
attentionReason: TerminalActivityAttentionReason | null;
changedAt: number;
}
export class TerminalActivityTracker {
// unknown != idle: a plain shell, or a terminal whose agent was killed, has no dot or rollup.
private resolvedState: TerminalActivityState | null = null;
private attentionReason: TerminalActivityAttentionReason | null = null;
private changedAt = Date.now();
private readonly changeListeners = new Set<
@@ -15,28 +20,44 @@ export class TerminalActivityTracker {
>();
set(state: TerminalActivityState): void {
this.setState(state);
if (state === "idle" && this.resolvedState === "working") {
this.setState("idle", "finished");
return;
}
if (state === "idle" && this.attentionReason === "finished") {
return;
}
this.setState(
state === "attention" ? "idle" : state,
state === "attention" ? "needs_input" : null,
);
}
clear(): void {
this.setState(null);
this.setState(null, null);
}
clearAttention(): boolean {
if (this.resolvedState !== "attention") {
if (!this.attentionReason) {
return false;
}
this.setState("idle");
this.setState("idle", null);
return true;
}
private setState(state: TerminalActivityState | null): void {
if (state === this.resolvedState) {
private setState(
state: TerminalActivityState | null,
attentionReason: TerminalActivityAttentionReason | null,
): void {
if (state === this.resolvedState && attentionReason === this.attentionReason) {
return;
}
const previous = this.getSnapshot();
this.resolvedState = state;
this.attentionReason = attentionReason;
this.changedAt = Date.now();
const snapshot = this.getSnapshot();
@@ -57,6 +78,7 @@ export class TerminalActivityTracker {
getSnapshot(): TerminalActivitySnapshot {
return {
state: this.resolvedState,
attentionReason: this.attentionReason,
changedAt: this.changedAt,
};
}
+6 -1
View File
@@ -132,12 +132,17 @@ export interface CreateTerminalOptions {
function toTerminalActivity(snapshot: {
state: TerminalActivityState | null;
attentionReason?: TerminalActivity["attentionReason"];
changedAt: number;
}): TerminalActivity | null {
if (!snapshot.state) {
return null;
}
return { state: snapshot.state, changedAt: snapshot.changedAt };
return {
state: snapshot.state,
...(snapshot.attentionReason ? { attentionReason: snapshot.attentionReason } : {}),
changedAt: snapshot.changedAt,
};
}
function resolveInitialTitleMode(presetTitle: string | undefined): "auto" | "manual" {