Move the canonical OpenCode terminal application from
packages/opencode/src/cli/cmd/tui into a self-contained workspace package while
the legacy CLI and the new CLI continue to use the same implementation.
Target package:
packages/tui
name: @opencode-ai/tui
Target dependency graph:
packages/opencode ---\
> @opencode-ai/tui -> @opencode-ai/sdk
packages/cli --------/
The TUI may directly depend on terminal and UI infrastructure such as
@opentui/core, @opentui/solid, @opentui/keymap, solid-js, Effect, and
generic presentation libraries. It must not depend on packages/opencode,
packages/cli, or @opencode-ai/core.
The SDK is the TUI's OpenCode boundary. Missing backend data or operations must be added to the server API and generated SDK rather than imported from backend implementation modules.
packages/cli and synchronize two trees.packages/tui back
into packages/opencode.packages/opencode imports with @opencode-ai/core
imports merely to make the package compile.unknown input and metadata are acceptable; importing backend
tool implementations for type safety is not.@opencode-ai/tui.@opencode-ai/tui OwnsThe canonical implementation currently lives under:
packages/opencode/src/cli/cmd/tui
Its private dependency on packages/opencode is primarily expressed through
the @/* TypeScript alias, which resolves to packages/opencode/src/*.
@tui/* imports are internal to the TUI and are not themselves a package
boundary problem.
The main private dependency groups are:
@/util/*: presentation helpers plus filesystem/process/RPC helpers@/tool/*: backend tool implementations used by renderers@/session/*, @/provider/*, and @/reference/*: backend data and actions@/config/*: config discovery, parsing, variables, and plugin resolution@/plugin/*: plugin loading and installation@/cli/*: yargs adapters, network setup, errors, and CLI presentation@/server/*: authentication and embedded server behaviorGlobal.Path, Flag, and process environment readsThe initial extraction should reduce these dependencies in place before moving the application root.
Status: Completed. The private @opencode-ai/tui workspace package now has an
independent OpenTUI Solid JSX configuration, narrow root export, package-local
alias, and in-memory render smoke test. Neither CLI consumes the package yet.
Create packages/tui without moving the application root yet.
Tasks:
packages/tui/package.json with the name @opencode-ai/tui.tsconfig.json configured for OpenTUI Solid JSX.bunfig.toml with the OpenTUI Solid preload for package-local development
and tests.typecheck and package-local tests.@tui/*
is acceptable, but it must resolve entirely inside packages/tui.Exit criteria:
packages/tui typechecks independently.packages/tui.opencode, @opencode-ai/cli, or
@opencode-ai/core.Checkpoint commit:
feat(tui): add standalone package skeleton
Status: Completed. Presentation utilities, bundled themes and their pure theme
engine, keybinding/keymap mechanics, and low-coupling border, link, and spinner
primitives now live in @opencode-ai/tui. The legacy host consumes explicit
package exports and retains only integration wrappers or compatibility
re-exports where backend and process concerns have not moved yet.
Move low-coupling code first so subsequent team changes land in the new package without waiting for the application root migration.
Tasks:
packages/tui/src/util, including the
portions of locale, error display, record checks, duration formatting, and
small functional helpers used by TUI code.Filesystem, Process, Rpc, worker startup, or config discovery
as generic utilities in this section.Exit criteria:
@/... imports.packages/tui.Checkpoint commit:
refactor(tui): move presentation utilities and primitives
Status: Completed. Legacy and V2 tool renderers now dispatch on SDK wire names,
accept Record<string, unknown> input and metadata, and use local guards for
nested presentation data. Web-search labels and structured metadata extraction
are TUI-owned, unknown tools retain the generic fallback, and no TUI source
imports backend tool implementations. The route components remain in the legacy
tree until the SDK state and route move in Section 6.
Make tool rendering depend only on SDK wire data and local presentation logic.
Tasks:
@/tool/* in TUI routes and feature plugins.read, write,
edit, apply_patch, grep, glob, bash, question, and task.unknown at
the package boundary.packages/tui.Exit criteria:
@/tool/*.Checkpoint commit:
refactor(tui): decouple tool rendering from backend tools
Status: Completed for the shared runtime contract and legacy host. The TUI now
receives immutable launch-directory, path, capability, terminal/editor, startup,
and build inputs through @opencode-ai/tui/runtime. Movable app, component,
route, and feature-plugin code no longer reads OpenCode globals or process state;
command, config, plugin-loading, custom-theme discovery, editor/clipboard, and
Windows lifecycle adapters remain host-owned. packages/cli does not consume
this contract yet; that integration remains deferred to Section 9.
Replace process-global OpenCode state with resolved TUI inputs.
Define narrow inputs rather than one unstructured host object. Expected groups include:
type TuiCapabilities = {
mouse: boolean
copyOnSelect: boolean
terminalTitle: boolean
workspaces: boolean
showTimeToFirstDraw: boolean
}
type TuiPaths = {
home: string
state: string
config: string
data: string
}
type TuiBuildInfo = {
version: string
channel?: string
}
Tasks:
Flag, Global.Path, and relevant environment
variables in movable TUI code.packages/opencode adapters.packages/tui sensible host-neutral defaults only when behavior is truly
local to a terminal client.Exit criteria:
Flag or Global.Checkpoint commit:
refactor(tui): make runtime capabilities explicit
Status: Completed for the package config contract and legacy host adapter.
@opencode-ai/tui/config now owns schemas, defaults, keybind resolution, the
resolved config type, and the Solid config provider. The legacy host retains
file discovery, precedence, JSONC parsing, substitutions, migration,
source-relative sound paths, plugin origins, dependency installation, and
Effect services. packages/cli remains untouched until Section 9.
Move config semantics needed by rendering while retaining filesystem discovery and migration in the legacy host.
Tasks:
packages/tui.packages/opencode initially.packages/opencode.Exit criteria:
packages/tui does not import @/config/*.Checkpoint commit:
refactor(tui): separate config resolution from loading
Status: Completed for the SDK/domain boundary. SDK, project, event, legacy sync,
V2 sync, local model state, prompt persistence, and pure prompt helpers are now
canonical in @opencode-ai/tui. Configured references resolve through the new
generated reference.list SDK operation; prompt payloads rely on optional
server-assigned IDs; local attachment reads use the package platform contract.
Legacy route files remain in place until the plugin slot boundary and app-root
move, but their only private dependencies are plugin presentation or local host
adapters rather than OpenCode domain implementations.
Make the SDK the only OpenCode domain boundary used by the TUI.
Tasks:
packages/tui.@/session/*, @/provider/*, @/reference/*,
@/lsp/*, and other backend domains with SDK data or TUI-owned presentation
helpers../packages/sdk/js/script/build.ts, and consume the
generated SDK API.Exit criteria:
@opencode-ai/sdk.Checkpoint strategy:
This section may be split into multiple commits when an SDK gap is substantial. Each commit must leave both the old TUI host and package tests working. Suggested commit pattern:
feat(sdk): expose <operation> for tui clients
refactor(tui): move <area> to sdk boundary
Final section checkpoint:
refactor(tui): move sdk state and routes into package
Status: Completed. Plugin slots, route registration, TUI-facing APIs, runtime
presentation state, and built-in feature plugins now live in
@opencode-ai/tui. The legacy host injects a narrow plugin host that retains
discovery, installation, manifest/config mutation, external module execution,
pure-mode filtering, and cleanup ownership. Missing or failing plugin hosts
degrade to the base TUI without blocking startup.
Keep plugin UI extensibility without importing the legacy plugin installer and loader into the TUI package.
Tasks:
packages/tui or the existing public plugin TUI contract package.packages/tui; retain installation/loading
integration tests in packages/opencode.Exit criteria:
packages/tui does not import @/plugin/* or the old TUI plugin runtime.Checkpoint commit:
refactor(tui): separate plugin presentation from loading
Status: Completed. packages/tui now owns the canonical application root,
provider composition, routes, components, parser presentation, renderer
configuration, and renderer lifecycle. Process mutation, Windows console
handling, backend worker startup, config loading, plugin loading, native audio,
and legacy platform implementations remain injected host adapters. Old source
paths are temporary compatibility re-exports for the legacy command host.
Move the canonical app composition after its dependencies have already crossed the package boundary.
Tasks:
app.tsx, remaining providers, routes, components, attention handling,
keymaps, and renderer lifecycle to packages/tui.Export a narrow public API such as:
export type TuiInput = {
url: string
directory?: string
headers?: RequestInit["headers"]
fetch?: typeof fetch
config: TuiConfig.Resolved
capabilities: TuiCapabilities
paths: TuiPaths
}
export function run(input: TuiInput): TuiHandle
export function createRenderer(config: TuiConfig.Resolved): Promise<CliRenderer>
Preserve the existing lifecycle guarantees: readiness, waiting until exit, idempotent cleanup, renderer destruction, SIGHUP handling where appropriate, and terminal restoration.
Keep Windows process adapters outside the package if they mutate host process state; invoke them from CLI adapters around the package lifecycle.
Keep OpenTUI parser-worker embedding in executable build scripts.
Move app lifecycle and rendering tests to packages/tui.
Exit criteria:
packages/tui contains the canonical application root.packages/opencode, packages/cli, or
@opencode-ai/core.Checkpoint commit:
refactor(tui): move application root into package
Status: Completed. The legacy thread and attach commands now lazily invoke the
public @opencode-ai/tui root while retaining worker/server/config/plugin and
process adapters. The new CLI default command launches the same package against
its authenticated daemon transport with a minimal local platform/host. Missing
legacy provider/config APIs currently degrade to the shared provider-connect
screen; source and compiled new-CLI behavior match, while named commands remain
outside the TUI path.
Make both executable packages consume the same TUI package.
Tasks:
thread.ts and
attach.ts in packages/opencode.packages/opencode.@opencode-ai/tui API.packages/cli's default command handler to call the same public API.packages/cli/src/tui shell after the shared package is
integrated.Exit criteria:
packages/cli.Checkpoint commit:
refactor(cli): share tui package across command hosts
Status: Completed. Package source imports are self-contained, package exports
are narrowed to active host contracts, package-owned tests and snapshots live
under packages/tui, and the obsolete compatibility tree has been removed.
Legacy command, worker, config, plugin-loader, process, editor, audio, and event
adapters now live in explicit host-owned locations outside src/cli/cmd/tui/.
Delete migration scaffolding only after both hosts consume the package.
Tasks:
packages/opencode/src/cli/cmd/tui.@tui/* path mappings from packages/opencode.@opencode-ai/tui exports to intentional public entrypoints.Exit criteria:
packages/tui imports @/..., @opencode-ai/core, or either
executable package.Checkpoint commit:
refactor(tui): complete standalone package extraction
Run verification after every section, adding narrower tests for the area being moved.
Package checks:
cd packages/tui && bun typecheck
cd packages/tui && bun test
cd packages/opencode && bun typecheck
cd packages/cli && bun typecheck
Dependency checks:
rg "from ['\"]@/" packages/tui/src
rg '@opencode-ai/core|packages/opencode|packages/cli' packages/tui
rg 'src/cli/cmd/tui|@tui/' packages/opencode/src packages/opencode/test
SDK checks when server APIs change:
./packages/sdk/js/script/build.ts
git diff --check
Interactive smoke checks should run in tmux so the terminal can be captured
and cleaned up reliably:
Compiled checks:
packages/opencode binary.packages/cli binary.Update each section's status and this checklist in the same commit that completes the section.