@tenonhq/dovetail-dashboard 0.0.21 → 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 +1 -1
- package/public/claude-plans.css +41 -0
- package/public/claude-plans.html +4 -0
- package/public/claude-plans.js +139 -3
- package/server.js +43 -1
package/package.json
CHANGED
package/public/claude-plans.css
CHANGED
|
@@ -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);
|
package/public/claude-plans.html
CHANGED
|
@@ -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>
|
package/public/claude-plans.js
CHANGED
|
@@ -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({
|
|
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
|
}
|