wave-code 1.1.5 → 1.3.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 (46) hide show
  1. package/dist/bundle/wave.mjs +1075 -613
  2. package/package.json +4 -3
  3. package/src/cli.tsx +9 -0
  4. package/src/commands/plugin/install.ts +6 -7
  5. package/src/commands/plugin/uninstall.ts +11 -5
  6. package/src/components/ChatInterface.tsx +3 -0
  7. package/src/components/ConfirmationDetails.tsx +36 -15
  8. package/src/components/ConfirmationSelector.tsx +39 -0
  9. package/src/components/HooksManager.tsx +286 -0
  10. package/src/components/InputBox.tsx +125 -51
  11. package/src/components/Markdown.tsx +21 -8
  12. package/src/components/MarketplaceDetail.tsx +10 -27
  13. package/src/components/MarketplaceView.tsx +17 -3
  14. package/src/components/MessageBlockItem.tsx +0 -5
  15. package/src/components/MessageList.tsx +0 -1
  16. package/src/components/Notifications.tsx +7 -0
  17. package/src/components/PlanView.tsx +146 -0
  18. package/src/components/PluginManagerShell.tsx +3 -3
  19. package/src/components/PluginManagerTypes.ts +2 -1
  20. package/src/components/ResumeCommand.tsx +84 -0
  21. package/src/components/ScrolledContent.tsx +71 -0
  22. package/src/components/SessionSelector.tsx +184 -6
  23. package/src/components/SessionSelectorFlow.tsx +246 -0
  24. package/src/constants/commands.ts +30 -0
  25. package/src/contexts/useChat.tsx +252 -98
  26. package/src/daemon/commands.ts +1046 -67
  27. package/src/daemon-cli.ts +6 -0
  28. package/src/hooks/useInputManager.ts +30 -0
  29. package/src/hooks/useLineScroll.ts +58 -0
  30. package/src/hooks/usePluginManager.ts +104 -78
  31. package/src/index.ts +156 -16
  32. package/src/managers/inputHandlers.ts +16 -9
  33. package/src/managers/inputReducer.ts +39 -9
  34. package/src/reducers/confirmationReducer.ts +20 -1
  35. package/src/reducers/hooksManagerReducer.ts +92 -0
  36. package/src/reducers/selectorReducer.ts +3 -0
  37. package/src/session-selector-cli.tsx +9 -255
  38. package/src/stdio/agentBridge.ts +932 -133
  39. package/src/stdio/daemonServer.ts +26 -82
  40. package/src/stdio/protocol.ts +23 -4
  41. package/src/stdio-cli.ts +60 -0
  42. package/src/utils/rewindCheckpoints.ts +2 -2
  43. package/src/utils/sessionOwnership.ts +62 -0
  44. package/src/utils/worktree.ts +175 -50
  45. package/src/components/BangDisplay.tsx +0 -41
  46. package/src/components/PlanDisplay.tsx +0 -18
package/src/daemon-cli.ts CHANGED
@@ -8,9 +8,15 @@
8
8
  * keeps the process alive — there is no stdin to wait on.
9
9
  */
10
10
 
11
+ import { ensureRuntimeDeps } from "wave-agent-sdk";
11
12
  import { DaemonServer } from "./stdio/daemonServer.js";
12
13
 
13
14
  export async function startDaemonCli(socketPath: string): Promise<void> {
15
+ // Install the on-demand runtime dependencies (image codec + ripgrep) before
16
+ // serving (see cli.tsx). The daemon serves turns on a remote host, so the
17
+ // install belongs here — and it must land before the first client attaches,
18
+ // since the host reports "daemon ready" on this socket.
19
+ await ensureRuntimeDeps();
14
20
  const server = new DaemonServer({ socketPath });
15
21
  await server.start();
16
22
  // Ready — any error that follows goes to the daemon log via stderr.
@@ -61,7 +61,10 @@ export const useInputManager = (
61
61
  onRecallQueuedMessage,
62
62
  onClearMessages,
63
63
  onCompact,
64
+ onReloadPlugins,
64
65
  onAddDir,
66
+ onRenameSession,
67
+ onPlanCommand,
65
68
  isIdle: isIdleProp,
66
69
  } = callbacks;
67
70
 
@@ -246,6 +249,8 @@ export const useInputManager = (
246
249
  dispatch({ type: "SET_SHOW_AGENTS_MANAGER", payload: true });
247
250
  } else if (command === "rewind") {
248
251
  dispatch({ type: "SET_SHOW_REWIND_MANAGER", payload: true });
252
+ } else if (command === "resume") {
253
+ dispatch({ type: "SET_SHOW_RESUME_SELECTOR", payload: true });
249
254
  } else if (command === "help") {
250
255
  dispatch({ type: "SET_SHOW_HELP", payload: true });
251
256
  } else if (command === "status") {
@@ -271,12 +276,22 @@ export const useInputManager = (
271
276
  dispatch({ type: "SET_SHOW_MODEL_SELECTOR", payload: true });
272
277
  } else if (command === "workflows") {
273
278
  dispatch({ type: "SET_SHOW_WORKFLOW_MANAGER", payload: true });
279
+ } else if (command === "skills") {
280
+ dispatch({ type: "SET_SHOW_SKILLS_MANAGER", payload: true });
281
+ } else if (command === "hooks") {
282
+ dispatch({ type: "SET_SHOW_HOOKS_MANAGER", payload: true });
274
283
  } else if (command === "clear") {
275
284
  await onClearMessages?.();
276
285
  } else if (command === "compact") {
277
286
  await onCompact?.(effect.args);
287
+ } else if (command === "reload-plugins") {
288
+ await onReloadPlugins?.();
278
289
  } else if (command === "add-dir") {
279
290
  await onAddDir?.(effect.args);
291
+ } else if (command === "rename") {
292
+ await onRenameSession?.(effect.args);
293
+ } else if (command === "plan") {
294
+ await onPlanCommand?.(effect.args);
280
295
  }
281
296
  }
282
297
  break;
@@ -305,7 +320,10 @@ export const useInputManager = (
305
320
  onRecallQueuedMessage,
306
321
  onClearMessages,
307
322
  onCompact,
323
+ onReloadPlugins,
308
324
  onAddDir,
325
+ onRenameSession,
326
+ onPlanCommand,
309
327
  ]);
310
328
 
311
329
  useEffect(() => {
@@ -503,6 +521,10 @@ export const useInputManager = (
503
521
  dispatch({ type: "SET_SHOW_REWIND_MANAGER", payload: show });
504
522
  }, []);
505
523
 
524
+ const setShowResumeSelector = useCallback((show: boolean) => {
525
+ dispatch({ type: "SET_SHOW_RESUME_SELECTOR", payload: show });
526
+ }, []);
527
+
506
528
  const setShowHelp = useCallback((show: boolean) => {
507
529
  dispatch({ type: "SET_SHOW_HELP", payload: show });
508
530
  }, []);
@@ -531,6 +553,10 @@ export const useInputManager = (
531
553
  dispatch({ type: "SET_SHOW_SKILLS_MANAGER", payload: show });
532
554
  }, []);
533
555
 
556
+ const setShowHooksManager = useCallback((show: boolean) => {
557
+ dispatch({ type: "SET_SHOW_HOOKS_MANAGER", payload: show });
558
+ }, []);
559
+
534
560
  const setPermissionMode = useCallback(
535
561
  (mode: PermissionMode) => {
536
562
  dispatch({ type: "SET_PERMISSION_MODE", payload: mode });
@@ -669,6 +695,7 @@ export const useInputManager = (
669
695
  showMcpManager: state.showMcpManager,
670
696
  showAgentsManager: state.showAgentsManager,
671
697
  showRewindManager: state.showRewindManager,
698
+ showResumeSelector: state.showResumeSelector,
672
699
  showHelp: state.showHelp,
673
700
  showStatusCommand: state.showStatusCommand,
674
701
  showLoginCommand: state.showLoginCommand,
@@ -676,6 +703,7 @@ export const useInputManager = (
676
703
  showModelSelector: state.showModelSelector,
677
704
  showWorkflowManager: state.showWorkflowManager,
678
705
  showSkillsManager: state.showSkillsManager,
706
+ showHooksManager: state.showHooksManager,
679
707
  permissionMode: state.permissionMode,
680
708
  attachedImages: state.attachedImages,
681
709
  btwState: state.btwState,
@@ -722,7 +750,9 @@ export const useInputManager = (
722
750
  setShowPluginManager,
723
751
  setShowModelSelector,
724
752
  setShowWorkflowManager,
753
+ setShowResumeSelector,
725
754
  setShowSkillsManager,
755
+ setShowHooksManager,
726
756
  setPermissionMode,
727
757
  setBtwState,
728
758
 
@@ -0,0 +1,58 @@
1
+ import { useState } from "react";
2
+ import { useInput } from "ink";
3
+
4
+ export interface UseLineScrollOptions {
5
+ /** Total number of content rows in the scrollable area. */
6
+ totalLines: number;
7
+ /** Number of rows visible at once (computed from the available height). */
8
+ visibleCount: number;
9
+ }
10
+
11
+ /**
12
+ * Fixed-height line scrolling shared by overlays whose content may exceed the
13
+ * terminal: keeps a scroll offset clamped to the visible window and handles
14
+ * PgUp/PgDn (full page) plus Ctrl+u/Ctrl+d (half page, aligned with Claude
15
+ * Code's modal pager keys). Returns the clamped offset and the ↑/↓ indicator
16
+ * flags so callers can render "N more" lines.
17
+ *
18
+ * Scroll position starts at the top on mount; callers that display a new
19
+ * content set remount this hook (e.g. an overlay component keyed by content),
20
+ * which resets the offset naturally.
21
+ */
22
+ export function useLineScroll({
23
+ totalLines,
24
+ visibleCount,
25
+ }: UseLineScrollOptions): {
26
+ scrollOffset: number;
27
+ hasMoreAbove: boolean;
28
+ hasMoreBelow: boolean;
29
+ } {
30
+ const [scrollOffset, setScrollOffset] = useState(0);
31
+
32
+ const maxScroll = Math.max(0, totalLines - visibleCount);
33
+ const halfPage = Math.max(1, Math.ceil(visibleCount / 2));
34
+
35
+ useInput((input, key) => {
36
+ if (key.pageUp) {
37
+ setScrollOffset((offset) => Math.max(0, offset - visibleCount));
38
+ }
39
+ if (key.pageDown) {
40
+ setScrollOffset((offset) => Math.min(maxScroll, offset + visibleCount));
41
+ }
42
+ // Ctrl+u/Ctrl+d scroll half a page — fallback for keyboards without
43
+ // PgUp/PgDn (aligned with Claude Code's modal pager keys).
44
+ if (key.ctrl && input === "u") {
45
+ setScrollOffset((offset) => Math.max(0, offset - halfPage));
46
+ }
47
+ if (key.ctrl && input === "d") {
48
+ setScrollOffset((offset) => Math.min(maxScroll, offset + halfPage));
49
+ }
50
+ });
51
+
52
+ const clampedOffset = Math.min(scrollOffset, maxScroll);
53
+ return {
54
+ scrollOffset: clampedOffset,
55
+ hasMoreAbove: clampedOffset > 0,
56
+ hasMoreBelow: clampedOffset + visibleCount < totalLines,
57
+ };
58
+ }
@@ -12,7 +12,11 @@ import {
12
12
  } from "../components/PluginManagerTypes.js";
13
13
 
14
14
  export function usePluginManager(options?: {
15
- onPluginInstalled?: () => void;
15
+ /**
16
+ * 插件变更落盘后的回调:宿主据此置「待应用」信号并提示一次,**不重建会话**
17
+ * (docs/specs/ecosystem/plugin.md「插件变更的就地重载」)。
18
+ */
19
+ onPluginChanged?: () => void;
16
20
  }): PluginManagerContextType {
17
21
  const [state, setState] = useState<PluginManagerState>({
18
22
  currentView: "DISCOVER",
@@ -24,6 +28,7 @@ export function usePluginManager(options?: {
24
28
  });
25
29
 
26
30
  const [marketplaces, setMarketplaces] = useState<KnownMarketplace[]>([]);
31
+ const [checkingForUpdates, setCheckingForUpdates] = useState(false);
27
32
  const [installedPlugins, setInstalledPlugins] = useState<
28
33
  (InstalledPlugin & { enabled: boolean })[]
29
34
  >([]);
@@ -63,21 +68,26 @@ export function usePluginManager(options?: {
63
68
  [setState],
64
69
  );
65
70
 
66
- const refresh = useCallback(async () => {
67
- clearPluginFeedback();
68
- setState((prev: PluginManagerState) => ({
69
- ...prev,
70
- isLoading: true,
71
- }));
72
- try {
73
- const [mks, installed, enabledMap] = await Promise.all([
74
- pluginCore.listMarketplaces(),
75
- pluginCore.getInstalledPlugins(),
76
- Promise.resolve(pluginCore.getMergedEnabledPlugins()),
77
- ]);
71
+ /** 读取市场清单 + 已安装插件 + 可发现插件(不碰 isLoading,供后台刷新后重读)。 */
72
+ const loadLists = useCallback(async () => {
73
+ const [mks, installed, enabledMap] = await Promise.all([
74
+ pluginCore.listMarketplaces(),
75
+ pluginCore.getInstalledPlugins(),
76
+ Promise.resolve(pluginCore.getMergedEnabledPlugins()),
77
+ ]);
78
78
 
79
- setMarketplaces(mks);
80
- const allInstalledWithEnabled = installed.plugins.map((p) => {
79
+ setMarketplaces(mks);
80
+ // 同一插件在本机可能有多条安装记录(各作用域/各项目一条,spec plugin A-015);
81
+ // 「已安装」列表按插件去重,否则同一个插件会重复成行。
82
+ const listedPlugins = new Set<string>();
83
+ const allInstalledWithEnabled = installed.plugins
84
+ .filter((p) => {
85
+ const pluginId = `${p.name}@${p.marketplace}`;
86
+ if (listedPlugins.has(pluginId)) return false;
87
+ listedPlugins.add(pluginId);
88
+ return true;
89
+ })
90
+ .map((p) => {
81
91
  const pluginId = `${p.name}@${p.marketplace}`;
82
92
  return {
83
93
  ...p,
@@ -86,40 +96,50 @@ export function usePluginManager(options?: {
86
96
  };
87
97
  });
88
98
 
89
- // Only show enabled plugins in the "Installed" view
90
- setInstalledPlugins(allInstalledWithEnabled.filter((p) => p.enabled));
99
+ // Only show enabled plugins in the "Installed" view
100
+ setInstalledPlugins(allInstalledWithEnabled.filter((p) => p.enabled));
91
101
 
92
- const allDiscoverable: (MarketplacePluginEntry & {
93
- marketplace: string;
94
- installed: boolean;
95
- version?: string;
96
- })[] = [];
97
- for (const mk of mks) {
98
- try {
99
- const manifest = await pluginCore.loadMarketplaceManifest(
100
- pluginCore.getMarketplacePath(mk),
102
+ const allDiscoverable: (MarketplacePluginEntry & {
103
+ marketplace: string;
104
+ installed: boolean;
105
+ version?: string;
106
+ })[] = [];
107
+ for (const mk of mks) {
108
+ try {
109
+ const manifest = await pluginCore.loadMarketplaceManifest(
110
+ pluginCore.getMarketplacePath(mk),
111
+ );
112
+ manifest.plugins.forEach((p) => {
113
+ const pluginId = `${p.name}@${mk.name}`;
114
+ const isInstalled = installed.plugins.find(
115
+ (ip) => ip.name === p.name && ip.marketplace === mk.name,
101
116
  );
102
- manifest.plugins.forEach((p) => {
103
- const pluginId = `${p.name}@${mk.name}`;
104
- const isInstalled = installed.plugins.find(
105
- (ip) => ip.name === p.name && ip.marketplace === mk.name,
106
- );
107
- const isEnabled = !!enabledMap[pluginId];
117
+ const isEnabled = !!enabledMap[pluginId];
108
118
 
109
- // Show in Discover if not installed OR if installed but not enabled in current scope
110
- if (!isInstalled || !isEnabled) {
111
- allDiscoverable.push({
112
- ...p,
113
- marketplace: mk.name,
114
- installed: !!isInstalled,
115
- });
116
- }
117
- });
118
- } catch {
119
- // Skip marketplaces that fail to load
120
- }
119
+ // Show in Discover if not installed OR if installed but not enabled in current scope
120
+ if (!isInstalled || !isEnabled) {
121
+ allDiscoverable.push({
122
+ ...p,
123
+ marketplace: mk.name,
124
+ installed: !!isInstalled,
125
+ });
126
+ }
127
+ });
128
+ } catch {
129
+ // Skip marketplaces that fail to load
121
130
  }
122
- setDiscoverablePlugins(allDiscoverable);
131
+ }
132
+ setDiscoverablePlugins(allDiscoverable);
133
+ }, [pluginCore]);
134
+
135
+ const refresh = useCallback(async () => {
136
+ clearPluginFeedback();
137
+ setState((prev: PluginManagerState) => ({
138
+ ...prev,
139
+ isLoading: true,
140
+ }));
141
+ try {
142
+ await loadLists();
123
143
  setState((prev: PluginManagerState) => ({ ...prev, isLoading: false }));
124
144
  } catch (error) {
125
145
  setState((prev: PluginManagerState) => ({
@@ -128,12 +148,31 @@ export function usePluginManager(options?: {
128
148
  error: error instanceof Error ? error.message : String(error),
129
149
  }));
130
150
  }
131
- }, [pluginCore, clearPluginFeedback]);
151
+ }, [loadLists, clearPluginFeedback]);
132
152
 
133
153
  useEffect(() => {
134
- refresh();
154
+ void refresh();
135
155
  }, [refresh]);
136
156
 
157
+ // 打开插件管理器即后台刷新各市场检出(只拉清单、不升级任何插件;spec
158
+ // ecosystem/plugin 场景 5/11):刷新与列表加载并行、不阻塞界面,完成后重读
159
+ // 列表呈现最新清单(场景 12)。失败静默——界面仍以刷新前的清单展示(场景 9)。
160
+ useEffect(() => {
161
+ let cancelled = false;
162
+ setCheckingForUpdates(true);
163
+ pluginCore
164
+ .refreshMarketplaces()
165
+ .catch(() => {})
166
+ .then(async () => {
167
+ if (cancelled) return;
168
+ setCheckingForUpdates(false);
169
+ await loadLists().catch(() => {});
170
+ });
171
+ return () => {
172
+ cancelled = true;
173
+ };
174
+ }, [pluginCore, loadLists]);
175
+
137
176
  const setView = useCallback((view: ViewType) => {
138
177
  setState((prev: PluginManagerState) => ({ ...prev, currentView: view }));
139
178
  }, []);
@@ -150,11 +189,15 @@ export function usePluginManager(options?: {
150
189
  isLoading: true,
151
190
  }));
152
191
  try {
153
- await pluginCore.addMarketplace(source, scope);
192
+ const marketplace = await pluginCore.addMarketplace(source, scope);
154
193
  await refresh();
155
194
  setSuccessMessage(`Marketplace added successfully (${scope} scope)`);
195
+ // 焦点落到新加的市场(spec plugin「管理市场」场景 5):刷新会把市场列表的
196
+ // 选中项归零,这里用 addMarketplace 的返回值(市场名由市场自身清单决定,
197
+ // 调用方事先不知道)把选中项设为新市场;失败路径不设置,保持原选中不变。
156
198
  setState((prev: PluginManagerState) => ({
157
199
  ...prev,
200
+ selectedId: marketplace.name,
158
201
  currentView: "MARKETPLACES",
159
202
  }));
160
203
  } catch (error) {
@@ -194,6 +237,8 @@ export function usePluginManager(options?: {
194
237
  [pluginCore, refresh, clearPluginFeedback, setSuccessMessage],
195
238
  );
196
239
 
240
+ // 市场详情的「批量更新插件」项:按当前清单批量升级该市场内已装插件,不拉检出
241
+ // ——检出已由打开插件管理器时的后台刷新处理(spec ecosystem/plugin A-013 场景 14)。
197
242
  const updateMarketplace = useCallback(
198
243
  async (name: string) => {
199
244
  clearPluginFeedback();
@@ -202,9 +247,12 @@ export function usePluginManager(options?: {
202
247
  isLoading: true,
203
248
  }));
204
249
  try {
205
- await pluginCore.updateMarketplace(name);
250
+ const updated = await pluginCore.updateMarketplacePlugins(name);
206
251
  await refresh();
207
252
  setSuccessMessage(`Marketplace '${name}' updated successfully`);
253
+ // 批量更新同属插件变更:真的升级了插件时才产生「待应用」提示
254
+ // (spec plugin「插件变更的就地重载」场景 1)。
255
+ if (updated > 0) options?.onPluginChanged?.();
208
256
  } catch (error) {
209
257
  setState((prev: PluginManagerState) => ({
210
258
  ...prev,
@@ -213,7 +261,7 @@ export function usePluginManager(options?: {
213
261
  }));
214
262
  }
215
263
  },
216
- [pluginCore, refresh, clearPluginFeedback, setSuccessMessage],
264
+ [pluginCore, refresh, clearPluginFeedback, setSuccessMessage, options],
217
265
  );
218
266
 
219
267
  const installPlugin = useCallback(
@@ -232,7 +280,7 @@ export function usePluginManager(options?: {
232
280
  await pluginCore.installPlugin(pluginId, scope);
233
281
  await refresh();
234
282
  setSuccessMessage(`Plugin '${name}' installed successfully`);
235
- options?.onPluginInstalled?.();
283
+ options?.onPluginChanged?.();
236
284
  } catch (error) {
237
285
  setState((prev: PluginManagerState) => ({
238
286
  ...prev,
@@ -256,6 +304,7 @@ export function usePluginManager(options?: {
256
304
  await pluginCore.uninstallPlugin(pluginId);
257
305
  await refresh();
258
306
  setSuccessMessage(`Plugin '${name}' uninstalled successfully`);
307
+ options?.onPluginChanged?.();
259
308
  } catch (error) {
260
309
  setState((prev: PluginManagerState) => ({
261
310
  ...prev,
@@ -264,7 +313,7 @@ export function usePluginManager(options?: {
264
313
  }));
265
314
  }
266
315
  },
267
- [pluginCore, refresh, clearPluginFeedback, setSuccessMessage],
316
+ [pluginCore, refresh, clearPluginFeedback, setSuccessMessage, options],
268
317
  );
269
318
 
270
319
  const updatePlugin = useCallback(
@@ -279,6 +328,7 @@ export function usePluginManager(options?: {
279
328
  await pluginCore.updatePlugin(pluginId);
280
329
  await refresh();
281
330
  setSuccessMessage(`Plugin '${name}' updated successfully`);
331
+ options?.onPluginChanged?.();
282
332
  } catch (error) {
283
333
  setState((prev: PluginManagerState) => ({
284
334
  ...prev,
@@ -287,36 +337,13 @@ export function usePluginManager(options?: {
287
337
  }));
288
338
  }
289
339
  },
290
- [pluginCore, refresh, clearPluginFeedback, setSuccessMessage],
291
- );
292
-
293
- const toggleAutoUpdate = useCallback(
294
- async (name: string, enabled: boolean) => {
295
- clearPluginFeedback();
296
- setState((prev: PluginManagerState) => ({
297
- ...prev,
298
- isLoading: true,
299
- }));
300
- try {
301
- await pluginCore.toggleAutoUpdate(name, enabled);
302
- await refresh();
303
- setSuccessMessage(
304
- `Auto-update for '${name}' ${enabled ? "enabled" : "disabled"}`,
305
- );
306
- } catch (error) {
307
- setState((prev: PluginManagerState) => ({
308
- ...prev,
309
- isLoading: false,
310
- error: error instanceof Error ? error.message : String(error),
311
- }));
312
- }
313
- },
314
- [pluginCore, refresh, clearPluginFeedback, setSuccessMessage],
340
+ [pluginCore, refresh, clearPluginFeedback, setSuccessMessage, options],
315
341
  );
316
342
 
317
343
  return {
318
344
  state,
319
345
  marketplaces,
346
+ checkingForUpdates,
320
347
  installedPlugins,
321
348
  discoverablePlugins,
322
349
  actions: {
@@ -328,7 +355,6 @@ export function usePluginManager(options?: {
328
355
  installPlugin,
329
356
  uninstallPlugin,
330
357
  updatePlugin,
331
- toggleAutoUpdate,
332
358
  refresh,
333
359
  clearPluginFeedback,
334
360
  },