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 +22 -0
- package/bin/clarity.js +2 -2
- package/package.json +10 -9
- package/src/app.js +121 -0
- package/src/chat.js +1 -1
- package/src/components/Banner.js +41 -0
- package/src/components/Footer.js +24 -0
- package/src/components/InputPanel.js +39 -0
- package/src/components/StatusBar.js +39 -13
- package/src/components/StreamView.js +104 -0
- package/src/config/theme.js +17 -13
- package/src/components/AppRoot.js +0 -97
- package/src/components/CodeBlock.js +0 -44
- package/src/components/CommandPicker.js +0 -73
- package/src/components/Composer.js +0 -74
- package/src/components/HeaderBar.js +0 -20
- package/src/components/InputBar.js +0 -59
- package/src/components/Layout.js +0 -30
- package/src/components/LoadingIndicator.js +0 -11
- package/src/components/MessageList.js +0 -92
- package/src/components/ModelPicker.js +0 -83
- package/src/components/ThinkingBlock.js +0 -27
- package/src/components/ToolCard.js +0 -36
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/
|
|
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
|
|
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": "
|
|
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
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"ink-spinner": "^5",
|
|
22
|
+
"@inkjs/ui": "^2",
|
|
23
|
+
"ansi-escapes": "^7",
|
|
25
24
|
"chalk": "^5",
|
|
26
|
-
"
|
|
27
|
-
"
|
|
25
|
+
"cli-truncate": "^6",
|
|
26
|
+
"ink-text-input": "^6",
|
|
28
27
|
"figures": "^6",
|
|
29
|
-
"
|
|
28
|
+
"gradient-string": "^3",
|
|
29
|
+
"ink": "^5",
|
|
30
|
+
"ink-spinner": "^5",
|
|
31
|
+
"react": "^18",
|
|
30
32
|
"string-width": "^7",
|
|
31
|
-
"
|
|
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 './
|
|
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({
|
|
8
|
-
const
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
+
}
|
package/src/config/theme.js
CHANGED
|
@@ -2,33 +2,37 @@ import chalk from 'chalk';
|
|
|
2
2
|
import gradient from 'gradient-string';
|
|
3
3
|
|
|
4
4
|
export const hex = {
|
|
5
|
-
bg: '#
|
|
6
|
-
surface: '#
|
|
7
|
-
surfaceAlt: '#
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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: '#
|
|
20
|
-
textDim: '#
|
|
21
|
-
textMuted: '#
|
|
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
|
-
|
|
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(['#
|
|
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
|
-
}
|
package/src/components/Layout.js
DELETED
|
@@ -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
|
-
}
|