openaxies 1.0.0 → 1.0.2

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 +4 -4
  2. package/package.json +1 -1
  3. package/src/App.js +318 -143
package/bin/openaxis.js CHANGED
@@ -33,14 +33,15 @@ for (let i = 0; i < consoleKeys.length; i++) {
33
33
  }
34
34
 
35
35
  const { waitUntilExit } = render(h(App, {}));
36
+ // Keep the process alive until the Ink UI signals exit (e.g., via SIGINT).
37
+ waitUntilExit();
36
38
 
37
39
  function cleanup() {
38
40
  if (debugBuffer.length > 0) {
39
41
  const logPath = path.join(process.cwd(), 'openaxies-debug.log');
40
42
  try {
41
43
  fs.writeFileSync(logPath, debugBuffer.join('\n') + '\n');
42
- } catch (_) {
43
- }
44
+ } catch (_) {}
44
45
  }
45
46
  process.exit(0);
46
47
  }
@@ -52,7 +53,6 @@ process.on('exit', function () {
52
53
  const logPath = path.join(process.cwd(), 'openaxies-debug.log');
53
54
  try {
54
55
  fs.writeFileSync(logPath, debugBuffer.join('\n') + '\n');
55
- } catch (_) {
56
- }
56
+ } catch (_) {}
57
57
  }
58
58
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openaxies",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
package/src/App.js CHANGED
@@ -1,13 +1,9 @@
1
1
  import React from 'react';
2
- import { Box, Text, useStdout } from 'ink';
2
+ import { Box, Text, useInput, useStdout } 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
- import { TodoTracker } from './components/timeline/TodoTracker.js';
11
7
 
12
8
  const h = React.createElement;
13
9
 
@@ -19,6 +15,15 @@ const STARTUP_LOGO = [
19
15
  ' \u255a\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255d',
20
16
  ];
21
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
+
22
27
  export default function AppRoot() {
23
28
  const { stdout } = useStdout();
24
29
  const rows = stdout.rows || 24;
@@ -26,166 +31,336 @@ export default function AppRoot() {
26
31
 
27
32
  const [messages, setMessages] = React.useState([]);
28
33
  const [activeModel, setActiveModel] = React.useState(DEFAULT_MODEL_ID);
29
- const [inputBuffer, setInputBuffer] = React.useState('');
30
- const [streamingActive, setStreamingActive] = React.useState(false);
31
- const [streamContent, setStreamContent] = React.useState('');
32
- const [thoughtContent, setThoughtContent] = React.useState('');
33
- const [thinkingElapsed, setThinkingElapsed] = React.useState(0);
34
- const [isThinking, setIsThinking] = React.useState(false);
34
+ const [input, setInput] = React.useState('');
35
+ const [streaming, setStreaming] = React.useState(false);
36
+ const [streamText, setStreamText] = React.useState('');
37
+ const [thinkingActive, setThinkingActive] = React.useState(false);
38
+ const [thinkingText, setThinkingText] = React.useState('');
39
+ const [thinkElapsed, setThinkElapsed] = React.useState(0);
35
40
  const [startupDone, setStartupDone] = React.useState(false);
36
- const [todos, setTodos] = React.useState([]);
41
+ const [showOverlay, setShowOverlay] = React.useState(false);
42
+ const [overlayIdx, setOverlayIdx] = React.useState(0);
43
+ const [showThought, setShowThought] = React.useState(true);
44
+ const [toolEvent, setToolEvent] = React.useState(null);
37
45
 
38
- const abortControllerRef = React.useRef(null);
46
+ const abortRef = React.useRef(null);
47
+ const accumulatedRef = React.useRef('');
48
+ const lastQueryRef = React.useRef('');
39
49
 
40
50
  React.useEffect(() => {
41
- let timer;
42
- if (isThinking) {
43
- timer = setInterval(() => {
44
- setThinkingElapsed(prev => prev + 0.1);
45
- }, 100);
46
- }
47
- return () => clearInterval(timer);
48
- }, [isThinking]);
51
+ if (!thinkingActive) return;
52
+ const t = setInterval(() => setThinkElapsed(p => p + 0.1), 100);
53
+ return () => clearInterval(t);
54
+ }, [thinkingActive]);
49
55
 
50
56
  const model = getModelById(activeModel);
51
- const modelLabel = model ? model.label : 'Unknown Model';
57
+ const modelLabel = model ? model.label : 'Unknown';
58
+
59
+ function stripThink(t) {
60
+ return showThought ? t : t.split('<think>').join('').split('</think>').join('');
61
+ }
52
62
 
53
63
  async function handleSubmit(text) {
54
- if (!text || streamingActive) return;
55
- if (!startupDone) setStartupDone(true);
56
-
57
- const userMsg = { role: 'user', content: text };
58
- setMessages(prev => [...prev, userMsg]);
59
- setInputBuffer('');
60
- setStreamingActive(true);
61
- setStreamContent('');
62
- setThoughtContent('');
63
- setThinkingElapsed(0);
64
-
65
- abortControllerRef.current = new AbortController();
66
-
64
+ const s = typeof text === 'string' ? text.trim() : '';
65
+ if (!s || streaming) return;
66
+ setStartupDone(true);
67
+ lastQueryRef.current = s;
68
+ accumulatedRef.current = '';
69
+ setStreaming(true);
70
+ setStreamText('');
71
+ setThinkingText('');
72
+ setThinkElapsed(0);
73
+ setToolEvent(null);
74
+ setMessages(p => [...p, { type: 'user', content: s }]);
75
+ setInput('');
76
+
77
+ const m = getModelById(activeModel);
78
+ if (!m) { setMessages(p => [...p, { type: 'assistant', content: 'No model loaded.' }]); setStreaming(false); return; }
79
+
80
+ const ac = new AbortController();
81
+ abortRef.current = ac;
82
+
67
83
  try {
68
- const history = [...messages, userMsg];
69
- const stream = callModel({ id: activeModel }, history, abortControllerRef.current.signal);
70
-
71
- for await (const event of stream) {
72
- if (event.type === 'tool') {
73
- setMessages(prev => [...prev, { type: 'tool', tool: event.tool, query: event.query }]);
74
- } else if (event.type === 'token') {
75
- const content = event.content;
76
- if (content.includes('<think>')) {
77
- setIsThinking(true);
78
- setThoughtContent(prev => prev + content.replace('<think>', ''));
79
- } else if (content.includes('</think>')) {
80
- setIsThinking(false);
81
- setThoughtContent(prev => prev + content.replace('</think>', ''));
82
- setMessages(prev => [...prev, { type: 'thought', content: thoughtContent, elapsed: thinkingElapsed.toFixed(1) + 's' }]);
83
- } else if (isThinking) {
84
- setThoughtContent(prev => prev + content);
84
+ const history = [];
85
+ for (const msg of messages) {
86
+ if (msg.type === 'user') history.push({ role: 'user', content: msg.content });
87
+ else if (msg.type === 'assistant' || msg.type === 'thought') history.push({ role: 'assistant', content: msg.content });
88
+ }
89
+ history.push({ role: 'user', content: s });
90
+
91
+ let thinkOpen = false;
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
+ setToolEvent({ 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
+
105
+ // Track think state
106
+ if (c.includes('<think>')) thinkOpen = true;
107
+ accumulatedRef.current += c;
108
+
109
+ const clean = stripThink(accumulatedRef.current);
110
+ if (thinkOpen || accumulatedRef.current.includes('<think>')) {
111
+ const stillOpen = thinkOpen || !accumulatedRef.current.includes('</think>');
112
+ if (stillOpen) {
113
+ setThinkingActive(true);
114
+ setThinkingText(clean);
115
+ setStreamText('');
116
+ } else {
117
+ thinkOpen = false;
118
+ setThinkingActive(false);
119
+ setStreamText(clean);
120
+ setThinkingText('');
121
+ }
85
122
  } else {
86
- setStreamContent(prev => prev + content);
123
+ setStreamText(clean);
124
+ }
125
+ }
126
+
127
+ if (ev.type === 'done' || ev.type === 'timeout') {
128
+ if (ev.type === 'timeout' && accumulatedRef.current) {
129
+ accumulatedRef.current += '\n\u2014 timed out \u2014';
87
130
  }
131
+ break;
88
132
  }
89
133
  }
90
- } catch (e) {
91
- setMessages(prev => [...prev, { type: 'assistant', content: 'Error connecting to model.' }]);
134
+ } catch (err) {
135
+ if (!ac.signal.aborted) {
136
+ setMessages(p => [...p, { type: 'assistant', content: 'Error: ' + (err.message || 'Connection failed') }]);
137
+ }
92
138
  } finally {
93
- setStreamingActive(false);
94
- setIsThinking(false);
95
- if (streamContent) {
96
- setMessages(prev => [...prev, { type: 'assistant', content: streamContent }]);
139
+ setStreaming(false);
140
+ setThinkingActive(false);
141
+ abortRef.current = null;
142
+ const final = accumulatedRef.current;
143
+ if (final) {
144
+ setMessages(p => [...p, { type: 'assistant', content: stripThink(final) }]);
97
145
  }
146
+ setStreamText('');
147
+ setThinkingText('');
98
148
  }
99
149
  }
100
150
 
101
- const renderHeader = () => {
102
- if (!startupDone) return null;
103
- return h(Box, { height: 1, paddingLeft: 1, paddingRight: 1 },
104
- h(Typography.Header, 'OpenAxies'),
105
- h(Typography.Muted, ` \u2022 ${modelLabel}`),
106
- h(Box, { flexGrow: 1 }),
107
- h(Typography.Dim, 'local')
108
- );
109
- };
110
-
111
- const renderFooter = () => {
112
- return h(Box, {
113
- height: 1,
114
- paddingLeft: 1,
115
- paddingRight: 1,
116
- flexDirection: 'row',
117
- alignItems: 'center'
118
- },
119
- h(Typography.Dim, 'esc interrupt \u2022 ctrl+t thoughts \u2022 ctrl+p models \u2022 ctrl+k commands'),
120
- h(Box, { flexGrow: 1 }),
121
- h(Typography.Muted, `${modelLabel.toLowerCase()} \u2022 local`)
122
- );
123
- };
124
-
125
- const renderStartup = () => {
126
- return h(Box, {
127
- flexDirection: 'column',
128
- paddingLeft: 1,
129
- paddingTop: 2,
130
- backgroundColor: '#000000'
131
- },
132
- ...STARTUP_LOGO.map(line => h(Typography.Accent, line)),
133
- h(Box, { height: 1 }),
134
- h(Typography.Header, 'OpenAxies'),
135
- h(Typography.Muted, 'local ai agent runtime'),
136
- h(Box, { height: 2 }),
137
- h(Typography.Dim, 'Press Enter to begin')
138
- );
139
- };
151
+ function cycleModel() {
152
+ const models = getModels();
153
+ setActiveModel(p => {
154
+ let found = false;
155
+ for (const m of models) {
156
+ if (found) return m.id;
157
+ if (m.id === p) found = true;
158
+ }
159
+ return models[0].id;
160
+ });
161
+ }
162
+
163
+ function handleCmd(item) {
164
+ if (!item) return;
165
+ const t = item.trigger;
166
+ if (t === '/exit') process.exit(0);
167
+ if (t === '/clear') setMessages([]);
168
+ if (t === '/model') cycleModel();
169
+ if (t === '/thoughts') setShowThought(p => !p);
170
+ if (t === '/resume') { const q = lastQueryRef.current; if (q && !streaming) handleSubmit(q); }
171
+ if (t === '/help') setMessages(p => [...p, { type: 'assistant', content: 'Commands: /help /clear /model /thoughts /resume /exit' }]);
172
+ setInput('');
173
+ setShowOverlay(false);
174
+ setOverlayIdx(0);
175
+ }
140
176
 
141
- const renderTimeline = () => {
142
- const events = [...messages];
143
- if (isThinking) {
144
- events.push({ type: 'thought', content: thoughtContent, elapsed: thinkingElapsed.toFixed(1) + 's' });
177
+ useInput((inp, key) => {
178
+ if (showOverlay) {
179
+ if (key.escape) { setShowOverlay(false); setInput(''); setOverlayIdx(0); return; }
180
+ if (key.upArrow) { setOverlayIdx(p => p > 0 ? p - 1 : COMMANDS.length - 1); return; }
181
+ if (key.downArrow) { setOverlayIdx(p => p < COMMANDS.length - 1 ? p + 1 : 0); return; }
182
+ if (key.return) { handleCmd(COMMANDS[overlayIdx]); return; }
183
+ return;
184
+ }
185
+ if (!startupDone && !streaming) {
186
+ if (key.return) { setStartupDone(true); setInput(''); return; }
187
+ return;
145
188
  }
146
- if (streamingActive && streamContent) {
147
- events.push({ type: 'assistant', content: streamContent });
189
+ if (key.escape) {
190
+ if (streaming && abortRef.current) { abortRef.current.abort(); setStreaming(false); setStreamText(''); setThinkingActive(false); }
191
+ return;
148
192
  }
193
+ if (key.ctrl) {
194
+ if (inp === 'c' || inp === 'C') { if (abortRef.current) abortRef.current.abort(); process.exit(0); }
195
+ if (inp === 't' || inp === 'T') { setShowThought(p => !p); return; }
196
+ if (inp === 'p' || inp === 'P') { cycleModel(); return; }
197
+ if (inp === 'r' || inp === 'R') { const q = lastQueryRef.current; if (q && !streaming) handleSubmit(q); return; }
198
+ if (inp === 'l' || inp === 'L') { setMessages([]); return; }
199
+ if (inp === 'k' || inp === 'K') { setInput('/'); setShowOverlay(true); setOverlayIdx(0); return; }
200
+ }
201
+ });
149
202
 
150
- return h(Box, {
151
- flexGrow: 1,
152
- flexDirection: 'column',
153
- overflow: 'hidden',
154
- paddingLeft: 0,
155
- paddingRight: 0,
156
- },
157
- ...events.slice(-15).map((event, i) => h(ActivityItem, { key: i, event }))
158
- );
159
- };
160
-
161
- return h(Box, {
162
- flexDirection: 'column',
163
- width: '100%',
164
- height: rows,
165
- overflow: 'hidden'
166
- },
167
- renderStartup() && !startupDone ? renderStartup() : null,
168
- startupDone && renderHeader(),
169
- startupDone && h(Separator),
170
- startupDone && renderTimeline(),
171
- startupDone && h(Box, { height: 1 }, h(Separator)),
172
- startupDone && h(Box, {
173
- height: 1,
174
- paddingLeft: 1,
175
- paddingRight: 1,
176
- flexDirection: 'row',
177
- alignItems: 'center'
178
- },
179
- h(Typography.Accent, ' Ask OpenAxies'),
203
+ // Build timeline
204
+ const events = [];
205
+ for (const msg of messages) {
206
+ if (msg.type === 'user' || msg.type === 'assistant' || msg.type === 'thought') {
207
+ events.push(msg);
208
+ }
209
+ }
210
+
211
+ // Tool event
212
+ if (toolEvent && streaming) {
213
+ events.push({ type: 'tool', tool: toolEvent.tool, query: toolEvent.query });
214
+ }
215
+
216
+ // Live thinking
217
+ if (thinkingActive && thinkingText) {
218
+ events.push({ type: 'thought', content: thinkingText, elapsed: thinkElapsed.toFixed(1) + 's' });
219
+ }
220
+
221
+ // Live stream
222
+ if (streaming && streamText && !thinkingActive) {
223
+ events.push({ type: 'assistant', content: streamText });
224
+ }
225
+
226
+ const visible = events.slice(-20);
227
+
228
+ // Startup screen
229
+ if (!startupDone && messages.length === 0) {
230
+ return h(Box, { flexDirection: 'column', width: '100%', height: rows, overflow: 'hidden' },
231
+ ...STARTUP_LOGO.map((l, i) => h(Box, { key: 'sl' + i, height: 1, paddingLeft: 1 },
232
+ h(Text, { color: '#00BBFF', bold: true }, l)
233
+ )),
234
+ h(Box, { key: 'sp1', height: 1 }),
235
+ h(Box, { key: 'st1', height: 1, paddingLeft: 1 }, h(Text, { color: '#FFFFFF', bold: true }, 'OpenAxies')),
236
+ h(Box, { key: 'st2', height: 1, paddingLeft: 1 }, h(Text, { color: '#777788' }, 'local ai agent runtime')),
237
+ h(Box, { key: 'sp2', height: 1 }),
238
+ h(Box, { key: 'st3', height: 1, paddingLeft: 1 }, h(Text, { color: '#555577' }, 'Press Enter to begin')),
180
239
  h(Box, { flexGrow: 1 }),
181
- h(TextInput, {
182
- value: inputBuffer,
183
- onChange: setInputBuffer,
184
- onSubmit: handleSubmit,
185
- focus: true,
186
- showCursor: true
187
- })
188
- ),
189
- startupDone && renderFooter()
240
+ h(Box, { key: 'foot', height: 1, paddingLeft: 1 }, h(Text, { color: '#333344' }, 'esc quit enter begin')),
241
+ );
242
+ }
243
+
244
+ // Overlay
245
+ if (showOverlay) {
246
+ visible.unshift({ type: '_sep' });
247
+ visible.unshift({ type: '_overlay', idx: overlayIdx });
248
+ }
249
+
250
+ // No content yet after startup
251
+ if (visible.length === 0) {
252
+ visible.push({ type: '_sep' });
253
+ visible.push({ type: '_ready' });
254
+ visible.push({ type: '_sep' });
255
+ }
256
+
257
+ // Estimate lines
258
+ const HEADER_H = 1;
259
+ const SEP_H = 1;
260
+ const INPUT_H = 1;
261
+ const FOOTER_H = 1;
262
+ const fixed = HEADER_H + SEP_H + INPUT_H + FOOTER_H;
263
+ const maxLines = Math.max(5, rows - fixed);
264
+
265
+ function estLines(ev) {
266
+ if (ev.type === '_sep' || ev.type === '_ready' || ev.type === '_overlay') return 1;
267
+ if (ev.type === 'tool') return 1;
268
+ if (ev.type === 'user') return 2;
269
+ if (ev.type === 'thought') return 2;
270
+ return 1;
271
+ }
272
+
273
+ let totalEst = 0;
274
+ let showN = 0;
275
+ for (let i = visible.length - 1; i >= 0; i--) {
276
+ const l = estLines(visible[i]);
277
+ if (totalEst + l > maxLines) break;
278
+ totalEst += l;
279
+ showN = visible.length - i;
280
+ }
281
+ const show = visible.slice(visible.length - showN);
282
+
283
+ const els = [];
284
+ for (let i = 0; i < show.length; i++) {
285
+ const ev = show[i];
286
+ const k = 'e' + i;
287
+
288
+ if (ev.type === '_sep') {
289
+ els.push(h(Box, { key: k, height: 1 }, h(Text, { color: '#1A1A28' }, '\u2500'.repeat(cols))));
290
+ } else if (ev.type === '_ready') {
291
+ els.push(h(Box, { key: k, height: 1, paddingLeft: 1 }, h(Text, { color: '#00FF88', bold: true }, 'Ready')));
292
+ } else if (ev.type === '_overlay') {
293
+ els.push(h(Box, { key: k + 'h', height: 1, paddingLeft: 1 }, h(Text, { color: '#888899', bold: true }, 'Commands')));
294
+ for (let ci = 0; ci < COMMANDS.length; ci++) {
295
+ const c = COMMANDS[ci];
296
+ const sel = ci === ev.idx;
297
+ const prefix = sel ? '\u276F ' : ' ';
298
+ const col = sel ? hex.neonBlue : '#666688';
299
+ els.push(h(Box, { key: k + 'c' + ci, height: 1, paddingLeft: 2 },
300
+ h(Text, { color: col }, prefix + c.trigger),
301
+ h(Text, { color: '#444466' }, ' ' + (c.desc || ''))
302
+ ));
303
+ }
304
+ } else if (ev.type === 'user') {
305
+ els.push(h(Box, { key: k + 'h', height: 1, paddingLeft: 1 }, h(Text, { color: '#888899', bold: true }, 'You')));
306
+ els.push(h(Box, { key: k + 'c', height: 1, paddingLeft: 2 }, h(Text, { color: hex.neonBlue, wrap: 'wrap' }, ev.content)));
307
+ } else if (ev.type === 'assistant') {
308
+ els.push(h(Box, { key: k, height: 1, paddingLeft: 1 }, h(Text, { color: '#FFFFFF', wrap: 'wrap' }, ev.content)));
309
+ } else if (ev.type === 'thought') {
310
+ els.push(h(Box, { key: k + 'h', height: 1, paddingLeft: 1 },
311
+ h(Text, { color: '#FF9F43', bold: true }, '\u25CB Thinking \u2022 ' + (ev.elapsed || '0.0s'))
312
+ ));
313
+ els.push(h(Box, { key: k + 'c', height: 1, paddingLeft: 2 }, h(Text, { color: '#FF9F43', wrap: 'wrap' }, ev.content)));
314
+ } else if (ev.type === 'tool') {
315
+ els.push(h(Box, { key: k, height: 1, paddingLeft: 1 },
316
+ h(Text, { color: '#5B5B8A' }, '\u2500\u2500 ' + (ev.tool || 'tool') + (ev.query ? ' \u2014 ' + ev.query : ''))
317
+ ));
318
+ }
319
+ }
320
+
321
+ // Fill
322
+ while (els.length < maxLines) {
323
+ els.push(h(Box, { key: 'f' + els.length, height: 1 }));
324
+ }
325
+
326
+ // Header
327
+ const header = h(Box, { height: HEADER_H, flexShrink: 0, paddingLeft: 1, paddingRight: 1 },
328
+ h(Text, { color: '#00BBFF', bold: true }, 'OpenAxies'),
329
+ h(Text, { color: '#555577' }, ' \u2022 '),
330
+ h(Text, { color: '#FFFFFF' }, modelLabel),
331
+ h(Box, { flexGrow: 1 }),
332
+ h(Text, { color: '#666688' }, 'local'),
333
+ );
334
+
335
+ // Separator
336
+ const sepLine = h(Box, { height: SEP_H, flexShrink: 0 }, h(Text, { color: '#1A1A28' }, '\u2500'.repeat(cols)));
337
+
338
+ // Input
339
+ const inputLine = h(Box, { height: INPUT_H, flexShrink: 0, paddingLeft: 1, paddingRight: 1 },
340
+ h(Text, { color: hex.greenOnline }, '\u258C'),
341
+ h(Box, { width: 1 }),
342
+ h(TextInput, {
343
+ value: input,
344
+ onChange: setInput,
345
+ onSubmit: function (v) { if (typeof v === 'string' && v.trim() && !streaming) handleSubmit(v); },
346
+ placeholder: '',
347
+ focus: !showOverlay,
348
+ showCursor: true,
349
+ })
350
+ );
351
+
352
+ // Footer
353
+ const footer = h(Box, { height: FOOTER_H, flexShrink: 0, paddingLeft: 1, paddingRight: 1 },
354
+ h(Text, { color: '#444466' }, 'esc \u2248 ctrl+t ctrl+p ctrl+k'),
355
+ h(Box, { flexGrow: 1 }),
356
+ h(Text, { color: '#555577' }, (modelLabel.replace('OpenAxies ', '') || 'model').toLowerCase() + ' \u2022 local'),
357
+ );
358
+
359
+ return h(Box, { flexDirection: 'column', width: '100%', height: rows, overflow: 'hidden' },
360
+ header,
361
+ sepLine,
362
+ h(Box, { flexGrow: 1, flexDirection: 'column', overflow: 'hidden' }, ...els),
363
+ inputLine,
364
+ footer,
190
365
  );
191
366
  }