opencode-vim-navigation 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -7,6 +7,13 @@ scrollback—while keeping your prompt draft safe.
7
7
  Press **Ctrl+Q** to switch between INPUT and NAV. A compact, right-aligned
8
8
  mode label appears on a divider immediately above the prompt.
9
9
 
10
+ When the native plan-review form is the only pending form, Ctrl+Q also enters
11
+ NAV even though the prompt does not own focus. Navigation scrolls the session
12
+ transcript behind the form; NAV consumes Enter, Escape, and all other
13
+ non-navigation keys so they cannot approve, cancel, or edit the form. Press
14
+ Ctrl+Q again to return to the form. Permission prompts and unrelated forms
15
+ continue to own their keys and block NAV.
16
+
10
17
  ## Compatibility
11
18
 
12
19
  Tested with **OpenCode 2.1.4**. This uses OpenCode V2's TUI plugin API, not the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-vim-navigation",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Modal Vim-style transcript navigation for the OpenCode terminal UI",
5
5
  "license": "MIT",
6
6
  "author": "Raaghav Arya",
@@ -17,11 +17,20 @@
17
17
  "registry": "https://registry.npmjs.org/"
18
18
  },
19
19
  "type": "module",
20
- "keywords": ["opencode", "opencode-plugin", "tui", "vim", "navigation"],
20
+ "keywords": [
21
+ "opencode",
22
+ "opencode-plugin",
23
+ "tui",
24
+ "vim",
25
+ "navigation"
26
+ ],
21
27
  "exports": {
22
28
  "./tui": "./tui.ts"
23
29
  },
24
- "files": ["tui.ts", "src/"],
30
+ "files": [
31
+ "tui.ts",
32
+ "src/"
33
+ ],
25
34
  "peerDependencies": {
26
35
  "@opentui/core": ">=0.6.8 <0.7.0",
27
36
  "@opentui/solid": ">=0.6.8 <0.7.0",
package/src/adapter.mjs CHANGED
@@ -25,19 +25,32 @@ export function createSetup({ InputRenderable, TextareaRenderable, createElement
25
25
  && !traits.capture?.includes("navigate") && !traits.capture?.includes("submit")
26
26
  && !traits.capture?.includes("autocomplete")
27
27
  }
28
- const blocked = () => {
28
+ const visibleForms = () => {
29
29
  const id = session()
30
- if (!id) return false
30
+ if (!id) return []
31
31
  const info = data.session.get(id)
32
32
  const location = info?.location ?? context.location
33
33
  // Match the host's visible request set: root sessions aggregate their
34
34
  // descendants; child sessions show global/location forms only.
35
- if (data.session.form.list("global", location)?.length) return true
36
- if (info?.parentID) return false
35
+ const forms = [...(data.session.form.list("global", location) ?? [])]
36
+ if (info?.parentID) return forms
37
37
  const ids = new Set([id, ...data.session.family(id)])
38
- return [...ids].some((sessionID) =>
39
- data.session.permission.list(sessionID)?.length || data.session.form.list(sessionID)?.length,
40
- )
38
+ for (const sessionID of ids) forms.push(...(data.session.form.list(sessionID) ?? []))
39
+ return forms
40
+ }
41
+ const planReviewForm = () => {
42
+ const forms = visibleForms()
43
+ return forms.length > 0 && forms.every((form) => form.metadata?.plugin === "raaghav.plan-review")
44
+ }
45
+ const blocked = () => {
46
+ const id = session()
47
+ if (!id) return false
48
+ const info = data.session.get(id)
49
+ if (info?.parentID) return visibleForms().some((form) => form.metadata?.plugin !== "raaghav.plan-review")
50
+ const location = info?.location ?? context.location
51
+ if (data.session.permission.list(id)?.length) return true
52
+ if (data.session.family(id).some((sessionID) => data.session.permission.list(sessionID)?.length)) return true
53
+ return visibleForms().some((form) => form.metadata?.plugin !== "raaghav.plan-review")
41
54
  }
42
55
  const required = ["session.line.up", "session.line.down", "session.page.up", "session.page.down", "session.first", "session.last"]
43
56
  const controller = navigation({
@@ -45,6 +58,7 @@ export function createSetup({ InputRenderable, TextareaRenderable, createElement
45
58
  mode: keymap.mode,
46
59
  promptFocused,
47
60
  blocked,
61
+ planReviewForm,
48
62
  ready: () => {
49
63
  const commands = new Set(keymap.commands().map((command) => command.id))
50
64
  if (required.every((id) => commands.has(id))) return true
@@ -78,6 +92,7 @@ export function createSetup({ InputRenderable, TextareaRenderable, createElement
78
92
  nav()
79
93
  session()
80
94
  blocked()
95
+ planReviewForm()
81
96
  promptFocused()
82
97
  controller.sync()
83
98
  })
@@ -6,6 +6,8 @@ export function navigation(context) {
6
6
  let pendingG = false
7
7
 
8
8
  const active = () => popMode !== undefined
9
+ const planReview = () => context.planReviewForm?.() === true
10
+ const navigationAllowed = () => !context.blocked?.() && (context.promptFocused() || planReview())
9
11
  function leave(follow = false) {
10
12
  if (!active()) return
11
13
  const release = popMode
@@ -16,8 +18,8 @@ export function navigation(context) {
16
18
  if (follow) context.dispatch("session.last")
17
19
  }
18
20
  function sync() {
19
- if (active() && (context.session() !== sessionID || context.blocked?.()
20
- || (context.mode.current() === MODE && !context.promptFocused()))) leave()
21
+ if (active() && (context.session() !== sessionID || !navigationAllowed()
22
+ || (context.mode.current() === MODE && !context.promptFocused() && !planReview()))) leave()
21
23
  }
22
24
  function scroll(id) {
23
25
  context.dispatch(id)
@@ -28,11 +30,12 @@ export function navigation(context) {
28
30
  }
29
31
  function key(event) {
30
32
  sync()
31
- if (!context.session() || context.blocked?.()) return
33
+ if (!context.session() || !navigationAllowed()) return
32
34
  // Dialogs, permission forms, autocomplete, and embedded terminals own their keys.
33
35
  const mode = context.mode.current()
34
- if (mode !== "base" && mode !== MODE) return
35
- if (!context.promptFocused()) {
36
+ const isPlanReview = planReview()
37
+ if (mode !== "base" && mode !== MODE && !isPlanReview) return
38
+ if (!context.promptFocused() && !isPlanReview) {
36
39
  if (active()) leave()
37
40
  return
38
41
  }
@@ -96,7 +99,7 @@ export function navigation(context) {
96
99
  function paste(event) {
97
100
  sync()
98
101
  if (active() && context.mode.current() === MODE) {
99
- if (!context.promptFocused()) leave()
102
+ if (!context.promptFocused() && !planReview()) leave()
100
103
  else consume(event)
101
104
  }
102
105
  }