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.
Files changed (131) hide show
  1. package/README.md +120 -0
  2. package/bin/wave-code.js +16 -0
  3. package/dist/cli.d.ts +6 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +62 -0
  6. package/dist/components/App.d.ts +8 -0
  7. package/dist/components/App.d.ts.map +1 -0
  8. package/dist/components/App.js +10 -0
  9. package/dist/components/BashHistorySelector.d.ts +10 -0
  10. package/dist/components/BashHistorySelector.d.ts.map +1 -0
  11. package/dist/components/BashHistorySelector.js +83 -0
  12. package/dist/components/BashShellManager.d.ts +6 -0
  13. package/dist/components/BashShellManager.d.ts.map +1 -0
  14. package/dist/components/BashShellManager.js +116 -0
  15. package/dist/components/ChatInterface.d.ts +3 -0
  16. package/dist/components/ChatInterface.d.ts.map +1 -0
  17. package/dist/components/ChatInterface.js +31 -0
  18. package/dist/components/CommandOutputDisplay.d.ts +9 -0
  19. package/dist/components/CommandOutputDisplay.d.ts.map +1 -0
  20. package/dist/components/CommandOutputDisplay.js +40 -0
  21. package/dist/components/CommandSelector.d.ts +11 -0
  22. package/dist/components/CommandSelector.d.ts.map +1 -0
  23. package/dist/components/CommandSelector.js +60 -0
  24. package/dist/components/CompressDisplay.d.ts +9 -0
  25. package/dist/components/CompressDisplay.d.ts.map +1 -0
  26. package/dist/components/CompressDisplay.js +17 -0
  27. package/dist/components/DiffViewer.d.ts +9 -0
  28. package/dist/components/DiffViewer.d.ts.map +1 -0
  29. package/dist/components/DiffViewer.js +221 -0
  30. package/dist/components/FileSelector.d.ts +13 -0
  31. package/dist/components/FileSelector.d.ts.map +1 -0
  32. package/dist/components/FileSelector.js +48 -0
  33. package/dist/components/InputBox.d.ts +23 -0
  34. package/dist/components/InputBox.d.ts.map +1 -0
  35. package/dist/components/InputBox.js +124 -0
  36. package/dist/components/McpManager.d.ts +10 -0
  37. package/dist/components/McpManager.d.ts.map +1 -0
  38. package/dist/components/McpManager.js +123 -0
  39. package/dist/components/MemoryDisplay.d.ts +8 -0
  40. package/dist/components/MemoryDisplay.d.ts.map +1 -0
  41. package/dist/components/MemoryDisplay.js +25 -0
  42. package/dist/components/MemoryTypeSelector.d.ts +8 -0
  43. package/dist/components/MemoryTypeSelector.d.ts.map +1 -0
  44. package/dist/components/MemoryTypeSelector.js +38 -0
  45. package/dist/components/MessageList.d.ts +12 -0
  46. package/dist/components/MessageList.d.ts.map +1 -0
  47. package/dist/components/MessageList.js +36 -0
  48. package/dist/components/ToolResultDisplay.d.ts +9 -0
  49. package/dist/components/ToolResultDisplay.d.ts.map +1 -0
  50. package/dist/components/ToolResultDisplay.js +52 -0
  51. package/dist/contexts/useAppConfig.d.ts +11 -0
  52. package/dist/contexts/useAppConfig.d.ts.map +1 -0
  53. package/dist/contexts/useAppConfig.js +13 -0
  54. package/dist/contexts/useChat.d.ts +36 -0
  55. package/dist/contexts/useChat.d.ts.map +1 -0
  56. package/dist/contexts/useChat.js +208 -0
  57. package/dist/hooks/useBashHistorySelector.d.ts +15 -0
  58. package/dist/hooks/useBashHistorySelector.d.ts.map +1 -0
  59. package/dist/hooks/useBashHistorySelector.js +61 -0
  60. package/dist/hooks/useCommandSelector.d.ts +24 -0
  61. package/dist/hooks/useCommandSelector.d.ts.map +1 -0
  62. package/dist/hooks/useCommandSelector.js +98 -0
  63. package/dist/hooks/useFileSelector.d.ts +16 -0
  64. package/dist/hooks/useFileSelector.d.ts.map +1 -0
  65. package/dist/hooks/useFileSelector.js +174 -0
  66. package/dist/hooks/useImageManager.d.ts +13 -0
  67. package/dist/hooks/useImageManager.d.ts.map +1 -0
  68. package/dist/hooks/useImageManager.js +46 -0
  69. package/dist/hooks/useInputHistory.d.ts +11 -0
  70. package/dist/hooks/useInputHistory.d.ts.map +1 -0
  71. package/dist/hooks/useInputHistory.js +64 -0
  72. package/dist/hooks/useInputKeyboardHandler.d.ts +83 -0
  73. package/dist/hooks/useInputKeyboardHandler.d.ts.map +1 -0
  74. package/dist/hooks/useInputKeyboardHandler.js +507 -0
  75. package/dist/hooks/useInputState.d.ts +14 -0
  76. package/dist/hooks/useInputState.d.ts.map +1 -0
  77. package/dist/hooks/useInputState.js +57 -0
  78. package/dist/hooks/useMemoryTypeSelector.d.ts +9 -0
  79. package/dist/hooks/useMemoryTypeSelector.d.ts.map +1 -0
  80. package/dist/hooks/useMemoryTypeSelector.js +27 -0
  81. package/dist/hooks/usePagination.d.ts +20 -0
  82. package/dist/hooks/usePagination.d.ts.map +1 -0
  83. package/dist/hooks/usePagination.js +168 -0
  84. package/dist/index.d.ts +5 -0
  85. package/dist/index.d.ts.map +1 -0
  86. package/dist/index.js +91 -0
  87. package/dist/plain-cli.d.ts +7 -0
  88. package/dist/plain-cli.d.ts.map +1 -0
  89. package/dist/plain-cli.js +49 -0
  90. package/dist/utils/clipboard.d.ts +22 -0
  91. package/dist/utils/clipboard.d.ts.map +1 -0
  92. package/dist/utils/clipboard.js +347 -0
  93. package/dist/utils/constants.d.ts +17 -0
  94. package/dist/utils/constants.d.ts.map +1 -0
  95. package/dist/utils/constants.js +18 -0
  96. package/dist/utils/logger.d.ts +72 -0
  97. package/dist/utils/logger.d.ts.map +1 -0
  98. package/dist/utils/logger.js +245 -0
  99. package/package.json +60 -0
  100. package/src/cli.tsx +82 -0
  101. package/src/components/App.tsx +31 -0
  102. package/src/components/BashHistorySelector.tsx +163 -0
  103. package/src/components/BashShellManager.tsx +306 -0
  104. package/src/components/ChatInterface.tsx +88 -0
  105. package/src/components/CommandOutputDisplay.tsx +81 -0
  106. package/src/components/CommandSelector.tsx +144 -0
  107. package/src/components/CompressDisplay.tsx +58 -0
  108. package/src/components/DiffViewer.tsx +321 -0
  109. package/src/components/FileSelector.tsx +137 -0
  110. package/src/components/InputBox.tsx +310 -0
  111. package/src/components/McpManager.tsx +328 -0
  112. package/src/components/MemoryDisplay.tsx +62 -0
  113. package/src/components/MemoryTypeSelector.tsx +96 -0
  114. package/src/components/MessageList.tsx +215 -0
  115. package/src/components/ToolResultDisplay.tsx +138 -0
  116. package/src/contexts/useAppConfig.tsx +32 -0
  117. package/src/contexts/useChat.tsx +300 -0
  118. package/src/hooks/useBashHistorySelector.ts +77 -0
  119. package/src/hooks/useCommandSelector.ts +131 -0
  120. package/src/hooks/useFileSelector.ts +227 -0
  121. package/src/hooks/useImageManager.ts +64 -0
  122. package/src/hooks/useInputHistory.ts +74 -0
  123. package/src/hooks/useInputKeyboardHandler.ts +778 -0
  124. package/src/hooks/useInputState.ts +66 -0
  125. package/src/hooks/useMemoryTypeSelector.ts +40 -0
  126. package/src/hooks/usePagination.ts +203 -0
  127. package/src/index.ts +108 -0
  128. package/src/plain-cli.ts +66 -0
  129. package/src/utils/clipboard.ts +384 -0
  130. package/src/utils/constants.ts +22 -0
  131. 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
+ };