rl-rockcli 0.0.9 → 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/package.json +1 -1
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { Show } from 'solid-js';
|
|
2
|
+
import { useTheme } from '../contexts/ThemeContext.tsx';
|
|
3
|
+
import type { OutputItem } from '../contexts/ReplContext.tsx';
|
|
4
|
+
|
|
5
|
+
const MAX_OUTPUT_LINES = 50;
|
|
6
|
+
|
|
7
|
+
function formatTime(ts: number): string {
|
|
8
|
+
const d = new Date(ts);
|
|
9
|
+
const hh = String(d.getHours()).padStart(2, '0');
|
|
10
|
+
const mm = String(d.getMinutes()).padStart(2, '0');
|
|
11
|
+
const ss = String(d.getSeconds()).padStart(2, '0');
|
|
12
|
+
return `${hh}:${mm}:${ss}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function truncateOutput(output: string, maxLines: number): { lines: string[]; truncated: number } {
|
|
16
|
+
if (!output) return { lines: [], truncated: 0 };
|
|
17
|
+
const allLines = output.split('\n');
|
|
18
|
+
if (allLines.length <= maxLines) {
|
|
19
|
+
return { lines: allLines, truncated: 0 };
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
lines: allLines.slice(0, maxLines),
|
|
23
|
+
truncated: allLines.length - maxLines,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function OutputBlock(props: { item: OutputItem }) {
|
|
28
|
+
const theme = useTheme();
|
|
29
|
+
|
|
30
|
+
// 判断是否显示错误样式:exitCode 不为 0 且不为 null,或者 exitCode 为 null 但有 tips(超时/网络错误)
|
|
31
|
+
const showErrorStyle =
|
|
32
|
+
(props.item.exitCode !== 0 && props.item.exitCode !== null) ||
|
|
33
|
+
(props.item.exitCode === null && !!props.item.tips);
|
|
34
|
+
|
|
35
|
+
// 判断是否显示 tips:有 tips 且(exitCode 不为 0 或 exitCode 为 null)
|
|
36
|
+
const showTips = props.item.tips && (props.item.exitCode !== 0 || props.item.exitCode === null);
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<Show when={!props.item.isWelcome}>
|
|
40
|
+
<box
|
|
41
|
+
flexDirection="column"
|
|
42
|
+
marginLeft={2}
|
|
43
|
+
marginRight={2}
|
|
44
|
+
borderStyle="single"
|
|
45
|
+
border
|
|
46
|
+
borderColor={showErrorStyle ? theme.colors.danger : theme.colors.border}
|
|
47
|
+
paddingLeft={1}
|
|
48
|
+
paddingRight={1}
|
|
49
|
+
paddingTop={0}
|
|
50
|
+
paddingBottom={0}
|
|
51
|
+
>
|
|
52
|
+
{/* Command line */}
|
|
53
|
+
<text>
|
|
54
|
+
<span style={{ fg: theme.colors.textSecondary }}>[{formatTime(props.item.timestamp)}] </span>
|
|
55
|
+
<span style={{ fg: theme.colors.prompt }}>{props.item.prompt || '$ '}</span>
|
|
56
|
+
<span style={{ fg: theme.colors.textPrimary }}>{props.item.command}</span>
|
|
57
|
+
</text>
|
|
58
|
+
|
|
59
|
+
{/* Output - only this part is selectable */}
|
|
60
|
+
<Show when={props.item.output !== null && props.item.output !== undefined}>
|
|
61
|
+
{(() => {
|
|
62
|
+
const { lines, truncated } = truncateOutput(props.item.output, MAX_OUTPUT_LINES);
|
|
63
|
+
return (
|
|
64
|
+
<box flexDirection="column">
|
|
65
|
+
{/* Output content */}
|
|
66
|
+
<text>
|
|
67
|
+
<span
|
|
68
|
+
style={{
|
|
69
|
+
fg: showErrorStyle ? theme.colors.danger : theme.colors.textPrimary,
|
|
70
|
+
}}
|
|
71
|
+
>
|
|
72
|
+
{lines.join('\n')}
|
|
73
|
+
</span>
|
|
74
|
+
</text>
|
|
75
|
+
{/* Truncation hint - not selectable */}
|
|
76
|
+
<Show when={truncated > 0}>
|
|
77
|
+
<text>
|
|
78
|
+
<span style={{ fg: theme.colors.warning }}>
|
|
79
|
+
... {truncated} more lines (use Ctrl+V to view full output)
|
|
80
|
+
</span>
|
|
81
|
+
</text>
|
|
82
|
+
</Show>
|
|
83
|
+
|
|
84
|
+
{/* Meta info (exit code) - displayed in gray when command fails */}
|
|
85
|
+
<Show when={props.item.metaInfo && showErrorStyle}>
|
|
86
|
+
<text>
|
|
87
|
+
<span style={{ fg: theme.colors.textSecondary, dim: true }}>
|
|
88
|
+
{props.item.metaInfo}
|
|
89
|
+
</span>
|
|
90
|
+
</text>
|
|
91
|
+
</Show>
|
|
92
|
+
|
|
93
|
+
{/* Tips - helpful hints when command fails */}
|
|
94
|
+
<Show when={showTips}>
|
|
95
|
+
<text>
|
|
96
|
+
<span style={{ fg: theme.colors.warning }}>
|
|
97
|
+
💡 {props.item.tips}
|
|
98
|
+
</span>
|
|
99
|
+
</text>
|
|
100
|
+
</Show>
|
|
101
|
+
</box>
|
|
102
|
+
);
|
|
103
|
+
})()}
|
|
104
|
+
</Show>
|
|
105
|
+
</box>
|
|
106
|
+
</Show>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { Show, createMemo } from 'solid-js';
|
|
2
|
+
import { useTheme } from '../contexts/ThemeContext.tsx';
|
|
3
|
+
import { useRepl } from '../contexts/ReplContext.tsx';
|
|
4
|
+
import type { InputRenderable } from '@opentui/core';
|
|
5
|
+
|
|
6
|
+
// Import paramHint from ink-repl (shared logic)
|
|
7
|
+
import { getPlaceholderText } from '../../ink-repl/utils/paramHint.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Input area with separator lines (like Ink InputArea / Claude Code style)
|
|
11
|
+
*
|
|
12
|
+
* Layout:
|
|
13
|
+
* ────────────────────────────────
|
|
14
|
+
* prompt> user input here█ @<param>
|
|
15
|
+
* ────────────────────────────────
|
|
16
|
+
*
|
|
17
|
+
* Shows parameter hints for /upload and /download commands.
|
|
18
|
+
* Input is always visible even during execution (spinner is separate).
|
|
19
|
+
*/
|
|
20
|
+
export function PromptInput(props: {
|
|
21
|
+
onSubmit?: (value: string) => void;
|
|
22
|
+
onInputChange?: (value: string) => void;
|
|
23
|
+
onInputRef?: (ref: InputRenderable) => void;
|
|
24
|
+
}) {
|
|
25
|
+
const theme = useTheme();
|
|
26
|
+
const [state, setState] = useRepl();
|
|
27
|
+
let inputRef: InputRenderable | undefined;
|
|
28
|
+
|
|
29
|
+
function handleSubmit(value: string) {
|
|
30
|
+
const trimmed = value.trim();
|
|
31
|
+
if (!trimmed) return;
|
|
32
|
+
|
|
33
|
+
// Clear input
|
|
34
|
+
setState('buffer', '');
|
|
35
|
+
if (inputRef) {
|
|
36
|
+
inputRef.value = '';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (props.onSubmit) {
|
|
40
|
+
props.onSubmit(trimmed);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function handleInput(value: string) {
|
|
45
|
+
setState('buffer', value);
|
|
46
|
+
if (props.onInputChange) {
|
|
47
|
+
props.onInputChange(value);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Calculate parameter hint text for /upload and /download commands
|
|
52
|
+
// Use createMemo for reactivity - recalculates when state.buffer changes
|
|
53
|
+
const hintText = createMemo(() => getPlaceholderText(state.buffer, state.buffer.length));
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<box
|
|
57
|
+
flexShrink={0}
|
|
58
|
+
flexDirection="column"
|
|
59
|
+
marginLeft={2}
|
|
60
|
+
marginRight={2}
|
|
61
|
+
borderStyle="single"
|
|
62
|
+
border
|
|
63
|
+
borderColor="#4a90d9"
|
|
64
|
+
paddingLeft={1}
|
|
65
|
+
paddingRight={1}
|
|
66
|
+
paddingTop={1}
|
|
67
|
+
paddingBottom={1}
|
|
68
|
+
>
|
|
69
|
+
{/* Input line - always visible */}
|
|
70
|
+
<box flexDirection="row" width="100%" overflow="hidden" position="relative">
|
|
71
|
+
{/* Prompt label: never shrink so long input won't cover it */}
|
|
72
|
+
<box flexShrink={0}>
|
|
73
|
+
<text selectable={false}>
|
|
74
|
+
<span style={{ fg: theme.colors.accent, bold: true }}>{state.shellPrompt}</span>
|
|
75
|
+
</text>
|
|
76
|
+
</box>
|
|
77
|
+
{/* Input container */}
|
|
78
|
+
<box flexGrow={1} overflow="hidden" minWidth={0}>
|
|
79
|
+
<input
|
|
80
|
+
ref={(el) => {
|
|
81
|
+
inputRef = el;
|
|
82
|
+
if (props.onInputRef && el) {
|
|
83
|
+
props.onInputRef(el);
|
|
84
|
+
}
|
|
85
|
+
}}
|
|
86
|
+
width="100%"
|
|
87
|
+
focused={state.viewMode === 'repl' && !state.consoleVisible}
|
|
88
|
+
value={state.buffer}
|
|
89
|
+
placeholder="Enter command..."
|
|
90
|
+
textColor={theme.colors.textPrimary}
|
|
91
|
+
focusedTextColor={theme.colors.textPrimary}
|
|
92
|
+
backgroundColor={theme.colors.background}
|
|
93
|
+
focusedBackgroundColor={theme.colors.background}
|
|
94
|
+
onSubmit={handleSubmit}
|
|
95
|
+
onInput={handleInput}
|
|
96
|
+
/>
|
|
97
|
+
</box>
|
|
98
|
+
{/* Parameter hint - overlay positioned after input content */}
|
|
99
|
+
<Show when={hintText()}>
|
|
100
|
+
<box position="absolute" left={state.shellPrompt.length + state.buffer.length + 1}>
|
|
101
|
+
<text selectable={false}>
|
|
102
|
+
<span style={{ fg: theme.colors.textHint }}> {hintText()}</span>
|
|
103
|
+
</text>
|
|
104
|
+
</box>
|
|
105
|
+
</Show>
|
|
106
|
+
</box>
|
|
107
|
+
</box>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Show } from 'solid-js';
|
|
2
|
+
import { useTheme } from '../contexts/ThemeContext.tsx';
|
|
3
|
+
import { useSession } from '../contexts/SessionContext.tsx';
|
|
4
|
+
import { useRepl } from '../contexts/ReplContext.tsx';
|
|
5
|
+
|
|
6
|
+
function formatResources(resources: any): string {
|
|
7
|
+
if (!resources) return '';
|
|
8
|
+
const parts: string[] = [];
|
|
9
|
+
if (resources.cpu) parts.push(`CPU ${resources.cpu}`);
|
|
10
|
+
if (resources.load) parts.push(`LOAD ${resources.load}`);
|
|
11
|
+
if (resources.memory) parts.push(`MEM ${resources.memory.used}/${resources.memory.total}`);
|
|
12
|
+
if (resources.disk) parts.push(`DISK ${resources.disk.used}/${resources.disk.total}`);
|
|
13
|
+
return parts.join(' ');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function StatusBar() {
|
|
17
|
+
const theme = useTheme();
|
|
18
|
+
const session = useSession();
|
|
19
|
+
const [state] = useRepl();
|
|
20
|
+
|
|
21
|
+
function getKeyHints(): string {
|
|
22
|
+
if (state.isExecuting) return 'ctrl+c cancel';
|
|
23
|
+
if (state.menuVisible) return '↑↓ select enter confirm esc cancel';
|
|
24
|
+
return '/ commands ↑↓ history ctrl+c×2 exit';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<box flexShrink={0} flexDirection="column">
|
|
29
|
+
<Show when={state.exitPending}>
|
|
30
|
+
<box paddingLeft={2}>
|
|
31
|
+
<text selectable={false}>
|
|
32
|
+
<span style={{ fg: theme.colors.warning, bold: true }}>Press Ctrl+C again to exit</span>
|
|
33
|
+
</text>
|
|
34
|
+
</box>
|
|
35
|
+
</Show>
|
|
36
|
+
<box flexDirection="row" paddingLeft={2} paddingRight={2}>
|
|
37
|
+
{/* Left: sandbox info */}
|
|
38
|
+
<box flexGrow={1} flexBasis={0}>
|
|
39
|
+
<text selectable={false}>
|
|
40
|
+
<span style={{ fg: theme.colors.accent }}>● {session.sandboxId}</span>
|
|
41
|
+
<Show when={session.hostIp}>
|
|
42
|
+
<span style={{ fg: theme.colors.textSecondary }}> ({session.hostIp})</span>
|
|
43
|
+
</Show>
|
|
44
|
+
</text>
|
|
45
|
+
</box>
|
|
46
|
+
|
|
47
|
+
{/* Center: resources */}
|
|
48
|
+
<box flexGrow={1} flexBasis={0} justifyContent="center">
|
|
49
|
+
<text selectable={false}>
|
|
50
|
+
<span style={{ fg: theme.colors.warning }}>{formatResources(state.resources)}</span>
|
|
51
|
+
</text>
|
|
52
|
+
</box>
|
|
53
|
+
|
|
54
|
+
{/* Right: key hints */}
|
|
55
|
+
<box flexGrow={1} flexBasis={0} alignItems="flex-end">
|
|
56
|
+
<text selectable={false}>
|
|
57
|
+
<span style={{ fg: theme.colors.textSecondary }}>{getKeyHints()}</span>
|
|
58
|
+
</text>
|
|
59
|
+
</box>
|
|
60
|
+
</box>
|
|
61
|
+
</box>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { Show } from 'solid-js';
|
|
2
|
+
import { useTerminalDimensions } from '@opentui/solid';
|
|
3
|
+
import { useTheme } from '../contexts/ThemeContext.tsx';
|
|
4
|
+
import { useToast } from '../contexts/ToastContext.tsx';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Variant-to-theme-color mapping.
|
|
8
|
+
*/
|
|
9
|
+
const VARIANT_COLOR_MAP = {
|
|
10
|
+
info: 'accent',
|
|
11
|
+
success: 'success',
|
|
12
|
+
warning: 'warning',
|
|
13
|
+
error: 'danger',
|
|
14
|
+
} as const;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Floating toast notification component.
|
|
18
|
+
*
|
|
19
|
+
* Renders in the top-right corner of the terminal as an
|
|
20
|
+
* absolute-positioned overlay. Auto-dismisses based on the
|
|
21
|
+
* duration set by ToastContext.
|
|
22
|
+
*/
|
|
23
|
+
export function Toast() {
|
|
24
|
+
const toast = useToast();
|
|
25
|
+
const theme = useTheme();
|
|
26
|
+
const dimensions = useTerminalDimensions();
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<Show when={toast.currentToast}>
|
|
30
|
+
{(current) => {
|
|
31
|
+
const borderColor = () => {
|
|
32
|
+
const colorKey = VARIANT_COLOR_MAP[current().variant];
|
|
33
|
+
return theme.colors[colorKey];
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<box
|
|
38
|
+
position="absolute"
|
|
39
|
+
justifyContent="center"
|
|
40
|
+
alignItems="flex-start"
|
|
41
|
+
top={2}
|
|
42
|
+
right={2}
|
|
43
|
+
maxWidth={Math.min(50, dimensions().width - 6)}
|
|
44
|
+
paddingLeft={2}
|
|
45
|
+
paddingRight={2}
|
|
46
|
+
paddingTop={1}
|
|
47
|
+
paddingBottom={1}
|
|
48
|
+
backgroundColor={theme.colors.surface}
|
|
49
|
+
borderColor={borderColor()}
|
|
50
|
+
border={['left', 'right']}
|
|
51
|
+
>
|
|
52
|
+
<Show when={current().title}>
|
|
53
|
+
<text marginBottom={1} fg={theme.colors.textPrimary}>
|
|
54
|
+
<span style={{ bold: true }}>{current().title}</span>
|
|
55
|
+
</text>
|
|
56
|
+
</Show>
|
|
57
|
+
<text fg={theme.colors.textPrimary} wrapMode="word" width="100%">
|
|
58
|
+
{current().message}
|
|
59
|
+
</text>
|
|
60
|
+
</box>
|
|
61
|
+
);
|
|
62
|
+
}}
|
|
63
|
+
</Show>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { For } from 'solid-js';
|
|
2
|
+
import { useTheme } from '../contexts/ThemeContext.tsx';
|
|
3
|
+
import i18n from '../../../../utils/i18n.js';
|
|
4
|
+
import { getLogoLines, getGradientColors } from '../../../../utils/asciiArt.js';
|
|
5
|
+
const { t } = i18n;
|
|
6
|
+
|
|
7
|
+
export function WelcomeBanner() {
|
|
8
|
+
const theme = useTheme();
|
|
9
|
+
const gradient = getGradientColors();
|
|
10
|
+
const ASCII_LINES = getLogoLines();
|
|
11
|
+
|
|
12
|
+
const TIPS = [
|
|
13
|
+
t('welcome.tip.commands'),
|
|
14
|
+
t('welcome.tip.execute'),
|
|
15
|
+
t('welcome.tip.console'),
|
|
16
|
+
t('welcome.tip.exit'),
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<box flexDirection="column" alignItems="center" paddingTop={1} paddingBottom={1} width="100%">
|
|
21
|
+
<box flexDirection="column" alignItems="center">
|
|
22
|
+
<For each={ASCII_LINES}>
|
|
23
|
+
{(line, i) => (
|
|
24
|
+
<text selectable={false}>
|
|
25
|
+
<span style={{ fg: gradient[i() % gradient.length], bold: true }}>{line}</span>
|
|
26
|
+
</text>
|
|
27
|
+
)}
|
|
28
|
+
</For>
|
|
29
|
+
</box>
|
|
30
|
+
<box marginTop={1} flexDirection="column">
|
|
31
|
+
<For each={TIPS}>
|
|
32
|
+
{(tip) => (
|
|
33
|
+
<text selectable={false}>
|
|
34
|
+
<span style={{ fg: theme.colors.textSecondary }}>{tip}</span>
|
|
35
|
+
</text>
|
|
36
|
+
)}
|
|
37
|
+
</For>
|
|
38
|
+
</box>
|
|
39
|
+
</box>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { createContext, useContext } from 'solid-js';
|
|
2
|
+
import { createStore, type SetStoreFunction } from 'solid-js/store';
|
|
3
|
+
|
|
4
|
+
export interface OutputItem {
|
|
5
|
+
id: string;
|
|
6
|
+
command: string;
|
|
7
|
+
output: string;
|
|
8
|
+
exitCode: number | null;
|
|
9
|
+
timestamp: number;
|
|
10
|
+
prompt: string;
|
|
11
|
+
isWelcome?: boolean;
|
|
12
|
+
metaInfo?: string | null;
|
|
13
|
+
tips?: string | null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ConsoleLog {
|
|
17
|
+
id: string;
|
|
18
|
+
timestamp: string;
|
|
19
|
+
level: 'info' | 'warn' | 'error' | 'debug';
|
|
20
|
+
message: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ReplState {
|
|
24
|
+
outputs: OutputItem[];
|
|
25
|
+
buffer: string;
|
|
26
|
+
isExecuting: boolean;
|
|
27
|
+
executingCommand: string;
|
|
28
|
+
lastExecutionDuration: number | null;
|
|
29
|
+
executionStartTime: number | null;
|
|
30
|
+
historyIndex: number;
|
|
31
|
+
commandHistory: string[];
|
|
32
|
+
savedBuffer: string;
|
|
33
|
+
shellPrompt: string;
|
|
34
|
+
exitPending: boolean;
|
|
35
|
+
resources: {
|
|
36
|
+
cpu: string;
|
|
37
|
+
load: string;
|
|
38
|
+
memory: { used: string; total: string };
|
|
39
|
+
disk: { used: string; total: string };
|
|
40
|
+
} | null;
|
|
41
|
+
menuVisible: boolean;
|
|
42
|
+
menuItems: { label: string; value: string; description: string }[];
|
|
43
|
+
selectedIndex: number;
|
|
44
|
+
menuType: 'slash' | 'path' | 'at' | null;
|
|
45
|
+
atContext: any;
|
|
46
|
+
viewMode: 'repl' | 'detail';
|
|
47
|
+
detailContent: string | null;
|
|
48
|
+
consoleVisible: boolean;
|
|
49
|
+
consoleLogs: ConsoleLog[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let _nextId = 0;
|
|
53
|
+
function nextId(): string {
|
|
54
|
+
return `out-${++_nextId}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function createInitialState(shellPrompt: string): ReplState {
|
|
58
|
+
return {
|
|
59
|
+
outputs: [
|
|
60
|
+
{
|
|
61
|
+
id: nextId(),
|
|
62
|
+
command: '',
|
|
63
|
+
output: '',
|
|
64
|
+
exitCode: 0,
|
|
65
|
+
timestamp: Date.now(),
|
|
66
|
+
prompt: shellPrompt,
|
|
67
|
+
isWelcome: true,
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
buffer: '',
|
|
71
|
+
isExecuting: false,
|
|
72
|
+
executingCommand: '',
|
|
73
|
+
lastExecutionDuration: null,
|
|
74
|
+
executionStartTime: null,
|
|
75
|
+
historyIndex: -1,
|
|
76
|
+
commandHistory: [],
|
|
77
|
+
savedBuffer: '',
|
|
78
|
+
shellPrompt,
|
|
79
|
+
exitPending: false,
|
|
80
|
+
resources: null,
|
|
81
|
+
menuVisible: false,
|
|
82
|
+
menuItems: [],
|
|
83
|
+
selectedIndex: 0,
|
|
84
|
+
menuType: null,
|
|
85
|
+
atContext: null,
|
|
86
|
+
viewMode: 'repl',
|
|
87
|
+
detailContent: null,
|
|
88
|
+
consoleVisible: false,
|
|
89
|
+
consoleLogs: [],
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Add a log entry to the console
|
|
95
|
+
*/
|
|
96
|
+
function createAddLog(setState: SetStoreFunction<ReplState>) {
|
|
97
|
+
return (level: ConsoleLog['level'], message: string) => {
|
|
98
|
+
const timestamp = new Date().toTimeString().slice(0, 8);
|
|
99
|
+
const newLog: ConsoleLog = {
|
|
100
|
+
id: `log-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
101
|
+
timestamp,
|
|
102
|
+
level,
|
|
103
|
+
message,
|
|
104
|
+
};
|
|
105
|
+
setState('consoleLogs', (logs) => [...logs, newLog]);
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface ReplActions {
|
|
110
|
+
nextId: () => string;
|
|
111
|
+
addLog: (level: ConsoleLog['level'], message: string) => void;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
type ReplContextValue = [ReplState, SetStoreFunction<ReplState>, ReplActions];
|
|
115
|
+
|
|
116
|
+
const ReplContext = createContext<ReplContextValue>();
|
|
117
|
+
|
|
118
|
+
export function ReplProvider(props: { shellPrompt: string; children: any }) {
|
|
119
|
+
const [state, setState] = createStore<ReplState>(createInitialState(props.shellPrompt));
|
|
120
|
+
|
|
121
|
+
const actions: ReplActions = {
|
|
122
|
+
nextId,
|
|
123
|
+
addLog: createAddLog(setState),
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<ReplContext.Provider value={[state, setState, actions]}>
|
|
128
|
+
{props.children}
|
|
129
|
+
</ReplContext.Provider>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function useRepl(): ReplContextValue {
|
|
134
|
+
const ctx = useContext(ReplContext);
|
|
135
|
+
if (!ctx) throw new Error('useRepl must be used within ReplProvider');
|
|
136
|
+
return ctx;
|
|
137
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { createContext, useContext } from 'solid-js';
|
|
2
|
+
|
|
3
|
+
export interface SessionInfo {
|
|
4
|
+
sandboxId: string;
|
|
5
|
+
hostname: string;
|
|
6
|
+
hostIp: string | null;
|
|
7
|
+
user: string;
|
|
8
|
+
version: string;
|
|
9
|
+
client: any;
|
|
10
|
+
sessionManager: any;
|
|
11
|
+
historyManager: any;
|
|
12
|
+
initialPrompt: string;
|
|
13
|
+
triggers: any[];
|
|
14
|
+
onExit?: () => void;
|
|
15
|
+
onThemeChange?: (name: string) => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const SessionContext = createContext<SessionInfo>();
|
|
19
|
+
|
|
20
|
+
export function SessionProvider(props: { session: SessionInfo; children: any }) {
|
|
21
|
+
return (
|
|
22
|
+
<SessionContext.Provider value={props.session}>
|
|
23
|
+
{props.children}
|
|
24
|
+
</SessionContext.Provider>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function useSession(): SessionInfo {
|
|
29
|
+
const ctx = useContext(SessionContext);
|
|
30
|
+
if (!ctx) throw new Error('useSession must be used within SessionProvider');
|
|
31
|
+
return ctx;
|
|
32
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { createContext, useContext } from 'solid-js';
|
|
2
|
+
|
|
3
|
+
const defaultGradient = ['#2563EB', '#3B82F6', '#4F46E5', '#6366F1', '#7C3AED', '#8B5CF6'];
|
|
4
|
+
|
|
5
|
+
export const themes = {
|
|
6
|
+
'rock-dark': {
|
|
7
|
+
name: 'rock-dark',
|
|
8
|
+
type: 'dark',
|
|
9
|
+
colors: {
|
|
10
|
+
background: '#000000',
|
|
11
|
+
surface: '#181825',
|
|
12
|
+
surfaceHighlight: '#11111b',
|
|
13
|
+
border: '#313244',
|
|
14
|
+
textPrimary: '#cdd6f4',
|
|
15
|
+
textSecondary: '#a6adc8',
|
|
16
|
+
textHint: '#6c7086',
|
|
17
|
+
accent: '#89b4fa',
|
|
18
|
+
accentMuted: '#b4befe',
|
|
19
|
+
warning: '#f9e2af',
|
|
20
|
+
success: '#a6e3a1',
|
|
21
|
+
danger: '#f38ba8',
|
|
22
|
+
prompt: '#89dceb',
|
|
23
|
+
cursor: '#cdd6f4',
|
|
24
|
+
gradient: defaultGradient,
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
'rock-light': {
|
|
28
|
+
name: 'rock-light',
|
|
29
|
+
type: 'light',
|
|
30
|
+
colors: {
|
|
31
|
+
background: '#eff1f5',
|
|
32
|
+
surface: '#e6e9ef',
|
|
33
|
+
surfaceHighlight: '#dce0e8',
|
|
34
|
+
border: '#9ca0b0',
|
|
35
|
+
textPrimary: '#4c4f69',
|
|
36
|
+
textSecondary: '#6c6f85',
|
|
37
|
+
textHint: '#9ca0b0',
|
|
38
|
+
accent: '#1e66f5',
|
|
39
|
+
accentMuted: '#7287fd',
|
|
40
|
+
warning: '#df8e1d',
|
|
41
|
+
success: '#40a02b',
|
|
42
|
+
danger: '#d20f39',
|
|
43
|
+
prompt: '#04a5e5',
|
|
44
|
+
cursor: '#4c4f69',
|
|
45
|
+
gradient: defaultGradient,
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export type ThemeColors = typeof themes['rock-dark']['colors'];
|
|
51
|
+
export type Theme = typeof themes['rock-dark'];
|
|
52
|
+
|
|
53
|
+
const ThemeContext = createContext<Theme>(themes['rock-dark']);
|
|
54
|
+
|
|
55
|
+
export function ThemeProvider(props: { theme?: string; children: any }) {
|
|
56
|
+
const themeName = props.theme && themes[props.theme as keyof typeof themes]
|
|
57
|
+
? (props.theme as keyof typeof themes)
|
|
58
|
+
: 'rock-dark';
|
|
59
|
+
const theme = themes[themeName];
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<ThemeContext.Provider value={theme}>
|
|
63
|
+
{props.children}
|
|
64
|
+
</ThemeContext.Provider>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function useTheme(): Theme {
|
|
69
|
+
return useContext(ThemeContext);
|
|
70
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { createContext, useContext, type ParentProps } from 'solid-js';
|
|
2
|
+
import { createStore } from 'solid-js/store';
|
|
3
|
+
|
|
4
|
+
export interface ToastOptions {
|
|
5
|
+
title?: string;
|
|
6
|
+
message: string;
|
|
7
|
+
variant: 'info' | 'success' | 'warning' | 'error';
|
|
8
|
+
duration?: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type ToastData = Omit<ToastOptions, 'duration'>;
|
|
12
|
+
|
|
13
|
+
const DEFAULT_DURATION = 3000;
|
|
14
|
+
|
|
15
|
+
function createToastState() {
|
|
16
|
+
const [store, setStore] = createStore({
|
|
17
|
+
currentToast: null as ToastData | null,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
let timeoutHandle: ReturnType<typeof setTimeout> | null = null;
|
|
21
|
+
|
|
22
|
+
const toast = {
|
|
23
|
+
show(options: ToastOptions) {
|
|
24
|
+
const { duration, ...toastData } = options;
|
|
25
|
+
const dismissAfter = duration != null ? duration : DEFAULT_DURATION;
|
|
26
|
+
|
|
27
|
+
setStore('currentToast', toastData);
|
|
28
|
+
|
|
29
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
30
|
+
timeoutHandle = setTimeout(() => {
|
|
31
|
+
setStore('currentToast', null);
|
|
32
|
+
timeoutHandle = null;
|
|
33
|
+
}, dismissAfter);
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
error(err: unknown) {
|
|
37
|
+
if (err instanceof Error) {
|
|
38
|
+
toast.show({ variant: 'error', message: err.message });
|
|
39
|
+
} else if (typeof err === 'string') {
|
|
40
|
+
toast.show({ variant: 'error', message: err });
|
|
41
|
+
} else {
|
|
42
|
+
toast.show({ variant: 'error', message: 'An unknown error occurred' });
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
get currentToast(): ToastData | null {
|
|
47
|
+
return store.currentToast;
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
return toast;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type ToastContext = ReturnType<typeof createToastState>;
|
|
55
|
+
|
|
56
|
+
const ToastCtx = createContext<ToastContext>();
|
|
57
|
+
|
|
58
|
+
export function ToastProvider(props: ParentProps) {
|
|
59
|
+
const value = createToastState();
|
|
60
|
+
return <ToastCtx.Provider value={value}>{props.children}</ToastCtx.Provider>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function useToast(): ToastContext {
|
|
64
|
+
const value = useContext(ToastCtx);
|
|
65
|
+
if (!value) {
|
|
66
|
+
throw new Error('useToast must be used within a ToastProvider');
|
|
67
|
+
}
|
|
68
|
+
return value;
|
|
69
|
+
}
|