wave-code 1.5.0 → 1.6.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/bin/wave-code.js +2 -0
- package/dist/bundle/wave.mjs +777 -855
- package/package.json +7 -7
- package/src/cli.tsx +9 -3
- package/src/components/FileSelector.tsx +11 -8
- package/src/components/HelpView.tsx +1 -1
- package/src/components/InputBox.tsx +3 -5
- package/src/components/McpManager.tsx +26 -0
- package/src/components/MessageBlockItem.tsx +14 -2
- package/src/components/RemoteControlView.tsx +1 -1
- package/src/components/RewindCommand.tsx +40 -46
- package/src/contexts/useChat.tsx +61 -49
- package/src/daemon/commands.ts +266 -44
- package/src/daemon/daemonLog.ts +132 -0
- package/src/daemon/socketPath.ts +123 -0
- package/src/daemon-cli.ts +19 -6
- package/src/hooks/useInputManager.ts +27 -25
- package/src/index.ts +36 -4
- package/src/managers/inputHandlers.ts +3 -13
- package/src/managers/inputReducer.ts +44 -13
- package/src/print-cli.ts +199 -49
- package/src/remote/hostIdentity.ts +1 -0
- package/src/remote/imagePreview.ts +182 -0
- package/src/remote/messageWindow.ts +15 -5
- package/src/remote/notificationCoalescer.ts +16 -3
- package/src/remote/relayClient.ts +1 -1
- package/src/remote/remoteControlCliHost.ts +26 -4
- package/src/remote/remoteControlService.ts +70 -19
- package/src/remote/remoteNotifications.ts +5 -4
- package/src/remote/textPreview.ts +155 -0
- package/src/stdio/agentBridge.ts +450 -117
- package/src/stdio/daemonServer.ts +49 -33
- package/src/stdio/hostSlashCommands.ts +9 -4
- package/src/stdio/uiPayload.ts +50 -2
- package/src/utils/agentSelector.ts +29 -0
- package/src/utils/argvCompat.ts +23 -0
- package/src/utils/printOutput.ts +403 -0
- package/src/utils/usageSummary.ts +74 -24
- package/src/utils/worktree.ts +222 -16
- package/src/utils/rewindCheckpoints.ts +0 -14
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wave-code",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "CLI-based code assistant powered by AI, built with React and Ink",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"wrap-ansi": "^10.0.0",
|
|
60
60
|
"yargs": "^17.7.2",
|
|
61
61
|
"zod": "^3.23.8",
|
|
62
|
-
"wave-agent-sdk": "1.
|
|
62
|
+
"wave-agent-sdk": "1.6.0"
|
|
63
63
|
},
|
|
64
64
|
"engines": {
|
|
65
65
|
"node": ">=22"
|
|
@@ -69,11 +69,11 @@
|
|
|
69
69
|
"wave": "tsx --tsconfig tsconfig.dev.json src/index.ts",
|
|
70
70
|
"build": "rimraf dist && node scripts/bundle.mjs",
|
|
71
71
|
"type-check": "tsc --noEmit --incremental",
|
|
72
|
-
"test": "vitest run
|
|
73
|
-
"test:coverage": "vitest run --coverage
|
|
74
|
-
"test:unit": "vitest run --
|
|
75
|
-
"test:unit:coverage": "vitest run --coverage --
|
|
76
|
-
"test:integration": "vitest run
|
|
72
|
+
"test": "vitest run",
|
|
73
|
+
"test:coverage": "vitest run --coverage",
|
|
74
|
+
"test:unit": "vitest run --exclude 'tests/integration/**' --exclude '**/*.integration.test.ts'",
|
|
75
|
+
"test:unit:coverage": "vitest run --coverage --exclude 'tests/integration/**' --exclude '**/*.integration.test.ts'",
|
|
76
|
+
"test:integration": "vitest run tests/integration .integration.test",
|
|
77
77
|
"lint": "oxlint",
|
|
78
78
|
"format": "prettier --write ."
|
|
79
79
|
}
|
package/src/cli.tsx
CHANGED
|
@@ -3,7 +3,7 @@ import { render } from "ink";
|
|
|
3
3
|
import { ensureRuntimeDeps } from "wave-agent-sdk";
|
|
4
4
|
import { App } from "./components/App.js";
|
|
5
5
|
import { cleanupLogs } from "./utils/logger.js";
|
|
6
|
-
import { removeWorktree } from "./utils/worktree.js";
|
|
6
|
+
import { describeWorktreeSurvival, removeWorktree } from "./utils/worktree.js";
|
|
7
7
|
import { BaseAppProps } from "./types.js";
|
|
8
8
|
|
|
9
9
|
export interface CliOptions extends BaseAppProps {
|
|
@@ -96,8 +96,14 @@ export async function startCli(options: CliOptions): Promise<void> {
|
|
|
96
96
|
if (shouldRemoveWorktree && worktreeSession) {
|
|
97
97
|
process.stdout.write("\nDeleting worktree ...\n");
|
|
98
98
|
process.chdir(worktreeSession.repoRoot);
|
|
99
|
-
|
|
100
|
-
|
|
99
|
+
// "Done." only when it is: removal is best-effort, and a surviving
|
|
100
|
+
// directory (with its branch kept for it) needs the cleanup commands
|
|
101
|
+
// printed rather than a success line over the leftovers.
|
|
102
|
+
process.stdout.write(
|
|
103
|
+
(await removeWorktree(worktreeSession))
|
|
104
|
+
? "Done.\n"
|
|
105
|
+
: `\n${describeWorktreeSurvival(worktreeSession)}\n`,
|
|
106
|
+
);
|
|
101
107
|
}
|
|
102
108
|
|
|
103
109
|
process.exit(0);
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import React, { useReducer, useEffect } from "react";
|
|
2
2
|
import { Box, Text, useInput } from "ink";
|
|
3
|
-
import type {
|
|
3
|
+
import type { FileSelectorItem } from "../managers/inputReducer.js";
|
|
4
4
|
import {
|
|
5
5
|
selectorReducer,
|
|
6
6
|
type SelectorState,
|
|
7
7
|
} from "../reducers/selectorReducer.js";
|
|
8
8
|
|
|
9
|
-
export {
|
|
9
|
+
export type { FileSelectorItem };
|
|
10
10
|
|
|
11
11
|
export interface FileSelectorProps {
|
|
12
|
-
files:
|
|
12
|
+
files: FileSelectorItem[];
|
|
13
13
|
searchQuery: string;
|
|
14
14
|
isLoading?: boolean;
|
|
15
|
-
onSelect: (
|
|
15
|
+
onSelect: (item: FileSelectorItem) => void;
|
|
16
16
|
onCancel: () => void;
|
|
17
17
|
}
|
|
18
18
|
|
|
@@ -23,11 +23,11 @@ export const FileSelector: React.FC<FileSelectorProps> = ({
|
|
|
23
23
|
onSelect,
|
|
24
24
|
onCancel,
|
|
25
25
|
}) => {
|
|
26
|
-
const [state, dispatch] = useReducer(selectorReducer<
|
|
26
|
+
const [state, dispatch] = useReducer(selectorReducer<FileSelectorItem>, {
|
|
27
27
|
selectedIndex: 0,
|
|
28
28
|
pendingDecision: null,
|
|
29
29
|
items: [],
|
|
30
|
-
} as SelectorState<
|
|
30
|
+
} as SelectorState<FileSelectorItem>);
|
|
31
31
|
|
|
32
32
|
const { selectedIndex, pendingDecision, items: filesInState } = state;
|
|
33
33
|
|
|
@@ -42,7 +42,7 @@ export const FileSelector: React.FC<FileSelectorProps> = ({
|
|
|
42
42
|
|
|
43
43
|
if (pendingDecision === "select" || pendingDecision === "insert") {
|
|
44
44
|
if (filesInState.length > 0 && selectedIndex < filesInState.length) {
|
|
45
|
-
onSelect(filesInState[selectedIndex]
|
|
45
|
+
onSelect(filesInState[selectedIndex]);
|
|
46
46
|
}
|
|
47
47
|
} else if (pendingDecision === "cancel") {
|
|
48
48
|
onCancel();
|
|
@@ -124,8 +124,11 @@ export const FileSelector: React.FC<FileSelectorProps> = ({
|
|
|
124
124
|
backgroundColor={isSelected ? "cyan" : undefined}
|
|
125
125
|
>
|
|
126
126
|
{isSelected ? "▶ " : " "}
|
|
127
|
-
{fileItem.path}
|
|
127
|
+
{fileItem.agentName ? `@${fileItem.agentName}` : fileItem.path}
|
|
128
128
|
</Text>
|
|
129
|
+
{fileItem.agentDescription && (
|
|
130
|
+
<Text dimColor> {fileItem.agentDescription}</Text>
|
|
131
|
+
)}
|
|
129
132
|
</Box>
|
|
130
133
|
);
|
|
131
134
|
})}
|
|
@@ -65,7 +65,7 @@ export const HelpView: React.FC<HelpViewProps> = ({
|
|
|
65
65
|
});
|
|
66
66
|
|
|
67
67
|
const helpItems = [
|
|
68
|
-
{ key: "@", description: "Reference files" },
|
|
68
|
+
{ key: "@", description: "Reference files or subagents" },
|
|
69
69
|
{ key: "/", description: "Commands" },
|
|
70
70
|
{ key: "!", description: "Shell commands (e.g. !ls)" },
|
|
71
71
|
{ key: "Ctrl+R", description: "Search history" },
|
|
@@ -90,8 +90,7 @@ export const InputBox: React.FC<InputBoxProps> = ({
|
|
|
90
90
|
handleRewindSelect,
|
|
91
91
|
resumeSession,
|
|
92
92
|
backgroundCurrentTask,
|
|
93
|
-
|
|
94
|
-
getFullMessageThread,
|
|
93
|
+
getRewindCheckpoints,
|
|
95
94
|
sessionId,
|
|
96
95
|
workingDirectory,
|
|
97
96
|
askBtw,
|
|
@@ -212,10 +211,10 @@ export const InputBox: React.FC<InputBoxProps> = ({
|
|
|
212
211
|
onPlanCommand: handlePlanCommand,
|
|
213
212
|
sessionId,
|
|
214
213
|
workdir: workingDirectory,
|
|
215
|
-
getFullMessageThread,
|
|
216
214
|
hasQueuedMessages,
|
|
217
215
|
isIdle,
|
|
218
216
|
onRecallQueuedMessage,
|
|
217
|
+
subagentConfigurations: agentDefinitions,
|
|
219
218
|
});
|
|
220
219
|
|
|
221
220
|
// Keep setInputText ref updated for queue callbacks
|
|
@@ -402,10 +401,9 @@ export const InputBox: React.FC<InputBoxProps> = ({
|
|
|
402
401
|
|
|
403
402
|
{showRewindManager && (
|
|
404
403
|
<RewindCommand
|
|
405
|
-
messages={messages}
|
|
406
404
|
onSelect={handleRewindSelectWithClose}
|
|
407
405
|
onCancel={handleRewindCancel}
|
|
408
|
-
|
|
406
|
+
getCheckpoints={getRewindCheckpoints}
|
|
409
407
|
/>
|
|
410
408
|
)}
|
|
411
409
|
|
|
@@ -157,6 +157,26 @@ export const McpManager: React.FC<McpManagerProps> = ({
|
|
|
157
157
|
</Text>
|
|
158
158
|
</Box>
|
|
159
159
|
)}
|
|
160
|
+
{/* Not dialed yet (lazy dial): the tool list comes from the local
|
|
161
|
+
snapshot of the last successful connection, and says so. */}
|
|
162
|
+
{selectedServer.status !== "connected" &&
|
|
163
|
+
selectedServer.toolsFromCache && (
|
|
164
|
+
<Box>
|
|
165
|
+
<Text>
|
|
166
|
+
<Text color="blue">Tools:</Text>{" "}
|
|
167
|
+
{selectedServer.toolsFromCache.length} tools{" "}
|
|
168
|
+
<Text color="gray" dimColor>
|
|
169
|
+
(cached
|
|
170
|
+
{selectedServer.toolsCachedAt
|
|
171
|
+
? ` · last connected ${formatTime(
|
|
172
|
+
selectedServer.toolsCachedAt,
|
|
173
|
+
)}`
|
|
174
|
+
: ""}
|
|
175
|
+
)
|
|
176
|
+
</Text>
|
|
177
|
+
</Text>
|
|
178
|
+
</Box>
|
|
179
|
+
)}
|
|
160
180
|
{selectedServer.capabilities && (
|
|
161
181
|
<Box>
|
|
162
182
|
<Text>
|
|
@@ -270,6 +290,12 @@ export const McpManager: React.FC<McpManagerProps> = ({
|
|
|
270
290
|
{server.status === "connected" && server.toolCount && (
|
|
271
291
|
<Text color="green"> · {server.toolCount} tools</Text>
|
|
272
292
|
)}
|
|
293
|
+
{server.status !== "connected" && server.toolsFromCache && (
|
|
294
|
+
<Text color="gray" dimColor>
|
|
295
|
+
{" "}
|
|
296
|
+
· {server.toolsFromCache.length} tools (cached)
|
|
297
|
+
</Text>
|
|
298
|
+
)}
|
|
273
299
|
</Text>
|
|
274
300
|
{index === state.selectedIndex && (
|
|
275
301
|
<Box marginLeft={4} flexDirection="column">
|
|
@@ -41,9 +41,21 @@ export const MessageBlockItem = ({
|
|
|
41
41
|
</Box>
|
|
42
42
|
)}
|
|
43
43
|
|
|
44
|
-
{
|
|
44
|
+
{/* A line for the human, never sent to the model (see NoticeBlock in the
|
|
45
|
+
SDK): a failed turn (`error`, red) or a hook's status line (`info`,
|
|
46
|
+
muted). */}
|
|
47
|
+
{block.type === "notice" && (
|
|
45
48
|
<Box>
|
|
46
|
-
|
|
49
|
+
{block.level === "error" ? (
|
|
50
|
+
<Text color="red">Error: {block.content}</Text>
|
|
51
|
+
) : (
|
|
52
|
+
<Text
|
|
53
|
+
color={block.level === "warning" ? "yellow" : "gray"}
|
|
54
|
+
dimColor
|
|
55
|
+
>
|
|
56
|
+
{block.content}
|
|
57
|
+
</Text>
|
|
58
|
+
)}
|
|
47
59
|
</Box>
|
|
48
60
|
)}
|
|
49
61
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* RemoteControlView — `/remote-control` 的覆盖层(把这条前台会话交给手机)。
|
|
3
3
|
*
|
|
4
|
-
* 规格:docs/specs/
|
|
4
|
+
* 规格:docs/specs/remote/remote-web.md「前台 CLI 会话也能被手机接管」。
|
|
5
5
|
*
|
|
6
6
|
* **这个斜杠命令不带参数**:没有 `on` / `off` / `status` 子命令,也不收机器名。
|
|
7
7
|
* 判据是「状态自己决定动作」——`off` / `error` 时唯一合法动作是开启,`connecting`
|
|
@@ -1,84 +1,78 @@
|
|
|
1
1
|
import React, { useState, useReducer, useEffect } from "react";
|
|
2
2
|
import { Box, Text, useInput } from "ink";
|
|
3
|
-
import type {
|
|
4
|
-
import { getMessageContent } from "wave-agent-sdk";
|
|
3
|
+
import type { RewindCheckpoint } from "wave-agent-sdk";
|
|
5
4
|
import { rewindSelectorReducer } from "../reducers/rewindSelectorReducer.js";
|
|
6
|
-
|
|
5
|
+
|
|
6
|
+
/** 稳定的空表:加载期也要能渲染,但不能每帧造一个新数组(effect 依赖它)。 */
|
|
7
|
+
const NO_CHECKPOINTS: RewindCheckpoint[] = [];
|
|
7
8
|
|
|
8
9
|
export interface RewindCommandProps {
|
|
9
|
-
|
|
10
|
+
/**
|
|
11
|
+
* 取检查点表。SDK 扫转录当场投影,内存里不留整条线程——这一屏要的只是
|
|
12
|
+
* 「哪几条能回滚到」和它们在完整线程里的位置。
|
|
13
|
+
*/
|
|
14
|
+
getCheckpoints: () => Promise<RewindCheckpoint[]>;
|
|
15
|
+
/** 参数是在完整线程里的绝对位置,直接喂 `truncateHistory(index)`。 */
|
|
10
16
|
onSelect: (index: number) => void;
|
|
11
17
|
onCancel: () => void;
|
|
12
|
-
getFullMessageThread?: () => Promise<{
|
|
13
|
-
messages: Message[];
|
|
14
|
-
sessionIds: string[];
|
|
15
|
-
}>;
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
export const RewindCommand: React.FC<RewindCommandProps> = ({
|
|
19
|
-
|
|
21
|
+
getCheckpoints,
|
|
20
22
|
onSelect,
|
|
21
23
|
onCancel,
|
|
22
|
-
getFullMessageThread,
|
|
23
24
|
}) => {
|
|
24
|
-
const [
|
|
25
|
-
|
|
25
|
+
const [checkpoints, setCheckpoints] = useState<RewindCheckpoint[] | null>(
|
|
26
|
+
null,
|
|
27
|
+
);
|
|
28
|
+
const isLoading = checkpoints === null;
|
|
26
29
|
|
|
27
30
|
useEffect(() => {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
// Compaction is append-only: the same message id appears twice on the full
|
|
39
|
-
// thread (pre-compact history + post-compact append), so dedupe by id and
|
|
40
|
-
// keep the last occurrence (matching the folded view the user sees).
|
|
41
|
-
const checkpointMap = new Map<string, { msg: Message; index: number }>();
|
|
42
|
-
messages.forEach((msg, index) => {
|
|
43
|
-
if (!isUserCheckpointMessage(msg)) return;
|
|
44
|
-
checkpointMap.set(msg.id ?? `index:${index}`, { msg, index });
|
|
45
|
-
});
|
|
46
|
-
const checkpoints = Array.from(checkpointMap.values());
|
|
31
|
+
let cancelled = false;
|
|
32
|
+
getCheckpoints().then((loaded) => {
|
|
33
|
+
if (!cancelled) setCheckpoints(loaded);
|
|
34
|
+
});
|
|
35
|
+
return () => {
|
|
36
|
+
cancelled = true;
|
|
37
|
+
};
|
|
38
|
+
}, [getCheckpoints]);
|
|
39
|
+
|
|
40
|
+
const items = checkpoints ?? NO_CHECKPOINTS;
|
|
47
41
|
|
|
48
42
|
const MAX_VISIBLE_ITEMS = 3;
|
|
49
43
|
|
|
50
44
|
const [state, dispatch] = useReducer(rewindSelectorReducer, {
|
|
51
|
-
selectedIndex:
|
|
45
|
+
selectedIndex: items.length - 1,
|
|
52
46
|
pendingDecision: null,
|
|
53
47
|
});
|
|
54
48
|
|
|
55
49
|
const { selectedIndex, pendingDecision } = state;
|
|
56
50
|
|
|
57
|
-
// Update selectedIndex when checkpoints
|
|
51
|
+
// Update selectedIndex when checkpoints arrive
|
|
58
52
|
useEffect(() => {
|
|
59
|
-
dispatch({ type: "RESET_INDEX", index:
|
|
60
|
-
}, [
|
|
53
|
+
dispatch({ type: "RESET_INDEX", index: items.length - 1 });
|
|
54
|
+
}, [items.length]);
|
|
61
55
|
|
|
62
56
|
// Handle decisions from reducer
|
|
63
57
|
useEffect(() => {
|
|
64
58
|
if (!pendingDecision) return;
|
|
65
59
|
|
|
66
60
|
if (pendingDecision === "select") {
|
|
67
|
-
if (
|
|
68
|
-
onSelect(
|
|
61
|
+
if (items.length > 0 && selectedIndex >= 0) {
|
|
62
|
+
onSelect(items[selectedIndex].index);
|
|
69
63
|
}
|
|
70
64
|
} else if (pendingDecision === "cancel") {
|
|
71
65
|
onCancel();
|
|
72
66
|
}
|
|
73
67
|
|
|
74
68
|
dispatch({ type: "CLEAR_DECISION" });
|
|
75
|
-
}, [pendingDecision, selectedIndex,
|
|
69
|
+
}, [pendingDecision, selectedIndex, items, onSelect, onCancel]);
|
|
76
70
|
|
|
77
71
|
useInput((input, key) => {
|
|
78
72
|
dispatch({
|
|
79
73
|
type: "HANDLE_KEY",
|
|
80
74
|
key,
|
|
81
|
-
maxIndex:
|
|
75
|
+
maxIndex: items.length - 1,
|
|
82
76
|
});
|
|
83
77
|
});
|
|
84
78
|
|
|
@@ -92,12 +86,12 @@ export const RewindCommand: React.FC<RewindCommandProps> = ({
|
|
|
92
86
|
borderLeft={false}
|
|
93
87
|
borderRight={false}
|
|
94
88
|
>
|
|
95
|
-
<Text color="cyan">Loading
|
|
89
|
+
<Text color="cyan">Loading rewind checkpoints...</Text>
|
|
96
90
|
</Box>
|
|
97
91
|
);
|
|
98
92
|
}
|
|
99
93
|
|
|
100
|
-
if (
|
|
94
|
+
if (items.length === 0) {
|
|
101
95
|
return (
|
|
102
96
|
<Box
|
|
103
97
|
flexDirection="column"
|
|
@@ -118,10 +112,10 @@ export const RewindCommand: React.FC<RewindCommandProps> = ({
|
|
|
118
112
|
0,
|
|
119
113
|
Math.min(
|
|
120
114
|
selectedIndex - Math.floor(MAX_VISIBLE_ITEMS / 2),
|
|
121
|
-
Math.max(0,
|
|
115
|
+
Math.max(0, items.length - MAX_VISIBLE_ITEMS),
|
|
122
116
|
),
|
|
123
117
|
);
|
|
124
|
-
const visibleCheckpoints =
|
|
118
|
+
const visibleCheckpoints = items.slice(
|
|
125
119
|
startIndex,
|
|
126
120
|
startIndex + MAX_VISIBLE_ITEMS,
|
|
127
121
|
);
|
|
@@ -146,19 +140,19 @@ export const RewindCommand: React.FC<RewindCommandProps> = ({
|
|
|
146
140
|
{visibleCheckpoints.map((checkpoint, index) => {
|
|
147
141
|
const actualIndex = startIndex + index;
|
|
148
142
|
const isSelected = actualIndex === selectedIndex;
|
|
149
|
-
const content =
|
|
143
|
+
const content = checkpoint.content
|
|
150
144
|
.replace(/\n/g, "\\n")
|
|
151
145
|
.substring(0, 60);
|
|
152
146
|
|
|
153
147
|
return (
|
|
154
|
-
<Box key={checkpoint.
|
|
148
|
+
<Box key={checkpoint.id}>
|
|
155
149
|
<Text
|
|
156
150
|
color={isSelected ? "black" : "white"}
|
|
157
151
|
backgroundColor={isSelected ? "cyan" : undefined}
|
|
158
152
|
>
|
|
159
153
|
{isSelected ? "▶ " : " "}[{checkpoint.index}]{" "}
|
|
160
154
|
{content || "(No text content)"}
|
|
161
|
-
{actualIndex ===
|
|
155
|
+
{actualIndex === items.length - 1 ? " (Latest)" : ""}
|
|
162
156
|
</Text>
|
|
163
157
|
</Box>
|
|
164
158
|
);
|
package/src/contexts/useChat.tsx
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
} from "../hooks/useRemoteControl.js";
|
|
15
15
|
import type {
|
|
16
16
|
Message,
|
|
17
|
+
MessageBlock,
|
|
17
18
|
McpServerStatus,
|
|
18
19
|
BackgroundTask,
|
|
19
20
|
Task,
|
|
@@ -26,6 +27,7 @@ import type {
|
|
|
26
27
|
QueuedMessage,
|
|
27
28
|
WorkflowRun,
|
|
28
29
|
ToolBlockUpdateCallbackParams,
|
|
30
|
+
RewindCheckpoint,
|
|
29
31
|
} from "wave-agent-sdk";
|
|
30
32
|
import {
|
|
31
33
|
Agent,
|
|
@@ -184,10 +186,8 @@ export interface ChatContextType {
|
|
|
184
186
|
forceRemount: (options?: { preserveScrollback?: boolean }) => void;
|
|
185
187
|
// Rewind functionality
|
|
186
188
|
handleRewindSelect: (index: number) => Promise<void>;
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
sessionIds: string[];
|
|
190
|
-
}>;
|
|
189
|
+
/** `/rewind` 的检查点表:SDK 扫转录投影,CLI 不留整条线程。 */
|
|
190
|
+
getRewindCheckpoints: () => Promise<RewindCheckpoint[]>;
|
|
191
191
|
// Status metadata
|
|
192
192
|
getGatewayConfig: () => import("wave-agent-sdk").GatewayConfig;
|
|
193
193
|
getModelConfig: () => import("wave-agent-sdk").ModelConfig;
|
|
@@ -364,6 +364,38 @@ const applyToolBlockUpdate = (
|
|
|
364
364
|
});
|
|
365
365
|
};
|
|
366
366
|
|
|
367
|
+
/**
|
|
368
|
+
* Append a block to the last message — only when it is an assistant message
|
|
369
|
+
* (the current turn's in-flight reply). If the last message is a user message,
|
|
370
|
+
* the block belongs BELOW it: create a new assistant message instead of
|
|
371
|
+
* polluting the stale assistant from an earlier turn. Same rule the webview
|
|
372
|
+
* reducer applies (APPEND_NOTICE_BLOCK).
|
|
373
|
+
*
|
|
374
|
+
* Both the error and the system line use this: the SDK appends them to the
|
|
375
|
+
* same place (`appendBlockToLastAssistant`), so the CLI must not invent a
|
|
376
|
+
* second placement rule.
|
|
377
|
+
*/
|
|
378
|
+
const appendBlockToLastAssistant = (
|
|
379
|
+
prev: Message[],
|
|
380
|
+
block: MessageBlock,
|
|
381
|
+
): Message[] => {
|
|
382
|
+
const last = prev[prev.length - 1];
|
|
383
|
+
if (last && last.role === "assistant") {
|
|
384
|
+
return prev.map((m, idx) =>
|
|
385
|
+
idx === prev.length - 1 ? { ...m, blocks: [...m.blocks, block] } : m,
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
return [
|
|
389
|
+
...prev,
|
|
390
|
+
{
|
|
391
|
+
id: `msg-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`,
|
|
392
|
+
role: "assistant",
|
|
393
|
+
timestamp: new Date().toISOString(),
|
|
394
|
+
blocks: [block],
|
|
395
|
+
},
|
|
396
|
+
];
|
|
397
|
+
};
|
|
398
|
+
|
|
367
399
|
export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
368
400
|
children,
|
|
369
401
|
bypassPermissions,
|
|
@@ -727,39 +759,19 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
727
759
|
// `end` carries the authoritative final values.
|
|
728
760
|
setMessages((prev) => applyToolBlockUpdate(prev, params));
|
|
729
761
|
},
|
|
730
|
-
|
|
731
|
-
remoteStream?.
|
|
762
|
+
onNoticeAdded: ({ level, content }) => {
|
|
763
|
+
remoteStream?.onNoticeAdded({ level, content });
|
|
732
764
|
if (isExpandedRef.current) return;
|
|
733
|
-
// One-shot structural update — applied directly (
|
|
734
|
-
// delayed by the tool-running window
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
if (last && last.role === "assistant") {
|
|
744
|
-
return prev.map((m, idx) =>
|
|
745
|
-
idx === prev.length - 1
|
|
746
|
-
? {
|
|
747
|
-
...m,
|
|
748
|
-
blocks: [...m.blocks, { type: "error", content: error }],
|
|
749
|
-
}
|
|
750
|
-
: m,
|
|
751
|
-
);
|
|
752
|
-
}
|
|
753
|
-
return [
|
|
754
|
-
...prev,
|
|
755
|
-
{
|
|
756
|
-
id: `msg-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`,
|
|
757
|
-
role: "assistant",
|
|
758
|
-
timestamp: new Date().toISOString(),
|
|
759
|
-
blocks: [{ type: "error", content: error }],
|
|
760
|
-
},
|
|
761
|
-
];
|
|
762
|
-
});
|
|
765
|
+
// One-shot structural update — applied directly (a failure must not
|
|
766
|
+
// be delayed by the tool-running window; a hook's round line must
|
|
767
|
+
// not wait for the next `refreshMessages`).
|
|
768
|
+
setMessages((prev) =>
|
|
769
|
+
appendBlockToLastAssistant(prev, {
|
|
770
|
+
type: "notice",
|
|
771
|
+
level,
|
|
772
|
+
content,
|
|
773
|
+
}),
|
|
774
|
+
);
|
|
763
775
|
},
|
|
764
776
|
onLatestTotalTokensChange: (tokens) => {
|
|
765
777
|
setLatestTotalTokens(tokens);
|
|
@@ -782,9 +794,9 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
782
794
|
}
|
|
783
795
|
},
|
|
784
796
|
onCompactionContentUpdate: (content) => {
|
|
785
|
-
//
|
|
786
|
-
// indicator renders only
|
|
787
|
-
setCompactionStream(content);
|
|
797
|
+
// Delta of newly produced compaction text — appended; the loading
|
|
798
|
+
// indicator renders only the last 30 characters (streaming tail).
|
|
799
|
+
setCompactionStream((prev) => prev + content);
|
|
788
800
|
},
|
|
789
801
|
onBackgroundTasksChange: async (tasks) => {
|
|
790
802
|
setBackgroundTasks([...tasks]);
|
|
@@ -1214,7 +1226,8 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
1214
1226
|
timestamp: new Date().toISOString(),
|
|
1215
1227
|
blocks: [
|
|
1216
1228
|
{
|
|
1217
|
-
type: "
|
|
1229
|
+
type: "notice",
|
|
1230
|
+
level: "error",
|
|
1218
1231
|
content: `Failed to resume conversation: ${
|
|
1219
1232
|
error instanceof Error ? error.message : String(error)
|
|
1220
1233
|
}`,
|
|
@@ -1468,12 +1481,11 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
1468
1481
|
renameSession: (title) => agentRef.current?.renameSession(title),
|
|
1469
1482
|
compact: (instructions) => compact(instructions),
|
|
1470
1483
|
slashCommands: () => agentRef.current?.getSlashCommands() ?? [],
|
|
1471
|
-
// 手机页取首屏尾段 / 补差都读 agent
|
|
1472
|
-
//
|
|
1484
|
+
// 手机页取首屏尾段 / 补差都读 agent 手上那份列表(压缩后的上下文);它这一侧
|
|
1485
|
+
// 不提供上翻,所以不会带来 `before` 锚点。
|
|
1473
1486
|
messagePage: (window) =>
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
),
|
|
1487
|
+
agentRef.current?.getMessagePage(window) ??
|
|
1488
|
+
Promise.resolve({ messages: [] }),
|
|
1477
1489
|
currentThread: () =>
|
|
1478
1490
|
Promise.resolve([...(agentRef.current?.messages ?? [])]),
|
|
1479
1491
|
threadSummary: () =>
|
|
@@ -1505,11 +1517,11 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
1505
1517
|
[forceRemount, refreshMessages],
|
|
1506
1518
|
);
|
|
1507
1519
|
|
|
1508
|
-
const
|
|
1520
|
+
const getRewindCheckpoints = useCallback(async () => {
|
|
1509
1521
|
if (agentRef.current) {
|
|
1510
|
-
return await agentRef.current.
|
|
1522
|
+
return await agentRef.current.getRewindCheckpoints();
|
|
1511
1523
|
}
|
|
1512
|
-
return
|
|
1524
|
+
return [];
|
|
1513
1525
|
}, []);
|
|
1514
1526
|
|
|
1515
1527
|
const getGatewayConfig = useCallback(() => {
|
|
@@ -1630,7 +1642,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
1630
1642
|
remountKey,
|
|
1631
1643
|
forceRemount,
|
|
1632
1644
|
handleRewindSelect,
|
|
1633
|
-
|
|
1645
|
+
getRewindCheckpoints,
|
|
1634
1646
|
|
|
1635
1647
|
getGatewayConfig,
|
|
1636
1648
|
getModelConfig,
|