@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 +23 -0
- package/docs/REGISTRY.md +11 -10
- package/docs/RULEBOOK.md +37 -5
- package/docs/abide.md +27 -15
- package/package.json +1 -1
- package/src/compiler/classifyTemplate.ts +5 -3
- package/src/compiler/emitClient.ts +23 -16
- package/src/compiler/invocationProps.ts +1 -1
- package/src/server/BRIDGE_VERSION.ts +1 -1
- package/src/server/assistant.ts +34 -2
- package/src/server/bridgeRelay.ts +1 -1
- package/src/server/claudeApi.ts +12 -4
- package/src/server/claudeCode.ts +9 -4
- package/src/server/generatedSurfaces.ts +33 -4
- package/src/ui/ArmRegion.ts +1 -1
- package/src/ui/RawRun.ts +2 -2
- package/src/ui/bindElement.ts +4 -0
- package/src/ui/bindProperty.ts +21 -6
- package/src/ui/listenForRoutes.ts +3 -0
- package/src/ui/parseMarkup.ts +59 -8
- package/src/ui/regions.ts +3 -3
- package/src/ui/rows.ts +11 -6
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
|
|
3064
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
{
|
|
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
|
|
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
|
|
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 `
|
|
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
|
@@ -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
|
|
468
|
-
* — `class:`, `style:`, a `bind:` with nothing bound — is an element's own.
|
|
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(
|
|
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(
|
|
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
|
|
562
|
-
//
|
|
563
|
-
//
|
|
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
|
|
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
|
|
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 `
|
|
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')}(${
|
|
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[] = [
|
|
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')}(${
|
|
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(
|
|
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 (
|
|
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 = (
|
|
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
|
|
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),
|
|
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
|
|
3
|
+
export const BRIDGE_VERSION = '0.3.2'
|
package/src/server/assistant.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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 =
|
|
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)
|
package/src/server/claudeApi.ts
CHANGED
|
@@ -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
|
-
|
|
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: '
|
|
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
|
package/src/server/claudeCode.ts
CHANGED
|
@@ -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,
|
|
7
|
-
// a site power over the visitor's machine —
|
|
8
|
-
// option here at all
|
|
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))
|
|
333
|
+
if (!isFailed(thrown)) {
|
|
334
|
+
logCall(one.name, 500, started)
|
|
335
|
+
throw thrown
|
|
336
|
+
}
|
|
330
337
|
answer = answered(thrown)
|
|
331
338
|
}
|
|
332
|
-
|
|
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.
|
package/src/ui/ArmRegion.ts
CHANGED
|
@@ -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: ((
|
|
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(
|
|
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(
|
|
73
|
+
parent.insertBefore(parseMarkup(parent, markup), run.close)
|
|
74
74
|
run.html = markup
|
|
75
75
|
}
|
|
76
76
|
|
package/src/ui/bindElement.ts
CHANGED
|
@@ -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,
|
package/src/ui/bindProperty.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/ui/parseMarkup.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// MARKUP PARSED IN THE DOCUMENT `
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
18
|
-
|
|
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(
|
|
77
|
+
export function markupNodes(parent: Node, markup: string): Node[] {
|
|
27
78
|
const nodes: Node[] = []
|
|
28
79
|
for (
|
|
29
|
-
let node = parseMarkup(
|
|
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?: (
|
|
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(
|
|
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?: (
|
|
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
|
|
73
|
-
*
|
|
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: (
|
|
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
|
|
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(
|
|
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
|
|
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)
|