Codex command cards are read-only; see Codex
sessions for their approval controls.
+
+ Answer permission and plan cards from the keyboard, like the CLIs:
+ y accepts (or approves the plan), n denies (or
+ keeps planning), and a always allows for the rest of the
+ session. A pending permission outranks typing: the keys work even
+ while the composer has focus, and are not typed into it. The card's
+ own fields (the command, the rule, plan notes) keep their keys, and a
+ card the guardrail flagged as risky never takes keys from the
+ composer. Always-allow on a shell command is a prefix rule such as
+ npm test:*, not all of Bash; edit it, or allow the whole
+ tool, under Always-allow options.
+
PR state is live: checks show as badges on the thread card, and merging
is a button, not a terminal command. Worktree merge and delete are
diff --git a/src/components/Composer.tsx b/src/components/Composer.tsx
index d6ce6d996..3251c2031 100644
--- a/src/components/Composer.tsx
+++ b/src/components/Composer.tsx
@@ -2074,6 +2074,7 @@ export const Composer = memo(function Composer({
disabled={disabled || sending}
readOnly={dictating}
data-vim-mode={vimEnabled ? vimMode : undefined}
+ data-composer-input=""
/>
void>>,
+ fromComposer = true,
+) {
+ useEffect(() => {
+ const onKey = (ev: KeyboardEvent) => {
+ if (ev.metaKey || ev.ctrlKey || ev.altKey || ev.isComposing) return;
+ const run = keys[ev.key as PermissionKey];
+ if (!run) return;
+ const t = ev.target as HTMLElement | null;
+ if (
+ t &&
+ !(fromComposer && t.hasAttribute?.("data-composer-input")) &&
+ (t.tagName === "INPUT" ||
+ t.tagName === "TEXTAREA" ||
+ t.tagName === "SELECT" ||
+ t.isContentEditable)
+ ) {
+ return;
+ }
+ ev.preventDefault();
+ ev.stopPropagation();
+ run();
+ };
+ window.addEventListener("keydown", onKey, true);
+ return () => window.removeEventListener("keydown", onKey, true);
+ });
+}
+
+/** Key hint on a card button: `y` Accept. */
+function KeyHint({ k }: { k: PermissionKey }) {
+ return (
+
+ {k}
+
+ );
+}
+
/**
* Plan approval (ExitPlanMode): the plan rendered as markdown in the prompt
* panel, approve or send the agent back to planning (with optional notes),
@@ -81,6 +131,9 @@ export function PlanPrompt({
notes || undefined,
);
};
+ usePermissionKeys(
+ sent ? {} : { y: () => answer("allow"), n: () => answer("deny") },
+ );
const save = async () => {
if (!onSave || saving) return;
setSaving(true);
@@ -128,17 +181,19 @@ export function PlanPrompt({
className={styles.permissionAllow}
disabled={sent}
onClick={() => answer("allow")}
+ aria-keyshortcuts="y"
>
- Approve plan
+ Approve plan
{onImplement ? (