Skip to content

Expose host context via useHostContext; theme becomes a selector#6

Merged
mgoldsborough merged 4 commits intomainfrom
host-context-workspace
Apr 25, 2026
Merged

Expose host context via useHostContext; theme becomes a selector#6
mgoldsborough merged 4 commits intomainfrom
host-context-workspace

Conversation

@mgoldsborough
Copy link
Copy Markdown
Contributor

Summary

  • Unify ext-apps host context as a single source of truth in the SDK; theme becomes a typed selector over it instead of parallel state.
  • New public API: synapse.getHostContext() / onHostContextChanged() and React useHostContext<T>().
  • useTheme() is unchanged for callers — internally it's a one-line useMemo selector. onThemeChanged now filters no-op fires (e.g. when only workspace changes).
  • Bumps to 0.6.0.

Motivation

The previous host-context-changed handler discarded every field except theme/styles. Iframe apps had no way to react to other parts of the spec-typed host context — displayMode, toolInfo, or host-published extensions like NimbleBrain's workspace. This blocked the host-side fix for "briefing doesn't refresh on workspace switch": even with the host pushing workspace updates, the SDK never surfaced them.

This is the portable SDK piece. NimbleBrain-specific vocabulary stays out of the SDK — useHostContext<T>() returns the spec-typed McpUiHostContext (open record with [key: string]: unknown); first-party apps narrow it themselves ({ workspace?: { id: string } }). Other hosts populate whatever fields make sense for them.

Design

One piece of state (currentHostContext: McpUiHostContext) feeding one callback set (hostContextCallbacks). The host-context-changed handler replaces (per spec — notification carries a full snapshot, not a delta) and fires subscribers. getTheme() derives via extractTheme(currentHostContext). onThemeChanged() wraps a host-context subscription with a themesEqual guard so theme consumers only re-render when the derived theme actually moves.

useTheme() keeps its public signature; reach for useHostContext() when you need anything else from the host context.

Test plan

  • bun test — 258/258 passing (10 new in core.test.ts covering replace semantics, unsubscribe, destroy, theme-fires-only-on-theme-change)
  • bunx tsc --noEmit clean
  • bun run lint — no new warnings introduced
  • bunx biome format clean

Unify ext-apps host context as a single source of truth in `createSynapse`.
Previously theme had its own state (`currentTheme`) and callback set
(`themeCallbacks`) populated from `ui/notifications/host-context-changed`,
which discarded every other field. That meant the SDK had no way to react
to non-theme host-context updates — workspace, displayMode, host
extensions — even though the spec carries them on the same notification.

Now there's one piece of state (`currentHostContext: McpUiHostContext`)
and one callback set. `getTheme()` / `onThemeChanged()` are selectors
over it, with `themesEqual` filtering no-op fires so existing theme
subscribers don't see spurious updates when only workspace changes.

New public API:
  - synapse.getHostContext() / onHostContextChanged()
  - useHostContext<T>() React hook (typed via `McpUiHostContext`)

`useTheme()` is unchanged for callers; internally it's now a one-line
useMemo selector over useHostContext.

This is the SDK half of the workspace-aware host context plumbing —
needed so iframe apps (e.g. NimbleBrain's home briefing) can react to
workspace switches without remounting. Bumps to 0.6.0.
Two correctness issues from review on PR #6:

1. useTheme bypassed the themesEqual filter. Building it on
   useHostContext meant every host-context-changed produced a new
   ctx reference, which propagated through useState and forced a
   re-render even when the derived theme was unchanged. Defeats the
   whole point of the selector. Route useTheme through
   synapse.onThemeChanged so React consumers inherit the SDK-level
   filter.

2. onThemeChanged could miss the handshake fire for subscribers
   added before synapse.ready resolves. `prev = extractTheme({})`
   at subscribe time, then on the handshake fire `next =
   extractTheme(ctx)` — when the host's hostContext derives to the
   default theme (light, no tokens) the wrapper silently filtered
   it as a no-op. Use `hostInfo === null` as the discriminator:
   pre-handshake subscribers seed `prev = null` (first fire always
   fires); post-handshake subscribers seed `prev = current theme`
   (workspace-only updates correctly filter).

Also:
- New `react/host-context-hooks.test.tsx` covers useHostContext
  re-renders on host-context-changed and useTheme does NOT re-render
  on workspace-only changes — locks in the fix from #1.
- New core.ts test covers the handshake-fire regression from #2.
- One-line clarifier on themesEqual explaining why iterating only
  `a`'s keys is sufficient under equal length.
Pre-refactor, `detectHost` populated `HostInfo.theme` and `core.ts`
read it once on handshake to seed the SDK's `currentTheme`. After
unifying state on `currentHostContext`, theme is derived via
`extractTheme(currentHostContext)` and `HostInfo.theme` was no longer
read anywhere — internally or externally (zero references in the
host repo or any bundle).

Drop the field rather than leave it as a deprecated remnant:
- `HostInfo` reports identity only (host name, protocol version,
  isNimbleBrain). Theme is host-context state, not host-identity.
- `detectHost` becomes leaner — no theme computation, no DEFAULT_THEME
  closure path.
- Theme tests in `detection.test.ts` move to a new `describe("extractTheme")`
  block that exercises the function directly. They were testing the
  wrong layer before — `detectHost` was a pass-through to `extractTheme`.
- Sort imports in host-context-hooks.test.tsx so `bun run ci` (which
  runs `bun run lint`) passes the assist/source/organizeImports rule.
  Confirmed by the existing CI script in package.json.
- Call out HostInfo.theme removal under a new ### Breaking section in
  the 0.6.0 CHANGELOG entry. HostInfo is publicly exported, so dropping
  a field from it is a breaking change for any external consumer reading
  hostInfo.theme — even at pre-1.0.
@mgoldsborough mgoldsborough added the qa-reviewed QA review completed with no critical issues label Apr 25, 2026
@mgoldsborough mgoldsborough merged commit 14ee5e9 into main Apr 25, 2026
5 checks passed
@mgoldsborough mgoldsborough deleted the host-context-workspace branch April 25, 2026 02:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

qa-reviewed QA review completed with no critical issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant