Skip to content

feat(cli): add opencode as a first-class interactive TUI harness#253

Merged
senamakel merged 5 commits into
tinyhumansai:mainfrom
senamakel:feat/opencode-tui-wrapper
Jul 13, 2026
Merged

feat(cli): add opencode as a first-class interactive TUI harness#253
senamakel merged 5 commits into
tinyhumansai:mainfrom
senamakel:feat/opencode-tui-wrapper

Conversation

@senamakel

Copy link
Copy Markdown
Member

What & why

opencode was a first-class HarnessProvider only for the headless daemon (opencode run --format json) — the interactive TUI wrapper (tinyplace codex / tinyplace claude) never supported it. The blocker: opencode keeps every session in a single SQLite DB, so the file-polling HarnessSessionTailer fundamentally cannot observe a live interactive session (as the old PROFILES.opencode comment noted).

This brings opencode to interactive-TUI + daemon parity with codex/claude by wrapping it over its HTTP server's SSE /event bus instead of files: the wrapper starts a headless opencode serve, subscribes to /event, and spawns opencode attach <url> in the PTY so the human's session and the observer share one server. Bus frames fold into the existing v1/v2 envelope pipeline.

How

  • SessionEnvelopeEmitter (harness-wrapper.ts) — extracted the v1-message + v2-typed-event + status emit machinery out of HarnessSessionTailer (composition, parameterized by an EmitIdentity) so a non-file source reuses identical envelope framing/publish/status. Behavior-preserving — the existing tailer delegates to it.
  • opencodeEventsFromBusEvent + createOpenCodeBusMapper (harness-events.ts) — map opencode SSE bus frames (message.part.updated / message.updated / session.*) into the shared HarnessSemanticEvent model, with tool + text dedup across the repeated part snapshots. The daemon's flat-shape opencodeEventsFromLine is untouched.
  • opencode-source.ts (new) — startOpenCodeServer (free ephemeral port, resolves on the server.connected frame / listening line, retry-once, SIGTERM→SIGKILL teardown) + OpenCodeEventSource (Node-fetch SSE loop, filters the global stream to the session whose directory === cwd, emits v1/v2, status heartbeat, flush-on-stop). Both take injectable spawn/connect seams for tests.
  • Wrapper + TUI wiringrunHarnessCommand boots the server, bridges via the SSE source, launches opencode attach <url>, and tears the server down in a finally. The interactive TUI gains an opencode profile (serverMode), home-menu entry, and server/bridge lifecycle. tinyplace opencode dispatches like codex/claude (bare→TUI, --raw→wrapper, --agent→plugin).

Testing

  • tsc build + typecheck: 0 errors.
  • New unit tests: bus mapper (tool/text dedup, roles, flush), OpenCodeEventSource (v1/v2 emit, session filtering, foreign-session drop, flush), startOpenCodeServer (port/readiness/teardown/retry via injected spawn+connect), and tinyplace opencode dispatch/parse.
  • Refactor is behavior-preserving — existing harness-wrapper-v2 / resume-tail / envelope tests stay green.
  • Opt-in live smoke (TINYPLACE_OPENCODE_SMOKE=1) drives a real opencode serve turn end-to-end and asserts an agent_message envelope — run locally against opencode 1.17.18 (confirmed text parts carry time.end, as the fixtures assume).

Scope / follow-ups

  • Resume is out of scope for opencode (the home-resume pane is file-discovery based; opencode always launches fresh). Documented as a follow-up.
  • cursor/windsurf/openclaw/hermes are untouched.

🤖 Generated with Claude Code

https://claude.ai/code/session_011nJsktzaVmQ1VnsYjzditQ

senamakel and others added 5 commits July 13, 2026 11:04
…mapper

Factor the v1/v2/status emit machinery out of HarnessSessionTailer into a
reusable SessionEnvelopeEmitter (parameterized by EmitIdentity) so a non-file
session source can share envelope framing/publish/status. Add
opencodeEventsFromBusEvent + createOpenCodeBusMapper mapping opencode's SSE
`/event` bus frames (message.part.updated / message.updated / session.*) into
the shared HarnessSemanticEvent model, with tool/text dedup across snapshots.

No behavior change to the file tailer (existing v2/resume/envelope tests green).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011nJsktzaVmQ1VnsYjzditQ
Add OpenCodeEventSource + startOpenCodeServer: start a headless `opencode
serve` on a free port, subscribe to its `/event` SSE bus, filter to the
session whose directory matches cwd, and emit the shared v1/v2 envelopes via
SessionEnvelopeEmitter. runHarnessCommand launches `opencode attach <url>` in
the PTY so the human's session and the bridge share one server; teardown kills
serve in a finally. Non-TTY/opencode falls back to a plain spawn with a note.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011nJsktzaVmQ1VnsYjzditQ
Extend the TUI (TinyVerseAgentKind, home menu, buildAgentProfile with a
serverMode marker) so `tinyplace opencode` runs like codex/claude. In
serverMode the TUI boots `opencode serve`, prepends `attach <url>` to the
launch, bridges the live session via OpenCodeEventSource (not the file tailer),
and tears the server down on exit. Add runOpencodeCommand + the opencode
dispatch/help entry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011nJsktzaVmQ1VnsYjzditQ
…patch

Unit tests for createOpenCodeBusMapper (tool/text dedup, roles, flush),
OpenCodeEventSource (v1/v2 emit, session filtering, foreign-session drop),
startOpenCodeServer (port/readiness/teardown/retry via injected spawn+connect),
and the opencode CLI dispatch/parse. Add an opt-in live smoke
(TINYPLACE_OPENCODE_SMOKE) that drives a real `opencode serve` turn end-to-end
and asserts an agent_message envelope — verified locally against opencode
1.17.18 (text parts carry time.end, as the fixtures assume). Also harden the
server's readiness wait to surface spawn errors (ENOENT).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011nJsktzaVmQ1VnsYjzditQ
The earlier commits ran the website prettier over edited SDK files, which
rewrapped unrelated pre-existing lines (the SDK is hand-wrapped and is not
prettier-managed — CI only formats the website). Restore those lines to their
original form so the diff contains only the opencode changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011nJsktzaVmQ1VnsYjzditQ
@vercel

vercel Bot commented Jul 13, 2026

Copy link
Copy Markdown

@senamakel is attempting to deploy a commit to the Vezures Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 2 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b6c408ae-a557-4b82-be8a-59a8fd7d087f

📥 Commits

Reviewing files that changed from the base of the PR and between 39ab493 and 6de4c8f.

📒 Files selected for processing (12)
  • sdk/typescript/src/cli/codex.ts
  • sdk/typescript/src/cli/commands.ts
  • sdk/typescript/src/cli/harness-events.ts
  • sdk/typescript/src/cli/harness-wrapper.ts
  • sdk/typescript/src/cli/index.ts
  • sdk/typescript/src/cli/opencode-source.ts
  • sdk/typescript/src/cli/tui.ts
  • sdk/typescript/tests/harness-events.test.ts
  • sdk/typescript/tests/opencode-cli.test.ts
  • sdk/typescript/tests/opencode-server.test.ts
  • sdk/typescript/tests/opencode-smoke.test.ts
  • sdk/typescript/tests/opencode-source.test.ts

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6de4c8f1bd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +288 to +290
launch = {
command: config.agentBin,
args: ["attach", opencodeServer.url, ...config.agentArgs],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Don't pass TUI args through opencode attach

When a user supplies normal OpenCode TUI args such as a project path, --model, or --agent (also via TINYPLACE_OPENCODE_ARGS in the TUI path), this wrapper appends them to opencode attach. The current OpenCode CLI docs list --model/--agent under opencode [project], while attach [url] only documents --dir, resume/session/fork, and auth flags (https://opencode.ai/docs/cli/#attach), so common invocations like tinyplace opencode --model provider/model will fail or be ignored instead of starting the requested OpenCode session.

Useful? React with 👍 / 👎.

Comment on lines +87 to +91
export async function runOpencodeCommand(
argv: Array<string>,
options: TinyPlaceCliOptions = {},
): Promise<TinyPlaceCliResult> {
return runHarnessAgentCommand("opencode", argv, options);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject opencode agent mode until the plugin supports it

This enables tinyplace opencode --agent, but the unified plugin it delegates to has no OpenCode adapter: sdk/plugin-tinyplace/mcp/harness.mjs registers only claude, codex, cursor, windsurf, and mock. Because the plugin ignores an unknown --harness opencode override and falls back to auto-detection/default Claude, users choosing the new OpenCode agent mode will launch the wrong harness rather than an OpenCode session.

Useful? React with 👍 / 👎.

Comment on lines +403 to +406
const response = await fetch(url, {
signal,
headers: { accept: "text/event-stream" },
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Authenticate the opencode SSE fetch when env enables auth

If the user has OPENCODE_SERVER_PASSWORD set, opencode serve enables HTTP Basic auth, and opencode attach can read that password from the env according to the OpenCode CLI docs (https://opencode.ai/docs/cli/#serve and https://opencode.ai/docs/cli/#attach). This direct /event fetch only sends Accept, so in that environment the server returns 401 and the bridge silently stops observing/publishing the session even though the attached TUI can still run.

Useful? React with 👍 / 👎.

@senamakel
senamakel merged commit 061e994 into tinyhumansai:main Jul 13, 2026
9 of 10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant