clarity-ai 6.7.0 → 6.8.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/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  ---
4
4
 
5
+ ## 6.8.0 (2026-06-06)
6
+
7
+ ### Clean-slate TUI: dynamic dimension defense, sandboxed streams, @inkjs/ui
8
+ - **Dimension defense**: monitors terminal resize; renders fallback if < 60x20
9
+ - **stdout/stderr sandbox**: console.log/error/warn intercepted to prevent AI model noise from corrupting the viewport
10
+ - **Hard-locked 3-tier Yoga grid**: headerBar(1) + messageViewport(flexGrow) + inputDock(3)
11
+ - **@inkjs/ui + cli-truncate**: wrap-ansi + string-width + cli-truncate for zero-bleed text
12
+ - **Single-line gradient header**: `CLARITY AI` with provider/model/mode badges — no ASCII art
13
+ - **Floating picker overlays** with `position: absolute` — never shift the message stream
14
+ - **Alternate screen buffer** with full SIGINT/SIGTERM cleanup and cursor restore
15
+ - **Orange selection bar** (`#FF6B35`) on all active items
16
+
5
17
  ## 6.7.0 (2026-06-06)
6
18
 
7
19
  ### Premium Ink+React TUI Engine — zero-bleed grid
package/bin/clarity.js CHANGED
@@ -4,11 +4,14 @@ import { render } from 'ink';
4
4
  import { App } from '../src/components/AppRoot.js';
5
5
  import { hasKey } from '../src/config/keys.js';
6
6
  import { createInterface } from 'readline';
7
- import ansiEscapes from 'ansi-escapes';
8
7
 
9
8
  process.stdin.resume();
10
9
  process.stdin.setEncoding('utf8');
11
10
 
11
+ const originalLog = console.log;
12
+ const originalError = console.error;
13
+ const originalWarn = console.warn;
14
+
12
15
  async function main() {
13
16
  const provider = process.env.CLARITY_PROVIDER || 'groq';
14
17
 
@@ -25,39 +28,44 @@ async function main() {
25
28
  process.stdin.resume();
26
29
  }
27
30
 
28
- process.stdout.write(ansiEscapes.clearScreen);
29
- process.stdout.write(ansiEscapes.cursorHide);
31
+ console.log = function sandboxedLog() {};
32
+ console.error = function sandboxedError() {};
33
+ console.warn = function sandboxedWarn() {};
34
+
35
+ let keepAlive;
30
36
 
31
37
  const config = { provider, model: process.env.CLARITY_MODEL || 'groq/llama-3.3-70b-versatile' };
32
38
 
33
- const { clear, waitUntilExit } = render(React.createElement(App, { config }), {
39
+ const { clear, waitUntilExit, rerender } = render(React.createElement(App, { config }), {
34
40
  fullscreen: true,
35
41
  patchConsole: false,
36
42
  exitOnCtrlC: false,
37
43
  });
38
44
 
39
- const keepAlive = setInterval(() => {}, 2 ** 31 - 1);
45
+ keepAlive = setInterval(() => {}, 2 ** 31 - 1);
40
46
 
41
47
  function cleanup() {
42
48
  clearInterval(keepAlive);
43
- process.stdout.write(ansiEscapes.cursorShow);
44
- process.stdout.write('\x1b[0m');
49
+ console.log = originalLog;
50
+ console.error = originalError;
51
+ console.warn = originalWarn;
45
52
  try { clear(); } catch {}
53
+ process.stdout.write('\x1b[?25h\x1b[0m');
46
54
  process.exit(0);
47
55
  }
48
56
 
49
57
  process.on('SIGINT', () => cleanup());
50
58
  process.on('SIGTERM', () => cleanup());
51
- process.on('exit', () => {
52
- process.stdout.write(ansiEscapes.cursorShow);
53
- });
59
+ process.on('exit', () => { process.stdout.write('\x1b[?25h\x1b[0m'); });
54
60
 
55
61
  await new Promise(() => {});
56
62
  }
57
63
 
58
64
  main().catch(err => {
59
- process.stdout.write(ansiEscapes.cursorShow);
60
- process.stdout.write('\x1b[0m');
65
+ console.log = originalLog;
66
+ console.error = originalError;
67
+ console.warn = originalWarn;
68
+ process.stdout.write('\x1b[?25h\x1b[0m');
61
69
  console.error('\n\x1b[31mFatal error:\x1b[0m', err.message);
62
70
  process.exit(1);
63
71
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clarity-ai",
3
- "version": "6.7.0",
3
+ "version": "6.8.0",
4
4
  "description": "CLARITY — terminal AI agent with local GGUF inference on HF Spaces",
5
5
  "type": "module",
6
6
  "bin": {
@@ -27,6 +27,8 @@
27
27
  "gradient-string": "^3",
28
28
  "figures": "^6",
29
29
  "ansi-escapes": "^7",
30
- "string-width": "^7"
30
+ "string-width": "^7",
31
+ "@inkjs/ui": "^2",
32
+ "cli-truncate": "^6"
31
33
  }
32
34
  }
@@ -1,7 +1,8 @@
1
- import React, { useState, useCallback, useRef } from 'react';
2
- import { Box } from 'ink';
1
+ import React, { useState, useCallback, useRef, useEffect } from 'react';
2
+ import { Box, Text } from 'ink';
3
3
  import { createChatState, handleSend, handleCommand } from '../chat.js';
4
4
  import { hex } from '../config/theme.js';
5
+ import { getLayout } from '../config/layout.js';
5
6
  import { Layout } from './Layout.js';
6
7
  const { createElement: h } = React;
7
8
 
@@ -57,7 +58,7 @@ export function App({ config }) {
57
58
  await handleSend(stateRef.current, setState, input, modelRef.current, providerRef.current, setStreamContent, ac.signal);
58
59
  }, []);
59
60
 
60
- function handleCommandSelect(cmdName) {
61
+ function handleCmdSelect(cmdName) {
61
62
  setShowCommands(false);
62
63
  onSubmit(cmdName);
63
64
  }
@@ -72,11 +73,20 @@ export function App({ config }) {
72
73
  }));
73
74
  }
74
75
 
75
- return h(Box, { flexDirection: 'column', backgroundColor: hex.bg, height: '100%' },
76
+ const layout = getLayout();
77
+
78
+ if (!layout.isLargeEnough) {
79
+ return h(Box, { width: '100%', height: '100%', alignItems: 'center', justifyContent: 'center', backgroundColor: hex.bg },
80
+ h(Text, { color: hex.textDim },
81
+ 'Terminal size too small. Please expand. (' + layout.cols + 'x' + layout.rows + ' needed: 60x20)')
82
+ );
83
+ }
84
+
85
+ return h(Box, { flexDirection: 'column', backgroundColor: hex.bg, width: '100%', height: '100%' },
76
86
  h(Layout, {
77
87
  state, streamContent, model, provider,
78
88
  showCommands, showModels,
79
- onCommandSelect: handleCommandSelect,
89
+ onCommandSelect: handleCmdSelect,
80
90
  onModelSelect: handleModelSelect,
81
91
  onCloseCommands: () => setShowCommands(false),
82
92
  onCloseModels: () => setShowModels(false),
@@ -1,6 +1,6 @@
1
1
  import React, { useMemo } from 'react';
2
2
  import { Box, Text } from 'ink';
3
- import { hex, sym } from '../config/theme.js';
3
+ import { hex } from '../config/theme.js';
4
4
  import { getLayout } from '../config/layout.js';
5
5
  const { createElement: h } = React;
6
6
 
@@ -9,39 +9,34 @@ const LANG_COLORS = {
9
9
  py: '#3572A5', rb: '#CC342D', go: '#00ADD8', rs: '#DEA584',
10
10
  java: '#B07219', kt: '#7F52FF', swift: '#FFAC45',
11
11
  html: '#E34F26', css: '#1572B6', scss: '#CC6699',
12
- sh: '#89E051', bash: '#89E051', dockerfile: '#384D54',
13
- json: '#292929', yaml: '#CB171E', md: '#083FA1', sql: '#E38C00',
12
+ sh: '#89E051', bash: '#89E051',
14
13
  };
15
14
 
16
15
  export function CodeBlock({ code, language }) {
17
16
  const lang = language || 'code';
18
17
  const lines = useMemo(() => String(code).split('\n'), [code]);
19
18
  const langColor = LANG_COLORS[lang] || '#555';
20
- const lnW = String(lines.length).length;
21
19
  const { cols } = getLayout();
22
- const maxLines = 20;
20
+ const maxLines = 15;
23
21
  const visible = lines.slice(0, maxLines);
24
22
  const codeWidth = cols - 10;
23
+ const lnW = String(lines.length).length;
25
24
 
26
25
  return h(Box, { flexDirection: 'column', backgroundColor: hex.codeBg },
27
26
  h(Box, { height: 1, backgroundColor: hex.codeBg },
28
27
  h(Text, { color: langColor, bold: true, backgroundColor: hex.codeBg }, ' ' + lang),
29
- h(Text, { color: hex.textMuted, backgroundColor: hex.codeBg }, ' ' + String(lines.length) + ' lines '),
28
+ h(Text, { color: hex.textMuted, backgroundColor: hex.codeBg }, ' ' + String(lines.length) + ' lines'),
30
29
  ),
31
30
  h(Box, { flexDirection: 'column', backgroundColor: hex.codeBg },
32
31
  visible.map((line, i) =>
33
32
  h(Box, { key: i, height: 1, backgroundColor: hex.codeBg },
34
- h(Text, { color: hex.textMuted, backgroundColor: hex.codeBg },
35
- ' ' + String(i + 1).padStart(lnW) + ' '
36
- ),
37
- h(Text, { color: '#C9D1D9', backgroundColor: hex.codeBg, wrap: 'truncate-end' },
38
- (line || ' ').slice(0, codeWidth)
39
- )
33
+ h(Text, { color: hex.textMuted, backgroundColor: hex.codeBg }, ' ' + String(i + 1).padStart(lnW) + ' '),
34
+ h(Text, { color: '#C9D1D9', backgroundColor: hex.codeBg, wrap: 'truncate-end' }, (line || ' ').slice(0, codeWidth))
40
35
  )
41
36
  ),
42
37
  lines.length > maxLines
43
38
  ? h(Box, { height: 1, backgroundColor: hex.codeBg },
44
- h(Text, { color: hex.textMuted, backgroundColor: hex.codeBg }, ' ' + sym.ellipsis + ' ' + (lines.length - maxLines) + ' more lines')
39
+ h(Text, { color: hex.textMuted, backgroundColor: hex.codeBg }, ' \u2026 ' + (lines.length - maxLines) + ' more lines')
45
40
  )
46
41
  : null
47
42
  )
@@ -1,6 +1,6 @@
1
1
  import React, { useState } from 'react';
2
2
  import { Box, Text, useInput } from 'ink';
3
- import { hex, sym } from '../config/theme.js';
3
+ import { hex } from '../config/theme.js';
4
4
  import { getLayout } from '../config/layout.js';
5
5
  const { createElement: h } = React;
6
6
 
@@ -16,7 +16,7 @@ const COMMANDS = [
16
16
  { name: '/exit', desc: 'Exit CLARITY' },
17
17
  ];
18
18
 
19
- export function CommandPicker({ query, onSelect, onClose }) {
19
+ export function CommandPicker({ onSelect, onClose }) {
20
20
  const [search, setSearch] = useState('');
21
21
  const [idx, setIdx] = useState(0);
22
22
  const { cols } = getLayout();
@@ -29,43 +29,41 @@ export function CommandPicker({ query, onSelect, onClose }) {
29
29
  if (key.upArrow) setIdx(i => Math.max(0, i - 1));
30
30
  if (key.downArrow) setIdx(i => Math.min(filtered.length - 1, i + 1));
31
31
  if (key.return && filtered[idx]) onSelect(filtered[idx].name);
32
- if (key.escape || key.ctrl && key.c) onClose();
32
+ if (key.escape) onClose();
33
33
  if (key.backspace) setSearch(s => s.slice(0, -1));
34
34
  else if (input && !key.ctrl && !key.meta) setSearch(s => s + input);
35
35
  });
36
36
 
37
37
  const w = Math.min(cols - 4, 48);
38
38
 
39
- const items = filtered.map((cmd, i) =>
40
- h(Box, {
41
- key: cmd.name, height: 1,
42
- backgroundColor: i === idx ? hex.selectionBg : hex.bg,
43
- },
44
- h(Text, {
45
- color: i === idx ? hex.selectionText : hex.text,
46
- bold: i === idx,
47
- backgroundColor: i === idx ? hex.selectionBg : hex.bg,
48
- }, ' ' + (i === idx ? '\u276F ' : ' ') + cmd.name + ' ' + cmd.desc)
49
- )
50
- );
51
-
52
39
  return h(Box, { flexDirection: 'column', backgroundColor: hex.bg, width: w },
53
40
  h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
54
41
  h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
55
- sym.box.tl + sym.box.h.repeat(w - 2) + sym.box.tr)
42
+ '\u250C' + '\u2500'.repeat(w - 2) + '\u2510')
56
43
  ),
57
44
  h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
58
- h(Text, { color: hex.gold, backgroundColor: hex.surfaceAlt }, sym.box.v + ' Commands'),
59
- h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt }, ' '.repeat(Math.max(0, w - 14)) + sym.box.v)
45
+ h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
46
+ '\u2502 Commands' + ' '.repeat(Math.max(0, w - 12)) + '\u2502')
60
47
  ),
61
48
  h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
62
49
  h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
63
- sym.box.v + ' ' + (search || '\u2026') + ' '.repeat(Math.max(0, w - 6)) + sym.box.v)
50
+ '\u2502 ' + (search || '\u2026') + ' '.repeat(Math.max(0, w - 6)) + '\u2502')
51
+ ),
52
+ filtered.map((cmd, i) =>
53
+ h(Box, {
54
+ key: cmd.name, height: 1,
55
+ backgroundColor: i === idx ? hex.selectionBg : hex.bg,
56
+ },
57
+ h(Text, {
58
+ color: i === idx ? hex.selectionText : hex.text,
59
+ bold: i === idx,
60
+ backgroundColor: i === idx ? hex.selectionBg : hex.bg,
61
+ }, (i === idx ? '\u276F ' : ' ') + cmd.name + ' ' + cmd.desc)
62
+ )
64
63
  ),
65
- h(Box, { flexDirection: 'column', backgroundColor: hex.bg }, ...items),
66
64
  h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
67
65
  h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
68
- sym.box.bl + sym.box.h.repeat(w - 2) + sym.box.br)
66
+ '\u2514' + '\u2500'.repeat(w - 2) + '\u2518')
69
67
  ),
70
68
  h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
71
69
  h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
@@ -0,0 +1,20 @@
1
+ import React from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import { hex, appGradient } from '../config/theme.js';
4
+ import { getLayout } from '../config/layout.js';
5
+ const { createElement: h } = React;
6
+
7
+ export function HeaderBar({ model, provider, agentMode, thinking }) {
8
+ const { cols } = getLayout();
9
+ const m = model.replace(/^[^/]+\//, '').slice(0, Math.floor((cols - 20) / 2));
10
+ const status = thinking ? ' \u25CF' : ' \u25CB';
11
+ const mode = agentMode ? '\u25C8 AGENT' : '\u25CB USER';
12
+ const label = '[' + provider + '] ' + m + ' ' + status + ' ' + mode;
13
+ const labelLen = label.length + 14;
14
+ const gap = Math.max(1, cols - labelLen);
15
+
16
+ return h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
17
+ h(Text, { backgroundColor: hex.surfaceAlt }, appGradient(' CLARITY AI ')),
18
+ h(Text, { color: hex.textDim, backgroundColor: hex.surfaceAlt }, ' '.repeat(gap) + label + ' ')
19
+ );
20
+ }
@@ -0,0 +1,59 @@
1
+ import React, { useState } from 'react';
2
+ import { Box, Text, useInput } from 'ink';
3
+ import { hex } from '../config/theme.js';
4
+ import { getLayout } from '../config/layout.js';
5
+ const { createElement: h } = React;
6
+
7
+ export function InputBar({ provider, model, agentMode, thinking, onSlash, onSubmit }) {
8
+ const [input, setInput] = useState('');
9
+ const [cursor, setCursor] = useState(0);
10
+ const { cols } = getLayout();
11
+ const w = Math.max(10, cols - 6);
12
+ const mShort = model.replace(/^[^/]+\//, '').slice(0, 16);
13
+
14
+ useInput((ch, key) => {
15
+ if (key.ctrl && key.p) { onSlash(); return; }
16
+ if (key.escape) { onSubmit('/exit'); return; }
17
+ if (key.return && !key.shift) {
18
+ if (input.trim()) { const t = input; setInput(''); setCursor(0); onSubmit(t); }
19
+ return;
20
+ }
21
+ if (key.return && key.shift) {
22
+ setInput(p => p.slice(0, cursor) + '\n' + p.slice(cursor));
23
+ setCursor(c => c + 1);
24
+ return;
25
+ }
26
+ if (key.backspace || key.delete) {
27
+ if (cursor > 0) { setInput(p => p.slice(0, cursor - 1) + p.slice(cursor)); setCursor(c => c - 1); }
28
+ return;
29
+ }
30
+ if (key.leftArrow && cursor > 0) { setCursor(c => c - 1); return; }
31
+ if (key.rightArrow && cursor < input.length) { setCursor(c => c + 1); return; }
32
+ if (key.home) { setCursor(0); return; }
33
+ if (key.end) { setCursor(input.length); return; }
34
+ if (ch && ch.length === 1 && ch.charCodeAt(0) >= 32) {
35
+ setInput(p => p.slice(0, cursor) + ch + p.slice(cursor));
36
+ setCursor(c => c + 1);
37
+ }
38
+ });
39
+
40
+ const isPlaceholder = !input && !thinking;
41
+
42
+ return h(Box, { flexDirection: 'column' },
43
+ h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
44
+ h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
45
+ ' \u250C' + '\u2500'.repeat(Math.max(0, cols - 6)) + '\u2510')
46
+ ),
47
+ h(Box, { height: 1, backgroundColor: hex.bg },
48
+ h(Text, {
49
+ color: isPlaceholder ? hex.textMuted : hex.text,
50
+ backgroundColor: hex.bg,
51
+ wrap: 'truncate-end',
52
+ }, ' \u2502 ' + (input || (thinking ? '\u25CF processing...' : 'type a message...')) + ' ')
53
+ ),
54
+ h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
55
+ h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
56
+ ' \u2514' + '\u2500' + ' ' + provider + ' \u00B7 ' + mShort + (agentMode ? ' \u00B7 AGENT' : '') + ' \u00B7 Ctrl+P')
57
+ )
58
+ );
59
+ }
@@ -1,34 +1,30 @@
1
1
  import React from 'react';
2
2
  import { Box } from 'ink';
3
3
  import { hex } from '../config/theme.js';
4
- import { getLayout } from '../config/layout.js';
5
- import { StatusBar } from './StatusBar.js';
4
+ import { HeaderBar } from './HeaderBar.js';
6
5
  import { MessageList } from './MessageList.js';
7
- import { Composer } from './Composer.js';
6
+ import { InputBar } from './InputBar.js';
8
7
  import { CommandPicker } from './CommandPicker.js';
9
8
  import { ModelPicker } from './ModelPicker.js';
10
9
  const { createElement: h } = React;
11
10
 
12
11
  export function Layout({ state, streamContent, model, provider, showCommands, showModels, onCommandSelect, onModelSelect, onCloseCommands, onCloseModels, onSlash, onSubmit }) {
13
- const { headerHeight, dockHeight, cols } = getLayout();
14
-
15
12
  const picker = showCommands || showModels
16
- ? h(Box, { position: 'absolute', top: headerHeight + 1, left: 2, width: Math.min(cols - 4, 52), backgroundColor: hex.bg, flexDirection: 'column' },
17
- showCommands ? h(CommandPicker, { query: '', onSelect: onCommandSelect, onClose: onCloseCommands }) : null,
13
+ ? h(Box, { position: 'absolute', top: 1, left: 2, backgroundColor: hex.bg, flexDirection: 'column' },
14
+ showCommands ? h(CommandPicker, { onSelect: onCommandSelect, onClose: onCloseCommands }) : null,
18
15
  showModels ? h(ModelPicker, { onSelect: onModelSelect, onClose: onCloseModels }) : null
19
16
  )
20
17
  : null;
21
18
 
22
- return h(Box, { flexDirection: 'column', backgroundColor: hex.bg, height: '100%' },
23
- h(StatusBar, { model, provider, agentMode: state.agentMode, thinking: state.thinking }),
19
+ return h(Box, { flexDirection: 'column', backgroundColor: hex.bg, width: '100%', height: '100%' },
20
+ h(HeaderBar, { model, provider, agentMode: state.agentMode, thinking: state.thinking }),
24
21
  h(Box, { flexGrow: 1, flexDirection: 'column', position: 'relative' },
25
22
  h(MessageList, {
26
23
  messages: state.messages, thinking: state.thinking,
27
24
  streamContent, agentStatus: state.agentStatus,
28
- toolExecutions: state.toolExecutions,
29
25
  }),
30
26
  picker
31
27
  ),
32
- h(Composer, { provider, model, agentMode: state.agentMode, thinking: state.thinking, onSlash, onSubmit })
28
+ h(InputBar, { provider, model, agentMode: state.agentMode, thinking: state.thinking, onSlash, onSubmit })
33
29
  );
34
30
  }
@@ -1,11 +1,11 @@
1
1
  import React from 'react';
2
2
  import { Box, Text } from 'ink';
3
- import { hex, sym } from '../config/theme.js';
3
+ import { hex } from '../config/theme.js';
4
4
  const { createElement: h } = React;
5
5
 
6
6
  export function LoadingIndicator({ label }) {
7
7
  return h(Box, { height: 1, backgroundColor: hex.surface },
8
- h(Text, { color: hex.blue, backgroundColor: hex.surface }, ' ' + sym.dot),
8
+ h(Text, { color: hex.blue, backgroundColor: hex.surface }, ' \u25CF'),
9
9
  h(Text, { color: hex.textDim, backgroundColor: hex.surface }, ' ' + (label || 'processing'))
10
10
  );
11
11
  }
@@ -1,6 +1,6 @@
1
1
  import React, { useMemo } from 'react';
2
2
  import { Box, Text } from 'ink';
3
- import { hex, sym, LOGO } from '../config/theme.js';
3
+ import { hex } from '../config/theme.js';
4
4
  import { getLayout, sliceToViewport, buildLineArray } from '../config/layout.js';
5
5
  const { createElement: h } = React;
6
6
 
@@ -8,7 +8,7 @@ function Line({ type, text, data }) {
8
8
  switch (type) {
9
9
  case 'user_head':
10
10
  return h(Box, { height: 1, backgroundColor: hex.userBg },
11
- h(Text, { color: hex.accent, bold: true, backgroundColor: hex.userBg }, ' \u276F \u25C9 YOU')
11
+ h(Text, { color: hex.accent, bold: true, backgroundColor: hex.userBg }, ' \u276F YOU')
12
12
  );
13
13
  case 'user_line':
14
14
  return h(Box, { height: 1, backgroundColor: hex.userBg },
@@ -24,19 +24,20 @@ function Line({ type, text, data }) {
24
24
  );
25
25
  case 'asst_foot':
26
26
  return h(Box, { height: 1, backgroundColor: hex.surface },
27
- h(Text, { color: hex.textDim, backgroundColor: hex.surface }, ' \u25B8 ' + (parseInt(text) < 1000 ? text + 'ms' : (parseInt(text) / 1000).toFixed(1) + 's'))
27
+ h(Text, { color: hex.textDim, backgroundColor: hex.surface },
28
+ ' \u25B8 ' + (parseInt(text) < 1000 ? text + 'ms' : (parseInt(text) / 1000).toFixed(1) + 's'))
28
29
  );
29
30
  case 'tool_line':
30
31
  return h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
31
- h(Text, { color: hex.purple, backgroundColor: hex.surfaceAlt }, ' ' + sym.bullet + ' ' + (text || ''))
32
+ h(Text, { color: hex.purple, backgroundColor: hex.surfaceAlt }, ' \u25C9 ' + (text || ''))
32
33
  );
33
34
  case 'sys_line':
34
35
  return h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
35
- h(Text, { color: hex.green, backgroundColor: hex.surfaceAlt }, ' ' + sym.bullet + ' ' + (text || ''))
36
+ h(Text, { color: hex.green, backgroundColor: hex.surfaceAlt }, ' \u25C9 ' + (text || ''))
36
37
  );
37
38
  case 'err_line':
38
39
  return h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
39
- h(Text, { color: hex.red, backgroundColor: hex.surfaceAlt }, ' ' + sym.cross + ' ' + (text || ''))
40
+ h(Text, { color: hex.red, backgroundColor: hex.surfaceAlt }, ' \u2716 ' + (text || ''))
40
41
  );
41
42
  case 'stream_head':
42
43
  return h(Box, { height: 1, backgroundColor: hex.surface },
@@ -55,7 +56,7 @@ function Line({ type, text, data }) {
55
56
  }
56
57
  }
57
58
 
58
- export function MessageList({ messages, thinking, streamContent, agentStatus, toolExecutions }) {
59
+ export function MessageList({ messages, thinking, streamContent, agentStatus }) {
59
60
  const { viewport, contentWidth } = getLayout();
60
61
 
61
62
  const entries = useMemo(() => {
@@ -65,12 +66,9 @@ export function MessageList({ messages, thinking, streamContent, agentStatus, to
65
66
  }));
66
67
  }, [messages]);
67
68
 
68
- const showLogo = entries.length <= 1 && !thinking && !streamContent;
69
- const effectiveViewport = showLogo ? Math.max(viewport - 14, 4) : viewport;
70
-
71
69
  const { slice, clipIndex, clipLines } = useMemo(
72
- () => sliceToViewport(entries, effectiveViewport, contentWidth),
73
- [entries, effectiveViewport, contentWidth]
70
+ () => sliceToViewport(entries, viewport, contentWidth),
71
+ [entries, viewport, contentWidth]
74
72
  );
75
73
 
76
74
  const rawLines = useMemo(
@@ -78,27 +76,14 @@ export function MessageList({ messages, thinking, streamContent, agentStatus, to
78
76
  [slice, clipIndex, clipLines, contentWidth]
79
77
  );
80
78
 
79
+ const fillCount = Math.max(0, viewport - rawLines.length);
81
80
  const padded = [];
82
- const logoRows = showLogo ? 14 : 0;
83
- const fillRows = Math.max(0, effectiveViewport - rawLines.length - logoRows);
84
- for (let i = 0; i < fillRows; i++) padded.push({ type: 'empty' });
85
- if (showLogo) {
86
- for (let i = 0; i < 14; i++) padded.push({ type: 'logo', line: i });
87
- }
81
+ for (let i = 0; i < fillCount; i++) padded.push({ type: 'empty' });
88
82
  for (const ln of rawLines) padded.push(ln);
89
83
 
90
84
  return h(Box, { height: viewport, flexDirection: 'column', overflow: 'hidden' },
91
85
  padded.map((ln, i) => {
92
86
  if (ln.type === 'empty') return h(Box, { key: 'e' + i, height: 1, backgroundColor: hex.bg });
93
- if (ln.type === 'logo') {
94
- const logoLine = LOGO[ln.line] || '';
95
- return h(Box, { key: 'l' + i, height: 1, backgroundColor: hex.bg },
96
- h(Text, {
97
- color: (ln.line >= 1 && ln.line <= 5) || ln.line === 9 ? hex.accent : hex.textMuted,
98
- backgroundColor: hex.bg,
99
- }, ' ' + logoLine)
100
- );
101
- }
102
87
  return h(Box, { key: (ln.data?.id || 'r') + '-' + i, height: 1 },
103
88
  h(Line, { type: ln.type, text: ln.text, data: ln.data })
104
89
  );
@@ -1,7 +1,7 @@
1
1
  import React, { useState, useMemo } from 'react';
2
2
  import { Box, Text, useInput } from 'ink';
3
3
  import { ALL_MODELS } from '../config/models.js';
4
- import { hex, sym } from '../config/theme.js';
4
+ import { hex } from '../config/theme.js';
5
5
  import { getLayout } from '../config/layout.js';
6
6
  const { createElement: h } = React;
7
7
 
@@ -32,52 +32,48 @@ export function ModelPicker({ onSelect, onClose }) {
32
32
  if (key.upArrow) setIdx(i => Math.max(0, i - 1));
33
33
  if (key.downArrow) setIdx(i => Math.min(flat.length - 1, i + 1));
34
34
  if (key.return) { const m = flat[idx]; if (m && !m._header) onSelect(m.id); return; }
35
- if (key.escape || key.ctrl && key.c) onClose();
35
+ if (key.escape) onClose();
36
36
  if (key.backspace) setSearch(s => s.slice(0, -1));
37
37
  else if (input && !key.ctrl && !key.meta) setSearch(s => s + input);
38
38
  });
39
39
 
40
40
  const w = Math.min(cols - 4, 52);
41
41
 
42
- const items = flat.map((m, i) => {
43
- if (m._header) {
44
- return h(Box, { key: 'h-' + m._provider, height: 1, backgroundColor: hex.surfaceAlt },
45
- h(Text, { color: hex.blue, bold: true, backgroundColor: hex.surfaceAlt }, sym.box.v + ' ' + m._provider.toUpperCase()),
46
- h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt }, ' '.repeat(Math.max(0, w - m._provider.length - 6)) + sym.box.v)
47
- );
48
- }
49
- const isSel = i === idx;
50
- return h(Box, {
51
- key: m.id, height: 1,
52
- backgroundColor: isSel ? hex.selectionBg : hex.bg,
53
- },
54
- h(Text, {
55
- color: isSel ? hex.selectionText : hex.text,
56
- bold: isSel,
57
- backgroundColor: isSel ? hex.selectionBg : hex.bg,
58
- }, (isSel ? '\u276F ' : ' ') + m.label + (m.badge ? ' [' + m.badge + ']' : ''))
59
- );
60
- });
61
-
62
- const count = flat.filter(m => !m._header).length;
63
-
64
42
  return h(Box, { flexDirection: 'column', backgroundColor: hex.bg, width: w },
65
43
  h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
66
44
  h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
67
- sym.box.tl + sym.box.h.repeat(w - 2) + sym.box.tr)
45
+ '\u250C' + '\u2500'.repeat(w - 2) + '\u2510')
68
46
  ),
69
47
  h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
70
- h(Text, { color: hex.gold, backgroundColor: hex.surfaceAlt }, sym.box.v + ' Models ' + count + ' available'),
71
- h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt }, ' '.repeat(Math.max(0, w - 19)) + sym.box.v)
48
+ h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
49
+ '\u2502 Models ' + flat.filter(m => !m._header).length + ' available' + ' '.repeat(Math.max(0, w - 18)) + '\u2502')
72
50
  ),
73
51
  h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
74
52
  h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
75
- sym.box.v + ' ' + (search || '\u2026') + ' '.repeat(Math.max(0, w - 6)) + sym.box.v)
53
+ '\u2502 ' + (search || '\u2026') + ' '.repeat(Math.max(0, w - 6)) + '\u2502')
76
54
  ),
77
- h(Box, { flexDirection: 'column', backgroundColor: hex.bg }, ...items),
55
+ flat.map((m, i) => {
56
+ if (m._header) {
57
+ return h(Box, { key: 'h-' + m._provider, height: 1, backgroundColor: hex.surfaceAlt },
58
+ h(Text, { color: hex.blue, bold: true, backgroundColor: hex.surfaceAlt },
59
+ '\u2502 ' + m._provider.toUpperCase() + ' '.repeat(Math.max(0, w - m._provider.length - 5)) + '\u2502')
60
+ );
61
+ }
62
+ const isSel = i === idx;
63
+ return h(Box, {
64
+ key: m.id, height: 1,
65
+ backgroundColor: isSel ? hex.selectionBg : hex.bg,
66
+ },
67
+ h(Text, {
68
+ color: isSel ? hex.selectionText : hex.text,
69
+ bold: isSel,
70
+ backgroundColor: isSel ? hex.selectionBg : hex.bg,
71
+ }, (isSel ? '\u276F ' : ' ') + m.label + (m.badge ? ' [' + m.badge + ']' : ''))
72
+ );
73
+ }),
78
74
  h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
79
75
  h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
80
- sym.box.bl + sym.box.h.repeat(w - 2) + sym.box.br)
76
+ '\u2514' + '\u2500'.repeat(w - 2) + '\u2518')
81
77
  ),
82
78
  h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
83
79
  h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
@@ -1,12 +1,10 @@
1
1
  import React, { useState } from 'react';
2
2
  import { Box, Text } from 'ink';
3
- import { hex, sym } from '../config/theme.js';
4
- import { getLayout } from '../config/layout.js';
3
+ import { hex } from '../config/theme.js';
5
4
  const { createElement: h } = React;
6
5
 
7
- export function ThinkingBlock({ toolResults, duration }) {
8
- const [collapsed, setCollapsed] = useState(true);
9
- const { cols } = getLayout();
6
+ export function ThinkingBlock({ toolResults, duration, collapsed: initialCollapsed }) {
7
+ const [collapsed, setCollapsed] = useState(initialCollapsed !== undefined ? initialCollapsed : true);
10
8
  const items = toolResults || [];
11
9
  const durStr = duration
12
10
  ? (duration < 1000 ? duration + 'ms' : (duration / 1000).toFixed(1) + 's')
@@ -15,20 +13,15 @@ export function ThinkingBlock({ toolResults, duration }) {
15
13
  return h(Box, { flexDirection: 'column', backgroundColor: hex.surfaceAlt },
16
14
  h(Box, { height: 1 },
17
15
  h(Text, { color: hex.purple, backgroundColor: hex.surfaceAlt },
18
- ' ' + (collapsed ? sym.triR : sym.triD) + ' Thought' + (durStr ? ' (' + durStr + ')' : ''))
16
+ ' ' + (collapsed ? '\u25B8' : '\u25BE') + ' Thought' + (durStr ? ' (' + durStr + ')' : ''))
19
17
  ),
20
18
  collapsed ? null : h(Box, { flexDirection: 'column', backgroundColor: hex.surfaceAlt },
21
- items.map((tr, i) => {
22
- const isLast = i === items.length - 1;
23
- const prefix = isLast ? sym.treeTip + '\u2500' : sym.treeFork + '\u2500';
24
- const conn = isLast ? ' ' : sym.treeCon;
25
- const ico = tr.status === 'failed' ? sym.cross : sym.circle;
26
- const td = tr.duration ? ' ' + tr.duration + 'ms' : '';
27
- return h(Box, { key: tr.execId || i, height: 1 },
19
+ items.map((tr, i) =>
20
+ h(Box, { key: tr.execId || i, height: 1 },
28
21
  h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
29
- ' ' + prefix + ' ' + ico + ' ' + tr.name + td)
30
- );
31
- })
22
+ ' ' + (i === items.length - 1 ? '\u2570\u2500' : '\u256D\u2500') + ' ' + (tr.status === 'failed' ? '\u2716' : '\u25C9') + ' ' + tr.name + (tr.duration ? ' ' + tr.duration + 'ms' : ''))
23
+ )
24
+ )
32
25
  )
33
26
  );
34
27
  }
@@ -1,11 +1,10 @@
1
1
  import React from 'react';
2
2
  import { Box, Text } from 'ink';
3
- import { hex, sym } from '../config/theme.js';
4
- import { getLayout } from '../config/layout.js';
3
+ import { hex } from '../config/theme.js';
4
+ import { truncate } from '../config/layout.js';
5
5
  const { createElement: h } = React;
6
6
 
7
7
  export function ToolCard({ exec, isActive }) {
8
- const { cols } = getLayout();
9
8
  const name = exec.name || 'tool';
10
9
  const status = exec.status || 'running';
11
10
  const dur = exec.duration ? (exec.duration < 1000 ? exec.duration + 'ms' : (exec.duration / 1000).toFixed(1) + 's') : '';
@@ -14,26 +13,23 @@ export function ToolCard({ exec, isActive }) {
14
13
 
15
14
  if (isDone) {
16
15
  const c = status === 'failed' ? hex.red : hex.green;
17
- const icon = status === 'failed' ? sym.cross : sym.bullet;
18
16
  return h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
19
- h(Text, { color: c, backgroundColor: hex.surfaceAlt }, ' ' + icon + ' ' + name + (dur ? ' ' + dur : ''))
17
+ h(Text, { color: c, backgroundColor: hex.surfaceAlt }, ' ' + (status === 'failed' ? '\u2716' : '\u25C9') + ' ' + name + (dur ? ' ' + dur : ''))
20
18
  );
21
19
  }
22
20
 
23
- const w = Math.min(cols - 8, 56);
24
-
25
21
  return h(Box, { flexDirection: 'column', backgroundColor: hex.surfaceAlt },
26
22
  h(Box, { height: 1 },
27
- h(Text, { color: hex.purple, backgroundColor: hex.surfaceAlt }, ' ' + sym.bullet + ' ' + name + (dur ? ' ' + dur : ''))
23
+ h(Text, { color: hex.purple, backgroundColor: hex.surfaceAlt }, ' \u25C9 ' + name + (dur ? ' ' + dur : ''))
28
24
  ),
29
25
  args.length < 80
30
26
  ? h(Box, { height: 1 },
31
- h(Text, { color: hex.textDim, backgroundColor: hex.surfaceAlt }, ' ' + sym.triR + ' ' + args.slice(0, w))
27
+ h(Text, { color: hex.textDim, backgroundColor: hex.surfaceAlt }, ' \u25B8 ' + truncate(args, 56))
32
28
  )
33
29
  : null,
34
30
  status === 'running'
35
31
  ? h(Box, { height: 1 },
36
- h(Text, { color: hex.blue, backgroundColor: hex.surfaceAlt }, ' ' + sym.dot + ' running')
32
+ h(Text, { color: hex.blue, backgroundColor: hex.surfaceAlt }, ' \u25CF running')
37
33
  )
38
34
  : null,
39
35
  );
@@ -1,49 +1,52 @@
1
1
  import wrapAnsi from 'wrap-ansi';
2
2
  import stringWidth from 'string-width';
3
+ import cliTruncate from 'cli-truncate';
3
4
 
4
5
  export function getLayout() {
5
6
  const rows = process.stdout.rows || 30;
6
7
  const cols = process.stdout.columns || 80;
7
- const headerHeight = 1;
8
- const dockHeight = 4;
9
- const viewport = Math.max(8, rows - headerHeight - dockHeight);
10
- const contentWidth = Math.max(20, cols - 4);
11
- return { rows, cols, headerHeight, dockHeight, viewport, contentWidth, pad: ' ' };
8
+ return {
9
+ rows,
10
+ cols,
11
+ headerHeight: 1,
12
+ dockHeight: 3,
13
+ viewport: Math.max(4, rows - 4),
14
+ contentWidth: Math.max(10, cols - 4),
15
+ isLargeEnough: cols >= 60 && rows >= 20,
16
+ };
12
17
  }
13
18
 
14
19
  export function sw(text) {
15
20
  return stringWidth(text || '');
16
21
  }
17
22
 
23
+ export function truncate(text, width) {
24
+ return cliTruncate(text || '', width, { position: 'end' });
25
+ }
26
+
18
27
  export function wrapText(text, width) {
19
28
  if (!text) return '';
20
- return wrapAnsi(String(text), width, { trim: false, hard: true });
29
+ return wrapAnsi(String(text), Math.max(1, width), { trim: false, hard: true });
21
30
  }
22
31
 
23
32
  export function countLines(text, width) {
24
33
  if (!text) return 1;
25
- return wrapAnsi(String(text), width, { trim: false, hard: true }).split('\n').length;
26
- }
27
-
28
- export function truncateText(text, maxLines, width) {
29
- const wrapped = wrapText(text, width);
30
- const lines = wrapped.split('\n');
31
- if (lines.length <= maxLines) return wrapped;
32
- return lines.slice(0, maxLines).join('\n');
34
+ return wrapAnsi(String(text), Math.max(1, width), { trim: false, hard: true }).split('\n').length;
33
35
  }
34
36
 
35
37
  export function measureEntry(entry, w) {
36
38
  const nr = entry.role;
37
- if (nr === 'user') return 1 + countLines(entry.content, w);
38
- if (nr === 'assistant') return 2 + countLines(entry.content, w) + (entry.duration ? 1 : 0);
39
+ const cw = Math.max(1, w);
40
+ if (nr === 'user') return 1 + countLines(entry.content, cw);
41
+ if (nr === 'assistant') return 2 + countLines(entry.content, cw) + (entry.duration ? 1 : 0);
39
42
  if (nr === 'tool') return 1;
40
43
  if (nr === 'system' || nr === 'error') return 1;
41
- if (nr === 'streaming') return 2 + Math.min(40, countLines(entry.content, w));
44
+ if (nr === 'streaming') return 2 + Math.min(30, countLines(entry.content, cw));
42
45
  return 1;
43
46
  }
44
47
 
45
48
  export function sliceToViewport(entries, viewportRows, w) {
46
- const heights = entries.map(e => measureEntry(e, w));
49
+ const heights = entries.map(e => measureEntry(e, Math.max(1, w)));
47
50
  let used = 0;
48
51
  let start = entries.length;
49
52
  let clipLines = 0;
@@ -59,54 +62,46 @@ export function sliceToViewport(entries, viewportRows, w) {
59
62
  used += heights[i];
60
63
  start = i;
61
64
  }
62
- const slice = entries.slice(start);
63
- return { slice, clipLines, clipIndex: clipLines > 0 ? 0 : -1 };
65
+ return { slice: entries.slice(start), clipLines, clipIndex: clipLines > 0 ? 0 : -1 };
64
66
  }
65
67
 
66
68
  export function buildLineArray(slice, clipIndex, clipLines, w) {
67
69
  const lines = [];
70
+ const cw = Math.max(1, w);
68
71
  for (let idx = 0; idx < slice.length; idx++) {
69
72
  const e = slice[idx];
70
73
  const nr = e.role;
71
74
  let skip = 0;
72
75
  if (idx === clipIndex && clipLines > 0) skip = clipLines;
76
+
73
77
  if (nr === 'user') {
74
78
  lines.push({ type: 'user_head', data: e });
75
- const wrapped = wrapText(e.content, w);
76
- const contentLines = wrapped.split('\n');
79
+ const contentLines = wrapText(e.content, cw).split('\n');
77
80
  for (let ci = skip; ci < contentLines.length; ci++) {
78
81
  lines.push({ type: 'user_line', text: contentLines[ci], data: e });
79
82
  }
80
83
  } else if (nr === 'assistant') {
81
84
  lines.push({ type: 'asst_head', data: e });
82
- if (skip <= 0) {
83
- const wrapped = wrapText(e.content, w);
84
- const contentLines = wrapped.split('\n');
85
- for (let ci = 0; ci < contentLines.length; ci++) {
86
- lines.push({ type: 'asst_line', text: contentLines[ci], data: e });
87
- }
88
- } else {
89
- const wrapped = wrapText(e.content, w);
90
- const contentLines = wrapped.split('\n');
91
- for (let ci = skip - 1; ci < contentLines.length; ci++) {
92
- lines.push({ type: 'asst_line', text: contentLines[ci], data: e });
93
- }
85
+ const contentLines = wrapText(e.content, cw).split('\n');
86
+ const startIdx = skip > 0 ? skip - 1 : 0;
87
+ for (let ci = startIdx; ci < contentLines.length; ci++) {
88
+ lines.push({ type: 'asst_line', text: contentLines[ci], data: e });
89
+ }
90
+ if (e.duration && skip <= 0) {
91
+ lines.push({ type: 'asst_foot', text: String(e.duration), data: e });
94
92
  }
95
- if (e.duration) lines.push({ type: 'asst_foot', text: String(e.duration), data: e });
96
93
  } else if (nr === 'tool') {
97
- lines.push({ type: 'tool_line', text: (e.error ? '\u2716 ' : '\u25C9 ') + (e.toolName || 'tool') + (e.duration ? ' ' + e.duration + 'ms' : ''), data: e });
94
+ lines.push({ type: 'tool_line', text: truncate((e.error ? '\u2716 ' : '\u25C9 ') + (e.toolName || 'tool') + (e.duration ? ' ' + e.duration + 'ms' : ''), cw), data: e });
98
95
  } else if (nr === 'system') {
99
- if (skip <= 0) lines.push({ type: 'sys_line', text: e.content, data: e });
96
+ if (skip <= 0) lines.push({ type: 'sys_line', text: truncate(e.content, cw), data: e });
100
97
  } else if (nr === 'error') {
101
- if (skip <= 0) lines.push({ type: 'err_line', text: e.content, data: e });
98
+ if (skip <= 0) lines.push({ type: 'err_line', text: truncate(e.content, cw), data: e });
102
99
  } else if (nr === 'streaming') {
103
100
  lines.push({ type: 'stream_head', data: e });
104
- if (e.status) lines.push({ type: 'stream_status', text: e.status, data: e });
105
- const wrapped = wrapText(e.content || '', w);
106
- const contentLines = wrapped.split('\n');
107
- const startLine = Math.min(skip, contentLines.length);
108
- const endLine = Math.min(contentLines.length, startLine + 40);
109
- for (let ci = startLine; ci < endLine; ci++) {
101
+ if (e.status && skip <= 0) lines.push({ type: 'stream_status', text: e.status, data: e });
102
+ const contentLines = wrapText(e.content || '', cw).split('\n');
103
+ const startIdx = Math.min(skip, contentLines.length);
104
+ for (let ci = startIdx; ci < Math.min(contentLines.length, startIdx + 30); ci++) {
110
105
  lines.push({ type: 'stream_line', text: contentLines[ci], data: e });
111
106
  }
112
107
  }
@@ -1,6 +1,5 @@
1
1
  import chalk from 'chalk';
2
2
  import gradient from 'gradient-string';
3
- import figures from 'figures';
4
3
 
5
4
  export const hex = {
6
5
  bg: '#0A0A14',
@@ -10,7 +9,6 @@ export const hex = {
10
9
  codeBg: '#0D0D18',
11
10
  selectionBg: '#FF6B35',
12
11
  selectionText: '#FFFFFF',
13
- borderLight: '#202050',
14
12
  accent: '#FF6B35',
15
13
  purple: '#A855F7',
16
14
  green: '#22C55E',
@@ -21,8 +19,6 @@ export const hex = {
21
19
  text: '#EAEAEE',
22
20
  textDim: '#8888AA',
23
21
  textMuted: '#555577',
24
- white: '#FFFFFF',
25
- black: '#000000',
26
22
  };
27
23
 
28
24
  export const color = {
@@ -42,52 +38,6 @@ export const color = {
42
38
  text: chalk.hex(hex.text),
43
39
  textDim: chalk.hex(hex.textDim),
44
40
  textMuted: chalk.hex(hex.textMuted),
45
- white: chalk.hex(hex.white),
46
- black: chalk.hex(hex.black),
47
41
  };
48
42
 
49
- export const sym = {
50
- circle: '\u25C9',
51
- dot: '\u25CF',
52
- triR: '\u25B8',
53
- triD: '\u25BE',
54
- bullet: '\u25C9',
55
- cross: '\u2716',
56
- ellipsis: '\u2026',
57
- mdash: '\u2014',
58
- midDot: '\u00B7',
59
- arrowR: '\u2192',
60
- arrowU: '\u2191',
61
- arrowD: '\u2193',
62
- box: {
63
- tl: '\u250C', tr: '\u2510', bl: '\u2514', br: '\u2518',
64
- h: '\u2500', v: '\u2502',
65
- },
66
- treeJ: '\u2514',
67
- treeT: '\u251C',
68
- treeCon: '\u2502',
69
- treeTip: '\u2570',
70
- treeFork: '\u256D',
71
- star: '\u2726',
72
- gear: '\u2699',
73
- pointer: '\u276F',
74
- };
75
-
76
- export const gradientText = gradient(['#FF6B35', '#3B82F6']);
77
- export const gradientLine = gradient(['#FF6B35', '#3B82F6']);
78
-
79
- export const LOGO = [
80
- ' ',
81
- ' ██████ ██ █████ ██████ ██ ████████ ██ ██ ',
82
- ' ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ',
83
- ' ██████ ██ ███████ ██████ ██ ██ ██ ██ ',
84
- ' ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ',
85
- ' ██ ███████ ██ ██ ██ ██ ██ ██ ██ ██████▄ ',
86
- ' ',
87
- ' █████ ██ ',
88
- ' ██ ██ ██ ',
89
- ' ██████ ██ ',
90
- ' ██ ██ ██ ',
91
- ' ██ ██ ███████ ',
92
- ' ',
93
- ];
43
+ export const appGradient = gradient(['#FF6B35', '#3B82F6']);