clarity-ai 6.8.0 → 7.1.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,28 @@
2
2
 
3
3
  ---
4
4
 
5
+ ## 7.1.0 (2026-06-06)
6
+
7
+ ### Premium UI/UX overhaul — high-texture capsule design with live streaming
8
+ - **Massive ASCII art banner**: 6-line figlet-style "CLARITY" in orange→purple→blue gradient (auto-falls back for narrow terminals)
9
+ - **Solid Ink capsule cards**: zero wire borders (`|`, `[`, `]`, `-` banned). All containers use `backgroundColor` blocks with graphite (#1C1C1C) surfaces
10
+ - **Orange accent selection**: `#FF9F43` full-width highlight bars with black text for all active/focus elements
11
+ - **State machine**: `IDLE` → `THINKING` (live elapsed counter in ms) → `STREAMING` — each with distinct badge coloring
12
+ - **Live token streaming**: every chunk runs through `wrap-ansi` + `string-width` against current terminal width; no pre-packaged truncation
13
+ - **Dynamic viewport hardening**: `process.stdout.on('resize')` tracks live dimensions; auto-scrolls old content up while keeping input anchored
14
+
15
+ ## 7.0.0 (2026-06-06)
16
+
17
+ ### Ground-up architectural rebuild — centered minimalist terminal platform
18
+ - **Complete UI obliteration**: all legacy components replaced with fresh centered layout
19
+ - **Ink-text-input**: controlled form text entry via `ink-text-input` (v6) with dim placeholder
20
+ - **Dynamic viewport hardening**: `process.stdout.on('resize')` listener tracks rows/columns live; auto-downgrades padding below 50 cols
21
+ - **Centered gradient CLARITY logo**: permanent eye-level anchor using gradient-string (orange→blue); never unmounts or wraps
22
+ - **Bordered PromptCard**: clean `┌─┐` container width-capped at 55 cols; status bar with model/provider/agent indicators
23
+ - **Stream concurrency protection**: wrap-ansi + cli-truncate gates all text output; keyboard locked during thinking/streaming
24
+ - **Keybind metadata footer**: right-aligned "tab agents ctrl+p commands" in muted text
25
+ - **System callout banner**: `▀ Tip: Use /help ...` at bottom edge with keyword highlighting
26
+
5
27
  ## 6.8.0 (2026-06-06)
6
28
 
7
29
  ### Clean-slate TUI: dynamic dimension defense, sandboxed streams, @inkjs/ui
package/bin/clarity.js CHANGED
@@ -1,7 +1,7 @@
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
7
 
@@ -36,7 +36,7 @@ async function main() {
36
36
 
37
37
  const config = { provider, model: process.env.CLARITY_MODEL || 'groq/llama-3.3-70b-versatile' };
38
38
 
39
- const { clear, waitUntilExit, rerender } = render(React.createElement(App, { config }), {
39
+ const { clear, waitUntilExit } = render(React.createElement(App, { config }), {
40
40
  fullscreen: true,
41
41
  patchConsole: false,
42
42
  exitOnCtrlC: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clarity-ai",
3
- "version": "6.8.0",
3
+ "version": "7.1.0",
4
4
  "description": "CLARITY — terminal AI agent with local GGUF inference on HF Spaces",
5
5
  "type": "module",
6
6
  "bin": {
@@ -19,16 +19,17 @@
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",
28
+ "gradient-string": "^3",
29
+ "ink": "^5",
30
+ "ink-spinner": "^5",
31
+ "react": "^18",
30
32
  "string-width": "^7",
31
- "@inkjs/ui": "^2",
32
- "cli-truncate": "^6"
33
+ "wrap-ansi": "^9"
33
34
  }
34
35
  }
package/src/app.js ADDED
@@ -0,0 +1,121 @@
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 { Banner } from './components/Banner.js';
7
+ import { StatusBar } from './components/StatusBar.js';
8
+ import { StreamView } from './components/StreamView.js';
9
+ import { InputPanel } from './components/InputPanel.js';
10
+ import { Footer } from './components/Footer.js';
11
+ const { createElement: h } = React;
12
+
13
+ let abortController = null;
14
+
15
+ export function getAbortController() {
16
+ return abortController;
17
+ }
18
+
19
+ export function setAbortController(ac) {
20
+ abortController = ac;
21
+ }
22
+
23
+ export function cancelStream() {
24
+ if (abortController) {
25
+ try { abortController.abort(); } catch {}
26
+ abortController = null;
27
+ }
28
+ }
29
+
30
+ function deriveStatus(state, streamContent) {
31
+ if (!state.thinking) return 'idle';
32
+ if (state.thinking && streamContent) return 'streaming';
33
+ return 'thinking';
34
+ }
35
+
36
+ export function App({ config }) {
37
+ const { stdout } = useStdout();
38
+ const [dims, setDims] = useState({ rows: stdout.rows || 30, cols: stdout.columns || 80 });
39
+ const [state, setState] = useState(() => createChatState());
40
+ const [streamContent, setStreamContent] = useState('');
41
+ const [thinkingStart, setThinkingStart] = useState(null);
42
+ const defaultModel = (config.model || 'groq/llama-3.3-70b-versatile').replace(/^[^/]+\//, '');
43
+ const [model, setModel] = useState(defaultModel);
44
+ const [provider, setProvider] = useState(config.provider || 'groq');
45
+
46
+ const stateRef = useRef(state);
47
+ const modelRef = useRef(model);
48
+ const providerRef = useRef(provider);
49
+ stateRef.current = state;
50
+ modelRef.current = model;
51
+ providerRef.current = provider;
52
+
53
+ useEffect(() => {
54
+ function onResize() {
55
+ setDims({ rows: process.stdout.rows || 30, cols: process.stdout.columns || 80 });
56
+ }
57
+ process.stdout.on('resize', onResize);
58
+ return () => process.stdout.removeListener('resize', onResize);
59
+ }, []);
60
+
61
+ useEffect(() => {
62
+ if (state.thinking && !thinkingStart) {
63
+ setThinkingStart(Date.now());
64
+ } else if (!state.thinking) {
65
+ setThinkingStart(null);
66
+ }
67
+ }, [state.thinking]);
68
+
69
+ const cols = dims.cols;
70
+ const rows = dims.rows;
71
+ const status = deriveStatus(state, streamContent);
72
+ const cardWidth = Math.min(cols - 4, 56);
73
+
74
+ const onSubmit = useCallback(async (input) => {
75
+ if (input === '/exit') { process.exit(0); return; }
76
+ cancelStream();
77
+ const ac = new AbortController();
78
+ setAbortController(ac);
79
+ ac.signal.addEventListener('abort', () => {
80
+ setState(s => ({ ...s, thinking: false, streamContent: '', agentStatus: '', toolExecutions: [], thoughtTimer: null }));
81
+ setStreamContent('');
82
+ });
83
+ await handleSend(stateRef.current, setState, input, modelRef.current, providerRef.current, setStreamContent, ac.signal);
84
+ }, []);
85
+
86
+ const onCommand = useCallback(async (input) => {
87
+ if (input.startsWith('/stop')) { cancelStream(); return; }
88
+ if (input.startsWith('/exit')) { process.exit(0); return; }
89
+ await handleCommand(input, stateRef.current, setState, setModel, setProvider, modelRef.current, providerRef.current);
90
+ }, []);
91
+
92
+ function handleInputSubmit(value) {
93
+ const trimmed = value.trim();
94
+ if (!trimmed) return;
95
+ if (trimmed.startsWith('/')) {
96
+ onCommand(trimmed);
97
+ } else {
98
+ onSubmit(trimmed);
99
+ }
100
+ }
101
+
102
+ return h(Box, { width: '100%', height: '100%', flexDirection: 'column', alignItems: 'center', backgroundColor: hex.bg },
103
+ h(Box, { flexGrow: 1, minHeight: 1 }),
104
+ h(Banner, { cols }),
105
+ h(StatusBar, { status, thinkingStart }),
106
+ h(Box, { flexGrow: 2, width: cardWidth, minHeight: 2, flexDirection: 'column' },
107
+ h(StreamView, { messages: state.messages, streamContent, status, width: cardWidth })
108
+ ),
109
+ h(Box, { height: 1 }),
110
+ h(InputPanel, {
111
+ width: cardWidth,
112
+ provider,
113
+ model,
114
+ agentMode: state.agentMode,
115
+ status,
116
+ onSubmit: handleInputSubmit,
117
+ }),
118
+ h(Box, { flexGrow: 1, minHeight: 1 }),
119
+ h(Footer, { cols })
120
+ );
121
+ }
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,41 @@
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
+ const BANNER_FULL = [
7
+ ' ██████╗██╗ █████╗ ██████╗ ██╗████████╗██╗ ██╗',
8
+ '██╔════╝██║ ██╔══██╗██╔══██╗██║╚══██╔══╝╚██╗ ██╔╝',
9
+ '██║ ███╗██║ ███████║██████╔╝██║ ██║ ╚████╔╝ ',
10
+ '██║ ██║██║ ██╔══██║██╔══██╗██║ ██║ ╚██╔╝ ',
11
+ '╚██████╔╝███████╗██║ ██║██║ ██║██║ ██║ ██║ ',
12
+ ' ╚═════╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ',
13
+ ];
14
+
15
+ const BANNER_MED = [
16
+ '██████╗ ██╗ █████╗ ██████╗ ██╗████████╗██╗ ██╗',
17
+ '██╔════╝ ██║ ██╔══██╗██╔══██╗██║╚══██╔══╝╚██╗ ██╔╝',
18
+ '███████╗ ██║ ███████║██████╔╝██║ ██║ ╚████╔╝ ',
19
+ '╚════██║ ██║ ██╔══██║██╔══██╗██║ ██║ ╚██╔╝ ',
20
+ '██████╔╝ ███████╗██║ ██║██║ ██║██║ ██║ ██║ ',
21
+ '╚═════╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ',
22
+ ];
23
+
24
+ export function Banner({ cols }) {
25
+ if (cols < 50) {
26
+ return h(Box, { height: 2, width: '100%', alignItems: 'center', justifyContent: 'center' },
27
+ h(Text, { bold: true }, appGradient('CLARITY'))
28
+ );
29
+ }
30
+
31
+ const lines = cols < 60 ? BANNER_MED : BANNER_FULL;
32
+ const bannerHeight = lines.length;
33
+
34
+ return h(Box, { height: bannerHeight, width: '100%', alignItems: 'center', justifyContent: 'center', flexDirection: 'column' },
35
+ lines.map((line, i) =>
36
+ h(Box, { key: i, height: 1 },
37
+ h(Text, { bold: true }, appGradient.multiline(line))
38
+ )
39
+ )
40
+ );
41
+ }
@@ -0,0 +1,24 @@
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 >= 58;
8
+ const keybindText = 'tab agents ctrl+p commands';
9
+
10
+ return h(Box, { width: '100%', flexDirection: 'column', backgroundColor: hex.bg },
11
+ h(Box, { height: 1, justifyContent: 'flex-end', paddingRight: 2, backgroundColor: hex.bg },
12
+ h(Text, { color: hex.textMuted }, keybindText)
13
+ ),
14
+ h(Box, { height: 1, paddingLeft: 2, paddingRight: 2, backgroundColor: hex.bg },
15
+ h(Text, { color: hex.textDim },
16
+ '\u2580 ',
17
+ h(Text, { color: hex.orange }, 'Tip'),
18
+ h(Text, { color: hex.textDim }, ': ' + (showFull
19
+ ? 'Use /help to view system commands and switch active models'
20
+ : 'Use /help for commands'))
21
+ )
22
+ )
23
+ );
24
+ }
@@ -0,0 +1,39 @@
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
+ export function InputPanel({ width, provider, model, agentMode, status, onSubmit }) {
8
+ const [input, setInput] = useState('');
9
+ const isLocked = status !== 'idle';
10
+ const mShort = model.replace(/^[^/]+\//, '').slice(0, 18);
11
+ const innerW = Math.max(4, width - 4);
12
+
13
+ function handleSubmit(value) {
14
+ const trimmed = value.trim();
15
+ if (!trimmed) return;
16
+ onSubmit(trimmed);
17
+ setInput('');
18
+ }
19
+
20
+ return h(Box, { width, flexDirection: 'column', backgroundColor: hex.cardBg },
21
+ h(Box, { height: 1, backgroundColor: hex.orange }),
22
+ h(Box, { height: 1, paddingLeft: 2, paddingRight: 2, backgroundColor: hex.cardBg },
23
+ h(TextInput, {
24
+ value: input,
25
+ onChange: setInput,
26
+ onSubmit: handleSubmit,
27
+ placeholder: 'Ask anything...',
28
+ focus: !isLocked,
29
+ })
30
+ ),
31
+ h(Box, { height: 1, paddingLeft: 2, paddingRight: 2, backgroundColor: hex.cardBg },
32
+ h(Text, { color: hex.textMuted },
33
+ provider + ' \u00B7 ' + mShort + (agentMode ? ' \u00B7 AGENT' : '') +
34
+ ' '.repeat(Math.max(0, innerW - provider.length - mShort.length - (agentMode ? 10 : 4))) +
35
+ (isLocked ? '' : ''))
36
+ ),
37
+ h(Box, { height: 1, backgroundColor: hex.cardBg }),
38
+ );
39
+ }
@@ -1,19 +1,45 @@
1
- import React from 'react';
1
+ import React, { useState, useEffect } from 'react';
2
2
  import { Box, Text } from 'ink';
3
3
  import { hex } from '../config/theme.js';
4
- import { getLayout } from '../config/layout.js';
5
4
  const { createElement: h } = React;
6
5
 
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);
6
+ export function StatusBar({ status, thinkingStart }) {
7
+ const [elapsed, setElapsed] = useState(0);
13
8
 
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
- );
9
+ useEffect(() => {
10
+ if (status === 'idle') { setElapsed(0); return; }
11
+ const id = setInterval(() => {
12
+ setElapsed(thinkingStart ? Date.now() - thinkingStart : 0);
13
+ }, 100);
14
+ return () => clearInterval(id);
15
+ }, [status, thinkingStart]);
16
+
17
+ const isThinking = status === 'thinking';
18
+ const isStreaming = status === 'streaming';
19
+
20
+ if (status === 'idle') {
21
+ return h(Box, { height: 1, width: '100%', alignItems: 'center', justifyContent: 'center' },
22
+ h(Box, { backgroundColor: hex.surfaceAlt, paddingX: 1 },
23
+ h(Text, { color: hex.textMuted }, ' IDLE ')
24
+ )
25
+ );
26
+ }
27
+
28
+ if (isThinking) {
29
+ return h(Box, { height: 1, width: '100%', alignItems: 'center', justifyContent: 'center' },
30
+ h(Box, { backgroundColor: hex.surfaceAlt, paddingX: 1 },
31
+ h(Text, { color: hex.orange }, ' \u25CF THINKING ' + elapsed + 'ms ')
32
+ )
33
+ );
34
+ }
35
+
36
+ if (isStreaming) {
37
+ return h(Box, { height: 1, width: '100%', alignItems: 'center', justifyContent: 'center' },
38
+ h(Box, { backgroundColor: hex.surfaceAlt, paddingX: 1 },
39
+ h(Text, { color: hex.blue }, ' \u25CF STREAMING ' + elapsed + 'ms ')
40
+ )
41
+ );
42
+ }
43
+
44
+ return null;
19
45
  }
@@ -0,0 +1,104 @@
1
+ import React, { useMemo, useRef, useEffect } from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import { hex } from '../config/theme.js';
4
+ import wrapAnsi from 'wrap-ansi';
5
+ import stringWidth from 'string-width';
6
+ const { createElement: h } = React;
7
+
8
+ function wrapText(text, width) {
9
+ if (!text) return [];
10
+ return wrapAnsi(String(text), Math.max(4, width), { trim: false, hard: true }).split('\n');
11
+ }
12
+
13
+ function MessageBlock({ msg, width }) {
14
+ const lines = useMemo(() => wrapText(msg.content, width), [msg.content, width]);
15
+
16
+ switch (msg.role) {
17
+ case 'user':
18
+ return h(Box, { flexDirection: 'column', marginBottom: 0 },
19
+ h(Box, { height: 1 },
20
+ h(Text, { color: hex.orange, bold: true }, ' \u276F '),
21
+ h(Text, { color: hex.text }, lines[0] || '')
22
+ ),
23
+ lines.slice(1).map((l, i) =>
24
+ h(Box, { key: i, height: 1 },
25
+ h(Text, { color: hex.text }, ' ' + l)
26
+ )
27
+ )
28
+ );
29
+ case 'assistant':
30
+ return h(Box, { flexDirection: 'column', marginBottom: 1 },
31
+ h(Box, { height: 1 },
32
+ h(Text, { color: hex.purple, bold: true }, ' \u25C6 '),
33
+ h(Text, { color: hex.text }, lines[0] || '')
34
+ ),
35
+ lines.slice(1).map((l, i) =>
36
+ h(Box, { key: i, height: 1 },
37
+ h(Text, { color: hex.text }, ' ' + l)
38
+ )
39
+ ),
40
+ msg.duration
41
+ ? h(Box, { height: 1 },
42
+ h(Text, { color: hex.textMuted }, ' ' + (msg.duration < 1000 ? msg.duration + 'ms' : (msg.duration / 1000).toFixed(1) + 's'))
43
+ )
44
+ : null
45
+ );
46
+ case 'system':
47
+ return h(Box, { height: 1 },
48
+ h(Text, { color: hex.blue }, ' \u25C9 ' + lines[0] || '')
49
+ );
50
+ case 'tool':
51
+ return h(Box, { height: 1 },
52
+ h(Text, { color: hex.green }, ' \u25C9 ' + (msg.toolName || 'tool') + (msg.duration ? ' ' + msg.duration + 'ms' : ''))
53
+ );
54
+ case 'error':
55
+ return h(Box, { height: 1 },
56
+ h(Text, { color: hex.red }, ' \u2716 ' + lines[0] || msg.content)
57
+ );
58
+ default:
59
+ return null;
60
+ }
61
+ }
62
+
63
+ function StreamingBlock({ content, width }) {
64
+ const lines = useMemo(() => wrapText(content, width), [content, width]);
65
+ const visible = lines.slice(-100);
66
+ const first = visible[0] || '';
67
+
68
+ return h(Box, { flexDirection: 'column' },
69
+ h(Box, { height: 1 },
70
+ h(Text, { color: hex.blue, bold: true }, ' \u25CF '),
71
+ h(Text, { color: hex.text }, first)
72
+ ),
73
+ visible.slice(1).map((l, i) =>
74
+ h(Box, { key: i, height: 1 },
75
+ h(Text, { color: hex.text }, ' ' + l)
76
+ )
77
+ )
78
+ );
79
+ }
80
+
81
+ export function StreamView({ messages, streamContent, status, width }) {
82
+ const contentW = Math.max(4, width - 2);
83
+
84
+ const displayMessages = useMemo(() => {
85
+ if (messages.length === 0) {
86
+ return [{ id: 'welcome', role: 'system', content: 'CLARITY AI ready \u00B7 /help for commands' }];
87
+ }
88
+ return messages;
89
+ }, [messages]);
90
+
91
+ return h(Box, { flexDirection: 'column', width: '100%', paddingLeft: 1, paddingRight: 1 },
92
+ displayMessages.map(m =>
93
+ h(MessageBlock, { key: m.id, msg: m, width: contentW })
94
+ ),
95
+ (status === 'streaming' || status === 'thinking') && streamContent
96
+ ? h(StreamingBlock, { content: streamContent, width: contentW })
97
+ : null,
98
+ status === 'thinking' && !streamContent
99
+ ? h(Box, { height: 1 },
100
+ h(Text, { color: hex.textMuted }, ' \u25CF processing...')
101
+ )
102
+ : null,
103
+ );
104
+ }
@@ -2,33 +2,37 @@ import chalk from 'chalk';
2
2
  import gradient from 'gradient-string';
3
3
 
4
4
  export const hex = {
5
- bg: '#0A0A14',
6
- surface: '#111125',
7
- surfaceAlt: '#161630',
8
- userBg: '#140A28',
9
- codeBg: '#0D0D18',
10
- selectionBg: '#FF6B35',
11
- selectionText: '#FFFFFF',
12
- accent: '#FF6B35',
5
+ bg: '#0D0D14',
6
+ surface: '#111118',
7
+ surfaceAlt: '#16161E',
8
+ cardBg: '#1C1C1C',
9
+ inputBg: '#1A1A22',
10
+ codeBg: '#0D0D14',
11
+ selectionBg: '#FF9F43',
12
+ selectionText: '#000000',
13
+ accent: '#FF9F43',
14
+ orange: '#FF9F43',
13
15
  purple: '#A855F7',
14
16
  green: '#22C55E',
15
17
  red: '#EF4444',
16
18
  gold: '#F59E0B',
17
19
  blue: '#3B82F6',
18
20
  cyan: '#22D3EE',
19
- text: '#EAEAEE',
20
- textDim: '#8888AA',
21
- textMuted: '#555577',
21
+ text: '#EEEEF0',
22
+ textDim: '#9999AA',
23
+ textMuted: '#555566',
22
24
  };
23
25
 
24
26
  export const color = {
25
27
  bg: chalk.hex(hex.bg),
26
28
  surface: chalk.hex(hex.surface),
27
29
  surfaceAlt: chalk.hex(hex.surfaceAlt),
28
- userBg: chalk.hex(hex.userBg),
30
+ cardBg: chalk.hex(hex.cardBg),
31
+ inputBg: chalk.hex(hex.inputBg),
29
32
  selectionBg: chalk.hex(hex.selectionBg),
30
33
  selectionText: chalk.hex(hex.selectionText),
31
34
  accent: chalk.hex(hex.accent),
35
+ orange: chalk.hex(hex.orange),
32
36
  purple: chalk.hex(hex.purple),
33
37
  green: chalk.hex(hex.green),
34
38
  red: chalk.hex(hex.red),
@@ -40,4 +44,4 @@ export const color = {
40
44
  textMuted: chalk.hex(hex.textMuted),
41
45
  };
42
46
 
43
- export const appGradient = gradient(['#FF6B35', '#3B82F6']);
47
+ export const appGradient = gradient(['#FF9F43', '#A855F7', '#3B82F6']);
@@ -1,97 +0,0 @@
1
- import React, { useState, useCallback, useRef, useEffect } from 'react';
2
- import { Box, Text } 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 { Layout } from './Layout.js';
7
- const { createElement: h } = React;
8
-
9
- let abortController = null;
10
-
11
- export function getAbortController() {
12
- return abortController;
13
- }
14
-
15
- export function setAbortController(ac) {
16
- abortController = ac;
17
- }
18
-
19
- export function cancelStream() {
20
- if (abortController) {
21
- try { abortController.abort(); } catch {}
22
- abortController = null;
23
- }
24
- }
25
-
26
- export function App({ config }) {
27
- const [state, setState] = useState(() => createChatState());
28
- const [streamContent, setStreamContent] = useState('');
29
- const defaultModel = (config.model || 'groq/llama-3.3-70b-versatile').replace(/^[^/]+\//, '');
30
- const [model, setModel] = useState(defaultModel);
31
- const [provider, setProvider] = useState(config.provider || 'groq');
32
- const [showCommands, setShowCommands] = useState(false);
33
- const [showModels, setShowModels] = useState(false);
34
-
35
- const stateRef = useRef(state);
36
- const modelRef = useRef(model);
37
- const providerRef = useRef(provider);
38
- stateRef.current = state;
39
- modelRef.current = model;
40
- providerRef.current = provider;
41
-
42
- const onSubmit = useCallback(async (input) => {
43
- if (input === '/exit') { process.exit(0); return; }
44
- if (input.startsWith('/')) {
45
- if (input === '/model' || input === '/models') { setShowModels(true); return; }
46
- if (input === '/help') { setShowCommands(true); return; }
47
- if (input === '/stop') { cancelStream(); return; }
48
- await handleCommand(input, stateRef.current, setState, setModel, setProvider, modelRef.current, providerRef.current);
49
- return;
50
- }
51
- cancelStream();
52
- const ac = new AbortController();
53
- setAbortController(ac);
54
- ac.signal.addEventListener('abort', () => {
55
- setState(s => ({ ...s, thinking: false, streamContent: '', agentStatus: '', toolExecutions: [], thoughtTimer: null }));
56
- setStreamContent('');
57
- });
58
- await handleSend(stateRef.current, setState, input, modelRef.current, providerRef.current, setStreamContent, ac.signal);
59
- }, []);
60
-
61
- function handleCmdSelect(cmdName) {
62
- setShowCommands(false);
63
- onSubmit(cmdName);
64
- }
65
-
66
- function handleModelSelect(modelId) {
67
- setProvider(modelId.split('/')[0]);
68
- setModel(modelId.replace(/^[^/]+\//, ''));
69
- setShowModels(false);
70
- setState(s => ({
71
- ...s,
72
- messages: [...s.messages, { id: 'sys-' + Date.now(), role: 'system', content: 'Switched to ' + modelId }],
73
- }));
74
- }
75
-
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%' },
86
- h(Layout, {
87
- state, streamContent, model, provider,
88
- showCommands, showModels,
89
- onCommandSelect: handleCmdSelect,
90
- onModelSelect: handleModelSelect,
91
- onCloseCommands: () => setShowCommands(false),
92
- onCloseModels: () => setShowModels(false),
93
- onSlash: () => setShowCommands(true),
94
- onSubmit,
95
- })
96
- );
97
- }
@@ -1,44 +0,0 @@
1
- import React, { useMemo } 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
- 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',
13
- };
14
-
15
- export function CodeBlock({ code, language }) {
16
- const lang = language || 'code';
17
- const lines = useMemo(() => String(code).split('\n'), [code]);
18
- const langColor = LANG_COLORS[lang] || '#555';
19
- const { cols } = getLayout();
20
- const maxLines = 15;
21
- const visible = lines.slice(0, maxLines);
22
- const codeWidth = cols - 10;
23
- const lnW = String(lines.length).length;
24
-
25
- return h(Box, { flexDirection: 'column', backgroundColor: hex.codeBg },
26
- h(Box, { height: 1, backgroundColor: hex.codeBg },
27
- h(Text, { color: langColor, bold: true, backgroundColor: hex.codeBg }, ' ' + lang),
28
- h(Text, { color: hex.textMuted, backgroundColor: hex.codeBg }, ' ' + String(lines.length) + ' lines'),
29
- ),
30
- h(Box, { flexDirection: 'column', backgroundColor: hex.codeBg },
31
- visible.map((line, i) =>
32
- h(Box, { key: i, height: 1, backgroundColor: hex.codeBg },
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))
35
- )
36
- ),
37
- lines.length > maxLines
38
- ? h(Box, { height: 1, backgroundColor: hex.codeBg },
39
- h(Text, { color: hex.textMuted, backgroundColor: hex.codeBg }, ' \u2026 ' + (lines.length - maxLines) + ' more lines')
40
- )
41
- : null
42
- )
43
- );
44
- }
@@ -1,73 +0,0 @@
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
- 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({ 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) 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
- return h(Box, { flexDirection: 'column', backgroundColor: hex.bg, width: w },
40
- h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
41
- h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
42
- '\u250C' + '\u2500'.repeat(w - 2) + '\u2510')
43
- ),
44
- h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
45
- h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
46
- '\u2502 Commands' + ' '.repeat(Math.max(0, w - 12)) + '\u2502')
47
- ),
48
- h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
49
- h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
50
- '\u2502 ' + (search || '\u2026') + ' '.repeat(Math.max(0, w - 6)) + '\u2502')
51
- ),
52
- filtered.map((cmd, i) =>
53
- h(Box, {
54
- key: cmd.name, height: 1,
55
- backgroundColor: i === idx ? hex.selectionBg : hex.bg,
56
- },
57
- h(Text, {
58
- color: i === idx ? hex.selectionText : hex.text,
59
- bold: i === idx,
60
- backgroundColor: i === idx ? hex.selectionBg : hex.bg,
61
- }, (i === idx ? '\u276F ' : ' ') + cmd.name + ' ' + cmd.desc)
62
- )
63
- ),
64
- h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
65
- h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
66
- '\u2514' + '\u2500'.repeat(w - 2) + '\u2518')
67
- ),
68
- h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
69
- h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
70
- ' \u2191\u2193 nav \u2192 select Esc close')
71
- ),
72
- );
73
- }
@@ -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,20 +0,0 @@
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
- }
@@ -1,59 +0,0 @@
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,30 +0,0 @@
1
- import React from 'react';
2
- import { Box } from 'ink';
3
- import { hex } from '../config/theme.js';
4
- import { HeaderBar } from './HeaderBar.js';
5
- import { MessageList } from './MessageList.js';
6
- import { InputBar } from './InputBar.js';
7
- import { CommandPicker } from './CommandPicker.js';
8
- import { ModelPicker } from './ModelPicker.js';
9
- const { createElement: h } = React;
10
-
11
- export function Layout({ state, streamContent, model, provider, showCommands, showModels, onCommandSelect, onModelSelect, onCloseCommands, onCloseModels, onSlash, onSubmit }) {
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' },
22
- h(MessageList, {
23
- messages: state.messages, thinking: state.thinking,
24
- streamContent, agentStatus: state.agentStatus,
25
- }),
26
- picker
27
- ),
28
- h(InputBar, { provider, model, agentMode: state.agentMode, thinking: state.thinking, onSlash, onSubmit })
29
- );
30
- }
@@ -1,11 +0,0 @@
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 LoadingIndicator({ label }) {
7
- return h(Box, { height: 1, backgroundColor: hex.surface },
8
- h(Text, { color: hex.blue, backgroundColor: hex.surface }, ' \u25CF'),
9
- h(Text, { color: hex.textDim, backgroundColor: hex.surface }, ' ' + (label || 'processing'))
10
- );
11
- }
@@ -1,92 +0,0 @@
1
- import React, { useMemo } from 'react';
2
- import { Box, Text } from 'ink';
3
- import { hex } 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 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 },
28
- ' \u25B8 ' + (parseInt(text) < 1000 ? text + 'ms' : (parseInt(text) / 1000).toFixed(1) + 's'))
29
- );
30
- case 'tool_line':
31
- return h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
32
- h(Text, { color: hex.purple, backgroundColor: hex.surfaceAlt }, ' \u25C9 ' + (text || ''))
33
- );
34
- case 'sys_line':
35
- return h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
36
- h(Text, { color: hex.green, backgroundColor: hex.surfaceAlt }, ' \u25C9 ' + (text || ''))
37
- );
38
- case 'err_line':
39
- return h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
40
- h(Text, { color: hex.red, backgroundColor: hex.surfaceAlt }, ' \u2716 ' + (text || ''))
41
- );
42
- case 'stream_head':
43
- return h(Box, { height: 1, backgroundColor: hex.surface },
44
- h(Text, { color: hex.purple, bold: true, backgroundColor: hex.surface }, ' \u25C6 CLARITY')
45
- );
46
- case 'stream_status':
47
- return h(Box, { height: 1, backgroundColor: hex.surface },
48
- h(Text, { color: hex.blue, backgroundColor: hex.surface }, ' \u25CF ' + (text || ''))
49
- );
50
- case 'stream_line':
51
- return h(Box, { height: 1, backgroundColor: hex.surface },
52
- h(Text, { color: hex.text, backgroundColor: hex.surface, wrap: 'wrap' }, ' \u2502 ' + (text || ' '))
53
- );
54
- default:
55
- return null;
56
- }
57
- }
58
-
59
- export function MessageList({ messages, thinking, streamContent, agentStatus }) {
60
- const { viewport, contentWidth } = getLayout();
61
-
62
- const entries = useMemo(() => {
63
- return messages.map(m => ({
64
- id: m.id, role: m.role, content: m.content,
65
- duration: m.duration, toolName: m.toolName, error: m.error, completed: true,
66
- }));
67
- }, [messages]);
68
-
69
- const { slice, clipIndex, clipLines } = useMemo(
70
- () => sliceToViewport(entries, viewport, contentWidth),
71
- [entries, viewport, contentWidth]
72
- );
73
-
74
- const rawLines = useMemo(
75
- () => buildLineArray(slice, clipIndex, clipLines, contentWidth),
76
- [slice, clipIndex, clipLines, contentWidth]
77
- );
78
-
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);
83
-
84
- return h(Box, { height: viewport, flexDirection: 'column', overflow: 'hidden' },
85
- padded.map((ln, i) => {
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 },
88
- h(Line, { type: ln.type, text: ln.text, data: ln.data })
89
- );
90
- })
91
- );
92
- }
@@ -1,83 +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 } 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) 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
- return h(Box, { flexDirection: 'column', backgroundColor: hex.bg, width: w },
43
- h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
44
- h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
45
- '\u250C' + '\u2500'.repeat(w - 2) + '\u2510')
46
- ),
47
- h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
48
- h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
49
- '\u2502 Models ' + flat.filter(m => !m._header).length + ' available' + ' '.repeat(Math.max(0, w - 18)) + '\u2502')
50
- ),
51
- h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
52
- h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
53
- '\u2502 ' + (search || '\u2026') + ' '.repeat(Math.max(0, w - 6)) + '\u2502')
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
- }),
74
- h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
75
- h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
76
- '\u2514' + '\u2500'.repeat(w - 2) + '\u2518')
77
- ),
78
- h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
79
- h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
80
- ' \u2191\u2193 nav \u2192 select Esc close')
81
- ),
82
- );
83
- }
@@ -1,27 +0,0 @@
1
- import React, { useState } from 'react';
2
- import { Box, Text } from 'ink';
3
- import { hex } from '../config/theme.js';
4
- const { createElement: h } = React;
5
-
6
- export function ThinkingBlock({ toolResults, duration, collapsed: initialCollapsed }) {
7
- const [collapsed, setCollapsed] = useState(initialCollapsed !== undefined ? initialCollapsed : true);
8
- const items = toolResults || [];
9
- const durStr = duration
10
- ? (duration < 1000 ? duration + 'ms' : (duration / 1000).toFixed(1) + 's')
11
- : '';
12
-
13
- return h(Box, { flexDirection: 'column', backgroundColor: hex.surfaceAlt },
14
- h(Box, { height: 1 },
15
- h(Text, { color: hex.purple, backgroundColor: hex.surfaceAlt },
16
- ' ' + (collapsed ? '\u25B8' : '\u25BE') + ' Thought' + (durStr ? ' (' + durStr + ')' : ''))
17
- ),
18
- collapsed ? null : h(Box, { flexDirection: 'column', backgroundColor: hex.surfaceAlt },
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
- )
25
- )
26
- );
27
- }
@@ -1,36 +0,0 @@
1
- import React from 'react';
2
- import { Box, Text } from 'ink';
3
- import { hex } from '../config/theme.js';
4
- import { truncate } from '../config/layout.js';
5
- const { createElement: h } = React;
6
-
7
- export function ToolCard({ exec, isActive }) {
8
- const name = exec.name || 'tool';
9
- const status = exec.status || 'running';
10
- const dur = exec.duration ? (exec.duration < 1000 ? exec.duration + 'ms' : (exec.duration / 1000).toFixed(1) + 's') : '';
11
- const args = typeof exec.args === 'string' ? exec.args : JSON.stringify(exec.args || {});
12
- const isDone = !isActive && (status === 'completed' || status === 'failed');
13
-
14
- if (isDone) {
15
- const c = status === 'failed' ? hex.red : hex.green;
16
- return h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
17
- h(Text, { color: c, backgroundColor: hex.surfaceAlt }, ' ' + (status === 'failed' ? '\u2716' : '\u25C9') + ' ' + name + (dur ? ' ' + dur : ''))
18
- );
19
- }
20
-
21
- return h(Box, { flexDirection: 'column', backgroundColor: hex.surfaceAlt },
22
- h(Box, { height: 1 },
23
- h(Text, { color: hex.purple, backgroundColor: hex.surfaceAlt }, ' \u25C9 ' + name + (dur ? ' ' + dur : ''))
24
- ),
25
- args.length < 80
26
- ? h(Box, { height: 1 },
27
- h(Text, { color: hex.textDim, backgroundColor: hex.surfaceAlt }, ' \u25B8 ' + truncate(args, 56))
28
- )
29
- : null,
30
- status === 'running'
31
- ? h(Box, { height: 1 },
32
- h(Text, { color: hex.blue, backgroundColor: hex.surfaceAlt }, ' \u25CF running')
33
- )
34
- : null,
35
- );
36
- }