opencode-navigator 0.21.7 → 0.22.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/CHANGELOG.md CHANGED
@@ -1,6 +1,17 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## [0.22.0] - 2026-10-23
4
+
5
+ - Pin Quick Actions and Skills to a compact sidebar toolbar independently of bookmarks. Choose icons in a searchable picker, customize supported state icons, reorder buttons, and select a More menu or wrapping rows. Preserve Skill confirmations, unavailable reasons, keyboard navigation, and Text fallback; hide the toolbar entirely when empty.
6
+ - Use OpenCode 2's built-in Todo history for the sidebar, with native completed-write and revert semantics, existing filters/progress, and manual Refresh. Keep edits in ordinary chat, preserve previous Navigator tasks as a read-only archive, and retain the independent provider-quota server integration.
7
+ - Support OpenCode 2.1's plugin and rendering APIs while retaining the OpenCode 1.x adapter; restore custom dialogs and rounded selections on the newer native renderer.
8
+ - Replace the shortcut toast with an icon-led hint featuring highlighted keys, navigation/update groups, a distinct Escape action, and responsive Text/no-corner-font layouts. Keep prompt focus and share its action list with scrollable keyboard help.
9
+ - Update Subagent status immediately from OpenCode 2's execution lifecycle and keep unresolved external credentials separate from verified provider accounts.
10
+ - Keep OpenCode updates on the current major line and preserve pinned-source Navigator updates when a target release needs newer runtime dependencies.
11
+
12
+ ## [0.21.8] - 2026-10-09
13
+
14
+ - Make the unfilled part of Limits scales substantially quieter, matching the theme-derived sidebar-divider tone. Keep the filled segment, reset text, and remaining values readable in both icon modes and after theme changes.
4
15
 
5
16
  ## [0.21.7] - 2026-10-09
6
17
 
@@ -108,6 +119,8 @@
108
119
 
109
120
  Earlier release notes are available on [GitHub Releases](https://github.com/St1ggy/opencode-navigator/releases).
110
121
 
122
+ [0.22.0]: https://github.com/St1ggy/opencode-navigator/compare/v0.21.8...v0.22.0
123
+ [0.21.8]: https://github.com/St1ggy/opencode-navigator/compare/v0.21.7...v0.21.8
111
124
  [0.21.7]: https://github.com/St1ggy/opencode-navigator/compare/v0.21.6...v0.21.7
112
125
  [0.21.6]: https://github.com/St1ggy/opencode-navigator/compare/v0.21.5...v0.21.6
113
126
  [0.21.5]: https://github.com/St1ggy/opencode-navigator/compare/v0.21.4...v0.21.5
package/README.md CHANGED
@@ -12,7 +12,7 @@ See the [changelog](CHANGELOG.md) for release-by-release changes.
12
12
 
13
13
  - Theme-aware session title with the session creation date
14
14
  - First-run setup guide with interactive section settings
15
- - Collapsible Todo section with status filters, grouped tasks, progress and priority indicators; OpenCode 2 adds session-owned tasks and optional model-context instructions
15
+ - Collapsible Todo section with status filters, grouped tasks and progress; OpenCode 2 follows the host's native Todo history, while OpenCode 1.x retains supported priority indicators
16
16
  - Active subagent list with live statuses and click-to-open navigation, including dev-team workers running in separate local processes
17
17
  - Compact, searchable workspace skill list with user-wide favorites and groups, recent skills, source details, and click-to-confirm slash commands
18
18
  - Configurable host quick actions, including OpenCode 1.x's auto-approve toggle and OpenCode 2.x's Permissions settings, with user-wide bookmarks and availability reasons
@@ -34,6 +34,7 @@ See the [changelog](CHANGELOG.md) for release-by-release changes.
34
34
  - Copy the selected scope's layout and MCP states as versioned JSON, then validate and preview imports
35
35
  - Focus the sidebar with `Ctrl+Shift+F`, then navigate with arrows or `j`/`k`
36
36
  - Open sidebar shortcut mode with `Ctrl+Shift+B`; press `h` to toggle the panel, `c` to check Navigator updates, or `u` to check and confirm an update
37
+ - Scan an icon-led shortcut hint with highlighted keys, separate navigation/update groups, and a clear Escape action; narrow terminals and Text fallback use the same controls
37
38
  - Optionally open new sessions directly in an empty chat with the sidebar, skipping Home on startup and from New session
38
39
  - Keep OpenCode's compact footer while showing host and Navigator versions with independent update indicators; click a version label or its update icon to confirm an in-place update through the current installation method and scope
39
40
 
@@ -41,10 +42,13 @@ Todo starts expanded. Subagents, skills, quick actions, LSP, and MCP start
41
42
  collapsed. Use `Save current layout as default` in sidebar settings to reuse the
42
43
  current visible/hidden, expanded/collapsed, and ordering states in new sessions.
43
44
 
44
- Supports OpenCode 1.18.30 and newer, including OpenCode 2.x. OpenCode 2.0.16 does
45
- not expose host Todo or LSP data to TUI plugins. Navigator supplies its own
46
- session Todo list through a separately configured server plugin in 2.x. Without
47
- that plugin, Todo stays visible with setup guidance. LSP is hidden on 2.x while retaining Subagents,
45
+ Supports OpenCode 1.18.30 and newer, including OpenCode 2.x. The compatibility
46
+ matrix covers 1.18.30, 1.19.3, 2.0.21, and 2.1.0 with separate real-host checks.
47
+ The 2.1 integration adapts its plugin API and native renderer while retaining
48
+ the 1.x adapter. On OpenCode 2, Navigator reads the same native Todo history as
49
+ the host sidebar, including completed `todowrite` calls and the current revert
50
+ boundary. Viewing Todo needs no Navigator server plugin. OpenCode 2.1 exposes
51
+ content and four statuses, without priorities or direct Todo CRUD. LSP is hidden on 2.x while retaining Subagents,
48
52
  Skills, Quick Actions, MCP, Search, settings, and presets. All interface icons use Nerd Font glyphs by
49
53
  default (Codicons for controls, keyboard symbols for hints, language logos for LSP). Use a Nerd Fonts v3
50
54
  terminal font, or select **Settings → Behavior → Icon style → Text fallback**.
@@ -190,9 +194,28 @@ OpenCode 2.x CLI plugins are global terminal settings; there is no project-local
190
194
  plugin overrides are required. OpenCode validates `cli.json` strictly, so keep it
191
195
  within the linked schema and restart OpenCode after changing it.
192
196
 
193
- To enable Todo in OpenCode 2.x, also install the **server** half of Navigator.
194
- Add the same package to `~/.config/opencode/opencode.json` (or the project
195
- `opencode.json`):
197
+ OpenCode 2.1's built-in Todo tool is opt-in. Enable it in the OpenCode server
198
+ configuration used by that host, then restart OpenCode:
199
+
200
+ ```json
201
+ {
202
+ "$schema": "https://opencode.ai/config.json",
203
+ "experimental": { "todo": true }
204
+ }
205
+ ```
206
+
207
+ Ask the agent in ordinary chat to create or change the list through `todowrite`.
208
+ Navigator displays the latest completed native call before the session's revert
209
+ boundary, reacts to host updates, and offers All / Active / Finished filters,
210
+ status groups, progress, Refresh, and retry after read failures. It does not start
211
+ model requests from Todo controls. Running or failed writes leave the last
212
+ completed list visible; a completed write with an empty array clears it.
213
+ Native tasks have `content` and one of `pending`, `in_progress`, `completed`, or
214
+ `cancelled`; priorities remain unavailable in OpenCode 2's native schema.
215
+
216
+ The optional **server** half of Navigator is used for provider-native quotas and
217
+ read-only access to the previous Navigator Todo archive. Add it to the server
218
+ configuration only when those integrations are needed:
196
219
 
197
220
  ```json
198
221
  {
@@ -204,14 +227,25 @@ Add the same package to `~/.config/opencode/opencode.json` (or the project
204
227
  For a local source installation, use the directory containing Navigator's
205
228
  `server.js` in the `plugin` array. Keep the TUI package in `cli.json` as above;
206
229
  the server configuration alone does not replace it. Restart OpenCode after
207
- configuring both. Todo items belong to the current session, survive restarts,
208
- and update in the sidebar when the agent calls `navigator_todo_write`. The
209
- **Todo instructions: Off/On** control in the Todo section is session-scoped and
210
- starts **Off**: tasks and the agent tool work without adding Todo instructions
211
- to the model's context. OpenCode 1.x continues to show the host's Todo list.
230
+ configuring both. OpenCode owns native Todo persistence and tool instructions;
231
+ the separate `navigator_todo_write` tool and Navigator Todo-instructions control
232
+ are retired. OpenCode 1.x continues to show its supported host Todo source.
233
+
234
+ Previous Navigator tasks stay in their original plugin-scoped storage. Their
235
+ existing RPC is now a read-only archive; it never replaces native history or
236
+ deletes the saved list. To inspect a previous session explicitly:
237
+
238
+ ```sh
239
+ opencode api --standalone rpc.call --param rpcID=opencode-navigator.todo --param method=list --data '{"input":{"sessionID":"ses_previous"},"location":{"directory":"/path/to/project"}}'
240
+ ```
241
+
242
+ After reviewing the archive, an ordinary chat request can transfer its content
243
+ and statuses through the host's native tool. Archive reads do not change tasks
244
+ or add model instructions.
212
245
 
213
246
  If you use both OpenCode 1.x and 2.x with the same global `opencode.json`, keep
214
- that shared server config compatible with 1.x. Place the server-plugin entry
247
+ that shared server config compatible with 1.x. Place the 2.x experimental Todo
248
+ setting and optional server-plugin entry
215
249
  in a separate JSON file and set `OPENCODE_CONFIG` to its path **only when
216
250
  launching OpenCode 2.x**. The TUI configuration in `cli.json` remains separate;
217
251
  the OpenCode 1.x launch must not load Navigator's 2.x server plugin.
@@ -346,6 +380,36 @@ Search Everything. Usage never changes the order. Visibility remains independent
346
380
  so any action, including a bookmarked one, can be hidden from the sidebar while it
347
381
  remains searchable. Unavailable actions stay visible with a route- or host-specific reason.
348
382
 
383
+ ### Pinned toolbar
384
+
385
+ Pin frequently used Quick Actions and Skills to an icon toolbar above the sidebar's
386
+ sections. In OpenCode 2.x it sits below the host-owned title. An empty toolbar takes
387
+ no space. Pins are independent of bookmarks, section visibility, filters, and item limits.
388
+
389
+ Use the pin control on an Action or Skill row, or open **Settings → Behavior →
390
+ Pinned toolbar…**. The command palette also offers **Manage pinned toolbar**.
391
+ Choose a base icon in the searchable picker, then select **Pin to toolbar**.
392
+ The manager lets you change icons, move buttons, or unpin them. Cancelling the
393
+ picker returns to the draft; cancelling the draft discards its changes.
394
+
395
+ Where the host exposes a state, the same button can use a different icon for each
396
+ state: auto-approve On/Off on OpenCode 1.x, and Idle/Compacting on OpenCode 2.x.
397
+ The 2.x permissions button opens host Settings and uses its base icon. Hover or
398
+ keyboard focus shows the button's name, observed state, or unavailable reason.
399
+ Skill buttons retain confirmation and trust settings; a missing or ambiguous
400
+ source stays pinned but cannot run.
401
+
402
+ **Behavior → Toolbar overflow** switches between one row with a **More** menu
403
+ (the default) and wrapping rows. Left/Right moves along a row; Up/Down moves
404
+ between rows. The setting follows Global/Worktree scope and portable defaults.
405
+ Pins, their order, icons, and exact Skill sources are private user-wide preferences
406
+ and are excluded from portable exports. Use **Reload settings from file** in other
407
+ running sessions after saving changes in another instance.
408
+
409
+ ![Pinned Action and Skill toolbar](https://raw.githubusercontent.com/St1ggy/opencode-navigator/main/screenshots/53-toolbar-permissions-off.png)
410
+
411
+ ![Searchable toolbar icon picker](https://raw.githubusercontent.com/St1ggy/opencode-navigator/main/screenshots/57-toolbar-icon-picker.png)
412
+
349
413
  In OpenCode 1.x, Navigator saves changes to the auto-approve mode as a user-wide
350
414
  default, including changes made through OpenCode's own command palette. Set the
351
415
  default under **Navigator Settings → Behavior → Default auto-approve permissions**;
@@ -372,7 +436,7 @@ account usage. Separate model-specific buckets require explicit model associatio
372
436
  Switching provider, model, session, or account invalidates stale reads and credit actions.
373
437
  In OpenCode 2.x, Limits follows the prompt's selected model immediately, before a
374
438
  message is sent, through the public reactive `ui.model.current()` API. OpenCode
375
- 1.x through 1.18.35 exposes only the saved session model: a newly selected prompt
439
+ 1.x, rechecked at 1.19.3, exposes only the saved session model: a newly selected prompt
376
440
  model becomes visible to Navigator when the host saves it, normally on submission.
377
441
  An immediate pre-submission switch in 1.x needs a new public host accessor or event.
378
442
  CodexBar is useful for discovering providers, but its cookie, credential-file,
@@ -437,8 +501,14 @@ windows have one blank row between them, without a vertical guide or gutter.
437
501
  Each window uses two compact rows: a full-width adaptive
438
502
  thin-line scale, then muted reset time on the left and remaining headroom aligned
439
503
  to the right. The remaining value keeps its own space; long reset text is clipped.
440
- Both rows stay within the sidebar's padding when it resizes. Healthy views omit
441
- account IDs, technical scope/window-duration suffixes, source explanations, and update timestamps. Account scope and native window durations remain in the data model;
504
+ Both rows stay within the sidebar's padding when it resizes.
505
+
506
+ The filled part of each scale uses muted text; the unfilled part uses the much
507
+ quieter, theme-derived sidebar-divider tone. Reset time and remaining values keep
508
+ their readable colors in both icon modes and when the theme changes.
509
+
510
+ Healthy views omit account IDs, technical scope/window-duration suffixes, source
511
+ explanations, and update timestamps. Account scope and native window durations remain in the data model;
442
512
  durations describe provider quota windows, not time remaining until reset. Balances retain their native
443
513
  currency or point unit; a missing total does not become a full progress scale.
444
514
 
@@ -513,6 +583,7 @@ Navigator plugin tuple:
513
583
  "cornerFont": true,
514
584
  "lspIconStyle": "nerd",
515
585
  "rowDensity": "compact",
586
+ "toolbarOverflow": "menu",
516
587
  "sectionItemLimits": { "todo": 0, "skills": 5, "mcp": 5 },
517
588
  "quickActionOrder": ["session.rename", "session.copy", "session.fork"],
518
589
  "quickActionVisibility": { "session.export": false }
@@ -550,7 +621,7 @@ density, item limits, and Navigator's safe Quick Action allowlist. Unknown value
550
621
  sections, actions, and MCP states are discarded. The previous snake_case tuple
551
622
  options (`toggle_key`, `focus_key`, `search_key`, `persist_mcp`, `limits_refresh_minutes`, `start_in_chat`, `show_session_title`,
552
623
  `show_session_date`, `corner_font`, `icon_style`,
553
- `lsp_icon_style`, `row_density`, `section_item_limits`, `quick_action_order`,
624
+ `lsp_icon_style`, `row_density`, `toolbar_overflow`, `section_item_limits`, `quick_action_order`,
554
625
  `quick_action_visibility`, `sections`, and `section_order`) remain compatible;
555
626
  canonical nested fields win when both forms are present.
556
627
 
@@ -661,14 +732,23 @@ Press `Ctrl+Shift+B` to enter Navigator shortcuts, then choose an action:
661
732
  | `u` | Check for a Navigator update, then open its confirmation if available |
662
733
  | `Escape` | Cancel shortcut mode |
663
734
 
664
- The mode ends after an action or five seconds of inactivity. Its toast lists only
665
- actions supported by the current host; LSP is omitted on OpenCode 2.x. Change the
735
+ The mode ends after an action or five seconds of inactivity. Its compact,
736
+ non-modal hint groups section navigation separately from update actions, uses
737
+ icons and highlighted key tokens, and keeps Escape visible. It adapts to narrow
738
+ or short terminals, light/dark themes, Text fallback, and the no-corner-font
739
+ setting without taking focus from the prompt. Keyboard help uses the same
740
+ action inventory and supports arrow-key and page-key scrolling. Only actions
741
+ supported by the current host are listed; LSP is omitted on OpenCode 2.x. Change the
666
742
  entry shortcut under **Settings → Behavior**. The command palette also offers
667
743
  **Check Navigator updates** and **Update Navigator**.
668
744
 
669
745
  Manual checks report when Navigator is current or the update source is unavailable.
670
746
  Updates require confirmation, preserve the existing installation method and scope,
671
- and show a restart reminder after completion. Background check failures stay quiet.
747
+ and show a restart reminder after completion. OpenCode updates stay on the running
748
+ major line; moving from 1.x to 2.x remains a separate installation decision.
749
+ Confirmed pinned-source Navigator updates install the target release's runtime
750
+ dependencies in a new snapshot before switching its wrapper. The previous
751
+ snapshot and private settings remain intact. Background check failures stay quiet.
672
752
 
673
753
  ### Search Everything
674
754
 
@@ -1,8 +1,9 @@
1
1
  var PLUGIN_ID="opencode-navigator"
2
2
  var TOGGLE_COMMAND=`${PLUGIN_ID}.toggle`
3
3
  var FOCUS_COMMAND=`${PLUGIN_ID}.focus`
4
- var SEARCH_COMMAND=`${PLUGIN_ID}.search`;
5
- var QUICK_ACTION_IDS=["session.rename","session.timeline","session.copy","session.export","session.compact","session.list","session.new","session.fork","messages.copy","session.first","session.last","session.messages_last_user","session.message.next","session.message.previous","permission.mode"]
4
+ var SEARCH_COMMAND=`${PLUGIN_ID}.search`
5
+ var DEFAULT_SEARCH_KEY="ctrl+shift+k";
6
+ var SIDEBAR_SECTIONS=["limits","todo","subagents","skills","quick_actions","lsp","mcp"]
6
7
  var SECTION_DEFINITIONS=[{name:"limits",label:"Limits"},{name:"todo",label:"Todo"},{name:"subagents",label:"Subagents"},{name:"skills",label:"Skills"},{name:"quick_actions",label:"Quick actions"},{name:"lsp",label:"LSP"},{name:"mcp",label:"MCP"}];
7
8
  var MAX_LIMITS_REFRESH_MINUTES=Math.floor(2147483647 / 60000)
8
- export{QUICK_ACTION_IDS,SECTION_DEFINITIONS,PLUGIN_ID}
9
+ export{SIDEBAR_SECTIONS,SECTION_DEFINITIONS,PLUGIN_ID,DEFAULT_SEARCH_KEY}
File without changes
@@ -0,0 +1,20 @@
1
+ function mcpToggleAction(status){if(status==="pending")
2
+ return
3
+ return status==="connected"?"disconnect":"connect"
4
+ }
5
+ function mcpPresetPreview(items,states){const byName=new Map(items.map((item)=>[item.name,item]))
6
+ const names=[...new Set([...Object.keys(states),...byName.keys()])].sort((a,b)=>a.localeCompare(b))
7
+ const rows=names.map((name)=>{const item=byName.get(name)
8
+ const desired=Object.hasOwn(states,name)?states[name]:undefined
9
+ const action=item&&mcpToggleAction(item.status)
10
+ let change="unchanged"
11
+ if(desired&&!item)
12
+ change="missing"
13
+ else if(desired&&!action)
14
+ change="unavailable"
15
+ else if(desired==="enabled"&&action==="connect"||desired==="disabled"&&action==="disconnect"){change=action
16
+ }
17
+ return{name,status:item?.status,desired,change}})
18
+ const changes=rows.flatMap((row)=>row.change==="connect"||row.change==="disconnect"?[{name:row.name,action:row.change}]:[])
19
+ return{rows,changes}}
20
+ export{mcpPresetPreview}
@@ -0,0 +1,175 @@
1
+ import {use as _$use} from "@opentui/solid"
2
+ import {effect as _$effect3} from "@opentui/solid"
3
+ import {createTextNode as _$createTextNode} from "@opentui/solid"
4
+ import {insertNode as _$insertNode} from "@opentui/solid"
5
+ import {insert as _$insert3} from "@opentui/solid"
6
+ import {memo as _$memo3} from "@opentui/solid"
7
+ import {setProp as _$setProp3} from "@opentui/solid"
8
+ import {createElement as _$createElement3} from "@opentui/solid"
9
+ import {createComponent as _$createComponent3} from "@opentui/solid"
10
+ import {useTerminalDimensions} from "@opentui/solid"
11
+ import {For,createEffect,createMemo,createSignal as createSignal2,onCleanup} from "solid-js"
12
+ import {DialogHeader,DialogSurface,SelectionBox as SelectionBox3,terminalIcon as terminalIcon2,terminalIconIDs,terminalIconLabel as terminalIconLabel2,useDialogState,useDialogs,useIcons as useIcons3} from "./tui.js"
13
+ import {createComponent as _$createComponent} from "@opentui/solid"
14
+ import {effect as _$effect} from "@opentui/solid"
15
+ import {insert as _$insert} from "@opentui/solid"
16
+ import {setProp as _$setProp} from "@opentui/solid"
17
+ import {createElement as _$createElement} from "@opentui/solid"
18
+ import {memo as _$memo} from "@opentui/solid"
19
+ import {SelectionBox,terminalIcon,terminalIconLabel,useIcons} from "./tui.js"
20
+ function IconPickerItem(props){const icons=useIcons()
21
+ const theme=()=>props.api.theme.current
22
+ return _$createComponent(SelectionBox,{get id(){return `icon-picker.${props.id}`
23
+ },width:9,height:2,paddingLeft:1,paddingRight:1,get backgroundColor(){return _$memo(()=>!!props.active)()?theme().backgroundElement:undefined
24
+ },get onMouseOver(){return props.onHover
25
+ },onMouseDown:(event)=>event.stopPropagation(),onMouseUp:(event)=>{event.stopPropagation()
26
+ props.onSelect()
27
+ },get children(){return [(()=>{var _el$=_$createElement("text")
28
+ _$setProp(_el$,"truncate",true)
29
+ _$setProp(_el$,"wrapMode","none")
30
+ _$insert(_el$,()=>terminalIcon(props.id,icons.style()),null)
31
+ _$insert(_el$,()=>props.selected?" *":"",null)
32
+ _$effect((_$p)=>_$setProp(_el$,"fg",props.active||props.selected?theme().accent:theme().text,_$p))
33
+ return _el$
34
+ })(),(()=>{var _el$2=_$createElement("text")
35
+ _$setProp(_el$2,"truncate",true)
36
+ _$setProp(_el$2,"wrapMode","none")
37
+ _$insert(_el$2,()=>terminalIconLabel(props.id))
38
+ _$effect((_$p)=>_$setProp(_el$2,"fg",theme().textMuted,_$p))
39
+ return _el$2
40
+ })()]
41
+ }})
42
+ }
43
+ import {createComponent as _$createComponent2} from "@opentui/solid"
44
+ import {setProp as _$setProp2} from "@opentui/solid"
45
+ import {effect as _$effect2} from "@opentui/solid"
46
+ import {insert as _$insert2} from "@opentui/solid"
47
+ import {createElement as _$createElement2} from "@opentui/solid"
48
+ import {memo as _$memo2} from "@opentui/solid"
49
+ import {createSignal} from "solid-js"
50
+ import {SelectionBox as SelectionBox2,useIcons as useIcons2} from "./tui.js"
51
+ function IconPickerSearch(props){const icons=useIcons2()
52
+ const [focused,setFocused]=createSignal(true)
53
+ const theme=()=>props.api.theme.current
54
+ const fg=()=>focused()?theme().selectedListItemText:theme().text
55
+ return _$createComponent2(SelectionBox2,{paddingLeft:1,paddingRight:1,flexDirection:"row",gap:1,get backgroundColor(){return _$memo2(()=>!!focused())()?theme().primary:theme().backgroundElement
56
+ },get children(){return [(()=>{var _el$=_$createElement2("text")
57
+ _$insert2(_el$,()=>icons.icon("search"))
58
+ _$effect2((_$p)=>_$setProp2(_el$,"fg",fg(),_$p))
59
+ return _el$
60
+ })(),(()=>{var _el$2=_$createElement2("input")
61
+ _$setProp2(_el$2,"id","icon-picker.search")
62
+ _$setProp2(_el$2,"flexGrow",1)
63
+ _$setProp2(_el$2,"focused",true)
64
+ _$setProp2(_el$2,"placeholder","Search icons...")
65
+ _$setProp2(_el$2,"on:focused",()=>setFocused(true))
66
+ _$setProp2(_el$2,"on:blurred",()=>setFocused(false))
67
+ _$setProp2(_el$2,"backgroundColor","transparent")
68
+ _$setProp2(_el$2,"focusedBackgroundColor","transparent")
69
+ _$effect2((_p$)=>{var{query:_v$,onInput:_v$2}=props,_v$3=fg(),_v$4=fg(),_v$5=fg(),_v$6=fg()
70
+ _v$!==_p$.e&&(_p$.e=_$setProp2(_el$2,"value",_v$,_p$.e))
71
+ _v$2!==_p$.t&&(_p$.t=_$setProp2(_el$2,"onInput",_v$2,_p$.t))
72
+ _v$3!==_p$.a&&(_p$.a=_$setProp2(_el$2,"textColor",_v$3,_p$.a))
73
+ _v$4!==_p$.o&&(_p$.o=_$setProp2(_el$2,"focusedTextColor",_v$4,_p$.o))
74
+ _v$5!==_p$.i&&(_p$.i=_$setProp2(_el$2,"placeholderColor",_v$5,_p$.i))
75
+ _v$6!==_p$.n&&(_p$.n=_$setProp2(_el$2,"cursorColor",_v$6,_p$.n))
76
+ return _p$
77
+ },{e:undefined,t:undefined,a:undefined,o:undefined,i:undefined,n:undefined
78
+ })
79
+ return _el$2
80
+ })()]
81
+ }})
82
+ }
83
+ function IconPicker(props){const icons=useIcons3()
84
+ const dialogs=useDialogs(props.api)
85
+ const dimensions=useTerminalDimensions()
86
+ const [query,setQuery]=useDialogState("icon-query","")
87
+ const [active,setActive]=useDialogState("icon-active",props.value)
88
+ const keywords={bolt:"action lightning",gear:"settings configuration",shield:"permissions security",copy:"clipboard transcript",check:"done success",refresh:"compact reload retry",book:"skill documentation",pin:"favorite bookmark"
89
+ }
90
+ const items=createMemo(()=>terminalIconIDs.filter((id)=>`${terminalIconLabel2(id)} ${keywords[id] ?? ""}`.includes(query().toLowerCase().trim())))
91
+ const [width,setWidth]=createSignal2(Math.max(10,dimensions().width-12))
92
+ const columns=createMemo(()=>Math.max(1,Math.min(6,Math.floor(width() / 10))))
93
+ const rows=createMemo(()=>Array.from({length:Math.ceil(items().length / columns())
94
+ },(_,index)=>items().slice(index*columns(),(index + 1)*columns())))
95
+ let body
96
+ createEffect(()=>{if(!items().includes(active()))
97
+ setActive(items()[0]??"")
98
+ })
99
+ function move(offset,horizontal=false){const index=items().indexOf(active())
100
+ const start=horizontal?Math.floor(index / columns())*columns():0
101
+ const end=horizontal?Math.min(items().length-1,start + columns()-1):items().length-1
102
+ const next=items()[Math.max(start,Math.min(end,index + offset))]
103
+ if(next){setActive(next)
104
+ body?.scrollChildIntoView(`icon-picker.${next}`)
105
+ }}
106
+ function select(id=active()){if(items().includes(id))
107
+ props.onSelect(id)
108
+ }
109
+ const prefix="opencode-navigator.icon-picker"
110
+ const unregister=props.api.keymap.registerLayer({mode:"modal",priority:1000,commands:[{name:`${prefix}.left`,run:()=>move(-1,true)
111
+ },{name:`${prefix}.right`,run:()=>move(1,true)
112
+ },{name:`${prefix}.up`,run:()=>move(-columns())
113
+ },{name:`${prefix}.down`,run:()=>move(columns())
114
+ },{name:`${prefix}.select`,run:()=>select()
115
+ }],bindings:["left","right","up","down","return"].map((key)=>({key,cmd:`${prefix}.${key === "return" ? "select" : key}`
116
+ }))
117
+ })
118
+ onCleanup(unregister)
119
+ const theme=()=>props.api.theme.current
120
+ return _$createComponent3(DialogSurface,{get api(){return props.api
121
+ },id:"opencode-navigator.icon-picker",get children(){return [_$createComponent3(DialogHeader,{get api(){return props.api
122
+ },title:"Choose an icon",get onBack(){return dialogs.back
123
+ }}),_$createComponent3(IconPickerSearch,{get api(){return props.api
124
+ },get query(){return query()
125
+ },onInput:setQuery
126
+ }),_$createComponent3(SelectionBox3,{paddingLeft:1,paddingRight:1,get backgroundColor(){return theme().backgroundElement
127
+ },get children(){var _el$=_$createElement3("text"),_el$2=_$createTextNode(` `)
128
+ _$insertNode(_el$,_el$2)
129
+ _$setProp3(_el$,"wrapMode","none")
130
+ _$setProp3(_el$,"truncate",true)
131
+ _$insert3(_el$,()=>terminalIcon2(active(),icons.style()),_el$2)
132
+ _$insert3(_el$,()=>terminalIconLabel2(active())||"No matching icons",null)
133
+ _$insert3(_el$,()=>active()===props.value?" \xB7 selected":"",null)
134
+ _$effect3((_$p)=>_$setProp3(_el$,"fg",theme().accent,_$p))
135
+ return _el$
136
+ }}),(()=>{var _el$3=_$createElement3("scrollbox")
137
+ _$use((node)=>{body=node
138
+ },_el$3)
139
+ _$setProp3(_el$3,"onSizeChange",function(){setWidth(this.width)
140
+ })
141
+ _$setProp3(_el$3,"scrollX",false)
142
+ _$insert3(_el$3,_$createComponent3(For,{get each(){return rows()
143
+ },children:(row)=>(()=>{var _el$0=_$createElement3("box")
144
+ _$setProp3(_el$0,"flexDirection","row")
145
+ _$setProp3(_el$0,"gap",1)
146
+ _$insert3(_el$0,_$createComponent3(For,{each:row,children:(id)=>_$createComponent3(IconPickerItem,{get api(){return props.api
147
+ },id,get active(){return id===active()
148
+ },get selected(){return id===props.value
149
+ },onHover:()=>setActive(id),onSelect:()=>select(id)
150
+ })
151
+ }))
152
+ return _el$0
153
+ })()
154
+ }))
155
+ _$effect3((_$p)=>_$setProp3(_el$3,"height",Math.max(2,Math.min(10,Math.floor(dimensions().height*0.6)-9)),_$p))
156
+ return _el$3
157
+ })(),(()=>{var _el$4=_$createElement3("text"),_el$5=_$createTextNode(` rows \xB7 `),_el$6=_$createTextNode(` icons \xB7 `),_el$7=_$createTextNode(` choose \xB7 `),_el$8=_$createTextNode(` cancel`)
158
+ _$insertNode(_el$4,_el$5)
159
+ _$insertNode(_el$4,_el$6)
160
+ _$insertNode(_el$4,_el$7)
161
+ _$insertNode(_el$4,_el$8)
162
+ _$setProp3(_el$4,"height",1)
163
+ _$setProp3(_el$4,"truncate",true)
164
+ _$setProp3(_el$4,"wrapMode","none")
165
+ _$insert3(_el$4,()=>icons.key("up/down"),_el$5)
166
+ _$insert3(_el$4,()=>icons.key("left/right"),_el$6)
167
+ _$insert3(_el$4,()=>icons.key("enter"),_el$7)
168
+ _$insert3(_el$4,()=>icons.key("esc"),_el$8)
169
+ _$effect3((_$p)=>_$setProp3(_el$4,"fg",theme().textMuted,_$p))
170
+ return _el$4
171
+ })()]
172
+ }})
173
+ }
174
+ export{IconPicker
175
+ }
@@ -1,5 +1,8 @@
1
1
  import {SECTION_DEFINITIONS,PLUGIN_ID
2
- } from "./chunk-d1bws32s.js";
2
+ } from "./chunk-dz8cfnfs.js"
3
+ import"./chunk-qmntexwm.js"
4
+ import {mcpPresetPreview
5
+ } from "./chunk-v6bz59xn.js";
3
6
  import {createComponent as _$createComponent7} from "@opentui/solid"
4
7
  import {setProp as _$setProp5} from "@opentui/solid"
5
8
  import {effect as _$effect5} from "@opentui/solid"
@@ -13,25 +16,6 @@ import {memo as _$memo4} from "@opentui/solid"
13
16
  import {For as For3,createMemo as createMemo3,onMount as onMount2} from "solid-js"
14
17
  import {useDialogs as useDialogs3,useIcons as useIcons6} from "./tui.js"
15
18
  import {createMemo} from "solid-js"
16
- function mcpToggleAction(status){if(status==="pending")
17
- return
18
- return status==="connected"?"disconnect":"connect"
19
- }
20
- function mcpPresetPreview(items,states){const byName=new Map(items.map((item)=>[item.name,item]))
21
- const names=[...new Set([...Object.keys(states),...byName.keys()])].sort((a,b)=>a.localeCompare(b))
22
- const rows=names.map((name)=>{const item=byName.get(name)
23
- const desired=Object.hasOwn(states,name)?states[name]:undefined
24
- const action=item&&mcpToggleAction(item.status)
25
- let change="unchanged"
26
- if(desired&&!item)
27
- change="missing"
28
- else if(desired&&!action)
29
- change="unavailable"
30
- else if(desired==="enabled"&&action==="connect"||desired==="disabled"&&action==="disconnect"){change=action
31
- }
32
- return{name,status:item?.status,desired,change}})
33
- const changes=rows.flatMap((row)=>row.change==="connect"||row.change==="disconnect"?[{name:row.name,action:row.change}]:[])
34
- return{rows,changes}}
35
19
  function createLayoutPresetPreview(preferences,name){const scope=preferences.selectedScopeKey()
36
20
  const valid=()=>preferences.selectedScopeKey()===scope
37
21
  const preset=createMemo(()=>preferences.layoutPresets()[name])
@@ -1,5 +1,5 @@
1
1
  import {PLUGIN_ID
2
- } from "./chunk-d1bws32s.js";
2
+ } from "./chunk-dz8cfnfs.js";
3
3
  import {createSignal} from "solid-js"
4
4
  function versionParts(value){const match=/^v?(\d+)\.(\d+)\.(\d+)(?:-([^+]+))?/.exec(value)
5
5
  return match?{numbers:match.slice(1,4).map(Number),prerelease:match[4]||undefined}:undefined
@@ -13,6 +13,10 @@ return next.numbers[index]>installed.numbers[index]
13
13
  }
14
14
  return installed.prerelease!==undefined&&next.prerelease===undefined
15
15
  }
16
+ function isSameMajorVersion(left,right){const first=versionParts(left)
17
+ const second=versionParts(right)
18
+ return Boolean(first&&second&&first.numbers[0]===second.numbers[0])
19
+ }
16
20
  var NAVIGATOR_PACKAGE_URL="https://registry.npmjs.org/opencode-navigator/latest"
17
21
  var REQUEST_TIMEOUT_MS=5000
18
22
  async function readNavigatorVersion(request,signal){const response=await request(NAVIGATOR_PACKAGE_URL,{headers:{accept:"application/json"},signal})
@@ -29,7 +33,7 @@ let controller
29
33
  let pending
30
34
  let disposed=false
31
35
  const unsubscribe=api.event.on("installation.update-available",(event)=>{const version=event.properties.version
32
- setOpenCodeUpdate(isNewerVersion(version,api.app.version)?version:undefined)
36
+ setOpenCodeUpdate(isSameMajorVersion(version,api.app.version)&&isNewerVersion(version,api.app.version)?version:undefined)
33
37
  })
34
38
  api.lifecycle.onDispose(()=>{disposed=true
35
39
  controller?.abort()
@@ -170,6 +174,10 @@ return{openCode:(version)=>open("openCode",version),navigator:(version)=>open("n
170
174
  import {homedir} from "os"
171
175
  import {basename,dirname,isAbsolute,join} from "path"
172
176
  import {fileURLToPath} from "url"
177
+ async function upgradeHost(current,target,execute){if(!isSameMajorVersion(current,target))
178
+ throw new Error("OpenCode updates must stay on the current major version")
179
+ await execute([process.execPath,"upgrade",target])
180
+ }
173
181
  var run=async (command)=>{const child=Bun.spawn([...command],{stdout:"ignore",stderr:"pipe"})
174
182
  const error=new Response(child.stderr).text()
175
183
  const exitCode=await child.exited
@@ -207,7 +215,7 @@ if(configHasPlugin(await read(path),spec))
207
215
  return false
208
216
  return configHasPlugin(await read(global),spec)
209
217
  }
210
- function createOpenCodeV1VersionUpdater(api,meta,execute=run,read=readConfig){return{openCode:(target)=>execute([process.execPath,"upgrade",target]),async navigator(target){if(meta.source==="file"){const wrapper=meta.spec.startsWith("file://")?fileURLToPath(meta.spec):meta.spec
218
+ function createOpenCodeV1VersionUpdater(api,meta,execute=run,read=readConfig){return{openCode:(target)=>upgradeHost(api.app.version,target,execute),async navigator(target){if(meta.source==="file"){const wrapper=meta.spec.startsWith("file://")?fileURLToPath(meta.spec):meta.spec
211
219
  return pinnedUpdate(wrapper,target,execute)
212
220
  }
213
221
  if(meta.source!=="npm")
@@ -217,7 +225,7 @@ const result=await api.plugins.install(`opencode-navigator@${target}`,{global:aw
217
225
  if(!result.ok)
218
226
  throw new Error(result.message)
219
227
  }}}
220
- function createOpenCodeV2VersionUpdater(context,execute=run){return{openCode:(target)=>execute([process.execPath,"upgrade",target]),async navigator(target){const response=await context.client.plugin.list(context.location?{location:context.location}:{})
228
+ function createOpenCodeV2VersionUpdater(context,execute=run){return{openCode:(target)=>upgradeHost(context.app.version,target,execute),async navigator(target){const response=await context.client.plugin.list(context.location?{location:context.location}:{})
221
229
  const item=response.data.find((plugin)=>plugin.id===PLUGIN_ID)
222
230
  if(!item)
223
231
  throw new Error("Navigator is not in the active plugin list")
@@ -65,7 +65,8 @@ function parsePluginSettings(value){const input=record(value)
65
65
  if(!input)
66
66
  return{}
67
67
  return{...typeof input.toggleKey==="string"&&input.toggleKey.trim()&&{toggleKey:input.toggleKey.trim()},...typeof input.focusKey==="string"&&input.focusKey.trim()&&{focusKey:input.focusKey.trim()},...typeof input.searchKey==="string"&&input.searchKey.trim()&&{searchKey:input.searchKey.trim()},...typeof input.persistMcp==="boolean"&&{persistMcp:input.persistMcp},...parseLimitsRefreshMinutes(input.limitsRefreshMinutes)!==undefined&&{limitsRefreshMinutes:parseLimitsRefreshMinutes(input.limitsRefreshMinutes)
68
- },...typeof input.startInChat==="boolean"&&{startInChat:input.startInChat},...typeof input.showSessionTitle==="boolean"&&{showSessionTitle:input.showSessionTitle},...typeof input.showSessionDate==="boolean"&&{showSessionDate:input.showSessionDate},...typeof input.cornerFont==="boolean"&&{cornerFont:input.cornerFont},...(input.lspIconStyle==="nerd"||input.lspIconStyle==="text")&&{lspIconStyle:input.lspIconStyle},...(input.rowDensity==="compact"||input.rowDensity==="comfortable")&&{rowDensity:input.rowDensity
68
+ },...typeof input.startInChat==="boolean"&&{startInChat:input.startInChat},...(input.toolbarOverflow==="menu"||input.toolbarOverflow==="wrap")&&{toolbarOverflow:input.toolbarOverflow
69
+ },...typeof input.showSessionTitle==="boolean"&&{showSessionTitle:input.showSessionTitle},...typeof input.showSessionDate==="boolean"&&{showSessionDate:input.showSessionDate},...typeof input.cornerFont==="boolean"&&{cornerFont:input.cornerFont},...(input.lspIconStyle==="nerd"||input.lspIconStyle==="text")&&{lspIconStyle:input.lspIconStyle},...(input.rowDensity==="compact"||input.rowDensity==="comfortable")&&{rowDensity:input.rowDensity
69
70
  },...Object.keys(parseSectionItemLimits(input.sectionItemLimits)).length>0&&{sectionItemLimits:parseSectionItemLimits(input.sectionItemLimits)
70
71
  },...parseQuickActionOrder(input.quickActionOrder)&&{quickActionOrder:parseQuickActionOrder(input.quickActionOrder)
71
72
  },...Object.keys(parseQuickActionVisibility(input.quickActionVisibility)).length>0&&{quickActionVisibility:parseQuickActionVisibility(input.quickActionVisibility)
@@ -189,12 +190,19 @@ throw new Error(`Expected format ${PORTABLE_SETTINGS_FORMAT}`)
189
190
  if(input.version!==1)
190
191
  throw new Error(`Unsupported settings version ${String(input.version)}`)
191
192
  const unsupported=[]
192
- unknownKeys(input,["format","version","layout","mcp"],"",unsupported)
193
+ unknownKeys(input,["format","version","layout","mcp","behavior"],"",unsupported)
193
194
  const hasLayout=Object.hasOwn(input,"layout")
194
195
  const hasMcp=Object.hasOwn(input,"mcp")
195
- if(!hasLayout&&!hasMcp)
196
+ const behavior={}
197
+ if(input.behavior!==undefined){const raw=object(input.behavior,"/behavior")
198
+ unknownKeys(raw,["toolbarOverflow"],"/behavior",unsupported)
199
+ if(raw.toolbarOverflow!==undefined){if(raw.toolbarOverflow!=="menu"&&raw.toolbarOverflow!=="wrap")
200
+ throw new Error("/behavior/toolbarOverflow must be menu or wrap")
201
+ behavior.toolbarOverflow=raw.toolbarOverflow
202
+ }}
203
+ if(!hasLayout&&!hasMcp&&!behavior.toolbarOverflow)
196
204
  throw new Error("Settings JSON contains no supported settings")
197
- const settings={format:PORTABLE_SETTINGS_FORMAT,version:1,...hasLayout&&{layout:parseLayout(input.layout,unsupported)},...hasMcp&&{mcp:parseMcp(input.mcp)}}
205
+ const settings={format:PORTABLE_SETTINGS_FORMAT,version:1,...hasLayout&&{layout:parseLayout(input.layout,unsupported)},...hasMcp&&{mcp:parseMcp(input.mcp)},...behavior.toolbarOverflow&&{behavior}}
198
206
  unsupported.sort()
199
207
  return{settings,unsupported}}
200
208
  var FILENAME=join(".opencode","navigator.json")