@tekyzinc/gsd-t 3.18.11 → 3.18.13
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/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to GSD-T are documented here. Updated with each release.
|
|
4
4
|
|
|
5
|
+
## [3.18.13] - 2026-04-23
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **Dashboard `/transcripts` returned raw JSON to browsers** — after the v3.18.12 always-enabled Live Stream button fix, opening the dashboard with no spawn data and clicking Live Stream landed the user on `{"spawns":[]}` because `/transcripts` always served JSON. The route now does Accept-header content negotiation: browsers (`Accept: text/html`) get a proper dark-themed HTML index page with a sortable table of spawns (or a friendly empty state with a `/gsd-t-quick` CTA when no transcripts exist); programmatic clients (`fetch()` default `*/*`, or explicit `application/json`) keep getting the JSON shape the dashboard polling code already consumes — full back-compat.
|
|
10
|
+
|
|
11
|
+
## [3.18.12] - 2026-04-23
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- **Dashboard Live Stream button stuck disabled** — the header button had `cursor:not-allowed` + `pointer-events:none` whenever the `/transcripts` index returned no spawns, including the common case of opening the dashboard before any agent had run. The button now stays enabled in all states. With a live spawn it links to the live transcript; with only finished spawns it links to the most recent one; with no spawn data at all it links to the `/transcripts` JSON index as a discoverable last resort.
|
|
16
|
+
|
|
5
17
|
## [3.18.11] - 2026-04-23
|
|
6
18
|
|
|
7
19
|
### Fixed
|
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tekyzinc/gsd-t",
|
|
3
|
-
"version": "3.18.
|
|
3
|
+
"version": "3.18.13",
|
|
4
4
|
"description": "GSD-T: Contract-Driven Development for Claude Code — 54 slash commands with headless-by-default workflow spawning, unattended supervisor relay with event stream, graph-powered code analysis, real-time agent dashboard, task telemetry, doc-ripple enforcement, backlog management, impact analysis, test sync, milestone archival, and PRD generation",
|
|
5
5
|
"author": "Tekyz, Inc.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -181,10 +181,97 @@ function handleTranscriptsList(req, res, projectDir) {
|
|
|
181
181
|
const sorted = idx.spawns
|
|
182
182
|
.slice()
|
|
183
183
|
.sort((a, b) => (Date.parse(b.startedAt) || 0) - (Date.parse(a.startedAt) || 0));
|
|
184
|
+
|
|
185
|
+
// Content negotiation: browser navigations send Accept: text/html, fetch()
|
|
186
|
+
// defaults to */*. We serve HTML only when the client explicitly asks for it,
|
|
187
|
+
// so the existing dashboard fetch (which expects JSON) stays unaffected.
|
|
188
|
+
const accept = String(req.headers["accept"] || "");
|
|
189
|
+
if (accept.includes("text/html")) {
|
|
190
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
191
|
+
res.end(renderTranscriptsHtml(sorted));
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
184
194
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
185
195
|
res.end(JSON.stringify({ spawns: sorted }));
|
|
186
196
|
}
|
|
187
197
|
|
|
198
|
+
function renderTranscriptsHtml(spawns) {
|
|
199
|
+
const escape = (s) => String(s == null ? "" : s)
|
|
200
|
+
.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")
|
|
201
|
+
.replace(/"/g, """);
|
|
202
|
+
const fmtDuration = (a, b) => {
|
|
203
|
+
const start = Date.parse(a); const end = Date.parse(b || "") || Date.now();
|
|
204
|
+
if (!Number.isFinite(start)) return "—";
|
|
205
|
+
const ms = Math.max(0, end - start);
|
|
206
|
+
const s = Math.floor(ms / 1000); const m = Math.floor(s / 60); const r = s % 60;
|
|
207
|
+
return m > 0 ? `${m}m ${r}s` : `${r}s`;
|
|
208
|
+
};
|
|
209
|
+
const fmtTime = (s) => { const d = new Date(s); return Number.isFinite(d.getTime()) ? d.toLocaleString() : "—"; };
|
|
210
|
+
const liveStatuses = new Set(["initializing", "running"]);
|
|
211
|
+
const isLive = (s) => liveStatuses.has(s);
|
|
212
|
+
|
|
213
|
+
const rows = spawns.map((s) => {
|
|
214
|
+
const live = isLive(s.status);
|
|
215
|
+
const statusBadge = `<span class="status status-${escape(s.status || 'unknown')}">${escape(s.status || 'unknown')}</span>`;
|
|
216
|
+
return `<tr class="${live ? 'row-live' : ''}">
|
|
217
|
+
<td><a href="/transcript/${encodeURIComponent(s.spawnId)}">${escape(s.spawnId)}</a></td>
|
|
218
|
+
<td>${escape(s.command || '—')}</td>
|
|
219
|
+
<td>${escape(s.description || '—')}</td>
|
|
220
|
+
<td>${statusBadge}</td>
|
|
221
|
+
<td>${escape(fmtTime(s.startedAt))}</td>
|
|
222
|
+
<td>${escape(fmtDuration(s.startedAt, s.endedAt))}</td>
|
|
223
|
+
</tr>`;
|
|
224
|
+
}).join("");
|
|
225
|
+
|
|
226
|
+
const empty = `<div class="empty">
|
|
227
|
+
<h2>No spawn transcripts yet</h2>
|
|
228
|
+
<p>Transcripts appear here as soon as the first agent spawns. Run any GSD-T command (for example <code>/gsd-t-quick</code>) to generate one.</p>
|
|
229
|
+
<p><a href="/">← Back to dashboard</a></p>
|
|
230
|
+
</div>`;
|
|
231
|
+
|
|
232
|
+
const table = spawns.length ? `<table>
|
|
233
|
+
<thead><tr><th>Spawn ID</th><th>Command</th><th>Description</th><th>Status</th><th>Started</th><th>Duration</th></tr></thead>
|
|
234
|
+
<tbody>${rows}</tbody>
|
|
235
|
+
</table>` : empty;
|
|
236
|
+
|
|
237
|
+
return `<!DOCTYPE html>
|
|
238
|
+
<html lang="en"><head><meta charset="UTF-8"><title>GSD-T Transcripts</title>
|
|
239
|
+
<style>
|
|
240
|
+
:root{--bg:#0d1117;--surface:#161b22;--border:#30363d;--text:#e6edf3;--muted:#7d8590;
|
|
241
|
+
--green:#3fb950;--green-bg:#1a3a1e;--blue:#388bfd;--blue-bg:#1f3a5f;--yellow:#d29922;--red:#f85149;
|
|
242
|
+
--font:'SF Mono','Fira Code',Menlo,monospace;}
|
|
243
|
+
*{box-sizing:border-box;margin:0;padding:0}
|
|
244
|
+
body{background:var(--bg);color:var(--text);font-family:var(--font);font-size:13px;padding:20px;line-height:1.5}
|
|
245
|
+
.hdr{display:flex;align-items:center;gap:12px;margin-bottom:18px;padding-bottom:12px;border-bottom:1px solid var(--border)}
|
|
246
|
+
.logo{color:var(--blue);font-weight:bold;font-size:14px}
|
|
247
|
+
.hright{margin-left:auto;color:var(--muted);font-size:11px}
|
|
248
|
+
a{color:var(--blue);text-decoration:none}a:hover{text-decoration:underline}
|
|
249
|
+
table{width:100%;border-collapse:collapse;background:var(--surface);border:1px solid var(--border);border-radius:6px;overflow:hidden}
|
|
250
|
+
th,td{padding:8px 12px;text-align:left;border-bottom:1px solid var(--border);font-size:12px}
|
|
251
|
+
th{background:#1c2128;color:var(--muted);text-transform:uppercase;font-size:10px;letter-spacing:0.5px}
|
|
252
|
+
tbody tr:last-child td{border-bottom:none}
|
|
253
|
+
tbody tr:hover{background:#1c2128}
|
|
254
|
+
tr.row-live{background:var(--green-bg)}
|
|
255
|
+
.status{display:inline-block;padding:2px 8px;border-radius:10px;font-size:10px;text-transform:uppercase;letter-spacing:0.5px;border:1px solid}
|
|
256
|
+
.status-running,.status-initializing{background:var(--green-bg);color:var(--green);border-color:var(--green)}
|
|
257
|
+
.status-done{background:var(--blue-bg);color:var(--blue);border-color:var(--blue)}
|
|
258
|
+
.status-stopped{background:#2a2a2a;color:var(--muted);border-color:var(--border)}
|
|
259
|
+
.status-failed,.status-crashed{background:#3a1a1a;color:var(--red);border-color:var(--red)}
|
|
260
|
+
.status-unknown{color:var(--muted);border-color:var(--border)}
|
|
261
|
+
.empty{text-align:center;padding:60px 20px;color:var(--muted);background:var(--surface);border:1px solid var(--border);border-radius:6px}
|
|
262
|
+
.empty h2{color:var(--text);margin-bottom:12px;font-size:16px}
|
|
263
|
+
.empty p{margin-bottom:8px}
|
|
264
|
+
.empty code{background:#1c2128;padding:2px 6px;border-radius:3px;color:var(--green)}
|
|
265
|
+
</style></head><body>
|
|
266
|
+
<div class="hdr">
|
|
267
|
+
<span class="logo">GSD-T Transcripts</span>
|
|
268
|
+
<a href="/" style="font-size:11px">← Dashboard</a>
|
|
269
|
+
<span class="hright">${spawns.length} spawn${spawns.length === 1 ? '' : 's'}</span>
|
|
270
|
+
</div>
|
|
271
|
+
${table}
|
|
272
|
+
</body></html>`;
|
|
273
|
+
}
|
|
274
|
+
|
|
188
275
|
function handleTranscriptPage(req, res, spawnId, transcriptHtmlPath) {
|
|
189
276
|
if (!isValidSpawnId(spawnId)) { res.writeHead(400); res.end("Invalid spawn id"); return; }
|
|
190
277
|
fs.readFile(transcriptHtmlPath, (err, data) => {
|
|
@@ -587,6 +674,7 @@ module.exports = {
|
|
|
587
674
|
readIndexEntry,
|
|
588
675
|
isValidSpawnId,
|
|
589
676
|
handleTranscriptsList,
|
|
677
|
+
renderTranscriptsHtml,
|
|
590
678
|
handleTranscriptStream,
|
|
591
679
|
handleTranscriptPage,
|
|
592
680
|
handleTranscriptKill,
|
|
@@ -71,7 +71,7 @@ body{background:var(--bg);color:var(--text);font-family:var(--font);font-size:12
|
|
|
71
71
|
<div class="hdr">
|
|
72
72
|
<span class="logo">GSD-T Agent Dashboard</span>
|
|
73
73
|
<div id="status" class="status wait"><span class="dot"></span><span id="status-txt">Connecting...</span></div>
|
|
74
|
-
<a id="livestream-btn" class="livestream-btn
|
|
74
|
+
<a id="livestream-btn" class="livestream-btn" href="/transcripts" title="Open the latest live spawn transcript">▶ Live Stream</a>
|
|
75
75
|
<div class="hright"><span id="ev-count">0 events</span></div>
|
|
76
76
|
</div>
|
|
77
77
|
<div class="main">
|
|
@@ -275,13 +275,13 @@ ReactDOM.render(React.createElement(Dashboard),document.getElementById('rf-root'
|
|
|
275
275
|
function refresh(){
|
|
276
276
|
fetch(`http://localhost:${PORT}/transcripts`,{cache:'no-store'}).then(r=>r.ok?r.json():null).then(d=>{
|
|
277
277
|
const latest=d&&pickLatest(d.spawns);
|
|
278
|
+
btn.classList.remove('disabled');
|
|
278
279
|
if(latest){
|
|
279
280
|
btn.href=`/transcript/${encodeURIComponent(latest.spawnId)}`;
|
|
280
|
-
btn.classList.remove('disabled');
|
|
281
281
|
const live=latest.status&&!['done','stopped','failed','crashed'].includes(latest.status);
|
|
282
282
|
btn.textContent=(live?'▶ Live Stream':'▶ Latest Transcript')+` · ${latest.spawnId.slice(0,10)}`;
|
|
283
283
|
}else{
|
|
284
|
-
btn.href='
|
|
284
|
+
btn.href='/transcripts';btn.textContent='▶ Live Stream (no spawns yet)';
|
|
285
285
|
}
|
|
286
286
|
}).catch(()=>{});
|
|
287
287
|
}
|