wave-code 0.12.1 → 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 (42) hide show
  1. package/dist/components/App.d.ts +1 -0
  2. package/dist/components/App.d.ts.map +1 -1
  3. package/dist/components/App.js +5 -2
  4. package/dist/components/BackgroundTaskManager.d.ts.map +1 -1
  5. package/dist/components/BackgroundTaskManager.js +2 -1
  6. package/dist/components/BangDisplay.d.ts.map +1 -1
  7. package/dist/components/BangDisplay.js +2 -14
  8. package/dist/components/ChatInterface.d.ts.map +1 -1
  9. package/dist/components/ChatInterface.js +17 -27
  10. package/dist/components/ConfirmationDetails.d.ts +0 -1
  11. package/dist/components/ConfirmationDetails.d.ts.map +1 -1
  12. package/dist/components/ConfirmationDetails.js +4 -12
  13. package/dist/components/ConfirmationSelector.d.ts +0 -1
  14. package/dist/components/ConfirmationSelector.d.ts.map +1 -1
  15. package/dist/components/ConfirmationSelector.js +4 -12
  16. package/dist/components/MessageList.d.ts +1 -2
  17. package/dist/components/MessageList.d.ts.map +1 -1
  18. package/dist/components/MessageList.js +4 -14
  19. package/dist/components/RewindCommand.d.ts.map +1 -1
  20. package/dist/components/RewindCommand.js +2 -4
  21. package/dist/components/ToolDisplay.d.ts.map +1 -1
  22. package/dist/components/ToolDisplay.js +2 -5
  23. package/dist/contexts/useChat.d.ts.map +1 -1
  24. package/dist/contexts/useChat.js +26 -11
  25. package/dist/utils/logger.d.ts.map +1 -1
  26. package/dist/utils/logger.js +3 -6
  27. package/dist/utils/throttle.d.ts +15 -0
  28. package/dist/utils/throttle.d.ts.map +1 -0
  29. package/dist/utils/throttle.js +55 -0
  30. package/package.json +2 -2
  31. package/src/components/App.tsx +6 -2
  32. package/src/components/BackgroundTaskManager.tsx +3 -6
  33. package/src/components/BangDisplay.tsx +4 -22
  34. package/src/components/ChatInterface.tsx +18 -33
  35. package/src/components/ConfirmationDetails.tsx +2 -15
  36. package/src/components/ConfirmationSelector.tsx +3 -15
  37. package/src/components/MessageList.tsx +3 -16
  38. package/src/components/RewindCommand.tsx +3 -5
  39. package/src/components/ToolDisplay.tsx +2 -5
  40. package/src/contexts/useChat.tsx +33 -10
  41. package/src/utils/logger.ts +3 -7
  42. package/src/utils/throttle.ts +75 -0
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Creates a throttled function that only invokes `func` at most once per
3
+ * every `wait` milliseconds.
4
+ */
5
+ export function throttle(func, wait, options = { leading: true, trailing: true }) {
6
+ let timeoutId = null;
7
+ let lastArgs = null;
8
+ let lastCallTime = 0;
9
+ const invokeFunc = () => {
10
+ if (lastArgs) {
11
+ func(...lastArgs);
12
+ lastCallTime = Date.now();
13
+ lastArgs = null;
14
+ }
15
+ };
16
+ const throttled = (...args) => {
17
+ const now = Date.now();
18
+ const remaining = wait - (now - lastCallTime);
19
+ lastArgs = args;
20
+ if (remaining <= 0 || remaining > wait) {
21
+ if (timeoutId) {
22
+ clearTimeout(timeoutId);
23
+ timeoutId = null;
24
+ }
25
+ if (options.leading !== false || lastCallTime !== 0) {
26
+ invokeFunc();
27
+ }
28
+ else {
29
+ lastCallTime = now;
30
+ }
31
+ }
32
+ else if (!timeoutId && options.trailing !== false) {
33
+ timeoutId = setTimeout(() => {
34
+ timeoutId = null;
35
+ invokeFunc();
36
+ }, remaining);
37
+ }
38
+ };
39
+ throttled.cancel = () => {
40
+ if (timeoutId) {
41
+ clearTimeout(timeoutId);
42
+ timeoutId = null;
43
+ }
44
+ lastArgs = null;
45
+ lastCallTime = 0;
46
+ };
47
+ throttled.flush = () => {
48
+ if (timeoutId) {
49
+ clearTimeout(timeoutId);
50
+ timeoutId = null;
51
+ }
52
+ invokeFunc();
53
+ };
54
+ return throttled;
55
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wave-code",
3
- "version": "0.12.1",
3
+ "version": "0.12.2",
4
4
  "description": "CLI-based code assistant powered by AI, built with React and Ink",
5
5
  "repository": {
6
6
  "type": "git",
@@ -42,7 +42,7 @@
42
42
  "semver": "^7.7.4",
43
43
  "yargs": "^17.7.2",
44
44
  "zod": "^3.23.8",
45
- "wave-agent-sdk": "0.12.1"
45
+ "wave-agent-sdk": "0.12.2"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@types/react": "^19.1.8",
@@ -112,7 +112,7 @@ const AppWithProviders: React.FC<AppWithProvidersProps> = ({
112
112
  );
113
113
  };
114
114
 
115
- const ChatInterfaceWithRemount: React.FC = () => {
115
+ export const ChatInterfaceWithRemount: React.FC = () => {
116
116
  const { stdout } = useStdout();
117
117
  const { isExpanded, rewindId, wasLastDetailsTooTall, sessionId } = useChat();
118
118
 
@@ -121,6 +121,7 @@ const ChatInterfaceWithRemount: React.FC = () => {
121
121
  );
122
122
 
123
123
  const prevSessionId = useRef(sessionId);
124
+ const isRemountScheduled = useRef(false);
124
125
 
125
126
  useEffect(() => {
126
127
  const newKey =
@@ -131,13 +132,16 @@ const ChatInterfaceWithRemount: React.FC = () => {
131
132
  ? sessionId
132
133
  : "");
133
134
 
134
- if (newKey !== remountKey) {
135
+ if (newKey !== remountKey && !isRemountScheduled.current) {
136
+ isRemountScheduled.current = true;
137
+
135
138
  const timeout = setTimeout(() => {
136
139
  stdout?.write("\u001b[2J\u001b[3J\u001b[0;0H", (err?: Error | null) => {
137
140
  if (err) {
138
141
  console.error("Failed to clear terminal:", err);
139
142
  }
140
143
  setRemountKey(newKey);
144
+ isRemountScheduled.current = false;
141
145
  });
142
146
  }, 100);
143
147
 
@@ -1,6 +1,7 @@
1
1
  import React, { useState, useEffect } from "react";
2
2
  import { Box, Text, useInput } from "ink";
3
3
  import { useChat } from "../contexts/useChat.js";
4
+ import { getLastLines } from "wave-agent-sdk";
4
5
 
5
6
  interface Task {
6
7
  id: string;
@@ -207,9 +208,7 @@ export const BackgroundTaskManager: React.FC<BackgroundTaskManagerProps> = ({
207
208
  OUTPUT (last 10 lines):
208
209
  </Text>
209
210
  <Box borderStyle="single" borderColor="green" padding={1}>
210
- <Text>
211
- {detailOutput.stdout.split("\n").slice(-10).join("\n")}
212
- </Text>
211
+ <Text>{getLastLines(detailOutput.stdout, 10)}</Text>
213
212
  </Box>
214
213
  </Box>
215
214
  )}
@@ -220,9 +219,7 @@ export const BackgroundTaskManager: React.FC<BackgroundTaskManagerProps> = ({
220
219
  ERRORS:
221
220
  </Text>
222
221
  <Box borderStyle="single" borderColor="red" padding={1}>
223
- <Text color="red">
224
- {detailOutput.stderr.split("\n").slice(-10).join("\n")}
225
- </Text>
222
+ <Text color="red">{getLastLines(detailOutput.stderr, 10)}</Text>
226
223
  </Box>
227
224
  </Box>
228
225
  )}
@@ -1,6 +1,7 @@
1
- import React, { useState, useEffect } from "react";
1
+ import React from "react";
2
2
  import { Box, Text } from "ink";
3
3
  import type { BangBlock } from "wave-agent-sdk";
4
+ import { getLastLines } from "wave-agent-sdk";
4
5
 
5
6
  interface BangDisplayProps {
6
7
  block: BangBlock;
@@ -12,17 +13,8 @@ export const BangDisplay: React.FC<BangDisplayProps> = ({
12
13
  isExpanded = false,
13
14
  }) => {
14
15
  const { command, output, isRunning, exitCode } = block;
15
- const [isOverflowing, setIsOverflowing] = useState(false);
16
16
  const MAX_LINES = 3; // Set maximum display lines
17
17
 
18
- // Detect if content is overflowing
19
- useEffect(() => {
20
- if (output) {
21
- const lines = output.split("\n");
22
- setIsOverflowing(!isExpanded && lines.length > MAX_LINES);
23
- }
24
- }, [output, isExpanded]);
25
-
26
18
  const getStatusColor = () => {
27
19
  if (isRunning) return "yellow";
28
20
  if (exitCode === 0) return "green";
@@ -38,19 +30,9 @@ export const BangDisplay: React.FC<BangDisplayProps> = ({
38
30
  </Box>
39
31
 
40
32
  {output && (
41
- <Box
42
- paddingLeft={2}
43
- height={
44
- isExpanded
45
- ? undefined
46
- : Math.min(output.split("\n").length, MAX_LINES)
47
- }
48
- overflow="hidden"
49
- >
33
+ <Box paddingLeft={2} overflow="hidden">
50
34
  <Text color="gray">
51
- {isOverflowing
52
- ? output.split("\n").slice(-MAX_LINES).join("\n")
53
- : output}
35
+ {isExpanded ? output : getLastLines(output, MAX_LINES)}
54
36
  </Text>
55
37
  </Box>
56
38
  )}
@@ -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,6 +1,6 @@
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";
3
+ import { Box, Text, Static } from "ink";
4
4
  import type { Message, MessageBlock } from "wave-agent-sdk";
5
5
  import { MessageBlockItem } from "./MessageBlockItem.js";
6
6
 
@@ -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}>
@@ -74,17 +72,6 @@ export const MessageList = React.memo(
74
72
  const staticBlocks = blocksWithStatus.filter((b) => !b.isDynamic);
75
73
  const dynamicBlocks = blocksWithStatus.filter((b) => b.isDynamic);
76
74
 
77
- const dynamicBlocksRef = useRef(null);
78
-
79
- useLayoutEffect(() => {
80
- if (dynamicBlocksRef.current) {
81
- const { height } = measureElement(dynamicBlocksRef.current);
82
- onDynamicBlocksHeightMeasured?.(height);
83
- } else {
84
- onDynamicBlocksHeightMeasured?.(0);
85
- }
86
- }, [dynamicBlocks, isExpanded, onDynamicBlocksHeightMeasured]);
87
-
88
75
  const staticItems = [
89
76
  {
90
77
  isWelcome: true,
@@ -130,7 +117,7 @@ export const MessageList = React.memo(
130
117
 
131
118
  {/* Dynamic blocks */}
132
119
  {dynamicBlocks.length > 0 && (
133
- <Box ref={dynamicBlocksRef} flexDirection="column">
120
+ <Box flexDirection="column">
134
121
  {dynamicBlocks.map((item) => (
135
122
  <MessageBlockItem
136
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
 
@@ -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;
@@ -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
 
@@ -156,10 +159,6 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
156
159
  const [isExpanded, setIsExpanded] = useState(false);
157
160
  const isExpandedRef = useRef(isExpanded);
158
161
 
159
- useEffect(() => {
160
- isExpandedRef.current = isExpanded;
161
- }, [isExpanded]);
162
-
163
162
  const [isTaskListVisible, setIsTaskListVisible] = useState(true);
164
163
 
165
164
  const [queuedMessages, setQueuedMessages] = useState<
@@ -170,9 +169,31 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
170
169
  }>
171
170
  >([]);
172
171
 
173
- // AI State
174
172
  const [messages, setMessages] = useState<Message[]>([]);
175
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
+
176
197
  const [isLoading, setIsLoading] = useState(false);
177
198
  const [latestTotalTokens, setlatestTotalTokens] = useState(0);
178
199
  const [sessionId, setSessionId] = useState("");
@@ -298,9 +319,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
298
319
  const initializeAgent = async () => {
299
320
  const callbacks: AgentCallbacks = {
300
321
  onMessagesChange: () => {
301
- if (!isExpandedRef.current && agentRef.current) {
302
- setMessages([...agentRef.current.messages]);
303
- }
322
+ throttledSetMessages();
304
323
  },
305
324
  onServersChange: (servers) => {
306
325
  setMcpServers([...servers]);
@@ -415,6 +434,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
415
434
  worktreeSession,
416
435
  model,
417
436
  initialPermissionMode,
437
+ throttledSetMessages,
418
438
  ]);
419
439
 
420
440
  // Cleanup on unmount
@@ -471,7 +491,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
471
491
  if (command) {
472
492
  setIsCommandRunning(true);
473
493
  try {
474
- await agentRef.current?.executeBashCommand(command);
494
+ await agentRef.current?.bang(command);
475
495
  } finally {
476
496
  setIsCommandRunning(false);
477
497
  }
@@ -661,13 +681,16 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
661
681
  // Clear terminal screen when expanded state changes
662
682
  setIsExpanded((prev) => {
663
683
  const newExpanded = !prev;
684
+ isExpandedRef.current = newExpanded;
664
685
  if (newExpanded) {
665
686
  // Transitioning to EXPANDED: Freeze the current view
687
+ // Cancel any pending throttled updates to avoid overwriting the frozen state
688
+ throttledSetMessages.cancel();
666
689
  // Deep copy the last message to ensure it doesn't update if the agent is still writing to it
667
690
  setMessages((currentMessages) => {
668
691
  if (currentMessages.length === 0) return currentMessages;
669
692
  const lastMessage = currentMessages[currentMessages.length - 1];
670
- const frozenLastMessage = JSON.parse(JSON.stringify(lastMessage));
693
+ const frozenLastMessage = cloneMessage(lastMessage);
671
694
  return [...currentMessages.slice(0, -1), frozenLastMessage];
672
695
  });
673
696
  } else {
@@ -10,6 +10,7 @@
10
10
 
11
11
  import * as fs from "fs";
12
12
  import { Chalk } from "chalk";
13
+ import { getLastLines } from "wave-agent-sdk";
13
14
  import { LOG_FILE, DATA_DIRECTORY } from "./constants.js";
14
15
 
15
16
  const chalk = new Chalk({ level: 3 });
@@ -269,19 +270,14 @@ const truncateLogFileIfNeeded = (config: LogCleanupConfig): void => {
269
270
  // If file size exceeds limit, truncate file
270
271
  if (stats.size > config.maxFileSize) {
271
272
  const content = fs.readFileSync(logFile, "utf8");
272
- const lines = content.split("\n");
273
-
274
- // Keep the last specified number of lines
275
- const keepLines = Math.min(config.keepLines, lines.length);
276
- const truncatedContent = lines.slice(-keepLines).join("\n");
273
+ const truncatedContent = getLastLines(content, config.keepLines);
277
274
 
278
275
  // Write truncated content
279
276
  fs.writeFileSync(logFile, truncatedContent);
280
277
 
281
278
  // Record truncation operation
282
- const removedLines = lines.length - keepLines;
283
279
  logger.debug(
284
- `Log file truncated: removed ${removedLines} lines, kept last ${keepLines} lines`,
280
+ `Log file truncated: file size ${stats.size} exceeded limit ${config.maxFileSize}, kept last ${config.keepLines} lines`,
285
281
  );
286
282
  }
287
283
  } catch (error) {