humanish 0.41.0 → 0.43.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.
Files changed (61) hide show
  1. package/README.md +36 -8
  2. package/dist/actor-contract.d.ts +9 -0
  3. package/dist/actor-contract.js.map +1 -1
  4. package/dist/adapter-extension.js +1 -0
  5. package/dist/adapter-extension.js.map +1 -1
  6. package/dist/cli.js +4 -0
  7. package/dist/cli.js.map +1 -1
  8. package/dist/computer-use-actor.d.ts +11 -0
  9. package/dist/computer-use-actor.js +2 -0
  10. package/dist/computer-use-actor.js.map +1 -1
  11. package/dist/computer-use.d.ts +30 -0
  12. package/dist/computer-use.js +65 -3
  13. package/dist/computer-use.js.map +1 -1
  14. package/dist/concurrent-shared-world-lab.js +71 -41
  15. package/dist/concurrent-shared-world-lab.js.map +1 -1
  16. package/dist/cua-actor-lab.d.ts +64 -2
  17. package/dist/cua-actor-lab.js +302 -26
  18. package/dist/cua-actor-lab.js.map +1 -1
  19. package/dist/e2b-desktop-executor.d.ts +1 -1
  20. package/dist/e2b-desktop-executor.js +2 -1
  21. package/dist/e2b-desktop-executor.js.map +1 -1
  22. package/dist/e2b-terminal-lab.js +1 -0
  23. package/dist/e2b-terminal-lab.js.map +1 -1
  24. package/dist/feedback.js +55 -1
  25. package/dist/feedback.js.map +1 -1
  26. package/dist/image-evidence.js +36 -22
  27. package/dist/image-evidence.js.map +1 -1
  28. package/dist/init-templates.js +3 -1
  29. package/dist/init-templates.js.map +1 -1
  30. package/dist/lab-config.d.ts +14 -1
  31. package/dist/lab-config.js +6 -2
  32. package/dist/lab-config.js.map +1 -1
  33. package/dist/observer-app.html +37 -0
  34. package/dist/observer-data.d.ts +4 -0
  35. package/dist/observer-data.js +7 -1
  36. package/dist/observer-data.js.map +1 -1
  37. package/dist/observer.d.ts +10 -0
  38. package/dist/observer.js +156 -25
  39. package/dist/observer.js.map +1 -1
  40. package/dist/program.js +5 -5
  41. package/dist/program.js.map +1 -1
  42. package/dist/redaction.js +4 -9
  43. package/dist/redaction.js.map +1 -1
  44. package/dist/run.d.ts +39 -1
  45. package/dist/run.js +45 -3
  46. package/dist/run.js.map +1 -1
  47. package/dist/screenshot-image.d.ts +11 -0
  48. package/dist/screenshot-image.js +26 -0
  49. package/dist/screenshot-image.js.map +1 -0
  50. package/dist/scripted-browser-lab.js +3 -1
  51. package/dist/scripted-browser-lab.js.map +1 -1
  52. package/dist/shared-world-lab.js +16 -2
  53. package/dist/shared-world-lab.js.map +1 -1
  54. package/docs/architecture/observer.md +17 -0
  55. package/docs/contracts/schemas.md +1 -1
  56. package/docs/goals/current.md +16 -3
  57. package/docs/ramp/README.md +1 -1
  58. package/package.json +2 -2
  59. package/dist/observer-assets.d.ts +0 -2
  60. package/dist/observer-assets.js +0 -2949
  61. package/dist/observer-assets.js.map +0 -1
@@ -1,2949 +0,0 @@
1
- // Humanish Observer: client assets (CSS + browser JS).
2
- //
3
- // This file is the redesigned Observer surface ported from the Claude Design
4
- // handoff (HTML/CSS/JS prototype) into the repo's self-contained renderer.
5
- // `observer.ts` injects `observerCss()` into a <style> and `observerClientJs()`
6
- // into a <script>, then hydrates from an embedded `observer-data.v1` JSON blob
7
- // and polls `observer-data.json` (served mode) for live updates.
8
- //
9
- // IMPORTANT: `observerClientJs()` runs verbatim in the browser. It is emitted
10
- // from a TS template literal, so it intentionally avoids backticks, `${`, and
11
- // backslash escapes; literal UTF-8 characters and `String.fromCharCode` are
12
- // used instead. Keep it that way when editing.
13
- export function observerCss() {
14
- return `
15
- /* ============================================================
16
- Humanish Observer · redesign
17
- Design tokens + shell. Dark is default; [data-theme="light"]
18
- on <html> flips the palette. All accent/status colors are
19
- semantic tokens so the whole UI re-themes from one place.
20
- ============================================================ */
21
-
22
- :root {
23
- /* surfaces */
24
- --bg: #0a0b0d;
25
- --surface-0: #0d0f12;
26
- --surface-1: #14171b;
27
- --surface-2: #1a1e23;
28
- --surface-3: #232830;
29
- --stream-void: #050608;
30
-
31
- /* hairlines */
32
- --line: rgba(255, 255, 255, 0.065);
33
- --line-2: rgba(255, 255, 255, 0.11);
34
- --line-3: rgba(255, 255, 255, 0.18);
35
-
36
- /* text */
37
- --text-1: #f2f4f6;
38
- --text-2: #a6aeb6;
39
- --text-3: #6d747d;
40
- --text-4: #464c54;
41
-
42
- /* brand + status */
43
- --accent: #4d7cfe;
44
- --accent-2: #7ea6ff;
45
- --accent-ink: #cdddff;
46
- --cyan: #41c8e6;
47
- --green: #34d399;
48
- --teal: #2dc3a6;
49
- --amber: #f0a92b;
50
- --red: #fb5d52;
51
- --violet: #a78bfa;
52
-
53
- --accent-soft: color-mix(in oklab, var(--accent) 18%, transparent);
54
- --green-soft: color-mix(in oklab, var(--green) 16%, transparent);
55
- --amber-soft: color-mix(in oklab, var(--amber) 16%, transparent);
56
- --red-soft: color-mix(in oklab, var(--red) 16%, transparent);
57
- --cyan-soft: color-mix(in oklab, var(--cyan) 16%, transparent);
58
-
59
- --radius: 10px;
60
- --radius-sm: 7px;
61
- --radius-lg: 16px;
62
- --radius-pill: 999px;
63
-
64
- --shadow-1: 0 1px 2px rgba(0, 0, 0, 0.4);
65
- --shadow-2: 0 8px 30px rgba(0, 0, 0, 0.4);
66
- --shadow-pop: 0 20px 60px rgba(0, 0, 0, 0.55);
67
-
68
- --sans: "Geist", system-ui, -apple-system, "Segoe UI", sans-serif;
69
- --mono: "Geist Mono", ui-monospace, "SF Mono", "JetBrains Mono", Consolas, monospace;
70
-
71
- --ease: cubic-bezier(0.22, 1, 0.36, 1);
72
- --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
73
- --header-h: 54px;
74
- --toolbar-h: 50px;
75
-
76
- --tile-min: 380px;
77
- --accent-color: var(--accent); /* overridable by Tweaks */
78
- }
79
-
80
- html[data-theme="light"] {
81
- --bg: #eef0f3;
82
- --surface-0: #f6f7f9;
83
- --surface-1: #ffffff;
84
- --surface-2: #ffffff;
85
- --surface-3: #eceef2;
86
- --stream-void: #0b0d10;
87
- --line: rgba(15, 20, 30, 0.09);
88
- --line-2: rgba(15, 20, 30, 0.14);
89
- --line-3: rgba(15, 20, 30, 0.22);
90
- --text-1: #14171c;
91
- --text-2: #525a64;
92
- --text-3: #828a94;
93
- --text-4: #aeb5bd;
94
- --shadow-1: 0 1px 2px rgba(20, 28, 40, 0.08);
95
- --shadow-2: 0 8px 30px rgba(20, 28, 40, 0.12);
96
- --shadow-pop: 0 20px 60px rgba(20, 28, 40, 0.22);
97
- }
98
-
99
- * { box-sizing: border-box; }
100
-
101
- html, body {
102
- margin: 0;
103
- height: 100%;
104
- overflow: hidden;
105
- }
106
-
107
- body {
108
- background: var(--bg);
109
- color: var(--text-1);
110
- font-family: var(--sans);
111
- font-size: 13px;
112
- line-height: 1.5;
113
- -webkit-font-smoothing: antialiased;
114
- text-rendering: optimizeLegibility;
115
- }
116
-
117
- button { font-family: inherit; cursor: pointer; color: inherit; background: none; border: none; }
118
- a { color: inherit; text-decoration: none; }
119
-
120
- :focus-visible {
121
- outline: 2px solid var(--accent-color);
122
- outline-offset: 2px;
123
- border-radius: 4px;
124
- }
125
-
126
- ::-webkit-scrollbar { width: 10px; height: 10px; }
127
- ::-webkit-scrollbar-track { background: transparent; }
128
- ::-webkit-scrollbar-thumb { background: var(--line-2); border-radius: 999px; border: 2px solid transparent; background-clip: padding-box; }
129
- ::-webkit-scrollbar-thumb:hover { background: var(--line-3); background-clip: padding-box; }
130
-
131
- .mono { font-family: var(--mono); font-feature-settings: "tnum" 1, "zero" 1; }
132
- .eyebrow {
133
- font-family: var(--mono);
134
- font-size: 9.5px;
135
- letter-spacing: 0.16em;
136
- text-transform: uppercase;
137
- color: var(--text-3);
138
- }
139
-
140
- /* ============================================================ APP SHELL */
141
- .app {
142
- display: flex;
143
- flex-direction: column;
144
- height: 100vh;
145
- height: 100dvh;
146
- position: relative;
147
- }
148
- .hdr, .toolbar { flex: none; }
149
- .stage { flex: 1 1 0; }
150
-
151
- /* top-edge run progress line */
152
- .runline {
153
- position: absolute;
154
- top: 0; left: 0; right: 0;
155
- height: 2px;
156
- z-index: 60;
157
- background: transparent;
158
- pointer-events: none;
159
- }
160
- .runline > span {
161
- display: block;
162
- height: 100%;
163
- background: linear-gradient(90deg, var(--accent-color), var(--accent-2));
164
- box-shadow: 0 0 12px color-mix(in oklab, var(--accent-color) 70%, transparent);
165
- transition: width 900ms var(--ease-out);
166
- border-radius: 0 2px 2px 0;
167
- }
168
- .runline[data-status="failed"] > span { background: linear-gradient(90deg, var(--red), #ff8b82); box-shadow: 0 0 12px var(--red-soft); }
169
- .runline[data-status="passed"] > span,
170
- .runline[data-status="complete"] > span { background: linear-gradient(90deg, var(--green), #7ef0c4); box-shadow: 0 0 12px var(--green-soft); }
171
-
172
- /* ============================================================ HEADER */
173
- .hdr {
174
- display: flex;
175
- align-items: center;
176
- gap: 14px;
177
- height: var(--header-h);
178
- padding: 0 16px;
179
- background: var(--surface-0);
180
- border-bottom: 1px solid var(--line);
181
- position: relative;
182
- z-index: 40;
183
- }
184
- .hdr-brand { display: flex; align-items: center; gap: 10px; flex: none; }
185
- .brand-mark {
186
- width: 26px; height: 26px; border-radius: 8px;
187
- display: grid; place-items: center;
188
- background: radial-gradient(circle at 30% 25%, color-mix(in oklab, var(--accent-color) 55%, transparent), transparent 70%), var(--surface-2);
189
- border: 1px solid var(--line-2);
190
- position: relative;
191
- overflow: hidden;
192
- color: var(--accent-ink);
193
- }
194
- .brand-mark svg { width: 16px; height: 16px; }
195
- .brand-word { font-size: 13px; font-weight: 600; letter-spacing: -0.01em; }
196
- .brand-word b { color: var(--accent-color); font-weight: 600; }
197
-
198
- .hdr-run {
199
- display: flex; flex-direction: column; gap: 1px; min-width: 0;
200
- padding-left: 14px; margin-left: 2px;
201
- border-left: 1px solid var(--line);
202
- }
203
- .hdr-run-title {
204
- font-size: 13.5px; font-weight: 560; color: var(--text-1);
205
- white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 42vw;
206
- letter-spacing: -0.01em;
207
- }
208
- .hdr-run-sub {
209
- display: flex; align-items: center; gap: 7px;
210
- font-size: 11px; color: var(--text-3); min-width: 0;
211
- }
212
- .hdr-run-sub .dot-sep { width: 2.5px; height: 2.5px; border-radius: 50%; background: var(--text-4); flex: none; }
213
- .hdr-persona { color: var(--text-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
214
- .run-chip {
215
- font-family: var(--mono); font-size: 10px; color: var(--text-3);
216
- background: var(--surface-2); border: 1px solid var(--line);
217
- padding: 1px 6px; border-radius: var(--radius-sm); cursor: pointer;
218
- white-space: nowrap; transition: border-color .15s, color .15s;
219
- }
220
- .run-chip:hover { border-color: var(--line-3); color: var(--text-1); }
221
-
222
- .hdr-spacer { flex: 1; }
223
-
224
- /* status pill */
225
- .status-pill {
226
- display: inline-flex; align-items: center; gap: 8px;
227
- height: 30px; padding: 0 12px 0 11px;
228
- border-radius: var(--radius-pill);
229
- background: var(--surface-2);
230
- border: 1px solid var(--line-2);
231
- font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.1em; text-transform: uppercase;
232
- color: var(--text-1); flex: none;
233
- }
234
- .status-pill[data-tone="running"] { background: var(--accent-soft); border-color: color-mix(in oklab, var(--accent) 35%, transparent); color: var(--accent-ink); }
235
- .status-pill[data-tone="passed"], .status-pill[data-tone="complete"] { background: var(--green-soft); border-color: color-mix(in oklab, var(--green) 35%, transparent); color: color-mix(in oklab, var(--green) 70%, white); }
236
- .status-pill[data-tone="blocked"] { background: var(--amber-soft); border-color: color-mix(in oklab, var(--amber) 35%, transparent); color: color-mix(in oklab, var(--amber) 80%, white); }
237
- .status-pill[data-tone="failed"] { background: var(--red-soft); border-color: color-mix(in oklab, var(--red) 35%, transparent); color: color-mix(in oklab, var(--red) 75%, white); }
238
- .status-pill .pct { color: inherit; opacity: .65; }
239
-
240
- /* lane counts */
241
- .lane-counts { display: flex; align-items: center; gap: 2px; flex: none; }
242
- .lane-count {
243
- display: inline-flex; align-items: center; gap: 6px;
244
- padding: 5px 9px; border-radius: var(--radius-sm);
245
- font-family: var(--mono); font-size: 11px; color: var(--text-2);
246
- transition: background .15s;
247
- }
248
- .lane-count:hover { background: var(--surface-2); }
249
- .lane-count b { color: var(--text-1); font-weight: 600; }
250
- .lane-count[data-dim="true"] { opacity: .4; }
251
-
252
- .icon-btn {
253
- width: 32px; height: 32px; border-radius: var(--radius-sm);
254
- display: grid; place-items: center; color: var(--text-2);
255
- border: 1px solid transparent; transition: background .15s, color .15s, border-color .15s; flex: none;
256
- }
257
- .icon-btn:hover { background: var(--surface-2); color: var(--text-1); }
258
- .icon-btn[aria-pressed="true"] { background: var(--surface-3); color: var(--text-1); border-color: var(--line-2); }
259
- .icon-btn svg { width: 17px; height: 17px; }
260
-
261
- .hdr-runs {
262
- display: inline-flex; align-items: center; gap: 7px;
263
- height: 32px; padding: 0 12px; border-radius: var(--radius-sm);
264
- border: 1px solid var(--line-2); color: var(--text-2);
265
- font-size: 12px; transition: background .15s, color .15s; flex: none;
266
- }
267
- .hdr-runs:hover { background: var(--surface-2); color: var(--text-1); }
268
- .hdr-runs svg { width: 15px; height: 15px; }
269
-
270
- /* ============================================================ TOOLBAR */
271
- .toolbar {
272
- display: flex; align-items: center; gap: 10px;
273
- height: var(--toolbar-h); padding: 0 16px;
274
- background: var(--surface-0); border-bottom: 1px solid var(--line);
275
- position: relative; z-index: 30;
276
- overflow-x: auto; overflow-y: hidden;
277
- scrollbar-width: none;
278
- }
279
- .toolbar::-webkit-scrollbar { display: none; }
280
-
281
- .filter-group { display: flex; align-items: center; gap: 3px; flex: none; }
282
- .chip {
283
- display: inline-flex; align-items: center; gap: 6px;
284
- height: 28px; padding: 0 10px; border-radius: var(--radius-pill);
285
- border: 1px solid var(--line); color: var(--text-2);
286
- font-size: 11.5px; white-space: nowrap;
287
- transition: background .15s, color .15s, border-color .15s;
288
- }
289
- .chip:hover { border-color: var(--line-2); color: var(--text-1); }
290
- .chip[aria-pressed="true"] { background: var(--surface-3); color: var(--text-1); border-color: var(--line-2); }
291
- .chip .chip-dot { width: 7px; height: 7px; border-radius: 50%; background: currentColor; flex: none; }
292
- .chip .chip-n { font-family: var(--mono); font-size: 10.5px; color: var(--text-3); }
293
- .chip[aria-pressed="true"] .chip-n { color: var(--text-2); }
294
-
295
- .tb-sep { width: 1px; height: 22px; background: var(--line); flex: none; }
296
- .tb-spacer { flex: 1; min-width: 8px; }
297
-
298
- /* search */
299
- .tb-search {
300
- display: flex; align-items: center; gap: 7px;
301
- height: 28px; padding: 0 10px; border-radius: var(--radius-sm);
302
- border: 1px solid var(--line); color: var(--text-3); flex: none;
303
- min-width: 150px; transition: border-color .15s;
304
- }
305
- .tb-search:focus-within { border-color: var(--line-3); }
306
- .tb-search svg { width: 14px; height: 14px; flex: none; }
307
- .tb-search input {
308
- background: none; border: none; color: var(--text-1); font-family: var(--sans);
309
- font-size: 12px; width: 100%; outline: none;
310
- }
311
- .tb-search input::placeholder { color: var(--text-4); }
312
-
313
- /* segmented control (density, media, view) */
314
- .seg {
315
- display: inline-flex; align-items: center; padding: 2px;
316
- background: var(--surface-1); border: 1px solid var(--line); border-radius: var(--radius-sm); flex: none;
317
- }
318
- .seg button {
319
- display: inline-flex; align-items: center; gap: 6px;
320
- height: 24px; padding: 0 9px; border-radius: 5px;
321
- font-size: 11px; color: var(--text-3); white-space: nowrap;
322
- transition: color .15s, background .15s;
323
- }
324
- .seg button svg { width: 14px; height: 14px; }
325
- .seg button:hover { color: var(--text-1); }
326
- .seg button[aria-pressed="true"] { background: var(--surface-3); color: var(--text-1); box-shadow: var(--shadow-1); }
327
- .seg button:disabled { opacity: .4; cursor: not-allowed; }
328
-
329
- .tb-label { font-size: 10px; letter-spacing: .14em; text-transform: uppercase; color: var(--text-4); font-family: var(--mono); flex: none; }
330
-
331
- /* ============================================================ GRID */
332
- .stage { position: relative; min-height: 0; overflow: hidden; }
333
- .grid-scroll { height: 100%; overflow-y: auto; overflow-x: hidden; padding: 16px; }
334
- .grid {
335
- display: grid;
336
- gap: 14px;
337
- grid-template-columns: repeat(auto-fill, minmax(min(var(--tile-min), 100%), 1fr));
338
- align-content: start; align-items: start;
339
- }
340
-
341
- .tile {
342
- display: flex; flex-direction: column;
343
- background: var(--surface-1);
344
- border: 1px solid var(--line);
345
- border-radius: var(--radius);
346
- overflow: hidden;
347
- cursor: pointer;
348
- position: relative;
349
- transition: border-color .18s var(--ease), transform .18s var(--ease), box-shadow .18s var(--ease);
350
- }
351
- .tile:hover { border-color: var(--line-3); transform: translateY(-2px); box-shadow: var(--shadow-2); }
352
- .tile:hover .tile-open { opacity: 1; transform: none; }
353
- .tile[data-selected="true"] { border-color: color-mix(in oklab, var(--accent) 60%, transparent); box-shadow: 0 0 0 1px color-mix(in oklab, var(--accent) 45%, transparent); }
354
-
355
- .tile-head {
356
- display: flex; align-items: center; gap: 8px;
357
- padding: 0 10px; height: 38px; flex: none;
358
- border-bottom: 1px solid var(--line);
359
- background: var(--surface-1);
360
- }
361
- .tile-idx { font-family: var(--mono); font-size: 11px; color: var(--text-4); flex: none; }
362
- .tile-name { font-size: 15px; font-weight: 520; color: var(--text-1); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 1; min-width: 0; }
363
- .kind-badge {
364
- font-family: var(--mono); font-size: 9px; letter-spacing: .08em; text-transform: uppercase;
365
- padding: 2px 6px; border-radius: 5px; flex: none;
366
- background: var(--surface-3); color: var(--text-2);
367
- }
368
- .kind-badge[data-kind="ui"], .kind-badge[data-kind="browser"] { color: var(--violet); background: color-mix(in oklab, var(--violet) 14%, transparent); }
369
- .kind-badge[data-kind="terminal"] { color: var(--green); background: var(--green-soft); }
370
- .kind-badge[data-kind="tui"] { color: var(--cyan); background: var(--cyan-soft); }
371
- .kind-badge[data-kind="codex-ui"] { color: var(--accent-2); background: var(--accent-soft); }
372
- .tile-dims { font-family: var(--mono); font-size: 9px; color: var(--text-4); flex: none; }
373
- .tile-open {
374
- width: 22px; height: 22px; border-radius: 5px; display: grid; place-items: center;
375
- color: var(--text-2); background: var(--surface-2); border: 1px solid var(--line-2);
376
- opacity: 0; transform: translateX(3px); transition: opacity .15s, transform .15s; flex: none;
377
- }
378
- .tile-open svg { width: 13px; height: 13px; }
379
-
380
- /* status pip */
381
- .pip { width: 8px; height: 8px; border-radius: 50%; flex: none; background: var(--text-4); position: relative; }
382
- .pip[data-status="queued"] { background: var(--text-4); }
383
- .pip[data-status="preparing"]{ background: var(--cyan); }
384
- .pip[data-status="running"] { background: var(--accent); }
385
- .pip[data-status="passed"], .pip[data-status="complete"] { background: var(--green); }
386
- .pip[data-status="contract_proof_only"] { background: var(--teal); }
387
- .pip[data-status="blocked"], .pip[data-status="timed_out"] { background: var(--amber); }
388
- .pip[data-status="failed"] { background: var(--red); }
389
- .pip[data-live="true"]::after {
390
- content: ""; position: absolute; inset: -3px; border-radius: 50%;
391
- background: currentColor; opacity: .35; animation: ping 1.6s var(--ease-out) infinite;
392
- }
393
- .pip[data-status="running"][data-live="true"] { color: var(--accent); }
394
- .pip[data-status="preparing"][data-live="true"] { color: var(--cyan); }
395
-
396
- .tile-surface {
397
- position: relative; background: var(--stream-void);
398
- aspect-ratio: var(--aspect, 16 / 9);
399
- width: 100%; overflow: hidden; flex: none;
400
- }
401
- .tile-foot {
402
- display: flex; align-items: center; gap: 8px;
403
- padding: 0 10px; height: 36px; flex: none;
404
- border-top: 1px solid var(--line); background: var(--surface-1);
405
- }
406
- .tile-foot-text { font-size: 14px; color: var(--text-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 1; min-width: 0; }
407
- .tile-foot-text .now-dot { color: var(--accent); margin-right: 6px; }
408
- .mini-prog { width: 46px; height: 3px; border-radius: 2px; background: var(--line); overflow: hidden; flex: none; }
409
- .mini-prog > span { display: block; height: 100%; background: var(--accent); transition: width .6s var(--ease-out); }
410
- .tile-foot[data-status="failed"] .mini-prog > span { background: var(--red); }
411
- .tile-foot[data-status="blocked"] .mini-prog > span,
412
- .tile-foot[data-status="timed_out"] .mini-prog > span { background: var(--amber); }
413
- .tile-foot[data-status="passed"] .mini-prog > span,
414
- .tile-foot[data-status="complete"] .mini-prog > span,
415
- .tile-foot[data-status="contract_proof_only"] .mini-prog > span { background: var(--green); }
416
-
417
- /* ============================================================ STREAM SURFACES */
418
- .surface-fill { position: absolute; inset: 0; width: 100%; height: 100%; }
419
- .surface-screenshot { object-fit: contain; object-position: top center; }
420
- /* Replay scrubber (#292 first slice): a per-turn keyframe timeline built from persisted frames + trace. */
421
- .replay { position: absolute; inset: 0; display: flex; flex-direction: column; background: #0b0d10; }
422
- .replay-stage { position: relative; flex: 1 1 auto; min-height: 0; }
423
- .replay-badge { position: absolute; top: 6px; right: 8px; z-index: 2; font-size: 9px; text-transform: uppercase; letter-spacing: .04em; color: var(--text-2); background: rgba(0,0,0,.55); border: 1px solid var(--line-2); border-radius: 999px; padding: 2px 7px; }
424
- .live-ended-badge { position: absolute; top: 6px; left: 8px; z-index: 3; font-size: 9px; letter-spacing: .02em; color: #fff; background: rgba(0,0,0,.62); border: 1px solid var(--line-2); border-radius: 999px; padding: 2px 8px; display: inline-flex; align-items: center; gap: 5px; }
425
- .live-ended-badge::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: var(--text-2); }
426
- .live-ended-badge[data-tone="complete"]::before { background: var(--green); }
427
- .live-ended-badge[data-tone="failed"]::before { background: var(--red); }
428
- .live-ended-badge[data-tone="blocked"]::before { background: var(--amber); }
429
- .run-verdict-banner { display: flex; align-items: center; gap: 10px; margin: 6px 14px 0; padding: 8px 12px; border: 1px solid var(--line-2); border-radius: 10px; background: var(--surface-2); font-size: 12px; }
430
- .run-verdict-banner[data-status="complete"] { border-color: color-mix(in oklab, var(--green) 45%, var(--line-2)); }
431
- .run-verdict-banner[data-status="failed"] { border-color: color-mix(in oklab, var(--red) 45%, var(--line-2)); }
432
- .run-verdict-banner .rvb-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--text-2); flex: none; }
433
- .run-verdict-banner[data-status="complete"] .rvb-dot { background: var(--green); }
434
- .run-verdict-banner[data-status="failed"] .rvb-dot { background: var(--red); }
435
- .run-verdict-banner[data-status="blocked"] .rvb-dot { background: var(--amber); }
436
- .replay-bar { flex: 0 0 auto; display: flex; align-items: center; gap: 6px; padding: 6px 8px; border-top: 1px solid var(--line-2); background: var(--surface-2); }
437
- .replay-btn { display: inline-flex; align-items: center; justify-content: center; width: 26px; height: 26px; border-radius: 7px; border: 1px solid var(--line-2); background: transparent; color: var(--text-1); cursor: pointer; }
438
- .replay-btn:hover { background: var(--surface-3); }
439
- .replay-range { flex: 1 1 auto; accent-color: var(--accent-color); cursor: pointer; height: 4px; min-width: 40px; }
440
- .replay-count { flex: 0 0 auto; font-size: 11px; color: var(--text-2); min-width: 44px; text-align: right; }
441
- .replay-info { flex: 0 0 auto; max-height: 32%; overflow-y: auto; padding: 8px 10px; border-top: 1px solid var(--line-2); background: var(--surface-1); font-size: 12px; line-height: 1.5; color: var(--text-1); }
442
- .replay-acts { display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: 6px; }
443
- .replay-act { font-family: var(--mono); font-size: 10px; color: var(--text-2); background: var(--surface-3); border: 1px solid var(--line-2); border-radius: 5px; padding: 2px 6px; }
444
- .replay-narr { white-space: pre-wrap; color: var(--text-1); }
445
- .replay-dim { color: var(--text-2); font-style: italic; }
446
- .live-stream-mount { position: absolute; inset: 0; overflow: hidden; background: #000; }
447
- .live-stream-overlay { position: absolute; overflow: hidden; pointer-events: none; z-index: 2; }
448
- .live-stream-overlay[data-focus="true"] .bw-lab-dock { max-height: 86px; }
449
- .live-stream-overlay[data-focus="true"] .bw-chip-v { max-width: min(420px, 38vw); }
450
-
451
- /* browser/ui mock */
452
- .bw { position: absolute; inset: 0; display: flex; flex-direction: column; background: #0b0d10; }
453
- .bw-chrome {
454
- display: flex; align-items: center; gap: 6px; padding: 0 9px; height: 26px; flex: none;
455
- background: #16181c; border-bottom: 1px solid rgba(255,255,255,.06);
456
- }
457
- .bw-dots { display: flex; gap: 4px; }
458
- .bw-dots i { width: 7px; height: 7px; border-radius: 50%; background: #353a40; }
459
- .bw-url {
460
- flex: 1; height: 16px; border-radius: 4px; background: #0d0f12; border: 1px solid rgba(255,255,255,.06);
461
- display: flex; align-items: center; padding: 0 7px; gap: 5px;
462
- font-family: var(--mono); font-size: 8.5px; color: #6f767e; min-width: 0;
463
- }
464
- .bw-url svg { width: 8px; height: 8px; flex: none; }
465
- .bw-url span { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
466
- .bw-viewport { flex: 1; position: relative; overflow: hidden; background: #fbfcfd; }
467
- .bw-app-wait { position: absolute; inset: 0; display: grid; place-items: center; align-content: center; gap: 8px; background: #fbfcfd; color: #5a626c; text-align: center; padding: 16px; }
468
- .bw-app-wait .wait-spinner { border-color: rgba(20,28,40,.12); border-top-color: var(--accent); }
469
- .bw-lab-dock {
470
- position: absolute; left: 8px; right: 8px; bottom: 8px; z-index: 8;
471
- display: flex; align-items: flex-end; gap: 5px; flex-wrap: wrap;
472
- max-height: 54px; overflow: hidden; pointer-events: none;
473
- }
474
- .bw-lab-chip {
475
- min-width: 0; max-width: 100%; pointer-events: auto;
476
- display: inline-flex; align-items: center; gap: 5px;
477
- padding: 3px 7px; border-radius: 6px;
478
- background: rgba(8,10,13,.78); backdrop-filter: blur(8px);
479
- border: 1px solid rgba(255,255,255,.12);
480
- color: var(--text-2); box-shadow: var(--shadow-1);
481
- }
482
- a.bw-lab-chip:hover { border-color: rgba(255,255,255,.22); color: var(--text-1); }
483
- .bw-lab-chip[data-tone="live"] { color: var(--accent-2); border-color: color-mix(in oklab, var(--accent) 35%, transparent); }
484
- .bw-lab-chip[data-tone="ok"] { color: var(--green); border-color: color-mix(in oklab, var(--green) 34%, transparent); }
485
- .bw-lab-chip[data-tone="warn"] { color: var(--amber); border-color: color-mix(in oklab, var(--amber) 34%, transparent); }
486
- .bw-lab-chip[data-tone="err"] { color: var(--red); border-color: color-mix(in oklab, var(--red) 34%, transparent); }
487
- .bw-chip-k {
488
- flex: none; font-family: var(--mono); font-size: 8.5px; letter-spacing: .08em;
489
- text-transform: uppercase; color: currentColor;
490
- }
491
- .bw-chip-v {
492
- min-width: 0; max-width: min(240px, 42vw);
493
- overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
494
- font-family: var(--mono); font-size: 9px; color: var(--text-2);
495
- }
496
- .focus-stage-area .bw-lab-dock { max-height: 86px; }
497
- .focus-stage-area .bw-chip-v { max-width: min(420px, 38vw); }
498
- .bw-cursor {
499
- position: absolute; width: 16px; height: 16px; z-index: 5; pointer-events: none;
500
- transition: left 1.1s var(--ease), top 1.1s var(--ease);
501
- filter: drop-shadow(0 1px 2px rgba(0,0,0,.4));
502
- }
503
- .bw-click {
504
- position: absolute; width: 26px; height: 26px; border-radius: 50%; z-index: 4;
505
- border: 2px solid var(--accent); margin: -13px 0 0 -13px; pointer-events: none;
506
- animation: clickpulse 1.4s var(--ease-out) infinite;
507
- }
508
-
509
- /* the fake app being driven */
510
- .app-mock { position: absolute; inset: 0; padding: 14px; color: #1a1d22; font-size: 10px; }
511
- .app-mock .am-top { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
512
- .am-logo { display: flex; align-items: center; gap: 6px; font-weight: 700; font-size: 11px; color: #14171c; }
513
- .am-logo i { width: 14px; height: 14px; border-radius: 4px; background: linear-gradient(135deg, var(--accent), var(--violet)); }
514
- .am-nav { display: flex; gap: 10px; color: #8a929b; font-size: 9px; }
515
- .am-h { font-size: 16px; font-weight: 700; letter-spacing: -0.02em; color: #11141a; margin: 6px 0 3px; }
516
- .am-p { color: #6a727b; line-height: 1.45; max-width: 80%; }
517
- .am-field { margin-top: 10px; height: 26px; border-radius: 6px; border: 1px solid #e2e6ea; background: #fff; display: flex; align-items: center; padding: 0 9px; color: #9aa0a8; font-size: 9px; }
518
- .am-field[data-focus="true"] { border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in oklab, var(--accent) 18%, transparent); }
519
- .am-btn { margin-top: 9px; height: 28px; border-radius: 7px; background: #11141a; color: #fff; display: inline-flex; align-items: center; padding: 0 16px; font-size: 10px; font-weight: 600; }
520
- .am-btn[data-variant="accent"] { background: var(--accent); }
521
- .am-cards { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 10px; }
522
- .am-card { border: 1px solid #e6e9ed; border-radius: 8px; padding: 9px; background: #fff; }
523
- .am-card .am-ck { height: 5px; width: 40%; background: #e9edf1; border-radius: 3px; margin-bottom: 6px; }
524
- .am-card .am-cl { height: 4px; width: 80%; background: #f0f3f6; border-radius: 3px; margin-bottom: 4px; }
525
-
526
- /* terminal */
527
- .term { position: absolute; inset: 0; display: flex; flex-direction: column; background: var(--stream-void); font-family: var(--mono); }
528
- .term-bar { display: flex; align-items: center; gap: 7px; padding: 0 10px; height: 24px; flex: none; background: rgba(255,255,255,.03); border-bottom: 1px solid var(--line); }
529
- .term-bar .tprompt { color: var(--green); font-size: 11px; }
530
- .term-bar .ttitle { flex: 1; font-size: 10px; color: var(--text-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
531
- .term-bar .tstat { font-size: 8.5px; letter-spacing: .08em; text-transform: uppercase; color: var(--text-3); }
532
- .term-body { flex: 1; min-height: 0; overflow: hidden; padding: 9px 10px; font-size: 11px; line-height: 1.55; display: flex; flex-direction: column; justify-content: flex-end; }
533
- .focus-stage-area .term-body { justify-content: flex-start; overflow-y: auto; font-size: 12.5px; }
534
- .term-line { display: flex; gap: 9px; white-space: pre-wrap; overflow-wrap: anywhere; animation: line-in .25s var(--ease-out) both; }
535
- .term-num { color: var(--text-4); flex: none; width: 18px; text-align: right; user-select: none; }
536
- .term-txt { color: var(--text-2); }
537
- .term-txt.ok { color: var(--green); }
538
- .term-txt.warn { color: var(--amber); }
539
- .term-txt.err { color: var(--red); }
540
- .term-txt.cmd { color: var(--text-1); }
541
- .term-txt.dim { color: var(--text-3); }
542
- .term-caret { display: inline-block; width: 7px; height: 13px; background: var(--green); vertical-align: middle; animation: blink 1.1s steps(2) infinite; margin-left: 2px; }
543
-
544
- /* codex agent session */
545
- .codex { position: absolute; inset: 0; display: flex; flex-direction: column; background: #0c0e11; }
546
- .codex-bar { display: flex; align-items: center; gap: 7px; padding: 0 10px; height: 26px; flex: none; background: #131519; border-bottom: 1px solid var(--line); font-family: var(--mono); font-size: 9px; color: var(--text-3); }
547
- .codex-bar .cx-spark { color: var(--accent-2); display: inline-flex; }
548
- .codex-body { flex: 1; min-height: 0; overflow: hidden; padding: 11px; display: flex; flex-direction: column; gap: 8px; }
549
- .focus-stage-area .codex-body { overflow-y: auto; }
550
- .cx-msg { max-width: 86%; padding: 7px 10px; border-radius: 10px; font-size: 10.5px; line-height: 1.45; animation: line-in .3s var(--ease-out) both; }
551
- .cx-msg.user { align-self: flex-end; background: var(--accent); color: #fff; border-bottom-right-radius: 3px; }
552
- .cx-msg.agent { align-self: flex-start; background: var(--surface-2); color: var(--text-1); border: 1px solid var(--line); border-bottom-left-radius: 3px; }
553
- .cx-tool { align-self: flex-start; display: inline-flex; align-items: center; gap: 7px; font-family: var(--mono); font-size: 9.5px; color: var(--text-3); padding: 5px 9px; border-radius: 7px; border: 1px solid var(--line); background: var(--surface-1); }
554
- .cx-tool .cx-spin { width: 11px; height: 11px; border-radius: 50%; border: 1.5px solid var(--line-3); border-top-color: var(--accent); animation: spin 1s linear infinite; flex: none; }
555
- .cx-tool svg { width: 12px; height: 12px; color: var(--green); }
556
- .cx-chips { display: flex; flex-wrap: wrap; gap: 5px; min-height: 0; }
557
- .cx-chip {
558
- min-width: 0; max-width: 100%;
559
- display: inline-flex; align-items: center; gap: 5px;
560
- padding: 3px 7px; border-radius: 6px;
561
- border: 1px solid var(--line); background: var(--surface-1);
562
- font-family: var(--mono); font-size: 9px; color: var(--text-2);
563
- }
564
- .cx-chip[data-tone="live"] { color: var(--accent-2); border-color: color-mix(in oklab, var(--accent) 34%, transparent); background: var(--accent-soft); }
565
- .cx-chip[data-tone="ok"] { color: var(--green); border-color: color-mix(in oklab, var(--green) 34%, transparent); background: var(--green-soft); }
566
- .cx-chip[data-tone="warn"] { color: var(--amber); border-color: color-mix(in oklab, var(--amber) 34%, transparent); background: var(--amber-soft); }
567
- .cx-chip[data-tone="err"] { color: var(--red); border-color: color-mix(in oklab, var(--red) 34%, transparent); background: var(--red-soft); }
568
- .cx-chip-k { flex: none; text-transform: uppercase; letter-spacing: .08em; color: currentColor; }
569
- .cx-chip-v { min-width: 0; max-width: min(260px, 46vw); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text-2); }
570
- .cx-lanes { display: flex; flex-direction: column; gap: 7px; min-height: 0; }
571
- .cx-lane {
572
- display: grid; grid-template-columns: 22px minmax(0, 1fr); gap: 8px;
573
- padding: 8px; border-radius: 8px; border: 1px solid var(--line);
574
- background: color-mix(in oklab, var(--surface-1) 88%, black);
575
- animation: line-in .24s var(--ease-out) both;
576
- }
577
- .cx-lane-ic {
578
- width: 22px; height: 22px; border-radius: 6px; display: grid; place-items: center;
579
- background: var(--surface-2); border: 1px solid var(--line); color: var(--text-3);
580
- }
581
- .cx-lane[data-cx-kind="message"] .cx-lane-ic { color: var(--accent-2); }
582
- .cx-lane[data-cx-kind="reasoning"] .cx-lane-ic { color: var(--violet); }
583
- .cx-lane[data-cx-kind="tool"] .cx-lane-ic { color: var(--green); }
584
- .cx-lane[data-cx-kind="file"] .cx-lane-ic { color: var(--cyan); }
585
- .cx-lane[data-cx-kind="command"] .cx-lane-ic { color: var(--amber); }
586
- .cx-lane-main { min-width: 0; display: flex; flex-direction: column; gap: 3px; }
587
- .cx-lane-head { display: flex; align-items: center; gap: 7px; min-width: 0; }
588
- .cx-lane-kind { font-family: var(--mono); font-size: 8.5px; letter-spacing: .08em; text-transform: uppercase; color: var(--text-3); flex: none; }
589
- .cx-lane-title { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 10.5px; color: var(--text-1); }
590
- .cx-lane-body { color: var(--text-2); font-size: 10.5px; line-height: 1.45; overflow-wrap: anywhere; white-space: pre-wrap; }
591
- .cx-lane-meta { font-family: var(--mono); font-size: 8.5px; color: var(--text-4); }
592
- .cx-artifacts { display: flex; flex-wrap: wrap; gap: 5px; padding-top: 2px; }
593
- .cx-artifact {
594
- display: inline-flex; align-items: center; gap: 5px; min-width: 0; max-width: 100%;
595
- padding: 4px 7px; border-radius: 6px; border: 1px solid var(--line);
596
- background: var(--surface-1); color: var(--text-2); font-family: var(--mono); font-size: 9px;
597
- }
598
- a.cx-artifact:hover { border-color: var(--line-3); color: var(--text-1); }
599
- .cx-artifact span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
600
-
601
- /* waiting / proof / empty surface */
602
- .wait { position: absolute; inset: 0; display: grid; place-items: center; background:
603
- radial-gradient(circle at 50% 38%, color-mix(in oklab, var(--accent) 12%, transparent), transparent 42%),
604
- repeating-linear-gradient(0deg, transparent 0 26px, color-mix(in oklab, white 3%, transparent) 26px 27px);
605
- text-align: center; padding: 16px;
606
- }
607
- .wait-inner { display: grid; justify-items: center; gap: 11px; max-width: 80%; }
608
- .wait-spinner { width: 30px; height: 30px; border-radius: 50%; border: 2px solid var(--line-2); border-top-color: var(--accent); animation: spin 1.1s linear infinite; }
609
- .wait-icon { width: 34px; height: 34px; display: grid; place-items: center; border-radius: 9px; background: var(--surface-2); border: 1px solid var(--line-2); }
610
- .wait-icon svg { width: 18px; height: 18px; }
611
- .wait-icon[data-tone="amber"] { color: var(--amber); border-color: color-mix(in oklab, var(--amber) 35%, transparent); }
612
- .wait-icon[data-tone="red"] { color: var(--red); border-color: color-mix(in oklab, var(--red) 35%, transparent); }
613
- .wait-icon[data-tone="green"] { color: var(--green); border-color: color-mix(in oklab, var(--green) 35%, transparent); }
614
- .wait-title { font-family: var(--mono); font-size: 10.5px; letter-spacing: .12em; text-transform: uppercase; color: var(--text-1); }
615
- .wait-sub { font-size: 11px; color: var(--text-3); line-height: 1.5; }
616
-
617
- /* tile live tag overlay */
618
- .live-tag {
619
- position: absolute; top: 8px; right: 8px; z-index: 6;
620
- display: inline-flex; align-items: center; gap: 5px;
621
- padding: 3px 8px; border-radius: var(--radius-pill);
622
- background: rgba(8,10,13,.7); backdrop-filter: blur(8px);
623
- border: 1px solid var(--line-2);
624
- font-family: var(--mono); font-size: 8.5px; letter-spacing: .1em; text-transform: uppercase; color: var(--text-1);
625
- }
626
-
627
- /* ============================================================ FOCUS VIEW */
628
- .focus {
629
- display: grid;
630
- grid-template-columns: minmax(0, var(--rail-w, 188px)) minmax(0, 1fr) minmax(0, var(--side-w, 360px));
631
- height: 100%; min-height: 0;
632
- transition: grid-template-columns .22s var(--ease);
633
- }
634
- .focus[data-rail="collapsed"] { --rail-w: 0px; }
635
- .focus[data-side="collapsed"] { --side-w: 0px; }
636
-
637
- /* left lane rail; focus navigation lives in the toolbar */
638
- .focus-rail {
639
- background: var(--surface-0); border-right: 1px solid var(--line);
640
- display: flex; flex-direction: column; min-height: 0; overflow: hidden;
641
- min-width: 0;
642
- transition: opacity .18s var(--ease), transform .22s var(--ease), border-color .18s var(--ease);
643
- }
644
- .focus[data-rail="collapsed"] .focus-rail { opacity: 0; pointer-events: none; transform: translateX(-10px); border-right-color: transparent; }
645
- .focus-rail-head { display: flex; align-items: center; justify-content: space-between; padding: 12px 14px 8px; }
646
- .focus-rail-list { flex: 1; min-height: 0; overflow-y: auto; padding: 0 8px 12px; display: flex; flex-direction: column; gap: 4px; }
647
- .rail-item {
648
- display: flex; align-items: center; gap: 9px; padding: 8px 9px; border-radius: var(--radius-sm);
649
- text-align: left; width: 100%; transition: background .14s; position: relative;
650
- }
651
- .rail-item:hover { background: var(--surface-2); }
652
- .rail-item[data-selected="true"] { background: var(--surface-3); }
653
- .rail-item[data-selected="true"]::before { content: ""; position: absolute; left: 0; top: 8px; bottom: 8px; width: 2px; border-radius: 2px; background: var(--accent); }
654
- .rail-thumb { width: 40px; height: 26px; border-radius: 5px; background: var(--stream-void); border: 1px solid var(--line); flex: none; overflow: hidden; position: relative; display: grid; place-items: center; color: var(--text-3); }
655
- .rail-meta { min-width: 0; flex: 1; }
656
- .rail-name { font-size: 11.5px; color: var(--text-1); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
657
- .rail-sub { font-family: var(--mono); font-size: 9px; color: var(--text-3); display: flex; align-items: center; gap: 5px; }
658
-
659
- .focus-stage { display: flex; flex-direction: column; min-width: 0; min-height: 0; background: var(--bg); }
660
- .focus-bar {
661
- display: flex; align-items: center; gap: 12px; padding: 0 14px; height: 46px; flex: none;
662
- background: var(--surface-0); border-bottom: 1px solid var(--line);
663
- }
664
- .toolbar.focus-toolbar { overflow-x: auto; gap: 12px; }
665
- .toolbar.focus-toolbar .crumbs { flex: 1 1 auto; }
666
- .toolbar.focus-toolbar .focus-status { flex: none; }
667
- .crumbs { display: flex; align-items: center; gap: 7px; font-size: 12px; color: var(--text-3); min-width: 0; }
668
- .crumbs button { color: var(--text-3); transition: color .15s; white-space: nowrap; }
669
- .crumbs button:hover { color: var(--text-1); }
670
- .crumbs .sep { color: var(--text-4); }
671
- .crumbs .cur { color: var(--text-1); font-weight: 520; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
672
- .crumbs .crumb-rail-toggle { display: inline-grid; place-items: center; width: 26px; height: 26px; border-radius: 6px; border: 1px solid var(--line-2); }
673
- .crumbs .crumb-rail-toggle:hover { background: var(--surface-2); }
674
- .crumbs .crumb-rail-toggle svg { width: 15px; height: 15px; }
675
- .focus-status { display: inline-flex; align-items: center; gap: 7px; padding: 4px 10px; border-radius: var(--radius-pill); font-family: var(--mono); font-size: 9.5px; letter-spacing: .1em; text-transform: uppercase; background: var(--surface-2); border: 1px solid var(--line-2); }
676
- .focus-nav { display: flex; align-items: center; gap: 2px; }
677
- .focus-stepper { font-family: var(--mono); font-size: 10px; color: var(--text-3); padding: 0 4px; }
678
-
679
- .focus-stage-area { flex: 1; min-height: 0; min-width: 0; display: grid; place-items: center; padding: 22px; overflow: auto; position: relative; }
680
- .focus-frame {
681
- position: relative; background: var(--stream-void); border-radius: var(--radius-lg);
682
- border: 1px solid var(--line-2); box-shadow: var(--shadow-pop); overflow: hidden;
683
- aspect-ratio: var(--aspect, 16 / 9);
684
- max-width: 100%; max-height: 100%;
685
- width: auto; height: 100%;
686
- }
687
- .focus-geometry {
688
- position: absolute; z-index: 4; left: 50%; bottom: 3px; transform: translateX(-50%);
689
- display: inline-flex; align-items: center; gap: 7px; max-width: calc(100% - 44px);
690
- height: 19px; padding: 0 8px; border: 1px solid var(--line-2); border-radius: var(--radius-pill);
691
- background: var(--surface-0); box-shadow: var(--shadow-1); color: var(--text-3);
692
- font-family: var(--mono); font-size: 8.5px; letter-spacing: .04em; white-space: nowrap;
693
- }
694
- .focus-geometry strong { color: var(--text-2); font-weight: 500; }
695
- .focus-geometry-sep { color: var(--text-4); }
696
- @supports not (aspect-ratio: 1) { .focus-frame { height: 100%; width: 100%; } }
697
-
698
- /* side panel */
699
- .focus-side {
700
- background: var(--surface-0); border-left: 1px solid var(--line);
701
- display: flex; flex-direction: column; min-height: 0; min-width: 0; overflow: hidden;
702
- transition: opacity .18s var(--ease), transform .22s var(--ease), border-color .18s var(--ease);
703
- }
704
- .focus[data-side="collapsed"] .focus-side { opacity: 0; pointer-events: none; transform: translateX(10px); border-left-color: transparent; }
705
- .side-context { flex: none; padding: 16px; border-bottom: 1px solid var(--line); }
706
- .side-persona-row { display: flex; align-items: center; gap: 10px; }
707
- .side-avatar { width: 34px; height: 34px; border-radius: 9px; display: grid; place-items: center; background: var(--surface-2); border: 1px solid var(--line-2); color: var(--accent-2); font-weight: 600; font-size: 13px; flex: none; }
708
- .side-persona-name { font-size: 15px; font-weight: 600; letter-spacing: -0.01em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
709
- .side-persona-id { font-family: var(--mono); font-size: 10px; color: var(--text-3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
710
- .side-goal { margin-top: 13px; }
711
- .side-goal-text { font-size: 12.5px; color: var(--text-2); margin-top: 4px; line-height: 1.5; }
712
- .side-now { margin-top: 13px; padding: 10px 11px; border-radius: var(--radius-sm); background: var(--surface-1); border: 1px solid var(--line); }
713
- .side-now-row { display: flex; align-items: center; gap: 7px; margin-bottom: 5px; }
714
- .side-now-text { font-size: 12.5px; color: var(--text-1); line-height: 1.45; }
715
-
716
- .side-tabs { display: flex; flex: none; padding: 0 8px; gap: 2px; border-bottom: 1px solid var(--line); overflow-x: auto; scrollbar-width: none; }
717
- .side-tabs::-webkit-scrollbar { display: none; }
718
- .side-tab { display: inline-flex; align-items: center; gap: 6px; padding: 11px 10px; font-size: 11.5px; color: var(--text-3); border-bottom: 2px solid transparent; white-space: nowrap; transition: color .15s; }
719
- .side-tab:hover { color: var(--text-1); }
720
- .side-tab[aria-selected="true"] { color: var(--text-1); border-bottom-color: var(--accent); }
721
- .side-tab .tab-n { font-family: var(--mono); font-size: 9.5px; padding: 1px 5px; border-radius: 999px; background: var(--surface-3); color: var(--text-2); }
722
- .side-body { flex: 1; min-height: 0; overflow-y: auto; }
723
-
724
- .evt { display: grid; grid-template-columns: auto 1fr; gap: 11px; padding: 12px 16px; border-bottom: 1px solid var(--line); }
725
- .evt-rail { display: flex; flex-direction: column; align-items: center; gap: 4px; }
726
- .evt-icon { width: 22px; height: 22px; border-radius: 6px; display: grid; place-items: center; background: var(--surface-2); border: 1px solid var(--line); flex: none; }
727
- .evt-icon svg { width: 12px; height: 12px; color: var(--text-3); }
728
- .evt[data-level="warn"] .evt-icon { color: var(--amber); border-color: color-mix(in oklab, var(--amber) 30%, transparent); }
729
- .evt[data-level="warn"] .evt-icon svg { color: var(--amber); }
730
- .evt[data-level="error"] .evt-icon { color: var(--red); border-color: color-mix(in oklab, var(--red) 30%, transparent); }
731
- .evt[data-level="error"] .evt-icon svg { color: var(--red); }
732
- .evt-conn { flex: 1; width: 1px; background: var(--line); min-height: 8px; }
733
- .evt-text { font-size: 12px; color: var(--text-1); line-height: 1.45; }
734
- .evt-meta { font-family: var(--mono); font-size: 9.5px; color: var(--text-3); margin-top: 4px; letter-spacing: .04em; display: flex; gap: 7px; flex-wrap: wrap; }
735
- .evt-type { color: var(--text-2); }
736
-
737
- .file-row { display: flex; align-items: center; gap: 11px; padding: 11px 16px; border-bottom: 1px solid var(--line); transition: background .14s; }
738
- .file-row:hover { background: var(--surface-1); }
739
- .file-row[data-selected="true"] { background: var(--surface-2); box-shadow: inset 2px 0 0 var(--accent); }
740
- .file-row > button, .file-row > a:not(.file-open) { display: flex; align-items: center; gap: 11px; min-width: 0; flex: 1; text-align: left; }
741
- .file-ic { width: 28px; height: 28px; border-radius: 7px; display: grid; place-items: center; background: var(--surface-2); border: 1px solid var(--line); color: var(--text-2); flex: none; }
742
- .file-ic svg { width: 14px; height: 14px; }
743
- .file-meta { min-width: 0; flex: 1; }
744
- .file-name { font-size: 12.5px; color: var(--text-1); }
745
- .file-path { font-family: var(--mono); font-size: 9.5px; color: var(--text-3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
746
- .file-kind { font-family: var(--mono); font-size: 9px; text-transform: uppercase; letter-spacing: .06em; color: var(--text-3); padding: 2px 7px; border-radius: 5px; background: var(--surface-2); flex: none; }
747
- .file-open { font-family: var(--mono); font-size: 9px; color: var(--accent-2); padding: 3px 6px; border-radius: 5px; border: 1px solid var(--line); flex: none; }
748
- .file-open:hover { background: var(--surface-3); }
749
- .file-inspector { border-bottom: 1px solid var(--line); background: var(--surface-1); }
750
- .meaning-card { margin-top: 12px; padding: 12px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface-1); }
751
- .meaning-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 8px; }
752
- .meaning-score { font-family: var(--mono); font-size: 10px; letter-spacing: .08em; text-transform: uppercase; padding: 4px 8px; border-radius: var(--radius-pill); border: 1px solid var(--line-2); color: var(--text-2); }
753
- .meaning-score[data-status="pass"] { color: color-mix(in oklab, var(--green) 72%, white); background: var(--green-soft); border-color: color-mix(in oklab, var(--green) 35%, transparent); }
754
- .meaning-score[data-status="partial"] { color: color-mix(in oklab, var(--amber) 82%, white); background: var(--amber-soft); border-color: color-mix(in oklab, var(--amber) 35%, transparent); }
755
- .meaning-score[data-status="fail"] { color: color-mix(in oklab, var(--red) 75%, white); background: var(--red-soft); border-color: color-mix(in oklab, var(--red) 35%, transparent); }
756
- .meaning-summary { color: var(--text-2); font-size: 11.5px; line-height: 1.45; }
757
- .meaning-components { display: grid; gap: 6px; margin-top: 10px; }
758
- .meaning-comp { display: grid; grid-template-columns: 18px 1fr auto; gap: 8px; align-items: start; padding: 8px; border-radius: var(--radius-sm); background: var(--surface-0); border: 1px solid var(--line); }
759
- .meaning-comp-dot { width: 8px; height: 8px; border-radius: 999px; margin-top: 5px; background: var(--text-4); }
760
- .meaning-comp[data-status="pass"] .meaning-comp-dot { background: var(--green); }
761
- .meaning-comp[data-status="partial"] .meaning-comp-dot { background: var(--amber); }
762
- .meaning-comp[data-status="fail"] .meaning-comp-dot { background: var(--red); }
763
- .meaning-comp-name { color: var(--text-1); font-weight: 560; font-size: 11.5px; }
764
- .meaning-comp-detail { color: var(--text-3); font-size: 10.5px; line-height: 1.35; margin-top: 1px; }
765
- .meaning-comp-score { font-family: var(--mono); color: var(--text-3); font-size: 10px; }
766
- .fi-head { padding: 14px 16px 12px; border-bottom: 1px solid var(--line); }
767
- .fi-title { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
768
- .fi-title h3 { margin: 0; font-size: 13px; font-weight: 600; letter-spacing: -0.01em; }
769
- .fi-status { font-family: var(--mono); font-size: 9px; text-transform: uppercase; letter-spacing: .08em; padding: 2px 7px; border-radius: 999px; background: var(--surface-3); color: var(--text-2); }
770
- .fi-status[data-status="passed"] { background: var(--green-soft); color: var(--green); }
771
- .fi-status[data-status="needs_review"], .fi-status[data-status="blocked"] { background: var(--amber-soft); color: var(--amber); }
772
- .fi-summary { margin-top: 7px; color: var(--text-2); font-size: 11.5px; line-height: 1.45; }
773
- .fi-section { padding: 12px 16px; border-bottom: 1px solid var(--line); }
774
- .fi-section:last-child { border-bottom: 0; }
775
- .fi-section-title { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 8px; }
776
- .fi-section-title span:first-child { font-family: var(--mono); font-size: 9px; letter-spacing: .14em; text-transform: uppercase; color: var(--text-3); }
777
- .fi-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 7px; }
778
- .fi-stat { padding: 8px; border: 1px solid var(--line); border-radius: 7px; background: var(--surface-0); min-width: 0; }
779
- .fi-stat-k { font-family: var(--mono); font-size: 8.5px; color: var(--text-3); text-transform: uppercase; letter-spacing: .08em; }
780
- .fi-stat-v { margin-top: 2px; font-size: 13px; font-weight: 600; color: var(--text-1); overflow-wrap: anywhere; }
781
- .fi-check { display: grid; grid-template-columns: auto 1fr; gap: 8px; padding: 8px 0; border-top: 1px solid var(--line); }
782
- .fi-check:first-child { border-top: 0; padding-top: 0; }
783
- .fi-dot { width: 18px; height: 18px; border-radius: 6px; display: grid; place-items: center; background: var(--surface-2); border: 1px solid var(--line); color: var(--text-3); }
784
- .fi-check[data-ok="true"] .fi-dot { color: var(--green); border-color: color-mix(in oklab, var(--green) 30%, transparent); background: var(--green-soft); }
785
- .fi-check[data-ok="false"] .fi-dot { color: var(--amber); border-color: color-mix(in oklab, var(--amber) 30%, transparent); background: var(--amber-soft); }
786
- .fi-check-name { font-size: 12px; color: var(--text-1); }
787
- .fi-check-detail { margin-top: 2px; font-size: 11px; color: var(--text-3); line-height: 1.4; }
788
- .fi-tree { max-height: 260px; overflow: auto; border: 1px solid var(--line); border-radius: 7px; background: var(--surface-0); padding: 7px 0; }
789
- .fi-tree-row { display: grid; grid-template-columns: auto 1fr auto; gap: 7px; padding: 3px 9px; font-family: var(--mono); font-size: 9.5px; color: var(--text-2); }
790
- .fi-tree-row[data-type="directory"] { color: var(--accent-2); }
791
- .fi-tree-size { color: var(--text-4); }
792
- .fi-pre { margin: 0; max-height: 320px; overflow: auto; white-space: pre-wrap; overflow-wrap: anywhere; font-family: var(--mono); font-size: 10px; line-height: 1.45; background: var(--surface-0); border: 1px solid var(--line); border-radius: 7px; padding: 10px; color: var(--text-2); }
793
- .fi-preview + .fi-preview { margin-top: 10px; }
794
- .fi-preview-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 5px; font-family: var(--mono); font-size: 9.5px; color: var(--text-3); }
795
- .fi-load { padding: 24px 16px; color: var(--text-3); text-align: center; }
796
-
797
- .tab-empty { padding: 36px 24px; text-align: center; color: var(--text-3); font-size: 12px; }
798
- .log-panel { height: 100%; min-height: 0; display: flex; flex-direction: column; background: var(--stream-void); }
799
- .log-scroll { flex: 1; min-height: 0; overflow-y: auto; padding: 12px 14px 18px; font-family: var(--mono); font-size: 11px; line-height: 1.55; }
800
- .log-section + .log-section { margin-top: 18px; padding-top: 12px; border-top: 1px solid var(--line); }
801
- .log-section-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 7px; color: var(--text-3); }
802
- .log-section-title { font-size: 9px; letter-spacing: .14em; text-transform: uppercase; }
803
- .log-section-count { font-size: 9px; color: var(--text-4); }
804
- .log-row { display: grid; grid-template-columns: 34px minmax(0, 1fr); gap: 10px; }
805
- .log-row + .log-row { margin-top: 1px; }
806
- .log-num { color: var(--text-4); text-align: right; user-select: none; }
807
- .log-txt { color: var(--text-2); white-space: pre-wrap; overflow-wrap: anywhere; }
808
- .log-txt.ok { color: var(--green); }
809
- .log-txt.warn { color: var(--amber); }
810
- .log-txt.err { color: var(--red); }
811
- .log-txt.cmd { color: var(--text-1); }
812
- .log-txt.dim { color: var(--text-3); }
813
-
814
- /* ============================================================ HISTORY DRAWER */
815
- .scrim { position: fixed; inset: 0; background: rgba(4,6,9,.5); backdrop-filter: blur(2px); z-index: 70; animation: fade-in .2s ease; }
816
- html[data-theme="light"] .scrim { background: rgba(20,28,40,.32); }
817
- .drawer {
818
- position: fixed; top: 0; bottom: 0; right: 0; z-index: 71;
819
- width: min(420px, 100vw); background: var(--surface-0);
820
- border-left: 1px solid var(--line-2); box-shadow: var(--shadow-pop);
821
- display: flex; flex-direction: column; animation: slide-in .28s var(--ease);
822
- }
823
- .drawer-head { display: flex; align-items: flex-start; justify-content: space-between; padding: 18px 18px 14px; border-bottom: 1px solid var(--line); }
824
- .drawer-head h2 { margin: 4px 0 0; font-size: 18px; font-weight: 600; letter-spacing: -0.01em; }
825
- .drawer-list { flex: 1; min-height: 0; overflow-y: auto; padding: 8px; }
826
- .run-row { display: grid; grid-template-columns: auto 1fr auto; gap: 4px 11px; align-items: center; padding: 12px; border-radius: var(--radius); transition: background .14s; width: 100%; text-align: left; }
827
- .run-row:hover { background: var(--surface-1); }
828
- .run-row[data-active="true"] { background: var(--surface-2); box-shadow: inset 2px 0 0 var(--accent); }
829
- .run-row .pip { grid-row: span 2; }
830
- .run-row-id { font-family: var(--mono); font-size: 11.5px; color: var(--text-1); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
831
- .run-row-meta { font-family: var(--mono); font-size: 9.5px; color: var(--text-3); }
832
- .run-row-stat { font-family: var(--mono); font-size: 9px; text-transform: uppercase; letter-spacing: .08em; color: var(--text-2); grid-row: span 2; justify-self: end; }
833
-
834
- /* ============================================================ POPOVER (run details) */
835
- .pop { position: absolute; z-index: 55; top: calc(var(--header-h) - 4px); left: 16px; width: 300px;
836
- background: var(--surface-1); border: 1px solid var(--line-2); border-radius: var(--radius); box-shadow: var(--shadow-pop);
837
- padding: 6px; animation: pop-in .16s var(--ease-out); }
838
- .pop-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 8px 9px; border-radius: var(--radius-sm); }
839
- .pop-row + .pop-row { border-top: 1px solid var(--line); }
840
- .pop-k { font-size: 11px; color: var(--text-3); }
841
- .pop-v { font-family: var(--mono); font-size: 11px; color: var(--text-1); text-align: right; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 190px; }
842
- .pop-v .tag { padding: 1px 6px; border-radius: 5px; background: var(--surface-3); display: inline-flex; align-items: center; gap: 5px; }
843
- .pop-v .tag[data-tone="green"] { color: var(--green); background: var(--green-soft); }
844
-
845
- /* ============================================================ TWEAKS POPOVER */
846
- .tweaks-pop { position: absolute; z-index: 56; top: calc(var(--header-h) - 4px); right: 16px; width: 264px;
847
- background: var(--surface-1); border: 1px solid var(--line-2); border-radius: var(--radius); box-shadow: var(--shadow-pop);
848
- padding: 12px; animation: pop-in .16s var(--ease-out); }
849
- .tw-sec { font-family: var(--mono); font-size: 9px; letter-spacing: .16em; text-transform: uppercase; color: var(--text-4); margin: 10px 2px 7px; }
850
- .tw-sec:first-child { margin-top: 2px; }
851
- .tw-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 8px; }
852
- .tw-label { font-size: 12px; color: var(--text-2); }
853
- .tw-seg { display: inline-flex; padding: 2px; background: var(--surface-0); border: 1px solid var(--line); border-radius: var(--radius-sm); }
854
- .tw-seg button { height: 22px; padding: 0 8px; border-radius: 5px; font-size: 10.5px; color: var(--text-3); text-transform: capitalize; }
855
- .tw-seg button[aria-pressed="true"] { background: var(--surface-3); color: var(--text-1); }
856
- .tw-swatches { display: inline-flex; gap: 6px; }
857
- .tw-swatch { width: 18px; height: 18px; border-radius: 50%; border: 2px solid transparent; }
858
- .tw-swatch[aria-pressed="true"] { border-color: var(--text-1); }
859
-
860
- /* ============================================================ EMPTY STATE (no matches) */
861
- .grid-empty { grid-column: 1 / -1; display: grid; place-items: center; padding: 60px 20px; text-align: center; gap: 12px; color: var(--text-3); }
862
- .grid-empty .ge-icon { width: 44px; height: 44px; border-radius: 12px; display: grid; place-items: center; background: var(--surface-1); border: 1px solid var(--line-2); color: var(--text-3); }
863
- .grid-empty h3 { margin: 0; font-size: 14px; color: var(--text-1); font-weight: 560; }
864
- .grid-empty button.linklike { color: var(--accent-2); }
865
-
866
- /* ============================================================ RESPONSIVE */
867
- @media (max-width: 1100px) {
868
- :root { --side-w: 320px; }
869
- .hdr-run-title { max-width: 32vw; }
870
- .lane-count .lc-label { display: none; }
871
- }
872
- @media (max-width: 860px) {
873
- .focus { display: block; height: 100%; min-height: 0; overflow-y: auto; }
874
- .focus-rail { display: none; }
875
- .focus[data-rail="collapsed"] .focus-rail { display: none; }
876
- .focus-stage { min-height: auto; }
877
- .focus-stage-area {
878
- display: block; min-height: min(60vh, 560px); height: auto;
879
- padding: 14px; overflow: visible;
880
- }
881
- .focus-frame {
882
- width: 100%; height: auto; max-height: none;
883
- min-height: 280px;
884
- }
885
- .focus-geometry {
886
- position: relative; left: auto; bottom: auto; transform: none;
887
- width: max-content; max-width: 100%; margin: 8px auto 0; overflow-x: auto;
888
- }
889
- .focus-side {
890
- position: relative; left: auto; right: auto; bottom: auto; top: auto; z-index: auto;
891
- min-height: 360px; max-height: none; border-left: none; border-top: 1px solid var(--line-2);
892
- border-radius: 0; box-shadow: none; transform: none;
893
- }
894
- .focus[data-side="collapsed"] .focus-side { display: none; }
895
- .focus-side[data-sheet="open"], .focus-side[data-sheet="closed"] { transform: none; }
896
- .sheet-grip { display: none; }
897
- }
898
- .sheet-grip { display: none; align-items: center; justify-content: center; gap: 8px; height: 40px; flex: none; border-bottom: 1px solid var(--line); cursor: grab; }
899
- .sheet-grip::before { content: ""; width: 36px; height: 4px; border-radius: 2px; background: var(--line-3); }
900
-
901
- @media (max-width: 720px) {
902
- :root { --header-h: 50px; }
903
- .brand-word, .hdr-runs span { display: none; }
904
- .hdr-run { padding-left: 10px; }
905
- .hdr-run-title { max-width: 40vw; font-size: 12.5px; }
906
- .lane-counts { display: none; }
907
- .run-chip { display: none; }
908
- .toolbar { gap: 7px; }
909
- .tb-search { min-width: 0; width: 130px; }
910
- .tb-label { display: none; }
911
- .grid-scroll { padding: 12px; }
912
- .grid { gap: 11px; grid-template-columns: 1fr; }
913
- .seg.density-seg button span { display: none; }
914
- }
915
- @media (max-width: 480px) {
916
- .seg.media-seg button span, .seg.view-seg button span { display: none; }
917
- }
918
-
919
- /* ============================================================ KEYFRAMES */
920
- @keyframes ping { 0% { transform: scale(.6); opacity: .5; } 100% { transform: scale(2.4); opacity: 0; } }
921
- @keyframes spin { to { transform: rotate(360deg); } }
922
- @keyframes blink { 0%, 50% { opacity: 1; } 50.01%, 100% { opacity: 0; } }
923
- @keyframes tile-in { from { opacity: 0; transform: translateY(8px) scale(.99); } to { opacity: 1; transform: none; } }
924
- @keyframes line-in { from { transform: translateY(4px); } to { transform: none; } }
925
- @keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }
926
- @keyframes slide-in { from { transform: translateX(20px); opacity: 0; } to { transform: none; opacity: 1; } }
927
- @keyframes pop-in { from { transform: translateY(-6px) scale(.98); opacity: 0; } to { transform: none; opacity: 1; } }
928
- @keyframes clickpulse { 0% { transform: scale(.5); opacity: .9; } 100% { transform: scale(1.5); opacity: 0; } }
929
- @keyframes console-up { from { opacity: 0; } to { opacity: 1; } }
930
-
931
- @media (prefers-reduced-motion: reduce) {
932
- *, *::before, *::after { animation-duration: .001ms !important; animation-iteration-count: 1 !important; transition-duration: .001ms !important; }
933
- }
934
- html[data-motion="reduced"] *, html[data-motion="reduced"] *::before, html[data-motion="reduced"] *::after {
935
- animation-duration: .001ms !important; animation-iteration-count: 1 !important; transition-duration: .001ms !important;
936
- }
937
-
938
- /* ============================================================ DROPDOWN FILTERS */
939
- .dd { position: relative; flex: none; }
940
- .dd-trigger {
941
- display: inline-flex; align-items: center; gap: 7px;
942
- height: 30px; padding: 0 9px 0 11px; border-radius: var(--radius-sm);
943
- border: 1px solid var(--line-2); color: var(--text-2); font-size: 12px;
944
- transition: background .15s, color .15s, border-color .15s; white-space: nowrap;
945
- }
946
- .dd-trigger:hover { background: var(--surface-2); color: var(--text-1); }
947
- .dd-trigger[data-active="true"] { background: var(--surface-3); color: var(--text-1); border-color: var(--line-3); }
948
- .dd-trigger svg:last-child { color: var(--text-3); }
949
- .dd-trigger-inner { display: inline-flex; align-items: center; gap: 7px; }
950
- .dd-badge {
951
- min-width: 16px; height: 16px; padding: 0 4px; border-radius: 999px;
952
- background: var(--accent-color); color: #fff; font-family: var(--mono); font-size: 9.5px;
953
- display: inline-grid; place-items: center; font-weight: 600;
954
- }
955
- .dd-menu {
956
- position: fixed; z-index: 90;
957
- background: var(--surface-1); border: 1px solid var(--line-2); border-radius: var(--radius);
958
- box-shadow: var(--shadow-pop); padding: 5px; animation: pop-in .14s var(--ease-out);
959
- }
960
- .dd-row {
961
- display: flex; align-items: center; gap: 9px; width: 100%; padding: 8px 9px;
962
- border-radius: var(--radius-sm); text-align: left; color: var(--text-2); font-size: 12.5px;
963
- transition: background .12s;
964
- }
965
- .dd-row:hover { background: var(--surface-2); color: var(--text-1); }
966
- .dd-row-all { color: var(--text-1); }
967
- .dd-row-label { flex: 1; }
968
- .dd-row-n { color: var(--text-3); font-size: 11px; }
969
- .dd-dot { width: 8px; height: 8px; border-radius: 50%; flex: none; }
970
- .dd-check {
971
- width: 16px; height: 16px; border-radius: 5px; border: 1px solid var(--line-3);
972
- display: grid; place-items: center; flex: none; color: #fff; transition: background .12s, border-color .12s;
973
- }
974
- .dd-check[data-on="true"] { background: var(--accent-color); border-color: var(--accent-color); }
975
- .dd-check svg { width: 11px; height: 11px; }
976
- .dd-sep { height: 1px; background: var(--line); margin: 5px 4px; }
977
-
978
- .tb-result { font-family: var(--mono); font-size: 11px; color: var(--text-3); flex: none; padding-left: 2px; }
979
- .lane-groups { display: inline-flex; align-items: center; gap: 5px; flex: none; min-width: 0; }
980
- .lane-group-chip {
981
- display: inline-flex; align-items: center; gap: 5px;
982
- height: 24px; padding: 0 8px; border-radius: var(--radius-pill);
983
- border: 1px solid var(--line); color: var(--text-2); background: var(--surface-1);
984
- font-size: 10.5px; white-space: nowrap; max-width: 170px;
985
- }
986
- .lane-group-k { color: var(--text-4); font-family: var(--mono); }
987
- .lane-group-v { color: var(--text-2); overflow: hidden; text-overflow: ellipsis; }
988
- .lane-group-n { color: var(--text-3); font-family: var(--mono); }
989
-
990
- /* compact icon-only segmented controls on the right */
991
- .seg.view-seg button, .seg.media-seg button, .seg.density-seg button { width: 30px; padding: 0; justify-content: center; }
992
-
993
- /* ============================================================ STATUS INDICATOR (header) */
994
- .si-badges { display: inline-flex; align-items: center; gap: 4px; flex: none; }
995
- .si-badge {
996
- display: inline-flex; align-items: center; gap: 6px; height: 28px; padding: 0 9px;
997
- border-radius: var(--radius-pill); border: 1px solid transparent; transition: background .15s, border-color .15s;
998
- }
999
- .si-badge:hover { background: var(--surface-2); }
1000
- .si-badge[aria-pressed="true"] { background: var(--surface-3); border-color: var(--line-2); }
1001
- .si-dot { width: 9px; height: 9px; border-radius: 50%; flex: none; }
1002
- .si-n { font-size: 12px; color: var(--text-1); font-weight: 600; }
1003
-
1004
- .si-bar { display: inline-flex; align-items: center; gap: 9px; flex: none; min-width: 140px; }
1005
- .si-bar-track { display: flex; height: 8px; flex: 1; border-radius: 999px; overflow: hidden; background: var(--surface-3); gap: 2px; }
1006
- .si-bar-seg { min-width: 4px; cursor: pointer; transition: filter .15s; }
1007
- .si-bar-seg:hover { filter: brightness(1.2); }
1008
- .si-bar-label { font-size: 11px; color: var(--text-2); }
1009
-
1010
- .si-ring { display: inline-flex; align-items: center; gap: 8px; padding: 2px 6px 2px 2px; border-radius: var(--radius-pill); transition: background .15s; }
1011
- .si-ring:hover { background: var(--surface-2); }
1012
- .si-ring-meta { display: flex; flex-direction: column; line-height: 1.1; align-items: flex-start; }
1013
- .si-ring-pct { font-size: 12px; color: var(--text-1); font-weight: 600; }
1014
- .si-ring-issue { font-size: 9.5px; }
1015
-
1016
- /* ============================================================ RUN CONSOLE */
1017
- .console {
1018
- display: flex; flex-direction: column; min-height: 0; flex: none;
1019
- background: var(--surface-0); border-top: 1px solid var(--line-2);
1020
- box-shadow: 0 -10px 30px rgba(0,0,0,.25); animation: console-up .26s var(--ease);
1021
- }
1022
- .console-head { display: flex; align-items: center; gap: 11px; padding: 0 12px; height: 38px; flex: none; border-bottom: 1px solid var(--line); }
1023
- .console-title { display: inline-flex; align-items: center; gap: 8px; font-size: 12px; color: var(--text-1); font-weight: 520; }
1024
- .console-title svg { color: var(--text-3); }
1025
- .console-cmd { font-size: 10.5px; color: var(--text-3); background: var(--surface-2); padding: 2px 7px; border-radius: 5px; }
1026
- .console-live { display: inline-flex; align-items: center; gap: 6px; font-family: var(--mono); font-size: 9.5px; letter-spacing: .08em; text-transform: uppercase; color: var(--accent-2); }
1027
- .console-meta { font-size: 10.5px; color: var(--text-3); }
1028
- .console-body { flex: 1; min-height: 0; overflow-y: auto; padding: 8px 12px; font-size: 11.5px; line-height: 1.6; }
1029
- .console-line { display: flex; gap: 12px; }
1030
- .console-t { color: var(--text-4); flex: none; user-select: none; }
1031
- .console-txt { color: var(--text-2); overflow-wrap: anywhere; }
1032
- .console-txt.info { color: var(--text-2); }
1033
- .console-txt.ok { color: var(--green); }
1034
- .console-txt.warn { color: var(--amber); }
1035
- .console-txt.err { color: var(--red); }
1036
- .console-txt.dim { color: var(--text-3); }
1037
-
1038
- @media (max-width: 720px) {
1039
- .tb-result { display: none; }
1040
- .lane-groups { display: none; }
1041
- .dd-trigger-inner span { max-width: 64px; overflow: hidden; text-overflow: ellipsis; }
1042
- .console { height: 200px !important; }
1043
- }
1044
-
1045
- /* ============================================================ STATUS BAR (persistent footer) */
1046
- .statusbar {
1047
- display: flex; align-items: center; gap: 12px; flex: none;
1048
- height: 30px; padding: 0 12px;
1049
- background: var(--surface-0); border-top: 1px solid var(--line-2);
1050
- font-size: 11.5px; position: relative; z-index: 25;
1051
- }
1052
- .sb-status { display: inline-flex; align-items: center; gap: 7px; flex: none; }
1053
- .sb-status-label { font-size: 9.5px; letter-spacing: .1em; text-transform: uppercase; color: var(--text-2); }
1054
- .sb-status[data-tone="running"] .sb-status-label { color: var(--accent-2); }
1055
- .sb-status[data-tone="failed"] .sb-status-label { color: var(--red); }
1056
- .sb-status[data-tone="blocked"] .sb-status-label { color: var(--amber); }
1057
- .sb-status[data-tone="complete"] .sb-status-label { color: var(--green); }
1058
- .sb-pct { color: var(--text-3); font-size: 11px; }
1059
- .sb-prog { width: 90px; height: 4px; border-radius: 999px; background: var(--surface-3); overflow: hidden; flex: none; }
1060
- .sb-prog > span { display: block; height: 100%; background: var(--accent-color); transition: width .6s var(--ease-out); }
1061
- .sb-prog > span[data-tone="failed"] { background: var(--red); }
1062
- .sb-prog > span[data-tone="complete"] { background: var(--green); }
1063
- .sb-prog > span[data-tone="blocked"] { background: var(--amber); }
1064
- .sb-counts { display: inline-flex; align-items: center; gap: 8px; flex: none; }
1065
- .sb-count { display: inline-flex; align-items: center; gap: 5px; color: var(--text-2); }
1066
- .sb-count .si-dot { width: 7px; height: 7px; border-radius: 50%; }
1067
- .sb-count-number, .sb-count-label { white-space: nowrap; }
1068
- .sb-run { color: var(--text-4); font-size: 10.5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
1069
-
1070
- .sb-console {
1071
- display: inline-flex; align-items: center; gap: 8px; flex: none; max-width: 52vw;
1072
- height: 30px; padding: 0 4px 0 11px; color: var(--text-2);
1073
- border-left: 1px solid var(--line); transition: color .15s, background .15s;
1074
- }
1075
- .sb-console:hover { color: var(--text-1); background: var(--surface-1); }
1076
- .sb-console[aria-expanded="true"] { color: var(--text-1); background: var(--surface-2); }
1077
- .sb-console svg:first-child { color: var(--text-3); }
1078
- .sb-console-label { font-size: 11.5px; flex: none; }
1079
- .sb-console-peek { display: inline-flex; align-items: center; gap: 8px; min-width: 0; padding-left: 8px; border-left: 1px solid var(--line); }
1080
- .sb-peek-t { color: var(--text-4); flex: none; font-size: 10px; }
1081
- .sb-peek-txt { color: var(--text-3); font-size: 10.5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 30vw; }
1082
- .sb-peek-txt.ok { color: color-mix(in oklab, var(--green) 80%, var(--text-2)); }
1083
- .sb-peek-txt.warn { color: color-mix(in oklab, var(--amber) 85%, var(--text-2)); }
1084
- .sb-peek-txt.err { color: color-mix(in oklab, var(--red) 85%, var(--text-2)); }
1085
- .sb-chev { display: inline-grid; place-items: center; color: var(--text-3); transition: transform .2s var(--ease); }
1086
- .sb-chev.open { transform: rotate(180deg); }
1087
- /* when docked above the bar, drop the console's bottom rounding/shadow seam */
1088
- .console + .statusbar { border-top-color: var(--line); }
1089
-
1090
- @media (max-width: 1024px) {
1091
- .sb-count-label { display: none; }
1092
- }
1093
- @media (max-width: 900px) {
1094
- .sb-console-peek { display: none; }
1095
- }
1096
- @media (max-width: 720px) {
1097
- .sb-counts, .sb-run, .sb-prog { display: none; }
1098
- .sb-console-label { display: none; }
1099
- .sb-peek-txt { max-width: 44vw; }
1100
- }
1101
- @media (max-width: 480px) {
1102
- .sb-console-peek { display: none; }
1103
- }
1104
- `;
1105
- }
1106
- export function observerClientJs() {
1107
- return `
1108
- (function () {
1109
- "use strict";
1110
-
1111
- // ---------------------------------------------------------------- hydrate
1112
- var DATA_FILE = "observer-data.json";
1113
- var REFRESH_MS = 5000;
1114
- var dataEl = document.getElementById("observer-data");
1115
- var currentData = null;
1116
- try { currentData = JSON.parse((dataEl && dataEl.textContent) || "null"); } catch (e) { currentData = null; }
1117
- if (!currentData || typeof currentData !== "object") currentData = {};
1118
- if (!currentData.run) currentData.run = {};
1119
- if (!currentData.streams) currentData.streams = [];
1120
- if (!currentData.events) currentData.events = [];
1121
- if (!currentData.laneGroups) currentData.laneGroups = [];
1122
-
1123
- var app = document.getElementById("app");
1124
- var historyIndex = null;
1125
- var refreshTimer = null;
1126
- var historyTimer = null;
1127
- var openDd = null;
1128
- var NL = String.fromCharCode(10);
1129
- var CR = String.fromCharCode(13);
1130
- var TIMES = String.fromCharCode(215);
1131
-
1132
- // ---------------------------------------------------------------- prefs
1133
- function readPref(key, fallback) {
1134
- try { var raw = window.localStorage.getItem("humanish-observer:" + key); return raw == null ? fallback : JSON.parse(raw); }
1135
- catch (e) { return fallback; }
1136
- }
1137
- function writePref(key, value) {
1138
- try { window.localStorage.setItem("humanish-observer:" + key, JSON.stringify(value)); } catch (e) {}
1139
- }
1140
-
1141
- function focusFromHash() {
1142
- var h = String(window.location.hash || "");
1143
- if (h.indexOf("focus=") < 0) return null;
1144
- try { return decodeURIComponent(h.split("focus=")[1].split("&")[0]); } catch (e) { return null; }
1145
- }
1146
-
1147
- // ---------------------------------------------------------------- state
1148
- var initialFocusId = focusFromHash();
1149
- var S = {
1150
- view: initialFocusId ? "focus" : "grid",
1151
- focusedId: initialFocusId,
1152
- statusSel: [],
1153
- kindSel: [],
1154
- query: "",
1155
- media: "live",
1156
- historyOpen: false,
1157
- detailsOpen: false,
1158
- tweaksOpen: false,
1159
- consoleOpen: false,
1160
- filePath: null,
1161
- railCollapsed: !!readPref("railCollapsed", false),
1162
- sideCollapsed: !!readPref("sideCollapsed", false),
1163
- sheetOpen: false,
1164
- tab: "logs",
1165
- theme: readPref("theme", "dark"),
1166
- accent: readPref("accent", "#4d7cfe"),
1167
- density: readPref("density", "comfortable"),
1168
- statusViz: readPref("statusViz", "badges"),
1169
- motion: readPref("motion", "full")
1170
- };
1171
- var artifactCache = {};
1172
- // Replay-scrubber state: per-stream current frame index + auto-advance timer. Kept OUTSIDE the S
1173
- // render-state so a data-poll re-render preserves the scrub position; indices survive because
1174
- // render() reads scrubIndexFor() (which defaults to the last frame — also fixing the "finished lane
1175
- // cuts to black" symptom). Timers are cleared on every render() so playback never orphans.
1176
- var scrubState = {};
1177
- var scrubTimers = {};
1178
- var liveStreamFrames = {};
1179
- var liveStreamHost = null;
1180
- var liveStreamLayoutRaf = null;
1181
-
1182
- // ---------------------------------------------------------------- tone / labels
1183
- var TONE = {
1184
- queued: "queued", preparing: "running", running: "running",
1185
- passed: "complete", complete: "complete", contract_proof_only: "complete",
1186
- blocked: "blocked", timed_out: "blocked", failed: "failed"
1187
- };
1188
- var STATUS_LABEL = {
1189
- queued: "Queued", preparing: "Preparing", running: "Running", passed: "Passed",
1190
- complete: "Complete", contract_proof_only: "Proof only", blocked: "Blocked",
1191
- timed_out: "Timed out", failed: "Failed"
1192
- };
1193
- function tone(s) { return TONE[s] || "queued"; }
1194
- function statusLabel(s) { return STATUS_LABEL[s] || s || "Unknown"; }
1195
- function kindGroup(k) { return k === "browser" ? "ui" : k; }
1196
-
1197
- // ---------------------------------------------------------------- esc / icons
1198
- function esc(v) {
1199
- return String(v == null ? "" : v)
1200
- .replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1201
- }
1202
- function pad2(n) { return (n < 10 ? "0" : "") + n; }
1203
-
1204
- var P = ' fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"';
1205
- var ICONS = {
1206
- grid: '<rect x="3" y="3" width="7" height="7" rx="1.4"' + P + '/><rect x="14" y="3" width="7" height="7" rx="1.4"' + P + '/><rect x="3" y="14" width="7" height="7" rx="1.4"' + P + '/><rect x="14" y="14" width="7" height="7" rx="1.4"' + P + '/>',
1207
- focus: '<rect x="3" y="4" width="18" height="16" rx="2"' + P + '/><path d="M3 9h18"' + P + '/>',
1208
- expand: '<path d="M9 4H5a1 1 0 0 0-1 1v4M15 4h4a1 1 0 0 1 1 1v4M9 20H5a1 1 0 0 1-1-1v-4M15 20h4a1 1 0 0 0 1-1v-4"' + P + '/>',
1209
- search: '<circle cx="11" cy="11" r="7"' + P + '/><path d="m20 20-3-3"' + P + '/>',
1210
- clock: '<circle cx="12" cy="12" r="9"' + P + '/><path d="M12 7v5l3 2"' + P + '/>',
1211
- sun: '<circle cx="12" cy="12" r="4"' + P + '/><path d="M12 2v2M12 20v2M4 12H2M22 12h-2M5 5l1.5 1.5M17.5 17.5 19 19M19 5l-1.5 1.5M6.5 17.5 5 19"' + P + '/>',
1212
- moon: '<path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8Z"' + P + '/>',
1213
- chevL: '<path d="m15 18-6-6 6-6"' + P + '/>',
1214
- chevR: '<path d="m9 18 6-6-6-6"' + P + '/>',
1215
- x: '<path d="M6 6l12 12M18 6 6 18"' + P + '/>',
1216
- sliders: '<path d="M4 6h10M18 6h2M4 12h2M10 12h10M4 18h7M15 18h5"' + P + '/><circle cx="16" cy="6" r="2"' + P + '/><circle cx="8" cy="12" r="2"' + P + '/><circle cx="13" cy="18" r="2"' + P + '/>',
1217
- comfy: '<rect x="3" y="4" width="18" height="7" rx="1.4"' + P + '/><rect x="3" y="13" width="18" height="7" rx="1.4"' + P + '/>',
1218
- compact: '<rect x="3" y="4" width="8" height="7" rx="1.2"' + P + '/><rect x="13" y="4" width="8" height="7" rx="1.2"' + P + '/><rect x="3" y="13" width="8" height="7" rx="1.2"' + P + '/><rect x="13" y="13" width="8" height="7" rx="1.2"' + P + '/>',
1219
- dense: '<rect x="3" y="4" width="5" height="5" rx="1"' + P + '/><rect x="10" y="4" width="5" height="5" rx="1"' + P + '/><rect x="17" y="4" width="4" height="5" rx="1"' + P + '/><rect x="3" y="11" width="5" height="5" rx="1"' + P + '/><rect x="10" y="11" width="5" height="5" rx="1"' + P + '/><rect x="17" y="11" width="4" height="5" rx="1"' + P + '/>',
1220
- image: '<rect x="3" y="4" width="18" height="16" rx="2"' + P + '/><circle cx="9" cy="10" r="1.6"' + P + '/><path d="m4 18 5-4 4 3 3-2 4 3"' + P + '/>',
1221
- live: '<circle cx="12" cy="12" r="3"' + P + '/><path d="M6.5 6.5a8 8 0 0 0 0 11M17.5 6.5a8 8 0 0 1 0 11M4 4a12 12 0 0 0 0 16M20 4a12 12 0 0 1 0 16"' + P + '/>',
1222
- file: '<path d="M14 3v5h5"' + P + '/><path d="M6 3h8l5 5v11a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1Z"' + P + '/>',
1223
- list: '<path d="M8 6h12M8 12h12M8 18h12M3.5 6h.01M3.5 12h.01M3.5 18h.01"' + P + '/>',
1224
- terminal: '<path d="m6 8 3.5 3L6 14M13 15h5"' + P + '/><rect x="3" y="4" width="18" height="16" rx="2"' + P + '/>',
1225
- spark: '<path d="M12 3v4M12 17v4M3 12h4M17 12h4M6 6l2.5 2.5M15.5 15.5 18 18M18 6l-2.5 2.5M8.5 15.5 6 18"' + P + '/>',
1226
- check: '<path d="m4 12 5 5L20 6"' + P + '/>',
1227
- alert: '<path d="M12 8v5M12 16.5h.01"' + P + '/><path d="M10.3 3.9 2.4 18a2 2 0 0 0 1.7 3h15.8a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0Z"' + P + '/>',
1228
- info: '<circle cx="12" cy="12" r="9"' + P + '/><path d="M12 11v5M12 8h.01"' + P + '/>',
1229
- globe: '<circle cx="12" cy="12" r="9"' + P + '/><path d="M3 12h18M12 3c2.5 2.7 2.5 15.3 0 18M12 3c-2.5 2.7-2.5 15.3 0 18"' + P + '/>',
1230
- lock: '<rect x="4.5" y="10" width="15" height="10" rx="2"' + P + '/><path d="M8 10V7a4 4 0 0 1 8 0v3"' + P + '/>',
1231
- caret: '<path d="m6 9 6 6 6-6"' + P + '/>',
1232
- filter: '<path d="M3 5h18l-7 8.2V20l-4-2.2v-4.6L3 5Z"' + P + '/>',
1233
- panelRight: '<rect x="3" y="4" width="18" height="16" rx="2"' + P + '/><path d="M15 4v16"' + P + '/>',
1234
- play: '<path d="M7 5l12 7-12 7z"' + P + '/>',
1235
- pause: '<path d="M9 5v14M15 5v14"' + P + '/>'
1236
- };
1237
- function icon(name, size) {
1238
- var s = size || 18;
1239
- return '<svg viewBox="0 0 24 24" width="' + s + '" height="' + s + '" aria-hidden="true">' + (ICONS[name] || ICONS.info) + '</svg>';
1240
- }
1241
- function pip(status, live) {
1242
- return '<span class="pip" data-status="' + esc(status) + '" data-live="' + (live ? "true" : "false") + '"></span>';
1243
- }
1244
-
1245
- // ---------------------------------------------------------------- derive (observer-data.v1 -> display)
1246
- function laneName(s) { return s.label || (s.sim && s.sim.summary) || s.id || "lane"; }
1247
- function laneRoute(s) { return (s.ui && (s.ui.route || s.ui.appUrl || s.ui.nestedObserverUrl)) || s.url || (s.terminal && s.terminal.title) || ""; }
1248
- function laneProgress(s) {
1249
- if (s.sim && typeof s.sim.progress === "number") return Math.max(0, Math.min(100, Math.round(s.sim.progress)));
1250
- var t = tone(s.status);
1251
- return (t === "complete" || t === "failed") ? 100 : 0;
1252
- }
1253
- function laneStep(s) { return (s.sim && s.sim.currentStep) || s.statusLabel || statusLabel(s.status); }
1254
- function laneSummary(s) { return (s.sim && s.sim.summary) || ""; }
1255
- function laneEvents(s) { return s.timeline || []; }
1256
- function laneArtifacts(s) { return s.artifacts || []; }
1257
-
1258
- // ---------------------------------------------------------------- cost (ALWAYS labeled estimated)
1259
- function fmtCostUsd(v) {
1260
- if (typeof v !== "number" || !isFinite(v)) return null;
1261
- return v >= 0.01 ? v.toFixed(2) : String(v);
1262
- }
1263
- // A single honest label for any cost figure. A number ALWAYS reads "~$X estimated (rates as of
1264
- // <date>)" (never a bare "$X"); a placeholder rate is tagged; a null is "not estimated (<reason>)".
1265
- function costText(usd, ratesAsOf, placeholder, reason) {
1266
- if (usd == null) return "not estimated (" + (reason || "unmeasured") + ")";
1267
- var s = "~$" + (fmtCostUsd(usd) || "0") + " estimated (rates as of " + (ratesAsOf || "unknown") + ")";
1268
- if (placeholder) s += " · placeholder rate";
1269
- return s;
1270
- }
1271
- function runCostText() {
1272
- var c = currentData.cost;
1273
- if (!c) return "";
1274
- return costText(c.estimatedTotalUsd, c.ratesAsOf, c.placeholder, "no priced lines");
1275
- }
1276
- function laneCostText(s) {
1277
- var est = s && s.actor && s.actor.estimatedCost;
1278
- if (!est) return "";
1279
- return costText(est.estimatedCostUsd, est.ratesAsOf, est.placeholder, est.reason);
1280
- }
1281
-
1282
- function geometrySize(value) {
1283
- var width = value && Number(value.width);
1284
- var height = value && Number(value.height);
1285
- return width > 0 && height > 0 ? { width: width, height: height } : null;
1286
- }
1287
- function screenSizeFor(s) {
1288
- var screen = s.desktopGeometry && s.desktopGeometry.screen;
1289
- return geometrySize(screen && screen.verified) || geometrySize(screen && screen.requested);
1290
- }
1291
- function geometrySource(value) {
1292
- if (value === "cdp") return "CDP";
1293
- return String(value || "runtime");
1294
- }
1295
- function geometryDetailFor(s) {
1296
- var geometry = s.desktopGeometry;
1297
- if (!geometry) return "";
1298
- var screen = geometry.screen || {};
1299
- var requested = geometrySize(screen.requested);
1300
- var verified = geometrySize(screen.verified);
1301
- var browserWindow = geometrySize(geometry.browserWindow);
1302
- var viewport = geometrySize(geometry.viewport);
1303
- var bits = [];
1304
- if (verified) {
1305
- bits.push("Screen " + verified.width + TIMES + verified.height + " (verified via " + geometrySource(screen.verified && screen.verified.source) + (requested ? "; requested " + requested.width + TIMES + requested.height : "") + ")");
1306
- } else if (requested) {
1307
- bits.push("Screen " + requested.width + TIMES + requested.height + " (requested; runtime verification unavailable)");
1308
- }
1309
- if (browserWindow) {
1310
- var x = Number(geometry.browserWindow.x);
1311
- var y = Number(geometry.browserWindow.y);
1312
- var origin = isFinite(x) && isFinite(y) ? " at " + x + "," + y : "";
1313
- bits.push("Browser window " + browserWindow.width + TIMES + browserWindow.height + origin + " (" + geometrySource(geometry.browserWindow.source) + ")");
1314
- }
1315
- if (viewport) {
1316
- var dpr = Number(geometry.viewport.deviceScaleFactor);
1317
- bits.push("CSS viewport " + viewport.width + TIMES + viewport.height + (dpr > 0 ? ", DPR " + dpr : "") + " (" + geometrySource(geometry.viewport.source) + ")");
1318
- }
1319
- return bits.join(" · ");
1320
- }
1321
- function focusGeometry(s) {
1322
- var screen = screenSizeFor(s);
1323
- var viewport = geometrySize(s.desktopGeometry && s.desktopGeometry.viewport);
1324
- if (!screen && !viewport) return "";
1325
- var parts = [];
1326
- if (screen) parts.push('<span>SCREEN <strong>' + screen.width + TIMES + screen.height + '</strong></span>');
1327
- if (viewport) {
1328
- var dpr = Number(s.desktopGeometry.viewport.deviceScaleFactor);
1329
- parts.push('<span>VIEWPORT <strong>' + viewport.width + TIMES + viewport.height + '</strong>' + (dpr > 0 ? ' · DPR <strong>' + esc(dpr) + '</strong>' : '') + '</span>');
1330
- }
1331
- var detail = geometryDetailFor(s);
1332
- return '<div class="focus-geometry" title="' + esc(detail) + '" aria-label="' + esc(detail) + '">' + parts.join('<span class="focus-geometry-sep">·</span>') + '</div>';
1333
- }
1334
- function aspectFor(s) {
1335
- if (s.kind === "terminal" || s.kind === "tui") return "16 / 10";
1336
- var frame = screenSizeFor(s) || geometrySize(s.viewport);
1337
- if (frame) return frame.width + " / " + frame.height;
1338
- return "16 / 9";
1339
- }
1340
- function dimsFor(s) {
1341
- if (s.kind === "terminal") return "CLI";
1342
- if (s.kind === "tui") return "TUI";
1343
- if (s.kind === "codex-ui") return "CODEX";
1344
- var screen = screenSizeFor(s);
1345
- if (screen) return screen.width + TIMES + screen.height;
1346
- var vp = geometrySize(s.viewport);
1347
- if (vp) return vp.width > vp.height ? (vp.width + TIMES + vp.height) : "MOB";
1348
- return "SIM";
1349
- }
1350
- function initials(name) {
1351
- var parts = String(name || "").replace(/[^a-zA-Z0-9]+/g, " ").trim().split(" ");
1352
- return (((parts[0] || "")[0] || "") + ((parts[1] || "")[0] || "")).toUpperCase();
1353
- }
1354
- function laneCounts(streams) {
1355
- var c = { running: 0, complete: 0, blocked: 0, failed: 0 };
1356
- streams.forEach(function (s) {
1357
- var t = tone(s.status);
1358
- if (t === "running") c.running += 1;
1359
- else if (t === "complete") c.complete += 1;
1360
- else if (t === "blocked") c.blocked += 1;
1361
- else if (t === "failed") c.failed += 1;
1362
- });
1363
- return c;
1364
- }
1365
- function shortTime(v) {
1366
- if (!v) return "";
1367
- var d = new Date(v);
1368
- if (isNaN(d.getTime())) return String(v);
1369
- return d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
1370
- }
1371
- function shortStamp(v) {
1372
- if (!v) return "";
1373
- var d = new Date(v);
1374
- if (isNaN(d.getTime())) return String(v);
1375
- return d.toLocaleString([], { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" });
1376
- }
1377
-
1378
- function hasText(low, list) {
1379
- for (var i = 0; i < list.length; i += 1) { if (low.indexOf(list[i]) >= 0) return true; }
1380
- return false;
1381
- }
1382
- function classify(line) {
1383
- var t = String(line == null ? "" : line).trim();
1384
- if (!t) return "dim";
1385
- var c0 = t.charAt(0);
1386
- if (c0 === "$" || c0 === "#" || c0 === ">") return "cmd";
1387
- var low = t.toLowerCase();
1388
- if (t.indexOf("FAIL") >= 0 || t.indexOf("✗") >= 0 || t.indexOf("✘") >= 0 ||
1389
- hasText(low, ["fail", "error", "not found", "missing", "cannot", "denied", "unreachable", "unresolved"])) return "err";
1390
- if (t.indexOf("✓") >= 0 || t.indexOf("✔") >= 0 ||
1391
- hasText(low, ["ok ", " ok", "passed", "granted", "ready", "success", "succeeded", "completed", "emitted", "scaffolded"])) return "ok";
1392
- if (t.indexOf("⚠") >= 0 || hasText(low, ["warn", "pending", "timeout", "timed out", "retry", "skipped", "best-effort", "blocked"])) return "warn";
1393
- if ("┌│└├─╭╰╮╯═┐┘".indexOf(c0) >= 0) return "dim";
1394
- return "";
1395
- }
1396
- function termLines(s) {
1397
- var raw = (s.terminalPlain != null ? s.terminalPlain : (s.terminal ? s.terminal.tail : "")) || "";
1398
- var arr = String(raw).split(NL);
1399
- while (arr.length && arr[arr.length - 1].trim() === "") arr.pop();
1400
- return arr.map(function (line) { return { text: line, cls: classify(line) }; });
1401
- }
1402
- function cleanLogText(v) {
1403
- return String(v == null ? "" : v).split(CR).join("").trim();
1404
- }
1405
- function splitLogText(v) {
1406
- var raw = cleanLogText(v);
1407
- if (!raw) return [];
1408
- var arr = raw.split(NL);
1409
- while (arr.length && arr[arr.length - 1].trim() === "") arr.pop();
1410
- return arr.map(function (line) { return { text: line, cls: classify(line) }; });
1411
- }
1412
- function logSections(s) {
1413
- var c = s.completion || {};
1414
- var sections = [];
1415
- var actorLast = splitLogText(c.actorLastMessageTail || "");
1416
- var actorLog = splitLogText(c.actorLogTail || "");
1417
- var bootstrap = splitLogText(c.logTail || "");
1418
- var terminal = termLines(s);
1419
- var terminalRaw = cleanLogText(s.terminalPlain != null ? s.terminalPlain : (s.terminal ? s.terminal.tail : ""));
1420
- var actorRaw = cleanLogText((c.actorLastMessageTail || "") + NL + (c.actorLogTail || "") + NL + (c.logTail || ""));
1421
- if (actorLast.length) sections.push({ id: "actor-last", title: "Actor last message", lines: actorLast });
1422
- if (actorLog.length) sections.push({ id: "actor-log", title: "Codex agent log", lines: actorLog });
1423
- if (bootstrap.length) sections.push({ id: "bootstrap-log", title: "Bootstrap log", lines: bootstrap });
1424
- if (terminal.length && (!actorRaw || actorRaw.indexOf(terminalRaw) < 0)) {
1425
- sections.push({ id: "lane-summary", title: actorRaw ? "Lane summary" : "Lane log", lines: terminal });
1426
- }
1427
- return sections;
1428
- }
1429
- function hasAgentLogs(s) {
1430
- var c = s.completion || {};
1431
- return !!(cleanLogText(c.actorLastMessageTail || "") || cleanLogText(c.actorLogTail || ""));
1432
- }
1433
- function logLineCount(s) {
1434
- return logSections(s).reduce(function (sum, section) { return sum + section.lines.length; }, 0);
1435
- }
1436
- function clip(v, n) {
1437
- var s = String(v == null ? "" : v).trim();
1438
- if (!s) return "";
1439
- return s.length > n ? (s.slice(0, Math.max(0, n - 1)) + "…") : s;
1440
- }
1441
- function shortSurfaceValue(v) {
1442
- var s = String(v == null ? "" : v).trim();
1443
- if (!s) return "";
1444
- var cut = s.split("?")[0].split("#")[0];
1445
- var parts = cut.split("/");
1446
- var tail = parts.filter(function (p) { return !!p; }).slice(-2).join("/");
1447
- return clip(tail || cut || s, 48);
1448
- }
1449
- function linkHref(v, artifactPath) {
1450
- var raw = String(v == null ? "" : v).trim();
1451
- var low = raw.toLowerCase();
1452
- if (!raw) return "";
1453
- if (low.indexOf("http://") === 0 || low.indexOf("https://") === 0 || low.indexOf("file:") === 0) return raw;
1454
- if (raw.indexOf("://") >= 0) return "";
1455
- if (raw.charAt(0) === "/" || raw.indexOf("..") >= 0) return "";
1456
- return artifactPath ? ("../" + raw) : raw;
1457
- }
1458
- function runArtifactHref(v) {
1459
- if (typeof v !== "string") return "";
1460
- var raw = v.trim();
1461
- var low = raw.toLowerCase();
1462
- if (!raw) return "";
1463
- if (low.indexOf("http://") === 0 || low.indexOf("https://") === 0 || low.indexOf("file:") === 0) return raw;
1464
- if (raw.indexOf("://") >= 0 || raw.charAt(0) === "/") return "";
1465
- if (raw.indexOf("../") === 0 && raw.indexOf("../..") !== 0 && raw.indexOf("/../") < 0) return raw;
1466
- if (raw.indexOf("..") >= 0) return "";
1467
- return "../" + raw;
1468
- }
1469
- function firstArtifactKind(s, kind) {
1470
- var arts = laneArtifacts(s);
1471
- for (var i = 0; i < arts.length; i += 1) {
1472
- if (arts[i] && arts[i].kind === kind) return arts[i];
1473
- }
1474
- return null;
1475
- }
1476
- function artifactKinds(arts) {
1477
- var seen = {}, out = [];
1478
- arts.forEach(function (a) {
1479
- var k = (a && a.kind) || "file";
1480
- if (!seen[k]) { seen[k] = true; out.push(k); }
1481
- });
1482
- var shown = out.slice(0, 3).join("+");
1483
- return shown + (out.length > 3 ? "+" : "");
1484
- }
1485
- function terminalExcerpt(s) {
1486
- var lines = termLines(s).filter(function (ln) { return !!String(ln.text || "").trim(); });
1487
- if (!lines.length) return "";
1488
- return clip(lines[lines.length - 1].text, 88);
1489
- }
1490
- function completionTone(c) {
1491
- var t = tone(c && c.status);
1492
- return t === "running" ? "live" : t === "complete" ? "ok" : t === "blocked" ? "warn" : c && c.status === "failed" ? "err" : "info";
1493
- }
1494
- function completionText(c) {
1495
- if (!c) return "";
1496
- var bits = [statusLabel(c.status)];
1497
- if (typeof c.exitCode === "number") bits.push("exit " + c.exitCode);
1498
- if (typeof c.nestedObserverPresent === "boolean") bits.push("observer " + (c.nestedObserverPresent ? "present" : "missing"));
1499
- if (typeof c.nestedVerifyPassed === "boolean") bits.push("verify " + (c.nestedVerifyPassed ? "passed" : "failed"));
1500
- if (c.reason) bits.push(c.reason);
1501
- return clip(bits.join(" · "), 110);
1502
- }
1503
- function meaningfulUseTone(m) {
1504
- if (!m) return "info";
1505
- return m.status === "pass" ? "ok" : m.status === "partial" ? "warn" : m.status === "fail" ? "err" : "info";
1506
- }
1507
- function meaningfulUseText(m) {
1508
- if (!m) return "";
1509
- return (m.status || "unknown") + " " + (typeof m.score === "number" ? Math.round(m.score) : 0) + "/100";
1510
- }
1511
- function buildMeaningfulUseCard(s) {
1512
- var m = s.completion && s.completion.meaningfulUse;
1513
- if (!m) return "";
1514
- var comps = Array.isArray(m.components) ? m.components : [];
1515
- return '<div class="meaning-card" aria-label="Meaningful-use score">'
1516
- + '<div class="meaning-head"><span class="eyebrow">Meaningful Use</span><span class="meaning-score" data-status="' + esc(m.status || "unknown") + '">' + esc(meaningfulUseText(m)) + '</span></div>'
1517
- + '<div class="meaning-summary">' + esc(m.summary || "No scoring summary captured.") + '</div>'
1518
- + (comps.length ? '<div class="meaning-components">' + comps.map(function (c) {
1519
- return '<div class="meaning-comp" data-status="' + esc(c.status || "unknown") + '"><span class="meaning-comp-dot"></span><div><div class="meaning-comp-name">' + esc(c.label || c.id || "Component") + '</div><div class="meaning-comp-detail">' + esc(c.detail || "") + '</div></div><span class="meaning-comp-score">' + esc(String(typeof c.score === "number" ? Math.round(c.score) : 0)) + '</span></div>';
1520
- }).join("") + '</div>' : "")
1521
- + '</div>';
1522
- }
1523
- function labChip(kind, label, detail, href, toneName) {
1524
- var title = label + (detail ? ": " + detail : "");
1525
- var inner = '<span class="bw-chip-k">' + esc(label) + '</span>' + (detail ? '<span class="bw-chip-v">' + esc(detail) + '</span>' : "");
1526
- var attrs = ' class="bw-lab-chip" data-kind="' + esc(kind) + '" data-tone="' + esc(toneName || "info") + '" title="' + esc(title) + '"';
1527
- if (href) return '<a' + attrs + ' href="' + esc(href) + '" target="_blank" rel="noopener noreferrer" data-action="external">' + inner + '</a>';
1528
- return '<span' + attrs + '>' + inner + '</span>';
1529
- }
1530
- function browserLiveUrl(s) {
1531
- return (s.embed && s.embed.kind === "iframe" && s.embed.url) || s.url || "";
1532
- }
1533
- function liveStreamMount(s, liveUrl) {
1534
- return '<div class="live-stream-mount surface-fill" data-live-stream-id="' + esc(s.id) + '" data-live-stream-url="' + esc(liveUrl) + '" data-live-stream-title="' + esc(laneName(s) || "live stream") + '">'
1535
- + '<div class="bw-app-wait"><div class="wait-spinner" style="width:24px;height:24px"></div>'
1536
- + '<div class="mono" style="font-size:9px">connecting live stream</div></div></div>';
1537
- }
1538
- function browserShot(s) {
1539
- var art = firstArtifactKind(s, "screenshot");
1540
- return runArtifactHref(s.ui && s.ui.screenshotUrl)
1541
- || runArtifactHref(s.embed && s.embed.kind === "screenshot" && s.embed.url)
1542
- || runArtifactHref(art && art.path)
1543
- || "";
1544
- }
1545
- function browserHasLabSignals(s) {
1546
- var ui = s.ui || {};
1547
- return !!(browserLiveUrl(s) || browserShot(s) || ui.appUrl || ui.nestedObserverUrl || ui.nestedObserverPath || ui.state || s.completion || terminalExcerpt(s) || laneArtifacts(s).length);
1548
- }
1549
- function browserLabDock(s, shot) {
1550
- var ui = s.ui || {};
1551
- var chips = [];
1552
- var appUrl = ui.appUrl || "";
1553
- var nestedArt = firstArtifactKind(s, "observer");
1554
- var nested = ui.nestedObserverUrl || ui.nestedObserverPath || (nestedArt && nestedArt.path) || "";
1555
- var completion = s.completion || null;
1556
- var shotArt = firstArtifactKind(s, "screenshot");
1557
- var arts = laneArtifacts(s);
1558
- var tail = terminalExcerpt(s);
1559
-
1560
- if (appUrl) chips.push(labChip("app", "app", shortSurfaceValue(appUrl), linkHref(appUrl, false), "live"));
1561
- if (nested) chips.push(labChip("observer", "observer", shortSurfaceValue(nested), linkHref(nested, !!(nestedArt && nested === nestedArt.path)), "info"));
1562
- else if (completion && typeof completion.nestedObserverPresent === "boolean") chips.push(labChip("observer", "observer", completion.nestedObserverPresent ? "present" : "missing", "", completion.nestedObserverPresent ? "ok" : "warn"));
1563
- if (completion) chips.push(labChip("completion", "status", completionText(completion), "", completionTone(completion)));
1564
- if (completion && completion.meaningfulUse) chips.push(labChip("meaningful-use", "score", meaningfulUseText(completion.meaningfulUse), "", meaningfulUseTone(completion.meaningfulUse)));
1565
- if (tail) chips.push(labChip("terminal", "terminal", tail, "", "info"));
1566
- if (shot || shotArt) chips.push(labChip("screenshot", "shot", shot ? (S.media === "screenshot" ? "viewing fallback" : "fallback ready") : "artifact", shot || runArtifactHref(shotArt.path), "info"));
1567
- if (arts.length) chips.push(labChip("artifact", "files", arts.length + " " + artifactKinds(arts), "", "info"));
1568
- if (ui.state && !completion) chips.push(labChip("state", "state", clip(ui.state, 72), "", "info"));
1569
- if (!chips.length) return "";
1570
- return '<div class="bw-lab-dock" aria-label="Browser lane lab surfaces">' + chips.slice(0, 8).join("") + '</div>';
1571
- }
1572
-
1573
- function consoleLines() {
1574
- var rows = [];
1575
- var lc = currentData.run.lifecycle || [];
1576
- lc.forEach(function (e) { rows.push({ at: e.at, lvl: "info", text: e.message || e.event || "" }); });
1577
- (currentData.events || []).forEach(function (e) {
1578
- var prefix = e.simId ? (e.simId + " ") : "";
1579
- rows.push({ at: e.at, lvl: (e.level === "error" ? "err" : e.level === "warn" ? "warn" : "info"), text: prefix + (e.message || e.type || "") });
1580
- });
1581
- rows.sort(function (a, b) { return String(a.at || "").localeCompare(String(b.at || "")); });
1582
- if (!rows.length) rows.push({ at: "", lvl: "dim", text: "No orchestrator log lines recorded for this run." });
1583
- return rows.map(function (r) { return { t: shortTime(r.at), lvl: r.lvl, text: r.text }; });
1584
- }
1585
-
1586
- // ---------------------------------------------------------------- overall
1587
- // When every lane is terminal and the live streams have ended, the board must not end on what
1588
- // looks like an outage (#357): one line states the verdict and why the tiles changed.
1589
- function buildRunVerdictBanner() {
1590
- var ss = currentData.streams;
1591
- if (!ss.length) return "";
1592
- if (!ss.some(function (s) { return s.liveEnded; })) return "";
1593
- if (ss.some(function (s) { return tone(s.status) === "running" || tone(s.status) === "queued"; })) return "";
1594
- var ok = ss.filter(function (s) { return tone(s.status) === "complete"; }).length;
1595
- var status = overallStatus();
1596
- // Prefer the PARTICIPANT outcome over the lane count. "2 of 3 lanes passed" is a fact about the
1597
- // harness; "2/3 reached the goal, 1 gave up" is the study result, which is what the person
1598
- // watching through the glass came for — and it carries its denominator, so a vivid moment on
1599
- // one tile cannot be read as the whole picture (docs/principles/three-roles.md).
1600
- var line = currentData.run.participantsLine
1601
- ? 'Run finished — ' + currentData.run.participantsLine + '.'
1602
- : 'Run finished — ' + ok + ' of ' + ss.length + ' lane(s) passed.';
1603
- return '<div class="run-verdict-banner" data-status="' + esc(status) + '" role="status">'
1604
- + '<span class="rvb-dot"></span>'
1605
- + '<span>' + esc(line) + ' Live streams have ended; tiles show each lane’s recorded evidence.</span>'
1606
- + '</div>';
1607
- }
1608
- function overallStatus() {
1609
- var ss = currentData.streams;
1610
- if (ss.some(function (s) { return tone(s.status) === "running"; })) return "running";
1611
- if (ss.some(function (s) { return s.status === "failed"; })) return "failed";
1612
- if (ss.some(function (s) { return tone(s.status) === "blocked"; })) return "blocked";
1613
- if (!ss.length) return currentData.run.status || "queued";
1614
- return "complete";
1615
- }
1616
- function overallPct() {
1617
- var ss = currentData.streams;
1618
- if (!ss.length) return 0;
1619
- var sum = 0;
1620
- ss.forEach(function (s) { sum += laneProgress(s); });
1621
- return Math.round(sum / ss.length);
1622
- }
1623
- function hasLive() { return currentData.streams.some(function (s) { return tone(s.status) === "running"; }); }
1624
-
1625
- function statusCount(id) { return currentData.streams.filter(function (s) { return tone(s.status) === id; }).length; }
1626
- function kindCount(id) { return currentData.streams.filter(function (s) { return kindGroup(s.kind) === id; }).length; }
1627
-
1628
- function filteredStreams() {
1629
- var q = S.query.trim().toLowerCase();
1630
- return currentData.streams.filter(function (s) {
1631
- if (S.statusSel.length && S.statusSel.indexOf(tone(s.status)) < 0) return false;
1632
- if (S.kindSel.length && S.kindSel.indexOf(kindGroup(s.kind)) < 0) return false;
1633
- if (q && (laneName(s) + " " + (s.kindLabel || "") + " " + laneRoute(s)).toLowerCase().indexOf(q) < 0) return false;
1634
- return true;
1635
- });
1636
- }
1637
-
1638
- // ================================================================ SURFACES
1639
- function liveTag() {
1640
- return '<span class="live-tag">' + pip("running", true) + ' Live</span>';
1641
- }
1642
- function waitSurface(s) {
1643
- var st = s.status, inner;
1644
- if (st === "blocked" || st === "timed_out") {
1645
- inner = '<div class="wait-icon" data-tone="amber">' + icon("alert") + '</div>'
1646
- + '<div class="wait-title">' + (st === "timed_out" ? "Lane timed out" : "Lane blocked") + '</div>'
1647
- + '<div class="wait-sub">' + esc(laneStep(s)) + '</div>';
1648
- } else if (st === "failed") {
1649
- inner = '<div class="wait-icon" data-tone="red">' + icon("x") + '</div>'
1650
- + '<div class="wait-title">Lane failed</div>'
1651
- + '<div class="wait-sub">' + esc(laneStep(s)) + '</div>';
1652
- } else if (st === "contract_proof_only") {
1653
- inner = '<div class="wait-icon" data-tone="green">' + icon("check") + '</div>'
1654
- + '<div class="wait-title">Contract proof only</div>'
1655
- + '<div class="wait-sub">' + esc(laneSummary(s) || laneStep(s)) + '</div>';
1656
- } else {
1657
- inner = '<div class="wait-spinner"></div>'
1658
- + '<div class="wait-title">Waiting for substrate</div>'
1659
- + '<div class="wait-sub">' + esc(laneRoute(s) || laneStep(s)) + '</div>';
1660
- }
1661
- return '<div class="wait"><div class="wait-inner">' + inner + '</div></div>';
1662
- }
1663
- // ---------------------------------------------------------------- replay scrubber
1664
- // A durable per-turn keyframe replay built PURELY from the artifacts already in the bundle: the
1665
- // ordered actor.items (reasoning/message/ui_action) sequenced against each persisted screenshot.
1666
- // No new capture, no schema change, no new privacy surface — it reuses the frames' existing
1667
- // gitignored / publishable:false posture. (#292 first slice.)
1668
- function replayFrames(s) {
1669
- var actor = s && s.actor;
1670
- var items = actor && actor.items;
1671
- if (!items || !items.length) return [];
1672
- var frames = [];
1673
- var narration = [];
1674
- var actions = [];
1675
- for (var i = 0; i < items.length; i += 1) {
1676
- var it = items[i];
1677
- if (!it) continue;
1678
- if (it.kind === "reasoning" || it.kind === "message") { if (it.text) narration.push(String(it.text)); }
1679
- else if (it.kind === "ui_action") { if (it.title) actions.push(String(it.title)); }
1680
- else if (it.kind === "screenshot" && it.screenshotRef && it.screenshotRef.path) {
1681
- frames.push({
1682
- src: it.screenshotRef.path,
1683
- label: it.title || ("frame " + (frames.length + 1)),
1684
- redaction: (it.screenshotRef.redaction && it.screenshotRef.redaction !== "none") ? it.screenshotRef.redaction : "raw",
1685
- narration: narration,
1686
- actions: actions
1687
- });
1688
- narration = [];
1689
- actions = [];
1690
- }
1691
- }
1692
- return frames;
1693
- }
1694
- function streamById(id) {
1695
- var ss = currentData.streams || [];
1696
- for (var i = 0; i < ss.length; i += 1) { if (ss[i].id === id) return ss[i]; }
1697
- return null;
1698
- }
1699
- function scrubIndexOf(id, frames) {
1700
- var v = scrubState[id];
1701
- if (v == null) return frames.length ? frames.length - 1 : 0; // default: the last real frame
1702
- v = Number(v);
1703
- if (!(v >= 0)) v = 0;
1704
- if (v > frames.length - 1) v = frames.length - 1;
1705
- return v;
1706
- }
1707
- function frameInfoHtml(f) {
1708
- if (!f) return "";
1709
- var out = "";
1710
- var acts = (f.actions || []).filter(Boolean);
1711
- var narr = (f.narration || []).filter(Boolean);
1712
- if (acts.length) out += '<div class="replay-acts">' + acts.map(function (a) { return '<span class="replay-act">' + esc(a) + '</span>'; }).join("") + '</div>';
1713
- if (narr.length) out += '<div class="replay-narr">' + esc(narr.join(NL + NL)) + '</div>';
1714
- if (!out) out = '<div class="replay-narr replay-dim">No narration recorded before this frame.</div>';
1715
- return out;
1716
- }
1717
- function replayScrubber(s) {
1718
- var frames = replayFrames(s);
1719
- var id = s.id;
1720
- var idx = scrubIndexOf(id, frames);
1721
- var f = frames[idx];
1722
- var playing = !!scrubTimers[id];
1723
- return '<div class="replay" data-stream="' + esc(id) + '">'
1724
- + '<div class="replay-stage"><img class="surface-fill surface-screenshot" id="replay-img-' + esc(id) + '" src="' + esc(runArtifactHref(f && f.src)) + '" alt="replay frame ' + (idx + 1) + '"/>'
1725
- + '<span class="replay-badge" id="replay-badge-' + esc(id) + '" title="frame capture fidelity">' + esc(f ? f.redaction : "raw") + '</span></div>'
1726
- + '<div class="replay-bar">'
1727
- + '<button class="replay-btn" id="replay-play-' + esc(id) + '" data-action="scrub:' + esc(id) + ':play" aria-label="Play replay">' + icon(playing ? "pause" : "play", 13) + '</button>'
1728
- + '<button class="replay-btn" data-action="scrub:' + esc(id) + ':-1" aria-label="Previous frame">' + icon("chevL", 13) + '</button>'
1729
- + '<input class="replay-range" type="range" data-role="scrub" data-stream="' + esc(id) + '" min="0" max="' + (frames.length - 1) + '" step="1" value="' + idx + '" aria-label="Scrub frames"/>'
1730
- + '<button class="replay-btn" data-action="scrub:' + esc(id) + ':1" aria-label="Next frame">' + icon("chevR", 13) + '</button>'
1731
- + '<span class="replay-count mono" id="replay-count-' + esc(id) + '">' + (idx + 1) + '/' + frames.length + '</span>'
1732
- + '</div>'
1733
- + '<div class="replay-info" id="replay-info-' + esc(id) + '">' + frameInfoHtml(f) + '</div>'
1734
- + '</div>';
1735
- }
1736
- function updateScrubDom(id) {
1737
- var s = streamById(id);
1738
- if (!s) return;
1739
- var frames = replayFrames(s);
1740
- if (!frames.length) return;
1741
- var idx = scrubIndexOf(id, frames);
1742
- var f = frames[idx];
1743
- var img = document.getElementById("replay-img-" + id);
1744
- if (img) img.setAttribute("src", runArtifactHref(f && f.src));
1745
- var badge = document.getElementById("replay-badge-" + id);
1746
- if (badge) badge.textContent = f ? f.redaction : "raw";
1747
- var cnt = document.getElementById("replay-count-" + id);
1748
- if (cnt) cnt.textContent = (idx + 1) + "/" + frames.length;
1749
- var info = document.getElementById("replay-info-" + id);
1750
- if (info) info.innerHTML = frameInfoHtml(f);
1751
- var range = app.querySelector('.replay-range[data-stream="' + id + '"]');
1752
- if (range && String(range.value) !== String(idx)) range.value = idx;
1753
- }
1754
- function syncPlayBtn(id) {
1755
- var btn = document.getElementById("replay-play-" + id);
1756
- if (btn) btn.innerHTML = icon(scrubTimers[id] ? "pause" : "play", 13);
1757
- }
1758
- function stopScrubPlay(id) {
1759
- if (scrubTimers[id]) { clearInterval(scrubTimers[id]); delete scrubTimers[id]; syncPlayBtn(id); }
1760
- }
1761
- function stopAllScrubTimers() {
1762
- for (var k in scrubTimers) { if (scrubTimers.hasOwnProperty(k)) clearInterval(scrubTimers[k]); }
1763
- scrubTimers = {};
1764
- }
1765
- function toggleScrubPlay(id) {
1766
- if (scrubTimers[id]) { stopScrubPlay(id); return; }
1767
- var s = streamById(id);
1768
- var frames = s ? replayFrames(s) : [];
1769
- if (frames.length < 2) return;
1770
- if (scrubIndexOf(id, frames) >= frames.length - 1) { scrubState[id] = 0; updateScrubDom(id); } // restart from the top
1771
- scrubTimers[id] = setInterval(function () {
1772
- var st = streamById(id);
1773
- var fr = st ? replayFrames(st) : [];
1774
- if (fr.length < 2) { stopScrubPlay(id); return; }
1775
- var cur = scrubIndexOf(id, fr);
1776
- if (cur >= fr.length - 1) { stopScrubPlay(id); return; }
1777
- scrubState[id] = cur + 1;
1778
- updateScrubDom(id);
1779
- }, 850);
1780
- syncPlayBtn(id);
1781
- }
1782
- function handleScrub(id, op) {
1783
- var s = streamById(id);
1784
- var frames = s ? replayFrames(s) : [];
1785
- if (!frames.length) return;
1786
- if (op === "play") { toggleScrubPlay(id); return; }
1787
- stopScrubPlay(id);
1788
- var idx = scrubIndexOf(id, frames) + Number(op);
1789
- if (idx < 0) idx = 0;
1790
- if (idx > frames.length - 1) idx = frames.length - 1;
1791
- scrubState[id] = idx;
1792
- updateScrubDom(id);
1793
- }
1794
- function browserSurface(s, focus) {
1795
- var live = tone(s.status) === "running";
1796
- var route = laneRoute(s) || "(local)";
1797
- var liveUrl = browserLiveUrl(s);
1798
- var shot = browserShot(s);
1799
- var dock = focus ? browserLabDock(s, shot) : "";
1800
- // Ended lanes replay in the GRID too (#357): a finished tile should show the lane's story,
1801
- // not a frozen screenshot, without requiring focus.
1802
- var showReplay = (focus || s.liveEnded) && !(liveUrl && S.media === "live") && replayFrames(s).length >= 2;
1803
- var body;
1804
- if (liveUrl && S.media === "live") body = liveStreamMount(s, liveUrl);
1805
- else if (showReplay) body = replayScrubber(s);
1806
- else if (shot) body = '<img class="surface-fill surface-screenshot" src="' + esc(shot) + '" alt="' + (s.desktopGeometry ? "desktop screenshot" : "browser screenshot") + '"/>';
1807
- else body = '<div class="bw-app-wait"><div class="wait-spinner" style="width:24px;height:24px"></div>'
1808
- + '<div class="mono" style="font-size:9px">' + esc(route) + '</div>'
1809
- + '<div style="font-size:10px">' + esc(laneStep(s)) + '</div></div>';
1810
- // A lane whose live stream ENDED must never read as a crash: the badge says what happened
1811
- // (finished + which verdict) and that the tile now shows recorded evidence (#357/#284).
1812
- var endedBadge = s.liveEnded
1813
- ? '<span class="live-ended-badge" data-tone="' + esc(tone(s.status)) + '">' + esc(statusLabel(s.status)) + ' — live stream ended; showing recorded evidence</span>'
1814
- : "";
1815
- return '<div class="bw">'
1816
- + '<div class="bw-chrome"><div class="bw-dots"><i></i><i></i><i></i></div>'
1817
- + '<div class="bw-url">' + icon("lock", 8) + '<span>' + esc(route) + '</span></div></div>'
1818
- + '<div class="bw-viewport">' + endedBadge + body + (liveUrl && S.media === "live" ? "" : dock) + '</div>'
1819
- + (live ? liveTag() : "")
1820
- + '</div>';
1821
- }
1822
- function terminalSurface(s, focus) {
1823
- var lines = termLines(s);
1824
- var live = tone(s.status) === "running";
1825
- var shown = focus ? lines : lines.slice(-9);
1826
- var start = lines.length - shown.length;
1827
- var rows = shown.map(function (ln, i) {
1828
- return '<div class="term-line">'
1829
- + (focus ? '<span class="term-num">' + pad2(start + i + 1) + '</span>' : '')
1830
- + '<span class="term-txt ' + ln.cls + '">' + esc(ln.text || " ") + '</span></div>';
1831
- }).join("");
1832
- var title = s.kind === "tui" ? (laneRoute(s) || "codex --tui") : (laneName(s) || laneRoute(s));
1833
- var stat = s.kind === "tui" ? "PTY" : "SNAPSHOT";
1834
- return '<div class="term">'
1835
- + '<div class="term-bar"><span class="tprompt">$</span><span class="ttitle">' + esc(title) + '</span><span class="tstat">' + stat + '</span></div>'
1836
- + '<div class="term-body">' + rows
1837
- + (live ? '<div class="term-line"><span class="term-txt cmd">$<span class="term-caret"></span></span></div>' : '')
1838
- + '</div></div>';
1839
- }
1840
- function codexTraceObject(s) {
1841
- var c = s.codex || {};
1842
- var meta = s.metadata || {};
1843
- var trace = c.trace || c.traceMetadata || s.trace || meta.codex || meta.codexAppServer || meta.appServerTrace || {};
1844
- if (!trace || typeof trace !== "object" || Array.isArray(trace)) trace = {};
1845
- return { codex: c, trace: trace };
1846
- }
1847
- function isCodexAppServer(s) {
1848
- var roots = codexTraceObject(s);
1849
- return s.kind === "codex-ui" && (s.transport === "app-server" || roots.codex.provider === "codex-app-server" || roots.trace.provider === "codex-app-server" || roots.trace.service === "codex-app-server");
1850
- }
1851
- function codexField(s, names) {
1852
- var roots = codexTraceObject(s);
1853
- var list = [roots.trace, roots.codex, s];
1854
- for (var i = 0; i < list.length; i += 1) {
1855
- var obj = list[i] || {};
1856
- for (var j = 0; j < names.length; j += 1) {
1857
- var v = obj[names[j]];
1858
- if (v != null && String(v).trim() !== "") return String(v);
1859
- }
1860
- }
1861
- return "";
1862
- }
1863
- function codexTraceEvents(s) {
1864
- if (!isCodexAppServer(s)) return [];
1865
- var roots = codexTraceObject(s);
1866
- var trace = roots.trace;
1867
- var c = roots.codex;
1868
- var groups = [
1869
- c.events,
1870
- c.traceEvents,
1871
- trace.events,
1872
- trace.messages,
1873
- trace.reasoning,
1874
- trace.commands,
1875
- trace.fileChanges,
1876
- trace.tools,
1877
- trace.approvals,
1878
- trace.items,
1879
- trace.timeline,
1880
- trace.traceEvents
1881
- ];
1882
- var out = [];
1883
- groups.forEach(function (items) {
1884
- if (Array.isArray(items)) out = out.concat(items);
1885
- });
1886
- return out;
1887
- }
1888
- function codexKind(raw) {
1889
- var k = String((raw && (raw.kind || raw.type || raw.event || raw.category)) || "").toLowerCase();
1890
- if (k.indexOf("reason") >= 0) return "reasoning";
1891
- if (k.indexOf("command") >= 0 || k.indexOf("shell") >= 0 || k.indexOf("exec") >= 0) return "command";
1892
- if (k.indexOf("file") >= 0 || k.indexOf("patch") >= 0 || k.indexOf("diff") >= 0) return "file";
1893
- if (k.indexOf("tool") >= 0 || k.indexOf("function") >= 0) return "tool";
1894
- if (k.indexOf("message") >= 0 || k.indexOf("assistant") >= 0 || k.indexOf("user") >= 0) return "message";
1895
- if (raw && (raw.command || raw.cmd)) return "command";
1896
- if (raw && (raw.path || raw.file || raw.filePath)) return "file";
1897
- if (raw && (raw.tool || raw.toolName || raw.name)) return "tool";
1898
- return "message";
1899
- }
1900
- function codexEventValue(raw, names) {
1901
- if (!raw) return "";
1902
- for (var i = 0; i < names.length; i += 1) {
1903
- var v = raw[names[i]];
1904
- if (v == null) continue;
1905
- if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
1906
- var out = String(v).trim();
1907
- if (out) return out;
1908
- }
1909
- }
1910
- return "";
1911
- }
1912
- function codexEventTitle(raw, kind) {
1913
- if (kind === "command") return codexEventValue(raw, ["title", "command", "cmd", "name"]) || "command";
1914
- if (kind === "file") return codexEventValue(raw, ["title", "path", "filePath", "file", "name"]) || "file";
1915
- if (kind === "tool") return codexEventValue(raw, ["title", "toolName", "tool", "name"]) || "tool";
1916
- if (kind === "reasoning") return codexEventValue(raw, ["title", "summary", "phase"]) || "reasoning";
1917
- return codexEventValue(raw, ["title", "role", "speaker", "name"]) || "message";
1918
- }
1919
- function codexEventBody(raw, kind) {
1920
- if (kind === "command") return codexEventValue(raw, ["text", "summary", "command", "cmd", "output", "result"]);
1921
- if (kind === "file") return codexEventValue(raw, ["text", "summary", "action", "path", "filePath", "file"]);
1922
- if (kind === "tool") return codexEventValue(raw, ["text", "summary", "input", "output", "result", "status"]);
1923
- return codexEventValue(raw, ["text", "content", "message", "summary", "delta"]);
1924
- }
1925
- function codexEventMeta(raw) {
1926
- var bits = [];
1927
- var at = codexEventValue(raw, ["at", "time", "timestamp", "createdAt"]);
1928
- var status = codexEventValue(raw, ["status", "state"]);
1929
- var exitCode = codexEventValue(raw, ["exitCode", "exit_code"]);
1930
- if (at) bits.push(shortTime(at) || at);
1931
- if (status) bits.push(status);
1932
- if (exitCode) bits.push("exit " + exitCode);
1933
- return bits.join(" · ");
1934
- }
1935
- function codexIcon(kind) {
1936
- if (kind === "command") return "terminal";
1937
- if (kind === "file") return "file";
1938
- if (kind === "tool") return "check";
1939
- if (kind === "reasoning") return "spark";
1940
- return "info";
1941
- }
1942
- function codexChip(label, value, toneName) {
1943
- if (!value) return "";
1944
- return '<span class="cx-chip" data-cx-chip="' + esc(label) + '" data-tone="' + esc(toneName || "info") + '"><span class="cx-chip-k">' + esc(label) + '</span><span class="cx-chip-v">' + esc(clip(value, 64)) + '</span></span>';
1945
- }
1946
- function codexMetaChips(s) {
1947
- if (!isCodexAppServer(s)) return "";
1948
- var status = codexField(s, ["status", "state"]) || s.statusLabel || statusLabel(s.status);
1949
- var thread = codexField(s, ["threadId", "thread_id", "thread", "sessionId", "session_id"]);
1950
- var turn = codexField(s, ["turnId", "turn_id", "turn"]);
1951
- var model = codexField(s, ["model", "modelName", "model_name"]);
1952
- var service = codexField(s, ["service", "serviceName", "service_name", "provider"]) || (s.transport === "app-server" ? "codex-app-server" : "");
1953
- var t = tone(s.status);
1954
- return [
1955
- codexChip("status", status, t === "running" ? "live" : t === "complete" ? "ok" : t === "blocked" ? "warn" : s.status === "failed" ? "err" : "info"),
1956
- codexChip("thread", thread, "info"),
1957
- codexChip("turn", turn, "info"),
1958
- codexChip("model", model, "info"),
1959
- codexChip("service", service, "info")
1960
- ].filter(function (x) { return !!x; }).join("");
1961
- }
1962
- function codexTraceRows(s) {
1963
- var evs = codexTraceEvents(s);
1964
- if (!evs.length) return "";
1965
- return '<div class="cx-lanes" aria-label="Codex app-server trace events">' + evs.slice(-12).map(function (ev) {
1966
- var kind = codexKind(ev);
1967
- var title = codexEventTitle(ev, kind);
1968
- var body = codexEventBody(ev, kind);
1969
- var meta = codexEventMeta(ev);
1970
- return '<div class="cx-lane" data-cx-kind="' + esc(kind) + '"><span class="cx-lane-ic">' + icon(codexIcon(kind), 13) + '</span><div class="cx-lane-main">'
1971
- + '<div class="cx-lane-head"><span class="cx-lane-kind">' + esc(kind) + '</span><span class="cx-lane-title">' + esc(title) + '</span></div>'
1972
- + (body ? '<div class="cx-lane-body">' + esc(clip(body, 220)) + '</div>' : '')
1973
- + (meta ? '<div class="cx-lane-meta">' + esc(meta) + '</div>' : '')
1974
- + '</div></div>';
1975
- }).join("") + '</div>';
1976
- }
1977
- function codexArtifactLinks(s) {
1978
- if (!isCodexAppServer(s)) return "";
1979
- var arts = laneArtifacts(s).filter(function (a) { return !!artifactHref(a); }).slice(0, 5);
1980
- if (!arts.length) return "";
1981
- return '<div class="cx-artifacts" aria-label="Codex linked artifacts">' + arts.map(function (a) {
1982
- return '<a class="cx-artifact" href="' + esc(artifactHref(a)) + '" target="_blank" rel="noopener noreferrer" data-action="external" data-kind="' + esc(a.kind || "artifact") + '">' + icon("file", 12) + '<span>' + esc(a.label || a.path) + '</span></a>';
1983
- }).join("") + '</div>';
1984
- }
1985
- function codexSurface(s) {
1986
- var live = tone(s.status) === "running";
1987
- var parts = [];
1988
- var summary = laneSummary(s) || (s.codex && s.codex.contract) || laneStep(s);
1989
- var chips = codexMetaChips(s);
1990
- var traceRows = codexTraceRows(s);
1991
- var artifacts = codexArtifactLinks(s);
1992
- if (chips) parts.push('<div class="cx-chips">' + chips + '</div>');
1993
- if (summary) parts.push('<div class="cx-msg agent">' + esc(summary) + '</div>');
1994
- if (traceRows) {
1995
- parts.push(traceRows);
1996
- } else {
1997
- var receipts = termLines(s);
1998
- receipts.forEach(function (ln, i) {
1999
- var pending = live && i === receipts.length - 1;
2000
- parts.push('<div class="cx-tool">' + (pending ? '<span class="cx-spin"></span>' : icon("check", 12)) + '<span>' + esc(ln.text) + '</span></div>');
2001
- });
2002
- }
2003
- if (artifacts) parts.push(artifacts);
2004
- if (live) parts.push('<div class="cx-tool"><span class="cx-spin"></span><span>thinking…</span></div>');
2005
- return '<div class="codex">'
2006
- + '<div class="codex-bar"><span class="cx-spark">' + icon("spark", 11) + '</span> ' + esc(laneRoute(s) || "codex.app-server · session") + '</div>'
2007
- + '<div class="codex-body">' + parts.join("") + '</div>'
2008
- + (live ? liveTag() : "")
2009
- + '</div>';
2010
- }
2011
- function streamSurface(s, focus) {
2012
- var k = s.kind, st = s.status;
2013
- var hasTail = !!(s.terminal && s.terminal.tail && String(s.terminal.tail).trim());
2014
- if ((st === "blocked" || st === "timed_out") && !hasTail && !((k === "ui" || k === "browser") && browserHasLabSignals(s))) return waitSurface(s);
2015
- if (k === "ui" || k === "browser") {
2016
- if ((st === "blocked" || st === "timed_out" || st === "failed" || st === "contract_proof_only") && !browserHasLabSignals(s)) return waitSurface(s);
2017
- return browserSurface(s, focus);
2018
- }
2019
- if (k === "terminal" || k === "tui") return hasTail ? terminalSurface(s, focus) : waitSurface(s);
2020
- if (k === "codex-ui") {
2021
- if (st === "contract_proof_only" && !hasTail) return waitSurface(s);
2022
- return codexSurface(s);
2023
- }
2024
- return waitSurface(s);
2025
- }
2026
-
2027
- // ================================================================ HEADER
2028
- function statusCountsModel() {
2029
- var lc = laneCounts(currentData.streams);
2030
- return [
2031
- { id: "running", label: "live", color: "var(--accent-2)", count: lc.running },
2032
- { id: "complete", label: "done", color: "var(--green)", count: lc.complete },
2033
- { id: "blocked", label: "blocked", color: "var(--amber)", count: lc.blocked },
2034
- { id: "failed", label: "failed", color: "var(--red)", count: lc.failed }
2035
- ];
2036
- }
2037
- function buildStatusIndicator(counts, pct) {
2038
- var shown = counts.filter(function (c) { return c.count > 0; });
2039
- if (S.statusViz === "bar") {
2040
- var segs = shown.map(function (c) {
2041
- return '<span class="si-bar-seg" title="' + c.count + ' ' + c.label + '" data-action="status:' + c.id + '" style="flex-grow:' + c.count + ';background:' + c.color + '"></span>';
2042
- }).join("");
2043
- return '<div class="si-bar" role="group" aria-label="Lane status"><div class="si-bar-track">' + segs + '</div><span class="si-bar-label mono">' + pct + '%</span></div>';
2044
- }
2045
- if (S.statusViz === "ring") {
2046
- var present = shown;
2047
- var sum = present.reduce(function (a, c) { return a + c.count; }, 0) || 1;
2048
- var issues = counts.filter(function (c) { return c.id === "blocked" || c.id === "failed"; }).reduce(function (a, c) { return a + c.count; }, 0);
2049
- var running = (counts.filter(function (c) { return c.id === "running"; })[0] || {}).count || 0;
2050
- var R = 9, CIRC = 2 * Math.PI * R, acc = 0;
2051
- var arcs = present.map(function (c) {
2052
- var frac = c.count / sum, len = frac * CIRC;
2053
- var seg = '<circle cx="13" cy="13" r="' + R + '" fill="none" stroke="' + c.color + '" stroke-width="3.5" stroke-dasharray="' + len + ' ' + (CIRC - len) + '" stroke-dashoffset="' + (-acc * CIRC) + '" transform="rotate(-90 13 13)"></circle>';
2054
- acc += frac;
2055
- return seg;
2056
- }).join("");
2057
- var meta = issues > 0
2058
- ? '<span class="si-ring-issue mono" style="color:var(--amber)">' + issues + ' ⚠</span>'
2059
- : running > 0
2060
- ? '<span class="si-ring-issue mono" style="color:var(--accent-2)">' + running + ' live</span>'
2061
- : '<span class="si-ring-issue mono" style="color:var(--green)">done</span>';
2062
- return '<button class="si-ring" data-action="status:' + (issues ? "blocked" : "all") + '" title="' + pct + '% complete">'
2063
- + '<svg width="26" height="26" viewBox="0 0 26 26"><circle cx="13" cy="13" r="' + R + '" fill="none" stroke="var(--line)" stroke-width="3.5"></circle>' + arcs + '</svg>'
2064
- + '<span class="si-ring-meta"><span class="si-ring-pct mono">' + pct + '%</span>' + meta + '</span></button>';
2065
- }
2066
- return '<div class="si-badges" role="group" aria-label="Lane status">' + shown.map(function (c) {
2067
- var glow = c.id === "running" ? ";box-shadow:0 0 0 3px color-mix(in oklab, " + c.color + " 22%, transparent)" : "";
2068
- return '<button class="si-badge" aria-pressed="' + (S.statusSel.indexOf(c.id) >= 0 ? "true" : "false") + '" title="' + c.count + ' ' + c.label + '" data-action="status:' + c.id + '">'
2069
- + '<span class="si-dot" style="background:' + c.color + glow + '"></span><span class="si-n mono">' + c.count + '</span></button>';
2070
- }).join("") + '</div>';
2071
- }
2072
- function buildHeader() {
2073
- var run = currentData.run;
2074
- var overall = overallStatus(), pct = overallPct();
2075
- var counts = statusCountsModel();
2076
- var title = (run.scenario && run.scenario.title) || "Humanish run";
2077
- var persona = (run.persona && run.persona.name) || "";
2078
- return '<header class="hdr">'
2079
- + '<div class="hdr-brand"><span class="brand-mark">' + icon("live", 15) + '</span><span class="brand-word">Humanish</span></div>'
2080
- + '<div class="hdr-run">'
2081
- + '<div class="hdr-run-title" title="' + esc(title) + '">' + esc(title) + '</div>'
2082
- + '<div class="hdr-run-sub"><span class="hdr-persona">' + esc(persona) + '</span><span class="dot-sep"></span>'
2083
- + '<button class="run-chip mono" data-action="toggle-details" aria-expanded="' + (S.detailsOpen ? "true" : "false") + '">' + esc(run.runId || "run") + '</button></div></div>'
2084
- + '<div class="hdr-spacer"></div>'
2085
- + buildStatusIndicator(counts, pct)
2086
- + '<span class="status-pill" data-tone="' + tone(overall) + '">' + pip(overall, tone(overall) === "running") + statusLabel(overall) + '<span class="pct mono">' + pct + '%</span></span>'
2087
- + '<button class="hdr-runs" data-action="open-history">' + icon("clock", 15) + '<span>Runs</span></button>'
2088
- + '<button class="icon-btn" data-action="toggle-tweaks" aria-label="Settings" aria-pressed="' + (S.tweaksOpen ? "true" : "false") + '">' + icon("sliders") + '</button>'
2089
- + '<button class="icon-btn" data-action="toggle-theme" aria-label="Toggle theme">' + icon(S.theme === "light" ? "moon" : "sun") + '</button>'
2090
- + '</header>';
2091
- }
2092
-
2093
- // ================================================================ TOOLBAR
2094
- var STATUS_OPTS = [
2095
- { id: "running", label: "Live", color: "var(--accent-2)" },
2096
- { id: "complete", label: "Done", color: "var(--green)" },
2097
- { id: "blocked", label: "Blocked", color: "var(--amber)" },
2098
- { id: "failed", label: "Failed", color: "var(--red)" }
2099
- ];
2100
- var KIND_OPTS = [
2101
- { id: "ui", label: "Browser" },
2102
- { id: "terminal", label: "CLI" },
2103
- { id: "tui", label: "TUI" },
2104
- { id: "codex-ui", label: "Codex" }
2105
- ];
2106
- function optLabel(opts, id) {
2107
- for (var i = 0; i < opts.length; i += 1) { if (opts[i].id === id) return opts[i].label; }
2108
- return id;
2109
- }
2110
- function ddTrigger(kind, label, iconName, sel, opts) {
2111
- var allOn = sel.length === 0;
2112
- var summary = allOn ? label : (sel.length === 1 ? optLabel(opts, sel[0]) : (sel.length + " selected"));
2113
- return '<div class="dd"><button id="dd-trigger-' + kind + '" class="dd-trigger" data-action="dd:' + kind + '" data-active="' + (allOn ? "false" : "true") + '" aria-haspopup="true" aria-expanded="' + (openDd === kind ? "true" : "false") + '">'
2114
- + '<span class="dd-trigger-inner">' + (iconName ? icon(iconName, 14) : "") + '<span>' + esc(summary) + '</span></span>'
2115
- + (allOn ? "" : '<span class="dd-badge">' + sel.length + '</span>')
2116
- + icon("caret", 13) + '</button></div>';
2117
- }
2118
- function buildToolbar() {
2119
- if (S.view === "focus") return buildFocusToolbar();
2120
- var total = currentData.streams.length;
2121
- var shown = filteredStreams().length;
2122
- return '<div class="toolbar">'
2123
- + ddTrigger("status", "Status", "filter", S.statusSel, STATUS_OPTS)
2124
- + ddTrigger("kind", "Kind", null, S.kindSel, KIND_OPTS)
2125
- + '<label class="tb-search">' + icon("search", 14)
2126
- + '<input data-role="search" type="text" placeholder="Filter lanes…" value="' + esc(S.query) + '" aria-label="Filter lanes by name or persona"/></label>'
2127
- + '<span class="tb-result mono">' + shown + ' / ' + total + '</span>'
2128
- + buildLaneGroupSummary()
2129
- + '<div class="tb-spacer"></div>'
2130
- + '<div class="seg view-seg" role="group" aria-label="View mode">'
2131
- + '<button aria-pressed="' + (S.view === "grid" ? "true" : "false") + '" title="Grid view" data-action="view:grid">' + icon("grid", 15) + '</button>'
2132
- + '<button aria-pressed="' + (S.view === "focus" ? "true" : "false") + '" title="Focus view" data-action="view:focus">' + icon("focus", 15) + '</button></div>'
2133
- + '<div class="seg media-seg" role="group" aria-label="Media mode">'
2134
- + '<button aria-pressed="' + (S.media === "live" ? "true" : "false") + '"' + (hasLive() ? "" : " disabled") + ' title="Live streams" data-action="media:live">' + icon("live", 15) + '</button>'
2135
- + '<button aria-pressed="' + (S.media === "screenshot" ? "true" : "false") + '" title="Screenshots" data-action="media:screenshot">' + icon("image", 15) + '</button></div>'
2136
- + '<div class="seg density-seg" role="group" aria-label="Tile density">'
2137
- + '<button aria-pressed="' + (S.density === "comfortable" ? "true" : "false") + '" title="Comfortable" data-action="density:comfortable">' + icon("comfy", 15) + '</button>'
2138
- + '<button aria-pressed="' + (S.density === "compact" ? "true" : "false") + '" title="Compact" data-action="density:compact">' + icon("compact", 15) + '</button>'
2139
- + '<button aria-pressed="' + (S.density === "dense" ? "true" : "false") + '" title="Dense" data-action="density:dense">' + icon("dense", 15) + '</button></div>'
2140
- + '</div>';
2141
- }
2142
- function buildLaneGroupSummary() {
2143
- var groups = currentData.laneGroups || [];
2144
- if (!groups.length) return "";
2145
- var rows = [];
2146
- function add(key, value) {
2147
- if (!value) return;
2148
- var found = null;
2149
- for (var i = 0; i < rows.length; i += 1) {
2150
- if (rows[i].key === key && rows[i].value === value) { found = rows[i]; break; }
2151
- }
2152
- if (found) found.count += 1;
2153
- else rows.push({ key: key, value: value, count: 1 });
2154
- }
2155
- groups.forEach(function (group) {
2156
- add("actorType", group.actorType);
2157
- add("surface", group.surface);
2158
- add("caseGroup", group.caseGroup);
2159
- });
2160
- if (!rows.length) return "";
2161
- return '<div class="lane-groups" aria-label="Lane groups">'
2162
- + rows.slice(0, 6).map(function (row) {
2163
- var title = row.key + ": " + row.value + " (" + row.count + ")";
2164
- return '<span class="lane-group-chip" data-lane-group="' + esc(row.key) + '" title="' + esc(title) + '">'
2165
- + '<span class="lane-group-k">' + esc(row.key) + '</span>'
2166
- + '<span class="lane-group-v">' + esc(row.value) + '</span>'
2167
- + '<span class="lane-group-n">' + row.count + '</span>'
2168
- + '</span>';
2169
- }).join("")
2170
- + '</div>';
2171
- }
2172
- function buildFocusToolbar() {
2173
- var ss = currentData.streams;
2174
- if (!ss.length) return '<div class="toolbar focus-toolbar"><div class="crumbs"><span class="cur">No lanes</span></div></div>';
2175
- var idx = focusIndex();
2176
- var s = ss[idx];
2177
- var live = tone(s.status) === "running";
2178
- return '<div class="toolbar focus-toolbar">'
2179
- + '<div class="crumbs">'
2180
- + '<button class="crumb-rail-toggle" data-action="toggle-rail" aria-label="' + (S.railCollapsed ? "Show lane rail" : "Hide lane rail") + '" title="' + (S.railCollapsed ? "Show lane rail - [" : "Hide lane rail - [") + '">' + icon("list", 15) + '</button>'
2181
- + '<button data-action="exit-focus">Grid</button><span class="sep">/</span>'
2182
- + '<span class="mono" style="color:var(--text-3)">' + pad2(idx + 1) + '</span>'
2183
- + '<span class="cur" title="' + esc(laneName(s)) + '">' + esc(laneName(s)) + '</span></div>'
2184
- + '<span class="focus-status" data-tone="' + tone(s.status) + '" style="color:' + statusColor(s.status) + '">' + pip(s.status, live) + ' ' + statusLabel(s.status) + '</span>'
2185
- + '<div class="focus-nav"><button class="icon-btn" data-action="nav:-1" aria-label="Previous lane">' + icon("chevL") + '</button>'
2186
- + '<span class="focus-stepper mono">' + (idx + 1) + ' / ' + ss.length + '</span>'
2187
- + '<button class="icon-btn" data-action="nav:1" aria-label="Next lane">' + icon("chevR") + '</button></div>'
2188
- + '<button class="icon-btn" data-action="toggle-side" aria-pressed="' + (S.sideCollapsed ? "false" : "true") + '" aria-label="' + (S.sideCollapsed ? "Show details panel" : "Hide details panel") + '" title="' + (S.sideCollapsed ? "Show details panel - ]" : "Hide details panel - ]") + '">' + icon("panelRight") + '</button>'
2189
- + '<button class="icon-btn" data-action="exit-focus" aria-label="Close focus (Esc)">' + icon("x") + '</button>'
2190
- + '</div>';
2191
- }
2192
- function buildDdMenu() {
2193
- if (!openDd) return "";
2194
- var kind = openDd;
2195
- var label = kind === "status" ? "Status" : "Kind";
2196
- var opts = kind === "status" ? STATUS_OPTS : KIND_OPTS;
2197
- var sel = kind === "status" ? S.statusSel : S.kindSel;
2198
- var cnt = kind === "status" ? statusCount : kindCount;
2199
- var allOn = sel.length === 0;
2200
- var rows = '<button class="dd-row dd-row-all" data-action="dd-all:' + kind + '"><span class="dd-check" data-on="' + (allOn ? "true" : "false") + '">' + (allOn ? icon("check", 11) : "") + '</span><span class="dd-row-label">All ' + label.toLowerCase() + '</span></button><div class="dd-sep"></div>';
2201
- rows += opts.map(function (o) {
2202
- var on = sel.indexOf(o.id) >= 0;
2203
- return '<button class="dd-row" data-action="dd-row:' + kind + ':' + o.id + '"><span class="dd-check" data-on="' + (on ? "true" : "false") + '">' + (on ? icon("check", 11) : "") + '</span>'
2204
- + (o.color ? '<span class="dd-dot" style="background:' + o.color + '"></span>' : "")
2205
- + '<span class="dd-row-label">' + o.label + '</span><span class="dd-row-n mono">' + cnt(o.id) + '</span></button>';
2206
- }).join("");
2207
- return '<div class="dd-menu" id="dd-menu" data-dd="' + kind + '" role="menu" style="visibility:hidden">' + rows + '</div>';
2208
- }
2209
-
2210
- // ================================================================ GRID
2211
- var TILE_MIN = { comfortable: "440px", compact: "330px", dense: "240px" };
2212
- function buildTile(s, i) {
2213
- var live = tone(s.status) === "running";
2214
- var geometryDetail = geometryDetailFor(s);
2215
- var accessibleLabel = "Open lane " + pad2(i + 1) + ": " + laneName(s) + (geometryDetail ? ". " + geometryDetail : "");
2216
- return '<article class="tile" data-selected="' + (s.id === S.focusedId ? "true" : "false") + '" tabindex="0" role="button" data-action="open:' + esc(s.id) + '" aria-label="' + esc(accessibleLabel) + '">'
2217
- + '<header class="tile-head"><span class="tile-idx mono">' + pad2(i + 1) + '</span>' + pip(s.status, live)
2218
- + '<span class="tile-name" title="' + esc(laneName(s)) + '">' + esc(laneName(s)) + '</span>'
2219
- + '<span class="kind-badge" data-kind="' + esc(s.kind) + '">' + esc(s.kindLabel || s.kind) + '</span>'
2220
- + '<span class="tile-dims mono"' + (geometryDetail ? ' title="' + esc(geometryDetail) + '"' : '') + '>' + esc(dimsFor(s)) + '</span>'
2221
- + '<span class="tile-open">' + icon("expand", 13) + '</span></header>'
2222
- + '<div class="tile-surface" style="--aspect:' + aspectFor(s) + '">' + streamSurface(s, false) + '</div>'
2223
- + '<footer class="tile-foot" data-status="' + esc(s.status) + '"><span class="tile-foot-text">' + (live ? '<span class="now-dot">▸</span>' : '') + esc(laneStep(s)) + '</span>'
2224
- + '<span class="mini-prog"><span style="width:' + laneProgress(s) + '%"></span></span></footer>'
2225
- + '</article>';
2226
- }
2227
- function buildGrid() {
2228
- var streams = filteredStreams();
2229
- if (!streams.length) {
2230
- var msg = S.query ? ('Nothing matches "' + esc(S.query) + '".') : "Try a different status or kind.";
2231
- return '<div class="grid-scroll"><div class="grid"><div class="grid-empty">'
2232
- + '<div class="ge-icon">' + icon("search", 20) + '</div><h3>No lanes match your filters</h3>'
2233
- + '<div>' + msg + ' <button class="linklike" data-action="clear-filters">Clear filters</button></div></div></div></div>';
2234
- }
2235
- var tiles = streams.map(function (s, i) { return buildTile(s, i); }).join("");
2236
- return '<div class="grid-scroll"><div class="grid" style="--tile-min:' + TILE_MIN[S.density] + '">' + tiles + '</div></div>';
2237
- }
2238
-
2239
- // ================================================================ FOCUS
2240
- function focusIndex() {
2241
- var ss = currentData.streams;
2242
- var idx = -1;
2243
- for (var i = 0; i < ss.length; i += 1) { if (ss[i].id === S.focusedId) { idx = i; break; } }
2244
- return idx < 0 ? 0 : idx;
2245
- }
2246
- function railIcon(kind) {
2247
- if (kind === "terminal" || kind === "tui") return "terminal";
2248
- if (kind === "codex-ui") return "spark";
2249
- return "globe";
2250
- }
2251
- function artifactHref(a) {
2252
- return a && a.path ? linkHref(a.path, true) : "";
2253
- }
2254
- function ensureArtifactLoaded(a) {
2255
- if (!a || !a.path || a.kind !== "filesystem") return;
2256
- var key = a.path;
2257
- if (artifactCache[key]) return;
2258
- if (location.protocol === "file:") {
2259
- artifactCache[key] = { status: "offline", text: "", json: null };
2260
- return;
2261
- }
2262
- var href = artifactHref(a);
2263
- if (!href) {
2264
- artifactCache[key] = { status: "error", text: "", json: null, error: "Artifact path is not fetchable." };
2265
- return;
2266
- }
2267
- artifactCache[key] = { status: "loading", text: "", json: null };
2268
- fetch(href, { cache: "no-store" }).then(function (r) {
2269
- if (!r.ok) throw new Error("HTTP " + r.status);
2270
- return r.text();
2271
- }).then(function (text) {
2272
- var json = null;
2273
- try { json = JSON.parse(text); } catch (e) {}
2274
- artifactCache[key] = { status: "loaded", text: text, json: json };
2275
- render();
2276
- }).catch(function (e) {
2277
- artifactCache[key] = { status: "error", text: "", json: null, error: e && e.message ? e.message : "Fetch failed." };
2278
- render();
2279
- });
2280
- }
2281
- function formatBytes(n) {
2282
- if (typeof n !== "number" || !isFinite(n)) return "";
2283
- if (n < 1024) return n + "b";
2284
- if (n < 1024 * 1024) return Math.round(n / 102.4) / 10 + "kb";
2285
- return Math.round(n / 1024 / 102.4) / 10 + "mb";
2286
- }
2287
- function renderGenericArtifact(a, state) {
2288
- if (!state || state.status === "loading") return '<div class="fi-load"><span class="wait-spinner" style="width:18px;height:18px;display:inline-block;vertical-align:middle;margin-right:8px"></span>Loading artifact…</div>';
2289
- if (state.status === "offline") return '<div class="fi-load">Static file view cannot hydrate artifacts inline. Open the artifact link directly.</div>';
2290
- if (state.status === "error") return '<div class="fi-load">Could not load artifact: ' + esc(state.error || "unknown error") + '</div>';
2291
- return '<div class="file-inspector"><div class="fi-head"><div class="fi-title"><h3>' + esc(a.label || a.path) + '</h3><span class="fi-status">artifact</span></div>'
2292
- + '<div class="fi-summary mono">' + esc(a.path) + '</div></div>'
2293
- + '<div class="fi-section"><pre class="fi-pre">' + esc(state.text || "") + '</pre></div></div>';
2294
- }
2295
- function renderSetupQualityArtifact(a, state) {
2296
- if (!state || state.status !== "loaded" || !state.json || state.json.schema !== "humanish.setup-quality.v1") {
2297
- return renderGenericArtifact(a, state);
2298
- }
2299
- var q = state.json;
2300
- var checks = q.checks || [];
2301
- var mim = q.humanish || {};
2302
- var scripts = q.packageScripts || {};
2303
- var tree = q.tree || [];
2304
- var previews = q.previews || [];
2305
- var scriptRows = Object.keys(scripts).sort().slice(0, 18).map(function (key) {
2306
- return '<div class="fi-tree-row"><span>$</span><span>' + esc(key) + '</span><span class="fi-tree-size">' + esc(scripts[key]) + '</span></div>';
2307
- }).join("");
2308
- var treeRows = tree.slice(0, 160).map(function (entry) {
2309
- return '<div class="fi-tree-row" data-type="' + esc(entry.type || "file") + '"><span>' + (entry.type === "directory" ? "dir" : "file") + '</span><span>' + esc(entry.path) + '</span><span class="fi-tree-size">' + esc(formatBytes(entry.sizeBytes)) + '</span></div>';
2310
- }).join("");
2311
- var previewRows = previews.length ? previews.map(function (preview) {
2312
- return '<div class="fi-preview"><div class="fi-preview-head"><span>' + esc(preview.path) + '</span><span>' + esc(preview.language || "text") + (preview.truncated ? " · truncated" : "") + '</span></div><pre class="fi-pre">' + esc(preview.text || "") + '</pre></div>';
2313
- }).join("") : '<div class="tab-empty">No raw file previews persisted for this run.</div>';
2314
- return '<div class="file-inspector"><div class="fi-head"><div class="fi-title"><h3>Setup quality</h3><span class="fi-status" data-status="' + esc(q.status || "unknown") + '">' + esc(q.status || "unknown") + '</span></div>'
2315
- + '<div class="fi-summary">' + esc(q.summary || "") + '</div><div class="fi-summary mono">' + esc(a.path) + '</div></div>'
2316
- + '<div class="fi-section"><div class="fi-grid">'
2317
- + '<div class="fi-stat"><div class="fi-stat-k">personas</div><div class="fi-stat-v">' + esc(mim.personaCount || 0) + '</div></div>'
2318
- + '<div class="fi-stat"><div class="fi-stat-k">scenarios</div><div class="fi-stat-v">' + esc(mim.scenarioCount || 0) + '</div></div>'
2319
- + '<div class="fi-stat"><div class="fi-stat-k">config</div><div class="fi-stat-v">' + (mim.configPresent ? "present" : "missing") + '</div></div>'
2320
- + '<div class="fi-stat"><div class="fi-stat-k">runtime ignore</div><div class="fi-stat-v">' + (mim.gitignoreContainsRuntimeIgnore ? "present" : "missing") + '</div></div>'
2321
- + '</div></div>'
2322
- + '<div class="fi-section"><div class="fi-section-title"><span>Checks</span><span class="mono">' + checks.filter(function (c) { return c.ok; }).length + ' / ' + checks.length + '</span></div>'
2323
- + checks.map(function (check) { return '<div class="fi-check" data-ok="' + (check.ok ? "true" : "false") + '"><span class="fi-dot">' + icon(check.ok ? "check" : "alert", 11) + '</span><div><div class="fi-check-name">' + esc(check.label) + '</div><div class="fi-check-detail">' + esc(check.detail) + '</div></div></div>'; }).join("") + '</div>'
2324
- + '<div class="fi-section"><div class="fi-section-title"><span>Package scripts</span><span class="mono">' + Object.keys(scripts).length + '</span></div><div class="fi-tree">' + (scriptRows || '<div class="tab-empty">No package scripts captured.</div>') + '</div></div>'
2325
- + '<div class="fi-section"><div class="fi-section-title"><span>Tree</span><span class="mono">' + tree.length + '</span></div><div class="fi-tree">' + (treeRows || '<div class="tab-empty">No tree entries captured.</div>') + '</div></div>'
2326
- + '<div class="fi-section"><div class="fi-section-title"><span>Previews</span><span class="mono">' + previews.length + '</span></div>' + previewRows + '</div></div>';
2327
- }
2328
- function buildFilesTab(s) {
2329
- var arts = laneArtifacts(s);
2330
- if (!arts.length) return '<div class="tab-empty">No evidence artifacts linked.</div>';
2331
- var selected = null;
2332
- for (var i = 0; i < arts.length; i += 1) {
2333
- if (arts[i].path === S.filePath) { selected = arts[i]; break; }
2334
- }
2335
- if (!selected) {
2336
- for (var j = 0; j < arts.length; j += 1) {
2337
- if (arts[j].kind === "filesystem") { selected = arts[j]; break; }
2338
- }
2339
- }
2340
- var inspector = "";
2341
- if (selected && selected.kind === "filesystem") {
2342
- ensureArtifactLoaded(selected);
2343
- inspector = renderSetupQualityArtifact(selected, artifactCache[selected.path]);
2344
- }
2345
- var rows = arts.map(function (a) {
2346
- var href = artifactHref(a);
2347
- var isSelected = selected && selected.path === a.path;
2348
- var inspectable = a.kind === "filesystem";
2349
- var main = inspectable
2350
- ? '<button data-action="inspect-file:' + esc(a.path) + '"><span class="file-ic">' + icon("file", 14) + '</span><span class="file-meta"><div class="file-name">' + esc(a.label) + '</div><div class="file-path mono">' + esc(a.path) + '</div></span><span class="file-kind">' + esc(a.kind) + '</span></button>'
2351
- : '<a href="' + esc(href) + '" target="_blank" rel="noopener noreferrer" data-action="external"><span class="file-ic">' + icon("file", 14) + '</span><span class="file-meta"><div class="file-name">' + esc(a.label) + '</div><div class="file-path mono">' + esc(a.path) + '</div></span><span class="file-kind">' + esc(a.kind) + '</span></a>';
2352
- return '<div class="file-row" data-selected="' + (isSelected ? "true" : "false") + '">' + main
2353
- + (href ? '<a class="file-open" href="' + esc(href) + '" target="_blank" rel="noopener noreferrer" data-action="external">open</a>' : "")
2354
- + '</div>';
2355
- }).join("");
2356
- return inspector + rows;
2357
- }
2358
- function buildSideBody(s) {
2359
- if (S.tab === "events") {
2360
- var evs = laneEvents(s);
2361
- if (!evs.length) return '<div class="tab-empty">No orchestrator lifecycle events recorded yet.</div>';
2362
- return evs.map(function (ev, i) {
2363
- var ic = (ev.level === "error" || ev.level === "warn") ? "alert" : "info";
2364
- return '<div class="evt" data-level="' + esc(ev.level) + '"><div class="evt-rail"><span class="evt-icon">' + icon(ic, 12) + '</span>' + (i === evs.length - 1 ? '' : '<span class="evt-conn"></span>') + '</div>'
2365
- + '<div><div class="evt-text">' + esc(ev.message) + '</div><div class="evt-meta"><span class="evt-type mono">' + esc(ev.type) + '</span><span>· ' + esc(shortTime(ev.at) || "just now") + '</span></div></div></div>';
2366
- }).join("");
2367
- }
2368
- if (S.tab === "files") {
2369
- return buildFilesTab(s);
2370
- }
2371
- return buildLogsTab(s);
2372
- }
2373
- function buildLogsTab(s) {
2374
- var sections = logSections(s);
2375
- if (!sections.length) return '<div class="tab-empty">No agent or terminal log text recorded for this lane.</div>';
2376
- return '<div class="log-panel"><div class="log-scroll">' + sections.map(function (section) {
2377
- var rows = section.lines.map(function (ln, i) {
2378
- return '<div class="log-row"><span class="log-num">' + pad2(i + 1) + '</span><span class="log-txt ' + ln.cls + '">' + esc(ln.text || " ") + '</span></div>';
2379
- }).join("");
2380
- return '<section class="log-section" data-log-section="' + esc(section.id) + '"><div class="log-section-head"><span class="log-section-title">' + esc(section.title) + '</span><span class="log-section-count">' + section.lines.length + ' lines</span></div>' + rows + '</section>';
2381
- }).join("") + '</div></div>';
2382
- }
2383
- function buildFocus() {
2384
- var ss = currentData.streams;
2385
- if (!ss.length) return '<div class="grid-empty"><h3>No lanes in this run.</h3></div>';
2386
- var idx = focusIndex();
2387
- var s = ss[idx];
2388
- var run = currentData.run;
2389
- var live = tone(s.status) === "running";
2390
- var tabs = [
2391
- { id: "logs", label: hasAgentLogs(s) ? "Agent Logs" : "Logs", n: logLineCount(s) },
2392
- { id: "files", label: "Files", n: laneArtifacts(s).length },
2393
- { id: "events", label: "Lifecycle", n: laneEvents(s).length }
2394
- ];
2395
- var rail = '<aside class="focus-rail" aria-label="Lanes"><div class="focus-rail-head"><span class="eyebrow">' + ss.length + ' lanes</span>'
2396
- + '<button class="icon-btn" style="width:26px;height:26px" data-action="toggle-rail" aria-label="Collapse lane rail">' + icon("chevL", 15) + '</button></div>'
2397
- + '<div class="focus-rail-list">' + ss.map(function (r) {
2398
- return '<button class="rail-item" data-selected="' + (r.id === s.id ? "true" : "false") + '" data-action="select:' + esc(r.id) + '">'
2399
- + '<span class="rail-thumb">' + icon(railIcon(r.kind), 13) + '</span>'
2400
- + '<span class="rail-meta"><span class="rail-name">' + esc(laneName(r)) + '</span><span class="rail-sub">' + pip(r.status, tone(r.status) === "running") + ' ' + esc(r.kindLabel || r.kind) + '</span></span></button>';
2401
- }).join("") + '</div></aside>';
2402
-
2403
- var stage = '<section class="focus-stage"><div class="focus-stage-area"><div class="focus-frame" style="--aspect:' + aspectFor(s) + '">' + streamSurface(s, true) + '</div>' + focusGeometry(s) + '</div></section>';
2404
-
2405
- var side = '<aside class="focus-side" aria-hidden="' + (S.sideCollapsed ? "true" : "false") + '" data-sheet="' + (S.sheetOpen ? "open" : "closed") + '">'
2406
- + '<button class="sheet-grip" data-action="toggle-sheet" aria-label="Toggle details"></button>'
2407
- + '<div class="side-context"><div class="side-persona-row"><span class="side-avatar mono">' + esc(initials((run.persona && run.persona.name) || "")) + '</span>'
2408
- + '<div style="min-width:0"><div class="side-persona-name">' + esc((run.persona && run.persona.name) || "Persona") + '</div><div class="side-persona-id mono">persona driving · ' + esc(laneName(s)) + '</div></div></div>'
2409
- + '<div class="side-goal"><span class="eyebrow">Persona</span><div class="side-goal-text">' + esc((run.persona && run.persona.name) || "Synthetic persona") + ' is attempting this lane as a realistic setup operator.</div></div>'
2410
- + '<div class="side-goal"><span class="eyebrow">Scenario</span><div class="side-goal-text">' + esc((run.scenario && run.scenario.goal) || laneSummary(s)) + '</div></div>'
2411
- + '<div class="side-now"><div class="side-now-row">' + pip(s.status, live) + '<span class="eyebrow" style="color:' + (live ? "var(--accent-2)" : "var(--text-3)") + '">' + (live ? "Now" : "Last step") + '</span></div>'
2412
- + '<div class="side-now-text">' + esc(laneStep(s)) + '</div></div>'
2413
- + (laneCostText(s) ? '<div class="side-goal"><span class="eyebrow">Est. cost</span><div class="side-goal-text">' + esc(laneCostText(s)) + '</div></div>' : "")
2414
- + buildMeaningfulUseCard(s) + '</div>'
2415
- + '<div class="side-tabs" role="tablist">' + tabs.map(function (t) {
2416
- return '<button class="side-tab" role="tab" aria-selected="' + (S.tab === t.id ? "true" : "false") + '" data-action="tab:' + t.id + '">' + t.label + '<span class="tab-n">' + t.n + '</span></button>';
2417
- }).join("") + '</div>'
2418
- + '<div class="side-body">' + buildSideBody(s) + '</div></aside>';
2419
- return '<div class="focus" data-rail="' + (S.railCollapsed ? "collapsed" : "open") + '" data-side="' + (S.sideCollapsed ? "collapsed" : "open") + '">' + rail + stage + side + '</div>';
2420
- }
2421
- function statusColor(st) {
2422
- var t = tone(st);
2423
- return t === "running" ? "var(--accent-2)" : t === "complete" ? "var(--green)" : t === "blocked" ? "var(--amber)" : st === "failed" ? "var(--red)" : "var(--text-2)";
2424
- }
2425
-
2426
- // ================================================================ CONSOLE + STATUSBAR
2427
- function buildConsole() {
2428
- var lines = consoleLines();
2429
- var rows = lines.map(function (ln) {
2430
- return '<div class="console-line"><span class="console-t">' + esc(ln.t) + '</span><span class="console-txt ' + ln.lvl + '">' + esc(ln.text) + '</span></div>';
2431
- }).join("");
2432
- var caretT = lines.length ? lines[lines.length - 1].t : "";
2433
- return '<section class="console" style="height:230px" role="dialog" aria-label="Run console">'
2434
- + '<header class="console-head"><span class="console-title">' + icon("terminal", 14) + ' Run console <span class="mono console-cmd">humanish watch</span></span>'
2435
- + (hasLive() ? '<span class="console-live">' + pip("running", true) + ' tailing</span>' : '')
2436
- + '<div class="tb-spacer"></div><span class="console-meta mono">' + lines.length + ' lines</span>'
2437
- + '<button class="icon-btn" style="width:28px;height:28px" data-action="toggle-console" aria-label="Close console">' + icon("x", 15) + '</button></header>'
2438
- + '<div class="console-body mono" id="console-body">' + rows
2439
- + '<div class="console-line"><span class="console-t">' + esc(caretT) + '</span><span class="console-txt"><span class="term-caret"></span></span></div></div></section>';
2440
- }
2441
- function buildStatusBar() {
2442
- var overall = overallStatus(), pct = overallPct();
2443
- var run = currentData.run;
2444
- var counts = statusCountsModel().filter(function (c) { return c.count > 0; });
2445
- var last = consoleLines();
2446
- var lastLine = last[last.length - 1];
2447
- var peek = (!S.consoleOpen && lastLine) ? ('<span class="sb-console-peek mono"><span class="sb-peek-t">' + esc(lastLine.t) + '</span><span class="sb-peek-txt ' + lastLine.lvl + '">' + esc(lastLine.text) + '</span></span>') : "";
2448
- return '<footer class="statusbar">'
2449
- + '<span class="sb-status" data-tone="' + tone(overall) + '">' + pip(overall, tone(overall) === "running") + '<span class="sb-status-label mono">' + statusLabel(overall) + '</span><span class="sb-pct mono">' + pct + '%</span></span>'
2450
- + '<span class="sb-prog"><span style="width:' + pct + '%" data-tone="' + tone(overall) + '"></span></span>'
2451
- + '<span class="sb-counts">' + counts.map(function (c) { return '<span class="sb-count" title="' + c.count + ' ' + c.label + '"><span class="si-dot" style="background:' + c.color + '"></span><span class="mono sb-count-number">' + c.count + '</span><span class="mono sb-count-label">' + c.label + '</span></span>'; }).join("") + '</span>'
2452
- + '<span class="sb-run mono">' + esc("mode: " + (run.mode || "unknown") + " · " + (run.runId || "")) + '</span>'
2453
- + (currentData.cost ? '<span class="sb-run sb-cost mono" title="' + esc(currentData.cost.note || "") + '">' + esc("cost: " + runCostText()) + '</span>' : "")
2454
- + '<div class="tb-spacer"></div>'
2455
- + '<button class="sb-console" aria-expanded="' + (S.consoleOpen ? "true" : "false") + '" data-action="toggle-console" title="Run console (backtick)">' + icon("terminal", 14)
2456
- + '<span class="sb-console-label">Run console</span>' + peek
2457
- + '<span class="sb-chev' + (S.consoleOpen ? " open" : "") + '">' + icon("caret", 13) + '</span></button>'
2458
- + '</footer>';
2459
- }
2460
-
2461
- // ================================================================ DRAWER + POPOVERS
2462
- function buildPopover() {
2463
- var run = currentData.run;
2464
- return '<div class="pop" role="dialog" aria-label="Run details">'
2465
- + popRow("Run id", esc(run.runId || ""))
2466
- + popRow("Started", esc(shortStamp(run.createdAt)))
2467
- + popRow("Mode", '<span class="tag">' + esc(run.mode || "") + '</span>')
2468
- + popRow("Package", esc(run.packageName || "(none)"))
2469
- + (currentData.cost ? popRow("Est. cost", esc(runCostText())) : "")
2470
- + popRow("Evidence", '<span class="tag" data-tone="green">' + icon("lock", 10) + ' local-only</span>')
2471
- + '</div>';
2472
- }
2473
- function popRow(k, v) { return '<div class="pop-row"><span class="pop-k">' + k + '</span><span class="pop-v">' + v + '</span></div>'; }
2474
-
2475
- function buildTweaks() {
2476
- return '<div class="tweaks-pop" role="dialog" aria-label="Settings">'
2477
- + '<div class="tw-sec">Appearance</div>'
2478
- + twSeg("Theme", "theme", S.theme, [["dark", "dark"], ["light", "light"]])
2479
- + '<div class="tw-row"><span class="tw-label">Accent</span><span class="tw-swatches">'
2480
- + ["#4d7cfe", "#7c5cff", "#34d399", "#f0a92b", "#fb5d52"].map(function (col) {
2481
- return '<button class="tw-swatch" aria-pressed="' + (S.accent === col ? "true" : "false") + '" data-action="tweak:accent:' + col + '" style="background:' + col + '" title="' + col + '"></button>';
2482
- }).join("") + '</span></div>'
2483
- + '<div class="tw-sec">Layout</div>'
2484
- + twSeg("Density", "density", S.density, [["comfortable", "comfy"], ["compact", "compact"], ["dense", "dense"]])
2485
- + twSeg("Status", "statusViz", S.statusViz, [["badges", "badges"], ["bar", "bar"], ["ring", "ring"]])
2486
- + '<div class="tw-sec">Motion</div>'
2487
- + twSeg("Motion", "motion", S.motion, [["full", "full"], ["reduced", "reduced"]])
2488
- + '</div>';
2489
- }
2490
- function twSeg(label, key, value, opts) {
2491
- return '<div class="tw-row"><span class="tw-label">' + label + '</span><span class="tw-seg">'
2492
- + opts.map(function (o) { return '<button aria-pressed="' + (value === o[0] ? "true" : "false") + '" data-action="tweak:' + key + ':' + o[0] + '">' + o[1] + '</button>'; }).join("")
2493
- + '</span></div>';
2494
- }
2495
-
2496
- function historyRuns() {
2497
- if (historyIndex && historyIndex.runs && historyIndex.runs.length) return historyIndex.runs;
2498
- var run = currentData.run;
2499
- return [{ runId: run.runId, createdAt: run.createdAt, mode: run.mode, status: overallStatus(), streamCount: currentData.streams.length, href: null,
2500
- estimatedCostUsd: currentData.cost ? currentData.cost.estimatedTotalUsd : null,
2501
- costRatesAsOf: currentData.cost ? currentData.cost.ratesAsOf : null,
2502
- costPlaceholder: currentData.cost ? currentData.cost.placeholder : false }];
2503
- }
2504
- function buildDrawer() {
2505
- var runs = historyRuns();
2506
- var activeId = currentData.run.runId;
2507
- var rows = runs.map(function (r) {
2508
- var t = tone(r.status);
2509
- var col = t === "running" ? "var(--accent-2)" : t === "complete" ? "var(--green)" : t === "blocked" ? "var(--amber)" : r.status === "failed" ? "var(--red)" : "var(--text-2)";
2510
- var liveTagTxt = (r.runId === activeId && t === "running") ? '<span style="color:var(--accent-2)"> · live</span>' : "";
2511
- var meta = (r.mode || "run") + " · " + (r.streamCount || 0) + " lanes · " + shortStamp(r.createdAt);
2512
- if (r.estimatedCostUsd != null) meta += " · ~$" + (fmtCostUsd(r.estimatedCostUsd) || "0") + " est." + (r.costPlaceholder ? " (placeholder)" : "");
2513
- var metaTitle = r.estimatedCostUsd != null && r.costRatesAsOf ? "estimated, rates as of " + r.costRatesAsOf : "";
2514
- return '<button class="run-row" data-active="' + (r.runId === activeId ? "true" : "false") + '" data-action="run:' + esc(r.runId) + '">'
2515
- + pip(r.status, t === "running")
2516
- + '<span class="run-row-id mono">' + esc(r.runId) + liveTagTxt + '</span>'
2517
- + '<span class="run-row-stat" style="color:' + col + '">' + statusLabel(r.status) + '</span>'
2518
- + '<span class="run-row-meta mono"' + (metaTitle ? ' title="' + esc(metaTitle) + '"' : "") + '>' + esc(meta) + '</span></button>';
2519
- }).join("");
2520
- return '<div class="scrim" data-action="close-history"></div><aside class="drawer" role="dialog" aria-label="Run history">'
2521
- + '<header class="drawer-head"><div><span class="eyebrow">Run history</span><h2>Recent runs</h2></div>'
2522
- + '<button class="icon-btn" data-action="close-history" aria-label="Close">' + icon("x") + '</button></header>'
2523
- + '<div class="drawer-list">' + rows + '</div></aside>';
2524
- }
2525
-
2526
- // ================================================================ RENDER
2527
- function captureScrolls() {
2528
- var map = {};
2529
- [".grid-scroll", ".console-body", ".focus-rail-list", ".side-body", ".focus-stage-area", ".drawer-list", ".log-scroll"].forEach(function (sel) {
2530
- var el = app.querySelector(sel);
2531
- if (el) map[sel] = el.scrollTop;
2532
- });
2533
- return map;
2534
- }
2535
- function restoreScrolls(map) {
2536
- Object.keys(map).forEach(function (sel) {
2537
- var el = app.querySelector(sel);
2538
- if (el) el.scrollTop = map[sel];
2539
- });
2540
- }
2541
- function placeMenus() {
2542
- var menu = document.getElementById("dd-menu");
2543
- if (!menu) return;
2544
- var kind = menu.getAttribute("data-dd");
2545
- var trg = document.getElementById("dd-trigger-" + kind);
2546
- if (!trg) { menu.style.visibility = "visible"; return; }
2547
- var w = 210;
2548
- var r = trg.getBoundingClientRect();
2549
- var left = r.left;
2550
- if (left + w > window.innerWidth - 8) left = Math.max(8, window.innerWidth - 8 - w);
2551
- menu.style.width = w + "px";
2552
- menu.style.top = (r.bottom + 6) + "px";
2553
- menu.style.left = left + "px";
2554
- menu.style.visibility = "visible";
2555
- }
2556
- function ensureLiveStreamHost() {
2557
- if (liveStreamHost) return liveStreamHost;
2558
- if (!document.createElement) return null;
2559
- var host = document.body || document.documentElement;
2560
- if (!host || !host.appendChild) return null;
2561
- liveStreamHost = document.createElement("div");
2562
- liveStreamHost.setAttribute("data-live-stream-host", "true");
2563
- liveStreamHost.style.position = "fixed";
2564
- liveStreamHost.style.inset = "0";
2565
- liveStreamHost.style.overflow = "visible";
2566
- liveStreamHost.style.pointerEvents = "none";
2567
- liveStreamHost.style.zIndex = "20";
2568
- host.appendChild(liveStreamHost);
2569
- return liveStreamHost;
2570
- }
2571
- function createLiveStreamRecord(id, url, title) {
2572
- var host = ensureLiveStreamHost();
2573
- if (!host) return null;
2574
- var wrapper = document.createElement("div");
2575
- wrapper.setAttribute("data-live-stream-wrapper", id);
2576
- wrapper.style.position = "fixed";
2577
- wrapper.style.overflow = "hidden";
2578
- wrapper.style.background = "#000";
2579
- wrapper.style.pointerEvents = "none";
2580
- wrapper.style.display = "none";
2581
- var frame = document.createElement("iframe");
2582
- frame.style.position = "absolute";
2583
- frame.style.border = "0";
2584
- frame.style.margin = "0";
2585
- frame.style.display = "block";
2586
- frame.setAttribute("allow", "clipboard-read; clipboard-write; fullscreen");
2587
- frame.setAttribute("referrerpolicy", "no-referrer");
2588
- frame.setAttribute("title", title || "live stream");
2589
- frame.setAttribute("data-live-stream-frame", id);
2590
- frame.src = url;
2591
- var overlay = document.createElement("div");
2592
- overlay.className = "live-stream-overlay";
2593
- overlay.style.position = "absolute";
2594
- overlay.style.overflow = "hidden";
2595
- overlay.style.pointerEvents = "none";
2596
- overlay.style.zIndex = "2";
2597
- wrapper.appendChild(frame);
2598
- wrapper.appendChild(overlay);
2599
- host.appendChild(wrapper);
2600
- return { url: url, wrapper: wrapper, frame: frame, overlay: overlay, overlayHtml: "" };
2601
- }
2602
- function removeLiveStreamRecord(id) {
2603
- var rec = liveStreamFrames[id];
2604
- if (rec && rec.wrapper && rec.wrapper.parentNode) rec.wrapper.parentNode.removeChild(rec.wrapper);
2605
- delete liveStreamFrames[id];
2606
- }
2607
- function hideLiveStreamRecord(rec) {
2608
- if (!rec || !rec.wrapper) return;
2609
- rec.wrapper.style.display = "none";
2610
- rec.wrapper.style.pointerEvents = "none";
2611
- }
2612
- function clipRect(rect) {
2613
- var stage = app.querySelector && app.querySelector(".stage");
2614
- var s = stage && stage.getBoundingClientRect ? stage.getBoundingClientRect() : null;
2615
- var left = Math.max(rect.left, 0, s ? s.left : 0);
2616
- var top = Math.max(rect.top, 0, s ? s.top : 0);
2617
- var right = Math.min(rect.right, window.innerWidth || rect.right, s ? s.right : rect.right);
2618
- var bottom = Math.min(rect.bottom, window.innerHeight || rect.bottom, s ? s.bottom : rect.bottom);
2619
- return { left: left, top: top, right: right, bottom: bottom, width: Math.max(0, right - left), height: Math.max(0, bottom - top) };
2620
- }
2621
- function layoutLiveStreamRecord(rec, mount, overlayHtml) {
2622
- if (!rec || !mount || !mount.getBoundingClientRect) return hideLiveStreamRecord(rec);
2623
- var rect = mount.getBoundingClientRect();
2624
- var clipped = clipRect(rect);
2625
- if (rect.width < 2 || rect.height < 2 || clipped.width < 2 || clipped.height < 2) return hideLiveStreamRecord(rec);
2626
- rec.wrapper.style.display = "block";
2627
- rec.wrapper.style.left = clipped.left + "px";
2628
- rec.wrapper.style.top = clipped.top + "px";
2629
- rec.wrapper.style.width = clipped.width + "px";
2630
- rec.wrapper.style.height = clipped.height + "px";
2631
- rec.wrapper.style.pointerEvents = S.view === "focus" ? "auto" : "none";
2632
- rec.frame.style.left = (rect.left - clipped.left) + "px";
2633
- rec.frame.style.top = (rect.top - clipped.top) + "px";
2634
- rec.frame.style.width = rect.width + "px";
2635
- rec.frame.style.height = rect.height + "px";
2636
- rec.frame.style.pointerEvents = S.view === "focus" ? "auto" : "none";
2637
- if (rec.overlay) {
2638
- rec.overlay.style.left = rec.frame.style.left;
2639
- rec.overlay.style.top = rec.frame.style.top;
2640
- rec.overlay.style.width = rec.frame.style.width;
2641
- rec.overlay.style.height = rec.frame.style.height;
2642
- rec.overlay.setAttribute("data-focus", S.view === "focus" ? "true" : "false");
2643
- if (rec.overlayHtml !== overlayHtml) {
2644
- rec.overlay.innerHTML = overlayHtml || "";
2645
- rec.overlayHtml = overlayHtml || "";
2646
- }
2647
- }
2648
- }
2649
- function reconcileLiveStreams() {
2650
- if (!app.querySelectorAll || !document.createElement) return;
2651
- var known = {};
2652
- currentData.streams.forEach(function (s) {
2653
- var url = browserLiveUrl(s);
2654
- if (url) known[s.id] = { url: url, stream: s };
2655
- });
2656
- var mounts = app.querySelectorAll("[data-live-stream-id]");
2657
- var visible = {};
2658
- Array.prototype.forEach.call(mounts, function (mount) {
2659
- var id = mount.getAttribute("data-live-stream-id");
2660
- var url = mount.getAttribute("data-live-stream-url") || "";
2661
- var title = mount.getAttribute("data-live-stream-title") || "live stream";
2662
- if (!id || !url) return;
2663
- var knownStream = known[id] && known[id].stream;
2664
- var overlayHtml = (S.view === "focus" && knownStream) ? browserLabDock(knownStream, browserShot(knownStream)) : "";
2665
- var rec = liveStreamFrames[id];
2666
- if (!rec || rec.url !== url) {
2667
- removeLiveStreamRecord(id);
2668
- rec = createLiveStreamRecord(id, url, title);
2669
- if (!rec) return;
2670
- liveStreamFrames[id] = rec;
2671
- } else {
2672
- rec.frame.setAttribute("title", title);
2673
- }
2674
- visible[id] = true;
2675
- layoutLiveStreamRecord(rec, mount, overlayHtml);
2676
- });
2677
- Object.keys(liveStreamFrames).forEach(function (id) {
2678
- var rec = liveStreamFrames[id];
2679
- if (!known[id] || (rec && rec.url !== known[id].url)) removeLiveStreamRecord(id);
2680
- else if (!visible[id]) hideLiveStreamRecord(rec);
2681
- });
2682
- }
2683
- function scheduleLiveStreamLayout() {
2684
- if (liveStreamLayoutRaf != null) return;
2685
- var raf = window.requestAnimationFrame || function (fn) { return window.setTimeout(fn, 16); };
2686
- liveStreamLayoutRaf = raf(function () {
2687
- liveStreamLayoutRaf = null;
2688
- reconcileLiveStreams();
2689
- });
2690
- }
2691
-
2692
- function render() {
2693
- stopAllScrubTimers(); // a full rebuild replaces the scrubber DOM; never leave a timer ticking on stale nodes
2694
- var docEl = document.documentElement;
2695
- docEl.setAttribute("data-theme", S.theme);
2696
- docEl.setAttribute("data-motion", S.motion);
2697
- docEl.style.setProperty("--accent-color", S.accent);
2698
- docEl.style.setProperty("--accent", S.accent);
2699
-
2700
- var ss = currentData.streams;
2701
- if (!S.focusedId || !ss.some(function (s) { return s.id === S.focusedId; })) S.focusedId = ss.length ? ss[0].id : null;
2702
-
2703
- var active = document.activeElement;
2704
- var searchFocused = !!(active && active.getAttribute && active.getAttribute("data-role") === "search");
2705
- var caret = searchFocused ? active.selectionStart : null;
2706
- var scrolls = captureScrolls();
2707
-
2708
- var parts = [];
2709
- parts.push('<div class="runline" data-status="' + overallStatus() + '"><span style="width:' + overallPct() + '%"></span></div>');
2710
- parts.push(buildHeader());
2711
- if (S.detailsOpen) parts.push(buildPopover());
2712
- if (S.tweaksOpen) parts.push(buildTweaks());
2713
- parts.push(buildToolbar());
2714
- parts.push(buildRunVerdictBanner());
2715
- parts.push('<main class="stage">' + (S.view === "grid" ? buildGrid() : buildFocus()) + '</main>');
2716
- if (S.consoleOpen) parts.push(buildConsole());
2717
- parts.push(buildStatusBar());
2718
- if (S.historyOpen) parts.push(buildDrawer());
2719
- parts.push(buildDdMenu());
2720
- app.innerHTML = parts.join("");
2721
-
2722
- restoreScrolls(scrolls);
2723
- if (searchFocused) {
2724
- var inp = app.querySelector('[data-role="search"]');
2725
- if (inp) { inp.focus(); try { inp.setSelectionRange(caret, caret); } catch (e) {} }
2726
- }
2727
- placeMenus();
2728
- reconcileLiveStreams();
2729
- var cb = document.getElementById("console-body");
2730
- if (cb && scrolls[".console-body"] == null) cb.scrollTop = cb.scrollHeight;
2731
- }
2732
-
2733
- // ================================================================ ACTIONS
2734
- function toggleArr(arr, id) {
2735
- var i = arr.indexOf(id);
2736
- if (i >= 0) { arr.splice(i, 1); } else { arr.push(id); }
2737
- }
2738
- function writeHash(id) {
2739
- var url = new URL(window.location.href);
2740
- url.hash = id ? "focus=" + encodeURIComponent(id) : "";
2741
- if (url.href !== window.location.href) window.history.replaceState({ focusedId: id }, "", url.href);
2742
- }
2743
- function openFocus(id) { S.focusedId = id; S.view = "focus"; writeHash(id); render(); }
2744
- function exitFocus() { S.view = "grid"; writeHash(null); render(); }
2745
- function navFocus(delta) {
2746
- var ss = currentData.streams;
2747
- if (!ss.length) return;
2748
- var idx = focusIndex();
2749
- var next = ((idx + delta) % ss.length + ss.length) % ss.length;
2750
- S.focusedId = ss[next].id;
2751
- writeHash(S.focusedId);
2752
- render();
2753
- }
2754
-
2755
- function handleAction(action) {
2756
- var parts = action.split(":");
2757
- var cmd = parts[0];
2758
- var arg = parts[1];
2759
- var arg2 = parts[2];
2760
- switch (cmd) {
2761
- case "scrub": handleScrub(arg, arg2); break;
2762
- case "open": openFocus(arg); break;
2763
- case "select": S.focusedId = arg; writeHash(arg); render(); break;
2764
- case "exit-focus": exitFocus(); break;
2765
- case "view":
2766
- if (arg === "focus" && !S.focusedId && currentData.streams[0]) S.focusedId = currentData.streams[0].id;
2767
- S.view = arg; writeHash(arg === "focus" ? S.focusedId : null); render(); break;
2768
- case "nav": navFocus(Number(arg)); break;
2769
- case "toggle-rail": S.railCollapsed = !S.railCollapsed; writePref("railCollapsed", S.railCollapsed); render(); break;
2770
- case "toggle-side": S.sideCollapsed = !S.sideCollapsed; writePref("sideCollapsed", S.sideCollapsed); render(); break;
2771
- case "toggle-sheet": S.sheetOpen = !S.sheetOpen; render(); break;
2772
- case "tab": S.tab = arg; render(); break;
2773
- case "density": S.density = arg; writePref("density", arg); render(); break;
2774
- case "media": if (arg === "live" && !hasLive()) break; S.media = arg; render(); break;
2775
- case "status":
2776
- if (arg === "all") S.statusSel = [];
2777
- else toggleArr(S.statusSel, arg);
2778
- render(); break;
2779
- case "clear-filters": S.statusSel = []; S.kindSel = []; S.query = ""; render(); break;
2780
- case "dd": openDd = (openDd === arg ? null : arg); render(); break;
2781
- case "dd-all":
2782
- if (arg === "status") S.statusSel = []; else S.kindSel = [];
2783
- render(); break;
2784
- case "dd-row":
2785
- if (arg === "status") toggleArr(S.statusSel, arg2); else toggleArr(S.kindSel, arg2);
2786
- render(); break;
2787
- case "toggle-console": S.consoleOpen = !S.consoleOpen; render(); break;
2788
- case "inspect-file": S.filePath = action.slice("inspect-file:".length); S.tab = "files"; render(); break;
2789
- case "toggle-details": S.detailsOpen = !S.detailsOpen; S.tweaksOpen = false; render(); break;
2790
- case "toggle-tweaks": S.tweaksOpen = !S.tweaksOpen; S.detailsOpen = false; render(); break;
2791
- case "toggle-theme": S.theme = (S.theme === "light" ? "dark" : "light"); writePref("theme", S.theme); render(); break;
2792
- case "tweak":
2793
- S[arg] = arg2; writePref(arg, arg2); render(); break;
2794
- case "open-history": S.historyOpen = true; render(); refreshHistoryIndex(); break;
2795
- case "close-history": S.historyOpen = false; render(); break;
2796
- case "run": gotoRun(arg); break;
2797
- default: break;
2798
- }
2799
- }
2800
- function gotoRun(runId) {
2801
- if (runId === currentData.run.runId) { S.historyOpen = false; render(); return; }
2802
- if (location.protocol === "file:") { S.historyOpen = false; render(); return; }
2803
- window.location.href = "/_humanish/runs/" + encodeURIComponent(runId) + "/observer/index.html";
2804
- }
2805
-
2806
- // ================================================================ EVENTS (bound once)
2807
- app.addEventListener("click", function (e) {
2808
- var t = e.target.closest ? e.target.closest("[data-action]") : null;
2809
- if (!t || !app.contains(t)) return;
2810
- var action = t.getAttribute("data-action");
2811
- if (t.tagName !== "A") e.preventDefault();
2812
- handleAction(action);
2813
- });
2814
- app.addEventListener("input", function (e) {
2815
- var t = e.target;
2816
- if (!t || !t.getAttribute) return;
2817
- if (t.getAttribute("data-role") === "search") { S.query = t.value; render(); return; }
2818
- if (t.getAttribute("data-role") === "scrub") {
2819
- // Direct DOM update (NOT a full render) so dragging the slider stays smooth and the slider
2820
- // element is not rebuilt mid-drag. Position is stashed in scrubState so a later render restores it.
2821
- var sid = t.getAttribute("data-stream");
2822
- stopScrubPlay(sid);
2823
- scrubState[sid] = Number(t.value);
2824
- updateScrubDom(sid);
2825
- }
2826
- });
2827
- document.addEventListener("mousedown", function (e) {
2828
- if (openDd) {
2829
- var inMenu = e.target.closest && (e.target.closest(".dd-menu") || e.target.closest(".dd-trigger"));
2830
- if (!inMenu) { openDd = null; render(); return; }
2831
- }
2832
- if (S.detailsOpen) {
2833
- var inPop = e.target.closest && (e.target.closest(".pop") || e.target.closest(".run-chip"));
2834
- if (!inPop) { S.detailsOpen = false; render(); }
2835
- }
2836
- if (S.tweaksOpen) {
2837
- var inTw = e.target.closest && (e.target.closest(".tweaks-pop") || e.target.closest('[data-action="toggle-tweaks"]'));
2838
- if (!inTw) { S.tweaksOpen = false; render(); }
2839
- }
2840
- });
2841
- document.addEventListener("keydown", function (e) {
2842
- var active = document.activeElement;
2843
- var tag = active && active.tagName ? String(active.tagName).toUpperCase() : "";
2844
- var typing = !!(active && (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT" || active.isContentEditable));
2845
- if (e.key === "Escape") {
2846
- if (openDd) { openDd = null; return render(); }
2847
- if (S.detailsOpen) { S.detailsOpen = false; return render(); }
2848
- if (S.tweaksOpen) { S.tweaksOpen = false; return render(); }
2849
- if (S.consoleOpen) { S.consoleOpen = false; return render(); }
2850
- if (S.historyOpen) { S.historyOpen = false; return render(); }
2851
- if (S.view === "focus") { return exitFocus(); }
2852
- }
2853
- if ((e.key === String.fromCharCode(96) || e.key === "~") && !typing) { e.preventDefault(); S.consoleOpen = !S.consoleOpen; render(); }
2854
- if (e.key === "/" && S.view === "grid" && !typing) {
2855
- e.preventDefault();
2856
- var el = app.querySelector(".tb-search input");
2857
- if (el) el.focus();
2858
- }
2859
- if (S.view === "focus" && e.key === "[" && !typing) {
2860
- e.preventDefault();
2861
- S.railCollapsed = !S.railCollapsed;
2862
- writePref("railCollapsed", S.railCollapsed);
2863
- render();
2864
- }
2865
- if (S.view === "focus" && e.key === "]" && !typing) {
2866
- e.preventDefault();
2867
- S.sideCollapsed = !S.sideCollapsed;
2868
- writePref("sideCollapsed", S.sideCollapsed);
2869
- render();
2870
- }
2871
- if (S.view === "focus" && (e.key === "ArrowRight" || e.key === "ArrowLeft") && !typing) {
2872
- navFocus(e.key === "ArrowRight" ? 1 : -1);
2873
- }
2874
- });
2875
- window.addEventListener("hashchange", function () {
2876
- var next = focusFromHash();
2877
- if (next && currentData.streams.some(function (s) { return s.id === next; })) {
2878
- S.focusedId = next; if (S.view !== "focus") S.view = "focus"; render();
2879
- }
2880
- });
2881
- window.addEventListener("resize", function () { if (openDd) placeMenus(); scheduleLiveStreamLayout(); });
2882
- app.addEventListener("scroll", scheduleLiveStreamLayout, true);
2883
-
2884
- // ================================================================ POLLING
2885
- function dataKey(d) {
2886
- var streams = (d.streams || []).map(function (s) {
2887
- return {
2888
- id: s.id,
2889
- status: s.status,
2890
- progress: laneProgress(s),
2891
- step: laneStep(s),
2892
- updatedAt: s.updatedAt,
2893
- url: browserLiveUrl(s),
2894
- route: laneRoute(s),
2895
- tail: (s.terminalPlain || (s.terminal && s.terminal.tail) || "").slice(-1200),
2896
- artifacts: (s.artifacts || []).map(function (a) { return [a.kind, a.path, a.label]; }),
2897
- completion: s.completion || null
2898
- };
2899
- });
2900
- var events = (d.events || []).slice(-200).map(function (e) { return [e.at, e.level, e.type, e.simId, e.streamId, e.message]; });
2901
- var run = d.run || {};
2902
- return JSON.stringify({
2903
- run: [run.runId, run.status, (run.lifecycle || []).length, (run.knownGaps || []).length],
2904
- summary: d.summary || {},
2905
- laneGroups: (d.laneGroups || []).map(function (g) { return [g.roleId, g.simId, g.streamId, g.status, g.actorType, g.surface, g.caseGroup]; }),
2906
- streams: streams,
2907
- events: events
2908
- });
2909
- }
2910
- function refresh() {
2911
- if (location.protocol === "file:") return Promise.resolve();
2912
- return fetch(DATA_FILE, { cache: "no-store" }).then(function (r) {
2913
- if (!r.ok) return;
2914
- return r.json().then(function (d) {
2915
- if (!d || !d.streams) return;
2916
- var prev = dataKey(currentData);
2917
- currentData = d;
2918
- if (!currentData.run) currentData.run = {};
2919
- if (!currentData.streams) currentData.streams = [];
2920
- if (!currentData.events) currentData.events = [];
2921
- if (!currentData.laneGroups) currentData.laneGroups = [];
2922
- if (dataKey(currentData) !== prev) render();
2923
- });
2924
- }).catch(function () {});
2925
- }
2926
- function refreshHistoryIndex() {
2927
- if (location.protocol === "file:") return Promise.resolve();
2928
- return fetch("/_humanish/history.json", { cache: "no-store" }).then(function (r) {
2929
- if (!r.ok) throw new Error("history " + r.status);
2930
- return r.json();
2931
- }).then(function (d) {
2932
- historyIndex = d;
2933
- if (S.historyOpen) render();
2934
- }).catch(function () { historyIndex = null; if (S.historyOpen) render(); });
2935
- }
2936
-
2937
- // ================================================================ BOOT
2938
- render();
2939
- refresh().then(function () {
2940
- if (location.protocol !== "file:") {
2941
- refreshTimer = setInterval(refresh, REFRESH_MS);
2942
- refreshHistoryIndex();
2943
- historyTimer = setInterval(refreshHistoryIndex, 30000);
2944
- }
2945
- });
2946
- }());
2947
- `;
2948
- }
2949
- //# sourceMappingURL=observer-assets.js.map