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
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "wave-code",
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"description": "CLI-based code assistant powered by AI, built with React and Ink",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ai",
|
|
7
|
+
"cli",
|
|
8
|
+
"code-assistant",
|
|
9
|
+
"react",
|
|
10
|
+
"ink",
|
|
11
|
+
"typescript"
|
|
12
|
+
],
|
|
13
|
+
"main": "dist/index.js",
|
|
14
|
+
"types": "dist/index.d.ts",
|
|
15
|
+
"type": "module",
|
|
16
|
+
"bin": {
|
|
17
|
+
"wave-code": "bin/wave-code.js",
|
|
18
|
+
"wave": "bin/wave-code.js"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"bin",
|
|
23
|
+
"src",
|
|
24
|
+
"README.md"
|
|
25
|
+
],
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"ink": "^6.0.1",
|
|
28
|
+
"react": "^19.1.0",
|
|
29
|
+
"yargs": "^17.7.2",
|
|
30
|
+
"diff": "^8.0.2",
|
|
31
|
+
"glob": "^11.0.3",
|
|
32
|
+
"wave-agent-sdk": "0.0.2"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/react": "^19.1.8",
|
|
36
|
+
"@types/yargs": "^17.0.0",
|
|
37
|
+
"eslint-plugin-react": "^7.37.5",
|
|
38
|
+
"eslint-plugin-react-hooks": "^5.2.0",
|
|
39
|
+
"ink-testing-library": "^4.0.0",
|
|
40
|
+
"rimraf": "^6.0.1",
|
|
41
|
+
"tsc-alias": "^1.8.16",
|
|
42
|
+
"tsx": "^4.20.4",
|
|
43
|
+
"vitest": "^3.2.4"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"react": ">=18.0.0"
|
|
47
|
+
},
|
|
48
|
+
"engines": {
|
|
49
|
+
"node": ">=16.0.0"
|
|
50
|
+
},
|
|
51
|
+
"license": "MIT",
|
|
52
|
+
"scripts": {
|
|
53
|
+
"build": "rimraf dist && tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json",
|
|
54
|
+
"type-check": "tsc --noEmit --incremental",
|
|
55
|
+
"dev": "tsc -p tsconfig.build.json --watch & tsc-alias -p tsconfig.build.json --watch",
|
|
56
|
+
"test": "vitest run",
|
|
57
|
+
"lint": "eslint --cache",
|
|
58
|
+
"format": "prettier --write ."
|
|
59
|
+
}
|
|
60
|
+
}
|
package/src/cli.tsx
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { render } from "ink";
|
|
3
|
+
import { App } from "./components/App.js";
|
|
4
|
+
import { cleanupLogs } from "./utils/logger.js";
|
|
5
|
+
|
|
6
|
+
export interface CliOptions {
|
|
7
|
+
restoreSessionId?: string;
|
|
8
|
+
continueLastSession?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function startCli(options: CliOptions): Promise<void> {
|
|
12
|
+
const { restoreSessionId, continueLastSession } = options;
|
|
13
|
+
|
|
14
|
+
// Continue with ink-based UI for normal mode
|
|
15
|
+
// Global cleanup tracker
|
|
16
|
+
let isCleaningUp = false;
|
|
17
|
+
let appUnmounted = false;
|
|
18
|
+
|
|
19
|
+
const cleanup = async () => {
|
|
20
|
+
if (isCleaningUp) return;
|
|
21
|
+
isCleaningUp = true;
|
|
22
|
+
|
|
23
|
+
console.log("\nShutting down gracefully...");
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
// Clean up old log files
|
|
27
|
+
await cleanupLogs().catch((error) => {
|
|
28
|
+
console.warn("Failed to cleanup old logs:", error);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Unmount the React app to trigger cleanup
|
|
32
|
+
if (!appUnmounted) {
|
|
33
|
+
unmount();
|
|
34
|
+
appUnmounted = true;
|
|
35
|
+
// Give React time to cleanup
|
|
36
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
process.exit(0);
|
|
40
|
+
} catch (error: unknown) {
|
|
41
|
+
console.error("Error during cleanup:", error);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// Handle process signals
|
|
47
|
+
process.on("SIGINT", cleanup);
|
|
48
|
+
process.on("SIGTERM", cleanup);
|
|
49
|
+
|
|
50
|
+
// Handle uncaught exceptions
|
|
51
|
+
process.on("uncaughtException", (error) => {
|
|
52
|
+
console.error("Uncaught exception:", error);
|
|
53
|
+
cleanup();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
process.on("unhandledRejection", (reason, promise) => {
|
|
57
|
+
console.error("Unhandled rejection at:", promise, "reason:", reason);
|
|
58
|
+
cleanup();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Render the application
|
|
62
|
+
const { unmount } = render(
|
|
63
|
+
<App
|
|
64
|
+
restoreSessionId={restoreSessionId}
|
|
65
|
+
continueLastSession={continueLastSession}
|
|
66
|
+
/>,
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
// Store unmount function for cleanup when process exits normally
|
|
70
|
+
process.on("exit", () => {
|
|
71
|
+
if (!appUnmounted) {
|
|
72
|
+
try {
|
|
73
|
+
unmount();
|
|
74
|
+
} catch {
|
|
75
|
+
// Ignore errors during unmount
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Return a promise that never resolves to keep the CLI running
|
|
81
|
+
return new Promise(() => {});
|
|
82
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { ChatInterface } from "./ChatInterface.js";
|
|
3
|
+
import { ChatProvider } from "../contexts/useChat.js";
|
|
4
|
+
import { AppProvider } from "../contexts/useAppConfig.js";
|
|
5
|
+
|
|
6
|
+
interface AppProps {
|
|
7
|
+
restoreSessionId?: string;
|
|
8
|
+
continueLastSession?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const AppWithProviders: React.FC = () => {
|
|
12
|
+
return (
|
|
13
|
+
<ChatProvider>
|
|
14
|
+
<ChatInterface />
|
|
15
|
+
</ChatProvider>
|
|
16
|
+
);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const App: React.FC<AppProps> = ({
|
|
20
|
+
restoreSessionId,
|
|
21
|
+
continueLastSession,
|
|
22
|
+
}) => {
|
|
23
|
+
return (
|
|
24
|
+
<AppProvider
|
|
25
|
+
restoreSessionId={restoreSessionId}
|
|
26
|
+
continueLastSession={continueLastSession}
|
|
27
|
+
>
|
|
28
|
+
<AppWithProviders />
|
|
29
|
+
</AppProvider>
|
|
30
|
+
);
|
|
31
|
+
};
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import React, { useState, useEffect } from "react";
|
|
2
|
+
import { Box, Text, useInput } from "ink";
|
|
3
|
+
import { searchBashHistory, type BashHistoryEntry } from "wave-agent-sdk";
|
|
4
|
+
import { logger } from "../utils/logger.js";
|
|
5
|
+
|
|
6
|
+
export interface BashHistorySelectorProps {
|
|
7
|
+
searchQuery: string;
|
|
8
|
+
workdir: string;
|
|
9
|
+
onSelect: (command: string) => void;
|
|
10
|
+
onExecute: (command: string) => void;
|
|
11
|
+
onCancel: () => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const BashHistorySelector: React.FC<BashHistorySelectorProps> = ({
|
|
15
|
+
searchQuery,
|
|
16
|
+
workdir,
|
|
17
|
+
onSelect,
|
|
18
|
+
onExecute,
|
|
19
|
+
onCancel,
|
|
20
|
+
}) => {
|
|
21
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
22
|
+
const [commands, setCommands] = useState<BashHistoryEntry[]>([]);
|
|
23
|
+
|
|
24
|
+
// Search bash history
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
const results = searchBashHistory(searchQuery, 10, workdir);
|
|
27
|
+
setCommands(results);
|
|
28
|
+
setSelectedIndex(0);
|
|
29
|
+
logger.debug("Bash history search:", {
|
|
30
|
+
searchQuery,
|
|
31
|
+
workdir,
|
|
32
|
+
resultCount: results.length,
|
|
33
|
+
});
|
|
34
|
+
}, [searchQuery, workdir]);
|
|
35
|
+
|
|
36
|
+
useInput((input, key) => {
|
|
37
|
+
logger.debug("BashHistorySelector useInput:", {
|
|
38
|
+
input,
|
|
39
|
+
key,
|
|
40
|
+
commandsLength: commands.length,
|
|
41
|
+
selectedIndex,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
if (key.return) {
|
|
45
|
+
if (commands.length > 0 && selectedIndex < commands.length) {
|
|
46
|
+
const selectedCommand = commands[selectedIndex];
|
|
47
|
+
onExecute(selectedCommand.command);
|
|
48
|
+
} else if (commands.length === 0 && searchQuery.trim()) {
|
|
49
|
+
// When no history records match, execute the search query as a new command
|
|
50
|
+
onExecute(searchQuery.trim());
|
|
51
|
+
}
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (key.tab) {
|
|
56
|
+
if (commands.length > 0 && selectedIndex < commands.length) {
|
|
57
|
+
const selectedCommand = commands[selectedIndex];
|
|
58
|
+
onSelect(selectedCommand.command);
|
|
59
|
+
} else if (commands.length === 0 && searchQuery.trim()) {
|
|
60
|
+
// When no history records match, insert the search query
|
|
61
|
+
onSelect(searchQuery.trim());
|
|
62
|
+
}
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (key.escape) {
|
|
67
|
+
onCancel();
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (key.upArrow) {
|
|
72
|
+
setSelectedIndex(Math.max(0, selectedIndex - 1));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (key.downArrow) {
|
|
77
|
+
setSelectedIndex(Math.min(commands.length - 1, selectedIndex + 1));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
if (commands.length === 0) {
|
|
83
|
+
return (
|
|
84
|
+
<Box
|
|
85
|
+
flexDirection="column"
|
|
86
|
+
borderStyle="single"
|
|
87
|
+
borderColor="yellow"
|
|
88
|
+
padding={1}
|
|
89
|
+
marginBottom={1}
|
|
90
|
+
>
|
|
91
|
+
<Text color="yellow">
|
|
92
|
+
No bash history found {searchQuery && `for "${searchQuery}"`}
|
|
93
|
+
</Text>
|
|
94
|
+
{searchQuery.trim() && (
|
|
95
|
+
<Text color="green">Press Enter to execute: {searchQuery}</Text>
|
|
96
|
+
)}
|
|
97
|
+
{searchQuery.trim() && (
|
|
98
|
+
<Text color="blue">Press Tab to insert: {searchQuery}</Text>
|
|
99
|
+
)}
|
|
100
|
+
<Text dimColor>Press Escape to cancel</Text>
|
|
101
|
+
</Box>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const formatTimestamp = (timestamp: number): string => {
|
|
106
|
+
const date = new Date(timestamp);
|
|
107
|
+
const now = new Date();
|
|
108
|
+
const diffMs = now.getTime() - date.getTime();
|
|
109
|
+
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
|
110
|
+
const diffDays = Math.floor(diffHours / 24);
|
|
111
|
+
|
|
112
|
+
if (diffDays > 0) {
|
|
113
|
+
return `${diffDays}d ago`;
|
|
114
|
+
} else if (diffHours > 0) {
|
|
115
|
+
return `${diffHours}h ago`;
|
|
116
|
+
} else {
|
|
117
|
+
const diffMinutes = Math.floor(diffMs / (1000 * 60));
|
|
118
|
+
return diffMinutes > 0 ? `${diffMinutes}m ago` : "just now";
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<Box
|
|
124
|
+
flexDirection="column"
|
|
125
|
+
borderStyle="single"
|
|
126
|
+
borderColor="blue"
|
|
127
|
+
padding={1}
|
|
128
|
+
gap={1}
|
|
129
|
+
marginBottom={1}
|
|
130
|
+
>
|
|
131
|
+
<Box>
|
|
132
|
+
<Text color="blue" bold>
|
|
133
|
+
Bash History {searchQuery && `(filtering: "${searchQuery}")`}
|
|
134
|
+
</Text>
|
|
135
|
+
</Box>
|
|
136
|
+
|
|
137
|
+
{commands.map((cmd, index) => (
|
|
138
|
+
<Box key={index} flexDirection="column">
|
|
139
|
+
<Text
|
|
140
|
+
color={index === selectedIndex ? "black" : "white"}
|
|
141
|
+
backgroundColor={index === selectedIndex ? "blue" : undefined}
|
|
142
|
+
>
|
|
143
|
+
{cmd.command}
|
|
144
|
+
</Text>
|
|
145
|
+
{index === selectedIndex && (
|
|
146
|
+
<Box marginLeft={4} flexDirection="column">
|
|
147
|
+
<Text color="gray" dimColor>
|
|
148
|
+
{formatTimestamp(cmd.timestamp)}
|
|
149
|
+
{cmd.workdir !== workdir && ` • in ${cmd.workdir}`}
|
|
150
|
+
</Text>
|
|
151
|
+
</Box>
|
|
152
|
+
)}
|
|
153
|
+
</Box>
|
|
154
|
+
))}
|
|
155
|
+
|
|
156
|
+
<Box>
|
|
157
|
+
<Text dimColor>
|
|
158
|
+
Use ↑↓ to navigate, Enter to execute, Tab to insert, Escape to cancel
|
|
159
|
+
</Text>
|
|
160
|
+
</Box>
|
|
161
|
+
</Box>
|
|
162
|
+
);
|
|
163
|
+
};
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import React, { useState, useEffect } from "react";
|
|
2
|
+
import { Box, Text, useInput } from "ink";
|
|
3
|
+
import { useChat } from "../contexts/useChat.js";
|
|
4
|
+
|
|
5
|
+
interface BashShell {
|
|
6
|
+
id: string;
|
|
7
|
+
command: string;
|
|
8
|
+
status: "running" | "completed" | "killed";
|
|
9
|
+
startTime: number;
|
|
10
|
+
exitCode?: number;
|
|
11
|
+
runtime?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface BashShellManagerProps {
|
|
15
|
+
onCancel: () => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const BashShellManager: React.FC<BashShellManagerProps> = ({
|
|
19
|
+
onCancel,
|
|
20
|
+
}) => {
|
|
21
|
+
const { backgroundShells, getBackgroundShellOutput, killBackgroundShell } =
|
|
22
|
+
useChat();
|
|
23
|
+
const [shells, setShells] = useState<BashShell[]>([]);
|
|
24
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
25
|
+
const [viewMode, setViewMode] = useState<"list" | "detail">("list");
|
|
26
|
+
const [detailShellId, setDetailShellId] = useState<string | null>(null);
|
|
27
|
+
const [detailOutput, setDetailOutput] = useState<{
|
|
28
|
+
stdout: string;
|
|
29
|
+
stderr: string;
|
|
30
|
+
status: string;
|
|
31
|
+
} | null>(null);
|
|
32
|
+
|
|
33
|
+
// Convert backgroundShells to local BashShell format
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
setShells(
|
|
36
|
+
backgroundShells.map((shell) => ({
|
|
37
|
+
id: shell.id,
|
|
38
|
+
command: shell.command,
|
|
39
|
+
status: shell.status,
|
|
40
|
+
startTime: shell.startTime,
|
|
41
|
+
exitCode: shell.exitCode,
|
|
42
|
+
runtime: shell.runtime,
|
|
43
|
+
})),
|
|
44
|
+
);
|
|
45
|
+
}, [backgroundShells]);
|
|
46
|
+
|
|
47
|
+
// Load detail output for selected shell
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (viewMode === "detail" && detailShellId) {
|
|
50
|
+
const output = getBackgroundShellOutput(detailShellId);
|
|
51
|
+
setDetailOutput(output);
|
|
52
|
+
}
|
|
53
|
+
}, [viewMode, detailShellId, getBackgroundShellOutput]);
|
|
54
|
+
|
|
55
|
+
const formatDuration = (ms: number): string => {
|
|
56
|
+
if (ms < 1000) return `${ms}ms`;
|
|
57
|
+
if (ms < 60000) return `${Math.round(ms / 1000)}s`;
|
|
58
|
+
const minutes = Math.floor(ms / 60000);
|
|
59
|
+
const seconds = Math.round((ms % 60000) / 1000);
|
|
60
|
+
return `${minutes}m ${seconds}s`;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const formatTime = (timestamp: number): string => {
|
|
64
|
+
return new Date(timestamp).toLocaleTimeString();
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const killShell = (shellId: string) => {
|
|
68
|
+
killBackgroundShell(shellId);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
useInput((input, key) => {
|
|
72
|
+
if (viewMode === "list") {
|
|
73
|
+
// List mode navigation
|
|
74
|
+
if (key.return) {
|
|
75
|
+
if (shells.length > 0 && selectedIndex < shells.length) {
|
|
76
|
+
const selectedShell = shells[selectedIndex];
|
|
77
|
+
setDetailShellId(selectedShell.id);
|
|
78
|
+
setViewMode("detail");
|
|
79
|
+
}
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (key.escape) {
|
|
84
|
+
onCancel();
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (key.upArrow) {
|
|
89
|
+
setSelectedIndex(Math.max(0, selectedIndex - 1));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (key.downArrow) {
|
|
94
|
+
setSelectedIndex(Math.min(shells.length - 1, selectedIndex + 1));
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (input === "k" && shells.length > 0 && selectedIndex < shells.length) {
|
|
99
|
+
const selectedShell = shells[selectedIndex];
|
|
100
|
+
if (selectedShell.status === "running") {
|
|
101
|
+
killShell(selectedShell.id);
|
|
102
|
+
}
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
} else if (viewMode === "detail") {
|
|
106
|
+
// Detail mode navigation
|
|
107
|
+
if (key.escape) {
|
|
108
|
+
setViewMode("list");
|
|
109
|
+
setDetailShellId(null);
|
|
110
|
+
setDetailOutput(null);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (input === "k" && detailShellId) {
|
|
115
|
+
const shell = shells.find((s) => s.id === detailShellId);
|
|
116
|
+
if (shell && shell.status === "running") {
|
|
117
|
+
killShell(detailShellId);
|
|
118
|
+
}
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
if (viewMode === "detail" && detailShellId && detailOutput) {
|
|
125
|
+
const shell = shells.find((s) => s.id === detailShellId);
|
|
126
|
+
if (!shell) {
|
|
127
|
+
setViewMode("list");
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<Box
|
|
133
|
+
flexDirection="column"
|
|
134
|
+
borderStyle="single"
|
|
135
|
+
borderColor="cyan"
|
|
136
|
+
padding={1}
|
|
137
|
+
gap={1}
|
|
138
|
+
marginBottom={1}
|
|
139
|
+
>
|
|
140
|
+
<Box>
|
|
141
|
+
<Text color="cyan" bold>
|
|
142
|
+
Background Shell Details: {shell.id}
|
|
143
|
+
</Text>
|
|
144
|
+
</Box>
|
|
145
|
+
|
|
146
|
+
<Box flexDirection="column" gap={1}>
|
|
147
|
+
<Box>
|
|
148
|
+
<Text>
|
|
149
|
+
<Text color="blue">Command:</Text> {shell.command}
|
|
150
|
+
</Text>
|
|
151
|
+
</Box>
|
|
152
|
+
<Box>
|
|
153
|
+
<Text>
|
|
154
|
+
<Text color="blue">Status:</Text> {shell.status}
|
|
155
|
+
{shell.exitCode !== undefined &&
|
|
156
|
+
` (exit code: ${shell.exitCode})`}
|
|
157
|
+
</Text>
|
|
158
|
+
</Box>
|
|
159
|
+
<Box>
|
|
160
|
+
<Text>
|
|
161
|
+
<Text color="blue">Started:</Text> {formatTime(shell.startTime)}
|
|
162
|
+
{shell.runtime !== undefined && (
|
|
163
|
+
<Text>
|
|
164
|
+
{" "}
|
|
165
|
+
| <Text color="blue">Runtime:</Text>{" "}
|
|
166
|
+
{formatDuration(shell.runtime)}
|
|
167
|
+
</Text>
|
|
168
|
+
)}
|
|
169
|
+
</Text>
|
|
170
|
+
</Box>
|
|
171
|
+
</Box>
|
|
172
|
+
|
|
173
|
+
{detailOutput.stdout && (
|
|
174
|
+
<Box flexDirection="column" marginTop={1}>
|
|
175
|
+
<Text color="green" bold>
|
|
176
|
+
STDOUT (last 10 lines):
|
|
177
|
+
</Text>
|
|
178
|
+
<Box borderStyle="single" borderColor="green" padding={1}>
|
|
179
|
+
<Text>
|
|
180
|
+
{detailOutput.stdout.split("\n").slice(-10).join("\n")}
|
|
181
|
+
</Text>
|
|
182
|
+
</Box>
|
|
183
|
+
</Box>
|
|
184
|
+
)}
|
|
185
|
+
|
|
186
|
+
{detailOutput.stderr && (
|
|
187
|
+
<Box flexDirection="column" marginTop={1}>
|
|
188
|
+
<Text color="red" bold>
|
|
189
|
+
STDERR:
|
|
190
|
+
</Text>
|
|
191
|
+
<Box borderStyle="single" borderColor="red" padding={1}>
|
|
192
|
+
<Text color="red">
|
|
193
|
+
{detailOutput.stderr.split("\n").slice(-10).join("\n")}
|
|
194
|
+
</Text>
|
|
195
|
+
</Box>
|
|
196
|
+
</Box>
|
|
197
|
+
)}
|
|
198
|
+
|
|
199
|
+
<Box marginTop={1}>
|
|
200
|
+
<Text dimColor>
|
|
201
|
+
{shell.status === "running" ? "k to kill · " : ""}Esc to go back
|
|
202
|
+
</Text>
|
|
203
|
+
</Box>
|
|
204
|
+
</Box>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (!backgroundShells) {
|
|
209
|
+
return (
|
|
210
|
+
<Box
|
|
211
|
+
flexDirection="column"
|
|
212
|
+
borderStyle="single"
|
|
213
|
+
borderColor="cyan"
|
|
214
|
+
padding={1}
|
|
215
|
+
marginBottom={1}
|
|
216
|
+
>
|
|
217
|
+
<Text color="cyan" bold>
|
|
218
|
+
Background Bash Shells
|
|
219
|
+
</Text>
|
|
220
|
+
<Text>Background bash shells not available</Text>
|
|
221
|
+
<Text dimColor>Press Escape to close</Text>
|
|
222
|
+
</Box>
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (shells.length === 0) {
|
|
227
|
+
return (
|
|
228
|
+
<Box
|
|
229
|
+
flexDirection="column"
|
|
230
|
+
borderStyle="single"
|
|
231
|
+
borderColor="cyan"
|
|
232
|
+
padding={1}
|
|
233
|
+
marginBottom={1}
|
|
234
|
+
>
|
|
235
|
+
<Text color="cyan" bold>
|
|
236
|
+
Background Bash Shells
|
|
237
|
+
</Text>
|
|
238
|
+
<Text>No background shells found</Text>
|
|
239
|
+
<Text dimColor>Press Escape to close</Text>
|
|
240
|
+
</Box>
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return (
|
|
245
|
+
<Box
|
|
246
|
+
flexDirection="column"
|
|
247
|
+
borderStyle="single"
|
|
248
|
+
borderColor="cyan"
|
|
249
|
+
padding={1}
|
|
250
|
+
gap={1}
|
|
251
|
+
marginBottom={1}
|
|
252
|
+
>
|
|
253
|
+
<Box>
|
|
254
|
+
<Text color="cyan" bold>
|
|
255
|
+
Background Bash Shells
|
|
256
|
+
</Text>
|
|
257
|
+
</Box>
|
|
258
|
+
<Text dimColor>Select a shell to view details</Text>
|
|
259
|
+
|
|
260
|
+
{shells.map((shell, index) => (
|
|
261
|
+
<Box key={shell.id} flexDirection="column">
|
|
262
|
+
<Text
|
|
263
|
+
color={index === selectedIndex ? "black" : "white"}
|
|
264
|
+
backgroundColor={index === selectedIndex ? "cyan" : undefined}
|
|
265
|
+
>
|
|
266
|
+
{index === selectedIndex ? "▶ " : " "}
|
|
267
|
+
{index + 1}.{" "}
|
|
268
|
+
{shell.command.length > 50
|
|
269
|
+
? shell.command.substring(0, 47) + "..."
|
|
270
|
+
: shell.command}
|
|
271
|
+
<Text
|
|
272
|
+
color={
|
|
273
|
+
shell.status === "running"
|
|
274
|
+
? "green"
|
|
275
|
+
: shell.status === "completed"
|
|
276
|
+
? "blue"
|
|
277
|
+
: "red"
|
|
278
|
+
}
|
|
279
|
+
>
|
|
280
|
+
{" "}
|
|
281
|
+
({shell.status})
|
|
282
|
+
</Text>
|
|
283
|
+
</Text>
|
|
284
|
+
{index === selectedIndex && (
|
|
285
|
+
<Box marginLeft={4} flexDirection="column">
|
|
286
|
+
<Text color="gray" dimColor>
|
|
287
|
+
ID: {shell.id} | Started: {formatTime(shell.startTime)}
|
|
288
|
+
{shell.runtime !== undefined &&
|
|
289
|
+
` | Runtime: ${formatDuration(shell.runtime)}`}
|
|
290
|
+
{shell.exitCode !== undefined && ` | Exit: ${shell.exitCode}`}
|
|
291
|
+
</Text>
|
|
292
|
+
</Box>
|
|
293
|
+
)}
|
|
294
|
+
</Box>
|
|
295
|
+
))}
|
|
296
|
+
|
|
297
|
+
<Box marginTop={1}>
|
|
298
|
+
<Text dimColor>
|
|
299
|
+
↑/↓ to select · Enter to view ·{" "}
|
|
300
|
+
{shells[selectedIndex]?.status === "running" ? "k to kill · " : ""}Esc
|
|
301
|
+
to close
|
|
302
|
+
</Text>
|
|
303
|
+
</Box>
|
|
304
|
+
</Box>
|
|
305
|
+
);
|
|
306
|
+
};
|