wave-code 0.12.1 → 0.12.3

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 (44) 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 +9 -14
  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 +3 -1
  9. package/dist/components/ChatInterface.d.ts.map +1 -1
  10. package/dist/components/ChatInterface.js +4 -65
  11. package/dist/components/ConfirmationDetails.d.ts +0 -2
  12. package/dist/components/ConfirmationDetails.d.ts.map +1 -1
  13. package/dist/components/ConfirmationDetails.js +3 -15
  14. package/dist/components/ConfirmationSelector.d.ts +0 -1
  15. package/dist/components/ConfirmationSelector.d.ts.map +1 -1
  16. package/dist/components/ConfirmationSelector.js +4 -12
  17. package/dist/components/MessageList.d.ts +1 -2
  18. package/dist/components/MessageList.d.ts.map +1 -1
  19. package/dist/components/MessageList.js +5 -16
  20. package/dist/components/RewindCommand.d.ts.map +1 -1
  21. package/dist/components/RewindCommand.js +2 -4
  22. package/dist/components/ToolDisplay.d.ts.map +1 -1
  23. package/dist/components/ToolDisplay.js +2 -5
  24. package/dist/contexts/useChat.d.ts +0 -2
  25. package/dist/contexts/useChat.d.ts.map +1 -1
  26. package/dist/contexts/useChat.js +26 -15
  27. package/dist/utils/logger.d.ts.map +1 -1
  28. package/dist/utils/logger.js +3 -6
  29. package/dist/utils/throttle.d.ts +15 -0
  30. package/dist/utils/throttle.d.ts.map +1 -0
  31. package/dist/utils/throttle.js +55 -0
  32. package/package.json +2 -2
  33. package/src/components/App.tsx +11 -17
  34. package/src/components/BackgroundTaskManager.tsx +3 -6
  35. package/src/components/BangDisplay.tsx +4 -22
  36. package/src/components/ChatInterface.tsx +8 -84
  37. package/src/components/ConfirmationDetails.tsx +2 -25
  38. package/src/components/ConfirmationSelector.tsx +3 -15
  39. package/src/components/MessageList.tsx +5 -25
  40. package/src/components/RewindCommand.tsx +3 -5
  41. package/src/components/ToolDisplay.tsx +2 -5
  42. package/src/contexts/useChat.tsx +34 -16
  43. package/src/utils/logger.ts +3 -7
  44. package/src/utils/throttle.ts +75 -0
@@ -9,6 +9,7 @@
9
9
  */
10
10
  import * as fs from "fs";
11
11
  import { Chalk } from "chalk";
12
+ import { getLastLines } from "wave-agent-sdk";
12
13
  import { LOG_FILE, DATA_DIRECTORY } from "./constants.js";
13
14
  const chalk = new Chalk({ level: 3 });
14
15
  const logFile = process.env.LOG_FILE || LOG_FILE;
@@ -225,15 +226,11 @@ const truncateLogFileIfNeeded = (config) => {
225
226
  // If file size exceeds limit, truncate file
226
227
  if (stats.size > config.maxFileSize) {
227
228
  const content = fs.readFileSync(logFile, "utf8");
228
- const lines = content.split("\n");
229
- // Keep the last specified number of lines
230
- const keepLines = Math.min(config.keepLines, lines.length);
231
- const truncatedContent = lines.slice(-keepLines).join("\n");
229
+ const truncatedContent = getLastLines(content, config.keepLines);
232
230
  // Write truncated content
233
231
  fs.writeFileSync(logFile, truncatedContent);
234
232
  // Record truncation operation
235
- const removedLines = lines.length - keepLines;
236
- logger.debug(`Log file truncated: removed ${removedLines} lines, kept last ${keepLines} lines`);
233
+ logger.debug(`Log file truncated: file size ${stats.size} exceeded limit ${config.maxFileSize}, kept last ${config.keepLines} lines`);
237
234
  }
238
235
  }
239
236
  catch (error) {
@@ -0,0 +1,15 @@
1
+ export interface ThrottleOptions {
2
+ leading?: boolean;
3
+ trailing?: boolean;
4
+ }
5
+ export interface ThrottledFunction<T extends (...args: unknown[]) => void> {
6
+ (...args: Parameters<T>): void;
7
+ cancel: () => void;
8
+ flush: () => void;
9
+ }
10
+ /**
11
+ * Creates a throttled function that only invokes `func` at most once per
12
+ * every `wait` milliseconds.
13
+ */
14
+ export declare function throttle<T extends (...args: unknown[]) => void>(func: T, wait: number, options?: ThrottleOptions): ThrottledFunction<T>;
15
+ //# sourceMappingURL=throttle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"throttle.d.ts","sourceRoot":"","sources":["../../src/utils/throttle.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,eAAe;IAC9B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,iBAAiB,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI;IACvE,CAAC,GAAG,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;IAC/B,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB;AAED;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,EAC7D,IAAI,EAAE,CAAC,EACP,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE,eAAmD,GAC3D,iBAAiB,CAAC,CAAC,CAAC,CAuDtB"}
@@ -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.3",
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.3"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@types/react": "^19.1.8",
@@ -112,32 +112,33 @@ 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
- const { isExpanded, rewindId, wasLastDetailsTooTall, sessionId } = useChat();
117
+ const { isExpanded, rewindId, sessionId } = useChat();
118
118
 
119
- const [remountKey, setRemountKey] = useState(
120
- String(isExpanded) + rewindId + wasLastDetailsTooTall,
121
- );
119
+ const [remountKey, setRemountKey] = useState(String(isExpanded) + rewindId);
122
120
 
123
121
  const prevSessionId = useRef(sessionId);
122
+ const isRemountScheduled = useRef(false);
124
123
 
125
124
  useEffect(() => {
126
125
  const newKey =
127
126
  String(isExpanded) +
128
127
  rewindId +
129
- wasLastDetailsTooTall +
130
128
  (prevSessionId.current && sessionId && prevSessionId.current !== sessionId
131
129
  ? sessionId
132
130
  : "");
133
131
 
134
- if (newKey !== remountKey) {
132
+ if (newKey !== remountKey && !isRemountScheduled.current) {
133
+ isRemountScheduled.current = true;
134
+
135
135
  const timeout = setTimeout(() => {
136
136
  stdout?.write("\u001b[2J\u001b[3J\u001b[0;0H", (err?: Error | null) => {
137
137
  if (err) {
138
138
  console.error("Failed to clear terminal:", err);
139
139
  }
140
140
  setRemountKey(newKey);
141
+ isRemountScheduled.current = false;
141
142
  });
142
143
  }, 100);
143
144
 
@@ -147,16 +148,9 @@ const ChatInterfaceWithRemount: React.FC = () => {
147
148
  if (sessionId) {
148
149
  prevSessionId.current = sessionId;
149
150
  }
150
- }, [
151
- isExpanded,
152
- rewindId,
153
- wasLastDetailsTooTall,
154
- sessionId,
155
- remountKey,
156
- stdout,
157
- ]);
158
-
159
- return <ChatInterface key={remountKey} />;
151
+ }, [isExpanded, rewindId, sessionId, remountKey, stdout]);
152
+
153
+ return <ChatInterface remountKey={remountKey} />;
160
154
  };
161
155
 
162
156
  export const App: React.FC<AppProps> = ({
@@ -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 from "react";
2
+ import { Box } from "ink";
3
3
  import { MessageList } from "./MessageList.js";
4
4
  import { BtwDisplay } from "./BtwDisplay.js";
5
5
  import { InputBox } from "./InputBox.js";
@@ -10,15 +10,10 @@ import { ConfirmationDetails } from "./ConfirmationDetails.js";
10
10
  import { ConfirmationSelector } from "./ConfirmationSelector.js";
11
11
 
12
12
  import { useChat } from "../contexts/useChat.js";
13
- import type { PermissionDecision } from "wave-agent-sdk";
14
-
15
- export const ChatInterface: React.FC = () => {
16
- const { stdout } = useStdout();
17
- const [detailsHeight, setDetailsHeight] = useState(0);
18
- const [selectorHeight, setSelectorHeight] = useState(0);
19
- const [dynamicBlocksHeight, setDynamicBlocksHeight] = useState(0);
20
- const [isConfirmationTooTall, setIsConfirmationTooTall] = useState(false);
21
13
 
14
+ export const ChatInterface: React.FC<{ remountKey: string }> = ({
15
+ remountKey,
16
+ }) => {
22
17
  const {
23
18
  messages,
24
19
  isLoading,
@@ -37,8 +32,7 @@ export const ChatInterface: React.FC = () => {
37
32
  isConfirmationVisible,
38
33
  confirmingTool,
39
34
  handleConfirmationDecision,
40
- handleConfirmationCancel: originalHandleConfirmationCancel,
41
- setWasLastDetailsTooTall,
35
+ handleConfirmationCancel,
42
36
  version,
43
37
  workdir,
44
38
  btwState,
@@ -46,83 +40,16 @@ export const ChatInterface: React.FC = () => {
46
40
 
47
41
  const displayMessages = messages;
48
42
 
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
- }, []);
60
-
61
- useLayoutEffect(() => {
62
- if (!isConfirmationVisible) {
63
- setIsConfirmationTooTall(false);
64
- setDetailsHeight(0);
65
- setSelectorHeight(0);
66
- setDynamicBlocksHeight(0);
67
- return;
68
- }
69
-
70
- if (isConfirmationTooTall) {
71
- return;
72
- }
73
-
74
- const terminalHeight = stdout?.rows || 24;
75
- const totalHeight = detailsHeight + selectorHeight + dynamicBlocksHeight;
76
- if (totalHeight > terminalHeight - 3) {
77
- setIsConfirmationTooTall(true);
78
- }
79
- }, [
80
- detailsHeight,
81
- selectorHeight,
82
- dynamicBlocksHeight,
83
- stdout?.rows,
84
- isConfirmationVisible,
85
- isConfirmationTooTall,
86
- ]);
87
-
88
- const handleConfirmationCancel = useCallback(() => {
89
- if (isConfirmationTooTall) {
90
- setWasLastDetailsTooTall((prev) => prev + 1);
91
- setIsConfirmationTooTall(false);
92
- }
93
- originalHandleConfirmationCancel();
94
- }, [
95
- isConfirmationTooTall,
96
- originalHandleConfirmationCancel,
97
- setWasLastDetailsTooTall,
98
- ]);
99
-
100
- const wrappedHandleConfirmationDecision = useCallback(
101
- (decision: PermissionDecision) => {
102
- if (isConfirmationTooTall) {
103
- setWasLastDetailsTooTall((prev) => prev + 1);
104
- setIsConfirmationTooTall(false);
105
- }
106
- handleConfirmationDecision(decision);
107
- },
108
- [
109
- isConfirmationTooTall,
110
- handleConfirmationDecision,
111
- setWasLastDetailsTooTall,
112
- ],
113
- );
114
-
115
43
  if (!sessionId) return null;
116
44
 
117
45
  return (
118
46
  <Box flexDirection="column">
119
47
  <MessageList
48
+ key={remountKey}
120
49
  messages={displayMessages}
121
50
  isExpanded={isExpanded}
122
- forceStatic={isConfirmationVisible && isConfirmationTooTall}
123
51
  version={version}
124
52
  workdir={workdir}
125
- onDynamicBlocksHeightMeasured={handleDynamicBlocksHeightMeasured}
126
53
  />
127
54
 
128
55
  {!isConfirmationVisible && !isExpanded && (
@@ -159,8 +86,6 @@ export const ChatInterface: React.FC = () => {
159
86
  toolInput={confirmingTool!.input}
160
87
  planContent={confirmingTool!.planContent}
161
88
  isExpanded={isExpanded}
162
- onHeightMeasured={handleDetailsHeightMeasured}
163
- isStatic={isConfirmationTooTall}
164
89
  />
165
90
  <ConfirmationSelector
166
91
  toolName={confirmingTool!.name}
@@ -168,10 +93,9 @@ export const ChatInterface: React.FC = () => {
168
93
  suggestedPrefix={confirmingTool!.suggestedPrefix}
169
94
  hidePersistentOption={confirmingTool!.hidePersistentOption}
170
95
  isExpanded={isExpanded}
171
- onDecision={wrappedHandleConfirmationDecision}
96
+ onDecision={handleConfirmationDecision}
172
97
  onCancel={handleConfirmationCancel}
173
98
  onAbort={abortMessage}
174
- onHeightMeasured={handleSelectorHeightMeasured}
175
99
  />
176
100
  </>
177
101
  )}
@@ -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 } from "ink";
3
3
  import {
4
4
  BASH_TOOL_NAME,
5
5
  EDIT_TOOL_NAME,
@@ -41,8 +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
- isStatic?: boolean;
46
44
  }
47
45
 
48
46
  export const ConfirmationDetails: React.FC<ConfirmationDetailsProps> = ({
@@ -50,26 +48,13 @@ export const ConfirmationDetails: React.FC<ConfirmationDetailsProps> = ({
50
48
  toolInput,
51
49
  planContent,
52
50
  isExpanded = false,
53
- onHeightMeasured,
54
- isStatic = false,
55
51
  }) => {
56
- const { stdout } = useStdout();
57
- const boxRef = useRef(null);
58
-
59
52
  const startLineNumber =
60
53
  (toolInput?.startLineNumber as number | undefined) ??
61
54
  (toolName === WRITE_TOOL_NAME ? 1 : undefined);
62
55
 
63
- useLayoutEffect(() => {
64
- if (boxRef.current) {
65
- const { height } = measureElement(boxRef.current);
66
- onHeightMeasured?.(height);
67
- }
68
- }, [stdout?.rows, onHeightMeasured, toolInput, isExpanded]);
69
-
70
56
  const content = (
71
57
  <Box
72
- ref={boxRef}
73
58
  flexDirection="column"
74
59
  borderStyle="single"
75
60
  borderColor="yellow"
@@ -109,14 +94,6 @@ export const ConfirmationDetails: React.FC<ConfirmationDetailsProps> = ({
109
94
  </Box>
110
95
  );
111
96
 
112
- if (isStatic) {
113
- return (
114
- <Static items={[1]}>
115
- {(item) => <React.Fragment key={item}>{content}</React.Fragment>}
116
- </Static>
117
- );
118
- }
119
-
120
97
  return content;
121
98
  };
122
99
 
@@ -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,18 +10,10 @@ 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(
17
- ({
18
- messages,
19
- isExpanded = false,
20
- forceStatic = false,
21
- version,
22
- workdir,
23
- onDynamicBlocksHeightMeasured,
24
- }: MessageListProps) => {
16
+ ({ messages, isExpanded = false, version, workdir }: MessageListProps) => {
25
17
  const welcomeMessage = (
26
18
  <Box flexDirection="column" paddingTop={1}>
27
19
  <Text color="gray">WAVE{version ? ` v${version}` : ""}</Text>
@@ -66,25 +58,13 @@ export const MessageList = React.memo(
66
58
  const blocksWithStatus = allBlocks.map((item) => {
67
59
  const { block } = item;
68
60
  const isDynamic =
69
- !forceStatic &&
70
- (messagesWithRunningBlocks.has(item.messageIndex) || isRunning(block));
61
+ messagesWithRunningBlocks.has(item.messageIndex) || isRunning(block);
71
62
  return { ...item, isDynamic };
72
63
  });
73
64
 
74
65
  const staticBlocks = blocksWithStatus.filter((b) => !b.isDynamic);
75
66
  const dynamicBlocks = blocksWithStatus.filter((b) => b.isDynamic);
76
67
 
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
68
  const staticItems = [
89
69
  {
90
70
  isWelcome: true,
@@ -130,7 +110,7 @@ export const MessageList = React.memo(
130
110
 
131
111
  {/* Dynamic blocks */}
132
112
  {dynamicBlocks.length > 0 && (
133
- <Box ref={dynamicBlocksRef} flexDirection="column">
113
+ <Box flexDirection="column">
134
114
  {dynamicBlocks.map((item) => (
135
115
  <MessageBlockItem
136
116
  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;