claudeck 1.1.1 → 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 (37) hide show
  1. package/README.md +30 -4
  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/worktree.css +442 -0
  12. package/public/index.html +43 -10
  13. package/public/js/core/api.js +83 -0
  14. package/public/js/core/dom.js +15 -0
  15. package/public/js/features/background-sessions.js +11 -0
  16. package/public/js/features/chat.js +501 -3
  17. package/public/js/features/input-history.js +122 -0
  18. package/public/js/features/projects.js +16 -1
  19. package/public/js/features/sessions.js +77 -30
  20. package/public/js/main.js +3 -0
  21. package/public/js/panels/git-panel.js +385 -6
  22. package/public/js/panels/skills-manager.js +1005 -0
  23. package/public/js/ui/messages.js +58 -0
  24. package/public/js/ui/notification-bell.js +240 -0
  25. package/public/js/ui/notification-history.js +210 -0
  26. package/public/js/ui/parallel.js +11 -0
  27. package/public/js/ui/tab-sdk.js +1 -1
  28. package/public/style.css +4 -0
  29. package/server/agent-loop.js +13 -0
  30. package/server/notification-logger.js +27 -0
  31. package/server/routes/notifications.js +57 -1
  32. package/server/routes/sessions.js +41 -0
  33. package/server/routes/skills.js +454 -0
  34. package/server/routes/worktrees.js +93 -0
  35. package/server/utils/git-worktree.js +297 -0
  36. package/server/ws-handler.js +708 -629
  37. package/server.js +17 -1
@@ -48,13 +48,32 @@ async function switchBranch(branch) {
48
48
 
49
49
  try {
50
50
  const result = await execCommand(`git checkout "${branch}"`, cwd);
51
- if (result.error && result.stderr) {
52
- console.error("Branch switch error:", result.stderr);
51
+ if (result.exitCode !== 0 || (result.error && result.stderr)) {
52
+ // Show error to the user — typically "uncommitted changes" blocking the switch
53
+ const errMsg = (result.stderr || result.stdout || "").trim();
54
+ showBranchError(errMsg || "Cannot switch branch");
55
+ // Reset dropdown to actual current branch
56
+ await loadBranches();
57
+ return;
53
58
  }
54
59
  await refreshAll();
55
60
  } catch {}
56
61
  }
57
62
 
63
+ function showBranchError(msg) {
64
+ // Remove old error
65
+ const old = $.gitBranchSelect.parentElement.querySelector(".git-branch-error");
66
+ if (old) old.remove();
67
+
68
+ const el = document.createElement("div");
69
+ el.className = "git-branch-error";
70
+ el.textContent = msg.split("\n")[0]; // first line only
71
+ $.gitBranchSelect.parentElement.appendChild(el);
72
+
73
+ // Auto-dismiss after 5s
74
+ setTimeout(() => el.remove(), 5000);
75
+ }
76
+
58
77
  // ── Status ──────────────────────────────────────────────
59
78
 
60
79
  function parseStatusCode(x, y) {
@@ -136,7 +155,27 @@ function renderGroup(title, files, action) {
136
155
 
137
156
  const group = document.createElement("div");
138
157
  group.className = "git-status-group";
139
- group.innerHTML = `<div class="git-status-group-title">${title} (${files.length})</div>`;
158
+
159
+ // Group header with bulk action button
160
+ const bulkLabel = action === "stage" ? "Stage All" : "Unstage All";
161
+ const bulkSymbol = action === "stage" ? "++" : "\u2212\u2212";
162
+ group.innerHTML = `
163
+ <div class="git-status-group-title">
164
+ <span>${title} (${files.length})</span>
165
+ <button class="git-bulk-action" title="${bulkLabel}">${bulkSymbol}</button>
166
+ </div>
167
+ `;
168
+
169
+ group.querySelector(".git-bulk-action").addEventListener("click", async () => {
170
+ const cwd = getCwd();
171
+ if (!cwd) return;
172
+ const fileArgs = files.map((f) => `"${f.file}"`).join(" ");
173
+ const cmd = action === "stage"
174
+ ? `git add ${fileArgs}`
175
+ : `git reset HEAD ${fileArgs}`;
176
+ await execCommand(cmd, cwd);
177
+ await loadStatus();
178
+ });
140
179
 
141
180
  for (const f of files) {
142
181
  const row = document.createElement("div");
@@ -144,10 +183,16 @@ function renderGroup(title, files, action) {
144
183
  row.innerHTML = `
145
184
  <span class="git-status-badge ${f.cls}">${f.badge}</span>
146
185
  <span class="git-status-name" title="${escapeHtml(f.file)}">${escapeHtml(f.file)}</span>
147
- <button class="git-status-action" title="${action === "stage" ? "Stage" : "Unstage"}">${action === "stage" ? "+" : "\u2212"}</button>
186
+ <button class="git-status-action git-stage-btn" title="${action === "stage" ? "Stage" : "Unstage"}">${action === "stage" ? "+" : "\u2212"}</button>
148
187
  `;
149
188
 
150
- row.querySelector(".git-status-action").addEventListener("click", async () => {
189
+ // File diff preview on name click
190
+ row.querySelector(".git-status-name").addEventListener("click", () => {
191
+ showFileDiff(f.file, action === "unstage", f.badge === "?");
192
+ });
193
+
194
+ // Stage / unstage button
195
+ row.querySelector(".git-stage-btn").addEventListener("click", async () => {
151
196
  const cwd = getCwd();
152
197
  if (!cwd) return;
153
198
  const cmd = action === "stage"
@@ -157,12 +202,51 @@ function renderGroup(title, files, action) {
157
202
  await loadStatus();
158
203
  });
159
204
 
205
+ // Discard button — only for tracked changed files (not staged, not untracked)
206
+ if (action === "stage" && f.badge !== "?") {
207
+ const discardBtn = document.createElement("button");
208
+ discardBtn.className = "git-status-action git-discard-btn";
209
+ discardBtn.title = "Discard changes";
210
+ discardBtn.textContent = "\u2715";
211
+ discardBtn.addEventListener("click", async () => {
212
+ const cwd = getCwd();
213
+ if (!cwd) return;
214
+ await execCommand(`git checkout -- "${f.file}"`, cwd);
215
+ await loadStatus();
216
+ });
217
+ row.appendChild(discardBtn);
218
+ }
219
+
160
220
  group.appendChild(row);
161
221
  }
162
222
 
163
223
  $.gitStatusList.appendChild(group);
164
224
  }
165
225
 
226
+ // ── File Diff ──────────────────────────────────────────
227
+
228
+ async function showFileDiff(file, isStaged, isUntracked) {
229
+ const cwd = getCwd();
230
+ if (!cwd) return;
231
+
232
+ let diffText = "";
233
+ if (isUntracked) {
234
+ // Untracked file — show full content as additions
235
+ const result = await execCommand(`cat "${file}"`, cwd);
236
+ if (!result.error && result.stdout) {
237
+ diffText = result.stdout.split("\n").map((l) => "+" + l).join("\n");
238
+ }
239
+ } else if (isStaged) {
240
+ const result = await execCommand(`git diff --cached -- "${file}"`, cwd);
241
+ diffText = result.stdout || "";
242
+ } else {
243
+ const result = await execCommand(`git diff -- "${file}"`, cwd);
244
+ diffText = result.stdout || "";
245
+ }
246
+
247
+ showDiffModal(diffText, file);
248
+ }
249
+
166
250
  // ── Commit ──────────────────────────────────────────────
167
251
 
168
252
  async function handleCommit() {
@@ -230,17 +314,312 @@ async function loadLog() {
230
314
  <span class="git-log-subject">${escapeHtml(subject || "")}</span>
231
315
  <span class="git-log-time">${escapeHtml(time || "")}</span>
232
316
  `;
317
+
318
+ // Clickable commit hash → show commit diff
319
+ item.querySelector(".git-log-hash").addEventListener("click", () => showCommitDiff(hash));
320
+
233
321
  $.gitLogList.appendChild(item);
234
322
  }
235
323
  } catch {}
236
324
  }
237
325
 
326
+ async function showCommitDiff(hash) {
327
+ const cwd = getCwd();
328
+ if (!cwd) return;
329
+ const result = await execCommand(`git show "${hash}" --stat --patch`, cwd);
330
+ if (!result.error) showDiffModal(result.stdout, `Commit ${hash}`);
331
+ }
332
+
333
+ // ── Branch Info ────────────────────────────────────────
334
+
335
+ async function loadBranchInfo() {
336
+ const cwd = getCwd();
337
+ if (!cwd || !$.gitBranchInfo) return;
338
+
339
+ try {
340
+ const branchResult = await execCommand("git rev-parse --abbrev-ref HEAD", cwd);
341
+ const branch = branchResult.stdout?.trim();
342
+ if (!branch || branchResult.error) {
343
+ $.gitBranchInfo.classList.add("hidden");
344
+ return;
345
+ }
346
+
347
+ // Get ahead/behind tracking info
348
+ const trackResult = await execCommand(
349
+ "git rev-list --left-right --count HEAD...@{upstream}",
350
+ cwd
351
+ );
352
+
353
+ let trackingHtml = "";
354
+ if (!trackResult.error && trackResult.stdout?.trim()) {
355
+ const [ahead, behind] = trackResult.stdout.trim().split(/\s+/);
356
+ const parts = [];
357
+ if (parseInt(ahead) > 0) parts.push(`<span class="git-branch-ahead">\u2191${ahead}</span>`);
358
+ if (parseInt(behind) > 0) parts.push(`<span class="git-branch-behind">\u2193${behind}</span>`);
359
+ if (parts.length > 0) trackingHtml = parts.join(" ");
360
+ }
361
+
362
+ $.gitBranchInfo.classList.remove("hidden");
363
+ $.gitBranchInfo.innerHTML = `
364
+ <svg class="git-branch-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
365
+ <line x1="6" y1="3" x2="6" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/>
366
+ </svg>
367
+ <strong>${escapeHtml(branch)}</strong>
368
+ ${trackingHtml ? `<span class="git-branch-tracking">${trackingHtml}</span>` : '<span class="git-branch-tracking git-branch-synced">in sync</span>'}
369
+ `;
370
+ } catch {
371
+ $.gitBranchInfo.classList.add("hidden");
372
+ }
373
+ }
374
+
375
+ // ── Worktrees ──────────────────────────────────────────
376
+
377
+ async function loadWorktrees() {
378
+ const cwd = getCwd();
379
+ if (!cwd || !$.gitWorktreeSection) return;
380
+
381
+ try {
382
+ const res = await fetch(`/api/worktrees?project_path=${encodeURIComponent(cwd)}`);
383
+ const worktrees = await res.json();
384
+ const visible = worktrees.filter((wt) => wt.status === "active" || wt.status === "completed");
385
+
386
+ if (visible.length === 0) {
387
+ $.gitWorktreeSection.classList.add("hidden");
388
+ return;
389
+ }
390
+
391
+ $.gitWorktreeSection.classList.remove("hidden");
392
+ $.gitWorktreeList.innerHTML = "";
393
+
394
+ for (const wt of visible) {
395
+ const badgeClass = wt.status === "active" ? "running" : "ready";
396
+ const item = document.createElement("div");
397
+ item.className = "git-worktree-item";
398
+ item.innerHTML = `
399
+ <span class="git-worktree-badge ${badgeClass}">${escapeHtml(wt.status)}</span>
400
+ <span class="git-worktree-name" title="${escapeHtml(wt.branch_name)}">${escapeHtml(wt.branch_name)}</span>
401
+ <div class="git-worktree-actions">
402
+ <button class="wt-view" data-tooltip="View Diff" title="View Diff">
403
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>
404
+ </button>
405
+ <button class="wt-merge" data-tooltip="Squash Merge" title="Squash Merge">
406
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 3 21 3 21 8"/><line x1="4" y1="20" x2="21" y2="3"/></svg>
407
+ </button>
408
+ <button class="wt-discard" data-tooltip="Discard Worktree" title="Discard Worktree">
409
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
410
+ </button>
411
+ </div>
412
+ `;
413
+
414
+ item.querySelector(".wt-view").addEventListener("click", async () => {
415
+ try {
416
+ const r = await fetch(`/api/worktrees/${encodeURIComponent(wt.id)}/diff`);
417
+ const data = await r.json();
418
+ if (data.error) return;
419
+ showDiffModal(data.diff, `Worktree: ${wt.branch_name}`);
420
+ } catch {}
421
+ });
422
+
423
+ item.querySelector(".wt-merge").addEventListener("click", async (e) => {
424
+ const btn = e.currentTarget;
425
+ btn.disabled = true;
426
+ try {
427
+ const r = await fetch(`/api/worktrees/${encodeURIComponent(wt.id)}/merge`, {
428
+ method: "POST",
429
+ headers: { "Content-Type": "application/json" },
430
+ body: JSON.stringify({}),
431
+ });
432
+ const data = await r.json();
433
+ if (data.ok) {
434
+ loadWorktrees();
435
+ loadLog();
436
+ }
437
+ } catch {} finally {
438
+ btn.disabled = false;
439
+ }
440
+ });
441
+
442
+ item.querySelector(".wt-discard").addEventListener("click", (e) => {
443
+ const btn = e.currentTarget;
444
+ const actions = btn.closest(".git-worktree-actions");
445
+
446
+ // Show inline confirm/cancel replacing the action buttons
447
+ const confirm = document.createElement("div");
448
+ confirm.className = "git-wt-confirm";
449
+ confirm.innerHTML = `
450
+ <span class="git-wt-confirm-label">Delete?</span>
451
+ <button class="git-wt-confirm-yes" data-tooltip="Confirm delete" title="Confirm">Yes</button>
452
+ <button class="git-wt-confirm-no" data-tooltip="Cancel" title="Cancel">No</button>
453
+ `;
454
+ actions.classList.add("hidden");
455
+ actions.parentElement.appendChild(confirm);
456
+
457
+ confirm.querySelector(".git-wt-confirm-no").addEventListener("click", () => {
458
+ confirm.remove();
459
+ actions.classList.remove("hidden");
460
+ });
461
+
462
+ confirm.querySelector(".git-wt-confirm-yes").addEventListener("click", async () => {
463
+ confirm.querySelector(".git-wt-confirm-yes").disabled = true;
464
+ confirm.querySelector(".git-wt-confirm-no").disabled = true;
465
+ confirm.querySelector(".git-wt-confirm-label").textContent = "Deleting...";
466
+ try {
467
+ const r = await fetch(`/api/worktrees/${encodeURIComponent(wt.id)}`, { method: "DELETE" });
468
+ const data = await r.json();
469
+ if (data.ok) loadWorktrees();
470
+ } catch {
471
+ confirm.remove();
472
+ actions.classList.remove("hidden");
473
+ }
474
+ });
475
+ });
476
+
477
+ $.gitWorktreeList.appendChild(item);
478
+ }
479
+ } catch {
480
+ $.gitWorktreeSection.classList.add("hidden");
481
+ }
482
+ }
483
+
484
+ /**
485
+ * Parse a unified diff into per-file sections.
486
+ * Splits on "diff --git" boundaries.
487
+ */
488
+ function parseDiffSections(diffText) {
489
+ const sections = [];
490
+ const lines = diffText.split("\n");
491
+ let current = null;
492
+
493
+ for (const line of lines) {
494
+ if (line.startsWith("diff --git ")) {
495
+ if (current) sections.push(current);
496
+ // Extract filename from "diff --git a/path b/path"
497
+ const match = line.match(/diff --git a\/(.+?) b\/(.+)/);
498
+ const fileName = match ? match[2] : line;
499
+ current = { fileName, lines: [] };
500
+ } else if (current) {
501
+ current.lines.push(line);
502
+ } else {
503
+ // Lines before the first "diff --git" (e.g., commit stats from git show)
504
+ if (!sections.length && !current) {
505
+ current = { fileName: "", lines: [] };
506
+ }
507
+ if (current) current.lines.push(line);
508
+ }
509
+ }
510
+ if (current) sections.push(current);
511
+ return sections;
512
+ }
513
+
514
+ /**
515
+ * Count additions and deletions in diff lines.
516
+ */
517
+ function countDiffStats(lines) {
518
+ let add = 0, del = 0;
519
+ for (const l of lines) {
520
+ if (l.startsWith("+") && !l.startsWith("+++")) add++;
521
+ else if (l.startsWith("-") && !l.startsWith("---")) del++;
522
+ }
523
+ return { add, del };
524
+ }
525
+
526
+ /**
527
+ * Render colored diff lines into a container element.
528
+ */
529
+ function renderDiffLines(container, lines) {
530
+ for (const line of lines) {
531
+ const span = document.createElement("span");
532
+ span.textContent = line + "\n";
533
+ if (line.startsWith("+++") || line.startsWith("---")) {
534
+ span.className = "diff-line-meta";
535
+ } else if (line.startsWith("+")) {
536
+ span.className = "diff-line-added";
537
+ } else if (line.startsWith("-")) {
538
+ span.className = "diff-line-removed";
539
+ } else if (line.startsWith("@@")) {
540
+ span.className = "diff-line-hunk";
541
+ }
542
+ container.appendChild(span);
543
+ }
544
+ }
545
+
546
+ function showDiffModal(diffText, title) {
547
+ const overlay = document.createElement("div");
548
+ overlay.className = "modal-overlay";
549
+ overlay.innerHTML = `
550
+ <div class="modal git-diff-modal">
551
+ <div class="modal-header">
552
+ <h3>${escapeHtml(title)}</h3>
553
+ <button class="modal-close">&times;</button>
554
+ </div>
555
+ <div class="git-diff-body"></div>
556
+ </div>
557
+ `;
558
+
559
+ const body = overlay.querySelector(".git-diff-body");
560
+
561
+ if (!diffText || !diffText.trim()) {
562
+ body.innerHTML = '<div class="git-diff-empty">(no changes)</div>';
563
+ } else {
564
+ const sections = parseDiffSections(diffText);
565
+
566
+ if (sections.length <= 1 && sections[0]?.fileName === "") {
567
+ // Single block without file headers (e.g., single file diff)
568
+ const pre = document.createElement("pre");
569
+ pre.className = "git-diff-content";
570
+ renderDiffLines(pre, sections[0]?.lines || diffText.split("\n"));
571
+ body.appendChild(pre);
572
+ } else {
573
+ // Multi-file diff — render per-file collapsible sections
574
+ for (const section of sections) {
575
+ const { add, del } = countDiffStats(section.lines);
576
+
577
+ const fileSection = document.createElement("div");
578
+ fileSection.className = "git-diff-file";
579
+
580
+ const header = document.createElement("div");
581
+ header.className = "git-diff-file-header";
582
+ header.innerHTML = `
583
+ <svg class="git-diff-chevron" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
584
+ <polyline points="6 9 12 15 18 9"/>
585
+ </svg>
586
+ <span class="git-diff-file-name">${escapeHtml(section.fileName)}</span>
587
+ <span class="git-diff-file-stats">
588
+ ${add ? `<span class="diff-stat-add">+${add}</span>` : ""}
589
+ ${del ? `<span class="diff-stat-del">-${del}</span>` : ""}
590
+ </span>
591
+ `;
592
+
593
+ const content = document.createElement("pre");
594
+ content.className = "git-diff-content git-diff-file-content";
595
+ renderDiffLines(content, section.lines);
596
+
597
+ // Toggle collapse on header click
598
+ header.addEventListener("click", () => {
599
+ fileSection.classList.toggle("collapsed");
600
+ });
601
+
602
+ fileSection.appendChild(header);
603
+ fileSection.appendChild(content);
604
+ body.appendChild(fileSection);
605
+ }
606
+ }
607
+ }
608
+
609
+ overlay.querySelector(".modal-close").addEventListener("click", () => overlay.remove());
610
+ overlay.addEventListener("click", (e) => { if (e.target === overlay) overlay.remove(); });
611
+ document.addEventListener("keydown", function esc(e) {
612
+ if (e.key === "Escape") { overlay.remove(); document.removeEventListener("keydown", esc); }
613
+ });
614
+ document.body.appendChild(overlay);
615
+ }
616
+
238
617
  // ── Refresh ─────────────────────────────────────────────
239
618
 
240
619
  async function refreshAll() {
241
620
  $.gitRefreshBtn.classList.add("spinning");
242
621
  try {
243
- await Promise.all([loadBranches(), loadStatus(), loadLog()]);
622
+ await Promise.all([loadBranches(), loadBranchInfo(), loadStatus(), loadLog(), loadWorktrees()]);
244
623
  } finally {
245
624
  $.gitRefreshBtn.classList.remove("spinning");
246
625
  }