@abide/abide 0.107.0 → 0.109.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 +19 -0
- package/docs/REGISTRY.md +9 -8
- package/docs/RULEBOOK.md +21 -2
- package/docs/abide.md +13 -9
- package/package.json +1 -1
- package/src/compiler/emitClient.ts +17 -12
- package/src/server/BRIDGE_VERSION.ts +1 -1
- package/src/server/assistant.ts +32 -1
- package/src/server/bridgeRelay.ts +1 -1
- package/src/server/claudeCode.ts +17 -5
- package/src/server/generatedSurfaces.ts +33 -4
- package/src/ui/ArmRegion.ts +1 -1
- package/src/ui/RawRun.ts +2 -2
- 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,24 @@
|
|
|
1
1
|
# @abide/abide
|
|
2
2
|
|
|
3
|
+
## 0.109.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- f969612: claudeCode.bridgeCommand and claudeCode.bridge take tools, and the command they mint grants them after its -- ([`d20a962`](https://github.com/briancray/abide/commit/d20a962523f91161cb731e766863256fd5d90208))
|
|
8
|
+
|
|
9
|
+
## 0.108.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- 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))
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- 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))
|
|
18
|
+
- 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))
|
|
19
|
+
- fda92c3: bind:value on a <select> selects an option added after the value was written ([`cd592b2`](https://github.com/briancray/abide/commit/cd592b2ccdc617c5cfed2a6acf5f5a458bc9986b))
|
|
20
|
+
- 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))
|
|
21
|
+
|
|
3
22
|
## 0.107.0
|
|
4
23
|
|
|
5
24
|
### Minor Changes
|
package/docs/REGISTRY.md
CHANGED
|
@@ -338,10 +338,10 @@ 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 |
|
|
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
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
|
-
| `claudeCode.bridgeCommand` | `() => { command: string; expires: number }` | The command that pairs a bridge with this browser. | 44.14 |
|
|
344
|
-
| `claudeCode.bridge` | `() => AsyncGenerator<BridgeStatus>` | This browser's pairing, as it changes. | 44.20, 44.22 |
|
|
343
|
+
| `claudeCode.bridgeCommand` | `(options?: { tools?: string[] }) => { command: string; expires: number }` | The command that pairs a bridge with this browser, granting the built-in tools it is given. | 44.14, 44.33 |
|
|
344
|
+
| `claudeCode.bridge` | `(options?: { tools?: string[] }) => AsyncGenerator<BridgeStatus>` | This browser's pairing, as it changes. | 44.20, 44.22, 44.33 |
|
|
345
345
|
| `BridgeStatus` | `{ paired: boolean; command: string \| undefined }` | Whether a bridge is paired, and the command that pairs one where none is. | 44.20 |
|
|
346
346
|
| `/__abide/bridge` | `GET`, `POST` | Where a bridge pairs, and is relayed its turns. | 36.7, 44.14, 44.15, 44.19, 44.23, 44.26, 44.27, 16.78 |
|
|
347
347
|
|
|
@@ -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 |
|
|
@@ -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
|
|
@@ -1965,6 +1971,9 @@ reading layout, querying its descendants or calling `focus()` acts on a connecte
|
|
|
1965
1971
|
|
|
1966
1972
|
32.69 `bind:element` on a component MUST be a build error.
|
|
1967
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
|
+
|
|
1968
1977
|
# 33. Props
|
|
1969
1978
|
|
|
1970
1979
|
33.1 `props` MUST be callable from a `<script>` block only, and calling it from a `<script module>`
|
|
@@ -3097,9 +3106,9 @@ its lines MUST be relayed.
|
|
|
3097
3106
|
carrying it was not taken by `/__abide/bridge`, or where no reply arrived within 120 seconds.
|
|
3098
3107
|
|
|
3099
3108
|
44.25 A bridge whose stream from `/__abide/bridge` ends unasked MUST redial it with its pairing's
|
|
3100
|
-
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.
|
|
3101
3110
|
|
|
3102
|
-
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
|
|
3103
3112
|
its token within them MUST resume the stream with every frame the dropped one had not sent.
|
|
3104
3113
|
|
|
3105
3114
|
44.27 `/__abide/bridge` MUST send a comment on a pairing's stream every 15 seconds for as long as
|
|
@@ -3114,3 +3123,13 @@ and its `input` event, with the same `id`, once the call is written.
|
|
|
3114
3123
|
44.30 A built-in tool MUST be available to a turn only where the site's `tools` names it and the
|
|
3115
3124
|
visitor's `--tools` does, MUST be approved without asking unless the visitor passed `--allowedTools`,
|
|
3116
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`.
|
|
3133
|
+
|
|
3134
|
+
44.33 A command `claudeCode.bridgeCommand` or `claudeCode.bridge` mints with `tools` MUST grant
|
|
3135
|
+
those built-in tools after its `--` as `--tools`, and one minted without `tools` MUST carry no `--`.
|
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
|
|
|
@@ -5351,6 +5351,7 @@ test('the model calls the CRM search, through the app', async () => {
|
|
|
5351
5351
|
| `Provider` | What runs the conversation: one method, `run`. Write one for another model, or a recording for a test. |
|
|
5352
5352
|
| `Toolbox` | What a provider is handed: `list` of the tools, and `call` to run one as the asker. |
|
|
5353
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. |
|
|
5354
5355
|
|
|
5355
5356
|
## `claudeApi` sends the request body you wrote
|
|
5356
5357
|
|
|
@@ -5632,6 +5633,8 @@ test('unpaired: asking is refused, the bridge shows a command', async () => {
|
|
|
5632
5633
|
bunx @abide/bridge <site> <code> -- --tools WebSearch,WebFetch
|
|
5633
5634
|
```
|
|
5634
5635
|
|
|
5636
|
+
Pass the same `tools` to `claudeCode.bridge` or `claudeCode.bridgeCommand` and the command it mints carries that grant, so a visitor who runs it as shown gives the tools. One minted without them grants none.
|
|
5637
|
+
|
|
5635
5638
|
The visitor pairs once per visit by running a command, which dials out to the site. Asking with nothing paired is refused 409.
|
|
5636
5639
|
|
|
5637
5640
|
| Name | What it does for the CRM |
|
|
@@ -7835,9 +7838,9 @@ Generated surfaces: what is written in place rather than imported.
|
|
|
7835
7838
|
|
|
7836
7839
|
| Name | Signature | Meaning | Rules |
|
|
7837
7840
|
| --- | --- | --- | --- |
|
|
7838
|
-
| `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.bridgeCommand` | `() => { command: string; expires: number }` | The command that pairs a bridge with this browser. | [44.14](/rulebook#44.14) |
|
|
7840
|
-
| `claudeCode.bridge` | `() => AsyncGenerator<BridgeStatus>` | This browser's pairing, as it changes. | [44.20](/rulebook#44.20), [44.22](/rulebook#44.22) |
|
|
7841
|
+
| `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) |
|
|
7842
|
+
| `claudeCode.bridgeCommand` | `(options?: { tools?: string[] }) => { command: string; expires: number }` | The command that pairs a bridge with this browser, granting the built-in tools it is given. | [44.14](/rulebook#44.14), [44.33](/rulebook#44.33) |
|
|
7843
|
+
| `claudeCode.bridge` | `(options?: { tools?: string[] }) => AsyncGenerator<BridgeStatus>` | This browser's pairing, as it changes. | [44.20](/rulebook#44.20), [44.22](/rulebook#44.22), [44.33](/rulebook#44.33) |
|
|
7841
7844
|
| `/__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) |
|
|
7842
7845
|
|
|
7843
7846
|
## Taught in
|
|
@@ -8293,7 +8296,8 @@ Generated surfaces: what is written in place rather than imported.
|
|
|
8293
8296
|
| `abide:lifecycle` | `debug` `error` | A lifecycle that ran. | [28.8](/rulebook#28.8) |
|
|
8294
8297
|
| `abide:principal` | `debug` `warning` | A principal set or cleared. | [28.8](/rulebook#28.8) |
|
|
8295
8298
|
| `abide:health` | `warning` | An `onHealth` that threw or answered a non-object. | [25.2](/rulebook#25.2), [28.8](/rulebook#28.8) |
|
|
8296
|
-
| `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) |
|
|
8299
|
+
| `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) |
|
|
8300
|
+
| `abide:assistant` | `debug` | One line per conversation, once it ends. | [44.32](/rulebook#44.32) |
|
|
8297
8301
|
| `abide:openapi` | `warning` | A handler whose schema had no JSON Schema export. | [20.11](/rulebook#20.11), [28.8](/rulebook#28.8) |
|
|
8298
8302
|
| `abide:render` | `warning` | An `error.abide` that itself failed. | [28.8](/rulebook#28.8) |
|
|
8299
8303
|
| `abide:refuse` | `warning` | A message formatter that threw. | [15.13](/rulebook#15.13), [28.8](/rulebook#28.8) |
|
|
@@ -8607,7 +8611,7 @@ Generated surfaces: what is written in place rather than imported.
|
|
|
8607
8611
|
| --- | --- | --- |
|
|
8608
8612
|
| `{expr}` | Reactive text, escaped. | [32.1](/rulebook#32.1), [32.23](/rulebook#32.23) |
|
|
8609
8613
|
| formatting whitespace | What a run of whitespace renders as. | [33.17](/rulebook#33.17), [33.18](/rulebook#33.18) |
|
|
8610
|
-
| `{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) |
|
|
8614
|
+
| `{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) |
|
|
8611
8615
|
| `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) |
|
|
8612
8616
|
| `on<event>={fn}` | A native listener on an element, and an ordinary prop on a component. | [32.4](/rulebook#32.4) |
|
|
8613
8617
|
| `name="…{expr}…"` | An interpolated quoted value. | [32.3](/rulebook#32.3) |
|
|
@@ -8627,8 +8631,8 @@ Generated surfaces: what is written in place rather than imported.
|
|
|
8627
8631
|
|
|
8628
8632
|
| Spelling | Branches | Meaning | Rules |
|
|
8629
8633
|
| --- | --- | --- | --- |
|
|
8630
|
-
| `{#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) |
|
|
8631
|
-
| `{#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) |
|
|
8634
|
+
| `{#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) |
|
|
8635
|
+
| `{#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) |
|
|
8632
8636
|
| `{#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) |
|
|
8633
8637
|
|
|
8634
8638
|
## Components
|
|
@@ -8670,7 +8674,7 @@ Generated surfaces: what is written in place rather than imported.
|
|
|
8670
8674
|
|
|
8671
8675
|
| Name | Signature | Meaning | Rules |
|
|
8672
8676
|
| --- | --- | --- | --- |
|
|
8673
|
-
| `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) |
|
|
8677
|
+
| `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) |
|
|
8674
8678
|
|
|
8675
8679
|
## Taught in
|
|
8676
8680
|
|
package/package.json
CHANGED
|
@@ -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`
|
|
@@ -837,7 +837,9 @@ const WRITES = {
|
|
|
837
837
|
// instance OPENS (`shareScope`): a page that shares nothing still carries this for a child that does.
|
|
838
838
|
const SHARE_AT = '__shareAt'
|
|
839
839
|
|
|
840
|
-
// 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.
|
|
841
843
|
const DOCUMENT = '__document'
|
|
842
844
|
|
|
843
845
|
// A run's anchors while it holds markup (32.30), as the JS string literals a built run parses with.
|
|
@@ -845,9 +847,9 @@ const RAW_OPEN_MARKUP = JSON.stringify(anchorMarkup(RAW_OPEN))
|
|
|
845
847
|
const RAW_CLOSE_MARKUP = JSON.stringify(anchorMarkup(RAW_CLOSE))
|
|
846
848
|
|
|
847
849
|
/**
|
|
848
|
-
* 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`.
|
|
849
851
|
*
|
|
850
|
-
* 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
|
|
851
853
|
* row, the arm, the slot's fallback, the caller's children and the page that fills itself are all
|
|
852
854
|
* made the same way. BUILT from the positions the markup walk placed (`emitBuild`) — or, where the
|
|
853
855
|
* region holds content only the parser makes right (`isParserOwned`), PARSED from the very markup
|
|
@@ -868,7 +870,7 @@ function emitMaker(
|
|
|
868
870
|
let parsed = !sink.repeated
|
|
869
871
|
for (const at of sink.positions) parsed ||= at.parserOwned
|
|
870
872
|
if (parsed)
|
|
871
|
-
return `return ${lowering.use('markupNodes')}(${
|
|
873
|
+
return `return ${lowering.use('markupNodes')}(${PARENT}, ${markup()})`
|
|
872
874
|
return emitBuild(sink, anchored, lowering)
|
|
873
875
|
}
|
|
874
876
|
|
|
@@ -903,7 +905,10 @@ function emitMaker(
|
|
|
903
905
|
* (`fillEmpty`), and a component or a slot is the same pair filled by the child.
|
|
904
906
|
*/
|
|
905
907
|
function emitBuild(row: Cursor, anchored: boolean, lowering: Lowering): string {
|
|
906
|
-
const lines: string[] = [
|
|
908
|
+
const lines: string[] = [
|
|
909
|
+
`const ${DOCUMENT} = ${PARENT}.ownerDocument`,
|
|
910
|
+
'const __built = []',
|
|
911
|
+
]
|
|
907
912
|
if (anchored)
|
|
908
913
|
lines.push(
|
|
909
914
|
`__built.push(${DOCUMENT}.createComment(${JSON.stringify(ROW_OPEN)}))`,
|
|
@@ -1000,7 +1005,7 @@ function builtRun(
|
|
|
1000
1005
|
return lines
|
|
1001
1006
|
}
|
|
1002
1007
|
lines.push(
|
|
1003
|
-
`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')}`,
|
|
1004
1009
|
)
|
|
1005
1010
|
if (!at.blank) {
|
|
1006
1011
|
lines.push(`else ${make(whole)}`)
|
|
@@ -1075,7 +1080,7 @@ function emitFillable(
|
|
|
1075
1080
|
const lowering = region.lowering
|
|
1076
1081
|
const filled = emitRegion(sink, region.nested())
|
|
1077
1082
|
region.helpers.push(
|
|
1078
|
-
`function ${name}_build(
|
|
1083
|
+
`function ${name}_build(__parent) {\n ${emitMaker(sink, lowering)}\n}`,
|
|
1079
1084
|
)
|
|
1080
1085
|
region.helpers.push(
|
|
1081
1086
|
mountFunction(
|
|
@@ -1339,7 +1344,7 @@ function emitArms(
|
|
|
1339
1344
|
: arm === 1 && caught !== undefined
|
|
1340
1345
|
? caught
|
|
1341
1346
|
: ''
|
|
1342
|
-
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)}`
|
|
1343
1348
|
if (local === '') {
|
|
1344
1349
|
helpers.push(
|
|
1345
1350
|
`${builders}\nconst ${own}_arm = { build: ${own}_build, mount: ${own}_mount }`,
|
|
@@ -1425,7 +1430,7 @@ function emitList(
|
|
|
1425
1430
|
// MADE FROM THE ITEM'S VALUE: every slot reading the item alone is written before the row is
|
|
1426
1431
|
// inserted, so making a row writes the document once per node (32.67).
|
|
1427
1432
|
const valued = `const ${item} = ${ROW_ROOT}.data`
|
|
1428
|
-
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}`
|
|
1429
1434
|
const walk = rowRegion.body()
|
|
1430
1435
|
// A PATTERN READ OFF THE RECORD is read through one picker a name, the list's rather than the
|
|
1431
1436
|
// row's, so a row allocates nothing for it.
|
|
@@ -1450,7 +1455,7 @@ function emitList(
|
|
|
1450
1455
|
// rebuilds whole: a node that refused is the row disagreeing, and re-making it is the
|
|
1451
1456
|
// answer the page does not have.
|
|
1452
1457
|
opened,
|
|
1453
|
-
`${lowering.use('replaceRow')}(${ROW_ROOT}, ${name}_make
|
|
1458
|
+
`${lowering.use('replaceRow')}(${ROW_ROOT}, ${name}_make, ${at})`,
|
|
1454
1459
|
lowering,
|
|
1455
1460
|
row.defersSetup,
|
|
1456
1461
|
block.setup !== null,
|
|
@@ -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.3.
|
|
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'
|
|
@@ -64,6 +65,7 @@ export type Provider = {
|
|
|
64
65
|
export type AssistantOptions = { mcp?: boolean; maxTurns?: number }
|
|
65
66
|
|
|
66
67
|
const MAX_TURNS = 10
|
|
68
|
+
const ASSISTANT_LOG = namedLogger('abide:assistant')
|
|
67
69
|
// 44.7 — the caller's credentials, and nothing else of their request.
|
|
68
70
|
const CARRIED_HEADERS = ['cookie', 'authorization']
|
|
69
71
|
|
|
@@ -83,10 +85,39 @@ export async function* assistant(
|
|
|
83
85
|
options.mcp === true ? appSurface() : NO_SURFACE,
|
|
84
86
|
askerOf(request()),
|
|
85
87
|
)
|
|
86
|
-
|
|
88
|
+
const events = provider.run(messages, tools, {
|
|
87
89
|
maxTurns: options.maxTurns ?? MAX_TURNS,
|
|
88
90
|
signal: request().signal,
|
|
89
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
|
+
}
|
|
90
121
|
}
|
|
91
122
|
|
|
92
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/claudeCode.ts
CHANGED
|
@@ -42,6 +42,9 @@ export type ClaudeCodeConfig = {
|
|
|
42
42
|
tools?: string[]
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
/** The built-in tools a pairing command grants, as the visitor would after its `--`. 44.33. */
|
|
46
|
+
type PairingOptions = Pick<ClaudeCodeConfig, 'tools'>
|
|
47
|
+
|
|
45
48
|
/** Whether this browser's bridge is connected, and the command that pairs one where it is not. */
|
|
46
49
|
export type BridgeStatus = { paired: boolean; command: string | undefined }
|
|
47
50
|
|
|
@@ -123,12 +126,19 @@ function askingCaller(): string {
|
|
|
123
126
|
return caller
|
|
124
127
|
}
|
|
125
128
|
|
|
126
|
-
/**
|
|
127
|
-
|
|
129
|
+
/**
|
|
130
|
+
* The command that pairs a bridge with this browser, and when its code stops standing. 44.14. The
|
|
131
|
+
* `tools` it is given are written after its `--`, so the visitor running it grants them (44.30, 44.33).
|
|
132
|
+
*/
|
|
133
|
+
function bridgeCommand({ tools }: PairingOptions = {}): {
|
|
134
|
+
command: string
|
|
135
|
+
expires: number
|
|
136
|
+
} {
|
|
128
137
|
const { code, expires } = mintCode(askingCaller())
|
|
129
138
|
const site = `${appOrigin() ?? new URL(request().url).origin}${mounted('')}`
|
|
139
|
+
const grant = tools?.length ? ` -- --tools ${tools.join(',')}` : ''
|
|
130
140
|
return {
|
|
131
|
-
command: `bunx @abide/bridge@${BRIDGE_VERSION} ${site} ${code}`,
|
|
141
|
+
command: `bunx @abide/bridge@${BRIDGE_VERSION} ${site} ${code}${grant}`,
|
|
132
142
|
expires,
|
|
133
143
|
}
|
|
134
144
|
}
|
|
@@ -137,7 +147,9 @@ function bridgeCommand(): { command: string; expires: number } {
|
|
|
137
147
|
* This browser's pairing as it changes: unpaired with the command to run, then paired the moment a
|
|
138
148
|
* bridge connects, and unpaired again if it goes. 44.20.
|
|
139
149
|
*/
|
|
140
|
-
async function* bridge(
|
|
150
|
+
async function* bridge(
|
|
151
|
+
options: PairingOptions = {},
|
|
152
|
+
): AsyncGenerator<BridgeStatus> {
|
|
141
153
|
const caller = askingCaller()
|
|
142
154
|
const signal = request().signal
|
|
143
155
|
// A change landing while the loop was not waiting — paused at its `yield` — stays queued, so it
|
|
@@ -156,7 +168,7 @@ async function* bridge(): AsyncGenerator<BridgeStatus> {
|
|
|
156
168
|
!paired &&
|
|
157
169
|
(pairing === undefined || pairing.expires <= Date.now())
|
|
158
170
|
)
|
|
159
|
-
pairing = bridgeCommand()
|
|
171
|
+
pairing = bridgeCommand(options)
|
|
160
172
|
const status: BridgeStatus = {
|
|
161
173
|
paired,
|
|
162
174
|
command: paired ? undefined : pairing?.command,
|
|
@@ -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/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)
|