wave-code 1.0.9 → 1.1.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 (244) hide show
  1. package/bin/wave-code.js +8 -2
  2. package/dist/bundle/wave.mjs +3207 -0
  3. package/package.json +24 -25
  4. package/src/commands/update.ts +2 -7
  5. package/src/components/App.tsx +3 -7
  6. package/src/components/BtwDisplay.tsx +10 -1
  7. package/src/components/ChatInterface.tsx +21 -52
  8. package/src/components/ConfirmationDetails.tsx +144 -37
  9. package/src/components/DiffDisplay.tsx +335 -301
  10. package/src/components/InputBox.tsx +3 -1
  11. package/src/components/LoadingIndicator.tsx +16 -2
  12. package/src/components/MessageBlockItem.tsx +2 -4
  13. package/src/components/MessageList.tsx +75 -14
  14. package/src/components/ReasoningDisplay.tsx +2 -4
  15. package/src/components/SessionSelector.tsx +82 -11
  16. package/src/components/StatusCommand.tsx +2 -9
  17. package/src/components/ToolDisplay.tsx +2 -4
  18. package/src/contexts/useChat.tsx +172 -350
  19. package/src/daemon/commands.ts +57 -27
  20. package/src/daemon/jsonRpcClient.ts +1 -1
  21. package/src/daemon/socketClient.ts +2 -2
  22. package/src/hooks/useInputManager.ts +1 -1
  23. package/src/index.ts +57 -10
  24. package/src/managers/inputReducer.ts +186 -104
  25. package/src/print-cli.ts +21 -17
  26. package/src/reducers/selectorReducer.ts +10 -0
  27. package/src/session-selector-cli.tsx +269 -28
  28. package/src/stdio/agentBridge.ts +22 -21
  29. package/src/stdio/daemonServer.ts +3 -1
  30. package/src/stdio/jsonRpcConnection.ts +19 -1
  31. package/src/stdio/stdioServer.ts +17 -0
  32. package/src/utils/clipboard.ts +21 -0
  33. package/src/utils/highlightUtils.ts +46 -1
  34. package/src/utils/readPackageJson.ts +29 -0
  35. package/src/utils/streamingText.ts +11 -0
  36. package/src/utils/worktree.ts +191 -43
  37. package/dist/cli.d.ts +0 -6
  38. package/dist/cli.js +0 -53
  39. package/dist/commands/plugin/disable.d.ts +0 -5
  40. package/dist/commands/plugin/disable.js +0 -15
  41. package/dist/commands/plugin/enable.d.ts +0 -5
  42. package/dist/commands/plugin/enable.js +0 -15
  43. package/dist/commands/plugin/install.d.ts +0 -5
  44. package/dist/commands/plugin/install.js +0 -21
  45. package/dist/commands/plugin/list.d.ts +0 -1
  46. package/dist/commands/plugin/list.js +0 -31
  47. package/dist/commands/plugin/marketplace.d.ts +0 -10
  48. package/dist/commands/plugin/marketplace.js +0 -90
  49. package/dist/commands/plugin/uninstall.d.ts +0 -3
  50. package/dist/commands/plugin/uninstall.js +0 -16
  51. package/dist/commands/plugin/update.d.ts +0 -3
  52. package/dist/commands/plugin/update.js +0 -15
  53. package/dist/commands/update.d.ts +0 -1
  54. package/dist/commands/update.js +0 -97
  55. package/dist/components/AgentsManager.d.ts +0 -7
  56. package/dist/components/AgentsManager.js +0 -109
  57. package/dist/components/App.d.ts +0 -9
  58. package/dist/components/App.js +0 -76
  59. package/dist/components/BackgroundTaskManager.d.ts +0 -5
  60. package/dist/components/BackgroundTaskManager.js +0 -99
  61. package/dist/components/BangDisplay.d.ts +0 -8
  62. package/dist/components/BangDisplay.js +0 -17
  63. package/dist/components/BtwDisplay.d.ts +0 -7
  64. package/dist/components/BtwDisplay.js +0 -19
  65. package/dist/components/ChatInterface.d.ts +0 -2
  66. package/dist/components/ChatInterface.js +0 -61
  67. package/dist/components/CommandSelector.d.ts +0 -10
  68. package/dist/components/CommandSelector.js +0 -66
  69. package/dist/components/CompactDisplay.d.ts +0 -8
  70. package/dist/components/CompactDisplay.js +0 -13
  71. package/dist/components/ConfirmationDetails.d.ts +0 -9
  72. package/dist/components/ConfirmationDetails.js +0 -45
  73. package/dist/components/ConfirmationSelector.d.ts +0 -13
  74. package/dist/components/ConfirmationSelector.js +0 -127
  75. package/dist/components/DiffDisplay.d.ts +0 -8
  76. package/dist/components/DiffDisplay.js +0 -201
  77. package/dist/components/DiscoverView.d.ts +0 -2
  78. package/dist/components/DiscoverView.js +0 -37
  79. package/dist/components/FileSelector.d.ts +0 -11
  80. package/dist/components/FileSelector.js +0 -50
  81. package/dist/components/HelpView.d.ts +0 -7
  82. package/dist/components/HelpView.js +0 -74
  83. package/dist/components/HistorySearch.d.ts +0 -8
  84. package/dist/components/HistorySearch.js +0 -66
  85. package/dist/components/InputBox.d.ts +0 -24
  86. package/dist/components/InputBox.js +0 -149
  87. package/dist/components/InstalledView.d.ts +0 -2
  88. package/dist/components/InstalledView.js +0 -45
  89. package/dist/components/LoadingIndicator.d.ts +0 -10
  90. package/dist/components/LoadingIndicator.js +0 -6
  91. package/dist/components/LoginCommand.d.ts +0 -5
  92. package/dist/components/LoginCommand.js +0 -153
  93. package/dist/components/Markdown.d.ts +0 -5
  94. package/dist/components/Markdown.js +0 -127
  95. package/dist/components/MarketplaceAddForm.d.ts +0 -2
  96. package/dist/components/MarketplaceAddForm.js +0 -55
  97. package/dist/components/MarketplaceDetail.d.ts +0 -2
  98. package/dist/components/MarketplaceDetail.js +0 -74
  99. package/dist/components/MarketplaceList.d.ts +0 -8
  100. package/dist/components/MarketplaceList.js +0 -17
  101. package/dist/components/MarketplaceView.d.ts +0 -2
  102. package/dist/components/MarketplaceView.js +0 -41
  103. package/dist/components/McpManager.d.ts +0 -9
  104. package/dist/components/McpManager.js +0 -88
  105. package/dist/components/MessageBlockItem.d.ts +0 -8
  106. package/dist/components/MessageBlockItem.js +0 -15
  107. package/dist/components/MessageList.d.ts +0 -10
  108. package/dist/components/MessageList.js +0 -80
  109. package/dist/components/ModelSelector.d.ts +0 -8
  110. package/dist/components/ModelSelector.js +0 -45
  111. package/dist/components/Notifications.d.ts +0 -7
  112. package/dist/components/Notifications.js +0 -9
  113. package/dist/components/PlanDisplay.d.ts +0 -7
  114. package/dist/components/PlanDisplay.js +0 -6
  115. package/dist/components/PluginDetail.d.ts +0 -2
  116. package/dist/components/PluginDetail.js +0 -87
  117. package/dist/components/PluginList.d.ts +0 -13
  118. package/dist/components/PluginList.js +0 -15
  119. package/dist/components/PluginManagerShell.d.ts +0 -6
  120. package/dist/components/PluginManagerShell.js +0 -92
  121. package/dist/components/PluginManagerTypes.d.ts +0 -35
  122. package/dist/components/PluginManagerTypes.js +0 -1
  123. package/dist/components/QueuedMessageList.d.ts +0 -2
  124. package/dist/components/QueuedMessageList.js +0 -18
  125. package/dist/components/ReasoningDisplay.d.ts +0 -8
  126. package/dist/components/ReasoningDisplay.js +0 -13
  127. package/dist/components/RewindCommand.d.ts +0 -12
  128. package/dist/components/RewindCommand.js +0 -78
  129. package/dist/components/SessionSelector.d.ts +0 -12
  130. package/dist/components/SessionSelector.js +0 -47
  131. package/dist/components/StatusCommand.d.ts +0 -5
  132. package/dist/components/StatusCommand.js +0 -28
  133. package/dist/components/StatusLine.d.ts +0 -6
  134. package/dist/components/StatusLine.js +0 -9
  135. package/dist/components/TaskList.d.ts +0 -5
  136. package/dist/components/TaskList.js +0 -197
  137. package/dist/components/TaskNotificationMessage.d.ts +0 -5
  138. package/dist/components/TaskNotificationMessage.js +0 -12
  139. package/dist/components/ToolDisplay.d.ts +0 -9
  140. package/dist/components/ToolDisplay.js +0 -55
  141. package/dist/components/WorkflowManager.d.ts +0 -5
  142. package/dist/components/WorkflowManager.js +0 -102
  143. package/dist/components/WorktreeExitPrompt.d.ts +0 -12
  144. package/dist/components/WorktreeExitPrompt.js +0 -35
  145. package/dist/constants/commands.d.ts +0 -2
  146. package/dist/constants/commands.js +0 -92
  147. package/dist/contexts/PluginManagerContext.d.ts +0 -3
  148. package/dist/contexts/PluginManagerContext.js +0 -9
  149. package/dist/contexts/useAppConfig.d.ts +0 -10
  150. package/dist/contexts/useAppConfig.js +0 -13
  151. package/dist/contexts/useChat.d.ts +0 -101
  152. package/dist/contexts/useChat.js +0 -992
  153. package/dist/contracts/status.d.ts +0 -7
  154. package/dist/contracts/status.js +0 -1
  155. package/dist/daemon/commands.d.ts +0 -49
  156. package/dist/daemon/commands.js +0 -341
  157. package/dist/daemon/jsonRpcClient.d.ts +0 -38
  158. package/dist/daemon/jsonRpcClient.js +0 -129
  159. package/dist/daemon/socketClient.d.ts +0 -13
  160. package/dist/daemon/socketClient.js +0 -26
  161. package/dist/daemon-cli.d.ts +0 -10
  162. package/dist/daemon-cli.js +0 -15
  163. package/dist/hooks/useInputManager.d.ts +0 -75
  164. package/dist/hooks/useInputManager.js +0 -583
  165. package/dist/hooks/usePluginManager.d.ts +0 -4
  166. package/dist/hooks/usePluginManager.js +0 -265
  167. package/dist/hooks/useTasks.d.ts +0 -1
  168. package/dist/hooks/useTasks.js +0 -5
  169. package/dist/index.d.ts +0 -4
  170. package/dist/index.js +0 -447
  171. package/dist/managers/inputHandlers.d.ts +0 -30
  172. package/dist/managers/inputHandlers.js +0 -625
  173. package/dist/managers/inputReducer.d.ts +0 -271
  174. package/dist/managers/inputReducer.js +0 -1121
  175. package/dist/plugin-manager-cli.d.ts +0 -5
  176. package/dist/plugin-manager-cli.js +0 -12
  177. package/dist/print-cli.d.ts +0 -10
  178. package/dist/print-cli.js +0 -209
  179. package/dist/reducers/agentsManagerReducer.d.ts +0 -26
  180. package/dist/reducers/agentsManagerReducer.js +0 -54
  181. package/dist/reducers/backgroundTaskManagerReducer.d.ts +0 -62
  182. package/dist/reducers/backgroundTaskManagerReducer.js +0 -106
  183. package/dist/reducers/confirmationReducer.d.ts +0 -37
  184. package/dist/reducers/confirmationReducer.js +0 -193
  185. package/dist/reducers/helpSelectorReducer.d.ts +0 -25
  186. package/dist/reducers/helpSelectorReducer.js +0 -52
  187. package/dist/reducers/marketplaceAddFormReducer.d.ts +0 -24
  188. package/dist/reducers/marketplaceAddFormReducer.js +0 -58
  189. package/dist/reducers/mcpManagerReducer.d.ts +0 -37
  190. package/dist/reducers/mcpManagerReducer.js +0 -74
  191. package/dist/reducers/pluginDetailReducer.d.ts +0 -32
  192. package/dist/reducers/pluginDetailReducer.js +0 -70
  193. package/dist/reducers/questionReducer.d.ts +0 -82
  194. package/dist/reducers/questionReducer.js +0 -375
  195. package/dist/reducers/rewindSelectorReducer.d.ts +0 -21
  196. package/dist/reducers/rewindSelectorReducer.js +0 -37
  197. package/dist/reducers/selectorReducer.d.ts +0 -23
  198. package/dist/reducers/selectorReducer.js +0 -42
  199. package/dist/reducers/workflowManagerReducer.d.ts +0 -40
  200. package/dist/reducers/workflowManagerReducer.js +0 -100
  201. package/dist/session-selector-cli.d.ts +0 -3
  202. package/dist/session-selector-cli.js +0 -25
  203. package/dist/stdio/agentBridge.d.ts +0 -139
  204. package/dist/stdio/agentBridge.js +0 -999
  205. package/dist/stdio/daemonServer.d.ts +0 -67
  206. package/dist/stdio/daemonServer.js +0 -191
  207. package/dist/stdio/index.d.ts +0 -5
  208. package/dist/stdio/index.js +0 -5
  209. package/dist/stdio/jsonRpcConnection.d.ts +0 -30
  210. package/dist/stdio/jsonRpcConnection.js +0 -127
  211. package/dist/stdio/protocol.d.ts +0 -41
  212. package/dist/stdio/protocol.js +0 -26
  213. package/dist/stdio/stdioServer.d.ts +0 -28
  214. package/dist/stdio/stdioServer.js +0 -36
  215. package/dist/stdio-cli.d.ts +0 -8
  216. package/dist/stdio-cli.js +0 -15
  217. package/dist/types.d.ts +0 -20
  218. package/dist/types.js +0 -1
  219. package/dist/utils/bracketedPaste.d.ts +0 -39
  220. package/dist/utils/bracketedPaste.js +0 -122
  221. package/dist/utils/clipboard.d.ts +0 -21
  222. package/dist/utils/clipboard.js +0 -347
  223. package/dist/utils/constants.d.ts +0 -16
  224. package/dist/utils/constants.js +0 -18
  225. package/dist/utils/highlightUtils.d.ts +0 -1
  226. package/dist/utils/highlightUtils.js +0 -93
  227. package/dist/utils/inputUtils.d.ts +0 -17
  228. package/dist/utils/inputUtils.js +0 -67
  229. package/dist/utils/logger.d.ts +0 -71
  230. package/dist/utils/logger.js +0 -259
  231. package/dist/utils/markdownTable.d.ts +0 -34
  232. package/dist/utils/markdownTable.js +0 -302
  233. package/dist/utils/rewindCheckpoints.d.ts +0 -8
  234. package/dist/utils/rewindCheckpoints.js +0 -17
  235. package/dist/utils/throttle.d.ts +0 -14
  236. package/dist/utils/throttle.js +0 -55
  237. package/dist/utils/toolParameterTransforms.d.ts +0 -18
  238. package/dist/utils/toolParameterTransforms.js +0 -73
  239. package/dist/utils/usageSummary.d.ts +0 -28
  240. package/dist/utils/usageSummary.js +0 -121
  241. package/dist/utils/version.d.ts +0 -1
  242. package/dist/utils/version.js +0 -4
  243. package/dist/utils/worktree.d.ts +0 -35
  244. package/dist/utils/worktree.js +0 -219
@@ -1,41 +1,282 @@
1
- import React from "react";
2
- import { render, Box } from "ink";
3
- import { listSessions, truncateContent } from "wave-agent-sdk";
1
+ import React, { useState, useCallback, useEffect, useRef } from "react";
2
+ import { render, Box, Text } from "ink";
3
+ import { existsSync } from "fs";
4
+ import {
5
+ PathEncoder,
6
+ listSessions,
7
+ listAllSessions,
8
+ truncateContent,
9
+ type SessionMetadata,
10
+ } from "wave-agent-sdk";
4
11
  import { SessionSelector } from "./components/SessionSelector.js";
12
+ import { setClipboardText } from "./utils/clipboard.js";
13
+
14
+ export interface SessionSelectorResult {
15
+ sessionId: string;
16
+ /**
17
+ * When the selected session belongs to a sibling git worktree of the same
18
+ * repo, the caller should chdir into this path before restoring.
19
+ */
20
+ resumeWorkdir?: string;
21
+ }
22
+
23
+ type SessionItem = SessionMetadata & { firstMessage?: string };
24
+
25
+ /** POSIX single-quote wrapping for use inside a `cd <path> && ...` command. */
26
+ function quotePath(p: string): string {
27
+ return `'${p.replace(/'/g, `'\\''`)}'`;
28
+ }
29
+
30
+ export type SessionOwnership =
31
+ | { kind: "current" }
32
+ | { kind: "worktree"; resumeWorkdir: string }
33
+ | { kind: "cross-project"; command: string };
34
+
35
+ /**
36
+ * Decide how a selected session relates to the current working directory.
37
+ *
38
+ * Ownership is decided by comparing encoded directory names — the decoded
39
+ * workdir is lossy for paths containing "-", but re-encoding it reproduces the
40
+ * original encoded directory name exactly.
41
+ *
42
+ * - current: session belongs to the current directory → resume in place
43
+ * - worktree: session belongs to a sibling same-repo worktree → resume
44
+ * after chdir into it (skipped when the worktree dir is gone)
45
+ * - cross-project: session belongs to another project → print a cd command
46
+ */
47
+ export async function resolveSessionOwnership(
48
+ session: { id: string; workdir: string },
49
+ opts: { worktreePaths: string[]; currentWorkdir: string },
50
+ ): Promise<SessionOwnership> {
51
+ const encoder = new PathEncoder();
52
+ // encode() resolves the path via realpath and throws ENOENT when the
53
+ // directory no longer exists (e.g. a deleted worktree); fall back to the
54
+ // pure string encodeSync so the picker still works for ghost sessions.
55
+ let sessionEncoded: string;
56
+ try {
57
+ sessionEncoded = await encoder.encode(session.workdir);
58
+ } catch {
59
+ sessionEncoded = encoder.encodeSync(session.workdir);
60
+ }
61
+ const cwdEncoded = await encoder.encode(opts.currentWorkdir);
62
+
63
+ if (sessionEncoded === cwdEncoded) {
64
+ return { kind: "current" };
65
+ }
66
+
67
+ for (const wt of opts.worktreePaths) {
68
+ let wtEncoded: string;
69
+ try {
70
+ wtEncoded = await encoder.encode(wt);
71
+ } catch {
72
+ wtEncoded = encoder.encodeSync(wt);
73
+ }
74
+ if (wtEncoded === sessionEncoded && existsSync(wt)) {
75
+ return { kind: "worktree", resumeWorkdir: wt };
76
+ }
77
+ }
78
+
79
+ return {
80
+ kind: "cross-project",
81
+ command: `cd ${quotePath(session.workdir)} && wave --restore ${session.id}`,
82
+ };
83
+ }
84
+
85
+ /**
86
+ * Interactive session picker for `wave -r` (restore without an ID).
87
+ *
88
+ * Three scopes, toggled with Ctrl+A / Ctrl+W (mirroring Claude Code's resume
89
+ * picker):
90
+ * - current: sessions in the current working directory (default)
91
+ * - worktrees: sessions in any worktree of the same git repository
92
+ * - all: sessions across every project directory
93
+ *
94
+ * Resume decision on selection:
95
+ * - current dir session → resume in place
96
+ * - same-repo worktree → resume after chdir into that worktree
97
+ * - other project → print `cd <path> && wave --restore <id>`,
98
+ * copy it to the clipboard, and exit
99
+ */
100
+ function SessionSelectorFlow({
101
+ currentWorkdir,
102
+ worktreePaths,
103
+ onDone,
104
+ }: {
105
+ currentWorkdir: string;
106
+ worktreePaths: string[];
107
+ onDone: (result: SessionSelectorResult | null) => void;
108
+ }): React.ReactNode {
109
+ const [showAllProjects, setShowAllProjects] = useState(false);
110
+ const [showAllWorktrees, setShowAllWorktrees] = useState(false);
111
+ const [sessions, setSessions] = useState<SessionItem[]>([]);
112
+ const [loading, setLoading] = useState(true);
113
+ const [crossProjectCommand, setCrossProjectCommand] = useState<string | null>(
114
+ null,
115
+ );
116
+
117
+ // Mirror of the scope flags for use inside async reload/decision callbacks.
118
+ const scopeRef = useRef({ showAllProjects: false, showAllWorktrees: false });
119
+
120
+ const loadSessions = useCallback(
121
+ async (allProjects: boolean, allWorktrees: boolean) => {
122
+ if (allProjects) {
123
+ return listAllSessions();
124
+ }
125
+ if (allWorktrees) {
126
+ return listAllSessions({ worktreePaths, workdir: currentWorkdir });
127
+ }
128
+ return listSessions(currentWorkdir);
129
+ },
130
+ [worktreePaths, currentWorkdir],
131
+ );
132
+
133
+ const reload = useCallback(
134
+ async (allProjects: boolean, allWorktrees: boolean) => {
135
+ setLoading(true);
136
+ try {
137
+ const result = await loadSessions(allProjects, allWorktrees);
138
+ setSessions(
139
+ result.map((s) => ({
140
+ ...s,
141
+ firstMessage: truncateContent(s.firstMessage || "No content", 80),
142
+ })),
143
+ );
144
+ } finally {
145
+ setLoading(false);
146
+ }
147
+ },
148
+ [loadSessions],
149
+ );
150
+
151
+ // Initial load: current directory scope
152
+ useEffect(() => {
153
+ void reload(false, false);
154
+ }, [reload]);
155
+
156
+ const handleToggleAllProjects = useCallback(
157
+ (value: boolean) => {
158
+ scopeRef.current.showAllProjects = value;
159
+ setShowAllProjects(value);
160
+ void reload(value, scopeRef.current.showAllWorktrees);
161
+ },
162
+ [reload],
163
+ );
164
+
165
+ const handleToggleAllWorktrees = useCallback(
166
+ (value: boolean) => {
167
+ scopeRef.current.showAllWorktrees = value;
168
+ setShowAllWorktrees(value);
169
+ void reload(scopeRef.current.showAllProjects, value);
170
+ },
171
+ [reload],
172
+ );
173
+
174
+ const handleSelect = useCallback(
175
+ async (sessionId: string) => {
176
+ const session = sessions.find((s) => s.id === sessionId);
177
+ if (!session) return;
178
+
179
+ const ownership = await resolveSessionOwnership(session, {
180
+ worktreePaths,
181
+ currentWorkdir,
182
+ });
183
+
184
+ switch (ownership.kind) {
185
+ case "current":
186
+ onDone({ sessionId });
187
+ return;
188
+ case "worktree":
189
+ onDone({ sessionId, resumeWorkdir: ownership.resumeWorkdir });
190
+ return;
191
+ case "cross-project":
192
+ // Different project — show the cd command instead of resuming in place
193
+ await setClipboardText(ownership.command);
194
+ setCrossProjectCommand(ownership.command);
195
+ return;
196
+ }
197
+ },
198
+ [sessions, worktreePaths, currentWorkdir, onDone],
199
+ );
200
+
201
+ if (crossProjectCommand) {
202
+ return (
203
+ <CrossProjectMessage
204
+ command={crossProjectCommand}
205
+ onExit={() => onDone(null)}
206
+ />
207
+ );
208
+ }
209
+
210
+ if (loading) {
211
+ return (
212
+ <Box>
213
+ <Text dimColor>Loading conversations…</Text>
214
+ </Box>
215
+ );
216
+ }
217
+
218
+ return (
219
+ <Box padding={1}>
220
+ <SessionSelector
221
+ sessions={sessions}
222
+ worktreePaths={worktreePaths}
223
+ showProjectPath={showAllProjects || showAllWorktrees}
224
+ onSelect={(id) => void handleSelect(id)}
225
+ onCancel={() => onDone(null)}
226
+ onToggleAllProjects={handleToggleAllProjects}
227
+ onToggleAllWorktrees={handleToggleAllWorktrees}
228
+ />
229
+ </Box>
230
+ );
231
+ }
232
+
233
+ /**
234
+ * Cross-project message auto-exits after a short delay (mirrors Claude Code's
235
+ * resume picker, which prints the cd command and exits).
236
+ */
237
+ function CrossProjectMessage({
238
+ command,
239
+ onExit,
240
+ }: {
241
+ command: string;
242
+ onExit: () => void;
243
+ }): React.ReactNode {
244
+ useEffect(() => {
245
+ const timer = setTimeout(onExit, 100);
246
+ return () => clearTimeout(timer);
247
+ }, [onExit]);
248
+
249
+ return (
250
+ <Box flexDirection="column" gap={1}>
251
+ <Text>This conversation is from a different directory.</Text>
252
+ <Box flexDirection="column">
253
+ <Text>To resume, run:</Text>
254
+ <Text> {command}</Text>
255
+ </Box>
256
+ <Text dimColor>(Command copied to clipboard)</Text>
257
+ </Box>
258
+ );
259
+ }
5
260
 
6
261
  export async function startSessionSelectorCli({
7
262
  workdir,
263
+ worktreePaths = [],
8
264
  }: {
9
265
  workdir?: string;
10
- } = {}): Promise<string | null> {
266
+ worktreePaths?: string[];
267
+ } = {}): Promise<SessionSelectorResult | null> {
11
268
  const currentWorkdir = workdir || process.cwd();
12
- const sessions = await listSessions(currentWorkdir);
13
-
14
- if (sessions.length === 0) {
15
- console.log(`No sessions found for workdir: ${currentWorkdir}`);
16
- return null;
17
- }
18
-
19
- const sessionsWithContent = sessions.map((s) => ({
20
- ...s,
21
- firstMessage: truncateContent(s.firstMessage || "No content", 80),
22
- }));
23
269
 
24
270
  return new Promise((resolve) => {
25
271
  const { unmount } = render(
26
- <Box padding={1}>
27
- <SessionSelector
28
- sessions={sessionsWithContent}
29
- onSelect={(sessionId) => {
30
- unmount();
31
- resolve(sessionId);
32
- }}
33
- onCancel={() => {
34
- unmount();
35
- resolve(null);
36
- }}
37
- />
38
- </Box>,
272
+ <SessionSelectorFlow
273
+ currentWorkdir={currentWorkdir}
274
+ worktreePaths={worktreePaths}
275
+ onDone={(result) => {
276
+ unmount();
277
+ resolve(result);
278
+ }}
279
+ />,
39
280
  );
40
281
  });
41
282
  }
@@ -375,7 +375,12 @@ export class AgentBridge {
375
375
  );
376
376
  case "removeWorktree":
377
377
  return this.removeWorktreeSession(
378
- p as unknown as { path: string; branch: string; repoRoot: string },
378
+ p as unknown as {
379
+ path: string;
380
+ branch: string;
381
+ repoRoot: string;
382
+ hookBased?: boolean;
383
+ },
379
384
  );
380
385
 
381
386
  default:
@@ -615,6 +620,7 @@ export class AgentBridge {
615
620
  repoRoot: string;
616
621
  baseBranch: string;
617
622
  isNew: boolean;
623
+ hookBased: boolean;
618
624
  }> {
619
625
  if (!params.workdir) {
620
626
  throw new RpcError(PROTOCOL_INTERNAL_ERROR, "workdir is required");
@@ -631,6 +637,7 @@ export class AgentBridge {
631
637
  repoRoot: session.repoRoot,
632
638
  baseBranch: params.baseBranch ?? getDefaultRemoteBranch(params.workdir),
633
639
  isNew: session.isNew,
640
+ hookBased: session.hookBased ?? false,
634
641
  };
635
642
  } catch (e) {
636
643
  throw new RpcError(PROTOCOL_INTERNAL_ERROR, (e as Error).message);
@@ -641,31 +648,24 @@ export class AgentBridge {
641
648
  path: string;
642
649
  branch: string;
643
650
  repoRoot: string;
651
+ hookBased?: boolean;
644
652
  }): Promise<{ ok: true }> {
645
- // Align with Claude Code v2.1.216+: refuse to remove a worktree whose path
646
- // is a symlink or resolves outside the repo root. Already-removed (missing)
647
- // paths pass validation so removal stays idempotent.
648
- try {
649
- validateWorktreeRemovalPath(params.path, params.repoRoot);
650
- } catch (e) {
651
- throw new RpcError(PROTOCOL_INTERNAL_ERROR, (e as Error).message);
652
- }
653
-
654
- // Trigger the WorktreeRemove hook (before git removal, non-blocking) using
655
- // the session that runs in this worktree, if it is still registered.
656
- for (const entry of this.sessions.values()) {
657
- if (entry.agent.workingDirectory === params.path) {
658
- try {
659
- await entry.agent.triggerWorktreeRemoveHook(params.path);
660
- } catch (e) {
661
- logger.warn("WorktreeRemove hooks execution failed:", e);
662
- }
663
- break;
653
+ // Hook-based worktrees are owned by the WorktreeRemove hook; the git-root
654
+ // containment check does not apply to them.
655
+ if (!params.hookBased) {
656
+ // Align with Claude Code v2.1.216+: refuse to remove a worktree whose
657
+ // path is a symlink or resolves outside the repo root. Already-removed
658
+ // (missing) paths pass validation so removal stays idempotent.
659
+ try {
660
+ validateWorktreeRemovalPath(params.path, params.repoRoot);
661
+ } catch (e) {
662
+ throw new RpcError(PROTOCOL_INTERNAL_ERROR, (e as Error).message);
664
663
  }
665
664
  }
666
665
 
667
666
  // removeWorktree is best-effort/idempotent: already-removed worktrees or
668
- // branches only log, never throw.
667
+ // branches only log, never throw. Hook-based worktrees go through the
668
+ // WorktreeRemove hook inside removeWorktree.
669
669
  await removeWorktree({
670
670
  name: "",
671
671
  path: params.path,
@@ -674,6 +674,7 @@ export class AgentBridge {
674
674
  hasUncommittedChanges: false,
675
675
  hasNewCommits: false,
676
676
  isNew: false,
677
+ hookBased: params.hookBased,
677
678
  });
678
679
  return { ok: true };
679
680
  }
@@ -104,7 +104,9 @@ export class DaemonServer {
104
104
  probe.once("connect", () => {
105
105
  probe.destroy();
106
106
  reject(
107
- new Error(`另一个 wave daemon 已在 ${this.socketPath} 监听`),
107
+ new Error(
108
+ `Another wave daemon is already listening on ${this.socketPath}`,
109
+ ),
108
110
  );
109
111
  });
110
112
  probe.once("error", (err: NodeJS.ErrnoException) => {
@@ -22,15 +22,28 @@ import {
22
22
  isNotification,
23
23
  } from "./protocol.js";
24
24
 
25
+ export interface JsonRpcConnectionOptions {
26
+ /** Called when the input stream reaches EOF (parent closed the pipe). Only
27
+ * stdio mode passes it — it must exit the process when its client dies;
28
+ * daemon mode never passes it (a detached client must not kill the daemon,
29
+ * see daemonServer.ts). */
30
+ onClose?: () => void;
31
+ }
32
+
25
33
  export class JsonRpcConnection {
26
34
  private rl: readline.Interface | undefined;
27
35
  private started = false;
36
+ private stoppedByOwner = false;
37
+ private onClose?: () => void;
28
38
 
29
39
  constructor(
30
40
  private input: Readable,
31
41
  private output: Writable,
32
42
  private bridge: AgentBridge,
33
- ) {}
43
+ options: JsonRpcConnectionOptions = {},
44
+ ) {
45
+ this.onClose = options.onClose;
46
+ }
34
47
 
35
48
  start(): void {
36
49
  if (this.started) return;
@@ -58,10 +71,15 @@ export class JsonRpcConnection {
58
71
 
59
72
  this.rl.on("close", () => {
60
73
  this.started = false;
74
+ // stdin EOF from the owning client — not a deliberate stop(). stdio
75
+ // mode reacts by exiting the process (its parent is gone); daemon mode
76
+ // has no onClose so a detached client reset never kills the daemon.
77
+ if (!this.stoppedByOwner) this.onClose?.();
61
78
  });
62
79
  }
63
80
 
64
81
  stop(): void {
82
+ this.stoppedByOwner = true;
65
83
  this.rl?.close();
66
84
  this.rl = undefined;
67
85
  this.started = false;
@@ -30,6 +30,7 @@ export class StdioServer {
30
30
  options.input ?? process.stdin,
31
31
  options.output ?? process.stdout,
32
32
  this.bridge,
33
+ { onClose: () => this.onConnectionClosed() },
33
34
  );
34
35
  }
35
36
 
@@ -45,6 +46,22 @@ export class StdioServer {
45
46
  this.conn.stop();
46
47
  }
47
48
 
49
+ /**
50
+ * The owning client closed stdin (EOF) — it's gone, so this process must
51
+ * follow it. destroyAll() saves each session's transcript and drains
52
+ * auto-memory first; a timeout bounds it so a stuck agent can't orphan the
53
+ * exit. This is the real-world "parent died" path: without it the process
54
+ * lingers forever whenever an agent keeps event-loop handles alive.
55
+ */
56
+ private async onConnectionClosed(): Promise<void> {
57
+ const SHUTDOWN_TIMEOUT_MS = 5_000;
58
+ await Promise.race([
59
+ this.bridge.destroyAll(),
60
+ new Promise((resolve) => setTimeout(resolve, SHUTDOWN_TIMEOUT_MS)),
61
+ ]);
62
+ process.exit(0);
63
+ }
64
+
48
65
  handleLine(line: string): Promise<void> {
49
66
  return this.conn.handleLine(line);
50
67
  }
@@ -9,6 +9,27 @@ export interface ClipboardImageResult {
9
9
  mimeType?: string;
10
10
  }
11
11
 
12
+ /**
13
+ * Write text to the system clipboard via the terminal OSC 52 escape sequence.
14
+ * Works in most modern terminals without external tools (mirrors Claude
15
+ * Code's setClipboard). Callers must treat a `false` return as "clipboard
16
+ * unavailable" and still surface the text itself.
17
+ *
18
+ * @param text - Text to copy
19
+ * @returns Promise resolving to true when the sequence was written
20
+ */
21
+ export async function setClipboardText(text: string): Promise<boolean> {
22
+ try {
23
+ process.stdout.write(
24
+ `\x1b]52;c;${Buffer.from(text, "utf-8").toString("base64")}\x07`,
25
+ );
26
+ return true;
27
+ } catch {
28
+ // stdout unavailable — clipboard could not be set
29
+ return false;
30
+ }
31
+ }
32
+
12
33
  /**
13
34
  * Read image from clipboard
14
35
  * @returns Promise<ClipboardImageResult> Result containing image path or error information
@@ -1,7 +1,52 @@
1
- import hljs from "highlight.js";
1
+ import hljs from "highlight.js/lib/core";
2
+ import javascript from "highlight.js/lib/languages/javascript";
3
+ import typescript from "highlight.js/lib/languages/typescript";
4
+ import bash from "highlight.js/lib/languages/bash";
5
+ import json from "highlight.js/lib/languages/json";
6
+ import markdown from "highlight.js/lib/languages/markdown";
7
+ import python from "highlight.js/lib/languages/python";
8
+ import yaml from "highlight.js/lib/languages/yaml";
9
+ import xml from "highlight.js/lib/languages/xml";
10
+ import css from "highlight.js/lib/languages/css";
11
+ import sql from "highlight.js/lib/languages/sql";
12
+ import rust from "highlight.js/lib/languages/rust";
13
+ import go from "highlight.js/lib/languages/go";
14
+ import java from "highlight.js/lib/languages/java";
15
+ import cpp from "highlight.js/lib/languages/cpp";
16
+ import c from "highlight.js/lib/languages/c";
17
+ import csharp from "highlight.js/lib/languages/csharp";
18
+ import php from "highlight.js/lib/languages/php";
19
+ import ruby from "highlight.js/lib/languages/ruby";
20
+ import swift from "highlight.js/lib/languages/swift";
21
+ import kotlin from "highlight.js/lib/languages/kotlin";
22
+ import ini from "highlight.js/lib/languages/ini";
2
23
  import { parse, Node, HTMLElement, TextNode } from "node-html-parser";
3
24
  import chalk from "chalk";
4
25
 
26
+ // Register only the languages we auto-detect (the full default import pulls
27
+ // in all 384 languages, ~1.5MB of the bundle).
28
+ hljs.registerLanguage("javascript", javascript);
29
+ hljs.registerLanguage("typescript", typescript);
30
+ hljs.registerLanguage("bash", bash);
31
+ hljs.registerLanguage("json", json);
32
+ hljs.registerLanguage("markdown", markdown);
33
+ hljs.registerLanguage("python", python);
34
+ hljs.registerLanguage("yaml", yaml);
35
+ hljs.registerLanguage("xml", xml);
36
+ hljs.registerLanguage("css", css);
37
+ hljs.registerLanguage("sql", sql);
38
+ hljs.registerLanguage("rust", rust);
39
+ hljs.registerLanguage("go", go);
40
+ hljs.registerLanguage("java", java);
41
+ hljs.registerLanguage("cpp", cpp);
42
+ hljs.registerLanguage("c", c);
43
+ hljs.registerLanguage("csharp", csharp);
44
+ hljs.registerLanguage("php", php);
45
+ hljs.registerLanguage("ruby", ruby);
46
+ hljs.registerLanguage("swift", swift);
47
+ hljs.registerLanguage("kotlin", kotlin);
48
+ hljs.registerLanguage("ini", ini);
49
+
5
50
  const theme: Record<string, (text: string) => string> = {
6
51
  "hljs-keyword": chalk.blue,
7
52
  "hljs-built_in": chalk.cyan,
@@ -0,0 +1,29 @@
1
+ import { readFileSync } from "fs";
2
+ import path from "path";
3
+ import { fileURLToPath } from "url";
4
+
5
+ /**
6
+ * Reads the nearest package.json by walking up from this module's location.
7
+ * Works for both the tsc output (dist/<subdir>/...) and the single-file esbuild
8
+ * bundle (dist/bundle/wave.mjs), whose relative depths differ.
9
+ */
10
+ export function readNearestPackageJson(): {
11
+ name: string;
12
+ version: string;
13
+ [key: string]: unknown;
14
+ } {
15
+ let dir = path.dirname(fileURLToPath(import.meta.url));
16
+ for (;;) {
17
+ const candidate = path.join(dir, "package.json");
18
+ try {
19
+ return JSON.parse(readFileSync(candidate, "utf-8"));
20
+ } catch {
21
+ // Not here — keep walking up.
22
+ }
23
+ const parent = path.dirname(dir);
24
+ if (parent === dir) {
25
+ throw new Error("package.json not found above this module");
26
+ }
27
+ dir = parent;
28
+ }
29
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Single-line tail representation of in-flight streaming text: newlines are
3
+ * flattened to `\n` and, when longer than `maxLen` characters, only the last
4
+ * `maxLen` characters are kept behind an ellipsis. Shared by the streaming
5
+ * displays (assistant text, reasoning, tool parameters) and the loading
6
+ * indicators (compaction / /btw) so the tail style stays consistent.
7
+ */
8
+ export const streamingTail = (content: string, maxLen = 30): string => {
9
+ const flat = content.replace(/\n/g, "\\n");
10
+ return flat.length > maxLen ? `…${flat.slice(-maxLen)}` : flat;
11
+ };