clarity-ai 7.0.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,16 @@
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
+
5
15
  ## 7.0.0 (2026-06-06)
6
16
 
7
17
  ### Ground-up architectural rebuild — centered minimalist terminal platform
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clarity-ai",
3
- "version": "7.0.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": {
@@ -28,7 +28,6 @@
28
28
  "gradient-string": "^3",
29
29
  "ink": "^5",
30
30
  "ink-spinner": "^5",
31
- "ink-text-input": "^6.0.0",
32
31
  "react": "^18",
33
32
  "string-width": "^7",
34
33
  "wrap-ansi": "^9"
package/src/app.js CHANGED
@@ -3,9 +3,10 @@ import { Box, Text, useStdout } from 'ink';
3
3
  import { createChatState, handleSend, handleCommand } from './chat.js';
4
4
  import { hex } from './config/theme.js';
5
5
  import { getLayout } from './config/layout.js';
6
- import { Header } from './components/Header.js';
6
+ import { Banner } from './components/Banner.js';
7
+ import { StatusBar } from './components/StatusBar.js';
7
8
  import { StreamView } from './components/StreamView.js';
8
- import { PromptCard } from './components/PromptCard.js';
9
+ import { InputPanel } from './components/InputPanel.js';
9
10
  import { Footer } from './components/Footer.js';
10
11
  const { createElement: h } = React;
11
12
 
@@ -26,11 +27,18 @@ export function cancelStream() {
26
27
  }
27
28
  }
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
+
29
36
  export function App({ config }) {
30
37
  const { stdout } = useStdout();
31
38
  const [dims, setDims] = useState({ rows: stdout.rows || 30, cols: stdout.columns || 80 });
32
39
  const [state, setState] = useState(() => createChatState());
33
40
  const [streamContent, setStreamContent] = useState('');
41
+ const [thinkingStart, setThinkingStart] = useState(null);
34
42
  const defaultModel = (config.model || 'groq/llama-3.3-70b-versatile').replace(/^[^/]+\//, '');
35
43
  const [model, setModel] = useState(defaultModel);
36
44
  const [provider, setProvider] = useState(config.provider || 'groq');
@@ -50,11 +58,18 @@ export function App({ config }) {
50
58
  return () => process.stdout.removeListener('resize', onResize);
51
59
  }, []);
52
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
+
53
69
  const cols = dims.cols;
54
70
  const rows = dims.rows;
55
- const isCompact = cols < 50;
56
- const cardWidth = Math.min(cols - 4, 55);
57
- const contentPad = isCompact ? 1 : 2;
71
+ const status = deriveStatus(state, streamContent);
72
+ const cardWidth = Math.min(cols - 4, 56);
58
73
 
59
74
  const onSubmit = useCallback(async (input) => {
60
75
  if (input === '/exit') { process.exit(0); return; }
@@ -86,17 +101,18 @@ export function App({ config }) {
86
101
 
87
102
  return h(Box, { width: '100%', height: '100%', flexDirection: 'column', alignItems: 'center', backgroundColor: hex.bg },
88
103
  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 })
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 })
92
108
  ),
93
- h(PromptCard, {
109
+ h(Box, { height: 1 }),
110
+ h(InputPanel, {
94
111
  width: cardWidth,
95
- compact: isCompact,
96
112
  provider,
97
113
  model,
98
114
  agentMode: state.agentMode,
99
- thinking: state.thinking,
115
+ status,
100
116
  onSubmit: handleInputSubmit,
101
117
  }),
102
118
  h(Box, { flexGrow: 1, minHeight: 1 }),
@@ -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
+ }
@@ -4,19 +4,20 @@ import { hex } from '../config/theme.js';
4
4
  const { createElement: h } = React;
5
5
 
6
6
  export function Footer({ cols }) {
7
- const showFull = cols >= 60;
7
+ const showFull = cols >= 58;
8
8
  const keybindText = 'tab agents ctrl+p commands';
9
- const tipText = 'Tip: Use /help to view system commands and switch active models';
10
9
 
11
10
  return h(Box, { width: '100%', flexDirection: 'column', backgroundColor: hex.bg },
12
- h(Box, { height: 1, justifyContent: 'flex-end', paddingRight: 2 },
11
+ h(Box, { height: 1, justifyContent: 'flex-end', paddingRight: 2, backgroundColor: hex.bg },
13
12
  h(Text, { color: hex.textMuted }, keybindText)
14
13
  ),
15
- h(Box, { height: 1, paddingLeft: 2, paddingRight: 2 },
14
+ h(Box, { height: 1, paddingLeft: 2, paddingRight: 2, backgroundColor: hex.bg },
16
15
  h(Text, { color: hex.textDim },
17
16
  '\u2580 ',
18
- h(Text, { color: hex.blue }, 'Tip'),
19
- h(Text, { color: hex.textDim }, ': ' + (showFull ? tipText : 'Use /help for commands'))
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'))
20
21
  )
21
22
  )
22
23
  );
@@ -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
+ }
@@ -0,0 +1,45 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import { hex } from '../config/theme.js';
4
+ const { createElement: h } = React;
5
+
6
+ export function StatusBar({ status, thinkingStart }) {
7
+ const [elapsed, setElapsed] = useState(0);
8
+
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;
45
+ }
@@ -1,77 +1,103 @@
1
- import React, { useMemo } from 'react';
1
+ import React, { useMemo, useRef, useEffect } from 'react';
2
2
  import { Box, Text } from 'ink';
3
3
  import { hex } from '../config/theme.js';
4
4
  import wrapAnsi from 'wrap-ansi';
5
- import cliTruncate from 'cli-truncate';
5
+ import stringWidth from 'string-width';
6
6
  const { createElement: h } = React;
7
7
 
8
- function wrap(text, width) {
9
- if (!text) return '';
10
- return wrapAnsi(String(text), Math.max(1, width), { trim: false, hard: true });
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
11
  }
12
12
 
13
- function trunc(text, width) {
14
- return cliTruncate(text || '', Math.max(1, width), { position: 'end' });
15
- }
13
+ function MessageBlock({ msg, width }) {
14
+ const lines = useMemo(() => wrapText(msg.content, width), [msg.content, width]);
16
15
 
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
- );
16
+ switch (msg.role) {
23
17
  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))
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
+ )
27
28
  );
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':
29
47
  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))
48
+ h(Text, { color: hex.blue }, ' \u25C9 ' + lines[0] || '')
32
49
  );
33
50
  case 'tool':
34
51
  return h(Box, { height: 1 },
35
- h(Text, { color: hex.green }, ' \u25C9 ' + trunc((line.toolName || 'tool') + (line.duration ? ' ' + line.duration + 'ms' : ''), width - 6))
52
+ h(Text, { color: hex.green }, ' \u25C9 ' + (msg.toolName || 'tool') + (msg.duration ? ' ' + msg.duration + 'ms' : ''))
36
53
  );
37
54
  case 'error':
38
55
  return h(Box, { height: 1 },
39
- h(Text, { color: hex.red }, ' \u2716 ' + trunc(line.content, width - 4))
56
+ h(Text, { color: hex.red }, ' \u2716 ' + lines[0] || msg.content)
40
57
  );
41
58
  default:
42
59
  return null;
43
60
  }
44
61
  }
45
62
 
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))
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
+ )
52
78
  );
53
79
  }
54
80
 
55
- export function StreamView({ messages, streamContent, thinking, width }) {
56
- const contentW = Math.max(1, width - 2);
81
+ export function StreamView({ messages, streamContent, status, width }) {
82
+ const contentW = Math.max(4, width - 2);
57
83
 
58
- const visibleMessages = useMemo(() => {
84
+ const displayMessages = useMemo(() => {
59
85
  if (messages.length === 0) {
60
- return [{ id: 'empty', role: 'system', content: 'CLARITY AI ready \u00B7 Ctrl+P commands \u00B7 /help' }];
86
+ return [{ id: 'welcome', role: 'system', content: 'CLARITY AI ready \u00B7 /help for commands' }];
61
87
  }
62
- return messages.slice(-20);
88
+ return messages;
63
89
  }, [messages]);
64
90
 
65
91
  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 })
92
+ displayMessages.map(m =>
93
+ h(MessageBlock, { key: m.id, msg: m, width: contentW })
68
94
  ),
69
- thinking && streamContent
70
- ? h(StreamingLine, { content: streamContent, width: contentW })
95
+ (status === 'streaming' || status === 'thinking') && streamContent
96
+ ? h(StreamingBlock, { content: streamContent, width: contentW })
71
97
  : null,
72
- thinking && !streamContent
98
+ status === 'thinking' && !streamContent
73
99
  ? h(Box, { height: 1 },
74
- h(Text, { color: hex.blue }, ' \u25CF processing...')
100
+ h(Text, { color: hex.textMuted }, ' \u25CF processing...')
75
101
  )
76
102
  : null,
77
103
  );
@@ -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,11 +0,0 @@
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
- }
@@ -1,50 +0,0 @@
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
- }