- add `serverConfigQueryOptions` in new `serverReactQuery` utility - switch `ChatView` from manual `getConfig` effect/state to `useQuery` - default to empty keybindings while config is unavailable
Unified · Split
| @@ -7,6 +7,7 @@ import { · 6 unmodified lines | |||
| 7 | PROVIDER_SEND_TURN_MAX_IMAGE_BYTES, | 7 | PROVIDER_SEND_TURN_MAX_IMAGE_BYTES, |
| 8 | type ProviderApprovalDecision, | 8 | type ProviderApprovalDecision, |
| 9 | type ProviderSendTurnAttachmentInput, | 9 | type ProviderSendTurnAttachmentInput, |
| 10 | type KeybindingsConfig, | ||
| 10 | } from "@t3tools/contracts"; | 11 | } from "@t3tools/contracts"; |
| 11 | import { | 12 | import { |
| 12 | type ClipboardEvent, | 13 | type ClipboardEvent, |
| @@ -26,6 +27,7 @@ import { · 13 unmodified lines | |||
| 26 | gitBranchesQueryOptions, | 27 | gitBranchesQueryOptions, |
| 27 | gitCreateWorktreeMutationOptions, | 28 | gitCreateWorktreeMutationOptions, |
| 28 | } from "~/lib/gitReactQuery"; | 29 | } from "~/lib/gitReactQuery"; |
| 30 | import { serverConfigQueryOptions } from "~/lib/serverReactQuery"; | ||
| 29 | 31 | ||
| 30 | import { isElectron } from "../env"; | 32 | import { isElectron } from "../env"; |
| 31 | import { buildBootstrapInput } from "../historyBootstrap"; | 33 | import { buildBootstrapInput } from "../historyBootstrap"; |
| @@ -55,7 +57,6 @@ import { · 23 unmodified lines | |||
| 55 | isTerminalNewShortcut, | 57 | isTerminalNewShortcut, |
| 56 | isTerminalSplitShortcut, | 58 | isTerminalSplitShortcut, |
| 57 | isTerminalToggleShortcut, | 59 | isTerminalToggleShortcut, |
| 58 | type ResolvedTerminalKeybindings, | ||
| 59 | shortcutLabelForCommand, | 60 | shortcutLabelForCommand, |
| 60 | } from "../keybindings"; | 61 | } from "../keybindings"; |
| 61 | import ChatMarkdown from "./ChatMarkdown"; | 62 | import ChatMarkdown from "./ChatMarkdown"; |
| @@ -90,6 +91,7 @@ const MAX_VISIBLE_WORK_LOG_ENTRIES = 6; · 28 unmodified lines | |||
| 90 | const IMAGE_SIZE_LIMIT_LABEL = `${Math.round(PROVIDER_SEND_TURN_MAX_IMAGE_BYTES / (1024 * 1024))}MB`; | 91 | const IMAGE_SIZE_LIMIT_LABEL = `${Math.round(PROVIDER_SEND_TURN_MAX_IMAGE_BYTES / (1024 * 1024))}MB`; |
| 91 | const IMAGE_ONLY_BOOTSTRAP_PROMPT = | 92 | const IMAGE_ONLY_BOOTSTRAP_PROMPT = |
| 92 | "[User attached one or more images without additional text. Respond using the conversation context and the attached image(s).]"; | 93 | "[User attached one or more images without additional text. Respond using the conversation context and the attached image(s).]"; |
| 94 | const EMPTY_KEYBINDINGS: KeybindingsConfig = []; | ||
| 93 | 95 | ||
| 94 | function workToneClass(tone: "thinking" | "tool" | "info" | "error"): string { | 96 | function workToneClass(tone: "thinking" | "tool" | "info" | "error"): string { |
| 95 | if (tone === "error") return "text-rose-300/50 dark:text-rose-300/50"; | 97 | if (tone === "error") return "text-rose-300/50 dark:text-rose-300/50"; |
| @@ -151,7 +153,6 @@ export default function ChatView() { · 55 unmodified lines | |||
| 151 | const [expandedWorkGroups, setExpandedWorkGroups] = useState<Record<string, boolean>>({}); | 153 | const [expandedWorkGroups, setExpandedWorkGroups] = useState<Record<string, boolean>>({}); |
| 152 | const [nowTick, setNowTick] = useState(() => Date.now()); | 154 | const [nowTick, setNowTick] = useState(() => Date.now()); |
| 153 | const [terminalFocusRequestId, setTerminalFocusRequestId] = useState(0); | 155 | const [terminalFocusRequestId, setTerminalFocusRequestId] = useState(0); |
| 154 | const [terminalKeybindings, setTerminalKeybindings] = useState<ResolvedTerminalKeybindings>([]); | ||
| 155 | const messagesScrollRef = useRef<HTMLDivElement>(null); | 156 | const messagesScrollRef = useRef<HTMLDivElement>(null); |
| 156 | const messagesEndRef = useRef<HTMLDivElement>(null); | 157 | const messagesEndRef = useRef<HTMLDivElement>(null); |
| 157 | const shouldAutoScrollRef = useRef(true); | 158 | const shouldAutoScrollRef = useRef(true); |
| @@ -262,6 +263,11 @@ export default function ChatView() { · 104 unmodified lines | |||
| 262 | } as const); | 263 | } as const); |
| 263 | const gitCwd = activeThread?.worktreePath ?? activeProject?.cwd ?? null; | 264 | const gitCwd = activeThread?.worktreePath ?? activeProject?.cwd ?? null; |
| 264 | const branchesQuery = useQuery(gitBranchesQueryOptions(api, gitCwd)); | 265 | const branchesQuery = useQuery(gitBranchesQueryOptions(api, gitCwd)); |
| 266 | const terminalKeybindingsQuery = useQuery({ | ||
| 267 | ...serverConfigQueryOptions(api), | ||
| 268 | select: (config) => config.keybindings, | ||
| 269 | }); | ||
| 270 | const terminalKeybindings = terminalKeybindingsQuery.data ?? EMPTY_KEYBINDINGS; | ||
| 265 | // Default true while loading to avoid toolbar flicker. | 271 | // Default true while loading to avoid toolbar flicker. |
| 266 | const isGitRepo = branchesQuery.data?.isRepo ?? true; | 272 | const isGitRepo = branchesQuery.data?.isRepo ?? true; |
| 267 | const splitTerminalShortcutLabel = useMemo( | 273 | const splitTerminalShortcutLabel = useMemo( |
| @@ -279,30 +285,6 @@ export default function ChatView() { · 11 unmodified lines | |||
| 279 | (activeThread.session !== null && activeThread.session.status !== "closed")), | 285 | (activeThread.session !== null && activeThread.session.status !== "closed")), |
| 280 | ); | 286 | ); |
| 281 | 287 | ||
| 282 | useEffect(() => { | ||
| 283 | if (!api || !api.server || typeof api.server.getConfig !== "function") { | ||
| 284 | setTerminalKeybindings([]); | ||
| 285 | return; | ||
| 286 | } | ||
| 287 | |||
| 288 | let disposed = false; | ||
| 289 | |||
| 290 | void api.server | ||
| 291 | .getConfig() | ||
| 292 | .then((config) => { | ||
| 293 | if (disposed) return; | ||
| 294 | setTerminalKeybindings(config.keybindings); | ||
| 295 | }) | ||
| 296 | .catch(() => { | ||
| 297 | if (disposed) return; | ||
| 298 | setTerminalKeybindings([]); | ||
| 299 | }); | ||
| 300 | |||
| 301 | return () => { | ||
| 302 | disposed = true; | ||
| 303 | }; | ||
| 304 | }, [api]); | ||
| 305 | |||
| 306 | const revokePreviewUrls = useCallback((images: Array<{ previewUrl?: string }>) => { | 288 | const revokePreviewUrls = useCallback((images: Array<{ previewUrl?: string }>) => { |
| 307 | for (const image of images) { | 289 | for (const image of images) { |
| 308 | if (!image.previewUrl) continue; | 290 | if (!image.previewUrl) continue; |
| @@ -0,0 +1,20 @@ | |||
| 1 | import type { NativeApi } from "@t3tools/contracts"; | ||
| 2 | import { queryOptions } from "@tanstack/react-query"; | ||
| 3 | |||
| 4 | export const serverQueryKeys = { | ||
| 5 | all: ["server"] as const, | ||
| 6 | config: () => ["server", "config"] as const, | ||
| 7 | }; | ||
| 8 | |||
| 9 | export function serverConfigQueryOptions(api: NativeApi | undefined) { | ||
| 10 | return queryOptions({ | ||
| 11 | queryKey: serverQueryKeys.config(), | ||
| 12 | queryFn: async () => { | ||
| 13 | if (!api || !api.server || typeof api.server.getConfig !== "function") { | ||
| 14 | throw new Error("Server config is unavailable."); | ||
| 15 | } | ||
| 16 | return api.server.getConfig(); | ||
| 17 | }, | ||
| 18 | enabled: !!api && !!api.server && typeof api.server.getConfig === "function", | ||
| 19 | }); | ||
| 20 | } | ||