Skip to content
Merged
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
6 changes: 5 additions & 1 deletion src/components/sidebar/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 (
Expand Down
8 changes: 7 additions & 1 deletion src/components/task/TabBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
<span className={cn("min-w-0 flex-1 truncate", tab.preview && "italic")} title={tab.liveTitle && !tab.customTitle ? tab.liveTitle : undefined}>
{tab.customTitle ? tab.title : (tab.liveTitle || tab.title)}
{visibleTitle}
</span>
)}
{/* Run tabs (GH #54): inline run controls, always visible — the pill IS
Expand Down
39 changes: 39 additions & 0 deletions src/lib/terminalTitle.test.ts
Original file line number Diff line number Diff line change
@@ -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");
});
});
21 changes: 21 additions & 0 deletions src/lib/terminalTitle.ts
Original file line number Diff line number Diff line change
@@ -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*/, "");
}
Loading