Skip to content

fix(tui): reload local plugin dependency graphs - #47388

Merged
kitlangton merged 6 commits into
v2from
plugin-graph-reload
Sep 5, 2026
Merged

kitlangton merged 6 commits into
v2from
plugin-graph-reload

Conversation

@kitlangton

@kitlangton kitlangton commented Sep 4, 2026 •

Copy link
Copy Markdown
Contributor

Why

Editing a local CLI plugin's imported helper leaves its old UI running. Touching the entrypoint does not fix it: the entrypoint gets a fresh import identity, but its dependencies remain cached. Refactors that introduce a new helper export can consequently fail with a missing-export error even though the export exists on disk.

Reproduced on the PR base before implementation, first through Host.load and then in the real TUI with a nested helper.

What Changes

Local source change Before After
Edit an imported sibling or nested helper Old UI remains Reload the plugin with fresh helper code
Rename an export and update its importer Cached export or failed replacement Load the matching new graph
Save identical contents No reload Still no reload
Introduce invalid code, then repair it Last-good fallback Preserve last-good fallback and reload on repair

Native reload, not a second module system

  • Track and watch the resolved local source files, including imports outside the entrypoint directory. Hash their contents before deciding whether a generation changed. The source owner returns the loaded module and version together, rather than making callers treat a version token as an import URL.
  • On Bun, use its scanner only to discover statically resolvable local dependencies. Invalidate those native module-cache entries and import the unchanged source normally. Do not bundle plugins, rewrite imports, synthesize metadata, or add compiler dependencies.
  • Keep bare package dependencies and host runtimes outside invalidation, including symlinked/workspace packages. Native loading preserves conditional exports, optional fallbacks, wildcard/live exports, import attributes, source metadata, and stack filenames.
  • On Node, propagate a private generation query parameter through local ESM resolution hooks, preserving existing query values/fragments and the requested node_modules boundary before symlink resolution.
  • Load the backend only on a local-source cache miss. Cache evaluation failures as well as successes, so identical failures cannot repeat import-time side effects on unrelated watch events. Explicitly invalidate failed native cache keys when the source changes, even though Bun does not enumerate them.
  • Retain serialized reconciliation, cleanup, unchanged-generation no-ops, the post-load content recheck, and the existing last-good registration fallback. Node resolution hooks are released when the TUI plugin host shuts down.

The earlier bundling approach was removed: local-plugin development does not need an independent module linker or immutable snapshots of every old callback's dependencies.

Demo

chat-before-after.mp4

Matched OpenCode Drive recordings with a demo workspace-check plugin at session.composer.top, below the chat transcript and above the composer. Left: base b274224af1. Right: final reviewed head 91dc9af065. Both use the same 100×40 project, conversation, and source edits without a TUI restart; the panel was also exercised at 80 and 120 columns. The provider response and check statuses are fixture data, not real model calls or check-command executions. Startup and waiting are trimmed; each of three matched checkpoints shows 0.7 seconds of recording followed by a 2.5-second final-frame hold: helper edit, export rename, and repair. Assertions cover visible status, exact setup/cleanup counts, identical-save no-ops, and last-good fallback during invalid edits.

Scope

Local CLI plugin source reloads only. This does not change server plugin reloads or package update behavior. Statically discoverable relative/file imports, including literal dynamic imports and free literal require() calls, are tracked. Computed imports/requires and explicitly bound createRequire() calls retain native runtime behavior rather than becoming statically watched dependencies.

This is best-effort development reload, not generation-isolated execution. A failed replacement keeps the previous registration, but its deferred imports/requires can observe the current cache and files. Restart only the TUI if a broken edit leaves a plugin inconsistent. Shared mutable local helpers are not a cross-plugin state contract; use an explicit shared package or host API for that state.

Verification

Using the repository's Bun 1.4.1:

cd packages/tui
bun run test test/plugin-source.test.ts
bun run test
bun typecheck

cd ../cli
bun typecheck
OPENCODE_CHANNEL=local bun run script/build.ts --single --skip-install --skip-web-ui
  • Focused suite: 58 passed, including conditional exports, optional/computed/aliased requires, require.resolve, lexical bindings, source metadata, wildcard/live re-exports, query/text import attributes, failed-evaluation recovery, cold/warm deferred imports, computed-only tracking exclusion, cycles, folded imports, nested package resolution, JSX helpers, export renames, identical/reverted contents, host and symlinked-package identity (including relative node_modules paths), atomic saves, and a real Node subprocess matrix covering plain/query/fragment imports and shared package identity.
  • Full TUI suite: 1,292 passed, 4 skipped, 0 failed.
  • TUI and CLI typechecks pass, as does the pre-push workspace typecheck: 33 tasks passed. Formatting and git diff --check pass.
  • Rebuilt macOS ARM64 executable, driven locally in an isolated instance: conditional exports, computed requires, user-bound loaders, wildcard/live exports, query/text attributes, source metadata and optional fallbacks all load natively. Nested-helper edits reload, syntax errors preserve the old UI, and repairs succeed. Symlinked package state remains shared across reloads, including a relative node_modules import. A same-ID setup failure restores the previous registration; an identical repeat is a no-op; repair succeeds. Final setup/cleanup counts are 8/7.

@kitlangton
kitlangton merged commit c72b535 into v2 Sep 5, 2026
10 checks passed
@kitlangton
kitlangton deleted the plugin-graph-reload branch September 5, 2026 03:41
jinhuang712 pushed a commit to jinhuang712/opencode that referenced this pull request Sep 26, 2026
Reload statically discovered local plugin dependencies through native module caches while preserving shared package identities and best-effort last-good registrations.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant