fix: restore the V2 TUI sidebar and palette command - #24
Open
Manaf941 wants to merge 1 commit into
Open
Conversation
The V2 TUI integration registered nothing on current OpenCode 2 previews,
so neither the goal sidebar nor the `goal.show` palette command appeared.
Three separate breakages, verified against real hosts:
- `ui.slot` takes a single claim object (`{ append, render }`) from preview
0.0.0-next-17226 onward, and the positional overload was removed. Passing
the old pair aborts `setupTuiV2` on the first call, taking the keymap layer
down with the sidebar. `registerSlotV2` now branches on the callback arity,
so previews on either side of that change work.
- The sidebar read flat theme keys (`theme.text`, `theme.textMuted`,
`theme.primary`). Both preview generations expose a nested theme, where
`theme.text` is a colour group rather than a colour and the other two are
undefined. `themeColorV2` resolves the first candidate path that exists and
descends into a `default` leaf, keeping the legacy flat keys as fallbacks.
- The keymap layer omitted `mode: "global"`, which the plugin contract has
documented since 0.0.0-next-17055 ("Use global to opt out; defaults to
base"), so the palette never listed the command even once the layer
registered.
The V2 test mock encoded the outdated positional-slot and flat-theme
contract, which is why the suite stayed green while the feature was dead; it
now mirrors the current host, and reverting the call shape fails 8 tests.
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Summary
On current OpenCode 2 previews the V2 TUI integration registers nothing: no goal sidebar, and no
Goalentry in thectrl+ppalette. The server side (tools,/goal, persistence) is unaffected, so the plugin looks healthy while its whole TUI surface is dead.There are three independent breakages, and the first one hides the other two.
1.
ui.slottakes a claim object, and the positional form was removed. Bisecting the published@opencode-ai/plugindeclarations (dist/tui/context.d.ts) pins the change to0.0.0-next-17226:ui.slot0.0.0-next-17055…0.0.0-next-17224<Name extends SlotName>(name: Name, render: Slot<Name>) => () => void0.0.0-next-17226and later(claim: SlotClaim) => () => void, carryingrenderplus exactly one ofprepend/append/before/after/replaceNo overload was retained. In the
0.0.0-next-17403host:So
slot("sidebar.content", render)throws,setupTuiV2aborts on its first statement, and theappslot — which mounts the keymap layer — is never reached. One throw removes both the sidebar and the palette command.2. The sidebar read theme keys that do not exist. It used
theme.text,theme.textMutedandtheme.primary, copied from the V1 sidebar. Both preview generations expose a nested theme (theme.text.default,theme.text.subdued,theme.text.feedback.success.default,theme.text.action.primary): the last two flat keys areundefinedandtheme.textis a colour group, not a colour. This was already wrong on0.0.0-next-17055(theme.primarydoes not occur in that binary at all), so it never worked — it was simply unreachable behind breakage 1.3. The keymap layer never opted into the global mode.
goal.showsetspalette: truebut the layer omittedmode, which the plugin contract has documented since0.0.0-next-17055("Limits the layer to one OpenCode input mode. Use global to opt out; defaults to base."). Without it the layer registers but the palette never lists the command; every host built-in exposing a palette command declaresmode: "global".Related issue
None open — filed directly from a live V2 debugging session.
Changes
src/tui.ts: addregisterSlotV2(), which branches oncontext.ui.slot.length— one parameter means the claim-object host, two means the pre-17226 host. Both shipped generations were checked directly:0.0.0-next-17403implementsslot(K)(arity 1) and0.0.0-next-17055implementsslot(Q, X)(arity 2). It also tolerates a host that returns no disposer.src/tui.ts: addthemeColorV2()+goalColorsV2(), resolving the first candidate theme path that exists and descending into adefaultleaf when the node is a colour group. Nested paths are tried first, legacy flat keys remain as fallbacks, so one path serves both generations.element()already skipsundefinedprops, so an unresolved colour degrades to the inherited default rather than throwing.src/tui.ts:setupTuiV2()registers both slots through the adapter;GoalKeymapLayerV2()declaresmode: "global".test/tui-v2.test.ts: update the mock to the current host contract (arity-1slot, nested theme) and add coverage for the claim-object host, the positional fallback, a host returning no disposer,themeColorV2across nested/grouped/flat themes, and themode: "global"assertion.Only V2 code paths are touched: the V1 integration still uses
api.theme.currentwith flat keys and is unchanged.The old mock encoded the outdated contract (
slot(name, render),{ text, textMuted, primary }), which is why the suite stayed green against an API that no longer exists. Reverting just the call shape inregisterSlotV2now fails 8 tests in that file, where previously it failed none.Verification
Gates:
End to end against
opencode2 0.0.0-next-17403(claim-object host), plugin loaded from this working copy, isolatedOPENCODE_DBandOPENCODE_GOAL_STATE_PATH, driven in tmux:Goal/Status: active/Time: …/Tokens: …/Auto-continues: …/Goal set./ objective, andTime:ticks while the goal is activeGoal; selecting it opens the dialog with the objective plus Refresh / History / Pause / Clear, and Refresh prompts the agent/goal pause→Status: paused+Stop: paused;/goal complete …→Goal achieved (1:13), exercising thetext.feedback.successgroup descent-c; invoking the command with no session open toasts "Open a session before viewing goal state." instead of crashingsetupactivations during one bootColour resolution confirmed byte-exact by matching the pane's ANSI codes to the host theme: header
38;2;205;214;244=text.default, detail lines38;2;147;153;178=text.subdued,Goal achieved38;2;166;227;161=text.feedback.success.default.Re-run against
opencode2 0.0.0-next-17055(positional host) in a pinnedlinux/arm64container, configured the way that era expects —~/.config/opencode/tui.jsonwith a relative plugin path, sincecli.jsononly appears with the 17403-era migration. The adapter reportedslotArity: 2, took the positional branch,setupreturned a cleanup function, the palette listedGoal, and/goal …rendered the sidebar. (The preview CLI silently self-updates;OPENCODE_DISABLE_AUTOUPDATE=1is needed to keep an old preview under test.)slotarity0.0.0-next-174030.0.0-next-17055Checklist
bun testpasses (new behavior has regression coverage)bun run lintpassesbun run typecheckpassesbun run buildpasses anddist/server.jsis committed if server code changed — build is clean;src/server.tsand its imports are untouched (src/server.tsdoes not importsrc/tui.ts) and the rebuilt bundle is byte-identical, so there is nodistchange to commitNotes for review
slotvariadically would defeat it, and guessing the claim object on a legacy host fails silently whereas the reverse throws loudly.try { slot(claim) } catch { slot(name, render) }would key off the observable throw instead — happy to switch if you prefer.goalColorsV2is evaluated in the component body rather than inside the render closure, so a runtime theme switch will not restyle an open sidebar. That matches the previousconst theme = api.themebehaviour, so it is not a regression, but the host's built-ins read theme values inside their render effects and this could follow suit.registerSlotV2andthemeColorV2are exported for tests; both are reachable throughsetupTuiV2with two mock variants if you would rather not widen the published surface of./tui.context.ui.slotis needed because the pinned@opencode-ai/plugin-v2(npm:@opencode-ai/plugin@0.0.0-next-17055) still declares the positional signature. Bumping that pin past0.0.0-next-17226would let the claim-object call type-check directly.AI attribution
Written with the OpenCode 2 agent harness (
opencode2) using Anthropic Claude Opus 5. All verification above — unit gates, both live host runs, and the type-declaration bisection — was executed, and the diff was reviewed by the submitter before opening this PR.