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
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { hex } from '../config/theme.js';
|
|
4
|
+
import { getModels } from '../config/models.js';
|
|
5
|
+
|
|
6
|
+
const h = React.createElement;
|
|
7
|
+
|
|
8
|
+
const BAR_COLOR = '#1A1A2E';
|
|
9
|
+
|
|
10
|
+
export function createModelBar(activeId, cols) {
|
|
11
|
+
const models = getModels();
|
|
12
|
+
const safeCols = typeof cols === 'number' && cols > 0 ? cols : 80;
|
|
13
|
+
const parts = [];
|
|
14
|
+
|
|
15
|
+
for (let i = 0; i < models.length; i++) {
|
|
16
|
+
const m = models[i];
|
|
17
|
+
const isActive = m.id === activeId;
|
|
18
|
+
if (i > 0) {
|
|
19
|
+
parts.push(h(Text, { key: 'sp-' + i, color: BAR_COLOR }, ' \u2502 '));
|
|
20
|
+
}
|
|
21
|
+
const prefix = isActive ? '\u25B8 ' : ' ';
|
|
22
|
+
const dot = isActive ? '\u25CF' : '\u25CB';
|
|
23
|
+
const dotColor = isActive ? hex.greenOnline : '#333355';
|
|
24
|
+
const nameColor = isActive ? hex.neonBlue : '#555577';
|
|
25
|
+
const badgeColor = isActive ? hex.muted : '#333344';
|
|
26
|
+
|
|
27
|
+
parts.push(
|
|
28
|
+
h(Text, { key: 'pre-' + i, color: hex.primary }, prefix)
|
|
29
|
+
);
|
|
30
|
+
parts.push(
|
|
31
|
+
h(Text, { key: 'nm-' + i, color: nameColor, bold: isActive }, m.label)
|
|
32
|
+
);
|
|
33
|
+
parts.push(
|
|
34
|
+
h(Text, { key: 'bd-' + i, color: badgeColor }, ' [' + m.badge + ']')
|
|
35
|
+
);
|
|
36
|
+
parts.push(
|
|
37
|
+
h(Text, { key: 'dt-' + i, color: dotColor }, ' ' + dot)
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return h(Box, { width: '100%', height: 1, paddingLeft: 1 }, ...parts);
|
|
42
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Text } from 'ink';
|
|
3
|
+
import { hex } from '../config/theme.js';
|
|
4
|
+
|
|
5
|
+
const h = React.createElement;
|
|
6
|
+
|
|
7
|
+
const CHAR_SET = [
|
|
8
|
+
'\u2726', '\u00B7', '\u2727', '\u00B7', '\u2605', '\u00B7',
|
|
9
|
+
'\u2726', '\u00B7', '\u2219', '\u2727', '\u00B7', '\u2726',
|
|
10
|
+
'\u2219', '\u2605', '\u2219', '\u2727', '\u2219', '\u2726',
|
|
11
|
+
'\u00B7', '\u2727', '\u2605', '\u00B7', '\u2726', '\u00B7',
|
|
12
|
+
'\u2219', '\u2727',
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
const COLORS = [
|
|
16
|
+
'#00F0FF', '#FF9F43', '#00FF88', '#FF6B9D', '#8B5CF6',
|
|
17
|
+
'#00F0FF', '#FFD700', '#00FF88',
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
function getColor(frame, index, total) {
|
|
21
|
+
const phase = Math.floor((index / total) * 52);
|
|
22
|
+
const idx = (frame + phase) % COLORS.length;
|
|
23
|
+
return COLORS[idx];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function createParticleRow(frame, cols, key) {
|
|
27
|
+
const safeCols = typeof cols === 'number' && cols > 0 ? cols : 80;
|
|
28
|
+
const safeFrame = typeof frame === 'number' ? frame : 0;
|
|
29
|
+
const parts = [];
|
|
30
|
+
|
|
31
|
+
for (let i = 0; i < 26; i++) {
|
|
32
|
+
const color = getColor(safeFrame, i, 52);
|
|
33
|
+
const ch = CHAR_SET[i % CHAR_SET.length];
|
|
34
|
+
parts.push(
|
|
35
|
+
h(Text, { key: key + '-' + i, color: color }, ch)
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return h(Text, null, parts);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function createParticlePair(frame, cols) {
|
|
43
|
+
return [
|
|
44
|
+
createParticleRow(frame, cols, 'top'),
|
|
45
|
+
createParticleRow(frame, cols, 'bot'),
|
|
46
|
+
];
|
|
47
|
+
}
|
package/src/config/commands.js
CHANGED
|
@@ -34,7 +34,7 @@ function checkCommands(arr) {
|
|
|
34
34
|
const COMMAND_DEFS = Object.freeze([
|
|
35
35
|
{ trigger: '/help', description: 'Show available commands' },
|
|
36
36
|
{ trigger: '/clear', description: 'Clear the conversation' },
|
|
37
|
-
{ trigger: '/model', description: 'Switch between
|
|
37
|
+
{ trigger: '/model', description: 'Switch between Flash and Heavy' },
|
|
38
38
|
{ trigger: '/exit', description: 'Quit the application' },
|
|
39
39
|
]);
|
|
40
40
|
|
package/src/config/models.js
CHANGED
|
@@ -33,19 +33,24 @@ function checkModels(arr) {
|
|
|
33
33
|
return arr;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
// Mixtral (fast/light) and Heavy (quality).
|
|
37
36
|
const MODELS = Object.freeze([
|
|
38
37
|
{
|
|
39
|
-
id: 'openaxis/openaxis-
|
|
38
|
+
id: 'openaxis/openaxis-llama',
|
|
40
39
|
provider: 'openaxis',
|
|
41
|
-
label: 'OpenAxies
|
|
40
|
+
label: 'OpenAxies Llama',
|
|
42
41
|
badge: 'Fast',
|
|
43
42
|
},
|
|
44
43
|
{
|
|
45
|
-
id: 'openaxis/openaxis-
|
|
44
|
+
id: 'openaxis/openaxis-gpt',
|
|
46
45
|
provider: 'openaxis',
|
|
47
|
-
label: 'OpenAxies
|
|
48
|
-
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',
|
|
49
54
|
},
|
|
50
55
|
]);
|
|
51
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
|
-
}
|
|
@@ -1,133 +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
|
-
function checkArray(arr) {
|
|
8
|
-
if (Array.isArray(arr) === false) {
|
|
9
|
-
return [];
|
|
10
|
-
}
|
|
11
|
-
return arr;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function clampWidth(width) {
|
|
15
|
-
if (typeof width !== 'number' || width < 20) {
|
|
16
|
-
return 80;
|
|
17
|
-
}
|
|
18
|
-
return Math.max(20, width - 2);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function pushLine(lines, text, color, bold) {
|
|
22
|
-
lines.push({
|
|
23
|
-
text: typeof text === 'string' ? text : '',
|
|
24
|
-
color: color,
|
|
25
|
-
bold: bold === true,
|
|
26
|
-
});
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function wrapText(text, width) {
|
|
30
|
-
const safeWidth = Math.max(1, width);
|
|
31
|
-
const raw = typeof text === 'string' ? text : '';
|
|
32
|
-
const sourceLines = raw.split('\n');
|
|
33
|
-
const wrapped = [];
|
|
34
|
-
|
|
35
|
-
for (let i = 0; i < sourceLines.length; i++) {
|
|
36
|
-
let line = sourceLines[i];
|
|
37
|
-
if (line.length === 0) {
|
|
38
|
-
wrapped.push('');
|
|
39
|
-
continue;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
while (line.length > safeWidth) {
|
|
43
|
-
let cut = line.lastIndexOf(' ', safeWidth);
|
|
44
|
-
if (cut < Math.floor(safeWidth / 2)) {
|
|
45
|
-
cut = safeWidth;
|
|
46
|
-
}
|
|
47
|
-
wrapped.push(line.slice(0, cut));
|
|
48
|
-
line = line.slice(cut).trimStart();
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
wrapped.push(line);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return wrapped;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function pushWrapped(lines, text, width, color, bold) {
|
|
58
|
-
const wrapped = wrapText(text, width);
|
|
59
|
-
for (let i = 0; i < wrapped.length; i++) {
|
|
60
|
-
pushLine(lines, wrapped[i], color, bold);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function pushRawMessage(lines, msg, safeCols) {
|
|
65
|
-
if (msg === null || msg === undefined || typeof msg !== 'object') {
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const content = typeof msg.content === 'string' ? msg.content : '';
|
|
70
|
-
if (content.length === 0) {
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if (msg.role === 'user') {
|
|
75
|
-
pushWrapped(lines, content, safeCols, hex.neonBlue, false);
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
pushWrapped(lines, content, safeCols, hex.primary, false);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export function createChatViewport(messages, streamText, availLines, cols, scrollOffset) {
|
|
83
|
-
const safeMessages = checkArray(messages);
|
|
84
|
-
const safeAvail = typeof availLines === 'number' && availLines > 0 ? availLines : 1;
|
|
85
|
-
const safeCols = clampWidth(cols);
|
|
86
|
-
const rawOffset = typeof scrollOffset === 'number' && scrollOffset > 0
|
|
87
|
-
? Math.floor(scrollOffset)
|
|
88
|
-
: 0;
|
|
89
|
-
const hasStream = typeof streamText === 'string' && streamText.length > 0;
|
|
90
|
-
const transcriptLines = [];
|
|
91
|
-
|
|
92
|
-
for (let i = 0; i < safeMessages.length; i++) {
|
|
93
|
-
pushRawMessage(transcriptLines, safeMessages[i], safeCols);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
if (hasStream === true) {
|
|
97
|
-
pushWrapped(transcriptLines, streamText, safeCols, hex.primary, false);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (transcriptLines.length === 0) {
|
|
101
|
-
pushLine(transcriptLines, ' awaiting input...', '#333355', false);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const visibleHeight = Math.max(1, safeAvail - 1);
|
|
105
|
-
const maxOffset = Math.max(0, transcriptLines.length - visibleHeight);
|
|
106
|
-
const safeOffset = Math.min(rawOffset, maxOffset);
|
|
107
|
-
const endIndex = transcriptLines.length - safeOffset;
|
|
108
|
-
const startIndex = Math.max(0, endIndex - visibleHeight);
|
|
109
|
-
const visibleLines = transcriptLines.slice(startIndex, endIndex);
|
|
110
|
-
const elements = [];
|
|
111
|
-
|
|
112
|
-
for (let i = 0; i < visibleLines.length; i++) {
|
|
113
|
-
const line = visibleLines[i];
|
|
114
|
-
elements.push(
|
|
115
|
-
h(Text, {
|
|
116
|
-
key: 'line-' + (startIndex + i),
|
|
117
|
-
color: line.color,
|
|
118
|
-
bold: line.bold,
|
|
119
|
-
}, line.text)
|
|
120
|
-
);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
return h(Box, {
|
|
124
|
-
flexGrow: 1,
|
|
125
|
-
height: safeAvail,
|
|
126
|
-
flexDirection: 'column',
|
|
127
|
-
overflow: 'hidden',
|
|
128
|
-
paddingLeft: 1,
|
|
129
|
-
paddingRight: 1,
|
|
130
|
-
paddingTop: 0,
|
|
131
|
-
paddingBottom: 1,
|
|
132
|
-
}, ...elements);
|
|
133
|
-
}
|