From f16da5b6ba746ae401793cbcfb0af96055e979f5 Mon Sep 17 00:00:00 2001 From: ori Date: Mon, 22 Jun 2026 18:51:53 +0300 Subject: [PATCH 1/2] fix: pass through keys when child sessions have pending permissions The permission pass-through check only looked at the current session's permissions/questions via api.state.session.permission(sid). When a subagent (child session) requested permission, it was stored under the child's session ID, so vimcode kept consuming Enter/Escape instead of letting them reach the permission dialog. Now uses api.event to track permission.asked/permission.replied events globally, aggregated by root session ID. The intercept checks both the direct session state and the event-tracked child prompt counter. Fixes #47 --- CHANGELOG.md | 4 ++++ src/index.ts | 57 +++++++++++++++++++++++++++++++++++++++------------- 2 files changed, 47 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a32c811..c58bc26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Version ## [Unreleased] +### Fixed + +- Enter/Escape now work on permission dialogs from subagent (child) sessions ([#47](https://github.com/oribarilan/vimcode/issues/47)). Previously, the pass-through check only looked at the current session's permissions, missing prompts from tasks running inside it. + ## [0.15.0] — 2026-06-16 ### Fixed diff --git a/src/index.ts b/src/index.ts index 3171c51..1049661 100644 --- a/src/index.ts +++ b/src/index.ts @@ -43,6 +43,39 @@ const plugin: TuiPluginModule = { let leaderPending = false; let leaderTimer: ReturnType | null = null; + // Track pending permissions/questions from child sessions via events. + // The plugin API's permission()/question() only covers the exact session + // ID, but subagent permissions live on child session IDs. Events fire + // globally, so we aggregate by root session. + const pendingChildPrompts = new Map(); + + function trackPromptEvent(event: any, delta: number) { + const sessionID = event?.properties?.sessionID ?? event?.sessionID; + if (!sessionID) return; + const session = api.state?.session?.get?.(sessionID); + const rootId = session?.parentID ?? sessionID; + const count = (pendingChildPrompts.get(rootId) ?? 0) + delta; + if (count <= 0) pendingChildPrompts.delete(rootId); + else pendingChildPrompts.set(rootId, count); + } + + const unsubPermsAsked = api.event?.on?.("permission.asked", (e: any) => trackPromptEvent(e, 1)); + const unsubPermsReplied = api.event?.on?.("permission.replied", (e: any) => trackPromptEvent(e, -1)); + const unsubQuestAsked = api.event?.on?.("question.asked", (e: any) => trackPromptEvent(e, 1)); + const unsubQuestReplied = api.event?.on?.("question.replied", (e: any) => trackPromptEvent(e, -1)); + api.lifecycle?.onDispose?.(() => { unsubPermsAsked?.(); unsubPermsReplied?.(); }); + api.lifecycle?.onDispose?.(() => { unsubQuestAsked?.(); unsubQuestReplied?.(); }); + + function hasActivePrompts(sid: string): boolean { + const q = api.state.session.question(sid); + if (q && q.length > 0) return true; + const p = api.state.session.permission(sid); + if (p && p.length > 0) return true; + // Check child sessions tracked via events + if ((pendingChildPrompts.get(sid) ?? 0) > 0) return true; + return false; + } + // Snapshots for single-step undo of vim changes. // The host editor's undo system splits repeated commands into multiple // entries, so we save/restore the buffer ourselves. @@ -255,21 +288,17 @@ const plugin: TuiPluginModule = { const route = api.route.current; if (route.name === "session") { const sid = route.params?.sessionID; - if (sid) { - const q = api.state.session.question(sid); - const p = api.state.session.permission(sid); - if ((q && q.length > 0) || (p && p.length > 0)) { - // Consume the leader key so dispatchLayers() doesn't - // match it as a leader token, which would enter pending- - // sequence state instead of typing a space. - const matched = findMatchingLeader(ctx.event, leaderKeys); - if (matched) { - ctx.consume(); - const ch = leaderChar(matched); - if (ch) api.renderer?.currentFocusedEditor?.insertText?.(ch); - } - return; + if (sid && hasActivePrompts(sid)) { + // Consume the leader key so dispatchLayers() doesn't + // match it as a leader token, which would enter pending- + // sequence state instead of typing a space. + const matched = findMatchingLeader(ctx.event, leaderKeys); + if (matched) { + ctx.consume(); + const ch = leaderChar(matched); + if (ch) api.renderer?.currentFocusedEditor?.insertText?.(ch); } + return; } } From 2877ec3107792d28a9aa3bf09bc7a9a61c21b048 Mon Sep 17 00:00:00 2001 From: ori Date: Mon, 22 Jun 2026 19:04:43 +0300 Subject: [PATCH 2/2] chore: lint fixes and tighten comments --- CHANGELOG.md | 2 +- src/index.ts | 22 ++++++++++++++-------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c58bc26..c73b4da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Version ### Fixed -- Enter/Escape now work on permission dialogs from subagent (child) sessions ([#47](https://github.com/oribarilan/vimcode/issues/47)). Previously, the pass-through check only looked at the current session's permissions, missing prompts from tasks running inside it. +- Enter/Escape now work on subagent permission dialogs instead of being consumed by vimcome ([#47](https://github.com/oribarilan/vimcode/issues/47)). ## [0.15.0] — 2026-06-16 diff --git a/src/index.ts b/src/index.ts index 1049661..6238b91 100644 --- a/src/index.ts +++ b/src/index.ts @@ -44,11 +44,11 @@ const plugin: TuiPluginModule = { let leaderTimer: ReturnType | null = null; // Track pending permissions/questions from child sessions via events. - // The plugin API's permission()/question() only covers the exact session - // ID, but subagent permissions live on child session IDs. Events fire - // globally, so we aggregate by root session. + // permission()/question() only covers one session ID, but subagent + // prompts live on child IDs. Events fire globally; we aggregate by root. const pendingChildPrompts = new Map(); + // biome-ignore lint/suspicious/noExplicitAny: event shape is untyped in the plugin API function trackPromptEvent(event: any, delta: number) { const sessionID = event?.properties?.sessionID ?? event?.sessionID; if (!sessionID) return; @@ -59,21 +59,27 @@ const plugin: TuiPluginModule = { else pendingChildPrompts.set(rootId, count); } + // biome-ignore lint/suspicious/noExplicitAny: event shape is untyped in the plugin API const unsubPermsAsked = api.event?.on?.("permission.asked", (e: any) => trackPromptEvent(e, 1)); + // biome-ignore lint/suspicious/noExplicitAny: event shape is untyped in the plugin API const unsubPermsReplied = api.event?.on?.("permission.replied", (e: any) => trackPromptEvent(e, -1)); + // biome-ignore lint/suspicious/noExplicitAny: event shape is untyped in the plugin API const unsubQuestAsked = api.event?.on?.("question.asked", (e: any) => trackPromptEvent(e, 1)); + // biome-ignore lint/suspicious/noExplicitAny: event shape is untyped in the plugin API const unsubQuestReplied = api.event?.on?.("question.replied", (e: any) => trackPromptEvent(e, -1)); - api.lifecycle?.onDispose?.(() => { unsubPermsAsked?.(); unsubPermsReplied?.(); }); - api.lifecycle?.onDispose?.(() => { unsubQuestAsked?.(); unsubQuestReplied?.(); }); + api.lifecycle?.onDispose?.(() => { + unsubPermsAsked?.(); + unsubPermsReplied?.(); + unsubQuestAsked?.(); + unsubQuestReplied?.(); + }); function hasActivePrompts(sid: string): boolean { const q = api.state.session.question(sid); if (q && q.length > 0) return true; const p = api.state.session.permission(sid); if (p && p.length > 0) return true; - // Check child sessions tracked via events - if ((pendingChildPrompts.get(sid) ?? 0) > 0) return true; - return false; + return (pendingChildPrompts.get(sid) ?? 0) > 0; } // Snapshots for single-step undo of vim changes.