osai-agent 4.2.37 → 4.2.41

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": "osai-agent",
3
- "version": "4.2.37",
3
+ "version": "4.2.41",
4
4
  "type": "module",
5
5
  "description": "OS AI Agent - YOUR AI AGENT",
6
6
  "main": "src/index.js",
@@ -39,6 +39,7 @@
39
39
  ],
40
40
  "dependencies": {
41
41
  "@cfworker/json-schema": "^4.1.1",
42
+ "@google/generative-ai": "^0.24.1",
42
43
  "@modelcontextprotocol/client": "^2.0.0-alpha.3",
43
44
  "@tavily/core": "^0.7.3",
44
45
  "boxen": "^8.0.1",
@@ -47,17 +48,17 @@
47
48
  "conf": "^12.0.0",
48
49
  "duck-duck-scrape": "^2.2.7",
49
50
  "fast-glob": "^3.3.3",
50
- "@google/generative-ai": "^0.24.1",
51
- "openai": "^6.42.0",
52
51
  "ink": "^7.0.3",
53
52
  "ink-scroll-view": "^0.3.7",
54
53
  "ink-spinner": "^5.0.0",
55
54
  "ink-text-input": "^6.0.0",
56
55
  "inquirer": "^13.4.2",
56
+ "is-unicode-supported": "^2.1.0",
57
57
  "marked": "^15.0.12",
58
58
  "marked-terminal": "^7.3.0",
59
59
  "minimist": "^1.2.8",
60
60
  "node-machine-id": "^1.1.12",
61
+ "openai": "^6.42.0",
61
62
  "ora": "^9.4.0",
62
63
  "react": "^19.2.6",
63
64
  "serpapi": "^2.2.1",
@@ -1,6 +1,7 @@
1
1
  import Conf from 'conf';
2
2
  import chalk from 'chalk';
3
3
  import boxen from 'boxen';
4
+ import { DOT, EMPTY } from '../utils/unicode.js';
4
5
  import ora from 'ora';
5
6
  import { createInterface } from 'node:readline';
6
7
  import { rmSync, existsSync } from 'node:fs';
@@ -20,7 +21,7 @@ export const deleteAccount = async ({ server: serverArg }) => {
20
21
  '',
21
22
  ` ${chalk.hex('#8ab4d8')('Your AI sysadmin, network engineer & senior developer')}`,
22
23
  '',
23
- ` ${token ? chalk.green('●') : chalk.red('○')} ${chalk.hex('#8ab4d8')('Auth:')} ${chalk.white(token ? 'Authenticated' : 'Not authenticated')}`,
24
+ ` ${token ? chalk.green(DOT) : chalk.red(EMPTY)} ${chalk.hex('#8ab4d8')('Auth:')} ${chalk.white(token ? 'Authenticated' : 'Not authenticated')}`,
24
25
  ].join('\n');
25
26
 
26
27
  console.log(boxen(headerContent, {
@@ -1,5 +1,6 @@
1
1
  import Conf from 'conf';
2
2
  import { mcpClientManager } from '../tools/mcp-client.js';
3
+ import { TICK, CROSS } from '../utils/unicode.js';
3
4
 
4
5
  const config = new Conf({ projectName: 'osai-agent' });
5
6
 
@@ -211,12 +212,12 @@ async function reloadServers() {
211
212
 
212
213
  if (results.connected.length > 0) {
213
214
  for (const r of results.connected) {
214
- console.log(` \x1b[32m✓\x1b[0m ${r.name}: ${r.toolCount} tools`);
215
+ console.log(` \x1b[32m${TICK}\x1b[0m ${r.name}: ${r.toolCount} tools`);
215
216
  }
216
217
  }
217
218
  if (results.failed.length > 0) {
218
219
  for (const r of results.failed) {
219
- console.log(` \x1b[31m✗\x1b[0m ${r.name}: ${r.error}`);
220
+ console.log(` \x1b[31m${CROSS}\x1b[0m ${r.name}: ${r.error}`);
220
221
  }
221
222
  }
222
223
  }
@@ -240,7 +241,7 @@ async function testServer(args) {
240
241
 
241
242
  try {
242
243
  const tools = await mcpClientManager.addServer(name, cfg);
243
- console.log(`\x1b[32m Connected\x1b[0m — ${tools.length} tools:`);
244
+ console.log(`\x1b[32m${TICK} Connected\x1b[0m — ${tools.length} tools:`);
244
245
  for (const tool of tools) {
245
246
  console.log(` - ${tool.name}: ${tool.description || '(no description)'}`);
246
247
  if (tool.inputSchema) {
@@ -253,6 +254,6 @@ async function testServer(args) {
253
254
  }
254
255
  await mcpClientManager.removeServer(name);
255
256
  } catch (err) {
256
- console.log(`\x1b[31m Connection failed\x1b[0m: ${err.message}`);
257
+ console.log(`\x1b[31m${CROSS} Connection failed\x1b[0m: ${err.message}`);
257
258
  }
258
259
  }
@@ -3,6 +3,7 @@ import Conf from 'conf';
3
3
  import inquirer from 'inquirer';
4
4
  import ora from 'ora';
5
5
  import { printError, printSuccess, printInfo, printNotLoggedIn } from '../ui/terminal.js';
6
+ import { isUnicode, DASH } from '../utils/unicode.js';
6
7
  import { toHttpUrl } from '../services/server-url.js';
7
8
  import { encrypt, decrypt, deriveKey } from '../services/crypto.js';
8
9
  import pkg from 'node-machine-id';
@@ -107,18 +108,18 @@ export const listProviders = async () => {
107
108
  const active = catalog.find(p => p.active);
108
109
  console.log();
109
110
  console.log(chalk.hex('#7aa2f7').bold(' Provider Catalog'));
110
- console.log(chalk.hex('#565f89')(' '.repeat(48)));
111
+ console.log(chalk.hex('#565f89')(' ' + DASH.repeat(48)));
111
112
  console.log(chalk.hex('#565f89')(` ${'Provider'.padEnd(20)} SDK Type Free Models`));
112
- console.log(chalk.hex('#565f89')(' '.repeat(48)));
113
+ console.log(chalk.hex('#565f89')(' ' + DASH.repeat(48)));
113
114
 
114
115
  for (const p of catalog) {
115
- const activeMark = p.active ? chalk.green(' ◀') : '';
116
+ const activeMark = p.active ? chalk.green(' ' + (isUnicode ? '◀' : '<')) : '';
116
117
  const freeMark = p.free_tier ? chalk.green('Yes') : chalk.red('No ');
117
118
  const name = p.id === active?.id ? chalk.white.bold(p.name) : chalk.white(p.name);
118
119
  const sdk = p.sdk_type === 'anthropic' ? 'native ' : 'OpenAI ';
119
120
  console.log(` ${(name + activeMark).padEnd(22)} ${sdk} ${freeMark} ${chalk.gray(p.models_count + ' models')}`);
120
121
  }
121
- console.log(chalk.hex('#565f89')(' '.repeat(48)));
122
+ console.log(chalk.hex('#565f89')(' ' + DASH.repeat(48)));
122
123
  console.log(chalk.gray(` ${active?.name || 'OS AI Agent'} is currently active\n`));
123
124
  } catch (err) {
124
125
  spinner.fail('Network error');
@@ -1,6 +1,7 @@
1
1
  import chalk from 'chalk';
2
2
  import Conf from 'conf';
3
3
  import { printError, printSuccess, printInfo } from '../ui/terminal.js';
4
+ import { isUnicode, DASH } from '../utils/unicode.js';
4
5
 
5
6
  const getConfig = () => new Conf({ projectName: 'osai-agent' });
6
7
 
@@ -44,7 +45,7 @@ export const searchList = async () => {
44
45
 
45
46
  console.log();
46
47
  console.log(chalk.hex('#7aa2f7').bold(' Search Providers'));
47
- console.log(chalk.hex('#565f89')(' '.repeat(32)));
48
+ console.log(chalk.hex('#565f89')(' ' + DASH.repeat(32)));
48
49
 
49
50
  if (Object.keys(searchKeys).length === 0) {
50
51
  console.log(chalk.hex('#565f89')(' No search API keys configured.'));
@@ -58,7 +59,7 @@ export const searchList = async () => {
58
59
  console.log(` ${chalk.white(provider.padEnd(20))} ${chalk.gray(maskKey(key))}`);
59
60
  }
60
61
 
61
- console.log(chalk.hex('#565f89')(' '.repeat(32)));
62
+ console.log(chalk.hex('#565f89')(' ' + DASH.repeat(32)));
62
63
  console.log(chalk.gray(' DuckDuckGo is always available (no key needed)'));
63
64
  console.log();
64
65
  };
@@ -1,6 +1,7 @@
1
1
  import Conf from 'conf';
2
2
  import chalk from 'chalk';
3
3
  import boxen from 'boxen';
4
+ import { DOT, EMPTY } from '../utils/unicode.js';
4
5
  import { showConfig } from './config.js';
5
6
  import { clearScreen, printError, printPanel } from '../ui/terminal.js';
6
7
  import { logger } from '../utils/logger.js';
@@ -15,7 +16,7 @@ export const stopSubagent = async ({ server }) => {
15
16
  const token = config.get('token');
16
17
  const serverUrl = server || config.get('server') || 'https://agent.osai.dev';
17
18
  // Default from env
18
- const authDot = token ? chalk.green('●') : chalk.red('○');
19
+ const authDot = token ? chalk.green(DOT) : chalk.red(EMPTY);
19
20
  const authLabel = token ? 'Authenticated' : 'Not authenticated';
20
21
  const serverHost = serverUrl.replace(/^ws(s?):\/\//, '');
21
22
  const headerContent = [
@@ -1,7 +1,7 @@
1
1
  // Markdown renderer — buffered + streaming
2
2
 
3
3
  import chalk from 'chalk';
4
-
4
+ import { isUnicode } from '../utils/unicode.js';
5
5
  // ─── Tokyo Night color tokens ─────────────────────────────────
6
6
  const C = {
7
7
  text: '#c0caf5',
@@ -22,10 +22,23 @@ const C = {
22
22
  };
23
23
 
24
24
  const ELLIPSIS = '…';
25
- const FORCE_ASCII_TABLES =
25
+ const FORCE_ASCII =
26
26
  process.env.OSAI_TABLE_ASCII === '1' ||
27
27
  process.env.NO_UNICODE_TABLES === '1' ||
28
- (process.env.TERM || '').toLowerCase() === 'dumb';
28
+ (process.env.TERM || '').toLowerCase() === 'dumb' ||
29
+ !isUnicode;
30
+
31
+ const S = FORCE_ASCII ? {
32
+ boxTL: '+', boxH: '-', boxV: '|', boxBL: '+',
33
+ h1: '==', h2: '>', h3: 'o', h4: '>',
34
+ hr: '-', bq: '|', dot: 'o',
35
+ ul0: '*', ul1: '-', ul2: '>',
36
+ } : {
37
+ boxTL: '╭', boxH: '─', boxV: '│', boxBL: '╰',
38
+ h1: '━━', h2: '◆', h3: '●', h4: '›',
39
+ hr: '─', bq: '┃', dot: '●',
40
+ ul0: '•', ul1: '◦', ul2: '▸',
41
+ };
29
42
 
30
43
  // ─── Tool names (for stripping tool JSON from output) ────────
31
44
  const ALL_TOOLS = 'LOCAL_CMD|SSH_CMD|READ_FILE|WRITE_FILE|EDIT_FILE|APPEND_FILE|DELETE_FILE|LIST_DIR|SEARCH_FILE|CREATE_DIR|TREE_VIEW|RUN_SCRIPT|MOVE_FILE|COPY_FILE|FILE_INFO|FETCH_URL|WEB_SEARCH|TODO_ADD|TODO_COMPLETE|TODO_UPDATE|TODO_LIST|TODO_CLEAR|POWERSHELL|GLOB|GREP|GIT|DIAG_POST_EDIT|ASK_USER|PLAN_MODE|SKILL_LIST|LOAD_SKILL|CREATE_SKILL|TASK';
@@ -114,7 +127,7 @@ const renderTableRows = (rows, maxWidth = 96) => {
114
127
  return out;
115
128
  });
116
129
 
117
- const useUnicode = !FORCE_ASCII_TABLES;
130
+ const useUnicode = !FORCE_ASCII;
118
131
  const colSep = useUnicode ? ' │ ' : ' | ';
119
132
  const rowSep = useUnicode ? '─┼─' : '-+-';
120
133
  const rowFill = useUnicode ? '─' : '-';
@@ -235,12 +248,12 @@ export const renderMarkdown = (text) => {
235
248
  if (!inCode) {
236
249
  inCode = true; codeLang = codeMatch[1] || ''; codeLines = [];
237
250
  const label = codeLang ? chalk.bgHex(C.border).hex(C.cyan)(` ${codeLang} `) : '';
238
- output.push(chalk.hex(C.border)(' ╭─') + (label || chalk.hex(C.border)('─')));
251
+ output.push(chalk.hex(C.border)(' ' + S.boxTL + S.boxH) + (label || chalk.hex(C.border)(S.boxH)));
239
252
  } else {
240
253
  const highlighted = highlightCode(codeLines.join('\n'), codeLang);
241
254
  for (const cl of highlighted.split('\n'))
242
- output.push(chalk.hex(C.border)(' ') + cl);
243
- output.push(chalk.hex(C.border)(' ╰────'));
255
+ output.push(chalk.hex(C.border)(' ' + S.boxV + ' ') + cl);
256
+ output.push(chalk.hex(C.border)(' ' + S.boxBL + S.boxH.repeat(4)));
244
257
  inCode = false; codeLines = []; codeLang = '';
245
258
  }
246
259
  continue;
@@ -257,32 +270,32 @@ export const renderMarkdown = (text) => {
257
270
  if (bigDot) {
258
271
  const text = bigDot[1].trim();
259
272
  output.push('');
260
- output.push(chalk.bold.hex(C.bright)(' ') + (text ? ' ' + chalk.hex(C.bright)(renderInline(text)) : ''));
273
+ output.push(chalk.bold.hex(C.bright)(' ' + S.dot) + (text ? ' ' + chalk.hex(C.bright)(renderInline(text)) : ''));
261
274
  continue;
262
275
  }
263
276
 
264
277
  // Headings
265
278
  const h1 = rawLine.match(/^# ?(.+)/);
266
- if (h1) { output.push(''); output.push(chalk.bold.hex(C.blue)(' ━━ ') + chalk.bold.hex(C.bright)(renderInline(h1[1]))); output.push(chalk.hex(C.border)(' ' + '─'.repeat(Math.min(w - 4, 58)))); continue; }
279
+ if (h1) { output.push(''); output.push(chalk.bold.hex(C.blue)(' ' + S.h1 + ' ') + chalk.bold.hex(C.bright)(renderInline(h1[1]))); output.push(chalk.hex(C.border)(' ' + S.hr.repeat(Math.min(w - 4, 58)))); continue; }
267
280
  const h2 = rawLine.match(/^## ?(.+)/);
268
- if (h2) { output.push(''); output.push(chalk.bold.hex(C.cyan)(' ') + chalk.bold.hex(C.text)(renderInline(h2[1]))); continue; }
281
+ if (h2) { output.push(''); output.push(chalk.bold.hex(C.cyan)(' ' + S.h2 + ' ') + chalk.bold.hex(C.text)(renderInline(h2[1]))); continue; }
269
282
  const h3 = rawLine.match(/^### ?(.+)/);
270
- if (h3) { output.push(''); output.push(chalk.bold.hex(C.teal)(' ') + chalk.bold.hex(C.soft)(renderInline(h3[1]))); continue; }
283
+ if (h3) { output.push(''); output.push(chalk.bold.hex(C.teal)(' ' + S.h3 + ' ') + chalk.bold.hex(C.soft)(renderInline(h3[1]))); continue; }
271
284
  const h4 = rawLine.match(/^#### ?(.+)/);
272
- if (h4) { output.push(chalk.hex(C.subtle)(' ') + chalk.italic.hex(C.soft)(renderInline(h4[1]))); continue; }
285
+ if (h4) { output.push(chalk.hex(C.subtle)(' ' + S.h4 + ' ') + chalk.italic.hex(C.soft)(renderInline(h4[1]))); continue; }
273
286
 
274
287
  // HR
275
- if (rawLine.match(/^[-*_]{3,}\s*$/)) { output.push(chalk.hex(C.border)(' ' + '─'.repeat(Math.min(w - 4, 60)))); continue; }
288
+ if (rawLine.match(/^[-*_]{3,}\s*$/)) { output.push(chalk.hex(C.border)(' ' + S.hr.repeat(Math.min(w - 4, 60)))); continue; }
276
289
 
277
290
  // Blockquote
278
291
  const bq = rawLine.match(/^>\s*(.*)/);
279
- if (bq) { output.push(chalk.hex(C.border)(' ') + chalk.italic.hex(C.soft)(renderInline(bq[1]))); continue; }
292
+ if (bq) { output.push(chalk.hex(C.border)(' ' + S.bq + ' ') + chalk.italic.hex(C.soft)(renderInline(bq[1]))); continue; }
280
293
 
281
294
  // Lists
282
295
  const num = rawLine.match(/^(\s*)(\d+)\.\s+(.*)/);
283
296
  if (num) { const [, ind, n, c] = num; const lv = Math.floor(ind.length / 2); output.push(' '.repeat(lv + 1) + chalk.hex(C.blue)(n + '.') + ' ' + renderInline(c)); continue; }
284
297
  const ul = rawLine.match(/^(\s*)[-*•]\s+(.*)/);
285
- if (ul) { const [, ind, c] = ul; const lv = Math.floor(ind.length / 2); const b = lv === 0 ? chalk.hex(C.emerald)('•') : lv === 1 ? chalk.hex(C.cyan)('◦') : chalk.hex(C.subtle)('▸'); output.push(' '.repeat(lv + 1) + b + ' ' + renderInline(c)); continue; }
298
+ if (ul) { const [, ind, c] = ul; const lv = Math.floor(ind.length / 2); const b = lv === 0 ? chalk.hex(C.emerald)(S.ul0) : lv === 1 ? chalk.hex(C.cyan)(S.ul1) : chalk.hex(C.subtle)(S.ul2); output.push(' '.repeat(lv + 1) + b + ' ' + renderInline(c)); continue; }
286
299
 
287
300
  // Empty line
288
301
  if (!rawLine.trim()) { output.push(''); continue; }
@@ -293,8 +306,8 @@ export const renderMarkdown = (text) => {
293
306
 
294
307
  if (inTable) flushTable();
295
308
  if (inCode && codeLines.length > 0) {
296
- for (const cl of codeLines) output.push(chalk.hex(C.border)(' ') + chalk.hex(C.text)(cl));
297
- output.push(chalk.hex(C.border)(' ╰────'));
309
+ for (const cl of codeLines) output.push(chalk.hex(C.border)(' ' + S.boxV + ' ') + chalk.hex(C.text)(cl));
310
+ output.push(chalk.hex(C.border)(' ' + S.boxBL + S.boxH.repeat(4)));
298
311
  }
299
312
 
300
313
  return output.join('\n');
@@ -425,12 +438,12 @@ export class StreamMarkdown {
425
438
  if (!this._inCode) {
426
439
  this._inCode = true; this._codeLang = codeMatch[1] || ''; this._codeLines = [];
427
440
  const label = this._codeLang ? chalk.bgHex(C.border).hex(C.cyan)(` ${this._codeLang} `) : '';
428
- return chalk.hex(C.border)(' ╭─') + (label || chalk.hex(C.border)('─')) + '\n';
441
+ return chalk.hex(C.border)(' ' + S.boxTL + S.boxH) + (label || chalk.hex(C.border)(S.boxH)) + '\n';
429
442
  } else {
430
443
  const highlighted = highlightCode(this._codeLines.join('\n'), this._codeLang);
431
444
  let out = '';
432
- for (const cl of highlighted.split('\n')) out += chalk.hex(C.border)(' ') + cl + '\n';
433
- out += chalk.hex(C.border)(' ╰────') + '\n';
445
+ for (const cl of highlighted.split('\n')) out += chalk.hex(C.border)(' ' + S.boxV + ' ') + cl + '\n';
446
+ out += chalk.hex(C.border)(' ' + S.boxBL + S.boxH.repeat(4)) + '\n';
434
447
  this._inCode = false; this._codeLines = []; this._codeLang = '';
435
448
  return out;
436
449
  }
@@ -465,26 +478,26 @@ export class StreamMarkdown {
465
478
  const bigDot = line.match(/(?:^\s*-{3,}\s*#{3}|^__BIGDOT__)\s*(.*)/);
466
479
  if (bigDot) {
467
480
  const text = bigDot[1].trim();
468
- return '\n' + chalk.bold.hex(C.bright)(' ') + (text ? ' ' + chalk.hex(C.bright)(renderInline(text)) : '') + '\n';
481
+ return '\n' + chalk.bold.hex(C.bright)(' ' + S.dot) + (text ? ' ' + chalk.hex(C.bright)(renderInline(text)) : '') + '\n';
469
482
  }
470
483
 
471
484
  // Headings — support both "## Title" and "##Title" (no space)
472
- const h1 = line.match(/^# ?(.+)/); if (h1) return '\n' + chalk.bold.hex(C.blue)(' ━━ ') + chalk.bold.hex(C.bright)(renderInline(h1[1])) + '\n' + chalk.hex(C.border)(' ' + '─'.repeat(56));
473
- const h2 = line.match(/^## ?(.+)/); if (h2) return '\n' + chalk.bold.hex(C.cyan)(' ') + chalk.bold(renderInline(h2[1]));
474
- const h3 = line.match(/^### ?(.+)/); if (h3) return '\n' + chalk.bold.hex(C.teal)(' ') + chalk.hex(C.soft)(renderInline(h3[1]));
475
- const h4 = line.match(/^#### ?(.+)/); if (h4) return chalk.hex(C.subtle)(' ') + chalk.italic.hex(C.soft)(renderInline(h4[1]));
485
+ const h1 = line.match(/^# ?(.+)/); if (h1) return '\n' + chalk.bold.hex(C.blue)(' ' + S.h1 + ' ') + chalk.bold.hex(C.bright)(renderInline(h1[1])) + '\n' + chalk.hex(C.border)(' ' + S.hr.repeat(56));
486
+ const h2 = line.match(/^## ?(.+)/); if (h2) return '\n' + chalk.bold.hex(C.cyan)(' ' + S.h2 + ' ') + chalk.bold(renderInline(h2[1]));
487
+ const h3 = line.match(/^### ?(.+)/); if (h3) return '\n' + chalk.bold.hex(C.teal)(' ' + S.h3 + ' ') + chalk.hex(C.soft)(renderInline(h3[1]));
488
+ const h4 = line.match(/^#### ?(.+)/); if (h4) return chalk.hex(C.subtle)(' ' + S.h4 + ' ') + chalk.italic.hex(C.soft)(renderInline(h4[1]));
476
489
 
477
490
  // HR
478
- if (line.match(/^[-*_]{3,}\s*$/)) return chalk.hex(C.border)(' ' + '─'.repeat(56));
491
+ if (line.match(/^[-*_]{3,}\s*$/)) return chalk.hex(C.border)(' ' + S.hr.repeat(56));
479
492
 
480
493
  // Blockquote
481
- const bq = line.match(/^>\s*(.*)/); if (bq) return chalk.hex(C.border)(' ') + chalk.italic.hex(C.soft)(renderInline(bq[1]));
494
+ const bq = line.match(/^>\s*(.*)/); if (bq) return chalk.hex(C.border)(' ' + S.bq + ' ') + chalk.italic.hex(C.soft)(renderInline(bq[1]));
482
495
 
483
496
  // Lists
484
497
  const num = line.match(/^(\s*)(\d+)\.\s+(.*)/);
485
498
  if (num) { const [, ind, n, c] = num; const lv = Math.floor(ind.length / 2); return ' '.repeat(lv + 1) + chalk.hex(C.blue)(n + '.') + ' ' + renderInline(c); }
486
499
  const ul = line.match(/^(\s*)[-*•]\s+(.*)/);
487
- if (ul) { const [, ind, c] = ul; const lv = Math.floor(ind.length / 2); const b = lv === 0 ? chalk.hex(C.emerald)('•') : lv === 1 ? chalk.hex(C.cyan)('◦') : chalk.hex(C.subtle)('▸'); return ' '.repeat(lv + 1) + b + ' ' + renderInline(c); }
500
+ if (ul) { const [, ind, c] = ul; const lv = Math.floor(ind.length / 2); const b = lv === 0 ? chalk.hex(C.emerald)(S.ul0) : lv === 1 ? chalk.hex(C.cyan)(S.ul1) : chalk.hex(C.subtle)(S.ul2); return ' '.repeat(lv + 1) + b + ' ' + renderInline(c); }
488
501
 
489
502
  return ' ' + renderInline(line);
490
503
  }
package/src/ui/App.js CHANGED
@@ -70,7 +70,7 @@ function formatDuration(seconds) {
70
70
  const remainMins = mins % 60;
71
71
  return `${hours}h ${remainMins}m ${Math.floor(secs)}s`;
72
72
  }
73
- const defaultRenderThrottle = process.platform !== 'win32' && process.stdout.isTTY ? '250' : '150';
73
+ const defaultRenderThrottle = process.stdout.isTTY ? '250' : '150';
74
74
  const requestedRenderThrottle = Number.parseInt(process.env.OSAI_UI_RENDER_THROTTLE_MS || defaultRenderThrottle, 10);
75
75
  const UI_RENDER_THROTTLE_MS = Number.isFinite(requestedRenderThrottle)
76
76
  ? Math.max(100, requestedRenderThrottle)
@@ -2,7 +2,7 @@ import { useEffect, useState } from 'react';
2
2
 
3
3
  export const ENABLE_UI_ANIMATIONS = process.env.OSAI_UI_ANIMATIONS !== '0';
4
4
 
5
- const defaultInterval = process.platform !== 'win32' && process.stdout.isTTY ? '800' : '400';
5
+ const defaultInterval = process.stdout.isTTY ? '800' : '400';
6
6
  const requestedInterval = Number.parseInt(process.env.OSAI_UI_ANIMATION_INTERVAL_MS || defaultInterval, 10);
7
7
  const TICK_INTERVAL_MS = Number.isFinite(requestedInterval)
8
8
  ? Math.max(300, requestedInterval)
@@ -3,6 +3,7 @@ import { Box, Text } from 'ink';
3
3
  import { h } from '../h.js';
4
4
  import { buildDiff, collapseContext, langFromPath } from '../diff.js';
5
5
  import { highlightCode } from '../../parser/markdown.js';
6
+ import { isUnicode, CROSS } from '../../utils/unicode.js';
6
7
 
7
8
  const C = {
8
9
  removedBg: '#2d1010',
@@ -109,7 +110,7 @@ export function EditFileDiff({ filePath, find, replace }) {
109
110
 
110
111
  return h(Box, { flexDirection: 'column', marginY: 1 },
111
112
  h(Box, { paddingLeft: 2, paddingY: 0 },
112
- h(Text, { color: C.headerFg, bold: true }, ' ✎ '),
113
+ h(Text, { color: C.headerFg, bold: true }, ' ' + (isUnicode ? '' : '[edit]') + ' '),
113
114
  h(Text, { color: C.headerFg }, filePath || '')
114
115
  ),
115
116
  h(Box, { flexDirection: 'column', paddingLeft: 2, borderStyle: 'round', borderColor: C.border },
@@ -191,7 +192,7 @@ export function AppendFileDiff({ filePath, content }) {
191
192
  export function DeleteFileDiff({ filePath }) {
192
193
  return h(Box, { flexDirection: 'column', marginY: 1 },
193
194
  h(Box, { paddingLeft: 2, paddingY: 0 },
194
- h(Text, { color: C.removedLabel, bold: true }, ' '),
195
+ h(Text, { color: C.removedLabel, bold: true }, ' ' + CROSS + ' '),
195
196
  h(Text, { color: C.removedLabel }, `${filePath} (deleted)`)
196
197
  ),
197
198
  h(Box, { paddingLeft: 2, borderStyle: 'round', borderColor: C.border },
@@ -1,5 +1,6 @@
1
1
  import { Box, Text } from 'ink';
2
2
  import { h } from '../h.js';
3
+ import { isUnicode, DOT, EMPTY, HEADER_LOGO_L, HEADER_LOGO_M, HEADER_LOGO_R, BAR } from '../../utils/unicode.js';
3
4
  import { ENABLE_UI_ANIMATIONS, useAnimationFrame } from '../animation.js';
4
5
 
5
6
  const chunkLength = (chunks) => chunks.reduce((sum, chunk) => sum + String(chunk.text || '').length, 0);
@@ -71,18 +72,18 @@ export function Header({ mode, device, isConnected, isLocal, provider, execution
71
72
  : null;
72
73
 
73
74
  const statusChunks = isLocal
74
- ? [{ text: '\u25CF Local', color: '#73daca', bold: true }]
75
- : [{ text: isConnected ? '\u25CF Connected' : '\u25CB Disconnected', color: isConnected ? '#9ece6a' : '#f7768e', bold: true }];
75
+ ? [{ text: DOT + ' Local', color: '#73daca', bold: true }]
76
+ : [{ text: (isConnected ? DOT : EMPTY) + ' Connected', color: isConnected ? '#9ece6a' : '#f7768e', bold: true }];
76
77
 
77
78
  const items = [
78
79
  makeItem([
79
- { text: '\u2590', color: '#7aa2f7' },
80
- { text: '\u25B3', color: '#4a9eff', bold: true },
81
- { text: '\u258C ', color: '#7aa2f7' },
80
+ { text: HEADER_LOGO_L, color: '#7aa2f7' },
81
+ { text: HEADER_LOGO_M, color: '#4a9eff', bold: true },
82
+ { text: HEADER_LOGO_R + ' ', color: '#7aa2f7' },
82
83
  { text: 'OS AI AGENT ', color: '#c0caf5', bold: true },
83
84
  ]),
84
85
  makeItem([
85
- { text: '\u2502 ', color: '#3b4261' },
86
+ { text: ' ' + BAR + ' ', color: '#3b4261' },
86
87
  { text: 'Provider: ', color: '#565f89' },
87
88
  {
88
89
  text: isDefault ? (isLocal ? 'not configured ' : 'osai/auto ') : `${providerType}${providerModel ? '/' + providerModel : ''} `,
@@ -91,31 +92,31 @@ export function Header({ mode, device, isConnected, isLocal, provider, execution
91
92
  },
92
93
  ]),
93
94
  makeItem([
94
- { text: '\u2502 ', color: '#3b4261' },
95
+ { text: ' ' + BAR + ' ', color: '#3b4261' },
95
96
  { text: 'Mode: ', color: '#565f89' },
96
97
  { text: `${mode}/${executionMode || 'EXEC'} `, color: '#c0caf5' },
97
98
  ]),
98
99
  makeItem([
99
- { text: '\u2502 ', color: '#3b4261' },
100
+ { text: ' ' + BAR + ' ', color: '#3b4261' },
100
101
  { text: 'Device: ', color: '#565f89' },
101
102
  { text: `${device || 'local'} `, color: '#c0caf5' },
102
103
  ]),
103
104
  makeItem([
104
- { text: '\u2502 ', color: '#3b4261' },
105
+ { text: ' ' + BAR + ' ', color: '#3b4261' },
105
106
  ...statusChunks,
106
107
  ]),
107
108
  ];
108
109
 
109
110
  if (subagentActive) {
110
111
  items.push(makeItem([
111
- { text: ' \u2502 ', color: '#3b4261' },
112
- { text: '\u25CF Subagent', color: subagentDim ? '#1a5fad' : '#4a9eff', bold: !subagentDim },
112
+ { text: ' ' + BAR + ' ', color: '#3b4261' },
113
+ { text: DOT + ' Subagent', color: subagentDim ? '#1a5fad' : '#4a9eff', bold: !subagentDim },
113
114
  ]));
114
115
  }
115
116
 
116
117
  if (tokenLabel) {
117
118
  items.push(makeItem([
118
- { text: ' \u2502 ', color: '#3b4261' },
119
+ { text: ' ' + BAR + ' ', color: '#3b4261' },
119
120
  { text: tokenLabel, color: '#e0af68' },
120
121
  ]));
121
122
  }
@@ -6,7 +6,7 @@ import { highlightCode } from '../../parser/markdown.js';
6
6
  import { EditFileDiff, NewFileDiff, AppendFileDiff, DeleteFileDiff } from './DiffView.js';
7
7
  import { ENABLE_UI_ANIMATIONS, useAnimationFrame } from '../animation.js';
8
8
  import { SubagentPanel } from './SubagentPanel.js';
9
-
9
+ import { isUnicode, TICK, DOT, EMPTY, ARROW, SPINNER_BRAILLE } from '../../utils/unicode.js';
10
10
  const stripAnsi = (s) => (s || '').replace(/\x1b\[[0-9;]*m/g, '').replace(/\x1b\]8;;[^\x1b]*\x1b\\/g, '');
11
11
  const visibleLen = (s) => stripAnsi(s).length;
12
12
 
@@ -26,12 +26,12 @@ const renderCellInline = (text) => {
26
26
  );
27
27
  };
28
28
 
29
- const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
29
+ const SPINNER_FRAMES = SPINNER_BRAILLE;
30
30
  const THINKING_DOTS = ['', '.', '..', '...'];
31
- const WRITING_DOTS = ['', '·', '··', '···'];
31
+ const WRITING_DOTS = !isUnicode ? ['', '.', '..', '...'] : ['', '·', '··', '···'];
32
32
 
33
33
  const WRITE_TOOLS = new Set(['WRITE_FILE', 'EDIT_FILE', 'APPEND_FILE', 'DELETE_FILE', 'MOVE_FILE', 'COPY_FILE', 'CREATE_DIR']);
34
- const READING_DOTS = ['', '∘', '∘∘', '∘∘∘'];
34
+ const READING_DOTS = !isUnicode ? ['', '.', '..', '...'] : ['', '∘', '∘∘', '∘∘∘'];
35
35
  const READ_TOOLS = new Set(['READ_FILE', 'LIST_DIR', 'TREE_VIEW', 'FILE_INFO']);
36
36
 
37
37
  function formatDuration(seconds) {
@@ -837,7 +837,7 @@ const TextContent = React.memo(({ content, events, animate = true }) => {
837
837
  return h(Box, { key: i, paddingLeft: 2 },
838
838
  part.level === 1 ? h(Text, { color: '#7aa2f7', bold: true }, `# ${part.content}`) :
839
839
  part.level === 2 ? h(Text, { color: '#7dcfff', bold: true }, `## ${part.content}`) :
840
- part.level === 3 ? h(Text, { color: '#ffffff', bold: true }, `● ${part.content}`) :
840
+ part.level === 3 ? h(Text, { color: '#ffffff', bold: true }, DOT + ' ' + part.content) :
841
841
  h(Text, { color: '#9aa5ce', bold: true }, `#### ${part.content}`)
842
842
  );
843
843
  case 'hr':
@@ -906,13 +906,13 @@ const CollapsibleThought = React.memo(function CollapsibleThought({ id, content,
906
906
  if (streaming) {
907
907
  const dots = animate && ENABLE_UI_ANIMATIONS ? THINKING_DOTS[frame % THINKING_DOTS.length].padEnd(3, ' ') : '...';
908
908
  return h(Box, { key: `thought_${id}`, paddingLeft: 2, paddingY: 0 },
909
- h(Text, { color: '#565f89', bold: true }, ' Thinking'),
909
+ h(Text, { color: '#565f89', bold: true }, ARROW + ' Thinking'),
910
910
  h(Text, { color: '#565f89' }, dots),
911
911
  h(Text, { color: '#2a2e3f' }, ' (Ctrl+O to expand)')
912
912
  );
913
913
  }
914
914
  return h(Box, { key: `thought_${id}`, paddingLeft: 2, paddingY: 0 },
915
- h(Text, { color: '#565f89' }, ' Thought'),
915
+ h(Text, { color: '#565f89' }, ARROW + ' Thought'),
916
916
  h(Text, { color: '#2a2e3f' }, ' (Ctrl+O to expand)')
917
917
  );
918
918
  });
@@ -982,7 +982,7 @@ function renderEvent(ev, i, events, expandedOutputIndexes, thoughtStreaming, exp
982
982
  ...items.map((t, j) => {
983
983
  const isDone = t.status === 'done' || t.status === 'completed';
984
984
  const isProgress = t.status === 'in_progress';
985
- const icon = isDone ? '✓' : isProgress ? '⟳' : '';
985
+ const icon = isDone ? TICK : isProgress ? (isUnicode ? '⟳' : '[.]') : EMPTY;
986
986
  const color = isDone ? '#73daca' : isProgress ? '#e0af68' : '#565f89';
987
987
  return h(Box, { key: j, paddingLeft: 2 },
988
988
  h(Text, { color }, `${icon} `),
@@ -3,6 +3,7 @@ import { Box, Text, useInput, useWindowSize } from 'ink';
3
3
  import { h } from '../h.js';
4
4
  import { InputShell } from './InputShell.js';
5
5
  import { isOnlySgrMouseInput } from '../mouse-scroll.js';
6
+ import { MODE_ICONS, ARROW, DOT, DASH } from '../../utils/unicode.js';
6
7
 
7
8
  const ALL_MODES = [
8
9
  { name: 'GENERAL', desc: 'System administration mode', value: 'GENERAL' },
@@ -13,15 +14,6 @@ const ALL_MODES = [
13
14
  { name: 'EXEC', desc: 'Exec mode — file modifications allowed', value: 'EXEC' },
14
15
  ];
15
16
 
16
- const MODE_ICONS = {
17
- GENERAL: '⚙',
18
- CODING: '⟨/⟩',
19
- NETWORK: '◈',
20
- SSH: '⇌',
21
- PLAN: '◷',
22
- EXEC: '▶',
23
- };
24
-
25
17
  export function ModePicker({ visible, onSelect, onCancel, currentMode, currentExecutionMode, hiddenModes }) {
26
18
  const [query, setQuery] = useState('');
27
19
  const [cursor, setCursor] = useState(0);
@@ -100,7 +92,7 @@ export function ModePicker({ visible, onSelect, onCancel, currentMode, currentEx
100
92
 
101
93
  if (!visible) return null;
102
94
 
103
- const separator = '─'.repeat(48);
95
+ const separator = DASH.repeat(48);
104
96
 
105
97
  return h(
106
98
  Box,
@@ -121,8 +113,8 @@ export function ModePicker({ visible, onSelect, onCancel, currentMode, currentEx
121
113
  ...filtered.map((m, i) => {
122
114
  const isHighlighted = i === cursor;
123
115
  const isCurrent = m.value === currentMode || m.value === currentExecutionMode;
124
- const icon = MODE_ICONS[m.value] || '●';
125
- const prefix = isHighlighted ? h(Text, { color: '#9ece6a' }, ' ') : h(Text, { color: '#3b3f52' }, ' ');
116
+ const icon = MODE_ICONS[m.value] || DOT;
117
+ const prefix = isHighlighted ? h(Text, { color: '#9ece6a' }, ' ' + ARROW + ' ') : h(Text, { color: '#3b3f52' }, ' ');
126
118
  const currentMark = isCurrent ? ' ◀ current' : '';
127
119
 
128
120
  return h(
@@ -2,6 +2,7 @@ import React, { useState, useEffect, useMemo } from 'react';
2
2
  import { Box, Text, useInput } from 'ink';
3
3
  import { h } from '../h.js';
4
4
  import { isOnlySgrMouseInput } from '../mouse-scroll.js';
5
+ import { isUnicode, ARROW, CROSS, DASH } from '../../utils/unicode.js';
5
6
 
6
7
  const OPTIONS = [
7
8
  { name: 'Local', desc: 'Save to local storage', value: 'local' },
@@ -68,7 +69,7 @@ export function SavePicker({ visible, onSelect, onCancel, hasCloud }) {
68
69
 
69
70
  if (!visible) return null;
70
71
 
71
- const separator = '─'.repeat(44);
72
+ const separator = DASH.repeat(44);
72
73
 
73
74
  return h(
74
75
  Box,
@@ -80,8 +81,8 @@ export function SavePicker({ visible, onSelect, onCancel, hasCloud }) {
80
81
  // Items
81
82
  ...filtered.map((o, i) => {
82
83
  const isHL = i === cursor;
83
- const prefix = isHL ? h(Text, { color: '#9ece6a' }, ' ') : h(Text, { color: '#3b3f52' }, ' ');
84
- const icon = o.value === 'local' ? ' ' : o.value === 'cloud' ? '☁ ' : ' ';
84
+ const prefix = isHL ? h(Text, { color: '#9ece6a' }, ' ' + ARROW + ' ') : h(Text, { color: '#3b3f52' }, ' ');
85
+ const icon = o.value === 'local' ? ' ' : o.value === 'cloud' ? (isUnicode ? '☁ ' : '[c] ') : CROSS + ' ';
85
86
 
86
87
  return h(Box, { key: i },
87
88
  prefix,
@@ -1,8 +1,9 @@
1
1
  import { Box, Text } from 'ink';
2
2
  import { h } from '../h.js';
3
3
  import { ENABLE_UI_ANIMATIONS, useAnimationFrame } from '../animation.js';
4
+ import { TICK, CROSS, SPINNER_CIRCLE, DOT } from '../../utils/unicode.js';
4
5
 
5
- const SPINNER = ['◐', '◓', '◑', '◒'];
6
+ const SPINNER = SPINNER_CIRCLE;
6
7
 
7
8
  function formatDuration(ms) {
8
9
  if (!ms || ms < 0) return '0s';
@@ -59,8 +60,8 @@ export function SubagentPanel({ state, events, isExpanded }) {
59
60
 
60
61
  const isRunning = state.status === 'running';
61
62
  const isFailed = state.status === 'failed';
62
- const spinner = ENABLE_UI_ANIMATIONS && isRunning ? SPINNER[frame % SPINNER.length] : '◎';
63
- const icon = isRunning ? spinner : isFailed ? '✗' : '✓';
63
+ const spinner = ENABLE_UI_ANIMATIONS && isRunning ? SPINNER[frame % SPINNER.length] : DOT;
64
+ const icon = isRunning ? spinner : isFailed ? CROSS : TICK;
64
65
  const iconColor = isRunning ? '#7dcfff' : isFailed ? '#f7768e' : '#9ece6a';
65
66
  const elapsed = state.elapsed || (state.startedAt ? Date.now() - state.startedAt : 0);
66
67
  const desc = truncate(state.description || 'Exploration task', 50);
@@ -106,7 +107,7 @@ export function SubagentPanel({ state, events, isExpanded }) {
106
107
  h(Text, { color: '#7aa2f7' }, ' ├─ '),
107
108
  h(Text, { color: getToolColor(ev.name), bold: true }, ev.name),
108
109
  formatTarget(ev) ? h(Text, { color: '#565f89' }, `: ${truncate(formatTarget(ev), 80)}`) : null,
109
- h(Text, { color: '#565f89' }, ' '),
110
+ h(Text, { color: '#565f89' }, ' ' + DOT),
110
111
  );
111
112
  }
112
113
  if (ev.type === 'tool_end') {
@@ -114,7 +115,7 @@ export function SubagentPanel({ state, events, isExpanded }) {
114
115
  h(Text, { color: ev.success ? '#9ece6a' : '#f7768e' }, ' ├─ '),
115
116
  h(Text, { color: getToolColor(ev.name), bold: true }, ev.name),
116
117
  formatTarget(ev) ? h(Text, { color: '#565f89' }, `: ${truncate(formatTarget(ev), 80)}`) : null,
117
- h(Text, { color: '#565f89' }, ev.success ? ' ' : ' '),
118
+ h(Text, { color: '#565f89' }, ev.success ? ' ' + TICK : ' ' + CROSS),
118
119
  );
119
120
  }
120
121
  return null;
@@ -3,8 +3,9 @@ import { Box, Text } from 'ink';
3
3
  import { h } from '../h.js';
4
4
  import { highlightCode } from '../../parser/markdown.js';
5
5
  import { ENABLE_UI_ANIMATIONS, useAnimationFrame } from '../animation.js';
6
+ import { SPINNER_BRAILLE, TICK, CROSS } from '../../utils/unicode.js';
6
7
 
7
- const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
8
+ const SPINNER_FRAMES = SPINNER_BRAILLE;
8
9
 
9
10
  const TOOL_COLORS = {
10
11
  READ_FILE: '#9ece6a',
@@ -1,9 +1,10 @@
1
1
  import chalk from 'chalk';
2
2
  import boxen from 'boxen';
3
+ import { TICK } from '../utils/unicode.js';
3
4
 
4
5
  export const printInfo = (msg) => console.log(chalk.blue('i ') + msg);
5
6
  export const printError = (msg) => console.error(chalk.red('x ') + msg);
6
- export const printSuccess = (msg) => console.log(chalk.green(' ') + msg);
7
+ export const printSuccess = (msg) => console.log(chalk.green(TICK + ' ') + msg);
7
8
  export const printHeader = () => {};
8
9
  export const printAuthHeader = (title, subtitle = '') => {
9
10
  console.log(boxen(
@@ -0,0 +1,42 @@
1
+ import isUnicodeSupported from 'is-unicode-supported';
2
+ export const isUnicode = isUnicodeSupported();
3
+ export const TICK = isUnicode ? '✓' : 'v';
4
+ export const CROSS = isUnicode ? '✗' : 'x';
5
+ export const DOT = isUnicode ? '●' : 'o';
6
+ export const EMPTY = isUnicode ? '○' : 'o';
7
+ export const ARROW = isUnicode ? '▸' : '>';
8
+ export const DASH = isUnicode ? '─' : '-';
9
+ export const BAR = isUnicode ? '│' : '|';
10
+ export const BULLET = isUnicode ? '•' : '*';
11
+ export const ELLIPSIS = '…';
12
+ export const SPINNER_BRAILLE = isUnicode
13
+ ? ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
14
+ : ['|', '/', '-', '\\'];
15
+ export const SPINNER_CIRCLE = isUnicode
16
+ ? ['◐', '◓', '◑', '◒']
17
+ : ['[', '-', 'o', ')'];
18
+ export const SPINNER_DOTS = isUnicode
19
+ ? ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
20
+ : ['.', '..', '...', ''];
21
+ export const BOX_TL = isUnicode ? '╭' : '+';
22
+ export const BOX_TR = isUnicode ? '╮' : '+';
23
+ export const BOX_BL = isUnicode ? '╰' : '+';
24
+ export const BOX_BR = isUnicode ? '╯' : '+';
25
+ export const BOX_H = isUnicode ? '─' : '-';
26
+ export const BOX_V = isUnicode ? '│' : '|';
27
+ export const H1_LINE = isUnicode ? '━━' : '==';
28
+ export const H2_BULLET = isUnicode ? '◆' : '>';
29
+ export const H3_BULLET = isUnicode ? '●' : 'o';
30
+ export const H4_ARROW = isUnicode ? '›' : '>';
31
+ export const BQ_LINE = isUnicode ? '┃' : '|';
32
+ export const HR_LINE = isUnicode ? '─' : '-';
33
+ export const UL_BULLET = (level) => {
34
+ if (!isUnicode) return '*';
35
+ return level === 0 ? '•' : level === 1 ? '◦' : '▸';
36
+ };
37
+ export const MODE_ICONS = isUnicode
38
+ ? { GENERAL: '⚙', CODING: '⟨/⟩', NETWORK: '◈', SSH: '⇌', PLAN: '◷', EXEC: '▶' }
39
+ : { GENERAL: '[.]', CODING: '</>', NETWORK: '(#)', SSH: '<->', PLAN: '(o)', EXEC: '>' };
40
+ export const HEADER_LOGO_L = isUnicode ? '▐' : '[';
41
+ export const HEADER_LOGO_M = isUnicode ? '△' : '^';
42
+ export const HEADER_LOGO_R = isUnicode ? '▌' : ']';