wave-code 1.1.0 → 1.1.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/bin/wave-code.js +9 -0
- package/dist/bundle/wave.mjs +490 -466
- package/package.json +2 -2
- package/src/cli.tsx +6 -1
- package/src/components/InputBox.tsx +14 -1
- package/src/components/SkillsManager.tsx +312 -0
- package/src/constants/commands.ts +6 -0
- package/src/contexts/useChat.tsx +259 -136
- package/src/hooks/useInputManager.ts +6 -0
- package/src/managers/inputHandlers.ts +2 -0
- package/src/managers/inputReducer.ts +9 -0
- package/src/print-cli.ts +4 -0
- package/src/reducers/skillsManagerReducer.ts +91 -0
- package/src/stdio/agentBridge.ts +8 -0
- package/src/stdio/protocol.ts +2 -0
package/src/contexts/useChat.tsx
CHANGED
|
@@ -5,6 +5,7 @@ import React, {
|
|
|
5
5
|
useRef,
|
|
6
6
|
useEffect,
|
|
7
7
|
useState,
|
|
8
|
+
useMemo,
|
|
8
9
|
} from "react";
|
|
9
10
|
import { useInput, useStdout } from "ink";
|
|
10
11
|
import { useAppConfig } from "./useAppConfig.js";
|
|
@@ -15,6 +16,7 @@ import type {
|
|
|
15
16
|
Task,
|
|
16
17
|
SlashCommand,
|
|
17
18
|
SubagentConfiguration,
|
|
19
|
+
SkillMetadata,
|
|
18
20
|
PermissionDecision,
|
|
19
21
|
PermissionMode,
|
|
20
22
|
QueuedMessage,
|
|
@@ -100,6 +102,8 @@ export interface ChatContextType {
|
|
|
100
102
|
hasSlashCommand: (commandId: string) => boolean;
|
|
101
103
|
// Agent definitions (for /agents overlay)
|
|
102
104
|
agentDefinitions: SubagentConfiguration[];
|
|
105
|
+
// Skill metadata (for /skills overlay)
|
|
106
|
+
skills: SkillMetadata[];
|
|
103
107
|
// Permission functionality
|
|
104
108
|
permissionMode: PermissionMode;
|
|
105
109
|
setPermissionMode: (mode: PermissionMode) => void;
|
|
@@ -193,6 +197,200 @@ const snapshotMessage = (message: Message): Message => ({
|
|
|
193
197
|
blocks: message.blocks.map((block) => ({ ...block })),
|
|
194
198
|
});
|
|
195
199
|
|
|
200
|
+
/**
|
|
201
|
+
* Pure updater: appends a text-content delta to the target message's text
|
|
202
|
+
* block (creating it on first delta), applying the stage signal.
|
|
203
|
+
*/
|
|
204
|
+
const applyContentDelta = (
|
|
205
|
+
prev: Message[],
|
|
206
|
+
params: StreamingUpdateParams,
|
|
207
|
+
): Message[] => {
|
|
208
|
+
const { messageId, chunk, stage } = params;
|
|
209
|
+
return prev.map((m) => {
|
|
210
|
+
if (m.id !== messageId) return m;
|
|
211
|
+
const textBlockIndex = m.blocks.findIndex((b) => b.type === "text");
|
|
212
|
+
if (textBlockIndex === -1) {
|
|
213
|
+
return {
|
|
214
|
+
...m,
|
|
215
|
+
blocks: [...m.blocks, { type: "text", content: chunk, stage }],
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
return {
|
|
219
|
+
...m,
|
|
220
|
+
blocks: m.blocks.map((b, idx) =>
|
|
221
|
+
idx === textBlockIndex && b.type === "text"
|
|
222
|
+
? {
|
|
223
|
+
...b,
|
|
224
|
+
content: (b.content || "") + chunk,
|
|
225
|
+
stage,
|
|
226
|
+
}
|
|
227
|
+
: b,
|
|
228
|
+
),
|
|
229
|
+
};
|
|
230
|
+
});
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Pure updater: appends a reasoning delta to the target message's reasoning
|
|
235
|
+
* block (creating it on first delta), applying the stage signal.
|
|
236
|
+
*/
|
|
237
|
+
const applyReasoningDelta = (
|
|
238
|
+
prev: Message[],
|
|
239
|
+
params: StreamingUpdateParams,
|
|
240
|
+
): Message[] => {
|
|
241
|
+
const { messageId, chunk, stage } = params;
|
|
242
|
+
return prev.map((m) => {
|
|
243
|
+
if (m.id !== messageId) return m;
|
|
244
|
+
const reasoningBlockIndex = m.blocks.findIndex(
|
|
245
|
+
(b) => b.type === "reasoning",
|
|
246
|
+
);
|
|
247
|
+
if (reasoningBlockIndex === -1) {
|
|
248
|
+
return {
|
|
249
|
+
...m,
|
|
250
|
+
blocks: [...m.blocks, { type: "reasoning", content: chunk, stage }],
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
return {
|
|
254
|
+
...m,
|
|
255
|
+
blocks: m.blocks.map((b, idx) =>
|
|
256
|
+
idx === reasoningBlockIndex && b.type === "reasoning"
|
|
257
|
+
? {
|
|
258
|
+
...b,
|
|
259
|
+
content: (b.content || "") + chunk,
|
|
260
|
+
stage,
|
|
261
|
+
}
|
|
262
|
+
: b,
|
|
263
|
+
),
|
|
264
|
+
};
|
|
265
|
+
});
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Pure updater: applies a tool block update. Streaming carries only the
|
|
270
|
+
* `parametersChunk` delta, appended to the accumulated parameters;
|
|
271
|
+
* start/running/end carry the authoritative value and replace wholesale.
|
|
272
|
+
*/
|
|
273
|
+
const applyToolBlockUpdate = (
|
|
274
|
+
prev: Message[],
|
|
275
|
+
params: ToolBlockUpdateCallbackParams,
|
|
276
|
+
): Message[] => {
|
|
277
|
+
const { messageId, id: toolBlockId, parametersChunk, ...updates } = params;
|
|
278
|
+
return prev.map((m) => {
|
|
279
|
+
if (m.id !== messageId) return m;
|
|
280
|
+
const toolBlockIndex = m.blocks.findIndex(
|
|
281
|
+
(b) => b.type === "tool" && b.id === toolBlockId,
|
|
282
|
+
);
|
|
283
|
+
if (toolBlockIndex === -1) {
|
|
284
|
+
return {
|
|
285
|
+
...m,
|
|
286
|
+
blocks: [
|
|
287
|
+
...m.blocks,
|
|
288
|
+
{
|
|
289
|
+
type: "tool",
|
|
290
|
+
id: toolBlockId,
|
|
291
|
+
name: updates.name || "",
|
|
292
|
+
stage: updates.stage || "start",
|
|
293
|
+
parameters: (updates.parameters || "") + (parametersChunk || ""),
|
|
294
|
+
result: updates.result || "",
|
|
295
|
+
...updates,
|
|
296
|
+
},
|
|
297
|
+
],
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
return {
|
|
301
|
+
...m,
|
|
302
|
+
blocks: m.blocks.map((b, idx) =>
|
|
303
|
+
idx === toolBlockIndex && b.type === "tool"
|
|
304
|
+
? {
|
|
305
|
+
...b,
|
|
306
|
+
...updates,
|
|
307
|
+
parameters: parametersChunk
|
|
308
|
+
? (b.parameters || "") + parametersChunk
|
|
309
|
+
: updates.parameters !== undefined
|
|
310
|
+
? updates.parameters
|
|
311
|
+
: b.parameters,
|
|
312
|
+
}
|
|
313
|
+
: b,
|
|
314
|
+
),
|
|
315
|
+
};
|
|
316
|
+
});
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Single throttled updater entry for ALL message-state updates. The one
|
|
321
|
+
* 500ms window-concat window replaces the previous three-channel throttles
|
|
322
|
+
* (content/reasoning/tool), so every message-state update — including the
|
|
323
|
+
* tool `running` stage's high-frequency `shortResult`/`result` updates from
|
|
324
|
+
* bash — coalesces into the same window.
|
|
325
|
+
*
|
|
326
|
+
* Semantics: leading edge applies immediately and opens a window; updates
|
|
327
|
+
* arriving within the window are queued in arrival order (FIFO) and applied
|
|
328
|
+
* at the trailing edge as ONE composed updater, so no update is lost and no
|
|
329
|
+
* update is reordered. `flush` applies queued updates immediately (used by
|
|
330
|
+
* end signals and one-shot structural updates); `cancel` drops queued
|
|
331
|
+
* updates (used by refreshMessages after pulling the authoritative snapshot —
|
|
332
|
+
* the queued updates are already contained in it).
|
|
333
|
+
*
|
|
334
|
+
* The FIFO queue structurally replaces the old tool throttle's "drop a
|
|
335
|
+
* tool's buffered streaming deltas when running arrives" logic: a running
|
|
336
|
+
* updater is queued BEHIND the streaming chunks that preceded it and applies
|
|
337
|
+
* after them, so a stale streaming event can never flush after running and
|
|
338
|
+
* regress the stage (yellow dot -> gray). See
|
|
339
|
+
* docs/specs/core/stream-content-updates.md.
|
|
340
|
+
*/
|
|
341
|
+
export function createThrottledUpdater<T>(
|
|
342
|
+
apply: (updater: (prev: T) => T) => void,
|
|
343
|
+
wait: number,
|
|
344
|
+
): {
|
|
345
|
+
(updater: (prev: T) => T): void;
|
|
346
|
+
cancel: () => void;
|
|
347
|
+
flush: () => void;
|
|
348
|
+
} {
|
|
349
|
+
let timer: NodeJS.Timeout | null = null;
|
|
350
|
+
let queued: Array<(prev: T) => T> = [];
|
|
351
|
+
|
|
352
|
+
const fire = () => {
|
|
353
|
+
if (queued.length === 0) return;
|
|
354
|
+
const batch = queued;
|
|
355
|
+
queued = [];
|
|
356
|
+
// Compose the whole batch into a single updater applied in FIFO order —
|
|
357
|
+
// one state commit per trailing edge, no update dropped, no reordering.
|
|
358
|
+
apply((prev) => batch.reduce((acc, upd) => upd(acc), prev));
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
const throttled = (updater: (prev: T) => T) => {
|
|
362
|
+
if (timer) {
|
|
363
|
+
queued.push(updater);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
// Leading edge: apply immediately, then open a window whose trailing
|
|
367
|
+
// edge only carries updates arriving within the window.
|
|
368
|
+
apply(updater);
|
|
369
|
+
timer = setTimeout(() => {
|
|
370
|
+
timer = null;
|
|
371
|
+
fire();
|
|
372
|
+
}, wait);
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
throttled.cancel = () => {
|
|
376
|
+
if (timer) {
|
|
377
|
+
clearTimeout(timer);
|
|
378
|
+
timer = null;
|
|
379
|
+
}
|
|
380
|
+
queued = [];
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
throttled.flush = () => {
|
|
384
|
+
if (timer) {
|
|
385
|
+
clearTimeout(timer);
|
|
386
|
+
timer = null;
|
|
387
|
+
}
|
|
388
|
+
fire();
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
return throttled;
|
|
392
|
+
}
|
|
393
|
+
|
|
196
394
|
export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
197
395
|
children,
|
|
198
396
|
bypassPermissions,
|
|
@@ -223,132 +421,26 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
223
421
|
const [latestTotalTokens, setLatestTotalTokens] = useState(0);
|
|
224
422
|
const [maxInputTokens, setMaxInputTokens] = useState(200000);
|
|
225
423
|
|
|
226
|
-
//
|
|
227
|
-
//
|
|
228
|
-
//
|
|
229
|
-
//
|
|
230
|
-
//
|
|
231
|
-
//
|
|
232
|
-
//
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
setMessages((prev) =>
|
|
237
|
-
prev.map((m) => {
|
|
238
|
-
if (m.id !== messageId) return m;
|
|
239
|
-
const textBlockIndex = m.blocks.findIndex((b) => b.type === "text");
|
|
240
|
-
if (textBlockIndex === -1) {
|
|
241
|
-
return {
|
|
242
|
-
...m,
|
|
243
|
-
blocks: [...m.blocks, { type: "text", content: chunk, stage }],
|
|
244
|
-
};
|
|
245
|
-
}
|
|
246
|
-
return {
|
|
247
|
-
...m,
|
|
248
|
-
blocks: m.blocks.map((b, idx) =>
|
|
249
|
-
idx === textBlockIndex && b.type === "text"
|
|
250
|
-
? {
|
|
251
|
-
...b,
|
|
252
|
-
content: (b.content || "") + chunk,
|
|
253
|
-
stage,
|
|
254
|
-
}
|
|
255
|
-
: b,
|
|
256
|
-
),
|
|
257
|
-
};
|
|
258
|
-
}),
|
|
259
|
-
);
|
|
260
|
-
}, []);
|
|
261
|
-
|
|
262
|
-
const applyReasoningUpdate = useCallback((params: StreamingUpdateParams) => {
|
|
263
|
-
const { messageId, chunk, stage } = params;
|
|
264
|
-
setMessages((prev) =>
|
|
265
|
-
prev.map((m) => {
|
|
266
|
-
if (m.id !== messageId) return m;
|
|
267
|
-
const reasoningBlockIndex = m.blocks.findIndex(
|
|
268
|
-
(b) => b.type === "reasoning",
|
|
269
|
-
);
|
|
270
|
-
if (reasoningBlockIndex === -1) {
|
|
271
|
-
return {
|
|
272
|
-
...m,
|
|
273
|
-
blocks: [...m.blocks, { type: "reasoning", content: chunk, stage }],
|
|
274
|
-
};
|
|
275
|
-
}
|
|
276
|
-
return {
|
|
277
|
-
...m,
|
|
278
|
-
blocks: m.blocks.map((b, idx) =>
|
|
279
|
-
idx === reasoningBlockIndex && b.type === "reasoning"
|
|
280
|
-
? {
|
|
281
|
-
...b,
|
|
282
|
-
content: (b.content || "") + chunk,
|
|
283
|
-
stage,
|
|
284
|
-
}
|
|
285
|
-
: b,
|
|
286
|
-
),
|
|
287
|
-
};
|
|
288
|
-
}),
|
|
289
|
-
);
|
|
290
|
-
}, []);
|
|
291
|
-
|
|
292
|
-
const applyToolBlockUpdate = useCallback(
|
|
293
|
-
(params: ToolBlockUpdateCallbackParams) => {
|
|
294
|
-
const {
|
|
295
|
-
messageId,
|
|
296
|
-
id: toolBlockId,
|
|
297
|
-
parametersChunk,
|
|
298
|
-
...updates
|
|
299
|
-
} = params;
|
|
300
|
-
setMessages((prev) =>
|
|
301
|
-
prev.map((m) => {
|
|
302
|
-
if (m.id !== messageId) return m;
|
|
303
|
-
const toolBlockIndex = m.blocks.findIndex(
|
|
304
|
-
(b) => b.type === "tool" && b.id === toolBlockId,
|
|
305
|
-
);
|
|
306
|
-
if (toolBlockIndex === -1) {
|
|
307
|
-
return {
|
|
308
|
-
...m,
|
|
309
|
-
blocks: [
|
|
310
|
-
...m.blocks,
|
|
311
|
-
{
|
|
312
|
-
type: "tool",
|
|
313
|
-
id: toolBlockId,
|
|
314
|
-
name: updates.name || "",
|
|
315
|
-
stage: updates.stage || "start",
|
|
316
|
-
parameters:
|
|
317
|
-
(updates.parameters || "") + (parametersChunk || ""),
|
|
318
|
-
result: updates.result || "",
|
|
319
|
-
...updates,
|
|
320
|
-
},
|
|
321
|
-
],
|
|
322
|
-
};
|
|
323
|
-
}
|
|
324
|
-
return {
|
|
325
|
-
...m,
|
|
326
|
-
blocks: m.blocks.map((b, idx) =>
|
|
327
|
-
idx === toolBlockIndex && b.type === "tool"
|
|
328
|
-
? {
|
|
329
|
-
...b,
|
|
330
|
-
...updates,
|
|
331
|
-
// Streaming carries only the delta; append it to the
|
|
332
|
-
// accumulated parameters. start/running/end carry the
|
|
333
|
-
// authoritative value and replace wholesale.
|
|
334
|
-
parameters: parametersChunk
|
|
335
|
-
? (b.parameters || "") + parametersChunk
|
|
336
|
-
: updates.parameters !== undefined
|
|
337
|
-
? updates.parameters
|
|
338
|
-
: b.parameters,
|
|
339
|
-
}
|
|
340
|
-
: b,
|
|
341
|
-
),
|
|
342
|
-
};
|
|
343
|
-
}),
|
|
344
|
-
);
|
|
345
|
-
},
|
|
424
|
+
// Single throttled entry for ALL message-state updates — one 500ms
|
|
425
|
+
// window-concat window shared by every callback (content/reasoning deltas,
|
|
426
|
+
// tool parametersChunk, tool `running` stage shortResult/result, one-shot
|
|
427
|
+
// structural updates). The tool `running` stage used to bypass throttling
|
|
428
|
+
// (bash shortResult height changes per chunk → layout flicker); routing it
|
|
429
|
+
// through the same window as streaming deltas coalesces it to ≤1 render per
|
|
430
|
+
// window. See createThrottledUpdater + docs/specs/core/stream-content-updates.md.
|
|
431
|
+
const updateMessages = useMemo(
|
|
432
|
+
() =>
|
|
433
|
+
createThrottledUpdater<Message[]>((updater) => setMessages(updater), 500),
|
|
346
434
|
[],
|
|
347
435
|
);
|
|
348
436
|
|
|
349
437
|
useEffect(() => {
|
|
350
438
|
isExpandedRef.current = isExpanded;
|
|
351
|
-
|
|
439
|
+
if (isExpanded) {
|
|
440
|
+
// Cancel pending throttled updates so the frozen expanded view isn't overwritten
|
|
441
|
+
updateMessages.cancel();
|
|
442
|
+
}
|
|
443
|
+
}, [isExpanded, updateMessages]);
|
|
352
444
|
|
|
353
445
|
const [isLoading, setIsLoading] = useState(false);
|
|
354
446
|
const [sessionId, setSessionId] = useState("");
|
|
@@ -378,6 +470,8 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
378
470
|
const [agentDefinitions, setAgentDefinitions] = useState<
|
|
379
471
|
SubagentConfiguration[]
|
|
380
472
|
>([]);
|
|
473
|
+
// Skill metadata (for /skills overlay)
|
|
474
|
+
const [skills, setSkills] = useState<SkillMetadata[]>([]);
|
|
381
475
|
|
|
382
476
|
// Permission state
|
|
383
477
|
const [permissionMode, setPermissionModeState] = useState<PermissionMode>(
|
|
@@ -447,10 +541,19 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
447
541
|
const refreshMessages = useCallback(() => {
|
|
448
542
|
if (!isExpandedRef.current && agentRef.current) {
|
|
449
543
|
const msgs = agentRef.current.messages.map(snapshotMessage);
|
|
544
|
+
// Snapshot-safe: the full-list replacement makes the SDK state
|
|
545
|
+
// authoritative. Any update still queued inside the 500ms throttle
|
|
546
|
+
// window was applied to the SDK before this pull, so it is already
|
|
547
|
+
// contained in `msgs` — dropping it prevents the trailing-edge flush
|
|
548
|
+
// from re-appending the pre-refresh chunk on top of the snapshot
|
|
549
|
+
// (first-word duplication). Updates arriving after the pull start fresh
|
|
550
|
+
// windows and append on top of the snapshot. See
|
|
551
|
+
// docs/specs/core/stream-content-updates.md.
|
|
552
|
+
updateMessages.cancel();
|
|
450
553
|
setMessages(msgs);
|
|
451
554
|
setLatestTotalTokens(extractLatestTotalTokens(msgs));
|
|
452
555
|
}
|
|
453
|
-
}, []);
|
|
556
|
+
}, [updateMessages]);
|
|
454
557
|
|
|
455
558
|
// Permission confirmation methods with queue support
|
|
456
559
|
const showConfirmation = useCallback(
|
|
@@ -491,18 +594,23 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
491
594
|
|
|
492
595
|
const callbacks: AgentCallbacks = {
|
|
493
596
|
// ── Incremental message updates (no full-list pushes) ──────
|
|
597
|
+
// All of these funnel through the single throttled `updateMessages`
|
|
598
|
+
// entry, so every message-state update shares one 500ms window.
|
|
494
599
|
onUserMessageAdded: () => {
|
|
495
600
|
if (isExpandedRef.current || !agentRef.current) return;
|
|
496
601
|
const msgs = agentRef.current.messages;
|
|
497
602
|
const last = msgs[msgs.length - 1];
|
|
498
603
|
if (!last || last.role !== "user") return;
|
|
499
604
|
// Eager snapshot at callback time — React defers updater execution to
|
|
500
|
-
// the batch flush, so evaluating snapshotMessage inside
|
|
605
|
+
// the batch flush, so evaluating snapshotMessage inside the updater
|
|
501
606
|
// would read the SDK message after its in-place mutations.
|
|
502
607
|
const snapshot = snapshotMessage(last);
|
|
503
|
-
|
|
608
|
+
updateMessages((prev) =>
|
|
504
609
|
prev.some((m) => m.id === last.id) ? prev : [...prev, snapshot],
|
|
505
610
|
);
|
|
611
|
+
// One-shot structural update — flush immediately so the message
|
|
612
|
+
// card appears at once (queuing it gains no coalescing).
|
|
613
|
+
updateMessages.flush();
|
|
506
614
|
},
|
|
507
615
|
onAssistantMessageAdded: (messageId: string) => {
|
|
508
616
|
if (isExpandedRef.current || !agentRef.current) return;
|
|
@@ -512,25 +620,29 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
512
620
|
// fires this callback BEFORE the first delta writes into the shared
|
|
513
621
|
// message, so capturing here copies the pre-mutation (empty) blocks.
|
|
514
622
|
const snapshot = snapshotMessage(msg);
|
|
515
|
-
|
|
623
|
+
updateMessages((prev) =>
|
|
516
624
|
prev.some((m) => m.id === messageId) ? prev : [...prev, snapshot],
|
|
517
625
|
);
|
|
626
|
+
updateMessages.flush();
|
|
518
627
|
},
|
|
519
628
|
onAssistantContentUpdated: (params) => {
|
|
520
629
|
if (isExpandedRef.current) return;
|
|
521
|
-
|
|
630
|
+
updateMessages((prev) => applyContentDelta(prev, params));
|
|
631
|
+
if (params.stage === "end") updateMessages.flush();
|
|
522
632
|
},
|
|
523
633
|
onAssistantReasoningUpdated: (params) => {
|
|
524
634
|
if (isExpandedRef.current) return;
|
|
525
|
-
|
|
635
|
+
updateMessages((prev) => applyReasoningDelta(prev, params));
|
|
636
|
+
if (params.stage === "end") updateMessages.flush();
|
|
526
637
|
},
|
|
527
638
|
onToolBlockUpdated: (params) => {
|
|
528
639
|
if (isExpandedRef.current) return;
|
|
529
|
-
applyToolBlockUpdate(params);
|
|
640
|
+
updateMessages((prev) => applyToolBlockUpdate(prev, params));
|
|
641
|
+
if (params.stage === "end") updateMessages.flush();
|
|
530
642
|
},
|
|
531
643
|
onErrorBlockAdded: (error: string) => {
|
|
532
644
|
if (isExpandedRef.current) return;
|
|
533
|
-
|
|
645
|
+
updateMessages((prev) => {
|
|
534
646
|
// Append to the LAST message only if it is an assistant message
|
|
535
647
|
// (the current turn's in-flight reply). If the last message is a
|
|
536
648
|
// user message, the error belongs BELOW it — create a new
|
|
@@ -558,10 +670,13 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
558
670
|
},
|
|
559
671
|
];
|
|
560
672
|
});
|
|
673
|
+
// One-shot structural update — flush immediately (errors must not be
|
|
674
|
+
// delayed by a streaming window).
|
|
675
|
+
updateMessages.flush();
|
|
561
676
|
},
|
|
562
677
|
onAddBangMessage: (command, messageId) => {
|
|
563
678
|
if (isExpandedRef.current) return;
|
|
564
|
-
|
|
679
|
+
updateMessages((prev) =>
|
|
565
680
|
prev.some((m) => m.id === messageId)
|
|
566
681
|
? prev
|
|
567
682
|
: [
|
|
@@ -582,10 +697,11 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
582
697
|
},
|
|
583
698
|
],
|
|
584
699
|
);
|
|
700
|
+
updateMessages.flush();
|
|
585
701
|
},
|
|
586
702
|
onUpdateBangMessage: (command, output, messageId) => {
|
|
587
703
|
if (isExpandedRef.current) return;
|
|
588
|
-
|
|
704
|
+
updateMessages((prev) =>
|
|
589
705
|
prev.map((m) =>
|
|
590
706
|
m.id === messageId
|
|
591
707
|
? {
|
|
@@ -602,7 +718,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
602
718
|
},
|
|
603
719
|
onCompleteBangMessage: (command, exitCode, messageId, output) => {
|
|
604
720
|
if (isExpandedRef.current) return;
|
|
605
|
-
|
|
721
|
+
updateMessages((prev) =>
|
|
606
722
|
prev.map((m) =>
|
|
607
723
|
m.id === messageId
|
|
608
724
|
? {
|
|
@@ -622,6 +738,8 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
622
738
|
: m,
|
|
623
739
|
),
|
|
624
740
|
);
|
|
741
|
+
// Completion signal — flush so the final state applies immediately.
|
|
742
|
+
updateMessages.flush();
|
|
625
743
|
},
|
|
626
744
|
onLatestTotalTokensChange: (tokens) => {
|
|
627
745
|
setLatestTotalTokens(tokens);
|
|
@@ -772,6 +890,10 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
772
890
|
const initialAgentDefinitions =
|
|
773
891
|
agent.getSubagentConfigurations?.() || [];
|
|
774
892
|
setAgentDefinitions(initialAgentDefinitions);
|
|
893
|
+
|
|
894
|
+
// Get initial skill metadata
|
|
895
|
+
const initialSkills = agent.getSkillMetadata?.() || [];
|
|
896
|
+
setSkills(initialSkills);
|
|
775
897
|
} catch (error) {
|
|
776
898
|
console.error("Failed to initialize AI manager:", error);
|
|
777
899
|
}
|
|
@@ -791,9 +913,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
791
913
|
originalCwd,
|
|
792
914
|
model,
|
|
793
915
|
initialPermissionMode,
|
|
794
|
-
|
|
795
|
-
applyReasoningUpdate,
|
|
796
|
-
applyToolBlockUpdate,
|
|
916
|
+
updateMessages,
|
|
797
917
|
mcpServers,
|
|
798
918
|
],
|
|
799
919
|
);
|
|
@@ -813,6 +933,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
813
933
|
setMcpServerStatuses([]);
|
|
814
934
|
setSlashCommands([]);
|
|
815
935
|
setAgentDefinitions([]);
|
|
936
|
+
setSkills([]);
|
|
816
937
|
setSessionId("");
|
|
817
938
|
setIsLoading(false);
|
|
818
939
|
setLatestTotalTokens(0);
|
|
@@ -832,6 +953,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
832
953
|
// Cleanup on unmount
|
|
833
954
|
useEffect(() => {
|
|
834
955
|
return () => {
|
|
956
|
+
updateMessages.cancel();
|
|
835
957
|
if (agentRef.current) {
|
|
836
958
|
try {
|
|
837
959
|
// Display usage summary before cleanup
|
|
@@ -845,7 +967,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
845
967
|
agentRef.current.destroy();
|
|
846
968
|
}
|
|
847
969
|
};
|
|
848
|
-
}, []);
|
|
970
|
+
}, [updateMessages]);
|
|
849
971
|
|
|
850
972
|
// Send message function (including judgment logic)
|
|
851
973
|
const sendMessage = useCallback(
|
|
@@ -1188,6 +1310,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
1188
1310
|
slashCommands,
|
|
1189
1311
|
hasSlashCommand,
|
|
1190
1312
|
agentDefinitions,
|
|
1313
|
+
skills,
|
|
1191
1314
|
permissionMode,
|
|
1192
1315
|
setPermissionMode,
|
|
1193
1316
|
isConfirmationVisible,
|
|
@@ -527,6 +527,10 @@ export const useInputManager = (
|
|
|
527
527
|
dispatch({ type: "SET_SHOW_WORKFLOW_MANAGER", payload: show });
|
|
528
528
|
}, []);
|
|
529
529
|
|
|
530
|
+
const setShowSkillsManager = useCallback((show: boolean) => {
|
|
531
|
+
dispatch({ type: "SET_SHOW_SKILLS_MANAGER", payload: show });
|
|
532
|
+
}, []);
|
|
533
|
+
|
|
530
534
|
const setPermissionMode = useCallback(
|
|
531
535
|
(mode: PermissionMode) => {
|
|
532
536
|
dispatch({ type: "SET_PERMISSION_MODE", payload: mode });
|
|
@@ -671,6 +675,7 @@ export const useInputManager = (
|
|
|
671
675
|
showPluginManager: state.showPluginManager,
|
|
672
676
|
showModelSelector: state.showModelSelector,
|
|
673
677
|
showWorkflowManager: state.showWorkflowManager,
|
|
678
|
+
showSkillsManager: state.showSkillsManager,
|
|
674
679
|
permissionMode: state.permissionMode,
|
|
675
680
|
attachedImages: state.attachedImages,
|
|
676
681
|
btwState: state.btwState,
|
|
@@ -717,6 +722,7 @@ export const useInputManager = (
|
|
|
717
722
|
setShowPluginManager,
|
|
718
723
|
setShowModelSelector,
|
|
719
724
|
setShowWorkflowManager,
|
|
725
|
+
setShowSkillsManager,
|
|
720
726
|
setPermissionMode,
|
|
721
727
|
setBtwState,
|
|
722
728
|
|
|
@@ -369,6 +369,8 @@ export const handleCommandSelect = (
|
|
|
369
369
|
dispatch({ type: "SET_SHOW_MCP_MANAGER", payload: true });
|
|
370
370
|
} else if (command === "agents") {
|
|
371
371
|
dispatch({ type: "SET_SHOW_AGENTS_MANAGER", payload: true });
|
|
372
|
+
} else if (command === "skills") {
|
|
373
|
+
dispatch({ type: "SET_SHOW_SKILLS_MANAGER", payload: true });
|
|
372
374
|
} else if (command === "rewind") {
|
|
373
375
|
dispatch({ type: "SET_SHOW_REWIND_MANAGER", payload: true });
|
|
374
376
|
} else if (command === "help") {
|
|
@@ -138,6 +138,7 @@ export interface InputState {
|
|
|
138
138
|
showPluginManager: boolean;
|
|
139
139
|
showModelSelector: boolean;
|
|
140
140
|
showWorkflowManager: boolean;
|
|
141
|
+
showSkillsManager: boolean;
|
|
141
142
|
permissionMode: PermissionMode;
|
|
142
143
|
selectorJustUsed: boolean;
|
|
143
144
|
history: PromptEntry[];
|
|
@@ -176,6 +177,7 @@ export const initialState: InputState = {
|
|
|
176
177
|
showPluginManager: false,
|
|
177
178
|
showModelSelector: false,
|
|
178
179
|
showWorkflowManager: false,
|
|
180
|
+
showSkillsManager: false,
|
|
179
181
|
permissionMode: "default",
|
|
180
182
|
selectorJustUsed: false,
|
|
181
183
|
history: [],
|
|
@@ -457,6 +459,7 @@ export type InputAction =
|
|
|
457
459
|
| { type: "SET_SHOW_PLUGIN_MANAGER"; payload: boolean }
|
|
458
460
|
| { type: "SET_SHOW_MODEL_SELECTOR"; payload: boolean }
|
|
459
461
|
| { type: "SET_SHOW_WORKFLOW_MANAGER"; payload: boolean }
|
|
462
|
+
| { type: "SET_SHOW_SKILLS_MANAGER"; payload: boolean }
|
|
460
463
|
| { type: "SET_PERMISSION_MODE"; payload: PermissionMode }
|
|
461
464
|
| { type: "SET_SELECTOR_JUST_USED"; payload: boolean }
|
|
462
465
|
| { type: "INSERT_TEXT_WITH_PLACEHOLDER"; payload: string }
|
|
@@ -687,6 +690,12 @@ export function inputReducer(
|
|
|
687
690
|
showWorkflowManager: action.payload,
|
|
688
691
|
selectorJustUsed: !action.payload ? true : state.selectorJustUsed,
|
|
689
692
|
};
|
|
693
|
+
case "SET_SHOW_SKILLS_MANAGER":
|
|
694
|
+
return {
|
|
695
|
+
...state,
|
|
696
|
+
showSkillsManager: action.payload,
|
|
697
|
+
selectorJustUsed: !action.payload ? true : state.selectorJustUsed,
|
|
698
|
+
};
|
|
690
699
|
case "SET_PERMISSION_MODE":
|
|
691
700
|
return { ...state, permissionMode: action.payload };
|
|
692
701
|
case "SET_SELECTOR_JUST_USED":
|
package/src/print-cli.ts
CHANGED
|
@@ -217,7 +217,9 @@ export async function startPrintCli(options: PrintCliOptions): Promise<void> {
|
|
|
217
217
|
worktreeSession.repoRoot,
|
|
218
218
|
);
|
|
219
219
|
}
|
|
220
|
+
process.stdout.write("\nDeleting worktree ...\n");
|
|
220
221
|
await removeWorktree(worktreeSession);
|
|
222
|
+
process.stdout.write("Done.\n");
|
|
221
223
|
} catch (error) {
|
|
222
224
|
// Never block print-mode exit on worktree cleanup failures
|
|
223
225
|
process.stdout.write(
|
|
@@ -273,7 +275,9 @@ export async function startPrintCli(options: PrintCliOptions): Promise<void> {
|
|
|
273
275
|
worktreeSession.repoRoot,
|
|
274
276
|
);
|
|
275
277
|
}
|
|
278
|
+
process.stdout.write("\nDeleting worktree ...\n");
|
|
276
279
|
await removeWorktree(worktreeSession);
|
|
280
|
+
process.stdout.write("Done.\n");
|
|
277
281
|
} catch (error) {
|
|
278
282
|
process.stdout.write(
|
|
279
283
|
`\n⚠️ Skipping worktree removal: ${(error as Error).message}\n`,
|