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