clarity-ai 6.6.0 → 6.7.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,19 @@
2
2
 
3
3
  ---
4
4
 
5
+ ## 6.7.0 (2026-06-06)
6
+
7
+ ### Premium Ink+React TUI Engine — zero-bleed grid
8
+ - **Hard-locked 3-tier grid**: Header(1) + Viewport(fill) + Dock(4) — absolute layout isolation
9
+ - **Floating picker overlays** via `position: absolute` — CommandPicker/ModelPicker never shift the message stream
10
+ - **Box-drawing borders** (`┌─┐│└─┘`) on all overlay panels
11
+ - **Orange `#FF6B35` selection bars** — full-width active item highlight
12
+ - **Gradient CLARITY header** via gradient-string (orange→blue)
13
+ - **Alternate screen buffer** (`fullscreen: true`) + SIGINT/SIGTERM cleanup
14
+ - **`string-width` render protection** — correct visual width for Unicode/emoji
15
+ - **Permanent ASCII logo** centered in empty viewport
16
+ - **ansi-escapes** for cursor hide/show, screen clear on boot
17
+
5
18
  ## 6.6.0 (2026-06-06)
6
19
 
7
20
  ### Premium UI rebuild + DeepSeek R1 reasoning models
package/bin/clarity.js CHANGED
@@ -4,6 +4,7 @@ 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';
7
8
 
8
9
  process.stdin.resume();
9
10
  process.stdin.setEncoding('utf8');
@@ -24,29 +25,39 @@ async function main() {
24
25
  process.stdin.resume();
25
26
  }
26
27
 
28
+ process.stdout.write(ansiEscapes.clearScreen);
29
+ process.stdout.write(ansiEscapes.cursorHide);
30
+
27
31
  const config = { provider, model: process.env.CLARITY_MODEL || 'groq/llama-3.3-70b-versatile' };
28
32
 
29
- const { clear } = render(React.createElement(App, { config }), {
33
+ const { clear, waitUntilExit } = render(React.createElement(App, { config }), {
30
34
  fullscreen: true,
31
35
  patchConsole: false,
36
+ exitOnCtrlC: false,
32
37
  });
33
38
 
34
- setInterval(() => {}, 2 ** 31 - 1);
39
+ const keepAlive = setInterval(() => {}, 2 ** 31 - 1);
35
40
 
36
41
  function cleanup() {
42
+ clearInterval(keepAlive);
43
+ process.stdout.write(ansiEscapes.cursorShow);
44
+ process.stdout.write('\x1b[0m');
37
45
  try { clear(); } catch {}
38
- process.stdout.write('\x1b[?25h\x1b[0m');
39
46
  process.exit(0);
40
47
  }
41
48
 
42
49
  process.on('SIGINT', () => cleanup());
43
50
  process.on('SIGTERM', () => cleanup());
51
+ process.on('exit', () => {
52
+ process.stdout.write(ansiEscapes.cursorShow);
53
+ });
44
54
 
45
55
  await new Promise(() => {});
46
56
  }
47
57
 
48
58
  main().catch(err => {
49
- process.stdout.write('\x1b[?25h\x1b[0m');
59
+ process.stdout.write(ansiEscapes.cursorShow);
60
+ process.stdout.write('\x1b[0m');
50
61
  console.error('\n\x1b[31mFatal error:\x1b[0m', err.message);
51
62
  process.exit(1);
52
63
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clarity-ai",
3
- "version": "6.6.0",
3
+ "version": "6.7.0",
4
4
  "description": "CLARITY — terminal AI agent with local GGUF inference on HF Spaces",
5
5
  "type": "module",
6
6
  "bin": {
@@ -23,6 +23,10 @@
23
23
  "react": "^18",
24
24
  "ink-spinner": "^5",
25
25
  "chalk": "^5",
26
- "wrap-ansi": "^9"
26
+ "wrap-ansi": "^9",
27
+ "gradient-string": "^3",
28
+ "figures": "^6",
29
+ "ansi-escapes": "^7",
30
+ "string-width": "^7"
27
31
  }
28
32
  }
@@ -34,11 +34,9 @@ export function App({ config }) {
34
34
  const stateRef = useRef(state);
35
35
  const modelRef = useRef(model);
36
36
  const providerRef = useRef(provider);
37
- const streamRef = useRef(streamContent);
38
37
  stateRef.current = state;
39
38
  modelRef.current = model;
40
39
  providerRef.current = provider;
41
- streamRef.current = streamContent;
42
40
 
43
41
  const onSubmit = useCallback(async (input) => {
44
42
  if (input === '/exit') { process.exit(0); return; }
@@ -74,7 +72,7 @@ export function App({ config }) {
74
72
  }));
75
73
  }
76
74
 
77
- return h(Box, { flexDirection: 'column', backgroundColor: hex.bg },
75
+ return h(Box, { flexDirection: 'column', backgroundColor: hex.bg, height: '100%' },
78
76
  h(Layout, {
79
77
  state, streamContent, model, provider,
80
78
  showCommands, showModels,
@@ -29,58 +29,47 @@ 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) onClose();
32
+ if (key.escape || key.ctrl && key.c) 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
- const isSel = i === idx;
41
- return h(Box, {
39
+ const items = filtered.map((cmd, i) =>
40
+ h(Box, {
42
41
  key: cmd.name, height: 1,
43
- backgroundColor: isSel ? hex.selectionBg : hex.bg,
42
+ backgroundColor: i === idx ? hex.selectionBg : hex.bg,
44
43
  },
45
44
  h(Text, {
46
- color: isSel ? hex.selectionText : hex.text,
47
- bold: isSel,
48
- backgroundColor: isSel ? hex.selectionBg : hex.bg,
49
- }, ' ' + (isSel ? '\u276F ' : ' ') + cmd.name),
50
- h(Text, {
51
- color: isSel ? hex.selectionText : hex.textDim,
52
- backgroundColor: isSel ? hex.selectionBg : hex.bg,
53
- }, ' ' + cmd.desc)
54
- );
55
- });
56
-
57
- const maxH = Math.min(items.length + 3, 16);
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
+ );
58
51
 
59
- return h(Box, { flexDirection: 'column', width: w, marginLeft: 2, backgroundColor: hex.bg },
52
+ return h(Box, { flexDirection: 'column', backgroundColor: hex.bg, width: w },
60
53
  h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
61
54
  h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
62
55
  sym.box.tl + sym.box.h.repeat(w - 2) + sym.box.tr)
63
56
  ),
64
57
  h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
65
- h(Text, { color: hex.gold, backgroundColor: hex.surfaceAlt }, sym.box.v + ' '),
66
- h(Text, { color: hex.text, backgroundColor: hex.surfaceAlt }, 'Commands'),
67
- h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
68
- ' type to filter...' + ' '.repeat(Math.max(0, w - 20 - 4)) + sym.box.v)
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)
69
60
  ),
70
61
  h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
71
62
  h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
72
- sym.box.v + ' ' + (search || sym.ellipsis) + ' '.repeat(Math.max(0, w - 6)) + sym.box.v)
73
- ),
74
- h(Box, { flexDirection: 'column', backgroundColor: hex.bg },
75
- ...items,
63
+ sym.box.v + ' ' + (search || '\u2026') + ' '.repeat(Math.max(0, w - 6)) + sym.box.v)
76
64
  ),
65
+ h(Box, { flexDirection: 'column', backgroundColor: hex.bg }, ...items),
77
66
  h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
78
67
  h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
79
68
  sym.box.bl + sym.box.h.repeat(w - 2) + sym.box.br)
80
69
  ),
81
70
  h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
82
71
  h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
83
- ' ' + sym.arrowU + sym.arrowD + ' nav ' + sym.arrowR + ' select Esc close')
72
+ ' \u2191\u2193 nav \u2192 select Esc close')
84
73
  ),
85
74
  );
86
75
  }
@@ -13,7 +13,7 @@ export function Composer({ provider, model, agentMode, thinking, onSlash, onSubm
13
13
  r.current = input;
14
14
 
15
15
  const { cols } = getLayout();
16
- const w = Math.max(10, cols - 8);
16
+ const w = Math.max(10, cols - 6);
17
17
  const lineCount = Math.max(1, Math.ceil((input.length || 1) / w));
18
18
  const visible = Math.min(lineCount, MAX_ROWS);
19
19
  const mShort = model.replace(/^[^/]+\//, '').slice(0, 18);
@@ -45,11 +45,11 @@ export function Composer({ provider, model, agentMode, thinking, onSlash, onSubm
45
45
  }
46
46
  });
47
47
 
48
- const sep = h(Box, { key: 'sep', height: 1, backgroundColor: hex.surfaceAlt },
49
- h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt }, ' ' + sym.box.tl + sym.box.h.repeat(Math.max(0, cols - 6)) + sym.box.tr)
50
- );
51
-
52
- const rows = [sep];
48
+ const rows = [];
49
+ rows.push(h(Box, { key: 'sep', height: 1, backgroundColor: hex.surfaceAlt },
50
+ h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
51
+ ' \u250C' + sym.box.h.repeat(Math.max(0, cols - 6)) + '\u2510')
52
+ ));
53
53
 
54
54
  for (let i = 0; i < MAX_ROWS; i++) {
55
55
  const start = i * w;
@@ -60,17 +60,15 @@ export function Composer({ provider, model, agentMode, thinking, onSlash, onSubm
60
60
  color: isPlaceholder ? hex.textMuted : hex.text,
61
61
  backgroundColor: hex.bg,
62
62
  wrap: 'truncate-end',
63
- }, ' \u2502 ' + (seg || (i === 0 && isPlaceholder ? 'type a message...' : ' ')) + ' ')
63
+ }, ' \u2502 ' + (seg || (i === 0 && isPlaceholder ? 'type a message...' : ' ')))
64
64
  )
65
65
  );
66
66
  }
67
67
 
68
- rows.push(
69
- h(Box, { key: 'st', height: 1, backgroundColor: hex.surfaceAlt },
70
- h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
71
- ' ' + sym.box.bl + sym.box.h + ' ' + provider + ' ' + sym.midDot + ' ' + mShort + (agentMode ? ' ' + sym.midDot + ' AGENT' : '') + ' ' + sym.midDot + ' Ctrl+P')
72
- )
73
- );
68
+ rows.push(h(Box, { key: 'st', height: 1, backgroundColor: hex.surfaceAlt },
69
+ h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
70
+ ' \u2514' + sym.box.h + ' ' + provider + ' \u00B7 ' + mShort + (agentMode ? ' \u00B7 AGENT' : '') + ' \u00B7 Ctrl+P')
71
+ ));
74
72
 
75
73
  return h(Box, { flexDirection: 'column', backgroundColor: hex.surfaceAlt }, ...rows);
76
74
  }
@@ -1,6 +1,7 @@
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';
4
5
  import { StatusBar } from './StatusBar.js';
5
6
  import { MessageList } from './MessageList.js';
6
7
  import { Composer } from './Composer.js';
@@ -9,21 +10,25 @@ import { ModelPicker } from './ModelPicker.js';
9
10
  const { createElement: h } = React;
10
11
 
11
12
  export function Layout({ state, streamContent, model, provider, showCommands, showModels, onCommandSelect, onModelSelect, onCloseCommands, onCloseModels, onSlash, onSubmit }) {
12
- return h(Box, { flexDirection: 'column', backgroundColor: hex.bg },
13
+ const { headerHeight, dockHeight, cols } = getLayout();
14
+
15
+ 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,
18
+ showModels ? h(ModelPicker, { onSelect: onModelSelect, onClose: onCloseModels }) : null
19
+ )
20
+ : null;
21
+
22
+ return h(Box, { flexDirection: 'column', backgroundColor: hex.bg, height: '100%' },
13
23
  h(StatusBar, { model, provider, agentMode: state.agentMode, thinking: state.thinking }),
14
- h(Box, { flexGrow: 1, flexDirection: 'column' },
24
+ h(Box, { flexGrow: 1, flexDirection: 'column', position: 'relative' },
15
25
  h(MessageList, {
16
26
  messages: state.messages, thinking: state.thinking,
17
27
  streamContent, agentStatus: state.agentStatus,
18
28
  toolExecutions: state.toolExecutions,
19
- })
29
+ }),
30
+ picker
20
31
  ),
21
- showCommands || showModels
22
- ? h(Box, { flexDirection: 'column', backgroundColor: hex.bg },
23
- showCommands ? h(CommandPicker, { query: '', onSelect: onCommandSelect, onClose: onCloseCommands }) : null,
24
- showModels ? h(ModelPicker, { onSelect: onModelSelect, onClose: onCloseModels }) : null
25
- )
26
- : null,
27
32
  h(Composer, { provider, model, agentMode: state.agentMode, thinking: state.thinking, onSlash, onSubmit })
28
33
  );
29
34
  }
@@ -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 ' + sym.bullet + ' YOU')
11
+ h(Text, { color: hex.accent, bold: true, backgroundColor: hex.userBg }, ' \u276F \u25C9 YOU')
12
12
  );
13
13
  case 'user_line':
14
14
  return h(Box, { height: 1, backgroundColor: hex.userBg },
@@ -16,7 +16,7 @@ function Line({ type, text, data }) {
16
16
  );
17
17
  case 'asst_head':
18
18
  return h(Box, { height: 1, backgroundColor: hex.surface },
19
- h(Text, { color: hex.purple, bold: true, backgroundColor: hex.surface }, ' ' + sym.diamond + ' CLARITY')
19
+ h(Text, { color: hex.purple, bold: true, backgroundColor: hex.surface }, ' \u25C6 CLARITY')
20
20
  );
21
21
  case 'asst_line':
22
22
  return h(Box, { height: 1, backgroundColor: hex.surface },
@@ -24,7 +24,7 @@ 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 }, ' ' + sym.triR + ' ' + (parseInt(text) < 1000 ? text + 'ms' : (parseInt(text) / 1000).toFixed(1) + 's'))
27
+ h(Text, { color: hex.textDim, backgroundColor: hex.surface }, ' \u25B8 ' + (parseInt(text) < 1000 ? text + 'ms' : (parseInt(text) / 1000).toFixed(1) + 's'))
28
28
  );
29
29
  case 'tool_line':
30
30
  return h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
@@ -40,11 +40,11 @@ function Line({ type, text, data }) {
40
40
  );
41
41
  case 'stream_head':
42
42
  return h(Box, { height: 1, backgroundColor: hex.surface },
43
- h(Text, { color: hex.purple, bold: true, backgroundColor: hex.surface }, ' ' + sym.diamond + ' CLARITY')
43
+ h(Text, { color: hex.purple, bold: true, backgroundColor: hex.surface }, ' \u25C6 CLARITY')
44
44
  );
45
45
  case 'stream_status':
46
46
  return h(Box, { height: 1, backgroundColor: hex.surface },
47
- h(Text, { color: hex.blue, backgroundColor: hex.surface }, ' ' + sym.dot + ' ' + (text || ''))
47
+ h(Text, { color: hex.blue, backgroundColor: hex.surface }, ' \u25CF ' + (text || ''))
48
48
  );
49
49
  case 'stream_line':
50
50
  return h(Box, { height: 1, backgroundColor: hex.surface },
@@ -66,46 +66,40 @@ export function MessageList({ messages, thinking, streamContent, agentStatus, to
66
66
  }, [messages]);
67
67
 
68
68
  const showLogo = entries.length <= 1 && !thinking && !streamContent;
69
+ const effectiveViewport = showLogo ? Math.max(viewport - 14, 4) : viewport;
69
70
 
70
71
  const { slice, clipIndex, clipLines } = useMemo(
71
- () => sliceToViewport(entries, Math.max(viewport - (showLogo ? 14 : 0), viewport), contentWidth),
72
- [entries, viewport, contentWidth, showLogo]
72
+ () => sliceToViewport(entries, effectiveViewport, contentWidth),
73
+ [entries, effectiveViewport, contentWidth]
73
74
  );
74
75
 
75
- const lines = useMemo(
76
+ const rawLines = useMemo(
76
77
  () => buildLineArray(slice, clipIndex, clipLines, contentWidth),
77
78
  [slice, clipIndex, clipLines, contentWidth]
78
79
  );
79
80
 
80
- const padded = [...lines];
81
+ 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' });
81
85
  if (showLogo) {
82
- while (padded.length < Math.max(viewport - 14, 0)) {
83
- padded.unshift({ type: 'empty' });
84
- }
85
- } else {
86
- for (let i = padded.length; i < viewport; i++) {
87
- padded.unshift({ type: 'empty' });
88
- }
86
+ for (let i = 0; i < 14; i++) padded.push({ type: 'logo', line: i });
89
87
  }
88
+ for (const ln of rawLines) padded.push(ln);
90
89
 
91
- const logoVisible = showLogo;
92
- const totalHeight = viewport;
93
-
94
- return h(Box, { height: totalHeight, flexDirection: 'column', overflow: 'hidden' },
90
+ return h(Box, { height: viewport, flexDirection: 'column', overflow: 'hidden' },
95
91
  padded.map((ln, i) => {
96
- if (ln.type === 'empty') {
97
- if (logoVisible && i < 14) {
98
- const logoLine = LOGO[i] || '';
99
- return h(Box, { key: 'e' + i, height: 1, backgroundColor: hex.bg },
100
- h(Text, {
101
- color: (i === 1 || i === 9 || i === 10 || i === 11) ? hex.accent : hex.textMuted,
102
- backgroundColor: hex.bg,
103
- }, ' ' + logoLine)
104
- );
105
- }
106
- return h(Box, { key: 'e' + i, height: 1, backgroundColor: hex.bg });
92
+ 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
+ );
107
101
  }
108
- return h(Box, { key: (ln.data?.id || 'l') + '-' + i, height: 1 },
102
+ return h(Box, { key: (ln.data?.id || 'r') + '-' + i, height: 1 },
109
103
  h(Line, { type: ln.type, text: ln.text, data: ln.data })
110
104
  );
111
105
  })
@@ -32,7 +32,7 @@ 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) onClose();
35
+ if (key.escape || key.ctrl && key.c) 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
  });
@@ -43,7 +43,7 @@ export function ModelPicker({ onSelect, onClose }) {
43
43
  if (m._header) {
44
44
  return h(Box, { key: 'h-' + m._provider, height: 1, backgroundColor: hex.surfaceAlt },
45
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 - 8)) + sym.box.v)
46
+ h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt }, ' '.repeat(Math.max(0, w - m._provider.length - 6)) + sym.box.v)
47
47
  );
48
48
  }
49
49
  const isSel = i === idx;
@@ -55,37 +55,33 @@ export function ModelPicker({ onSelect, onClose }) {
55
55
  color: isSel ? hex.selectionText : hex.text,
56
56
  bold: isSel,
57
57
  backgroundColor: isSel ? hex.selectionBg : hex.bg,
58
- }, (isSel ? '\u276F ' : ' ') + m.label + (m.badge ? ' [' + m.badge + ']' : '') + ' '.repeat(Math.max(0, w - m.label.length - (m.badge ? m.badge.length + 4 : 0) - 4)) + (isSel ? '\u276F' : ' '))
58
+ }, (isSel ? '\u276F ' : ' ') + m.label + (m.badge ? ' [' + m.badge + ']' : ''))
59
59
  );
60
60
  });
61
61
 
62
62
  const count = flat.filter(m => !m._header).length;
63
63
 
64
- return h(Box, { flexDirection: 'column', width: w, marginLeft: 2, backgroundColor: hex.bg },
64
+ return h(Box, { flexDirection: 'column', backgroundColor: hex.bg, width: w },
65
65
  h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
66
66
  h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
67
67
  sym.box.tl + sym.box.h.repeat(w - 2) + sym.box.tr)
68
68
  ),
69
69
  h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
70
- h(Text, { color: hex.gold, backgroundColor: hex.surfaceAlt }, sym.box.v + ' '),
71
- h(Text, { color: hex.text, backgroundColor: hex.surfaceAlt }, 'Models'),
72
- h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
73
- ' ' + count + ' available' + ' '.repeat(Math.max(0, w - 14 - 8)) + sym.box.v)
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)
74
72
  ),
75
73
  h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
76
74
  h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
77
- sym.box.v + ' ' + (search || sym.ellipsis) + ' '.repeat(Math.max(0, w - 6)) + sym.box.v)
78
- ),
79
- h(Box, { flexDirection: 'column', backgroundColor: hex.bg },
80
- ...items,
75
+ sym.box.v + ' ' + (search || '\u2026') + ' '.repeat(Math.max(0, w - 6)) + sym.box.v)
81
76
  ),
77
+ h(Box, { flexDirection: 'column', backgroundColor: hex.bg }, ...items),
82
78
  h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
83
79
  h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
84
80
  sym.box.bl + sym.box.h.repeat(w - 2) + sym.box.br)
85
81
  ),
86
82
  h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
87
83
  h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
88
- ' ' + sym.arrowU + sym.arrowD + ' nav ' + sym.arrowR + ' select Esc close')
84
+ ' \u2191\u2193 nav \u2192 select Esc close')
89
85
  ),
90
86
  );
91
87
  }
@@ -1,16 +1,19 @@
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
  import { getLayout } from '../config/layout.js';
5
5
  const { createElement: h } = React;
6
6
 
7
7
  export function StatusBar({ model, provider, agentMode, thinking }) {
8
8
  const { cols } = getLayout();
9
- const m = model.replace(/^[^/]+\//, '').slice(0, 22);
10
- const left = sym.circle + ' CLARITY ' + sym.midDot + ' ' + m + ' ' + sym.midDot + ' ' + provider;
11
- const right = (agentMode ? '\u25C8 AGENT' : '\u25CB USER') + (thinking ? ' ' + sym.dot : '');
12
- const gap = Math.max(1, cols - left.length - right.length - 4);
9
+ const m = model.replace(/^[^/]+\//, '').slice(0, 20);
10
+ const left = '\u25C9 CLARITY \u00B7 ' + m + ' \u00B7 ' + provider;
11
+ const right = (agentMode ? '\u25C8 AGENT' : '\u25CB USER') + (thinking ? ' \u25CF' : '');
12
+ const gap = Math.max(1, cols - left.length - right.length - 2);
13
+
13
14
  return h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
14
- h(Text, { color: hex.textDim, backgroundColor: hex.surfaceAlt }, ' ' + left + ' '.repeat(gap) + right + ' ')
15
+ h(Text, { color: hex.accent, bold: true, backgroundColor: hex.surfaceAlt }, ' ' + left),
16
+ h(Text, { backgroundColor: hex.surfaceAlt }, ' '.repeat(gap)),
17
+ h(Text, { color: hex.textDim, backgroundColor: hex.surfaceAlt }, right + ' ')
15
18
  );
16
19
  }
@@ -12,32 +12,21 @@ export function ThinkingBlock({ toolResults, duration }) {
12
12
  ? (duration < 1000 ? duration + 'ms' : (duration / 1000).toFixed(1) + 's')
13
13
  : '';
14
14
 
15
- const headerText = sym.triR + ' Thought' + (durStr ? ' (' + durStr + ')' : '');
16
- const icon = collapsed ? sym.triR : sym.triD;
17
-
18
15
  return h(Box, { flexDirection: 'column', backgroundColor: hex.surfaceAlt },
19
16
  h(Box, { height: 1 },
20
- h(Text, { color: hex.purple, backgroundColor: hex.surfaceAlt }, ' ' + icon + ' ' + headerText)
17
+ h(Text, { color: hex.purple, backgroundColor: hex.surfaceAlt },
18
+ ' ' + (collapsed ? sym.triR : sym.triD) + ' Thought' + (durStr ? ' (' + durStr + ')' : ''))
21
19
  ),
22
20
  collapsed ? null : h(Box, { flexDirection: 'column', backgroundColor: hex.surfaceAlt },
23
21
  items.map((tr, i) => {
24
22
  const isLast = i === items.length - 1;
25
- const prefix = isLast ? sym.treeTip + sym.u.h : sym.treeFork + sym.u.h;
23
+ const prefix = isLast ? sym.treeTip + '\u2500' : sym.treeFork + '\u2500';
26
24
  const conn = isLast ? ' ' : sym.treeCon;
27
25
  const ico = tr.status === 'failed' ? sym.cross : sym.circle;
28
- const col = tr.status === 'failed' ? hex.red : hex.green;
29
26
  const td = tr.duration ? ' ' + tr.duration + 'ms' : '';
30
- const line = ' ' + prefix + ' ' + ico + ' ' + tr.name + td;
31
- const contentLine = tr.content && tr.content.length < 200
32
- ? ' ' + conn + ' ' + String(tr.content).slice(0, cols - 14)
33
- : null;
34
- return h(Box, { key: tr.execId || i, flexDirection: 'column' },
35
- h(Box, { height: 1 },
36
- h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt }, line)
37
- ),
38
- contentLine ? h(Box, { height: 1 },
39
- h(Text, { color: hex.textDim, backgroundColor: hex.surfaceAlt }, contentLine)
40
- ) : null
27
+ return h(Box, { key: tr.execId || i, height: 1 },
28
+ h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
29
+ ' ' + prefix + ' ' + ico + ' ' + tr.name + td)
41
30
  );
42
31
  })
43
32
  )
@@ -36,10 +36,5 @@ export function ToolCard({ exec, isActive }) {
36
36
  h(Text, { color: hex.blue, backgroundColor: hex.surfaceAlt }, ' ' + sym.dot + ' running')
37
37
  )
38
38
  : null,
39
- status === 'failed' && exec.error
40
- ? h(Box, { height: 1 },
41
- h(Text, { color: hex.red, backgroundColor: hex.surfaceAlt }, ' ' + sym.cross + ' ' + String(exec.error).slice(0, w - 4))
42
- )
43
- : null,
44
39
  );
45
40
  }
@@ -1,18 +1,18 @@
1
1
  import wrapAnsi from 'wrap-ansi';
2
+ import stringWidth from 'string-width';
2
3
 
3
4
  export function getLayout() {
4
5
  const rows = process.stdout.rows || 30;
5
6
  const cols = process.stdout.columns || 80;
6
- return {
7
- rows,
8
- cols,
9
- topBar: 1,
10
- dock: 4,
11
- viewport: Math.max(8, rows - 5),
12
- contentWidth: Math.max(20, cols - 4),
13
- padLeft: ' ',
14
- padRight: ' ',
15
- };
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: ' ' };
12
+ }
13
+
14
+ export function sw(text) {
15
+ return stringWidth(text || '');
16
16
  }
17
17
 
18
18
  export function wrapText(text, width) {
@@ -34,9 +34,9 @@ export function truncateText(text, maxLines, width) {
34
34
 
35
35
  export function measureEntry(entry, w) {
36
36
  const nr = entry.role;
37
- if (nr === 'user') return 1 + countLines(entry.content, w) + 0;
37
+ if (nr === 'user') return 1 + countLines(entry.content, w);
38
38
  if (nr === 'assistant') return 2 + countLines(entry.content, w) + (entry.duration ? 1 : 0);
39
- if (nr === 'tool') return 2 + (entry.completed ? 0 : Math.min(6, countLines(entry.content || '', w) + 2));
39
+ if (nr === 'tool') return 1;
40
40
  if (nr === 'system' || nr === 'error') return 1;
41
41
  if (nr === 'streaming') return 2 + Math.min(40, countLines(entry.content, w));
42
42
  return 1;
@@ -94,12 +94,7 @@ export function buildLineArray(slice, clipIndex, clipLines, w) {
94
94
  }
95
95
  if (e.duration) lines.push({ type: 'asst_foot', text: String(e.duration), data: e });
96
96
  } else if (nr === 'tool') {
97
- const label = (e.error ? '\u2716 ' : '\u25C9 ') + (e.toolName || 'tool');
98
- if (e.completed) {
99
- if (skip <= 0) lines.push({ type: 'tool_line', text: label + ' ' + (e.duration ? e.duration + 'ms' : ''), data: e });
100
- } else {
101
- lines.push({ type: 'tool_line', text: label, data: e });
102
- }
97
+ lines.push({ type: 'tool_line', text: (e.error ? '\u2716 ' : '\u25C9 ') + (e.toolName || 'tool') + (e.duration ? ' ' + e.duration + 'ms' : ''), data: e });
103
98
  } else if (nr === 'system') {
104
99
  if (skip <= 0) lines.push({ type: 'sys_line', text: e.content, data: e });
105
100
  } else if (nr === 'error') {
@@ -109,9 +104,8 @@ export function buildLineArray(slice, clipIndex, clipLines, w) {
109
104
  if (e.status) lines.push({ type: 'stream_status', text: e.status, data: e });
110
105
  const wrapped = wrapText(e.content || '', w);
111
106
  const contentLines = wrapped.split('\n');
112
- const maxLines = 40;
113
107
  const startLine = Math.min(skip, contentLines.length);
114
- const endLine = Math.min(contentLines.length, startLine + (maxLines - lines.length));
108
+ const endLine = Math.min(contentLines.length, startLine + 40);
115
109
  for (let ci = startLine; ci < endLine; ci++) {
116
110
  lines.push({ type: 'stream_line', text: contentLines[ci], data: e });
117
111
  }
@@ -1,4 +1,6 @@
1
1
  import chalk from 'chalk';
2
+ import gradient from 'gradient-string';
3
+ import figures from 'figures';
2
4
 
3
5
  export const hex = {
4
6
  bg: '#0A0A14',
@@ -21,9 +23,6 @@ export const hex = {
21
23
  textMuted: '#555577',
22
24
  white: '#FFFFFF',
23
25
  black: '#000000',
24
- modalOverlay: 'rgba(0,0,0,0.85)',
25
- logoOrange: '#FF6B35',
26
- logoBlue: '#3B82F6',
27
26
  };
28
27
 
29
28
  export const color = {
@@ -48,55 +47,42 @@ export const color = {
48
47
  };
49
48
 
50
49
  export const sym = {
51
- diamond: '\u25C6',
52
50
  circle: '\u25C9',
53
51
  dot: '\u25CF',
54
- smallDot: '\u25CB',
55
52
  triR: '\u25B8',
56
53
  triD: '\u25BE',
57
54
  bullet: '\u25C9',
58
55
  cross: '\u2716',
59
56
  ellipsis: '\u2026',
60
57
  mdash: '\u2014',
61
- ndash: '\u2013',
62
58
  midDot: '\u00B7',
63
59
  arrowR: '\u2192',
64
- arrowL: '\u2190',
65
60
  arrowU: '\u2191',
66
61
  arrowD: '\u2193',
67
- lightV: '\u2502',
68
- lightH: '\u2500',
69
62
  box: {
70
63
  tl: '\u250C', tr: '\u2510', bl: '\u2514', br: '\u2518',
71
64
  h: '\u2500', v: '\u2502',
72
- tm: '\u252C', bm: '\u2534', lm: '\u251C', rm: '\u2524',
73
- cross: '\u253C',
74
- },
75
- powerline: {
76
- padlock: '\uE0B0',
77
- rpadlock: '\uE0B2',
78
65
  },
79
66
  treeJ: '\u2514',
80
67
  treeT: '\u251C',
81
68
  treeCon: '\u2502',
82
- triR2: '\u25B8',
83
- triD2: '\u25BE',
84
- u: { h: '\u2500' },
85
69
  treeTip: '\u2570',
86
70
  treeFork: '\u256D',
87
71
  star: '\u2726',
88
- asterisk: '\u2731',
89
72
  gear: '\u2699',
90
- thought: '\u25CB',
73
+ pointer: '\u276F',
91
74
  };
92
75
 
76
+ export const gradientText = gradient(['#FF6B35', '#3B82F6']);
77
+ export const gradientLine = gradient(['#FF6B35', '#3B82F6']);
78
+
93
79
  export const LOGO = [
94
80
  ' ',
95
81
  ' ██████ ██ █████ ██████ ██ ████████ ██ ██ ',
96
82
  ' ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ',
97
83
  ' ██████ ██ ███████ ██████ ██ ██ ██ ██ ',
98
84
  ' ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ',
99
- ' ██ ███████ ██ ██ ██ ██ ██ ██ ██████ ',
85
+ ' ██ ███████ ██ ██ ██ ██ ██ ██ ██ ██████▄ ',
100
86
  ' ',
101
87
  ' █████ ██ ',
102
88
  ' ██ ██ ██ ',