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.tsxBlame884 lines · 2 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"
180 | "traffic";
181 /** Current authenticated user — used for owner-only tab gating. */
182 currentUser?: string | null;
183 /** Repo owner username — used for owner-only tab gating. */
184 repoOwner?: string;
185}> = ({ owner, repo, active, currentUser, repoOwner }) => (
fc1817aClaude186 <div class="repo-nav">
06d5ffeClaude187 <a href={`/${owner}/${repo}`} class={active === "code" ? "active" : ""}>
fc1817aClaude188 Code
189 </a>
79136bbClaude190 <a
191 href={`/${owner}/${repo}/issues`}
192 class={active === "issues" ? "active" : ""}
193 >
194 Issues
195 </a>
c645a86Claude196 <a
197 href={`/${owner}/${repo}/discussions`}
198 class={active === "discussions" ? "active" : ""}
199 >
200 Discussions
201 </a>
1e162a8Claude202 <a
203 href={`/${owner}/${repo}/wiki`}
204 class={active === "wiki" ? "active" : ""}
205 >
206 Wiki
207 </a>
0074234Claude208 <a
209 href={`/${owner}/${repo}/pulls`}
210 class={active === "pulls" ? "active" : ""}
211 >
212 Pull Requests
213 </a>
1e162a8Claude214 <a
215 href={`/${owner}/${repo}/projects`}
216 class={active === "projects" ? "active" : ""}
217 >
218 Projects
219 </a>
fc1817aClaude220 <a
221 href={`/${owner}/${repo}/commits`}
222 class={active === "commits" ? "active" : ""}
223 >
224 Commits
225 </a>
eafe8c6Claude226 <a
227 href={`/${owner}/${repo}/actions`}
228 class={active === "actions" ? "active" : ""}
229 >
230 Actions
231 </a>
3ef4c9dClaude232 <a
233 href={`/${owner}/${repo}/releases`}
234 class={active === "releases" ? "active" : ""}
235 >
236 Releases
237 </a>
0224546Claude238 <a
239 href={`/${owner}/${repo}/contributors`}
240 class={active === "contributors" ? "active" : ""}
241 >
242 Contributors
243 </a>
244 <a
245 href={`/${owner}/${repo}/pulse`}
246 class={active === "pulse" ? "active" : ""}
247 >
248 Pulse
249 </a>
250 {currentUser && repoOwner && currentUser === repoOwner && (
251 <a
252 href={`/${owner}/${repo}/traffic`}
253 class={active === "traffic" ? "active" : ""}
254 >
255 Traffic
256 </a>
257 )}
3ef4c9dClaude258 <a
259 href={`/${owner}/${repo}/gates`}
260 class={active === "gates" ? "active" : ""}
261 >
262 {"\u25CF"} Gates
263 </a>
da3fc18Claude264 <a
265 href={`/${owner}/${repo}/security/vulnerabilities`}
266 class={active === "security" ? "active" : ""}
267 >
268 Security
269 </a>
ae2a071ccanty labs270 <a
271 href={`/${owner}/${repo}/settings`}
272 class={active === "settings" ? "active" : ""}
273 >
274 Settings
275 </a>
c9ed210Claude276 <a
277 href={`/${owner}/${repo}/cloud-deployments`}
278 class={active === "deployments" ? "active" : ""}
279 >
280 Deployments
281 </a>
2c61840Claude282 <a
283 href={`/${owner}/${repo}/pipeline`}
284 class={active === "pipeline" ? "active" : ""}
285 >
286 Pipeline
287 </a>
3ef4c9dClaude288 <a
289 href={`/${owner}/${repo}/insights`}
290 class={active === "insights" ? "active" : ""}
291 >
292 Insights
293 </a>
ef3fd93Claude294 <a
295 href={`/${owner}/${repo}/agents`}
296 class={active === "agents" ? "active" : ""}
297 >
298 Agents
299 </a>
3cbe3d6Claude300 <a
301 href={`/${owner}/${repo}/explain`}
debcf27Claude302 class={`repo-nav-ai${active === "explain" ? " active" : ""}`}
303 style="margin-left: auto"
3cbe3d6Claude304 >
305 {"\u2728"} Explain
306 </a>
debcf27Claude307 <a href={`/${owner}/${repo}/ask`} class="repo-nav-ai">
3ef4c9dClaude308 {"\u2728"} Ask AI
309 </a>
2c61840Claude310 <a
311 href={`/${owner}/${repo}/workspace`}
312 class={`repo-nav-ai${active === "workspace" ? " active" : ""}`}
313 title="AI Workspace — spec-to-PR, fix issues with AI, web editor"
314 >
315 {"✨"} Workspace
316 </a>
14c3cc8Claude317 <a
318 href={`/${owner}/${repo}/spec`}
debcf27Claude319 class="repo-nav-ai"
14c3cc8Claude320 title="Spec to PR — paste a feature spec, AI opens a draft PR"
321 >
322 {"\u2728"} Spec
323 </a>
d8ef5efClaude324 <a
325 href={`/${owner}/${repo}/ai/tests`}
debcf27Claude326 class="repo-nav-ai"
d8ef5efClaude327 title="AI Tests \u2014 generate failing test stubs from a source file"
328 >
329 {"\u2728"} Tests
330 </a>
bcc4020Claude331 <a
332 href={`/${owner}/${repo}/debt-map`}
333 class={`repo-nav-ai${active === "debt-map" ? " active" : ""}`}
334 title="AI Debt Map \u2014 visual technical debt graph with Claude analysis"
335 >
336 {"\u2593"} Debt Map
337 </a>
77cf834Claude338 <a
339 href={`/${owner}/${repo}/search/nl`}
340 class={`repo-nav-ai${active === "nl-search" ? " active" : ""}`}
341 title="Natural Language Search \u2014 search by intent, not keywords"
342 >
343 {"\u2728"} NL Search
344 </a>
b1070a5Claude345 <a
346 href={`/${owner}/${repo}/archaeology`}
347 class={`repo-nav-ai${active === "archaeology" ? " active" : ""}`}
348 title="AI Archaeology \u2014 excavate why any file exists using git history, PRs, and issues"
349 >
350 {"\ud83c\udfdb"} Archaeology
351 </a>
fc1817aClaude352 </div>
353);
354
06d5ffeClaude355export const BranchSwitcher: FC<{
356 owner: string;
357 repo: string;
358 currentRef: string;
359 branches: string[];
360 pathType: "tree" | "blob" | "commits";
361 subPath?: string;
362}> = ({ owner, repo, currentRef, branches, pathType, subPath }) => {
363 if (branches.length <= 1) {
364 return <div class="branch-selector">{currentRef}</div>;
365 }
366
367 return (
368 <div class="branch-dropdown">
369 <button class="branch-selector" type="button">
370 {currentRef} &#9662;
371 </button>
372 <div class="branch-dropdown-content">
373 {branches.map((branch) => {
374 let href: string;
375 if (pathType === "commits") {
376 href = `/${owner}/${repo}/commits/${branch}`;
377 } else if (subPath) {
378 href = `/${owner}/${repo}/${pathType}/${branch}/${subPath}`;
379 } else {
380 href = `/${owner}/${repo}/tree/${branch}`;
381 }
382 return (
383 <a
384 href={href}
385 class={branch === currentRef ? "active-branch" : ""}
386 >
387 {branch}
388 </a>
389 );
390 })}
391 </div>
392 </div>
393 );
394};
395
fc1817aClaude396export const Breadcrumb: FC<{
397 owner: string;
398 repo: string;
399 ref: string;
400 path: string;
401}> = ({ owner, repo, ref, path }) => {
402 const parts = path.split("/").filter(Boolean);
403 const crumbs: { name: string; href: string }[] = [
404 { name: repo, href: `/${owner}/${repo}/tree/${ref}` },
405 ];
406 let accumulated = "";
407 for (const part of parts) {
408 accumulated += (accumulated ? "/" : "") + part;
409 crumbs.push({
410 name: part,
411 href: `/${owner}/${repo}/tree/${ref}/${accumulated}`,
412 });
413 }
414 return (
415 <div class="breadcrumb">
416 {crumbs.map((crumb, i) => (
417 <>
418 {i > 0 && <span>/</span>}
419 {i === crumbs.length - 1 ? (
420 <strong>{crumb.name}</strong>
421 ) : (
422 <a href={crumb.href}>{crumb.name}</a>
423 )}
424 </>
425 ))}
426 </div>
427 );
428};
429
430export const FileTable: FC<{
431 entries: GitTreeEntry[];
432 owner: string;
433 repo: string;
434 ref: string;
435 path: string;
436}> = ({ entries, owner, repo, ref, path }) => (
437 <table class="file-table">
438 <tbody>
439 {entries.map((entry) => {
440 const fullPath = path ? `${path}/${entry.name}` : entry.name;
441 const href =
442 entry.type === "tree"
443 ? `/${owner}/${repo}/tree/${ref}/${fullPath}`
444 : `/${owner}/${repo}/blob/${ref}/${fullPath}`;
445 return (
446 <tr>
447 <td class="file-icon">
448 {entry.type === "tree" ? "\u{1F4C1}" : "\u{1F4C4}"}
449 </td>
450 <td class="file-name">
451 <a href={href}>{entry.name}</a>
452 </td>
453 <td style="text-align: right; color: var(--text-muted); font-size: 13px;">
454 {entry.size !== undefined ? formatSize(entry.size) : ""}
455 </td>
456 </tr>
457 );
458 })}
459 </tbody>
460 </table>
461);
462
06d5ffeClaude463export const HighlightedCode: FC<{
464 highlightedHtml: string;
465 lineCount: number;
466}> = ({ highlightedHtml, lineCount }) => {
467 const lineNums = Array.from({ length: lineCount }, (_, i) => i + 1);
468 return (
469 <div class="blob-code">
470 <table>
471 <tbody>
472 <tr>
473 <td class="line-num" style="vertical-align: top; padding-top: 0; padding-bottom: 0">
474 <pre style="margin: 0; line-height: 1.6; font-size: 13px">
475 {lineNums.map((n) => (
476 <>
477 <span>{n}</span>
478 {"\n"}
479 </>
480 ))}
481 </pre>
482 </td>
483 <td class="line-content" style="vertical-align: top; padding-top: 0; padding-bottom: 0">
484 <pre style="margin: 0; line-height: 1.6; font-size: 13px">{html([highlightedHtml] as unknown as TemplateStringsArray)}</pre>
485 </td>
486 </tr>
487 </tbody>
488 </table>
489 </div>
490 );
491};
492
493export const PlainCode: FC<{ lines: string[] }> = ({ lines }) => (
494 <div class="blob-code">
495 <table>
496 <tbody>
497 {lines.map((line, i) => (
498 <tr>
499 <td class="line-num">{i + 1}</td>
500 <td class="line-content">{line}</td>
501 </tr>
502 ))}
503 </tbody>
504 </table>
505 </div>
506);
507
fc1817aClaude508export const CommitList: FC<{
509 commits: GitCommit[];
510 owner: string;
511 repo: string;
3951454Claude512 verifications?: Record<string, { verified: boolean; reason: string }>;
513}> = ({ commits, owner, repo, verifications }) => (
fc1817aClaude514 <div class="commit-list">
3951454Claude515 {commits.map((commit) => {
516 const v = verifications?.[commit.sha];
517 return (
518 <div class="commit-item">
519 <div>
520 <div class="commit-message">
521 <a href={`/${owner}/${repo}/commit/${commit.sha}`}>
522 {commit.message}
523 </a>
524 {v?.verified && (
ae2a071ccanty labs525 <a
526 href="/settings/signing-keys"
527 title="Signed with a registered key — manage your signing keys"
528 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"
3951454Claude529 >
530 Verified
ae2a071ccanty labs531 </a>
3951454Claude532 )}
533 </div>
534 <div class="commit-meta">
535 {commit.author} committed {formatRelativeDate(commit.date)}
536 </div>
fc1817aClaude537 </div>
3951454Claude538 <a
539 href={`/${owner}/${repo}/commit/${commit.sha}`}
540 class="commit-sha"
541 >
542 {commit.sha.slice(0, 7)}
543 </a>
fc1817aClaude544 </div>
3951454Claude545 );
546 })}
fc1817aClaude547 </div>
548);
549
82c3a38ccanty labs550// ---- Diff viewer ----------------------------------------------------------
551
552const _diffScript = `
553function diffToggleView(id,mode){
554 var v=document.getElementById(id);if(!v)return;
555 v.querySelectorAll('.diff-table-inline').forEach(function(t){t.hidden=(mode==='split');});
556 v.querySelectorAll('.diff-table-split').forEach(function(t){t.hidden=(mode!=='split');});
557 v.querySelectorAll('.diff-view-btn').forEach(function(b){b.classList.remove('active');});
558 var btn=v.querySelector('.diff-view-btn-'+mode);if(btn)btn.classList.add('active');
559}
560function diffToggleFile(id){
561 var body=document.getElementById(id);if(!body)return;
562 body.hidden=!body.hidden;
563 var hdr=body.previousElementSibling;if(hdr)hdr.classList.toggle('collapsed',body.hidden);
564}
565`;
566
fc1817aClaude567export const DiffView: FC<{ raw: string; files: GitDiffFile[] }> = ({
568 raw,
569 files,
570}) => {
82c3a38ccanty labs571 const parsed = parseDiff(raw);
572 const totalAdd = files.reduce((s, f) => s + f.additions, 0);
573 const totalDel = files.reduce((s, f) => s + f.deletions, 0);
574
575 if (parsed.length === 0 && files.length === 0) {
576 return (
577 <div class="diff-view">
578 <p style="color:var(--text-muted);font-size:14px">No changes.</p>
579 </div>
580 );
581 }
fc1817aClaude582
583 return (
82c3a38ccanty labs584 <div class="diff-view" id="diff-view-main">
585 <script dangerouslySetInnerHTML={{ __html: _diffScript }} />
586
587 {/* Toolbar */}
588 <div class="diff-toolbar">
589 <div class="diff-summary">
590 <strong>{files.length}</strong> file{files.length !== 1 ? "s" : ""} changed,{" "}
591 <span class="stat-add">+{totalAdd}</span>{" "}
592 <span class="stat-del">-{totalDel}</span>
593 </div>
594 <div class="diff-view-toggle">
595 <button class="diff-view-btn diff-view-btn-inline active" onclick="diffToggleView('diff-view-main','inline')" type="button">Inline</button>
596 <button class="diff-view-btn diff-view-btn-split" onclick="diffToggleView('diff-view-main','split')" type="button">Split</button>
597 </div>
fc1817aClaude598 </div>
82c3a38ccanty labs599
600 {/* File jump list (shown when > 1 file) */}
601 {parsed.length > 1 && (
602 <div class="diff-jump-list">
603 {parsed.map((f, i) => {
604 const isAdd = f.additions > 0 && f.deletions === 0;
605 const isDel = f.additions === 0 && f.deletions > 0;
606 return (
607 <a class="diff-jump-item" href={`#diff-file-${i}`}>
608 <span class={isAdd ? "stat-add" : isDel ? "stat-del" : "diff-jump-mod"}>
609 {isAdd ? "+" : isDel ? "−" : "~"}
610 </span>{" "}
611 {f.path.split("/").pop() || f.path}
612 </a>
613 );
614 })}
fc1817aClaude615 </div>
82c3a38ccanty labs616 )}
617
618 {/* Per-file blocks */}
619 {parsed.map((file, idx) => {
620 const bodyId = `diff-body-${idx}`;
621 const splitRows = pairLines(file.lines);
622 return (
623 <div class="diff-file" id={`diff-file-${idx}`}>
624 <div class="diff-file-header" onclick={`diffToggleFile('${bodyId}')`}>
625 <span class="diff-file-path" title={file.path}>{file.path}</span>
626 <span class="diff-file-meta">
627 {file.isBinary
628 ? <span style="color:var(--text-muted)">binary</span>
629 : <><span class="stat-add">+{file.additions}</span>{" "}<span class="stat-del">-{file.deletions}</span></>
630 }
631 <span class="diff-file-chevron">▾</span>
632 </span>
633 </div>
634 <div id={bodyId}>
635 {file.isBinary ? (
636 <div class="diff-binary">Binary file changed</div>
637 ) : (
638 <>
639 {/* Inline table (default) */}
640 <table class="diff-table diff-table-inline">
641 <tbody>
642 {file.lines.map((line) => {
643 if (line.type === "hunk") {
644 return (
645 <tr class="diff-row diff-row-hunk">
646 <td class="diff-ln diff-ln-old"></td>
647 <td class="diff-ln diff-ln-new"></td>
648 <td class="diff-cell">{line.content}</td>
649 </tr>
650 );
651 }
652 const sigil = line.type === "add" ? "+" : line.type === "del" ? "-" : " ";
653 return (
654 <tr class={`diff-row diff-row-${line.type}${line.wsOnly ? " diff-row-ws" : ""}`}>
655 <td class="diff-ln diff-ln-old">{line.oldLine ?? ""}</td>
656 <td class="diff-ln diff-ln-new">{line.newLine ?? ""}</td>
657 <td class="diff-cell">
658 <span class="diff-sigil">{sigil}</span>
659 {line.content}
660 {line.wsOnly && <span class="diff-ws-badge" title="whitespace-only change">ws</span>}
661 </td>
662 </tr>
663 );
664 })}
665 </tbody>
666 </table>
667 {/* Split table (hidden until toggled) */}
668 <table class="diff-table diff-table-split" hidden>
669 <tbody>
670 {splitRows.map((row) => {
671 const isHunk = row.left?.type === "hunk" || row.right?.type === "hunk";
672 if (isHunk) {
673 const hunk = row.left ?? row.right;
674 return (
675 <tr class="diff-row diff-row-hunk">
676 <td class="diff-ln diff-ln-old"></td>
677 <td class="diff-cell">{hunk?.content ?? ""}</td>
678 <td class="diff-ln diff-ln-new"></td>
679 <td class="diff-cell"></td>
680 </tr>
681 );
682 }
683 const isCtx = row.left?.type === "ctx";
684 const L = row.left;
685 const R = row.right;
686 const leftCls = `diff-cell${L ? (isCtx ? " diff-cell-ctx" : " diff-cell-del") : " diff-cell-empty"}${L?.wsOnly ? " diff-cell-ws" : ""}`;
687 const rightCls = `diff-cell${R ? (isCtx ? " diff-cell-ctx" : " diff-cell-add") : " diff-cell-empty"}${R?.wsOnly ? " diff-cell-ws" : ""}`;
688 return (
689 <tr class="diff-row diff-split-row">
690 <td class="diff-ln diff-ln-old">{L?.oldLine ?? ""}</td>
691 <td class={leftCls}>
692 {L && <span class="diff-sigil">{isCtx ? " " : "-"}</span>}
693 {L?.content ?? ""}
694 {L?.wsOnly && <span class="diff-ws-badge" title="whitespace-only change">ws</span>}
695 </td>
696 <td class="diff-ln diff-ln-new">{R?.newLine ?? ""}</td>
697 <td class={rightCls}>
698 {R && <span class="diff-sigil">{isCtx ? " " : "+"}</span>}
699 {isCtx ? L?.content ?? "" : R?.content ?? ""}
700 {R?.wsOnly && !isCtx && <span class="diff-ws-badge" title="whitespace-only change">ws</span>}
701 </td>
702 </tr>
703 );
704 })}
705 </tbody>
706 </table>
707 </>
708 )}
709 </div>
710 </div>
711 );
712 })}
fc1817aClaude713 </div>
714 );
715};
716
06d5ffeClaude717export const RepoCard: FC<{ repo: Repository; ownerName: string }> = ({
718 repo,
719 ownerName,
720}) => (
721 <div class="card">
722 <h3>
723 <a href={`/${ownerName}/${repo.name}`}>{repo.name}</a>
724 </h3>
725 {repo.description && <p>{repo.description}</p>}
726 <div class="card-meta">
727 {repo.isPrivate && <span class="badge">Private</span>}
728 <span>{"\u2606"} {repo.starCount}</span>
729 {repo.pushedAt && (
730 <span>Updated {formatRelativeDate(repo.pushedAt.toString())}</span>
731 )}
732 </div>
733 </div>
734);
735
fc1817aClaude736function formatSize(bytes: number): string {
737 if (bytes < 1024) return `${bytes} B`;
738 if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
739 return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
740}
741
742function formatRelativeDate(dateStr: string): string {
743 const date = new Date(dateStr);
744 const now = new Date();
745 const diffMs = now.getTime() - date.getTime();
746 const diffMins = Math.floor(diffMs / 60000);
747 if (diffMins < 1) return "just now";
748 if (diffMins < 60) return `${diffMins} minute${diffMins > 1 ? "s" : ""} ago`;
749 const diffHours = Math.floor(diffMins / 60);
750 if (diffHours < 24)
751 return `${diffHours} hour${diffHours > 1 ? "s" : ""} ago`;
752 const diffDays = Math.floor(diffHours / 24);
753 if (diffDays < 30) return `${diffDays} day${diffDays > 1 ? "s" : ""} ago`;
754 return date.toLocaleDateString("en-US", {
755 month: "short",
756 day: "numeric",
757 year: "numeric",
758 });
759}
6682dbeClaude760
761// ---------------------------------------------------------------------------
762// CiFailureProfileCard — Phase 2 Human-Agent Canvas
763// ---------------------------------------------------------------------------
764
765export interface CiTraceIteration {
766 iteration: number;
767 modelRoute: string;
768 errorDelta: string;
769 tokenCost: number;
770}
771
772export interface CiFailureProfileProps {
773 gateRunId: string;
774 failureTarget: string;
775 errorDelta: string;
776 mitigationHint: string;
777 traceLog: CiTraceIteration[];
778}
779
780export const CiFailureProfileCard: FC<CiFailureProfileProps> = ({
781 gateRunId,
782 failureTarget,
783 errorDelta,
784 mitigationHint,
785 traceLog,
786}) => (
787 <div class="ci-failure-profile-card">
788 <style>{`
789 .ci-failure-profile-card {
790 background: #1a1a2e;
791 border: 1px solid #2d2d4a;
792 border-radius: 8px;
793 padding: 16px;
794 margin: 12px 0;
795 font-family: monospace;
796 color: #e0e0ff;
797 }
798 .ci-failure-profile-card h3 {
799 margin: 0 0 12px;
800 color: #ff6b6b;
801 font-size: 14px;
802 text-transform: uppercase;
803 letter-spacing: 0.5px;
804 }
805 .ci-failure-profile-card .field-label {
806 color: #888;
807 font-size: 11px;
808 text-transform: uppercase;
809 margin-top: 10px;
810 }
811 .ci-failure-profile-card .field-value {
812 color: #e0e0ff;
813 font-size: 13px;
814 margin: 2px 0 6px;
815 white-space: pre-wrap;
816 word-break: break-word;
817 }
818 .ci-failure-profile-card .trace-row {
819 display: grid;
820 grid-template-columns: 40px 1fr 80px;
821 gap: 8px;
822 padding: 4px 0;
823 border-bottom: 1px solid #2d2d4a;
824 font-size: 12px;
825 }
826 .ci-failure-profile-card .feedback-row {
827 margin-top: 14px;
828 display: flex;
829 gap: 8px;
830 }
831 .ci-failure-profile-card .btn-feedback {
832 padding: 6px 12px;
833 border: none;
834 border-radius: 6px;
835 cursor: pointer;
836 font-size: 12px;
837 font-weight: bold;
838 }
839 .ci-failure-profile-card .btn-helpful {
840 background: #1a6b3c;
841 color: #7fff9e;
842 }
843 .ci-failure-profile-card .btn-hallucination {
844 background: #6b1a1a;
845 color: #ff9e9e;
846 }
847 `}</style>
848 <h3>🔍 CI Failure Profile</h3>
849 <div class="field-label">Failure Target</div>
850 <div class="field-value">{failureTarget}</div>
851 <div class="field-label">Error Delta</div>
852 <div class="field-value">{errorDelta}</div>
853 <div class="field-label">Mitigation Hint</div>
854 <div class="field-value">{mitigationHint}</div>
855 {traceLog.length > 0 && (
856 <>
857 <div class="field-label" style="margin-top:12px">Model Trace</div>
858 {traceLog.map((t) => (
859 <div class="trace-row" key={t.iteration}>
860 <span>#{t.iteration}</span>
861 <span>{t.modelRoute}</span>
862 <span>{t.tokenCost}¢</span>
863 </div>
864 ))}
865 </>
866 )}
867 <div class="feedback-row">
868 <form method="post" action="/api/flywheel-telemetry/feedback" style="display:inline">
869 <input type="hidden" name="gateRunId" value={gateRunId} />
870 <input type="hidden" name="verdict" value="helpful" />
871 <button type="submit" class="btn-feedback btn-helpful">
872 👍 Diagnostic Helpful
873 </button>
874 </form>
875 <form method="post" action="/api/flywheel-telemetry/feedback" style="display:inline">
876 <input type="hidden" name="gateRunId" value={gateRunId} />
877 <input type="hidden" name="verdict" value="hallucination" />
878 <button type="submit" class="btn-feedback btn-hallucination">
879 👎 Hallucination Flagged
880 </button>
881 </form>
882 </div>
883 </div>
884);