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,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output selection utilities
|
|
3
|
+
* Handles text extraction from output items, filtering out borders and padding
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Build a line map for outputs, tracking which lines belong to which output
|
|
8
|
+
* and filtering out border characters
|
|
9
|
+
* @param {Array} outputs - Array of output items
|
|
10
|
+
* @param {string} shellPrompt - Current shell prompt
|
|
11
|
+
* @returns {Object} { lines: string[], lineMap: Array<{outputId, lineType, originalLine}> }
|
|
12
|
+
*/
|
|
13
|
+
export function buildOutputLineMap(outputs, shellPrompt) {
|
|
14
|
+
const lines = [];
|
|
15
|
+
const lineMap = [];
|
|
16
|
+
|
|
17
|
+
for (const output of outputs) {
|
|
18
|
+
const { id, command, output: text, exitCode, isWelcome } = output;
|
|
19
|
+
|
|
20
|
+
if (isWelcome) {
|
|
21
|
+
// Welcome message - single line, no border filtering needed
|
|
22
|
+
lines.push(text);
|
|
23
|
+
lineMap.push({ outputId: id, lineType: 'content', originalLine: text });
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Command line: prompt + command
|
|
28
|
+
const commandLine = `${shellPrompt}${command}`;
|
|
29
|
+
lines.push(commandLine);
|
|
30
|
+
lineMap.push({ outputId: id, lineType: 'command', originalLine: commandLine });
|
|
31
|
+
|
|
32
|
+
// Output text - split by newlines
|
|
33
|
+
if (text) {
|
|
34
|
+
const outputLines = text.split('\n');
|
|
35
|
+
for (const line of outputLines) {
|
|
36
|
+
lines.push(line);
|
|
37
|
+
lineMap.push({ outputId: id, lineType: 'output', originalLine: line });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return { lines, lineMap };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Filter border characters from selected text
|
|
47
|
+
* Removes box drawing characters commonly used for borders
|
|
48
|
+
* @param {string} text - Selected text
|
|
49
|
+
* @returns {string} Filtered text
|
|
50
|
+
*/
|
|
51
|
+
export function filterBorderCharacters(text) {
|
|
52
|
+
// Box drawing characters used by Ink borders
|
|
53
|
+
const borderChars = /[─│┌┐└┘├┤┬┴┼╭╮╰╯═║╔╗╚╝╠╣╦╩╬]/g;
|
|
54
|
+
|
|
55
|
+
return text
|
|
56
|
+
.split('\n')
|
|
57
|
+
.map(line => {
|
|
58
|
+
// Remove border characters
|
|
59
|
+
let cleaned = line.replace(borderChars, '');
|
|
60
|
+
// Trim excessive whitespace but preserve meaningful indentation
|
|
61
|
+
return cleaned.trimEnd();
|
|
62
|
+
})
|
|
63
|
+
.filter(line => line.length > 0) // Remove empty lines that were just borders
|
|
64
|
+
.join('\n');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Extract text from a screen region, considering the rendered layout
|
|
69
|
+
* This function is aware of the OutputItem layout with borders
|
|
70
|
+
* @param {number} startX - Start X coordinate (1-based)
|
|
71
|
+
* @param {number} startY - Start Y coordinate (1-based)
|
|
72
|
+
* @param {number} endX - End X coordinate (1-based)
|
|
73
|
+
* @param {number} endY - End Y coordinate (1-based)
|
|
74
|
+
* @param {Array} outputs - Array of output items
|
|
75
|
+
* @param {string} shellPrompt - Current shell prompt
|
|
76
|
+
* @returns {string} Selected text with borders filtered
|
|
77
|
+
*/
|
|
78
|
+
export function extractOutputText(startX, startY, endX, endY, outputs, shellPrompt) {
|
|
79
|
+
const { lines } = buildOutputLineMap(outputs, shellPrompt);
|
|
80
|
+
|
|
81
|
+
if (!lines || lines.length === 0) return '';
|
|
82
|
+
|
|
83
|
+
// Normalize coordinates (ensure start <= end)
|
|
84
|
+
if (startY > endY || (startY === endY && startX > endX)) {
|
|
85
|
+
[startX, endX] = [endX, startX];
|
|
86
|
+
[startY, endY] = [endY, startY];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Convert 1-based to 0-based
|
|
90
|
+
const startRow = Math.max(0, startY - 1);
|
|
91
|
+
const endRow = Math.min(lines.length - 1, endY - 1);
|
|
92
|
+
const startCol = Math.max(0, startX - 1);
|
|
93
|
+
const endCol = Math.max(0, endX - 1);
|
|
94
|
+
|
|
95
|
+
if (startRow > endRow) return '';
|
|
96
|
+
if (startRow === endRow && startCol > endCol) return '';
|
|
97
|
+
|
|
98
|
+
const selectedLines = [];
|
|
99
|
+
|
|
100
|
+
for (let row = startRow; row <= endRow; row++) {
|
|
101
|
+
const line = lines[row] || '';
|
|
102
|
+
|
|
103
|
+
if (row === startRow && row === endRow) {
|
|
104
|
+
// Single line selection
|
|
105
|
+
selectedLines.push(line.substring(startCol, endCol));
|
|
106
|
+
} else if (row === startRow) {
|
|
107
|
+
// First line of multi-line selection
|
|
108
|
+
selectedLines.push(line.substring(startCol));
|
|
109
|
+
} else if (row === endRow) {
|
|
110
|
+
// Last line of multi-line selection
|
|
111
|
+
selectedLines.push(line.substring(0, endCol));
|
|
112
|
+
} else {
|
|
113
|
+
// Middle lines - select entire line
|
|
114
|
+
selectedLines.push(line);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const rawText = selectedLines.join('\n');
|
|
119
|
+
return filterBorderCharacters(rawText);
|
|
120
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { truncateOutputToLines } from './truncate.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Estimate the rendered height (rows) of an OutputItem card in the OutputArea.
|
|
5
|
+
*
|
|
6
|
+
* This mirrors the structure in `components/OutputItem.js`:
|
|
7
|
+
* - Welcome item: 1 row (best-effort; welcome is rare after first render)
|
|
8
|
+
* - Normal item: top border + command line + (N output lines) + (optional hint) + bottom border
|
|
9
|
+
*
|
|
10
|
+
* Note: OutputArea applies `rowGap: 1` between items, which is handled by the caller.
|
|
11
|
+
*
|
|
12
|
+
* @param {Object} item
|
|
13
|
+
* @param {number} contentWidth Outer card width
|
|
14
|
+
* @param {number} maxLines Max visual output lines to show
|
|
15
|
+
* @returns {number}
|
|
16
|
+
*/
|
|
17
|
+
export function estimateOutputItemHeight(item, contentWidth, maxLines = 50) {
|
|
18
|
+
if (!item) return 0;
|
|
19
|
+
if (item.isWelcome) {
|
|
20
|
+
return 1;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const boxInnerWidth = Math.max(10, contentWidth - 4);
|
|
24
|
+
const hasOutput = typeof item.output === 'string' ? item.output.length > 0 : !!item.output;
|
|
25
|
+
const { lines, truncated, hiddenLines } = hasOutput
|
|
26
|
+
? truncateOutputToLines(String(item.output), boxInnerWidth, maxLines)
|
|
27
|
+
: { lines: [], truncated: false, hiddenLines: 0 };
|
|
28
|
+
|
|
29
|
+
const outputLineCount = lines.length;
|
|
30
|
+
const hintLineCount = truncated && hiddenLines > 0 ? 1 : 0;
|
|
31
|
+
|
|
32
|
+
// borders(2) + command line(1) + output lines + hint line (optional)
|
|
33
|
+
return 3 + outputLineCount + hintLineCount;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Pick the tail of outputs that best fits within a target height.
|
|
38
|
+
* Always returns at least 1 item if outputs is non-empty.
|
|
39
|
+
*
|
|
40
|
+
* @param {Array<Object>} outputs
|
|
41
|
+
* @param {Object} options
|
|
42
|
+
* @param {number} options.contentWidth Outer card width
|
|
43
|
+
* @param {number} options.maxLines Max visual output lines to show per card
|
|
44
|
+
* @param {number} options.maxHeight Max rows available for cards (excluding fixed UI)
|
|
45
|
+
* @param {number} [options.rowGap=1] Gap rows between cards
|
|
46
|
+
* @returns {Array<Object>}
|
|
47
|
+
*/
|
|
48
|
+
export function pickVisibleOutputsByHeight(
|
|
49
|
+
outputs,
|
|
50
|
+
{ contentWidth, maxLines, maxHeight, rowGap = 1 } = {},
|
|
51
|
+
) {
|
|
52
|
+
const list = Array.isArray(outputs) ? outputs : [];
|
|
53
|
+
if (list.length === 0) return [];
|
|
54
|
+
|
|
55
|
+
const heightLimit = Math.max(1, Number(maxHeight || 1));
|
|
56
|
+
let used = 0;
|
|
57
|
+
let start = list.length;
|
|
58
|
+
|
|
59
|
+
for (let i = list.length - 1; i >= 0; i--) {
|
|
60
|
+
const itemHeight = estimateOutputItemHeight(list[i], contentWidth, maxLines);
|
|
61
|
+
const needed = used === 0 ? itemHeight : itemHeight + rowGap;
|
|
62
|
+
|
|
63
|
+
// Always include at least one (the most recent) item.
|
|
64
|
+
if (used > 0 && used + needed > heightLimit) {
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
used += needed;
|
|
69
|
+
start = i;
|
|
70
|
+
|
|
71
|
+
if (used >= heightLimit) {
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return list.slice(start);
|
|
77
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilities for paginated file loading
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Extract file path from cat command
|
|
7
|
+
* @param {string} command - Shell command
|
|
8
|
+
* @returns {string|null} - File path or null
|
|
9
|
+
*/
|
|
10
|
+
export function extractCatFilePath(command) {
|
|
11
|
+
const trimmed = command.trim();
|
|
12
|
+
const match = trimmed.match(/^cat\s+(.+)$/);
|
|
13
|
+
return match ? match[1].trim() : null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Check if command should enable pagination
|
|
18
|
+
* @param {string} command - Shell command
|
|
19
|
+
* @returns {boolean}
|
|
20
|
+
*/
|
|
21
|
+
export function shouldEnablePagination(command) {
|
|
22
|
+
const trimmed = command.trim();
|
|
23
|
+
// Must be pure cat command (no pipes, no -n option)
|
|
24
|
+
return /^cat\s+[^\|]+$/.test(trimmed) && !/-n\s+/.test(trimmed);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Calculate line range for previous page (prepend)
|
|
29
|
+
* @param {number} currentStartLine
|
|
30
|
+
* @param {number} pageSize
|
|
31
|
+
* @returns {{newStartLine: number, newEndLine: number}}
|
|
32
|
+
*/
|
|
33
|
+
export function calculatePreviousPageRange(currentStartLine, pageSize) {
|
|
34
|
+
const newStartLine = Math.max(1, currentStartLine - pageSize);
|
|
35
|
+
const newEndLine = currentStartLine - 1;
|
|
36
|
+
return { newStartLine, newEndLine };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Calculate line range for next page (append)
|
|
41
|
+
* @param {number} currentEndLine
|
|
42
|
+
* @param {number} totalLines
|
|
43
|
+
* @param {number} pageSize
|
|
44
|
+
* @returns {{newStartLine: number, newEndLine: number}}
|
|
45
|
+
*/
|
|
46
|
+
export function calculateNextPageRange(currentEndLine, totalLines, pageSize) {
|
|
47
|
+
const newStartLine = currentEndLine + 1;
|
|
48
|
+
const newEndLine = Math.min(totalLines, currentEndLine + pageSize);
|
|
49
|
+
return { newStartLine, newEndLine };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Check if should trigger previous page load (near top)
|
|
54
|
+
* @param {number} scrollOffset - Current scroll position
|
|
55
|
+
* @param {number} threshold - Lines from top to trigger
|
|
56
|
+
* @param {number} loadedStartLine - Currently loaded start line
|
|
57
|
+
* @returns {boolean}
|
|
58
|
+
*/
|
|
59
|
+
export function shouldLoadPreviousPage(scrollOffset, threshold, loadedStartLine) {
|
|
60
|
+
return scrollOffset < threshold && loadedStartLine > 1;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Check if should trigger next page load (near bottom)
|
|
65
|
+
* @param {number} scrollOffset - Current scroll position
|
|
66
|
+
* @param {number} totalVisibleLines - Total lines in content
|
|
67
|
+
* @param {number} viewportHeight - Visible height in lines
|
|
68
|
+
* @param {number} threshold - Lines from bottom to trigger
|
|
69
|
+
* @param {number} loadedEndLine - Currently loaded end line
|
|
70
|
+
* @param {number} totalLines - Total lines in file
|
|
71
|
+
* @returns {boolean}
|
|
72
|
+
*/
|
|
73
|
+
export function shouldLoadNextPage(scrollOffset, totalVisibleLines, viewportHeight, threshold, loadedEndLine, totalLines) {
|
|
74
|
+
const distanceFromBottom = totalVisibleLines - scrollOffset - viewportHeight;
|
|
75
|
+
return distanceFromBottom < threshold && loadedEndLine < totalLines;
|
|
76
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parameter hint calculation for inline placeholder display
|
|
3
|
+
*
|
|
4
|
+
* Shows placeholder text (e.g., "@<local> @<remote>") when user is typing
|
|
5
|
+
* /upload or /download commands to help them remember the parameter format.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { COMMAND_PARAM_TEMPLATES } from '../builtinCommands.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Calculate placeholder text to display
|
|
12
|
+
*
|
|
13
|
+
* Key logic:
|
|
14
|
+
* 1. Cursor must be at end of buffer (FR-006)
|
|
15
|
+
* 2. Command must be /upload or /download with space after
|
|
16
|
+
* 3. Show remaining @ parameters based on how many have been started
|
|
17
|
+
*
|
|
18
|
+
* @param {string} buffer - Full input buffer
|
|
19
|
+
* @param {number} cursorPosition - Current cursor position
|
|
20
|
+
* @returns {string|null} Placeholder text or null if not applicable
|
|
21
|
+
*/
|
|
22
|
+
export function getPlaceholderText(buffer, cursorPosition) {
|
|
23
|
+
// FR-006: Cursor must be at end of buffer
|
|
24
|
+
if (cursorPosition !== buffer.length) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Parse command - must have space after command name
|
|
29
|
+
const match = buffer.match(/^\/(upload|download)\s+/);
|
|
30
|
+
if (!match) {
|
|
31
|
+
// No space after command = don't show placeholder (might be typing other command)
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const command = match[1];
|
|
36
|
+
const template = COMMAND_PARAM_TEMPLATES[command];
|
|
37
|
+
if (!template) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Get input after command (including the space)
|
|
42
|
+
const afterCommand = buffer.slice(match[0].length);
|
|
43
|
+
|
|
44
|
+
// Count @ symbols that have been started
|
|
45
|
+
let startedParams = 0;
|
|
46
|
+
for (let i = 0; i < afterCommand.length; i++) {
|
|
47
|
+
if (afterCommand[i] === '@') {
|
|
48
|
+
startedParams++;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// If all params started, no placeholder needed
|
|
53
|
+
if (startedParams >= template.params.length) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Show remaining params
|
|
58
|
+
const remainingParams = template.params.slice(startedParams);
|
|
59
|
+
return remainingParams.map(p => `${p.prefix}${p.placeholder}`).join(' ');
|
|
60
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 底层错误消息友好化映射表
|
|
3
|
+
* 将底层技术错误转换为用户友好的提示信息
|
|
4
|
+
*/
|
|
5
|
+
const ERROR_FRIENDLY_MAP = {
|
|
6
|
+
// Bun/运行时兼容性问题
|
|
7
|
+
'First argument must be an Error object': '网络连接异常,请检查网络后重试',
|
|
8
|
+
|
|
9
|
+
// Node 网络错误
|
|
10
|
+
'ECONNREFUSED': '无法连接到服务器',
|
|
11
|
+
'ETIMEDOUT': '请求超时,请重试',
|
|
12
|
+
'ENOTFOUND': '无法解析服务器地址',
|
|
13
|
+
'ECONNRESET': '网络连接已断开',
|
|
14
|
+
'EPIPE': '连接已关闭',
|
|
15
|
+
'EHOSTUNREACH': '无法访问服务器',
|
|
16
|
+
'ENETUNREACH': '网络不可达',
|
|
17
|
+
|
|
18
|
+
// HTTP 错误
|
|
19
|
+
'socket hang up': '网络连接已断开',
|
|
20
|
+
'network error': '网络异常',
|
|
21
|
+
'Network Error': '网络异常',
|
|
22
|
+
'request aborted': '请求被中断',
|
|
23
|
+
'canceled': '请求已取消',
|
|
24
|
+
|
|
25
|
+
// SSL/TLS 错误
|
|
26
|
+
'UNABLE_TO_VERIFY_LEAF_SIGNATURE': 'SSL证书验证失败',
|
|
27
|
+
'CERT_HAS_EXPIRED': 'SSL证书已过期',
|
|
28
|
+
'DEPTH_ZERO_SELF_SIGNED_CERT': 'SSL证书不受信任',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 错误消息友好化转换
|
|
33
|
+
* 将底层技术错误转换为用户友好的提示
|
|
34
|
+
*
|
|
35
|
+
* @param {string} message - 原始错误消息
|
|
36
|
+
* @returns {string} 友好化后的错误消息
|
|
37
|
+
*/
|
|
38
|
+
export function friendlyErrorMessage(message) {
|
|
39
|
+
if (!message || typeof message !== 'string') {
|
|
40
|
+
return '未知错误';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 1. 精确匹配
|
|
44
|
+
if (ERROR_FRIENDLY_MAP[message]) {
|
|
45
|
+
return ERROR_FRIENDLY_MAP[message];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 2. 部分匹配(处理包含错误码的消息)
|
|
49
|
+
for (const [key, friendly] of Object.entries(ERROR_FRIENDLY_MAP)) {
|
|
50
|
+
if (message.includes(key)) {
|
|
51
|
+
return friendly;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 3. HTTP 状态码错误处理
|
|
56
|
+
const statusMatch = message.match(/status code (\d+)/i);
|
|
57
|
+
if (statusMatch) {
|
|
58
|
+
const code = parseInt(statusMatch[1], 10);
|
|
59
|
+
if (code >= 500) {
|
|
60
|
+
return '服务暂时不可用,请稍后重试';
|
|
61
|
+
}
|
|
62
|
+
if (code === 401 || code === 403) {
|
|
63
|
+
return '认证失败,请检查登录状态';
|
|
64
|
+
}
|
|
65
|
+
if (code === 404) {
|
|
66
|
+
return '请求的资源不存在';
|
|
67
|
+
}
|
|
68
|
+
if (code === 429) {
|
|
69
|
+
return '请求过于频繁,请稍后重试';
|
|
70
|
+
}
|
|
71
|
+
return `请求失败 (${code})`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 4. JSON 解析错误
|
|
75
|
+
if (message.includes('Unexpected token') || message.includes('JSON')) {
|
|
76
|
+
return '服务器响应格式异常';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 5. 超时相关
|
|
80
|
+
if (message.toLowerCase().includes('timeout') || message.includes('超时')) {
|
|
81
|
+
return '请求超时,请重试';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 6. 网络相关关键词
|
|
85
|
+
const networkKeywords = ['network', 'socket', 'connection', 'connect', '网络'];
|
|
86
|
+
const lowerMessage = message.toLowerCase();
|
|
87
|
+
if (networkKeywords.some(kw => lowerMessage.includes(kw))) {
|
|
88
|
+
return '网络连接异常,请检查网络后重试';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 7. 返回原始消息(如果已经是用户友好的)
|
|
92
|
+
return message;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Parse execution error to extract meaningful output
|
|
97
|
+
*
|
|
98
|
+
* Handles errors like:
|
|
99
|
+
* Error: Failed to run in session: {"status":"Failed","error":"Command 'aaa' failed with exit code 127. Here is the output:\n'bash: aaa: command not found'","result":null}
|
|
100
|
+
*
|
|
101
|
+
* Exit codes:
|
|
102
|
+
* - 0: Success
|
|
103
|
+
* - 1-255: Command exit codes
|
|
104
|
+
* - -1: Timeout/Network/Session errors (returned by SDK, not parsed from error message)
|
|
105
|
+
*
|
|
106
|
+
* @param {Error} error - The error object
|
|
107
|
+
* @returns {{ output: string, exitCode: number }}
|
|
108
|
+
*/
|
|
109
|
+
export function parseExecutionError(error) {
|
|
110
|
+
const message = error?.message || '';
|
|
111
|
+
|
|
112
|
+
if (!message) {
|
|
113
|
+
return { output: 'Unknown error', exitCode: 1 };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Try to extract JSON from the error message first
|
|
117
|
+
const jsonMatch = message.match(/\{[\s\S]*\}/);
|
|
118
|
+
|
|
119
|
+
if (jsonMatch) {
|
|
120
|
+
try {
|
|
121
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
122
|
+
|
|
123
|
+
if (parsed.error) {
|
|
124
|
+
// Extract exit code from error message
|
|
125
|
+
const exitCodeMatch = parsed.error.match(/exit code (\d+)/);
|
|
126
|
+
const exitCode = exitCodeMatch ? parseInt(exitCodeMatch[1], 10) : 1;
|
|
127
|
+
|
|
128
|
+
// Extract the actual shell output from the error
|
|
129
|
+
// Pattern 1: "Here is the output:\n'actual message'"
|
|
130
|
+
let outputMatch = parsed.error.match(/Here is the output:\s*\n'(.+?)'/s);
|
|
131
|
+
|
|
132
|
+
if (outputMatch) {
|
|
133
|
+
// Extract just the command output, remove extra formatting
|
|
134
|
+
let output = outputMatch[1];
|
|
135
|
+
|
|
136
|
+
// If output contains "---- Stderr ----" sections, simplify it
|
|
137
|
+
const stderrMatch = output.match(/---- Stderr ----\s*([\s\S]*?)(?:---- Stdout ----)?/);
|
|
138
|
+
if (stderrMatch && stderrMatch[1].trim()) {
|
|
139
|
+
// Only show stderr content for syntax errors
|
|
140
|
+
output = stderrMatch[1].trim();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
output,
|
|
145
|
+
exitCode,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Pattern 2: Extract just the bash error message
|
|
150
|
+
const bashErrorMatch = parsed.error.match(/bash: .+/);
|
|
151
|
+
if (bashErrorMatch) {
|
|
152
|
+
return {
|
|
153
|
+
output: bashErrorMatch[0],
|
|
154
|
+
exitCode,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Fallback: use the error field directly
|
|
159
|
+
return {
|
|
160
|
+
output: parsed.error,
|
|
161
|
+
exitCode,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
} catch (e) {
|
|
165
|
+
// JSON parsing failed, fall through to default handling
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// 对底层错误消息进行友好化转换
|
|
170
|
+
return {
|
|
171
|
+
output: friendlyErrorMessage(message),
|
|
172
|
+
exitCode: 1,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path Completion Utilities
|
|
3
|
+
* Provides path auto-completion for shell commands in the REPL
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { hasDangerousShellChars, shellEscapePosix } from './remotePathSafety.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Commands that typically take path arguments
|
|
10
|
+
*/
|
|
11
|
+
export const PATH_COMMANDS = [
|
|
12
|
+
'cd', 'cat', 'ls', 'less', 'more', 'head', 'tail',
|
|
13
|
+
'vi', 'vim', 'nano', 'emacs',
|
|
14
|
+
'rm', 'cp', 'mv', 'mkdir', 'rmdir', 'touch',
|
|
15
|
+
'chmod', 'chown', 'chgrp',
|
|
16
|
+
'source', '.',
|
|
17
|
+
'python', 'python3', 'node', 'bash', 'sh',
|
|
18
|
+
'file', 'stat', 'du', 'find',
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Check if a command typically takes path arguments
|
|
23
|
+
* @param {string} cmd - Command name
|
|
24
|
+
* @returns {boolean}
|
|
25
|
+
*/
|
|
26
|
+
export function isPathCommand(cmd) {
|
|
27
|
+
if (!cmd) return false;
|
|
28
|
+
return PATH_COMMANDS.includes(cmd);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Parse input to extract path completion context
|
|
33
|
+
* @param {string} input - Current input buffer
|
|
34
|
+
* @returns {Object} Parsed path info
|
|
35
|
+
*/
|
|
36
|
+
export function parsePathInput(input) {
|
|
37
|
+
if (!input || !input.trim()) {
|
|
38
|
+
return { needsCompletion: false };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Check if input ends with whitespace (user wants to complete from current dir)
|
|
42
|
+
const endsWithSpace = /\s$/.test(input);
|
|
43
|
+
const parts = input.split(/\s+/).filter(Boolean);
|
|
44
|
+
const command = parts[0];
|
|
45
|
+
|
|
46
|
+
// Skip path completion for built-in commands (starting with /)
|
|
47
|
+
// These commands use @ for path completion instead
|
|
48
|
+
if (command && command.startsWith('/')) {
|
|
49
|
+
return { needsCompletion: false };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Handle trailing space case: "cd " should trigger current directory completion
|
|
53
|
+
if (endsWithSpace && isPathCommand(command)) {
|
|
54
|
+
return {
|
|
55
|
+
command,
|
|
56
|
+
prefix: input,
|
|
57
|
+
dirPath: './',
|
|
58
|
+
filePrefix: '',
|
|
59
|
+
needsCompletion: true,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const lastPart = parts[parts.length - 1] || '';
|
|
64
|
+
|
|
65
|
+
// No argument to complete
|
|
66
|
+
if (parts.length < 2 || !lastPart) {
|
|
67
|
+
return { needsCompletion: false };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Check if completion is needed
|
|
71
|
+
const looksLikePath = lastPart.includes('/') || lastPart.startsWith('.') || lastPart.startsWith('~');
|
|
72
|
+
const isPathCmd = isPathCommand(command);
|
|
73
|
+
|
|
74
|
+
if (!looksLikePath && !isPathCmd) {
|
|
75
|
+
return { needsCompletion: false };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Parse the path
|
|
79
|
+
let dirPath, filePrefix;
|
|
80
|
+
|
|
81
|
+
if (lastPart.endsWith('/')) {
|
|
82
|
+
dirPath = lastPart;
|
|
83
|
+
filePrefix = '';
|
|
84
|
+
} else if (lastPart.includes('/')) {
|
|
85
|
+
const lastSlash = lastPart.lastIndexOf('/');
|
|
86
|
+
dirPath = lastPart.substring(0, lastSlash + 1);
|
|
87
|
+
filePrefix = lastPart.substring(lastSlash + 1);
|
|
88
|
+
} else {
|
|
89
|
+
// No slash - complete in current directory
|
|
90
|
+
dirPath = './';
|
|
91
|
+
filePrefix = lastPart;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Build prefix (everything before the path argument)
|
|
95
|
+
const prefix = parts.slice(0, -1).join(' ') + ' ';
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
command,
|
|
99
|
+
prefix,
|
|
100
|
+
dirPath,
|
|
101
|
+
filePrefix,
|
|
102
|
+
needsCompletion: true,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Build completion menu items from file list
|
|
108
|
+
* @param {Object} parsed - Parsed path info from parsePathInput
|
|
109
|
+
* @param {string[]} files - List of files in directory
|
|
110
|
+
* @returns {Array<{label: string, value: string, description: string}>}
|
|
111
|
+
*/
|
|
112
|
+
export function buildCompletions(parsed, files) {
|
|
113
|
+
const { prefix, dirPath, filePrefix } = parsed;
|
|
114
|
+
|
|
115
|
+
// Filter files by prefix
|
|
116
|
+
const matches = files.filter(f => f.startsWith(filePrefix));
|
|
117
|
+
|
|
118
|
+
// Build completion items
|
|
119
|
+
// For current directory (./) don't include the ./ in the result
|
|
120
|
+
const pathPrefix = dirPath === './' ? '' : dirPath;
|
|
121
|
+
|
|
122
|
+
return matches.map(f => ({
|
|
123
|
+
label: f,
|
|
124
|
+
value: `${prefix}${pathPrefix}${f}`,
|
|
125
|
+
description: '',
|
|
126
|
+
}));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Get path completions from sandbox (async)
|
|
131
|
+
* @param {Object} sessionManager - Session manager for sandbox communication
|
|
132
|
+
* @param {string} input - Current input buffer
|
|
133
|
+
* @returns {Promise<Array>} Completion items
|
|
134
|
+
*/
|
|
135
|
+
export async function getPathCompletions(sessionManager, input) {
|
|
136
|
+
if (hasDangerousShellChars(input)) {
|
|
137
|
+
return [];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const parsed = parsePathInput(input);
|
|
141
|
+
|
|
142
|
+
if (!parsed.needsCompletion) {
|
|
143
|
+
return [];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
const combined = `${parsed.dirPath || ''}${parsed.filePrefix || ''}`;
|
|
148
|
+
if (hasDangerousShellChars(combined)) {
|
|
149
|
+
return [];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Query sandbox for directory contents
|
|
153
|
+
const result = await sessionManager.execute(`ls -1Ap -- ${shellEscapePosix(parsed.dirPath)} 2>/dev/null`);
|
|
154
|
+
|
|
155
|
+
if (!result.output) {
|
|
156
|
+
return [];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Parse file list
|
|
160
|
+
const files = result.output.split(/[\r\n]+/).filter(f => f.trim()).slice(0, 200);
|
|
161
|
+
|
|
162
|
+
return buildCompletions(parsed, files);
|
|
163
|
+
} catch (e) {
|
|
164
|
+
// Silently fail - don't interrupt user input
|
|
165
|
+
return [];
|
|
166
|
+
}
|
|
167
|
+
}
|