wave-code 1.0.10 → 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 (242) 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 +156 -335
  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/utils/clipboard.ts +21 -0
  31. package/src/utils/highlightUtils.ts +46 -1
  32. package/src/utils/readPackageJson.ts +29 -0
  33. package/src/utils/streamingText.ts +11 -0
  34. package/src/utils/worktree.ts +191 -43
  35. package/dist/cli.d.ts +0 -6
  36. package/dist/cli.js +0 -53
  37. package/dist/commands/plugin/disable.d.ts +0 -5
  38. package/dist/commands/plugin/disable.js +0 -15
  39. package/dist/commands/plugin/enable.d.ts +0 -5
  40. package/dist/commands/plugin/enable.js +0 -15
  41. package/dist/commands/plugin/install.d.ts +0 -5
  42. package/dist/commands/plugin/install.js +0 -21
  43. package/dist/commands/plugin/list.d.ts +0 -1
  44. package/dist/commands/plugin/list.js +0 -31
  45. package/dist/commands/plugin/marketplace.d.ts +0 -10
  46. package/dist/commands/plugin/marketplace.js +0 -90
  47. package/dist/commands/plugin/uninstall.d.ts +0 -3
  48. package/dist/commands/plugin/uninstall.js +0 -16
  49. package/dist/commands/plugin/update.d.ts +0 -3
  50. package/dist/commands/plugin/update.js +0 -15
  51. package/dist/commands/update.d.ts +0 -1
  52. package/dist/commands/update.js +0 -97
  53. package/dist/components/AgentsManager.d.ts +0 -7
  54. package/dist/components/AgentsManager.js +0 -109
  55. package/dist/components/App.d.ts +0 -9
  56. package/dist/components/App.js +0 -76
  57. package/dist/components/BackgroundTaskManager.d.ts +0 -5
  58. package/dist/components/BackgroundTaskManager.js +0 -99
  59. package/dist/components/BangDisplay.d.ts +0 -8
  60. package/dist/components/BangDisplay.js +0 -17
  61. package/dist/components/BtwDisplay.d.ts +0 -7
  62. package/dist/components/BtwDisplay.js +0 -19
  63. package/dist/components/ChatInterface.d.ts +0 -2
  64. package/dist/components/ChatInterface.js +0 -61
  65. package/dist/components/CommandSelector.d.ts +0 -10
  66. package/dist/components/CommandSelector.js +0 -66
  67. package/dist/components/CompactDisplay.d.ts +0 -8
  68. package/dist/components/CompactDisplay.js +0 -13
  69. package/dist/components/ConfirmationDetails.d.ts +0 -9
  70. package/dist/components/ConfirmationDetails.js +0 -45
  71. package/dist/components/ConfirmationSelector.d.ts +0 -13
  72. package/dist/components/ConfirmationSelector.js +0 -127
  73. package/dist/components/DiffDisplay.d.ts +0 -8
  74. package/dist/components/DiffDisplay.js +0 -201
  75. package/dist/components/DiscoverView.d.ts +0 -2
  76. package/dist/components/DiscoverView.js +0 -37
  77. package/dist/components/FileSelector.d.ts +0 -11
  78. package/dist/components/FileSelector.js +0 -50
  79. package/dist/components/HelpView.d.ts +0 -7
  80. package/dist/components/HelpView.js +0 -74
  81. package/dist/components/HistorySearch.d.ts +0 -8
  82. package/dist/components/HistorySearch.js +0 -66
  83. package/dist/components/InputBox.d.ts +0 -24
  84. package/dist/components/InputBox.js +0 -149
  85. package/dist/components/InstalledView.d.ts +0 -2
  86. package/dist/components/InstalledView.js +0 -45
  87. package/dist/components/LoadingIndicator.d.ts +0 -10
  88. package/dist/components/LoadingIndicator.js +0 -6
  89. package/dist/components/LoginCommand.d.ts +0 -5
  90. package/dist/components/LoginCommand.js +0 -153
  91. package/dist/components/Markdown.d.ts +0 -5
  92. package/dist/components/Markdown.js +0 -127
  93. package/dist/components/MarketplaceAddForm.d.ts +0 -2
  94. package/dist/components/MarketplaceAddForm.js +0 -55
  95. package/dist/components/MarketplaceDetail.d.ts +0 -2
  96. package/dist/components/MarketplaceDetail.js +0 -74
  97. package/dist/components/MarketplaceList.d.ts +0 -8
  98. package/dist/components/MarketplaceList.js +0 -17
  99. package/dist/components/MarketplaceView.d.ts +0 -2
  100. package/dist/components/MarketplaceView.js +0 -41
  101. package/dist/components/McpManager.d.ts +0 -9
  102. package/dist/components/McpManager.js +0 -88
  103. package/dist/components/MessageBlockItem.d.ts +0 -8
  104. package/dist/components/MessageBlockItem.js +0 -15
  105. package/dist/components/MessageList.d.ts +0 -10
  106. package/dist/components/MessageList.js +0 -80
  107. package/dist/components/ModelSelector.d.ts +0 -8
  108. package/dist/components/ModelSelector.js +0 -45
  109. package/dist/components/Notifications.d.ts +0 -7
  110. package/dist/components/Notifications.js +0 -9
  111. package/dist/components/PlanDisplay.d.ts +0 -7
  112. package/dist/components/PlanDisplay.js +0 -6
  113. package/dist/components/PluginDetail.d.ts +0 -2
  114. package/dist/components/PluginDetail.js +0 -87
  115. package/dist/components/PluginList.d.ts +0 -13
  116. package/dist/components/PluginList.js +0 -15
  117. package/dist/components/PluginManagerShell.d.ts +0 -6
  118. package/dist/components/PluginManagerShell.js +0 -92
  119. package/dist/components/PluginManagerTypes.d.ts +0 -35
  120. package/dist/components/PluginManagerTypes.js +0 -1
  121. package/dist/components/QueuedMessageList.d.ts +0 -2
  122. package/dist/components/QueuedMessageList.js +0 -18
  123. package/dist/components/ReasoningDisplay.d.ts +0 -8
  124. package/dist/components/ReasoningDisplay.js +0 -13
  125. package/dist/components/RewindCommand.d.ts +0 -12
  126. package/dist/components/RewindCommand.js +0 -78
  127. package/dist/components/SessionSelector.d.ts +0 -12
  128. package/dist/components/SessionSelector.js +0 -47
  129. package/dist/components/StatusCommand.d.ts +0 -5
  130. package/dist/components/StatusCommand.js +0 -28
  131. package/dist/components/StatusLine.d.ts +0 -6
  132. package/dist/components/StatusLine.js +0 -9
  133. package/dist/components/TaskList.d.ts +0 -5
  134. package/dist/components/TaskList.js +0 -197
  135. package/dist/components/TaskNotificationMessage.d.ts +0 -5
  136. package/dist/components/TaskNotificationMessage.js +0 -12
  137. package/dist/components/ToolDisplay.d.ts +0 -9
  138. package/dist/components/ToolDisplay.js +0 -55
  139. package/dist/components/WorkflowManager.d.ts +0 -5
  140. package/dist/components/WorkflowManager.js +0 -102
  141. package/dist/components/WorktreeExitPrompt.d.ts +0 -12
  142. package/dist/components/WorktreeExitPrompt.js +0 -35
  143. package/dist/constants/commands.d.ts +0 -2
  144. package/dist/constants/commands.js +0 -92
  145. package/dist/contexts/PluginManagerContext.d.ts +0 -3
  146. package/dist/contexts/PluginManagerContext.js +0 -9
  147. package/dist/contexts/useAppConfig.d.ts +0 -10
  148. package/dist/contexts/useAppConfig.js +0 -13
  149. package/dist/contexts/useChat.d.ts +0 -101
  150. package/dist/contexts/useChat.js +0 -993
  151. package/dist/contracts/status.d.ts +0 -7
  152. package/dist/contracts/status.js +0 -1
  153. package/dist/daemon/commands.d.ts +0 -49
  154. package/dist/daemon/commands.js +0 -341
  155. package/dist/daemon/jsonRpcClient.d.ts +0 -38
  156. package/dist/daemon/jsonRpcClient.js +0 -129
  157. package/dist/daemon/socketClient.d.ts +0 -13
  158. package/dist/daemon/socketClient.js +0 -26
  159. package/dist/daemon-cli.d.ts +0 -10
  160. package/dist/daemon-cli.js +0 -15
  161. package/dist/hooks/useInputManager.d.ts +0 -75
  162. package/dist/hooks/useInputManager.js +0 -583
  163. package/dist/hooks/usePluginManager.d.ts +0 -4
  164. package/dist/hooks/usePluginManager.js +0 -265
  165. package/dist/hooks/useTasks.d.ts +0 -1
  166. package/dist/hooks/useTasks.js +0 -5
  167. package/dist/index.d.ts +0 -4
  168. package/dist/index.js +0 -447
  169. package/dist/managers/inputHandlers.d.ts +0 -30
  170. package/dist/managers/inputHandlers.js +0 -625
  171. package/dist/managers/inputReducer.d.ts +0 -271
  172. package/dist/managers/inputReducer.js +0 -1121
  173. package/dist/plugin-manager-cli.d.ts +0 -5
  174. package/dist/plugin-manager-cli.js +0 -12
  175. package/dist/print-cli.d.ts +0 -10
  176. package/dist/print-cli.js +0 -209
  177. package/dist/reducers/agentsManagerReducer.d.ts +0 -26
  178. package/dist/reducers/agentsManagerReducer.js +0 -54
  179. package/dist/reducers/backgroundTaskManagerReducer.d.ts +0 -62
  180. package/dist/reducers/backgroundTaskManagerReducer.js +0 -106
  181. package/dist/reducers/confirmationReducer.d.ts +0 -37
  182. package/dist/reducers/confirmationReducer.js +0 -193
  183. package/dist/reducers/helpSelectorReducer.d.ts +0 -25
  184. package/dist/reducers/helpSelectorReducer.js +0 -52
  185. package/dist/reducers/marketplaceAddFormReducer.d.ts +0 -24
  186. package/dist/reducers/marketplaceAddFormReducer.js +0 -58
  187. package/dist/reducers/mcpManagerReducer.d.ts +0 -37
  188. package/dist/reducers/mcpManagerReducer.js +0 -74
  189. package/dist/reducers/pluginDetailReducer.d.ts +0 -32
  190. package/dist/reducers/pluginDetailReducer.js +0 -70
  191. package/dist/reducers/questionReducer.d.ts +0 -82
  192. package/dist/reducers/questionReducer.js +0 -375
  193. package/dist/reducers/rewindSelectorReducer.d.ts +0 -21
  194. package/dist/reducers/rewindSelectorReducer.js +0 -37
  195. package/dist/reducers/selectorReducer.d.ts +0 -23
  196. package/dist/reducers/selectorReducer.js +0 -42
  197. package/dist/reducers/workflowManagerReducer.d.ts +0 -40
  198. package/dist/reducers/workflowManagerReducer.js +0 -100
  199. package/dist/session-selector-cli.d.ts +0 -3
  200. package/dist/session-selector-cli.js +0 -25
  201. package/dist/stdio/agentBridge.d.ts +0 -139
  202. package/dist/stdio/agentBridge.js +0 -999
  203. package/dist/stdio/daemonServer.d.ts +0 -67
  204. package/dist/stdio/daemonServer.js +0 -191
  205. package/dist/stdio/index.d.ts +0 -5
  206. package/dist/stdio/index.js +0 -5
  207. package/dist/stdio/jsonRpcConnection.d.ts +0 -39
  208. package/dist/stdio/jsonRpcConnection.js +0 -135
  209. package/dist/stdio/protocol.d.ts +0 -41
  210. package/dist/stdio/protocol.js +0 -26
  211. package/dist/stdio/stdioServer.d.ts +0 -36
  212. package/dist/stdio/stdioServer.js +0 -51
  213. package/dist/stdio-cli.d.ts +0 -8
  214. package/dist/stdio-cli.js +0 -15
  215. package/dist/types.d.ts +0 -20
  216. package/dist/types.js +0 -1
  217. package/dist/utils/bracketedPaste.d.ts +0 -39
  218. package/dist/utils/bracketedPaste.js +0 -122
  219. package/dist/utils/clipboard.d.ts +0 -21
  220. package/dist/utils/clipboard.js +0 -347
  221. package/dist/utils/constants.d.ts +0 -16
  222. package/dist/utils/constants.js +0 -18
  223. package/dist/utils/highlightUtils.d.ts +0 -1
  224. package/dist/utils/highlightUtils.js +0 -93
  225. package/dist/utils/inputUtils.d.ts +0 -17
  226. package/dist/utils/inputUtils.js +0 -67
  227. package/dist/utils/logger.d.ts +0 -71
  228. package/dist/utils/logger.js +0 -259
  229. package/dist/utils/markdownTable.d.ts +0 -34
  230. package/dist/utils/markdownTable.js +0 -302
  231. package/dist/utils/rewindCheckpoints.d.ts +0 -8
  232. package/dist/utils/rewindCheckpoints.js +0 -17
  233. package/dist/utils/throttle.d.ts +0 -14
  234. package/dist/utils/throttle.js +0 -55
  235. package/dist/utils/toolParameterTransforms.d.ts +0 -18
  236. package/dist/utils/toolParameterTransforms.js +0 -73
  237. package/dist/utils/usageSummary.d.ts +0 -28
  238. package/dist/utils/usageSummary.js +0 -121
  239. package/dist/utils/version.d.ts +0 -1
  240. package/dist/utils/version.js +0 -4
  241. package/dist/utils/worktree.d.ts +0 -35
  242. 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) => {
@@ -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
+ };