Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history

agent-workspace.tsx

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

agent-workspace.tsxBlame1237 lines · 1 contributor
11c3ab6ccanty labs1import type { FC } from "hono/jsx";
2
3export interface AgentWorkspaceProps {
4 owner?: string;
5 repo?: string;
6 prNumber?: number;
7 user?: any;
8}
9
10const EVENTS = [
11 {
12 title: "Gate failed on push",
13 time: "12:04",
14 dotColor: "#b42318",
15 body: "gate: tests went red on feat/repair-sandbox-loop — 2 of 412 tests failing after the sandbox lifecycle change.",
16 code: "✗ auto-repair › reuses sandbox session across attempts\n✗ auto-repair › caps repair attempts at limit",
17 diff: false,
18 },
19 {
20 title: "Claude attached to the PR sandbox",
21 time: "12:05",
22 dotColor: "#16181d",
23 body: "Instead of opening a detached loop, the agent joined the existing pr-218 sandbox and reproduced both failures in isolation.",
24 code: null,
25 diff: false,
26 },
27 {
28 title: "Patch drafted",
29 time: "12:07",
30 dotColor: "#16181d",
31 body: "Root cause: the repair loop never terminated when a gate stayed red. Claude bounded the loop and threaded the sandbox session through each attempt.",
32 code: null,
33 diff: true,
34 },
35 {
36 title: "Gates re-run — all green",
37 time: "12:08",
38 dotColor: "#1e7f5c",
39 body: "412 tests pass. Security and type gates unchanged. The repair attempt, patch, and outcome were recorded to flywheel telemetry.",
40 code: null,
41 diff: false,
42 },
43 {
44 title: "Waiting on your review",
45 time: "12:09",
46 dotColor: "#b45309",
47 body: "Branch protection requires one human approval. Review the diff, hop into the sandbox, or approve to let auto-merge finish.",
48 code: null,
49 diff: false,
50 },
51];
52
53const FILES = [
54 { path: "src/lib/auto-repair.ts", add: 184, del: 62 },
55 { path: "src/lib/pr-sandbox.ts", add: 97, del: 18 },
56 { path: "src/lib/ai-ci-healer.ts", add: 41, del: 12 },
57 { path: "drizzle/0107_auto_repair_mode.sql", add: 14, del: 0 },
58 { path: "src/hooks/post-receive.ts", add: 22, del: 6 },
59];
60
61export const AgentWorkspace: FC<AgentWorkspaceProps> = (props) => {
62 const owner = props.owner ?? "ccantynz-alt";
63 const repo = props.repo ?? "gluecron";
64 const prNumber = props.prNumber ?? 218;
65 const userInitial =
66 props.user?.name?.[0]?.toUpperCase() ??
67 props.user?.username?.[0]?.toUpperCase() ??
68 "C";
69
70 return (
71 <>
72 <style dangerouslySetInnerHTML={{ __html: css }} />
73 <div class="aw-root">
74 {/* Top bar */}
75 <header class="aw-topbar">
76 <a href="/" class="aw-logo">
77 <span class="aw-logo-mark" />
78 gluecron
79 </a>
80 <nav class="aw-breadcrumb">
81 <a href={`/${owner}`} class="aw-breadcrumb-link">
82 {owner}
83 </a>
84 <span class="aw-breadcrumb-sep">/</span>
85 <a href={`/${owner}/${repo}`} class="aw-breadcrumb-link">
86 {repo}
87 </a>
88 <span class="aw-breadcrumb-sep">/</span>
89 <span class="aw-breadcrumb-current">Workspace</span>
90 </nav>
91 <div class="aw-topbar-right">
92 <span class="aw-system-status">
93 <span class="aw-system-dot" />
94 All systems normal
95 </span>
96 <span class="aw-topbar-divider" />
97 <span class="aw-avatar">{userInitial}</span>
98 </div>
99 </header>
100
101 <main class="aw-main">
102 {/* Page header */}
103 <div class="aw-page-header">
104 <div>
105 <div class="aw-eyebrow">
106 Pull request #{prNumber} · Agent session
107 </div>
108 <h1 class="aw-title">
109 Unify auto-repair loop with PR sandbox lifecycle
110 </h1>
111 <div class="aw-meta">
112 <span class="aw-branch-label">
113 feat/repair-sandbox-loop → main
114 </span>
115 <span class="aw-meta-sep">·</span>
116 <span>opened by Claude, supervised by you</span>
117 </div>
118 </div>
119 <div class="aw-header-actions">
120 <a
121 href={`/${owner}/${repo}/pull/${prNumber}`}
122 class="aw-btn-secondary"
123 >
124 View full diff
125 </a>
126 <button
127 id="aw-approve-btn"
128 type="button"
129 class="aw-btn-approve"
130 >
131 Approve &amp; merge
132 </button>
133 </div>
134 </div>
135
136 <div class="aw-layout">
137 {/* Left: session activity */}
138 <section class="aw-left">
139 <div class="aw-section-header">
140 <h2 class="aw-section-title">Session activity</h2>
141 <span id="aw-session-clock" class="aw-session-clock">
142 session · 47:12
143 </span>
144 </div>
145
146 <div id="aw-timeline" class="aw-timeline">
147 {EVENTS.map((e, i) => (
148 <div
149 id={`aw-event-${i}`}
150 class={`aw-event${i === 0 ? " aw-event--visible" : " aw-event--hidden"}`}
151 >
152 <div class="aw-event-gutter">
153 <span
154 id={`aw-event-dot-${i}`}
155 class="aw-event-dot"
156 style={`background:${e.dotColor}`}
157 />
158 <span
159 id={`aw-event-line-${i}`}
160 class={`aw-event-line${i === 0 ? " aw-event-line--hidden" : ""}`}
161 />
162 </div>
163 <div class="aw-event-body">
164 <div class="aw-event-header">
165 <span class="aw-event-title">{e.title}</span>
166 <span class="aw-event-time">{e.time}</span>
167 </div>
168 <p class="aw-event-text">{e.body}</p>
169 {e.code && (
170 <pre class="aw-code-block">{e.code}</pre>
171 )}
172 {e.diff && (
173 <div class="aw-diff-card">
174 <div class="aw-diff-header">
175 <span>src/lib/auto-repair.ts</span>
176 <span>
177 <span class="aw-diff-plus">+9</span>{" "}
178 <span class="aw-diff-minus">&#8722;3</span>
179 </span>
180 </div>
181 <div class="aw-diff-body">
182 <div class="aw-diff-ctx">
183 {" const session = await sandbox.attach(pr.id);"}
184 </div>
185 <div class="aw-diff-rem">
186 {"− while (gate.failing) { await attemptPatch(gate); }"}
187 </div>
188 <div class="aw-diff-ins">
189 {"+ for (let i = 0; i < MAX_REPAIR_ATTEMPTS; i++) {"}
190 </div>
191 <div class="aw-diff-ins">
192 {"+   const patch = await attemptPatch(gate, session);"}
193 </div>
194 <div class="aw-diff-ins">
195 {"+   if (await gates.rerun(session, patch)) break;"}
196 </div>
197 <div class="aw-diff-ins">{"+ }"}</div>
198 <div class="aw-diff-ctx">
199 {' telemetry.record("repair.attempt", patch);'}
200 </div>
201 </div>
202 </div>
203 )}
204 </div>
205 </div>
206 ))}
207 </div>
208
209 {/* Composer */}
210 <div class="aw-composer">
211 <input
212 id="aw-composer-input"
213 type="text"
214 placeholder="Ask Claude about this session, or give it direction…"
215 class="aw-composer-input"
216 />
217 <button type="button" id="aw-send-btn" class="aw-composer-send">
218 Send
219 </button>
220 </div>
221 </section>
222
223 {/* Right sticky rail */}
224 <aside class="aw-rail">
225 {/* Where this stands */}
226 <div class="aw-rail-section">
227 <h3 class="aw-rail-label">Where this stands</h3>
228 <div class="aw-card">
229 <div class="aw-status-row">
230 <span
231 id="aw-status-dot"
232 class="aw-status-dot aw-pulse"
233 style="background:#b45309"
234 />
235 <span id="aw-status-label" class="aw-status-label">
236 Agent repairing
237 </span>
238 </div>
239 <p id="aw-status-body" class="aw-status-body">
240 Claude is working inside the PR sandbox. You can watch each
241 step, join the terminal, or interrupt at any time.
242 </p>
243 <div class="aw-card-divider" />
244 <div class="aw-gates">
245 <div class="aw-gate-row">
246 <span
247 class="aw-gate-dot"
248 style="background:#1e7f5c"
249 />
250 <span class="aw-gate-name">gate: security</span>
251 <span
252 class="aw-gate-state"
253 style="color:#1e7f5c"
254 >
255 passed
256 </span>
257 </div>
258 <div class="aw-gate-row">
259 <span
260 class="aw-gate-dot"
261 style="background:#1e7f5c"
262 />
263 <span class="aw-gate-name">gate: types</span>
264 <span
265 class="aw-gate-state"
266 style="color:#1e7f5c"
267 >
268 passed
269 </span>
270 </div>
271 <div class="aw-gate-row">
272 <span
273 id="aw-gate-tests-dot"
274 class="aw-gate-dot"
275 style="background:#b42318"
276 />
277 <span class="aw-gate-name">gate: tests</span>
278 <span
279 id="aw-gate-tests-state"
280 class="aw-gate-state"
281 style="color:#b42318"
282 >
283 failed
284 </span>
285 </div>
286 <div class="aw-gate-row">
287 <span
288 id="aw-gate-review-dot"
289 class="aw-gate-dot"
290 style="background:#c4c6cf"
291 />
292 <span class="aw-gate-name">review: human</span>
293 <span
294 id="aw-gate-review-state"
295 class="aw-gate-state"
296 style="color:#8a8d99"
297 >
298 waiting on you
299 </span>
300 </div>
301 </div>
302 </div>
303 </div>
304
305 {/* Sandbox */}
306 <div class="aw-rail-section">
307 <h3 class="aw-rail-label">Sandbox</h3>
308 <div class="aw-card">
309 <div class="aw-sandbox-url-row">
310 <a href="#" class="aw-sandbox-url">
311 pr-{prNumber}.preview.gluecron.com
312 </a>
313 <span class="aw-sandbox-live">live</span>
314 </div>
315 <p class="aw-sandbox-desc">
316 Isolated environment on this branch. Expires on merge.
317 </p>
318 <div class="aw-sandbox-actions">
319 <a href="#" class="aw-btn-sandbox">
320 Open preview
321 </a>
322 <a href="#" class="aw-btn-sandbox">
323 Join terminal
324 </a>
325 </div>
326 </div>
327 </div>
328
329 {/* Changes */}
330 <div class="aw-rail-section">
331 <h3 class="aw-rail-label">Changes · 11 files</h3>
332 <div class="aw-files">
333 {FILES.map((f, i) => (
334 <a
335 href={`/${owner}/${repo}/pull/${prNumber}/files`}
336 class="aw-file-row"
337 >
338 <span class="aw-file-path">{f.path}</span>
339 <span class="aw-file-add">+{f.add}</span>
340 <span class="aw-file-del">&#8722;{f.del}</span>
341 </a>
342 ))}
343 <a
344 href={`/${owner}/${repo}/pull/${prNumber}/files`}
345 class="aw-files-more"
346 >
347 Show all 11 files →
348 </a>
349 </div>
350 </div>
351
352 {/* Manual control reassurance */}
353 <div class="aw-control-note">
354 <p class="aw-control-text">
355 Nothing merges without you. Every agent action above is
356 recorded, reversible, and waiting on your review — approve,
357 request changes, or take over the branch manually at any
358 point.
359 </p>
360 </div>
361 </aside>
362 </div>
363 </main>
364 </div>
365 <script dangerouslySetInnerHTML={{ __html: js }} />
366 </>
367 );
368};
369
370export default AgentWorkspace;
371
372const css = `
373@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;450;500;600;700&family=Inter+Tight:wght@500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap');
374
375@keyframes gcPulse {
376 0%, 100% { opacity: 1; }
377 50% { opacity: 0.35; }
378}
379
380*, *::before, *::after { box-sizing: border-box; }
381
382.aw-root {
383 font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
384 font-size: 14px;
385 line-height: 1.55;
386 letter-spacing: -0.008em;
387 color: #16181d;
388 background: #fcfcfd;
389 min-height: 100vh;
390 display: flex;
391 flex-direction: column;
392 -webkit-font-smoothing: antialiased;
393 text-rendering: optimizeLegibility;
394}
395
396/* ── Top bar ────────────────────────────────────────────────────── */
397.aw-topbar {
398 height: 56px;
399 border-bottom: 1px solid rgba(22,24,29,0.07);
400 background: #fcfcfd;
401 display: flex;
402 align-items: center;
403 padding: 0 28px;
404 gap: 20px;
405 position: sticky;
406 top: 0;
407 z-index: 50;
408}
409
410.aw-logo {
411 display: inline-flex;
412 align-items: center;
413 gap: 9px;
414 font-family: 'Inter Tight', sans-serif;
415 font-weight: 600;
416 font-size: 15px;
417 letter-spacing: -0.02em;
418 color: #16181d;
419 text-decoration: none;
420 flex-shrink: 0;
421}
422
423.aw-logo-mark {
424 width: 16px;
425 height: 16px;
426 border-radius: 5px;
427 background: #4353c9;
428 display: inline-block;
429 flex-shrink: 0;
430}
431
432.aw-breadcrumb {
433 display: flex;
434 align-items: center;
435 gap: 4px;
436 font-size: 13px;
437 color: #6b7080;
438}
439
440.aw-breadcrumb-link {
441 color: #6b7080;
442 text-decoration: none;
443 padding: 5px 8px;
444 border-radius: 6px;
445 transition: color 0.15s;
446}
447.aw-breadcrumb-link:hover { color: #16181d; }
448
449.aw-breadcrumb-sep { color: #c4c6cf; }
450
451.aw-breadcrumb-current {
452 color: #16181d;
453 font-weight: 500;
454 padding: 5px 8px;
455}
456
457.aw-topbar-right {
458 margin-left: auto;
459 display: flex;
460 align-items: center;
461 gap: 14px;
462}
463
464.aw-system-status {
465 display: inline-flex;
466 align-items: center;
467 gap: 7px;
468 font-size: 12.5px;
469 color: #6b7080;
470}
471
472.aw-system-dot {
473 width: 6px;
474 height: 6px;
475 border-radius: 50%;
476 background: #1e7f5c;
477 flex-shrink: 0;
478}
479
480.aw-topbar-divider {
481 width: 1px;
482 height: 20px;
483 background: rgba(22,24,29,0.07);
484 flex-shrink: 0;
485}
486
487.aw-avatar {
488 width: 28px;
489 height: 28px;
490 border-radius: 50%;
491 background: #16181d;
492 color: #fff;
493 font-size: 11px;
494 font-weight: 600;
495 display: inline-flex;
496 align-items: center;
497 justify-content: center;
498 flex-shrink: 0;
499 user-select: none;
500}
501
502/* ── Main ───────────────────────────────────────────────────────── */
503.aw-main {
504 flex: 1;
505 max-width: 1360px;
506 width: 100%;
507 margin: 0 auto;
508 padding: 40px 32px 72px;
509}
510
511/* ── Page header ────────────────────────────────────────────────── */
512.aw-page-header {
513 display: flex;
514 align-items: flex-end;
515 justify-content: space-between;
516 gap: 24px;
517 flex-wrap: wrap;
518 margin-bottom: 36px;
519}
520
521.aw-eyebrow {
522 font-family: 'JetBrains Mono', monospace;
523 font-size: 11px;
524 letter-spacing: 0.12em;
525 text-transform: uppercase;
526 color: #8a8d99;
527 margin-bottom: 10px;
528}
529
530.aw-title {
531 font-family: 'Inter Tight', sans-serif;
532 font-size: 28px;
533 font-weight: 600;
534 letter-spacing: -0.025em;
535 line-height: 1.15;
536 color: #16181d;
537 margin: 0 0 6px;
538}
539
540.aw-meta {
541 display: flex;
542 align-items: center;
543 gap: 10px;
544 font-size: 13px;
545 color: #6b7080;
546 flex-wrap: wrap;
547}
548
549.aw-branch-label {
550 font-family: 'JetBrains Mono', monospace;
551 font-size: 12px;
552}
553
554.aw-meta-sep { color: #c4c6cf; }
555
556.aw-header-actions {
557 display: flex;
558 gap: 10px;
559 align-items: center;
560 flex-shrink: 0;
561}
562
563/* ── Buttons ────────────────────────────────────────────────────── */
564.aw-btn-secondary {
565 padding: 8px 16px;
566 border-radius: 8px;
567 font-size: 13px;
568 font-weight: 500;
569 border: 1px solid rgba(22,24,29,0.12);
570 background: #ffffff;
571 color: #16181d;
572 text-decoration: none;
573 display: inline-block;
574 cursor: pointer;
575 transition: border-color 0.15s;
576 font-family: inherit;
577 line-height: 1;
578}
579.aw-btn-secondary:hover { border-color: rgba(22,24,29,0.24); }
580
581.aw-btn-approve {
582 padding: 8px 16px;
583 border-radius: 8px;
584 font-size: 13px;
585 font-weight: 600;
586 border: 1px solid transparent;
587 background: #9a9da8;
588 color: #ffffff;
589 cursor: default;
590 font-family: inherit;
591 transition: background 0.2s, color 0.2s;
592 white-space: nowrap;
593}
594
595/* ── Two-column layout ──────────────────────────────────────────── */
596.aw-layout {
597 display: grid;
598 grid-template-columns: minmax(0, 1.55fr) minmax(300px, 1fr);
599 gap: 48px;
600 align-items: start;
601}
602
603/* ── Left column ────────────────────────────────────────────────── */
604.aw-section-header {
605 display: flex;
606 align-items: baseline;
607 justify-content: space-between;
608 margin-bottom: 18px;
609}
610
611.aw-section-title {
612 font-family: 'Inter Tight', sans-serif;
613 font-size: 15px;
614 font-weight: 600;
615 letter-spacing: -0.01em;
616 color: #16181d;
617 margin: 0;
618}
619
620.aw-session-clock {
621 font-size: 12px;
622 color: #8a8d99;
623}
624
625/* ── Timeline ───────────────────────────────────────────────────── */
626.aw-timeline {
627 display: flex;
628 flex-direction: column;
629}
630
631.aw-event {
632 display: grid;
633 grid-template-columns: 20px minmax(0, 1fr);
634 gap: 16px;
635 position: relative;
636 padding-bottom: 28px;
637}
638
639.aw-event--hidden { display: none; }
640.aw-event--visible { display: grid; }
641
642.aw-event-gutter {
643 display: flex;
644 flex-direction: column;
645 align-items: center;
646}
647
648.aw-event-dot {
649 width: 8px;
650 height: 8px;
651 border-radius: 50%;
652 margin-top: 6px;
653 flex-shrink: 0;
654}
655
656.aw-event-line {
657 width: 1px;
658 flex: 1;
659 background: rgba(22,24,29,0.08);
660 margin-top: 6px;
661 min-height: 12px;
662}
663
664.aw-event-line--hidden { display: none; }
665
666.aw-event-header {
667 display: flex;
668 align-items: baseline;
669 gap: 10px;
670 flex-wrap: wrap;
671}
672
673.aw-event-title {
674 font-size: 13.5px;
675 font-weight: 600;
676 color: #16181d;
677}
678
679.aw-event-time {
680 font-size: 12px;
681 color: #8a8d99;
682}
683
684.aw-event-text {
685 font-size: 13px;
686 color: #6b7080;
687 margin: 4px 0 0;
688 max-width: 56ch;
689}
690
691.aw-code-block {
692 margin: 12px 0 0;
693 padding: 14px 16px;
694 background: #ffffff;
695 border: 1px solid rgba(22,24,29,0.07);
696 border-radius: 10px;
697 font-family: 'JetBrains Mono', monospace;
698 font-size: 12px;
699 line-height: 1.7;
700 color: #16181d;
701 overflow-x: auto;
702 white-space: pre;
703}
704
705/* ── Diff card ──────────────────────────────────────────────────── */
706.aw-diff-card {
707 margin: 12px 0 0;
708 border: 1px solid rgba(22,24,29,0.07);
709 border-radius: 10px;
710 overflow: hidden;
711 background: #ffffff;
712}
713
714.aw-diff-header {
715 padding: 9px 16px;
716 border-bottom: 1px solid rgba(22,24,29,0.07);
717 font-family: 'JetBrains Mono', monospace;
718 font-size: 11.5px;
719 color: #6b7080;
720 display: flex;
721 justify-content: space-between;
722 align-items: center;
723}
724
725.aw-diff-plus { color: #1e7f5c; }
726.aw-diff-minus { color: #b42318; }
727
728.aw-diff-body {
729 font-family: 'JetBrains Mono', monospace;
730 font-size: 12px;
731 line-height: 1.75;
732 padding: 8px 0;
733}
734
735.aw-diff-ctx {
736 padding: 0 16px;
737 color: #8a8d99;
738}
739
740.aw-diff-rem {
741 padding: 0 16px;
742 background: rgba(180,35,24,0.05);
743 color: #b42318;
744}
745
746.aw-diff-ins {
747 padding: 0 16px;
748 background: rgba(30,127,92,0.06);
749 color: #1e7f5c;
750}
751
752/* ── Composer ───────────────────────────────────────────────────── */
753.aw-composer {
754 margin-top: 8px;
755 display: flex;
756 gap: 12px;
757 align-items: center;
758 border: 1px solid rgba(22,24,29,0.10);
759 border-radius: 10px;
760 background: #ffffff;
761 padding: 6px 6px 6px 16px;
762 transition: border-color 0.15s;
763}
764.aw-composer:focus-within { border-color: rgba(22,24,29,0.22); }
765
766.aw-composer-input {
767 flex: 1;
768 border: 0;
769 outline: none;
770 font-family: inherit;
771 font-size: 13.5px;
772 color: #16181d;
773 background: transparent;
774 min-width: 0;
775}
776.aw-composer-input::placeholder { color: #8a8d99; }
777
778.aw-composer-send {
779 padding: 7px 14px;
780 border-radius: 7px;
781 font-size: 12.5px;
782 font-weight: 500;
783 border: 0;
784 background: #16181d;
785 color: #fff;
786 cursor: pointer;
787 font-family: inherit;
788 flex-shrink: 0;
789 transition: opacity 0.15s;
790}
791.aw-composer-send:hover { opacity: 0.85; }
792
793/* ── Right rail ─────────────────────────────────────────────────── */
794.aw-rail {
795 display: flex;
796 flex-direction: column;
797 gap: 36px;
798 position: sticky;
799 top: 92px;
800}
801
802.aw-rail-section {}
803
804.aw-rail-label {
805 font-family: 'JetBrains Mono', monospace;
806 font-size: 11px;
807 letter-spacing: 0.12em;
808 text-transform: uppercase;
809 color: #8a8d99;
810 font-weight: 500;
811 margin: 0 0 14px;
812}
813
814.aw-card {
815 border: 1px solid rgba(22,24,29,0.07);
816 border-radius: 12px;
817 background: #ffffff;
818 padding: 20px;
819}
820
821/* ── Status card ────────────────────────────────────────────────── */
822.aw-status-row {
823 display: flex;
824 align-items: center;
825 gap: 10px;
826 margin-bottom: 14px;
827}
828
829.aw-status-dot {
830 width: 8px;
831 height: 8px;
832 border-radius: 50%;
833 flex-shrink: 0;
834}
835
836.aw-pulse { animation: gcPulse 1.4s ease-in-out infinite; }
837
838.aw-status-label {
839 font-size: 14px;
840 font-weight: 600;
841 color: #16181d;
842}
843
844.aw-status-body {
845 font-size: 13px;
846 color: #6b7080;
847 margin: 0;
848 line-height: 1.6;
849}
850
851.aw-card-divider {
852 height: 1px;
853 background: rgba(22,24,29,0.06);
854 margin: 16px 0;
855}
856
857.aw-gates {
858 display: flex;
859 flex-direction: column;
860 gap: 10px;
861}
862
863.aw-gate-row {
864 display: flex;
865 align-items: center;
866 gap: 10px;
867 font-size: 13px;
868}
869
870.aw-gate-dot {
871 width: 7px;
872 height: 7px;
873 border-radius: 50%;
874 flex-shrink: 0;
875}
876
877.aw-gate-name {
878 font-family: 'JetBrains Mono', monospace;
879 font-size: 12px;
880 color: #16181d;
881}
882
883.aw-gate-state {
884 margin-left: auto;
885 font-size: 12px;
886}
887
888/* ── Sandbox card ───────────────────────────────────────────────── */
889.aw-sandbox-url-row {
890 display: flex;
891 align-items: center;
892 justify-content: space-between;
893 gap: 10px;
894 margin-bottom: 6px;
895}
896
897.aw-sandbox-url {
898 font-family: 'JetBrains Mono', monospace;
899 font-size: 12.5px;
900 color: #4353c9;
901 text-decoration: none;
902 overflow: hidden;
903 text-overflow: ellipsis;
904 white-space: nowrap;
905}
906.aw-sandbox-url:hover { text-decoration: underline; }
907
908.aw-sandbox-live {
909 font-size: 11px;
910 color: #1e7f5c;
911 font-weight: 500;
912 flex-shrink: 0;
913}
914
915.aw-sandbox-desc {
916 font-size: 12.5px;
917 color: #6b7080;
918 margin: 0 0 16px;
919 line-height: 1.6;
920}
921
922.aw-sandbox-actions {
923 display: flex;
924 gap: 8px;
925}
926
927.aw-btn-sandbox {
928 flex: 1;
929 padding: 7px 0;
930 border-radius: 8px;
931 font-size: 12.5px;
932 font-weight: 500;
933 border: 1px solid rgba(22,24,29,0.12);
934 background: #ffffff;
935 color: #16181d;
936 text-decoration: none;
937 text-align: center;
938 transition: border-color 0.15s;
939}
940.aw-btn-sandbox:hover { border-color: rgba(22,24,29,0.24); }
941
942/* ── Files list ─────────────────────────────────────────────────── */
943.aw-files {
944 display: flex;
945 flex-direction: column;
946 gap: 9px;
947}
948
949.aw-file-row {
950 display: flex;
951 align-items: center;
952 gap: 8px;
953 font-family: 'JetBrains Mono', monospace;
954 font-size: 12px;
955 color: #16181d;
956 text-decoration: none;
957 overflow: hidden;
958 transition: color 0.15s;
959}
960.aw-file-row:hover { color: #4353c9; }
961
962.aw-file-path {
963 overflow: hidden;
964 text-overflow: ellipsis;
965 white-space: nowrap;
966 flex: 1;
967 min-width: 0;
968}
969
970.aw-file-add {
971 margin-left: auto;
972 color: #1e7f5c;
973 flex-shrink: 0;
974}
975
976.aw-file-del {
977 color: #b42318;
978 flex-shrink: 0;
979}
980
981.aw-files-more {
982 font-size: 12px;
983 color: #6b7080;
984 text-decoration: none;
985 margin-top: 2px;
986 transition: color 0.15s;
987}
988.aw-files-more:hover { color: #16181d; }
989
990/* ── Control note ───────────────────────────────────────────────── */
991.aw-control-note {
992 border-top: 1px solid rgba(22,24,29,0.07);
993 padding-top: 20px;
994}
995
996.aw-control-text {
997 font-size: 12.5px;
998 color: #8a8d99;
999 margin: 0;
1000 line-height: 1.6;
1001}
1002
1003/* ── Responsive ─────────────────────────────────────────────────── */
1004@media (max-width: 900px) {
1005 .aw-layout {
1006 grid-template-columns: 1fr;
1007 }
1008 .aw-rail {
1009 position: static;
1010 }
1011}
1012`;
1013
1014const js = `
1015(function () {
1016 var step = 0;
1017 var approved = false;
1018 var seconds = 47 * 60 + 12;
1019
1020 var GREEN = '#1e7f5c';
1021 var AMBER = '#b45309';
1022 var RED = '#b42318';
1023 var INK = '#16181d';
1024 var GREY = '#c4c6cf';
1025 var PULSE = 'gcPulse 1.4s ease-in-out infinite';
1026
1027 var EVENT_DOTS = [RED, INK, INK, GREEN, AMBER];
1028
1029 /* ── helpers ── */
1030 function el(id) { return document.getElementById(id); }
1031
1032 function setAnim(elem, doAnim) {
1033 if (!elem) return;
1034 elem.style.animation = doAnim ? PULSE : 'none';
1035 }
1036
1037 /* ── clock ── */
1038 function updateClock() {
1039 var e = el('aw-session-clock');
1040 if (!e) return;
1041 var m = Math.floor(seconds / 60);
1042 var s = String(seconds % 60).padStart(2, '0');
1043 e.textContent = 'session · ' + m + ':' + s;
1044 }
1045
1046 /* ── timeline ── */
1047 function updateTimeline() {
1048 for (var i = 0; i <= 4; i++) {
1049 var ev = el('aw-event-' + i);
1050 if (!ev) continue;
1051
1052 if (i <= step) {
1053 ev.classList.remove('aw-event--hidden');
1054 ev.classList.add('aw-event--visible');
1055 ev.style.paddingBottom = (i === step) ? '0' : '28px';
1056
1057 var line = el('aw-event-line-' + i);
1058 if (line) {
1059 if (i === step) {
1060 line.classList.add('aw-event-line--hidden');
1061 } else {
1062 line.classList.remove('aw-event-line--hidden');
1063 }
1064 }
1065
1066 var dot = el('aw-event-dot-' + i);
1067 if (dot) {
1068 dot.style.background = EVENT_DOTS[i];
1069 setAnim(dot, i === step && step < 4);
1070 }
1071 }
1072 }
1073 }
1074
1075 /* ── status card ── */
1076 function updateStatus() {
1077 var dot = el('aw-status-dot');
1078 var label = el('aw-status-label');
1079 var body = el('aw-status-body');
1080
1081 var lbl, txt, color, pulse;
1082
1083 if (approved) {
1084 lbl = 'Merging';
1085 color = GREEN;
1086 pulse = true;
1087 txt = 'Approved. Auto-merge is completing and the deploy will follow — typically under a minute end-to-end.';
1088 } else if (step < 3) {
1089 lbl = 'Agent repairing';
1090 color = AMBER;
1091 pulse = true;
1092 txt = 'Claude is working inside the PR sandbox. You can watch each step, join the terminal, or interrupt at any time.';
1093 } else if (step < 4) {
1094 lbl = 'Gates green';
1095 color = GREEN;
1096 pulse = false;
1097 txt = 'The repair passed every gate. Claude is writing up the session summary.';
1098 } else {
1099 lbl = 'Your review';
1100 color = AMBER;
1101 pulse = true;
1102 txt = 'Everything is green and waiting on one human approval. The full diff and sandbox are one click away.';
1103 }
1104
1105 if (dot) { dot.style.background = color; setAnim(dot, pulse); }
1106 if (label) { label.textContent = lbl; }
1107 if (body) { body.textContent = txt; }
1108 }
1109
1110 /* ── gate rows ── */
1111 function updateGates() {
1112 var testsDot = el('aw-gate-tests-dot');
1113 var testsState = el('aw-gate-tests-state');
1114
1115 if (testsDot && testsState) {
1116 if (step >= 3) {
1117 testsDot.style.background = GREEN;
1118 setAnim(testsDot, false);
1119 testsState.textContent = 'passed';
1120 testsState.style.color = GREEN;
1121 } else if (step >= 1) {
1122 testsDot.style.background = AMBER;
1123 setAnim(testsDot, true);
1124 testsState.textContent = 're-running';
1125 testsState.style.color = AMBER;
1126 } else {
1127 testsDot.style.background = RED;
1128 setAnim(testsDot, false);
1129 testsState.textContent = 'failed';
1130 testsState.style.color = RED;
1131 }
1132 }
1133
1134 var reviewDot = el('aw-gate-review-dot');
1135 var reviewState = el('aw-gate-review-state');
1136
1137 if (reviewDot && reviewState) {
1138 if (approved) {
1139 reviewDot.style.background = GREEN;
1140 setAnim(reviewDot, false);
1141 reviewState.textContent = 'approved';
1142 reviewState.style.color = GREEN;
1143 } else if (step >= 4) {
1144 reviewDot.style.background = AMBER;
1145 setAnim(reviewDot, true);
1146 reviewState.textContent = 'waiting on you';
1147 reviewState.style.color = AMBER;
1148 } else {
1149 reviewDot.style.background = GREY;
1150 setAnim(reviewDot, false);
1151 reviewState.textContent = 'waiting on you';
1152 reviewState.style.color = '#8a8d99';
1153 }
1154 }
1155 }
1156
1157 /* ── approve button ── */
1158 function updateApproveBtn() {
1159 var btn = el('aw-approve-btn');
1160 if (!btn) return;
1161
1162 if (approved) {
1163 btn.textContent = 'Approved — merging';
1164 btn.style.background = GREEN;
1165 btn.style.cursor = 'default';
1166 } else if (step >= 4) {
1167 btn.textContent = 'Approve & merge';
1168 btn.style.background = INK;
1169 btn.style.cursor = 'pointer';
1170 } else {
1171 btn.textContent = 'Approve & merge';
1172 btn.style.background = '#9a9da8';
1173 btn.style.cursor = 'default';
1174 }
1175 }
1176
1177 /* ── full render ── */
1178 function render() {
1179 updateClock();
1180 updateTimeline();
1181 updateStatus();
1182 updateGates();
1183 updateApproveBtn();
1184 }
1185
1186 /* ── init ── */
1187 function init() {
1188 render();
1189
1190 /* step timer: advance every 4 s */
1191 var stepTimer = setInterval(function () {
1192 if (step < 4) {
1193 step++;
1194 render();
1195 } else {
1196 clearInterval(stepTimer);
1197 }
1198 }, 4000);
1199
1200 /* wall-clock: tick every 1 s */
1201 setInterval(function () {
1202 seconds++;
1203 updateClock();
1204 }, 1000);
1205
1206 /* approve button */
1207 var btn = el('aw-approve-btn');
1208 if (btn) {
1209 btn.addEventListener('click', function () {
1210 if (step >= 4 && !approved) {
1211 approved = true;
1212 render();
1213 }
1214 });
1215 }
1216
1217 /* composer send */
1218 var sendBtn = el('aw-send-btn');
1219 var input = el('aw-composer-input');
1220 if (sendBtn && input) {
1221 sendBtn.addEventListener('click', function () {
1222 input.value = '';
1223 input.focus();
1224 });
1225 input.addEventListener('keydown', function (e) {
1226 if (e.key === 'Enter') { input.value = ''; }
1227 });
1228 }
1229 }
1230
1231 if (document.readyState === 'loading') {
1232 document.addEventListener('DOMContentLoaded', init);
1233 } else {
1234 init();
1235 }
1236})();
1237`;