clarity-ai 4.0.0 → 4.2.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.
Files changed (116) hide show
  1. package/bin/clarity.js +18 -5
  2. package/package.json +11 -14
  3. package/src/app.js +76 -0
  4. package/src/chat.js +172 -0
  5. package/{ui → src/components}/Banner.js +1 -1
  6. package/src/components/CodeBlock.js +13 -0
  7. package/src/components/CommandPicker.js +42 -0
  8. package/src/components/ErrorMessage.js +26 -0
  9. package/src/components/InputArea.js +33 -0
  10. package/src/components/LoadingIndicator.js +11 -0
  11. package/src/components/MessageBubble.js +66 -0
  12. package/src/components/MessageList.js +16 -0
  13. package/src/components/ModelPicker.js +35 -0
  14. package/src/components/PromptBox.js +41 -0
  15. package/src/components/StatusBar.js +22 -0
  16. package/src/components/ThoughtBlock.js +17 -0
  17. package/src/config/keys.js +34 -0
  18. package/src/config/models.js +15 -0
  19. package/src/config/themes.js +17 -0
  20. package/src/hooks/useHistory.js +28 -0
  21. package/src/hooks/useScroll.js +19 -0
  22. package/src/providers/errors.js +22 -0
  23. package/src/providers/index.js +53 -0
  24. package/src/providers/streaming.js +41 -0
  25. package/src/renderer/diff.js +22 -0
  26. package/src/renderer/markdown.js +53 -0
  27. package/src/renderer/table.js +25 -0
  28. package/src/tools.js +118 -0
  29. package/src/utils/formatTokens.js +11 -0
  30. package/src/utils/wrapText.js +8 -0
  31. package/AGENTS.md.old +0 -51
  32. package/core/agent.js +0 -52
  33. package/core/config.js +0 -39
  34. package/core/keyCheck.js +0 -21
  35. package/core/providers/index.js +0 -79
  36. package/core/tools.js +0 -178
  37. package/src.old/agents/code-agent.js +0 -18
  38. package/src.old/agents/file-agent.js +0 -24
  39. package/src.old/agents/git-agent.js +0 -27
  40. package/src.old/agents/loop.js +0 -303
  41. package/src.old/agents/monitor-agent.js +0 -22
  42. package/src.old/agents/planner.js +0 -21
  43. package/src.old/agents/shell-agent.js +0 -11
  44. package/src.old/agents/web-agent.js +0 -16
  45. package/src.old/commands/agent.js +0 -14
  46. package/src.old/commands/chat.js +0 -8
  47. package/src.old/commands/clear.js +0 -5
  48. package/src.old/commands/config.js +0 -19
  49. package/src.old/commands/diff.js +0 -12
  50. package/src.old/commands/exit.js +0 -5
  51. package/src.old/commands/export.js +0 -12
  52. package/src.old/commands/fetch.js +0 -16
  53. package/src.old/commands/git.js +0 -12
  54. package/src.old/commands/help.js +0 -32
  55. package/src.old/commands/history.js +0 -13
  56. package/src.old/commands/index.js +0 -125
  57. package/src.old/commands/keys.js +0 -30
  58. package/src.old/commands/memory.js +0 -23
  59. package/src.old/commands/model.js +0 -100
  60. package/src.old/commands/provider.js +0 -26
  61. package/src.old/commands/run.js +0 -16
  62. package/src.old/commands/search.js +0 -13
  63. package/src.old/commands/task.js +0 -24
  64. package/src.old/commands/tools.js +0 -43
  65. package/src.old/commands/undo.js +0 -10
  66. package/src.old/config/settings.js +0 -35
  67. package/src.old/core/context.js +0 -39
  68. package/src.old/core/history.js +0 -27
  69. package/src.old/core/memory.js +0 -40
  70. package/src.old/core/setup.js +0 -87
  71. package/src.old/main.js +0 -88
  72. package/src.old/providers/claude.js +0 -35
  73. package/src.old/providers/deepseek.js +0 -47
  74. package/src.old/providers/gemini.js +0 -35
  75. package/src.old/providers/groq.js +0 -99
  76. package/src.old/providers/index.js +0 -55
  77. package/src.old/providers/openai.js +0 -47
  78. package/src.old/providers/openrouter.js +0 -49
  79. package/src.old/tools/agent-spawn.js +0 -13
  80. package/src.old/tools/bash.js +0 -9
  81. package/src.old/tools/clipboard-tool.js +0 -14
  82. package/src.old/tools/code-runner.js +0 -17
  83. package/src.old/tools/compress-tool.js +0 -21
  84. package/src.old/tools/context-tool.js +0 -16
  85. package/src.old/tools/delete-file.js +0 -11
  86. package/src.old/tools/diff-tool.js +0 -19
  87. package/src.old/tools/edit-file.js +0 -13
  88. package/src.old/tools/env-tool.js +0 -19
  89. package/src.old/tools/git-tool.js +0 -9
  90. package/src.old/tools/grep.js +0 -13
  91. package/src.old/tools/list-dir.js +0 -20
  92. package/src.old/tools/memory-tool.js +0 -18
  93. package/src.old/tools/notify-tool.js +0 -9
  94. package/src.old/tools/pkg-manager.js +0 -19
  95. package/src.old/tools/read-file.js +0 -9
  96. package/src.old/tools/run-tests.js +0 -10
  97. package/src.old/tools/screenshot-tool.js +0 -9
  98. package/src.old/tools/search-files.js +0 -10
  99. package/src.old/tools/task-planner.js +0 -11
  100. package/src.old/tools/version-check.js +0 -12
  101. package/src.old/tools/web-fetch.js +0 -15
  102. package/src.old/tools/web-search.js +0 -18
  103. package/src.old/tools/write-file.js +0 -12
  104. package/src.old/ui/banner.js +0 -45
  105. package/src.old/ui/blocks.js +0 -132
  106. package/src.old/ui/colors.js +0 -41
  107. package/src.old/ui/input.js +0 -29
  108. package/src.old/ui/prompt.js +0 -127
  109. package/src.old/ui/spinner.js +0 -100
  110. package/ui/App.js +0 -129
  111. package/ui/CommandPicker.js +0 -74
  112. package/ui/InputBar.js +0 -33
  113. package/ui/MessageList.js +0 -50
  114. package/ui/ModelPicker.js +0 -96
  115. package/ui/StatusBar.js +0 -21
  116. /package/{core → src}/intentDetect.js +0 -0
package/bin/clarity.js CHANGED
@@ -1,13 +1,26 @@
1
1
  #!/usr/bin/env node
2
2
  import React from 'react';
3
3
  import { render } from 'ink';
4
- import { App } from '../ui/App.js';
5
- import { loadConfig } from '../core/config.js';
6
- import { ensureApiKey } from '../core/keyCheck.js';
4
+ import { App } from '../src/app.js';
5
+ import { hasKey } from '../src/config/keys.js';
6
+ import { createInterface } from 'readline';
7
7
 
8
8
  async function main() {
9
- const config = loadConfig();
10
- await ensureApiKey(config.provider || 'groq');
9
+ const provider = process.env.CLARITY_PROVIDER || 'groq';
10
+
11
+ if (!hasKey(provider)) {
12
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
13
+ const key = await new Promise(resolve => {
14
+ rl.question('\n\x1b[33m No ' + provider + ' API key found.\x1b[0m\n Enter your ' + provider + ' key: ', (answer) => {
15
+ rl.close();
16
+ resolve(answer.trim());
17
+ });
18
+ });
19
+ const { setKey } = await import('../src/config/keys.js');
20
+ setKey(provider, key);
21
+ }
22
+
23
+ const config = { provider, model: process.env.CLARITY_MODEL || 'groq/llama-3.3-70b-versatile' };
11
24
  render(React.createElement(App, { config }), { fullscreen: true });
12
25
  }
13
26
 
package/package.json CHANGED
@@ -1,28 +1,25 @@
1
1
  {
2
2
  "name": "clarity-ai",
3
- "version": "4.0.0",
4
- "description": "Ink+React TUI Autonomous AI Agent CLI for Termux",
3
+ "version": "4.2.0",
4
+ "description": "Premium terminal AI chat for Termux — OpenCode-style UI with prompt box, bg colors, side lines",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "clarity": "bin/clarity.js"
8
8
  },
9
9
  "scripts": {
10
- "start": "node bin/clarity.js",
11
- "dev": "node --watch bin/clarity.js"
10
+ "start": "node bin/clarity.js"
12
11
  },
13
12
  "engines": {
14
13
  "node": ">=18.0.0"
15
14
  },
16
15
  "dependencies": {
17
- "@google/generative-ai": "^0.24.1",
18
- "@inkjs/ui": "^2.0.0",
19
- "@langchain/langgraph": "^1.3.5",
20
- "groq-sdk": "^1.2.1",
21
- "ink": "^7.0.5",
22
- "ink-big-text": "^2.0.0",
23
- "ink-gradient": "^4.0.1",
24
- "ink-select-input": "^6.2.0",
25
- "ink-spinner": "^5.0.0",
26
- "ink-text-input": "^6.0.0"
16
+ "ink": "^5",
17
+ "react": "^18",
18
+ "ink-text-input": "^6.0.0",
19
+ "ink-spinner": "^5",
20
+ "ink-big-text": "^2",
21
+ "ink-gradient": "^3",
22
+ "marked": "^12",
23
+ "cli-highlight": "^2"
27
24
  }
28
25
  }
package/src/app.js ADDED
@@ -0,0 +1,76 @@
1
+ import React, { useState, useCallback } from 'react';
2
+ import { Box } from 'ink';
3
+ import { Banner } from './components/Banner.js';
4
+ import { MessageList } from './components/MessageList.js';
5
+ import { PromptBox } from './components/PromptBox.js';
6
+ import { CommandPicker } from './components/CommandPicker.js';
7
+ import { ModelPicker } from './components/ModelPicker.js';
8
+ import { useScroll } from './hooks/useScroll.js';
9
+ import { createChatState, handleSend, handleCommand } from './chat.js';
10
+ const { createElement: h } = React;
11
+
12
+ export function App({ config }) {
13
+ const [state, setState] = useState(createChatState);
14
+ const [model, setModel] = useState(config.model || 'groq/llama-3.3-70b-versatile');
15
+ const [provider, setProvider] = useState(config.provider || 'groq');
16
+ const [showCommands, setShowCommands] = useState(false);
17
+ const [showModels, setShowModels] = useState(false);
18
+ const [showBanner, setShowBanner] = useState(true);
19
+ const { scrollOffset, termHeight } = useScroll(state.messages.length);
20
+
21
+ const onSubmit = useCallback(async (input) => {
22
+ if (input.startsWith('/')) {
23
+ if (input === '/model' || input === '/models') { setShowModels(true); return; }
24
+ if (input === '/help') { setShowCommands(true); return; }
25
+ await handleCommand(input, state, setState, setModel, setProvider, model, provider);
26
+ return;
27
+ }
28
+ if (showBanner) setShowBanner(false);
29
+ await handleSend(state, setState, input, model, provider);
30
+ }, [state, state.messages, model, provider, showBanner]);
31
+
32
+ function handleCommandSelect(cmd) {
33
+ setShowCommands(false);
34
+ onSubmit(cmd);
35
+ }
36
+
37
+ function handleModelSelect(modelId) {
38
+ const p = modelId.split('/')[0];
39
+ setProvider(p);
40
+ setModel(modelId);
41
+ setShowModels(false);
42
+ setState(s => ({
43
+ ...s,
44
+ messages: [...s.messages, { id: 'sys-' + Date.now(), role: 'system', content: 'Switched to ' + modelId }],
45
+ }));
46
+ }
47
+
48
+ return h(Box, { flexDirection: 'column', height: '100%' },
49
+ h(Box, { flexGrow: 1, flexDirection: 'column' },
50
+ showBanner ? h(Banner) : null,
51
+ h(MessageList, {
52
+ messages: state.messages,
53
+ thinking: state.thinking,
54
+ scrollOffset,
55
+ termHeight,
56
+ })
57
+ ),
58
+ showCommands ? h(CommandPicker, {
59
+ query: '',
60
+ onSelect: handleCommandSelect,
61
+ onClose: () => setShowCommands(false),
62
+ }) : null,
63
+ showModels ? h(ModelPicker, {
64
+ onSelect: handleModelSelect,
65
+ onClose: () => setShowModels(false),
66
+ }) : null,
67
+ h(PromptBox, {
68
+ provider,
69
+ model,
70
+ agentMode: state.agentMode,
71
+ thinking: state.thinking,
72
+ onSlash: () => setShowCommands(true),
73
+ onSubmit,
74
+ })
75
+ );
76
+ }
package/src/chat.js ADDED
@@ -0,0 +1,172 @@
1
+ import { callAI } from './providers/index.js';
2
+ import { setKey } from './config/keys.js';
3
+ import { TOOLS, executeTool } from './tools.js';
4
+ import { extractCommandFromText } from './intentDetect.js';
5
+
6
+ export function createChatState() {
7
+ return {
8
+ messages: [],
9
+ thinking: false,
10
+ streamBuffer: '',
11
+ awaitingKey: false,
12
+ blockedProvider: null,
13
+ agentMode: true,
14
+ tokenCount: 0,
15
+ idCounter: 0,
16
+ };
17
+ }
18
+
19
+ let msgId = 0;
20
+ function nextId() { return 'm' + (++msgId); }
21
+
22
+ export async function handleSend(state, setState, input, model, provider) {
23
+ if (!input.trim() || state.awaitingKey) return;
24
+
25
+ const userMsg = { id: nextId(), role: 'user', content: input };
26
+ setState(s => ({ ...s, messages: [...s.messages, userMsg], thinking: true, streamBuffer: '' }));
27
+
28
+ try {
29
+ const history = [...state.messages, userMsg].map(m => ({
30
+ role: m.role === 'error' ? 'assistant' : m.role,
31
+ content: m.content,
32
+ }));
33
+
34
+ const stream = callAI(provider, model, history, { tools: state.agentMode ? TOOLS : undefined });
35
+ let buffer = '';
36
+
37
+ for await (const event of stream) {
38
+ if (event.type === 'token') {
39
+ buffer += event.content;
40
+ setState(s => ({
41
+ ...s,
42
+ streamBuffer: buffer,
43
+ messages: updateLastAssistant(s.messages, buffer),
44
+ }));
45
+ }
46
+ if (event.type === 'error') {
47
+ handleError(setState, event);
48
+ break;
49
+ }
50
+ }
51
+
52
+ if (buffer) {
53
+ setState(s => ({ ...s, thinking: false, streamBuffer: '' }));
54
+ }
55
+ } catch (err) {
56
+ if (err.type) {
57
+ handleError(setState, err);
58
+ } else {
59
+ setState(s => ({
60
+ ...s, thinking: false,
61
+ messages: [...s.messages, { id: nextId(), role: 'error', content: err.message || 'Unknown error' }],
62
+ }));
63
+ }
64
+ }
65
+ }
66
+
67
+ function handleError(setState, err) {
68
+ if (err.type === 'auth_error') {
69
+ setState(s => ({
70
+ ...s, thinking: false, awaitingKey: true, blockedProvider: err.provider,
71
+ messages: [...s.messages, { id: nextId(), role: 'error', content: err.hint || err.message }],
72
+ }));
73
+ } else if (err.type === 'rate_limit') {
74
+ setState(s => ({
75
+ ...s, thinking: false,
76
+ messages: [...s.messages, { id: nextId(), role: 'error', content: err.message }],
77
+ }));
78
+ } else {
79
+ setState(s => ({
80
+ ...s, thinking: false,
81
+ messages: [...s.messages, { id: nextId(), role: 'error', content: err.message }],
82
+ }));
83
+ }
84
+ }
85
+
86
+ function updateLastAssistant(messages, buffer) {
87
+ const copy = [...messages];
88
+ const last = copy[copy.length - 1];
89
+ if (last && last.role === 'assistant') {
90
+ copy[copy.length - 1] = { ...last, content: buffer, streaming: true };
91
+ } else {
92
+ copy.push({ id: nextId(), role: 'assistant', content: buffer, streaming: true });
93
+ }
94
+ return copy;
95
+ }
96
+
97
+ export async function handleCommand(input, state, setState, modelSetter, providerSetter, model, provider) {
98
+ const parts = input.trim().split(/\s+/);
99
+ const cmd = parts[0];
100
+ const args = parts.slice(1);
101
+
102
+ switch (cmd) {
103
+ case '/keys':
104
+ if (args.length >= 2) {
105
+ setKey(args[0], args[1]);
106
+ setState(s => ({
107
+ ...s, awaitingKey: false, blockedProvider: null,
108
+ messages: [...s.messages, { id: nextId(), role: 'system', content: 'Key saved for ' + args[0] }],
109
+ }));
110
+ } else {
111
+ setState(s => ({
112
+ ...s,
113
+ messages: [...s.messages, { id: nextId(), role: 'error', content: 'Usage: /keys <provider> <key>' }],
114
+ }));
115
+ }
116
+ break;
117
+ case '/agent':
118
+ setState(s => ({
119
+ ...s, agentMode: !s.agentMode,
120
+ messages: [...s.messages, { id: nextId(), role: 'system', content: 'Agent mode ' + (!s.agentMode ? 'ON' : 'OFF') }],
121
+ }));
122
+ break;
123
+ case '/clear':
124
+ setState(s => ({ ...s, messages: [], streamBuffer: '' }));
125
+ break;
126
+ case '/theme':
127
+ setState(s => ({
128
+ ...s,
129
+ messages: [...s.messages, { id: nextId(), role: 'system', content: 'Themes: dark (only theme available)' }],
130
+ }));
131
+ break;
132
+ case '/export': {
133
+ const text = state.messages.map(m => '[' + m.role + '] ' + m.content).join('\n\n');
134
+ const { writeFileSync } = await import('fs');
135
+ const path = 'clarity-export-' + Date.now() + '.md';
136
+ writeFileSync(path, text);
137
+ setState(s => ({
138
+ ...s,
139
+ messages: [...s.messages, { id: nextId(), role: 'system', content: 'Exported to ' + path }],
140
+ }));
141
+ break;
142
+ }
143
+ case '/help': {
144
+ const cmds = [
145
+ '/keys <provider> <key> Set API key',
146
+ '/model Switch model',
147
+ '/provider Switch provider',
148
+ '/agent Toggle agent mode',
149
+ '/clear Clear conversation',
150
+ '/theme Change color theme',
151
+ '/export Export conversation',
152
+ '/help Show this help',
153
+ '/exit Exit CLARITY',
154
+ ];
155
+ for (const line of cmds) {
156
+ setState(s => ({
157
+ ...s,
158
+ messages: [...s.messages, { id: nextId(), role: 'system', content: line }],
159
+ }));
160
+ }
161
+ break;
162
+ }
163
+ case '/exit':
164
+ process.exit(0);
165
+ break;
166
+ default:
167
+ setState(s => ({
168
+ ...s,
169
+ messages: [...s.messages, { id: nextId(), role: 'error', content: 'Unknown command: ' + cmd + '. Type /help for commands.' }],
170
+ }));
171
+ }
172
+ }
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { Box, Text } from 'ink';
2
+ import { Box } from 'ink';
3
3
  import BigText from 'ink-big-text';
4
4
  import Gradient from 'ink-gradient';
5
5
  const { createElement: h } = React;
@@ -0,0 +1,13 @@
1
+ import React from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import highlight from 'cli-highlight';
4
+ const { createElement: h } = React;
5
+
6
+ export function CodeBlock({ code, lang }) {
7
+ const highlighted = lang ? highlight(code, { language: lang, ignoreIllegals: true }) : code;
8
+
9
+ return h(Box, { flexDirection: 'column', paddingLeft: 2, marginY: 1 },
10
+ lang ? h(Text, { color: 'gray', dimColor: true }, lang) : null,
11
+ h(Text, null, highlighted)
12
+ );
13
+ }
@@ -0,0 +1,42 @@
1
+ import React, { useState } from 'react';
2
+ import { Box, Text, useInput } from 'ink';
3
+ const { createElement: h } = React;
4
+
5
+ const COMMANDS = [
6
+ { name: '/keys', desc: 'Set API key for a provider' },
7
+ { name: '/model', desc: 'Switch model' },
8
+ { name: '/provider', desc: 'Switch provider' },
9
+ { name: '/agent', desc: 'Toggle agent mode' },
10
+ { name: '/clear', desc: 'Clear conversation' },
11
+ { name: '/theme', desc: 'Change color theme' },
12
+ { name: '/export', desc: 'Export conversation' },
13
+ { name: '/help', desc: 'Show all commands' },
14
+ { name: '/exit', desc: 'Exit CLARITY' },
15
+ ];
16
+
17
+ export function CommandPicker({ query, onSelect, onClose }) {
18
+ const filtered = COMMANDS.filter(c =>
19
+ c.name.includes(query) || c.desc.toLowerCase().includes(query.toLowerCase())
20
+ );
21
+ const [idx, setIdx] = useState(0);
22
+
23
+ useInput((input, key) => {
24
+ if (key.upArrow) setIdx(i => Math.max(0, i - 1));
25
+ if (key.downArrow) setIdx(i => Math.min(filtered.length - 1, i + 1));
26
+ if (key.return) onSelect(filtered[idx]);
27
+ if (key.escape) onClose();
28
+ });
29
+
30
+ return h(Box, { flexDirection: 'column', paddingX: 2, paddingY: 1 },
31
+ filtered.map((cmd, i) =>
32
+ h(Box, { key: cmd.name },
33
+ h(Text, {
34
+ color: i === idx ? '#00FFD1' : '#F0F0F0',
35
+ bold: i === idx,
36
+ },
37
+ ' ' + cmd.name.padEnd(18)),
38
+ h(Text, { color: '#555555' }, cmd.desc)
39
+ )
40
+ )
41
+ );
42
+ }
@@ -0,0 +1,26 @@
1
+ import React from 'react';
2
+ import { Box, Text } from 'ink';
3
+ const { createElement: h } = React;
4
+
5
+ export function ErrorMessage({ raw }) {
6
+ let msg = raw;
7
+ try {
8
+ const parsed = typeof raw === 'string' ? JSON.parse(raw.replace(/^[^{]+/, '')) : raw;
9
+ const code = parsed?.error?.code || parsed?.error?.type || '';
10
+ const message = parsed?.error?.message || raw;
11
+ if (code === 'invalid_api_key' || code === 'authentication_error' || raw.includes('auth_error') || raw.includes('401')) {
12
+ msg = 'Auth failed — invalid API key';
13
+ } else if (message.includes('rate limit') || raw.includes('429')) {
14
+ msg = 'Rate limited — try again in a moment';
15
+ } else if (message.includes('model')) {
16
+ msg = 'Model error — ' + message;
17
+ } else {
18
+ msg = String(message).slice(0, 120);
19
+ }
20
+ } catch {}
21
+
22
+ return h(Box, { paddingLeft: 2, marginBottom: 1 },
23
+ h(Text, { color: 'red' }, '\u2717 '),
24
+ h(Text, { color: 'red' }, msg)
25
+ );
26
+ }
@@ -0,0 +1,33 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import TextInput from 'ink-text-input';
4
+ const { createElement: h } = React;
5
+
6
+ export function InputArea({ onSubmit, onSlash, thinking, inputHistory, historyGoBack, historyGoForward }) {
7
+ const [value, setValue] = useState('');
8
+
9
+ function handleChange(val) {
10
+ setValue(val);
11
+ if (val === '/') {
12
+ setValue('');
13
+ onSlash?.();
14
+ }
15
+ }
16
+
17
+ function handleSubmit(val) {
18
+ setValue('');
19
+ onSubmit(val);
20
+ }
21
+
22
+ return h(Box, { flexDirection: 'column', paddingX: 1 },
23
+ h(Box, null,
24
+ h(Text, { color: '#00FFD1' }, '> '),
25
+ h(TextInput, {
26
+ value,
27
+ onChange: handleChange,
28
+ onSubmit: handleSubmit,
29
+ placeholder: thinking ? 'Thinking...' : 'Ask anything or type / for commands',
30
+ })
31
+ )
32
+ );
33
+ }
@@ -0,0 +1,11 @@
1
+ import React from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import Spinner from 'ink-spinner';
4
+ const { createElement: h } = React;
5
+
6
+ export function LoadingIndicator({ label }) {
7
+ return h(Box, { paddingLeft: 2, marginBottom: 1 },
8
+ h(Text, { color: 'cyan' }, h(Spinner, { type: 'dots' })),
9
+ h(Text, { color: 'gray' }, ' ' + (label || 'Thinking'))
10
+ );
11
+ }
@@ -0,0 +1,66 @@
1
+ import React from 'react';
2
+ import { Box, Text } from 'ink';
3
+ const { createElement: h } = React;
4
+ const termWidth = () => process.stdout.columns || 80;
5
+
6
+ export function MessageBubble({ msg }) {
7
+ if (msg.role === 'user') {
8
+ const lines = String(msg.content).split('\n');
9
+ return h(Box, { flexDirection: 'column', marginBottom: 1 },
10
+ h(Box, null,
11
+ h(Text, { color: '#9B59FF', bold: true }, '\u276f YOU '),
12
+ h(Text, { color: '#9B59FF' }, '\u2500'.repeat(Math.max(0, termWidth() - 9)))
13
+ ),
14
+ lines.map((line, i) =>
15
+ h(Text, { key: i, color: '#C39BD3', backgroundColor: '#2D1B4E' },
16
+ ' ' + line
17
+ )
18
+ )
19
+ );
20
+ }
21
+
22
+ if (msg.role === 'assistant') {
23
+ const lines = String(msg.content).split('\n');
24
+ return h(Box, { flexDirection: 'column', marginBottom: 1 },
25
+ h(Box, null,
26
+ h(Text, { color: '#7B2FFF', bold: true }, '\u25c6 CLARITY '),
27
+ h(Text, { color: '#555555' }, '\u2500'.repeat(Math.max(0, termWidth() - 13)))
28
+ ),
29
+ lines.map((line, i) =>
30
+ h(Box, { key: i },
31
+ h(Text, { color: '#7B2FFF' }, '\u2502'),
32
+ h(Text, { color: '#F0F0F0' }, ' ' + line)
33
+ )
34
+ )
35
+ );
36
+ }
37
+
38
+ if (msg.role === 'tool') {
39
+ return h(Box, { flexDirection: 'column', marginBottom: 1 },
40
+ h(Box, null,
41
+ h(Text, { color: '#FFD700', bold: true }, '\u2699 TOOL '),
42
+ h(Text, { color: '#555555' }, '\u2500'.repeat(Math.max(0, termWidth() - 11)))
43
+ ),
44
+ h(Box, { paddingLeft: 2 },
45
+ h(Text, { color: '#AAAAAA' }, String(msg.content))
46
+ )
47
+ );
48
+ }
49
+
50
+ if (msg.role === 'error') {
51
+ let display = String(msg.content).slice(0, 120);
52
+ return h(Box, { paddingLeft: 2, marginBottom: 1 },
53
+ h(Text, { color: '#FF4455' }, '\u2716 '),
54
+ h(Text, { color: '#FF4455' }, display)
55
+ );
56
+ }
57
+
58
+ if (msg.role === 'system') {
59
+ return h(Box, { paddingLeft: 2, marginBottom: 1 },
60
+ h(Text, { color: '#00FF88' }, '\u2714 '),
61
+ h(Text, { color: '#00FF88' }, String(msg.content))
62
+ );
63
+ }
64
+
65
+ return null;
66
+ }
@@ -0,0 +1,16 @@
1
+ import React from 'react';
2
+ import { Box } from 'ink';
3
+ import { MessageBubble } from './MessageBubble.js';
4
+ import { LoadingIndicator } from './LoadingIndicator.js';
5
+ const { createElement: h } = React;
6
+
7
+ export function MessageList({ messages, thinking, scrollOffset, termHeight }) {
8
+ const visible = messages.slice(scrollOffset, scrollOffset + termHeight);
9
+
10
+ return h(Box, { flexDirection: 'column' },
11
+ visible.map((msg, i) =>
12
+ h(MessageBubble, { key: msg.id || i, msg })
13
+ ),
14
+ thinking ? h(LoadingIndicator, { label: 'Thinking' }) : null
15
+ );
16
+ }
@@ -0,0 +1,35 @@
1
+ import React, { useState, useMemo } from 'react';
2
+ import { Box, Text, useInput } from 'ink';
3
+ import { ALL_MODELS } from '../config/models.js';
4
+ const { createElement: h } = React;
5
+
6
+ export function ModelPicker({ onSelect, onClose }) {
7
+ const [search, setSearch] = useState('');
8
+ const filtered = useMemo(() =>
9
+ ALL_MODELS.filter(m => m.id.toLowerCase().includes(search.toLowerCase())),
10
+ [search]
11
+ );
12
+ const [idx, setIdx] = useState(0);
13
+
14
+ useInput((input, key) => {
15
+ if (key.upArrow) setIdx(i => Math.max(0, i - 1));
16
+ if (key.downArrow) setIdx(i => Math.min(filtered.length - 1, i + 1));
17
+ if (key.return) { onSelect(filtered[idx]?.id); return; }
18
+ if (key.escape) onClose();
19
+ if (key.backspace) setSearch(s => s.slice(0, -1));
20
+ else if (input && !key.ctrl && !key.meta) setSearch(s => s + input);
21
+ });
22
+
23
+ return h(Box, { flexDirection: 'column', paddingX: 2, paddingY: 1 },
24
+ h(Text, { color: search ? '#F0F0F0' : '#555555' }, 'Search: ' + (search || '')),
25
+ filtered.map((m, i) =>
26
+ h(Box, { key: m.id, justifyContent: 'space-between' },
27
+ h(Text, {
28
+ color: i === idx ? '#00FFD1' : '#F0F0F0',
29
+ bold: i === idx,
30
+ }, ' ' + m.id),
31
+ m.badge ? h(Text, { color: '#555555' }, '[' + m.badge + ']') : null
32
+ )
33
+ )
34
+ );
35
+ }
@@ -0,0 +1,41 @@
1
+ import React, { useState } from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import TextInput from 'ink-text-input';
4
+ const { createElement: h } = React;
5
+
6
+ export function PromptBox({ provider, model, agentMode, thinking, onSlash, onSubmit }) {
7
+ const [value, setValue] = useState('');
8
+ const w = process.stdout.columns || 80;
9
+
10
+ function handleChange(val) {
11
+ setValue(val);
12
+ if (val === '/') { setValue(''); onSlash?.(); }
13
+ }
14
+
15
+ function handleSubmit(val) {
16
+ setValue('');
17
+ onSubmit(val);
18
+ }
19
+
20
+ return h(Box, { flexDirection: 'column', flexShrink: 0 },
21
+ h(Text, null,
22
+ h(Text, { color: '#333333' }, '\u2502 '),
23
+ h(Text, { color: '#00FFFF' }, '\u276f '),
24
+ h(Text, { color: '#555555' }, provider + '/' + model + ' '),
25
+ h(Text, { color: agentMode ? '#00FF9F' : '#555555' }, agentMode ? '\u2714 agent' : '\u2716 agent'),
26
+ h(Text, { color: '#333333' }, ' '.repeat(Math.max(1, w - (provider + '/' + model).length - 18)) + '\u2502')
27
+ ),
28
+ h(Text, { color: '#333333' },
29
+ '\u2514' + '\u2500'.repeat(Math.max(0, w - 2)) + '\u2518'
30
+ ),
31
+ h(Box, null,
32
+ h(Text, { color: '#00FFFF' }, ' \u276f '),
33
+ h(TextInput, {
34
+ value,
35
+ onChange: handleChange,
36
+ onSubmit: handleSubmit,
37
+ placeholder: thinking ? 'Thinking...' : 'Ask anything or type / for commands',
38
+ })
39
+ )
40
+ );
41
+ }
@@ -0,0 +1,22 @@
1
+ import React from 'react';
2
+ import { Box, Text } from 'ink';
3
+ const { createElement: h } = React;
4
+ const SEP = ' \u00b7 ';
5
+
6
+ export function StatusBar({ provider, model, agentMode, tokenCount }) {
7
+ return h(Box, { paddingX: 1, paddingTop: 1 },
8
+ h(Text, { color: '#00FFD1' }, provider),
9
+ h(Text, { color: '#555555' }, SEP),
10
+ h(Text, { color: '#F0F0F0' }, model),
11
+ h(Text, { color: '#555555' }, SEP),
12
+ h(Text, { color: agentMode ? '#00FF88' : '#555555' }, 'agent:' + (agentMode ? 'ON' : 'OFF')),
13
+ tokenCount ? h(Text, { color: '#555555' }, SEP + 'ctx:' + formatToken(tokenCount)) : null,
14
+ h(Text, { color: '#555555' }, ' ctrl+p commands'),
15
+ );
16
+ }
17
+
18
+ function formatToken(n) {
19
+ if (!n) return '0';
20
+ if (n < 1000) return String(n);
21
+ return (n / 1000).toFixed(1) + 'k';
22
+ }
@@ -0,0 +1,17 @@
1
+ import React, { useState } from 'react';
2
+ import { Box, Text, useInput } from 'ink';
3
+ const { createElement: h } = React;
4
+
5
+ export function ThoughtBlock({ text, collapsed = true }) {
6
+ const [open, setOpen] = useState(!collapsed);
7
+
8
+ useInput((input) => {
9
+ if (input === 't') setOpen(o => !o);
10
+ });
11
+
12
+ return h(Box, { flexDirection: 'column', paddingLeft: 2, marginBottom: 1 },
13
+ h(Text, { color: 'gray', dimColor: true },
14
+ (open ? '\u21b3 ' : '\u27f3 ') + (open ? text : 'Thinking... (t to expand)')
15
+ )
16
+ );
17
+ }