@tenonhq/dovetail-dashboard 0.0.20 → 0.0.22

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tenonhq/dovetail-dashboard",
3
- "version": "0.0.20",
3
+ "version": "0.0.22",
4
4
  "description": "Update Set Dashboard for Dovetail",
5
5
  "main": "server.js",
6
6
  "scripts": {
@@ -311,6 +311,47 @@
311
311
  border-radius: var(--radius-pill);
312
312
  letter-spacing: 0.07em;
313
313
  }
314
+
315
+ /* ── Prompt cards (Prompt tab) ──────────────────────────────────────────── */
316
+ .cp-prompt-card .cp-prompt-score {
317
+ background: var(--accent-bg, var(--bg-muted));
318
+ color: var(--accent);
319
+ text-transform: none;
320
+ letter-spacing: 0;
321
+ font-size: 11px;
322
+ }
323
+ .cp-prompt-source {
324
+ margin: 0 0 12px 0;
325
+ font-size: 12px;
326
+ color: var(--fg-muted);
327
+ }
328
+ .cp-prompt-source summary {
329
+ cursor: pointer;
330
+ user-select: none;
331
+ padding: 4px 0;
332
+ }
333
+ .cp-prompt-source pre,
334
+ .cp-prompt-body {
335
+ background: var(--bg-paper);
336
+ border: 1px solid var(--border);
337
+ border-radius: var(--radius-md);
338
+ padding: 12px;
339
+ overflow-x: auto;
340
+ white-space: pre-wrap;
341
+ word-break: break-word;
342
+ font-family: var(--font-mono);
343
+ font-size: 12px;
344
+ line-height: 1.5;
345
+ margin: 0;
346
+ }
347
+ .cp-prompt-body code,
348
+ .cp-prompt-source pre code {
349
+ background: none;
350
+ padding: 0;
351
+ font-family: inherit;
352
+ font-size: inherit;
353
+ color: var(--fg);
354
+ }
314
355
  .cp-mermaid {
315
356
  background: var(--bg-paper);
316
357
  border: 1px solid var(--border);
@@ -83,11 +83,15 @@
83
83
  </header>
84
84
  <nav class="cp-tabs">
85
85
  <button class="cp-tab active" data-tab="plan">Plan</button>
86
+ <button class="cp-tab" data-tab="prompts">
87
+ Prompt <span class="cp-tab-count" id="cp-prompt-count">0</span>
88
+ </button>
86
89
  <button class="cp-tab" data-tab="artifacts">
87
90
  Artifacts <span class="cp-tab-count" id="cp-artifact-count">0</span>
88
91
  </button>
89
92
  </nav>
90
93
  <div class="cp-tab-panel" id="cp-tab-plan"></div>
94
+ <div class="cp-tab-panel" id="cp-tab-prompts" hidden></div>
91
95
  <div class="cp-tab-panel" id="cp-tab-artifacts" hidden></div>
92
96
  </article>
93
97
  </section>
@@ -33,6 +33,7 @@
33
33
  var state = {
34
34
  plans: new Map(), // slug -> plan
35
35
  artifacts: new Map(), // slug -> Map<artifactSlug, artifact>
36
+ prompts: new Map(), // slug -> Map<promptSlug, prompt>
36
37
  selectedSlug: null,
37
38
  activeTab: "plan",
38
39
  query: "" // lowercased search query; "" means show all
@@ -53,8 +54,10 @@
53
54
  detailStatus: document.getElementById("cp-detail-status"),
54
55
  detailStamp: document.getElementById("cp-detail-stamp"),
55
56
  artifactCount: document.getElementById("cp-artifact-count"),
57
+ promptCount: document.getElementById("cp-prompt-count"),
56
58
  planPanel: document.getElementById("cp-tab-plan"),
57
59
  artifactsPanel: document.getElementById("cp-tab-artifacts"),
60
+ promptsPanel: document.getElementById("cp-tab-prompts"),
58
61
  tabs: document.querySelectorAll(".cp-tab")
59
62
  };
60
63
 
@@ -106,6 +109,14 @@
106
109
  });
107
110
  }
108
111
 
112
+ function sortedPrompts(slug) {
113
+ var map = state.prompts.get(slug);
114
+ if (!map) return [];
115
+ return Array.from(map.values()).sort(function (a, b) {
116
+ return (a.created_at || "").localeCompare(b.created_at || "");
117
+ });
118
+ }
119
+
109
120
  function fmtTime(iso) {
110
121
  if (!iso) return "";
111
122
  var d = new Date(iso);
@@ -241,7 +252,7 @@
241
252
  return btn;
242
253
  }
243
254
 
244
- function addTabsCopyAll(plan, artifacts) {
255
+ function addTabsCopyAll(plan, artifacts, prompts) {
245
256
  var existing = document.getElementById("cp-tabs-copy-all");
246
257
  if (existing) existing.remove();
247
258
 
@@ -251,6 +262,11 @@
251
262
  return "# " + a.title + "\n\n" + a.content;
252
263
  }).join("\n\n---\n\n");
253
264
  }
265
+ if (state.activeTab === "prompts") {
266
+ return prompts.map(function (p) {
267
+ return "# " + p.title + "\n\n" + p.content;
268
+ }).join("\n\n---\n\n");
269
+ }
254
270
  return plan.content_md && plan.content_md.trim()
255
271
  ? plan.content_md
256
272
  : els.planPanel.innerText.trim();
@@ -363,6 +379,7 @@
363
379
  }
364
380
  var plan = state.plans.get(state.selectedSlug);
365
381
  var artifacts = sortedArtifacts(state.selectedSlug);
382
+ var prompts = sortedPrompts(state.selectedSlug);
366
383
 
367
384
  els.detailEmpty.style.display = "none";
368
385
  els.detailBody.hidden = false;
@@ -371,6 +388,7 @@
371
388
  els.detailStatus.className = "cp-status-pill cp-status-" + plan.status;
372
389
  els.detailStamp.textContent = "updated " + fmtTime(plan.updated_at);
373
390
  els.artifactCount.textContent = String(artifacts.length);
391
+ if (els.promptCount) els.promptCount.textContent = String(prompts.length);
374
392
 
375
393
  var existingPrBadge = document.getElementById("cp-pr-badge");
376
394
  if (existingPrBadge) existingPrBadge.remove();
@@ -409,6 +427,28 @@
409
427
  });
410
428
  els.detailStatus.insertAdjacentElement("afterend", resumeBtn);
411
429
 
430
+ var existingPromptBtn = document.getElementById("cp-gen-prompt-btn");
431
+ if (existingPromptBtn) existingPromptBtn.remove();
432
+ var genPromptBtn = document.createElement("button");
433
+ genPromptBtn.id = "cp-gen-prompt-btn";
434
+ genPromptBtn.className = "cp-resume-btn";
435
+ genPromptBtn.textContent = "Generate Prompt";
436
+ genPromptBtn.title = "Copy /improve-prompt --from-plan <slug> to clipboard";
437
+ genPromptBtn.addEventListener("click", function () {
438
+ var cmd = "/improve-prompt --from-plan " + plan.slug;
439
+ var finish = function () { showToast("Copied — paste into Claude Code."); };
440
+ if (navigator.clipboard && navigator.clipboard.writeText) {
441
+ navigator.clipboard.writeText(cmd).then(finish).catch(function () {
442
+ fallbackCopy(cmd);
443
+ finish();
444
+ });
445
+ } else {
446
+ fallbackCopy(cmd);
447
+ finish();
448
+ }
449
+ });
450
+ resumeBtn.insertAdjacentElement("afterend", genPromptBtn);
451
+
412
452
  if (plan.content_html) {
413
453
  els.planPanel.innerHTML = window.DOMPurify
414
454
  ? window.DOMPurify.sanitize(plan.content_html)
@@ -418,7 +458,7 @@
418
458
  }
419
459
 
420
460
  addPlanSectionCopyBtns();
421
- addTabsCopyAll(plan, artifacts);
461
+ addTabsCopyAll(plan, artifacts, prompts);
422
462
 
423
463
  els.artifactsPanel.innerHTML = "";
424
464
  if (artifacts.length === 0) {
@@ -458,6 +498,69 @@
458
498
  els.artifactsPanel.appendChild(card);
459
499
  });
460
500
  }
501
+
502
+ els.promptsPanel.innerHTML = "";
503
+ if (prompts.length === 0) {
504
+ var emptyPrompts = document.createElement("div");
505
+ emptyPrompts.className = "cp-detail-empty";
506
+ emptyPrompts.style.padding = "40px 0";
507
+ emptyPrompts.textContent = 'No prompts yet. Click "Generate Prompt" above to seed one, or push from /improve-prompt --push.';
508
+ els.promptsPanel.appendChild(emptyPrompts);
509
+ } else {
510
+ prompts.forEach(function (prompt) {
511
+ var card = document.createElement("div");
512
+ card.className = "cp-artifact-card cp-prompt-card";
513
+ var head = document.createElement("div");
514
+ head.className = "cp-artifact-head";
515
+ var title = document.createElement("span");
516
+ title.className = "cp-artifact-title";
517
+ title.textContent = prompt.title;
518
+ head.appendChild(title);
519
+
520
+ if (typeof prompt.score_before === "number" && typeof prompt.score_after === "number") {
521
+ var scorePill = document.createElement("span");
522
+ scorePill.className = "cp-kind-pill cp-prompt-score";
523
+ scorePill.textContent = prompt.score_before + " → " + prompt.score_after + "%";
524
+ head.appendChild(scorePill);
525
+ } else if (typeof prompt.score_after === "number") {
526
+ var scorePillOnly = document.createElement("span");
527
+ scorePillOnly.className = "cp-kind-pill cp-prompt-score";
528
+ scorePillOnly.textContent = prompt.score_after + "%";
529
+ head.appendChild(scorePillOnly);
530
+ }
531
+
532
+ var copyGroup = document.createElement("div");
533
+ copyGroup.className = "cp-copy-btn-group cp-copy-btn-group--artifact";
534
+ copyGroup.appendChild(makeCopyBtn("Copy", (function (content) {
535
+ return function () { return content; };
536
+ })(prompt.content)));
537
+ head.appendChild(copyGroup);
538
+
539
+ card.appendChild(head);
540
+
541
+ if (prompt.source_draft) {
542
+ var details = document.createElement("details");
543
+ details.className = "cp-prompt-source";
544
+ var summary = document.createElement("summary");
545
+ summary.textContent = "Original draft";
546
+ details.appendChild(summary);
547
+ var pre = document.createElement("pre");
548
+ var code = document.createElement("code");
549
+ code.textContent = prompt.source_draft;
550
+ pre.appendChild(code);
551
+ details.appendChild(pre);
552
+ card.appendChild(details);
553
+ }
554
+
555
+ var body = document.createElement("pre");
556
+ body.className = "cp-prompt-body";
557
+ var bodyCode = document.createElement("code");
558
+ bodyCode.textContent = prompt.content;
559
+ body.appendChild(bodyCode);
560
+ card.appendChild(body);
561
+ els.promptsPanel.appendChild(card);
562
+ });
563
+ }
461
564
  }
462
565
 
463
566
  function setActiveTab(tab) {
@@ -467,6 +570,7 @@
467
570
  });
468
571
  els.planPanel.hidden = tab !== "plan";
469
572
  els.artifactsPanel.hidden = tab !== "artifacts";
573
+ if (els.promptsPanel) els.promptsPanel.hidden = tab !== "prompts";
470
574
  }
471
575
 
472
576
  function selectPlan(slug) {
@@ -551,6 +655,7 @@
551
655
  function removePlan(slug) {
552
656
  state.plans.delete(slug);
553
657
  state.artifacts.delete(slug);
658
+ state.prompts.delete(slug);
554
659
  if (state.selectedSlug === slug) state.selectedSlug = null;
555
660
  renderRail();
556
661
  renderDetail();
@@ -575,6 +680,23 @@
575
680
  if (state.selectedSlug === planSlug) renderDetail();
576
681
  }
577
682
 
683
+ function upsertPrompt(prompt) {
684
+ var bucket = state.prompts.get(prompt.plan_slug);
685
+ if (!bucket) {
686
+ bucket = new Map();
687
+ state.prompts.set(prompt.plan_slug, bucket);
688
+ }
689
+ bucket.set(prompt.slug, prompt);
690
+ if (state.selectedSlug === prompt.plan_slug) renderDetail();
691
+ }
692
+
693
+ function removePrompt(planSlug, slug) {
694
+ var bucket = state.prompts.get(planSlug);
695
+ if (!bucket) return;
696
+ bucket.delete(slug);
697
+ if (state.selectedSlug === planSlug) renderDetail();
698
+ }
699
+
578
700
  async function loadInitial() {
579
701
  try {
580
702
  var res = await fetch("/api/claude-plans");
@@ -586,7 +708,7 @@
586
708
  } catch (err) {
587
709
  console.warn("[claude-plans] failed to load initial state:", err);
588
710
  }
589
- // Preload artifacts for visible plans
711
+ // Preload artifacts + prompts for visible plans
590
712
  var slugs = Array.from(state.plans.keys());
591
713
  await Promise.all(slugs.map(function (slug) {
592
714
  return fetch("/api/claude-plans/" + encodeURIComponent(slug))
@@ -597,6 +719,11 @@
597
719
  data.artifacts.forEach(function (a) { bucket.set(a.slug, a); });
598
720
  state.artifacts.set(slug, bucket);
599
721
  }
722
+ if (data && Array.isArray(data.prompts)) {
723
+ var pBucket = new Map();
724
+ data.prompts.forEach(function (p) { pBucket.set(p.slug, p); });
725
+ state.prompts.set(slug, pBucket);
726
+ }
600
727
  })
601
728
  .catch(function () { /* ignore */ });
602
729
  }));
@@ -624,6 +751,15 @@
624
751
  removeArtifact(payload.plan_slug, payload.slug);
625
752
  } catch (_) {}
626
753
  });
754
+ es.addEventListener("prompt:upsert", function (e) {
755
+ try { upsertPrompt(JSON.parse(e.data).prompt); } catch (_) {}
756
+ });
757
+ es.addEventListener("prompt:delete", function (e) {
758
+ try {
759
+ var payload = JSON.parse(e.data);
760
+ removePrompt(payload.plan_slug, payload.slug);
761
+ } catch (_) {}
762
+ });
627
763
  es.addEventListener("plan:focus", function (e) {
628
764
  try {
629
765
  var data = JSON.parse(e.data);
package/server.js CHANGED
@@ -830,6 +830,10 @@ function artifactsDirFor(slug) {
830
830
  return path.join(CLAUDE_PLANS_DIR, slug, "artifacts");
831
831
  }
832
832
 
833
+ function promptsDirFor(slug) {
834
+ return path.join(CLAUDE_PLANS_DIR, slug, "prompts");
835
+ }
836
+
833
837
  function safeReadJson(filePath) {
834
838
  try {
835
839
  if (!fs.existsSync(filePath)) return null;
@@ -872,6 +876,25 @@ function listClaudeArtifacts(slug) {
872
876
  return artifacts;
873
877
  }
874
878
 
879
+ function listClaudePrompts(slug) {
880
+ if (!isValidSlug(slug)) return [];
881
+ const baseDir = path.resolve(CLAUDE_PLANS_DIR);
882
+ const dir = path.resolve(promptsDirFor(slug));
883
+ if (!dir.startsWith(baseDir + path.sep)) return [];
884
+ if (!fs.existsSync(dir)) return [];
885
+ const entries = fs.readdirSync(dir);
886
+ const prompts = [];
887
+ for (let i = 0; i < entries.length; i++) {
888
+ if (!entries[i].endsWith(".json")) continue;
889
+ const p = safeReadJson(path.join(dir, entries[i]));
890
+ if (p) prompts.push(p);
891
+ }
892
+ prompts.sort(function (a, b) {
893
+ return (a.created_at || "").localeCompare(b.created_at || "");
894
+ });
895
+ return prompts;
896
+ }
897
+
875
898
  // Parse a watcher path like "<root>/<slug>.json" or
876
899
  // "<root>/<slug>/artifacts/<artifact-slug>.json" into { kind, slug, artifactSlug }.
877
900
  function classifyPath(filePath) {
@@ -887,6 +910,9 @@ function classifyPath(filePath) {
887
910
  if (parts.length === 3 && parts[1] === "artifacts" && parts[2].endsWith(".json")) {
888
911
  return { kind: "artifact", slug: parts[0], artifactSlug: parts[2].slice(0, -5) };
889
912
  }
913
+ if (parts.length === 3 && parts[1] === "prompts" && parts[2].endsWith(".json")) {
914
+ return { kind: "prompt", slug: parts[0], promptSlug: parts[2].slice(0, -5) };
915
+ }
890
916
  return null;
891
917
  }
892
918
 
@@ -934,6 +960,18 @@ function handleWatcherChange(event, filePath) {
934
960
  }
935
961
  const artifact = safeReadJson(filePath);
936
962
  if (artifact) broadcastClaudePlanEvent("artifact:upsert", { artifact: artifact });
963
+ return;
964
+ }
965
+ if (info.kind === "prompt") {
966
+ if (event === "unlink") {
967
+ broadcastClaudePlanEvent("prompt:delete", {
968
+ plan_slug: info.slug,
969
+ slug: info.promptSlug
970
+ });
971
+ return;
972
+ }
973
+ const prompt = safeReadJson(filePath);
974
+ if (prompt) broadcastClaudePlanEvent("prompt:upsert", { prompt: prompt });
937
975
  }
938
976
  }
939
977
 
@@ -1003,7 +1041,11 @@ app.get("/api/claude-plans/:slug", function (req, res) {
1003
1041
  if (!isValidSlug(slug)) return res.status(400).json({ error: "invalid slug" });
1004
1042
  const plan = safeReadJson(planFilePath(slug));
1005
1043
  if (!plan) return res.status(404).json({ error: "plan not found" });
1006
- res.json({ plan: plan, artifacts: listClaudeArtifacts(slug) });
1044
+ res.json({
1045
+ plan: plan,
1046
+ artifacts: listClaudeArtifacts(slug),
1047
+ prompts: listClaudePrompts(slug)
1048
+ });
1007
1049
  } catch (e) {
1008
1050
  res.status(500).json({ error: e.message });
1009
1051
  }