authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-17 13:04:49-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-02-17 19:47:09-08:00
log1b409c39468298a2a0c01d988fc2d90340877f13
treea72fb92ead04015ce31fdee38c395451d76ee30d
parent60ed67e5e1e5b983c0d6f0315baeb98b6372daea

Refactor web query hooks and debounce composer path search

- add `@tanstack/react-pacer` and switch composer path debounce to `useDebouncedValue` - extract reusable `projectSearchEntriesQueryOptions` for workspace entry lookup - add `serverKeybindingsQueryOptions` and update consumers in chat/sidebar - keep branch toolbar mutation error handling at call sites

7 files changed, 99 insertions(+), 60 deletions(-)

apps/web/package.json+1
......@@ -13,6 +13,7 @@
1313 "dependencies": {
1414 "@base-ui/react": "^1.2.0",
1515 "@t3tools/contracts": "workspace:*",
16 "@tanstack/react-pacer": "^0.19.4",
1617 "@tanstack/react-query": "^5.90.0",
1718 "@xterm/addon-fit": "^0.11.0",
1819 "@xterm/xterm": "^6.0.0",
apps/web/src/components/BranchToolbar.tsx+13-12
......@@ -68,19 +68,13 @@ export default function BranchToolbar({
6868 : branchNames;
6969 // ── Mutations ─────────────────────────────────────────────────────────
7070
71 const checkoutMutation = useMutation({
72 ...gitCheckoutMutationOptions({ api, cwd: branchCwd, queryClient }),
73 onError: (error) => {
74 setThreadError(error instanceof Error ? error.message : "Failed to checkout branch.");
75 setIsBranchMenuOpen(true);
76 },
77 });
71 const checkoutMutation = useMutation(
72 gitCheckoutMutationOptions({ api, cwd: branchCwd, queryClient }),
73 );
7874
79 const createBranchMutation = useMutation({
80 ...gitCreateBranchAndCheckoutMutationOptions({ api, cwd: branchCwd, queryClient }),
81 onError: (error) =>
82 setThreadError(error instanceof Error ? error.message : "Failed to create branch."),
83 });
75 const createBranchMutation = useMutation(
76 gitCreateBranchAndCheckoutMutationOptions({ api, cwd: branchCwd, queryClient }),
77 );
8478
8579 // ── Effects ───────────────────────────────────────────────────────────
8680
......@@ -159,6 +153,10 @@ export default function BranchToolbar({
159153 setIsBranchMenuOpen(false);
160154 onComposerFocusRequest?.();
161155 },
156 onError: (error) => {
157 setThreadError(error instanceof Error ? error.message : "Failed to checkout branch.");
158 setIsBranchMenuOpen(true);
159 },
162160 });
163161 };
164162
......@@ -173,6 +171,9 @@ export default function BranchToolbar({
173171 setIsBranchMenuOpen(false);
174172 onComposerFocusRequest?.();
175173 },
174 onError: (error) => {
175 setThreadError(error instanceof Error ? error.message : "Failed to create branch.");
176 },
176177 });
177178 };
178179
apps/web/src/components/ChatView.tsx+23-43
......@@ -27,8 +27,10 @@ import {
2727 useState,
2828} from "react";
2929import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
30import { useDebouncedValue } from "@tanstack/react-pacer/debouncer";
3031import { gitBranchesQueryOptions, gitCreateWorktreeMutationOptions } from "~/lib/gitReactQuery";
31import { serverConfigQueryOptions } from "~/lib/serverReactQuery";
32import { projectSearchEntriesQueryOptions } from "~/lib/projectReactQuery";
33import { serverKeybindingsQueryOptions } from "~/lib/serverReactQuery";
3234
3335import { isElectron } from "../env";
3436import { buildBootstrapInput } from "../historyBootstrap";
......@@ -340,7 +342,6 @@ export default function ChatView() {
340342 const [nowTick, setNowTick] = useState(() => Date.now());
341343 const [terminalFocusRequestId, setTerminalFocusRequestId] = useState(0);
342344 const [composerCursor, setComposerCursor] = useState(0);
343 const [debouncedPathQuery, setDebouncedPathQuery] = useState("");
344345 const [composerMenuIndex, setComposerMenuIndex] = useState(0);
345346 const [composerKeyboardHighlight, setComposerKeyboardHighlight] = useState(false);
346347 const messagesScrollRef = useRef<HTMLDivElement>(null);
......@@ -462,47 +463,23 @@ export default function ChatView() {
462463 const composerTriggerKind = composerTrigger?.kind ?? null;
463464 const pathTriggerQuery = composerTrigger?.kind === "path" ? composerTrigger.query : "";
464465 const isPathTrigger = composerTriggerKind === "path";
466 const [debouncedPathQuery, composerPathQueryDebouncer] = useDebouncedValue(
467 pathTriggerQuery,
468 { wait: COMPOSER_PATH_QUERY_DEBOUNCE_MS },
469 (debouncerState) => ({ isPending: debouncerState.isPending }),
470 );
471 const effectivePathQuery = pathTriggerQuery.length > 0 ? debouncedPathQuery : "";
465472 const branchesQuery = useQuery(gitBranchesQueryOptions(api, gitCwd));
466 const keybindingsQuery = useQuery({
467 ...serverConfigQueryOptions(api),
468 select: (config) => config.keybindings,
469 });
470 useEffect(() => {
471 if (!isPathTrigger) {
472 setDebouncedPathQuery("");
473 return;
474 }
475 if (pathTriggerQuery.length === 0) {
476 setDebouncedPathQuery("");
477 return;
478 }
479 const timer = window.setTimeout(() => {
480 setDebouncedPathQuery(pathTriggerQuery);
481 }, COMPOSER_PATH_QUERY_DEBOUNCE_MS);
482 return () => {
483 window.clearTimeout(timer);
484 };
485 }, [isPathTrigger, pathTriggerQuery]);
486 const workspaceEntriesQuery = useQuery({
487 queryKey: [
488 "composer-workspace-entries",
489 gitCwd,
490 debouncedPathQuery,
491 ],
492 enabled: Boolean(api && gitCwd && isPathTrigger),
493 staleTime: 15_000,
494 placeholderData: (previous) => previous,
495 queryFn: async () => {
496 if (!api || !gitCwd || !isPathTrigger) {
497 return { entries: [], truncated: false };
498 }
499 return api.projects.searchEntries({
500 cwd: gitCwd,
501 query: debouncedPathQuery,
502 limit: 80,
503 });
504 },
505 });
473 const keybindingsQuery = useQuery(serverKeybindingsQueryOptions(api));
474 const workspaceEntriesQuery = useQuery(
475 projectSearchEntriesQueryOptions({
476 api,
477 cwd: gitCwd,
478 query: effectivePathQuery,
479 enabled: isPathTrigger,
480 limit: 80,
481 }),
482 );
506483 const workspaceEntries = workspaceEntriesQuery.data?.entries ?? EMPTY_PROJECT_ENTRIES;
507484 const composerMenuItems = useMemo<ComposerCommandItem[]>(() => {
508485 if (!composerTrigger) return [];
......@@ -1301,7 +1278,10 @@ export default function ChatView() {
13011278 setComposerMenuIndex(index);
13021279 }, []);
13031280 const isComposerMenuLoading =
1304 composerTriggerKind === "path" && (workspaceEntriesQuery.isLoading || workspaceEntriesQuery.isFetching);
1281 composerTriggerKind === "path" &&
1282 ((pathTriggerQuery.length > 0 && composerPathQueryDebouncer.state.isPending) ||
1283 workspaceEntriesQuery.isLoading ||
1284 workspaceEntriesQuery.isFetching);
13051285
13061286 const onPromptChange = useCallback((nextPrompt: string, nextCursor: number) => {
13071287 setPrompt(nextPrompt);
apps/web/src/components/Sidebar.tsx+2-5
......@@ -16,7 +16,7 @@ import {
1616} from "../types";
1717import { useNativeApi } from "../hooks/useNativeApi";
1818import { gitRemoveWorktreeMutationOptions } from "../lib/gitReactQuery";
19import { serverConfigQueryOptions } from "../lib/serverReactQuery";
19import { serverKeybindingsQueryOptions } from "../lib/serverReactQuery";
2020import { toastManager } from "./ui/toast";
2121import { formatWorktreePathForDisplay, getOrphanedWorktreePathForThread } from "../worktreeCleanup";
2222
......@@ -118,10 +118,7 @@ function terminalStatusIndicator(thread: Thread): TerminalStatusIndicator | null
118118export default function Sidebar() {
119119 const { state, dispatch } = useStore();
120120 const api = useNativeApi();
121 const { data: keybindings = EMPTY_KEYBINDINGS } = useQuery({
122 ...serverConfigQueryOptions(api),
123 select: (config) => config.keybindings,
124 });
121 const { data: keybindings = EMPTY_KEYBINDINGS } = useQuery(serverKeybindingsQueryOptions(api));
125122 const queryClient = useQueryClient();
126123 const removeWorktreeMutation = useMutation(
127124 gitRemoveWorktreeMutationOptions({ api, queryClient }),
apps/web/src/lib/projectReactQuery.ts created+42
......@@ -0,0 +1,42 @@
1import type { NativeApi, ProjectSearchEntriesResult } from "@t3tools/contracts";
2import { queryOptions } from "@tanstack/react-query";
3
4export const projectQueryKeys = {
5 all: ["projects"] as const,
6 searchEntries: (cwd: string | null, query: string, limit: number) =>
7 ["projects", "search-entries", cwd, query, limit] as const,
8};
9
10const DEFAULT_SEARCH_ENTRIES_LIMIT = 80;
11const DEFAULT_SEARCH_ENTRIES_STALE_TIME = 15_000;
12const EMPTY_SEARCH_ENTRIES_RESULT: ProjectSearchEntriesResult = {
13 entries: [],
14 truncated: false,
15};
16
17export function projectSearchEntriesQueryOptions(input: {
18 api: NativeApi | undefined;
19 cwd: string | null;
20 query: string;
21 enabled?: boolean;
22 limit?: number;
23 staleTime?: number;
24}) {
25 const limit = input.limit ?? DEFAULT_SEARCH_ENTRIES_LIMIT;
26 return queryOptions({
27 queryKey: projectQueryKeys.searchEntries(input.cwd, input.query, limit),
28 queryFn: async () => {
29 if (!input.api || !input.cwd) {
30 throw new Error("Workspace entry search is unavailable.");
31 }
32 return input.api.projects.searchEntries({
33 cwd: input.cwd,
34 query: input.query,
35 limit,
36 });
37 },
38 enabled: input.enabled ?? Boolean(input.api && input.cwd),
39 staleTime: input.staleTime ?? DEFAULT_SEARCH_ENTRIES_STALE_TIME,
40 placeholderData: (previous) => previous ?? EMPTY_SEARCH_ENTRIES_RESULT,
41 });
42}
apps/web/src/lib/serverReactQuery.ts+7
......@@ -19,3 +19,10 @@ export function serverConfigQueryOptions(api: NativeApi | undefined) {
1919 staleTime: Infinity,
2020 });
2121}
22
23export function serverKeybindingsQueryOptions(api: NativeApi | undefined) {
24 return queryOptions({
25 ...serverConfigQueryOptions(api),
26 select: (config) => config.keybindings,
27 });
28}
bun.lock+11
......@@ -53,6 +53,7 @@
5353 "dependencies": {
5454 "@base-ui/react": "^1.2.0",
5555 "@t3tools/contracts": "workspace:*",
56 "@tanstack/react-pacer": "^0.19.4",
5657 "@tanstack/react-query": "^5.90.0",
5758 "@xterm/addon-fit": "^0.11.0",
5859 "@xterm/xterm": "^6.0.0",
......@@ -425,10 +426,20 @@
425426
426427 "@tailwindcss/vite": ["@tailwindcss/vite@4.1.18", "", { "dependencies": { "@tailwindcss/node": "4.1.18", "@tailwindcss/oxide": "4.1.18", "tailwindcss": "4.1.18" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA=="],
427428
429 "@tanstack/devtools-event-client": ["@tanstack/devtools-event-client@0.4.0", "", {}, "sha512-RPfGuk2bDZgcu9bAJodvO2lnZeHuz4/71HjZ0bGb/SPg8+lyTA+RLSKQvo7fSmPSi8/vcH3aKQ8EM9ywf1olaw=="],
430
431 "@tanstack/pacer": ["@tanstack/pacer@0.18.0", "", { "dependencies": { "@tanstack/devtools-event-client": "^0.4.0", "@tanstack/store": "^0.8.0" } }, "sha512-qhCRSFei0hokQr3xYcQXqxsRD/LKlgHCxHXtKHrQoImp4x2Zu6tUOpUGVH4y2qexIrzSu3aibQBNNfC3Eay6Mg=="],
432
428433 "@tanstack/query-core": ["@tanstack/query-core@5.90.20", "", {}, "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg=="],
429434
435 "@tanstack/react-pacer": ["@tanstack/react-pacer@0.19.4", "", { "dependencies": { "@tanstack/pacer": "0.18.0", "@tanstack/react-store": "^0.8.0" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-coj8ULAuR0qFpjAKD44gTgRuZyjxU6Xu+IX5MwwYvr4e61OtZcJshaExoOBKpCGde0Edb12jDnzzj2Im13Qm9Q=="],
436
430437 "@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=="],
431438
439 "@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=="],
440
441 "@tanstack/store": ["@tanstack/store@0.8.1", "", {}, "sha512-PtOisLjUZPz5VyPRSCGjNOlwTvabdTBQ2K80DpVL1chGVr35WRxfeavAPdNq6pm/t7F8GhoR2qtmkkqtCEtHYw=="],
442
432443 "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
433444
434445 "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],