wave-code 1.3.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/dist/bundle/wave.mjs +1382 -1131
  2. package/package.json +9 -7
  3. package/src/cli.tsx +14 -22
  4. package/src/commands/update.ts +62 -22
  5. package/src/components/AgentsManager.tsx +3 -3
  6. package/src/components/App.tsx +5 -0
  7. package/src/components/BackgroundTaskManager.tsx +76 -54
  8. package/src/components/BtwDisplay.tsx +71 -15
  9. package/src/components/CompactDisplay.tsx +1 -6
  10. package/src/components/ConfirmationDetails.tsx +34 -13
  11. package/src/components/ConfirmationSelector.tsx +31 -36
  12. package/src/components/DiffDisplay.tsx +1 -1
  13. package/src/components/HooksManager.tsx +3 -3
  14. package/src/components/InputBox.tsx +37 -11
  15. package/src/components/LoginCommand.tsx +16 -35
  16. package/src/components/Markdown.tsx +7 -3
  17. package/src/components/MarketplaceAddForm.tsx +15 -19
  18. package/src/components/MessageBlockItem.tsx +0 -6
  19. package/src/components/RemoteControlView.tsx +175 -0
  20. package/src/components/ScrolledContent.tsx +15 -13
  21. package/src/components/SkillsManager.tsx +3 -3
  22. package/src/components/TaskList.tsx +3 -3
  23. package/src/components/ToolDisplay.tsx +3 -18
  24. package/src/components/WorkflowManager.tsx +150 -136
  25. package/src/components/WorkflowRunDetail.tsx +193 -0
  26. package/src/constants/commands.ts +6 -0
  27. package/src/contexts/useChat.tsx +308 -154
  28. package/src/daemon/commands.ts +31 -22
  29. package/src/daemon-cli.ts +6 -1
  30. package/src/hooks/useInputManager.ts +25 -39
  31. package/src/hooks/useLineScroll.ts +51 -5
  32. package/src/hooks/useRemoteControl.ts +346 -0
  33. package/src/index.ts +35 -0
  34. package/src/managers/inputHandlers.ts +5 -0
  35. package/src/managers/inputReducer.ts +39 -0
  36. package/src/print-cli.ts +5 -0
  37. package/src/reducers/workflowManagerReducer.ts +21 -2
  38. package/src/remote/frameBudget.ts +45 -0
  39. package/src/remote/hostIdentity.ts +128 -0
  40. package/src/remote/messageWindow.ts +46 -0
  41. package/src/remote/notificationCoalescer.ts +192 -0
  42. package/src/remote/relayClient.ts +690 -0
  43. package/src/remote/remoteControlCliHost.ts +420 -0
  44. package/src/remote/remoteControlCommands.ts +197 -0
  45. package/src/remote/remoteControlConfig.ts +353 -0
  46. package/src/remote/remoteControlService.ts +822 -0
  47. package/src/remote/remoteControlSink.ts +78 -0
  48. package/src/remote/remoteNotifications.ts +200 -0
  49. package/src/stdio/agentBridge.ts +874 -106
  50. package/src/stdio/daemonServer.ts +30 -0
  51. package/src/stdio/hostSlashCommands.ts +209 -0
  52. package/src/stdio/jsonRpcConnection.ts +11 -2
  53. package/src/stdio/protocol.ts +14 -115
  54. package/src/stdio/stdioServer.ts +33 -2
  55. package/src/stdio/uiPayload.ts +65 -0
  56. package/src/stdio-cli.ts +11 -2
  57. package/src/types.ts +7 -0
  58. package/src/utils/gitCredentialHelper.ts +106 -0
  59. package/src/utils/logger.ts +5 -2
  60. package/src/utils/rewindCheckpoints.ts +2 -4
  61. package/src/utils/workspaceDiff.ts +620 -0
  62. package/src/utils/worktree.ts +97 -0
  63. package/src/utils/bracketedPaste.ts +0 -170
@@ -0,0 +1,78 @@
1
+ /**
2
+ * The seam between a session holder and the outbound relay that serves a
3
+ * paired phone.
4
+ *
5
+ * Every process that holds sessions needs it, for the same reason and with the
6
+ * same shape:
7
+ *
8
+ * - `--daemon <socket>` — the resident process on a remote host;
9
+ * - `--stdio` — the child each GUI host (desktop / VS Code / JetBrains) spawns
10
+ * and drives over JSON-RPC;
11
+ * - the interactive CLI itself — the session the user is sitting in front of,
12
+ * which lives in that process's React state and nowhere else.
13
+ *
14
+ * Whichever one holds the sessions has to be the one serving them: a phone
15
+ * pointed at another process would show a different session set than the user
16
+ * is looking at. So the sink is built where the sessions are, never shared
17
+ * across processes — and, for the CLI, that means built on a `RemoteControlHost`
18
+ * the front end implements rather than on an `AgentBridge` (which the CLI has
19
+ * none of).
20
+ */
21
+
22
+ import { isRemoteControlEnabled } from "wave-agent-sdk";
23
+ import { resolveRemoteControlEndpoint } from "./remoteControlConfig.js";
24
+ import {
25
+ RemoteControlService,
26
+ type RemoteControlController,
27
+ type RemoteControlHost,
28
+ } from "./remoteControlService.js";
29
+
30
+ /**
31
+ * A second fan-out target for a server's notifications, beside its local
32
+ * clients (unix-socket clients for the daemon, the host's pipe for `--stdio`).
33
+ *
34
+ * Its lifecycle is deliberately decoupled from the client set: the daemon is
35
+ * resident and a paired phone keeps watching while no desktop is attached —
36
+ * that is the point of serving from the daemon rather than from a GUI host.
37
+ */
38
+ export interface RemoteNotificationSink {
39
+ /** Same signature as `JsonRpcConnection.sendNotification`. */
40
+ sendNotification(method: string, params: unknown, sessionId?: string): void;
41
+ stop(): void;
42
+ }
43
+
44
+ /**
45
+ * The two halves of a remote-control sink: the server fans notifications into
46
+ * it, and the bridge routes the three `remoteControl*` RPCs to it.
47
+ */
48
+ export interface RemoteControlSink
49
+ extends RemoteNotificationSink,
50
+ RemoteControlController {}
51
+
52
+ /**
53
+ * Build the sink factory a session holder takes, or `undefined` when remote
54
+ * control is switched off for this process.
55
+ *
56
+ * The switch is read **once**, here, at process start: a machine that was
57
+ * already running keeps its old answer, which is why the CLI hint says to
58
+ * restart (the daemon, or the TUI itself) after changing it. `workdir` decides
59
+ * which settings files are merged — the user-level `~/.wave/settings.json` is
60
+ * always consulted, the workdir's own `.wave/settings.json` only when the
61
+ * process was started there (a GUI host's `--stdio` child inherits the app's
62
+ * cwd, not the project's, so user-level is the reliable place to set it).
63
+ */
64
+ export function createRemoteControlSinkFactory(
65
+ workdir: string,
66
+ ): ((host: RemoteControlHost) => RemoteControlSink) | undefined {
67
+ // Two-step by design: this switch (on unless a settings file or a remote
68
+ // push turns it off) says the machine *may* serve its sessions to a paired
69
+ // phone, and `wave remote-control on` / `/remote-control` is what actually
70
+ // dials out. The sink is therefore built but not connected.
71
+ if (!isRemoteControlEnabled(workdir)) {
72
+ return undefined;
73
+ }
74
+ return (host: RemoteControlHost): RemoteControlSink =>
75
+ new RemoteControlService(host, {
76
+ resolveEndpoint: resolveRemoteControlEndpoint,
77
+ });
78
+ }
@@ -0,0 +1,200 @@
1
+ /**
2
+ * The single table of notification **shapes**, shared by the two serving paths.
3
+ *
4
+ * `AgentBridge` (a daemon or a GUI-spawned `--stdio` child) puts the agent's
5
+ * callbacks straight onto the wire; the foreground TUI has no notification
6
+ * fan-out at all (`useRemoteControl` supplies it from its own React state). They
7
+ * are two different hosts — one has a session registry, the other a component
8
+ * tree — so each still decides *which* callbacks it emits. What must not exist in
9
+ * two copies is the **contract**: a method name and the fields of its payload.
10
+ * Both hosts build every frame they share from `remoteWire` below, so a payload
11
+ * change lands once and the phone cannot tell which process served it (spec:
12
+ * remote-control「前台 CLI 会话也能被手机接管」场景 10/11).
13
+ *
14
+ * What is deliberately *not* here: the families only a bridge emits (session
15
+ * registry reads, `tasksChange`, `queuedMessagesChange`, `commandRunningChange`,
16
+ * the compaction / plan / mcp changes). Nothing mirrors those, so there is no
17
+ * shape to keep in sync — and the foreground host is not required to invent one:
18
+ * the page reads that state off `getSessionInfo` on its alignment paths (spec
19
+ * 同故事场景 11).
20
+ */
21
+
22
+ import type {
23
+ AgentCallbacks,
24
+ BackgroundTask,
25
+ BackgroundTaskSummary,
26
+ PermissionMode,
27
+ ToolBlockUpdateCallbackParams,
28
+ ToolPermissionContext,
29
+ } from "wave-agent-sdk";
30
+ import { toolBlockUpdateForUi } from "../stdio/uiPayload.js";
31
+
32
+ /**
33
+ * A content delta exactly as the SDK callback hands it over — derived rather
34
+ * than re-declared, so a field added there cannot leave this table describing a
35
+ * shape the agent does not produce.
36
+ */
37
+ type StreamDelta = Parameters<
38
+ NonNullable<AgentCallbacks["onAssistantContentUpdated"]>
39
+ >[0];
40
+
41
+ /**
42
+ * Context-window usage as the percentage the ring renders — one rule for every
43
+ * producer (the bridge's change push and its `getMessages` read, the CLI host's
44
+ * push and its `getSessionInfo` read). 0 when the cap is unknown, so a caller
45
+ * that has one reads a number rather than a special case.
46
+ */
47
+ export function percentOf(tokens: number, maxInputTokens: number): number {
48
+ if (!maxInputTokens || maxInputTokens <= 0) return 0;
49
+ return Math.min(100, Math.round((tokens / maxInputTokens) * 100));
50
+ }
51
+
52
+ /** The shapes both hosts put on the wire, keyed by the method they ride under. */
53
+ export const remoteWire = {
54
+ /**
55
+ * A pure-delta frame carries the delta and nothing else — no accumulated value,
56
+ * so a notification's size is independent of how long the stream has been
57
+ * running (spec: stdio-transport「流式通知纯增量负载」). Explicit pick rather than
58
+ * a spread, so a field added to the SDK callback does not silently join the wire.
59
+ */
60
+ assistantContentUpdated: (params: StreamDelta) => ({
61
+ messageId: params.messageId,
62
+ chunk: params.chunk,
63
+ stage: params.stage,
64
+ }),
65
+
66
+ assistantReasoningUpdated: (params: StreamDelta) => ({
67
+ messageId: params.messageId,
68
+ chunk: params.chunk,
69
+ stage: params.stage,
70
+ }),
71
+
72
+ /**
73
+ * `streaming` frames carry only the delta (`parametersChunk`), while `start` /
74
+ * `running` (one-time snapshots) and `end` (the authoritative full value) keep
75
+ * `parameters`. The coalescer's merge rule is keyed on exactly this shape, so
76
+ * this pick is what makes the 150 ms window behave the same in both hosts.
77
+ *
78
+ * `images` is cut on every stage: tool pictures are base64 that no UI renders
79
+ * (specs/ui/stdio-transport.md「工具图片的字节不下发 UI」), and one of them can
80
+ * be larger than a remote frame may be.
81
+ */
82
+ toolBlockUpdated: (params: ToolBlockUpdateCallbackParams) => {
83
+ const trimmed = toolBlockUpdateForUi(params);
84
+ if (trimmed.stage !== "streaming") return trimmed;
85
+ const { parameters, ...rest } = trimmed;
86
+ void parameters;
87
+ return rest;
88
+ },
89
+
90
+ errorBlockAdded: (error: string) => ({ error }),
91
+
92
+ loadingChange: (loading: boolean, latestTotalTokens?: number) => ({
93
+ loading,
94
+ ...(latestTotalTokens === undefined ? {} : { latestTotalTokens }),
95
+ }),
96
+
97
+ contextUsage: (latestTotalTokens: number, maxInputTokens: number) => ({
98
+ percent: percentOf(latestTotalTokens, maxInputTokens),
99
+ }),
100
+
101
+ permissionModeChange: (mode: PermissionMode) => ({ mode }),
102
+
103
+ permissionRequest: (requestId: string, context: ToolPermissionContext) => ({
104
+ requestId,
105
+ context,
106
+ }),
107
+
108
+ /**
109
+ * `messagesChange` has exactly one shape now — content-free, on **every** path
110
+ * (spec「messagesChange 只有一种形态」): the bridge's re-attach push, the
111
+ * foreground CLI's structural notice and the relay's fan-out all carry `{}`.
112
+ * The phone answers it with `getMessages({ since })`; no producer attaches the
113
+ * list.
114
+ */
115
+ messagesChangeSignal: () => ({}),
116
+ } as const;
117
+
118
+ /** The methods this table defines — the only ones a mirroring host may send. */
119
+ export type RemoteMethod = keyof typeof remoteWire;
120
+
121
+ /**
122
+ * A background task (shell / subagent / workflow) as it may travel: the
123
+ * serializable half of `BackgroundTask`.
124
+ *
125
+ * A shape, hence this table's business — the two hosts each own the *list* (the
126
+ * bridge reads it off the agent on every snapshot, the foreground TUI off its own
127
+ * state) and both have to describe a task the same way, or the `/tasks` dialog
128
+ * renders two different things depending on which process served the page. The
129
+ * fields dropped here are the reasons it exists: `process` (a `ChildProcess`) and
130
+ * `onStop` (a function) cannot go over a relay, and the full `stdout`/`stderr` are
131
+ * fetched per task instead (`getBackgroundTaskOutput`).
132
+ */
133
+ export function toBackgroundTaskSummaries(
134
+ tasks: BackgroundTask[],
135
+ ): BackgroundTaskSummary[] {
136
+ return tasks.map((task) => ({
137
+ id: task.id,
138
+ type: task.type,
139
+ status: task.status,
140
+ startTime: task.startTime,
141
+ endTime: task.endTime,
142
+ command: task.command,
143
+ description: task.description,
144
+ exitCode: task.exitCode,
145
+ runtime: task.runtime,
146
+ outputPath: task.outputPath,
147
+ }));
148
+ }
149
+
150
+ /**
151
+ * What `RemoteControlSink.sendNotification` expects of a frame, minus the
152
+ * session it belongs to (the sink already knows). The method is narrowed to the
153
+ * table's keys so a caller cannot mirror a family that has no agreed shape.
154
+ */
155
+ export type RemoteNotify = (method: RemoteMethod, params: unknown) => void;
156
+
157
+ /**
158
+ * The callbacks whose **payload** a foreground host has to mirror, mapped onto
159
+ * the remote path under the bridge's method names.
160
+ *
161
+ * One rule decides membership: **can a pull get the change back?** A streaming
162
+ * delta grows a reply the phone is already holding, and an error block is
163
+ * appended *inside* the last message — in both cases the `since` anchor is that
164
+ * very message, so a signal would be a round trip that returns nothing and the
165
+ * change would never reach the phone (spec remote-control「前台 CLI 会话也能被手机
166
+ * 接管」场景 10/11). Structural changes (the list's id composition) and the state
167
+ * the page reads off `getSessionInfo` travel on the hook's own signals instead.
168
+ */
169
+ export function remoteNotifications(
170
+ notify: RemoteNotify,
171
+ ): Required<
172
+ Pick<
173
+ AgentCallbacks,
174
+ | "onAssistantContentUpdated"
175
+ | "onAssistantReasoningUpdated"
176
+ | "onToolBlockUpdated"
177
+ | "onErrorBlockAdded"
178
+ >
179
+ > {
180
+ return {
181
+ onAssistantContentUpdated: (params) => {
182
+ notify(
183
+ "assistantContentUpdated",
184
+ remoteWire.assistantContentUpdated(params),
185
+ );
186
+ },
187
+ onAssistantReasoningUpdated: (params) => {
188
+ notify(
189
+ "assistantReasoningUpdated",
190
+ remoteWire.assistantReasoningUpdated(params),
191
+ );
192
+ },
193
+ onToolBlockUpdated: (params) => {
194
+ notify("toolBlockUpdated", remoteWire.toolBlockUpdated(params));
195
+ },
196
+ onErrorBlockAdded: (error: string) => {
197
+ notify("errorBlockAdded", remoteWire.errorBlockAdded(error));
198
+ },
199
+ };
200
+ }