clarity-ai 4.0.0 → 4.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.
Files changed (115) hide show
  1. package/bin/clarity.js +18 -5
  2. package/package.json +17 -14
  3. package/src/app.js +70 -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 +82 -0
  12. package/src/components/MessageList.js +16 -0
  13. package/src/components/ModelPicker.js +35 -0
  14. package/src/components/StatusBar.js +22 -0
  15. package/src/components/ThoughtBlock.js +17 -0
  16. package/src/config/keys.js +34 -0
  17. package/src/config/models.js +15 -0
  18. package/src/config/themes.js +17 -0
  19. package/src/hooks/useHistory.js +28 -0
  20. package/src/hooks/useScroll.js +19 -0
  21. package/src/providers/errors.js +22 -0
  22. package/src/providers/index.js +53 -0
  23. package/src/providers/streaming.js +41 -0
  24. package/src/renderer/diff.js +22 -0
  25. package/src/renderer/markdown.js +53 -0
  26. package/src/renderer/table.js +25 -0
  27. package/src/tools.js +118 -0
  28. package/src/utils/formatTokens.js +11 -0
  29. package/src/utils/wrapText.js +8 -0
  30. package/AGENTS.md.old +0 -51
  31. package/core/agent.js +0 -52
  32. package/core/config.js +0 -39
  33. package/core/keyCheck.js +0 -21
  34. package/core/providers/index.js +0 -79
  35. package/core/tools.js +0 -178
  36. package/src.old/agents/code-agent.js +0 -18
  37. package/src.old/agents/file-agent.js +0 -24
  38. package/src.old/agents/git-agent.js +0 -27
  39. package/src.old/agents/loop.js +0 -303
  40. package/src.old/agents/monitor-agent.js +0 -22
  41. package/src.old/agents/planner.js +0 -21
  42. package/src.old/agents/shell-agent.js +0 -11
  43. package/src.old/agents/web-agent.js +0 -16
  44. package/src.old/commands/agent.js +0 -14
  45. package/src.old/commands/chat.js +0 -8
  46. package/src.old/commands/clear.js +0 -5
  47. package/src.old/commands/config.js +0 -19
  48. package/src.old/commands/diff.js +0 -12
  49. package/src.old/commands/exit.js +0 -5
  50. package/src.old/commands/export.js +0 -12
  51. package/src.old/commands/fetch.js +0 -16
  52. package/src.old/commands/git.js +0 -12
  53. package/src.old/commands/help.js +0 -32
  54. package/src.old/commands/history.js +0 -13
  55. package/src.old/commands/index.js +0 -125
  56. package/src.old/commands/keys.js +0 -30
  57. package/src.old/commands/memory.js +0 -23
  58. package/src.old/commands/model.js +0 -100
  59. package/src.old/commands/provider.js +0 -26
  60. package/src.old/commands/run.js +0 -16
  61. package/src.old/commands/search.js +0 -13
  62. package/src.old/commands/task.js +0 -24
  63. package/src.old/commands/tools.js +0 -43
  64. package/src.old/commands/undo.js +0 -10
  65. package/src.old/config/settings.js +0 -35
  66. package/src.old/core/context.js +0 -39
  67. package/src.old/core/history.js +0 -27
  68. package/src.old/core/memory.js +0 -40
  69. package/src.old/core/setup.js +0 -87
  70. package/src.old/main.js +0 -88
  71. package/src.old/providers/claude.js +0 -35
  72. package/src.old/providers/deepseek.js +0 -47
  73. package/src.old/providers/gemini.js +0 -35
  74. package/src.old/providers/groq.js +0 -99
  75. package/src.old/providers/index.js +0 -55
  76. package/src.old/providers/openai.js +0 -47
  77. package/src.old/providers/openrouter.js +0 -49
  78. package/src.old/tools/agent-spawn.js +0 -13
  79. package/src.old/tools/bash.js +0 -9
  80. package/src.old/tools/clipboard-tool.js +0 -14
  81. package/src.old/tools/code-runner.js +0 -17
  82. package/src.old/tools/compress-tool.js +0 -21
  83. package/src.old/tools/context-tool.js +0 -16
  84. package/src.old/tools/delete-file.js +0 -11
  85. package/src.old/tools/diff-tool.js +0 -19
  86. package/src.old/tools/edit-file.js +0 -13
  87. package/src.old/tools/env-tool.js +0 -19
  88. package/src.old/tools/git-tool.js +0 -9
  89. package/src.old/tools/grep.js +0 -13
  90. package/src.old/tools/list-dir.js +0 -20
  91. package/src.old/tools/memory-tool.js +0 -18
  92. package/src.old/tools/notify-tool.js +0 -9
  93. package/src.old/tools/pkg-manager.js +0 -19
  94. package/src.old/tools/read-file.js +0 -9
  95. package/src.old/tools/run-tests.js +0 -10
  96. package/src.old/tools/screenshot-tool.js +0 -9
  97. package/src.old/tools/search-files.js +0 -10
  98. package/src.old/tools/task-planner.js +0 -11
  99. package/src.old/tools/version-check.js +0 -12
  100. package/src.old/tools/web-fetch.js +0 -15
  101. package/src.old/tools/web-search.js +0 -18
  102. package/src.old/tools/write-file.js +0 -12
  103. package/src.old/ui/banner.js +0 -45
  104. package/src.old/ui/blocks.js +0 -132
  105. package/src.old/ui/colors.js +0 -41
  106. package/src.old/ui/input.js +0 -29
  107. package/src.old/ui/prompt.js +0 -127
  108. package/src.old/ui/spinner.js +0 -100
  109. package/ui/App.js +0 -129
  110. package/ui/CommandPicker.js +0 -74
  111. package/ui/InputBar.js +0 -33
  112. package/ui/MessageList.js +0 -50
  113. package/ui/ModelPicker.js +0 -96
  114. package/ui/StatusBar.js +0 -21
  115. /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,31 @@
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.1.0",
4
+ "description": "Premium terminal AI chat for Termux — Ink+React TUI with streaming, markdown, agent mode",
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": "^5",
19
+ "ink-select-input": "^5",
20
+ "ink-spinner": "^5",
21
+ "ink-big-text": "^2",
22
+ "ink-gradient": "^3",
23
+ "marked": "^12",
24
+ "cli-highlight": "^2",
25
+ "wrap-ansi": "^9",
26
+ "string-width": "^7",
27
+ "strip-ansi": "^7",
28
+ "ansi-escapes": "^6",
29
+ "groq-sdk": "^2"
27
30
  }
28
31
  }
package/src/app.js ADDED
@@ -0,0 +1,70 @@
1
+ import React, { useState, useCallback } from 'react';
2
+ import { Box, useApp } from 'ink';
3
+ import { Banner } from './components/Banner.js';
4
+ import { MessageList } from './components/MessageList.js';
5
+ import { InputArea } from './components/InputArea.js';
6
+ import { StatusBar } from './components/StatusBar.js';
7
+ import { CommandPicker } from './components/CommandPicker.js';
8
+ import { ModelPicker } from './components/ModelPicker.js';
9
+ import { useScroll } from './hooks/useScroll.js';
10
+ import { useHistory } from './hooks/useHistory.js';
11
+ import { createChatState, handleSend, handleCommand } from './chat.js';
12
+ const { createElement: h } = React;
13
+
14
+ export function App({ config }) {
15
+ const [state, setState] = useState(createChatState);
16
+ const [model, setModel] = useState(config.model || 'groq/llama-3.3-70b-versatile');
17
+ const [provider, setProvider] = useState(config.provider || 'groq');
18
+ const [showCommands, setShowCommands] = useState(false);
19
+ const [showModels, setShowModels] = useState(false);
20
+ const [showBanner, setShowBanner] = useState(true);
21
+ const { scrollOffset, termHeight } = useScroll(state.messages.length);
22
+ const { goBack, goForward } = useHistory();
23
+
24
+ const onSubmit = useCallback(async (input) => {
25
+ if (input.startsWith('/')) {
26
+ if (input === '/model' || input === '/models') { setShowModels(true); return; }
27
+ if (input === '/help') { setShowCommands(true); return; }
28
+ await handleCommand(input, state, setState, setModel, setProvider, model, provider);
29
+ return;
30
+ }
31
+ if (showBanner) setShowBanner(false);
32
+ await handleSend(state, setState, input, model, provider);
33
+ }, [state, state.messages, model, provider, showBanner]);
34
+
35
+ function handleCommandSelect(cmd) {
36
+ setShowCommands(false);
37
+ onSubmit(cmd);
38
+ }
39
+
40
+ function handleModelSelect(modelId) {
41
+ const p = modelId.split('/')[0];
42
+ setProvider(p);
43
+ setModel(modelId);
44
+ setShowModels(false);
45
+ setState(s => ({
46
+ ...s,
47
+ messages: [...s.messages, { id: 'sys-' + Date.now(), role: 'system', content: 'Switched to ' + modelId }],
48
+ }));
49
+ }
50
+
51
+ const messages = state.messages;
52
+
53
+ return h(Box, { flexDirection: 'column', height: '100%' },
54
+ h(Box, { flexGrow: 1, flexDirection: 'column', overflowY: 'hidden' },
55
+ showBanner ? h(Banner) : null,
56
+ h(MessageList, { messages, thinking: state.thinking, scrollOffset, termHeight })
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(InputArea, { onSubmit, onSlash: () => setShowCommands(true), thinking: state.thinking }),
68
+ h(StatusBar, { provider, model, agentMode: state.agentMode })
69
+ );
70
+ }
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,82 @@
1
+ import React from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import { marked } from 'marked';
4
+ import { renderMarkdown } from '../renderer/markdown.js';
5
+ import { renderTable } from '../renderer/table.js';
6
+ import { renderDiff } from '../renderer/diff.js';
7
+ import { ErrorMessage } from './ErrorMessage.js';
8
+ const { createElement: h } = React;
9
+
10
+ const termWidth = () => process.stdout.columns || 80;
11
+
12
+ function YouMessage({ text }) {
13
+ return h(Box, { flexDirection: 'column', marginBottom: 1 },
14
+ h(Box, null,
15
+ h(Text, { color: '#00FFD1', bold: true }, 'YOU '),
16
+ h(Text, { color: '#FF2EF7' }, '\u2500'.repeat(Math.max(0, termWidth() - 7)))
17
+ ),
18
+ h(Box, { paddingLeft: 2 },
19
+ h(Text, { wrap: 'wrap' }, text)
20
+ )
21
+ );
22
+ }
23
+
24
+ function AssistantMessage({ text, streaming }) {
25
+ let content;
26
+ if (streaming) {
27
+ content = h(Text, { wrap: 'wrap' }, text);
28
+ } else {
29
+ try {
30
+ const tokens = marked.lexer(text);
31
+ const rendered = renderMarkdown(tokens);
32
+ content = h(Box, { flexDirection: 'column' }, ...rendered);
33
+ } catch {
34
+ content = h(Text, { wrap: 'wrap' }, text);
35
+ }
36
+ }
37
+
38
+ return h(Box, { flexDirection: 'column', marginBottom: 1 },
39
+ h(Box, null,
40
+ h(Text, { color: '#F0F0F0', bold: true }, 'CLARITY '),
41
+ h(Text, { color: '#555555' }, '\u2500'.repeat(Math.max(0, termWidth() - 11)))
42
+ ),
43
+ h(Box, { paddingLeft: 2 }, content)
44
+ );
45
+ }
46
+
47
+ function ToolMessage({ text }) {
48
+ const lines = text.split('\n');
49
+ const header = lines[0];
50
+ const body = lines.slice(1).join('\n');
51
+
52
+ return h(Box, { flexDirection: 'column', marginBottom: 1 },
53
+ h(Box, null,
54
+ h(Text, { color: '#FFD700', bold: true }, 'TOOL '),
55
+ h(Text, { color: '#555555' }, '\u2500'.repeat(Math.max(0, termWidth() - 8)))
56
+ ),
57
+ h(Box, { paddingLeft: 2 },
58
+ h(Text, { color: '#FFD700', dimColor: true }, header)
59
+ ),
60
+ body ? h(Box, { paddingLeft: 2 },
61
+ h(Text, { color: '#F0F0F0' }, body)
62
+ ) : null
63
+ );
64
+ }
65
+
66
+ function SystemMessage({ text }) {
67
+ return h(Box, { paddingLeft: 2, marginBottom: 1 },
68
+ h(Text, { color: '#00FF88' }, '\u2714 '),
69
+ h(Text, { color: '#00FF88' }, text)
70
+ );
71
+ }
72
+
73
+ export function MessageBubble({ msg }) {
74
+ if (msg.role === 'user') return h(YouMessage, { text: msg.content });
75
+ if (msg.role === 'assistant') return h(AssistantMessage, { text: msg.content, streaming: msg.streaming });
76
+ if (msg.role === 'tool') return h(ToolMessage, { text: msg.content });
77
+ if (msg.role === 'error') {
78
+ return h(ErrorMessage, { raw: msg.content });
79
+ }
80
+ if (msg.role === 'system') return h(SystemMessage, { text: msg.content });
81
+ return null;
82
+ }
@@ -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,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
+ }
@@ -0,0 +1,34 @@
1
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
2
+ import { homedir } from 'os';
3
+ import { join } from 'path';
4
+
5
+ const KEYS_PATH = join(homedir(), '.clarity', 'keys.json');
6
+
7
+ function ensureDir() {
8
+ const dir = join(homedir(), '.clarity');
9
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
10
+ }
11
+
12
+ export function setKey(provider, key) {
13
+ ensureDir();
14
+ const existing = {};
15
+ try {
16
+ const raw = readFileSync(KEYS_PATH, 'utf-8');
17
+ Object.assign(existing, JSON.parse(raw));
18
+ } catch {}
19
+ existing[provider] = key;
20
+ writeFileSync(KEYS_PATH, JSON.stringify(existing, null, 2));
21
+ }
22
+
23
+ export function getKey(provider) {
24
+ try {
25
+ const keys = JSON.parse(readFileSync(KEYS_PATH, 'utf-8'));
26
+ return keys[provider] || process.env[provider.toUpperCase() + '_API_KEY'] || null;
27
+ } catch {
28
+ return process.env[provider.toUpperCase() + '_API_KEY'] || null;
29
+ }
30
+ }
31
+
32
+ export function hasKey(provider) {
33
+ return !!getKey(provider);
34
+ }