@unifedev/thread-pages 1.0.3 → 1.1.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/dist/server.js +3801 -3447
- package/dist/server.meta.json +1 -1
- package/docs/B1-OWN-FILES.md +133 -0
- package/docs/FOR-PAGE-AUTHORS-1.1.md +167 -0
- package/docs/UPGRADING.md +53 -0
- package/package.json +2 -1
- package/src/agent/guide.ts +106 -11
- package/src/agent/seed/seed.ts +21 -17
- package/src/agent/seed/theme-css.ts +5 -0
- package/src/domain/capabilities/protocol.ts +2 -1
- package/src/domain/capabilities/renamed.ts +34 -0
- package/src/domain/limits.ts +10 -0
- package/src/generated/kernel-runtime.ts +1 -1
- package/src/generated/shell-runtime.ts +1 -1
- package/src/pages/inline.ts +277 -0
- package/src/pages/page-store.ts +40 -6
- package/src/plugin.ts +11 -1
package/dist/server.meta.json
CHANGED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# A page's own files, and the workaround that should be deleted
|
|
2
|
+
|
|
3
|
+
**Status:** the workaround shipped. It is temporary by design. This document
|
|
4
|
+
says what it works around, how to know the workaround is no longer needed, and
|
|
5
|
+
exactly what to delete.
|
|
6
|
+
|
|
7
|
+
## The bug
|
|
8
|
+
|
|
9
|
+
A page that loads its own stylesheet or data file renders correctly on
|
|
10
|
+
loopback and **renders empty for the reader on bb Connect** — the origin a
|
|
11
|
+
reader actually uses on a phone. Nothing errors. The author cannot see it from
|
|
12
|
+
the machine that wrote the page, so every fixture in `verify/` passed while the
|
|
13
|
+
feature was broken in the field.
|
|
14
|
+
|
|
15
|
+
Reported as B1 in `unife-bb-plugin/docs/FIELD-ISSUES-2026-09-08.md`, where a
|
|
16
|
+
reader saw a fully functional interface reporting that a 300-row wiki was
|
|
17
|
+
empty.
|
|
18
|
+
|
|
19
|
+
## The cause, measured
|
|
20
|
+
|
|
21
|
+
Unauthenticated GETs, no cookie, 2026-09-09, bb 0.42.1:
|
|
22
|
+
|
|
23
|
+
| Path | loopback `127.0.0.1:38886` | `bart.getbb.app` |
|
|
24
|
+
| --- | --- | --- |
|
|
25
|
+
| `…/plugins/thread-pages/http/page?session=…` | 200 | 401 |
|
|
26
|
+
| `…/threads/<id>/thread-storage/files/index.html` | 200 | 401 |
|
|
27
|
+
| `/api/v1/health` (does not exist) | 404 | 401 |
|
|
28
|
+
| `…/plugins/nonexistent-xyz/http/page` | 404 | **401** |
|
|
29
|
+
|
|
30
|
+
The last row is the one that matters: a plugin that does not exist gets bb's
|
|
31
|
+
own 404 locally and Cloudflare's 401 remotely. **bb Connect authenticates at
|
|
32
|
+
the edge, before bb is reached at all.** The remote responses carry
|
|
33
|
+
`server: cloudflare` and a `cf-ray`, and their body is the Connect sign-in
|
|
34
|
+
gate — `apps/connect/src/worker.ts` in `get-bb/bb`, which ends its credential
|
|
35
|
+
ladder with `signInPage(...)` when no session cookie is present.
|
|
36
|
+
|
|
37
|
+
The page frame is `sandbox="allow-scripts allow-forms"`, so it has an opaque
|
|
38
|
+
origin. Its **document** request is issued by the trusted shell, whose
|
|
39
|
+
site-for-cookies is the Connect host, so the `SameSite=Lax` cookie is attached
|
|
40
|
+
and the document loads. Any **subresource** request issued from inside that
|
|
41
|
+
frame has an opaque origin in its ancestor chain, so its site-for-cookies is
|
|
42
|
+
null, the request counts as cross-site, and the Lax cookie is not attached.
|
|
43
|
+
Both Connect cookies are Lax: `apps/desktop/src/connect-desktop-session.ts`
|
|
44
|
+
hard-codes `sameSite: "lax"`, and the account cookie is better-auth's default.
|
|
45
|
+
|
|
46
|
+
The document-request half and the edge-gate half are measured. The
|
|
47
|
+
cookie-attachment rule is the specification applied to a case that cannot be
|
|
48
|
+
instrumented without devtools on the authenticated remote origin; it is the
|
|
49
|
+
only account consistent with what was observed.
|
|
50
|
+
|
|
51
|
+
### What this rules out
|
|
52
|
+
|
|
53
|
+
**Plugin prefix routes do not fix this.** The `plugin-prefix` site strategy in
|
|
54
|
+
`src/pages/site.ts` and the proposal in
|
|
55
|
+
`unife-bb-plugin/rewrite/07-bb-prefix-routes-proposal.md` move a page's files
|
|
56
|
+
from one path the edge 401s to another path the edge 401s. Prefix routes fix a
|
|
57
|
+
different bug — X17, `fetch()` of a page's own files, refused because bb core
|
|
58
|
+
routes reject `Origin: null` — and they are the substrate the real fix needs.
|
|
59
|
+
They are not the fix.
|
|
60
|
+
|
|
61
|
+
## The workaround, as shipped
|
|
62
|
+
|
|
63
|
+
`src/pages/inline.ts` resolves every relative reference in the entry document
|
|
64
|
+
against the page root **when the document is served**, and rewrites it to a
|
|
65
|
+
`data:` URL. The document is the one artifact whose request is always
|
|
66
|
+
authorised, so it carries the page's files with it.
|
|
67
|
+
|
|
68
|
+
- The URL is rewritten, not the element, so `defer`, `type="module"`, `media`,
|
|
69
|
+
`alt` and every other attribute keep their meaning — and a file containing
|
|
70
|
+
`</script>` cannot break out of the document.
|
|
71
|
+
- `url()` inside a resolved stylesheet is followed, bounded by
|
|
72
|
+
`LIMITS.inlineCssDepth`.
|
|
73
|
+
- Anything absolute, protocol-relative, already a URL, or a bare fragment is
|
|
74
|
+
left exactly as written.
|
|
75
|
+
- Anything missing, over `LIMITS.inlineFileBytes`, or over
|
|
76
|
+
`LIMITS.inlineTotalBytes` is left as written and logged by path and reason.
|
|
77
|
+
The page degrades to its pre-workaround behaviour rather than to a broken
|
|
78
|
+
document.
|
|
79
|
+
- Traversal is refused by `isSafeRelativePath` and reported as `unsafe-path`.
|
|
80
|
+
|
|
81
|
+
Wired in `src/plugin.ts` (one argument to `createPageStore`) and resolved
|
|
82
|
+
inside `PageStore.load`, so the served document, its revision, its ETag and its
|
|
83
|
+
offline copy all describe the same bytes.
|
|
84
|
+
|
|
85
|
+
### What it costs
|
|
86
|
+
|
|
87
|
+
- A page's files are inside the document, so they count against the 5 MiB
|
|
88
|
+
entry limit, and a page over 200 KiB keeps no offline copy. That drop used
|
|
89
|
+
to be silent; `page-store.ts` now logs it with the size and the limit.
|
|
90
|
+
- base64 adds a third.
|
|
91
|
+
- Files are re-read on each document load. Sidecar edits therefore reload an
|
|
92
|
+
open page, which is a better story than before — but a very large data file
|
|
93
|
+
is re-read on every revision poll that returns a body.
|
|
94
|
+
- A `data:` URL module script cannot resolve relative imports.
|
|
95
|
+
|
|
96
|
+
## When to delete it
|
|
97
|
+
|
|
98
|
+
When the host can authorise a sandboxed document's own subresource requests.
|
|
99
|
+
Concretely, when **both** of these have landed in bb:
|
|
100
|
+
|
|
101
|
+
1. **Prefix plugin HTTP routes** — `bb.http.route("GET", "/page/*", …)` with
|
|
102
|
+
`auth: "none"`, so this plugin can serve its own page directory and the
|
|
103
|
+
document URL becomes path-shaped. Branch: `feat/plugin-prefix-http-routes`.
|
|
104
|
+
2. **A Connect page grant** — a short-lived, signed, GET-only credential
|
|
105
|
+
carried as a path segment (`…/http/__grant/<token>/…`, so relative
|
|
106
|
+
references inherit it) that the Connect worker verifies and strips before
|
|
107
|
+
forwarding. Branch: `feat/connect-page-grant`.
|
|
108
|
+
|
|
109
|
+
A cookie change (`SameSite=None`) was considered and rejected: it attaches the
|
|
110
|
+
cookie to every cross-site request to the Connect domain, including subresource
|
|
111
|
+
GETs that carry no `Origin` header for bb's own origin checks to reject.
|
|
112
|
+
|
|
113
|
+
### How to verify it is no longer needed
|
|
114
|
+
|
|
115
|
+
Write a file beside `index.html`, reference it relatively, disable the
|
|
116
|
+
workaround, and read the page **over bb Connect on a phone or with devtools on
|
|
117
|
+
the remote origin** — never on loopback, where it has always worked. This is
|
|
118
|
+
the acceptance criterion `verify/` is missing.
|
|
119
|
+
|
|
120
|
+
### What to delete
|
|
121
|
+
|
|
122
|
+
1. `src/pages/inline.ts` and `test/pages/inline.test.ts`.
|
|
123
|
+
2. The `resolve` argument to `createPageStore` in `src/plugin.ts`, and the
|
|
124
|
+
`PageResolver` type, the `site` field on `LoadedPage`, and the resolve block
|
|
125
|
+
in `PageStore.load` in `src/pages/page-store.ts`.
|
|
126
|
+
3. `inlineFileBytes`, `inlineTotalBytes` and `inlineCssDepth` from
|
|
127
|
+
`src/domain/limits.ts`.
|
|
128
|
+
4. The "How this actually works" block in `ownFiles()` and the added row in
|
|
129
|
+
`limitations()` in `src/agent/guide.ts`, plus the guide tests asserting them.
|
|
130
|
+
5. This document.
|
|
131
|
+
|
|
132
|
+
Keep the offline-copy warning in `page-store.ts`. It was always right; the
|
|
133
|
+
workaround only made it matter sooner.
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# Thread Pages 1.1 — for an agent that already has a page
|
|
2
|
+
|
|
3
|
+
You are reading this because your session has a page you wrote against 1.0.x.
|
|
4
|
+
Nothing you wrote has broken. Some things you were told were impossible are now
|
|
5
|
+
possible, one workaround you were told to use is no longer needed, and two
|
|
6
|
+
things in your page are worth fixing while you are here.
|
|
7
|
+
|
|
8
|
+
Run `bb thread-page guide` for the full current contract. This file is only the
|
|
9
|
+
difference.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 1. Your page's own files work now — on the reader's phone, not just yours
|
|
14
|
+
|
|
15
|
+
**What you were told in 1.0:** subresources "load normally", and `fetch()` of
|
|
16
|
+
your own files is refused.
|
|
17
|
+
|
|
18
|
+
**What was actually true:** a file beside `index.html` loaded on loopback and
|
|
19
|
+
was **refused on any authenticated origin**, including bb Connect, which is how
|
|
20
|
+
your reader opens the page on a phone. Your page rendered; its stylesheet and
|
|
21
|
+
data did not; nothing errored. If you built anything that reads a file beside
|
|
22
|
+
`index.html`, it has probably been silently empty for your reader this whole
|
|
23
|
+
time, and you could not have seen it from your own machine.
|
|
24
|
+
|
|
25
|
+
**What is true now:** the host resolves every relative reference when it serves
|
|
26
|
+
the document and rewrites it to a `data:` URL. Write the reference; it works on
|
|
27
|
+
every origin.
|
|
28
|
+
|
|
29
|
+
<link rel="stylesheet" href="page.css">
|
|
30
|
+
<script src="data.js"></script>
|
|
31
|
+
<img src="figures/chart.png" alt="…">
|
|
32
|
+
|
|
33
|
+
`url()` inside a stylesheet you reference is followed too, so backgrounds and
|
|
34
|
+
`@font-face` survive.
|
|
35
|
+
|
|
36
|
+
### What to do about it
|
|
37
|
+
|
|
38
|
+
**If you inlined data into `index.html` because the guide told you to** — move
|
|
39
|
+
it back out. `<script src="data.js">` works, and it makes your entry document
|
|
40
|
+
small again, which makes it cheap to edit.
|
|
41
|
+
|
|
42
|
+
**If your page has a file beside it and shows zeroes** — that was this bug. It
|
|
43
|
+
should be right now; no change needed on your side.
|
|
44
|
+
|
|
45
|
+
## 2. You can publish new data without rewriting `index.html`
|
|
46
|
+
|
|
47
|
+
Changing a file beside `index.html` changes the served document, so an open page
|
|
48
|
+
reloads within about ten seconds. That is the whole live-data mechanism: write
|
|
49
|
+
`data.js`, the reader's page updates.
|
|
50
|
+
|
|
51
|
+
Two things to get right, both in the guide under *Keeping a page's data current*:
|
|
52
|
+
|
|
53
|
+
- **Make the write deterministic.** An unchanged data set must produce a
|
|
54
|
+
byte-identical file. A generated timestamp in the payload turns every rebuild
|
|
55
|
+
into a reload for every reader, and your page will look like it is flickering
|
|
56
|
+
for no reason.
|
|
57
|
+
- **Call `setDirty(true)`** while the reader is mid-edit in state the host
|
|
58
|
+
cannot see. Captured forms do this for you; your own widgets do not.
|
|
59
|
+
|
|
60
|
+
## 3. Your files now count against the document's size
|
|
61
|
+
|
|
62
|
+
Because they are carried inside the document:
|
|
63
|
+
|
|
64
|
+
| | |
|
|
65
|
+
| --- | --- |
|
|
66
|
+
| Entry document | 5 MiB, including everything it carries |
|
|
67
|
+
| Per file | 2 MiB |
|
|
68
|
+
| All files together | 3 MiB |
|
|
69
|
+
| Offline copy kept | only under 200 KiB total |
|
|
70
|
+
| base64 | adds a third to all of the above |
|
|
71
|
+
|
|
72
|
+
A file that is missing, too large or over the budget is **left exactly as you
|
|
73
|
+
wrote it** and named in the log — `bb plugin logs thread-pages`. Your page still
|
|
74
|
+
renders; that one reference does not resolve. Check the log once after you
|
|
75
|
+
publish; it is the only place this is reported.
|
|
76
|
+
|
|
77
|
+
If your page carries a large dataset and you care about it opening offline, keep
|
|
78
|
+
the document under 200 KiB. The host now warns in the log when it drops the
|
|
79
|
+
offline copy instead of dropping it silently.
|
|
80
|
+
|
|
81
|
+
## 4. `[hidden]` — fix this in your page by hand
|
|
82
|
+
|
|
83
|
+
A class rule that sets `display` outranks the user-agent rule for the `hidden`
|
|
84
|
+
attribute, so `<p class="banner" hidden>` rendered as an empty coloured bar. New
|
|
85
|
+
pages get the fix in their seeded stylesheet. **Your page carries its own copy of
|
|
86
|
+
the stylesheet and will not get it.** Add this to your `<style>`:
|
|
87
|
+
|
|
88
|
+
[hidden] { display: none !important; }
|
|
89
|
+
|
|
90
|
+
If you have ever written `hidden` on an element and been confused by a stray
|
|
91
|
+
empty bar in a screenshot, this was why.
|
|
92
|
+
|
|
93
|
+
## 5. Delete the old seed comment from your page
|
|
94
|
+
|
|
95
|
+
The 1.0 seed's authoring comment spelled HTML tags out literally — `<main>`,
|
|
96
|
+
`<style>`, `<script src="app.js">`. They are comment text, not elements, but
|
|
97
|
+
every string operation you run on your own file sees them. A structural check on
|
|
98
|
+
a healthy page reports two `<main>` elements and one unclosed `<script>`, and
|
|
99
|
+
`indexOf("<main>")` finds the comment before the real element, so the obvious
|
|
100
|
+
splice starts in the wrong place.
|
|
101
|
+
|
|
102
|
+
**If that comment is still in your `index.html`, delete it or rewrite it without
|
|
103
|
+
angle brackets.** It is costing you every time you edit your page. The current
|
|
104
|
+
seed names no tags at all.
|
|
105
|
+
|
|
106
|
+
## 6. Edit your page by rewriting it, not by splicing
|
|
107
|
+
|
|
108
|
+
There is no page-editing command and there is not meant to be one: `index.html`
|
|
109
|
+
is a file in your storage directory that you read and write with your ordinary
|
|
110
|
+
tools. **Nothing in it is reserved** — not the stylesheet, not the header, not
|
|
111
|
+
the comment it came with. Rewrite the whole document. A splice computed from
|
|
112
|
+
string indices can silently eat content; a whole-document write cannot.
|
|
113
|
+
|
|
114
|
+
This got much cheaper in 1.1: with §1 fixed, your stylesheet can live in
|
|
115
|
+
`page.css` instead of taking up 27 KB of the file you are rewriting.
|
|
116
|
+
|
|
117
|
+
## 7. If your page should stay put, it must not carry a form
|
|
118
|
+
|
|
119
|
+
A page nobody should have to rewrite — a dashboard, a console — holds no
|
|
120
|
+
captured `<form>`, because a form messages your session and something then has
|
|
121
|
+
to rewrite the page. Its buttons should call `sessions.start` and `pages.open`
|
|
122
|
+
instead. The 1.0 seed shipped a reply form, which read as an endorsement of the
|
|
123
|
+
wrong default; the current seed says to delete it.
|
|
124
|
+
|
|
125
|
+
**A page may now also be build output** — a script generating it from
|
|
126
|
+
version-controlled source is legitimate. The rule that does not bend: the page
|
|
127
|
+
still has one owning session, and that session's agent builds it the first time.
|
|
128
|
+
A page with no agent behind it is a page nobody can be asked to change.
|
|
129
|
+
|
|
130
|
+
## 8. If you call a 0.3.x method, the error now tells you the new name
|
|
131
|
+
|
|
132
|
+
Unknown capability: threads.spawn (renamed to sessions.start in 1.0; there is no alias)
|
|
133
|
+
|
|
134
|
+
The full table is in `docs/UPGRADING.md`. There are still no aliases; the old
|
|
135
|
+
name still fails.
|
|
136
|
+
|
|
137
|
+
## 9. Before you save, read it over the reader's real origin
|
|
138
|
+
|
|
139
|
+
The old checklist covered 320px, dark mode and reduced motion — all things you
|
|
140
|
+
can check locally. It missed the only axis where behaviour actually differs:
|
|
141
|
+
**authentication.** A local bb requires no credential and a remote one does.
|
|
142
|
+
Read your page once over bb Connect, not only loopback. That is what would have
|
|
143
|
+
caught §1 two days earlier.
|
|
144
|
+
|
|
145
|
+
## What is still not possible
|
|
146
|
+
|
|
147
|
+
Unchanged in 1.1, so do not spend time on them:
|
|
148
|
+
|
|
149
|
+
- `fetch("data.json")` of your own file from page script. Only references in the
|
|
150
|
+
document are resolved. Use `<script src="data.js">` or inline it.
|
|
151
|
+
- Embedding another page or site in an `<iframe>` (`frame-src 'none'`).
|
|
152
|
+
- `window.open`, `window.prompt`, `alert`, `confirm`, top-level navigation.
|
|
153
|
+
- Reading a file from the project your session belongs to.
|
|
154
|
+
- Reading or writing another agent's page, and any registry that would let one
|
|
155
|
+
page list itself on another. If two pages should look alike, read the other
|
|
156
|
+
page and copy from it.
|
|
157
|
+
- `voice.captureAndTranscribe`.
|
|
158
|
+
|
|
159
|
+
## A five-minute pass over your page
|
|
160
|
+
|
|
161
|
+
1. `bb thread-page init` — confirm it still says EXISTING and prints no warning.
|
|
162
|
+
2. Add `[hidden] { display: none !important; }` to your `<style>` (§4).
|
|
163
|
+
3. Delete the old seed comment if it is still there (§5).
|
|
164
|
+
4. If you inlined data to work around §1, move it into a file beside the page.
|
|
165
|
+
5. If the page should stay put, remove any captured `<form>` (§7).
|
|
166
|
+
6. `bb plugin logs thread-pages` — check nothing of yours failed to resolve (§3).
|
|
167
|
+
7. Read it once over bb Connect, on a phone if you can (§9).
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Upgrading
|
|
2
|
+
|
|
3
|
+
## 0.3.x → 1.0
|
|
4
|
+
|
|
5
|
+
1.0.0 was a rewrite against the specification, and it renamed the whole
|
|
6
|
+
page-facing surface at once. Nothing is aliased: an old name fails rather than
|
|
7
|
+
silently doing the right thing. A page written against 0.3.x therefore breaks
|
|
8
|
+
in several places at the same time, each of them quietly.
|
|
9
|
+
|
|
10
|
+
| 0.3.x | 1.0 |
|
|
11
|
+
| --- | --- |
|
|
12
|
+
| `thread-page.html` | `index.html` |
|
|
13
|
+
| `thread-page-assets/` | any file beside the entry document, nested paths included |
|
|
14
|
+
| `threads.spawn` | `sessions.start` |
|
|
15
|
+
| `threads.snapshot` | `sessions.snapshot` |
|
|
16
|
+
| `threads.send` | `sessions.send` |
|
|
17
|
+
| `threads.stop` | `sessions.stop` |
|
|
18
|
+
| `threads.archive` | `sessions.archive` |
|
|
19
|
+
| `threads.openPage` | `pages.open` |
|
|
20
|
+
| `threads.openBb` | `sessions.openHost` |
|
|
21
|
+
| `thread.get` | `context.get` |
|
|
22
|
+
| `thread.reply` / `threads.reply` | `session.reply` |
|
|
23
|
+
| `thread.activity` / `threads.activity` | `session.activity` |
|
|
24
|
+
| `page.storage.get` / `page.storage.set` | `storage.get` / `storage.set` |
|
|
25
|
+
| `navigation.open` | `navigation.openExternal` |
|
|
26
|
+
| `homeThreadId` (plugin setting) | `homeSessionId` (`bb thread-page status`) |
|
|
27
|
+
| `window.threadPage.assetUrl()` | removed — the API is frozen at `invoke` / `watch` / `setDirty` / `version` |
|
|
28
|
+
|
|
29
|
+
### The two that cost the most
|
|
30
|
+
|
|
31
|
+
**The entry document was renamed.** `thread-page.html` is not served and not
|
|
32
|
+
migrated. A session that had a page looks like a session that never had one.
|
|
33
|
+
`bb thread-page init` notices the old file and says so; move what you want out
|
|
34
|
+
of it by hand.
|
|
35
|
+
|
|
36
|
+
**`assetUrl()` was removed rather than deprecated**, so a page that calls it
|
|
37
|
+
throws instead of degrading. Reference files relatively instead.
|
|
38
|
+
|
|
39
|
+
### What the host tells you now
|
|
40
|
+
|
|
41
|
+
Calling a renamed method returns `unknown_method` with the replacement named:
|
|
42
|
+
|
|
43
|
+
Unknown capability: threads.spawn (renamed to sessions.start in 1.0; there is no alias)
|
|
44
|
+
|
|
45
|
+
The table above is the complete list the host recognises; see
|
|
46
|
+
`src/domain/capabilities/renamed.ts`.
|
|
47
|
+
|
|
48
|
+
### Not a rename
|
|
49
|
+
|
|
50
|
+
`sessions.snapshot` also changed behaviour in 1.0.2: it lists root sessions
|
|
51
|
+
only unless you pass `includeChildren: true`, and every session now carries
|
|
52
|
+
`unread` and `attentionAtMs`. Check `bb thread-page guide` for the current
|
|
53
|
+
shape rather than assuming the 0.3.x one.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unifedev/thread-pages",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Thread Pages for bb: every agent session gets one directly editable HTML page you can read and answer from, on any device.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Bartosz",
|
|
@@ -66,6 +66,7 @@
|
|
|
66
66
|
"!src/**/*.test.ts",
|
|
67
67
|
"dist/server.js",
|
|
68
68
|
"dist/server.meta.json",
|
|
69
|
+
"docs/*.md",
|
|
69
70
|
"README.md",
|
|
70
71
|
"tsconfig.json",
|
|
71
72
|
"package-lock.json"
|
package/src/agent/guide.ts
CHANGED
|
@@ -17,6 +17,7 @@ export function buildGuide(registry: CapabilityRegistry, site: SiteStrategy): st
|
|
|
17
17
|
forms(),
|
|
18
18
|
uploads(),
|
|
19
19
|
ownFiles(site),
|
|
20
|
+
keepingCurrent(),
|
|
20
21
|
runtimeApi(),
|
|
21
22
|
capabilities(registry),
|
|
22
23
|
startingSessions(),
|
|
@@ -25,6 +26,7 @@ export function buildGuide(registry: CapabilityRegistry, site: SiteStrategy): st
|
|
|
25
26
|
composition(),
|
|
26
27
|
home(),
|
|
27
28
|
accessibility(),
|
|
29
|
+
upgrading(),
|
|
28
30
|
limits(),
|
|
29
31
|
limitations(site),
|
|
30
32
|
].join("\n\n");
|
|
@@ -145,20 +147,65 @@ spaces and punctuation in names are all fine:
|
|
|
145
147
|
|
|
146
148
|
No permission, no declaration, no API: writing a file into your page root is
|
|
147
149
|
enough. Keep everything inside your own page root; another agent's page is
|
|
148
|
-
not yours to write
|
|
150
|
+
not yours to write.
|
|
151
|
+
|
|
152
|
+
**How this actually works, because it constrains what you can do.** Your page
|
|
153
|
+
runs in a sandbox on an opaque origin, and a request it makes for itself
|
|
154
|
+
carries no credential. A bb on loopback asks for none and the file arrives; a
|
|
155
|
+
bb reached over an authenticated origin — which is how the reader opens the
|
|
156
|
+
page on a phone — refuses it. So the host resolves your relative references
|
|
157
|
+
**when it serves the document**: each one is read from your page root and
|
|
158
|
+
rewritten to a \`data:\` URL before the reader's browser ever sees it. The
|
|
159
|
+
consequences worth knowing:
|
|
160
|
+
|
|
161
|
+
- It works the same on every origin. Write the reference; do not work around it.
|
|
162
|
+
- Your files are **inside the document**, so they count against the ${mebibytes(LIMITS.entryDocumentBytes)}
|
|
163
|
+
entry limit, and a page over ${kibibytes(LIMITS.offlineCopyBytes)} keeps no offline copy. Per file at
|
|
164
|
+
most ${mebibytes(LIMITS.inlineFileBytes)}, ${mebibytes(LIMITS.inlineTotalBytes)} across the page; base64 adds a third to both.
|
|
165
|
+
- A file that is missing, too large or over the budget is **left as you wrote
|
|
166
|
+
it** and named in the plugin log (\`bb plugin logs thread-pages\`). The page
|
|
167
|
+
still renders; that one reference does not resolve.
|
|
168
|
+
- \`url()\` inside a stylesheet you reference is followed too, so backgrounds
|
|
169
|
+
and \`@font-face\` survive. Absolute and remote URLs are never touched.
|
|
170
|
+
- Changing a file beside ${ENTRY_FILE} changes the document, so an open page
|
|
171
|
+
reloads — see *Keeping a page's data current*. You do not have to touch
|
|
172
|
+
${ENTRY_FILE} to publish new data.
|
|
173
|
+
${
|
|
149
174
|
site.name === "core-storage"
|
|
150
175
|
? `
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
fetches work (see Network).`
|
|
176
|
+
**One limitation left on this host:** \`fetch("data.json")\` of your own file
|
|
177
|
+
from page script is refused (403) — the host's file route rejects the
|
|
178
|
+
sandbox's \`Origin: null\`, and only subresource references are resolved for
|
|
179
|
+
you. Load data with <script src="data.js"> or inline it in the document.
|
|
180
|
+
Remote fetches work (see Network).`
|
|
157
181
|
: `
|
|
158
|
-
|
|
159
182
|
Page script may also fetch its own files as data: \`await fetch("data.json")\`.`
|
|
160
183
|
}`;
|
|
161
184
|
|
|
185
|
+
const keepingCurrent = () => `## Keeping a page's data current
|
|
186
|
+
|
|
187
|
+
The entry document is the only artifact guaranteed to reach every reader, on
|
|
188
|
+
every origin. Rewriting it is therefore how you push new data to an open page:
|
|
189
|
+
the shell notices the new revision within ${LIMITS.shellPollMs / 1000} s and reloads the page under
|
|
190
|
+
the reader, preserving what they were typing. You do not need a poller, a
|
|
191
|
+
sidecar or a socket for this — a page that follows a data source is a page
|
|
192
|
+
something rewrites.
|
|
193
|
+
|
|
194
|
+
Three things to get right:
|
|
195
|
+
|
|
196
|
+
- **Make the build deterministic.** An unchanged data set must produce a
|
|
197
|
+
byte-identical document. This is the non-obvious half: a generated timestamp
|
|
198
|
+
in the payload turns every rebuild into a reload for every reader, and the
|
|
199
|
+
page will look like it is flickering for no reason.
|
|
200
|
+
- **Set \`setDirty(true)\` while the reader is mid-edit** in state the host
|
|
201
|
+
cannot see. A captured form does this for you; your own widgets do not.
|
|
202
|
+
- **Refresh on a slow watch, not a tight timer.** A page shares a budget of
|
|
203
|
+
${LIMITS.ratePerMinute} requests a minute with its own forms.
|
|
204
|
+
|
|
205
|
+
\`window.threadPage.watch\` is the other half, for live host state — sessions,
|
|
206
|
+
activity — that does not live in your file. Use the document rewrite for data
|
|
207
|
+
you generate, and \`watch\` for data the host owns.`;
|
|
208
|
+
|
|
162
209
|
const runtimeApi = () => `## window.threadPage
|
|
163
210
|
|
|
164
211
|
The complete page-facing API; it is frozen and cannot be replaced.
|
|
@@ -270,7 +317,23 @@ writes its own page. Link to it with \`pages.open\`, or suggest making it home.
|
|
|
270
317
|
If you want another agent's page changed, send that agent a message with
|
|
271
318
|
\`sessions.send\` rather than editing its file. Do not create a session merely
|
|
272
319
|
to hold a page: a page that stays put is owned by a real agent that built it
|
|
273
|
-
and then stopped
|
|
320
|
+
and then stopped.
|
|
321
|
+
|
|
322
|
+
**The whole file is yours.** There is no page-editing API and there is not
|
|
323
|
+
meant to be one: ${ENTRY_FILE} is a file in your storage directory that you
|
|
324
|
+
read and write with your ordinary tools. Nothing in it is reserved — not the
|
|
325
|
+
stylesheet, not the header, not the comment the seed came with. Rewriting the
|
|
326
|
+
document whole is the expected way to change it, and safer than splicing,
|
|
327
|
+
because a splice computed from string indices can silently eat content that a
|
|
328
|
+
whole-document write cannot.
|
|
329
|
+
|
|
330
|
+
**A page may be build output.** A repository script generating pages into
|
|
331
|
+
several sessions' storage — so a team gets one identical interface from a
|
|
332
|
+
checkout rather than from three agents independently writing HTML — is
|
|
333
|
+
legitimate. The rule that does not bend: every page still has one owning
|
|
334
|
+
session, and that session's agent builds the page the first time, whether or
|
|
335
|
+
not a script takes over afterwards. A page with no agent behind it is a page
|
|
336
|
+
nobody can be asked to change.`;
|
|
274
337
|
|
|
275
338
|
const home = () => `## The home page
|
|
276
339
|
|
|
@@ -316,7 +379,36 @@ const accessibility = () => `## Before you save
|
|
|
316
379
|
- Every action reachable by keyboard; nothing pointer-only.
|
|
317
380
|
- Inline SVG for diagrams and charts, with var(--accent) inside it; a zero
|
|
318
381
|
gets a visible stub or the eye reads missing data.
|
|
319
|
-
- grep -o '#[0-9a-fA-F]\\{3,8\\}' ${ENTRY_FILE} inside your <style> should be empty
|
|
382
|
+
- grep -o '#[0-9a-fA-F]\\{3,8\\}' ${ENTRY_FILE} inside your <style> should be empty.
|
|
383
|
+
- Read it once over the reader's real origin, not only loopback. A local bb
|
|
384
|
+
requires no credential and a remote one does, so anything the page loads for
|
|
385
|
+
itself can work for you and fail for them. Authentication is the one axis
|
|
386
|
+
where behaviour genuinely differs between your machine and theirs.`;
|
|
387
|
+
|
|
388
|
+
const upgrading = () => `## If your page predates 1.1
|
|
389
|
+
|
|
390
|
+
Three things to fix in a page written against 1.0.x. Each is a one-line edit
|
|
391
|
+
and none of them announces itself.
|
|
392
|
+
|
|
393
|
+
1. **Add \`[hidden] { display: none !important; }\`** to your <style>. A class
|
|
394
|
+
rule that sets display outranks the attribute, so an element you wrote
|
|
395
|
+
\`hidden\` renders as an empty bar. New pages carry the fix; yours has its
|
|
396
|
+
own copy of the stylesheet and will not get it.
|
|
397
|
+
2. **Delete the seed's old authoring comment** if it is still there. It spelled
|
|
398
|
+
tags out literally, so every string operation you run on your own file sees
|
|
399
|
+
a <main> and a <style> that are not elements, and the obvious splice starts
|
|
400
|
+
inside the comment.
|
|
401
|
+
3. **Move inlined data back out.** 1.0 told you to inline anything the page
|
|
402
|
+
could not do without, because a file beside the page failed on the reader's
|
|
403
|
+
origin. That is fixed: reference it relatively and it works everywhere. Your
|
|
404
|
+
entry document gets small again, which makes it cheap to rewrite.
|
|
405
|
+
|
|
406
|
+
Then check \`bb plugin logs thread-pages\` once, and read the page over the
|
|
407
|
+
reader's real origin rather than loopback.
|
|
408
|
+
|
|
409
|
+
Full notes, including what still is not possible:
|
|
410
|
+
\`docs/FOR-PAGE-AUTHORS-1.1.md\` in the plugin, and \`docs/UPGRADING.md\` for
|
|
411
|
+
the 0.3.x method names.`;
|
|
320
412
|
|
|
321
413
|
const limits = () => `## Limits
|
|
322
414
|
|
|
@@ -351,5 +443,8 @@ const limitations = (site: SiteStrategy) => `## Known limitations
|
|
|
351
443
|
generic message; the cause is in the plugin log (\`bb plugin logs thread-pages\`).
|
|
352
444
|
- Embedding another page or site in an <iframe> is blocked (frame-src 'none').
|
|
353
445
|
- \`voice.captureAndTranscribe\` is not implemented: unknown_method.${
|
|
354
|
-
site.name === "core-storage"
|
|
446
|
+
site.name === "core-storage"
|
|
447
|
+
? `\n- fetch() of your own files from page script is refused on this host (see Files you show the reader).` +
|
|
448
|
+
`\n- Your own files are carried inside the entry document rather than served as files, because this host cannot authorise a sandboxed document's own requests. That is why they count against the document's size limits.`
|
|
449
|
+
: ""
|
|
355
450
|
}`;
|
package/src/agent/seed/seed.ts
CHANGED
|
@@ -24,27 +24,31 @@ export const DEFAULT_PAGE_SEED = `<!doctype html>
|
|
|
24
24
|
</header>
|
|
25
25
|
|
|
26
26
|
<!--
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
blocked on the reader, .label is a small uppercase tag.
|
|
27
|
+
Everything below is yours: this whole file, including the stylesheet in
|
|
28
|
+
the head. Rewrite it however the task needs. Two things the file cannot
|
|
29
|
+
tell you, because they are behaviour rather than markup:
|
|
31
30
|
|
|
32
|
-
Every
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
1. Every form element in this document is captured and delivered to this
|
|
32
|
+
session as a message. That includes a dialog form you only meant as a
|
|
33
|
+
local confirm; put data-thread-page-manual on any form that is not
|
|
34
|
+
meant to answer. Nothing is required and blank is a real answer.
|
|
35
|
+
2. window.prompt, alert, confirm and window.open do nothing here — the
|
|
36
|
+
sandbox silences them. Build the input into the page instead.
|
|
35
37
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
The look is three attributes on the html element. data-theme: paper,
|
|
39
|
+
terminal, atrium, volume or bloom. data-mode: system, light or dark.
|
|
40
|
+
data-atmos: on or off.
|
|
39
41
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
colour and shape from var(--token) only.
|
|
42
|
+
If this page should stay put — a dashboard, a console, a page nobody
|
|
43
|
+
should have to rewrite — delete the reply form below and let its buttons
|
|
44
|
+
start fresh sessions instead. See the guide.
|
|
44
45
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
Everything else — files beside this one, charts, live session state,
|
|
47
|
+
starting sessions, links, limits — is in: bb thread-page guide
|
|
48
|
+
|
|
49
|
+
This comment deliberately names no HTML tags. An earlier version spelled
|
|
50
|
+
them out, and every agent that edited its page by string surgery found
|
|
51
|
+
tags here that were not in the document. Keep it that way.
|
|
48
52
|
-->
|
|
49
53
|
<main>
|
|
50
54
|
<p>Replace this with what changed and what you need from the reader.</p>
|
|
@@ -37,6 +37,11 @@ export const THEME_CSS = String.raw`
|
|
|
37
37
|
|
|
38
38
|
*, *::before, *::after { box-sizing: border-box; }
|
|
39
39
|
|
|
40
|
+
/* A class rule that sets display outranks the user-agent rule for the
|
|
41
|
+
hidden attribute, so a page that styles .banner with display:flex would
|
|
42
|
+
render a hidden banner as an empty bar. This keeps hidden meaning hidden. */
|
|
43
|
+
[hidden] { display: none !important; }
|
|
44
|
+
|
|
40
45
|
html { -webkit-text-size-adjust: 100%; }
|
|
41
46
|
|
|
42
47
|
body {
|
|
@@ -4,6 +4,7 @@ import { isJsonObject, validateJson, type JsonValue } from "../json/strict-json.
|
|
|
4
4
|
import { LIMITS } from "../limits.ts";
|
|
5
5
|
import type { CapabilityRegistry } from "./registry.ts";
|
|
6
6
|
import type { AnyCapabilitySpec } from "./contract.ts";
|
|
7
|
+
import { unknownMethodMessage } from "./renamed.ts";
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* The bridge protocol: what a page sends over the port and what it gets back.
|
|
@@ -89,7 +90,7 @@ export interface ResolvedInvocation<Params = unknown> {
|
|
|
89
90
|
export function resolveInvocation(request: BridgeRequest, registry: CapabilityRegistry, currentRevision: string): ResolvedInvocation {
|
|
90
91
|
if (request.pageRevision !== currentRevision) throw new PageError("stale_page", "This page changed; reload it before responding.");
|
|
91
92
|
const spec = registry.get(request.method);
|
|
92
|
-
if (!spec || !spec.implemented) throw new PageError("unknown_method",
|
|
93
|
+
if (!spec || !spec.implemented) throw new PageError("unknown_method", unknownMethodMessage(request.method));
|
|
93
94
|
const params = spec.validateParams(request.params);
|
|
94
95
|
if (!params.ok) {
|
|
95
96
|
const first = params.issues[0];
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Names the 0.3.x API answered for, when a page still calls it. spec R5.38–R5.41
|
|
3
|
+
*
|
|
4
|
+
* 1.0.0 renamed the whole page-facing surface with no migration path, and the
|
|
5
|
+
* only signal a page got back was `unknown_method: threads.spawn` — which
|
|
6
|
+
* reads as "this host cannot do that" rather than "this is called something
|
|
7
|
+
* else now". Naming the replacement turns a debugging session into a one-line
|
|
8
|
+
* fix. There are no aliases and there will be none: the old name still fails.
|
|
9
|
+
*/
|
|
10
|
+
export const RENAMED_METHODS: Readonly<Record<string, string>> = Object.freeze({
|
|
11
|
+
"threads.spawn": "sessions.start",
|
|
12
|
+
"threads.snapshot": "sessions.snapshot",
|
|
13
|
+
"threads.send": "sessions.send",
|
|
14
|
+
"threads.stop": "sessions.stop",
|
|
15
|
+
"threads.archive": "sessions.archive",
|
|
16
|
+
"threads.activity": "session.activity",
|
|
17
|
+
"threads.reply": "session.reply",
|
|
18
|
+
"threads.openPage": "pages.open",
|
|
19
|
+
"threads.openBb": "sessions.openHost",
|
|
20
|
+
"thread.get": "context.get",
|
|
21
|
+
"thread.reply": "session.reply",
|
|
22
|
+
"thread.activity": "session.activity",
|
|
23
|
+
"page.storage.get": "storage.get",
|
|
24
|
+
"page.storage.set": "storage.set",
|
|
25
|
+
"navigation.open": "navigation.openExternal",
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
/** The `unknown_method` message for a method, naming its replacement when there is one. */
|
|
29
|
+
export function unknownMethodMessage(method: string): string {
|
|
30
|
+
const replacement = RENAMED_METHODS[method];
|
|
31
|
+
return replacement
|
|
32
|
+
? `Unknown capability: ${method} (renamed to ${replacement} in 1.0; there is no alias)`
|
|
33
|
+
: `Unknown capability: ${method}`;
|
|
34
|
+
}
|