@tenonhq/dovetail-dashboard 0.0.21 → 0.0.23
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 +176 -0
- package/public/claude-plans.html +25 -0
- package/public/claude-plans.js +280 -9
- 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);
|
|
@@ -1082,3 +1123,138 @@
|
|
|
1082
1123
|
background: #14384a;
|
|
1083
1124
|
color: #b6e8ff;
|
|
1084
1125
|
}
|
|
1126
|
+
|
|
1127
|
+
/* ─── Topics cloud ─────────────────────────────────────────────────────────── */
|
|
1128
|
+
|
|
1129
|
+
.cp-topics {
|
|
1130
|
+
margin: 0 24px 12px;
|
|
1131
|
+
background: var(--surface-raised);
|
|
1132
|
+
border: 1px solid var(--border);
|
|
1133
|
+
border-radius: var(--radius-md, 8px);
|
|
1134
|
+
box-shadow: var(--shadow-1, 0 1px 2px rgba(0,0,0,0.04));
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
.cp-topics-summary {
|
|
1138
|
+
display: flex;
|
|
1139
|
+
align-items: center;
|
|
1140
|
+
gap: 8px;
|
|
1141
|
+
padding: 10px 14px;
|
|
1142
|
+
cursor: pointer;
|
|
1143
|
+
list-style: none;
|
|
1144
|
+
user-select: none;
|
|
1145
|
+
font-size: 13px;
|
|
1146
|
+
color: var(--fg);
|
|
1147
|
+
}
|
|
1148
|
+
.cp-topics-summary::-webkit-details-marker { display: none; }
|
|
1149
|
+
.cp-topics-summary:hover { background: color-mix(in srgb, var(--accent) 6%, transparent); }
|
|
1150
|
+
|
|
1151
|
+
.cp-topics-caret {
|
|
1152
|
+
display: inline-block;
|
|
1153
|
+
transition: transform 0.15s ease;
|
|
1154
|
+
color: var(--fg-muted);
|
|
1155
|
+
font-size: 12px;
|
|
1156
|
+
width: 12px;
|
|
1157
|
+
}
|
|
1158
|
+
.cp-topics:not([open]) .cp-topics-caret { transform: rotate(-90deg); }
|
|
1159
|
+
|
|
1160
|
+
.cp-topics-title {
|
|
1161
|
+
font-weight: 600;
|
|
1162
|
+
letter-spacing: 0.02em;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
.cp-topics-count {
|
|
1166
|
+
display: inline-flex;
|
|
1167
|
+
align-items: center;
|
|
1168
|
+
justify-content: center;
|
|
1169
|
+
min-width: 22px;
|
|
1170
|
+
height: 20px;
|
|
1171
|
+
padding: 0 6px;
|
|
1172
|
+
background: var(--bg-muted, var(--surface));
|
|
1173
|
+
color: var(--fg-muted);
|
|
1174
|
+
border: 1px solid var(--border);
|
|
1175
|
+
border-radius: 999px;
|
|
1176
|
+
font-size: 11px;
|
|
1177
|
+
font-weight: 600;
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
.cp-topics-clear {
|
|
1181
|
+
margin-left: auto;
|
|
1182
|
+
background: transparent;
|
|
1183
|
+
border: 1px solid var(--border);
|
|
1184
|
+
border-radius: 6px;
|
|
1185
|
+
padding: 3px 10px;
|
|
1186
|
+
font-size: 11px;
|
|
1187
|
+
color: var(--fg-muted);
|
|
1188
|
+
cursor: pointer;
|
|
1189
|
+
}
|
|
1190
|
+
.cp-topics-clear:hover {
|
|
1191
|
+
background: var(--bg-muted, var(--surface));
|
|
1192
|
+
color: var(--fg);
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
.cp-topics-cloud {
|
|
1196
|
+
display: flex;
|
|
1197
|
+
flex-wrap: wrap;
|
|
1198
|
+
gap: 6px 8px;
|
|
1199
|
+
padding: 10px 14px 14px;
|
|
1200
|
+
border-top: 1px solid var(--border-subtle, var(--border));
|
|
1201
|
+
line-height: 1.3;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
.cp-topics-empty {
|
|
1205
|
+
color: var(--fg-muted);
|
|
1206
|
+
font-size: 12px;
|
|
1207
|
+
font-style: italic;
|
|
1208
|
+
padding: 2px 0;
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
.cp-topic-chip {
|
|
1212
|
+
display: inline-flex;
|
|
1213
|
+
align-items: baseline;
|
|
1214
|
+
gap: 6px;
|
|
1215
|
+
padding: 4px 10px;
|
|
1216
|
+
background: var(--surface);
|
|
1217
|
+
color: var(--fg);
|
|
1218
|
+
border: 1px solid var(--border);
|
|
1219
|
+
border-radius: 999px;
|
|
1220
|
+
font-family: inherit;
|
|
1221
|
+
font-weight: 500;
|
|
1222
|
+
letter-spacing: 0.01em;
|
|
1223
|
+
cursor: pointer;
|
|
1224
|
+
transition: background 0.12s ease, border-color 0.12s ease, color 0.12s ease;
|
|
1225
|
+
}
|
|
1226
|
+
.cp-topic-chip:hover {
|
|
1227
|
+
background: color-mix(in srgb, var(--accent) 10%, var(--surface));
|
|
1228
|
+
border-color: var(--accent);
|
|
1229
|
+
}
|
|
1230
|
+
.cp-topic-chip:focus-visible {
|
|
1231
|
+
outline: none;
|
|
1232
|
+
box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 35%, transparent);
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
.cp-topic-chip--active {
|
|
1236
|
+
background: var(--accent);
|
|
1237
|
+
color: var(--accent-fg);
|
|
1238
|
+
border-color: var(--accent);
|
|
1239
|
+
}
|
|
1240
|
+
.cp-topic-chip--active:hover {
|
|
1241
|
+
background: var(--accent-hover, var(--accent));
|
|
1242
|
+
color: var(--accent-fg);
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
.cp-topic-chip-count {
|
|
1246
|
+
display: inline-flex;
|
|
1247
|
+
align-items: center;
|
|
1248
|
+
justify-content: center;
|
|
1249
|
+
min-width: 18px;
|
|
1250
|
+
padding: 0 5px;
|
|
1251
|
+
background: color-mix(in srgb, currentColor 12%, transparent);
|
|
1252
|
+
border-radius: 999px;
|
|
1253
|
+
font-size: 10px;
|
|
1254
|
+
font-weight: 700;
|
|
1255
|
+
line-height: 1.6;
|
|
1256
|
+
letter-spacing: 0;
|
|
1257
|
+
}
|
|
1258
|
+
.cp-topic-chip--active .cp-topic-chip-count {
|
|
1259
|
+
background: color-mix(in srgb, var(--accent-fg) 20%, transparent);
|
|
1260
|
+
}
|
package/public/claude-plans.html
CHANGED
|
@@ -36,6 +36,27 @@
|
|
|
36
36
|
</div>
|
|
37
37
|
</header>
|
|
38
38
|
|
|
39
|
+
<details class="cp-topics" id="cp-topics" open>
|
|
40
|
+
<summary class="cp-topics-summary">
|
|
41
|
+
<span class="cp-topics-caret" aria-hidden="true">▾</span>
|
|
42
|
+
<span class="cp-topics-title">Topics</span>
|
|
43
|
+
<span class="cp-topics-count" id="cp-topics-count">0</span>
|
|
44
|
+
<button
|
|
45
|
+
type="button"
|
|
46
|
+
class="cp-topics-clear"
|
|
47
|
+
id="cp-topics-clear"
|
|
48
|
+
hidden
|
|
49
|
+
title="Clear topic filter"
|
|
50
|
+
aria-label="Clear topic filter"
|
|
51
|
+
>Clear filter</button>
|
|
52
|
+
</summary>
|
|
53
|
+
<div class="cp-topics-cloud" id="cp-topics-cloud" role="group" aria-label="Plan topics">
|
|
54
|
+
<span class="cp-topics-empty" id="cp-topics-empty">
|
|
55
|
+
No topics yet. Push a plan and topics will appear here.
|
|
56
|
+
</span>
|
|
57
|
+
</div>
|
|
58
|
+
</details>
|
|
59
|
+
|
|
39
60
|
<main class="cp-layout">
|
|
40
61
|
<aside class="cp-rail" id="cp-rail">
|
|
41
62
|
<div class="cp-rail-header">
|
|
@@ -83,11 +104,15 @@
|
|
|
83
104
|
</header>
|
|
84
105
|
<nav class="cp-tabs">
|
|
85
106
|
<button class="cp-tab active" data-tab="plan">Plan</button>
|
|
107
|
+
<button class="cp-tab" data-tab="prompts">
|
|
108
|
+
Prompt <span class="cp-tab-count" id="cp-prompt-count">0</span>
|
|
109
|
+
</button>
|
|
86
110
|
<button class="cp-tab" data-tab="artifacts">
|
|
87
111
|
Artifacts <span class="cp-tab-count" id="cp-artifact-count">0</span>
|
|
88
112
|
</button>
|
|
89
113
|
</nav>
|
|
90
114
|
<div class="cp-tab-panel" id="cp-tab-plan"></div>
|
|
115
|
+
<div class="cp-tab-panel" id="cp-tab-prompts" hidden></div>
|
|
91
116
|
<div class="cp-tab-panel" id="cp-tab-artifacts" hidden></div>
|
|
92
117
|
</article>
|
|
93
118
|
</section>
|
package/public/claude-plans.js
CHANGED
|
@@ -33,9 +33,11 @@
|
|
|
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
|
-
query: ""
|
|
39
|
+
query: "", // lowercased search query; "" means show all
|
|
40
|
+
topicFilter: null // active topic label, null = no topic filter
|
|
39
41
|
};
|
|
40
42
|
|
|
41
43
|
var els = {
|
|
@@ -53,11 +55,28 @@
|
|
|
53
55
|
detailStatus: document.getElementById("cp-detail-status"),
|
|
54
56
|
detailStamp: document.getElementById("cp-detail-stamp"),
|
|
55
57
|
artifactCount: document.getElementById("cp-artifact-count"),
|
|
58
|
+
promptCount: document.getElementById("cp-prompt-count"),
|
|
56
59
|
planPanel: document.getElementById("cp-tab-plan"),
|
|
57
60
|
artifactsPanel: document.getElementById("cp-tab-artifacts"),
|
|
58
|
-
|
|
61
|
+
promptsPanel: document.getElementById("cp-tab-prompts"),
|
|
62
|
+
tabs: document.querySelectorAll(".cp-tab"),
|
|
63
|
+
topics: document.getElementById("cp-topics"),
|
|
64
|
+
topicsCloud: document.getElementById("cp-topics-cloud"),
|
|
65
|
+
topicsCount: document.getElementById("cp-topics-count"),
|
|
66
|
+
topicsEmpty: document.getElementById("cp-topics-empty"),
|
|
67
|
+
topicsClear: document.getElementById("cp-topics-clear")
|
|
59
68
|
};
|
|
60
69
|
|
|
70
|
+
function planMatchesTopic(plan, topic) {
|
|
71
|
+
if (!topic) return true;
|
|
72
|
+
var cats = plan.categories;
|
|
73
|
+
if (!cats || !cats.length) return false;
|
|
74
|
+
for (var i = 0; i < cats.length; i++) {
|
|
75
|
+
if (cats[i] === topic) return true;
|
|
76
|
+
}
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
|
|
61
80
|
// True when plan matches q across title, slug, status, plan markdown/html,
|
|
62
81
|
// pr title, and every artifact title + content. Lets you find a plan by any
|
|
63
82
|
// snippet that appears anywhere in or attached to it.
|
|
@@ -93,9 +112,121 @@
|
|
|
93
112
|
|
|
94
113
|
function sortedPlans() {
|
|
95
114
|
var q = state.query;
|
|
115
|
+
var topic = state.topicFilter;
|
|
96
116
|
var all = allPlansSorted();
|
|
97
|
-
if (!q) return all;
|
|
98
|
-
return all.filter(function (p) {
|
|
117
|
+
if (!q && !topic) return all;
|
|
118
|
+
return all.filter(function (p) {
|
|
119
|
+
return planMatchesQuery(p, q) && planMatchesTopic(p, topic);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/* ─── Topics aggregation + render ──────────────────────────────────────────
|
|
124
|
+
* Counts topic occurrences across all plans (not just filtered ones) so the
|
|
125
|
+
* cloud reflects the full corpus. Click a chip to filter the rail by that
|
|
126
|
+
* topic; click the active chip again to clear.
|
|
127
|
+
*
|
|
128
|
+
* Counts are computed client-side from plan.categories arrays — no new
|
|
129
|
+
* server endpoint or SSE event. The existing plan:upsert stream rerenders
|
|
130
|
+
* the cloud automatically because renderRail() now also calls renderTopics().
|
|
131
|
+
*/
|
|
132
|
+
function aggregateTopics() {
|
|
133
|
+
var counts = new Map();
|
|
134
|
+
state.plans.forEach(function (plan) {
|
|
135
|
+
var cats = plan.categories;
|
|
136
|
+
if (!cats || !cats.length) return;
|
|
137
|
+
for (var i = 0; i < cats.length; i++) {
|
|
138
|
+
var label = cats[i];
|
|
139
|
+
counts.set(label, (counts.get(label) || 0) + 1);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
var rows = [];
|
|
143
|
+
counts.forEach(function (count, label) {
|
|
144
|
+
rows.push({ label: label, count: count });
|
|
145
|
+
});
|
|
146
|
+
rows.sort(function (a, b) {
|
|
147
|
+
if (b.count !== a.count) return b.count - a.count;
|
|
148
|
+
return a.label.localeCompare(b.label);
|
|
149
|
+
});
|
|
150
|
+
return rows;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// sqrt compression keeps a 30-count topic from dwarfing a 2-count one.
|
|
154
|
+
function topicFontSize(count, max) {
|
|
155
|
+
if (max <= 1) return 14;
|
|
156
|
+
var ratio = Math.sqrt(count) / Math.sqrt(max);
|
|
157
|
+
return Math.round(12 + ratio * 12);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function renderTopics() {
|
|
161
|
+
if (!els.topicsCloud) return;
|
|
162
|
+
var rows = aggregateTopics();
|
|
163
|
+
if (els.topicsCount) els.topicsCount.textContent = String(rows.length);
|
|
164
|
+
if (els.topicsClear) els.topicsClear.hidden = !state.topicFilter;
|
|
165
|
+
if (rows.length === 0) {
|
|
166
|
+
els.topicsCloud.innerHTML = "";
|
|
167
|
+
if (els.topicsEmpty) {
|
|
168
|
+
els.topicsCloud.appendChild(els.topicsEmpty);
|
|
169
|
+
els.topicsEmpty.style.display = "inline";
|
|
170
|
+
}
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (els.topicsEmpty) els.topicsEmpty.style.display = "none";
|
|
174
|
+
els.topicsCloud.innerHTML = "";
|
|
175
|
+
var max = rows[0].count;
|
|
176
|
+
rows.forEach(function (row) {
|
|
177
|
+
var chip = document.createElement("button");
|
|
178
|
+
chip.type = "button";
|
|
179
|
+
var isActive = state.topicFilter === row.label;
|
|
180
|
+
chip.className = "cp-topic-chip" + (isActive ? " cp-topic-chip--active" : "");
|
|
181
|
+
chip.style.fontSize = topicFontSize(row.count, max) + "px";
|
|
182
|
+
chip.setAttribute("aria-pressed", isActive ? "true" : "false");
|
|
183
|
+
chip.title = row.count + " plan" + (row.count === 1 ? "" : "s");
|
|
184
|
+
var labelEl = document.createElement("span");
|
|
185
|
+
labelEl.className = "cp-topic-chip-label";
|
|
186
|
+
labelEl.textContent = row.label;
|
|
187
|
+
var countEl = document.createElement("span");
|
|
188
|
+
countEl.className = "cp-topic-chip-count";
|
|
189
|
+
countEl.textContent = String(row.count);
|
|
190
|
+
chip.appendChild(labelEl);
|
|
191
|
+
chip.appendChild(countEl);
|
|
192
|
+
chip.addEventListener("click", function () {
|
|
193
|
+
applyTopicFilter(state.topicFilter === row.label ? null : row.label);
|
|
194
|
+
});
|
|
195
|
+
els.topicsCloud.appendChild(chip);
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function applyTopicFilter(label) {
|
|
200
|
+
state.topicFilter = label;
|
|
201
|
+
renderTopics();
|
|
202
|
+
renderRail();
|
|
203
|
+
if (label) {
|
|
204
|
+
var visible = sortedPlans();
|
|
205
|
+
var stillVisible = state.selectedSlug &&
|
|
206
|
+
visible.some(function (p) { return p.slug === state.selectedSlug; });
|
|
207
|
+
if (!stillVisible && visible.length > 0) selectPlan(visible[0].slug);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function setupTopics() {
|
|
212
|
+
if (els.topicsClear) {
|
|
213
|
+
els.topicsClear.addEventListener("click", function (e) {
|
|
214
|
+
e.preventDefault();
|
|
215
|
+
e.stopPropagation();
|
|
216
|
+
applyTopicFilter(null);
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
if (els.topics) {
|
|
220
|
+
try {
|
|
221
|
+
var stored = localStorage.getItem("cp-topics-collapsed");
|
|
222
|
+
if (stored === "1") els.topics.open = false;
|
|
223
|
+
} catch (_) {}
|
|
224
|
+
els.topics.addEventListener("toggle", function () {
|
|
225
|
+
try {
|
|
226
|
+
localStorage.setItem("cp-topics-collapsed", els.topics.open ? "0" : "1");
|
|
227
|
+
} catch (_) {}
|
|
228
|
+
});
|
|
229
|
+
}
|
|
99
230
|
}
|
|
100
231
|
|
|
101
232
|
function sortedArtifacts(slug) {
|
|
@@ -106,6 +237,14 @@
|
|
|
106
237
|
});
|
|
107
238
|
}
|
|
108
239
|
|
|
240
|
+
function sortedPrompts(slug) {
|
|
241
|
+
var map = state.prompts.get(slug);
|
|
242
|
+
if (!map) return [];
|
|
243
|
+
return Array.from(map.values()).sort(function (a, b) {
|
|
244
|
+
return (a.created_at || "").localeCompare(b.created_at || "");
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
109
248
|
function fmtTime(iso) {
|
|
110
249
|
if (!iso) return "";
|
|
111
250
|
var d = new Date(iso);
|
|
@@ -241,7 +380,7 @@
|
|
|
241
380
|
return btn;
|
|
242
381
|
}
|
|
243
382
|
|
|
244
|
-
function addTabsCopyAll(plan, artifacts) {
|
|
383
|
+
function addTabsCopyAll(plan, artifacts, prompts) {
|
|
245
384
|
var existing = document.getElementById("cp-tabs-copy-all");
|
|
246
385
|
if (existing) existing.remove();
|
|
247
386
|
|
|
@@ -251,6 +390,11 @@
|
|
|
251
390
|
return "# " + a.title + "\n\n" + a.content;
|
|
252
391
|
}).join("\n\n---\n\n");
|
|
253
392
|
}
|
|
393
|
+
if (state.activeTab === "prompts") {
|
|
394
|
+
return prompts.map(function (p) {
|
|
395
|
+
return "# " + p.title + "\n\n" + p.content;
|
|
396
|
+
}).join("\n\n---\n\n");
|
|
397
|
+
}
|
|
254
398
|
return plan.content_md && plan.content_md.trim()
|
|
255
399
|
? plan.content_md
|
|
256
400
|
: els.planPanel.innerText.trim();
|
|
@@ -280,9 +424,10 @@
|
|
|
280
424
|
/* ─────────────────────────────────────────────────────────────────────────── */
|
|
281
425
|
|
|
282
426
|
function renderRail() {
|
|
427
|
+
renderTopics();
|
|
283
428
|
var plans = sortedPlans();
|
|
284
429
|
var totalPlans = state.plans.size;
|
|
285
|
-
var isFiltering = !!state.query;
|
|
430
|
+
var isFiltering = !!state.query || !!state.topicFilter;
|
|
286
431
|
els.count.textContent = isFiltering
|
|
287
432
|
? plans.length + " / " + totalPlans
|
|
288
433
|
: String(totalPlans);
|
|
@@ -291,7 +436,12 @@
|
|
|
291
436
|
if (isFiltering && totalPlans > 0) {
|
|
292
437
|
els.railEmpty.style.display = "none";
|
|
293
438
|
els.railNoMatch.hidden = false;
|
|
294
|
-
if (els.railNoMatchQ)
|
|
439
|
+
if (els.railNoMatchQ) {
|
|
440
|
+
var pieces = [];
|
|
441
|
+
if (state.query) pieces.push('"' + state.query + '"');
|
|
442
|
+
if (state.topicFilter) pieces.push("topic “" + state.topicFilter + "”");
|
|
443
|
+
els.railNoMatchQ.textContent = pieces.join(" + ");
|
|
444
|
+
}
|
|
295
445
|
} else {
|
|
296
446
|
els.railEmpty.style.display = "block";
|
|
297
447
|
els.railNoMatch.hidden = true;
|
|
@@ -363,6 +513,7 @@
|
|
|
363
513
|
}
|
|
364
514
|
var plan = state.plans.get(state.selectedSlug);
|
|
365
515
|
var artifacts = sortedArtifacts(state.selectedSlug);
|
|
516
|
+
var prompts = sortedPrompts(state.selectedSlug);
|
|
366
517
|
|
|
367
518
|
els.detailEmpty.style.display = "none";
|
|
368
519
|
els.detailBody.hidden = false;
|
|
@@ -371,6 +522,7 @@
|
|
|
371
522
|
els.detailStatus.className = "cp-status-pill cp-status-" + plan.status;
|
|
372
523
|
els.detailStamp.textContent = "updated " + fmtTime(plan.updated_at);
|
|
373
524
|
els.artifactCount.textContent = String(artifacts.length);
|
|
525
|
+
if (els.promptCount) els.promptCount.textContent = String(prompts.length);
|
|
374
526
|
|
|
375
527
|
var existingPrBadge = document.getElementById("cp-pr-badge");
|
|
376
528
|
if (existingPrBadge) existingPrBadge.remove();
|
|
@@ -409,6 +561,28 @@
|
|
|
409
561
|
});
|
|
410
562
|
els.detailStatus.insertAdjacentElement("afterend", resumeBtn);
|
|
411
563
|
|
|
564
|
+
var existingPromptBtn = document.getElementById("cp-gen-prompt-btn");
|
|
565
|
+
if (existingPromptBtn) existingPromptBtn.remove();
|
|
566
|
+
var genPromptBtn = document.createElement("button");
|
|
567
|
+
genPromptBtn.id = "cp-gen-prompt-btn";
|
|
568
|
+
genPromptBtn.className = "cp-resume-btn";
|
|
569
|
+
genPromptBtn.textContent = "Generate Prompt";
|
|
570
|
+
genPromptBtn.title = "Copy /improve-prompt --from-plan <slug> to clipboard";
|
|
571
|
+
genPromptBtn.addEventListener("click", function () {
|
|
572
|
+
var cmd = "/improve-prompt --from-plan " + plan.slug;
|
|
573
|
+
var finish = function () { showToast("Copied — paste into Claude Code."); };
|
|
574
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
575
|
+
navigator.clipboard.writeText(cmd).then(finish).catch(function () {
|
|
576
|
+
fallbackCopy(cmd);
|
|
577
|
+
finish();
|
|
578
|
+
});
|
|
579
|
+
} else {
|
|
580
|
+
fallbackCopy(cmd);
|
|
581
|
+
finish();
|
|
582
|
+
}
|
|
583
|
+
});
|
|
584
|
+
resumeBtn.insertAdjacentElement("afterend", genPromptBtn);
|
|
585
|
+
|
|
412
586
|
if (plan.content_html) {
|
|
413
587
|
els.planPanel.innerHTML = window.DOMPurify
|
|
414
588
|
? window.DOMPurify.sanitize(plan.content_html)
|
|
@@ -418,7 +592,7 @@
|
|
|
418
592
|
}
|
|
419
593
|
|
|
420
594
|
addPlanSectionCopyBtns();
|
|
421
|
-
addTabsCopyAll(plan, artifacts);
|
|
595
|
+
addTabsCopyAll(plan, artifacts, prompts);
|
|
422
596
|
|
|
423
597
|
els.artifactsPanel.innerHTML = "";
|
|
424
598
|
if (artifacts.length === 0) {
|
|
@@ -458,6 +632,69 @@
|
|
|
458
632
|
els.artifactsPanel.appendChild(card);
|
|
459
633
|
});
|
|
460
634
|
}
|
|
635
|
+
|
|
636
|
+
els.promptsPanel.innerHTML = "";
|
|
637
|
+
if (prompts.length === 0) {
|
|
638
|
+
var emptyPrompts = document.createElement("div");
|
|
639
|
+
emptyPrompts.className = "cp-detail-empty";
|
|
640
|
+
emptyPrompts.style.padding = "40px 0";
|
|
641
|
+
emptyPrompts.textContent = 'No prompts yet. Click "Generate Prompt" above to seed one, or push from /improve-prompt --push.';
|
|
642
|
+
els.promptsPanel.appendChild(emptyPrompts);
|
|
643
|
+
} else {
|
|
644
|
+
prompts.forEach(function (prompt) {
|
|
645
|
+
var card = document.createElement("div");
|
|
646
|
+
card.className = "cp-artifact-card cp-prompt-card";
|
|
647
|
+
var head = document.createElement("div");
|
|
648
|
+
head.className = "cp-artifact-head";
|
|
649
|
+
var title = document.createElement("span");
|
|
650
|
+
title.className = "cp-artifact-title";
|
|
651
|
+
title.textContent = prompt.title;
|
|
652
|
+
head.appendChild(title);
|
|
653
|
+
|
|
654
|
+
if (typeof prompt.score_before === "number" && typeof prompt.score_after === "number") {
|
|
655
|
+
var scorePill = document.createElement("span");
|
|
656
|
+
scorePill.className = "cp-kind-pill cp-prompt-score";
|
|
657
|
+
scorePill.textContent = prompt.score_before + " → " + prompt.score_after + "%";
|
|
658
|
+
head.appendChild(scorePill);
|
|
659
|
+
} else if (typeof prompt.score_after === "number") {
|
|
660
|
+
var scorePillOnly = document.createElement("span");
|
|
661
|
+
scorePillOnly.className = "cp-kind-pill cp-prompt-score";
|
|
662
|
+
scorePillOnly.textContent = prompt.score_after + "%";
|
|
663
|
+
head.appendChild(scorePillOnly);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
var copyGroup = document.createElement("div");
|
|
667
|
+
copyGroup.className = "cp-copy-btn-group cp-copy-btn-group--artifact";
|
|
668
|
+
copyGroup.appendChild(makeCopyBtn("Copy", (function (content) {
|
|
669
|
+
return function () { return content; };
|
|
670
|
+
})(prompt.content)));
|
|
671
|
+
head.appendChild(copyGroup);
|
|
672
|
+
|
|
673
|
+
card.appendChild(head);
|
|
674
|
+
|
|
675
|
+
if (prompt.source_draft) {
|
|
676
|
+
var details = document.createElement("details");
|
|
677
|
+
details.className = "cp-prompt-source";
|
|
678
|
+
var summary = document.createElement("summary");
|
|
679
|
+
summary.textContent = "Original draft";
|
|
680
|
+
details.appendChild(summary);
|
|
681
|
+
var pre = document.createElement("pre");
|
|
682
|
+
var code = document.createElement("code");
|
|
683
|
+
code.textContent = prompt.source_draft;
|
|
684
|
+
pre.appendChild(code);
|
|
685
|
+
details.appendChild(pre);
|
|
686
|
+
card.appendChild(details);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
var body = document.createElement("pre");
|
|
690
|
+
body.className = "cp-prompt-body";
|
|
691
|
+
var bodyCode = document.createElement("code");
|
|
692
|
+
bodyCode.textContent = prompt.content;
|
|
693
|
+
body.appendChild(bodyCode);
|
|
694
|
+
card.appendChild(body);
|
|
695
|
+
els.promptsPanel.appendChild(card);
|
|
696
|
+
});
|
|
697
|
+
}
|
|
461
698
|
}
|
|
462
699
|
|
|
463
700
|
function setActiveTab(tab) {
|
|
@@ -467,6 +704,7 @@
|
|
|
467
704
|
});
|
|
468
705
|
els.planPanel.hidden = tab !== "plan";
|
|
469
706
|
els.artifactsPanel.hidden = tab !== "artifacts";
|
|
707
|
+
if (els.promptsPanel) els.promptsPanel.hidden = tab !== "prompts";
|
|
470
708
|
}
|
|
471
709
|
|
|
472
710
|
function selectPlan(slug) {
|
|
@@ -551,6 +789,7 @@
|
|
|
551
789
|
function removePlan(slug) {
|
|
552
790
|
state.plans.delete(slug);
|
|
553
791
|
state.artifacts.delete(slug);
|
|
792
|
+
state.prompts.delete(slug);
|
|
554
793
|
if (state.selectedSlug === slug) state.selectedSlug = null;
|
|
555
794
|
renderRail();
|
|
556
795
|
renderDetail();
|
|
@@ -575,6 +814,23 @@
|
|
|
575
814
|
if (state.selectedSlug === planSlug) renderDetail();
|
|
576
815
|
}
|
|
577
816
|
|
|
817
|
+
function upsertPrompt(prompt) {
|
|
818
|
+
var bucket = state.prompts.get(prompt.plan_slug);
|
|
819
|
+
if (!bucket) {
|
|
820
|
+
bucket = new Map();
|
|
821
|
+
state.prompts.set(prompt.plan_slug, bucket);
|
|
822
|
+
}
|
|
823
|
+
bucket.set(prompt.slug, prompt);
|
|
824
|
+
if (state.selectedSlug === prompt.plan_slug) renderDetail();
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
function removePrompt(planSlug, slug) {
|
|
828
|
+
var bucket = state.prompts.get(planSlug);
|
|
829
|
+
if (!bucket) return;
|
|
830
|
+
bucket.delete(slug);
|
|
831
|
+
if (state.selectedSlug === planSlug) renderDetail();
|
|
832
|
+
}
|
|
833
|
+
|
|
578
834
|
async function loadInitial() {
|
|
579
835
|
try {
|
|
580
836
|
var res = await fetch("/api/claude-plans");
|
|
@@ -586,7 +842,7 @@
|
|
|
586
842
|
} catch (err) {
|
|
587
843
|
console.warn("[claude-plans] failed to load initial state:", err);
|
|
588
844
|
}
|
|
589
|
-
// Preload artifacts for visible plans
|
|
845
|
+
// Preload artifacts + prompts for visible plans
|
|
590
846
|
var slugs = Array.from(state.plans.keys());
|
|
591
847
|
await Promise.all(slugs.map(function (slug) {
|
|
592
848
|
return fetch("/api/claude-plans/" + encodeURIComponent(slug))
|
|
@@ -597,6 +853,11 @@
|
|
|
597
853
|
data.artifacts.forEach(function (a) { bucket.set(a.slug, a); });
|
|
598
854
|
state.artifacts.set(slug, bucket);
|
|
599
855
|
}
|
|
856
|
+
if (data && Array.isArray(data.prompts)) {
|
|
857
|
+
var pBucket = new Map();
|
|
858
|
+
data.prompts.forEach(function (p) { pBucket.set(p.slug, p); });
|
|
859
|
+
state.prompts.set(slug, pBucket);
|
|
860
|
+
}
|
|
600
861
|
})
|
|
601
862
|
.catch(function () { /* ignore */ });
|
|
602
863
|
}));
|
|
@@ -624,6 +885,15 @@
|
|
|
624
885
|
removeArtifact(payload.plan_slug, payload.slug);
|
|
625
886
|
} catch (_) {}
|
|
626
887
|
});
|
|
888
|
+
es.addEventListener("prompt:upsert", function (e) {
|
|
889
|
+
try { upsertPrompt(JSON.parse(e.data).prompt); } catch (_) {}
|
|
890
|
+
});
|
|
891
|
+
es.addEventListener("prompt:delete", function (e) {
|
|
892
|
+
try {
|
|
893
|
+
var payload = JSON.parse(e.data);
|
|
894
|
+
removePrompt(payload.plan_slug, payload.slug);
|
|
895
|
+
} catch (_) {}
|
|
896
|
+
});
|
|
627
897
|
es.addEventListener("plan:focus", function (e) {
|
|
628
898
|
try {
|
|
629
899
|
var data = JSON.parse(e.data);
|
|
@@ -664,6 +934,7 @@
|
|
|
664
934
|
document.addEventListener("DOMContentLoaded", function () {
|
|
665
935
|
setupTheme();
|
|
666
936
|
setupSearch();
|
|
937
|
+
setupTopics();
|
|
667
938
|
setActiveTab("plan");
|
|
668
939
|
loadInitial().then(startStream);
|
|
669
940
|
});
|
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
|
}
|