- 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 | 15 | "@t3tools/contracts": "workspace:*", |
| 16 | 16 | "@tanstack/react-pacer": "^0.19.4", |
| 17 | 17 | "@tanstack/react-query": "^5.90.0", |
| 18 | + "@tanstack/react-router": "^1.160.2", | |
| 18 | 19 | "@xterm/addon-fit": "^0.11.0", |
| 19 | 20 | "@xterm/xterm": "^6.0.0", |
| 20 | 21 | "class-variance-authority": "^0.7.1", |
| @@ -1,204 +1,9 @@ | ||
| 1 | −import { QueryClient, QueryClientProvider, useQueryClient } from "@tanstack/react-query"; | |
| 2 | −import { useEffect, useRef } from "react"; | |
| 1 | +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; | |
| 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 | 4 | import { AnchoredToastProvider, ToastProvider } from "./components/ui/toast"; |
| 14 | −import { invalidateGitQueries } from "./lib/gitReactQuery"; | |
| 15 | − | |
| 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 | −} | |
| 5 | +import { router } from "./router"; | |
| 6 | +import { StoreProvider } from "./store"; | |
| 202 | 7 | |
| 203 | 8 | const queryClient = new QueryClient(); |
| 204 | 9 | |
| @@ -208,7 +13,7 @@ export default function App() { · 3 unmodified lines | ||
| 208 | 13 | <StoreProvider> |
| 209 | 14 | <ToastProvider> |
| 210 | 15 | <AnchoredToastProvider> |
| 211 | − <Layout /> | |
| 16 | + <RouterProvider router={router} /> | |
| 212 | 17 | </AnchoredToastProvider> |
| 213 | 18 | </ToastProvider> |
| 214 | 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 | 338 | const { state, dispatch } = useStore(); |
| 335 | 339 | const api = useNativeApi(); |
| 336 | 340 | const { resolvedTheme } = useTheme(); |
| @@ -366,7 +370,7 @@ export default function ChatView() { · 29 unmodified lines | ||
| 366 | 370 | const dragDepthRef = useRef(0); |
| 367 | 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 | 374 | const activeThreadId = activeThread?.id ?? null; |
| 371 | 375 | const activeSessionId = activeThread?.session?.sessionId; |
| 372 | 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 | 2 | import { useCallback, useEffect, useMemo, useState } from "react"; |
| 3 | 3 | import type { ResolvedKeybindingsConfig } from "@t3tools/contracts"; |
| 4 | 4 | import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; |
| 5 | +import { useNavigate, useParams } from "@tanstack/react-router"; | |
| 5 | 6 | import { isElectron } from "../env"; |
| 6 | 7 | import { useTheme } from "../hooks/useTheme"; |
| 7 | 8 | import { DEFAULT_MODEL } from "../model-logic"; |
| 8 | 9 | import { derivePendingApprovals } from "../session-logic"; |
| 9 | 10 | import { useStore } from "../store"; |
| 10 | 11 | import { isChatNewLocalShortcut, isChatNewShortcut } from "../keybindings"; |
| 11 | −import { | |
| 12 | − DEFAULT_THREAD_TERMINAL_HEIGHT, | |
| 13 | − DEFAULT_THREAD_TERMINAL_ID, | |
| 14 | − type Project, | |
| 15 | − type Thread, | |
| 16 | −} from "../types"; | |
| 12 | +import { type Project, type Thread } from "../types"; | |
| 17 | 13 | import { useNativeApi } from "../hooks/useNativeApi"; |
| 18 | 14 | import { gitRemoveWorktreeMutationOptions } from "../lib/gitReactQuery"; |
| 19 | 15 | import { serverConfigQueryOptions } from "../lib/serverReactQuery"; |
| 20 | 16 | import { toastManager } from "./ui/toast"; |
| 21 | 17 | import { formatWorktreePathForDisplay, getOrphanedWorktreePathForThread } from "../worktreeCleanup"; |
| 18 | +import { createThread } from "../threadFactory"; | |
| 22 | 19 | |
| 23 | 20 | const THEME_CYCLE = { system: "light", light: "dark", dark: "system" } as const; |
| 24 | 21 | const EMPTY_KEYBINDINGS: ResolvedKeybindingsConfig = []; |
| @@ -118,6 +115,9 @@ function terminalStatusIndicator(thread: Thread): TerminalStatusIndicator | null · 93 unmodified lines | ||
| 118 | 115 | export default function Sidebar() { |
| 119 | 116 | const { state, dispatch } = useStore(); |
| 120 | 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 | 121 | const { data: keybindings = EMPTY_KEYBINDINGS } = useQuery({ |
| 122 | 122 | ...serverConfigQueryOptions(api), |
| 123 | 123 | select: (config) => config.keybindings, |
| @@ -147,37 +147,21 @@ export default function Sidebar() { · 23 unmodified lines | ||
| 147 | 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 | 155 | dispatch({ |
| 151 | 156 | type: "ADD_THREAD", |
| 152 | − thread: { | |
| 153 | − id: crypto.randomUUID(), | |
| 154 | − codexThreadId: null, | |
| 155 | − projectId, | |
| 156 | − title: "New thread", | |
| 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 | − }, | |
| 157 | + thread, | |
| 158 | + }); | |
| 159 | + void navigate({ | |
| 160 | + to: "/$threadId", | |
| 161 | + params: { threadId: thread.id }, | |
| 178 | 162 | }); |
| 179 | 163 | }, |
| 180 | − [dispatch, state.projects], | |
| 164 | + [dispatch, navigate, state.projects], | |
| 181 | 165 | ); |
| 182 | 166 | |
| 183 | 167 | const focusMostRecentThreadForProject = useCallback( |
| @@ -191,12 +175,12 @@ export default function Sidebar() { · 7 unmodified lines | ||
| 191 | 175 | })[0]; |
| 192 | 176 | if (!latestThread) return; |
| 193 | 177 | |
| 194 | − dispatch({ | |
| 195 | − type: "SET_ACTIVE_THREAD", | |
| 196 | − threadId: latestThread.id, | |
| 178 | + void navigate({ | |
| 179 | + to: "/$threadId", | |
| 180 | + params: { threadId: latestThread.id }, | |
| 197 | 181 | }); |
| 198 | 182 | }, |
| 199 | − [dispatch, state.threads], | |
| 183 | + [navigate, state.threads], | |
| 200 | 184 | ); |
| 201 | 185 | |
| 202 | 186 | const addProjectFromPath = useCallback( |
| @@ -324,7 +308,20 @@ export default function Sidebar() { · 121 unmodified lines | ||
| 324 | 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 | 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 | 326 | if (!shouldDeleteWorktree || !orphanedWorktreePath || !threadProject) { |
| 330 | 327 | return; |
| @@ -339,7 +336,7 @@ export default function Sidebar() { · 8 unmodified lines | ||
| 339 | 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 | 342 | const handleProjectContextMenu = useCallback( |
| @@ -497,7 +494,7 @@ export default function Sidebar() { · 151 unmodified lines | ||
| 497 | 494 | {project.expanded && ( |
| 498 | 495 | <div className="ml-2 border-l border-border/80 pl-2"> |
| 499 | 496 | {threads.map((thread) => { |
| 500 | − const isActive = state.activeThreadId === thread.id; | |
| 497 | + const isActive = routeThreadId === thread.id; | |
| 501 | 498 | const threadStatus = threadStatusPill( |
| 502 | 499 | thread, |
| 503 | 500 | pendingApprovalByThreadId.get(thread.id) === true, |
| @@ -512,12 +509,12 @@ export default function Sidebar() { · 8 unmodified lines | ||
| 512 | 509 | ? "bg-accent text-foreground" |
| 513 | 510 | : "text-muted-foreground hover:bg-secondary" |
| 514 | 511 | }`} |
| 515 | − onClick={() => | |
| 516 | − dispatch({ | |
| 517 | − type: "SET_ACTIVE_THREAD", | |
| 518 | − threadId: thread.id, | |
| 519 | − }) | |
| 520 | − } | |
| 512 | + onClick={() => { | |
| 513 | + void navigate({ | |
| 514 | + to: "/$threadId", | |
| 515 | + params: { threadId: thread.id }, | |
| 516 | + }); | |
| 517 | + }} | |
| 521 | 518 | onContextMenu={(e) => { |
| 522 | 519 | e.preventDefault(); |
| 523 | 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 | 55 | "@t3tools/contracts": "workspace:*", |
| 56 | 56 | "@tanstack/react-pacer": "^0.19.4", |
| 57 | 57 | "@tanstack/react-query": "^5.90.0", |
| 58 | + "@tanstack/react-router": "^1.160.2", | |
| 58 | 59 | "@xterm/addon-fit": "^0.11.0", |
| 59 | 60 | "@xterm/xterm": "^6.0.0", |
| 60 | 61 | "class-variance-authority": "^0.7.1", |
| @@ -428,6 +429,8 @@ · 367 unmodified lines | ||
| 428 | 429 | |
| 429 | 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 | 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 | 436 | "@tanstack/query-core": ["@tanstack/query-core@5.90.20", "", {}, "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg=="], |
| @@ -436,8 +439,12 @@ · 2 unmodified lines | ||
| 436 | 439 | |
| 437 | 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 | 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 | 448 | "@tanstack/store": ["@tanstack/store@0.8.1", "", {}, "sha512-PtOisLjUZPz5VyPRSCGjNOlwTvabdTBQ2K80DpVL1chGVr35WRxfeavAPdNq6pm/t7F8GhoR2qtmkkqtCEtHYw=="], |
| 442 | 449 | |
| 443 | 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 | 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 | 602 | "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], |
| 594 | 603 | |
| 595 | 604 | "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], |
| @@ -768,6 +777,8 @@ · 172 unmodified lines | ||
| 768 | 777 | |
| 769 | 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 | 782 | "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], |
| 772 | 783 | |
| 773 | 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 | 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 | 1064 | "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], |
| 1050 | 1065 | |
| 1051 | 1066 | "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], |
| @@ -1090,6 +1105,10 @@ · 38 unmodified lines | ||
| 1090 | 1105 | |
| 1091 | 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 | 1112 | "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], |
| 1094 | 1113 | |
| 1095 | 1114 | "tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], |