wave-code 0.0.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/README.md +120 -0
- package/bin/wave-code.js +16 -0
- package/dist/cli.d.ts +6 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +62 -0
- package/dist/components/App.d.ts +8 -0
- package/dist/components/App.d.ts.map +1 -0
- package/dist/components/App.js +10 -0
- package/dist/components/BashHistorySelector.d.ts +10 -0
- package/dist/components/BashHistorySelector.d.ts.map +1 -0
- package/dist/components/BashHistorySelector.js +83 -0
- package/dist/components/BashShellManager.d.ts +6 -0
- package/dist/components/BashShellManager.d.ts.map +1 -0
- package/dist/components/BashShellManager.js +116 -0
- package/dist/components/ChatInterface.d.ts +3 -0
- package/dist/components/ChatInterface.d.ts.map +1 -0
- package/dist/components/ChatInterface.js +31 -0
- package/dist/components/CommandOutputDisplay.d.ts +9 -0
- package/dist/components/CommandOutputDisplay.d.ts.map +1 -0
- package/dist/components/CommandOutputDisplay.js +40 -0
- package/dist/components/CommandSelector.d.ts +11 -0
- package/dist/components/CommandSelector.d.ts.map +1 -0
- package/dist/components/CommandSelector.js +60 -0
- package/dist/components/CompressDisplay.d.ts +9 -0
- package/dist/components/CompressDisplay.d.ts.map +1 -0
- package/dist/components/CompressDisplay.js +17 -0
- package/dist/components/DiffViewer.d.ts +9 -0
- package/dist/components/DiffViewer.d.ts.map +1 -0
- package/dist/components/DiffViewer.js +221 -0
- package/dist/components/FileSelector.d.ts +13 -0
- package/dist/components/FileSelector.d.ts.map +1 -0
- package/dist/components/FileSelector.js +48 -0
- package/dist/components/InputBox.d.ts +23 -0
- package/dist/components/InputBox.d.ts.map +1 -0
- package/dist/components/InputBox.js +124 -0
- package/dist/components/McpManager.d.ts +10 -0
- package/dist/components/McpManager.d.ts.map +1 -0
- package/dist/components/McpManager.js +123 -0
- package/dist/components/MemoryDisplay.d.ts +8 -0
- package/dist/components/MemoryDisplay.d.ts.map +1 -0
- package/dist/components/MemoryDisplay.js +25 -0
- package/dist/components/MemoryTypeSelector.d.ts +8 -0
- package/dist/components/MemoryTypeSelector.d.ts.map +1 -0
- package/dist/components/MemoryTypeSelector.js +38 -0
- package/dist/components/MessageList.d.ts +12 -0
- package/dist/components/MessageList.d.ts.map +1 -0
- package/dist/components/MessageList.js +36 -0
- package/dist/components/ToolResultDisplay.d.ts +9 -0
- package/dist/components/ToolResultDisplay.d.ts.map +1 -0
- package/dist/components/ToolResultDisplay.js +52 -0
- package/dist/contexts/useAppConfig.d.ts +11 -0
- package/dist/contexts/useAppConfig.d.ts.map +1 -0
- package/dist/contexts/useAppConfig.js +13 -0
- package/dist/contexts/useChat.d.ts +36 -0
- package/dist/contexts/useChat.d.ts.map +1 -0
- package/dist/contexts/useChat.js +208 -0
- package/dist/hooks/useBashHistorySelector.d.ts +15 -0
- package/dist/hooks/useBashHistorySelector.d.ts.map +1 -0
- package/dist/hooks/useBashHistorySelector.js +61 -0
- package/dist/hooks/useCommandSelector.d.ts +24 -0
- package/dist/hooks/useCommandSelector.d.ts.map +1 -0
- package/dist/hooks/useCommandSelector.js +98 -0
- package/dist/hooks/useFileSelector.d.ts +16 -0
- package/dist/hooks/useFileSelector.d.ts.map +1 -0
- package/dist/hooks/useFileSelector.js +174 -0
- package/dist/hooks/useImageManager.d.ts +13 -0
- package/dist/hooks/useImageManager.d.ts.map +1 -0
- package/dist/hooks/useImageManager.js +46 -0
- package/dist/hooks/useInputHistory.d.ts +11 -0
- package/dist/hooks/useInputHistory.d.ts.map +1 -0
- package/dist/hooks/useInputHistory.js +64 -0
- package/dist/hooks/useInputKeyboardHandler.d.ts +83 -0
- package/dist/hooks/useInputKeyboardHandler.d.ts.map +1 -0
- package/dist/hooks/useInputKeyboardHandler.js +507 -0
- package/dist/hooks/useInputState.d.ts +14 -0
- package/dist/hooks/useInputState.d.ts.map +1 -0
- package/dist/hooks/useInputState.js +57 -0
- package/dist/hooks/useMemoryTypeSelector.d.ts +9 -0
- package/dist/hooks/useMemoryTypeSelector.d.ts.map +1 -0
- package/dist/hooks/useMemoryTypeSelector.js +27 -0
- package/dist/hooks/usePagination.d.ts +20 -0
- package/dist/hooks/usePagination.d.ts.map +1 -0
- package/dist/hooks/usePagination.js +168 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +91 -0
- package/dist/plain-cli.d.ts +7 -0
- package/dist/plain-cli.d.ts.map +1 -0
- package/dist/plain-cli.js +49 -0
- package/dist/utils/clipboard.d.ts +22 -0
- package/dist/utils/clipboard.d.ts.map +1 -0
- package/dist/utils/clipboard.js +347 -0
- package/dist/utils/constants.d.ts +17 -0
- package/dist/utils/constants.d.ts.map +1 -0
- package/dist/utils/constants.js +18 -0
- package/dist/utils/logger.d.ts +72 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +245 -0
- package/package.json +60 -0
- package/src/cli.tsx +82 -0
- package/src/components/App.tsx +31 -0
- package/src/components/BashHistorySelector.tsx +163 -0
- package/src/components/BashShellManager.tsx +306 -0
- package/src/components/ChatInterface.tsx +88 -0
- package/src/components/CommandOutputDisplay.tsx +81 -0
- package/src/components/CommandSelector.tsx +144 -0
- package/src/components/CompressDisplay.tsx +58 -0
- package/src/components/DiffViewer.tsx +321 -0
- package/src/components/FileSelector.tsx +137 -0
- package/src/components/InputBox.tsx +310 -0
- package/src/components/McpManager.tsx +328 -0
- package/src/components/MemoryDisplay.tsx +62 -0
- package/src/components/MemoryTypeSelector.tsx +96 -0
- package/src/components/MessageList.tsx +215 -0
- package/src/components/ToolResultDisplay.tsx +138 -0
- package/src/contexts/useAppConfig.tsx +32 -0
- package/src/contexts/useChat.tsx +300 -0
- package/src/hooks/useBashHistorySelector.ts +77 -0
- package/src/hooks/useCommandSelector.ts +131 -0
- package/src/hooks/useFileSelector.ts +227 -0
- package/src/hooks/useImageManager.ts +64 -0
- package/src/hooks/useInputHistory.ts +74 -0
- package/src/hooks/useInputKeyboardHandler.ts +778 -0
- package/src/hooks/useInputState.ts +66 -0
- package/src/hooks/useMemoryTypeSelector.ts +40 -0
- package/src/hooks/usePagination.ts +203 -0
- package/src/index.ts +108 -0
- package/src/plain-cli.ts +66 -0
- package/src/utils/clipboard.ts +384 -0
- package/src/utils/constants.ts +22 -0
- package/src/utils/logger.ts +301 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import React, { useRef, useEffect } from "react";
|
|
2
|
+
import { Box } from "ink";
|
|
3
|
+
import { MessageList } from "./MessageList.js";
|
|
4
|
+
import { InputBox } from "./InputBox.js";
|
|
5
|
+
import { useChat } from "../contexts/useChat.js";
|
|
6
|
+
import type { Message } from "wave-agent-sdk";
|
|
7
|
+
|
|
8
|
+
export const ChatInterface: React.FC = () => {
|
|
9
|
+
const {
|
|
10
|
+
messages,
|
|
11
|
+
isLoading,
|
|
12
|
+
isCommandRunning,
|
|
13
|
+
userInputHistory,
|
|
14
|
+
isCompressing,
|
|
15
|
+
sendMessage,
|
|
16
|
+
abortMessage,
|
|
17
|
+
saveMemory,
|
|
18
|
+
mcpServers,
|
|
19
|
+
connectMcpServer,
|
|
20
|
+
disconnectMcpServer,
|
|
21
|
+
isExpanded,
|
|
22
|
+
latestTotalTokens,
|
|
23
|
+
slashCommands,
|
|
24
|
+
hasSlashCommand,
|
|
25
|
+
} = useChat();
|
|
26
|
+
|
|
27
|
+
// Create a ref to store messages in expanded mode
|
|
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]);
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<Box flexDirection="column" height="100%">
|
|
48
|
+
<Box flexGrow={1} flexDirection="column" paddingX={1}>
|
|
49
|
+
{isExpanded ? (
|
|
50
|
+
// Expanded mode uses messages from ref, loading and tokens are hardcoded to false and 0
|
|
51
|
+
<MessageList
|
|
52
|
+
messages={expandedMessagesRef.current}
|
|
53
|
+
isLoading={false}
|
|
54
|
+
isCommandRunning={false}
|
|
55
|
+
latestTotalTokens={0}
|
|
56
|
+
isExpanded={true}
|
|
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>
|
|
70
|
+
|
|
71
|
+
{!isExpanded && (
|
|
72
|
+
<InputBox
|
|
73
|
+
isLoading={isLoading}
|
|
74
|
+
isCommandRunning={isCommandRunning}
|
|
75
|
+
userInputHistory={userInputHistory}
|
|
76
|
+
sendMessage={sendMessage}
|
|
77
|
+
abortMessage={abortMessage}
|
|
78
|
+
saveMemory={saveMemory}
|
|
79
|
+
mcpServers={mcpServers}
|
|
80
|
+
connectMcpServer={connectMcpServer}
|
|
81
|
+
disconnectMcpServer={disconnectMcpServer}
|
|
82
|
+
slashCommands={slashCommands}
|
|
83
|
+
hasSlashCommand={hasSlashCommand}
|
|
84
|
+
/>
|
|
85
|
+
)}
|
|
86
|
+
</Box>
|
|
87
|
+
);
|
|
88
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import React, { useState, useEffect } from "react";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import type { CommandOutputBlock } from "wave-agent-sdk";
|
|
4
|
+
|
|
5
|
+
interface CommandOutputDisplayProps {
|
|
6
|
+
block: CommandOutputBlock;
|
|
7
|
+
isExpanded?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const CommandOutputDisplay: React.FC<CommandOutputDisplayProps> = ({
|
|
11
|
+
block,
|
|
12
|
+
isExpanded = false,
|
|
13
|
+
}) => {
|
|
14
|
+
const { command, output, isRunning, exitCode } = block;
|
|
15
|
+
const [isOverflowing, setIsOverflowing] = useState(false);
|
|
16
|
+
const MAX_LINES = 10; // Set maximum display lines
|
|
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
|
+
const getStatusColor = () => {
|
|
27
|
+
if (isRunning) return "yellow";
|
|
28
|
+
if (exitCode === 0) return "green";
|
|
29
|
+
if (exitCode !== null && exitCode !== 0) return "red";
|
|
30
|
+
return "gray"; // Unknown state
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const getStatusText = () => {
|
|
34
|
+
if (isRunning) return "🔄";
|
|
35
|
+
if (exitCode === 0) return "✅";
|
|
36
|
+
if (exitCode === 130) return "⚠️"; // SIGINT (Ctrl+C)
|
|
37
|
+
if (exitCode !== null && exitCode !== 0) return "❌";
|
|
38
|
+
return ""; // Don't display text for unknown state
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<Box flexDirection="column">
|
|
43
|
+
<Box>
|
|
44
|
+
<Text color="cyan">$ </Text>
|
|
45
|
+
<Text color="white">{command}</Text>
|
|
46
|
+
<Text color={getStatusColor()}> {getStatusText()}</Text>
|
|
47
|
+
</Box>
|
|
48
|
+
|
|
49
|
+
{output && (
|
|
50
|
+
<Box marginTop={1} flexDirection="column">
|
|
51
|
+
<Box
|
|
52
|
+
paddingLeft={2}
|
|
53
|
+
borderLeft
|
|
54
|
+
borderColor="gray"
|
|
55
|
+
flexDirection="column"
|
|
56
|
+
height={
|
|
57
|
+
isExpanded
|
|
58
|
+
? undefined
|
|
59
|
+
: Math.min(output.split("\n").length, MAX_LINES)
|
|
60
|
+
}
|
|
61
|
+
overflow="hidden"
|
|
62
|
+
>
|
|
63
|
+
<Text color="gray">
|
|
64
|
+
{isOverflowing
|
|
65
|
+
? output.split("\n").slice(-MAX_LINES).join("\n")
|
|
66
|
+
: output}
|
|
67
|
+
</Text>
|
|
68
|
+
</Box>
|
|
69
|
+
{isOverflowing && (
|
|
70
|
+
<Box paddingLeft={2} marginTop={1}>
|
|
71
|
+
<Text color="yellow" dimColor>
|
|
72
|
+
Content truncated ({output.split("\n").length} lines total,
|
|
73
|
+
showing last {MAX_LINES} lines)
|
|
74
|
+
</Text>
|
|
75
|
+
</Box>
|
|
76
|
+
)}
|
|
77
|
+
</Box>
|
|
78
|
+
)}
|
|
79
|
+
</Box>
|
|
80
|
+
);
|
|
81
|
+
};
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { Box, Text, useInput } from "ink";
|
|
3
|
+
import type { SlashCommand } from "wave-agent-sdk";
|
|
4
|
+
|
|
5
|
+
const AVAILABLE_COMMANDS: SlashCommand[] = [
|
|
6
|
+
{
|
|
7
|
+
id: "bashes",
|
|
8
|
+
name: "bashes",
|
|
9
|
+
description: "View and manage background bash shells",
|
|
10
|
+
handler: () => {}, // Handler here won't be used, actual processing is in the hook
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
id: "mcp",
|
|
14
|
+
name: "mcp",
|
|
15
|
+
description: "View and manage MCP servers",
|
|
16
|
+
handler: () => {}, // Handler here won't be used, actual processing is in the hook
|
|
17
|
+
},
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
export interface CommandSelectorProps {
|
|
21
|
+
searchQuery: string;
|
|
22
|
+
onSelect: (command: string) => void;
|
|
23
|
+
onInsert?: (command: string) => void; // New: Tab key to insert command into input box
|
|
24
|
+
onCancel: () => void;
|
|
25
|
+
commands?: SlashCommand[]; // New optional command list parameter
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const CommandSelector: React.FC<CommandSelectorProps> = ({
|
|
29
|
+
searchQuery,
|
|
30
|
+
onSelect,
|
|
31
|
+
onInsert,
|
|
32
|
+
onCancel,
|
|
33
|
+
commands = [], // Default to empty array
|
|
34
|
+
}) => {
|
|
35
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
36
|
+
|
|
37
|
+
// Merge agent commands and local commands
|
|
38
|
+
const allCommands = [...commands, ...AVAILABLE_COMMANDS];
|
|
39
|
+
|
|
40
|
+
// Filter command list
|
|
41
|
+
const filteredCommands = allCommands.filter(
|
|
42
|
+
(command) =>
|
|
43
|
+
!searchQuery ||
|
|
44
|
+
command.name.toLowerCase().includes(searchQuery.toLowerCase()),
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
useInput((input, key) => {
|
|
48
|
+
if (key.return) {
|
|
49
|
+
if (
|
|
50
|
+
filteredCommands.length > 0 &&
|
|
51
|
+
selectedIndex < filteredCommands.length
|
|
52
|
+
) {
|
|
53
|
+
const selectedCommand = filteredCommands[selectedIndex].name;
|
|
54
|
+
onSelect(selectedCommand);
|
|
55
|
+
}
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (key.tab && onInsert) {
|
|
60
|
+
if (
|
|
61
|
+
filteredCommands.length > 0 &&
|
|
62
|
+
selectedIndex < filteredCommands.length
|
|
63
|
+
) {
|
|
64
|
+
const selectedCommand = filteredCommands[selectedIndex].name;
|
|
65
|
+
onInsert(selectedCommand);
|
|
66
|
+
}
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (key.escape) {
|
|
71
|
+
onCancel();
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (key.upArrow) {
|
|
76
|
+
setSelectedIndex(Math.max(0, selectedIndex - 1));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (key.downArrow) {
|
|
81
|
+
setSelectedIndex(
|
|
82
|
+
Math.min(filteredCommands.length - 1, selectedIndex + 1),
|
|
83
|
+
);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
if (filteredCommands.length === 0) {
|
|
89
|
+
return (
|
|
90
|
+
<Box
|
|
91
|
+
flexDirection="column"
|
|
92
|
+
borderStyle="single"
|
|
93
|
+
borderColor="yellow"
|
|
94
|
+
padding={1}
|
|
95
|
+
marginBottom={1}
|
|
96
|
+
>
|
|
97
|
+
<Text color="yellow">No commands found for "{searchQuery}"</Text>
|
|
98
|
+
<Text dimColor>Press Escape to cancel</Text>
|
|
99
|
+
</Box>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<Box
|
|
105
|
+
flexDirection="column"
|
|
106
|
+
borderStyle="single"
|
|
107
|
+
borderColor="magenta"
|
|
108
|
+
padding={1}
|
|
109
|
+
gap={1}
|
|
110
|
+
marginBottom={1}
|
|
111
|
+
>
|
|
112
|
+
<Box>
|
|
113
|
+
<Text color="magenta" bold>
|
|
114
|
+
Command Selector {searchQuery && `(filtering: "${searchQuery}")`}
|
|
115
|
+
</Text>
|
|
116
|
+
</Box>
|
|
117
|
+
|
|
118
|
+
{filteredCommands.map((command, index) => (
|
|
119
|
+
<Box key={command.name} flexDirection="column">
|
|
120
|
+
<Text
|
|
121
|
+
color={index === selectedIndex ? "black" : "white"}
|
|
122
|
+
backgroundColor={index === selectedIndex ? "magenta" : undefined}
|
|
123
|
+
>
|
|
124
|
+
{index === selectedIndex ? "▶ " : " "}/{command.name}
|
|
125
|
+
</Text>
|
|
126
|
+
{index === selectedIndex && (
|
|
127
|
+
<Box marginLeft={4}>
|
|
128
|
+
<Text color="gray" dimColor>
|
|
129
|
+
{command.description}
|
|
130
|
+
</Text>
|
|
131
|
+
</Box>
|
|
132
|
+
)}
|
|
133
|
+
</Box>
|
|
134
|
+
))}
|
|
135
|
+
|
|
136
|
+
<Box>
|
|
137
|
+
<Text dimColor>
|
|
138
|
+
↑↓ navigate • Enter execute • {onInsert ? "Tab insert • " : ""}Esc
|
|
139
|
+
cancel
|
|
140
|
+
</Text>
|
|
141
|
+
</Box>
|
|
142
|
+
</Box>
|
|
143
|
+
);
|
|
144
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import React, { useMemo } from "react";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import type { CompressBlock } from "wave-agent-sdk";
|
|
4
|
+
|
|
5
|
+
interface CompressDisplayProps {
|
|
6
|
+
block: CompressBlock;
|
|
7
|
+
isExpanded?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const CompressDisplay: React.FC<CompressDisplayProps> = ({
|
|
11
|
+
block,
|
|
12
|
+
isExpanded = false,
|
|
13
|
+
}) => {
|
|
14
|
+
const { content } = block;
|
|
15
|
+
const MAX_LINES = 3; // Set maximum display lines for compressed content
|
|
16
|
+
|
|
17
|
+
const { displayContent, isOverflowing } = useMemo(() => {
|
|
18
|
+
if (!content) {
|
|
19
|
+
return { displayContent: "", isOverflowing: false };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const lines = content.split("\n");
|
|
23
|
+
const overflow = !isExpanded && lines.length > MAX_LINES;
|
|
24
|
+
|
|
25
|
+
const display = overflow ? lines.slice(0, MAX_LINES).join("\n") : content;
|
|
26
|
+
|
|
27
|
+
return { displayContent: display, isOverflowing: overflow };
|
|
28
|
+
}, [content, isExpanded]);
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<Box flexDirection="column">
|
|
32
|
+
<Box>
|
|
33
|
+
<Text>📦 Compressed Messages</Text>
|
|
34
|
+
</Box>
|
|
35
|
+
|
|
36
|
+
{content && (
|
|
37
|
+
<Box marginTop={1} flexDirection="column">
|
|
38
|
+
<Box
|
|
39
|
+
paddingLeft={2}
|
|
40
|
+
borderLeft
|
|
41
|
+
borderColor="gray"
|
|
42
|
+
flexDirection="column"
|
|
43
|
+
>
|
|
44
|
+
<Text color="white">{displayContent}</Text>
|
|
45
|
+
</Box>
|
|
46
|
+
{isOverflowing && (
|
|
47
|
+
<Box paddingLeft={2} marginTop={1}>
|
|
48
|
+
<Text color="yellow" dimColor>
|
|
49
|
+
Content truncated ({content.split("\n").length} lines total,
|
|
50
|
+
showing first {MAX_LINES} lines. Press Ctrl+O to expand.
|
|
51
|
+
</Text>
|
|
52
|
+
</Box>
|
|
53
|
+
)}
|
|
54
|
+
</Box>
|
|
55
|
+
)}
|
|
56
|
+
</Box>
|
|
57
|
+
);
|
|
58
|
+
};
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import React, { useMemo } from "react";
|
|
2
|
+
import { Text, Box } from "ink";
|
|
3
|
+
import { diffWords } from "diff";
|
|
4
|
+
import type { DiffBlock } from "wave-agent-sdk";
|
|
5
|
+
|
|
6
|
+
interface DiffViewerProps {
|
|
7
|
+
block: DiffBlock;
|
|
8
|
+
isExpanded?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Render word-level diff
|
|
12
|
+
const renderWordLevelDiff = (removedLine: string, addedLine: string) => {
|
|
13
|
+
const changes = diffWords(removedLine, addedLine);
|
|
14
|
+
|
|
15
|
+
const removedParts: React.ReactNode[] = [];
|
|
16
|
+
const addedParts: React.ReactNode[] = [];
|
|
17
|
+
|
|
18
|
+
changes.forEach((part, index) => {
|
|
19
|
+
if (part.removed) {
|
|
20
|
+
removedParts.push(
|
|
21
|
+
<Text key={`removed-${index}`} color="black" backgroundColor="red">
|
|
22
|
+
{part.value}
|
|
23
|
+
</Text>,
|
|
24
|
+
);
|
|
25
|
+
} else if (part.added) {
|
|
26
|
+
addedParts.push(
|
|
27
|
+
<Text key={`added-${index}`} color="black" backgroundColor="green">
|
|
28
|
+
{part.value}
|
|
29
|
+
</Text>,
|
|
30
|
+
);
|
|
31
|
+
} else {
|
|
32
|
+
// Unchanged parts, need to display on both sides
|
|
33
|
+
removedParts.push(
|
|
34
|
+
<Text key={`removed-unchanged-${index}`} color="red">
|
|
35
|
+
{part.value}
|
|
36
|
+
</Text>,
|
|
37
|
+
);
|
|
38
|
+
addedParts.push(
|
|
39
|
+
<Text key={`added-unchanged-${index}`} color="green">
|
|
40
|
+
{part.value}
|
|
41
|
+
</Text>,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
return { removedParts, addedParts };
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const DiffViewer: React.FC<DiffViewerProps> = ({
|
|
50
|
+
block,
|
|
51
|
+
isExpanded = false,
|
|
52
|
+
}) => {
|
|
53
|
+
const { diffResult } = block;
|
|
54
|
+
|
|
55
|
+
const diffLines = useMemo(() => {
|
|
56
|
+
if (!diffResult) return [];
|
|
57
|
+
|
|
58
|
+
const lines: Array<{
|
|
59
|
+
content: string;
|
|
60
|
+
type: "added" | "removed" | "unchanged" | "separator";
|
|
61
|
+
lineNumber?: number;
|
|
62
|
+
rawContent?: string; // Store original content for word-level comparison
|
|
63
|
+
wordDiff?: {
|
|
64
|
+
removedParts: React.ReactNode[];
|
|
65
|
+
addedParts: React.ReactNode[];
|
|
66
|
+
};
|
|
67
|
+
}> = [];
|
|
68
|
+
|
|
69
|
+
let originalLineNum = 1;
|
|
70
|
+
let modifiedLineNum = 1;
|
|
71
|
+
const maxContext = 3; // Show at most 3 lines of context
|
|
72
|
+
|
|
73
|
+
// Buffer for storing context
|
|
74
|
+
let contextBuffer: Array<{
|
|
75
|
+
content: string;
|
|
76
|
+
type: "unchanged";
|
|
77
|
+
lineNumber: number;
|
|
78
|
+
}> = [];
|
|
79
|
+
|
|
80
|
+
let hasAnyChanges = false;
|
|
81
|
+
let afterChangeContext = 0;
|
|
82
|
+
|
|
83
|
+
// Temporarily store adjacent deleted and added lines for word-level comparison
|
|
84
|
+
let pendingRemovedLines: Array<{
|
|
85
|
+
content: string;
|
|
86
|
+
rawContent: string;
|
|
87
|
+
lineNumber: number;
|
|
88
|
+
}> = [];
|
|
89
|
+
|
|
90
|
+
const flushPendingLines = () => {
|
|
91
|
+
pendingRemovedLines.forEach((line) => {
|
|
92
|
+
lines.push({
|
|
93
|
+
content: line.content,
|
|
94
|
+
type: "removed",
|
|
95
|
+
lineNumber: line.lineNumber,
|
|
96
|
+
rawContent: line.rawContent,
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
pendingRemovedLines = [];
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
diffResult.forEach(
|
|
103
|
+
(part: { value: string; added?: boolean; removed?: boolean }) => {
|
|
104
|
+
const partLines = part.value.split("\n");
|
|
105
|
+
// Remove the last empty line (produced by split)
|
|
106
|
+
if (partLines[partLines.length - 1] === "") {
|
|
107
|
+
partLines.pop();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (part.removed) {
|
|
111
|
+
// If this is the first change encountered, add preceding context
|
|
112
|
+
if (!hasAnyChanges) {
|
|
113
|
+
// Take the last few lines from the buffer as preceding context
|
|
114
|
+
const preContext = contextBuffer.slice(-maxContext);
|
|
115
|
+
if (contextBuffer.length > maxContext) {
|
|
116
|
+
lines.push({
|
|
117
|
+
content: "...",
|
|
118
|
+
type: "separator",
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
lines.push(...preContext);
|
|
122
|
+
} else if (afterChangeContext > maxContext) {
|
|
123
|
+
// If there's too much context after the previous change, add a separator
|
|
124
|
+
lines.push({
|
|
125
|
+
content: "...",
|
|
126
|
+
type: "separator",
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Temporarily store deleted lines, waiting for possible added lines for word-level comparison
|
|
131
|
+
partLines.forEach((line: string) => {
|
|
132
|
+
pendingRemovedLines.push({
|
|
133
|
+
content: `- ${line}`,
|
|
134
|
+
rawContent: line,
|
|
135
|
+
lineNumber: originalLineNum++,
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
hasAnyChanges = true;
|
|
140
|
+
afterChangeContext = 0;
|
|
141
|
+
contextBuffer = []; // Clear buffer
|
|
142
|
+
} else if (part.added) {
|
|
143
|
+
// If this is the first change encountered, add preceding context
|
|
144
|
+
if (!hasAnyChanges) {
|
|
145
|
+
const preContext = contextBuffer.slice(-maxContext);
|
|
146
|
+
if (contextBuffer.length > maxContext) {
|
|
147
|
+
lines.push({
|
|
148
|
+
content: "...",
|
|
149
|
+
type: "separator",
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
lines.push(...preContext);
|
|
153
|
+
} else if (afterChangeContext > maxContext) {
|
|
154
|
+
lines.push({
|
|
155
|
+
content: "...",
|
|
156
|
+
type: "separator",
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Process added lines, try to do word-level comparison with pending deleted lines
|
|
161
|
+
partLines.forEach((line: string, index: number) => {
|
|
162
|
+
if (index < pendingRemovedLines.length) {
|
|
163
|
+
// Has corresponding deleted line, perform word-level comparison
|
|
164
|
+
const removedLine = pendingRemovedLines[index];
|
|
165
|
+
const wordDiff = renderWordLevelDiff(
|
|
166
|
+
removedLine.rawContent,
|
|
167
|
+
line,
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
// Add deleted line (with word-level highlighting)
|
|
171
|
+
lines.push({
|
|
172
|
+
content: `- ${removedLine.rawContent}`,
|
|
173
|
+
type: "removed",
|
|
174
|
+
lineNumber: removedLine.lineNumber,
|
|
175
|
+
rawContent: removedLine.rawContent,
|
|
176
|
+
wordDiff: {
|
|
177
|
+
removedParts: wordDiff.removedParts,
|
|
178
|
+
addedParts: [],
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// Add added line (with word-level highlighting)
|
|
183
|
+
lines.push({
|
|
184
|
+
content: `+ ${line}`,
|
|
185
|
+
type: "added",
|
|
186
|
+
lineNumber: modifiedLineNum++,
|
|
187
|
+
rawContent: line,
|
|
188
|
+
wordDiff: { removedParts: [], addedParts: wordDiff.addedParts },
|
|
189
|
+
});
|
|
190
|
+
} else {
|
|
191
|
+
// No corresponding deleted line, directly add the added line
|
|
192
|
+
lines.push({
|
|
193
|
+
content: `+ ${line}`,
|
|
194
|
+
type: "added",
|
|
195
|
+
lineNumber: modifiedLineNum++,
|
|
196
|
+
rawContent: line,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// If there are more deleted lines than added lines, add remaining deleted lines
|
|
202
|
+
for (let i = partLines.length; i < pendingRemovedLines.length; i++) {
|
|
203
|
+
const removedLine = pendingRemovedLines[i];
|
|
204
|
+
lines.push({
|
|
205
|
+
content: removedLine.content,
|
|
206
|
+
type: "removed",
|
|
207
|
+
lineNumber: removedLine.lineNumber,
|
|
208
|
+
rawContent: removedLine.rawContent,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
pendingRemovedLines = []; // Clear pending deleted lines
|
|
213
|
+
hasAnyChanges = true;
|
|
214
|
+
afterChangeContext = 0;
|
|
215
|
+
contextBuffer = [];
|
|
216
|
+
} else {
|
|
217
|
+
// Before processing unchanged lines, first clear pending deleted lines
|
|
218
|
+
flushPendingLines();
|
|
219
|
+
|
|
220
|
+
// Process unchanged lines
|
|
221
|
+
partLines.forEach((line: string) => {
|
|
222
|
+
const contextLine = {
|
|
223
|
+
content: ` ${line}`,
|
|
224
|
+
type: "unchanged" as const,
|
|
225
|
+
lineNumber: originalLineNum,
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
if (hasAnyChanges) {
|
|
229
|
+
// If there are already changes, these are post-change context
|
|
230
|
+
if (afterChangeContext < maxContext) {
|
|
231
|
+
lines.push(contextLine);
|
|
232
|
+
afterChangeContext++;
|
|
233
|
+
}
|
|
234
|
+
} else {
|
|
235
|
+
// If no changes yet, add to buffer
|
|
236
|
+
contextBuffer.push(contextLine);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
originalLineNum++;
|
|
240
|
+
modifiedLineNum++;
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
},
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
// Handle remaining deleted lines at the end
|
|
247
|
+
flushPendingLines();
|
|
248
|
+
|
|
249
|
+
// Only limit displayed lines in collapsed state
|
|
250
|
+
if (!isExpanded) {
|
|
251
|
+
const MAX_DISPLAY_LINES = 50;
|
|
252
|
+
if (lines.length > MAX_DISPLAY_LINES) {
|
|
253
|
+
const truncatedLines = lines.slice(0, MAX_DISPLAY_LINES);
|
|
254
|
+
truncatedLines.push({
|
|
255
|
+
content: `... (${lines.length - MAX_DISPLAY_LINES} more lines truncated, press Ctrl+O to expand)`,
|
|
256
|
+
type: "separator",
|
|
257
|
+
});
|
|
258
|
+
return truncatedLines;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return lines;
|
|
263
|
+
}, [diffResult, isExpanded]);
|
|
264
|
+
|
|
265
|
+
if (!diffResult || diffResult.length === 0) {
|
|
266
|
+
return (
|
|
267
|
+
<Box flexDirection="column">
|
|
268
|
+
<Text color="gray">No changes detected</Text>
|
|
269
|
+
</Box>
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return (
|
|
274
|
+
<Box flexDirection="column">
|
|
275
|
+
<Box flexDirection="column">
|
|
276
|
+
<Box flexDirection="column">
|
|
277
|
+
{diffLines.map((line, index) => {
|
|
278
|
+
// If has word-level diff, render special effects
|
|
279
|
+
if (line.wordDiff) {
|
|
280
|
+
const prefix = line.type === "removed" ? "- " : "+ ";
|
|
281
|
+
const parts =
|
|
282
|
+
line.type === "removed"
|
|
283
|
+
? line.wordDiff.removedParts
|
|
284
|
+
: line.wordDiff.addedParts;
|
|
285
|
+
|
|
286
|
+
return (
|
|
287
|
+
<Box key={index} flexDirection="row">
|
|
288
|
+
<Text color={line.type === "removed" ? "red" : "green"}>
|
|
289
|
+
{prefix}
|
|
290
|
+
</Text>
|
|
291
|
+
<Box flexDirection="row" flexWrap="wrap">
|
|
292
|
+
{parts}
|
|
293
|
+
</Box>
|
|
294
|
+
</Box>
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Normal rendering
|
|
299
|
+
return (
|
|
300
|
+
<Text
|
|
301
|
+
key={index}
|
|
302
|
+
color={
|
|
303
|
+
line.type === "added"
|
|
304
|
+
? "green"
|
|
305
|
+
: line.type === "removed"
|
|
306
|
+
? "red"
|
|
307
|
+
: line.type === "separator"
|
|
308
|
+
? "gray"
|
|
309
|
+
: "white"
|
|
310
|
+
}
|
|
311
|
+
dimColor={line.type === "separator"}
|
|
312
|
+
>
|
|
313
|
+
{line.content}
|
|
314
|
+
</Text>
|
|
315
|
+
);
|
|
316
|
+
})}
|
|
317
|
+
</Box>
|
|
318
|
+
</Box>
|
|
319
|
+
</Box>
|
|
320
|
+
);
|
|
321
|
+
};
|