wave-code 0.12.0 → 0.12.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 (66) hide show
  1. package/README.md +9 -51
  2. package/dist/acp/agent.js +1 -1
  3. package/dist/components/App.d.ts +1 -0
  4. package/dist/components/App.d.ts.map +1 -1
  5. package/dist/components/App.js +5 -2
  6. package/dist/components/BackgroundTaskManager.d.ts.map +1 -1
  7. package/dist/components/BackgroundTaskManager.js +2 -1
  8. package/dist/components/BangDisplay.d.ts.map +1 -1
  9. package/dist/components/BangDisplay.js +2 -14
  10. package/dist/components/ChatInterface.d.ts.map +1 -1
  11. package/dist/components/ChatInterface.js +17 -27
  12. package/dist/components/ConfirmationDetails.d.ts +0 -1
  13. package/dist/components/ConfirmationDetails.d.ts.map +1 -1
  14. package/dist/components/ConfirmationDetails.js +4 -12
  15. package/dist/components/ConfirmationSelector.d.ts +0 -1
  16. package/dist/components/ConfirmationSelector.d.ts.map +1 -1
  17. package/dist/components/ConfirmationSelector.js +4 -12
  18. package/dist/components/DiffDisplay.d.ts.map +1 -1
  19. package/dist/components/DiffDisplay.js +2 -23
  20. package/dist/components/MessageList.d.ts +1 -2
  21. package/dist/components/MessageList.d.ts.map +1 -1
  22. package/dist/components/MessageList.js +12 -17
  23. package/dist/components/RewindCommand.d.ts.map +1 -1
  24. package/dist/components/RewindCommand.js +2 -4
  25. package/dist/components/StatusCommand.d.ts.map +1 -1
  26. package/dist/components/StatusCommand.js +1 -1
  27. package/dist/components/ToolDisplay.d.ts.map +1 -1
  28. package/dist/components/ToolDisplay.js +3 -6
  29. package/dist/contexts/useChat.d.ts +0 -2
  30. package/dist/contexts/useChat.d.ts.map +1 -1
  31. package/dist/contexts/useChat.js +27 -44
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +10 -0
  34. package/dist/utils/highlightUtils.d.ts.map +1 -1
  35. package/dist/utils/highlightUtils.js +0 -8
  36. package/dist/utils/logger.d.ts.map +1 -1
  37. package/dist/utils/logger.js +13 -11
  38. package/dist/utils/throttle.d.ts +15 -0
  39. package/dist/utils/throttle.d.ts.map +1 -0
  40. package/dist/utils/throttle.js +55 -0
  41. package/dist/utils/toolParameterTransforms.d.ts +5 -1
  42. package/dist/utils/toolParameterTransforms.d.ts.map +1 -1
  43. package/dist/utils/toolParameterTransforms.js +14 -0
  44. package/dist/utils/worktree.d.ts +0 -1
  45. package/dist/utils/worktree.d.ts.map +1 -1
  46. package/dist/utils/worktree.js +2 -2
  47. package/package.json +3 -3
  48. package/src/acp/agent.ts +1 -1
  49. package/src/components/App.tsx +6 -2
  50. package/src/components/BackgroundTaskManager.tsx +3 -6
  51. package/src/components/BangDisplay.tsx +4 -22
  52. package/src/components/ChatInterface.tsx +18 -33
  53. package/src/components/ConfirmationDetails.tsx +2 -15
  54. package/src/components/ConfirmationSelector.tsx +3 -15
  55. package/src/components/DiffDisplay.tsx +2 -27
  56. package/src/components/MessageList.tsx +17 -20
  57. package/src/components/RewindCommand.tsx +3 -5
  58. package/src/components/StatusCommand.tsx +7 -0
  59. package/src/components/ToolDisplay.tsx +3 -6
  60. package/src/contexts/useChat.tsx +34 -59
  61. package/src/index.ts +13 -0
  62. package/src/utils/highlightUtils.ts +0 -8
  63. package/src/utils/logger.ts +12 -14
  64. package/src/utils/throttle.ts +75 -0
  65. package/src/utils/toolParameterTransforms.ts +23 -1
  66. package/src/utils/worktree.ts +7 -3
@@ -1,5 +1,5 @@
1
- import React, { useState, useCallback, useLayoutEffect } from "react";
2
- import { Box, useStdout } from "ink";
1
+ import React, { useState, useCallback, useLayoutEffect, useRef } from "react";
2
+ import { Box, useStdout, measureElement } from "ink";
3
3
  import { MessageList } from "./MessageList.js";
4
4
  import { BtwDisplay } from "./BtwDisplay.js";
5
5
  import { InputBox } from "./InputBox.js";
@@ -14,9 +14,6 @@ import type { PermissionDecision } from "wave-agent-sdk";
14
14
 
15
15
  export const ChatInterface: React.FC = () => {
16
16
  const { stdout } = useStdout();
17
- const [detailsHeight, setDetailsHeight] = useState(0);
18
- const [selectorHeight, setSelectorHeight] = useState(0);
19
- const [dynamicBlocksHeight, setDynamicBlocksHeight] = useState(0);
20
17
  const [isConfirmationTooTall, setIsConfirmationTooTall] = useState(false);
21
18
 
22
19
  const {
@@ -44,26 +41,11 @@ export const ChatInterface: React.FC = () => {
44
41
  btwState,
45
42
  } = useChat();
46
43
 
47
- const displayMessages = messages;
48
-
49
- const handleDetailsHeightMeasured = useCallback((height: number) => {
50
- setDetailsHeight(height);
51
- }, []);
52
-
53
- const handleSelectorHeightMeasured = useCallback((height: number) => {
54
- setSelectorHeight(height);
55
- }, []);
56
-
57
- const handleDynamicBlocksHeightMeasured = useCallback((height: number) => {
58
- setDynamicBlocksHeight(height);
59
- }, []);
44
+ const interfaceRef = useRef(null);
60
45
 
61
46
  useLayoutEffect(() => {
62
47
  if (!isConfirmationVisible) {
63
48
  setIsConfirmationTooTall(false);
64
- setDetailsHeight(0);
65
- setSelectorHeight(0);
66
- setDynamicBlocksHeight(0);
67
49
  return;
68
50
  }
69
51
 
@@ -71,20 +53,26 @@ export const ChatInterface: React.FC = () => {
71
53
  return;
72
54
  }
73
55
 
74
- const terminalHeight = stdout?.rows || 24;
75
- const totalHeight = detailsHeight + selectorHeight + dynamicBlocksHeight;
76
- if (totalHeight > terminalHeight - 3) {
77
- setIsConfirmationTooTall(true);
56
+ if (interfaceRef.current) {
57
+ const { height } = measureElement(interfaceRef.current);
58
+ const terminalHeight = stdout?.rows || 24;
59
+ if (height > terminalHeight - 3) {
60
+ setIsConfirmationTooTall(true);
61
+ }
78
62
  }
79
63
  }, [
80
- detailsHeight,
81
- selectorHeight,
82
- dynamicBlocksHeight,
83
- stdout?.rows,
64
+ messages,
65
+ isLoading,
66
+ isCommandRunning,
67
+ isCompressing,
68
+ isExpanded,
84
69
  isConfirmationVisible,
85
70
  isConfirmationTooTall,
71
+ stdout?.rows,
86
72
  ]);
87
73
 
74
+ const displayMessages = messages;
75
+
88
76
  const handleConfirmationCancel = useCallback(() => {
89
77
  if (isConfirmationTooTall) {
90
78
  setWasLastDetailsTooTall((prev) => prev + 1);
@@ -115,14 +103,13 @@ export const ChatInterface: React.FC = () => {
115
103
  if (!sessionId) return null;
116
104
 
117
105
  return (
118
- <Box flexDirection="column">
106
+ <Box ref={interfaceRef} flexDirection="column">
119
107
  <MessageList
120
108
  messages={displayMessages}
121
109
  isExpanded={isExpanded}
122
110
  forceStatic={isConfirmationVisible && isConfirmationTooTall}
123
111
  version={version}
124
112
  workdir={workdir}
125
- onDynamicBlocksHeightMeasured={handleDynamicBlocksHeightMeasured}
126
113
  />
127
114
 
128
115
  {!isConfirmationVisible && !isExpanded && (
@@ -159,7 +146,6 @@ export const ChatInterface: React.FC = () => {
159
146
  toolInput={confirmingTool!.input}
160
147
  planContent={confirmingTool!.planContent}
161
148
  isExpanded={isExpanded}
162
- onHeightMeasured={handleDetailsHeightMeasured}
163
149
  isStatic={isConfirmationTooTall}
164
150
  />
165
151
  <ConfirmationSelector
@@ -171,7 +157,6 @@ export const ChatInterface: React.FC = () => {
171
157
  onDecision={wrappedHandleConfirmationDecision}
172
158
  onCancel={handleConfirmationCancel}
173
159
  onAbort={abortMessage}
174
- onHeightMeasured={handleSelectorHeightMeasured}
175
160
  />
176
161
  </>
177
162
  )}
@@ -1,5 +1,5 @@
1
- import React, { useLayoutEffect, useRef } from "react";
2
- import { Box, Text, useStdout, measureElement, Static } from "ink";
1
+ import React from "react";
2
+ import { Box, Text, Static } from "ink";
3
3
  import {
4
4
  BASH_TOOL_NAME,
5
5
  EDIT_TOOL_NAME,
@@ -41,7 +41,6 @@ export interface ConfirmationDetailsProps {
41
41
  toolInput?: Record<string, unknown>;
42
42
  planContent?: string;
43
43
  isExpanded?: boolean;
44
- onHeightMeasured?: (height: number) => void;
45
44
  isStatic?: boolean;
46
45
  }
47
46
 
@@ -50,26 +49,14 @@ export const ConfirmationDetails: React.FC<ConfirmationDetailsProps> = ({
50
49
  toolInput,
51
50
  planContent,
52
51
  isExpanded = false,
53
- onHeightMeasured,
54
52
  isStatic = false,
55
53
  }) => {
56
- const { stdout } = useStdout();
57
- const boxRef = useRef(null);
58
-
59
54
  const startLineNumber =
60
55
  (toolInput?.startLineNumber as number | undefined) ??
61
56
  (toolName === WRITE_TOOL_NAME ? 1 : undefined);
62
57
 
63
- useLayoutEffect(() => {
64
- if (boxRef.current) {
65
- const { height } = measureElement(boxRef.current);
66
- onHeightMeasured?.(height);
67
- }
68
- }, [stdout?.rows, onHeightMeasured, toolInput, isExpanded]);
69
-
70
58
  const content = (
71
59
  <Box
72
- ref={boxRef}
73
60
  flexDirection="column"
74
61
  borderStyle="single"
75
62
  borderColor="yellow"
@@ -1,5 +1,5 @@
1
- import React, { useEffect, useLayoutEffect, useRef, useState } from "react";
2
- import { Box, Text, useInput, useStdout, measureElement } from "ink";
1
+ import React, { useEffect, useRef, useState } from "react";
2
+ import { Box, Text, useInput } from "ink";
3
3
  import type { PermissionDecision, AskUserQuestionInput } from "wave-agent-sdk";
4
4
  import {
5
5
  BASH_TOOL_NAME,
@@ -25,7 +25,6 @@ export interface ConfirmationSelectorProps {
25
25
  onDecision: (decision: PermissionDecision) => void;
26
26
  onCancel: () => void;
27
27
  onAbort: () => void;
28
- onHeightMeasured?: (height: number) => void;
29
28
  }
30
29
 
31
30
  interface ConfirmationState {
@@ -44,18 +43,7 @@ export const ConfirmationSelector: React.FC<ConfirmationSelectorProps> = ({
44
43
  onDecision,
45
44
  onCancel,
46
45
  onAbort,
47
- onHeightMeasured,
48
46
  }) => {
49
- const { stdout } = useStdout();
50
- const boxRef = useRef(null);
51
-
52
- useLayoutEffect(() => {
53
- if (boxRef.current) {
54
- const { height } = measureElement(boxRef.current);
55
- onHeightMeasured?.(height);
56
- }
57
- }, [stdout?.rows, onHeightMeasured, toolName, toolInput, isExpanded]);
58
-
59
47
  const [state, setState] = useState<ConfirmationState>({
60
48
  selectedOption: toolName === EXIT_PLAN_MODE_TOOL_NAME ? "clear" : "allow",
61
49
  alternativeText: "",
@@ -497,7 +485,7 @@ export const ConfirmationSelector: React.FC<ConfirmationSelectorProps> = ({
497
485
  state.selectedOption === "alternative" && !state.hasUserInput;
498
486
 
499
487
  return (
500
- <Box ref={boxRef} flexDirection="column">
488
+ <Box flexDirection="column">
501
489
  {toolName === ASK_USER_QUESTION_TOOL_NAME &&
502
490
  currentQuestion &&
503
491
  !isExpanded && (
@@ -1,14 +1,8 @@
1
1
  import React, { useMemo } from "react";
2
2
  import { Box, Text } from "ink";
3
- import {
4
- WRITE_TOOL_NAME,
5
- EDIT_TOOL_NAME,
6
- type WriteToolParameters,
7
- } from "wave-agent-sdk";
3
+ import { WRITE_TOOL_NAME, EDIT_TOOL_NAME } from "wave-agent-sdk";
8
4
  import { transformToolBlockToChanges } from "../utils/toolParameterTransforms.js";
9
5
  import { diffLines, diffWords } from "diff";
10
- import path from "path";
11
- import { Markdown } from "./Markdown.js";
12
6
 
13
7
  interface DiffDisplayProps {
14
8
  toolName?: string;
@@ -103,23 +97,6 @@ export const DiffDisplay: React.FC<DiffDisplayProps> = ({
103
97
  }
104
98
  };
105
99
 
106
- // Render highlighted code for Write tool
107
- const renderWriteContent = () => {
108
- if (!parameters) return null;
109
- try {
110
- const parsed = JSON.parse(parameters) as WriteToolParameters;
111
- const content = parsed.content || "";
112
- const filePath = parsed.file_path || "";
113
- const ext = path.extname(filePath).slice(1);
114
-
115
- const markdown = `\`\`\`${ext}\n${content}\n\`\`\``;
116
- return <Markdown>{markdown}</Markdown>;
117
- } catch (error) {
118
- console.warn("Error rendering write content:", error);
119
- return null;
120
- }
121
- };
122
-
123
100
  // Render expanded diff display
124
101
  const renderExpandedDiff = () => {
125
102
  try {
@@ -375,9 +352,7 @@ export const DiffDisplay: React.FC<DiffDisplayProps> = ({
375
352
  return (
376
353
  <Box flexDirection="column">
377
354
  <Box paddingLeft={2} borderLeft borderColor="cyan" flexDirection="column">
378
- {toolName === WRITE_TOOL_NAME
379
- ? renderWriteContent()
380
- : renderExpandedDiff()}
355
+ {renderExpandedDiff()}
381
356
  </Box>
382
357
  </Box>
383
358
  );
@@ -1,7 +1,7 @@
1
- import React, { useLayoutEffect, useRef } from "react";
1
+ import React from "react";
2
2
  import os from "os";
3
- import { Box, Text, Static, measureElement } from "ink";
4
- import type { Message } from "wave-agent-sdk";
3
+ import { Box, Text, Static } from "ink";
4
+ import type { Message, MessageBlock } from "wave-agent-sdk";
5
5
  import { MessageBlockItem } from "./MessageBlockItem.js";
6
6
 
7
7
  export interface MessageListProps {
@@ -10,7 +10,6 @@ export interface MessageListProps {
10
10
  forceStatic?: boolean;
11
11
  version?: string;
12
12
  workdir?: string;
13
- onDynamicBlocksHeightMeasured?: (height: number) => void;
14
13
  }
15
14
 
16
15
  export const MessageList = React.memo(
@@ -20,7 +19,6 @@ export const MessageList = React.memo(
20
19
  forceStatic = false,
21
20
  version,
22
21
  workdir,
23
- onDynamicBlocksHeightMeasured,
24
22
  }: MessageListProps) => {
25
23
  const welcomeMessage = (
26
24
  <Box flexDirection="column" paddingTop={1}>
@@ -39,6 +37,18 @@ export const MessageList = React.memo(
39
37
  // Filter out meta messages
40
38
  const visibleMessages = messages.filter((m) => !m.isMeta);
41
39
 
40
+ const isRunning = (b: MessageBlock) =>
41
+ (b.type === "tool" && b.stage === "running") ||
42
+ (b.type === "bang" && b.isRunning) ||
43
+ (b.type === "slash" && b.stage === "running");
44
+
45
+ const messagesWithRunningBlocks = new Set(
46
+ visibleMessages
47
+ .map((m, i) => ({ m, i }))
48
+ .filter(({ m }) => m.blocks.some(isRunning))
49
+ .map(({ i }) => i),
50
+ );
51
+
42
52
  // Flatten messages into blocks with metadata
43
53
  const allBlocks = visibleMessages.flatMap((message, messageIndex) => {
44
54
  return message.blocks.map((block, blockIndex) => ({
@@ -55,26 +65,13 @@ export const MessageList = React.memo(
55
65
  const { block } = item;
56
66
  const isDynamic =
57
67
  !forceStatic &&
58
- ((block.type === "tool" && block.stage === "running") ||
59
- (block.type === "bang" && block.isRunning) ||
60
- (block.type === "slash" && block.stage === "running"));
68
+ (messagesWithRunningBlocks.has(item.messageIndex) || isRunning(block));
61
69
  return { ...item, isDynamic };
62
70
  });
63
71
 
64
72
  const staticBlocks = blocksWithStatus.filter((b) => !b.isDynamic);
65
73
  const dynamicBlocks = blocksWithStatus.filter((b) => b.isDynamic);
66
74
 
67
- const dynamicBlocksRef = useRef(null);
68
-
69
- useLayoutEffect(() => {
70
- if (dynamicBlocksRef.current) {
71
- const { height } = measureElement(dynamicBlocksRef.current);
72
- onDynamicBlocksHeightMeasured?.(height);
73
- } else {
74
- onDynamicBlocksHeightMeasured?.(0);
75
- }
76
- }, [dynamicBlocks, isExpanded, onDynamicBlocksHeightMeasured]);
77
-
78
75
  const staticItems = [
79
76
  {
80
77
  isWelcome: true,
@@ -120,7 +117,7 @@ export const MessageList = React.memo(
120
117
 
121
118
  {/* Dynamic blocks */}
122
119
  {dynamicBlocks.length > 0 && (
123
- <Box ref={dynamicBlocksRef} flexDirection="column">
120
+ <Box flexDirection="column">
124
121
  {dynamicBlocks.map((item) => (
125
122
  <MessageBlockItem
126
123
  key={item.key}
@@ -1,6 +1,7 @@
1
1
  import React, { useState } from "react";
2
2
  import { Box, Text, useInput } from "ink";
3
- import type { Message, TextBlock } from "wave-agent-sdk";
3
+ import type { Message } from "wave-agent-sdk";
4
+ import { getMessageContent } from "wave-agent-sdk";
4
5
 
5
6
  export interface RewindCommandProps {
6
7
  messages: Message[];
@@ -131,10 +132,7 @@ export const RewindCommand: React.FC<RewindCommandProps> = ({
131
132
  {visibleCheckpoints.map((checkpoint, index) => {
132
133
  const actualIndex = startIndex + index;
133
134
  const isSelected = actualIndex === selectedIndex;
134
- const content = checkpoint.msg.blocks
135
- .filter((b): b is TextBlock => b.type === "text")
136
- .map((b) => b.content)
137
- .join(" ")
135
+ const content = getMessageContent(checkpoint.msg)
138
136
  .replace(/\n/g, "\\n")
139
137
  .substring(0, 60);
140
138
 
@@ -86,6 +86,13 @@ export const StatusCommand: React.FC<StatusCommandProps> = ({ onCancel }) => {
86
86
  <Text color="white">{modelConfig.model}</Text>
87
87
  </Box>
88
88
 
89
+ <Box>
90
+ <Box width={20}>
91
+ <Text color="yellow">Fast model:</Text>
92
+ </Box>
93
+ <Text color="white">{modelConfig.fastModel}</Text>
94
+ </Box>
95
+
89
96
  <Box marginTop={1}>
90
97
  <Text dimColor>Esc to cancel</Text>
91
98
  </Box>
@@ -1,6 +1,7 @@
1
1
  import React from "react";
2
2
  import { Box, Text } from "ink";
3
3
  import type { ToolBlock } from "wave-agent-sdk";
4
+ import { getLastLines } from "wave-agent-sdk";
4
5
  import { DiffDisplay } from "./DiffDisplay.js";
5
6
 
6
7
  interface ToolDisplayProps {
@@ -45,11 +46,7 @@ export const ToolDisplay: React.FC<ToolDisplayProps> = ({
45
46
 
46
47
  // If no shortResult but has result, return last 5 lines
47
48
  if (block.result) {
48
- const lines = block.result.split("\n");
49
- if (lines.length > 5) {
50
- return lines.slice(-5).join("\n");
51
- }
52
- return block.result;
49
+ return getLastLines(block.result, 5);
53
50
  }
54
51
 
55
52
  return null;
@@ -81,7 +78,7 @@ export const ToolDisplay: React.FC<ToolDisplayProps> = ({
81
78
  flexDirection="column"
82
79
  >
83
80
  {shortResult.split("\n").map((line, index) => (
84
- <Text key={index} color="gray">
81
+ <Text key={index} color="gray" wrap="truncate-end">
85
82
  {line}
86
83
  </Text>
87
84
  ))}
@@ -5,6 +5,7 @@ import React, {
5
5
  useRef,
6
6
  useEffect,
7
7
  useState,
8
+ useMemo,
8
9
  } from "react";
9
10
  import { useInput } from "ink";
10
11
  import { useAppConfig } from "./useAppConfig.js";
@@ -22,8 +23,10 @@ import {
22
23
  AgentCallbacks,
23
24
  type ToolPermissionContext,
24
25
  OPERATION_CANCELLED_BY_USER,
26
+ cloneMessage,
25
27
  } from "wave-agent-sdk";
26
28
  import { logger } from "../utils/logger.js";
29
+ import { throttle } from "../utils/throttle.js";
27
30
  import { displayUsageSummary } from "../utils/usageSummary.js";
28
31
  import { expandLongTextPlaceholders } from "../managers/inputHandlers.js";
29
32
 
@@ -77,9 +80,6 @@ export interface ChatContextType {
77
80
  // Slash Command functionality
78
81
  slashCommands: SlashCommand[];
79
82
  hasSlashCommand: (commandId: string) => boolean;
80
- // Subagent messages
81
- subagentMessages: Record<string, Message[]>;
82
- subagentLatestTokens: Record<string, number>;
83
83
  // Permission functionality
84
84
  permissionMode: PermissionMode;
85
85
  setPermissionMode: (mode: PermissionMode) => void;
@@ -159,10 +159,6 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
159
159
  const [isExpanded, setIsExpanded] = useState(false);
160
160
  const isExpandedRef = useRef(isExpanded);
161
161
 
162
- useEffect(() => {
163
- isExpandedRef.current = isExpanded;
164
- }, [isExpanded]);
165
-
166
162
  const [isTaskListVisible, setIsTaskListVisible] = useState(true);
167
163
 
168
164
  const [queuedMessages, setQueuedMessages] = useState<
@@ -173,9 +169,31 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
173
169
  }>
174
170
  >([]);
175
171
 
176
- // AI State
177
172
  const [messages, setMessages] = useState<Message[]>([]);
178
173
 
174
+ const throttledSetMessages = useMemo(
175
+ () =>
176
+ throttle(() => {
177
+ if (!isExpandedRef.current && agentRef.current) {
178
+ setMessages([...agentRef.current.messages]);
179
+ }
180
+ }, 300),
181
+ [],
182
+ );
183
+
184
+ useEffect(() => {
185
+ isExpandedRef.current = isExpanded;
186
+ if (isExpanded) {
187
+ throttledSetMessages.cancel();
188
+ }
189
+ }, [isExpanded, throttledSetMessages]);
190
+
191
+ useEffect(() => {
192
+ return () => {
193
+ throttledSetMessages.cancel();
194
+ };
195
+ }, [throttledSetMessages]);
196
+
179
197
  const [isLoading, setIsLoading] = useState(false);
180
198
  const [latestTotalTokens, setlatestTotalTokens] = useState(0);
181
199
  const [sessionId, setSessionId] = useState("");
@@ -195,14 +213,6 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
195
213
  // Command state
196
214
  const [slashCommands, setSlashCommands] = useState<SlashCommand[]>([]);
197
215
 
198
- // Subagent messages state
199
- const [subagentMessages, setSubagentMessages] = useState<
200
- Record<string, Message[]>
201
- >({});
202
- const [subagentLatestTokens, setSubagentLatestTokens] = useState<
203
- Record<string, number>
204
- >({});
205
-
206
216
  // Permission state
207
217
  const [permissionMode, setPermissionModeState] = useState<PermissionMode>(
208
218
  initialPermissionMode ||
@@ -276,20 +286,6 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
276
286
  !!bypassPermissions || initialPermissionMode === "bypassPermissions";
277
287
 
278
288
  const agentRef = useRef<Agent | null>(null);
279
- const taskUpdateTimerRef = useRef<NodeJS.Timeout | null>(null);
280
-
281
- const debouncedSetTasks = useCallback(
282
- (newTasks: Task[]) => {
283
- if (taskUpdateTimerRef.current) {
284
- clearTimeout(taskUpdateTimerRef.current);
285
- }
286
- taskUpdateTimerRef.current = setTimeout(() => {
287
- setTasks([...newTasks]);
288
- taskUpdateTimerRef.current = null;
289
- }, 100);
290
- },
291
- [setTasks],
292
- );
293
289
 
294
290
  // Permission confirmation methods with queue support
295
291
  const showConfirmation = useCallback(
@@ -323,9 +319,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
323
319
  const initializeAgent = async () => {
324
320
  const callbacks: AgentCallbacks = {
325
321
  onMessagesChange: () => {
326
- if (!isExpandedRef.current && agentRef.current) {
327
- setMessages([...agentRef.current.messages]);
328
- }
322
+ throttledSetMessages();
329
323
  },
330
324
  onServersChange: (servers) => {
331
325
  setMcpServers([...servers]);
@@ -343,23 +337,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
343
337
  setBackgroundTasks([...tasks]);
344
338
  },
345
339
  onTasksChange: (tasks) => {
346
- debouncedSetTasks(tasks);
347
- },
348
- onSubagentMessagesChange: (subagentId: string, messages: Message[]) => {
349
- logger.debug("onSubagentMessagesChange", subagentId, messages.length);
350
- setSubagentMessages((prev) => ({
351
- ...prev,
352
- [subagentId]: [...messages],
353
- }));
354
- },
355
- onSubagentLatestTotalTokensChange: (
356
- subagentId: string,
357
- tokens: number,
358
- ) => {
359
- setSubagentLatestTokens((prev) => ({
360
- ...prev,
361
- [subagentId]: tokens,
362
- }));
340
+ setTasks([...tasks]);
363
341
  },
364
342
  onPermissionModeChange: (mode) => {
365
343
  setPermissionModeState(mode);
@@ -455,17 +433,13 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
455
433
  workdir,
456
434
  worktreeSession,
457
435
  model,
458
- debouncedSetTasks,
459
436
  initialPermissionMode,
437
+ throttledSetMessages,
460
438
  ]);
461
439
 
462
440
  // Cleanup on unmount
463
441
  useEffect(() => {
464
442
  return () => {
465
- if (taskUpdateTimerRef.current) {
466
- clearTimeout(taskUpdateTimerRef.current);
467
- }
468
-
469
443
  if (agentRef.current) {
470
444
  try {
471
445
  // Display usage summary before cleanup
@@ -517,7 +491,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
517
491
  if (command) {
518
492
  setIsCommandRunning(true);
519
493
  try {
520
- await agentRef.current?.executeBashCommand(command);
494
+ await agentRef.current?.bang(command);
521
495
  } finally {
522
496
  setIsCommandRunning(false);
523
497
  }
@@ -707,13 +681,16 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
707
681
  // Clear terminal screen when expanded state changes
708
682
  setIsExpanded((prev) => {
709
683
  const newExpanded = !prev;
684
+ isExpandedRef.current = newExpanded;
710
685
  if (newExpanded) {
711
686
  // Transitioning to EXPANDED: Freeze the current view
687
+ // Cancel any pending throttled updates to avoid overwriting the frozen state
688
+ throttledSetMessages.cancel();
712
689
  // Deep copy the last message to ensure it doesn't update if the agent is still writing to it
713
690
  setMessages((currentMessages) => {
714
691
  if (currentMessages.length === 0) return currentMessages;
715
692
  const lastMessage = currentMessages[currentMessages.length - 1];
716
- const frozenLastMessage = JSON.parse(JSON.stringify(lastMessage));
693
+ const frozenLastMessage = cloneMessage(lastMessage);
717
694
  return [...currentMessages.slice(0, -1), frozenLastMessage];
718
695
  });
719
696
  } else {
@@ -772,8 +749,6 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
772
749
  stopBackgroundTask,
773
750
  slashCommands,
774
751
  hasSlashCommand,
775
- subagentMessages,
776
- subagentLatestTokens,
777
752
  permissionMode,
778
753
  setPermissionMode,
779
754
  allowBypassInCycle,
package/src/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import yargs from "yargs";
2
2
  import { hideBin } from "yargs/helpers";
3
3
  import { startCli } from "./cli.js";
4
+ import { logger } from "./utils/logger.js";
4
5
  import { Scope, generateRandomName, type PermissionMode } from "wave-agent-sdk";
5
6
  import { createWorktree, type WorktreeSession } from "./utils/worktree.js";
6
7
  import path from "path";
@@ -14,6 +15,18 @@ const version = packageJson.version;
14
15
 
15
16
  // Export main function for external use
16
17
  export async function main() {
18
+ // Start memory monitoring in debug mode
19
+ if (process.env.LOG_LEVEL === "DEBUG") {
20
+ setInterval(() => {
21
+ const usage = process.memoryUsage();
22
+ logger.debug(
23
+ `[Memory] RSS: ${Math.round(usage.rss / 1024 / 1024)}MB, ` +
24
+ `Heap: ${Math.round(usage.heapUsed / 1024 / 1024)}/${Math.round(usage.heapTotal / 1024 / 1024)}MB, ` +
25
+ `External: ${Math.round(usage.external / 1024 / 1024)}MB`,
26
+ );
27
+ }, 10000).unref();
28
+ }
29
+
17
30
  try {
18
31
  const originalCwd = process.cwd();
19
32
  const argv = await yargs(hideBin(process.argv))
@@ -49,14 +49,6 @@ function nodeToAnsi(node: Node): string {
49
49
 
50
50
  for (const className of classes) {
51
51
  if (theme[className]) {
52
- // If content has newlines, split it and apply style to each line
53
- // to ensure ANSI codes are correctly applied when splitting the final string by lines.
54
- if (content.includes("\n")) {
55
- return content
56
- .split("\n")
57
- .map((line) => theme[className](line))
58
- .join("\n");
59
- }
60
52
  return theme[className](content);
61
53
  }
62
54
  }