- Remove early return guard in EventRouter that silently dropped all provider events when no thread was active (e.g., user on index route). Use a ref for activeThreadId to avoid re-subscribing the event listener on navigation. - Add VISIT_THREAD action to update lastVisitedAt when navigating to a thread, restoring the ability to dismiss the 'Completed' pill in the sidebar. - Restore ADD_THREAD dispatch in AutoProjectBootstrap so first-time browser users get an initial thread instead of a blank placeholder. - Delete duplicate vite.config.js that duplicated the existing vite.config.ts. - Await navigation before dispatching DELETE_THREAD in sidebar to prevent the route guard redirect from racing and overriding the fallback thread navigation.
Unified · Split
| @@ -310,18 +310,18 @@ export default function Sidebar() { · 309 unmodified lines | ||
| 310 | 310 | |
| 311 | 311 | const shouldNavigateToFallback = routeThreadId === threadId; |
| 312 | 312 | const fallbackThreadId = state.threads.find((entry) => entry.id !== threadId)?.id ?? null; |
| 313 | − dispatch({ type: "DELETE_THREAD", threadId }); | |
| 314 | 313 | if (shouldNavigateToFallback) { |
| 315 | 314 | if (fallbackThreadId) { |
| 316 | − void navigate({ | |
| 315 | + await navigate({ | |
| 317 | 316 | to: "/$threadId", |
| 318 | 317 | params: { threadId: fallbackThreadId }, |
| 319 | 318 | replace: true, |
| 320 | 319 | }); |
| 321 | 320 | } else { |
| 322 | − void navigate({ to: "/", replace: true }); | |
| 321 | + await navigate({ to: "/", replace: true }); | |
| 323 | 322 | } |
| 324 | 323 | } |
| 324 | + dispatch({ type: "DELETE_THREAD", threadId }); | |
| 325 | 325 | |
| 326 | 326 | if (!shouldDeleteWorktree || !orphanedWorktreePath || !threadProject) { |
| 327 | 327 | return; |
| @@ -8,6 +8,7 @@ import { useNativeApi } from "../hooks/useNativeApi"; · 7 unmodified lines | ||
| 8 | 8 | import { invalidateGitQueries } from "../lib/gitReactQuery"; |
| 9 | 9 | import { DEFAULT_MODEL } from "../model-logic"; |
| 10 | 10 | import { useStore } from "../store"; |
| 11 | +import { createThread } from "../threadFactory"; | |
| 11 | 12 | import { onServerWelcome } from "../wsNativeApi"; |
| 12 | 13 | |
| 13 | 14 | export const Route = createRootRouteWithContext<{ |
| @@ -50,6 +51,8 @@ function EventRouter() { · 36 unmodified lines | ||
| 50 | 51 | strict: false, |
| 51 | 52 | select: (params) => params.threadId, |
| 52 | 53 | }); |
| 54 | + const activeThreadIdRef = useRef(activeThreadId); | |
| 55 | + activeThreadIdRef.current = activeThreadId; | |
| 53 | 56 | |
| 54 | 57 | useEffect(() => { |
| 55 | 58 | if (!api) return; |
| @@ -57,15 +60,14 @@ function EventRouter() { · 1 unmodified line | ||
| 57 | 60 | if (event.method === "turn/completed") { |
| 58 | 61 | void invalidateGitQueries(queryClient); |
| 59 | 62 | } |
| 60 | − if (!activeThreadId) return; | |
| 61 | 63 | dispatch({ |
| 62 | 64 | type: "APPLY_EVENT", |
| 63 | 65 | event, |
| 64 | 66 | activeAssistantItemRef, |
| 65 | − activeThreadId, | |
| 67 | + activeThreadId: activeThreadIdRef.current, | |
| 66 | 68 | }); |
| 67 | 69 | }); |
| 68 | − }, [activeThreadId, api, dispatch, queryClient]); | |
| 70 | + }, [api, dispatch, queryClient]); | |
| 69 | 71 | |
| 70 | 72 | useEffect(() => { |
| 71 | 73 | if (!api) return; |
| @@ -115,6 +117,10 @@ function AutoProjectBootstrap() { · 43 unmodified lines | ||
| 115 | 117 | scripts: [], |
| 116 | 118 | }, |
| 117 | 119 | }); |
| 120 | + dispatch({ | |
| 121 | + type: "ADD_THREAD", | |
| 122 | + thread: createThread(projectId), | |
| 123 | + }); | |
| 118 | 124 | dispatch({ type: "SET_THREADS_HYDRATED", hydrated: true }); |
| 119 | 125 | }); |
| 120 | 126 | }, [state.projects, dispatch]); |
| @@ -5,7 +5,7 @@ import ChatView from "../components/ChatView"; · 4 unmodified lines | ||
| 5 | 5 | import { useStore } from "../store"; |
| 6 | 6 | |
| 7 | 7 | function ChatThreadRouteView() { |
| 8 | − const { state } = useStore(); | |
| 8 | + const { state, dispatch } = useStore(); | |
| 9 | 9 | const navigate = useNavigate(); |
| 10 | 10 | const { threadId } = Route.useParams(); |
| 11 | 11 | const threadExists = threadId ? state.threads.some((thread) => thread.id === threadId) : false; |
| @@ -24,7 +24,9 @@ function ChatThreadRouteView() { · 12 unmodified lines | ||
| 24 | 24 | void navigate({ to: "/", replace: true }); |
| 25 | 25 | return; |
| 26 | 26 | } |
| 27 | − }, [navigate, state.threadsHydrated, threadExists, threadId]); | |
| 27 | + | |
| 28 | + dispatch({ type: "VISIT_THREAD", threadId }); | |
| 29 | + }, [dispatch, navigate, state.threadsHydrated, threadExists, threadId]); | |
| 28 | 30 | |
| 29 | 31 | if (!threadId || !state.threadsHydrated || !threadExists) { |
| 30 | 32 | return null; |
| @@ -67,7 +67,8 @@ type Action = · 66 unmodified lines | ||
| 67 | 67 | worktreePath: string | null; |
| 68 | 68 | } |
| 69 | 69 | | { type: "SET_RUNTIME_MODE"; mode: RuntimeMode } |
| 70 | − | { type: "DELETE_THREAD"; threadId: string }; | |
| 70 | + | { type: "DELETE_THREAD"; threadId: string } | |
| 71 | + | { type: "VISIT_THREAD"; threadId: string }; | |
| 71 | 72 | |
| 72 | 73 | // ── State ──────────────────────────────────────────────────────────── |
| 73 | 74 | |
| @@ -849,6 +850,15 @@ export function reducer(state: AppState, action: Action): AppState { · 775 unmodified lines | ||
| 849 | 850 | runtimeMode: action.mode, |
| 850 | 851 | }; |
| 851 | 852 | |
| 853 | + case "VISIT_THREAD": | |
| 854 | + return { | |
| 855 | + ...state, | |
| 856 | + threads: updateThread(state.threads, action.threadId, (t) => ({ | |
| 857 | + ...t, | |
| 858 | + lastVisitedAt: new Date().toISOString(), | |
| 859 | + })), | |
| 860 | + }; | |
| 861 | + | |
| 852 | 862 | case "DELETE_THREAD": |
| 853 | 863 | return { ...state, threads: state.threads.filter((t) => t.id !== action.threadId) }; |
| 854 | 864 | |
| @@ -1,41 +0,0 @@ | ||
| 1 | −import tailwindcss from "@tailwindcss/vite"; | |
| 2 | −import react from "@vitejs/plugin-react"; | |
| 3 | −import { tanstackRouter } from "@tanstack/router-plugin/vite"; | |
| 4 | −import { defineConfig } from "vite"; | |
| 5 | −const port = Number(process.env.PORT ?? 5173); | |
| 6 | −export default defineConfig({ | |
| 7 | − plugins: [ | |
| 8 | − tanstackRouter(), | |
| 9 | − react({ | |
| 10 | − babel: { | |
| 11 | − plugins: [["babel-plugin-react-compiler", { target: "19" }]], | |
| 12 | − }, | |
| 13 | − }), | |
| 14 | − tailwindcss(), | |
| 15 | − ], | |
| 16 | − define: { | |
| 17 | − // In dev mode, tell the web app where the WebSocket server lives | |
| 18 | − "import.meta.env.VITE_WS_URL": JSON.stringify(process.env.VITE_WS_URL ?? ""), | |
| 19 | − }, | |
| 20 | − experimental: { | |
| 21 | − enableNativePlugin: true, | |
| 22 | − }, | |
| 23 | − resolve: { | |
| 24 | − tsconfigPaths: true, | |
| 25 | − }, | |
| 26 | − server: { | |
| 27 | − port, | |
| 28 | − strictPort: true, | |
| 29 | − hmr: { | |
| 30 | − // Explicit config so Vite's HMR WebSocket connects reliably | |
| 31 | − // inside Electron's BrowserWindow. Vite 8 uses console.debug for | |
| 32 | − // connection logs — enable "Verbose" in DevTools to see them. | |
| 33 | − protocol: "ws", | |
| 34 | − host: "localhost", | |
| 35 | − }, | |
| 36 | − }, | |
| 37 | − build: { | |
| 38 | − outDir: "dist", | |
| 39 | − emptyOutDir: true, | |
| 40 | − }, | |
| 41 | −}); | |