Skip to content

Commit 4947330

Browse files
authored
Merge branch 'main' into feat/close-on-last-terminal-exit
2 parents 96dbd53 + 4034526 commit 4947330

85 files changed

Lines changed: 3330 additions & 648 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.kilocode/skills/add-rpc/SKILL.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ RPC commands in Wave Terminal follow these conventions:
2626

2727
- **Method names** must end with `Command`
2828
- **First parameter** must be `context.Context`
29-
- **Second parameter** (optional) is the command data structure
29+
- **Remaining parameters** are a regular Go parameter list (zero or more typed args)
3030
- **Return values** can be either just an error, or one return value plus an error
3131
- **Streaming commands** return a channel instead of a direct value
3232

@@ -49,7 +49,7 @@ type WshRpcInterface interface {
4949

5050
- Method name must end with `Command`
5151
- First parameter must be `ctx context.Context`
52-
- Optional second parameter for input data
52+
- Remaining parameters are a regular Go parameter list (zero or more)
5353
- Return either `error` or `(ReturnType, error)`
5454
- For streaming, return `chan RespOrErrorUnion[T]`
5555

.kilocode/skills/waveenv/SKILL.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,12 @@ export type MyEnv = WaveEnvSubset<{
6969
// --- wos: always take the whole thing, no sub-typing needed ---
7070
wos: WaveEnv["wos"];
7171

72+
// --- services: list only the services you call; no method-level narrowing ---
73+
services: {
74+
block: WaveEnv["services"]["block"];
75+
workspace: WaveEnv["services"]["workspace"];
76+
};
77+
7278
// --- key-parameterized atom factories: enumerate the keys you use ---
7379
getSettingsKeyAtom: SettingsKeyAtomFnType<"app:focusfollowscursor" | "window:magnifiedblockopacity">;
7480
getBlockMetaKeyAtom: BlockMetaKeyAtomFnType<"view" | "frame:title" | "connection">;
@@ -80,6 +86,14 @@ export type MyEnv = WaveEnvSubset<{
8086
}>;
8187
```
8288

89+
### Automatically Included Fields
90+
91+
Every `WaveEnvSubset<T>` automatically includes the mock fields — you never need to declare them:
92+
93+
- `isMock: boolean`
94+
- `mockSetWaveObj: <T extends WaveObj>(oref: string, obj: T) => void`
95+
- `mockModels?: Map<any, any>`
96+
8397
### Rules for Each Section
8498

8599
| Section | Pattern | Notes |
@@ -88,6 +102,7 @@ export type MyEnv = WaveEnvSubset<{
88102
| `rpc` | `rpc: { Cmd: WaveEnv["rpc"]["Cmd"]; }` | List every RPC command called; omit the rest. |
89103
| `atoms` | `atoms: { atom: WaveEnv["atoms"]["atom"]; }` | List every atom read; omit the rest. |
90104
| `wos` | `wos: WaveEnv["wos"]` | Take the whole `wos` object (no sub-typing needed), but **only add it if `wos` is actually used**. |
105+
| `services` | `services: { svc: WaveEnv["services"]["svc"]; }` | List each service used; take the whole service object (no method-level narrowing). |
91106
| `getSettingsKeyAtom` | `SettingsKeyAtomFnType<"key1" \| "key2">` | Union all settings keys accessed. |
92107
| `getBlockMetaKeyAtom` | `BlockMetaKeyAtomFnType<"key1" \| "key2">` | Union all block meta keys accessed. |
93108
| `getConnConfigKeyAtom` | `ConnConfigKeyAtomFnType<"key1">` | Union all conn config keys accessed. |

cmd/generatets/main-generatets.go

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,14 +88,37 @@ func generateServicesFile(tsTypesMap map[reflect.Type]string) error {
8888
fmt.Fprintf(&buf, "// Copyright 2026, Command Line Inc.\n")
8989
fmt.Fprintf(&buf, "// SPDX-License-Identifier: Apache-2.0\n\n")
9090
fmt.Fprintf(&buf, "// generated by cmd/generate/main-generatets.go\n\n")
91-
fmt.Fprintf(&buf, "import * as WOS from \"./wos\";\n\n")
91+
fmt.Fprintf(&buf, "import * as WOS from \"./wos\";\n")
92+
fmt.Fprintf(&buf, "import type { WaveEnv } from \"@/app/waveenv/waveenv\";\n\n")
93+
fmt.Fprintf(&buf, "function callBackendService(waveEnv: WaveEnv, service: string, method: string, args: any[], noUIContext?: boolean): Promise<any> {\n")
94+
fmt.Fprintf(&buf, " if (waveEnv != null) {\n")
95+
fmt.Fprintf(&buf, " return waveEnv.callBackendService(service, method, args, noUIContext)\n")
96+
fmt.Fprintf(&buf, " }\n")
97+
fmt.Fprintf(&buf, " return WOS.callBackendService(service, method, args, noUIContext);\n")
98+
fmt.Fprintf(&buf, "}\n\n")
9299
orderedKeys := utilfn.GetOrderedMapKeys(service.ServiceMap)
93100
for _, serviceName := range orderedKeys {
94101
serviceObj := service.ServiceMap[serviceName]
95102
svcStr := tsgen.GenerateServiceClass(serviceName, serviceObj, tsTypesMap)
96103
fmt.Fprint(&buf, svcStr)
97104
fmt.Fprint(&buf, "\n")
98105
}
106+
fmt.Fprintf(&buf, "export const AllServiceTypes = {\n")
107+
for _, serviceName := range orderedKeys {
108+
serviceObj := service.ServiceMap[serviceName]
109+
serviceType := reflect.TypeOf(serviceObj)
110+
tsServiceName := serviceType.Elem().Name()
111+
fmt.Fprintf(&buf, " %q: %sType,\n", serviceName, tsServiceName)
112+
}
113+
fmt.Fprintf(&buf, "};\n\n")
114+
fmt.Fprintf(&buf, "export const AllServiceImpls = {\n")
115+
for _, serviceName := range orderedKeys {
116+
serviceObj := service.ServiceMap[serviceName]
117+
serviceType := reflect.TypeOf(serviceObj)
118+
tsServiceName := serviceType.Elem().Name()
119+
fmt.Fprintf(&buf, " %q: %s,\n", serviceName, tsServiceName)
120+
}
121+
fmt.Fprintf(&buf, "};\n")
99122
written, err := utilfn.WriteFileIfDifferent(fileName, buf.Bytes())
100123
if !written {
101124
fmt.Fprintf(os.Stderr, "no changes to %s\n", fileName)

cmd/wsh/cmd/wshcmd-ssh.go

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2025, Command Line Inc.
1+
// Copyright 2026, Command Line Inc.
22
// SPDX-License-Identifier: Apache-2.0
33

44
package cmd
@@ -7,6 +7,7 @@ import (
77
"fmt"
88

99
"github.com/spf13/cobra"
10+
"github.com/wavetermdev/waveterm/pkg/remote"
1011
"github.com/wavetermdev/waveterm/pkg/waveobj"
1112
"github.com/wavetermdev/waveterm/pkg/wconfig"
1213
"github.com/wavetermdev/waveterm/pkg/wshrpc"
@@ -15,6 +16,8 @@ import (
1516

1617
var (
1718
identityFiles []string
19+
sshLogin string
20+
sshPort string
1821
newBlock bool
1922
)
2023

@@ -28,6 +31,8 @@ var sshCmd = &cobra.Command{
2831

2932
func init() {
3033
sshCmd.Flags().StringArrayVarP(&identityFiles, "identityfile", "i", []string{}, "add an identity file for publickey authentication")
34+
sshCmd.Flags().StringVarP(&sshLogin, "login", "l", "", "set the remote login name")
35+
sshCmd.Flags().StringVarP(&sshPort, "port", "p", "", "set the remote port")
3136
sshCmd.Flags().BoolVarP(&newBlock, "new", "n", false, "create a new terminal block with this connection")
3237
rootCmd.AddCommand(sshCmd)
3338
}
@@ -38,6 +43,11 @@ func sshRun(cmd *cobra.Command, args []string) (rtnErr error) {
3843
}()
3944

4045
sshArg := args[0]
46+
var err error
47+
sshArg, err = applySSHOverrides(sshArg, sshLogin, sshPort)
48+
if err != nil {
49+
return err
50+
}
4151
blockId := RpcContext.BlockId
4252
if blockId == "" && !newBlock {
4353
return fmt.Errorf("cannot determine blockid (not in JWT)")
@@ -91,10 +101,27 @@ func sshRun(cmd *cobra.Command, args []string) (rtnErr error) {
91101
waveobj.MetaKey_CmdCwd: nil,
92102
},
93103
}
94-
err := wshclient.SetMetaCommand(RpcClient, data, nil)
104+
err = wshclient.SetMetaCommand(RpcClient, data, nil)
95105
if err != nil {
96106
return fmt.Errorf("setting connection in block: %w", err)
97107
}
98108
WriteStderr("switched connection to %q\n", sshArg)
99109
return nil
100110
}
111+
112+
func applySSHOverrides(sshArg string, login string, port string) (string, error) {
113+
if login == "" && port == "" {
114+
return sshArg, nil
115+
}
116+
opts, err := remote.ParseOpts(sshArg)
117+
if err != nil {
118+
return "", err
119+
}
120+
if login != "" {
121+
opts.SSHUser = login
122+
}
123+
if port != "" {
124+
opts.SSHPort = port
125+
}
126+
return opts.String(), nil
127+
}

cmd/wsh/cmd/wshcmd-ssh_test.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// Copyright 2026, Command Line Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package cmd
5+
6+
import "testing"
7+
8+
func TestApplySSHOverrides(t *testing.T) {
9+
tests := []struct {
10+
name string
11+
sshArg string
12+
login string
13+
port string
14+
want string
15+
wantErr bool
16+
}{
17+
{
18+
name: "no overrides preserves target",
19+
sshArg: "root@bar.com:2022",
20+
want: "root@bar.com:2022",
21+
},
22+
{
23+
name: "login override replaces parsed user",
24+
sshArg: "root@bar.com",
25+
login: "foo",
26+
want: "foo@bar.com",
27+
},
28+
{
29+
name: "port override replaces parsed port",
30+
sshArg: "root@bar.com:2022",
31+
port: "2222",
32+
want: "root@bar.com:2222",
33+
},
34+
{
35+
name: "both overrides replace parsed user and port",
36+
sshArg: "root@bar.com:2022",
37+
login: "foo",
38+
port: "2200",
39+
want: "foo@bar.com:2200",
40+
},
41+
{
42+
name: "login override adds user to bare host",
43+
sshArg: "bar.com",
44+
login: "foo",
45+
want: "foo@bar.com",
46+
},
47+
{
48+
name: "port override adds port to bare host",
49+
sshArg: "bar.com",
50+
port: "2200",
51+
want: "bar.com:2200",
52+
},
53+
{
54+
name: "invalid target returns parse error when override requested",
55+
sshArg: "bad host",
56+
login: "foo",
57+
wantErr: true,
58+
},
59+
}
60+
61+
for _, tt := range tests {
62+
t.Run(tt.name, func(t *testing.T) {
63+
got, err := applySSHOverrides(tt.sshArg, tt.login, tt.port)
64+
if (err != nil) != tt.wantErr {
65+
t.Fatalf("applySSHOverrides() error = %v, wantErr %v", err, tt.wantErr)
66+
}
67+
if tt.wantErr {
68+
return
69+
}
70+
if got != tt.want {
71+
t.Fatalf("applySSHOverrides() = %q, want %q", got, tt.want)
72+
}
73+
})
74+
}
75+
}

docs/docs/claude-code.mdx

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
---
2+
sidebar_position: 1.9
3+
id: "claude-code"
4+
title: "Claude Code Integration"
5+
---
6+
7+
import { VersionBadge } from "@site/src/components/versionbadge";
8+
9+
# Claude Code Tab Badges <VersionBadge version="v0.14.2" />
10+
11+
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.
12+
13+
:::info
14+
tl;dr You can copy and paste this page directly into Claude Code and it will help you set everything up!
15+
:::
16+
17+
## How it works
18+
19+
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.
20+
21+
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.
22+
23+
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.
24+
25+
### Badge rollup
26+
27+
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.
28+
29+
## Setup
30+
31+
These hooks go in your global Claude Code settings so they apply to every session on your machine, not just one project.
32+
33+
Add the following to `~/.claude/settings.json`. If you already have a `hooks` key, merge the entries in:
34+
35+
```json
36+
{
37+
"hooks": {
38+
"Notification": [
39+
{
40+
"matcher": "permission_prompt",
41+
"hooks": [
42+
{
43+
"type": "command",
44+
"command": "wsh badge bell-exclamation --color '#e0b956' --priority 20 --beep"
45+
}
46+
]
47+
},
48+
{
49+
"matcher": "elicitation_dialog",
50+
"hooks": [
51+
{
52+
"type": "command",
53+
"command": "wsh badge message-question --color '#e0b956' --priority 20 --beep"
54+
}
55+
]
56+
}
57+
],
58+
"Stop": [
59+
{
60+
"hooks": [
61+
{
62+
"type": "command",
63+
"command": "wsh badge check --color '#58c142' --priority 10"
64+
}
65+
]
66+
}
67+
],
68+
"PreToolUse": [
69+
{
70+
"matcher": "AskUserQuestion",
71+
"hooks": [
72+
{
73+
"type": "command",
74+
"command": "wsh badge message-question --color '#e0b956' --priority 20 --beep"
75+
}
76+
]
77+
}
78+
]
79+
}
80+
}
81+
```
82+
83+
That's it. Restart any running Claude Code sessions for the hooks to take effect.
84+
85+
:::warning Known Issue
86+
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.
87+
:::
88+
89+
## What each hook does
90+
91+
### Permission prompt — `bell-exclamation` gold, priority 20
92+
93+
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.
94+
95+
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.
96+
97+
When you click into the tab and approve or deny the request, the badge clears automatically.
98+
99+
### Session complete — `check` green, priority 10
100+
101+
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.
102+
103+
### AskUserQuestion — `message-question` gold, priority 20
104+
105+
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.
106+
107+
`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.
108+
109+
## Choosing your own icons and colors
110+
111+
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.
112+
113+
Some icon and color ideas:
114+
115+
| Situation | Icon | Color |
116+
|-----------|------|-------|
117+
| Custom high-priority alert | `triangle-exclamation` | `#FF453A` |
118+
| Blocked / waiting on input | `hourglass-half` | `#FF9500` |
119+
| Neutral / informational | `circle-info` | `#429DFF` |
120+
| Background task running | `spinner` | `#00FFDB` |
121+
122+
See the [`wsh badge` reference](/wsh-reference#badge) for all available flags.
123+
124+
## Adjusting priorities
125+
126+
Priority controls which badge wins when multiple blocks in a tab each have one. Higher numbers take precedence. The defaults above use:
127+
128+
- **20** for permission prompts — always surfaces above everything else
129+
- **10** for session complete — visible when nothing more urgent is active
130+
131+
If you add more hooks, keep permission-blocking signals at the high end (15–25) and informational signals at the low end (5–10).

docs/docs/config.mdx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ wsh editconfig
8181
| editor:fontsize | float64 | set the font size for the editor (defaults to 12px) |
8282
| editor:inlinediff | bool | set to true to show diffs inline instead of side-by-side, false for side-by-side (defaults to undefined which uses Monaco's responsive behavior) |
8383
| preview:showhiddenfiles | bool | set to false to disable showing hidden files in the directory preview (defaults to true) |
84+
| preview:defaultsort <VersionBadge version="v0.14.2" /> | string | sets the default sort column for directory preview. `"name"` (default) sorts alphabetically by name ascending; `"modtime"` sorts by last modified time descending (newest first) |
8485
| markdown:fontsize | float64 | font size for the normal text when rendering markdown in preview. headers are scaled up from this size, (default 14px) |
8586
| markdown:fixedfontsize | float64 | font size for the code blocks when rendering markdown in preview (default is 12px) |
8687
| web:openlinksinternally | bool | set to false to open web links in external browser |
@@ -154,7 +155,8 @@ For reference, this is the current default configuration (v0.14.0):
154155
"term:copyonselect": true,
155156
"term:durable": false,
156157
"waveai:showcloudmodes": true,
157-
"waveai:defaultmode": "waveai@balanced"
158+
"waveai:defaultmode": "waveai@balanced",
159+
"preview:defaultsort": "name"
158160
}
159161
```
160162

0 commit comments

Comments
 (0)