wave-code 1.0.10 → 1.1.1

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 (248) hide show
  1. package/bin/wave-code.js +8 -2
  2. package/dist/bundle/wave.mjs +3231 -0
  3. package/package.json +24 -25
  4. package/src/cli.tsx +6 -1
  5. package/src/commands/update.ts +2 -7
  6. package/src/components/App.tsx +3 -7
  7. package/src/components/BtwDisplay.tsx +10 -1
  8. package/src/components/ChatInterface.tsx +21 -52
  9. package/src/components/ConfirmationDetails.tsx +144 -37
  10. package/src/components/DiffDisplay.tsx +335 -301
  11. package/src/components/InputBox.tsx +17 -2
  12. package/src/components/LoadingIndicator.tsx +16 -2
  13. package/src/components/MessageBlockItem.tsx +2 -4
  14. package/src/components/MessageList.tsx +75 -14
  15. package/src/components/ReasoningDisplay.tsx +2 -4
  16. package/src/components/SessionSelector.tsx +82 -11
  17. package/src/components/SkillsManager.tsx +312 -0
  18. package/src/components/StatusCommand.tsx +2 -9
  19. package/src/components/ToolDisplay.tsx +2 -4
  20. package/src/constants/commands.ts +6 -0
  21. package/src/contexts/useChat.tsx +245 -301
  22. package/src/daemon/commands.ts +57 -27
  23. package/src/daemon/jsonRpcClient.ts +1 -1
  24. package/src/daemon/socketClient.ts +2 -2
  25. package/src/hooks/useInputManager.ts +7 -1
  26. package/src/index.ts +57 -10
  27. package/src/managers/inputHandlers.ts +2 -0
  28. package/src/managers/inputReducer.ts +195 -104
  29. package/src/print-cli.ts +25 -17
  30. package/src/reducers/selectorReducer.ts +10 -0
  31. package/src/reducers/skillsManagerReducer.ts +91 -0
  32. package/src/session-selector-cli.tsx +269 -28
  33. package/src/stdio/agentBridge.ts +30 -21
  34. package/src/stdio/daemonServer.ts +3 -1
  35. package/src/stdio/protocol.ts +2 -0
  36. package/src/utils/clipboard.ts +21 -0
  37. package/src/utils/highlightUtils.ts +46 -1
  38. package/src/utils/readPackageJson.ts +29 -0
  39. package/src/utils/streamingText.ts +11 -0
  40. package/src/utils/worktree.ts +191 -43
  41. package/dist/cli.d.ts +0 -6
  42. package/dist/cli.js +0 -53
  43. package/dist/commands/plugin/disable.d.ts +0 -5
  44. package/dist/commands/plugin/disable.js +0 -15
  45. package/dist/commands/plugin/enable.d.ts +0 -5
  46. package/dist/commands/plugin/enable.js +0 -15
  47. package/dist/commands/plugin/install.d.ts +0 -5
  48. package/dist/commands/plugin/install.js +0 -21
  49. package/dist/commands/plugin/list.d.ts +0 -1
  50. package/dist/commands/plugin/list.js +0 -31
  51. package/dist/commands/plugin/marketplace.d.ts +0 -10
  52. package/dist/commands/plugin/marketplace.js +0 -90
  53. package/dist/commands/plugin/uninstall.d.ts +0 -3
  54. package/dist/commands/plugin/uninstall.js +0 -16
  55. package/dist/commands/plugin/update.d.ts +0 -3
  56. package/dist/commands/plugin/update.js +0 -15
  57. package/dist/commands/update.d.ts +0 -1
  58. package/dist/commands/update.js +0 -97
  59. package/dist/components/AgentsManager.d.ts +0 -7
  60. package/dist/components/AgentsManager.js +0 -109
  61. package/dist/components/App.d.ts +0 -9
  62. package/dist/components/App.js +0 -76
  63. package/dist/components/BackgroundTaskManager.d.ts +0 -5
  64. package/dist/components/BackgroundTaskManager.js +0 -99
  65. package/dist/components/BangDisplay.d.ts +0 -8
  66. package/dist/components/BangDisplay.js +0 -17
  67. package/dist/components/BtwDisplay.d.ts +0 -7
  68. package/dist/components/BtwDisplay.js +0 -19
  69. package/dist/components/ChatInterface.d.ts +0 -2
  70. package/dist/components/ChatInterface.js +0 -61
  71. package/dist/components/CommandSelector.d.ts +0 -10
  72. package/dist/components/CommandSelector.js +0 -66
  73. package/dist/components/CompactDisplay.d.ts +0 -8
  74. package/dist/components/CompactDisplay.js +0 -13
  75. package/dist/components/ConfirmationDetails.d.ts +0 -9
  76. package/dist/components/ConfirmationDetails.js +0 -45
  77. package/dist/components/ConfirmationSelector.d.ts +0 -13
  78. package/dist/components/ConfirmationSelector.js +0 -127
  79. package/dist/components/DiffDisplay.d.ts +0 -8
  80. package/dist/components/DiffDisplay.js +0 -201
  81. package/dist/components/DiscoverView.d.ts +0 -2
  82. package/dist/components/DiscoverView.js +0 -37
  83. package/dist/components/FileSelector.d.ts +0 -11
  84. package/dist/components/FileSelector.js +0 -50
  85. package/dist/components/HelpView.d.ts +0 -7
  86. package/dist/components/HelpView.js +0 -74
  87. package/dist/components/HistorySearch.d.ts +0 -8
  88. package/dist/components/HistorySearch.js +0 -66
  89. package/dist/components/InputBox.d.ts +0 -24
  90. package/dist/components/InputBox.js +0 -149
  91. package/dist/components/InstalledView.d.ts +0 -2
  92. package/dist/components/InstalledView.js +0 -45
  93. package/dist/components/LoadingIndicator.d.ts +0 -10
  94. package/dist/components/LoadingIndicator.js +0 -6
  95. package/dist/components/LoginCommand.d.ts +0 -5
  96. package/dist/components/LoginCommand.js +0 -153
  97. package/dist/components/Markdown.d.ts +0 -5
  98. package/dist/components/Markdown.js +0 -127
  99. package/dist/components/MarketplaceAddForm.d.ts +0 -2
  100. package/dist/components/MarketplaceAddForm.js +0 -55
  101. package/dist/components/MarketplaceDetail.d.ts +0 -2
  102. package/dist/components/MarketplaceDetail.js +0 -74
  103. package/dist/components/MarketplaceList.d.ts +0 -8
  104. package/dist/components/MarketplaceList.js +0 -17
  105. package/dist/components/MarketplaceView.d.ts +0 -2
  106. package/dist/components/MarketplaceView.js +0 -41
  107. package/dist/components/McpManager.d.ts +0 -9
  108. package/dist/components/McpManager.js +0 -88
  109. package/dist/components/MessageBlockItem.d.ts +0 -8
  110. package/dist/components/MessageBlockItem.js +0 -15
  111. package/dist/components/MessageList.d.ts +0 -10
  112. package/dist/components/MessageList.js +0 -80
  113. package/dist/components/ModelSelector.d.ts +0 -8
  114. package/dist/components/ModelSelector.js +0 -45
  115. package/dist/components/Notifications.d.ts +0 -7
  116. package/dist/components/Notifications.js +0 -9
  117. package/dist/components/PlanDisplay.d.ts +0 -7
  118. package/dist/components/PlanDisplay.js +0 -6
  119. package/dist/components/PluginDetail.d.ts +0 -2
  120. package/dist/components/PluginDetail.js +0 -87
  121. package/dist/components/PluginList.d.ts +0 -13
  122. package/dist/components/PluginList.js +0 -15
  123. package/dist/components/PluginManagerShell.d.ts +0 -6
  124. package/dist/components/PluginManagerShell.js +0 -92
  125. package/dist/components/PluginManagerTypes.d.ts +0 -35
  126. package/dist/components/PluginManagerTypes.js +0 -1
  127. package/dist/components/QueuedMessageList.d.ts +0 -2
  128. package/dist/components/QueuedMessageList.js +0 -18
  129. package/dist/components/ReasoningDisplay.d.ts +0 -8
  130. package/dist/components/ReasoningDisplay.js +0 -13
  131. package/dist/components/RewindCommand.d.ts +0 -12
  132. package/dist/components/RewindCommand.js +0 -78
  133. package/dist/components/SessionSelector.d.ts +0 -12
  134. package/dist/components/SessionSelector.js +0 -47
  135. package/dist/components/StatusCommand.d.ts +0 -5
  136. package/dist/components/StatusCommand.js +0 -28
  137. package/dist/components/StatusLine.d.ts +0 -6
  138. package/dist/components/StatusLine.js +0 -9
  139. package/dist/components/TaskList.d.ts +0 -5
  140. package/dist/components/TaskList.js +0 -197
  141. package/dist/components/TaskNotificationMessage.d.ts +0 -5
  142. package/dist/components/TaskNotificationMessage.js +0 -12
  143. package/dist/components/ToolDisplay.d.ts +0 -9
  144. package/dist/components/ToolDisplay.js +0 -55
  145. package/dist/components/WorkflowManager.d.ts +0 -5
  146. package/dist/components/WorkflowManager.js +0 -102
  147. package/dist/components/WorktreeExitPrompt.d.ts +0 -12
  148. package/dist/components/WorktreeExitPrompt.js +0 -35
  149. package/dist/constants/commands.d.ts +0 -2
  150. package/dist/constants/commands.js +0 -92
  151. package/dist/contexts/PluginManagerContext.d.ts +0 -3
  152. package/dist/contexts/PluginManagerContext.js +0 -9
  153. package/dist/contexts/useAppConfig.d.ts +0 -10
  154. package/dist/contexts/useAppConfig.js +0 -13
  155. package/dist/contexts/useChat.d.ts +0 -101
  156. package/dist/contexts/useChat.js +0 -993
  157. package/dist/contracts/status.d.ts +0 -7
  158. package/dist/contracts/status.js +0 -1
  159. package/dist/daemon/commands.d.ts +0 -49
  160. package/dist/daemon/commands.js +0 -341
  161. package/dist/daemon/jsonRpcClient.d.ts +0 -38
  162. package/dist/daemon/jsonRpcClient.js +0 -129
  163. package/dist/daemon/socketClient.d.ts +0 -13
  164. package/dist/daemon/socketClient.js +0 -26
  165. package/dist/daemon-cli.d.ts +0 -10
  166. package/dist/daemon-cli.js +0 -15
  167. package/dist/hooks/useInputManager.d.ts +0 -75
  168. package/dist/hooks/useInputManager.js +0 -583
  169. package/dist/hooks/usePluginManager.d.ts +0 -4
  170. package/dist/hooks/usePluginManager.js +0 -265
  171. package/dist/hooks/useTasks.d.ts +0 -1
  172. package/dist/hooks/useTasks.js +0 -5
  173. package/dist/index.d.ts +0 -4
  174. package/dist/index.js +0 -447
  175. package/dist/managers/inputHandlers.d.ts +0 -30
  176. package/dist/managers/inputHandlers.js +0 -625
  177. package/dist/managers/inputReducer.d.ts +0 -271
  178. package/dist/managers/inputReducer.js +0 -1121
  179. package/dist/plugin-manager-cli.d.ts +0 -5
  180. package/dist/plugin-manager-cli.js +0 -12
  181. package/dist/print-cli.d.ts +0 -10
  182. package/dist/print-cli.js +0 -209
  183. package/dist/reducers/agentsManagerReducer.d.ts +0 -26
  184. package/dist/reducers/agentsManagerReducer.js +0 -54
  185. package/dist/reducers/backgroundTaskManagerReducer.d.ts +0 -62
  186. package/dist/reducers/backgroundTaskManagerReducer.js +0 -106
  187. package/dist/reducers/confirmationReducer.d.ts +0 -37
  188. package/dist/reducers/confirmationReducer.js +0 -193
  189. package/dist/reducers/helpSelectorReducer.d.ts +0 -25
  190. package/dist/reducers/helpSelectorReducer.js +0 -52
  191. package/dist/reducers/marketplaceAddFormReducer.d.ts +0 -24
  192. package/dist/reducers/marketplaceAddFormReducer.js +0 -58
  193. package/dist/reducers/mcpManagerReducer.d.ts +0 -37
  194. package/dist/reducers/mcpManagerReducer.js +0 -74
  195. package/dist/reducers/pluginDetailReducer.d.ts +0 -32
  196. package/dist/reducers/pluginDetailReducer.js +0 -70
  197. package/dist/reducers/questionReducer.d.ts +0 -82
  198. package/dist/reducers/questionReducer.js +0 -375
  199. package/dist/reducers/rewindSelectorReducer.d.ts +0 -21
  200. package/dist/reducers/rewindSelectorReducer.js +0 -37
  201. package/dist/reducers/selectorReducer.d.ts +0 -23
  202. package/dist/reducers/selectorReducer.js +0 -42
  203. package/dist/reducers/workflowManagerReducer.d.ts +0 -40
  204. package/dist/reducers/workflowManagerReducer.js +0 -100
  205. package/dist/session-selector-cli.d.ts +0 -3
  206. package/dist/session-selector-cli.js +0 -25
  207. package/dist/stdio/agentBridge.d.ts +0 -139
  208. package/dist/stdio/agentBridge.js +0 -999
  209. package/dist/stdio/daemonServer.d.ts +0 -67
  210. package/dist/stdio/daemonServer.js +0 -191
  211. package/dist/stdio/index.d.ts +0 -5
  212. package/dist/stdio/index.js +0 -5
  213. package/dist/stdio/jsonRpcConnection.d.ts +0 -39
  214. package/dist/stdio/jsonRpcConnection.js +0 -135
  215. package/dist/stdio/protocol.d.ts +0 -41
  216. package/dist/stdio/protocol.js +0 -26
  217. package/dist/stdio/stdioServer.d.ts +0 -36
  218. package/dist/stdio/stdioServer.js +0 -51
  219. package/dist/stdio-cli.d.ts +0 -8
  220. package/dist/stdio-cli.js +0 -15
  221. package/dist/types.d.ts +0 -20
  222. package/dist/types.js +0 -1
  223. package/dist/utils/bracketedPaste.d.ts +0 -39
  224. package/dist/utils/bracketedPaste.js +0 -122
  225. package/dist/utils/clipboard.d.ts +0 -21
  226. package/dist/utils/clipboard.js +0 -347
  227. package/dist/utils/constants.d.ts +0 -16
  228. package/dist/utils/constants.js +0 -18
  229. package/dist/utils/highlightUtils.d.ts +0 -1
  230. package/dist/utils/highlightUtils.js +0 -93
  231. package/dist/utils/inputUtils.d.ts +0 -17
  232. package/dist/utils/inputUtils.js +0 -67
  233. package/dist/utils/logger.d.ts +0 -71
  234. package/dist/utils/logger.js +0 -259
  235. package/dist/utils/markdownTable.d.ts +0 -34
  236. package/dist/utils/markdownTable.js +0 -302
  237. package/dist/utils/rewindCheckpoints.d.ts +0 -8
  238. package/dist/utils/rewindCheckpoints.js +0 -17
  239. package/dist/utils/throttle.d.ts +0 -14
  240. package/dist/utils/throttle.js +0 -55
  241. package/dist/utils/toolParameterTransforms.d.ts +0 -18
  242. package/dist/utils/toolParameterTransforms.js +0 -73
  243. package/dist/utils/usageSummary.d.ts +0 -28
  244. package/dist/utils/usageSummary.js +0 -121
  245. package/dist/utils/version.d.ts +0 -1
  246. package/dist/utils/version.js +0 -4
  247. package/dist/utils/worktree.d.ts +0 -35
  248. 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
  }
@@ -44,6 +44,7 @@ import {
44
44
  type SlashCommand,
45
45
  loadUserConfigEnv,
46
46
  type SubagentConfiguration,
47
+ type SkillMetadata,
47
48
  } from "wave-agent-sdk";
48
49
  import {
49
50
  type JsonRpcError,
@@ -241,6 +242,8 @@ export class AgentBridge {
241
242
  return this.getSlashCommands(sessionId);
242
243
  case "getSubagentConfigurations":
243
244
  return this.getSubagentConfigurations(sessionId);
245
+ case "getSkillMetadata":
246
+ return this.getSkillMetadata(sessionId);
244
247
 
245
248
  // ── File / History (global — no session required) ──
246
249
  case "searchFiles":
@@ -375,7 +378,12 @@ export class AgentBridge {
375
378
  );
376
379
  case "removeWorktree":
377
380
  return this.removeWorktreeSession(
378
- p as unknown as { path: string; branch: string; repoRoot: string },
381
+ p as unknown as {
382
+ path: string;
383
+ branch: string;
384
+ repoRoot: string;
385
+ hookBased?: boolean;
386
+ },
379
387
  );
380
388
 
381
389
  default:
@@ -615,6 +623,7 @@ export class AgentBridge {
615
623
  repoRoot: string;
616
624
  baseBranch: string;
617
625
  isNew: boolean;
626
+ hookBased: boolean;
618
627
  }> {
619
628
  if (!params.workdir) {
620
629
  throw new RpcError(PROTOCOL_INTERNAL_ERROR, "workdir is required");
@@ -631,6 +640,7 @@ export class AgentBridge {
631
640
  repoRoot: session.repoRoot,
632
641
  baseBranch: params.baseBranch ?? getDefaultRemoteBranch(params.workdir),
633
642
  isNew: session.isNew,
643
+ hookBased: session.hookBased ?? false,
634
644
  };
635
645
  } catch (e) {
636
646
  throw new RpcError(PROTOCOL_INTERNAL_ERROR, (e as Error).message);
@@ -641,31 +651,24 @@ export class AgentBridge {
641
651
  path: string;
642
652
  branch: string;
643
653
  repoRoot: string;
654
+ hookBased?: boolean;
644
655
  }): 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;
656
+ // Hook-based worktrees are owned by the WorktreeRemove hook; the git-root
657
+ // containment check does not apply to them.
658
+ if (!params.hookBased) {
659
+ // Align with Claude Code v2.1.216+: refuse to remove a worktree whose
660
+ // path is a symlink or resolves outside the repo root. Already-removed
661
+ // (missing) paths pass validation so removal stays idempotent.
662
+ try {
663
+ validateWorktreeRemovalPath(params.path, params.repoRoot);
664
+ } catch (e) {
665
+ throw new RpcError(PROTOCOL_INTERNAL_ERROR, (e as Error).message);
664
666
  }
665
667
  }
666
668
 
667
669
  // removeWorktree is best-effort/idempotent: already-removed worktrees or
668
- // branches only log, never throw.
670
+ // branches only log, never throw. Hook-based worktrees go through the
671
+ // WorktreeRemove hook inside removeWorktree.
669
672
  await removeWorktree({
670
673
  name: "",
671
674
  path: params.path,
@@ -674,6 +677,7 @@ export class AgentBridge {
674
677
  hasUncommittedChanges: false,
675
678
  hasNewCommits: false,
676
679
  isNew: false,
680
+ hookBased: params.hookBased,
677
681
  });
678
682
  return { ok: true };
679
683
  }
@@ -1075,6 +1079,11 @@ export class AgentBridge {
1075
1079
  return { configurations: entry.agent.getSubagentConfigurations() };
1076
1080
  }
1077
1081
 
1082
+ private getSkillMetadata(sessionId?: string): { skills: SkillMetadata[] } {
1083
+ const entry = this.requireSession(sessionId);
1084
+ return { skills: entry.agent.getSkillMetadata() };
1085
+ }
1086
+
1078
1087
  // ── File / History (global) ───────────────────────────────────
1079
1088
 
1080
1089
  private async searchFiles(
@@ -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) => {
@@ -70,6 +70,8 @@ export type RequestMethod =
70
70
  | "connectMcpServer"
71
71
  | "disconnectMcpServer"
72
72
  | "getSlashCommands"
73
+ | "getSubagentConfigurations"
74
+ | "getSkillMetadata"
73
75
  | "searchFiles"
74
76
  | "writeArtifactFile"
75
77
  | "getPromptHistory"
@@ -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
+ };