Scoped Context

A scoped context is a namespaced view of the context: it auto-prefixes every RPC id, shared-state key, and streaming channel with your tool's id, and adds a typed, persisted settings store, used from a single tool's code.

A scoped context is a namespaced view of the context: it auto-prefixes every RPC id, shared-state key, and streaming channel with your tool's id, and adds a typed, persisted settings store, used from a single tool's code.

Node side

setup(ctx) receives the full DevframeNodeContext; scope it with ctx.scope(id), conventionally your devframe id:

import { defineDevframe, defineRpcFunction } from 'devframe'

export default defineDevframe({
  id: 'my-plugin',
  name: 'My Plugin',
  setup(ctx) {
    const my = ctx.scope('my-plugin')

    my.rpc.register(defineRpcFunction({
      name: 'get-modules', // bare name: stored as `my-plugin:get-modules`
      type: 'query',
      handler: () => loadModules(),
    }))
  },
})

declare function loadModules(): { id: string }[]

ctx.scope(id) is stable per id, re-exposing unscoped APIs (views, diagnostics, agent, host, cwd, mode), swapping in the auto-namespaced rpc, and keeping the original as my.base.

Browser side

(await connectDevframe()).scope(id) gives the matching view: my.rpc carries call / callEvent / callOptional, register, sharedState, and streaming, plus my.settings.

Auto-namespacing

Bare names are prefixed <namespace>: (call('get-modules')my-plugin:get-modules); a name with : passes through unchanged to another tool (call('other-plugin:status')).

register accepts only bare names; an already-namespaced one throws DF0034; use ctx.base.rpc.register.

Bare names stay typed: call('get-modules') resolves to your RPC registry entry, sharedState('selection') to the matching DevframeRpcSharedStates key.

Settings

my.settings is a persisted key-value store (alongside my.rpc), with two scopes:

  • project: per-checkout values, under the workspace storage dir.
  • global: per-user values, under the global storage dir.

Both are file-backed and synced to the browser over the shared-state protocol; a set propagates to peers, surviving restarts.

const { settings } = my

await settings.project.set('theme', 'dark')
await settings.project.get('theme') // => 'dark'
await settings.project.all() // => theme is 'dark'
await settings.project.delete('theme')

const off = await settings.global.onChange((value) => {
  console.log('global settings changed', value)
})

Every method is async; the store resolves on first access.

Typed settings

Augment DevframeSettingsRegistry to type a namespace's settings once; the scope types settings.global and settings.project:

declare module 'devframe' {
  interface DevframeSettingsRegistry {
    'my-plugin': {
      theme: 'light' | 'dark'
      recentFiles: string[]
    }
  }
}
const my = ctx.scope('my-plugin')
await my.settings.project.set('theme', 'dark') // ✓ typed
await my.settings.project.set('theme', 'blue') // ✗ not assignable

Unaugmented namespaces fall back to an open record.

What's next