| 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,6 +12,7 @@ |
| 12 | }, | 12 | }, |
| 13 | "dependencies": { | 13 | "dependencies": { |
| 14 | "@base-ui/react": "^1.2.0", | 14 | "@base-ui/react": "^1.2.0", |
| 15 | "@pierre/diffs": "^1.0.11", | ||
| 15 | "@t3tools/contracts": "workspace:*", | 16 | "@t3tools/contracts": "workspace:*", |
| 16 | "@tanstack/react-pacer": "^0.19.4", | 17 | "@tanstack/react-pacer": "^0.19.4", |
| 17 | "@tanstack/react-query": "^5.90.0", | 18 | "@tanstack/react-query": "^5.90.0", |
apps/web/src/App.tsx+14-3| ... | @@ -1,8 +1,7 @@ | ... | @@ -1,8 +1,7 @@ |
| 1 | import { QueryClient, QueryClientProvider, useQueryClient } from "@tanstack/react-query"; | 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 | import ChatView from "./components/ChatView"; | 4 | import ChatView from "./components/ChatView"; |
| 5 | import DiffPanel from "./components/DiffPanel"; | ||
| 6 | import Sidebar from "./components/Sidebar"; | 5 | import Sidebar from "./components/Sidebar"; |
| 7 | import { isElectron } from "./env"; | 6 | import { isElectron } from "./env"; |
| 8 | import { DEFAULT_MODEL } from "./model-logic"; | 7 | import { DEFAULT_MODEL } from "./model-logic"; |
| ... | @@ -13,6 +12,8 @@ import { useNativeApi } from "./hooks/useNativeApi"; | ... | @@ -13,6 +12,8 @@ import { useNativeApi } from "./hooks/useNativeApi"; |
| 13 | import { AnchoredToastProvider, ToastProvider } from "./components/ui/toast"; | 12 | import { AnchoredToastProvider, ToastProvider } from "./components/ui/toast"; |
| 14 | import { invalidateGitQueries } from "./lib/gitReactQuery"; | 13 | import { invalidateGitQueries } from "./lib/gitReactQuery"; |
| 15 | 14 | ||
| 15 | const DiffPanel = lazy(() => import("./components/DiffPanel")); | ||
| 16 | |||
| 16 | function EventRouter() { | 17 | function EventRouter() { |
| 17 | const api = useNativeApi(); | 18 | const api = useNativeApi(); |
| 18 | const { dispatch } = useStore(); | 19 | const { dispatch } = useStore(); |
| ... | @@ -195,7 +196,17 @@ function Layout() { | ... | @@ -195,7 +196,17 @@ function Layout() { |
| 195 | <DesktopProjectBootstrap /> | 196 | <DesktopProjectBootstrap /> |
| 196 | <Sidebar /> | 197 | <Sidebar /> |
| 197 | <ChatView /> | 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 | </div> | 210 | </div> |
| 200 | ); | 211 | ); |
| 201 | } | 212 | } |
apps/web/src/components/ChatView.tsx+83-2| ... | @@ -52,7 +52,9 @@ import { | ... | @@ -52,7 +52,9 @@ import { |
| 52 | import { | 52 | import { |
| 53 | derivePendingApprovals, | 53 | derivePendingApprovals, |
| 54 | derivePhase, | 54 | derivePhase, |
| 55 | deriveTurnDiffSummaries, | ||
| 55 | deriveTimelineEntries, | 56 | deriveTimelineEntries, |
| 57 | type TurnDiffSummary, | ||
| 56 | type PendingApproval, | 58 | type PendingApproval, |
| 57 | deriveWorkLogEntries, | 59 | deriveWorkLogEntries, |
| 58 | formatDuration, | 60 | formatDuration, |
| ... | @@ -406,6 +408,18 @@ export default function ChatView() { | ... | @@ -406,6 +408,18 @@ export default function ChatView() { |
| 406 | () => deriveTimelineEntries(activeThread?.messages ?? [], workLogEntries), | 408 | () => deriveTimelineEntries(activeThread?.messages ?? [], workLogEntries), |
| 407 | [activeThread?.messages, workLogEntries], | 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 | const completionSummary = useMemo(() => { | 423 | const completionSummary = useMemo(() => { |
| 410 | if (!activeThread?.latestTurnStartedAt) return null; | 424 | if (!activeThread?.latestTurnStartedAt) return null; |
| 411 | if (!activeThread.latestTurnCompletedAt) return null; | 425 | if (!activeThread.latestTurnCompletedAt) return null; |
| ... | @@ -1685,8 +1699,36 @@ export default function ChatView() { | ... | @@ -1685,8 +1699,36 @@ export default function ChatView() { |
| 1685 | setExpandedImage(image); | 1699 | setExpandedImage(image); |
| 1686 | }, []); | 1700 | }, []); |
| 1687 | const onToggleDiff = useCallback(() => { | 1701 | const onToggleDiff = useCallback(() => { |
| 1688 | dispatch({ type: "TOGGLE_DIFF" }); | 1702 | if (state.diffOpen) { |
| 1689 | }, [dispatch]); | 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 | // Empty state: no active thread | 1733 | // Empty state: no active thread |
| 1692 | if (!activeThread) { | 1734 | if (!activeThread) { |
| ... | @@ -1761,9 +1803,11 @@ export default function ChatView() { | ... | @@ -1761,9 +1803,11 @@ export default function ChatView() { |
| 1761 | completionDividerBeforeEntryId={completionDividerBeforeEntryId} | 1803 | completionDividerBeforeEntryId={completionDividerBeforeEntryId} |
| 1762 | completionSummary={completionSummary} | 1804 | completionSummary={completionSummary} |
| 1763 | assistantCompletionByItemId={assistantCompletionByItemId} | 1805 | assistantCompletionByItemId={assistantCompletionByItemId} |
| 1806 | turnDiffSummaryByAssistantMessageId={turnDiffSummaryByAssistantMessageId} | ||
| 1764 | nowIso={nowIso} | 1807 | nowIso={nowIso} |
| 1765 | expandedWorkGroups={expandedWorkGroups} | 1808 | expandedWorkGroups={expandedWorkGroups} |
| 1766 | onToggleWorkGroup={onToggleWorkGroup} | 1809 | onToggleWorkGroup={onToggleWorkGroup} |
| 1810 | onOpenTurnDiff={onOpenTurnDiff} | ||
| 1767 | onImageExpand={onExpandTimelineImage} | 1811 | onImageExpand={onExpandTimelineImage} |
| 1768 | messagesEndRef={messagesEndRef} | 1812 | messagesEndRef={messagesEndRef} |
| 1769 | /> | 1813 | /> |
| ... | @@ -2268,9 +2312,11 @@ interface MessagesTimelineProps { | ... | @@ -2268,9 +2312,11 @@ interface MessagesTimelineProps { |
| 2268 | completionDividerBeforeEntryId: string | null; | 2312 | completionDividerBeforeEntryId: string | null; |
| 2269 | completionSummary: string | null; | 2313 | completionSummary: string | null; |
| 2270 | assistantCompletionByItemId: Map<string, string>; | 2314 | assistantCompletionByItemId: Map<string, string>; |
| 2315 | turnDiffSummaryByAssistantMessageId: Map<string, TurnDiffSummary>; | ||
| 2271 | nowIso: string; | 2316 | nowIso: string; |
| 2272 | expandedWorkGroups: Record<string, boolean>; | 2317 | expandedWorkGroups: Record<string, boolean>; |
| 2273 | onToggleWorkGroup: (groupId: string) => void; | 2318 | onToggleWorkGroup: (groupId: string) => void; |
| 2319 | onOpenTurnDiff: (turnId: string, filePath?: string) => void; | ||
| 2274 | onImageExpand: (image: ExpandedImagePreview) => void; | 2320 | onImageExpand: (image: ExpandedImagePreview) => void; |
| 2275 | messagesEndRef: RefObject<HTMLDivElement | null>; | 2321 | messagesEndRef: RefObject<HTMLDivElement | null>; |
| 2276 | } | 2322 | } |
| ... | @@ -2282,9 +2328,11 @@ const MessagesTimeline = memo(function MessagesTimeline({ | ... | @@ -2282,9 +2328,11 @@ const MessagesTimeline = memo(function MessagesTimeline({ |
| 2282 | completionDividerBeforeEntryId, | 2328 | completionDividerBeforeEntryId, |
| 2283 | completionSummary, | 2329 | completionSummary, |
| 2284 | assistantCompletionByItemId, | 2330 | assistantCompletionByItemId, |
| 2331 | turnDiffSummaryByAssistantMessageId, | ||
| 2285 | nowIso, | 2332 | nowIso, |
| 2286 | expandedWorkGroups, | 2333 | expandedWorkGroups, |
| 2287 | onToggleWorkGroup, | 2334 | onToggleWorkGroup, |
| 2335 | onOpenTurnDiff, | ||
| 2288 | onImageExpand, | 2336 | onImageExpand, |
| 2289 | messagesEndRef, | 2337 | messagesEndRef, |
| 2290 | }: MessagesTimelineProps) { | 2338 | }: MessagesTimelineProps) { |
| ... | @@ -2446,6 +2494,39 @@ const MessagesTimeline = memo(function MessagesTimeline({ | ... | @@ -2446,6 +2494,39 @@ const MessagesTimeline = memo(function MessagesTimeline({ |
| 2446 | (timelineEntry.message.streaming ? "" : "(empty response)") | 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 | <p className="mt-1.5 text-[10px] text-muted-foreground/30"> | 2530 | <p className="mt-1.5 text-[10px] text-muted-foreground/30"> |
| 2450 | {formatMessageMeta( | 2531 | {formatMessageMeta( |
| 2451 | timelineEntry.message.createdAt, | 2532 | timelineEntry.message.createdAt, |
apps/web/src/components/DiffPanel.tsx+123-36| ... | @@ -1,58 +1,145 @@ | ... | @@ -1,58 +1,145 @@ |
| 1 | import { PatchDiff } from "@pierre/diffs/react"; | ||
| 1 | import { XIcon } from "lucide-react"; | 2 | import { XIcon } from "lucide-react"; |
| 3 | import { useMemo } from "react"; | ||
| 2 | import { isElectron } from "../env"; | 4 | import { isElectron } from "../env"; |
| 5 | import { deriveTurnDiffSummaries, formatTimestamp } from "../session-logic"; | ||
| 3 | import { useStore } from "../store"; | 6 | import { useStore } from "../store"; |
| 4 | import { Button } from "./ui/button"; | 7 | import { Button } from "./ui/button"; |
| 8 | import { cn } from "~/lib/utils"; | ||
| 5 | 9 | ||
| 6 | export default function DiffPanel() { | 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 | return ( | 48 | return ( |
| 10 | <aside className="flex h-full w-[400px] shrink-0 flex-col border-l border-border bg-card"> | 49 | <aside className="flex h-full w-[560px] shrink-0 flex-col border-l border-border bg-card"> |
| 11 | {/* Header */} | ||
| 12 | <div | 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> | 56 | <h3 className="text-xs font-medium text-foreground">Turn diffs</h3> |
| 16 | <Button | 57 | <Button type="button" size="icon-xs" variant="ghost" onClick={() => dispatch({ type: "CLOSE_DIFF" })}> |
| 17 | type="button" | ||
| 18 | size="icon-xs" | ||
| 19 | variant="ghost" | ||
| 20 | onClick={() => dispatch({ type: "TOGGLE_DIFF" })} | ||
| 21 | > | ||
| 22 | <XIcon /> | 58 | <XIcon /> |
| 23 | </Button> | 59 | </Button> |
| 24 | </div> | 60 | </div> |
| 25 | 61 | ||
| 26 | {/* Placeholder content */} | 62 | {!activeThread ? ( |
| 27 | <div className="flex-1 overflow-y-auto p-4"> | 63 | <div className="flex flex-1 items-center justify-center px-5 text-center text-xs text-muted-foreground/70"> |
| 28 | <div className="space-y-3"> | 64 | Select a thread to inspect turn diffs. |
| 29 | <div className="rounded-lg border border-border/80 bg-muted p-3"> | 65 | </div> |
| 30 | <div className="mb-2 flex items-center gap-2"> | 66 | ) : turnDiffSummaries.length === 0 ? ( |
| 31 | <span className="rounded bg-emerald-500/20 px-1.5 py-0.5 text-[9px] font-medium text-emerald-400"> | 67 | <div className="flex flex-1 items-center justify-center px-5 text-center text-xs text-muted-foreground/70"> |
| 32 | M | 68 | No completed turn file changes yet. |
| 33 | </span> | 69 | </div> |
| 34 | <span className="font-mono text-xs text-muted-foreground/80">src/App.tsx</span> | 70 | ) : ( |
| 35 | </div> | 71 | <> |
| 36 | <div className="space-y-0.5 font-mono text-[11px]"> | 72 | <div className="border-b border-border px-3 py-2.5"> |
| 37 | <div className="text-red-400/60">- const oldValue = "hello";</div> | 73 | <p className="mb-1.5 text-[10px] uppercase tracking-[0.12em] text-muted-foreground/60"> |
| 38 | <div className="text-emerald-400/60">+ const newValue = "world";</div> | 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 | </div> | 94 | </div> |
| 40 | </div> | 95 | </div> |
| 41 | 96 | ||
| 42 | <div className="rounded-lg border border-border/80 bg-muted p-3"> | 97 | <div className="min-h-0 flex flex-1"> |
| 43 | <div className="flex items-center gap-2"> | 98 | <div className="w-[220px] shrink-0 border-r border-border px-2 py-2"> |
| 44 | <span className="rounded bg-blue-500/20 px-1.5 py-0.5 text-[9px] font-medium text-blue-400"> | 99 | <p className="mb-1.5 px-1 text-[10px] uppercase tracking-[0.12em] text-muted-foreground/60"> |
| 45 | A | 100 | Files |
| 46 | </span> | 101 | </p> |
| 47 | <span className="font-mono text-xs text-muted-foreground/80">src/utils.ts</span> | 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 | </div> | 124 | </div> |
| 49 | </div> | ||
| 50 | </div> | ||
| 51 | 125 | ||
| 52 | <p className="mt-6 text-center text-[10px] text-muted-foreground/30"> | 126 | <div className="min-w-0 flex-1 overflow-auto px-3 py-2"> |
| 53 | Diff integration coming soon | 127 | {!canApplyStoredTarget && state.diffThreadId && ( |
| 54 | </p> | 128 | <p className="mb-2 text-[11px] text-muted-foreground/65"> |
| 55 | </div> | 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 | </aside> | 143 | </aside> |
| 57 | ); | 144 | ); |
| 58 | } | 145 | } |
apps/web/src/session-logic.test.ts+104| ... | @@ -5,6 +5,7 @@ import { | ... | @@ -5,6 +5,7 @@ import { |
| 5 | type WorkLogEntry, | 5 | type WorkLogEntry, |
| 6 | applyEventToMessages, | 6 | applyEventToMessages, |
| 7 | derivePendingApprovals, | 7 | derivePendingApprovals, |
| 8 | deriveTurnDiffSummaries, | ||
| 8 | deriveTimelineEntries, | 9 | deriveTimelineEntries, |
| 9 | deriveWorkLogEntries, | 10 | deriveWorkLogEntries, |
| 10 | evolveSession, | 11 | evolveSession, |
| ... | @@ -393,6 +394,109 @@ describe("deriveWorkLogEntries", () => { | ... | @@ -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 | describe("derivePendingApprovals", () => { | 500 | describe("derivePendingApprovals", () => { |
| 397 | it("returns pending command/file-change approvals", () => { | 501 | it("returns pending command/file-change approvals", () => { |
| 398 | const approvals = derivePendingApprovals([ | 502 | const approvals = derivePendingApprovals([ |
apps/web/src/session-logic.ts+230| ... | @@ -99,12 +99,31 @@ export type TimelineEntry = | ... | @@ -99,12 +99,31 @@ export type TimelineEntry = |
| 99 | entry: WorkLogEntry; | 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 | function normalizeDetail(value: string | undefined): string | undefined { | 117 | function normalizeDetail(value: string | undefined): string | undefined { |
| 103 | if (!value) return undefined; | 118 | if (!value) return undefined; |
| 104 | const trimmed = value.trim(); | 119 | const trimmed = value.trim(); |
| 105 | return trimmed.length > 0 ? trimmed : undefined; | 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 | function approvalDetail(event: ProviderEvent): string | undefined { | 127 | function approvalDetail(event: ProviderEvent): string | undefined { |
| 109 | const payload = asObject(event.payload); | 128 | const payload = asObject(event.payload); |
| 110 | const command = asString(payload?.command); | 129 | const command = asString(payload?.command); |
| ... | @@ -438,6 +457,217 @@ export function deriveWorkLogEntries( | ... | @@ -438,6 +457,217 @@ export function deriveWorkLogEntries( |
| 438 | return entries; | 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 | function toTimestamp(isoDate: string): number { | 671 | function toTimestamp(isoDate: string): number { |
| 442 | const parsed = Date.parse(isoDate); | 672 | const parsed = Date.parse(isoDate); |
| 443 | return Number.isNaN(parsed) ? Number.POSITIVE_INFINITY : parsed; | 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,6 +81,9 @@ function makeState(thread: Thread): AppState { |
| 81 | activeThreadId: thread.id, | 81 | activeThreadId: thread.id, |
| 82 | runtimeMode: "full-access", | 82 | runtimeMode: "full-access", |
| 83 | diffOpen: false, | 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,6 +148,21 @@ describe("store reducer thread continuity", () => { |
| 145 | expect(next.threads[0]?.terminalOpen).toBe(true); | 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 | it("sets terminal open state per thread", () => { | 166 | it("sets terminal open state per thread", () => { |
| 149 | const state = makeState(makeThread({ terminalOpen: true })); | 167 | const state = makeState(makeThread({ terminalOpen: true })); |
| 150 | const next = reducer(state, { | 168 | const next = reducer(state, { |
| ... | @@ -532,6 +550,9 @@ describe("store reducer thread continuity", () => { | ... | @@ -532,6 +550,9 @@ describe("store reducer thread continuity", () => { |
| 532 | activeThreadId: "thread-b", | 550 | activeThreadId: "thread-b", |
| 533 | runtimeMode: "full-access", | 551 | runtimeMode: "full-access", |
| 534 | diffOpen: false, | 552 | diffOpen: false, |
| 553 | diffThreadId: null, | ||
| 554 | diffTurnId: null, | ||
| 555 | diffFilePath: null, | ||
| 535 | }; | 556 | }; |
| 536 | 557 | ||
| 537 | const next = reducer(state, { | 558 | const next = reducer(state, { |
| ... | @@ -661,6 +682,9 @@ describe("store reducer thread continuity", () => { | ... | @@ -661,6 +682,9 @@ describe("store reducer thread continuity", () => { |
| 661 | activeThreadId: "thread-a", | 682 | activeThreadId: "thread-a", |
| 662 | runtimeMode: "full-access", | 683 | runtimeMode: "full-access", |
| 663 | diffOpen: false, | 684 | diffOpen: false, |
| 685 | diffThreadId: null, | ||
| 686 | diffTurnId: null, | ||
| 687 | diffFilePath: null, | ||
| 664 | }; | 688 | }; |
| 665 | 689 | ||
| 666 | const next = reducer(state, { | 690 | const next = reducer(state, { |
apps/web/src/store.ts+62-2| ... | @@ -42,6 +42,19 @@ type Action = | ... | @@ -42,6 +42,19 @@ type Action = |
| 42 | | { type: "SET_THREAD_ACTIVE_TERMINAL"; threadId: string; terminalId: string } | 42 | | { type: "SET_THREAD_ACTIVE_TERMINAL"; threadId: string; terminalId: string } |
| 43 | | { type: "CLOSE_THREAD_TERMINAL"; threadId: string; terminalId: string } | 43 | | { type: "CLOSE_THREAD_TERMINAL"; threadId: string; terminalId: string } |
| 44 | | { type: "TOGGLE_DIFF" } | 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 | type: "APPLY_EVENT"; | 59 | type: "APPLY_EVENT"; |
| 47 | event: ProviderEvent; | 60 | event: ProviderEvent; |
| ... | @@ -83,6 +96,9 @@ export interface AppState { | ... | @@ -83,6 +96,9 @@ export interface AppState { |
| 83 | activeThreadId: string | null; | 96 | activeThreadId: string | null; |
| 84 | runtimeMode: RuntimeMode; | 97 | runtimeMode: RuntimeMode; |
| 85 | diffOpen: boolean; | 98 | diffOpen: boolean; |
| 99 | diffThreadId: string | null; | ||
| 100 | diffTurnId: string | null; | ||
| 101 | diffFilePath: string | null; | ||
| 86 | } | 102 | } |
| 87 | 103 | ||
| 88 | const PERSISTED_STATE_KEY = "t3code:renderer-state:v7"; | 104 | const PERSISTED_STATE_KEY = "t3code:renderer-state:v7"; |
| ... | @@ -103,6 +119,9 @@ const initialState: AppState = { | ... | @@ -103,6 +119,9 @@ const initialState: AppState = { |
| 103 | activeThreadId: null, | 119 | activeThreadId: null, |
| 104 | runtimeMode: DEFAULT_RUNTIME_MODE, | 120 | runtimeMode: DEFAULT_RUNTIME_MODE, |
| 105 | diffOpen: false, | 121 | diffOpen: false, |
| 122 | diffThreadId: null, | ||
| 123 | diffTurnId: null, | ||
| 124 | diffFilePath: null, | ||
| 106 | }; | 125 | }; |
| 107 | 126 | ||
| 108 | // ── Helpers ────────────────────────────────────────────────────────── | 127 | // ── Helpers ────────────────────────────────────────────────────────── |
| ... | @@ -125,7 +144,15 @@ function readPersistedState(): AppState { | ... | @@ -125,7 +144,15 @@ function readPersistedState(): AppState { |
| 125 | ? hydrated.activeThreadId | 144 | ? hydrated.activeThreadId |
| 126 | : (threads[0]?.id ?? null); | 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 | } catch { | 156 | } catch { |
| 130 | return initialState; | 157 | return initialState; |
| 131 | } | 158 | } |
| ... | @@ -717,6 +744,26 @@ export function reducer(state: AppState, action: Action): AppState { | ... | @@ -717,6 +744,26 @@ export function reducer(state: AppState, action: Action): AppState { |
| 717 | case "TOGGLE_DIFF": | 744 | case "TOGGLE_DIFF": |
| 718 | return { ...state, diffOpen: !state.diffOpen }; | 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 | case "APPLY_TERMINAL_EVENT": | 767 | case "APPLY_TERMINAL_EVENT": |
| 721 | if (!state.threads.some((thread) => thread.id === action.event.threadId)) { | 768 | if (!state.threads.some((thread) => thread.id === action.event.threadId)) { |
| 722 | return state; | 769 | return state; |
| ... | @@ -901,7 +948,20 @@ export function reducer(state: AppState, action: Action): AppState { | ... | @@ -901,7 +948,20 @@ export function reducer(state: AppState, action: Action): AppState { |
| 901 | const threads = state.threads.filter((t) => t.id !== action.threadId); | 948 | const threads = state.threads.filter((t) => t.id !== action.threadId); |
| 902 | const activeThreadId = | 949 | const activeThreadId = |
| 903 | state.activeThreadId === action.threadId ? (threads[0]?.id ?? null) : state.activeThreadId; | 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 | default: | 967 | default: |
bun.lock+39| ... | @@ -52,6 +52,7 @@ | ... | @@ -52,6 +52,7 @@ |
| 52 | "version": "0.0.0", | 52 | "version": "0.0.0", |
| 53 | "dependencies": { | 53 | "dependencies": { |
| 54 | "@base-ui/react": "^1.2.0", | 54 | "@base-ui/react": "^1.2.0", |
| 55 | "@pierre/diffs": "^1.0.11", | ||
| 55 | "@t3tools/contracts": "workspace:*", | 56 | "@t3tools/contracts": "workspace:*", |
| 56 | "@tanstack/react-pacer": "^0.19.4", | 57 | "@tanstack/react-pacer": "^0.19.4", |
| 57 | "@tanstack/react-query": "^5.90.0", | 58 | "@tanstack/react-query": "^5.90.0", |
| ... | @@ -304,6 +305,8 @@ | ... | @@ -304,6 +305,8 @@ |
| 304 | 305 | ||
| 305 | "@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.46.0", "", { "os": "win32", "cpu": "x64" }, "sha512-DJbQsSJUr4KSi9uU0QqOgI7PX2C+fKGZX+YDprt3vM2sC0dWZsgVTLoN2vtkNyEWJSY2mnvRFUshWXT3bmo0Ug=="], | 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 | "@quansync/fs": ["@quansync/fs@1.0.0", "", { "dependencies": { "quansync": "^1.0.0" } }, "sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ=="], | 310 | "@quansync/fs": ["@quansync/fs@1.0.0", "", { "dependencies": { "quansync": "^1.0.0" } }, "sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ=="], |
| 308 | 311 | ||
| 309 | "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.3", "", { "os": "android", "cpu": "arm64" }, "sha512-0T1k9FinuBZ/t7rZ8jN6OpUKPnUjNdYHoj/cESWrQ3ZraAJ4OMm6z7QjSfCxqj8mOp9kTKc1zHK3kGz5vMu+nQ=="], | 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,6 +387,22 @@ |
| 384 | 387 | ||
| 385 | "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.57.1", "", { "os": "win32", "cpu": "x64" }, "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA=="], | 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 | "@sindresorhus/is": ["@sindresorhus/is@4.6.0", "", {}, "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw=="], | 406 | "@sindresorhus/is": ["@sindresorhus/is@4.6.0", "", {}, "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw=="], |
| 388 | 407 | ||
| 389 | "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], | 408 | "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], |
| ... | @@ -622,6 +641,8 @@ | ... | @@ -622,6 +641,8 @@ |
| 622 | 641 | ||
| 623 | "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], | 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 | "dts-resolver": ["dts-resolver@2.1.3", "", { "peerDependencies": { "oxc-resolver": ">=11.0.0" }, "optionalPeers": ["oxc-resolver"] }, "sha512-bihc7jPC90VrosXNzK0LTE2cuLP6jr0Ro8jk+kMugHReJVLIpHz/xadeq3MhuwyO4TD4OA3L1Q8pBBFRc08Tsw=="], | 646 | "dts-resolver": ["dts-resolver@2.1.3", "", { "peerDependencies": { "oxc-resolver": ">=11.0.0" }, "optionalPeers": ["oxc-resolver"] }, "sha512-bihc7jPC90VrosXNzK0LTE2cuLP6jr0Ro8jk+kMugHReJVLIpHz/xadeq3MhuwyO4TD4OA3L1Q8pBBFRc08Tsw=="], |
| 626 | 647 | ||
| 627 | "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=="], | 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,6 +743,8 @@ |
| 722 | 743 | ||
| 723 | "hast-util-is-element": ["hast-util-is-element@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g=="], | 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 | "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=="], | 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 | "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=="], | 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,6 +759,8 @@ |
| 736 | 759 | ||
| 737 | "html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="], | 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 | "http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="], | 764 | "http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="], |
| 740 | 765 | ||
| 741 | "http2-wrapper": ["http2-wrapper@1.0.3", "", { "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.0.0" } }, "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg=="], | 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,6 +847,8 @@ |
| 822 | 847 | ||
| 823 | "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], | 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 | "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=="], | 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 | "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], | 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,6 +979,10 @@ |
| 952 | 979 | ||
| 953 | "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], | 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 | "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=="], | 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 | "outvariant": ["outvariant@1.4.3", "", {}, "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA=="], | 988 | "outvariant": ["outvariant@1.4.3", "", {}, "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA=="], |
| ... | @@ -998,6 +1029,12 @@ | ... | @@ -998,6 +1029,12 @@ |
| 998 | 1029 | ||
| 999 | "react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="], | 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 | "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=="], | 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 | "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=="], | 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,6 +1083,8 @@ |
| 1046 | 1083 | ||
| 1047 | "serialize-error": ["serialize-error@7.0.1", "", { "dependencies": { "type-fest": "^0.13.1" } }, "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw=="], | 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 | "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], | 1088 | "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], |
| 1050 | 1089 | ||
| 1051 | "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], | 1090 | "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], |