wave-code 1.0.9 → 1.1.0

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.
Files changed (244) hide show
  1. package/bin/wave-code.js +8 -2
  2. package/dist/bundle/wave.mjs +3207 -0
  3. package/package.json +24 -25
  4. package/src/commands/update.ts +2 -7
  5. package/src/components/App.tsx +3 -7
  6. package/src/components/BtwDisplay.tsx +10 -1
  7. package/src/components/ChatInterface.tsx +21 -52
  8. package/src/components/ConfirmationDetails.tsx +144 -37
  9. package/src/components/DiffDisplay.tsx +335 -301
  10. package/src/components/InputBox.tsx +3 -1
  11. package/src/components/LoadingIndicator.tsx +16 -2
  12. package/src/components/MessageBlockItem.tsx +2 -4
  13. package/src/components/MessageList.tsx +75 -14
  14. package/src/components/ReasoningDisplay.tsx +2 -4
  15. package/src/components/SessionSelector.tsx +82 -11
  16. package/src/components/StatusCommand.tsx +2 -9
  17. package/src/components/ToolDisplay.tsx +2 -4
  18. package/src/contexts/useChat.tsx +172 -350
  19. package/src/daemon/commands.ts +57 -27
  20. package/src/daemon/jsonRpcClient.ts +1 -1
  21. package/src/daemon/socketClient.ts +2 -2
  22. package/src/hooks/useInputManager.ts +1 -1
  23. package/src/index.ts +57 -10
  24. package/src/managers/inputReducer.ts +186 -104
  25. package/src/print-cli.ts +21 -17
  26. package/src/reducers/selectorReducer.ts +10 -0
  27. package/src/session-selector-cli.tsx +269 -28
  28. package/src/stdio/agentBridge.ts +22 -21
  29. package/src/stdio/daemonServer.ts +3 -1
  30. package/src/stdio/jsonRpcConnection.ts +19 -1
  31. package/src/stdio/stdioServer.ts +17 -0
  32. package/src/utils/clipboard.ts +21 -0
  33. package/src/utils/highlightUtils.ts +46 -1
  34. package/src/utils/readPackageJson.ts +29 -0
  35. package/src/utils/streamingText.ts +11 -0
  36. package/src/utils/worktree.ts +191 -43
  37. package/dist/cli.d.ts +0 -6
  38. package/dist/cli.js +0 -53
  39. package/dist/commands/plugin/disable.d.ts +0 -5
  40. package/dist/commands/plugin/disable.js +0 -15
  41. package/dist/commands/plugin/enable.d.ts +0 -5
  42. package/dist/commands/plugin/enable.js +0 -15
  43. package/dist/commands/plugin/install.d.ts +0 -5
  44. package/dist/commands/plugin/install.js +0 -21
  45. package/dist/commands/plugin/list.d.ts +0 -1
  46. package/dist/commands/plugin/list.js +0 -31
  47. package/dist/commands/plugin/marketplace.d.ts +0 -10
  48. package/dist/commands/plugin/marketplace.js +0 -90
  49. package/dist/commands/plugin/uninstall.d.ts +0 -3
  50. package/dist/commands/plugin/uninstall.js +0 -16
  51. package/dist/commands/plugin/update.d.ts +0 -3
  52. package/dist/commands/plugin/update.js +0 -15
  53. package/dist/commands/update.d.ts +0 -1
  54. package/dist/commands/update.js +0 -97
  55. package/dist/components/AgentsManager.d.ts +0 -7
  56. package/dist/components/AgentsManager.js +0 -109
  57. package/dist/components/App.d.ts +0 -9
  58. package/dist/components/App.js +0 -76
  59. package/dist/components/BackgroundTaskManager.d.ts +0 -5
  60. package/dist/components/BackgroundTaskManager.js +0 -99
  61. package/dist/components/BangDisplay.d.ts +0 -8
  62. package/dist/components/BangDisplay.js +0 -17
  63. package/dist/components/BtwDisplay.d.ts +0 -7
  64. package/dist/components/BtwDisplay.js +0 -19
  65. package/dist/components/ChatInterface.d.ts +0 -2
  66. package/dist/components/ChatInterface.js +0 -61
  67. package/dist/components/CommandSelector.d.ts +0 -10
  68. package/dist/components/CommandSelector.js +0 -66
  69. package/dist/components/CompactDisplay.d.ts +0 -8
  70. package/dist/components/CompactDisplay.js +0 -13
  71. package/dist/components/ConfirmationDetails.d.ts +0 -9
  72. package/dist/components/ConfirmationDetails.js +0 -45
  73. package/dist/components/ConfirmationSelector.d.ts +0 -13
  74. package/dist/components/ConfirmationSelector.js +0 -127
  75. package/dist/components/DiffDisplay.d.ts +0 -8
  76. package/dist/components/DiffDisplay.js +0 -201
  77. package/dist/components/DiscoverView.d.ts +0 -2
  78. package/dist/components/DiscoverView.js +0 -37
  79. package/dist/components/FileSelector.d.ts +0 -11
  80. package/dist/components/FileSelector.js +0 -50
  81. package/dist/components/HelpView.d.ts +0 -7
  82. package/dist/components/HelpView.js +0 -74
  83. package/dist/components/HistorySearch.d.ts +0 -8
  84. package/dist/components/HistorySearch.js +0 -66
  85. package/dist/components/InputBox.d.ts +0 -24
  86. package/dist/components/InputBox.js +0 -149
  87. package/dist/components/InstalledView.d.ts +0 -2
  88. package/dist/components/InstalledView.js +0 -45
  89. package/dist/components/LoadingIndicator.d.ts +0 -10
  90. package/dist/components/LoadingIndicator.js +0 -6
  91. package/dist/components/LoginCommand.d.ts +0 -5
  92. package/dist/components/LoginCommand.js +0 -153
  93. package/dist/components/Markdown.d.ts +0 -5
  94. package/dist/components/Markdown.js +0 -127
  95. package/dist/components/MarketplaceAddForm.d.ts +0 -2
  96. package/dist/components/MarketplaceAddForm.js +0 -55
  97. package/dist/components/MarketplaceDetail.d.ts +0 -2
  98. package/dist/components/MarketplaceDetail.js +0 -74
  99. package/dist/components/MarketplaceList.d.ts +0 -8
  100. package/dist/components/MarketplaceList.js +0 -17
  101. package/dist/components/MarketplaceView.d.ts +0 -2
  102. package/dist/components/MarketplaceView.js +0 -41
  103. package/dist/components/McpManager.d.ts +0 -9
  104. package/dist/components/McpManager.js +0 -88
  105. package/dist/components/MessageBlockItem.d.ts +0 -8
  106. package/dist/components/MessageBlockItem.js +0 -15
  107. package/dist/components/MessageList.d.ts +0 -10
  108. package/dist/components/MessageList.js +0 -80
  109. package/dist/components/ModelSelector.d.ts +0 -8
  110. package/dist/components/ModelSelector.js +0 -45
  111. package/dist/components/Notifications.d.ts +0 -7
  112. package/dist/components/Notifications.js +0 -9
  113. package/dist/components/PlanDisplay.d.ts +0 -7
  114. package/dist/components/PlanDisplay.js +0 -6
  115. package/dist/components/PluginDetail.d.ts +0 -2
  116. package/dist/components/PluginDetail.js +0 -87
  117. package/dist/components/PluginList.d.ts +0 -13
  118. package/dist/components/PluginList.js +0 -15
  119. package/dist/components/PluginManagerShell.d.ts +0 -6
  120. package/dist/components/PluginManagerShell.js +0 -92
  121. package/dist/components/PluginManagerTypes.d.ts +0 -35
  122. package/dist/components/PluginManagerTypes.js +0 -1
  123. package/dist/components/QueuedMessageList.d.ts +0 -2
  124. package/dist/components/QueuedMessageList.js +0 -18
  125. package/dist/components/ReasoningDisplay.d.ts +0 -8
  126. package/dist/components/ReasoningDisplay.js +0 -13
  127. package/dist/components/RewindCommand.d.ts +0 -12
  128. package/dist/components/RewindCommand.js +0 -78
  129. package/dist/components/SessionSelector.d.ts +0 -12
  130. package/dist/components/SessionSelector.js +0 -47
  131. package/dist/components/StatusCommand.d.ts +0 -5
  132. package/dist/components/StatusCommand.js +0 -28
  133. package/dist/components/StatusLine.d.ts +0 -6
  134. package/dist/components/StatusLine.js +0 -9
  135. package/dist/components/TaskList.d.ts +0 -5
  136. package/dist/components/TaskList.js +0 -197
  137. package/dist/components/TaskNotificationMessage.d.ts +0 -5
  138. package/dist/components/TaskNotificationMessage.js +0 -12
  139. package/dist/components/ToolDisplay.d.ts +0 -9
  140. package/dist/components/ToolDisplay.js +0 -55
  141. package/dist/components/WorkflowManager.d.ts +0 -5
  142. package/dist/components/WorkflowManager.js +0 -102
  143. package/dist/components/WorktreeExitPrompt.d.ts +0 -12
  144. package/dist/components/WorktreeExitPrompt.js +0 -35
  145. package/dist/constants/commands.d.ts +0 -2
  146. package/dist/constants/commands.js +0 -92
  147. package/dist/contexts/PluginManagerContext.d.ts +0 -3
  148. package/dist/contexts/PluginManagerContext.js +0 -9
  149. package/dist/contexts/useAppConfig.d.ts +0 -10
  150. package/dist/contexts/useAppConfig.js +0 -13
  151. package/dist/contexts/useChat.d.ts +0 -101
  152. package/dist/contexts/useChat.js +0 -992
  153. package/dist/contracts/status.d.ts +0 -7
  154. package/dist/contracts/status.js +0 -1
  155. package/dist/daemon/commands.d.ts +0 -49
  156. package/dist/daemon/commands.js +0 -341
  157. package/dist/daemon/jsonRpcClient.d.ts +0 -38
  158. package/dist/daemon/jsonRpcClient.js +0 -129
  159. package/dist/daemon/socketClient.d.ts +0 -13
  160. package/dist/daemon/socketClient.js +0 -26
  161. package/dist/daemon-cli.d.ts +0 -10
  162. package/dist/daemon-cli.js +0 -15
  163. package/dist/hooks/useInputManager.d.ts +0 -75
  164. package/dist/hooks/useInputManager.js +0 -583
  165. package/dist/hooks/usePluginManager.d.ts +0 -4
  166. package/dist/hooks/usePluginManager.js +0 -265
  167. package/dist/hooks/useTasks.d.ts +0 -1
  168. package/dist/hooks/useTasks.js +0 -5
  169. package/dist/index.d.ts +0 -4
  170. package/dist/index.js +0 -447
  171. package/dist/managers/inputHandlers.d.ts +0 -30
  172. package/dist/managers/inputHandlers.js +0 -625
  173. package/dist/managers/inputReducer.d.ts +0 -271
  174. package/dist/managers/inputReducer.js +0 -1121
  175. package/dist/plugin-manager-cli.d.ts +0 -5
  176. package/dist/plugin-manager-cli.js +0 -12
  177. package/dist/print-cli.d.ts +0 -10
  178. package/dist/print-cli.js +0 -209
  179. package/dist/reducers/agentsManagerReducer.d.ts +0 -26
  180. package/dist/reducers/agentsManagerReducer.js +0 -54
  181. package/dist/reducers/backgroundTaskManagerReducer.d.ts +0 -62
  182. package/dist/reducers/backgroundTaskManagerReducer.js +0 -106
  183. package/dist/reducers/confirmationReducer.d.ts +0 -37
  184. package/dist/reducers/confirmationReducer.js +0 -193
  185. package/dist/reducers/helpSelectorReducer.d.ts +0 -25
  186. package/dist/reducers/helpSelectorReducer.js +0 -52
  187. package/dist/reducers/marketplaceAddFormReducer.d.ts +0 -24
  188. package/dist/reducers/marketplaceAddFormReducer.js +0 -58
  189. package/dist/reducers/mcpManagerReducer.d.ts +0 -37
  190. package/dist/reducers/mcpManagerReducer.js +0 -74
  191. package/dist/reducers/pluginDetailReducer.d.ts +0 -32
  192. package/dist/reducers/pluginDetailReducer.js +0 -70
  193. package/dist/reducers/questionReducer.d.ts +0 -82
  194. package/dist/reducers/questionReducer.js +0 -375
  195. package/dist/reducers/rewindSelectorReducer.d.ts +0 -21
  196. package/dist/reducers/rewindSelectorReducer.js +0 -37
  197. package/dist/reducers/selectorReducer.d.ts +0 -23
  198. package/dist/reducers/selectorReducer.js +0 -42
  199. package/dist/reducers/workflowManagerReducer.d.ts +0 -40
  200. package/dist/reducers/workflowManagerReducer.js +0 -100
  201. package/dist/session-selector-cli.d.ts +0 -3
  202. package/dist/session-selector-cli.js +0 -25
  203. package/dist/stdio/agentBridge.d.ts +0 -139
  204. package/dist/stdio/agentBridge.js +0 -999
  205. package/dist/stdio/daemonServer.d.ts +0 -67
  206. package/dist/stdio/daemonServer.js +0 -191
  207. package/dist/stdio/index.d.ts +0 -5
  208. package/dist/stdio/index.js +0 -5
  209. package/dist/stdio/jsonRpcConnection.d.ts +0 -30
  210. package/dist/stdio/jsonRpcConnection.js +0 -127
  211. package/dist/stdio/protocol.d.ts +0 -41
  212. package/dist/stdio/protocol.js +0 -26
  213. package/dist/stdio/stdioServer.d.ts +0 -28
  214. package/dist/stdio/stdioServer.js +0 -36
  215. package/dist/stdio-cli.d.ts +0 -8
  216. package/dist/stdio-cli.js +0 -15
  217. package/dist/types.d.ts +0 -20
  218. package/dist/types.js +0 -1
  219. package/dist/utils/bracketedPaste.d.ts +0 -39
  220. package/dist/utils/bracketedPaste.js +0 -122
  221. package/dist/utils/clipboard.d.ts +0 -21
  222. package/dist/utils/clipboard.js +0 -347
  223. package/dist/utils/constants.d.ts +0 -16
  224. package/dist/utils/constants.js +0 -18
  225. package/dist/utils/highlightUtils.d.ts +0 -1
  226. package/dist/utils/highlightUtils.js +0 -93
  227. package/dist/utils/inputUtils.d.ts +0 -17
  228. package/dist/utils/inputUtils.js +0 -67
  229. package/dist/utils/logger.d.ts +0 -71
  230. package/dist/utils/logger.js +0 -259
  231. package/dist/utils/markdownTable.d.ts +0 -34
  232. package/dist/utils/markdownTable.js +0 -302
  233. package/dist/utils/rewindCheckpoints.d.ts +0 -8
  234. package/dist/utils/rewindCheckpoints.js +0 -17
  235. package/dist/utils/throttle.d.ts +0 -14
  236. package/dist/utils/throttle.js +0 -55
  237. package/dist/utils/toolParameterTransforms.d.ts +0 -18
  238. package/dist/utils/toolParameterTransforms.js +0 -73
  239. package/dist/utils/usageSummary.d.ts +0 -28
  240. package/dist/utils/usageSummary.js +0 -121
  241. package/dist/utils/version.d.ts +0 -1
  242. package/dist/utils/version.js +0 -4
  243. package/dist/utils/worktree.d.ts +0 -35
  244. package/dist/utils/worktree.js +0 -219
@@ -5,7 +5,6 @@ import React, {
5
5
  useRef,
6
6
  useEffect,
7
7
  useState,
8
- useMemo,
9
8
  } from "react";
10
9
  import { useInput, useStdout } from "ink";
11
10
  import { useAppConfig } from "./useAppConfig.js";
@@ -42,6 +41,9 @@ export interface ChatContextType {
42
41
  isLoading: boolean;
43
42
  isCommandRunning: boolean;
44
43
  isCompacting: boolean;
44
+ /** Accumulated streaming text from the compaction fork, shown as the
45
+ * last-30-chars tail on the CLI compacting loading indicator. */
46
+ compactionStream: string;
45
47
  // Message display state
46
48
  isExpanded: boolean;
47
49
  isTaskListVisible: boolean;
@@ -144,8 +146,6 @@ export interface ChatContextType {
144
146
  workdir?: string;
145
147
  // Agent recreation (e.g. after plugin install)
146
148
  recreateAgent: () => void;
147
- // Trigger WorktreeRemove hook BEFORE agent destruction
148
- triggerWorktreeRemoveHook: (worktreePath: string) => Promise<void>;
149
149
  }
150
150
 
151
151
  const ChatContext = createContext<ChatContextType | null>(null);
@@ -178,177 +178,21 @@ interface StreamingUpdateParams {
178
178
  * reasoning and text content alike. See docs/specs/core/stream-content-updates.md.
179
179
  * The clone must be at least one layer deep (message + blocks) so the in-place
180
180
  * block mutation never leaks into consumer state.
181
+ *
182
+ * IMPORTANT: the caller must invoke this EAGERLY at callback time and capture
183
+ * the result before scheduling any state update. React batches setState calls
184
+ * that happen in the same synchronous tick and runs the updater functions only
185
+ * at flush time — after the SDK has already written the first chunk into the
186
+ * shared message. A snapshot evaluated inside the updater (lazily) would copy
187
+ * the post-mutation blocks, and the first delta append would then double-count
188
+ * the first word ("HelloHello") whenever `onAssistantMessageAdded` and the
189
+ * first delta callback land in the same batch.
181
190
  */
182
191
  const snapshotMessage = (message: Message): Message => ({
183
192
  ...message,
184
193
  blocks: message.blocks.map((block) => ({ ...block })),
185
194
  });
186
195
 
187
- /**
188
- * Window-concat throttle for pure-delta streaming updates: chunks arriving
189
- * within the cooldown window are merged so no delta is lost (a dropped delta
190
- * would permanently lose content, unlike the accumulated-payload throttle it
191
- * replaces). Leading edge fires immediately; the trailing edge carries only
192
- * chunks that arrived within the window. `end` flushes any pending deltas
193
- * first, then forwards the end signal right away.
194
- */
195
- function createStreamingWindowThrottle(
196
- fn: (params: StreamingUpdateParams) => void,
197
- wait: number,
198
- ): {
199
- (params: StreamingUpdateParams): void;
200
- cancel: () => void;
201
- flush: () => void;
202
- } {
203
- let timer: NodeJS.Timeout | null = null;
204
- let pending: { messageId: string; chunk: string } | null = null;
205
-
206
- const fire = (stage: "streaming" | "end") => {
207
- if (pending) {
208
- fn({ ...pending, stage });
209
- pending = null;
210
- }
211
- };
212
-
213
- const throttled = (params: StreamingUpdateParams) => {
214
- if (params.stage === "end") {
215
- // Flush any deltas still pending inside the cooldown window first
216
- if (timer) {
217
- clearTimeout(timer);
218
- timer = null;
219
- }
220
- fire("streaming");
221
- fn(params);
222
- return;
223
- }
224
- if (pending) {
225
- pending.chunk += params.chunk;
226
- } else {
227
- pending = { messageId: params.messageId, chunk: params.chunk };
228
- }
229
- if (!timer) {
230
- // Leading edge: fire the current delta immediately, then reset pending so
231
- // the trailing edge only carries chunks arriving within this window
232
- fire("streaming");
233
- timer = setTimeout(() => {
234
- timer = null;
235
- fire("streaming");
236
- }, wait);
237
- }
238
- };
239
-
240
- throttled.cancel = () => {
241
- if (timer) {
242
- clearTimeout(timer);
243
- timer = null;
244
- }
245
- pending = null;
246
- };
247
-
248
- throttled.flush = () => {
249
- if (timer) {
250
- clearTimeout(timer);
251
- timer = null;
252
- }
253
- fire("streaming");
254
- };
255
-
256
- return throttled;
257
- }
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
-
352
196
  export const ChatProvider: React.FC<ChatProviderProps> = ({
353
197
  children,
354
198
  bypassPermissions,
@@ -379,157 +223,138 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
379
223
  const [latestTotalTokens, setLatestTotalTokens] = useState(0);
380
224
  const [maxInputTokens, setMaxInputTokens] = useState(200000);
381
225
 
382
- // Throttled incremental streaming updaters — 500ms window-concat, the same
383
- // interval as the pre-incremental throttledSetMessages. Chunks are pure
384
- // deltas: within-window chunks are merged so none is dropped, and
385
- // `stage === "end"` flushes pending deltas + applies the end signal
386
- // immediately so completion results are never delayed.
387
- const throttledContentUpdate = useMemo(
388
- () =>
389
- createStreamingWindowThrottle((params) => {
390
- const { messageId, chunk, stage } = params;
391
- setMessages((prev) =>
392
- prev.map((m) => {
393
- if (m.id !== messageId) return m;
394
- const textBlockIndex = m.blocks.findIndex((b) => b.type === "text");
395
- if (textBlockIndex === -1) {
396
- return {
397
- ...m,
398
- blocks: [...m.blocks, { type: "text", content: chunk, stage }],
399
- };
400
- }
401
- return {
402
- ...m,
403
- blocks: m.blocks.map((b, idx) =>
404
- idx === textBlockIndex && b.type === "text"
405
- ? {
406
- ...b,
407
- content: (b.content || "") + chunk,
408
- stage,
409
- }
410
- : b,
411
- ),
412
- };
413
- }),
414
- );
415
- }, 500),
416
- [],
417
- );
226
+ // Direct incremental streaming updaters — every delta is applied to the
227
+ // message state immediately, without a cooldown window. The former 500ms
228
+ // window-concat throttle is gone: its trailing-edge flush could interleave
229
+ // with a full-list `refreshMessages` snapshot replacement mid-stream and
230
+ // re-append old deltas on top of the authoritative content (first-word
231
+ // duplication). Terminal write frequency is bounded downstream by Ink's
232
+ // built-in ~30fps output throttle. See
233
+ // docs/specs/core/stream-content-updates.md.
234
+ const applyContentUpdate = useCallback((params: StreamingUpdateParams) => {
235
+ const { messageId, chunk, stage } = params;
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
+ }, []);
418
261
 
419
- const throttledReasoningUpdate = useMemo(
420
- () =>
421
- createStreamingWindowThrottle((params) => {
422
- const { messageId, chunk, stage } = params;
423
- setMessages((prev) =>
424
- prev.map((m) => {
425
- if (m.id !== messageId) return m;
426
- const reasoningBlockIndex = m.blocks.findIndex(
427
- (b) => b.type === "reasoning",
428
- );
429
- if (reasoningBlockIndex === -1) {
430
- return {
431
- ...m,
432
- blocks: [
433
- ...m.blocks,
434
- { type: "reasoning", content: chunk, stage },
435
- ],
436
- };
437
- }
438
- return {
439
- ...m,
440
- blocks: m.blocks.map((b, idx) =>
441
- idx === reasoningBlockIndex && b.type === "reasoning"
442
- ? {
443
- ...b,
444
- content: (b.content || "") + chunk,
445
- stage,
446
- }
447
- : b,
448
- ),
449
- };
450
- }),
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",
451
269
  );
452
- }, 500),
453
- [],
454
- );
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
+ }, []);
455
291
 
456
- const throttledToolBlockUpdate = useMemo(
457
- () =>
458
- createToolStreamingThrottle((params) => {
459
- const {
460
- messageId,
461
- id: toolBlockId,
462
- parametersChunk,
463
- ...updates
464
- } = params;
465
- setMessages((prev) =>
466
- prev.map((m) => {
467
- if (m.id !== messageId) return m;
468
- const toolBlockIndex = m.blocks.findIndex(
469
- (b) => b.type === "tool" && b.id === toolBlockId,
470
- );
471
- if (toolBlockIndex === -1) {
472
- return {
473
- ...m,
474
- blocks: [
475
- ...m.blocks,
476
- {
477
- type: "tool",
478
- id: toolBlockId,
479
- name: updates.name || "",
480
- stage: updates.stage || "start",
481
- parameters:
482
- (updates.parameters || "") + (parametersChunk || ""),
483
- result: updates.result || "",
484
- ...updates,
485
- },
486
- ],
487
- };
488
- }
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) {
489
307
  return {
490
308
  ...m,
491
- blocks: m.blocks.map((b, idx) =>
492
- idx === toolBlockIndex && b.type === "tool"
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
- }
505
- : b,
506
- ),
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
+ ],
507
322
  };
508
- }),
509
- );
510
- }, 500),
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
+ },
511
346
  [],
512
347
  );
513
348
 
514
349
  useEffect(() => {
515
350
  isExpandedRef.current = isExpanded;
516
- if (isExpanded) {
517
- // Cancel pending throttled updates so the frozen expanded view isn't overwritten
518
- throttledContentUpdate.cancel();
519
- throttledReasoningUpdate.cancel();
520
- throttledToolBlockUpdate.cancel();
521
- }
522
- }, [
523
- isExpanded,
524
- throttledContentUpdate,
525
- throttledReasoningUpdate,
526
- throttledToolBlockUpdate,
527
- ]);
351
+ }, [isExpanded]);
528
352
 
529
353
  const [isLoading, setIsLoading] = useState(false);
530
354
  const [sessionId, setSessionId] = useState("");
531
355
  const [isCommandRunning, setIsCommandRunning] = useState(false);
532
356
  const [isCompacting, setIsCompacting] = useState(false);
357
+ const [compactionStream, setCompactionStream] = useState("");
533
358
  const [currentModel, setCurrentModelState] = useState("");
534
359
  const [configuredModels, setConfiguredModels] = useState<string[]>([]);
535
360
  const [queuedMessages, setQueuedMessages] = useState<QueuedMessage[]>([]);
@@ -603,8 +428,8 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
603
428
  const [remountKey, setRemountKey] = useState(0);
604
429
 
605
430
  // Full terminal clear + remount so Ink's append-only <Static> re-renders.
606
- // Used on structural actions (/clear, /compact, rewind, ctrl-o, forceStatic
607
- // exit) where stale Static output must not linger on screen.
431
+ // Used on structural actions (/clear, /compact, rewind, ctrl-o) where stale
432
+ // Static output must not linger on screen.
608
433
  const forceRemount = useCallback(() => {
609
434
  stdout?.write("\u001b[2J\u001b[3J\u001b[0;0H", () => {
610
435
  setRemountKey((prev) => prev + 1);
@@ -671,53 +496,57 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
671
496
  const msgs = agentRef.current.messages;
672
497
  const last = msgs[msgs.length - 1];
673
498
  if (!last || last.role !== "user") return;
499
+ // Eager snapshot at callback time — React defers updater execution to
500
+ // the batch flush, so evaluating snapshotMessage inside setMessages
501
+ // would read the SDK message after its in-place mutations.
502
+ const snapshot = snapshotMessage(last);
674
503
  setMessages((prev) =>
675
- prev.some((m) => m.id === last.id)
676
- ? prev
677
- : [...prev, snapshotMessage(last)],
504
+ prev.some((m) => m.id === last.id) ? prev : [...prev, snapshot],
678
505
  );
679
506
  },
680
507
  onAssistantMessageAdded: (messageId: string) => {
681
508
  if (isExpandedRef.current || !agentRef.current) return;
682
509
  const msg = agentRef.current.messages.find((m) => m.id === messageId);
683
510
  if (!msg) return;
511
+ // Eager snapshot (see onUserMessageAdded): `addAssistantMessage()`
512
+ // fires this callback BEFORE the first delta writes into the shared
513
+ // message, so capturing here copies the pre-mutation (empty) blocks.
514
+ const snapshot = snapshotMessage(msg);
684
515
  setMessages((prev) =>
685
- prev.some((m) => m.id === messageId)
686
- ? prev
687
- : [...prev, snapshotMessage(msg)],
516
+ prev.some((m) => m.id === messageId) ? prev : [...prev, snapshot],
688
517
  );
689
518
  },
690
519
  onAssistantContentUpdated: (params) => {
691
520
  if (isExpandedRef.current) return;
692
- throttledContentUpdate(params);
521
+ applyContentUpdate(params);
693
522
  },
694
523
  onAssistantReasoningUpdated: (params) => {
695
524
  if (isExpandedRef.current) return;
696
- throttledReasoningUpdate(params);
525
+ applyReasoningUpdate(params);
697
526
  },
698
527
  onToolBlockUpdated: (params) => {
699
528
  if (isExpandedRef.current) return;
700
- throttledToolBlockUpdate(params);
701
- if (params.stage === "end") throttledToolBlockUpdate.flush();
529
+ applyToolBlockUpdate(params);
702
530
  },
703
531
  onErrorBlockAdded: (error: string) => {
704
532
  if (isExpandedRef.current) return;
705
533
  setMessages((prev) => {
706
- // Append to the last assistant message, or create one if none exists
707
- for (let i = prev.length - 1; i >= 0; i--) {
708
- if (prev[i].role === "assistant") {
709
- return prev.map((m, idx) =>
710
- idx === i
711
- ? {
712
- ...m,
713
- blocks: [
714
- ...m.blocks,
715
- { type: "error", content: error },
716
- ],
717
- }
718
- : m,
719
- );
720
- }
534
+ // Append to the LAST message only if it is an assistant message
535
+ // (the current turn's in-flight reply). If the last message is a
536
+ // user message, the error belongs BELOW it — create a new
537
+ // assistant message instead of polluting the stale assistant from
538
+ // an earlier turn (which surfaced the error above the latest
539
+ // user message and accumulated it there).
540
+ const last = prev[prev.length - 1];
541
+ if (last && last.role === "assistant") {
542
+ return prev.map((m, idx) =>
543
+ idx === prev.length - 1
544
+ ? {
545
+ ...m,
546
+ blocks: [...m.blocks, { type: "error", content: error }],
547
+ }
548
+ : m,
549
+ );
721
550
  }
722
551
  return [
723
552
  ...prev,
@@ -805,6 +634,14 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
805
634
  },
806
635
  onCompactionStateChange: (isCompactingState) => {
807
636
  setIsCompacting(isCompactingState);
637
+ if (!isCompactingState) {
638
+ setCompactionStream("");
639
+ }
640
+ },
641
+ onCompactionContentUpdate: (content) => {
642
+ // The SDK delivers the accumulated compaction text; the loading
643
+ // indicator renders only its last 30 characters (streaming tail).
644
+ setCompactionStream(content);
808
645
  },
809
646
  onBackgroundTasksChange: async (tasks) => {
810
647
  setBackgroundTasks([...tasks]);
@@ -954,9 +791,9 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
954
791
  originalCwd,
955
792
  model,
956
793
  initialPermissionMode,
957
- throttledContentUpdate,
958
- throttledReasoningUpdate,
959
- throttledToolBlockUpdate,
794
+ applyContentUpdate,
795
+ applyReasoningUpdate,
796
+ applyToolBlockUpdate,
960
797
  mcpServers,
961
798
  ],
962
799
  );
@@ -995,9 +832,6 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
995
832
  // Cleanup on unmount
996
833
  useEffect(() => {
997
834
  return () => {
998
- throttledContentUpdate.cancel();
999
- throttledReasoningUpdate.cancel();
1000
- throttledToolBlockUpdate.cancel();
1001
835
  if (agentRef.current) {
1002
836
  try {
1003
837
  // Display usage summary before cleanup
@@ -1011,19 +845,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
1011
845
  agentRef.current.destroy();
1012
846
  }
1013
847
  };
1014
- }, [
1015
- throttledContentUpdate,
1016
- throttledReasoningUpdate,
1017
- throttledToolBlockUpdate,
1018
- ]);
1019
-
1020
- // Trigger WorktreeRemove hook BEFORE agent destruction
1021
- const triggerWorktreeRemoveHook = useCallback(
1022
- async (worktreePath: string) => {
1023
- await agentRef.current?.triggerWorktreeRemoveHook(worktreePath);
1024
- },
1025
- [],
1026
- );
848
+ }, []);
1027
849
 
1028
850
  // Send message function (including judgment logic)
1029
851
  const sendMessage = useCallback(
@@ -1353,6 +1175,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
1353
1175
  getConfiguredModels,
1354
1176
  setModel,
1355
1177
  isCompacting,
1178
+ compactionStream,
1356
1179
  mcpServers: mcpServerStatuses,
1357
1180
  connectMcpServer,
1358
1181
  disconnectMcpServer,
@@ -1386,7 +1209,6 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
1386
1209
  version,
1387
1210
  workdir,
1388
1211
  recreateAgent,
1389
- triggerWorktreeRemoveHook,
1390
1212
  };
1391
1213
 
1392
1214
  return (