rl-rockcli 0.0.9 → 0.0.11
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/attach.js +186 -0
- package/package.json +1 -1
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapter for ink-repl/builtinCommands.js
|
|
3
|
+
*
|
|
4
|
+
* Reuses the existing command handlers (which are UI-agnostic)
|
|
5
|
+
* and wires them into the OpenTUI REPL's state management.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createRequire } from 'module';
|
|
9
|
+
|
|
10
|
+
// Use sync require for reliable path resolution across TypeScript runtimes
|
|
11
|
+
let _builtinModule: any = null;
|
|
12
|
+
|
|
13
|
+
function getBuiltinModule() {
|
|
14
|
+
if (!_builtinModule) {
|
|
15
|
+
try {
|
|
16
|
+
const require = createRequire(import.meta.url);
|
|
17
|
+
_builtinModule = require('../ink-repl/builtinCommands.js');
|
|
18
|
+
} catch (err) {
|
|
19
|
+
console.error('[builtinCommands] Failed to load ink-repl/builtinCommands.js:', err);
|
|
20
|
+
throw err;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return _builtinModule;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function isBuiltinCommand(input: string): Promise<boolean> {
|
|
27
|
+
const mod = getBuiltinModule();
|
|
28
|
+
return mod.isBuiltinCommand(input);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ConfirmOption {
|
|
32
|
+
label: string;
|
|
33
|
+
value: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ConfirmData {
|
|
37
|
+
title: string;
|
|
38
|
+
message: string;
|
|
39
|
+
items?: string[];
|
|
40
|
+
moreCount?: number;
|
|
41
|
+
options: ConfirmOption[];
|
|
42
|
+
onConfirm: (confirmed: boolean) => Promise<{ output: string; exitCode: number }>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface BuiltinResult {
|
|
46
|
+
output?: string;
|
|
47
|
+
exitCode?: number;
|
|
48
|
+
action?: 'exit' | 'clear' | 'retry' | 'confirm';
|
|
49
|
+
command?: string;
|
|
50
|
+
confirmData?: ConfirmData;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface BuiltinContext {
|
|
54
|
+
client: any;
|
|
55
|
+
sessionManager: any;
|
|
56
|
+
historyManager: any;
|
|
57
|
+
sandboxId: string;
|
|
58
|
+
version: string;
|
|
59
|
+
stats: { startTime: number; shellCommands: number; builtinCommands: number };
|
|
60
|
+
lastCommand: string;
|
|
61
|
+
lastOutput: string;
|
|
62
|
+
themeManager?: any;
|
|
63
|
+
setTheme?: (name: string) => void;
|
|
64
|
+
setUIConfig?: (key: string, value: any) => void;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function executeBuiltinCommand(
|
|
68
|
+
input: string,
|
|
69
|
+
context: BuiltinContext
|
|
70
|
+
): Promise<BuiltinResult> {
|
|
71
|
+
const mod = getBuiltinModule();
|
|
72
|
+
return mod.executeBuiltinCommand(input, context);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function checkInteractiveCommand(
|
|
76
|
+
command: string
|
|
77
|
+
): Promise<{ isInteractive: boolean; cmdName: string; alternative: string | null }> {
|
|
78
|
+
const mod = getBuiltinModule();
|
|
79
|
+
return mod.checkInteractiveCommand(command);
|
|
80
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { Show, For, createMemo } from 'solid-js';
|
|
2
|
+
import { useTheme } from '../contexts/ThemeContext.tsx';
|
|
3
|
+
import { useRepl, useReplSetters } from '../contexts/ReplContext.tsx';
|
|
4
|
+
|
|
5
|
+
export interface ConfirmOption {
|
|
6
|
+
label: string;
|
|
7
|
+
value: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ConfirmData {
|
|
11
|
+
title: string;
|
|
12
|
+
message: string;
|
|
13
|
+
items?: string[];
|
|
14
|
+
moreCount?: number;
|
|
15
|
+
options: ConfirmOption[];
|
|
16
|
+
onConfirm: (confirmed: boolean) => Promise<{ output: string; exitCode: number }>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function ConfirmDialog() {
|
|
20
|
+
const theme = useTheme();
|
|
21
|
+
const [state] = useRepl();
|
|
22
|
+
const setState = useReplSetters();
|
|
23
|
+
|
|
24
|
+
const confirmData = () => state.confirmData as ConfirmData | undefined;
|
|
25
|
+
const selectedIndex = () => state.confirmSelectedIndex || 0;
|
|
26
|
+
|
|
27
|
+
const hasItems = () => confirmData()?.items && confirmData()!.items!.length > 0;
|
|
28
|
+
const showMoreCount = () => (confirmData()?.moreCount || 0) > 0;
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<Show when={confirmData()}>
|
|
32
|
+
<box
|
|
33
|
+
flexDirection="column"
|
|
34
|
+
flexGrow={1}
|
|
35
|
+
backgroundColor={theme.colors.background}
|
|
36
|
+
border={{ type: 'line', fg: theme.colors.border }}
|
|
37
|
+
paddingX={2}
|
|
38
|
+
paddingY={1}
|
|
39
|
+
>
|
|
40
|
+
{/* Title */}
|
|
41
|
+
<text>
|
|
42
|
+
<span style={{ fg: theme.colors.accent, bold: true }}>
|
|
43
|
+
{confirmData()!.title}
|
|
44
|
+
</span>
|
|
45
|
+
</text>
|
|
46
|
+
|
|
47
|
+
{/* Message */}
|
|
48
|
+
<text marginTop={1}>
|
|
49
|
+
<span style={{ fg: theme.colors.textPrimary }}>
|
|
50
|
+
{confirmData()!.message}
|
|
51
|
+
</span>
|
|
52
|
+
</text>
|
|
53
|
+
|
|
54
|
+
{/* Items list */}
|
|
55
|
+
<Show when={hasItems()}>
|
|
56
|
+
<box marginTop={1} flexDirection="column">
|
|
57
|
+
<text marginBottom={1}>
|
|
58
|
+
<span style={{ fg: theme.colors.textSecondary }}>
|
|
59
|
+
{'─'.repeat(50)}
|
|
60
|
+
</span>
|
|
61
|
+
</text>
|
|
62
|
+
<For each={confirmData()!.items}>
|
|
63
|
+
{(item) => (
|
|
64
|
+
<text>
|
|
65
|
+
<span style={{ fg: theme.colors.textPrimary }}>
|
|
66
|
+
{' '}{item}
|
|
67
|
+
</span>
|
|
68
|
+
</text>
|
|
69
|
+
)}
|
|
70
|
+
</For>
|
|
71
|
+
<Show when={showMoreCount()}>
|
|
72
|
+
<text>
|
|
73
|
+
<span style={{ fg: theme.colors.textSecondary }}>
|
|
74
|
+
{' '}and {confirmData()!.moreCount} more sessions not shown
|
|
75
|
+
</span>
|
|
76
|
+
</text>
|
|
77
|
+
</Show>
|
|
78
|
+
<text marginTop={1}>
|
|
79
|
+
<span style={{ fg: theme.colors.textSecondary }}>
|
|
80
|
+
{'─'.repeat(50)}
|
|
81
|
+
</span>
|
|
82
|
+
</text>
|
|
83
|
+
</box>
|
|
84
|
+
</Show>
|
|
85
|
+
|
|
86
|
+
{/* Options */}
|
|
87
|
+
<box marginTop={1} flexDirection="column">
|
|
88
|
+
<For each={confirmData()!.options}>
|
|
89
|
+
{(option, i) => {
|
|
90
|
+
const isSelected = () => i() === selectedIndex();
|
|
91
|
+
return (
|
|
92
|
+
<text>
|
|
93
|
+
<span
|
|
94
|
+
style={{
|
|
95
|
+
fg: isSelected() ? theme.colors.accent : theme.colors.textPrimary,
|
|
96
|
+
bold: isSelected(),
|
|
97
|
+
}}
|
|
98
|
+
>
|
|
99
|
+
{isSelected() ? '● ' : '○ '}{option.label}
|
|
100
|
+
</span>
|
|
101
|
+
</text>
|
|
102
|
+
);
|
|
103
|
+
}}
|
|
104
|
+
</For>
|
|
105
|
+
</box>
|
|
106
|
+
|
|
107
|
+
{/* Help text */}
|
|
108
|
+
<text marginTop={1}>
|
|
109
|
+
<span style={{ fg: theme.colors.textSecondary }}>
|
|
110
|
+
↑/↓ Select Enter Confirm Esc Cancel
|
|
111
|
+
</span>
|
|
112
|
+
</text>
|
|
113
|
+
</box>
|
|
114
|
+
</Show>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { Show, createSignal, onCleanup } from 'solid-js';
|
|
2
|
+
import { useTheme } from '../contexts/ThemeContext';
|
|
3
|
+
import i18n from '../../../../utils/i18n.js';
|
|
4
|
+
import { getLogoLines, getGradientColors } from '../../../../utils/asciiArt.js';
|
|
5
|
+
const { t } = i18n;
|
|
6
|
+
|
|
7
|
+
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
8
|
+
|
|
9
|
+
interface ConnectingScreenProps {
|
|
10
|
+
sandboxId: string;
|
|
11
|
+
attempt?: number;
|
|
12
|
+
maxAttempts?: number;
|
|
13
|
+
error?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function ConnectingScreen(props: ConnectingScreenProps) {
|
|
17
|
+
const theme = useTheme();
|
|
18
|
+
const [spinnerIndex, setSpinnerIndex] = createSignal(0);
|
|
19
|
+
const gradient = getGradientColors();
|
|
20
|
+
const ROCK_ASCII_LINES = getLogoLines();
|
|
21
|
+
|
|
22
|
+
const timer = setInterval(() => {
|
|
23
|
+
setSpinnerIndex((i) => (i + 1) % SPINNER_FRAMES.length);
|
|
24
|
+
}, 80);
|
|
25
|
+
|
|
26
|
+
onCleanup(() => clearInterval(timer));
|
|
27
|
+
|
|
28
|
+
const connectingText = () => t('connecting.message');
|
|
29
|
+
const attemptText = () => {
|
|
30
|
+
if ((props.attempt ?? 1) > 1) {
|
|
31
|
+
return t('connecting.attempt')
|
|
32
|
+
.replace('${attempt}', String(props.attempt))
|
|
33
|
+
.replace('${maxAttempts}', String(props.maxAttempts ?? 30));
|
|
34
|
+
}
|
|
35
|
+
return '';
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// If error is provided, show error screen
|
|
39
|
+
if (props.error) {
|
|
40
|
+
return (
|
|
41
|
+
<box
|
|
42
|
+
width="100%"
|
|
43
|
+
height="100%"
|
|
44
|
+
flexDirection="column"
|
|
45
|
+
justifyContent="center"
|
|
46
|
+
alignItems="center"
|
|
47
|
+
backgroundColor={theme.colors.background}
|
|
48
|
+
>
|
|
49
|
+
{/* ASCII Art */}
|
|
50
|
+
<box flexDirection="column" alignItems="center">
|
|
51
|
+
{ROCK_ASCII_LINES.map((line, index) => (
|
|
52
|
+
<text selectable={false}>
|
|
53
|
+
<span style={{ fg: gradient[index % gradient.length], bold: true }}>{line}</span>
|
|
54
|
+
</text>
|
|
55
|
+
))}
|
|
56
|
+
</box>
|
|
57
|
+
|
|
58
|
+
{/* Error message */}
|
|
59
|
+
<box marginTop={2}>
|
|
60
|
+
<text selectable={false}>
|
|
61
|
+
<span style={{ fg: theme.colors.danger }}>❌ {props.error}</span>
|
|
62
|
+
</text>
|
|
63
|
+
</box>
|
|
64
|
+
|
|
65
|
+
{/* Sandbox ID */}
|
|
66
|
+
<box marginTop={1}>
|
|
67
|
+
<text selectable={false}>
|
|
68
|
+
<span style={{ fg: theme.colors.textSecondary }}>{props.sandboxId}</span>
|
|
69
|
+
</text>
|
|
70
|
+
</box>
|
|
71
|
+
|
|
72
|
+
{/* Exit hint */}
|
|
73
|
+
<box marginTop={2}>
|
|
74
|
+
<text selectable={false}>
|
|
75
|
+
<span style={{ fg: theme.colors.textSecondary }}>Exiting...</span>
|
|
76
|
+
</text>
|
|
77
|
+
</box>
|
|
78
|
+
</box>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<box
|
|
84
|
+
width="100%"
|
|
85
|
+
height="100%"
|
|
86
|
+
flexDirection="column"
|
|
87
|
+
justifyContent="center"
|
|
88
|
+
alignItems="center"
|
|
89
|
+
backgroundColor={theme.colors.background}
|
|
90
|
+
>
|
|
91
|
+
{/* ASCII Art */}
|
|
92
|
+
<box flexDirection="column" alignItems="center">
|
|
93
|
+
{ROCK_ASCII_LINES.map((line, index) => (
|
|
94
|
+
<text selectable={false}>
|
|
95
|
+
<span style={{ fg: gradient[index % gradient.length], bold: true }}>{line}</span>
|
|
96
|
+
</text>
|
|
97
|
+
))}
|
|
98
|
+
</box>
|
|
99
|
+
|
|
100
|
+
{/* Connecting message */}
|
|
101
|
+
<box marginTop={2}>
|
|
102
|
+
<text selectable={false}>
|
|
103
|
+
<span style={{ fg: theme.colors.accent }}>{connectingText()}</span>
|
|
104
|
+
</text>
|
|
105
|
+
</box>
|
|
106
|
+
|
|
107
|
+
{/* Sandbox ID */}
|
|
108
|
+
<box marginTop={1}>
|
|
109
|
+
<text selectable={false}>
|
|
110
|
+
<span style={{ fg: theme.colors.textSecondary }}>{props.sandboxId}</span>
|
|
111
|
+
</text>
|
|
112
|
+
</box>
|
|
113
|
+
|
|
114
|
+
{/* Spinner */}
|
|
115
|
+
<box marginTop={2}>
|
|
116
|
+
<text selectable={false}>
|
|
117
|
+
<span style={{ fg: theme.colors.accent }}>{SPINNER_FRAMES[spinnerIndex()]} Loading...</span>
|
|
118
|
+
</text>
|
|
119
|
+
</box>
|
|
120
|
+
|
|
121
|
+
{/* Attempt counter (only show if attempt > 1) */}
|
|
122
|
+
<Show when={(props.attempt ?? 1) > 1}>
|
|
123
|
+
<box marginTop={1}>
|
|
124
|
+
<text selectable={false}>
|
|
125
|
+
<span style={{ fg: theme.colors.textSecondary }}>{attemptText()}</span>
|
|
126
|
+
</text>
|
|
127
|
+
</box>
|
|
128
|
+
</Show>
|
|
129
|
+
</box>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Show, For } from 'solid-js';
|
|
2
|
+
import { useTheme } from '../contexts/ThemeContext.tsx';
|
|
3
|
+
import { useRepl } from '../contexts/ReplContext.tsx';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Get color based on log level
|
|
7
|
+
*/
|
|
8
|
+
function getLogColor(level: string, theme: any): string {
|
|
9
|
+
switch (level) {
|
|
10
|
+
case 'error': return theme.colors.danger;
|
|
11
|
+
case 'warn': return theme.colors.warning;
|
|
12
|
+
case 'info': return theme.colors.accent;
|
|
13
|
+
case 'debug': return theme.colors.textSecondary;
|
|
14
|
+
default: return theme.colors.textPrimary;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function Console() {
|
|
19
|
+
const theme = useTheme();
|
|
20
|
+
const [state] = useRepl();
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<Show when={state.consoleVisible}>
|
|
24
|
+
<box
|
|
25
|
+
flexDirection="column"
|
|
26
|
+
height={10}
|
|
27
|
+
minHeight={5}
|
|
28
|
+
borderStyle="rounded"
|
|
29
|
+
border
|
|
30
|
+
borderColor={theme.colors.border}
|
|
31
|
+
backgroundColor={theme.colors.surface}
|
|
32
|
+
paddingLeft={1}
|
|
33
|
+
paddingRight={1}
|
|
34
|
+
>
|
|
35
|
+
{/* Header */}
|
|
36
|
+
<box justifyContent="flex-start">
|
|
37
|
+
<text selectable={false}>
|
|
38
|
+
<span style={{ fg: theme.colors.accent, bold: true }}>CONSOLE</span>
|
|
39
|
+
<span style={{ fg: theme.colors.textSecondary }}>
|
|
40
|
+
{' '}{state.consoleLogs.length} logs F12 close
|
|
41
|
+
</span>
|
|
42
|
+
</text>
|
|
43
|
+
</box>
|
|
44
|
+
|
|
45
|
+
{/* Log entries */}
|
|
46
|
+
<scrollbox
|
|
47
|
+
flexGrow={1}
|
|
48
|
+
scrollY
|
|
49
|
+
stickyScroll
|
|
50
|
+
stickyStart="bottom"
|
|
51
|
+
>
|
|
52
|
+
<box flexDirection="column">
|
|
53
|
+
<Show when={state.consoleLogs.length === 0}>
|
|
54
|
+
<text selectable={false}>
|
|
55
|
+
<span style={{ fg: theme.colors.textSecondary }}>No logs yet</span>
|
|
56
|
+
</text>
|
|
57
|
+
</Show>
|
|
58
|
+
<For each={state.consoleLogs}>
|
|
59
|
+
{(log: any) => (
|
|
60
|
+
<text selectable={true}>
|
|
61
|
+
<span style={{ fg: theme.colors.textSecondary }}>[{log.timestamp}]</span>
|
|
62
|
+
<span style={{ fg: getLogColor(log.level, theme), bold: log.level === 'error' }}>
|
|
63
|
+
{' '}{log.message}
|
|
64
|
+
</span>
|
|
65
|
+
</text>
|
|
66
|
+
)}
|
|
67
|
+
</For>
|
|
68
|
+
</box>
|
|
69
|
+
</scrollbox>
|
|
70
|
+
</box>
|
|
71
|
+
</Show>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Show } from 'solid-js';
|
|
2
|
+
import { useTheme } from '../contexts/ThemeContext.tsx';
|
|
3
|
+
import { useRepl } from '../contexts/ReplContext.tsx';
|
|
4
|
+
|
|
5
|
+
export function DetailView() {
|
|
6
|
+
const theme = useTheme();
|
|
7
|
+
const [state] = useRepl();
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<Show when={state.viewMode === 'detail' && state.detailContent !== null}>
|
|
11
|
+
<box
|
|
12
|
+
width="100%"
|
|
13
|
+
height="100%"
|
|
14
|
+
flexDirection="column"
|
|
15
|
+
backgroundColor={theme.colors.background}
|
|
16
|
+
>
|
|
17
|
+
<box
|
|
18
|
+
flexShrink={0}
|
|
19
|
+
paddingLeft={2}
|
|
20
|
+
paddingRight={2}
|
|
21
|
+
borderStyle="single"
|
|
22
|
+
border={['bottom']}
|
|
23
|
+
borderColor={theme.colors.border}
|
|
24
|
+
>
|
|
25
|
+
<text selectable={false}>
|
|
26
|
+
<span style={{ fg: theme.colors.accent, bold: true }}>Full Output</span>
|
|
27
|
+
<span style={{ fg: theme.colors.textSecondary }}>{' '}Press Esc to go back</span>
|
|
28
|
+
</text>
|
|
29
|
+
</box>
|
|
30
|
+
<scrollbox
|
|
31
|
+
flexGrow={1}
|
|
32
|
+
scrollY
|
|
33
|
+
stickyStart="top"
|
|
34
|
+
paddingLeft={2}
|
|
35
|
+
paddingRight={2}
|
|
36
|
+
paddingTop={1}
|
|
37
|
+
>
|
|
38
|
+
<text>
|
|
39
|
+
<span style={{ fg: theme.colors.textPrimary }}>{state.detailContent}</span>
|
|
40
|
+
</text>
|
|
41
|
+
</scrollbox>
|
|
42
|
+
</box>
|
|
43
|
+
</Show>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { Show, For, createMemo } from 'solid-js';
|
|
2
|
+
import { useTheme } from '../contexts/ThemeContext.tsx';
|
|
3
|
+
import { useRepl } from '../contexts/ReplContext.tsx';
|
|
4
|
+
import { useRenderer } from '@opentui/solid';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Truncate text with ellipsis if it exceeds maxLength
|
|
8
|
+
*/
|
|
9
|
+
export function truncateText(text: string, maxLength: number): string {
|
|
10
|
+
if (!text || text.length <= maxLength) return text || '';
|
|
11
|
+
return text.slice(0, maxLength - 1) + '…';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Calculate the label width based on the longest label in items
|
|
16
|
+
* Adds padding and caps at 40 characters
|
|
17
|
+
*/
|
|
18
|
+
export function calculateLabelWidth(items: Array<{ label: string }>): number {
|
|
19
|
+
if (!items || items.length === 0) return 10;
|
|
20
|
+
const maxLabel = Math.max(...items.map(i => i.label.length));
|
|
21
|
+
return Math.min(maxLabel + 2, 40); // Cap at 40 chars
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Dropdown menu with scrolling - Claude Code style
|
|
26
|
+
* No border, uses › prefix for selection indicator.
|
|
27
|
+
*
|
|
28
|
+
* Layout:
|
|
29
|
+
* ↑ 2 more
|
|
30
|
+
* › /command description text...
|
|
31
|
+
* /other another description
|
|
32
|
+
* ↓ 3 more
|
|
33
|
+
*/
|
|
34
|
+
export function DropdownMenu() {
|
|
35
|
+
const theme = useTheme();
|
|
36
|
+
const [state] = useRepl();
|
|
37
|
+
const renderer = useRenderer();
|
|
38
|
+
const maxVisible = 8;
|
|
39
|
+
|
|
40
|
+
// Calculate column widths based on terminal size and item labels
|
|
41
|
+
const labelWidth = createMemo(() => calculateLabelWidth(state.menuItems));
|
|
42
|
+
|
|
43
|
+
// Get terminal width for description truncation
|
|
44
|
+
const termWidth = createMemo(() => {
|
|
45
|
+
const width = renderer?.width || 80;
|
|
46
|
+
return Math.max(40, width);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Description takes remaining space (minus prefix and gaps: 2 + 4 + some buffer)
|
|
50
|
+
const descWidth = createMemo(() => {
|
|
51
|
+
return Math.max(20, termWidth() - labelWidth() - 10);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Calculate scroll offset
|
|
55
|
+
const scrollOffset = createMemo(() => {
|
|
56
|
+
const items = state.menuItems;
|
|
57
|
+
if (items.length <= maxVisible) return 0;
|
|
58
|
+
const halfVisible = Math.floor(maxVisible / 2);
|
|
59
|
+
let offset = state.selectedIndex - halfVisible;
|
|
60
|
+
offset = Math.max(0, offset);
|
|
61
|
+
offset = Math.min(items.length - maxVisible, offset);
|
|
62
|
+
return offset;
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const visibleItems = createMemo(() => {
|
|
66
|
+
return state.menuItems.slice(scrollOffset(), scrollOffset() + maxVisible);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const hasMoreAbove = createMemo(() => scrollOffset() > 0);
|
|
70
|
+
const hasMoreBelow = createMemo(() => scrollOffset() + maxVisible < state.menuItems.length);
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<Show when={state.menuVisible && state.menuItems.length > 0}>
|
|
74
|
+
<box flexDirection="column" paddingLeft={2}>
|
|
75
|
+
{/* Scroll up indicator */}
|
|
76
|
+
<Show when={hasMoreAbove()}>
|
|
77
|
+
<text selectable={false}>
|
|
78
|
+
<span style={{ fg: theme.colors.textSecondary }}> ↑ {scrollOffset()} more</span>
|
|
79
|
+
</text>
|
|
80
|
+
</Show>
|
|
81
|
+
|
|
82
|
+
{/* Menu items */}
|
|
83
|
+
<For each={visibleItems()}>
|
|
84
|
+
{(item, i) => {
|
|
85
|
+
const actualIndex = () => i() + scrollOffset();
|
|
86
|
+
const isSelected = () => actualIndex() === state.selectedIndex;
|
|
87
|
+
// Pad label to fixed width for alignment
|
|
88
|
+
const paddedLabel = () => item.label.padEnd(labelWidth());
|
|
89
|
+
// Truncate description to prevent overflow
|
|
90
|
+
const truncatedDesc = () => truncateText(item.description, descWidth());
|
|
91
|
+
// Use › indicator for selected item
|
|
92
|
+
const prefix = () => isSelected() ? '› ' : ' ';
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<text selectable={false}>
|
|
96
|
+
<span
|
|
97
|
+
style={{
|
|
98
|
+
fg: isSelected() ? theme.colors.accent : theme.colors.textSecondary,
|
|
99
|
+
}}
|
|
100
|
+
>
|
|
101
|
+
{prefix()}
|
|
102
|
+
</span>
|
|
103
|
+
<span
|
|
104
|
+
style={{
|
|
105
|
+
fg: isSelected() ? theme.colors.accent : theme.colors.textPrimary,
|
|
106
|
+
bold: isSelected(),
|
|
107
|
+
}}
|
|
108
|
+
>
|
|
109
|
+
{paddedLabel()}
|
|
110
|
+
</span>
|
|
111
|
+
<span style={{ fg: theme.colors.textSecondary }}>
|
|
112
|
+
{' '}{truncatedDesc()}
|
|
113
|
+
</span>
|
|
114
|
+
</text>
|
|
115
|
+
);
|
|
116
|
+
}}
|
|
117
|
+
</For>
|
|
118
|
+
|
|
119
|
+
{/* Scroll down indicator */}
|
|
120
|
+
<Show when={hasMoreBelow()}>
|
|
121
|
+
<text selectable={false}>
|
|
122
|
+
<span style={{ fg: theme.colors.textSecondary }}>
|
|
123
|
+
{' '}↓ {state.menuItems.length - scrollOffset() - maxVisible} more
|
|
124
|
+
</span>
|
|
125
|
+
</text>
|
|
126
|
+
</Show>
|
|
127
|
+
</box>
|
|
128
|
+
</Show>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Show } from 'solid-js';
|
|
2
|
+
import { useTheme } from '../contexts/ThemeContext.tsx';
|
|
3
|
+
import { useRepl } from '../contexts/ReplContext.tsx';
|
|
4
|
+
import { useSpinner, formatSpinnerText } from '../hooks/useSpinner.ts';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Format duration in human readable format
|
|
8
|
+
*/
|
|
9
|
+
export function formatDuration(ms: number | null | undefined): string {
|
|
10
|
+
if (ms === null || ms === undefined) return '';
|
|
11
|
+
if (ms < 1000) return `${ms}ms`;
|
|
12
|
+
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
|
13
|
+
const minutes = Math.floor(ms / 60000);
|
|
14
|
+
const seconds = ((ms % 60000) / 1000).toFixed(0);
|
|
15
|
+
return `${minutes}m${seconds}s`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Execution status area - shows spinner when executing, duration when done.
|
|
20
|
+
* This component sits in the output area (not input area) like in the Ink version.
|
|
21
|
+
*
|
|
22
|
+
* Layout when executing:
|
|
23
|
+
* ⏵ executing command...
|
|
24
|
+
*
|
|
25
|
+
* Layout when done:
|
|
26
|
+
* ⏵ completed in 1.5s
|
|
27
|
+
*
|
|
28
|
+
* Layout when ready:
|
|
29
|
+
* ⏵ ready
|
|
30
|
+
*/
|
|
31
|
+
export function ExecutionStatus(props: {
|
|
32
|
+
isExecuting: boolean;
|
|
33
|
+
command?: string;
|
|
34
|
+
lastDuration?: number | null;
|
|
35
|
+
maxWidth?: number;
|
|
36
|
+
}) {
|
|
37
|
+
const theme = useTheme();
|
|
38
|
+
const spinnerFrame = useSpinner(() => props.isExecuting);
|
|
39
|
+
const maxWidth = props.maxWidth || 60;
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<box flexDirection="column" marginTop={1} marginBottom={1} flexShrink={0}>
|
|
43
|
+
<box paddingLeft={1}>
|
|
44
|
+
<Show when={props.isExecuting} fallback={
|
|
45
|
+
<Show when={props.lastDuration !== null && props.lastDuration !== undefined} fallback={
|
|
46
|
+
<text selectable={false}>
|
|
47
|
+
<span style={{ fg: theme.colors.textSecondary }}>⏵ ready</span>
|
|
48
|
+
</text>
|
|
49
|
+
}>
|
|
50
|
+
<text selectable={false}>
|
|
51
|
+
<span style={{ fg: theme.colors.textSecondary }}>
|
|
52
|
+
⏵ completed in {formatDuration(props.lastDuration)}
|
|
53
|
+
</span>
|
|
54
|
+
</text>
|
|
55
|
+
</Show>
|
|
56
|
+
}>
|
|
57
|
+
<text selectable={false}>
|
|
58
|
+
<span style={{ fg: theme.colors.warning }}>
|
|
59
|
+
{formatSpinnerText(spinnerFrame(), props.command || '', maxWidth)}
|
|
60
|
+
</span>
|
|
61
|
+
</text>
|
|
62
|
+
</Show>
|
|
63
|
+
</box>
|
|
64
|
+
</box>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useTheme } from '../contexts/ThemeContext.tsx';
|
|
2
|
+
import { useSession } from '../contexts/SessionContext.tsx';
|
|
3
|
+
|
|
4
|
+
export function Header() {
|
|
5
|
+
const theme = useTheme();
|
|
6
|
+
const session = useSession();
|
|
7
|
+
|
|
8
|
+
return (
|
|
9
|
+
<box
|
|
10
|
+
flexShrink={0}
|
|
11
|
+
flexDirection="row"
|
|
12
|
+
paddingLeft={2}
|
|
13
|
+
paddingRight={2}
|
|
14
|
+
borderStyle="single"
|
|
15
|
+
border={['bottom']}
|
|
16
|
+
borderColor={theme.colors.border}
|
|
17
|
+
>
|
|
18
|
+
<box flexGrow={1} />
|
|
19
|
+
<text selectable={false}>
|
|
20
|
+
<span style={{ fg: theme.colors.textSecondary }}>v{session.version}</span>
|
|
21
|
+
</text>
|
|
22
|
+
</box>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { For } from 'solid-js';
|
|
2
|
+
import { useRepl } from '../contexts/ReplContext.tsx';
|
|
3
|
+
import { OutputBlock } from './OutputBlock.tsx';
|
|
4
|
+
import { WelcomeBanner } from './WelcomeBanner.tsx';
|
|
5
|
+
|
|
6
|
+
export function OutputArea() {
|
|
7
|
+
const [state] = useRepl();
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<scrollbox
|
|
11
|
+
flexGrow={1}
|
|
12
|
+
flexShrink={1}
|
|
13
|
+
stickyScroll
|
|
14
|
+
stickyStart="bottom"
|
|
15
|
+
scrollY
|
|
16
|
+
>
|
|
17
|
+
<box flexDirection="column" gap={1}>
|
|
18
|
+
<WelcomeBanner />
|
|
19
|
+
<For each={state.outputs}>
|
|
20
|
+
{(item) => <OutputBlock item={item} />}
|
|
21
|
+
</For>
|
|
22
|
+
</box>
|
|
23
|
+
</scrollbox>
|
|
24
|
+
);
|
|
25
|
+
}
|