From ffbffa96aaf7e59bcca73e53fb86f060c14cc866 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Sat, 25 Apr 2026 07:36:33 -0400 Subject: [PATCH] fix(run-files): align diff stats with visible files Track numstat by path so sensitive, binary, symlink, and submodule entries do not inflate aggregate line counts, and clean generated client whitespace churn from the API update. --- apps/fabro-web/app/routes/run-files.tsx | 8 +- .../app/routes/run-files/toolbar.tsx | 18 +- lib/crates/fabro-server/src/run_files.rs | 189 +++++++++++++++--- .../{entry-7q292616.js => entry-grs8nqaw.js} | 2 +- lib/crates/fabro-spa/assets/index.html | 2 +- lib/crates/fabro-workflow/src/sandbox_git.rs | 44 ++-- .../fabro-api-client/src/api/runs-api.ts | 106 +++++----- .../fabro-api-client/src/models/diff-stats.ts | 2 +- .../src/models/fork-request.ts | 3 +- .../src/models/fork-response.ts | 3 +- .../src/models/install-object-store-input.ts | 4 +- .../models/install-object-store-summary.ts | 4 +- .../src/models/install-prefill.ts | 3 +- .../src/models/rewind-request.ts | 3 +- .../src/models/rewind-response.ts | 3 +- .../src/models/run-superseded-by-props.ts | 3 +- .../src/models/timeline-entry-response.ts | 3 +- 17 files changed, 268 insertions(+), 132 deletions(-) rename lib/crates/fabro-spa/assets/assets/{entry-7q292616.js => entry-grs8nqaw.js} (98%) diff --git a/apps/fabro-web/app/routes/run-files.tsx b/apps/fabro-web/app/routes/run-files.tsx index 93691e594..5361d01b5 100644 --- a/apps/fabro-web/app/routes/run-files.tsx +++ b/apps/fabro-web/app/routes/run-files.tsx @@ -521,9 +521,11 @@ export default function RunFiles({ loaderData }: any) { const toolbar = ( revalidator.revalidate()} refreshing={isRevalidating} refreshDisabled={refreshDisabled} diff --git a/apps/fabro-web/app/routes/run-files/toolbar.tsx b/apps/fabro-web/app/routes/run-files/toolbar.tsx index b6f4ac8e5..282616eb5 100644 --- a/apps/fabro-web/app/routes/run-files/toolbar.tsx +++ b/apps/fabro-web/app/routes/run-files/toolbar.tsx @@ -8,10 +8,14 @@ import { ArrowPathIcon } from "@heroicons/react/20/solid"; */ export type DiffStyle = "split" | "unified"; +type ChangeSummary = { + totalChanged: number; + additions: number; + deletions: number; +}; + export function Toolbar({ - totalChanged, - additions, - deletions, + changeSummary, onRefresh, refreshing, refreshDisabled, @@ -21,12 +25,7 @@ export function Toolbar({ onDiffStyleChange, diffStyleForced, }: { - /** From `meta.total_changed`. May exceed the rendered file list when truncated. */ - totalChanged: number; - /** From `meta.stats.additions`. Aggregate `+` line count across the diff. */ - additions: number; - /** From `meta.stats.deletions`. Aggregate `-` line count across the diff. */ - deletions: number; + changeSummary: ChangeSummary; onRefresh: () => void; refreshing: boolean; /** True when the server has nothing new to show (to_sha unchanged). */ @@ -43,6 +42,7 @@ export function Toolbar({ */ diffStyleForced: boolean; }) { + const { totalChanged, additions, deletions } = changeSummary; const refreshTitle = refreshing ? "Refreshing" : refreshDisabled diff --git a/lib/crates/fabro-server/src/run_files.rs b/lib/crates/fabro-server/src/run_files.rs index 1bb5c1c7e..3e9aa8486 100644 --- a/lib/crates/fabro-server/src/run_files.rs +++ b/lib/crates/fabro-server/src/run_files.rs @@ -34,8 +34,8 @@ use fabro_sandbox::reconnect::reconnect; use fabro_static::EnvVars; use fabro_types::RunId; use fabro_workflow::sandbox_git::{ - DiffError, DiffNumstat, RawDiffEntry, SubmoduleChange, SymlinkChange, list_changed_files_raw, - list_diff_numstat, stream_blob_metadata, stream_blobs, + DiffError, DiffLineStats, DiffNumstat, RawDiffEntry, SubmoduleChange, SymlinkChange, + list_changed_files_raw, list_diff_numstat, stream_blob_metadata, stream_blobs, }; use futures_util::FutureExt; use serde::Deserialize; @@ -297,18 +297,17 @@ async fn materialize_sandbox_path(state: &Arc, run_id: &RunId) -> List return Err(transient_503("git diff --numstat", &message)); } }; - let binary_paths = numstat.binary_paths.clone(); - let total_changed_before_cap = raw_entries.len(); // Classify every entry against the denylist + binary/symlink/submodule // flags FIRST so no-blob-needed placeholders don't consume cap slots // that belong to real file changes. - let classified = classify_entries(&raw_entries, &binary_paths, is_sensitive); + let classified = classify_entries(&raw_entries, &numstat, is_sensitive); + let stats = classified.stats; // Then cap the combined list at 200 entries. - let truncated_by_count = classified.len() > FILE_COUNT_CAP; - let mut classified = classified; + let truncated_by_count = classified.entries.len() > FILE_COUNT_CAP; + let mut classified = classified.entries; if truncated_by_count { classified.truncate(FILE_COUNT_CAP); } @@ -364,7 +363,7 @@ async fn materialize_sandbox_path(state: &Arc, run_id: &RunId) -> List files_omitted_by_budget: (files_omitted_by_budget > 0) .then(|| i64::try_from(files_omitted_by_budget).unwrap_or(i64::MAX)), total_changed: i64::try_from(total_changed_before_cap).unwrap_or(i64::MAX), - stats: numstat_to_stats(&numstat), + stats, to_sha: Some(to_sha_wrapper(&to_sha)), to_sha_committed_at, degraded: Some(false), @@ -374,10 +373,10 @@ async fn materialize_sandbox_path(state: &Arc, run_id: &RunId) -> List }) } -fn numstat_to_stats(n: &DiffNumstat) -> DiffStats { +fn line_stats_to_api(stats: DiffLineStats) -> DiffStats { DiffStats { - additions: i64::try_from(n.additions).unwrap_or(i64::MAX), - deletions: i64::try_from(n.deletions).unwrap_or(i64::MAX), + additions: i64::try_from(stats.additions).unwrap_or(i64::MAX), + deletions: i64::try_from(stats.deletions).unwrap_or(i64::MAX), } } @@ -452,22 +451,44 @@ fn build_fallback_response( /// summation we use on the live-sandbox path so the toolbar shows the same /// numbers regardless of which response branch we took. fn patch_to_stats(patch: &str) -> DiffStats { - let mut additions: u64 = 0; - let mut deletions: u64 = 0; - for line in patch.lines() { - if line.starts_with("+++") || line.starts_with("---") { + let mut stats = DiffLineStats::default(); + for section in patch.split("diff --git ").skip(1) { + if patch_section_omits_line_stats(section) { continue; } - match line.as_bytes().first() { - Some(b'+') => additions = additions.saturating_add(1), - Some(b'-') => deletions = deletions.saturating_add(1), - _ => {} + for line in section.lines() { + if line.starts_with("+++") || line.starts_with("---") { + continue; + } + match line.as_bytes().first() { + Some(b'+') => stats.additions = stats.additions.saturating_add(1), + Some(b'-') => stats.deletions = stats.deletions.saturating_add(1), + _ => {} + } } } - DiffStats { - additions: i64::try_from(additions).unwrap_or(i64::MAX), - deletions: i64::try_from(deletions).unwrap_or(i64::MAX), + line_stats_to_api(stats) +} + +fn patch_section_omits_line_stats(section: &str) -> bool { + section.lines().any(|line| { + patch_mode_line_matches(line, "120000") || patch_mode_line_matches(line, "160000") + }) +} + +fn patch_mode_line_matches(line: &str, mode: &str) -> bool { + let line = line.trim_end(); + if let Some(rest) = line + .strip_prefix("new file mode ") + .or_else(|| line.strip_prefix("deleted file mode ")) + .or_else(|| line.strip_prefix("old mode ")) + .or_else(|| line.strip_prefix("new mode ")) + { + return rest == mode; } + line.strip_prefix("index ") + .and_then(|rest| rest.split_whitespace().last()) + == Some(mode) } /// Truncate a patch at `cap_bytes` on a UTF-8 character boundary. Returns @@ -693,14 +714,22 @@ enum ClassifiedEntry { NeedsFetch(RawDiffEntry), } +struct ClassifiedEntries { + entries: Vec, + stats: DiffStats, +} + /// Classify every raw entry against the denylist + binary flags. Runs before -/// the 200-file cap so sensitive entries don't evict real changes. +/// the 200-file cap so sensitive entries don't evict real changes, and so +/// stats include every eligible text file even when the rendered list is +/// capped. fn classify_entries( raw: &[RawDiffEntry], - binary_paths: &HashSet, + numstat: &DiffNumstat, is_sensitive_fn: fn(&str) -> bool, -) -> Vec { +) -> ClassifiedEntries { let mut out = Vec::with_capacity(raw.len()); + let mut stats = DiffLineStats::default(); for entry in raw { let (new_path, old_path) = match entry { @@ -739,19 +768,26 @@ fn classify_entries( } // `git diff --numstat` reports the post-rename path on renames, // so checking `new_path` covers both non-rename and rename cases. - _ if binary_paths.contains(new_path) => { + _ if numstat.binary_paths.contains(new_path) => { out.push(ClassifiedEntry::Prebuilt(build_placeholder_file_diff( entry, &PlaceholderKind::Binary, ))); } _ => { + if let Some(line_stats) = numstat.line_stats_by_path.get(new_path) { + stats.additions = stats.additions.saturating_add(line_stats.additions); + stats.deletions = stats.deletions.saturating_add(line_stats.deletions); + } out.push(ClassifiedEntry::NeedsFetch(entry.clone())); } } } - out + ClassifiedEntries { + entries: out, + stats: line_stats_to_api(stats), + } } enum PlaceholderKind { @@ -1432,6 +1468,105 @@ diff --git a/src/bar.rs b/src/bar.rs assert_eq!(count_diff_headers(patch), 2); } + #[test] + fn patch_to_stats_counts_content_lines_only() { + let patch = "\ +diff --git a/src/a.rs b/src/a.rs +--- a/src/a.rs ++++ b/src/a.rs +@@ -1 +1,2 @@ +-old ++new ++extra +"; + let stats = patch_to_stats(patch); + assert_eq!(stats.additions, 2); + assert_eq!(stats.deletions, 1); + } + + #[test] + fn patch_to_stats_skips_symlink_and_submodule_sections() { + let patch = "\ +diff --git a/link b/link +new file mode 120000 +--- /dev/null ++++ b/link +@@ -0,0 +1 @@ ++target +diff --git a/vendor/lib b/vendor/lib +index 1111111..2222222 160000 +--- a/vendor/lib ++++ b/vendor/lib +@@ -1 +1 @@ +-Subproject commit 1111111 ++Subproject commit 2222222 +diff --git a/src/lib.rs b/src/lib.rs +--- a/src/lib.rs ++++ b/src/lib.rs +@@ -1 +1 @@ +-old ++new +"; + let stats = patch_to_stats(patch); + assert_eq!(stats.additions, 1); + assert_eq!(stats.deletions, 1); + } + + #[test] + fn classify_entries_sums_only_displayable_text_stats() { + let raw = vec![ + RawDiffEntry::Added { + path: "src/visible.rs".to_string(), + new_blob: "a1".to_string(), + new_mode: "100644".to_string(), + }, + RawDiffEntry::Modified { + path: ".env".to_string(), + old_blob: "b1".to_string(), + new_blob: "b2".to_string(), + new_mode: "100644".to_string(), + }, + RawDiffEntry::Modified { + path: "image.png".to_string(), + old_blob: "c1".to_string(), + new_blob: "c2".to_string(), + new_mode: "100644".to_string(), + }, + RawDiffEntry::Symlink { + path: "link".to_string(), + change_kind: SymlinkChange::Added, + old_blob: None, + new_blob: Some("d1".to_string()), + }, + RawDiffEntry::Submodule { + path: "vendor/lib".to_string(), + change_kind: SubmoduleChange::Modified, + }, + ]; + let mut numstat = DiffNumstat::default(); + for (path, additions, deletions) in [ + ("src/visible.rs", 5, 1), + (".env", 100, 100), + ("image.png", 200, 200), + ("link", 1, 0), + ("vendor/lib", 1, 1), + ] { + numstat + .line_stats_by_path + .insert(path.to_string(), DiffLineStats { + additions, + deletions, + }); + } + numstat.binary_paths.insert("image.png".to_string()); + + let classified = classify_entries(&raw, &numstat, is_sensitive); + + assert_eq!(classified.stats.additions, 5); + assert_eq!(classified.stats.deletions, 1); + assert_eq!(classified.entries.len(), raw.len()); + } + #[test] fn extract_diff_header_paths_pulls_both_sides_from_header() { assert_eq!( diff --git a/lib/crates/fabro-spa/assets/assets/entry-7q292616.js b/lib/crates/fabro-spa/assets/assets/entry-grs8nqaw.js similarity index 98% rename from lib/crates/fabro-spa/assets/assets/entry-7q292616.js rename to lib/crates/fabro-spa/assets/assets/entry-grs8nqaw.js index d6d481634..6052c5f2a 100644 --- a/lib/crates/fabro-spa/assets/assets/entry-7q292616.js +++ b/lib/crates/fabro-spa/assets/assets/entry-grs8nqaw.js @@ -2369,7 +2369,7 @@ Please report this to https://github.com/markedjs/marked.`,Z){let z="

An error review -> apply [label="Accept"] review -> propose [label="Revise", style=dashed] apply -> exit -}`}function EY0(Z){let Y=Z.querySelector(".graph > title");if(!Y)return;let Q=Y.nextElementSibling;while(Q&&Q.tagName==="text"){let z=Q.nextElementSibling;Q.remove(),Q=z}Y.remove()}function Fc({loaderData:Z}){let{id:Y}=B7(),{stages:Q,graphSvg:z}=Z,q=_7.useRef(null),B=_7.useRef(null),W=_7.useRef(null),[$,U]=_7.useState(null),[M,H]=_7.useState(XN),[O,_]=_7.useState("LR"),[A,P]=_7.useState({x:0,y:0}),L=_7.useRef(null),v=q8[M];_7.useEffect(()=>{let b=!1;async function S(){try{let I;if(z){let g=await fetch(`/api/v1/runs/${Y}/graph?direction=${O}`,{credentials:"include"});if(b)return;if(!g.ok){U("Failed to load graph");return}let l=await g.text(),X0=new DOMParser().parseFromString(l,"image/svg+xml").documentElement;if(!(X0 instanceof SVGSVGElement)){U("Invalid SVG from server");return}I=X0}else{let{instance:g}=await import("./chunk-a51kkeyv.js"),l=await g();if(b)return;I=l.renderSVGElement(bY0(O))}if(EY0(I),W.current=I,B.current)B.current.replaceChildren(I)}catch(I){U(I instanceof Error?I.message:"Failed to render diagram")}}return P({x:0,y:0}),S(),()=>{b=!0}},[O,z,Y]);let R=_7.useCallback((b)=>{if(b.target.closest("button"))return;b.currentTarget.setPointerCapture(b.pointerId),L.current={startX:b.clientX,startY:b.clientY,startPanX:A.x,startPanY:A.y}},[A]),T=_7.useCallback((b)=>{let S=L.current;if(!S)return;P({x:S.startPanX+b.clientX-S.startX,y:S.startPanY+b.clientY-S.startY})},[]),C=_7.useCallback(()=>{L.current=null},[]),y=_7.useCallback(()=>{let b=W.current,S=q.current;if(!b||!S)return;let I=b.viewBox.baseVal.width||b.getBoundingClientRect().width,g=b.viewBox.baseVal.height||b.getBoundingClientRect().height,l=48,k=S.clientWidth-l,i=S.clientHeight-l,X0=Math.min(k/I,i/g)*100,W0=0;for(let w0=q8.length-1;w0>=0;w0--)if(q8[w0]<=X0){W0=w0;break}H(W0),P({x:0,y:0})},[]);if($)return W8.jsxDEV("p",{className:"text-sm text-coral",children:$},void 0,!1,void 0,this);return W8.jsxDEV("div",{className:"flex gap-6",children:[W8.jsxDEV(rZ,{stages:Q,runId:Y,activeLink:"graph"},void 0,!1,void 0,this),W8.jsxDEV("div",{className:"min-w-0 flex-1",children:W8.jsxDEV("div",{className:"graph-svg relative rounded-md border border-line bg-panel-alt",children:[W8.jsxDEV(zN,{direction:O,setDirection:_,fitToWindow:y,zoomIndex:M,setZoomIndex:H},void 0,!1,void 0,this),W8.jsxDEV("div",{ref:q,className:"overflow-hidden p-6",style:{cursor:L.current?"grabbing":"grab"},onPointerDown:R,onPointerMove:T,onPointerUp:C,onPointerCancel:C,children:W8.jsxDEV("div",{ref:B,className:"flex items-center justify-center",style:{transform:`translate(${A.x}px, ${A.y}px) scale(${v/100})`,transformOrigin:"center center"},children:W8.jsxDEV("p",{className:"text-sm text-fg-muted",children:"Loading diagram..."},void 0,!1,void 0,this)},void 0,!1,void 0,this)},void 0,!1,void 0,this)]},void 0,!0,void 0,this)},void 0,!1,void 0,this)]},void 0,!0,void 0,this)}var GT={};p5(GT,{loader:()=>cY0,handle:()=>pY0,extractRequestId:()=>yc,emptyTransitionToastMessage:()=>Sc,default:()=>xc,deepLinkToastMessage:()=>nY0,ErrorBoundary:()=>rY0});var i5=h(Y0(),1);var x2=h(E0(),1);function IY0({name:Z}){return x2.jsxDEV("div",{className:"flex items-center justify-between rounded-md border border-line bg-panel/60 px-4 py-3 text-sm text-fg-muted",children:[x2.jsxDEV("span",{className:"font-mono text-fg-2",children:Z},void 0,!1,void 0,this),x2.jsxDEV("span",{className:"rounded bg-rose-950/40 px-2 py-0.5 text-xs text-rose-200",children:"sensitive — contents omitted"},void 0,!1,void 0,this)]},void 0,!0,void 0,this)}function yY0({name:Z}){return x2.jsxDEV("div",{className:"flex items-center justify-between rounded-md border border-line bg-panel/60 px-4 py-3 text-sm text-fg-muted",children:[x2.jsxDEV("span",{className:"font-mono text-fg-2",children:Z},void 0,!1,void 0,this),x2.jsxDEV("span",{className:"rounded bg-panel-alt/80 px-2 py-0.5 text-xs text-fg-3",children:"binary — not shown inline"},void 0,!1,void 0,this)]},void 0,!0,void 0,this)}function jY0({name:Z,reason:Y}){return x2.jsxDEV("div",{className:"flex items-center justify-between rounded-md border border-line bg-panel/60 px-4 py-3 text-sm text-fg-muted",children:[x2.jsxDEV("span",{className:"font-mono text-fg-2",children:Z},void 0,!1,void 0,this),x2.jsxDEV("span",{className:"rounded bg-panel-alt/80 px-2 py-0.5 text-xs text-fg-3",children:Y==="budget_exhausted"?"omitted — too many files changed":"too large to render inline"},void 0,!1,void 0,this)]},void 0,!0,void 0,this)}function Pc({name:Z,kind:Y}){return x2.jsxDEV("div",{className:"flex items-center justify-between rounded-md border border-line bg-panel/60 px-4 py-3 text-sm text-fg-muted",children:[x2.jsxDEV("span",{className:"font-mono text-fg-2",children:Z},void 0,!1,void 0,this),x2.jsxDEV("span",{className:"rounded bg-panel-alt/80 px-2 py-0.5 text-xs text-fg-3",children:Y},void 0,!1,void 0,this)]},void 0,!0,void 0,this)}function Vc(Z){let Y=Z.new_file.name||Z.old_file.name;if(Z.sensitive)return x2.jsxDEV(IY0,{name:Y},void 0,!1,void 0,this);if(Z.binary)return x2.jsxDEV(yY0,{name:Y},void 0,!1,void 0,this);if(Z.change_kind==="symlink")return x2.jsxDEV(Pc,{name:Y,kind:"symlink"},void 0,!1,void 0,this);if(Z.change_kind==="submodule")return x2.jsxDEV(Pc,{name:Y,kind:"submodule"},void 0,!1,void 0,this);if(Z.truncated)return x2.jsxDEV(jY0,{name:Y,reason:Z.truncation_reason},void 0,!1,void 0,this);return null}function Lc({reason:Z}){return x2.jsxDEV("div",{className:"rounded-md border border-amber-500/30 bg-amber-950/20 px-4 py-3 text-sm text-amber-100",children:SY0(Z)},void 0,!1,void 0,this)}function SY0(Z){switch(Z){case"sandbox_gone":return"Showing final patch only. This run's sandbox has been cleaned up, so individual file contents are no longer available.";case"provider_unsupported":return"Live diff isn't supported for this sandbox provider. Showing the patch captured at the last checkpoint.";case"sandbox_unreachable":default:return"Couldn't reach this run's sandbox. Showing the patch captured at the last checkpoint — refresh to try again."}}var N4=h(E0(),1);function KT({kind:Z}){return N4.jsxDEV("div",{role:"status",className:"rounded-md border border-dashed border-line bg-panel/40 px-6 py-10 text-center text-sm text-fg-muted",children:kY0(Z)},void 0,!1,void 0,this)}function kY0(Z){switch(Z){case"starting":return"Run is still starting. Files will appear once it begins.";case"no_changes":return"This run didn't change any files.";case"failed_before_checkpoint":return"This run failed before capturing any changes.";case"diff_lost":return"The diff for this run is no longer available. If you expect files here, please report it.";case"unknown":default:return"The diff for this run is not available right now."}}function BT(Z){let{runStatus:Y,totalChanged:Q,degraded:z}=Z;if(!Y)return"unknown";let q=Y.toLowerCase();if(q==="submitted"||q==="queued"||q==="starting")return"starting";if(q==="running"||q==="blocked"||q==="paused")return"no_changes";if(q==="failed"||q==="dead")return z?"unknown":"failed_before_checkpoint";if(q==="succeeded"||q==="removing"||q==="archived"){if(z)return"unknown";return Q>0?"diff_lost":"no_changes"}return"unknown"}function Rc(){return N4.jsxDEV("div",{className:"flex flex-col gap-3","aria-label":"Loading files",children:[N4.jsxDEV("div",{className:"h-8 rounded-md bg-panel/60 motion-safe:animate-pulse"},void 0,!1,void 0,this),N4.jsxDEV("div",{className:"h-32 rounded-md bg-panel/60 motion-safe:animate-pulse"},void 0,!1,void 0,this),N4.jsxDEV("div",{className:"h-32 rounded-md bg-panel/60 motion-safe:animate-pulse"},void 0,!1,void 0,this)]},void 0,!0,void 0,this)}function cW({message:Z,onRetry:Y}){return N4.jsxDEV("div",{className:"flex items-center justify-between gap-3 rounded-md border border-rose-500/30 bg-rose-950/20 px-4 py-3 text-sm text-rose-100",children:[N4.jsxDEV("span",{children:Z},void 0,!1,void 0,this),N4.jsxDEV("button",{type:"button",onClick:Y,className:"min-h-[32px] rounded-md border border-rose-500/40 bg-rose-950/40 px-3 py-1 text-xs font-medium text-rose-50 transition-colors hover:bg-rose-950/60",children:"Retry"},void 0,!1,void 0,this)]},void 0,!0,void 0,this)}function WT(Z){let{status:Y,requestId:Q,onRetry:z}=Z;if(Y===401||Y===403)return N4.jsxDEV("div",{role:"status",className:"rounded-md border border-dashed border-line bg-panel/40 px-6 py-10 text-center text-sm text-fg-muted",children:"You don't have access to this run's files."},void 0,!1,void 0,this);if(Y===429||Y===503)return N4.jsxDEV(cW,{message:"The diff service is temporarily unavailable.",onRetry:z},void 0,!1,void 0,this);if(Y>=500){let q=Q?` Request ID: ${Q}.`:"";return N4.jsxDEV("div",{role:"status",className:"rounded-md border border-dashed border-line bg-panel/40 px-6 py-10 text-center text-sm text-fg-muted",children:["Something went wrong.",q," Please contact support if this persists."]},void 0,!0,void 0,this)}return N4.jsxDEV(cW,{message:`Couldn't load files (${Y}).`,onRetry:z},void 0,!1,void 0,this)}function Tc(){let Z=OU();if(j3(Z))return WT({status:Z.status,requestId:xY0(Z.data),onRetry:()=>window.location.reload()});return N4.jsxDEV("div",{role:"status",className:"rounded-md border border-dashed border-line bg-panel/40 px-6 py-10 text-center text-sm text-fg-muted",children:"Something went wrong loading this run's files."},void 0,!1,void 0,this)}function xY0(Z){if(!Z||typeof Z!=="object")return null;let Y=Z;if(typeof Y.request_id==="string")return Y.request_id;let Q=Y.errors;if(Array.isArray(Q)&&Q.length>0){let z=Q[0];if(z&&typeof z==="object"){let q=z;if(typeof q.request_id==="string")return q.request_id;if(typeof q.detail==="string"){let B=q.detail.match(/request[_ ]id[=:]?\s*([a-zA-Z0-9-_]+)/i);if(B)return B[1]}}}return null}var vc=h(Y0(),1);function fY0(Z){if(!Z)return!1;let Y=Z.tagName.toLowerCase();if(Y==="input"||Y==="textarea"||Y==="select")return!0;return Z.isContentEditable===!0}function Cc(Z,Y){vc.useEffect(()=>{if(!Z.current)return;let Q=(z)=>{if(z.key!=="j"&&z.key!=="k")return;if(z.metaKey||z.ctrlKey||z.altKey)return;if(fY0(document.activeElement))return;let q=Z.current;if(!q)return;let B=Array.from(q.querySelectorAll('[data-run-file-row="true"]'));if(B.length===0)return;let W=document.activeElement,$=B.findIndex((H)=>H.contains(W)),U;if($<0)U=0;else U=z.key==="j"?$+1:$-1;if(U<0||U>=B.length)return;z.preventDefault();let M=B[U];M.focus({preventScroll:!1}),M.scrollIntoView({block:"nearest",behavior:"smooth"})};return document.addEventListener("keydown",Q),()=>document.removeEventListener("keydown",Q)},[Z,Y])}var A7=h(E0(),1);function Dc({totalChanged:Z,additions:Y,deletions:Q,onRefresh:z,refreshing:q,refreshDisabled:B,freshness:W,refreshButtonRef:$,diffStyle:U,onDiffStyleChange:M,diffStyleForced:H}){let O=q?"Refreshing":B?"Up to date":"Refresh";return A7.jsxDEV("div",{className:"flex flex-wrap items-center justify-between gap-x-4 gap-y-2 border-b border-line pb-3",children:[A7.jsxDEV("div",{className:"flex min-w-0 items-baseline gap-3",children:[A7.jsxDEV("p",{className:"text-base font-semibold text-fg",children:[A7.jsxDEV("span",{className:"tabular-nums",children:Z},void 0,!1,void 0,this)," ",Z===1?"file":"files"," changed"]},void 0,!0,void 0,this),Z>0&&(Y>0||Q>0)?A7.jsxDEV("p",{className:"font-mono text-sm tabular-nums",children:[A7.jsxDEV("span",{className:"font-medium text-mint",children:["+",Y]},void 0,!0,void 0,this),A7.jsxDEV("span",{className:"ml-1 font-medium text-coral",children:["−",Q]},void 0,!0,void 0,this)]},void 0,!0,void 0,this):null]},void 0,!0,void 0,this),A7.jsxDEV("div",{className:"flex items-center gap-3 text-xs",children:[W?A7.jsxDEV("span",{"aria-live":"polite",className:"hidden min-w-0 truncate text-fg-muted md:inline",children:W},void 0,!1,void 0,this):null,A7.jsxDEV(hY0,{value:U,onChange:M,forced:H},void 0,!1,void 0,this),A7.jsxDEV("button",{ref:$,type:"button",onClick:z,disabled:q||B,"aria-label":q?"Refreshing files":"Refresh files",title:O,className:"relative inline-flex size-7 items-center justify-center rounded-md border border-line bg-panel text-fg-3 transition-colors hover:bg-overlay hover:text-fg disabled:cursor-default disabled:opacity-60 disabled:hover:bg-panel disabled:hover:text-fg-3",children:[A7.jsxDEV(WZ,{className:`size-3.5 ${q?"animate-spin":""}`,"aria-hidden":"true"},void 0,!1,void 0,this),A7.jsxDEV("span",{className:"pointer-fine:hidden absolute top-1/2 left-1/2 size-[max(100%,3rem)] -translate-x-1/2 -translate-y-1/2","aria-hidden":"true"},void 0,!1,void 0,this)]},void 0,!0,void 0,this)]},void 0,!0,void 0,this)]},void 0,!0,void 0,this)}function hY0({value:Z,onChange:Y,forced:Q}){return A7.jsxDEV("div",{className:"inline-flex rounded-md bg-panel-alt p-0.5 ring-1 ring-line",role:"group","aria-label":"Diff layout",children:[A7.jsxDEV("button",{type:"button",onClick:()=>Y("split"),disabled:Q,"aria-pressed":Z==="split",className:`rounded px-2.5 py-1 text-xs font-medium transition-colors disabled:opacity-60 ${Z==="split"?"bg-overlay-strong text-fg":"text-fg-3 hover:text-fg"}`,children:"Split"},void 0,!1,void 0,this),A7.jsxDEV("button",{type:"button",onClick:()=>Y("unified"),disabled:Q,"aria-pressed":Z==="unified",className:`rounded px-2.5 py-1 text-xs font-medium transition-colors disabled:opacity-60 ${Z==="unified"?"bg-overlay-strong text-fg":"text-fg-3 hover:text-fg"}`,children:"Stacked"},void 0,!1,void 0,this)]},void 0,!0,void 0,this)}var W2=h(E0(),1),{MultiFileDiff:gY0,PatchDiff:mY0}=LR,bc=iw,uY0=typeof bc==="function"?bc:function({children:Y}){return W2.jsxDEV(W2.Fragment,{children:Y},void 0,!1,void 0,this)},pY0={wide:!0};async function cY0({request:Z,params:Y}){let Q=await fetch(`/api/v1/runs/${Y.id}/files`,{credentials:"include",...Z?.signal?{signal:Z.signal}:{}});if(Q.status===404||Q.status===501)return{data:null,error:null};if(Q.ok)return{data:await Q.json(),error:null};let z="";try{z=await Q.text()}catch{}let q=null;if(z)try{q=JSON.parse(z)}catch{}return{data:null,error:{status:Q.status,message:Q.statusText||`HTTP ${Q.status}`,requestId:yc(q)}}}function yc(Z){if(!Z||typeof Z!=="object")return null;let Y=Z;if(typeof Y.request_id==="string")return Y.request_id;let Q=Y.errors;if(Array.isArray(Q)&&Q.length>0){let z=Q[0];if(z&&typeof z==="object"){let q=z;if(typeof q.request_id==="string")return q.request_id;if(typeof q.detail==="string"){let B=q.detail.match(/request[_ ]id[=:]?\s*([a-zA-Z0-9-_]+)/i);if(B)return B[1]}}}return null}var dY0=new Set(["checkpoint.completed","run.completed","run.failed"]),lY0=768,jc="fabro.run-files.diff-style",rY0=Tc;function aY0(){let[Z,Y]=i5.useState(!1);return i5.useEffect(()=>{if(typeof window>"u")return;let Q=window.matchMedia(`(max-width: ${lY0-1}px)`),z=()=>Y(Q.matches);return z(),Q.addEventListener("change",z),()=>Q.removeEventListener("change",z)},[]),Z}function sY0(Z){Uq(Z,{allowlist:dY0,debounceMs:500})}function oY0(Z,Y){let Q=!!Z&&(!!Z.to_sha_committed_at||Y!==null),[,z]=i5.useState(0);if(i5.useEffect(()=>{if(!Q)return;let $=setInterval(()=>z((U)=>U+1),1e4);return()=>clearInterval($)},[Q]),!Z)return null;let q=Date.now(),B=Z.to_sha_committed_at?`Captured ${Ec(Z.to_sha_committed_at,q)}`:null,W=Y?`Fetched ${Ec(new Date(Y).toISOString(),q)}`:null;if(Z.degraded&&B&&W)return`${B} · ${W}`;return B??W??null}function Ec(Z,Y){if(!Z)return"";let Q=Date.parse(Z);if(Number.isNaN(Q))return"";let z=Math.max(0,Math.floor((Y-Q)/1000));if(z<5)return"just now";if(z<60)return`${z}s ago`;let q=Math.floor(z/60);if(q<60)return`${q}m ago`;let B=Math.floor(q/60);if(B<24)return`${B}h ago`;return`${Math.floor(B/24)}d ago`}function iY0(){if(typeof window>"u")return"split";try{let Z=window.localStorage.getItem(jc);if(Z==="split"||Z==="unified")return Z}catch{}return"split"}function tY0(Z){if(typeof window>"u")return;try{window.localStorage.setItem(jc,Z)}catch{}}function $T(Z){return`run-file:${Z}`}function Ic(Z){if(!Z)return null;let Y=Z.startsWith("#")?Z.slice(1):Z,Q="file=";if(!Y.startsWith(Q))return null;try{return decodeURIComponent(Y.slice(Q.length))}catch{return null}}function Sc(Z,Y){return Z!==null&&Z>0&&Y===0?"No changes in this run.":null}function kc(Z,Y){if(!Z||!Y)return null;if(Y.meta.degraded&&Y.meta.patch)return{key:`patch-only:${Z}`,message:"File-level navigation isn't available in the patch-only view."};if(!Y.data.some((z)=>z.new_file.name===Z||z.old_file.name===Z))return{key:`missing:${Z}`,message:`File ${Z} is not in this run.`};return null}function nY0(Z,Y){return kc(Z,Y)?.message??null}function eY0(Z){for(let Y of Z){let Q=Y.data;if(!Q)continue;if(typeof Q?.run?.lifecycleStatus==="string")return Q.run.lifecycleStatus;if(typeof Q?.lifecycleStatus==="string")return Q.lifecycleStatus}return}function xc({loaderData:Z}){let Y=B7(),Q=NU(),z=h8(),q=fY(),{push:B}=YN(),W=Z,$=aY0(),U=eY0(q),M=i5.useRef(null),H=i5.useRef(null);i5.useEffect(()=>{if(!W?.data)return;let f=Sc(M.current?.data.length??null,W.data.data.length);if(f)B({message:f});M.current=W.data,H.current=Date.now()},[B,W?.data]);let O=W?.data??M.current;sY0(Y.id);let _=Q.state==="loading"&&!Z,A=z.state==="loading",P=W?.error&&M.current?`Couldn't refresh (${W.error.status}).`:null,L=W?.error&&!M.current?W.error:null,v=oY0(O?.meta??null,H.current),[R,T]=i5.useState(iY0),C=$?"unified":R,y=$,b=i5.useCallback((f)=>{if(y)return;T(f),tY0(f)},[y]),S=i5.useRef(null),I=i5.useRef(null),g=i5.useRef(null),l=i5.useRef(!1);i5.useEffect(()=>{if(l.current&&!A)S.current?.focus({preventScroll:!0});l.current=A},[A]);let k=O?.data.length??0;Cc(I,k);let[i,X0]=i5.useState(()=>{if(typeof window>"u")return null;return Ic(window.location.hash)});i5.useEffect(()=>{if(typeof window>"u")return;let f=()=>X0(Ic(window.location.hash));return window.addEventListener("hashchange",f),()=>window.removeEventListener("hashchange",f)},[]),i5.useEffect(()=>{let f=kc(i,O);if(f){if(g.current!==f.key)B({message:f.message,autoDismissMs:5000}),g.current=f.key;return}if(g.current=null,!i||!O)return;let z0=document.getElementById($T(i));if(z0)z0.scrollIntoView({block:"start",behavior:"smooth"}),z0.focus({preventScroll:!0})},[O,i,B]);let W0=i5.useCallback((f)=>f.map((z0,N0)=>{let s=z0.new_file.name||z0.old_file.name,H0=Vc(z0);if(H0)return W2.jsxDEV("div",{id:$T(s),tabIndex:-1,"data-run-file-row":"true",role:"region","aria-label":`${z0.change_kind??"changed"}: ${s}`,className:"focus:outline-2 focus:outline-focus focus:outline-offset-2 rounded-md",children:H0},`${s}-${N0}`,!1,void 0,this);let y0=!!i&&(z0.new_file.name===i||z0.old_file.name===i);return W2.jsxDEV("div",{id:$T(s),tabIndex:-1,"data-run-file-row":"true",role:"region","aria-label":`${z0.change_kind??"modified"}: ${z0.new_file.name}`,className:"focus:outline-2 focus:outline-focus focus:outline-offset-2 rounded-md",children:W2.jsxDEV(gY0,{oldFile:z0.old_file,newFile:z0.new_file,options:{diffStyle:C,theme:"pierre-dark",expandUnchanged:y0?!0:void 0}},void 0,!1,void 0,this)},`${z0.new_file.name}-${N0}`,!1,void 0,this)}),[C,i]);if(_)return W2.jsxDEV(Rc,{},void 0,!1,void 0,this);if(L)return WT({status:L.status,requestId:L.requestId,onRetry:()=>z.revalidate()});if(!O)return W2.jsxDEV(KT,{kind:BT({runStatus:U,totalChanged:0,degraded:!1})},void 0,!1,void 0,this);let{data:w0,meta:n}=O,Q0=M.current?.meta?.to_sha??null,$0=!!n.to_sha&&Q0!==null&&Q0===n.to_sha,e=W2.jsxDEV(Dc,{totalChanged:n.total_changed,additions:n.stats.additions,deletions:n.stats.deletions,onRefresh:()=>z.revalidate(),refreshing:A,refreshDisabled:$0,freshness:v,refreshButtonRef:S,diffStyle:C,onDiffStyleChange:b,diffStyleForced:y},void 0,!1,void 0,this);if(n.degraded&&n.patch)return W2.jsxDEV("div",{ref:I,className:"flex flex-col gap-4",children:[e,P?W2.jsxDEV(cW,{message:P,onRetry:()=>z.revalidate()},void 0,!1,void 0,this):null,W2.jsxDEV(Lc,{reason:n.degraded_reason},void 0,!1,void 0,this),W2.jsxDEV(mY0,{patch:n.patch,options:{diffStyle:C,theme:"pierre-dark"}},void 0,!1,void 0,this)]},void 0,!0,void 0,this);if(w0.length===0)return W2.jsxDEV("div",{ref:I,className:"flex flex-col gap-4",children:[e,W2.jsxDEV(KT,{kind:BT({runStatus:U,totalChanged:n.total_changed,degraded:n.degraded??!1})},void 0,!1,void 0,this)]},void 0,!0,void 0,this);let q0=w0.length>20?W2.jsxDEV(uY0,{children:W0(w0)},void 0,!1,void 0,this):W2.jsxDEV(W2.Fragment,{children:W0(w0)},void 0,!1,void 0,this);return W2.jsxDEV("div",{ref:I,className:"flex flex-col gap-4",children:[e,P?W2.jsxDEV(cW,{message:P,onRetry:()=>z.revalidate()},void 0,!1,void 0,this):null,q0]},void 0,!0,void 0,this)}var UT={};p5(UT,{loader:()=>ZJ0,default:()=>fc});var n0=h(E0(),1);function oZ(Z){return`${(Z/1000).toFixed(1)}k`}function wN(Z){return Z==null?"-":`$${(Z/1e6).toFixed(2)}`}async function ZJ0({request:Z,params:Y}){let Q=await h4(`/runs/${Y.id}/billing`,{request:Z}),z=Q.stages.map((M)=>({stage:M.stage.name,model:M.model.id,inputTokens:M.billing.input_tokens,outputTokens:M.billing.output_tokens+(M.billing.reasoning_tokens??0),runtime:b7(M.runtime_secs),totalUsdMicros:M.billing.total_usd_micros})),q=b7(Q.totals.runtime_secs),B=Q.totals.input_tokens,W=Q.totals.output_tokens+(Q.totals.reasoning_tokens??0),$=Q.totals.total_usd_micros,U=Q.by_model.map((M)=>({model:M.model.id,stages:M.stages,inputTokens:M.billing.input_tokens,outputTokens:M.billing.output_tokens+(M.billing.reasoning_tokens??0),totalUsdMicros:M.billing.total_usd_micros})).sort((M,H)=>(H.totalUsdMicros??-1)-(M.totalUsdMicros??-1));return{stages:z,totalRuntime:q,totalUsdMicros:$,totalInput:B,totalOutput:W,modelBreakdown:U}}function fc({loaderData:Z}){let{stages:Y,totalRuntime:Q,totalUsdMicros:z,totalInput:q,totalOutput:B,modelBreakdown:W}=Z;if(!Y.length)return n0.jsxDEV("div",{className:"py-12",children:n0.jsxDEV(g8,{title:"No billing yet",description:"Token usage and cost will appear here once stages complete."},void 0,!1,void 0,this)},void 0,!1,void 0,this);return n0.jsxDEV("div",{className:"space-y-6",children:[n0.jsxDEV("div",{className:"overflow-hidden rounded-md border border-line",children:n0.jsxDEV("table",{className:"w-full text-sm",children:[n0.jsxDEV("thead",{children:n0.jsxDEV("tr",{className:"border-b border-line bg-panel/60 text-left text-xs font-medium text-fg-3",children:[n0.jsxDEV("th",{className:"px-4 py-2.5 font-medium",children:"Stage"},void 0,!1,void 0,this),n0.jsxDEV("th",{className:"px-4 py-2.5 font-medium",children:"Model"},void 0,!1,void 0,this),n0.jsxDEV("th",{className:"px-4 py-2.5 font-medium text-right",children:"Tokens"},void 0,!1,void 0,this),n0.jsxDEV("th",{className:"px-4 py-2.5 font-medium text-right",children:"Run time"},void 0,!1,void 0,this),n0.jsxDEV("th",{className:"px-4 py-2.5 font-medium text-right",children:"Billing"},void 0,!1,void 0,this)]},void 0,!0,void 0,this)},void 0,!1,void 0,this),n0.jsxDEV("tbody",{children:Y.map(($)=>n0.jsxDEV("tr",{className:"border-b border-line last:border-b-0",children:[n0.jsxDEV("td",{className:"px-4 py-3 text-fg-2",children:$.stage},void 0,!1,void 0,this),n0.jsxDEV("td",{className:"px-4 py-3 font-mono text-xs text-fg-3",children:$.model},void 0,!1,void 0,this),n0.jsxDEV("td",{className:"px-4 py-3 text-right font-mono text-xs tabular-nums text-fg-3",children:[oZ($.inputTokens)," ",n0.jsxDEV("span",{className:"text-fg-muted",children:"/"},void 0,!1,void 0,this)," ",oZ($.outputTokens)]},void 0,!0,void 0,this),n0.jsxDEV("td",{className:"px-4 py-3 text-right font-mono text-xs text-fg-3",children:$.runtime},void 0,!1,void 0,this),n0.jsxDEV("td",{className:"px-4 py-3 text-right font-mono text-xs text-fg-3",children:wN($.totalUsdMicros)},void 0,!1,void 0,this)]},$.stage,!0,void 0,this))},void 0,!1,void 0,this),n0.jsxDEV("tfoot",{children:n0.jsxDEV("tr",{className:"border-t border-line-strong bg-overlay",children:[n0.jsxDEV("td",{className:"px-4 py-3 font-medium text-fg",children:"Total"},void 0,!1,void 0,this),n0.jsxDEV("td",{},void 0,!1,void 0,this),n0.jsxDEV("td",{className:"px-4 py-3 text-right font-mono text-xs tabular-nums font-medium text-fg",children:[oZ(q)," ",n0.jsxDEV("span",{className:"text-fg-muted",children:"/"},void 0,!1,void 0,this)," ",oZ(B)]},void 0,!0,void 0,this),n0.jsxDEV("td",{className:"px-4 py-3 text-right font-mono text-xs font-medium text-fg",children:Q},void 0,!1,void 0,this),n0.jsxDEV("td",{className:"px-4 py-3 text-right font-mono text-xs font-medium text-fg",children:wN(z)},void 0,!1,void 0,this)]},void 0,!0,void 0,this)},void 0,!1,void 0,this)]},void 0,!0,void 0,this)},void 0,!1,void 0,this),n0.jsxDEV("div",{children:[n0.jsxDEV("h3",{className:"mb-3 text-sm font-semibold text-fg",children:"By model"},void 0,!1,void 0,this),n0.jsxDEV("div",{className:"overflow-hidden rounded-md border border-line",children:n0.jsxDEV("table",{className:"w-full text-sm",children:[n0.jsxDEV("thead",{children:n0.jsxDEV("tr",{className:"border-b border-line bg-panel/60 text-left text-xs font-medium text-fg-3",children:[n0.jsxDEV("th",{className:"px-4 py-2.5 font-medium",children:"Model"},void 0,!1,void 0,this),n0.jsxDEV("th",{className:"px-4 py-2.5 font-medium text-right",children:"Stages"},void 0,!1,void 0,this),n0.jsxDEV("th",{className:"px-4 py-2.5 font-medium text-right",children:"Tokens"},void 0,!1,void 0,this),n0.jsxDEV("th",{className:"px-4 py-2.5 font-medium text-right",children:"Billing"},void 0,!1,void 0,this)]},void 0,!0,void 0,this)},void 0,!1,void 0,this),n0.jsxDEV("tbody",{children:W.map(($)=>n0.jsxDEV("tr",{className:"border-b border-line last:border-b-0",children:[n0.jsxDEV("td",{className:"px-4 py-3 font-mono text-xs text-fg-2",children:$.model},void 0,!1,void 0,this),n0.jsxDEV("td",{className:"px-4 py-3 text-right font-mono text-xs tabular-nums text-fg-3",children:$.stages},void 0,!1,void 0,this),n0.jsxDEV("td",{className:"px-4 py-3 text-right font-mono text-xs tabular-nums text-fg-3",children:[oZ($.inputTokens)," ",n0.jsxDEV("span",{className:"text-fg-muted",children:"/"},void 0,!1,void 0,this)," ",oZ($.outputTokens)]},void 0,!0,void 0,this),n0.jsxDEV("td",{className:"px-4 py-3 text-right font-mono text-xs text-fg-3",children:wN($.totalUsdMicros)},void 0,!1,void 0,this)]},$.model,!0,void 0,this))},void 0,!1,void 0,this),n0.jsxDEV("tfoot",{children:n0.jsxDEV("tr",{className:"border-t border-line-strong bg-overlay",children:[n0.jsxDEV("td",{className:"px-4 py-3 font-medium text-fg",children:"Total"},void 0,!1,void 0,this),n0.jsxDEV("td",{className:"px-4 py-3 text-right font-mono text-xs tabular-nums font-medium text-fg",children:Y.length},void 0,!1,void 0,this),n0.jsxDEV("td",{className:"px-4 py-3 text-right font-mono text-xs tabular-nums font-medium text-fg",children:[oZ(q)," ",n0.jsxDEV("span",{className:"text-fg-muted",children:"/"},void 0,!1,void 0,this)," ",oZ(B)]},void 0,!0,void 0,this),n0.jsxDEV("td",{className:"px-4 py-3 text-right font-mono text-xs font-medium text-fg",children:wN(z)},void 0,!1,void 0,this)]},void 0,!0,void 0,this)},void 0,!1,void 0,this)]},void 0,!0,void 0,this)},void 0,!1,void 0,this)]},void 0,!0,void 0,this)]},void 0,!0,void 0,this)}var MT={};p5(MT,{meta:()=>YJ0,loader:()=>QJ0,handle:()=>JJ0,default:()=>hc});var L2=h(E0(),1);function YJ0({}){return[{title:"Insights — Fabro"}]}var JJ0={wide:!0};async function QJ0({request:Z}){let[{data:Y},{data:Q}]=await Promise.all([h4("/insights/queries",{request:Z}),h4("/insights/history",{request:Z})]),z=Y.map((B)=>({id:B.id,name:B.name,sql:B.sql})),q=Q.map((B)=>({id:B.id,sql:B.sql,timestamp:B.timestamp,elapsed:B.elapsed,rowsReturned:B.row_count}));return{savedQueries:z,historyEntries:q}}function hc({loaderData:Z}){let{savedQueries:Y,historyEntries:Q}=Z,z=x4();return L2.jsxDEV("div",{className:"flex gap-6",children:[L2.jsxDEV("div",{className:"w-56 shrink-0",children:L2.jsxDEV("div",{className:"sticky top-6 space-y-4",children:[L2.jsxDEV(l1,{to:"/insights/new",className:"inline-flex w-full items-center justify-center gap-1.5 rounded-md border border-line bg-panel/80 px-3 py-2 text-sm font-medium text-fg-3 transition-colors hover:border-line-strong hover:bg-panel hover:text-fg",children:[L2.jsxDEV(vV,{className:"size-3.5"},void 0,!1,void 0,this),"New Query"]},void 0,!0,void 0,this),L2.jsxDEV("div",{children:[L2.jsxDEV("h3",{className:"mb-2 text-[11px] font-semibold uppercase tracking-wider text-fg-muted",children:"Saved Queries"},void 0,!1,void 0,this),L2.jsxDEV("div",{className:"space-y-0.5",children:Y.map((q)=>L2.jsxDEV("button",{type:"button",onClick:()=>{z("/insights",{state:{sql:q.sql,name:q.name}})},className:"flex w-full flex-col gap-0.5 rounded-md px-2.5 py-2 text-left transition-colors hover:bg-overlay",children:[L2.jsxDEV("span",{className:"text-sm font-medium text-fg-2",children:q.name},void 0,!1,void 0,this),L2.jsxDEV("span",{className:"truncate font-mono text-[10px] text-fg-muted",children:q.sql.split(` +}`}function EY0(Z){let Y=Z.querySelector(".graph > title");if(!Y)return;let Q=Y.nextElementSibling;while(Q&&Q.tagName==="text"){let z=Q.nextElementSibling;Q.remove(),Q=z}Y.remove()}function Fc({loaderData:Z}){let{id:Y}=B7(),{stages:Q,graphSvg:z}=Z,q=_7.useRef(null),B=_7.useRef(null),W=_7.useRef(null),[$,U]=_7.useState(null),[M,H]=_7.useState(XN),[O,_]=_7.useState("LR"),[A,P]=_7.useState({x:0,y:0}),L=_7.useRef(null),v=q8[M];_7.useEffect(()=>{let b=!1;async function S(){try{let I;if(z){let g=await fetch(`/api/v1/runs/${Y}/graph?direction=${O}`,{credentials:"include"});if(b)return;if(!g.ok){U("Failed to load graph");return}let l=await g.text(),X0=new DOMParser().parseFromString(l,"image/svg+xml").documentElement;if(!(X0 instanceof SVGSVGElement)){U("Invalid SVG from server");return}I=X0}else{let{instance:g}=await import("./chunk-a51kkeyv.js"),l=await g();if(b)return;I=l.renderSVGElement(bY0(O))}if(EY0(I),W.current=I,B.current)B.current.replaceChildren(I)}catch(I){U(I instanceof Error?I.message:"Failed to render diagram")}}return P({x:0,y:0}),S(),()=>{b=!0}},[O,z,Y]);let R=_7.useCallback((b)=>{if(b.target.closest("button"))return;b.currentTarget.setPointerCapture(b.pointerId),L.current={startX:b.clientX,startY:b.clientY,startPanX:A.x,startPanY:A.y}},[A]),T=_7.useCallback((b)=>{let S=L.current;if(!S)return;P({x:S.startPanX+b.clientX-S.startX,y:S.startPanY+b.clientY-S.startY})},[]),C=_7.useCallback(()=>{L.current=null},[]),y=_7.useCallback(()=>{let b=W.current,S=q.current;if(!b||!S)return;let I=b.viewBox.baseVal.width||b.getBoundingClientRect().width,g=b.viewBox.baseVal.height||b.getBoundingClientRect().height,l=48,k=S.clientWidth-l,i=S.clientHeight-l,X0=Math.min(k/I,i/g)*100,W0=0;for(let w0=q8.length-1;w0>=0;w0--)if(q8[w0]<=X0){W0=w0;break}H(W0),P({x:0,y:0})},[]);if($)return W8.jsxDEV("p",{className:"text-sm text-coral",children:$},void 0,!1,void 0,this);return W8.jsxDEV("div",{className:"flex gap-6",children:[W8.jsxDEV(rZ,{stages:Q,runId:Y,activeLink:"graph"},void 0,!1,void 0,this),W8.jsxDEV("div",{className:"min-w-0 flex-1",children:W8.jsxDEV("div",{className:"graph-svg relative rounded-md border border-line bg-panel-alt",children:[W8.jsxDEV(zN,{direction:O,setDirection:_,fitToWindow:y,zoomIndex:M,setZoomIndex:H},void 0,!1,void 0,this),W8.jsxDEV("div",{ref:q,className:"overflow-hidden p-6",style:{cursor:L.current?"grabbing":"grab"},onPointerDown:R,onPointerMove:T,onPointerUp:C,onPointerCancel:C,children:W8.jsxDEV("div",{ref:B,className:"flex items-center justify-center",style:{transform:`translate(${A.x}px, ${A.y}px) scale(${v/100})`,transformOrigin:"center center"},children:W8.jsxDEV("p",{className:"text-sm text-fg-muted",children:"Loading diagram..."},void 0,!1,void 0,this)},void 0,!1,void 0,this)},void 0,!1,void 0,this)]},void 0,!0,void 0,this)},void 0,!1,void 0,this)]},void 0,!0,void 0,this)}var GT={};p5(GT,{loader:()=>cY0,handle:()=>pY0,extractRequestId:()=>yc,emptyTransitionToastMessage:()=>Sc,default:()=>xc,deepLinkToastMessage:()=>nY0,ErrorBoundary:()=>rY0});var i5=h(Y0(),1);var x2=h(E0(),1);function IY0({name:Z}){return x2.jsxDEV("div",{className:"flex items-center justify-between rounded-md border border-line bg-panel/60 px-4 py-3 text-sm text-fg-muted",children:[x2.jsxDEV("span",{className:"font-mono text-fg-2",children:Z},void 0,!1,void 0,this),x2.jsxDEV("span",{className:"rounded bg-rose-950/40 px-2 py-0.5 text-xs text-rose-200",children:"sensitive — contents omitted"},void 0,!1,void 0,this)]},void 0,!0,void 0,this)}function yY0({name:Z}){return x2.jsxDEV("div",{className:"flex items-center justify-between rounded-md border border-line bg-panel/60 px-4 py-3 text-sm text-fg-muted",children:[x2.jsxDEV("span",{className:"font-mono text-fg-2",children:Z},void 0,!1,void 0,this),x2.jsxDEV("span",{className:"rounded bg-panel-alt/80 px-2 py-0.5 text-xs text-fg-3",children:"binary — not shown inline"},void 0,!1,void 0,this)]},void 0,!0,void 0,this)}function jY0({name:Z,reason:Y}){return x2.jsxDEV("div",{className:"flex items-center justify-between rounded-md border border-line bg-panel/60 px-4 py-3 text-sm text-fg-muted",children:[x2.jsxDEV("span",{className:"font-mono text-fg-2",children:Z},void 0,!1,void 0,this),x2.jsxDEV("span",{className:"rounded bg-panel-alt/80 px-2 py-0.5 text-xs text-fg-3",children:Y==="budget_exhausted"?"omitted — too many files changed":"too large to render inline"},void 0,!1,void 0,this)]},void 0,!0,void 0,this)}function Pc({name:Z,kind:Y}){return x2.jsxDEV("div",{className:"flex items-center justify-between rounded-md border border-line bg-panel/60 px-4 py-3 text-sm text-fg-muted",children:[x2.jsxDEV("span",{className:"font-mono text-fg-2",children:Z},void 0,!1,void 0,this),x2.jsxDEV("span",{className:"rounded bg-panel-alt/80 px-2 py-0.5 text-xs text-fg-3",children:Y},void 0,!1,void 0,this)]},void 0,!0,void 0,this)}function Vc(Z){let Y=Z.new_file.name||Z.old_file.name;if(Z.sensitive)return x2.jsxDEV(IY0,{name:Y},void 0,!1,void 0,this);if(Z.binary)return x2.jsxDEV(yY0,{name:Y},void 0,!1,void 0,this);if(Z.change_kind==="symlink")return x2.jsxDEV(Pc,{name:Y,kind:"symlink"},void 0,!1,void 0,this);if(Z.change_kind==="submodule")return x2.jsxDEV(Pc,{name:Y,kind:"submodule"},void 0,!1,void 0,this);if(Z.truncated)return x2.jsxDEV(jY0,{name:Y,reason:Z.truncation_reason},void 0,!1,void 0,this);return null}function Lc({reason:Z}){return x2.jsxDEV("div",{className:"rounded-md border border-amber-500/30 bg-amber-950/20 px-4 py-3 text-sm text-amber-100",children:SY0(Z)},void 0,!1,void 0,this)}function SY0(Z){switch(Z){case"sandbox_gone":return"Showing final patch only. This run's sandbox has been cleaned up, so individual file contents are no longer available.";case"provider_unsupported":return"Live diff isn't supported for this sandbox provider. Showing the patch captured at the last checkpoint.";case"sandbox_unreachable":default:return"Couldn't reach this run's sandbox. Showing the patch captured at the last checkpoint — refresh to try again."}}var N4=h(E0(),1);function KT({kind:Z}){return N4.jsxDEV("div",{role:"status",className:"rounded-md border border-dashed border-line bg-panel/40 px-6 py-10 text-center text-sm text-fg-muted",children:kY0(Z)},void 0,!1,void 0,this)}function kY0(Z){switch(Z){case"starting":return"Run is still starting. Files will appear once it begins.";case"no_changes":return"This run didn't change any files.";case"failed_before_checkpoint":return"This run failed before capturing any changes.";case"diff_lost":return"The diff for this run is no longer available. If you expect files here, please report it.";case"unknown":default:return"The diff for this run is not available right now."}}function BT(Z){let{runStatus:Y,totalChanged:Q,degraded:z}=Z;if(!Y)return"unknown";let q=Y.toLowerCase();if(q==="submitted"||q==="queued"||q==="starting")return"starting";if(q==="running"||q==="blocked"||q==="paused")return"no_changes";if(q==="failed"||q==="dead")return z?"unknown":"failed_before_checkpoint";if(q==="succeeded"||q==="removing"||q==="archived"){if(z)return"unknown";return Q>0?"diff_lost":"no_changes"}return"unknown"}function Rc(){return N4.jsxDEV("div",{className:"flex flex-col gap-3","aria-label":"Loading files",children:[N4.jsxDEV("div",{className:"h-8 rounded-md bg-panel/60 motion-safe:animate-pulse"},void 0,!1,void 0,this),N4.jsxDEV("div",{className:"h-32 rounded-md bg-panel/60 motion-safe:animate-pulse"},void 0,!1,void 0,this),N4.jsxDEV("div",{className:"h-32 rounded-md bg-panel/60 motion-safe:animate-pulse"},void 0,!1,void 0,this)]},void 0,!0,void 0,this)}function cW({message:Z,onRetry:Y}){return N4.jsxDEV("div",{className:"flex items-center justify-between gap-3 rounded-md border border-rose-500/30 bg-rose-950/20 px-4 py-3 text-sm text-rose-100",children:[N4.jsxDEV("span",{children:Z},void 0,!1,void 0,this),N4.jsxDEV("button",{type:"button",onClick:Y,className:"min-h-[32px] rounded-md border border-rose-500/40 bg-rose-950/40 px-3 py-1 text-xs font-medium text-rose-50 transition-colors hover:bg-rose-950/60",children:"Retry"},void 0,!1,void 0,this)]},void 0,!0,void 0,this)}function WT(Z){let{status:Y,requestId:Q,onRetry:z}=Z;if(Y===401||Y===403)return N4.jsxDEV("div",{role:"status",className:"rounded-md border border-dashed border-line bg-panel/40 px-6 py-10 text-center text-sm text-fg-muted",children:"You don't have access to this run's files."},void 0,!1,void 0,this);if(Y===429||Y===503)return N4.jsxDEV(cW,{message:"The diff service is temporarily unavailable.",onRetry:z},void 0,!1,void 0,this);if(Y>=500){let q=Q?` Request ID: ${Q}.`:"";return N4.jsxDEV("div",{role:"status",className:"rounded-md border border-dashed border-line bg-panel/40 px-6 py-10 text-center text-sm text-fg-muted",children:["Something went wrong.",q," Please contact support if this persists."]},void 0,!0,void 0,this)}return N4.jsxDEV(cW,{message:`Couldn't load files (${Y}).`,onRetry:z},void 0,!1,void 0,this)}function Tc(){let Z=OU();if(j3(Z))return WT({status:Z.status,requestId:xY0(Z.data),onRetry:()=>window.location.reload()});return N4.jsxDEV("div",{role:"status",className:"rounded-md border border-dashed border-line bg-panel/40 px-6 py-10 text-center text-sm text-fg-muted",children:"Something went wrong loading this run's files."},void 0,!1,void 0,this)}function xY0(Z){if(!Z||typeof Z!=="object")return null;let Y=Z;if(typeof Y.request_id==="string")return Y.request_id;let Q=Y.errors;if(Array.isArray(Q)&&Q.length>0){let z=Q[0];if(z&&typeof z==="object"){let q=z;if(typeof q.request_id==="string")return q.request_id;if(typeof q.detail==="string"){let B=q.detail.match(/request[_ ]id[=:]?\s*([a-zA-Z0-9-_]+)/i);if(B)return B[1]}}}return null}var vc=h(Y0(),1);function fY0(Z){if(!Z)return!1;let Y=Z.tagName.toLowerCase();if(Y==="input"||Y==="textarea"||Y==="select")return!0;return Z.isContentEditable===!0}function Cc(Z,Y){vc.useEffect(()=>{if(!Z.current)return;let Q=(z)=>{if(z.key!=="j"&&z.key!=="k")return;if(z.metaKey||z.ctrlKey||z.altKey)return;if(fY0(document.activeElement))return;let q=Z.current;if(!q)return;let B=Array.from(q.querySelectorAll('[data-run-file-row="true"]'));if(B.length===0)return;let W=document.activeElement,$=B.findIndex((H)=>H.contains(W)),U;if($<0)U=0;else U=z.key==="j"?$+1:$-1;if(U<0||U>=B.length)return;z.preventDefault();let M=B[U];M.focus({preventScroll:!1}),M.scrollIntoView({block:"nearest",behavior:"smooth"})};return document.addEventListener("keydown",Q),()=>document.removeEventListener("keydown",Q)},[Z,Y])}var A7=h(E0(),1);function Dc({changeSummary:Z,onRefresh:Y,refreshing:Q,refreshDisabled:z,freshness:q,refreshButtonRef:B,diffStyle:W,onDiffStyleChange:$,diffStyleForced:U}){let{totalChanged:M,additions:H,deletions:O}=Z,_=Q?"Refreshing":z?"Up to date":"Refresh";return A7.jsxDEV("div",{className:"flex flex-wrap items-center justify-between gap-x-4 gap-y-2 border-b border-line pb-3",children:[A7.jsxDEV("div",{className:"flex min-w-0 items-baseline gap-3",children:[A7.jsxDEV("p",{className:"text-base font-semibold text-fg",children:[A7.jsxDEV("span",{className:"tabular-nums",children:M},void 0,!1,void 0,this)," ",M===1?"file":"files"," changed"]},void 0,!0,void 0,this),M>0&&(H>0||O>0)?A7.jsxDEV("p",{className:"font-mono text-sm tabular-nums",children:[A7.jsxDEV("span",{className:"font-medium text-mint",children:["+",H]},void 0,!0,void 0,this),A7.jsxDEV("span",{className:"ml-1 font-medium text-coral",children:["−",O]},void 0,!0,void 0,this)]},void 0,!0,void 0,this):null]},void 0,!0,void 0,this),A7.jsxDEV("div",{className:"flex items-center gap-3 text-xs",children:[q?A7.jsxDEV("span",{"aria-live":"polite",className:"hidden min-w-0 truncate text-fg-muted md:inline",children:q},void 0,!1,void 0,this):null,A7.jsxDEV(hY0,{value:W,onChange:$,forced:U},void 0,!1,void 0,this),A7.jsxDEV("button",{ref:B,type:"button",onClick:Y,disabled:Q||z,"aria-label":Q?"Refreshing files":"Refresh files",title:_,className:"relative inline-flex size-7 items-center justify-center rounded-md border border-line bg-panel text-fg-3 transition-colors hover:bg-overlay hover:text-fg disabled:cursor-default disabled:opacity-60 disabled:hover:bg-panel disabled:hover:text-fg-3",children:[A7.jsxDEV(WZ,{className:`size-3.5 ${Q?"animate-spin":""}`,"aria-hidden":"true"},void 0,!1,void 0,this),A7.jsxDEV("span",{className:"pointer-fine:hidden absolute top-1/2 left-1/2 size-[max(100%,3rem)] -translate-x-1/2 -translate-y-1/2","aria-hidden":"true"},void 0,!1,void 0,this)]},void 0,!0,void 0,this)]},void 0,!0,void 0,this)]},void 0,!0,void 0,this)}function hY0({value:Z,onChange:Y,forced:Q}){return A7.jsxDEV("div",{className:"inline-flex rounded-md bg-panel-alt p-0.5 ring-1 ring-line",role:"group","aria-label":"Diff layout",children:[A7.jsxDEV("button",{type:"button",onClick:()=>Y("split"),disabled:Q,"aria-pressed":Z==="split",className:`rounded px-2.5 py-1 text-xs font-medium transition-colors disabled:opacity-60 ${Z==="split"?"bg-overlay-strong text-fg":"text-fg-3 hover:text-fg"}`,children:"Split"},void 0,!1,void 0,this),A7.jsxDEV("button",{type:"button",onClick:()=>Y("unified"),disabled:Q,"aria-pressed":Z==="unified",className:`rounded px-2.5 py-1 text-xs font-medium transition-colors disabled:opacity-60 ${Z==="unified"?"bg-overlay-strong text-fg":"text-fg-3 hover:text-fg"}`,children:"Stacked"},void 0,!1,void 0,this)]},void 0,!0,void 0,this)}var W2=h(E0(),1),{MultiFileDiff:gY0,PatchDiff:mY0}=LR,bc=iw,uY0=typeof bc==="function"?bc:function({children:Y}){return W2.jsxDEV(W2.Fragment,{children:Y},void 0,!1,void 0,this)},pY0={wide:!0};async function cY0({request:Z,params:Y}){let Q=await fetch(`/api/v1/runs/${Y.id}/files`,{credentials:"include",...Z?.signal?{signal:Z.signal}:{}});if(Q.status===404||Q.status===501)return{data:null,error:null};if(Q.ok)return{data:await Q.json(),error:null};let z="";try{z=await Q.text()}catch{}let q=null;if(z)try{q=JSON.parse(z)}catch{}return{data:null,error:{status:Q.status,message:Q.statusText||`HTTP ${Q.status}`,requestId:yc(q)}}}function yc(Z){if(!Z||typeof Z!=="object")return null;let Y=Z;if(typeof Y.request_id==="string")return Y.request_id;let Q=Y.errors;if(Array.isArray(Q)&&Q.length>0){let z=Q[0];if(z&&typeof z==="object"){let q=z;if(typeof q.request_id==="string")return q.request_id;if(typeof q.detail==="string"){let B=q.detail.match(/request[_ ]id[=:]?\s*([a-zA-Z0-9-_]+)/i);if(B)return B[1]}}}return null}var dY0=new Set(["checkpoint.completed","run.completed","run.failed"]),lY0=768,jc="fabro.run-files.diff-style",rY0=Tc;function aY0(){let[Z,Y]=i5.useState(!1);return i5.useEffect(()=>{if(typeof window>"u")return;let Q=window.matchMedia(`(max-width: ${lY0-1}px)`),z=()=>Y(Q.matches);return z(),Q.addEventListener("change",z),()=>Q.removeEventListener("change",z)},[]),Z}function sY0(Z){Uq(Z,{allowlist:dY0,debounceMs:500})}function oY0(Z,Y){let Q=!!Z&&(!!Z.to_sha_committed_at||Y!==null),[,z]=i5.useState(0);if(i5.useEffect(()=>{if(!Q)return;let $=setInterval(()=>z((U)=>U+1),1e4);return()=>clearInterval($)},[Q]),!Z)return null;let q=Date.now(),B=Z.to_sha_committed_at?`Captured ${Ec(Z.to_sha_committed_at,q)}`:null,W=Y?`Fetched ${Ec(new Date(Y).toISOString(),q)}`:null;if(Z.degraded&&B&&W)return`${B} · ${W}`;return B??W??null}function Ec(Z,Y){if(!Z)return"";let Q=Date.parse(Z);if(Number.isNaN(Q))return"";let z=Math.max(0,Math.floor((Y-Q)/1000));if(z<5)return"just now";if(z<60)return`${z}s ago`;let q=Math.floor(z/60);if(q<60)return`${q}m ago`;let B=Math.floor(q/60);if(B<24)return`${B}h ago`;return`${Math.floor(B/24)}d ago`}function iY0(){if(typeof window>"u")return"split";try{let Z=window.localStorage.getItem(jc);if(Z==="split"||Z==="unified")return Z}catch{}return"split"}function tY0(Z){if(typeof window>"u")return;try{window.localStorage.setItem(jc,Z)}catch{}}function $T(Z){return`run-file:${Z}`}function Ic(Z){if(!Z)return null;let Y=Z.startsWith("#")?Z.slice(1):Z,Q="file=";if(!Y.startsWith(Q))return null;try{return decodeURIComponent(Y.slice(Q.length))}catch{return null}}function Sc(Z,Y){return Z!==null&&Z>0&&Y===0?"No changes in this run.":null}function kc(Z,Y){if(!Z||!Y)return null;if(Y.meta.degraded&&Y.meta.patch)return{key:`patch-only:${Z}`,message:"File-level navigation isn't available in the patch-only view."};if(!Y.data.some((z)=>z.new_file.name===Z||z.old_file.name===Z))return{key:`missing:${Z}`,message:`File ${Z} is not in this run.`};return null}function nY0(Z,Y){return kc(Z,Y)?.message??null}function eY0(Z){for(let Y of Z){let Q=Y.data;if(!Q)continue;if(typeof Q?.run?.lifecycleStatus==="string")return Q.run.lifecycleStatus;if(typeof Q?.lifecycleStatus==="string")return Q.lifecycleStatus}return}function xc({loaderData:Z}){let Y=B7(),Q=NU(),z=h8(),q=fY(),{push:B}=YN(),W=Z,$=aY0(),U=eY0(q),M=i5.useRef(null),H=i5.useRef(null);i5.useEffect(()=>{if(!W?.data)return;let f=Sc(M.current?.data.length??null,W.data.data.length);if(f)B({message:f});M.current=W.data,H.current=Date.now()},[B,W?.data]);let O=W?.data??M.current;sY0(Y.id);let _=Q.state==="loading"&&!Z,A=z.state==="loading",P=W?.error&&M.current?`Couldn't refresh (${W.error.status}).`:null,L=W?.error&&!M.current?W.error:null,v=oY0(O?.meta??null,H.current),[R,T]=i5.useState(iY0),C=$?"unified":R,y=$,b=i5.useCallback((f)=>{if(y)return;T(f),tY0(f)},[y]),S=i5.useRef(null),I=i5.useRef(null),g=i5.useRef(null),l=i5.useRef(!1);i5.useEffect(()=>{if(l.current&&!A)S.current?.focus({preventScroll:!0});l.current=A},[A]);let k=O?.data.length??0;Cc(I,k);let[i,X0]=i5.useState(()=>{if(typeof window>"u")return null;return Ic(window.location.hash)});i5.useEffect(()=>{if(typeof window>"u")return;let f=()=>X0(Ic(window.location.hash));return window.addEventListener("hashchange",f),()=>window.removeEventListener("hashchange",f)},[]),i5.useEffect(()=>{let f=kc(i,O);if(f){if(g.current!==f.key)B({message:f.message,autoDismissMs:5000}),g.current=f.key;return}if(g.current=null,!i||!O)return;let z0=document.getElementById($T(i));if(z0)z0.scrollIntoView({block:"start",behavior:"smooth"}),z0.focus({preventScroll:!0})},[O,i,B]);let W0=i5.useCallback((f)=>f.map((z0,N0)=>{let s=z0.new_file.name||z0.old_file.name,H0=Vc(z0);if(H0)return W2.jsxDEV("div",{id:$T(s),tabIndex:-1,"data-run-file-row":"true",role:"region","aria-label":`${z0.change_kind??"changed"}: ${s}`,className:"focus:outline-2 focus:outline-focus focus:outline-offset-2 rounded-md",children:H0},`${s}-${N0}`,!1,void 0,this);let y0=!!i&&(z0.new_file.name===i||z0.old_file.name===i);return W2.jsxDEV("div",{id:$T(s),tabIndex:-1,"data-run-file-row":"true",role:"region","aria-label":`${z0.change_kind??"modified"}: ${z0.new_file.name}`,className:"focus:outline-2 focus:outline-focus focus:outline-offset-2 rounded-md",children:W2.jsxDEV(gY0,{oldFile:z0.old_file,newFile:z0.new_file,options:{diffStyle:C,theme:"pierre-dark",expandUnchanged:y0?!0:void 0}},void 0,!1,void 0,this)},`${z0.new_file.name}-${N0}`,!1,void 0,this)}),[C,i]);if(_)return W2.jsxDEV(Rc,{},void 0,!1,void 0,this);if(L)return WT({status:L.status,requestId:L.requestId,onRetry:()=>z.revalidate()});if(!O)return W2.jsxDEV(KT,{kind:BT({runStatus:U,totalChanged:0,degraded:!1})},void 0,!1,void 0,this);let{data:w0,meta:n}=O,Q0=M.current?.meta?.to_sha??null,$0=!!n.to_sha&&Q0!==null&&Q0===n.to_sha,e=W2.jsxDEV(Dc,{changeSummary:{totalChanged:n.total_changed,additions:n.stats.additions,deletions:n.stats.deletions},onRefresh:()=>z.revalidate(),refreshing:A,refreshDisabled:$0,freshness:v,refreshButtonRef:S,diffStyle:C,onDiffStyleChange:b,diffStyleForced:y},void 0,!1,void 0,this);if(n.degraded&&n.patch)return W2.jsxDEV("div",{ref:I,className:"flex flex-col gap-4",children:[e,P?W2.jsxDEV(cW,{message:P,onRetry:()=>z.revalidate()},void 0,!1,void 0,this):null,W2.jsxDEV(Lc,{reason:n.degraded_reason},void 0,!1,void 0,this),W2.jsxDEV(mY0,{patch:n.patch,options:{diffStyle:C,theme:"pierre-dark"}},void 0,!1,void 0,this)]},void 0,!0,void 0,this);if(w0.length===0)return W2.jsxDEV("div",{ref:I,className:"flex flex-col gap-4",children:[e,W2.jsxDEV(KT,{kind:BT({runStatus:U,totalChanged:n.total_changed,degraded:n.degraded??!1})},void 0,!1,void 0,this)]},void 0,!0,void 0,this);let q0=w0.length>20?W2.jsxDEV(uY0,{children:W0(w0)},void 0,!1,void 0,this):W2.jsxDEV(W2.Fragment,{children:W0(w0)},void 0,!1,void 0,this);return W2.jsxDEV("div",{ref:I,className:"flex flex-col gap-4",children:[e,P?W2.jsxDEV(cW,{message:P,onRetry:()=>z.revalidate()},void 0,!1,void 0,this):null,q0]},void 0,!0,void 0,this)}var UT={};p5(UT,{loader:()=>ZJ0,default:()=>fc});var n0=h(E0(),1);function oZ(Z){return`${(Z/1000).toFixed(1)}k`}function wN(Z){return Z==null?"-":`$${(Z/1e6).toFixed(2)}`}async function ZJ0({request:Z,params:Y}){let Q=await h4(`/runs/${Y.id}/billing`,{request:Z}),z=Q.stages.map((M)=>({stage:M.stage.name,model:M.model.id,inputTokens:M.billing.input_tokens,outputTokens:M.billing.output_tokens+(M.billing.reasoning_tokens??0),runtime:b7(M.runtime_secs),totalUsdMicros:M.billing.total_usd_micros})),q=b7(Q.totals.runtime_secs),B=Q.totals.input_tokens,W=Q.totals.output_tokens+(Q.totals.reasoning_tokens??0),$=Q.totals.total_usd_micros,U=Q.by_model.map((M)=>({model:M.model.id,stages:M.stages,inputTokens:M.billing.input_tokens,outputTokens:M.billing.output_tokens+(M.billing.reasoning_tokens??0),totalUsdMicros:M.billing.total_usd_micros})).sort((M,H)=>(H.totalUsdMicros??-1)-(M.totalUsdMicros??-1));return{stages:z,totalRuntime:q,totalUsdMicros:$,totalInput:B,totalOutput:W,modelBreakdown:U}}function fc({loaderData:Z}){let{stages:Y,totalRuntime:Q,totalUsdMicros:z,totalInput:q,totalOutput:B,modelBreakdown:W}=Z;if(!Y.length)return n0.jsxDEV("div",{className:"py-12",children:n0.jsxDEV(g8,{title:"No billing yet",description:"Token usage and cost will appear here once stages complete."},void 0,!1,void 0,this)},void 0,!1,void 0,this);return n0.jsxDEV("div",{className:"space-y-6",children:[n0.jsxDEV("div",{className:"overflow-hidden rounded-md border border-line",children:n0.jsxDEV("table",{className:"w-full text-sm",children:[n0.jsxDEV("thead",{children:n0.jsxDEV("tr",{className:"border-b border-line bg-panel/60 text-left text-xs font-medium text-fg-3",children:[n0.jsxDEV("th",{className:"px-4 py-2.5 font-medium",children:"Stage"},void 0,!1,void 0,this),n0.jsxDEV("th",{className:"px-4 py-2.5 font-medium",children:"Model"},void 0,!1,void 0,this),n0.jsxDEV("th",{className:"px-4 py-2.5 font-medium text-right",children:"Tokens"},void 0,!1,void 0,this),n0.jsxDEV("th",{className:"px-4 py-2.5 font-medium text-right",children:"Run time"},void 0,!1,void 0,this),n0.jsxDEV("th",{className:"px-4 py-2.5 font-medium text-right",children:"Billing"},void 0,!1,void 0,this)]},void 0,!0,void 0,this)},void 0,!1,void 0,this),n0.jsxDEV("tbody",{children:Y.map(($)=>n0.jsxDEV("tr",{className:"border-b border-line last:border-b-0",children:[n0.jsxDEV("td",{className:"px-4 py-3 text-fg-2",children:$.stage},void 0,!1,void 0,this),n0.jsxDEV("td",{className:"px-4 py-3 font-mono text-xs text-fg-3",children:$.model},void 0,!1,void 0,this),n0.jsxDEV("td",{className:"px-4 py-3 text-right font-mono text-xs tabular-nums text-fg-3",children:[oZ($.inputTokens)," ",n0.jsxDEV("span",{className:"text-fg-muted",children:"/"},void 0,!1,void 0,this)," ",oZ($.outputTokens)]},void 0,!0,void 0,this),n0.jsxDEV("td",{className:"px-4 py-3 text-right font-mono text-xs text-fg-3",children:$.runtime},void 0,!1,void 0,this),n0.jsxDEV("td",{className:"px-4 py-3 text-right font-mono text-xs text-fg-3",children:wN($.totalUsdMicros)},void 0,!1,void 0,this)]},$.stage,!0,void 0,this))},void 0,!1,void 0,this),n0.jsxDEV("tfoot",{children:n0.jsxDEV("tr",{className:"border-t border-line-strong bg-overlay",children:[n0.jsxDEV("td",{className:"px-4 py-3 font-medium text-fg",children:"Total"},void 0,!1,void 0,this),n0.jsxDEV("td",{},void 0,!1,void 0,this),n0.jsxDEV("td",{className:"px-4 py-3 text-right font-mono text-xs tabular-nums font-medium text-fg",children:[oZ(q)," ",n0.jsxDEV("span",{className:"text-fg-muted",children:"/"},void 0,!1,void 0,this)," ",oZ(B)]},void 0,!0,void 0,this),n0.jsxDEV("td",{className:"px-4 py-3 text-right font-mono text-xs font-medium text-fg",children:Q},void 0,!1,void 0,this),n0.jsxDEV("td",{className:"px-4 py-3 text-right font-mono text-xs font-medium text-fg",children:wN(z)},void 0,!1,void 0,this)]},void 0,!0,void 0,this)},void 0,!1,void 0,this)]},void 0,!0,void 0,this)},void 0,!1,void 0,this),n0.jsxDEV("div",{children:[n0.jsxDEV("h3",{className:"mb-3 text-sm font-semibold text-fg",children:"By model"},void 0,!1,void 0,this),n0.jsxDEV("div",{className:"overflow-hidden rounded-md border border-line",children:n0.jsxDEV("table",{className:"w-full text-sm",children:[n0.jsxDEV("thead",{children:n0.jsxDEV("tr",{className:"border-b border-line bg-panel/60 text-left text-xs font-medium text-fg-3",children:[n0.jsxDEV("th",{className:"px-4 py-2.5 font-medium",children:"Model"},void 0,!1,void 0,this),n0.jsxDEV("th",{className:"px-4 py-2.5 font-medium text-right",children:"Stages"},void 0,!1,void 0,this),n0.jsxDEV("th",{className:"px-4 py-2.5 font-medium text-right",children:"Tokens"},void 0,!1,void 0,this),n0.jsxDEV("th",{className:"px-4 py-2.5 font-medium text-right",children:"Billing"},void 0,!1,void 0,this)]},void 0,!0,void 0,this)},void 0,!1,void 0,this),n0.jsxDEV("tbody",{children:W.map(($)=>n0.jsxDEV("tr",{className:"border-b border-line last:border-b-0",children:[n0.jsxDEV("td",{className:"px-4 py-3 font-mono text-xs text-fg-2",children:$.model},void 0,!1,void 0,this),n0.jsxDEV("td",{className:"px-4 py-3 text-right font-mono text-xs tabular-nums text-fg-3",children:$.stages},void 0,!1,void 0,this),n0.jsxDEV("td",{className:"px-4 py-3 text-right font-mono text-xs tabular-nums text-fg-3",children:[oZ($.inputTokens)," ",n0.jsxDEV("span",{className:"text-fg-muted",children:"/"},void 0,!1,void 0,this)," ",oZ($.outputTokens)]},void 0,!0,void 0,this),n0.jsxDEV("td",{className:"px-4 py-3 text-right font-mono text-xs text-fg-3",children:wN($.totalUsdMicros)},void 0,!1,void 0,this)]},$.model,!0,void 0,this))},void 0,!1,void 0,this),n0.jsxDEV("tfoot",{children:n0.jsxDEV("tr",{className:"border-t border-line-strong bg-overlay",children:[n0.jsxDEV("td",{className:"px-4 py-3 font-medium text-fg",children:"Total"},void 0,!1,void 0,this),n0.jsxDEV("td",{className:"px-4 py-3 text-right font-mono text-xs tabular-nums font-medium text-fg",children:Y.length},void 0,!1,void 0,this),n0.jsxDEV("td",{className:"px-4 py-3 text-right font-mono text-xs tabular-nums font-medium text-fg",children:[oZ(q)," ",n0.jsxDEV("span",{className:"text-fg-muted",children:"/"},void 0,!1,void 0,this)," ",oZ(B)]},void 0,!0,void 0,this),n0.jsxDEV("td",{className:"px-4 py-3 text-right font-mono text-xs font-medium text-fg",children:wN(z)},void 0,!1,void 0,this)]},void 0,!0,void 0,this)},void 0,!1,void 0,this)]},void 0,!0,void 0,this)},void 0,!1,void 0,this)]},void 0,!0,void 0,this)]},void 0,!0,void 0,this)}var MT={};p5(MT,{meta:()=>YJ0,loader:()=>QJ0,handle:()=>JJ0,default:()=>hc});var L2=h(E0(),1);function YJ0({}){return[{title:"Insights — Fabro"}]}var JJ0={wide:!0};async function QJ0({request:Z}){let[{data:Y},{data:Q}]=await Promise.all([h4("/insights/queries",{request:Z}),h4("/insights/history",{request:Z})]),z=Y.map((B)=>({id:B.id,name:B.name,sql:B.sql})),q=Q.map((B)=>({id:B.id,sql:B.sql,timestamp:B.timestamp,elapsed:B.elapsed,rowsReturned:B.row_count}));return{savedQueries:z,historyEntries:q}}function hc({loaderData:Z}){let{savedQueries:Y,historyEntries:Q}=Z,z=x4();return L2.jsxDEV("div",{className:"flex gap-6",children:[L2.jsxDEV("div",{className:"w-56 shrink-0",children:L2.jsxDEV("div",{className:"sticky top-6 space-y-4",children:[L2.jsxDEV(l1,{to:"/insights/new",className:"inline-flex w-full items-center justify-center gap-1.5 rounded-md border border-line bg-panel/80 px-3 py-2 text-sm font-medium text-fg-3 transition-colors hover:border-line-strong hover:bg-panel hover:text-fg",children:[L2.jsxDEV(vV,{className:"size-3.5"},void 0,!1,void 0,this),"New Query"]},void 0,!0,void 0,this),L2.jsxDEV("div",{children:[L2.jsxDEV("h3",{className:"mb-2 text-[11px] font-semibold uppercase tracking-wider text-fg-muted",children:"Saved Queries"},void 0,!1,void 0,this),L2.jsxDEV("div",{className:"space-y-0.5",children:Y.map((q)=>L2.jsxDEV("button",{type:"button",onClick:()=>{z("/insights",{state:{sql:q.sql,name:q.name}})},className:"flex w-full flex-col gap-0.5 rounded-md px-2.5 py-2 text-left transition-colors hover:bg-overlay",children:[L2.jsxDEV("span",{className:"text-sm font-medium text-fg-2",children:q.name},void 0,!1,void 0,this),L2.jsxDEV("span",{className:"truncate font-mono text-[10px] text-fg-muted",children:q.sql.split(` `)[0]},void 0,!1,void 0,this)]},q.id,!0,void 0,this))},void 0,!1,void 0,this)]},void 0,!0,void 0,this),L2.jsxDEV("div",{children:[L2.jsxDEV("h3",{className:"mb-2 text-[11px] font-semibold uppercase tracking-wider text-fg-muted",children:"History"},void 0,!1,void 0,this),L2.jsxDEV("div",{className:"space-y-0.5",children:Q.map((q)=>L2.jsxDEV("button",{type:"button",onClick:()=>{z("/insights",{state:{sql:q.sql}})},className:"flex w-full flex-col gap-0.5 rounded-md px-2.5 py-2 text-left transition-colors hover:bg-overlay",children:[L2.jsxDEV("span",{className:"truncate font-mono text-[10px] text-fg-3",children:q.sql},void 0,!1,void 0,this),L2.jsxDEV("span",{className:"font-mono text-[10px] text-fg-muted",children:[oM(q.timestamp)," · ",q.rowsReturned," rows"]},void 0,!0,void 0,this)]},q.id,!0,void 0,this))},void 0,!1,void 0,this)]},void 0,!0,void 0,this)]},void 0,!0,void 0,this)},void 0,!1,void 0,this),L2.jsxDEV("div",{className:"min-w-0 flex-1",children:L2.jsxDEV(u6,{},void 0,!1,void 0,this)},void 0,!1,void 0,this)]},void 0,!0,void 0,this)}var wT={};p5(wT,{default:()=>mc});var d5=h(Y0(),1);var D0=h(E0(),1);function XJ0(Z){let Y=Z.toLowerCase();if(Y.includes("workflow_name")&&Y.includes("avg"))return{columns:["workflow_name","avg_duration","run_count"],rows:[{workflow_name:"Expand Product",avg_duration:342.5,run_count:48},{workflow_name:"Implement Feature",avg_duration:287.3,run_count:156},{workflow_name:"Security Scan",avg_duration:198.1,run_count:312},{workflow_name:"Fix Build",avg_duration:145.7,run_count:482},{workflow_name:"Sync Drift",avg_duration:89.2,run_count:94},{workflow_name:"Dependency Audit",avg_duration:67.4,run_count:201}],elapsed:0.531,rowsRead:5182366,bytesRead:357780000,rowsReturned:6};if(Y.includes("failure_rate")||Y.includes("failed"))return{columns:["day","failures","total","failure_rate"],rows:Array.from({length:14},(Q,z)=>{let q=new Date;q.setDate(q.getDate()-z);let B=80+Math.floor(Math.random()*60),W=Math.floor(Math.random()*15);return{day:q.toISOString().slice(0,10),failures:W,total:B,failure_rate:Math.round(1000*W/B)/10}}),elapsed:0.287,rowsRead:2841092,bytesRead:198400000,rowsReturned:14};return{columns:["repo","runs","total_additions","total_deletions"],rows:[{repo:"fabro-engine",runs:482,total_additions:28450,total_deletions:12300},{repo:"fabro-web",runs:356,total_additions:19200,total_deletions:8900},{repo:"fabro-cli",runs:198,total_additions:8700,total_deletions:4200},{repo:"fabro-docs",runs:145,total_additions:12100,total_deletions:3400},{repo:"fabro-sdk",runs:89,total_additions:5600,total_deletions:2100},{repo:"fabro-infra",runs:67,total_additions:3200,total_deletions:1800},{repo:"fabro-actions",runs:42,total_additions:2100,total_deletions:980},{repo:"fabro-proto",runs:28,total_additions:1400,total_deletions:650}],elapsed:0.148,rowsRead:1204588,bytesRead:89200000,rowsReturned:8}}function zJ0(Z){if(Z>=1e9)return`${(Z/1e9).toFixed(2)} GB`;if(Z>=1e6)return`${(Z/1e6).toFixed(2)} MB`;if(Z>=1000)return`${(Z/1000).toFixed(2)} KB`;return`${Z} B`}function HN(Z){return Z.toLocaleString()}var gc=["rgba(90, 200, 168, 0.85)","rgba(103, 178, 215, 0.85)","rgba(181, 221, 239, 0.65)","rgba(240, 164, 91, 0.75)"];function qJ0({result:Z}){let Y=d5.useRef(null),[Q,z]=d5.useState(0);d5.useEffect(()=>{let T=Y.current;if(!T)return;let C=new ResizeObserver((y)=>{let b=y[0];if(b)z(b.contentRect.width)});return C.observe(T),()=>C.disconnect()},[]);let q=Z.columns[0],B=Z.columns.slice(1).filter((T)=>{return typeof Z.rows[0]?.[T]==="number"});if(B.length===0||Z.rows.length===0)return D0.jsxDEV("div",{className:"flex h-64 items-center justify-center text-sm text-fg-muted",children:"No numeric columns to chart"},void 0,!1,void 0,this);let W=B[0],$=Math.max(...Z.rows.map((T)=>{let C=T[W];return typeof C==="number"?C:0})),U=260,M=52,H={top:12,bottom:48,right:16},O=U-H.top-H.bottom,_=Q-M-H.right,A=Z.rows.length,P=Math.max(8,Math.min(16,_/A*0.3)),L=Math.max(12,(_-P*(A+1))/A),v=5,R=Array.from({length:v},(T,C)=>Math.round($*(v-1-C)/(v-1)));return D0.jsxDEV("div",{ref:Y,children:[Q>0&&D0.jsxDEV("svg",{width:Q,height:U,className:"select-none",children:[R.map((T,C)=>{let y=H.top+O*C/(v-1);return D0.jsxDEV("g",{children:[D0.jsxDEV("line",{x1:M,y1:y,x2:Q-H.right,y2:y,stroke:"var(--chart-gridline)",strokeDasharray:"4,4"},void 0,!1,void 0,this),D0.jsxDEV("text",{x:M-8,y:y+4,textAnchor:"end",fill:"var(--chart-axis-label)",fontSize:"11",fontFamily:"JetBrains Mono, monospace",children:T>=1000?`${(T/1000).toFixed(T>=1e4?0:1)}k`:T},void 0,!1,void 0,this)]},T,!0,void 0,this)}),Z.rows.map((T,C)=>{let y=T[W],b=typeof y==="number"?y:0,S=$>0?b/$*O:0,I=M+P+C*(L+P),g=H.top+O-S,l=String(T[q]),k=C%gc.length,i=Math.floor(L/6);return D0.jsxDEV("g",{children:[D0.jsxDEV("rect",{x:I,y:g,width:L,height:S,rx:3,fill:gc[k],className:"transition-opacity hover:opacity-100",opacity:0.85},void 0,!1,void 0,this),D0.jsxDEV("title",{children:[l,": ",HN(b)]},void 0,!0,void 0,this),D0.jsxDEV("text",{x:I+L/2,y:U-H.bottom+16,textAnchor:"middle",fill:"var(--chart-axis-label)",fontSize:"10",fontFamily:"JetBrains Mono, monospace",children:l.length>i?l.slice(0,Math.max(3,i-1))+"…":l},void 0,!1,void 0,this)]},l+C,!0,void 0,this)})]},void 0,!0,void 0,this),D0.jsxDEV("div",{className:"mt-1 flex items-center gap-4 px-2",children:D0.jsxDEV("span",{className:"font-mono text-[10px] uppercase tracking-wider text-fg-muted",children:W.replace(/_/g," ")},void 0,!1,void 0,this)},void 0,!1,void 0,this)]},void 0,!0,void 0,this)}function KJ0({result:Z}){return D0.jsxDEV("div",{className:"overflow-x-auto",children:D0.jsxDEV("table",{className:"w-full text-left",children:[D0.jsxDEV("thead",{children:D0.jsxDEV("tr",{className:"border-b border-line",children:Z.columns.map((Y)=>D0.jsxDEV("th",{className:"whitespace-nowrap px-3 py-2.5 font-mono text-[11px] font-semibold uppercase tracking-wider text-fg-muted",children:Y},Y,!1,void 0,this))},void 0,!1,void 0,this)},void 0,!1,void 0,this),D0.jsxDEV("tbody",{children:Z.rows.map((Y)=>D0.jsxDEV("tr",{className:"border-b border-line transition-colors hover:bg-overlay",children:Z.columns.map((Q)=>{let z=Y[Q],q=typeof z==="number";return D0.jsxDEV("td",{className:`whitespace-nowrap px-3 py-2 font-mono text-xs ${q?"tabular-nums text-fg-2":"text-fg-3"}`,children:q?HN(z):String(z)},Q,!1,void 0,this)})},String(Y[Z.columns[0]]),!1,void 0,this))},void 0,!1,void 0,this)]},void 0,!0,void 0,this)},void 0,!1,void 0,this)}function BJ0({value:Z,onChange:Y,onRun:Q}){let z=d5.useRef(null),q=d5.useRef(null),B=Z.split(` `).length,W=d5.useCallback(()=>{if(z.current&&q.current)q.current.scrollTop=z.current.scrollTop},[]),$=(U)=>{if(U.key==="Enter"&&(U.metaKey||U.ctrlKey)){U.preventDefault(),Q();return}if(U.key==="Tab"){U.preventDefault();let M=U.currentTarget,H=M.selectionStart,O=M.selectionEnd,_=Z.slice(0,H)+" "+Z.slice(O);Y(_),requestAnimationFrame(()=>{M.selectionStart=H+2,M.selectionEnd=H+2})}};return D0.jsxDEV("div",{className:"relative flex overflow-hidden rounded-md border border-line bg-page/80 font-mono text-sm",children:[D0.jsxDEV("div",{ref:q,className:"pointer-events-none flex shrink-0 flex-col overflow-hidden border-r border-line bg-page/60 px-3 py-3 text-right leading-[1.625rem] text-fg-muted select-none","aria-hidden":"true",children:Array.from({length:B},(U,M)=>D0.jsxDEV("span",{className:"text-[11px]",children:M+1},M+1,!1,void 0,this))},void 0,!1,void 0,this),D0.jsxDEV("textarea",{ref:z,value:Z,onChange:(U)=>Y(U.target.value),onScroll:W,onKeyDown:$,spellCheck:!1,autoCapitalize:"off",autoCorrect:"off",className:"min-h-[7lh] w-full resize-y bg-transparent px-4 py-3 leading-[1.625rem] text-fg-2 placeholder-fg-muted outline-none",placeholder:"SELECT * FROM runs LIMIT 10"},void 0,!1,void 0,this)]},void 0,!0,void 0,this)}var WJ0=`SELECT workflow_name, AVG(duration_seconds) as avg_duration, COUNT(*) as run_count diff --git a/lib/crates/fabro-spa/assets/index.html b/lib/crates/fabro-spa/assets/index.html index 6db9788bd..6f5b647aa 100644 --- a/lib/crates/fabro-spa/assets/index.html +++ b/lib/crates/fabro-spa/assets/index.html @@ -58,7 +58,7 @@ - + diff --git a/lib/crates/fabro-workflow/src/sandbox_git.rs b/lib/crates/fabro-workflow/src/sandbox_git.rs index c916d1687..67d18f29a 100644 --- a/lib/crates/fabro-workflow/src/sandbox_git.rs +++ b/lib/crates/fabro-workflow/src/sandbox_git.rs @@ -1,3 +1,4 @@ +use std::collections::{HashMap, HashSet}; use std::path::Path; use fabro_agent::Sandbox; @@ -552,24 +553,26 @@ fn classify_entry( }) } -/// Output of `git diff --numstat`: which paths are binary, plus aggregate -/// `+/-` line totals across the text files in the range. Both pieces come -/// from a single git invocation so callers don't need to run two diffs. -#[derive(Debug, Default, Clone)] +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub struct DiffLineStats { + pub additions: u64, + pub deletions: u64, +} + +/// Output of `git diff --numstat`: which paths are binary, plus per-path +/// `+/-` line totals for text files in the range. Both pieces come from a +/// single git invocation so callers don't need to run two diffs. +#[derive(Debug, Default)] pub struct DiffNumstat { /// Repo-relative paths (post-rename) that git classifies as binary. - pub binary_paths: std::collections::HashSet, - /// Sum of `+` columns across text files. Binary entries (`-\t-`) do not - /// contribute. - pub additions: u64, - /// Sum of `-` columns across text files. Binary entries (`-\t-`) do not - /// contribute. - pub deletions: u64, + pub binary_paths: HashSet, + /// Repo-relative paths (post-rename) to line stats for text files. + pub line_stats_by_path: HashMap, } /// Run `git diff --numstat` once and return both the set of binary paths and -/// the aggregate text-file `+/-` totals. The single call replaces the -/// previous binary-only helper. +/// text-file `+/-` totals. The single call replaces the previous binary-only +/// helper. pub async fn list_diff_numstat( sandbox: &dyn Sandbox, base_sha: &str, @@ -611,14 +614,20 @@ pub async fn list_diff_numstat( let mut parts = line.splitn(3, '\t'); let adds_s = parts.next().unwrap_or(""); let dels_s = parts.next().unwrap_or(""); + let Some(path_s) = parts.next() else { + continue; + }; let Ok(adds) = adds_s.parse::() else { continue; }; let Ok(dels) = dels_s.parse::() else { continue; }; - out.additions = out.additions.saturating_add(adds); - out.deletions = out.deletions.saturating_add(dels); + let path = extract_new_path_from_numstat(path_s); + out.line_stats_by_path.insert(path, DiffLineStats { + additions: adds, + deletions: dels, + }); } Ok(out) } @@ -1264,8 +1273,9 @@ mod tests { "binary_paths: {:?}", stats.binary_paths ); - assert_eq!(stats.additions, 3, "additions: {stats:?}"); - assert_eq!(stats.deletions, 2, "deletions: {stats:?}"); + let doc_stats = stats.line_stats_by_path.get("doc.md").unwrap(); + assert_eq!(doc_stats.additions, 3, "additions: {stats:?}"); + assert_eq!(doc_stats.deletions, 2, "deletions: {stats:?}"); } #[tokio::test] diff --git a/lib/packages/fabro-api-client/src/api/runs-api.ts b/lib/packages/fabro-api-client/src/api/runs-api.ts index e5efefcbd..81b94fca0 100644 --- a/lib/packages/fabro-api-client/src/api/runs-api.ts +++ b/lib/packages/fabro-api-client/src/api/runs-api.ts @@ -5,7 +5,7 @@ * HTTP API for managing Fabro workflow run executions. * * The version of the OpenAPI document: 0.1.0 - * + * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech @@ -67,7 +67,7 @@ import type { TimelineEntryResponse } from '../models'; export const RunsApiAxiosParamCreator = function (configuration?: Configuration) { return { /** - * Marks a terminal run (`succeeded`, `failed`, or `dead`) as `archived`. Archived runs are hidden from default listings and are read-only until unarchived. Idempotent on already-archived runs. Returns 409 if the run is not terminal. + * Marks a terminal run (`succeeded`, `failed`, or `dead`) as `archived`. Archived runs are hidden from default listings and are read-only until unarchived. Idempotent on already-archived runs. Returns 409 if the run is not terminal. * @summary Archive Run * @param {string} id Unique run identifier (ULID). * @param {*} [options] Override http request option. @@ -189,7 +189,7 @@ export const RunsApiAxiosParamCreator = function (configuration?: Configuration) /** * Creates a new workflow run in `submitted` status from a self-contained manifest. * @summary Create Run - * @param {RunManifest} runManifest + * @param {RunManifest} runManifest * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -231,7 +231,7 @@ export const RunsApiAxiosParamCreator = function (configuration?: Configuration) * Creates a pull request for a completed run on GitHub and persists the record on the server. * @summary Create Run Pull Request * @param {string} id Unique run identifier (ULID). - * @param {CreateRunPullRequestRequest} createRunPullRequestRequest + * @param {CreateRunPullRequestRequest} createRunPullRequestRequest * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -318,10 +318,10 @@ export const RunsApiAxiosParamCreator = function (configuration?: Configuration) }; }, /** - * Creates a new run from a checkpoint of the source run. The source run is left untouched. + * Creates a new run from a checkpoint of the source run. The source run is left untouched. * @summary Fork Run * @param {string} id Unique run identifier (ULID). - * @param {ForkRequest} [forkRequest] + * @param {ForkRequest} [forkRequest] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -401,7 +401,7 @@ export const RunsApiAxiosParamCreator = function (configuration?: Configuration) }; }, /** - * Returns checkpoint timeline entries read from the run metadata branch. This endpoint does not rebuild missing metadata branches. + * Returns checkpoint timeline entries read from the run metadata branch. This endpoint does not rebuild missing metadata branches. * @summary Get Run Timeline * @param {string} id Unique run identifier (ULID). * @param {*} [options] Override http request option. @@ -541,7 +541,7 @@ export const RunsApiAxiosParamCreator = function (configuration?: Configuration) * Merges the stored pull request for a run on GitHub. * @summary Merge Run Pull Request * @param {string} id Unique run identifier (ULID). - * @param {MergeRunPullRequestRequest} mergeRunPullRequestRequest + * @param {MergeRunPullRequestRequest} mergeRunPullRequestRequest * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -625,7 +625,7 @@ export const RunsApiAxiosParamCreator = function (configuration?: Configuration) /** * Validates and renders a workflow manifest as SVG without creating a run. * @summary Render Workflow Graph - * @param {RenderWorkflowGraphRequest} renderWorkflowGraphRequest + * @param {RenderWorkflowGraphRequest} renderWorkflowGraphRequest * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -787,10 +787,10 @@ export const RunsApiAxiosParamCreator = function (configuration?: Configuration) }; }, /** - * Creates a new run from an earlier checkpoint of a terminal source run, archives the source run, and records `run.superseded_by` on the source after archive succeeds. Returns 207 when the new run was created but the source archive step failed. + * Creates a new run from an earlier checkpoint of a terminal source run, archives the source run, and records `run.superseded_by` on the source after archive succeeds. Returns 207 when the new run was created but the source archive step failed. * @summary Rewind Run * @param {string} id Unique run identifier (ULID). - * @param {RewindRequest} [rewindRequest] + * @param {RewindRequest} [rewindRequest] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -832,7 +832,7 @@ export const RunsApiAxiosParamCreator = function (configuration?: Configuration) /** * Validates a workflow manifest without creating a run. * @summary Validate Workflow Manifest - * @param {RunManifest} runManifest + * @param {RunManifest} runManifest * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -874,7 +874,7 @@ export const RunsApiAxiosParamCreator = function (configuration?: Configuration) * Starts a submitted run, queuing it for execution. Provide `resume=true` to resume an interrupted run from checkpoint. Returns 409 if the run is not startable. * @summary Start Run * @param {string} id Unique run identifier (ULID). - * @param {StartRunRequest} [startRunRequest] + * @param {StartRunRequest} [startRunRequest] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -914,7 +914,7 @@ export const RunsApiAxiosParamCreator = function (configuration?: Configuration) }; }, /** - * Restores an archived run to its prior terminal status. Idempotent on runs that are terminal but not archived (returns the current status without emitting an event). Returns 409 if the run is active. + * Restores an archived run to its prior terminal status. Idempotent on runs that are terminal but not archived (returns the current status without emitting an event). Returns 409 if the run is active. * @summary Unarchive Run * @param {string} id Unique run identifier (ULID). * @param {*} [options] Override http request option. @@ -1003,7 +1003,7 @@ export const RunsApiFp = function(configuration?: Configuration) { const localVarAxiosParamCreator = RunsApiAxiosParamCreator(configuration) return { /** - * Marks a terminal run (`succeeded`, `failed`, or `dead`) as `archived`. Archived runs are hidden from default listings and are read-only until unarchived. Idempotent on already-archived runs. Returns 409 if the run is not terminal. + * Marks a terminal run (`succeeded`, `failed`, or `dead`) as `archived`. Archived runs are hidden from default listings and are read-only until unarchived. Idempotent on already-archived runs. Returns 409 if the run is not terminal. * @summary Archive Run * @param {string} id Unique run identifier (ULID). * @param {*} [options] Override http request option. @@ -1044,7 +1044,7 @@ export const RunsApiFp = function(configuration?: Configuration) { /** * Creates a new workflow run in `submitted` status from a self-contained manifest. * @summary Create Run - * @param {RunManifest} runManifest + * @param {RunManifest} runManifest * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -1058,7 +1058,7 @@ export const RunsApiFp = function(configuration?: Configuration) { * Creates a pull request for a completed run on GitHub and persists the record on the server. * @summary Create Run Pull Request * @param {string} id Unique run identifier (ULID). - * @param {CreateRunPullRequestRequest} createRunPullRequestRequest + * @param {CreateRunPullRequestRequest} createRunPullRequestRequest * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -1083,10 +1083,10 @@ export const RunsApiFp = function(configuration?: Configuration) { return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); }, /** - * Creates a new run from a checkpoint of the source run. The source run is left untouched. + * Creates a new run from a checkpoint of the source run. The source run is left untouched. * @summary Fork Run * @param {string} id Unique run identifier (ULID). - * @param {ForkRequest} [forkRequest] + * @param {ForkRequest} [forkRequest] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -1110,7 +1110,7 @@ export const RunsApiFp = function(configuration?: Configuration) { return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); }, /** - * Returns checkpoint timeline entries read from the run metadata branch. This endpoint does not rebuild missing metadata branches. + * Returns checkpoint timeline entries read from the run metadata branch. This endpoint does not rebuild missing metadata branches. * @summary Get Run Timeline * @param {string} id Unique run identifier (ULID). * @param {*} [options] Override http request option. @@ -1155,7 +1155,7 @@ export const RunsApiFp = function(configuration?: Configuration) { * Merges the stored pull request for a run on GitHub. * @summary Merge Run Pull Request * @param {string} id Unique run identifier (ULID). - * @param {MergeRunPullRequestRequest} mergeRunPullRequestRequest + * @param {MergeRunPullRequestRequest} mergeRunPullRequestRequest * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -1181,7 +1181,7 @@ export const RunsApiFp = function(configuration?: Configuration) { /** * Validates and renders a workflow manifest as SVG without creating a run. * @summary Render Workflow Graph - * @param {RenderWorkflowGraphRequest} renderWorkflowGraphRequest + * @param {RenderWorkflowGraphRequest} renderWorkflowGraphRequest * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -1231,10 +1231,10 @@ export const RunsApiFp = function(configuration?: Configuration) { return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); }, /** - * Creates a new run from an earlier checkpoint of a terminal source run, archives the source run, and records `run.superseded_by` on the source after archive succeeds. Returns 207 when the new run was created but the source archive step failed. + * Creates a new run from an earlier checkpoint of a terminal source run, archives the source run, and records `run.superseded_by` on the source after archive succeeds. Returns 207 when the new run was created but the source archive step failed. * @summary Rewind Run * @param {string} id Unique run identifier (ULID). - * @param {RewindRequest} [rewindRequest] + * @param {RewindRequest} [rewindRequest] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -1247,7 +1247,7 @@ export const RunsApiFp = function(configuration?: Configuration) { /** * Validates a workflow manifest without creating a run. * @summary Validate Workflow Manifest - * @param {RunManifest} runManifest + * @param {RunManifest} runManifest * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -1261,7 +1261,7 @@ export const RunsApiFp = function(configuration?: Configuration) { * Starts a submitted run, queuing it for execution. Provide `resume=true` to resume an interrupted run from checkpoint. Returns 409 if the run is not startable. * @summary Start Run * @param {string} id Unique run identifier (ULID). - * @param {StartRunRequest} [startRunRequest] + * @param {StartRunRequest} [startRunRequest] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -1272,7 +1272,7 @@ export const RunsApiFp = function(configuration?: Configuration) { return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); }, /** - * Restores an archived run to its prior terminal status. Idempotent on runs that are terminal but not archived (returns the current status without emitting an event). Returns 409 if the run is active. + * Restores an archived run to its prior terminal status. Idempotent on runs that are terminal but not archived (returns the current status without emitting an event). Returns 409 if the run is active. * @summary Unarchive Run * @param {string} id Unique run identifier (ULID). * @param {*} [options] Override http request option. @@ -1307,7 +1307,7 @@ export const RunsApiFactory = function (configuration?: Configuration, basePath? const localVarFp = RunsApiFp(configuration) return { /** - * Marks a terminal run (`succeeded`, `failed`, or `dead`) as `archived`. Archived runs are hidden from default listings and are read-only until unarchived. Idempotent on already-archived runs. Returns 409 if the run is not terminal. + * Marks a terminal run (`succeeded`, `failed`, or `dead`) as `archived`. Archived runs are hidden from default listings and are read-only until unarchived. Idempotent on already-archived runs. Returns 409 if the run is not terminal. * @summary Archive Run * @param {string} id Unique run identifier (ULID). * @param {*} [options] Override http request option. @@ -1339,7 +1339,7 @@ export const RunsApiFactory = function (configuration?: Configuration, basePath? /** * Creates a new workflow run in `submitted` status from a self-contained manifest. * @summary Create Run - * @param {RunManifest} runManifest + * @param {RunManifest} runManifest * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -1350,7 +1350,7 @@ export const RunsApiFactory = function (configuration?: Configuration, basePath? * Creates a pull request for a completed run on GitHub and persists the record on the server. * @summary Create Run Pull Request * @param {string} id Unique run identifier (ULID). - * @param {CreateRunPullRequestRequest} createRunPullRequestRequest + * @param {CreateRunPullRequestRequest} createRunPullRequestRequest * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -1369,10 +1369,10 @@ export const RunsApiFactory = function (configuration?: Configuration, basePath? return localVarFp.deleteRun(id, force, options).then((request) => request(axios, basePath)); }, /** - * Creates a new run from a checkpoint of the source run. The source run is left untouched. + * Creates a new run from a checkpoint of the source run. The source run is left untouched. * @summary Fork Run * @param {string} id Unique run identifier (ULID). - * @param {ForkRequest} [forkRequest] + * @param {ForkRequest} [forkRequest] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -1390,7 +1390,7 @@ export const RunsApiFactory = function (configuration?: Configuration, basePath? return localVarFp.getRunPullRequest(id, options).then((request) => request(axios, basePath)); }, /** - * Returns checkpoint timeline entries read from the run metadata branch. This endpoint does not rebuild missing metadata branches. + * Returns checkpoint timeline entries read from the run metadata branch. This endpoint does not rebuild missing metadata branches. * @summary Get Run Timeline * @param {string} id Unique run identifier (ULID). * @param {*} [options] Override http request option. @@ -1426,7 +1426,7 @@ export const RunsApiFactory = function (configuration?: Configuration, basePath? * Merges the stored pull request for a run on GitHub. * @summary Merge Run Pull Request * @param {string} id Unique run identifier (ULID). - * @param {MergeRunPullRequestRequest} mergeRunPullRequestRequest + * @param {MergeRunPullRequestRequest} mergeRunPullRequestRequest * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -1446,7 +1446,7 @@ export const RunsApiFactory = function (configuration?: Configuration, basePath? /** * Validates and renders a workflow manifest as SVG without creating a run. * @summary Render Workflow Graph - * @param {RenderWorkflowGraphRequest} renderWorkflowGraphRequest + * @param {RenderWorkflowGraphRequest} renderWorkflowGraphRequest * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -1484,10 +1484,10 @@ export const RunsApiFactory = function (configuration?: Configuration, basePath? return localVarFp.retrieveRunGraph(id, options).then((request) => request(axios, basePath)); }, /** - * Creates a new run from an earlier checkpoint of a terminal source run, archives the source run, and records `run.superseded_by` on the source after archive succeeds. Returns 207 when the new run was created but the source archive step failed. + * Creates a new run from an earlier checkpoint of a terminal source run, archives the source run, and records `run.superseded_by` on the source after archive succeeds. Returns 207 when the new run was created but the source archive step failed. * @summary Rewind Run * @param {string} id Unique run identifier (ULID). - * @param {RewindRequest} [rewindRequest] + * @param {RewindRequest} [rewindRequest] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -1497,7 +1497,7 @@ export const RunsApiFactory = function (configuration?: Configuration, basePath? /** * Validates a workflow manifest without creating a run. * @summary Validate Workflow Manifest - * @param {RunManifest} runManifest + * @param {RunManifest} runManifest * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -1508,7 +1508,7 @@ export const RunsApiFactory = function (configuration?: Configuration, basePath? * Starts a submitted run, queuing it for execution. Provide `resume=true` to resume an interrupted run from checkpoint. Returns 409 if the run is not startable. * @summary Start Run * @param {string} id Unique run identifier (ULID). - * @param {StartRunRequest} [startRunRequest] + * @param {StartRunRequest} [startRunRequest] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -1516,7 +1516,7 @@ export const RunsApiFactory = function (configuration?: Configuration, basePath? return localVarFp.startRun(id, startRunRequest, options).then((request) => request(axios, basePath)); }, /** - * Restores an archived run to its prior terminal status. Idempotent on runs that are terminal but not archived (returns the current status without emitting an event). Returns 409 if the run is active. + * Restores an archived run to its prior terminal status. Idempotent on runs that are terminal but not archived (returns the current status without emitting an event). Returns 409 if the run is active. * @summary Unarchive Run * @param {string} id Unique run identifier (ULID). * @param {*} [options] Override http request option. @@ -1543,7 +1543,7 @@ export const RunsApiFactory = function (configuration?: Configuration, basePath? */ export class RunsApi extends BaseAPI { /** - * Marks a terminal run (`succeeded`, `failed`, or `dead`) as `archived`. Archived runs are hidden from default listings and are read-only until unarchived. Idempotent on already-archived runs. Returns 409 if the run is not terminal. + * Marks a terminal run (`succeeded`, `failed`, or `dead`) as `archived`. Archived runs are hidden from default listings and are read-only until unarchived. Idempotent on already-archived runs. Returns 409 if the run is not terminal. * @summary Archive Run * @param {string} id Unique run identifier (ULID). * @param {*} [options] Override http request option. @@ -1578,7 +1578,7 @@ export class RunsApi extends BaseAPI { /** * Creates a new workflow run in `submitted` status from a self-contained manifest. * @summary Create Run - * @param {RunManifest} runManifest + * @param {RunManifest} runManifest * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -1590,7 +1590,7 @@ export class RunsApi extends BaseAPI { * Creates a pull request for a completed run on GitHub and persists the record on the server. * @summary Create Run Pull Request * @param {string} id Unique run identifier (ULID). - * @param {CreateRunPullRequestRequest} createRunPullRequestRequest + * @param {CreateRunPullRequestRequest} createRunPullRequestRequest * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -1611,10 +1611,10 @@ export class RunsApi extends BaseAPI { } /** - * Creates a new run from a checkpoint of the source run. The source run is left untouched. + * Creates a new run from a checkpoint of the source run. The source run is left untouched. * @summary Fork Run * @param {string} id Unique run identifier (ULID). - * @param {ForkRequest} [forkRequest] + * @param {ForkRequest} [forkRequest] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -1634,7 +1634,7 @@ export class RunsApi extends BaseAPI { } /** - * Returns checkpoint timeline entries read from the run metadata branch. This endpoint does not rebuild missing metadata branches. + * Returns checkpoint timeline entries read from the run metadata branch. This endpoint does not rebuild missing metadata branches. * @summary Get Run Timeline * @param {string} id Unique run identifier (ULID). * @param {*} [options] Override http request option. @@ -1673,7 +1673,7 @@ export class RunsApi extends BaseAPI { * Merges the stored pull request for a run on GitHub. * @summary Merge Run Pull Request * @param {string} id Unique run identifier (ULID). - * @param {MergeRunPullRequestRequest} mergeRunPullRequestRequest + * @param {MergeRunPullRequestRequest} mergeRunPullRequestRequest * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -1695,7 +1695,7 @@ export class RunsApi extends BaseAPI { /** * Validates and renders a workflow manifest as SVG without creating a run. * @summary Render Workflow Graph - * @param {RenderWorkflowGraphRequest} renderWorkflowGraphRequest + * @param {RenderWorkflowGraphRequest} renderWorkflowGraphRequest * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -1737,10 +1737,10 @@ export class RunsApi extends BaseAPI { } /** - * Creates a new run from an earlier checkpoint of a terminal source run, archives the source run, and records `run.superseded_by` on the source after archive succeeds. Returns 207 when the new run was created but the source archive step failed. + * Creates a new run from an earlier checkpoint of a terminal source run, archives the source run, and records `run.superseded_by` on the source after archive succeeds. Returns 207 when the new run was created but the source archive step failed. * @summary Rewind Run * @param {string} id Unique run identifier (ULID). - * @param {RewindRequest} [rewindRequest] + * @param {RewindRequest} [rewindRequest] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -1751,7 +1751,7 @@ export class RunsApi extends BaseAPI { /** * Validates a workflow manifest without creating a run. * @summary Validate Workflow Manifest - * @param {RunManifest} runManifest + * @param {RunManifest} runManifest * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -1763,7 +1763,7 @@ export class RunsApi extends BaseAPI { * Starts a submitted run, queuing it for execution. Provide `resume=true` to resume an interrupted run from checkpoint. Returns 409 if the run is not startable. * @summary Start Run * @param {string} id Unique run identifier (ULID). - * @param {StartRunRequest} [startRunRequest] + * @param {StartRunRequest} [startRunRequest] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -1772,7 +1772,7 @@ export class RunsApi extends BaseAPI { } /** - * Restores an archived run to its prior terminal status. Idempotent on runs that are terminal but not archived (returns the current status without emitting an event). Returns 409 if the run is active. + * Restores an archived run to its prior terminal status. Idempotent on runs that are terminal but not archived (returns the current status without emitting an event). Returns 409 if the run is active. * @summary Unarchive Run * @param {string} id Unique run identifier (ULID). * @param {*} [options] Override http request option. diff --git a/lib/packages/fabro-api-client/src/models/diff-stats.ts b/lib/packages/fabro-api-client/src/models/diff-stats.ts index 394ac9aa4..f1124f53c 100644 --- a/lib/packages/fabro-api-client/src/models/diff-stats.ts +++ b/lib/packages/fabro-api-client/src/models/diff-stats.ts @@ -15,7 +15,7 @@ /** - * Aggregate `+/-` line counts across all files in a diff (or the unified patch in degraded mode). Binary, sensitive, symlink, and submodule files contribute 0/0 since they have no line-level diff. Both fields are 0 for empty / pre-start envelopes. + * Aggregate `+/-` line counts across all files in a diff (or the unified patch in degraded mode). Binary, sensitive, symlink, and submodule files contribute 0/0 since they have no line-level diff. Both fields are 0 for empty / pre-start envelopes. */ export interface DiffStats { /** diff --git a/lib/packages/fabro-api-client/src/models/fork-request.ts b/lib/packages/fabro-api-client/src/models/fork-request.ts index 5da18402a..0b5b0b59f 100644 --- a/lib/packages/fabro-api-client/src/models/fork-request.ts +++ b/lib/packages/fabro-api-client/src/models/fork-request.ts @@ -5,7 +5,7 @@ * HTTP API for managing Fabro workflow run executions. * * The version of the OpenAPI document: 0.1.0 - * + * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech @@ -27,4 +27,3 @@ export interface ForkRequest { */ 'push'?: boolean | null; } - diff --git a/lib/packages/fabro-api-client/src/models/fork-response.ts b/lib/packages/fabro-api-client/src/models/fork-response.ts index 0b03a87dd..3099927c0 100644 --- a/lib/packages/fabro-api-client/src/models/fork-response.ts +++ b/lib/packages/fabro-api-client/src/models/fork-response.ts @@ -5,7 +5,7 @@ * HTTP API for managing Fabro workflow run executions. * * The version of the OpenAPI document: 0.1.0 - * + * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech @@ -22,4 +22,3 @@ export interface ForkResponse { 'new_run_id': string; 'target': string; } - diff --git a/lib/packages/fabro-api-client/src/models/install-object-store-input.ts b/lib/packages/fabro-api-client/src/models/install-object-store-input.ts index 1e6a940b0..8dd8f497d 100644 --- a/lib/packages/fabro-api-client/src/models/install-object-store-input.ts +++ b/lib/packages/fabro-api-client/src/models/install-object-store-input.ts @@ -5,7 +5,7 @@ * HTTP API for managing Fabro workflow run executions. * * The version of the OpenAPI document: 0.1.0 - * + * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech @@ -39,5 +39,3 @@ export const InstallObjectStoreInputCredentialModeEnum = { } as const; export type InstallObjectStoreInputCredentialModeEnum = typeof InstallObjectStoreInputCredentialModeEnum[keyof typeof InstallObjectStoreInputCredentialModeEnum]; - - diff --git a/lib/packages/fabro-api-client/src/models/install-object-store-summary.ts b/lib/packages/fabro-api-client/src/models/install-object-store-summary.ts index 7d473a448..47c32b0ba 100644 --- a/lib/packages/fabro-api-client/src/models/install-object-store-summary.ts +++ b/lib/packages/fabro-api-client/src/models/install-object-store-summary.ts @@ -5,7 +5,7 @@ * HTTP API for managing Fabro workflow run executions. * * The version of the OpenAPI document: 0.1.0 - * + * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech @@ -38,5 +38,3 @@ export const InstallObjectStoreSummaryCredentialModeEnum = { } as const; export type InstallObjectStoreSummaryCredentialModeEnum = typeof InstallObjectStoreSummaryCredentialModeEnum[keyof typeof InstallObjectStoreSummaryCredentialModeEnum]; - - diff --git a/lib/packages/fabro-api-client/src/models/install-prefill.ts b/lib/packages/fabro-api-client/src/models/install-prefill.ts index 7bdc45ef8..1c20eb029 100644 --- a/lib/packages/fabro-api-client/src/models/install-prefill.ts +++ b/lib/packages/fabro-api-client/src/models/install-prefill.ts @@ -5,7 +5,7 @@ * HTTP API for managing Fabro workflow run executions. * * The version of the OpenAPI document: 0.1.0 - * + * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech @@ -21,4 +21,3 @@ export interface InstallPrefill { 'canonical_url': string; 'object_store_local_root': string; } - diff --git a/lib/packages/fabro-api-client/src/models/rewind-request.ts b/lib/packages/fabro-api-client/src/models/rewind-request.ts index 438fd8b84..afcd3398d 100644 --- a/lib/packages/fabro-api-client/src/models/rewind-request.ts +++ b/lib/packages/fabro-api-client/src/models/rewind-request.ts @@ -5,7 +5,7 @@ * HTTP API for managing Fabro workflow run executions. * * The version of the OpenAPI document: 0.1.0 - * + * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech @@ -27,4 +27,3 @@ export interface RewindRequest { */ 'push'?: boolean | null; } - diff --git a/lib/packages/fabro-api-client/src/models/rewind-response.ts b/lib/packages/fabro-api-client/src/models/rewind-response.ts index a926b9b4c..527c0e62f 100644 --- a/lib/packages/fabro-api-client/src/models/rewind-response.ts +++ b/lib/packages/fabro-api-client/src/models/rewind-response.ts @@ -5,7 +5,7 @@ * HTTP API for managing Fabro workflow run executions. * * The version of the OpenAPI document: 0.1.0 - * + * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech @@ -24,4 +24,3 @@ export interface RewindResponse { 'archived': boolean; 'archive_error'?: string | null; } - diff --git a/lib/packages/fabro-api-client/src/models/run-superseded-by-props.ts b/lib/packages/fabro-api-client/src/models/run-superseded-by-props.ts index 4c9c64fbc..49cfa9fa6 100644 --- a/lib/packages/fabro-api-client/src/models/run-superseded-by-props.ts +++ b/lib/packages/fabro-api-client/src/models/run-superseded-by-props.ts @@ -5,7 +5,7 @@ * HTTP API for managing Fabro workflow run executions. * * The version of the OpenAPI document: 0.1.0 - * + * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech @@ -23,4 +23,3 @@ export interface RunSupersededByProps { 'target_node_id': string; 'target_visit': number; } - diff --git a/lib/packages/fabro-api-client/src/models/timeline-entry-response.ts b/lib/packages/fabro-api-client/src/models/timeline-entry-response.ts index e0d90c6c9..f0f233ac1 100644 --- a/lib/packages/fabro-api-client/src/models/timeline-entry-response.ts +++ b/lib/packages/fabro-api-client/src/models/timeline-entry-response.ts @@ -5,7 +5,7 @@ * HTTP API for managing Fabro workflow run executions. * * The version of the OpenAPI document: 0.1.0 - * + * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech @@ -23,4 +23,3 @@ export interface TimelineEntryResponse { 'visit': number; 'run_commit_sha'?: string | null; } -