Skip to content

Commit b446caa

Browse files
committed
feat: full-width input box + 'runcode solana' shortcut
- Input box stretches to terminal width (like Claude Code) - 'runcode solana' sets Solana chain and starts directly - 'runcode base' sets Base chain and starts directly - Status bar: model · balance · esc to quit
1 parent a340d26 commit b446caa

File tree

4 files changed

+91
-41
lines changed

4 files changed

+91
-41
lines changed

dist/index.js

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,26 @@ program
9090
const args = process.argv.slice(2);
9191
const knownCommands = program.commands.map(c => c.name());
9292
const firstArg = args[0];
93-
if (!firstArg || (firstArg.startsWith('-') && firstArg !== '-h' && firstArg !== '--help' && firstArg !== '-V' && firstArg !== '--version')) {
93+
// Handle chain shortcuts: `runcode solana` or `runcode base`
94+
if (firstArg === 'solana' || firstArg === 'base') {
95+
// Set chain and start
96+
import('./config.js').then(({ saveChain }) => {
97+
saveChain(firstArg);
98+
const startOpts = { version };
99+
// Parse remaining flags
100+
for (let i = 1; i < args.length; i++) {
101+
if (args[i] === '--trust')
102+
startOpts.trust = true;
103+
else if (args[i] === '--debug')
104+
startOpts.debug = true;
105+
else if ((args[i] === '-m' || args[i] === '--model') && args[i + 1]) {
106+
startOpts.model = args[++i];
107+
}
108+
}
109+
startCommand(startOpts);
110+
});
111+
}
112+
else if (!firstArg || (firstArg.startsWith('-') && firstArg !== '-h' && firstArg !== '--help' && firstArg !== '-V' && firstArg !== '--version')) {
94113
// No subcommand or only flags — treat as 'start' with flags
95114
const startOpts = { version };
96115
for (let i = 0; i < args.length; i++) {

dist/ui/app.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,17 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
44
* Real-time streaming, thinking animation, tool progress, slash commands.
55
*/
66
import { useState, useEffect, useCallback } from 'react';
7-
import { render, Box, Text, useApp, useInput } from 'ink';
7+
import { render, Box, Text, useApp, useInput, useStdout } from 'ink';
88
import Spinner from 'ink-spinner';
99
import TextInput from 'ink-text-input';
1010
import { resolveModel } from './model-picker.js';
11+
// ─── Full-width input box ──────────────────────────────────────────────────
12+
function InputBox({ input, setInput, onSubmit, model, balance }) {
13+
const { stdout } = useStdout();
14+
const cols = stdout?.columns ?? 80;
15+
const innerWidth = Math.max(40, cols - 4); // 4 = borders + padding
16+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { dimColor: true, children: '╭' + '─'.repeat(cols - 2) + '╮' }), _jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "\u2502 " }), _jsx(Box, { width: innerWidth, children: _jsx(TextInput, { value: input, onChange: setInput, onSubmit: onSubmit, placeholder: "Ask anything... (/model to switch, /help for commands)" }) }), _jsxs(Text, { dimColor: true, children: [' '.repeat(Math.max(0, cols - innerWidth - 4)), "\u2502"] })] }), _jsx(Text, { dimColor: true, children: '╰' + '─'.repeat(cols - 2) + '╯' }), _jsx(Box, { marginLeft: 1, children: _jsxs(Text, { dimColor: true, children: [model, " \u00B7 ", balance, " \u00B7 esc to quit"] }) })] }));
17+
}
1118
// ─── Model picker data ─────────────────────────────────────────────────────
1219
const PICKER_MODELS = [
1320
{ id: 'anthropic/claude-sonnet-4.6', shortcut: 'sonnet', label: 'Claude Sonnet 4.6', price: '$3/$15' },
@@ -195,7 +202,7 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
195202
// ── Normal Mode ──
196203
return (_jsxs(Box, { flexDirection: "column", children: [statusMsg && (_jsx(Box, { marginLeft: 2, children: _jsx(Text, { color: "green", children: statusMsg }) })), showHelp && (_jsxs(Box, { flexDirection: "column", marginLeft: 2, marginTop: 1, marginBottom: 1, children: [_jsx(Text, { bold: true, children: "Commands" }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/model" }), " [name] Switch model (picker if no name)"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/wallet" }), " Show wallet address & balance"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/cost" }), " Session cost"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/help" }), " This help"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/exit" }), " Quit"] }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: " Shortcuts: sonnet, opus, gpt, gemini, deepseek, flash, free, r1, o4, nano, mini, haiku" })] })), showWallet && (_jsxs(Box, { flexDirection: "column", marginLeft: 2, marginTop: 1, marginBottom: 1, children: [_jsx(Text, { bold: true, children: "Wallet" }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [" Chain: ", _jsx(Text, { color: "magenta", children: chain })] }), _jsxs(Text, { children: [" Address: ", _jsx(Text, { color: "cyan", children: walletAddress })] }), _jsxs(Text, { children: [" Balance: ", _jsx(Text, { color: "green", children: walletBalance })] })] })), Array.from(tools.values()).map((tool, i) => (_jsx(Box, { marginLeft: 1, children: tool.done ? (tool.error
197204
? _jsxs(Text, { color: "red", children: [" \u2717 ", tool.name, " ", _jsxs(Text, { dimColor: true, children: [tool.elapsed, "ms"] })] })
198-
: _jsxs(Text, { color: "green", children: [" \u2713 ", tool.name, " ", _jsxs(Text, { dimColor: true, children: [tool.elapsed, "ms \u2014 ", tool.preview.slice(0, 60), tool.preview.length > 60 ? '...' : ''] })] })) : (_jsxs(Text, { color: "cyan", children: [" ", _jsx(Spinner, { type: "dots" }), " ", tool.name, "..."] })) }, i))), thinking && (_jsx(Box, { marginLeft: 1, children: _jsxs(Text, { color: "magenta", children: [" ", _jsx(Spinner, { type: "dots" }), " thinking..."] }) })), waiting && !thinking && tools.size === 0 && (_jsx(Box, { marginLeft: 1, children: _jsxs(Text, { color: "yellow", children: [" ", _jsx(Spinner, { type: "dots" }), " ", _jsx(Text, { dimColor: true, children: currentModel })] }) })), streamText && (_jsx(Box, { marginTop: 0, marginBottom: 0, children: _jsx(Text, { children: streamText }) })), ready && (turnTokens.input > 0 || turnTokens.output > 0) && streamText && (_jsx(Box, { marginLeft: 1, marginTop: 0, children: _jsxs(Text, { dimColor: true, children: [turnTokens.input.toLocaleString(), " in / ", turnTokens.output.toLocaleString(), " out"] }) })), ready && (_jsxs(Box, { flexDirection: "column", marginTop: streamText ? 1 : 0, children: [_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "\u256D\u2500" }), _jsx(Text, { dimColor: true, children: '─'.repeat(58) }), _jsx(Text, { dimColor: true, children: "\u256E" })] }), _jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "\u2502 " }), _jsx(Box, { width: 58, children: input ? (_jsx(TextInput, { value: input, onChange: setInput, onSubmit: handleSubmit })) : (_jsx(TextInput, { value: input, onChange: setInput, onSubmit: handleSubmit, placeholder: "Ask anything... (/model to switch, /help for commands)" })) }), _jsx(Text, { dimColor: true, children: " \u2502" })] }), _jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "\u2570\u2500" }), _jsx(Text, { dimColor: true, children: '─'.repeat(58) }), _jsx(Text, { dimColor: true, children: "\u256F" })] }), _jsxs(Box, { marginLeft: 2, children: [_jsx(Text, { dimColor: true, children: currentModel }), _jsx(Text, { dimColor: true, children: " \u00B7 " }), _jsx(Text, { dimColor: true, children: walletBalance }), _jsx(Text, { dimColor: true, children: " \u00B7 " }), _jsx(Text, { dimColor: true, children: "esc to quit" })] })] }))] }));
205+
: _jsxs(Text, { color: "green", children: [" \u2713 ", tool.name, " ", _jsxs(Text, { dimColor: true, children: [tool.elapsed, "ms \u2014 ", tool.preview.slice(0, 60), tool.preview.length > 60 ? '...' : ''] })] })) : (_jsxs(Text, { color: "cyan", children: [" ", _jsx(Spinner, { type: "dots" }), " ", tool.name, "..."] })) }, i))), thinking && (_jsx(Box, { marginLeft: 1, children: _jsxs(Text, { color: "magenta", children: [" ", _jsx(Spinner, { type: "dots" }), " thinking..."] }) })), waiting && !thinking && tools.size === 0 && (_jsx(Box, { marginLeft: 1, children: _jsxs(Text, { color: "yellow", children: [" ", _jsx(Spinner, { type: "dots" }), " ", _jsx(Text, { dimColor: true, children: currentModel })] }) })), streamText && (_jsx(Box, { marginTop: 0, marginBottom: 0, children: _jsx(Text, { children: streamText }) })), ready && (turnTokens.input > 0 || turnTokens.output > 0) && streamText && (_jsx(Box, { marginLeft: 1, marginTop: 0, children: _jsxs(Text, { dimColor: true, children: [turnTokens.input.toLocaleString(), " in / ", turnTokens.output.toLocaleString(), " out"] }) })), ready && (_jsx(InputBox, { input: input, setInput: setInput, onSubmit: handleSubmit, model: currentModel, balance: walletBalance }))] }));
199206
}
200207
export function launchInkUI(opts) {
201208
let resolveInput = null;

src/index.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,23 @@ const args = process.argv.slice(2);
116116
const knownCommands = program.commands.map(c => c.name());
117117
const firstArg = args[0];
118118

119-
if (!firstArg || (firstArg.startsWith('-') && firstArg !== '-h' && firstArg !== '--help' && firstArg !== '-V' && firstArg !== '--version')) {
119+
// Handle chain shortcuts: `runcode solana` or `runcode base`
120+
if (firstArg === 'solana' || firstArg === 'base') {
121+
// Set chain and start
122+
import('./config.js').then(({ saveChain }) => {
123+
saveChain(firstArg as 'base' | 'solana');
124+
const startOpts: Record<string, unknown> = { version };
125+
// Parse remaining flags
126+
for (let i = 1; i < args.length; i++) {
127+
if (args[i] === '--trust') startOpts.trust = true;
128+
else if (args[i] === '--debug') startOpts.debug = true;
129+
else if ((args[i] === '-m' || args[i] === '--model') && args[i + 1]) {
130+
startOpts.model = args[++i];
131+
}
132+
}
133+
startCommand(startOpts as Parameters<typeof startCommand>[0]);
134+
});
135+
} else if (!firstArg || (firstArg.startsWith('-') && firstArg !== '-h' && firstArg !== '--help' && firstArg !== '-V' && firstArg !== '--version')) {
120136
// No subcommand or only flags — treat as 'start' with flags
121137
const startOpts: Record<string, unknown> = { version };
122138
for (let i = 0; i < args.length; i++) {

src/ui/app.tsx

Lines changed: 45 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,48 @@
44
*/
55

66
import React, { useState, useEffect, useCallback } from 'react';
7-
import { render, Box, Text, useApp, useInput } from 'ink';
7+
import { render, Box, Text, useApp, useInput, useStdout } from 'ink';
88
import Spinner from 'ink-spinner';
99
import TextInput from 'ink-text-input';
1010
import type { StreamEvent } from '../agent/types.js';
1111
import { resolveModel } from './model-picker.js';
1212

13+
// ─── Full-width input box ──────────────────────────────────────────────────
14+
15+
function InputBox({ input, setInput, onSubmit, model, balance }: {
16+
input: string;
17+
setInput: (v: string) => void;
18+
onSubmit: (v: string) => void;
19+
model: string;
20+
balance: string;
21+
}) {
22+
const { stdout } = useStdout();
23+
const cols = stdout?.columns ?? 80;
24+
const innerWidth = Math.max(40, cols - 4); // 4 = borders + padding
25+
26+
return (
27+
<Box flexDirection="column" marginTop={1}>
28+
<Text dimColor>{'╭' + '─'.repeat(cols - 2) + '╮'}</Text>
29+
<Box>
30+
<Text dimColor></Text>
31+
<Box width={innerWidth}>
32+
<TextInput
33+
value={input}
34+
onChange={setInput}
35+
onSubmit={onSubmit}
36+
placeholder="Ask anything... (/model to switch, /help for commands)"
37+
/>
38+
</Box>
39+
<Text dimColor>{' '.repeat(Math.max(0, cols - innerWidth - 4))}</Text>
40+
</Box>
41+
<Text dimColor>{'╰' + '─'.repeat(cols - 2) + '╯'}</Text>
42+
<Box marginLeft={1}>
43+
<Text dimColor>{model} · {balance} · esc to quit</Text>
44+
</Box>
45+
</Box>
46+
);
47+
}
48+
1349
// ─── Model picker data ─────────────────────────────────────────────────────
1450

1551
const PICKER_MODELS = [
@@ -326,43 +362,15 @@ function RunCodeApp({
326362
</Box>
327363
)}
328364

329-
{/* Claude Code-style input box */}
365+
{/* Full-width input box */}
330366
{ready && (
331-
<Box flexDirection="column" marginTop={streamText ? 1 : 0}>
332-
<Box>
333-
<Text dimColor>╭─</Text>
334-
<Text dimColor>{'─'.repeat(58)}</Text>
335-
<Text dimColor></Text>
336-
</Box>
337-
<Box>
338-
<Text dimColor></Text>
339-
<Box width={58}>
340-
{input ? (
341-
<TextInput value={input} onChange={setInput} onSubmit={handleSubmit} />
342-
) : (
343-
<TextInput
344-
value={input}
345-
onChange={setInput}
346-
onSubmit={handleSubmit}
347-
placeholder="Ask anything... (/model to switch, /help for commands)"
348-
/>
349-
)}
350-
</Box>
351-
<Text dimColor></Text>
352-
</Box>
353-
<Box>
354-
<Text dimColor>╰─</Text>
355-
<Text dimColor>{'─'.repeat(58)}</Text>
356-
<Text dimColor></Text>
357-
</Box>
358-
<Box marginLeft={2}>
359-
<Text dimColor>{currentModel}</Text>
360-
<Text dimColor> · </Text>
361-
<Text dimColor>{walletBalance}</Text>
362-
<Text dimColor> · </Text>
363-
<Text dimColor>esc to quit</Text>
364-
</Box>
365-
</Box>
367+
<InputBox
368+
input={input}
369+
setInput={setInput}
370+
onSubmit={handleSubmit}
371+
model={currentModel}
372+
balance={walletBalance}
373+
/>
366374
)}
367375
</Box>
368376
);

0 commit comments

Comments
 (0)