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

components.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.

components.tsxBlame887 lines · 3 contributors
fc1817aClaude1import type { FC } from "hono/jsx";
06d5ffeClaude2import { html } from "hono/html";
fc1817aClaude3import type { GitCommit, GitTreeEntry, GitDiffFile } from "../git/repository";
06d5ffeClaude4import type { Repository } from "../db/schema";
82c3a38ccanty labs5import { parseDiff, pairLines } from "../lib/diff";
6import type { DiffLine, ParsedFile, SplitRow } from "../lib/diff";
fc1817aClaude7
8c790e0Claude8/**
9 * Describes the most recent push to a repo, used by RepoHeader to render
10 * the Push Watch discoverability indicator.
11 *
12 * - ageMs < 5 min \u2192 pulsing red "\u25cf Live" badge
13 * - ageMs < 24 hr \u2192 dimmer "\u25cb Watch" link
14 * - otherwise \u2192 nothing shown
15 */
16export interface RecentPush {
17 sha: string;
18 ageMs: number;
19}
20
06d5ffeClaude21export const RepoHeader: FC<{
22 owner: string;
23 repo: string;
24 starCount?: number;
25 starred?: boolean;
c81ab7aClaude26 forkCount?: number;
06d5ffeClaude27 currentUser?: string | null;
c81ab7aClaude28 forkedFrom?: string | null;
71cd5ecClaude29 archived?: boolean;
30 isTemplate?: boolean;
8c790e0Claude31 /** Most recent push info for Push Watch discoverability indicator. */
32 recentPush?: RecentPush | null;
77cf834Claude33 /** 0-100 health score badge rendered after the repo name. Optional — omit to hide. */
34 healthScore?: number;
71cd5ecClaude35}> = ({
36 owner,
37 repo,
38 starCount,
39 starred,
40 forkCount,
41 currentUser,
42 forkedFrom,
43 archived,
44 isTemplate,
8c790e0Claude45 recentPush,
77cf834Claude46 healthScore,
8c790e0Claude47}) => {
48 const FIVE_MIN = 5 * 60 * 1000;
49 const TWENTY_FOUR_HR = 24 * 60 * 60 * 1000;
50 const isLive = recentPush != null && recentPush.ageMs < FIVE_MIN;
51 const isRecent = recentPush != null && recentPush.ageMs < TWENTY_FOUR_HR;
52
77cf834Claude53 const healthColor =
54 healthScore === undefined ? null :
55 healthScore >= 80 ? "#34d399" :
56 healthScore >= 50 ? "#facc15" :
57 "#f87171";
58
8c790e0Claude59 return (
60 <div class="repo-header">
61 <div>
62 <div class="repo-header-title">
63 <a href={`/${owner}`} class="owner">
64 {owner}
65 </a>
66 <span class="separator">/</span>
67 <a href={`/${owner}/${repo}`} class="name">
68 {repo}
69 </a>
70 {archived && (
71 <span
72 class="repo-header-pill repo-header-pill-archived"
73 title="Read-only: pushes and new issues/PRs disabled"
74 >
75 Archived
76 </span>
77 )}
78 {isTemplate && (
79 <span
80 class="repo-header-pill repo-header-pill-template"
81 title="This repository can be used as a template"
82 >
83 Template
84 </span>
85 )}
77cf834Claude86 {healthScore !== undefined && healthColor && (
87 <a
88 href={`/${owner}/${repo}/health`}
89 title={`Repository health score: ${healthScore}/100 — click for breakdown`}
90 style={`display:inline-flex;align-items:center;gap:4px;padding:2px 8px;border-radius:9999px;font-size:11px;font-weight:700;text-decoration:none;color:${healthColor};background:${healthColor}22;border:1px solid ${healthColor}44;`}
91 >
92 &#x2665; Health {healthScore}
93 </a>
94 )}
8c790e0Claude95 {isLive && recentPush && (
96 <a
97 href={`/${owner}/${repo}/push/${recentPush.sha}`}
98 class="repo-header-live-badge repo-header-live-badge--live"
99 title="Push in progress \u2014 watch live gate + deploy status"
100 aria-label="Live push \u2014 click to watch status"
101 >
102 <span class="repo-header-live-dot" aria-hidden="true">{"\u25cf"}</span>
103 Live
104 </a>
105 )}
106 {!isLive && isRecent && recentPush && (
107 <a
108 href={`/${owner}/${repo}/push/${recentPush.sha}`}
109 class="repo-header-live-badge repo-header-live-badge--recent"
110 title="Watch the most recent push's gate + deploy results"
111 aria-label="Watch most recent push"
112 >
113 <span aria-hidden="true">{"\u25cb"}</span>
114 Watch
115 </a>
116 )}
117 </div>
118 {forkedFrom && (
119 <div class="repo-header-fork">
120 forked from <a href={`/${forkedFrom}`}>{forkedFrom}</a>
121 </div>
71cd5ecClaude122 )}
c81ab7aClaude123 </div>
8c790e0Claude124 <div class="repo-header-actions">
125 {currentUser && currentUser !== owner && (
126 <form method="post" action={`/${owner}/${repo}/fork`} style="display:inline">
127 <button type="submit" class="star-btn">
128 {"\u2442"} Fork {forkCount !== undefined && forkCount > 0 ? forkCount : ""}
06d5ffeClaude129 </button>
130 </form>
8c790e0Claude131 )}
132 {starCount !== undefined && (
133 currentUser ? (
134 <form method="post" action={`/${owner}/${repo}/star`} style="display:inline">
135 <button
136 type="submit"
137 class={`star-btn${starred ? " starred" : ""}`}
138 >
139 {starred ? "\u2605" : "\u2606"} {starCount}
140 </button>
141 </form>
142 ) : (
143 <span class="star-btn">
144 {"\u2606"} {starCount}
145 </span>
146 )
147 )}
148 </div>
06d5ffeClaude149 </div>
8c790e0Claude150 );
151};
fc1817aClaude152
153export const RepoNav: FC<{
154 owner: string;
155 repo: string;
3ef4c9dClaude156 active:
157 | "code"
158 | "commits"
159 | "issues"
160 | "pulls"
161 | "releases"
eafe8c6Claude162 | "actions"
3ef4c9dClaude163 | "gates"
3cbe3d6Claude164 | "insights"
165 | "explain"
166 | "changelog"
1e162a8Claude167 | "semantic"
168 | "wiki"
0316dbbClaude169 | "projects"
ef3fd93Claude170 | "agents"
c645a86Claude171 | "discussions"
da3fc18Claude172 | "security"
bcc4020Claude173 | "settings"
7f992cdClaude174 | "debt-map"
175 | "migrate"
77cf834Claude176 | "deployments"
0224546Claude177 | "nl-search"
178 | "contributors"
179 | "pulse"
8ed88f2ccantynz-alt180 | "traffic"
181 | "pipeline"
182 | "workspace"
183 | "archaeology";
0224546Claude184 /** Current authenticated user — used for owner-only tab gating. */
185 currentUser?: string | null;
186 /** Repo owner username — used for owner-only tab gating. */
187 repoOwner?: string;
188}> = ({ owner, repo, active, currentUser, repoOwner }) => (
fc1817aClaude189 <div class="repo-nav">
06d5ffeClaude190 <a href={`/${owner}/${repo}`} class={active === "code" ? "active" : ""}>
fc1817aClaude191 Code
192 </a>
79136bbClaude193 <a
194 href={`/${owner}/${repo}/issues`}
195 class={active === "issues" ? "active" : ""}
196 >
197 Issues
198 </a>
c645a86Claude199 <a
200 href={`/${owner}/${repo}/discussions`}
201 class={active === "discussions" ? "active" : ""}
202 >
203 Discussions
204 </a>
1e162a8Claude205 <a
206 href={`/${owner}/${repo}/wiki`}
207 class={active === "wiki" ? "active" : ""}
208 >
209 Wiki
210 </a>
0074234Claude211 <a
212 href={`/${owner}/${repo}/pulls`}
213 class={active === "pulls" ? "active" : ""}
214 >
215 Pull Requests
216 </a>
1e162a8Claude217 <a
218 href={`/${owner}/${repo}/projects`}
219 class={active === "projects" ? "active" : ""}
220 >
221 Projects
222 </a>
fc1817aClaude223 <a
224 href={`/${owner}/${repo}/commits`}
225 class={active === "commits" ? "active" : ""}
226 >
227 Commits
228 </a>
eafe8c6Claude229 <a
230 href={`/${owner}/${repo}/actions`}
231 class={active === "actions" ? "active" : ""}
232 >
233 Actions
234 </a>
3ef4c9dClaude235 <a
236 href={`/${owner}/${repo}/releases`}
237 class={active === "releases" ? "active" : ""}
238 >
239 Releases
240 </a>
0224546Claude241 <a
242 href={`/${owner}/${repo}/contributors`}
243 class={active === "contributors" ? "active" : ""}
244 >
245 Contributors
246 </a>
247 <a
248 href={`/${owner}/${repo}/pulse`}
249 class={active === "pulse" ? "active" : ""}
250 >
251 Pulse
252 </a>
253 {currentUser && repoOwner && currentUser === repoOwner && (
254 <a
255 href={`/${owner}/${repo}/traffic`}
256 class={active === "traffic" ? "active" : ""}
257 >
258 Traffic
259 </a>
260 )}
3ef4c9dClaude261 <a
262 href={`/${owner}/${repo}/gates`}
263 class={active === "gates" ? "active" : ""}
264 >
265 {"\u25CF"} Gates
266 </a>
da3fc18Claude267 <a
268 href={`/${owner}/${repo}/security/vulnerabilities`}
269 class={active === "security" ? "active" : ""}
270 >
271 Security
272 </a>
ae2a071ccanty labs273 <a
274 href={`/${owner}/${repo}/settings`}
275 class={active === "settings" ? "active" : ""}
276 >
277 Settings
278 </a>
c9ed210Claude279 <a
280 href={`/${owner}/${repo}/cloud-deployments`}
281 class={active === "deployments" ? "active" : ""}
282 >
283 Deployments
284 </a>
2c61840Claude285 <a
286 href={`/${owner}/${repo}/pipeline`}
287 class={active === "pipeline" ? "active" : ""}
288 >
289 Pipeline
290 </a>
3ef4c9dClaude291 <a
292 href={`/${owner}/${repo}/insights`}
293 class={active === "insights" ? "active" : ""}
294 >
295 Insights
296 </a>
ef3fd93Claude297 <a
298 href={`/${owner}/${repo}/agents`}
299 class={active === "agents" ? "active" : ""}
300 >
301 Agents
302 </a>
3cbe3d6Claude303 <a
304 href={`/${owner}/${repo}/explain`}
debcf27Claude305 class={`repo-nav-ai${active === "explain" ? " active" : ""}`}
306 style="margin-left: auto"
3cbe3d6Claude307 >
308 {"\u2728"} Explain
309 </a>
debcf27Claude310 <a href={`/${owner}/${repo}/ask`} class="repo-nav-ai">
3ef4c9dClaude311 {"\u2728"} Ask AI
312 </a>
2c61840Claude313 <a
314 href={`/${owner}/${repo}/workspace`}
315 class={`repo-nav-ai${active === "workspace" ? " active" : ""}`}
316 title="AI Workspace — spec-to-PR, fix issues with AI, web editor"
317 >
318 {"✨"} Workspace
319 </a>
14c3cc8Claude320 <a
321 href={`/${owner}/${repo}/spec`}
debcf27Claude322 class="repo-nav-ai"
14c3cc8Claude323 title="Spec to PR — paste a feature spec, AI opens a draft PR"
324 >
325 {"\u2728"} Spec
326 </a>
d8ef5efClaude327 <a
328 href={`/${owner}/${repo}/ai/tests`}
debcf27Claude329 class="repo-nav-ai"
d8ef5efClaude330 title="AI Tests \u2014 generate failing test stubs from a source file"
331 >
332 {"\u2728"} Tests
333 </a>
bcc4020Claude334 <a
335 href={`/${owner}/${repo}/debt-map`}
336 class={`repo-nav-ai${active === "debt-map" ? " active" : ""}`}
337 title="AI Debt Map \u2014 visual technical debt graph with Claude analysis"
338 >
339 {"\u2593"} Debt Map
340 </a>
77cf834Claude341 <a
342 href={`/${owner}/${repo}/search/nl`}
343 class={`repo-nav-ai${active === "nl-search" ? " active" : ""}`}
344 title="Natural Language Search \u2014 search by intent, not keywords"
345 >
346 {"\u2728"} NL Search
347 </a>
b1070a5Claude348 <a
349 href={`/${owner}/${repo}/archaeology`}
350 class={`repo-nav-ai${active === "archaeology" ? " active" : ""}`}
351 title="AI Archaeology \u2014 excavate why any file exists using git history, PRs, and issues"
352 >
353 {"\ud83c\udfdb"} Archaeology
354 </a>
fc1817aClaude355 </div>
356);
357
06d5ffeClaude358export const BranchSwitcher: FC<{
359 owner: string;
360 repo: string;
361 currentRef: string;
362 branches: string[];
363 pathType: "tree" | "blob" | "commits";
364 subPath?: string;
365}> = ({ owner, repo, currentRef, branches, pathType, subPath }) => {
366 if (branches.length <= 1) {
367 return <div class="branch-selector">{currentRef}</div>;
368 }
369
370 return (
371 <div class="branch-dropdown">
372 <button class="branch-selector" type="button">
373 {currentRef} &#9662;
374 </button>
375 <div class="branch-dropdown-content">
376 {branches.map((branch) => {
377 let href: string;
378 if (pathType === "commits") {
379 href = `/${owner}/${repo}/commits/${branch}`;
380 } else if (subPath) {
381 href = `/${owner}/${repo}/${pathType}/${branch}/${subPath}`;
382 } else {
383 href = `/${owner}/${repo}/tree/${branch}`;
384 }
385 return (
386 <a
387 href={href}
388 class={branch === currentRef ? "active-branch" : ""}
389 >
390 {branch}
391 </a>
392 );
393 })}
394 </div>
395 </div>
396 );
397};
398
fc1817aClaude399export const Breadcrumb: FC<{
400 owner: string;
401 repo: string;
402 ref: string;
403 path: string;
404}> = ({ owner, repo, ref, path }) => {
405 const parts = path.split("/").filter(Boolean);
406 const crumbs: { name: string; href: string }[] = [
407 { name: repo, href: `/${owner}/${repo}/tree/${ref}` },
408 ];
409 let accumulated = "";
410 for (const part of parts) {
411 accumulated += (accumulated ? "/" : "") + part;
412 crumbs.push({
413 name: part,
414 href: `/${owner}/${repo}/tree/${ref}/${accumulated}`,
415 });
416 }
417 return (
418 <div class="breadcrumb">
419 {crumbs.map((crumb, i) => (
420 <>
421 {i > 0 && <span>/</span>}
422 {i === crumbs.length - 1 ? (
423 <strong>{crumb.name}</strong>
424 ) : (
425 <a href={crumb.href}>{crumb.name}</a>
426 )}
427 </>
428 ))}
429 </div>
430 );
431};
432
433export const FileTable: FC<{
434 entries: GitTreeEntry[];
435 owner: string;
436 repo: string;
437 ref: string;
438 path: string;
439}> = ({ entries, owner, repo, ref, path }) => (
440 <table class="file-table">
441 <tbody>
442 {entries.map((entry) => {
443 const fullPath = path ? `${path}/${entry.name}` : entry.name;
444 const href =
445 entry.type === "tree"
446 ? `/${owner}/${repo}/tree/${ref}/${fullPath}`
447 : `/${owner}/${repo}/blob/${ref}/${fullPath}`;
448 return (
449 <tr>
450 <td class="file-icon">
451 {entry.type === "tree" ? "\u{1F4C1}" : "\u{1F4C4}"}
452 </td>
453 <td class="file-name">
454 <a href={href}>{entry.name}</a>
455 </td>
456 <td style="text-align: right; color: var(--text-muted); font-size: 13px;">
457 {entry.size !== undefined ? formatSize(entry.size) : ""}
458 </td>
459 </tr>
460 );
461 })}
462 </tbody>
463 </table>
464);
465
06d5ffeClaude466export const HighlightedCode: FC<{
467 highlightedHtml: string;
468 lineCount: number;
469}> = ({ highlightedHtml, lineCount }) => {
470 const lineNums = Array.from({ length: lineCount }, (_, i) => i + 1);
471 return (
472 <div class="blob-code">
473 <table>
474 <tbody>
475 <tr>
476 <td class="line-num" style="vertical-align: top; padding-top: 0; padding-bottom: 0">
477 <pre style="margin: 0; line-height: 1.6; font-size: 13px">
478 {lineNums.map((n) => (
479 <>
480 <span>{n}</span>
481 {"\n"}
482 </>
483 ))}
484 </pre>
485 </td>
486 <td class="line-content" style="vertical-align: top; padding-top: 0; padding-bottom: 0">
487 <pre style="margin: 0; line-height: 1.6; font-size: 13px">{html([highlightedHtml] as unknown as TemplateStringsArray)}</pre>
488 </td>
489 </tr>
490 </tbody>
491 </table>
492 </div>
493 );
494};
495
496export const PlainCode: FC<{ lines: string[] }> = ({ lines }) => (
497 <div class="blob-code">
498 <table>
499 <tbody>
500 {lines.map((line, i) => (
501 <tr>
502 <td class="line-num">{i + 1}</td>
503 <td class="line-content">{line}</td>
504 </tr>
505 ))}
506 </tbody>
507 </table>
508 </div>
509);
510
fc1817aClaude511export const CommitList: FC<{
512 commits: GitCommit[];
513 owner: string;
514 repo: string;
3951454Claude515 verifications?: Record<string, { verified: boolean; reason: string }>;
516}> = ({ commits, owner, repo, verifications }) => (
fc1817aClaude517 <div class="commit-list">
3951454Claude518 {commits.map((commit) => {
519 const v = verifications?.[commit.sha];
520 return (
521 <div class="commit-item">
522 <div>
523 <div class="commit-message">
524 <a href={`/${owner}/${repo}/commit/${commit.sha}`}>
525 {commit.message}
526 </a>
527 {v?.verified && (
ae2a071ccanty labs528 <a
529 href="/settings/signing-keys"
530 title="Signed with a registered key — manage your signing keys"
531 style="margin-left:8px;font-size:10px;padding:1px 6px;border-radius:3px;background:var(--green,#2ea043);color:#fff;text-transform:uppercase;letter-spacing:.4px;text-decoration:none"
3951454Claude532 >
533 Verified
ae2a071ccanty labs534 </a>
3951454Claude535 )}
536 </div>
537 <div class="commit-meta">
538 {commit.author} committed {formatRelativeDate(commit.date)}
539 </div>
fc1817aClaude540 </div>
3951454Claude541 <a
542 href={`/${owner}/${repo}/commit/${commit.sha}`}
543 class="commit-sha"
544 >
545 {commit.sha.slice(0, 7)}
546 </a>
fc1817aClaude547 </div>
3951454Claude548 );
549 })}
fc1817aClaude550 </div>
551);
552
82c3a38ccanty labs553// ---- Diff viewer ----------------------------------------------------------
554
555const _diffScript = `
556function diffToggleView(id,mode){
557 var v=document.getElementById(id);if(!v)return;
558 v.querySelectorAll('.diff-table-inline').forEach(function(t){t.hidden=(mode==='split');});
559 v.querySelectorAll('.diff-table-split').forEach(function(t){t.hidden=(mode!=='split');});
560 v.querySelectorAll('.diff-view-btn').forEach(function(b){b.classList.remove('active');});
561 var btn=v.querySelector('.diff-view-btn-'+mode);if(btn)btn.classList.add('active');
562}
563function diffToggleFile(id){
564 var body=document.getElementById(id);if(!body)return;
565 body.hidden=!body.hidden;
566 var hdr=body.previousElementSibling;if(hdr)hdr.classList.toggle('collapsed',body.hidden);
567}
568`;
569
fc1817aClaude570export const DiffView: FC<{ raw: string; files: GitDiffFile[] }> = ({
571 raw,
572 files,
573}) => {
82c3a38ccanty labs574 const parsed = parseDiff(raw);
575 const totalAdd = files.reduce((s, f) => s + f.additions, 0);
576 const totalDel = files.reduce((s, f) => s + f.deletions, 0);
577
578 if (parsed.length === 0 && files.length === 0) {
579 return (
580 <div class="diff-view">
581 <p style="color:var(--text-muted);font-size:14px">No changes.</p>
582 </div>
583 );
584 }
fc1817aClaude585
586 return (
82c3a38ccanty labs587 <div class="diff-view" id="diff-view-main">
588 <script dangerouslySetInnerHTML={{ __html: _diffScript }} />
589
590 {/* Toolbar */}
591 <div class="diff-toolbar">
592 <div class="diff-summary">
593 <strong>{files.length}</strong> file{files.length !== 1 ? "s" : ""} changed,{" "}
594 <span class="stat-add">+{totalAdd}</span>{" "}
595 <span class="stat-del">-{totalDel}</span>
596 </div>
597 <div class="diff-view-toggle">
598 <button class="diff-view-btn diff-view-btn-inline active" onclick="diffToggleView('diff-view-main','inline')" type="button">Inline</button>
599 <button class="diff-view-btn diff-view-btn-split" onclick="diffToggleView('diff-view-main','split')" type="button">Split</button>
600 </div>
fc1817aClaude601 </div>
82c3a38ccanty labs602
603 {/* File jump list (shown when > 1 file) */}
604 {parsed.length > 1 && (
605 <div class="diff-jump-list">
606 {parsed.map((f, i) => {
607 const isAdd = f.additions > 0 && f.deletions === 0;
608 const isDel = f.additions === 0 && f.deletions > 0;
609 return (
610 <a class="diff-jump-item" href={`#diff-file-${i}`}>
611 <span class={isAdd ? "stat-add" : isDel ? "stat-del" : "diff-jump-mod"}>
612 {isAdd ? "+" : isDel ? "−" : "~"}
613 </span>{" "}
614 {f.path.split("/").pop() || f.path}
615 </a>
616 );
617 })}
fc1817aClaude618 </div>
82c3a38ccanty labs619 )}
620
621 {/* Per-file blocks */}
622 {parsed.map((file, idx) => {
623 const bodyId = `diff-body-${idx}`;
624 const splitRows = pairLines(file.lines);
625 return (
626 <div class="diff-file" id={`diff-file-${idx}`}>
627 <div class="diff-file-header" onclick={`diffToggleFile('${bodyId}')`}>
628 <span class="diff-file-path" title={file.path}>{file.path}</span>
629 <span class="diff-file-meta">
630 {file.isBinary
631 ? <span style="color:var(--text-muted)">binary</span>
632 : <><span class="stat-add">+{file.additions}</span>{" "}<span class="stat-del">-{file.deletions}</span></>
633 }
634 <span class="diff-file-chevron">▾</span>
635 </span>
636 </div>
637 <div id={bodyId}>
638 {file.isBinary ? (
639 <div class="diff-binary">Binary file changed</div>
640 ) : (
641 <>
642 {/* Inline table (default) */}
643 <table class="diff-table diff-table-inline">
644 <tbody>
645 {file.lines.map((line) => {
646 if (line.type === "hunk") {
647 return (
648 <tr class="diff-row diff-row-hunk">
649 <td class="diff-ln diff-ln-old"></td>
650 <td class="diff-ln diff-ln-new"></td>
651 <td class="diff-cell">{line.content}</td>
652 </tr>
653 );
654 }
655 const sigil = line.type === "add" ? "+" : line.type === "del" ? "-" : " ";
656 return (
657 <tr class={`diff-row diff-row-${line.type}${line.wsOnly ? " diff-row-ws" : ""}`}>
658 <td class="diff-ln diff-ln-old">{line.oldLine ?? ""}</td>
659 <td class="diff-ln diff-ln-new">{line.newLine ?? ""}</td>
660 <td class="diff-cell">
661 <span class="diff-sigil">{sigil}</span>
662 {line.content}
663 {line.wsOnly && <span class="diff-ws-badge" title="whitespace-only change">ws</span>}
664 </td>
665 </tr>
666 );
667 })}
668 </tbody>
669 </table>
670 {/* Split table (hidden until toggled) */}
671 <table class="diff-table diff-table-split" hidden>
672 <tbody>
673 {splitRows.map((row) => {
674 const isHunk = row.left?.type === "hunk" || row.right?.type === "hunk";
675 if (isHunk) {
676 const hunk = row.left ?? row.right;
677 return (
678 <tr class="diff-row diff-row-hunk">
679 <td class="diff-ln diff-ln-old"></td>
680 <td class="diff-cell">{hunk?.content ?? ""}</td>
681 <td class="diff-ln diff-ln-new"></td>
682 <td class="diff-cell"></td>
683 </tr>
684 );
685 }
686 const isCtx = row.left?.type === "ctx";
687 const L = row.left;
688 const R = row.right;
689 const leftCls = `diff-cell${L ? (isCtx ? " diff-cell-ctx" : " diff-cell-del") : " diff-cell-empty"}${L?.wsOnly ? " diff-cell-ws" : ""}`;
690 const rightCls = `diff-cell${R ? (isCtx ? " diff-cell-ctx" : " diff-cell-add") : " diff-cell-empty"}${R?.wsOnly ? " diff-cell-ws" : ""}`;
691 return (
692 <tr class="diff-row diff-split-row">
693 <td class="diff-ln diff-ln-old">{L?.oldLine ?? ""}</td>
694 <td class={leftCls}>
695 {L && <span class="diff-sigil">{isCtx ? " " : "-"}</span>}
696 {L?.content ?? ""}
697 {L?.wsOnly && <span class="diff-ws-badge" title="whitespace-only change">ws</span>}
698 </td>
699 <td class="diff-ln diff-ln-new">{R?.newLine ?? ""}</td>
700 <td class={rightCls}>
701 {R && <span class="diff-sigil">{isCtx ? " " : "+"}</span>}
702 {isCtx ? L?.content ?? "" : R?.content ?? ""}
703 {R?.wsOnly && !isCtx && <span class="diff-ws-badge" title="whitespace-only change">ws</span>}
704 </td>
705 </tr>
706 );
707 })}
708 </tbody>
709 </table>
710 </>
711 )}
712 </div>
713 </div>
714 );
715 })}
fc1817aClaude716 </div>
717 );
718};
719
06d5ffeClaude720export const RepoCard: FC<{ repo: Repository; ownerName: string }> = ({
721 repo,
722 ownerName,
723}) => (
724 <div class="card">
725 <h3>
726 <a href={`/${ownerName}/${repo.name}`}>{repo.name}</a>
727 </h3>
728 {repo.description && <p>{repo.description}</p>}
729 <div class="card-meta">
730 {repo.isPrivate && <span class="badge">Private</span>}
731 <span>{"\u2606"} {repo.starCount}</span>
732 {repo.pushedAt && (
733 <span>Updated {formatRelativeDate(repo.pushedAt.toString())}</span>
734 )}
735 </div>
736 </div>
737);
738
fc1817aClaude739function formatSize(bytes: number): string {
740 if (bytes < 1024) return `${bytes} B`;
741 if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
742 return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
743}
744
745function formatRelativeDate(dateStr: string): string {
746 const date = new Date(dateStr);
747 const now = new Date();
748 const diffMs = now.getTime() - date.getTime();
749 const diffMins = Math.floor(diffMs / 60000);
750 if (diffMins < 1) return "just now";
751 if (diffMins < 60) return `${diffMins} minute${diffMins > 1 ? "s" : ""} ago`;
752 const diffHours = Math.floor(diffMins / 60);
753 if (diffHours < 24)
754 return `${diffHours} hour${diffHours > 1 ? "s" : ""} ago`;
755 const diffDays = Math.floor(diffHours / 24);
756 if (diffDays < 30) return `${diffDays} day${diffDays > 1 ? "s" : ""} ago`;
757 return date.toLocaleDateString("en-US", {
758 month: "short",
759 day: "numeric",
760 year: "numeric",
761 });
762}
6682dbeClaude763
764// ---------------------------------------------------------------------------
765// CiFailureProfileCard — Phase 2 Human-Agent Canvas
766// ---------------------------------------------------------------------------
767
768export interface CiTraceIteration {
769 iteration: number;
770 modelRoute: string;
771 errorDelta: string;
772 tokenCost: number;
773}
774
775export interface CiFailureProfileProps {
776 gateRunId: string;
777 failureTarget: string;
778 errorDelta: string;
779 mitigationHint: string;
780 traceLog: CiTraceIteration[];
781}
782
783export const CiFailureProfileCard: FC<CiFailureProfileProps> = ({
784 gateRunId,
785 failureTarget,
786 errorDelta,
787 mitigationHint,
788 traceLog,
789}) => (
790 <div class="ci-failure-profile-card">
791 <style>{`
792 .ci-failure-profile-card {
793 background: #1a1a2e;
794 border: 1px solid #2d2d4a;
795 border-radius: 8px;
796 padding: 16px;
797 margin: 12px 0;
798 font-family: monospace;
799 color: #e0e0ff;
800 }
801 .ci-failure-profile-card h3 {
802 margin: 0 0 12px;
803 color: #ff6b6b;
804 font-size: 14px;
805 text-transform: uppercase;
806 letter-spacing: 0.5px;
807 }
808 .ci-failure-profile-card .field-label {
809 color: #888;
810 font-size: 11px;
811 text-transform: uppercase;
812 margin-top: 10px;
813 }
814 .ci-failure-profile-card .field-value {
815 color: #e0e0ff;
816 font-size: 13px;
817 margin: 2px 0 6px;
818 white-space: pre-wrap;
819 word-break: break-word;
820 }
821 .ci-failure-profile-card .trace-row {
822 display: grid;
823 grid-template-columns: 40px 1fr 80px;
824 gap: 8px;
825 padding: 4px 0;
826 border-bottom: 1px solid #2d2d4a;
827 font-size: 12px;
828 }
829 .ci-failure-profile-card .feedback-row {
830 margin-top: 14px;
831 display: flex;
832 gap: 8px;
833 }
834 .ci-failure-profile-card .btn-feedback {
835 padding: 6px 12px;
836 border: none;
837 border-radius: 6px;
838 cursor: pointer;
839 font-size: 12px;
840 font-weight: bold;
841 }
842 .ci-failure-profile-card .btn-helpful {
843 background: #1a6b3c;
844 color: #7fff9e;
845 }
846 .ci-failure-profile-card .btn-hallucination {
847 background: #6b1a1a;
848 color: #ff9e9e;
849 }
850 `}</style>
851 <h3>🔍 CI Failure Profile</h3>
852 <div class="field-label">Failure Target</div>
853 <div class="field-value">{failureTarget}</div>
854 <div class="field-label">Error Delta</div>
855 <div class="field-value">{errorDelta}</div>
856 <div class="field-label">Mitigation Hint</div>
857 <div class="field-value">{mitigationHint}</div>
858 {traceLog.length > 0 && (
859 <>
860 <div class="field-label" style="margin-top:12px">Model Trace</div>
861 {traceLog.map((t) => (
862 <div class="trace-row" key={t.iteration}>
863 <span>#{t.iteration}</span>
864 <span>{t.modelRoute}</span>
865 <span>{t.tokenCost}¢</span>
866 </div>
867 ))}
868 </>
869 )}
870 <div class="feedback-row">
871 <form method="post" action="/api/flywheel-telemetry/feedback" style="display:inline">
872 <input type="hidden" name="gateRunId" value={gateRunId} />
873 <input type="hidden" name="verdict" value="helpful" />
874 <button type="submit" class="btn-feedback btn-helpful">
875 👍 Diagnostic Helpful
876 </button>
877 </form>
878 <form method="post" action="/api/flywheel-telemetry/feedback" style="display:inline">
879 <input type="hidden" name="gateRunId" value={gateRunId} />
880 <input type="hidden" name="verdict" value="hallucination" />
881 <button type="submit" class="btn-feedback btn-hallucination">
882 👎 Hallucination Flagged
883 </button>
884 </form>
885 </div>
886 </div>
887);