openaxies 0.5.0 → 0.5.1

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.1",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
package/src/App.js CHANGED
@@ -4,12 +4,9 @@ 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
 
@@ -17,9 +14,7 @@ function buildHistory(messages, newText) {
17
14
  const history = [];
18
15
  for (let i = 0; i < messages.length; i++) {
19
16
  const m = messages[i];
20
- if (m === null || m === undefined || typeof m !== 'object') {
21
- continue;
22
- }
17
+ if (m === null || m === undefined || typeof m !== 'object') continue;
23
18
  if (m.role === 'user' || m.role === 'assistant') {
24
19
  const content = typeof m.content === 'string' ? m.content : '';
25
20
  history.push({ role: m.role, content: content });
@@ -32,21 +27,19 @@ function buildHistory(messages, newText) {
32
27
  }
33
28
 
34
29
  function formatTimer(seconds) {
35
- if (typeof seconds !== 'number' || seconds < 0) {
36
- return '0.0s';
37
- }
30
+ if (typeof seconds !== 'number' || seconds < 0) return '0.0s';
38
31
  return seconds.toFixed(1) + 's';
39
32
  }
40
33
 
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);
34
+ function hr(cols) {
35
+ const n = typeof cols === 'number' && cols > 0 ? cols : 80;
36
+ let s = '';
37
+ for (let i = 0; i < n; i++) s = s + '\u2500';
38
+ return h(Text, { color: '#1A1A28' }, s);
48
39
  }
49
40
 
41
+ const SPINNER = ['\u25CB', '\u25D4', '\u25D0', '\u25D5', '\u25CF', '\u25D5', '\u25D0', '\u25D4'];
42
+
50
43
  function AppRoot() {
51
44
  const { stdout } = useStdout();
52
45
  const rows = stdout.rows || 24;
@@ -57,20 +50,16 @@ function AppRoot() {
57
50
  const [streamText, setStreamText] = React.useState('');
58
51
  const [streamingActive, setStreamingActive] = React.useState(false);
59
52
  const [isThinking, setIsThinking] = React.useState(false);
60
- const [spinnerChar, setSpinnerChar] = React.useState('');
53
+ const [spinnerIdx, setSpinnerIdx] = React.useState(0);
61
54
  const [thinkingElapsed, setThinkingElapsed] = React.useState(0);
62
55
  const [inputBuffer, setInputBuffer] = React.useState('');
63
56
  const [showOverlay, setShowOverlay] = React.useState(false);
64
57
  const [overlayIndex, setOverlayIndex] = React.useState(0);
65
- const [particleFrame, setParticleFrame] = React.useState(0);
66
58
  const [toolInfo, setToolInfo] = React.useState(null);
67
59
 
68
60
  const commands = getCommands();
69
- const MODEL_BAR_H = 1;
70
- const SEP_H = 1;
71
- const DOCK = DOCK_HEIGHT;
72
61
  const overlayH = showOverlay ? 6 : 0;
73
- const fixedH = MODEL_BAR_H + SEP_H + overlayH + DOCK;
62
+ const fixedH = 1 + overlayH + DOCK_HEIGHT;
74
63
  const availLines = Math.max(1, rows - fixedH);
75
64
 
76
65
  const abortRef = React.useRef(null);
@@ -78,27 +67,6 @@ function AppRoot() {
78
67
  const spinnerRef = React.useRef(null);
79
68
  const timerRef = React.useRef(null);
80
69
  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
70
 
103
71
  React.useEffect(function () {
104
72
  if (isThinking === false) {
@@ -110,14 +78,12 @@ function AppRoot() {
110
78
  clearInterval(timerRef.current);
111
79
  timerRef.current = null;
112
80
  }
113
- setSpinnerChar('');
114
81
  return;
115
82
  }
116
83
  let idx = 0;
117
- setSpinnerChar(SPINNER_FRAMES[0]);
118
84
  spinnerRef.current = setInterval(function () {
119
- idx = (idx + 1) % SPINNER_FRAMES.length;
120
- setSpinnerChar(SPINNER_FRAMES[idx]);
85
+ idx = (idx + 1) % SPINNER.length;
86
+ setSpinnerIdx(idx);
121
87
  }, 100);
122
88
  timerRef.current = setInterval(function () {
123
89
  if (thinkStartRef.current !== null) {
@@ -148,36 +114,33 @@ function AppRoot() {
148
114
  });
149
115
  }
150
116
 
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;
117
+ function checkThink(text) {
118
+ const o = text.lastIndexOf('<think>');
119
+ const c = text.lastIndexOf('</think>');
120
+ if (o === -1 && c === -1) return false;
121
+ return o > c;
156
122
  }
157
123
 
158
124
  async function handleSubmit(text) {
159
- const safeText = typeof text === 'string' ? text : '';
160
- if (safeText.length === 0 || streamingActive === true) return;
125
+ const safe = typeof text === 'string' ? text : '';
126
+ if (safe.length === 0 || streamingActive === true) return;
161
127
 
162
128
  setStreamingActive(true);
163
129
  setToolInfo(null);
164
130
  setStreamText(' connecting');
165
131
  setThinkingElapsed(0);
132
+
166
133
  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...';
134
+ setStreamText(function (p) {
135
+ if (p === ' connecting') return ' connecting.';
136
+ if (p === ' connecting.') return ' connecting..';
137
+ if (p === ' connecting..') return ' connecting...';
171
138
  return ' connecting';
172
139
  });
173
140
  }, 400);
174
141
 
175
- setMessages(function (prev) {
176
- return prev.concat([{
177
- role: 'user',
178
- content: safeText,
179
- id: 'u-' + Date.now(),
180
- }]);
142
+ setMessages(function (p) {
143
+ return p.concat([{ role: 'user', content: safe, id: 'u-' + Date.now() }]);
181
144
  });
182
145
  setInputBuffer('');
183
146
 
@@ -189,17 +152,13 @@ function AppRoot() {
189
152
  }
190
153
  setStreamingActive(false);
191
154
  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
- }]);
155
+ setMessages(function (p) {
156
+ return p.concat([{ role: 'assistant', content: 'No model selected. Use /model.', id: 'e-' + Date.now() }]);
198
157
  });
199
158
  return;
200
159
  }
201
160
 
202
- const history = buildHistory(messages, safeText);
161
+ const history = buildHistory(messages, safe);
203
162
  const abortController = new AbortController();
204
163
  abortRef.current = abortController;
205
164
 
@@ -221,12 +180,7 @@ function AppRoot() {
221
180
  }
222
181
 
223
182
  if (event.type === 'tool') {
224
- setToolInfo({
225
- tool: event.tool,
226
- used: event.used,
227
- sites: event.sites,
228
- query: event.query,
229
- });
183
+ setToolInfo({ tool: event.tool, used: event.used, sites: event.sites, query: event.query });
230
184
  continue;
231
185
  }
232
186
 
@@ -234,15 +188,15 @@ function AppRoot() {
234
188
  const content = typeof event.content === 'string' ? event.content : '';
235
189
  for (let i = 0; i < content.length; i++) {
236
190
  accumulated = accumulated + content[i];
237
- const insideThink = checkThinkState(accumulated);
191
+ const inside = checkThink(accumulated);
238
192
  setStreamText(accumulated);
239
- setIsThinking(insideThink);
240
- if (insideThink === true && thinkStarted === false) {
193
+ setIsThinking(inside);
194
+ if (inside === true && thinkStarted === false) {
241
195
  thinkStarted = true;
242
196
  thinkStartRef.current = Date.now();
243
197
  setThinkingElapsed(0);
244
198
  }
245
- if (insideThink === false && thinkStarted === true) {
199
+ if (inside === false && thinkStarted === true) {
246
200
  thinkStarted = false;
247
201
  thinkStartRef.current = null;
248
202
  setThinkingElapsed(0);
@@ -274,12 +228,8 @@ function AppRoot() {
274
228
  setStreamText('');
275
229
 
276
230
  if (finalText.length > 0) {
277
- setMessages(function (prev) {
278
- return prev.concat([{
279
- role: 'assistant',
280
- content: finalText,
281
- id: 'a-' + Date.now(),
282
- }]);
231
+ setMessages(function (p) {
232
+ return p.concat([{ role: 'assistant', content: finalText, id: 'a-' + Date.now() }]);
283
233
  });
284
234
  }
285
235
  } catch (err) {
@@ -295,52 +245,38 @@ function AppRoot() {
295
245
  const errMsg = err !== null && err !== undefined
296
246
  ? (err.message || String(err))
297
247
  : 'Unknown error';
298
- setMessages(function (prev) {
299
- return prev.concat([{
248
+ setMessages(function (p) {
249
+ return p.concat([{
300
250
  role: 'assistant',
301
- content: '**Error:** ' + errMsg + '\n\nThe Space may be cold-starting (30\u2013120 s first request). Try again.',
251
+ content: 'Error: ' + errMsg + '\n\nSpace cold-starting? Try again.',
302
252
  id: 'er-' + Date.now(),
303
253
  }]);
304
254
  });
305
255
  }
306
256
  }
307
257
 
308
- function handleInputChange(v) {
309
- const safe = typeof v === 'string' ? v : '';
310
- setInputBuffer(safe);
311
- if (safe.length === 1 && safe[0] === '/' && showOverlay === false) {
258
+ function handleChange(v) {
259
+ const s = typeof v === 'string' ? v : '';
260
+ setInputBuffer(s);
261
+ if (s.length === 1 && s[0] === '/' && showOverlay === false) {
312
262
  setShowOverlay(true);
313
263
  setOverlayIndex(0);
314
264
  }
315
- if (showOverlay === true && (safe.length === 0 || safe[0] !== '/')) {
265
+ if (showOverlay === true && (s.length === 0 || s[0] !== '/')) {
316
266
  setShowOverlay(false);
317
267
  }
318
268
  }
319
269
 
320
- function handleSlashSelect(item) {
270
+ function handleCmd(item) {
321
271
  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
- }]);
272
+ const t = item.value;
273
+ if (typeof t !== 'string') return;
274
+ if (t === '/exit') process.exit(0);
275
+ if (t === '/clear') setMessages([]);
276
+ if (t === '/model') cycleModel();
277
+ if (t === '/help') {
278
+ setMessages(function (p) {
279
+ return p.concat([{ role: 'assistant', content: 'Commands: /help /clear /model /exit', id: 'h-' + Date.now() }]);
344
280
  });
345
281
  }
346
282
  setInputBuffer('');
@@ -366,17 +302,15 @@ function AppRoot() {
366
302
  }
367
303
  if (key.return === true) {
368
304
  const cmd = commands[overlayIndex];
369
- if (cmd !== null && cmd !== undefined) handleSlashSelect({ value: cmd.trigger });
305
+ if (cmd !== null && cmd !== undefined) handleCmd({ value: cmd.trigger });
370
306
  return;
371
307
  }
372
308
  return;
373
309
  }
374
310
 
375
311
  if (key.return === true) {
376
- const safe = typeof inputBuffer === 'string' ? inputBuffer : '';
377
- if (safe.length > 0 && streamingActive === false) {
378
- handleSubmit(safe);
379
- }
312
+ const s = typeof inputBuffer === 'string' ? inputBuffer : '';
313
+ if (s.length > 0 && streamingActive === false) handleSubmit(s);
380
314
  return;
381
315
  }
382
316
 
@@ -391,8 +325,9 @@ function AppRoot() {
391
325
  }
392
326
  });
393
327
 
394
- const modelBar = createModelBar(activeModel, cols);
395
- const particleTop = createParticleRow(particleFrame, cols, 'tp');
328
+ const activeInfo = getModelById(activeModel);
329
+ const activeLabel = activeInfo !== null ? activeInfo.label : 'OpenAxies';
330
+ const activeBadge = activeInfo !== null ? activeInfo.badge : '';
396
331
 
397
332
  const viewLines = [];
398
333
  for (let i = 0; i < messages.length; i++) {
@@ -401,103 +336,77 @@ function AppRoot() {
401
336
  const content = typeof msg.content === 'string' ? msg.content : '';
402
337
  if (content.length === 0) continue;
403
338
 
404
- if (i > 0) viewLines.push({ type: 'sep' });
405
-
406
339
  if (msg.role === 'user') {
407
- viewLines.push({ type: 'user', text: content });
408
- viewLines.push({ type: 'sep' });
340
+ if (i > 0) viewLines.push({ t: 'sep' });
341
+ viewLines.push({ t: 'user', text: content });
342
+ viewLines.push({ t: 'sep' });
409
343
  } else {
410
- viewLines.push({ type: 'assistant', text: content });
344
+ viewLines.push({ t: 'text', text: content });
411
345
  }
412
346
  }
413
347
 
414
348
  const hasStream = typeof streamText === 'string' && streamText.length > 0;
415
349
  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
- });
350
+ viewLines.push({ t: 'sep' });
351
+ if (toolInfo !== null && toolInfo.used === true && toolInfo.query.length > 0) {
352
+ viewLines.push({ t: 'tool', text: '\u2500\u2500 ' + toolInfo.tool + ': "' + toolInfo.query + '" (' + toolInfo.sites + ' sites)' });
423
353
  }
424
-
425
354
  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
- });
355
+ viewLines.push({ t: 'think', spin: SPINNER[spinnerIdx], elapsed: formatTimer(thinkingElapsed) });
432
356
  const clean = streamText.split('<think>').join('').split('</think>').join('');
433
- viewLines.push({ type: 'stream', text: clean, color: '#FF9F43' });
357
+ viewLines.push({ t: 'think-text', text: clean });
434
358
  } else {
435
359
  const clean = streamText.split('<think>').join('').split('</think>').join('');
436
- viewLines.push({ type: 'stream', text: clean, color: hex.primary });
360
+ viewLines.push({ t: 'text', text: clean });
437
361
  }
438
362
  }
439
363
 
440
364
  if (viewLines.length === 0) {
441
- viewLines.push({ type: 'muted', text: ' type a message to start...' });
365
+ viewLines.push({ t: 'idle', text: ' start typing to chat...' });
442
366
  }
443
367
 
444
- const visibleHeight = Math.max(1, availLines - 1);
445
- const displayLines = viewLines.slice(-visibleHeight);
446
- const viewElements = [];
447
-
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
- )
368
+ const visible = Math.max(1, availLines - 1);
369
+ const display = viewLines.slice(-visible);
370
+ const viewEls = [];
371
+
372
+ for (let i = 0; i < display.length; i++) {
373
+ const l = display[i];
374
+ if (l.t === 'sep') {
375
+ viewEls.push(h(Box, { key: 's-' + i, height: 1 }, hr(cols)));
376
+ } else if (l.t === 'think') {
377
+ viewEls.push(
378
+ h(Box, { key: 'k-' + i, height: 1, paddingLeft: 1 },
379
+ h(Text, { color: '#FF9F43', bold: true }, ' ' + l.spin + ' Thinking \u2022 ' + l.elapsed)
458
380
  )
459
381
  );
460
- } else if (line.type === 'tool') {
461
- viewElements.push(
462
- h(Box, { key: 'vtl-' + i, height: 1 },
463
- h(Text, { color: '#5B5B8A' }, line.text)
382
+ } else if (l.t === 'think-text') {
383
+ viewEls.push(
384
+ h(Box, { key: 'kt-' + i, height: 1, paddingLeft: 2 },
385
+ h(Text, { color: '#FF9F43' }, l.text)
464
386
  )
465
387
  );
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)
388
+ } else if (l.t === 'tool') {
389
+ viewEls.push(
390
+ h(Box, { key: 'tl-' + i, height: 1, paddingLeft: 1 },
391
+ h(Text, { color: '#5B5B8A' }, l.text)
475
392
  )
476
393
  );
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)
394
+ } else if (l.t === 'user') {
395
+ viewEls.push(
396
+ h(Box, { key: 'u-' + i, height: 1, paddingLeft: 1 },
397
+ h(Text, { color: hex.neonBlue }, l.text)
485
398
  )
486
399
  );
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)
400
+ } else if (l.t === 'text') {
401
+ viewEls.push(
402
+ h(Box, { key: 'a-' + i, height: 1, paddingLeft: 1 },
403
+ h(Text, { color: hex.primary }, l.text)
495
404
  )
496
405
  );
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)
406
+ } else if (l.t === 'idle') {
407
+ viewEls.push(
408
+ h(Box, { key: 'i-' + i, height: 1, paddingLeft: 1 },
409
+ h(Text, { color: '#333355' }, l.text)
501
410
  )
502
411
  );
503
412
  }
@@ -509,9 +418,7 @@ function AppRoot() {
509
418
  flexDirection: 'column',
510
419
  overflow: 'hidden',
511
420
  paddingBottom: 1,
512
- }, ...viewElements);
513
-
514
- const particleBot = createParticleRow(particleFrame, cols, 'bt');
421
+ }, ...viewEls);
515
422
 
516
423
  const slashOverlay = showOverlay === true
517
424
  ? createSlashOverlay(commands, overlayIndex)
@@ -519,11 +426,13 @@ function AppRoot() {
519
426
 
520
427
  const dock = createComposerDock({
521
428
  value: inputBuffer,
522
- onChange: handleInputChange,
429
+ onChange: handleChange,
523
430
  onSubmit: function () {},
524
431
  inputActive: showOverlay === false,
525
432
  isStreaming: streamingActive,
526
433
  terminalWidth: cols,
434
+ modelLabel: activeLabel + ' \u25CF',
435
+ modelStatus: true,
527
436
  });
528
437
 
529
438
  return h(Box, {
@@ -533,11 +442,8 @@ function AppRoot() {
533
442
  backgroundColor: hex.black,
534
443
  overflow: 'hidden',
535
444
  },
536
- modelBar,
537
- h(Box, { height: 1 }, barLine(cols)),
538
- h(Box, { height: 1, paddingLeft: 1 }, particleTop),
445
+ h(Box, { height: 1 }, hr(cols)),
539
446
  viewport,
540
- h(Box, { height: 1, paddingLeft: 1 }, particleBot),
541
447
  slashOverlay,
542
448
  dock
543
449
  );
@@ -1,105 +1,54 @@
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
8
  export const DOCK_HEIGHT = 3;
9
- const MAX_FRAME_WIDTH = 80;
10
- const PROMPT = 'openaxies@root:~$ ';
11
9
 
12
- function checkString(value) {
13
- if (typeof value !== 'string') {
14
- return '';
15
- }
16
- return value;
17
- }
10
+ const PROMPT = '> ';
18
11
 
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
- }
24
-
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;
54
24
 
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);
25
+ const placeholder = isStreaming ? 'Ctrl+C to interrupt...' : 'type a message...';
62
26
 
63
27
  return h(Box, {
64
28
  width: '100%',
65
29
  height: DOCK_HEIGHT,
66
30
  flexDirection: 'column',
67
- alignItems: 'center',
68
31
  flexShrink: 0,
69
32
  },
70
- h(Box, { width: frameWidth, height: 1, flexShrink: 0 },
71
- h(Text, { color: hex.border }, '\u250C' + horizontalRule + '\u2510')
72
- ),
73
- h(Box, {
74
- width: frameWidth,
75
- height: 1,
76
- flexDirection: 'row',
77
- flexShrink: 0,
78
- overflow: 'hidden',
79
- },
80
- 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
- )
33
+ h(Box, { height: 1, paddingLeft: 1, paddingRight: 1 },
34
+ h(Text, { color: inputActive ? hex.primary : '#444466' }, PROMPT),
35
+ h(Box, { flexGrow: 1, height: 1, overflow: 'hidden' },
36
+ h(TextInput, {
37
+ value: value,
38
+ onChange: onChange,
39
+ onSubmit: onSubmit,
40
+ placeholder: placeholder,
41
+ focus: inputActive,
42
+ showCursor: true,
43
+ })
98
44
  ),
99
- h(Text, { color: hex.border }, '\u2502')
45
+ h(Text, { color: '#444466' }, ' '),
46
+ h(Text, { color: modelStatus ? hex.greenOnline : '#444466', bold: modelStatus }, modelLabel || '')
100
47
  ),
101
- h(Box, { width: frameWidth, height: 1, flexShrink: 0 },
102
- h(Text, { color: hex.border }, '\u2514' + horizontalRule + '\u2518')
48
+ h(Box, { height: 1, paddingLeft: 1, paddingRight: 1 },
49
+ h(Text, { color: '#333344' }, '/help /clear /model /exit'),
50
+ h(Box, { flexGrow: 1 }),
51
+ h(Text, { color: '#333344' }, isStreaming ? 'Ctrl+C to stop' : '')
103
52
  )
104
53
  );
105
54
  }
@@ -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
- }