diff --git a/src/components/sidebar/Sidebar.tsx b/src/components/sidebar/Sidebar.tsx index cd3b2e5..59f0bf0 100644 --- a/src/components/sidebar/Sidebar.tsx +++ b/src/components/sidebar/Sidebar.tsx @@ -14,6 +14,7 @@ import { UpdateCard } from "./UpdateCard"; import { CliIcon, CLI_BRAND_COLOR, resolveIconId } from "@/icons/cli"; import { useUI } from "@/store/ui"; import { cn } from "@/lib/utils"; +import { formatTerminalTitle } from "@/lib/terminalTitle"; import { requestCloseTab } from "@/lib/closeTab"; import { taskRename, projectRename, openPath, projectReorder, taskSetYolo, projectRemove, projectUpdate } from "@/lib/ipc"; import { createQuickTask, derivedBranch, type NewTaskMode } from "@/lib/quickTask"; @@ -1489,10 +1490,13 @@ function TaskRow({ w, compact }: { w: Task; compact: boolean }) { {!collapsed && terminalTabs.map(tab => { const isTabActive = isActive && tab.id === activeTabId; const isTabHot = isTabActive; - const title = tab.customTitle ? tab.title : (tab.liveTitle || tab.title); const showBell = settledHighlight && tab.unread?.reason === "attention"; const showDone = settledHighlight && !showBell && tab.workState === "done"; const showWorking = workingIndicator && !showBell && !showDone && tab.workState === "working"; + const rawTitle = tab.customTitle ? tab.title : (tab.liveTitle || tab.title); + const title = tab.customTitle + ? rawTitle + : formatTerminalTitle(rawTitle, tab.cli, showWorking); const isTabRenaming = tabRenaming?.id === tab.id; return ( diff --git a/src/components/task/TabBar.tsx b/src/components/task/TabBar.tsx index e8277ff..dd9ac67 100644 --- a/src/components/task/TabBar.tsx +++ b/src/components/task/TabBar.tsx @@ -17,6 +17,7 @@ import { requestCloseTab } from "@/lib/closeTab"; import { focusMainTab } from "@/lib/tabFocus"; import { visibleCliIds, agentDisplayName, isTerminalEntry } from "@/lib/agents"; import { cn } from "@/lib/utils"; +import { formatTerminalTitle } from "@/lib/terminalTitle"; import { fileIconUrl } from "@/lib/explorer/iconResolver"; const CLIS = ["claude", "codex", "agy", "grok", "opencode"] as const; @@ -303,6 +304,11 @@ export function TabPill({ task, tab, active, paneFocused, compact, onSelect, onC const iconId = tab.type === "terminal" ? resolveIconId(tab.cli, agents) : ""; const color = tab.type === "terminal" ? CLI_BRAND_COLOR[iconId] : "text-[var(--color-fg-dim)]"; const isRenaming = renaming !== null; + const rawTitle = tab.customTitle ? tab.title : (tab.liveTitle || tab.title); + const visibleTitle = + tab.type === "terminal" && !tab.customTitle + ? formatTerminalTitle(rawTitle, tab.cli, showWorking) + : rawTitle; // Reveal the pill when it becomes active — keyboard tab switches (⇧⌘[/], // ⌘1..9, cross-pane cycling) can land on a tab scrolled out of the strip's @@ -416,7 +422,7 @@ export function TabPill({ task, tab, active, paneFocused, compact, onSelect, onC // width and pushes the pill larger, defeating the fixed-cell // layout. Title attr surfaces the full text on hover. - {tab.customTitle ? tab.title : (tab.liveTitle || tab.title)} + {visibleTitle} )} {/* Run tabs (GH #54): inline run controls, always visible — the pill IS diff --git a/src/lib/terminalTitle.test.ts b/src/lib/terminalTitle.test.ts new file mode 100644 index 0000000..7b9fc40 --- /dev/null +++ b/src/lib/terminalTitle.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, it } from "vitest"; + +import { formatTerminalTitle } from "./terminalTitle"; + +describe("formatTerminalTitle", () => { + it("removes Claude's idle brand glyph when hiding is enabled", () => { + expect(formatTerminalTitle("✳ Task name", "claude", true)).toBe( + "Task name", + ); + }); + + it("removes one Claude Braille spinner glyph", () => { + expect(formatTerminalTitle("⠋ Task name", "claude", true)).toBe( + "Task name", + ); + }); + + it("removes multiple Claude Braille spinner glyphs", () => { + expect(formatTerminalTitle("⠐ ⠂ Task name", "claude", true)).toBe( + "Task name", + ); + }); + + it("keeps the raw Claude title when hiding is disabled", () => { + expect(formatTerminalTitle("⠋ Task name", "claude", false)).toBe( + "⠋ Task name", + ); + }); + + it("does not modify other CLI titles", () => { + expect(formatTerminalTitle("⠋ Task name", "codex", true)).toBe( + "⠋ Task name", + ); + }); + + it("does not modify ordinary Claude titles", () => { + expect(formatTerminalTitle("Task name", "claude", true)).toBe("Task name"); + }); +}); diff --git a/src/lib/terminalTitle.ts b/src/lib/terminalTitle.ts new file mode 100644 index 0000000..8455f12 --- /dev/null +++ b/src/lib/terminalTitle.ts @@ -0,0 +1,21 @@ +/** + * Remove Claude Code's leading status glyphs from a live terminal title. + * + * Claude prefixes idle titles with ✳ and working titles with one or more + * Braille spinner glyphs. We only hide those prefixes when Termic is already + * showing its own working indicator, so users with the indicator disabled + * still retain Claude's built-in state signal. + */ +export function formatTerminalTitle( + title: string, + cli: string, + hideClaudeStatusGlyph: boolean, +): string { + if (cli !== "claude" || !hideClaudeStatusGlyph) { + return title; + } + + return title + .replace(/^\s*✳\s*/, "") + .replace(/^\s*[\u2800-\u28ff](?:\s+[\u2800-\u28ff])*\s*/, ""); +}