diff --git a/apps/web/package.json b/apps/web/package.json index f777c322208965df8f78a8144e91c75b297c44a4..535ee79e1ec949f8e210048b20271da484db83a8 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -16,6 +16,7 @@ "@t3tools/contracts": "workspace:*", "@tanstack/react-pacer": "^0.19.4", "@tanstack/react-query": "^5.90.0", + "@tanstack/react-router": "^1.160.2", "@xterm/addon-fit": "^0.11.0", "@xterm/xterm": "^6.0.0", "class-variance-authority": "^0.7.1", diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 3afec7e3a2989b8ae0437241d735ae3d99dc1c1c..e1bcc719b51827377ef98a059fec48f1628c0a3c 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -1,265 +1,9 @@ -import { QueryClient, QueryClientProvider, useQueryClient } from "@tanstack/react-query"; -import { Activity, Suspense, lazy, useEffect, useRef } from "react"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { RouterProvider } from "@tanstack/react-router"; -import ChatView from "./components/ChatView"; -import Sidebar from "./components/Sidebar"; -import { isElectron } from "./env"; -import { DEFAULT_MODEL } from "./model-logic"; -import { StoreProvider, useStore } from "./store"; -import { DEFAULT_THREAD_TERMINAL_HEIGHT, DEFAULT_THREAD_TERMINAL_ID } from "./types"; -import { onServerWelcome } from "./wsNativeApi"; -import { useNativeApi } from "./hooks/useNativeApi"; -import { useMediaQuery } from "./hooks/useMediaQuery"; import { AnchoredToastProvider, ToastProvider } from "./components/ui/toast"; -import { Sheet, SheetPopup } from "./components/ui/sheet"; -import { invalidateGitQueries } from "./lib/gitReactQuery"; - -const DiffPanel = lazy(() => import("./components/DiffPanel")); -const DiffWorkerPoolProvider = lazy(() => - import("./components/DiffPanel").then((module) => ({ - default: module.DiffWorkerPoolProvider, - })), -); -const DIFF_INLINE_LAYOUT_MEDIA_QUERY = "(max-width: 1180px)"; - -const DiffPanelWrapper = (props: { children: React.ReactNode; sheet: boolean }) => { - const { state, dispatch } = useStore(); - if (props.sheet) { - return ( - { - if (!open) { - dispatch({ type: "CLOSE_DIFF" }); - } - }} - > - - {props.children} - - - ); - } - - return ( - - ); -}; - -function EventRouter() { - const api = useNativeApi(); - const { dispatch } = useStore(); - const queryClient = useQueryClient(); - const activeAssistantItemRef = useRef(null); - - useEffect(() => { - if (!api) return; - return api.providers.onEvent((event) => { - if (event.method === "turn/completed") { - void invalidateGitQueries(queryClient); - } - dispatch({ - type: "APPLY_EVENT", - event, - activeAssistantItemRef, - }); - }); - }, [api, dispatch, queryClient]); - - useEffect(() => { - if (!api) return; - return api.terminal.onEvent((event) => { - dispatch({ - type: "APPLY_TERMINAL_EVENT", - event, - }); - }); - }, [api, dispatch]); - - return null; -} - -function AutoProjectBootstrap() { - const { state, dispatch } = useStore(); - const bootstrappedRef = useRef(false); - - useEffect(() => { - // Browser mode bootstraps from server welcome. - // Electron bootstraps from persisted projects via DesktopProjectBootstrap. - if (isElectron) return; - - return onServerWelcome((payload) => { - if (bootstrappedRef.current) return; - - // Don't create duplicate projects for the same cwd - const existing = state.projects.find((p) => p.cwd === payload.cwd); - if (existing) { - bootstrappedRef.current = true; - // Ensure a thread is active - const existingThread = state.threads.find((t) => t.projectId === existing.id); - if (existingThread && !state.activeThreadId) { - dispatch({ - type: "SET_ACTIVE_THREAD", - threadId: existingThread.id, - }); - } - return; - } - - bootstrappedRef.current = true; - - // Create project + thread from server cwd - const projectId = crypto.randomUUID(); - dispatch({ - type: "ADD_PROJECT", - project: { - id: projectId, - name: payload.projectName, - cwd: payload.cwd, - model: DEFAULT_MODEL, - expanded: true, - scripts: [], - }, - }); - dispatch({ - type: "ADD_THREAD", - thread: { - id: crypto.randomUUID(), - codexThreadId: null, - projectId, - title: "New thread", - model: DEFAULT_MODEL, - terminalOpen: false, - terminalHeight: DEFAULT_THREAD_TERMINAL_HEIGHT, - terminalIds: [DEFAULT_THREAD_TERMINAL_ID], - runningTerminalIds: [], - activeTerminalId: DEFAULT_THREAD_TERMINAL_ID, - terminalGroups: [ - { - id: `group-${DEFAULT_THREAD_TERMINAL_ID}`, - terminalIds: [DEFAULT_THREAD_TERMINAL_ID], - }, - ], - activeTerminalGroupId: `group-${DEFAULT_THREAD_TERMINAL_ID}`, - session: null, - messages: [], - events: [], - turnDiffSummaries: [], - error: null, - createdAt: new Date().toISOString(), - branch: null, - worktreePath: null, - }, - }); - }); - }, [state.projects, state.threads, state.activeThreadId, dispatch]); - - return null; -} - -function DesktopProjectBootstrap() { - const api = useNativeApi(); - const { dispatch } = useStore(); - const bootstrappedRef = useRef(false); - - useEffect(() => { - if (!isElectron || !api || bootstrappedRef.current) return; - - let disposed = false; - let retryDelayMs = 500; - let retryTimer: ReturnType | null = null; - - const attemptBootstrap = async () => { - try { - const projects = await api.projects.list(); - if (disposed) return; - dispatch({ - type: "SYNC_PROJECTS", - projects: projects.map((project) => ({ - id: project.id, - name: project.name, - cwd: project.cwd, - model: DEFAULT_MODEL, - expanded: true, - scripts: project.scripts, - })), - }); - bootstrappedRef.current = true; - } catch { - if (disposed) return; - retryTimer = setTimeout(() => { - retryTimer = null; - void attemptBootstrap(); - }, retryDelayMs); - retryDelayMs = Math.min(retryDelayMs * 2, 5_000); - } - }; - - void attemptBootstrap(); - - return () => { - disposed = true; - if (retryTimer) { - clearTimeout(retryTimer); - } - }; - }, [api, dispatch]); - - return null; -} - -function Layout() { - const api = useNativeApi(); - const { state } = useStore(); - const shouldUseDiffSheet = useMediaQuery(DIFF_INLINE_LAYOUT_MEDIA_QUERY); - - const diffLoadingFallback = - !state.diffOpen || shouldUseDiffSheet ? ( - - Loading diff viewer... - - ) : ( - - ); - - if (!api) { - return ( - - - Connecting to T3 Code server... - - - ); - } - - return ( - - - - - - - - - - - - - - - - - ); -} +import { router } from "./router"; +import { StoreProvider } from "./store"; const queryClient = new QueryClient(); @@ -269,7 +13,7 @@ export default function App() { - + diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 232c07d97658a0dd6b8188d0564ba2de0c8a4275..b51308274ff6ba8db11c79fa593284e52e96e35f 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -337,7 +337,11 @@ const ComposerCommandMenu = memo(function ComposerCommandMenu(props: { ); }); -export default function ChatView() { +interface ChatViewProps { + threadId: string; +} + +export default function ChatView({ threadId }: ChatViewProps) { const { state, dispatch } = useStore(); const api = useNativeApi(); const { resolvedTheme } = useTheme(); @@ -375,7 +379,7 @@ export default function ChatView() { const terminalOpenByThreadRef = useRef>({}); const checkpointHydrationSessionRequestRef = useRef(new Set()); - const activeThread = state.threads.find((t) => t.id === state.activeThreadId); + const activeThread = state.threads.find((t) => t.id === threadId); const activeThreadId = activeThread?.id ?? null; const activeSessionId = activeThread?.session?.sessionId ?? null; const activeThreadRuntimeId = diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index a8ebff1b41bfa262abe83baf7cb0b3137e9a4d80..35ae22e1af0a066d393f712f87e810ac995d85ba 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -2,23 +2,20 @@ import { MonitorIcon, MoonIcon, SunIcon, TerminalIcon } from "lucide-react"; import { useCallback, useEffect, useMemo, useState } from "react"; import type { ResolvedKeybindingsConfig } from "@t3tools/contracts"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { useNavigate, useParams } from "@tanstack/react-router"; import { isElectron } from "../env"; import { useTheme } from "../hooks/useTheme"; import { DEFAULT_MODEL } from "../model-logic"; import { derivePendingApprovals } from "../session-logic"; import { useStore } from "../store"; import { isChatNewLocalShortcut, isChatNewShortcut } from "../keybindings"; -import { - DEFAULT_THREAD_TERMINAL_HEIGHT, - DEFAULT_THREAD_TERMINAL_ID, - type Project, - type Thread, -} from "../types"; +import { type Project, type Thread } from "../types"; import { useNativeApi } from "../hooks/useNativeApi"; import { gitRemoveWorktreeMutationOptions } from "../lib/gitReactQuery"; import { serverConfigQueryOptions } from "../lib/serverReactQuery"; import { toastManager } from "./ui/toast"; import { formatWorktreePathForDisplay, getOrphanedWorktreePathForThread } from "../worktreeCleanup"; +import { createThread } from "../threadFactory"; const THEME_CYCLE = { system: "light", light: "dark", dark: "system" } as const; const EMPTY_KEYBINDINGS: ResolvedKeybindingsConfig = []; @@ -118,6 +115,9 @@ function terminalStatusIndicator(thread: Thread): TerminalStatusIndicator | null export default function Sidebar() { const { state, dispatch } = useStore(); const api = useNativeApi(); + const navigate = useNavigate(); + const params = useParams({ strict: false }); + const routeThreadId = typeof params.threadId === "string" ? params.threadId : null; const { data: keybindings = EMPTY_KEYBINDINGS } = useQuery({ ...serverConfigQueryOptions(api), select: (config) => config.keybindings, @@ -147,38 +147,21 @@ export default function Sidebar() { worktreePath?: string | null; }, ) => { + const thread = createThread(projectId, { + model: state.projects.find((project) => project.id === projectId)?.model ?? DEFAULT_MODEL, + branch: options?.branch ?? null, + worktreePath: options?.worktreePath ?? null, + }); dispatch({ type: "ADD_THREAD", - thread: { - id: crypto.randomUUID(), - codexThreadId: null, - projectId, - title: "New thread", - model: state.projects.find((p) => p.id === projectId)?.model ?? DEFAULT_MODEL, - terminalOpen: false, - terminalHeight: DEFAULT_THREAD_TERMINAL_HEIGHT, - terminalIds: [DEFAULT_THREAD_TERMINAL_ID], - runningTerminalIds: [], - activeTerminalId: DEFAULT_THREAD_TERMINAL_ID, - terminalGroups: [ - { - id: `group-${DEFAULT_THREAD_TERMINAL_ID}`, - terminalIds: [DEFAULT_THREAD_TERMINAL_ID], - }, - ], - activeTerminalGroupId: `group-${DEFAULT_THREAD_TERMINAL_ID}`, - session: null, - messages: [], - events: [], - turnDiffSummaries: [], - error: null, - createdAt: new Date().toISOString(), - branch: options?.branch ?? null, - worktreePath: options?.worktreePath ?? null, - }, + thread, + }); + void navigate({ + to: "/$threadId", + params: { threadId: thread.id }, }); }, - [dispatch, state.projects], + [dispatch, navigate, state.projects], ); const focusMostRecentThreadForProject = useCallback( @@ -192,12 +175,12 @@ export default function Sidebar() { })[0]; if (!latestThread) return; - dispatch({ - type: "SET_ACTIVE_THREAD", - threadId: latestThread.id, + void navigate({ + to: "/$threadId", + params: { threadId: latestThread.id }, }); }, - [dispatch, state.threads], + [navigate, state.threads], ); const addProjectFromPath = useCallback( @@ -325,7 +308,20 @@ export default function Sidebar() { // Terminal may already be closed } + const shouldNavigateToFallback = routeThreadId === threadId; + const fallbackThreadId = state.threads.find((entry) => entry.id !== threadId)?.id ?? null; dispatch({ type: "DELETE_THREAD", threadId }); + if (shouldNavigateToFallback) { + if (fallbackThreadId) { + void navigate({ + to: "/$threadId", + params: { threadId: fallbackThreadId }, + replace: true, + }); + } else { + void navigate({ to: "/", replace: true }); + } + } if (!shouldDeleteWorktree || !orphanedWorktreePath || !threadProject) { return; @@ -351,7 +347,7 @@ export default function Sidebar() { }); } }, - [api, dispatch, removeWorktreeMutation, state.projects, state.threads], + [api, dispatch, navigate, removeWorktreeMutation, routeThreadId, state.projects, state.threads], ); const handleProjectContextMenu = useCallback( @@ -509,7 +505,7 @@ export default function Sidebar() { {project.expanded && ( {threads.map((thread) => { - const isActive = state.activeThreadId === thread.id; + const isActive = routeThreadId === thread.id; const threadStatus = threadStatusPill( thread, pendingApprovalByThreadId.get(thread.id) === true, @@ -524,12 +520,12 @@ export default function Sidebar() { ? "bg-accent text-foreground" : "text-muted-foreground hover:bg-secondary" }`} - onClick={() => - dispatch({ - type: "SET_ACTIVE_THREAD", - threadId: thread.id, - }) - } + onClick={() => { + void navigate({ + to: "/$threadId", + params: { threadId: thread.id }, + }); + }} onContextMenu={(e) => { e.preventDefault(); void handleThreadContextMenu(thread.id, { diff --git a/apps/web/src/router.tsx b/apps/web/src/router.tsx new file mode 100644 index 0000000000000000000000000000000000000000..6653a465a9d477eadbb918d566731497ee891790 --- /dev/null +++ b/apps/web/src/router.tsx @@ -0,0 +1,37 @@ +import { Outlet, createRootRoute, createRoute, createRouter } from "@tanstack/react-router"; + +import { ChatThreadRouteView } from "./routes/_chat.$threadId"; +import { ChatIndexRouteView } from "./routes/_chat.index"; +import { ChatRouteLayout } from "./routes/_chat"; + +const rootRoute = createRootRoute({ + component: Outlet, +}); + +const chatRoute = createRoute({ + getParentRoute: () => rootRoute, + path: "/", + component: ChatRouteLayout, +}); + +const chatIndexRoute = createRoute({ + getParentRoute: () => chatRoute, + path: "/", + component: ChatIndexRouteView, +}); + +const chatThreadRoute = createRoute({ + getParentRoute: () => chatRoute, + path: "$threadId", + component: ChatThreadRouteView, +}); + +const routeTree = rootRoute.addChildren([chatRoute.addChildren([chatIndexRoute, chatThreadRoute])]); + +export const router = createRouter({ routeTree }); + +declare module "@tanstack/react-router" { + interface Register { + router: typeof router; + } +} diff --git a/apps/web/src/routes/_chat.$threadId.tsx b/apps/web/src/routes/_chat.$threadId.tsx new file mode 100644 index 0000000000000000000000000000000000000000..ed0d9bf1733b8f0ca0f6f341bec907dc64f9d76d --- /dev/null +++ b/apps/web/src/routes/_chat.$threadId.tsx @@ -0,0 +1,37 @@ +import { useNavigate, useParams } from "@tanstack/react-router"; +import { useEffect } from "react"; + +import ChatView from "../components/ChatView"; +import { useStore } from "../store"; + +export function ChatThreadRouteView() { + const { state, dispatch } = useStore(); + const navigate = useNavigate(); + const params = useParams({ strict: false }); + const threadId = typeof params.threadId === "string" ? params.threadId : null; + const threadExists = threadId ? state.threads.some((thread) => thread.id === threadId) : false; + + useEffect(() => { + if (!threadId) { + void navigate({ to: "/", replace: true }); + return; + } + + if (!threadExists) { + void navigate({ to: "/", replace: true }); + return; + } + + if (state.activeThreadId === threadId) { + return; + } + + dispatch({ type: "SET_ACTIVE_THREAD", threadId }); + }, [dispatch, navigate, state.activeThreadId, threadExists, threadId]); + + if (!threadId || !threadExists) { + return null; + } + + return ; +} diff --git a/apps/web/src/routes/_chat.index.tsx b/apps/web/src/routes/_chat.index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..0a564166f30f8eb243bc2b5db99e20d074435414 --- /dev/null +++ b/apps/web/src/routes/_chat.index.tsx @@ -0,0 +1,84 @@ +import { useNavigate } from "@tanstack/react-router"; +import { type FormEvent, useMemo, useState } from "react"; + +import { Button } from "../components/ui/button"; +import { Textarea } from "../components/ui/textarea"; +import { createThread } from "../threadFactory"; +import { useStore } from "../store"; + +export function ChatIndexRouteView() { + const { state, dispatch } = useStore(); + const navigate = useNavigate(); + const [draft, setDraft] = useState(""); + const defaultProject = state.projects[0] ?? null; + const canCreateThread = defaultProject !== null; + + const placeholder = useMemo(() => { + if (!canCreateThread) { + return "Add a project in the sidebar to start chatting."; + } + return "Start with a goal, bug report, or implementation idea..."; + }, [canCreateThread]); + + const onCreateThread = (event: FormEvent) => { + event.preventDefault(); + if (!defaultProject) return; + + const titleSeed = draft.trim(); + const threadTitle = + titleSeed.length > 0 + ? (titleSeed.length > 50 ? `${titleSeed.slice(0, 50)}...` : titleSeed) + : undefined; + const thread = createThread( + defaultProject.id, + threadTitle + ? { model: defaultProject.model, title: threadTitle } + : { model: defaultProject.model }, + ); + + dispatch({ + type: "ADD_THREAD", + thread, + }); + + setDraft(""); + + void navigate({ + to: "/$threadId", + params: { threadId: thread.id }, + }); + }; + + return ( + + + + New chat + + Create a thread and continue in the full chat workspace. + + + + + setDraft(event.target.value)} + placeholder={placeholder} + className="min-h-36 resize-y" + disabled={!canCreateThread} + /> + + + {canCreateThread + ? "Create a thread first, then send your first prompt." + : "Projects are required before creating threads."} + + + Create thread + + + + + + ); +} diff --git a/apps/web/src/routes/_chat.tsx b/apps/web/src/routes/_chat.tsx new file mode 100644 index 0000000000000000000000000000000000000000..a101e231c139cfb8aeac7272e2655225482b6860 --- /dev/null +++ b/apps/web/src/routes/_chat.tsx @@ -0,0 +1,185 @@ +import { Outlet, useNavigate, useParams } from "@tanstack/react-router"; +import { useEffect, useRef } from "react"; +import { useQueryClient } from "@tanstack/react-query"; + +import DiffPanel from "../components/DiffPanel"; +import Sidebar from "../components/Sidebar"; +import { isElectron } from "../env"; +import { useNativeApi } from "../hooks/useNativeApi"; +import { invalidateGitQueries } from "../lib/gitReactQuery"; +import { DEFAULT_MODEL } from "../model-logic"; +import { createThread } from "../threadFactory"; +import { useStore } from "../store"; +import { onServerWelcome } from "../wsNativeApi"; + +function EventRouter() { + const api = useNativeApi(); + const { dispatch } = useStore(); + const queryClient = useQueryClient(); + const activeAssistantItemRef = useRef(null); + + useEffect(() => { + if (!api) return; + return api.providers.onEvent((event) => { + if (event.method === "turn/completed") { + void invalidateGitQueries(queryClient); + } + dispatch({ + type: "APPLY_EVENT", + event, + activeAssistantItemRef, + }); + }); + }, [api, dispatch, queryClient]); + + useEffect(() => { + if (!api) return; + return api.terminal.onEvent((event) => { + dispatch({ + type: "APPLY_TERMINAL_EVENT", + event, + }); + }); + }, [api, dispatch]); + + return null; +} + +function AutoProjectBootstrap() { + const { state, dispatch } = useStore(); + const bootstrappedRef = useRef(false); + + useEffect(() => { + // Browser mode bootstraps from server welcome. + // Electron bootstraps from persisted projects via DesktopProjectBootstrap. + if (isElectron) return; + + return onServerWelcome((payload) => { + if (bootstrappedRef.current) return; + + // Don't create duplicate projects for the same cwd + const existing = state.projects.find((project) => project.cwd === payload.cwd); + if (existing) { + bootstrappedRef.current = true; + // Ensure a thread is active + const existingThread = state.threads.find((thread) => thread.projectId === existing.id); + if (existingThread && !state.activeThreadId) { + dispatch({ + type: "SET_ACTIVE_THREAD", + threadId: existingThread.id, + }); + } + return; + } + + bootstrappedRef.current = true; + + // Create project + thread from server cwd + const projectId = crypto.randomUUID(); + dispatch({ + type: "ADD_PROJECT", + project: { + id: projectId, + name: payload.projectName, + cwd: payload.cwd, + model: DEFAULT_MODEL, + expanded: true, + }, + }); + dispatch({ + type: "ADD_THREAD", + thread: createThread(projectId), + }); + }); + }, [state.projects, state.threads, state.activeThreadId, dispatch]); + + return null; +} + +function DesktopProjectBootstrap() { + const api = useNativeApi(); + const { dispatch } = useStore(); + const bootstrappedRef = useRef(false); + + useEffect(() => { + if (!isElectron || !api || bootstrappedRef.current) return; + + let disposed = false; + let retryDelayMs = 500; + let retryTimer: ReturnType | null = null; + + const attemptBootstrap = async () => { + try { + const projects = await api.projects.list(); + if (disposed) return; + dispatch({ + type: "SYNC_PROJECTS", + projects: projects.map((project) => ({ + id: project.id, + name: project.name, + cwd: project.cwd, + model: DEFAULT_MODEL, + expanded: true, + })), + }); + bootstrappedRef.current = true; + } catch { + if (disposed) return; + retryTimer = setTimeout(() => { + retryTimer = null; + void attemptBootstrap(); + }, retryDelayMs); + retryDelayMs = Math.min(retryDelayMs * 2, 5_000); + } + }; + + void attemptBootstrap(); + + return () => { + disposed = true; + if (retryTimer) { + clearTimeout(retryTimer); + } + }; + }, [api, dispatch]); + + return null; +} + +export function ChatRouteLayout() { + const api = useNativeApi(); + const { state } = useStore(); + const navigate = useNavigate(); + const params = useParams({ strict: false }); + const routeThreadId = typeof params.threadId === "string" ? params.threadId : null; + + useEffect(() => { + if (routeThreadId || !state.activeThreadId) return; + void navigate({ + to: "/$threadId", + params: { threadId: state.activeThreadId }, + replace: true, + }); + }, [navigate, routeThreadId, state.activeThreadId]); + + if (!api) { + return ( + + + Connecting to T3 Code server... + + + ); + } + + return ( + + + + + + + {state.diffOpen && } + + ); +} diff --git a/apps/web/src/threadFactory.ts b/apps/web/src/threadFactory.ts new file mode 100644 index 0000000000000000000000000000000000000000..812564c0c37cabfd90fb1637e0d074e288c5a3cc --- /dev/null +++ b/apps/web/src/threadFactory.ts @@ -0,0 +1,41 @@ +import { DEFAULT_MODEL } from "./model-logic"; +import { DEFAULT_THREAD_TERMINAL_HEIGHT, DEFAULT_THREAD_TERMINAL_ID, type Thread } from "./types"; + +interface CreateThreadOptions { + branch?: string | null; + createdAt?: string; + model?: string; + title?: string; + worktreePath?: string | null; +} + +export function createThread(projectId: string, options: CreateThreadOptions = {}): Thread { + const createdAt = options.createdAt ?? new Date().toISOString(); + + return { + id: crypto.randomUUID(), + codexThreadId: null, + projectId, + title: options.title ?? "New thread", + model: options.model ?? DEFAULT_MODEL, + terminalOpen: false, + terminalHeight: DEFAULT_THREAD_TERMINAL_HEIGHT, + terminalIds: [DEFAULT_THREAD_TERMINAL_ID], + runningTerminalIds: [], + activeTerminalId: DEFAULT_THREAD_TERMINAL_ID, + terminalGroups: [ + { + id: `group-${DEFAULT_THREAD_TERMINAL_ID}`, + terminalIds: [DEFAULT_THREAD_TERMINAL_ID], + }, + ], + activeTerminalGroupId: `group-${DEFAULT_THREAD_TERMINAL_ID}`, + session: null, + messages: [], + events: [], + error: null, + createdAt, + branch: options.branch ?? null, + worktreePath: options.worktreePath ?? null, + }; +} diff --git a/bun.lock b/bun.lock index 687ca1bd4831d604f60ea58b21e73b162db2b743..ee7d0b4b095cd39acc7c01803354ebbb72105632 100644 --- a/bun.lock +++ b/bun.lock @@ -56,6 +56,7 @@ "@t3tools/contracts": "workspace:*", "@tanstack/react-pacer": "^0.19.4", "@tanstack/react-query": "^5.90.0", + "@tanstack/react-router": "^1.160.2", "@xterm/addon-fit": "^0.11.0", "@xterm/xterm": "^6.0.0", "class-variance-authority": "^0.7.1", @@ -447,6 +448,8 @@ "@tanstack/devtools-event-client": ["@tanstack/devtools-event-client@0.4.0", "", {}, "sha512-RPfGuk2bDZgcu9bAJodvO2lnZeHuz4/71HjZ0bGb/SPg8+lyTA+RLSKQvo7fSmPSi8/vcH3aKQ8EM9ywf1olaw=="], + "@tanstack/history": ["@tanstack/history@1.154.14", "", {}, "sha512-xyIfof8eHBuub1CkBnbKNKQXeRZC4dClhmzePHVOEel4G7lk/dW+TQ16da7CFdeNLv6u6Owf5VoBQxoo6DFTSA=="], + "@tanstack/pacer": ["@tanstack/pacer@0.18.0", "", { "dependencies": { "@tanstack/devtools-event-client": "^0.4.0", "@tanstack/store": "^0.8.0" } }, "sha512-qhCRSFei0hokQr3xYcQXqxsRD/LKlgHCxHXtKHrQoImp4x2Zu6tUOpUGVH4y2qexIrzSu3aibQBNNfC3Eay6Mg=="], "@tanstack/query-core": ["@tanstack/query-core@5.90.20", "", {}, "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg=="], @@ -455,8 +458,12 @@ "@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=="], + "@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=="], + "@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=="], + "@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=="], + "@tanstack/store": ["@tanstack/store@0.8.1", "", {}, "sha512-PtOisLjUZPz5VyPRSCGjNOlwTvabdTBQ2K80DpVL1chGVr35WRxfeavAPdNq6pm/t7F8GhoR2qtmkkqtCEtHYw=="], "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], @@ -609,6 +616,8 @@ "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], + "cookie-es": ["cookie-es@2.0.0", "", {}, "sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg=="], + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], @@ -793,6 +802,8 @@ "is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="], + "isbot": ["isbot@5.1.35", "", {}, "sha512-waFfC72ZNfwLLuJ2iLaoVaqcNo+CAaLR7xCpAn0Y5WfGzkNHv7ZN39Vbi1y+kb+Zs46XHOX3tZNExroFUPX+Kg=="], + "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], "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 @@ "serialize-error": ["serialize-error@7.0.1", "", { "dependencies": { "type-fest": "^0.13.1" } }, "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw=="], + "seroval": ["seroval@1.5.0", "", {}, "sha512-OE4cvmJ1uSPrKorFIH9/w/Qwuvi/IMcGbv5RKgcJ/zjA/IohDLU6SVaxFN9FwajbP7nsX0dQqMDes1whk3y+yw=="], + + "seroval-plugins": ["seroval-plugins@1.5.0", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-EAHqADIQondwRZIdeW2I636zgsODzoBDwb3PT/+7TLDWyw1Dy/Xv7iGUIEXXav7usHDE9HVhOU61irI3EnyyHA=="], + "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=="], "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], @@ -1129,6 +1144,10 @@ "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], + "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], + + "tiny-warning": ["tiny-warning@1.0.3", "", {}, "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="], + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], "tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
Connecting to T3 Code server...
+ Create a thread and continue in the full chat workspace. +
+ {canCreateThread + ? "Create a thread first, then send your first prompt." + : "Projects are required before creating threads."} +