wave-code 1.0.10 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (248) hide show
  1. package/bin/wave-code.js +8 -2
  2. package/dist/bundle/wave.mjs +3231 -0
  3. package/package.json +24 -25
  4. package/src/cli.tsx +6 -1
  5. package/src/commands/update.ts +2 -7
  6. package/src/components/App.tsx +3 -7
  7. package/src/components/BtwDisplay.tsx +10 -1
  8. package/src/components/ChatInterface.tsx +21 -52
  9. package/src/components/ConfirmationDetails.tsx +144 -37
  10. package/src/components/DiffDisplay.tsx +335 -301
  11. package/src/components/InputBox.tsx +17 -2
  12. package/src/components/LoadingIndicator.tsx +16 -2
  13. package/src/components/MessageBlockItem.tsx +2 -4
  14. package/src/components/MessageList.tsx +75 -14
  15. package/src/components/ReasoningDisplay.tsx +2 -4
  16. package/src/components/SessionSelector.tsx +82 -11
  17. package/src/components/SkillsManager.tsx +312 -0
  18. package/src/components/StatusCommand.tsx +2 -9
  19. package/src/components/ToolDisplay.tsx +2 -4
  20. package/src/constants/commands.ts +6 -0
  21. package/src/contexts/useChat.tsx +245 -301
  22. package/src/daemon/commands.ts +57 -27
  23. package/src/daemon/jsonRpcClient.ts +1 -1
  24. package/src/daemon/socketClient.ts +2 -2
  25. package/src/hooks/useInputManager.ts +7 -1
  26. package/src/index.ts +57 -10
  27. package/src/managers/inputHandlers.ts +2 -0
  28. package/src/managers/inputReducer.ts +195 -104
  29. package/src/print-cli.ts +25 -17
  30. package/src/reducers/selectorReducer.ts +10 -0
  31. package/src/reducers/skillsManagerReducer.ts +91 -0
  32. package/src/session-selector-cli.tsx +269 -28
  33. package/src/stdio/agentBridge.ts +30 -21
  34. package/src/stdio/daemonServer.ts +3 -1
  35. package/src/stdio/protocol.ts +2 -0
  36. package/src/utils/clipboard.ts +21 -0
  37. package/src/utils/highlightUtils.ts +46 -1
  38. package/src/utils/readPackageJson.ts +29 -0
  39. package/src/utils/streamingText.ts +11 -0
  40. package/src/utils/worktree.ts +191 -43
  41. package/dist/cli.d.ts +0 -6
  42. package/dist/cli.js +0 -53
  43. package/dist/commands/plugin/disable.d.ts +0 -5
  44. package/dist/commands/plugin/disable.js +0 -15
  45. package/dist/commands/plugin/enable.d.ts +0 -5
  46. package/dist/commands/plugin/enable.js +0 -15
  47. package/dist/commands/plugin/install.d.ts +0 -5
  48. package/dist/commands/plugin/install.js +0 -21
  49. package/dist/commands/plugin/list.d.ts +0 -1
  50. package/dist/commands/plugin/list.js +0 -31
  51. package/dist/commands/plugin/marketplace.d.ts +0 -10
  52. package/dist/commands/plugin/marketplace.js +0 -90
  53. package/dist/commands/plugin/uninstall.d.ts +0 -3
  54. package/dist/commands/plugin/uninstall.js +0 -16
  55. package/dist/commands/plugin/update.d.ts +0 -3
  56. package/dist/commands/plugin/update.js +0 -15
  57. package/dist/commands/update.d.ts +0 -1
  58. package/dist/commands/update.js +0 -97
  59. package/dist/components/AgentsManager.d.ts +0 -7
  60. package/dist/components/AgentsManager.js +0 -109
  61. package/dist/components/App.d.ts +0 -9
  62. package/dist/components/App.js +0 -76
  63. package/dist/components/BackgroundTaskManager.d.ts +0 -5
  64. package/dist/components/BackgroundTaskManager.js +0 -99
  65. package/dist/components/BangDisplay.d.ts +0 -8
  66. package/dist/components/BangDisplay.js +0 -17
  67. package/dist/components/BtwDisplay.d.ts +0 -7
  68. package/dist/components/BtwDisplay.js +0 -19
  69. package/dist/components/ChatInterface.d.ts +0 -2
  70. package/dist/components/ChatInterface.js +0 -61
  71. package/dist/components/CommandSelector.d.ts +0 -10
  72. package/dist/components/CommandSelector.js +0 -66
  73. package/dist/components/CompactDisplay.d.ts +0 -8
  74. package/dist/components/CompactDisplay.js +0 -13
  75. package/dist/components/ConfirmationDetails.d.ts +0 -9
  76. package/dist/components/ConfirmationDetails.js +0 -45
  77. package/dist/components/ConfirmationSelector.d.ts +0 -13
  78. package/dist/components/ConfirmationSelector.js +0 -127
  79. package/dist/components/DiffDisplay.d.ts +0 -8
  80. package/dist/components/DiffDisplay.js +0 -201
  81. package/dist/components/DiscoverView.d.ts +0 -2
  82. package/dist/components/DiscoverView.js +0 -37
  83. package/dist/components/FileSelector.d.ts +0 -11
  84. package/dist/components/FileSelector.js +0 -50
  85. package/dist/components/HelpView.d.ts +0 -7
  86. package/dist/components/HelpView.js +0 -74
  87. package/dist/components/HistorySearch.d.ts +0 -8
  88. package/dist/components/HistorySearch.js +0 -66
  89. package/dist/components/InputBox.d.ts +0 -24
  90. package/dist/components/InputBox.js +0 -149
  91. package/dist/components/InstalledView.d.ts +0 -2
  92. package/dist/components/InstalledView.js +0 -45
  93. package/dist/components/LoadingIndicator.d.ts +0 -10
  94. package/dist/components/LoadingIndicator.js +0 -6
  95. package/dist/components/LoginCommand.d.ts +0 -5
  96. package/dist/components/LoginCommand.js +0 -153
  97. package/dist/components/Markdown.d.ts +0 -5
  98. package/dist/components/Markdown.js +0 -127
  99. package/dist/components/MarketplaceAddForm.d.ts +0 -2
  100. package/dist/components/MarketplaceAddForm.js +0 -55
  101. package/dist/components/MarketplaceDetail.d.ts +0 -2
  102. package/dist/components/MarketplaceDetail.js +0 -74
  103. package/dist/components/MarketplaceList.d.ts +0 -8
  104. package/dist/components/MarketplaceList.js +0 -17
  105. package/dist/components/MarketplaceView.d.ts +0 -2
  106. package/dist/components/MarketplaceView.js +0 -41
  107. package/dist/components/McpManager.d.ts +0 -9
  108. package/dist/components/McpManager.js +0 -88
  109. package/dist/components/MessageBlockItem.d.ts +0 -8
  110. package/dist/components/MessageBlockItem.js +0 -15
  111. package/dist/components/MessageList.d.ts +0 -10
  112. package/dist/components/MessageList.js +0 -80
  113. package/dist/components/ModelSelector.d.ts +0 -8
  114. package/dist/components/ModelSelector.js +0 -45
  115. package/dist/components/Notifications.d.ts +0 -7
  116. package/dist/components/Notifications.js +0 -9
  117. package/dist/components/PlanDisplay.d.ts +0 -7
  118. package/dist/components/PlanDisplay.js +0 -6
  119. package/dist/components/PluginDetail.d.ts +0 -2
  120. package/dist/components/PluginDetail.js +0 -87
  121. package/dist/components/PluginList.d.ts +0 -13
  122. package/dist/components/PluginList.js +0 -15
  123. package/dist/components/PluginManagerShell.d.ts +0 -6
  124. package/dist/components/PluginManagerShell.js +0 -92
  125. package/dist/components/PluginManagerTypes.d.ts +0 -35
  126. package/dist/components/PluginManagerTypes.js +0 -1
  127. package/dist/components/QueuedMessageList.d.ts +0 -2
  128. package/dist/components/QueuedMessageList.js +0 -18
  129. package/dist/components/ReasoningDisplay.d.ts +0 -8
  130. package/dist/components/ReasoningDisplay.js +0 -13
  131. package/dist/components/RewindCommand.d.ts +0 -12
  132. package/dist/components/RewindCommand.js +0 -78
  133. package/dist/components/SessionSelector.d.ts +0 -12
  134. package/dist/components/SessionSelector.js +0 -47
  135. package/dist/components/StatusCommand.d.ts +0 -5
  136. package/dist/components/StatusCommand.js +0 -28
  137. package/dist/components/StatusLine.d.ts +0 -6
  138. package/dist/components/StatusLine.js +0 -9
  139. package/dist/components/TaskList.d.ts +0 -5
  140. package/dist/components/TaskList.js +0 -197
  141. package/dist/components/TaskNotificationMessage.d.ts +0 -5
  142. package/dist/components/TaskNotificationMessage.js +0 -12
  143. package/dist/components/ToolDisplay.d.ts +0 -9
  144. package/dist/components/ToolDisplay.js +0 -55
  145. package/dist/components/WorkflowManager.d.ts +0 -5
  146. package/dist/components/WorkflowManager.js +0 -102
  147. package/dist/components/WorktreeExitPrompt.d.ts +0 -12
  148. package/dist/components/WorktreeExitPrompt.js +0 -35
  149. package/dist/constants/commands.d.ts +0 -2
  150. package/dist/constants/commands.js +0 -92
  151. package/dist/contexts/PluginManagerContext.d.ts +0 -3
  152. package/dist/contexts/PluginManagerContext.js +0 -9
  153. package/dist/contexts/useAppConfig.d.ts +0 -10
  154. package/dist/contexts/useAppConfig.js +0 -13
  155. package/dist/contexts/useChat.d.ts +0 -101
  156. package/dist/contexts/useChat.js +0 -993
  157. package/dist/contracts/status.d.ts +0 -7
  158. package/dist/contracts/status.js +0 -1
  159. package/dist/daemon/commands.d.ts +0 -49
  160. package/dist/daemon/commands.js +0 -341
  161. package/dist/daemon/jsonRpcClient.d.ts +0 -38
  162. package/dist/daemon/jsonRpcClient.js +0 -129
  163. package/dist/daemon/socketClient.d.ts +0 -13
  164. package/dist/daemon/socketClient.js +0 -26
  165. package/dist/daemon-cli.d.ts +0 -10
  166. package/dist/daemon-cli.js +0 -15
  167. package/dist/hooks/useInputManager.d.ts +0 -75
  168. package/dist/hooks/useInputManager.js +0 -583
  169. package/dist/hooks/usePluginManager.d.ts +0 -4
  170. package/dist/hooks/usePluginManager.js +0 -265
  171. package/dist/hooks/useTasks.d.ts +0 -1
  172. package/dist/hooks/useTasks.js +0 -5
  173. package/dist/index.d.ts +0 -4
  174. package/dist/index.js +0 -447
  175. package/dist/managers/inputHandlers.d.ts +0 -30
  176. package/dist/managers/inputHandlers.js +0 -625
  177. package/dist/managers/inputReducer.d.ts +0 -271
  178. package/dist/managers/inputReducer.js +0 -1121
  179. package/dist/plugin-manager-cli.d.ts +0 -5
  180. package/dist/plugin-manager-cli.js +0 -12
  181. package/dist/print-cli.d.ts +0 -10
  182. package/dist/print-cli.js +0 -209
  183. package/dist/reducers/agentsManagerReducer.d.ts +0 -26
  184. package/dist/reducers/agentsManagerReducer.js +0 -54
  185. package/dist/reducers/backgroundTaskManagerReducer.d.ts +0 -62
  186. package/dist/reducers/backgroundTaskManagerReducer.js +0 -106
  187. package/dist/reducers/confirmationReducer.d.ts +0 -37
  188. package/dist/reducers/confirmationReducer.js +0 -193
  189. package/dist/reducers/helpSelectorReducer.d.ts +0 -25
  190. package/dist/reducers/helpSelectorReducer.js +0 -52
  191. package/dist/reducers/marketplaceAddFormReducer.d.ts +0 -24
  192. package/dist/reducers/marketplaceAddFormReducer.js +0 -58
  193. package/dist/reducers/mcpManagerReducer.d.ts +0 -37
  194. package/dist/reducers/mcpManagerReducer.js +0 -74
  195. package/dist/reducers/pluginDetailReducer.d.ts +0 -32
  196. package/dist/reducers/pluginDetailReducer.js +0 -70
  197. package/dist/reducers/questionReducer.d.ts +0 -82
  198. package/dist/reducers/questionReducer.js +0 -375
  199. package/dist/reducers/rewindSelectorReducer.d.ts +0 -21
  200. package/dist/reducers/rewindSelectorReducer.js +0 -37
  201. package/dist/reducers/selectorReducer.d.ts +0 -23
  202. package/dist/reducers/selectorReducer.js +0 -42
  203. package/dist/reducers/workflowManagerReducer.d.ts +0 -40
  204. package/dist/reducers/workflowManagerReducer.js +0 -100
  205. package/dist/session-selector-cli.d.ts +0 -3
  206. package/dist/session-selector-cli.js +0 -25
  207. package/dist/stdio/agentBridge.d.ts +0 -139
  208. package/dist/stdio/agentBridge.js +0 -999
  209. package/dist/stdio/daemonServer.d.ts +0 -67
  210. package/dist/stdio/daemonServer.js +0 -191
  211. package/dist/stdio/index.d.ts +0 -5
  212. package/dist/stdio/index.js +0 -5
  213. package/dist/stdio/jsonRpcConnection.d.ts +0 -39
  214. package/dist/stdio/jsonRpcConnection.js +0 -135
  215. package/dist/stdio/protocol.d.ts +0 -41
  216. package/dist/stdio/protocol.js +0 -26
  217. package/dist/stdio/stdioServer.d.ts +0 -36
  218. package/dist/stdio/stdioServer.js +0 -51
  219. package/dist/stdio-cli.d.ts +0 -8
  220. package/dist/stdio-cli.js +0 -15
  221. package/dist/types.d.ts +0 -20
  222. package/dist/types.js +0 -1
  223. package/dist/utils/bracketedPaste.d.ts +0 -39
  224. package/dist/utils/bracketedPaste.js +0 -122
  225. package/dist/utils/clipboard.d.ts +0 -21
  226. package/dist/utils/clipboard.js +0 -347
  227. package/dist/utils/constants.d.ts +0 -16
  228. package/dist/utils/constants.js +0 -18
  229. package/dist/utils/highlightUtils.d.ts +0 -1
  230. package/dist/utils/highlightUtils.js +0 -93
  231. package/dist/utils/inputUtils.d.ts +0 -17
  232. package/dist/utils/inputUtils.js +0 -67
  233. package/dist/utils/logger.d.ts +0 -71
  234. package/dist/utils/logger.js +0 -259
  235. package/dist/utils/markdownTable.d.ts +0 -34
  236. package/dist/utils/markdownTable.js +0 -302
  237. package/dist/utils/rewindCheckpoints.d.ts +0 -8
  238. package/dist/utils/rewindCheckpoints.js +0 -17
  239. package/dist/utils/throttle.d.ts +0 -14
  240. package/dist/utils/throttle.js +0 -55
  241. package/dist/utils/toolParameterTransforms.d.ts +0 -18
  242. package/dist/utils/toolParameterTransforms.js +0 -73
  243. package/dist/utils/usageSummary.d.ts +0 -28
  244. package/dist/utils/usageSummary.js +0 -121
  245. package/dist/utils/version.d.ts +0 -1
  246. package/dist/utils/version.js +0 -4
  247. package/dist/utils/worktree.d.ts +0 -35
  248. package/dist/utils/worktree.js +0 -219
@@ -16,6 +16,7 @@ import type {
16
16
  Task,
17
17
  SlashCommand,
18
18
  SubagentConfiguration,
19
+ SkillMetadata,
19
20
  PermissionDecision,
20
21
  PermissionMode,
21
22
  QueuedMessage,
@@ -42,6 +43,9 @@ export interface ChatContextType {
42
43
  isLoading: boolean;
43
44
  isCommandRunning: boolean;
44
45
  isCompacting: boolean;
46
+ /** Accumulated streaming text from the compaction fork, shown as the
47
+ * last-30-chars tail on the CLI compacting loading indicator. */
48
+ compactionStream: string;
45
49
  // Message display state
46
50
  isExpanded: boolean;
47
51
  isTaskListVisible: boolean;
@@ -98,6 +102,8 @@ export interface ChatContextType {
98
102
  hasSlashCommand: (commandId: string) => boolean;
99
103
  // Agent definitions (for /agents overlay)
100
104
  agentDefinitions: SubagentConfiguration[];
105
+ // Skill metadata (for /skills overlay)
106
+ skills: SkillMetadata[];
101
107
  // Permission functionality
102
108
  permissionMode: PermissionMode;
103
109
  setPermissionMode: (mode: PermissionMode) => void;
@@ -144,8 +150,6 @@ export interface ChatContextType {
144
150
  workdir?: string;
145
151
  // Agent recreation (e.g. after plugin install)
146
152
  recreateAgent: () => void;
147
- // Trigger WorktreeRemove hook BEFORE agent destruction
148
- triggerWorktreeRemoveHook: (worktreePath: string) => Promise<void>;
149
153
  }
150
154
 
151
155
  const ChatContext = createContext<ChatContextType | null>(null);
@@ -178,6 +182,15 @@ interface StreamingUpdateParams {
178
182
  * reasoning and text content alike. See docs/specs/core/stream-content-updates.md.
179
183
  * The clone must be at least one layer deep (message + blocks) so the in-place
180
184
  * block mutation never leaks into consumer state.
185
+ *
186
+ * IMPORTANT: the caller must invoke this EAGERLY at callback time and capture
187
+ * the result before scheduling any state update. React batches setState calls
188
+ * that happen in the same synchronous tick and runs the updater functions only
189
+ * at flush time — after the SDK has already written the first chunk into the
190
+ * shared message. A snapshot evaluated inside the updater (lazily) would copy
191
+ * the post-mutation blocks, and the first delta append would then double-count
192
+ * the first word ("HelloHello") whenever `onAssistantMessageAdded` and the
193
+ * first delta callback land in the same batch.
181
194
  */
182
195
  const snapshotMessage = (message: Message): Message => ({
183
196
  ...message,
@@ -185,149 +198,178 @@ const snapshotMessage = (message: Message): Message => ({
185
198
  });
186
199
 
187
200
  /**
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.
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.
194
203
  */
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);
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
+ };
237
217
  }
238
- };
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
+ };
239
232
 
240
- throttled.cancel = () => {
241
- if (timer) {
242
- clearTimeout(timer);
243
- timer = null;
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
+ };
244
252
  }
245
- pending = null;
246
- };
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
+ };
247
267
 
248
- throttled.flush = () => {
249
- if (timer) {
250
- clearTimeout(timer);
251
- timer = null;
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
+ };
252
299
  }
253
- fire("streaming");
254
- };
255
-
256
- return throttled;
257
- }
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
+ };
258
318
 
259
319
  /**
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.
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.
267
340
  */
268
- export function createToolStreamingThrottle(
269
- fn: (params: ToolBlockUpdateCallbackParams) => void,
341
+ export function createThrottledUpdater<T>(
342
+ apply: (updater: (prev: T) => T) => void,
270
343
  wait: number,
271
344
  ): {
272
- (params: ToolBlockUpdateCallbackParams): void;
345
+ (updater: (prev: T) => T): void;
273
346
  cancel: () => void;
274
347
  flush: () => void;
275
348
  } {
276
349
  let timer: NodeJS.Timeout | null = null;
277
- let pending: { messageId: string; chunks: Map<string, string> } | null = null;
350
+ let queued: Array<(prev: T) => T> = [];
278
351
 
279
352
  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
- }
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));
287
359
  };
288
360
 
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
- }
361
+ const throttled = (updater: (prev: T) => T) => {
362
+ if (timer) {
363
+ queued.push(updater);
312
364
  return;
313
365
  }
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);
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);
331
373
  };
332
374
 
333
375
  throttled.cancel = () => {
@@ -335,7 +377,7 @@ export function createToolStreamingThrottle(
335
377
  clearTimeout(timer);
336
378
  timer = null;
337
379
  }
338
- pending = null;
380
+ queued = [];
339
381
  };
340
382
 
341
383
  throttled.flush = () => {
@@ -379,135 +421,16 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
379
421
  const [latestTotalTokens, setLatestTotalTokens] = useState(0);
380
422
  const [maxInputTokens, setMaxInputTokens] = useState(200000);
381
423
 
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(
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(
388
432
  () =>
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
- );
418
-
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
- }),
451
- );
452
- }, 500),
453
- [],
454
- );
455
-
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
- }
489
- return {
490
- ...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
- ),
507
- };
508
- }),
509
- );
510
- }, 500),
433
+ createThrottledUpdater<Message[]>((updater) => setMessages(updater), 500),
511
434
  [],
512
435
  );
513
436
 
@@ -515,21 +438,15 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
515
438
  isExpandedRef.current = isExpanded;
516
439
  if (isExpanded) {
517
440
  // Cancel pending throttled updates so the frozen expanded view isn't overwritten
518
- throttledContentUpdate.cancel();
519
- throttledReasoningUpdate.cancel();
520
- throttledToolBlockUpdate.cancel();
441
+ updateMessages.cancel();
521
442
  }
522
- }, [
523
- isExpanded,
524
- throttledContentUpdate,
525
- throttledReasoningUpdate,
526
- throttledToolBlockUpdate,
527
- ]);
443
+ }, [isExpanded, updateMessages]);
528
444
 
529
445
  const [isLoading, setIsLoading] = useState(false);
530
446
  const [sessionId, setSessionId] = useState("");
531
447
  const [isCommandRunning, setIsCommandRunning] = useState(false);
532
448
  const [isCompacting, setIsCompacting] = useState(false);
449
+ const [compactionStream, setCompactionStream] = useState("");
533
450
  const [currentModel, setCurrentModelState] = useState("");
534
451
  const [configuredModels, setConfiguredModels] = useState<string[]>([]);
535
452
  const [queuedMessages, setQueuedMessages] = useState<QueuedMessage[]>([]);
@@ -553,6 +470,8 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
553
470
  const [agentDefinitions, setAgentDefinitions] = useState<
554
471
  SubagentConfiguration[]
555
472
  >([]);
473
+ // Skill metadata (for /skills overlay)
474
+ const [skills, setSkills] = useState<SkillMetadata[]>([]);
556
475
 
557
476
  // Permission state
558
477
  const [permissionMode, setPermissionModeState] = useState<PermissionMode>(
@@ -603,8 +522,8 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
603
522
  const [remountKey, setRemountKey] = useState(0);
604
523
 
605
524
  // 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.
525
+ // Used on structural actions (/clear, /compact, rewind, ctrl-o) where stale
526
+ // Static output must not linger on screen.
608
527
  const forceRemount = useCallback(() => {
609
528
  stdout?.write("\u001b[2J\u001b[3J\u001b[0;0H", () => {
610
529
  setRemountKey((prev) => prev + 1);
@@ -622,10 +541,19 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
622
541
  const refreshMessages = useCallback(() => {
623
542
  if (!isExpandedRef.current && agentRef.current) {
624
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();
625
553
  setMessages(msgs);
626
554
  setLatestTotalTokens(extractLatestTotalTokens(msgs));
627
555
  }
628
- }, []);
556
+ }, [updateMessages]);
629
557
 
630
558
  // Permission confirmation methods with queue support
631
559
  const showConfirmation = useCallback(
@@ -666,43 +594,55 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
666
594
 
667
595
  const callbacks: AgentCallbacks = {
668
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.
669
599
  onUserMessageAdded: () => {
670
600
  if (isExpandedRef.current || !agentRef.current) return;
671
601
  const msgs = agentRef.current.messages;
672
602
  const last = msgs[msgs.length - 1];
673
603
  if (!last || last.role !== "user") return;
674
- setMessages((prev) =>
675
- prev.some((m) => m.id === last.id)
676
- ? prev
677
- : [...prev, snapshotMessage(last)],
604
+ // Eager snapshot at callback time — React defers updater execution to
605
+ // the batch flush, so evaluating snapshotMessage inside the updater
606
+ // would read the SDK message after its in-place mutations.
607
+ const snapshot = snapshotMessage(last);
608
+ updateMessages((prev) =>
609
+ prev.some((m) => m.id === last.id) ? prev : [...prev, snapshot],
678
610
  );
611
+ // One-shot structural update — flush immediately so the message
612
+ // card appears at once (queuing it gains no coalescing).
613
+ updateMessages.flush();
679
614
  },
680
615
  onAssistantMessageAdded: (messageId: string) => {
681
616
  if (isExpandedRef.current || !agentRef.current) return;
682
617
  const msg = agentRef.current.messages.find((m) => m.id === messageId);
683
618
  if (!msg) return;
684
- setMessages((prev) =>
685
- prev.some((m) => m.id === messageId)
686
- ? prev
687
- : [...prev, snapshotMessage(msg)],
619
+ // Eager snapshot (see onUserMessageAdded): `addAssistantMessage()`
620
+ // fires this callback BEFORE the first delta writes into the shared
621
+ // message, so capturing here copies the pre-mutation (empty) blocks.
622
+ const snapshot = snapshotMessage(msg);
623
+ updateMessages((prev) =>
624
+ prev.some((m) => m.id === messageId) ? prev : [...prev, snapshot],
688
625
  );
626
+ updateMessages.flush();
689
627
  },
690
628
  onAssistantContentUpdated: (params) => {
691
629
  if (isExpandedRef.current) return;
692
- throttledContentUpdate(params);
630
+ updateMessages((prev) => applyContentDelta(prev, params));
631
+ if (params.stage === "end") updateMessages.flush();
693
632
  },
694
633
  onAssistantReasoningUpdated: (params) => {
695
634
  if (isExpandedRef.current) return;
696
- throttledReasoningUpdate(params);
635
+ updateMessages((prev) => applyReasoningDelta(prev, params));
636
+ if (params.stage === "end") updateMessages.flush();
697
637
  },
698
638
  onToolBlockUpdated: (params) => {
699
639
  if (isExpandedRef.current) return;
700
- throttledToolBlockUpdate(params);
701
- if (params.stage === "end") throttledToolBlockUpdate.flush();
640
+ updateMessages((prev) => applyToolBlockUpdate(prev, params));
641
+ if (params.stage === "end") updateMessages.flush();
702
642
  },
703
643
  onErrorBlockAdded: (error: string) => {
704
644
  if (isExpandedRef.current) return;
705
- setMessages((prev) => {
645
+ updateMessages((prev) => {
706
646
  // Append to the LAST message only if it is an assistant message
707
647
  // (the current turn's in-flight reply). If the last message is a
708
648
  // user message, the error belongs BELOW it — create a new
@@ -730,10 +670,13 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
730
670
  },
731
671
  ];
732
672
  });
673
+ // One-shot structural update — flush immediately (errors must not be
674
+ // delayed by a streaming window).
675
+ updateMessages.flush();
733
676
  },
734
677
  onAddBangMessage: (command, messageId) => {
735
678
  if (isExpandedRef.current) return;
736
- setMessages((prev) =>
679
+ updateMessages((prev) =>
737
680
  prev.some((m) => m.id === messageId)
738
681
  ? prev
739
682
  : [
@@ -754,10 +697,11 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
754
697
  },
755
698
  ],
756
699
  );
700
+ updateMessages.flush();
757
701
  },
758
702
  onUpdateBangMessage: (command, output, messageId) => {
759
703
  if (isExpandedRef.current) return;
760
- setMessages((prev) =>
704
+ updateMessages((prev) =>
761
705
  prev.map((m) =>
762
706
  m.id === messageId
763
707
  ? {
@@ -774,7 +718,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
774
718
  },
775
719
  onCompleteBangMessage: (command, exitCode, messageId, output) => {
776
720
  if (isExpandedRef.current) return;
777
- setMessages((prev) =>
721
+ updateMessages((prev) =>
778
722
  prev.map((m) =>
779
723
  m.id === messageId
780
724
  ? {
@@ -794,6 +738,8 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
794
738
  : m,
795
739
  ),
796
740
  );
741
+ // Completion signal — flush so the final state applies immediately.
742
+ updateMessages.flush();
797
743
  },
798
744
  onLatestTotalTokensChange: (tokens) => {
799
745
  setLatestTotalTokens(tokens);
@@ -806,6 +752,14 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
806
752
  },
807
753
  onCompactionStateChange: (isCompactingState) => {
808
754
  setIsCompacting(isCompactingState);
755
+ if (!isCompactingState) {
756
+ setCompactionStream("");
757
+ }
758
+ },
759
+ onCompactionContentUpdate: (content) => {
760
+ // The SDK delivers the accumulated compaction text; the loading
761
+ // indicator renders only its last 30 characters (streaming tail).
762
+ setCompactionStream(content);
809
763
  },
810
764
  onBackgroundTasksChange: async (tasks) => {
811
765
  setBackgroundTasks([...tasks]);
@@ -936,6 +890,10 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
936
890
  const initialAgentDefinitions =
937
891
  agent.getSubagentConfigurations?.() || [];
938
892
  setAgentDefinitions(initialAgentDefinitions);
893
+
894
+ // Get initial skill metadata
895
+ const initialSkills = agent.getSkillMetadata?.() || [];
896
+ setSkills(initialSkills);
939
897
  } catch (error) {
940
898
  console.error("Failed to initialize AI manager:", error);
941
899
  }
@@ -955,9 +913,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
955
913
  originalCwd,
956
914
  model,
957
915
  initialPermissionMode,
958
- throttledContentUpdate,
959
- throttledReasoningUpdate,
960
- throttledToolBlockUpdate,
916
+ updateMessages,
961
917
  mcpServers,
962
918
  ],
963
919
  );
@@ -977,6 +933,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
977
933
  setMcpServerStatuses([]);
978
934
  setSlashCommands([]);
979
935
  setAgentDefinitions([]);
936
+ setSkills([]);
980
937
  setSessionId("");
981
938
  setIsLoading(false);
982
939
  setLatestTotalTokens(0);
@@ -996,9 +953,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
996
953
  // Cleanup on unmount
997
954
  useEffect(() => {
998
955
  return () => {
999
- throttledContentUpdate.cancel();
1000
- throttledReasoningUpdate.cancel();
1001
- throttledToolBlockUpdate.cancel();
956
+ updateMessages.cancel();
1002
957
  if (agentRef.current) {
1003
958
  try {
1004
959
  // Display usage summary before cleanup
@@ -1012,19 +967,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
1012
967
  agentRef.current.destroy();
1013
968
  }
1014
969
  };
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
- );
970
+ }, [updateMessages]);
1028
971
 
1029
972
  // Send message function (including judgment logic)
1030
973
  const sendMessage = useCallback(
@@ -1354,6 +1297,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
1354
1297
  getConfiguredModels,
1355
1298
  setModel,
1356
1299
  isCompacting,
1300
+ compactionStream,
1357
1301
  mcpServers: mcpServerStatuses,
1358
1302
  connectMcpServer,
1359
1303
  disconnectMcpServer,
@@ -1366,6 +1310,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
1366
1310
  slashCommands,
1367
1311
  hasSlashCommand,
1368
1312
  agentDefinitions,
1313
+ skills,
1369
1314
  permissionMode,
1370
1315
  setPermissionMode,
1371
1316
  isConfirmationVisible,
@@ -1387,7 +1332,6 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
1387
1332
  version,
1388
1333
  workdir,
1389
1334
  recreateAgent,
1390
- triggerWorktreeRemoveHook,
1391
1335
  };
1392
1336
 
1393
1337
  return (