wave-code 1.0.0 → 1.0.2

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 (72) hide show
  1. package/dist/cli.js +20 -1
  2. package/dist/components/App.js +7 -0
  3. package/dist/components/BtwDisplay.js +13 -3
  4. package/dist/components/ChatInterface.js +25 -8
  5. package/dist/components/InputBox.d.ts +1 -3
  6. package/dist/components/InputBox.js +12 -9
  7. package/dist/components/LoadingIndicator.d.ts +1 -2
  8. package/dist/components/LoadingIndicator.js +2 -2
  9. package/dist/components/LoginCommand.js +4 -2
  10. package/dist/components/Markdown.js +13 -16
  11. package/dist/components/Notifications.d.ts +7 -0
  12. package/dist/components/Notifications.js +9 -0
  13. package/dist/components/StatusLine.d.ts +0 -4
  14. package/dist/components/StatusLine.js +6 -10
  15. package/dist/components/TaskList.js +2 -1
  16. package/dist/components/ToolDisplay.d.ts +1 -0
  17. package/dist/components/ToolDisplay.js +17 -9
  18. package/dist/constants/commands.js +0 -6
  19. package/dist/contexts/useChat.d.ts +4 -6
  20. package/dist/contexts/useChat.js +253 -110
  21. package/dist/daemon-cli.d.ts +10 -0
  22. package/dist/daemon-cli.js +15 -0
  23. package/dist/hooks/useInputManager.js +99 -22
  24. package/dist/index.js +10 -0
  25. package/dist/managers/inputHandlers.js +50 -22
  26. package/dist/managers/inputReducer.d.ts +12 -2
  27. package/dist/managers/inputReducer.js +57 -9
  28. package/dist/stdio/agentBridge.d.ts +23 -0
  29. package/dist/stdio/agentBridge.js +134 -16
  30. package/dist/stdio/daemonServer.d.ts +67 -0
  31. package/dist/stdio/daemonServer.js +191 -0
  32. package/dist/stdio/index.d.ts +2 -0
  33. package/dist/stdio/index.js +2 -0
  34. package/dist/stdio/jsonRpcConnection.d.ts +30 -0
  35. package/dist/stdio/jsonRpcConnection.js +127 -0
  36. package/dist/stdio/protocol.d.ts +2 -2
  37. package/dist/stdio/stdioServer.d.ts +2 -7
  38. package/dist/stdio/stdioServer.js +9 -100
  39. package/dist/utils/bracketedPaste.d.ts +39 -0
  40. package/dist/utils/bracketedPaste.js +122 -0
  41. package/dist/utils/markdownTable.d.ts +34 -0
  42. package/dist/utils/markdownTable.js +302 -0
  43. package/dist/utils/throttle.d.ts +3 -3
  44. package/package.json +4 -2
  45. package/src/cli.tsx +20 -1
  46. package/src/components/App.tsx +5 -0
  47. package/src/components/BtwDisplay.tsx +36 -12
  48. package/src/components/ChatInterface.tsx +30 -15
  49. package/src/components/InputBox.tsx +25 -24
  50. package/src/components/LoadingIndicator.tsx +1 -4
  51. package/src/components/LoginCommand.tsx +4 -2
  52. package/src/components/Markdown.tsx +15 -18
  53. package/src/components/Notifications.tsx +31 -0
  54. package/src/components/StatusLine.tsx +17 -44
  55. package/src/components/TaskList.tsx +2 -1
  56. package/src/components/ToolDisplay.tsx +17 -6
  57. package/src/constants/commands.ts +0 -6
  58. package/src/contexts/useChat.tsx +326 -140
  59. package/src/daemon-cli.ts +17 -0
  60. package/src/hooks/useInputManager.ts +108 -22
  61. package/src/index.ts +12 -0
  62. package/src/managers/inputHandlers.ts +49 -22
  63. package/src/managers/inputReducer.ts +66 -11
  64. package/src/stdio/agentBridge.ts +196 -17
  65. package/src/stdio/daemonServer.ts +212 -0
  66. package/src/stdio/index.ts +2 -0
  67. package/src/stdio/jsonRpcConnection.ts +160 -0
  68. package/src/stdio/protocol.ts +5 -2
  69. package/src/stdio/stdioServer.ts +14 -120
  70. package/src/utils/bracketedPaste.ts +170 -0
  71. package/src/utils/markdownTable.ts +359 -0
  72. package/src/utils/throttle.ts +8 -8
@@ -19,6 +19,7 @@ import type {
19
19
  PermissionMode,
20
20
  QueuedMessage,
21
21
  WorkflowRun,
22
+ ToolBlockUpdateCallbackParams,
22
23
  } from "wave-agent-sdk";
23
24
  import {
24
25
  Agent,
@@ -45,6 +46,9 @@ export interface ChatContextType {
45
46
  isExpanded: boolean;
46
47
  isTaskListVisible: boolean;
47
48
  setIsTaskListVisible: (visible: boolean) => void;
49
+ // True while the /btw side-question overlay is on display
50
+ isBtwActive: boolean;
51
+ setIsBtwActive: (active: boolean) => void;
48
52
  queuedMessages: QueuedMessage[];
49
53
  // AI functionality
50
54
  sessionId: string;
@@ -53,10 +57,13 @@ export interface ChatContextType {
53
57
  images?: Array<{ path: string; mimeType: string }>,
54
58
  longTextMap?: Record<string, string>,
55
59
  ) => Promise<void>;
56
- askBtw: (question: string) => Promise<string>;
60
+ askBtw: (
61
+ question: string,
62
+ abortSignal?: AbortSignal,
63
+ onContent?: (content: string) => void,
64
+ ) => Promise<string>;
57
65
  clearMessages: () => Promise<void>;
58
66
  compact: (instructions?: string) => Promise<void>;
59
- goalCommand: (args?: string) => Promise<void>;
60
67
  abortMessage: () => void;
61
68
  recallQueuedMessage: () => QueuedMessage | null;
62
69
  removeQueuedMessageById: (id: string) => boolean;
@@ -115,7 +122,7 @@ export interface ChatContextType {
115
122
  backgroundCurrentTask: () => void;
116
123
  // Remount functionality
117
124
  remountKey: number;
118
- requestRemount: () => void;
125
+ forceRemount: () => void;
119
126
  // Rewind functionality
120
127
  handleRewindSelect: (index: number) => Promise<void>;
121
128
  getFullMessageThread: () => Promise<{
@@ -132,10 +139,6 @@ export interface ChatContextType {
132
139
  recreateAgent: () => void;
133
140
  // Trigger WorktreeRemove hook BEFORE agent destruction
134
141
  triggerWorktreeRemoveHook: (worktreePath: string) => Promise<void>;
135
- // Goal state
136
- isGoalActive: boolean;
137
- goalElapsed?: string;
138
- isGoalEvaluating: boolean;
139
142
  }
140
143
 
141
144
  const ChatContext = createContext<ChatContextType | null>(null);
@@ -175,39 +178,150 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
175
178
  const isExpandedRef = useRef(isExpanded);
176
179
 
177
180
  const [isTaskListVisible, setIsTaskListVisible] = useState(true);
181
+ const [isBtwActive, setIsBtwActive] = useState(false);
178
182
 
179
183
  const [messages, setMessages] = useState<Message[]>([]);
180
184
  const [latestTotalTokens, setLatestTotalTokens] = useState(0);
181
185
  const [maxInputTokens, setMaxInputTokens] = useState(200000);
182
186
 
183
- const throttledSetMessages = useMemo(
187
+ // Throttled incremental streaming updaters — 500ms leading+trailing, the same interval
188
+ // as the pre-incremental throttledSetMessages. `stage === "end"` flushes the final
189
+ // update immediately so completion results are never delayed.
190
+ const throttledContentUpdate = useMemo(
184
191
  () =>
185
192
  throttle(
186
- () => {
187
- if (!isExpandedRef.current && agentRef.current) {
188
- const msgs = [...agentRef.current.messages];
189
- setMessages(msgs);
190
- setLatestTotalTokens(extractLatestTotalTokens(msgs));
191
- }
193
+ (params: {
194
+ messageId: string;
195
+ accumulated: string;
196
+ stage: "streaming" | "end";
197
+ }) => {
198
+ const { messageId, accumulated, stage } = params;
199
+ setMessages((prev) =>
200
+ prev.map((m) => {
201
+ if (m.id !== messageId) return m;
202
+ const textBlockIndex = m.blocks.findIndex(
203
+ (b) => b.type === "text",
204
+ );
205
+ if (textBlockIndex === -1) {
206
+ return {
207
+ ...m,
208
+ blocks: [
209
+ ...m.blocks,
210
+ { type: "text", content: accumulated, stage },
211
+ ],
212
+ };
213
+ }
214
+ return {
215
+ ...m,
216
+ blocks: m.blocks.map((b, idx) =>
217
+ idx === textBlockIndex && b.type === "text"
218
+ ? { ...b, content: accumulated, stage }
219
+ : b,
220
+ ),
221
+ };
222
+ }),
223
+ );
224
+ },
225
+ 500,
226
+ ),
227
+ [],
228
+ );
229
+
230
+ const throttledReasoningUpdate = useMemo(
231
+ () =>
232
+ throttle(
233
+ (params: {
234
+ messageId: string;
235
+ accumulated: string;
236
+ stage: "streaming" | "end";
237
+ }) => {
238
+ const { messageId, accumulated, stage } = params;
239
+ setMessages((prev) =>
240
+ prev.map((m) => {
241
+ if (m.id !== messageId) return m;
242
+ const reasoningBlockIndex = m.blocks.findIndex(
243
+ (b) => b.type === "reasoning",
244
+ );
245
+ if (reasoningBlockIndex === -1) {
246
+ return {
247
+ ...m,
248
+ blocks: [
249
+ ...m.blocks,
250
+ { type: "reasoning", content: accumulated, stage },
251
+ ],
252
+ };
253
+ }
254
+ return {
255
+ ...m,
256
+ blocks: m.blocks.map((b, idx) =>
257
+ idx === reasoningBlockIndex && b.type === "reasoning"
258
+ ? { ...b, content: accumulated, stage }
259
+ : b,
260
+ ),
261
+ };
262
+ }),
263
+ );
192
264
  },
193
265
  500,
194
- { leading: true, trailing: true },
195
266
  ),
196
267
  [],
197
268
  );
198
269
 
270
+ const throttledToolBlockUpdate = useMemo(
271
+ () =>
272
+ throttle((params: ToolBlockUpdateCallbackParams) => {
273
+ const { messageId, id: toolBlockId, ...updates } = params;
274
+ setMessages((prev) =>
275
+ prev.map((m) => {
276
+ if (m.id !== messageId) return m;
277
+ const toolBlockIndex = m.blocks.findIndex(
278
+ (b) => b.type === "tool" && b.id === toolBlockId,
279
+ );
280
+ if (toolBlockIndex === -1) {
281
+ return {
282
+ ...m,
283
+ blocks: [
284
+ ...m.blocks,
285
+ {
286
+ type: "tool",
287
+ id: toolBlockId,
288
+ name: updates.name || "",
289
+ stage: updates.stage || "start",
290
+ parameters: updates.parameters || "",
291
+ result: updates.result || "",
292
+ ...updates,
293
+ },
294
+ ],
295
+ };
296
+ }
297
+ return {
298
+ ...m,
299
+ blocks: m.blocks.map((b, idx) =>
300
+ idx === toolBlockIndex && b.type === "tool"
301
+ ? { ...b, ...updates }
302
+ : b,
303
+ ),
304
+ };
305
+ }),
306
+ );
307
+ }, 500),
308
+ [],
309
+ );
310
+
199
311
  useEffect(() => {
200
312
  isExpandedRef.current = isExpanded;
201
313
  if (isExpanded) {
202
- throttledSetMessages.cancel();
314
+ // Cancel pending throttled updates so the frozen expanded view isn't overwritten
315
+ throttledContentUpdate.cancel();
316
+ throttledReasoningUpdate.cancel();
317
+ throttledToolBlockUpdate.cancel();
203
318
  }
204
- }, [isExpanded, throttledSetMessages]);
205
-
206
- useEffect(() => {
207
- return () => {
208
- throttledSetMessages.cancel();
209
- };
210
- }, [throttledSetMessages]);
319
+ }, [
320
+ isExpanded,
321
+ throttledContentUpdate,
322
+ throttledReasoningUpdate,
323
+ throttledToolBlockUpdate,
324
+ ]);
211
325
 
212
326
  const [isLoading, setIsLoading] = useState(false);
213
327
  const [sessionId, setSessionId] = useState("");
@@ -216,28 +330,6 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
216
330
  const [currentModel, setCurrentModelState] = useState("");
217
331
  const [configuredModels, setConfiguredModels] = useState<string[]>([]);
218
332
  const [queuedMessages, setQueuedMessages] = useState<QueuedMessage[]>([]);
219
- const [isGoalActive, setIsGoalActive] = useState(false);
220
- const [goalElapsed, setGoalElapsed] = useState<string | undefined>();
221
- const [isGoalEvaluating, setIsGoalEvaluating] = useState(false);
222
- const goalStartedAt = useRef<number | null>(null);
223
-
224
- // Update goal elapsed time every 30s while active
225
- useEffect(() => {
226
- if (!isGoalActive || goalStartedAt.current === null) return;
227
- const formatElapsed = (ms: number): string => {
228
- const minutes = Math.floor(ms / 60000);
229
- if (minutes < 1) return "<1m";
230
- if (minutes < 60) return `${minutes}m`;
231
- const hours = Math.floor(minutes / 60);
232
- const remainingMin = minutes % 60;
233
- return `${hours}h${remainingMin}m`;
234
- };
235
- const update = () =>
236
- setGoalElapsed(formatElapsed(Date.now() - goalStartedAt.current!));
237
- update();
238
- const timer = setInterval(update, 30_000);
239
- return () => clearInterval(timer);
240
- }, [isGoalActive]);
241
333
 
242
334
  // MCP State
243
335
  const [mcpServerStatuses, setMcpServerStatuses] = useState<McpServerStatus[]>(
@@ -295,48 +387,32 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
295
387
 
296
388
  // Remount state
297
389
  const [remountKey, setRemountKey] = useState(0);
298
- const prevSessionId = useRef<string | null>(null);
299
390
 
300
- const requestRemount = useMemo(
301
- () =>
302
- throttle(
303
- () => {
304
- logger.debug("requesting remount");
305
- stdout?.write("\u001b[2J\u001b[3J\u001b[0;0H", () => {
306
- setRemountKey((prev) => prev + 1);
307
- });
308
- },
309
- 1000,
310
- { leading: true, trailing: false },
311
- ),
312
- [stdout],
313
- );
314
-
315
- useEffect(() => {
316
- return () => {
317
- requestRemount.cancel();
318
- };
319
- }, [requestRemount]);
320
-
321
- // Track sessionId changes to trigger remount
322
- useEffect(() => {
323
- if (
324
- prevSessionId.current &&
325
- sessionId &&
326
- prevSessionId.current !== sessionId
327
- ) {
328
- requestRemount();
329
- }
330
- if (sessionId) {
331
- prevSessionId.current = sessionId;
332
- }
333
- }, [sessionId, requestRemount]);
391
+ // Full terminal clear + remount so Ink's append-only <Static> re-renders.
392
+ // Used on structural actions (/clear, /compact, rewind, ctrl-o, forceStatic
393
+ // exit) where stale Static output must not linger on screen.
394
+ const forceRemount = useCallback(() => {
395
+ stdout?.write("\u001b[2J\u001b[3J\u001b[0;0H", () => {
396
+ setRemountKey((prev) => prev + 1);
397
+ });
398
+ }, [stdout]);
334
399
 
335
400
  // Status metadata state
336
401
  const [workingDirectory, setWorkingDirectory] = useState("");
337
402
 
338
403
  const agentRef = useRef<Agent | null>(null);
339
404
 
405
+ // Full-list refresh — one-shot pull from the agent, used only for structural
406
+ // changes (compact/clear/rewind/collapse/init). Streaming updates flow through
407
+ // the incremental callbacks in initializeAgent below.
408
+ const refreshMessages = useCallback(() => {
409
+ if (!isExpandedRef.current && agentRef.current) {
410
+ const msgs = [...agentRef.current.messages];
411
+ setMessages(msgs);
412
+ setLatestTotalTokens(extractLatestTotalTokens(msgs));
413
+ }
414
+ }, []);
415
+
340
416
  // Permission confirmation methods with queue support
341
417
  const showConfirmation = useCallback(
342
418
  async (
@@ -371,8 +447,129 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
371
447
  restoreSessionIdOverride ?? restoreSessionId;
372
448
 
373
449
  const callbacks: AgentCallbacks = {
374
- onMessagesChange: () => {
375
- throttledSetMessages();
450
+ // ── Incremental message updates (no full-list pushes) ──────
451
+ onUserMessageAdded: () => {
452
+ if (isExpandedRef.current || !agentRef.current) return;
453
+ const msgs = agentRef.current.messages;
454
+ const last = msgs[msgs.length - 1];
455
+ if (!last || last.role !== "user") return;
456
+ setMessages((prev) =>
457
+ prev.some((m) => m.id === last.id) ? prev : [...prev, last],
458
+ );
459
+ },
460
+ onAssistantMessageAdded: (messageId: string) => {
461
+ if (isExpandedRef.current || !agentRef.current) return;
462
+ const msg = agentRef.current.messages.find((m) => m.id === messageId);
463
+ if (!msg) return;
464
+ setMessages((prev) =>
465
+ prev.some((m) => m.id === messageId) ? prev : [...prev, msg],
466
+ );
467
+ },
468
+ onAssistantContentUpdated: (params) => {
469
+ if (isExpandedRef.current) return;
470
+ throttledContentUpdate(params);
471
+ if (params.stage === "end") throttledContentUpdate.flush();
472
+ },
473
+ onAssistantReasoningUpdated: (params) => {
474
+ if (isExpandedRef.current) return;
475
+ throttledReasoningUpdate(params);
476
+ if (params.stage === "end") throttledReasoningUpdate.flush();
477
+ },
478
+ onToolBlockUpdated: (params) => {
479
+ if (isExpandedRef.current) return;
480
+ throttledToolBlockUpdate(params);
481
+ if (params.stage === "end") throttledToolBlockUpdate.flush();
482
+ },
483
+ onErrorBlockAdded: (error: string) => {
484
+ if (isExpandedRef.current) return;
485
+ setMessages((prev) => {
486
+ // Append to the last assistant message, or create one if none exists
487
+ for (let i = prev.length - 1; i >= 0; i--) {
488
+ if (prev[i].role === "assistant") {
489
+ return prev.map((m, idx) =>
490
+ idx === i
491
+ ? {
492
+ ...m,
493
+ blocks: [
494
+ ...m.blocks,
495
+ { type: "error", content: error },
496
+ ],
497
+ }
498
+ : m,
499
+ );
500
+ }
501
+ }
502
+ return [
503
+ ...prev,
504
+ {
505
+ id: `msg-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`,
506
+ role: "assistant",
507
+ timestamp: new Date().toISOString(),
508
+ blocks: [{ type: "error", content: error }],
509
+ },
510
+ ];
511
+ });
512
+ },
513
+ onAddBangMessage: (command, messageId) => {
514
+ if (isExpandedRef.current) return;
515
+ setMessages((prev) =>
516
+ prev.some((m) => m.id === messageId)
517
+ ? prev
518
+ : [
519
+ ...prev,
520
+ {
521
+ id: messageId,
522
+ role: "user",
523
+ timestamp: new Date().toISOString(),
524
+ blocks: [
525
+ {
526
+ type: "bang",
527
+ command,
528
+ output: "",
529
+ stage: "running",
530
+ exitCode: null,
531
+ },
532
+ ],
533
+ },
534
+ ],
535
+ );
536
+ },
537
+ onUpdateBangMessage: (command, output, messageId) => {
538
+ if (isExpandedRef.current) return;
539
+ setMessages((prev) =>
540
+ prev.map((m) =>
541
+ m.id === messageId
542
+ ? {
543
+ ...m,
544
+ blocks: m.blocks.map((b, idx) =>
545
+ idx === m.blocks.length - 1 && b.type === "bang"
546
+ ? { ...b, command, output }
547
+ : b,
548
+ ),
549
+ }
550
+ : m,
551
+ ),
552
+ );
553
+ },
554
+ onCompleteBangMessage: (command, exitCode, messageId) => {
555
+ if (isExpandedRef.current) return;
556
+ setMessages((prev) =>
557
+ prev.map((m) =>
558
+ m.id === messageId
559
+ ? {
560
+ ...m,
561
+ blocks: m.blocks.map((b, idx) =>
562
+ idx === m.blocks.length - 1 && b.type === "bang"
563
+ ? { ...b, command, exitCode, stage: "end" }
564
+ : b,
565
+ ),
566
+ }
567
+ : m,
568
+ ),
569
+ );
570
+ },
571
+ onLatestTotalTokensChange: (tokens) => {
572
+ setLatestTotalTokens(tokens);
376
573
  },
377
574
  onMcpServersChange: (servers) => {
378
575
  setMcpServerStatuses([...servers]);
@@ -421,18 +618,6 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
421
618
  onQueuedMessagesChange: (messages) => {
422
619
  setQueuedMessages([...messages]);
423
620
  },
424
- onGoalStateChange: (active, _condition, elapsed) => {
425
- setIsGoalActive(active);
426
- if (active) {
427
- goalStartedAt.current = Date.now();
428
- } else {
429
- goalStartedAt.current = null;
430
- }
431
- setGoalElapsed(elapsed);
432
- },
433
- onGoalEvaluating: (evaluating) => {
434
- setIsGoalEvaluating(evaluating);
435
- },
436
621
  };
437
622
 
438
623
  try {
@@ -533,7 +718,10 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
533
718
  originalCwd,
534
719
  model,
535
720
  initialPermissionMode,
536
- throttledSetMessages,
721
+ refreshMessages,
722
+ throttledContentUpdate,
723
+ throttledReasoningUpdate,
724
+ throttledToolBlockUpdate,
537
725
  mcpServers,
538
726
  ],
539
727
  );
@@ -571,6 +759,9 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
571
759
  // Cleanup on unmount
572
760
  useEffect(() => {
573
761
  return () => {
762
+ throttledContentUpdate.cancel();
763
+ throttledReasoningUpdate.cancel();
764
+ throttledToolBlockUpdate.cancel();
574
765
  if (agentRef.current) {
575
766
  try {
576
767
  // Display usage summary before cleanup
@@ -584,7 +775,11 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
584
775
  agentRef.current.destroy();
585
776
  }
586
777
  };
587
- }, []);
778
+ }, [
779
+ throttledContentUpdate,
780
+ throttledReasoningUpdate,
781
+ throttledToolBlockUpdate,
782
+ ]);
588
783
 
589
784
  // Trigger WorktreeRemove hook BEFORE agent destruction
590
785
  const triggerWorktreeRemoveHook = useCallback(
@@ -637,33 +832,34 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
637
832
  [],
638
833
  );
639
834
 
640
- const askBtw = useCallback(async (question: string) => {
641
- if (!agentRef.current) {
642
- throw new Error("Agent not initialized");
643
- }
644
- return await agentRef.current.askBtw(question);
645
- }, []);
835
+ const askBtw = useCallback(
836
+ async (
837
+ question: string,
838
+ abortSignal?: AbortSignal,
839
+ onContent?: (content: string) => void,
840
+ ) => {
841
+ if (!agentRef.current) {
842
+ throw new Error("Agent not initialized");
843
+ }
844
+ return await agentRef.current.askBtw(question, abortSignal, onContent);
845
+ },
846
+ [],
847
+ );
646
848
 
647
849
  const clearMessages = useCallback(async () => {
648
850
  await agentRef.current?.clearMessages();
649
- }, []);
650
-
651
- const compact = useCallback(async (instructions?: string) => {
652
- await agentRef.current?.compact(instructions);
653
- }, []);
654
-
655
- const goalCommand = useCallback(async (args?: string) => {
656
- const trimmed = args?.trim() ?? "";
657
- if (!trimmed) {
658
- await agentRef.current?.showGoalStatus();
659
- } else if (
660
- ["clear", "stop", "off", "reset", "none", "cancel"].includes(trimmed)
661
- ) {
662
- await agentRef.current?.clearGoal();
663
- } else {
664
- await agentRef.current?.setGoal(trimmed);
665
- }
666
- }, []);
851
+ refreshMessages();
852
+ forceRemount();
853
+ }, [refreshMessages, forceRemount]);
854
+
855
+ const compact = useCallback(
856
+ async (instructions?: string) => {
857
+ await agentRef.current?.compact(instructions);
858
+ refreshMessages();
859
+ forceRemount();
860
+ },
861
+ [refreshMessages, forceRemount],
862
+ );
667
863
 
668
864
  // Unified interrupt method, interrupt both AI messages and command execution
669
865
  const abortMessage = useCallback(() => {
@@ -773,13 +969,14 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
773
969
  if (agentRef.current) {
774
970
  try {
775
971
  await agentRef.current.truncateHistory(index);
776
- requestRemount();
972
+ refreshMessages();
973
+ forceRemount();
777
974
  } catch (error) {
778
975
  logger.error("Failed to rewind:", error);
779
976
  }
780
977
  }
781
978
  },
782
- [requestRemount],
979
+ [forceRemount, refreshMessages],
783
980
  );
784
981
 
785
982
  const getFullMessageThread = useCallback(async () => {
@@ -827,22 +1024,13 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
827
1024
 
828
1025
  if (nextExpanded) {
829
1026
  // Transitioning to EXPANDED: Freeze the current view
830
- // Cancel any pending throttled updates to avoid overwriting the frozen state
831
- throttledSetMessages.cancel();
1027
+ // Incremental updates are skipped while expanded (isExpandedRef guard)
832
1028
  } else {
833
1029
  // Transitioning to COLLAPSED: Restore from agent's actual state
834
- if (agentRef.current) {
835
- const msgs = [...agentRef.current.messages];
836
- setMessages(msgs);
837
- setLatestTotalTokens(extractLatestTotalTokens(msgs));
838
- }
1030
+ refreshMessages();
839
1031
  }
840
- // Force remount directly (bypass throttle) to ensure Static items re-render
841
- // The throttled requestRemount can be dropped if pressed too quickly after
842
- // a previous remount, leaving the UI stuck without a visual update
843
- stdout?.write("\u001b[2J\u001b[3J\u001b[0;0H", () => {
844
- setRemountKey((prev) => prev + 1);
845
- });
1032
+ // Force remount to ensure Static items re-render
1033
+ forceRemount();
846
1034
  }
847
1035
 
848
1036
  if (key.ctrl && input === "t") {
@@ -862,13 +1050,14 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
862
1050
  isExpanded,
863
1051
  isTaskListVisible,
864
1052
  setIsTaskListVisible,
1053
+ isBtwActive,
1054
+ setIsBtwActive,
865
1055
  queuedMessages,
866
1056
  sessionId,
867
1057
  sendMessage,
868
1058
  askBtw,
869
1059
  clearMessages,
870
1060
  compact,
871
- goalCommand,
872
1061
  abortMessage,
873
1062
  recallQueuedMessage,
874
1063
  removeQueuedMessageById,
@@ -901,7 +1090,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
901
1090
  handleConfirmationCancel,
902
1091
  backgroundCurrentTask,
903
1092
  remountKey,
904
- requestRemount: requestRemount as () => void,
1093
+ forceRemount,
905
1094
  handleRewindSelect,
906
1095
  getFullMessageThread,
907
1096
 
@@ -912,9 +1101,6 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
912
1101
  workdir,
913
1102
  recreateAgent,
914
1103
  triggerWorktreeRemoveHook,
915
- isGoalActive,
916
- goalElapsed,
917
- isGoalEvaluating,
918
1104
  };
919
1105
 
920
1106
  return (
@@ -0,0 +1,17 @@
1
+ /**
2
+ * daemon-cli.ts — Entry point for `wave --daemon <socket-path>` mode.
3
+ *
4
+ * Starts a DaemonServer that serves JSON-RPC over a unix socket. The desktop
5
+ * app launches this on a remote host via nohup/setsid and tunnels the socket
6
+ * back with `ssh -L`; multiple attach/detach cycles share one process, so
7
+ * sessions and pending permissions survive client disconnects. The net server
8
+ * keeps the process alive — there is no stdin to wait on.
9
+ */
10
+
11
+ import { DaemonServer } from "./stdio/daemonServer.js";
12
+
13
+ export async function startDaemonCli(socketPath: string): Promise<void> {
14
+ const server = new DaemonServer({ socketPath });
15
+ await server.start();
16
+ // Ready — any error that follows goes to the daemon log via stderr.
17
+ }