clarity-ai 6.6.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,31 @@
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
+
17
+ ## 6.7.0 (2026-06-06)
18
+
19
+ ### Premium Ink+React TUI Engine — zero-bleed grid
20
+ - **Hard-locked 3-tier grid**: Header(1) + Viewport(fill) + Dock(4) — absolute layout isolation
21
+ - **Floating picker overlays** via `position: absolute` — CommandPicker/ModelPicker never shift the message stream
22
+ - **Box-drawing borders** (`┌─┐│└─┘`) on all overlay panels
23
+ - **Orange `#FF6B35` selection bars** — full-width active item highlight
24
+ - **Gradient CLARITY header** via gradient-string (orange→blue)
25
+ - **Alternate screen buffer** (`fullscreen: true`) + SIGINT/SIGTERM cleanup
26
+ - **`string-width` render protection** — correct visual width for Unicode/emoji
27
+ - **Permanent ASCII logo** centered in empty viewport
28
+ - **ansi-escapes** for cursor hide/show, screen clear on boot
29
+
5
30
  ## 6.6.0 (2026-06-06)
6
31
 
7
32
  ### Premium UI rebuild + DeepSeek R1 reasoning models
package/bin/clarity.js CHANGED
@@ -8,6 +8,10 @@ import { createInterface } from 'readline';
8
8
  process.stdin.resume();
9
9
  process.stdin.setEncoding('utf8');
10
10
 
11
+ const originalLog = console.log;
12
+ const originalError = console.error;
13
+ const originalWarn = console.warn;
14
+
11
15
  async function main() {
12
16
  const provider = process.env.CLARITY_PROVIDER || 'groq';
13
17
 
@@ -24,16 +28,27 @@ async function main() {
24
28
  process.stdin.resume();
25
29
  }
26
30
 
31
+ console.log = function sandboxedLog() {};
32
+ console.error = function sandboxedError() {};
33
+ console.warn = function sandboxedWarn() {};
34
+
35
+ let keepAlive;
36
+
27
37
  const config = { provider, model: process.env.CLARITY_MODEL || 'groq/llama-3.3-70b-versatile' };
28
38
 
29
- const { clear } = render(React.createElement(App, { config }), {
39
+ const { clear, waitUntilExit, rerender } = render(React.createElement(App, { config }), {
30
40
  fullscreen: true,
31
41
  patchConsole: false,
42
+ exitOnCtrlC: false,
32
43
  });
33
44
 
34
- setInterval(() => {}, 2 ** 31 - 1);
45
+ keepAlive = setInterval(() => {}, 2 ** 31 - 1);
35
46
 
36
47
  function cleanup() {
48
+ clearInterval(keepAlive);
49
+ console.log = originalLog;
50
+ console.error = originalError;
51
+ console.warn = originalWarn;
37
52
  try { clear(); } catch {}
38
53
  process.stdout.write('\x1b[?25h\x1b[0m');
39
54
  process.exit(0);
@@ -41,11 +56,15 @@ async function main() {
41
56
 
42
57
  process.on('SIGINT', () => cleanup());
43
58
  process.on('SIGTERM', () => cleanup());
59
+ process.on('exit', () => { process.stdout.write('\x1b[?25h\x1b[0m'); });
44
60
 
45
61
  await new Promise(() => {});
46
62
  }
47
63
 
48
64
  main().catch(err => {
65
+ console.log = originalLog;
66
+ console.error = originalError;
67
+ console.warn = originalWarn;
49
68
  process.stdout.write('\x1b[?25h\x1b[0m');
50
69
  console.error('\n\x1b[31mFatal error:\x1b[0m', err.message);
51
70
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clarity-ai",
3
- "version": "6.6.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": {
@@ -23,6 +23,12 @@
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",
31
+ "@inkjs/ui": "^2",
32
+ "cli-truncate": "^6"
27
33
  }
28
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
 
@@ -34,11 +35,9 @@ export function App({ config }) {
34
35
  const stateRef = useRef(state);
35
36
  const modelRef = useRef(model);
36
37
  const providerRef = useRef(provider);
37
- const streamRef = useRef(streamContent);
38
38
  stateRef.current = state;
39
39
  modelRef.current = model;
40
40
  providerRef.current = provider;
41
- streamRef.current = streamContent;
42
41
 
43
42
  const onSubmit = useCallback(async (input) => {
44
43
  if (input === '/exit') { process.exit(0); return; }
@@ -59,7 +58,7 @@ export function App({ config }) {
59
58
  await handleSend(stateRef.current, setState, input, modelRef.current, providerRef.current, setStreamContent, ac.signal);
60
59
  }, []);
61
60
 
62
- function handleCommandSelect(cmdName) {
61
+ function handleCmdSelect(cmdName) {
63
62
  setShowCommands(false);
64
63
  onSubmit(cmdName);
65
64
  }
@@ -74,11 +73,20 @@ export function App({ config }) {
74
73
  }));
75
74
  }
76
75
 
77
- return h(Box, { flexDirection: 'column', backgroundColor: hex.bg },
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%' },
78
86
  h(Layout, {
79
87
  state, streamContent, model, provider,
80
88
  showCommands, showModels,
81
- onCommandSelect: handleCommandSelect,
89
+ onCommandSelect: handleCmdSelect,
82
90
  onModelSelect: handleModelSelect,
83
91
  onCloseCommands: () => setShowCommands(false),
84
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();
@@ -36,51 +36,38 @@ export function CommandPicker({ query, onSelect, onClose }) {
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, {
42
- key: cmd.name, height: 1,
43
- backgroundColor: isSel ? hex.selectionBg : hex.bg,
44
- },
45
- 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);
58
-
59
- return h(Box, { flexDirection: 'column', width: w, marginLeft: 2, backgroundColor: hex.bg },
39
+ return h(Box, { flexDirection: 'column', backgroundColor: hex.bg, width: w },
60
40
  h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
61
41
  h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
62
- sym.box.tl + sym.box.h.repeat(w - 2) + sym.box.tr)
42
+ '\u250C' + '\u2500'.repeat(w - 2) + '\u2510')
63
43
  ),
64
44
  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
45
  h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
68
- ' type to filter...' + ' '.repeat(Math.max(0, w - 20 - 4)) + sym.box.v)
46
+ '\u2502 Commands' + ' '.repeat(Math.max(0, w - 12)) + '\u2502')
69
47
  ),
70
48
  h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
71
49
  h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
72
- sym.box.v + ' ' + (search || sym.ellipsis) + ' '.repeat(Math.max(0, w - 6)) + sym.box.v)
50
+ '\u2502 ' + (search || '\u2026') + ' '.repeat(Math.max(0, w - 6)) + '\u2502')
73
51
  ),
74
- h(Box, { flexDirection: 'column', backgroundColor: hex.bg },
75
- ...items,
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
+ )
76
63
  ),
77
64
  h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
78
65
  h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
79
- sym.box.bl + sym.box.h.repeat(w - 2) + sym.box.br)
66
+ '\u2514' + '\u2500'.repeat(w - 2) + '\u2518')
80
67
  ),
81
68
  h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
82
69
  h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
83
- ' ' + sym.arrowU + sym.arrowD + ' nav ' + sym.arrowR + ' select Esc close')
70
+ ' \u2191\u2193 nav \u2192 select Esc close')
84
71
  ),
85
72
  );
86
73
  }
@@ -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
  }
@@ -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,29 +1,30 @@
1
1
  import React from 'react';
2
2
  import { Box } from 'ink';
3
3
  import { hex } from '../config/theme.js';
4
- import { StatusBar } from './StatusBar.js';
4
+ import { HeaderBar } from './HeaderBar.js';
5
5
  import { MessageList } from './MessageList.js';
6
- import { Composer } from './Composer.js';
6
+ import { InputBar } from './InputBar.js';
7
7
  import { CommandPicker } from './CommandPicker.js';
8
8
  import { ModelPicker } from './ModelPicker.js';
9
9
  const { createElement: h } = React;
10
10
 
11
11
  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
- h(StatusBar, { model, provider, agentMode: state.agentMode, thinking: state.thinking }),
14
- h(Box, { flexGrow: 1, flexDirection: 'column' },
12
+ const picker = showCommands || showModels
13
+ ? h(Box, { position: 'absolute', top: 1, left: 2, backgroundColor: hex.bg, flexDirection: 'column' },
14
+ showCommands ? h(CommandPicker, { onSelect: onCommandSelect, onClose: onCloseCommands }) : null,
15
+ showModels ? h(ModelPicker, { onSelect: onModelSelect, onClose: onCloseModels }) : null
16
+ )
17
+ : null;
18
+
19
+ return h(Box, { flexDirection: 'column', backgroundColor: hex.bg, width: '100%', height: '100%' },
20
+ h(HeaderBar, { model, provider, agentMode: state.agentMode, thinking: state.thinking }),
21
+ h(Box, { flexGrow: 1, flexDirection: 'column', position: 'relative' },
15
22
  h(MessageList, {
16
23
  messages: state.messages, thinking: state.thinking,
17
24
  streamContent, agentStatus: state.agentStatus,
18
- toolExecutions: state.toolExecutions,
19
- })
25
+ }),
26
+ picker
20
27
  ),
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
- 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 })
28
29
  );
29
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 ' + sym.bullet + ' 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 },
@@ -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,27 +24,28 @@ 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 },
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 },
43
- h(Text, { color: hex.purple, bold: true, backgroundColor: hex.surface }, ' ' + sym.diamond + ' CLARITY')
44
+ h(Text, { color: hex.purple, bold: true, backgroundColor: hex.surface }, ' \u25C6 CLARITY')
44
45
  );
45
46
  case 'stream_status':
46
47
  return h(Box, { height: 1, backgroundColor: hex.surface },
47
- h(Text, { color: hex.blue, backgroundColor: hex.surface }, ' ' + sym.dot + ' ' + (text || ''))
48
+ h(Text, { color: hex.blue, backgroundColor: hex.surface }, ' \u25CF ' + (text || ''))
48
49
  );
49
50
  case 'stream_line':
50
51
  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,47 +66,25 @@ export function MessageList({ messages, thinking, streamContent, agentStatus, to
65
66
  }));
66
67
  }, [messages]);
67
68
 
68
- const showLogo = entries.length <= 1 && !thinking && !streamContent;
69
-
70
69
  const { slice, clipIndex, clipLines } = useMemo(
71
- () => sliceToViewport(entries, Math.max(viewport - (showLogo ? 14 : 0), viewport), contentWidth),
72
- [entries, viewport, contentWidth, showLogo]
70
+ () => sliceToViewport(entries, viewport, contentWidth),
71
+ [entries, viewport, contentWidth]
73
72
  );
74
73
 
75
- const lines = useMemo(
74
+ const rawLines = useMemo(
76
75
  () => buildLineArray(slice, clipIndex, clipLines, contentWidth),
77
76
  [slice, clipIndex, clipLines, contentWidth]
78
77
  );
79
78
 
80
- const padded = [...lines];
81
- 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
- }
89
- }
90
-
91
- const logoVisible = showLogo;
92
- const totalHeight = viewport;
79
+ const fillCount = Math.max(0, viewport - rawLines.length);
80
+ const padded = [];
81
+ for (let i = 0; i < fillCount; i++) padded.push({ type: 'empty' });
82
+ for (const ln of rawLines) padded.push(ln);
93
83
 
94
- return h(Box, { height: totalHeight, flexDirection: 'column', overflow: 'hidden' },
84
+ return h(Box, { height: viewport, flexDirection: 'column', overflow: 'hidden' },
95
85
  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 });
107
- }
108
- return h(Box, { key: (ln.data?.id || 'l') + '-' + i, height: 1 },
86
+ if (ln.type === 'empty') return h(Box, { key: 'e' + i, height: 1, backgroundColor: hex.bg });
87
+ return h(Box, { key: (ln.data?.id || 'r') + '-' + i, height: 1 },
109
88
  h(Line, { type: ln.type, text: ln.text, data: ln.data })
110
89
  );
111
90
  })
@@ -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
 
@@ -39,53 +39,45 @@ export function ModelPicker({ onSelect, onClose }) {
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 - 8)) + 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 + ']' : '') + ' '.repeat(Math.max(0, w - m.label.length - (m.badge ? m.badge.length + 4 : 0) - 4)) + (isSel ? '\u276F' : ' '))
59
- );
60
- });
61
-
62
- const count = flat.filter(m => !m._header).length;
63
-
64
- return h(Box, { flexDirection: 'column', width: w, marginLeft: 2, backgroundColor: hex.bg },
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 + ' '),
71
- h(Text, { color: hex.text, backgroundColor: hex.surfaceAlt }, 'Models'),
72
48
  h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
73
- ' ' + count + ' available' + ' '.repeat(Math.max(0, w - 14 - 8)) + sym.box.v)
49
+ '\u2502 Models ' + flat.filter(m => !m._header).length + ' available' + ' '.repeat(Math.max(0, w - 18)) + '\u2502')
74
50
  ),
75
51
  h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
76
52
  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,
53
+ '\u2502 ' + (search || '\u2026') + ' '.repeat(Math.max(0, w - 6)) + '\u2502')
81
54
  ),
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
+ }),
82
74
  h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
83
75
  h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
84
- sym.box.bl + sym.box.h.repeat(w - 2) + sym.box.br)
76
+ '\u2514' + '\u2500'.repeat(w - 2) + '\u2518')
85
77
  ),
86
78
  h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
87
79
  h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
88
- ' ' + sym.arrowU + sym.arrowD + ' nav ' + sym.arrowR + ' select Esc close')
80
+ ' \u2191\u2193 nav \u2192 select Esc close')
89
81
  ),
90
82
  );
91
83
  }
@@ -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
  }
@@ -1,45 +1,27 @@
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')
13
11
  : '';
14
12
 
15
- const headerText = sym.triR + ' Thought' + (durStr ? ' (' + durStr + ')' : '');
16
- const icon = collapsed ? sym.triR : sym.triD;
17
-
18
13
  return h(Box, { flexDirection: 'column', backgroundColor: hex.surfaceAlt },
19
14
  h(Box, { height: 1 },
20
- h(Text, { color: hex.purple, backgroundColor: hex.surfaceAlt }, ' ' + icon + ' ' + headerText)
15
+ h(Text, { color: hex.purple, backgroundColor: hex.surfaceAlt },
16
+ ' ' + (collapsed ? '\u25B8' : '\u25BE') + ' Thought' + (durStr ? ' (' + durStr + ')' : ''))
21
17
  ),
22
18
  collapsed ? null : h(Box, { flexDirection: 'column', backgroundColor: hex.surfaceAlt },
23
- items.map((tr, i) => {
24
- const isLast = i === items.length - 1;
25
- const prefix = isLast ? sym.treeTip + sym.u.h : sym.treeFork + sym.u.h;
26
- const conn = isLast ? ' ' : sym.treeCon;
27
- const ico = tr.status === 'failed' ? sym.cross : sym.circle;
28
- const col = tr.status === 'failed' ? hex.red : hex.green;
29
- 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
41
- );
42
- })
19
+ items.map((tr, i) =>
20
+ h(Box, { key: tr.execId || i, height: 1 },
21
+ h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
22
+ ' ' + (i === items.length - 1 ? '\u2570\u2500' : '\u256D\u2500') + ' ' + (tr.status === 'failed' ? '\u2716' : '\u25C9') + ' ' + tr.name + (tr.duration ? ' ' + tr.duration + 'ms' : ''))
23
+ )
24
+ )
43
25
  )
44
26
  );
45
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,31 +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')
37
- )
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))
32
+ h(Text, { color: hex.blue, backgroundColor: hex.surfaceAlt }, ' \u25CF running')
42
33
  )
43
34
  : null,
44
35
  );
@@ -1,4 +1,6 @@
1
1
  import wrapAnsi from 'wrap-ansi';
2
+ import stringWidth from 'string-width';
3
+ import cliTruncate from 'cli-truncate';
2
4
 
3
5
  export function getLayout() {
4
6
  const rows = process.stdout.rows || 30;
@@ -6,44 +8,45 @@ export function getLayout() {
6
8
  return {
7
9
  rows,
8
10
  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: ' ',
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,
15
16
  };
16
17
  }
17
18
 
19
+ export function sw(text) {
20
+ return stringWidth(text || '');
21
+ }
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) + 0;
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
+ 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);
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,60 +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 });
94
89
  }
95
- if (e.duration) lines.push({ type: 'asst_foot', text: String(e.duration), data: e });
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 });
90
+ if (e.duration && skip <= 0) {
91
+ lines.push({ type: 'asst_foot', text: String(e.duration), data: e });
102
92
  }
93
+ } else if (nr === 'tool') {
94
+ lines.push({ type: 'tool_line', text: truncate((e.error ? '\u2716 ' : '\u25C9 ') + (e.toolName || 'tool') + (e.duration ? ' ' + e.duration + 'ms' : ''), cw), data: e });
103
95
  } else if (nr === 'system') {
104
- 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 });
105
97
  } else if (nr === 'error') {
106
- 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 });
107
99
  } else if (nr === 'streaming') {
108
100
  lines.push({ type: 'stream_head', data: e });
109
- if (e.status) lines.push({ type: 'stream_status', text: e.status, data: e });
110
- const wrapped = wrapText(e.content || '', w);
111
- const contentLines = wrapped.split('\n');
112
- const maxLines = 40;
113
- const startLine = Math.min(skip, contentLines.length);
114
- const endLine = Math.min(contentLines.length, startLine + (maxLines - lines.length));
115
- 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++) {
116
105
  lines.push({ type: 'stream_line', text: contentLines[ci], data: e });
117
106
  }
118
107
  }
@@ -1,4 +1,5 @@
1
1
  import chalk from 'chalk';
2
+ import gradient from 'gradient-string';
2
3
 
3
4
  export const hex = {
4
5
  bg: '#0A0A14',
@@ -8,7 +9,6 @@ export const hex = {
8
9
  codeBg: '#0D0D18',
9
10
  selectionBg: '#FF6B35',
10
11
  selectionText: '#FFFFFF',
11
- borderLight: '#202050',
12
12
  accent: '#FF6B35',
13
13
  purple: '#A855F7',
14
14
  green: '#22C55E',
@@ -19,11 +19,6 @@ export const hex = {
19
19
  text: '#EAEAEE',
20
20
  textDim: '#8888AA',
21
21
  textMuted: '#555577',
22
- white: '#FFFFFF',
23
- black: '#000000',
24
- modalOverlay: 'rgba(0,0,0,0.85)',
25
- logoOrange: '#FF6B35',
26
- logoBlue: '#3B82F6',
27
22
  };
28
23
 
29
24
  export const color = {
@@ -43,65 +38,6 @@ export const color = {
43
38
  text: chalk.hex(hex.text),
44
39
  textDim: chalk.hex(hex.textDim),
45
40
  textMuted: chalk.hex(hex.textMuted),
46
- white: chalk.hex(hex.white),
47
- black: chalk.hex(hex.black),
48
41
  };
49
42
 
50
- export const sym = {
51
- diamond: '\u25C6',
52
- circle: '\u25C9',
53
- dot: '\u25CF',
54
- smallDot: '\u25CB',
55
- triR: '\u25B8',
56
- triD: '\u25BE',
57
- bullet: '\u25C9',
58
- cross: '\u2716',
59
- ellipsis: '\u2026',
60
- mdash: '\u2014',
61
- ndash: '\u2013',
62
- midDot: '\u00B7',
63
- arrowR: '\u2192',
64
- arrowL: '\u2190',
65
- arrowU: '\u2191',
66
- arrowD: '\u2193',
67
- lightV: '\u2502',
68
- lightH: '\u2500',
69
- box: {
70
- tl: '\u250C', tr: '\u2510', bl: '\u2514', br: '\u2518',
71
- 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
- },
79
- treeJ: '\u2514',
80
- treeT: '\u251C',
81
- treeCon: '\u2502',
82
- triR2: '\u25B8',
83
- triD2: '\u25BE',
84
- u: { h: '\u2500' },
85
- treeTip: '\u2570',
86
- treeFork: '\u256D',
87
- star: '\u2726',
88
- asterisk: '\u2731',
89
- gear: '\u2699',
90
- thought: '\u25CB',
91
- };
92
-
93
- export const LOGO = [
94
- ' ',
95
- ' ██████ ██ █████ ██████ ██ ████████ ██ ██ ',
96
- ' ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ',
97
- ' ██████ ██ ███████ ██████ ██ ██ ██ ██ ',
98
- ' ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ',
99
- ' ██ ███████ ██ ██ ██ ██ ██ ██ ██████ ',
100
- ' ',
101
- ' █████ ██ ',
102
- ' ██ ██ ██ ',
103
- ' ██████ ██ ',
104
- ' ██ ██ ██ ',
105
- ' ██ ██ ███████ ',
106
- ' ',
107
- ];
43
+ export const appGradient = gradient(['#FF6B35', '#3B82F6']);