authorgravatar for maria@kuuro.netmaria <maria@kuuro.net> 2026-10-14 02:30:30+00:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-14 02:30:30+00:00
log573cec01664d8bfef9da53dd31d68398e5814170
tree1a93861ae09f440a71f4194ad868da7bd22c741e
parentdff750b1b8818eb004b8c3d060edcdc09f88a07f
signature Signed by PGP key B5690EEEBB952194

fix(web): app shortcuts keep working after clicking into an HTML render (#18898)


4 files changed, 219 insertions(+), 5 deletions(-)

apps/web/src/components/files/BrowserDocumentFrame.tsx+128-2
...@@ -1,18 +1,35 @@...@@ -1,18 +1,35 @@
1import type { ScopedThreadRef } from "@t3tools/contracts";1import { useAtomValue } from "@effect/atom-react";
2import {2import {
3 type KeybindingCommand,
4 type KeybindingShortcut,
5 MODEL_PICKER_JUMP_KEYBINDING_COMMANDS,
6 type ScopedThreadRef,
7 THREAD_JUMP_KEYBINDING_COMMANDS,
8} from "@t3tools/contracts";
9import {
10 type HtmlRenderShortcut,
11 htmlRenderShortcutsMessage,
3 htmlRenderThemeFragment,12 htmlRenderThemeFragment,
4 htmlRenderThemeMessage,13 htmlRenderThemeMessage,
5 htmlRenderResult,14 htmlRenderResult,
6 readHtmlRenderContentHeight,15 readHtmlRenderContentHeight,
16 readHtmlRenderKeyDown,
7 readHtmlRenderLinkRequest,17 readHtmlRenderLinkRequest,
8} from "@t3tools/shared/htmlRender";18} from "@t3tools/shared/htmlRender";
9import { useEffect, useEffectEvent, useLayoutEffect, useRef, useState } from "react";19import {
20 isMacPlatform,
21 matchesKeybindingShortcut,
22 normalizeEventKey,
23} from "@t3tools/shared/keybindings";
24import { useEffect, useEffectEvent, useLayoutEffect, useMemo, useRef, useState } from "react";
1025
11import { useOpenLink } from "~/browser/useOpenLink";26import { useOpenLink } from "~/browser/useOpenLink";
12import { useHtmlRenderTheme } from "~/hooks/useHtmlRenderTheme";27import { useHtmlRenderTheme } from "~/hooks/useHtmlRenderTheme";
28import { shortcutConflictKey } from "~/keybindings";
13import { useOpenChangeRequestLink } from "~/lib/openPullRequestLink";29import { useOpenChangeRequestLink } from "~/lib/openPullRequestLink";
14import { readLocalApi } from "~/localApi";30import { readLocalApi } from "~/localApi";
15import { cn } from "~/lib/utils";31import { cn } from "~/lib/utils";
32import { primaryServerKeybindingsAtom, serverEnvironment } from "~/state/server";
1633
17/**34/**
18 * Chromium's viewer opens with its own toolbar, a thumbnail rail and a small35 * Chromium's viewer opens with its own toolbar, a thumbnail rail and a small
...@@ -22,6 +39,32 @@ import { cn } from "~/lib/utils";...@@ -22,6 +39,32 @@ import { cn } from "~/lib/utils";
22 */39 */
23const PDF_VIEWER_FRAGMENT = "#toolbar=0&view=FitH";40const PDF_VIEWER_FRAGMENT = "#toolbar=0&view=FitH";
2441
42/**
43 * Shortcuts that still work while a focused HTML render holds the keyboard.
44 * The page reports the keys, and a page script can report any key it likes,
45 * so only commands that change what the reader sees are here, never one that
46 * changes a thread.
47 */
48const HTML_RENDER_SHORTCUT_COMMANDS: ReadonlySet<KeybindingCommand> = new Set([
49 "commandPalette.toggle",
50 "filePicker.toggle",
51 "projectSearch.toggle",
52 "chat.find",
53 "sidebar.toggle",
54 "rightPanel.toggle",
55 "rightPanel.toggleMaximized",
56 "threadPanel.toggle",
57 "terminal.toggle",
58 "preview.toggle",
59 "navigation.back",
60 "navigation.forward",
61 "thread.previous",
62 "thread.next",
63 ...THREAD_JUMP_KEYBINDING_COMMANDS,
64 // Share Cmd/Ctrl+digit with thread jumps, and act only while the picker is open.
65 ...MODEL_PICKER_JUMP_KEYBINDING_COMMANDS,
66]);
67
25export const isPdfPreviewFile = (path: string): boolean =>68export const isPdfPreviewFile = (path: string): boolean =>
26 /\.pdf$/i.test(path.split(/[?#]/, 1)[0] ?? "");69 /\.pdf$/i.test(path.split(/[?#]/, 1)[0] ?? "");
2770
...@@ -90,6 +133,63 @@ export function HtmlRenderDocument(props: {...@@ -90,6 +133,63 @@ export function HtmlRenderDocument(props: {
90 frameRef.current?.contentWindow?.postMessage(htmlRenderThemeMessage(theme), "*");133 frameRef.current?.contentWindow?.postMessage(htmlRenderThemeMessage(theme), "*");
91 };134 };
92 useEffect(postTheme, [theme]);135 useEffect(postTheme, [theme]);
136 const keybindings = useAtomValue(primaryServerKeybindingsAtom);
137 // Chat also resolves project scripts from the thread's own environment.
138 const environmentKeybindings = useAtomValue(
139 serverEnvironment.configValueAtom(props.threadRef?.environmentId ?? null),
140 )?.keybindings;
141 // The app resolves a replayed key in its live context, so every allowed
142 // binding counts whatever its `when`, and a key that any binding gives to
143 // another command stays with the page. Only Cmd/Ctrl chords: a printable key
144 // would type into the composer instead.
145 const { shortcuts, blocked } = useMemo(() => {
146 const mac = isMacPlatform(navigator.platform);
147 const resolve = (shortcut: KeybindingShortcut): HtmlRenderShortcut => ({
148 key: shortcut.key,
149 metaKey: shortcut.metaKey || (shortcut.modKey && mac),
150 ctrlKey: shortcut.ctrlKey || (shortcut.modKey && !mac),
151 shiftKey: shortcut.shiftKey,
152 altKey: shortcut.altKey,
153 });
154 const blocked = [...keybindings, ...(environmentKeybindings ?? [])]
155 .filter((binding) => !HTML_RENDER_SHORTCUT_COMMANDS.has(binding.command))
156 .map((binding) => resolve(binding.shortcut));
157 const shortcuts = new Map<string, HtmlRenderShortcut>();
158 for (const { command, shortcut } of keybindings) {
159 if (
160 HTML_RENDER_SHORTCUT_COMMANDS.has(command) &&
161 (shortcut.metaKey || shortcut.ctrlKey || shortcut.modKey)
162 ) {
163 shortcuts.set(shortcutConflictKey(shortcut), resolve(shortcut));
164 }
165 }
166 return { shortcuts: [...shortcuts.values()], blocked };
167 }, [environmentKeybindings, keybindings]);
168 const postShortcuts = () => {
169 frameRef.current?.contentWindow?.postMessage(
170 htmlRenderShortcutsMessage(shortcuts, blocked),
171 "*",
172 );
173 };
174 useEffect(postShortcuts, [blocked, shortcuts]);
175 // The page reports the key and code, and a page script can report any pair,
176 // so a key that could match another command's binding is never replayed. A
177 // named key (Backspace, Enter) must be the shortcut's own, since some app
178 // handlers read `event.key` directly. A dead key matches by its code.
179 const isShortcut = useEffectEvent(
180 (key: NonNullable<ReturnType<typeof readHtmlRenderKeyDown>>) => {
181 const matches = (shortcut: HtmlRenderShortcut) =>
182 matchesKeybindingShortcut(key, { ...shortcut, modKey: false }, navigator.platform);
183 const allowed = shortcuts.filter(matches);
184 return (
185 allowed.length > 0 &&
186 !blocked.some(matches) &&
187 (key.key.length === 1 ||
188 key.key === "Dead" ||
189 allowed.some((shortcut) => shortcut.key === normalizeEventKey(key.key)))
190 );
191 },
192 );
93 const openChangeRequestLink = useOpenChangeRequestLink(props.threadRef);193 const openChangeRequestLink = useOpenChangeRequestLink(props.threadRef);
94 const openLink = useOpenLink(props.threadRef);194 const openLink = useOpenLink(props.threadRef);
95 // Same rules as a chat link: a pull request opens in the right panel, any195 // Same rules as a chat link: a pull request opens in the right panel, any
...@@ -140,6 +240,31 @@ export function HtmlRenderDocument(props: {...@@ -140,6 +240,31 @@ export function HtmlRenderDocument(props: {
140 window.addEventListener("message", openLink);240 window.addEventListener("message", openLink);
141 return () => window.removeEventListener("message", openLink);241 return () => window.removeEventListener("message", openLink);
142 }, []);242 }, []);
243 // A focused frame takes every key, so app shortcuts would stop until the
244 // reader clicked out. The page passes on the chords posted to it, which replay
245 // on the frame element and bubble to the app's window listeners. A report is
246 // gated like a link and must be one of those chords.
247 useEffect(() => {
248 const replayKey = (event: MessageEvent) => {
249 const frame = frameRef.current;
250 const key = readHtmlRenderKeyDown(event.data);
251 if (
252 key === undefined ||
253 frame === null ||
254 event.source !== frame.contentWindow ||
255 document.activeElement !== frame ||
256 navigator.userActivation?.isActive === false
257 ) {
258 return;
259 }
260 if (!isShortcut(key)) return;
261 frame.dispatchEvent(
262 new KeyboardEvent("keydown", { ...key, bubbles: true, cancelable: true }),
263 );
264 };
265 window.addEventListener("message", replayKey);
266 return () => window.removeEventListener("message", replayKey);
267 }, []);
143 const { onContentHeight } = props;268 const { onContentHeight } = props;
144 // A page posts its height once per change, so listen from the commit that269 // A page posts its height once per change, so listen from the commit that
145 // inserts the frame; a passive effect could run after a fast page's first post.270 // inserts the frame; a passive effect could run after a fast page's first post.
...@@ -166,6 +291,7 @@ export function HtmlRenderDocument(props: {...@@ -166,6 +291,7 @@ export function HtmlRenderDocument(props: {
166 setLoaded(true);291 setLoaded(true);
167 // Covers a theme change that landed while the page was loading.292 // Covers a theme change that landed while the page was loading.
168 postTheme();293 postTheme();
294 postShortcuts();
169 }}295 }}
170 // A frame whose color scheme differs from its document's paints an opaque296 // A frame whose color scheme differs from its document's paints an opaque
171 // canvas, so the blank document a frame starts with would flash white in297 // canvas, so the blank document a frame starts with would flash white in
packages/shared/src/htmlRender.test.ts+24
...@@ -9,6 +9,7 @@ import {...@@ -9,6 +9,7 @@ import {
9 injectHtmlRenderBootstrap,9 injectHtmlRenderBootstrap,
10 htmlRenderThemeMessage,10 htmlRenderThemeMessage,
11 readHtmlRenderContentHeight,11 readHtmlRenderContentHeight,
12 readHtmlRenderKeyDown,
12 readHtmlRenderLinkRequest,13 readHtmlRenderLinkRequest,
13 readHtmlRenderReference,14 readHtmlRenderReference,
14} from "./htmlRender.ts";15} from "./htmlRender.ts";
...@@ -106,6 +107,29 @@ describe("readHtmlRenderLinkRequest", () => {...@@ -106,6 +107,29 @@ describe("readHtmlRenderLinkRequest", () => {
106 });107 });
107});108});
108109
110describe("readHtmlRenderKeyDown", () => {
111 it("reads the key of a keydown notification and nothing else", () => {
112 const notification = (params: unknown) => ({
113 jsonrpc: "2.0",
114 method: "t3/notifications/keydown",
115 params,
116 });
117 expect(readHtmlRenderKeyDown(notification({ key: "k", code: "KeyK", metaKey: true }))).toEqual({
118 key: "k",
119 code: "KeyK",
120 metaKey: true,
121 ctrlKey: false,
122 shiftKey: false,
123 altKey: false,
124 repeat: false,
125 });
126 expect(readHtmlRenderKeyDown(notification({ key: 1, code: "KeyK" }))).toBeUndefined();
127 expect(
128 readHtmlRenderKeyDown({ jsonrpc: "2.0", method: "ui/open-link", params: { key: "k" } }),
129 ).toBeUndefined();
130 });
131});
132
109describe("readHtmlRenderContentHeight", () => {133describe("readHtmlRenderContentHeight", () => {
110 it("reads only the height of an MCP Apps size-changed notification", () => {134 it("reads only the height of an MCP Apps size-changed notification", () => {
111 const notification = (params: unknown) => ({135 const notification = (params: unknown) => ({
packages/shared/src/htmlRender.ts+66-2
...@@ -4,6 +4,7 @@ import {...@@ -4,6 +4,7 @@ import {
4 type ThemeAppearance,4 type ThemeAppearance,
5 type ThemeColors,5 type ThemeColors,
6} from "./themePalettes.ts";6} from "./themePalettes.ts";
7import { EVENT_CODE_SHORTCUT_KEYS } from "./keybindings.ts";
78
8/**9/**
9 * Agent-authored HTML pages ("HTML renders") are self-contained documents an10 * Agent-authored HTML pages ("HTML renders") are self-contained documents an
...@@ -274,6 +275,11 @@ export const HTML_RENDER_LAYOUT_GUIDE = [...@@ -274,6 +275,11 @@ export const HTML_RENDER_LAYOUT_GUIDE = [
274const HOST_CONTEXT_CHANGED_METHOD = "ui/notifications/host-context-changed";275const HOST_CONTEXT_CHANGED_METHOD = "ui/notifications/host-context-changed";
275const OPEN_LINK_METHOD = "ui/open-link";276const OPEN_LINK_METHOD = "ui/open-link";
276const SIZE_CHANGED_METHOD = "ui/notifications/size-changed";277const SIZE_CHANGED_METHOD = "ui/notifications/size-changed";
278// Not part of MCP Apps: a focused frame keeps its keys from the app, so the
279// client tells the page which chords are app shortcuts and the page passes
280// those on.
281const SHORTCUTS_METHOD = "t3/notifications/shortcuts";
282const KEY_DOWN_METHOD = "t3/notifications/keydown";
277283
278/** The content height in a framed render's `ui/notifications/size-changed` notification. */284/** The content height in a framed render's `ui/notifications/size-changed` notification. */
279export function readHtmlRenderContentHeight(data: unknown): number | undefined {285export function readHtmlRenderContentHeight(data: unknown): number | undefined {
...@@ -311,6 +317,60 @@ export function readHtmlRenderLinkRequest(data: unknown):...@@ -311,6 +317,60 @@ export function readHtmlRenderLinkRequest(data: unknown):
311 : undefined;317 : undefined;
312}318}
313319
320/** The key a framed render passed on in a `t3/notifications/keydown` notification. */
321export function readHtmlRenderKeyDown(data: unknown):
322 | {
323 readonly key: string;
324 readonly code: string;
325 readonly metaKey: boolean;
326 readonly ctrlKey: boolean;
327 readonly shiftKey: boolean;
328 readonly altKey: boolean;
329 readonly repeat: boolean;
330 }
331 | undefined {
332 if (typeof data !== "object" || data === null) return undefined;
333 const { jsonrpc, method, params } = data as Record<string, unknown>;
334 if (jsonrpc !== "2.0" || method !== KEY_DOWN_METHOD) return undefined;
335 if (typeof params !== "object" || params === null) return undefined;
336 const { key, code, metaKey, ctrlKey, shiftKey, altKey, repeat } = params as Record<
337 string,
338 unknown
339 >;
340 if (typeof key !== "string" || typeof code !== "string") return undefined;
341 return {
342 key,
343 code,
344 metaKey: metaKey === true,
345 ctrlKey: ctrlKey === true,
346 shiftKey: shiftKey === true,
347 altKey: altKey === true,
348 repeat: repeat === true,
349 };
350}
351
352/** A chord as the page sees it: `mod` already resolved to Meta or Control. */
353export interface HtmlRenderShortcut {
354 /** Lowercase key name, as in a keybinding: `k`, `[`, `enter`. */
355 readonly key: string;
356 readonly metaKey: boolean;
357 readonly ctrlKey: boolean;
358 readonly shiftKey: boolean;
359 readonly altKey: boolean;
360}
361
362/**
363 * The `t3/notifications/shortcuts` notification a client posts into a render:
364 * the chords it may pass on, and chords of other commands that the page keeps
365 * even when one also matches an allowed chord.
366 */
367export function htmlRenderShortcutsMessage(
368 shortcuts: ReadonlyArray<HtmlRenderShortcut>,
369 blocked: ReadonlyArray<HtmlRenderShortcut>,
370) {
371 return { jsonrpc: "2.0", method: SHORTCUTS_METHOD, params: { shortcuts, blocked } } as const;
372}
373
314/** The empty result a client sends back for a render's request. */374/** The empty result a client sends back for a render's request. */
315export function htmlRenderResult(id: string | number) {375export function htmlRenderResult(id: string | number) {
316 return { jsonrpc: "2.0", id, result: {} } as const;376 return { jsonrpc: "2.0", id, result: {} } as const;
...@@ -355,8 +415,12 @@ function rootRule(theme: HtmlRenderTheme): string {...@@ -355,8 +415,12 @@ function rootRule(theme: HtmlRenderTheme): string {
355// a framed page asks its client to open it, and a top-level page (mobile)415// a framed page asks its client to open it, and a top-level page (mobile)
356// opens it as a new window, which the client sends to the browser. A framed416// opens it as a new window, which the client sends to the browser. A framed
357// page also reports its content height, measured as the server measures it,417// page also reports its content height, measured as the server measures it,
358// so its client can fit the frame to the page.418// so its client can fit the frame to the page. A chord the client named as an
359const BOOTSTRAP_SCRIPT = `(function(){var s=document.getElementById("t3-theme"),n=0;if(!s)return;var b=${JSON.stringify(BASE_CSS)};function a(t){if(!t||typeof t!=="object"||!t.variables||typeof t.variables!=="object")return;var c=":root{color-scheme:"+(t.appearance==="light"?"light":"dark")+";";for(var k in t.variables){if(/^--[a-z0-9-]+$/.test(k))c+=k+":"+String(t.variables[k]).replace(/[;{}<>]/g,"")+";";}s.textContent=c+"}"+b;}try{var m=/[#&]${THEME_FRAGMENT_KEY}=([^&]*)/.exec(location.hash);if(m){a(JSON.parse(decodeURIComponent(m[1])));history.replaceState(history.state,"",location.pathname+location.search);}}catch(e){}window.addEventListener("message",function(e){var d=e.data,p=d&&d.params;if(d&&d.jsonrpc==="2.0"&&d.method===${JSON.stringify(HOST_CONTEXT_CHANGED_METHOD)}&&p&&p.styles)a({appearance:p.theme,variables:p.styles.variables});});document.addEventListener("click",function(e){var l=e.isTrusted?e.composedPath().find(function(t){return t&&t.matches&&t.matches("a[href]");}):null,u;if(!l)return;try{u=new URL(l.getAttribute("href"),document.baseURI);}catch(x){return;}if(!/^https?:$/.test(u.protocol)||u.href.split("#")[0]===location.href.split("#")[0])return;if(window.parent!==window){e.preventDefault();window.parent.postMessage({jsonrpc:"2.0",id:"t3-link-"+(++n),method:${JSON.stringify(OPEN_LINK_METHOD)},params:{url:u.href,metaKey:e.metaKey,ctrlKey:e.ctrlKey}},"*");}else{l.setAttribute("target","_blank");l.setAttribute("rel","noopener");}},true);if(window.parent!==window){var h,o,z=function(){var r=document.documentElement,v=Math.ceil(r.scrollHeight>r.clientHeight?r.scrollHeight:r.getBoundingClientRect().height);if(v===h)return;h=v;window.parent.postMessage({jsonrpc:"2.0",method:${JSON.stringify(SIZE_CHANGED_METHOD)},params:{height:v}},"*");};if(window.ResizeObserver){o=new ResizeObserver(z);o.observe(document.documentElement);}document.addEventListener("DOMContentLoaded",function(){if(o&&document.body)o.observe(document.body);z();});window.addEventListener("load",z);}})();`;419// app shortcut is passed on rather than left to the browser, unless a text
420// field has focus or the page handled it: the deciding listener is added while
421// the key dispatches, so it runs after every listener the page has. Keys
422// match as resolveEventKeys reads them, so the client accepts what it claims.
423const BOOTSTRAP_SCRIPT = `(function(){var s=document.getElementById("t3-theme"),n=0;if(!s)return;var b=${JSON.stringify(BASE_CSS)};function a(t){if(!t||typeof t!=="object"||!t.variables||typeof t.variables!=="object")return;var c=":root{color-scheme:"+(t.appearance==="light"?"light":"dark")+";";for(var k in t.variables){if(/^--[a-z0-9-]+$/.test(k))c+=k+":"+String(t.variables[k]).replace(/[;{}<>]/g,"")+";";}s.textContent=c+"}"+b;}try{var m=/[#&]${THEME_FRAGMENT_KEY}=([^&]*)/.exec(location.hash);if(m){a(JSON.parse(decodeURIComponent(m[1])));history.replaceState(history.state,"",location.pathname+location.search);}}catch(e){}window.addEventListener("message",function(e){var d=e.data,p=d&&d.params;if(d&&d.jsonrpc==="2.0"&&d.method===${JSON.stringify(HOST_CONTEXT_CHANGED_METHOD)}&&p&&p.styles)a({appearance:p.theme,variables:p.styles.variables});});document.addEventListener("click",function(e){var l=e.isTrusted?e.composedPath().find(function(t){return t&&t.matches&&t.matches("a[href]");}):null,u;if(!l)return;try{u=new URL(l.getAttribute("href"),document.baseURI);}catch(x){return;}if(!/^https?:$/.test(u.protocol)||u.href.split("#")[0]===location.href.split("#")[0])return;if(window.parent!==window){e.preventDefault();window.parent.postMessage({jsonrpc:"2.0",id:"t3-link-"+(++n),method:${JSON.stringify(OPEN_LINK_METHOD)},params:{url:u.href,metaKey:e.metaKey,ctrlKey:e.ctrlKey}},"*");}else{l.setAttribute("target","_blank");l.setAttribute("rel","noopener");}},true);if(window.parent!==window){var h,o,z=function(){var r=document.documentElement,v=Math.ceil(r.scrollHeight>r.clientHeight?r.scrollHeight:r.getBoundingClientRect().height);if(v===h)return;h=v;window.parent.postMessage({jsonrpc:"2.0",method:${JSON.stringify(SIZE_CHANGED_METHOD)},params:{height:v}},"*");};if(window.ResizeObserver){o=new ResizeObserver(z);o.observe(document.documentElement);}document.addEventListener("DOMContentLoaded",function(){if(o&&document.body)o.observe(document.body);z();});window.addEventListener("load",z);var K=[],B=[],C=${JSON.stringify(EVENT_CODE_SHORTCUT_KEYS)};window.addEventListener("message",function(e){var d=e.data,p=d&&d.params;if(e.source===window.parent&&d&&d.jsonrpc==="2.0"&&d.method===${JSON.stringify(SHORTCUTS_METHOD)}&&p&&Array.isArray(p.shortcuts)&&Array.isArray(p.blocked)){K=p.shortcuts;B=p.blocked;}});window.addEventListener("keydown",function(e){var t=e.composedPath()[0],c=e.code||"",l=e.key.toLowerCase(),x=/^[a-z]$/.test(l)?[l]:[l,/^Key[A-Z]$/.test(c)?c.slice(3).toLowerCase():"",C.hasOwnProperty(c)?C[c]:""],f,m=function(s){return s&&s.metaKey===e.metaKey&&s.ctrlKey===e.ctrlKey&&s.shiftKey===e.shiftKey&&s.altKey===e.altKey&&x.indexOf(s.key)>-1;};if(!e.isTrusted||e.isComposing||e.getModifierState("AltGraph")&&!/^[a-z0-9]$/i.test(e.key)||t&&(t.isContentEditable||/^(?:INPUT|TEXTAREA|SELECT)$/.test(t.tagName))||!K.some(m)||B.some(m))return;f=function(v){window.removeEventListener("keydown",f);if(v!==e||e.defaultPrevented)return;e.preventDefault();window.parent.postMessage({jsonrpc:"2.0",method:${JSON.stringify(KEY_DOWN_METHOD)},params:{key:e.key,code:e.code,metaKey:e.metaKey,ctrlKey:e.ctrlKey,shiftKey:e.shiftKey,altKey:e.altKey,repeat:e.repeat}},"*");};window.addEventListener("keydown",f);setTimeout(function(){window.removeEventListener("keydown",f);});},true);}})();`;
360424
361function bootstrapMarkup(markup: string): string {425function bootstrapMarkup(markup: string): string {
362 const dark = htmlRenderTheme(T3_CODE_DARK_THEME_COLORS, "dark");426 const dark = htmlRenderTheme(T3_CODE_DARK_THEME_COLORS, "dark");
packages/shared/src/keybindings.ts+1-1
...@@ -29,7 +29,7 @@ export interface ShortcutModifierStateLike {...@@ -29,7 +29,7 @@ export interface ShortcutModifierStateLike {
29 altKey: boolean;29 altKey: boolean;
30}30}
3131
32const EVENT_CODE_SHORTCUT_KEYS: Readonly<Record<string, string>> = {32export const EVENT_CODE_SHORTCUT_KEYS: Readonly<Record<string, string>> = {
33 Backquote: "`",33 Backquote: "`",
34 Backslash: "\\",34 Backslash: "\\",
35 BracketLeft: "[",35 BracketLeft: "[",