| author | |
| committer | |
| log | 582615971ef76e55f994e2fc0ccd9171277551c2 |
| tree | c6c9ee405de1041d998020792c7436646dd835b8 |
| parent | d2e6cfe10929eea55cbc70d6ffad8f7077b4d858 |
- Replace placeholder diff sidebar with real turn/file patch rendering
- Link assistant messages to changed-file summaries and open targeted diffs
- Persist diff selection state in the store and cover parsing/reducer behavior with tests
- Lazy-load `DiffPanel` to reduce initial bundle cost9 files changed, 680 insertions(+), 43 deletions(-)
apps/web/package.json+1| ... | ... | @@ -12,6 +12,7 @@ |
| 12 | 12 | }, |
| 13 | 13 | "dependencies": { |
| 14 | 14 | "@base-ui/react": "^1.2.0", |
| 15 | "@pierre/diffs": "^1.0.11", | |
| 15 | 16 | "@t3tools/contracts": "workspace:*", |
| 16 | 17 | "@tanstack/react-pacer": "^0.19.4", |
| 17 | 18 | "@tanstack/react-query": "^5.90.0", |
apps/web/src/App.tsx+14-3| ... | ... | @@ -1,8 +1,7 @@ |
| 1 | 1 | import { QueryClient, QueryClientProvider, useQueryClient } from "@tanstack/react-query"; |
| 2 | import { useEffect, useRef } from "react"; | |
| 2 | import { Suspense, lazy, useEffect, useRef } from "react"; | |
| 3 | 3 | |
| 4 | 4 | import ChatView from "./components/ChatView"; |
| 5 | import DiffPanel from "./components/DiffPanel"; | |
| 6 | 5 | import Sidebar from "./components/Sidebar"; |
| 7 | 6 | import { isElectron } from "./env"; |
| 8 | 7 | import { DEFAULT_MODEL } from "./model-logic"; |
| ... | ... | @@ -13,6 +12,8 @@ import { useNativeApi } from "./hooks/useNativeApi"; |
| 13 | 12 | import { AnchoredToastProvider, ToastProvider } from "./components/ui/toast"; |
| 14 | 13 | import { invalidateGitQueries } from "./lib/gitReactQuery"; |
| 15 | 14 | |
| 15 | const DiffPanel = lazy(() => import("./components/DiffPanel")); | |
| 16 | ||
| 16 | 17 | function EventRouter() { |
| 17 | 18 | const api = useNativeApi(); |
| 18 | 19 | const { dispatch } = useStore(); |
| ... | ... | @@ -195,7 +196,17 @@ function Layout() { |
| 195 | 196 | <DesktopProjectBootstrap /> |
| 196 | 197 | <Sidebar /> |
| 197 | 198 | <ChatView /> |
| 198 | {state.diffOpen && <DiffPanel />} | |
| 199 | {state.diffOpen && ( | |
| 200 | <Suspense | |
| 201 | fallback={ | |
| 202 | <aside className="flex h-full w-[560px] shrink-0 items-center justify-center border-l border-border bg-card px-4 text-center text-xs text-muted-foreground/70"> | |
| 203 | Loading diff viewer... | |
| 204 | </aside> | |
| 205 | } | |
| 206 | > | |
| 207 | <DiffPanel /> | |
| 208 | </Suspense> | |
| 209 | )} | |
| 199 | 210 | </div> |
| 200 | 211 | ); |
| 201 | 212 | } |
apps/web/src/components/ChatView.tsx+83-2| ... | ... | @@ -52,7 +52,9 @@ import { |
| 52 | 52 | import { |
| 53 | 53 | derivePendingApprovals, |
| 54 | 54 | derivePhase, |
| 55 | deriveTurnDiffSummaries, | |
| 55 | 56 | deriveTimelineEntries, |
| 57 | type TurnDiffSummary, | |
| 56 | 58 | type PendingApproval, |
| 57 | 59 | deriveWorkLogEntries, |
| 58 | 60 | formatDuration, |
| ... | ... | @@ -406,6 +408,18 @@ export default function ChatView() { |
| 406 | 408 | () => deriveTimelineEntries(activeThread?.messages ?? [], workLogEntries), |
| 407 | 409 | [activeThread?.messages, workLogEntries], |
| 408 | 410 | ); |
| 411 | const turnDiffSummaries = useMemo( | |
| 412 | () => deriveTurnDiffSummaries(activeThread?.events ?? []), | |
| 413 | [activeThread?.events], | |
| 414 | ); | |
| 415 | const turnDiffSummaryByAssistantMessageId = useMemo(() => { | |
| 416 | const byMessageId = new Map<string, TurnDiffSummary>(); | |
| 417 | for (const summary of turnDiffSummaries) { | |
| 418 | if (!summary.assistantMessageId) continue; | |
| 419 | byMessageId.set(summary.assistantMessageId, summary); | |
| 420 | } | |
| 421 | return byMessageId; | |
| 422 | }, [turnDiffSummaries]); | |
| 409 | 423 | const completionSummary = useMemo(() => { |
| 410 | 424 | if (!activeThread?.latestTurnStartedAt) return null; |
| 411 | 425 | if (!activeThread.latestTurnCompletedAt) return null; |
| ... | ... | @@ -1685,8 +1699,36 @@ export default function ChatView() { |
| 1685 | 1699 | setExpandedImage(image); |
| 1686 | 1700 | }, []); |
| 1687 | 1701 | const onToggleDiff = useCallback(() => { |
| 1688 | dispatch({ type: "TOGGLE_DIFF" }); | |
| 1689 | }, [dispatch]); | |
| 1702 | if (state.diffOpen) { | |
| 1703 | dispatch({ type: "CLOSE_DIFF" }); | |
| 1704 | return; | |
| 1705 | } | |
| 1706 | ||
| 1707 | const latestSummary = turnDiffSummaries[0]; | |
| 1708 | if (!activeThread || !latestSummary) { | |
| 1709 | dispatch({ type: "TOGGLE_DIFF" }); | |
| 1710 | return; | |
| 1711 | } | |
| 1712 | ||
| 1713 | dispatch({ | |
| 1714 | type: "OPEN_DIFF", | |
| 1715 | threadId: activeThread.id, | |
| 1716 | turnId: latestSummary.turnId, | |
| 1717 | ...(latestSummary.files[0]?.path ? { filePath: latestSummary.files[0].path } : {}), | |
| 1718 | }); | |
| 1719 | }, [activeThread, dispatch, state.diffOpen, turnDiffSummaries]); | |
| 1720 | const onOpenTurnDiff = useCallback( | |
| 1721 | (turnId: string, filePath?: string) => { | |
| 1722 | if (!activeThread) return; | |
| 1723 | dispatch({ | |
| 1724 | type: "OPEN_DIFF", | |
| 1725 | threadId: activeThread.id, | |
| 1726 | turnId, | |
| 1727 | ...(filePath ? { filePath } : {}), | |
| 1728 | }); | |
| 1729 | }, | |
| 1730 | [activeThread, dispatch], | |
| 1731 | ); | |
| 1690 | 1732 | |
| 1691 | 1733 | // Empty state: no active thread |
| 1692 | 1734 | if (!activeThread) { |
| ... | ... | @@ -1761,9 +1803,11 @@ export default function ChatView() { |
| 1761 | 1803 | completionDividerBeforeEntryId={completionDividerBeforeEntryId} |
| 1762 | 1804 | completionSummary={completionSummary} |
| 1763 | 1805 | assistantCompletionByItemId={assistantCompletionByItemId} |
| 1806 | turnDiffSummaryByAssistantMessageId={turnDiffSummaryByAssistantMessageId} | |
| 1764 | 1807 | nowIso={nowIso} |
| 1765 | 1808 | expandedWorkGroups={expandedWorkGroups} |
| 1766 | 1809 | onToggleWorkGroup={onToggleWorkGroup} |
| 1810 | onOpenTurnDiff={onOpenTurnDiff} | |
| 1767 | 1811 | onImageExpand={onExpandTimelineImage} |
| 1768 | 1812 | messagesEndRef={messagesEndRef} |
| 1769 | 1813 | /> |
| ... | ... | @@ -2268,9 +2312,11 @@ interface MessagesTimelineProps { |
| 2268 | 2312 | completionDividerBeforeEntryId: string | null; |
| 2269 | 2313 | completionSummary: string | null; |
| 2270 | 2314 | assistantCompletionByItemId: Map<string, string>; |
| 2315 | turnDiffSummaryByAssistantMessageId: Map<string, TurnDiffSummary>; | |
| 2271 | 2316 | nowIso: string; |
| 2272 | 2317 | expandedWorkGroups: Record<string, boolean>; |
| 2273 | 2318 | onToggleWorkGroup: (groupId: string) => void; |
| 2319 | onOpenTurnDiff: (turnId: string, filePath?: string) => void; | |
| 2274 | 2320 | onImageExpand: (image: ExpandedImagePreview) => void; |
| 2275 | 2321 | messagesEndRef: RefObject<HTMLDivElement | null>; |
| 2276 | 2322 | } |
| ... | ... | @@ -2282,9 +2328,11 @@ const MessagesTimeline = memo(function MessagesTimeline({ |
| 2282 | 2328 | completionDividerBeforeEntryId, |
| 2283 | 2329 | completionSummary, |
| 2284 | 2330 | assistantCompletionByItemId, |
| 2331 | turnDiffSummaryByAssistantMessageId, | |
| 2285 | 2332 | nowIso, |
| 2286 | 2333 | expandedWorkGroups, |
| 2287 | 2334 | onToggleWorkGroup, |
| 2335 | onOpenTurnDiff, | |
| 2288 | 2336 | onImageExpand, |
| 2289 | 2337 | messagesEndRef, |
| 2290 | 2338 | }: MessagesTimelineProps) { |
| ... | ... | @@ -2446,6 +2494,39 @@ const MessagesTimeline = memo(function MessagesTimeline({ |
| 2446 | 2494 | (timelineEntry.message.streaming ? "" : "(empty response)") |
| 2447 | 2495 | } |
| 2448 | 2496 | /> |
| 2497 | {(() => { | |
| 2498 | const turnSummary = turnDiffSummaryByAssistantMessageId.get(timelineEntry.message.id); | |
| 2499 | if (!turnSummary) return null; | |
| 2500 | return ( | |
| 2501 | <div className="mt-2 rounded-lg border border-border/80 bg-card/45 p-2.5"> | |
| 2502 | <div className="mb-1.5 flex items-center justify-between gap-2"> | |
| 2503 | <p className="text-[10px] uppercase tracking-[0.12em] text-muted-foreground/65"> | |
| 2504 | Changed files ({turnSummary.files.length}) | |
| 2505 | </p> | |
| 2506 | <Button | |
| 2507 | type="button" | |
| 2508 | size="xs" | |
| 2509 | variant="outline" | |
| 2510 | onClick={() => onOpenTurnDiff(turnSummary.turnId, turnSummary.files[0]?.path)} | |
| 2511 | > | |
| 2512 | View diff | |
| 2513 | </Button> | |
| 2514 | </div> | |
| 2515 | <div className="flex flex-wrap gap-1.5"> | |
| 2516 | {turnSummary.files.map((file) => ( | |
| 2517 | <button | |
| 2518 | key={`${turnSummary.turnId}:${file.path}`} | |
| 2519 | type="button" | |
| 2520 | className="rounded-md border border-border/70 bg-background/70 px-2 py-1 font-mono text-[11px] text-muted-foreground/80 transition-colors hover:border-border hover:text-foreground/90" | |
| 2521 | onClick={() => onOpenTurnDiff(turnSummary.turnId, file.path)} | |
| 2522 | > | |
| 2523 | {file.path} | |
| 2524 | </button> | |
| 2525 | ))} | |
| 2526 | </div> | |
| 2527 | </div> | |
| 2528 | ); | |
| 2529 | })()} | |
| 2449 | 2530 | <p className="mt-1.5 text-[10px] text-muted-foreground/30"> |
| 2450 | 2531 | {formatMessageMeta( |
| 2451 | 2532 | timelineEntry.message.createdAt, |
apps/web/src/components/DiffPanel.tsx+123-36| ... | ... | @@ -1,58 +1,145 @@ |
| 1 | import { PatchDiff } from "@pierre/diffs/react"; | |
| 1 | 2 | import { XIcon } from "lucide-react"; |
| 3 | import { useMemo } from "react"; | |
| 2 | 4 | import { isElectron } from "../env"; |
| 5 | import { deriveTurnDiffSummaries, formatTimestamp } from "../session-logic"; | |
| 3 | 6 | import { useStore } from "../store"; |
| 4 | 7 | import { Button } from "./ui/button"; |
| 8 | import { cn } from "~/lib/utils"; | |
| 5 | 9 | |
| 6 | 10 | export default function DiffPanel() { |
| 7 | const { dispatch } = useStore(); | |
| 11 | const { state, dispatch } = useStore(); | |
| 12 | const activeThread = state.threads.find((thread) => thread.id === state.activeThreadId); | |
| 13 | const turnDiffSummaries = useMemo( | |
| 14 | () => deriveTurnDiffSummaries(activeThread?.events ?? []), | |
| 15 | [activeThread?.events], | |
| 16 | ); | |
| 17 | ||
| 18 | const canApplyStoredTarget = Boolean(activeThread && state.diffThreadId === activeThread.id); | |
| 19 | const selectedTurnId = canApplyStoredTarget ? state.diffTurnId : null; | |
| 20 | const selectedFilePath = canApplyStoredTarget ? state.diffFilePath : null; | |
| 21 | const selectedTurn = | |
| 22 | turnDiffSummaries.find((summary) => summary.turnId === selectedTurnId) ?? turnDiffSummaries[0]; | |
| 23 | const selectedFile = | |
| 24 | selectedTurn?.files.find((file) => file.path === selectedFilePath) ?? selectedTurn?.files[0]; | |
| 25 | const selectedPatch = selectedFile?.diff ?? selectedTurn?.unifiedDiff; | |
| 26 | ||
| 27 | const selectTurn = (turnId: string) => { | |
| 28 | if (!activeThread) return; | |
| 29 | const turn = turnDiffSummaries.find((summary) => summary.turnId === turnId); | |
| 30 | dispatch({ | |
| 31 | type: "SET_DIFF_TARGET", | |
| 32 | threadId: activeThread.id, | |
| 33 | turnId, | |
| 34 | ...(turn?.files[0]?.path ? { filePath: turn.files[0].path } : {}), | |
| 35 | }); | |
| 36 | }; | |
| 37 | ||
| 38 | const selectFile = (filePath: string) => { | |
| 39 | if (!activeThread || !selectedTurn) return; | |
| 40 | dispatch({ | |
| 41 | type: "SET_DIFF_TARGET", | |
| 42 | threadId: activeThread.id, | |
| 43 | turnId: selectedTurn.turnId, | |
| 44 | filePath, | |
| 45 | }); | |
| 46 | }; | |
| 8 | 47 | |
| 9 | 48 | return ( |
| 10 | <aside className="flex h-full w-[400px] shrink-0 flex-col border-l border-border bg-card"> | |
| 11 | {/* Header */} | |
| 49 | <aside className="flex h-full w-[560px] shrink-0 flex-col border-l border-border bg-card"> | |
| 12 | 50 | <div |
| 13 | className={`flex items-center justify-between border-b border-border px-4 ${isElectron ? "drag-region h-[52px]" : "py-3"}`} | |
| 51 | className={cn( | |
| 52 | "flex items-center justify-between border-b border-border px-4", | |
| 53 | isElectron ? "drag-region h-[52px]" : "py-3", | |
| 54 | )} | |
| 14 | 55 | > |
| 15 | <h3 className="text-xs font-medium text-foreground">Uncommitted changes</h3> | |
| 16 | <Button | |
| 17 | type="button" | |
| 18 | size="icon-xs" | |
| 19 | variant="ghost" | |
| 20 | onClick={() => dispatch({ type: "TOGGLE_DIFF" })} | |
| 21 | > | |
| 56 | <h3 className="text-xs font-medium text-foreground">Turn diffs</h3> | |
| 57 | <Button type="button" size="icon-xs" variant="ghost" onClick={() => dispatch({ type: "CLOSE_DIFF" })}> | |
| 22 | 58 | <XIcon /> |
| 23 | 59 | </Button> |
| 24 | 60 | </div> |
| 25 | 61 | |
| 26 | {/* Placeholder content */} | |
| 27 | <div className="flex-1 overflow-y-auto p-4"> | |
| 28 | <div className="space-y-3"> | |
| 29 | <div className="rounded-lg border border-border/80 bg-muted p-3"> | |
| 30 | <div className="mb-2 flex items-center gap-2"> | |
| 31 | <span className="rounded bg-emerald-500/20 px-1.5 py-0.5 text-[9px] font-medium text-emerald-400"> | |
| 32 | M | |
| 33 | </span> | |
| 34 | <span className="font-mono text-xs text-muted-foreground/80">src/App.tsx</span> | |
| 35 | </div> | |
| 36 | <div className="space-y-0.5 font-mono text-[11px]"> | |
| 37 | <div className="text-red-400/60">- const oldValue = "hello";</div> | |
| 38 | <div className="text-emerald-400/60">+ const newValue = "world";</div> | |
| 62 | {!activeThread ? ( | |
| 63 | <div className="flex flex-1 items-center justify-center px-5 text-center text-xs text-muted-foreground/70"> | |
| 64 | Select a thread to inspect turn diffs. | |
| 65 | </div> | |
| 66 | ) : turnDiffSummaries.length === 0 ? ( | |
| 67 | <div className="flex flex-1 items-center justify-center px-5 text-center text-xs text-muted-foreground/70"> | |
| 68 | No completed turn file changes yet. | |
| 69 | </div> | |
| 70 | ) : ( | |
| 71 | <> | |
| 72 | <div className="border-b border-border px-3 py-2.5"> | |
| 73 | <p className="mb-1.5 text-[10px] uppercase tracking-[0.12em] text-muted-foreground/60"> | |
| 74 | Turns | |
| 75 | </p> | |
| 76 | <div className="flex flex-wrap gap-1.5"> | |
| 77 | {turnDiffSummaries.map((summary, index) => ( | |
| 78 | <button | |
| 79 | key={summary.turnId} | |
| 80 | type="button" | |
| 81 | className={cn( | |
| 82 | "rounded-md border px-2 py-1 text-left transition-colors", | |
| 83 | summary.turnId === selectedTurn?.turnId | |
| 84 | ? "border-border bg-accent text-accent-foreground" | |
| 85 | : "border-border/70 bg-background/70 text-muted-foreground/80 hover:border-border hover:text-foreground/80", | |
| 86 | )} | |
| 87 | onClick={() => selectTurn(summary.turnId)} | |
| 88 | title={summary.turnId} | |
| 89 | > | |
| 90 | <div className="text-[10px] font-medium">Turn {turnDiffSummaries.length - index}</div> | |
| 91 | <div className="text-[10px] opacity-70">{formatTimestamp(summary.completedAt)}</div> | |
| 92 | </button> | |
| 93 | ))} | |
| 39 | 94 | </div> |
| 40 | 95 | </div> |
| 41 | 96 | |
| 42 | <div className="rounded-lg border border-border/80 bg-muted p-3"> | |
| 43 | <div className="flex items-center gap-2"> | |
| 44 | <span className="rounded bg-blue-500/20 px-1.5 py-0.5 text-[9px] font-medium text-blue-400"> | |
| 45 | A | |
| 46 | </span> | |
| 47 | <span className="font-mono text-xs text-muted-foreground/80">src/utils.ts</span> | |
| 97 | <div className="min-h-0 flex flex-1"> | |
| 98 | <div className="w-[220px] shrink-0 border-r border-border px-2 py-2"> | |
| 99 | <p className="mb-1.5 px-1 text-[10px] uppercase tracking-[0.12em] text-muted-foreground/60"> | |
| 100 | Files | |
| 101 | </p> | |
| 102 | <div className="space-y-1 overflow-y-auto"> | |
| 103 | {selectedTurn?.files.map((file) => ( | |
| 104 | <button | |
| 105 | key={`${selectedTurn.turnId}:${file.path}`} | |
| 106 | type="button" | |
| 107 | className={cn( | |
| 108 | "flex w-full items-center justify-between gap-2 rounded-md border px-2 py-1.5 text-left font-mono text-[11px] transition-colors", | |
| 109 | selectedFile?.path === file.path | |
| 110 | ? "border-border bg-accent text-accent-foreground" | |
| 111 | : "border-border/60 bg-background/60 text-muted-foreground/85 hover:border-border hover:text-foreground/90", | |
| 112 | )} | |
| 113 | onClick={() => selectFile(file.path)} | |
| 114 | > | |
| 115 | <span className="truncate">{file.path}</span> | |
| 116 | {file.kind && ( | |
| 117 | <span className="rounded-sm border border-border/60 px-1 py-0.5 text-[9px] uppercase tracking-[0.08em]"> | |
| 118 | {file.kind} | |
| 119 | </span> | |
| 120 | )} | |
| 121 | </button> | |
| 122 | ))} | |
| 123 | </div> | |
| 48 | 124 | </div> |
| 49 | </div> | |
| 50 | </div> | |
| 51 | 125 | |
| 52 | <p className="mt-6 text-center text-[10px] text-muted-foreground/30"> | |
| 53 | Diff integration coming soon | |
| 54 | </p> | |
| 55 | </div> | |
| 126 | <div className="min-w-0 flex-1 overflow-auto px-3 py-2"> | |
| 127 | {!canApplyStoredTarget && state.diffThreadId && ( | |
| 128 | <p className="mb-2 text-[11px] text-muted-foreground/65"> | |
| 129 | Showing diffs for the active thread. | |
| 130 | </p> | |
| 131 | )} | |
| 132 | {!selectedPatch ? ( | |
| 133 | <div className="flex h-full items-center justify-center text-xs text-muted-foreground/70"> | |
| 134 | No patch available for this selection. | |
| 135 | </div> | |
| 136 | ) : ( | |
| 137 | <PatchDiff patch={selectedPatch} /> | |
| 138 | )} | |
| 139 | </div> | |
| 140 | </div> | |
| 141 | </> | |
| 142 | )} | |
| 56 | 143 | </aside> |
| 57 | 144 | ); |
| 58 | 145 | } |
apps/web/src/session-logic.test.ts+104| ... | ... | @@ -5,6 +5,7 @@ import { |
| 5 | 5 | type WorkLogEntry, |
| 6 | 6 | applyEventToMessages, |
| 7 | 7 | derivePendingApprovals, |
| 8 | deriveTurnDiffSummaries, | |
| 8 | 9 | deriveTimelineEntries, |
| 9 | 10 | deriveWorkLogEntries, |
| 10 | 11 | evolveSession, |
| ... | ... | @@ -393,6 +394,109 @@ describe("deriveWorkLogEntries", () => { |
| 393 | 394 | }); |
| 394 | 395 | }); |
| 395 | 396 | |
| 397 | describe("deriveTurnDiffSummaries", () => { | |
| 398 | it("collects per-turn file changes and links them to assistant message completion", () => { | |
| 399 | const summaries = deriveTurnDiffSummaries([ | |
| 400 | makeEvent({ | |
| 401 | id: "evt-turn-start", | |
| 402 | method: "turn/started", | |
| 403 | turnId: "turn-1", | |
| 404 | createdAt: "2026-02-08T10:00:00.000Z", | |
| 405 | }), | |
| 406 | makeEvent({ | |
| 407 | id: "evt-file-change", | |
| 408 | method: "item/completed", | |
| 409 | turnId: "turn-1", | |
| 410 | createdAt: "2026-02-08T10:00:01.000Z", | |
| 411 | payload: { | |
| 412 | item: { | |
| 413 | id: "item-file-change", | |
| 414 | type: "fileChange", | |
| 415 | status: "completed", | |
| 416 | changes: [ | |
| 417 | { | |
| 418 | path: "src/a.ts", | |
| 419 | kind: "modified", | |
| 420 | diff: "diff --git a/src/a.ts b/src/a.ts\n@@ -1 +1 @@\n-old\n+new\n", | |
| 421 | }, | |
| 422 | ], | |
| 423 | }, | |
| 424 | }, | |
| 425 | }), | |
| 426 | makeEvent({ | |
| 427 | id: "evt-turn-diff-updated", | |
| 428 | method: "turn/diff/updated", | |
| 429 | turnId: "turn-1", | |
| 430 | createdAt: "2026-02-08T10:00:02.000Z", | |
| 431 | payload: { | |
| 432 | threadId: "thr-1", | |
| 433 | turnId: "turn-1", | |
| 434 | diff: [ | |
| 435 | "diff --git a/src/a.ts b/src/a.ts", | |
| 436 | "@@ -1 +1 @@", | |
| 437 | "-old", | |
| 438 | "+new", | |
| 439 | "diff --git a/src/b.ts b/src/b.ts", | |
| 440 | "@@ -0,0 +1 @@", | |
| 441 | "+created", | |
| 442 | ].join("\n"), | |
| 443 | }, | |
| 444 | }), | |
| 445 | makeEvent({ | |
| 446 | id: "evt-agent-msg", | |
| 447 | method: "item/completed", | |
| 448 | turnId: "turn-1", | |
| 449 | createdAt: "2026-02-08T10:00:03.000Z", | |
| 450 | payload: { | |
| 451 | item: { | |
| 452 | id: "msg-1", | |
| 453 | type: "agentMessage", | |
| 454 | text: "Done", | |
| 455 | }, | |
| 456 | }, | |
| 457 | }), | |
| 458 | makeEvent({ | |
| 459 | id: "evt-turn-completed", | |
| 460 | method: "turn/completed", | |
| 461 | turnId: "turn-1", | |
| 462 | createdAt: "2026-02-08T10:00:04.000Z", | |
| 463 | payload: { | |
| 464 | turn: { | |
| 465 | id: "turn-1", | |
| 466 | status: "completed", | |
| 467 | }, | |
| 468 | }, | |
| 469 | }), | |
| 470 | makeEvent({ | |
| 471 | id: "evt-turn-2-file-change", | |
| 472 | method: "item/completed", | |
| 473 | turnId: "turn-2", | |
| 474 | createdAt: "2026-02-08T10:00:05.000Z", | |
| 475 | payload: { | |
| 476 | item: { | |
| 477 | id: "item-file-change-2", | |
| 478 | type: "fileChange", | |
| 479 | status: "completed", | |
| 480 | changes: [ | |
| 481 | { | |
| 482 | path: "src/unfinished.ts", | |
| 483 | kind: "modified", | |
| 484 | diff: "diff --git a/src/unfinished.ts b/src/unfinished.ts", | |
| 485 | }, | |
| 486 | ], | |
| 487 | }, | |
| 488 | }, | |
| 489 | }), | |
| 490 | ]); | |
| 491 | ||
| 492 | expect(summaries).toHaveLength(1); | |
| 493 | expect(summaries[0]?.turnId).toBe("turn-1"); | |
| 494 | expect(summaries[0]?.assistantMessageId).toBe("msg-1"); | |
| 495 | expect(summaries[0]?.files.map((file) => file.path)).toEqual(["src/a.ts", "src/b.ts"]); | |
| 496 | expect(summaries[0]?.files[0]?.kind).toBe("modified"); | |
| 497 | }); | |
| 498 | }); | |
| 499 | ||
| 396 | 500 | describe("derivePendingApprovals", () => { |
| 397 | 501 | it("returns pending command/file-change approvals", () => { |
| 398 | 502 | const approvals = derivePendingApprovals([ |
apps/web/src/session-logic.ts+230| ... | ... | @@ -99,12 +99,31 @@ export type TimelineEntry = |
| 99 | 99 | entry: WorkLogEntry; |
| 100 | 100 | }; |
| 101 | 101 | |
| 102 | export interface TurnDiffFileChange { | |
| 103 | path: string; | |
| 104 | kind?: string; | |
| 105 | diff?: string; | |
| 106 | } | |
| 107 | ||
| 108 | export interface TurnDiffSummary { | |
| 109 | turnId: string; | |
| 110 | completedAt: string; | |
| 111 | status?: string; | |
| 112 | files: TurnDiffFileChange[]; | |
| 113 | unifiedDiff?: string; | |
| 114 | assistantMessageId?: string; | |
| 115 | } | |
| 116 | ||
| 102 | 117 | function normalizeDetail(value: string | undefined): string | undefined { |
| 103 | 118 | if (!value) return undefined; |
| 104 | 119 | const trimmed = value.trim(); |
| 105 | 120 | return trimmed.length > 0 ? trimmed : undefined; |
| 106 | 121 | } |
| 107 | 122 | |
| 123 | function asArray(value: unknown): unknown[] | undefined { | |
| 124 | return Array.isArray(value) ? value : undefined; | |
| 125 | } | |
| 126 | ||
| 108 | 127 | function approvalDetail(event: ProviderEvent): string | undefined { |
| 109 | 128 | const payload = asObject(event.payload); |
| 110 | 129 | const command = asString(payload?.command); |
| ... | ... | @@ -438,6 +457,217 @@ export function deriveWorkLogEntries( |
| 438 | 457 | return entries; |
| 439 | 458 | } |
| 440 | 459 | |
| 460 | interface MutableTurnDiffFileChange { | |
| 461 | path: string; | |
| 462 | kind: string | undefined; | |
| 463 | diff: string | undefined; | |
| 464 | } | |
| 465 | ||
| 466 | interface MutableTurnDiffSummary { | |
| 467 | turnId: string; | |
| 468 | completedAt: string | undefined; | |
| 469 | status: string | undefined; | |
| 470 | unifiedDiff: string | undefined; | |
| 471 | assistantMessageId: string | undefined; | |
| 472 | filesByPath: Map<string, MutableTurnDiffFileChange>; | |
| 473 | } | |
| 474 | ||
| 475 | function parsePathFromDiff(diff: string): string | undefined { | |
| 476 | const normalized = diff.replace(/\r\n/g, "\n"); | |
| 477 | const gitHeader = normalized.match(/^diff --git a\/(.+) b\/(.+)$/m); | |
| 478 | if (gitHeader && gitHeader[2]) return gitHeader[2]; | |
| 479 | const bPath = normalized.match(/^\+\+\+ b\/(.+)$/m); | |
| 480 | if (bPath && bPath[1]) return bPath[1]; | |
| 481 | const direct = normalized.match(/^\+\+\+ (.+)$/m); | |
| 482 | if (!direct || !direct[1] || direct[1] === "/dev/null") { | |
| 483 | return undefined; | |
| 484 | } | |
| 485 | return direct[1]; | |
| 486 | } | |
| 487 | ||
| 488 | function splitUnifiedDiffByFile(diff: string): Map<string, string> { | |
| 489 | const normalized = diff.replace(/\r\n/g, "\n"); | |
| 490 | const byPath = new Map<string, string>(); | |
| 491 | const headerMatches = [...normalized.matchAll(/^diff --git a\/(.+) b\/(.+)$/gm)]; | |
| 492 | ||
| 493 | if (headerMatches.length === 0) { | |
| 494 | const path = parsePathFromDiff(normalized); | |
| 495 | if (path) { | |
| 496 | byPath.set(path, normalized.trim()); | |
| 497 | } | |
| 498 | return byPath; | |
| 499 | } | |
| 500 | ||
| 501 | for (let index = 0; index < headerMatches.length; index += 1) { | |
| 502 | const match = headerMatches[index]; | |
| 503 | if (!match) continue; | |
| 504 | const start = match.index ?? 0; | |
| 505 | const nextStart = headerMatches[index + 1]?.index ?? normalized.length; | |
| 506 | const segment = normalized.slice(start, nextStart).trim(); | |
| 507 | const path = match[2] ?? match[1] ?? parsePathFromDiff(segment); | |
| 508 | if (!path || segment.length === 0) continue; | |
| 509 | byPath.set(path, segment); | |
| 510 | } | |
| 511 | ||
| 512 | return byPath; | |
| 513 | } | |
| 514 | ||
| 515 | function parseFileChangeEntriesFromEvent(event: ProviderEvent): TurnDiffFileChange[] { | |
| 516 | if (event.method !== "item/started" && event.method !== "item/completed") { | |
| 517 | return []; | |
| 518 | } | |
| 519 | ||
| 520 | const payload = asObject(event.payload); | |
| 521 | const item = asObject(payload?.item); | |
| 522 | if (asString(item?.type) !== "fileChange") { | |
| 523 | return []; | |
| 524 | } | |
| 525 | ||
| 526 | const changes = asArray(item?.changes); | |
| 527 | if (!changes || changes.length === 0) { | |
| 528 | return []; | |
| 529 | } | |
| 530 | ||
| 531 | const files: TurnDiffFileChange[] = []; | |
| 532 | for (const rawChange of changes) { | |
| 533 | const change = asObject(rawChange); | |
| 534 | if (!change) continue; | |
| 535 | const diff = normalizeDetail(asString(change.diff)); | |
| 536 | const path = normalizeDetail(asString(change.path)) ?? (diff ? parsePathFromDiff(diff) : undefined); | |
| 537 | if (!path) continue; | |
| 538 | const kind = normalizeDetail(asString(change.kind)); | |
| 539 | files.push({ | |
| 540 | path, | |
| 541 | ...(kind ? { kind } : {}), | |
| 542 | ...(diff ? { diff } : {}), | |
| 543 | }); | |
| 544 | } | |
| 545 | ||
| 546 | return files; | |
| 547 | } | |
| 548 | ||
| 549 | export function deriveTurnDiffSummaries(events: ProviderEvent[]): TurnDiffSummary[] { | |
| 550 | const ordered = [...events].toReversed(); | |
| 551 | const byTurnId = new Map<string, MutableTurnDiffSummary>(); | |
| 552 | ||
| 553 | const ensureSummary = (turnId: string): MutableTurnDiffSummary => { | |
| 554 | const existing = byTurnId.get(turnId); | |
| 555 | if (existing) return existing; | |
| 556 | const next: MutableTurnDiffSummary = { | |
| 557 | turnId, | |
| 558 | completedAt: undefined, | |
| 559 | status: undefined, | |
| 560 | unifiedDiff: undefined, | |
| 561 | assistantMessageId: undefined, | |
| 562 | filesByPath: new Map(), | |
| 563 | }; | |
| 564 | byTurnId.set(turnId, next); | |
| 565 | return next; | |
| 566 | }; | |
| 567 | ||
| 568 | for (const event of ordered) { | |
| 569 | const turnId = eventTurnId(event); | |
| 570 | if (!turnId) continue; | |
| 571 | const summary = ensureSummary(turnId); | |
| 572 | ||
| 573 | if (event.method === "turn/completed") { | |
| 574 | summary.completedAt = event.createdAt; | |
| 575 | const turn = asObject(asObject(event.payload)?.turn); | |
| 576 | summary.status = normalizeDetail(asString(turn?.status)); | |
| 577 | continue; | |
| 578 | } | |
| 579 | ||
| 580 | if (event.method === "turn/diff/updated") { | |
| 581 | const diff = normalizeDetail(asString(asObject(event.payload)?.diff)); | |
| 582 | if (diff) summary.unifiedDiff = diff; | |
| 583 | continue; | |
| 584 | } | |
| 585 | ||
| 586 | if (event.method === "item/completed") { | |
| 587 | const item = asObject(asObject(event.payload)?.item); | |
| 588 | if (asString(item?.type) === "agentMessage") { | |
| 589 | const itemId = normalizeDetail(asString(item?.id)); | |
| 590 | if (itemId) { | |
| 591 | summary.assistantMessageId = itemId; | |
| 592 | } | |
| 593 | } | |
| 594 | } | |
| 595 | ||
| 596 | for (const change of parseFileChangeEntriesFromEvent(event)) { | |
| 597 | const existing = summary.filesByPath.get(change.path); | |
| 598 | if (!existing) { | |
| 599 | summary.filesByPath.set(change.path, { | |
| 600 | path: change.path, | |
| 601 | kind: change.kind, | |
| 602 | diff: change.diff, | |
| 603 | }); | |
| 604 | continue; | |
| 605 | } | |
| 606 | summary.filesByPath.set(change.path, { | |
| 607 | path: change.path, | |
| 608 | kind: change.kind ?? existing.kind, | |
| 609 | diff: change.diff ?? existing.diff, | |
| 610 | }); | |
| 611 | } | |
| 612 | } | |
| 613 | ||
| 614 | const summaries: TurnDiffSummary[] = []; | |
| 615 | for (const summary of byTurnId.values()) { | |
| 616 | if (!summary.completedAt) continue; | |
| 617 | ||
| 618 | const filesByPath = new Map(summary.filesByPath); | |
| 619 | if (summary.unifiedDiff) { | |
| 620 | const fileDiffs = splitUnifiedDiffByFile(summary.unifiedDiff); | |
| 621 | for (const [path, diff] of fileDiffs) { | |
| 622 | const existing = filesByPath.get(path); | |
| 623 | if (!existing) { | |
| 624 | filesByPath.set(path, { path, kind: undefined, diff }); | |
| 625 | continue; | |
| 626 | } | |
| 627 | if (!existing.diff) { | |
| 628 | filesByPath.set(path, { ...existing, diff }); | |
| 629 | } | |
| 630 | } | |
| 631 | } | |
| 632 | ||
| 633 | const files: TurnDiffFileChange[] = []; | |
| 634 | for (const file of Array.from(filesByPath.values()).toSorted((a, b) => | |
| 635 | a.path.localeCompare(b.path), | |
| 636 | )) { | |
| 637 | const nextFile: TurnDiffFileChange = { path: file.path }; | |
| 638 | if (file.kind) { | |
| 639 | nextFile.kind = file.kind; | |
| 640 | } | |
| 641 | if (file.diff) { | |
| 642 | nextFile.diff = file.diff; | |
| 643 | } | |
| 644 | files.push(nextFile); | |
| 645 | } | |
| 646 | ||
| 647 | if (files.length === 0) continue; | |
| 648 | ||
| 649 | summaries.push({ | |
| 650 | turnId: summary.turnId, | |
| 651 | completedAt: summary.completedAt, | |
| 652 | ...(summary.status ? { status: summary.status } : {}), | |
| 653 | files, | |
| 654 | ...(summary.unifiedDiff ? { unifiedDiff: summary.unifiedDiff } : {}), | |
| 655 | ...(summary.assistantMessageId ? { assistantMessageId: summary.assistantMessageId } : {}), | |
| 656 | }); | |
| 657 | } | |
| 658 | ||
| 659 | summaries.sort((a, b) => { | |
| 660 | const aTime = Date.parse(a.completedAt); | |
| 661 | const bTime = Date.parse(b.completedAt); | |
| 662 | if (Number.isNaN(aTime) || Number.isNaN(bTime)) { | |
| 663 | return b.completedAt.localeCompare(a.completedAt); | |
| 664 | } | |
| 665 | return bTime - aTime; | |
| 666 | }); | |
| 667 | ||
| 668 | return summaries; | |
| 669 | } | |
| 670 | ||
| 441 | 671 | function toTimestamp(isoDate: string): number { |
| 442 | 672 | const parsed = Date.parse(isoDate); |
| 443 | 673 | return Number.isNaN(parsed) ? Number.POSITIVE_INFINITY : parsed; |
apps/web/src/store.test.ts+24| ... | ... | @@ -81,6 +81,9 @@ function makeState(thread: Thread): AppState { |
| 81 | 81 | activeThreadId: thread.id, |
| 82 | 82 | runtimeMode: "full-access", |
| 83 | 83 | diffOpen: false, |
| 84 | diffThreadId: null, | |
| 85 | diffTurnId: null, | |
| 86 | diffFilePath: null, | |
| 84 | 87 | }; |
| 85 | 88 | } |
| 86 | 89 | |
| ... | ... | @@ -145,6 +148,21 @@ describe("store reducer thread continuity", () => { |
| 145 | 148 | expect(next.threads[0]?.terminalOpen).toBe(true); |
| 146 | 149 | }); |
| 147 | 150 | |
| 151 | it("opens diff panel with an explicit turn/file target", () => { | |
| 152 | const state = makeState(makeThread()); | |
| 153 | const next = reducer(state, { | |
| 154 | type: "OPEN_DIFF", | |
| 155 | threadId: "thread-local-1", | |
| 156 | turnId: "turn-1", | |
| 157 | filePath: "src/app.ts", | |
| 158 | }); | |
| 159 | ||
| 160 | expect(next.diffOpen).toBe(true); | |
| 161 | expect(next.diffThreadId).toBe("thread-local-1"); | |
| 162 | expect(next.diffTurnId).toBe("turn-1"); | |
| 163 | expect(next.diffFilePath).toBe("src/app.ts"); | |
| 164 | }); | |
| 165 | ||
| 148 | 166 | it("sets terminal open state per thread", () => { |
| 149 | 167 | const state = makeState(makeThread({ terminalOpen: true })); |
| 150 | 168 | const next = reducer(state, { |
| ... | ... | @@ -532,6 +550,9 @@ describe("store reducer thread continuity", () => { |
| 532 | 550 | activeThreadId: "thread-b", |
| 533 | 551 | runtimeMode: "full-access", |
| 534 | 552 | diffOpen: false, |
| 553 | diffThreadId: null, | |
| 554 | diffTurnId: null, | |
| 555 | diffFilePath: null, | |
| 535 | 556 | }; |
| 536 | 557 | |
| 537 | 558 | const next = reducer(state, { |
| ... | ... | @@ -661,6 +682,9 @@ describe("store reducer thread continuity", () => { |
| 661 | 682 | activeThreadId: "thread-a", |
| 662 | 683 | runtimeMode: "full-access", |
| 663 | 684 | diffOpen: false, |
| 685 | diffThreadId: null, | |
| 686 | diffTurnId: null, | |
| 687 | diffFilePath: null, | |
| 664 | 688 | }; |
| 665 | 689 | |
| 666 | 690 | const next = reducer(state, { |
apps/web/src/store.ts+62-2| ... | ... | @@ -42,6 +42,19 @@ type Action = |
| 42 | 42 | | { type: "SET_THREAD_ACTIVE_TERMINAL"; threadId: string; terminalId: string } |
| 43 | 43 | | { type: "CLOSE_THREAD_TERMINAL"; threadId: string; terminalId: string } |
| 44 | 44 | | { type: "TOGGLE_DIFF" } |
| 45 | | { | |
| 46 | type: "OPEN_DIFF"; | |
| 47 | threadId: string; | |
| 48 | turnId?: string; | |
| 49 | filePath?: string; | |
| 50 | } | |
| 51 | | { | |
| 52 | type: "SET_DIFF_TARGET"; | |
| 53 | threadId: string; | |
| 54 | turnId?: string; | |
| 55 | filePath?: string; | |
| 56 | } | |
| 57 | | { type: "CLOSE_DIFF" } | |
| 45 | 58 | | { |
| 46 | 59 | type: "APPLY_EVENT"; |
| 47 | 60 | event: ProviderEvent; |
| ... | ... | @@ -83,6 +96,9 @@ export interface AppState { |
| 83 | 96 | activeThreadId: string | null; |
| 84 | 97 | runtimeMode: RuntimeMode; |
| 85 | 98 | diffOpen: boolean; |
| 99 | diffThreadId: string | null; | |
| 100 | diffTurnId: string | null; | |
| 101 | diffFilePath: string | null; | |
| 86 | 102 | } |
| 87 | 103 | |
| 88 | 104 | const PERSISTED_STATE_KEY = "t3code:renderer-state:v7"; |
| ... | ... | @@ -103,6 +119,9 @@ const initialState: AppState = { |
| 103 | 119 | activeThreadId: null, |
| 104 | 120 | runtimeMode: DEFAULT_RUNTIME_MODE, |
| 105 | 121 | diffOpen: false, |
| 122 | diffThreadId: null, | |
| 123 | diffTurnId: null, | |
| 124 | diffFilePath: null, | |
| 106 | 125 | }; |
| 107 | 126 | |
| 108 | 127 | // ── Helpers ────────────────────────────────────────────────────────── |
| ... | ... | @@ -125,7 +144,15 @@ function readPersistedState(): AppState { |
| 125 | 144 | ? hydrated.activeThreadId |
| 126 | 145 | : (threads[0]?.id ?? null); |
| 127 | 146 | |
| 128 | return { ...hydrated, threads, activeThreadId, diffOpen: false }; | |
| 147 | return { | |
| 148 | ...hydrated, | |
| 149 | threads, | |
| 150 | activeThreadId, | |
| 151 | diffOpen: false, | |
| 152 | diffThreadId: null, | |
| 153 | diffTurnId: null, | |
| 154 | diffFilePath: null, | |
| 155 | }; | |
| 129 | 156 | } catch { |
| 130 | 157 | return initialState; |
| 131 | 158 | } |
| ... | ... | @@ -717,6 +744,26 @@ export function reducer(state: AppState, action: Action): AppState { |
| 717 | 744 | case "TOGGLE_DIFF": |
| 718 | 745 | return { ...state, diffOpen: !state.diffOpen }; |
| 719 | 746 | |
| 747 | case "OPEN_DIFF": | |
| 748 | return { | |
| 749 | ...state, | |
| 750 | diffOpen: true, | |
| 751 | diffThreadId: action.threadId, | |
| 752 | diffTurnId: action.turnId ?? null, | |
| 753 | diffFilePath: action.filePath ?? null, | |
| 754 | }; | |
| 755 | ||
| 756 | case "SET_DIFF_TARGET": | |
| 757 | return { | |
| 758 | ...state, | |
| 759 | diffThreadId: action.threadId, | |
| 760 | diffTurnId: action.turnId ?? null, | |
| 761 | diffFilePath: action.filePath ?? null, | |
| 762 | }; | |
| 763 | ||
| 764 | case "CLOSE_DIFF": | |
| 765 | return { ...state, diffOpen: false }; | |
| 766 | ||
| 720 | 767 | case "APPLY_TERMINAL_EVENT": |
| 721 | 768 | if (!state.threads.some((thread) => thread.id === action.event.threadId)) { |
| 722 | 769 | return state; |
| ... | ... | @@ -901,7 +948,20 @@ export function reducer(state: AppState, action: Action): AppState { |
| 901 | 948 | const threads = state.threads.filter((t) => t.id !== action.threadId); |
| 902 | 949 | const activeThreadId = |
| 903 | 950 | state.activeThreadId === action.threadId ? (threads[0]?.id ?? null) : state.activeThreadId; |
| 904 | return { ...state, threads, activeThreadId }; | |
| 951 | const shouldClearDiffTarget = state.diffThreadId === action.threadId; | |
| 952 | return { | |
| 953 | ...state, | |
| 954 | threads, | |
| 955 | activeThreadId, | |
| 956 | ...(shouldClearDiffTarget | |
| 957 | ? { | |
| 958 | diffOpen: false, | |
| 959 | diffThreadId: null, | |
| 960 | diffTurnId: null, | |
| 961 | diffFilePath: null, | |
| 962 | } | |
| 963 | : {}), | |
| 964 | }; | |
| 905 | 965 | } |
| 906 | 966 | |
| 907 | 967 | default: |
bun.lock+39| ... | ... | @@ -52,6 +52,7 @@ |
| 52 | 52 | "version": "0.0.0", |
| 53 | 53 | "dependencies": { |
| 54 | 54 | "@base-ui/react": "^1.2.0", |
| 55 | "@pierre/diffs": "^1.0.11", | |
| 55 | 56 | "@t3tools/contracts": "workspace:*", |
| 56 | 57 | "@tanstack/react-pacer": "^0.19.4", |
| 57 | 58 | "@tanstack/react-query": "^5.90.0", |
| ... | ... | @@ -304,6 +305,8 @@ |
| 304 | 305 | |
| 305 | 306 | "@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.46.0", "", { "os": "win32", "cpu": "x64" }, "sha512-DJbQsSJUr4KSi9uU0QqOgI7PX2C+fKGZX+YDprt3vM2sC0dWZsgVTLoN2vtkNyEWJSY2mnvRFUshWXT3bmo0Ug=="], |
| 306 | 307 | |
| 308 | "@pierre/diffs": ["@pierre/diffs@1.0.11", "", { "dependencies": { "@shikijs/core": "^3.0.0", "@shikijs/engine-javascript": "^3.0.0", "@shikijs/transformers": "^3.0.0", "diff": "8.0.3", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "^3.0.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-j6zIEoyImQy1HfcJqbrDwP0O5I7V2VNXAaw53FqQ+SykRfaNwABeZHs9uibXO4supaXPmTx6LEH9Lffr03e1Tw=="], | |
| 309 | ||
| 307 | 310 | "@quansync/fs": ["@quansync/fs@1.0.0", "", { "dependencies": { "quansync": "^1.0.0" } }, "sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ=="], |
| 308 | 311 | |
| 309 | 312 | "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.3", "", { "os": "android", "cpu": "arm64" }, "sha512-0T1k9FinuBZ/t7rZ8jN6OpUKPnUjNdYHoj/cESWrQ3ZraAJ4OMm6z7QjSfCxqj8mOp9kTKc1zHK3kGz5vMu+nQ=="], |
| ... | ... | @@ -384,6 +387,22 @@ |
| 384 | 387 | |
| 385 | 388 | "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.57.1", "", { "os": "win32", "cpu": "x64" }, "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA=="], |
| 386 | 389 | |
| 390 | "@shikijs/core": ["@shikijs/core@3.22.0", "", { "dependencies": { "@shikijs/types": "3.22.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-iAlTtSDDbJiRpvgL5ugKEATDtHdUVkqgHDm/gbD2ZS9c88mx7G1zSYjjOxp5Qa0eaW0MAQosFRmJSk354PRoQA=="], | |
| 391 | ||
| 392 | "@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.22.0", "", { "dependencies": { "@shikijs/types": "3.22.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-jdKhfgW9CRtj3Tor0L7+yPwdG3CgP7W+ZEqSsojrMzCjD1e0IxIbwUMDDpYlVBlC08TACg4puwFGkZfLS+56Tw=="], | |
| 393 | ||
| 394 | "@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.22.0", "", { "dependencies": { "@shikijs/types": "3.22.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-DyXsOG0vGtNtl7ygvabHd7Mt5EY8gCNqR9Y7Lpbbd/PbJvgWrqaKzH1JW6H6qFkuUa8aCxoiYVv8/YfFljiQxA=="], | |
| 395 | ||
| 396 | "@shikijs/langs": ["@shikijs/langs@3.22.0", "", { "dependencies": { "@shikijs/types": "3.22.0" } }, "sha512-x/42TfhWmp6H00T6uwVrdTJGKgNdFbrEdhaDwSR5fd5zhQ1Q46bHq9EO61SCEWJR0HY7z2HNDMaBZp8JRmKiIA=="], | |
| 397 | ||
| 398 | "@shikijs/themes": ["@shikijs/themes@3.22.0", "", { "dependencies": { "@shikijs/types": "3.22.0" } }, "sha512-o+tlOKqsr6FE4+mYJG08tfCFDS+3CG20HbldXeVoyP+cYSUxDhrFf3GPjE60U55iOkkjbpY2uC3It/eeja35/g=="], | |
| 399 | ||
| 400 | "@shikijs/transformers": ["@shikijs/transformers@3.22.0", "", { "dependencies": { "@shikijs/core": "3.22.0", "@shikijs/types": "3.22.0" } }, "sha512-E7eRV7mwDBjueLF6852n2oYeJYxBq3NSsDk+uyruYAXONv4U8holGmIrT+mPRJQ1J1SNOH6L8G19KRzmBawrFw=="], | |
| 401 | ||
| 402 | "@shikijs/types": ["@shikijs/types@3.22.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-491iAekgKDBFE67z70Ok5a8KBMsQ2IJwOWw3us/7ffQkIBCyOQfm/aNwVMBUriP02QshIfgHCBSIYAl3u2eWjg=="], | |
| 403 | ||
| 404 | "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="], | |
| 405 | ||
| 387 | 406 | "@sindresorhus/is": ["@sindresorhus/is@4.6.0", "", {}, "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw=="], |
| 388 | 407 | |
| 389 | 408 | "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], |
| ... | ... | @@ -622,6 +641,8 @@ |
| 622 | 641 | |
| 623 | 642 | "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], |
| 624 | 643 | |
| 644 | "diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="], | |
| 645 | ||
| 625 | 646 | "dts-resolver": ["dts-resolver@2.1.3", "", { "peerDependencies": { "oxc-resolver": ">=11.0.0" }, "optionalPeers": ["oxc-resolver"] }, "sha512-bihc7jPC90VrosXNzK0LTE2cuLP6jr0Ro8jk+kMugHReJVLIpHz/xadeq3MhuwyO4TD4OA3L1Q8pBBFRc08Tsw=="], |
| 626 | 647 | |
| 627 | 648 | "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], |
| ... | ... | @@ -722,6 +743,8 @@ |
| 722 | 743 | |
| 723 | 744 | "hast-util-is-element": ["hast-util-is-element@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g=="], |
| 724 | 745 | |
| 746 | "hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="], | |
| 747 | ||
| 725 | 748 | "hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="], |
| 726 | 749 | |
| 727 | 750 | "hast-util-to-text": ["hast-util-to-text@4.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "hast-util-is-element": "^3.0.0", "unist-util-find-after": "^5.0.0" } }, "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A=="], |
| ... | ... | @@ -736,6 +759,8 @@ |
| 736 | 759 | |
| 737 | 760 | "html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="], |
| 738 | 761 | |
| 762 | "html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="], | |
| 763 | ||
| 739 | 764 | "http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="], |
| 740 | 765 | |
| 741 | 766 | "http2-wrapper": ["http2-wrapper@1.0.3", "", { "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.0.0" } }, "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg=="], |
| ... | ... | @@ -822,6 +847,8 @@ |
| 822 | 847 | |
| 823 | 848 | "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], |
| 824 | 849 | |
| 850 | "lru_map": ["lru_map@0.4.1", "", {}, "sha512-I+lBvqMMFfqaV8CJCISjI3wbjmwVu/VyOoU7+qtu9d7ioW5klMgsTTiUOUp+DJvfTTzKXoPbyC6YfgkNcyPSOg=="], | |
| 851 | ||
| 825 | 852 | "lucide-react": ["lucide-react@0.564.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-JJ8GVTQqFwuliifD48U6+h7DXEHdkhJ/E87kksGByII3qHxtPciVb8T8woQONHBQgHVOl7rSMrrip3SeVNy7Fg=="], |
| 826 | 853 | |
| 827 | 854 | "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], |
| ... | ... | @@ -952,6 +979,10 @@ |
| 952 | 979 | |
| 953 | 980 | "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], |
| 954 | 981 | |
| 982 | "oniguruma-parser": ["oniguruma-parser@0.12.1", "", {}, "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w=="], | |
| 983 | ||
| 984 | "oniguruma-to-es": ["oniguruma-to-es@4.3.4", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.0.1", "regex-recursion": "^6.0.2" } }, "sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA=="], | |
| 985 | ||
| 955 | 986 | "open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], |
| 956 | 987 | |
| 957 | 988 | "outvariant": ["outvariant@1.4.3", "", {}, "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA=="], |
| ... | ... | @@ -998,6 +1029,12 @@ |
| 998 | 1029 | |
| 999 | 1030 | "react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="], |
| 1000 | 1031 | |
| 1032 | "regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="], | |
| 1033 | ||
| 1034 | "regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="], | |
| 1035 | ||
| 1036 | "regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="], | |
| 1037 | ||
| 1001 | 1038 | "rehype-highlight": ["rehype-highlight@7.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-to-text": "^4.0.0", "lowlight": "^3.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-k158pK7wdC2qL3M5NcZROZ2tR/l7zOzjxXd5VGdcfIyoijjQqpHd3JKtYSBDpDZ38UI2WJWuFAtkMDxmx5kstA=="], |
| 1002 | 1039 | |
| 1003 | 1040 | "remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="], |
| ... | ... | @@ -1046,6 +1083,8 @@ |
| 1046 | 1083 | |
| 1047 | 1084 | "serialize-error": ["serialize-error@7.0.1", "", { "dependencies": { "type-fest": "^0.13.1" } }, "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw=="], |
| 1048 | 1085 | |
| 1086 | "shiki": ["shiki@3.22.0", "", { "dependencies": { "@shikijs/core": "3.22.0", "@shikijs/engine-javascript": "3.22.0", "@shikijs/engine-oniguruma": "3.22.0", "@shikijs/langs": "3.22.0", "@shikijs/themes": "3.22.0", "@shikijs/types": "3.22.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-LBnhsoYEe0Eou4e1VgJACes+O6S6QC0w71fCSp5Oya79inkwkm15gQ1UF6VtQ8j/taMDh79hAB49WUk8ALQW3g=="], | |
| 1087 | ||
| 1049 | 1088 | "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], |
| 1050 | 1089 | |
| 1051 | 1090 | "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], |