bughunters 0.1.2 → 0.1.3

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.
@@ -1,166 +0,0 @@
1
- // Pure layout for the agent app map. Coordinates are SVG user units.
2
- export function layoutGraph(screens, edges, entryId) {
3
- const w = 190, h = 150, xGap = 130, yGap = 55, pad = 45;
4
- const xStep = w + xGap, yStep = h + yGap;
5
- const ids = new Set(screens.map((screen) => screen.id));
6
- const virtual = new Set(screens.filter((screen) => screen.virtual).map((screen) => screen.id));
7
- const valid = edges.filter((edge) => ids.has(edge.from) && ids.has(edge.to) && edge.from !== edge.to);
8
- const neighbors = new Map([...ids].map((id) => [id, []]));
9
- const outgoing = new Map([...ids].map((id) => [id, []]));
10
- const incoming = new Map([...ids].map((id) => [id, 0]));
11
- for (const edge of valid) {
12
- neighbors.get(edge.from).push(edge.to);
13
- neighbors.get(edge.to).push(edge.from);
14
- if (edge.kind !== 'back') {
15
- outgoing.get(edge.from).push(edge.to);
16
- incoming.set(edge.to, incoming.get(edge.to) + 1);
17
- }
18
- }
19
-
20
- const isolated = [...ids].filter((id) => neighbors.get(id).length === 0).sort();
21
- const seen = new Set();
22
- const components = [];
23
- for (const id of [...ids].sort()) {
24
- if (seen.has(id) || neighbors.get(id).length === 0) continue;
25
- const members = [];
26
- const queue = [id];
27
- seen.add(id);
28
- for (const current of queue) {
29
- members.push(current);
30
- for (const next of neighbors.get(current)) if (!seen.has(next)) {
31
- seen.add(next);
32
- queue.push(next);
33
- }
34
- }
35
- components.push(members.sort());
36
- }
37
- components.sort((a, b) => Number(b.includes(entryId)) - Number(a.includes(entryId)) || a[0].localeCompare(b[0]));
38
-
39
- const blocks = [];
40
- for (const members of components) {
41
- const memberSet = new Set(members);
42
- const reachedFromEntry = new Set();
43
- if (memberSet.has(entryId)) {
44
- const queue = [entryId];
45
- reachedFromEntry.add(entryId);
46
- for (const id of queue) for (const to of outgoing.get(id)) if (!reachedFromEntry.has(to)) {
47
- reachedFromEntry.add(to);
48
- queue.push(to);
49
- }
50
- }
51
- const roots = [
52
- ...(memberSet.has(entryId) ? [entryId] : []),
53
- ...members.filter((id) => id !== entryId && incoming.get(id) === 0 && !reachedFromEntry.has(id)),
54
- ];
55
- const layer = new Map(roots.map((id) => [id, 0]));
56
- const queue = [...roots];
57
- const visit = () => {
58
- for (let index = 0; index < queue.length; index++) {
59
- const id = queue[index];
60
- for (const to of outgoing.get(id)) if (memberSet.has(to) && !layer.has(to)) {
61
- layer.set(to, layer.get(id) + 1);
62
- queue.push(to);
63
- }
64
- }
65
- };
66
- visit();
67
- // A directed cycle can have no zero-incoming root.
68
- for (const id of members) if (!layer.has(id)) { layer.set(id, 0); queue.push(id); visit(); }
69
-
70
- const columns = Array.from({ length: Math.max(...layer.values()) + 1 }, () => []);
71
- for (const id of members) columns[layer.get(id)].push(id);
72
- const order = new Map();
73
- columns.forEach((list) => list.forEach((id, index) => order.set(id, index)));
74
- const sweep = (column, neighbor) => {
75
- const list = columns[column];
76
- const center = (id) => {
77
- const positions = valid.filter((edge) => edge.kind !== 'back' &&
78
- ((edge.from === id && layer.get(edge.to) === neighbor) || (edge.to === id && layer.get(edge.from) === neighbor)))
79
- .map((edge) => order.get(edge.from === id ? edge.to : edge.from)).filter((value) => value !== undefined);
80
- return positions.length ? positions.reduce((sum, value) => sum + value, 0) / positions.length : order.get(id);
81
- };
82
- list.sort((a, b) => center(a) - center(b) || a.localeCompare(b));
83
- list.forEach((id, index) => order.set(id, index));
84
- };
85
- for (let pass = 0; pass < 4; pass++) {
86
- for (let col = 1; col < columns.length; col++) sweep(col, col - 1);
87
- for (let col = columns.length - 2; col >= 0; col--) sweep(col, col + 1);
88
- }
89
-
90
- const nodes = [];
91
- let xColumn = 0;
92
- for (const list of columns) {
93
- list.forEach((id, index) => {
94
- const x = (xColumn + Math.floor(index / 10)) * xStep, y = (index % 10) * yStep;
95
- nodes.push(virtual.has(id) ? { id, x: x + 35, y: y + 53, w: 120, h: 44 }
96
- : { id, x, y, w, h });
97
- });
98
- xColumn += Math.ceil(list.length / 10);
99
- }
100
- blocks.push({ nodes, width: xColumn * xStep - xGap,
101
- height: Math.max(...nodes.map((node) => node.y + node.h)), isolated: false });
102
- }
103
-
104
- if (isolated.length) {
105
- blocks.push({ nodes: isolated.map((id, index) => {
106
- const x = Math.floor(index / 6) * xStep, y = 35 + (index % 6) * yStep;
107
- return virtual.has(id) ? { id, x: x + 35, y: y + 53, w: 120, h: 44 } : { id, x, y, w, h };
108
- }),
109
- width: Math.ceil(isolated.length / 6) * xStep - xGap,
110
- height: 35 + (Math.min(6, isolated.length) - 1) * yStep + h, isolated: true });
111
- }
112
-
113
- const gap = 100;
114
- const widths = new Set();
115
- for (let start = 0; start < blocks.length; start++) {
116
- let width = 0;
117
- for (let end = start; end < blocks.length; end++) {
118
- width += blocks[end].width + (end === start ? 0 : gap);
119
- widths.add(width);
120
- }
121
- }
122
- let targetWidth = 0, bestScore = Infinity;
123
- for (const width of widths) {
124
- let rowWidth = 0, rowHeight = 0, usedWidth = 0, usedHeight = 0;
125
- for (const block of blocks) {
126
- if (rowWidth && rowWidth + gap + block.width > width) {
127
- usedHeight += rowHeight + gap;
128
- rowWidth = 0;
129
- rowHeight = 0;
130
- }
131
- rowWidth += block.width + (rowWidth ? gap : 0);
132
- rowHeight = Math.max(rowHeight, block.height);
133
- usedWidth = Math.max(usedWidth, rowWidth);
134
- }
135
- const ratio = (usedWidth + pad * 2) / (usedHeight + rowHeight + pad * 2);
136
- const score = Math.abs(Math.log(ratio / 1.6));
137
- if (score < bestScore) { bestScore = score; targetWidth = width; }
138
- }
139
- const nodes = [];
140
- let x = pad, y = pad, rowHeight = 0, maxRight = 0, maxBottom = 0;
141
- let unlinkedX = null, unlinkedY = null;
142
- for (const block of blocks) {
143
- if (x > pad && x - pad + block.width > targetWidth) { x = pad; y += rowHeight + gap; rowHeight = 0; }
144
- nodes.push(...block.nodes.map((node) => ({ ...node, x: node.x + x, y: node.y + y })));
145
- if (block.isolated) { unlinkedX = x; unlinkedY = y; }
146
- maxRight = Math.max(maxRight, x + block.width);
147
- maxBottom = Math.max(maxBottom, y + block.height);
148
- rowHeight = Math.max(rowHeight, block.height);
149
- x += block.width + gap;
150
- }
151
-
152
- const positions = new Map(nodes.map((node) => [node.id, node]));
153
- const paths = valid.map((edge, index) => {
154
- const from = positions.get(edge.from), to = positions.get(edge.to);
155
- if (to.x > from.x) {
156
- const x1 = from.x + from.w, x2 = to.x, y1 = from.y + from.h / 2, y2 = to.y + to.h / 2;
157
- const bend = Math.max(65, (x2 - x1) / 2);
158
- return { ...edge, path: `M ${x1} ${y1} C ${x1 + bend} ${y1}, ${x2 - bend} ${y2}, ${x2} ${y2}` };
159
- }
160
- const y = Math.min(from.y, to.y) - 25 - (index % 5) * 12;
161
- const x1 = from.x + from.w / 2, x2 = to.x + to.w / 2;
162
- return { ...edge, path: `M ${x1} ${from.y} C ${x1} ${y}, ${x2} ${y}, ${x2} ${to.y}` };
163
- });
164
- return { nodes, edges: paths, width: Math.max(320, maxRight + pad),
165
- height: Math.max(320, maxBottom + pad), unlinkedX, unlinkedY };
166
- }
package/src/ui/index.html DELETED
@@ -1,40 +0,0 @@
1
- <!doctype html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="utf-8">
5
- <meta name="viewport" content="width=device-width,initial-scale=1">
6
- <title>Bughunters</title>
7
- <link rel="stylesheet" href="/style.css">
8
- </head>
9
- <body>
10
- <header class="topbar">
11
- <div class="brand">
12
- <span class="mark" aria-hidden="true"></span>
13
- <strong id="project-name">Bughunters</strong>
14
- </div>
15
- <nav class="tabs" id="tabs">
16
- <button data-view="overview" class="active">Overview</button>
17
- <button data-view="issues">Issues</button>
18
- <button data-view="activity">Activity</button>
19
- <button data-view="screens">Screens</button>
20
- <button data-view="memory">Memory</button>
21
- <button data-view="checks">Checks</button>
22
- </nav>
23
- <div class="status">
24
- <span id="live" class="live" title="Live connection to the local watcher">live</span>
25
- <span id="platform" class="muted"></span>
26
- <span id="patrol" class="pill"></span>
27
- </div>
28
- </header>
29
-
30
- <main id="view"></main>
31
-
32
- <div id="lightbox" class="lightbox" hidden>
33
- <button class="lightbox-close" aria-label="Close">&times;</button>
34
- <img id="lightbox-img" alt="">
35
- <p id="lightbox-caption"></p>
36
- </div>
37
-
38
- <script type="module" src="/app.js"></script>
39
- </body>
40
- </html>
package/src/ui/style.css DELETED
@@ -1,425 +0,0 @@
1
- :root {
2
- --bg: #0f1115;
3
- --panel: #171a21;
4
- --panel-2: #1e222b;
5
- --line: #2a2f3a;
6
- --fg: #e7e9ee;
7
- --muted: #9aa3b2;
8
- --accent: #6ea8fe;
9
- --critical: #ff6b6b;
10
- --major: #ffa94d;
11
- --minor: #ffd43b;
12
- --cosmetic: #8ce99a;
13
- --ok: #51cf66;
14
- --gh-open: #51cf66;
15
- --gh-merged: #b197fc;
16
- --gh-closed: #ff6b6b;
17
- }
18
- @media (prefers-color-scheme: light) {
19
- :root {
20
- --bg: #f6f7f9; --panel: #fff; --panel-2: #f0f2f6; --line: #dfe3ea;
21
- --fg: #14171c; --muted: #5b6270; --accent: #2b59d8;
22
- --gh-open: #237a36; --gh-merged: #7042b8; --gh-closed: #b73535;
23
- }
24
- }
25
-
26
- * { box-sizing: border-box; }
27
- html, body { height: 100%; }
28
- body {
29
- margin: 0;
30
- background: var(--bg);
31
- color: var(--fg);
32
- font: 14px/1.55 ui-sans-serif, system-ui, -apple-system, sans-serif;
33
- }
34
- button { font: inherit; color: inherit; cursor: pointer; }
35
-
36
- /* ---------- top bar ---------- */
37
- .topbar {
38
- display: flex; align-items: center; gap: 1.25rem;
39
- padding: .6rem 1rem; border-bottom: 1px solid var(--line);
40
- background: var(--panel); position: sticky; top: 0; z-index: 20;
41
- }
42
- .brand { display: flex; align-items: center; gap: .5rem; }
43
- .mark { width: 12px; height: 12px; border-radius: 3px; background: var(--accent); }
44
- .tabs { display: flex; gap: .25rem; }
45
- .tabs button {
46
- background: transparent; border: 1px solid transparent; border-radius: 6px;
47
- padding: .3rem .7rem; color: var(--muted);
48
- }
49
- .tabs button:hover { color: var(--fg); }
50
- .tabs button.active { background: var(--panel-2); border-color: var(--line); color: var(--fg); }
51
- .status { margin-left: auto; display: flex; align-items: center; gap: .75rem; font-size: .8rem; }
52
- .live {
53
- display: inline-flex; align-items: center; gap: .35rem;
54
- color: var(--ok); font-size: .72rem; text-transform: uppercase; letter-spacing: .04em;
55
- }
56
- .live::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--ok); }
57
- .live.stale { color: var(--muted); }
58
- .live.stale::before { background: var(--muted); }
59
- .muted { color: var(--muted); }
60
-
61
- main { padding: 1rem; }
62
-
63
- /* ---------- generic ---------- */
64
- .empty { color: var(--muted); padding: 3rem 1rem; text-align: center; }
65
- .empty code { background: var(--panel-2); padding: .15rem .4rem; border-radius: 4px; }
66
- .card { background: var(--panel); border: 1px solid var(--line); border-radius: 10px; }
67
- h2 { font-size: .95rem; margin: 0 0 .6rem; }
68
- h3 { font-size: .85rem; margin: 0 0 .4rem; }
69
- code, .mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .82em; }
70
-
71
- .badge {
72
- display: inline-block; padding: .08rem .45rem; border-radius: 999px;
73
- font-size: .7rem; font-weight: 600; letter-spacing: .02em;
74
- }
75
- .badge.fail { background: var(--critical); color: #fff; }
76
- .badge.pass { background: var(--ok); color: #05270d; }
77
- .badge.warn { background: var(--major); color: #2a1600; }
78
- .badge.info { background: var(--panel-2); color: var(--muted); border: 1px solid var(--line); }
79
- .gh-chip { display: inline-flex; flex: none; align-items: center; border: 1px solid currentColor;
80
- border-radius: 999px; padding: .08rem .45rem; font-size: .7rem; text-decoration: none;
81
- white-space: nowrap; color: var(--muted); }
82
- .gh-chip:hover { text-decoration: underline; }
83
- .gh-open { color: var(--gh-open); }
84
- .gh-merged { color: var(--gh-merged); }
85
- .gh-closed { color: var(--gh-closed); }
86
- .gh-chip.ci-failed, .gh-chip.ci-gave-up { color: var(--gh-closed); }
87
- .github-summary { font-size: .8rem; margin-bottom: .75rem; }
88
-
89
- .sev-critical { --sev: var(--critical); }
90
- .sev-major { --sev: var(--major); }
91
- .sev-minor { --sev: var(--minor); }
92
- .sev-cosmetic { --sev: var(--cosmetic); }
93
-
94
- /* ---------- runs list ---------- */
95
- .layout { display: grid; grid-template-columns: 300px 1fr; gap: 1rem; align-items: start; }
96
- @media (max-width: 900px) { .layout { grid-template-columns: 1fr; } }
97
-
98
- .runlist { padding: .4rem; max-height: calc(100vh - 6rem); overflow: auto; }
99
- .runitem {
100
- display: block; width: 100%; text-align: left; background: transparent;
101
- border: 1px solid transparent; border-radius: 8px; padding: .55rem .6rem; margin-bottom: .2rem;
102
- }
103
- .runitem:hover { background: var(--panel-2); }
104
- .runitem.active { background: var(--panel-2); border-color: var(--accent); }
105
- .runitem .row { display: flex; align-items: center; gap: .4rem; }
106
- .runitem .when { color: var(--muted); font-size: .75rem; margin-top: .15rem; }
107
-
108
- /* ---------- run detail ---------- */
109
- .detail { padding: 1rem 1.1rem; }
110
- .kv { display: flex; flex-wrap: wrap; gap: .4rem 1.25rem; color: var(--muted); font-size: .8rem; margin-bottom: .9rem; }
111
- .notes {
112
- margin: 0 0 1rem;
113
- padding: .6rem .8rem;
114
- background: var(--panel-2);
115
- border-left: 3px solid var(--accent);
116
- border-radius: 0 6px 6px 0;
117
- }
118
- .notes p { margin: .2rem 0; font-size: .82rem; color: var(--muted); }
119
-
120
- .screen { border-top: 1px solid var(--line); padding: 1rem 0; }
121
- .screen:first-of-type { border-top: 0; }
122
- .screen-head { display: flex; align-items: baseline; gap: .6rem; flex-wrap: wrap; margin-bottom: .6rem; }
123
- .screen-head .vp { color: var(--muted); font-size: .78rem; }
124
-
125
- .shots { display: grid; grid-template-columns: repeat(3, 1fr); gap: .5rem; margin: .5rem 0 .8rem; }
126
- .shots figure { margin: 0; }
127
- .shots img {
128
- width: 100%; display: block; border: 1px solid var(--line); border-radius: 6px;
129
- background: var(--panel-2); cursor: zoom-in; max-height: 260px; object-fit: cover; object-position: top;
130
- }
131
- .shots figcaption { color: var(--muted); font-size: .72rem; text-align: center; margin-top: .25rem; }
132
-
133
- .checks { display: flex; flex-wrap: wrap; gap: .3rem; margin: .5rem 0; }
134
- .chk {
135
- font-size: .72rem; padding: .1rem .45rem; border-radius: 4px;
136
- border: 1px solid var(--line); color: var(--muted); background: var(--panel-2);
137
- }
138
- .chk.fired {
139
- border-color: var(--sev, var(--major));
140
- color: var(--fg);
141
- background: color-mix(in srgb, var(--sev, var(--major)) 18%, transparent);
142
- }
143
-
144
- .finding {
145
- border-left: 3px solid var(--sev, var(--line)); background: var(--panel-2);
146
- border-radius: 0 8px 8px 0; padding: .6rem .8rem; margin: .45rem 0;
147
- }
148
- .finding .why { color: var(--muted); font-size: .78rem; margin-top: .3rem; }
149
- .finding .meta { color: var(--muted); font-size: .72rem; margin-top: .25rem; }
150
-
151
- .reasoning { margin-top: .6rem; }
152
- .reasoning summary { cursor: pointer; color: var(--muted); font-size: .78rem; }
153
- .reasoning table { width: 100%; border-collapse: collapse; margin-top: .4rem; font-size: .78rem; }
154
- .reasoning td { padding: .2rem .4rem; border-bottom: 1px solid var(--line); vertical-align: top; }
155
- .reasoning td:first-child { color: var(--muted); white-space: nowrap; width: 1%; }
156
-
157
- /* ---------- lightbox ---------- */
158
- .lightbox {
159
- position: fixed; inset: 0; background: rgba(0,0,0,.85); z-index: 50;
160
- display: flex; flex-direction: column; align-items: center; justify-content: center; gap: .75rem; padding: 2rem;
161
- }
162
- /* `display: flex` above outranks the UA rule for [hidden], so without this the
163
- closed lightbox stays a full-screen transparent overlay that swallows every
164
- click -- the "the button does nothing" bug, in our own UI. */
165
- .lightbox[hidden] { display: none; }
166
- .lightbox img { max-width: 95vw; max-height: 85vh; object-fit: contain; border-radius: 6px; }
167
- .lightbox p { color: #ddd; font-size: .8rem; margin: 0; }
168
- .lightbox-close {
169
- position: absolute; top: 1rem; right: 1.25rem; background: transparent;
170
- border: 0; color: #fff; font-size: 2rem; line-height: 1;
171
- }
172
-
173
- /* ---------- live run banner ---------- */
174
- .runbanner {
175
- display: flex; align-items: center; gap: .6rem; margin-bottom: .75rem;
176
- padding: .5rem .75rem; border-radius: 8px; font-size: .82rem;
177
- background: color-mix(in srgb, var(--accent) 14%, transparent);
178
- border: 1px solid color-mix(in srgb, var(--accent) 40%, transparent);
179
- }
180
- .spinner {
181
- width: 11px; height: 11px; border-radius: 50%; flex: none;
182
- border: 2px solid color-mix(in srgb, var(--accent) 35%, transparent);
183
- border-top-color: var(--accent); animation: spin .8s linear infinite;
184
- }
185
- @keyframes spin { to { transform: rotate(360deg); } }
186
- @media (prefers-reduced-motion: reduce) { .spinner { animation: none; } }
187
-
188
- /* ---------- grouped findings ---------- */
189
- .finding .members { margin-top: .35rem; }
190
- .finding .members summary { cursor: pointer; color: var(--muted); font-size: .75rem; }
191
- .finding .members ul { margin: .35rem 0 0; padding-left: 1.1rem; max-height: 220px; overflow: auto; }
192
- .finding .members li { font-size: .72rem; color: var(--muted); margin: .1rem 0; }
193
-
194
- /* Agent views keep the same visual hierarchy in light and dark schemes. */
195
- :root { --cosmetic: #8e949d; }
196
- main { max-width: 1600px; margin: auto; padding: 1.25rem; }
197
- h1 { font-size: 1.35rem; line-height: 1.25; margin: 0; }
198
- .topbar { min-height: 56px; }
199
- .status { white-space: nowrap; }
200
- .pill { border: 1px solid var(--line); border-radius: 999px; padding: .15rem .6rem; color: var(--muted); }
201
- .pill.running { color: var(--accent); border-color: var(--accent); }
202
- .live { font-size: 0; }
203
- .live::before { background: var(--muted); }
204
- .live.working::before { background: var(--ok); animation: pulse 1.5s infinite; }
205
- @keyframes pulse { 50% { opacity: .3; } }
206
- @media (prefers-reduced-motion: reduce) { .live.working::before, .state-dot.working { animation: none; } }
207
- .agent-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: .85rem; margin-bottom: .85rem; }
208
- .agent-card, .panel { padding: 1rem; }
209
- .agent-card p { margin: .75rem 0; min-height: 1.4em; }
210
- .agent-head, .section-title, .detail-heading { display: flex; align-items: center; gap: .55rem; }
211
- .agent-head .state-dot { margin-left: auto; }
212
- .role-icon { display: inline-flex; width: 18px; height: 18px; flex: none; color: var(--accent); }
213
- .role-icon svg { width: 100%; height: 100%; }
214
- .state-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--muted); }
215
- .state-dot.working { background: var(--accent); animation: pulse 1.5s infinite; }
216
- .state-dot.idle, .state-dot.waiting { background: var(--ok); }
217
- .state-dot.failed { background: var(--critical); }
218
- .section-title { justify-content: space-between; margin-bottom: .75rem; }
219
- .section-title h2 { margin: 0; font-size: 1rem; }
220
- .overview-grid { display: grid; grid-template-columns: minmax(0, 1.6fr) minmax(300px, 1fr); gap: .85rem; }
221
- .coverage { margin-top: .85rem; }
222
- .usage { margin-top: .85rem; overflow-x: auto; }
223
- .usage-table { width: 100%; border-collapse: collapse; font-size: .82rem; }
224
- .usage-table th { text-align: left; font-weight: 600; color: var(--muted, #64748b); padding: .35rem .5rem; border-bottom: 1px solid var(--line, #e2e8f0); }
225
- .usage-table td { padding: .35rem .5rem; border-bottom: 1px solid var(--line, #e2e8f0); font-variant-numeric: tabular-nums; }
226
- .session-usage { margin: .35rem 0 .6rem; font-size: .8rem; }
227
- .token-chip { white-space: nowrap; font-variant-numeric: tabular-nums; }
228
- .stack, .event-list { display: flex; flex-direction: column; }
229
- .attention-row, .list-row {
230
- display: flex;
231
- align-items: center;
232
- width: 100%;
233
- gap: .7rem;
234
- text-align: left;
235
- background: transparent;
236
- border: 0;
237
- border-top: 1px solid var(--line);
238
- padding: .65rem .2rem;
239
- }
240
- .attention-row:hover, .list-row:hover, .list-row.active { background: var(--panel-2); }
241
- .grow { flex: 1; min-width: 0; }
242
- .attention-row strong, .list-row strong { display: block; font-weight: 600; }
243
- .attention-row small, .list-row small { display: block; }
244
- .thumb, .event-thumb {
245
- width: 48px;
246
- height: 42px;
247
- border-radius: 5px;
248
- object-fit: cover;
249
- background: var(--panel-2);
250
- flex: none;
251
- }
252
- .severity {
253
- border-radius: 999px;
254
- padding: .12rem .5rem;
255
- font-size: .72rem;
256
- font-weight: 650;
257
- text-transform: capitalize;
258
- white-space: nowrap;
259
- }
260
- .severity-critical { background: #b93038; color: white; }
261
- .severity-major { background: #ae551c; color: white; }
262
- .severity-minor { background: #f0d36a; color: #302600; }
263
- .severity-cosmetic { background: #737983; color: white; }
264
- .live-shot { width: 100%; height: 230px; object-fit: contain; background: var(--panel-2); border-radius: 6px; }
265
- .image-empty { background: var(--panel-2); color: var(--muted); display: grid; place-items: center; }
266
- .event-row {
267
- display: flex;
268
- align-items: center;
269
- gap: .5rem;
270
- border-top: 1px solid var(--line);
271
- padding: .4rem 0;
272
- min-width: 0;
273
- }
274
- .event-row time { color: var(--muted); font-size: .75rem; white-space: nowrap; }
275
- .event-row details { flex: none; font-size: .75rem; }
276
- .event-row details[open] { max-width: 45%; }
277
- .event-row pre { white-space: pre-wrap; overflow-wrap: anywhere; }
278
- .truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
279
- .quiet { color: var(--muted); }
280
- .coverage-row { display: flex; gap: .65rem; overflow-x: auto; }
281
- .coverage-item {
282
- width: 136px;
283
- flex: none;
284
- background: transparent;
285
- border: 0;
286
- color: var(--fg);
287
- text-align: left;
288
- padding: 0;
289
- }
290
- .coverage-item img, .coverage-item .image-empty { width: 136px; height: 80px; object-fit: cover; border-radius: 6px; }
291
- .coverage-item span { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
292
- .master-detail {
293
- display: grid;
294
- grid-template-columns: minmax(260px, 350px) minmax(0, 1fr);
295
- gap: .85rem;
296
- align-items: start;
297
- }
298
- .filters { display: flex; gap: .3rem; flex-wrap: wrap; margin-bottom: .6rem; }
299
- .filter {
300
- background: transparent;
301
- border: 1px solid var(--line);
302
- border-radius: 999px;
303
- padding: .17rem .5rem;
304
- color: var(--muted);
305
- font-size: .8rem;
306
- }
307
- .filter.active { border-color: var(--accent); color: var(--fg); background: var(--panel-2); }
308
- .detail-heading { justify-content: space-between; align-items: flex-start; margin-bottom: .75rem; }
309
- /* A grid item grows to its widest child; a long diff line must scroll in its
310
- own block, not widen the page. */
311
- .master-detail > * { min-width: 0; }
312
- .issue-detail .section-title { margin-top: 1.2rem; }
313
- .issue-detail .kv { margin-bottom: 1rem; }
314
- .markdown p { margin: .5rem 0; }
315
- .markdown pre, .diff-block, .fix pre {
316
- background: var(--panel-2);
317
- border-radius: 6px;
318
- padding: .6rem;
319
- overflow: auto;
320
- font-size: .75rem;
321
- }
322
- .evidence-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: .5rem; }
323
- .evidence-grid figure { margin: 0; }
324
- .evidence-grid img { width: 100%; height: 180px; object-fit: contain; background: var(--panel-2); cursor: zoom-in; }
325
- .evidence-grid figcaption { color: var(--muted); text-align: center; font-size: .75rem; }
326
- .diff-file { border: 1px solid var(--line); border-radius: 6px; margin: .6rem 0; overflow: hidden; }
327
- .diff-file h3 { padding: .5rem .6rem; margin: 0; background: var(--panel-2); overflow-wrap: anywhere; }
328
- .diff-block { margin: 0; border-radius: 0; }
329
- .diff-block span { display: block; min-height: 1em; white-space: pre; }
330
- .diff-block .added { background: color-mix(in srgb, var(--ok) 18%, transparent); }
331
- .diff-block .removed { background: color-mix(in srgb, var(--critical) 18%, transparent); }
332
- .diff-block .hunk { color: var(--muted); }
333
- .retest-block { border-top: 1px solid var(--line); padding: .75rem 0; }
334
- .retest-block p { margin: .35rem 0; }
335
- .retest-shot-pair { margin: .7rem 0; }
336
- .retest-shot-pair h4 { font-size: .85rem; margin: 0 0 .25rem; }
337
- .retest-shots { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .6rem; }
338
- .retest-shots figure { margin: 0; min-width: 0; }
339
- .retest-shots img, .retest-shots .image-empty { width: 100%; height: 220px; object-fit: contain; background: var(--panel-2); cursor: zoom-in; }
340
- .retest-shots figcaption { text-align: center; color: var(--muted); font-size: .75rem; }
341
- .earlier-retests summary { cursor: pointer; color: var(--muted); }
342
- @media (max-width: 600px) { .retest-shots { grid-template-columns: 1fr; } }
343
- .toggle { display: flex; align-items: center; gap: .4rem; margin-bottom: .5rem; }
344
- .candidate {
345
- display: flex;
346
- justify-content: space-between;
347
- gap: .5rem;
348
- border-top: 1px solid var(--line);
349
- padding: .5rem 0;
350
- }
351
- .screen-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: .85rem; }
352
- .screen-card { text-align: left; padding: 0; overflow: hidden; }
353
- .screen-card:hover { border-color: var(--accent); }
354
- .screen-card > img, .screen-card > .image-empty { display: block; width: 100%; height: 145px; object-fit: cover; }
355
- .screen-copy { padding: .75rem; }
356
- .screen-copy p { margin: .2rem 0; }
357
- .issue-count { float: right; color: var(--major); font-size: .75rem; }
358
- .screen-large { width: 100%; max-height: 520px; object-fit: contain; background: var(--panel-2); }
359
- .back { border: 0; background: transparent; color: var(--accent); padding: 0 0 .7rem; }
360
- .screen-mode { display: inline-flex; margin: .5rem 0 .75rem; border: 1px solid var(--line); border-radius: 7px; overflow: hidden; }
361
- .screen-mode button { border: 0; background: var(--panel); padding: .25rem .8rem; }
362
- .screen-mode button.active { background: var(--accent); color: var(--bg); }
363
- .graph-wrap { min-width: 0; width: 100%; }
364
- .graph-toolbar { display: flex; align-items: center; gap: .35rem; flex-wrap: wrap; }
365
- .graph-toolbar button { border: 1px solid var(--line); border-radius: 5px; background: var(--panel); padding: .15rem .55rem; }
366
- .graph-toolbar label { margin-left: .5rem; color: var(--muted); }
367
- .graph-legend { color: var(--muted); font-size: .75rem; margin: .45rem 0; }
368
- .screen-graph { display: block; width: 100%; height: min(70vh, 700px); min-height: 420px;
369
- background: var(--panel); border: 1px solid var(--line); border-radius: 8px; touch-action: none; cursor: grab; }
370
- .screen-graph:active { cursor: grabbing; }
371
- .graph-edge { fill: none; stroke: var(--accent); opacity: .7; pointer-events: stroke; }
372
- .graph-edge.open { stroke-dasharray: 7 5; }
373
- .graph-edge.route { stroke: var(--muted); opacity: .6; }
374
- .graph-edge.other { stroke: var(--muted); opacity: .45; }
375
- .graph-edge.back { stroke: var(--muted); stroke-dasharray: 3 5; opacity: .45; }
376
- .graph-edge.faded { opacity: .08; }
377
- #graph-arrow path { fill: var(--accent); }
378
- #graph-arrow-muted path { fill: var(--muted); }
379
- .graph-card { fill: var(--panel-2); stroke: var(--line); }
380
- .graph-start rect { fill: var(--panel-2); stroke: var(--accent); }
381
- .graph-start text { fill: var(--fg); font-size: 13px; font-weight: 650; }
382
- .graph-node { cursor: pointer; }
383
- .graph-node:hover .graph-card, .graph-node:focus .graph-card { stroke: var(--accent); stroke-width: 2; }
384
- .graph-name, .graph-unlinked { fill: var(--fg); font-size: 14px; font-weight: 600; }
385
- .graph-unlinked { fill: var(--muted); }
386
- .graph-issue { fill: var(--critical); }
387
- .graph-issue-text { fill: white; font-size: 12px; font-weight: 700; }
388
- .screen-links { margin: .8rem 0; }
389
- .screen-links .list-row { display: flex; justify-content: space-between; }
390
- @media (max-width: 900px) {
391
- .topbar { flex-wrap: wrap; }
392
- .status { margin-left: 0; }
393
- .overview-grid, .master-detail { grid-template-columns: 1fr; }
394
- }
395
- @media (max-width: 600px) {
396
- main { min-width: 0; overflow-x: hidden; }
397
- .screen-graph { height: 60vh; min-height: 360px; }
398
- .agent-grid { grid-template-columns: 1fr; }
399
- .tabs { overflow-x: auto; width: 100%; order: 3; }
400
- .attention-row { flex-wrap: wrap; }
401
- }
402
- .viewport-tags { display: flex; gap: .3rem; margin-top: .4rem; }
403
- .viewport-tags span {
404
- border: 1px solid var(--line);
405
- border-radius: 4px;
406
- padding: 0 .35rem;
407
- color: var(--muted);
408
- font-size: .72rem;
409
- }
410
-
411
- /* The replayable path behind an issue: exact, but one click away. */
412
- .recorded-path { margin: 16px 0; }
413
- .recorded-path summary { cursor: pointer; color: var(--muted, #6b7280); font-weight: 600; }
414
- .recorded-path ol { margin-top: 8px; }
415
-
416
- /* A long agent summary reads as a note under the timeline title. */
417
- .session-summary { margin: 4px 0 12px; color: var(--muted, #6b7280); font-size: 13px; }
418
-
419
- .diff-details summary { cursor: pointer; color: var(--muted); margin: .4rem 0; }
420
- .memory-group { margin: 1rem 0; }
421
- .memory-row { display: flex; align-items: center; gap: .75rem; border-top: 1px solid var(--line); padding: .45rem 0; }
422
- .memory-row .muted { font-size: .75rem; white-space: nowrap; }
423
- @media (max-width: 600px) { .memory-row { flex-wrap: wrap; } }
424
-
425
- .row-meta { display: flex; flex-wrap: wrap; align-items: center; gap: .35rem .5rem; margin-top: .2rem; }