openaxies 0.4.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 +1 -3
- package/src/App.js +271 -263
- package/src/components/ComposerDock.js +23 -74
- package/src/config/models.js +11 -5
- package/src/providers/index.js +16 -12
- package/src/providers/streaming.js +92 -96
- package/src/components/BrandHeader.js +0 -60
- package/src/components/ChatViewport.js +0 -193
- package/src/components/RouterBar.js +0 -98
package/src/App.js
CHANGED
|
@@ -1,30 +1,20 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { Box, useInput, useStdout } from 'ink';
|
|
2
|
+
import { Box, Text, useInput, useStdout } from 'ink';
|
|
3
3
|
import { hex } from './config/theme.js';
|
|
4
4
|
import { getCommands } from './config/commands.js';
|
|
5
|
-
import { getModelById, DEFAULT_MODEL_ID } from './config/models.js';
|
|
5
|
+
import { getModels, getModelById, DEFAULT_MODEL_ID } from './config/models.js';
|
|
6
6
|
import { callModel } from './providers/index.js';
|
|
7
|
-
import { createBrandHeader, BRAND_HEIGHT } from './components/BrandHeader.js';
|
|
8
|
-
import { createRouterBar, ROUTER_HEIGHT } from './components/RouterBar.js';
|
|
9
|
-
import { createChatViewport } from './components/ChatViewport.js';
|
|
10
|
-
import { createSlashOverlay, SLASH_OVERLAY_HEIGHT } from './components/SlashOverlay.js';
|
|
11
7
|
import { createComposerDock, DOCK_HEIGHT } from './components/ComposerDock.js';
|
|
12
8
|
|
|
13
9
|
const h = React.createElement;
|
|
14
10
|
|
|
15
11
|
export default AppRoot;
|
|
16
12
|
|
|
17
|
-
// ---------------------------------------------------------------------------
|
|
18
|
-
// Helpers
|
|
19
|
-
// ---------------------------------------------------------------------------
|
|
20
|
-
|
|
21
13
|
function buildHistory(messages, newText) {
|
|
22
14
|
const history = [];
|
|
23
15
|
for (let i = 0; i < messages.length; i++) {
|
|
24
16
|
const m = messages[i];
|
|
25
|
-
if (m === null || m === undefined || typeof m !== 'object')
|
|
26
|
-
continue;
|
|
27
|
-
}
|
|
17
|
+
if (m === null || m === undefined || typeof m !== 'object') continue;
|
|
28
18
|
if (m.role === 'user' || m.role === 'assistant') {
|
|
29
19
|
const content = typeof m.content === 'string' ? m.content : '';
|
|
30
20
|
history.push({ role: m.role, content: content });
|
|
@@ -36,49 +26,47 @@ function buildHistory(messages, newText) {
|
|
|
36
26
|
return history;
|
|
37
27
|
}
|
|
38
28
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
29
|
+
function formatTimer(seconds) {
|
|
30
|
+
if (typeof seconds !== 'number' || seconds < 0) return '0.0s';
|
|
31
|
+
return seconds.toFixed(1) + 's';
|
|
32
|
+
}
|
|
33
|
+
|
|
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);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const SPINNER = ['\u25CB', '\u25D4', '\u25D0', '\u25D5', '\u25CF', '\u25D5', '\u25D0', '\u25D4'];
|
|
42
42
|
|
|
43
43
|
function AppRoot() {
|
|
44
44
|
const { stdout } = useStdout();
|
|
45
45
|
const rows = stdout.rows || 24;
|
|
46
46
|
const cols = stdout.columns || 80;
|
|
47
47
|
|
|
48
|
-
// --- UI state ---
|
|
49
|
-
const [inputBuffer, setInputBuffer] = React.useState('');
|
|
50
|
-
const [showOverlay, setShowOverlay] = React.useState(false);
|
|
51
|
-
const [overlayIndex, setOverlayIndex] = React.useState(0); // cursor inside overlay
|
|
52
|
-
|
|
53
|
-
// --- Chat state ---
|
|
54
48
|
const [messages, setMessages] = React.useState([]);
|
|
55
49
|
const [activeModel, setActiveModel] = React.useState(DEFAULT_MODEL_ID);
|
|
56
50
|
const [streamText, setStreamText] = React.useState('');
|
|
57
51
|
const [streamingActive, setStreamingActive] = React.useState(false);
|
|
58
|
-
const [scrollOffset, setScrollOffset] = React.useState(0);
|
|
59
52
|
const [isThinking, setIsThinking] = React.useState(false);
|
|
60
|
-
const [
|
|
53
|
+
const [spinnerIdx, setSpinnerIdx] = React.useState(0);
|
|
54
|
+
const [thinkingElapsed, setThinkingElapsed] = React.useState(0);
|
|
55
|
+
const [inputBuffer, setInputBuffer] = React.useState('');
|
|
56
|
+
const [showOverlay, setShowOverlay] = React.useState(false);
|
|
57
|
+
const [overlayIndex, setOverlayIndex] = React.useState(0);
|
|
58
|
+
const [toolInfo, setToolInfo] = React.useState(null);
|
|
61
59
|
|
|
62
60
|
const commands = getCommands();
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
// Strict row budget
|
|
66
|
-
// BrandHeader : BRAND_HEIGHT (7)
|
|
67
|
-
// RouterBar : ROUTER_HEIGHT (2)
|
|
68
|
-
// SlashOverlay : SLASH_OVERLAY_HEIGHT (6) — conditional
|
|
69
|
-
// ComposerDock : DOCK_HEIGHT (3)
|
|
70
|
-
// ChatViewport : rows - all fixed rows (flexGrow fills remainder)
|
|
71
|
-
// ---------------------------------------------------------------------------
|
|
72
|
-
const overlayH = showOverlay ? SLASH_OVERLAY_HEIGHT : 0;
|
|
73
|
-
const fixedH = BRAND_HEIGHT + ROUTER_HEIGHT + overlayH + DOCK_HEIGHT;
|
|
61
|
+
const overlayH = showOverlay ? 6 : 0;
|
|
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);
|
|
77
|
-
const
|
|
66
|
+
const connRef = React.useRef(null);
|
|
78
67
|
const spinnerRef = React.useRef(null);
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
const SPINNER_FRAMES = ['◐', '◓', '◑', '◒'];
|
|
68
|
+
const timerRef = React.useRef(null);
|
|
69
|
+
const thinkStartRef = React.useRef(null);
|
|
82
70
|
|
|
83
71
|
React.useEffect(function () {
|
|
84
72
|
if (isThinking === false) {
|
|
@@ -86,255 +74,217 @@ function AppRoot() {
|
|
|
86
74
|
clearInterval(spinnerRef.current);
|
|
87
75
|
spinnerRef.current = null;
|
|
88
76
|
}
|
|
89
|
-
|
|
77
|
+
if (timerRef.current !== null) {
|
|
78
|
+
clearInterval(timerRef.current);
|
|
79
|
+
timerRef.current = null;
|
|
80
|
+
}
|
|
90
81
|
return;
|
|
91
82
|
}
|
|
92
83
|
let idx = 0;
|
|
93
|
-
setSpinnerChar(SPINNER_FRAMES[0]);
|
|
94
84
|
spinnerRef.current = setInterval(function () {
|
|
95
|
-
idx = (idx + 1) %
|
|
96
|
-
|
|
97
|
-
},
|
|
98
|
-
|
|
85
|
+
idx = (idx + 1) % SPINNER.length;
|
|
86
|
+
setSpinnerIdx(idx);
|
|
87
|
+
}, 100);
|
|
88
|
+
timerRef.current = setInterval(function () {
|
|
89
|
+
if (thinkStartRef.current !== null) {
|
|
90
|
+
setThinkingElapsed((Date.now() - thinkStartRef.current) / 1000);
|
|
91
|
+
}
|
|
92
|
+
}, 100);
|
|
93
|
+
return function () {
|
|
99
94
|
if (spinnerRef.current !== null) {
|
|
100
95
|
clearInterval(spinnerRef.current);
|
|
101
96
|
spinnerRef.current = null;
|
|
102
97
|
}
|
|
98
|
+
if (timerRef.current !== null) {
|
|
99
|
+
clearInterval(timerRef.current);
|
|
100
|
+
timerRef.current = null;
|
|
101
|
+
}
|
|
103
102
|
};
|
|
104
103
|
}, [isThinking]);
|
|
105
104
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
setOverlayIndex(0);
|
|
117
|
-
}
|
|
118
|
-
// Close overlay if buffer no longer starts with '/'
|
|
119
|
-
if (showOverlay === true && (safeValue.length === 0 || safeValue[0] !== '/')) {
|
|
120
|
-
setShowOverlay(false);
|
|
121
|
-
}
|
|
105
|
+
function cycleModel() {
|
|
106
|
+
const models = getModels();
|
|
107
|
+
setActiveModel(function (prev) {
|
|
108
|
+
let found = false;
|
|
109
|
+
for (let i = 0; i < models.length; i++) {
|
|
110
|
+
if (found) return models[i].id;
|
|
111
|
+
if (models[i].id === prev) found = true;
|
|
112
|
+
}
|
|
113
|
+
return models[0].id;
|
|
114
|
+
});
|
|
122
115
|
}
|
|
123
116
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
function handleSlashSelect(item) {
|
|
130
|
-
if (item === null || item === undefined) {
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
const trigger = item.value;
|
|
134
|
-
if (typeof trigger !== 'string') {
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
if (trigger === '/exit') {
|
|
139
|
-
process.exit(0);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
if (trigger === '/clear') {
|
|
143
|
-
setMessages([]);
|
|
144
|
-
setScrollOffset(0);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
if (trigger === '/model') {
|
|
148
|
-
// Toggle between the two local models
|
|
149
|
-
setActiveModel(function (prev) {
|
|
150
|
-
if (prev === 'openaxis/openaxis-flash') {
|
|
151
|
-
return 'openaxis/openaxis-heavy';
|
|
152
|
-
}
|
|
153
|
-
return 'openaxis/openaxis-flash';
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if (trigger === '/help') {
|
|
158
|
-
setScrollOffset(0);
|
|
159
|
-
setMessages(function (prev) {
|
|
160
|
-
return prev.concat([{
|
|
161
|
-
role: 'assistant',
|
|
162
|
-
content:
|
|
163
|
-
'**Available commands:**\n' +
|
|
164
|
-
'- `/help` — Show this help\n' +
|
|
165
|
-
'- `/clear` — Clear the conversation\n' +
|
|
166
|
-
'- `/model` — Toggle between Flash and Heavy\n' +
|
|
167
|
-
'- `/exit` — Quit the application\n\n' +
|
|
168
|
-
'Type your message and press **Enter** to chat.',
|
|
169
|
-
id: 'help-' + Date.now(),
|
|
170
|
-
}]);
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// Always close & clear after any selection
|
|
175
|
-
setInputBuffer('');
|
|
176
|
-
setShowOverlay(false);
|
|
177
|
-
setOverlayIndex(0);
|
|
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;
|
|
178
122
|
}
|
|
179
123
|
|
|
180
124
|
async function handleSubmit(text) {
|
|
181
|
-
const
|
|
182
|
-
if (
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const msgId = 'user-' + Date.now() + '-' + Math.random().toString(36).slice(2, 8);
|
|
125
|
+
const safe = typeof text === 'string' ? text : '';
|
|
126
|
+
if (safe.length === 0 || streamingActive === true) return;
|
|
187
127
|
|
|
188
128
|
setStreamingActive(true);
|
|
189
|
-
|
|
190
|
-
setMessages(function (prev) {
|
|
191
|
-
return prev.concat([{ role: 'user', content: safeText, id: msgId }]);
|
|
192
|
-
});
|
|
193
|
-
setInputBuffer('');
|
|
129
|
+
setToolInfo(null);
|
|
194
130
|
setStreamText(' connecting');
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
if (
|
|
200
|
-
if (
|
|
201
|
-
return
|
|
131
|
+
setThinkingElapsed(0);
|
|
132
|
+
|
|
133
|
+
connRef.current = setInterval(function () {
|
|
134
|
+
setStreamText(function (p) {
|
|
135
|
+
if (p === ' connecting') return ' connecting.';
|
|
136
|
+
if (p === ' connecting.') return ' connecting..';
|
|
137
|
+
if (p === ' connecting..') return ' connecting...';
|
|
138
|
+
return ' connecting';
|
|
202
139
|
});
|
|
203
140
|
}, 400);
|
|
204
141
|
|
|
142
|
+
setMessages(function (p) {
|
|
143
|
+
return p.concat([{ role: 'user', content: safe, id: 'u-' + Date.now() }]);
|
|
144
|
+
});
|
|
145
|
+
setInputBuffer('');
|
|
146
|
+
|
|
205
147
|
const modelInfo = getModelById(activeModel);
|
|
206
|
-
if (modelInfo === null
|
|
207
|
-
if (
|
|
208
|
-
clearInterval(
|
|
209
|
-
|
|
148
|
+
if (modelInfo === null) {
|
|
149
|
+
if (connRef.current !== null) {
|
|
150
|
+
clearInterval(connRef.current);
|
|
151
|
+
connRef.current = null;
|
|
210
152
|
}
|
|
211
153
|
setStreamingActive(false);
|
|
212
154
|
setStreamText('');
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
return prev.concat([{
|
|
216
|
-
role: 'assistant',
|
|
217
|
-
content: 'No model selected. Use `/model` to switch.',
|
|
218
|
-
id: 'err-' + Date.now(),
|
|
219
|
-
}]);
|
|
155
|
+
setMessages(function (p) {
|
|
156
|
+
return p.concat([{ role: 'assistant', content: 'No model selected. Use /model.', id: 'e-' + Date.now() }]);
|
|
220
157
|
});
|
|
221
158
|
return;
|
|
222
159
|
}
|
|
223
160
|
|
|
224
|
-
const history = buildHistory(messages,
|
|
161
|
+
const history = buildHistory(messages, safe);
|
|
225
162
|
const abortController = new AbortController();
|
|
226
163
|
abortRef.current = abortController;
|
|
227
164
|
|
|
228
165
|
try {
|
|
229
|
-
function checkThinkState(text) {
|
|
230
|
-
const openIdx = text.lastIndexOf('<think>');
|
|
231
|
-
const closeIdx = text.lastIndexOf('</think>');
|
|
232
|
-
if (openIdx === -1 && closeIdx === -1) {
|
|
233
|
-
return false;
|
|
234
|
-
}
|
|
235
|
-
if (openIdx > closeIdx) {
|
|
236
|
-
return true;
|
|
237
|
-
}
|
|
238
|
-
return false;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
166
|
const gen = callModel(modelInfo, history, abortController.signal);
|
|
242
167
|
let accumulated = '';
|
|
243
168
|
let firstEvent = true;
|
|
169
|
+
let thinkStarted = false;
|
|
244
170
|
|
|
245
171
|
for await (const event of gen) {
|
|
246
|
-
if (abortController.signal.aborted === true)
|
|
247
|
-
|
|
248
|
-
}
|
|
172
|
+
if (abortController.signal.aborted === true) break;
|
|
173
|
+
|
|
249
174
|
if (firstEvent === true) {
|
|
250
175
|
firstEvent = false;
|
|
251
|
-
if (
|
|
252
|
-
clearInterval(
|
|
253
|
-
|
|
176
|
+
if (connRef.current !== null) {
|
|
177
|
+
clearInterval(connRef.current);
|
|
178
|
+
connRef.current = null;
|
|
254
179
|
}
|
|
255
180
|
}
|
|
181
|
+
|
|
182
|
+
if (event.type === 'tool') {
|
|
183
|
+
setToolInfo({ tool: event.tool, used: event.used, sites: event.sites, query: event.query });
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
|
|
256
187
|
if (event.type === 'thinking' || event.type === 'token') {
|
|
257
188
|
const content = typeof event.content === 'string' ? event.content : '';
|
|
258
189
|
for (let i = 0; i < content.length; i++) {
|
|
259
190
|
accumulated = accumulated + content[i];
|
|
260
|
-
const
|
|
261
|
-
setScrollOffset(0);
|
|
191
|
+
const inside = checkThink(accumulated);
|
|
262
192
|
setStreamText(accumulated);
|
|
263
|
-
setIsThinking(
|
|
264
|
-
if (
|
|
265
|
-
|
|
193
|
+
setIsThinking(inside);
|
|
194
|
+
if (inside === true && thinkStarted === false) {
|
|
195
|
+
thinkStarted = true;
|
|
196
|
+
thinkStartRef.current = Date.now();
|
|
197
|
+
setThinkingElapsed(0);
|
|
198
|
+
}
|
|
199
|
+
if (inside === false && thinkStarted === true) {
|
|
200
|
+
thinkStarted = false;
|
|
201
|
+
thinkStartRef.current = null;
|
|
202
|
+
setThinkingElapsed(0);
|
|
266
203
|
}
|
|
267
|
-
await new Promise(function (
|
|
268
|
-
setTimeout(resolve, 0);
|
|
269
|
-
});
|
|
204
|
+
await new Promise(function (r) { setTimeout(r, 0); });
|
|
270
205
|
}
|
|
271
206
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
}
|
|
207
|
+
|
|
208
|
+
if (event.type === 'done') break;
|
|
275
209
|
if (event.type === 'timeout') {
|
|
276
210
|
if (accumulated.length > 0) {
|
|
277
|
-
accumulated = accumulated + '\n\n
|
|
211
|
+
accumulated = accumulated + '\n\n\u2014 stream timed out \u2014';
|
|
278
212
|
}
|
|
279
213
|
setStreamText(accumulated);
|
|
280
214
|
break;
|
|
281
215
|
}
|
|
282
216
|
}
|
|
283
217
|
|
|
284
|
-
if (
|
|
285
|
-
clearInterval(
|
|
286
|
-
|
|
218
|
+
if (connRef.current !== null) {
|
|
219
|
+
clearInterval(connRef.current);
|
|
220
|
+
connRef.current = null;
|
|
287
221
|
}
|
|
288
222
|
setIsThinking(false);
|
|
289
|
-
|
|
223
|
+
thinkStartRef.current = null;
|
|
224
|
+
setThinkingElapsed(0);
|
|
290
225
|
setStreamingActive(false);
|
|
226
|
+
|
|
227
|
+
const finalText = streamText;
|
|
291
228
|
setStreamText('');
|
|
292
229
|
|
|
293
|
-
if (
|
|
294
|
-
setMessages(function (
|
|
295
|
-
return
|
|
296
|
-
role: 'assistant',
|
|
297
|
-
content: accumulated,
|
|
298
|
-
id: 'resp-' + Date.now() + '-' + Math.random().toString(36).slice(2, 8),
|
|
299
|
-
}]);
|
|
230
|
+
if (finalText.length > 0) {
|
|
231
|
+
setMessages(function (p) {
|
|
232
|
+
return p.concat([{ role: 'assistant', content: finalText, id: 'a-' + Date.now() }]);
|
|
300
233
|
});
|
|
301
234
|
}
|
|
302
235
|
} catch (err) {
|
|
303
|
-
if (
|
|
304
|
-
clearInterval(
|
|
305
|
-
|
|
236
|
+
if (connRef.current !== null) {
|
|
237
|
+
clearInterval(connRef.current);
|
|
238
|
+
connRef.current = null;
|
|
306
239
|
}
|
|
307
240
|
setIsThinking(false);
|
|
308
|
-
|
|
241
|
+
thinkStartRef.current = null;
|
|
242
|
+
setThinkingElapsed(0);
|
|
309
243
|
setStreamingActive(false);
|
|
310
244
|
setStreamText('');
|
|
311
|
-
|
|
312
|
-
const errMsg = (err !== null && err !== undefined)
|
|
245
|
+
const errMsg = err !== null && err !== undefined
|
|
313
246
|
? (err.message || String(err))
|
|
314
247
|
: 'Unknown error';
|
|
315
|
-
setMessages(function (
|
|
316
|
-
return
|
|
248
|
+
setMessages(function (p) {
|
|
249
|
+
return p.concat([{
|
|
317
250
|
role: 'assistant',
|
|
318
|
-
content:
|
|
319
|
-
|
|
320
|
-
'\n\nThe Space may be cold-starting (model download can take 30\u2013120 s). Please try again.',
|
|
321
|
-
id: 'err-' + Date.now() + '-' + Math.random().toString(36).slice(2, 8),
|
|
251
|
+
content: 'Error: ' + errMsg + '\n\nSpace cold-starting? Try again.',
|
|
252
|
+
id: 'er-' + Date.now(),
|
|
322
253
|
}]);
|
|
323
254
|
});
|
|
324
255
|
}
|
|
325
256
|
}
|
|
326
257
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
258
|
+
function handleChange(v) {
|
|
259
|
+
const s = typeof v === 'string' ? v : '';
|
|
260
|
+
setInputBuffer(s);
|
|
261
|
+
if (s.length === 1 && s[0] === '/' && showOverlay === false) {
|
|
262
|
+
setShowOverlay(true);
|
|
263
|
+
setOverlayIndex(0);
|
|
264
|
+
}
|
|
265
|
+
if (showOverlay === true && (s.length === 0 || s[0] !== '/')) {
|
|
266
|
+
setShowOverlay(false);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
336
269
|
|
|
337
|
-
|
|
270
|
+
function handleCmd(item) {
|
|
271
|
+
if (item === null || item === undefined) return;
|
|
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() }]);
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
setInputBuffer('');
|
|
283
|
+
setShowOverlay(false);
|
|
284
|
+
setOverlayIndex(0);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
useInput(function handleInput(input, key) {
|
|
338
288
|
if (showOverlay === true) {
|
|
339
289
|
if (key.escape === true) {
|
|
340
290
|
setShowOverlay(false);
|
|
@@ -342,63 +292,31 @@ function AppRoot() {
|
|
|
342
292
|
setOverlayIndex(0);
|
|
343
293
|
return;
|
|
344
294
|
}
|
|
345
|
-
|
|
346
295
|
if (key.upArrow === true) {
|
|
347
|
-
setOverlayIndex(function (
|
|
348
|
-
return prev > 0 ? prev - 1 : commands.length - 1; // wrap around top
|
|
349
|
-
});
|
|
296
|
+
setOverlayIndex(function (p) { return p > 0 ? p - 1 : commands.length - 1; });
|
|
350
297
|
return;
|
|
351
298
|
}
|
|
352
|
-
|
|
353
299
|
if (key.downArrow === true) {
|
|
354
|
-
setOverlayIndex(function (
|
|
355
|
-
return prev < commands.length - 1 ? prev + 1 : 0; // wrap around bottom
|
|
356
|
-
});
|
|
300
|
+
setOverlayIndex(function (p) { return p < commands.length - 1 ? p + 1 : 0; });
|
|
357
301
|
return;
|
|
358
302
|
}
|
|
359
|
-
|
|
360
303
|
if (key.return === true) {
|
|
361
|
-
const
|
|
362
|
-
if (
|
|
363
|
-
handleSlashSelect({ value: selectedCmd.trigger });
|
|
364
|
-
}
|
|
304
|
+
const cmd = commands[overlayIndex];
|
|
305
|
+
if (cmd !== null && cmd !== undefined) handleCmd({ value: cmd.trigger });
|
|
365
306
|
return;
|
|
366
307
|
}
|
|
367
|
-
|
|
368
|
-
// Absorb everything else while overlay is open
|
|
369
308
|
return;
|
|
370
309
|
}
|
|
371
310
|
|
|
372
|
-
// ── Normal branch ────────────────────────────────────────────────────────
|
|
373
311
|
if (key.return === true) {
|
|
374
|
-
const
|
|
375
|
-
if (
|
|
376
|
-
handleSubmit(safeValue);
|
|
377
|
-
}
|
|
378
|
-
return;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
if (key.pageUp === true || (key.ctrl === true && key.upArrow === true)) {
|
|
382
|
-
const pageSize = Math.max(1, availLines - 2);
|
|
383
|
-
setScrollOffset(function (prev) {
|
|
384
|
-
return prev + pageSize;
|
|
385
|
-
});
|
|
386
|
-
return;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
if (key.pageDown === true || (key.ctrl === true && key.downArrow === true)) {
|
|
390
|
-
const pageSize = Math.max(1, availLines - 2);
|
|
391
|
-
setScrollOffset(function (prev) {
|
|
392
|
-
return Math.max(0, prev - pageSize);
|
|
393
|
-
});
|
|
312
|
+
const s = typeof inputBuffer === 'string' ? inputBuffer : '';
|
|
313
|
+
if (s.length > 0 && streamingActive === false) handleSubmit(s);
|
|
394
314
|
return;
|
|
395
315
|
}
|
|
396
316
|
|
|
397
317
|
if (key.ctrl === true && (input === 'c' || input === 'C')) {
|
|
398
318
|
if (abortRef.current !== null) {
|
|
399
|
-
try {
|
|
400
|
-
abortRef.current.abort();
|
|
401
|
-
} catch (_) { }
|
|
319
|
+
try { abortRef.current.abort(); } catch (_) {}
|
|
402
320
|
abortRef.current = null;
|
|
403
321
|
setStreamingActive(false);
|
|
404
322
|
setStreamText('');
|
|
@@ -407,27 +325,116 @@ function AppRoot() {
|
|
|
407
325
|
}
|
|
408
326
|
});
|
|
409
327
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
328
|
+
const activeInfo = getModelById(activeModel);
|
|
329
|
+
const activeLabel = activeInfo !== null ? activeInfo.label : 'OpenAxies';
|
|
330
|
+
const activeBadge = activeInfo !== null ? activeInfo.badge : '';
|
|
331
|
+
|
|
332
|
+
const viewLines = [];
|
|
333
|
+
for (let i = 0; i < messages.length; i++) {
|
|
334
|
+
const msg = messages[i];
|
|
335
|
+
if (msg === null || typeof msg !== 'object') continue;
|
|
336
|
+
const content = typeof msg.content === 'string' ? msg.content : '';
|
|
337
|
+
if (content.length === 0) continue;
|
|
338
|
+
|
|
339
|
+
if (msg.role === 'user') {
|
|
340
|
+
if (i > 0) viewLines.push({ t: 'sep' });
|
|
341
|
+
viewLines.push({ t: 'user', text: content });
|
|
342
|
+
viewLines.push({ t: 'sep' });
|
|
343
|
+
} else {
|
|
344
|
+
viewLines.push({ t: 'text', text: content });
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const hasStream = typeof streamText === 'string' && streamText.length > 0;
|
|
349
|
+
if (hasStream === true) {
|
|
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)' });
|
|
353
|
+
}
|
|
354
|
+
if (isThinking === true) {
|
|
355
|
+
viewLines.push({ t: 'think', spin: SPINNER[spinnerIdx], elapsed: formatTimer(thinkingElapsed) });
|
|
356
|
+
const clean = streamText.split('<think>').join('').split('</think>').join('');
|
|
357
|
+
viewLines.push({ t: 'think-text', text: clean });
|
|
358
|
+
} else {
|
|
359
|
+
const clean = streamText.split('<think>').join('').split('</think>').join('');
|
|
360
|
+
viewLines.push({ t: 'text', text: clean });
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (viewLines.length === 0) {
|
|
365
|
+
viewLines.push({ t: 'idle', text: ' start typing to chat...' });
|
|
366
|
+
}
|
|
367
|
+
|
|
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)
|
|
380
|
+
)
|
|
381
|
+
);
|
|
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)
|
|
386
|
+
)
|
|
387
|
+
);
|
|
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)
|
|
392
|
+
)
|
|
393
|
+
);
|
|
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)
|
|
398
|
+
)
|
|
399
|
+
);
|
|
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)
|
|
404
|
+
)
|
|
405
|
+
);
|
|
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)
|
|
410
|
+
)
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const viewport = h(Box, {
|
|
416
|
+
flexGrow: 1,
|
|
417
|
+
height: availLines,
|
|
418
|
+
flexDirection: 'column',
|
|
419
|
+
overflow: 'hidden',
|
|
420
|
+
paddingBottom: 1,
|
|
421
|
+
}, ...viewEls);
|
|
413
422
|
|
|
414
|
-
const header = createBrandHeader(cols);
|
|
415
|
-
const routerBar = createRouterBar(activeModel, cols);
|
|
416
|
-
const viewport = createChatViewport(messages, streamText, availLines, cols, scrollOffset, isThinking, spinnerChar);
|
|
417
423
|
const slashOverlay = showOverlay === true
|
|
418
424
|
? createSlashOverlay(commands, overlayIndex)
|
|
419
425
|
: null;
|
|
426
|
+
|
|
420
427
|
const dock = createComposerDock({
|
|
421
428
|
value: inputBuffer,
|
|
422
|
-
onChange:
|
|
423
|
-
onSubmit:
|
|
424
|
-
inputActive: showOverlay === false,
|
|
429
|
+
onChange: handleChange,
|
|
430
|
+
onSubmit: function () {},
|
|
431
|
+
inputActive: showOverlay === false,
|
|
425
432
|
isStreaming: streamingActive,
|
|
426
433
|
terminalWidth: cols,
|
|
434
|
+
modelLabel: activeLabel + ' \u25CF',
|
|
435
|
+
modelStatus: true,
|
|
427
436
|
});
|
|
428
437
|
|
|
429
|
-
// Strict column layout — total height === rows (no scroll, no overflow).
|
|
430
|
-
// ComposerDock has flexShrink:0 so it is always pinned at bottom.
|
|
431
438
|
return h(Box, {
|
|
432
439
|
flexDirection: 'column',
|
|
433
440
|
width: '100%',
|
|
@@ -435,10 +442,11 @@ function AppRoot() {
|
|
|
435
442
|
backgroundColor: hex.black,
|
|
436
443
|
overflow: 'hidden',
|
|
437
444
|
},
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
dock // 3 rows — flexShrink:0, always at bottom
|
|
445
|
+
h(Box, { height: 1 }, hr(cols)),
|
|
446
|
+
viewport,
|
|
447
|
+
slashOverlay,
|
|
448
|
+
dock
|
|
443
449
|
);
|
|
444
450
|
}
|
|
451
|
+
|
|
452
|
+
import { createSlashOverlay } from './components/SlashOverlay.js';
|