diff --git a/packages/agent/src/adapters/codex-app-server/codex-app-server-agent.ts b/packages/agent/src/adapters/codex-app-server/codex-app-server-agent.ts index 8b1ce61b89..f4a223d514 100644 --- a/packages/agent/src/adapters/codex-app-server/codex-app-server-agent.ts +++ b/packages/agent/src/adapters/codex-app-server/codex-app-server-agent.ts @@ -72,7 +72,11 @@ import { APP_SERVER_NOTIFICATIONS, APP_SERVER_REQUESTS, } from "./protocol"; -import { type CodexSandboxPolicy, SessionConfigState } from "./session-config"; +import { + type CodexSandboxPolicy, + type RawModel, + SessionConfigState, +} from "./session-config"; import { type CodexAppServerProcess, type CodexAppServerProcessOptions, @@ -590,7 +594,7 @@ export class CodexAppServerAgent extends BaseAcpAgent { private async loadModelConfig(): Promise { try { - const res = await this.rpc.request<{ data?: any[] }>( + const res = await this.rpc.request<{ data?: RawModel[] }>( APP_SERVER_METHODS.MODEL_LIST, {}, ); diff --git a/packages/agent/src/adapters/codex-app-server/session-config.ts b/packages/agent/src/adapters/codex-app-server/session-config.ts index 6070ca7e27..109410e877 100644 --- a/packages/agent/src/adapters/codex-app-server/session-config.ts +++ b/packages/agent/src/adapters/codex-app-server/session-config.ts @@ -209,7 +209,7 @@ export function buildConfigOptions(s: ConfigSelectors): SessionConfigOption[] { } /** A model entry from the app-server's `model/list` (loosely typed). */ -interface RawModel { +export interface RawModel { id?: string; model?: string; displayName?: string; diff --git a/packages/ui/src/features/command/hotkeysLayout.test.ts b/packages/ui/src/features/command/hotkeysLayout.test.ts new file mode 100644 index 0000000000..b6adf46274 --- /dev/null +++ b/packages/ui/src/features/command/hotkeysLayout.test.ts @@ -0,0 +1,37 @@ +import { renderHook } from "@testing-library/react"; +import { useHotkeys } from "react-hotkeys-hook"; +import { describe, expect, it, vi } from "vitest"; + +// Regression guard for patches/react-hotkeys-hook.patch. Single-letter shortcuts must match the +// layout-aware event.key, not the physical event.code. On a Dvorak layout the key labelled "c" +// sits at the physical QWERTY-"I" slot (event.code "KeyI"), which used to trigger the "mod+i" +// Inbox shortcut and hijack Cmd+C. +function press(init: KeyboardEventInit): void { + document.dispatchEvent( + new KeyboardEvent("keydown", { bubbles: true, ...init }), + ); +} + +describe("react-hotkeys-hook layout-aware matching", () => { + it("fires mod+i when the logical key is i", () => { + const onInbox = vi.fn(); + renderHook(() => + useHotkeys("mod+i", onInbox, { enableOnContentEditable: true }), + ); + + press({ key: "i", code: "KeyI", metaKey: true }); + + expect(onInbox).toHaveBeenCalledTimes(1); + }); + + it("does not fire mod+i on a Dvorak Cmd+C that lands on the physical KeyI slot", () => { + const onInbox = vi.fn(); + renderHook(() => + useHotkeys("mod+i", onInbox, { enableOnContentEditable: true }), + ); + + press({ key: "c", code: "KeyI", metaKey: true }); + + expect(onInbox).not.toHaveBeenCalled(); + }); +}); diff --git a/patches/react-hotkeys-hook.patch b/patches/react-hotkeys-hook.patch new file mode 100644 index 0000000000..d504db662f --- /dev/null +++ b/patches/react-hotkeys-hook.patch @@ -0,0 +1,38 @@ +--- a/dist/react-hotkeys-hook.esm.js ++++ b/dist/react-hotkeys-hook.esm.js +@@ -206,7 +206,12 @@ + metaKey = e.metaKey, + shiftKey = e.shiftKey, + altKey = e.altKey; +- var keyCode = mapKey(code); ++ var layoutKey = mapKey(pressedKeyUppercase); ++ // PostHog patch: prefer the layout-aware key (event.key). Fall back to the physical ++ // event.code only when a modifier such as macOS Option turns event.key into a ++ // non-alphanumeric glyph, so single-letter shortcuts match the printed letter on ++ // non-QWERTY layouts (Dvorak, AZERTY, ...) instead of the QWERTY key at that slot. ++ var keyCode = /^[a-z0-9]$/.test(layoutKey) ? layoutKey : mapKey(code); + var pressedKey = pressedKeyUppercase.toLowerCase(); + if (!(keys != null && keys.includes(keyCode)) && !(keys != null && keys.includes(pressedKey)) && !['ctrl', 'control', 'unknown', 'meta', 'alt', 'shift', 'os'].includes(keyCode)) { + return false; +--- a/dist/react-hotkeys-hook.cjs.development.js ++++ b/dist/react-hotkeys-hook.cjs.development.js +@@ -208,7 +208,12 @@ + metaKey = e.metaKey, + shiftKey = e.shiftKey, + altKey = e.altKey; +- var keyCode = mapKey(code); ++ var layoutKey = mapKey(pressedKeyUppercase); ++ // PostHog patch: prefer the layout-aware key (event.key). Fall back to the physical ++ // event.code only when a modifier such as macOS Option turns event.key into a ++ // non-alphanumeric glyph, so single-letter shortcuts match the printed letter on ++ // non-QWERTY layouts (Dvorak, AZERTY, ...) instead of the QWERTY key at that slot. ++ var keyCode = /^[a-z0-9]$/.test(layoutKey) ? layoutKey : mapKey(code); + var pressedKey = pressedKeyUppercase.toLowerCase(); + if (!(keys != null && keys.includes(keyCode)) && !(keys != null && keys.includes(pressedKey)) && !['ctrl', 'control', 'unknown', 'meta', 'alt', 'shift', 'os'].includes(keyCode)) { + return false; +--- a/dist/react-hotkeys-hook.cjs.production.min.js ++++ b/dist/react-hotkeys-hook.cjs.production.min.js +@@ -1,2 +1,2 @@ +-"use strict";var e=require("react"),t=require("react/jsx-runtime");function n(){return(n=Object.assign?Object.assign.bind():function(e){for(var t=1;t0?r:["*"]),c=u[0],a=u[1],l=e.useState([]),s=l[0],d=l[1],f=e.useCallback((function(e){a((function(t){return t.includes("*")?[e]:Array.from(new Set([].concat(t,[e])))}))}),[]),v=e.useCallback((function(e){a((function(t){return 0===t.filter((function(t){return t!==e})).length?["*"]:t.filter((function(t){return t!==e}))}))}),[]),y=e.useCallback((function(e){a((function(t){return t.includes(e)?0===t.filter((function(t){return t!==e})).length?["*"]:t.filter((function(t){return t!==e})):t.includes("*")?[e]:Array.from(new Set([].concat(t,[e])))}))}),[]),h=e.useCallback((function(e){d((function(t){return[].concat(t,[e])}))}),[]),b=e.useCallback((function(e){d((function(t){return t.filter((function(t){return!m(t,e)}))}))}),[]);return t.jsx(k.Provider,{value:{enabledScopes:c,hotkeys:s,enableScope:f,disableScope:v,toggleScope:y},children:t.jsx(p,{addHotkey:h,removeHotkey:b,children:i})})},exports.isHotkeyPressed=s,exports.useHotkeys=function(t,n,o,r){var a=e.useState(null),p=a[0],k=a[1],w=e.useRef(!1),L=o instanceof Array?r instanceof Array?void 0:r:o,C=l(t)?t.join(null==L?void 0:L.splitKey):t,S=o instanceof Array?o:r instanceof Array?r:void 0,E=e.useCallback(n,null!=S?S:[]),A=e.useRef(E);A.current=S?E:n;var x=function(t){var n=e.useRef(void 0);return m(n.current,t)||(n.current=t),n.current}(L),H=h().enabledScopes,O=e.useContext(y);return g((function(){if(!1!==(null==x?void 0:x.enabled)&&(t=null==x?void 0:x.scopes,0===(e=H).length&&t?(console.warn('A hotkey has the "scopes" option set, however no active scopes were found. If you want to use the global scopes feature, you need to wrap your app in a '),1):!t||e.some((function(e){return t.includes(e)}))||e.includes("*"))){var e,t,n=function(e,t){var n;if(void 0===t&&(t=!1),!v(e,["input","textarea","select"])||v(e,null==x?void 0:x.enableOnFormTags)){if(null!==p){var o=p.getRootNode();if((o instanceof Document||o instanceof ShadowRoot)&&o.activeElement!==p&&!p.contains(o.activeElement))return void b(e)}(null==(n=e.target)||!n.isContentEditable||null!=x&&x.enableOnContentEditable)&&u(C,null==x?void 0:x.splitKey).forEach((function(n){var o,r=c(n,null==x?void 0:x.combinationKey);if(function(e,t,n){void 0===n&&(n=!1);var o=t.alt,r=t.meta,u=t.mod,c=t.shift,a=t.ctrl,l=t.keys,d=e.key,f=e.ctrlKey,v=e.metaKey,y=e.shiftKey,p=e.altKey,m=i(e.code),k=d.toLowerCase();if(!(null!=l&&l.includes(m)||null!=l&&l.includes(k)||["ctrl","control","unknown","meta","alt","shift","os"].includes(m)))return!1;if(!n){if(o===!p&&"alt"!==k)return!1;if(c===!y&&"shift"!==k)return!1;if(u){if(!v&&!f)return!1}else{if(r===!v&&"meta"!==k&&"os"!==k)return!1;if(a===!f&&"ctrl"!==k&&"control"!==k)return!1}}return!(!l||1!==l.length||!l.includes(k)&&!l.includes(m))||(l?s(l):!l)}(e,r,null==x?void 0:x.ignoreModifiers)||null!=(o=r.keys)&&o.includes("*")){if(null!=x&&null!=x.ignoreEventWhen&&x.ignoreEventWhen(e))return;if(t&&w.current)return;if(function(e,t,n){("function"==typeof n&&n(e,t)||!0===n)&&e.preventDefault()}(e,r,null==x?void 0:x.preventDefault),!function(e,t,n){return"function"==typeof n?n(e,t):!0===n||void 0===n}(e,r,null==x?void 0:x.enabled))return void b(e);A.current(e,r),t||(w.current=!0)}}))}},o=function(e){void 0!==e.key&&(d(i(e.code)),(void 0===(null==x?void 0:x.keydown)&&!0!==(null==x?void 0:x.keyup)||null!=x&&x.keydown)&&n(e))},r=function(e){void 0!==e.key&&(f(i(e.code)),w.current=!1,null!=x&&x.keyup&&n(e,!0))},a=p||(null==L?void 0:L.document)||document;return a.addEventListener("keyup",r,null==L?void 0:L.eventListenerOptions),a.addEventListener("keydown",o,null==L?void 0:L.eventListenerOptions),O&&u(C,null==x?void 0:x.splitKey).forEach((function(e){return O.addHotkey(c(e,null==x?void 0:x.combinationKey,null==x?void 0:x.description))})),function(){a.removeEventListener("keyup",r,null==L?void 0:L.eventListenerOptions),a.removeEventListener("keydown",o,null==L?void 0:L.eventListenerOptions),O&&u(C,null==x?void 0:x.splitKey).forEach((function(e){return O.removeHotkey(c(e,null==x?void 0:x.combinationKey,null==x?void 0:x.description))}))}}}),[p,C,x,H]),k},exports.useHotkeysContext=h,exports.useRecordHotkeys=function(){var t=e.useState(new Set),n=t[0],o=t[1],r=e.useState(!1),u=r[0],c=r[1],a=e.useCallback((function(e){void 0!==e.key&&(e.preventDefault(),e.stopPropagation(),o((function(t){var n=new Set(t);return n.add(i(e.code)),n})))}),[]),l=e.useCallback((function(){"undefined"!=typeof document&&(document.removeEventListener("keydown",a),c(!1))}),[a]),s=e.useCallback((function(){o(new Set),"undefined"!=typeof document&&(l(),document.addEventListener("keydown",a),c(!0))}),[a,l]),d=e.useCallback((function(){o(new Set)}),[]);return[n,{start:s,stop:l,resetKeys:d,isRecording:u}]}; ++"use strict";var e=require("react"),t=require("react/jsx-runtime");function n(){return(n=Object.assign?Object.assign.bind():function(e){for(var t=1;t0?r:["*"]),c=u[0],a=u[1],l=e.useState([]),s=l[0],d=l[1],f=e.useCallback((function(e){a((function(t){return t.includes("*")?[e]:Array.from(new Set([].concat(t,[e])))}))}),[]),v=e.useCallback((function(e){a((function(t){return 0===t.filter((function(t){return t!==e})).length?["*"]:t.filter((function(t){return t!==e}))}))}),[]),y=e.useCallback((function(e){a((function(t){return t.includes(e)?0===t.filter((function(t){return t!==e})).length?["*"]:t.filter((function(t){return t!==e})):t.includes("*")?[e]:Array.from(new Set([].concat(t,[e])))}))}),[]),h=e.useCallback((function(e){d((function(t){return[].concat(t,[e])}))}),[]),b=e.useCallback((function(e){d((function(t){return t.filter((function(t){return!m(t,e)}))}))}),[]);return t.jsx(k.Provider,{value:{enabledScopes:c,hotkeys:s,enableScope:f,disableScope:v,toggleScope:y},children:t.jsx(p,{addHotkey:h,removeHotkey:b,children:i})})},exports.isHotkeyPressed=s,exports.useHotkeys=function(t,n,o,r){var a=e.useState(null),p=a[0],k=a[1],w=e.useRef(!1),L=o instanceof Array?r instanceof Array?void 0:r:o,C=l(t)?t.join(null==L?void 0:L.splitKey):t,S=o instanceof Array?o:r instanceof Array?r:void 0,E=e.useCallback(n,null!=S?S:[]),A=e.useRef(E);A.current=S?E:n;var x=function(t){var n=e.useRef(void 0);return m(n.current,t)||(n.current=t),n.current}(L),H=h().enabledScopes,O=e.useContext(y);return g((function(){if(!1!==(null==x?void 0:x.enabled)&&(t=null==x?void 0:x.scopes,0===(e=H).length&&t?(console.warn('A hotkey has the "scopes" option set, however no active scopes were found. If you want to use the global scopes feature, you need to wrap your app in a '),1):!t||e.some((function(e){return t.includes(e)}))||e.includes("*"))){var e,t,n=function(e,t){var n;if(void 0===t&&(t=!1),!v(e,["input","textarea","select"])||v(e,null==x?void 0:x.enableOnFormTags)){if(null!==p){var o=p.getRootNode();if((o instanceof Document||o instanceof ShadowRoot)&&o.activeElement!==p&&!p.contains(o.activeElement))return void b(e)}(null==(n=e.target)||!n.isContentEditable||null!=x&&x.enableOnContentEditable)&&u(C,null==x?void 0:x.splitKey).forEach((function(n){var o,r=c(n,null==x?void 0:x.combinationKey);if(function(e,t,n){void 0===n&&(n=!1);var o=t.alt,r=t.meta,u=t.mod,c=t.shift,a=t.ctrl,l=t.keys,d=e.key,f=e.ctrlKey,v=e.metaKey,y=e.shiftKey,p=e.altKey,m=/^[a-z0-9]$/.test(i(d))?i(d):i(e.code),k=d.toLowerCase();if(!(null!=l&&l.includes(m)||null!=l&&l.includes(k)||["ctrl","control","unknown","meta","alt","shift","os"].includes(m)))return!1;if(!n){if(o===!p&&"alt"!==k)return!1;if(c===!y&&"shift"!==k)return!1;if(u){if(!v&&!f)return!1}else{if(r===!v&&"meta"!==k&&"os"!==k)return!1;if(a===!f&&"ctrl"!==k&&"control"!==k)return!1}}return!(!l||1!==l.length||!l.includes(k)&&!l.includes(m))||(l?s(l):!l)}(e,r,null==x?void 0:x.ignoreModifiers)||null!=(o=r.keys)&&o.includes("*")){if(null!=x&&null!=x.ignoreEventWhen&&x.ignoreEventWhen(e))return;if(t&&w.current)return;if(function(e,t,n){("function"==typeof n&&n(e,t)||!0===n)&&e.preventDefault()}(e,r,null==x?void 0:x.preventDefault),!function(e,t,n){return"function"==typeof n?n(e,t):!0===n||void 0===n}(e,r,null==x?void 0:x.enabled))return void b(e);A.current(e,r),t||(w.current=!0)}}))}},o=function(e){void 0!==e.key&&(d(i(e.code)),(void 0===(null==x?void 0:x.keydown)&&!0!==(null==x?void 0:x.keyup)||null!=x&&x.keydown)&&n(e))},r=function(e){void 0!==e.key&&(f(i(e.code)),w.current=!1,null!=x&&x.keyup&&n(e,!0))},a=p||(null==L?void 0:L.document)||document;return a.addEventListener("keyup",r,null==L?void 0:L.eventListenerOptions),a.addEventListener("keydown",o,null==L?void 0:L.eventListenerOptions),O&&u(C,null==x?void 0:x.splitKey).forEach((function(e){return O.addHotkey(c(e,null==x?void 0:x.combinationKey,null==x?void 0:x.description))})),function(){a.removeEventListener("keyup",r,null==L?void 0:L.eventListenerOptions),a.removeEventListener("keydown",o,null==L?void 0:L.eventListenerOptions),O&&u(C,null==x?void 0:x.splitKey).forEach((function(e){return O.removeHotkey(c(e,null==x?void 0:x.combinationKey,null==x?void 0:x.description))}))}}}),[p,C,x,H]),k},exports.useHotkeysContext=h,exports.useRecordHotkeys=function(){var t=e.useState(new Set),n=t[0],o=t[1],r=e.useState(!1),u=r[0],c=r[1],a=e.useCallback((function(e){void 0!==e.key&&(e.preventDefault(),e.stopPropagation(),o((function(t){var n=new Set(t);return n.add(i(e.code)),n})))}),[]),l=e.useCallback((function(){"undefined"!=typeof document&&(document.removeEventListener("keydown",a),c(!1))}),[a]),s=e.useCallback((function(){o(new Set),"undefined"!=typeof document&&(l(),document.addEventListener("keydown",a),c(!0))}),[a,l]),d=e.useCallback((function(){o(new Set)}),[]);return[n,{start:s,stop:l,resetKeys:d,isRecording:u}]}; + //# sourceMappingURL=react-hotkeys-hook.cjs.production.min.js.map diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 68e8fe7e82..bd98b6a2b3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -83,6 +83,9 @@ patchedDependencies: node-pty: hash: 4dfdf785f5ac51a03f5d6032371cebe89036381acd403621f250a896245647c5 path: patches/node-pty.patch + react-hotkeys-hook: + hash: a8cd00b963d4ae6787cc267a2345cc1b32bcba5fa1131328b8017ee0d316ab0e + path: patches/react-hotkeys-hook.patch importers: @@ -288,7 +291,7 @@ importers: version: 19.2.6(react@19.2.6) react-hotkeys-hook: specifier: ^4.4.4 - version: 4.6.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + version: 4.6.2(patch_hash=a8cd00b963d4ae6787cc267a2345cc1b32bcba5fa1131328b8017ee0d316ab0e)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) react-scan: specifier: ^0.5.6 version: 0.5.7(@opentelemetry/core@2.8.0(@opentelemetry/api@1.9.1))(esbuild@0.27.2)(eslint@10.5.0(jiti@2.7.0))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(rollup@4.57.1) @@ -1345,7 +1348,7 @@ importers: version: 0.2.3 react-hotkeys-hook: specifier: ^4.4.4 - version: 4.6.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + version: 4.6.2(patch_hash=a8cd00b963d4ae6787cc267a2345cc1b32bcba5fa1131328b8017ee0d316ab0e)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) react-markdown: specifier: ^10.1.0 version: 10.1.0(@types/react@19.2.17)(react@19.2.6) @@ -29268,7 +29271,7 @@ snapshots: optionalDependencies: react: 19.2.6 - react-hotkeys-hook@4.6.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6): + react-hotkeys-hook@4.6.2(patch_hash=a8cd00b963d4ae6787cc267a2345cc1b32bcba5fa1131328b8017ee0d316ab0e)(react-dom@19.2.6(react@19.2.6))(react@19.2.6): dependencies: react: 19.2.6 react-dom: 19.2.6(react@19.2.6) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 09e51fe577..a2c1afcf02 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -115,3 +115,4 @@ onlyBuiltDependencies: patchedDependencies: node-pty: patches/node-pty.patch + react-hotkeys-hook: patches/react-hotkeys-hook.patch