From 88b8565ae12f40600ec807cce1d45b2e3f9fb67b Mon Sep 17 00:00:00 2001 From: F0Rextasy Date: Mon, 21 Sep 2026 01:45:43 +0300 Subject: [PATCH 1/2] feat(tui): include project directory name in terminal title Fixes #12600 --- .../coding-agent/src/utils/title-generator.ts | 19 ++++++- .../test/terminal-title-state.test.ts | 57 +++++++++++++++++++ 2 files changed, 74 insertions(+), 2 deletions(-) diff --git a/packages/coding-agent/src/utils/title-generator.ts b/packages/coding-agent/src/utils/title-generator.ts index 4194744cf97..c1489e195ea 100644 --- a/packages/coding-agent/src/utils/title-generator.ts +++ b/packages/coding-agent/src/utils/title-generator.ts @@ -571,9 +571,20 @@ function getFallbackTerminalTitle(cwd: string | undefined): string | undefined { return sanitizeTerminalTitlePart(baseName); } +function getProjectTerminalTitleSuffix(cwd: string | undefined): string | undefined { + // Kept separate from the no-session fallback above: when a session title + // exists the project directory is appended (`label - project`) so tabs on + // different repositories stay identifiable (issue #12600). + return getFallbackTerminalTitle(cwd); +} + export function formatSessionTerminalTitle(sessionName: string | undefined, cwd?: string): string { const label = sanitizeTerminalTitlePart(sessionName) ?? getFallbackTerminalTitle(cwd); - return label ? `${DEFAULT_TERMINAL_TITLE}: ${label}` : DEFAULT_TERMINAL_TITLE; + const project = label !== undefined && sanitizeTerminalTitlePart(sessionName) !== undefined + ? getProjectTerminalTitleSuffix(cwd) + : undefined; + const fullLabel = project !== undefined && project !== label ? `${label} - ${project}` : label; + return fullLabel ? `${DEFAULT_TERMINAL_TITLE}: ${fullLabel}` : DEFAULT_TERMINAL_TITLE; } /** @@ -653,7 +664,11 @@ export function setSessionTerminalTitle(sessionName: string | undefined, cwd?: s // write into the parent shell's tab. Only `initTerminalTitleState()`, the // explicit terminal-ownership path, releases the latch. terminalTitleRuntime.extensionOverride = undefined; - terminalTitleRuntime.label = sanitizeTerminalTitlePart(sessionName) ?? getFallbackTerminalTitle(cwd); + const sessionLabel = sanitizeTerminalTitlePart(sessionName); + const project = sessionLabel !== undefined ? getProjectTerminalTitleSuffix(cwd) : undefined; + terminalTitleRuntime.label = sessionLabel !== undefined && project !== undefined && project !== sessionLabel + ? `${sessionLabel} - ${project}` + : (sessionLabel ?? getFallbackTerminalTitle(cwd)); emitTerminalTitle(); } diff --git a/packages/coding-agent/test/terminal-title-state.test.ts b/packages/coding-agent/test/terminal-title-state.test.ts index f8f67388d85..2b4e1f7d8eb 100644 --- a/packages/coding-agent/test/terminal-title-state.test.ts +++ b/packages/coding-agent/test/terminal-title-state.test.ts @@ -90,6 +90,63 @@ describe("buildTerminalTitleWithState", () => { expect(buildTerminalTitleWithState(undefined, "working", 1, true, "linux", "line", wslEnv)).toBe(`${BRAND} :`); }); }); +describe("setSessionTerminalTitle project suffix (issue #12600)", () => { + let writes: string[] = []; + let stdoutSpy: { mockRestore(): void } | undefined; + let prevHeadless = false; + let ttyDescriptor: PropertyDescriptor | undefined; + let windowsTitleMock: WindowsConsoleTitleMock | undefined; + + beforeEach(() => { + prevHeadless = setTerminalHeadless(false); + ttyDescriptor = Object.getOwnPropertyDescriptor(process.stdout, "isTTY"); + Object.defineProperty(process.stdout, "isTTY", { value: true, configurable: true }); + windowsTitleMock = mockWindowsConsoleTitle(); + windowsTitleMock.succeeds = true; + writes = []; + stdoutSpy = spyOn(process.stdout, "write").mockImplementation((chunk: unknown) => { + writes.push(typeof chunk === "string" ? chunk : new TextDecoder().decode(chunk as Uint8Array)); + return true; + }); + initTerminalTitleState(); + resetObserved(writes, windowsTitleMock); + }); + + afterEach(() => { + disposeTerminalTitleState(); + stdoutSpy?.mockRestore(); + stdoutSpy = undefined; + windowsTitleMock?.restore(); + windowsTitleMock = undefined; + if (ttyDescriptor) Object.defineProperty(process.stdout, "isTTY", ttyDescriptor); + else Reflect.deleteProperty(process.stdout, "isTTY"); + setTerminalHeadless(prevHeadless); + }); + it("appends the project directory to the session title", () => { + setTerminalTitleState("idle"); + resetObserved(writes, windowsTitleMock); + setSessionTerminalTitle("Fix authentication flow", "/repo/oh-my-pi"); + const titles = observedTitles(writes, windowsTitleMock); + expect(titles[titles.length - 1]).toBe(`${BRAND} > Fix authentication flow - oh-my-pi`); + }); + + it("omits the suffix when the session title already is the project name", () => { + setTerminalTitleState("idle"); + resetObserved(writes, windowsTitleMock); + setSessionTerminalTitle("oh-my-pi", "/repo/oh-my-pi"); + const titles = observedTitles(writes, windowsTitleMock); + expect(titles[titles.length - 1]).toBe(`${BRAND} > oh-my-pi`); + }); + + it("keeps the no-session fallback as the bare project name", () => { + setSessionTerminalTitle("Temporary probe title", "/repo/oh-my-pi"); + setTerminalTitleState("idle"); + resetObserved(writes, windowsTitleMock); + setSessionTerminalTitle(undefined, "/repo/other-project"); + const titles = observedTitles(writes, windowsTitleMock); + expect(titles[titles.length - 1]).toBe(`${BRAND} > other-project`); + }); +}); // Regression coverage for the shutdown-leak bug (PR #4451): the run-state // `working` spinner arms a periodic `setInterval` that, on every tick, re-emits From c364eb53dc47f7c117baf25802edcf032174e218 Mon Sep 17 00:00:00 2001 From: F0Rextasy Date: Mon, 21 Sep 2026 01:46:39 +0300 Subject: [PATCH 2/2] style(tui): apply oxfmt to terminal title project suffix --- packages/coding-agent/src/utils/title-generator.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/coding-agent/src/utils/title-generator.ts b/packages/coding-agent/src/utils/title-generator.ts index c1489e195ea..d40a660d75e 100644 --- a/packages/coding-agent/src/utils/title-generator.ts +++ b/packages/coding-agent/src/utils/title-generator.ts @@ -580,9 +580,10 @@ function getProjectTerminalTitleSuffix(cwd: string | undefined): string | undefi export function formatSessionTerminalTitle(sessionName: string | undefined, cwd?: string): string { const label = sanitizeTerminalTitlePart(sessionName) ?? getFallbackTerminalTitle(cwd); - const project = label !== undefined && sanitizeTerminalTitlePart(sessionName) !== undefined - ? getProjectTerminalTitleSuffix(cwd) - : undefined; + const project = + label !== undefined && sanitizeTerminalTitlePart(sessionName) !== undefined + ? getProjectTerminalTitleSuffix(cwd) + : undefined; const fullLabel = project !== undefined && project !== label ? `${label} - ${project}` : label; return fullLabel ? `${DEFAULT_TERMINAL_TITLE}: ${fullLabel}` : DEFAULT_TERMINAL_TITLE; } @@ -666,9 +667,10 @@ export function setSessionTerminalTitle(sessionName: string | undefined, cwd?: s terminalTitleRuntime.extensionOverride = undefined; const sessionLabel = sanitizeTerminalTitlePart(sessionName); const project = sessionLabel !== undefined ? getProjectTerminalTitleSuffix(cwd) : undefined; - terminalTitleRuntime.label = sessionLabel !== undefined && project !== undefined && project !== sessionLabel - ? `${sessionLabel} - ${project}` - : (sessionLabel ?? getFallbackTerminalTitle(cwd)); + terminalTitleRuntime.label = + sessionLabel !== undefined && project !== undefined && project !== sessionLabel + ? `${sessionLabel} - ${project}` + : (sessionLabel ?? getFallbackTerminalTitle(cwd)); emitTerminalTitle(); }