wave-code 1.0.7 → 1.0.9
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 +21 -0
- package/dist/components/ChatInterface.js +1 -1
- package/dist/components/ConfirmationDetails.d.ts +1 -0
- package/dist/components/ConfirmationDetails.js +5 -3
- package/dist/components/RewindCommand.js +11 -4
- package/dist/contexts/useChat.d.ts +17 -2
- package/dist/contexts/useChat.js +124 -12
- package/dist/daemon/commands.d.ts +49 -0
- package/dist/daemon/commands.js +341 -0
- package/dist/daemon/jsonRpcClient.d.ts +38 -0
- package/dist/daemon/jsonRpcClient.js +129 -0
- package/dist/daemon/socketClient.d.ts +13 -0
- package/dist/daemon/socketClient.js +26 -0
- package/dist/index.js +88 -0
- package/dist/stdio/agentBridge.d.ts +12 -0
- package/dist/stdio/agentBridge.js +63 -10
- package/dist/stdio/protocol.d.ts +1 -1
- package/package.json +2 -2
- package/src/components/ChatInterface.tsx +2 -0
- package/src/components/ConfirmationDetails.tsx +6 -0
- package/src/components/RewindCommand.tsx +10 -4
- package/src/contexts/useChat.tsx +149 -10
- package/src/daemon/commands.ts +444 -0
- package/src/daemon/jsonRpcClient.ts +158 -0
- package/src/daemon/socketClient.ts +34 -0
- package/src/index.ts +130 -0
- package/src/stdio/agentBridge.ts +76 -10
- package/src/stdio/protocol.ts +2 -0
package/bin/wave-code.js
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import { readFileSync, writeSync } from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
// `wave -v` / `wave --version` must be fast: editors probe the installed CLI
|
|
8
|
+
// version on every launch (e.g. the desktop app's auto-update check). Loading
|
|
9
|
+
// the full app graph (wave-agent-sdk, ink, highlight.js, ...) just to print
|
|
10
|
+
// the version takes 2-3s+ on a warm machine and can exceed callers' probe
|
|
11
|
+
// timeouts on cold starts (AV scan of freshly installed files), which they
|
|
12
|
+
// misread as "CLI missing/corrupt" → spurious re-installs. Print the version
|
|
13
|
+
// straight from package.json and exit before touching the app graph.
|
|
14
|
+
const versionArgs = ["-v", "--version"];
|
|
15
|
+
if (process.argv.slice(2).some((a) => versionArgs.includes(a))) {
|
|
16
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const packageJson = JSON.parse(
|
|
18
|
+
readFileSync(path.resolve(__dirname, "../package.json"), "utf-8"),
|
|
19
|
+
);
|
|
20
|
+
writeSync(1, `${packageJson.version}\n`);
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
|
|
3
24
|
// Import and start the CLI
|
|
4
25
|
import("../dist/index.js")
|
|
5
26
|
.then(async ({ main }) => {
|
|
@@ -57,5 +57,5 @@ export const ChatInterface = () => {
|
|
|
57
57
|
]);
|
|
58
58
|
if (!sessionId)
|
|
59
59
|
return null;
|
|
60
|
-
return (_jsxs(Box, { ref: chatInterfaceRef, flexDirection: "column", children: [_jsx(MessageList, { messages: displayMessages, isExpanded: isExpanded, version: version, workdir: workdir, forceStatic: forceStatic }, remountKey), !isConfirmationVisible && !isExpanded && (_jsxs(_Fragment, { children: [(isLoading || isCommandRunning || isCompacting) && (_jsx(LoadingIndicator, { isLoading: isLoading, isCommandRunning: isCommandRunning, isCompacting: isCompacting, latestTotalTokens: latestTotalTokens })), _jsx(TaskList, {}), _jsx(QueuedMessageList, {}), _jsx(InputBox, { isLoading: isLoading, isCommandRunning: isCommandRunning, isCompacting: isCompacting, sendMessage: sendMessage, abortMessage: abortMessage, mcpServers: mcpServers, connectMcpServer: connectMcpServer, disconnectMcpServer: disconnectMcpServer, slashCommands: slashCommands, hasSlashCommand: hasSlashCommand, latestTotalTokens: latestTotalTokens, maxInputTokens: maxInputTokens, showLoginHint: showLoginHint })] })), isConfirmationVisible && (_jsxs(_Fragment, { children: [forceStatic ? (_jsx(Static, { items: [{ key: "confirmation-details" }], children: () => (_jsx(ConfirmationDetails, { toolName: confirmingTool.name, toolInput: confirmingTool.input, planContent: confirmingTool.planContent, isExpanded: isExpanded }, "confirmation-details")) })) : (_jsx(ConfirmationDetails, { toolName: confirmingTool.name, toolInput: confirmingTool.input, planContent: confirmingTool.planContent, isExpanded: isExpanded })), _jsx(ConfirmationSelector, { toolName: confirmingTool.name, toolInput: confirmingTool.input, suggestedPrefix: confirmingTool.suggestedPrefix, hidePersistentOption: confirmingTool.hidePersistentOption, permissionMode: confirmingTool.permissionMode, isExpanded: isExpanded, onDecision: handleConfirmationDecision, onCancel: handleConfirmationCancel })] }))] }));
|
|
60
|
+
return (_jsxs(Box, { ref: chatInterfaceRef, flexDirection: "column", children: [_jsx(MessageList, { messages: displayMessages, isExpanded: isExpanded, version: version, workdir: workdir, forceStatic: forceStatic }, remountKey), !isConfirmationVisible && !isExpanded && (_jsxs(_Fragment, { children: [(isLoading || isCommandRunning || isCompacting) && (_jsx(LoadingIndicator, { isLoading: isLoading, isCommandRunning: isCommandRunning, isCompacting: isCompacting, latestTotalTokens: latestTotalTokens })), _jsx(TaskList, {}), _jsx(QueuedMessageList, {}), _jsx(InputBox, { isLoading: isLoading, isCommandRunning: isCommandRunning, isCompacting: isCompacting, sendMessage: sendMessage, abortMessage: abortMessage, mcpServers: mcpServers, connectMcpServer: connectMcpServer, disconnectMcpServer: disconnectMcpServer, slashCommands: slashCommands, hasSlashCommand: hasSlashCommand, latestTotalTokens: latestTotalTokens, maxInputTokens: maxInputTokens, showLoginHint: showLoginHint })] })), isConfirmationVisible && (_jsxs(_Fragment, { children: [forceStatic ? (_jsx(Static, { items: [{ key: "confirmation-details" }], children: () => (_jsx(ConfirmationDetails, { toolName: confirmingTool.name, toolInput: confirmingTool.input, planContent: confirmingTool.planContent, warning: confirmingTool.warning, isExpanded: isExpanded }, "confirmation-details")) })) : (_jsx(ConfirmationDetails, { toolName: confirmingTool.name, toolInput: confirmingTool.input, planContent: confirmingTool.planContent, warning: confirmingTool.warning, isExpanded: isExpanded })), _jsx(ConfirmationSelector, { toolName: confirmingTool.name, toolInput: confirmingTool.input, suggestedPrefix: confirmingTool.suggestedPrefix, hidePersistentOption: confirmingTool.hidePersistentOption, permissionMode: confirmingTool.permissionMode, isExpanded: isExpanded, onDecision: handleConfirmationDecision, onCancel: handleConfirmationCancel })] }))] }));
|
|
61
61
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
|
-
import { BASH_TOOL_NAME, EDIT_TOOL_NAME, WRITE_TOOL_NAME, EXIT_PLAN_MODE_TOOL_NAME, ENTER_PLAN_MODE_TOOL_NAME, ASK_USER_QUESTION_TOOL_NAME, } from "wave-agent-sdk";
|
|
3
|
+
import { BASH_TOOL_NAME, EDIT_TOOL_NAME, WRITE_TOOL_NAME, EXIT_PLAN_MODE_TOOL_NAME, ENTER_PLAN_MODE_TOOL_NAME, ASK_USER_QUESTION_TOOL_NAME, ARTIFACT_TOOL_NAME, } from "wave-agent-sdk";
|
|
4
4
|
import { DiffDisplay } from "./DiffDisplay.js";
|
|
5
5
|
import { PlanDisplay } from "./PlanDisplay.js";
|
|
6
6
|
import { highlightToAnsi } from "../utils/highlightUtils.js";
|
|
@@ -22,14 +22,16 @@ const getActionDescription = (toolName, toolInput) => {
|
|
|
22
22
|
return "Enter plan mode for complex task planning";
|
|
23
23
|
case ASK_USER_QUESTION_TOOL_NAME:
|
|
24
24
|
return "Answer questions to clarify intent";
|
|
25
|
+
case ARTIFACT_TOOL_NAME:
|
|
26
|
+
return `Publish file: ${toolInput.file_path || "unknown file"}`;
|
|
25
27
|
default:
|
|
26
28
|
return "Execute operation";
|
|
27
29
|
}
|
|
28
30
|
};
|
|
29
|
-
export const ConfirmationDetails = ({ toolName, toolInput, planContent, isExpanded = false, }) => {
|
|
31
|
+
export const ConfirmationDetails = ({ toolName, toolInput, planContent, warning, isExpanded = false, }) => {
|
|
30
32
|
const startLineNumber = toolInput?.startLineNumber ??
|
|
31
33
|
(toolName === WRITE_TOOL_NAME ? 1 : undefined);
|
|
32
|
-
const content = (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "yellow", borderBottom: false, borderLeft: false, borderRight: false, children: [_jsxs(Text, { color: "yellow", bold: true, children: ["Tool: ", toolName] }), _jsx(Text, { color: "yellow", children: getActionDescription(toolName, toolInput) }), _jsx(DiffDisplay, { toolName: toolName, parameters: JSON.stringify(toolInput), startLineNumber: startLineNumber }), toolName !== WRITE_TOOL_NAME &&
|
|
34
|
+
const content = (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "yellow", borderBottom: false, borderLeft: false, borderRight: false, children: [_jsxs(Text, { color: "yellow", bold: true, children: ["Tool: ", toolName] }), _jsx(Text, { color: "yellow", children: getActionDescription(toolName, toolInput) }), warning && _jsxs(Text, { color: "red", children: ["\u26A0 ", warning] }), _jsx(DiffDisplay, { toolName: toolName, parameters: JSON.stringify(toolInput), startLineNumber: startLineNumber }), toolName !== WRITE_TOOL_NAME &&
|
|
33
35
|
toolName !== EDIT_TOOL_NAME &&
|
|
34
36
|
toolName !== EXIT_PLAN_MODE_TOOL_NAME &&
|
|
35
37
|
toolName !== ENTER_PLAN_MODE_TOOL_NAME &&
|
|
@@ -16,10 +16,17 @@ export const RewindCommand = ({ messages: initialMessages, onSelect, onCancel, g
|
|
|
16
16
|
}
|
|
17
17
|
}, [getFullMessageThread]);
|
|
18
18
|
// Filter user messages as checkpoints, excluding meta messages and
|
|
19
|
-
// system-generated user-role messages (task notifications, hook injections)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
// system-generated user-role messages (task notifications, hook injections).
|
|
20
|
+
// Compaction is append-only: the same message id appears twice on the full
|
|
21
|
+
// thread (pre-compact history + post-compact append), so dedupe by id and
|
|
22
|
+
// keep the last occurrence (matching the folded view the user sees).
|
|
23
|
+
const checkpointMap = new Map();
|
|
24
|
+
messages.forEach((msg, index) => {
|
|
25
|
+
if (!isUserCheckpointMessage(msg))
|
|
26
|
+
return;
|
|
27
|
+
checkpointMap.set(msg.id ?? `index:${index}`, { msg, index });
|
|
28
|
+
});
|
|
29
|
+
const checkpoints = Array.from(checkpointMap.values());
|
|
23
30
|
const MAX_VISIBLE_ITEMS = 3;
|
|
24
31
|
const [state, dispatch] = useReducer(rewindSelectorReducer, {
|
|
25
32
|
selectedIndex: checkpoints.length - 1,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import type { Message, McpServerStatus, BackgroundTask, Task, SlashCommand, SubagentConfiguration, PermissionDecision, PermissionMode, QueuedMessage, WorkflowRun } from "wave-agent-sdk";
|
|
2
|
+
import type { Message, McpServerStatus, BackgroundTask, Task, SlashCommand, SubagentConfiguration, PermissionDecision, PermissionMode, QueuedMessage, WorkflowRun, ToolBlockUpdateCallbackParams } from "wave-agent-sdk";
|
|
3
3
|
import { BaseAppProps } from "../types.js";
|
|
4
4
|
export interface ChatContextType {
|
|
5
5
|
messages: Message[];
|
|
@@ -58,8 +58,9 @@ export interface ChatContextType {
|
|
|
58
58
|
hidePersistentOption?: boolean;
|
|
59
59
|
planContent?: string;
|
|
60
60
|
permissionMode?: PermissionMode;
|
|
61
|
+
warning?: string;
|
|
61
62
|
};
|
|
62
|
-
showConfirmation: (toolName: string, toolInput?: Record<string, unknown>, suggestedPrefix?: string, hidePersistentOption?: boolean, planContent?: string, permissionMode?: PermissionMode) => Promise<PermissionDecision>;
|
|
63
|
+
showConfirmation: (toolName: string, toolInput?: Record<string, unknown>, suggestedPrefix?: string, hidePersistentOption?: boolean, planContent?: string, permissionMode?: PermissionMode, warning?: string) => Promise<PermissionDecision>;
|
|
63
64
|
hideConfirmation: () => void;
|
|
64
65
|
handleConfirmationDecision: (decision: PermissionDecision) => void;
|
|
65
66
|
handleConfirmationCancel: () => void;
|
|
@@ -83,4 +84,18 @@ export declare const useChat: () => ChatContextType;
|
|
|
83
84
|
export interface ChatProviderProps extends BaseAppProps {
|
|
84
85
|
children: React.ReactNode;
|
|
85
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* Per-tool window-concat throttle for pure-delta tool parameter streaming:
|
|
89
|
+
* `parametersChunk` deltas are accumulated independently per tool block id
|
|
90
|
+
* within the cooldown window, so interleaved multi-tool streams lose no delta
|
|
91
|
+
* (a plain throttle's single last-args slot would drop every earlier tool's
|
|
92
|
+
* deltas, leaving the first tool without streaming parameters). `start` /
|
|
93
|
+
* `running` apply immediately (one-shot snapshots); `end` flushes pending
|
|
94
|
+
* streaming deltas first, then applies the authoritative parameters/result.
|
|
95
|
+
*/
|
|
96
|
+
export declare function createToolStreamingThrottle(fn: (params: ToolBlockUpdateCallbackParams) => void, wait: number): {
|
|
97
|
+
(params: ToolBlockUpdateCallbackParams): void;
|
|
98
|
+
cancel: () => void;
|
|
99
|
+
flush: () => void;
|
|
100
|
+
};
|
|
86
101
|
export declare const ChatProvider: React.FC<ChatProviderProps>;
|
package/dist/contexts/useChat.js
CHANGED
|
@@ -4,7 +4,6 @@ import { useInput, useStdout } from "ink";
|
|
|
4
4
|
import { useAppConfig } from "./useAppConfig.js";
|
|
5
5
|
import { Agent, OPERATION_CANCELLED_BY_USER, extractLatestTotalTokens, } from "wave-agent-sdk";
|
|
6
6
|
import { logger } from "../utils/logger.js";
|
|
7
|
-
import { throttle } from "../utils/throttle.js";
|
|
8
7
|
import { displayUsageSummary } from "../utils/usageSummary.js";
|
|
9
8
|
import { expandLongTextPlaceholders } from "../managers/inputHandlers.js";
|
|
10
9
|
const ChatContext = createContext(null);
|
|
@@ -15,6 +14,21 @@ export const useChat = () => {
|
|
|
15
14
|
}
|
|
16
15
|
return context;
|
|
17
16
|
};
|
|
17
|
+
/**
|
|
18
|
+
* Snapshot a SDK message for consumer state. The SDK mutates its internal
|
|
19
|
+
* message blocks in-place BEFORE firing the delta callback (it writes the full
|
|
20
|
+
* accumulated value to the shared block, then computes the chunk delta by
|
|
21
|
+
* slicing the new value). A consumer that pushed the SDK message object by
|
|
22
|
+
* live reference would read the already-updated block and append the delta
|
|
23
|
+
* again — the first delta is double-counted ("LetLet me think..."), affecting
|
|
24
|
+
* reasoning and text content alike. See docs/specs/core/stream-content-updates.md.
|
|
25
|
+
* The clone must be at least one layer deep (message + blocks) so the in-place
|
|
26
|
+
* block mutation never leaks into consumer state.
|
|
27
|
+
*/
|
|
28
|
+
const snapshotMessage = (message) => ({
|
|
29
|
+
...message,
|
|
30
|
+
blocks: message.blocks.map((block) => ({ ...block })),
|
|
31
|
+
});
|
|
18
32
|
/**
|
|
19
33
|
* Window-concat throttle for pure-delta streaming updates: chunks arriving
|
|
20
34
|
* within the cooldown window are merged so no delta is lost (a dropped delta
|
|
@@ -75,6 +89,86 @@ function createStreamingWindowThrottle(fn, wait) {
|
|
|
75
89
|
};
|
|
76
90
|
return throttled;
|
|
77
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* Per-tool window-concat throttle for pure-delta tool parameter streaming:
|
|
94
|
+
* `parametersChunk` deltas are accumulated independently per tool block id
|
|
95
|
+
* within the cooldown window, so interleaved multi-tool streams lose no delta
|
|
96
|
+
* (a plain throttle's single last-args slot would drop every earlier tool's
|
|
97
|
+
* deltas, leaving the first tool without streaming parameters). `start` /
|
|
98
|
+
* `running` apply immediately (one-shot snapshots); `end` flushes pending
|
|
99
|
+
* streaming deltas first, then applies the authoritative parameters/result.
|
|
100
|
+
*/
|
|
101
|
+
export function createToolStreamingThrottle(fn, wait) {
|
|
102
|
+
let timer = null;
|
|
103
|
+
let pending = null;
|
|
104
|
+
const fire = () => {
|
|
105
|
+
if (pending && pending.chunks.size > 0) {
|
|
106
|
+
const { messageId, chunks } = pending;
|
|
107
|
+
pending = null;
|
|
108
|
+
for (const [id, chunk] of chunks) {
|
|
109
|
+
fn({ messageId, id, parametersChunk: chunk, stage: "streaming" });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
const throttled = (params) => {
|
|
114
|
+
if (params.stage === "end") {
|
|
115
|
+
// Flush any deltas still pending inside the cooldown window first
|
|
116
|
+
if (timer) {
|
|
117
|
+
clearTimeout(timer);
|
|
118
|
+
timer = null;
|
|
119
|
+
}
|
|
120
|
+
fire();
|
|
121
|
+
fn(params);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (params.stage === "streaming") {
|
|
125
|
+
if (!pending) {
|
|
126
|
+
pending = { messageId: params.messageId, chunks: new Map() };
|
|
127
|
+
}
|
|
128
|
+
const prev = pending.chunks.get(params.id) || "";
|
|
129
|
+
pending.chunks.set(params.id, prev + (params.parametersChunk || ""));
|
|
130
|
+
if (!timer) {
|
|
131
|
+
timer = setTimeout(() => {
|
|
132
|
+
timer = null;
|
|
133
|
+
fire();
|
|
134
|
+
}, wait);
|
|
135
|
+
}
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
// start / running — one-shot snapshots applied immediately. Drop this
|
|
139
|
+
// tool's buffered streaming deltas first: start/running carry the
|
|
140
|
+
// authoritative parameters, and a pending timer would otherwise fire late
|
|
141
|
+
// with a stale `streaming` event, regressing this tool block's stage back
|
|
142
|
+
// to streaming (yellow dot -> gray) mid-execution. Other tools' in-flight
|
|
143
|
+
// chunks are kept so interleaved multi-tool streaming still accumulates.
|
|
144
|
+
if (pending) {
|
|
145
|
+
pending.chunks.delete(params.id);
|
|
146
|
+
if (pending.chunks.size === 0) {
|
|
147
|
+
pending = null;
|
|
148
|
+
if (timer) {
|
|
149
|
+
clearTimeout(timer);
|
|
150
|
+
timer = null;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
fn(params);
|
|
155
|
+
};
|
|
156
|
+
throttled.cancel = () => {
|
|
157
|
+
if (timer) {
|
|
158
|
+
clearTimeout(timer);
|
|
159
|
+
timer = null;
|
|
160
|
+
}
|
|
161
|
+
pending = null;
|
|
162
|
+
};
|
|
163
|
+
throttled.flush = () => {
|
|
164
|
+
if (timer) {
|
|
165
|
+
clearTimeout(timer);
|
|
166
|
+
timer = null;
|
|
167
|
+
}
|
|
168
|
+
fire();
|
|
169
|
+
};
|
|
170
|
+
return throttled;
|
|
171
|
+
}
|
|
78
172
|
export const ChatProvider = ({ children, bypassPermissions, permissionMode: initialPermissionMode, pluginDirs, additionalDirectories, tools, allowedTools, disallowedTools, workdir, worktreeSession, originalCwd, version, model, mcpServers, }) => {
|
|
79
173
|
const { restoreSessionId, continueLastSession } = useAppConfig();
|
|
80
174
|
const { stdout } = useStdout();
|
|
@@ -142,8 +236,8 @@ export const ChatProvider = ({ children, bypassPermissions, permissionMode: init
|
|
|
142
236
|
};
|
|
143
237
|
}));
|
|
144
238
|
}, 500), []);
|
|
145
|
-
const throttledToolBlockUpdate = useMemo(() =>
|
|
146
|
-
const { messageId, id: toolBlockId, ...updates } = params;
|
|
239
|
+
const throttledToolBlockUpdate = useMemo(() => createToolStreamingThrottle((params) => {
|
|
240
|
+
const { messageId, id: toolBlockId, parametersChunk, ...updates } = params;
|
|
147
241
|
setMessages((prev) => prev.map((m) => {
|
|
148
242
|
if (m.id !== messageId)
|
|
149
243
|
return m;
|
|
@@ -158,7 +252,7 @@ export const ChatProvider = ({ children, bypassPermissions, permissionMode: init
|
|
|
158
252
|
id: toolBlockId,
|
|
159
253
|
name: updates.name || "",
|
|
160
254
|
stage: updates.stage || "start",
|
|
161
|
-
parameters: updates.parameters || "",
|
|
255
|
+
parameters: (updates.parameters || "") + (parametersChunk || ""),
|
|
162
256
|
result: updates.result || "",
|
|
163
257
|
...updates,
|
|
164
258
|
},
|
|
@@ -168,7 +262,18 @@ export const ChatProvider = ({ children, bypassPermissions, permissionMode: init
|
|
|
168
262
|
return {
|
|
169
263
|
...m,
|
|
170
264
|
blocks: m.blocks.map((b, idx) => idx === toolBlockIndex && b.type === "tool"
|
|
171
|
-
? {
|
|
265
|
+
? {
|
|
266
|
+
...b,
|
|
267
|
+
...updates,
|
|
268
|
+
// Streaming carries only the delta; append it to the
|
|
269
|
+
// accumulated parameters. start/running/end carry the
|
|
270
|
+
// authoritative value and replace wholesale.
|
|
271
|
+
parameters: parametersChunk
|
|
272
|
+
? (b.parameters || "") + parametersChunk
|
|
273
|
+
: updates.parameters !== undefined
|
|
274
|
+
? updates.parameters
|
|
275
|
+
: b.parameters,
|
|
276
|
+
}
|
|
172
277
|
: b),
|
|
173
278
|
};
|
|
174
279
|
}));
|
|
@@ -232,13 +337,13 @@ export const ChatProvider = ({ children, bypassPermissions, permissionMode: init
|
|
|
232
337
|
// the incremental callbacks in initializeAgent below.
|
|
233
338
|
const refreshMessages = useCallback(() => {
|
|
234
339
|
if (!isExpandedRef.current && agentRef.current) {
|
|
235
|
-
const msgs =
|
|
340
|
+
const msgs = agentRef.current.messages.map(snapshotMessage);
|
|
236
341
|
setMessages(msgs);
|
|
237
342
|
setLatestTotalTokens(extractLatestTotalTokens(msgs));
|
|
238
343
|
}
|
|
239
344
|
}, []);
|
|
240
345
|
// Permission confirmation methods with queue support
|
|
241
|
-
const showConfirmation = useCallback(async (toolName, toolInput, suggestedPrefix, hidePersistentOption, planContent, permissionMode) => {
|
|
346
|
+
const showConfirmation = useCallback(async (toolName, toolInput, suggestedPrefix, hidePersistentOption, planContent, permissionMode, warning) => {
|
|
242
347
|
return new Promise((resolve, reject) => {
|
|
243
348
|
const queueItem = {
|
|
244
349
|
toolName,
|
|
@@ -247,6 +352,7 @@ export const ChatProvider = ({ children, bypassPermissions, permissionMode: init
|
|
|
247
352
|
hidePersistentOption,
|
|
248
353
|
planContent,
|
|
249
354
|
permissionMode,
|
|
355
|
+
warning,
|
|
250
356
|
resolver: resolve,
|
|
251
357
|
reject,
|
|
252
358
|
};
|
|
@@ -266,7 +372,9 @@ export const ChatProvider = ({ children, bypassPermissions, permissionMode: init
|
|
|
266
372
|
const last = msgs[msgs.length - 1];
|
|
267
373
|
if (!last || last.role !== "user")
|
|
268
374
|
return;
|
|
269
|
-
setMessages((prev) => prev.some((m) => m.id === last.id)
|
|
375
|
+
setMessages((prev) => prev.some((m) => m.id === last.id)
|
|
376
|
+
? prev
|
|
377
|
+
: [...prev, snapshotMessage(last)]);
|
|
270
378
|
},
|
|
271
379
|
onAssistantMessageAdded: (messageId) => {
|
|
272
380
|
if (isExpandedRef.current || !agentRef.current)
|
|
@@ -274,7 +382,9 @@ export const ChatProvider = ({ children, bypassPermissions, permissionMode: init
|
|
|
274
382
|
const msg = agentRef.current.messages.find((m) => m.id === messageId);
|
|
275
383
|
if (!msg)
|
|
276
384
|
return;
|
|
277
|
-
setMessages((prev) => prev.some((m) => m.id === messageId)
|
|
385
|
+
setMessages((prev) => prev.some((m) => m.id === messageId)
|
|
386
|
+
? prev
|
|
387
|
+
: [...prev, snapshotMessage(msg)]);
|
|
278
388
|
},
|
|
279
389
|
onAssistantContentUpdated: (params) => {
|
|
280
390
|
if (isExpandedRef.current)
|
|
@@ -426,7 +536,7 @@ export const ChatProvider = ({ children, bypassPermissions, permissionMode: init
|
|
|
426
536
|
// Create the permission callback inside the try block to access showConfirmation
|
|
427
537
|
const permissionCallback = async (context) => {
|
|
428
538
|
try {
|
|
429
|
-
return await showConfirmation(context.toolName, context.toolInput, context.suggestedPrefix, context.hidePersistentOption, context.planContent, context.permissionMode);
|
|
539
|
+
return await showConfirmation(context.toolName, context.toolInput, context.suggestedPrefix, context.hidePersistentOption, context.planContent, context.permissionMode, context.warning);
|
|
430
540
|
}
|
|
431
541
|
catch {
|
|
432
542
|
// If confirmation was cancelled or failed, deny the operation
|
|
@@ -471,9 +581,10 @@ export const ChatProvider = ({ children, bypassPermissions, permissionMode: init
|
|
|
471
581
|
};
|
|
472
582
|
agent.setWorktreeSession(session);
|
|
473
583
|
}
|
|
474
|
-
// Get initial state
|
|
584
|
+
// Get initial state — snapshot the SDK messages (never hold live
|
|
585
|
+
// references; see snapshotMessage)
|
|
475
586
|
setSessionId(agent.sessionId);
|
|
476
|
-
setMessages(agent.messages);
|
|
587
|
+
setMessages(agent.messages.map(snapshotMessage));
|
|
477
588
|
setIsLoading(agent.isLoading);
|
|
478
589
|
setLatestTotalTokens(extractLatestTotalTokens(agent.messages));
|
|
479
590
|
setIsCommandRunning(agent.isCommandRunning);
|
|
@@ -718,6 +829,7 @@ export const ChatProvider = ({ children, bypassPermissions, permissionMode: init
|
|
|
718
829
|
hidePersistentOption: next.hidePersistentOption,
|
|
719
830
|
planContent: next.planContent,
|
|
720
831
|
permissionMode: next.permissionMode,
|
|
832
|
+
warning: next.warning,
|
|
721
833
|
});
|
|
722
834
|
setIsConfirmationVisible(true);
|
|
723
835
|
setConfirmationQueue((prev) => prev.slice(1));
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `wave daemon` client subcommands — talk to the wave daemon's unix socket
|
|
3
|
+
* (JSON-RPC over newline-delimited JSON) to list hosted sessions, inspect
|
|
4
|
+
* progress, inject messages and respond to pending permission requests.
|
|
5
|
+
*
|
|
6
|
+
* All subcommands are non-interactive: results go to stdout, diagnostics to
|
|
7
|
+
* stderr, and every handler calls process.exit() itself (yargs would fall
|
|
8
|
+
* through to the TUI otherwise). Every command connects to the fixed default
|
|
9
|
+
* socket `~/.wave/daemon.sock` — the daemon only runs on remote hosts, so no
|
|
10
|
+
* `--socket` override is offered (spec: daemon-command.md).
|
|
11
|
+
*
|
|
12
|
+
* Attach semantics: `initialize {workdir, restoreSessionId}` + `restoreSession`
|
|
13
|
+
* re-attach to a live session in the daemon's in-memory registry, or reload a
|
|
14
|
+
* transcript from disk under the current working directory. A session that is
|
|
15
|
+
* nowhere (live registry or disk) silently starts a FRESH session under a
|
|
16
|
+
* different id — the only reliable existence check is the `restoreSession`
|
|
17
|
+
* rejection ("Session not found: <id>"), after which the junk fresh session
|
|
18
|
+
* must be destroyed via the envelope sessionId returned by `initialize`.
|
|
19
|
+
*/
|
|
20
|
+
/** Fixed default daemon socket (spec: 默认 socket 固定,无 --socket 覆盖). */
|
|
21
|
+
export declare const DEFAULT_DAEMON_SOCKET: string;
|
|
22
|
+
export declare function daemonListCommand(socketPath: string): Promise<void>;
|
|
23
|
+
export declare function daemonStatusCommand(socketPath: string, sessionId: string, lines?: number): Promise<void>;
|
|
24
|
+
export interface SendOptions {
|
|
25
|
+
timeout: number;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Send a message and wait for the reply that corresponds to it.
|
|
29
|
+
*
|
|
30
|
+
* Completion detection: `sendMessage` on an idle session resolves only after
|
|
31
|
+
* the whole turn finishes (InteractionService awaits sendAIMessage), while on a
|
|
32
|
+
* busy session it enqueues and returns immediately — so stopping on a bare
|
|
33
|
+
* `loadingChange:false` would exit early on the PREVIOUS turn's completion when
|
|
34
|
+
* queued behind a busy session. Instead, track the message IDs: `ourUserMessage`
|
|
35
|
+
* is the user message added when OUR turn starts (userMessageAdded), and the
|
|
36
|
+
* reply is the last assistantMessageAdded observed after it. A stale
|
|
37
|
+
* loading:false can then never satisfy the wait condition early (the reply has
|
|
38
|
+
* not been added yet).
|
|
39
|
+
*/
|
|
40
|
+
export declare function daemonSendCommand(socketPath: string, sessionId: string, message: string, options?: SendOptions): Promise<void>;
|
|
41
|
+
export interface RespondOptions {
|
|
42
|
+
allow?: boolean;
|
|
43
|
+
deny?: boolean;
|
|
44
|
+
reason?: string;
|
|
45
|
+
answer?: string;
|
|
46
|
+
rule?: string;
|
|
47
|
+
mode?: string;
|
|
48
|
+
}
|
|
49
|
+
export declare function daemonRespondCommand(socketPath: string, sessionId: string, requestId: string, options: RespondOptions): Promise<void>;
|