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
|
@@ -59,6 +59,8 @@ export class AgentBridge {
|
|
|
59
59
|
return this.getSessionInfo(sessionId);
|
|
60
60
|
case "listPendingPermissions":
|
|
61
61
|
return this.listPendingPermissions();
|
|
62
|
+
case "listDaemonSessions":
|
|
63
|
+
return this.listDaemonSessions();
|
|
62
64
|
case "updateConfig":
|
|
63
65
|
return this.updateConfig(p, sessionId);
|
|
64
66
|
case "getConfiguredModels":
|
|
@@ -474,9 +476,39 @@ export class AgentBridge {
|
|
|
474
476
|
catch {
|
|
475
477
|
// Best-effort; don't block message sending on history save failure
|
|
476
478
|
}
|
|
477
|
-
await entry.agent.sendMessage(params.text, params.images);
|
|
479
|
+
await entry.agent.sendMessage(params.text, this.persistDataUrlImages(params.images));
|
|
478
480
|
return null;
|
|
479
481
|
}
|
|
482
|
+
/**
|
|
483
|
+
* Webview hosts (desktop/vscode/jetbrains) send pasted images as inline
|
|
484
|
+
* data URLs — there is no local file behind them. Persist each to a temp
|
|
485
|
+
* file so the model gets a real path it can reference with tools (aligned
|
|
486
|
+
* with Claude Code's `[Image source: <path>]` metadata). Real paths pass
|
|
487
|
+
* through untouched; unparseable data URLs pass through as-is and are
|
|
488
|
+
* skipped by the SDK rather than blocking the message.
|
|
489
|
+
*/
|
|
490
|
+
persistDataUrlImages(images) {
|
|
491
|
+
if (!images || images.length === 0)
|
|
492
|
+
return images;
|
|
493
|
+
return images.map((img) => {
|
|
494
|
+
if (!img.path.startsWith("data:"))
|
|
495
|
+
return img;
|
|
496
|
+
const match = /^data:([^;,]+);base64,(.*)$/s.exec(img.path);
|
|
497
|
+
if (!match)
|
|
498
|
+
return img;
|
|
499
|
+
try {
|
|
500
|
+
const mimeType = match[1];
|
|
501
|
+
const ext = mimeType.split("/")[1]?.replace("jpeg", "jpg") || "png";
|
|
502
|
+
const filePath = join(tmpdir(), `wave-image-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.${ext}`);
|
|
503
|
+
writeFileSync(filePath, Buffer.from(match[2], "base64"));
|
|
504
|
+
return { path: filePath, mimeType };
|
|
505
|
+
}
|
|
506
|
+
catch (error) {
|
|
507
|
+
logger.warn("Failed to persist pasted image to temp file:", error);
|
|
508
|
+
return img;
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
}
|
|
480
512
|
async bang(command, sessionId) {
|
|
481
513
|
const entry = this.requireSession(sessionId);
|
|
482
514
|
await entry.agent.bang(command);
|
|
@@ -511,7 +543,10 @@ export class AgentBridge {
|
|
|
511
543
|
async rewindToMessage(messageId, sessionId) {
|
|
512
544
|
const entry = this.requireSession(sessionId);
|
|
513
545
|
const { messages } = await entry.agent.getFullMessageThread();
|
|
514
|
-
|
|
546
|
+
// 压缩是 append-only:同 id 消息(压缩前历史 + 压缩后 append 的重复)会
|
|
547
|
+
// 在磁盘完整线程中出现多次。用户看到的折叠视图对应最后一次出现,
|
|
548
|
+
// 因此匹配最后一个而非第一个,避免回滚时连压缩摘要一起删掉。
|
|
549
|
+
const index = messages.map((m) => m.id).lastIndexOf(messageId);
|
|
515
550
|
if (index === -1) {
|
|
516
551
|
throw new RpcError(PROTOCOL_INTERNAL_ERROR, `Message not found: ${messageId}`);
|
|
517
552
|
}
|
|
@@ -523,13 +558,19 @@ export class AgentBridge {
|
|
|
523
558
|
async listRewindCheckpoints(sessionId) {
|
|
524
559
|
const entry = this.requireSession(sessionId);
|
|
525
560
|
const { messages } = await entry.agent.getFullMessageThread();
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
561
|
+
// 压缩 append-only 后同 id 消息在磁盘完整线程中重复出现(压缩前历史 +
|
|
562
|
+
// 压缩后 append 的重复)。按 id 去重并保留最后一次出现(与折叠后的
|
|
563
|
+
// UI/内存视图一致),避免弹窗把同一条用户消息显示两遍。
|
|
564
|
+
const checkpointMap = new Map();
|
|
565
|
+
for (const m of messages) {
|
|
566
|
+
if (isUserCheckpointMessage(m) && m.id) {
|
|
567
|
+
checkpointMap.set(m.id, {
|
|
568
|
+
id: m.id,
|
|
569
|
+
content: getMessageContent(m).replace(/\s+/g, " ").trim(),
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
return { checkpoints: Array.from(checkpointMap.values()) };
|
|
533
574
|
}
|
|
534
575
|
deleteQueuedMessage(index, sessionId) {
|
|
535
576
|
const entry = this.requireSession(sessionId);
|
|
@@ -540,7 +581,7 @@ export class AgentBridge {
|
|
|
540
581
|
const entry = this.requireSession(sessionId);
|
|
541
582
|
const ok = entry.agent.updateQueuedMessageById(id, {
|
|
542
583
|
content: text,
|
|
543
|
-
images,
|
|
584
|
+
images: this.persistDataUrlImages(images),
|
|
544
585
|
});
|
|
545
586
|
return { ok };
|
|
546
587
|
}
|
|
@@ -698,6 +739,18 @@ export class AgentBridge {
|
|
|
698
739
|
})),
|
|
699
740
|
};
|
|
700
741
|
}
|
|
742
|
+
/** Daemon list: expose the in-memory session registry (live sessions only,
|
|
743
|
+
* not disk-scanning). Registration order is preserved. */
|
|
744
|
+
listDaemonSessions() {
|
|
745
|
+
return {
|
|
746
|
+
sessions: [...this.sessions.entries()].map(([sessionId, entry]) => ({
|
|
747
|
+
sessionId,
|
|
748
|
+
workingDirectory: entry.agent.workingDirectory,
|
|
749
|
+
isLoading: entry.agent.isLoading,
|
|
750
|
+
messageCount: entry.agent.messages.length,
|
|
751
|
+
})),
|
|
752
|
+
};
|
|
753
|
+
}
|
|
701
754
|
// ── Auth (global) ────────────────────────────────────────────
|
|
702
755
|
async getAuthStatus() {
|
|
703
756
|
const authService = AuthService.getInstance();
|
package/dist/stdio/protocol.d.ts
CHANGED
|
@@ -34,7 +34,7 @@ export declare const INVALID_REQUEST = -32600;
|
|
|
34
34
|
export declare const METHOD_NOT_FOUND = -32601;
|
|
35
35
|
export declare const INVALID_PARAMS = -32602;
|
|
36
36
|
export declare const INTERNAL_ERROR = -32603;
|
|
37
|
-
export type RequestMethod = "initialize" | "destroy" | "restoreSession" | "listSessions" | "getSessionInfo" | "sendMessage" | "bang" | "askBtw" | "abortMessage" | "clearMessages" | "rewindToMessage" | "listRewindCheckpoints" | "deleteQueuedMessage" | "updateQueuedMessage" | "deleteQueuedMessageById" | "getMessages" | "getFullMessageThread" | "setPermissionMode" | "getPermissionMode" | "getMcpServers" | "connectMcpServer" | "disconnectMcpServer" | "getSlashCommands" | "searchFiles" | "writeArtifactFile" | "getPromptHistory" | "searchPromptHistory" | "updateConfig" | "getConfiguredModels" | "setModel" | "listPendingPermissions" | "getAuthStatus" | "login" | "logout" | "listPlugins" | "installPlugin" | "uninstallPlugin" | "enablePlugin" | "disablePlugin" | "updatePlugin" | "listMarketplaces" | "addMarketplace" | "removeMarketplace" | "updateMarketplace" | "compact" | "getBackgroundTaskOutput" | "stopBackgroundTask" | "getWorkflowRuns" | "stopWorkflowRun" | "listGitBranches" | "createWorktree" | "removeWorktree";
|
|
37
|
+
export type RequestMethod = "initialize" | "destroy" | "restoreSession" | "listSessions" | "getSessionInfo" | "sendMessage" | "bang" | "askBtw" | "abortMessage" | "clearMessages" | "rewindToMessage" | "listRewindCheckpoints" | "deleteQueuedMessage" | "updateQueuedMessage" | "deleteQueuedMessageById" | "getMessages" | "getFullMessageThread" | "setPermissionMode" | "getPermissionMode" | "getMcpServers" | "connectMcpServer" | "disconnectMcpServer" | "getSlashCommands" | "searchFiles" | "writeArtifactFile" | "getPromptHistory" | "searchPromptHistory" | "updateConfig" | "getConfiguredModels" | "setModel" | "listPendingPermissions" | "listDaemonSessions" | "getAuthStatus" | "login" | "logout" | "listPlugins" | "installPlugin" | "uninstallPlugin" | "enablePlugin" | "disablePlugin" | "updatePlugin" | "listMarketplaces" | "addMarketplace" | "removeMarketplace" | "updateMarketplace" | "compact" | "getBackgroundTaskOutput" | "stopBackgroundTask" | "getWorkflowRuns" | "stopWorkflowRun" | "listGitBranches" | "createWorktree" | "removeWorktree";
|
|
38
38
|
export type ClientNotificationMethod = "permissionResponse";
|
|
39
39
|
export type ServerNotificationMethod = "userMessageAdded" | "assistantMessageAdded" | "assistantContentUpdated" | "assistantReasoningUpdated" | "toolBlockUpdated" | "errorBlockAdded" | "loadingChange" | "commandRunningChange" | "queuedMessagesChange" | "tasksChange" | "sessionIdChange" | "permissionModeChange" | "mcpServersChange" | "workdirChange" | "bangMessageAdded" | "bangMessageUpdated" | "bangMessageCompleted" | "notificationMessageAdded" | "permissionRequest" | "authUrl" | "compactBlockAdded" | "compactionStateChange" | "backgroundTasksChange" | "btwContent";
|
|
40
40
|
export declare function isRequest(msg: unknown): msg is JsonRpcRequest;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wave-code",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.9",
|
|
4
4
|
"description": "CLI-based code assistant powered by AI, built with React and Ink",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"wrap-ansi": "^10.0.0",
|
|
44
44
|
"yargs": "^17.7.2",
|
|
45
45
|
"zod": "^3.23.8",
|
|
46
|
-
"wave-agent-sdk": "1.0.
|
|
46
|
+
"wave-agent-sdk": "1.0.9"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@types/react": "^19.1.8",
|
|
@@ -142,6 +142,7 @@ export const ChatInterface: React.FC = () => {
|
|
|
142
142
|
toolName={confirmingTool!.name}
|
|
143
143
|
toolInput={confirmingTool!.input}
|
|
144
144
|
planContent={confirmingTool!.planContent}
|
|
145
|
+
warning={confirmingTool!.warning}
|
|
145
146
|
isExpanded={isExpanded}
|
|
146
147
|
/>
|
|
147
148
|
)}
|
|
@@ -151,6 +152,7 @@ export const ChatInterface: React.FC = () => {
|
|
|
151
152
|
toolName={confirmingTool!.name}
|
|
152
153
|
toolInput={confirmingTool!.input}
|
|
153
154
|
planContent={confirmingTool!.planContent}
|
|
155
|
+
warning={confirmingTool!.warning}
|
|
154
156
|
isExpanded={isExpanded}
|
|
155
157
|
/>
|
|
156
158
|
)}
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
EXIT_PLAN_MODE_TOOL_NAME,
|
|
8
8
|
ENTER_PLAN_MODE_TOOL_NAME,
|
|
9
9
|
ASK_USER_QUESTION_TOOL_NAME,
|
|
10
|
+
ARTIFACT_TOOL_NAME,
|
|
10
11
|
} from "wave-agent-sdk";
|
|
11
12
|
import { DiffDisplay } from "./DiffDisplay.js";
|
|
12
13
|
import { PlanDisplay } from "./PlanDisplay.js";
|
|
@@ -34,6 +35,8 @@ const getActionDescription = (
|
|
|
34
35
|
return "Enter plan mode for complex task planning";
|
|
35
36
|
case ASK_USER_QUESTION_TOOL_NAME:
|
|
36
37
|
return "Answer questions to clarify intent";
|
|
38
|
+
case ARTIFACT_TOOL_NAME:
|
|
39
|
+
return `Publish file: ${toolInput.file_path || "unknown file"}`;
|
|
37
40
|
default:
|
|
38
41
|
return "Execute operation";
|
|
39
42
|
}
|
|
@@ -43,6 +46,7 @@ export interface ConfirmationDetailsProps {
|
|
|
43
46
|
toolName: string;
|
|
44
47
|
toolInput?: Record<string, unknown>;
|
|
45
48
|
planContent?: string;
|
|
49
|
+
warning?: string;
|
|
46
50
|
isExpanded?: boolean;
|
|
47
51
|
}
|
|
48
52
|
|
|
@@ -50,6 +54,7 @@ export const ConfirmationDetails: React.FC<ConfirmationDetailsProps> = ({
|
|
|
50
54
|
toolName,
|
|
51
55
|
toolInput,
|
|
52
56
|
planContent,
|
|
57
|
+
warning,
|
|
53
58
|
isExpanded = false,
|
|
54
59
|
}) => {
|
|
55
60
|
const startLineNumber =
|
|
@@ -69,6 +74,7 @@ export const ConfirmationDetails: React.FC<ConfirmationDetailsProps> = ({
|
|
|
69
74
|
Tool: {toolName}
|
|
70
75
|
</Text>
|
|
71
76
|
<Text color="yellow">{getActionDescription(toolName, toolInput)}</Text>
|
|
77
|
+
{warning && <Text color="red">⚠ {warning}</Text>}
|
|
72
78
|
|
|
73
79
|
<DiffDisplay
|
|
74
80
|
toolName={toolName}
|
|
@@ -34,10 +34,16 @@ export const RewindCommand: React.FC<RewindCommandProps> = ({
|
|
|
34
34
|
}, [getFullMessageThread]);
|
|
35
35
|
|
|
36
36
|
// Filter user messages as checkpoints, excluding meta messages and
|
|
37
|
-
// system-generated user-role messages (task notifications, hook injections)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
// system-generated user-role messages (task notifications, hook injections).
|
|
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());
|
|
41
47
|
|
|
42
48
|
const MAX_VISIBLE_ITEMS = 3;
|
|
43
49
|
|
package/src/contexts/useChat.tsx
CHANGED
|
@@ -31,7 +31,6 @@ import {
|
|
|
31
31
|
extractLatestTotalTokens,
|
|
32
32
|
} from "wave-agent-sdk";
|
|
33
33
|
import { logger } from "../utils/logger.js";
|
|
34
|
-
import { throttle } from "../utils/throttle.js";
|
|
35
34
|
import { displayUsageSummary } from "../utils/usageSummary.js";
|
|
36
35
|
import { expandLongTextPlaceholders } from "../managers/inputHandlers.js";
|
|
37
36
|
|
|
@@ -112,6 +111,7 @@ export interface ChatContextType {
|
|
|
112
111
|
hidePersistentOption?: boolean;
|
|
113
112
|
planContent?: string;
|
|
114
113
|
permissionMode?: PermissionMode;
|
|
114
|
+
warning?: string;
|
|
115
115
|
};
|
|
116
116
|
showConfirmation: (
|
|
117
117
|
toolName: string,
|
|
@@ -120,6 +120,7 @@ export interface ChatContextType {
|
|
|
120
120
|
hidePersistentOption?: boolean,
|
|
121
121
|
planContent?: string,
|
|
122
122
|
permissionMode?: PermissionMode,
|
|
123
|
+
warning?: string,
|
|
123
124
|
) => Promise<PermissionDecision>;
|
|
124
125
|
hideConfirmation: () => void;
|
|
125
126
|
handleConfirmationDecision: (decision: PermissionDecision) => void;
|
|
@@ -167,6 +168,22 @@ interface StreamingUpdateParams {
|
|
|
167
168
|
stage: "streaming" | "end";
|
|
168
169
|
}
|
|
169
170
|
|
|
171
|
+
/**
|
|
172
|
+
* Snapshot a SDK message for consumer state. The SDK mutates its internal
|
|
173
|
+
* message blocks in-place BEFORE firing the delta callback (it writes the full
|
|
174
|
+
* accumulated value to the shared block, then computes the chunk delta by
|
|
175
|
+
* slicing the new value). A consumer that pushed the SDK message object by
|
|
176
|
+
* live reference would read the already-updated block and append the delta
|
|
177
|
+
* again — the first delta is double-counted ("LetLet me think..."), affecting
|
|
178
|
+
* reasoning and text content alike. See docs/specs/core/stream-content-updates.md.
|
|
179
|
+
* The clone must be at least one layer deep (message + blocks) so the in-place
|
|
180
|
+
* block mutation never leaks into consumer state.
|
|
181
|
+
*/
|
|
182
|
+
const snapshotMessage = (message: Message): Message => ({
|
|
183
|
+
...message,
|
|
184
|
+
blocks: message.blocks.map((block) => ({ ...block })),
|
|
185
|
+
});
|
|
186
|
+
|
|
170
187
|
/**
|
|
171
188
|
* Window-concat throttle for pure-delta streaming updates: chunks arriving
|
|
172
189
|
* within the cooldown window are merged so no delta is lost (a dropped delta
|
|
@@ -239,6 +256,99 @@ function createStreamingWindowThrottle(
|
|
|
239
256
|
return throttled;
|
|
240
257
|
}
|
|
241
258
|
|
|
259
|
+
/**
|
|
260
|
+
* Per-tool window-concat throttle for pure-delta tool parameter streaming:
|
|
261
|
+
* `parametersChunk` deltas are accumulated independently per tool block id
|
|
262
|
+
* within the cooldown window, so interleaved multi-tool streams lose no delta
|
|
263
|
+
* (a plain throttle's single last-args slot would drop every earlier tool's
|
|
264
|
+
* deltas, leaving the first tool without streaming parameters). `start` /
|
|
265
|
+
* `running` apply immediately (one-shot snapshots); `end` flushes pending
|
|
266
|
+
* streaming deltas first, then applies the authoritative parameters/result.
|
|
267
|
+
*/
|
|
268
|
+
export function createToolStreamingThrottle(
|
|
269
|
+
fn: (params: ToolBlockUpdateCallbackParams) => void,
|
|
270
|
+
wait: number,
|
|
271
|
+
): {
|
|
272
|
+
(params: ToolBlockUpdateCallbackParams): void;
|
|
273
|
+
cancel: () => void;
|
|
274
|
+
flush: () => void;
|
|
275
|
+
} {
|
|
276
|
+
let timer: NodeJS.Timeout | null = null;
|
|
277
|
+
let pending: { messageId: string; chunks: Map<string, string> } | null = null;
|
|
278
|
+
|
|
279
|
+
const fire = () => {
|
|
280
|
+
if (pending && pending.chunks.size > 0) {
|
|
281
|
+
const { messageId, chunks } = pending;
|
|
282
|
+
pending = null;
|
|
283
|
+
for (const [id, chunk] of chunks) {
|
|
284
|
+
fn({ messageId, id, parametersChunk: chunk, stage: "streaming" });
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
const throttled = (params: ToolBlockUpdateCallbackParams) => {
|
|
290
|
+
if (params.stage === "end") {
|
|
291
|
+
// Flush any deltas still pending inside the cooldown window first
|
|
292
|
+
if (timer) {
|
|
293
|
+
clearTimeout(timer);
|
|
294
|
+
timer = null;
|
|
295
|
+
}
|
|
296
|
+
fire();
|
|
297
|
+
fn(params);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
if (params.stage === "streaming") {
|
|
301
|
+
if (!pending) {
|
|
302
|
+
pending = { messageId: params.messageId, chunks: new Map() };
|
|
303
|
+
}
|
|
304
|
+
const prev = pending.chunks.get(params.id) || "";
|
|
305
|
+
pending.chunks.set(params.id, prev + (params.parametersChunk || ""));
|
|
306
|
+
if (!timer) {
|
|
307
|
+
timer = setTimeout(() => {
|
|
308
|
+
timer = null;
|
|
309
|
+
fire();
|
|
310
|
+
}, wait);
|
|
311
|
+
}
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
// start / running — one-shot snapshots applied immediately. Drop this
|
|
315
|
+
// tool's buffered streaming deltas first: start/running carry the
|
|
316
|
+
// authoritative parameters, and a pending timer would otherwise fire late
|
|
317
|
+
// with a stale `streaming` event, regressing this tool block's stage back
|
|
318
|
+
// to streaming (yellow dot -> gray) mid-execution. Other tools' in-flight
|
|
319
|
+
// chunks are kept so interleaved multi-tool streaming still accumulates.
|
|
320
|
+
if (pending) {
|
|
321
|
+
pending.chunks.delete(params.id);
|
|
322
|
+
if (pending.chunks.size === 0) {
|
|
323
|
+
pending = null;
|
|
324
|
+
if (timer) {
|
|
325
|
+
clearTimeout(timer);
|
|
326
|
+
timer = null;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
fn(params);
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
throttled.cancel = () => {
|
|
334
|
+
if (timer) {
|
|
335
|
+
clearTimeout(timer);
|
|
336
|
+
timer = null;
|
|
337
|
+
}
|
|
338
|
+
pending = null;
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
throttled.flush = () => {
|
|
342
|
+
if (timer) {
|
|
343
|
+
clearTimeout(timer);
|
|
344
|
+
timer = null;
|
|
345
|
+
}
|
|
346
|
+
fire();
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
return throttled;
|
|
350
|
+
}
|
|
351
|
+
|
|
242
352
|
export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
243
353
|
children,
|
|
244
354
|
bypassPermissions,
|
|
@@ -345,8 +455,13 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
345
455
|
|
|
346
456
|
const throttledToolBlockUpdate = useMemo(
|
|
347
457
|
() =>
|
|
348
|
-
|
|
349
|
-
const {
|
|
458
|
+
createToolStreamingThrottle((params) => {
|
|
459
|
+
const {
|
|
460
|
+
messageId,
|
|
461
|
+
id: toolBlockId,
|
|
462
|
+
parametersChunk,
|
|
463
|
+
...updates
|
|
464
|
+
} = params;
|
|
350
465
|
setMessages((prev) =>
|
|
351
466
|
prev.map((m) => {
|
|
352
467
|
if (m.id !== messageId) return m;
|
|
@@ -363,7 +478,8 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
363
478
|
id: toolBlockId,
|
|
364
479
|
name: updates.name || "",
|
|
365
480
|
stage: updates.stage || "start",
|
|
366
|
-
parameters:
|
|
481
|
+
parameters:
|
|
482
|
+
(updates.parameters || "") + (parametersChunk || ""),
|
|
367
483
|
result: updates.result || "",
|
|
368
484
|
...updates,
|
|
369
485
|
},
|
|
@@ -374,7 +490,18 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
374
490
|
...m,
|
|
375
491
|
blocks: m.blocks.map((b, idx) =>
|
|
376
492
|
idx === toolBlockIndex && b.type === "tool"
|
|
377
|
-
? {
|
|
493
|
+
? {
|
|
494
|
+
...b,
|
|
495
|
+
...updates,
|
|
496
|
+
// Streaming carries only the delta; append it to the
|
|
497
|
+
// accumulated parameters. start/running/end carry the
|
|
498
|
+
// authoritative value and replace wholesale.
|
|
499
|
+
parameters: parametersChunk
|
|
500
|
+
? (b.parameters || "") + parametersChunk
|
|
501
|
+
: updates.parameters !== undefined
|
|
502
|
+
? updates.parameters
|
|
503
|
+
: b.parameters,
|
|
504
|
+
}
|
|
378
505
|
: b,
|
|
379
506
|
),
|
|
380
507
|
};
|
|
@@ -443,6 +570,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
443
570
|
hidePersistentOption?: boolean;
|
|
444
571
|
planContent?: string;
|
|
445
572
|
permissionMode?: PermissionMode;
|
|
573
|
+
warning?: string;
|
|
446
574
|
}
|
|
447
575
|
| undefined
|
|
448
576
|
>();
|
|
@@ -454,6 +582,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
454
582
|
hidePersistentOption?: boolean;
|
|
455
583
|
planContent?: string;
|
|
456
584
|
permissionMode?: PermissionMode;
|
|
585
|
+
warning?: string;
|
|
457
586
|
resolver: (decision: PermissionDecision) => void;
|
|
458
587
|
reject: () => void;
|
|
459
588
|
}>
|
|
@@ -465,6 +594,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
465
594
|
hidePersistentOption?: boolean;
|
|
466
595
|
planContent?: string;
|
|
467
596
|
permissionMode?: PermissionMode;
|
|
597
|
+
warning?: string;
|
|
468
598
|
resolver: (decision: PermissionDecision) => void;
|
|
469
599
|
reject: () => void;
|
|
470
600
|
} | null>(null);
|
|
@@ -491,7 +621,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
491
621
|
// the incremental callbacks in initializeAgent below.
|
|
492
622
|
const refreshMessages = useCallback(() => {
|
|
493
623
|
if (!isExpandedRef.current && agentRef.current) {
|
|
494
|
-
const msgs =
|
|
624
|
+
const msgs = agentRef.current.messages.map(snapshotMessage);
|
|
495
625
|
setMessages(msgs);
|
|
496
626
|
setLatestTotalTokens(extractLatestTotalTokens(msgs));
|
|
497
627
|
}
|
|
@@ -506,6 +636,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
506
636
|
hidePersistentOption?: boolean,
|
|
507
637
|
planContent?: string,
|
|
508
638
|
permissionMode?: PermissionMode,
|
|
639
|
+
warning?: string,
|
|
509
640
|
): Promise<PermissionDecision> => {
|
|
510
641
|
return new Promise<PermissionDecision>((resolve, reject) => {
|
|
511
642
|
const queueItem = {
|
|
@@ -515,6 +646,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
515
646
|
hidePersistentOption,
|
|
516
647
|
planContent,
|
|
517
648
|
permissionMode,
|
|
649
|
+
warning,
|
|
518
650
|
resolver: resolve,
|
|
519
651
|
reject,
|
|
520
652
|
};
|
|
@@ -540,7 +672,9 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
540
672
|
const last = msgs[msgs.length - 1];
|
|
541
673
|
if (!last || last.role !== "user") return;
|
|
542
674
|
setMessages((prev) =>
|
|
543
|
-
prev.some((m) => m.id === last.id)
|
|
675
|
+
prev.some((m) => m.id === last.id)
|
|
676
|
+
? prev
|
|
677
|
+
: [...prev, snapshotMessage(last)],
|
|
544
678
|
);
|
|
545
679
|
},
|
|
546
680
|
onAssistantMessageAdded: (messageId: string) => {
|
|
@@ -548,7 +682,9 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
548
682
|
const msg = agentRef.current.messages.find((m) => m.id === messageId);
|
|
549
683
|
if (!msg) return;
|
|
550
684
|
setMessages((prev) =>
|
|
551
|
-
prev.some((m) => m.id === messageId)
|
|
685
|
+
prev.some((m) => m.id === messageId)
|
|
686
|
+
? prev
|
|
687
|
+
: [...prev, snapshotMessage(msg)],
|
|
552
688
|
);
|
|
553
689
|
},
|
|
554
690
|
onAssistantContentUpdated: (params) => {
|
|
@@ -723,6 +859,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
723
859
|
context.hidePersistentOption,
|
|
724
860
|
context.planContent,
|
|
725
861
|
context.permissionMode,
|
|
862
|
+
context.warning,
|
|
726
863
|
);
|
|
727
864
|
} catch {
|
|
728
865
|
// If confirmation was cancelled or failed, deny the operation
|
|
@@ -772,9 +909,10 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
772
909
|
agent.setWorktreeSession(session);
|
|
773
910
|
}
|
|
774
911
|
|
|
775
|
-
// Get initial state
|
|
912
|
+
// Get initial state — snapshot the SDK messages (never hold live
|
|
913
|
+
// references; see snapshotMessage)
|
|
776
914
|
setSessionId(agent.sessionId);
|
|
777
|
-
setMessages(agent.messages);
|
|
915
|
+
setMessages(agent.messages.map(snapshotMessage));
|
|
778
916
|
setIsLoading(agent.isLoading);
|
|
779
917
|
setLatestTotalTokens(extractLatestTotalTokens(agent.messages));
|
|
780
918
|
setIsCommandRunning(agent.isCommandRunning);
|
|
@@ -1070,6 +1208,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
1070
1208
|
hidePersistentOption: next.hidePersistentOption,
|
|
1071
1209
|
planContent: next.planContent,
|
|
1072
1210
|
permissionMode: next.permissionMode,
|
|
1211
|
+
warning: next.warning,
|
|
1073
1212
|
});
|
|
1074
1213
|
setIsConfirmationVisible(true);
|
|
1075
1214
|
setConfirmationQueue((prev) => prev.slice(1));
|