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
|
@@ -1,105 +1,54 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { Box, Text } from 'ink';
|
|
2
|
+
import { Box, Text, useInput, useStdout } from 'ink';
|
|
3
3
|
import TextInput from 'ink-text-input';
|
|
4
4
|
import { hex } from '../config/theme.js';
|
|
5
5
|
|
|
6
6
|
const h = React.createElement;
|
|
7
7
|
|
|
8
8
|
export const DOCK_HEIGHT = 3;
|
|
9
|
-
const MAX_FRAME_WIDTH = 80;
|
|
10
|
-
const PROMPT = 'openaxies@root:~$ ';
|
|
11
9
|
|
|
12
|
-
|
|
13
|
-
if (typeof value !== 'string') {
|
|
14
|
-
return '';
|
|
15
|
-
}
|
|
16
|
-
return value;
|
|
17
|
-
}
|
|
10
|
+
const PROMPT = '> ';
|
|
18
11
|
|
|
19
|
-
function checkCallback(fn, label) {
|
|
20
|
-
if (fn !== null && fn !== undefined && typeof fn !== 'function') {
|
|
21
|
-
throw new Error(label + ' must be a function');
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// ComposerDock — 3 rows, permanently at the bottom of the layout.
|
|
26
|
-
//
|
|
27
|
-
// Layout:
|
|
28
|
-
// ┌──────────────────────────────────────────────────────────────────────────────┐
|
|
29
|
-
// │ openaxies@root:~$ [TextInput] │
|
|
30
|
-
// └──────────────────────────────────────────────────────────────────────────────┘
|
|
31
|
-
//
|
|
32
|
-
// The frame is capped at 80 columns for narrow terminal clients.
|
|
33
|
-
// flexShrink: 0 prevents Ink from ever yielding these rows to the viewport.
|
|
34
|
-
//
|
|
35
|
-
// config.inputActive (bool, default true):
|
|
36
|
-
// Set to false when SlashOverlay is open so TextInput releases keyboard
|
|
37
|
-
// focus and arrow keys don't corrupt the input buffer.
|
|
38
12
|
export function createComposerDock(config) {
|
|
39
13
|
if (config === null || config === undefined || typeof config !== 'object') {
|
|
40
14
|
throw new Error('createComposerDock requires a config object');
|
|
41
15
|
}
|
|
42
16
|
|
|
43
|
-
const value =
|
|
17
|
+
const value = typeof config.value === 'string' ? config.value : '';
|
|
44
18
|
const onChange = config.onChange;
|
|
45
19
|
const onSubmit = config.onSubmit;
|
|
46
20
|
const inputActive = config.inputActive !== false;
|
|
47
21
|
const isStreaming = config.isStreaming === true;
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
: MAX_FRAME_WIDTH;
|
|
51
|
-
|
|
52
|
-
checkCallback(onChange, 'onChange');
|
|
53
|
-
checkCallback(onSubmit, 'onSubmit');
|
|
22
|
+
const modelLabel = typeof config.modelLabel === 'string' ? config.modelLabel : '';
|
|
23
|
+
const modelStatus = config.modelStatus === true;
|
|
54
24
|
|
|
55
|
-
const placeholder = isStreaming
|
|
56
|
-
? 'Ctrl+C to interrupt...'
|
|
57
|
-
: '/ commands';
|
|
58
|
-
const frameWidth = Math.max(2, Math.min(MAX_FRAME_WIDTH, terminalWidth));
|
|
59
|
-
const innerWidth = frameWidth - 2;
|
|
60
|
-
const inputWidth = Math.max(1, innerWidth - PROMPT.length);
|
|
61
|
-
const horizontalRule = '\u2500'.repeat(innerWidth);
|
|
25
|
+
const placeholder = isStreaming ? 'Ctrl+C to interrupt...' : 'type a message...';
|
|
62
26
|
|
|
63
27
|
return h(Box, {
|
|
64
28
|
width: '100%',
|
|
65
29
|
height: DOCK_HEIGHT,
|
|
66
30
|
flexDirection: 'column',
|
|
67
|
-
alignItems: 'center',
|
|
68
31
|
flexShrink: 0,
|
|
69
32
|
},
|
|
70
|
-
h(Box, {
|
|
71
|
-
h(Text, { color: hex.
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
h(Box, {
|
|
82
|
-
width: innerWidth,
|
|
83
|
-
height: 1,
|
|
84
|
-
flexDirection: 'row',
|
|
85
|
-
overflow: 'hidden',
|
|
86
|
-
},
|
|
87
|
-
h(Text, { color: inputActive ? hex.greenOnline : hex.muted, bold: true }, PROMPT),
|
|
88
|
-
h(Box, { width: inputWidth, height: 1, overflow: 'hidden' },
|
|
89
|
-
h(TextInput, {
|
|
90
|
-
value: value,
|
|
91
|
-
onChange: onChange,
|
|
92
|
-
onSubmit: onSubmit,
|
|
93
|
-
placeholder: placeholder,
|
|
94
|
-
focus: inputActive,
|
|
95
|
-
showCursor: true,
|
|
96
|
-
})
|
|
97
|
-
)
|
|
33
|
+
h(Box, { height: 1, paddingLeft: 1, paddingRight: 1 },
|
|
34
|
+
h(Text, { color: inputActive ? hex.primary : '#444466' }, PROMPT),
|
|
35
|
+
h(Box, { flexGrow: 1, height: 1, overflow: 'hidden' },
|
|
36
|
+
h(TextInput, {
|
|
37
|
+
value: value,
|
|
38
|
+
onChange: onChange,
|
|
39
|
+
onSubmit: onSubmit,
|
|
40
|
+
placeholder: placeholder,
|
|
41
|
+
focus: inputActive,
|
|
42
|
+
showCursor: true,
|
|
43
|
+
})
|
|
98
44
|
),
|
|
99
|
-
h(Text, { color:
|
|
45
|
+
h(Text, { color: '#444466' }, ' '),
|
|
46
|
+
h(Text, { color: modelStatus ? hex.greenOnline : '#444466', bold: modelStatus }, modelLabel || '')
|
|
100
47
|
),
|
|
101
|
-
h(Box, {
|
|
102
|
-
h(Text, { color:
|
|
48
|
+
h(Box, { height: 1, paddingLeft: 1, paddingRight: 1 },
|
|
49
|
+
h(Text, { color: '#333344' }, '/help /clear /model /exit'),
|
|
50
|
+
h(Box, { flexGrow: 1 }),
|
|
51
|
+
h(Text, { color: '#333344' }, isStreaming ? 'Ctrl+C to stop' : '')
|
|
103
52
|
)
|
|
104
53
|
);
|
|
105
54
|
}
|
package/src/config/models.js
CHANGED
|
@@ -35,16 +35,22 @@ function checkModels(arr) {
|
|
|
35
35
|
|
|
36
36
|
const MODELS = Object.freeze([
|
|
37
37
|
{
|
|
38
|
-
id: 'openaxis/openaxis-
|
|
38
|
+
id: 'openaxis/openaxis-llama',
|
|
39
39
|
provider: 'openaxis',
|
|
40
|
-
label: 'OpenAxies
|
|
40
|
+
label: 'OpenAxies Llama',
|
|
41
41
|
badge: 'Fast',
|
|
42
42
|
},
|
|
43
43
|
{
|
|
44
|
-
id: 'openaxis/openaxis-
|
|
44
|
+
id: 'openaxis/openaxis-gpt',
|
|
45
45
|
provider: 'openaxis',
|
|
46
|
-
label: 'OpenAxies
|
|
47
|
-
badge: '
|
|
46
|
+
label: 'OpenAxies GPT',
|
|
47
|
+
badge: 'Balanced',
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: 'openaxis/openaxis-deepseek',
|
|
51
|
+
provider: 'openaxis',
|
|
52
|
+
label: 'OpenAxies DeepSeek',
|
|
53
|
+
badge: 'Reasoning',
|
|
48
54
|
},
|
|
49
55
|
]);
|
|
50
56
|
|
package/src/providers/index.js
CHANGED
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
import { streamResponse } from './streaming.js';
|
|
2
2
|
import { runWebSearchGraph, shouldUseWebSearch } from './websearch.js';
|
|
3
3
|
|
|
4
|
-
// Primary + two warm fallbacks per model.
|
|
5
|
-
// HF Spaces cold-start on first request (30–120 s model download).
|
|
6
|
-
// If the primary is waking up, we fall through to the next Space.
|
|
7
4
|
const MODEL_ROUTES = Object.freeze({
|
|
8
|
-
'openaxis/openaxis-
|
|
5
|
+
'openaxis/openaxis-llama': Object.freeze([
|
|
9
6
|
'https://universal-618-clarity-main.hf.space/v1/chat/completions',
|
|
10
|
-
'https://universal-618-clarity-2.hf.space/v1/chat/completions',
|
|
11
|
-
'https://universal-618-clarity-3.hf.space/v1/chat/completions',
|
|
12
|
-
]),
|
|
13
|
-
'openaxis/openaxis-heavy': Object.freeze([
|
|
14
7
|
'https://universal-618-clarity-4.hf.space/v1/chat/completions',
|
|
8
|
+
]),
|
|
9
|
+
'openaxis/openaxis-gpt': Object.freeze([
|
|
10
|
+
'https://universal-618-clarity-2.hf.space/v1/chat/completions',
|
|
15
11
|
'https://universal-618-clarity-5.hf.space/v1/chat/completions',
|
|
12
|
+
]),
|
|
13
|
+
'openaxis/openaxis-deepseek': Object.freeze([
|
|
14
|
+
'https://universal-618-clarity-3.hf.space/v1/chat/completions',
|
|
16
15
|
'https://universal-618-clarity-6.hf.space/v1/chat/completions',
|
|
17
16
|
]),
|
|
18
17
|
});
|
|
@@ -53,11 +52,17 @@ export async function* callModel(modelConfig, messages, signal) {
|
|
|
53
52
|
|
|
54
53
|
const modelShort = modelConfig.id.replace(/^[^/]+\//, '');
|
|
55
54
|
let modelMessages = messages;
|
|
55
|
+
let webUsed = false;
|
|
56
|
+
let webSites = 0;
|
|
57
|
+
let webQuery = '';
|
|
56
58
|
|
|
57
59
|
if (shouldUseWebSearch(messages) === true) {
|
|
58
60
|
try {
|
|
59
61
|
const webResult = await runWebSearchGraph(messages, signal);
|
|
60
62
|
if (webResult.used === true && webResult.sites > 0) {
|
|
63
|
+
webUsed = true;
|
|
64
|
+
webSites = webResult.sites;
|
|
65
|
+
webQuery = webResult.query || '';
|
|
61
66
|
modelMessages = [{
|
|
62
67
|
role: 'system',
|
|
63
68
|
content:
|
|
@@ -70,6 +75,8 @@ export async function* callModel(modelConfig, messages, signal) {
|
|
|
70
75
|
}
|
|
71
76
|
}
|
|
72
77
|
|
|
78
|
+
yield { type: 'tool', tool: 'websearch', used: webUsed, sites: webSites, query: webQuery };
|
|
79
|
+
|
|
73
80
|
const body = {
|
|
74
81
|
model: modelShort,
|
|
75
82
|
messages: modelMessages,
|
|
@@ -79,7 +86,6 @@ export async function* callModel(modelConfig, messages, signal) {
|
|
|
79
86
|
|
|
80
87
|
let lastError = null;
|
|
81
88
|
|
|
82
|
-
// Try each endpoint in order — fall through if one throws or returns an error.
|
|
83
89
|
for (let i = 0; i < endpoints.length; i++) {
|
|
84
90
|
const endpoint = endpoints[i];
|
|
85
91
|
try {
|
|
@@ -87,16 +93,14 @@ export async function* callModel(modelConfig, messages, signal) {
|
|
|
87
93
|
for await (const event of stream) {
|
|
88
94
|
yield event;
|
|
89
95
|
}
|
|
90
|
-
return;
|
|
96
|
+
return;
|
|
91
97
|
} catch (err) {
|
|
92
98
|
lastError = err;
|
|
93
|
-
// Only retry on genuine failures, not on user-initiated abort
|
|
94
99
|
if (signal !== null && signal !== undefined && signal.aborted === true) {
|
|
95
100
|
return;
|
|
96
101
|
}
|
|
97
102
|
}
|
|
98
103
|
}
|
|
99
104
|
|
|
100
|
-
// All endpoints failed
|
|
101
105
|
throw lastError !== null ? lastError : new Error('All endpoints failed for ' + modelConfig.id);
|
|
102
106
|
}
|
|
@@ -1,20 +1,12 @@
|
|
|
1
|
-
const CONNECT_TIMEOUT =
|
|
2
|
-
const READ_TIMEOUT =
|
|
3
|
-
|
|
4
|
-
function readWithTimeout(reader, timeoutMs) {
|
|
5
|
-
return Promise.race([
|
|
6
|
-
reader.read(),
|
|
7
|
-
new Promise(function (_, reject) {
|
|
8
|
-
setTimeout(function () {
|
|
9
|
-
reject(new Error('stream_read_timeout'));
|
|
10
|
-
}, timeoutMs);
|
|
11
|
-
}),
|
|
12
|
-
]);
|
|
13
|
-
}
|
|
1
|
+
const CONNECT_TIMEOUT = 120000;
|
|
2
|
+
const READ_TIMEOUT = 60000;
|
|
14
3
|
|
|
15
|
-
function checkEndpoint(
|
|
16
|
-
if (typeof
|
|
17
|
-
throw new Error('endpoint must be a
|
|
4
|
+
function checkEndpoint(endpoint) {
|
|
5
|
+
if (typeof endpoint !== 'string') {
|
|
6
|
+
throw new Error('endpoint must be a string');
|
|
7
|
+
}
|
|
8
|
+
if (endpoint.length === 0) {
|
|
9
|
+
throw new Error('endpoint must not be empty');
|
|
18
10
|
}
|
|
19
11
|
}
|
|
20
12
|
|
|
@@ -24,120 +16,124 @@ function checkBody(body) {
|
|
|
24
16
|
}
|
|
25
17
|
}
|
|
26
18
|
|
|
19
|
+
function checkSignal(signal) {
|
|
20
|
+
if (signal !== null && signal !== undefined && !(signal instanceof AbortSignal)) {
|
|
21
|
+
throw new Error('signal must be an AbortSignal or null');
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
27
25
|
export async function* streamResponse(endpoint, body, signal) {
|
|
28
26
|
checkEndpoint(endpoint);
|
|
29
27
|
checkBody(body);
|
|
28
|
+
checkSignal(signal);
|
|
30
29
|
|
|
31
30
|
const controller = new AbortController();
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
controller.abort();
|
|
36
|
-
} catch (_) {
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
if (signal !== null && signal !== undefined) {
|
|
41
|
-
signal.addEventListener('abort', onAbort, { once: true });
|
|
42
|
-
}
|
|
31
|
+
const combinedSignal = signal !== null && signal !== undefined
|
|
32
|
+
? AbortSignal.any ? AbortSignal.any([signal, controller.signal]) : controller.signal
|
|
33
|
+
: controller.signal;
|
|
43
34
|
|
|
44
35
|
const connectTimeout = setTimeout(function () {
|
|
45
|
-
|
|
46
|
-
controller.abort();
|
|
47
|
-
} catch (_) {
|
|
48
|
-
}
|
|
36
|
+
controller.abort(new Error('Connect timeout: ' + CONNECT_TIMEOUT + 'ms'));
|
|
49
37
|
}, CONNECT_TIMEOUT);
|
|
50
38
|
|
|
39
|
+
let response;
|
|
40
|
+
|
|
51
41
|
try {
|
|
52
|
-
|
|
42
|
+
response = await fetch(endpoint, {
|
|
53
43
|
method: 'POST',
|
|
54
|
-
headers: {
|
|
55
|
-
|
|
56
|
-
|
|
44
|
+
headers: {
|
|
45
|
+
'Content-Type': 'application/json',
|
|
46
|
+
'Accept': 'text/event-stream',
|
|
47
|
+
},
|
|
48
|
+
body: JSON.stringify(body),
|
|
49
|
+
signal: combinedSignal,
|
|
57
50
|
});
|
|
58
|
-
|
|
51
|
+
} catch (err) {
|
|
59
52
|
clearTimeout(connectTimeout);
|
|
53
|
+
throw err;
|
|
54
|
+
}
|
|
60
55
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
56
|
+
clearTimeout(connectTimeout);
|
|
57
|
+
|
|
58
|
+
if (response.ok === false) {
|
|
59
|
+
const text = await response.text().catch(function () { return ''; });
|
|
60
|
+
throw new Error('HTTP ' + response.status + ': ' + text.slice(0, 200));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (response.body === null || response.body === undefined) {
|
|
64
|
+
throw new Error('Response body is null');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const reader = response.body.getReader();
|
|
68
|
+
const decoder = new TextDecoder();
|
|
69
|
+
let buffer = '';
|
|
70
|
+
let readTimeout = null;
|
|
71
|
+
|
|
72
|
+
function resetReadTimeout() {
|
|
73
|
+
if (readTimeout !== null) {
|
|
74
|
+
clearTimeout(readTimeout);
|
|
69
75
|
}
|
|
76
|
+
readTimeout = setTimeout(function () {
|
|
77
|
+
controller.abort(new Error('Read timeout: ' + READ_TIMEOUT + 'ms'));
|
|
78
|
+
}, READ_TIMEOUT);
|
|
79
|
+
}
|
|
70
80
|
|
|
71
|
-
|
|
72
|
-
const decoder = new TextDecoder();
|
|
73
|
-
let buffer = '';
|
|
81
|
+
resetReadTimeout();
|
|
74
82
|
|
|
83
|
+
try {
|
|
75
84
|
while (true) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
result = await readWithTimeout(reader, READ_TIMEOUT);
|
|
79
|
-
} catch (err) {
|
|
80
|
-
if (err.message === 'stream_read_timeout') {
|
|
81
|
-
yield { type: 'timeout' };
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
throw err;
|
|
85
|
-
}
|
|
85
|
+
const result = await reader.read();
|
|
86
|
+
resetReadTimeout();
|
|
86
87
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
if (done === true) {
|
|
91
|
-
break;
|
|
88
|
+
if (result.done === true) {
|
|
89
|
+
yield { type: 'done' };
|
|
90
|
+
return;
|
|
92
91
|
}
|
|
93
92
|
|
|
94
|
-
|
|
93
|
+
const chunk = decoder.decode(result.value, { stream: true });
|
|
94
|
+
buffer = buffer + chunk;
|
|
95
95
|
const lines = buffer.split('\n');
|
|
96
96
|
buffer = lines.pop() || '';
|
|
97
97
|
|
|
98
98
|
for (let i = 0; i < lines.length; i++) {
|
|
99
99
|
const line = lines[i];
|
|
100
|
-
if (line.startsWith('data: ') ===
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
yield { type: 'done' };
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
try {
|
|
109
|
-
const json = JSON.parse(data);
|
|
110
|
-
const choices = json.choices;
|
|
111
|
-
if (choices === null || choices === undefined || choices.length < 1) {
|
|
112
|
-
continue;
|
|
113
|
-
}
|
|
114
|
-
const choice = choices[0];
|
|
115
|
-
const delta = choice.delta;
|
|
116
|
-
if (delta === null || delta === undefined) {
|
|
117
|
-
continue;
|
|
118
|
-
}
|
|
119
|
-
if (delta.content !== null && delta.content !== undefined && delta.content !== '') {
|
|
120
|
-
yield { type: 'token', content: delta.content };
|
|
100
|
+
if (line.startsWith('data: ') === true) {
|
|
101
|
+
const data = line.slice(6).trim();
|
|
102
|
+
if (data === '[DONE]') {
|
|
103
|
+
yield { type: 'done' };
|
|
104
|
+
return;
|
|
121
105
|
}
|
|
122
|
-
|
|
123
|
-
|
|
106
|
+
try {
|
|
107
|
+
const parsed = JSON.parse(data);
|
|
108
|
+
const choice = parsed.choices && parsed.choices[0];
|
|
109
|
+
if (choice !== null && choice !== undefined) {
|
|
110
|
+
const delta = choice.delta;
|
|
111
|
+
if (delta !== null && delta !== undefined) {
|
|
112
|
+
if (typeof delta.content === 'string') {
|
|
113
|
+
yield { type: 'token', content: delta.content };
|
|
114
|
+
}
|
|
115
|
+
if (typeof delta.reasoning_content === 'string') {
|
|
116
|
+
yield { type: 'token', content: delta.reasoning_content };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
} catch (_) {
|
|
124
121
|
}
|
|
125
|
-
} catch (_) {
|
|
126
122
|
}
|
|
127
123
|
}
|
|
128
124
|
}
|
|
129
|
-
|
|
130
|
-
|
|
125
|
+
} catch (err) {
|
|
126
|
+
if (err !== null && err !== undefined && err.name === 'AbortError') {
|
|
127
|
+
yield { type: 'timeout' };
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
throw err;
|
|
131
131
|
} finally {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
try {
|
|
135
|
-
signal.removeEventListener('abort', onAbort);
|
|
136
|
-
} catch (_) {
|
|
137
|
-
}
|
|
132
|
+
if (readTimeout !== null) {
|
|
133
|
+
clearTimeout(readTimeout);
|
|
138
134
|
}
|
|
139
135
|
try {
|
|
140
|
-
|
|
136
|
+
await reader.cancel();
|
|
141
137
|
} catch (_) {
|
|
142
138
|
}
|
|
143
139
|
}
|
|
@@ -1,60 +0,0 @@
|
|
|
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
|
-
}
|