nearly-cli 0.1.0

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.
@@ -0,0 +1,449 @@
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">
6
+ <title>__TITLE__ · Nearly record</title>
7
+ <meta name="description" content="__DESC__">
8
+ <meta property="og:title" content="__TITLE__">
9
+ <meta property="og:description" content="__DESC__">
10
+ <meta property="og:type" content="video.other">
11
+ <meta name="twitter:card" content="summary">
12
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
13
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap">
14
+ <style>
15
+ :root {
16
+ --bg: #0B0D11;
17
+ --panel: #13161C;
18
+ --panel-2: #191D25;
19
+ --rule: #242932;
20
+ --rule-soft: #1B1F27;
21
+ --ink: #E9ECF1;
22
+ --ink-mute: #98A1AF;
23
+ --ink-faint: #667080;
24
+ --allow: #3DD68C;
25
+ --deny: #FF7A7A;
26
+ --ask: #FFC145;
27
+ --auto: #667080;
28
+ --accent: #7C8CFF;
29
+ color-scheme: dark;
30
+ }
31
+ * { box-sizing: border-box; }
32
+ html, body { height: 100%; }
33
+ body {
34
+ margin: 0; background: var(--bg); color: var(--ink);
35
+ font-family: "Plus Jakarta Sans", ui-sans-serif, system-ui, -apple-system, sans-serif;
36
+ font-size: 14px; line-height: 1.55; -webkit-font-smoothing: antialiased; letter-spacing: -0.006em;
37
+ }
38
+ .mono { font-family: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace; }
39
+ .lbl {
40
+ font-family: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
41
+ font-size: 10px; letter-spacing: .12em; text-transform: uppercase; color: var(--ink-faint);
42
+ }
43
+ a { color: var(--accent); text-decoration: none; }
44
+ a:hover { text-decoration: underline; }
45
+ .shell { max-width: 960px; margin: 0 auto; padding: 24px 20px 56px; display: flex; flex-direction: column; gap: 14px; }
46
+
47
+ header { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
48
+ .brand { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
49
+ h1 { margin: 0; font-size: 15px; font-weight: 700; letter-spacing: -.02em; }
50
+ .chip {
51
+ font-family: "JetBrains Mono", ui-monospace, monospace; font-size: 10px; letter-spacing: .1em;
52
+ padding: 2px 7px; border: 1px solid var(--rule); border-radius: 3px; color: var(--ink-mute);
53
+ }
54
+ .chip[data-tone="recap"] { color: var(--accent); border-color: color-mix(in srgb, var(--accent) 40%, transparent); }
55
+ .meta { font-size: 12px; color: var(--ink-mute); display: flex; gap: 14px; flex-wrap: wrap; }
56
+
57
+ /* progress */
58
+ .prog { display: flex; gap: 4px; height: 4px; }
59
+ .seg { flex: 1; background: var(--rule); border-radius: 2px; overflow: hidden; cursor: pointer; position: relative; }
60
+ .seg > i { display: block; height: 100%; width: 0; background: var(--ink); }
61
+ .seg[data-tone="deny"] > i { background: var(--deny); }
62
+ .seg[data-tone="allow"] > i { background: var(--allow); }
63
+ .seg[data-tone="undo"] > i { background: var(--accent); }
64
+
65
+ /* stage */
66
+ .stage {
67
+ position: relative; background: var(--panel); border: 1px solid var(--rule); border-radius: 14px;
68
+ aspect-ratio: 16 / 9; min-height: 440px; overflow: hidden;
69
+ }
70
+ .scene { position: absolute; inset: 0; padding: 34px 38px; overflow: auto; display: flex; flex-direction: column; gap: 16px; animation: in .35s ease-out; }
71
+ @keyframes in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
72
+ .eyebrow { display: flex; align-items: center; gap: 10px; }
73
+ .idx { position: absolute; top: 14px; left: 16px; z-index: 2; }
74
+ .avatar {
75
+ position: absolute; top: 14px; right: 16px; z-index: 2; width: 36px; height: 36px; border-radius: 50%;
76
+ background: var(--panel-2); border: 1px solid var(--rule); display: grid; place-items: center;
77
+ font-family: "JetBrains Mono", ui-monospace, monospace; font-size: 11px; letter-spacing: .06em; color: var(--ink);
78
+ }
79
+ .avatar[data-speaking="true"] { box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 35%, transparent); }
80
+
81
+ .title { font-size: clamp(23px, 3.5vw, 36px); font-weight: 700; letter-spacing: -.03em; line-height: 1.15; margin: 0; max-width: 20ch; }
82
+ .sub { color: var(--ink-mute); font-size: 14px; max-width: 64ch; }
83
+ /* one line telling a reviewer with no context what they are looking at */
84
+ .orient { margin: 0; font-size: 15.5px; line-height: 1.5; color: var(--ink); max-width: 58ch; border-left: 2px solid var(--accent); padding-left: 14px; }
85
+ .stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 1px; background: var(--rule-soft); border: 1px solid var(--rule); border-radius: 3px; overflow: hidden; margin-top: auto; }
86
+ .stat { background: var(--panel); padding: 12px 14px; display: flex; flex-direction: column; gap: 2px; }
87
+ .stat-v { font-family: "JetBrains Mono", ui-monospace, monospace; font-size: 20px; font-variant-numeric: tabular-nums; letter-spacing: -.01em; }
88
+ .stat-v[data-tone="deny"] { color: var(--deny); }
89
+ .stat-v[data-tone="ask"] { color: var(--ask); }
90
+ .stat-v[data-tone="undo"] { color: var(--accent); }
91
+
92
+ .quote {
93
+ font-size: clamp(15px, 1.9vw, 19px); line-height: 1.5; color: var(--ink); max-width: 62ch;
94
+ border-left: 2px solid var(--accent); padding: 4px 0 4px 18px; white-space: pre-wrap;
95
+ }
96
+
97
+ .rows { display: flex; flex-direction: column; border: 1px solid var(--rule); border-radius: 3px; overflow: hidden; }
98
+ .row { display: grid; grid-template-columns: 90px 1fr auto; gap: 12px; align-items: center; padding: 9px 14px; border-bottom: 1px solid var(--rule-soft); background: var(--panel); }
99
+ .row:last-child { border-bottom: none; }
100
+ .row .t { font-weight: 500; }
101
+ .row .s { font-family: "JetBrains Mono", ui-monospace, monospace; font-size: 12px; color: var(--ink-mute); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
102
+ .tag {
103
+ font-family: "JetBrains Mono", ui-monospace, monospace; font-size: 10px; letter-spacing: .08em; text-transform: uppercase;
104
+ padding: 1px 6px; border-radius: 2px; border: 1px solid; white-space: nowrap;
105
+ }
106
+ .tag[data-k="ask"] { color: var(--ask); border-color: color-mix(in srgb, var(--ask) 40%, transparent); }
107
+ .tag[data-k="allow"] { color: var(--allow); border-color: color-mix(in srgb, var(--allow) 40%, transparent); }
108
+ .tag[data-k="deny"] { color: var(--deny); border-color: color-mix(in srgb, var(--deny) 40%, transparent); }
109
+ .tag[data-k="auto"] { color: var(--auto); border-color: var(--rule); }
110
+ .tag[data-k="info"] { color: var(--accent);border-color: color-mix(in srgb, var(--accent) 40%, transparent); }
111
+ .tag[data-k="never"] { color: var(--deny); border-color: var(--deny); background: color-mix(in srgb, var(--deny) 12%, transparent); }
112
+
113
+ /* the ask card, as it appeared */
114
+ .card { border: 1px solid var(--rule); border-radius: 10px; overflow: hidden; background: var(--panel-2); display: flex; flex-direction: column; }
115
+ .card-h { display: flex; align-items: center; gap: 10px; padding: 10px 14px; border-bottom: 1px solid var(--rule-soft); }
116
+ .card-h .tool { font-weight: 600; }
117
+ .card-h .key { font-family: "JetBrains Mono", ui-monospace, monospace; font-size: 11px; color: var(--ink-mute); }
118
+ .card pre {
119
+ margin: 0; padding: 12px 14px; font-family: "JetBrains Mono", ui-monospace, monospace; font-size: 12.5px; line-height: 1.5;
120
+ color: var(--ink); background: var(--bg); white-space: pre-wrap; word-break: break-word; max-height: 190px; overflow: auto;
121
+ }
122
+ .card .blast { padding: 9px 14px; font-size: 12px; color: var(--ink-mute); border-top: 1px solid var(--rule-soft); }
123
+ .stamp {
124
+ display: flex; align-items: center; gap: 12px; padding: 12px 14px; border-top: 1px solid var(--rule-soft);
125
+ animation: stamp .3s ease-out 1.1s both;
126
+ }
127
+ @keyframes stamp { from { opacity: 0; transform: scale(1.04); } to { opacity: 1; transform: none; } }
128
+ .stamp .big { font-family: "JetBrains Mono", ui-monospace, monospace; font-size: 14px; letter-spacing: .08em; text-transform: uppercase; font-weight: 500; }
129
+ .stamp[data-d="allow"] .big { color: var(--allow); }
130
+ .stamp[data-d="deny"] .big { color: var(--deny); }
131
+ .stamp[data-d="deny"], .stamp[data-d="never"] { background: color-mix(in srgb, var(--deny) 7%, transparent); }
132
+ .stamp[data-d="never"] .big { color: var(--deny); }
133
+ .stamp .wait { font-size: 12px; color: var(--ink-mute); font-variant-numeric: tabular-nums; }
134
+ .stamp .res { font-family: "JetBrains Mono", ui-monospace, monospace; font-size: 11.5px; color: var(--ink-mute); margin-left: auto; max-width: 40ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
135
+ .stamp .res[data-err="true"] { color: var(--deny); }
136
+
137
+ /* diff */
138
+ .diffwrap { display: grid; grid-template-columns: 220px 1fr; gap: 12px; min-height: 0; flex: 1; }
139
+ .files { display: flex; flex-direction: column; gap: 6px; }
140
+ .file { display: flex; justify-content: space-between; gap: 8px; font-family: "JetBrains Mono", ui-monospace, monospace; font-size: 12px; padding: 7px 10px; border: 1px solid var(--rule); border-radius: 3px; background: var(--panel-2); }
141
+ .file .n { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
142
+ .file .a { color: var(--allow); } .file .d { color: var(--deny); }
143
+ .patch {
144
+ margin: 0; font-family: "JetBrains Mono", ui-monospace, monospace; font-size: 12px; line-height: 1.5; background: var(--bg);
145
+ border: 1px solid var(--rule); border-radius: 3px; overflow: auto; padding: 10px 0; max-height: 300px;
146
+ }
147
+ .patch div { padding: 0 14px; white-space: pre; }
148
+ .patch .add { background: color-mix(in srgb, var(--allow) 10%, transparent); color: var(--allow); }
149
+ .patch .del { background: color-mix(in srgb, var(--deny) 10%, transparent); color: var(--deny); }
150
+ .patch .hunk { color: var(--accent); opacity: .8; }
151
+ .patch .hdr { color: var(--ink-faint); }
152
+ .patch .more { color: var(--ink-faint); font-style: italic; padding-top: 6px; }
153
+ .reverted { position: relative; }
154
+ .reverted .patch { opacity: .55; border-color: color-mix(in srgb, var(--accent) 40%, transparent); }
155
+ .reverted::after {
156
+ content: "REVERTED"; position: absolute; right: 14px; top: 10px; z-index: 1;
157
+ font-family: "JetBrains Mono", ui-monospace, monospace; font-size: 10px; letter-spacing: .14em;
158
+ color: var(--accent); border: 1px solid var(--accent); padding: 2px 8px; border-radius: 2px;
159
+ background: var(--bg);
160
+ }
161
+
162
+ /* outcome */
163
+ .cols { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; flex: 1; min-height: 0; }
164
+ .col { border: 1px solid var(--rule); border-radius: 3px; background: var(--panel-2); display: flex; flex-direction: column; min-height: 0; }
165
+ .col .lbl { padding: 9px 14px; border-bottom: 1px solid var(--rule-soft); }
166
+ .col .body { padding: 12px 14px; font-size: 13px; line-height: 1.55; overflow: auto; white-space: pre-wrap; color: var(--ink); }
167
+ .notdone { display: flex; flex-direction: column; gap: 6px; }
168
+ .notdone .row { grid-template-columns: auto 1fr; border: 1px solid color-mix(in srgb, var(--deny) 30%, transparent); border-radius: 3px; }
169
+
170
+ .credits { justify-content: center; align-items: center; text-align: center; gap: 12px; }
171
+ .credits .big-avatar { width: 64px; height: 64px; border-radius: 50%; background: var(--panel-2); border: 1px solid var(--rule); display: grid; place-items: center; font-family: "JetBrains Mono", ui-monospace, monospace; font-size: 16px; letter-spacing: .06em; }
172
+ .credits .links { display: flex; gap: 18px; flex-wrap: wrap; justify-content: center; font-size: 13px; }
173
+
174
+ /* overlay */
175
+ .overlay { position: absolute; inset: 0; z-index: 5; display: grid; place-items: center; background: color-mix(in srgb, var(--bg) 72%, transparent); backdrop-filter: blur(2px); }
176
+ .overlay button { font-size: 15px; padding: 12px 26px; }
177
+ .overlay .hint { margin-top: 10px; font-size: 12px; color: var(--ink-mute); text-align: center; }
178
+ .overlay[hidden] { display: none; }
179
+
180
+ /* caption + controls */
181
+ .caption { min-height: 52px; font-size: clamp(15px, 1.8vw, 17px); line-height: 1.5; color: var(--ink); max-width: 80ch; overflow-wrap: anywhere; }
182
+ .caption b { color: var(--accent); font-weight: 500; }
183
+ .ctrls { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
184
+ button {
185
+ font-family: inherit; font-size: 12.5px; font-weight: 600; letter-spacing: -0.01em;
186
+ color: var(--ink); background: var(--panel-2);
187
+ border: 1px solid var(--rule); border-radius: 7px; padding: 7px 13px; cursor: pointer;
188
+ }
189
+ button:hover { background: var(--rule); }
190
+ button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
191
+ button[data-primary] { background: var(--accent); color: #0B0D11; border-color: var(--accent); font-weight: 600; }
192
+ button[data-primary]:hover { background: #8F9CFF; }
193
+ button[aria-pressed="true"] { border-color: var(--accent); color: var(--accent); }
194
+ .clock { font-family: "JetBrains Mono", ui-monospace, monospace; font-size: 12px; color: var(--ink-mute); font-variant-numeric: tabular-nums; margin-left: auto; }
195
+ .foot { font-size: 12px; color: var(--ink-faint); }
196
+
197
+ /* grid and flex children must be allowed to shrink, or long tokens push the stage sideways */
198
+ .cols, .col, .diffwrap, .diffwrap > *, .row > *, .files, .file, .card, .stamp > * { min-width: 0; }
199
+ .stamp { flex-wrap: wrap; }
200
+
201
+ @media (max-width: 720px) {
202
+ .scene { padding: 44px 18px 18px; }
203
+ .diffwrap, .cols { grid-template-columns: 1fr; }
204
+ .stage { aspect-ratio: auto; min-height: 520px; }
205
+ .row { grid-template-columns: 70px 1fr auto; }
206
+ }
207
+ @media (prefers-reduced-motion: reduce) { * { animation: none !important; transition: none !important; } }
208
+ </style>
209
+ </head>
210
+ <body>
211
+ <div class="shell">
212
+
213
+ <header>
214
+ <div class="brand">
215
+ <h1>Nearly</h1>
216
+ <span class="chip" data-tone="recap">SESSION RECORD</span>
217
+ <span class="chip mono" id="hName">—</span>
218
+ <span class="chip mono" id="hModel">—</span>
219
+ </div>
220
+ <div class="meta"><span id="hSup"></span><span id="hDate"></span><a href="../" id="hReplay">All records →</a></div>
221
+ </header>
222
+
223
+ <div class="prog" id="prog" role="list" aria-label="Scenes"></div>
224
+
225
+ <div class="stage" id="stage">
226
+ <span class="lbl idx" id="idx"></span>
227
+ <div class="avatar" id="avatar" title="Narrator">—</div>
228
+ <div id="sceneHost"></div>
229
+ <div class="overlay" id="overlay">
230
+ <div>
231
+ <button data-primary id="start">▶ Play recap</button>
232
+ <div class="hint" id="hint"></div>
233
+ </div>
234
+ </div>
235
+ </div>
236
+
237
+ <div class="caption" id="caption"></div>
238
+
239
+ <div class="ctrls">
240
+ <button data-primary id="play">Play</button>
241
+ <button id="prev" title="Previous scene (←)">‹</button>
242
+ <button id="next" title="Next scene (→)">›</button>
243
+ <button id="mute" title="Mute (m)" aria-pressed="false">Sound on</button>
244
+ <span class="clock" id="clock">0:00 / 0:00</span>
245
+ </div>
246
+
247
+ <p class="foot" id="foot"></p>
248
+
249
+ </div>
250
+
251
+ <script>
252
+ const R = __RECAP__;
253
+
254
+ const $ = (id) => document.getElementById(id);
255
+ const esc = (s) => String(s ?? '').replace(/[&<>"]/g, (c) => ({ '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;' }[c]));
256
+ const mmss = (s) => { s = Math.max(0, Math.round(s)); return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, '0')}`; };
257
+ const T = (k, t) => `<span class="tag" data-k="${k}">${esc(t)}</span>`;
258
+ // A reviewer was not in the room, so name the supervisor instead of saying "you".
259
+ const REV = R.audience !== 'supervisor';
260
+ const SUPN = R.supervisor || 'the supervisor';
261
+ const saidNo = REV ? `${SUPN} said no` : 'you said no';
262
+ const askedFor = REV ? `${SUPN} asked for` : 'You asked';
263
+
264
+ const SC = R.scenes;
265
+ const TOTAL = R.totalS;
266
+ const startAt = SC.map((s, i) => SC.slice(0, i).reduce((n, x) => n + x.durS, 0));
267
+ const hasAudio = SC.some((s) => s.audio);
268
+
269
+ // ---------------------------------------------------------------------------
270
+ // renderers: one per scene kind
271
+ // ---------------------------------------------------------------------------
272
+ function patchHtml(p) {
273
+ if (!p) return `<div class="sub">Diff not available: the worktree is gone.</div>`;
274
+ const lines = p.text.split('\n').map((l) => {
275
+ const cls = l.startsWith('+++') || l.startsWith('---') || l.startsWith('diff ') || l.startsWith('index ') ? 'hdr'
276
+ : l.startsWith('@@') ? 'hunk' : l.startsWith('+') ? 'add' : l.startsWith('-') ? 'del' : '';
277
+ return `<div class="${cls}">${esc(l) || ' '}</div>`;
278
+ });
279
+ if (p.truncated) lines.push(`<div class="more">… ${p.total - lines.length} more lines in the full replay</div>`);
280
+ return `<pre class="patch">${lines.join('')}</pre>`;
281
+ }
282
+ function filesHtml(stat) {
283
+ if (!stat || !stat.length) return `<div class="sub">No files changed.</div>`;
284
+ return `<div class="files">${stat.map((f) => `<div class="file"><span class="n" title="${esc(f.file)}">${esc(f.file)}</span><span>${f.untracked ? '<span class="a">new</span>' : `<span class="a">+${f.add}</span> <span class="d">−${f.del}</span>`}</span></div>`).join('')}</div>`;
285
+ }
286
+
287
+ const render = {
288
+ cover: (s) => `
289
+ <div class="eyebrow"><span class="lbl">Session record</span>${s.branch ? `<span class="chip mono">${esc(s.branch)}</span>` : ''}${s.repo ? `<span class="chip mono">${esc(s.repo)}</span>` : ''}</div>
290
+ <h2 class="title">${esc(s.title)}</h2>
291
+ <p class="orient">${esc(s.orient)}</p>
292
+ <div class="sub">${esc(R.name)} · ${esc(R.model)} · ${esc(R.date)}${R.audience === 'supervisor' ? '' : ` · supervised by ${esc(R.supervisor)}`}</div>
293
+ <div class="stats">${s.stats.map(([k, v, tone]) => `<div class="stat"><span class="lbl">${esc(k)}</span><span class="stat-v" ${tone ? `data-tone="${tone}"` : ''}>${esc(v)}</span></div>`).join('')}</div>`,
294
+
295
+ intent: (s) => `
296
+ <div class="eyebrow"><span class="lbl">${s.ordinal ? `Instruction ${s.ordinal} of ${s.of}` : 'The task, verbatim'}</span>${T('info', 'frozen spec')}</div>
297
+ <div class="quote">${esc(s.text)}</div>
298
+ <div class="sub" style="margin-top:auto">Everything that follows is measured against this text, not against what the agent later said it did.</div>`,
299
+
300
+ quiet: (s) => `
301
+ <div class="eyebrow"><span class="lbl">Ran without asking</span>${T('auto', 'log tier')}</div>
302
+ <div class="rows">${s.items.map((i) => `<div class="row"><span class="t">${esc(i.tool)}</span><span class="s" title="${esc(i.sub)}">${esc(i.sub)}</span>${T('auto', 'receipt')}</div>`).join('')}</div>
303
+ <div class="sub" style="margin-top:auto">Read-only tools run on the log tier. Nobody was asked, but every call is on the record.</div>`,
304
+
305
+ decision: (s) => {
306
+ const d = s.tier === 'never' ? 'never' : s.decision;
307
+ const big = d === 'never' ? 'Blocked by policy' : d === 'allow' ? (s.scope === 'always' ? 'Allowed, now a rule' : 'Allowed once') : (s.scope === 'always' ? 'Denied, now a never rule' : 'Denied');
308
+ return `
309
+ <div class="eyebrow"><span class="lbl">The agent asked</span>${T(s.tier === 'never' ? 'never' : 'ask', s.tier === 'never' ? 'never' : 'held')}</div>
310
+ <div class="card">
311
+ <div class="card-h"><span class="tool">${esc(s.tool)}</span><span class="key">rule key ${esc(s.key)}</span></div>
312
+ <pre>${esc(s.input)}</pre>
313
+ <div class="blast">${esc(s.blast)}</div>
314
+ <div class="stamp" data-d="${d}">
315
+ <span class="big">${esc(big)}</span>
316
+ ${s.waitedS ? `<span class="wait">after ${esc(s.waitedS)}s</span>` : `<span class="wait">never reached you</span>`}
317
+ ${s.resultPreview ? `<span class="res" data-err="${s.resultError}" title="${esc(s.resultPreview)}">${esc(s.resultPreview)}</span>` : ''}
318
+ </div>
319
+ </div>`;
320
+ },
321
+
322
+ diff: (s) => `
323
+ <div class="eyebrow"><span class="lbl">${s.sha ? `Turn ${s.turn} committed` : `Turn ${s.turn}, uncommitted changes`}</span>${s.sha ? `<span class="chip mono">${esc(s.sha)}</span>` : T('info', 'your branch')}</div>
324
+ <div class="sub">${esc(s.msg)}</div>
325
+ <div class="diffwrap">${filesHtml(s.stat)}${patchHtml(s.patch)}</div>`,
326
+
327
+ undo: (s) => `
328
+ <div class="eyebrow"><span class="lbl">Undone</span><span class="chip mono">${esc(s.from)} → ${esc(s.to)}</span>${T('info', 'git reset --hard')}</div>
329
+ <div class="sub">This change was on disk, then wasn’t. The recording keeps it so the story stays honest.</div>
330
+ <div class="diffwrap reverted">${filesHtml(s.stat)}${patchHtml(s.patch)}</div>`,
331
+
332
+ outcome: (s) => `
333
+ <div class="eyebrow"><span class="lbl">Intent vs outcome</span></div>
334
+ <div class="cols">
335
+ <div class="col"><span class="lbl">${esc(askedFor)}</span><div class="body">${esc(s.task)}</div></div>
336
+ <div class="col"><span class="lbl">Agent reported</span><div class="body">${esc(s.report || '(no final message)')}</div></div>
337
+ </div>
338
+ ${s.notDone.length ? `<div class="notdone">${s.notDone.map((n) => `<div class="row">${T(n.by === 'policy' ? 'never' : 'deny', n.by === 'policy' ? 'blocked by policy' : saidNo)}<span class="s" title="${esc(n.what)}">${esc(n.tool)} · ${esc(n.what)}</span></div>`).join('')}</div>` : ''}
339
+ <div class="sub">${s.head ? `Worktree now at <span class="mono">${esc(s.head)}</span>` : ''}${s.turns ? ` · ${s.turns} turns` : ''}${s.cost != null ? ` · est. $${Number(s.cost).toFixed(2)} of subscription` : ''}</div>`,
340
+
341
+ credits: () => `
342
+ <div class="scene-inner credits" style="display:flex;flex-direction:column;flex:1">
343
+ <div class="big-avatar">${esc(R.avatar)}</div>
344
+ <h2 class="title" style="max-width:none">Every number here was computed.</h2>
345
+ ${R.dropped ? `<div class="sub" style="color:var(--deny)">${R.dropped} line${R.dropped > 1 ? 's' : ''} of the recording could not be read, so this record may be missing events. Treat the counts as a floor, not a total.</div>` : ''}
346
+ <div class="sub">Facts from the session recording. ${R.polished ? 'Sentences rewritten by a model; facts were not.' : 'No model wrote any of it.'}${R.voice ? ` Voice: macOS ${esc(R.voice)}.` : ''}</div>
347
+ <div class="links">${R.project ? `<a href="${esc(R.project)}" target="_blank" rel="noopener">How this was made</a>` : ''}<a href="../">All session records</a></div>
348
+ </div>`,
349
+ };
350
+
351
+ // ---------------------------------------------------------------------------
352
+ // player
353
+ // ---------------------------------------------------------------------------
354
+ let cur = -1, playing = false, muted = false, raf = null, sceneStart = 0, elapsed = 0, timer = null;
355
+ const audio = new Audio();
356
+ audio.preload = 'auto';
357
+
358
+ function tone(s) { return s.kind === 'decision' ? (s.decision === 'deny' || s.tier === 'never' ? 'deny' : 'allow') : s.kind === 'undo' ? 'undo' : ''; }
359
+
360
+ function show(i, { autoplay = playing } = {}) {
361
+ cur = Math.max(0, Math.min(SC.length - 1, i));
362
+ const s = SC[cur];
363
+ if (i !== 0 || autoplay) $('overlay').hidden = true;
364
+ $('sceneHost').innerHTML = `<div class="scene">${render[s.kind] ? render[s.kind](s) : ''}</div>`;
365
+ $('idx').textContent = `${cur + 1} / ${SC.length} · ${s.kind}`;
366
+ $('caption').innerHTML = esc(s.narration);
367
+ document.querySelectorAll('.seg').forEach((el, k) => { el.firstChild.style.width = k < cur ? '100%' : '0'; });
368
+ elapsed = 0;
369
+ stopClocks();
370
+ if (s.audio) {
371
+ audio.src = s.audio;
372
+ audio.muted = muted;
373
+ if (autoplay) audio.play().catch(() => {});
374
+ } else {
375
+ audio.removeAttribute('src');
376
+ }
377
+ if (autoplay) startClocks();
378
+ }
379
+
380
+ function startClocks() {
381
+ const s = SC[cur];
382
+ sceneStart = performance.now() - elapsed * 1000;
383
+ const loop = () => {
384
+ if (!playing) return;
385
+ elapsed = s.audio && !audio.paused && audio.duration ? audio.currentTime : (performance.now() - sceneStart) / 1000;
386
+ const frac = Math.min(1, elapsed / s.durS);
387
+ document.querySelectorAll('.seg')[cur].firstChild.style.width = (frac * 100) + '%';
388
+ $('clock').textContent = `${mmss(startAt[cur] + elapsed)} / ${mmss(TOTAL)}`;
389
+ $('avatar').dataset.speaking = String(s.audio && !audio.paused);
390
+ raf = requestAnimationFrame(loop);
391
+ };
392
+ raf = requestAnimationFrame(loop);
393
+ if (!s.audio) timer = setTimeout(advance, s.durS * 1000);
394
+ }
395
+ function stopClocks() { if (raf) cancelAnimationFrame(raf); if (timer) clearTimeout(timer); raf = timer = null; $('avatar').dataset.speaking = 'false'; }
396
+
397
+ function advance() {
398
+ if (cur >= SC.length - 1) { pause(); return; }
399
+ show(cur + 1, { autoplay: true });
400
+ }
401
+ audio.addEventListener('ended', () => { if (playing) timer = setTimeout(advance, 800); });
402
+
403
+ function play() {
404
+ playing = true;
405
+ $('play').textContent = 'Pause';
406
+ $('overlay').hidden = true;
407
+ if (cur === -1 || cur === SC.length - 1 && elapsed >= SC[cur].durS) show(0, { autoplay: true });
408
+ else { if (SC[cur].audio) audio.play().catch(() => {}); startClocks(); }
409
+ }
410
+ function pause() {
411
+ playing = false;
412
+ $('play').textContent = 'Play';
413
+ audio.pause();
414
+ stopClocks();
415
+ }
416
+
417
+ // ---------------------------------------------------------------------------
418
+ // chrome
419
+ // ---------------------------------------------------------------------------
420
+ $('hName').textContent = R.name;
421
+ $('hModel').textContent = R.model;
422
+ $('hSup').textContent = (R.runs > 1 ? `${R.runs} sessions · ` : '') + (R.audience === 'supervisor' ? 'your session' : `supervised by ${R.supervisor}`);
423
+ $('hDate').textContent = `${R.date} · ${R.durationS.toFixed(0)}s of agent work · ${mmss(TOTAL)} to watch`;
424
+ $('avatar').textContent = R.avatar;
425
+ $('hint').textContent = `${SC.length} scenes · ${mmss(TOTAL)}${hasAudio ? ' · narrated' : ''}`;
426
+ $('start').textContent = '▶ Play the record';
427
+ $('clock').textContent = `0:00 / ${mmss(TOTAL)}`;
428
+ $('foot').textContent = `Generated ${new Date(R.generatedAt).toLocaleString('en-GB')} from recordings/${R.id}.jsonl. ${R.polished ? 'Narration rewritten by a model from computed facts.' : 'Narration computed, not generated.'}`;
429
+ $('prog').innerHTML = SC.map((s, i) => `<div class="seg" data-tone="${tone(s)}" role="listitem" title="${i + 1}. ${esc(s.kind)}"><i></i></div>`).join('');
430
+ $('prog').onclick = (e) => { const seg = e.target.closest('.seg'); if (!seg) return; show([...seg.parentNode.children].indexOf(seg), { autoplay: playing }); };
431
+
432
+ $('start').onclick = play;
433
+ $('play').onclick = () => (playing ? pause() : play());
434
+ $('prev').onclick = () => show(Math.max(0, cur - 1), { autoplay: playing });
435
+ $('next').onclick = () => show(Math.min(SC.length - 1, cur + 1), { autoplay: playing });
436
+ $('mute').onclick = () => { muted = !muted; audio.muted = muted; $('mute').textContent = muted ? 'Sound off' : 'Sound on'; $('mute').setAttribute('aria-pressed', String(muted)); };
437
+ if (!hasAudio) { $('mute').textContent = 'No audio'; $('mute').disabled = true; }
438
+ document.addEventListener('keydown', (e) => {
439
+ if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
440
+ if (e.key === ' ') { e.preventDefault(); playing ? pause() : play(); }
441
+ if (e.key === 'ArrowRight') $('next').click();
442
+ if (e.key === 'ArrowLeft') $('prev').click();
443
+ if (e.key === 'm') $('mute').click();
444
+ });
445
+
446
+ show(0, { autoplay: false });
447
+ </script>
448
+ </body>
449
+ </html>