openaxies 0.5.0 → 0.5.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openaxies",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
package/src/App.js CHANGED
@@ -4,49 +4,53 @@ import { hex } from './config/theme.js';
4
4
  import { getCommands } from './config/commands.js';
5
5
  import { getModels, getModelById, DEFAULT_MODEL_ID } from './config/models.js';
6
6
  import { callModel } from './providers/index.js';
7
- import { createModelBar } from './components/ModelBar.js';
8
- import { createParticleRow } from './components/Particles.js';
9
7
  import { createComposerDock, DOCK_HEIGHT } from './components/ComposerDock.js';
10
8
 
11
9
  const h = React.createElement;
12
- const BAR_COLOR = '#1A1A2E';
13
10
 
14
11
  export default AppRoot;
15
12
 
13
+ const LOGO = [
14
+ ' \u2588\u2588\u2588 \u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588 \u2588 \u2588 \u2588\u2588 \u2588 \u2588 \u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588',
15
+ '\u2588 \u2588 \u2588 \u2588 \u2588 \u2588\u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2588 ',
16
+ '\u2588 \u2588 \u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588 \u2588 \u2588 \u2588 \u2588\u2588\u2588\u2588 \u2588 \u2588 \u2588\u2588\u2588\u2588 \u2588\u2588\u2588',
17
+ '\u2588 \u2588 \u2588 \u2588 \u2588 \u2588\u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2588',
18
+ ' \u2588\u2588\u2588 \u2588 \u2588\u2588\u2588\u2588\u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588 ',
19
+ ];
20
+
21
+ const LOGO_COLORS = ['#00F0FF', '#00DDFF', '#00CCFF', '#00BBFF', '#00AAFF'];
22
+
16
23
  function buildHistory(messages, newText) {
17
- const history = [];
24
+ const h = [];
18
25
  for (let i = 0; i < messages.length; i++) {
19
26
  const m = messages[i];
20
- if (m === null || m === undefined || typeof m !== 'object') {
21
- continue;
22
- }
27
+ if (m === null || m === undefined || typeof m !== 'object') continue;
23
28
  if (m.role === 'user' || m.role === 'assistant') {
24
- const content = typeof m.content === 'string' ? m.content : '';
25
- history.push({ role: m.role, content: content });
29
+ const c = typeof m.content === 'string' ? m.content : '';
30
+ h.push({ role: m.role, content: c });
26
31
  }
27
32
  }
28
33
  if (typeof newText === 'string' && newText.length > 0) {
29
- history.push({ role: 'user', content: newText });
34
+ h.push({ role: 'user', content: newText });
30
35
  }
31
- return history;
36
+ return h;
32
37
  }
33
38
 
34
- function formatTimer(seconds) {
35
- if (typeof seconds !== 'number' || seconds < 0) {
36
- return '0.0s';
37
- }
38
- return seconds.toFixed(1) + 's';
39
+ function formatTimer(s) {
40
+ if (typeof s !== 'number' || s < 0) return '0.0s';
41
+ return s.toFixed(1) + 's';
39
42
  }
40
43
 
41
- function barLine(cols) {
42
- const safeCols = typeof cols === 'number' && cols > 0 ? cols : 80;
43
- let line = '';
44
- for (let i = 0; i < safeCols; i++) {
45
- line = line + '\u2500';
46
- }
47
- return h(Text, { color: BAR_COLOR }, line);
44
+ function hr(cols, color) {
45
+ const n = typeof cols === 'number' && cols > 0 ? cols : 80;
46
+ const c = typeof color === 'string' ? color : '#1A1A28';
47
+ let s = '';
48
+ for (let i = 0; i < n; i++) s = s + '\u2500';
49
+ return h(Text, { color: c }, s);
48
50
  }
49
51
 
52
+ const SPINNER = ['\u25CB', '\u25D4', '\u25D0', '\u25D5', '\u25CF', '\u25D5', '\u25D0', '\u25D4'];
53
+
50
54
  function AppRoot() {
51
55
  const { stdout } = useStdout();
52
56
  const rows = stdout.rows || 24;
@@ -57,20 +61,17 @@ function AppRoot() {
57
61
  const [streamText, setStreamText] = React.useState('');
58
62
  const [streamingActive, setStreamingActive] = React.useState(false);
59
63
  const [isThinking, setIsThinking] = React.useState(false);
60
- const [spinnerChar, setSpinnerChar] = React.useState('');
64
+ const [spinnerIdx, setSpinnerIdx] = React.useState(0);
61
65
  const [thinkingElapsed, setThinkingElapsed] = React.useState(0);
62
66
  const [inputBuffer, setInputBuffer] = React.useState('');
63
67
  const [showOverlay, setShowOverlay] = React.useState(false);
64
68
  const [overlayIndex, setOverlayIndex] = React.useState(0);
65
- const [particleFrame, setParticleFrame] = React.useState(0);
66
69
  const [toolInfo, setToolInfo] = React.useState(null);
67
70
 
68
71
  const commands = getCommands();
69
- const MODEL_BAR_H = 1;
70
- const SEP_H = 1;
71
- const DOCK = DOCK_HEIGHT;
72
+ const LOGO_H = 5;
72
73
  const overlayH = showOverlay ? 6 : 0;
73
- const fixedH = MODEL_BAR_H + SEP_H + overlayH + DOCK;
74
+ const fixedH = LOGO_H + overlayH + DOCK_HEIGHT;
74
75
  const availLines = Math.max(1, rows - fixedH);
75
76
 
76
77
  const abortRef = React.useRef(null);
@@ -78,27 +79,6 @@ function AppRoot() {
78
79
  const spinnerRef = React.useRef(null);
79
80
  const timerRef = React.useRef(null);
80
81
  const thinkStartRef = React.useRef(null);
81
- const particleRef = React.useRef(null);
82
-
83
- const SPINNER_FRAMES = [
84
- '\u25CB', '\u25D4', '\u25D0', '\u25D5', '\u25CF',
85
- '\u25D5', '\u25D0', '\u25D4', '\u25CB', '\u25D1',
86
- '\u25D2', '\u25D3', '\u25CF', '\u25D3', '\u25D2',
87
- '\u25D1', '\u25CC', '\u25CB', '\u25CD', '\u25CE',
88
- '\u25CF', '\u25CE', '\u25CD', '\u25CB',
89
- ];
90
-
91
- React.useEffect(function () {
92
- particleRef.current = setInterval(function () {
93
- setParticleFrame(function (p) { return (p + 1) % 64; });
94
- }, 100);
95
- return function () {
96
- if (particleRef.current !== null) {
97
- clearInterval(particleRef.current);
98
- particleRef.current = null;
99
- }
100
- };
101
- }, []);
102
82
 
103
83
  React.useEffect(function () {
104
84
  if (isThinking === false) {
@@ -110,14 +90,12 @@ function AppRoot() {
110
90
  clearInterval(timerRef.current);
111
91
  timerRef.current = null;
112
92
  }
113
- setSpinnerChar('');
114
93
  return;
115
94
  }
116
95
  let idx = 0;
117
- setSpinnerChar(SPINNER_FRAMES[0]);
118
96
  spinnerRef.current = setInterval(function () {
119
- idx = (idx + 1) % SPINNER_FRAMES.length;
120
- setSpinnerChar(SPINNER_FRAMES[idx]);
97
+ idx = (idx + 1) % SPINNER.length;
98
+ setSpinnerIdx(idx);
121
99
  }, 100);
122
100
  timerRef.current = setInterval(function () {
123
101
  if (thinkStartRef.current !== null) {
@@ -148,101 +126,71 @@ function AppRoot() {
148
126
  });
149
127
  }
150
128
 
151
- function checkThinkState(text) {
152
- const openIdx = text.lastIndexOf('<think>');
153
- const closeIdx = text.lastIndexOf('</think>');
154
- if (openIdx === -1 && closeIdx === -1) return false;
155
- return openIdx > closeIdx;
129
+ function checkThink(text) {
130
+ const o = text.lastIndexOf('<think>');
131
+ const c = text.lastIndexOf('</think>');
132
+ if (o === -1 && c === -1) return false;
133
+ return o > c;
156
134
  }
157
135
 
158
136
  async function handleSubmit(text) {
159
- const safeText = typeof text === 'string' ? text : '';
160
- if (safeText.length === 0 || streamingActive === true) return;
161
-
137
+ const safe = typeof text === 'string' ? text : '';
138
+ if (safe.length === 0 || streamingActive === true) return;
162
139
  setStreamingActive(true);
163
140
  setToolInfo(null);
164
141
  setStreamText(' connecting');
165
142
  setThinkingElapsed(0);
166
143
  connRef.current = setInterval(function () {
167
- setStreamText(function (prev) {
168
- if (prev === ' connecting') return ' connecting.';
169
- if (prev === ' connecting.') return ' connecting..';
170
- if (prev === ' connecting..') return ' connecting...';
144
+ setStreamText(function (p) {
145
+ if (p === ' connecting') return ' connecting.';
146
+ if (p === ' connecting.') return ' connecting..';
147
+ if (p === ' connecting..') return ' connecting...';
171
148
  return ' connecting';
172
149
  });
173
150
  }, 400);
174
-
175
- setMessages(function (prev) {
176
- return prev.concat([{
177
- role: 'user',
178
- content: safeText,
179
- id: 'u-' + Date.now(),
180
- }]);
151
+ setMessages(function (p) {
152
+ return p.concat([{ role: 'user', content: safe, id: 'u-' + Date.now() }]);
181
153
  });
182
154
  setInputBuffer('');
183
-
184
155
  const modelInfo = getModelById(activeModel);
185
156
  if (modelInfo === null) {
186
- if (connRef.current !== null) {
187
- clearInterval(connRef.current);
188
- connRef.current = null;
189
- }
157
+ if (connRef.current !== null) clearInterval(connRef.current), connRef.current = null;
190
158
  setStreamingActive(false);
191
159
  setStreamText('');
192
- setMessages(function (prev) {
193
- return prev.concat([{
194
- role: 'assistant',
195
- content: 'No model selected. Use `/model` to switch.',
196
- id: 'e-' + Date.now(),
197
- }]);
198
- });
160
+ setMessages(function (p) { return p.concat([{ role: 'assistant', content: 'No model selected. Use /model.', id: 'e-' + Date.now() }]); });
199
161
  return;
200
162
  }
201
-
202
- const history = buildHistory(messages, safeText);
163
+ const history = buildHistory(messages, safe);
203
164
  const abortController = new AbortController();
204
165
  abortRef.current = abortController;
205
-
206
166
  try {
207
167
  const gen = callModel(modelInfo, history, abortController.signal);
208
168
  let accumulated = '';
209
169
  let firstEvent = true;
210
170
  let thinkStarted = false;
211
-
212
171
  for await (const event of gen) {
213
172
  if (abortController.signal.aborted === true) break;
214
-
215
173
  if (firstEvent === true) {
216
174
  firstEvent = false;
217
- if (connRef.current !== null) {
218
- clearInterval(connRef.current);
219
- connRef.current = null;
220
- }
175
+ if (connRef.current !== null) clearInterval(connRef.current), connRef.current = null;
221
176
  }
222
-
223
177
  if (event.type === 'tool') {
224
- setToolInfo({
225
- tool: event.tool,
226
- used: event.used,
227
- sites: event.sites,
228
- query: event.query,
229
- });
178
+ setToolInfo({ tool: event.tool, used: event.used, sites: event.sites, query: event.query });
230
179
  continue;
231
180
  }
232
-
233
181
  if (event.type === 'thinking' || event.type === 'token') {
234
182
  const content = typeof event.content === 'string' ? event.content : '';
235
183
  for (let i = 0; i < content.length; i++) {
236
184
  accumulated = accumulated + content[i];
237
- const insideThink = checkThinkState(accumulated);
185
+ const inside = checkThink(accumulated);
238
186
  setStreamText(accumulated);
239
- setIsThinking(insideThink);
240
- if (insideThink === true && thinkStarted === false) {
187
+ setIsThinking(inside);
188
+ if (inside === true && thinkStarted === false) {
241
189
  thinkStarted = true;
242
190
  thinkStartRef.current = Date.now();
243
191
  setThinkingElapsed(0);
244
192
  }
245
- if (insideThink === false && thinkStarted === true) {
193
+ if (inside === false && thinkStarted === true) {
246
194
  thinkStarted = false;
247
195
  thinkStartRef.current = null;
248
196
  setThinkingElapsed(0);
@@ -250,98 +198,53 @@ function AppRoot() {
250
198
  await new Promise(function (r) { setTimeout(r, 0); });
251
199
  }
252
200
  }
253
-
254
201
  if (event.type === 'done') break;
255
202
  if (event.type === 'timeout') {
256
- if (accumulated.length > 0) {
257
- accumulated = accumulated + '\n\n\u2014 stream timed out \u2014';
258
- }
203
+ if (accumulated.length > 0) accumulated = accumulated + '\n\n\u2014 stream timed out \u2014';
259
204
  setStreamText(accumulated);
260
205
  break;
261
206
  }
262
207
  }
263
-
264
- if (connRef.current !== null) {
265
- clearInterval(connRef.current);
266
- connRef.current = null;
267
- }
208
+ if (connRef.current !== null) clearInterval(connRef.current), connRef.current = null;
268
209
  setIsThinking(false);
269
210
  thinkStartRef.current = null;
270
211
  setThinkingElapsed(0);
271
212
  setStreamingActive(false);
272
-
273
213
  const finalText = streamText;
274
214
  setStreamText('');
275
-
276
215
  if (finalText.length > 0) {
277
- setMessages(function (prev) {
278
- return prev.concat([{
279
- role: 'assistant',
280
- content: finalText,
281
- id: 'a-' + Date.now(),
282
- }]);
283
- });
216
+ setMessages(function (p) { return p.concat([{ role: 'assistant', content: finalText, id: 'a-' + Date.now() }]); });
284
217
  }
285
218
  } catch (err) {
286
- if (connRef.current !== null) {
287
- clearInterval(connRef.current);
288
- connRef.current = null;
289
- }
219
+ if (connRef.current !== null) clearInterval(connRef.current), connRef.current = null;
290
220
  setIsThinking(false);
291
221
  thinkStartRef.current = null;
292
222
  setThinkingElapsed(0);
293
223
  setStreamingActive(false);
294
224
  setStreamText('');
295
- const errMsg = err !== null && err !== undefined
296
- ? (err.message || String(err))
297
- : 'Unknown error';
298
- setMessages(function (prev) {
299
- return prev.concat([{
300
- role: 'assistant',
301
- content: '**Error:** ' + errMsg + '\n\nThe Space may be cold-starting (30\u2013120 s first request). Try again.',
302
- id: 'er-' + Date.now(),
303
- }]);
225
+ const errMsg = err !== null && err !== undefined ? (err.message || String(err)) : 'Unknown error';
226
+ setMessages(function (p) {
227
+ return p.concat([{ role: 'assistant', content: 'Error: ' + errMsg + '\n\nSpace cold-starting? Try again.', id: 'er-' + Date.now() }]);
304
228
  });
305
229
  }
306
230
  }
307
231
 
308
- function handleInputChange(v) {
309
- const safe = typeof v === 'string' ? v : '';
310
- setInputBuffer(safe);
311
- if (safe.length === 1 && safe[0] === '/' && showOverlay === false) {
312
- setShowOverlay(true);
313
- setOverlayIndex(0);
314
- }
315
- if (showOverlay === true && (safe.length === 0 || safe[0] !== '/')) {
316
- setShowOverlay(false);
317
- }
232
+ function handleChange(v) {
233
+ const s = typeof v === 'string' ? v : '';
234
+ setInputBuffer(s);
235
+ if (s.length === 1 && s[0] === '/' && showOverlay === false) { setShowOverlay(true); setOverlayIndex(0); }
236
+ if (showOverlay === true && (s.length === 0 || s[0] !== '/')) { setShowOverlay(false); }
318
237
  }
319
238
 
320
- function handleSlashSelect(item) {
239
+ function handleCmd(item) {
321
240
  if (item === null || item === undefined) return;
322
- const trigger = item.value;
323
- if (typeof trigger !== 'string') return;
324
-
325
- if (trigger === '/exit') process.exit(0);
326
- if (trigger === '/clear') {
327
- setMessages([]);
328
- }
329
- if (trigger === '/model') {
330
- cycleModel();
331
- }
332
- if (trigger === '/help') {
333
- setMessages(function (prev) {
334
- return prev.concat([{
335
- role: 'assistant',
336
- content:
337
- '**Commands:**\n' +
338
- '- `/help` \u2014 this help\n' +
339
- '- `/clear` \u2014 clear chat\n' +
340
- '- `/model` \u2014 cycle: Llama \u2192 GPT \u2192 DeepSeek\n' +
341
- '- `/exit` \u2014 quit',
342
- id: 'hlp-' + Date.now(),
343
- }]);
344
- });
241
+ const t = item.value;
242
+ if (typeof t !== 'string') return;
243
+ if (t === '/exit') process.exit(0);
244
+ if (t === '/clear') setMessages([]);
245
+ if (t === '/model') cycleModel();
246
+ if (t === '/help') {
247
+ setMessages(function (p) { return p.concat([{ role: 'assistant', content: 'Commands: /help /clear /model /exit', id: 'h-' + Date.now() }]); });
345
248
  }
346
249
  setInputBuffer('');
347
250
  setShowOverlay(false);
@@ -350,49 +253,33 @@ function AppRoot() {
350
253
 
351
254
  useInput(function handleInput(input, key) {
352
255
  if (showOverlay === true) {
353
- if (key.escape === true) {
354
- setShowOverlay(false);
355
- setInputBuffer('');
356
- setOverlayIndex(0);
357
- return;
358
- }
359
- if (key.upArrow === true) {
360
- setOverlayIndex(function (p) { return p > 0 ? p - 1 : commands.length - 1; });
361
- return;
362
- }
363
- if (key.downArrow === true) {
364
- setOverlayIndex(function (p) { return p < commands.length - 1 ? p + 1 : 0; });
365
- return;
366
- }
367
- if (key.return === true) {
368
- const cmd = commands[overlayIndex];
369
- if (cmd !== null && cmd !== undefined) handleSlashSelect({ value: cmd.trigger });
370
- return;
371
- }
256
+ if (key.escape === true) { setShowOverlay(false); setInputBuffer(''); setOverlayIndex(0); return; }
257
+ if (key.upArrow === true) { setOverlayIndex(function (p) { return p > 0 ? p - 1 : commands.length - 1; }); return; }
258
+ if (key.downArrow === true) { setOverlayIndex(function (p) { return p < commands.length - 1 ? p + 1 : 0; }); return; }
259
+ if (key.return === true) { const cmd = commands[overlayIndex]; if (cmd !== null && cmd !== undefined) handleCmd({ value: cmd.trigger }); return; }
372
260
  return;
373
261
  }
374
-
375
262
  if (key.return === true) {
376
- const safe = typeof inputBuffer === 'string' ? inputBuffer : '';
377
- if (safe.length > 0 && streamingActive === false) {
378
- handleSubmit(safe);
379
- }
263
+ const s = typeof inputBuffer === 'string' ? inputBuffer : '';
264
+ if (s.length > 0 && streamingActive === false) handleSubmit(s);
380
265
  return;
381
266
  }
382
-
383
267
  if (key.ctrl === true && (input === 'c' || input === 'C')) {
384
- if (abortRef.current !== null) {
385
- try { abortRef.current.abort(); } catch (_) {}
386
- abortRef.current = null;
387
- setStreamingActive(false);
388
- setStreamText('');
389
- }
268
+ if (abortRef.current !== null) { try { abortRef.current.abort(); } catch (_) {} abortRef.current = null; setStreamingActive(false); setStreamText(''); }
390
269
  process.exit(0);
391
270
  }
392
271
  });
393
272
 
394
- const modelBar = createModelBar(activeModel, cols);
395
- const particleTop = createParticleRow(particleFrame, cols, 'tp');
273
+ const activeInfo = getModelById(activeModel);
274
+ const activeLabel = activeInfo !== null ? activeInfo.label : 'OpenAxies';
275
+
276
+ const logoEls = [];
277
+ for (let r = 0; r < LOGO.length; r++) {
278
+ logoEls.push(
279
+ h(Text, { key: 'l' + r, color: LOGO_COLORS[r % LOGO_COLORS.length], bold: true }, LOGO[r])
280
+ );
281
+ }
282
+ const logo = h(Box, { flexDirection: 'column', width: '100%', flexShrink: 0, paddingLeft: 1 }, ...logoEls);
396
283
 
397
284
  const viewLines = [];
398
285
  for (let i = 0; i < messages.length; i++) {
@@ -400,104 +287,72 @@ function AppRoot() {
400
287
  if (msg === null || typeof msg !== 'object') continue;
401
288
  const content = typeof msg.content === 'string' ? msg.content : '';
402
289
  if (content.length === 0) continue;
403
-
404
- if (i > 0) viewLines.push({ type: 'sep' });
405
-
406
290
  if (msg.role === 'user') {
407
- viewLines.push({ type: 'user', text: content });
408
- viewLines.push({ type: 'sep' });
291
+ viewLines.push({ t: 'user', text: '\u25B7 ' + content });
409
292
  } else {
410
- viewLines.push({ type: 'assistant', text: content });
293
+ viewLines.push({ t: 'text', text: content });
411
294
  }
412
295
  }
413
296
 
414
297
  const hasStream = typeof streamText === 'string' && streamText.length > 0;
415
298
  if (hasStream === true) {
416
- viewLines.push({ type: 'sep' });
417
-
418
- if (toolInfo !== null && toolInfo.used === true) {
419
- viewLines.push({
420
- type: 'tool',
421
- text: ' \u2500\u2500 ' + toolInfo.tool + ': "' + toolInfo.query + '" (' + toolInfo.sites + ' sites)',
422
- });
299
+ if (toolInfo !== null && toolInfo.used === true && toolInfo.query.length > 0) {
300
+ viewLines.push({ t: 'tool', text: '\u2500\u2500 ' + toolInfo.tool + ': "' + toolInfo.query + '" (' + toolInfo.sites + ' sites)' });
423
301
  }
424
-
425
302
  if (isThinking === true) {
426
- const sp = typeof spinnerChar === 'string' && spinnerChar.length > 0 ? spinnerChar : '\u25CB';
427
- viewLines.push({
428
- type: 'think',
429
- spin: sp,
430
- elapsed: formatTimer(thinkingElapsed),
431
- });
303
+ viewLines.push({ t: 'think', spin: SPINNER[spinnerIdx], elapsed: formatTimer(thinkingElapsed) });
432
304
  const clean = streamText.split('<think>').join('').split('</think>').join('');
433
- viewLines.push({ type: 'stream', text: clean, color: '#FF9F43' });
305
+ viewLines.push({ t: 'think-text', text: clean });
434
306
  } else {
435
307
  const clean = streamText.split('<think>').join('').split('</think>').join('');
436
- viewLines.push({ type: 'stream', text: clean, color: hex.primary });
308
+ viewLines.push({ t: 'text', text: clean });
437
309
  }
438
310
  }
439
311
 
440
312
  if (viewLines.length === 0) {
441
- viewLines.push({ type: 'muted', text: ' type a message to start...' });
313
+ viewLines.push({ t: 'idle', text: ' \u25B8 type a message to begin... [/help for commands]' });
442
314
  }
443
315
 
444
- const visibleHeight = Math.max(1, availLines - 1);
445
- const displayLines = viewLines.slice(-visibleHeight);
446
- const viewElements = [];
316
+ const visible = Math.max(1, availLines - 1);
317
+ const display = viewLines.slice(-visible);
318
+ const viewEls = [];
447
319
 
448
- for (let i = 0; i < displayLines.length; i++) {
449
- const line = displayLines[i];
450
- if (line.type === 'sep') {
451
- viewElements.push(h(Box, { key: 'vs-' + i, height: 1 }, barLine(cols)));
452
- } else if (line.type === 'think') {
453
- viewElements.push(
454
- h(Box, { key: 'vt-' + i, height: 1 },
455
- h(Text, { color: '#FF9F43', bold: true },
456
- ' ' + line.spin + ' Thinking \u2022 ' + line.elapsed
457
- )
320
+ for (let i = 0; i < display.length; i++) {
321
+ const l = display[i];
322
+ if (l.t === 'think') {
323
+ viewEls.push(
324
+ h(Box, { key: 'k-' + i, height: 1, paddingLeft: 2 },
325
+ h(Text, { color: '#FF9F43', bold: true }, l.spin + ' Thinking \u2022 ' + l.elapsed)
458
326
  )
459
327
  );
460
- } else if (line.type === 'tool') {
461
- viewElements.push(
462
- h(Box, { key: 'vtl-' + i, height: 1 },
463
- h(Text, { color: '#5B5B8A' }, line.text)
328
+ } else if (l.t === 'think-text') {
329
+ viewEls.push(
330
+ h(Box, { key: 'kt-' + i, height: 1, paddingLeft: 3 },
331
+ h(Text, { color: '#FF9F43' }, l.text)
464
332
  )
465
333
  );
466
- } else if (line.type === 'user') {
467
- viewElements.push(
468
- h(Box, {
469
- key: 'vu-' + i,
470
- height: 1,
471
- paddingLeft: 1,
472
- },
473
- h(Text, { color: hex.neonBlue, bold: true }, '\u25B7 '),
474
- h(Text, { color: hex.neonBlue }, line.text)
334
+ } else if (l.t === 'tool') {
335
+ viewEls.push(
336
+ h(Box, { key: 'tl-' + i, height: 1, paddingLeft: 2 },
337
+ h(Text, { color: '#5B5B8A' }, l.text)
475
338
  )
476
339
  );
477
- } else if (line.type === 'assistant') {
478
- viewElements.push(
479
- h(Box, {
480
- key: 'va-' + i,
481
- height: 1,
482
- paddingLeft: 1,
483
- },
484
- h(Text, { color: hex.primary }, line.text)
340
+ } else if (l.t === 'user') {
341
+ viewEls.push(
342
+ h(Box, { key: 'u-' + i, height: 1, paddingLeft: 1 },
343
+ h(Text, { color: hex.neonBlue }, l.text)
485
344
  )
486
345
  );
487
- } else if (line.type === 'stream') {
488
- viewElements.push(
489
- h(Box, {
490
- key: 'vst-' + i,
491
- height: 1,
492
- paddingLeft: 1,
493
- },
494
- h(Text, { color: line.color }, line.text)
346
+ } else if (l.t === 'text') {
347
+ viewEls.push(
348
+ h(Box, { key: 'a-' + i, height: 1, paddingLeft: 2 },
349
+ h(Text, { color: hex.primary }, l.text)
495
350
  )
496
351
  );
497
- } else if (line.type === 'muted') {
498
- viewElements.push(
499
- h(Box, { key: 'vm-' + i, height: 1, paddingLeft: 1 },
500
- h(Text, { color: '#333355' }, line.text)
352
+ } else if (l.t === 'idle') {
353
+ viewEls.push(
354
+ h(Box, { key: 'i-' + i, height: 1, paddingLeft: 1 },
355
+ h(Text, { color: '#444466' }, l.text)
501
356
  )
502
357
  );
503
358
  }
@@ -508,22 +363,21 @@ function AppRoot() {
508
363
  height: availLines,
509
364
  flexDirection: 'column',
510
365
  overflow: 'hidden',
511
- paddingBottom: 1,
512
- }, ...viewElements);
513
-
514
- const particleBot = createParticleRow(particleFrame, cols, 'bt');
366
+ paddingTop: 0,
367
+ paddingBottom: 0,
368
+ }, ...viewEls);
515
369
 
516
- const slashOverlay = showOverlay === true
517
- ? createSlashOverlay(commands, overlayIndex)
518
- : null;
370
+ const slashOverlay = showOverlay === true ? createSlashOverlay(commands, overlayIndex) : null;
519
371
 
520
372
  const dock = createComposerDock({
521
373
  value: inputBuffer,
522
- onChange: handleInputChange,
374
+ onChange: handleChange,
523
375
  onSubmit: function () {},
524
376
  inputActive: showOverlay === false,
525
377
  isStreaming: streamingActive,
526
378
  terminalWidth: cols,
379
+ modelLabel: activeLabel + ' \u25CF',
380
+ modelStatus: true,
527
381
  });
528
382
 
529
383
  return h(Box, {
@@ -533,11 +387,9 @@ function AppRoot() {
533
387
  backgroundColor: hex.black,
534
388
  overflow: 'hidden',
535
389
  },
536
- modelBar,
537
- h(Box, { height: 1 }, barLine(cols)),
538
- h(Box, { height: 1, paddingLeft: 1 }, particleTop),
390
+ logo,
391
+ h(Box, { height: 1 }, hr(cols, '#1A1A2E')),
539
392
  viewport,
540
- h(Box, { height: 1, paddingLeft: 1 }, particleBot),
541
393
  slashOverlay,
542
394
  dock
543
395
  );
@@ -1,105 +1,66 @@
1
1
  import React from 'react';
2
- import { Box, Text } 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
 
6
6
  const h = React.createElement;
7
7
 
8
- export const DOCK_HEIGHT = 3;
9
- const MAX_FRAME_WIDTH = 80;
10
- const PROMPT = 'openaxies@root:~$ ';
8
+ export const DOCK_HEIGHT = 4;
11
9
 
12
- function checkString(value) {
13
- if (typeof value !== 'string') {
14
- return '';
15
- }
16
- return value;
17
- }
18
-
19
- function checkCallback(fn, label) {
20
- if (fn !== null && fn !== undefined && typeof fn !== 'function') {
21
- throw new Error(label + ' must be a function');
22
- }
23
- }
10
+ const PROMPT = '> ';
24
11
 
25
- // ComposerDock — 3 rows, permanently at the bottom of the layout.
26
- //
27
- // Layout:
28
- // ┌──────────────────────────────────────────────────────────────────────────────┐
29
- // │ openaxies@root:~$ [TextInput] │
30
- // └──────────────────────────────────────────────────────────────────────────────┘
31
- //
32
- // The frame is capped at 80 columns for narrow terminal clients.
33
- // flexShrink: 0 prevents Ink from ever yielding these rows to the viewport.
34
- //
35
- // config.inputActive (bool, default true):
36
- // Set to false when SlashOverlay is open so TextInput releases keyboard
37
- // focus and arrow keys don't corrupt the input buffer.
38
12
  export function createComposerDock(config) {
39
13
  if (config === null || config === undefined || typeof config !== 'object') {
40
14
  throw new Error('createComposerDock requires a config object');
41
15
  }
42
16
 
43
- const value = checkString(config.value);
17
+ const value = typeof config.value === 'string' ? config.value : '';
44
18
  const onChange = config.onChange;
45
19
  const onSubmit = config.onSubmit;
46
20
  const inputActive = config.inputActive !== false;
47
21
  const isStreaming = config.isStreaming === true;
48
- const terminalWidth = Number.isFinite(config.terminalWidth)
49
- ? Math.floor(config.terminalWidth)
50
- : MAX_FRAME_WIDTH;
51
-
52
- checkCallback(onChange, 'onChange');
53
- checkCallback(onSubmit, 'onSubmit');
22
+ const modelLabel = typeof config.modelLabel === 'string' ? config.modelLabel : '';
23
+ const modelStatus = config.modelStatus === true;
24
+ const safeCols = typeof config.terminalWidth === 'number' && config.terminalWidth > 0 ? config.terminalWidth : 80;
54
25
 
55
- const placeholder = isStreaming
56
- ? 'Ctrl+C to interrupt...'
57
- : '/ commands';
58
- const frameWidth = Math.max(2, Math.min(MAX_FRAME_WIDTH, terminalWidth));
59
- const innerWidth = frameWidth - 2;
60
- const inputWidth = Math.max(1, innerWidth - PROMPT.length);
61
- const horizontalRule = '\u2500'.repeat(innerWidth);
26
+ const placeholder = isStreaming ? 'Ctrl+C to interrupt...' : 'type a message...';
27
+ const innerW = safeCols - 2;
62
28
 
63
29
  return h(Box, {
64
30
  width: '100%',
65
31
  height: DOCK_HEIGHT,
66
32
  flexDirection: 'column',
67
- alignItems: 'center',
68
33
  flexShrink: 0,
69
34
  },
70
- h(Box, { width: frameWidth, height: 1, flexShrink: 0 },
71
- h(Text, { color: hex.border }, '\u250C' + horizontalRule + '\u2510')
35
+ h(Box, { height: 1, paddingLeft: 1 },
36
+ h(Text, { color: '#1A1A28' }, '\u2500'.repeat(innerW))
72
37
  ),
73
- h(Box, {
74
- width: frameWidth,
75
- height: 1,
76
- flexDirection: 'row',
77
- flexShrink: 0,
78
- overflow: 'hidden',
79
- },
38
+ h(Box, { height: 1, paddingLeft: 1, paddingRight: 1 },
80
39
  h(Text, { color: hex.border }, '\u2502'),
81
- h(Box, {
82
- width: innerWidth,
83
- height: 1,
84
- flexDirection: 'row',
85
- overflow: 'hidden',
86
- },
87
- h(Text, { color: inputActive ? hex.greenOnline : hex.muted, bold: true }, PROMPT),
88
- h(Box, { width: inputWidth, height: 1, overflow: 'hidden' },
89
- h(TextInput, {
90
- value: value,
91
- onChange: onChange,
92
- onSubmit: onSubmit,
93
- placeholder: placeholder,
94
- focus: inputActive,
95
- showCursor: true,
96
- })
97
- )
40
+ h(Text, { color: inputActive ? hex.neonBlue : '#444466', bold: true }, ' ' + PROMPT),
41
+ h(Box, { flexGrow: 1, height: 1, overflow: 'hidden' },
42
+ h(TextInput, {
43
+ value: value,
44
+ onChange: onChange,
45
+ onSubmit: onSubmit,
46
+ placeholder: placeholder,
47
+ focus: inputActive,
48
+ showCursor: true,
49
+ })
98
50
  ),
51
+ h(Text, { color: '#444466' }, ' '),
99
52
  h(Text, { color: hex.border }, '\u2502')
100
53
  ),
101
- h(Box, { width: frameWidth, height: 1, flexShrink: 0 },
102
- h(Text, { color: hex.border }, '\u2514' + horizontalRule + '\u2518')
54
+ h(Box, { height: 1, paddingLeft: 1, paddingRight: 1 },
55
+ h(Text, { color: hex.border }, '\u2514'),
56
+ h(Box, { flexGrow: 1, height: 1, overflow: 'hidden' },
57
+ h(Text, { color: '#333344' }, '\u2500'.repeat(innerW - 2))
58
+ ),
59
+ h(Text, { color: hex.border }, '\u2518')
60
+ ),
61
+ h(Box, { height: 1, paddingRight: 1 },
62
+ h(Box, { flexGrow: 1 }),
63
+ h(Text, { color: '#444466', bold: modelStatus }, modelLabel || '')
103
64
  )
104
65
  );
105
66
  }
@@ -1,42 +0,0 @@
1
- import React from 'react';
2
- import { Box, Text } from 'ink';
3
- import { hex } from '../config/theme.js';
4
- import { getModels } from '../config/models.js';
5
-
6
- const h = React.createElement;
7
-
8
- const BAR_COLOR = '#1A1A2E';
9
-
10
- export function createModelBar(activeId, cols) {
11
- const models = getModels();
12
- const safeCols = typeof cols === 'number' && cols > 0 ? cols : 80;
13
- const parts = [];
14
-
15
- for (let i = 0; i < models.length; i++) {
16
- const m = models[i];
17
- const isActive = m.id === activeId;
18
- if (i > 0) {
19
- parts.push(h(Text, { key: 'sp-' + i, color: BAR_COLOR }, ' \u2502 '));
20
- }
21
- const prefix = isActive ? '\u25B8 ' : ' ';
22
- const dot = isActive ? '\u25CF' : '\u25CB';
23
- const dotColor = isActive ? hex.greenOnline : '#333355';
24
- const nameColor = isActive ? hex.neonBlue : '#555577';
25
- const badgeColor = isActive ? hex.muted : '#333344';
26
-
27
- parts.push(
28
- h(Text, { key: 'pre-' + i, color: hex.primary }, prefix)
29
- );
30
- parts.push(
31
- h(Text, { key: 'nm-' + i, color: nameColor, bold: isActive }, m.label)
32
- );
33
- parts.push(
34
- h(Text, { key: 'bd-' + i, color: badgeColor }, ' [' + m.badge + ']')
35
- );
36
- parts.push(
37
- h(Text, { key: 'dt-' + i, color: dotColor }, ' ' + dot)
38
- );
39
- }
40
-
41
- return h(Box, { width: '100%', height: 1, paddingLeft: 1 }, ...parts);
42
- }
@@ -1,47 +0,0 @@
1
- import React from 'react';
2
- import { Text } from 'ink';
3
- import { hex } from '../config/theme.js';
4
-
5
- const h = React.createElement;
6
-
7
- const CHAR_SET = [
8
- '\u2726', '\u00B7', '\u2727', '\u00B7', '\u2605', '\u00B7',
9
- '\u2726', '\u00B7', '\u2219', '\u2727', '\u00B7', '\u2726',
10
- '\u2219', '\u2605', '\u2219', '\u2727', '\u2219', '\u2726',
11
- '\u00B7', '\u2727', '\u2605', '\u00B7', '\u2726', '\u00B7',
12
- '\u2219', '\u2727',
13
- ];
14
-
15
- const COLORS = [
16
- '#00F0FF', '#FF9F43', '#00FF88', '#FF6B9D', '#8B5CF6',
17
- '#00F0FF', '#FFD700', '#00FF88',
18
- ];
19
-
20
- function getColor(frame, index, total) {
21
- const phase = Math.floor((index / total) * 52);
22
- const idx = (frame + phase) % COLORS.length;
23
- return COLORS[idx];
24
- }
25
-
26
- export function createParticleRow(frame, cols, key) {
27
- const safeCols = typeof cols === 'number' && cols > 0 ? cols : 80;
28
- const safeFrame = typeof frame === 'number' ? frame : 0;
29
- const parts = [];
30
-
31
- for (let i = 0; i < 26; i++) {
32
- const color = getColor(safeFrame, i, 52);
33
- const ch = CHAR_SET[i % CHAR_SET.length];
34
- parts.push(
35
- h(Text, { key: key + '-' + i, color: color }, ch)
36
- );
37
- }
38
-
39
- return h(Text, null, parts);
40
- }
41
-
42
- export function createParticlePair(frame, cols) {
43
- return [
44
- createParticleRow(frame, cols, 'top'),
45
- createParticleRow(frame, cols, 'bot'),
46
- ];
47
- }