@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.
@@ -3,7 +3,7 @@
3
3
  "sdkVersion": "0.4.47",
4
4
  "artifactFormatVersion": 1,
5
5
  "pluginId": "thread-pages",
6
- "pluginVersion": "1.0.3",
6
+ "pluginVersion": "1.1.0",
7
7
  "builtWith": {
8
8
  "bbVersion": "0.42.1",
9
9
  "pluginSdkVersion": "0.4.47"
@@ -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",
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"
@@ -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
- **One limitation on this host:** \`fetch("data.json")\` of your own file from
153
- page script is refused (403) the host's file route rejects the sandbox's
154
- \`Origin: null\`. Subresources (<script>, <link>, <img>) load normally, so
155
- load data with <script src="data.js"> or inline it in the document. Remote
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" ? `\n- fetch() of your own files from page script is refused on this host (see Files you show the reader).` : ""
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
  }`;
@@ -24,27 +24,31 @@ export const DEFAULT_PAGE_SEED = `<!doctype html>
24
24
  </header>
25
25
 
26
26
  <!--
27
- Write inside <main>. Plain semantic HTML is already styled: h2, p, ul,
28
- table, form, fieldset/legend, a wrapping label, small, details. Three
29
- class names exist: .card boxes an aside, .needs-you flags a block that is
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 <form> answers this session automatically unless it carries
33
- data-thread-page-manual. Blank answers are valid. A <form method="dialog">
34
- you only meant as a local confirm still sends a message unless it opts out.
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
- Files you put beside this index.html are served relatively: <img
37
- src="chart.png">, <link href="page.css">, <script src="app.js">, nested
38
- paths included. Ordinary <a href="https://…"> links work.
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
- data-theme: paper | terminal | atrium | volume | bloom.
41
- data-mode: system | light | dark. data-atmos: on | off.
42
- Extra CSS goes in one more <style>, everything inside @scope (main),
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
- Never use window.prompt, alert, confirm or window.open: the sandbox
46
- silences them. For anything more charts, files, live session state,
47
- starting sessions, links — run: bb thread-page guide
46
+ Everything else — files beside this one, charts, live session state,
47
+ starting sessions, links, limitsis 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", `Unknown capability: ${request.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
+ }