feat: interactive command modals for the OpenCode TUI#76
Open
iceteaSA wants to merge 21 commits into
Open
Conversation
There was a problem hiding this comment.
iceteaSA has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.
There was a problem hiding this comment.
1 issue found across 14 files
Confidence score: 3/5
- In
packages/opencode/src/rpc/rpc-server.ts, the RPC body-size guard uses string length rather than byte length, so multibyte payloads can bypass the intended limit and increase DoS exposure if merged as-is—switch the check to a byte-based calculation (for example via Buffer byte length) and add a regression test before merging.
Architecture diagram
sequenceDiagram
participant User as TUI User
participant TUI as OpenCode TUI
participant TUICmd as command-dialogs.tsx
participant RPCClient as rpc-client.ts
participant RPCServer as rpc-server.ts
participant Notif as notifications.ts
participant PortFile as port-file.ts
participant Plugin as Plugin index.ts
participant Storage as Config Storage
participant Sidebar as Sidebar State
Note over User,Sidebar: Command Modal Flow (TUI connected)
User->>TUI: Types /claude-<command>
TUI->>Plugin: onCommand hook triggered
Plugin->>Plugin: buildDialogPayload()
Plugin->>Notif: isTuiConnected(sessionID)
alt TUI is polling
Notif-->>Plugin: true
Plugin->>Notif: pushNotification(payload, sessionID)
Plugin->>Plugin: cleanAbort() - abort command
else No TUI connected
Plugin->>Plugin: sendIgnoredMessage() - text fallback
Plugin->>Plugin: cleanAbort() - abort command
end
Note over TUI,RPCClient: TUI Poll Loop (~500ms)
TUI->>TUI: Poll interval fires
TUI->>RPCClient: pending(lastNotificationId, sessionId)
RPCClient->>PortFile: discoverPortFile(rpcDir)
PortFile-->>RPCClient: { port, token, pid }
RPCClient->>RPCServer: POST /rpc/pending-notifications (Bearer auth)
RPCServer->>Notif: drainNotifications(lastReceivedId, sessionId)
Notif-->>RPCServer: new notifications[]
RPCServer-->>RPCClient: { messages: [...] }
RPCClient-->>TUI: notifications[]
TUI->>TUI: Sort notifications by id
loop For each notification
TUI->>TUICmd: openCommandDialog(api, payload, applyFn)
alt quota command
TUICmd->>TUI: Show read-only quota modal
Note over TUI,Sidebar: Modal reuses AccountBlock components
TUI->>Sidebar: readStateFromFile()
Sidebar-->>TUI: quota data
else routing/fast/dump/cache commands
TUICmd->>TUI: Show selection/toggle dialog
User->>TUICmd: Makes selection
TUICmd->>RPCClient: apply({ command, arguments })
RPCClient->>RPCServer: POST /rpc/apply (Bearer auth)
RPCServer->>Plugin: apply(request)
Plugin->>Plugin: buildDialogPayload() with args
Plugin->>Storage: Persist config change
Storage-->>Plugin: Confirmed
Plugin-->>RPCServer: { text, knobs }
RPCServer-->>RPCClient: ApplyResult
RPCClient-->>TUICmd: result
TUICmd->>TUI: Show toast and close dialog
else cachekeep command
TUICmd->>TUI: Show prompt dialog
User->>TUICmd: Enters window value
TUICmd->>RPCClient: apply({ command, arguments })
Note over RPCClient,RPCServer: Same apply flow as above
RPCClient-->>TUICmd: result
TUICmd->>TUI: Show toast and close dialog
else killswitch command
TUICmd->>TUI: Show selection dialog (enable/disable/edit)
alt edit thresholds
User->>TUICmd: Selects edit
TUICmd->>TUI: Show prompt dialog for thresholds
User->>TUICmd: Enters threshold values
else toggle
User->>TUICmd: Selects on/off
end
TUICmd->>RPCClient: apply()
Note over RPCClient,RPCServer: Same apply flow
RPCClient-->>TUICmd: result
TUICmd->>TUI: Show toast and close dialog
end
end
Note over TUI,RPCClient: TUI Heartbeat
loop Every ~500ms
TUI->>RPCClient: pending(0, sessionId)
RPCClient->>RPCServer: POST /rpc/pending-notifications
RPCServer->>Notif: drainNotifications(0, sessionId)
Notif->>Notif: Record drain timestamp
Notif-->>RPCServer: []
RPCServer-->>RPCClient: { messages: [] }
RPCClient-->>TUI: []
Note over Notif: Updates isTuiConnected window
end
Note over RPCServer,PortFile: RPC Server Lifecycle
Plugin->>Plugin: Start RPC server on plugin init
Plugin->>RPCServer: startRpcServer(options)
RPCServer->>RPCServer: Generate bearer token
RPCServer->>RPCServer: Listen on 127.0.0.1:0
RPCServer->>PortFile: writePortFile(dir, { port, token, pid })
PortFile-->>RPCServer: File written
RPCServer-->>Plugin: { port, token, stop }
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
There was a problem hiding this comment.
iceteaSA has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.
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
In the OpenCode TUI, the seven
/claude-*slash commands now open interactive modal dialogs instead of posting a text reply. Applying a change in a modal persists it through the same configuration the slash arguments already use, so a modal and the typed command (e.g./claude-routing fallback-first) are equivalent. Outside the OpenCode TUI (OpenCode desktop or headless) the commands print their text summary exactly as before, and Pi is unaffected (separate plugin, no TUI)./claude-quota/claude-routing/claude-fast/claude-dump/claude-cache/claude-cachekeepHH-HH) oroff/claude-killswitch5h,1wthreshold editorHow it works
The OpenCode server process and the TUI run as separate processes, so the command hook (server side) can't open a dialog (TUI side) directly. This adds a small loopback RPC bridge:
127.0.0.1, ephemeral port, bearer-authenticated, body-size capped) started from the plugin's server entry.0600) under a per-project path in the OpenCode state dir, discovered by the TUI via live-pid matching. Override withOPENCODE_ANTHROPIC_AUTH_RPC_DIR.executePersistent*setters.The command hook pushes a dialog notification when a TUI is connected, otherwise falls back to the existing ignored-message text path; either way it aborts the command so the slash template never reaches the model. The
/claude-quotamodal reuses the sidebar'sAccountBlockcomponents fed by the same state file + theme, so it matches the sidebar's bars/pacing and respects the same section prefs.Testing
rpc-notifications,rpc-port-file,rpc-server(queue scoping/eviction, port-file atomicity + dead-pid pruning, bearer 401/200, oversized-body rejection, the apply route).format,lint,typecheck,build, and the full test suite — no regressions.Notes
/claude-*command still emits a singleerr_… failedline in the OpenCode log, because the command hook aborts via a thrown sentinel (the existing mechanism — same as the prior text-mode commands and as other plugins do). The command works correctly; silencing that log cleanly is a possible follow-up.Need help on this PR? Tag
/codesmithwith what you need. Autofix is disabled.Summary by cubic
Adds interactive modals for the seven
/claude-*commands in the OpenCode TUI; changes made in a modal persist the same as typed slash args. Adds a localhost, bearer‑auth RPC bridge so the server can trigger TUI dialogs; outside the TUI, commands still print text summaries.New Features
OPENCODE_ANTHROPIC_AUTH_RPC_DIR.src/rpc/*and dialog modules; docs updated.Bug Fixes
Written for commit 6c6ecfc. Summary will update on new commits.