History

Address PR feedback on router and thread hydration

Julius Marminge committed 9 months ago

Commit fe69e961 parent e9d40e412 files · +104 −20Browse files

Unified · Split

12 files
  1. apps/web/src/components/BranchToolbar.tsx +3 −1
  2. apps/web/src/components/ChatView.tsx +18 −4
  3. apps/web/src/components/GitActionsControl.tsx +6 −4
  4. apps/web/src/components/Sidebar.tsx +4 −2
  5. apps/web/src/router.tsx +9 −2
  6. apps/web/src/routes/__root.tsx +5 −0
  7. apps/web/src/routes/_chat.$threadId.tsx +6 −2
  8. apps/web/src/routes/_chat.index.tsx +2 −5
  9. apps/web/src/store.test.ts +14 −0
  10. apps/web/src/store.ts +13 −0
  11. apps/web/src/truncateTitle.test.ts +17 −0
  12. apps/web/src/truncateTitle.ts +7 −0
apps/web/src/components/BranchToolbar.tsx+3−1
@@ -23,6 +23,7 @@ import { · 22 unmodified lines
2323 import { ChevronDownIcon } from "lucide-react";
2424
2525 interface BranchToolbarProps {
26+ threadId: string;
2627 envMode: "local" | "worktree";
2728 onEnvModeChange: (mode: "local" | "worktree") => void;
2829 envLocked: boolean;
@@ -30,6 +31,7 @@ interface BranchToolbarProps { · 1 unmodified line
3031 }
3132
3233 export default function BranchToolbar({
34+ threadId,
3335 envMode,
3436 onEnvModeChange,
3537 envLocked,
@@ -42,7 +44,7 @@ export default function BranchToolbar({ · 6 unmodified lines
4244 const [isBranchMenuOpen, setIsBranchMenuOpen] = useState(false);
4345 const [branchQuery, setBranchQuery] = useState("");
4446
45− const activeThread = state.threads.find((thread) => thread.id === state.activeThreadId);
47+ const activeThread = state.threads.find((thread) => thread.id === threadId);
4648 const activeProject = state.projects.find((project) => project.id === activeThread?.projectId);
4749 const activeThreadId = activeThread?.id;
4850 const activeThreadBranch = activeThread?.branch ?? null;
apps/web/src/components/ChatView.tsx+18−4
@@ -63,6 +63,7 @@ import { · 62 unmodified lines
6363 } from "../session-logic";
6464 import { isScrollContainerNearBottom } from "../chat-scroll";
6565 import { useStore } from "../store";
66+import { truncateTitle } from "../truncateTitle";
6667 import {
6768 DEFAULT_THREAD_TERMINAL_ID,
6869 MAX_THREAD_TERMINAL_COUNT,
@@ -1129,6 +1130,9 @@ export default function ChatView({ threadId }: ChatViewProps) { · 1060 unmodified lines
11291130 revokePreviewUrls(existing);
11301131 return [];
11311132 });
1133+ setPrompt("");
1134+ promptRef.current = "";
1135+ setIsSending(false);
11321136 setComposerCursor(0);
11331137 setComposerHighlightedItemId(null);
11341138 dragDepthRef.current = 0;
@@ -1650,7 +1654,7 @@ export default function ChatView({ threadId }: ChatViewProps) { · 515 unmodified lines
16501654 (composerImagesSnapshot.length > 0
16511655 ? `Image: ${composerImagesSnapshot[0]?.name ?? "attachment"}`
16521656 : "New thread");
1653− const title = titleSeed.length > 50 ? `${titleSeed.slice(0, 50)}...` : titleSeed;
1657+ const title = truncateTitle(titleSeed);
16541658 dispatch({
16551659 type: "SET_THREAD_TITLE",
16561660 threadId: activeThread.id,
@@ -1944,6 +1948,7 @@ export default function ChatView({ threadId }: ChatViewProps) { · 287 unmodified lines
19441948 className={`flex items-center justify-between border-b border-border px-5 ${isElectron ? "drag-region h-[52px]" : "py-3"}`}
19451949 >
19461950 <ChatHeader
1951+ activeThreadId={activeThread.id}
19471952 activeThreadTitle={activeThread.title}
19481953 activeProjectName={activeProject?.name}
19491954 activeProjectScripts={activeProject?.scripts}
@@ -2210,6 +2215,7 @@ export default function ChatView({ threadId }: ChatViewProps) { · 260 unmodified lines
22102215
22112216 {isGitRepo && (
22122217 <BranchToolbar
2218+ threadId={activeThread.id}
22132219 envMode={envMode}
22142220 onEnvModeChange={onEnvModeChange}
22152221 envLocked={envLocked}
@@ -2286,6 +2292,7 @@ export default function ChatView({ threadId }: ChatViewProps) { · 70 unmodified lines
22862292 }
22872293
22882294 interface ChatHeaderProps {
2295+ activeThreadId: string;
22892296 activeThreadTitle: string;
22902297 activeProjectName: string | undefined;
22912298 activeProjectScripts: ProjectScript[] | undefined;
@@ -2301,6 +2308,7 @@ interface ChatHeaderProps { · 9 unmodified lines
23012308 }
23022309
23032310 const ChatHeader = memo(function ChatHeader({
2311+ activeThreadId,
23042312 activeThreadTitle,
23052313 activeProjectName,
23062314 activeProjectScripts,
@@ -2333,8 +2341,12 @@ const ChatHeader = memo(function ChatHeader({ · 26 unmodified lines
23332341 onUpdateScript={onUpdateProjectScript}
23342342 />
23352343 )}
2336− {activeProjectName && <OpenInPicker keybindings={keybindings} />}
2337− {activeProjectName && <GitActionsControl api={api} gitCwd={gitCwd} />}
2344+ {activeProjectName && (
2345+ <OpenInPicker keybindings={keybindings} activeThreadId={activeThreadId} />
2346+ )}
2347+ {activeProjectName && (
2348+ <GitActionsControl api={api} gitCwd={gitCwd} activeThreadId={activeThreadId} />
2349+ )}
23382350 <Toggle
23392351 pressed={diffOpen}
23402352 onPressedChange={onToggleDiff}
@@ -2827,8 +2839,10 @@ const ReasoningEffortPicker = memo(function ReasoningEffortPicker(props: { · 486 unmodified lines
28272839
28282840 const OpenInPicker = memo(function OpenInPicker({
28292841 keybindings,
2842+ activeThreadId,
28302843 }: {
28312844 keybindings: ResolvedKeybindingsConfig;
2845+ activeThreadId: string | null;
28322846 }) {
28332847 const [lastEditor, setLastEditor] = useState<EditorId>(() => {
28342848 const stored = localStorage.getItem(LAST_EDITOR_KEY);
@@ -2855,7 +2869,7 @@ const OpenInPicker = memo(function OpenInPicker({ · 20 unmodified lines
28552869
28562870 const api = useNativeApi();
28572871 const { state } = useStore();
2858− const activeThread = state.threads.find((t) => t.id === state.activeThreadId);
2872+ const activeThread = state.threads.find((t) => t.id === activeThreadId);
28592873 const activeProject = state.projects.find((p) => p.id === activeThread?.projectId);
28602874
28612875 const openInEditor = useCallback(
apps/web/src/components/GitActionsControl.tsx+6−4
@@ -38,12 +38,12 @@ import { · 37 unmodified lines
3838 gitStatusQueryOptions,
3939 invalidateGitQueries,
4040 } from "~/lib/gitReactQuery";
41−import { useStore } from "~/store";
4241 import { preferredTerminalEditor, resolvePathLinkTarget } from "~/terminal-links";
4342
4443 interface GitActionsControlProps {
4544 api: NativeApi | undefined;
4645 gitCwd: string | null;
46+ activeThreadId: string | null;
4747 }
4848
4949 function getMenuActionDisabledReason(
@@ -136,9 +136,11 @@ function GitQuickActionIcon({ quickAction }: { quickAction: GitQuickAction }) { · 86 unmodified lines
136136 return <InfoIcon className={iconClassName} />;
137137 }
138138
139−export default function GitActionsControl({ api, gitCwd }: GitActionsControlProps) {
140− const { state } = useStore();
141− const activeThreadId = state.activeThreadId;
139+export default function GitActionsControl({
140+ api,
141+ gitCwd,
142+ activeThreadId,
143+}: GitActionsControlProps) {
142144 const threadToastData = useMemo(
143145 () => (activeThreadId ? { threadId: activeThreadId } : undefined),
144146 [activeThreadId],
apps/web/src/components/Sidebar.tsx+4−2
@@ -397,7 +397,9 @@ export default function Sidebar() { · 396 unmodified lines
397397
398398 useEffect(() => {
399399 const onWindowKeyDown = (event: KeyboardEvent) => {
400− const activeThread = state.threads.find((t) => t.id === state.activeThreadId);
400+ const activeThread = routeThreadId
401+ ? state.threads.find((thread) => thread.id === routeThreadId)
402+ : undefined;
401403 if (isChatNewLocalShortcut(event, keybindings)) {
402404 const projectId = activeThread?.projectId ?? state.projects[0]?.id;
403405 if (!projectId) return;
@@ -420,7 +422,7 @@ export default function Sidebar() { · 16 unmodified lines
420422 return () => {
421423 window.removeEventListener("keydown", onWindowKeyDown);
422424 };
423− }, [handleNewThread, keybindings, state.activeThreadId, state.projects, state.threads]);
425+ }, [handleNewThread, keybindings, routeThreadId, state.projects, state.threads]);
424426
425427 return (
426428 <aside className="sidebar flex h-full w-[260px] shrink-0 flex-col border-r border-border bg-card">
apps/web/src/router.tsx+9−2
@@ -1,8 +1,15 @@
1−import { createRouter } from "@tanstack/react-router";
1+import { createHashHistory, createRouter } from "@tanstack/react-router";
22
3+import { isElectron } from "./env";
34 import { routeTree } from "./routeTree.gen";
45
5−export const router = createRouter({ routeTree });
6+const useHashHistory =
7+ isElectron && typeof window !== "undefined" && window.location.protocol === "file:";
8+
9+export const router = createRouter({
10+ routeTree,
11+ ...(useHashHistory ? { history: createHashHistory() } : {}),
12+});
613
714 declare module "@tanstack/react-router" {
815 interface Register {
apps/web/src/routes/__root.tsx+5−0
@@ -67,6 +67,7 @@ function AutoProjectBootstrap() { · 66 unmodified lines
6767 threadId: existingThread.id,
6868 });
6969 }
70+ dispatch({ type: "SET_THREADS_HYDRATED", hydrated: true });
7071 return;
7172 }
7273
@@ -82,12 +83,14 @@ function AutoProjectBootstrap() { · 9 unmodified lines
8283 cwd: payload.cwd,
8384 model: DEFAULT_MODEL,
8485 expanded: true,
86+ scripts: [],
8587 },
8688 });
8789 dispatch({
8890 type: "ADD_THREAD",
8991 thread: createThread(projectId),
9092 });
93+ dispatch({ type: "SET_THREADS_HYDRATED", hydrated: true });
9194 });
9295 }, [state.projects, state.threads, state.activeThreadId, dispatch]);
9396
@@ -118,8 +121,10 @@ function DesktopProjectBootstrap() { · 24 unmodified lines
118121 cwd: project.cwd,
119122 model: DEFAULT_MODEL,
120123 expanded: true,
124+ scripts: project.scripts,
121125 })),
122126 });
127+ dispatch({ type: "SET_THREADS_HYDRATED", hydrated: true });
123128 bootstrappedRef.current = true;
124129 } catch {
125130 if (disposed) return;
apps/web/src/routes/_chat.$threadId.tsx+6−2
@@ -16,6 +16,10 @@ function ChatThreadRouteView() { · 15 unmodified lines
1616 return;
1717 }
1818
19+ if (!state.threadsHydrated) {
20+ return;
21+ }
22+
1923 if (!threadExists) {
2024 void navigate({ to: "/", replace: true });
2125 return;
@@ -26,9 +30,9 @@ function ChatThreadRouteView() { · 4 unmodified lines
2630 }
2731
2832 dispatch({ type: "SET_ACTIVE_THREAD", threadId });
29− }, [dispatch, navigate, state.activeThreadId, threadExists, threadId]);
33+ }, [dispatch, navigate, state.activeThreadId, state.threadsHydrated, threadExists, threadId]);
3034
31− if (!threadId || !threadExists) {
35+ if (!threadId || !state.threadsHydrated || !threadExists) {
3236 return null;
3337 }
3438
apps/web/src/routes/_chat.index.tsx+2−5
@@ -4,6 +4,7 @@ import { type FormEvent, useMemo, useState } from "react"; · 3 unmodified lines
44 import { Button } from "../components/ui/button";
55 import { Textarea } from "../components/ui/textarea";
66 import { createThread } from "../threadFactory";
7+import { truncateTitle } from "../truncateTitle";
78 import { useStore } from "../store";
89
910 function ChatIndexRouteView() {
@@ -24,11 +25,7 @@ function ChatIndexRouteView() { · 14 unmodified lines
2425 event.preventDefault();
2526 if (!defaultProject) return;
2627
27− const titleSeed = draft.trim();
28− const threadTitle =
29− titleSeed.length > 0
30− ? (titleSeed.length > 50 ? `${titleSeed.slice(0, 50)}...` : titleSeed)
31− : undefined;
28+ const threadTitle = truncateTitle(draft);
3229 const thread = createThread(
3330 defaultProject.id,
3431 threadTitle
apps/web/src/store.test.ts+14−0
@@ -80,6 +80,7 @@ function makeState(thread: Thread): AppState { · 79 unmodified lines
8080 ],
8181 threads: [thread],
8282 activeThreadId: thread.id,
83+ threadsHydrated: true,
8384 runtimeMode: "full-access",
8485 diffOpen: false,
8586 diffThreadId: null,
@@ -617,6 +618,7 @@ describe("store reducer thread continuity", () => { · 531 unmodified lines
617618 }),
618619 ],
619620 activeThreadId: "thread-b",
621+ threadsHydrated: true,
620622 runtimeMode: "full-access",
621623 diffOpen: true,
622624 diffThreadId: "thread-b",
@@ -674,6 +676,7 @@ describe("store reducer thread continuity", () => { · 51 unmodified lines
674676 ],
675677 threads: [makeThread({ id: "thread-a", projectId: "project-old-a" })],
676678 activeThreadId: "thread-a",
679+ threadsHydrated: true,
677680 runtimeMode: "full-access",
678681 diffOpen: false,
679682 diffThreadId: null,
@@ -725,6 +728,16 @@ describe("store reducer thread continuity", () => { · 45 unmodified lines
725728 ]);
726729 });
727730
731+ it("marks threads hydration state", () => {
732+ const state = makeState(makeThread());
733+ const next = reducer(state, {
734+ type: "SET_THREADS_HYDRATED",
735+ hydrated: false,
736+ });
737+
738+ expect(next.threadsHydrated).toBe(false);
739+ });
740+
728741 it("deletes a project and all of its threads", () => {
729742 const state: AppState = {
730743 projects: [
@@ -756,6 +769,7 @@ describe("store reducer thread continuity", () => { · 25 unmodified lines
756769 }),
757770 ],
758771 activeThreadId: "thread-a",
772+ threadsHydrated: true,
759773 runtimeMode: "full-access",
760774 diffOpen: true,
761775 diffThreadId: "thread-a",
apps/web/src/store.ts+13−0
@@ -38,6 +38,7 @@ type Action = · 37 unmodified lines
3838 | { type: "ADD_PROJECT"; project: Project }
3939 | { type: "SET_PROJECT_SCRIPTS"; projectId: string; scripts: ProjectScript[] }
4040 | { type: "SYNC_PROJECTS"; projects: Project[] }
41+ | { type: "SET_THREADS_HYDRATED"; hydrated: boolean }
4142 | { type: "TOGGLE_PROJECT"; projectId: string }
4243 | { type: "DELETE_PROJECT"; projectId: string }
4344 | { type: "ADD_THREAD"; thread: Thread }
@@ -113,6 +114,7 @@ export interface AppState { · 69 unmodified lines
113114 projects: Project[];
114115 threads: Thread[];
115116 activeThreadId: string | null;
117+ threadsHydrated: boolean;
116118 runtimeMode: RuntimeMode;
117119 diffOpen: boolean;
118120 diffThreadId: string | null;
@@ -136,6 +138,7 @@ const initialState: AppState = { · 17 unmodified lines
136138 projects: [],
137139 threads: [],
138140 activeThreadId: null,
141+ threadsHydrated: false,
139142 runtimeMode: DEFAULT_RUNTIME_MODE,
140143 diffOpen: false,
141144 diffThreadId: null,
@@ -167,6 +170,7 @@ function readPersistedState(): AppState { · 25 unmodified lines
167170 ...hydrated,
168171 threads,
169172 activeThreadId,
173+ threadsHydrated: threads.length > 0,
170174 diffOpen: false,
171175 diffThreadId: null,
172176 diffTurnId: null,
@@ -593,6 +597,15 @@ export function reducer(state: AppState, action: Action): AppState { · 420 unmodified lines
593597 ),
594598 };
595599
600+ case "SET_THREADS_HYDRATED":
601+ if (state.threadsHydrated === action.hydrated) {
602+ return state;
603+ }
604+ return {
605+ ...state,
606+ threadsHydrated: action.hydrated,
607+ };
608+
596609 case "SYNC_PROJECTS": {
597610 const previousByCwd = new Map(
598611 state.projects.map((project) => [project.cwd, project] as const),
apps/web/src/truncateTitle.test.ts+17−0
@@ -0,0 +1,17 @@
1+import { describe, expect, it } from "vitest";
2+
3+import { truncateTitle } from "./truncateTitle";
4+
5+describe("truncateTitle", () => {
6+ it("trims surrounding whitespace", () => {
7+ expect(truncateTitle(" hello world ")).toBe("hello world");
8+ });
9+
10+ it("returns trimmed text when within max length", () => {
11+ expect(truncateTitle("alpha", 10)).toBe("alpha");
12+ });
13+
14+ it("appends ellipsis when text exceeds max length", () => {
15+ expect(truncateTitle("abcdefghij", 5)).toBe("abcde...");
16+ });
17+});
apps/web/src/truncateTitle.ts+7−0
@@ -0,0 +1,7 @@
1+export function truncateTitle(text: string, maxLength = 50): string {
2+ const trimmed = text.trim();
3+ if (trimmed.length <= maxLength) {
4+ return trimmed;
5+ }
6+ return `${trimmed.slice(0, maxLength)}...`;
7+}

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