openaxies 1.0.1 → 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 +317 -145
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.1",
3
+ "version": "1.0.2",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
package/src/App.js CHANGED
@@ -1,12 +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
7
 
11
8
  const h = React.createElement;
12
9
 
@@ -18,6 +15,15 @@ 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
29
  const rows = stdout.rows || 24;
@@ -25,170 +31,336 @@ export default function AppRoot() {
25
31
 
26
32
  const [messages, setMessages] = React.useState([]);
27
33
  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);
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);
34
40
  const [startupDone, setStartupDone] = React.useState(false);
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);
35
45
 
36
- const abortControllerRef = React.useRef(null);
46
+ const abortRef = React.useRef(null);
47
+ const accumulatedRef = React.useRef('');
48
+ const lastQueryRef = React.useRef('');
37
49
 
38
50
  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]);
51
+ if (!thinkingActive) return;
52
+ const t = setInterval(() => setThinkElapsed(p => p + 0.1), 100);
53
+ return () => clearInterval(t);
54
+ }, [thinkingActive]);
47
55
 
48
56
  const model = getModelById(activeModel);
49
- 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
+ }
50
62
 
51
63
  async function handleSubmit(text) {
52
- if (!text || streamingActive) return;
64
+ const s = typeof text === 'string' ? text.trim() : '';
65
+ if (!s || streaming) return;
53
66
  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
-
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
+
65
83
  try {
66
- const history = messages.map(m => ({
67
- role: m.type === 'user' ? 'user' : 'assistant',
68
- content: m.content
69
- }));
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);
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
+ }
89
122
  } else {
90
- 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';
91
130
  }
131
+ break;
92
132
  }
93
133
  }
94
- } catch (e) {
95
- 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
+ }
96
138
  } finally {
97
- setStreamingActive(false);
98
- setIsThinking(false);
99
- if (streamContent) {
100
- 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) }]);
101
145
  }
146
+ setStreamText('');
147
+ setThinkingText('');
102
148
  }
103
149
  }
104
150
 
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)),
136
- h(Box, { height: 1 }),
137
- h(Typography.Header, 'OpenAxies'),
138
- h(Typography.Muted, 'local ai agent runtime'),
139
- h(Box, { height: 2 }),
140
- h(Typography.Dim, 'Press Enter to begin')
141
- );
142
- };
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
+ }
143
176
 
144
- const renderTimeline = () => {
145
- const events = [...messages];
146
- if (isThinking) {
147
- 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;
148
188
  }
149
- if (streamingActive && streamContent) {
150
- 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;
151
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
+ });
152
202
 
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'),
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')),
183
239
  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()
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,
193
365
  );
194
366
  }