Skip to content

feat(extensions): add interactive file-view modes - #675

Open
benvinegar wants to merge 4 commits into
mainfrom
claude/inline-diff-editing-esm5b8-modes
Open

feat(extensions): add interactive file-view modes#675
benvinegar wants to merge 4 commits into
mainfrom
claude/inline-diff-editing-esm5b8-modes

Conversation

@benvinegar

@benvinegar benvinegar commented Aug 4, 2026

Copy link
Copy Markdown
Member

Summary

Adds opt-in keyboard modes for extension file views. A command can activate a view and its mode with fileViews.enterMode(viewId); the view then handles keys through onKey.

This also makes ctrl+<letter> chord matching accept bare C0 control bytes from synthetic or embedded input sources.

Mode behavior

  • enterMode(viewId) selects the view and activates its mode in one step. Invalid or unavailable views are refused with a warning.
  • Only one mode runs at a time. Replacing or exiting a mode preserves ordered, exactly-once onEnter and onExit callbacks, including re-entrant handoffs.
  • onKey returns:
    • "handled" to consume the key
    • "pass" to continue through Hunk's commands and scrolling
    • "exit" to consume the key and leave the mode
  • Dialogs and focused text inputs keep priority. Escape is always host-owned and exits the active mode.
  • Modes also exit when the selected file, active presentation, extension set, or review session changes.
  • Stale controls cannot activate a mode after a hard reload. Modes entered after a soft reload belong to the new review.
  • Callback failures show a warning without breaking the review.
  • The status bar shows <extension>:<view> mode — Esc exits while active.

C0 matching applies only to unnamed bare control-byte events. Named Tab and Enter events remain distinct, so ctrl+i and ctrl+m do not claim them.

Validation

Coverage includes activation and refusal policy, lifecycle ordering and re-entrancy, reload behavior, key routing, double-Escape handling, callback failures, C0 matching, scrollbox pass-through, and a real PTY interaction flow.

Typecheck, unit tests, PTY integration, TTY smoke tests, lint, package checks, documentation checks, and CI pass. Documentation and changesets are included.

This PR description was generated by Pi using gpt-5.6-sol

@vercel

vercel Bot commented Aug 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
hunk-web Ready Ready Preview Aug 6, 2026 3:36pm

Request Review

@greptile-apps

greptile-apps Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds opt-in interactive keyboard modes for extension file views, including public API controls, host routing, lifecycle containment, status feedback, documentation, and end-to-end tests.

  • Adds synchronous handled/pass/exit key routing ahead of the command table while preserving modal and focused-input precedence.
  • Adds activation validation, automatic teardown on review or presentation changes, and extension-attributed failure containment.
  • Extends consumer checks, documentation examples, app-level tests, and PTY integration coverage.

Confidence Score: 4/5

The mode replacement lifecycle defect should be fixed before merging because a valid extension callback or command can skip an active mode's required cleanup.

beginFileViewMode unconditionally publishes a new activation, so nested or repeated enterMode calls can overwrite the current activation without invoking its onExit callback and can report success after reentrant teardown.

Files Needing Attention: src/ui/App.tsx

Important Files Changed

Filename Overview
src/ui/App.tsx Integrates mode state, controls, lifecycle, validity checks, and status feedback, but replacing or reentrantly entering a mode can skip the prior activation's teardown.
src/ui/fileViews/mode.ts Defines focused activation, validity, lifecycle-containment, key-delivery, and status-hint helpers.
src/ui/hooks/useAppKeyboardShortcuts.ts Inserts mode routing after modal and focused-input owners and before command dispatch.
src/extension-api/types.ts Publishes the mode callbacks, context, key results, and file-view control methods.
src/extensions/runExtension.ts Validates that every declared file-view mode provides an onKey callback.
src/ui/AppHost.file-view-modes.test.tsx Covers normal routing, exits, refusal cases, and thrown handlers, but not replacement or reentrant activation.
docs/extensions.md Documents interactive mode activation, routing results, automatic exits, and a compiling stateful-view example.

Sequence Diagram

sequenceDiagram
  participant E as Extension command
  participant A as App mode state
  participant K as Keyboard router
  participant M as File-view mode
  participant C as Command table
  E->>A: enterMode(view)
  A->>M: onEnter(context)
  K->>M: onKey(key)
  alt handled
    M-->>K: handled
    K-->>K: consume key
  else pass
    M-->>K: pass
    K->>C: dispatch key
  else exit or Escape
    M-->>K: exit
    K->>A: exit mode
    A->>M: onExit(context)
  end
Loading
Prompt To Fix All With AI
### Issue 1
src/ui/App.tsx:719-720
**Mode replacement skips teardown**

When an active mode's `onKey` callback or a passed-through command calls `enterMode`, `beginFileViewMode` overwrites the current activation without invoking its `onExit`, causing that activation's cleanup and extension state teardown to be skipped. Reentrant entry from `onEnter` can also replace or exit the activation while the outer `enterMode` still reports success.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1): Last reviewed commit: "chore: add changeset for interactive fil..." | Re-trigger Greptile

Comment thread src/ui/App.tsx Outdated
Comment on lines +719 to +720
setActiveFileViewMode(active);
if (!runFileViewModeLifecycle(active, "onEnter", warnFileViewMode)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Mode replacement skips teardown

When an active mode's onKey callback or a passed-through command calls enterMode, beginFileViewMode overwrites the current activation without invoking its onExit, causing that activation's cleanup and extension state teardown to be skipped. Reentrant entry from onEnter can also replace or exit the activation while the outer enterMode still reports success.

Knowledge Base Used: Extension System: API, Loading, and Host Integration

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/ui/App.tsx
Line: 719-720

Comment:
**Mode replacement skips teardown**

When an active mode's `onKey` callback or a passed-through command calls `enterMode`, `beginFileViewMode` overwrites the current activation without invoking its `onExit`, causing that activation's cleanup and extension state teardown to be skipped. Reentrant entry from `onEnter` can also replace or exit the activation while the outer `enterMode` still reports success.

**Knowledge Base Used:** [Extension System: API, Loading, and Host Integration](https://app.greptile.com/modem/-/custom-context/knowledge-base/modem-dev/hunk/-/docs/extension-system.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

@benvinegar
benvinegar force-pushed the claude/inline-diff-editing-esm5b8-workspace branch from 356e70a to 5cec20e Compare August 4, 2026 14:54
@benvinegar
benvinegar force-pushed the claude/inline-diff-editing-esm5b8-modes branch from c62bfe6 to 1a478cd Compare August 4, 2026 14:59
@benvinegar
benvinegar force-pushed the claude/inline-diff-editing-esm5b8-modes branch from 1a478cd to 88f3ce8 Compare August 4, 2026 15:34
@benvinegar
benvinegar force-pushed the claude/inline-diff-editing-esm5b8-workspace branch from 5cec20e to 41d71cc Compare August 4, 2026 16:40
@benvinegar
benvinegar force-pushed the claude/inline-diff-editing-esm5b8-modes branch from 88f3ce8 to 143749e Compare August 4, 2026 16:53
@benvinegar
benvinegar force-pushed the claude/inline-diff-editing-esm5b8-workspace branch from 4d9adcb to b2edc6f Compare August 5, 2026 23:35
@benvinegar
benvinegar force-pushed the claude/inline-diff-editing-esm5b8-modes branch from 9ace620 to b3ca3f0 Compare August 6, 2026 00:06
@benvinegar
benvinegar changed the base branch from claude/inline-diff-editing-esm5b8-workspace to main August 6, 2026 00:06
claude added 3 commits August 6, 2026 10:44
File views were pure presentations: the host owned all input, so a
stateful view (folds, pickers, a future inline editor) had no way to
receive keys. A view may now register an opt-in mode; while active, keys
the modal surfaces do not claim route to its onKey ahead of the command
table, with "pass" declining back to normal handling. Escape stays
host-owned so there is always a way out, the mode auto-exits on file
change, deselection, and reload, and a throwing handler is contained to
a warning. Entering is one step: enterMode makes the view the selected
file's presentation and gives its mode the keys, since requiring a
prior select cost every mode entry a second keypress for an invariant
selection itself preserves. ExtensionKeyEvent moved into the contract
module so the mode types stay import-free; its published path is
unchanged. The published chord matcher also learned the bare C0 form of
ctrl+<letter> that some terminals send, so mode and host bindings match
it without hand-rolled control-byte checks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015aJpBUupsP9L7Wtd7MEzmU
…andoffs

A mode entered by an async handler that resumed after a soft reload was
tagged with the closed-over pre-reload bootstrap, so the auto-exit
effect tore the fresh mode down on its first render; the generation now
reads through the live ref like every other input to activation.
Entering a mode over an active one silently replaced it, skipping the
old mode's onExit — activation now funnels through the single exit path
first, so onExit stays exactly-once per activation. Escape ownership in
the key chain answered from a render-refreshed mirror, letting a second
Escape in one input flush be swallowed after the mode had already
exited; both branches now ask the same live App-side authority. The C0
chord fallback's stated rationale is corrected to what it actually is —
a compatibility net for structural events the OpenTUI parser never
produced — with no behavior change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015aJpBUupsP9L7Wtd7MEzmU
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants