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,138 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import type { ToolBlock } from "wave-agent-sdk";
|
|
4
|
+
|
|
5
|
+
interface ToolResultDisplayProps {
|
|
6
|
+
block: ToolBlock;
|
|
7
|
+
isExpanded?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const ToolResultDisplay: React.FC<ToolResultDisplayProps> = ({
|
|
11
|
+
block,
|
|
12
|
+
isExpanded = false,
|
|
13
|
+
}) => {
|
|
14
|
+
const { parameters, result, compactParams, isRunning, success, error, name } =
|
|
15
|
+
block;
|
|
16
|
+
|
|
17
|
+
// Directly use compactParams
|
|
18
|
+
// (no change needed as we destructured it above)
|
|
19
|
+
|
|
20
|
+
const getStatusColor = () => {
|
|
21
|
+
if (isRunning) return "yellow";
|
|
22
|
+
if (success) return "green";
|
|
23
|
+
if (error || success === false) return "red";
|
|
24
|
+
return "gray"; // Unknown state or no state information
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const getStatusText = () => {
|
|
28
|
+
if (isRunning) return "🔄";
|
|
29
|
+
if (success) return "";
|
|
30
|
+
if (error || success === false) return "❌ Failed";
|
|
31
|
+
return ""; // Don't display text for unknown state
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const hasImages = () => {
|
|
35
|
+
return block.images && block.images.length > 0;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const getImageIndicator = () => {
|
|
39
|
+
if (!hasImages()) return "";
|
|
40
|
+
const imageCount = block.images!.length;
|
|
41
|
+
return imageCount === 1 ? "🖼️" : `🖼️×${imageCount}`;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const toolName = name ? String(name) : "Tool";
|
|
45
|
+
|
|
46
|
+
// Get shortResult, if not available show last 5 lines of result
|
|
47
|
+
const getShortResult = () => {
|
|
48
|
+
if (block.shortResult) {
|
|
49
|
+
return block.shortResult;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// If no shortResult but has result, return last 5 lines
|
|
53
|
+
if (block.result) {
|
|
54
|
+
const lines = block.result.split("\n");
|
|
55
|
+
if (lines.length > 5) {
|
|
56
|
+
return lines.slice(-5).join("\n");
|
|
57
|
+
}
|
|
58
|
+
return block.result;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return null;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const shortResult = getShortResult();
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<Box flexDirection="column" gap={1}>
|
|
68
|
+
<Box>
|
|
69
|
+
<Text color="magenta">🔧 </Text>
|
|
70
|
+
<Text color="white">{toolName}</Text>
|
|
71
|
+
{/* Display compactParams in collapsed state */}
|
|
72
|
+
{!isExpanded && compactParams && (
|
|
73
|
+
<Text color="gray"> ({compactParams})</Text>
|
|
74
|
+
)}
|
|
75
|
+
<Text color={getStatusColor()}> {getStatusText()}</Text>
|
|
76
|
+
{/* Display image indicator */}
|
|
77
|
+
{hasImages() && <Text color="blue"> {getImageIndicator()}</Text>}
|
|
78
|
+
</Box>
|
|
79
|
+
|
|
80
|
+
{/* Display shortResult in collapsed state */}
|
|
81
|
+
{!isExpanded && shortResult && (
|
|
82
|
+
<Box
|
|
83
|
+
paddingLeft={2}
|
|
84
|
+
borderLeft
|
|
85
|
+
borderColor="gray"
|
|
86
|
+
flexDirection="column"
|
|
87
|
+
>
|
|
88
|
+
{shortResult.split("\n").map((line, index) => (
|
|
89
|
+
<Text key={index} color="white">
|
|
90
|
+
{line}
|
|
91
|
+
</Text>
|
|
92
|
+
))}
|
|
93
|
+
</Box>
|
|
94
|
+
)}
|
|
95
|
+
|
|
96
|
+
{/* Display complete parameters in expanded state */}
|
|
97
|
+
{isExpanded && parameters && (
|
|
98
|
+
<Box
|
|
99
|
+
paddingLeft={2}
|
|
100
|
+
borderLeft
|
|
101
|
+
borderColor="gray"
|
|
102
|
+
flexDirection="column"
|
|
103
|
+
>
|
|
104
|
+
<Text color="cyan" bold>
|
|
105
|
+
Parameters:
|
|
106
|
+
</Text>
|
|
107
|
+
<Text color="gray">{parameters}</Text>
|
|
108
|
+
</Box>
|
|
109
|
+
)}
|
|
110
|
+
|
|
111
|
+
{/* Display complete result in expanded state */}
|
|
112
|
+
{isExpanded && result && (
|
|
113
|
+
<Box flexDirection="column">
|
|
114
|
+
<Box
|
|
115
|
+
paddingLeft={2}
|
|
116
|
+
borderLeft
|
|
117
|
+
borderColor="green"
|
|
118
|
+
flexDirection="column"
|
|
119
|
+
>
|
|
120
|
+
<Text color="cyan" bold>
|
|
121
|
+
Result:
|
|
122
|
+
</Text>
|
|
123
|
+
<Text color="white">{result}</Text>
|
|
124
|
+
</Box>
|
|
125
|
+
</Box>
|
|
126
|
+
)}
|
|
127
|
+
|
|
128
|
+
{/* Error information always displayed */}
|
|
129
|
+
{error && (
|
|
130
|
+
<Box>
|
|
131
|
+
<Text color="red">
|
|
132
|
+
Error: {typeof error === "string" ? error : String(error)}
|
|
133
|
+
</Text>
|
|
134
|
+
</Box>
|
|
135
|
+
)}
|
|
136
|
+
</Box>
|
|
137
|
+
);
|
|
138
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import React, { createContext, useContext } from "react";
|
|
2
|
+
|
|
3
|
+
export interface AppConfig {
|
|
4
|
+
restoreSessionId?: string;
|
|
5
|
+
continueLastSession?: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const AppContext = createContext<AppConfig | null>(null);
|
|
9
|
+
|
|
10
|
+
export const useAppConfig = () => {
|
|
11
|
+
const context = useContext(AppContext);
|
|
12
|
+
if (!context) {
|
|
13
|
+
throw new Error("useAppConfig must be used within AppProvider");
|
|
14
|
+
}
|
|
15
|
+
return context;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export interface AppProviderProps extends AppConfig {
|
|
19
|
+
children: React.ReactNode;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const AppProvider: React.FC<AppProviderProps> = ({
|
|
23
|
+
restoreSessionId,
|
|
24
|
+
continueLastSession,
|
|
25
|
+
children,
|
|
26
|
+
}) => {
|
|
27
|
+
return (
|
|
28
|
+
<AppContext.Provider value={{ restoreSessionId, continueLastSession }}>
|
|
29
|
+
{children}
|
|
30
|
+
</AppContext.Provider>
|
|
31
|
+
);
|
|
32
|
+
};
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
createContext,
|
|
3
|
+
useContext,
|
|
4
|
+
useCallback,
|
|
5
|
+
useRef,
|
|
6
|
+
useEffect,
|
|
7
|
+
useState,
|
|
8
|
+
} from "react";
|
|
9
|
+
import { useInput } from "ink";
|
|
10
|
+
import { useAppConfig } from "./useAppConfig.js";
|
|
11
|
+
import type {
|
|
12
|
+
Message,
|
|
13
|
+
McpServerStatus,
|
|
14
|
+
BackgroundShell,
|
|
15
|
+
SlashCommand,
|
|
16
|
+
} from "wave-agent-sdk";
|
|
17
|
+
import { Agent, AgentCallbacks } from "wave-agent-sdk";
|
|
18
|
+
import { logger } from "../utils/logger.js";
|
|
19
|
+
|
|
20
|
+
// Main Chat Context
|
|
21
|
+
export interface ChatContextType {
|
|
22
|
+
messages: Message[];
|
|
23
|
+
isLoading: boolean;
|
|
24
|
+
isCommandRunning: boolean;
|
|
25
|
+
isCompressing: boolean;
|
|
26
|
+
userInputHistory: string[];
|
|
27
|
+
// Message display state
|
|
28
|
+
isExpanded: boolean;
|
|
29
|
+
// AI functionality
|
|
30
|
+
sessionId: string;
|
|
31
|
+
sendMessage: (
|
|
32
|
+
content: string,
|
|
33
|
+
images?: Array<{ path: string; mimeType: string }>,
|
|
34
|
+
) => Promise<void>;
|
|
35
|
+
abortMessage: () => void;
|
|
36
|
+
latestTotalTokens: number;
|
|
37
|
+
// Memory functionality
|
|
38
|
+
saveMemory: (message: string, type: "project" | "user") => Promise<void>;
|
|
39
|
+
// MCP functionality
|
|
40
|
+
mcpServers: McpServerStatus[];
|
|
41
|
+
connectMcpServer: (serverName: string) => Promise<boolean>;
|
|
42
|
+
disconnectMcpServer: (serverName: string) => Promise<boolean>;
|
|
43
|
+
// Background bash shells
|
|
44
|
+
backgroundShells: BackgroundShell[];
|
|
45
|
+
getBackgroundShellOutput: (
|
|
46
|
+
shellId: string,
|
|
47
|
+
) => { stdout: string; stderr: string; status: string } | null;
|
|
48
|
+
killBackgroundShell: (shellId: string) => boolean;
|
|
49
|
+
// Slash Command functionality
|
|
50
|
+
slashCommands: SlashCommand[];
|
|
51
|
+
hasSlashCommand: (commandId: string) => boolean;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const ChatContext = createContext<ChatContextType | null>(null);
|
|
55
|
+
|
|
56
|
+
export const useChat = () => {
|
|
57
|
+
const context = useContext(ChatContext);
|
|
58
|
+
if (!context) {
|
|
59
|
+
throw new Error("useChat must be used within ChatProvider");
|
|
60
|
+
}
|
|
61
|
+
return context;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export interface ChatProviderProps {
|
|
65
|
+
children: React.ReactNode;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const ChatProvider: React.FC<ChatProviderProps> = ({ children }) => {
|
|
69
|
+
const { restoreSessionId, continueLastSession } = useAppConfig();
|
|
70
|
+
|
|
71
|
+
// Message Display State
|
|
72
|
+
const [isExpanded, setIsExpanded] = useState(false);
|
|
73
|
+
|
|
74
|
+
// AI State
|
|
75
|
+
const [messages, setMessages] = useState<Message[]>([]);
|
|
76
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
77
|
+
const [latestTotalTokens, setlatestTotalTokens] = useState(0);
|
|
78
|
+
const [sessionId, setSessionId] = useState("");
|
|
79
|
+
const [isCommandRunning, setIsCommandRunning] = useState(false);
|
|
80
|
+
const [isCompressing, setIsCompressing] = useState(false);
|
|
81
|
+
const [userInputHistory, setUserInputHistory] = useState<string[]>([]);
|
|
82
|
+
|
|
83
|
+
// MCP State
|
|
84
|
+
const [mcpServers, setMcpServers] = useState<McpServerStatus[]>([]);
|
|
85
|
+
|
|
86
|
+
// Background bash shells state
|
|
87
|
+
const [backgroundShells, setBackgroundShells] = useState<BackgroundShell[]>(
|
|
88
|
+
[],
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
// Command state
|
|
92
|
+
const [slashCommands, setSlashCommands] = useState<SlashCommand[]>([]);
|
|
93
|
+
|
|
94
|
+
const agentRef = useRef<Agent | null>(null);
|
|
95
|
+
|
|
96
|
+
// Listen for Ctrl+O hotkey to toggle collapse/expand state
|
|
97
|
+
useInput((input, key) => {
|
|
98
|
+
if (key.ctrl && input === "o") {
|
|
99
|
+
setIsExpanded((prev) => !prev);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// Initialize AI manager
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
const initializeAgent = async () => {
|
|
106
|
+
const callbacks: AgentCallbacks = {
|
|
107
|
+
onMessagesChange: (newMessages) => {
|
|
108
|
+
setMessages([...newMessages]);
|
|
109
|
+
},
|
|
110
|
+
onServersChange: (servers) => {
|
|
111
|
+
setMcpServers([...servers]);
|
|
112
|
+
},
|
|
113
|
+
onSessionIdChange: (sessionId) => {
|
|
114
|
+
setSessionId(sessionId);
|
|
115
|
+
},
|
|
116
|
+
onLatestTotalTokensChange: (tokens) => {
|
|
117
|
+
setlatestTotalTokens(tokens);
|
|
118
|
+
},
|
|
119
|
+
onUserInputHistoryChange: (history) => {
|
|
120
|
+
setUserInputHistory([...history]);
|
|
121
|
+
},
|
|
122
|
+
onCompressionStateChange: (isCompressingState) => {
|
|
123
|
+
setIsCompressing(isCompressingState);
|
|
124
|
+
},
|
|
125
|
+
onShellsChange: (shells) => {
|
|
126
|
+
setBackgroundShells([...shells]);
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
const agent = await Agent.create({
|
|
132
|
+
callbacks,
|
|
133
|
+
restoreSessionId,
|
|
134
|
+
continueLastSession,
|
|
135
|
+
logger,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
agentRef.current = agent;
|
|
139
|
+
|
|
140
|
+
// Get initial state
|
|
141
|
+
setSessionId(agent.sessionId);
|
|
142
|
+
setMessages(agent.messages);
|
|
143
|
+
setIsLoading(agent.isLoading);
|
|
144
|
+
setlatestTotalTokens(agent.latestTotalTokens);
|
|
145
|
+
setIsCommandRunning(agent.isCommandRunning);
|
|
146
|
+
setIsCompressing(agent.isCompressing);
|
|
147
|
+
setUserInputHistory(agent.userInputHistory);
|
|
148
|
+
|
|
149
|
+
// Get initial MCP servers state
|
|
150
|
+
const mcpServers = agent.getMcpServers?.() || [];
|
|
151
|
+
setMcpServers(mcpServers);
|
|
152
|
+
|
|
153
|
+
// Get initial commands
|
|
154
|
+
const agentSlashCommands = agent.getSlashCommands?.() || [];
|
|
155
|
+
setSlashCommands(agentSlashCommands);
|
|
156
|
+
} catch (error) {
|
|
157
|
+
console.error("Failed to initialize AI manager:", error);
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
initializeAgent();
|
|
162
|
+
}, [restoreSessionId, continueLastSession]);
|
|
163
|
+
|
|
164
|
+
// Cleanup on unmount
|
|
165
|
+
useEffect(() => {
|
|
166
|
+
return () => {
|
|
167
|
+
if (agentRef.current) {
|
|
168
|
+
agentRef.current.destroy();
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
}, []);
|
|
172
|
+
|
|
173
|
+
// Send message function (including judgment logic)
|
|
174
|
+
const sendMessage = useCallback(
|
|
175
|
+
async (
|
|
176
|
+
content: string,
|
|
177
|
+
images?: Array<{ path: string; mimeType: string }>,
|
|
178
|
+
) => {
|
|
179
|
+
// Check if there's content to send (text content or image attachments)
|
|
180
|
+
const hasTextContent = content.trim();
|
|
181
|
+
const hasImageAttachments = images && images.length > 0;
|
|
182
|
+
|
|
183
|
+
if (!hasTextContent && !hasImageAttachments) return;
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
// Handle memory mode - check if it's a memory message (starts with # and only one line)
|
|
187
|
+
if (content.startsWith("#") && !content.includes("\n")) {
|
|
188
|
+
const memoryText = content.substring(1).trim();
|
|
189
|
+
if (!memoryText) return;
|
|
190
|
+
|
|
191
|
+
// In memory mode, don't add user message, only wait for user to choose memory type then add assistant message
|
|
192
|
+
// Don't auto-save, wait for user to choose memory type
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Handle bash mode - check if it's a bash command (starts with ! and only one line)
|
|
197
|
+
if (content.startsWith("!") && !content.includes("\n")) {
|
|
198
|
+
const command = content.substring(1).trim();
|
|
199
|
+
if (!command) return;
|
|
200
|
+
|
|
201
|
+
// In bash mode, don't add user message to UI, directly execute command
|
|
202
|
+
// Executing bash command will automatically add assistant message
|
|
203
|
+
|
|
204
|
+
// Set command running state
|
|
205
|
+
setIsCommandRunning(true);
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
await agentRef.current?.executeBashCommand(command);
|
|
209
|
+
} finally {
|
|
210
|
+
// Clear command running state
|
|
211
|
+
setIsCommandRunning(false);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Handle normal AI message and slash commands
|
|
218
|
+
// Slash commands are now handled internally in agent.sendMessage
|
|
219
|
+
|
|
220
|
+
// Set loading state
|
|
221
|
+
setIsLoading(true);
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
await agentRef.current?.sendMessage(content, images);
|
|
225
|
+
} finally {
|
|
226
|
+
// Clear loading state
|
|
227
|
+
setIsLoading(false);
|
|
228
|
+
}
|
|
229
|
+
} catch (error) {
|
|
230
|
+
console.error("Failed to send message:", error);
|
|
231
|
+
// Loading state will be automatically updated by the useEffect that watches messages
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
[],
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
// Unified interrupt method, interrupt both AI messages and command execution
|
|
238
|
+
const abortMessage = useCallback(() => {
|
|
239
|
+
agentRef.current?.abortMessage();
|
|
240
|
+
}, []);
|
|
241
|
+
|
|
242
|
+
// Memory save function - delegate to Agent
|
|
243
|
+
const saveMemory = useCallback(
|
|
244
|
+
async (message: string, type: "project" | "user") => {
|
|
245
|
+
await agentRef.current?.saveMemory(message, type);
|
|
246
|
+
},
|
|
247
|
+
[],
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
// MCP management methods - delegate to Agent
|
|
251
|
+
const connectMcpServer = useCallback(async (serverName: string) => {
|
|
252
|
+
return (await agentRef.current?.connectMcpServer(serverName)) ?? false;
|
|
253
|
+
}, []);
|
|
254
|
+
|
|
255
|
+
const disconnectMcpServer = useCallback(async (serverName: string) => {
|
|
256
|
+
return (await agentRef.current?.disconnectMcpServer(serverName)) ?? false;
|
|
257
|
+
}, []);
|
|
258
|
+
|
|
259
|
+
// Background bash management methods - delegate to Agent
|
|
260
|
+
const getBackgroundShellOutput = useCallback((shellId: string) => {
|
|
261
|
+
if (!agentRef.current) return null;
|
|
262
|
+
return agentRef.current.getBackgroundShellOutput(shellId);
|
|
263
|
+
}, []);
|
|
264
|
+
|
|
265
|
+
const killBackgroundShell = useCallback((shellId: string) => {
|
|
266
|
+
if (!agentRef.current) return false;
|
|
267
|
+
return agentRef.current.killBackgroundShell(shellId);
|
|
268
|
+
}, []);
|
|
269
|
+
|
|
270
|
+
const hasSlashCommand = useCallback((commandId: string) => {
|
|
271
|
+
if (!agentRef.current) return false;
|
|
272
|
+
return agentRef.current.hasSlashCommand(commandId);
|
|
273
|
+
}, []);
|
|
274
|
+
|
|
275
|
+
const contextValue: ChatContextType = {
|
|
276
|
+
messages,
|
|
277
|
+
isLoading,
|
|
278
|
+
isCommandRunning,
|
|
279
|
+
userInputHistory,
|
|
280
|
+
isExpanded,
|
|
281
|
+
sessionId,
|
|
282
|
+
sendMessage,
|
|
283
|
+
abortMessage,
|
|
284
|
+
latestTotalTokens,
|
|
285
|
+
isCompressing,
|
|
286
|
+
saveMemory,
|
|
287
|
+
mcpServers,
|
|
288
|
+
connectMcpServer,
|
|
289
|
+
disconnectMcpServer,
|
|
290
|
+
backgroundShells,
|
|
291
|
+
getBackgroundShellOutput,
|
|
292
|
+
killBackgroundShell,
|
|
293
|
+
slashCommands,
|
|
294
|
+
hasSlashCommand,
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
return (
|
|
298
|
+
<ChatContext.Provider value={contextValue}>{children}</ChatContext.Provider>
|
|
299
|
+
);
|
|
300
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { useState, useCallback } from "react";
|
|
2
|
+
|
|
3
|
+
export const useBashHistorySelector = () => {
|
|
4
|
+
const [showBashHistorySelector, setShowBashHistorySelector] = useState(false);
|
|
5
|
+
const [exclamationPosition, setExclamationPosition] = useState(-1);
|
|
6
|
+
const [bashHistorySearchQuery, setBashHistorySearchQuery] = useState("");
|
|
7
|
+
|
|
8
|
+
const activateBashHistorySelector = useCallback((position: number) => {
|
|
9
|
+
setShowBashHistorySelector(true);
|
|
10
|
+
setExclamationPosition(position);
|
|
11
|
+
setBashHistorySearchQuery("");
|
|
12
|
+
}, []);
|
|
13
|
+
|
|
14
|
+
const handleBashHistorySelect = useCallback(
|
|
15
|
+
(command: string, inputText: string, cursorPosition: number) => {
|
|
16
|
+
if (exclamationPosition >= 0) {
|
|
17
|
+
// Replace ! and search query with selected command
|
|
18
|
+
const beforeExclamation = inputText.substring(0, exclamationPosition);
|
|
19
|
+
const afterQuery = inputText.substring(cursorPosition);
|
|
20
|
+
const newInput = beforeExclamation + `!${command}` + afterQuery;
|
|
21
|
+
const newCursorPosition = beforeExclamation.length + command.length + 1;
|
|
22
|
+
|
|
23
|
+
setShowBashHistorySelector(false);
|
|
24
|
+
setExclamationPosition(-1);
|
|
25
|
+
setBashHistorySearchQuery("");
|
|
26
|
+
|
|
27
|
+
return { newInput, newCursorPosition };
|
|
28
|
+
}
|
|
29
|
+
return { newInput: inputText, newCursorPosition: cursorPosition };
|
|
30
|
+
},
|
|
31
|
+
[exclamationPosition],
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const handleCancelBashHistorySelect = useCallback(() => {
|
|
35
|
+
setShowBashHistorySelector(false);
|
|
36
|
+
setExclamationPosition(-1);
|
|
37
|
+
setBashHistorySearchQuery("");
|
|
38
|
+
}, []);
|
|
39
|
+
|
|
40
|
+
const handleBashHistoryExecute = useCallback((command: string) => {
|
|
41
|
+
setShowBashHistorySelector(false);
|
|
42
|
+
setExclamationPosition(-1);
|
|
43
|
+
setBashHistorySearchQuery("");
|
|
44
|
+
return command; // Return command to execute
|
|
45
|
+
}, []);
|
|
46
|
+
|
|
47
|
+
const updateBashHistorySearchQuery = useCallback((query: string) => {
|
|
48
|
+
setBashHistorySearchQuery(query);
|
|
49
|
+
}, []);
|
|
50
|
+
|
|
51
|
+
const checkForExclamationDeletion = useCallback(
|
|
52
|
+
(cursorPosition: number) => {
|
|
53
|
+
if (showBashHistorySelector && cursorPosition <= exclamationPosition) {
|
|
54
|
+
handleCancelBashHistorySelect();
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
return false;
|
|
58
|
+
},
|
|
59
|
+
[
|
|
60
|
+
showBashHistorySelector,
|
|
61
|
+
exclamationPosition,
|
|
62
|
+
handleCancelBashHistorySelect,
|
|
63
|
+
],
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
showBashHistorySelector,
|
|
68
|
+
bashHistorySearchQuery,
|
|
69
|
+
activateBashHistorySelector,
|
|
70
|
+
handleBashHistorySelect,
|
|
71
|
+
handleBashHistoryExecute,
|
|
72
|
+
handleCancelBashHistorySelect,
|
|
73
|
+
updateBashHistorySearchQuery,
|
|
74
|
+
checkForExclamationDeletion,
|
|
75
|
+
exclamationPosition,
|
|
76
|
+
};
|
|
77
|
+
};
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { useState, useCallback } from "react";
|
|
2
|
+
|
|
3
|
+
export interface UseCommandSelectorParams {
|
|
4
|
+
onShowBashManager?: () => void;
|
|
5
|
+
onShowMcpManager?: () => void;
|
|
6
|
+
sendMessage?: (content: string) => Promise<void>; // Use sendMessage instead of executeSlashCommand
|
|
7
|
+
hasSlashCommand?: (commandId: string) => boolean; // Function to check if command exists
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const useCommandSelector = ({
|
|
11
|
+
onShowBashManager,
|
|
12
|
+
onShowMcpManager,
|
|
13
|
+
sendMessage,
|
|
14
|
+
hasSlashCommand,
|
|
15
|
+
}: UseCommandSelectorParams) => {
|
|
16
|
+
const [showCommandSelector, setShowCommandSelector] = useState(false);
|
|
17
|
+
const [slashPosition, setSlashPosition] = useState(-1);
|
|
18
|
+
const [commandSearchQuery, setCommandSearchQuery] = useState("");
|
|
19
|
+
|
|
20
|
+
const activateCommandSelector = useCallback((position: number) => {
|
|
21
|
+
setShowCommandSelector(true);
|
|
22
|
+
setSlashPosition(position);
|
|
23
|
+
setCommandSearchQuery("");
|
|
24
|
+
}, []);
|
|
25
|
+
|
|
26
|
+
const handleCommandInsert = useCallback(
|
|
27
|
+
(command: string, inputText: string, cursorPosition: number) => {
|
|
28
|
+
if (slashPosition >= 0) {
|
|
29
|
+
// Replace content from / to current cursor position with /command_name + space
|
|
30
|
+
const beforeSlash = inputText.substring(0, slashPosition);
|
|
31
|
+
const afterQuery = inputText.substring(cursorPosition);
|
|
32
|
+
const newInput = beforeSlash + `/${command} ` + afterQuery;
|
|
33
|
+
const newCursorPosition = beforeSlash.length + command.length + 2; // +2 for "/" and " "
|
|
34
|
+
|
|
35
|
+
setShowCommandSelector(false);
|
|
36
|
+
setSlashPosition(-1);
|
|
37
|
+
setCommandSearchQuery("");
|
|
38
|
+
|
|
39
|
+
return { newInput, newCursorPosition };
|
|
40
|
+
}
|
|
41
|
+
return { newInput: inputText, newCursorPosition: cursorPosition };
|
|
42
|
+
},
|
|
43
|
+
[slashPosition],
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const handleCommandSelect = useCallback(
|
|
47
|
+
(command: string, inputText: string, cursorPosition: number) => {
|
|
48
|
+
if (slashPosition >= 0) {
|
|
49
|
+
// Replace command part, keep other content
|
|
50
|
+
const beforeSlash = inputText.substring(0, slashPosition);
|
|
51
|
+
const afterQuery = inputText.substring(cursorPosition);
|
|
52
|
+
const newInput = beforeSlash + afterQuery;
|
|
53
|
+
const newCursorPosition = beforeSlash.length;
|
|
54
|
+
|
|
55
|
+
// Execute command asynchronously
|
|
56
|
+
(async () => {
|
|
57
|
+
// First check if it's an agent command
|
|
58
|
+
let commandExecuted = false;
|
|
59
|
+
if (sendMessage && hasSlashCommand && hasSlashCommand(command)) {
|
|
60
|
+
// Execute complete command (replace partial input with complete command name)
|
|
61
|
+
const fullCommand = `/${command}`;
|
|
62
|
+
try {
|
|
63
|
+
await sendMessage(fullCommand);
|
|
64
|
+
commandExecuted = true;
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.error("Failed to execute slash command:", error);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// If not an agent command or execution failed, check local commands
|
|
71
|
+
if (!commandExecuted) {
|
|
72
|
+
if (command === "bashes" && onShowBashManager) {
|
|
73
|
+
onShowBashManager();
|
|
74
|
+
commandExecuted = true;
|
|
75
|
+
} else if (command === "mcp" && onShowMcpManager) {
|
|
76
|
+
onShowMcpManager();
|
|
77
|
+
commandExecuted = true;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
})();
|
|
81
|
+
|
|
82
|
+
setShowCommandSelector(false);
|
|
83
|
+
setSlashPosition(-1);
|
|
84
|
+
setCommandSearchQuery("");
|
|
85
|
+
|
|
86
|
+
return { newInput, newCursorPosition };
|
|
87
|
+
}
|
|
88
|
+
return { newInput: inputText, newCursorPosition: cursorPosition };
|
|
89
|
+
},
|
|
90
|
+
[
|
|
91
|
+
slashPosition,
|
|
92
|
+
onShowBashManager,
|
|
93
|
+
onShowMcpManager,
|
|
94
|
+
sendMessage,
|
|
95
|
+
hasSlashCommand,
|
|
96
|
+
],
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const handleCancelCommandSelect = useCallback(() => {
|
|
100
|
+
setShowCommandSelector(false);
|
|
101
|
+
setSlashPosition(-1);
|
|
102
|
+
setCommandSearchQuery("");
|
|
103
|
+
}, []);
|
|
104
|
+
|
|
105
|
+
const updateCommandSearchQuery = useCallback((query: string) => {
|
|
106
|
+
setCommandSearchQuery(query);
|
|
107
|
+
}, []);
|
|
108
|
+
|
|
109
|
+
const checkForSlashDeletion = useCallback(
|
|
110
|
+
(cursorPosition: number) => {
|
|
111
|
+
if (showCommandSelector && cursorPosition <= slashPosition) {
|
|
112
|
+
handleCancelCommandSelect();
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
return false;
|
|
116
|
+
},
|
|
117
|
+
[showCommandSelector, slashPosition, handleCancelCommandSelect],
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
showCommandSelector,
|
|
122
|
+
commandSearchQuery,
|
|
123
|
+
activateCommandSelector,
|
|
124
|
+
handleCommandSelect,
|
|
125
|
+
handleCommandInsert,
|
|
126
|
+
handleCancelCommandSelect,
|
|
127
|
+
updateCommandSearchQuery,
|
|
128
|
+
checkForSlashDeletion,
|
|
129
|
+
slashPosition,
|
|
130
|
+
};
|
|
131
|
+
};
|