@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.
Files changed (138) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +256 -0
  3. package/dist/agent/commands.d.ts +27 -0
  4. package/dist/agent/commands.js +659 -0
  5. package/dist/agent/compact.d.ts +31 -0
  6. package/dist/agent/compact.js +366 -0
  7. package/dist/agent/context.d.ts +11 -0
  8. package/dist/agent/context.js +184 -0
  9. package/dist/agent/error-classifier.d.ts +10 -0
  10. package/dist/agent/error-classifier.js +61 -0
  11. package/dist/agent/llm.d.ts +63 -0
  12. package/dist/agent/llm.js +448 -0
  13. package/dist/agent/loop.d.ts +12 -0
  14. package/dist/agent/loop.js +346 -0
  15. package/dist/agent/optimize.d.ts +53 -0
  16. package/dist/agent/optimize.js +262 -0
  17. package/dist/agent/permissions.d.ts +39 -0
  18. package/dist/agent/permissions.js +226 -0
  19. package/dist/agent/reduce.d.ts +49 -0
  20. package/dist/agent/reduce.js +317 -0
  21. package/dist/agent/streaming-executor.d.ts +36 -0
  22. package/dist/agent/streaming-executor.js +149 -0
  23. package/dist/agent/tokens.d.ts +53 -0
  24. package/dist/agent/tokens.js +185 -0
  25. package/dist/agent/types.d.ts +125 -0
  26. package/dist/agent/types.js +5 -0
  27. package/dist/banner.d.ts +1 -0
  28. package/dist/banner.js +27 -0
  29. package/dist/commands/balance.d.ts +1 -0
  30. package/dist/commands/balance.js +40 -0
  31. package/dist/commands/config.d.ts +14 -0
  32. package/dist/commands/config.js +107 -0
  33. package/dist/commands/daemon.d.ts +3 -0
  34. package/dist/commands/daemon.js +117 -0
  35. package/dist/commands/history.d.ts +5 -0
  36. package/dist/commands/history.js +31 -0
  37. package/dist/commands/init.d.ts +3 -0
  38. package/dist/commands/init.js +92 -0
  39. package/dist/commands/logs.d.ts +5 -0
  40. package/dist/commands/logs.js +89 -0
  41. package/dist/commands/models.d.ts +1 -0
  42. package/dist/commands/models.js +56 -0
  43. package/dist/commands/plugin.d.ts +14 -0
  44. package/dist/commands/plugin.js +176 -0
  45. package/dist/commands/proxy.d.ts +13 -0
  46. package/dist/commands/proxy.js +106 -0
  47. package/dist/commands/setup.d.ts +1 -0
  48. package/dist/commands/setup.js +49 -0
  49. package/dist/commands/start.d.ts +8 -0
  50. package/dist/commands/start.js +292 -0
  51. package/dist/commands/stats.d.ts +10 -0
  52. package/dist/commands/stats.js +94 -0
  53. package/dist/commands/uninit.d.ts +1 -0
  54. package/dist/commands/uninit.js +63 -0
  55. package/dist/config.d.ts +9 -0
  56. package/dist/config.js +41 -0
  57. package/dist/index.d.ts +2 -0
  58. package/dist/index.js +179 -0
  59. package/dist/mcp/client.d.ts +44 -0
  60. package/dist/mcp/client.js +147 -0
  61. package/dist/mcp/config.d.ts +20 -0
  62. package/dist/mcp/config.js +138 -0
  63. package/dist/plugin-sdk/channel.d.ts +100 -0
  64. package/dist/plugin-sdk/channel.js +10 -0
  65. package/dist/plugin-sdk/index.d.ts +14 -0
  66. package/dist/plugin-sdk/index.js +9 -0
  67. package/dist/plugin-sdk/plugin.d.ts +87 -0
  68. package/dist/plugin-sdk/plugin.js +7 -0
  69. package/dist/plugin-sdk/search.d.ts +13 -0
  70. package/dist/plugin-sdk/search.js +4 -0
  71. package/dist/plugin-sdk/tracker.d.ts +27 -0
  72. package/dist/plugin-sdk/tracker.js +5 -0
  73. package/dist/plugin-sdk/workflow.d.ts +126 -0
  74. package/dist/plugin-sdk/workflow.js +11 -0
  75. package/dist/plugins/registry.d.ts +33 -0
  76. package/dist/plugins/registry.js +155 -0
  77. package/dist/plugins/runner.d.ts +21 -0
  78. package/dist/plugins/runner.js +453 -0
  79. package/dist/plugins-bundled/social/index.d.ts +10 -0
  80. package/dist/plugins-bundled/social/index.js +363 -0
  81. package/dist/plugins-bundled/social/plugin.json +14 -0
  82. package/dist/plugins-bundled/social/prompts.d.ts +19 -0
  83. package/dist/plugins-bundled/social/prompts.js +67 -0
  84. package/dist/plugins-bundled/social/types.d.ts +58 -0
  85. package/dist/plugins-bundled/social/types.js +16 -0
  86. package/dist/pricing.d.ts +21 -0
  87. package/dist/pricing.js +91 -0
  88. package/dist/proxy/fallback.d.ts +38 -0
  89. package/dist/proxy/fallback.js +144 -0
  90. package/dist/proxy/server.d.ts +18 -0
  91. package/dist/proxy/server.js +576 -0
  92. package/dist/proxy/sse-translator.d.ts +29 -0
  93. package/dist/proxy/sse-translator.js +270 -0
  94. package/dist/router/index.d.ts +22 -0
  95. package/dist/router/index.js +269 -0
  96. package/dist/session/search.d.ts +33 -0
  97. package/dist/session/search.js +229 -0
  98. package/dist/session/storage.d.ts +48 -0
  99. package/dist/session/storage.js +173 -0
  100. package/dist/stats/insights.d.ts +55 -0
  101. package/dist/stats/insights.js +195 -0
  102. package/dist/stats/tracker.d.ts +54 -0
  103. package/dist/stats/tracker.js +165 -0
  104. package/dist/tools/askuser.d.ts +6 -0
  105. package/dist/tools/askuser.js +76 -0
  106. package/dist/tools/bash.d.ts +5 -0
  107. package/dist/tools/bash.js +336 -0
  108. package/dist/tools/edit.d.ts +5 -0
  109. package/dist/tools/edit.js +148 -0
  110. package/dist/tools/glob.d.ts +5 -0
  111. package/dist/tools/glob.js +158 -0
  112. package/dist/tools/grep.d.ts +5 -0
  113. package/dist/tools/grep.js +194 -0
  114. package/dist/tools/imagegen.d.ts +6 -0
  115. package/dist/tools/imagegen.js +172 -0
  116. package/dist/tools/index.d.ts +17 -0
  117. package/dist/tools/index.js +30 -0
  118. package/dist/tools/read.d.ts +11 -0
  119. package/dist/tools/read.js +90 -0
  120. package/dist/tools/subagent.d.ts +5 -0
  121. package/dist/tools/subagent.js +116 -0
  122. package/dist/tools/task.d.ts +5 -0
  123. package/dist/tools/task.js +91 -0
  124. package/dist/tools/webfetch.d.ts +5 -0
  125. package/dist/tools/webfetch.js +166 -0
  126. package/dist/tools/websearch.d.ts +5 -0
  127. package/dist/tools/websearch.js +103 -0
  128. package/dist/tools/write.d.ts +5 -0
  129. package/dist/tools/write.js +114 -0
  130. package/dist/ui/app.d.ts +26 -0
  131. package/dist/ui/app.js +545 -0
  132. package/dist/ui/model-picker.d.ts +14 -0
  133. package/dist/ui/model-picker.js +161 -0
  134. package/dist/ui/terminal.d.ts +35 -0
  135. package/dist/ui/terminal.js +337 -0
  136. package/dist/wallet/manager.d.ts +10 -0
  137. package/dist/wallet/manager.js +23 -0
  138. 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>;