claudeck 1.2.0 → 1.3.1

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 (42) hide show
  1. package/README.md +64 -5
  2. package/cli.js +53 -4
  3. package/package.json +3 -2
  4. package/public/css/core/responsive.css +2 -2
  5. package/public/css/ui/file-picker.css +243 -17
  6. package/public/css/ui/messages.css +72 -9
  7. package/public/css/ui/toolbox.css +43 -0
  8. package/public/index.html +80 -745
  9. package/public/js/components/add-project-modal.js +27 -0
  10. package/public/js/components/agent-modal.js +73 -0
  11. package/public/js/components/agent-monitor-modal.js +19 -0
  12. package/public/js/components/bg-confirm-modal.js +22 -0
  13. package/public/js/components/chain-modal.js +52 -0
  14. package/public/js/components/cost-dashboard-modal.js +39 -0
  15. package/public/js/components/dag-editor-modal.js +55 -0
  16. package/public/js/components/file-picker-modal.js +45 -0
  17. package/public/js/components/linear-create-modal.js +43 -0
  18. package/public/js/components/mcp-modal.js +58 -0
  19. package/public/js/components/orchestrate-modal.js +40 -0
  20. package/public/js/components/permission-modal.js +30 -0
  21. package/public/js/components/prompt-modal.js +31 -0
  22. package/public/js/components/shortcuts-modal.js +45 -0
  23. package/public/js/components/status-bar.js +97 -0
  24. package/public/js/components/system-prompt-modal.js +29 -0
  25. package/public/js/components/telegram-modal.js +84 -0
  26. package/public/js/components/welcome-overlay.js +60 -0
  27. package/public/js/components/workflow-modal.js +41 -0
  28. package/public/js/core/api.js +10 -0
  29. package/public/js/core/dom.js +3 -2
  30. package/public/js/core/ws.js +7 -1
  31. package/public/js/features/attachments.js +226 -23
  32. package/public/js/features/projects.js +7 -0
  33. package/public/js/main.js +22 -0
  34. package/public/js/ui/shortcuts.js +4 -8
  35. package/public/login.html +470 -0
  36. package/public/offline.html +300 -168
  37. package/public/sw.js +10 -2
  38. package/server/agent-loop.js +1 -0
  39. package/server/auth.js +141 -0
  40. package/server/orchestrator.js +1 -0
  41. package/server/ws-handler.js +2 -0
  42. package/server.js +14 -3
package/README.md CHANGED
@@ -8,6 +8,10 @@
8
8
  A browser-based UI for <a href="https://docs.anthropic.com/en/docs/claude-code">Claude Code</a> — chat, workflows, agents, cost tracking, and more.
9
9
  </p>
10
10
 
11
+ <p align="center">
12
+ <a href="https://www.producthunt.com/products/claudeck?embed=true&utm_source=badge-featured&utm_medium=badge&utm_campaign=badge-claudeck" target="_blank" rel="noopener noreferrer"><img alt="Claudeck on Product Hunt" width="250" height="54" src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=1105387&theme=light&t=1774340505179" /></a>
13
+ </p>
14
+
11
15
  <p align="center">
12
16
  <a href="https://www.npmjs.com/package/claudeck"><img src="https://img.shields.io/npm/v/claudeck?color=cb3837&label=npm" alt="npm version" /></a>
13
17
  <a href="https://github.com/hamedafarag/claudeck/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/claudeck?color=blue" alt="license" /></a>
@@ -27,6 +31,9 @@ npx claudeck
27
31
  # Custom port
28
32
  npx claudeck --port 3000
29
33
 
34
+ # Enable authentication (for remote access via Cloudflare Tunnel, etc.)
35
+ npx claudeck --auth
36
+
30
37
  # Or install globally
31
38
  npm install -g claudeck
32
39
  claudeck
@@ -42,10 +49,11 @@ User data lives in `~/.claudeck/` (config, database, plugins) — safe for NPX u
42
49
 
43
50
  ## Why Claudeck?
44
51
 
45
- - **Zero-framework** — Vanilla JS, 6 npm dependencies, no build step
52
+ - **Zero-framework** — Vanilla JS + Web Components, 6 npm dependencies, no build step
46
53
  - **Full agent orchestration** — Chains, DAGs, orchestrator, and monitoring dashboard
47
54
  - **Persistent memory** — Cross-session project knowledge with FTS5 search and AI optimization
48
55
  - **Cost visibility** — Per-session tracking, daily charts, token breakdowns
56
+ - **Secure remote access** — Token-based auth for Cloudflare Tunnel or reverse proxy setups
49
57
  - **Works everywhere** — PWA, mobile responsive, Telegram AFK approval
50
58
  - **Extensible** — Full-stack plugin system with auto-discovery
51
59
 
@@ -80,6 +88,7 @@ User data lives in `~/.claudeck/` (config, database, plugins) — safe for NPX u
80
88
  ### Code & Files
81
89
 
82
90
  - **File Explorer** — Lazy tree, syntax-highlighted preview, drag-to-chat
91
+ - **File Picker** — Attach files with type dots, binary detection, search, selected chips
83
92
  - **Git Panel** — Branch switching, staging, commit, log, inline diff viewer
84
93
  - **Git Worktrees** — Run any chat/agent task in an isolated worktree; merge, diff, or discard results
85
94
  - **Repos Manager** — Organize repos in nested groups with GitHub links
@@ -141,8 +150,9 @@ User data lives in `~/.claudeck/` (config, database, plugins) — safe for NPX u
141
150
  | **Confirm All** | Prompt for every tool call |
142
151
  | **Plan Mode** | No execution, planning only |
143
152
 
144
- ### UI & Experience
153
+ ### Security & UI
145
154
 
155
+ - **Authentication** — `--auth` flag enables token-based auth with login page, HttpOnly cookies, and WebSocket verification. Localhost bypasses auth by default (auto-detected proxy headers like `X-Forwarded-For` disable the bypass for tunneled requests).
146
156
  - Dark theme (terminal CRT aesthetic) and light theme
147
157
  - Installable as a PWA with offline fallback
148
158
  - Mobile responsive with tablet/mobile breakpoints
@@ -162,7 +172,8 @@ browser ──── WebSocket ──── server.js ──── Claude Code S
162
172
  server/dag-executor.js ├── data.db (SQLite + memories)
163
173
  server/notification-logger.js
164
174
  server/utils/git-worktree.js
165
- server/memory-optimizer.js └── .env (VAPID keys)
175
+ server/auth.js
176
+ server/memory-optimizer.js └── .env (VAPID keys, auth token)
166
177
  plugins/
167
178
  ```
168
179
 
@@ -172,7 +183,8 @@ browser ──── WebSocket ──── server.js ──── Claude Code S
172
183
  | Backend | Express 4, WebSocket (ws 8), web-push 3 |
173
184
  | AI SDK | @anthropic-ai/claude-code |
174
185
  | Database | SQLite via better-sqlite3 (WAL mode) |
175
- | Frontend | Vanilla JS ES modules, CSS custom properties |
186
+ | Frontend | Vanilla JS ES modules + Web Components (Light DOM), CSS custom properties |
187
+ | Testing | Vitest + happy-dom (2,400+ unit tests, 55% coverage) + WS perf benchmarks |
176
188
  | Rendering | highlight.js, Mermaid (diagrams) — CDN |
177
189
 
178
190
  ---
@@ -226,7 +238,7 @@ All user data lives in `~/.claudeck/` (override with `CLAUDECK_HOME`):
226
238
  │ └── skillsmp-config.json Skills Marketplace config
227
239
  ├── plugins/ User-installed plugins
228
240
  ├── data.db SQLite database
229
- └── .env VAPID keys, port config
241
+ └── .env VAPID keys, port, auth token
230
242
  ```
231
243
 
232
244
  Defaults are copied on first run. User edits are never overwritten on upgrade.
@@ -272,6 +284,52 @@ npx skills add https://github.com/hamedafarag/claudeck-skills
272
284
 
273
285
  ---
274
286
 
287
+ ## Testing
288
+
289
+ ```bash
290
+ npm test # Run all 2,400+ tests
291
+ npm test -- --coverage # With coverage report
292
+ npm run test:perf # WebSocket performance benchmarks
293
+ ```
294
+
295
+ | Layer | Tests | Coverage |
296
+ |-------|-------|----------|
297
+ | **components/** (Web Components) | 170+ | 100% |
298
+ | **core/** | 110+ | 90% |
299
+ | **ui/** | 280+ | 65% |
300
+ | **features/** | 210+ | 22% |
301
+ | **panels/** | 150+ | 35% |
302
+ | **server/** | 1,350+ | 95% |
303
+
304
+ 19 Web Components in `public/js/components/` — each is a self-contained Custom Element (Light DOM) that owns its HTML, testable with zero mocks.
305
+
306
+ ### Performance Benchmarks
307
+
308
+ The `test:perf` suite measures WebSocket relay performance with real TCP connections over localhost (no mocked sockets). Results from 4 scenarios:
309
+
310
+ **Approval Round-Trip Latency** — server sends `permission_request` → client responds → server receives:
311
+
312
+ | Concurrent Sessions | p50 | p95 | p99 |
313
+ |---|---|---|---|
314
+ | 1 | 70 µs | 132 µs | 196 µs |
315
+ | 5 | 187 µs | 222 µs | 244 µs |
316
+ | 10 | 300 µs | 466 µs | 721 µs |
317
+ | 25 | 382 µs | 570 µs | 764 µs |
318
+
319
+ **Message Throughput** — streaming text chunks to connected clients:
320
+
321
+ | Clients | Total msg/s |
322
+ |---|---|
323
+ | 1 | ~295k |
324
+ | 10 | ~393k |
325
+ | 50 | ~435k |
326
+
327
+ **Connection Scaling** — 100 simultaneous connections: p50 establish time 156 µs, ~35 KB memory per connection.
328
+
329
+ **Broadcast Fan-Out** — notification delivery to all connected clients: p50 under 1 ms even at 100 clients.
330
+
331
+ ---
332
+
275
333
  ## Contributing
276
334
 
277
335
  Contributions are welcome! Fork the repo, make your changes, and open a PR.
@@ -281,6 +339,7 @@ git clone https://github.com/hamedafarag/claudeck.git
281
339
  cd claudeck
282
340
  npm install
283
341
  npm start
342
+ npm test # Run tests before submitting
284
343
  ```
285
344
 
286
345
  See [DOCUMENTATION.md](docs/DOCUMENTATION.md) for architecture details and [CONFIGURATION.md](docs/CONFIGURATION.md) for the config system.
package/cli.js CHANGED
@@ -3,6 +3,7 @@ import { homedir } from "os";
3
3
  import { join } from "path";
4
4
  import { readFileSync, writeFileSync, mkdirSync } from "fs";
5
5
  import { createInterface } from "readline";
6
+ import crypto from "crypto";
6
7
 
7
8
  const DEFAULT_PORT = 9009;
8
9
  const envDir = process.env.CLAUDECK_HOME || join(homedir(), ".claudeck");
@@ -13,16 +14,21 @@ function readEnv() {
13
14
  try { return readFileSync(envPath, "utf-8"); } catch { return ""; }
14
15
  }
15
16
 
16
- function savePort(port) {
17
+ function saveEnvVar(key, value) {
17
18
  let content = readEnv();
18
- if (/^PORT=.*/m.test(content)) {
19
- content = content.replace(/^PORT=.*/m, `PORT=${port}`);
19
+ const re = new RegExp(`^${key}=.*`, "m");
20
+ if (re.test(content)) {
21
+ content = content.replace(re, `${key}=${value}`);
20
22
  } else {
21
- content = content.trimEnd() + `\nPORT=${port}\n`;
23
+ content = content.trimEnd() + `\n${key}=${value}\n`;
22
24
  }
23
25
  writeFileSync(envPath, content);
24
26
  }
25
27
 
28
+ function savePort(port) {
29
+ saveEnvVar("PORT", port);
30
+ }
31
+
26
32
  function getSavedPort() {
27
33
  const match = readEnv().match(/^PORT=(\d+)/m);
28
34
  return match ? match[1] : null;
@@ -35,7 +41,50 @@ function ask(question) {
35
41
  });
36
42
  }
37
43
 
44
+ function handleAuthFlags() {
45
+ // --no-auth: explicitly disable for this run
46
+ if (process.argv.includes("--no-auth")) {
47
+ process.env.CLAUDECK_AUTH = "false";
48
+ return;
49
+ }
50
+
51
+ // --token <value> or --token=<value>: set custom token + enable auth
52
+ const tokenArg = process.argv.find(a => a.startsWith("--token"));
53
+ if (tokenArg) {
54
+ const token = tokenArg.includes("=")
55
+ ? tokenArg.split("=")[1]
56
+ : process.argv[process.argv.indexOf(tokenArg) + 1];
57
+ if (token) {
58
+ process.env.CLAUDECK_TOKEN = token;
59
+ process.env.CLAUDECK_AUTH = "true";
60
+ saveEnvVar("CLAUDECK_TOKEN", token);
61
+ saveEnvVar("CLAUDECK_AUTH", "true");
62
+ console.log(`\x1b[2m Auth token set and saved to ~/.claudeck/.env\x1b[0m`);
63
+ }
64
+ return;
65
+ }
66
+
67
+ // --auth: enable auth, auto-generate token if missing
68
+ if (process.argv.includes("--auth")) {
69
+ process.env.CLAUDECK_AUTH = "true";
70
+ const envContent = readEnv();
71
+ const existingToken = envContent.match(/^CLAUDECK_TOKEN=(.+)/m);
72
+ if (existingToken) {
73
+ process.env.CLAUDECK_TOKEN = existingToken[1];
74
+ } else {
75
+ const token = crypto.randomBytes(32).toString("hex");
76
+ process.env.CLAUDECK_TOKEN = token;
77
+ saveEnvVar("CLAUDECK_TOKEN", token);
78
+ saveEnvVar("CLAUDECK_AUTH", "true");
79
+ console.log(`\x1b[2m Generated auth token and saved to ~/.claudeck/.env\x1b[0m`);
80
+ }
81
+ }
82
+ }
83
+
38
84
  async function main() {
85
+ // Handle auth flags before anything else
86
+ handleAuthFlags();
87
+
39
88
  // --port flag takes priority
40
89
  const portArg = process.argv.find(a => a.startsWith('--port'));
41
90
  if (portArg) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudeck",
3
- "version": "1.2.0",
3
+ "version": "1.3.1",
4
4
  "type": "module",
5
5
  "description": "A browser-based UI for Claude Code — chat, run workflows, manage MCP servers, track costs, and orchestrate autonomous agents from a local web interface. Installable as a PWA.",
6
6
  "main": "server.js",
@@ -45,7 +45,8 @@
45
45
  "start": "node server.js",
46
46
  "test": "vitest run",
47
47
  "test:watch": "vitest",
48
- "test:coverage": "vitest run --coverage"
48
+ "test:coverage": "vitest run --coverage",
49
+ "test:perf": "vitest run --config vitest.config.perf.js"
49
50
  },
50
51
  "dependencies": {
51
52
  "@anthropic-ai/claude-code": "^1.0.128",
@@ -150,8 +150,8 @@ body.sidebar-open #sidebar-backdrop {
150
150
  padding: 6px 8px;
151
151
  }
152
152
 
153
- /* Hide toolbox/agent/workflow buttons on mobile */
154
- .input-bar > .toolbox-toggle {
153
+ /* Hide toolbox strip on mobile */
154
+ .toolbox-strip {
155
155
  display: none;
156
156
  }
157
157
 
@@ -1,46 +1,266 @@
1
1
  /* ── File Picker ──────────────────────────────────────── */
2
2
  .file-picker-modal {
3
- width: 560px;
4
- max-height: 70vh;
3
+ width: 580px;
4
+ max-height: 75vh;
5
5
  display: flex;
6
6
  flex-direction: column;
7
7
  }
8
8
 
9
- .file-picker-search {
10
- width: 100%;
9
+ /* Constraint info banner */
10
+ .fp-info-banner {
11
+ display: flex;
12
+ align-items: center;
13
+ gap: 8px;
14
+ padding: 8px 12px;
15
+ margin-bottom: 12px;
16
+ background: var(--accent-dim);
17
+ border: 1px solid var(--accent-mid);
18
+ border-radius: var(--radius-md);
19
+ font-size: 11px;
20
+ color: var(--text-secondary);
21
+ font-family: var(--font-sans);
22
+ line-height: 1.4;
23
+ }
24
+
25
+ .fp-info-banner svg {
26
+ flex-shrink: 0;
27
+ color: var(--accent);
28
+ }
29
+
30
+ .fp-info-banner strong {
31
+ color: var(--text);
32
+ font-weight: 600;
33
+ }
34
+
35
+ /* Search wrapper */
36
+ .fp-search-wrap {
37
+ position: relative;
11
38
  margin-bottom: 10px;
12
39
  }
13
40
 
41
+ .fp-search-icon {
42
+ position: absolute;
43
+ left: 10px;
44
+ top: 50%;
45
+ transform: translateY(-50%);
46
+ color: var(--text-dim);
47
+ pointer-events: none;
48
+ }
49
+
50
+ .fp-search-wrap .file-picker-search {
51
+ width: 100%;
52
+ padding-left: 32px;
53
+ }
54
+
55
+ /* Selected files chip strip */
56
+ .fp-selected-strip {
57
+ display: flex;
58
+ flex-wrap: wrap;
59
+ gap: 6px;
60
+ padding-bottom: 10px;
61
+ margin-bottom: 8px;
62
+ border-bottom: 1px solid var(--border-subtle);
63
+ }
64
+
65
+ .fp-selected-strip.hidden { display: none; }
66
+
67
+ .fp-chip {
68
+ display: inline-flex;
69
+ align-items: center;
70
+ gap: 4px;
71
+ padding: 3px 4px 3px 8px;
72
+ background: var(--accent-dim);
73
+ border: 1px solid var(--accent-mid);
74
+ border-radius: 4px;
75
+ font-size: 11px;
76
+ font-family: var(--font-mono);
77
+ color: var(--accent);
78
+ max-width: 220px;
79
+ animation: fpChipIn 0.15s var(--ease-out-expo);
80
+ }
81
+
82
+ @keyframes fpChipIn {
83
+ from { opacity: 0; transform: scale(0.9); }
84
+ to { opacity: 1; transform: scale(1); }
85
+ }
86
+
87
+ .fp-chip-name {
88
+ overflow: hidden;
89
+ text-overflow: ellipsis;
90
+ white-space: nowrap;
91
+ direction: rtl;
92
+ text-align: left;
93
+ }
94
+
95
+ .fp-chip-remove {
96
+ background: none;
97
+ border: none;
98
+ color: var(--accent);
99
+ font-size: 15px;
100
+ cursor: pointer;
101
+ padding: 0 2px;
102
+ line-height: 1;
103
+ opacity: 0.5;
104
+ transition: opacity 0.15s;
105
+ flex-shrink: 0;
106
+ }
107
+
108
+ .fp-chip-remove:hover { opacity: 1; }
109
+
110
+ /* File list */
14
111
  .file-picker-list {
15
112
  flex: 1;
16
113
  overflow-y: auto;
17
- max-height: 350px;
114
+ max-height: 320px;
18
115
  border: 1px solid var(--border-subtle);
19
116
  border-radius: var(--radius-md);
20
117
  background: var(--bg);
21
118
  }
22
119
 
120
+ /* File items */
23
121
  .file-picker-item {
122
+ display: flex;
123
+ align-items: center;
124
+ gap: 8px;
24
125
  padding: 7px 12px;
25
126
  font-size: 12px;
26
127
  font-family: var(--font-mono);
27
128
  color: var(--text-secondary);
28
129
  cursor: pointer;
29
130
  border-bottom: 1px solid var(--border-subtle);
30
- transition: background 0.08s, color 0.08s;
31
- white-space: nowrap;
32
- overflow: hidden;
33
- text-overflow: ellipsis;
131
+ transition: background 0.1s, color 0.1s;
132
+ position: relative;
34
133
  }
35
134
 
36
135
  .file-picker-item:last-child { border-bottom: none; }
37
- .file-picker-item:hover { background: var(--bg-tertiary); color: var(--text); }
136
+
137
+ .file-picker-item:hover {
138
+ background: var(--bg-tertiary);
139
+ color: var(--text);
140
+ }
38
141
 
39
142
  .file-picker-item.selected {
40
143
  background: var(--accent-dim);
41
144
  color: var(--accent);
42
145
  }
43
146
 
147
+ /* File type color dot */
148
+ .fp-type-dot {
149
+ width: 6px;
150
+ height: 6px;
151
+ border-radius: 50%;
152
+ flex-shrink: 0;
153
+ }
154
+
155
+ .fp-type-dot.type-code { background: var(--accent); }
156
+ .fp-type-dot.type-config { background: var(--amber); }
157
+ .fp-type-dot.type-markup { background: var(--cyan); }
158
+ .fp-type-dot.type-docs { background: var(--purple); }
159
+ .fp-type-dot.type-data { background: var(--user); }
160
+ .fp-type-dot.type-binary { background: var(--error); }
161
+ .fp-type-dot.type-default { background: var(--text-dim); }
162
+
163
+ /* File path text */
164
+ .fp-path {
165
+ flex: 1;
166
+ overflow: hidden;
167
+ text-overflow: ellipsis;
168
+ white-space: nowrap;
169
+ }
170
+
171
+ /* Checkmark for selected items */
172
+ .fp-check {
173
+ opacity: 0;
174
+ color: var(--accent);
175
+ flex-shrink: 0;
176
+ transition: opacity 0.1s;
177
+ margin-left: auto;
178
+ font-size: 13px;
179
+ font-weight: 700;
180
+ }
181
+
182
+ .file-picker-item.selected .fp-check {
183
+ opacity: 1;
184
+ }
185
+
186
+ /* Loading state */
187
+ .file-picker-item.loading {
188
+ pointer-events: none;
189
+ opacity: 0.6;
190
+ }
191
+
192
+ .fp-spinner {
193
+ width: 14px;
194
+ height: 14px;
195
+ border: 2px solid var(--border);
196
+ border-top-color: var(--accent);
197
+ border-radius: 50%;
198
+ animation: fpSpin 0.6s linear infinite;
199
+ flex-shrink: 0;
200
+ margin-left: auto;
201
+ }
202
+
203
+ @keyframes fpSpin {
204
+ to { transform: rotate(360deg); }
205
+ }
206
+
207
+ /* Error state */
208
+ .file-picker-item.error {
209
+ background: rgba(237, 51, 59, 0.05);
210
+ cursor: not-allowed;
211
+ }
212
+
213
+ .file-picker-item.error .fp-path {
214
+ text-decoration: line-through;
215
+ opacity: 0.5;
216
+ }
217
+
218
+ .fp-error-msg {
219
+ font-size: 10px;
220
+ color: var(--error);
221
+ margin-left: auto;
222
+ flex-shrink: 0;
223
+ font-family: var(--font-sans);
224
+ white-space: nowrap;
225
+ font-weight: 500;
226
+ }
227
+
228
+ /* Binary file warning */
229
+ .file-picker-item.binary-warn {
230
+ opacity: 0.45;
231
+ cursor: not-allowed;
232
+ }
233
+
234
+ .fp-binary-label {
235
+ font-size: 10px;
236
+ color: var(--text-dim);
237
+ margin-left: auto;
238
+ flex-shrink: 0;
239
+ font-family: var(--font-sans);
240
+ font-style: italic;
241
+ }
242
+
243
+ /* Empty state */
244
+ .fp-empty-state {
245
+ display: flex;
246
+ flex-direction: column;
247
+ align-items: center;
248
+ justify-content: center;
249
+ padding: 40px 16px;
250
+ color: var(--text-dim);
251
+ font-size: 13px;
252
+ font-family: var(--font-sans);
253
+ gap: 10px;
254
+ min-height: 120px;
255
+ }
256
+
257
+ .fp-empty-state.hidden { display: none; }
258
+
259
+ .fp-empty-state svg {
260
+ opacity: 0.3;
261
+ }
262
+
263
+ /* Footer */
44
264
  .file-picker-footer {
45
265
  display: flex;
46
266
  align-items: center;
@@ -50,20 +270,26 @@
50
270
  color: var(--text-dim);
51
271
  }
52
272
 
273
+ /* Badge */
274
+ #attach-btn {
275
+ position: relative;
276
+ }
277
+
53
278
  .attach-badge {
54
279
  position: absolute;
55
- top: -4px;
56
- right: -4px;
57
- min-width: 16px;
58
- height: 16px;
280
+ top: -3px;
281
+ right: -3px;
282
+ min-width: 14px;
283
+ height: 14px;
59
284
  background: var(--accent-solid);
60
285
  color: #fff;
61
- font-size: 10px;
286
+ font-size: 9px;
62
287
  font-weight: 700;
63
- border-radius: 8px;
288
+ border-radius: 7px;
64
289
  display: flex;
65
290
  align-items: center;
66
291
  justify-content: center;
67
- padding: 0 4px;
292
+ padding: 0 3px;
68
293
  line-height: 1;
294
+ pointer-events: none;
69
295
  }
@@ -770,12 +770,17 @@ html[data-theme="light"] .msg-user {
770
770
  border-top: 1px solid var(--border);
771
771
  display: flex;
772
772
  gap: 8px;
773
- align-items: flex-start;
773
+ align-items: flex-end;
774
774
  max-width: calc(var(--chat-max-w) + 48px);
775
775
  width: 100%;
776
776
  margin: 0 auto;
777
777
  }
778
778
 
779
+ .input-bar > .send-history-group {
780
+ align-self: flex-end;
781
+ margin-bottom: 24px;
782
+ }
783
+
779
784
  /* Gradient fade above input */
780
785
  .input-bar::before {
781
786
  content: "";
@@ -837,21 +842,79 @@ html[data-theme="light"] .msg-user {
837
842
  transition: all 0.2s var(--ease-smooth);
838
843
  }
839
844
 
840
- #send-btn { background: var(--accent-solid); color: #000; }
841
- #send-btn:hover { background: var(--accent); box-shadow: var(--glow-strong); transform: scale(1.02); }
842
- #send-btn:active { transform: scale(0.97); }
843
- #send-btn:disabled { opacity: 0.25; cursor: not-allowed; transform: none; }
844
- #stop-btn { background: var(--error); color: #fff; }
845
- #stop-btn:hover { opacity: 0.85; transform: scale(1.02); }
846
- #stop-btn:active { transform: scale(0.97); }
845
+ /* Send button */
846
+ #send-btn {
847
+ width: 42px;
848
+ height: 42px;
849
+ border-radius: 50%;
850
+ background: var(--accent-solid);
851
+ color: #000;
852
+ position: relative;
853
+ box-shadow: 0 0 12px rgba(46, 194, 126, 0.2);
854
+ }
855
+
856
+ #send-btn::after {
857
+ content: "";
858
+ position: absolute;
859
+ inset: -2px;
860
+ border-radius: 50%;
861
+ border: 1.5px solid var(--accent);
862
+ opacity: 0;
863
+ transition: opacity 0.2s, transform 0.2s;
864
+ transform: scale(0.95);
865
+ }
866
+
867
+ #send-btn:hover {
868
+ background: var(--accent);
869
+ box-shadow: 0 0 20px rgba(46, 194, 126, 0.35), var(--glow-strong);
870
+ transform: scale(1.05);
871
+ }
872
+
873
+ #send-btn:hover::after {
874
+ opacity: 0.4;
875
+ transform: scale(1);
876
+ }
877
+
878
+ #send-btn:active {
879
+ transform: scale(0.93);
880
+ box-shadow: 0 0 8px rgba(46, 194, 126, 0.15);
881
+ }
882
+
883
+ #send-btn:disabled {
884
+ opacity: 0.2;
885
+ cursor: not-allowed;
886
+ transform: none;
887
+ box-shadow: none;
888
+ }
889
+
890
+ #send-btn:disabled::after { display: none; }
891
+
892
+ /* Stop button */
893
+ #stop-btn {
894
+ width: 42px;
895
+ height: 42px;
896
+ border-radius: 50%;
897
+ background: var(--error);
898
+ color: #fff;
899
+ box-shadow: 0 0 12px rgba(237, 51, 59, 0.2);
900
+ }
901
+
902
+ #stop-btn:hover {
903
+ box-shadow: 0 0 20px rgba(237, 51, 59, 0.35);
904
+ transform: scale(1.05);
905
+ }
906
+
907
+ #stop-btn:active {
908
+ transform: scale(0.93);
909
+ }
847
910
 
848
911
  /* ── Input Meta Labels ────────────────────────────────── */
849
912
  .input-meta {
850
913
  display: flex;
851
914
  align-items: center;
852
915
  gap: 6px;
853
- padding: 4px 2px 0;
854
916
  user-select: none;
917
+ margin-left: auto;
855
918
  }
856
919
 
857
920
  .input-meta-item {