Skip to content

Commit 78504be

Browse files
committed
fix: useInput/TextInput conflict resolved, ImageGen 60s timeout, clean exit
- useInput only active during picker/esc, TextInput focus controlled - ImageGen aborts after 60s instead of hanging forever - process.exit(0) on both ink and basic UI paths - Default model GLM-5 always shown (no empty Model:)
1 parent e710c9e commit 78504be

File tree

6 files changed

+43
-14
lines changed

6 files changed

+43
-14
lines changed

dist/commands/start.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ async function runWithBasicUI(agentConfig, model, workDir) {
148148
}
149149
}
150150
ui.printGoodbye();
151+
process.exit(0);
151152
}
152153
async function handleSlashCommand(cmd, config, ui) {
153154
const parts = cmd.trim().split(/\s+/);

dist/tools/imagegen.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,12 @@ async function execute(input, ctx) {
3232
'User-Agent': 'runcode/1.0',
3333
};
3434
try {
35+
const controller = new AbortController();
36+
const timeout = setTimeout(() => controller.abort(), 60_000); // 60s timeout
3537
// First request — will get 402
3638
let response = await fetch(endpoint, {
3739
method: 'POST',
40+
signal: controller.signal,
3841
headers,
3942
body,
4043
});
@@ -46,10 +49,12 @@ async function execute(input, ctx) {
4649
}
4750
response = await fetch(endpoint, {
4851
method: 'POST',
52+
signal: controller.signal,
4953
headers: { ...headers, ...paymentHeaders },
5054
body,
5155
});
5256
}
57+
clearTimeout(timeout);
5358
if (!response.ok) {
5459
const errText = await response.text().catch(() => '');
5560
return { output: `Image generation failed (${response.status}): ${errText.slice(0, 200)}`, isError: true };
@@ -83,7 +88,11 @@ async function execute(input, ctx) {
8388
};
8489
}
8590
catch (err) {
86-
return { output: `Error: ${err.message}`, isError: true };
91+
const msg = err.message || '';
92+
if (msg.includes('abort')) {
93+
return { output: 'Image generation timed out (60s limit). Try a simpler prompt.', isError: true };
94+
}
95+
return { output: `Error: ${msg}`, isError: true };
8796
}
8897
}
8998
// ─── Payment ───────────────────────────────────────────────────────────────

dist/ui/app.js

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@ import Spinner from 'ink-spinner';
99
import TextInput from 'ink-text-input';
1010
import { resolveModel } from './model-picker.js';
1111
// ─── Full-width input box ──────────────────────────────────────────────────
12-
function InputBox({ input, setInput, onSubmit, model, balance }) {
12+
function InputBox({ input, setInput, onSubmit, model, balance, focused }) {
1313
const { stdout } = useStdout();
1414
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"] }) })] }));
15+
const innerWidth = Math.max(40, cols - 4);
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)", focus: focused !== false }) }), _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"] }) })] }));
1717
}
1818
// ─── Model picker data ─────────────────────────────────────────────────────
1919
const PICKER_MODELS = [
@@ -49,8 +49,10 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
4949
const [totalCost, setTotalCost] = useState(0);
5050
const [showHelp, setShowHelp] = useState(false);
5151
const [showWallet, setShowWallet] = useState(false);
52+
// Key handler for picker + esc — ONLY active when TextInput is NOT focused
53+
const isPickerOrEsc = mode === 'model-picker' || (mode === 'input' && ready && !input);
5254
useInput((ch, key) => {
53-
// Esc to quit (when not in picker)
55+
// Esc to quit (only when input is empty and in input mode)
5456
if (key.escape && mode === 'input' && ready && !input) {
5557
onExit();
5658
exit();
@@ -69,14 +71,14 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
6971
onModelChange(selected.id);
7072
setStatusMsg(`Model → ${selected.label}`);
7173
setMode('input');
72-
setReady(true); // Show input box after picking
74+
setReady(true);
7375
setTimeout(() => setStatusMsg(''), 3000);
7476
}
7577
else if (key.escape) {
7678
setMode('input');
7779
setReady(true);
7880
}
79-
});
81+
}, { isActive: isPickerOrEsc });
8082
const handleSubmit = useCallback((value) => {
8183
const trimmed = value.trim();
8284
if (!trimmed)
@@ -218,7 +220,7 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
218220
// ── Normal Mode ──
219221
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
220222
? _jsxs(Text, { color: "red", children: [" \u2717 ", tool.name, " ", _jsxs(Text, { dimColor: true, children: [tool.elapsed, "ms"] })] })
221-
: _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 }))] }));
223+
: _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, focused: mode === 'input' }))] }));
222224
}
223225
export function launchInkUI(opts) {
224226
let resolveInput = null;

src/commands/start.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ async function runWithBasicUI(
180180
}
181181

182182
ui.printGoodbye();
183+
process.exit(0);
183184
}
184185

185186
// ─── Slash commands ────────────────────────────────────────────────────────

src/tools/imagegen.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,13 @@ async function execute(input: Record<string, unknown>, ctx: ExecutionScope): Pro
5757
};
5858

5959
try {
60+
const controller = new AbortController();
61+
const timeout = setTimeout(() => controller.abort(), 60_000); // 60s timeout
62+
6063
// First request — will get 402
6164
let response = await fetch(endpoint, {
6265
method: 'POST',
66+
signal: controller.signal,
6367
headers,
6468
body,
6569
});
@@ -73,11 +77,14 @@ async function execute(input: Record<string, unknown>, ctx: ExecutionScope): Pro
7377

7478
response = await fetch(endpoint, {
7579
method: 'POST',
80+
signal: controller.signal,
7681
headers: { ...headers, ...paymentHeaders },
7782
body,
7883
});
7984
}
8085

86+
clearTimeout(timeout);
87+
8188
if (!response.ok) {
8289
const errText = await response.text().catch(() => '');
8390
return { output: `Image generation failed (${response.status}): ${errText.slice(0, 200)}`, isError: true };
@@ -115,7 +122,11 @@ async function execute(input: Record<string, unknown>, ctx: ExecutionScope): Pro
115122
output: `Image saved to ${outPath} (${sizeKB}KB, ${imageSize})${revisedPrompt}\n\nOpen with: open ${outPath}`,
116123
};
117124
} catch (err) {
118-
return { output: `Error: ${(err as Error).message}`, isError: true };
125+
const msg = (err as Error).message || '';
126+
if (msg.includes('abort')) {
127+
return { output: 'Image generation timed out (60s limit). Try a simpler prompt.', isError: true };
128+
}
129+
return { output: `Error: ${msg}`, isError: true };
119130
}
120131
}
121132

src/ui/app.tsx

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,17 @@ import { resolveModel } from './model-picker.js';
1212

1313
// ─── Full-width input box ──────────────────────────────────────────────────
1414

15-
function InputBox({ input, setInput, onSubmit, model, balance }: {
15+
function InputBox({ input, setInput, onSubmit, model, balance, focused }: {
1616
input: string;
1717
setInput: (v: string) => void;
1818
onSubmit: (v: string) => void;
1919
model: string;
2020
balance: string;
21+
focused?: boolean;
2122
}) {
2223
const { stdout } = useStdout();
2324
const cols = stdout?.columns ?? 80;
24-
const innerWidth = Math.max(40, cols - 4); // 4 = borders + padding
25+
const innerWidth = Math.max(40, cols - 4);
2526

2627
return (
2728
<Box flexDirection="column" marginTop={1}>
@@ -34,6 +35,7 @@ function InputBox({ input, setInput, onSubmit, model, balance }: {
3435
onChange={setInput}
3536
onSubmit={onSubmit}
3637
placeholder="Ask anything... (/model to switch, /help for commands)"
38+
focus={focused !== false}
3739
/>
3840
</Box>
3941
<Text dimColor>{' '.repeat(Math.max(0, cols - innerWidth - 4))}</Text>
@@ -111,8 +113,10 @@ function RunCodeApp({
111113
const [showHelp, setShowHelp] = useState(false);
112114
const [showWallet, setShowWallet] = useState(false);
113115

116+
// Key handler for picker + esc — ONLY active when TextInput is NOT focused
117+
const isPickerOrEsc = mode === 'model-picker' || (mode === 'input' && ready && !input);
114118
useInput((ch, key) => {
115-
// Esc to quit (when not in picker)
119+
// Esc to quit (only when input is empty and in input mode)
116120
if (key.escape && mode === 'input' && ready && !input) {
117121
onExit();
118122
exit();
@@ -129,14 +133,14 @@ function RunCodeApp({
129133
onModelChange(selected.id);
130134
setStatusMsg(`Model → ${selected.label}`);
131135
setMode('input');
132-
setReady(true); // Show input box after picking
136+
setReady(true);
133137
setTimeout(() => setStatusMsg(''), 3000);
134138
}
135139
else if (key.escape) {
136140
setMode('input');
137141
setReady(true);
138142
}
139-
});
143+
}, { isActive: isPickerOrEsc });
140144

141145
const handleSubmit = useCallback((value: string) => {
142146
const trimmed = value.trim();
@@ -393,6 +397,7 @@ function RunCodeApp({
393397
onSubmit={handleSubmit}
394398
model={currentModel}
395399
balance={walletBalance}
400+
focused={mode === 'input'}
396401
/>
397402
)}
398403
</Box>

0 commit comments

Comments
 (0)