openaxies 1.0.2 → 1.0.3

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