| author | |
| committer | |
| log | ed9f561dc3c75f65acb9bd60174afbbbfc4c3652 |
| tree | 41279b43505b5fdd51fed169a4f2bc9c23c964ff |
| parent | 7de054fa4964e5cc6c406f39551dc62a2c214b38 |
- add route-based chat layout with index and `/$threadId` views
- navigate sidebar thread actions through router instead of store-only selection
- extract shared thread creation into `threadFactory` and wire new router deps10 files changed, 457 insertions(+), 309 deletions(-)
apps/web/package.json+1| ... | @@ -16,6 +16,7 @@ | ... | @@ -16,6 +16,7 @@ |
| 16 | "@t3tools/contracts": "workspace:*", | 16 | "@t3tools/contracts": "workspace:*", |
| 17 | "@tanstack/react-pacer": "^0.19.4", | 17 | "@tanstack/react-pacer": "^0.19.4", |
| 18 | "@tanstack/react-query": "^5.90.0", | 18 | "@tanstack/react-query": "^5.90.0", |
| 19 | "@tanstack/react-router": "^1.160.2", | ||
| 19 | "@xterm/addon-fit": "^0.11.0", | 20 | "@xterm/addon-fit": "^0.11.0", |
| 20 | "@xterm/xterm": "^6.0.0", | 21 | "@xterm/xterm": "^6.0.0", |
| 21 | "class-variance-authority": "^0.7.1", | 22 | "class-variance-authority": "^0.7.1", |
apps/web/src/App.tsx+5-261| ... | @@ -1,265 +1,9 @@ | ... | @@ -1,265 +1,9 @@ |
| 1 | import { QueryClient, QueryClientProvider, useQueryClient } from "@tanstack/react-query"; | 1 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; |
| 2 | import { Activity, Suspense, lazy, useEffect, useRef } from "react"; | 2 | import { RouterProvider } from "@tanstack/react-router"; |
| 3 | 3 | ||
| 4 | import ChatView from "./components/ChatView"; | ||
| 5 | import Sidebar from "./components/Sidebar"; | ||
| 6 | import { isElectron } from "./env"; | ||
| 7 | import { DEFAULT_MODEL } from "./model-logic"; | ||
| 8 | import { StoreProvider, useStore } from "./store"; | ||
| 9 | import { DEFAULT_THREAD_TERMINAL_HEIGHT, DEFAULT_THREAD_TERMINAL_ID } from "./types"; | ||
| 10 | import { onServerWelcome } from "./wsNativeApi"; | ||
| 11 | import { useNativeApi } from "./hooks/useNativeApi"; | ||
| 12 | import { useMediaQuery } from "./hooks/useMediaQuery"; | ||
| 13 | import { AnchoredToastProvider, ToastProvider } from "./components/ui/toast"; | 4 | import { AnchoredToastProvider, ToastProvider } from "./components/ui/toast"; |
| 14 | import { Sheet, SheetPopup } from "./components/ui/sheet"; | 5 | import { router } from "./router"; |
| 15 | import { invalidateGitQueries } from "./lib/gitReactQuery"; | 6 | import { StoreProvider } from "./store"; |
| 16 | |||
| 17 | const DiffPanel = lazy(() => import("./components/DiffPanel")); | ||
| 18 | const DiffWorkerPoolProvider = lazy(() => | ||
| 19 | import("./components/DiffPanel").then((module) => ({ | ||
| 20 | default: module.DiffWorkerPoolProvider, | ||
| 21 | })), | ||
| 22 | ); | ||
| 23 | const DIFF_INLINE_LAYOUT_MEDIA_QUERY = "(max-width: 1180px)"; | ||
| 24 | |||
| 25 | const DiffPanelWrapper = (props: { children: React.ReactNode; sheet: boolean }) => { | ||
| 26 | const { state, dispatch } = useStore(); | ||
| 27 | if (props.sheet) { | ||
| 28 | return ( | ||
| 29 | <Sheet | ||
| 30 | open={state.diffOpen} | ||
| 31 | onOpenChange={(open) => { | ||
| 32 | if (!open) { | ||
| 33 | dispatch({ type: "CLOSE_DIFF" }); | ||
| 34 | } | ||
| 35 | }} | ||
| 36 | > | ||
| 37 | <SheetPopup | ||
| 38 | side="right" | ||
| 39 | showCloseButton={false} | ||
| 40 | keepMounted | ||
| 41 | className="w-[min(88vw,820px)] max-w-[820px] p-0" | ||
| 42 | > | ||
| 43 | {props.children} | ||
| 44 | </SheetPopup> | ||
| 45 | </Sheet> | ||
| 46 | ); | ||
| 47 | } | ||
| 48 | |||
| 49 | return ( | ||
| 50 | <aside className={state.diffOpen ? undefined : "hidden"} aria-hidden={!state.diffOpen}> | ||
| 51 | {props.children} | ||
| 52 | </aside> | ||
| 53 | ); | ||
| 54 | }; | ||
| 55 | |||
| 56 | function EventRouter() { | ||
| 57 | const api = useNativeApi(); | ||
| 58 | const { dispatch } = useStore(); | ||
| 59 | const queryClient = useQueryClient(); | ||
| 60 | const activeAssistantItemRef = useRef<string | null>(null); | ||
| 61 | |||
| 62 | useEffect(() => { | ||
| 63 | if (!api) return; | ||
| 64 | return api.providers.onEvent((event) => { | ||
| 65 | if (event.method === "turn/completed") { | ||
| 66 | void invalidateGitQueries(queryClient); | ||
| 67 | } | ||
| 68 | dispatch({ | ||
| 69 | type: "APPLY_EVENT", | ||
| 70 | event, | ||
| 71 | activeAssistantItemRef, | ||
| 72 | }); | ||
| 73 | }); | ||
| 74 | }, [api, dispatch, queryClient]); | ||
| 75 | |||
| 76 | useEffect(() => { | ||
| 77 | if (!api) return; | ||
| 78 | return api.terminal.onEvent((event) => { | ||
| 79 | dispatch({ | ||
| 80 | type: "APPLY_TERMINAL_EVENT", | ||
| 81 | event, | ||
| 82 | }); | ||
| 83 | }); | ||
| 84 | }, [api, dispatch]); | ||
| 85 | |||
| 86 | return null; | ||
| 87 | } | ||
| 88 | |||
| 89 | function AutoProjectBootstrap() { | ||
| 90 | const { state, dispatch } = useStore(); | ||
| 91 | const bootstrappedRef = useRef(false); | ||
| 92 | |||
| 93 | useEffect(() => { | ||
| 94 | // Browser mode bootstraps from server welcome. | ||
| 95 | // Electron bootstraps from persisted projects via DesktopProjectBootstrap. | ||
| 96 | if (isElectron) return; | ||
| 97 | |||
| 98 | return onServerWelcome((payload) => { | ||
| 99 | if (bootstrappedRef.current) return; | ||
| 100 | |||
| 101 | // Don't create duplicate projects for the same cwd | ||
| 102 | const existing = state.projects.find((p) => p.cwd === payload.cwd); | ||
| 103 | if (existing) { | ||
| 104 | bootstrappedRef.current = true; | ||
| 105 | // Ensure a thread is active | ||
| 106 | const existingThread = state.threads.find((t) => t.projectId === existing.id); | ||
| 107 | if (existingThread && !state.activeThreadId) { | ||
| 108 | dispatch({ | ||
| 109 | type: "SET_ACTIVE_THREAD", | ||
| 110 | threadId: existingThread.id, | ||
| 111 | }); | ||
| 112 | } | ||
| 113 | return; | ||
| 114 | } | ||
| 115 | |||
| 116 | bootstrappedRef.current = true; | ||
| 117 | |||
| 118 | // Create project + thread from server cwd | ||
| 119 | const projectId = crypto.randomUUID(); | ||
| 120 | dispatch({ | ||
| 121 | type: "ADD_PROJECT", | ||
| 122 | project: { | ||
| 123 | id: projectId, | ||
| 124 | name: payload.projectName, | ||
| 125 | cwd: payload.cwd, | ||
| 126 | model: DEFAULT_MODEL, | ||
| 127 | expanded: true, | ||
| 128 | scripts: [], | ||
| 129 | }, | ||
| 130 | }); | ||
| 131 | dispatch({ | ||
| 132 | type: "ADD_THREAD", | ||
| 133 | thread: { | ||
| 134 | id: crypto.randomUUID(), | ||
| 135 | codexThreadId: null, | ||
| 136 | projectId, | ||
| 137 | title: "New thread", | ||
| 138 | model: DEFAULT_MODEL, | ||
| 139 | terminalOpen: false, | ||
| 140 | terminalHeight: DEFAULT_THREAD_TERMINAL_HEIGHT, | ||
| 141 | terminalIds: [DEFAULT_THREAD_TERMINAL_ID], | ||
| 142 | runningTerminalIds: [], | ||
| 143 | activeTerminalId: DEFAULT_THREAD_TERMINAL_ID, | ||
| 144 | terminalGroups: [ | ||
| 145 | { | ||
| 146 | id: `group-${DEFAULT_THREAD_TERMINAL_ID}`, | ||
| 147 | terminalIds: [DEFAULT_THREAD_TERMINAL_ID], | ||
| 148 | }, | ||
| 149 | ], | ||
| 150 | activeTerminalGroupId: `group-${DEFAULT_THREAD_TERMINAL_ID}`, | ||
| 151 | session: null, | ||
| 152 | messages: [], | ||
| 153 | events: [], | ||
| 154 | turnDiffSummaries: [], | ||
| 155 | error: null, | ||
| 156 | createdAt: new Date().toISOString(), | ||
| 157 | branch: null, | ||
| 158 | worktreePath: null, | ||
| 159 | }, | ||
| 160 | }); | ||
| 161 | }); | ||
| 162 | }, [state.projects, state.threads, state.activeThreadId, dispatch]); | ||
| 163 | |||
| 164 | return null; | ||
| 165 | } | ||
| 166 | |||
| 167 | function DesktopProjectBootstrap() { | ||
| 168 | const api = useNativeApi(); | ||
| 169 | const { dispatch } = useStore(); | ||
| 170 | const bootstrappedRef = useRef(false); | ||
| 171 | |||
| 172 | useEffect(() => { | ||
| 173 | if (!isElectron || !api || bootstrappedRef.current) return; | ||
| 174 | |||
| 175 | let disposed = false; | ||
| 176 | let retryDelayMs = 500; | ||
| 177 | let retryTimer: ReturnType<typeof setTimeout> | null = null; | ||
| 178 | |||
| 179 | const attemptBootstrap = async () => { | ||
| 180 | try { | ||
| 181 | const projects = await api.projects.list(); | ||
| 182 | if (disposed) return; | ||
| 183 | dispatch({ | ||
| 184 | type: "SYNC_PROJECTS", | ||
| 185 | projects: projects.map((project) => ({ | ||
| 186 | id: project.id, | ||
| 187 | name: project.name, | ||
| 188 | cwd: project.cwd, | ||
| 189 | model: DEFAULT_MODEL, | ||
| 190 | expanded: true, | ||
| 191 | scripts: project.scripts, | ||
| 192 | })), | ||
| 193 | }); | ||
| 194 | bootstrappedRef.current = true; | ||
| 195 | } catch { | ||
| 196 | if (disposed) return; | ||
| 197 | retryTimer = setTimeout(() => { | ||
| 198 | retryTimer = null; | ||
| 199 | void attemptBootstrap(); | ||
| 200 | }, retryDelayMs); | ||
| 201 | retryDelayMs = Math.min(retryDelayMs * 2, 5_000); | ||
| 202 | } | ||
| 203 | }; | ||
| 204 | |||
| 205 | void attemptBootstrap(); | ||
| 206 | |||
| 207 | return () => { | ||
| 208 | disposed = true; | ||
| 209 | if (retryTimer) { | ||
| 210 | clearTimeout(retryTimer); | ||
| 211 | } | ||
| 212 | }; | ||
| 213 | }, [api, dispatch]); | ||
| 214 | |||
| 215 | return null; | ||
| 216 | } | ||
| 217 | |||
| 218 | function Layout() { | ||
| 219 | const api = useNativeApi(); | ||
| 220 | const { state } = useStore(); | ||
| 221 | const shouldUseDiffSheet = useMediaQuery(DIFF_INLINE_LAYOUT_MEDIA_QUERY); | ||
| 222 | |||
| 223 | const diffLoadingFallback = | ||
| 224 | !state.diffOpen || shouldUseDiffSheet ? ( | ||
| 225 | <div className="flex h-full min-h-0 items-center justify-center px-4 text-center text-xs text-muted-foreground/70"> | ||
| 226 | Loading diff viewer... | ||
| 227 | </div> | ||
| 228 | ) : ( | ||
| 229 | <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"> | ||
| 230 | Loading diff viewer... | ||
| 231 | </aside> | ||
| 232 | ); | ||
| 233 | |||
| 234 | if (!api) { | ||
| 235 | return ( | ||
| 236 | <div className="flex h-screen flex-col bg-background text-foreground"> | ||
| 237 | <div className="flex flex-1 items-center justify-center"> | ||
| 238 | <p className="text-sm text-muted-foreground">Connecting to T3 Code server...</p> | ||
| 239 | </div> | ||
| 240 | </div> | ||
| 241 | ); | ||
| 242 | } | ||
| 243 | |||
| 244 | return ( | ||
| 245 | <div className="flex h-screen overflow-hidden bg-background text-foreground isolate"> | ||
| 246 | <EventRouter /> | ||
| 247 | <AutoProjectBootstrap /> | ||
| 248 | <DesktopProjectBootstrap /> | ||
| 249 | <Sidebar /> | ||
| 250 | <ChatView /> | ||
| 251 | <Activity mode={state.diffOpen ? "visible" : "hidden"}> | ||
| 252 | <DiffPanelWrapper sheet={shouldUseDiffSheet}> | ||
| 253 | <Suspense fallback={diffLoadingFallback}> | ||
| 254 | <DiffWorkerPoolProvider> | ||
| 255 | <DiffPanel mode={shouldUseDiffSheet ? "sheet" : "inline"} /> | ||
| 256 | </DiffWorkerPoolProvider> | ||
| 257 | </Suspense> | ||
| 258 | </DiffPanelWrapper> | ||
| 259 | </Activity> | ||
| 260 | </div> | ||
| 261 | ); | ||
| 262 | } | ||
| 263 | 7 | ||
| 264 | const queryClient = new QueryClient(); | 8 | const queryClient = new QueryClient(); |
| 265 | 9 | ||
| ... | @@ -269,7 +13,7 @@ export default function App() { | ... | @@ -269,7 +13,7 @@ export default function App() { |
| 269 | <StoreProvider> | 13 | <StoreProvider> |
| 270 | <ToastProvider> | 14 | <ToastProvider> |
| 271 | <AnchoredToastProvider> | 15 | <AnchoredToastProvider> |
| 272 | <Layout /> | 16 | <RouterProvider router={router} /> |
| 273 | </AnchoredToastProvider> | 17 | </AnchoredToastProvider> |
| 274 | </ToastProvider> | 18 | </ToastProvider> |
| 275 | </StoreProvider> | 19 | </StoreProvider> |
apps/web/src/components/ChatView.tsx+6-2| ... | @@ -337,7 +337,11 @@ const ComposerCommandMenu = memo(function ComposerCommandMenu(props: { | ... | @@ -337,7 +337,11 @@ const ComposerCommandMenu = memo(function ComposerCommandMenu(props: { |
| 337 | ); | 337 | ); |
| 338 | }); | 338 | }); |
| 339 | 339 | ||
| 340 | export default function ChatView() { | 340 | interface ChatViewProps { |
| 341 | threadId: string; | ||
| 342 | } | ||
| 343 | |||
| 344 | export default function ChatView({ threadId }: ChatViewProps) { | ||
| 341 | const { state, dispatch } = useStore(); | 345 | const { state, dispatch } = useStore(); |
| 342 | const api = useNativeApi(); | 346 | const api = useNativeApi(); |
| 343 | const { resolvedTheme } = useTheme(); | 347 | const { resolvedTheme } = useTheme(); |
| ... | @@ -375,7 +379,7 @@ export default function ChatView() { | ... | @@ -375,7 +379,7 @@ export default function ChatView() { |
| 375 | const terminalOpenByThreadRef = useRef<Record<string, boolean>>({}); | 379 | const terminalOpenByThreadRef = useRef<Record<string, boolean>>({}); |
| 376 | const checkpointHydrationSessionRequestRef = useRef(new Set<string>()); | 380 | const checkpointHydrationSessionRequestRef = useRef(new Set<string>()); |
| 377 | 381 | ||
| 378 | const activeThread = state.threads.find((t) => t.id === state.activeThreadId); | 382 | const activeThread = state.threads.find((t) => t.id === threadId); |
| 379 | const activeThreadId = activeThread?.id ?? null; | 383 | const activeThreadId = activeThread?.id ?? null; |
| 380 | const activeSessionId = activeThread?.session?.sessionId ?? null; | 384 | const activeSessionId = activeThread?.session?.sessionId ?? null; |
| 381 | const activeThreadRuntimeId = | 385 | const activeThreadRuntimeId = |
apps/web/src/components/Sidebar.tsx+42-46| ... | @@ -2,23 +2,20 @@ import { MonitorIcon, MoonIcon, SunIcon, TerminalIcon } from "lucide-react"; | ... | @@ -2,23 +2,20 @@ import { MonitorIcon, MoonIcon, SunIcon, TerminalIcon } from "lucide-react"; |
| 2 | import { useCallback, useEffect, useMemo, useState } from "react"; | 2 | import { useCallback, useEffect, useMemo, useState } from "react"; |
| 3 | import type { ResolvedKeybindingsConfig } from "@t3tools/contracts"; | 3 | import type { ResolvedKeybindingsConfig } from "@t3tools/contracts"; |
| 4 | import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; | 4 | import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; |
| 5 | import { useNavigate, useParams } from "@tanstack/react-router"; | ||
| 5 | import { isElectron } from "../env"; | 6 | import { isElectron } from "../env"; |
| 6 | import { useTheme } from "../hooks/useTheme"; | 7 | import { useTheme } from "../hooks/useTheme"; |
| 7 | import { DEFAULT_MODEL } from "../model-logic"; | 8 | import { DEFAULT_MODEL } from "../model-logic"; |
| 8 | import { derivePendingApprovals } from "../session-logic"; | 9 | import { derivePendingApprovals } from "../session-logic"; |
| 9 | import { useStore } from "../store"; | 10 | import { useStore } from "../store"; |
| 10 | import { isChatNewLocalShortcut, isChatNewShortcut } from "../keybindings"; | 11 | import { isChatNewLocalShortcut, isChatNewShortcut } from "../keybindings"; |
| 11 | import { | 12 | import { type Project, type Thread } from "../types"; |
| 12 | DEFAULT_THREAD_TERMINAL_HEIGHT, | ||
| 13 | DEFAULT_THREAD_TERMINAL_ID, | ||
| 14 | type Project, | ||
| 15 | type Thread, | ||
| 16 | } from "../types"; | ||
| 17 | import { useNativeApi } from "../hooks/useNativeApi"; | 13 | import { useNativeApi } from "../hooks/useNativeApi"; |
| 18 | import { gitRemoveWorktreeMutationOptions } from "../lib/gitReactQuery"; | 14 | import { gitRemoveWorktreeMutationOptions } from "../lib/gitReactQuery"; |
| 19 | import { serverConfigQueryOptions } from "../lib/serverReactQuery"; | 15 | import { serverConfigQueryOptions } from "../lib/serverReactQuery"; |
| 20 | import { toastManager } from "./ui/toast"; | 16 | import { toastManager } from "./ui/toast"; |
| 21 | import { formatWorktreePathForDisplay, getOrphanedWorktreePathForThread } from "../worktreeCleanup"; | 17 | import { formatWorktreePathForDisplay, getOrphanedWorktreePathForThread } from "../worktreeCleanup"; |
| 18 | import { createThread } from "../threadFactory"; | ||
| 22 | 19 | ||
| 23 | const THEME_CYCLE = { system: "light", light: "dark", dark: "system" } as const; | 20 | const THEME_CYCLE = { system: "light", light: "dark", dark: "system" } as const; |
| 24 | const EMPTY_KEYBINDINGS: ResolvedKeybindingsConfig = []; | 21 | const EMPTY_KEYBINDINGS: ResolvedKeybindingsConfig = []; |
| ... | @@ -118,6 +115,9 @@ function terminalStatusIndicator(thread: Thread): TerminalStatusIndicator | null | ... | @@ -118,6 +115,9 @@ function terminalStatusIndicator(thread: Thread): TerminalStatusIndicator | null |
| 118 | export default function Sidebar() { | 115 | export default function Sidebar() { |
| 119 | const { state, dispatch } = useStore(); | 116 | const { state, dispatch } = useStore(); |
| 120 | const api = useNativeApi(); | 117 | const api = useNativeApi(); |
| 118 | const navigate = useNavigate(); | ||
| 119 | const params = useParams({ strict: false }); | ||
| 120 | const routeThreadId = typeof params.threadId === "string" ? params.threadId : null; | ||
| 121 | const { data: keybindings = EMPTY_KEYBINDINGS } = useQuery({ | 121 | const { data: keybindings = EMPTY_KEYBINDINGS } = useQuery({ |
| 122 | ...serverConfigQueryOptions(api), | 122 | ...serverConfigQueryOptions(api), |
| 123 | select: (config) => config.keybindings, | 123 | select: (config) => config.keybindings, |
| ... | @@ -147,38 +147,21 @@ export default function Sidebar() { | ... | @@ -147,38 +147,21 @@ export default function Sidebar() { |
| 147 | worktreePath?: string | null; | 147 | worktreePath?: string | null; |
| 148 | }, | 148 | }, |
| 149 | ) => { | 149 | ) => { |
| 150 | const thread = createThread(projectId, { | ||
| 151 | model: state.projects.find((project) => project.id === projectId)?.model ?? DEFAULT_MODEL, | ||
| 152 | branch: options?.branch ?? null, | ||
| 153 | worktreePath: options?.worktreePath ?? null, | ||
| 154 | }); | ||
| 150 | dispatch({ | 155 | dispatch({ |
| 151 | type: "ADD_THREAD", | 156 | type: "ADD_THREAD", |
| 152 | thread: { | 157 | thread, |
| 153 | id: crypto.randomUUID(), | 158 | }); |
| 154 | codexThreadId: null, | 159 | void navigate({ |
| 155 | projectId, | 160 | to: "/$threadId", |
| 156 | title: "New thread", | 161 | params: { threadId: thread.id }, |
| 157 | model: state.projects.find((p) => p.id === projectId)?.model ?? DEFAULT_MODEL, | ||
| 158 | terminalOpen: false, | ||
| 159 | terminalHeight: DEFAULT_THREAD_TERMINAL_HEIGHT, | ||
| 160 | terminalIds: [DEFAULT_THREAD_TERMINAL_ID], | ||
| 161 | runningTerminalIds: [], | ||
| 162 | activeTerminalId: DEFAULT_THREAD_TERMINAL_ID, | ||
| 163 | terminalGroups: [ | ||
| 164 | { | ||
| 165 | id: `group-${DEFAULT_THREAD_TERMINAL_ID}`, | ||
| 166 | terminalIds: [DEFAULT_THREAD_TERMINAL_ID], | ||
| 167 | }, | ||
| 168 | ], | ||
| 169 | activeTerminalGroupId: `group-${DEFAULT_THREAD_TERMINAL_ID}`, | ||
| 170 | session: null, | ||
| 171 | messages: [], | ||
| 172 | events: [], | ||
| 173 | turnDiffSummaries: [], | ||
| 174 | error: null, | ||
| 175 | createdAt: new Date().toISOString(), | ||
| 176 | branch: options?.branch ?? null, | ||
| 177 | worktreePath: options?.worktreePath ?? null, | ||
| 178 | }, | ||
| 179 | }); | 162 | }); |
| 180 | }, | 163 | }, |
| 181 | [dispatch, state.projects], | 164 | [dispatch, navigate, state.projects], |
| 182 | ); | 165 | ); |
| 183 | 166 | ||
| 184 | const focusMostRecentThreadForProject = useCallback( | 167 | const focusMostRecentThreadForProject = useCallback( |
| ... | @@ -192,12 +175,12 @@ export default function Sidebar() { | ... | @@ -192,12 +175,12 @@ export default function Sidebar() { |
| 192 | })[0]; | 175 | })[0]; |
| 193 | if (!latestThread) return; | 176 | if (!latestThread) return; |
| 194 | 177 | ||
| 195 | dispatch({ | 178 | void navigate({ |
| 196 | type: "SET_ACTIVE_THREAD", | 179 | to: "/$threadId", |
| 197 | threadId: latestThread.id, | 180 | params: { threadId: latestThread.id }, |
| 198 | }); | 181 | }); |
| 199 | }, | 182 | }, |
| 200 | [dispatch, state.threads], | 183 | [navigate, state.threads], |
| 201 | ); | 184 | ); |
| 202 | 185 | ||
| 203 | const addProjectFromPath = useCallback( | 186 | const addProjectFromPath = useCallback( |
| ... | @@ -325,7 +308,20 @@ export default function Sidebar() { | ... | @@ -325,7 +308,20 @@ export default function Sidebar() { |
| 325 | // Terminal may already be closed | 308 | // Terminal may already be closed |
| 326 | } | 309 | } |
| 327 | 310 | ||
| 311 | const shouldNavigateToFallback = routeThreadId === threadId; | ||
| 312 | const fallbackThreadId = state.threads.find((entry) => entry.id !== threadId)?.id ?? null; | ||
| 328 | dispatch({ type: "DELETE_THREAD", threadId }); | 313 | dispatch({ type: "DELETE_THREAD", threadId }); |
| 314 | if (shouldNavigateToFallback) { | ||
| 315 | if (fallbackThreadId) { | ||
| 316 | void navigate({ | ||
| 317 | to: "/$threadId", | ||
| 318 | params: { threadId: fallbackThreadId }, | ||
| 319 | replace: true, | ||
| 320 | }); | ||
| 321 | } else { | ||
| 322 | void navigate({ to: "/", replace: true }); | ||
| 323 | } | ||
| 324 | } | ||
| 329 | 325 | ||
| 330 | if (!shouldDeleteWorktree || !orphanedWorktreePath || !threadProject) { | 326 | if (!shouldDeleteWorktree || !orphanedWorktreePath || !threadProject) { |
| 331 | return; | 327 | return; |
| ... | @@ -351,7 +347,7 @@ export default function Sidebar() { | ... | @@ -351,7 +347,7 @@ export default function Sidebar() { |
| 351 | }); | 347 | }); |
| 352 | } | 348 | } |
| 353 | }, | 349 | }, |
| 354 | [api, dispatch, removeWorktreeMutation, state.projects, state.threads], | 350 | [api, dispatch, navigate, removeWorktreeMutation, routeThreadId, state.projects, state.threads], |
| 355 | ); | 351 | ); |
| 356 | 352 | ||
| 357 | const handleProjectContextMenu = useCallback( | 353 | const handleProjectContextMenu = useCallback( |
| ... | @@ -509,7 +505,7 @@ export default function Sidebar() { | ... | @@ -509,7 +505,7 @@ export default function Sidebar() { |
| 509 | {project.expanded && ( | 505 | {project.expanded && ( |
| 510 | <div className="ml-2 border-l border-border/80 pl-2"> | 506 | <div className="ml-2 border-l border-border/80 pl-2"> |
| 511 | {threads.map((thread) => { | 507 | {threads.map((thread) => { |
| 512 | const isActive = state.activeThreadId === thread.id; | 508 | const isActive = routeThreadId === thread.id; |
| 513 | const threadStatus = threadStatusPill( | 509 | const threadStatus = threadStatusPill( |
| 514 | thread, | 510 | thread, |
| 515 | pendingApprovalByThreadId.get(thread.id) === true, | 511 | pendingApprovalByThreadId.get(thread.id) === true, |
| ... | @@ -524,12 +520,12 @@ export default function Sidebar() { | ... | @@ -524,12 +520,12 @@ export default function Sidebar() { |
| 524 | ? "bg-accent text-foreground" | 520 | ? "bg-accent text-foreground" |
| 525 | : "text-muted-foreground hover:bg-secondary" | 521 | : "text-muted-foreground hover:bg-secondary" |
| 526 | }`} | 522 | }`} |
| 527 | onClick={() => | 523 | onClick={() => { |
| 528 | dispatch({ | 524 | void navigate({ |
| 529 | type: "SET_ACTIVE_THREAD", | 525 | to: "/$threadId", |
| 530 | threadId: thread.id, | 526 | params: { threadId: thread.id }, |
| 531 | }) | 527 | }); |
| 532 | } | 528 | }} |
| 533 | onContextMenu={(e) => { | 529 | onContextMenu={(e) => { |
| 534 | e.preventDefault(); | 530 | e.preventDefault(); |
| 535 | void handleThreadContextMenu(thread.id, { | 531 | void handleThreadContextMenu(thread.id, { |
apps/web/src/router.tsx created+37| ... | @@ -0,0 +1,37 @@ | ||
| 1 | import { Outlet, createRootRoute, createRoute, createRouter } from "@tanstack/react-router"; | ||
| 2 | |||
| 3 | import { ChatThreadRouteView } from "./routes/_chat.$threadId"; | ||
| 4 | import { ChatIndexRouteView } from "./routes/_chat.index"; | ||
| 5 | import { ChatRouteLayout } from "./routes/_chat"; | ||
| 6 | |||
| 7 | const rootRoute = createRootRoute({ | ||
| 8 | component: Outlet, | ||
| 9 | }); | ||
| 10 | |||
| 11 | const chatRoute = createRoute({ | ||
| 12 | getParentRoute: () => rootRoute, | ||
| 13 | path: "/", | ||
| 14 | component: ChatRouteLayout, | ||
| 15 | }); | ||
| 16 | |||
| 17 | const chatIndexRoute = createRoute({ | ||
| 18 | getParentRoute: () => chatRoute, | ||
| 19 | path: "/", | ||
| 20 | component: ChatIndexRouteView, | ||
| 21 | }); | ||
| 22 | |||
| 23 | const chatThreadRoute = createRoute({ | ||
| 24 | getParentRoute: () => chatRoute, | ||
| 25 | path: "$threadId", | ||
| 26 | component: ChatThreadRouteView, | ||
| 27 | }); | ||
| 28 | |||
| 29 | const routeTree = rootRoute.addChildren([chatRoute.addChildren([chatIndexRoute, chatThreadRoute])]); | ||
| 30 | |||
| 31 | export const router = createRouter({ routeTree }); | ||
| 32 | |||
| 33 | declare module "@tanstack/react-router" { | ||
| 34 | interface Register { | ||
| 35 | router: typeof router; | ||
| 36 | } | ||
| 37 | } | ||
apps/web/src/routes/_chat.$threadId.tsx created+37| ... | @@ -0,0 +1,37 @@ | ||
| 1 | import { useNavigate, useParams } from "@tanstack/react-router"; | ||
| 2 | import { useEffect } from "react"; | ||
| 3 | |||
| 4 | import ChatView from "../components/ChatView"; | ||
| 5 | import { useStore } from "../store"; | ||
| 6 | |||
| 7 | export function ChatThreadRouteView() { | ||
| 8 | const { state, dispatch } = useStore(); | ||
| 9 | const navigate = useNavigate(); | ||
| 10 | const params = useParams({ strict: false }); | ||
| 11 | const threadId = typeof params.threadId === "string" ? params.threadId : null; | ||
| 12 | const threadExists = threadId ? state.threads.some((thread) => thread.id === threadId) : false; | ||
| 13 | |||
| 14 | useEffect(() => { | ||
| 15 | if (!threadId) { | ||
| 16 | void navigate({ to: "/", replace: true }); | ||
| 17 | return; | ||
| 18 | } | ||
| 19 | |||
| 20 | if (!threadExists) { | ||
| 21 | void navigate({ to: "/", replace: true }); | ||
| 22 | return; | ||
| 23 | } | ||
| 24 | |||
| 25 | if (state.activeThreadId === threadId) { | ||
| 26 | return; | ||
| 27 | } | ||
| 28 | |||
| 29 | dispatch({ type: "SET_ACTIVE_THREAD", threadId }); | ||
| 30 | }, [dispatch, navigate, state.activeThreadId, threadExists, threadId]); | ||
| 31 | |||
| 32 | if (!threadId || !threadExists) { | ||
| 33 | return null; | ||
| 34 | } | ||
| 35 | |||
| 36 | return <ChatView threadId={threadId} />; | ||
| 37 | } | ||
apps/web/src/routes/_chat.index.tsx created+84| ... | @@ -0,0 +1,84 @@ | ||
| 1 | import { useNavigate } from "@tanstack/react-router"; | ||
| 2 | import { type FormEvent, useMemo, useState } from "react"; | ||
| 3 | |||
| 4 | import { Button } from "../components/ui/button"; | ||
| 5 | import { Textarea } from "../components/ui/textarea"; | ||
| 6 | import { createThread } from "../threadFactory"; | ||
| 7 | import { useStore } from "../store"; | ||
| 8 | |||
| 9 | export function ChatIndexRouteView() { | ||
| 10 | const { state, dispatch } = useStore(); | ||
| 11 | const navigate = useNavigate(); | ||
| 12 | const [draft, setDraft] = useState(""); | ||
| 13 | const defaultProject = state.projects[0] ?? null; | ||
| 14 | const canCreateThread = defaultProject !== null; | ||
| 15 | |||
| 16 | const placeholder = useMemo(() => { | ||
| 17 | if (!canCreateThread) { | ||
| 18 | return "Add a project in the sidebar to start chatting."; | ||
| 19 | } | ||
| 20 | return "Start with a goal, bug report, or implementation idea..."; | ||
| 21 | }, [canCreateThread]); | ||
| 22 | |||
| 23 | const onCreateThread = (event: FormEvent<HTMLFormElement>) => { | ||
| 24 | event.preventDefault(); | ||
| 25 | if (!defaultProject) return; | ||
| 26 | |||
| 27 | const titleSeed = draft.trim(); | ||
| 28 | const threadTitle = | ||
| 29 | titleSeed.length > 0 | ||
| 30 | ? (titleSeed.length > 50 ? `${titleSeed.slice(0, 50)}...` : titleSeed) | ||
| 31 | : undefined; | ||
| 32 | const thread = createThread( | ||
| 33 | defaultProject.id, | ||
| 34 | threadTitle | ||
| 35 | ? { model: defaultProject.model, title: threadTitle } | ||
| 36 | : { model: defaultProject.model }, | ||
| 37 | ); | ||
| 38 | |||
| 39 | dispatch({ | ||
| 40 | type: "ADD_THREAD", | ||
| 41 | thread, | ||
| 42 | }); | ||
| 43 | |||
| 44 | setDraft(""); | ||
| 45 | |||
| 46 | void navigate({ | ||
| 47 | to: "/$threadId", | ||
| 48 | params: { threadId: thread.id }, | ||
| 49 | }); | ||
| 50 | }; | ||
| 51 | |||
| 52 | return ( | ||
| 53 | <section className="flex min-h-0 min-w-0 flex-1 flex-col bg-background"> | ||
| 54 | <div className="mx-auto flex h-full w-full max-w-3xl flex-col justify-center px-5 py-6"> | ||
| 55 | <div className="mb-5"> | ||
| 56 | <h1 className="text-xl font-semibold text-foreground">New chat</h1> | ||
| 57 | <p className="mt-1 text-sm text-muted-foreground/70"> | ||
| 58 | Create a thread and continue in the full chat workspace. | ||
| 59 | </p> | ||
| 60 | </div> | ||
| 61 | |||
| 62 | <form onSubmit={onCreateThread} className="space-y-3"> | ||
| 63 | <Textarea | ||
| 64 | value={draft} | ||
| 65 | onChange={(event) => setDraft(event.target.value)} | ||
| 66 | placeholder={placeholder} | ||
| 67 | className="min-h-36 resize-y" | ||
| 68 | disabled={!canCreateThread} | ||
| 69 | /> | ||
| 70 | <div className="flex items-center justify-between"> | ||
| 71 | <p className="text-xs text-muted-foreground/60"> | ||
| 72 | {canCreateThread | ||
| 73 | ? "Create a thread first, then send your first prompt." | ||
| 74 | : "Projects are required before creating threads."} | ||
| 75 | </p> | ||
| 76 | <Button type="submit" disabled={!canCreateThread}> | ||
| 77 | Create thread | ||
| 78 | </Button> | ||
| 79 | </div> | ||
| 80 | </form> | ||
| 81 | </div> | ||
| 82 | </section> | ||
| 83 | ); | ||
| 84 | } | ||
apps/web/src/routes/_chat.tsx created+185| ... | @@ -0,0 +1,185 @@ | ||
| 1 | import { Outlet, useNavigate, useParams } from "@tanstack/react-router"; | ||
| 2 | import { useEffect, useRef } from "react"; | ||
| 3 | import { useQueryClient } from "@tanstack/react-query"; | ||
| 4 | |||
| 5 | import DiffPanel from "../components/DiffPanel"; | ||
| 6 | import Sidebar from "../components/Sidebar"; | ||
| 7 | import { isElectron } from "../env"; | ||
| 8 | import { useNativeApi } from "../hooks/useNativeApi"; | ||
| 9 | import { invalidateGitQueries } from "../lib/gitReactQuery"; | ||
| 10 | import { DEFAULT_MODEL } from "../model-logic"; | ||
| 11 | import { createThread } from "../threadFactory"; | ||
| 12 | import { useStore } from "../store"; | ||
| 13 | import { onServerWelcome } from "../wsNativeApi"; | ||
| 14 | |||
| 15 | function EventRouter() { | ||
| 16 | const api = useNativeApi(); | ||
| 17 | const { dispatch } = useStore(); | ||
| 18 | const queryClient = useQueryClient(); | ||
| 19 | const activeAssistantItemRef = useRef<string | null>(null); | ||
| 20 | |||
| 21 | useEffect(() => { | ||
| 22 | if (!api) return; | ||
| 23 | return api.providers.onEvent((event) => { | ||
| 24 | if (event.method === "turn/completed") { | ||
| 25 | void invalidateGitQueries(queryClient); | ||
| 26 | } | ||
| 27 | dispatch({ | ||
| 28 | type: "APPLY_EVENT", | ||
| 29 | event, | ||
| 30 | activeAssistantItemRef, | ||
| 31 | }); | ||
| 32 | }); | ||
| 33 | }, [api, dispatch, queryClient]); | ||
| 34 | |||
| 35 | useEffect(() => { | ||
| 36 | if (!api) return; | ||
| 37 | return api.terminal.onEvent((event) => { | ||
| 38 | dispatch({ | ||
| 39 | type: "APPLY_TERMINAL_EVENT", | ||
| 40 | event, | ||
| 41 | }); | ||
| 42 | }); | ||
| 43 | }, [api, dispatch]); | ||
| 44 | |||
| 45 | return null; | ||
| 46 | } | ||
| 47 | |||
| 48 | function AutoProjectBootstrap() { | ||
| 49 | const { state, dispatch } = useStore(); | ||
| 50 | const bootstrappedRef = useRef(false); | ||
| 51 | |||
| 52 | useEffect(() => { | ||
| 53 | // Browser mode bootstraps from server welcome. | ||
| 54 | // Electron bootstraps from persisted projects via DesktopProjectBootstrap. | ||
| 55 | if (isElectron) return; | ||
| 56 | |||
| 57 | return onServerWelcome((payload) => { | ||
| 58 | if (bootstrappedRef.current) return; | ||
| 59 | |||
| 60 | // Don't create duplicate projects for the same cwd | ||
| 61 | const existing = state.projects.find((project) => project.cwd === payload.cwd); | ||
| 62 | if (existing) { | ||
| 63 | bootstrappedRef.current = true; | ||
| 64 | // Ensure a thread is active | ||
| 65 | const existingThread = state.threads.find((thread) => thread.projectId === existing.id); | ||
| 66 | if (existingThread && !state.activeThreadId) { | ||
| 67 | dispatch({ | ||
| 68 | type: "SET_ACTIVE_THREAD", | ||
| 69 | threadId: existingThread.id, | ||
| 70 | }); | ||
| 71 | } | ||
| 72 | return; | ||
| 73 | } | ||
| 74 | |||
| 75 | bootstrappedRef.current = true; | ||
| 76 | |||
| 77 | // Create project + thread from server cwd | ||
| 78 | const projectId = crypto.randomUUID(); | ||
| 79 | dispatch({ | ||
| 80 | type: "ADD_PROJECT", | ||
| 81 | project: { | ||
| 82 | id: projectId, | ||
| 83 | name: payload.projectName, | ||
| 84 | cwd: payload.cwd, | ||
| 85 | model: DEFAULT_MODEL, | ||
| 86 | expanded: true, | ||
| 87 | }, | ||
| 88 | }); | ||
| 89 | dispatch({ | ||
| 90 | type: "ADD_THREAD", | ||
| 91 | thread: createThread(projectId), | ||
| 92 | }); | ||
| 93 | }); | ||
| 94 | }, [state.projects, state.threads, state.activeThreadId, dispatch]); | ||
| 95 | |||
| 96 | return null; | ||
| 97 | } | ||
| 98 | |||
| 99 | function DesktopProjectBootstrap() { | ||
| 100 | const api = useNativeApi(); | ||
| 101 | const { dispatch } = useStore(); | ||
| 102 | const bootstrappedRef = useRef(false); | ||
| 103 | |||
| 104 | useEffect(() => { | ||
| 105 | if (!isElectron || !api || bootstrappedRef.current) return; | ||
| 106 | |||
| 107 | let disposed = false; | ||
| 108 | let retryDelayMs = 500; | ||
| 109 | let retryTimer: ReturnType<typeof setTimeout> | null = null; | ||
| 110 | |||
| 111 | const attemptBootstrap = async () => { | ||
| 112 | try { | ||
| 113 | const projects = await api.projects.list(); | ||
| 114 | if (disposed) return; | ||
| 115 | dispatch({ | ||
| 116 | type: "SYNC_PROJECTS", | ||
| 117 | projects: projects.map((project) => ({ | ||
| 118 | id: project.id, | ||
| 119 | name: project.name, | ||
| 120 | cwd: project.cwd, | ||
| 121 | model: DEFAULT_MODEL, | ||
| 122 | expanded: true, | ||
| 123 | })), | ||
| 124 | }); | ||
| 125 | bootstrappedRef.current = true; | ||
| 126 | } catch { | ||
| 127 | if (disposed) return; | ||
| 128 | retryTimer = setTimeout(() => { | ||
| 129 | retryTimer = null; | ||
| 130 | void attemptBootstrap(); | ||
| 131 | }, retryDelayMs); | ||
| 132 | retryDelayMs = Math.min(retryDelayMs * 2, 5_000); | ||
| 133 | } | ||
| 134 | }; | ||
| 135 | |||
| 136 | void attemptBootstrap(); | ||
| 137 | |||
| 138 | return () => { | ||
| 139 | disposed = true; | ||
| 140 | if (retryTimer) { | ||
| 141 | clearTimeout(retryTimer); | ||
| 142 | } | ||
| 143 | }; | ||
| 144 | }, [api, dispatch]); | ||
| 145 | |||
| 146 | return null; | ||
| 147 | } | ||
| 148 | |||
| 149 | export function ChatRouteLayout() { | ||
| 150 | const api = useNativeApi(); | ||
| 151 | const { state } = useStore(); | ||
| 152 | const navigate = useNavigate(); | ||
| 153 | const params = useParams({ strict: false }); | ||
| 154 | const routeThreadId = typeof params.threadId === "string" ? params.threadId : null; | ||
| 155 | |||
| 156 | useEffect(() => { | ||
| 157 | if (routeThreadId || !state.activeThreadId) return; | ||
| 158 | void navigate({ | ||
| 159 | to: "/$threadId", | ||
| 160 | params: { threadId: state.activeThreadId }, | ||
| 161 | replace: true, | ||
| 162 | }); | ||
| 163 | }, [navigate, routeThreadId, state.activeThreadId]); | ||
| 164 | |||
| 165 | if (!api) { | ||
| 166 | return ( | ||
| 167 | <div className="flex h-screen flex-col bg-background text-foreground"> | ||
| 168 | <div className="flex flex-1 items-center justify-center"> | ||
| 169 | <p className="text-sm text-muted-foreground">Connecting to T3 Code server...</p> | ||
| 170 | </div> | ||
| 171 | </div> | ||
| 172 | ); | ||
| 173 | } | ||
| 174 | |||
| 175 | return ( | ||
| 176 | <div className="flex h-screen overflow-hidden bg-background text-foreground isolate"> | ||
| 177 | <EventRouter /> | ||
| 178 | <AutoProjectBootstrap /> | ||
| 179 | <DesktopProjectBootstrap /> | ||
| 180 | <Sidebar /> | ||
| 181 | <Outlet /> | ||
| 182 | {state.diffOpen && <DiffPanel />} | ||
| 183 | </div> | ||
| 184 | ); | ||
| 185 | } | ||
apps/web/src/threadFactory.ts created+41| ... | @@ -0,0 +1,41 @@ | ||
| 1 | import { DEFAULT_MODEL } from "./model-logic"; | ||
| 2 | import { DEFAULT_THREAD_TERMINAL_HEIGHT, DEFAULT_THREAD_TERMINAL_ID, type Thread } from "./types"; | ||
| 3 | |||
| 4 | interface CreateThreadOptions { | ||
| 5 | branch?: string | null; | ||
| 6 | createdAt?: string; | ||
| 7 | model?: string; | ||
| 8 | title?: string; | ||
| 9 | worktreePath?: string | null; | ||
| 10 | } | ||
| 11 | |||
| 12 | export function createThread(projectId: string, options: CreateThreadOptions = {}): Thread { | ||
| 13 | const createdAt = options.createdAt ?? new Date().toISOString(); | ||
| 14 | |||
| 15 | return { | ||
| 16 | id: crypto.randomUUID(), | ||
| 17 | codexThreadId: null, | ||
| 18 | projectId, | ||
| 19 | title: options.title ?? "New thread", | ||
| 20 | model: options.model ?? DEFAULT_MODEL, | ||
| 21 | terminalOpen: false, | ||
| 22 | terminalHeight: DEFAULT_THREAD_TERMINAL_HEIGHT, | ||
| 23 | terminalIds: [DEFAULT_THREAD_TERMINAL_ID], | ||
| 24 | runningTerminalIds: [], | ||
| 25 | activeTerminalId: DEFAULT_THREAD_TERMINAL_ID, | ||
| 26 | terminalGroups: [ | ||
| 27 | { | ||
| 28 | id: `group-${DEFAULT_THREAD_TERMINAL_ID}`, | ||
| 29 | terminalIds: [DEFAULT_THREAD_TERMINAL_ID], | ||
| 30 | }, | ||
| 31 | ], | ||
| 32 | activeTerminalGroupId: `group-${DEFAULT_THREAD_TERMINAL_ID}`, | ||
| 33 | session: null, | ||
| 34 | messages: [], | ||
| 35 | events: [], | ||
| 36 | error: null, | ||
| 37 | createdAt, | ||
| 38 | branch: options.branch ?? null, | ||
| 39 | worktreePath: options.worktreePath ?? null, | ||
| 40 | }; | ||
| 41 | } | ||
bun.lock+19| ... | @@ -56,6 +56,7 @@ | ... | @@ -56,6 +56,7 @@ |
| 56 | "@t3tools/contracts": "workspace:*", | 56 | "@t3tools/contracts": "workspace:*", |
| 57 | "@tanstack/react-pacer": "^0.19.4", | 57 | "@tanstack/react-pacer": "^0.19.4", |
| 58 | "@tanstack/react-query": "^5.90.0", | 58 | "@tanstack/react-query": "^5.90.0", |
| 59 | "@tanstack/react-router": "^1.160.2", | ||
| 59 | "@xterm/addon-fit": "^0.11.0", | 60 | "@xterm/addon-fit": "^0.11.0", |
| 60 | "@xterm/xterm": "^6.0.0", | 61 | "@xterm/xterm": "^6.0.0", |
| 61 | "class-variance-authority": "^0.7.1", | 62 | "class-variance-authority": "^0.7.1", |
| ... | @@ -447,6 +448,8 @@ | ... | @@ -447,6 +448,8 @@ |
| 447 | 448 | ||
| 448 | "@tanstack/devtools-event-client": ["@tanstack/devtools-event-client@0.4.0", "", {}, "sha512-RPfGuk2bDZgcu9bAJodvO2lnZeHuz4/71HjZ0bGb/SPg8+lyTA+RLSKQvo7fSmPSi8/vcH3aKQ8EM9ywf1olaw=="], | 449 | "@tanstack/devtools-event-client": ["@tanstack/devtools-event-client@0.4.0", "", {}, "sha512-RPfGuk2bDZgcu9bAJodvO2lnZeHuz4/71HjZ0bGb/SPg8+lyTA+RLSKQvo7fSmPSi8/vcH3aKQ8EM9ywf1olaw=="], |
| 449 | 450 | ||
| 451 | "@tanstack/history": ["@tanstack/history@1.154.14", "", {}, "sha512-xyIfof8eHBuub1CkBnbKNKQXeRZC4dClhmzePHVOEel4G7lk/dW+TQ16da7CFdeNLv6u6Owf5VoBQxoo6DFTSA=="], | ||
| 452 | |||
| 450 | "@tanstack/pacer": ["@tanstack/pacer@0.18.0", "", { "dependencies": { "@tanstack/devtools-event-client": "^0.4.0", "@tanstack/store": "^0.8.0" } }, "sha512-qhCRSFei0hokQr3xYcQXqxsRD/LKlgHCxHXtKHrQoImp4x2Zu6tUOpUGVH4y2qexIrzSu3aibQBNNfC3Eay6Mg=="], | 453 | "@tanstack/pacer": ["@tanstack/pacer@0.18.0", "", { "dependencies": { "@tanstack/devtools-event-client": "^0.4.0", "@tanstack/store": "^0.8.0" } }, "sha512-qhCRSFei0hokQr3xYcQXqxsRD/LKlgHCxHXtKHrQoImp4x2Zu6tUOpUGVH4y2qexIrzSu3aibQBNNfC3Eay6Mg=="], |
| 451 | 454 | ||
| 452 | "@tanstack/query-core": ["@tanstack/query-core@5.90.20", "", {}, "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg=="], | 455 | "@tanstack/query-core": ["@tanstack/query-core@5.90.20", "", {}, "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg=="], |
| ... | @@ -455,8 +458,12 @@ | ... | @@ -455,8 +458,12 @@ |
| 455 | 458 | ||
| 456 | "@tanstack/react-query": ["@tanstack/react-query@5.90.21", "", { "dependencies": { "@tanstack/query-core": "5.90.20" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg=="], | 459 | "@tanstack/react-query": ["@tanstack/react-query@5.90.21", "", { "dependencies": { "@tanstack/query-core": "5.90.20" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg=="], |
| 457 | 460 | ||
| 461 | "@tanstack/react-router": ["@tanstack/react-router@1.160.2", "", { "dependencies": { "@tanstack/history": "1.154.14", "@tanstack/react-store": "^0.8.0", "@tanstack/router-core": "1.160.0", "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-EJWAMS4qCfWKNCzzYGy6ZuWTdBATYEEWieaQdmM7zUesyOQ01j7o6aKXdmCp9rWuSKjPHXagWubEnEo+Puhi3w=="], | ||
| 462 | |||
| 458 | "@tanstack/react-store": ["@tanstack/react-store@0.8.1", "", { "dependencies": { "@tanstack/store": "0.8.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-XItJt+rG8c5Wn/2L/bnxys85rBpm0BfMbhb4zmPVLXAKY9POrp1xd6IbU4PKoOI+jSEGc3vntPRfLGSgXfE2Ig=="], | 463 | "@tanstack/react-store": ["@tanstack/react-store@0.8.1", "", { "dependencies": { "@tanstack/store": "0.8.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-XItJt+rG8c5Wn/2L/bnxys85rBpm0BfMbhb4zmPVLXAKY9POrp1xd6IbU4PKoOI+jSEGc3vntPRfLGSgXfE2Ig=="], |
| 459 | 464 | ||
| 465 | "@tanstack/router-core": ["@tanstack/router-core@1.160.0", "", { "dependencies": { "@tanstack/history": "1.154.14", "@tanstack/store": "^0.8.0", "cookie-es": "^2.0.0", "seroval": "^1.4.2", "seroval-plugins": "^1.4.2", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-vbh6OsE0MG+0c+SKh2uk5yEEZlWsxT96Ub2JaTs7ixOvZp3Wu9PTEIe2BA3cShNZhEsDI0Le4NqgY4XIaHLLvA=="], | ||
| 466 | |||
| 460 | "@tanstack/store": ["@tanstack/store@0.8.1", "", {}, "sha512-PtOisLjUZPz5VyPRSCGjNOlwTvabdTBQ2K80DpVL1chGVr35WRxfeavAPdNq6pm/t7F8GhoR2qtmkkqtCEtHYw=="], | 467 | "@tanstack/store": ["@tanstack/store@0.8.1", "", {}, "sha512-PtOisLjUZPz5VyPRSCGjNOlwTvabdTBQ2K80DpVL1chGVr35WRxfeavAPdNq6pm/t7F8GhoR2qtmkkqtCEtHYw=="], |
| 461 | 468 | ||
| 462 | "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], | 469 | "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], |
| ... | @@ -609,6 +616,8 @@ | ... | @@ -609,6 +616,8 @@ |
| 609 | 616 | ||
| 610 | "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], | 617 | "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], |
| 611 | 618 | ||
| 619 | "cookie-es": ["cookie-es@2.0.0", "", {}, "sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg=="], | ||
| 620 | |||
| 612 | "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], | 621 | "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], |
| 613 | 622 | ||
| 614 | "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], | 623 | "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], |
| ... | @@ -793,6 +802,8 @@ | ... | @@ -793,6 +802,8 @@ |
| 793 | 802 | ||
| 794 | "is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="], | 803 | "is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="], |
| 795 | 804 | ||
| 805 | "isbot": ["isbot@5.1.35", "", {}, "sha512-waFfC72ZNfwLLuJ2iLaoVaqcNo+CAaLR7xCpAn0Y5WfGzkNHv7ZN39Vbi1y+kb+Zs46XHOX3tZNExroFUPX+Kg=="], | ||
| 806 | |||
| 796 | "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], | 807 | "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], |
| 797 | 808 | ||
| 798 | "joi": ["joi@18.0.2", "", { "dependencies": { "@hapi/address": "^5.1.1", "@hapi/formula": "^3.0.2", "@hapi/hoek": "^11.0.7", "@hapi/pinpoint": "^2.0.1", "@hapi/tlds": "^1.1.1", "@hapi/topo": "^6.0.2", "@standard-schema/spec": "^1.0.0" } }, "sha512-RuCOQMIt78LWnktPoeBL0GErkNaJPTBGcYuyaBvUOQSpcpcLfWrHPPihYdOGbV5pam9VTWbeoF7TsGiHugcjGA=="], | 809 | "joi": ["joi@18.0.2", "", { "dependencies": { "@hapi/address": "^5.1.1", "@hapi/formula": "^3.0.2", "@hapi/hoek": "^11.0.7", "@hapi/pinpoint": "^2.0.1", "@hapi/tlds": "^1.1.1", "@hapi/topo": "^6.0.2", "@standard-schema/spec": "^1.0.0" } }, "sha512-RuCOQMIt78LWnktPoeBL0GErkNaJPTBGcYuyaBvUOQSpcpcLfWrHPPihYdOGbV5pam9VTWbeoF7TsGiHugcjGA=="], |
| ... | @@ -1083,6 +1094,10 @@ | ... | @@ -1083,6 +1094,10 @@ |
| 1083 | 1094 | ||
| 1084 | "serialize-error": ["serialize-error@7.0.1", "", { "dependencies": { "type-fest": "^0.13.1" } }, "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw=="], | 1095 | "serialize-error": ["serialize-error@7.0.1", "", { "dependencies": { "type-fest": "^0.13.1" } }, "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw=="], |
| 1085 | 1096 | ||
| 1097 | "seroval": ["seroval@1.5.0", "", {}, "sha512-OE4cvmJ1uSPrKorFIH9/w/Qwuvi/IMcGbv5RKgcJ/zjA/IohDLU6SVaxFN9FwajbP7nsX0dQqMDes1whk3y+yw=="], | ||
| 1098 | |||
| 1099 | "seroval-plugins": ["seroval-plugins@1.5.0", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-EAHqADIQondwRZIdeW2I636zgsODzoBDwb3PT/+7TLDWyw1Dy/Xv7iGUIEXXav7usHDE9HVhOU61irI3EnyyHA=="], | ||
| 1100 | |||
| 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=="], | 1101 | "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 | 1102 | ||
| 1088 | "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], | 1103 | "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], |
| ... | @@ -1129,6 +1144,10 @@ | ... | @@ -1129,6 +1144,10 @@ |
| 1129 | 1144 | ||
| 1130 | "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], | 1145 | "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], |
| 1131 | 1146 | ||
| 1147 | "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], | ||
| 1148 | |||
| 1149 | "tiny-warning": ["tiny-warning@1.0.3", "", {}, "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="], | ||
| 1150 | |||
| 1132 | "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], | 1151 | "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], |
| 1133 | 1152 | ||
| 1134 | "tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], | 1153 | "tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], |