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.
- package/dist/components/App.d.ts +1 -0
- package/dist/components/App.d.ts.map +1 -1
- package/dist/components/App.js +5 -2
- package/dist/components/BackgroundTaskManager.d.ts.map +1 -1
- package/dist/components/BackgroundTaskManager.js +2 -1
- package/dist/components/BangDisplay.d.ts.map +1 -1
- package/dist/components/BangDisplay.js +2 -14
- package/dist/components/ChatInterface.d.ts.map +1 -1
- package/dist/components/ChatInterface.js +17 -27
- package/dist/components/ConfirmationDetails.d.ts +0 -1
- package/dist/components/ConfirmationDetails.d.ts.map +1 -1
- package/dist/components/ConfirmationDetails.js +4 -12
- package/dist/components/ConfirmationSelector.d.ts +0 -1
- package/dist/components/ConfirmationSelector.d.ts.map +1 -1
- package/dist/components/ConfirmationSelector.js +4 -12
- package/dist/components/MessageList.d.ts +1 -2
- package/dist/components/MessageList.d.ts.map +1 -1
- package/dist/components/MessageList.js +4 -14
- package/dist/components/RewindCommand.d.ts.map +1 -1
- package/dist/components/RewindCommand.js +2 -4
- package/dist/components/ToolDisplay.d.ts.map +1 -1
- package/dist/components/ToolDisplay.js +2 -5
- package/dist/contexts/useChat.d.ts.map +1 -1
- package/dist/contexts/useChat.js +26 -11
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +3 -6
- package/dist/utils/throttle.d.ts +15 -0
- package/dist/utils/throttle.d.ts.map +1 -0
- package/dist/utils/throttle.js +55 -0
- package/package.json +2 -2
- package/src/components/App.tsx +6 -2
- package/src/components/BackgroundTaskManager.tsx +3 -6
- package/src/components/BangDisplay.tsx +4 -22
- package/src/components/ChatInterface.tsx +18 -33
- package/src/components/ConfirmationDetails.tsx +2 -15
- package/src/components/ConfirmationSelector.tsx +3 -15
- package/src/components/MessageList.tsx +3 -16
- package/src/components/RewindCommand.tsx +3 -5
- package/src/components/ToolDisplay.tsx +2 -5
- package/src/contexts/useChat.tsx +33 -10
- package/src/utils/logger.ts +3 -7
- 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.
|
|
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.
|
|
45
|
+
"wave-agent-sdk": "0.12.2"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@types/react": "^19.1.8",
|
package/src/components/App.tsx
CHANGED
|
@@ -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
|
|
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
|
-
{
|
|
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
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
|
2
|
-
import { Box, Text,
|
|
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,
|
|
2
|
-
import { Box, Text, useInput
|
|
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
|
|
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
|
|
1
|
+
import React from "react";
|
|
2
2
|
import os from "os";
|
|
3
|
-
import { Box, Text, Static
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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;
|
package/src/contexts/useChat.tsx
CHANGED
|
@@ -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
|
-
|
|
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?.
|
|
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 =
|
|
693
|
+
const frozenLastMessage = cloneMessage(lastMessage);
|
|
671
694
|
return [...currentMessages.slice(0, -1), frozenLastMessage];
|
|
672
695
|
});
|
|
673
696
|
} else {
|
package/src/utils/logger.ts
CHANGED
|
@@ -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
|
|
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:
|
|
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) {
|