diff --git a/packages/app/src/assistant-file-links/parse.ts b/packages/app/src/assistant-file-links/parse.ts index bd0015f0b..092f3b4dc 100644 --- a/packages/app/src/assistant-file-links/parse.ts +++ b/packages/app/src/assistant-file-links/parse.ts @@ -140,6 +140,7 @@ function parseLineFragment(value: string): Pick { expect(splitHtmlishMarkdown(source)).toEqual([{ kind: "markdown", text: source }]); }); + it("terminates when an unmatched backtick follows a closed inline code span", () => { + const source = "Use `
ExampleBody
` before `dangling"; + + expect(splitHtmlishMarkdown(source)).toEqual([{ kind: "markdown", text: source }]); + }); + it("still parses normal details outside code", () => { expect(splitHtmlishMarkdown("`code`\n
RealBody
")).toEqual([ { kind: "markdown", text: "`code`\n" }, diff --git a/packages/app/src/components/markdown/html-ish.ts b/packages/app/src/components/markdown/html-ish.ts index b68e734b5..7a92f0d20 100644 --- a/packages/app/src/components/markdown/html-ish.ts +++ b/packages/app/src/components/markdown/html-ish.ts @@ -651,8 +651,13 @@ function getInlineCodeRanges( } const marker = open[0]; - const close = findClosingBacktickRun(source, BACKTICK_RUN_RE.lastIndex, marker, fencedRanges); + const afterOpen = BACKTICK_RUN_RE.lastIndex; + const close = findClosingBacktickRun(source, afterOpen, marker, fencedRanges); if (!close) { + // Unmatched backtick run — skip past it so the loop doesn't restart from 0. + // findClosingBacktickRun exhausts the global regex, which resets lastIndex + // to 0 when exec() returns null (ECMAScript §22.2.7.2). + BACKTICK_RUN_RE.lastIndex = afterOpen; continue; } diff --git a/packages/app/src/hooks/use-keyboard-shortcuts.ts b/packages/app/src/hooks/use-keyboard-shortcuts.ts index ae0671c25..eb58f3ba6 100644 --- a/packages/app/src/hooks/use-keyboard-shortcuts.ts +++ b/packages/app/src/hooks/use-keyboard-shortcuts.ts @@ -9,6 +9,7 @@ import { type ChordState, resolveKeyboardShortcut, buildEffectiveBindings, + getWorkspaceIndexJumpModifierKey, } from "@/keyboard/keyboard-shortcuts"; import { resolveKeyboardFocusScope } from "@/keyboard/focus-scope"; import { @@ -73,6 +74,20 @@ export function useKeyboardShortcuts({ const isDesktopApp = getIsElectronRuntime(); const isMac = getShortcutOs() === "mac"; + // Only the modifier that actually performs the workspace-index jump on this + // runtime should reveal the sidebar number badges (Alt on web, Cmd on + // desktop Mac, Ctrl on desktop non-Mac). The store ORs altDown/cmdOrCtrlDown + // to drive badge visibility, so we set the flag matching this runtime. + const badgeModifierKey = getWorkspaceIndexJumpModifierKey({ isMac, isDesktop: isDesktopApp }); + const setBadgeModifierDown = (down: boolean) => { + const state = useKeyboardShortcutsStore.getState(); + if (isDesktopApp) { + state.setCmdOrCtrlDown(down); + } else { + state.setAltDown(down); + } + }; + const shouldHandle = () => { if (typeof document === "undefined") return false; if (document.visibilityState !== "visible") return false; @@ -156,11 +171,8 @@ export function useKeyboardShortcuts({ } const key = event.key ?? ""; - if (key === "Alt" && !event.shiftKey) { - useKeyboardShortcutsStore.getState().setAltDown(true); - } - if (isDesktopApp && (key === "Meta" || key === "Control") && !event.shiftKey) { - useKeyboardShortcutsStore.getState().setCmdOrCtrlDown(true); + if (key === badgeModifierKey && !event.shiftKey) { + setBadgeModifierDown(true); } if (key === "Shift") { const state = useKeyboardShortcutsStore.getState(); @@ -231,11 +243,8 @@ export function useKeyboardShortcuts({ const handleKeyUp = (event: KeyboardEvent) => { const key = event.key ?? ""; - if (key === "Alt") { - useKeyboardShortcutsStore.getState().setAltDown(false); - } - if (isDesktopApp && (key === "Meta" || key === "Control")) { - useKeyboardShortcutsStore.getState().setCmdOrCtrlDown(false); + if (key === badgeModifierKey) { + setBadgeModifierDown(false); } }; diff --git a/packages/app/src/keyboard/keyboard-shortcuts.test.ts b/packages/app/src/keyboard/keyboard-shortcuts.test.ts index a797aea9b..bec812563 100644 --- a/packages/app/src/keyboard/keyboard-shortcuts.test.ts +++ b/packages/app/src/keyboard/keyboard-shortcuts.test.ts @@ -3,6 +3,7 @@ import { buildKeyboardShortcutHelpSections, buildEffectiveBindings, getBindingIdForAction, + getWorkspaceIndexJumpModifierKey, resolveKeyboardShortcut, type ChordState, type KeyboardShortcutContext, @@ -638,3 +639,18 @@ describe("keyboard-shortcut help sections", () => { ).toBeNull(); }); }); + +describe("getWorkspaceIndexJumpModifierKey", () => { + it("uses Alt on web, regardless of OS", () => { + expect(getWorkspaceIndexJumpModifierKey({ isMac: true, isDesktop: false })).toBe("Alt"); + expect(getWorkspaceIndexJumpModifierKey({ isMac: false, isDesktop: false })).toBe("Alt"); + }); + + it("uses Cmd (Meta) on desktop Mac, not Control or Alt", () => { + expect(getWorkspaceIndexJumpModifierKey({ isMac: true, isDesktop: true })).toBe("Meta"); + }); + + it("uses Ctrl on desktop non-Mac, not Meta or Alt", () => { + expect(getWorkspaceIndexJumpModifierKey({ isMac: false, isDesktop: true })).toBe("Control"); + }); +}); diff --git a/packages/app/src/keyboard/keyboard-shortcuts.ts b/packages/app/src/keyboard/keyboard-shortcuts.ts index d3fbbd088..40358977d 100644 --- a/packages/app/src/keyboard/keyboard-shortcuts.ts +++ b/packages/app/src/keyboard/keyboard-shortcuts.ts @@ -1344,6 +1344,21 @@ export function getDefaultKeysForAction( return null; } +/** + * The `KeyboardEvent.key` whose hold reveals the sidebar workspace-jump number + * badges. It must match the modifier of the active `workspace.navigate.index` + * binding for this runtime, otherwise the badges appear for a modifier that + * does not actually jump: Alt on web, Cmd (Meta) on desktop Mac, Ctrl on + * desktop non-Mac. + */ +export function getWorkspaceIndexJumpModifierKey(platform: { + isMac: boolean; + isDesktop: boolean; +}): "Alt" | "Meta" | "Control" { + if (!platform.isDesktop) return "Alt"; + return platform.isMac ? "Meta" : "Control"; +} + export function buildKeyboardShortcutHelpSections( input: KeyboardShortcutPlatformContext, bindings: readonly ParsedShortcutBinding[] = DEFAULT_BINDINGS,