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
|
@@ -0,0 +1,194 @@
|
|
|
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
|
+
// Splits content on <think>...</think> tags.
|
|
15
|
+
// Returns an array of { type: 'text' | 'think', content: string } parts.
|
|
16
|
+
function parseThinkTags(content) {
|
|
17
|
+
if (typeof content !== 'string') {
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
const parts = [];
|
|
21
|
+
let remaining = content;
|
|
22
|
+
let inThink = false;
|
|
23
|
+
|
|
24
|
+
while (remaining.length > 0) {
|
|
25
|
+
if (inThink === false) {
|
|
26
|
+
const startIdx = remaining.indexOf('<think>');
|
|
27
|
+
if (startIdx === -1) {
|
|
28
|
+
if (remaining.length > 0) {
|
|
29
|
+
parts.push({ type: 'text', content: remaining });
|
|
30
|
+
}
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
if (startIdx > 0) {
|
|
34
|
+
parts.push({ type: 'text', content: remaining.slice(0, startIdx) });
|
|
35
|
+
}
|
|
36
|
+
remaining = remaining.slice(startIdx + 7);
|
|
37
|
+
inThink = true;
|
|
38
|
+
} else {
|
|
39
|
+
const endIdx = remaining.indexOf('</think>');
|
|
40
|
+
if (endIdx === -1) {
|
|
41
|
+
if (remaining.length > 0) {
|
|
42
|
+
parts.push({ type: 'think', content: remaining });
|
|
43
|
+
}
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
if (endIdx > 0) {
|
|
47
|
+
parts.push({ type: 'think', content: remaining.slice(0, endIdx) });
|
|
48
|
+
}
|
|
49
|
+
remaining = remaining.slice(endIdx + 8);
|
|
50
|
+
inThink = false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return parts;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function clampWidth(width) {
|
|
58
|
+
if (typeof width !== 'number' || width < 20) {
|
|
59
|
+
return 80;
|
|
60
|
+
}
|
|
61
|
+
return Math.max(20, width - 2);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function pushLine(lines, text, color, bold) {
|
|
65
|
+
lines.push({
|
|
66
|
+
text: typeof text === 'string' ? text : '',
|
|
67
|
+
color: color,
|
|
68
|
+
bold: bold === true,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function wrapText(text, width) {
|
|
73
|
+
const safeWidth = Math.max(1, width);
|
|
74
|
+
const raw = typeof text === 'string' ? text : '';
|
|
75
|
+
const sourceLines = raw.split('\n');
|
|
76
|
+
const wrapped = [];
|
|
77
|
+
|
|
78
|
+
for (let i = 0; i < sourceLines.length; i++) {
|
|
79
|
+
let line = sourceLines[i];
|
|
80
|
+
if (line.length === 0) {
|
|
81
|
+
wrapped.push('');
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
while (line.length > safeWidth) {
|
|
86
|
+
let cut = line.lastIndexOf(' ', safeWidth);
|
|
87
|
+
if (cut < Math.floor(safeWidth / 2)) {
|
|
88
|
+
cut = safeWidth;
|
|
89
|
+
}
|
|
90
|
+
wrapped.push(line.slice(0, cut));
|
|
91
|
+
line = line.slice(cut).trimStart();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
wrapped.push(line);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return wrapped;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function pushWrapped(lines, text, width, color, bold) {
|
|
101
|
+
const wrapped = wrapText(text, width);
|
|
102
|
+
for (let i = 0; i < wrapped.length; i++) {
|
|
103
|
+
pushLine(lines, wrapped[i], color, bold);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function pushContentLines(lines, content, width) {
|
|
108
|
+
const parts = parseThinkTags(content);
|
|
109
|
+
for (let i = 0; i < parts.length; i++) {
|
|
110
|
+
const part = parts[i];
|
|
111
|
+
if (part.type === 'think') {
|
|
112
|
+
pushWrapped(lines, '# think ' + part.content.trim(), width, '#555577', false);
|
|
113
|
+
} else {
|
|
114
|
+
pushWrapped(lines, part.content, width, hex.primary, false);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Transcript message - linear terminal flow, no side panels or bubbles.
|
|
120
|
+
function pushMessageLines(lines, msg, safeCols) {
|
|
121
|
+
if (msg === null || msg === undefined || typeof msg !== 'object') {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const content = typeof msg.content === 'string' ? msg.content : '';
|
|
126
|
+
|
|
127
|
+
if (msg.role === 'thinking') {
|
|
128
|
+
pushWrapped(lines, content, safeCols, hex.orange, false);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (msg.role === 'user') {
|
|
133
|
+
pushWrapped(lines, 'You > ' + content, safeCols, hex.neonBlue, true);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
pushLine(lines, 'OpenAxies $', hex.greenOnline, true);
|
|
138
|
+
pushContentLines(lines, content, safeCols);
|
|
139
|
+
pushLine(lines, '', hex.primary, false);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function createChatViewport(messages, streamText, availLines, cols, scrollOffset) {
|
|
143
|
+
const safeMessages = checkArray(messages);
|
|
144
|
+
const safeAvail = typeof availLines === 'number' && availLines > 0 ? availLines : 1;
|
|
145
|
+
const safeCols = clampWidth(cols);
|
|
146
|
+
const rawOffset = typeof scrollOffset === 'number' && scrollOffset > 0
|
|
147
|
+
? Math.floor(scrollOffset)
|
|
148
|
+
: 0;
|
|
149
|
+
const hasStream = typeof streamText === 'string' && streamText.length > 0;
|
|
150
|
+
const transcriptLines = [];
|
|
151
|
+
|
|
152
|
+
for (let i = 0; i < safeMessages.length; i++) {
|
|
153
|
+
pushMessageLines(transcriptLines, safeMessages[i], safeCols);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (hasStream === true) {
|
|
157
|
+
pushLine(transcriptLines, 'OpenAxies $ streaming...', hex.greenOnline, true);
|
|
158
|
+
pushContentLines(transcriptLines, streamText, safeCols);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (transcriptLines.length === 0) {
|
|
162
|
+
pushLine(transcriptLines, '# waiting for input', '#333355', false);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const visibleHeight = Math.max(1, safeAvail - 1);
|
|
166
|
+
const maxOffset = Math.max(0, transcriptLines.length - visibleHeight);
|
|
167
|
+
const safeOffset = Math.min(rawOffset, maxOffset);
|
|
168
|
+
const endIndex = transcriptLines.length - safeOffset;
|
|
169
|
+
const startIndex = Math.max(0, endIndex - visibleHeight);
|
|
170
|
+
const visibleLines = transcriptLines.slice(startIndex, endIndex);
|
|
171
|
+
const elements = [];
|
|
172
|
+
|
|
173
|
+
for (let i = 0; i < visibleLines.length; i++) {
|
|
174
|
+
const line = visibleLines[i];
|
|
175
|
+
elements.push(
|
|
176
|
+
h(Text, {
|
|
177
|
+
key: 'line-' + (startIndex + i),
|
|
178
|
+
color: line.color,
|
|
179
|
+
bold: line.bold,
|
|
180
|
+
}, line.text)
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return h(Box, {
|
|
185
|
+
flexGrow: 1,
|
|
186
|
+
height: safeAvail,
|
|
187
|
+
flexDirection: 'column',
|
|
188
|
+
overflow: 'hidden',
|
|
189
|
+
paddingLeft: 1,
|
|
190
|
+
paddingRight: 1,
|
|
191
|
+
paddingTop: 0,
|
|
192
|
+
paddingBottom: 1,
|
|
193
|
+
}, ...elements);
|
|
194
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import TextInput from 'ink-text-input';
|
|
4
|
+
import { hex } from '../config/theme.js';
|
|
5
|
+
|
|
6
|
+
const h = React.createElement;
|
|
7
|
+
|
|
8
|
+
export const DOCK_HEIGHT = 3;
|
|
9
|
+
const MAX_FRAME_WIDTH = 80;
|
|
10
|
+
const PROMPT = 'openaxies@root:~$ ';
|
|
11
|
+
|
|
12
|
+
function checkString(value) {
|
|
13
|
+
if (typeof value !== 'string') {
|
|
14
|
+
return '';
|
|
15
|
+
}
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
|
|
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
|
+
export function createComposerDock(config) {
|
|
39
|
+
if (config === null || config === undefined || typeof config !== 'object') {
|
|
40
|
+
throw new Error('createComposerDock requires a config object');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const value = checkString(config.value);
|
|
44
|
+
const onChange = config.onChange;
|
|
45
|
+
const onSubmit = config.onSubmit;
|
|
46
|
+
const inputActive = config.inputActive !== false;
|
|
47
|
+
const isStreaming = config.isStreaming === true;
|
|
48
|
+
const terminalWidth = Number.isFinite(config.terminalWidth)
|
|
49
|
+
? Math.floor(config.terminalWidth)
|
|
50
|
+
: MAX_FRAME_WIDTH;
|
|
51
|
+
|
|
52
|
+
checkCallback(onChange, 'onChange');
|
|
53
|
+
checkCallback(onSubmit, 'onSubmit');
|
|
54
|
+
|
|
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);
|
|
62
|
+
|
|
63
|
+
return h(Box, {
|
|
64
|
+
width: '100%',
|
|
65
|
+
height: DOCK_HEIGHT,
|
|
66
|
+
flexDirection: 'column',
|
|
67
|
+
alignItems: 'center',
|
|
68
|
+
flexShrink: 0,
|
|
69
|
+
},
|
|
70
|
+
h(Box, { width: frameWidth, height: 1, flexShrink: 0 },
|
|
71
|
+
h(Text, { color: hex.border }, '\u250C' + horizontalRule + '\u2510')
|
|
72
|
+
),
|
|
73
|
+
h(Box, {
|
|
74
|
+
width: frameWidth,
|
|
75
|
+
height: 1,
|
|
76
|
+
flexDirection: 'row',
|
|
77
|
+
flexShrink: 0,
|
|
78
|
+
overflow: 'hidden',
|
|
79
|
+
},
|
|
80
|
+
h(Text, { color: hex.border }, '\u2502'),
|
|
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
|
+
)
|
|
98
|
+
),
|
|
99
|
+
h(Text, { color: hex.border }, '\u2502')
|
|
100
|
+
),
|
|
101
|
+
h(Box, { width: frameWidth, height: 1, flexShrink: 0 },
|
|
102
|
+
h(Text, { color: hex.border }, '\u2514' + horizontalRule + '\u2518')
|
|
103
|
+
)
|
|
104
|
+
);
|
|
105
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
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 ROUTER_HEIGHT = 2;
|
|
8
|
+
|
|
9
|
+
function checkActiveModel(modelId) {
|
|
10
|
+
if (typeof modelId !== 'string') {
|
|
11
|
+
return 'openaxis/openaxis-flash';
|
|
12
|
+
}
|
|
13
|
+
return modelId;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function checkColumns(cols) {
|
|
17
|
+
if (typeof cols !== 'number' || cols < 1) {
|
|
18
|
+
return 80;
|
|
19
|
+
}
|
|
20
|
+
return cols;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function buildSeparator(cols) {
|
|
24
|
+
const safeCols = checkColumns(cols);
|
|
25
|
+
let line = '';
|
|
26
|
+
for (let i = 0; i < safeCols; i++) {
|
|
27
|
+
line = line + '\u2500';
|
|
28
|
+
}
|
|
29
|
+
return line;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// RouterBar — 2 rows:
|
|
33
|
+
// Row 1: model status with active/standby indicators
|
|
34
|
+
// Row 2: full-width separator line
|
|
35
|
+
export function createRouterBar(activeModel, cols) {
|
|
36
|
+
const modelStr = checkActiveModel(activeModel);
|
|
37
|
+
const isFlash = modelStr === 'openaxis/openaxis-flash';
|
|
38
|
+
const sepLine = buildSeparator(cols);
|
|
39
|
+
|
|
40
|
+
return h(Box, {
|
|
41
|
+
flexDirection: 'column',
|
|
42
|
+
width: '100%',
|
|
43
|
+
height: ROUTER_HEIGHT,
|
|
44
|
+
backgroundColor: hex.black,
|
|
45
|
+
paddingLeft: 1,
|
|
46
|
+
paddingRight: 1,
|
|
47
|
+
flexShrink: 0,
|
|
48
|
+
},
|
|
49
|
+
h(Box, {
|
|
50
|
+
height: 1,
|
|
51
|
+
flexDirection: 'row',
|
|
52
|
+
alignItems: 'center',
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
// ── Flash slot ──
|
|
56
|
+
h(Text, {
|
|
57
|
+
color: isFlash ? hex.neonBlue : '#333355',
|
|
58
|
+
bold: isFlash,
|
|
59
|
+
}, isFlash ? '\u25B6 ' : '\u25B7 '),
|
|
60
|
+
h(Text, {
|
|
61
|
+
color: isFlash ? hex.primary : '#444466',
|
|
62
|
+
bold: isFlash,
|
|
63
|
+
}, 'Flash'),
|
|
64
|
+
h(Text, {
|
|
65
|
+
color: isFlash ? hex.greenOnline : '#2A3A2A',
|
|
66
|
+
}, ' \u25CF '),
|
|
67
|
+
h(Text, {
|
|
68
|
+
color: isFlash ? hex.greenOnline : '#2A3A2A',
|
|
69
|
+
dimColor: !isFlash,
|
|
70
|
+
}, isFlash ? 'ACTIVE' : 'STANDBY'),
|
|
71
|
+
|
|
72
|
+
// ── Divider ──
|
|
73
|
+
h(Text, { color: '#1E1E2E' }, ' \u2502 '),
|
|
74
|
+
|
|
75
|
+
// ── Heavy slot ──
|
|
76
|
+
h(Text, {
|
|
77
|
+
color: !isFlash ? hex.neonBlue : '#333355',
|
|
78
|
+
bold: !isFlash,
|
|
79
|
+
}, !isFlash ? '\u25B6 ' : '\u25B7 '),
|
|
80
|
+
h(Text, {
|
|
81
|
+
color: !isFlash ? hex.primary : '#444466',
|
|
82
|
+
bold: !isFlash,
|
|
83
|
+
}, 'Heavy'),
|
|
84
|
+
h(Text, {
|
|
85
|
+
color: !isFlash ? hex.greenOnline : '#2A3A2A',
|
|
86
|
+
}, ' \u25CF '),
|
|
87
|
+
h(Text, {
|
|
88
|
+
color: !isFlash ? hex.greenOnline : '#2A3A2A',
|
|
89
|
+
dimColor: isFlash,
|
|
90
|
+
}, !isFlash ? 'ACTIVE' : 'STANDBY'),
|
|
91
|
+
|
|
92
|
+
// ── Right-side hint ──
|
|
93
|
+
h(Text, { color: '#1E1E2E' }, ' \u2502 '),
|
|
94
|
+
h(Text, { color: '#2A2A40' }, '/model to switch'),
|
|
95
|
+
),
|
|
96
|
+
h(Text, { color: '#1A1A28' }, sepLine)
|
|
97
|
+
);
|
|
98
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
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 SLASH_OVERLAY_HEIGHT = 6;
|
|
8
|
+
|
|
9
|
+
// Row budget (no border box — flat design matching OpenCode command palette):
|
|
10
|
+
// Row 1 → header + hint
|
|
11
|
+
// Row 2 → ─── separator line
|
|
12
|
+
// Rows 3–6 → up to 4 command rows
|
|
13
|
+
//
|
|
14
|
+
// The selected row renders with a full-width orange highlight bar.
|
|
15
|
+
// Navigation is driven by App.js useInput; this component is purely presentational.
|
|
16
|
+
|
|
17
|
+
function checkCommands(arr) {
|
|
18
|
+
if (Array.isArray(arr) === false) {
|
|
19
|
+
throw new Error('commands must be an array');
|
|
20
|
+
}
|
|
21
|
+
if (arr.length === 0) {
|
|
22
|
+
throw new Error('commands must not be empty');
|
|
23
|
+
}
|
|
24
|
+
for (let i = 0; i < arr.length; i++) {
|
|
25
|
+
const cmd = arr[i];
|
|
26
|
+
if (cmd === null || cmd === undefined || typeof cmd !== 'object') {
|
|
27
|
+
throw new Error('command at index ' + i + ' must be an object');
|
|
28
|
+
}
|
|
29
|
+
if (typeof cmd.trigger !== 'string' || cmd.trigger.length === 0) {
|
|
30
|
+
throw new Error('command at index ' + i + ' trigger must be non-empty');
|
|
31
|
+
}
|
|
32
|
+
if (typeof cmd.description !== 'string') {
|
|
33
|
+
throw new Error('command at index ' + i + ' description must be a string');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return arr;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function createSlashOverlay(commands, selectedIndex) {
|
|
40
|
+
const safeCommands = checkCommands(commands);
|
|
41
|
+
const safeIndex =
|
|
42
|
+
typeof selectedIndex === 'number' &&
|
|
43
|
+
selectedIndex >= 0 &&
|
|
44
|
+
selectedIndex < safeCommands.length
|
|
45
|
+
? selectedIndex
|
|
46
|
+
: 0;
|
|
47
|
+
|
|
48
|
+
// ── Header row ─────────────────────────────────────────────────────────────
|
|
49
|
+
const headerRow = h(Box, {
|
|
50
|
+
key: 'slash-header',
|
|
51
|
+
width: '100%',
|
|
52
|
+
height: 1,
|
|
53
|
+
flexDirection: 'row',
|
|
54
|
+
alignItems: 'center',
|
|
55
|
+
paddingLeft: 1,
|
|
56
|
+
backgroundColor: '#111118',
|
|
57
|
+
},
|
|
58
|
+
h(Text, { color: hex.neonBlue, bold: true }, '\u2713 '),
|
|
59
|
+
h(Text, { color: hex.primary, bold: true }, 'Commands'),
|
|
60
|
+
h(Text, { color: '#333355' }, ' \u2500\u2500 '),
|
|
61
|
+
h(Text, { color: '#44445A' }, '\u2191\u2193 navigate'),
|
|
62
|
+
h(Text, { color: '#2A2A3A' }, ' \u00B7 '),
|
|
63
|
+
h(Text, { color: '#44445A' }, '\u23CE select'),
|
|
64
|
+
h(Text, { color: '#2A2A3A' }, ' \u00B7 '),
|
|
65
|
+
h(Text, { color: '#44445A' }, 'esc dismiss'),
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
// ── Separator ──────────────────────────────────────────────────────────────
|
|
69
|
+
const sepRow = h(Box, {
|
|
70
|
+
key: 'slash-sep',
|
|
71
|
+
width: '100%',
|
|
72
|
+
height: 1,
|
|
73
|
+
paddingLeft: 1,
|
|
74
|
+
backgroundColor: '#0D0D14',
|
|
75
|
+
},
|
|
76
|
+
h(Text, { color: '#1E1E2E' }, '\u2500'.repeat(60))
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
// ── Command rows ───────────────────────────────────────────────────────────
|
|
80
|
+
const maxVisible = SLASH_OVERLAY_HEIGHT - 2; // 4 rows after header + separator
|
|
81
|
+
const itemRows = [];
|
|
82
|
+
|
|
83
|
+
for (let i = 0; i < Math.min(safeCommands.length, maxVisible); i++) {
|
|
84
|
+
const cmd = safeCommands[i];
|
|
85
|
+
const isSelected = i === safeIndex;
|
|
86
|
+
|
|
87
|
+
itemRows.push(
|
|
88
|
+
h(Box, {
|
|
89
|
+
key: cmd.trigger,
|
|
90
|
+
width: '100%',
|
|
91
|
+
height: 1,
|
|
92
|
+
flexDirection: 'row',
|
|
93
|
+
alignItems: 'center',
|
|
94
|
+
paddingLeft: 1,
|
|
95
|
+
paddingRight: 1,
|
|
96
|
+
// Orange highlight bar on selected row — matches OpenCode palette style
|
|
97
|
+
backgroundColor: isSelected ? '#2C1A00' : '#0D0D14',
|
|
98
|
+
},
|
|
99
|
+
// Selection marker
|
|
100
|
+
h(Text, {
|
|
101
|
+
color: isSelected ? hex.orange : '#1E1E2E',
|
|
102
|
+
bold: true,
|
|
103
|
+
}, isSelected ? '\u25B6 ' : ' '),
|
|
104
|
+
// Command trigger
|
|
105
|
+
h(Text, {
|
|
106
|
+
color: isSelected ? hex.orange : '#5A5A7A',
|
|
107
|
+
bold: isSelected,
|
|
108
|
+
}, cmd.trigger),
|
|
109
|
+
// Spacer
|
|
110
|
+
h(Text, { color: '#1A1A2A' }, ' '),
|
|
111
|
+
// Description
|
|
112
|
+
h(Text, {
|
|
113
|
+
color: isSelected ? '#DDDDEE' : '#3A3A55',
|
|
114
|
+
}, cmd.description),
|
|
115
|
+
)
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return h(Box, {
|
|
120
|
+
width: '100%',
|
|
121
|
+
height: SLASH_OVERLAY_HEIGHT,
|
|
122
|
+
flexDirection: 'column',
|
|
123
|
+
flexShrink: 0,
|
|
124
|
+
overflow: 'hidden',
|
|
125
|
+
},
|
|
126
|
+
headerRow,
|
|
127
|
+
sepRow,
|
|
128
|
+
...itemRows
|
|
129
|
+
);
|
|
130
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
function checkString(value, label) {
|
|
2
|
+
if (typeof value !== 'string') {
|
|
3
|
+
throw new Error(label + ' must be a string');
|
|
4
|
+
}
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function checkCommand(entry, index) {
|
|
8
|
+
if (entry === null || entry === undefined || typeof entry !== 'object') {
|
|
9
|
+
throw new Error('Command at index ' + index + ' must be an object');
|
|
10
|
+
}
|
|
11
|
+
checkString(entry.trigger, 'Command ' + index + ' trigger');
|
|
12
|
+
checkString(entry.description, 'Command ' + index + ' description');
|
|
13
|
+
if (entry.trigger[0] !== '/') {
|
|
14
|
+
throw new Error('Command ' + index + ' trigger must start with "/"');
|
|
15
|
+
}
|
|
16
|
+
if (entry.trigger.length < 2) {
|
|
17
|
+
throw new Error('Command ' + index + ' trigger must be at least 2 characters');
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function checkCommands(arr) {
|
|
22
|
+
if (!Array.isArray(arr)) {
|
|
23
|
+
throw new Error('Commands must be an array');
|
|
24
|
+
}
|
|
25
|
+
if (arr.length === 0) {
|
|
26
|
+
throw new Error('Commands array must not be empty');
|
|
27
|
+
}
|
|
28
|
+
for (let i = 0; i < arr.length; i++) {
|
|
29
|
+
checkCommand(arr[i], i);
|
|
30
|
+
}
|
|
31
|
+
return arr;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const COMMAND_DEFS = Object.freeze([
|
|
35
|
+
{ trigger: '/help', description: 'Show available commands' },
|
|
36
|
+
{ trigger: '/clear', description: 'Clear the conversation' },
|
|
37
|
+
{ trigger: '/model', description: 'Switch between Flash and Heavy' },
|
|
38
|
+
{ trigger: '/exit', description: 'Quit the application' },
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
checkCommands(COMMAND_DEFS);
|
|
42
|
+
|
|
43
|
+
export const SLASH_MENU_HEIGHT = 6;
|
|
44
|
+
|
|
45
|
+
export function getCommands() {
|
|
46
|
+
return COMMAND_DEFS;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function findCommandByTrigger(input) {
|
|
50
|
+
for (let i = 0; i < COMMAND_DEFS.length; i++) {
|
|
51
|
+
if (input === COMMAND_DEFS[i].trigger || input.startsWith(COMMAND_DEFS[i].trigger + ' ')) {
|
|
52
|
+
return COMMAND_DEFS[i];
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
function checkString(value, label) {
|
|
2
|
+
if (typeof value !== 'string') {
|
|
3
|
+
throw new Error(label + ' must be a string');
|
|
4
|
+
}
|
|
5
|
+
if (value.length === 0) {
|
|
6
|
+
throw new Error(label + ' must not be empty');
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function checkModelEntry(entry, index) {
|
|
11
|
+
if (entry === null || entry === undefined || typeof entry !== 'object') {
|
|
12
|
+
throw new Error('Model entry at index ' + index + ' must be an object');
|
|
13
|
+
}
|
|
14
|
+
checkString(entry.id, 'Model ' + index + ' id');
|
|
15
|
+
checkString(entry.label, 'Model ' + index + ' label');
|
|
16
|
+
checkString(entry.badge, 'Model ' + index + ' badge');
|
|
17
|
+
checkString(entry.provider, 'Model ' + index + ' provider');
|
|
18
|
+
if (entry.id.indexOf('/') === -1) {
|
|
19
|
+
throw new Error('Model ' + index + ' id must contain "/" (provider/model format)');
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function checkModels(arr) {
|
|
24
|
+
if (!Array.isArray(arr)) {
|
|
25
|
+
throw new Error('Models must be an array');
|
|
26
|
+
}
|
|
27
|
+
if (arr.length === 0) {
|
|
28
|
+
throw new Error('Models array must not be empty');
|
|
29
|
+
}
|
|
30
|
+
for (let i = 0; i < arr.length; i++) {
|
|
31
|
+
checkModelEntry(arr[i], i);
|
|
32
|
+
}
|
|
33
|
+
return arr;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Two routed models only — Flash (fast) and Heavy (quality).
|
|
37
|
+
const MODELS = Object.freeze([
|
|
38
|
+
{
|
|
39
|
+
id: 'openaxis/openaxis-flash',
|
|
40
|
+
provider: 'openaxis',
|
|
41
|
+
label: 'OpenAxies Flash',
|
|
42
|
+
badge: 'Fast',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: 'openaxis/openaxis-heavy',
|
|
46
|
+
provider: 'openaxis',
|
|
47
|
+
label: 'OpenAxies Heavy',
|
|
48
|
+
badge: 'Quality',
|
|
49
|
+
},
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
checkModels(MODELS);
|
|
53
|
+
|
|
54
|
+
export function getModels() {
|
|
55
|
+
return MODELS;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function getModelById(id) {
|
|
59
|
+
for (let i = 0; i < MODELS.length; i++) {
|
|
60
|
+
if (MODELS[i].id === id) {
|
|
61
|
+
return MODELS[i];
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const DEFAULT_MODEL_ID = MODELS[0].id;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
function checkString(value, label) {
|
|
2
|
+
if (typeof value !== 'string') {
|
|
3
|
+
throw new Error(label + ' must be a string');
|
|
4
|
+
}
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function checkHex(value, label) {
|
|
8
|
+
checkString(value, label);
|
|
9
|
+
if (!/^#[0-9A-Fa-f]{6}$/.test(value)) {
|
|
10
|
+
throw new Error(label + ' must be a 6-digit hex color');
|
|
11
|
+
}
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const hex = Object.freeze({
|
|
16
|
+
orange: '#FF9F43',
|
|
17
|
+
cardBg: '#1C1C1C',
|
|
18
|
+
border: '#2A2A3A',
|
|
19
|
+
muted: '#666688',
|
|
20
|
+
primary: '#FFFFFF',
|
|
21
|
+
secondary: '#888888',
|
|
22
|
+
black: '#000000',
|
|
23
|
+
deepGray: '#262626',
|
|
24
|
+
dockBg: '#1A1A1A',
|
|
25
|
+
neonBlue: '#00F0FF',
|
|
26
|
+
dimWhite: '#AAAAAA',
|
|
27
|
+
greenOnline: '#00FF88',
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const keys = Object.keys(hex);
|
|
31
|
+
for (let i = 0; i < keys.length; i++) {
|
|
32
|
+
checkHex(hex[keys[i]], keys[i]);
|
|
33
|
+
}
|