wave-code 1.0.10 → 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 (242) 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 +156 -335
  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/utils/clipboard.ts +21 -0
  31. package/src/utils/highlightUtils.ts +46 -1
  32. package/src/utils/readPackageJson.ts +29 -0
  33. package/src/utils/streamingText.ts +11 -0
  34. package/src/utils/worktree.ts +191 -43
  35. package/dist/cli.d.ts +0 -6
  36. package/dist/cli.js +0 -53
  37. package/dist/commands/plugin/disable.d.ts +0 -5
  38. package/dist/commands/plugin/disable.js +0 -15
  39. package/dist/commands/plugin/enable.d.ts +0 -5
  40. package/dist/commands/plugin/enable.js +0 -15
  41. package/dist/commands/plugin/install.d.ts +0 -5
  42. package/dist/commands/plugin/install.js +0 -21
  43. package/dist/commands/plugin/list.d.ts +0 -1
  44. package/dist/commands/plugin/list.js +0 -31
  45. package/dist/commands/plugin/marketplace.d.ts +0 -10
  46. package/dist/commands/plugin/marketplace.js +0 -90
  47. package/dist/commands/plugin/uninstall.d.ts +0 -3
  48. package/dist/commands/plugin/uninstall.js +0 -16
  49. package/dist/commands/plugin/update.d.ts +0 -3
  50. package/dist/commands/plugin/update.js +0 -15
  51. package/dist/commands/update.d.ts +0 -1
  52. package/dist/commands/update.js +0 -97
  53. package/dist/components/AgentsManager.d.ts +0 -7
  54. package/dist/components/AgentsManager.js +0 -109
  55. package/dist/components/App.d.ts +0 -9
  56. package/dist/components/App.js +0 -76
  57. package/dist/components/BackgroundTaskManager.d.ts +0 -5
  58. package/dist/components/BackgroundTaskManager.js +0 -99
  59. package/dist/components/BangDisplay.d.ts +0 -8
  60. package/dist/components/BangDisplay.js +0 -17
  61. package/dist/components/BtwDisplay.d.ts +0 -7
  62. package/dist/components/BtwDisplay.js +0 -19
  63. package/dist/components/ChatInterface.d.ts +0 -2
  64. package/dist/components/ChatInterface.js +0 -61
  65. package/dist/components/CommandSelector.d.ts +0 -10
  66. package/dist/components/CommandSelector.js +0 -66
  67. package/dist/components/CompactDisplay.d.ts +0 -8
  68. package/dist/components/CompactDisplay.js +0 -13
  69. package/dist/components/ConfirmationDetails.d.ts +0 -9
  70. package/dist/components/ConfirmationDetails.js +0 -45
  71. package/dist/components/ConfirmationSelector.d.ts +0 -13
  72. package/dist/components/ConfirmationSelector.js +0 -127
  73. package/dist/components/DiffDisplay.d.ts +0 -8
  74. package/dist/components/DiffDisplay.js +0 -201
  75. package/dist/components/DiscoverView.d.ts +0 -2
  76. package/dist/components/DiscoverView.js +0 -37
  77. package/dist/components/FileSelector.d.ts +0 -11
  78. package/dist/components/FileSelector.js +0 -50
  79. package/dist/components/HelpView.d.ts +0 -7
  80. package/dist/components/HelpView.js +0 -74
  81. package/dist/components/HistorySearch.d.ts +0 -8
  82. package/dist/components/HistorySearch.js +0 -66
  83. package/dist/components/InputBox.d.ts +0 -24
  84. package/dist/components/InputBox.js +0 -149
  85. package/dist/components/InstalledView.d.ts +0 -2
  86. package/dist/components/InstalledView.js +0 -45
  87. package/dist/components/LoadingIndicator.d.ts +0 -10
  88. package/dist/components/LoadingIndicator.js +0 -6
  89. package/dist/components/LoginCommand.d.ts +0 -5
  90. package/dist/components/LoginCommand.js +0 -153
  91. package/dist/components/Markdown.d.ts +0 -5
  92. package/dist/components/Markdown.js +0 -127
  93. package/dist/components/MarketplaceAddForm.d.ts +0 -2
  94. package/dist/components/MarketplaceAddForm.js +0 -55
  95. package/dist/components/MarketplaceDetail.d.ts +0 -2
  96. package/dist/components/MarketplaceDetail.js +0 -74
  97. package/dist/components/MarketplaceList.d.ts +0 -8
  98. package/dist/components/MarketplaceList.js +0 -17
  99. package/dist/components/MarketplaceView.d.ts +0 -2
  100. package/dist/components/MarketplaceView.js +0 -41
  101. package/dist/components/McpManager.d.ts +0 -9
  102. package/dist/components/McpManager.js +0 -88
  103. package/dist/components/MessageBlockItem.d.ts +0 -8
  104. package/dist/components/MessageBlockItem.js +0 -15
  105. package/dist/components/MessageList.d.ts +0 -10
  106. package/dist/components/MessageList.js +0 -80
  107. package/dist/components/ModelSelector.d.ts +0 -8
  108. package/dist/components/ModelSelector.js +0 -45
  109. package/dist/components/Notifications.d.ts +0 -7
  110. package/dist/components/Notifications.js +0 -9
  111. package/dist/components/PlanDisplay.d.ts +0 -7
  112. package/dist/components/PlanDisplay.js +0 -6
  113. package/dist/components/PluginDetail.d.ts +0 -2
  114. package/dist/components/PluginDetail.js +0 -87
  115. package/dist/components/PluginList.d.ts +0 -13
  116. package/dist/components/PluginList.js +0 -15
  117. package/dist/components/PluginManagerShell.d.ts +0 -6
  118. package/dist/components/PluginManagerShell.js +0 -92
  119. package/dist/components/PluginManagerTypes.d.ts +0 -35
  120. package/dist/components/PluginManagerTypes.js +0 -1
  121. package/dist/components/QueuedMessageList.d.ts +0 -2
  122. package/dist/components/QueuedMessageList.js +0 -18
  123. package/dist/components/ReasoningDisplay.d.ts +0 -8
  124. package/dist/components/ReasoningDisplay.js +0 -13
  125. package/dist/components/RewindCommand.d.ts +0 -12
  126. package/dist/components/RewindCommand.js +0 -78
  127. package/dist/components/SessionSelector.d.ts +0 -12
  128. package/dist/components/SessionSelector.js +0 -47
  129. package/dist/components/StatusCommand.d.ts +0 -5
  130. package/dist/components/StatusCommand.js +0 -28
  131. package/dist/components/StatusLine.d.ts +0 -6
  132. package/dist/components/StatusLine.js +0 -9
  133. package/dist/components/TaskList.d.ts +0 -5
  134. package/dist/components/TaskList.js +0 -197
  135. package/dist/components/TaskNotificationMessage.d.ts +0 -5
  136. package/dist/components/TaskNotificationMessage.js +0 -12
  137. package/dist/components/ToolDisplay.d.ts +0 -9
  138. package/dist/components/ToolDisplay.js +0 -55
  139. package/dist/components/WorkflowManager.d.ts +0 -5
  140. package/dist/components/WorkflowManager.js +0 -102
  141. package/dist/components/WorktreeExitPrompt.d.ts +0 -12
  142. package/dist/components/WorktreeExitPrompt.js +0 -35
  143. package/dist/constants/commands.d.ts +0 -2
  144. package/dist/constants/commands.js +0 -92
  145. package/dist/contexts/PluginManagerContext.d.ts +0 -3
  146. package/dist/contexts/PluginManagerContext.js +0 -9
  147. package/dist/contexts/useAppConfig.d.ts +0 -10
  148. package/dist/contexts/useAppConfig.js +0 -13
  149. package/dist/contexts/useChat.d.ts +0 -101
  150. package/dist/contexts/useChat.js +0 -993
  151. package/dist/contracts/status.d.ts +0 -7
  152. package/dist/contracts/status.js +0 -1
  153. package/dist/daemon/commands.d.ts +0 -49
  154. package/dist/daemon/commands.js +0 -341
  155. package/dist/daemon/jsonRpcClient.d.ts +0 -38
  156. package/dist/daemon/jsonRpcClient.js +0 -129
  157. package/dist/daemon/socketClient.d.ts +0 -13
  158. package/dist/daemon/socketClient.js +0 -26
  159. package/dist/daemon-cli.d.ts +0 -10
  160. package/dist/daemon-cli.js +0 -15
  161. package/dist/hooks/useInputManager.d.ts +0 -75
  162. package/dist/hooks/useInputManager.js +0 -583
  163. package/dist/hooks/usePluginManager.d.ts +0 -4
  164. package/dist/hooks/usePluginManager.js +0 -265
  165. package/dist/hooks/useTasks.d.ts +0 -1
  166. package/dist/hooks/useTasks.js +0 -5
  167. package/dist/index.d.ts +0 -4
  168. package/dist/index.js +0 -447
  169. package/dist/managers/inputHandlers.d.ts +0 -30
  170. package/dist/managers/inputHandlers.js +0 -625
  171. package/dist/managers/inputReducer.d.ts +0 -271
  172. package/dist/managers/inputReducer.js +0 -1121
  173. package/dist/plugin-manager-cli.d.ts +0 -5
  174. package/dist/plugin-manager-cli.js +0 -12
  175. package/dist/print-cli.d.ts +0 -10
  176. package/dist/print-cli.js +0 -209
  177. package/dist/reducers/agentsManagerReducer.d.ts +0 -26
  178. package/dist/reducers/agentsManagerReducer.js +0 -54
  179. package/dist/reducers/backgroundTaskManagerReducer.d.ts +0 -62
  180. package/dist/reducers/backgroundTaskManagerReducer.js +0 -106
  181. package/dist/reducers/confirmationReducer.d.ts +0 -37
  182. package/dist/reducers/confirmationReducer.js +0 -193
  183. package/dist/reducers/helpSelectorReducer.d.ts +0 -25
  184. package/dist/reducers/helpSelectorReducer.js +0 -52
  185. package/dist/reducers/marketplaceAddFormReducer.d.ts +0 -24
  186. package/dist/reducers/marketplaceAddFormReducer.js +0 -58
  187. package/dist/reducers/mcpManagerReducer.d.ts +0 -37
  188. package/dist/reducers/mcpManagerReducer.js +0 -74
  189. package/dist/reducers/pluginDetailReducer.d.ts +0 -32
  190. package/dist/reducers/pluginDetailReducer.js +0 -70
  191. package/dist/reducers/questionReducer.d.ts +0 -82
  192. package/dist/reducers/questionReducer.js +0 -375
  193. package/dist/reducers/rewindSelectorReducer.d.ts +0 -21
  194. package/dist/reducers/rewindSelectorReducer.js +0 -37
  195. package/dist/reducers/selectorReducer.d.ts +0 -23
  196. package/dist/reducers/selectorReducer.js +0 -42
  197. package/dist/reducers/workflowManagerReducer.d.ts +0 -40
  198. package/dist/reducers/workflowManagerReducer.js +0 -100
  199. package/dist/session-selector-cli.d.ts +0 -3
  200. package/dist/session-selector-cli.js +0 -25
  201. package/dist/stdio/agentBridge.d.ts +0 -139
  202. package/dist/stdio/agentBridge.js +0 -999
  203. package/dist/stdio/daemonServer.d.ts +0 -67
  204. package/dist/stdio/daemonServer.js +0 -191
  205. package/dist/stdio/index.d.ts +0 -5
  206. package/dist/stdio/index.js +0 -5
  207. package/dist/stdio/jsonRpcConnection.d.ts +0 -39
  208. package/dist/stdio/jsonRpcConnection.js +0 -135
  209. package/dist/stdio/protocol.d.ts +0 -41
  210. package/dist/stdio/protocol.js +0 -26
  211. package/dist/stdio/stdioServer.d.ts +0 -36
  212. package/dist/stdio/stdioServer.js +0 -51
  213. package/dist/stdio-cli.d.ts +0 -8
  214. package/dist/stdio-cli.js +0 -15
  215. package/dist/types.d.ts +0 -20
  216. package/dist/types.js +0 -1
  217. package/dist/utils/bracketedPaste.d.ts +0 -39
  218. package/dist/utils/bracketedPaste.js +0 -122
  219. package/dist/utils/clipboard.d.ts +0 -21
  220. package/dist/utils/clipboard.js +0 -347
  221. package/dist/utils/constants.d.ts +0 -16
  222. package/dist/utils/constants.js +0 -18
  223. package/dist/utils/highlightUtils.d.ts +0 -1
  224. package/dist/utils/highlightUtils.js +0 -93
  225. package/dist/utils/inputUtils.d.ts +0 -17
  226. package/dist/utils/inputUtils.js +0 -67
  227. package/dist/utils/logger.d.ts +0 -71
  228. package/dist/utils/logger.js +0 -259
  229. package/dist/utils/markdownTable.d.ts +0 -34
  230. package/dist/utils/markdownTable.js +0 -302
  231. package/dist/utils/rewindCheckpoints.d.ts +0 -8
  232. package/dist/utils/rewindCheckpoints.js +0 -17
  233. package/dist/utils/throttle.d.ts +0 -14
  234. package/dist/utils/throttle.js +0 -55
  235. package/dist/utils/toolParameterTransforms.d.ts +0 -18
  236. package/dist/utils/toolParameterTransforms.js +0 -73
  237. package/dist/utils/usageSummary.d.ts +0 -28
  238. package/dist/utils/usageSummary.js +0 -121
  239. package/dist/utils/version.d.ts +0 -1
  240. package/dist/utils/version.js +0 -4
  241. package/dist/utils/worktree.d.ts +0 -35
  242. 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,34 +496,37 @@ 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;
@@ -806,6 +634,14 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
806
634
  },
807
635
  onCompactionStateChange: (isCompactingState) => {
808
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);
809
645
  },
810
646
  onBackgroundTasksChange: async (tasks) => {
811
647
  setBackgroundTasks([...tasks]);
@@ -955,9 +791,9 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
955
791
  originalCwd,
956
792
  model,
957
793
  initialPermissionMode,
958
- throttledContentUpdate,
959
- throttledReasoningUpdate,
960
- throttledToolBlockUpdate,
794
+ applyContentUpdate,
795
+ applyReasoningUpdate,
796
+ applyToolBlockUpdate,
961
797
  mcpServers,
962
798
  ],
963
799
  );
@@ -996,9 +832,6 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
996
832
  // Cleanup on unmount
997
833
  useEffect(() => {
998
834
  return () => {
999
- throttledContentUpdate.cancel();
1000
- throttledReasoningUpdate.cancel();
1001
- throttledToolBlockUpdate.cancel();
1002
835
  if (agentRef.current) {
1003
836
  try {
1004
837
  // Display usage summary before cleanup
@@ -1012,19 +845,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
1012
845
  agentRef.current.destroy();
1013
846
  }
1014
847
  };
1015
- }, [
1016
- throttledContentUpdate,
1017
- throttledReasoningUpdate,
1018
- throttledToolBlockUpdate,
1019
- ]);
1020
-
1021
- // Trigger WorktreeRemove hook BEFORE agent destruction
1022
- const triggerWorktreeRemoveHook = useCallback(
1023
- async (worktreePath: string) => {
1024
- await agentRef.current?.triggerWorktreeRemoveHook(worktreePath);
1025
- },
1026
- [],
1027
- );
848
+ }, []);
1028
849
 
1029
850
  // Send message function (including judgment logic)
1030
851
  const sendMessage = useCallback(
@@ -1354,6 +1175,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
1354
1175
  getConfiguredModels,
1355
1176
  setModel,
1356
1177
  isCompacting,
1178
+ compactionStream,
1357
1179
  mcpServers: mcpServerStatuses,
1358
1180
  connectMcpServer,
1359
1181
  disconnectMcpServer,
@@ -1387,7 +1209,6 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
1387
1209
  version,
1388
1210
  workdir,
1389
1211
  recreateAgent,
1390
- triggerWorktreeRemoveHook,
1391
1212
  };
1392
1213
 
1393
1214
  return (