wave-code 1.2.0 → 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.
- package/dist/bundle/wave.mjs +1049 -584
- package/package.json +4 -3
- package/src/cli.tsx +9 -0
- package/src/commands/plugin/install.ts +6 -7
- package/src/commands/plugin/uninstall.ts +11 -5
- package/src/components/ChatInterface.tsx +1 -0
- package/src/components/ConfirmationDetails.tsx +36 -15
- package/src/components/ConfirmationSelector.tsx +39 -0
- package/src/components/InputBox.tsx +39 -2
- package/src/components/Markdown.tsx +21 -8
- package/src/components/MarketplaceDetail.tsx +10 -27
- package/src/components/MarketplaceView.tsx +17 -3
- package/src/components/Notifications.tsx +7 -0
- package/src/components/PlanView.tsx +22 -17
- package/src/components/PluginManagerShell.tsx +3 -3
- package/src/components/PluginManagerTypes.ts +2 -1
- package/src/components/ResumeCommand.tsx +84 -0
- package/src/components/ScrolledContent.tsx +71 -0
- package/src/components/SessionSelector.tsx +184 -6
- package/src/components/SessionSelectorFlow.tsx +246 -0
- package/src/constants/commands.ts +18 -0
- package/src/contexts/useChat.tsx +158 -28
- package/src/daemon/commands.ts +569 -46
- package/src/daemon-cli.ts +6 -0
- package/src/hooks/useInputManager.ts +16 -0
- package/src/hooks/usePluginManager.ts +104 -78
- package/src/index.ts +62 -8
- package/src/managers/inputHandlers.ts +14 -9
- package/src/managers/inputReducer.ts +29 -9
- package/src/reducers/confirmationReducer.ts +20 -1
- package/src/reducers/selectorReducer.ts +3 -0
- package/src/session-selector-cli.tsx +9 -255
- package/src/stdio/agentBridge.ts +397 -95
- package/src/stdio/protocol.ts +9 -0
- package/src/stdio-cli.ts +6 -0
- package/src/utils/sessionOwnership.ts +62 -0
- 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,9 @@ export const useInputManager = (
|
|
|
61
61
|
onRecallQueuedMessage,
|
|
62
62
|
onClearMessages,
|
|
63
63
|
onCompact,
|
|
64
|
+
onReloadPlugins,
|
|
64
65
|
onAddDir,
|
|
66
|
+
onRenameSession,
|
|
65
67
|
onPlanCommand,
|
|
66
68
|
isIdle: isIdleProp,
|
|
67
69
|
} = callbacks;
|
|
@@ -247,6 +249,8 @@ export const useInputManager = (
|
|
|
247
249
|
dispatch({ type: "SET_SHOW_AGENTS_MANAGER", payload: true });
|
|
248
250
|
} else if (command === "rewind") {
|
|
249
251
|
dispatch({ type: "SET_SHOW_REWIND_MANAGER", payload: true });
|
|
252
|
+
} else if (command === "resume") {
|
|
253
|
+
dispatch({ type: "SET_SHOW_RESUME_SELECTOR", payload: true });
|
|
250
254
|
} else if (command === "help") {
|
|
251
255
|
dispatch({ type: "SET_SHOW_HELP", payload: true });
|
|
252
256
|
} else if (command === "status") {
|
|
@@ -280,8 +284,12 @@ export const useInputManager = (
|
|
|
280
284
|
await onClearMessages?.();
|
|
281
285
|
} else if (command === "compact") {
|
|
282
286
|
await onCompact?.(effect.args);
|
|
287
|
+
} else if (command === "reload-plugins") {
|
|
288
|
+
await onReloadPlugins?.();
|
|
283
289
|
} else if (command === "add-dir") {
|
|
284
290
|
await onAddDir?.(effect.args);
|
|
291
|
+
} else if (command === "rename") {
|
|
292
|
+
await onRenameSession?.(effect.args);
|
|
285
293
|
} else if (command === "plan") {
|
|
286
294
|
await onPlanCommand?.(effect.args);
|
|
287
295
|
}
|
|
@@ -312,7 +320,9 @@ export const useInputManager = (
|
|
|
312
320
|
onRecallQueuedMessage,
|
|
313
321
|
onClearMessages,
|
|
314
322
|
onCompact,
|
|
323
|
+
onReloadPlugins,
|
|
315
324
|
onAddDir,
|
|
325
|
+
onRenameSession,
|
|
316
326
|
onPlanCommand,
|
|
317
327
|
]);
|
|
318
328
|
|
|
@@ -511,6 +521,10 @@ export const useInputManager = (
|
|
|
511
521
|
dispatch({ type: "SET_SHOW_REWIND_MANAGER", payload: show });
|
|
512
522
|
}, []);
|
|
513
523
|
|
|
524
|
+
const setShowResumeSelector = useCallback((show: boolean) => {
|
|
525
|
+
dispatch({ type: "SET_SHOW_RESUME_SELECTOR", payload: show });
|
|
526
|
+
}, []);
|
|
527
|
+
|
|
514
528
|
const setShowHelp = useCallback((show: boolean) => {
|
|
515
529
|
dispatch({ type: "SET_SHOW_HELP", payload: show });
|
|
516
530
|
}, []);
|
|
@@ -681,6 +695,7 @@ export const useInputManager = (
|
|
|
681
695
|
showMcpManager: state.showMcpManager,
|
|
682
696
|
showAgentsManager: state.showAgentsManager,
|
|
683
697
|
showRewindManager: state.showRewindManager,
|
|
698
|
+
showResumeSelector: state.showResumeSelector,
|
|
684
699
|
showHelp: state.showHelp,
|
|
685
700
|
showStatusCommand: state.showStatusCommand,
|
|
686
701
|
showLoginCommand: state.showLoginCommand,
|
|
@@ -735,6 +750,7 @@ export const useInputManager = (
|
|
|
735
750
|
setShowPluginManager,
|
|
736
751
|
setShowModelSelector,
|
|
737
752
|
setShowWorkflowManager,
|
|
753
|
+
setShowResumeSelector,
|
|
738
754
|
setShowSkillsManager,
|
|
739
755
|
setShowHooksManager,
|
|
740
756
|
setPermissionMode,
|
|
@@ -12,7 +12,11 @@ import {
|
|
|
12
12
|
} from "../components/PluginManagerTypes.js";
|
|
13
13
|
|
|
14
14
|
export function usePluginManager(options?: {
|
|
15
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
80
|
-
|
|
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
|
-
|
|
90
|
-
|
|
99
|
+
// Only show enabled plugins in the "Installed" view
|
|
100
|
+
setInstalledPlugins(allInstalledWithEnabled.filter((p) => p.enabled));
|
|
91
101
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
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
|
-
}, [
|
|
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.
|
|
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?.
|
|
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
|
},
|
package/src/index.ts
CHANGED
|
@@ -218,18 +218,30 @@ export async function main() {
|
|
|
218
218
|
)
|
|
219
219
|
.command(
|
|
220
220
|
"uninstall <plugin>",
|
|
221
|
-
"Uninstall a plugin",
|
|
221
|
+
"Uninstall a plugin from one scope (default: the scope it is enabled in for the current directory)",
|
|
222
222
|
(yargs) => {
|
|
223
|
-
return yargs
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
223
|
+
return yargs
|
|
224
|
+
.positional("plugin", {
|
|
225
|
+
describe: "Plugin to uninstall (format: name@marketplace)",
|
|
226
|
+
type: "string",
|
|
227
|
+
})
|
|
228
|
+
.option("scope", {
|
|
229
|
+
alias: "s",
|
|
230
|
+
describe: "Scope to uninstall from",
|
|
231
|
+
choices: ["user", "project", "local"],
|
|
232
|
+
type: "string",
|
|
233
|
+
});
|
|
227
234
|
},
|
|
228
235
|
async (argv) => {
|
|
229
236
|
const { uninstallPluginCommand } = await import(
|
|
230
237
|
"./commands/plugin/uninstall.js"
|
|
231
238
|
);
|
|
232
|
-
await uninstallPluginCommand(
|
|
239
|
+
await uninstallPluginCommand(
|
|
240
|
+
argv as {
|
|
241
|
+
plugin: string;
|
|
242
|
+
scope?: Scope;
|
|
243
|
+
},
|
|
244
|
+
);
|
|
233
245
|
},
|
|
234
246
|
)
|
|
235
247
|
.command(
|
|
@@ -312,8 +324,9 @@ export async function main() {
|
|
|
312
324
|
type: "string",
|
|
313
325
|
})
|
|
314
326
|
.option("lines", {
|
|
315
|
-
describe:
|
|
316
|
-
|
|
327
|
+
describe:
|
|
328
|
+
"Number of recent messages to show (0 = status line only)",
|
|
329
|
+
default: 1,
|
|
317
330
|
type: "number",
|
|
318
331
|
});
|
|
319
332
|
},
|
|
@@ -327,6 +340,47 @@ export async function main() {
|
|
|
327
340
|
);
|
|
328
341
|
},
|
|
329
342
|
)
|
|
343
|
+
.command(
|
|
344
|
+
"wait <sessionId>",
|
|
345
|
+
"Block until a session goes idle (or waits for approval), then print its final snapshot (exit 0 = idle, 3 = waiting for approval, 1 = error/timeout). Idle = no turn running, no background task/subagent running, nothing queued (same as `wave -p`); a session holding a long-running background task never goes idle, so give it --timeout",
|
|
346
|
+
(yargs) => {
|
|
347
|
+
return yargs
|
|
348
|
+
.positional("sessionId", {
|
|
349
|
+
describe: "Session ID hosted by the daemon",
|
|
350
|
+
type: "string",
|
|
351
|
+
})
|
|
352
|
+
.option("lines", {
|
|
353
|
+
describe:
|
|
354
|
+
"Number of recent messages to print on exit (0 = status line only)",
|
|
355
|
+
default: 1,
|
|
356
|
+
type: "number",
|
|
357
|
+
})
|
|
358
|
+
.option("from-busy", {
|
|
359
|
+
describe:
|
|
360
|
+
"Wait until the session has been observed busy at least once before accepting idle (removes the stale-snapshot race right after an async `send`; a running background task also counts as busy)",
|
|
361
|
+
default: false,
|
|
362
|
+
type: "boolean",
|
|
363
|
+
})
|
|
364
|
+
.option("timeout", {
|
|
365
|
+
describe:
|
|
366
|
+
"Seconds to wait before giving up (default: wait forever) — required for sessions with a long-running background task, which never go idle",
|
|
367
|
+
type: "number",
|
|
368
|
+
});
|
|
369
|
+
},
|
|
370
|
+
async (argv) => {
|
|
371
|
+
const { daemonWaitCommand, DEFAULT_DAEMON_SOCKET } =
|
|
372
|
+
await import("./daemon/commands.js");
|
|
373
|
+
await daemonWaitCommand(
|
|
374
|
+
DEFAULT_DAEMON_SOCKET,
|
|
375
|
+
argv.sessionId as string,
|
|
376
|
+
{
|
|
377
|
+
lines: argv.lines as number,
|
|
378
|
+
fromBusy: argv.fromBusy as boolean,
|
|
379
|
+
timeout: argv.timeout as number | undefined,
|
|
380
|
+
},
|
|
381
|
+
);
|
|
382
|
+
},
|
|
383
|
+
)
|
|
330
384
|
.command(
|
|
331
385
|
"send <sessionId> <message>",
|
|
332
386
|
"Inject a message into a session (async by default; --wait N to get the reply)",
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
InputAction,
|
|
7
7
|
InputManagerCallbacks,
|
|
8
8
|
isPasteImageKey,
|
|
9
|
+
permissionModeCycle,
|
|
9
10
|
} from "./inputReducer.js";
|
|
10
11
|
|
|
11
12
|
export const expandLongTextPlaceholders = (
|
|
@@ -136,15 +137,10 @@ export const cyclePermissionMode = (
|
|
|
136
137
|
dispatch: React.Dispatch<InputAction>,
|
|
137
138
|
callbacks: Partial<InputManagerCallbacks>,
|
|
138
139
|
) => {
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
"plan",
|
|
144
|
-
];
|
|
145
|
-
const currentIndex = modes.indexOf(currentMode);
|
|
146
|
-
const nextIndex = currentIndex === -1 ? 0 : (currentIndex + 1) % modes.length;
|
|
147
|
-
const nextMode = modes[nextIndex];
|
|
140
|
+
const currentIndex = permissionModeCycle.indexOf(currentMode);
|
|
141
|
+
const nextIndex =
|
|
142
|
+
currentIndex === -1 ? 0 : (currentIndex + 1) % permissionModeCycle.length;
|
|
143
|
+
const nextMode = permissionModeCycle[nextIndex];
|
|
148
144
|
|
|
149
145
|
callbacks.logger?.debug("Cycling permission mode", {
|
|
150
146
|
from: currentMode,
|
|
@@ -376,6 +372,8 @@ export const handleCommandSelect = (
|
|
|
376
372
|
dispatch({ type: "SET_SHOW_HOOKS_MANAGER", payload: true });
|
|
377
373
|
} else if (command === "rewind") {
|
|
378
374
|
dispatch({ type: "SET_SHOW_REWIND_MANAGER", payload: true });
|
|
375
|
+
} else if (command === "resume") {
|
|
376
|
+
dispatch({ type: "SET_SHOW_RESUME_SELECTOR", payload: true });
|
|
379
377
|
} else if (command === "help") {
|
|
380
378
|
dispatch({ type: "SET_SHOW_HELP", payload: true });
|
|
381
379
|
} else if (command === "status") {
|
|
@@ -401,8 +399,12 @@ export const handleCommandSelect = (
|
|
|
401
399
|
await callbacks.onClearMessages?.();
|
|
402
400
|
} else if (command === "compact") {
|
|
403
401
|
await callbacks.onCompact?.();
|
|
402
|
+
} else if (command === "reload-plugins") {
|
|
403
|
+
await callbacks.onReloadPlugins?.();
|
|
404
404
|
} else if (command === "add-dir") {
|
|
405
405
|
await callbacks.onAddDir?.();
|
|
406
|
+
} else if (command === "rename") {
|
|
407
|
+
await callbacks.onRenameSession?.();
|
|
406
408
|
}
|
|
407
409
|
}
|
|
408
410
|
})();
|
|
@@ -756,6 +758,7 @@ export const handleInput = async (
|
|
|
756
758
|
state.showBackgroundTaskManager ||
|
|
757
759
|
state.showMcpManager ||
|
|
758
760
|
state.showRewindManager ||
|
|
761
|
+
state.showResumeSelector ||
|
|
759
762
|
state.showHelp ||
|
|
760
763
|
state.showStatusCommand ||
|
|
761
764
|
state.showPluginManager ||
|
|
@@ -780,6 +783,7 @@ export const handleInput = async (
|
|
|
780
783
|
state.showBackgroundTaskManager ||
|
|
781
784
|
state.showMcpManager ||
|
|
782
785
|
state.showRewindManager ||
|
|
786
|
+
state.showResumeSelector ||
|
|
783
787
|
state.showHelp ||
|
|
784
788
|
state.showStatusCommand ||
|
|
785
789
|
state.showPluginManager ||
|
|
@@ -790,6 +794,7 @@ export const handleInput = async (
|
|
|
790
794
|
state.showBackgroundTaskManager ||
|
|
791
795
|
state.showMcpManager ||
|
|
792
796
|
state.showRewindManager ||
|
|
797
|
+
state.showResumeSelector ||
|
|
793
798
|
state.showHelp ||
|
|
794
799
|
state.showStatusCommand ||
|
|
795
800
|
state.showPluginManager ||
|
|
@@ -37,6 +37,18 @@ export const btwOverlayActiveRef: { current: boolean } = { current: false };
|
|
|
37
37
|
|
|
38
38
|
export const ESC_DOUBLE_PRESS_TIMEOUT_MS = 1000;
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Shift+Tab 循环的权限模式顺序(对齐 Claude Code:plan 之后才进入
|
|
42
|
+
* bypassPermissions)。单独导出,避免 handler 与 reducer 两条路径各写一份
|
|
43
|
+
* 而漂移。
|
|
44
|
+
*/
|
|
45
|
+
export const permissionModeCycle: PermissionMode[] = [
|
|
46
|
+
"default",
|
|
47
|
+
"acceptEdits",
|
|
48
|
+
"plan",
|
|
49
|
+
"bypassPermissions",
|
|
50
|
+
];
|
|
51
|
+
|
|
40
52
|
/**
|
|
41
53
|
* Paste-image shortcut: Ctrl+V everywhere, plus Alt+V on Windows — Windows
|
|
42
54
|
* terminals reserve Ctrl+V for their own system paste, so the key never
|
|
@@ -108,7 +120,9 @@ export interface InputManagerCallbacks {
|
|
|
108
120
|
) => Promise<string>;
|
|
109
121
|
onClearMessages?: () => Promise<void>;
|
|
110
122
|
onCompact?: (instructions?: string) => Promise<void>;
|
|
123
|
+
onReloadPlugins?: () => Promise<void>;
|
|
111
124
|
onAddDir?: (args?: string) => Promise<void>;
|
|
125
|
+
onRenameSession?: (args?: string) => Promise<void>;
|
|
112
126
|
onPlanCommand?: (args?: string) => Promise<void> | void;
|
|
113
127
|
sessionId?: string;
|
|
114
128
|
workdir?: string;
|
|
@@ -142,6 +156,7 @@ export interface InputState {
|
|
|
142
156
|
showMcpManager: boolean;
|
|
143
157
|
showAgentsManager: boolean;
|
|
144
158
|
showRewindManager: boolean;
|
|
159
|
+
showResumeSelector: boolean;
|
|
145
160
|
showHelp: boolean;
|
|
146
161
|
showStatusCommand: boolean;
|
|
147
162
|
showLoginCommand: boolean;
|
|
@@ -182,6 +197,7 @@ export const initialState: InputState = {
|
|
|
182
197
|
showMcpManager: false,
|
|
183
198
|
showAgentsManager: false,
|
|
184
199
|
showRewindManager: false,
|
|
200
|
+
showResumeSelector: false,
|
|
185
201
|
showHelp: false,
|
|
186
202
|
showStatusCommand: false,
|
|
187
203
|
showLoginCommand: false,
|
|
@@ -465,6 +481,7 @@ export type InputAction =
|
|
|
465
481
|
| { type: "SET_SHOW_MCP_MANAGER"; payload: boolean }
|
|
466
482
|
| { type: "SET_SHOW_AGENTS_MANAGER"; payload: boolean }
|
|
467
483
|
| { type: "SET_SHOW_REWIND_MANAGER"; payload: boolean }
|
|
484
|
+
| { type: "SET_SHOW_RESUME_SELECTOR"; payload: boolean }
|
|
468
485
|
| { type: "SET_SHOW_HELP"; payload: boolean }
|
|
469
486
|
| { type: "SET_SHOW_STATUS_COMMAND"; payload: boolean }
|
|
470
487
|
| { type: "SET_SHOW_LOGIN_COMMAND"; payload: boolean }
|
|
@@ -667,6 +684,12 @@ export function inputReducer(
|
|
|
667
684
|
showRewindManager: action.payload,
|
|
668
685
|
selectorJustUsed: !action.payload ? true : state.selectorJustUsed,
|
|
669
686
|
};
|
|
687
|
+
case "SET_SHOW_RESUME_SELECTOR":
|
|
688
|
+
return {
|
|
689
|
+
...state,
|
|
690
|
+
showResumeSelector: action.payload,
|
|
691
|
+
selectorJustUsed: !action.payload ? true : state.selectorJustUsed,
|
|
692
|
+
};
|
|
670
693
|
case "SET_SHOW_HELP":
|
|
671
694
|
return {
|
|
672
695
|
...state,
|
|
@@ -1076,6 +1099,7 @@ export function inputReducer(
|
|
|
1076
1099
|
state.showMcpManager ||
|
|
1077
1100
|
state.showAgentsManager ||
|
|
1078
1101
|
state.showRewindManager ||
|
|
1102
|
+
state.showResumeSelector ||
|
|
1079
1103
|
state.showHelp ||
|
|
1080
1104
|
state.showStatusCommand ||
|
|
1081
1105
|
state.showLoginCommand ||
|
|
@@ -1123,16 +1147,12 @@ export function inputReducer(
|
|
|
1123
1147
|
|
|
1124
1148
|
// 3. Special Shortcuts
|
|
1125
1149
|
if (key.tab && key.shift) {
|
|
1126
|
-
const
|
|
1127
|
-
"default",
|
|
1128
|
-
"acceptEdits",
|
|
1129
|
-
"bypassPermissions",
|
|
1130
|
-
"plan",
|
|
1131
|
-
];
|
|
1132
|
-
const currentIndex = modes.indexOf(state.permissionMode);
|
|
1150
|
+
const currentIndex = permissionModeCycle.indexOf(state.permissionMode);
|
|
1133
1151
|
const nextIndex =
|
|
1134
|
-
currentIndex === -1
|
|
1135
|
-
|
|
1152
|
+
currentIndex === -1
|
|
1153
|
+
? 0
|
|
1154
|
+
: (currentIndex + 1) % permissionModeCycle.length;
|
|
1155
|
+
const nextMode = permissionModeCycle[nextIndex];
|
|
1136
1156
|
return {
|
|
1137
1157
|
...state,
|
|
1138
1158
|
permissionMode: nextMode,
|