openaxies 1.0.1 → 1.0.3

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 (3) hide show
  1. package/bin/openaxis.js +2 -51
  2. package/package.json +1 -1
  3. package/src/App.js +284 -146
package/bin/openaxis.js CHANGED
@@ -1,58 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import React from 'react';
3
3
  import { render } from 'ink';
4
- import fs from 'fs';
5
- import path from 'path';
6
4
  import App from '../src/App.js';
7
5
 
8
6
  const h = React.createElement;
9
7
 
10
- const debugBuffer = [];
11
-
12
- function sandboxWrite(chunk) {
13
- const str = typeof chunk === 'string' ? chunk : String(chunk);
14
- const trimmed = str.trim();
15
- if (trimmed.length > 0) {
16
- debugBuffer.push(trimmed);
17
- }
18
- return true;
19
- }
20
-
21
- const consoleKeys = ['log', 'error', 'warn'];
22
- for (let i = 0; i < consoleKeys.length; i++) {
23
- const key = consoleKeys[i];
24
- const original = console[key];
25
- console[key] = function sandboxedConsole() {
26
- const args = [];
27
- for (let j = 0; j < arguments.length; j++) {
28
- args.push(arguments[j]);
29
- }
30
- sandboxWrite(args.join(' '));
31
- return original.apply(console, args);
32
- };
33
- }
34
-
35
- const { waitUntilExit } = render(h(App, {}));
36
-
37
- function cleanup() {
38
- if (debugBuffer.length > 0) {
39
- const logPath = path.join(process.cwd(), 'openaxies-debug.log');
40
- try {
41
- fs.writeFileSync(logPath, debugBuffer.join('\n') + '\n');
42
- } catch (_) {
43
- }
44
- }
45
- process.exit(0);
46
- }
47
-
48
- process.on('SIGINT', cleanup);
49
- process.on('SIGTERM', cleanup);
50
- process.on('exit', function () {
51
- if (debugBuffer.length > 0) {
52
- const logPath = path.join(process.cwd(), 'openaxies-debug.log');
53
- try {
54
- fs.writeFileSync(logPath, debugBuffer.join('\n') + '\n');
55
- } catch (_) {
56
- }
57
- }
58
- });
8
+ const { waitUntilExit } = render(h(App, {}), { exitOnCtrlC: false });
9
+ waitUntilExit();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openaxies",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
package/src/App.js CHANGED
@@ -1,16 +1,13 @@
1
1
  import React from 'react';
2
- import { Box, Text, useStdout } from 'ink';
2
+ import { Box, Text, useInput, useStdout, useApp } from 'ink';
3
3
  import TextInput from 'ink-text-input';
4
4
  import { hex } from './config/theme.js';
5
5
  import { getModels, getModelById, DEFAULT_MODEL_ID } from './config/models.js';
6
6
  import { callModel } from './providers/index.js';
7
- import { Typography } from './components/ui/Typography.js';
8
- import { Separator } from './components/ui/Separator.js';
9
- import { ActivityItem } from './components/timeline/ActivityItem.js';
10
7
 
11
8
  const h = React.createElement;
12
9
 
13
- const STARTUP_LOGO = [
10
+ const LOGO = [
14
11
  ' \u2588\u2588\u2588\u2588\u2588\u2588\u2557',
15
12
  ' \u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557',
16
13
  ' \u2588\u2588\u2551 \u2588\u2588\u2551',
@@ -18,177 +15,318 @@ const STARTUP_LOGO = [
18
15
  ' \u255a\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255d',
19
16
  ];
20
17
 
18
+ const COMMANDS = [
19
+ { id: 'model', trigger: '/model', desc: 'Switch model' },
20
+ { id: 'thoughts', trigger: '/thoughts', desc: 'Toggle thinking' },
21
+ { id: 'resume', trigger: '/resume', desc: 'Re-run last query' },
22
+ { id: 'clear', trigger: '/clear', desc: 'Clear conversation' },
23
+ { id: 'help', trigger: '/help', desc: 'Show commands' },
24
+ { id: 'exit', trigger: '/exit', desc: 'Quit' },
25
+ ];
26
+
21
27
  export default function AppRoot() {
22
28
  const { stdout } = useStdout();
23
- const rows = stdout.rows || 24;
29
+ const { exit } = useApp();
24
30
  const cols = stdout.columns || 80;
31
+ const rows = stdout.rows || 24;
25
32
 
26
33
  const [messages, setMessages] = React.useState([]);
27
34
  const [activeModel, setActiveModel] = React.useState(DEFAULT_MODEL_ID);
28
- const [inputBuffer, setInputBuffer] = React.useState('');
29
- const [streamingActive, setStreamingActive] = React.useState(false);
30
- const [streamContent, setStreamContent] = React.useState('');
31
- const [thoughtContent, setThoughtContent] = React.useState('');
32
- const [thinkingElapsed, setThinkingElapsed] = React.useState(0);
33
- const [isThinking, setIsThinking] = React.useState(false);
35
+ const [input, setInput] = React.useState('');
36
+ const [streaming, setStreaming] = React.useState(false);
37
+ const [streamText, setStreamText] = React.useState('');
38
+ const [thinkingActive, setThinkingActive] = React.useState(false);
39
+ const [thinkingText, setThinkingText] = React.useState('');
40
+ const [thinkTime, setThinkTime] = React.useState(0);
34
41
  const [startupDone, setStartupDone] = React.useState(false);
42
+ const [showOverlay, setShowOverlay] = React.useState(false);
43
+ const [overlayIdx, setOverlayIdx] = React.useState(0);
44
+ const [showThought, setShowThought] = React.useState(true);
45
+ const [toolEv, setToolEv] = React.useState(null);
35
46
 
36
- const abortControllerRef = React.useRef(null);
47
+ const abortRef = React.useRef(null);
48
+ const accumRef = React.useRef('');
49
+ const lastQRef = React.useRef('');
37
50
 
38
51
  React.useEffect(() => {
39
- let timer;
40
- if (isThinking) {
41
- timer = setInterval(() => {
42
- setThinkingElapsed(prev => prev + 0.1);
43
- }, 100);
44
- }
45
- return () => clearInterval(timer);
46
- }, [isThinking]);
52
+ if (!thinkingActive) return;
53
+ const t = setInterval(() => setThinkTime(p => p + 0.1), 100);
54
+ return () => clearInterval(t);
55
+ }, [thinkingActive]);
47
56
 
48
57
  const model = getModelById(activeModel);
49
- const modelLabel = model ? model.label : 'Unknown Model';
58
+ const label = model ? model.label.replace('OpenAxies ', '') : 'llama';
59
+ const W = Math.min(cols - 4, 56);
60
+
61
+ function strip(t) {
62
+ return showThought ? t : t.split('<think>').join('').split('</think>').join('');
63
+ }
50
64
 
51
65
  async function handleSubmit(text) {
52
- if (!text || streamingActive) return;
66
+ const s = typeof text === 'string' ? text.trim() : '';
67
+ if (!s || streaming) return;
53
68
  setStartupDone(true);
54
-
55
- const userMsg = { type: 'user', content: text };
56
- setMessages(prev => [...prev, userMsg]);
57
- setInputBuffer('');
58
- setStreamingActive(true);
59
- setStreamContent('');
60
- setThoughtContent('');
61
- setThinkingElapsed(0);
62
-
63
- abortControllerRef.current = new AbortController();
64
-
69
+ lastQRef.current = s;
70
+ accumRef.current = '';
71
+ setStreaming(true);
72
+ setStreamText('');
73
+ setThinkingText('');
74
+ setThinkTime(0);
75
+ setToolEv(null);
76
+ setMessages(p => [...p, { role: 'user', content: s }]);
77
+ setInput('');
78
+
79
+ const m = getModelById(activeModel);
80
+ if (!m) { setMessages(p => [...p, { role: 'assistant', content: 'No model loaded.' }]); setStreaming(false); return; }
81
+
82
+ const ac = new AbortController();
83
+ abortRef.current = ac;
84
+ let thinkOpen = false;
85
+
65
86
  try {
66
- const history = messages.map(m => ({
67
- role: m.type === 'user' ? 'user' : 'assistant',
68
- content: m.content
87
+ const history = messages.map(msg => ({
88
+ role: msg.role === 'user' ? 'user' : 'assistant',
89
+ content: msg.content || '',
69
90
  }));
70
- history.push({ role: 'user', content: text });
71
-
72
- const stream = callModel({ id: activeModel }, history, abortControllerRef.current.signal);
73
-
74
- for await (const event of stream) {
75
- if (event.type === 'tool') {
76
- setMessages(prev => [...prev, { type: 'tool', tool: event.tool, query: event.query }]);
77
- } else if (event.type === 'token') {
78
- const content = event.content;
79
- if (content.includes('<think>')) {
80
- setIsThinking(true);
81
- setThoughtContent(prev => prev + content.replace('<think>', ''));
82
- } else if (content.includes('</think>')) {
83
- setIsThinking(false);
84
- const finalThought = thoughtContent + content.replace('</think>', '');
85
- setMessages(prev => [...prev, { type: 'thought', content: finalThought, elapsed: thinkingElapsed.toFixed(1) + 's' }]);
86
- setThoughtContent('');
87
- } else if (isThinking) {
88
- setThoughtContent(prev => prev + content);
91
+ history.push({ role: 'user', content: s });
92
+
93
+ for await (const ev of callModel({ id: activeModel }, history, ac.signal)) {
94
+ if (ac.signal.aborted) break;
95
+
96
+ if (ev.type === 'tool') {
97
+ setToolEv({ tool: ev.tool, query: ev.query, sites: ev.sites });
98
+ continue;
99
+ }
100
+
101
+ if (ev.type === 'token' || ev.type === 'thinking') {
102
+ const c = typeof ev.content === 'string' ? ev.content : '';
103
+ if (!c) continue;
104
+ if (c.includes('<think>')) thinkOpen = true;
105
+ accumRef.current += c;
106
+ const clean = strip(accumRef.current);
107
+ if (thinkOpen || accumRef.current.includes('<think>')) {
108
+ const still = thinkOpen || !accumRef.current.includes('</think>');
109
+ if (still) {
110
+ setThinkingActive(true);
111
+ setThinkingText(clean);
112
+ setStreamText('');
113
+ } else {
114
+ thinkOpen = false;
115
+ setThinkingActive(false);
116
+ setStreamText(clean);
117
+ setThinkingText('');
118
+ }
89
119
  } else {
90
- setStreamContent(prev => prev + content);
120
+ setStreamText(clean);
91
121
  }
92
122
  }
123
+ if (ev.type === 'done' || ev.type === 'timeout') {
124
+ if (ev.type === 'timeout' && accumRef.current) accumRef.current += '\n\u2014timed out\u2014';
125
+ break;
126
+ }
93
127
  }
94
- } catch (e) {
95
- setMessages(prev => [...prev, { type: 'assistant', content: 'Error connecting to model.' }]);
128
+ } catch (err) {
129
+ if (!ac.signal.aborted) setMessages(p => [...p, { role: 'assistant', content: 'Error: ' + (err.message || 'connection failed') }]);
96
130
  } finally {
97
- setStreamingActive(false);
98
- setIsThinking(false);
99
- if (streamContent) {
100
- setMessages(prev => [...prev, { type: 'assistant', content: streamContent }]);
101
- }
131
+ setStreaming(false);
132
+ setThinkingActive(false);
133
+ abortRef.current = null;
134
+ const final = accumRef.current;
135
+ if (final) setMessages(p => [...p, { role: 'assistant', content: strip(final) }]);
136
+ setStreamText('');
137
+ setThinkingText('');
102
138
  }
103
139
  }
104
140
 
105
- const renderHeader = () => {
106
- return h(Box, { height: 1, paddingLeft: 1, paddingRight: 1 },
107
- h(Typography.Header, 'OpenAxies'),
108
- h(Typography.Muted, ` \u2022 ${modelLabel}`),
109
- h(Box, { flexGrow: 1 }),
110
- h(Typography.Dim, 'local')
111
- );
112
- };
113
-
114
- const renderFooter = () => {
115
- return h(Box, {
116
- height: 1,
117
- paddingLeft: 1,
118
- paddingRight: 1,
119
- flexDirection: 'row',
120
- alignItems: 'center'
121
- },
122
- h(Typography.Dim, 'esc interrupt \u2022 ctrl+t thoughts \u2022 ctrl+p models \u2022 ctrl+k commands'),
123
- h(Box, { flexGrow: 1 }),
124
- h(Typography.Muted, `${modelLabel.toLowerCase()} \u2022 local`)
125
- );
126
- };
127
-
128
- const renderStartup = () => {
129
- return h(Box, {
130
- flexDirection: 'column',
131
- paddingLeft: 1,
132
- paddingTop: 2,
133
- backgroundColor: '#000000'
134
- },
135
- ...STARTUP_LOGO.map(line => h(Typography.Accent, line)),
141
+ function cycleModel() {
142
+ const models = getModels();
143
+ setActiveModel(p => {
144
+ let f = false;
145
+ for (const m of models) { if (f) return m.id; if (m.id === p) f = true; }
146
+ return models[0].id;
147
+ });
148
+ }
149
+
150
+ function execCmd(item) {
151
+ if (!item) return;
152
+ const t = item.trigger;
153
+ if (t === '/exit') { exit(); return; }
154
+ if (t === '/clear') setMessages([]);
155
+ if (t === '/model') cycleModel();
156
+ if (t === '/thoughts') setShowThought(p => !p);
157
+ if (t === '/resume') { const q = lastQRef.current; if (q && !streaming) handleSubmit(q); return; }
158
+ if (t === '/help') setMessages(p => [...p, { role: 'assistant', content: 'Commands: /help /clear /model /thoughts /resume /exit' }]);
159
+ setInput(''); setShowOverlay(false); setOverlayIdx(0);
160
+ }
161
+
162
+ useInput((inp, key) => {
163
+ if (showOverlay) {
164
+ if (key.escape) { setShowOverlay(false); setInput(''); setOverlayIdx(0); return; }
165
+ if (key.upArrow) { setOverlayIdx(p => p > 0 ? p - 1 : COMMANDS.length - 1); return; }
166
+ if (key.downArrow) { setOverlayIdx(p => p < COMMANDS.length - 1 ? p + 1 : 0); return; }
167
+ if (key.return) { execCmd(COMMANDS[overlayIdx]); return; }
168
+ return;
169
+ }
170
+ if (!startupDone && !streaming) {
171
+ if (key.return) { setStartupDone(true); setInput(''); return; }
172
+ return;
173
+ }
174
+ if (key.escape && streaming) {
175
+ if (abortRef.current) abortRef.current.abort();
176
+ setStreaming(false); setStreamText(''); setThinkingActive(false);
177
+ return;
178
+ }
179
+ if (key.ctrl) {
180
+ if (inp === 'c' || inp === 'C') { exit(); return; }
181
+ if (inp === 't' || inp === 'T') { setShowThought(p => !p); return; }
182
+ if (inp === 'p' || inp === 'P') { cycleModel(); return; }
183
+ if (inp === 'r' || inp === 'R') { const q = lastQRef.current; if (q && !streaming) handleSubmit(q); return; }
184
+ if (inp === 'l' || inp === 'L') { setMessages([]); return; }
185
+ if (inp === 'k' || inp === 'K') { setInput('/'); setShowOverlay(true); setOverlayIdx(0); return; }
186
+ }
187
+ });
188
+
189
+ // Build entries
190
+ const entries = [];
191
+ for (const msg of messages) {
192
+ if (msg.role === 'user' || msg.role === 'assistant') entries.push(msg);
193
+ }
194
+
195
+ if (toolEv && streaming) entries.push({ role: 'tool', tool: toolEv.tool, query: toolEv.query, sites: toolEv.sites });
196
+ if (thinkingActive && thinkingText) entries.push({ role: 'thought', content: thinkingText, time: thinkTime.toFixed(1) + 's' });
197
+ if (streaming && streamText && !thinkingActive) entries.push({ role: 'ongoing', content: streamText });
198
+
199
+ const MAX_CHAT = Math.max(3, rows - 10);
200
+ const vis = entries.slice(-MAX_CHAT);
201
+
202
+ // Startup screen
203
+ if (!startupDone && messages.length === 0) {
204
+ return h(Box, { flexDirection: 'column', width: '100%', height: rows, overflow: 'hidden' },
205
+ h(Box, { height: 2 }),
206
+ ...LOGO.map((l, i) => h(Box, { key: 'l' + i, height: 1, paddingLeft: 2 },
207
+ h(Text, { color: '#00BBFF', bold: true }, l)
208
+ )),
136
209
  h(Box, { height: 1 }),
137
- h(Typography.Header, 'OpenAxies'),
138
- h(Typography.Muted, 'local ai agent runtime'),
210
+ h(Box, { height: 1, paddingLeft: 2 }, h(Text, { color: '#FFFFFF', bold: true }, 'OpenAxies')),
211
+ h(Box, { height: 1, paddingLeft: 2 }, h(Text, { color: '#777788' }, 'local agent')),
139
212
  h(Box, { height: 2 }),
140
- h(Typography.Dim, 'Press Enter to begin')
213
+ h(Box, { height: 1, paddingLeft: 2 }, h(Text, { color: '#555577' }, 'Press Enter to begin')),
214
+ h(Box, { flexGrow: 1 }),
215
+ h(Box, { height: 1, paddingLeft: 2 }, h(Text, { color: '#333344' }, 'esc interrupt ctrl+p models /help')),
141
216
  );
142
- };
217
+ }
143
218
 
144
- const renderTimeline = () => {
145
- const events = [...messages];
146
- if (isThinking) {
147
- events.push({ type: 'thought', content: thoughtContent, elapsed: thinkingElapsed.toFixed(1) + 's' });
148
- }
149
- if (streamingActive && streamContent) {
150
- events.push({ type: 'assistant', content: streamContent });
219
+ // Bordered header
220
+ const borderT = '\u250C' + '\u2500'.repeat(W + 2) + '\u2510';
221
+ const borderB = '\u2514' + '\u2500'.repeat(W + 2) + '\u2518';
222
+ const pad = '\u2502' + ' '.repeat(W + 2) + '\u2502';
223
+
224
+ function brdr(line) {
225
+ return '\u2502 ' + line + ' '.repeat(Math.max(0, W + 1 - line.length)) + '\u2502';
226
+ }
227
+
228
+ // Logo combined with info text
229
+ const line1 = brdr(' ' + LOGO[0] + ' OpenAxies');
230
+ const line2 = brdr(' ' + LOGO[1] + ' model: ' + label);
231
+ const line3 = brdr(' ' + LOGO[2] + ' dir: ~');
232
+ const line4 = brdr(' ' + LOGO[3] + ' cmds: /model /help /exit');
233
+ const line5 = brdr(' ' + LOGO[4]);
234
+
235
+ const header = h(Box, { flexShrink: 0, flexDirection: 'column', paddingLeft: 1, paddingRight: 1 },
236
+ h(Text, { color: '#555577' }, borderT),
237
+ h(Text, { color: '#555577' }, pad),
238
+ h(Text, { color: '#555577' }, line1),
239
+ h(Text, { color: '#555577' }, line2),
240
+ h(Text, { color: '#555577' }, line3),
241
+ h(Text, { color: '#555577' }, line4),
242
+ h(Text, { color: '#555577' }, line5),
243
+ h(Text, { color: '#555577' }, pad),
244
+ h(Text, { color: '#555577' }, borderB),
245
+ );
246
+
247
+ // Overlay
248
+ if (showOverlay) {
249
+ vis.unshift({ role: '_ocmd', idx: overlayIdx });
250
+ }
251
+
252
+ // Empty state
253
+ if (vis.length === 0) {
254
+ vis.push({ role: '_sep' });
255
+ vis.push({ role: '_idle' });
256
+ }
257
+
258
+ // Render entries
259
+ const els = [];
260
+ for (let i = 0; i < vis.length; i++) {
261
+ const e = vis[i];
262
+ const k = 'e' + i;
263
+
264
+ if (e.role === '_idle') {
265
+ els.push(h(Box, { key: k, height: 1, paddingLeft: 2 }, h(Text, { color: '#00FF88', bold: true }, '\u25CB Ready')));
266
+ } else if (e.role === '_ocmd') {
267
+ els.push(h(Box, { key: k + 'h', height: 1, paddingLeft: 2 }, h(Text, { color: '#888899', bold: true }, 'Commands:')));
268
+ for (let ci = 0; ci < COMMANDS.length; ci++) {
269
+ const c = COMMANDS[ci];
270
+ const sel = ci === e.idx;
271
+ els.push(h(Box, { key: k + 'c' + ci, height: 1, paddingLeft: 3 },
272
+ h(Text, { color: sel ? hex.neonBlue : '#666688' }, (sel ? '\u276F ' : ' ') + c.trigger),
273
+ h(Text, { color: '#444466' }, ' ' + (c.desc || ''))
274
+ ));
275
+ }
276
+ } else if (e.role === 'user') {
277
+ els.push(h(Box, { key: k, height: 1, paddingLeft: 2 },
278
+ h(Text, { color: '#888899' }, '\u203A '),
279
+ h(Text, { color: hex.neonBlue, wrap: 'wrap' }, e.content),
280
+ ));
281
+ } else if (e.role === 'assistant' || e.role === 'ongoing') {
282
+ els.push(h(Box, { key: k, height: 1, paddingLeft: 2 },
283
+ h(Text, { color: '#88FF88' }, '\u2022 '),
284
+ h(Text, { color: '#FFFFFF', wrap: 'wrap' }, e.content),
285
+ ));
286
+ } else if (e.role === 'thought') {
287
+ els.push(h(Box, { key: k, height: 1, paddingLeft: 2 },
288
+ h(Text, { color: '#FF9F43', bold: true }, '\u2022 Thinking \u2022 ' + e.time),
289
+ ));
290
+ if (e.content) {
291
+ els.push(h(Box, { key: k + 'c', height: 1, paddingLeft: 4 },
292
+ h(Text, { color: '#FF9F43', wrap: 'wrap' }, e.content),
293
+ ));
294
+ }
295
+ } else if (e.role === 'tool') {
296
+ els.push(h(Box, { key: k, height: 1, paddingLeft: 2 },
297
+ h(Text, { color: '#5B5B8A' }, '\u2022 ' + (e.tool || 'tool') + (e.query ? ' "' + e.query + '"' : '') + (e.sites ? ' (' + e.sites + ' sites)' : '')),
298
+ ));
151
299
  }
300
+ }
152
301
 
153
- return h(Box, {
154
- flexGrow: 1,
155
- flexDirection: 'column',
156
- overflow: 'hidden',
157
- paddingLeft: 0,
158
- paddingRight: 0,
159
- },
160
- ...events.slice(-15).map((event, i) => h(ActivityItem, { key: i, event }))
161
- );
162
- };
163
-
164
- return h(Box, {
165
- flexDirection: 'column',
166
- width: '100%',
167
- height: rows,
168
- overflow: 'hidden'
169
- },
170
- !startupDone ? renderStartup() : null,
171
- startupDone && renderHeader(),
172
- startupDone && h(Separator),
173
- startupDone && renderTimeline(),
174
- startupDone && h(Box, { height: 1 }, h(Separator)),
175
- startupDone && h(Box, {
176
- height: 1,
177
- paddingLeft: 1,
178
- paddingRight: 1,
179
- flexDirection: 'row',
180
- alignItems: 'center'
181
- },
182
- h(Typography.Accent, '▌ Ask OpenAxies'),
183
- h(Box, { flexGrow: 1 }),
184
- h(TextInput, {
185
- value: inputBuffer,
186
- onChange: setInputBuffer,
187
- onSubmit: handleSubmit,
188
- focus: true,
189
- showCursor: true
190
- })
191
- ),
192
- startupDone && renderFooter()
302
+ const chat = h(Box, { flexGrow: 1, flexDirection: 'column', overflow: 'hidden' }, ...els);
303
+
304
+ // Input
305
+ const inputLine = h(Box, { height: 1, flexShrink: 0, paddingLeft: 2, paddingRight: 2 },
306
+ h(Text, { color: '#888899' }, '\u203A '),
307
+ h(Text, { color: hex.greenOnline }, '\u258C'),
308
+ h(Box, { width: 1 }),
309
+ h(TextInput, {
310
+ value: input,
311
+ onChange: setInput,
312
+ onSubmit: v => { if (typeof v === 'string' && v.trim() && !streaming) handleSubmit(v); },
313
+ placeholder: '',
314
+ focus: !showOverlay,
315
+ showCursor: true,
316
+ })
317
+ );
318
+
319
+ // Footer bar
320
+ const footer = h(Box, { height: 1, flexShrink: 0, paddingLeft: 2, paddingRight: 2 },
321
+ h(Text, { color: '#444466' }, 'esc \u2248 ctrl+t ctrl+p ctrl+k'),
322
+ h(Box, { flexGrow: 1 }),
323
+ h(Text, { color: '#555577' }, label.toLowerCase() + ' \u2022 local'),
324
+ );
325
+
326
+ return h(Box, { flexDirection: 'column', width: '100%', height: rows, overflow: 'hidden' },
327
+ header,
328
+ chat,
329
+ inputLine,
330
+ footer,
193
331
  );
194
332
  }