Files
ai-agent/frontend/src/lib/diffRoute.ts
Gabriel Vidal 9258267fbb feat(ai-agent): uncommitted-changes diff link in the conversation detail panel
Replays the transcript's accumulated Write/Edit/MultiEdit toolUseResults (and
Bash rm deletes) into per-file before/after content, cut off at each repo's
last conversation commit, and serves it as /api/conversation-uncommitted-diff
(same DiffResult model, stats variant for the panel). The detail panel's
Commits card links it above the commit rows; /diff/uncommitted/<id> renders it
in the shared DiffView.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 17:17:38 +02:00

84 lines
3.3 KiB
TypeScript

import { useLocation } from "react-router-dom";
import { useCommitDiff, useConversationDiff, useUncommittedDiff } from "@/lib/queries";
import type { DiffResult } from "@/types";
/**
* The diff view is reachable three ways, all under `/diff/*`:
* - `/diff/commit/<kind>/<slug>/<sha>` — one commit in a repo
* - `/diff/conversation/<encodedId>` — a conversation's aggregated diff
* - `/diff/uncommitted/<encodedId>` — its not-yet-committed changes
* Both the page (`DiffView`) and the left-drawer file tree (`DiffFilesPanel`)
* parse the same path and read the same React Query, so the fetch is deduped.
*/
export type DiffRoute =
| { mode: "commit"; repo: string; sha: string; label: string }
| { mode: "conversation"; id: string }
| { mode: "uncommitted"; id: string }
| null;
export function parseDiffPath(pathname: string): DiffRoute {
const seg = pathname.split("/").filter(Boolean);
if (seg[0] !== "diff") return null;
if (seg[1] === "commit" && seg.length >= 5) {
const kind = seg[2];
const slug = decodeURIComponent(seg[3]);
const sha = decodeURIComponent(seg[4]);
return { mode: "commit", repo: `${kind}:${slug}`, sha, label: slug };
}
if (seg[1] === "conversation" && seg.length >= 3) {
return { mode: "conversation", id: decodeURIComponent(seg.slice(2).join("/")) };
}
if (seg[1] === "uncommitted" && seg.length >= 3) {
return { mode: "uncommitted", id: decodeURIComponent(seg.slice(2).join("/")) };
}
return null;
}
/** Build the URL to a single commit's diff page. */
export function commitDiffHref(kind: "project" | "service" | "super", slug: string, sha: string): string {
return `/diff/commit/${kind}/${encodeURIComponent(slug)}/${encodeURIComponent(sha)}`;
}
/** Build the URL to a repo token's commit diff page (token = `kind:slug`). */
export function repoTokenHref(repo: string, sha: string): string {
const [kind, ...rest] = repo.split(":");
return commitDiffHref(kind as "project" | "service" | "super", rest.join(":") || "_", sha);
}
/** Build the URL to a conversation's aggregated diff page. */
export function conversationDiffHref(id: string): string {
return `/diff/conversation/${encodeURIComponent(id)}`;
}
/** Build the URL to a conversation's uncommitted-changes diff page. */
export function uncommittedDiffHref(id: string): string {
return `/diff/uncommitted/${encodeURIComponent(id)}`;
}
/** Stable DOM id for a file's diff card, so the tree can scroll to it. */
export function fileAnchor(path: string): string {
return "f-" + path.replace(/[^a-zA-Z0-9]+/g, "-");
}
/** Resolve the current `/diff/*` route to its DiffResult query. */
export function useDiffRouteData(): {
route: DiffRoute;
data: DiffResult | undefined;
isLoading: boolean;
error: unknown;
} {
const { pathname } = useLocation();
const route = parseDiffPath(pathname);
const commit = useCommitDiff(
route?.mode === "commit" ? route.repo : "",
route?.mode === "commit" ? route.sha : "",
);
const convo = useConversationDiff(route?.mode === "conversation" ? route.id : "");
const uncommitted = useUncommittedDiff(route?.mode === "uncommitted" ? route.id : "");
const q =
route?.mode === "commit" ? commit
: route?.mode === "uncommitted" ? uncommitted
: convo;
return { route, data: q.data, isLoading: q.isLoading, error: q.error };
}