wave-code 0.0.5 → 0.0.6
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/README.md +2 -2
- package/dist/components/ChatInterface.d.ts.map +1 -1
- package/dist/components/ChatInterface.js +4 -24
- package/dist/components/CommandSelector.js +4 -4
- package/dist/components/DiffViewer.d.ts +1 -1
- package/dist/components/DiffViewer.d.ts.map +1 -1
- package/dist/components/DiffViewer.js +15 -15
- package/dist/components/FileSelector.js +2 -2
- package/dist/components/InputBox.d.ts.map +1 -1
- package/dist/components/InputBox.js +21 -50
- package/dist/components/Markdown.d.ts +6 -0
- package/dist/components/Markdown.d.ts.map +1 -0
- package/dist/components/Markdown.js +22 -0
- package/dist/components/MessageItem.d.ts +9 -0
- package/dist/components/MessageItem.d.ts.map +1 -0
- package/dist/components/MessageItem.js +15 -0
- package/dist/components/MessageList.d.ts +1 -1
- package/dist/components/MessageList.d.ts.map +1 -1
- package/dist/components/MessageList.js +33 -33
- package/dist/components/SubagentBlock.d.ts +0 -1
- package/dist/components/SubagentBlock.d.ts.map +1 -1
- package/dist/components/SubagentBlock.js +29 -30
- package/dist/components/ToolResultDisplay.js +5 -5
- package/dist/contexts/useChat.d.ts +2 -2
- package/dist/contexts/useChat.d.ts.map +1 -1
- package/dist/contexts/useChat.js +18 -9
- package/dist/hooks/useInputManager.d.ts +3 -1
- package/dist/hooks/useInputManager.d.ts.map +1 -1
- package/dist/hooks/useInputManager.js +15 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -2
- package/dist/managers/InputManager.d.ts +3 -1
- package/dist/managers/InputManager.d.ts.map +1 -1
- package/dist/managers/InputManager.js +44 -24
- package/dist/print-cli.d.ts +1 -0
- package/dist/print-cli.d.ts.map +1 -1
- package/dist/print-cli.js +88 -23
- package/dist/utils/usageSummary.d.ts +6 -0
- package/dist/utils/usageSummary.d.ts.map +1 -1
- package/dist/utils/usageSummary.js +72 -0
- package/package.json +10 -6
- package/src/components/ChatInterface.tsx +13 -43
- package/src/components/CommandSelector.tsx +5 -5
- package/src/components/DiffViewer.tsx +18 -16
- package/src/components/FileSelector.tsx +2 -2
- package/src/components/InputBox.tsx +22 -74
- package/src/components/Markdown.tsx +29 -0
- package/src/components/MessageItem.tsx +104 -0
- package/src/components/MessageList.tsx +142 -202
- package/src/components/SubagentBlock.tsx +56 -84
- package/src/components/ToolResultDisplay.tsx +5 -5
- package/src/contexts/useChat.tsx +22 -13
- package/src/hooks/useInputManager.ts +21 -3
- package/src/index.ts +12 -2
- package/src/managers/InputManager.ts +55 -25
- package/src/print-cli.ts +103 -21
- package/src/utils/usageSummary.ts +109 -0
|
@@ -23,6 +23,31 @@ export function calculateTokenSummary(usages) {
|
|
|
23
23
|
summary.prompt_tokens += usage.prompt_tokens;
|
|
24
24
|
summary.completion_tokens += usage.completion_tokens;
|
|
25
25
|
summary.total_tokens += usage.total_tokens;
|
|
26
|
+
// Handle cache tokens if present and non-zero
|
|
27
|
+
if (usage.cache_read_input_tokens && usage.cache_read_input_tokens > 0) {
|
|
28
|
+
summary.cache_read_input_tokens =
|
|
29
|
+
(summary.cache_read_input_tokens || 0) + usage.cache_read_input_tokens;
|
|
30
|
+
}
|
|
31
|
+
if (usage.cache_creation_input_tokens &&
|
|
32
|
+
usage.cache_creation_input_tokens > 0) {
|
|
33
|
+
summary.cache_creation_input_tokens =
|
|
34
|
+
(summary.cache_creation_input_tokens || 0) +
|
|
35
|
+
usage.cache_creation_input_tokens;
|
|
36
|
+
}
|
|
37
|
+
if (usage.cache_creation &&
|
|
38
|
+
(usage.cache_creation.ephemeral_5m_input_tokens > 0 ||
|
|
39
|
+
usage.cache_creation.ephemeral_1h_input_tokens > 0)) {
|
|
40
|
+
if (!summary.cache_creation) {
|
|
41
|
+
summary.cache_creation = {
|
|
42
|
+
ephemeral_5m_input_tokens: 0,
|
|
43
|
+
ephemeral_1h_input_tokens: 0,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
summary.cache_creation.ephemeral_5m_input_tokens +=
|
|
47
|
+
usage.cache_creation.ephemeral_5m_input_tokens || 0;
|
|
48
|
+
summary.cache_creation.ephemeral_1h_input_tokens +=
|
|
49
|
+
usage.cache_creation.ephemeral_1h_input_tokens || 0;
|
|
50
|
+
}
|
|
26
51
|
// Track operation types
|
|
27
52
|
if (usage.operation_type === "agent") {
|
|
28
53
|
summary.operations.agent_calls += 1;
|
|
@@ -59,11 +84,43 @@ export function displayUsageSummary(usages, sessionFilePath) {
|
|
|
59
84
|
let totalTokens = 0;
|
|
60
85
|
let totalAgentCalls = 0;
|
|
61
86
|
let totalCompressions = 0;
|
|
87
|
+
let totalCacheRead = 0;
|
|
88
|
+
let totalCacheCreation = 0;
|
|
89
|
+
let totalCache5m = 0;
|
|
90
|
+
let totalCache1h = 0;
|
|
91
|
+
let hasCacheData = false;
|
|
62
92
|
for (const [, summary] of Object.entries(summaries)) {
|
|
63
93
|
console.log(`Model: ${summary.model}`);
|
|
64
94
|
console.log(` Prompt tokens: ${summary.prompt_tokens.toLocaleString()}`);
|
|
65
95
|
console.log(` Completion tokens: ${summary.completion_tokens.toLocaleString()}`);
|
|
66
96
|
console.log(` Total tokens: ${summary.total_tokens.toLocaleString()}`);
|
|
97
|
+
// Display cache information if available
|
|
98
|
+
if (summary.cache_read_input_tokens ||
|
|
99
|
+
summary.cache_creation_input_tokens ||
|
|
100
|
+
summary.cache_creation) {
|
|
101
|
+
hasCacheData = true;
|
|
102
|
+
console.log(" Cache Usage:");
|
|
103
|
+
if (summary.cache_read_input_tokens &&
|
|
104
|
+
summary.cache_read_input_tokens > 0) {
|
|
105
|
+
console.log(` Read from cache: ${summary.cache_read_input_tokens.toLocaleString()} tokens`);
|
|
106
|
+
totalCacheRead += summary.cache_read_input_tokens;
|
|
107
|
+
}
|
|
108
|
+
if (summary.cache_creation_input_tokens &&
|
|
109
|
+
summary.cache_creation_input_tokens > 0) {
|
|
110
|
+
console.log(` Created cache: ${summary.cache_creation_input_tokens.toLocaleString()} tokens`);
|
|
111
|
+
totalCacheCreation += summary.cache_creation_input_tokens;
|
|
112
|
+
}
|
|
113
|
+
if (summary.cache_creation) {
|
|
114
|
+
if (summary.cache_creation.ephemeral_5m_input_tokens > 0) {
|
|
115
|
+
console.log(` 5m cache: ${summary.cache_creation.ephemeral_5m_input_tokens.toLocaleString()} tokens`);
|
|
116
|
+
totalCache5m += summary.cache_creation.ephemeral_5m_input_tokens;
|
|
117
|
+
}
|
|
118
|
+
if (summary.cache_creation.ephemeral_1h_input_tokens > 0) {
|
|
119
|
+
console.log(` 1h cache: ${summary.cache_creation.ephemeral_1h_input_tokens.toLocaleString()} tokens`);
|
|
120
|
+
totalCache1h += summary.cache_creation.ephemeral_1h_input_tokens;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
67
124
|
console.log(` Operations: ${summary.operations.agent_calls} agent calls, ${summary.operations.compressions} compressions`);
|
|
68
125
|
console.log();
|
|
69
126
|
totalPrompt += summary.prompt_tokens;
|
|
@@ -77,6 +134,21 @@ export function displayUsageSummary(usages, sessionFilePath) {
|
|
|
77
134
|
console.log(` Prompt tokens: ${totalPrompt.toLocaleString()}`);
|
|
78
135
|
console.log(` Completion tokens: ${totalCompletion.toLocaleString()}`);
|
|
79
136
|
console.log(` Total tokens: ${totalTokens.toLocaleString()}`);
|
|
137
|
+
if (hasCacheData) {
|
|
138
|
+
console.log(" Cache Usage:");
|
|
139
|
+
if (totalCacheRead > 0) {
|
|
140
|
+
console.log(` Read from cache: ${totalCacheRead.toLocaleString()} tokens`);
|
|
141
|
+
}
|
|
142
|
+
if (totalCacheCreation > 0) {
|
|
143
|
+
console.log(` Created cache: ${totalCacheCreation.toLocaleString()} tokens`);
|
|
144
|
+
}
|
|
145
|
+
if (totalCache5m > 0) {
|
|
146
|
+
console.log(` 5m cache: ${totalCache5m.toLocaleString()} tokens`);
|
|
147
|
+
}
|
|
148
|
+
if (totalCache1h > 0) {
|
|
149
|
+
console.log(` 1h cache: ${totalCache1h.toLocaleString()} tokens`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
80
152
|
console.log(` Operations: ${totalAgentCalls} agent calls, ${totalCompressions} compressions`);
|
|
81
153
|
}
|
|
82
154
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wave-code",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
4
4
|
"description": "CLI-based code assistant powered by AI, built with React and Ink",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -24,14 +24,18 @@
|
|
|
24
24
|
"README.md"
|
|
25
25
|
],
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"
|
|
28
|
-
"react": "^19.1.0",
|
|
29
|
-
"yargs": "^17.7.2",
|
|
27
|
+
"chalk": "^5.6.2",
|
|
30
28
|
"diff": "^8.0.2",
|
|
31
29
|
"glob": "^11.0.3",
|
|
32
|
-
"
|
|
30
|
+
"ink": "^6.5.1",
|
|
31
|
+
"marked": "^11.2.0",
|
|
32
|
+
"marked-terminal": "^7.3.0",
|
|
33
|
+
"react": "^19.1.0",
|
|
34
|
+
"yargs": "^17.7.2",
|
|
35
|
+
"wave-agent-sdk": "0.0.8"
|
|
33
36
|
},
|
|
34
37
|
"devDependencies": {
|
|
38
|
+
"@types/marked-terminal": "^6.1.1",
|
|
35
39
|
"@types/react": "^19.1.8",
|
|
36
40
|
"@types/yargs": "^17.0.0",
|
|
37
41
|
"eslint-plugin-react": "^7.37.5",
|
|
@@ -52,7 +56,7 @@
|
|
|
52
56
|
"scripts": {
|
|
53
57
|
"build": "rimraf dist && tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json",
|
|
54
58
|
"type-check": "tsc --noEmit --incremental",
|
|
55
|
-
"
|
|
59
|
+
"watch": "tsc -p tsconfig.build.json --watch & tsc-alias -p tsconfig.build.json --watch",
|
|
56
60
|
"test": "vitest run",
|
|
57
61
|
"lint": "eslint --cache",
|
|
58
62
|
"format": "prettier --write ."
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import React
|
|
1
|
+
import React from "react";
|
|
2
2
|
import { Box } from "ink";
|
|
3
3
|
import { MessageList } from "./MessageList.js";
|
|
4
4
|
import { InputBox } from "./InputBox.js";
|
|
5
5
|
import { useChat } from "../contexts/useChat.js";
|
|
6
|
-
import type { Message } from "wave-agent-sdk";
|
|
7
6
|
|
|
8
7
|
export const ChatInterface: React.FC = () => {
|
|
9
8
|
const {
|
|
@@ -19,54 +18,25 @@ export const ChatInterface: React.FC = () => {
|
|
|
19
18
|
connectMcpServer,
|
|
20
19
|
disconnectMcpServer,
|
|
21
20
|
isExpanded,
|
|
21
|
+
sessionId,
|
|
22
22
|
latestTotalTokens,
|
|
23
23
|
slashCommands,
|
|
24
24
|
hasSlashCommand,
|
|
25
25
|
} = useChat();
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
const expandedMessagesRef = useRef<Message[]>([]);
|
|
29
|
-
|
|
30
|
-
useEffect(() => {
|
|
31
|
-
// Only sync when collapsed
|
|
32
|
-
if (!isExpanded) {
|
|
33
|
-
expandedMessagesRef.current = messages.map((message, index) => {
|
|
34
|
-
// If it's the last message, deep copy its blocks
|
|
35
|
-
if (index === messages.length - 1) {
|
|
36
|
-
return {
|
|
37
|
-
...message,
|
|
38
|
-
blocks: message.blocks.map((block) => ({ ...block })),
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
return message;
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
}, [isExpanded, messages]);
|
|
27
|
+
if (!sessionId) return null;
|
|
45
28
|
|
|
46
29
|
return (
|
|
47
|
-
<Box flexDirection="column" height="100%">
|
|
48
|
-
<
|
|
49
|
-
{
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
/>
|
|
58
|
-
) : (
|
|
59
|
-
// Normal mode uses real-time state
|
|
60
|
-
<MessageList
|
|
61
|
-
messages={messages}
|
|
62
|
-
isLoading={isLoading}
|
|
63
|
-
isCommandRunning={isCommandRunning}
|
|
64
|
-
isCompressing={isCompressing}
|
|
65
|
-
latestTotalTokens={latestTotalTokens}
|
|
66
|
-
isExpanded={false}
|
|
67
|
-
/>
|
|
68
|
-
)}
|
|
69
|
-
</Box>
|
|
30
|
+
<Box flexDirection="column" height="100%" paddingY={1}>
|
|
31
|
+
<MessageList
|
|
32
|
+
messages={messages}
|
|
33
|
+
isLoading={isLoading}
|
|
34
|
+
isCommandRunning={isCommandRunning}
|
|
35
|
+
isCompressing={isCompressing}
|
|
36
|
+
latestTotalTokens={latestTotalTokens}
|
|
37
|
+
isExpanded={isExpanded}
|
|
38
|
+
key={String(isExpanded) + sessionId}
|
|
39
|
+
/>
|
|
70
40
|
|
|
71
41
|
{!isExpanded && (
|
|
72
42
|
<InputBox
|
|
@@ -41,7 +41,7 @@ export const CommandSelector: React.FC<CommandSelectorProps> = ({
|
|
|
41
41
|
const filteredCommands = allCommands.filter(
|
|
42
42
|
(command) =>
|
|
43
43
|
!searchQuery ||
|
|
44
|
-
command.
|
|
44
|
+
command.id.toLowerCase().includes(searchQuery.toLowerCase()),
|
|
45
45
|
);
|
|
46
46
|
|
|
47
47
|
useInput((input, key) => {
|
|
@@ -50,7 +50,7 @@ export const CommandSelector: React.FC<CommandSelectorProps> = ({
|
|
|
50
50
|
filteredCommands.length > 0 &&
|
|
51
51
|
selectedIndex < filteredCommands.length
|
|
52
52
|
) {
|
|
53
|
-
const selectedCommand = filteredCommands[selectedIndex].
|
|
53
|
+
const selectedCommand = filteredCommands[selectedIndex].id;
|
|
54
54
|
onSelect(selectedCommand);
|
|
55
55
|
}
|
|
56
56
|
return;
|
|
@@ -61,7 +61,7 @@ export const CommandSelector: React.FC<CommandSelectorProps> = ({
|
|
|
61
61
|
filteredCommands.length > 0 &&
|
|
62
62
|
selectedIndex < filteredCommands.length
|
|
63
63
|
) {
|
|
64
|
-
const selectedCommand = filteredCommands[selectedIndex].
|
|
64
|
+
const selectedCommand = filteredCommands[selectedIndex].id;
|
|
65
65
|
onInsert(selectedCommand);
|
|
66
66
|
}
|
|
67
67
|
return;
|
|
@@ -116,12 +116,12 @@ export const CommandSelector: React.FC<CommandSelectorProps> = ({
|
|
|
116
116
|
</Box>
|
|
117
117
|
|
|
118
118
|
{filteredCommands.map((command, index) => (
|
|
119
|
-
<Box key={command.
|
|
119
|
+
<Box key={command.id} flexDirection="column">
|
|
120
120
|
<Text
|
|
121
121
|
color={index === selectedIndex ? "black" : "white"}
|
|
122
122
|
backgroundColor={index === selectedIndex ? "magenta" : undefined}
|
|
123
123
|
>
|
|
124
|
-
{index === selectedIndex ? "▶ " : " "}/{command.
|
|
124
|
+
{index === selectedIndex ? "▶ " : " "}/{command.id}
|
|
125
125
|
</Text>
|
|
126
126
|
{index === selectedIndex && (
|
|
127
127
|
<Box marginLeft={4}>
|
|
@@ -5,7 +5,7 @@ import type { DiffBlock } from "wave-agent-sdk";
|
|
|
5
5
|
|
|
6
6
|
interface DiffViewerProps {
|
|
7
7
|
block: DiffBlock;
|
|
8
|
-
|
|
8
|
+
isStatic?: boolean;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
// Render word-level diff
|
|
@@ -48,7 +48,7 @@ const renderWordLevelDiff = (removedLine: string, addedLine: string) => {
|
|
|
48
48
|
|
|
49
49
|
export const DiffViewer: React.FC<DiffViewerProps> = ({
|
|
50
50
|
block,
|
|
51
|
-
|
|
51
|
+
isStatic = true,
|
|
52
52
|
}) => {
|
|
53
53
|
const { diffResult } = block;
|
|
54
54
|
|
|
@@ -246,21 +246,22 @@ export const DiffViewer: React.FC<DiffViewerProps> = ({
|
|
|
246
246
|
// Handle remaining deleted lines at the end
|
|
247
247
|
flushPendingLines();
|
|
248
248
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
type: "separator",
|
|
257
|
-
});
|
|
258
|
-
return truncatedLines;
|
|
259
|
-
}
|
|
249
|
+
return lines;
|
|
250
|
+
}, [diffResult]);
|
|
251
|
+
|
|
252
|
+
// Truncate to last 10 lines for non-static items
|
|
253
|
+
const displayLines = useMemo(() => {
|
|
254
|
+
if (isStatic) {
|
|
255
|
+
return diffLines;
|
|
260
256
|
}
|
|
261
257
|
|
|
262
|
-
|
|
263
|
-
|
|
258
|
+
const MAX_LINES = 10;
|
|
259
|
+
if (diffLines.length <= MAX_LINES) {
|
|
260
|
+
return diffLines;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return diffLines.slice(-MAX_LINES);
|
|
264
|
+
}, [diffLines, isStatic]);
|
|
264
265
|
|
|
265
266
|
if (!diffResult || diffResult.length === 0) {
|
|
266
267
|
return (
|
|
@@ -270,11 +271,12 @@ export const DiffViewer: React.FC<DiffViewerProps> = ({
|
|
|
270
271
|
);
|
|
271
272
|
}
|
|
272
273
|
|
|
274
|
+
// Show traditional diff view
|
|
273
275
|
return (
|
|
274
276
|
<Box flexDirection="column">
|
|
275
277
|
<Box flexDirection="column">
|
|
276
278
|
<Box flexDirection="column">
|
|
277
|
-
{
|
|
279
|
+
{displayLines.map((line, index) => {
|
|
278
280
|
// If has word-level diff, render special effects
|
|
279
281
|
if (line.wordDiff) {
|
|
280
282
|
const prefix = line.type === "removed" ? "- " : "+ ";
|
|
@@ -22,7 +22,7 @@ export const FileSelector: React.FC<FileSelectorProps> = ({
|
|
|
22
22
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
23
23
|
|
|
24
24
|
useInput((input, key) => {
|
|
25
|
-
if (key.return) {
|
|
25
|
+
if (key.return || key.tab) {
|
|
26
26
|
if (files.length > 0 && selectedIndex < files.length) {
|
|
27
27
|
onSelect(files[selectedIndex].path);
|
|
28
28
|
}
|
|
@@ -126,7 +126,7 @@ export const FileSelector: React.FC<FileSelectorProps> = ({
|
|
|
126
126
|
|
|
127
127
|
<Box marginTop={1}>
|
|
128
128
|
<Text dimColor>
|
|
129
|
-
Use ↑↓ to navigate, Enter to select, Escape to cancel
|
|
129
|
+
Use ↑↓ to navigate, Enter/Tab to select, Escape to cancel
|
|
130
130
|
</Text>
|
|
131
131
|
<Text dimColor>
|
|
132
132
|
File {selectedIndex + 1} of {files.length}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, { useEffect, useMemo } from "react";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
3
|
import { useInput } from "ink";
|
|
4
4
|
import { FileSelector } from "./FileSelector.js";
|
|
@@ -53,19 +53,13 @@ export const InputBox: React.FC<InputBoxProps> = ({
|
|
|
53
53
|
slashCommands = [],
|
|
54
54
|
hasSlashCommand = () => false,
|
|
55
55
|
}) => {
|
|
56
|
-
// Get current working directory
|
|
57
|
-
const currentWorkdir = workdir || process.cwd();
|
|
58
|
-
|
|
59
|
-
// Simple history navigation reset function
|
|
60
|
-
const resetHistoryNavigation = useCallback(() => {
|
|
61
|
-
// This will be handled by InputManager through callbacks
|
|
62
|
-
}, []);
|
|
56
|
+
// Get current working directory - memoized to avoid repeated process.cwd() calls
|
|
57
|
+
const currentWorkdir = useMemo(() => workdir || process.cwd(), [workdir]);
|
|
63
58
|
|
|
64
59
|
// Input manager with all input state and functionality (including images)
|
|
65
60
|
const {
|
|
66
61
|
inputText,
|
|
67
62
|
cursorPosition,
|
|
68
|
-
clearInput,
|
|
69
63
|
// Image management
|
|
70
64
|
attachedImages,
|
|
71
65
|
clearImages,
|
|
@@ -73,24 +67,23 @@ export const InputBox: React.FC<InputBoxProps> = ({
|
|
|
73
67
|
showFileSelector,
|
|
74
68
|
filteredFiles,
|
|
75
69
|
fileSearchQuery: searchQuery,
|
|
76
|
-
handleFileSelect
|
|
70
|
+
handleFileSelect,
|
|
77
71
|
handleCancelFileSelect,
|
|
78
72
|
// Command selector
|
|
79
73
|
showCommandSelector,
|
|
80
74
|
commandSearchQuery,
|
|
81
|
-
handleCommandSelect
|
|
82
|
-
handleCommandInsert
|
|
75
|
+
handleCommandSelect,
|
|
76
|
+
handleCommandInsert,
|
|
83
77
|
handleCancelCommandSelect,
|
|
84
78
|
// Bash history selector
|
|
85
79
|
showBashHistorySelector,
|
|
86
80
|
bashHistorySearchQuery,
|
|
87
|
-
handleBashHistorySelect
|
|
88
|
-
handleBashHistoryExecute,
|
|
81
|
+
handleBashHistorySelect,
|
|
89
82
|
handleCancelBashHistorySelect,
|
|
90
83
|
// Memory type selector
|
|
91
84
|
showMemoryTypeSelector,
|
|
92
85
|
memoryMessage,
|
|
93
|
-
handleMemoryTypeSelect
|
|
86
|
+
handleMemoryTypeSelect,
|
|
94
87
|
handleCancelMemoryTypeSelect,
|
|
95
88
|
// Bash/MCP Manager
|
|
96
89
|
showBashManager,
|
|
@@ -99,16 +92,17 @@ export const InputBox: React.FC<InputBoxProps> = ({
|
|
|
99
92
|
setShowMcpManager,
|
|
100
93
|
// Input history
|
|
101
94
|
setUserInputHistory,
|
|
95
|
+
// Complex handlers combining multiple operations
|
|
96
|
+
handleBashHistoryExecuteAndSend,
|
|
102
97
|
// Main handler
|
|
103
98
|
handleInput,
|
|
99
|
+
// Manager ready state
|
|
100
|
+
isManagerReady,
|
|
104
101
|
} = useInputManager({
|
|
105
|
-
onShowBashManager: () => setShowBashManager(true),
|
|
106
|
-
onShowMcpManager: () => setShowMcpManager(true),
|
|
107
102
|
onSendMessage: sendMessage,
|
|
108
103
|
onHasSlashCommand: hasSlashCommand,
|
|
109
104
|
onSaveMemory: saveMemory,
|
|
110
105
|
onAbortMessage: abortMessage,
|
|
111
|
-
onResetHistoryNavigation: resetHistoryNavigation,
|
|
112
106
|
});
|
|
113
107
|
|
|
114
108
|
// Set user input history when it changes
|
|
@@ -128,65 +122,14 @@ export const InputBox: React.FC<InputBoxProps> = ({
|
|
|
128
122
|
);
|
|
129
123
|
});
|
|
130
124
|
|
|
131
|
-
//
|
|
132
|
-
const handleFileSelect = useCallback(
|
|
133
|
-
(filePath: string) => {
|
|
134
|
-
handleFileSelectorSelect(filePath);
|
|
135
|
-
},
|
|
136
|
-
[handleFileSelectorSelect],
|
|
137
|
-
);
|
|
138
|
-
|
|
139
|
-
const handleCommandSelect = useCallback(
|
|
140
|
-
(command: string) => {
|
|
141
|
-
handleCommandSelectorSelect(command);
|
|
142
|
-
},
|
|
143
|
-
[handleCommandSelectorSelect],
|
|
144
|
-
);
|
|
125
|
+
// These methods are already memoized in useInputManager, no need to wrap again
|
|
145
126
|
|
|
146
|
-
|
|
147
|
-
(command: string) => {
|
|
148
|
-
handleBashHistorySelectorSelect(command);
|
|
149
|
-
},
|
|
150
|
-
[handleBashHistorySelectorSelect],
|
|
151
|
-
);
|
|
152
|
-
|
|
153
|
-
const keyboardHandleBashHistoryExecute = useCallback(
|
|
154
|
-
(command: string) => {
|
|
155
|
-
const commandToExecute = handleBashHistoryExecute(command);
|
|
156
|
-
// Clear input box and execute command, ensure command starts with !
|
|
157
|
-
const bashCommand = commandToExecute.startsWith("!")
|
|
158
|
-
? commandToExecute
|
|
159
|
-
: `!${commandToExecute}`;
|
|
160
|
-
clearInput();
|
|
161
|
-
sendMessage(bashCommand);
|
|
162
|
-
},
|
|
163
|
-
[handleBashHistoryExecute, clearInput, sendMessage],
|
|
164
|
-
);
|
|
165
|
-
|
|
166
|
-
const handleMemoryTypeSelect = useCallback(
|
|
167
|
-
async (type: "project" | "user") => {
|
|
168
|
-
const currentMessage = inputText.trim();
|
|
169
|
-
if (currentMessage.startsWith("#")) {
|
|
170
|
-
await saveMemory(currentMessage, type);
|
|
171
|
-
}
|
|
172
|
-
// Call the handler function to close the selector
|
|
173
|
-
handleMemoryTypeSelectorSelect(type);
|
|
174
|
-
// Clear input box
|
|
175
|
-
clearInput();
|
|
176
|
-
},
|
|
177
|
-
[inputText, saveMemory, handleMemoryTypeSelectorSelect, clearInput],
|
|
178
|
-
);
|
|
127
|
+
// These methods are already memoized in useInputManager and combine multiple operations
|
|
179
128
|
|
|
180
129
|
const isPlaceholder = !inputText;
|
|
181
130
|
const placeholderText = INPUT_PLACEHOLDER_TEXT;
|
|
182
131
|
|
|
183
|
-
//
|
|
184
|
-
const handleCommandInsert = useCallback(
|
|
185
|
-
(command: string) => {
|
|
186
|
-
handleCommandSelectorInsert(command);
|
|
187
|
-
},
|
|
188
|
-
[handleCommandSelectorInsert],
|
|
189
|
-
);
|
|
132
|
+
// handleCommandSelectorInsert is already memoized in useInputManager, no need to wrap again
|
|
190
133
|
|
|
191
134
|
// Split text into three parts: before cursor, cursor position, after cursor
|
|
192
135
|
const displayText = isPlaceholder ? placeholderText : inputText;
|
|
@@ -198,8 +141,13 @@ export const InputBox: React.FC<InputBoxProps> = ({
|
|
|
198
141
|
// Always show cursor, allow user to continue input during loading
|
|
199
142
|
const shouldShowCursor = true;
|
|
200
143
|
|
|
144
|
+
// Only show the Box after InputManager is created on first mount
|
|
145
|
+
if (!isManagerReady) {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
201
149
|
return (
|
|
202
|
-
<Box flexDirection="column"
|
|
150
|
+
<Box flexDirection="column">
|
|
203
151
|
{showFileSelector && (
|
|
204
152
|
<FileSelector
|
|
205
153
|
files={filteredFiles}
|
|
@@ -224,7 +172,7 @@ export const InputBox: React.FC<InputBoxProps> = ({
|
|
|
224
172
|
searchQuery={bashHistorySearchQuery}
|
|
225
173
|
workdir={currentWorkdir}
|
|
226
174
|
onSelect={handleBashHistorySelect}
|
|
227
|
-
onExecute={
|
|
175
|
+
onExecute={handleBashHistoryExecuteAndSend}
|
|
228
176
|
onCancel={handleCancelBashHistorySelect}
|
|
229
177
|
/>
|
|
230
178
|
)}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import React, { useMemo } from "react";
|
|
2
|
+
import { Text } from "ink";
|
|
3
|
+
import { marked } from "marked";
|
|
4
|
+
import TerminalRenderer from "marked-terminal";
|
|
5
|
+
|
|
6
|
+
export interface MarkdownProps {
|
|
7
|
+
children: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Markdown component using marked-terminal with proper unescape option
|
|
11
|
+
export const Markdown = React.memo(({ children }: MarkdownProps) => {
|
|
12
|
+
const result = useMemo(() => {
|
|
13
|
+
// Configure marked with TerminalRenderer using default options
|
|
14
|
+
marked.setOptions({
|
|
15
|
+
renderer: new TerminalRenderer({
|
|
16
|
+
// Use official unescape option to handle HTML entities
|
|
17
|
+
unescape: true,
|
|
18
|
+
}),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const output = marked(children);
|
|
22
|
+
return typeof output === "string" ? output.trim() : "";
|
|
23
|
+
}, [children]);
|
|
24
|
+
|
|
25
|
+
return <Text>{result}</Text>;
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Add display name for debugging
|
|
29
|
+
Markdown.displayName = "Markdown";
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import type { Message } from "wave-agent-sdk";
|
|
4
|
+
import { MessageSource } from "wave-agent-sdk";
|
|
5
|
+
import { DiffViewer } from "./DiffViewer.js";
|
|
6
|
+
import { CommandOutputDisplay } from "./CommandOutputDisplay.js";
|
|
7
|
+
import { ToolResultDisplay } from "./ToolResultDisplay.js";
|
|
8
|
+
import { MemoryDisplay } from "./MemoryDisplay.js";
|
|
9
|
+
import { CompressDisplay } from "./CompressDisplay.js";
|
|
10
|
+
import { SubagentBlock } from "./SubagentBlock.js";
|
|
11
|
+
import { Markdown } from "./Markdown.js";
|
|
12
|
+
|
|
13
|
+
export interface MessageItemProps {
|
|
14
|
+
message: Message;
|
|
15
|
+
isExpanded: boolean;
|
|
16
|
+
shouldShowHeader: boolean;
|
|
17
|
+
isStatic?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const MessageItem = ({
|
|
21
|
+
message,
|
|
22
|
+
isExpanded,
|
|
23
|
+
shouldShowHeader,
|
|
24
|
+
isStatic = true,
|
|
25
|
+
}: MessageItemProps) => {
|
|
26
|
+
if (message.blocks.length === 0) return null;
|
|
27
|
+
return (
|
|
28
|
+
<Box flexDirection="column" gap={1} marginTop={1}>
|
|
29
|
+
{shouldShowHeader && (
|
|
30
|
+
<Box>
|
|
31
|
+
<Text color={message.role === "user" ? "cyan" : "green"} bold>
|
|
32
|
+
{message.role === "user" ? "👤 You" : "🤖 Assistant"}
|
|
33
|
+
</Text>
|
|
34
|
+
</Box>
|
|
35
|
+
)}
|
|
36
|
+
|
|
37
|
+
<Box flexDirection="column" gap={1}>
|
|
38
|
+
{message.blocks.map((block, blockIndex) => (
|
|
39
|
+
<Box key={blockIndex}>
|
|
40
|
+
{block.type === "text" && block.content.trim() && (
|
|
41
|
+
<Box>
|
|
42
|
+
{block.customCommandContent && (
|
|
43
|
+
<Text color="cyan" bold>
|
|
44
|
+
⚡{" "}
|
|
45
|
+
</Text>
|
|
46
|
+
)}
|
|
47
|
+
{block.source === MessageSource.HOOK && (
|
|
48
|
+
<Text color="magenta" bold>
|
|
49
|
+
🔗{" "}
|
|
50
|
+
</Text>
|
|
51
|
+
)}
|
|
52
|
+
{isStatic ? (
|
|
53
|
+
<Markdown>{block.content}</Markdown>
|
|
54
|
+
) : (
|
|
55
|
+
<Text>{block.content.split("\n").slice(-10).join("\n")}</Text>
|
|
56
|
+
)}
|
|
57
|
+
</Box>
|
|
58
|
+
)}
|
|
59
|
+
|
|
60
|
+
{block.type === "error" && (
|
|
61
|
+
<Box>
|
|
62
|
+
<Text color="red">❌ Error: {block.content}</Text>
|
|
63
|
+
</Box>
|
|
64
|
+
)}
|
|
65
|
+
|
|
66
|
+
{block.type === "diff" && (
|
|
67
|
+
<DiffViewer block={block} isStatic={isStatic} />
|
|
68
|
+
)}
|
|
69
|
+
|
|
70
|
+
{block.type === "command_output" && (
|
|
71
|
+
<CommandOutputDisplay block={block} isExpanded={isExpanded} />
|
|
72
|
+
)}
|
|
73
|
+
|
|
74
|
+
{block.type === "tool" && (
|
|
75
|
+
<ToolResultDisplay block={block} isExpanded={isExpanded} />
|
|
76
|
+
)}
|
|
77
|
+
|
|
78
|
+
{block.type === "image" && (
|
|
79
|
+
<Box>
|
|
80
|
+
<Text color="magenta" bold>
|
|
81
|
+
📷 Image
|
|
82
|
+
</Text>
|
|
83
|
+
{block.imageUrls && block.imageUrls.length > 0 && (
|
|
84
|
+
<Text color="gray" dimColor>
|
|
85
|
+
{" "}
|
|
86
|
+
({block.imageUrls.length})
|
|
87
|
+
</Text>
|
|
88
|
+
)}
|
|
89
|
+
</Box>
|
|
90
|
+
)}
|
|
91
|
+
|
|
92
|
+
{block.type === "memory" && <MemoryDisplay block={block} />}
|
|
93
|
+
|
|
94
|
+
{block.type === "compress" && (
|
|
95
|
+
<CompressDisplay block={block} isExpanded={isExpanded} />
|
|
96
|
+
)}
|
|
97
|
+
|
|
98
|
+
{block.type === "subagent" && <SubagentBlock block={block} />}
|
|
99
|
+
</Box>
|
|
100
|
+
))}
|
|
101
|
+
</Box>
|
|
102
|
+
</Box>
|
|
103
|
+
);
|
|
104
|
+
};
|