wave-code 1.0.0 → 1.0.2
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/cli.js +20 -1
- package/dist/components/App.js +7 -0
- package/dist/components/BtwDisplay.js +13 -3
- package/dist/components/ChatInterface.js +25 -8
- package/dist/components/InputBox.d.ts +1 -3
- package/dist/components/InputBox.js +12 -9
- package/dist/components/LoadingIndicator.d.ts +1 -2
- package/dist/components/LoadingIndicator.js +2 -2
- package/dist/components/LoginCommand.js +4 -2
- package/dist/components/Markdown.js +13 -16
- package/dist/components/Notifications.d.ts +7 -0
- package/dist/components/Notifications.js +9 -0
- package/dist/components/StatusLine.d.ts +0 -4
- package/dist/components/StatusLine.js +6 -10
- package/dist/components/TaskList.js +2 -1
- package/dist/components/ToolDisplay.d.ts +1 -0
- package/dist/components/ToolDisplay.js +17 -9
- package/dist/constants/commands.js +0 -6
- package/dist/contexts/useChat.d.ts +4 -6
- package/dist/contexts/useChat.js +253 -110
- package/dist/daemon-cli.d.ts +10 -0
- package/dist/daemon-cli.js +15 -0
- package/dist/hooks/useInputManager.js +99 -22
- package/dist/index.js +10 -0
- package/dist/managers/inputHandlers.js +50 -22
- package/dist/managers/inputReducer.d.ts +12 -2
- package/dist/managers/inputReducer.js +57 -9
- package/dist/stdio/agentBridge.d.ts +23 -0
- package/dist/stdio/agentBridge.js +134 -16
- package/dist/stdio/daemonServer.d.ts +67 -0
- package/dist/stdio/daemonServer.js +191 -0
- package/dist/stdio/index.d.ts +2 -0
- package/dist/stdio/index.js +2 -0
- package/dist/stdio/jsonRpcConnection.d.ts +30 -0
- package/dist/stdio/jsonRpcConnection.js +127 -0
- package/dist/stdio/protocol.d.ts +2 -2
- package/dist/stdio/stdioServer.d.ts +2 -7
- package/dist/stdio/stdioServer.js +9 -100
- package/dist/utils/bracketedPaste.d.ts +39 -0
- package/dist/utils/bracketedPaste.js +122 -0
- package/dist/utils/markdownTable.d.ts +34 -0
- package/dist/utils/markdownTable.js +302 -0
- package/dist/utils/throttle.d.ts +3 -3
- package/package.json +4 -2
- package/src/cli.tsx +20 -1
- package/src/components/App.tsx +5 -0
- package/src/components/BtwDisplay.tsx +36 -12
- package/src/components/ChatInterface.tsx +30 -15
- package/src/components/InputBox.tsx +25 -24
- package/src/components/LoadingIndicator.tsx +1 -4
- package/src/components/LoginCommand.tsx +4 -2
- package/src/components/Markdown.tsx +15 -18
- package/src/components/Notifications.tsx +31 -0
- package/src/components/StatusLine.tsx +17 -44
- package/src/components/TaskList.tsx +2 -1
- package/src/components/ToolDisplay.tsx +17 -6
- package/src/constants/commands.ts +0 -6
- package/src/contexts/useChat.tsx +326 -140
- package/src/daemon-cli.ts +17 -0
- package/src/hooks/useInputManager.ts +108 -22
- package/src/index.ts +12 -0
- package/src/managers/inputHandlers.ts +49 -22
- package/src/managers/inputReducer.ts +66 -11
- package/src/stdio/agentBridge.ts +196 -17
- package/src/stdio/daemonServer.ts +212 -0
- package/src/stdio/index.ts +2 -0
- package/src/stdio/jsonRpcConnection.ts +160 -0
- package/src/stdio/protocol.ts +5 -2
- package/src/stdio/stdioServer.ts +14 -120
- package/src/utils/bracketedPaste.ts +170 -0
- package/src/utils/markdownTable.ts +359 -0
- package/src/utils/throttle.ts +8 -8
package/src/contexts/useChat.tsx
CHANGED
|
@@ -19,6 +19,7 @@ import type {
|
|
|
19
19
|
PermissionMode,
|
|
20
20
|
QueuedMessage,
|
|
21
21
|
WorkflowRun,
|
|
22
|
+
ToolBlockUpdateCallbackParams,
|
|
22
23
|
} from "wave-agent-sdk";
|
|
23
24
|
import {
|
|
24
25
|
Agent,
|
|
@@ -45,6 +46,9 @@ export interface ChatContextType {
|
|
|
45
46
|
isExpanded: boolean;
|
|
46
47
|
isTaskListVisible: boolean;
|
|
47
48
|
setIsTaskListVisible: (visible: boolean) => void;
|
|
49
|
+
// True while the /btw side-question overlay is on display
|
|
50
|
+
isBtwActive: boolean;
|
|
51
|
+
setIsBtwActive: (active: boolean) => void;
|
|
48
52
|
queuedMessages: QueuedMessage[];
|
|
49
53
|
// AI functionality
|
|
50
54
|
sessionId: string;
|
|
@@ -53,10 +57,13 @@ export interface ChatContextType {
|
|
|
53
57
|
images?: Array<{ path: string; mimeType: string }>,
|
|
54
58
|
longTextMap?: Record<string, string>,
|
|
55
59
|
) => Promise<void>;
|
|
56
|
-
askBtw: (
|
|
60
|
+
askBtw: (
|
|
61
|
+
question: string,
|
|
62
|
+
abortSignal?: AbortSignal,
|
|
63
|
+
onContent?: (content: string) => void,
|
|
64
|
+
) => Promise<string>;
|
|
57
65
|
clearMessages: () => Promise<void>;
|
|
58
66
|
compact: (instructions?: string) => Promise<void>;
|
|
59
|
-
goalCommand: (args?: string) => Promise<void>;
|
|
60
67
|
abortMessage: () => void;
|
|
61
68
|
recallQueuedMessage: () => QueuedMessage | null;
|
|
62
69
|
removeQueuedMessageById: (id: string) => boolean;
|
|
@@ -115,7 +122,7 @@ export interface ChatContextType {
|
|
|
115
122
|
backgroundCurrentTask: () => void;
|
|
116
123
|
// Remount functionality
|
|
117
124
|
remountKey: number;
|
|
118
|
-
|
|
125
|
+
forceRemount: () => void;
|
|
119
126
|
// Rewind functionality
|
|
120
127
|
handleRewindSelect: (index: number) => Promise<void>;
|
|
121
128
|
getFullMessageThread: () => Promise<{
|
|
@@ -132,10 +139,6 @@ export interface ChatContextType {
|
|
|
132
139
|
recreateAgent: () => void;
|
|
133
140
|
// Trigger WorktreeRemove hook BEFORE agent destruction
|
|
134
141
|
triggerWorktreeRemoveHook: (worktreePath: string) => Promise<void>;
|
|
135
|
-
// Goal state
|
|
136
|
-
isGoalActive: boolean;
|
|
137
|
-
goalElapsed?: string;
|
|
138
|
-
isGoalEvaluating: boolean;
|
|
139
142
|
}
|
|
140
143
|
|
|
141
144
|
const ChatContext = createContext<ChatContextType | null>(null);
|
|
@@ -175,39 +178,150 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
175
178
|
const isExpandedRef = useRef(isExpanded);
|
|
176
179
|
|
|
177
180
|
const [isTaskListVisible, setIsTaskListVisible] = useState(true);
|
|
181
|
+
const [isBtwActive, setIsBtwActive] = useState(false);
|
|
178
182
|
|
|
179
183
|
const [messages, setMessages] = useState<Message[]>([]);
|
|
180
184
|
const [latestTotalTokens, setLatestTotalTokens] = useState(0);
|
|
181
185
|
const [maxInputTokens, setMaxInputTokens] = useState(200000);
|
|
182
186
|
|
|
183
|
-
|
|
187
|
+
// Throttled incremental streaming updaters — 500ms leading+trailing, the same interval
|
|
188
|
+
// as the pre-incremental throttledSetMessages. `stage === "end"` flushes the final
|
|
189
|
+
// update immediately so completion results are never delayed.
|
|
190
|
+
const throttledContentUpdate = useMemo(
|
|
184
191
|
() =>
|
|
185
192
|
throttle(
|
|
186
|
-
(
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
}
|
|
193
|
+
(params: {
|
|
194
|
+
messageId: string;
|
|
195
|
+
accumulated: string;
|
|
196
|
+
stage: "streaming" | "end";
|
|
197
|
+
}) => {
|
|
198
|
+
const { messageId, accumulated, stage } = params;
|
|
199
|
+
setMessages((prev) =>
|
|
200
|
+
prev.map((m) => {
|
|
201
|
+
if (m.id !== messageId) return m;
|
|
202
|
+
const textBlockIndex = m.blocks.findIndex(
|
|
203
|
+
(b) => b.type === "text",
|
|
204
|
+
);
|
|
205
|
+
if (textBlockIndex === -1) {
|
|
206
|
+
return {
|
|
207
|
+
...m,
|
|
208
|
+
blocks: [
|
|
209
|
+
...m.blocks,
|
|
210
|
+
{ type: "text", content: accumulated, stage },
|
|
211
|
+
],
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
...m,
|
|
216
|
+
blocks: m.blocks.map((b, idx) =>
|
|
217
|
+
idx === textBlockIndex && b.type === "text"
|
|
218
|
+
? { ...b, content: accumulated, stage }
|
|
219
|
+
: b,
|
|
220
|
+
),
|
|
221
|
+
};
|
|
222
|
+
}),
|
|
223
|
+
);
|
|
224
|
+
},
|
|
225
|
+
500,
|
|
226
|
+
),
|
|
227
|
+
[],
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
const throttledReasoningUpdate = useMemo(
|
|
231
|
+
() =>
|
|
232
|
+
throttle(
|
|
233
|
+
(params: {
|
|
234
|
+
messageId: string;
|
|
235
|
+
accumulated: string;
|
|
236
|
+
stage: "streaming" | "end";
|
|
237
|
+
}) => {
|
|
238
|
+
const { messageId, accumulated, stage } = params;
|
|
239
|
+
setMessages((prev) =>
|
|
240
|
+
prev.map((m) => {
|
|
241
|
+
if (m.id !== messageId) return m;
|
|
242
|
+
const reasoningBlockIndex = m.blocks.findIndex(
|
|
243
|
+
(b) => b.type === "reasoning",
|
|
244
|
+
);
|
|
245
|
+
if (reasoningBlockIndex === -1) {
|
|
246
|
+
return {
|
|
247
|
+
...m,
|
|
248
|
+
blocks: [
|
|
249
|
+
...m.blocks,
|
|
250
|
+
{ type: "reasoning", content: accumulated, stage },
|
|
251
|
+
],
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
return {
|
|
255
|
+
...m,
|
|
256
|
+
blocks: m.blocks.map((b, idx) =>
|
|
257
|
+
idx === reasoningBlockIndex && b.type === "reasoning"
|
|
258
|
+
? { ...b, content: accumulated, stage }
|
|
259
|
+
: b,
|
|
260
|
+
),
|
|
261
|
+
};
|
|
262
|
+
}),
|
|
263
|
+
);
|
|
192
264
|
},
|
|
193
265
|
500,
|
|
194
|
-
{ leading: true, trailing: true },
|
|
195
266
|
),
|
|
196
267
|
[],
|
|
197
268
|
);
|
|
198
269
|
|
|
270
|
+
const throttledToolBlockUpdate = useMemo(
|
|
271
|
+
() =>
|
|
272
|
+
throttle((params: ToolBlockUpdateCallbackParams) => {
|
|
273
|
+
const { messageId, id: toolBlockId, ...updates } = params;
|
|
274
|
+
setMessages((prev) =>
|
|
275
|
+
prev.map((m) => {
|
|
276
|
+
if (m.id !== messageId) return m;
|
|
277
|
+
const toolBlockIndex = m.blocks.findIndex(
|
|
278
|
+
(b) => b.type === "tool" && b.id === toolBlockId,
|
|
279
|
+
);
|
|
280
|
+
if (toolBlockIndex === -1) {
|
|
281
|
+
return {
|
|
282
|
+
...m,
|
|
283
|
+
blocks: [
|
|
284
|
+
...m.blocks,
|
|
285
|
+
{
|
|
286
|
+
type: "tool",
|
|
287
|
+
id: toolBlockId,
|
|
288
|
+
name: updates.name || "",
|
|
289
|
+
stage: updates.stage || "start",
|
|
290
|
+
parameters: updates.parameters || "",
|
|
291
|
+
result: updates.result || "",
|
|
292
|
+
...updates,
|
|
293
|
+
},
|
|
294
|
+
],
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
return {
|
|
298
|
+
...m,
|
|
299
|
+
blocks: m.blocks.map((b, idx) =>
|
|
300
|
+
idx === toolBlockIndex && b.type === "tool"
|
|
301
|
+
? { ...b, ...updates }
|
|
302
|
+
: b,
|
|
303
|
+
),
|
|
304
|
+
};
|
|
305
|
+
}),
|
|
306
|
+
);
|
|
307
|
+
}, 500),
|
|
308
|
+
[],
|
|
309
|
+
);
|
|
310
|
+
|
|
199
311
|
useEffect(() => {
|
|
200
312
|
isExpandedRef.current = isExpanded;
|
|
201
313
|
if (isExpanded) {
|
|
202
|
-
|
|
314
|
+
// Cancel pending throttled updates so the frozen expanded view isn't overwritten
|
|
315
|
+
throttledContentUpdate.cancel();
|
|
316
|
+
throttledReasoningUpdate.cancel();
|
|
317
|
+
throttledToolBlockUpdate.cancel();
|
|
203
318
|
}
|
|
204
|
-
}, [
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
}, [throttledSetMessages]);
|
|
319
|
+
}, [
|
|
320
|
+
isExpanded,
|
|
321
|
+
throttledContentUpdate,
|
|
322
|
+
throttledReasoningUpdate,
|
|
323
|
+
throttledToolBlockUpdate,
|
|
324
|
+
]);
|
|
211
325
|
|
|
212
326
|
const [isLoading, setIsLoading] = useState(false);
|
|
213
327
|
const [sessionId, setSessionId] = useState("");
|
|
@@ -216,28 +330,6 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
216
330
|
const [currentModel, setCurrentModelState] = useState("");
|
|
217
331
|
const [configuredModels, setConfiguredModels] = useState<string[]>([]);
|
|
218
332
|
const [queuedMessages, setQueuedMessages] = useState<QueuedMessage[]>([]);
|
|
219
|
-
const [isGoalActive, setIsGoalActive] = useState(false);
|
|
220
|
-
const [goalElapsed, setGoalElapsed] = useState<string | undefined>();
|
|
221
|
-
const [isGoalEvaluating, setIsGoalEvaluating] = useState(false);
|
|
222
|
-
const goalStartedAt = useRef<number | null>(null);
|
|
223
|
-
|
|
224
|
-
// Update goal elapsed time every 30s while active
|
|
225
|
-
useEffect(() => {
|
|
226
|
-
if (!isGoalActive || goalStartedAt.current === null) return;
|
|
227
|
-
const formatElapsed = (ms: number): string => {
|
|
228
|
-
const minutes = Math.floor(ms / 60000);
|
|
229
|
-
if (minutes < 1) return "<1m";
|
|
230
|
-
if (minutes < 60) return `${minutes}m`;
|
|
231
|
-
const hours = Math.floor(minutes / 60);
|
|
232
|
-
const remainingMin = minutes % 60;
|
|
233
|
-
return `${hours}h${remainingMin}m`;
|
|
234
|
-
};
|
|
235
|
-
const update = () =>
|
|
236
|
-
setGoalElapsed(formatElapsed(Date.now() - goalStartedAt.current!));
|
|
237
|
-
update();
|
|
238
|
-
const timer = setInterval(update, 30_000);
|
|
239
|
-
return () => clearInterval(timer);
|
|
240
|
-
}, [isGoalActive]);
|
|
241
333
|
|
|
242
334
|
// MCP State
|
|
243
335
|
const [mcpServerStatuses, setMcpServerStatuses] = useState<McpServerStatus[]>(
|
|
@@ -295,48 +387,32 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
295
387
|
|
|
296
388
|
// Remount state
|
|
297
389
|
const [remountKey, setRemountKey] = useState(0);
|
|
298
|
-
const prevSessionId = useRef<string | null>(null);
|
|
299
390
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
},
|
|
309
|
-
1000,
|
|
310
|
-
{ leading: true, trailing: false },
|
|
311
|
-
),
|
|
312
|
-
[stdout],
|
|
313
|
-
);
|
|
314
|
-
|
|
315
|
-
useEffect(() => {
|
|
316
|
-
return () => {
|
|
317
|
-
requestRemount.cancel();
|
|
318
|
-
};
|
|
319
|
-
}, [requestRemount]);
|
|
320
|
-
|
|
321
|
-
// Track sessionId changes to trigger remount
|
|
322
|
-
useEffect(() => {
|
|
323
|
-
if (
|
|
324
|
-
prevSessionId.current &&
|
|
325
|
-
sessionId &&
|
|
326
|
-
prevSessionId.current !== sessionId
|
|
327
|
-
) {
|
|
328
|
-
requestRemount();
|
|
329
|
-
}
|
|
330
|
-
if (sessionId) {
|
|
331
|
-
prevSessionId.current = sessionId;
|
|
332
|
-
}
|
|
333
|
-
}, [sessionId, requestRemount]);
|
|
391
|
+
// Full terminal clear + remount so Ink's append-only <Static> re-renders.
|
|
392
|
+
// Used on structural actions (/clear, /compact, rewind, ctrl-o, forceStatic
|
|
393
|
+
// exit) where stale Static output must not linger on screen.
|
|
394
|
+
const forceRemount = useCallback(() => {
|
|
395
|
+
stdout?.write("\u001b[2J\u001b[3J\u001b[0;0H", () => {
|
|
396
|
+
setRemountKey((prev) => prev + 1);
|
|
397
|
+
});
|
|
398
|
+
}, [stdout]);
|
|
334
399
|
|
|
335
400
|
// Status metadata state
|
|
336
401
|
const [workingDirectory, setWorkingDirectory] = useState("");
|
|
337
402
|
|
|
338
403
|
const agentRef = useRef<Agent | null>(null);
|
|
339
404
|
|
|
405
|
+
// Full-list refresh — one-shot pull from the agent, used only for structural
|
|
406
|
+
// changes (compact/clear/rewind/collapse/init). Streaming updates flow through
|
|
407
|
+
// the incremental callbacks in initializeAgent below.
|
|
408
|
+
const refreshMessages = useCallback(() => {
|
|
409
|
+
if (!isExpandedRef.current && agentRef.current) {
|
|
410
|
+
const msgs = [...agentRef.current.messages];
|
|
411
|
+
setMessages(msgs);
|
|
412
|
+
setLatestTotalTokens(extractLatestTotalTokens(msgs));
|
|
413
|
+
}
|
|
414
|
+
}, []);
|
|
415
|
+
|
|
340
416
|
// Permission confirmation methods with queue support
|
|
341
417
|
const showConfirmation = useCallback(
|
|
342
418
|
async (
|
|
@@ -371,8 +447,129 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
371
447
|
restoreSessionIdOverride ?? restoreSessionId;
|
|
372
448
|
|
|
373
449
|
const callbacks: AgentCallbacks = {
|
|
374
|
-
|
|
375
|
-
|
|
450
|
+
// ── Incremental message updates (no full-list pushes) ──────
|
|
451
|
+
onUserMessageAdded: () => {
|
|
452
|
+
if (isExpandedRef.current || !agentRef.current) return;
|
|
453
|
+
const msgs = agentRef.current.messages;
|
|
454
|
+
const last = msgs[msgs.length - 1];
|
|
455
|
+
if (!last || last.role !== "user") return;
|
|
456
|
+
setMessages((prev) =>
|
|
457
|
+
prev.some((m) => m.id === last.id) ? prev : [...prev, last],
|
|
458
|
+
);
|
|
459
|
+
},
|
|
460
|
+
onAssistantMessageAdded: (messageId: string) => {
|
|
461
|
+
if (isExpandedRef.current || !agentRef.current) return;
|
|
462
|
+
const msg = agentRef.current.messages.find((m) => m.id === messageId);
|
|
463
|
+
if (!msg) return;
|
|
464
|
+
setMessages((prev) =>
|
|
465
|
+
prev.some((m) => m.id === messageId) ? prev : [...prev, msg],
|
|
466
|
+
);
|
|
467
|
+
},
|
|
468
|
+
onAssistantContentUpdated: (params) => {
|
|
469
|
+
if (isExpandedRef.current) return;
|
|
470
|
+
throttledContentUpdate(params);
|
|
471
|
+
if (params.stage === "end") throttledContentUpdate.flush();
|
|
472
|
+
},
|
|
473
|
+
onAssistantReasoningUpdated: (params) => {
|
|
474
|
+
if (isExpandedRef.current) return;
|
|
475
|
+
throttledReasoningUpdate(params);
|
|
476
|
+
if (params.stage === "end") throttledReasoningUpdate.flush();
|
|
477
|
+
},
|
|
478
|
+
onToolBlockUpdated: (params) => {
|
|
479
|
+
if (isExpandedRef.current) return;
|
|
480
|
+
throttledToolBlockUpdate(params);
|
|
481
|
+
if (params.stage === "end") throttledToolBlockUpdate.flush();
|
|
482
|
+
},
|
|
483
|
+
onErrorBlockAdded: (error: string) => {
|
|
484
|
+
if (isExpandedRef.current) return;
|
|
485
|
+
setMessages((prev) => {
|
|
486
|
+
// Append to the last assistant message, or create one if none exists
|
|
487
|
+
for (let i = prev.length - 1; i >= 0; i--) {
|
|
488
|
+
if (prev[i].role === "assistant") {
|
|
489
|
+
return prev.map((m, idx) =>
|
|
490
|
+
idx === i
|
|
491
|
+
? {
|
|
492
|
+
...m,
|
|
493
|
+
blocks: [
|
|
494
|
+
...m.blocks,
|
|
495
|
+
{ type: "error", content: error },
|
|
496
|
+
],
|
|
497
|
+
}
|
|
498
|
+
: m,
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
return [
|
|
503
|
+
...prev,
|
|
504
|
+
{
|
|
505
|
+
id: `msg-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`,
|
|
506
|
+
role: "assistant",
|
|
507
|
+
timestamp: new Date().toISOString(),
|
|
508
|
+
blocks: [{ type: "error", content: error }],
|
|
509
|
+
},
|
|
510
|
+
];
|
|
511
|
+
});
|
|
512
|
+
},
|
|
513
|
+
onAddBangMessage: (command, messageId) => {
|
|
514
|
+
if (isExpandedRef.current) return;
|
|
515
|
+
setMessages((prev) =>
|
|
516
|
+
prev.some((m) => m.id === messageId)
|
|
517
|
+
? prev
|
|
518
|
+
: [
|
|
519
|
+
...prev,
|
|
520
|
+
{
|
|
521
|
+
id: messageId,
|
|
522
|
+
role: "user",
|
|
523
|
+
timestamp: new Date().toISOString(),
|
|
524
|
+
blocks: [
|
|
525
|
+
{
|
|
526
|
+
type: "bang",
|
|
527
|
+
command,
|
|
528
|
+
output: "",
|
|
529
|
+
stage: "running",
|
|
530
|
+
exitCode: null,
|
|
531
|
+
},
|
|
532
|
+
],
|
|
533
|
+
},
|
|
534
|
+
],
|
|
535
|
+
);
|
|
536
|
+
},
|
|
537
|
+
onUpdateBangMessage: (command, output, messageId) => {
|
|
538
|
+
if (isExpandedRef.current) return;
|
|
539
|
+
setMessages((prev) =>
|
|
540
|
+
prev.map((m) =>
|
|
541
|
+
m.id === messageId
|
|
542
|
+
? {
|
|
543
|
+
...m,
|
|
544
|
+
blocks: m.blocks.map((b, idx) =>
|
|
545
|
+
idx === m.blocks.length - 1 && b.type === "bang"
|
|
546
|
+
? { ...b, command, output }
|
|
547
|
+
: b,
|
|
548
|
+
),
|
|
549
|
+
}
|
|
550
|
+
: m,
|
|
551
|
+
),
|
|
552
|
+
);
|
|
553
|
+
},
|
|
554
|
+
onCompleteBangMessage: (command, exitCode, messageId) => {
|
|
555
|
+
if (isExpandedRef.current) return;
|
|
556
|
+
setMessages((prev) =>
|
|
557
|
+
prev.map((m) =>
|
|
558
|
+
m.id === messageId
|
|
559
|
+
? {
|
|
560
|
+
...m,
|
|
561
|
+
blocks: m.blocks.map((b, idx) =>
|
|
562
|
+
idx === m.blocks.length - 1 && b.type === "bang"
|
|
563
|
+
? { ...b, command, exitCode, stage: "end" }
|
|
564
|
+
: b,
|
|
565
|
+
),
|
|
566
|
+
}
|
|
567
|
+
: m,
|
|
568
|
+
),
|
|
569
|
+
);
|
|
570
|
+
},
|
|
571
|
+
onLatestTotalTokensChange: (tokens) => {
|
|
572
|
+
setLatestTotalTokens(tokens);
|
|
376
573
|
},
|
|
377
574
|
onMcpServersChange: (servers) => {
|
|
378
575
|
setMcpServerStatuses([...servers]);
|
|
@@ -421,18 +618,6 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
421
618
|
onQueuedMessagesChange: (messages) => {
|
|
422
619
|
setQueuedMessages([...messages]);
|
|
423
620
|
},
|
|
424
|
-
onGoalStateChange: (active, _condition, elapsed) => {
|
|
425
|
-
setIsGoalActive(active);
|
|
426
|
-
if (active) {
|
|
427
|
-
goalStartedAt.current = Date.now();
|
|
428
|
-
} else {
|
|
429
|
-
goalStartedAt.current = null;
|
|
430
|
-
}
|
|
431
|
-
setGoalElapsed(elapsed);
|
|
432
|
-
},
|
|
433
|
-
onGoalEvaluating: (evaluating) => {
|
|
434
|
-
setIsGoalEvaluating(evaluating);
|
|
435
|
-
},
|
|
436
621
|
};
|
|
437
622
|
|
|
438
623
|
try {
|
|
@@ -533,7 +718,10 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
533
718
|
originalCwd,
|
|
534
719
|
model,
|
|
535
720
|
initialPermissionMode,
|
|
536
|
-
|
|
721
|
+
refreshMessages,
|
|
722
|
+
throttledContentUpdate,
|
|
723
|
+
throttledReasoningUpdate,
|
|
724
|
+
throttledToolBlockUpdate,
|
|
537
725
|
mcpServers,
|
|
538
726
|
],
|
|
539
727
|
);
|
|
@@ -571,6 +759,9 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
571
759
|
// Cleanup on unmount
|
|
572
760
|
useEffect(() => {
|
|
573
761
|
return () => {
|
|
762
|
+
throttledContentUpdate.cancel();
|
|
763
|
+
throttledReasoningUpdate.cancel();
|
|
764
|
+
throttledToolBlockUpdate.cancel();
|
|
574
765
|
if (agentRef.current) {
|
|
575
766
|
try {
|
|
576
767
|
// Display usage summary before cleanup
|
|
@@ -584,7 +775,11 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
584
775
|
agentRef.current.destroy();
|
|
585
776
|
}
|
|
586
777
|
};
|
|
587
|
-
}, [
|
|
778
|
+
}, [
|
|
779
|
+
throttledContentUpdate,
|
|
780
|
+
throttledReasoningUpdate,
|
|
781
|
+
throttledToolBlockUpdate,
|
|
782
|
+
]);
|
|
588
783
|
|
|
589
784
|
// Trigger WorktreeRemove hook BEFORE agent destruction
|
|
590
785
|
const triggerWorktreeRemoveHook = useCallback(
|
|
@@ -637,33 +832,34 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
637
832
|
[],
|
|
638
833
|
);
|
|
639
834
|
|
|
640
|
-
const askBtw = useCallback(
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
835
|
+
const askBtw = useCallback(
|
|
836
|
+
async (
|
|
837
|
+
question: string,
|
|
838
|
+
abortSignal?: AbortSignal,
|
|
839
|
+
onContent?: (content: string) => void,
|
|
840
|
+
) => {
|
|
841
|
+
if (!agentRef.current) {
|
|
842
|
+
throw new Error("Agent not initialized");
|
|
843
|
+
}
|
|
844
|
+
return await agentRef.current.askBtw(question, abortSignal, onContent);
|
|
845
|
+
},
|
|
846
|
+
[],
|
|
847
|
+
);
|
|
646
848
|
|
|
647
849
|
const clearMessages = useCallback(async () => {
|
|
648
850
|
await agentRef.current?.clearMessages();
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
) {
|
|
662
|
-
await agentRef.current?.clearGoal();
|
|
663
|
-
} else {
|
|
664
|
-
await agentRef.current?.setGoal(trimmed);
|
|
665
|
-
}
|
|
666
|
-
}, []);
|
|
851
|
+
refreshMessages();
|
|
852
|
+
forceRemount();
|
|
853
|
+
}, [refreshMessages, forceRemount]);
|
|
854
|
+
|
|
855
|
+
const compact = useCallback(
|
|
856
|
+
async (instructions?: string) => {
|
|
857
|
+
await agentRef.current?.compact(instructions);
|
|
858
|
+
refreshMessages();
|
|
859
|
+
forceRemount();
|
|
860
|
+
},
|
|
861
|
+
[refreshMessages, forceRemount],
|
|
862
|
+
);
|
|
667
863
|
|
|
668
864
|
// Unified interrupt method, interrupt both AI messages and command execution
|
|
669
865
|
const abortMessage = useCallback(() => {
|
|
@@ -773,13 +969,14 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
773
969
|
if (agentRef.current) {
|
|
774
970
|
try {
|
|
775
971
|
await agentRef.current.truncateHistory(index);
|
|
776
|
-
|
|
972
|
+
refreshMessages();
|
|
973
|
+
forceRemount();
|
|
777
974
|
} catch (error) {
|
|
778
975
|
logger.error("Failed to rewind:", error);
|
|
779
976
|
}
|
|
780
977
|
}
|
|
781
978
|
},
|
|
782
|
-
[
|
|
979
|
+
[forceRemount, refreshMessages],
|
|
783
980
|
);
|
|
784
981
|
|
|
785
982
|
const getFullMessageThread = useCallback(async () => {
|
|
@@ -827,22 +1024,13 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
827
1024
|
|
|
828
1025
|
if (nextExpanded) {
|
|
829
1026
|
// Transitioning to EXPANDED: Freeze the current view
|
|
830
|
-
//
|
|
831
|
-
throttledSetMessages.cancel();
|
|
1027
|
+
// Incremental updates are skipped while expanded (isExpandedRef guard)
|
|
832
1028
|
} else {
|
|
833
1029
|
// Transitioning to COLLAPSED: Restore from agent's actual state
|
|
834
|
-
|
|
835
|
-
const msgs = [...agentRef.current.messages];
|
|
836
|
-
setMessages(msgs);
|
|
837
|
-
setLatestTotalTokens(extractLatestTotalTokens(msgs));
|
|
838
|
-
}
|
|
1030
|
+
refreshMessages();
|
|
839
1031
|
}
|
|
840
|
-
// Force remount
|
|
841
|
-
|
|
842
|
-
// a previous remount, leaving the UI stuck without a visual update
|
|
843
|
-
stdout?.write("\u001b[2J\u001b[3J\u001b[0;0H", () => {
|
|
844
|
-
setRemountKey((prev) => prev + 1);
|
|
845
|
-
});
|
|
1032
|
+
// Force remount to ensure Static items re-render
|
|
1033
|
+
forceRemount();
|
|
846
1034
|
}
|
|
847
1035
|
|
|
848
1036
|
if (key.ctrl && input === "t") {
|
|
@@ -862,13 +1050,14 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
862
1050
|
isExpanded,
|
|
863
1051
|
isTaskListVisible,
|
|
864
1052
|
setIsTaskListVisible,
|
|
1053
|
+
isBtwActive,
|
|
1054
|
+
setIsBtwActive,
|
|
865
1055
|
queuedMessages,
|
|
866
1056
|
sessionId,
|
|
867
1057
|
sendMessage,
|
|
868
1058
|
askBtw,
|
|
869
1059
|
clearMessages,
|
|
870
1060
|
compact,
|
|
871
|
-
goalCommand,
|
|
872
1061
|
abortMessage,
|
|
873
1062
|
recallQueuedMessage,
|
|
874
1063
|
removeQueuedMessageById,
|
|
@@ -901,7 +1090,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
901
1090
|
handleConfirmationCancel,
|
|
902
1091
|
backgroundCurrentTask,
|
|
903
1092
|
remountKey,
|
|
904
|
-
|
|
1093
|
+
forceRemount,
|
|
905
1094
|
handleRewindSelect,
|
|
906
1095
|
getFullMessageThread,
|
|
907
1096
|
|
|
@@ -912,9 +1101,6 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
912
1101
|
workdir,
|
|
913
1102
|
recreateAgent,
|
|
914
1103
|
triggerWorktreeRemoveHook,
|
|
915
|
-
isGoalActive,
|
|
916
|
-
goalElapsed,
|
|
917
|
-
isGoalEvaluating,
|
|
918
1104
|
};
|
|
919
1105
|
|
|
920
1106
|
return (
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* daemon-cli.ts — Entry point for `wave --daemon <socket-path>` mode.
|
|
3
|
+
*
|
|
4
|
+
* Starts a DaemonServer that serves JSON-RPC over a unix socket. The desktop
|
|
5
|
+
* app launches this on a remote host via nohup/setsid and tunnels the socket
|
|
6
|
+
* back with `ssh -L`; multiple attach/detach cycles share one process, so
|
|
7
|
+
* sessions and pending permissions survive client disconnects. The net server
|
|
8
|
+
* keeps the process alive — there is no stdin to wait on.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { DaemonServer } from "./stdio/daemonServer.js";
|
|
12
|
+
|
|
13
|
+
export async function startDaemonCli(socketPath: string): Promise<void> {
|
|
14
|
+
const server = new DaemonServer({ socketPath });
|
|
15
|
+
await server.start();
|
|
16
|
+
// Ready — any error that follows goes to the daemon log via stderr.
|
|
17
|
+
}
|