rl-rockcli 0.0.8 → 0.0.10
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/commands/attach/basic-repl.js +212 -0
- package/commands/attach/cleanup-history.js +189 -0
- package/commands/attach/cleanup-manager.js +163 -0
- package/commands/attach/copy-ui/copyRepl.js +195 -0
- package/commands/attach/copy-ui/index.js +7 -0
- package/commands/attach/copy-ui/render/outputBlock.js +25 -0
- package/commands/attach/copy-ui/viewport/viewport.js +23 -0
- package/commands/attach/copy-ui/viewport/wheel.js +14 -0
- package/commands/attach/history-manager.js +507 -0
- package/commands/attach/history-session.js +48 -0
- package/commands/attach/ink-repl/InkREPL.js +1507 -0
- package/commands/attach/ink-repl/builtinCommands.js +1253 -0
- package/commands/attach/ink-repl/components/ConnectingScreen.js +76 -0
- package/commands/attach/ink-repl/components/Console.js +191 -0
- package/commands/attach/ink-repl/components/DetailView.js +148 -0
- package/commands/attach/ink-repl/components/DropdownMenu.js +86 -0
- package/commands/attach/ink-repl/components/InputArea.js +125 -0
- package/commands/attach/ink-repl/components/InputLine.js +18 -0
- package/commands/attach/ink-repl/components/OutputArea.js +22 -0
- package/commands/attach/ink-repl/components/OutputItem.js +96 -0
- package/commands/attach/ink-repl/components/ShellLayout.js +61 -0
- package/commands/attach/ink-repl/components/Spinner.js +79 -0
- package/commands/attach/ink-repl/components/StatusBar.js +106 -0
- package/commands/attach/ink-repl/components/WelcomeBanner.js +48 -0
- package/commands/attach/ink-repl/contexts/LayoutContext.js +12 -0
- package/commands/attach/ink-repl/contexts/ThemeContext.js +43 -0
- package/commands/attach/ink-repl/hooks/useFunctionKeys.js +70 -0
- package/commands/attach/ink-repl/hooks/useMouse.js +162 -0
- package/commands/attach/ink-repl/hooks/useResources.js +132 -0
- package/commands/attach/ink-repl/hooks/useSpinner.js +49 -0
- package/commands/attach/ink-repl/index.js +112 -0
- package/commands/attach/ink-repl/package.json +3 -0
- package/commands/attach/ink-repl/replState.js +947 -0
- package/commands/attach/ink-repl/shortcuts/defaultKeybindings.js +138 -0
- package/commands/attach/ink-repl/shortcuts/index.js +332 -0
- package/commands/attach/ink-repl/themes/defaultDark.js +18 -0
- package/commands/attach/ink-repl/themes/defaultLight.js +18 -0
- package/commands/attach/ink-repl/themes/index.js +4 -0
- package/commands/attach/ink-repl/themes/themeManager.js +45 -0
- package/commands/attach/ink-repl/themes/themeTokens.js +15 -0
- package/commands/attach/ink-repl/utils/atCompletion.js +346 -0
- package/commands/attach/ink-repl/utils/clipboard.js +50 -0
- package/commands/attach/ink-repl/utils/consoleLogger.js +81 -0
- package/commands/attach/ink-repl/utils/exitCodeHandler.js +49 -0
- package/commands/attach/ink-repl/utils/exitCodeTips.js +56 -0
- package/commands/attach/ink-repl/utils/formatTime.js +12 -0
- package/commands/attach/ink-repl/utils/outputSelection.js +120 -0
- package/commands/attach/ink-repl/utils/outputViewport.js +77 -0
- package/commands/attach/ink-repl/utils/paginatedFileLoading.js +76 -0
- package/commands/attach/ink-repl/utils/paramHint.js +60 -0
- package/commands/attach/ink-repl/utils/parseError.js +174 -0
- package/commands/attach/ink-repl/utils/pathCompletion.js +167 -0
- package/commands/attach/ink-repl/utils/remotePathSafety.js +56 -0
- package/commands/attach/ink-repl/utils/replSelection.js +205 -0
- package/commands/attach/ink-repl/utils/responseFormatter.js +127 -0
- package/commands/attach/ink-repl/utils/textWrap.js +117 -0
- package/commands/attach/ink-repl/utils/truncate.js +115 -0
- package/commands/attach/opentui-repl/App.tsx +891 -0
- package/commands/attach/opentui-repl/builtinCommands.ts +80 -0
- package/commands/attach/opentui-repl/components/ConfirmDialog.tsx +116 -0
- package/commands/attach/opentui-repl/components/ConnectingScreen.tsx +131 -0
- package/commands/attach/opentui-repl/components/Console.tsx +73 -0
- package/commands/attach/opentui-repl/components/DetailView.tsx +45 -0
- package/commands/attach/opentui-repl/components/DropdownMenu.tsx +130 -0
- package/commands/attach/opentui-repl/components/ExecutionStatus.tsx +66 -0
- package/commands/attach/opentui-repl/components/Header.tsx +24 -0
- package/commands/attach/opentui-repl/components/OutputArea.tsx +25 -0
- package/commands/attach/opentui-repl/components/OutputBlock.tsx +108 -0
- package/commands/attach/opentui-repl/components/PromptInput.tsx +109 -0
- package/commands/attach/opentui-repl/components/StatusBar.tsx +63 -0
- package/commands/attach/opentui-repl/components/Toast.tsx +65 -0
- package/commands/attach/opentui-repl/components/WelcomeBanner.tsx +41 -0
- package/commands/attach/opentui-repl/contexts/ReplContext.tsx +137 -0
- package/commands/attach/opentui-repl/contexts/SessionContext.tsx +32 -0
- package/commands/attach/opentui-repl/contexts/ThemeContext.tsx +70 -0
- package/commands/attach/opentui-repl/contexts/ToastContext.tsx +69 -0
- package/commands/attach/opentui-repl/contexts/toast-logic.js +71 -0
- package/commands/attach/opentui-repl/hooks/useResources.ts +102 -0
- package/commands/attach/opentui-repl/hooks/useSpinner.ts +46 -0
- package/commands/attach/opentui-repl/index.js +99 -0
- package/commands/attach/opentui-repl/keybindings.ts +39 -0
- package/commands/attach/opentui-repl/package.json +3 -0
- package/commands/attach/opentui-repl/render.tsx +72 -0
- package/commands/attach/opentui-repl/tsconfig.json +12 -0
- package/commands/attach/repl.js +791 -0
- package/commands/attach/sandbox-id-resolver.js +56 -0
- package/commands/attach/session-manager.js +307 -0
- package/commands/attach/ui-mode.js +146 -0
- package/commands/log/core/constants.js +237 -0
- package/commands/log/core/display.js +370 -0
- package/commands/log/core/search.js +330 -0
- package/commands/log/core/tail.js +216 -0
- package/commands/log/core/utils.js +424 -0
- package/commands/log.js +298 -0
- package/commands/sandbox/core/log-bridge.js +119 -0
- package/commands/sandbox/core/replay/analyzer.js +311 -0
- package/commands/sandbox/core/replay/batch-orchestrator.js +536 -0
- package/commands/sandbox/core/replay/batch-task.js +369 -0
- package/commands/sandbox/core/replay/concurrent-display.js +70 -0
- package/commands/sandbox/core/replay/concurrent-orchestrator.js +170 -0
- package/commands/sandbox/core/replay/data-source.js +86 -0
- package/commands/sandbox/core/replay/display.js +231 -0
- package/commands/sandbox/core/replay/executor.js +634 -0
- package/commands/sandbox/core/replay/history-fetcher.js +124 -0
- package/commands/sandbox/core/replay/index.js +338 -0
- package/commands/sandbox/core/replay/loghouse-data-source.js +177 -0
- package/commands/sandbox/core/replay/pid-mapping.js +26 -0
- package/commands/sandbox/core/replay/request.js +109 -0
- package/commands/sandbox/core/replay/worker.js +166 -0
- package/commands/sandbox/core/session.js +346 -0
- package/commands/sandbox/log-bridge.js +2 -0
- package/commands/sandbox/ray.js +2 -0
- package/commands/sandbox/replay/analyzer.js +311 -0
- package/commands/sandbox/replay/batch-orchestrator.js +536 -0
- package/commands/sandbox/replay/batch-task.js +369 -0
- package/commands/sandbox/replay/concurrent-display.js +70 -0
- package/commands/sandbox/replay/concurrent-orchestrator.js +170 -0
- package/commands/sandbox/replay/display.js +231 -0
- package/commands/sandbox/replay/executor.js +634 -0
- package/commands/sandbox/replay/history-fetcher.js +118 -0
- package/commands/sandbox/replay/index.js +338 -0
- package/commands/sandbox/replay/pid-mapping.js +26 -0
- package/commands/sandbox/replay/request.js +109 -0
- package/commands/sandbox/replay/worker.js +166 -0
- package/commands/sandbox/replay.js +2 -0
- package/commands/sandbox/session.js +2 -0
- package/commands/sandbox-original.js +1393 -0
- package/commands/sandbox.js +499 -0
- package/help/help.json +1071 -0
- package/help/middleware.js +71 -0
- package/help/renderer.js +800 -0
- package/index.js +5 -15
- package/lib/plugin-context.js +40 -0
- package/package.json +2 -2
- package/sdks/sandbox/core/client.js +845 -0
- package/sdks/sandbox/core/config.js +70 -0
- package/sdks/sandbox/core/types.js +74 -0
- package/sdks/sandbox/httpLogger.js +251 -0
- package/sdks/sandbox/index.js +9 -0
- package/utils/asciiArt.js +138 -0
- package/utils/bun-compat.js +59 -0
- package/utils/ciPipelines.js +138 -0
- package/utils/cli.js +17 -0
- package/utils/command-router.js +79 -0
- package/utils/configManager.js +503 -0
- package/utils/dependency-resolver.js +135 -0
- package/utils/eagleeye_traceid.js +151 -0
- package/utils/envDetector.js +78 -0
- package/utils/execution_logger.js +415 -0
- package/utils/featureManager.js +68 -0
- package/utils/firstTimeTip.js +44 -0
- package/utils/hook-manager.js +125 -0
- package/utils/http-logger.js +264 -0
- package/utils/i18n.js +139 -0
- package/utils/image-progress.js +159 -0
- package/utils/logger.js +154 -0
- package/utils/plugin-loader.js +124 -0
- package/utils/plugin-manager.js +348 -0
- package/utils/ray_cli_wrapper.js +746 -0
- package/utils/sandbox-client.js +419 -0
- package/utils/terminal.js +32 -0
- package/utils/tips.js +106 -0
|
@@ -0,0 +1,891 @@
|
|
|
1
|
+
import { Show, createSignal, createEffect, on, onCleanup, onMount } from 'solid-js';
|
|
2
|
+
import { useKeyboard, useRenderer } from '@opentui/solid';
|
|
3
|
+
import { ThemeProvider, useTheme } from './contexts/ThemeContext.tsx';
|
|
4
|
+
import { SessionProvider, type SessionInfo } from './contexts/SessionContext.tsx';
|
|
5
|
+
import { ReplProvider, useRepl } from './contexts/ReplContext.tsx';
|
|
6
|
+
import { ToastProvider, useToast } from './contexts/ToastContext.tsx';
|
|
7
|
+
import { Header } from './components/Header.tsx';
|
|
8
|
+
import { OutputArea } from './components/OutputArea.tsx';
|
|
9
|
+
import { ExecutionStatus } from './components/ExecutionStatus.tsx';
|
|
10
|
+
import { PromptInput } from './components/PromptInput.tsx';
|
|
11
|
+
import { StatusBar } from './components/StatusBar.tsx';
|
|
12
|
+
import { DropdownMenu } from './components/DropdownMenu.tsx';
|
|
13
|
+
import { DetailView } from './components/DetailView.tsx';
|
|
14
|
+
import { Console } from './components/Console.tsx';
|
|
15
|
+
import { Toast } from './components/Toast.tsx';
|
|
16
|
+
import { ConnectingScreen } from './components/ConnectingScreen.tsx';
|
|
17
|
+
import { useResources } from './hooks/useResources.ts';
|
|
18
|
+
import { executeBuiltinCommand, isBuiltinCommand, checkInteractiveCommand } from './builtinCommands.ts';
|
|
19
|
+
import {
|
|
20
|
+
parseAtContext,
|
|
21
|
+
getLocalCompletions,
|
|
22
|
+
getRemoteCompletions,
|
|
23
|
+
replaceAtPath,
|
|
24
|
+
} from '../ink-repl/utils/atCompletion.js';
|
|
25
|
+
import { parseExecutionError, friendlyErrorMessage } from '../ink-repl/utils/parseError.js';
|
|
26
|
+
import { formatCommandOutput } from '../ink-repl/utils/responseFormatter.js';
|
|
27
|
+
|
|
28
|
+
// Flag to prevent handleInputChange from resetting historyIndex during navigation
|
|
29
|
+
let _navigatingHistory = false;
|
|
30
|
+
|
|
31
|
+
function ReplShell(props: { onExit?: () => void }) {
|
|
32
|
+
const theme = useTheme();
|
|
33
|
+
const [state, setState, { nextId, addLog }] = useRepl();
|
|
34
|
+
const session = useSession();
|
|
35
|
+
const renderer = useRenderer();
|
|
36
|
+
const toast = useToast();
|
|
37
|
+
|
|
38
|
+
// Connection is already established by repl.js before starting OpenTUI
|
|
39
|
+
// Skip loading state and show UI immediately
|
|
40
|
+
const [ready] = createSignal(true);
|
|
41
|
+
|
|
42
|
+
// Wire up console copy-to-clipboard via opentui's onCopySelection callback
|
|
43
|
+
// This handles copy-selection action triggered by Ctrl+Y key binding within the Console
|
|
44
|
+
renderer.console.onCopySelection = (text: string) => {
|
|
45
|
+
if (!text || text.length === 0) return;
|
|
46
|
+
copyToClipboard(text);
|
|
47
|
+
toast.show({ variant: 'success', message: 'Console log copied to clipboard' });
|
|
48
|
+
renderer.clearSelection();
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// Store ref to input for focus management
|
|
52
|
+
let inputRef: any = null;
|
|
53
|
+
|
|
54
|
+
// Auto-copy on mouse selection release
|
|
55
|
+
// When user selects text with mouse and releases button, auto-copy to clipboard.
|
|
56
|
+
// This is needed because consoleOptions.onCopySelection only works for the
|
|
57
|
+
// built-in Console component, not the main app content area.
|
|
58
|
+
function handleMouseUp() {
|
|
59
|
+
const selection = renderer.getSelection();
|
|
60
|
+
if (selection) {
|
|
61
|
+
const text = selection.getSelectedText();
|
|
62
|
+
if (text && text.length > 0) {
|
|
63
|
+
copyToClipboard(text);
|
|
64
|
+
toast.show({ variant: 'success', message: 'Selection copied to clipboard' });
|
|
65
|
+
renderer.clearSelection();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// Restore focus to input after any mouse interaction
|
|
69
|
+
setTimeout(() => {
|
|
70
|
+
if (inputRef && state.viewMode === 'repl' && !state.consoleVisible && inputRef.focus) {
|
|
71
|
+
inputRef.focus();
|
|
72
|
+
}
|
|
73
|
+
}, 50);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Stats tracking
|
|
77
|
+
const stats = { startTime: Date.now(), shellCommands: 0, builtinCommands: 0 };
|
|
78
|
+
|
|
79
|
+
// AbortController for cancelling execution
|
|
80
|
+
let abortController: AbortController | null = null;
|
|
81
|
+
|
|
82
|
+
// Track last save time for auto-save
|
|
83
|
+
let lastSaveTime = 0;
|
|
84
|
+
|
|
85
|
+
// Track @ completion timeout
|
|
86
|
+
let atCompletionTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
87
|
+
|
|
88
|
+
// Trigger @ completion for /upload command
|
|
89
|
+
async function triggerAtCompletion(atContext: any) {
|
|
90
|
+
if (!atContext) return;
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
let items;
|
|
94
|
+
if (atContext.isLocal) {
|
|
95
|
+
// First @: local file completion
|
|
96
|
+
items = getLocalCompletions(atContext.partialPath, process.cwd());
|
|
97
|
+
} else {
|
|
98
|
+
// Second @: remote file completion
|
|
99
|
+
items = await getRemoteCompletions(session.sessionManager, atContext.partialPath);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (items.length > 0) {
|
|
103
|
+
setState('menuItems', items);
|
|
104
|
+
setState('menuVisible', true);
|
|
105
|
+
setState('selectedIndex', 0);
|
|
106
|
+
setState('menuType', 'at');
|
|
107
|
+
setState('atContext', atContext);
|
|
108
|
+
} else if (state.menuType === 'at') {
|
|
109
|
+
setState('menuVisible', false);
|
|
110
|
+
setState('menuItems', []);
|
|
111
|
+
setState('menuType', null);
|
|
112
|
+
setState('atContext', null);
|
|
113
|
+
}
|
|
114
|
+
} catch {
|
|
115
|
+
// Silently ignore completion errors
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Watch buffer changes for @ completion
|
|
120
|
+
createEffect(on(() => state.buffer, (buffer) => {
|
|
121
|
+
if (atCompletionTimeout) {
|
|
122
|
+
clearTimeout(atCompletionTimeout);
|
|
123
|
+
atCompletionTimeout = null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (state.isExecuting) return;
|
|
127
|
+
|
|
128
|
+
// Handle /upload @ completion
|
|
129
|
+
if (buffer.startsWith('/upload')) {
|
|
130
|
+
const atContext = parseAtContext(buffer);
|
|
131
|
+
if (atContext) {
|
|
132
|
+
atCompletionTimeout = setTimeout(() => {
|
|
133
|
+
triggerAtCompletion(atContext);
|
|
134
|
+
}, 100);
|
|
135
|
+
}
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Handle /download @ completion
|
|
140
|
+
if (buffer.startsWith('/download')) {
|
|
141
|
+
const atContext = parseAtContext(buffer);
|
|
142
|
+
if (atContext) {
|
|
143
|
+
atCompletionTimeout = setTimeout(() => {
|
|
144
|
+
triggerAtCompletion(atContext);
|
|
145
|
+
}, 100);
|
|
146
|
+
}
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Clear menu if not in upload/download command
|
|
151
|
+
if (state.menuType === 'at') {
|
|
152
|
+
setState('menuVisible', false);
|
|
153
|
+
setState('menuItems', []);
|
|
154
|
+
setState('menuType', null);
|
|
155
|
+
setState('atContext', null);
|
|
156
|
+
}
|
|
157
|
+
}));
|
|
158
|
+
|
|
159
|
+
// Start resource monitoring
|
|
160
|
+
useResources();
|
|
161
|
+
|
|
162
|
+
// Load history on startup - use onMount to ensure it runs after component is initialized
|
|
163
|
+
onMount(() => {
|
|
164
|
+
loadHistory();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Auto-save outputs periodically (every 5 seconds)
|
|
168
|
+
const autoSaveTimer = setInterval(() => {
|
|
169
|
+
if (state.outputs.length > 1 && Date.now() - lastSaveTime > 5000) {
|
|
170
|
+
saveOutputs();
|
|
171
|
+
}
|
|
172
|
+
}, 5000);
|
|
173
|
+
|
|
174
|
+
onCleanup(() => {
|
|
175
|
+
clearInterval(autoSaveTimer);
|
|
176
|
+
saveOutputs();
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
async function loadHistory() {
|
|
180
|
+
const { historyManager } = session;
|
|
181
|
+
if (!historyManager) return;
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
const [outputs, history] = await Promise.all([
|
|
185
|
+
typeof historyManager.loadOutputs === 'function'
|
|
186
|
+
? Promise.resolve(historyManager.loadOutputs()).catch(() => [])
|
|
187
|
+
: Promise.resolve([]),
|
|
188
|
+
typeof historyManager.getHistory === 'function'
|
|
189
|
+
? Promise.resolve(historyManager.getHistory()).catch(() => [])
|
|
190
|
+
: Promise.resolve([]),
|
|
191
|
+
]);
|
|
192
|
+
|
|
193
|
+
const restoredOutputs = Array.isArray(outputs) ? outputs : [];
|
|
194
|
+
const restoredCommands = Array.isArray(history) ? history : [];
|
|
195
|
+
|
|
196
|
+
if (restoredOutputs.length > 0 || restoredCommands.length > 0) {
|
|
197
|
+
// Update welcome message to show restored count
|
|
198
|
+
setState('outputs', 0, 'output',
|
|
199
|
+
`Connected! Type / for commands, double Ctrl+C to exit.\n✓ Restored ${restoredOutputs.length} output(s), ${restoredCommands.length} command(s) from history.`
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
if (restoredOutputs.length > 0) {
|
|
203
|
+
setState('outputs', (prev) => [...prev, ...restoredOutputs]);
|
|
204
|
+
}
|
|
205
|
+
if (restoredCommands.length > 0) {
|
|
206
|
+
setState('commandHistory', restoredCommands);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
} catch {
|
|
210
|
+
// Ignore history loading errors
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function saveOutputs() {
|
|
215
|
+
const { historyManager } = session;
|
|
216
|
+
if (historyManager && typeof historyManager.saveOutputs === 'function') {
|
|
217
|
+
try {
|
|
218
|
+
historyManager.saveOutputs(state.outputs);
|
|
219
|
+
lastSaveTime = Date.now();
|
|
220
|
+
} catch {
|
|
221
|
+
// Ignore history saving errors
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Cancel the currently executing command and recreate the session.
|
|
228
|
+
* This is needed because aborting the HTTP request does NOT kill the
|
|
229
|
+
* server-side process — the old session remains blocked.
|
|
230
|
+
* Creating a new session unblocks command execution immediately.
|
|
231
|
+
*/
|
|
232
|
+
async function cancelExecution() {
|
|
233
|
+
// 1. Abort the client-side HTTP request
|
|
234
|
+
if (abortController) {
|
|
235
|
+
abortController.abort();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// 2. Update the output to show "Cancelled"
|
|
239
|
+
const outputId = state.outputs[state.outputs.length - 1]?.id;
|
|
240
|
+
if (outputId) {
|
|
241
|
+
setState('outputs', (items) =>
|
|
242
|
+
items.map((item) =>
|
|
243
|
+
item.id === outputId
|
|
244
|
+
? { ...item, output: 'Cancelled by user', exitCode: 130 }
|
|
245
|
+
: item
|
|
246
|
+
)
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
setState('isExecuting', false);
|
|
250
|
+
setState('executingCommand', '');
|
|
251
|
+
|
|
252
|
+
// 3. Recreate the session to unblock it from the cancelled command
|
|
253
|
+
const cwdMatch = state.shellPrompt.match(/:([^#]+)#/);
|
|
254
|
+
const cwd = cwdMatch ? cwdMatch[1] : '/';
|
|
255
|
+
try {
|
|
256
|
+
await session.sessionManager.interruptSession(cwd);
|
|
257
|
+
addLog('info', 'Session recreated after cancel');
|
|
258
|
+
} catch (err: any) {
|
|
259
|
+
addLog('warn', `Session recreate failed: ${err.message}`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Global keyboard handling
|
|
264
|
+
useKeyboard(async (e) => {
|
|
265
|
+
// Detail view: Escape goes back
|
|
266
|
+
if (state.viewMode === 'detail') {
|
|
267
|
+
if (e.name === 'escape') {
|
|
268
|
+
setState('viewMode', 'repl');
|
|
269
|
+
setState('detailContent', null);
|
|
270
|
+
e.preventDefault();
|
|
271
|
+
}
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Menu navigation
|
|
276
|
+
if (state.menuVisible) {
|
|
277
|
+
if (e.name === 'up') {
|
|
278
|
+
setState('selectedIndex', Math.max(0, state.selectedIndex - 1));
|
|
279
|
+
e.preventDefault();
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
if (e.name === 'down') {
|
|
283
|
+
setState('selectedIndex', Math.min(state.menuItems.length - 1, state.selectedIndex + 1));
|
|
284
|
+
e.preventDefault();
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
if (e.name === 'return') {
|
|
288
|
+
// #8: Enter on slash menu directly executes the command
|
|
289
|
+
// For @ / download completion, Enter should fill the selection like Tab
|
|
290
|
+
const selected = state.menuItems[state.selectedIndex];
|
|
291
|
+
if (selected) {
|
|
292
|
+
if (state.menuType === 'at' && state.atContext) {
|
|
293
|
+
// For @ completion, replace the @path part
|
|
294
|
+
const newBuffer = replaceAtPath(state.buffer, state.atContext, selected.value);
|
|
295
|
+
setState('buffer', newBuffer);
|
|
296
|
+
setState('menuVisible', false);
|
|
297
|
+
setState('menuItems', []);
|
|
298
|
+
setState('menuType', null);
|
|
299
|
+
setState('atContext', null);
|
|
300
|
+
} else {
|
|
301
|
+
// For slash menu, execute the command
|
|
302
|
+
setState('menuVisible', false);
|
|
303
|
+
setState('menuItems', []);
|
|
304
|
+
setState('menuType', null);
|
|
305
|
+
setState('buffer', '');
|
|
306
|
+
handleCommand(selected.value);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
e.preventDefault();
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
if (e.name === 'tab') {
|
|
313
|
+
const selected = state.menuItems[state.selectedIndex];
|
|
314
|
+
if (selected) {
|
|
315
|
+
if (state.menuType === 'at' && state.atContext) {
|
|
316
|
+
// For @ completion, replace only the @path part
|
|
317
|
+
const newBuffer = replaceAtPath(state.buffer, state.atContext, selected.value);
|
|
318
|
+
setState('buffer', newBuffer);
|
|
319
|
+
} else {
|
|
320
|
+
// For slash/path completions, replace entire buffer
|
|
321
|
+
setState('buffer', selected.value + ' ');
|
|
322
|
+
}
|
|
323
|
+
setState('menuVisible', false);
|
|
324
|
+
setState('menuItems', []);
|
|
325
|
+
setState('menuType', null);
|
|
326
|
+
setState('atContext', null);
|
|
327
|
+
}
|
|
328
|
+
e.preventDefault();
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
if (e.name === 'escape') {
|
|
332
|
+
setState('menuVisible', false);
|
|
333
|
+
setState('menuItems', []);
|
|
334
|
+
setState('menuType', null);
|
|
335
|
+
setState('atContext', null);
|
|
336
|
+
e.preventDefault();
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Escape during execution: cancel (#3)
|
|
342
|
+
if (state.isExecuting && e.name === 'escape') {
|
|
343
|
+
await cancelExecution();
|
|
344
|
+
e.preventDefault();
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Ctrl+C: exit handling
|
|
349
|
+
if (e.name === 'c' && e.ctrl) {
|
|
350
|
+
if (state.isExecuting) {
|
|
351
|
+
await cancelExecution();
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
if (state.exitPending) {
|
|
355
|
+
// Second Ctrl+C -> exit
|
|
356
|
+
saveOutputs();
|
|
357
|
+
// Destroy renderer to exit alternate screen
|
|
358
|
+
renderer.destroy();
|
|
359
|
+
// Wait a bit for renderer to fully destroy before calling onExit
|
|
360
|
+
setTimeout(() => {
|
|
361
|
+
// Notify render.tsx that we're done (triggers Promise resolve)
|
|
362
|
+
if (props.onExit) {
|
|
363
|
+
props.onExit();
|
|
364
|
+
}
|
|
365
|
+
}, 100);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
// #9: First Ctrl+C clears buffer
|
|
369
|
+
setState('buffer', '');
|
|
370
|
+
setState('exitPending', true);
|
|
371
|
+
setTimeout(() => setState('exitPending', false), 2000);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Ctrl+V: show detail view of last output
|
|
376
|
+
if (e.name === 'v' && e.ctrl) {
|
|
377
|
+
const lastOutput = state.outputs.filter((o) => !o.isWelcome);
|
|
378
|
+
if (lastOutput.length > 0) {
|
|
379
|
+
const last = lastOutput[lastOutput.length - 1];
|
|
380
|
+
setState('detailContent', `$ ${last.command}\n\n${last.output}`);
|
|
381
|
+
setState('viewMode', 'detail');
|
|
382
|
+
}
|
|
383
|
+
e.preventDefault();
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Ctrl+Y: copy selected text or last output to clipboard
|
|
388
|
+
if (e.name === 'y' && e.ctrl) {
|
|
389
|
+
// First check if there's a mouse selection
|
|
390
|
+
const selection = renderer.getSelection();
|
|
391
|
+
if (selection) {
|
|
392
|
+
const selectedText = selection.getSelectedText();
|
|
393
|
+
if (selectedText && selectedText.length > 0) {
|
|
394
|
+
copyToClipboard(selectedText);
|
|
395
|
+
toast.show({ variant: 'success', message: 'Selection copied to clipboard' });
|
|
396
|
+
renderer.clearSelection();
|
|
397
|
+
e.preventDefault();
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
// Fallback: copy last output
|
|
402
|
+
const lastOutput = state.outputs.filter((o) => !o.isWelcome);
|
|
403
|
+
if (lastOutput.length > 0) {
|
|
404
|
+
const last = lastOutput[lastOutput.length - 1];
|
|
405
|
+
copyToClipboard(last.output);
|
|
406
|
+
toast.show({ variant: 'success', message: 'Last output copied to clipboard' });
|
|
407
|
+
}
|
|
408
|
+
e.preventDefault();
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// F12: toggle console
|
|
413
|
+
if (e.name === 'f12') {
|
|
414
|
+
const isVisible = !state.consoleVisible;
|
|
415
|
+
setState('consoleVisible', isVisible);
|
|
416
|
+
toast.show({
|
|
417
|
+
variant: 'info',
|
|
418
|
+
message: isVisible ? 'Console opened' : 'Console closed'
|
|
419
|
+
});
|
|
420
|
+
e.preventDefault();
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Tab: path completion (#6)
|
|
425
|
+
if (e.name === 'tab' && !state.isExecuting && !state.menuVisible) {
|
|
426
|
+
handleTabCompletion();
|
|
427
|
+
e.preventDefault();
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// History navigation (only when not executing and menu not visible)
|
|
432
|
+
if (!state.isExecuting && !state.menuVisible) {
|
|
433
|
+
if (e.name === 'up') {
|
|
434
|
+
navigateHistory('up');
|
|
435
|
+
e.preventDefault();
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
if (e.name === 'down') {
|
|
439
|
+
navigateHistory('down');
|
|
440
|
+
e.preventDefault();
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Any other key resets exit pending
|
|
446
|
+
if (state.exitPending && e.name !== 'c') {
|
|
447
|
+
setState('exitPending', false);
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
// #6: Tab path completion
|
|
452
|
+
async function handleTabCompletion() {
|
|
453
|
+
const input = state.buffer;
|
|
454
|
+
if (!input || !input.trim()) return;
|
|
455
|
+
|
|
456
|
+
try {
|
|
457
|
+
const { getPathCompletions } = await import('../ink-repl/utils/pathCompletion.js');
|
|
458
|
+
const completions = await getPathCompletions(session.sessionManager, input);
|
|
459
|
+
|
|
460
|
+
if (completions.length === 0) return;
|
|
461
|
+
|
|
462
|
+
if (completions.length === 1) {
|
|
463
|
+
// Single completion: auto-fill
|
|
464
|
+
setState('buffer', completions[0].value);
|
|
465
|
+
} else {
|
|
466
|
+
// Multiple completions: show in dropdown menu
|
|
467
|
+
setState('menuItems', completions);
|
|
468
|
+
setState('menuVisible', true);
|
|
469
|
+
setState('selectedIndex', 0);
|
|
470
|
+
}
|
|
471
|
+
} catch {
|
|
472
|
+
// Silently fail
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function navigateHistory(direction: 'up' | 'down') {
|
|
477
|
+
const history = state.commandHistory;
|
|
478
|
+
if (history.length === 0) return;
|
|
479
|
+
|
|
480
|
+
_navigatingHistory = true;
|
|
481
|
+
|
|
482
|
+
if (direction === 'up') {
|
|
483
|
+
if (state.historyIndex === -1) {
|
|
484
|
+
setState('savedBuffer', state.buffer);
|
|
485
|
+
}
|
|
486
|
+
const newIndex = Math.min(state.historyIndex + 1, history.length - 1);
|
|
487
|
+
setState('historyIndex', newIndex);
|
|
488
|
+
setState('buffer', history[newIndex]);
|
|
489
|
+
} else {
|
|
490
|
+
if (state.historyIndex <= 0) {
|
|
491
|
+
setState('historyIndex', -1);
|
|
492
|
+
setState('buffer', state.savedBuffer);
|
|
493
|
+
} else {
|
|
494
|
+
const newIndex = state.historyIndex - 1;
|
|
495
|
+
setState('historyIndex', newIndex);
|
|
496
|
+
setState('buffer', history[newIndex]);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
queueMicrotask(() => { _navigatingHistory = false; });
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
async function handleCommand(input: string) {
|
|
504
|
+
const trimmed = input.trim();
|
|
505
|
+
if (!trimmed) return;
|
|
506
|
+
|
|
507
|
+
// Reset state
|
|
508
|
+
setState('exitPending', false);
|
|
509
|
+
setState('historyIndex', -1);
|
|
510
|
+
setState('savedBuffer', '');
|
|
511
|
+
setState('commandHistory', (prev) => [trimmed, ...prev]);
|
|
512
|
+
setState('buffer', '');
|
|
513
|
+
setState('menuVisible', false);
|
|
514
|
+
setState('menuItems', []);
|
|
515
|
+
|
|
516
|
+
// Check if it's a builtin command
|
|
517
|
+
if (await isBuiltinCommand(trimmed)) {
|
|
518
|
+
stats.builtinCommands++;
|
|
519
|
+
const outputId = nextId();
|
|
520
|
+
setState('outputs', (prev) => [
|
|
521
|
+
...prev,
|
|
522
|
+
{
|
|
523
|
+
id: outputId,
|
|
524
|
+
command: trimmed,
|
|
525
|
+
output: '',
|
|
526
|
+
exitCode: null,
|
|
527
|
+
timestamp: Date.now(),
|
|
528
|
+
prompt: state.shellPrompt,
|
|
529
|
+
},
|
|
530
|
+
]);
|
|
531
|
+
|
|
532
|
+
try {
|
|
533
|
+
const lastOutputs = state.outputs.filter((o) => !o.isWelcome);
|
|
534
|
+
const lastItem = lastOutputs.length > 0 ? lastOutputs[lastOutputs.length - 1] : null;
|
|
535
|
+
|
|
536
|
+
const ctx = {
|
|
537
|
+
client: session.client,
|
|
538
|
+
sessionManager: session.sessionManager,
|
|
539
|
+
historyManager: session.historyManager,
|
|
540
|
+
sandboxId: session.sandboxId,
|
|
541
|
+
version: session.version,
|
|
542
|
+
stats,
|
|
543
|
+
lastCommand: lastItem?.command || '',
|
|
544
|
+
lastOutput: lastItem?.output || '',
|
|
545
|
+
terminalWidth: process.stdout.columns || 120,
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
const result = await executeBuiltinCommand(trimmed, ctx);
|
|
549
|
+
|
|
550
|
+
if (result.action === 'exit') {
|
|
551
|
+
setState('outputs', (items) =>
|
|
552
|
+
items.map((item) =>
|
|
553
|
+
item.id === outputId
|
|
554
|
+
? { ...item, output: result.output || 'Exiting...', exitCode: result.exitCode ?? 0 }
|
|
555
|
+
: item
|
|
556
|
+
)
|
|
557
|
+
);
|
|
558
|
+
saveOutputs();
|
|
559
|
+
// Destroy renderer to exit alternate screen
|
|
560
|
+
renderer.destroy();
|
|
561
|
+
// Wait a bit for renderer to fully destroy before calling onExit
|
|
562
|
+
setTimeout(() => {
|
|
563
|
+
// Notify render.tsx that we're done (triggers Promise resolve)
|
|
564
|
+
if (props.onExit) {
|
|
565
|
+
props.onExit();
|
|
566
|
+
}
|
|
567
|
+
}, 100);
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
if (result.action === 'clear') {
|
|
572
|
+
setState('outputs', (prev) => prev.filter((o) => o.isWelcome));
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if (result.action === 'retry' && result.command) {
|
|
577
|
+
setState('outputs', (items) => items.filter((item) => item.id !== outputId));
|
|
578
|
+
handleCommand(result.command);
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
setState('outputs', (items) =>
|
|
583
|
+
items.map((item) =>
|
|
584
|
+
item.id === outputId
|
|
585
|
+
? { ...item, output: result.output || '', exitCode: result.exitCode ?? 0 }
|
|
586
|
+
: item
|
|
587
|
+
)
|
|
588
|
+
);
|
|
589
|
+
addLog('info', `Builtin command: ${trimmed.split(' ')[0]}`);
|
|
590
|
+
|
|
591
|
+
// Save builtin command to history manager
|
|
592
|
+
if (session.historyManager) {
|
|
593
|
+
try {
|
|
594
|
+
await session.historyManager.addCommand(trimmed);
|
|
595
|
+
} catch {
|
|
596
|
+
// Ignore history save errors
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
} catch (err: any) {
|
|
600
|
+
const friendlyMsg = friendlyErrorMessage(err.message || String(err));
|
|
601
|
+
setState('outputs', (items) =>
|
|
602
|
+
items.map((item) =>
|
|
603
|
+
item.id === outputId
|
|
604
|
+
? { ...item, output: friendlyMsg, exitCode: 1 }
|
|
605
|
+
: item
|
|
606
|
+
)
|
|
607
|
+
);
|
|
608
|
+
addLog('error', `Builtin error: ${err.message || String(err)}`);
|
|
609
|
+
}
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// #2: Check for interactive commands before executing
|
|
614
|
+
try {
|
|
615
|
+
const interactiveCheck = await checkInteractiveCommand(trimmed);
|
|
616
|
+
if (interactiveCheck.isInteractive) {
|
|
617
|
+
const outputId = nextId();
|
|
618
|
+
const altMsg = interactiveCheck.alternative
|
|
619
|
+
? `\n${interactiveCheck.alternative}`
|
|
620
|
+
: '';
|
|
621
|
+
setState('outputs', (prev) => [
|
|
622
|
+
...prev,
|
|
623
|
+
{
|
|
624
|
+
id: outputId,
|
|
625
|
+
command: trimmed,
|
|
626
|
+
output: `'${interactiveCheck.cmdName}' is an interactive command and cannot run in REPL mode.${altMsg}`,
|
|
627
|
+
exitCode: 1,
|
|
628
|
+
timestamp: Date.now(),
|
|
629
|
+
prompt: state.shellPrompt,
|
|
630
|
+
},
|
|
631
|
+
]);
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
} catch {
|
|
635
|
+
// If check fails, proceed with execution
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// Shell command
|
|
639
|
+
stats.shellCommands++;
|
|
640
|
+
const outputId = nextId();
|
|
641
|
+
setState('outputs', (prev) => [
|
|
642
|
+
...prev,
|
|
643
|
+
{
|
|
644
|
+
id: outputId,
|
|
645
|
+
command: trimmed,
|
|
646
|
+
output: '',
|
|
647
|
+
exitCode: null,
|
|
648
|
+
timestamp: Date.now(),
|
|
649
|
+
prompt: state.shellPrompt,
|
|
650
|
+
},
|
|
651
|
+
]);
|
|
652
|
+
|
|
653
|
+
setState('isExecuting', true);
|
|
654
|
+
setState('executingCommand', trimmed);
|
|
655
|
+
setState('executionStartTime', Date.now());
|
|
656
|
+
|
|
657
|
+
// #3: Create AbortController for this execution
|
|
658
|
+
abortController = new AbortController();
|
|
659
|
+
const currentAbort = abortController;
|
|
660
|
+
|
|
661
|
+
try {
|
|
662
|
+
const result = await session.sessionManager.execute(trimmed, { signal: currentAbort.signal });
|
|
663
|
+
|
|
664
|
+
// Check if cancelled during execution
|
|
665
|
+
if (currentAbort.signal.aborted) return;
|
|
666
|
+
|
|
667
|
+
// Format command output using responseFormatter
|
|
668
|
+
const formatted = formatCommandOutput(result ?? { exit_code: 0, output: '' }, { locale: 'zh' });
|
|
669
|
+
let output = formatted.output ?? '';
|
|
670
|
+
// #1: Fix exit_code field name (SDK returns snake_case)
|
|
671
|
+
const exitCode = formatted.exitCode ?? result?.exit_code ?? 0;
|
|
672
|
+
|
|
673
|
+
// Handle abnormal exit codes with helpful messages
|
|
674
|
+
if (exitCode === -1) {
|
|
675
|
+
// -1 typically indicates process was killed or internal error
|
|
676
|
+
const hint = output ? '' : 'Command terminated abnormally (exit code -1). This may indicate a timeout, signal termination, or internal error.';
|
|
677
|
+
output = output || hint;
|
|
678
|
+
addLog('error', `Command terminated abnormally (exit code -1)`);
|
|
679
|
+
} else if (exitCode === 137) {
|
|
680
|
+
// 128 + 9 (SIGKILL) - often OOM killed
|
|
681
|
+
output += output ? '\n' : '';
|
|
682
|
+
output += '[Process killed (exit code 137). Possibly OOM or manually terminated.]';
|
|
683
|
+
addLog('warn', `Process killed (exit code 137)`);
|
|
684
|
+
} else if (exitCode === 143) {
|
|
685
|
+
// 128 + 15 (SIGTERM) - graceful termination
|
|
686
|
+
output += output ? '\n' : '';
|
|
687
|
+
output += '[Process terminated (exit code 143). Received SIGTERM.]';
|
|
688
|
+
addLog('warn', `Process terminated (exit code 143)`);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
setState('outputs', (items) =>
|
|
692
|
+
items.map((item) =>
|
|
693
|
+
item.id === outputId ? { ...item, output, exitCode, metaInfo: formatted.metaInfo, tips: formatted.tips } : item
|
|
694
|
+
)
|
|
695
|
+
);
|
|
696
|
+
|
|
697
|
+
// Log execution result
|
|
698
|
+
if (exitCode === 0) {
|
|
699
|
+
addLog('info', `Command completed: ${trimmed.slice(0, 30)}${trimmed.length > 30 ? '...' : ''}`);
|
|
700
|
+
} else if (exitCode > 0) {
|
|
701
|
+
addLog('warn', `Command exited with code ${exitCode}`);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// #7: Update CWD after every shell command
|
|
705
|
+
try {
|
|
706
|
+
const pwdResult = await session.sessionManager.execute('pwd');
|
|
707
|
+
if (pwdResult?.output) {
|
|
708
|
+
const cwd = pwdResult.output.trim();
|
|
709
|
+
const newPrompt = `${session.user}@${session.hostname}:${cwd}# `;
|
|
710
|
+
setState('shellPrompt', newPrompt);
|
|
711
|
+
|
|
712
|
+
// Update work_dir in history manager
|
|
713
|
+
if (session.historyManager && session.historyManager.updateWorkDir) {
|
|
714
|
+
await session.historyManager.updateWorkDir(cwd);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
} catch {
|
|
718
|
+
// Ignore prompt update errors
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// #5: Save command to history manager
|
|
722
|
+
if (session.historyManager) {
|
|
723
|
+
try {
|
|
724
|
+
await session.historyManager.addCommand(trimmed);
|
|
725
|
+
} catch {
|
|
726
|
+
// Ignore history save errors
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
} catch (err: any) {
|
|
730
|
+
if (currentAbort.signal.aborted) return;
|
|
731
|
+
|
|
732
|
+
// Parse error to extract meaningful output and exit code
|
|
733
|
+
const { output, exitCode } = parseExecutionError(err);
|
|
734
|
+
|
|
735
|
+
// Check if error message contains an actual exit code from command execution
|
|
736
|
+
// If not (timeout/network error), don't pass exit_code to formatCommandOutput
|
|
737
|
+
const hasExitCodeInMessage = /exit code \d+/.test(err.message || '');
|
|
738
|
+
|
|
739
|
+
// Format error output with metaInfo and tips
|
|
740
|
+
const errorResult = hasExitCodeInMessage ? { output, exit_code: exitCode } : { output };
|
|
741
|
+
const formatted = formatCommandOutput(errorResult, { locale: 'zh' });
|
|
742
|
+
|
|
743
|
+
setState('outputs', (items) =>
|
|
744
|
+
items.map((item) =>
|
|
745
|
+
item.id === outputId
|
|
746
|
+
? { ...item, output: formatted.output || output, exitCode: formatted.exitCode, metaInfo: formatted.metaInfo, tips: formatted.tips }
|
|
747
|
+
: item
|
|
748
|
+
)
|
|
749
|
+
);
|
|
750
|
+
addLog('error', `Execution error: ${err.message || String(err)}`);
|
|
751
|
+
} finally {
|
|
752
|
+
// Only reset execution state if no new command has taken over
|
|
753
|
+
// (abortController changes when a new command starts)
|
|
754
|
+
const isCurrentExecution = abortController === currentAbort;
|
|
755
|
+
if (isCurrentExecution) {
|
|
756
|
+
abortController = null;
|
|
757
|
+
// Calculate execution duration
|
|
758
|
+
const startTime = state.executionStartTime;
|
|
759
|
+
if (startTime) {
|
|
760
|
+
setState('lastExecutionDuration', Date.now() - startTime);
|
|
761
|
+
}
|
|
762
|
+
setState('isExecuting', false);
|
|
763
|
+
setState('executingCommand', '');
|
|
764
|
+
setState('executionStartTime', null);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function handleInputChange(value: string) {
|
|
770
|
+
setState('buffer', value);
|
|
771
|
+
|
|
772
|
+
// #4: Reset historyIndex when typing after navigating history
|
|
773
|
+
if (state.historyIndex !== -1 && !_navigatingHistory) {
|
|
774
|
+
setState('historyIndex', -1);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// Slash command autocomplete
|
|
778
|
+
if (value === '/') {
|
|
779
|
+
const trigger = session.triggers[0];
|
|
780
|
+
if (trigger) {
|
|
781
|
+
const items = trigger.getItems('/');
|
|
782
|
+
setState('menuItems', items);
|
|
783
|
+
setState('menuVisible', true);
|
|
784
|
+
setState('selectedIndex', 0);
|
|
785
|
+
}
|
|
786
|
+
} else if (value.startsWith('/') && state.menuVisible) {
|
|
787
|
+
const trigger = session.triggers[0];
|
|
788
|
+
if (trigger) {
|
|
789
|
+
const items = trigger.getItems(value).filter(
|
|
790
|
+
(item: any) => item.value.toLowerCase().startsWith(value.toLowerCase())
|
|
791
|
+
);
|
|
792
|
+
setState('menuItems', items);
|
|
793
|
+
setState('selectedIndex', 0);
|
|
794
|
+
if (items.length === 0) {
|
|
795
|
+
setState('menuVisible', false);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
} else if (!value.startsWith('/')) {
|
|
799
|
+
setState('menuVisible', false);
|
|
800
|
+
setState('menuItems', []);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
return (
|
|
805
|
+
<box
|
|
806
|
+
width="100%"
|
|
807
|
+
height="100%"
|
|
808
|
+
flexDirection="column"
|
|
809
|
+
backgroundColor={theme.colors.background}
|
|
810
|
+
onMouseUp={handleMouseUp}
|
|
811
|
+
>
|
|
812
|
+
<Show when={ready()} fallback={<ConnectingScreen sandboxId={session.sandboxId} />}>
|
|
813
|
+
<Show when={state.viewMode === 'detail'}>
|
|
814
|
+
<DetailView />
|
|
815
|
+
</Show>
|
|
816
|
+
<Show when={state.viewMode === 'repl'}>
|
|
817
|
+
{/* Header */}
|
|
818
|
+
<Header />
|
|
819
|
+
|
|
820
|
+
{/* Scrollable content area */}
|
|
821
|
+
<box flexDirection="column" flexGrow={1} overflow="hidden" minHeight={0}>
|
|
822
|
+
<OutputArea />
|
|
823
|
+
<ExecutionStatus
|
|
824
|
+
isExecuting={state.isExecuting}
|
|
825
|
+
command={state.executingCommand}
|
|
826
|
+
lastDuration={state.lastExecutionDuration}
|
|
827
|
+
/>
|
|
828
|
+
</box>
|
|
829
|
+
|
|
830
|
+
{/* Fixed bottom UI */}
|
|
831
|
+
<box flexDirection="column" flexShrink={0}>
|
|
832
|
+
<PromptInput onSubmit={handleCommand} onInputChange={handleInputChange} onInputRef={(ref) => { inputRef = ref; }} />
|
|
833
|
+
<DropdownMenu />
|
|
834
|
+
<Console />
|
|
835
|
+
</box>
|
|
836
|
+
|
|
837
|
+
<StatusBar />
|
|
838
|
+
<Toast />
|
|
839
|
+
</Show>
|
|
840
|
+
</Show>
|
|
841
|
+
</box>
|
|
842
|
+
);
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
function copyToClipboard(text: string) {
|
|
846
|
+
try {
|
|
847
|
+
const { execSync } = require('child_process');
|
|
848
|
+
if (process.platform === 'darwin') {
|
|
849
|
+
execSync('pbcopy', { input: text, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
850
|
+
} else {
|
|
851
|
+
const linuxTools = [
|
|
852
|
+
'xclip -selection clipboard',
|
|
853
|
+
'xsel --clipboard --input',
|
|
854
|
+
'wl-copy',
|
|
855
|
+
];
|
|
856
|
+
for (const cmd of linuxTools) {
|
|
857
|
+
try {
|
|
858
|
+
execSync(cmd, { input: text, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
859
|
+
break;
|
|
860
|
+
} catch {
|
|
861
|
+
// try next tool
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
} catch {
|
|
866
|
+
// Clipboard not available
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// Re-import useSession here to avoid circular dependency
|
|
871
|
+
import { useSession } from './contexts/SessionContext.tsx';
|
|
872
|
+
|
|
873
|
+
export interface AppProps {
|
|
874
|
+
session: SessionInfo;
|
|
875
|
+
theme?: string;
|
|
876
|
+
onExit?: () => void;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
export function App(props: AppProps) {
|
|
880
|
+
return (
|
|
881
|
+
<ThemeProvider theme={props.theme}>
|
|
882
|
+
<SessionProvider session={props.session}>
|
|
883
|
+
<ReplProvider shellPrompt={props.session.initialPrompt}>
|
|
884
|
+
<ToastProvider>
|
|
885
|
+
<ReplShell onExit={props.onExit} />
|
|
886
|
+
</ToastProvider>
|
|
887
|
+
</ReplProvider>
|
|
888
|
+
</SessionProvider>
|
|
889
|
+
</ThemeProvider>
|
|
890
|
+
);
|
|
891
|
+
}
|