Skip to content

feat(gui-extensions): add typed composition and lifetime primitives - #52868

Merged
Hona merged 17 commits into
anomalyco:v2from
Hona:extension-sdk-primitives
Oct 4, 2026
Merged

Hona merged 17 commits into
anomalyco:v2from
Hona:extension-sdk-primitives

Conversation

@Hona

@Hona Hona commented Oct 2, 2026 •

Copy link
Copy Markdown
Member

Typed composition, without an Effect runtime

  • Built-ins declare dependencies and stored state; types reject missing providers, duplicate providers, conflicting keys, and window Ipcs without a main provider.
  • Hosts activate extensions in parallel. The dependency graph checks composition; it does not serialize startup.
  • Solid owners govern window lifetimes; main uses a small Scope. Registrations withdraw with their owner.
flowchart LR
  Manifest[Extension.define] --> Compose[Extension.compose]
  Compose --> Window[Window host]
  Compose --> Main[Main host]
  Main -- "Ipc: methods / state / events" --> Window
  Window -- "Contract: Live dependencies" --> Extensions[Other extensions]
  Window --> Points[Commands / panels / settings / embeds]
Loading

Author API

export default Extension.define({
  id: "pairing",
  provides: { pairing: Pairing }, // also readable as ctx.uses.pairing
  stores: {
    keepScreenActive: Store.main(Schema.Boolean, false, {
      state: ["opencode.settings", "keepScreenActive"],
    }),
  },
  i18n: { en },
})

const setup: Setup<typeof definition> = (ctx) => {
  ctx.add(Command, {
    id: "open",
    get title() {
      return ctx.t("command.title")
    },
    run: () => ctx.layout.settings("pairing"),
  })
}
Before After
Grab-bag App / MainApp tokens Lazy context properties: ctx.build, ctx.layout, ctx.locale, ctx.serverEndpoints, …
Optional values whose absence has no reason Declared ctx.uses with Live: pending, active, inactive
Hand-written provider and async lifetimes createKeyed, createLatest, createVisitState
Remote, Service, Native, Surface Ipc, Contract, Desktop, Embed
Session objects whose identity follows the route Stable MountedSession; route-following actions live on ctx.screen.current()
Ad hoc reads before desktop storage loads Declared, preloaded stores; queued mutations; typed legacy imports
Four teardown forms Window onCleanup; main ctx.scope.addFinalizer
  • MenuItem is discriminated by menu; dialogs return their own close handles.
  • Host APIs return defaults and queue writes before the interface mounts.
  • PanelTab flags describe independent behaviours; Layout.open takes { tab, background }.
  • summary → details and usage → context, with storage, keybind and panel-key migrations. Settings and What's New move to their owning extensions; existing copy stays unchanged.

Enforcement and guide

  • Author guide: diagrams, API catalogs and source-checked walkthroughs of shipped plugins, including pairing.
  • TSDoc on the public SDK; sdk-docs rejects undocumented exports, members and parameters, and guide examples that drift from their source.
  • GUI files changed by a PR must have zero Oxlint problems, including existing warnings. Raw Solid effects are banned outside the SDK.
  • CI runs the whole app component suite, including the composition disable matrix and real-host lifetime contracts.

Behaviour differences

  • Updater, WSL and SSH actions report unavailable providers instead of silently doing nothing.
  • Browser reconnects when compatibility returns; its picker follows the routed session.
  • Leaving a session abandons its pending btw question, restoring the pre-extension behaviour.
  • Failed development reloads keep the last good revision.
  • The crash page can render an initialization error with data: null.
  • Stored-value replacement and removal prevent ignored updates and legacy-data resurrection.

No intentional layout or copy changes.

Performance

Production builds, v2 d113a40f55a vs this PR. All runs pinned to P-cores.

Scenario v2 (ms) PR (ms)
Home: first settings shortcut handled 258 264
Restore: composer shown 428 440
Restore: last timeline row shown 535 542
New session from Home: ready 225 229
Cold session from Home: ready 455 460
New session from a session: ready (GPU compositing, traced) 76 77
  • The first five rows have 3 samples per side.
  • The last row has 5 traces per side. With software compositing, a compositor stall of about 50 ms after each new-session draw affects both builds the same way, and made 3-sample medians unreliable.
  • The extension host costs under 0.5 ms per session switch.

Hona added 9 commits October 2, 2026 13:36
…a pure main host core

Phase 1 of the extension SDK overhaul. Built-ins are unchanged; the old shapes stay until they migrate.

- Renderer: Live<T> for declared dependencies, createActive (with otherwise), createLatest, createVisitState, declared Store.app/Store.session with app stores preloaded before setup and queued writes, SessionView.visit, registrations that follow the current Solid owner and dispose when late, layout writes held until the session is located, requires gating, and an error boundary per contribution.
- Typing: Extension.define with provides/uses/requires/stores, Setup<typeof Def> exposing only declared dependencies, and Extension.compose failing to compile on a missing or duplicate provider (RemotesProvided checks renderer remotes against the main composition).
- Main: a lifecycle core without Electron (queue, generations, withdrawal, cleanup deadline, kept-scope handoff, last good revision on a failed reload, stall log) behind a thin Electron adapter, plus Scope with Effect's names and MainApp.restart(handoff, { keep }).
- Tests: table tests per primitive, type-level composition tests, the extension graph test, lifecycle tables and a seeded lifecycle fuzz test.
- Lint: bun run lint:changed fails when a touched GUI package file has any oxlint problem, including the warn-level anti-slop rules; CI runs it on pull requests.
…and lifetime primitives

Phase 2 of the extension SDK overhaul.

- Every built-in declares provides / uses / requires / stores and reads dependencies as Live values; actions that need an optional dependency answer while it is inactive.
- About 88 raw effects replaced: createActive for side work per provider generation or identity, memos for derived values, createLatest for async data, createVisitState for per-visit state, handlers for user intent.
- Review and summary state live in declared stores (same keys and legacy imports); session-store writes made before load are queued; session stores keep their own session identity.
- Layout.sidebar.opened() replaces review's mirrored sidebar preference; Remote.ref declares a remote by id with type-only imports, so the browser's pane remote adds no schemas to startup.
- src/renderer.ts and src/main.ts export Extension.compose; builtins.typecheck.ts fails to compile when a renderer remote has no main provider; the graph test covers every declared optional edge.
- Deprecated shapes and Live.legacy are removed, and oxlint bans createEffect, createRenderEffect and createComputed in extension code outside src/sdk.
- Solid computations created without an owner in tab-popover and the composer editor are created under their component; the btw keeper fails on the warning.
Remote becomes Ipc (IpcProvider for what main holds), Service becomes Contract, Host becomes HostApi, Native becomes Desktop, Surfaces becomes Embeds, the Link, Menu, Menubar, Status and Setting points become LinkHandler, MenuItem, MenubarItem, TitlebarItem and SettingsPage, createActive becomes createKeyed, SessionView becomes MountedSession, MainStorage becomes Storage, MainServer becomes ServerEndpoint and Store.app becomes Store.global. The renderer App splits into Build, Locale, Appearance, Router, Keybinds, Servers and Workspaces; main's MainApp into Build, Servers, Lifecycle and Log. Host files and identifiers follow the SDK names, and app code calls the side and dock open state a region. Stored keys, DOM ids and copy are unchanged.
…al shapes

Host APIs are lazy properties on the setup context and useExtension() (ctx.layout, ctx.storage, ctx.build, ...); ctx.use is removed and declared dependencies are read only through ctx.uses and ctx.requires, with ctx.uses.pane.load for Ipc.ref. Layout.open takes { tab, background }; PanelTab uses pinned, draggable and closable flags. Build has one shape in both processes and main's endpoints API is ServerEndpoints. Each process has one teardown and one signal (onCleanup and ctx.signal in the window, ctx.scope in main). Main storage returns Persisted. System.open becomes System.openExternal and the shell.bottom slot becomes window.bottom. IPC channels, payload fields and the browser embed event use the SDK names. Stored keys, DOM ids and copy are unchanged.
…to their owners

- The summary extension is now details (command details.toggle) and usage is now context (panel key context:main). Stored state, keybinds and panel keys migrate from every older id, and a migration test table proves each mapping.
- The Preferences host API is removed. The updater owns releaseNotes and What's New (seen versions migrate from highlights.v1); review owns mobile diff wrap through a SettingsPage section in the general page. Both import once from settings.v3 and render in the same place with the same copy.
- PanelTab.transient names the launcher behaviour; closable only chooses the close button.
- Persisted.update applies a returned value in both processes, and main stores a decoded copy.
- storage.remove no longer lets a store re-import from its from source.
- Store from accepts a list of older keys.
- windows-menu.tsx creates its computations under its component owner.
Pins when the dialog shows, paging, media, Get started, Don't show these in the future, the failed-request retry, and the upgrade rows for highlights.v1 and settings.v3 releaseNotes.
Every exported SDK declaration, member, option field and parameter has TSDoc, and script/sdk-docs.ts (run by bun run lint) fails on a missing one, a stale @PARAM, an unknown ctx member in the guide, or a guide code block that differs from src/example. README.md is the author guide; src/example is a small two-process extension that CI typechecks and tests and that never ships. AGENTS.md is rewritten for the final API.
The extension graph matrix, the real-host extension tests, the example extension and the browser pane tests live in packages/app/component-tests and ran only locally.
- Provides are readable through ctx.uses without a second declaration; conflicting keys are compile errors.
- MountedSession retains one session's identity and data. Screen-wide files, comments and composer live under ctx.screen.current(), which explicitly follows the route.
- Reactive panel and slot inputs receive new sessions without extra remounts. Session stores no longer need to pin route-following identities.
- MenuItem is discriminated by menu. Dialogs.open returns its own close handle. Host APIs return defaults and queue writes before the interface mounts.
- Store.main declares main-process stores alongside window stores, with typed legacy sources and process-specific ctx.stores.
- Remove the unused app slot, honor every file-search option, and allow informational titlebar items without actions.
- Keep the browser picker on the routed session and abandon pending btw questions when leaving it.
- Update the author guide, example, TSDoc and contract tests together.
@Hona
Hona requested a review from Brendonovich as a code owner October 2, 2026 23:15
Copilot AI balanced review requested due to automatic review settings October 2, 2026 23:15

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Hona added 2 commits October 3, 2026 09:26
Keep upstream Git initialization, browser geometry transition tracking, unused-code cleanup and version updates while preserving typed composition, context properties and screen-owned extension models.
Remove the non-shipped example extension and its test. The guide walks through pairing and other built-ins, with code blocks checked against their source declarations.
Hona added 2 commits October 3, 2026 10:52
Clear the file store before effects can load files from a new directory, so a file tab opened during a cross-folder session switch keeps its content. Reset keybinds by deleting the key, so migrated shortcuts leave no stale row.
A dialog opened during startup now waits for the app interface, like other early host writes. This keeps What's New above a restored Settings screen and keeps keyboard focus inside it. Closing a waiting dialog cancels it.
Hona added 4 commits October 4, 2026 11:09
Release SSH resources as each is acquired, follow re-authenticated server data in the browser model, share one main storage cache per key, clear stale createLatest errors, restrict main provide to declared Ipcs, add createKeyed equals for object sources, and derive the review mode instead of overwriting the stored choice.
Give a moved session a new MountedSession for its new directory, cancel dialogs closed before their deferred open, bind session stores to the live session ref, keep older store homes readable when a pick finds nothing, and import main storage from the first source the schema accepts.
…sion stores

Attach host APIs only once every extension has settled and the routes render, so early writes and dialogs land after the interface's first render. Keep the routed session's stores and stores no tab has listed yet when pruning closed tabs. Report ctx.screen as unavailable in extension stories.
The pull_request event's base SHA can predate the base branch commits GitHub merges into the checked-out ref, so lint:changed counted upstream edits as the pull request's. Diff against the merge commit's first parent instead.
@Hona
Hona merged commit 0048c97 into anomalyco:v2 Oct 4, 2026
4 checks passed
Ichinose-Kazuki pushed a commit to Ichinose-Kazuki/opencode that referenced this pull request Oct 7, 2026
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.

2 participants