- 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 deps
Unified · Split
| @@ -15,6 +15,7 @@ · 14 unmodified lines | |||
| 15 | "@t3tools/contracts": "workspace:*", | 15 | "@t3tools/contracts": "workspace:*", |
| 16 | "@tanstack/react-pacer": "^0.19.4", | 16 | "@tanstack/react-pacer": "^0.19.4", |
| 17 | "@tanstack/react-query": "^5.90.0", | 17 | "@tanstack/react-query": "^5.90.0", |
| 18 | "@tanstack/react-router": "^1.160.2", | ||
| 18 | "@xterm/addon-fit": "^0.11.0", | 19 | "@xterm/addon-fit": "^0.11.0", |
| 19 | "@xterm/xterm": "^6.0.0", | 20 | "@xterm/xterm": "^6.0.0", |
| 20 | "class-variance-authority": "^0.7.1", | 21 | "class-variance-authority": "^0.7.1", |
| @@ -1,204 +1,9 @@ | |||
| 1 | import { QueryClient, QueryClientProvider, useQueryClient } from "@tanstack/react-query"; | 1 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; |
| 2 | import { useEffect, useRef } from "react"; | 2 | import { RouterProvider } from "@tanstack/react-router"; |
| 3 | 3 | ||
| 4 | import ChatView from "./components/ChatView"; | ||
| 5 | import DiffPanel from "./components/DiffPanel"; | ||
| 6 | import Sidebar from "./components/Sidebar"; | ||
| 7 | import { isElectron } from "./env"; | ||
| 8 | import { DEFAULT_MODEL } from "./model-logic"; | ||
| 9 | import { StoreProvider, useStore } from "./store"; | ||
| 10 | import { DEFAULT_THREAD_TERMINAL_HEIGHT, DEFAULT_THREAD_TERMINAL_ID } from "./types"; | ||
| 11 | import { onServerWelcome } from "./wsNativeApi"; | ||
| 12 | import { useNativeApi } from "./hooks/useNativeApi"; | ||
| 13 | import { AnchoredToastProvider, ToastProvider } from "./components/ui/toast"; | 4 | import { AnchoredToastProvider, ToastProvider } from "./components/ui/toast"; |
| 14 | import { invalidateGitQueries } from "./lib/gitReactQuery"; | 5 | import { router } from "./router"; |
| 15 | 6 | import { StoreProvider } from "./store"; | |
| 16 | function EventRouter() { | ||
| 17 | const api = useNativeApi(); | ||
| 18 | const { dispatch } = useStore(); | ||
| 19 | const queryClient = useQueryClient(); | ||
| 20 | const activeAssistantItemRef = useRef<string | null>(null); | ||
| 21 | |||
| 22 | useEffect(() => { | ||
| 23 | if (!api) return; | ||
| 24 | return api.providers.onEvent((event) => { | ||
| 25 | if (event.method === "turn/completed") { | ||
| 26 | void invalidateGitQueries(queryClient); | ||
| 27 | } | ||
| 28 | dispatch({ | ||
| 29 | type: "APPLY_EVENT", | ||
| 30 | event, | ||
| 31 | activeAssistantItemRef, | ||
| 32 | }); | ||
| 33 | }); | ||
| 34 | }, [api, dispatch, queryClient]); | ||
| 35 | |||
| 36 | useEffect(() => { | ||
| 37 | if (!api) return; | ||
| 38 | return api.terminal.onEvent((event) => { | ||
| 39 | dispatch({ | ||
| 40 | type: "APPLY_TERMINAL_EVENT", | ||
| 41 | event, | ||
| 42 | }); | ||
| 43 | }); | ||
| 44 | }, [api, dispatch]); | ||
| 45 | |||
| 46 | return null; | ||
| 47 | } | ||
| 48 | |||
| 49 | function AutoProjectBootstrap() { | ||
| 50 | const { state, dispatch } = useStore(); | ||
| 51 | const bootstrappedRef = useRef(false); | ||
| 52 | |||
| 53 | useEffect(() => { | ||
| 54 | // Browser mode bootstraps from server welcome. | ||
| 55 | // Electron bootstraps from persisted projects via DesktopProjectBootstrap. | ||
| 56 | if (isElectron) return; | ||
| 57 | |||
| 58 | return onServerWelcome((payload) => { | ||
| 59 | if (bootstrappedRef.current) return; | ||
| 60 | |||
| 61 | // Don't create duplicate projects for the same cwd | ||
| 62 | const existing = state.projects.find((p) => p.cwd === payload.cwd); | ||
| 63 | if (existing) { | ||
| 64 | bootstrappedRef.current = true; | ||
| 65 | // Ensure a thread is active | ||
| 66 | const existingThread = state.threads.find((t) => t.projectId === existing.id); | ||
| 67 | if (existingThread && !state.activeThreadId) { | ||
| 68 | dispatch({ | ||
| 69 | type: "SET_ACTIVE_THREAD", | ||
| 70 | threadId: existingThread.id, | ||
| 71 | }); | ||
| 72 | } | ||
| 73 | return; | ||
| 74 | } | ||
| 75 | |||
| 76 | bootstrappedRef.current = true; | ||
| 77 | |||
| 78 | // Create project + thread from server cwd | ||
| 79 | const projectId = crypto.randomUUID(); | ||
| 80 | dispatch({ | ||
| 81 | type: "ADD_PROJECT", | ||
| 82 | project: { | ||
| 83 | id: projectId, | ||
| 84 | name: payload.projectName, | ||
| 85 | cwd: payload.cwd, | ||
| 86 | model: DEFAULT_MODEL, | ||
| 87 | expanded: true, | ||
| 88 | scripts: [], | ||
| 89 | }, | ||
| 90 | }); | ||
| 91 | dispatch({ | ||
| 92 | type: "ADD_THREAD", | ||
| 93 | thread: { | ||
| 94 | id: crypto.randomUUID(), | ||
| 95 | codexThreadId: null, | ||
| 96 | projectId, | ||
| 97 | title: "New thread", | ||
| 98 | model: DEFAULT_MODEL, | ||
| 99 | terminalOpen: false, | ||
| 100 | terminalHeight: DEFAULT_THREAD_TERMINAL_HEIGHT, | ||
| 101 | terminalIds: [DEFAULT_THREAD_TERMINAL_ID], | ||
| 102 | runningTerminalIds: [], | ||
| 103 | activeTerminalId: DEFAULT_THREAD_TERMINAL_ID, | ||
| 104 | terminalGroups: [ | ||
| 105 | { | ||
| 106 | id: `group-${DEFAULT_THREAD_TERMINAL_ID}`, | ||
| 107 | terminalIds: [DEFAULT_THREAD_TERMINAL_ID], | ||
| 108 | }, | ||
| 109 | ], | ||
| 110 | activeTerminalGroupId: `group-${DEFAULT_THREAD_TERMINAL_ID}`, | ||
| 111 | session: null, | ||
| 112 | messages: [], | ||
| 113 | events: [], | ||
| 114 | error: null, | ||
| 115 | createdAt: new Date().toISOString(), | ||
| 116 | branch: null, | ||
| 117 | worktreePath: null, | ||
| 118 | }, | ||
| 119 | }); | ||
| 120 | }); | ||
| 121 | }, [state.projects, state.threads, state.activeThreadId, dispatch]); | ||
| 122 | |||
| 123 | return null; | ||
| 124 | } | ||
| 125 | |||
| 126 | function DesktopProjectBootstrap() { | ||
| 127 | const api = useNativeApi(); | ||
| 128 | const { dispatch } = useStore(); | ||
| 129 | const bootstrappedRef = useRef(false); | ||
| 130 | |||
| 131 | useEffect(() => { | ||
| 132 | if (!isElectron || !api || bootstrappedRef.current) return; | ||
| 133 | |||
| 134 | let disposed = false; | ||
| 135 | let retryDelayMs = 500; | ||
| 136 | let retryTimer: ReturnType<typeof setTimeout> | null = null; | ||
| 137 | |||
| 138 | const attemptBootstrap = async () => { | ||
| 139 | try { | ||
| 140 | const projects = await api.projects.list(); | ||
| 141 | if (disposed) return; | ||
| 142 | dispatch({ | ||
| 143 | type: "SYNC_PROJECTS", | ||
| 144 | projects: projects.map((project) => ({ | ||
| 145 | id: project.id, | ||
| 146 | name: project.name, | ||
| 147 | cwd: project.cwd, | ||
| 148 | model: DEFAULT_MODEL, | ||
| 149 | expanded: true, | ||
| 150 | scripts: project.scripts, | ||
| 151 | })), | ||
| 152 | }); | ||
| 153 | bootstrappedRef.current = true; | ||
| 154 | } catch { | ||
| 155 | if (disposed) return; | ||
| 156 | retryTimer = setTimeout(() => { | ||
| 157 | retryTimer = null; | ||
| 158 | void attemptBootstrap(); | ||
| 159 | }, retryDelayMs); | ||
| 160 | retryDelayMs = Math.min(retryDelayMs * 2, 5_000); | ||
| 161 | } | ||
| 162 | }; | ||
| 163 | |||
| 164 | void attemptBootstrap(); | ||
| 165 | |||
| 166 | return () => { | ||
| 167 | disposed = true; | ||
| 168 | if (retryTimer) { | ||
| 169 | clearTimeout(retryTimer); | ||
| 170 | } | ||
| 171 | }; | ||
| 172 | }, [api, dispatch]); | ||
| 173 | |||
| 174 | return null; | ||
| 175 | } | ||
| 176 | |||
| 177 | function Layout() { | ||
| 178 | const api = useNativeApi(); | ||
| 179 | const { state } = useStore(); | ||
| 180 | |||
| 181 | if (!api) { | ||
| 182 | return ( | ||
| 183 | <div className="flex h-screen flex-col bg-background text-foreground"> | ||
| 184 | <div className="flex flex-1 items-center justify-center"> | ||
| 185 | <p className="text-sm text-muted-foreground">Connecting to T3 Code server...</p> | ||
| 186 | </div> | ||
| 187 | </div> | ||
| 188 | ); | ||
| 189 | } | ||
| 190 | |||
| 191 | return ( | ||
| 192 | <div className="flex h-screen overflow-hidden bg-background text-foreground isolate"> | ||
| 193 | <EventRouter /> | ||
| 194 | <AutoProjectBootstrap /> | ||
| 195 | <DesktopProjectBootstrap /> | ||
| 196 | <Sidebar /> | ||
| 197 | <ChatView /> | ||
| 198 | {state.diffOpen && <DiffPanel />} | ||
| 199 | </div> | ||
| 200 | ); | ||
| 201 | } | ||
| 202 | 7 | ||
| 203 | const queryClient = new QueryClient(); | 8 | const queryClient = new QueryClient(); |
| 204 | 9 | ||
| @@ -208,7 +13,7 @@ export default function App() { · 3 unmodified lines | |||
| 208 | <StoreProvider> | 13 | <StoreProvider> |
| 209 | <ToastProvider> | 14 | <ToastProvider> |
| 210 | <AnchoredToastProvider> | 15 | <AnchoredToastProvider> |
| 211 | <Layout /> | 16 | <RouterProvider router={router} /> |
| 212 | </AnchoredToastProvider> | 17 | </AnchoredToastProvider> |
| 213 | </ToastProvider> | 18 | </ToastProvider> |
| 214 | </StoreProvider> | 19 | </StoreProvider> |
| @@ -330,7 +330,11 @@ const ComposerCommandMenu = memo(function ComposerCommandMenu(props: { · 329 unmodified lines | |||
| 330 | ); | 330 | ); |
| 331 | }); | 331 | }); |
| 332 | 332 | ||
| 333 | export default function ChatView() { | 333 | interface ChatViewProps { |
| 334 | threadId: string; | ||
| 335 | } | ||
| 336 | |||
| 337 | export default function ChatView({ threadId }: ChatViewProps) { | ||
| 334 | const { state, dispatch } = useStore(); | 338 | const { state, dispatch } = useStore(); |
| 335 | const api = useNativeApi(); | 339 | const api = useNativeApi(); |
| 336 | const { resolvedTheme } = useTheme(); | 340 | const { resolvedTheme } = useTheme(); |
| @@ -366,7 +370,7 @@ export default function ChatView() { · 29 unmodified lines | |||
| 366 | const dragDepthRef = useRef(0); | 370 | const dragDepthRef = useRef(0); |
| 367 | const terminalOpenByThreadRef = useRef<Record<string, boolean>>({}); | 371 | const terminalOpenByThreadRef = useRef<Record<string, boolean>>({}); |
| 368 | 372 | ||
| 369 | const activeThread = state.threads.find((t) => t.id === state.activeThreadId); | 373 | const activeThread = state.threads.find((t) => t.id === threadId); |
| 370 | const activeThreadId = activeThread?.id ?? null; | 374 | const activeThreadId = activeThread?.id ?? null; |
| 371 | const activeSessionId = activeThread?.session?.sessionId; | 375 | const activeSessionId = activeThread?.session?.sessionId; |
| 372 | const activeProject = state.projects.find((p) => p.id === activeThread?.projectId); | 376 | const activeProject = state.projects.find((p) => p.id === activeThread?.projectId); |
| @@ -2,23 +2,20 @@ import { MonitorIcon, MoonIcon, SunIcon, TerminalIcon } from "lucide-react"; · 1 unmodified line | |||
| 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 · 93 unmodified lines | |||
| 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,37 +147,21 @@ export default function Sidebar() { · 23 unmodified lines | |||
| 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 | error: null, | ||
| 174 | createdAt: new Date().toISOString(), | ||
| 175 | branch: options?.branch ?? null, | ||
| 176 | worktreePath: options?.worktreePath ?? null, | ||
| 177 | }, | ||
| 178 | }); | 162 | }); |
| 179 | }, | 163 | }, |
| 180 | [dispatch, state.projects], | 164 | [dispatch, navigate, state.projects], |
| 181 | ); | 165 | ); |
| 182 | 166 | ||
| 183 | const focusMostRecentThreadForProject = useCallback( | 167 | const focusMostRecentThreadForProject = useCallback( |
| @@ -191,12 +175,12 @@ export default function Sidebar() { · 7 unmodified lines | |||
| 191 | })[0]; | 175 | })[0]; |
| 192 | if (!latestThread) return; | 176 | if (!latestThread) return; |
| 193 | 177 | ||
| 194 | dispatch({ | 178 | void navigate({ |
| 195 | type: "SET_ACTIVE_THREAD", | 179 | to: "/$threadId", |
| 196 | threadId: latestThread.id, | 180 | params: { threadId: latestThread.id }, |
| 197 | }); | 181 | }); |
| 198 | }, | 182 | }, |
| 199 | [dispatch, state.threads], | 183 | [navigate, state.threads], |
| 200 | ); | 184 | ); |
| 201 | 185 | ||
| 202 | const addProjectFromPath = useCallback( | 186 | const addProjectFromPath = useCallback( |
| @@ -324,7 +308,20 @@ export default function Sidebar() { · 121 unmodified lines | |||
| 324 | // Terminal may already be closed | 308 | // Terminal may already be closed |
| 325 | } | 309 | } |
| 326 | 310 | ||
| 311 | const shouldNavigateToFallback = routeThreadId === threadId; | ||
| 312 | const fallbackThreadId = state.threads.find((entry) => entry.id !== threadId)?.id ?? null; | ||
| 327 | 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 | } | ||
| 328 | 325 | ||
| 329 | if (!shouldDeleteWorktree || !orphanedWorktreePath || !threadProject) { | 326 | if (!shouldDeleteWorktree || !orphanedWorktreePath || !threadProject) { |
| 330 | return; | 327 | return; |
| @@ -339,7 +336,7 @@ export default function Sidebar() { · 8 unmodified lines | |||
| 339 | // Worktree deletion is best-effort and should not block thread deletion. | 336 | // Worktree deletion is best-effort and should not block thread deletion. |
| 340 | } | 337 | } |
| 341 | }, | 338 | }, |
| 342 | [api, dispatch, removeWorktreeMutation, state.projects, state.threads], | 339 | [api, dispatch, navigate, removeWorktreeMutation, routeThreadId, state.projects, state.threads], |
| 343 | ); | 340 | ); |
| 344 | 341 | ||
| 345 | const handleProjectContextMenu = useCallback( | 342 | const handleProjectContextMenu = useCallback( |
| @@ -497,7 +494,7 @@ export default function Sidebar() { · 151 unmodified lines | |||
| 497 | {project.expanded && ( | 494 | {project.expanded && ( |
| 498 | <div className="ml-2 border-l border-border/80 pl-2"> | 495 | <div className="ml-2 border-l border-border/80 pl-2"> |
| 499 | {threads.map((thread) => { | 496 | {threads.map((thread) => { |
| 500 | const isActive = state.activeThreadId === thread.id; | 497 | const isActive = routeThreadId === thread.id; |
| 501 | const threadStatus = threadStatusPill( | 498 | const threadStatus = threadStatusPill( |
| 502 | thread, | 499 | thread, |
| 503 | pendingApprovalByThreadId.get(thread.id) === true, | 500 | pendingApprovalByThreadId.get(thread.id) === true, |
| @@ -512,12 +509,12 @@ export default function Sidebar() { · 8 unmodified lines | |||
| 512 | ? "bg-accent text-foreground" | 509 | ? "bg-accent text-foreground" |
| 513 | : "text-muted-foreground hover:bg-secondary" | 510 | : "text-muted-foreground hover:bg-secondary" |
| 514 | }`} | 511 | }`} |
| 515 | onClick={() => | 512 | onClick={() => { |
| 516 | dispatch({ | 513 | void navigate({ |
| 517 | type: "SET_ACTIVE_THREAD", | 514 | to: "/$threadId", |
| 518 | threadId: thread.id, | 515 | params: { threadId: thread.id }, |
| 519 | }) | 516 | }); |
| 520 | } | 517 | }} |
| 521 | onContextMenu={(e) => { | 518 | onContextMenu={(e) => { |
| 522 | e.preventDefault(); | 519 | e.preventDefault(); |
| 523 | void handleThreadContextMenu(thread.id, { | 520 | void handleThreadContextMenu(thread.id, { |
| @@ -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 | } | ||
| @@ -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 | } | ||
| @@ -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 | } | ||
| @@ -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 | } | ||
| @@ -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 | } | ||
| @@ -55,6 +55,7 @@ · 54 unmodified lines | |||
| 55 | "@t3tools/contracts": "workspace:*", | 55 | "@t3tools/contracts": "workspace:*", |
| 56 | "@tanstack/react-pacer": "^0.19.4", | 56 | "@tanstack/react-pacer": "^0.19.4", |
| 57 | "@tanstack/react-query": "^5.90.0", | 57 | "@tanstack/react-query": "^5.90.0", |
| 58 | "@tanstack/react-router": "^1.160.2", | ||
| 58 | "@xterm/addon-fit": "^0.11.0", | 59 | "@xterm/addon-fit": "^0.11.0", |
| 59 | "@xterm/xterm": "^6.0.0", | 60 | "@xterm/xterm": "^6.0.0", |
| 60 | "class-variance-authority": "^0.7.1", | 61 | "class-variance-authority": "^0.7.1", |
| @@ -428,6 +429,8 @@ · 367 unmodified lines | |||
| 428 | 429 | ||
| 429 | "@tanstack/devtools-event-client": ["@tanstack/devtools-event-client@0.4.0", "", {}, "sha512-RPfGuk2bDZgcu9bAJodvO2lnZeHuz4/71HjZ0bGb/SPg8+lyTA+RLSKQvo7fSmPSi8/vcH3aKQ8EM9ywf1olaw=="], | 430 | "@tanstack/devtools-event-client": ["@tanstack/devtools-event-client@0.4.0", "", {}, "sha512-RPfGuk2bDZgcu9bAJodvO2lnZeHuz4/71HjZ0bGb/SPg8+lyTA+RLSKQvo7fSmPSi8/vcH3aKQ8EM9ywf1olaw=="], |
| 430 | 431 | ||
| 432 | "@tanstack/history": ["@tanstack/history@1.154.14", "", {}, "sha512-xyIfof8eHBuub1CkBnbKNKQXeRZC4dClhmzePHVOEel4G7lk/dW+TQ16da7CFdeNLv6u6Owf5VoBQxoo6DFTSA=="], | ||
| 433 | |||
| 431 | "@tanstack/pacer": ["@tanstack/pacer@0.18.0", "", { "dependencies": { "@tanstack/devtools-event-client": "^0.4.0", "@tanstack/store": "^0.8.0" } }, "sha512-qhCRSFei0hokQr3xYcQXqxsRD/LKlgHCxHXtKHrQoImp4x2Zu6tUOpUGVH4y2qexIrzSu3aibQBNNfC3Eay6Mg=="], | 434 | "@tanstack/pacer": ["@tanstack/pacer@0.18.0", "", { "dependencies": { "@tanstack/devtools-event-client": "^0.4.0", "@tanstack/store": "^0.8.0" } }, "sha512-qhCRSFei0hokQr3xYcQXqxsRD/LKlgHCxHXtKHrQoImp4x2Zu6tUOpUGVH4y2qexIrzSu3aibQBNNfC3Eay6Mg=="], |
| 432 | 435 | ||
| 433 | "@tanstack/query-core": ["@tanstack/query-core@5.90.20", "", {}, "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg=="], | 436 | "@tanstack/query-core": ["@tanstack/query-core@5.90.20", "", {}, "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg=="], |
| @@ -436,8 +439,12 @@ · 2 unmodified lines | |||
| 436 | 439 | ||
| 437 | "@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=="], | 440 | "@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=="], |
| 438 | 441 | ||
| 442 | "@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=="], | ||
| 443 | |||
| 439 | "@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=="], | 444 | "@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=="], |
| 440 | 445 | ||
| 446 | "@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=="], | ||
| 447 | |||
| 441 | "@tanstack/store": ["@tanstack/store@0.8.1", "", {}, "sha512-PtOisLjUZPz5VyPRSCGjNOlwTvabdTBQ2K80DpVL1chGVr35WRxfeavAPdNq6pm/t7F8GhoR2qtmkkqtCEtHYw=="], | 448 | "@tanstack/store": ["@tanstack/store@0.8.1", "", {}, "sha512-PtOisLjUZPz5VyPRSCGjNOlwTvabdTBQ2K80DpVL1chGVr35WRxfeavAPdNq6pm/t7F8GhoR2qtmkkqtCEtHYw=="], |
| 442 | 449 | ||
| 443 | "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], | 450 | "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], |
| @@ -590,6 +597,8 @@ · 146 unmodified lines | |||
| 590 | 597 | ||
| 591 | "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], | 598 | "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], |
| 592 | 599 | ||
| 600 | "cookie-es": ["cookie-es@2.0.0", "", {}, "sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg=="], | ||
| 601 | |||
| 593 | "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], | 602 | "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], |
| 594 | 603 | ||
| 595 | "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], | 604 | "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], |
| @@ -768,6 +777,8 @@ · 172 unmodified lines | |||
| 768 | 777 | ||
| 769 | "is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="], | 778 | "is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="], |
| 770 | 779 | ||
| 780 | "isbot": ["isbot@5.1.35", "", {}, "sha512-waFfC72ZNfwLLuJ2iLaoVaqcNo+CAaLR7xCpAn0Y5WfGzkNHv7ZN39Vbi1y+kb+Zs46XHOX3tZNExroFUPX+Kg=="], | ||
| 781 | |||
| 771 | "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], | 782 | "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], |
| 772 | 783 | ||
| 773 | "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=="], | 784 | "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=="], |
| @@ -1046,6 +1057,10 @@ · 272 unmodified lines | |||
| 1046 | 1057 | ||
| 1047 | "serialize-error": ["serialize-error@7.0.1", "", { "dependencies": { "type-fest": "^0.13.1" } }, "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw=="], | 1058 | "serialize-error": ["serialize-error@7.0.1", "", { "dependencies": { "type-fest": "^0.13.1" } }, "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw=="], |
| 1048 | 1059 | ||
| 1060 | "seroval": ["seroval@1.5.0", "", {}, "sha512-OE4cvmJ1uSPrKorFIH9/w/Qwuvi/IMcGbv5RKgcJ/zjA/IohDLU6SVaxFN9FwajbP7nsX0dQqMDes1whk3y+yw=="], | ||
| 1061 | |||
| 1062 | "seroval-plugins": ["seroval-plugins@1.5.0", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-EAHqADIQondwRZIdeW2I636zgsODzoBDwb3PT/+7TLDWyw1Dy/Xv7iGUIEXXav7usHDE9HVhOU61irI3EnyyHA=="], | ||
| 1063 | |||
| 1049 | "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], | 1064 | "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], |
| 1050 | 1065 | ||
| 1051 | "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], | 1066 | "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], |
| @@ -1090,6 +1105,10 @@ · 38 unmodified lines | |||
| 1090 | 1105 | ||
| 1091 | "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], | 1106 | "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], |
| 1092 | 1107 | ||
| 1108 | "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], | ||
| 1109 | |||
| 1110 | "tiny-warning": ["tiny-warning@1.0.3", "", {}, "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="], | ||
| 1111 | |||
| 1093 | "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], | 1112 | "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], |
| 1094 | 1113 | ||
| 1095 | "tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], | 1114 | "tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], |