claudeck 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +46 -7
  2. package/config/skillsmp-config.json +5 -0
  3. package/db.js +248 -0
  4. package/package.json +11 -2
  5. package/public/css/panels/git-panel.css +220 -0
  6. package/public/css/panels/skills-manager.css +975 -0
  7. package/public/css/ui/input-history.css +109 -0
  8. package/public/css/ui/messages.css +51 -0
  9. package/public/css/ui/notification-bell.css +421 -0
  10. package/public/css/ui/sessions.css +41 -0
  11. package/public/css/ui/status-bar.css +42 -12
  12. package/public/css/ui/worktree.css +442 -0
  13. package/public/index.html +47 -10
  14. package/public/js/core/api.js +83 -0
  15. package/public/js/core/dom.js +15 -0
  16. package/public/js/features/background-sessions.js +11 -0
  17. package/public/js/features/chat.js +501 -3
  18. package/public/js/features/input-history.js +122 -0
  19. package/public/js/features/projects.js +16 -1
  20. package/public/js/features/sessions.js +77 -30
  21. package/public/js/main.js +3 -0
  22. package/public/js/panels/git-panel.js +385 -6
  23. package/public/js/panels/skills-manager.js +1005 -0
  24. package/public/js/ui/messages.js +58 -0
  25. package/public/js/ui/notification-bell.js +240 -0
  26. package/public/js/ui/notification-history.js +210 -0
  27. package/public/js/ui/parallel.js +11 -0
  28. package/public/js/ui/tab-sdk.js +1 -1
  29. package/public/style.css +4 -0
  30. package/server/agent-loop.js +13 -0
  31. package/server/notification-logger.js +27 -0
  32. package/server/routes/notifications.js +57 -1
  33. package/server/routes/sessions.js +41 -0
  34. package/server/routes/skills.js +454 -0
  35. package/server/routes/worktrees.js +93 -0
  36. package/server/utils/git-worktree.js +297 -0
  37. package/server/ws-handler.js +708 -629
  38. package/server.js +17 -1
package/README.md CHANGED
@@ -44,6 +44,7 @@ User data lives in `~/.claudeck/` (config, database, plugins) — safe for NPX u
44
44
 
45
45
  - **Zero-framework** — Vanilla JS, 6 npm dependencies, no build step
46
46
  - **Full agent orchestration** — Chains, DAGs, orchestrator, and monitoring dashboard
47
+ - **Persistent memory** — Cross-session project knowledge with FTS5 search and AI optimization
47
48
  - **Cost visibility** — Per-session tracking, daily charts, token breakdowns
48
49
  - **Works everywhere** — PWA, mobile responsive, Telegram AFK approval
49
50
  - **Extensible** — Full-stack plugin system with auto-discovery
@@ -58,6 +59,8 @@ User data lives in `~/.claudeck/` (config, database, plugins) — safe for NPX u
58
59
  - **Parallel mode** — 2x2 grid of 4 independent conversations
59
60
  - Background sessions that keep running when you switch away
60
61
  - Session search, pinning, auto-generated titles
62
+ - **Session branching** — fork any conversation at an assistant message to explore alternatives
63
+ - **Message recall** — press `↑` on empty input to cycle through previous messages, or click the history button to browse and re-use
61
64
  - Voice input via Web Speech API (Chrome/Safari)
62
65
 
63
66
  ### Autonomous Agents
@@ -77,7 +80,8 @@ User data lives in `~/.claudeck/` (config, database, plugins) — safe for NPX u
77
80
  ### Code & Files
78
81
 
79
82
  - **File Explorer** — Lazy tree, syntax-highlighted preview, drag-to-chat
80
- - **Git Panel** — Branch switching, staging, commit, log
83
+ - **Git Panel** — Branch switching, staging, commit, log, inline diff viewer
84
+ - **Git Worktrees** — Run any chat/agent task in an isolated worktree; merge, diff, or discard results
81
85
  - **Repos Manager** — Organize repos in nested groups with GitHub links
82
86
  - Code diff viewer with LCS-based line highlighting
83
87
 
@@ -87,6 +91,34 @@ User data lives in `~/.claudeck/` (config, database, plugins) — safe for NPX u
87
91
  - Input/output token breakdown, streaming token counter
88
92
  - Error pattern analysis (9 categories), tool usage stats
89
93
 
94
+ ### Persistent Memory
95
+
96
+ - Cross-session project knowledge that survives restarts
97
+ - Auto-capture from assistant responses using pattern-based heuristic extraction
98
+ - `/remember` command for manual memory creation
99
+ - FTS5 full-text search with relevance scoring and time-decay
100
+ - AI-powered optimization (consolidation via Claude Haiku)
101
+ - Memory panel in right sidebar with search, filtering, and inline editing
102
+
103
+ ### Notifications
104
+
105
+ - **Notification Bell** — Persistent notification history with unread badge in the header
106
+ - Background session events (completed, errored, input needed) logged automatically
107
+ - Agent completion/error notifications with cost and duration metrics
108
+ - Full history modal with type/status filters, bulk actions, and pagination
109
+ - 4 read strategies: explicit click, mark all, auto-read on view, click-through to session
110
+ - Real-time cross-tab sync via WebSocket broadcasts
111
+
112
+ ### Skills Marketplace
113
+
114
+ - **SkillsMP Integration** — Browse and install agent skills from the [SkillsMP](https://skillsmp.com/) registry
115
+ - Keyword search and AI semantic search with mode toggle
116
+ - Install skills globally (`~/.claude/skills/`) or per-project (`.claude/skills/`)
117
+ - Enable/disable skills via toggle (renames `SKILL.md` ↔ `SKILL.md.disabled`)
118
+ - Installed skills auto-register as `/` slash commands
119
+ - "Skill used" system messages in chat for both user-invoked and model-invoked skills
120
+ - Token-gated — enter your free SkillsMP API key to activate
121
+
90
122
  ### Integrations
91
123
 
92
124
  - **MCP Manager** — Add/edit/remove MCP servers (global + per-project)
@@ -127,8 +159,11 @@ browser ──── WebSocket ──── server.js ──── Claude Code S
127
159
  server/routes/ ~/.claudeck/
128
160
  server/agent-loop.js ├── config/ (JSON configs)
129
161
  server/orchestrator.js ├── plugins/ (user plugins)
130
- server/dag-executor.js ├── data.db (SQLite)
131
- plugins/ └── .env (VAPID keys)
162
+ server/dag-executor.js ├── data.db (SQLite + memories)
163
+ server/notification-logger.js
164
+ server/utils/git-worktree.js
165
+ server/memory-optimizer.js └── .env (VAPID keys)
166
+ plugins/
132
167
  ```
133
168
 
134
169
  | Layer | Technology |
@@ -147,7 +182,8 @@ browser ──── WebSocket ──── server.js ──── Claude Code S
147
182
  ```
148
183
  /clear /new /parallel /export /theme /shortcuts App
149
184
  /costs /analytics Dashboards
150
- /files /git /repos /events /mcp /tips Panels
185
+ /files /git /repos /events /mcp /tips /skills Panels
186
+ /remember Memory
151
187
  /review-pr /onboard-repo /migration-plan /code-health Workflows
152
188
  /agent-pr-reviewer /agent-bug-hunter /agent-test-writer Agents
153
189
  /orchestrate /monitor /chain-* /dag-* Multi-Agent
@@ -165,8 +201,9 @@ browser ──── WebSocket ──── server.js ──── Claude Code S
165
201
  | `Cmd+N` | New session |
166
202
  | `Cmd+B` | Toggle right panel |
167
203
  | `Cmd+/` | Show all shortcuts |
168
- | `Cmd+Shift+E/G/R/V/T` | Files / Git / Repos / Events / Tips |
204
+ | `Cmd+Shift+E/G/R/V/T` | Files / Git / Repos / Events / Tips |
169
205
  | `Cmd+1`-`4` | Focus parallel pane |
206
+ | `↑` / `↓` | Recall previous/next message (empty input) |
170
207
 
171
208
  ---
172
209
 
@@ -185,7 +222,8 @@ All user data lives in `~/.claudeck/` (override with `CLAUDECK_HOME`):
185
222
  │ ├── agent-dags.json Dependency graphs
186
223
  │ ├── repos.json Repository groups
187
224
  │ ├── bot-prompt.json Assistant bot prompt
188
- └── telegram-config.json Telegram config
225
+ ├── telegram-config.json Telegram config
226
+ │ └── skillsmp-config.json Skills Marketplace config
189
227
  ├── plugins/ User-installed plugins
190
228
  ├── data.db SQLite database
191
229
  └── .env VAPID keys, port config
@@ -199,7 +237,7 @@ See [CONFIGURATION.md](docs/CONFIGURATION.md) for the full guide.
199
237
 
200
238
  ## Plugins
201
239
 
202
- Claudeck includes 6 built-in plugins and supports user plugins via `~/.claudeck/plugins/`:
240
+ Claudeck includes 7 built-in plugins and supports user plugins via `~/.claudeck/plugins/`:
203
241
 
204
242
  | Plugin | Description |
205
243
  |--------|-------------|
@@ -228,6 +266,7 @@ npx skills add https://github.com/hamedafarag/claudeck-skills
228
266
  |----------|-------------|
229
267
  | [DOCUMENTATION.md](docs/DOCUMENTATION.md) | Full feature docs, API reference, database schema |
230
268
  | [CONFIGURATION.md](docs/CONFIGURATION.md) | User data directory, config files, plugin system |
269
+ | [AGENT-ARCHITECTURE.md](docs/AGENT-ARCHITECTURE.md) | How agents, chains, DAGs, and orchestrator work |
231
270
  | [CROSS-PLATFORM-AUDIT.md](docs/CROSS-PLATFORM-AUDIT.md) | Windows/Linux compatibility |
232
271
  | [COMPETITIVE-ANALYSIS.md](docs/COMPETITIVE-ANALYSIS.md) | Feature comparison with similar tools |
233
272
 
@@ -0,0 +1,5 @@
1
+ {
2
+ "apiKey": "",
3
+ "defaultScope": "project",
4
+ "searchMode": "keyword"
5
+ }
package/db.js CHANGED
@@ -81,6 +81,9 @@ try { db.exec(`ALTER TABLE sessions ADD COLUMN summary TEXT DEFAULT NULL`); } ca
81
81
  try { db.exec(`ALTER TABLE todos ADD COLUMN archived INTEGER DEFAULT 0`); } catch { /* exists */ }
82
82
  // Todo priority (0=none, 1=low, 2=medium, 3=high)
83
83
  try { db.exec(`ALTER TABLE todos ADD COLUMN priority INTEGER DEFAULT 0`); } catch { /* exists */ }
84
+ // Session branching / conversation forking
85
+ try { db.exec(`ALTER TABLE sessions ADD COLUMN parent_session_id TEXT DEFAULT NULL`); } catch { /* exists */ }
86
+ try { db.exec(`ALTER TABLE sessions ADD COLUMN fork_message_id INTEGER DEFAULT NULL`); } catch { /* exists */ }
84
87
 
85
88
  // Agent context (shared memory between agents in a chain/orchestration run)
86
89
  db.exec(`
@@ -167,6 +170,41 @@ db.exec(`
167
170
  END;
168
171
  `);
169
172
 
173
+ // ── Notifications table ──────────────────────────────────
174
+ db.exec(`
175
+ CREATE TABLE IF NOT EXISTS notifications (
176
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
177
+ type TEXT NOT NULL,
178
+ title TEXT NOT NULL,
179
+ body TEXT,
180
+ metadata TEXT,
181
+ source_session_id TEXT,
182
+ source_agent_id TEXT,
183
+ read_at INTEGER DEFAULT NULL,
184
+ created_at INTEGER DEFAULT (unixepoch())
185
+ );
186
+ CREATE INDEX IF NOT EXISTS idx_notif_created ON notifications(created_at DESC);
187
+ CREATE INDEX IF NOT EXISTS idx_notif_unread ON notifications(read_at) WHERE read_at IS NULL;
188
+ `);
189
+
190
+ // ── Worktrees table ──────────────────────────────────────
191
+ db.exec(`
192
+ CREATE TABLE IF NOT EXISTS worktrees (
193
+ id TEXT PRIMARY KEY,
194
+ session_id TEXT,
195
+ project_path TEXT NOT NULL,
196
+ worktree_path TEXT NOT NULL,
197
+ branch_name TEXT NOT NULL,
198
+ base_branch TEXT NOT NULL,
199
+ status TEXT DEFAULT 'active',
200
+ user_prompt TEXT,
201
+ created_at INTEGER DEFAULT (unixepoch()),
202
+ completed_at INTEGER DEFAULT NULL
203
+ );
204
+ CREATE INDEX IF NOT EXISTS idx_wt_project ON worktrees(project_path);
205
+ CREATE INDEX IF NOT EXISTS idx_wt_status ON worktrees(status);
206
+ `);
207
+
170
208
  // Backfill content_hash for existing rows
171
209
  const unhashed = db.prepare(`SELECT id, project_path, content FROM memories WHERE content_hash IS NULL`).all();
172
210
  if (unhashed.length > 0) {
@@ -208,6 +246,7 @@ db.exec(`
208
246
  CREATE INDEX IF NOT EXISTS idx_costs_created_at ON costs(created_at);
209
247
  CREATE INDEX IF NOT EXISTS idx_sessions_project_path ON sessions(project_path);
210
248
  CREATE INDEX IF NOT EXISTS idx_sessions_pinned_last_used ON sessions(pinned DESC, last_used_at DESC);
249
+ CREATE INDEX IF NOT EXISTS idx_sessions_parent ON sessions(parent_session_id) WHERE parent_session_id IS NOT NULL;
211
250
  `);
212
251
 
213
252
  // Deduplicated mode CASE subquery — used in 4 session listing queries
@@ -289,6 +328,22 @@ const stmts = {
289
328
  `SELECT s.*, ${MODE_CASE}
290
329
  FROM sessions s WHERE (s.title LIKE ? OR s.project_name LIKE ?) ORDER BY s.pinned DESC, s.last_used_at DESC LIMIT ?`
291
330
  ),
331
+ // Session branching
332
+ getMessagesByIdRange: db.prepare(
333
+ `SELECT role, content, created_at FROM messages WHERE session_id = ? AND id <= ? AND chat_id IS NULL ORDER BY id ASC`
334
+ ),
335
+ getLastMessageId: db.prepare(
336
+ `SELECT MAX(id) as maxId FROM messages WHERE session_id = ? AND chat_id IS NULL`
337
+ ),
338
+ getBranches: db.prepare(
339
+ `SELECT s.*, ${MODE_CASE} FROM sessions s WHERE s.parent_session_id = ? ORDER BY s.created_at DESC`
340
+ ),
341
+ getBranchCount: db.prepare(
342
+ `SELECT COUNT(*) as count FROM sessions WHERE parent_session_id = ?`
343
+ ),
344
+ orphanChildren: db.prepare(
345
+ `UPDATE sessions SET parent_session_id = NULL WHERE parent_session_id = ?`
346
+ ),
292
347
  getSessionCosts: db.prepare(
293
348
  `SELECT s.id, s.title, s.project_name, s.last_used_at,
294
349
  COALESCE(SUM(c.cost_usd), 0) AS total_cost,
@@ -460,12 +515,74 @@ export function searchSessions(query, limit = 20, projectPath) {
460
515
  }
461
516
 
462
517
  export const deleteSession = db.transaction((id) => {
518
+ // Orphan child forks before deleting parent
519
+ stmts.orphanChildren.run(id);
463
520
  db.prepare("DELETE FROM claude_sessions WHERE session_id = ?").run(id);
464
521
  db.prepare("DELETE FROM costs WHERE session_id = ?").run(id);
465
522
  db.prepare("DELETE FROM messages WHERE session_id = ?").run(id);
466
523
  db.prepare("DELETE FROM sessions WHERE id = ?").run(id);
467
524
  });
468
525
 
526
+ // ── Session Branching / Forking ─────────────────────────
527
+ export const forkSession = db.transaction((parentSessionId, forkMessageId) => {
528
+ const parent = stmts.getSession.get(parentSessionId);
529
+ if (!parent) throw new Error("Session not found");
530
+
531
+ if (!forkMessageId) {
532
+ const last = stmts.getLastMessageId.get(parentSessionId);
533
+ forkMessageId = last?.maxId;
534
+ if (!forkMessageId) throw new Error("No messages to fork");
535
+ }
536
+
537
+ const newId = createHash("sha256")
538
+ .update(parentSessionId + Date.now() + Math.random())
539
+ .digest("hex")
540
+ .slice(0, 36);
541
+ const title = `Fork of: ${parent.title || parent.project_name || "Untitled"}`;
542
+
543
+ db.prepare(
544
+ `INSERT INTO sessions (id, project_name, project_path, title, parent_session_id, fork_message_id)
545
+ VALUES (?, ?, ?, ?, ?, ?)`
546
+ ).run(newId, parent.project_name, parent.project_path, title, parentSessionId, forkMessageId);
547
+
548
+ const messages = stmts.getMessagesByIdRange.all(parentSessionId, forkMessageId);
549
+ const insertMsg = db.prepare(
550
+ "INSERT INTO messages (session_id, role, content, created_at) VALUES (?, ?, ?, ?)"
551
+ );
552
+ for (const msg of messages) {
553
+ insertMsg.run(newId, msg.role, msg.content, msg.created_at);
554
+ }
555
+
556
+ return stmts.getSession.get(newId);
557
+ });
558
+
559
+ export function getSessionBranches(sessionId) {
560
+ return stmts.getBranches.all(sessionId);
561
+ }
562
+
563
+ export function getSessionBranchCount(sessionId) {
564
+ return stmts.getBranchCount.get(sessionId).count;
565
+ }
566
+
567
+ export function getSessionLineage(sessionId) {
568
+ const ancestors = [];
569
+ let current = stmts.getSession.get(sessionId);
570
+ while (current && current.parent_session_id) {
571
+ const parent = stmts.getSession.get(current.parent_session_id);
572
+ if (!parent) break;
573
+ ancestors.unshift(parent);
574
+ current = parent;
575
+ }
576
+ // Get siblings (other forks of the same parent)
577
+ const session = stmts.getSession.get(sessionId);
578
+ let siblings = [];
579
+ if (session?.parent_session_id) {
580
+ siblings = stmts.getBranches.all(session.parent_session_id)
581
+ .filter(s => s.id !== sessionId);
582
+ }
583
+ return { ancestors, siblings };
584
+ }
585
+
469
586
  export function getSessionCosts(projectPath) {
470
587
  if (projectPath) {
471
588
  return stmts.getSessionCosts.all(projectPath);
@@ -1263,6 +1380,137 @@ export function getAgentRunsDaily() {
1263
1380
  return runStmts.dailyRuns.all();
1264
1381
  }
1265
1382
 
1383
+ // ── Notifications ────────────────────────────────────────
1384
+ const notifStmts = {
1385
+ insert: db.prepare(
1386
+ `INSERT INTO notifications (type, title, body, metadata, source_session_id, source_agent_id)
1387
+ VALUES (?, ?, ?, ?, ?, ?)`
1388
+ ),
1389
+ history: db.prepare(
1390
+ `SELECT * FROM notifications ORDER BY created_at DESC LIMIT ? OFFSET ?`
1391
+ ),
1392
+ historyUnread: db.prepare(
1393
+ `SELECT * FROM notifications WHERE read_at IS NULL ORDER BY created_at DESC LIMIT ? OFFSET ?`
1394
+ ),
1395
+ historyByType: db.prepare(
1396
+ `SELECT * FROM notifications WHERE type = ? ORDER BY created_at DESC LIMIT ? OFFSET ?`
1397
+ ),
1398
+ historyByTypeUnread: db.prepare(
1399
+ `SELECT * FROM notifications WHERE type = ? AND read_at IS NULL ORDER BY created_at DESC LIMIT ? OFFSET ?`
1400
+ ),
1401
+ unreadCount: db.prepare(
1402
+ `SELECT COUNT(*) as count FROM notifications WHERE read_at IS NULL`
1403
+ ),
1404
+ markRead: db.prepare(
1405
+ `UPDATE notifications SET read_at = unixepoch() WHERE id = ? AND read_at IS NULL`
1406
+ ),
1407
+ markAllRead: db.prepare(
1408
+ `UPDATE notifications SET read_at = unixepoch() WHERE read_at IS NULL`
1409
+ ),
1410
+ markReadBefore: db.prepare(
1411
+ `UPDATE notifications SET read_at = unixepoch() WHERE read_at IS NULL AND created_at < ?`
1412
+ ),
1413
+ purgeOld: db.prepare(
1414
+ `DELETE FROM notifications WHERE created_at < unixepoch() - (? * 86400)`
1415
+ ),
1416
+ markStaleRead: db.prepare(
1417
+ `UPDATE notifications SET read_at = unixepoch() WHERE read_at IS NULL AND created_at < unixepoch() - (7 * 86400)`
1418
+ ),
1419
+ };
1420
+
1421
+ export function createNotification(type, title, body = null, metadata = null, sourceSessionId = null, sourceAgentId = null) {
1422
+ const result = notifStmts.insert.run(type, title, body, metadata, sourceSessionId, sourceAgentId);
1423
+ return {
1424
+ id: result.lastInsertRowid,
1425
+ type, title, body, metadata,
1426
+ source_session_id: sourceSessionId,
1427
+ source_agent_id: sourceAgentId,
1428
+ read_at: null,
1429
+ created_at: Math.floor(Date.now() / 1000),
1430
+ };
1431
+ }
1432
+
1433
+ export function getNotificationHistory(limit = 20, offset = 0, unreadOnly = false, type = null) {
1434
+ if (type && unreadOnly) return notifStmts.historyByTypeUnread.all(type, limit, offset);
1435
+ if (type) return notifStmts.historyByType.all(type, limit, offset);
1436
+ if (unreadOnly) return notifStmts.historyUnread.all(limit, offset);
1437
+ return notifStmts.history.all(limit, offset);
1438
+ }
1439
+
1440
+ export function getUnreadNotificationCount() {
1441
+ return notifStmts.unreadCount.get().count;
1442
+ }
1443
+
1444
+ export function markNotificationsRead(ids) {
1445
+ const tx = db.transaction((idList) => {
1446
+ for (const id of idList) notifStmts.markRead.run(id);
1447
+ });
1448
+ tx(ids);
1449
+ }
1450
+
1451
+ export function markAllNotificationsRead() {
1452
+ notifStmts.markAllRead.run();
1453
+ }
1454
+
1455
+ export function markNotificationsReadBefore(timestamp) {
1456
+ notifStmts.markReadBefore.run(timestamp);
1457
+ }
1458
+
1459
+ export function purgeOldNotifications(days = 90) {
1460
+ notifStmts.markStaleRead.run();
1461
+ notifStmts.purgeOld.run(days);
1462
+ }
1463
+
1464
+ // ── Worktrees ─────────────────────────────────────────────
1465
+ const wtStmts = {
1466
+ create: db.prepare(
1467
+ `INSERT INTO worktrees (id, session_id, project_path, worktree_path, branch_name, base_branch, status, user_prompt)
1468
+ VALUES (?, ?, ?, ?, ?, ?, 'active', ?)`
1469
+ ),
1470
+ get: db.prepare(`SELECT * FROM worktrees WHERE id = ?`),
1471
+ listByProject: db.prepare(
1472
+ `SELECT * FROM worktrees WHERE project_path = ? ORDER BY created_at DESC`
1473
+ ),
1474
+ listActive: db.prepare(
1475
+ `SELECT * FROM worktrees WHERE status IN ('active', 'completed') ORDER BY created_at DESC`
1476
+ ),
1477
+ updateStatus: db.prepare(
1478
+ `UPDATE worktrees SET status = ?, completed_at = unixepoch() WHERE id = ?`
1479
+ ),
1480
+ updateSession: db.prepare(
1481
+ `UPDATE worktrees SET session_id = ? WHERE id = ?`
1482
+ ),
1483
+ delete: db.prepare(`DELETE FROM worktrees WHERE id = ?`),
1484
+ };
1485
+
1486
+ export function createWorktreeRecord(id, sessionId, projectPath, worktreePath, branchName, baseBranch, userPrompt) {
1487
+ wtStmts.create.run(id, sessionId, projectPath, worktreePath, branchName, baseBranch, userPrompt);
1488
+ }
1489
+
1490
+ export function getWorktreeRecord(id) {
1491
+ return wtStmts.get.get(id);
1492
+ }
1493
+
1494
+ export function listWorktreesByProject(projectPath) {
1495
+ return wtStmts.listByProject.all(projectPath);
1496
+ }
1497
+
1498
+ export function listActiveWorktrees() {
1499
+ return wtStmts.listActive.all();
1500
+ }
1501
+
1502
+ export function updateWorktreeStatus(id, status) {
1503
+ wtStmts.updateStatus.run(status, id);
1504
+ }
1505
+
1506
+ export function updateWorktreeSession(id, sessionId) {
1507
+ wtStmts.updateSession.run(sessionId, id);
1508
+ }
1509
+
1510
+ export function deleteWorktreeRecord(id) {
1511
+ wtStmts.delete.run(id);
1512
+ }
1513
+
1266
1514
  // ── Memories (persistent cross-session context) ──────────
1267
1515
  function hashContent(projectPath, content) {
1268
1516
  return createHash("sha256").update(`${projectPath}:${content}`).digest("hex");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudeck",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
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",
@@ -42,7 +42,10 @@
42
42
  "node": ">=18.0.0"
43
43
  },
44
44
  "scripts": {
45
- "start": "node server.js"
45
+ "start": "node server.js",
46
+ "test": "vitest run",
47
+ "test:watch": "vitest",
48
+ "test:coverage": "vitest run --coverage"
46
49
  },
47
50
  "dependencies": {
48
51
  "@anthropic-ai/claude-code": "^1.0.128",
@@ -51,5 +54,11 @@
51
54
  "express": "^4",
52
55
  "web-push": "^3.6.7",
53
56
  "ws": "^8"
57
+ },
58
+ "devDependencies": {
59
+ "@vitest/coverage-v8": "^4.1.0",
60
+ "happy-dom": "^20.8.4",
61
+ "supertest": "^7.2.2",
62
+ "vitest": "^4.1.0"
54
63
  }
55
64
  }
@@ -55,6 +55,9 @@
55
55
  }
56
56
 
57
57
  .git-status-group-title {
58
+ display: flex;
59
+ justify-content: space-between;
60
+ align-items: center;
58
61
  font-size: 10px;
59
62
  font-weight: 600;
60
63
  color: var(--text-dim);
@@ -63,6 +66,23 @@
63
66
  padding: 6px 10px 2px;
64
67
  }
65
68
 
69
+ .git-bulk-action {
70
+ background: none;
71
+ border: none;
72
+ color: var(--text-dim);
73
+ font-size: 10px;
74
+ font-weight: 700;
75
+ font-family: var(--font-mono);
76
+ cursor: pointer;
77
+ padding: 0 4px;
78
+ border-radius: var(--radius);
79
+ transition: color 0.15s;
80
+ }
81
+
82
+ .git-bulk-action:hover {
83
+ color: var(--accent);
84
+ }
85
+
66
86
  .git-status-file {
67
87
  display: flex;
68
88
  align-items: center;
@@ -97,6 +117,12 @@
97
117
  text-overflow: ellipsis;
98
118
  white-space: nowrap;
99
119
  color: var(--text);
120
+ cursor: pointer;
121
+ }
122
+
123
+ .git-status-name:hover {
124
+ color: var(--accent);
125
+ text-decoration: underline;
100
126
  }
101
127
 
102
128
  .git-status-action {
@@ -219,3 +245,197 @@
219
245
  .git-empty svg {
220
246
  opacity: 0.4;
221
247
  }
248
+
249
+ /* Discard button */
250
+ .git-discard-btn:hover {
251
+ color: var(--error) !important;
252
+ }
253
+
254
+ /* Branch info bar */
255
+ .git-branch-info {
256
+ display: flex;
257
+ align-items: center;
258
+ gap: 6px;
259
+ padding: 6px 10px;
260
+ font-size: 11px;
261
+ border-bottom: 1px solid var(--border);
262
+ background: var(--bg-secondary);
263
+ }
264
+
265
+ .git-branch-icon {
266
+ color: var(--accent);
267
+ flex-shrink: 0;
268
+ }
269
+
270
+ .git-branch-info strong {
271
+ color: var(--text);
272
+ font-family: var(--font-mono);
273
+ font-size: 12px;
274
+ }
275
+
276
+ .git-branch-tracking {
277
+ margin-left: auto;
278
+ font-size: 10px;
279
+ display: flex;
280
+ gap: 6px;
281
+ }
282
+
283
+ .git-branch-ahead {
284
+ color: var(--accent);
285
+ font-weight: 600;
286
+ }
287
+
288
+ .git-branch-behind {
289
+ color: var(--warning);
290
+ font-weight: 600;
291
+ }
292
+
293
+ .git-branch-synced {
294
+ color: var(--text-dim);
295
+ font-style: italic;
296
+ }
297
+
298
+ /* Clickable commit hashes */
299
+ .git-log-hash {
300
+ cursor: pointer;
301
+ }
302
+
303
+ .git-log-hash:hover {
304
+ text-decoration: underline;
305
+ }
306
+
307
+ /* Branch switch error */
308
+ .git-branch-error {
309
+ font-size: 10px;
310
+ color: var(--error);
311
+ padding: 4px 8px;
312
+ line-height: 1.3;
313
+ }
314
+
315
+ /* Diff modal — large size */
316
+ .git-diff-modal {
317
+ max-width: 90vw;
318
+ width: 1000px;
319
+ max-height: 90vh;
320
+ display: flex;
321
+ flex-direction: column;
322
+ }
323
+
324
+ /* Scrollable diff body */
325
+ .git-diff-body {
326
+ flex: 1;
327
+ overflow-y: auto;
328
+ padding: 12px;
329
+ }
330
+
331
+ .git-diff-body::-webkit-scrollbar { width: 6px; }
332
+ .git-diff-body::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
333
+
334
+ .git-diff-empty {
335
+ text-align: center;
336
+ color: var(--text-dim);
337
+ padding: 32px;
338
+ font-size: 13px;
339
+ }
340
+
341
+ /* Per-file section */
342
+ .git-diff-file {
343
+ margin-bottom: 8px;
344
+ border: 1px solid var(--border);
345
+ border-radius: var(--radius);
346
+ overflow: hidden;
347
+ }
348
+
349
+ .git-diff-file-header {
350
+ display: flex;
351
+ align-items: center;
352
+ gap: 8px;
353
+ padding: 8px 12px;
354
+ background: var(--bg-tertiary);
355
+ cursor: pointer;
356
+ user-select: none;
357
+ transition: background 0.15s;
358
+ }
359
+
360
+ .git-diff-file-header:hover {
361
+ background: var(--bg-secondary);
362
+ }
363
+
364
+ .git-diff-chevron {
365
+ flex-shrink: 0;
366
+ color: var(--text-dim);
367
+ transition: transform 0.15s;
368
+ }
369
+
370
+ .git-diff-file.collapsed .git-diff-chevron {
371
+ transform: rotate(-90deg);
372
+ }
373
+
374
+ .git-diff-file.collapsed .git-diff-file-content {
375
+ display: none;
376
+ }
377
+
378
+ .git-diff-file-name {
379
+ font-family: var(--font-mono);
380
+ font-size: 12px;
381
+ font-weight: 600;
382
+ color: var(--text);
383
+ flex: 1;
384
+ overflow: hidden;
385
+ text-overflow: ellipsis;
386
+ white-space: nowrap;
387
+ }
388
+
389
+ .git-diff-file-stats {
390
+ display: flex;
391
+ gap: 6px;
392
+ font-family: var(--font-mono);
393
+ font-size: 11px;
394
+ flex-shrink: 0;
395
+ }
396
+
397
+ .diff-stat-add { color: #3fb950; font-weight: 600; }
398
+ .diff-stat-del { color: #f85149; font-weight: 600; }
399
+
400
+ /* Diff content block (inside per-file section) */
401
+ .git-diff-file-content {
402
+ max-height: none;
403
+ border: none;
404
+ border-radius: 0;
405
+ border-top: 1px solid var(--border);
406
+ margin: 0;
407
+ }
408
+
409
+ /* Colored diff in modal */
410
+ .git-diff-content {
411
+ max-height: 75vh;
412
+ overflow: auto;
413
+ padding: 12px 16px;
414
+ font-size: 12px;
415
+ font-family: var(--font-mono);
416
+ background: var(--bg-primary);
417
+ border: 1px solid var(--border);
418
+ border-radius: var(--radius);
419
+ white-space: pre;
420
+ line-height: 1.5;
421
+ color: var(--text);
422
+ tab-size: 4;
423
+ }
424
+
425
+ .diff-line-added {
426
+ color: #3fb950;
427
+ }
428
+
429
+ .diff-line-removed {
430
+ color: #f85149;
431
+ }
432
+
433
+ .diff-line-hunk {
434
+ color: var(--accent);
435
+ font-weight: 600;
436
+ }
437
+
438
+ .diff-line-meta {
439
+ color: var(--text-dim);
440
+ font-weight: 600;
441
+ }