wave-code 1.0.0 → 1.0.1
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 +21 -6
- package/dist/components/InputBox.d.ts +0 -3
- package/dist/components/InputBox.js +11 -9
- package/dist/components/LoadingIndicator.d.ts +1 -2
- package/dist/components/LoadingIndicator.js +2 -2
- package/dist/components/Markdown.js +13 -16
- package/dist/components/MessageList.d.ts +2 -1
- package/dist/components/MessageList.js +2 -2
- package/dist/components/StatusLine.d.ts +0 -2
- package/dist/components/StatusLine.js +6 -6
- 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 +3 -5
- package/dist/contexts/useChat.js +242 -82
- 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 +126 -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 +25 -12
- package/src/components/InputBox.tsx +10 -18
- package/src/components/LoadingIndicator.tsx +1 -4
- package/src/components/Markdown.tsx +15 -18
- package/src/components/MessageList.tsx +6 -0
- package/src/components/StatusLine.tsx +0 -10
- 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 +310 -95
- 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 +188 -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/dist/contexts/useChat.js
CHANGED
|
@@ -22,27 +22,104 @@ export const ChatProvider = ({ children, bypassPermissions, permissionMode: init
|
|
|
22
22
|
const [isExpanded, setIsExpanded] = useState(false);
|
|
23
23
|
const isExpandedRef = useRef(isExpanded);
|
|
24
24
|
const [isTaskListVisible, setIsTaskListVisible] = useState(true);
|
|
25
|
+
const [isBtwActive, setIsBtwActive] = useState(false);
|
|
25
26
|
const [messages, setMessages] = useState([]);
|
|
26
27
|
const [latestTotalTokens, setLatestTotalTokens] = useState(0);
|
|
27
28
|
const [maxInputTokens, setMaxInputTokens] = useState(200000);
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
29
|
+
// Throttled incremental streaming updaters — 500ms leading+trailing, the same interval
|
|
30
|
+
// as the pre-incremental throttledSetMessages. `stage === "end"` flushes the final
|
|
31
|
+
// update immediately so completion results are never delayed.
|
|
32
|
+
const throttledContentUpdate = useMemo(() => throttle((params) => {
|
|
33
|
+
const { messageId, accumulated, stage } = params;
|
|
34
|
+
setMessages((prev) => prev.map((m) => {
|
|
35
|
+
if (m.id !== messageId)
|
|
36
|
+
return m;
|
|
37
|
+
const textBlockIndex = m.blocks.findIndex((b) => b.type === "text");
|
|
38
|
+
if (textBlockIndex === -1) {
|
|
39
|
+
return {
|
|
40
|
+
...m,
|
|
41
|
+
blocks: [
|
|
42
|
+
...m.blocks,
|
|
43
|
+
{ type: "text", content: accumulated, stage },
|
|
44
|
+
],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
...m,
|
|
49
|
+
blocks: m.blocks.map((b, idx) => idx === textBlockIndex && b.type === "text"
|
|
50
|
+
? { ...b, content: accumulated, stage }
|
|
51
|
+
: b),
|
|
52
|
+
};
|
|
53
|
+
}));
|
|
54
|
+
}, 500), []);
|
|
55
|
+
const throttledReasoningUpdate = useMemo(() => throttle((params) => {
|
|
56
|
+
const { messageId, accumulated, stage } = params;
|
|
57
|
+
setMessages((prev) => prev.map((m) => {
|
|
58
|
+
if (m.id !== messageId)
|
|
59
|
+
return m;
|
|
60
|
+
const reasoningBlockIndex = m.blocks.findIndex((b) => b.type === "reasoning");
|
|
61
|
+
if (reasoningBlockIndex === -1) {
|
|
62
|
+
return {
|
|
63
|
+
...m,
|
|
64
|
+
blocks: [
|
|
65
|
+
...m.blocks,
|
|
66
|
+
{ type: "reasoning", content: accumulated, stage },
|
|
67
|
+
],
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
...m,
|
|
72
|
+
blocks: m.blocks.map((b, idx) => idx === reasoningBlockIndex && b.type === "reasoning"
|
|
73
|
+
? { ...b, content: accumulated, stage }
|
|
74
|
+
: b),
|
|
75
|
+
};
|
|
76
|
+
}));
|
|
77
|
+
}, 500), []);
|
|
78
|
+
const throttledToolBlockUpdate = useMemo(() => throttle((params) => {
|
|
79
|
+
const { messageId, id: toolBlockId, ...updates } = params;
|
|
80
|
+
setMessages((prev) => prev.map((m) => {
|
|
81
|
+
if (m.id !== messageId)
|
|
82
|
+
return m;
|
|
83
|
+
const toolBlockIndex = m.blocks.findIndex((b) => b.type === "tool" && b.id === toolBlockId);
|
|
84
|
+
if (toolBlockIndex === -1) {
|
|
85
|
+
return {
|
|
86
|
+
...m,
|
|
87
|
+
blocks: [
|
|
88
|
+
...m.blocks,
|
|
89
|
+
{
|
|
90
|
+
type: "tool",
|
|
91
|
+
id: toolBlockId,
|
|
92
|
+
name: updates.name || "",
|
|
93
|
+
stage: updates.stage || "start",
|
|
94
|
+
parameters: updates.parameters || "",
|
|
95
|
+
result: updates.result || "",
|
|
96
|
+
...updates,
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
...m,
|
|
103
|
+
blocks: m.blocks.map((b, idx) => idx === toolBlockIndex && b.type === "tool"
|
|
104
|
+
? { ...b, ...updates }
|
|
105
|
+
: b),
|
|
106
|
+
};
|
|
107
|
+
}));
|
|
108
|
+
}, 500), []);
|
|
35
109
|
useEffect(() => {
|
|
36
110
|
isExpandedRef.current = isExpanded;
|
|
37
111
|
if (isExpanded) {
|
|
38
|
-
|
|
112
|
+
// Cancel pending throttled updates so the frozen expanded view isn't overwritten
|
|
113
|
+
throttledContentUpdate.cancel();
|
|
114
|
+
throttledReasoningUpdate.cancel();
|
|
115
|
+
throttledToolBlockUpdate.cancel();
|
|
39
116
|
}
|
|
40
|
-
}, [
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
117
|
+
}, [
|
|
118
|
+
isExpanded,
|
|
119
|
+
throttledContentUpdate,
|
|
120
|
+
throttledReasoningUpdate,
|
|
121
|
+
throttledToolBlockUpdate,
|
|
122
|
+
]);
|
|
46
123
|
const [isLoading, setIsLoading] = useState(false);
|
|
47
124
|
const [sessionId, setSessionId] = useState("");
|
|
48
125
|
const [isCommandRunning, setIsCommandRunning] = useState(false);
|
|
@@ -50,29 +127,6 @@ export const ChatProvider = ({ children, bypassPermissions, permissionMode: init
|
|
|
50
127
|
const [currentModel, setCurrentModelState] = useState("");
|
|
51
128
|
const [configuredModels, setConfiguredModels] = useState([]);
|
|
52
129
|
const [queuedMessages, setQueuedMessages] = useState([]);
|
|
53
|
-
const [isGoalActive, setIsGoalActive] = useState(false);
|
|
54
|
-
const [goalElapsed, setGoalElapsed] = useState();
|
|
55
|
-
const [isGoalEvaluating, setIsGoalEvaluating] = useState(false);
|
|
56
|
-
const goalStartedAt = useRef(null);
|
|
57
|
-
// Update goal elapsed time every 30s while active
|
|
58
|
-
useEffect(() => {
|
|
59
|
-
if (!isGoalActive || goalStartedAt.current === null)
|
|
60
|
-
return;
|
|
61
|
-
const formatElapsed = (ms) => {
|
|
62
|
-
const minutes = Math.floor(ms / 60000);
|
|
63
|
-
if (minutes < 1)
|
|
64
|
-
return "<1m";
|
|
65
|
-
if (minutes < 60)
|
|
66
|
-
return `${minutes}m`;
|
|
67
|
-
const hours = Math.floor(minutes / 60);
|
|
68
|
-
const remainingMin = minutes % 60;
|
|
69
|
-
return `${hours}h${remainingMin}m`;
|
|
70
|
-
};
|
|
71
|
-
const update = () => setGoalElapsed(formatElapsed(Date.now() - goalStartedAt.current));
|
|
72
|
-
update();
|
|
73
|
-
const timer = setInterval(update, 30000);
|
|
74
|
-
return () => clearInterval(timer);
|
|
75
|
-
}, [isGoalActive]);
|
|
76
130
|
// MCP State
|
|
77
131
|
const [mcpServerStatuses, setMcpServerStatuses] = useState([]);
|
|
78
132
|
// Background tasks state
|
|
@@ -119,6 +173,16 @@ export const ChatProvider = ({ children, bypassPermissions, permissionMode: init
|
|
|
119
173
|
// Status metadata state
|
|
120
174
|
const [workingDirectory, setWorkingDirectory] = useState("");
|
|
121
175
|
const agentRef = useRef(null);
|
|
176
|
+
// Full-list refresh — one-shot pull from the agent, used only for structural
|
|
177
|
+
// changes (compact/clear/rewind/collapse/init). Streaming updates flow through
|
|
178
|
+
// the incremental callbacks in initializeAgent below.
|
|
179
|
+
const refreshMessages = useCallback(() => {
|
|
180
|
+
if (!isExpandedRef.current && agentRef.current) {
|
|
181
|
+
const msgs = [...agentRef.current.messages];
|
|
182
|
+
setMessages(msgs);
|
|
183
|
+
setLatestTotalTokens(extractLatestTotalTokens(msgs));
|
|
184
|
+
}
|
|
185
|
+
}, []);
|
|
122
186
|
// Permission confirmation methods with queue support
|
|
123
187
|
const showConfirmation = useCallback(async (toolName, toolInput, suggestedPrefix, hidePersistentOption, planContent) => {
|
|
124
188
|
return new Promise((resolve, reject) => {
|
|
@@ -139,8 +203,123 @@ export const ChatProvider = ({ children, bypassPermissions, permissionMode: init
|
|
|
139
203
|
const initializeAgent = useCallback(async (restoreSessionIdOverride) => {
|
|
140
204
|
const effectiveRestoreSessionId = restoreSessionIdOverride ?? restoreSessionId;
|
|
141
205
|
const callbacks = {
|
|
142
|
-
|
|
143
|
-
|
|
206
|
+
// ── Incremental message updates (no full-list pushes) ──────
|
|
207
|
+
onUserMessageAdded: () => {
|
|
208
|
+
if (isExpandedRef.current || !agentRef.current)
|
|
209
|
+
return;
|
|
210
|
+
const msgs = agentRef.current.messages;
|
|
211
|
+
const last = msgs[msgs.length - 1];
|
|
212
|
+
if (!last || last.role !== "user")
|
|
213
|
+
return;
|
|
214
|
+
setMessages((prev) => prev.some((m) => m.id === last.id) ? prev : [...prev, last]);
|
|
215
|
+
},
|
|
216
|
+
onAssistantMessageAdded: (messageId) => {
|
|
217
|
+
if (isExpandedRef.current || !agentRef.current)
|
|
218
|
+
return;
|
|
219
|
+
const msg = agentRef.current.messages.find((m) => m.id === messageId);
|
|
220
|
+
if (!msg)
|
|
221
|
+
return;
|
|
222
|
+
setMessages((prev) => prev.some((m) => m.id === messageId) ? prev : [...prev, msg]);
|
|
223
|
+
},
|
|
224
|
+
onAssistantContentUpdated: (params) => {
|
|
225
|
+
if (isExpandedRef.current)
|
|
226
|
+
return;
|
|
227
|
+
throttledContentUpdate(params);
|
|
228
|
+
if (params.stage === "end")
|
|
229
|
+
throttledContentUpdate.flush();
|
|
230
|
+
},
|
|
231
|
+
onAssistantReasoningUpdated: (params) => {
|
|
232
|
+
if (isExpandedRef.current)
|
|
233
|
+
return;
|
|
234
|
+
throttledReasoningUpdate(params);
|
|
235
|
+
if (params.stage === "end")
|
|
236
|
+
throttledReasoningUpdate.flush();
|
|
237
|
+
},
|
|
238
|
+
onToolBlockUpdated: (params) => {
|
|
239
|
+
if (isExpandedRef.current)
|
|
240
|
+
return;
|
|
241
|
+
throttledToolBlockUpdate(params);
|
|
242
|
+
if (params.stage === "end")
|
|
243
|
+
throttledToolBlockUpdate.flush();
|
|
244
|
+
},
|
|
245
|
+
onErrorBlockAdded: (error) => {
|
|
246
|
+
if (isExpandedRef.current)
|
|
247
|
+
return;
|
|
248
|
+
setMessages((prev) => {
|
|
249
|
+
// Append to the last assistant message, or create one if none exists
|
|
250
|
+
for (let i = prev.length - 1; i >= 0; i--) {
|
|
251
|
+
if (prev[i].role === "assistant") {
|
|
252
|
+
return prev.map((m, idx) => idx === i
|
|
253
|
+
? {
|
|
254
|
+
...m,
|
|
255
|
+
blocks: [
|
|
256
|
+
...m.blocks,
|
|
257
|
+
{ type: "error", content: error },
|
|
258
|
+
],
|
|
259
|
+
}
|
|
260
|
+
: m);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return [
|
|
264
|
+
...prev,
|
|
265
|
+
{
|
|
266
|
+
id: `msg-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`,
|
|
267
|
+
role: "assistant",
|
|
268
|
+
timestamp: new Date().toISOString(),
|
|
269
|
+
blocks: [{ type: "error", content: error }],
|
|
270
|
+
},
|
|
271
|
+
];
|
|
272
|
+
});
|
|
273
|
+
},
|
|
274
|
+
onAddBangMessage: (command, messageId) => {
|
|
275
|
+
if (isExpandedRef.current)
|
|
276
|
+
return;
|
|
277
|
+
setMessages((prev) => prev.some((m) => m.id === messageId)
|
|
278
|
+
? prev
|
|
279
|
+
: [
|
|
280
|
+
...prev,
|
|
281
|
+
{
|
|
282
|
+
id: messageId,
|
|
283
|
+
role: "user",
|
|
284
|
+
timestamp: new Date().toISOString(),
|
|
285
|
+
blocks: [
|
|
286
|
+
{
|
|
287
|
+
type: "bang",
|
|
288
|
+
command,
|
|
289
|
+
output: "",
|
|
290
|
+
stage: "running",
|
|
291
|
+
exitCode: null,
|
|
292
|
+
},
|
|
293
|
+
],
|
|
294
|
+
},
|
|
295
|
+
]);
|
|
296
|
+
},
|
|
297
|
+
onUpdateBangMessage: (command, output, messageId) => {
|
|
298
|
+
if (isExpandedRef.current)
|
|
299
|
+
return;
|
|
300
|
+
setMessages((prev) => prev.map((m) => m.id === messageId
|
|
301
|
+
? {
|
|
302
|
+
...m,
|
|
303
|
+
blocks: m.blocks.map((b, idx) => idx === m.blocks.length - 1 && b.type === "bang"
|
|
304
|
+
? { ...b, command, output }
|
|
305
|
+
: b),
|
|
306
|
+
}
|
|
307
|
+
: m));
|
|
308
|
+
},
|
|
309
|
+
onCompleteBangMessage: (command, exitCode, messageId) => {
|
|
310
|
+
if (isExpandedRef.current)
|
|
311
|
+
return;
|
|
312
|
+
setMessages((prev) => prev.map((m) => m.id === messageId
|
|
313
|
+
? {
|
|
314
|
+
...m,
|
|
315
|
+
blocks: m.blocks.map((b, idx) => idx === m.blocks.length - 1 && b.type === "bang"
|
|
316
|
+
? { ...b, command, exitCode, stage: "end" }
|
|
317
|
+
: b),
|
|
318
|
+
}
|
|
319
|
+
: m));
|
|
320
|
+
},
|
|
321
|
+
onLatestTotalTokensChange: (tokens) => {
|
|
322
|
+
setLatestTotalTokens(tokens);
|
|
144
323
|
},
|
|
145
324
|
onMcpServersChange: (servers) => {
|
|
146
325
|
setMcpServerStatuses([...servers]);
|
|
@@ -185,19 +364,6 @@ export const ChatProvider = ({ children, bypassPermissions, permissionMode: init
|
|
|
185
364
|
onQueuedMessagesChange: (messages) => {
|
|
186
365
|
setQueuedMessages([...messages]);
|
|
187
366
|
},
|
|
188
|
-
onGoalStateChange: (active, _condition, elapsed) => {
|
|
189
|
-
setIsGoalActive(active);
|
|
190
|
-
if (active) {
|
|
191
|
-
goalStartedAt.current = Date.now();
|
|
192
|
-
}
|
|
193
|
-
else {
|
|
194
|
-
goalStartedAt.current = null;
|
|
195
|
-
}
|
|
196
|
-
setGoalElapsed(elapsed);
|
|
197
|
-
},
|
|
198
|
-
onGoalEvaluating: (evaluating) => {
|
|
199
|
-
setIsGoalEvaluating(evaluating);
|
|
200
|
-
},
|
|
201
367
|
};
|
|
202
368
|
try {
|
|
203
369
|
// Create the permission callback inside the try block to access showConfirmation
|
|
@@ -283,7 +449,10 @@ export const ChatProvider = ({ children, bypassPermissions, permissionMode: init
|
|
|
283
449
|
originalCwd,
|
|
284
450
|
model,
|
|
285
451
|
initialPermissionMode,
|
|
286
|
-
|
|
452
|
+
refreshMessages,
|
|
453
|
+
throttledContentUpdate,
|
|
454
|
+
throttledReasoningUpdate,
|
|
455
|
+
throttledToolBlockUpdate,
|
|
287
456
|
mcpServers,
|
|
288
457
|
]);
|
|
289
458
|
// Recreate agent (e.g. after plugin install) — destroys current agent and reinitializes
|
|
@@ -318,6 +487,9 @@ export const ChatProvider = ({ children, bypassPermissions, permissionMode: init
|
|
|
318
487
|
// Cleanup on unmount
|
|
319
488
|
useEffect(() => {
|
|
320
489
|
return () => {
|
|
490
|
+
throttledContentUpdate.cancel();
|
|
491
|
+
throttledReasoningUpdate.cancel();
|
|
492
|
+
throttledToolBlockUpdate.cancel();
|
|
321
493
|
if (agentRef.current) {
|
|
322
494
|
try {
|
|
323
495
|
// Display usage summary before cleanup
|
|
@@ -331,7 +503,11 @@ export const ChatProvider = ({ children, bypassPermissions, permissionMode: init
|
|
|
331
503
|
agentRef.current.destroy();
|
|
332
504
|
}
|
|
333
505
|
};
|
|
334
|
-
}, [
|
|
506
|
+
}, [
|
|
507
|
+
throttledContentUpdate,
|
|
508
|
+
throttledReasoningUpdate,
|
|
509
|
+
throttledToolBlockUpdate,
|
|
510
|
+
]);
|
|
335
511
|
// Trigger WorktreeRemove hook BEFORE agent destruction
|
|
336
512
|
const triggerWorktreeRemoveHook = useCallback(async (worktreePath) => {
|
|
337
513
|
await agentRef.current?.triggerWorktreeRemoveHook(worktreePath);
|
|
@@ -368,30 +544,20 @@ export const ChatProvider = ({ children, bypassPermissions, permissionMode: init
|
|
|
368
544
|
console.error("Failed to send message:", error);
|
|
369
545
|
}
|
|
370
546
|
}, []);
|
|
371
|
-
const askBtw = useCallback(async (question) => {
|
|
547
|
+
const askBtw = useCallback(async (question, abortSignal, onContent) => {
|
|
372
548
|
if (!agentRef.current) {
|
|
373
549
|
throw new Error("Agent not initialized");
|
|
374
550
|
}
|
|
375
|
-
return await agentRef.current.askBtw(question);
|
|
551
|
+
return await agentRef.current.askBtw(question, abortSignal, onContent);
|
|
376
552
|
}, []);
|
|
377
553
|
const clearMessages = useCallback(async () => {
|
|
378
554
|
await agentRef.current?.clearMessages();
|
|
379
|
-
|
|
555
|
+
refreshMessages();
|
|
556
|
+
}, [refreshMessages]);
|
|
380
557
|
const compact = useCallback(async (instructions) => {
|
|
381
558
|
await agentRef.current?.compact(instructions);
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
const trimmed = args?.trim() ?? "";
|
|
385
|
-
if (!trimmed) {
|
|
386
|
-
await agentRef.current?.showGoalStatus();
|
|
387
|
-
}
|
|
388
|
-
else if (["clear", "stop", "off", "reset", "none", "cancel"].includes(trimmed)) {
|
|
389
|
-
await agentRef.current?.clearGoal();
|
|
390
|
-
}
|
|
391
|
-
else {
|
|
392
|
-
await agentRef.current?.setGoal(trimmed);
|
|
393
|
-
}
|
|
394
|
-
}, []);
|
|
559
|
+
refreshMessages();
|
|
560
|
+
}, [refreshMessages]);
|
|
395
561
|
// Unified interrupt method, interrupt both AI messages and command execution
|
|
396
562
|
const abortMessage = useCallback(() => {
|
|
397
563
|
agentRef.current?.abortMessage();
|
|
@@ -484,13 +650,14 @@ export const ChatProvider = ({ children, bypassPermissions, permissionMode: init
|
|
|
484
650
|
if (agentRef.current) {
|
|
485
651
|
try {
|
|
486
652
|
await agentRef.current.truncateHistory(index);
|
|
653
|
+
refreshMessages();
|
|
487
654
|
requestRemount();
|
|
488
655
|
}
|
|
489
656
|
catch (error) {
|
|
490
657
|
logger.error("Failed to rewind:", error);
|
|
491
658
|
}
|
|
492
659
|
}
|
|
493
|
-
}, [requestRemount]);
|
|
660
|
+
}, [requestRemount, refreshMessages]);
|
|
494
661
|
const getFullMessageThread = useCallback(async () => {
|
|
495
662
|
if (agentRef.current) {
|
|
496
663
|
return await agentRef.current.getFullMessageThread();
|
|
@@ -530,16 +697,11 @@ export const ChatProvider = ({ children, bypassPermissions, permissionMode: init
|
|
|
530
697
|
isExpandedRef.current = nextExpanded;
|
|
531
698
|
if (nextExpanded) {
|
|
532
699
|
// Transitioning to EXPANDED: Freeze the current view
|
|
533
|
-
//
|
|
534
|
-
throttledSetMessages.cancel();
|
|
700
|
+
// Incremental updates are skipped while expanded (isExpandedRef guard)
|
|
535
701
|
}
|
|
536
702
|
else {
|
|
537
703
|
// Transitioning to COLLAPSED: Restore from agent's actual state
|
|
538
|
-
|
|
539
|
-
const msgs = [...agentRef.current.messages];
|
|
540
|
-
setMessages(msgs);
|
|
541
|
-
setLatestTotalTokens(extractLatestTotalTokens(msgs));
|
|
542
|
-
}
|
|
704
|
+
refreshMessages();
|
|
543
705
|
}
|
|
544
706
|
// Force remount directly (bypass throttle) to ensure Static items re-render
|
|
545
707
|
// The throttled requestRemount can be dropped if pressed too quickly after
|
|
@@ -563,13 +725,14 @@ export const ChatProvider = ({ children, bypassPermissions, permissionMode: init
|
|
|
563
725
|
isExpanded,
|
|
564
726
|
isTaskListVisible,
|
|
565
727
|
setIsTaskListVisible,
|
|
728
|
+
isBtwActive,
|
|
729
|
+
setIsBtwActive,
|
|
566
730
|
queuedMessages,
|
|
567
731
|
sessionId,
|
|
568
732
|
sendMessage,
|
|
569
733
|
askBtw,
|
|
570
734
|
clearMessages,
|
|
571
735
|
compact,
|
|
572
|
-
goalCommand,
|
|
573
736
|
abortMessage,
|
|
574
737
|
recallQueuedMessage,
|
|
575
738
|
removeQueuedMessageById,
|
|
@@ -612,9 +775,6 @@ export const ChatProvider = ({ children, bypassPermissions, permissionMode: init
|
|
|
612
775
|
workdir,
|
|
613
776
|
recreateAgent,
|
|
614
777
|
triggerWorktreeRemoveHook,
|
|
615
|
-
isGoalActive,
|
|
616
|
-
goalElapsed,
|
|
617
|
-
isGoalEvaluating,
|
|
618
778
|
};
|
|
619
779
|
return (_jsx(ChatContext.Provider, { value: contextValue, children: children }));
|
|
620
780
|
};
|
|
@@ -0,0 +1,10 @@
|
|
|
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
|
+
export declare function startDaemonCli(socketPath: string): Promise<void>;
|
|
@@ -0,0 +1,15 @@
|
|
|
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
|
+
import { DaemonServer } from "./stdio/daemonServer.js";
|
|
11
|
+
export async function startDaemonCli(socketPath) {
|
|
12
|
+
const server = new DaemonServer({ socketPath });
|
|
13
|
+
await server.start();
|
|
14
|
+
// Ready — any error that follows goes to the daemon log via stderr.
|
|
15
|
+
}
|
|
@@ -1,10 +1,20 @@
|
|
|
1
|
-
import { useEffect, useReducer, useCallback } from "react";
|
|
2
|
-
import { inputReducer, initialState, ESC_DOUBLE_PRESS_TIMEOUT_MS, } from "../managers/inputReducer.js";
|
|
1
|
+
import { useEffect, useReducer, useCallback, useRef } from "react";
|
|
2
|
+
import { inputReducer, initialState, ESC_DOUBLE_PRESS_TIMEOUT_MS, btwOverlayActiveRef, } from "../managers/inputReducer.js";
|
|
3
|
+
import { createBracketedPasteDetector } from "../utils/bracketedPaste.js";
|
|
3
4
|
import { searchFiles as searchFilesUtil, PromptHistoryManager, } from "wave-agent-sdk";
|
|
4
5
|
import * as handlers from "../managers/inputHandlers.js";
|
|
5
6
|
export const useInputManager = (callbacks = {}) => {
|
|
6
7
|
const [state, dispatch] = useReducer(inputReducer, initialState);
|
|
7
|
-
|
|
8
|
+
// Detects bracketed paste (DECSET 2004) markers so pasted text is inserted
|
|
9
|
+
// without triggering submit — a pasted trailing \r must not be treated as
|
|
10
|
+
// Enter (see utils/bracketedPaste.ts).
|
|
11
|
+
const pasteDetectorRef = useRef(createBracketedPasteDetector());
|
|
12
|
+
// Abort plumbing for the /btw side question: ABORT_BTW (set when the overlay
|
|
13
|
+
// is dismissed while loading) aborts the in-flight onAskBtw call; the
|
|
14
|
+
// dismissed ref suppresses the late SET_BTW_STATE dispatch / error logging.
|
|
15
|
+
const btwAbortRef = useRef(null);
|
|
16
|
+
const btwDismissedRef = useRef(false);
|
|
17
|
+
const { onInputTextChange, onCursorPositionChange, onFileSelectorStateChange, onCommandSelectorStateChange, onHistorySearchStateChange, onBackgroundTaskManagerStateChange, onMcpManagerStateChange, onRewindManagerStateChange, onHelpStateChange, onStatusCommandStateChange, onPluginManagerStateChange, onModelSelectorStateChange, onWorkflowManagerStateChange, onImagesStateChange, onSendMessage, onHasSlashCommand, onAbortMessage, onBackgroundCurrentTask, onPermissionModeChange, onAskBtw, sessionId, workdir, getFullMessageThread, logger, hasQueuedMessages: hasQueuedMessagesProp, onRecallQueuedMessage, onClearMessages, onCompact, isIdle: isIdleProp, } = callbacks;
|
|
8
18
|
// Handle debounced file search
|
|
9
19
|
useEffect(() => {
|
|
10
20
|
if (state.showFileSelector) {
|
|
@@ -67,25 +77,49 @@ export const useInputManager = (callbacks = {}) => {
|
|
|
67
77
|
case "BACKGROUND_CURRENT_TASK":
|
|
68
78
|
onBackgroundCurrentTask?.();
|
|
69
79
|
break;
|
|
70
|
-
case "ASK_BTW":
|
|
80
|
+
case "ASK_BTW": {
|
|
81
|
+
const controller = new AbortController();
|
|
82
|
+
btwAbortRef.current = controller;
|
|
83
|
+
btwDismissedRef.current = false;
|
|
71
84
|
try {
|
|
72
|
-
const answer = await onAskBtw?.(effect.question)
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
85
|
+
const answer = await onAskBtw?.(effect.question, controller.signal, (content) => {
|
|
86
|
+
// Stream partial answers into the overlay so the user sees
|
|
87
|
+
// the response grow in real time (same visual language as
|
|
88
|
+
// assistant text / thinking blocks) instead of a static
|
|
89
|
+
// "Answering..." indicator.
|
|
90
|
+
if (!btwDismissedRef.current) {
|
|
91
|
+
dispatch({
|
|
92
|
+
type: "SET_BTW_STATE",
|
|
93
|
+
payload: { answer: content, isLoading: true },
|
|
94
|
+
});
|
|
95
|
+
}
|
|
76
96
|
});
|
|
97
|
+
if (!btwDismissedRef.current) {
|
|
98
|
+
dispatch({
|
|
99
|
+
type: "SET_BTW_STATE",
|
|
100
|
+
payload: { answer, isLoading: false },
|
|
101
|
+
});
|
|
102
|
+
}
|
|
77
103
|
}
|
|
78
104
|
catch (error) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
105
|
+
if (!btwDismissedRef.current) {
|
|
106
|
+
console.error("Failed to ask side question:", error);
|
|
107
|
+
dispatch({
|
|
108
|
+
type: "SET_BTW_STATE",
|
|
109
|
+
payload: {
|
|
110
|
+
answer: "Error: Failed to get an answer for your side question.",
|
|
111
|
+
isLoading: false,
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
}
|
|
87
115
|
}
|
|
88
116
|
break;
|
|
117
|
+
}
|
|
118
|
+
case "ABORT_BTW":
|
|
119
|
+
btwDismissedRef.current = true;
|
|
120
|
+
btwAbortRef.current?.abort();
|
|
121
|
+
btwAbortRef.current = null;
|
|
122
|
+
break;
|
|
89
123
|
case "PERMISSION_MODE_CHANGE":
|
|
90
124
|
onPermissionModeChange?.(effect.mode);
|
|
91
125
|
break;
|
|
@@ -155,6 +189,18 @@ export const useInputManager = (callbacks = {}) => {
|
|
|
155
189
|
else if (command === "logout") {
|
|
156
190
|
dispatch({ type: "SET_SHOW_LOGIN_COMMAND", payload: true });
|
|
157
191
|
}
|
|
192
|
+
else if (command === "btw") {
|
|
193
|
+
// Bare /btw executed via the command selector — show usage
|
|
194
|
+
// (aligned with Claude Code's empty-args message).
|
|
195
|
+
dispatch({
|
|
196
|
+
type: "SET_BTW_STATE",
|
|
197
|
+
payload: {
|
|
198
|
+
question: "",
|
|
199
|
+
isLoading: false,
|
|
200
|
+
answer: "Usage: /btw <your question>",
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
}
|
|
158
204
|
else if (command === "plugin") {
|
|
159
205
|
dispatch({ type: "SET_SHOW_PLUGIN_MANAGER", payload: true });
|
|
160
206
|
}
|
|
@@ -170,9 +216,6 @@ export const useInputManager = (callbacks = {}) => {
|
|
|
170
216
|
else if (command === "compact") {
|
|
171
217
|
await onCompact?.(effect.args);
|
|
172
218
|
}
|
|
173
|
-
else if (command === "goal") {
|
|
174
|
-
await onGoalCommand?.(effect.args);
|
|
175
|
-
}
|
|
176
219
|
}
|
|
177
220
|
break;
|
|
178
221
|
case "RECALL_QUEUED_MESSAGE":
|
|
@@ -200,7 +243,6 @@ export const useInputManager = (callbacks = {}) => {
|
|
|
200
243
|
onRecallQueuedMessage,
|
|
201
244
|
onClearMessages,
|
|
202
245
|
onCompact,
|
|
203
|
-
onGoalCommand,
|
|
204
246
|
]);
|
|
205
247
|
useEffect(() => {
|
|
206
248
|
onFileSelectorStateChange?.(state.showFileSelector, state.filteredFiles, state.fileSearchQuery, state.atPosition);
|
|
@@ -253,7 +295,13 @@ export const useInputManager = (callbacks = {}) => {
|
|
|
253
295
|
useEffect(() => {
|
|
254
296
|
onImagesStateChange?.(state.attachedImages);
|
|
255
297
|
}, [state.attachedImages, onImagesStateChange]);
|
|
256
|
-
//
|
|
298
|
+
// Keep the shared overlay-active flag in sync so App's Ctrl+C exit handler
|
|
299
|
+
// can defer to the /btw overlay (ink runs ALL useInput handlers per keypress
|
|
300
|
+
// with no propagation control).
|
|
301
|
+
useEffect(() => {
|
|
302
|
+
btwOverlayActiveRef.current =
|
|
303
|
+
state.btwState.question !== "" || state.btwState.answer !== undefined;
|
|
304
|
+
}, [state.btwState.question, state.btwState.answer]);
|
|
257
305
|
// Methods
|
|
258
306
|
const insertTextAtCursor = useCallback((text) => {
|
|
259
307
|
dispatch({ type: "INSERT_TEXT", payload: text });
|
|
@@ -397,10 +445,39 @@ export const useInputManager = (callbacks = {}) => {
|
|
|
397
445
|
dispatch({ type: "CLEAR_LONG_TEXT_MAP" });
|
|
398
446
|
}, []);
|
|
399
447
|
const handleInput = useCallback(async (input, key) => {
|
|
448
|
+
const result = pasteDetectorRef.current.process(input);
|
|
449
|
+
if (result.kind === "consume") {
|
|
450
|
+
// Content of an in-flight bracketed paste (or an empty paste):
|
|
451
|
+
// hold it, never submit or insert prematurely.
|
|
452
|
+
return true;
|
|
453
|
+
}
|
|
454
|
+
if (result.kind === "paste") {
|
|
455
|
+
if (result.leadingInput) {
|
|
456
|
+
dispatch({
|
|
457
|
+
type: "HANDLE_KEY",
|
|
458
|
+
payload: {
|
|
459
|
+
input: result.leadingInput,
|
|
460
|
+
key,
|
|
461
|
+
hasSlashCommand: (cmd) => !!onHasSlashCommand?.(cmd),
|
|
462
|
+
hasQueuedMessages: hasQueuedMessagesProp ?? false,
|
|
463
|
+
isIdle: isIdleProp ?? false,
|
|
464
|
+
},
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
if (result.text !== "") {
|
|
468
|
+
// Insert-only: \r → \n normalizes CRLF terminals, matching the
|
|
469
|
+
// canonical paste path (inputHandlers.handlePasteInput).
|
|
470
|
+
dispatch({
|
|
471
|
+
type: "INSERT_TEXT_WITH_PLACEHOLDER",
|
|
472
|
+
payload: result.text.replace(/\r/g, "\n"),
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
return true;
|
|
476
|
+
}
|
|
400
477
|
dispatch({
|
|
401
478
|
type: "HANDLE_KEY",
|
|
402
479
|
payload: {
|
|
403
|
-
input,
|
|
480
|
+
input: result.input,
|
|
404
481
|
key,
|
|
405
482
|
hasSlashCommand: (cmd) => !!onHasSlashCommand?.(cmd),
|
|
406
483
|
hasQueuedMessages: hasQueuedMessagesProp ?? false,
|
package/dist/index.js
CHANGED
|
@@ -44,6 +44,11 @@ export async function main() {
|
|
|
44
44
|
type: "boolean",
|
|
45
45
|
default: false,
|
|
46
46
|
global: false,
|
|
47
|
+
})
|
|
48
|
+
.option("daemon", {
|
|
49
|
+
description: "Start as a background daemon (JSON-RPC over a unix socket at PATH)",
|
|
50
|
+
type: "string",
|
|
51
|
+
global: false,
|
|
47
52
|
})
|
|
48
53
|
.option("show-stats", {
|
|
49
54
|
description: "Show timing and usage statistics in print mode",
|
|
@@ -300,6 +305,11 @@ export async function main() {
|
|
|
300
305
|
const { startStdioCli } = await import("./stdio-cli.js");
|
|
301
306
|
return startStdioCli();
|
|
302
307
|
}
|
|
308
|
+
// Handle daemon mode (remote background sessions)
|
|
309
|
+
if (typeof argv.daemon === "string") {
|
|
310
|
+
const { startDaemonCli } = await import("./daemon-cli.js");
|
|
311
|
+
return startDaemonCli(argv.daemon);
|
|
312
|
+
}
|
|
303
313
|
await startCli({
|
|
304
314
|
restoreSessionId: argv.restore,
|
|
305
315
|
continueLastSession: argv.continue,
|