openaxies 0.7.0 → 1.0.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.
@@ -0,0 +1,6 @@
1
+ The above error occurred in the <AppRoot> component:
2
+
3
+ at AppRoot (file:///storage/emulated/0/agent-open/openaxis/src/App.js:60:22)
4
+ at App (file:///storage/emulated/0/agent-open/openaxis/node_modules/ink/build/components/App.js:17:16)
5
+
6
+ React will try to recreate this component tree from scratch using the error boundary you provided, InternalApp.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openaxies",
3
- "version": "0.7.0",
3
+ "version": "1.0.1",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
package/src/App.js CHANGED
@@ -1,471 +1,194 @@
1
1
  import React from 'react';
2
- import { Box, Text, useInput, useStdout } from 'ink';
2
+ import { Box, Text, useStdout } from 'ink';
3
+ import TextInput from 'ink-text-input';
3
4
  import { hex } from './config/theme.js';
4
5
  import { getModels, getModelById, DEFAULT_MODEL_ID } from './config/models.js';
5
6
  import { callModel } from './providers/index.js';
6
- import { createHeaderBar, HEADER_HEIGHT } from './components/BrandHeader.js';
7
- import TextInput from 'ink-text-input';
7
+ import { Typography } from './components/ui/Typography.js';
8
+ import { Separator } from './components/ui/Separator.js';
9
+ import { ActivityItem } from './components/timeline/ActivityItem.js';
8
10
 
9
11
  const h = React.createElement;
10
- export default AppRoot;
11
12
 
12
13
  const STARTUP_LOGO = [
13
14
  ' \u2588\u2588\u2588\u2588\u2588\u2588\u2557',
14
- ' \u2588\u2588\u2594\u2594\u2594\u2594\u2588\u2588\u2557',
15
- ' \u2588\u2588\u2591 \u2588\u2588\u2591',
16
- ' \u2588\u2588\u2591 \u2588\u2588\u2591',
17
- ' \u255a\u2588\u2588\u2588\u2588\u2588\u2588\u2594\u259d',
15
+ ' \u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557',
16
+ ' \u2588\u2588\u2551 \u2588\u2588\u2551',
17
+ ' \u2588\u2588\u2551 \u2588\u2588\u2551',
18
+ ' \u255a\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255d',
18
19
  ];
19
20
 
20
- function buildHistory(messages, newText) {
21
- const h = [];
22
- for (let i = 0; i < messages.length; i++) {
23
- const m = messages[i];
24
- if (m === null || m === undefined || typeof m !== 'object') continue;
25
- if (m.role === 'user' || m.role === 'assistant') {
26
- const c = typeof m.content === 'string' ? m.content : '';
27
- h.push({ role: m.role, content: c });
28
- }
29
- }
30
- if (typeof newText === 'string' && newText.length > 0) {
31
- h.push({ role: 'user', content: newText });
32
- }
33
- return h;
34
- }
35
-
36
- function fmtTime(s) {
37
- if (typeof s !== 'number' || s < 0) return '0.0s';
38
- return s.toFixed(1) + 's';
39
- }
40
-
41
- function hr(cols) {
42
- const n = typeof cols === 'number' && cols > 0 ? cols : 80;
43
- let s = '';
44
- for (let i = 0; i < n; i++) s = s + '\u2500';
45
- return h(Text, { color: '#1A1A28' }, s);
46
- }
47
-
48
- const SPINNER = ['\u25CB', '\u25D4', '\u25D0', '\u25D5', '\u25CF', '\u25D5', '\u25D0', '\u25D4', '\u25D1', '\u25D2', '\u25D3'];
49
-
50
- const COMMANDS = [
51
- { id: 'model', trigger: '/model', desc: 'Switch model' },
52
- { id: 'thoughts', trigger: '/thoughts', desc: 'Toggle thinking visibility' },
53
- { id: 'resume', trigger: '/resume', desc: 'Re-run last query' },
54
- { id: 'clear', trigger: '/clear', desc: 'Clear conversation' },
55
- { id: 'help', trigger: '/help', desc: 'Show commands' },
56
- { id: 'exit', trigger: '/exit', desc: 'Quit' },
57
- ];
58
-
59
- function AppRoot() {
21
+ export default function AppRoot() {
60
22
  const { stdout } = useStdout();
61
23
  const rows = stdout.rows || 24;
62
24
  const cols = stdout.columns || 80;
63
25
 
64
26
  const [messages, setMessages] = React.useState([]);
65
27
  const [activeModel, setActiveModel] = React.useState(DEFAULT_MODEL_ID);
66
- const [streamText, setStreamText] = React.useState('');
28
+ const [inputBuffer, setInputBuffer] = React.useState('');
67
29
  const [streamingActive, setStreamingActive] = React.useState(false);
68
- const [isThinking, setIsThinking] = React.useState(false);
69
- const [spinnerIdx, setSpinnerIdx] = React.useState(0);
30
+ const [streamContent, setStreamContent] = React.useState('');
31
+ const [thoughtContent, setThoughtContent] = React.useState('');
70
32
  const [thinkingElapsed, setThinkingElapsed] = React.useState(0);
71
- const [inputBuffer, setInputBuffer] = React.useState('');
72
- const [showOverlay, setShowOverlay] = React.useState(false);
73
- const [overlayIndex, setOverlayIndex] = React.useState(0);
74
- const [toolInfo, setToolInfo] = React.useState(null);
75
- const [showThoughts, setShowThoughts] = React.useState(true);
33
+ const [isThinking, setIsThinking] = React.useState(false);
76
34
  const [startupDone, setStartupDone] = React.useState(false);
77
35
 
78
- const SEP_H = 1;
79
- const FIXED_H = startupDone ? (HEADER_HEIGHT + SEP_H + 1 + 1) : 0;
80
- const DOCK_H = 1;
81
- const STATUS_H = 1;
82
- const totalFixed = startupDone ? (HEADER_HEIGHT + SEP_H + DOCK_H + STATUS_H) : 11;
83
- const availLines = Math.max(10, rows - totalFixed);
84
-
85
- const abortRef = React.useRef(null);
86
- const connRef = React.useRef(null);
87
- const spinnerRef = React.useRef(null);
88
- const timerRef = React.useRef(null);
89
- const thinkStartRef = React.useRef(null);
90
- const lastQueryRef = React.useRef('');
36
+ const abortControllerRef = React.useRef(null);
91
37
 
92
- React.useEffect(function () {
93
- if (isThinking === false) return;
94
- let idx = 0;
95
- const si = setInterval(function () { idx = (idx + 1) % SPINNER.length; setSpinnerIdx(idx); }, 120);
96
- const ti = setInterval(function () {
97
- if (thinkStartRef.current !== null) setThinkingElapsed((Date.now() - thinkStartRef.current) / 1000);
98
- }, 100);
99
- spinnerRef.current = si;
100
- timerRef.current = ti;
101
- return function () { clearInterval(si); clearInterval(ti); };
38
+ React.useEffect(() => {
39
+ let timer;
40
+ if (isThinking) {
41
+ timer = setInterval(() => {
42
+ setThinkingElapsed(prev => prev + 0.1);
43
+ }, 100);
44
+ }
45
+ return () => clearInterval(timer);
102
46
  }, [isThinking]);
103
47
 
104
- function cycleModel() {
105
- const models = getModels();
106
- setActiveModel(function (prev) {
107
- let found = false;
108
- for (let i = 0; i < models.length; i++) {
109
- if (found) return models[i].id;
110
- if (models[i].id === prev) found = true;
111
- }
112
- return models[0].id;
113
- });
114
- }
115
-
116
- function checkThink(text) {
117
- const o = text.lastIndexOf('<think>');
118
- const c = text.lastIndexOf('</think>');
119
- if (o === -1 && c === -1) return false;
120
- return o > c;
121
- }
122
-
123
- function filterText(t) {
124
- if (showThoughts === true) return t;
125
- return t.split('<think>').join('').split('</think>').join('');
126
- }
48
+ const model = getModelById(activeModel);
49
+ const modelLabel = model ? model.label : 'Unknown Model';
127
50
 
128
51
  async function handleSubmit(text) {
129
- const safe = typeof text === 'string' ? text : '';
130
- if (safe.length === 0 || streamingActive === true) return;
131
- if (startupDone === false) setStartupDone(true);
132
- lastQueryRef.current = safe;
52
+ if (!text || streamingActive) return;
53
+ setStartupDone(true);
54
+
55
+ const userMsg = { type: 'user', content: text };
56
+ setMessages(prev => [...prev, userMsg]);
57
+ setInputBuffer('');
133
58
  setStreamingActive(true);
134
- setToolInfo(null);
135
- setStreamText(' connecting');
59
+ setStreamContent('');
60
+ setThoughtContent('');
136
61
  setThinkingElapsed(0);
137
- connRef.current = setInterval(function () {
138
- setStreamText(function (p) {
139
- if (p === ' connecting') return ' connecting.';
140
- if (p === ' connecting.') return ' connecting..';
141
- if (p === ' connecting..') return ' connecting...';
142
- return ' connecting';
143
- });
144
- }, 400);
145
- setMessages(function (p) { return p.concat([{ role: 'user', content: safe, id: 'u-' + Date.now() }]); });
146
- setInputBuffer('');
147
- const modelInfo = getModelById(activeModel);
148
- if (modelInfo === null) {
149
- if (connRef.current !== null) clearInterval(connRef.current), connRef.current = null;
150
- setStreamingActive(false); setStreamText('');
151
- setMessages(function (p) { return p.concat([{ role: 'assistant', content: 'No model selected.', id: 'e-' + Date.now() }]); });
152
- return;
153
- }
154
- const history = buildHistory(messages, safe);
155
- const abortController = new AbortController();
156
- abortRef.current = abortController;
62
+
63
+ abortControllerRef.current = new AbortController();
64
+
157
65
  try {
158
- const gen = callModel(modelInfo, history, abortController.signal);
159
- let accumulated = '';
160
- let firstEvent = true;
161
- let thinkStarted = false;
162
- for await (const event of gen) {
163
- if (abortController.signal.aborted === true) break;
164
- if (firstEvent === true) { firstEvent = false; if (connRef.current !== null) clearInterval(connRef.current), connRef.current = null; }
165
- if (event.type === 'tool') { setToolInfo({ tool: event.tool, used: event.used, sites: event.sites, query: event.query }); continue; }
166
- if (event.type === 'thinking' || event.type === 'token') {
167
- const c = typeof event.content === 'string' ? event.content : '';
168
- accumulated = accumulated + c;
169
- const inside = checkThink(accumulated);
170
- setStreamText(accumulated);
171
- setIsThinking(inside);
172
- if (inside === true && thinkStarted === false) { thinkStarted = true; thinkStartRef.current = Date.now(); setThinkingElapsed(0); }
173
- if (inside === false && thinkStarted === true) { thinkStarted = false; thinkStartRef.current = null; setThinkingElapsed(0); }
174
- }
175
- if (event.type === 'done') break;
176
- if (event.type === 'timeout') {
177
- if (accumulated.length > 0) accumulated = accumulated + '\n\n\u2014 stream timed out \u2014';
178
- setStreamText(accumulated); break;
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);
89
+ } else {
90
+ setStreamContent(prev => prev + content);
91
+ }
179
92
  }
180
93
  }
181
- if (connRef.current !== null) clearInterval(connRef.current), connRef.current = null;
182
- setIsThinking(false); thinkStartRef.current = null; setThinkingElapsed(0); setStreamingActive(false);
183
- setStreamText('');
184
- if (accumulated.length > 0) {
185
- setMessages(function (p) { return p.concat([{ role: 'assistant', content: accumulated, id: 'a-' + Date.now() }]); });
186
- }
187
- } catch (err) {
188
- if (connRef.current !== null) clearInterval(connRef.current), connRef.current = null;
189
- setIsThinking(false); thinkStartRef.current = null; setThinkingElapsed(0); setStreamingActive(false); setStreamText('');
190
- const errMsg = err !== null && err !== undefined ? (err.message || String(err)) : 'Unknown error';
191
- setMessages(function (p) { return p.concat([{ role: 'assistant', content: 'Error: ' + errMsg, id: 'er-' + Date.now() }]); });
192
- }
193
- }
194
-
195
- function handleChange(v) {
196
- setInputBuffer(v);
197
- if (v.length === 1 && v[0] === '/') { setShowOverlay(true); setOverlayIndex(0); }
198
- if (showOverlay === true && (v.length === 0 || v[0] !== '/')) { setShowOverlay(false); }
199
- }
200
-
201
- function handleCmd(item) {
202
- if (item === null || item === undefined) return;
203
- const t = item.trigger;
204
- if (typeof t !== 'string') return;
205
- if (t === '/exit') process.exit(0);
206
- if (t === '/clear') { setMessages([]); }
207
- if (t === '/model') cycleModel();
208
- if (t === '/thoughts') { setShowThoughts(function (p) { return !p; }); }
209
- if (t === '/resume') {
210
- const q = lastQueryRef.current;
211
- if (q.length > 0 && streamingActive === false) handleSubmit(q);
212
- }
213
- if (t === '/help') { setMessages(function (p) { return p.concat([{ role: 'assistant', content: 'Commands: /help /clear /model /thoughts /resume /exit', id: 'h-' + Date.now() }]); }); }
214
- setInputBuffer(''); setShowOverlay(false); setOverlayIndex(0);
215
- }
216
-
217
- useInput(function handleInput(input, key) {
218
- if (showOverlay === true) {
219
- if (key.escape === true) { setShowOverlay(false); setInputBuffer(''); setOverlayIndex(0); return; }
220
- if (key.upArrow === true) { setOverlayIndex(function (p) { return p > 0 ? p - 1 : COMMANDS.length - 1; }); return; }
221
- if (key.downArrow === true) { setOverlayIndex(function (p) { return p < COMMANDS.length - 1 ? p + 1 : 0; }); return; }
222
- if (key.return === true) { const cmd = COMMANDS[overlayIndex]; if (cmd !== null && cmd !== undefined) handleCmd(cmd); return; }
223
- return;
224
- }
225
- if (key.escape === true) {
226
- if (streamingActive === true) {
227
- if (abortRef.current !== null) { try { abortRef.current.abort(); } catch (_) {} abortRef.current = null; }
228
- setStreamingActive(false); setStreamText(''); setIsThinking(false);
229
- }
230
- return;
231
- }
232
- if (key.return === true) {
233
- const s = typeof inputBuffer === 'string' ? inputBuffer : '';
234
- if (s.length > 0 && streamingActive === false) handleSubmit(s);
235
- return;
236
- }
237
- if (key.ctrl === true) {
238
- if (input === 'c' || input === 'C') {
239
- if (abortRef.current !== null) { try { abortRef.current.abort(); } catch (_) {} abortRef.current = null; }
240
- setStreamingActive(false); setStreamText(''); setIsThinking(false);
241
- process.exit(0);
242
- }
243
- if (input === 't' || input === 'T') { setShowThoughts(function (p) { return !p; }); return; }
244
- if (input === 'p' || input === 'P') { cycleModel(); return; }
245
- if (input === 'r' || input === 'R') {
246
- const q = lastQueryRef.current;
247
- if (q.length > 0 && streamingActive === false) handleSubmit(q);
248
- return;
94
+ } catch (e) {
95
+ setMessages(prev => [...prev, { type: 'assistant', content: 'Error connecting to model.' }]);
96
+ } finally {
97
+ setStreamingActive(false);
98
+ setIsThinking(false);
99
+ if (streamContent) {
100
+ setMessages(prev => [...prev, { type: 'assistant', content: streamContent }]);
249
101
  }
250
- if (input === 'l' || input === 'L') { setMessages([]); return; }
251
- if (input === 'k' || input === 'K') { setInputBuffer('/'); setShowOverlay(true); setOverlayIndex(0); return; }
252
- }
253
- });
254
-
255
- const modelInfo = getModelById(activeModel);
256
- const modelLabel = modelInfo !== null ? modelInfo.label : 'OpenAxies Llama';
257
-
258
- // Build viewport sections
259
- const sections = [];
260
-
261
- // Overlay items
262
- if (showOverlay === true) {
263
- sections.push({ t: 'sep' });
264
- sections.push({ t: 'overlay_header' });
265
- for (let i = 0; i < COMMANDS.length; i++) {
266
- const c = COMMANDS[i];
267
- sections.push({ t: 'cmd', trigger: c.trigger, desc: c.desc, sel: i === overlayIndex });
268
- }
269
- sections.push({ t: 'sep' });
270
- }
271
-
272
- // Messages
273
- for (let i = 0; i < messages.length; i++) {
274
- const msg = messages[i];
275
- if (msg === null || typeof msg !== 'object') continue;
276
- const content = typeof msg.content === 'string' ? msg.content : '';
277
- if (content.length === 0) continue;
278
- if (msg.role === 'user') {
279
- sections.push({ t: 'sep' });
280
- sections.push({ t: 'user', text: content });
281
- } else {
282
- if (sections.length > 0 && sections[sections.length - 1].t !== 'sep') sections.push({ t: 'sep' });
283
- sections.push({ t: 'assistant', text: filterText(content) });
284
102
  }
285
103
  }
286
104
 
287
- // Streaming
288
- if (typeof streamText === 'string' && streamText.length > 0) {
289
- const clean = filterText(streamText);
290
- if (toolInfo !== null && toolInfo.used === true && toolInfo.query.length > 0) {
291
- sections.push({ t: 'tool', tool: toolInfo.tool, query: toolInfo.query, sites: toolInfo.sites });
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
+ };
143
+
144
+ const renderTimeline = () => {
145
+ const events = [...messages];
146
+ if (isThinking) {
147
+ events.push({ type: 'thought', content: thoughtContent, elapsed: thinkingElapsed.toFixed(1) + 's' });
292
148
  }
293
- if (isThinking === true) {
294
- sections.push({ t: 'thinking', spin: SPINNER[spinnerIdx], elapsed: fmtTime(thinkingElapsed), content: clean });
295
- } else {
296
- sections.push({ t: 'stream', text: clean });
149
+ if (streamingActive && streamContent) {
150
+ events.push({ type: 'assistant', content: streamContent });
297
151
  }
298
- }
299
-
300
- // Startup screen
301
- if (startupDone === false && messages.length === 0) {
302
- sections.push({ t: 'startup' });
303
- }
304
-
305
- // Ready state
306
- if (sections.length === 0) {
307
- sections.push({ t: 'sep' });
308
- sections.push({ t: 'ready' });
309
- sections.push({ t: 'sep' });
310
- }
311
-
312
- // Estimate and limit sections
313
- function estLines(s) {
314
- if (s.t === 'sep' || s.t === 'assistant' || s.t === 'stream' || s.t === 'tool' || s.t === 'ready' || s.t === 'overlay_header') return 1;
315
- if (s.t === 'cmd') return 1;
316
- if (s.t === 'user') return 2;
317
- if (s.t === 'thinking') return 2;
318
- if (s.t === 'startup') return 11;
319
- return 1;
320
- }
321
-
322
- let totalEst = 0;
323
- let lastN = 0;
324
- for (let i = sections.length - 1; i >= 0; i--) {
325
- const l = estLines(sections[i]);
326
- if (totalEst + l > availLines) break;
327
- totalEst += l;
328
- lastN = sections.length - i;
329
- }
330
- const visible = sections.slice(sections.length - lastN);
331
-
332
- // Render sections
333
- const sectionEls = [];
334
- for (let i = 0; i < visible.length; i++) {
335
- const s = visible[i];
336
- const ki = 's-' + i;
337
-
338
- if (s.t === 'sep') {
339
- sectionEls.push(h(Box, { key: ki, height: 1 }, hr(cols)));
340
- } else if (s.t === 'user') {
341
- sectionEls.push(
342
- h(Box, { key: ki + '-h', height: 1, paddingLeft: 1 },
343
- h(Text, { color: '#888899', bold: true }, 'You')
344
- )
345
- );
346
- sectionEls.push(
347
- h(Box, { key: ki + '-t', height: 1, paddingLeft: 2 },
348
- h(Text, { color: hex.neonBlue, wrap: 'wrap' }, s.text)
349
- )
350
- );
351
- } else if (s.t === 'assistant' || s.t === 'stream') {
352
- sectionEls.push(
353
- h(Box, { key: ki, height: 1, paddingLeft: 1 },
354
- h(Text, { color: hex.primary, wrap: 'wrap' }, s.text)
355
- )
356
- );
357
- } else if (s.t === 'thinking') {
358
- sectionEls.push(
359
- h(Box, { key: ki + '-h', height: 1, paddingLeft: 1 },
360
- h(Text, { color: '#FF9F43', bold: true }, s.spin + ' Thinking \u2022 ' + s.elapsed)
361
- )
362
- );
363
- sectionEls.push(
364
- h(Box, { key: ki + '-c', height: 1, paddingLeft: 2 },
365
- h(Text, { color: '#FF9F43', wrap: 'wrap' }, s.content)
366
- )
367
- );
368
- } else if (s.t === 'tool') {
369
- sectionEls.push(
370
- h(Box, { key: ki, height: 1, paddingLeft: 1 },
371
- h(Text, { color: '#5B5B8A' }, '\u2500\u2500 ' + s.tool + ' \u2014 ' + (s.query || '') + ' (' + (s.sites || 0) + ' sites)')
372
- )
373
- );
374
- } else if (s.t === 'overlay_header') {
375
- sectionEls.push(
376
- h(Box, { key: ki, height: 1, paddingLeft: 1 },
377
- h(Text, { color: '#888899', bold: true }, 'Commands')
378
- )
379
- );
380
- } else if (s.t === 'cmd') {
381
- const prefix = s.sel === true ? '\u276F ' : ' ';
382
- const col = s.sel === true ? hex.neonBlue : '#666688';
383
- sectionEls.push(
384
- h(Box, { key: ki, height: 1, paddingLeft: 2 },
385
- h(Text, { color: col }, prefix + s.trigger),
386
- h(Text, { color: '#444466' }, ' ' + (s.desc || ''))
387
- )
388
- );
389
- } else if (s.t === 'ready') {
390
- sectionEls.push(
391
- h(Box, { key: ki, height: 1, paddingLeft: 1 },
392
- h(Text, { color: '#00FF88', bold: true }, 'Ready')
393
- )
394
- );
395
- } else if (s.t === 'startup') {
396
- for (let li = 0; li < STARTUP_LOGO.length; li++) {
397
- sectionEls.push(
398
- h(Box, { key: ki + '-l' + li, height: 1, paddingLeft: 1 },
399
- h(Text, { color: '#00BBFF', bold: true }, STARTUP_LOGO[li])
400
- )
401
- );
402
- }
403
- sectionEls.push(h(Box, { key: ki + '-e1', height: 1 }, null));
404
- sectionEls.push(
405
- h(Box, { key: ki + '-t1', height: 1, paddingLeft: 1 },
406
- h(Text, { color: '#FFFFFF', bold: true }, 'OpenAxies')
407
- )
408
- );
409
- sectionEls.push(
410
- h(Box, { key: ki + '-t2', height: 1, paddingLeft: 1 },
411
- h(Text, { color: '#777788' }, 'local agent runtime')
412
- )
413
- );
414
- sectionEls.push(h(Box, { key: ki + '-e2', height: 1 }, null));
415
- sectionEls.push(
416
- h(Box, { key: ki + '-t3', height: 1, paddingLeft: 1 },
417
- h(Text, { color: '#555577' }, 'Press Enter to begin')
418
- )
419
- );
420
- }
421
- }
422
-
423
- // Fill remaining
424
- while (sectionEls.length < availLines) {
425
- sectionEls.push(h(Box, { key: 'fl-' + sectionEls.length, height: 1 }, h(Text, {}, '')));
426
- }
427
-
428
- const viewport = h(Box, {
429
- flexGrow: 1,
430
- height: availLines,
431
- flexDirection: 'column',
432
- overflow: 'hidden',
433
- }, ...sectionEls);
434
-
435
- // Main layout
436
- const headerBar = startupDone === true ? createHeaderBar(modelLabel) : null;
437
- const headerSep = startupDone === true ? h(Box, { height: 1 }, hr(cols)) : null;
438
-
439
- // Input line
440
- const inputLine = h(Box, { height: DOCK_H, flexShrink: 0, paddingLeft: 2, paddingRight: 2 },
441
- h(Text, { color: hex.greenOnline }, '\u258C'),
442
- h(Text, { color: '#555577' }, ' Ask OpenAxies'),
443
- h(TextInput, {
444
- value: inputBuffer,
445
- onChange: handleChange,
446
- onSubmit: function () {},
447
- placeholder: '',
448
- focus: showOverlay === false,
449
- showCursor: true,
450
- })
451
- );
452
-
453
- // Shortcuts footer
454
- const modelShort = modelLabel.replace('OpenAxies ', '');
455
- const footerLine = h(Box, { height: STATUS_H, flexShrink: 0, paddingLeft: 2, paddingRight: 2 },
456
- h(Text, { color: '#444466' }, 'esc \u2248 '),
457
- h(Text, { color: '#444466' }, 'ctrl+t '),
458
- h(Text, { color: '#444466' }, 'ctrl+p '),
459
- h(Text, { color: '#444466' }, 'ctrl+k'),
460
- h(Box, { flexGrow: 1 }),
461
- h(Text, { color: '#555577' }, modelShort.toLowerCase() + ' \u2022 local'),
462
- );
463
152
 
464
- return h(Box, { flexDirection: 'column', width: '100%', height: rows, overflow: 'hidden' },
465
- headerBar,
466
- headerSep,
467
- viewport,
468
- inputLine,
469
- footerLine,
153
+ return h(Box, {
154
+ flexGrow: 1,
155
+ flexDirection: 'column',
156
+ overflow: 'hidden',
157
+ paddingLeft: 0,
158
+ paddingRight: 0,
159
+ },
160
+ ...events.slice(-15).map((event, i) => h(ActivityItem, { key: i, event }))
161
+ );
162
+ };
163
+
164
+ return h(Box, {
165
+ flexDirection: 'column',
166
+ width: '100%',
167
+ height: rows,
168
+ overflow: 'hidden'
169
+ },
170
+ !startupDone ? renderStartup() : null,
171
+ startupDone && renderHeader(),
172
+ startupDone && h(Separator),
173
+ startupDone && renderTimeline(),
174
+ startupDone && h(Box, { height: 1 }, h(Separator)),
175
+ startupDone && h(Box, {
176
+ height: 1,
177
+ paddingLeft: 1,
178
+ paddingRight: 1,
179
+ flexDirection: 'row',
180
+ alignItems: 'center'
181
+ },
182
+ h(Typography.Accent, '▌ Ask OpenAxies'),
183
+ h(Box, { flexGrow: 1 }),
184
+ h(TextInput, {
185
+ value: inputBuffer,
186
+ onChange: setInputBuffer,
187
+ onSubmit: handleSubmit,
188
+ focus: true,
189
+ showCursor: true
190
+ })
191
+ ),
192
+ startupDone && renderFooter()
470
193
  );
471
194
  }
@@ -0,0 +1,46 @@
1
+ import React from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import { Typography } from '../ui/Typography.js';
4
+ import { hex } from '../../config/theme.js';
5
+
6
+ const h = React.createElement;
7
+
8
+ export function ActivityItem({ event }) {
9
+ switch (event.type) {
10
+ case 'user':
11
+ return h(Box, { flexDirection: 'column', paddingLeft: 1, paddingRight: 1, marginBottom: 1 },
12
+ h(Typography.Subheader, 'You'),
13
+ h(Box, { paddingLeft: 1 }, h(Typography.Body, { color: hex.neonBlue }, event.content))
14
+ );
15
+ case 'assistant':
16
+ return h(Box, { flexDirection: 'column', paddingLeft: 1, paddingRight: 1, marginBottom: 1 },
17
+ h(Typography.Body, event.content)
18
+ );
19
+ case 'thought':
20
+ return h(Box, {
21
+ flexDirection: 'column',
22
+ paddingLeft: 1,
23
+ paddingRight: 1,
24
+ marginBottom: 1,
25
+ backgroundColor: '#12121A'
26
+ },
27
+ h(Box, { flexDirection: 'row', alignItems: 'center' },
28
+ h(Typography.Thinking, `\u25CB Thinking \u2022 ${event.elapsed || '0.0s'}`)
29
+ ),
30
+ h(Box, { paddingLeft: 1 }, h(Typography.Thinking, event.content))
31
+ );
32
+ case 'tool':
33
+ return h(Box, { paddingLeft: 1, paddingRight: 1, marginBottom: 0.5 },
34
+ h(Typography.Muted, `\u2500\u2500 ${event.tool} ${event.query ? ' \u2014 ' + event.query : ''}`)
35
+ );
36
+ case 'todo':
37
+ const icon = event.status === 'completed' ? '\u2713' : (event.status === 'running' ? '\u25B2' : '\u25CB');
38
+ const color = event.status === 'completed' ? '#00FF88' : (event.status === 'running' ? hex.neonBlue : '#666688');
39
+ return h(Box, { paddingLeft: 1, paddingRight: 1 },
40
+ h(Text, { color }, `${icon} `),
41
+ h(Typography.Muted, event.text)
42
+ );
43
+ default:
44
+ return null;
45
+ }
46
+ }
@@ -0,0 +1,23 @@
1
+ import React from 'react';
2
+ import { Box } from 'ink';
3
+ import { ActivityItem } from './ActivityItem.js';
4
+
5
+ const h = React.createElement;
6
+
7
+ export function TodoTracker({ todos }) {
8
+ if (!todos || todos.length === 0) return null;
9
+
10
+ return h(Box, {
11
+ flexDirection: 'column',
12
+ paddingLeft: 1,
13
+ paddingRight: 1,
14
+ marginBottom: 1,
15
+ borderStyle: 'single',
16
+ borderColor: '#1A1A28'
17
+ },
18
+ ...todos.map((todo, i) => h(ActivityItem, {
19
+ key: i,
20
+ event: { type: 'todo', text: todo.text, status: todo.status }
21
+ }))
22
+ );
23
+ }
@@ -0,0 +1,10 @@
1
+ import React from 'react';
2
+ import { Box, Text } from 'ink';
3
+
4
+ const h = React.createElement;
5
+
6
+ export function Separator() {
7
+ return h(Box, { height: 1, width: '100%' },
8
+ h(Text, { color: '#1A1A28' }, '\u2500'.repeat(80))
9
+ );
10
+ }
@@ -0,0 +1,15 @@
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
+ export const Typography = {
8
+ Header: ({ children }) => h(Text, { color: '#FFFFFF', bold: true }, children),
9
+ Subheader: ({ children }) => h(Text, { color: '#AAAAAA', bold: true }, children),
10
+ Body: ({ children, color = '#FFFFFF' }) => h(Text, { color, wrap: 'wrap' }, children),
11
+ Muted: ({ children }) => h(Text, { color: '#666688' }, children),
12
+ Dim: ({ children }) => h(Text, { color: '#444466' }, children),
13
+ Accent: ({ children }) => h(Text, { color: hex.neonBlue, bold: true }, children),
14
+ Thinking: ({ children }) => h(Text, { color: '#FF9F43' }, children),
15
+ };
@@ -1,17 +0,0 @@
1
- import React from 'react';
2
- import { Box, Text } from 'ink';
3
-
4
- const h = React.createElement;
5
-
6
- export function createHeaderBar(modelLabel) {
7
- const label = typeof modelLabel === 'string' && modelLabel.length > 0 ? modelLabel : 'OpenAxies Llama';
8
- return h(Box, { height: 1, paddingLeft: 1, paddingTop: 0, paddingBottom: 0 },
9
- h(Text, { color: '#00BBFF', bold: true }, 'OpenAxies'),
10
- h(Text, { color: '#555577' }, ' \u2022 '),
11
- h(Text, { color: '#FFFFFF' }, label),
12
- h(Text, { color: '#555577' }, ' \u2022 '),
13
- h(Text, { color: '#777788' }, 'local'),
14
- );
15
- }
16
-
17
- export const HEADER_HEIGHT = 1;
@@ -1,66 +0,0 @@
1
- import React from 'react';
2
- import { Box, Text, useInput, useStdout } from 'ink';
3
- import TextInput from 'ink-text-input';
4
- import { hex } from '../config/theme.js';
5
-
6
- const h = React.createElement;
7
-
8
- export const DOCK_HEIGHT = 4;
9
-
10
- const PROMPT = '> ';
11
-
12
- export function createComposerDock(config) {
13
- if (config === null || config === undefined || typeof config !== 'object') {
14
- throw new Error('createComposerDock requires a config object');
15
- }
16
-
17
- const value = typeof config.value === 'string' ? config.value : '';
18
- const onChange = config.onChange;
19
- const onSubmit = config.onSubmit;
20
- const inputActive = config.inputActive !== false;
21
- const isStreaming = config.isStreaming === true;
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;
25
-
26
- const placeholder = isStreaming ? 'Ctrl+C to interrupt...' : 'type a message...';
27
- const innerW = safeCols - 2;
28
-
29
- return h(Box, {
30
- width: '100%',
31
- height: DOCK_HEIGHT,
32
- flexDirection: 'column',
33
- flexShrink: 0,
34
- },
35
- h(Box, { height: 1, paddingLeft: 1 },
36
- h(Text, { color: '#1A1A28' }, '\u2500'.repeat(innerW))
37
- ),
38
- h(Box, { height: 1, paddingLeft: 1, paddingRight: 1 },
39
- h(Text, { color: hex.border }, '\u2502'),
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
- })
50
- ),
51
- h(Text, { color: '#444466' }, ' '),
52
- h(Text, { color: hex.border }, '\u2502')
53
- ),
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 || '')
64
- )
65
- );
66
- }