History

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

Julius Marminge committed 9 months ago

Commit e94a47a1 parent ea140aa2 files · +28 −26Browse files

Unified · Split

2 files
  1. apps/web/src/components/ChatView.tsx +8 −26
  2. apps/web/src/lib/serverReactQuery.ts +20 −0
apps/web/src/components/ChatView.tsx+8−26
@@ -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";
11import {12import {
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";
30import { serverConfigQueryOptions } from "~/lib/serverReactQuery";
29 31
30import { isElectron } from "../env";32import { isElectron } from "../env";
31import { buildBootstrapInput } from "../historyBootstrap";33import { 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";
60import ChatMarkdown from "./ChatMarkdown";61import ChatMarkdown from "./ChatMarkdown";
@@ -89,6 +90,7 @@ const MAX_VISIBLE_WORK_LOG_ENTRIES = 6; · 28 unmodified lines
89const IMAGE_SIZE_LIMIT_LABEL = `${Math.round(PROVIDER_SEND_TURN_MAX_IMAGE_BYTES / (1024 * 1024))}MB`;90const IMAGE_SIZE_LIMIT_LABEL = `${Math.round(PROVIDER_SEND_TURN_MAX_IMAGE_BYTES / (1024 * 1024))}MB`;
90const IMAGE_ONLY_BOOTSTRAP_PROMPT =91const 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).]";
93const EMPTY_KEYBINDINGS: KeybindingsConfig = [];
92 94
93function workToneClass(tone: "thinking" | "tool" | "info" | "error"): string {95function 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;
apps/web/src/lib/serverReactQuery.ts+20−0
@@ -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}