opencode-vim-navigation 0.0.0-stage → 0.1.0
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/LICENSE +21 -0
- package/README.md +257 -2
- package/package.json +33 -4
- package/src/navigation.mjs +97 -0
- package/src/tui.tsx +68 -0
- package/tui.ts +2 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Raaghav Arya
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,258 @@
|
|
|
1
|
-
#
|
|
1
|
+
# opencode-vim-navigation
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Modal Vim-style transcript navigation for the **OpenCode terminal UI**.
|
|
4
|
+
Scroll the conversation inside OpenCode itself—not your terminal emulator's,
|
|
5
|
+
Neovim's, or tmux's scrollback—while keeping your prompt draft safe.
|
|
6
|
+
|
|
7
|
+
Press **Ctrl+Q** to switch between INPUT and NAV. A compact, right-aligned
|
|
8
|
+
mode label appears on a divider just above the prompt.
|
|
9
|
+
|
|
10
|
+
## Compatibility
|
|
11
|
+
|
|
12
|
+
Tested with **OpenCode 2.1.4**. This uses OpenCode V2's TUI plugin API and is
|
|
13
|
+
not a server plugin or a plugin for the desktop/web UI. Older releases and
|
|
14
|
+
future API changes may require adaptation.
|
|
15
|
+
|
|
16
|
+
No build step is needed: OpenCode supplies `@opencode/plugin/tui`, OpenTUI,
|
|
17
|
+
and `solid-js`, and loads the TSX entrypoint. OpenTUI and Solid are declared
|
|
18
|
+
as peer dependencies so the package documents its host compatibility.
|
|
19
|
+
Do not run this entrypoint directly with Node.js.
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
### From a local checkout (available now)
|
|
24
|
+
|
|
25
|
+
Clone the repository:
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
git clone https://github.com/Raaghav-Arya/opencode-vim-navigation.git
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Then add its **absolute directory path** to the `plugins` array in
|
|
32
|
+
`~/.config/opencode/cli.json`:
|
|
33
|
+
|
|
34
|
+
```json
|
|
35
|
+
{
|
|
36
|
+
"plugins": ["/absolute/path/to/opencode-vim-navigation"]
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Merge this into any existing TUI config; do not overwrite other settings.
|
|
41
|
+
Restart OpenCode. No `npm install` is needed for this local package.
|
|
42
|
+
|
|
43
|
+
Alternatively, symlink the package directory into
|
|
44
|
+
`~/.config/opencode/plugins/opencode-vim-navigation` for automatic discovery.
|
|
45
|
+
Use **one installation method**, not both.
|
|
46
|
+
|
|
47
|
+
### From npm (after publication)
|
|
48
|
+
|
|
49
|
+
Once the package has been published as `opencode-vim-navigation`, use:
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
{
|
|
53
|
+
"plugins": ["opencode-vim-navigation@0.1.0"]
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
This belongs in **`cli.json`**, not the server plugin list in `opencode.json`.
|
|
58
|
+
OpenCode V1-era documentation may mention `tui.json`; these instructions target V2.
|
|
59
|
+
OpenCode installs the package. Pin a version for reproducible configuration.
|
|
60
|
+
Until the first npm release, use the local-checkout installation above.
|
|
61
|
+
Check [npm](https://www.npmjs.com/package/opencode-vim-navigation) for availability.
|
|
62
|
+
|
|
63
|
+
## Keys
|
|
64
|
+
|
|
65
|
+
| Key | INPUT mode | NAV mode |
|
|
66
|
+
| --- | --- | --- |
|
|
67
|
+
| **Ctrl+Q** | Enter NAV | Return to INPUT and follow latest output |
|
|
68
|
+
| **j / k**, **Down / Up** | OpenCode default | One transcript line down/up |
|
|
69
|
+
| **gg / G** | OpenCode default | Jump to beginning/end |
|
|
70
|
+
| **Ctrl+U / Ctrl+D** | OpenCode default | Half a viewport up/down |
|
|
71
|
+
| **PageUp / PageDown** | OpenCode default | Half a viewport up/down |
|
|
72
|
+
| **Ctrl+Home / Ctrl+End** | OpenCode default | Jump to beginning/end |
|
|
73
|
+
| **i** | Type normally | Return to INPUT, follow latest output, preserve draft |
|
|
74
|
+
| **Escape / Ctrl+C** | OpenCode default | Leave NAV without moving the viewport, then perform OpenCode's normal interrupt/clear action |
|
|
75
|
+
|
|
76
|
+
Only Ctrl+Q is intercepted in INPUT. In particular, Ctrl+U/D retain their
|
|
77
|
+
normal prompt-editing/exit behavior until you enter NAV.
|
|
78
|
+
|
|
79
|
+
In NAV, other keys, Enter, and pasted text are ignored to protect your draft
|
|
80
|
+
and prevent accidental submissions. This is **transcript navigation**, not
|
|
81
|
+
Vim editing, cursor selection, or visual mode.
|
|
82
|
+
|
|
83
|
+
Dialogs, autocomplete, composer panels, and other non-base modes keep their
|
|
84
|
+
own keyboard handling. Incoming permission/form requests and session changes
|
|
85
|
+
exit NAV. Existing mouse behavior is unchanged. The home screen is unchanged.
|
|
86
|
+
|
|
87
|
+
### Neovim / Sidekick
|
|
88
|
+
|
|
89
|
+
Stay in Neovim **terminal mode**. OpenCode draws its own transcript, so normal
|
|
90
|
+
terminal-buffer scrollback cannot reliably navigate the conversation.
|
|
91
|
+
|
|
92
|
+
Let OpenCode receive Ctrl+Q and handle scrolling:
|
|
93
|
+
|
|
94
|
+
```lua
|
|
95
|
+
cli = {
|
|
96
|
+
tools = {
|
|
97
|
+
opencode = { native_scroll = true },
|
|
98
|
+
},
|
|
99
|
+
win = {
|
|
100
|
+
keys = { stopinsert = false },
|
|
101
|
+
},
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Merge these options into your existing Sidekick configuration. Terminal or
|
|
106
|
+
multiplexer mappings that intercept these keys must also be disabled.
|
|
107
|
+
|
|
108
|
+
## Development
|
|
109
|
+
|
|
110
|
+
Use Node.js 22 or newer for the development commands:
|
|
111
|
+
|
|
112
|
+
```sh
|
|
113
|
+
npm test
|
|
114
|
+
npm run check
|
|
115
|
+
npm pack --dry-run
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
There are no development dependencies to install. `npm run check` checks the
|
|
119
|
+
JavaScript controller and tests; it is **not** a TSX typecheck. OpenCode itself
|
|
120
|
+
loads and compiles the TUI adapter.
|
|
121
|
+
|
|
122
|
+
```text
|
|
123
|
+
tui.ts Conventional local/package entrypoint
|
|
124
|
+
src/
|
|
125
|
+
navigation.mjs Mode state and input routing
|
|
126
|
+
tui.tsx OpenCode adapter and mode indicator
|
|
127
|
+
tests/
|
|
128
|
+
navigation.test.mjs Controller tests using node:test
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
The 13 tests mock the host and cover toggling, navigation, INPUT passthrough,
|
|
132
|
+
paste/draft protection, dialog isolation, route cleanup, and missing commands.
|
|
133
|
+
They do not replace interactive rendering tests.
|
|
134
|
+
|
|
135
|
+
The initial implementation also passed a live pseudo-terminal smoke test
|
|
136
|
+
against OpenCode 2.1.4: INPUT/NAV switched, navigation changed the viewport,
|
|
137
|
+
and returning to INPUT preserved the draft, without submitting any prompt.
|
|
138
|
+
Recheck the same behavior after changing the adapter or upgrading OpenCode.
|
|
139
|
+
|
|
140
|
+
### Implementation details
|
|
141
|
+
|
|
142
|
+
The root `tui.ts` re-exports `src/tui.tsx`. OpenCode 2.1.4's local-directory
|
|
143
|
+
loader looks for a conventional root `tui` entrypoint; npm packages also
|
|
144
|
+
expose it via the `./tui` package export. Keep both when reorganizing source.
|
|
145
|
+
|
|
146
|
+
See the [OpenCode V2 CLI plugin guide](https://opencode.ai/v2/docs/build/plugins/cli)
|
|
147
|
+
for the host API and package conventions.
|
|
148
|
+
|
|
149
|
+
The plugin uses OpenTUI's public prepended key/paste listeners to stop NAV
|
|
150
|
+
input before it reaches the prompt. A named OpenCode keymap mode isolates
|
|
151
|
+
normal prompt shortcuts. Host transcript commands scroll the actual
|
|
152
|
+
conversation, including virtualized history/backfill; the plugin does not
|
|
153
|
+
scrape a terminal buffer. Listeners, mode, effects, and the mode indicator
|
|
154
|
+
are cleaned up when the plugin deactivates.
|
|
155
|
+
|
|
156
|
+
In **2.1.4**, `session.page.up/down` move by half of `scroll.height`, whereas
|
|
157
|
+
`session.half.page.up/down` move by a quarter. The plugin deliberately uses
|
|
158
|
+
the former. It checks for expected commands before entering NAV, but future
|
|
159
|
+
releases could retain names while changing semantics.
|
|
160
|
+
|
|
161
|
+
## Disable / uninstall
|
|
162
|
+
|
|
163
|
+
Remove the package entry from `cli.json`, then restart OpenCode. If installed
|
|
164
|
+
via automatic discovery instead, remove the symlink from `plugins/`.
|
|
165
|
+
Merely renaming a package directory within `plugins/` does not disable it.
|
|
166
|
+
|
|
167
|
+
The plugin ID in OpenCode's TUI plugin manager is `opencode-vim-navigation`.
|
|
168
|
+
|
|
169
|
+
## Maintainer: publishing to npm
|
|
170
|
+
|
|
171
|
+
GitHub hosts the source; npm distributes the installable package. Publishing
|
|
172
|
+
to GitHub does not automatically publish to npm. Public unscoped packages
|
|
173
|
+
can be published with a free npm account.
|
|
174
|
+
|
|
175
|
+
### First release
|
|
176
|
+
|
|
177
|
+
1. Create an account at [npmjs.com/signup](https://www.npmjs.com/signup) if
|
|
178
|
+
needed, verify your email, and enable two-factor authentication in your
|
|
179
|
+
account settings. GitHub CLI authentication does **not** log you into npm.
|
|
180
|
+
Save your recovery codes privately; never commit tokens or recovery codes.
|
|
181
|
+
2. From your checkout, log in using the browser flow:
|
|
182
|
+
|
|
183
|
+
```sh
|
|
184
|
+
npm login --registry=https://registry.npmjs.org/
|
|
185
|
+
npm whoami --registry=https://registry.npmjs.org/
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
3. Check the package name:
|
|
189
|
+
|
|
190
|
+
```sh
|
|
191
|
+
npm view opencode-vim-navigation name version --registry=https://registry.npmjs.org/
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
An E404 means no public package was found; it is not a reservation or a
|
|
195
|
+
guarantee that npm will accept the name. If someone else owns it, choose
|
|
196
|
+
a scoped name such as `@YOUR_NPM_USERNAME/opencode-vim-navigation` and
|
|
197
|
+
update `package.json` and the installation examples before publishing.
|
|
198
|
+
4. Review the release from the repository root:
|
|
199
|
+
|
|
200
|
+
```sh
|
|
201
|
+
npm test
|
|
202
|
+
npm run check
|
|
203
|
+
npm pack --dry-run
|
|
204
|
+
npm publish --dry-run
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
The package should include `LICENSE`, `README.md`, `package.json`, `tui.ts`,
|
|
208
|
+
and `src/`. Tests and GitHub workflows stay on GitHub, not in the tarball.
|
|
209
|
+
Dry runs do not upload the package and do not prove your account has
|
|
210
|
+
permission to publish. The `prepublishOnly` script reruns tests and
|
|
211
|
+
JavaScript syntax checks before publication.
|
|
212
|
+
5. Publish version **0.1.0**:
|
|
213
|
+
|
|
214
|
+
```sh
|
|
215
|
+
npm publish --access public
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Complete npm's browser / two-factor prompts. Do not paste authentication
|
|
219
|
+
codes or tokens into issues or chat. A successful publish ends with
|
|
220
|
+
`+ opencode-vim-navigation@0.1.0`.
|
|
221
|
+
6. Verify the release:
|
|
222
|
+
|
|
223
|
+
```sh
|
|
224
|
+
npm view opencode-vim-navigation@0.1.0 version dist.tarball
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Replace the local path in your OpenCode `cli.json` with
|
|
228
|
+
`opencode-vim-navigation@0.1.0` and restart OpenCode. Remove the
|
|
229
|
+
"after publication" wording in this README once the package is live.
|
|
230
|
+
7. Optionally mark the published source on GitHub:
|
|
231
|
+
|
|
232
|
+
```sh
|
|
233
|
+
git tag v0.1.0
|
|
234
|
+
git push origin v0.1.0
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Tag the exact commit used for publication; commit any release changes first.
|
|
238
|
+
|
|
239
|
+
### Later releases
|
|
240
|
+
|
|
241
|
+
npm versions cannot be overwritten or reused. For a compatible bug fix,
|
|
242
|
+
start from a clean checkout, update the changelog/README as appropriate, run
|
|
243
|
+
checks, then:
|
|
244
|
+
|
|
245
|
+
```sh
|
|
246
|
+
npm version patch
|
|
247
|
+
npm publish --access public
|
|
248
|
+
git push origin main --follow-tags
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
`npm version patch` updates the version and creates a commit and tag. Use a
|
|
252
|
+
minor or major bump when appropriate instead. Publish manually initially;
|
|
253
|
+
if automated releases are added later, prefer npm trusted publishing rather
|
|
254
|
+
than storing a long-lived npm token in the repository.
|
|
255
|
+
|
|
256
|
+
## License
|
|
257
|
+
|
|
258
|
+
[MIT](LICENSE) © 2026 Raaghav Arya.
|
package/package.json
CHANGED
|
@@ -1,6 +1,35 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-vim-navigation",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Modal Vim-style transcript navigation for the OpenCode terminal UI",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Raaghav Arya",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/Raaghav-Arya/opencode-vim-navigation.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/Raaghav-Arya/opencode-vim-navigation#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/Raaghav-Arya/opencode-vim-navigation/issues"
|
|
14
|
+
},
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public",
|
|
17
|
+
"registry": "https://registry.npmjs.org/"
|
|
18
|
+
},
|
|
19
|
+
"type": "module",
|
|
20
|
+
"keywords": ["opencode", "opencode-plugin", "tui", "vim", "navigation"],
|
|
21
|
+
"exports": {
|
|
22
|
+
"./tui": "./tui.ts"
|
|
23
|
+
},
|
|
24
|
+
"files": ["tui.ts", "src/"],
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"@opentui/core": ">=0.6.8 <0.7.0",
|
|
27
|
+
"@opentui/solid": ">=0.6.8 <0.7.0",
|
|
28
|
+
"solid-js": "^1.9.15"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"test": "node --test tests/*.test.mjs",
|
|
32
|
+
"check": "node --check src/navigation.mjs && node --check tests/navigation.test.mjs",
|
|
33
|
+
"prepublishOnly": "npm test && npm run check"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// Input routing is separate from the TUI adapter so it can be tested without a server.
|
|
2
|
+
export function navigation(context) {
|
|
3
|
+
const MODE = "transcript-navigation"
|
|
4
|
+
let popMode
|
|
5
|
+
let sessionID
|
|
6
|
+
let pendingG = false
|
|
7
|
+
|
|
8
|
+
const active = () => popMode !== undefined
|
|
9
|
+
function leave(follow = false) {
|
|
10
|
+
if (!active()) return
|
|
11
|
+
popMode()
|
|
12
|
+
popMode = undefined
|
|
13
|
+
pendingG = false
|
|
14
|
+
sessionID = undefined
|
|
15
|
+
context.changed(false)
|
|
16
|
+
if (follow) context.dispatch("session.last")
|
|
17
|
+
}
|
|
18
|
+
function sync() {
|
|
19
|
+
if (active() && context.session() !== sessionID) leave()
|
|
20
|
+
}
|
|
21
|
+
function scroll(id) {
|
|
22
|
+
context.dispatch(id)
|
|
23
|
+
}
|
|
24
|
+
function consume(event) {
|
|
25
|
+
event.preventDefault()
|
|
26
|
+
event.stopPropagation()
|
|
27
|
+
}
|
|
28
|
+
function key(event) {
|
|
29
|
+
sync()
|
|
30
|
+
if (!context.session()) return
|
|
31
|
+
// Dialogs, permission forms, autocomplete, and embedded terminals own their keys.
|
|
32
|
+
const mode = context.mode.current()
|
|
33
|
+
if (mode !== "base" && mode !== MODE) return
|
|
34
|
+
if (!active() && !context.promptFocused()) return
|
|
35
|
+
const plain = !event.ctrl && !event.meta && !event.super && !event.hyper
|
|
36
|
+
const ctrl = event.ctrl && !event.meta && !event.super && !event.hyper && !event.shift
|
|
37
|
+
const name = event.name
|
|
38
|
+
if (ctrl && name === "q") {
|
|
39
|
+
consume(event)
|
|
40
|
+
if (active()) leave(true)
|
|
41
|
+
else {
|
|
42
|
+
if (!context.ready()) return
|
|
43
|
+
sessionID = context.session()
|
|
44
|
+
popMode = context.mode.push(MODE)
|
|
45
|
+
context.changed(true)
|
|
46
|
+
}
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
if (!active()) return // INPUT mode retains every default except Ctrl+Q.
|
|
50
|
+
// OpenCode 2.1.4's session.page.* commands move by half the viewport;
|
|
51
|
+
// session.half.page.* surprisingly move by a quarter. Use the actual half-page commands.
|
|
52
|
+
const controlScroll = ctrl && ({ u: "session.page.up", d: "session.page.down" })[name]
|
|
53
|
+
const pageScroll = plain && ({
|
|
54
|
+
pageup: "session.page.up", pagedown: "session.page.down",
|
|
55
|
+
})[name]
|
|
56
|
+
const edgeScroll = ctrl && ({ home: "session.first", end: "session.last" })[name]
|
|
57
|
+
if (controlScroll || pageScroll || edgeScroll) {
|
|
58
|
+
consume(event)
|
|
59
|
+
pendingG = false
|
|
60
|
+
scroll(controlScroll || pageScroll || edgeScroll)
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
if ((plain && name === "escape") || (ctrl && name === "c")) {
|
|
64
|
+
// Let OpenCode perform its usual interrupt/clear action after leaving NAV.
|
|
65
|
+
leave()
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
consume(event) // Protect the draft, including Enter and unknown shortcuts.
|
|
69
|
+
if (plain && name === "i" && !event.shift) {
|
|
70
|
+
leave(true)
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
const upperG = plain && (name === "G" || (name === "g" && event.shift))
|
|
74
|
+
if (upperG) {
|
|
75
|
+
pendingG = false
|
|
76
|
+
scroll("session.last")
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
if (plain && name === "g" && !event.shift) {
|
|
80
|
+
if (pendingG) scroll("session.first")
|
|
81
|
+
pendingG = !pendingG
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
pendingG = false
|
|
85
|
+
if (!plain || event.shift) return
|
|
86
|
+
const command = ({
|
|
87
|
+
j: "session.line.down", down: "session.line.down",
|
|
88
|
+
k: "session.line.up", up: "session.line.up",
|
|
89
|
+
})[name]
|
|
90
|
+
if (command) scroll(command)
|
|
91
|
+
}
|
|
92
|
+
function paste(event) {
|
|
93
|
+
sync()
|
|
94
|
+
if (active() && context.mode.current() === MODE) consume(event)
|
|
95
|
+
}
|
|
96
|
+
return { key, paste, leave, sync, active }
|
|
97
|
+
}
|
package/src/tui.tsx
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Plugin } from "@opencode/plugin/tui"
|
|
2
|
+
import { InputRenderable, TextareaRenderable } from "@opentui/core"
|
|
3
|
+
import { createEffect, createRoot, createSignal } from "solid-js"
|
|
4
|
+
import { navigation } from "./navigation.mjs"
|
|
5
|
+
|
|
6
|
+
export default Plugin.define({
|
|
7
|
+
id: "opencode-vim-navigation",
|
|
8
|
+
setup(context) {
|
|
9
|
+
const { renderer, keymap, ui } = context
|
|
10
|
+
if (!renderer.keyInput?.prependListener || !keymap.mode?.push) {
|
|
11
|
+
throw new Error("Transcript navigation requires the OpenCode 2.1.4 TUI API")
|
|
12
|
+
}
|
|
13
|
+
const [nav, setNav] = createSignal(false)
|
|
14
|
+
const session = () => {
|
|
15
|
+
const route = ui.router.current()
|
|
16
|
+
return route.type === "session" ? route.sessionID : undefined
|
|
17
|
+
}
|
|
18
|
+
const required = ["session.line.up", "session.line.down", "session.page.up", "session.page.down", "session.first", "session.last"]
|
|
19
|
+
const controller = navigation({
|
|
20
|
+
session,
|
|
21
|
+
mode: keymap.mode,
|
|
22
|
+
promptFocused: () => renderer.currentFocusedEditor instanceof TextareaRenderable
|
|
23
|
+
&& !(renderer.currentFocusedEditor instanceof InputRenderable),
|
|
24
|
+
ready: () => {
|
|
25
|
+
const commands = new Set(keymap.commands().map((command) => command.id))
|
|
26
|
+
if (required.every((id) => commands.has(id))) return true
|
|
27
|
+
ui.toast.show({ message: "Transcript navigation: this OpenCode version lacks the expected scrolling commands", variant: "error" })
|
|
28
|
+
return false
|
|
29
|
+
},
|
|
30
|
+
dispatch: (id) => keymap.dispatch(id),
|
|
31
|
+
changed: setNav,
|
|
32
|
+
})
|
|
33
|
+
// OpenTUI's public global input listeners run before the focused editor.
|
|
34
|
+
// Prepending also precedes OpenCode's own keymap, so NAV cannot submit or edit a draft.
|
|
35
|
+
renderer.keyInput.prependListener("keypress", controller.key)
|
|
36
|
+
renderer.keyInput.prependListener("paste", controller.paste)
|
|
37
|
+
const dispose = createRoot((dispose) => {
|
|
38
|
+
createEffect(() => {
|
|
39
|
+
controller.sync()
|
|
40
|
+
const id = session()
|
|
41
|
+
// Never trap an incoming permission request or interactive form in NAV.
|
|
42
|
+
if (id && (context.data.session.permission.list(id)?.length || context.data.session.form.list(id)?.length)) {
|
|
43
|
+
controller.leave()
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
return dispose
|
|
47
|
+
})
|
|
48
|
+
const removeStatus = ui.slot({
|
|
49
|
+
append: "session.composer.top",
|
|
50
|
+
render: () => <box
|
|
51
|
+
height={1}
|
|
52
|
+
width="100%"
|
|
53
|
+
border={["top"]}
|
|
54
|
+
borderColor={context.theme.border.base}
|
|
55
|
+
title={nav() ? " NAV · i: input " : " INPUT · Ctrl+Q: navigate "}
|
|
56
|
+
titleColor={nav() ? context.theme.text.base : context.theme.text.muted}
|
|
57
|
+
titleAlignment="right"
|
|
58
|
+
/>,
|
|
59
|
+
})
|
|
60
|
+
return () => {
|
|
61
|
+
renderer.keyInput.removeListener("keypress", controller.key)
|
|
62
|
+
renderer.keyInput.removeListener("paste", controller.paste)
|
|
63
|
+
controller.leave()
|
|
64
|
+
removeStatus()
|
|
65
|
+
dispose()
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
})
|
package/tui.ts
ADDED