openaxies 0.2.1 → 0.4.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openaxies",
3
- "version": "0.2.1",
3
+ "version": "0.4.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
package/src/App.js CHANGED
@@ -56,6 +56,8 @@ function AppRoot() {
56
56
  const [streamText, setStreamText] = React.useState('');
57
57
  const [streamingActive, setStreamingActive] = React.useState(false);
58
58
  const [scrollOffset, setScrollOffset] = React.useState(0);
59
+ const [isThinking, setIsThinking] = React.useState(false);
60
+ const [spinnerChar, setSpinnerChar] = React.useState('');
59
61
 
60
62
  const commands = getCommands();
61
63
 
@@ -72,6 +74,34 @@ function AppRoot() {
72
74
  const availLines = Math.max(1, rows - fixedH);
73
75
 
74
76
  const abortRef = React.useRef(null);
77
+ const connectingRef = React.useRef(null);
78
+ const spinnerRef = React.useRef(null);
79
+ const thinkingAccumRef = React.useRef('');
80
+
81
+ const SPINNER_FRAMES = ['◐', '◓', '◑', '◒'];
82
+
83
+ React.useEffect(function () {
84
+ if (isThinking === false) {
85
+ if (spinnerRef.current !== null) {
86
+ clearInterval(spinnerRef.current);
87
+ spinnerRef.current = null;
88
+ }
89
+ setSpinnerChar('');
90
+ return;
91
+ }
92
+ let idx = 0;
93
+ setSpinnerChar(SPINNER_FRAMES[0]);
94
+ spinnerRef.current = setInterval(function () {
95
+ idx = (idx + 1) % SPINNER_FRAMES.length;
96
+ setSpinnerChar(SPINNER_FRAMES[idx]);
97
+ }, 120);
98
+ return function cleanup() {
99
+ if (spinnerRef.current !== null) {
100
+ clearInterval(spinnerRef.current);
101
+ spinnerRef.current = null;
102
+ }
103
+ };
104
+ }, [isThinking]);
75
105
 
76
106
  // ---------------------------------------------------------------------------
77
107
  // Input handlers
@@ -120,7 +150,7 @@ function AppRoot() {
120
150
  if (prev === 'openaxis/openaxis-flash') {
121
151
  return 'openaxis/openaxis-heavy';
122
152
  }
123
- return 'openaxis/openaxis-flash';
153
+ return 'openaxis/openaxis-flash';
124
154
  });
125
155
  }
126
156
 
@@ -133,7 +163,7 @@ function AppRoot() {
133
163
  '**Available commands:**\n' +
134
164
  '- `/help` — Show this help\n' +
135
165
  '- `/clear` — Clear the conversation\n' +
136
- '- `/model` — Toggle between OpenAxies Flash and Heavy\n' +
166
+ '- `/model` — Toggle between Flash and Heavy\n' +
137
167
  '- `/exit` — Quit the application\n\n' +
138
168
  'Type your message and press **Enter** to chat.',
139
169
  id: 'help-' + Date.now(),
@@ -161,10 +191,23 @@ function AppRoot() {
161
191
  return prev.concat([{ role: 'user', content: safeText, id: msgId }]);
162
192
  });
163
193
  setInputBuffer('');
164
- setStreamText('\u2022\u2022\u2022'); // animated placeholder
194
+ setStreamText(' connecting');
195
+ connectingRef.current = setInterval(function () {
196
+ setStreamText(function (prev) {
197
+ if (prev === ' connecting') return ' connecting.';
198
+ if (prev === ' connecting.') return ' connecting..';
199
+ if (prev === ' connecting..') return ' connecting...';
200
+ if (prev === ' connecting...') return ' connecting';
201
+ return prev;
202
+ });
203
+ }, 400);
165
204
 
166
205
  const modelInfo = getModelById(activeModel);
167
206
  if (modelInfo === null || modelInfo === undefined) {
207
+ if (connectingRef.current !== null) {
208
+ clearInterval(connectingRef.current);
209
+ connectingRef.current = null;
210
+ }
168
211
  setStreamingActive(false);
169
212
  setStreamText('');
170
213
  setScrollOffset(0);
@@ -183,46 +226,44 @@ function AppRoot() {
183
226
  abortRef.current = abortController;
184
227
 
185
228
  try {
229
+ function checkThinkState(text) {
230
+ const openIdx = text.lastIndexOf('<think>');
231
+ const closeIdx = text.lastIndexOf('</think>');
232
+ if (openIdx === -1 && closeIdx === -1) {
233
+ return false;
234
+ }
235
+ if (openIdx > closeIdx) {
236
+ return true;
237
+ }
238
+ return false;
239
+ }
240
+
186
241
  const gen = callModel(modelInfo, history, abortController.signal);
187
242
  let accumulated = '';
188
- let thinkingId = null;
189
- const thinkingLines = [];
243
+ let firstEvent = true;
190
244
 
191
245
  for await (const event of gen) {
192
246
  if (abortController.signal.aborted === true) {
193
247
  break;
194
248
  }
195
- if (event.type === 'thinking') {
196
- if (thinkingId === null) {
197
- thinkingId = 'think-' + Date.now() + '-' + Math.random().toString(36).slice(2, 8);
249
+ if (firstEvent === true) {
250
+ firstEvent = false;
251
+ if (connectingRef.current !== null) {
252
+ clearInterval(connectingRef.current);
253
+ connectingRef.current = null;
198
254
  }
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
255
  }
220
- if (event.type === 'token') {
256
+ if (event.type === 'thinking' || event.type === 'token') {
221
257
  const content = typeof event.content === 'string' ? event.content : '';
222
258
  for (let i = 0; i < content.length; i++) {
223
259
  accumulated = accumulated + content[i];
260
+ const isInsideThink = checkThinkState(accumulated);
224
261
  setScrollOffset(0);
225
262
  setStreamText(accumulated);
263
+ setIsThinking(isInsideThink);
264
+ if (isInsideThink === true) {
265
+ thinkingAccumRef.current = accumulated;
266
+ }
226
267
  await new Promise(function (resolve) {
227
268
  setTimeout(resolve, 0);
228
269
  });
@@ -240,6 +281,12 @@ function AppRoot() {
240
281
  }
241
282
  }
242
283
 
284
+ if (connectingRef.current !== null) {
285
+ clearInterval(connectingRef.current);
286
+ connectingRef.current = null;
287
+ }
288
+ setIsThinking(false);
289
+ thinkingAccumRef.current = '';
243
290
  setStreamingActive(false);
244
291
  setStreamText('');
245
292
 
@@ -253,6 +300,12 @@ function AppRoot() {
253
300
  });
254
301
  }
255
302
  } catch (err) {
303
+ if (connectingRef.current !== null) {
304
+ clearInterval(connectingRef.current);
305
+ connectingRef.current = null;
306
+ }
307
+ setIsThinking(false);
308
+ thinkingAccumRef.current = '';
256
309
  setStreamingActive(false);
257
310
  setStreamText('');
258
311
  setScrollOffset(0);
@@ -360,7 +413,7 @@ function AppRoot() {
360
413
 
361
414
  const header = createBrandHeader(cols);
362
415
  const routerBar = createRouterBar(activeModel, cols);
363
- const viewport = createChatViewport(messages, streamText, availLines, cols, scrollOffset);
416
+ const viewport = createChatViewport(messages, streamText, availLines, cols, scrollOffset, isThinking, spinnerChar);
364
417
  const slashOverlay = showOverlay === true
365
418
  ? createSlashOverlay(commands, overlayIndex)
366
419
  : null;
@@ -4,6 +4,9 @@ import { hex } from '../config/theme.js';
4
4
 
5
5
  const h = React.createElement;
6
6
 
7
+ const THINK_OPEN = '<think>';
8
+ const THINK_CLOSE = '</think>';
9
+
7
10
  function checkArray(arr) {
8
11
  if (Array.isArray(arr) === false) {
9
12
  return [];
@@ -11,49 +14,6 @@ function checkArray(arr) {
11
14
  return arr;
12
15
  }
13
16
 
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
17
  function clampWidth(width) {
58
18
  if (typeof width !== 'number' || width < 20) {
59
19
  return 80;
@@ -104,42 +64,70 @@ function pushWrapped(lines, text, width, color, bold) {
104
64
  }
105
65
  }
106
66
 
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
- }
67
+ function stripThinkTags(text) {
68
+ if (typeof text !== 'string') {
69
+ return '';
70
+ }
71
+ return text.split(THINK_OPEN).join('').split(THINK_CLOSE).join('');
72
+ }
73
+
74
+ function extractThinkContent(text) {
75
+ if (typeof text !== 'string') {
76
+ return '';
77
+ }
78
+ const openIdx = text.indexOf(THINK_OPEN);
79
+ if (openIdx === -1) {
80
+ return '';
81
+ }
82
+ const closeIdx = text.indexOf(THINK_CLOSE, openIdx + THINK_OPEN.length);
83
+ if (closeIdx === -1) {
84
+ return text.slice(openIdx + THINK_OPEN.length);
85
+ }
86
+ return text.slice(openIdx + THINK_OPEN.length, closeIdx);
87
+ }
88
+
89
+ function extractAfterThink(text) {
90
+ if (typeof text !== 'string') {
91
+ return '';
92
+ }
93
+ const closeIdx = text.lastIndexOf(THINK_CLOSE);
94
+ if (closeIdx === -1) {
95
+ return '';
116
96
  }
97
+ return text.slice(closeIdx + THINK_CLOSE.length);
117
98
  }
118
99
 
119
- // Transcript message - linear terminal flow, no side panels or bubbles.
120
- function pushMessageLines(lines, msg, safeCols) {
100
+ function pushRawMessage(lines, msg, safeCols) {
121
101
  if (msg === null || msg === undefined || typeof msg !== 'object') {
122
102
  return;
123
103
  }
124
104
 
125
105
  const content = typeof msg.content === 'string' ? msg.content : '';
106
+ if (content.length === 0) {
107
+ return;
108
+ }
126
109
 
127
- if (msg.role === 'thinking') {
128
- pushWrapped(lines, content, safeCols, hex.orange, false);
110
+ const clean = stripThinkTags(content);
111
+ if (clean.length === 0) {
129
112
  return;
130
113
  }
131
114
 
132
115
  if (msg.role === 'user') {
133
- pushWrapped(lines, 'You > ' + content, safeCols, hex.neonBlue, true);
116
+ pushWrapped(lines, clean, safeCols, hex.neonBlue, false);
134
117
  return;
135
118
  }
136
119
 
137
- pushLine(lines, 'OpenAxies $', hex.greenOnline, true);
138
- pushContentLines(lines, content, safeCols);
139
- pushLine(lines, '', hex.primary, false);
120
+ pushWrapped(lines, clean, safeCols, hex.primary, false);
121
+ }
122
+
123
+ function pushSpinnerLine(lines, spinnerChar, safeCols) {
124
+ const safeChar = typeof spinnerChar === 'string' && spinnerChar.length > 0
125
+ ? spinnerChar
126
+ : '◐';
127
+ pushLine(lines, ' ' + safeChar + ' thinking', '#FF9F43', true);
140
128
  }
141
129
 
142
- export function createChatViewport(messages, streamText, availLines, cols, scrollOffset) {
130
+ export function createChatViewport(messages, streamText, availLines, cols, scrollOffset, isThinking, spinnerChar) {
143
131
  const safeMessages = checkArray(messages);
144
132
  const safeAvail = typeof availLines === 'number' && availLines > 0 ? availLines : 1;
145
133
  const safeCols = clampWidth(cols);
@@ -147,19 +135,30 @@ export function createChatViewport(messages, streamText, availLines, cols, scrol
147
135
  ? Math.floor(scrollOffset)
148
136
  : 0;
149
137
  const hasStream = typeof streamText === 'string' && streamText.length > 0;
138
+ const isStreamingThink = isThinking === true && hasStream === true;
150
139
  const transcriptLines = [];
151
140
 
152
141
  for (let i = 0; i < safeMessages.length; i++) {
153
- pushMessageLines(transcriptLines, safeMessages[i], safeCols);
142
+ pushRawMessage(transcriptLines, safeMessages[i], safeCols);
154
143
  }
155
144
 
156
145
  if (hasStream === true) {
157
- pushLine(transcriptLines, 'OpenAxies $ streaming...', hex.greenOnline, true);
158
- pushContentLines(transcriptLines, streamText, safeCols);
146
+ if (isStreamingThink === true) {
147
+ pushSpinnerLine(transcriptLines, spinnerChar, safeCols);
148
+ const thinkContent = extractThinkContent(streamText);
149
+ if (thinkContent.length > 0) {
150
+ pushWrapped(transcriptLines, thinkContent, safeCols, '#FF9F43', false);
151
+ }
152
+ } else {
153
+ const clean = stripThinkTags(streamText);
154
+ if (clean.length > 0) {
155
+ pushWrapped(transcriptLines, clean, safeCols, hex.primary, false);
156
+ }
157
+ }
159
158
  }
160
159
 
161
160
  if (transcriptLines.length === 0) {
162
- pushLine(transcriptLines, '# waiting for input', '#333355', false);
161
+ pushLine(transcriptLines, ' awaiting input...', '#333355', false);
163
162
  }
164
163
 
165
164
  const visibleHeight = Math.max(1, safeAvail - 1);
@@ -52,7 +52,7 @@ export function createRouterBar(activeModel, cols) {
52
52
  alignItems: 'center',
53
53
  },
54
54
 
55
- // ── Flash slot ──
55
+ // ── Mixtral slot ──
56
56
  h(Text, {
57
57
  color: isFlash ? hex.neonBlue : '#333355',
58
58
  bold: isFlash,
@@ -33,7 +33,6 @@ function checkModels(arr) {
33
33
  return arr;
34
34
  }
35
35
 
36
- // Two routed models only — Flash (fast) and Heavy (quality).
37
36
  const MODELS = Object.freeze([
38
37
  {
39
38
  id: 'openaxis/openaxis-flash',
@@ -55,26 +55,18 @@ export async function* callModel(modelConfig, messages, signal) {
55
55
  let modelMessages = messages;
56
56
 
57
57
  if (shouldUseWebSearch(messages) === true) {
58
- yield { type: 'thinking', content: 'OpenAxies thinking > [Internal reasoning]' };
59
- yield { type: 'thinking', content: 'Checking tools...' };
60
- yield { type: 'thinking', content: 'Let me do websearch' };
61
- yield { type: 'thinking', content: 'Doing websearch' };
62
-
63
58
  try {
64
59
  const webResult = await runWebSearchGraph(messages, signal);
65
- if (webResult.used === true) {
66
- yield { type: 'thinking', content: 'Searched ' + webResult.sites + ' sites' };
67
- yield { type: 'thinking', content: 'Thinking...' };
60
+ if (webResult.used === true && webResult.sites > 0) {
68
61
  modelMessages = [{
69
62
  role: 'system',
70
63
  content:
71
- 'Use these websearch results when they are relevant. Cite URLs inline when possible.\n\n' +
72
- webResult.summary,
64
+ 'Current web search results (use when relevant, cite URLs inline):\n\n' +
65
+ webResult.summary +
66
+ '\n\nNow respond to the user using these results as needed.',
73
67
  }].concat(messages);
74
68
  }
75
- } catch (err) {
76
- yield { type: 'thinking', content: 'Searched 0 sites' };
77
- yield { type: 'thinking', content: 'Thinking...' };
69
+ } catch (_) {
78
70
  }
79
71
  }
80
72
 
@@ -1,5 +1,5 @@
1
- const CONNECT_TIMEOUT = 120000;
2
- const READ_TIMEOUT = 15000;
1
+ const CONNECT_TIMEOUT = 20000;
2
+ const READ_TIMEOUT = 8000;
3
3
 
4
4
  function readWithTimeout(reader, timeoutMs) {
5
5
  return Promise.race([