Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
131 changes: 131 additions & 0 deletions docs/docs/claude-code.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
---
sidebar_position: 1.9
id: "claude-code"
title: "Claude Code Integration"
---

import { VersionBadge } from "@site/src/components/versionbadge";

# Claude Code Tab Badges <VersionBadge version="v0.14.2" />

When you run multiple Claude Code sessions in parallel — one per feature, one per repo, a few long-running tasks — it gets hard to know which tabs need your attention without clicking through each one. Wave's badge system solves this: hooks in Claude Code write a small visual indicator to the tab header whenever something important happens, so you can see at a glance which sessions are waiting, done, or in trouble.

:::info
tl;dr You can copy and paste this page directly into Claude Code and it will help you set everything up!
:::

## How it works

Claude Code supports [lifecycle hooks](https://code.claude.com/docs/en/hooks) — shell commands that run automatically at specific points in a session. Wave's `wsh badge` command sets or clears a visual indicator on the current block or tab. By wiring these together, you get ambient awareness across all your sessions without watching any of them.

Badges auto-clear when you focus the block, so they're purely a "hey, look over here" signal. Once you click in and read what's happening, the badge disappears on its own.

Wave already shows a bell icon when a terminal outputs a BEL character. These hooks complement that with semantic badges — *permission needed*, *done* — that survive across tab switches and work across splits.

### Badge rollup

If a tab has multiple terminals (block), Wave shows the highest-priority badge on the tab header. Ties at the same priority go to the earliest badge set, so the most urgent signal from any pane in the tab floats to the top.

## Setup

These hooks go in your global Claude Code settings so they apply to every session on your machine, not just one project.

Add the following to `~/.claude/settings.json`. If you already have a `hooks` key, merge the entries in:

```json
{
"hooks": {
"Notification": [
{
"matcher": "permission_prompt",
"hooks": [
{
"type": "command",
"command": "wsh badge bell-exclamation --color '#e0b956' --priority 20 --beep"
}
]
},
{
"matcher": "elicitation_dialog",
"hooks": [
{
"type": "command",
"command": "wsh badge message-question --color '#e0b956' --priority 20 --beep"
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "wsh badge check --color '#58c142' --priority 10"
}
]
}
],
"PreToolUse": [
{
"matcher": "AskUserQuestion",
"hooks": [
{
"type": "command",
"command": "wsh badge message-question --color '#e0b965' --priority 20 --beep"
}
]
}
]
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}
```

That's it. Restart any running Claude Code sessions for the hooks to take effect.

:::warning Known Issue
There is a known issue in Claude Code where `Notification` hooks may be delayed by several seconds before firing. This delay is unrelated to Wave — it occurs in Claude Code itself. See [#5186](https://github.com/anthropics/claude-code/issues/5186) and [#19627](https://github.com/anthropics/claude-code/issues/19627) for details.
:::

## What each hook does

### Permission prompt — `bell-exclamation` gold, priority 20

Claude Code occasionally needs your approval before it can continue — to run a command, write a file outside the project, or use a tool that requires explicit permission. When it hits one of these, it stops and waits. Without a signal, you might not notice for minutes.

This hook fires on the `permission_prompt` notification type and sets a high-priority gold badge with an audible beep. Priority 20 means it beats any other badge on that tab, so a waiting session always surfaces above a finished one.

When you click into the tab and approve or deny the request, the badge clears automatically.

### Session complete — `check` green, priority 10

When Claude Code finishes responding, this hook sets a green check badge. It's a low-key signal: glance at the tab bar, see which sessions are done, review their output in whatever order you like.

### AskUserQuestion — `message-question` gold, priority 20

When Claude Code uses the `AskUserQuestion` tool, it's paused and waiting for you to respond before it can proceed. This `PreToolUse` hook fires just before that tool call and sets the same high-priority gold badge as the permission prompt.

`PreToolUse` hooks can match any tool by name, so you can add badges for other tools as well — for example, to get a signal whenever Claude runs a shell command (`Bash`) or edits a file (`Edit`). Any tool name Claude Code supports can be used as a matcher.

## Choosing your own icons and colors

Icon names are [Font Awesome](https://fontawesome.com/icons) icon names without the `fa-` prefix. Colors are any valid CSS color — hex values, named colors, or anything else CSS accepts.

Some icon and color ideas:

| Situation | Icon | Color |
|-----------|------|-------|
| Custom high-priority alert | `triangle-exclamation` | `#FF453A` |
| Blocked / waiting on input | `hourglass-half` | `#FF9500` |
| Neutral / informational | `circle-info` | `#429DFF` |
| Background task running | `spinner` | `#00FFDB` |

See the [`wsh badge` reference](/reference/wsh-cmd/badge) for all available flags.

## Adjusting priorities

Priority controls which badge wins when multiple blocks in a tab each have one. Higher numbers take precedence. The defaults above use:

- **20** for permission prompts — always surfaces above everything else
- **10** for session complete — visible when nothing more urgent is active

If you add more hooks, keep permission-blocking signals at the high end (15–25) and informational signals at the low end (5–10).
5 changes: 3 additions & 2 deletions frontend/app/onboarding/onboarding-upgrade-v0140.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
// Copyright 2026, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0

import { getApi } from "@/app/store/global";
import { useWaveEnv } from "@/app/waveenv/waveenv";

const UpgradeOnboardingModal_v0_14_0_Content = () => {
const waveEnv = useWaveEnv();
return (
<div className="flex flex-col items-start w-full mb-2 unselectable">
<div className="text-secondary leading-relaxed mb-4">
Expand All @@ -22,7 +23,7 @@ const UpgradeOnboardingModal_v0_14_0_Content = () => {
<div className="text-foreground text-base font-semibold leading-[18px]">
Durable SSH Sessions{" "}
<button
onClick={() => getApi().openExternal("https://docs.waveterm.dev/durable-sessions")}
onClick={() => waveEnv.electron.openExternal("https://docs.waveterm.dev/durable-sessions")}
className="text-accent text-sm font-normal cursor-pointer hover:underline"
>
[see docs]
Expand Down
13 changes: 12 additions & 1 deletion frontend/app/onboarding/onboarding-upgrade-v0142.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
// Copyright 2026, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0

import { useWaveEnv } from "@/app/waveenv/waveenv";

const UpgradeOnboardingModal_v0_14_2_Content = () => {
const waveEnv = useWaveEnv();
return (
<div className="flex flex-col items-start w-full mb-2 unselectable">
<div className="text-secondary leading-relaxed mb-4">
Expand Down Expand Up @@ -36,7 +39,15 @@ const UpgradeOnboardingModal_v0_14_2_Content = () => {
</li>
<li>
<strong>Claude Code Integration</strong> - Use <code>wsh badge</code> with Claude Code
hooks to surface AI task status as tab bar notifications
hooks to surface AI task status as tab bar notifications{" "}
<button
onClick={() =>
waveEnv.electron.openExternal("https://docs.waveterm.dev/claude-code")
}
className="text-accent text-sm font-normal cursor-pointer hover:underline"
>
[see docs]
</button>
</li>
</ul>
</div>
Expand Down
11 changes: 8 additions & 3 deletions frontend/app/store/badge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,7 @@ function getTabBadgeAtom(tabId: string, env?: TabBadgesEnv): Atom<Badge[]> {
}
const tabOref = WOS.makeORef("tab", tabId);
const tabBadgeAtom = getBadgeAtom(tabOref);
const tabAtom =
env != null ? env.wos.getWaveObjectAtom<Tab>(tabOref) : WOS.getWaveObjectAtom<Tab>(tabOref);
const tabAtom = env != null ? env.wos.getWaveObjectAtom<Tab>(tabOref) : WOS.getWaveObjectAtom<Tab>(tabOref);
rtn = atom((get) => {
const tab = get(tabAtom);
const blockIds = tab?.blockids ?? [];
Expand Down Expand Up @@ -213,7 +212,13 @@ function setupBadgesSubscription() {
}
return;
}
globalStore.set(curAtom, data.clear ? null : (data.badge ?? null));
if (data.clear) {
globalStore.set(curAtom, null);
} else if (data.badge != null) {
const existing = globalStore.get(curAtom);
const candidates = existing != null ? [existing, data.badge] : [data.badge];
globalStore.set(curAtom, sortBadges(candidates)[0]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

The client subscription still forgets suppressed badges.

sortBadges(candidates)[0] only keeps the displayed badge. If a higher-priority badge temporarily overrides a lower-priority active badge on the same oref, the later clear / clearbyid event leaves the atom empty instead of restoring the still-active lower-priority badge. Mirror the backend fix here too by tracking badges per badgeid and deriving the winner from that set.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/app/store/badge.ts` around lines 215 - 220, The client currently
replaces the atom's badge with sortBadges(candidates)[0], which loses suppressed
badges; change the atom storage to maintain a map of badgeid→badge (use the same
curAtom/globalStore.get/set and sortBadges helpers) so on incoming data.badge
you set map[badge.badgeid]=badge, on clear/clearbyid you delete the badgeid
entry (or clear the map), and after each update derive and set the visible
winner as sortBadges(Object.values(map))[0]; keep the map in globalStore for
that curAtom so clears restore lower-priority active badges.

}
},
});
}
Expand Down
3 changes: 3 additions & 0 deletions frontend/preview/mock/mockwaveenv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,9 @@ export function makeMockWaveEnv(mockEnv?: MockEnv): MockWaveEnv {
electron: {
...previewElectronApi,
getPlatform: () => platform,
openExternal: (url: string) => {
window.open(url, "_blank");
},
...overrides.electron,
},
rpc: makeMockRpc(overrides.rpc, mockWosFns),
Expand Down
8 changes: 6 additions & 2 deletions pkg/wcore/badge.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,12 @@ func setBadge(oref waveobj.ORef, data baseds.BadgeEvent) {
delete(globalBadgeStore.transient, orefStr)
log.Printf("badge store: badge cleared: oref=%s\n", orefStr)
} else {
globalBadgeStore.transient[orefStr] = *data.Badge
log.Printf("badge store: badge set: oref=%s badge=%+v\n", orefStr, *data.Badge)
incoming := *data.Badge
existing, hasExisting := globalBadgeStore.transient[orefStr]
if !hasExisting || incoming.Priority > existing.Priority || (incoming.Priority == existing.Priority && incoming.BadgeId > existing.BadgeId) {
globalBadgeStore.transient[orefStr] = incoming
log.Printf("badge store: badge set: oref=%s badge=%+v\n", orefStr, incoming)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't collapse per-badge state into a single stored badge.

ClearById makes badge lifecycle per badge, but this still retains only one winner in transient[orefStr]. If badge A is active, badge B with higher priority arrives, and later B clears by ID, A is gone forever even though it was never cleared. Please store active badges per oref keyed by BadgeId, then recompute the visible winner after each set/clear instead of overwriting the store with a single badge.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/wcore/badge.go` around lines 97 - 102, The current logic overwrites
globalBadgeStore.transient[orefStr] with a single Badge, causing cleared
BadgeIds to remove other active badges; change transient to hold a map of
BadgeId->Badge per oref (keep symbol names: globalBadgeStore.transient,
incoming, BadgeId, Priority, orefStr) so Set path inserts/updates incoming into
that per-oref map and ClearById removes by BadgeId, and after each set/clear
recompute and publish the visible winner (choose highest Priority, break ties by
BadgeId) instead of storing a single Badge directly.

}
}

Expand Down
Loading