wave-code 1.0.4 → 1.0.6

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.
@@ -15,6 +15,66 @@ export const useChat = () => {
15
15
  }
16
16
  return context;
17
17
  };
18
+ /**
19
+ * Window-concat throttle for pure-delta streaming updates: chunks arriving
20
+ * within the cooldown window are merged so no delta is lost (a dropped delta
21
+ * would permanently lose content, unlike the accumulated-payload throttle it
22
+ * replaces). Leading edge fires immediately; the trailing edge carries only
23
+ * chunks that arrived within the window. `end` flushes any pending deltas
24
+ * first, then forwards the end signal right away.
25
+ */
26
+ function createStreamingWindowThrottle(fn, wait) {
27
+ let timer = null;
28
+ let pending = null;
29
+ const fire = (stage) => {
30
+ if (pending) {
31
+ fn({ ...pending, stage });
32
+ pending = null;
33
+ }
34
+ };
35
+ const throttled = (params) => {
36
+ if (params.stage === "end") {
37
+ // Flush any deltas still pending inside the cooldown window first
38
+ if (timer) {
39
+ clearTimeout(timer);
40
+ timer = null;
41
+ }
42
+ fire("streaming");
43
+ fn(params);
44
+ return;
45
+ }
46
+ if (pending) {
47
+ pending.chunk += params.chunk;
48
+ }
49
+ else {
50
+ pending = { messageId: params.messageId, chunk: params.chunk };
51
+ }
52
+ if (!timer) {
53
+ // Leading edge: fire the current delta immediately, then reset pending so
54
+ // the trailing edge only carries chunks arriving within this window
55
+ fire("streaming");
56
+ timer = setTimeout(() => {
57
+ timer = null;
58
+ fire("streaming");
59
+ }, wait);
60
+ }
61
+ };
62
+ throttled.cancel = () => {
63
+ if (timer) {
64
+ clearTimeout(timer);
65
+ timer = null;
66
+ }
67
+ pending = null;
68
+ };
69
+ throttled.flush = () => {
70
+ if (timer) {
71
+ clearTimeout(timer);
72
+ timer = null;
73
+ }
74
+ fire("streaming");
75
+ };
76
+ return throttled;
77
+ }
18
78
  export const ChatProvider = ({ children, bypassPermissions, permissionMode: initialPermissionMode, pluginDirs, additionalDirectories, tools, allowedTools, disallowedTools, workdir, worktreeSession, originalCwd, version, model, mcpServers, }) => {
19
79
  const { restoreSessionId, continueLastSession } = useAppConfig();
20
80
  const { stdout } = useStdout();
@@ -26,11 +86,13 @@ export const ChatProvider = ({ children, bypassPermissions, permissionMode: init
26
86
  const [messages, setMessages] = useState([]);
27
87
  const [latestTotalTokens, setLatestTotalTokens] = useState(0);
28
88
  const [maxInputTokens, setMaxInputTokens] = useState(200000);
29
- // Throttled incremental streaming updaters — 500ms leading+trailing, the same interval
30
- // as the pre-incremental throttledSetMessages. `stage === "end"` flushes the final
31
- // update immediately so completion results are never delayed.
32
- const throttledContentUpdate = useMemo(() => throttle((params) => {
33
- const { messageId, accumulated, stage } = params;
89
+ // Throttled incremental streaming updaters — 500ms window-concat, the same
90
+ // interval as the pre-incremental throttledSetMessages. Chunks are pure
91
+ // deltas: within-window chunks are merged so none is dropped, and
92
+ // `stage === "end"` flushes pending deltas + applies the end signal
93
+ // immediately so completion results are never delayed.
94
+ const throttledContentUpdate = useMemo(() => createStreamingWindowThrottle((params) => {
95
+ const { messageId, chunk, stage } = params;
34
96
  setMessages((prev) => prev.map((m) => {
35
97
  if (m.id !== messageId)
36
98
  return m;
@@ -38,22 +100,23 @@ export const ChatProvider = ({ children, bypassPermissions, permissionMode: init
38
100
  if (textBlockIndex === -1) {
39
101
  return {
40
102
  ...m,
41
- blocks: [
42
- ...m.blocks,
43
- { type: "text", content: accumulated, stage },
44
- ],
103
+ blocks: [...m.blocks, { type: "text", content: chunk, stage }],
45
104
  };
46
105
  }
47
106
  return {
48
107
  ...m,
49
108
  blocks: m.blocks.map((b, idx) => idx === textBlockIndex && b.type === "text"
50
- ? { ...b, content: accumulated, stage }
109
+ ? {
110
+ ...b,
111
+ content: (b.content || "") + chunk,
112
+ stage,
113
+ }
51
114
  : b),
52
115
  };
53
116
  }));
54
117
  }, 500), []);
55
- const throttledReasoningUpdate = useMemo(() => throttle((params) => {
56
- const { messageId, accumulated, stage } = params;
118
+ const throttledReasoningUpdate = useMemo(() => createStreamingWindowThrottle((params) => {
119
+ const { messageId, chunk, stage } = params;
57
120
  setMessages((prev) => prev.map((m) => {
58
121
  if (m.id !== messageId)
59
122
  return m;
@@ -63,14 +126,18 @@ export const ChatProvider = ({ children, bypassPermissions, permissionMode: init
63
126
  ...m,
64
127
  blocks: [
65
128
  ...m.blocks,
66
- { type: "reasoning", content: accumulated, stage },
129
+ { type: "reasoning", content: chunk, stage },
67
130
  ],
68
131
  };
69
132
  }
70
133
  return {
71
134
  ...m,
72
135
  blocks: m.blocks.map((b, idx) => idx === reasoningBlockIndex && b.type === "reasoning"
73
- ? { ...b, content: accumulated, stage }
136
+ ? {
137
+ ...b,
138
+ content: (b.content || "") + chunk,
139
+ stage,
140
+ }
74
141
  : b),
75
142
  };
76
143
  }));
@@ -211,15 +278,11 @@ export const ChatProvider = ({ children, bypassPermissions, permissionMode: init
211
278
  if (isExpandedRef.current)
212
279
  return;
213
280
  throttledContentUpdate(params);
214
- if (params.stage === "end")
215
- throttledContentUpdate.flush();
216
281
  },
217
282
  onAssistantReasoningUpdated: (params) => {
218
283
  if (isExpandedRef.current)
219
284
  return;
220
285
  throttledReasoningUpdate(params);
221
- if (params.stage === "end")
222
- throttledReasoningUpdate.flush();
223
286
  },
224
287
  onToolBlockUpdated: (params) => {
225
288
  if (isExpandedRef.current)
@@ -437,7 +500,6 @@ export const ChatProvider = ({ children, bypassPermissions, permissionMode: init
437
500
  originalCwd,
438
501
  model,
439
502
  initialPermissionMode,
440
- refreshMessages,
441
503
  throttledContentUpdate,
442
504
  throttledReasoningUpdate,
443
505
  throttledToolBlockUpdate,
@@ -71,6 +71,7 @@ export declare class AgentBridge {
71
71
  private compact;
72
72
  private getBackgroundTaskOutput;
73
73
  private stopBackgroundTask;
74
+ private backgroundCurrentTask;
74
75
  private getWorkflowRuns;
75
76
  private stopWorkflowRun;
76
77
  private setPermissionMode;
@@ -140,6 +140,8 @@ export class AgentBridge {
140
140
  return this.getBackgroundTaskOutput(p.taskId, sessionId);
141
141
  case "stopBackgroundTask":
142
142
  return this.stopBackgroundTask(p.taskId, sessionId);
143
+ case "backgroundCurrentTask":
144
+ return this.backgroundCurrentTask(sessionId);
143
145
  case "getWorkflowRuns":
144
146
  return this.getWorkflowRuns(sessionId);
145
147
  case "stopWorkflowRun":
@@ -539,6 +541,11 @@ export class AgentBridge {
539
541
  const success = entry.agent.stopBackgroundTask(taskId);
540
542
  return { success };
541
543
  }
544
+ async backgroundCurrentTask(sessionId) {
545
+ const entry = this.requireSession(sessionId);
546
+ await entry.agent.backgroundCurrentTask();
547
+ return null;
548
+ }
542
549
  async getWorkflowRuns(sessionId) {
543
550
  const entry = this.requireSession(sessionId);
544
551
  const runs = await entry.agent.getWorkflowRuns();
@@ -1,8 +1,8 @@
1
1
  import type { Message } from "wave-agent-sdk";
2
2
  /**
3
3
  * 判断一条 user 消息能否作为 /rewind 检查点。
4
- * 后台任务通知(task_notification)与 hook 注入的消息(source: "hook")
5
- * 都是系统生成、用户不可见的,不能作为回滚点。
4
+ * 后台任务通知(task_notification)、hook 注入的消息(source: "hook")
5
+ * 与 bang 命令消息都是系统生成、用户不可见的,不能作为回滚点。
6
6
  * CLI 交互式选择器与 stdio listRewindCheckpoints 共用此判定,避免两处漂移。
7
7
  */
8
8
  export declare function isUserCheckpointMessage(m: Message): boolean;
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * 判断一条 user 消息能否作为 /rewind 检查点。
3
- * 后台任务通知(task_notification)与 hook 注入的消息(source: "hook")
4
- * 都是系统生成、用户不可见的,不能作为回滚点。
3
+ * 后台任务通知(task_notification)、hook 注入的消息(source: "hook")
4
+ * 与 bang 命令消息都是系统生成、用户不可见的,不能作为回滚点。
5
5
  * CLI 交互式选择器与 stdio listRewindCheckpoints 共用此判定,避免两处漂移。
6
6
  */
7
7
  export function isUserCheckpointMessage(m) {
@@ -11,5 +11,7 @@ export function isUserCheckpointMessage(m) {
11
11
  return false;
12
12
  if (m.blocks.some((b) => b.type === "text" && b.source === "hook"))
13
13
  return false;
14
+ if (m.blocks.some((b) => b.type === "bang"))
15
+ return false;
14
16
  return true;
15
17
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wave-code",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "CLI-based code assistant powered by AI, built with React and Ink",
5
5
  "repository": {
6
6
  "type": "git",
@@ -43,7 +43,7 @@
43
43
  "wrap-ansi": "^10.0.0",
44
44
  "yargs": "^17.7.2",
45
45
  "zod": "^3.23.8",
46
- "wave-agent-sdk": "1.0.4"
46
+ "wave-agent-sdk": "1.0.6"
47
47
  },
48
48
  "devDependencies": {
49
49
  "@types/react": "^19.1.8",
@@ -158,6 +158,84 @@ export interface ChatProviderProps extends BaseAppProps {
158
158
  children: React.ReactNode;
159
159
  }
160
160
 
161
+ interface StreamingUpdateParams {
162
+ messageId: string;
163
+ chunk: string;
164
+ stage: "streaming" | "end";
165
+ }
166
+
167
+ /**
168
+ * Window-concat throttle for pure-delta streaming updates: chunks arriving
169
+ * within the cooldown window are merged so no delta is lost (a dropped delta
170
+ * would permanently lose content, unlike the accumulated-payload throttle it
171
+ * replaces). Leading edge fires immediately; the trailing edge carries only
172
+ * chunks that arrived within the window. `end` flushes any pending deltas
173
+ * first, then forwards the end signal right away.
174
+ */
175
+ function createStreamingWindowThrottle(
176
+ fn: (params: StreamingUpdateParams) => void,
177
+ wait: number,
178
+ ): {
179
+ (params: StreamingUpdateParams): void;
180
+ cancel: () => void;
181
+ flush: () => void;
182
+ } {
183
+ let timer: NodeJS.Timeout | null = null;
184
+ let pending: { messageId: string; chunk: string } | null = null;
185
+
186
+ const fire = (stage: "streaming" | "end") => {
187
+ if (pending) {
188
+ fn({ ...pending, stage });
189
+ pending = null;
190
+ }
191
+ };
192
+
193
+ const throttled = (params: StreamingUpdateParams) => {
194
+ if (params.stage === "end") {
195
+ // Flush any deltas still pending inside the cooldown window first
196
+ if (timer) {
197
+ clearTimeout(timer);
198
+ timer = null;
199
+ }
200
+ fire("streaming");
201
+ fn(params);
202
+ return;
203
+ }
204
+ if (pending) {
205
+ pending.chunk += params.chunk;
206
+ } else {
207
+ pending = { messageId: params.messageId, chunk: params.chunk };
208
+ }
209
+ if (!timer) {
210
+ // Leading edge: fire the current delta immediately, then reset pending so
211
+ // the trailing edge only carries chunks arriving within this window
212
+ fire("streaming");
213
+ timer = setTimeout(() => {
214
+ timer = null;
215
+ fire("streaming");
216
+ }, wait);
217
+ }
218
+ };
219
+
220
+ throttled.cancel = () => {
221
+ if (timer) {
222
+ clearTimeout(timer);
223
+ timer = null;
224
+ }
225
+ pending = null;
226
+ };
227
+
228
+ throttled.flush = () => {
229
+ if (timer) {
230
+ clearTimeout(timer);
231
+ timer = null;
232
+ }
233
+ fire("streaming");
234
+ };
235
+
236
+ return throttled;
237
+ }
238
+
161
239
  export const ChatProvider: React.FC<ChatProviderProps> = ({
162
240
  children,
163
241
  bypassPermissions,
@@ -188,86 +266,77 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
188
266
  const [latestTotalTokens, setLatestTotalTokens] = useState(0);
189
267
  const [maxInputTokens, setMaxInputTokens] = useState(200000);
190
268
 
191
- // Throttled incremental streaming updaters — 500ms leading+trailing, the same interval
192
- // as the pre-incremental throttledSetMessages. `stage === "end"` flushes the final
193
- // update immediately so completion results are never delayed.
269
+ // Throttled incremental streaming updaters — 500ms window-concat, the same
270
+ // interval as the pre-incremental throttledSetMessages. Chunks are pure
271
+ // deltas: within-window chunks are merged so none is dropped, and
272
+ // `stage === "end"` flushes pending deltas + applies the end signal
273
+ // immediately so completion results are never delayed.
194
274
  const throttledContentUpdate = useMemo(
195
275
  () =>
196
- throttle(
197
- (params: {
198
- messageId: string;
199
- accumulated: string;
200
- stage: "streaming" | "end";
201
- }) => {
202
- const { messageId, accumulated, stage } = params;
203
- setMessages((prev) =>
204
- prev.map((m) => {
205
- if (m.id !== messageId) return m;
206
- const textBlockIndex = m.blocks.findIndex(
207
- (b) => b.type === "text",
208
- );
209
- if (textBlockIndex === -1) {
210
- return {
211
- ...m,
212
- blocks: [
213
- ...m.blocks,
214
- { type: "text", content: accumulated, stage },
215
- ],
216
- };
217
- }
276
+ createStreamingWindowThrottle((params) => {
277
+ const { messageId, chunk, stage } = params;
278
+ setMessages((prev) =>
279
+ prev.map((m) => {
280
+ if (m.id !== messageId) return m;
281
+ const textBlockIndex = m.blocks.findIndex((b) => b.type === "text");
282
+ if (textBlockIndex === -1) {
218
283
  return {
219
284
  ...m,
220
- blocks: m.blocks.map((b, idx) =>
221
- idx === textBlockIndex && b.type === "text"
222
- ? { ...b, content: accumulated, stage }
223
- : b,
224
- ),
285
+ blocks: [...m.blocks, { type: "text", content: chunk, stage }],
225
286
  };
226
- }),
227
- );
228
- },
229
- 500,
230
- ),
287
+ }
288
+ return {
289
+ ...m,
290
+ blocks: m.blocks.map((b, idx) =>
291
+ idx === textBlockIndex && b.type === "text"
292
+ ? {
293
+ ...b,
294
+ content: (b.content || "") + chunk,
295
+ stage,
296
+ }
297
+ : b,
298
+ ),
299
+ };
300
+ }),
301
+ );
302
+ }, 500),
231
303
  [],
232
304
  );
233
305
 
234
306
  const throttledReasoningUpdate = useMemo(
235
307
  () =>
236
- throttle(
237
- (params: {
238
- messageId: string;
239
- accumulated: string;
240
- stage: "streaming" | "end";
241
- }) => {
242
- const { messageId, accumulated, stage } = params;
243
- setMessages((prev) =>
244
- prev.map((m) => {
245
- if (m.id !== messageId) return m;
246
- const reasoningBlockIndex = m.blocks.findIndex(
247
- (b) => b.type === "reasoning",
248
- );
249
- if (reasoningBlockIndex === -1) {
250
- return {
251
- ...m,
252
- blocks: [
253
- ...m.blocks,
254
- { type: "reasoning", content: accumulated, stage },
255
- ],
256
- };
257
- }
308
+ createStreamingWindowThrottle((params) => {
309
+ const { messageId, chunk, stage } = params;
310
+ setMessages((prev) =>
311
+ prev.map((m) => {
312
+ if (m.id !== messageId) return m;
313
+ const reasoningBlockIndex = m.blocks.findIndex(
314
+ (b) => b.type === "reasoning",
315
+ );
316
+ if (reasoningBlockIndex === -1) {
258
317
  return {
259
318
  ...m,
260
- blocks: m.blocks.map((b, idx) =>
261
- idx === reasoningBlockIndex && b.type === "reasoning"
262
- ? { ...b, content: accumulated, stage }
263
- : b,
264
- ),
319
+ blocks: [
320
+ ...m.blocks,
321
+ { type: "reasoning", content: chunk, stage },
322
+ ],
265
323
  };
266
- }),
267
- );
268
- },
269
- 500,
270
- ),
324
+ }
325
+ return {
326
+ ...m,
327
+ blocks: m.blocks.map((b, idx) =>
328
+ idx === reasoningBlockIndex && b.type === "reasoning"
329
+ ? {
330
+ ...b,
331
+ content: (b.content || "") + chunk,
332
+ stage,
333
+ }
334
+ : b,
335
+ ),
336
+ };
337
+ }),
338
+ );
339
+ }, 500),
271
340
  [],
272
341
  );
273
342
 
@@ -477,12 +546,10 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
477
546
  onAssistantContentUpdated: (params) => {
478
547
  if (isExpandedRef.current) return;
479
548
  throttledContentUpdate(params);
480
- if (params.stage === "end") throttledContentUpdate.flush();
481
549
  },
482
550
  onAssistantReasoningUpdated: (params) => {
483
551
  if (isExpandedRef.current) return;
484
552
  throttledReasoningUpdate(params);
485
- if (params.stage === "end") throttledReasoningUpdate.flush();
486
553
  },
487
554
  onToolBlockUpdated: (params) => {
488
555
  if (isExpandedRef.current) return;
@@ -730,7 +797,6 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
730
797
  originalCwd,
731
798
  model,
732
799
  initialPermissionMode,
733
- refreshMessages,
734
800
  throttledContentUpdate,
735
801
  throttledReasoningUpdate,
736
802
  throttledToolBlockUpdate,
@@ -335,6 +335,8 @@ export class AgentBridge {
335
335
  return this.getBackgroundTaskOutput(p.taskId as string, sessionId);
336
336
  case "stopBackgroundTask":
337
337
  return this.stopBackgroundTask(p.taskId as string, sessionId);
338
+ case "backgroundCurrentTask":
339
+ return this.backgroundCurrentTask(sessionId);
338
340
 
339
341
  case "getWorkflowRuns":
340
342
  return this.getWorkflowRuns(sessionId);
@@ -905,6 +907,12 @@ export class AgentBridge {
905
907
  return { success };
906
908
  }
907
909
 
910
+ private async backgroundCurrentTask(sessionId?: string): Promise<null> {
911
+ const entry = this.requireSession(sessionId);
912
+ await entry.agent.backgroundCurrentTask();
913
+ return null;
914
+ }
915
+
908
916
  private async getWorkflowRuns(
909
917
  sessionId?: string,
910
918
  ): Promise<{ runs: SerializableWorkflowRun[] }> {
@@ -2,8 +2,8 @@ import type { Message } from "wave-agent-sdk";
2
2
 
3
3
  /**
4
4
  * 判断一条 user 消息能否作为 /rewind 检查点。
5
- * 后台任务通知(task_notification)与 hook 注入的消息(source: "hook")
6
- * 都是系统生成、用户不可见的,不能作为回滚点。
5
+ * 后台任务通知(task_notification)、hook 注入的消息(source: "hook")
6
+ * 与 bang 命令消息都是系统生成、用户不可见的,不能作为回滚点。
7
7
  * CLI 交互式选择器与 stdio listRewindCheckpoints 共用此判定,避免两处漂移。
8
8
  */
9
9
  export function isUserCheckpointMessage(m: Message): boolean {
@@ -11,5 +11,6 @@ export function isUserCheckpointMessage(m: Message): boolean {
11
11
  if (m.blocks.some((b) => b.type === "task_notification")) return false;
12
12
  if (m.blocks.some((b) => b.type === "text" && b.source === "hook"))
13
13
  return false;
14
+ if (m.blocks.some((b) => b.type === "bang")) return false;
14
15
  return true;
15
16
  }