From c8b9291eee4ee7d98a57045f44ee9573e742b8a2 Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 11 Mar 2026 17:27:37 -0700 Subject: [PATCH 1/5] new CC hooks docs page, link in onboarding, add openExternal to preview mock --- docs/docs/claude-code.mdx | 97 +++++++++++++++++++ .../onboarding/onboarding-upgrade-v0140.tsx | 5 +- .../onboarding/onboarding-upgrade-v0142.tsx | 13 ++- frontend/preview/mock/mockwaveenv.ts | 3 + 4 files changed, 115 insertions(+), 3 deletions(-) create mode 100644 docs/docs/claude-code.mdx diff --git a/docs/docs/claude-code.mdx b/docs/docs/claude-code.mdx new file mode 100644 index 0000000000..1083aeab60 --- /dev/null +++ b/docs/docs/claude-code.mdx @@ -0,0 +1,97 @@ +--- +sidebar_position: 1.9 +id: "claude-code" +title: "Claude Code Integration" +--- + +# Claude Code Tab Badges + +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. + +## 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" + } + ] + } + ], + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "wsh badge check --color '#58c142' --priority 10" + } + ] + } + ] + } +} +``` + +That's it. Restart any running Claude Code sessions for the hooks to take effect. + +## What each hook does + +### Permission prompt — `circle-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 — `circle-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. + +The `stop_hook_active` guard in the command prevents the hook from firing recursively if you have other `Stop` hooks that keep Claude running. + +## 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 — named colors like `gold` and `green`, hex values like `#f59e0b`, or anything else CSS accepts. + +Some alternatives to consider: + +| Situation | Icon | Color | +|-----------|------|-------| +| Permission needed | `circle-exclamation` | `gold` | +| Session complete | `circle-check` | `green` | +| Custom high-priority alert | `triangle-exclamation` | `red` | +| Neutral / informational | `circle-info` | `steelblue` | + +To target a specific block or tab instead of the current one, add `-b ` or `-b tab`. 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). \ No newline at end of file diff --git a/frontend/app/onboarding/onboarding-upgrade-v0140.tsx b/frontend/app/onboarding/onboarding-upgrade-v0140.tsx index d2b7b18215..0102b691e2 100644 --- a/frontend/app/onboarding/onboarding-upgrade-v0140.tsx +++ b/frontend/app/onboarding/onboarding-upgrade-v0140.tsx @@ -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 (
@@ -22,7 +23,7 @@ const UpgradeOnboardingModal_v0_14_0_Content = () => {
Durable SSH Sessions{" "}
diff --git a/frontend/preview/mock/mockwaveenv.ts b/frontend/preview/mock/mockwaveenv.ts index fdbeb60e4a..0ad89b399a 100644 --- a/frontend/preview/mock/mockwaveenv.ts +++ b/frontend/preview/mock/mockwaveenv.ts @@ -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), From 669e7c39d6757e564656b0d6f892fafac6c09100 Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 11 Mar 2026 19:21:57 -0700 Subject: [PATCH 2/5] fix badge priority, update CC docs --- docs/docs/claude-code.mdx | 56 +++++++++++++++++++++++++++++-------- frontend/app/store/badge.ts | 11 ++++++-- pkg/wcore/badge.go | 8 ++++-- 3 files changed, 59 insertions(+), 16 deletions(-) diff --git a/docs/docs/claude-code.mdx b/docs/docs/claude-code.mdx index 1083aeab60..f61fcba910 100644 --- a/docs/docs/claude-code.mdx +++ b/docs/docs/claude-code.mdx @@ -4,10 +4,16 @@ id: "claude-code" title: "Claude Code Integration" --- -# Claude Code Tab Badges +import { VersionBadge } from "@site/src/components/versionbadge"; + +# Claude Code Tab Badges 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. @@ -38,6 +44,15 @@ Add the following to `~/.claude/settings.json`. If you already have a `hooks` ke "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": [ @@ -49,6 +64,17 @@ Add the following to `~/.claude/settings.json`. If you already have a `hooks` ke } ] } + ], + "PreToolUse": [ + { + "matcher": "AskUserQuestion", + "hooks": [ + { + "type": "command", + "command": "wsh badge message-question --color '#e0b965' --priority 20 --beep" + } + ] + } ] } } @@ -56,9 +82,13 @@ Add the following to `~/.claude/settings.json`. If you already have a `hooks` ke 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 — `circle-exclamation` gold, priority 20 +### 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. @@ -66,26 +96,30 @@ This hook fires on the `permission_prompt` notification type and sets a high-pri When you click into the tab and approve or deny the request, the badge clears automatically. -### Session complete — `circle-check` green, priority 10 +### 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. -The `stop_hook_active` guard in the command prevents the hook from firing recursively if you have other `Stop` hooks that keep Claude running. +### 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 — named colors like `gold` and `green`, hex values like `#f59e0b`, or anything else CSS accepts. +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 alternatives to consider: +Some icon and color ideas: | Situation | Icon | Color | |-----------|------|-------| -| Permission needed | `circle-exclamation` | `gold` | -| Session complete | `circle-check` | `green` | -| Custom high-priority alert | `triangle-exclamation` | `red` | -| Neutral / informational | `circle-info` | `steelblue` | +| Custom high-priority alert | `triangle-exclamation` | `#FF453A` | +| Blocked / waiting on input | `hourglass-half` | `#FF9500` | +| Neutral / informational | `circle-info` | `#429DFF` | +| Background task running | `spinner` | `#00FFDB` | -To target a specific block or tab instead of the current one, add `-b ` or `-b tab`. See the [`wsh badge` reference](/reference/wsh-cmd/badge) for all available flags. +See the [`wsh badge` reference](/reference/wsh-cmd/badge) for all available flags. ## Adjusting priorities diff --git a/frontend/app/store/badge.ts b/frontend/app/store/badge.ts index e1cf8e5fe4..ea9fbdc123 100644 --- a/frontend/app/store/badge.ts +++ b/frontend/app/store/badge.ts @@ -123,8 +123,7 @@ function getTabBadgeAtom(tabId: string, env?: TabBadgesEnv): Atom { } const tabOref = WOS.makeORef("tab", tabId); const tabBadgeAtom = getBadgeAtom(tabOref); - const tabAtom = - env != null ? env.wos.getWaveObjectAtom(tabOref) : WOS.getWaveObjectAtom(tabOref); + const tabAtom = env != null ? env.wos.getWaveObjectAtom(tabOref) : WOS.getWaveObjectAtom(tabOref); rtn = atom((get) => { const tab = get(tabAtom); const blockIds = tab?.blockids ?? []; @@ -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]); + } }, }); } diff --git a/pkg/wcore/badge.go b/pkg/wcore/badge.go index 9c45950c16..446c614847 100644 --- a/pkg/wcore/badge.go +++ b/pkg/wcore/badge.go @@ -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) + } } } From 6c68946faa72bb1f08123e833908bf99ff832046 Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 11 Mar 2026 21:42:09 -0700 Subject: [PATCH 3/5] typo --- docs/docs/claude-code.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/claude-code.mdx b/docs/docs/claude-code.mdx index f61fcba910..625992f2d2 100644 --- a/docs/docs/claude-code.mdx +++ b/docs/docs/claude-code.mdx @@ -71,7 +71,7 @@ Add the following to `~/.claude/settings.json`. If you already have a `hooks` ke "hooks": [ { "type": "command", - "command": "wsh badge message-question --color '#e0b965' --priority 20 --beep" + "command": "wsh badge message-question --color '#e0b956' --priority 20 --beep" } ] } From afb73e8840755936a6c9cdfb7868924bf4297c90 Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 11 Mar 2026 21:47:08 -0700 Subject: [PATCH 4/5] fix links --- docs/docs/claude-code.mdx | 2 +- docs/docs/wsh-reference.mdx | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/docs/claude-code.mdx b/docs/docs/claude-code.mdx index 625992f2d2..d16b0f0b0b 100644 --- a/docs/docs/claude-code.mdx +++ b/docs/docs/claude-code.mdx @@ -119,7 +119,7 @@ Some icon and color ideas: | Neutral / informational | `circle-info` | `#429DFF` | | Background task running | `spinner` | `#00FFDB` | -See the [`wsh badge` reference](/reference/wsh-cmd/badge) for all available flags. +See the [`wsh badge` reference](/wsh-reference#badge) for all available flags. ## Adjusting priorities diff --git a/docs/docs/wsh-reference.mdx b/docs/docs/wsh-reference.mdx index c83cf28d0e..6ff8c2e8f5 100644 --- a/docs/docs/wsh-reference.mdx +++ b/docs/docs/wsh-reference.mdx @@ -263,7 +263,9 @@ Use `--print` to preview the metadata for any background configuration without a --- -## badge +## badge + + The `badge` command sets or clears a visual badge indicator on a block or tab header. From 7bbf7b074103f594ad4843017623db4918ca44c4 Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 11 Mar 2026 21:56:36 -0700 Subject: [PATCH 5/5] standardize badge logic --- frontend/app/store/badge.ts | 29 ++++++++++++++-------- pkg/wcore/badge.go | 48 +++++++++++++++++++++++++++---------- 2 files changed, 54 insertions(+), 23 deletions(-) diff --git a/frontend/app/store/badge.ts b/frontend/app/store/badge.ts index ea9fbdc123..745a2eb4da 100644 --- a/frontend/app/store/badge.ts +++ b/frontend/app/store/badge.ts @@ -214,22 +214,31 @@ function setupBadgesSubscription() { } 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]); + return; + } + if (data.badge == null) { + return; + } + const existing = globalStore.get(curAtom); + if (existing == null || cmpBadge(data.badge, existing) > 0) { + globalStore.set(curAtom, data.badge); } }, }); } +function cmpBadge(a: Badge, b: Badge): number { + if (a.priority !== b.priority) { + return a.priority > b.priority ? 1 : -1; + } + if (a.badgeid !== b.badgeid) { + return a.badgeid > b.badgeid ? 1 : -1; + } + return 0; +} + function sortBadges(badges: Badge[]): Badge[] { - return [...badges].sort((a, b) => { - if (a.priority !== b.priority) { - return b.priority - a.priority; - } - return b.badgeid < a.badgeid ? -1 : b.badgeid > a.badgeid ? 1 : 0; - }); + return [...badges].sort((a, b) => cmpBadge(b, a)); } function sortBadgesForTab(badges: Badge[]): Badge[] { diff --git a/pkg/wcore/badge.go b/pkg/wcore/badge.go index 446c614847..a60ecb8fc1 100644 --- a/pkg/wcore/badge.go +++ b/pkg/wcore/badge.go @@ -72,34 +72,56 @@ func handleBadgeEvent(event *wps.WaveEvent) { setBadge(oref, data) } +// cmpBadge compares two badges by priority then by badgeid (both descending). +// Returns 1 if a > b, -1 if a < b, 0 if equal. +func cmpBadge(a, b baseds.Badge) int { + if a.Priority != b.Priority { + if a.Priority > b.Priority { + return 1 + } + return -1 + } + if a.BadgeId != b.BadgeId { + if a.BadgeId > b.BadgeId { + return 1 + } + return -1 + } + return 0 +} + // setBadge updates the in-memory transient map. func setBadge(oref waveobj.ORef, data baseds.BadgeEvent) { globalBadgeStore.lock.Lock() defer globalBadgeStore.lock.Unlock() orefStr := oref.String() + if orefStr == "" { + return + } - shouldClear := data.Clear if data.ClearById != "" { existing, ok := globalBadgeStore.transient[orefStr] if !ok || existing.BadgeId != data.ClearById { return } - shouldClear = true - } else if !data.Clear { - shouldClear = data.Badge == nil + delete(globalBadgeStore.transient, orefStr) + log.Printf("badge store: badge cleared by id: oref=%s id=%s\n", orefStr, data.ClearById) + return } - - if shouldClear { + if data.Clear { delete(globalBadgeStore.transient, orefStr) log.Printf("badge store: badge cleared: oref=%s\n", orefStr) - } else { - 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) - } + return + } + if data.Badge == nil { + return + } + incoming := *data.Badge + existing, hasExisting := globalBadgeStore.transient[orefStr] + if !hasExisting || cmpBadge(incoming, existing) > 0 { + globalBadgeStore.transient[orefStr] = incoming + log.Printf("badge store: badge set: oref=%s badge=%+v\n", orefStr, incoming) } }