Skip to content

Commit 4393281

Browse files
Copilotsawka
authored andcommitted
Show Claude icon in terminal header while Claude Code is active (wavetermdev#3046)
This updates the terminal shell-integration badge so it reflects Claude Code activity instead of always rendering the generic AI icon. When the active shell command is Claude Code, the header now shows the Claude logo. - **Terminal shell-integration badge** - Updated `getShellIntegrationIconButton()` to render the Claude logo while Claude Code is the active running command. - Kept the existing shell-integration states and messaging intact for non-Claude commands. - **Claude Code detection** - Added command detection for Claude Code in the OSC shell-integration flow. - Tracks active Claude sessions on `TermWrap`, including initial runtime-info hydration and command lifecycle transitions. - Handles common invocation forms, including direct binary paths and commands wrapped by env var assignments / `env`. - **UI rendering** - Added `@lobehub/icons` and used its `Claude` icon in the terminal header path. - Reused the existing icon-button rendering contract by passing a React node for the icon where needed. - **Focused coverage** - Added a small unit test for Claude command detection to lock in the supported command forms. ```ts const claudeCodeActive = get(this.termRef.current.claudeCodeActiveAtom); const icon = claudeCodeActive ? React.createElement(TermClaudeIcon) : "sparkles"; ``` - **screenshot** - ![Claude terminal header badge](https://github.com/user-attachments/assets/4b53f671-8432-4878-b2d2-e3afeba7814f) <!-- START COPILOT CODING AGENT TIPS --> --- 💬 Send tasks to Copilot coding agent from [Slack](https://gh.io/cca-slack-docs) and [Teams](https://gh.io/cca-teams-docs) to turn conversations into code. Copilot posts an update in your thread when it's finished. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: sawka <2722291+sawka@users.noreply.github.com> Co-authored-by: sawka <mike@commandline.dev>
1 parent bb46dcd commit 4393281

5 files changed

Lines changed: 57 additions & 25 deletions

File tree

Lines changed: 1 addition & 0 deletions
Loading

frontend/app/view/term/term-model.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { waveEventSubscribe } from "@/app/store/wps";
1010
import { RpcApi } from "@/app/store/wshclientapi";
1111
import { makeFeBlockRouteId } from "@/app/store/wshrouter";
1212
import { DefaultRouter, TabRpcClient } from "@/app/store/wshrpcutil";
13-
import { TerminalView } from "@/app/view/term/term";
13+
import { TermClaudeIcon, TerminalView } from "@/app/view/term/term";
1414
import { TermWshClient } from "@/app/view/term/term-wsh";
1515
import { WorkspaceLayoutModel } from "@/app/workspace/workspace-layout-model";
1616
import {
@@ -422,10 +422,12 @@ export class TermViewModel implements ViewModel {
422422
return null;
423423
}
424424
const shellIntegrationStatus = get(this.termRef.current.shellIntegrationStatusAtom);
425+
const claudeCodeActive = get(this.termRef.current.claudeCodeActiveAtom);
426+
const icon = claudeCodeActive ? React.createElement(TermClaudeIcon) : "sparkles";
425427
if (shellIntegrationStatus == null) {
426428
return {
427429
elemtype: "iconbutton",
428-
icon: "sparkles",
430+
icon,
429431
className: "text-muted",
430432
title: "No shell integration — Wave AI unable to run commands.",
431433
noAction: true,
@@ -434,14 +436,16 @@ export class TermViewModel implements ViewModel {
434436
if (shellIntegrationStatus === "ready") {
435437
return {
436438
elemtype: "iconbutton",
437-
icon: "sparkles",
439+
icon,
438440
className: "text-accent",
439441
title: "Shell ready — Wave AI can run commands in this terminal.",
440442
noAction: true,
441443
};
442444
}
443445
if (shellIntegrationStatus === "running-command") {
444-
let title = "Shell busy — Wave AI unable to run commands while another command is running.";
446+
let title = claudeCodeActive
447+
? "Claude Code Detected"
448+
: "Shell busy — Wave AI unable to run commands while another command is running.";
445449

446450
if (this.termRef.current) {
447451
const inAltBuffer = this.termRef.current.terminal?.buffer?.active?.type === "alternate";
@@ -454,7 +458,7 @@ export class TermViewModel implements ViewModel {
454458

455459
return {
456460
elemtype: "iconbutton",
457-
icon: "sparkles",
461+
icon,
458462
className: "text-warning",
459463
title: title,
460464
noAction: true,

frontend/app/view/term/term.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import ClaudeColorSvg from "@/app/asset/claude-color.svg";
12
import { Search, useSearch } from "@/app/element/search";
23
import { ContextMenuModel } from "@/app/store/contextmenu";
34
import { useTabModel } from "@/app/store/tab-model";
@@ -23,6 +24,16 @@ interface TerminalViewProps {
2324
model: TermViewModel;
2425
}
2526

27+
const TermClaudeIcon = React.memo(() => {
28+
return (
29+
<div className="[&_svg]:w-[15px] [&_svg]:h-[15px]" aria-hidden="true">
30+
<ClaudeColorSvg />
31+
</div>
32+
);
33+
});
34+
35+
TermClaudeIcon.displayName = "TermClaudeIcon";
36+
2637
const TermResyncHandler = React.memo(({ model }: TerminalViewProps) => {
2738
const connStatus = jotai.useAtomValue(model.connStatus);
2839
const [lastConnStatus, setLastConnStatus] = React.useState<ConnStatus>(connStatus);
@@ -561,4 +572,4 @@ const TerminalView = ({ blockId, model }: ViewComponentProps<TermViewModel>) =>
561572
);
562573
};
563574

564-
export { TerminalView };
575+
export { TermClaudeIcon, TerminalView };

frontend/app/view/term/termwrap.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import {
4343
handleOsc16162Command,
4444
handleOsc52Command,
4545
handleOsc7Command,
46+
isClaudeCodeCommand,
4647
} from "./termwrap-osc";
4748

4849
export { cleanupOsc7DebounceForTab };
@@ -96,6 +97,7 @@ export class TermWrap {
9697
promptMarkers: TermTypes.IMarker[] = [];
9798
shellIntegrationStatusAtom: jotai.PrimitiveAtom<"ready" | "running-command" | null>;
9899
lastCommandAtom: jotai.PrimitiveAtom<string | null>;
100+
claudeCodeActiveAtom: jotai.PrimitiveAtom<boolean>;
99101
nodeModel: BlockNodeModel;
100102
onShellIntegrationStatusChange?: () => void;
101103
pendingTermSize: TermSize | null = null;
@@ -134,6 +136,7 @@ export class TermWrap {
134136
this.promptMarkers = [];
135137
this.shellIntegrationStatusAtom = jotai.atom(null) as jotai.PrimitiveAtom<"ready" | "running-command" | null>;
136138
this.lastCommandAtom = jotai.atom(null) as jotai.PrimitiveAtom<string | null>;
139+
this.claudeCodeActiveAtom = jotai.atom(false);
137140
this.terminal = new Terminal(options);
138141
this.fitAddon = new FitAddon();
139142
this.fitAddon.noScrollbar = PLATFORM === PlatformMacOS;
@@ -197,10 +200,20 @@ export class TermWrap {
197200
return result;
198201
});
199202
this.terminal.parser.registerOscHandler(52, (data: string) => {
200-
return handleOsc52Command(data, this.blockId, this.loaded, this);
203+
try {
204+
return handleOsc52Command(data, this.blockId, this.loaded, this);
205+
} catch (e) {
206+
console.error("[termwrap] osc 52 handler error", this.blockId, e);
207+
return false;
208+
}
201209
});
202210
this.terminal.parser.registerOscHandler(16162, (data: string) => {
203-
return handleOsc16162Command(data, this.blockId, this.loaded, this);
211+
try {
212+
return handleOsc16162Command(data, this.blockId, this.loaded, this);
213+
} catch (e) {
214+
console.error("[termwrap] osc 16162 handler error", this.blockId, e);
215+
return false;
216+
}
204217
});
205218
let lastTitle: string | null = null;
206219
this.terminal.onTitleChange((title: string) => {
@@ -413,16 +426,19 @@ export class TermWrap {
413426
const rtInfo = await RpcApi.GetRTInfoCommand(TabRpcClient, {
414427
oref: WOS.makeORef("block", this.blockId),
415428
});
429+
let shellState: ShellIntegrationStatus = null;
416430

417431
if (rtInfo && rtInfo["shell:integration"]) {
418-
const shellState = rtInfo["shell:state"] as ShellIntegrationStatus;
432+
shellState = rtInfo["shell:state"] as ShellIntegrationStatus;
419433
this.setShellIntegrationStatus(shellState || null);
420434
} else {
421435
this.setShellIntegrationStatus(null);
422436
}
423437

424438
const lastCmd = rtInfo ? rtInfo["shell:lastcmd"] : null;
439+
const isCC = shellState === "running-command" && isClaudeCodeCommand(lastCmd);
425440
globalStore.set(this.lastCommandAtom, lastCmd || null);
441+
globalStore.set(this.claudeCodeActiveAtom, isCC);
426442
} catch (e) {
427443
console.log("Error loading runtime info:", e);
428444
}

package-lock.json

Lines changed: 16 additions & 16 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)