wave-code 1.0.5 → 1.0.7

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 (39) hide show
  1. package/dist/components/AgentsManager.d.ts +7 -0
  2. package/dist/components/AgentsManager.js +109 -0
  3. package/dist/components/ConfirmationSelector.js +17 -3
  4. package/dist/components/InputBox.js +7 -21
  5. package/dist/components/LoginCommand.js +31 -2
  6. package/dist/components/MarketplaceAddForm.js +16 -2
  7. package/dist/constants/commands.js +6 -0
  8. package/dist/contexts/useChat.d.ts +2 -1
  9. package/dist/contexts/useChat.js +96 -21
  10. package/dist/hooks/useInputManager.d.ts +2 -0
  11. package/dist/hooks/useInputManager.js +8 -0
  12. package/dist/managers/inputHandlers.js +3 -0
  13. package/dist/managers/inputReducer.d.ts +4 -0
  14. package/dist/managers/inputReducer.js +8 -0
  15. package/dist/reducers/agentsManagerReducer.d.ts +26 -0
  16. package/dist/reducers/agentsManagerReducer.js +54 -0
  17. package/dist/stdio/agentBridge.d.ts +4 -0
  18. package/dist/stdio/agentBridge.js +45 -10
  19. package/dist/stdio/protocol.d.ts +1 -1
  20. package/dist/utils/rewindCheckpoints.d.ts +2 -2
  21. package/dist/utils/rewindCheckpoints.js +4 -2
  22. package/dist/utils/usageSummary.d.ts +0 -4
  23. package/dist/utils/usageSummary.js +1 -34
  24. package/package.json +2 -2
  25. package/src/components/AgentsManager.tsx +290 -0
  26. package/src/components/ConfirmationSelector.tsx +18 -3
  27. package/src/components/InputBox.tsx +54 -45
  28. package/src/components/LoginCommand.tsx +35 -2
  29. package/src/components/MarketplaceAddForm.tsx +17 -2
  30. package/src/constants/commands.ts +6 -0
  31. package/src/contexts/useChat.tsx +159 -72
  32. package/src/hooks/useInputManager.ts +8 -0
  33. package/src/managers/inputHandlers.ts +2 -0
  34. package/src/managers/inputReducer.ts +10 -0
  35. package/src/reducers/agentsManagerReducer.ts +91 -0
  36. package/src/stdio/agentBridge.ts +55 -9
  37. package/src/stdio/protocol.ts +2 -0
  38. package/src/utils/rewindCheckpoints.ts +3 -2
  39. package/src/utils/usageSummary.ts +2 -46
@@ -15,6 +15,7 @@ import type {
15
15
  BackgroundTask,
16
16
  Task,
17
17
  SlashCommand,
18
+ SubagentConfiguration,
18
19
  PermissionDecision,
19
20
  PermissionMode,
20
21
  QueuedMessage,
@@ -96,6 +97,8 @@ export interface ChatContextType {
96
97
  // Slash Command functionality
97
98
  slashCommands: SlashCommand[];
98
99
  hasSlashCommand: (commandId: string) => boolean;
100
+ // Agent definitions (for /agents overlay)
101
+ agentDefinitions: SubagentConfiguration[];
99
102
  // Permission functionality
100
103
  permissionMode: PermissionMode;
101
104
  setPermissionMode: (mode: PermissionMode) => void;
@@ -158,6 +161,84 @@ export interface ChatProviderProps extends BaseAppProps {
158
161
  children: React.ReactNode;
159
162
  }
160
163
 
164
+ interface StreamingUpdateParams {
165
+ messageId: string;
166
+ chunk: string;
167
+ stage: "streaming" | "end";
168
+ }
169
+
170
+ /**
171
+ * Window-concat throttle for pure-delta streaming updates: chunks arriving
172
+ * within the cooldown window are merged so no delta is lost (a dropped delta
173
+ * would permanently lose content, unlike the accumulated-payload throttle it
174
+ * replaces). Leading edge fires immediately; the trailing edge carries only
175
+ * chunks that arrived within the window. `end` flushes any pending deltas
176
+ * first, then forwards the end signal right away.
177
+ */
178
+ function createStreamingWindowThrottle(
179
+ fn: (params: StreamingUpdateParams) => void,
180
+ wait: number,
181
+ ): {
182
+ (params: StreamingUpdateParams): void;
183
+ cancel: () => void;
184
+ flush: () => void;
185
+ } {
186
+ let timer: NodeJS.Timeout | null = null;
187
+ let pending: { messageId: string; chunk: string } | null = null;
188
+
189
+ const fire = (stage: "streaming" | "end") => {
190
+ if (pending) {
191
+ fn({ ...pending, stage });
192
+ pending = null;
193
+ }
194
+ };
195
+
196
+ const throttled = (params: StreamingUpdateParams) => {
197
+ if (params.stage === "end") {
198
+ // Flush any deltas still pending inside the cooldown window first
199
+ if (timer) {
200
+ clearTimeout(timer);
201
+ timer = null;
202
+ }
203
+ fire("streaming");
204
+ fn(params);
205
+ return;
206
+ }
207
+ if (pending) {
208
+ pending.chunk += params.chunk;
209
+ } else {
210
+ pending = { messageId: params.messageId, chunk: params.chunk };
211
+ }
212
+ if (!timer) {
213
+ // Leading edge: fire the current delta immediately, then reset pending so
214
+ // the trailing edge only carries chunks arriving within this window
215
+ fire("streaming");
216
+ timer = setTimeout(() => {
217
+ timer = null;
218
+ fire("streaming");
219
+ }, wait);
220
+ }
221
+ };
222
+
223
+ throttled.cancel = () => {
224
+ if (timer) {
225
+ clearTimeout(timer);
226
+ timer = null;
227
+ }
228
+ pending = null;
229
+ };
230
+
231
+ throttled.flush = () => {
232
+ if (timer) {
233
+ clearTimeout(timer);
234
+ timer = null;
235
+ }
236
+ fire("streaming");
237
+ };
238
+
239
+ return throttled;
240
+ }
241
+
161
242
  export const ChatProvider: React.FC<ChatProviderProps> = ({
162
243
  children,
163
244
  bypassPermissions,
@@ -188,86 +269,77 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
188
269
  const [latestTotalTokens, setLatestTotalTokens] = useState(0);
189
270
  const [maxInputTokens, setMaxInputTokens] = useState(200000);
190
271
 
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.
272
+ // Throttled incremental streaming updaters — 500ms window-concat, the same
273
+ // interval as the pre-incremental throttledSetMessages. Chunks are pure
274
+ // deltas: within-window chunks are merged so none is dropped, and
275
+ // `stage === "end"` flushes pending deltas + applies the end signal
276
+ // immediately so completion results are never delayed.
194
277
  const throttledContentUpdate = useMemo(
195
278
  () =>
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
- }
279
+ createStreamingWindowThrottle((params) => {
280
+ const { messageId, chunk, stage } = params;
281
+ setMessages((prev) =>
282
+ prev.map((m) => {
283
+ if (m.id !== messageId) return m;
284
+ const textBlockIndex = m.blocks.findIndex((b) => b.type === "text");
285
+ if (textBlockIndex === -1) {
218
286
  return {
219
287
  ...m,
220
- blocks: m.blocks.map((b, idx) =>
221
- idx === textBlockIndex && b.type === "text"
222
- ? { ...b, content: accumulated, stage }
223
- : b,
224
- ),
288
+ blocks: [...m.blocks, { type: "text", content: chunk, stage }],
225
289
  };
226
- }),
227
- );
228
- },
229
- 500,
230
- ),
290
+ }
291
+ return {
292
+ ...m,
293
+ blocks: m.blocks.map((b, idx) =>
294
+ idx === textBlockIndex && b.type === "text"
295
+ ? {
296
+ ...b,
297
+ content: (b.content || "") + chunk,
298
+ stage,
299
+ }
300
+ : b,
301
+ ),
302
+ };
303
+ }),
304
+ );
305
+ }, 500),
231
306
  [],
232
307
  );
233
308
 
234
309
  const throttledReasoningUpdate = useMemo(
235
310
  () =>
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
- }
311
+ createStreamingWindowThrottle((params) => {
312
+ const { messageId, chunk, stage } = params;
313
+ setMessages((prev) =>
314
+ prev.map((m) => {
315
+ if (m.id !== messageId) return m;
316
+ const reasoningBlockIndex = m.blocks.findIndex(
317
+ (b) => b.type === "reasoning",
318
+ );
319
+ if (reasoningBlockIndex === -1) {
258
320
  return {
259
321
  ...m,
260
- blocks: m.blocks.map((b, idx) =>
261
- idx === reasoningBlockIndex && b.type === "reasoning"
262
- ? { ...b, content: accumulated, stage }
263
- : b,
264
- ),
322
+ blocks: [
323
+ ...m.blocks,
324
+ { type: "reasoning", content: chunk, stage },
325
+ ],
265
326
  };
266
- }),
267
- );
268
- },
269
- 500,
270
- ),
327
+ }
328
+ return {
329
+ ...m,
330
+ blocks: m.blocks.map((b, idx) =>
331
+ idx === reasoningBlockIndex && b.type === "reasoning"
332
+ ? {
333
+ ...b,
334
+ content: (b.content || "") + chunk,
335
+ stage,
336
+ }
337
+ : b,
338
+ ),
339
+ };
340
+ }),
341
+ );
342
+ }, 500),
271
343
  [],
272
344
  );
273
345
 
@@ -350,6 +422,11 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
350
422
  // Command state
351
423
  const [slashCommands, setSlashCommands] = useState<SlashCommand[]>([]);
352
424
 
425
+ // Agent definitions (for /agents overlay)
426
+ const [agentDefinitions, setAgentDefinitions] = useState<
427
+ SubagentConfiguration[]
428
+ >([]);
429
+
353
430
  // Permission state
354
431
  const [permissionMode, setPermissionModeState] = useState<PermissionMode>(
355
432
  initialPermissionMode ||
@@ -477,12 +554,10 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
477
554
  onAssistantContentUpdated: (params) => {
478
555
  if (isExpandedRef.current) return;
479
556
  throttledContentUpdate(params);
480
- if (params.stage === "end") throttledContentUpdate.flush();
481
557
  },
482
558
  onAssistantReasoningUpdated: (params) => {
483
559
  if (isExpandedRef.current) return;
484
560
  throttledReasoningUpdate(params);
485
- if (params.stage === "end") throttledReasoningUpdate.flush();
486
561
  },
487
562
  onToolBlockUpdated: (params) => {
488
563
  if (isExpandedRef.current) return;
@@ -560,7 +635,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
560
635
  ),
561
636
  );
562
637
  },
563
- onCompleteBangMessage: (command, exitCode, messageId) => {
638
+ onCompleteBangMessage: (command, exitCode, messageId, output) => {
564
639
  if (isExpandedRef.current) return;
565
640
  setMessages((prev) =>
566
641
  prev.map((m) =>
@@ -569,7 +644,13 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
569
644
  ...m,
570
645
  blocks: m.blocks.map((b, idx) =>
571
646
  idx === m.blocks.length - 1 && b.type === "bang"
572
- ? { ...b, command, exitCode, stage: "end" }
647
+ ? {
648
+ ...b,
649
+ command,
650
+ exitCode,
651
+ stage: "end",
652
+ ...(output !== undefined ? { output } : {}),
653
+ }
573
654
  : b,
574
655
  ),
575
656
  }
@@ -711,6 +792,11 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
711
792
  // Get initial commands
712
793
  const agentSlashCommands = agent.getSlashCommands?.() || [];
713
794
  setSlashCommands(agentSlashCommands);
795
+
796
+ // Get initial agent definitions
797
+ const initialAgentDefinitions =
798
+ agent.getSubagentConfigurations?.() || [];
799
+ setAgentDefinitions(initialAgentDefinitions);
714
800
  } catch (error) {
715
801
  console.error("Failed to initialize AI manager:", error);
716
802
  }
@@ -730,7 +816,6 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
730
816
  originalCwd,
731
817
  model,
732
818
  initialPermissionMode,
733
- refreshMessages,
734
819
  throttledContentUpdate,
735
820
  throttledReasoningUpdate,
736
821
  throttledToolBlockUpdate,
@@ -752,6 +837,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
752
837
  setMessages([]);
753
838
  setMcpServerStatuses([]);
754
839
  setSlashCommands([]);
840
+ setAgentDefinitions([]);
755
841
  setSessionId("");
756
842
  setIsLoading(false);
757
843
  setLatestTotalTokens(0);
@@ -1139,6 +1225,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
1139
1225
  stopBackgroundTask,
1140
1226
  slashCommands,
1141
1227
  hasSlashCommand,
1228
+ agentDefinitions,
1142
1229
  permissionMode,
1143
1230
  setPermissionMode,
1144
1231
  isConfirmationVisible,
@@ -242,6 +242,8 @@ export const useInputManager = (
242
242
  });
243
243
  } else if (command === "mcp") {
244
244
  dispatch({ type: "SET_SHOW_MCP_MANAGER", payload: true });
245
+ } else if (command === "agents") {
246
+ dispatch({ type: "SET_SHOW_AGENTS_MANAGER", payload: true });
245
247
  } else if (command === "rewind") {
246
248
  dispatch({ type: "SET_SHOW_REWIND_MANAGER", payload: true });
247
249
  } else if (command === "help") {
@@ -493,6 +495,10 @@ export const useInputManager = (
493
495
  dispatch({ type: "SET_SHOW_MCP_MANAGER", payload: show });
494
496
  }, []);
495
497
 
498
+ const setShowAgentsManager = useCallback((show: boolean) => {
499
+ dispatch({ type: "SET_SHOW_AGENTS_MANAGER", payload: show });
500
+ }, []);
501
+
496
502
  const setShowRewindManager = useCallback((show: boolean) => {
497
503
  dispatch({ type: "SET_SHOW_REWIND_MANAGER", payload: show });
498
504
  }, []);
@@ -657,6 +663,7 @@ export const useInputManager = (
657
663
  historySearchQuery: state.historySearchQuery,
658
664
  showBackgroundTaskManager: state.showBackgroundTaskManager,
659
665
  showMcpManager: state.showMcpManager,
666
+ showAgentsManager: state.showAgentsManager,
660
667
  showRewindManager: state.showRewindManager,
661
668
  showHelp: state.showHelp,
662
669
  showStatusCommand: state.showStatusCommand,
@@ -702,6 +709,7 @@ export const useInputManager = (
702
709
  // Bash/MCP Manager
703
710
  setShowBackgroundTaskManager,
704
711
  setShowMcpManager,
712
+ setShowAgentsManager,
705
713
  setShowRewindManager,
706
714
  setShowHelp,
707
715
  setShowStatusCommand,
@@ -367,6 +367,8 @@ export const handleCommandSelect = (
367
367
  });
368
368
  } else if (command === "mcp") {
369
369
  dispatch({ type: "SET_SHOW_MCP_MANAGER", payload: true });
370
+ } else if (command === "agents") {
371
+ dispatch({ type: "SET_SHOW_AGENTS_MANAGER", payload: true });
370
372
  } else if (command === "rewind") {
371
373
  dispatch({ type: "SET_SHOW_REWIND_MANAGER", payload: true });
372
374
  } else if (command === "help") {
@@ -130,6 +130,7 @@ export interface InputState {
130
130
  imageIdCounter: number;
131
131
  showBackgroundTaskManager: boolean;
132
132
  showMcpManager: boolean;
133
+ showAgentsManager: boolean;
133
134
  showRewindManager: boolean;
134
135
  showHelp: boolean;
135
136
  showStatusCommand: boolean;
@@ -167,6 +168,7 @@ export const initialState: InputState = {
167
168
  imageIdCounter: 1,
168
169
  showBackgroundTaskManager: false,
169
170
  showMcpManager: false,
171
+ showAgentsManager: false,
170
172
  showRewindManager: false,
171
173
  showHelp: false,
172
174
  showStatusCommand: false,
@@ -400,6 +402,7 @@ export type InputAction =
400
402
  | { type: "CLEAR_IMAGES" }
401
403
  | { type: "SET_SHOW_BACKGROUND_TASK_MANAGER"; payload: boolean }
402
404
  | { type: "SET_SHOW_MCP_MANAGER"; payload: boolean }
405
+ | { type: "SET_SHOW_AGENTS_MANAGER"; payload: boolean }
403
406
  | { type: "SET_SHOW_REWIND_MANAGER"; payload: boolean }
404
407
  | { type: "SET_SHOW_HELP"; payload: boolean }
405
408
  | { type: "SET_SHOW_STATUS_COMMAND"; payload: boolean }
@@ -589,6 +592,12 @@ export function inputReducer(
589
592
  showMcpManager: action.payload,
590
593
  selectorJustUsed: !action.payload ? true : state.selectorJustUsed,
591
594
  };
595
+ case "SET_SHOW_AGENTS_MANAGER":
596
+ return {
597
+ ...state,
598
+ showAgentsManager: action.payload,
599
+ selectorJustUsed: !action.payload ? true : state.selectorJustUsed,
600
+ };
592
601
  case "SET_SHOW_REWIND_MANAGER":
593
602
  return {
594
603
  ...state,
@@ -970,6 +979,7 @@ export function inputReducer(
970
979
  !(
971
980
  state.showBackgroundTaskManager ||
972
981
  state.showMcpManager ||
982
+ state.showAgentsManager ||
973
983
  state.showRewindManager ||
974
984
  state.showHelp ||
975
985
  state.showStatusCommand ||
@@ -0,0 +1,91 @@
1
+ import { Key } from "ink";
2
+
3
+ export type PendingEffect = { type: "CANCEL" };
4
+
5
+ export interface AgentsManagerState {
6
+ selectedIndex: number;
7
+ viewMode: "list" | "detail";
8
+ pendingEffect: PendingEffect | null;
9
+ }
10
+
11
+ export type AgentsManagerAction =
12
+ | { type: "MOVE_UP" }
13
+ | { type: "MOVE_DOWN"; itemCount: number }
14
+ | { type: "SET_VIEW_MODE"; viewMode: "list" | "detail" }
15
+ | {
16
+ type: "HANDLE_KEY";
17
+ input: string;
18
+ key: Key;
19
+ itemCount: number;
20
+ }
21
+ | { type: "CLEAR_PENDING_EFFECT" };
22
+
23
+ export function agentsManagerReducer(
24
+ state: AgentsManagerState,
25
+ action: AgentsManagerAction,
26
+ ): AgentsManagerState {
27
+ switch (action.type) {
28
+ case "MOVE_UP":
29
+ return {
30
+ ...state,
31
+ selectedIndex: Math.max(0, state.selectedIndex - 1),
32
+ };
33
+ case "MOVE_DOWN":
34
+ return {
35
+ ...state,
36
+ selectedIndex: Math.min(
37
+ Math.max(0, action.itemCount - 1),
38
+ state.selectedIndex + 1,
39
+ ),
40
+ };
41
+ case "SET_VIEW_MODE":
42
+ return { ...state, viewMode: action.viewMode };
43
+ case "HANDLE_KEY": {
44
+ const { key, itemCount } = action;
45
+
46
+ if (key.return) {
47
+ if (state.viewMode === "list") {
48
+ return { ...state, viewMode: "detail" };
49
+ }
50
+ // Aligned with Claude Code AgentDetail: Enter returns to the list.
51
+ return { ...state, viewMode: "list" };
52
+ }
53
+
54
+ if (key.escape) {
55
+ if (state.viewMode === "detail") {
56
+ return { ...state, viewMode: "list" };
57
+ }
58
+ return { ...state, pendingEffect: { type: "CANCEL" } };
59
+ }
60
+
61
+ // Detail view does not respond to arrow keys (aligned with CC
62
+ // AgentDetail, which only Esc/Enter back to the list).
63
+ if (state.viewMode === "detail") {
64
+ return state;
65
+ }
66
+
67
+ if (key.upArrow) {
68
+ return {
69
+ ...state,
70
+ selectedIndex: Math.max(0, state.selectedIndex - 1),
71
+ };
72
+ }
73
+
74
+ if (key.downArrow) {
75
+ return {
76
+ ...state,
77
+ selectedIndex: Math.min(
78
+ Math.max(0, itemCount - 1),
79
+ state.selectedIndex + 1,
80
+ ),
81
+ };
82
+ }
83
+
84
+ return state;
85
+ }
86
+ case "CLEAR_PENDING_EFFECT":
87
+ return { ...state, pendingEffect: null };
88
+ default:
89
+ return state;
90
+ }
91
+ }
@@ -42,6 +42,8 @@ import {
42
42
  PluginCore,
43
43
  validateWorktreeRemovalPath,
44
44
  type SlashCommand,
45
+ loadUserConfigEnv,
46
+ type SubagentConfiguration,
45
47
  } from "wave-agent-sdk";
46
48
  import {
47
49
  type JsonRpcError,
@@ -140,6 +142,15 @@ export class AgentBridge {
140
142
 
141
143
  constructor(options: AgentBridgeOptions) {
142
144
  this.emit = options.emit;
145
+ // Mirror the user-level settings env WAVE_SERVER_URL into process.env
146
+ // before any agent initializes. getAuthStatus (webviewReady →
147
+ // pushInitialState) can run before the first agent, and AuthService falls
148
+ // back to the default URL otherwise — refreshing a custom-domain token
149
+ // against the wrong host 401s into a logged-out state.
150
+ const userEnv = loadUserConfigEnv();
151
+ if (userEnv.WAVE_SERVER_URL) {
152
+ process.env.WAVE_SERVER_URL = userEnv.WAVE_SERVER_URL;
153
+ }
143
154
  }
144
155
 
145
156
  // ── Public API ────────────────────────────────────────────────
@@ -166,6 +177,10 @@ export class AgentBridge {
166
177
  return this.listPendingPermissions();
167
178
  case "updateConfig":
168
179
  return this.updateConfig(p as unknown as UpdateConfigParams, sessionId);
180
+ case "getConfiguredModels":
181
+ return this.getConfiguredModels(sessionId);
182
+ case "setModel":
183
+ return this.setModel(p.model as string, sessionId);
169
184
 
170
185
  // ── Messages ──
171
186
  case "sendMessage":
@@ -222,6 +237,8 @@ export class AgentBridge {
222
237
  // ── Commands ──
223
238
  case "getSlashCommands":
224
239
  return this.getSlashCommands(sessionId);
240
+ case "getSubagentConfigurations":
241
+ return this.getSubagentConfigurations(sessionId);
225
242
 
226
243
  // ── File / History (global — no session required) ──
227
244
  case "searchFiles":
@@ -335,6 +352,8 @@ export class AgentBridge {
335
352
  return this.getBackgroundTaskOutput(p.taskId as string, sessionId);
336
353
  case "stopBackgroundTask":
337
354
  return this.stopBackgroundTask(p.taskId as string, sessionId);
355
+ case "backgroundCurrentTask":
356
+ return this.backgroundCurrentTask(sessionId);
338
357
 
339
358
  case "getWorkflowRuns":
340
359
  return this.getWorkflowRuns(sessionId);
@@ -725,6 +744,27 @@ export class AgentBridge {
725
744
  return { sessionId: agent.sessionId };
726
745
  }
727
746
 
747
+ private getConfiguredModels(sessionId?: string): {
748
+ models: string[];
749
+ currentModel: string | undefined;
750
+ } {
751
+ const entry = this.requireSession(sessionId);
752
+ return {
753
+ models: entry.agent.getConfiguredModels(),
754
+ currentModel: entry.agent.getModelConfig().model,
755
+ };
756
+ }
757
+
758
+ private async setModel(model: string, sessionId?: string): Promise<null> {
759
+ const entry = this.requireSession(sessionId);
760
+ entry.agent.setModel(model);
761
+ // Keep storedConfig in sync: updateConfig recreates the agent from
762
+ // storedConfig, so without this a later config save would revert the
763
+ // model chosen here.
764
+ entry.storedConfig = { ...entry.storedConfig, model };
765
+ return null;
766
+ }
767
+
728
768
  // ── Messages ──────────────────────────────────────────────────
729
769
 
730
770
  private async sendMessage(
@@ -905,6 +945,12 @@ export class AgentBridge {
905
945
  return { success };
906
946
  }
907
947
 
948
+ private async backgroundCurrentTask(sessionId?: string): Promise<null> {
949
+ const entry = this.requireSession(sessionId);
950
+ await entry.agent.backgroundCurrentTask();
951
+ return null;
952
+ }
953
+
908
954
  private async getWorkflowRuns(
909
955
  sessionId?: string,
910
956
  ): Promise<{ runs: SerializableWorkflowRun[] }> {
@@ -976,6 +1022,13 @@ export class AgentBridge {
976
1022
  return { commands: entry.agent.getSlashCommands() };
977
1023
  }
978
1024
 
1025
+ private getSubagentConfigurations(sessionId?: string): {
1026
+ configurations: SubagentConfiguration[];
1027
+ } {
1028
+ const entry = this.requireSession(sessionId);
1029
+ return { configurations: entry.agent.getSubagentConfigurations() };
1030
+ }
1031
+
979
1032
  // ── File / History (global) ───────────────────────────────────
980
1033
 
981
1034
  private async searchFiles(
@@ -1099,13 +1152,6 @@ export class AgentBridge {
1099
1152
  serverUrl: string;
1100
1153
  }> {
1101
1154
  const authService = AuthService.getInstance();
1102
- // A stale-but-refreshable token still means "logged in" — the daemon may
1103
- // have started with an expired access token (hourly expiry) and only
1104
- // refreshes lazily on the first API call. Without this proactive refresh a
1105
- // fresh client querying right after daemon start gets a false
1106
- // isAuthenticated and e.g. the desktop welcome page keeps showing the
1107
- // login button for an authenticated host. Mirrors the refresh that
1108
- // createAuthAwareFetch does before every real request.
1109
1155
  await authService.checkAndRefreshTokenIfNeeded();
1110
1156
  return {
1111
1157
  isAuthenticated: authService.isSSOAuthenticated(),
@@ -1416,10 +1462,10 @@ export class AgentBridge {
1416
1462
  ctx.registeredSessionId,
1417
1463
  );
1418
1464
  },
1419
- onCompleteBangMessage: (command, exitCode, messageId) => {
1465
+ onCompleteBangMessage: (command, exitCode, messageId, output) => {
1420
1466
  this.emit(
1421
1467
  "bangMessageCompleted",
1422
- { command, exitCode, messageId },
1468
+ { command, exitCode, messageId, output },
1423
1469
  ctx.registeredSessionId,
1424
1470
  );
1425
1471
  },
@@ -75,6 +75,8 @@ export type RequestMethod =
75
75
  | "getPromptHistory"
76
76
  | "searchPromptHistory"
77
77
  | "updateConfig"
78
+ | "getConfiguredModels"
79
+ | "setModel"
78
80
  // Permissions (daemon attach: re-surface pending approvals after reconnect)
79
81
  | "listPendingPermissions"
80
82
  // Auth
@@ -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
  }