| author | |
| committer | |
| log | 1b409c39468298a2a0c01d988fc2d90340877f13 |
| tree | a72fb92ead04015ce31fdee38c395451d76ee30d |
| parent | 60ed67e5e1e5b983c0d6f0315baeb98b6372daea |
- 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 sites7 files changed, 99 insertions(+), 60 deletions(-)
apps/web/package.json+1| ... | ... | @@ -13,6 +13,7 @@ |
| 13 | 13 | "dependencies": { |
| 14 | 14 | "@base-ui/react": "^1.2.0", |
| 15 | 15 | "@t3tools/contracts": "workspace:*", |
| 16 | "@tanstack/react-pacer": "^0.19.4", | |
| 16 | 17 | "@tanstack/react-query": "^5.90.0", |
| 17 | 18 | "@xterm/addon-fit": "^0.11.0", |
| 18 | 19 | "@xterm/xterm": "^6.0.0", |
apps/web/src/components/BranchToolbar.tsx+13-12| ... | ... | @@ -68,19 +68,13 @@ export default function BranchToolbar({ |
| 68 | 68 | : branchNames; |
| 69 | 69 | // ── Mutations ───────────────────────────────────────────────────────── |
| 70 | 70 | |
| 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 | ); | |
| 78 | 74 | |
| 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 | ); | |
| 84 | 78 | |
| 85 | 79 | // ── Effects ─────────────────────────────────────────────────────────── |
| 86 | 80 | |
| ... | ... | @@ -159,6 +153,10 @@ export default function BranchToolbar({ |
| 159 | 153 | setIsBranchMenuOpen(false); |
| 160 | 154 | onComposerFocusRequest?.(); |
| 161 | 155 | }, |
| 156 | onError: (error) => { | |
| 157 | setThreadError(error instanceof Error ? error.message : "Failed to checkout branch."); | |
| 158 | setIsBranchMenuOpen(true); | |
| 159 | }, | |
| 162 | 160 | }); |
| 163 | 161 | }; |
| 164 | 162 | |
| ... | ... | @@ -173,6 +171,9 @@ export default function BranchToolbar({ |
| 173 | 171 | setIsBranchMenuOpen(false); |
| 174 | 172 | onComposerFocusRequest?.(); |
| 175 | 173 | }, |
| 174 | onError: (error) => { | |
| 175 | setThreadError(error instanceof Error ? error.message : "Failed to create branch."); | |
| 176 | }, | |
| 176 | 177 | }); |
| 177 | 178 | }; |
| 178 | 179 |
apps/web/src/components/ChatView.tsx+23-43| ... | ... | @@ -27,8 +27,10 @@ import { |
| 27 | 27 | useState, |
| 28 | 28 | } from "react"; |
| 29 | 29 | import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; |
| 30 | import { useDebouncedValue } from "@tanstack/react-pacer/debouncer"; | |
| 30 | 31 | import { gitBranchesQueryOptions, gitCreateWorktreeMutationOptions } from "~/lib/gitReactQuery"; |
| 31 | import { serverConfigQueryOptions } from "~/lib/serverReactQuery"; | |
| 32 | import { projectSearchEntriesQueryOptions } from "~/lib/projectReactQuery"; | |
| 33 | import { serverKeybindingsQueryOptions } from "~/lib/serverReactQuery"; | |
| 32 | 34 | |
| 33 | 35 | import { isElectron } from "../env"; |
| 34 | 36 | import { buildBootstrapInput } from "../historyBootstrap"; |
| ... | ... | @@ -340,7 +342,6 @@ export default function ChatView() { |
| 340 | 342 | const [nowTick, setNowTick] = useState(() => Date.now()); |
| 341 | 343 | const [terminalFocusRequestId, setTerminalFocusRequestId] = useState(0); |
| 342 | 344 | const [composerCursor, setComposerCursor] = useState(0); |
| 343 | const [debouncedPathQuery, setDebouncedPathQuery] = useState(""); | |
| 344 | 345 | const [composerMenuIndex, setComposerMenuIndex] = useState(0); |
| 345 | 346 | const [composerKeyboardHighlight, setComposerKeyboardHighlight] = useState(false); |
| 346 | 347 | const messagesScrollRef = useRef<HTMLDivElement>(null); |
| ... | ... | @@ -462,47 +463,23 @@ export default function ChatView() { |
| 462 | 463 | const composerTriggerKind = composerTrigger?.kind ?? null; |
| 463 | 464 | const pathTriggerQuery = composerTrigger?.kind === "path" ? composerTrigger.query : ""; |
| 464 | 465 | 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 : ""; | |
| 465 | 472 | 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 | ); | |
| 506 | 483 | const workspaceEntries = workspaceEntriesQuery.data?.entries ?? EMPTY_PROJECT_ENTRIES; |
| 507 | 484 | const composerMenuItems = useMemo<ComposerCommandItem[]>(() => { |
| 508 | 485 | if (!composerTrigger) return []; |
| ... | ... | @@ -1301,7 +1278,10 @@ export default function ChatView() { |
| 1301 | 1278 | setComposerMenuIndex(index); |
| 1302 | 1279 | }, []); |
| 1303 | 1280 | 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); | |
| 1305 | 1285 | |
| 1306 | 1286 | const onPromptChange = useCallback((nextPrompt: string, nextCursor: number) => { |
| 1307 | 1287 | setPrompt(nextPrompt); |
apps/web/src/components/Sidebar.tsx+2-5| ... | ... | @@ -16,7 +16,7 @@ import { |
| 16 | 16 | } from "../types"; |
| 17 | 17 | import { useNativeApi } from "../hooks/useNativeApi"; |
| 18 | 18 | import { gitRemoveWorktreeMutationOptions } from "../lib/gitReactQuery"; |
| 19 | import { serverConfigQueryOptions } from "../lib/serverReactQuery"; | |
| 19 | import { serverKeybindingsQueryOptions } from "../lib/serverReactQuery"; | |
| 20 | 20 | import { toastManager } from "./ui/toast"; |
| 21 | 21 | import { formatWorktreePathForDisplay, getOrphanedWorktreePathForThread } from "../worktreeCleanup"; |
| 22 | 22 | |
| ... | ... | @@ -118,10 +118,7 @@ function terminalStatusIndicator(thread: Thread): TerminalStatusIndicator | null |
| 118 | 118 | export default function Sidebar() { |
| 119 | 119 | const { state, dispatch } = useStore(); |
| 120 | 120 | 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)); | |
| 125 | 122 | const queryClient = useQueryClient(); |
| 126 | 123 | const removeWorktreeMutation = useMutation( |
| 127 | 124 | gitRemoveWorktreeMutationOptions({ api, queryClient }), |
apps/web/src/lib/projectReactQuery.ts created+42| ... | ... | @@ -0,0 +1,42 @@ |
| 1 | import type { NativeApi, ProjectSearchEntriesResult } from "@t3tools/contracts"; | |
| 2 | import { queryOptions } from "@tanstack/react-query"; | |
| 3 | ||
| 4 | export 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 | ||
| 10 | const DEFAULT_SEARCH_ENTRIES_LIMIT = 80; | |
| 11 | const DEFAULT_SEARCH_ENTRIES_STALE_TIME = 15_000; | |
| 12 | const EMPTY_SEARCH_ENTRIES_RESULT: ProjectSearchEntriesResult = { | |
| 13 | entries: [], | |
| 14 | truncated: false, | |
| 15 | }; | |
| 16 | ||
| 17 | export 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) { |
| 19 | 19 | staleTime: Infinity, |
| 20 | 20 | }); |
| 21 | 21 | } |
| 22 | ||
| 23 | export 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 @@ |
| 53 | 53 | "dependencies": { |
| 54 | 54 | "@base-ui/react": "^1.2.0", |
| 55 | 55 | "@t3tools/contracts": "workspace:*", |
| 56 | "@tanstack/react-pacer": "^0.19.4", | |
| 56 | 57 | "@tanstack/react-query": "^5.90.0", |
| 57 | 58 | "@xterm/addon-fit": "^0.11.0", |
| 58 | 59 | "@xterm/xterm": "^6.0.0", |
| ... | ... | @@ -425,10 +426,20 @@ |
| 425 | 426 | |
| 426 | 427 | "@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=="], |
| 427 | 428 | |
| 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 | ||
| 428 | 433 | "@tanstack/query-core": ["@tanstack/query-core@5.90.20", "", {}, "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg=="], |
| 429 | 434 | |
| 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 | ||
| 430 | 437 | "@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=="], |
| 431 | 438 | |
| 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 | ||
| 432 | 443 | "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], |
| 433 | 444 | |
| 434 | 445 | "@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=="], |