- 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 | const shouldNavigateToFallback = routeThreadId === threadId; | 311 | const shouldNavigateToFallback = routeThreadId === threadId; |
| 312 | const fallbackThreadId = state.threads.find((entry) => entry.id !== threadId)?.id ?? null; | 312 | const fallbackThreadId = state.threads.find((entry) => entry.id !== threadId)?.id ?? null; |
| 313 | dispatch({ type: "DELETE_THREAD", threadId }); | ||
| 314 | if (shouldNavigateToFallback) { | 313 | if (shouldNavigateToFallback) { |
| 315 | if (fallbackThreadId) { | 314 | if (fallbackThreadId) { |
| 316 | void navigate({ | 315 | await navigate({ |
| 317 | to: "/$threadId", | 316 | to: "/$threadId", |
| 318 | params: { threadId: fallbackThreadId }, | 317 | params: { threadId: fallbackThreadId }, |
| 319 | replace: true, | 318 | replace: true, |
| 320 | }); | 319 | }); |
| 321 | } else { | 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 | if (!shouldDeleteWorktree || !orphanedWorktreePath || !threadProject) { | 326 | if (!shouldDeleteWorktree || !orphanedWorktreePath || !threadProject) { |
| 327 | return; | 327 | return; |
| @@ -8,6 +8,7 @@ import { useNativeApi } from "../hooks/useNativeApi"; · 7 unmodified lines | |||
| 8 | import { invalidateGitQueries } from "../lib/gitReactQuery"; | 8 | import { invalidateGitQueries } from "../lib/gitReactQuery"; |
| 9 | import { DEFAULT_MODEL } from "../model-logic"; | 9 | import { DEFAULT_MODEL } from "../model-logic"; |
| 10 | import { useStore } from "../store"; | 10 | import { useStore } from "../store"; |
| 11 | import { createThread } from "../threadFactory"; | ||
| 11 | import { onServerWelcome } from "../wsNativeApi"; | 12 | import { onServerWelcome } from "../wsNativeApi"; |
| 12 | 13 | ||
| 13 | export const Route = createRootRouteWithContext<{ | 14 | export const Route = createRootRouteWithContext<{ |
| @@ -50,6 +51,8 @@ function EventRouter() { · 36 unmodified lines | |||
| 50 | strict: false, | 51 | strict: false, |
| 51 | select: (params) => params.threadId, | 52 | select: (params) => params.threadId, |
| 52 | }); | 53 | }); |
| 54 | const activeThreadIdRef = useRef(activeThreadId); | ||
| 55 | activeThreadIdRef.current = activeThreadId; | ||
| 53 | 56 | ||
| 54 | useEffect(() => { | 57 | useEffect(() => { |
| 55 | if (!api) return; | 58 | if (!api) return; |
| @@ -57,15 +60,14 @@ function EventRouter() { · 1 unmodified line | |||
| 57 | if (event.method === "turn/completed") { | 60 | if (event.method === "turn/completed") { |
| 58 | void invalidateGitQueries(queryClient); | 61 | void invalidateGitQueries(queryClient); |
| 59 | } | 62 | } |
| 60 | if (!activeThreadId) return; | ||
| 61 | dispatch({ | 63 | dispatch({ |
| 62 | type: "APPLY_EVENT", | 64 | type: "APPLY_EVENT", |
| 63 | event, | 65 | event, |
| 64 | activeAssistantItemRef, | 66 | activeAssistantItemRef, |
| 65 | activeThreadId, | 67 | activeThreadId: activeThreadIdRef.current, |
| 66 | }); | 68 | }); |
| 67 | }); | 69 | }); |
| 68 | }, [activeThreadId, api, dispatch, queryClient]); | 70 | }, [api, dispatch, queryClient]); |
| 69 | 71 | ||
| 70 | useEffect(() => { | 72 | useEffect(() => { |
| 71 | if (!api) return; | 73 | if (!api) return; |
| @@ -115,6 +117,10 @@ function AutoProjectBootstrap() { · 43 unmodified lines | |||
| 115 | scripts: [], | 117 | scripts: [], |
| 116 | }, | 118 | }, |
| 117 | }); | 119 | }); |
| 120 | dispatch({ | ||
| 121 | type: "ADD_THREAD", | ||
| 122 | thread: createThread(projectId), | ||
| 123 | }); | ||
| 118 | dispatch({ type: "SET_THREADS_HYDRATED", hydrated: true }); | 124 | dispatch({ type: "SET_THREADS_HYDRATED", hydrated: true }); |
| 119 | }); | 125 | }); |
| 120 | }, [state.projects, dispatch]); | 126 | }, [state.projects, dispatch]); |
| @@ -5,7 +5,7 @@ import ChatView from "../components/ChatView"; · 4 unmodified lines | |||
| 5 | import { useStore } from "../store"; | 5 | import { useStore } from "../store"; |
| 6 | 6 | ||
| 7 | function ChatThreadRouteView() { | 7 | function ChatThreadRouteView() { |
| 8 | const { state } = useStore(); | 8 | const { state, dispatch } = useStore(); |
| 9 | const navigate = useNavigate(); | 9 | const navigate = useNavigate(); |
| 10 | const { threadId } = Route.useParams(); | 10 | const { threadId } = Route.useParams(); |
| 11 | const threadExists = threadId ? state.threads.some((thread) => thread.id === threadId) : false; | 11 | const threadExists = threadId ? state.threads.some((thread) => thread.id === threadId) : false; |
| @@ -24,7 +24,9 @@ function ChatThreadRouteView() { · 12 unmodified lines | |||
| 24 | void navigate({ to: "/", replace: true }); | 24 | void navigate({ to: "/", replace: true }); |
| 25 | return; | 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 | if (!threadId || !state.threadsHydrated || !threadExists) { | 31 | if (!threadId || !state.threadsHydrated || !threadExists) { |
| 30 | return null; | 32 | return null; |
| @@ -67,7 +67,8 @@ type Action = · 66 unmodified lines | |||
| 67 | worktreePath: string | null; | 67 | worktreePath: string | null; |
| 68 | } | 68 | } |
| 69 | | { type: "SET_RUNTIME_MODE"; mode: RuntimeMode } | 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 | // ── State ──────────────────────────────────────────────────────────── | 73 | // ── State ──────────────────────────────────────────────────────────── |
| 73 | 74 | ||
| @@ -849,6 +850,15 @@ export function reducer(state: AppState, action: Action): AppState { · 775 unmodified lines | |||
| 849 | runtimeMode: action.mode, | 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 | case "DELETE_THREAD": | 862 | case "DELETE_THREAD": |
| 853 | return { ...state, threads: state.threads.filter((t) => t.id !== action.threadId) }; | 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 | }); | ||