authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-15 11:40:39-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-02-15 15:51:05-08:00
log61e9ba4b19fc4ed58bb59e23a2aaa995ee3197bd
tree5939139dfa8a51b4793826bf9cc06ed95369d9d3
parentad6247577e9f8cf23e97caac862d0f130ac92547

Load terminal keybindings via React Query server config helper

- add `serverConfigQueryOptions` in new `serverReactQuery` utility - switch `ChatView` from manual `getConfig` effect/state to `useQuery` - default to empty keybindings while config is unavailable

2 files changed, 28 insertions(+), 26 deletions(-)

apps/web/src/components/ChatView.tsx+8-26
...@@ -7,6 +7,7 @@ import {...@@ -7,6 +7,7 @@ import {
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";
11import {12import {
12 type ClipboardEvent,13 type ClipboardEvent,
...@@ -26,6 +27,7 @@ import {...@@ -26,6 +27,7 @@ import {
26 gitBranchesQueryOptions,27 gitBranchesQueryOptions,
27 gitCreateWorktreeMutationOptions,28 gitCreateWorktreeMutationOptions,
28} from "~/lib/gitReactQuery";29} from "~/lib/gitReactQuery";
30import { serverConfigQueryOptions } from "~/lib/serverReactQuery";
2931
30import { isElectron } from "../env";32import { isElectron } from "../env";
31import { buildBootstrapInput } from "../historyBootstrap";33import { buildBootstrapInput } from "../historyBootstrap";
...@@ -55,7 +57,6 @@ import {...@@ -55,7 +57,6 @@ import {
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";
61import ChatMarkdown from "./ChatMarkdown";62import ChatMarkdown from "./ChatMarkdown";
...@@ -90,6 +91,7 @@ const MAX_VISIBLE_WORK_LOG_ENTRIES = 6;...@@ -90,6 +91,7 @@ const MAX_VISIBLE_WORK_LOG_ENTRIES = 6;
90const IMAGE_SIZE_LIMIT_LABEL = `${Math.round(PROVIDER_SEND_TURN_MAX_IMAGE_BYTES / (1024 * 1024))}MB`;91const IMAGE_SIZE_LIMIT_LABEL = `${Math.round(PROVIDER_SEND_TURN_MAX_IMAGE_BYTES / (1024 * 1024))}MB`;
91const IMAGE_ONLY_BOOTSTRAP_PROMPT =92const 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).]";
94const EMPTY_KEYBINDINGS: KeybindingsConfig = [];
9395
94function workToneClass(tone: "thinking" | "tool" | "info" | "error"): string {96function 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() {...@@ -151,7 +153,6 @@ export default function ChatView() {
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() {...@@ -262,6 +263,11 @@ export default function ChatView() {
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() {...@@ -279,30 +285,6 @@ export default function ChatView() {
279 (activeThread.session !== null && activeThread.session.status !== "closed")),285 (activeThread.session !== null && activeThread.session.status !== "closed")),
280 );286 );
281287
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;
apps/web/src/lib/serverReactQuery.ts created+20
...@@ -0,0 +1,20 @@
1import type { NativeApi } from "@t3tools/contracts";
2import { queryOptions } from "@tanstack/react-query";
3
4export const serverQueryKeys = {
5 all: ["server"] as const,
6 config: () => ["server", "config"] as const,
7};
8
9export 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}