fix tui approval hotkeys when question modal is open#39
fix tui approval hotkeys when question modal is open#39igun997 wants to merge 16 commits intomadebyaris:devfrom
Conversation
…adebyaris#32) Prevents stale modal-open state when resuming a session that was interrupted mid-question.
…s#32) Keep the question header and prompt visible in the transcript but skip the numbered options, tips, and click targets while the modal popup is handling selection.
- Remove stale session_id from InvokeSkillTool (was not updated on session reset, causing events tagged to wrong session) - Block path traversal (../) in skill file reference resolution - Prevent AGENTS.md skills from leaking parent directory files via Level-2 fallback (only filesystem skills use catalog root fallback) - Detect invoke_skill phase in subagent activity for sidebar indicator - Add path traversal rejection test
…ebyaris#34) When the same tool fails 3 times in a row (e.g., ask_question called with malformed inputs by a weaker model), the agent loop now stops and surfaces an error instead of retrying indefinitely.
There was a problem hiding this comment.
9 issues found across 22 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="docs/superpowers/plans/2026-03-24-skill-supporting-files.md">
<violation number="1" location="docs/superpowers/plans/2026-03-24-skill-supporting-files.md:585">
P2: Verification commands use developer-specific absolute paths, making the plan non-portable and brittle for other contributors/CI.</violation>
</file>
<file name="docs/superpowers/specs/2026-03-24-question-modal-picker-design.md">
<violation number="1" location="docs/superpowers/specs/2026-03-24-question-modal-picker-design.md:53">
P2: The new spec claims the question modal intercepts key events before any other handler, but current TUI logic explicitly prioritizes approval hotkeys over the question modal. This documentation contradicts the implemented behavior and the PR’s intent, risking future regressions.</violation>
</file>
<file name="docs/superpowers/specs/2026-03-24-invoke-skill-tool-design.md">
<violation number="1" location="docs/superpowers/specs/2026-03-24-invoke-skill-tool-design.md:66">
P2: Spec defines two different success response contracts (wrapped message vs raw `expanded_body()`), creating avoidable implementation/test drift.</violation>
</file>
<file name="crates/core/src/agent.rs">
<violation number="1" location="crates/core/src/agent.rs:520">
P2: The infinite-loop guard emits an error event but exits with `break msg`, so the function returns `Ok(final_text)` instead of an error. This makes a tool-loop abort look like a successful turn and skips the normal assistant message update, creating inconsistent result semantics.</violation>
</file>
<file name="crates/core/src/skills.rs">
<violation number="1" location="crates/core/src/skills.rs:475">
P2: AGENTS.md skills can still fall back to Level 2/3 resolution because `is_likely_skill_subdir` is true for normal workspace roots, enabling reads from `workspace_root.parent()` and escaping the workspace boundary.</violation>
<violation number="2" location="crates/core/src/skills.rs:542">
P2: extract_file_references claims to return references in first-occurrence order, but it scans in three separate passes (./, then @, then backticks), so mixed syntaxes will be reordered vs. their textual appearance.</violation>
</file>
<file name="crates/cli/src/main.rs">
<violation number="1" location="crates/cli/src/main.rs:1403">
P2: `git_head_commit` errors are silently converted to `None` and then persisted, which can clear a previously stored commit hash. That causes future `skills update` runs to treat the skill as a local install and skip updates permanently after a transient failure.</violation>
</file>
<file name="crates/core/src/skill_installer.rs">
<violation number="1" location="crates/core/src/skill_installer.rs:234">
P1: Recursive copy follows symlinks, allowing untrusted skills to copy files from outside the source tree. Add a symlink check to avoid traversing or copying symlink targets.</violation>
<violation number="2" location="crates/core/src/skill_installer.rs:331">
P2: No post-sanitization collision check: two different skill directories that sanitize to the same name will overwrite each other and the lock entry, leading to nondeterministic installs.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| let entry = entry.map_err(|e| e.to_string())?; | ||
| let src_path = entry.path(); | ||
| let dst_path = dst.join(entry.file_name()); | ||
| if src_path.is_dir() { |
There was a problem hiding this comment.
P1: Recursive copy follows symlinks, allowing untrusted skills to copy files from outside the source tree. Add a symlink check to avoid traversing or copying symlink targets.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At crates/core/src/skill_installer.rs, line 234:
<comment>Recursive copy follows symlinks, allowing untrusted skills to copy files from outside the source tree. Add a symlink check to avoid traversing or copying symlink targets.</comment>
<file context>
@@ -0,0 +1,588 @@
+ let entry = entry.map_err(|e| e.to_string())?;
+ let src_path = entry.path();
+ let dst_path = dst.join(entry.file_name());
+ if src_path.is_dir() {
+ copy_dir_recursive(&src_path, &dst_path)?;
+ } else {
</file context>
|
|
||
| ```bash | ||
| mkdir -p .nca/skills/test-skill | ||
| cp /home/nst/Documents/superpower/skills/subagent-driven-development/SKILL.md .nca/skills/test-skill/ |
There was a problem hiding this comment.
P2: Verification commands use developer-specific absolute paths, making the plan non-portable and brittle for other contributors/CI.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At docs/superpowers/plans/2026-03-24-skill-supporting-files.md, line 585:
<comment>Verification commands use developer-specific absolute paths, making the plan non-portable and brittle for other contributors/CI.</comment>
<file context>
@@ -0,0 +1,603 @@
+
+```bash
+mkdir -p .nca/skills/test-skill
+cp /home/nst/Documents/superpower/skills/subagent-driven-development/SKILL.md .nca/skills/test-skill/
+cp /home/nst/Documents/superpower/skills/subagent-driven-development/implementer-prompt.md .nca/skills/test-skill/
+cp /home/nst/Documents/superpower/skills/subagent-driven-development/spec-reviewer-prompt.md .nca/skills/test-skill/
</file context>
|
|
||
| ### Key Event Handling | ||
|
|
||
| When `question_modal_open` is true, key events are intercepted before any other handler (same priority as existing pickers): |
There was a problem hiding this comment.
P2: The new spec claims the question modal intercepts key events before any other handler, but current TUI logic explicitly prioritizes approval hotkeys over the question modal. This documentation contradicts the implemented behavior and the PR’s intent, risking future regressions.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At docs/superpowers/specs/2026-03-24-question-modal-picker-design.md, line 53:
<comment>The new spec claims the question modal intercepts key events before any other handler, but current TUI logic explicitly prioritizes approval hotkeys over the question modal. This documentation contradicts the implemented behavior and the PR’s intent, risking future regressions.</comment>
<file context>
@@ -0,0 +1,131 @@
+
+### Key Event Handling
+
+When `question_modal_open` is true, key events are intercepted before any other handler (same priority as existing pickers):
+
+| Key | Action |
</file context>
| When `question_modal_open` is true, key events are intercepted before any other handler (same priority as existing pickers): | |
| When `question_modal_open` is true, key events are intercepted before normal composer handling, but approval hotkeys still take precedence when an approval is active (same priority as existing pickers). |
| 1. Parse `skill_name` from `call.input` | ||
| 2. Call `SkillCatalog::discover()` with stored workspace root and skill directories | ||
| 3. Find matching skill by `command` field | ||
| 4. Return `skill.expanded_body()` as the tool result |
There was a problem hiding this comment.
P2: Spec defines two different success response contracts (wrapped message vs raw expanded_body()), creating avoidable implementation/test drift.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At docs/superpowers/specs/2026-03-24-invoke-skill-tool-design.md, line 66:
<comment>Spec defines two different success response contracts (wrapped message vs raw `expanded_body()`), creating avoidable implementation/test drift.</comment>
<file context>
@@ -0,0 +1,111 @@
+1. Parse `skill_name` from `call.input`
+2. Call `SkillCatalog::discover()` with stored workspace root and skill directories
+3. Find matching skill by `command` field
+4. Return `skill.expanded_body()` as the tool result
+5. On no match, return error listing available skill commands
+
</file context>
| let mut refs = Vec::new(); | ||
|
|
||
| // Pattern 1: ./path references | ||
| for m in DOT_SLASH_RE.find_iter(body) { |
There was a problem hiding this comment.
P2: extract_file_references claims to return references in first-occurrence order, but it scans in three separate passes (./, then @, then backticks), so mixed syntaxes will be reordered vs. their textual appearance.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At crates/core/src/skills.rs, line 542:
<comment>extract_file_references claims to return references in first-occurrence order, but it scans in three separate passes (./, then @, then backticks), so mixed syntaxes will be reordered vs. their textual appearance.</comment>
<file context>
@@ -394,6 +438,139 @@ fn parse_permission_mode_str(raw: &str) -> Option<PermissionMode> {
+ let mut refs = Vec::new();
+
+ // Pattern 1: ./path references
+ for m in DOT_SLASH_RE.find_iter(body) {
+ let path = m.as_str().to_string();
+ let key = path.trim_start_matches("./").to_string();
</file context>
| }; | ||
|
|
||
| let tmp = git_clone_to_temp(&url).map_err(anyhow::Error::msg)?; | ||
| let new_commit = git_head_commit(tmp.path()).ok(); |
There was a problem hiding this comment.
P2: git_head_commit errors are silently converted to None and then persisted, which can clear a previously stored commit hash. That causes future skills update runs to treat the skill as a local install and skip updates permanently after a transient failure.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At crates/cli/src/main.rs, line 1403:
<comment>`git_head_commit` errors are silently converted to `None` and then persisted, which can clear a previously stored commit hash. That causes future `skills update` runs to treat the skill as a local install and skip updates permanently after a transient failure.</comment>
<file context>
@@ -1274,6 +1329,120 @@ fn list_skills(config: &NcaConfig, workspace_root: &Path, json: bool) -> anyhow:
+ };
+
+ let tmp = git_clone_to_temp(&url).map_err(anyhow::Error::msg)?;
+ let new_commit = git_head_commit(tmp.path()).ok();
+
+ if new_commit.as_deref() == entry.commit.as_deref() {
</file context>
|
|
||
| let mut installed = Vec::new(); | ||
| for (name, src_dir) in &filtered { | ||
| let safe_name = sanitize_skill_name(name)?; |
There was a problem hiding this comment.
P2: No post-sanitization collision check: two different skill directories that sanitize to the same name will overwrite each other and the lock entry, leading to nondeterministic installs.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At crates/core/src/skill_installer.rs, line 331:
<comment>No post-sanitization collision check: two different skill directories that sanitize to the same name will overwrite each other and the lock entry, leading to nondeterministic installs.</comment>
<file context>
@@ -0,0 +1,588 @@
+
+ let mut installed = Vec::new();
+ for (name, src_dir) in &filtered {
+ let safe_name = sanitize_skill_name(name)?;
+ let dest = target_dir.join(&safe_name);
+
</file context>
Ensure active approvals always receive Ctrl+Y/Ctrl+N/Ctrl+U handling even when the question modal is open. Also harden git integration tests against inherited git environment and host hooks, and make doctor JSON CLI test deterministic against host env overrides.
ab40e95 to
7d4b5d3
Compare
Summary
Test plan
Fixes #38.
Summary by cubic
Fixes broken approval hotkeys in the TUI when the question modal is open and adds a consistent modal-based question picker. Also introduces dynamic skill invocation and management: an
invoke_skilltool, supporting-file inlining, andnca skillssubcommands.New Features
invoke_skilltool to load full skill instructions on demand; registered in the supervisor and reflected in the system prompt.Skill::expanded_body()(usesregex); shows active skill in subagent sidebar rows.nca skillsCLI: list/add/remove/update skills backed by a newskill_installer(GitHub or local sources, lock file management).Bug Fixes
Written for commit 7d4b5d3. Summary will update on new commits.