openaxies 0.3.0 → 0.5.0
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 +364 -212
- package/src/components/ModelBar.js +42 -0
- package/src/components/Particles.js +47 -0
- package/src/config/commands.js +1 -1
- package/src/config/models.js +11 -6
- 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 -133
- package/src/components/RouterBar.js +0 -98
package/src/App.js
CHANGED
|
@@ -1,23 +1,18 @@
|
|
|
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 {
|
|
8
|
-
import {
|
|
9
|
-
import { createChatViewport } from './components/ChatViewport.js';
|
|
10
|
-
import { createSlashOverlay, SLASH_OVERLAY_HEIGHT } from './components/SlashOverlay.js';
|
|
7
|
+
import { createModelBar } from './components/ModelBar.js';
|
|
8
|
+
import { createParticleRow } from './components/Particles.js';
|
|
11
9
|
import { createComposerDock, DOCK_HEIGHT } from './components/ComposerDock.js';
|
|
12
10
|
|
|
13
11
|
const h = React.createElement;
|
|
12
|
+
const BAR_COLOR = '#1A1A2E';
|
|
14
13
|
|
|
15
14
|
export default AppRoot;
|
|
16
15
|
|
|
17
|
-
// ---------------------------------------------------------------------------
|
|
18
|
-
// Helpers
|
|
19
|
-
// ---------------------------------------------------------------------------
|
|
20
|
-
|
|
21
16
|
function buildHistory(messages, newText) {
|
|
22
17
|
const history = [];
|
|
23
18
|
for (let i = 0; i < messages.length; i++) {
|
|
@@ -36,157 +31,169 @@ function buildHistory(messages, newText) {
|
|
|
36
31
|
return history;
|
|
37
32
|
}
|
|
38
33
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
34
|
+
function formatTimer(seconds) {
|
|
35
|
+
if (typeof seconds !== 'number' || seconds < 0) {
|
|
36
|
+
return '0.0s';
|
|
37
|
+
}
|
|
38
|
+
return seconds.toFixed(1) + 's';
|
|
39
|
+
}
|
|
40
|
+
|
|
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);
|
|
48
|
+
}
|
|
42
49
|
|
|
43
50
|
function AppRoot() {
|
|
44
51
|
const { stdout } = useStdout();
|
|
45
52
|
const rows = stdout.rows || 24;
|
|
46
53
|
const cols = stdout.columns || 80;
|
|
47
54
|
|
|
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
55
|
const [messages, setMessages] = React.useState([]);
|
|
55
56
|
const [activeModel, setActiveModel] = React.useState(DEFAULT_MODEL_ID);
|
|
56
57
|
const [streamText, setStreamText] = React.useState('');
|
|
57
58
|
const [streamingActive, setStreamingActive] = React.useState(false);
|
|
58
|
-
const [
|
|
59
|
+
const [isThinking, setIsThinking] = React.useState(false);
|
|
60
|
+
const [spinnerChar, setSpinnerChar] = React.useState('');
|
|
61
|
+
const [thinkingElapsed, setThinkingElapsed] = React.useState(0);
|
|
62
|
+
const [inputBuffer, setInputBuffer] = React.useState('');
|
|
63
|
+
const [showOverlay, setShowOverlay] = React.useState(false);
|
|
64
|
+
const [overlayIndex, setOverlayIndex] = React.useState(0);
|
|
65
|
+
const [particleFrame, setParticleFrame] = React.useState(0);
|
|
66
|
+
const [toolInfo, setToolInfo] = React.useState(null);
|
|
59
67
|
|
|
60
68
|
const commands = getCommands();
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
// SlashOverlay : SLASH_OVERLAY_HEIGHT (6) — conditional
|
|
67
|
-
// ComposerDock : DOCK_HEIGHT (3)
|
|
68
|
-
// ChatViewport : rows - all fixed rows (flexGrow fills remainder)
|
|
69
|
-
// ---------------------------------------------------------------------------
|
|
70
|
-
const overlayH = showOverlay ? SLASH_OVERLAY_HEIGHT : 0;
|
|
71
|
-
const fixedH = BRAND_HEIGHT + ROUTER_HEIGHT + overlayH + DOCK_HEIGHT;
|
|
69
|
+
const MODEL_BAR_H = 1;
|
|
70
|
+
const SEP_H = 1;
|
|
71
|
+
const DOCK = DOCK_HEIGHT;
|
|
72
|
+
const overlayH = showOverlay ? 6 : 0;
|
|
73
|
+
const fixedH = MODEL_BAR_H + SEP_H + overlayH + DOCK;
|
|
72
74
|
const availLines = Math.max(1, rows - fixedH);
|
|
73
75
|
|
|
74
76
|
const abortRef = React.useRef(null);
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
77
|
+
const connRef = React.useRef(null);
|
|
78
|
+
const spinnerRef = React.useRef(null);
|
|
79
|
+
const timerRef = React.useRef(null);
|
|
80
|
+
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
|
+
|
|
103
|
+
React.useEffect(function () {
|
|
104
|
+
if (isThinking === false) {
|
|
105
|
+
if (spinnerRef.current !== null) {
|
|
106
|
+
clearInterval(spinnerRef.current);
|
|
107
|
+
spinnerRef.current = null;
|
|
108
|
+
}
|
|
109
|
+
if (timerRef.current !== null) {
|
|
110
|
+
clearInterval(timerRef.current);
|
|
111
|
+
timerRef.current = null;
|
|
112
|
+
}
|
|
113
|
+
setSpinnerChar('');
|
|
106
114
|
return;
|
|
107
115
|
}
|
|
116
|
+
let idx = 0;
|
|
117
|
+
setSpinnerChar(SPINNER_FRAMES[0]);
|
|
118
|
+
spinnerRef.current = setInterval(function () {
|
|
119
|
+
idx = (idx + 1) % SPINNER_FRAMES.length;
|
|
120
|
+
setSpinnerChar(SPINNER_FRAMES[idx]);
|
|
121
|
+
}, 100);
|
|
122
|
+
timerRef.current = setInterval(function () {
|
|
123
|
+
if (thinkStartRef.current !== null) {
|
|
124
|
+
setThinkingElapsed((Date.now() - thinkStartRef.current) / 1000);
|
|
125
|
+
}
|
|
126
|
+
}, 100);
|
|
127
|
+
return function () {
|
|
128
|
+
if (spinnerRef.current !== null) {
|
|
129
|
+
clearInterval(spinnerRef.current);
|
|
130
|
+
spinnerRef.current = null;
|
|
131
|
+
}
|
|
132
|
+
if (timerRef.current !== null) {
|
|
133
|
+
clearInterval(timerRef.current);
|
|
134
|
+
timerRef.current = null;
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
}, [isThinking]);
|
|
138
|
+
|
|
139
|
+
function cycleModel() {
|
|
140
|
+
const models = getModels();
|
|
141
|
+
setActiveModel(function (prev) {
|
|
142
|
+
let found = false;
|
|
143
|
+
for (let i = 0; i < models.length; i++) {
|
|
144
|
+
if (found) return models[i].id;
|
|
145
|
+
if (models[i].id === prev) found = true;
|
|
146
|
+
}
|
|
147
|
+
return models[0].id;
|
|
148
|
+
});
|
|
149
|
+
}
|
|
108
150
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
setMessages([]);
|
|
115
|
-
setScrollOffset(0);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
if (trigger === '/model') {
|
|
119
|
-
// Toggle between the two local models
|
|
120
|
-
setActiveModel(function (prev) {
|
|
121
|
-
if (prev === 'openaxis/openaxis-mixtral') {
|
|
122
|
-
return 'openaxis/openaxis-heavy';
|
|
123
|
-
}
|
|
124
|
-
return 'openaxis/openaxis-mixtral';
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
if (trigger === '/help') {
|
|
129
|
-
setScrollOffset(0);
|
|
130
|
-
setMessages(function (prev) {
|
|
131
|
-
return prev.concat([{
|
|
132
|
-
role: 'assistant',
|
|
133
|
-
content:
|
|
134
|
-
'**Available commands:**\n' +
|
|
135
|
-
'- `/help` — Show this help\n' +
|
|
136
|
-
'- `/clear` — Clear the conversation\n' +
|
|
137
|
-
'- `/model` — Toggle between Mixtral and Heavy\n' +
|
|
138
|
-
'- `/exit` — Quit the application\n\n' +
|
|
139
|
-
'Type your message and press **Enter** to chat.',
|
|
140
|
-
id: 'help-' + Date.now(),
|
|
141
|
-
}]);
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Always close & clear after any selection
|
|
146
|
-
setInputBuffer('');
|
|
147
|
-
setShowOverlay(false);
|
|
148
|
-
setOverlayIndex(0);
|
|
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;
|
|
149
156
|
}
|
|
150
157
|
|
|
151
158
|
async function handleSubmit(text) {
|
|
152
159
|
const safeText = typeof text === 'string' ? text : '';
|
|
153
|
-
if (safeText.length === 0 || streamingActive === true)
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const msgId = 'user-' + Date.now() + '-' + Math.random().toString(36).slice(2, 8);
|
|
160
|
+
if (safeText.length === 0 || streamingActive === true) return;
|
|
158
161
|
|
|
159
162
|
setStreamingActive(true);
|
|
160
|
-
|
|
161
|
-
setMessages(function (prev) {
|
|
162
|
-
return prev.concat([{ role: 'user', content: safeText, id: msgId }]);
|
|
163
|
-
});
|
|
164
|
-
setInputBuffer('');
|
|
163
|
+
setToolInfo(null);
|
|
165
164
|
setStreamText(' connecting');
|
|
166
|
-
|
|
165
|
+
setThinkingElapsed(0);
|
|
166
|
+
connRef.current = setInterval(function () {
|
|
167
167
|
setStreamText(function (prev) {
|
|
168
168
|
if (prev === ' connecting') return ' connecting.';
|
|
169
169
|
if (prev === ' connecting.') return ' connecting..';
|
|
170
170
|
if (prev === ' connecting..') return ' connecting...';
|
|
171
|
-
|
|
172
|
-
return prev;
|
|
171
|
+
return ' connecting';
|
|
173
172
|
});
|
|
174
173
|
}, 400);
|
|
175
174
|
|
|
175
|
+
setMessages(function (prev) {
|
|
176
|
+
return prev.concat([{
|
|
177
|
+
role: 'user',
|
|
178
|
+
content: safeText,
|
|
179
|
+
id: 'u-' + Date.now(),
|
|
180
|
+
}]);
|
|
181
|
+
});
|
|
182
|
+
setInputBuffer('');
|
|
183
|
+
|
|
176
184
|
const modelInfo = getModelById(activeModel);
|
|
177
|
-
if (modelInfo === null
|
|
178
|
-
if (
|
|
179
|
-
clearInterval(
|
|
180
|
-
|
|
185
|
+
if (modelInfo === null) {
|
|
186
|
+
if (connRef.current !== null) {
|
|
187
|
+
clearInterval(connRef.current);
|
|
188
|
+
connRef.current = null;
|
|
181
189
|
}
|
|
182
190
|
setStreamingActive(false);
|
|
183
191
|
setStreamText('');
|
|
184
|
-
setScrollOffset(0);
|
|
185
192
|
setMessages(function (prev) {
|
|
186
193
|
return prev.concat([{
|
|
187
194
|
role: 'assistant',
|
|
188
195
|
content: 'No model selected. Use `/model` to switch.',
|
|
189
|
-
id: '
|
|
196
|
+
id: 'e-' + Date.now(),
|
|
190
197
|
}]);
|
|
191
198
|
});
|
|
192
199
|
return;
|
|
@@ -200,91 +207,148 @@ function AppRoot() {
|
|
|
200
207
|
const gen = callModel(modelInfo, history, abortController.signal);
|
|
201
208
|
let accumulated = '';
|
|
202
209
|
let firstEvent = true;
|
|
210
|
+
let thinkStarted = false;
|
|
203
211
|
|
|
204
212
|
for await (const event of gen) {
|
|
205
|
-
if (abortController.signal.aborted === true)
|
|
206
|
-
|
|
207
|
-
}
|
|
213
|
+
if (abortController.signal.aborted === true) break;
|
|
214
|
+
|
|
208
215
|
if (firstEvent === true) {
|
|
209
216
|
firstEvent = false;
|
|
210
|
-
if (
|
|
211
|
-
clearInterval(
|
|
212
|
-
|
|
217
|
+
if (connRef.current !== null) {
|
|
218
|
+
clearInterval(connRef.current);
|
|
219
|
+
connRef.current = null;
|
|
213
220
|
}
|
|
214
221
|
}
|
|
222
|
+
|
|
223
|
+
if (event.type === 'tool') {
|
|
224
|
+
setToolInfo({
|
|
225
|
+
tool: event.tool,
|
|
226
|
+
used: event.used,
|
|
227
|
+
sites: event.sites,
|
|
228
|
+
query: event.query,
|
|
229
|
+
});
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
|
|
215
233
|
if (event.type === 'thinking' || event.type === 'token') {
|
|
216
234
|
const content = typeof event.content === 'string' ? event.content : '';
|
|
217
235
|
for (let i = 0; i < content.length; i++) {
|
|
218
236
|
accumulated = accumulated + content[i];
|
|
219
|
-
|
|
237
|
+
const insideThink = checkThinkState(accumulated);
|
|
220
238
|
setStreamText(accumulated);
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
239
|
+
setIsThinking(insideThink);
|
|
240
|
+
if (insideThink === true && thinkStarted === false) {
|
|
241
|
+
thinkStarted = true;
|
|
242
|
+
thinkStartRef.current = Date.now();
|
|
243
|
+
setThinkingElapsed(0);
|
|
244
|
+
}
|
|
245
|
+
if (insideThink === false && thinkStarted === true) {
|
|
246
|
+
thinkStarted = false;
|
|
247
|
+
thinkStartRef.current = null;
|
|
248
|
+
setThinkingElapsed(0);
|
|
249
|
+
}
|
|
250
|
+
await new Promise(function (r) { setTimeout(r, 0); });
|
|
224
251
|
}
|
|
225
252
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
}
|
|
253
|
+
|
|
254
|
+
if (event.type === 'done') break;
|
|
229
255
|
if (event.type === 'timeout') {
|
|
230
256
|
if (accumulated.length > 0) {
|
|
231
|
-
accumulated = accumulated + '\n\n
|
|
257
|
+
accumulated = accumulated + '\n\n\u2014 stream timed out \u2014';
|
|
232
258
|
}
|
|
233
259
|
setStreamText(accumulated);
|
|
234
260
|
break;
|
|
235
261
|
}
|
|
236
262
|
}
|
|
237
263
|
|
|
238
|
-
if (
|
|
239
|
-
clearInterval(
|
|
240
|
-
|
|
264
|
+
if (connRef.current !== null) {
|
|
265
|
+
clearInterval(connRef.current);
|
|
266
|
+
connRef.current = null;
|
|
241
267
|
}
|
|
268
|
+
setIsThinking(false);
|
|
269
|
+
thinkStartRef.current = null;
|
|
270
|
+
setThinkingElapsed(0);
|
|
242
271
|
setStreamingActive(false);
|
|
272
|
+
|
|
273
|
+
const finalText = streamText;
|
|
243
274
|
setStreamText('');
|
|
244
275
|
|
|
245
|
-
if (
|
|
276
|
+
if (finalText.length > 0) {
|
|
246
277
|
setMessages(function (prev) {
|
|
247
278
|
return prev.concat([{
|
|
248
279
|
role: 'assistant',
|
|
249
|
-
content:
|
|
250
|
-
id: '
|
|
280
|
+
content: finalText,
|
|
281
|
+
id: 'a-' + Date.now(),
|
|
251
282
|
}]);
|
|
252
283
|
});
|
|
253
284
|
}
|
|
254
285
|
} catch (err) {
|
|
255
|
-
if (
|
|
256
|
-
clearInterval(
|
|
257
|
-
|
|
286
|
+
if (connRef.current !== null) {
|
|
287
|
+
clearInterval(connRef.current);
|
|
288
|
+
connRef.current = null;
|
|
258
289
|
}
|
|
290
|
+
setIsThinking(false);
|
|
291
|
+
thinkStartRef.current = null;
|
|
292
|
+
setThinkingElapsed(0);
|
|
259
293
|
setStreamingActive(false);
|
|
260
294
|
setStreamText('');
|
|
261
|
-
|
|
262
|
-
const errMsg = (err !== null && err !== undefined)
|
|
295
|
+
const errMsg = err !== null && err !== undefined
|
|
263
296
|
? (err.message || String(err))
|
|
264
297
|
: 'Unknown error';
|
|
298
|
+
setMessages(function (prev) {
|
|
299
|
+
return prev.concat([{
|
|
300
|
+
role: 'assistant',
|
|
301
|
+
content: '**Error:** ' + errMsg + '\n\nThe Space may be cold-starting (30\u2013120 s first request). Try again.',
|
|
302
|
+
id: 'er-' + Date.now(),
|
|
303
|
+
}]);
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function handleInputChange(v) {
|
|
309
|
+
const safe = typeof v === 'string' ? v : '';
|
|
310
|
+
setInputBuffer(safe);
|
|
311
|
+
if (safe.length === 1 && safe[0] === '/' && showOverlay === false) {
|
|
312
|
+
setShowOverlay(true);
|
|
313
|
+
setOverlayIndex(0);
|
|
314
|
+
}
|
|
315
|
+
if (showOverlay === true && (safe.length === 0 || safe[0] !== '/')) {
|
|
316
|
+
setShowOverlay(false);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function handleSlashSelect(item) {
|
|
321
|
+
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') {
|
|
265
333
|
setMessages(function (prev) {
|
|
266
334
|
return prev.concat([{
|
|
267
335
|
role: 'assistant',
|
|
268
336
|
content:
|
|
269
|
-
'**
|
|
270
|
-
'
|
|
271
|
-
|
|
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(),
|
|
272
343
|
}]);
|
|
273
344
|
});
|
|
274
345
|
}
|
|
346
|
+
setInputBuffer('');
|
|
347
|
+
setShowOverlay(false);
|
|
348
|
+
setOverlayIndex(0);
|
|
275
349
|
}
|
|
276
350
|
|
|
277
|
-
// ---------------------------------------------------------------------------
|
|
278
|
-
// Keyboard router
|
|
279
|
-
//
|
|
280
|
-
// Overlay mode: ↑↓ move cursor · Enter selects · Esc dismisses
|
|
281
|
-
// All other keys are absorbed (TextInput has focus=false)
|
|
282
|
-
//
|
|
283
|
-
// Normal mode: Enter submits · Ctrl+C abort/quit
|
|
284
|
-
// ---------------------------------------------------------------------------
|
|
285
351
|
useInput(function handleInput(input, key) {
|
|
286
|
-
|
|
287
|
-
// ── Overlay branch ──────────────────────────────────────────────────────
|
|
288
352
|
if (showOverlay === true) {
|
|
289
353
|
if (key.escape === true) {
|
|
290
354
|
setShowOverlay(false);
|
|
@@ -292,63 +356,33 @@ function AppRoot() {
|
|
|
292
356
|
setOverlayIndex(0);
|
|
293
357
|
return;
|
|
294
358
|
}
|
|
295
|
-
|
|
296
359
|
if (key.upArrow === true) {
|
|
297
|
-
setOverlayIndex(function (
|
|
298
|
-
return prev > 0 ? prev - 1 : commands.length - 1; // wrap around top
|
|
299
|
-
});
|
|
360
|
+
setOverlayIndex(function (p) { return p > 0 ? p - 1 : commands.length - 1; });
|
|
300
361
|
return;
|
|
301
362
|
}
|
|
302
|
-
|
|
303
363
|
if (key.downArrow === true) {
|
|
304
|
-
setOverlayIndex(function (
|
|
305
|
-
return prev < commands.length - 1 ? prev + 1 : 0; // wrap around bottom
|
|
306
|
-
});
|
|
364
|
+
setOverlayIndex(function (p) { return p < commands.length - 1 ? p + 1 : 0; });
|
|
307
365
|
return;
|
|
308
366
|
}
|
|
309
|
-
|
|
310
367
|
if (key.return === true) {
|
|
311
|
-
const
|
|
312
|
-
if (
|
|
313
|
-
handleSlashSelect({ value: selectedCmd.trigger });
|
|
314
|
-
}
|
|
368
|
+
const cmd = commands[overlayIndex];
|
|
369
|
+
if (cmd !== null && cmd !== undefined) handleSlashSelect({ value: cmd.trigger });
|
|
315
370
|
return;
|
|
316
371
|
}
|
|
317
|
-
|
|
318
|
-
// Absorb everything else while overlay is open
|
|
319
372
|
return;
|
|
320
373
|
}
|
|
321
374
|
|
|
322
|
-
// ── Normal branch ────────────────────────────────────────────────────────
|
|
323
375
|
if (key.return === true) {
|
|
324
|
-
const
|
|
325
|
-
if (
|
|
326
|
-
handleSubmit(
|
|
376
|
+
const safe = typeof inputBuffer === 'string' ? inputBuffer : '';
|
|
377
|
+
if (safe.length > 0 && streamingActive === false) {
|
|
378
|
+
handleSubmit(safe);
|
|
327
379
|
}
|
|
328
380
|
return;
|
|
329
381
|
}
|
|
330
382
|
|
|
331
|
-
if (key.pageUp === true || (key.ctrl === true && key.upArrow === true)) {
|
|
332
|
-
const pageSize = Math.max(1, availLines - 2);
|
|
333
|
-
setScrollOffset(function (prev) {
|
|
334
|
-
return prev + pageSize;
|
|
335
|
-
});
|
|
336
|
-
return;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
if (key.pageDown === true || (key.ctrl === true && key.downArrow === true)) {
|
|
340
|
-
const pageSize = Math.max(1, availLines - 2);
|
|
341
|
-
setScrollOffset(function (prev) {
|
|
342
|
-
return Math.max(0, prev - pageSize);
|
|
343
|
-
});
|
|
344
|
-
return;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
383
|
if (key.ctrl === true && (input === 'c' || input === 'C')) {
|
|
348
384
|
if (abortRef.current !== null) {
|
|
349
|
-
try {
|
|
350
|
-
abortRef.current.abort();
|
|
351
|
-
} catch (_) { }
|
|
385
|
+
try { abortRef.current.abort(); } catch (_) {}
|
|
352
386
|
abortRef.current = null;
|
|
353
387
|
setStreamingActive(false);
|
|
354
388
|
setStreamText('');
|
|
@@ -357,27 +391,141 @@ function AppRoot() {
|
|
|
357
391
|
}
|
|
358
392
|
});
|
|
359
393
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
394
|
+
const modelBar = createModelBar(activeModel, cols);
|
|
395
|
+
const particleTop = createParticleRow(particleFrame, cols, 'tp');
|
|
396
|
+
|
|
397
|
+
const viewLines = [];
|
|
398
|
+
for (let i = 0; i < messages.length; i++) {
|
|
399
|
+
const msg = messages[i];
|
|
400
|
+
if (msg === null || typeof msg !== 'object') continue;
|
|
401
|
+
const content = typeof msg.content === 'string' ? msg.content : '';
|
|
402
|
+
if (content.length === 0) continue;
|
|
403
|
+
|
|
404
|
+
if (i > 0) viewLines.push({ type: 'sep' });
|
|
405
|
+
|
|
406
|
+
if (msg.role === 'user') {
|
|
407
|
+
viewLines.push({ type: 'user', text: content });
|
|
408
|
+
viewLines.push({ type: 'sep' });
|
|
409
|
+
} else {
|
|
410
|
+
viewLines.push({ type: 'assistant', text: content });
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const hasStream = typeof streamText === 'string' && streamText.length > 0;
|
|
415
|
+
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
|
+
});
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
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
|
+
});
|
|
432
|
+
const clean = streamText.split('<think>').join('').split('</think>').join('');
|
|
433
|
+
viewLines.push({ type: 'stream', text: clean, color: '#FF9F43' });
|
|
434
|
+
} else {
|
|
435
|
+
const clean = streamText.split('<think>').join('').split('</think>').join('');
|
|
436
|
+
viewLines.push({ type: 'stream', text: clean, color: hex.primary });
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (viewLines.length === 0) {
|
|
441
|
+
viewLines.push({ type: 'muted', text: ' type a message to start...' });
|
|
442
|
+
}
|
|
443
|
+
|
|
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
|
+
)
|
|
458
|
+
)
|
|
459
|
+
);
|
|
460
|
+
} else if (line.type === 'tool') {
|
|
461
|
+
viewElements.push(
|
|
462
|
+
h(Box, { key: 'vtl-' + i, height: 1 },
|
|
463
|
+
h(Text, { color: '#5B5B8A' }, line.text)
|
|
464
|
+
)
|
|
465
|
+
);
|
|
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)
|
|
475
|
+
)
|
|
476
|
+
);
|
|
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)
|
|
485
|
+
)
|
|
486
|
+
);
|
|
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)
|
|
495
|
+
)
|
|
496
|
+
);
|
|
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)
|
|
501
|
+
)
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const viewport = h(Box, {
|
|
507
|
+
flexGrow: 1,
|
|
508
|
+
height: availLines,
|
|
509
|
+
flexDirection: 'column',
|
|
510
|
+
overflow: 'hidden',
|
|
511
|
+
paddingBottom: 1,
|
|
512
|
+
}, ...viewElements);
|
|
513
|
+
|
|
514
|
+
const particleBot = createParticleRow(particleFrame, cols, 'bt');
|
|
363
515
|
|
|
364
|
-
const header = createBrandHeader(cols);
|
|
365
|
-
const routerBar = createRouterBar(activeModel, cols);
|
|
366
|
-
const viewport = createChatViewport(messages, streamText, availLines, cols, scrollOffset);
|
|
367
516
|
const slashOverlay = showOverlay === true
|
|
368
517
|
? createSlashOverlay(commands, overlayIndex)
|
|
369
518
|
: null;
|
|
519
|
+
|
|
370
520
|
const dock = createComposerDock({
|
|
371
521
|
value: inputBuffer,
|
|
372
522
|
onChange: handleInputChange,
|
|
373
|
-
onSubmit:
|
|
374
|
-
inputActive: showOverlay === false,
|
|
523
|
+
onSubmit: function () {},
|
|
524
|
+
inputActive: showOverlay === false,
|
|
375
525
|
isStreaming: streamingActive,
|
|
376
526
|
terminalWidth: cols,
|
|
377
527
|
});
|
|
378
528
|
|
|
379
|
-
// Strict column layout — total height === rows (no scroll, no overflow).
|
|
380
|
-
// ComposerDock has flexShrink:0 so it is always pinned at bottom.
|
|
381
529
|
return h(Box, {
|
|
382
530
|
flexDirection: 'column',
|
|
383
531
|
width: '100%',
|
|
@@ -385,10 +533,14 @@ function AppRoot() {
|
|
|
385
533
|
backgroundColor: hex.black,
|
|
386
534
|
overflow: 'hidden',
|
|
387
535
|
},
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
536
|
+
modelBar,
|
|
537
|
+
h(Box, { height: 1 }, barLine(cols)),
|
|
538
|
+
h(Box, { height: 1, paddingLeft: 1 }, particleTop),
|
|
539
|
+
viewport,
|
|
540
|
+
h(Box, { height: 1, paddingLeft: 1 }, particleBot),
|
|
541
|
+
slashOverlay,
|
|
542
|
+
dock
|
|
393
543
|
);
|
|
394
544
|
}
|
|
545
|
+
|
|
546
|
+
import { createSlashOverlay } from './components/SlashOverlay.js';
|