clarity-ai 6.3.2 → 6.3.3

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.
@@ -0,0 +1,245 @@
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "metadata": {},
6
+ "source": [
7
+ "# CLARITY Flash 14B — TPU Fine-Tuning\n",
8
+ "Trains a 14B parameter model on agent CoT + tool-calling data.\n",
9
+ "Target: Google Colab TPU v2-8 (free tier)\n",
10
+ "HF token: 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"
11
+ ]
12
+ },
13
+ {
14
+ "cell_type": "code",
15
+ "metadata": {},
16
+ "source": [
17
+ "# === Install ===\n",
18
+ "!pip install -q torch torch-xla torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu\n",
19
+ "!pip install -q transformers datasets accelerate peft bitsandbytes sentencepiece huggingface_hub"
20
+ ],
21
+ "execution_count": null,
22
+ "outputs": []
23
+ },
24
+ {
25
+ "cell_type": "code",
26
+ "metadata": {},
27
+ "source": [
28
+ "# === HF Auth ===\n",
29
+ "from huggingface_hub import login, HfApi, create_repo\n",
30
+ "HF_TOKEN = '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'\n",
31
+ "login(token=HF_TOKEN, add_to_git_credential=True)\n",
32
+ "api = HfApi(token=HF_TOKEN)"
33
+ ],
34
+ "execution_count": null,
35
+ "outputs": []
36
+ },
37
+ {
38
+ "cell_type": "code",
39
+ "metadata": {},
40
+ "source": [
41
+ "# === TPU Setup ===\n",
42
+ "import torch\n",
43
+ "import torch_xla\n",
44
+ "import torch_xla.core.xla_model as xm\n",
45
+ "device = xm.xla_device()\n",
46
+ "print('Device:', device)\n",
47
+ "print('TPU cores:', torch_xla._XLAC._xla_get_num_devices())"
48
+ ],
49
+ "execution_count": null,
50
+ "outputs": []
51
+ },
52
+ {
53
+ "cell_type": "code",
54
+ "metadata": {},
55
+ "source": [
56
+ "# === Data Loading ===\n",
57
+ "import requests\n",
58
+ "import json\n",
59
+ "from datasets import Dataset\n",
60
+ "\n",
61
+ "DATA_URLS = [\n",
62
+ " 'https://huggingface.co/spaces/Universal-618/Clarity-main/main-data',\n",
63
+ " 'https://huggingface.co/spaces/Universal-618/Clarity-2/main-data',\n",
64
+ " 'https://huggingface.co/spaces/Universal-618/Clarity-3/main-data',\n",
65
+ "]\n",
66
+ "\n",
67
+ "all_samples = []\n",
68
+ "for url in DATA_URLS:\n",
69
+ " try:\n",
70
+ " r = requests.get(url, headers={'Authorization': f'Bearer {HF_TOKEN}'}, timeout=60)\n",
71
+ " if r.status_code == 200:\n",
72
+ " data = r.json()\n",
73
+ " samples = data if isinstance(data, list) else data.get('data', [])\n",
74
+ " all_samples.extend(samples)\n",
75
+ " print(f'Loaded {len(samples)} from {url}')\n",
76
+ " except Exception as e:\n",
77
+ " print(f'Skipped {url}: {e}')\n",
78
+ "\n",
79
+ "# Fallback: synthetic CoT samples if no data\n",
80
+ "if len(all_samples) < 10:\n",
81
+ " print('No remote data found — using synthetic samples')\n",
82
+ " all_samples = [\n",
83
+ " {'instruction': 'List files in current directory', 'response': 'I will run the ls command.\\n<tool>bash</tool><cmd>ls -la</cmd>', 'tools': 'bash'},\n",
84
+ " {'instruction': 'Read the file config.json', 'response': 'Let me read that file.\\n<tool>read_file</tool><path>config.json</path>', 'tools': 'read_file'},\n",
85
+ " {'instruction': 'Write hello world script', 'response': 'I will create the file.\\n<tool>write_file</tool><path>hello.py</path><content>print(\"hello\")</content>', 'tools': 'write_file'},\n",
86
+ " ]\n",
87
+ "\n",
88
+ "print(f'Total training samples: {len(all_samples)}')"
89
+ ],
90
+ "execution_count": null,
91
+ "outputs": []
92
+ },
93
+ {
94
+ "cell_type": "code",
95
+ "metadata": {},
96
+ "source": [
97
+ "# === Format for Training ===\n",
98
+ "def format_chat(sample):\n",
99
+ " inst = sample.get('instruction', sample.get('prompt', sample.get('input', '')))\n",
100
+ " resp = sample.get('response', sample.get('completion', sample.get('output', '')))\n",
101
+ " return {\n",
102
+ " 'text': f'<|im_start|>user\\n{inst}<|im_end|>\\n<|im_start|>assistant\\n{resp}<|im_end|>'\n",
103
+ " }\n",
104
+ "\n",
105
+ "dataset = Dataset.from_list([format_chat(s) for s in all_samples])\n",
106
+ "dataset = dataset.train_test_split(test_size=0.05, seed=42)\n",
107
+ "print(f'Train: {len(dataset[\"train\"])}, Test: {len(dataset[\"test\"])}')"
108
+ ],
109
+ "execution_count": null,
110
+ "outputs": []
111
+ },
112
+ {
113
+ "cell_type": "code",
114
+ "metadata": {},
115
+ "source": [
116
+ "# === Load Model ===\n",
117
+ "from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig\n",
118
+ "import torch\n",
119
+ "\n",
120
+ "MODEL_ID = 'Qwen/Qwen2.5-14B-Instruct'\n",
121
+ "\n",
122
+ "bnb = BitsAndBytesConfig(\n",
123
+ " load_in_4bit=True,\n",
124
+ " bnb_4bit_use_double_quant=True,\n",
125
+ " bnb_4bit_quant_type='nf4',\n",
126
+ " bnb_4bit_compute_dtype=torch.bfloat16,\n",
127
+ ")\n",
128
+ "\n",
129
+ "tokenizer = AutoTokenizer.from_pretrained(MODEL_ID, token=HF_TOKEN, trust_remote_code=True)\n",
130
+ "tokenizer.pad_token = tokenizer.eos_token\n",
131
+ "\n",
132
+ "model = AutoModelForCausalLM.from_pretrained(\n",
133
+ " MODEL_ID,\n",
134
+ " quantization_config=bnb,\n",
135
+ " device_map='auto',\n",
136
+ " torch_dtype=torch.bfloat16,\n",
137
+ " token=HF_TOKEN,\n",
138
+ " trust_remote_code=True,\n",
139
+ ")\n",
140
+ "print(f'Model loaded: {MODEL_ID}')"
141
+ ],
142
+ "execution_count": null,
143
+ "outputs": []
144
+ },
145
+ {
146
+ "cell_type": "code",
147
+ "metadata": {},
148
+ "source": [
149
+ "# === LoRA Config ===\n",
150
+ "from peft import LoraConfig, get_peft_model, TaskType\n",
151
+ "\n",
152
+ "lora_config = LoraConfig(\n",
153
+ " task_type=TaskType.CAUSAL_LM,\n",
154
+ " r=16,\n",
155
+ " lora_alpha=32,\n",
156
+ " lora_dropout=0.05,\n",
157
+ " target_modules=['q_proj', 'k_proj', 'v_proj', 'o_proj', 'gate_proj', 'up_proj', 'down_proj'],\n",
158
+ " bias='none',\n",
159
+ ")\n",
160
+ "model = get_peft_model(model, lora_config)\n",
161
+ "model.print_trainable_parameters()"
162
+ ],
163
+ "execution_count": null,
164
+ "outputs": []
165
+ },
166
+ {
167
+ "cell_type": "code",
168
+ "metadata": {},
169
+ "source": [
170
+ "# === Training ===\n",
171
+ "from transformers import TrainingArguments, Trainer, DataCollatorForSeq2Seq\n",
172
+ "import numpy as np\n",
173
+ "\n",
174
+ "def tokenize_fn(examples):\n",
175
+ " tok = tokenizer(examples['text'], truncation=True, max_length=2048, padding=False)\n",
176
+ " tok['labels'] = tok['input_ids'].copy()\n",
177
+ " return tok\n",
178
+ "\n",
179
+ "tokenized = dataset.map(tokenize_fn, remove_columns=['text'], batched=True)\n",
180
+ "\n",
181
+ "args = TrainingArguments(\n",
182
+ " output_dir='./clarity-flash-14b',\n",
183
+ " per_device_train_batch_size=1,\n",
184
+ " gradient_accumulation_steps=16,\n",
185
+ " num_train_epochs=3,\n",
186
+ " learning_rate=2e-4,\n",
187
+ " bf16=True,\n",
188
+ " logging_steps=10,\n",
189
+ " save_steps=200,\n",
190
+ " save_total_limit=2,\n",
191
+ " optim='adamw_8bit',\n",
192
+ " report_to='none',\n",
193
+ " dataloader_drop_last=False,\n",
194
+ ")\n",
195
+ "\n",
196
+ "trainer = Trainer(\n",
197
+ " model=model,\n",
198
+ " args=args,\n",
199
+ " train_dataset=tokenized['train'],\n",
200
+ " eval_dataset=tokenized['test'],\n",
201
+ " data_collator=DataCollatorForSeq2Seq(tokenizer, padding=True),\n",
202
+ ")\n",
203
+ "\n",
204
+ "trainer.train()"
205
+ ],
206
+ "execution_count": null,
207
+ "outputs": []
208
+ },
209
+ {
210
+ "cell_type": "code",
211
+ "metadata": {},
212
+ "source": [
213
+ "# === Push Weights to HF ===\n",
214
+ "WEIGHTS_REPO = 'Universal-618/Clarity-flash-weights'\n",
215
+ "try:\n",
216
+ " create_repo(WEIGHTS_REPO, repo_type='dataset', exist_ok=True, token=HF_TOKEN)\n",
217
+ " print(f'Repo {WEIGHTS_REPO} ready')\n",
218
+ "except Exception as e:\n",
219
+ " print(f'Repo exists or error: {e}')\n",
220
+ "\n",
221
+ "model.push_to_hub(WEIGHTS_REPO, token=HF_TOKEN, use_temp_dir=True)\n",
222
+ "tokenizer.push_to_hub(WEIGHTS_REPO, token=HF_TOKEN)\n",
223
+ "print(f'Weights pushed to {WEIGHTS_REPO}')"
224
+ ],
225
+ "execution_count": null,
226
+ "outputs": []
227
+ }
228
+ ],
229
+ "metadata": {
230
+ "accelerator": "TPU",
231
+ "colab": {
232
+ "provenance": []
233
+ },
234
+ "kernelspec": {
235
+ "display_name": "Python 3",
236
+ "name": "python3"
237
+ },
238
+ "language_info": {
239
+ "name": "python",
240
+ "version": "3.10.0"
241
+ }
242
+ },
243
+ "nbformat": 4,
244
+ "nbformat_minor": 4
245
+ }
@@ -0,0 +1,270 @@
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "metadata": {},
6
+ "source": [
7
+ "# CLARITY Heavy 20B MoE — Multi-GPU Fine-Tuning\n",
8
+ "Trains a 20B Mixture-of-Experts model on deep CoT + recursive tool execution data.\n",
9
+ "Target: Kaggle dual T4 (2x 16GB) with 4-bit quantization + FSDP.\n",
10
+ "HF token: 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"
11
+ ]
12
+ },
13
+ {
14
+ "cell_type": "code",
15
+ "metadata": {},
16
+ "source": [
17
+ "# === Install ===\n",
18
+ "!pip install -q torch transformers datasets accelerate peft bitsandbytes\n",
19
+ "!pip install -q deepspeed sentencepiece huggingface_hub"
20
+ ],
21
+ "execution_count": null,
22
+ "outputs": []
23
+ },
24
+ {
25
+ "cell_type": "code",
26
+ "metadata": {},
27
+ "source": [
28
+ "# === Check GPUs ===\n",
29
+ "import torch\n",
30
+ "n_gpus = torch.cuda.device_count()\n",
31
+ "for i in range(n_gpus):\n",
32
+ " print(f'GPU {i}: {torch.cuda.get_device_name(i)} — {torch.cuda.get_device_properties(i).total_memory / 1e9:.1f} GB')\n",
33
+ "assert n_gpus >= 2, 'Need at least 2 GPUs'"
34
+ ],
35
+ "execution_count": null,
36
+ "outputs": []
37
+ },
38
+ {
39
+ "cell_type": "code",
40
+ "metadata": {},
41
+ "source": [
42
+ "# === HF Auth ===\n",
43
+ "from huggingface_hub import login, HfApi, create_repo\n",
44
+ "HF_TOKEN = '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'\n",
45
+ "login(token=HF_TOKEN, add_to_git_credential=True)\n",
46
+ "api = HfApi(token=HF_TOKEN)"
47
+ ],
48
+ "execution_count": null,
49
+ "outputs": []
50
+ },
51
+ {
52
+ "cell_type": "code",
53
+ "metadata": {},
54
+ "source": [
55
+ "# === Data Loading ===\n",
56
+ "import requests\n",
57
+ "import json\n",
58
+ "from datasets import Dataset, concatenate_datasets\n",
59
+ "\n",
60
+ "DATA_URLS = [\n",
61
+ " 'https://huggingface.co/spaces/Universal-618/Clarity-4/main-data',\n",
62
+ " 'https://huggingface.co/spaces/Universal-618/Clarity-5/main-data',\n",
63
+ " 'https://huggingface.co/spaces/Universal-618/Clarity-6/main-data',\n",
64
+ " 'https://huggingface.co/spaces/Universal-618/Clarity-main/main-data',\n",
65
+ "]\n",
66
+ "\n",
67
+ "all_samples = []\n",
68
+ "for url in DATA_URLS:\n",
69
+ " try:\n",
70
+ " r = requests.get(url, headers={'Authorization': f'Bearer {HF_TOKEN}'}, timeout=120)\n",
71
+ " if r.status_code == 200:\n",
72
+ " data = r.json()\n",
73
+ " samples = data if isinstance(data, list) else data.get('data', [])\n",
74
+ " all_samples.extend(samples)\n",
75
+ " print(f'Loaded {len(samples)} from {url}')\n",
76
+ " except Exception as e:\n",
77
+ " print(f'Skipped {url}: {e}')\n",
78
+ "\n",
79
+ "if len(all_samples) < 10:\n",
80
+ " print('No remote data — generating synthetic deep CoT samples')\n",
81
+ " import random\n",
82
+ " code_snippets = [\n",
83
+ " 'def fib(n): return n if n < 2 else fib(n-1) + fib(n-2)',\n",
84
+ " 'for i in range(10): print(i**2)',\n",
85
+ " 'with open(\"data.txt\") as f: content = f.read()',\n",
86
+ " ]\n",
87
+ " for _ in range(50):\n",
88
+ " cs = random.choice(code_snippets)\n",
89
+ " all_samples.append({\n",
90
+ " 'instruction': f'Write and test a function',\n",
91
+ " 'thinking': f'I need to think step by step. First, I will analyze what the user wants. Then I will write the code. Let me reason through this carefully.',\n",
92
+ " 'response': f'I will write the code now.\\n<tool>bash</tool><cmd>cat > /tmp/test.py << \\'EOF\\'\\n{cs}\\nEOF\\npython3 /tmp/test.py</cmd>',\n",
93
+ " 'tools': 'bash,write_file',\n",
94
+ " })\n",
95
+ "\n",
96
+ "print(f'Total training samples: {len(all_samples)}')"
97
+ ],
98
+ "execution_count": null,
99
+ "outputs": []
100
+ },
101
+ {
102
+ "cell_type": "code",
103
+ "metadata": {},
104
+ "source": [
105
+ "# === Format ===\n",
106
+ "def format_deep_cot(sample):\n",
107
+ " inst = sample.get('instruction', sample.get('prompt', ''))\n",
108
+ " thinking = sample.get('thinking', '')\n",
109
+ " resp = sample.get('response', sample.get('completion', ''))\n",
110
+ " thinking_block = f'<|thinking_start|>{thinking}<|thinking_end|>' if thinking else ''\n",
111
+ " return {\n",
112
+ " 'text': f'<|im_start|>user\\n{inst}<|im_end|>\\n<|im_start|>assistant\\n{thinking_block}{resp}<|im_end|>'\n",
113
+ " }\n",
114
+ "\n",
115
+ "dataset = Dataset.from_list([format_deep_cot(s) for s in all_samples])\n",
116
+ "split = dataset.train_test_split(test_size=0.05, seed=42)\n",
117
+ "print(f'Train: {len(split[\"train\"])}, Test: {len(split[\"test\"])}')"
118
+ ],
119
+ "execution_count": null,
120
+ "outputs": []
121
+ },
122
+ {
123
+ "cell_type": "code",
124
+ "metadata": {},
125
+ "source": [
126
+ "# === Load MoE Model (4-bit) ===\n",
127
+ "from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig\n",
128
+ "\n",
129
+ "MODEL_ID = 'deepseek-ai/DeepSeek-MoE-16B-Chat'\n",
130
+ "\n",
131
+ "bnb = BitsAndBytesConfig(\n",
132
+ " load_in_4bit=True,\n",
133
+ " bnb_4bit_use_double_quant=True,\n",
134
+ " bnb_4bit_quant_type='nf4',\n",
135
+ " bnb_4bit_compute_dtype=torch.bfloat16,\n",
136
+ ")\n",
137
+ "\n",
138
+ "tokenizer = AutoTokenizer.from_pretrained(MODEL_ID, token=HF_TOKEN, trust_remote_code=True)\n",
139
+ "tokenizer.pad_token = tokenizer.eos_token\n",
140
+ "\n",
141
+ "model = AutoModelForCausalLM.from_pretrained(\n",
142
+ " MODEL_ID,\n",
143
+ " quantization_config=bnb,\n",
144
+ " device_map='auto',\n",
145
+ " torch_dtype=torch.bfloat16,\n",
146
+ " token=HF_TOKEN,\n",
147
+ " trust_remote_code=True,\n",
148
+ ")\n",
149
+ "print(f'MoE model loaded: {MODEL_ID}')\n",
150
+ "print(f'Params: {model.num_parameters():,.0f}')"
151
+ ],
152
+ "execution_count": null,
153
+ "outputs": []
154
+ },
155
+ {
156
+ "cell_type": "code",
157
+ "metadata": {},
158
+ "source": [
159
+ "# === LoRA for MoE ===\n",
160
+ "from peft import LoraConfig, get_peft_model, TaskType\n",
161
+ "\n",
162
+ "lora_config = LoraConfig(\n",
163
+ " task_type=TaskType.CAUSAL_LM,\n",
164
+ " r=8,\n",
165
+ " lora_alpha=16,\n",
166
+ " lora_dropout=0.1,\n",
167
+ " target_modules=['q_proj', 'k_proj', 'v_proj', 'o_proj', 'gate_proj', 'up_proj', 'down_proj', 'gate'],\n",
168
+ " bias='none',\n",
169
+ ")\n",
170
+ "model = get_peft_model(model, lora_config)\n",
171
+ "model.print_trainable_parameters()"
172
+ ],
173
+ "execution_count": null,
174
+ "outputs": []
175
+ },
176
+ {
177
+ "cell_type": "code",
178
+ "metadata": {},
179
+ "source": [
180
+ "# === Gradient Checkpointing (prevents OOM) ===\n",
181
+ "model.gradient_checkpointing_enable(gradient_checkpointing_kwargs={'use_reentrant': False})\n",
182
+ "model.config.use_cache = False\n",
183
+ "print('Gradient checkpointing enabled')"
184
+ ],
185
+ "execution_count": null,
186
+ "outputs": []
187
+ },
188
+ {
189
+ "cell_type": "code",
190
+ "metadata": {},
191
+ "source": [
192
+ "# === Training ===\n",
193
+ "from transformers import TrainingArguments, Trainer, DataCollatorForSeq2Seq\n",
194
+ "\n",
195
+ "def tokenize_fn(examples):\n",
196
+ " tok = tokenizer(examples['text'], truncation=True, max_length=2048, padding=False)\n",
197
+ " tok['labels'] = tok['input_ids'].copy()\n",
198
+ " return tok\n",
199
+ "\n",
200
+ "tokenized = split.map(tokenize_fn, remove_columns=['text'], batched=True)\n",
201
+ "\n",
202
+ "args = TrainingArguments(\n",
203
+ " output_dir='./clarity-heavy-20b-moe',\n",
204
+ " per_device_train_batch_size=1,\n",
205
+ " per_device_eval_batch_size=1,\n",
206
+ " gradient_accumulation_steps=8,\n",
207
+ " num_train_epochs=3,\n",
208
+ " learning_rate=1e-4,\n",
209
+ " bf16=True,\n",
210
+ " logging_steps=10,\n",
211
+ " save_steps=200,\n",
212
+ " save_total_limit=2,\n",
213
+ " optim='adamw_8bit',\n",
214
+ " gradient_checkpointing=True,\n",
215
+ " report_to='none',\n",
216
+ " ddp_find_unused_parameters=False,\n",
217
+ ")\n",
218
+ "\n",
219
+ "trainer = Trainer(\n",
220
+ " model=model,\n",
221
+ " args=args,\n",
222
+ " train_dataset=tokenized['train'],\n",
223
+ " eval_dataset=tokenized['test'],\n",
224
+ " data_collator=DataCollatorForSeq2Seq(tokenizer, padding=True, pad_to_multiple_of=8),\n",
225
+ ")\n",
226
+ "\n",
227
+ "trainer.train()"
228
+ ],
229
+ "execution_count": null,
230
+ "outputs": []
231
+ },
232
+ {
233
+ "cell_type": "code",
234
+ "metadata": {},
235
+ "source": [
236
+ "# === Push to HF ===\n",
237
+ "WEIGHTS_REPO = 'Universal-618/Clarity-heavy-weights'\n",
238
+ "try:\n",
239
+ " create_repo(WEIGHTS_REPO, repo_type='dataset', exist_ok=True, token=HF_TOKEN)\n",
240
+ " print(f'Repo {WEIGHTS_REPO} ready')\n",
241
+ "except Exception as e:\n",
242
+ " print(f'Repo notice: {e}')\n",
243
+ "\n",
244
+ "model.push_to_hub(WEIGHTS_REPO, token=HF_TOKEN, use_temp_dir=True)\n",
245
+ "tokenizer.push_to_hub(WEIGHTS_REPO, token=HF_TOKEN)\n",
246
+ "print(f'Weights pushed to https://huggingface.co/datasets/{WEIGHTS_REPO}')"
247
+ ],
248
+ "execution_count": null,
249
+ "outputs": []
250
+ }
251
+ ],
252
+ "metadata": {
253
+ "accelerator": "GPU",
254
+ "kaggle": {
255
+ "accelerator": "GPU",
256
+ "gpuModel": "T4",
257
+ "gpuCount": 2
258
+ },
259
+ "kernelspec": {
260
+ "display_name": "Python 3",
261
+ "name": "python3"
262
+ },
263
+ "language_info": {
264
+ "name": "python",
265
+ "version": "3.10.0"
266
+ }
267
+ },
268
+ "nbformat": 4,
269
+ "nbformat_minor": 4
270
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clarity-ai",
3
- "version": "6.3.2",
3
+ "version": "6.3.3",
4
4
  "description": "Premium terminal AI agent — fixed-height viewport, box-drawing UI, TrueColor theme, streaming with abort",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,7 @@
1
1
  import React, { useState } from 'react';
2
2
  import { Box, Text, useInput } from 'ink';
3
- import { hex, usym } from '../config/theme.js';
3
+ import { hex, sym } from '../config/theme.js';
4
+ import { getLayout } from '../config/layout.js';
4
5
  const { createElement: h } = React;
5
6
 
6
7
  const COMMANDS = [
@@ -8,7 +9,7 @@ const COMMANDS = [
8
9
  { name: '/model', desc: 'Switch model' },
9
10
  { name: '/provider', desc: 'Switch provider' },
10
11
  { name: '/agent', desc: 'Toggle agent mode' },
11
- { name: '/stop', desc: 'Cancel streaming' },
12
+ { name: '/stop', desc: 'Cancel running stream' },
12
13
  { name: '/clear', desc: 'Clear conversation' },
13
14
  { name: '/export', desc: 'Export conversation' },
14
15
  { name: '/help', desc: 'Show all commands' },
@@ -18,6 +19,7 @@ const COMMANDS = [
18
19
  export function CommandPicker({ query, onSelect, onClose }) {
19
20
  const [search, setSearch] = useState('');
20
21
  const [idx, setIdx] = useState(0);
22
+ const { cols } = getLayout();
21
23
 
22
24
  const filtered = COMMANDS.filter(c =>
23
25
  c.name.includes(search) || c.desc.toLowerCase().includes(search.toLowerCase())
@@ -26,40 +28,36 @@ export function CommandPicker({ query, onSelect, onClose }) {
26
28
  useInput((input, key) => {
27
29
  if (key.upArrow) setIdx(i => Math.max(0, i - 1));
28
30
  if (key.downArrow) setIdx(i => Math.min(filtered.length - 1, i + 1));
29
- if (key.return) onSelect(filtered[idx]?.name || '');
31
+ if (key.return && filtered[idx]) onSelect(filtered[idx].name);
30
32
  if (key.escape) onClose();
31
33
  if (key.backspace) setSearch(s => s.slice(0, -1));
32
34
  else if (input && !key.ctrl && !key.meta) setSearch(s => s + input);
33
35
  });
34
36
 
35
- const tw = process.stdout.columns || 80;
36
- const boxWidth = Math.min(tw - 4, 50);
37
+ const w = Math.min(cols - 4, 48);
37
38
 
38
- return h(Box, { flexDirection: 'column', width: boxWidth },
39
- h(Box, { flexDirection: 'row', marginBottom: 1, gap: 1 },
40
- h(Text, { color: hex.textMuted }, usym.bulb),
41
- h(Text, { color: search ? hex.text : hex.textMuted }, search || 'type to filter...'),
39
+ return h(Box, { flexDirection: 'column', backgroundColor: hex.surfaceAlt, width: w },
40
+ h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
41
+ h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt }, ' ' + sym.star + ' ' + (search || 'filter commands...'))
42
42
  ),
43
43
  filtered.map((cmd, i) =>
44
44
  h(Box, {
45
- key: cmd.name,
46
- flexDirection: 'row',
47
- backgroundColor: i === idx ? hex.selectionBg : undefined,
48
- width: boxWidth,
45
+ key: cmd.name, height: 1,
46
+ backgroundColor: i === idx ? hex.selectionBg : 'transparent',
49
47
  },
50
48
  h(Text, {
51
49
  color: i === idx ? hex.selectionText : hex.text,
52
50
  bold: i === idx,
53
- backgroundColor: i === idx ? hex.selectionBg : undefined,
54
- wrap: 'truncate-end',
55
- }, ' ' + cmd.name.padEnd(16)),
51
+ backgroundColor: i === idx ? hex.selectionBg : 'transparent',
52
+ }, ' ' + cmd.name + ' '),
56
53
  h(Text, {
57
54
  color: i === idx ? hex.selectionText : hex.textDim,
58
- backgroundColor: i === idx ? hex.selectionBg : undefined,
59
- wrap: 'truncate-end',
55
+ backgroundColor: i === idx ? hex.selectionBg : 'transparent',
60
56
  }, cmd.desc)
61
57
  )
62
58
  ),
63
- h(Text, { color: hex.textMuted }, ' ' + usym.arrowU + usym.arrowD + ' nav Enter select Esc close')
59
+ h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
60
+ h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt }, ' ' + sym.arrowU + sym.arrowD + ' nav ' + sym.arrowR + ' select Esc close')
61
+ )
64
62
  );
65
63
  }
@@ -1,46 +1,38 @@
1
1
  import React, { useState, useRef } from 'react';
2
2
  import { Box, Text, useInput } from 'ink';
3
- import { hex, usym, u } from '../config/theme.js';
3
+ import { hex, sym } from '../config/theme.js';
4
4
  import { getLayout } from '../config/layout.js';
5
5
  const { createElement: h } = React;
6
6
 
7
- const MAX_VISIBLE_ROWS = 3;
7
+ const MAX_ROWS = 3;
8
8
 
9
9
  export function Composer({ provider, model, agentMode, thinking, onSlash, onSubmit }) {
10
10
  const [input, setInput] = useState('');
11
11
  const [cursor, setCursor] = useState(0);
12
- const inputRef = useRef('');
13
- inputRef.current = input;
12
+ const r = useRef('');
13
+ r.current = input;
14
14
 
15
15
  const { cols } = getLayout();
16
16
  const w = Math.max(10, cols - 6);
17
- const lineCount = Math.max(1, Math.ceil((input.slice(0, cursor).length + 1) / w));
18
- const visibleLines = Math.min(lineCount, MAX_VISIBLE_ROWS);
19
-
20
- const modelShort = model.replace(/^[^/]+\//, '').slice(0, 18);
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
21
 
22
22
  useInput((ch, key) => {
23
23
  if (key.ctrl && key.p) { onSlash(); return; }
24
24
  if (key.escape) { onSubmit('/exit'); return; }
25
25
  if (key.return && !key.shift) {
26
- if (input.trim()) {
27
- const text = input;
28
- setInput('');
29
- setCursor(0);
30
- onSubmit(text);
31
- }
26
+ if (input.trim()) { const t = input; setInput(''); setCursor(0); onSubmit(t); }
32
27
  return;
33
28
  }
34
29
  if (key.return && key.shift) {
35
- setInput(prev => prev.slice(0, cursor) + '\n' + prev.slice(cursor));
30
+ setInput(p => p.slice(0, cursor) + '\n' + p.slice(cursor));
36
31
  setCursor(c => c + 1);
37
32
  return;
38
33
  }
39
34
  if (key.backspace || key.delete) {
40
- if (cursor > 0) {
41
- setInput(prev => prev.slice(0, cursor - 1) + prev.slice(cursor));
42
- setCursor(c => c - 1);
43
- }
35
+ if (cursor > 0) { setInput(p => p.slice(0, cursor - 1) + p.slice(cursor)); setCursor(c => c - 1); }
44
36
  return;
45
37
  }
46
38
  if (key.leftArrow && cursor > 0) { setCursor(c => c - 1); return; }
@@ -48,43 +40,35 @@ export function Composer({ provider, model, agentMode, thinking, onSlash, onSubm
48
40
  if (key.home) { setCursor(0); return; }
49
41
  if (key.end) { setCursor(input.length); return; }
50
42
  if (ch && ch.length === 1 && ch.charCodeAt(0) >= 32) {
51
- setInput(prev => prev.slice(0, cursor) + ch + prev.slice(cursor));
43
+ setInput(p => p.slice(0, cursor) + ch + p.slice(cursor));
52
44
  setCursor(c => c + 1);
53
45
  }
54
46
  });
55
47
 
56
- const displayText = input || (thinking ? '' : 'Type a message...');
57
- const isPlaceholder = !input && !thinking;
58
-
59
- const rows = [];
60
- rows.push(
61
- h(Box, { key: 'dock_header', height: 1, backgroundColor: hex.surfaceAlt },
62
- h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt }, ' ' + u.h.repeat(Math.max(0, cols - 2)))
63
- )
64
- );
48
+ const rows = [
49
+ h(Box, { key: 'sep', height: 1, backgroundColor: hex.surfaceAlt },
50
+ h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt }, ' ' + sym.lightH.repeat(Math.max(0, cols - 4)))
51
+ ),
52
+ ];
65
53
 
66
- for (let i = 0; i < MAX_VISIBLE_ROWS; i++) {
54
+ for (let i = 0; i < MAX_ROWS; i++) {
67
55
  const start = i * w;
68
- const end = start + w;
69
- const seg = displayText.slice(start, end);
56
+ const seg = input.slice(start, start + w);
70
57
  rows.push(
71
- h(Box, { key: 'in' + i, height: 1, backgroundColor: hex.bg },
58
+ h(Box, { key: 'r' + i, height: 1, backgroundColor: hex.bg },
72
59
  h(Text, {
73
60
  color: isPlaceholder ? hex.textMuted : hex.text,
74
61
  backgroundColor: hex.bg,
75
62
  wrap: 'truncate-end',
76
- }, ' ' + usym.triR2 + ' ' + (seg || ' '))
63
+ }, ' ' + sym.triR + ' ' + (seg || (i === 0 && isPlaceholder ? 'type a message...' : ' ')))
77
64
  )
78
65
  );
79
66
  }
80
67
 
81
68
  rows.push(
82
- h(Box, { key: 'dock_status', height: 1, backgroundColor: hex.surfaceAlt },
69
+ h(Box, { key: 'st', height: 1, backgroundColor: hex.surfaceAlt },
83
70
  h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
84
- ' ' + provider + ' ' + usym.midDot + ' ' + modelShort +
85
- (agentMode ? ' ' + usym.midDot + ' Agent' : '') +
86
- ' ' + usym.midDot + ' Ctrl+P commands'
87
- )
71
+ ' ' + provider + ' ' + sym.midDot + ' ' + mShort + (agentMode ? ' ' + sym.midDot + ' AGENT' : '') + ' ' + sym.midDot + ' Ctrl+P')
88
72
  )
89
73
  );
90
74
 
@@ -1,14 +1,11 @@
1
1
  import React from 'react';
2
2
  import { Box, Text } from 'ink';
3
- import Spinner from 'ink-spinner';
4
- import { hex, usym } from '../config/theme.js';
3
+ import { hex, sym } from '../config/theme.js';
5
4
  const { createElement: h } = React;
6
5
 
7
6
  export function LoadingIndicator({ label }) {
8
- return h(Box, { flexDirection: 'row', marginLeft: 0, marginY: 1, backgroundColor: hex.surface },
9
- h(Text, { color: hex.blue, backgroundColor: hex.surface }, usym.triR2),
10
- h(Text, { color: hex.blue, backgroundColor: hex.surface }, ' '),
11
- h(Spinner, { type: 'dots' }),
12
- h(Text, { color: hex.textDim, backgroundColor: hex.surface }, ' ' + (label || 'Thinking'))
7
+ return h(Box, { flexDirection: 'row', 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'))
13
10
  );
14
11
  }
@@ -1,16 +1,14 @@
1
1
  import React, { useMemo } from 'react';
2
2
  import { Box, Text } from 'ink';
3
- import { hex, usym, u } from '../config/theme.js';
3
+ import { hex, sym } from '../config/theme.js';
4
4
  import { getLayout, sliceToViewport, buildLineArray } from '../config/layout.js';
5
5
  const { createElement: h } = React;
6
6
 
7
- function LineRenderer({ type, text, data }) {
8
- const { cols } = getLayout();
7
+ function Line({ type, text, data }) {
9
8
  switch (type) {
10
9
  case 'user_head':
11
10
  return h(Box, { height: 1, backgroundColor: hex.userBg },
12
- h(Text, { color: hex.accent, bold: true, backgroundColor: hex.userBg }, ' ' + usym.circle + ' YOU'),
13
- h(Text, { color: hex.textMuted, backgroundColor: hex.userBg }, ' ' + u.h.repeat(Math.max(0, cols - 10)))
11
+ h(Text, { color: hex.accent, bold: true, backgroundColor: hex.userBg }, ' ' + sym.bullet + ' YOU')
14
12
  );
15
13
  case 'user_line':
16
14
  return h(Box, { height: 1, backgroundColor: hex.userBg },
@@ -18,12 +16,7 @@ function LineRenderer({ type, text, data }) {
18
16
  );
19
17
  case 'asst_head':
20
18
  return h(Box, { height: 1, backgroundColor: hex.surface },
21
- h(Text, { color: hex.purple, bold: true, backgroundColor: hex.surface }, ' ' + usym.circle + ' CLARITY'),
22
- h(Text, { color: hex.textMuted, backgroundColor: hex.surface }, ' ' + u.h.repeat(Math.max(0, cols - 14)))
23
- );
24
- case 'asst_bar':
25
- return h(Box, { height: 1, backgroundColor: hex.surface },
26
- h(Text, { color: hex.textMuted, backgroundColor: hex.surface }, ' ' + usym.lightV)
19
+ h(Text, { color: hex.purple, bold: true, backgroundColor: hex.surface }, ' ' + sym.diamond + ' CLARITY')
27
20
  );
28
21
  case 'asst_line':
29
22
  return h(Box, { height: 1, backgroundColor: hex.surface },
@@ -31,36 +24,27 @@ function LineRenderer({ type, text, data }) {
31
24
  );
32
25
  case 'asst_foot':
33
26
  return h(Box, { height: 1, backgroundColor: hex.surface },
34
- h(Text, { color: hex.textDim, backgroundColor: hex.surface }, ' ' + usym.triR2 + ' ' + (parseInt(text) < 1000 ? text + 'ms' : (parseInt(text) / 1000).toFixed(1) + 's'))
35
- );
36
- case 'tool_head':
37
- return h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
38
- h(Text, { color: hex.purple, backgroundColor: hex.surfaceAlt }, ' ' + usym.circle + ' ' + text)
27
+ h(Text, { color: hex.textDim, backgroundColor: hex.surface }, ' ' + sym.triR + ' ' + (parseInt(text) < 1000 ? text + 'ms' : (parseInt(text) / 1000).toFixed(1) + 's'))
39
28
  );
40
29
  case 'tool_line':
41
30
  return h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
42
- h(Text, { color: hex.textDim, backgroundColor: hex.surfaceAlt }, ' ' + (text || ' '))
31
+ h(Text, { color: hex.purple, backgroundColor: hex.surfaceAlt }, ' ' + sym.bullet + ' ' + (text || ''))
43
32
  );
44
33
  case 'sys_line':
45
34
  return h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
46
- h(Text, { color: hex.green, backgroundColor: hex.surfaceAlt }, ' ' + usym.circle + ' ' + (text || ''))
35
+ h(Text, { color: hex.green, backgroundColor: hex.surfaceAlt }, ' ' + sym.bullet + ' ' + (text || ''))
47
36
  );
48
37
  case 'err_line':
49
38
  return h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
50
- h(Text, { color: hex.red, backgroundColor: hex.surfaceAlt }, ' ' + usym.cross + ' ' + (text || ''))
39
+ h(Text, { color: hex.red, backgroundColor: hex.surfaceAlt }, ' ' + sym.cross + ' ' + (text || ''))
51
40
  );
52
41
  case 'stream_head':
53
42
  return h(Box, { height: 1, backgroundColor: hex.surface },
54
- h(Text, { color: hex.purple, bold: true, backgroundColor: hex.surface }, ' ' + usym.circle + ' CLARITY'),
55
- h(Text, { color: hex.textMuted, backgroundColor: hex.surface }, ' ' + u.h.repeat(Math.max(0, cols - 14)))
56
- );
57
- case 'stream_bar':
58
- return h(Box, { height: 1, backgroundColor: hex.surface },
59
- h(Text, { color: hex.textMuted, backgroundColor: hex.surface }, ' ' + usym.lightV)
43
+ h(Text, { color: hex.purple, bold: true, backgroundColor: hex.surface }, ' ' + sym.diamond + ' CLARITY')
60
44
  );
61
45
  case 'stream_status':
62
46
  return h(Box, { height: 1, backgroundColor: hex.surface },
63
- h(Text, { color: hex.blue, backgroundColor: hex.surface }, ' ' + usym.dot + ' ' + (text || ''))
47
+ h(Text, { color: hex.blue, backgroundColor: hex.surface }, ' ' + sym.dot + ' ' + (text || ''))
64
48
  );
65
49
  case 'stream_line':
66
50
  return h(Box, { height: 1, backgroundColor: hex.surface },
@@ -74,32 +58,25 @@ function LineRenderer({ type, text, data }) {
74
58
  export function MessageList({ messages, thinking, streamContent, agentStatus, toolExecutions }) {
75
59
  const { viewport, contentWidth } = getLayout();
76
60
 
77
- const entryForMsg = (msg) => ({
78
- id: msg.id,
79
- role: msg.role,
80
- content: msg.content,
81
- toolResults: msg.toolResults,
82
- duration: msg.duration,
83
- toolName: msg.toolName,
84
- error: msg.error,
85
- completed: true,
86
- });
87
-
88
- const streamEntry = {
89
- id: 'stream',
90
- role: 'streaming',
91
- content: streamContent || '',
92
- status: agentStatus || (thinking ? 'thinking...' : ''),
93
- completed: false,
94
- };
95
-
96
- const rawEntries = messages.map(entryForMsg);
97
- const showStream = thinking || streamContent;
98
- if (showStream) rawEntries.push(streamEntry);
61
+ const entries = useMemo(() => {
62
+ const e = 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
+ if (thinking || streamContent) {
67
+ e.push({
68
+ id: 'stream', role: 'streaming',
69
+ content: streamContent || '',
70
+ status: agentStatus || (thinking ? 'processing...' : ''),
71
+ completed: false,
72
+ });
73
+ }
74
+ return e;
75
+ }, [messages, thinking, streamContent, agentStatus]);
99
76
 
100
77
  const { slice, clipIndex, clipLines } = useMemo(
101
- () => sliceToViewport(rawEntries, viewport, contentWidth),
102
- [rawEntries, viewport, contentWidth]
78
+ () => sliceToViewport(entries, viewport, contentWidth),
79
+ [entries, viewport, contentWidth]
103
80
  );
104
81
 
105
82
  const lines = useMemo(
@@ -107,17 +84,17 @@ export function MessageList({ messages, thinking, streamContent, agentStatus, to
107
84
  [slice, clipIndex, clipLines, contentWidth]
108
85
  );
109
86
 
110
- const padCount = Math.max(0, viewport - lines.length);
111
- const paddedLines = lines;
112
- for (let i = 0; i < padCount; i++) paddedLines.unshift({ type: 'empty' });
87
+ const padded = [...lines];
88
+ for (let i = padded.length; i < viewport; i++) {
89
+ padded.unshift({ type: 'empty' });
90
+ }
113
91
 
114
92
  return h(Box, { height: viewport, flexDirection: 'column', overflow: 'hidden' },
115
- paddedLines.map((ln, i) =>
93
+ padded.map((ln, i) =>
116
94
  ln.type === 'empty'
117
- ? h(Box, { key: 'e' + i, height: 1, backgroundColor: hex.bg },
118
- h(Text, { color: hex.textMuted, backgroundColor: hex.bg }, ' '))
95
+ ? h(Box, { key: 'e' + i, height: 1, backgroundColor: hex.bg })
119
96
  : h(Box, { key: (ln.data?.id || 'l') + '-' + i, height: 1 },
120
- h(LineRenderer, { type: ln.type, text: ln.text, data: ln.data })
97
+ h(Line, { type: ln.type, text: ln.text, data: ln.data })
121
98
  )
122
99
  )
123
100
  );
@@ -1,12 +1,14 @@
1
1
  import React, { useState, useMemo } from 'react';
2
2
  import { Box, Text, useInput } from 'ink';
3
3
  import { ALL_MODELS } from '../config/models.js';
4
- import { hex, usym } from '../config/theme.js';
4
+ import { hex, sym } from '../config/theme.js';
5
+ import { getLayout } from '../config/layout.js';
5
6
  const { createElement: h } = React;
6
7
 
7
8
  export function ModelPicker({ onSelect, onClose }) {
8
9
  const [search, setSearch] = useState('');
9
10
  const [idx, setIdx] = useState(0);
11
+ const { cols } = getLayout();
10
12
 
11
13
  const flat = useMemo(() => {
12
14
  const q = search.toLowerCase();
@@ -35,44 +37,32 @@ export function ModelPicker({ onSelect, onClose }) {
35
37
  else if (input && !key.ctrl && !key.meta) setSearch(s => s + input);
36
38
  });
37
39
 
38
- const tw = process.stdout.columns || 80;
39
- const boxWidth = Math.min(tw - 4, 54);
40
+ const w = Math.min(cols - 4, 52);
40
41
 
41
- return h(Box, { flexDirection: 'column', width: boxWidth },
42
- h(Box, { flexDirection: 'row', marginBottom: 1, gap: 1 },
43
- h(Text, { color: hex.textMuted }, usym.bulb),
44
- h(Text, { color: search ? hex.text : hex.textMuted }, search || 'search models...'),
42
+ return h(Box, { flexDirection: 'column', backgroundColor: hex.surfaceAlt, width: w },
43
+ h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
44
+ h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt }, ' ' + sym.star + ' ' + (search || 'search models...'))
45
45
  ),
46
46
  flat.map((m, i) => {
47
47
  if (m._header) {
48
- return h(Text, {
49
- key: 'h-' + m._provider,
50
- color: hex.blue,
51
- bold: true,
52
- backgroundColor: hex.surfaceAlt,
53
- wrap: 'truncate-end',
54
- }, ' ' + usym.triD + ' ' + m._provider.toUpperCase());
48
+ return h(Box, { key: 'h-' + m._provider, height: 1, backgroundColor: hex.surfaceAlt },
49
+ h(Text, { color: hex.blue, bold: true, backgroundColor: hex.surfaceAlt }, ' ' + sym.triD + ' ' + m._provider.toUpperCase())
50
+ );
55
51
  }
56
52
  const isSel = i === idx;
57
53
  return h(Box, {
58
- key: m.id,
59
- flexDirection: 'row',
60
- backgroundColor: isSel ? hex.selectionBg : undefined,
61
- width: boxWidth,
54
+ key: m.id, height: 1,
55
+ backgroundColor: isSel ? hex.selectionBg : 'transparent',
62
56
  },
63
57
  h(Text, {
64
58
  color: isSel ? hex.selectionText : hex.text,
65
59
  bold: isSel,
66
- backgroundColor: isSel ? hex.selectionBg : undefined,
67
- wrap: 'truncate-end',
68
- }, ' ' + m.label),
69
- m.badge
70
- ? h(Text, {
71
- color: isSel ? hex.selectionText : hex.textMuted,
72
- backgroundColor: isSel ? hex.selectionBg : undefined,
73
- }, ' [' + m.badge + ']')
74
- : null
60
+ backgroundColor: isSel ? hex.selectionBg : 'transparent',
61
+ }, ' ' + m.label + (m.badge ? ' [' + m.badge + ']' : ''))
75
62
  );
76
- })
63
+ }),
64
+ h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
65
+ h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt }, ' ' + sym.arrowU + sym.arrowD + ' nav ' + sym.arrowR + ' select Esc close')
66
+ )
77
67
  );
78
68
  }
@@ -1,17 +1,16 @@
1
1
  import React from 'react';
2
2
  import { Box, Text } from 'ink';
3
+ import { hex, sym } from '../config/theme.js';
3
4
  import { getLayout } from '../config/layout.js';
4
- import { hex, usym } from '../config/theme.js';
5
5
  const { createElement: h } = React;
6
6
 
7
7
  export function StatusBar({ model, provider, agentMode, thinking }) {
8
8
  const { cols } = getLayout();
9
- const modelShort = model.replace(/^[^/]+\//, '').slice(0, 20);
10
- const left = '\u25C9 CLARITY ' + usym.midDot + ' ' + modelShort + ' ' + usym.midDot + ' ' + provider;
11
- const right = (agentMode ? 'Agent:ON' : 'Agent:OFF') + (thinking ? ' \u25CF' : '');
12
- const gap = Math.max(1, cols - left.length - right.length - 2);
13
- const line = left + ' '.repeat(gap) + right;
9
+ const m = model.replace(/^[^/]+\//, '').slice(0, 22);
10
+ const left = sym.circle + ' CLARITY ' + sym.midDot + ' ' + m + ' ' + sym.midDot + ' ' + provider;
11
+ const right = (agentMode ? 'AGENT' : 'USER') + (thinking ? ' ' + sym.dot : '');
12
+ const gap = Math.max(1, cols - left.length - right.length - 4);
14
13
  return h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
15
- h(Text, { color: hex.textDim, backgroundColor: hex.surfaceAlt }, ' ' + line)
14
+ h(Text, { color: hex.textDim, backgroundColor: hex.surfaceAlt }, ' ' + left + ' '.repeat(gap) + right + ' ')
16
15
  );
17
16
  }
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
2
  import { Box, Text } from 'ink';
3
- import { hex, usym } from '../config/theme.js';
3
+ import { hex, sym } from '../config/theme.js';
4
4
  import { getLayout } from '../config/layout.js';
5
5
  const { createElement: h } = React;
6
6
 
@@ -8,37 +8,25 @@ export function ToolCard({ exec, isActive }) {
8
8
  const { cols } = getLayout();
9
9
  const name = exec.name || 'tool';
10
10
  const status = exec.status || 'running';
11
- const duration = exec.duration ? (exec.duration < 1000 ? exec.duration + 'ms' : (exec.duration / 1000).toFixed(1) + 's') : '';
12
- const args = exec.args || {};
13
- const argsStr = typeof args === 'string' ? args : JSON.stringify(args);
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
14
 
15
- const minRender = 1;
16
- const maxRender = 5;
17
- const canRender = !isActive && (status === 'completed' || status === 'failed');
18
-
19
- if (canRender) {
20
- const col = status === 'failed' ? hex.red : hex.green;
21
- const icon = status === 'failed' ? usym.cross : usym.circle;
22
- const label = icon + ' Tool: ' + name + ' ' + (status === 'failed' ? 'Failed' : 'Successful') + (duration ? ' (' + duration + ')' : '');
23
- return h(Box, { height: minRender },
24
- h(Text, { color: col, backgroundColor: hex.surfaceAlt }, ' ' + label)
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 : ''))
25
20
  );
26
21
  }
27
22
 
28
- const w = Math.min(cols - 6, 60);
29
- const errLine = status === 'failed' && exec.error ? ' ' + usym.cross + ' ' + String(exec.error).slice(0, w - 4) : '';
30
- const resultLine = status === 'completed' && exec.result && String(exec.result).length < 150
31
- ? ' ' + String(exec.result).slice(0, w - 4) : '';
32
-
33
- const hasExtra = errLine || resultLine;
34
- const lines = [usym.circle + ' ' + name + (duration ? ' ' + duration : '')];
35
- if (argsStr.length < 80) lines.push(' args: ' + argsStr.slice(0, w));
36
- if (errLine) lines.push(errLine);
37
- if (resultLine) lines.push(resultLine);
38
- if (status === 'running') lines.push(' running...');
23
+ const w = Math.min(cols - 6, 56);
24
+ const lines = [sym.bullet + ' ' + name + (dur ? ' ' + dur : '')];
25
+ if (args.length < 80) lines.push(' ' + sym.triR + ' ' + args.slice(0, w));
26
+ if (status === 'running') lines.push(' ' + sym.dot + ' running');
27
+ if (status === 'failed' && exec.error) lines.push(' ' + sym.cross + ' ' + String(exec.error).slice(0, w - 4));
39
28
 
40
- const totalLines = Math.min(lines.length, 4);
41
- return h(Box, { height: totalLines, flexDirection: 'column', backgroundColor: hex.surfaceAlt },
29
+ return h(Box, { flexDirection: 'column', backgroundColor: hex.surfaceAlt },
42
30
  lines.slice(0, 4).map((line, i) =>
43
31
  h(Box, { key: i, height: 1 },
44
32
  h(Text, { color: i === 0 ? hex.purple : hex.textDim, backgroundColor: hex.surfaceAlt }, ' ' + line)
@@ -1,26 +1,27 @@
1
1
  import chalk from 'chalk';
2
2
 
3
3
  export const hex = {
4
- bg: '#0D0D1A',
5
- surface: '#13132B',
6
- surfaceAlt: '#1A1A3E',
7
- userBg: '#1A0A2E',
8
- codeBg: '#0D1117',
9
- selectionBg: '#FF6B6B',
4
+ bg: '#0A0A14',
5
+ surface: '#111125',
6
+ surfaceAlt: '#161630',
7
+ userBg: '#140A28',
8
+ codeBg: '#0D0D18',
9
+ selectionBg: '#FF6B35',
10
10
  selectionText: '#FFFFFF',
11
- borderLight: '#2A2A5E',
12
- accent: '#FF6B6B',
11
+ borderLight: '#202050',
12
+ accent: '#FF6B35',
13
13
  purple: '#A855F7',
14
14
  green: '#22C55E',
15
15
  red: '#EF4444',
16
16
  gold: '#F59E0B',
17
17
  blue: '#3B82F6',
18
18
  cyan: '#22D3EE',
19
- text: '#E2E8F0',
20
- textDim: '#94A3B8',
21
- textMuted: '#64748B',
19
+ text: '#EAEAEE',
20
+ textDim: '#8888AA',
21
+ textMuted: '#555577',
22
22
  white: '#FFFFFF',
23
23
  black: '#000000',
24
+ modalOverlay: 'rgba(0,0,0,0.85)',
24
25
  };
25
26
 
26
27
  export const color = {
@@ -30,7 +31,6 @@ export const color = {
30
31
  userBg: chalk.hex(hex.userBg),
31
32
  selectionBg: chalk.hex(hex.selectionBg),
32
33
  selectionText: chalk.hex(hex.selectionText),
33
- border: chalk.hex(hex.borderLight),
34
34
  accent: chalk.hex(hex.accent),
35
35
  purple: chalk.hex(hex.purple),
36
36
  green: chalk.hex(hex.green),
@@ -45,60 +45,16 @@ export const color = {
45
45
  black: chalk.hex(hex.black),
46
46
  };
47
47
 
48
- export const dim = {
49
- surface: chalk.hex('#0A0A1A'),
50
- surfaceAlt: chalk.hex('#12122E'),
51
- userBg: chalk.hex('#12081E'),
52
- border: chalk.hex('#1E1E4E'),
53
- purple: chalk.hex('#7C3AED'),
54
- accent: chalk.hex('#E05555'),
55
- green: chalk.hex('#16A34A'),
56
- gold: chalk.hex('#D97706'),
57
- };
58
-
59
- export const u = {
60
- tl: '\u250C',
61
- tr: '\u2510',
62
- bl: '\u2514',
63
- br: '\u2518',
64
- h: '\u2500',
65
- v: '\u2502',
66
- lc: '\u251C',
67
- rc: '\u2524',
68
- tc: '\u252C',
69
- bc: '\u2534',
70
- cr: '\u253C',
71
- };
72
-
73
- export const u2 = {
74
- tl: '\u2554',
75
- tr: '\u2557',
76
- bl: '\u255A',
77
- br: '\u255D',
78
- h: '\u2550',
79
- v: '\u2551',
80
- };
81
-
82
- export const usym = {
83
- topR: '\u256D',
84
- topL: '\u256E',
85
- botR: '\u2570',
86
- botL: '\u256F',
87
- triR: '\u25B6',
88
- triD: '\u25BC',
89
- triR2: '\u25B8',
90
- triD2: '\u25BE',
91
- dot: '\u25CF',
92
- circle: '\u25C9',
93
- smallCircle: '\u25CB',
48
+ export const sym = {
94
49
  diamond: '\u25C6',
95
- asterisk: '\u2731',
96
- bulb: '\u25C3',
97
- leaf: '\u2726',
50
+ circle: '\u25C9',
51
+ dot: '\u25CF',
52
+ smallDot: '\u25CB',
53
+ triR: '\u25B8',
54
+ triD: '\u25BE',
55
+ bullet: '\u25C9',
98
56
  cross: '\u2716',
99
57
  ellipsis: '\u2026',
100
- quoteD: '\u201C',
101
- quoteS: '\u2018',
102
58
  mdash: '\u2014',
103
59
  ndash: '\u2013',
104
60
  midDot: '\u00B7',
@@ -108,21 +64,11 @@ export const usym = {
108
64
  arrowD: '\u2193',
109
65
  lightV: '\u2502',
110
66
  lightH: '\u2500',
111
- teeL: '\u251C',
112
- teeR: '\u2524',
113
67
  treeJ: '\u2514',
114
68
  treeT: '\u251C',
115
69
  treeCon: '\u2502',
116
70
  treeTip: '\u2570',
117
71
  treeFork: '\u256D',
72
+ star: '\u2726',
73
+ asterisk: '\u2731',
118
74
  };
119
-
120
- export function boxTop(w) {
121
- return u.lc + u.h.repeat(w - 2) + u.rc;
122
- }
123
- export function boxBottom(w) {
124
- return u.lc + u.h.repeat(w - 2) + u.rc;
125
- }
126
- export function boxTitle(title, w) {
127
- return u.tl + ' ' + title + ' ' + u.h.repeat(Math.max(0, w - title.length - 4)) + u.tr;
128
- }