Skip to content
Open
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
1 change: 1 addition & 0 deletions packages/coding-agent/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Added

- Added the GLM 5.2 prompt preset with automatic model detection.
- Added a working/idle agent status to the terminal title so a busy session is visible without focusing it. The title is prefixed with `[working]` while the agent runs (and while compacting) and returns to the plain title when idle. Terminals that take their tab/window title from the OSC title show this directly; Zed shows it in the terminal breadcrumbs, since Zed builds its tab label from the foreground process instead.

### Fixed

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* Working/idle activity status surfaced to the host terminal via the OSC title.
*
* The status is a fixed, event-driven token rather than an animated spinner:
* spinners either spam the title stream or render as bells under screen/tmux
* (openai/codex#17198).
*
* Reach, measured rather than assumed:
* - Terminals that take their tab/window title from the OSC title show the
* token directly.
* - Zed routes the OSC title to the terminal *breadcrumbs*; its tab label is
* built from the foreground process instead, so the token reaches Zed's
* breadcrumbs but not its tab. See the Zed note in the PR/QA evidence.
*/

export type AgentActivityStatus = "working" | "idle";

const WORKING_TOKEN = "[working]";

/**
* Prefix a terminal title with the activity status token.
*
* Idle is the resting state and renders the plain title, so only actively
* working sessions stand out. The token leads the string so it survives the
* truncation tab bars apply.
*/
export function formatAgentActivityTitle(status: AgentActivityStatus, title: string): string {
if (status !== "working") {
return title;
}
if (!title) {
return WORKING_TOKEN;
}
return `${WORKING_TOKEN} ${title}`;
}
27 changes: 22 additions & 5 deletions packages/coding-agent/src/modes/interactive/interactive-mode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ import { getPiUserAgent } from "../../utils/pi-user-agent.ts";
import { killTrackedDetachedChildren } from "../../utils/shell.ts";
import { checkForNewPiVersion } from "../../utils/version-check.ts";
import { abortedErrorLabel } from "./aborted-error-label.ts";
import { type AgentActivityStatus, formatAgentActivityTitle } from "./agent-activity-status.ts";
import { ArminComponent } from "./components/armin.ts";
import { AssistantMessageComponent } from "./components/assistant-message.ts";
import { BashExecutionComponent } from "./components/bash-execution.ts";
Expand Down Expand Up @@ -378,6 +379,7 @@ export class InteractiveMode {
private hookStatusIntervalId: NodeJS.Timeout | undefined = undefined;
private activeToolTerminalTitle: string | undefined = undefined;
private extensionTerminalTitle: string | undefined = undefined;
private agentActivityStatus: AgentActivityStatus = "idle";

private lastSigintTime = 0;
private lastEscapeTime = 0;
Expand Down Expand Up @@ -856,12 +858,20 @@ export class InteractiveMode {
}

private applyTerminalTitle(): void {
this.ui.terminal.setTitle(
const title =
this.activeToolTerminalTitle ??
this.activeToolExecutionTerminalTitle ??
this.extensionTerminalTitle ??
this.getNormalTerminalTitle(),
);
this.activeToolExecutionTerminalTitle ??
this.extensionTerminalTitle ??
this.getNormalTerminalTitle();
this.ui.terminal.setTitle(formatAgentActivityTitle(this.agentActivityStatus, title));
}

private setAgentActivityStatus(status: AgentActivityStatus): void {
if (this.agentActivityStatus === status) {
return;
}
this.agentActivityStatus = status;
this.applyTerminalTitle();
}

private updateTerminalTitle(): void {
Expand Down Expand Up @@ -3071,6 +3081,7 @@ export class InteractiveMode {
this.clearPendingTools();
this.clearActiveToolExecutionStatus();
this.clearToolHookStatuses();
this.setAgentActivityStatus("working");
if (this.settingsManager.getShowTerminalProgress()) {
this.ui.terminal.setProgress(true);
}
Expand Down Expand Up @@ -3262,6 +3273,7 @@ export class InteractiveMode {
}

case "agent_end":
this.setAgentActivityStatus("idle");
if (this.settingsManager.getShowTerminalProgress()) {
this.ui.terminal.setProgress(false);
}
Expand All @@ -3281,6 +3293,7 @@ export class InteractiveMode {
break;

case "compaction_start": {
this.setAgentActivityStatus("working");
if (this.settingsManager.getShowTerminalProgress()) {
this.ui.terminal.setProgress(true);
}
Expand Down Expand Up @@ -3327,6 +3340,9 @@ export class InteractiveMode {
}

case "compaction_end": {
if (!this.session.isStreaming) {
this.setAgentActivityStatus("idle");
}
if (this.settingsManager.getShowTerminalProgress()) {
this.ui.terminal.setProgress(false);
}
Expand Down Expand Up @@ -6119,6 +6135,7 @@ export class InteractiveMode {
}

stop(): void {
this.setAgentActivityStatus("idle");
if (this.settingsManager.getShowTerminalProgress()) {
this.ui.terminal.setProgress(false);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ describe("InteractiveMode compaction events", () => {
autoCompactionEscapeHandler: undefined as (() => void) | undefined,
autoCompactionLoader: undefined as { stop(): void } | undefined,
defaultEditor: {} as { onEscape?: () => void },
session: { abortCompaction: vi.fn() },
session: { abortCompaction: vi.fn(), isStreaming: false },
setAgentActivityStatus: vi.fn(),
statusContainer,
settingsManager: { getShowTerminalProgress: () => false },
ui: { requestRender: vi.fn(), terminal: { setProgress: vi.fn() } },
Expand Down Expand Up @@ -56,7 +57,8 @@ describe("InteractiveMode compaction events", () => {
autoCompactionLoader: undefined as { stop(): void } | undefined,
autoCompactionProgressText: "",
defaultEditor: {} as { onEscape?: () => void },
session: { abortCompaction: vi.fn() },
session: { abortCompaction: vi.fn(), isStreaming: false },
setAgentActivityStatus: vi.fn(),
statusContainer,
settingsManager: { getShowTerminalProgress: () => false },
ui: { requestRender: vi.fn(), terminal: { setProgress: vi.fn() } },
Expand Down Expand Up @@ -107,6 +109,8 @@ describe("InteractiveMode compaction events", () => {
showError: vi.fn(),
showStatus: vi.fn(),
flushCompactionQueue: vi.fn().mockResolvedValue(undefined),
session: { isStreaming: false },
setAgentActivityStatus: vi.fn(),
settingsManager: { getShowTerminalProgress: () => false },
ui: { requestRender: vi.fn(), terminal: { setProgress: vi.fn() } },
};
Expand Down
Loading