@blockrun/franklin 3.0.0
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/LICENSE +190 -0
- package/README.md +256 -0
- package/dist/agent/commands.d.ts +27 -0
- package/dist/agent/commands.js +659 -0
- package/dist/agent/compact.d.ts +31 -0
- package/dist/agent/compact.js +366 -0
- package/dist/agent/context.d.ts +11 -0
- package/dist/agent/context.js +184 -0
- package/dist/agent/error-classifier.d.ts +10 -0
- package/dist/agent/error-classifier.js +61 -0
- package/dist/agent/llm.d.ts +63 -0
- package/dist/agent/llm.js +448 -0
- package/dist/agent/loop.d.ts +12 -0
- package/dist/agent/loop.js +346 -0
- package/dist/agent/optimize.d.ts +53 -0
- package/dist/agent/optimize.js +262 -0
- package/dist/agent/permissions.d.ts +39 -0
- package/dist/agent/permissions.js +226 -0
- package/dist/agent/reduce.d.ts +49 -0
- package/dist/agent/reduce.js +317 -0
- package/dist/agent/streaming-executor.d.ts +36 -0
- package/dist/agent/streaming-executor.js +149 -0
- package/dist/agent/tokens.d.ts +53 -0
- package/dist/agent/tokens.js +185 -0
- package/dist/agent/types.d.ts +125 -0
- package/dist/agent/types.js +5 -0
- package/dist/banner.d.ts +1 -0
- package/dist/banner.js +27 -0
- package/dist/commands/balance.d.ts +1 -0
- package/dist/commands/balance.js +40 -0
- package/dist/commands/config.d.ts +14 -0
- package/dist/commands/config.js +107 -0
- package/dist/commands/daemon.d.ts +3 -0
- package/dist/commands/daemon.js +117 -0
- package/dist/commands/history.d.ts +5 -0
- package/dist/commands/history.js +31 -0
- package/dist/commands/init.d.ts +3 -0
- package/dist/commands/init.js +92 -0
- package/dist/commands/logs.d.ts +5 -0
- package/dist/commands/logs.js +89 -0
- package/dist/commands/models.d.ts +1 -0
- package/dist/commands/models.js +56 -0
- package/dist/commands/plugin.d.ts +14 -0
- package/dist/commands/plugin.js +176 -0
- package/dist/commands/proxy.d.ts +13 -0
- package/dist/commands/proxy.js +106 -0
- package/dist/commands/setup.d.ts +1 -0
- package/dist/commands/setup.js +49 -0
- package/dist/commands/start.d.ts +8 -0
- package/dist/commands/start.js +292 -0
- package/dist/commands/stats.d.ts +10 -0
- package/dist/commands/stats.js +94 -0
- package/dist/commands/uninit.d.ts +1 -0
- package/dist/commands/uninit.js +63 -0
- package/dist/config.d.ts +9 -0
- package/dist/config.js +41 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +179 -0
- package/dist/mcp/client.d.ts +44 -0
- package/dist/mcp/client.js +147 -0
- package/dist/mcp/config.d.ts +20 -0
- package/dist/mcp/config.js +138 -0
- package/dist/plugin-sdk/channel.d.ts +100 -0
- package/dist/plugin-sdk/channel.js +10 -0
- package/dist/plugin-sdk/index.d.ts +14 -0
- package/dist/plugin-sdk/index.js +9 -0
- package/dist/plugin-sdk/plugin.d.ts +87 -0
- package/dist/plugin-sdk/plugin.js +7 -0
- package/dist/plugin-sdk/search.d.ts +13 -0
- package/dist/plugin-sdk/search.js +4 -0
- package/dist/plugin-sdk/tracker.d.ts +27 -0
- package/dist/plugin-sdk/tracker.js +5 -0
- package/dist/plugin-sdk/workflow.d.ts +126 -0
- package/dist/plugin-sdk/workflow.js +11 -0
- package/dist/plugins/registry.d.ts +33 -0
- package/dist/plugins/registry.js +155 -0
- package/dist/plugins/runner.d.ts +21 -0
- package/dist/plugins/runner.js +453 -0
- package/dist/plugins-bundled/social/index.d.ts +10 -0
- package/dist/plugins-bundled/social/index.js +363 -0
- package/dist/plugins-bundled/social/plugin.json +14 -0
- package/dist/plugins-bundled/social/prompts.d.ts +19 -0
- package/dist/plugins-bundled/social/prompts.js +67 -0
- package/dist/plugins-bundled/social/types.d.ts +58 -0
- package/dist/plugins-bundled/social/types.js +16 -0
- package/dist/pricing.d.ts +21 -0
- package/dist/pricing.js +91 -0
- package/dist/proxy/fallback.d.ts +38 -0
- package/dist/proxy/fallback.js +144 -0
- package/dist/proxy/server.d.ts +18 -0
- package/dist/proxy/server.js +576 -0
- package/dist/proxy/sse-translator.d.ts +29 -0
- package/dist/proxy/sse-translator.js +270 -0
- package/dist/router/index.d.ts +22 -0
- package/dist/router/index.js +269 -0
- package/dist/session/search.d.ts +33 -0
- package/dist/session/search.js +229 -0
- package/dist/session/storage.d.ts +48 -0
- package/dist/session/storage.js +173 -0
- package/dist/stats/insights.d.ts +55 -0
- package/dist/stats/insights.js +195 -0
- package/dist/stats/tracker.d.ts +54 -0
- package/dist/stats/tracker.js +165 -0
- package/dist/tools/askuser.d.ts +6 -0
- package/dist/tools/askuser.js +76 -0
- package/dist/tools/bash.d.ts +5 -0
- package/dist/tools/bash.js +336 -0
- package/dist/tools/edit.d.ts +5 -0
- package/dist/tools/edit.js +148 -0
- package/dist/tools/glob.d.ts +5 -0
- package/dist/tools/glob.js +158 -0
- package/dist/tools/grep.d.ts +5 -0
- package/dist/tools/grep.js +194 -0
- package/dist/tools/imagegen.d.ts +6 -0
- package/dist/tools/imagegen.js +172 -0
- package/dist/tools/index.d.ts +17 -0
- package/dist/tools/index.js +30 -0
- package/dist/tools/read.d.ts +11 -0
- package/dist/tools/read.js +90 -0
- package/dist/tools/subagent.d.ts +5 -0
- package/dist/tools/subagent.js +116 -0
- package/dist/tools/task.d.ts +5 -0
- package/dist/tools/task.js +91 -0
- package/dist/tools/webfetch.d.ts +5 -0
- package/dist/tools/webfetch.js +166 -0
- package/dist/tools/websearch.d.ts +5 -0
- package/dist/tools/websearch.js +103 -0
- package/dist/tools/write.d.ts +5 -0
- package/dist/tools/write.js +114 -0
- package/dist/ui/app.d.ts +26 -0
- package/dist/ui/app.js +545 -0
- package/dist/ui/model-picker.d.ts +14 -0
- package/dist/ui/model-picker.js +161 -0
- package/dist/ui/terminal.d.ts +35 -0
- package/dist/ui/terminal.js +337 -0
- package/dist/wallet/manager.d.ts +10 -0
- package/dist/wallet/manager.js +23 -0
- package/package.json +79 -0
package/dist/ui/app.js
ADDED
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* RunCode ink-based terminal UI.
|
|
4
|
+
* Real-time streaming, thinking animation, tool progress, slash commands.
|
|
5
|
+
*/
|
|
6
|
+
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
7
|
+
import { render, Static, Box, Text, useApp, useInput, useStdout } from 'ink';
|
|
8
|
+
import Spinner from 'ink-spinner';
|
|
9
|
+
import TextInput from 'ink-text-input';
|
|
10
|
+
import { resolveModel } from './model-picker.js';
|
|
11
|
+
import { estimateCost } from '../pricing.js';
|
|
12
|
+
// ─── Full-width input box ──────────────────────────────────────────────────
|
|
13
|
+
function InputBox({ input, setInput, onSubmit, model, balance, sessionCost, queued, focused, busy }) {
|
|
14
|
+
const { stdout } = useStdout();
|
|
15
|
+
const cols = stdout?.columns ?? 80;
|
|
16
|
+
const innerWidth = Math.min(Math.max(30, cols - 4), cols - 2);
|
|
17
|
+
const placeholder = busy
|
|
18
|
+
? (queued ? `⏎ queued: ${queued.slice(0, 40)}` : 'Working...')
|
|
19
|
+
: 'Type a message...';
|
|
20
|
+
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 " }), busy && !input ? _jsxs(Text, { color: "yellow", children: [_jsx(Spinner, { type: "dots" }), " "] }) : null, _jsx(Box, { width: busy && !input ? innerWidth - 4 : innerWidth, children: _jsx(TextInput, { value: input, onChange: setInput, onSubmit: onSubmit, placeholder: placeholder, 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: [busy ? _jsx(Text, { color: "yellow", children: _jsx(Spinner, { type: "dots" }) }) : null, busy ? ' ' : '', model, " \u00B7 ", balance, sessionCost > 0.00001 ? _jsxs(Text, { color: "yellow", children: [" -$", sessionCost.toFixed(4)] }) : '', ' · esc to abort/quit'] }) })] }));
|
|
21
|
+
}
|
|
22
|
+
// ─── Model picker data ─────────────────────────────────────────────────────
|
|
23
|
+
const PICKER_MODELS = [
|
|
24
|
+
{ id: 'zai/glm-5.1', shortcut: 'glm', label: '🔥 GLM-5.1 (promo til Apr 15)', price: '$0.001/call', highlight: true },
|
|
25
|
+
{ id: 'zai/glm-5.1-turbo', shortcut: 'glm-turbo', label: 'GLM-5.1 Turbo', price: '$0.001/call' },
|
|
26
|
+
{ id: 'anthropic/claude-sonnet-4.6', shortcut: 'sonnet', label: 'Claude Sonnet 4.6', price: '$3/$15' },
|
|
27
|
+
{ id: 'anthropic/claude-opus-4.6', shortcut: 'opus', label: 'Claude Opus 4.6', price: '$5/$25' },
|
|
28
|
+
{ id: 'openai/gpt-5.4', shortcut: 'gpt', label: 'GPT-5.4', price: '$2.5/$15' },
|
|
29
|
+
{ id: 'google/gemini-2.5-pro', shortcut: 'gemini', label: 'Gemini 2.5 Pro', price: '$1.25/$10' },
|
|
30
|
+
{ id: 'deepseek/deepseek-chat', shortcut: 'deepseek', label: 'DeepSeek V3', price: '$0.28/$0.42' },
|
|
31
|
+
{ id: 'google/gemini-2.5-flash', shortcut: 'flash', label: 'Gemini 2.5 Flash', price: '$0.15/$0.6' },
|
|
32
|
+
{ id: 'openai/gpt-5-mini', shortcut: 'mini', label: 'GPT-5 Mini', price: '$0.25/$2' },
|
|
33
|
+
{ id: 'anthropic/claude-haiku-4.5-20251001', shortcut: 'haiku', label: 'Claude Haiku 4.5', price: '$1/$5' },
|
|
34
|
+
{ id: 'openai/gpt-5-nano', shortcut: 'nano', label: 'GPT-5 Nano', price: '$0.05/$0.4' },
|
|
35
|
+
{ id: 'deepseek/deepseek-reasoner', shortcut: 'r1', label: 'DeepSeek R1', price: '$0.28/$0.42' },
|
|
36
|
+
{ id: 'openai/o4-mini', shortcut: 'o4', label: 'O4 Mini', price: '$1.1/$4.4' },
|
|
37
|
+
{ id: 'nvidia/nemotron-ultra-253b', shortcut: 'free', label: 'Nemotron Ultra 253B', price: 'FREE' },
|
|
38
|
+
{ id: 'nvidia/qwen3-coder-480b', shortcut: 'qwen-coder', label: 'Qwen3 Coder 480B', price: 'FREE' },
|
|
39
|
+
{ id: 'nvidia/devstral-2-123b', shortcut: 'devstral', label: 'Devstral 2 123B', price: 'FREE' },
|
|
40
|
+
];
|
|
41
|
+
function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain, startWithPicker, onSubmit, onModelChange, onAbort, onExit, }) {
|
|
42
|
+
const { exit } = useApp();
|
|
43
|
+
const [input, setInput] = useState('');
|
|
44
|
+
const [streamText, setStreamText] = useState('');
|
|
45
|
+
const [thinking, setThinking] = useState(false);
|
|
46
|
+
const [waiting, setWaiting] = useState(false);
|
|
47
|
+
const [tools, setTools] = useState(new Map());
|
|
48
|
+
// Completed tool results committed to Static (permanent scrollback — no re-render artifacts)
|
|
49
|
+
const [completedTools, setCompletedTools] = useState([]);
|
|
50
|
+
// Full responses committed to Static immediately — goes into terminal scrollback like Claude Code
|
|
51
|
+
const [committedResponses, setCommittedResponses] = useState([]);
|
|
52
|
+
// Short preview of latest response shown in dynamic area (last ~5 lines, cleared on next turn)
|
|
53
|
+
const [responsePreview, setResponsePreview] = useState('');
|
|
54
|
+
const [currentModel, setCurrentModel] = useState(initialModel || PICKER_MODELS[0].id);
|
|
55
|
+
const [ready, setReady] = useState(!startWithPicker);
|
|
56
|
+
const [mode, setMode] = useState(startWithPicker ? 'model-picker' : 'input');
|
|
57
|
+
const [pickerIdx, setPickerIdx] = useState(0);
|
|
58
|
+
const [statusMsg, setStatusMsg] = useState('');
|
|
59
|
+
const [turnTokens, setTurnTokens] = useState({ input: 0, output: 0, calls: 0 });
|
|
60
|
+
const [totalCost, setTotalCost] = useState(0);
|
|
61
|
+
const [showHelp, setShowHelp] = useState(false);
|
|
62
|
+
const [showWallet, setShowWallet] = useState(false);
|
|
63
|
+
const [balance, setBalance] = useState(walletBalance);
|
|
64
|
+
// Parse the fetched balance to a number so we can compute live balance = fetchedBalance - sessionCost.
|
|
65
|
+
// costAtLastFetch tracks totalCost when balance was last fetched, to avoid double-subtracting.
|
|
66
|
+
const parseBalanceNum = (s) => {
|
|
67
|
+
const m = s.match(/\$([\d.]+)/);
|
|
68
|
+
return m ? parseFloat(m[1]) : null;
|
|
69
|
+
};
|
|
70
|
+
const [baseBalanceNum, setBaseBalanceNum] = useState(() => parseBalanceNum(walletBalance));
|
|
71
|
+
const [costAtLastFetch, setCostAtLastFetch] = useState(0);
|
|
72
|
+
const costAtLastFetchRef = useRef(0);
|
|
73
|
+
const baseBalanceNumRef = useRef(parseBalanceNum(walletBalance));
|
|
74
|
+
const [thinkingText, setThinkingText] = useState('');
|
|
75
|
+
const [lastPrompt, setLastPrompt] = useState('');
|
|
76
|
+
const [inputHistory, setInputHistory] = useState([]);
|
|
77
|
+
const [historyIdx, setHistoryIdx] = useState(-1);
|
|
78
|
+
const [permissionRequest, setPermissionRequest] = useState(null);
|
|
79
|
+
const [askUserRequest, setAskUserRequest] = useState(null);
|
|
80
|
+
const [askUserInput, setAskUserInput] = useState('');
|
|
81
|
+
// Message queued while agent is busy — auto-submitted when turn completes
|
|
82
|
+
const [queuedInput, setQueuedInput] = useState('');
|
|
83
|
+
const turnDoneCallbackRef = useRef(null);
|
|
84
|
+
// Refs to read current state values inside memoized event handlers (avoids stale closures)
|
|
85
|
+
const streamTextRef = useRef('');
|
|
86
|
+
const turnTokensRef = useRef({ input: 0, output: 0, calls: 0 });
|
|
87
|
+
const totalCostRef = useRef(0);
|
|
88
|
+
const turnCostRef = useRef(0); // per-turn cost (reset each turn)
|
|
89
|
+
const queuedInputRef = useRef('');
|
|
90
|
+
// Keep refs in sync so memoized event handlers can read current values
|
|
91
|
+
streamTextRef.current = streamText;
|
|
92
|
+
turnTokensRef.current = turnTokens;
|
|
93
|
+
totalCostRef.current = totalCost;
|
|
94
|
+
queuedInputRef.current = queuedInput;
|
|
95
|
+
costAtLastFetchRef.current = costAtLastFetch;
|
|
96
|
+
baseBalanceNumRef.current = baseBalanceNum;
|
|
97
|
+
// Compute live balance = fetchedBalance - spend_since_last_fetch
|
|
98
|
+
const liveBalance = baseBalanceNum !== null
|
|
99
|
+
? `$${Math.max(0, baseBalanceNum - (totalCost - costAtLastFetch)).toFixed(2)} USDC`
|
|
100
|
+
: balance;
|
|
101
|
+
// Permission dialog key handler — captures y/n/a when dialog is visible.
|
|
102
|
+
// ink 6.x: useInput handlers all fire regardless of TextInput focus prop,
|
|
103
|
+
// so we handle here AND block TextInput onChange (see focused prop below).
|
|
104
|
+
useInput((ch, _key) => {
|
|
105
|
+
if (!permissionRequest)
|
|
106
|
+
return;
|
|
107
|
+
// Clear any character that leaked into the text input
|
|
108
|
+
setInput('');
|
|
109
|
+
const c = ch.toLowerCase();
|
|
110
|
+
if (c === 'y') {
|
|
111
|
+
const r = permissionRequest.resolve;
|
|
112
|
+
setPermissionRequest(null);
|
|
113
|
+
r('yes');
|
|
114
|
+
}
|
|
115
|
+
else if (c === 'n') {
|
|
116
|
+
const r = permissionRequest.resolve;
|
|
117
|
+
setPermissionRequest(null);
|
|
118
|
+
r('no');
|
|
119
|
+
}
|
|
120
|
+
else if (c === 'a') {
|
|
121
|
+
const r = permissionRequest.resolve;
|
|
122
|
+
setPermissionRequest(null);
|
|
123
|
+
r('always');
|
|
124
|
+
}
|
|
125
|
+
}, { isActive: !!permissionRequest });
|
|
126
|
+
// Key handler for picker + esc + abort
|
|
127
|
+
const isPickerOrEsc = mode === 'model-picker' || (mode === 'input' && ready && !input) || !ready;
|
|
128
|
+
useInput((ch, key) => {
|
|
129
|
+
// Escape during generation → abort current turn (skip if permission dialog open)
|
|
130
|
+
if (key.escape && !ready && !permissionRequest) {
|
|
131
|
+
onAbort();
|
|
132
|
+
setStatusMsg('Aborted');
|
|
133
|
+
setReady(true);
|
|
134
|
+
setWaiting(false);
|
|
135
|
+
setThinking(false);
|
|
136
|
+
setTimeout(() => setStatusMsg(''), 3000);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
// Esc to quit (only when input is empty and in input mode)
|
|
140
|
+
if (key.escape && mode === 'input' && ready && !input) {
|
|
141
|
+
onExit();
|
|
142
|
+
exit();
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
// Arrow key navigation for model picker
|
|
146
|
+
if (mode !== 'model-picker')
|
|
147
|
+
return;
|
|
148
|
+
if (key.upArrow)
|
|
149
|
+
setPickerIdx(i => Math.max(0, i - 1));
|
|
150
|
+
else if (key.downArrow)
|
|
151
|
+
setPickerIdx(i => Math.min(PICKER_MODELS.length - 1, i + 1));
|
|
152
|
+
else if (key.return) {
|
|
153
|
+
const selected = PICKER_MODELS[pickerIdx];
|
|
154
|
+
setCurrentModel(selected.id);
|
|
155
|
+
onModelChange(selected.id);
|
|
156
|
+
setStatusMsg(`Model → ${selected.label}`);
|
|
157
|
+
setMode('input');
|
|
158
|
+
setReady(true);
|
|
159
|
+
setTimeout(() => setStatusMsg(''), 3000);
|
|
160
|
+
}
|
|
161
|
+
else if (key.escape) {
|
|
162
|
+
setMode('input');
|
|
163
|
+
setReady(true);
|
|
164
|
+
}
|
|
165
|
+
}, { isActive: isPickerOrEsc });
|
|
166
|
+
// Input history: Up/Down arrow when in ready input mode
|
|
167
|
+
useInput((_ch, key) => {
|
|
168
|
+
if (key.upArrow && inputHistory.length > 0) {
|
|
169
|
+
const newIdx = historyIdx < 0 ? inputHistory.length - 1 : Math.max(0, historyIdx - 1);
|
|
170
|
+
setHistoryIdx(newIdx);
|
|
171
|
+
setInput(inputHistory[newIdx]);
|
|
172
|
+
}
|
|
173
|
+
else if (key.downArrow) {
|
|
174
|
+
if (historyIdx >= 0 && historyIdx < inputHistory.length - 1) {
|
|
175
|
+
const newIdx = historyIdx + 1;
|
|
176
|
+
setHistoryIdx(newIdx);
|
|
177
|
+
setInput(inputHistory[newIdx]);
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
setHistoryIdx(-1);
|
|
181
|
+
setInput('');
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}, { isActive: ready && mode === 'input' });
|
|
185
|
+
const handleSubmit = useCallback((value) => {
|
|
186
|
+
const trimmed = value.trim();
|
|
187
|
+
if (!trimmed)
|
|
188
|
+
return;
|
|
189
|
+
// If agent is busy, queue the message — it will be auto-submitted when the turn finishes
|
|
190
|
+
if (!ready) {
|
|
191
|
+
setQueuedInput(trimmed);
|
|
192
|
+
setInput('');
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
// Bare exit/quit (no slash needed)
|
|
196
|
+
const lower = trimmed.toLowerCase();
|
|
197
|
+
if (lower === 'exit' || lower === 'quit' || lower === 'q') {
|
|
198
|
+
onExit();
|
|
199
|
+
exit();
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
// ── Slash commands ──
|
|
203
|
+
if (trimmed.startsWith('/')) {
|
|
204
|
+
setInput('');
|
|
205
|
+
setShowHelp(false);
|
|
206
|
+
setShowWallet(false);
|
|
207
|
+
const parts = trimmed.split(/\s+/);
|
|
208
|
+
const cmd = parts[0].toLowerCase();
|
|
209
|
+
switch (cmd) {
|
|
210
|
+
case '/exit':
|
|
211
|
+
case '/quit':
|
|
212
|
+
onExit();
|
|
213
|
+
exit();
|
|
214
|
+
return;
|
|
215
|
+
case '/model':
|
|
216
|
+
case '/models':
|
|
217
|
+
if (parts[1]) {
|
|
218
|
+
const resolved = resolveModel(parts[1]);
|
|
219
|
+
setCurrentModel(resolved);
|
|
220
|
+
onModelChange(resolved);
|
|
221
|
+
setStatusMsg(`Model → ${resolved}`);
|
|
222
|
+
setTimeout(() => setStatusMsg(''), 3000);
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
const idx = PICKER_MODELS.findIndex(m => m.id === currentModel);
|
|
226
|
+
setPickerIdx(idx >= 0 ? idx : 0);
|
|
227
|
+
setMode('model-picker');
|
|
228
|
+
}
|
|
229
|
+
return;
|
|
230
|
+
case '/wallet':
|
|
231
|
+
case '/balance':
|
|
232
|
+
setShowWallet(true);
|
|
233
|
+
setShowHelp(false);
|
|
234
|
+
return;
|
|
235
|
+
case '/cost':
|
|
236
|
+
case '/usage':
|
|
237
|
+
setStatusMsg(`Cost: $${totalCost.toFixed(4)} this session`);
|
|
238
|
+
setTimeout(() => setStatusMsg(''), 4000);
|
|
239
|
+
return;
|
|
240
|
+
case '/help':
|
|
241
|
+
setShowHelp(true);
|
|
242
|
+
setShowWallet(false);
|
|
243
|
+
return;
|
|
244
|
+
case '/clear':
|
|
245
|
+
setStreamText('');
|
|
246
|
+
setTools(new Map());
|
|
247
|
+
setTurnTokens({ input: 0, output: 0, calls: 0 });
|
|
248
|
+
turnCostRef.current = 0;
|
|
249
|
+
setWaiting(true);
|
|
250
|
+
setReady(false);
|
|
251
|
+
// Pass through to agent loop to clear the actual conversation history
|
|
252
|
+
onSubmit('/clear');
|
|
253
|
+
return;
|
|
254
|
+
case '/retry':
|
|
255
|
+
if (!lastPrompt) {
|
|
256
|
+
setStatusMsg('No previous prompt to retry');
|
|
257
|
+
setTimeout(() => setStatusMsg(''), 3000);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
setStreamText('');
|
|
261
|
+
setThinking(false);
|
|
262
|
+
setThinkingText('');
|
|
263
|
+
setTools(new Map());
|
|
264
|
+
setReady(false);
|
|
265
|
+
setWaiting(true);
|
|
266
|
+
setTurnTokens({ input: 0, output: 0, calls: 0 });
|
|
267
|
+
turnCostRef.current = 0;
|
|
268
|
+
onSubmit(lastPrompt);
|
|
269
|
+
return;
|
|
270
|
+
default:
|
|
271
|
+
// All other slash commands pass through to the agent loop's command registry
|
|
272
|
+
setStreamText('');
|
|
273
|
+
setThinking(false);
|
|
274
|
+
setThinkingText('');
|
|
275
|
+
setTools(new Map());
|
|
276
|
+
setWaiting(true);
|
|
277
|
+
setReady(false);
|
|
278
|
+
onSubmit(trimmed);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
// ── Normal prompt ──
|
|
283
|
+
setResponsePreview('');
|
|
284
|
+
setLastPrompt(trimmed);
|
|
285
|
+
setInputHistory(prev => [...prev.slice(-49), trimmed]); // Keep last 50
|
|
286
|
+
setHistoryIdx(-1);
|
|
287
|
+
setInput('');
|
|
288
|
+
setStreamText('');
|
|
289
|
+
setThinking(false);
|
|
290
|
+
setThinkingText('');
|
|
291
|
+
setTools(new Map());
|
|
292
|
+
setCompletedTools([]);
|
|
293
|
+
setReady(false);
|
|
294
|
+
setWaiting(true);
|
|
295
|
+
setStatusMsg('');
|
|
296
|
+
setShowHelp(false);
|
|
297
|
+
setShowWallet(false);
|
|
298
|
+
setTurnTokens({ input: 0, output: 0, calls: 0 });
|
|
299
|
+
turnCostRef.current = 0;
|
|
300
|
+
onSubmit(trimmed);
|
|
301
|
+
}, [ready, currentModel, totalCost, onSubmit, onModelChange, onAbort, onExit, exit, lastPrompt, inputHistory]);
|
|
302
|
+
// Expose event handler, balance updater, and permission bridge
|
|
303
|
+
useEffect(() => {
|
|
304
|
+
globalThis.__runcode_ui = {
|
|
305
|
+
updateModel: (model) => { setCurrentModel(model); },
|
|
306
|
+
updateBalance: (bal) => {
|
|
307
|
+
setBalance(bal);
|
|
308
|
+
const num = parseBalanceNum(bal);
|
|
309
|
+
if (num !== null) {
|
|
310
|
+
setBaseBalanceNum(num);
|
|
311
|
+
// Reset cost baseline — the fetched balance already reflects costs up to this point
|
|
312
|
+
setCostAtLastFetch(totalCostRef.current);
|
|
313
|
+
}
|
|
314
|
+
},
|
|
315
|
+
onTurnDone: (cb) => { turnDoneCallbackRef.current = cb; },
|
|
316
|
+
requestPermission: (toolName, description) => {
|
|
317
|
+
return new Promise((resolve) => {
|
|
318
|
+
// Ring the terminal bell — causes tab to show notification badge in iTerm2/Terminal.app
|
|
319
|
+
process.stderr.write('\x07');
|
|
320
|
+
setPermissionRequest({ toolName, description, resolve });
|
|
321
|
+
});
|
|
322
|
+
},
|
|
323
|
+
requestAskUser: (question, options) => {
|
|
324
|
+
return new Promise((resolve) => {
|
|
325
|
+
process.stderr.write('\x07');
|
|
326
|
+
setAskUserInput('');
|
|
327
|
+
setAskUserRequest({ question, options, resolve });
|
|
328
|
+
});
|
|
329
|
+
},
|
|
330
|
+
handleEvent: (event) => {
|
|
331
|
+
switch (event.kind) {
|
|
332
|
+
case 'text_delta':
|
|
333
|
+
setWaiting(false);
|
|
334
|
+
setThinking(false);
|
|
335
|
+
setStreamText(prev => prev + event.text);
|
|
336
|
+
break;
|
|
337
|
+
case 'thinking_delta':
|
|
338
|
+
setWaiting(false);
|
|
339
|
+
setThinking(true);
|
|
340
|
+
setThinkingText(prev => {
|
|
341
|
+
// Keep last 500 chars of thinking for display
|
|
342
|
+
const updated = prev + event.text;
|
|
343
|
+
return updated.length > 500 ? updated.slice(-500) : updated;
|
|
344
|
+
});
|
|
345
|
+
break;
|
|
346
|
+
case 'capability_start':
|
|
347
|
+
setWaiting(false);
|
|
348
|
+
setTools(prev => {
|
|
349
|
+
const next = new Map(prev);
|
|
350
|
+
next.set(event.id, {
|
|
351
|
+
name: event.name, startTime: Date.now(),
|
|
352
|
+
done: false, error: false,
|
|
353
|
+
preview: event.preview || '',
|
|
354
|
+
liveOutput: '',
|
|
355
|
+
elapsed: 0,
|
|
356
|
+
});
|
|
357
|
+
return next;
|
|
358
|
+
});
|
|
359
|
+
break;
|
|
360
|
+
case 'capability_progress':
|
|
361
|
+
setTools(prev => {
|
|
362
|
+
const t = prev.get(event.id);
|
|
363
|
+
if (!t || t.done)
|
|
364
|
+
return prev;
|
|
365
|
+
const next = new Map(prev);
|
|
366
|
+
next.set(event.id, { ...t, liveOutput: event.text });
|
|
367
|
+
return next;
|
|
368
|
+
});
|
|
369
|
+
break;
|
|
370
|
+
case 'capability_done': {
|
|
371
|
+
setTools(prev => {
|
|
372
|
+
const next = new Map(prev);
|
|
373
|
+
const t = next.get(event.id);
|
|
374
|
+
if (t) {
|
|
375
|
+
// On success: show input preview (command/path). On error: show error output.
|
|
376
|
+
const resultPreview = event.result.isError
|
|
377
|
+
? event.result.output.replace(/\n/g, ' ').slice(0, 150)
|
|
378
|
+
: (t.preview || event.result.output.replace(/\n/g, ' ').slice(0, 120));
|
|
379
|
+
const completed = {
|
|
380
|
+
...t,
|
|
381
|
+
key: event.id,
|
|
382
|
+
done: true,
|
|
383
|
+
error: !!event.result.isError,
|
|
384
|
+
preview: resultPreview,
|
|
385
|
+
liveOutput: '',
|
|
386
|
+
elapsed: Date.now() - t.startTime,
|
|
387
|
+
};
|
|
388
|
+
// Move to Static (permanent scrollback) — prevents re-render artifacts
|
|
389
|
+
setCompletedTools(prev2 => [...prev2, completed]);
|
|
390
|
+
next.delete(event.id);
|
|
391
|
+
}
|
|
392
|
+
return next;
|
|
393
|
+
});
|
|
394
|
+
break;
|
|
395
|
+
}
|
|
396
|
+
case 'usage': {
|
|
397
|
+
setCurrentModel(event.model);
|
|
398
|
+
setTurnTokens(prev => ({
|
|
399
|
+
input: prev.input + event.inputTokens,
|
|
400
|
+
output: prev.output + event.outputTokens,
|
|
401
|
+
calls: prev.calls + (event.calls ?? 1),
|
|
402
|
+
}));
|
|
403
|
+
const turnCallCost = estimateCost(event.model, event.inputTokens, event.outputTokens, event.calls ?? 1);
|
|
404
|
+
turnCostRef.current += turnCallCost;
|
|
405
|
+
setTotalCost(prev => prev + turnCallCost);
|
|
406
|
+
break;
|
|
407
|
+
}
|
|
408
|
+
case 'turn_done': {
|
|
409
|
+
// Commit full response to Static immediately — enters terminal scrollback like Claude Code.
|
|
410
|
+
// Also keep a short preview (last 5 lines) visible in the dynamic area.
|
|
411
|
+
const text = streamTextRef.current;
|
|
412
|
+
if (text.trim()) {
|
|
413
|
+
setCommittedResponses(rs => [...rs, {
|
|
414
|
+
key: String(Date.now()),
|
|
415
|
+
text,
|
|
416
|
+
tokens: turnTokensRef.current,
|
|
417
|
+
cost: turnCostRef.current, // per-turn cost, not cumulative
|
|
418
|
+
}]);
|
|
419
|
+
// Preview = only show when response is long enough to scroll off screen
|
|
420
|
+
const allLines = text.split('\n');
|
|
421
|
+
if (allLines.length > 20) {
|
|
422
|
+
setResponsePreview(' ↑ scroll to see full reply\n' + allLines.slice(-20).join('\n'));
|
|
423
|
+
}
|
|
424
|
+
else {
|
|
425
|
+
// Short response: already fully visible in scrollback, no preview needed
|
|
426
|
+
setResponsePreview('');
|
|
427
|
+
}
|
|
428
|
+
setStreamText('');
|
|
429
|
+
}
|
|
430
|
+
setReady(true);
|
|
431
|
+
setWaiting(false);
|
|
432
|
+
setThinking(false);
|
|
433
|
+
setThinkingText('');
|
|
434
|
+
// Trigger balance refresh after each completed turn
|
|
435
|
+
turnDoneCallbackRef.current?.();
|
|
436
|
+
// Ring the terminal bell so the user knows the AI finished
|
|
437
|
+
// (shows notification badge in iTerm2/Terminal.app when tabbed away)
|
|
438
|
+
process.stderr.write('\x07');
|
|
439
|
+
// Auto-submit any message queued while agent was busy
|
|
440
|
+
const queued = queuedInputRef.current;
|
|
441
|
+
if (queued) {
|
|
442
|
+
setQueuedInput('');
|
|
443
|
+
// Small delay so React can flush the ready=true state first
|
|
444
|
+
setTimeout(() => {
|
|
445
|
+
const fn = globalThis.__runcode_submit;
|
|
446
|
+
if (typeof fn === 'function')
|
|
447
|
+
fn(queued);
|
|
448
|
+
}, 50);
|
|
449
|
+
}
|
|
450
|
+
break;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
},
|
|
454
|
+
};
|
|
455
|
+
globalThis.__runcode_submit = (msg) => {
|
|
456
|
+
handleSubmit(msg);
|
|
457
|
+
};
|
|
458
|
+
return () => {
|
|
459
|
+
delete globalThis.__runcode_ui;
|
|
460
|
+
delete globalThis.__runcode_submit;
|
|
461
|
+
};
|
|
462
|
+
}, [handleSubmit]);
|
|
463
|
+
// ── Model Picker ──
|
|
464
|
+
if (mode === 'model-picker') {
|
|
465
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: true, children: ['\n', " Select a model ", _jsx(Text, { dimColor: true, children: "(\u2191\u2193 navigate, Enter select, Esc cancel)" })] }), _jsx(Text, { children: " " }), PICKER_MODELS.map((m, i) => {
|
|
466
|
+
const isHighlight = 'highlight' in m && m.highlight;
|
|
467
|
+
const isSelected = i === pickerIdx;
|
|
468
|
+
const isCurrent = m.id === currentModel;
|
|
469
|
+
return (_jsxs(Box, { marginLeft: 2, children: [_jsxs(Text, { inverse: isSelected, color: isSelected ? 'cyan' : isHighlight ? 'yellow' : undefined, bold: isSelected || isHighlight, children: [' ', m.label.padEnd(26), ' '] }), _jsxs(Text, { dimColor: true, children: [" ", m.shortcut.padEnd(12)] }), _jsx(Text, { color: m.price === 'FREE' ? 'green' : isHighlight ? 'yellow' : undefined, dimColor: !isHighlight && m.price !== 'FREE', children: m.price }), isCurrent && _jsx(Text, { color: "green", children: " \u2190" })] }, m.id));
|
|
470
|
+
}), _jsx(Text, { children: " " })] }));
|
|
471
|
+
}
|
|
472
|
+
// ── Normal Mode ──
|
|
473
|
+
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 & savings"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/retry" }), " Retry the last prompt"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/compact" }), " Compress conversation history"] }), _jsx(Text, { dimColor: true, children: " \u2500\u2500 Coding \u2500\u2500" }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/test" }), " Run tests"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/fix" }), " Fix last error"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/review" }), " Code review"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/explain" }), " file Explain code"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/search" }), " query Search codebase"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/refactor" }), " desc Refactor code"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/scaffold" }), " desc Generate boilerplate"] }), _jsx(Text, { dimColor: true, children: " \u2500\u2500 Git \u2500\u2500" }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/commit" }), " Commit changes"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/push" }), " Push to remote"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/pr" }), " Create pull request"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/status" }), " Git status"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/diff" }), " Git diff"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/log" }), " Git log"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/branch" }), " [name] Branches"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/stash" }), " Stash changes"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/undo" }), " Undo last commit"] }), _jsx(Text, { dimColor: true, children: " \u2500\u2500 Analysis \u2500\u2500" }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/security" }), " Security audit"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/lint" }), " Quality check"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/optimize" }), " Performance check"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/todo" }), " Find TODOs"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/deps" }), " Dependencies"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/clean" }), " Dead code removal"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/context" }), " Session info (model, tokens, mode)"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/plan" }), " Enter plan mode (read-only tools)"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/execute" }), " Exit plan mode (enable all tools)"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/sessions" }), " List saved sessions"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/resume" }), " id Resume a saved session"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/clear" }), " Clear conversation display"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/doctor" }), " Diagnose setup issues"] }), _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: balance })] })] })), _jsx(Static, { items: completedTools, children: (tool) => (_jsx(Box, { marginLeft: 1, children: tool.error
|
|
474
|
+
? _jsxs(Text, { color: "red", children: [" \u2717 ", tool.name, " ", _jsxs(Text, { dimColor: true, children: [tool.elapsed, "ms", tool.preview ? ` — ${tool.preview}` : ''] })] })
|
|
475
|
+
: _jsxs(Text, { color: "green", children: [" \u2713 ", tool.name, " ", _jsxs(Text, { dimColor: true, children: [tool.elapsed, "ms", tool.preview ? ` — ${tool.preview}` : ''] })] }) }, tool.key)) }), _jsx(Static, { items: committedResponses, children: (r) => (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { wrap: "wrap", children: r.text }), (r.tokens.input > 0 || r.tokens.output > 0) && (_jsx(Box, { marginLeft: 1, children: _jsxs(Text, { dimColor: true, children: [r.tokens.calls > 0 && r.tokens.input === 0
|
|
476
|
+
? `${r.tokens.calls} calls`
|
|
477
|
+
: `${r.tokens.input.toLocaleString()} in / ${r.tokens.output.toLocaleString()} out${r.tokens.calls > 0 ? ` / ${r.tokens.calls} calls` : ''}`, r.cost > 0 ? ` · $${r.cost.toFixed(4)}` : ''] }) }))] }, r.key)) }), permissionRequest && (_jsxs(Box, { flexDirection: "column", marginTop: 1, marginLeft: 1, children: [_jsx(Text, { color: "yellow", children: " \u256D\u2500 Permission required \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }), _jsxs(Text, { color: "yellow", children: [" \u2502 ", _jsx(Text, { bold: true, children: permissionRequest.toolName })] }), permissionRequest.description.split('\n').map((line, i) => (_jsxs(Text, { dimColor: true, children: [" \u2502 ", line] }, i))), _jsx(Text, { color: "yellow", children: " \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }), _jsx(Box, { marginLeft: 3, children: _jsxs(Text, { children: [_jsx(Text, { bold: true, color: "green", children: "[y]" }), _jsx(Text, { dimColor: true, children: " yes " }), _jsx(Text, { bold: true, color: "cyan", children: "[a]" }), _jsx(Text, { dimColor: true, children: " always " }), _jsx(Text, { bold: true, color: "red", children: "[n]" }), _jsx(Text, { dimColor: true, children: " no" })] }) })] })), askUserRequest && (_jsxs(Box, { flexDirection: "column", marginTop: 1, marginLeft: 1, children: [_jsx(Text, { color: "cyan", children: " \u256D\u2500 Question \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }), _jsxs(Text, { color: "cyan", children: [" \u2502 ", _jsx(Text, { bold: true, children: askUserRequest.question })] }), askUserRequest.options && askUserRequest.options.length > 0 && (askUserRequest.options.map((opt, i) => (_jsxs(Text, { dimColor: true, children: [" \u2502 ", i + 1, ". ", opt] }, i)))), _jsx(Text, { color: "cyan", children: " \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }), _jsxs(Box, { marginLeft: 3, children: [_jsx(Text, { bold: true, children: "answer> " }), _jsx(TextInput, { value: askUserInput, onChange: setAskUserInput, onSubmit: (val) => {
|
|
478
|
+
const answer = val.trim() || '(no response)';
|
|
479
|
+
const r = askUserRequest.resolve;
|
|
480
|
+
setAskUserRequest(null);
|
|
481
|
+
setAskUserInput('');
|
|
482
|
+
r(answer);
|
|
483
|
+
}, focus: true })] })] })), Array.from(tools.entries()).map(([id, tool]) => (_jsxs(Box, { flexDirection: "column", marginLeft: 1, children: [_jsxs(Text, { color: "cyan", children: [' ', _jsx(Spinner, { type: "dots" }), ' ', tool.name, tool.preview ? _jsxs(Text, { dimColor: true, children: [": ", tool.preview] }) : null, _jsx(Text, { dimColor: true, children: (() => { const s = Math.round((Date.now() - tool.startTime) / 1000); return s > 0 ? ` ${s}s` : ''; })() })] }), tool.liveOutput ? (_jsxs(Text, { dimColor: true, children: [" \u2514 ", tool.liveOutput] })) : null] }, id))), thinking && (_jsxs(Box, { flexDirection: "column", marginLeft: 1, children: [_jsxs(Text, { color: "magenta", children: [" ", _jsx(Spinner, { type: "dots" }), " thinking..."] }), thinkingText && (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [" ", thinkingText.split('\n').pop()?.slice(0, 80)] }))] })), 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, { wrap: "wrap", children: streamText }) })), responsePreview && !streamText && (_jsx(Box, { flexDirection: "column", marginBottom: 0, children: _jsx(Text, { wrap: "wrap", children: responsePreview }) })), _jsx(InputBox, { input: (permissionRequest || askUserRequest) ? '' : input, setInput: (permissionRequest || askUserRequest) ? () => { } : setInput, onSubmit: (permissionRequest || askUserRequest) ? () => { } : handleSubmit, model: currentModel, balance: liveBalance, sessionCost: totalCost, queued: queuedInput || undefined, focused: !permissionRequest && !askUserRequest, busy: !askUserRequest && (waiting || thinking || tools.size > 0) })] }));
|
|
484
|
+
}
|
|
485
|
+
export function launchInkUI(opts) {
|
|
486
|
+
let resolveInput = null;
|
|
487
|
+
let pendingInput = null; // Queue for inputs that arrive before waitForInput
|
|
488
|
+
let exiting = false;
|
|
489
|
+
let abortCallback = null;
|
|
490
|
+
const instance = render(_jsx(RunCodeApp, { initialModel: opts.model, workDir: opts.workDir, walletAddress: opts.walletAddress || 'not set — run: runcode setup', walletBalance: opts.walletBalance || 'unknown', chain: opts.chain || 'base', startWithPicker: opts.showPicker, onSubmit: (value) => {
|
|
491
|
+
if (resolveInput) {
|
|
492
|
+
resolveInput(value);
|
|
493
|
+
resolveInput = null;
|
|
494
|
+
}
|
|
495
|
+
else {
|
|
496
|
+
// Agent loop hasn't called waitForInput yet — queue the input
|
|
497
|
+
pendingInput = value;
|
|
498
|
+
}
|
|
499
|
+
}, onModelChange: (model) => { opts.onModelChange?.(model); }, onAbort: () => { abortCallback?.(); }, onExit: () => {
|
|
500
|
+
exiting = true;
|
|
501
|
+
if (resolveInput) {
|
|
502
|
+
resolveInput(null);
|
|
503
|
+
resolveInput = null;
|
|
504
|
+
}
|
|
505
|
+
} }));
|
|
506
|
+
return {
|
|
507
|
+
handleEvent: (event) => {
|
|
508
|
+
const ui = globalThis.__runcode_ui;
|
|
509
|
+
ui?.handleEvent(event);
|
|
510
|
+
},
|
|
511
|
+
updateModel: (model) => {
|
|
512
|
+
const ui = globalThis.__runcode_ui;
|
|
513
|
+
ui?.updateModel(model);
|
|
514
|
+
},
|
|
515
|
+
updateBalance: (bal) => {
|
|
516
|
+
const ui = globalThis.__runcode_ui;
|
|
517
|
+
ui?.updateBalance(bal);
|
|
518
|
+
},
|
|
519
|
+
onTurnDone: (cb) => {
|
|
520
|
+
const ui = globalThis.__runcode_ui;
|
|
521
|
+
ui?.onTurnDone(cb);
|
|
522
|
+
},
|
|
523
|
+
waitForInput: () => {
|
|
524
|
+
if (exiting)
|
|
525
|
+
return Promise.resolve(null);
|
|
526
|
+
// If user already submitted while we were processing, return immediately
|
|
527
|
+
if (pendingInput !== null) {
|
|
528
|
+
const input = pendingInput;
|
|
529
|
+
pendingInput = null;
|
|
530
|
+
return Promise.resolve(input);
|
|
531
|
+
}
|
|
532
|
+
return new Promise((resolve) => { resolveInput = resolve; });
|
|
533
|
+
},
|
|
534
|
+
onAbort: (cb) => { abortCallback = cb; },
|
|
535
|
+
cleanup: () => { instance.unmount(); },
|
|
536
|
+
requestPermission: (toolName, description) => {
|
|
537
|
+
const ui = globalThis.__runcode_ui;
|
|
538
|
+
return ui?.requestPermission(toolName, description) ?? Promise.resolve('no');
|
|
539
|
+
},
|
|
540
|
+
requestAskUser: (question, options) => {
|
|
541
|
+
const ui = globalThis.__runcode_ui;
|
|
542
|
+
return ui?.requestAskUser(question, options) ?? Promise.resolve('(no response)');
|
|
543
|
+
},
|
|
544
|
+
};
|
|
545
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive model picker for runcode.
|
|
3
|
+
* Shows categorized model list, supports shortcuts and arrow-key selection.
|
|
4
|
+
*/
|
|
5
|
+
export declare const MODEL_SHORTCUTS: Record<string, string>;
|
|
6
|
+
/**
|
|
7
|
+
* Resolve a model name — supports shortcuts.
|
|
8
|
+
*/
|
|
9
|
+
export declare function resolveModel(input: string): string;
|
|
10
|
+
/**
|
|
11
|
+
* Show interactive model picker. Returns the selected model ID.
|
|
12
|
+
* Falls back to text input if terminal doesn't support raw mode.
|
|
13
|
+
*/
|
|
14
|
+
export declare function pickModel(currentModel?: string): Promise<string | null>;
|