History

Fix 5 bugs: dropped events, lastVisitedAt, bootstrap thread, duplicate config, delete race

- 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.

Cursor Agent committed 8 months ago

Commit 141f6d21 parent de9c0595 files · +27 −50Browse files

Unified · Split

5 files
  1. apps/web/src/components/Sidebar.tsx +3 −3
  2. apps/web/src/routes/__root.tsx +9 −3
  3. apps/web/src/routes/_chat.$threadId.tsx +4 −2
  4. apps/web/src/store.ts +11 −1
  5. apps/web/vite.config.js +0 −41
apps/web/src/components/Sidebar.tsx+3−3
@@ -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;
apps/web/src/routes/__root.tsx+9−3
@@ -8,6 +8,7 @@ import { useNativeApi } from "../hooks/useNativeApi"; · 7 unmodified lines
8import { invalidateGitQueries } from "../lib/gitReactQuery";8import { invalidateGitQueries } from "../lib/gitReactQuery";
9import { DEFAULT_MODEL } from "../model-logic";9import { DEFAULT_MODEL } from "../model-logic";
10import { useStore } from "../store";10import { useStore } from "../store";
11import { createThread } from "../threadFactory";
11import { onServerWelcome } from "../wsNativeApi";12import { onServerWelcome } from "../wsNativeApi";
12 13
13export const Route = createRootRouteWithContext<{14export 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]);
apps/web/src/routes/_chat.$threadId.tsx+4−2
@@ -5,7 +5,7 @@ import ChatView from "../components/ChatView"; · 4 unmodified lines
5import { useStore } from "../store";5import { useStore } from "../store";
6 6
7function ChatThreadRouteView() {7function 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;
apps/web/src/store.ts+11−1
@@ -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
apps/web/vite.config.js+0−41
@@ -1,41 +0,0 @@
1import tailwindcss from "@tailwindcss/vite";
2import react from "@vitejs/plugin-react";
3import { tanstackRouter } from "@tanstack/router-plugin/vite";
4import { defineConfig } from "vite";
5const port = Number(process.env.PORT ?? 5173);
6export 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});

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t