@tekyzinc/gsd-t 3.15.10 → 3.16.11
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/bin/gsd-t-orchestrator-worker.cjs +35 -3
- package/bin/gsd-t-token-capture.cjs +24 -3
- package/bin/gsd-t-token-regenerate-log.cjs +129 -0
- package/bin/gsd-t-transcript-tee.cjs +246 -0
- package/bin/gsd-t-unattended-heartbeat.cjs +188 -0
- package/bin/gsd-t-unattended-platform.cjs +191 -27
- package/bin/gsd-t-unattended-safety.cjs +8 -1
- package/bin/gsd-t-unattended.cjs +192 -31
- package/bin/gsd-t.js +15 -1
- package/bin/supervisor-pid-fingerprint.cjs +126 -0
- package/commands/gsd-t-resume.md +18 -4
- package/docs/architecture.md +16 -0
- package/package.json +1 -1
- package/scripts/gsd-t-dashboard-server.js +291 -4
- package/scripts/gsd-t-dashboard.html +31 -1
- package/scripts/gsd-t-transcript.html +422 -0
- package/scripts/hooks/gsd-t-in-session-probe.js +62 -0
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>GSD-T Transcript</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
--bg: #0d1117;
|
|
10
|
+
--bg-raised: #161b22;
|
|
11
|
+
--bg-soft: #1f2937;
|
|
12
|
+
--fg: #c9d1d9;
|
|
13
|
+
--fg-dim: #8b949e;
|
|
14
|
+
--fg-xdim: #6e7681;
|
|
15
|
+
--accent: #58a6ff;
|
|
16
|
+
--accent-warm: #f0883e;
|
|
17
|
+
--border: #30363d;
|
|
18
|
+
--green: #3fb950;
|
|
19
|
+
--yellow: #d29922;
|
|
20
|
+
--red: #f85149;
|
|
21
|
+
--mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
22
|
+
--sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
|
23
|
+
}
|
|
24
|
+
* { box-sizing: border-box; }
|
|
25
|
+
body { margin: 0; background: var(--bg); color: var(--fg); font-family: var(--sans); font-size: 14px; line-height: 1.55; display: grid; grid-template-columns: 280px 1fr; grid-template-rows: auto 1fr; min-height: 100vh; }
|
|
26
|
+
body > header { grid-column: 1 / -1; }
|
|
27
|
+
body > aside { grid-column: 1; grid-row: 2; background: var(--bg-raised); border-right: 1px solid var(--border); padding: 12px 0; overflow-y: auto; max-height: calc(100vh - 50px); position: sticky; top: 49px; align-self: start; }
|
|
28
|
+
body > main { grid-column: 2; grid-row: 2; }
|
|
29
|
+
aside h3 { margin: 0 12px 8px 12px; font-size: 11px; text-transform: uppercase; color: var(--fg-xdim); letter-spacing: 0.08em; }
|
|
30
|
+
aside .tree { font-size: 13px; }
|
|
31
|
+
aside .node { display: flex; align-items: center; padding: 5px 12px; cursor: pointer; gap: 6px; }
|
|
32
|
+
aside .node:hover { background: var(--bg-soft); }
|
|
33
|
+
aside .node.active { background: var(--bg-soft); border-left: 2px solid var(--accent); }
|
|
34
|
+
aside .node .dot { flex: 0 0 8px; width: 8px; height: 8px; border-radius: 50%; background: var(--fg-xdim); }
|
|
35
|
+
aside .node.running .dot { background: var(--green); animation: pulse 2s ease-in-out infinite; }
|
|
36
|
+
aside .node.stopped .dot { background: var(--yellow); }
|
|
37
|
+
aside .node.failed .dot { background: var(--red); }
|
|
38
|
+
aside .node .name { flex: 1; font-family: var(--mono); font-size: 11px; color: var(--fg-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
39
|
+
aside .node.active .name { color: var(--fg); }
|
|
40
|
+
aside .node .kill { flex: 0 0 auto; background: transparent; border: 1px solid var(--border); color: var(--fg-xdim); border-radius: 3px; padding: 1px 6px; font-size: 10px; cursor: pointer; font-family: var(--mono); }
|
|
41
|
+
aside .node.running .kill { color: var(--red); border-color: var(--red); }
|
|
42
|
+
aside .node .kill:hover:not([disabled]) { background: var(--red); color: #fff; }
|
|
43
|
+
aside .node .kill[disabled] { opacity: 0.3; cursor: not-allowed; }
|
|
44
|
+
aside .empty { padding: 12px; color: var(--fg-xdim); font-size: 12px; font-style: italic; }
|
|
45
|
+
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
|
|
46
|
+
header { position: sticky; top: 0; z-index: 10; background: var(--bg-raised); border-bottom: 1px solid var(--border); padding: 10px 16px; display: flex; align-items: center; gap: 12px; }
|
|
47
|
+
header .title { font-weight: 600; }
|
|
48
|
+
header .spawn-id { font-family: var(--mono); color: var(--fg-dim); font-size: 12px; }
|
|
49
|
+
header .status { margin-left: auto; font-size: 12px; color: var(--fg-dim); }
|
|
50
|
+
header .status .dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: var(--yellow); margin-right: 6px; vertical-align: middle; }
|
|
51
|
+
header .status.connected .dot { background: var(--green); }
|
|
52
|
+
header .status.ended .dot { background: var(--fg-xdim); }
|
|
53
|
+
header .status.error .dot { background: var(--red); }
|
|
54
|
+
|
|
55
|
+
main { padding: 16px; padding-bottom: 120px; max-width: 960px; margin: 0 auto; width: 100%; }
|
|
56
|
+
.frame { margin: 8px 0; }
|
|
57
|
+
.frame.system { color: var(--fg-xdim); font-style: italic; font-size: 12px; padding: 2px 0; }
|
|
58
|
+
.frame.user { border-left: 3px solid var(--accent); padding: 6px 12px; background: var(--bg-raised); border-radius: 0 4px 4px 0; }
|
|
59
|
+
.frame.user .prefix { color: var(--accent); font-weight: 600; margin-right: 6px; }
|
|
60
|
+
.frame.assistant-text { font-family: var(--mono); white-space: pre-wrap; word-break: break-word; }
|
|
61
|
+
.frame.thinking { color: var(--fg-xdim); font-style: italic; font-size: 13px; padding: 4px 8px; border-left: 2px dashed var(--border); margin: 6px 0; }
|
|
62
|
+
.frame.thinking summary { cursor: pointer; color: var(--fg-dim); }
|
|
63
|
+
.frame.tool-use { margin: 8px 0; border: 1px solid var(--border); border-radius: 4px; background: var(--bg-raised); }
|
|
64
|
+
.frame.tool-use summary { padding: 8px 12px; cursor: pointer; color: var(--accent-warm); font-family: var(--mono); font-size: 13px; list-style: none; }
|
|
65
|
+
.frame.tool-use summary::before { content: "⎿ "; color: var(--fg-dim); }
|
|
66
|
+
.frame.tool-use[open] summary { border-bottom: 1px solid var(--border); }
|
|
67
|
+
.frame.tool-use pre { margin: 0; padding: 10px 12px; font-family: var(--mono); font-size: 12px; color: var(--fg); white-space: pre-wrap; word-break: break-word; overflow-x: auto; }
|
|
68
|
+
.frame.tool-result { margin: 4px 0 8px 24px; padding: 8px 12px; border-left: 2px solid var(--border); background: var(--bg-soft); border-radius: 0 4px 4px 0; }
|
|
69
|
+
.frame.tool-result pre { margin: 0; font-family: var(--mono); font-size: 12px; color: var(--fg-dim); white-space: pre-wrap; word-break: break-word; max-height: 240px; overflow: hidden; }
|
|
70
|
+
.frame.tool-result.expanded pre { max-height: none; }
|
|
71
|
+
.frame.tool-result .show-more { display: block; margin-top: 6px; color: var(--accent); cursor: pointer; font-size: 11px; font-family: var(--mono); }
|
|
72
|
+
.frame.raw { color: var(--fg-xdim); font-family: var(--mono); font-size: 11px; white-space: pre-wrap; padding: 2px 0; }
|
|
73
|
+
.frame.boundary { margin: 12px 0; padding: 8px 12px; background: var(--bg-soft); border-left: 3px solid var(--accent-warm); border-radius: 0 4px 4px 0; font-size: 13px; }
|
|
74
|
+
.frame.boundary.done { border-left-color: var(--green); }
|
|
75
|
+
.frame.boundary.failed { border-left-color: var(--red); }
|
|
76
|
+
.frame.boundary .label { color: var(--accent-warm); font-weight: 600; font-family: var(--mono); font-size: 12px; margin-right: 8px; }
|
|
77
|
+
.frame.boundary.done .label { color: var(--green); }
|
|
78
|
+
.frame.boundary.failed .label { color: var(--red); }
|
|
79
|
+
.frame.boundary .meta { color: var(--fg-dim); font-family: var(--mono); font-size: 12px; }
|
|
80
|
+
|
|
81
|
+
.jump-to-live { position: fixed; bottom: 24px; right: 24px; background: var(--accent); color: #fff; border: none; padding: 10px 16px; border-radius: 20px; cursor: pointer; font-size: 13px; font-family: var(--sans); font-weight: 600; box-shadow: 0 4px 12px rgba(0,0,0,0.4); display: none; z-index: 20; }
|
|
82
|
+
.jump-to-live.visible { display: block; }
|
|
83
|
+
.jump-to-live:hover { background: #79c0ff; }
|
|
84
|
+
</style>
|
|
85
|
+
</head>
|
|
86
|
+
<body data-spawn-id="__SPAWN_ID__">
|
|
87
|
+
<header>
|
|
88
|
+
<div class="title">GSD-T Transcript</div>
|
|
89
|
+
<div class="spawn-id" id="hdr-spawn-id"></div>
|
|
90
|
+
<div class="status" id="hdr-status"><span class="dot"></span><span class="label">connecting…</span></div>
|
|
91
|
+
</header>
|
|
92
|
+
<aside>
|
|
93
|
+
<h3>Spawns</h3>
|
|
94
|
+
<div class="tree" id="tree"></div>
|
|
95
|
+
</aside>
|
|
96
|
+
<main id="stream"></main>
|
|
97
|
+
<button class="jump-to-live" id="jump-btn">↓ Jump to live</button>
|
|
98
|
+
|
|
99
|
+
<script>
|
|
100
|
+
(function () {
|
|
101
|
+
'use strict';
|
|
102
|
+
|
|
103
|
+
const spawnId = document.body.getAttribute('data-spawn-id');
|
|
104
|
+
document.getElementById('hdr-spawn-id').textContent = spawnId;
|
|
105
|
+
|
|
106
|
+
const stream = document.getElementById('stream');
|
|
107
|
+
const statusEl = document.getElementById('hdr-status');
|
|
108
|
+
const statusLabel = statusEl.querySelector('.label');
|
|
109
|
+
const jumpBtn = document.getElementById('jump-btn');
|
|
110
|
+
|
|
111
|
+
// Pair tool_result → tool_use by tool_use_id so the renderer can place
|
|
112
|
+
// the result next to the call even when they come as separate frames.
|
|
113
|
+
const toolUseById = new Map();
|
|
114
|
+
|
|
115
|
+
let autoScroll = true;
|
|
116
|
+
|
|
117
|
+
function setStatus(cls, text) {
|
|
118
|
+
statusEl.className = 'status ' + cls;
|
|
119
|
+
statusLabel.textContent = text;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function atBottom() {
|
|
123
|
+
return window.innerHeight + window.scrollY >= document.body.offsetHeight - 40;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
window.addEventListener('scroll', () => {
|
|
127
|
+
if (atBottom()) { autoScroll = true; jumpBtn.classList.remove('visible'); }
|
|
128
|
+
else { autoScroll = false; jumpBtn.classList.add('visible'); }
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
jumpBtn.addEventListener('click', () => {
|
|
132
|
+
autoScroll = true;
|
|
133
|
+
window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
|
|
134
|
+
jumpBtn.classList.remove('visible');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
function appendFrame(el) {
|
|
138
|
+
stream.appendChild(el);
|
|
139
|
+
if (autoScroll) {
|
|
140
|
+
requestAnimationFrame(() => window.scrollTo(0, document.body.scrollHeight));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function renderSystem(frame) {
|
|
145
|
+
const div = document.createElement('div');
|
|
146
|
+
div.className = 'frame system';
|
|
147
|
+
const sid = frame.session_id ? ' session=' + frame.session_id.slice(0, 8) : '';
|
|
148
|
+
const model = frame.model ? ' model=' + frame.model : '';
|
|
149
|
+
const subtype = frame.subtype ? ' ' + frame.subtype : '';
|
|
150
|
+
div.textContent = 'system' + subtype + sid + model;
|
|
151
|
+
appendFrame(div);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function renderUserMessage(content) {
|
|
155
|
+
const div = document.createElement('div');
|
|
156
|
+
div.className = 'frame user';
|
|
157
|
+
const p = document.createElement('span');
|
|
158
|
+
p.className = 'prefix';
|
|
159
|
+
p.textContent = '>';
|
|
160
|
+
div.appendChild(p);
|
|
161
|
+
const span = document.createElement('span');
|
|
162
|
+
span.textContent = typeof content === 'string' ? content : JSON.stringify(content);
|
|
163
|
+
div.appendChild(span);
|
|
164
|
+
appendFrame(div);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function renderAssistantText(text) {
|
|
168
|
+
const div = document.createElement('div');
|
|
169
|
+
div.className = 'frame assistant-text';
|
|
170
|
+
div.textContent = text;
|
|
171
|
+
appendFrame(div);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function renderThinking(text) {
|
|
175
|
+
const d = document.createElement('details');
|
|
176
|
+
d.className = 'frame thinking';
|
|
177
|
+
const s = document.createElement('summary');
|
|
178
|
+
s.textContent = '✻ Thinking (' + text.length + ' chars)';
|
|
179
|
+
d.appendChild(s);
|
|
180
|
+
const pre = document.createElement('div');
|
|
181
|
+
pre.textContent = text;
|
|
182
|
+
d.appendChild(pre);
|
|
183
|
+
appendFrame(d);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function toolUsePreview(input) {
|
|
187
|
+
if (!input || typeof input !== 'object') return '';
|
|
188
|
+
// Surface the most useful single arg: file_path, command, pattern, path, description
|
|
189
|
+
const keys = ['file_path', 'command', 'pattern', 'path', 'description', 'prompt'];
|
|
190
|
+
for (const k of keys) {
|
|
191
|
+
if (typeof input[k] === 'string') {
|
|
192
|
+
const v = input[k];
|
|
193
|
+
return k + '="' + (v.length > 80 ? v.slice(0, 77) + '…' : v) + '"';
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
const first = Object.keys(input)[0];
|
|
197
|
+
if (first) return first + '=…';
|
|
198
|
+
return '';
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function renderToolUse(block) {
|
|
202
|
+
const d = document.createElement('details');
|
|
203
|
+
d.className = 'frame tool-use';
|
|
204
|
+
const s = document.createElement('summary');
|
|
205
|
+
s.textContent = block.name + '(' + toolUsePreview(block.input) + ')';
|
|
206
|
+
d.appendChild(s);
|
|
207
|
+
const pre = document.createElement('pre');
|
|
208
|
+
try { pre.textContent = JSON.stringify(block.input, null, 2); }
|
|
209
|
+
catch { pre.textContent = String(block.input); }
|
|
210
|
+
d.appendChild(pre);
|
|
211
|
+
toolUseById.set(block.id, { name: block.name, el: d });
|
|
212
|
+
appendFrame(d);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function renderToolResult(block) {
|
|
216
|
+
const div = document.createElement('div');
|
|
217
|
+
div.className = 'frame tool-result';
|
|
218
|
+
const pre = document.createElement('pre');
|
|
219
|
+
let text;
|
|
220
|
+
if (typeof block.content === 'string') text = block.content;
|
|
221
|
+
else if (Array.isArray(block.content)) {
|
|
222
|
+
text = block.content.map(c => typeof c === 'string' ? c : (c.text || JSON.stringify(c))).join('\n');
|
|
223
|
+
} else text = JSON.stringify(block.content || '');
|
|
224
|
+
pre.textContent = text;
|
|
225
|
+
div.appendChild(pre);
|
|
226
|
+
if (text.length > 800) {
|
|
227
|
+
const more = document.createElement('span');
|
|
228
|
+
more.className = 'show-more';
|
|
229
|
+
more.textContent = 'show more';
|
|
230
|
+
more.addEventListener('click', () => {
|
|
231
|
+
div.classList.toggle('expanded');
|
|
232
|
+
more.textContent = div.classList.contains('expanded') ? 'show less' : 'show more';
|
|
233
|
+
});
|
|
234
|
+
div.appendChild(more);
|
|
235
|
+
}
|
|
236
|
+
appendFrame(div);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function renderBoundary(frame) {
|
|
240
|
+
const div = document.createElement('div');
|
|
241
|
+
div.className = 'frame boundary ' + (frame.state || '');
|
|
242
|
+
const label = document.createElement('span');
|
|
243
|
+
label.className = 'label';
|
|
244
|
+
label.textContent = '▸ task ' + (frame.state || '?');
|
|
245
|
+
div.appendChild(label);
|
|
246
|
+
const meta = document.createElement('span');
|
|
247
|
+
meta.className = 'meta';
|
|
248
|
+
const bits = [];
|
|
249
|
+
if (frame.taskId) bits.push('id=' + frame.taskId);
|
|
250
|
+
if (frame.domain) bits.push('domain=' + frame.domain);
|
|
251
|
+
if (frame.wave != null) bits.push('wave=' + frame.wave);
|
|
252
|
+
if (frame.exitCode != null) bits.push('exit=' + frame.exitCode);
|
|
253
|
+
if (frame.durationMs != null) bits.push('duration=' + Math.round(frame.durationMs/1000) + 's');
|
|
254
|
+
meta.textContent = bits.join(' ');
|
|
255
|
+
div.appendChild(meta);
|
|
256
|
+
appendFrame(div);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function renderRaw(line) {
|
|
260
|
+
const div = document.createElement('div');
|
|
261
|
+
div.className = 'frame raw';
|
|
262
|
+
div.textContent = line;
|
|
263
|
+
appendFrame(div);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function renderFrame(frame) {
|
|
267
|
+
if (!frame || typeof frame !== 'object') return;
|
|
268
|
+
const type = frame.type;
|
|
269
|
+
if (type === 'system') { renderSystem(frame); return; }
|
|
270
|
+
if (type === 'task-boundary') { renderBoundary(frame); return; }
|
|
271
|
+
if (type === 'raw') { renderRaw(frame.line || ''); return; }
|
|
272
|
+
if (type === 'assistant' && frame.message && Array.isArray(frame.message.content)) {
|
|
273
|
+
for (const b of frame.message.content) {
|
|
274
|
+
if (b.type === 'text') renderAssistantText(b.text || '');
|
|
275
|
+
else if (b.type === 'thinking') renderThinking(b.thinking || b.text || '');
|
|
276
|
+
else if (b.type === 'tool_use') renderToolUse(b);
|
|
277
|
+
else renderRaw(JSON.stringify(b));
|
|
278
|
+
}
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
if (type === 'user' && frame.message && Array.isArray(frame.message.content)) {
|
|
282
|
+
for (const b of frame.message.content) {
|
|
283
|
+
if (b.type === 'tool_result') renderToolResult(b);
|
|
284
|
+
else if (b.type === 'text') renderUserMessage(b.text || '');
|
|
285
|
+
else renderRaw(JSON.stringify(b));
|
|
286
|
+
}
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (type === 'user' && typeof frame.message === 'string') {
|
|
290
|
+
renderUserMessage(frame.message);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
if (type === 'result') {
|
|
294
|
+
const d = document.createElement('div');
|
|
295
|
+
d.className = 'frame system';
|
|
296
|
+
d.textContent = 'result subtype=' + (frame.subtype || '?') + ' duration=' + (frame.duration_ms || 0) + 'ms';
|
|
297
|
+
appendFrame(d);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
renderRaw(JSON.stringify(frame));
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// ── Sidebar ─────────────────────────────────────────────────────────
|
|
304
|
+
//
|
|
305
|
+
// Poll /transcripts every 3s, build a parent-indented tree, render.
|
|
306
|
+
// Exposed as window.__gsdtBuildTree for unit-testable tree-build logic.
|
|
307
|
+
|
|
308
|
+
function buildTree(spawns) {
|
|
309
|
+
// Map spawnId → node, then link children to parents. Orphans float
|
|
310
|
+
// to the root level.
|
|
311
|
+
const byId = new Map();
|
|
312
|
+
const roots = [];
|
|
313
|
+
for (const s of spawns) byId.set(s.spawnId, { ...s, children: [] });
|
|
314
|
+
for (const node of byId.values()) {
|
|
315
|
+
if (node.parentId && byId.has(node.parentId)) {
|
|
316
|
+
byId.get(node.parentId).children.push(node);
|
|
317
|
+
} else {
|
|
318
|
+
roots.push(node);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
// Sort each level newest-first
|
|
322
|
+
const ts = (a, b) => (Date.parse(b.startedAt) || 0) - (Date.parse(a.startedAt) || 0);
|
|
323
|
+
roots.sort(ts);
|
|
324
|
+
for (const n of byId.values()) n.children.sort(ts);
|
|
325
|
+
return roots;
|
|
326
|
+
}
|
|
327
|
+
window.__gsdtBuildTree = buildTree;
|
|
328
|
+
|
|
329
|
+
function statusClass(s) {
|
|
330
|
+
if (s.status === 'running') return 'running';
|
|
331
|
+
if (s.status === 'failed') return 'failed';
|
|
332
|
+
if (s.status === 'stopped') return 'stopped';
|
|
333
|
+
return '';
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function renderTree(roots) {
|
|
337
|
+
const tree = document.getElementById('tree');
|
|
338
|
+
tree.innerHTML = '';
|
|
339
|
+
if (!roots.length) {
|
|
340
|
+
const e = document.createElement('div');
|
|
341
|
+
e.className = 'empty';
|
|
342
|
+
e.textContent = 'No spawns yet.';
|
|
343
|
+
tree.appendChild(e);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
const currentId = (location.hash || '#').slice(1) || spawnId;
|
|
347
|
+
function render(node, depth) {
|
|
348
|
+
const el = document.createElement('div');
|
|
349
|
+
el.className = 'node ' + statusClass(node);
|
|
350
|
+
if (node.spawnId === currentId) el.classList.add('active');
|
|
351
|
+
el.style.paddingLeft = (12 + depth * 14) + 'px';
|
|
352
|
+
const dot = document.createElement('span'); dot.className = 'dot';
|
|
353
|
+
const name = document.createElement('span'); name.className = 'name';
|
|
354
|
+
name.textContent = (node.command || 'spawn') + ' · ' + node.spawnId.slice(-8);
|
|
355
|
+
name.title = (node.description || node.spawnId) + '\n' + (node.startedAt || '');
|
|
356
|
+
const kill = document.createElement('button');
|
|
357
|
+
kill.className = 'kill';
|
|
358
|
+
kill.textContent = 'kill';
|
|
359
|
+
kill.disabled = node.status !== 'running' || !node.workerPid;
|
|
360
|
+
kill.addEventListener('click', (ev) => {
|
|
361
|
+
ev.stopPropagation();
|
|
362
|
+
if (!confirm('SIGTERM spawn ' + node.spawnId + ' (pid ' + node.workerPid + ')?')) return;
|
|
363
|
+
fetch('/transcript/' + encodeURIComponent(node.spawnId) + '/kill', { method: 'POST' })
|
|
364
|
+
.then((r) => r.json())
|
|
365
|
+
.then((j) => { kill.textContent = j.status || 'killed'; })
|
|
366
|
+
.catch(() => { kill.textContent = 'err'; });
|
|
367
|
+
});
|
|
368
|
+
el.appendChild(dot); el.appendChild(name); el.appendChild(kill);
|
|
369
|
+
el.addEventListener('click', () => {
|
|
370
|
+
if (node.spawnId === currentId) return;
|
|
371
|
+
location.hash = node.spawnId;
|
|
372
|
+
});
|
|
373
|
+
document.getElementById('tree').appendChild(el);
|
|
374
|
+
for (const c of node.children) render(c, depth + 1);
|
|
375
|
+
}
|
|
376
|
+
for (const r of roots) render(r, 0);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
let pollTimer = null;
|
|
380
|
+
function pollSpawns() {
|
|
381
|
+
fetch('/transcripts')
|
|
382
|
+
.then((r) => r.json())
|
|
383
|
+
.then((j) => renderTree(buildTree(j.spawns || [])))
|
|
384
|
+
.catch(() => { /* keep last render */ });
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// ── SSE connection (reconnectable on hash change) ───────────────────
|
|
388
|
+
|
|
389
|
+
let src = null;
|
|
390
|
+
function connect(id) {
|
|
391
|
+
if (src) { try { src.close(); } catch { /* gone */ } src = null; }
|
|
392
|
+
stream.innerHTML = '';
|
|
393
|
+
toolUseById.clear();
|
|
394
|
+
autoScroll = true;
|
|
395
|
+
jumpBtn.classList.remove('visible');
|
|
396
|
+
document.getElementById('hdr-spawn-id').textContent = id;
|
|
397
|
+
setStatus('', 'connecting…');
|
|
398
|
+
src = new EventSource('/transcript/' + encodeURIComponent(id) + '/stream');
|
|
399
|
+
src.onopen = () => setStatus('connected', 'live');
|
|
400
|
+
src.onerror = () => setStatus('error', 'disconnected');
|
|
401
|
+
src.onmessage = (ev) => {
|
|
402
|
+
if (!ev.data) return;
|
|
403
|
+
try { renderFrame(JSON.parse(ev.data)); }
|
|
404
|
+
catch { renderRaw(ev.data); }
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
window.addEventListener('hashchange', () => {
|
|
409
|
+
const id = (location.hash || '').slice(1);
|
|
410
|
+
if (id) { connect(id); pollSpawns(); }
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
const initialId = (location.hash || '').slice(1) || spawnId;
|
|
414
|
+
if (!location.hash && spawnId) location.hash = spawnId;
|
|
415
|
+
connect(initialId);
|
|
416
|
+
|
|
417
|
+
pollSpawns();
|
|
418
|
+
pollTimer = setInterval(pollSpawns, 3000);
|
|
419
|
+
})();
|
|
420
|
+
</script>
|
|
421
|
+
</body>
|
|
422
|
+
</html>
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* M43 D1-T1 Probe — In-Session Usage Capture Branch Selection
|
|
4
|
+
*
|
|
5
|
+
* Captures the RAW Claude Code hook payload for Stop / SessionEnd / PostToolUse
|
|
6
|
+
* into .gsd-t/.hook-probe/{event}-{ts}.json so D1-T1 can decide:
|
|
7
|
+
* Branch A (hook-based) — if payload carries a `usage` object.
|
|
8
|
+
* Branch B (transcript tee) — otherwise.
|
|
9
|
+
*
|
|
10
|
+
* Behavior:
|
|
11
|
+
* - Zero-dep. Silent failure on any error (never interferes with Claude Code).
|
|
12
|
+
* - Writes at most 10 files per event type (rotating) to avoid growth.
|
|
13
|
+
* - Only active when .gsd-t/.hook-probe/ exists in the cwd; creating/deleting
|
|
14
|
+
* that directory is the on/off switch.
|
|
15
|
+
*/
|
|
16
|
+
const fs = require("fs");
|
|
17
|
+
const path = require("path");
|
|
18
|
+
|
|
19
|
+
const MAX_STDIN = 1024 * 1024;
|
|
20
|
+
const MAX_PER_EVENT = 10;
|
|
21
|
+
|
|
22
|
+
let input = "";
|
|
23
|
+
let aborted = false;
|
|
24
|
+
process.stdin.setEncoding("utf8");
|
|
25
|
+
process.stdin.on("data", (d) => {
|
|
26
|
+
input += d;
|
|
27
|
+
if (input.length > MAX_STDIN) { aborted = true; process.stdin.destroy(); }
|
|
28
|
+
});
|
|
29
|
+
process.stdin.on("end", () => {
|
|
30
|
+
if (aborted) return;
|
|
31
|
+
try {
|
|
32
|
+
const hook = JSON.parse(input);
|
|
33
|
+
const cwd = hook.cwd || process.cwd();
|
|
34
|
+
if (!path.isAbsolute(cwd)) return;
|
|
35
|
+
const probeDir = path.join(cwd, ".gsd-t", ".hook-probe");
|
|
36
|
+
if (!fs.existsSync(probeDir)) return; // disabled unless dir exists
|
|
37
|
+
const event = hook.hook_event_name || "unknown";
|
|
38
|
+
const ts = new Date().toISOString().replace(/[:.]/g, "-");
|
|
39
|
+
const sid = (hook.session_id || "nosid").slice(0, 12);
|
|
40
|
+
const file = path.join(probeDir, `${event}-${ts}-${sid}.json`);
|
|
41
|
+
const resolved = path.resolve(file);
|
|
42
|
+
if (!resolved.startsWith(path.resolve(probeDir) + path.sep)) return;
|
|
43
|
+
fs.writeFileSync(file, JSON.stringify(hook, null, 2) + "\n");
|
|
44
|
+
rotate(probeDir, event);
|
|
45
|
+
} catch {
|
|
46
|
+
// silent
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
function rotate(dir, event) {
|
|
51
|
+
try {
|
|
52
|
+
const files = fs.readdirSync(dir)
|
|
53
|
+
.filter((f) => f.startsWith(event + "-") && f.endsWith(".json"))
|
|
54
|
+
.map((f) => ({ f, t: fs.statSync(path.join(dir, f)).mtimeMs }))
|
|
55
|
+
.sort((a, b) => b.t - a.t);
|
|
56
|
+
for (const { f } of files.slice(MAX_PER_EVENT)) {
|
|
57
|
+
try { fs.unlinkSync(path.join(dir, f)); } catch { /* noop */ }
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
// silent
|
|
61
|
+
}
|
|
62
|
+
}
|