- 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"; |
| @@ -54,7 +56,6 @@ import { · 22 unmodified lines | |||
| 54 | isTerminalNewShortcut, | 56 | isTerminalNewShortcut, |
| 55 | isTerminalSplitShortcut, | 57 | isTerminalSplitShortcut, |
| 56 | isTerminalToggleShortcut, | 58 | isTerminalToggleShortcut, |
| 57 | type ResolvedTerminalKeybindings, | ||
| 58 | shortcutLabelForCommand, | 59 | shortcutLabelForCommand, |
| 59 | } from "../keybindings"; | 60 | } from "../keybindings"; |
| 60 | import ChatMarkdown from "./ChatMarkdown"; | 61 | import ChatMarkdown from "./ChatMarkdown"; |
| @@ -89,6 +90,7 @@ const MAX_VISIBLE_WORK_LOG_ENTRIES = 6; · 28 unmodified lines | |||
| 89 | const IMAGE_SIZE_LIMIT_LABEL = `${Math.round(PROVIDER_SEND_TURN_MAX_IMAGE_BYTES / (1024 * 1024))}MB`; | 90 | const IMAGE_SIZE_LIMIT_LABEL = `${Math.round(PROVIDER_SEND_TURN_MAX_IMAGE_BYTES / (1024 * 1024))}MB`; |
| 90 | const IMAGE_ONLY_BOOTSTRAP_PROMPT = | 91 | const IMAGE_ONLY_BOOTSTRAP_PROMPT = |
| 91 | "[User attached one or more images without additional text. Respond using the conversation context and the attached image(s).]"; | 92 | "[User attached one or more images without additional text. Respond using the conversation context and the attached image(s).]"; |
| 93 | const EMPTY_KEYBINDINGS: KeybindingsConfig = []; | ||
| 92 | 94 | ||
| 93 | function workToneClass(tone: "thinking" | "tool" | "info" | "error"): string { | 95 | function workToneClass(tone: "thinking" | "tool" | "info" | "error"): string { |
| 94 | if (tone === "error") return "text-rose-300/50 dark:text-rose-300/50"; | 96 | if (tone === "error") return "text-rose-300/50 dark:text-rose-300/50"; |
| @@ -150,7 +152,6 @@ export default function ChatView() { · 55 unmodified lines | |||
| 150 | const [expandedWorkGroups, setExpandedWorkGroups] = useState<Record<string, boolean>>({}); | 152 | const [expandedWorkGroups, setExpandedWorkGroups] = useState<Record<string, boolean>>({}); |
| 151 | const [nowTick, setNowTick] = useState(() => Date.now()); | 153 | const [nowTick, setNowTick] = useState(() => Date.now()); |
| 152 | const [terminalFocusRequestId, setTerminalFocusRequestId] = useState(0); | 154 | const [terminalFocusRequestId, setTerminalFocusRequestId] = useState(0); |
| 153 | const [terminalKeybindings, setTerminalKeybindings] = useState<ResolvedTerminalKeybindings>([]); | ||
| 154 | const messagesScrollRef = useRef<HTMLDivElement>(null); | 155 | const messagesScrollRef = useRef<HTMLDivElement>(null); |
| 155 | const messagesEndRef = useRef<HTMLDivElement>(null); | 156 | const messagesEndRef = useRef<HTMLDivElement>(null); |
| 156 | const textareaRef = useRef<HTMLTextAreaElement>(null); | 157 | const textareaRef = useRef<HTMLTextAreaElement>(null); |
| @@ -260,6 +261,11 @@ export default function ChatView() { · 103 unmodified lines | |||
| 260 | } as const); | 261 | } as const); |
| 261 | const gitCwd = activeThread?.worktreePath ?? activeProject?.cwd ?? null; | 262 | const gitCwd = activeThread?.worktreePath ?? activeProject?.cwd ?? null; |
| 262 | const branchesQuery = useQuery(gitBranchesQueryOptions(api, gitCwd)); | 263 | const branchesQuery = useQuery(gitBranchesQueryOptions(api, gitCwd)); |
| 264 | const terminalKeybindingsQuery = useQuery({ | ||
| 265 | ...serverConfigQueryOptions(api), | ||
| 266 | select: (config) => config.keybindings, | ||
| 267 | }); | ||
| 268 | const terminalKeybindings = terminalKeybindingsQuery.data ?? EMPTY_KEYBINDINGS; | ||
| 263 | // Default true while loading to avoid toolbar flicker. | 269 | // Default true while loading to avoid toolbar flicker. |
| 264 | const isGitRepo = branchesQuery.data?.isRepo ?? true; | 270 | const isGitRepo = branchesQuery.data?.isRepo ?? true; |
| 265 | const splitTerminalShortcutLabel = useMemo( | 271 | const splitTerminalShortcutLabel = useMemo( |
| @@ -277,30 +283,6 @@ export default function ChatView() { · 11 unmodified lines | |||
| 277 | (activeThread.session !== null && activeThread.session.status !== "closed")), | 283 | (activeThread.session !== null && activeThread.session.status !== "closed")), |
| 278 | ); | 284 | ); |
| 279 | 285 | ||
| 280 | useEffect(() => { | ||
| 281 | if (!api || !api.server || typeof api.server.getConfig !== "function") { | ||
| 282 | setTerminalKeybindings([]); | ||
| 283 | return; | ||
| 284 | } | ||
| 285 | |||
| 286 | let disposed = false; | ||
| 287 | |||
| 288 | void api.server | ||
| 289 | .getConfig() | ||
| 290 | .then((config) => { | ||
| 291 | if (disposed) return; | ||
| 292 | setTerminalKeybindings(config.keybindings); | ||
| 293 | }) | ||
| 294 | .catch(() => { | ||
| 295 | if (disposed) return; | ||
| 296 | setTerminalKeybindings([]); | ||
| 297 | }); | ||
| 298 | |||
| 299 | return () => { | ||
| 300 | disposed = true; | ||
| 301 | }; | ||
| 302 | }, [api]); | ||
| 303 | |||
| 304 | const revokePreviewUrls = useCallback((images: Array<{ previewUrl?: string }>) => { | 286 | const revokePreviewUrls = useCallback((images: Array<{ previewUrl?: string }>) => { |
| 305 | for (const image of images) { | 287 | for (const image of images) { |
| 306 | if (!image.previewUrl) continue; | 288 | 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 | } | ||