@abide/abide 0.106.0 → 0.108.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # @abide/abide
2
2
 
3
+ ## 0.108.0
4
+
5
+ ### Minor Changes
6
+
7
+ - fda92c3: every MCP tool call writes a debug line on abide:mcp, and every assistant conversation one on abide:assistant ([`6c4d8c7`](https://github.com/briancray/abide/commit/6c4d8c745491308c0208eb18e900473fb8ee9e03))
8
+
9
+ ### Patch Changes
10
+
11
+ - fda92c3: a back or forward that reloads no longer writes the departed page's scroll into the entry being loaded ([`294c0e6`](https://github.com/briancray/abide/commit/294c0e6ed806c38e80a1f872e7be6ac5579c7382))
12
+ - fda92c3: a site holds a dropped bridge pairing for 60 seconds, past when the bridge notices a silent drop ([`ac8e1d3`](https://github.com/briancray/abide/commit/ac8e1d313b7ea54414cd1034b56eb4cfa88ab31b))
13
+ - fda92c3: bind:value on a <select> selects an option added after the value was written ([`cd592b2`](https://github.com/briancray/abide/commit/cd592b2ccdc617c5cfed2a6acf5f5a458bc9986b))
14
+ - fda92c3: markup the client parses for a raw value, an {#if} arm or a {#for} row inside an <svg> or <math> is parsed in that namespace ([`f8ba81e`](https://github.com/briancray/abide/commit/f8ba81e94e0583858811e3c403535c0c0632b87c))
15
+
16
+ ## 0.107.0
17
+
18
+ ### Minor Changes
19
+
20
+ - bd2a74f: Breaking: an assistant announces a tool call as the model starts writing it, and a site may ask the visitor's Claude Code for built-in tools ([`c959804`](https://github.com/briancray/abide/commit/c959804fd19c3e99fe439f3f1120f1ef13f42ff8))
21
+
22
+ ### Patch Changes
23
+
24
+ - bd2a74f: a bind:element callback is handed a connected node whose blocks and components have built, and bind:element on a component is a build error ([`e15fbee`](https://github.com/briancray/abide/commit/e15fbee6f7c780dab2105ca245dd79dbb4a2e317))
25
+
3
26
  ## 0.106.0
4
27
 
5
28
  ### Minor Changes
package/docs/REGISTRY.md CHANGED
@@ -321,9 +321,9 @@ type Gated<Gate, Accepted> = [Schema<Accepted>] extends [Gate] ? never : Failed<
321
321
  | --- | --- | --- | --- |
322
322
  | `assistant` | `(messages: AssistantMessage[], provider: Provider, options?: AssistantOptions) => AsyncGenerator<AssistantEvent>` | A conversation with a model, as the events it produces. | 44.1, 44.2, 44.3, 44.4, 44.21 |
323
323
  | `AssistantMessage` | `{ role: 'user' \| 'assistant'; content: string }` | One turn of the transcript a caller sends. | 44.3, 44.4 |
324
- | `AssistantEvent` | `{ type: 'text'; text: string } \| { type: 'tool'; id: string; name: string; input: unknown } \| { type: 'result'; id: string; text: string; failed: boolean } \| { type: 'done'; stop: string; usage: { input: number; output: number } }` | What a conversation produces: text, a tool call, its result, and the end. | 44.2, 44.11 |
324
+ | `AssistantEvent` | `{ type: 'text'; text: string } \| { type: 'tool'; id: string; name: string } \| { type: 'input'; id: string; input: unknown } \| { type: 'result'; id: string; text: string; failed: boolean } \| { type: 'done'; stop: string; usage: { input: number; output: number } }` | What a conversation produces: text, a tool call as it starts, its input once written, its result, and the end. | 44.2, 44.11, 44.29 |
325
325
  | `AssistantOptions` | `{ mcp?: boolean; maxTurns?: number }` | What `assistant` takes beside a provider. | 44.6, 44.8, 44.11 |
326
- | `Provider` | `{ run(messages: AssistantMessage[], tools: Toolbox, limits: { maxTurns: number; signal: AbortSignal }): AsyncGenerator<AssistantEvent> }` | What runs a conversation. | 44.2 |
326
+ | `Provider` | `{ run(messages: AssistantMessage[], tools: Toolbox, limits: { maxTurns: number; signal: AbortSignal }): AsyncGenerator<AssistantEvent> }` | What runs a conversation. | 44.2, 44.29 |
327
327
  | `Toolbox` | `{ list: ToolDefinition[]; call(name: string, input: unknown): Promise<{ text: string; failed: boolean }> }` | The app's MCP tools, called as the asker. | 44.6, 44.7 |
328
328
  | `ToolDefinition` | `{ name: string; description: string; inputSchema: unknown; annotations: { readOnlyHint: boolean; destructiveHint: boolean; idempotentHint: boolean }; outputSchema: unknown }` | A tool as the app's MCP surface lists it. | 20.14, 20.16, 44.6 |
329
329
 
@@ -338,8 +338,8 @@ type Gated<Gate, Accepted> = [Schema<Accepted>] extends [Gate] ? never : Failed<
338
338
 
339
339
  | Name | Signature | Meaning | Rules |
340
340
  | --- | --- | --- | --- |
341
- | `claudeCode` | `(config?: ClaudeCodeConfig) => Provider` | The visitor's own Claude Code, reached through the bridge paired with their browser, as a provider. | 44.5, 44.13, 44.17, 44.18, 44.19, 44.24, 44.25, 44.28 |
342
- | `ClaudeCodeConfig` | `{ model?: string; fallbackModel?: string; systemPrompt?: string; appendSystemPrompt?: string; effort?: 'low' \| 'medium' \| 'high' \| 'xhigh' \| 'max' }` | The Claude Code options a site may set for a turn. | 44.16 |
341
+ | `claudeCode` | `(config?: ClaudeCodeConfig) => Provider` | The visitor's own Claude Code, reached through the bridge paired with their browser, as a provider. | 44.5, 44.13, 44.17, 44.18, 44.19, 44.24, 44.25, 44.28, 44.31 |
342
+ | `ClaudeCodeConfig` | `{ model?: string; fallbackModel?: string; systemPrompt?: string; appendSystemPrompt?: string; effort?: 'low' \| 'medium' \| 'high' \| 'xhigh' \| 'max'; maxBudgetUsd?: number; betas?: string[]; disallowedTools?: string[]; tools?: string[] }` | The Claude Code options a site may set for a turn, and the built-in tools it asks the visitor for. | 44.16, 44.30 |
343
343
  | `claudeCode.bridgeCommand` | `() => { command: string; expires: number }` | The command that pairs a bridge with this browser. | 44.14 |
344
344
  | `claudeCode.bridge` | `() => AsyncGenerator<BridgeStatus>` | This browser's pairing, as it changes. | 44.20, 44.22 |
345
345
  | `BridgeStatus` | `{ paired: boolean; command: string \| undefined }` | Whether a bridge is paired, and the command that pairs one where none is. | 44.20 |
@@ -464,7 +464,8 @@ type Gated<Gate, Accepted> = [Schema<Accepted>] extends [Gate] ? never : Failed<
464
464
  | `abide:lifecycle` | `debug` `error` | A lifecycle that ran. | 28.8 |
465
465
  | `abide:principal` | `debug` `warning` | A principal set or cleared. | 28.8 |
466
466
  | `abide:health` | `warning` | An `onHealth` that threw or answered a non-object. | 25.2, 28.8 |
467
- | `abide:mcp` | `debug` `warning` | A handler withheld from MCP, or a tool published with no description. | 20.3, 20.9, 28.8 |
467
+ | `abide:mcp` | `debug` `warning` | A handler withheld from MCP, a tool call answered, or a tool published with no description. | 20.3, 20.9, 20.18, 28.8 |
468
+ | `abide:assistant` | `debug` | One line per conversation, once it ends. | 44.32 |
468
469
  | `abide:openapi` | `warning` | A handler whose schema had no JSON Schema export. | 20.11, 28.8 |
469
470
  | `abide:render` | `warning` | An `error.abide` that itself failed. | 28.8 |
470
471
  | `abide:refuse` | `warning` | A message formatter that threw. | 15.13, 28.8 |
@@ -527,7 +528,7 @@ type Gated<Gate, Accepted> = [Schema<Accepted>] extends [Gate] ? never : Failed<
527
528
  | --- | --- | --- |
528
529
  | `{expr}` | Reactive text, escaped. | 32.1, 32.23 |
529
530
  | formatting whitespace | What a run of whitespace renders as. | 33.17, 33.18 |
530
- | `{raw(...)}` | Raw HTML. | 32.2, 32.25, 32.26, 32.30, 32.31 |
531
+ | `{raw(...)}` | Raw HTML. | 32.2, 32.25, 32.26, 32.30, 32.31, 32.70 |
531
532
  | `name={expr}` | A reactive attribute or property. | 32.3, 32.20, 32.23, 32.37 |
532
533
  | `on<event>={fn}` | A native listener on an element, and an ordinary prop on a component. | 32.4 |
533
534
  | `name="…{expr}…"` | An interpolated quoted value. | 32.3 |
@@ -537,7 +538,7 @@ type Gated<Gate, Accepted> = [Schema<Accepted>] extends [Gate] ? never : Failed<
537
538
  | `bind:open` | The same, on a `<details>`. | 32.7 |
538
539
  | `bind:group` | Radio or checkbox membership. | 32.8 |
539
540
  | `bind:value={{get, set}}` | Two-way bind over an explicit accessor pair, on an element or a component. | 32.5, 32.6 |
540
- | `bind:element={Reactive<Element> \| ((element: Element) => void \| Disposer)}` | A node reference. | 32.9, 32.38, 32.66 |
541
+ | `bind:element={Reactive<Element> \| ((element: Element) => void \| Disposer)}` | A node reference. | 32.9, 32.38, 32.66, 32.68, 32.69 |
541
542
  | `class:name={cond}` | Toggles a class. | 32.10 |
542
543
  | `style:prop={value}` | Sets one style property. | 32.10, 32.19 |
543
544
  | `{...expr}` | Spreads props or attributes. | 32.11, 32.37, 33.12, 33.13 |
@@ -547,15 +548,15 @@ type Gated<Gate, Accepted> = [Schema<Accepted>] extends [Gate] ? never : Failed<
547
548
 
548
549
  | Name | Signature | Meaning | Rules |
549
550
  | --- | --- | --- | --- |
550
- | `raw` | `(value: unknown) => Raw` | The branding a value takes to render as markup rather than as text. | 32.2, 32.24, 32.26, 32.30, 32.31 |
551
+ | `raw` | `(value: unknown) => Raw` | The branding a value takes to render as markup rather than as text. | 32.2, 32.24, 32.26, 32.30, 32.31, 32.70 |
551
552
  | `Raw` | `class Raw { html: string }` | What `raw` hands back. | 32.24 |
552
553
 
553
554
  ### Control flow
554
555
 
555
556
  | Spelling | Branches | Meaning | Rules |
556
557
  | --- | --- | --- | --- |
557
- | `{#if cond}` | `{:else if cond}`, `{:else}` | Conditional markup. | 32.12, 32.21, 32.63, 32.64, 32.65, 41.24 |
558
- | `{#for item, index of list by key}` | — | Repeats markup over a list, or over a cursor as it produces. | 32.13, 32.14, 32.15, 32.22, 32.29, 32.32, 32.33, 32.63, 32.64, 32.67, 41.8, 41.14, 41.15 |
558
+ | `{#if cond}` | `{:else if cond}`, `{:else}` | Conditional markup. | 32.12, 32.21, 32.63, 32.64, 32.65, 32.70, 41.24 |
559
+ | `{#for item, index of list by key}` | — | Repeats markup over a list, or over a cursor as it produces. | 32.13, 32.14, 32.15, 32.22, 32.29, 32.32, 32.33, 32.63, 32.64, 32.67, 32.70, 41.8, 41.14, 41.15 |
559
560
  | `{#try}` | `{:catch e}`, `{:finally}` | A render-time boundary and a region. | 32.16, 32.56, 32.63, 32.64 |
560
561
 
561
562
  ### Components
package/docs/RULEBOOK.md CHANGED
@@ -1312,6 +1312,11 @@ where that schema is an object, and MUST declare none otherwise.
1312
1312
  20.17 A `tools/call` that `/__abide/mcp` answers for a tool declaring an `outputSchema` MUST carry
1313
1313
  the value as `structuredContent` beside its text, and a refused call MUST carry none.
1314
1314
 
1315
+ 20.18 Every tool call the MCP surface answers MUST write one `debug` line on `abide:mcp` naming the
1316
+ tool, the status it was answered with and how long it took, whether `/__abide/mcp` or a `Toolbox`
1317
+ made the call — a call naming a tool it does not publish answered 404, and one whose answer threw
1318
+ 500.
1319
+
1315
1320
  # 21. Generated headers
1316
1321
 
1317
1322
  21.1 Every response MUST carry `x-content-type-options: nosniff`.
@@ -1941,6 +1946,7 @@ each one a list holds under 32.62.
1941
1946
 
1942
1947
  32.62 A `bind:value` on a `<select multiple>` MUST hold a list: each option is selected where the list
1943
1948
  holds its value as text, and a change MUST write the values of the selected options, in order.
1949
+ On any `<select>`, an option added after the value was written MUST be selected where it holds it.
1944
1950
 
1945
1951
  32.63 A `{#if}`, `{#for}`, `{#try}` or `{#component Name(pattern)}` not closed by its own close tag
1946
1952
  before the end of the file, or before the close tag of the element it was opened in, MUST be a build
@@ -1959,6 +1965,15 @@ markup.
1959
1965
  32.67 A `{#for}` MUST keep the row and the nodes of a key it keeps when that key's item is a
1960
1966
  different value, and MUST update the row's bindings that read the item in place.
1961
1967
 
1968
+ 32.68 A `bind:element` value MUST be handed its element after a region's build has put that element
1969
+ in the region, and after every block and component inside the element has built, so a callback
1970
+ reading layout, querying its descendants or calling `focus()` acts on a connected, whole node.
1971
+
1972
+ 32.69 `bind:element` on a component MUST be a build error.
1973
+
1974
+ 32.70 Markup the client parses for a slot holding a value branded by `raw`, a `{#if}` arm or a
1975
+ `{#for}` row MUST be parsed as the document's parser parses it inside the element it goes into.
1976
+
1962
1977
  # 33. Props
1963
1978
 
1964
1979
  33.1 `props` MUST be callable from a `<script>` block only, and calling it from a `<script module>`
@@ -3060,10 +3075,13 @@ and MUST refuse 409 where none is paired.
3060
3075
  44.14 A code `claudeCode.bridgeCommand` mints MUST pair one bridge with the browser that asked for
3061
3076
  it, once, and MUST NOT pair once it has expired.
3062
3077
 
3063
- 44.15 A bridge MUST run Claude Code with its built-in tools off, no setting source read and no MCP
3064
- server but the bridge's own, in an empty directory removed when the run ends.
3078
+ 44.15 A bridge MUST run Claude Code with no MCP server but the bridge's own, in an empty directory
3079
+ removed when the run ends, and with no built-in tool and no setting source the visitor did not pass
3080
+ it after `--`.
3065
3081
 
3066
- 44.16 A bridge MUST take from the site no Claude Code option `ClaudeCodeConfig` does not name.
3082
+ 44.16 A bridge MUST lay each option a site sets but `tools` (44.30) on as Claude Code's flag of that
3083
+ name, MUST fail a turn whose config sets any option `ClaudeCodeConfig` does not name, and MUST lay
3084
+ an option the visitor passed after `--` over the site's.
3067
3085
 
3068
3086
  44.17 An MCP request a bridge's Claude Code makes MUST be answered from the `Toolbox` of the turn it
3069
3087
  was made in.
@@ -3088,9 +3106,9 @@ its lines MUST be relayed.
3088
3106
  carrying it was not taken by `/__abide/bridge`, or where no reply arrived within 120 seconds.
3089
3107
 
3090
3108
  44.25 A bridge whose stream from `/__abide/bridge` ends unasked MUST redial it with its pairing's
3091
- token, each wait twice the last, until it is refused or 30 seconds have passed.
3109
+ token, each wait twice the last, until it is refused or 60 seconds have passed.
3092
3110
 
3093
- 44.26 `/__abide/bridge` MUST hold a pairing whose stream dropped for 30 seconds, and a `GET` bearing
3111
+ 44.26 `/__abide/bridge` MUST hold a pairing whose stream dropped for 60 seconds, and a `GET` bearing
3094
3112
  its token within them MUST resume the stream with every frame the dropped one had not sent.
3095
3113
 
3096
3114
  44.27 `/__abide/bridge` MUST send a comment on a pairing's stream every 15 seconds for as long as
@@ -3098,3 +3116,17 @@ the stream is open.
3098
3116
 
3099
3117
  44.28 A bridge whose stream from `/__abide/bridge` carries no line for 45 seconds MUST end it and
3100
3118
  redial it as 44.25 says.
3119
+
3120
+ 44.29 A `Provider` MUST yield a tool call's `tool` event when the model starts writing the call,
3121
+ and its `input` event, with the same `id`, once the call is written.
3122
+
3123
+ 44.30 A built-in tool MUST be available to a turn only where the site's `tools` names it and the
3124
+ visitor's `--tools` does, MUST be approved without asking unless the visitor passed `--allowedTools`,
3125
+ and the bridge MUST say once each tool the site asked for that the visitor did not grant.
3126
+
3127
+ 44.31 A bridge stopped by `SIGTERM`, `SIGINT` or `SIGHUP` MUST end every Claude Code run it started
3128
+ before it exits.
3129
+
3130
+ 44.32 A conversation MUST write one `debug` line on `abide:assistant` when its events end, naming
3131
+ its stop reason, the transcript's length, the tool calls it made, its usage and how long it took,
3132
+ and naming it `unfinished` where it ended without a `done`.
package/docs/abide.md CHANGED
@@ -5130,7 +5130,7 @@ test('a model lists the handler as a tool, and calls it', async () => {
5130
5130
  })
5131
5131
  ```
5132
5132
 
5133
- Every handler the app publishes is a tool, a read as much as a write. The model picks a tool by its `description`, the same sentence the OpenAPI operation carries, so write one on every handler. A tool with none warns on `abide:mcp`.
5133
+ Every handler the app publishes is a tool, a read as much as a write. The model picks a tool by its `description`, the same sentence the OpenAPI operation carries, so write one on every handler. A tool with none warns on `abide:mcp`, and with `DEBUG=abide:mcp` every call writes a line naming the tool, its status and how long it took.
5134
5134
 
5135
5135
  The endpoint runs inside the app's own middleware. The rung that decides what a browser may call decides what a model may call, and a declared refusal reaches the model by its name, so it can tell the person why it stopped. Read on: [Who sees what](/guide/decide-who-sees-what.md)
5136
5136
 
@@ -5253,8 +5253,11 @@ const events = reactive(() =>
5253
5253
  {#for event of events}
5254
5254
  {#if event.type === 'tool'}
5255
5255
  <li><span>tool call</span>
5256
+ <p class="font-mono text-chrome">{event.name}</p></li>
5257
+ {:else if event.type === 'input'}
5258
+ <li><span>with input</span>
5256
5259
  <p class="font-mono text-chrome">
5257
- {event.name} {JSON.stringify(event.input)}
5260
+ {JSON.stringify(event.input)}
5258
5261
  </p></li>
5259
5262
  {:else if event.type === 'result'}
5260
5263
  <li><span>your handler answered</span>
@@ -5286,7 +5289,8 @@ const recorded: Provider = {
5286
5289
  )
5287
5290
  const name = tool?.name ?? ''
5288
5291
  const input = { name: 'Ada' }
5289
- yield { type: 'tool', id: 'toolu_1', name, input }
5292
+ yield { type: 'tool', id: 'toolu_1', name }
5293
+ yield { type: 'input', id: 'toolu_1', input }
5290
5294
  const answered = await tools.call(name, input)
5291
5295
  yield { type: 'result', id: 'toolu_1', ...answered }
5292
5296
  yield { type: 'text', text: 'Ada Lovelace is at Analytical Engines.' }
@@ -5343,10 +5347,11 @@ test('the model calls the CRM search, through the app', async () => {
5343
5347
  | --- | --- |
5344
5348
  | `AssistantMessage` | One turn of the transcript, `{ role, content }`. The last is the user's. |
5345
5349
  | `AssistantOptions` | `mcp` to offer the tools, and `maxTurns`, 10 by default, to cap the model's turns in one call. |
5346
- | `AssistantEvent` | What the page reads: `text`, `tool`, `result`, then `done` with the stop reason and token usage. The same events from every provider. |
5350
+ | `AssistantEvent` | What the page reads: `text`, `tool` as a call starts, `input` once it is written, `result`, then `done` with the stop reason and token usage. The same events from every provider. |
5347
5351
  | `Provider` | What runs the conversation: one method, `run`. Write one for another model, or a recording for a test. |
5348
5352
  | `Toolbox` | What a provider is handed: `list` of the tools, and `call` to run one as the asker. |
5349
5353
  | `ToolDefinition` | One tool in `list`: its name, description and input schema. |
5354
+ | `abide:assistant` | The log namespace that writes one `debug` line per conversation once it ends: the stop reason, the transcript's length, the tool calls, the tokens and the time. |
5350
5355
 
5351
5356
  ## `claudeApi` sends the request body you wrote
5352
5357
 
@@ -5620,7 +5625,13 @@ test('unpaired: asking is refused, the bridge shows a command', async () => {
5620
5625
  })
5621
5626
  ```
5622
5627
 
5623
- `claudeCode` sends the conversation to Claude Code on the asker's machine, on their own sign-in and plan. Its config, `ClaudeCodeConfig`, is Claude Code's own option names: `model`, `fallbackModel`, `systemPrompt`, `appendSystemPrompt` and `effort`. Its tools, permissions and directory are not the site's to set.
5628
+ `claudeCode` sends the conversation to Claude Code on the asker's machine, on their own sign-in and plan. Its config, `ClaudeCodeConfig`, is Claude Code's own option names, each passed as its flag: `model`, `fallbackModel`, `systemPrompt`, `appendSystemPrompt`, `effort`, `maxBudgetUsd`, `betas` and `disallowedTools`. Permissions, directories and settings are not the site's to set.
5629
+
5630
+ `tools` asks for Claude Code's built-in tools, and the visitor decides. A tool runs only where they granted it after the command's `--`, and the bridge tells them about any it was asked for and not given. Anything else after `--` goes to Claude Code as written, and overrides what the site set.
5631
+
5632
+ ```
5633
+ bunx @abide/bridge <site> <code> -- --tools WebSearch,WebFetch
5634
+ ```
5624
5635
 
5625
5636
  The visitor pairs once per visit by running a command, which dials out to the site. Asking with nothing paired is refused 409.
5626
5637
 
@@ -7699,7 +7710,7 @@ Generated surfaces: what is written in place rather than imported.
7699
7710
 
7700
7711
  | Name | Signature | Meaning | Rules |
7701
7712
  | --- | --- | --- | --- |
7702
- | `AssistantEvent` | `{ type: 'text'; text: string } \| { type: 'tool'; id: string; name: string; input: unknown } \| { type: 'result'; id: string; text: string; failed: boolean } \| { type: 'done'; stop: string; usage: { input: number; output: number } }` | What a conversation produces: text, a tool call, its result, and the end. | [44.2](/rulebook#44.2), [44.11](/rulebook#44.11) |
7713
+ | `AssistantEvent` | `{ type: 'text'; text: string } \| { type: 'tool'; id: string; name: string } \| { type: 'input'; id: string; input: unknown } \| { type: 'result'; id: string; text: string; failed: boolean } \| { type: 'done'; stop: string; usage: { input: number; output: number } }` | What a conversation produces: text, a tool call as it starts, its input once written, its result, and the end. | [44.2](/rulebook#44.2), [44.11](/rulebook#44.11), [44.29](/rulebook#44.29) |
7703
7714
 
7704
7715
  ## Taught in
7705
7716
 
@@ -7735,7 +7746,7 @@ Generated surfaces: what is written in place rather than imported.
7735
7746
 
7736
7747
  | Name | Signature | Meaning | Rules |
7737
7748
  | --- | --- | --- | --- |
7738
- | `Provider` | `{ run(messages: AssistantMessage[], tools: Toolbox, limits: { maxTurns: number; signal: AbortSignal }): AsyncGenerator<AssistantEvent> }` | What runs a conversation. | [44.2](/rulebook#44.2) |
7749
+ | `Provider` | `{ run(messages: AssistantMessage[], tools: Toolbox, limits: { maxTurns: number; signal: AbortSignal }): AsyncGenerator<AssistantEvent> }` | What runs a conversation. | [44.2](/rulebook#44.2), [44.29](/rulebook#44.29) |
7739
7750
 
7740
7751
  ## Taught in
7741
7752
 
@@ -7825,7 +7836,7 @@ Generated surfaces: what is written in place rather than imported.
7825
7836
 
7826
7837
  | Name | Signature | Meaning | Rules |
7827
7838
  | --- | --- | --- | --- |
7828
- | `claudeCode` | `(config?: ClaudeCodeConfig) => Provider` | The visitor's own Claude Code, reached through the bridge paired with their browser, as a provider. | [44.5](/rulebook#44.5), [44.13](/rulebook#44.13), [44.17](/rulebook#44.17), [44.18](/rulebook#44.18), [44.19](/rulebook#44.19), [44.24](/rulebook#44.24), [44.25](/rulebook#44.25), [44.28](/rulebook#44.28) |
7839
+ | `claudeCode` | `(config?: ClaudeCodeConfig) => Provider` | The visitor's own Claude Code, reached through the bridge paired with their browser, as a provider. | [44.5](/rulebook#44.5), [44.13](/rulebook#44.13), [44.17](/rulebook#44.17), [44.18](/rulebook#44.18), [44.19](/rulebook#44.19), [44.24](/rulebook#44.24), [44.25](/rulebook#44.25), [44.28](/rulebook#44.28), [44.31](/rulebook#44.31) |
7829
7840
  | `claudeCode.bridgeCommand` | `() => { command: string; expires: number }` | The command that pairs a bridge with this browser. | [44.14](/rulebook#44.14) |
7830
7841
  | `claudeCode.bridge` | `() => AsyncGenerator<BridgeStatus>` | This browser's pairing, as it changes. | [44.20](/rulebook#44.20), [44.22](/rulebook#44.22) |
7831
7842
  | `/__abide/bridge` | `GET`, `POST` | Where a bridge pairs, and is relayed its turns. | [36.7](/rulebook#36.7), [44.14](/rulebook#44.14), [44.15](/rulebook#44.15), [44.19](/rulebook#44.19), [44.23](/rulebook#44.23), [44.26](/rulebook#44.26), [44.27](/rulebook#44.27), [16.78](/rulebook#16.78) |
@@ -7846,7 +7857,7 @@ Generated surfaces: what is written in place rather than imported.
7846
7857
 
7847
7858
  | Name | Signature | Meaning | Rules |
7848
7859
  | --- | --- | --- | --- |
7849
- | `ClaudeCodeConfig` | `{ model?: string; fallbackModel?: string; systemPrompt?: string; appendSystemPrompt?: string; effort?: 'low' \| 'medium' \| 'high' \| 'xhigh' \| 'max' }` | The Claude Code options a site may set for a turn. | [44.16](/rulebook#44.16) |
7860
+ | `ClaudeCodeConfig` | `{ model?: string; fallbackModel?: string; systemPrompt?: string; appendSystemPrompt?: string; effort?: 'low' \| 'medium' \| 'high' \| 'xhigh' \| 'max'; maxBudgetUsd?: number; betas?: string[]; disallowedTools?: string[]; tools?: string[] }` | The Claude Code options a site may set for a turn, and the built-in tools it asks the visitor for. | [44.16](/rulebook#44.16), [44.30](/rulebook#44.30) |
7850
7861
 
7851
7862
  ## Taught in
7852
7863
 
@@ -8283,7 +8294,8 @@ Generated surfaces: what is written in place rather than imported.
8283
8294
  | `abide:lifecycle` | `debug` `error` | A lifecycle that ran. | [28.8](/rulebook#28.8) |
8284
8295
  | `abide:principal` | `debug` `warning` | A principal set or cleared. | [28.8](/rulebook#28.8) |
8285
8296
  | `abide:health` | `warning` | An `onHealth` that threw or answered a non-object. | [25.2](/rulebook#25.2), [28.8](/rulebook#28.8) |
8286
- | `abide:mcp` | `debug` `warning` | A handler withheld from MCP, or a tool published with no description. | [20.3](/rulebook#20.3), [20.9](/rulebook#20.9), [28.8](/rulebook#28.8) |
8297
+ | `abide:mcp` | `debug` `warning` | A handler withheld from MCP, a tool call answered, or a tool published with no description. | [20.3](/rulebook#20.3), [20.9](/rulebook#20.9), [20.18](/rulebook#20.18), [28.8](/rulebook#28.8) |
8298
+ | `abide:assistant` | `debug` | One line per conversation, once it ends. | [44.32](/rulebook#44.32) |
8287
8299
  | `abide:openapi` | `warning` | A handler whose schema had no JSON Schema export. | [20.11](/rulebook#20.11), [28.8](/rulebook#28.8) |
8288
8300
  | `abide:render` | `warning` | An `error.abide` that itself failed. | [28.8](/rulebook#28.8) |
8289
8301
  | `abide:refuse` | `warning` | A message formatter that threw. | [15.13](/rulebook#15.13), [28.8](/rulebook#28.8) |
@@ -8597,7 +8609,7 @@ Generated surfaces: what is written in place rather than imported.
8597
8609
  | --- | --- | --- |
8598
8610
  | `{expr}` | Reactive text, escaped. | [32.1](/rulebook#32.1), [32.23](/rulebook#32.23) |
8599
8611
  | formatting whitespace | What a run of whitespace renders as. | [33.17](/rulebook#33.17), [33.18](/rulebook#33.18) |
8600
- | `{raw(...)}` | Raw HTML. | [32.2](/rulebook#32.2), [32.25](/rulebook#32.25), [32.26](/rulebook#32.26), [32.30](/rulebook#32.30), [32.31](/rulebook#32.31) |
8612
+ | `{raw(...)}` | Raw HTML. | [32.2](/rulebook#32.2), [32.25](/rulebook#32.25), [32.26](/rulebook#32.26), [32.30](/rulebook#32.30), [32.31](/rulebook#32.31), [32.70](/rulebook#32.70) |
8601
8613
  | `name={expr}` | A reactive attribute or property. | [32.3](/rulebook#32.3), [32.20](/rulebook#32.20), [32.23](/rulebook#32.23), [32.37](/rulebook#32.37) |
8602
8614
  | `on<event>={fn}` | A native listener on an element, and an ordinary prop on a component. | [32.4](/rulebook#32.4) |
8603
8615
  | `name="…{expr}…"` | An interpolated quoted value. | [32.3](/rulebook#32.3) |
@@ -8607,7 +8619,7 @@ Generated surfaces: what is written in place rather than imported.
8607
8619
  | `bind:open` | The same, on a `<details>`. | [32.7](/rulebook#32.7) |
8608
8620
  | `bind:group` | Radio or checkbox membership. | [32.8](/rulebook#32.8) |
8609
8621
  | `bind:value={{get, set}}` | Two-way bind over an explicit accessor pair, on an element or a component. | [32.5](/rulebook#32.5), [32.6](/rulebook#32.6) |
8610
- | `bind:element={Reactive<Element> \| ((element: Element) => void \| Disposer)}` | A node reference. | [32.9](/rulebook#32.9), [32.38](/rulebook#32.38), [32.66](/rulebook#32.66) |
8622
+ | `bind:element={Reactive<Element> \| ((element: Element) => void \| Disposer)}` | A node reference. | [32.9](/rulebook#32.9), [32.38](/rulebook#32.38), [32.66](/rulebook#32.66), [32.68](/rulebook#32.68), [32.69](/rulebook#32.69) |
8611
8623
  | `class:name={cond}` | Toggles a class. | [32.10](/rulebook#32.10) |
8612
8624
  | `style:prop={value}` | Sets one style property. | [32.10](/rulebook#32.10), [32.19](/rulebook#32.19) |
8613
8625
  | `{...expr}` | Spreads props or attributes. | [32.11](/rulebook#32.11), [32.37](/rulebook#32.37), [33.12](/rulebook#33.12), [33.13](/rulebook#33.13) |
@@ -8617,8 +8629,8 @@ Generated surfaces: what is written in place rather than imported.
8617
8629
 
8618
8630
  | Spelling | Branches | Meaning | Rules |
8619
8631
  | --- | --- | --- | --- |
8620
- | `{#if cond}` | `{:else if cond}`, `{:else}` | Conditional markup. | [32.12](/rulebook#32.12), [32.21](/rulebook#32.21), [32.63](/rulebook#32.63), [32.64](/rulebook#32.64), [32.65](/rulebook#32.65), [41.24](/rulebook#41.24) |
8621
- | `{#for item, index of list by key}` | — | Repeats markup over a list, or over a cursor as it produces. | [32.13](/rulebook#32.13), [32.14](/rulebook#32.14), [32.15](/rulebook#32.15), [32.22](/rulebook#32.22), [32.29](/rulebook#32.29), [32.32](/rulebook#32.32), [32.33](/rulebook#32.33), [32.63](/rulebook#32.63), [32.64](/rulebook#32.64), [32.67](/rulebook#32.67), [41.8](/rulebook#41.8), [41.14](/rulebook#41.14), [41.15](/rulebook#41.15) |
8632
+ | `{#if cond}` | `{:else if cond}`, `{:else}` | Conditional markup. | [32.12](/rulebook#32.12), [32.21](/rulebook#32.21), [32.63](/rulebook#32.63), [32.64](/rulebook#32.64), [32.65](/rulebook#32.65), [32.70](/rulebook#32.70), [41.24](/rulebook#41.24) |
8633
+ | `{#for item, index of list by key}` | — | Repeats markup over a list, or over a cursor as it produces. | [32.13](/rulebook#32.13), [32.14](/rulebook#32.14), [32.15](/rulebook#32.15), [32.22](/rulebook#32.22), [32.29](/rulebook#32.29), [32.32](/rulebook#32.32), [32.33](/rulebook#32.33), [32.63](/rulebook#32.63), [32.64](/rulebook#32.64), [32.67](/rulebook#32.67), [32.70](/rulebook#32.70), [41.8](/rulebook#41.8), [41.14](/rulebook#41.14), [41.15](/rulebook#41.15) |
8622
8634
  | `{#try}` | `{:catch e}`, `{:finally}` | A render-time boundary and a region. | [32.16](/rulebook#32.16), [32.56](/rulebook#32.56), [32.63](/rulebook#32.63), [32.64](/rulebook#32.64) |
8623
8635
 
8624
8636
  ## Components
@@ -8660,7 +8672,7 @@ Generated surfaces: what is written in place rather than imported.
8660
8672
 
8661
8673
  | Name | Signature | Meaning | Rules |
8662
8674
  | --- | --- | --- | --- |
8663
- | `raw` | `(value: unknown) => Raw` | The branding a value takes to render as markup rather than as text. | [32.2](/rulebook#32.2), [32.24](/rulebook#32.24), [32.26](/rulebook#32.26), [32.30](/rulebook#32.30), [32.31](/rulebook#32.31) |
8675
+ | `raw` | `(value: unknown) => Raw` | The branding a value takes to render as markup rather than as text. | [32.2](/rulebook#32.2), [32.24](/rulebook#32.24), [32.26](/rulebook#32.26), [32.30](/rulebook#32.30), [32.31](/rulebook#32.31), [32.70](/rulebook#32.70) |
8664
8676
 
8665
8677
  ## Taught in
8666
8678
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abide/abide",
3
- "version": "0.106.0",
3
+ "version": "0.108.0",
4
4
  "type": "module",
5
5
  "description": "Isomorphic type-safe framework for reactive async interfaces for humans and machines",
6
6
  "license": "MIT",
@@ -464,13 +464,15 @@ export function isParserOwned(lowerTag: string): boolean {
464
464
  /**
465
465
  * The key a component's attribute passes a prop under, or `undefined` where it is no prop.
466
466
  *
467
- * 32.6, 32.10 — a `bind:` handing over a `Reactive` keys the name after it, and any other `:` name
468
- * — `class:`, `style:`, a `bind:` with nothing bound — is an element's own. The build writes the
467
+ * 32.6, 32.10, 32.69 — a `bind:` handing over a `Reactive` keys the name after it, and any other
468
+ * `:` name — `class:`, `style:`, `bind:element`, a `bind:` with nothing bound — is an element's own.
469
+ * Read as a prop, `bind:element` was one named `element` the child never declared, and the
470
+ * callback never ran. The build writes the
469
471
  * props record by this and the checker projects it by this, so a tag is held to what it sends.
470
472
  */
471
473
  export function propKeyOf(attribute: Attribute): string | undefined {
472
474
  if (attribute.lowerName.startsWith('bind:'))
473
- return attribute.expression
475
+ return attribute.expression && !isElementBind(attribute.lowerName)
474
476
  ? attribute.name.slice('bind:'.length)
475
477
  : undefined
476
478
  return attribute.name.includes(':') ? undefined : attribute.name
@@ -244,7 +244,7 @@ export function emitClient(
244
244
  let fills = contributed
245
245
  if (applied !== '' || builds) {
246
246
  const build = builds
247
- ? `function __build(__document) {\n ${emitMaker(page, lowering)}\n}\n`
247
+ ? `function __build(__parent) {\n ${emitMaker(page, lowering)}\n}\n`
248
248
  : ''
249
249
  // THE HEAD GOES BEFORE THE FILL, so the guard is written out where there is one to apply.
250
250
  fills +=
@@ -453,7 +453,7 @@ function clientComponent(
453
453
  const fill =
454
454
  sink.positions.length === 0
455
455
  ? ''
456
- : `function __build(__document) {\n ${emitMaker(sink, lowering)}\n}
456
+ : `function __build(__parent) {\n ${emitMaker(sink, lowering)}\n}
457
457
  ${lowering.use('fillEmpty')}(${REGION_ROOT}, __build)\n`
458
458
  const captured = sink.defersSetup
459
459
  ? `const ${SHARE_AT} = ${lowering.use('currentShare')}()\n`
@@ -558,9 +558,11 @@ function emitRegion(sink: Cursor, region: RegionEmission): RegionEmission {
558
558
 
559
559
  for (const line of walkTo(positions, live, lowering)) chain.push(line)
560
560
 
561
- // THE UNITS, in the order a page always installed them — text, listeners, attributes, the node
562
- // handed over, then the regions. The walk does not depend on it; a `bind:element` callback has
563
- // always run with every other binding already in place, and that is kept.
561
+ // THE UNITS — text, listeners, attributes, the regions, then the node handed over. The walk
562
+ // does not depend on the order.
563
+ // 32.68 — A `bind:element` CALLBACK RUNS LAST, so the node it is handed is whole: a child
564
+ // component, an arm or a list inside it has built, and `form.querySelector('input')` finds an
565
+ // `<input>` an `<Input>` makes.
564
566
  // 32.67 — A ROW'S ITEM WATCHES FIRST, one after another, which is how its scope finds them
565
567
  // (`Scope.itemWatch`).
566
568
  const items: string[] = []
@@ -597,7 +599,6 @@ function emitRegion(sink: Cursor, region: RegionEmission): RegionEmission {
597
599
  for (const unit of texts) lines.push(unit)
598
600
  for (const unit of handlers) lines.push(unit)
599
601
  for (const unit of attributes) lines.push(unit)
600
- for (const unit of elements) lines.push(unit)
601
602
 
602
603
  // 32.67 — WHAT A ROW'S REGIONS AND COMPONENTS BIND reads the row's item through its cell: the
603
604
  // row's `update` reaches its own top level and no further.
@@ -613,6 +614,7 @@ function emitRegion(sink: Cursor, region: RegionEmission): RegionEmission {
613
614
  else emitInvocation(at, holder, region)
614
615
  }
615
616
  }
617
+ for (const unit of elements) lines.push(unit)
616
618
  // 32.43 — A REGION THAT MOUNTS NOTHING INVOKES NOTHING, so what it declares is reached
617
619
  // from nowhere and is not declared: a row declaring a component it never invokes stays a
618
620
  // row whose mount is the shared empty one.
@@ -835,7 +837,9 @@ const WRITES = {
835
837
  // instance OPENS (`shareScope`): a page that shares nothing still carries this for a child that does.
836
838
  const SHARE_AT = '__shareAt'
837
839
 
838
- // The document a region's maker is handed and builds its nodes from.
840
+ // The parent a region's maker is handed, which its nodes are made for and go into (`parseMarkup`).
841
+ const PARENT = '__parent'
842
+ // The document a built region makes its nodes with, read off `PARENT` once a build.
839
843
  const DOCUMENT = '__document'
840
844
 
841
845
  // A run's anchors while it holds markup (32.30), as the JS string literals a built run parses with.
@@ -843,9 +847,9 @@ const RAW_OPEN_MARKUP = JSON.stringify(anchorMarkup(RAW_OPEN))
843
847
  const RAW_CLOSE_MARKUP = JSON.stringify(anchorMarkup(RAW_CLOSE))
844
848
 
845
849
  /**
846
- * The body of the function that makes a region's nodes from a `__document`.
850
+ * The body of the function that makes a region's nodes for a `__parent`.
847
851
  *
848
- * WHAT MAKES A REGION'S NODES, as the body of a function taking the region's `__document`: the
852
+ * WHAT MAKES A REGION'S NODES, as the body of a function taking the region's `__parent`: the
849
853
  * row, the arm, the slot's fallback, the caller's children and the page that fills itself are all
850
854
  * made the same way. BUILT from the positions the markup walk placed (`emitBuild`) — or, where the
851
855
  * region holds content only the parser makes right (`isParserOwned`), PARSED from the very markup
@@ -866,7 +870,7 @@ function emitMaker(
866
870
  let parsed = !sink.repeated
867
871
  for (const at of sink.positions) parsed ||= at.parserOwned
868
872
  if (parsed)
869
- return `return ${lowering.use('markupNodes')}(${DOCUMENT}, ${markup()})`
873
+ return `return ${lowering.use('markupNodes')}(${PARENT}, ${markup()})`
870
874
  return emitBuild(sink, anchored, lowering)
871
875
  }
872
876
 
@@ -901,7 +905,10 @@ function emitMaker(
901
905
  * (`fillEmpty`), and a component or a slot is the same pair filled by the child.
902
906
  */
903
907
  function emitBuild(row: Cursor, anchored: boolean, lowering: Lowering): string {
904
- const lines: string[] = ['const __built = []']
908
+ const lines: string[] = [
909
+ `const ${DOCUMENT} = ${PARENT}.ownerDocument`,
910
+ 'const __built = []',
911
+ ]
905
912
  if (anchored)
906
913
  lines.push(
907
914
  `__built.push(${DOCUMENT}.createComment(${JSON.stringify(ROW_OPEN)}))`,
@@ -998,7 +1005,7 @@ function builtRun(
998
1005
  return lines
999
1006
  }
1000
1007
  lines.push(
1001
- `if (${raw.join(' || ')}) for (const __made of ${lowering.use('markupNodes')}(${DOCUMENT}, ${RAW_OPEN_MARKUP} + ${markup.join(' + ')} + ${RAW_CLOSE_MARKUP})) ${into(at, '__made')}`,
1008
+ `if (${raw.join(' || ')}) for (const __made of ${lowering.use('markupNodes')}(${at.within === null ? PARENT : at.within.name}, ${RAW_OPEN_MARKUP} + ${markup.join(' + ')} + ${RAW_CLOSE_MARKUP})) ${into(at, '__made')}`,
1002
1009
  )
1003
1010
  if (!at.blank) {
1004
1011
  lines.push(`else ${make(whole)}`)
@@ -1073,7 +1080,7 @@ function emitFillable(
1073
1080
  const lowering = region.lowering
1074
1081
  const filled = emitRegion(sink, region.nested())
1075
1082
  region.helpers.push(
1076
- `function ${name}_build(__document) {\n ${emitMaker(sink, lowering)}\n}`,
1083
+ `function ${name}_build(__parent) {\n ${emitMaker(sink, lowering)}\n}`,
1077
1084
  )
1078
1085
  region.helpers.push(
1079
1086
  mountFunction(
@@ -1337,7 +1344,7 @@ function emitArms(
1337
1344
  : arm === 1 && caught !== undefined
1338
1345
  ? caught
1339
1346
  : ''
1340
- const builders = `const ${own}_build = function (__document) {\n${emitMaker(sink, lowering)}\n}\n${mountFunction(own, REGION_ROOT, body, `${fillRegion}(${REGION_ROOT}, ${own}_build)`, lowering, sink.defersSetup, setup !== null)}`
1347
+ const builders = `const ${own}_build = function (__parent) {\n${emitMaker(sink, lowering)}\n}\n${mountFunction(own, REGION_ROOT, body, `${fillRegion}(${REGION_ROOT}, ${own}_build)`, lowering, sink.defersSetup, setup !== null)}`
1341
1348
  if (local === '') {
1342
1349
  helpers.push(
1343
1350
  `${builders}\nconst ${own}_arm = { build: ${own}_build, mount: ${own}_mount }`,
@@ -1423,7 +1430,7 @@ function emitList(
1423
1430
  // MADE FROM THE ITEM'S VALUE: every slot reading the item alone is written before the row is
1424
1431
  // inserted, so making a row writes the document once per node (32.67).
1425
1432
  const valued = `const ${item} = ${ROW_ROOT}.data`
1426
- const make = `const ${name}_make = (__document, ${ROW_ROOT}, ${at}) => {\n ${valued}\n ${emitMaker(row, lowering, () => rowMarkup(row, !single, lowering, CLIENT_LANE), !single)}\n}`
1433
+ const make = `const ${name}_make = (__parent, ${ROW_ROOT}, ${at}) => {\n ${valued}\n ${emitMaker(row, lowering, () => rowMarkup(row, !single, lowering, CLIENT_LANE), !single)}\n}`
1427
1434
  const walk = rowRegion.body()
1428
1435
  // A PATTERN READ OFF THE RECORD is read through one picker a name, the list's rather than the
1429
1436
  // row's, so a row allocates nothing for it.
@@ -1448,7 +1455,7 @@ function emitList(
1448
1455
  // rebuilds whole: a node that refused is the row disagreeing, and re-making it is the
1449
1456
  // answer the page does not have.
1450
1457
  opened,
1451
- `${lowering.use('replaceRow')}(${ROW_ROOT}, ${name}_make(${ROW_ROOT}.node.ownerDocument, ${ROW_ROOT}, ${at}))`,
1458
+ `${lowering.use('replaceRow')}(${ROW_ROOT}, ${name}_make, ${at})`,
1452
1459
  lowering,
1453
1460
  row.defersSetup,
1454
1461
  block.setup !== null,
@@ -31,7 +31,7 @@ export function invocationProps(
31
31
  const key = propKeyOf(attribute)
32
32
  if (key === undefined)
33
33
  throw new Error(
34
- `abide: ${attribute.name} on <${node.tag}> is not a prop a component takes — \`bind:\` hands over the \`Reactive\` in its braces (RULEBOOK 32.6), and \`class:\` and \`style:\` set an element's own class and style. RULEBOOK 32.10.`,
34
+ `abide: ${attribute.name} on <${node.tag}> is not a prop a component takes — \`bind:\` hands over the \`Reactive\` in its braces (RULEBOOK 32.6), \`class:\` and \`style:\` set an element's own class and style (RULEBOOK 32.10), and \`bind:element\` hands over an element — put it on one inside <${node.tag}>, or pass the callback as a prop (RULEBOOK 32.69).`,
35
35
  )
36
36
  // A STATIC PROP IS STILL AN ACCESSOR. One shape per prop whatever the call site spelled
37
37
  // (33.10), so the child reads every key the same way and a literal is not a second case.
@@ -1,3 +1,3 @@
1
1
  // The `abide-bridge` release a pairing command names, so a visitor's `bunx` runs the relay this
2
2
  // server speaks. `packages/abide/tests/claudeCode.test.ts` holds it to the bridge package's own.
3
- export const BRIDGE_VERSION = '0.2.0'
3
+ export const BRIDGE_VERSION = '0.3.2'
@@ -10,6 +10,7 @@
10
10
  // another request's scope — `claudeCode` answers one from the bridge's — and the call is still this
11
11
  // asker's.
12
12
 
13
+ import { namedLogger } from '#shared/log.ts'
13
14
  import { refuse } from '#shared/refuse.ts'
14
15
  import { type McpReply, McpSurface } from './generatedSurfaces.ts'
15
16
  import { request, within } from './requestScope.ts'
@@ -20,7 +21,8 @@ export type AssistantMessage = { role: 'user' | 'assistant'; content: string }
20
21
  /** What a conversation yields, the same from every provider. */
21
22
  export type AssistantEvent =
22
23
  | { type: 'text'; text: string }
23
- | { type: 'tool'; id: string; name: string; input: unknown }
24
+ | { type: 'tool'; id: string; name: string }
25
+ | { type: 'input'; id: string; input: unknown }
24
26
  | { type: 'result'; id: string; text: string; failed: boolean }
25
27
  | {
26
28
  type: 'done'
@@ -63,6 +65,7 @@ export type Provider = {
63
65
  export type AssistantOptions = { mcp?: boolean; maxTurns?: number }
64
66
 
65
67
  const MAX_TURNS = 10
68
+ const ASSISTANT_LOG = namedLogger('abide:assistant')
66
69
  // 44.7 — the caller's credentials, and nothing else of their request.
67
70
  const CARRIED_HEADERS = ['cookie', 'authorization']
68
71
 
@@ -82,10 +85,39 @@ export async function* assistant(
82
85
  options.mcp === true ? appSurface() : NO_SURFACE,
83
86
  askerOf(request()),
84
87
  )
85
- yield* provider.run(messages, tools, {
88
+ const events = provider.run(messages, tools, {
86
89
  maxTurns: options.maxTurns ?? MAX_TURNS,
87
90
  signal: request().signal,
88
91
  })
92
+ // 28.7 — a closed namespace runs the provider's stream untouched.
93
+ yield* ASSISTANT_LOG.enabled() ? traced(events, messages.length) : events
94
+ }
95
+
96
+ /** `events`, and one line on `abide:assistant` once they end, however they end. 44.32. */
97
+ async function* traced(
98
+ events: AsyncGenerator<AssistantEvent>,
99
+ messageCount: number,
100
+ ): AsyncGenerator<AssistantEvent> {
101
+ const started = performance.now()
102
+ let calls = 0
103
+ let done: Extract<AssistantEvent, { type: 'done' }> | undefined
104
+ try {
105
+ for await (const event of events) {
106
+ if (event.type === 'tool') calls += 1
107
+ else if (event.type === 'done') done = event
108
+ yield event
109
+ }
110
+ } finally {
111
+ ASSISTANT_LOG.debug(
112
+ done === undefined ? 'unfinished' : done.stop,
113
+ `${messageCount} messages`,
114
+ `${calls} tool calls`,
115
+ done === undefined
116
+ ? 'no usage'
117
+ : `${done.usage.input}/${done.usage.output} tokens`,
118
+ `${(performance.now() - started).toFixed(1)}ms`,
119
+ )
120
+ }
89
121
  }
90
122
 
91
123
  /** Where an MCP request is answered, as JSON-RPC's `result` or `error`. */
@@ -52,7 +52,7 @@ const CODE_LIFETIME = 10 * 60_000
52
52
  // How long a bridge outlives the last page watching it, so a reload does not end it.
53
53
  const IDLE_GRACE = 30_000
54
54
  /** How long a pairing whose stream dropped waits to be resumed. 44.26. */
55
- export const RESUME_MILLISECONDS = 30_000
55
+ export const RESUME_MILLISECONDS = 60_000
56
56
  /** How often a pairing's stream carries a comment while it has nothing else to send. 44.27. */
57
57
  export const HEARTBEAT_MILLISECONDS = 15_000
58
58
  const HEARTBEAT = new TextEncoder().encode(EVENT_STREAM_OPENER)
@@ -118,10 +118,19 @@ export function claudeApi(config: ClaudeApiConfig): Provider {
118
118
  case 'message_start':
119
119
  usage.input += inputOf(event.message.usage)
120
120
  break
121
- case 'content_block_start':
122
- blocks[event.index] = event.content_block
121
+ case 'content_block_start': {
122
+ const block = event.content_block
123
+ blocks[event.index] = block
123
124
  partials[event.index] = ''
125
+ // 44.29 — announced as the model starts writing it.
126
+ if (block.type === 'tool_use')
127
+ yield {
128
+ type: 'tool',
129
+ id: block.id as string,
130
+ name: block.name as string,
131
+ }
124
132
  break
133
+ }
125
134
  case 'content_block_delta': {
126
135
  const block = blocks[event.index] as Block
127
136
  const delta = event.delta
@@ -152,9 +161,8 @@ export function claudeApi(config: ClaudeApiConfig): Provider {
152
161
  }
153
162
  called.push(block)
154
163
  yield {
155
- type: 'tool',
164
+ type: 'input',
156
165
  id: block.id as string,
157
- name: block.name as string,
158
166
  input: block.input,
159
167
  }
160
168
  break
@@ -3,9 +3,10 @@
3
3
  // turn and answer the MCP requests it makes from the `Toolbox` — in this request's scope, as this
4
4
  // asker (44.7) — so its tools are the site's and nothing of the visitor's machine is one.
5
5
  //
6
- // CONFIGURED BY CLAUDE CODE'S OWN OPTION NAMES, and only the ones that shape the answer. What would give
7
- // a site power over the visitor's machine — its own tools, permissions, directory, settings — is not an
8
- // option here at all: the bridge decides it (44.15, 44.16).
6
+ // CONFIGURED BY CLAUDE CODE'S OWN OPTION NAMES, which the bridge lays on as their flags, and only the ones
7
+ // that shape the answer. What would give a site power over the visitor's machine — permissions,
8
+ // directory, settings — is not an option here at all, and its built-in `tools` are a request the visitor
9
+ // grants or does not: the bridge decides (44.15, 44.16, 44.30).
9
10
 
10
11
  import { mounted } from '#shared/mount.ts'
11
12
  import { refuse } from '#shared/refuse.ts'
@@ -28,13 +29,17 @@ import { mcpFailure } from './generatedSurfaces.ts'
28
29
  import { Inbox } from './Inbox.ts'
29
30
  import { request } from './requestScope.ts'
30
31
 
31
- /** The Claude Code options a site may set for a turn. */
32
+ /** The Claude Code options a site may set for a turn, and the built-in tools it asks the visitor for. */
32
33
  export type ClaudeCodeConfig = {
33
34
  model?: string
34
35
  fallbackModel?: string
35
36
  systemPrompt?: string
36
37
  appendSystemPrompt?: string
37
38
  effort?: 'low' | 'medium' | 'high' | 'xhigh' | 'max'
39
+ maxBudgetUsd?: number
40
+ betas?: string[]
41
+ disallowedTools?: string[]
42
+ tools?: string[]
38
43
  }
39
44
 
40
45
  /** Whether this browser's bridge is connected, and the command that pairs one where it is not. */
@@ -304,11 +304,13 @@ export class McpSurface {
304
304
  asker: Request,
305
305
  ): Promise<{ text: string; failed: boolean }> {
306
306
  const tool = this.#tool(name)
307
- if (tool === undefined)
307
+ if (tool === undefined) {
308
+ logCall(String(name), 404, undefined)
308
309
  return Promise.resolve({
309
310
  text: `No tool named ${name}.`,
310
311
  failed: true,
311
312
  })
313
+ }
312
314
  return this.#called(tool, args as Record<string, unknown>, asker)
313
315
  }
314
316
 
@@ -321,15 +323,23 @@ export class McpSurface {
321
323
  args: Record<string, unknown> | undefined,
322
324
  asker: Request,
323
325
  ): Promise<{ text: string; failed: boolean }> {
326
+ // 28.7 — a closed namespace does not pay for the line it would have written.
327
+ const started = MCP_LOG.enabled() ? performance.now() : undefined
324
328
  let answer: Response
325
329
  try {
326
330
  answer = await this.#answer(callOf(asker, one, args))
327
331
  } catch (thrown) {
328
332
  // 15.8 — a path parameter no address carries, refused as the wire's 422 is (`rpcRequest`).
329
- if (!isFailed(thrown)) throw thrown
333
+ if (!isFailed(thrown)) {
334
+ logCall(one.name, 500, started)
335
+ throw thrown
336
+ }
330
337
  answer = answered(thrown)
331
338
  }
332
- return { text: await answer.text(), failed: answer.status >= 400 }
339
+ // The body is the call's too: a handler that streams has only begun when its head arrives.
340
+ const text = await answer.text()
341
+ logCall(one.name, answer.status, started)
342
+ return { text, failed: answer.status >= 400 }
333
343
  }
334
344
 
335
345
  /** One MCP request, made as `asker`, as JSON-RPC's `result` or `error`. */
@@ -353,11 +363,13 @@ export class McpSurface {
353
363
  return { result: { tools: this.list() } }
354
364
  case 'tools/call': {
355
365
  const tool = this.#tool(params?.name)
356
- if (tool === undefined)
366
+ if (tool === undefined) {
367
+ logCall(String(params?.name), 404, undefined)
357
368
  return mcpFailure(
358
369
  -32602,
359
370
  `No tool named ${String(params?.name)}.`,
360
371
  )
372
+ }
361
373
  const { text, failed } = await this.#called(
362
374
  tool,
363
375
  params?.arguments as Record<string, unknown> | undefined,
@@ -382,6 +394,23 @@ export class McpSurface {
382
394
  }
383
395
  }
384
396
 
397
+ // 20.18 — ONE LINE PER CALL, whoever framed it: this endpoint or a provider's `Toolbox`, and a call
398
+ // to a tool that is not published (404) or whose answer threw (500) included. `started` is unset
399
+ // where nothing ran, or where the namespace is closed (28.7).
400
+ function logCall(
401
+ name: string,
402
+ status: number,
403
+ started: number | undefined,
404
+ ): void {
405
+ if (!MCP_LOG.enabled()) return
406
+ MCP_LOG.debug(
407
+ 'call',
408
+ name,
409
+ status,
410
+ `${started === undefined ? '0.0' : (performance.now() - started).toFixed(1)}ms`,
411
+ )
412
+ }
413
+
385
414
  /**
386
415
  * Whether a tool declares an `outputSchema`: MCP's structured content is an object, so only a
387
416
  * result schema that is one. 20.16.
@@ -17,7 +17,7 @@ import { Scope, type Stoppable } from './Scope.ts'
17
17
 
18
18
  /** One arm: what makes its nodes, and what binds them. A `build` of `undefined` makes none. */
19
19
  export type Arm = {
20
- build: ((document: Document) => Node[]) | undefined
20
+ build: ((parent: Node) => Node[]) | undefined
21
21
  mount: (root: Region) => Stoppable | undefined
22
22
  }
23
23
 
package/src/ui/RawRun.ts CHANGED
@@ -60,7 +60,7 @@ export function textToMarkup(text: Text, markup: string): RawRun {
60
60
  const open = document.createComment(RAW_OPEN)
61
61
  const close = document.createComment(RAW_CLOSE)
62
62
  parent.insertBefore(open, text)
63
- parent.insertBefore(parseMarkup(text, markup), text)
63
+ parent.insertBefore(parseMarkup(parent, markup), text)
64
64
  parent.replaceChild(close, text)
65
65
  return new RawRun(open, close, markup)
66
66
  }
@@ -70,7 +70,7 @@ export function refillRun(run: RawRun, markup: string): void {
70
70
  const parent = run.close.parentNode
71
71
  if (parent === null) throw detached()
72
72
  clearRun(run)
73
- parent.insertBefore(parseMarkup(run.close, markup), run.close)
73
+ parent.insertBefore(parseMarkup(parent, markup), run.close)
74
74
  run.html = markup
75
75
  }
76
76
 
@@ -21,6 +21,10 @@ import { holdWhileLeaving } from './leave.ts'
21
21
  * form is where teardown is decided, and its `Disposer` is the whole of it.
22
22
  *
23
23
  * 32.38 — A `Disposer` HANDING BACK A THENABLE IS AN EXIT: the element's nodes stay until it settles.
24
+ *
25
+ * 32.68 — THE NODE IS CONNECTED AND WHOLE, and nothing here makes it so: this runs from a region's
26
+ * MOUNT, which every caller (`ArmRegion`, `TryRegion`, `makeRow`) calls after `build`'s nodes are
27
+ * inserted, and last in it, after the blocks and components inside the node (`emitRegion`).
24
28
  */
25
29
  export function bindElement(
26
30
  element: Element,
@@ -68,11 +68,12 @@ function bindValue(bound: Bound, operand: unknown, scope: Scope): void {
68
68
  ) {
69
69
  // 32.62 — the list of what is selected, read and written per option.
70
70
  const select = bound as unknown as HTMLSelectElement
71
- scope.watch(() => {
72
- const value = boundValue(operand)
71
+ const choose = (value: unknown) => {
73
72
  for (const option of select.options)
74
73
  option.selected = selectHolds(value, option.value)
75
- })
74
+ }
75
+ scope.watch(() => choose(boundValue(operand)))
76
+ chooseOnNewOptions(select, () => choose(heldValue(operand)), scope)
76
77
  select.addEventListener('change', () => {
77
78
  const chosen: string[] = []
78
79
  for (const option of select.selectedOptions)
@@ -84,8 +85,7 @@ function bindValue(bound: Bound, operand: unknown, scope: Scope): void {
84
85
  // 32.58 — NUMERIC ONCE, NUMERIC FROM THEN ON: a blank field writes `undefined`, and the
85
86
  // keystroke after it still has to know the value was a number.
86
87
  let numeric = bound.type === 'number' || bound.type === 'range'
87
- scope.watch(() => {
88
- const value = boundValue(operand)
88
+ const show = (value: unknown) => {
89
89
  if (typeof value === 'number') numeric = true
90
90
  // 32.59 — text that already reads as the value is the reader's spelling of it: a trailing
91
91
  // decimal point or zero is mid-typing, and rewriting it to the number's own text takes the
@@ -94,7 +94,8 @@ function bindValue(bound: Bound, operand: unknown, scope: Scope): void {
94
94
  return
95
95
  const text = value === undefined || value === null ? '' : String(value)
96
96
  if (bound.value !== text) bound.value = text
97
- })
97
+ }
98
+ scope.watch(() => show(boundValue(operand)))
98
99
  const write = () => {
99
100
  const typed = bound.value
100
101
  if (!numeric) {
@@ -108,6 +109,7 @@ function bindValue(bound: Bound, operand: unknown, scope: Scope): void {
108
109
  }
109
110
  // A `<select>` settles on `change`; everything else that takes text reports each keystroke.
110
111
  if (bound.tagName === 'SELECT') {
112
+ chooseOnNewOptions(bound, () => show(heldValue(operand)), scope)
111
113
  bound.addEventListener('change', write)
112
114
  return
113
115
  }
@@ -125,3 +127,16 @@ function bindValue(bound: Bound, operand: unknown, scope: Scope): void {
125
127
  if (!composing) write()
126
128
  })
127
129
  }
130
+
131
+ // 32.62 — AN OPTION THAT ARRIVES AFTER THE VALUE IS SELECTED WHEN IT IS THE VALUE. The value's
132
+ // watch does not read the options, so a `{#for}` that grows them later wakes nothing of its own, and
133
+ // the select would stay on whatever the browser picked when it changed.
134
+ function chooseOnNewOptions(
135
+ select: Element,
136
+ choose: () => void,
137
+ scope: Scope,
138
+ ): void {
139
+ const observer = new MutationObserver(choose)
140
+ observer.observe(select, { childList: true, subtree: true })
141
+ scope.stops.push(() => observer.disconnect())
142
+ }
@@ -36,6 +36,9 @@ export function listenForRoutes(router: Router): () => void {
36
36
  // mounted: the browser's own restoration runs before that page exists.
37
37
  const onPop = (): void => {
38
38
  if (router.match(location.pathname) === undefined) {
39
+ // The bar already stands at the entry being loaded, so the `pagehide` this reload
40
+ // fires would write the departed page's scroll into it: the listeners go first.
41
+ listening.abort()
39
42
  location.reload()
40
43
  return
41
44
  }
@@ -1,4 +1,4 @@
1
- // MARKUP PARSED IN THE DOCUMENT `near` BELONGS TO, not the global one. A page mounted into a second
1
+ // MARKUP PARSED IN THE DOCUMENT `parent` BELONGS TO, not the global one. A page mounted into a second
2
2
  // document (an example `<iframe>`) parsed its `{#if}` arms in the right one and its `{#for}` rows in
3
3
  // the page's while the two were separate copies of this; one copy is one answer.
4
4
  //
@@ -6,16 +6,67 @@
6
6
  // every caller moves those nodes out before it parses again.
7
7
  const TEMPLATES = new WeakMap<Document, HTMLTemplateElement>()
8
8
 
9
- /** Parses markup in `near`'s document; the fragment is reused by the next parse. */
10
- export function parseMarkup(near: Node, markup: string): DocumentFragment {
11
- const document = near.ownerDocument ?? (near as Document)
9
+ const SVG = 'http://www.w3.org/2000/svg'
10
+ const MATHML = 'http://www.w3.org/1998/Math/MathML'
11
+
12
+ // The foreign elements whose children the parser reads as HTML again — the integration points —
13
+ // so markup going into one parses the way it would at the template's own top level.
14
+ const INTEGRATION_POINTS = new Set([
15
+ 'foreignObject',
16
+ 'desc',
17
+ 'title',
18
+ 'mi',
19
+ 'mo',
20
+ 'mn',
21
+ 'ms',
22
+ 'mtext',
23
+ ])
24
+
25
+ /** Whether the parser reads `element`'s children as HTML again, though it is SVG or MathML. */
26
+ function isIntegrationPoint(element: Element): boolean {
27
+ if (INTEGRATION_POINTS.has(element.localName)) return true
28
+ // The HTML standard makes an `<annotation-xml>` one only by the encoding it declares.
29
+ if (element.localName !== 'annotation-xml') return false
30
+ const encoding = element.getAttribute('encoding')?.toLowerCase()
31
+ return encoding === 'text/html' || encoding === 'application/xhtml+xml'
32
+ }
33
+
34
+ /**
35
+ * Parses markup for `parent` — in its document, and in its NAMESPACE; the fragment is reused by the
36
+ * next parse.
37
+ *
38
+ * IN THE PARENT'S NAMESPACE because the server's parser read the same bytes inside it: a template's
39
+ * own children are HTML, so a `<circle>` parsed there alone is an `HTMLUnknownElement` that draws
40
+ * nothing in the `<svg>` it lands in. Markup bound for an SVG or MathML parent is parsed inside an
41
+ * `<svg>` or a `<math>` and the wrapper unwrapped — what the parser closes the wrapper on (a `<p>`
42
+ * breaks out of foreign content) stays, beside what was inside it, in order.
43
+ */
44
+ export function parseMarkup(parent: Node, markup: string): DocumentFragment {
45
+ const document = parent.ownerDocument ?? (parent as Document)
12
46
  let template = TEMPLATES.get(document)
13
47
  if (template === undefined) {
14
48
  template = document.createElement('template')
15
49
  TEMPLATES.set(document, template)
16
50
  }
17
- template.innerHTML = markup
18
- return template.content
51
+ const space = (parent as Element).namespaceURI
52
+ const wrapper =
53
+ (space !== SVG && space !== MATHML) ||
54
+ isIntegrationPoint(parent as Element)
55
+ ? undefined
56
+ : space === SVG
57
+ ? 'svg'
58
+ : 'math'
59
+ if (wrapper === undefined) {
60
+ template.innerHTML = markup
61
+ return template.content
62
+ }
63
+ template.innerHTML = `<${wrapper}>${markup}</${wrapper}>`
64
+ const content = template.content
65
+ const wrapped = content.firstChild as Element
66
+ while (wrapped.firstChild !== null)
67
+ content.insertBefore(wrapped.firstChild, wrapped)
68
+ wrapped.remove()
69
+ return content
19
70
  }
20
71
 
21
72
  /**
@@ -23,10 +74,10 @@ export function parseMarkup(near: Node, markup: string): DocumentFragment {
23
74
  *
24
75
  * Indexed rather than spread, so no iterator runs per row.
25
76
  */
26
- export function markupNodes(near: Node, markup: string): Node[] {
77
+ export function markupNodes(parent: Node, markup: string): Node[] {
27
78
  const nodes: Node[] = []
28
79
  for (
29
- let node = parseMarkup(near, markup).firstChild;
80
+ let node = parseMarkup(parent, markup).firstChild;
30
81
  node !== null;
31
82
  node = node.nextSibling
32
83
  )
package/src/ui/regions.ts CHANGED
@@ -86,7 +86,7 @@ export function isEmptyRegion(region: Region): boolean {
86
86
  */
87
87
  export function fillRegion(
88
88
  region: Region,
89
- build?: (document: Document) => Node[],
89
+ build?: (parent: Node) => Node[],
90
90
  ): void {
91
91
  // THE PARENT IS READ BEFORE ANYTHING IS REMOVED, and the removal only happens if there is
92
92
  // one. Written as an unguarded removal followed by a `?.`-guarded insert, a region whose
@@ -101,7 +101,7 @@ export function fillRegion(
101
101
  if (build === undefined) return
102
102
  beginLayer(LAYER_FILL)
103
103
  try {
104
- const made = build(region.close.ownerDocument as Document)
104
+ const made = build(parent)
105
105
  for (const one of made) parent.insertBefore(one, region.close)
106
106
  } finally {
107
107
  endLayer(LAYER_FILL)
@@ -119,7 +119,7 @@ export function fillRegion(
119
119
  */
120
120
  export function fillEmpty(
121
121
  region: Region,
122
- build?: (document: Document) => Node[],
122
+ build?: (parent: Node) => Node[],
123
123
  ): boolean {
124
124
  if (!isEmptyRegion(region)) return false
125
125
  fillRegion(region, build)
package/src/ui/rows.ts CHANGED
@@ -69,11 +69,11 @@ export type Row = {
69
69
  */
70
70
  export type RowBuilders = {
71
71
  /**
72
- * A row's nodes, BUILT in the document the list stands in — handed over by the reconcile
73
- * rather than found by the row, which is the one place a row's document is decided. Every slot
72
+ * A row's nodes, BUILT for the parent the list stands in — handed over by the reconcile rather
73
+ * than found by the row, which is the one place a row's document and namespace are decided. Every slot
74
74
  * reading the item alone is written here, before the nodes are inserted.
75
75
  */
76
- make: (document: Document, row: Row, at: number) => Node[]
76
+ make: (parent: Node, row: Row, at: number) => Node[]
77
77
  mount: (row: Row, at: number) => Scope
78
78
  /** 32.67 — rewrites, from `row.data`, the slots `make` wrote from the item alone. */
79
79
  update: (row: Row, at: number) => void
@@ -172,15 +172,20 @@ export function rowsIn(region: Region, anchored: boolean): ChildNode[] {
172
172
  }
173
173
 
174
174
  /**
175
- * Rebuilds a row in place from `next`, keeping its record.
175
+ * Rebuilds a row in place with `make`, keeping its record.
176
176
  *
177
177
  * 41.10 — a row that cannot be hydrated is rebuilt: the fresh nodes go where the row stood and the
178
178
  * row's record takes the new first node, so the reconcile still reaches all of it.
179
179
  */
180
- export function replaceRow(row: Row, next: Node[]): void {
180
+ export function replaceRow(
181
+ row: Row,
182
+ make: RowBuilders['make'],
183
+ at: number,
184
+ ): void {
181
185
  const first = row.node
182
186
  const parent = first.parentNode
183
187
  if (parent === null) return
188
+ const next = make(parent, row, at)
184
189
  for (const node of next) parent.insertBefore(node, first)
185
190
  removeRow(first)
186
191
  row.node = next[0] as ChildNode
@@ -201,7 +206,7 @@ export function makeRow(
201
206
  // 14.8 — set up ONCE for the row, and the same builders make it and mount it.
202
207
  const builders = setUp(row, at)
203
208
  row.update = builders.update
204
- const nodes = builders.make(parent.ownerDocument as Document, row, at)
209
+ const nodes = builders.make(parent, row, at)
205
210
  for (const node of nodes) parent.insertBefore(node, before)
206
211
  row.node = nodes[0] as ChildNode
207
212
  row.scope = builders.mount(row, at)