clarity-ai 6.7.0 → 7.0.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,30 @@
2
2
 
3
3
  ---
4
4
 
5
+ ## 7.0.0 (2026-06-06)
6
+
7
+ ### Ground-up architectural rebuild — centered minimalist terminal platform
8
+ - **Complete UI obliteration**: all legacy components replaced with fresh centered layout
9
+ - **Ink-text-input**: controlled form text entry via `ink-text-input` (v6) with dim placeholder
10
+ - **Dynamic viewport hardening**: `process.stdout.on('resize')` listener tracks rows/columns live; auto-downgrades padding below 50 cols
11
+ - **Centered gradient CLARITY logo**: permanent eye-level anchor using gradient-string (orange→blue); never unmounts or wraps
12
+ - **Bordered PromptCard**: clean `┌─┐` container width-capped at 55 cols; status bar with model/provider/agent indicators
13
+ - **Stream concurrency protection**: wrap-ansi + cli-truncate gates all text output; keyboard locked during thinking/streaming
14
+ - **Keybind metadata footer**: right-aligned "tab agents ctrl+p commands" in muted text
15
+ - **System callout banner**: `▀ Tip: Use /help ...` at bottom edge with keyword highlighting
16
+
17
+ ## 6.8.0 (2026-06-06)
18
+
19
+ ### Clean-slate TUI: dynamic dimension defense, sandboxed streams, @inkjs/ui
20
+ - **Dimension defense**: monitors terminal resize; renders fallback if < 60x20
21
+ - **stdout/stderr sandbox**: console.log/error/warn intercepted to prevent AI model noise from corrupting the viewport
22
+ - **Hard-locked 3-tier Yoga grid**: headerBar(1) + messageViewport(flexGrow) + inputDock(3)
23
+ - **@inkjs/ui + cli-truncate**: wrap-ansi + string-width + cli-truncate for zero-bleed text
24
+ - **Single-line gradient header**: `CLARITY AI` with provider/model/mode badges — no ASCII art
25
+ - **Floating picker overlays** with `position: absolute` — never shift the message stream
26
+ - **Alternate screen buffer** with full SIGINT/SIGTERM cleanup and cursor restore
27
+ - **Orange selection bar** (`#FF6B35`) on all active items
28
+
5
29
  ## 6.7.0 (2026-06-06)
6
30
 
7
31
  ### Premium Ink+React TUI Engine — zero-bleed grid
package/bin/clarity.js CHANGED
@@ -1,14 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
  import React from 'react';
3
3
  import { render } from 'ink';
4
- import { App } from '../src/components/AppRoot.js';
4
+ import { App } from '../src/app.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,8 +28,11 @@ 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
 
@@ -36,28 +42,30 @@ async function main() {
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": "7.0.0",
4
4
  "description": "CLARITY — terminal AI agent with local GGUF inference on HF Spaces",
5
5
  "type": "module",
6
6
  "bin": {
@@ -19,14 +19,18 @@
19
19
  "access": "public"
20
20
  },
21
21
  "dependencies": {
22
- "ink": "^5",
23
- "react": "^18",
24
- "ink-spinner": "^5",
22
+ "@inkjs/ui": "^2",
23
+ "ansi-escapes": "^7",
25
24
  "chalk": "^5",
26
- "wrap-ansi": "^9",
27
- "gradient-string": "^3",
25
+ "cli-truncate": "^6",
26
+ "ink-text-input": "^6",
28
27
  "figures": "^6",
29
- "ansi-escapes": "^7",
30
- "string-width": "^7"
28
+ "gradient-string": "^3",
29
+ "ink": "^5",
30
+ "ink-spinner": "^5",
31
+ "ink-text-input": "^6.0.0",
32
+ "react": "^18",
33
+ "string-width": "^7",
34
+ "wrap-ansi": "^9"
31
35
  }
32
36
  }
package/src/app.js ADDED
@@ -0,0 +1,105 @@
1
+ import React, { useState, useCallback, useRef, useEffect } from 'react';
2
+ import { Box, Text, useStdout } from 'ink';
3
+ import { createChatState, handleSend, handleCommand } from './chat.js';
4
+ import { hex } from './config/theme.js';
5
+ import { getLayout } from './config/layout.js';
6
+ import { Header } from './components/Header.js';
7
+ import { StreamView } from './components/StreamView.js';
8
+ import { PromptCard } from './components/PromptCard.js';
9
+ import { Footer } from './components/Footer.js';
10
+ const { createElement: h } = React;
11
+
12
+ let abortController = null;
13
+
14
+ export function getAbortController() {
15
+ return abortController;
16
+ }
17
+
18
+ export function setAbortController(ac) {
19
+ abortController = ac;
20
+ }
21
+
22
+ export function cancelStream() {
23
+ if (abortController) {
24
+ try { abortController.abort(); } catch {}
25
+ abortController = null;
26
+ }
27
+ }
28
+
29
+ export function App({ config }) {
30
+ const { stdout } = useStdout();
31
+ const [dims, setDims] = useState({ rows: stdout.rows || 30, cols: stdout.columns || 80 });
32
+ const [state, setState] = useState(() => createChatState());
33
+ const [streamContent, setStreamContent] = useState('');
34
+ const defaultModel = (config.model || 'groq/llama-3.3-70b-versatile').replace(/^[^/]+\//, '');
35
+ const [model, setModel] = useState(defaultModel);
36
+ const [provider, setProvider] = useState(config.provider || 'groq');
37
+
38
+ const stateRef = useRef(state);
39
+ const modelRef = useRef(model);
40
+ const providerRef = useRef(provider);
41
+ stateRef.current = state;
42
+ modelRef.current = model;
43
+ providerRef.current = provider;
44
+
45
+ useEffect(() => {
46
+ function onResize() {
47
+ setDims({ rows: process.stdout.rows || 30, cols: process.stdout.columns || 80 });
48
+ }
49
+ process.stdout.on('resize', onResize);
50
+ return () => process.stdout.removeListener('resize', onResize);
51
+ }, []);
52
+
53
+ const cols = dims.cols;
54
+ const rows = dims.rows;
55
+ const isCompact = cols < 50;
56
+ const cardWidth = Math.min(cols - 4, 55);
57
+ const contentPad = isCompact ? 1 : 2;
58
+
59
+ const onSubmit = useCallback(async (input) => {
60
+ if (input === '/exit') { process.exit(0); return; }
61
+ cancelStream();
62
+ const ac = new AbortController();
63
+ setAbortController(ac);
64
+ ac.signal.addEventListener('abort', () => {
65
+ setState(s => ({ ...s, thinking: false, streamContent: '', agentStatus: '', toolExecutions: [], thoughtTimer: null }));
66
+ setStreamContent('');
67
+ });
68
+ await handleSend(stateRef.current, setState, input, modelRef.current, providerRef.current, setStreamContent, ac.signal);
69
+ }, []);
70
+
71
+ const onCommand = useCallback(async (input) => {
72
+ if (input.startsWith('/stop')) { cancelStream(); return; }
73
+ if (input.startsWith('/exit')) { process.exit(0); return; }
74
+ await handleCommand(input, stateRef.current, setState, setModel, setProvider, modelRef.current, providerRef.current);
75
+ }, []);
76
+
77
+ function handleInputSubmit(value) {
78
+ const trimmed = value.trim();
79
+ if (!trimmed) return;
80
+ if (trimmed.startsWith('/')) {
81
+ onCommand(trimmed);
82
+ } else {
83
+ onSubmit(trimmed);
84
+ }
85
+ }
86
+
87
+ return h(Box, { width: '100%', height: '100%', flexDirection: 'column', alignItems: 'center', backgroundColor: hex.bg },
88
+ h(Box, { flexGrow: 1, minHeight: 1 }),
89
+ h(Header, { cols }),
90
+ h(Box, { flexGrow: 2, width: cardWidth, minHeight: 2 },
91
+ h(StreamView, { messages: state.messages, streamContent, thinking: state.thinking, width: cardWidth - 2 })
92
+ ),
93
+ h(PromptCard, {
94
+ width: cardWidth,
95
+ compact: isCompact,
96
+ provider,
97
+ model,
98
+ agentMode: state.agentMode,
99
+ thinking: state.thinking,
100
+ onSubmit: handleInputSubmit,
101
+ }),
102
+ h(Box, { flexGrow: 1, minHeight: 1 }),
103
+ h(Footer, { cols })
104
+ );
105
+ }
package/src/chat.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { callAI } from './providers/index.js';
2
2
  import { setKey } from './config/keys.js';
3
3
  import { TOOLS, executeTool } from './tools.js';
4
- import { cancelStream } from './components/AppRoot.js';
4
+ import { cancelStream } from './app.js';
5
5
 
6
6
  const sleep = ms => new Promise(r => setTimeout(r, ms));
7
7
 
@@ -0,0 +1,23 @@
1
+ import React from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import { hex } from '../config/theme.js';
4
+ const { createElement: h } = React;
5
+
6
+ export function Footer({ cols }) {
7
+ const showFull = cols >= 60;
8
+ const keybindText = 'tab agents ctrl+p commands';
9
+ const tipText = 'Tip: Use /help to view system commands and switch active models';
10
+
11
+ return h(Box, { width: '100%', flexDirection: 'column', backgroundColor: hex.bg },
12
+ h(Box, { height: 1, justifyContent: 'flex-end', paddingRight: 2 },
13
+ h(Text, { color: hex.textMuted }, keybindText)
14
+ ),
15
+ h(Box, { height: 1, paddingLeft: 2, paddingRight: 2 },
16
+ h(Text, { color: hex.textDim },
17
+ '\u2580 ',
18
+ h(Text, { color: hex.blue }, 'Tip'),
19
+ h(Text, { color: hex.textDim }, ': ' + (showFull ? tipText : 'Use /help for commands'))
20
+ )
21
+ )
22
+ );
23
+ }
@@ -0,0 +1,11 @@
1
+ import React from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import { appGradient } from '../config/theme.js';
4
+ const { createElement: h } = React;
5
+
6
+ export function Header({ cols }) {
7
+ const label = appGradient('CLARITY');
8
+ return h(Box, { height: 3, width: '100%', alignItems: 'center', justifyContent: 'center' },
9
+ h(Text, { bold: true }, label)
10
+ );
11
+ }
@@ -0,0 +1,50 @@
1
+ import React, { useState } from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import TextInput from 'ink-text-input';
4
+ import { hex } from '../config/theme.js';
5
+ const { createElement: h } = React;
6
+
7
+ const DIVIDER = '\u2500';
8
+
9
+ export function PromptCard({ width, compact, provider, model, agentMode, thinking, onSubmit }) {
10
+ const [input, setInput] = useState('');
11
+ const mShort = model.replace(/^[^/]+\//, '').slice(0, compact ? 10 : 20);
12
+ const pShort = provider.slice(0, compact ? 6 : 10);
13
+ const statusLine = '[ ' + pShort + ' \u00B7 ' + mShort + (agentMode ? ' \u00B7 AGENT' : '') + ' ]';
14
+ const innerW = width - 2;
15
+
16
+ function handleSubmit(value) {
17
+ onSubmit(value);
18
+ setInput('');
19
+ }
20
+
21
+ return h(Box, { flexDirection: 'column', width, backgroundColor: hex.surface },
22
+ h(Box, { height: 1 },
23
+ h(Text, { color: hex.textMuted },
24
+ '\u250C' + '\u2500'.repeat(Math.max(0, innerW)) + '\u2510')
25
+ ),
26
+ h(Box, { height: 1, paddingLeft: 1, paddingRight: 1 },
27
+ h(TextInput, {
28
+ value: input,
29
+ onChange: setInput,
30
+ onSubmit: handleSubmit,
31
+ placeholder: 'Ask anything...',
32
+ focus: !thinking,
33
+ })
34
+ ),
35
+ h(Box, { height: 1 },
36
+ h(Text, { color: hex.textMuted },
37
+ '\u2502' + '\u2500'.repeat(Math.max(0, innerW)) + '\u2502')
38
+ ),
39
+ h(Box, { height: 1, paddingLeft: 1, paddingRight: 1, alignItems: 'center' },
40
+ h(Text, { color: hex.textDim },
41
+ statusLine + ' '.repeat(Math.max(0, innerW - statusLine.length - 1))
42
+ ),
43
+ h(Text, { color: hex.textMuted }, '\u2502')
44
+ ),
45
+ h(Box, { height: 1 },
46
+ h(Text, { color: hex.textMuted },
47
+ '\u2514' + '\u2500'.repeat(Math.max(0, innerW)) + '\u2518')
48
+ ),
49
+ );
50
+ }
@@ -0,0 +1,78 @@
1
+ import React, { useMemo } from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import { hex } from '../config/theme.js';
4
+ import wrapAnsi from 'wrap-ansi';
5
+ import cliTruncate from 'cli-truncate';
6
+ const { createElement: h } = React;
7
+
8
+ function wrap(text, width) {
9
+ if (!text) return '';
10
+ return wrapAnsi(String(text), Math.max(1, width), { trim: false, hard: true });
11
+ }
12
+
13
+ function trunc(text, width) {
14
+ return cliTruncate(text || '', Math.max(1, width), { position: 'end' });
15
+ }
16
+
17
+ function MessageLine({ line, width }) {
18
+ switch (line.role) {
19
+ case 'system':
20
+ return h(Box, { height: 1 },
21
+ h(Text, { color: hex.blue }, ' \u25C9 ' + trunc(line.content, width - 4))
22
+ );
23
+ case 'user':
24
+ return h(Box, { height: 1 },
25
+ h(Text, { color: hex.accent, bold: true }, ' \u276F '),
26
+ h(Text, { color: hex.text }, trunc(line.content, width - 4))
27
+ );
28
+ case 'assistant':
29
+ return h(Box, { height: 1 },
30
+ h(Text, { color: hex.purple, bold: true }, ' \u25C6 '),
31
+ h(Text, { color: hex.text }, trunc(line.content, width - 4))
32
+ );
33
+ case 'tool':
34
+ return h(Box, { height: 1 },
35
+ h(Text, { color: hex.green }, ' \u25C9 ' + trunc((line.toolName || 'tool') + (line.duration ? ' ' + line.duration + 'ms' : ''), width - 6))
36
+ );
37
+ case 'error':
38
+ return h(Box, { height: 1 },
39
+ h(Text, { color: hex.red }, ' \u2716 ' + trunc(line.content, width - 4))
40
+ );
41
+ default:
42
+ return null;
43
+ }
44
+ }
45
+
46
+ function StreamingLine({ content, width }) {
47
+ const wrapped = wrap(content || '', width - 4);
48
+ const first = wrapped.split('\n')[0] || '';
49
+ return h(Box, { height: 1 },
50
+ h(Text, { color: hex.purple }, ' \u25CF '),
51
+ h(Text, { color: hex.text }, trunc(first, width - 4))
52
+ );
53
+ }
54
+
55
+ export function StreamView({ messages, streamContent, thinking, width }) {
56
+ const contentW = Math.max(1, width - 2);
57
+
58
+ const visibleMessages = useMemo(() => {
59
+ if (messages.length === 0) {
60
+ return [{ id: 'empty', role: 'system', content: 'CLARITY AI ready \u00B7 Ctrl+P commands \u00B7 /help' }];
61
+ }
62
+ return messages.slice(-20);
63
+ }, [messages]);
64
+
65
+ return h(Box, { flexDirection: 'column', width: '100%', paddingLeft: 1, paddingRight: 1 },
66
+ visibleMessages.map(m =>
67
+ h(MessageLine, { key: m.id, line: m, width: contentW })
68
+ ),
69
+ thinking && streamContent
70
+ ? h(StreamingLine, { content: streamContent, width: contentW })
71
+ : null,
72
+ thinking && !streamContent
73
+ ? h(Box, { height: 1 },
74
+ h(Text, { color: hex.blue }, ' \u25CF processing...')
75
+ )
76
+ : null,
77
+ );
78
+ }
@@ -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']);
@@ -1,87 +0,0 @@
1
- import React, { useState, useCallback, useRef } from 'react';
2
- import { Box } from 'ink';
3
- import { createChatState, handleSend, handleCommand } from '../chat.js';
4
- import { hex } from '../config/theme.js';
5
- import { Layout } from './Layout.js';
6
- const { createElement: h } = React;
7
-
8
- let abortController = null;
9
-
10
- export function getAbortController() {
11
- return abortController;
12
- }
13
-
14
- export function setAbortController(ac) {
15
- abortController = ac;
16
- }
17
-
18
- export function cancelStream() {
19
- if (abortController) {
20
- try { abortController.abort(); } catch {}
21
- abortController = null;
22
- }
23
- }
24
-
25
- export function App({ config }) {
26
- const [state, setState] = useState(() => createChatState());
27
- const [streamContent, setStreamContent] = useState('');
28
- const defaultModel = (config.model || 'groq/llama-3.3-70b-versatile').replace(/^[^/]+\//, '');
29
- const [model, setModel] = useState(defaultModel);
30
- const [provider, setProvider] = useState(config.provider || 'groq');
31
- const [showCommands, setShowCommands] = useState(false);
32
- const [showModels, setShowModels] = useState(false);
33
-
34
- const stateRef = useRef(state);
35
- const modelRef = useRef(model);
36
- const providerRef = useRef(provider);
37
- stateRef.current = state;
38
- modelRef.current = model;
39
- providerRef.current = provider;
40
-
41
- const onSubmit = useCallback(async (input) => {
42
- if (input === '/exit') { process.exit(0); return; }
43
- if (input.startsWith('/')) {
44
- if (input === '/model' || input === '/models') { setShowModels(true); return; }
45
- if (input === '/help') { setShowCommands(true); return; }
46
- if (input === '/stop') { cancelStream(); return; }
47
- await handleCommand(input, stateRef.current, setState, setModel, setProvider, modelRef.current, providerRef.current);
48
- return;
49
- }
50
- cancelStream();
51
- const ac = new AbortController();
52
- setAbortController(ac);
53
- ac.signal.addEventListener('abort', () => {
54
- setState(s => ({ ...s, thinking: false, streamContent: '', agentStatus: '', toolExecutions: [], thoughtTimer: null }));
55
- setStreamContent('');
56
- });
57
- await handleSend(stateRef.current, setState, input, modelRef.current, providerRef.current, setStreamContent, ac.signal);
58
- }, []);
59
-
60
- function handleCommandSelect(cmdName) {
61
- setShowCommands(false);
62
- onSubmit(cmdName);
63
- }
64
-
65
- function handleModelSelect(modelId) {
66
- setProvider(modelId.split('/')[0]);
67
- setModel(modelId.replace(/^[^/]+\//, ''));
68
- setShowModels(false);
69
- setState(s => ({
70
- ...s,
71
- messages: [...s.messages, { id: 'sys-' + Date.now(), role: 'system', content: 'Switched to ' + modelId }],
72
- }));
73
- }
74
-
75
- return h(Box, { flexDirection: 'column', backgroundColor: hex.bg, height: '100%' },
76
- h(Layout, {
77
- state, streamContent, model, provider,
78
- showCommands, showModels,
79
- onCommandSelect: handleCommandSelect,
80
- onModelSelect: handleModelSelect,
81
- onCloseCommands: () => setShowCommands(false),
82
- onCloseModels: () => setShowModels(false),
83
- onSlash: () => setShowCommands(true),
84
- onSubmit,
85
- })
86
- );
87
- }
@@ -1,49 +0,0 @@
1
- import React, { useMemo } from 'react';
2
- import { Box, Text } from 'ink';
3
- import { hex, sym } from '../config/theme.js';
4
- import { getLayout } from '../config/layout.js';
5
- const { createElement: h } = React;
6
-
7
- const LANG_COLORS = {
8
- js: '#F0DB4F', jsx: '#F0DB4F', ts: '#3178C6', tsx: '#3178C6',
9
- py: '#3572A5', rb: '#CC342D', go: '#00ADD8', rs: '#DEA584',
10
- java: '#B07219', kt: '#7F52FF', swift: '#FFAC45',
11
- html: '#E34F26', css: '#1572B6', scss: '#CC6699',
12
- sh: '#89E051', bash: '#89E051', dockerfile: '#384D54',
13
- json: '#292929', yaml: '#CB171E', md: '#083FA1', sql: '#E38C00',
14
- };
15
-
16
- export function CodeBlock({ code, language }) {
17
- const lang = language || 'code';
18
- const lines = useMemo(() => String(code).split('\n'), [code]);
19
- const langColor = LANG_COLORS[lang] || '#555';
20
- const lnW = String(lines.length).length;
21
- const { cols } = getLayout();
22
- const maxLines = 20;
23
- const visible = lines.slice(0, maxLines);
24
- const codeWidth = cols - 10;
25
-
26
- return h(Box, { flexDirection: 'column', backgroundColor: hex.codeBg },
27
- h(Box, { height: 1, backgroundColor: hex.codeBg },
28
- h(Text, { color: langColor, bold: true, backgroundColor: hex.codeBg }, ' ' + lang),
29
- h(Text, { color: hex.textMuted, backgroundColor: hex.codeBg }, ' ' + String(lines.length) + ' lines '),
30
- ),
31
- h(Box, { flexDirection: 'column', backgroundColor: hex.codeBg },
32
- visible.map((line, i) =>
33
- 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
- )
40
- )
41
- ),
42
- lines.length > maxLines
43
- ? h(Box, { height: 1, backgroundColor: hex.codeBg },
44
- h(Text, { color: hex.textMuted, backgroundColor: hex.codeBg }, ' ' + sym.ellipsis + ' ' + (lines.length - maxLines) + ' more lines')
45
- )
46
- : null
47
- )
48
- );
49
- }
@@ -1,75 +0,0 @@
1
- import React, { useState } from 'react';
2
- import { Box, Text, useInput } from 'ink';
3
- import { hex, sym } from '../config/theme.js';
4
- import { getLayout } from '../config/layout.js';
5
- const { createElement: h } = React;
6
-
7
- const COMMANDS = [
8
- { name: '/keys', desc: 'Set API key' },
9
- { name: '/model', desc: 'Switch model' },
10
- { name: '/provider', desc: 'Switch provider' },
11
- { name: '/agent', desc: 'Toggle agent mode' },
12
- { name: '/stop', desc: 'Cancel running stream' },
13
- { name: '/clear', desc: 'Clear conversation' },
14
- { name: '/export', desc: 'Export conversation' },
15
- { name: '/help', desc: 'Show all commands' },
16
- { name: '/exit', desc: 'Exit CLARITY' },
17
- ];
18
-
19
- export function CommandPicker({ query, onSelect, onClose }) {
20
- const [search, setSearch] = useState('');
21
- const [idx, setIdx] = useState(0);
22
- const { cols } = getLayout();
23
-
24
- const filtered = COMMANDS.filter(c =>
25
- c.name.includes(search) || c.desc.toLowerCase().includes(search.toLowerCase())
26
- );
27
-
28
- useInput((input, key) => {
29
- if (key.upArrow) setIdx(i => Math.max(0, i - 1));
30
- if (key.downArrow) setIdx(i => Math.min(filtered.length - 1, i + 1));
31
- if (key.return && filtered[idx]) onSelect(filtered[idx].name);
32
- if (key.escape || key.ctrl && key.c) onClose();
33
- if (key.backspace) setSearch(s => s.slice(0, -1));
34
- else if (input && !key.ctrl && !key.meta) setSearch(s => s + input);
35
- });
36
-
37
- const w = Math.min(cols - 4, 48);
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
- return h(Box, { flexDirection: 'column', backgroundColor: hex.bg, width: w },
53
- h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
54
- h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
55
- sym.box.tl + sym.box.h.repeat(w - 2) + sym.box.tr)
56
- ),
57
- 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)
60
- ),
61
- h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
62
- h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
63
- sym.box.v + ' ' + (search || '\u2026') + ' '.repeat(Math.max(0, w - 6)) + sym.box.v)
64
- ),
65
- h(Box, { flexDirection: 'column', backgroundColor: hex.bg }, ...items),
66
- h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
67
- h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
68
- sym.box.bl + sym.box.h.repeat(w - 2) + sym.box.br)
69
- ),
70
- h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
71
- h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
72
- ' \u2191\u2193 nav \u2192 select Esc close')
73
- ),
74
- );
75
- }
@@ -1,74 +0,0 @@
1
- import React, { useState, useRef } from 'react';
2
- import { Box, Text, useInput } from 'ink';
3
- import { hex, sym } from '../config/theme.js';
4
- import { getLayout } from '../config/layout.js';
5
- const { createElement: h } = React;
6
-
7
- const MAX_ROWS = 3;
8
-
9
- export function Composer({ provider, model, agentMode, thinking, onSlash, onSubmit }) {
10
- const [input, setInput] = useState('');
11
- const [cursor, setCursor] = useState(0);
12
- const r = useRef('');
13
- r.current = input;
14
-
15
- const { cols } = getLayout();
16
- const w = Math.max(10, cols - 6);
17
- const lineCount = Math.max(1, Math.ceil((input.length || 1) / w));
18
- const visible = Math.min(lineCount, MAX_ROWS);
19
- const mShort = model.replace(/^[^/]+\//, '').slice(0, 18);
20
- const isPlaceholder = !input && !thinking;
21
-
22
- useInput((ch, key) => {
23
- if (key.ctrl && key.p) { onSlash(); return; }
24
- if (key.escape) { onSubmit('/exit'); return; }
25
- if (key.return && !key.shift) {
26
- if (input.trim()) { const t = input; setInput(''); setCursor(0); onSubmit(t); }
27
- return;
28
- }
29
- if (key.return && key.shift) {
30
- setInput(p => p.slice(0, cursor) + '\n' + p.slice(cursor));
31
- setCursor(c => c + 1);
32
- return;
33
- }
34
- if (key.backspace || key.delete) {
35
- if (cursor > 0) { setInput(p => p.slice(0, cursor - 1) + p.slice(cursor)); setCursor(c => c - 1); }
36
- return;
37
- }
38
- if (key.leftArrow && cursor > 0) { setCursor(c => c - 1); return; }
39
- if (key.rightArrow && cursor < input.length) { setCursor(c => c + 1); return; }
40
- if (key.home) { setCursor(0); return; }
41
- if (key.end) { setCursor(input.length); return; }
42
- if (ch && ch.length === 1 && ch.charCodeAt(0) >= 32) {
43
- setInput(p => p.slice(0, cursor) + ch + p.slice(cursor));
44
- setCursor(c => c + 1);
45
- }
46
- });
47
-
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
-
54
- for (let i = 0; i < MAX_ROWS; i++) {
55
- const start = i * w;
56
- const seg = input.slice(start, start + w);
57
- rows.push(
58
- h(Box, { key: 'r' + i, height: 1, backgroundColor: hex.bg },
59
- h(Text, {
60
- color: isPlaceholder ? hex.textMuted : hex.text,
61
- backgroundColor: hex.bg,
62
- wrap: 'truncate-end',
63
- }, ' \u2502 ' + (seg || (i === 0 && isPlaceholder ? 'type a message...' : ' ')))
64
- )
65
- );
66
- }
67
-
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
- ));
72
-
73
- return h(Box, { flexDirection: 'column', backgroundColor: hex.surfaceAlt }, ...rows);
74
- }
@@ -1,34 +0,0 @@
1
- import React from 'react';
2
- import { Box } from 'ink';
3
- import { hex } from '../config/theme.js';
4
- import { getLayout } from '../config/layout.js';
5
- import { StatusBar } from './StatusBar.js';
6
- import { MessageList } from './MessageList.js';
7
- import { Composer } from './Composer.js';
8
- import { CommandPicker } from './CommandPicker.js';
9
- import { ModelPicker } from './ModelPicker.js';
10
- const { createElement: h } = React;
11
-
12
- export function Layout({ state, streamContent, model, provider, showCommands, showModels, onCommandSelect, onModelSelect, onCloseCommands, onCloseModels, onSlash, onSubmit }) {
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%' },
23
- h(StatusBar, { model, provider, agentMode: state.agentMode, thinking: state.thinking }),
24
- h(Box, { flexGrow: 1, flexDirection: 'column', position: 'relative' },
25
- h(MessageList, {
26
- messages: state.messages, thinking: state.thinking,
27
- streamContent, agentStatus: state.agentStatus,
28
- toolExecutions: state.toolExecutions,
29
- }),
30
- picker
31
- ),
32
- h(Composer, { provider, model, agentMode: state.agentMode, thinking: state.thinking, onSlash, onSubmit })
33
- );
34
- }
@@ -1,11 +0,0 @@
1
- import React from 'react';
2
- import { Box, Text } from 'ink';
3
- import { hex, sym } from '../config/theme.js';
4
- const { createElement: h } = React;
5
-
6
- export function LoadingIndicator({ label }) {
7
- return h(Box, { height: 1, backgroundColor: hex.surface },
8
- h(Text, { color: hex.blue, backgroundColor: hex.surface }, ' ' + sym.dot),
9
- h(Text, { color: hex.textDim, backgroundColor: hex.surface }, ' ' + (label || 'processing'))
10
- );
11
- }
@@ -1,107 +0,0 @@
1
- import React, { useMemo } from 'react';
2
- import { Box, Text } from 'ink';
3
- import { hex, sym, LOGO } from '../config/theme.js';
4
- import { getLayout, sliceToViewport, buildLineArray } from '../config/layout.js';
5
- const { createElement: h } = React;
6
-
7
- function Line({ type, text, data }) {
8
- switch (type) {
9
- case 'user_head':
10
- return h(Box, { height: 1, backgroundColor: hex.userBg },
11
- h(Text, { color: hex.accent, bold: true, backgroundColor: hex.userBg }, ' \u276F \u25C9 YOU')
12
- );
13
- case 'user_line':
14
- return h(Box, { height: 1, backgroundColor: hex.userBg },
15
- h(Text, { color: hex.text, backgroundColor: hex.userBg, wrap: 'wrap' }, ' ' + (text || ' '))
16
- );
17
- case 'asst_head':
18
- return h(Box, { height: 1, backgroundColor: hex.surface },
19
- h(Text, { color: hex.purple, bold: true, backgroundColor: hex.surface }, ' \u25C6 CLARITY')
20
- );
21
- case 'asst_line':
22
- return h(Box, { height: 1, backgroundColor: hex.surface },
23
- h(Text, { color: hex.text, backgroundColor: hex.surface, wrap: 'wrap' }, ' \u2502 ' + (text || ' '))
24
- );
25
- case 'asst_foot':
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'))
28
- );
29
- case 'tool_line':
30
- return h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
31
- h(Text, { color: hex.purple, backgroundColor: hex.surfaceAlt }, ' ' + sym.bullet + ' ' + (text || ''))
32
- );
33
- case 'sys_line':
34
- return h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
35
- h(Text, { color: hex.green, backgroundColor: hex.surfaceAlt }, ' ' + sym.bullet + ' ' + (text || ''))
36
- );
37
- case 'err_line':
38
- return h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
39
- h(Text, { color: hex.red, backgroundColor: hex.surfaceAlt }, ' ' + sym.cross + ' ' + (text || ''))
40
- );
41
- case 'stream_head':
42
- return h(Box, { height: 1, backgroundColor: hex.surface },
43
- h(Text, { color: hex.purple, bold: true, backgroundColor: hex.surface }, ' \u25C6 CLARITY')
44
- );
45
- case 'stream_status':
46
- return h(Box, { height: 1, backgroundColor: hex.surface },
47
- h(Text, { color: hex.blue, backgroundColor: hex.surface }, ' \u25CF ' + (text || ''))
48
- );
49
- case 'stream_line':
50
- return h(Box, { height: 1, backgroundColor: hex.surface },
51
- h(Text, { color: hex.text, backgroundColor: hex.surface, wrap: 'wrap' }, ' \u2502 ' + (text || ' '))
52
- );
53
- default:
54
- return null;
55
- }
56
- }
57
-
58
- export function MessageList({ messages, thinking, streamContent, agentStatus, toolExecutions }) {
59
- const { viewport, contentWidth } = getLayout();
60
-
61
- const entries = useMemo(() => {
62
- return messages.map(m => ({
63
- id: m.id, role: m.role, content: m.content,
64
- duration: m.duration, toolName: m.toolName, error: m.error, completed: true,
65
- }));
66
- }, [messages]);
67
-
68
- const showLogo = entries.length <= 1 && !thinking && !streamContent;
69
- const effectiveViewport = showLogo ? Math.max(viewport - 14, 4) : viewport;
70
-
71
- const { slice, clipIndex, clipLines } = useMemo(
72
- () => sliceToViewport(entries, effectiveViewport, contentWidth),
73
- [entries, effectiveViewport, contentWidth]
74
- );
75
-
76
- const rawLines = useMemo(
77
- () => buildLineArray(slice, clipIndex, clipLines, contentWidth),
78
- [slice, clipIndex, clipLines, contentWidth]
79
- );
80
-
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' });
85
- if (showLogo) {
86
- for (let i = 0; i < 14; i++) padded.push({ type: 'logo', line: i });
87
- }
88
- for (const ln of rawLines) padded.push(ln);
89
-
90
- return h(Box, { height: viewport, flexDirection: 'column', overflow: 'hidden' },
91
- padded.map((ln, i) => {
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
- );
101
- }
102
- return h(Box, { key: (ln.data?.id || 'r') + '-' + i, height: 1 },
103
- h(Line, { type: ln.type, text: ln.text, data: ln.data })
104
- );
105
- })
106
- );
107
- }
@@ -1,87 +0,0 @@
1
- import React, { useState, useMemo } from 'react';
2
- import { Box, Text, useInput } from 'ink';
3
- import { ALL_MODELS } from '../config/models.js';
4
- import { hex, sym } from '../config/theme.js';
5
- import { getLayout } from '../config/layout.js';
6
- const { createElement: h } = React;
7
-
8
- export function ModelPicker({ onSelect, onClose }) {
9
- const [search, setSearch] = useState('');
10
- const [idx, setIdx] = useState(0);
11
- const { cols } = getLayout();
12
-
13
- const flat = useMemo(() => {
14
- const q = search.toLowerCase();
15
- const filtered = ALL_MODELS.filter(m =>
16
- m.id.toLowerCase().includes(q) || m.label.toLowerCase().includes(q)
17
- );
18
- const groups = {};
19
- for (const m of filtered) {
20
- if (!groups[m.provider]) groups[m.provider] = [];
21
- groups[m.provider].push(m);
22
- }
23
- const list = [];
24
- for (const [provider, models] of Object.entries(groups)) {
25
- list.push({ _header: provider, _provider: provider });
26
- for (const m of models) list.push(m);
27
- }
28
- return list;
29
- }, [search]);
30
-
31
- useInput((input, key) => {
32
- if (key.upArrow) setIdx(i => Math.max(0, i - 1));
33
- if (key.downArrow) setIdx(i => Math.min(flat.length - 1, i + 1));
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();
36
- if (key.backspace) setSearch(s => s.slice(0, -1));
37
- else if (input && !key.ctrl && !key.meta) setSearch(s => s + input);
38
- });
39
-
40
- const w = Math.min(cols - 4, 52);
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
- return h(Box, { flexDirection: 'column', backgroundColor: hex.bg, width: w },
65
- h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
66
- h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
67
- sym.box.tl + sym.box.h.repeat(w - 2) + sym.box.tr)
68
- ),
69
- 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)
72
- ),
73
- h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
74
- h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
75
- sym.box.v + ' ' + (search || '\u2026') + ' '.repeat(Math.max(0, w - 6)) + sym.box.v)
76
- ),
77
- h(Box, { flexDirection: 'column', backgroundColor: hex.bg }, ...items),
78
- h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
79
- h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
80
- sym.box.bl + sym.box.h.repeat(w - 2) + sym.box.br)
81
- ),
82
- h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
83
- h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
84
- ' \u2191\u2193 nav \u2192 select Esc close')
85
- ),
86
- );
87
- }
@@ -1,19 +0,0 @@
1
- import React from 'react';
2
- import { Box, Text } 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 StatusBar({ model, provider, agentMode, thinking }) {
8
- const { cols } = getLayout();
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
-
14
- return h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
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 + ' ')
18
- );
19
- }
@@ -1,34 +0,0 @@
1
- import React, { useState } from 'react';
2
- import { Box, Text } from 'ink';
3
- import { hex, sym } from '../config/theme.js';
4
- import { getLayout } from '../config/layout.js';
5
- const { createElement: h } = React;
6
-
7
- export function ThinkingBlock({ toolResults, duration }) {
8
- const [collapsed, setCollapsed] = useState(true);
9
- const { cols } = getLayout();
10
- const items = toolResults || [];
11
- const durStr = duration
12
- ? (duration < 1000 ? duration + 'ms' : (duration / 1000).toFixed(1) + 's')
13
- : '';
14
-
15
- return h(Box, { flexDirection: 'column', backgroundColor: hex.surfaceAlt },
16
- h(Box, { height: 1 },
17
- h(Text, { color: hex.purple, backgroundColor: hex.surfaceAlt },
18
- ' ' + (collapsed ? sym.triR : sym.triD) + ' Thought' + (durStr ? ' (' + durStr + ')' : ''))
19
- ),
20
- 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 },
28
- h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
29
- ' ' + prefix + ' ' + ico + ' ' + tr.name + td)
30
- );
31
- })
32
- )
33
- );
34
- }
@@ -1,40 +0,0 @@
1
- import React from 'react';
2
- import { Box, Text } from 'ink';
3
- import { hex, sym } from '../config/theme.js';
4
- import { getLayout } from '../config/layout.js';
5
- const { createElement: h } = React;
6
-
7
- export function ToolCard({ exec, isActive }) {
8
- const { cols } = getLayout();
9
- const name = exec.name || 'tool';
10
- const status = exec.status || 'running';
11
- const dur = exec.duration ? (exec.duration < 1000 ? exec.duration + 'ms' : (exec.duration / 1000).toFixed(1) + 's') : '';
12
- const args = typeof exec.args === 'string' ? exec.args : JSON.stringify(exec.args || {});
13
- const isDone = !isActive && (status === 'completed' || status === 'failed');
14
-
15
- if (isDone) {
16
- const c = status === 'failed' ? hex.red : hex.green;
17
- const icon = status === 'failed' ? sym.cross : sym.bullet;
18
- return h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
19
- h(Text, { color: c, backgroundColor: hex.surfaceAlt }, ' ' + icon + ' ' + name + (dur ? ' ' + dur : ''))
20
- );
21
- }
22
-
23
- const w = Math.min(cols - 8, 56);
24
-
25
- return h(Box, { flexDirection: 'column', backgroundColor: hex.surfaceAlt },
26
- h(Box, { height: 1 },
27
- h(Text, { color: hex.purple, backgroundColor: hex.surfaceAlt }, ' ' + sym.bullet + ' ' + name + (dur ? ' ' + dur : ''))
28
- ),
29
- args.length < 80
30
- ? h(Box, { height: 1 },
31
- h(Text, { color: hex.textDim, backgroundColor: hex.surfaceAlt }, ' ' + sym.triR + ' ' + args.slice(0, w))
32
- )
33
- : null,
34
- status === 'running'
35
- ? h(Box, { height: 1 },
36
- h(Text, { color: hex.blue, backgroundColor: hex.surfaceAlt }, ' ' + sym.dot + ' running')
37
- )
38
- : null,
39
- );
40
- }