openaxies 0.1.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/bin/openaxis.js +58 -0
- package/package.json +19 -0
- package/src/App.js +391 -0
- package/src/components/BrandHeader.js +60 -0
- package/src/components/ChatViewport.js +194 -0
- package/src/components/ComposerDock.js +105 -0
- package/src/components/RouterBar.js +98 -0
- package/src/components/SlashOverlay.js +130 -0
- package/src/config/commands.js +56 -0
- package/src/config/models.js +67 -0
- package/src/config/theme.js +33 -0
- package/src/providers/index.js +110 -0
- package/src/providers/streaming.js +144 -0
- package/src/providers/websearch.js +152 -0
package/bin/openaxis.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { render } from 'ink';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import App from '../src/App.js';
|
|
7
|
+
|
|
8
|
+
const h = React.createElement;
|
|
9
|
+
|
|
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
|
+
|
|
37
|
+
function cleanup() {
|
|
38
|
+
if (debugBuffer.length > 0) {
|
|
39
|
+
const logPath = path.join(process.cwd(), 'openaxies-debug.log');
|
|
40
|
+
try {
|
|
41
|
+
fs.writeFileSync(logPath, debugBuffer.join('\n') + '\n');
|
|
42
|
+
} catch (_) {
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
process.exit(0);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
process.on('SIGINT', cleanup);
|
|
49
|
+
process.on('SIGTERM', cleanup);
|
|
50
|
+
process.on('exit', function () {
|
|
51
|
+
if (debugBuffer.length > 0) {
|
|
52
|
+
const logPath = path.join(process.cwd(), 'openaxies-debug.log');
|
|
53
|
+
try {
|
|
54
|
+
fs.writeFileSync(logPath, debugBuffer.join('\n') + '\n');
|
|
55
|
+
} catch (_) {
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "openaxies",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"openaxies": "./bin/openaxis.js",
|
|
8
|
+
"openaxis": "./bin/openaxis.js"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@langchain/core": "^1.0.0",
|
|
12
|
+
"@langchain/langgraph": "^1.0.0",
|
|
13
|
+
"ink": "^5.0.0",
|
|
14
|
+
"ink-big-text": "^2.0.0",
|
|
15
|
+
"ink-select-input": "^6.2.0",
|
|
16
|
+
"ink-text-input": "^6.0.0",
|
|
17
|
+
"react": "^18.3.1"
|
|
18
|
+
}
|
|
19
|
+
}
|
package/src/App.js
ADDED
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, useInput, useStdout } from 'ink';
|
|
3
|
+
import { hex } from './config/theme.js';
|
|
4
|
+
import { getCommands } from './config/commands.js';
|
|
5
|
+
import { getModelById, DEFAULT_MODEL_ID } from './config/models.js';
|
|
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
|
+
import { createComposerDock, DOCK_HEIGHT } from './components/ComposerDock.js';
|
|
12
|
+
|
|
13
|
+
const h = React.createElement;
|
|
14
|
+
|
|
15
|
+
export default AppRoot;
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Helpers
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
function buildHistory(messages, newText) {
|
|
22
|
+
const history = [];
|
|
23
|
+
for (let i = 0; i < messages.length; i++) {
|
|
24
|
+
const m = messages[i];
|
|
25
|
+
if (m === null || m === undefined || typeof m !== 'object') {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (m.role === 'user' || m.role === 'assistant') {
|
|
29
|
+
const content = typeof m.content === 'string' ? m.content : '';
|
|
30
|
+
history.push({ role: m.role, content: content });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (typeof newText === 'string' && newText.length > 0) {
|
|
34
|
+
history.push({ role: 'user', content: newText });
|
|
35
|
+
}
|
|
36
|
+
return history;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Root component
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
function AppRoot() {
|
|
44
|
+
const { stdout } = useStdout();
|
|
45
|
+
const rows = stdout.rows || 24;
|
|
46
|
+
const cols = stdout.columns || 80;
|
|
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
|
+
const [messages, setMessages] = React.useState([]);
|
|
55
|
+
const [activeModel, setActiveModel] = React.useState(DEFAULT_MODEL_ID);
|
|
56
|
+
const [streamText, setStreamText] = React.useState('');
|
|
57
|
+
const [streamingActive, setStreamingActive] = React.useState(false);
|
|
58
|
+
const [scrollOffset, setScrollOffset] = React.useState(0);
|
|
59
|
+
|
|
60
|
+
const commands = getCommands();
|
|
61
|
+
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// Strict row budget
|
|
64
|
+
// BrandHeader : BRAND_HEIGHT (7)
|
|
65
|
+
// RouterBar : ROUTER_HEIGHT (2)
|
|
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;
|
|
72
|
+
const availLines = Math.max(1, rows - fixedH);
|
|
73
|
+
|
|
74
|
+
const abortRef = React.useRef(null);
|
|
75
|
+
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
// Input handlers
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
function handleInputChange(value) {
|
|
81
|
+
const safeValue = typeof value === 'string' ? value : '';
|
|
82
|
+
setInputBuffer(safeValue);
|
|
83
|
+
// Open overlay on first '/' character
|
|
84
|
+
if (safeValue.length === 1 && safeValue[0] === '/' && showOverlay === false) {
|
|
85
|
+
setShowOverlay(true);
|
|
86
|
+
setOverlayIndex(0);
|
|
87
|
+
}
|
|
88
|
+
// Close overlay if buffer no longer starts with '/'
|
|
89
|
+
if (showOverlay === true && (safeValue.length === 0 || safeValue[0] !== '/')) {
|
|
90
|
+
setShowOverlay(false);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// onSubmit for TextInput — actual submit goes through useInput Enter handler.
|
|
95
|
+
function handleInputSubmit() {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function handleSlashSelect(item) {
|
|
100
|
+
if (item === null || item === undefined) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const trigger = item.value;
|
|
104
|
+
if (typeof trigger !== 'string') {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (trigger === '/exit') {
|
|
109
|
+
process.exit(0);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (trigger === '/clear') {
|
|
113
|
+
setMessages([]);
|
|
114
|
+
setScrollOffset(0);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (trigger === '/model') {
|
|
118
|
+
// Toggle between the two local models
|
|
119
|
+
setActiveModel(function (prev) {
|
|
120
|
+
if (prev === 'openaxis/openaxis-flash') {
|
|
121
|
+
return 'openaxis/openaxis-heavy';
|
|
122
|
+
}
|
|
123
|
+
return 'openaxis/openaxis-flash';
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (trigger === '/help') {
|
|
128
|
+
setScrollOffset(0);
|
|
129
|
+
setMessages(function (prev) {
|
|
130
|
+
return prev.concat([{
|
|
131
|
+
role: 'assistant',
|
|
132
|
+
content:
|
|
133
|
+
'**Available commands:**\n' +
|
|
134
|
+
'- `/help` — Show this help\n' +
|
|
135
|
+
'- `/clear` — Clear the conversation\n' +
|
|
136
|
+
'- `/model` — Toggle between OpenAxies Flash and Heavy\n' +
|
|
137
|
+
'- `/exit` — Quit the application\n\n' +
|
|
138
|
+
'Type your message and press **Enter** to chat.',
|
|
139
|
+
id: 'help-' + Date.now(),
|
|
140
|
+
}]);
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Always close & clear after any selection
|
|
145
|
+
setInputBuffer('');
|
|
146
|
+
setShowOverlay(false);
|
|
147
|
+
setOverlayIndex(0);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function handleSubmit(text) {
|
|
151
|
+
const safeText = typeof text === 'string' ? text : '';
|
|
152
|
+
if (safeText.length === 0 || streamingActive === true) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const msgId = 'user-' + Date.now() + '-' + Math.random().toString(36).slice(2, 8);
|
|
157
|
+
|
|
158
|
+
setStreamingActive(true);
|
|
159
|
+
setScrollOffset(0);
|
|
160
|
+
setMessages(function (prev) {
|
|
161
|
+
return prev.concat([{ role: 'user', content: safeText, id: msgId }]);
|
|
162
|
+
});
|
|
163
|
+
setInputBuffer('');
|
|
164
|
+
setStreamText('\u2022\u2022\u2022'); // animated placeholder
|
|
165
|
+
|
|
166
|
+
const modelInfo = getModelById(activeModel);
|
|
167
|
+
if (modelInfo === null || modelInfo === undefined) {
|
|
168
|
+
setStreamingActive(false);
|
|
169
|
+
setStreamText('');
|
|
170
|
+
setScrollOffset(0);
|
|
171
|
+
setMessages(function (prev) {
|
|
172
|
+
return prev.concat([{
|
|
173
|
+
role: 'assistant',
|
|
174
|
+
content: 'No model selected. Use `/model` to switch.',
|
|
175
|
+
id: 'err-' + Date.now(),
|
|
176
|
+
}]);
|
|
177
|
+
});
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const history = buildHistory(messages, safeText);
|
|
182
|
+
const abortController = new AbortController();
|
|
183
|
+
abortRef.current = abortController;
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
const gen = callModel(modelInfo, history, abortController.signal);
|
|
187
|
+
let accumulated = '';
|
|
188
|
+
let thinkingId = null;
|
|
189
|
+
const thinkingLines = [];
|
|
190
|
+
|
|
191
|
+
for await (const event of gen) {
|
|
192
|
+
if (abortController.signal.aborted === true) {
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
if (event.type === 'thinking') {
|
|
196
|
+
if (thinkingId === null) {
|
|
197
|
+
thinkingId = 'think-' + Date.now() + '-' + Math.random().toString(36).slice(2, 8);
|
|
198
|
+
}
|
|
199
|
+
thinkingLines.push(typeof event.content === 'string' ? event.content : '');
|
|
200
|
+
const content = thinkingLines.join('\n');
|
|
201
|
+
setMessages(function (prev) {
|
|
202
|
+
let replaced = false;
|
|
203
|
+
const next = [];
|
|
204
|
+
for (let i = 0; i < prev.length; i++) {
|
|
205
|
+
const item = prev[i];
|
|
206
|
+
if (item !== null && item !== undefined && item.id === thinkingId) {
|
|
207
|
+
next.push({ role: 'thinking', content: content, id: thinkingId });
|
|
208
|
+
replaced = true;
|
|
209
|
+
} else {
|
|
210
|
+
next.push(item);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (replaced === false) {
|
|
214
|
+
next.push({ role: 'thinking', content: content, id: thinkingId });
|
|
215
|
+
}
|
|
216
|
+
return next;
|
|
217
|
+
});
|
|
218
|
+
setScrollOffset(0);
|
|
219
|
+
}
|
|
220
|
+
if (event.type === 'token') {
|
|
221
|
+
const content = typeof event.content === 'string' ? event.content : '';
|
|
222
|
+
for (let i = 0; i < content.length; i++) {
|
|
223
|
+
accumulated = accumulated + content[i];
|
|
224
|
+
setScrollOffset(0);
|
|
225
|
+
setStreamText(accumulated);
|
|
226
|
+
await new Promise(function (resolve) {
|
|
227
|
+
setTimeout(resolve, 0);
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (event.type === 'done') {
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
if (event.type === 'timeout') {
|
|
235
|
+
if (accumulated.length > 0) {
|
|
236
|
+
accumulated = accumulated + '\n\n*[Response incomplete \u2014 stream timed out]*';
|
|
237
|
+
}
|
|
238
|
+
setStreamText(accumulated);
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
setStreamingActive(false);
|
|
244
|
+
setStreamText('');
|
|
245
|
+
|
|
246
|
+
if (accumulated.length > 0) {
|
|
247
|
+
setMessages(function (prev) {
|
|
248
|
+
return prev.concat([{
|
|
249
|
+
role: 'assistant',
|
|
250
|
+
content: accumulated,
|
|
251
|
+
id: 'resp-' + Date.now() + '-' + Math.random().toString(36).slice(2, 8),
|
|
252
|
+
}]);
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
} catch (err) {
|
|
256
|
+
setStreamingActive(false);
|
|
257
|
+
setStreamText('');
|
|
258
|
+
setScrollOffset(0);
|
|
259
|
+
const errMsg = (err !== null && err !== undefined)
|
|
260
|
+
? (err.message || String(err))
|
|
261
|
+
: 'Unknown error';
|
|
262
|
+
setMessages(function (prev) {
|
|
263
|
+
return prev.concat([{
|
|
264
|
+
role: 'assistant',
|
|
265
|
+
content:
|
|
266
|
+
'**Connection Error:** ' + errMsg +
|
|
267
|
+
'\n\nThe Space may be cold-starting (model download can take 30\u2013120 s). Please try again.',
|
|
268
|
+
id: 'err-' + Date.now() + '-' + Math.random().toString(36).slice(2, 8),
|
|
269
|
+
}]);
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ---------------------------------------------------------------------------
|
|
275
|
+
// Keyboard router
|
|
276
|
+
//
|
|
277
|
+
// Overlay mode: ↑↓ move cursor · Enter selects · Esc dismisses
|
|
278
|
+
// All other keys are absorbed (TextInput has focus=false)
|
|
279
|
+
//
|
|
280
|
+
// Normal mode: Enter submits · Ctrl+C abort/quit
|
|
281
|
+
// ---------------------------------------------------------------------------
|
|
282
|
+
useInput(function handleInput(input, key) {
|
|
283
|
+
|
|
284
|
+
// ── Overlay branch ──────────────────────────────────────────────────────
|
|
285
|
+
if (showOverlay === true) {
|
|
286
|
+
if (key.escape === true) {
|
|
287
|
+
setShowOverlay(false);
|
|
288
|
+
setInputBuffer('');
|
|
289
|
+
setOverlayIndex(0);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (key.upArrow === true) {
|
|
294
|
+
setOverlayIndex(function (prev) {
|
|
295
|
+
return prev > 0 ? prev - 1 : commands.length - 1; // wrap around top
|
|
296
|
+
});
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (key.downArrow === true) {
|
|
301
|
+
setOverlayIndex(function (prev) {
|
|
302
|
+
return prev < commands.length - 1 ? prev + 1 : 0; // wrap around bottom
|
|
303
|
+
});
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (key.return === true) {
|
|
308
|
+
const selectedCmd = commands[overlayIndex];
|
|
309
|
+
if (selectedCmd !== null && selectedCmd !== undefined) {
|
|
310
|
+
handleSlashSelect({ value: selectedCmd.trigger });
|
|
311
|
+
}
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Absorb everything else while overlay is open
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// ── Normal branch ────────────────────────────────────────────────────────
|
|
320
|
+
if (key.return === true) {
|
|
321
|
+
const safeValue = typeof inputBuffer === 'string' ? inputBuffer : '';
|
|
322
|
+
if (safeValue.length > 0 && streamingActive === false) {
|
|
323
|
+
handleSubmit(safeValue);
|
|
324
|
+
}
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (key.pageUp === true || (key.ctrl === true && key.upArrow === true)) {
|
|
329
|
+
const pageSize = Math.max(1, availLines - 2);
|
|
330
|
+
setScrollOffset(function (prev) {
|
|
331
|
+
return prev + pageSize;
|
|
332
|
+
});
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (key.pageDown === true || (key.ctrl === true && key.downArrow === true)) {
|
|
337
|
+
const pageSize = Math.max(1, availLines - 2);
|
|
338
|
+
setScrollOffset(function (prev) {
|
|
339
|
+
return Math.max(0, prev - pageSize);
|
|
340
|
+
});
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (key.ctrl === true && (input === 'c' || input === 'C')) {
|
|
345
|
+
if (abortRef.current !== null) {
|
|
346
|
+
try {
|
|
347
|
+
abortRef.current.abort();
|
|
348
|
+
} catch (_) { }
|
|
349
|
+
abortRef.current = null;
|
|
350
|
+
setStreamingActive(false);
|
|
351
|
+
setStreamText('');
|
|
352
|
+
}
|
|
353
|
+
process.exit(0);
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
// ---------------------------------------------------------------------------
|
|
358
|
+
// Render
|
|
359
|
+
// ---------------------------------------------------------------------------
|
|
360
|
+
|
|
361
|
+
const header = createBrandHeader(cols);
|
|
362
|
+
const routerBar = createRouterBar(activeModel, cols);
|
|
363
|
+
const viewport = createChatViewport(messages, streamText, availLines, cols, scrollOffset);
|
|
364
|
+
const slashOverlay = showOverlay === true
|
|
365
|
+
? createSlashOverlay(commands, overlayIndex)
|
|
366
|
+
: null;
|
|
367
|
+
const dock = createComposerDock({
|
|
368
|
+
value: inputBuffer,
|
|
369
|
+
onChange: handleInputChange,
|
|
370
|
+
onSubmit: handleInputSubmit,
|
|
371
|
+
inputActive: showOverlay === false, // yield focus to overlay navigation
|
|
372
|
+
isStreaming: streamingActive,
|
|
373
|
+
terminalWidth: cols,
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
// Strict column layout — total height === rows (no scroll, no overflow).
|
|
377
|
+
// ComposerDock has flexShrink:0 so it is always pinned at bottom.
|
|
378
|
+
return h(Box, {
|
|
379
|
+
flexDirection: 'column',
|
|
380
|
+
width: '100%',
|
|
381
|
+
height: rows,
|
|
382
|
+
backgroundColor: hex.black,
|
|
383
|
+
overflow: 'hidden',
|
|
384
|
+
},
|
|
385
|
+
header, // 7 rows
|
|
386
|
+
routerBar, // 2 rows
|
|
387
|
+
viewport, // dynamic — flexGrow fills all remaining rows
|
|
388
|
+
slashOverlay, // 6 rows (null when closed)
|
|
389
|
+
dock // 3 rows — flexShrink:0, always at bottom
|
|
390
|
+
);
|
|
391
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { hex } from '../config/theme.js';
|
|
4
|
+
|
|
5
|
+
const h = React.createElement;
|
|
6
|
+
|
|
7
|
+
export const BRAND_HEIGHT = 5;
|
|
8
|
+
|
|
9
|
+
function checkColumns(cols) {
|
|
10
|
+
if (typeof cols !== 'number' || cols < 20) {
|
|
11
|
+
return 80;
|
|
12
|
+
}
|
|
13
|
+
return Math.max(20, Math.floor(cols));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function padLine(content, innerWidth) {
|
|
17
|
+
const safeContent = typeof content === 'string' ? content : '';
|
|
18
|
+
const clipped = safeContent.length > innerWidth
|
|
19
|
+
? safeContent.slice(0, innerWidth)
|
|
20
|
+
: safeContent;
|
|
21
|
+
return clipped + ' '.repeat(Math.max(0, innerWidth - clipped.length));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function createBrandHeader(cols) {
|
|
25
|
+
const width = checkColumns(cols);
|
|
26
|
+
const innerWidth = Math.max(1, width - 2);
|
|
27
|
+
const rule = '\u2500'.repeat(innerWidth);
|
|
28
|
+
const statusLine = ' [STATUS: ACTIVE] [ENGINE: 32B-OLLAMA] [WEBSEARCH: READY]';
|
|
29
|
+
|
|
30
|
+
return h(Box, {
|
|
31
|
+
flexDirection: 'column',
|
|
32
|
+
width: '100%',
|
|
33
|
+
height: BRAND_HEIGHT,
|
|
34
|
+
backgroundColor: hex.black,
|
|
35
|
+
flexShrink: 0,
|
|
36
|
+
overflow: 'hidden',
|
|
37
|
+
},
|
|
38
|
+
h(Box, { height: 1 },
|
|
39
|
+
h(Text, { color: hex.primary, bold: true }, '\u250C' + rule + '\u2510')
|
|
40
|
+
),
|
|
41
|
+
h(Box, { height: 1 },
|
|
42
|
+
h(Text, { color: hex.primary, bold: true },
|
|
43
|
+
'\u2502' + padLine(' OPEN', innerWidth) + '\u2502'
|
|
44
|
+
)
|
|
45
|
+
),
|
|
46
|
+
h(Box, { height: 1 },
|
|
47
|
+
h(Text, { color: hex.primary, bold: true },
|
|
48
|
+
'\u2502' + padLine(' AXIES', innerWidth) + '\u2502'
|
|
49
|
+
)
|
|
50
|
+
),
|
|
51
|
+
h(Box, { height: 1 },
|
|
52
|
+
h(Text, { color: hex.greenOnline, bold: true },
|
|
53
|
+
'\u2502' + padLine(statusLine, innerWidth) + '\u2502'
|
|
54
|
+
)
|
|
55
|
+
),
|
|
56
|
+
h(Box, { height: 1 },
|
|
57
|
+
h(Text, { color: hex.primary, bold: true }, '\u2514' + rule + '\u2518')
|
|
58
|
+
),
|
|
59
|
+
);
|
|
60
|
+
}
|