Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

README.md

@conciv/extension

The conciv extension-authoring contract: defineExtension/defineTool plus the SolidJS runtime context and typed useSlot/useContext hooks.

Part of conciv. Author an extension under conciv/extensions/*.tsx in your app:

import {defineExtension} from '@conciv/extension'

export default defineExtension({
  name: 'my-extension',
  // tools, slots, context…
})

Browser-bodied tools (defineTool().client(body))

An extension can declare registry tools whose bodies run in the browser (where the widget is mounted, with access to live client state, the DOM, or a framework fiber) and reach them from its server half through the tool registry. There is no page-only vocabulary: a browser capability is an ordinary registry tool with a fully qualified name, dispatched to the page by that name over the {requestId, name, input} wire.

Declare once, bind twice

A declaration is registry-grade when it carries meta (with a summary) and an outputSchema. Keep the declaration in a shared module; the server half registers it with a body-less .client() (core forwards calls to the page), and the client half binds the browser body with .client(body):

// shared/defs.ts
import {z} from 'zod'
import {defineTool} from '@conciv/extension/tool'

export const routerStateDef = defineTool({
  name: 'demo.routerState',
  description: 'read the live router path off the page',
  inputSchema: z.object({}),
  outputSchema: z.object({path: z.string()}),
  meta: {summary: 'read the live router path off the page', category: 'demo'},
})
// server.ts — declaration only; the registry forwards the call into the browser
import {defineExtension} from '@conciv/extension'
import {routerStateDef} from './shared/defs.js'

export default defineExtension({name: 'demo', tools: [routerStateDef.client()]}).server((server) => {
  async function currentPath() {
    const state = await server.tools.call('demo.routerState', {})
    return state
  }
  return {context: {currentPath}}
})
// client.ts — the browser body, collected off the mounted extension instance
import {defineExtension} from '@conciv/extension'
import {routerStateDef} from './shared/defs.js'

export default defineExtension({
  name: 'demo',
  tools: [routerStateDef.client(() => ({path: location.pathname}))],
}).client(() => ({value: {}}))

Bodies are (input, ctx): the input arrives schema-validated, and the dispatcher builds ctx per call — ctx.document, a lazy ctx.target(locator) that resolves a {ref, selector, name} locator (and fires the action mirror when the declaration sets meta.mirrors), a non-throwing ctx.resolve(locator), ctx.addRef/ctx.resetRefs for snapshot refs, and ctx.consoleEntries(). Return a plain JSON-serializable record; declare meta.mutating: true and the call is journaled and prompts for user approval on every surface.

Only tools declared by extensions the widget actually mounted are dispatchable — a failed client mount contributes no browser tools.

Every failure is a declared registry error

server.tools.call goes through the tool registry, and the registry converts every page failure into one of the tool's declared transport errors before rejecting — a typed oRPC error whose code is uppercase. By the time your catch runs there is no PageVerbError left to guard for; match on the declared code instead:

Declared code Meaning
NO_PAGE_CLIENT No widget is connected, so the body cannot run in any browser.
PAGE_TIMEOUT A widget is connected but never replied within the page-bus timeout.
UNKNOWN_TOOL No mounted extension declares a client tool by that name.
INVALID_ARGS The arguments failed the tool's zod schema, or the target is missing.
HANDLER_ERROR The body threw, or returned a non-JSON-serializable value.

A toolError(code) thrown by the browser body whose code the declaration lists under errors is rebuilt as that declared error instead of HANDLER_ERROR.

function declaredCode(error: unknown): string | null {
  if (typeof error !== 'object' || error === null || !('code' in error)) return null
  return typeof error.code === 'string' ? error.code : null
}

try {
  await server.tools.call('demo.routerState', {})
} catch (error) {
  if (declaredCode(error) === 'NO_PAGE_CLIENT') {
    // degrade gracefully — nothing is looking at the page
  }
}

isPageVerbError still exists, but it only fires on the raw page seam — server.page.call, the unwrapped browser ask that skips the registry (used, for example, by a server handler that wraps its own client body). There the rejection is a lowercase-coded PageVerbError (no-widget, timeout, unknown-verb, invalid-args, handler-error).

Loading / error card contract

When a tool's execute awaits server.tools.call, the tool part stays in its running state until the call resolves or rejects, so the card renders a loading state and then a result — or, if the call rejects, an error card (the rejection propagates out of execute and surfaces as the tool part's output-error state). A failed page verb never renders as a green success. Do not catch and swallow a PageVerbError inside execute if you want the failure reflected in the card; let it reject.