@unifedev/thread-pages 0.3.2
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/ARCHITECTURE.md +230 -0
- package/PLUGIN_OVERVIEW.md +83 -0
- package/README.md +153 -0
- package/authoring.ts +368 -0
- package/bridge.ts +1721 -0
- package/dist/server.js +12830 -0
- package/dist/server.meta.json +11 -0
- package/docs/MODEL.md +211 -0
- package/docs/ROADMAP.md +96 -0
- package/home.ts +419 -0
- package/package.json +66 -0
- package/page.ts +782 -0
- package/server.ts +2179 -0
- package/theme.ts +676 -0
- package/tsconfig.json +15 -0
package/authoring.ts
ADDED
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_HOME_BODY,
|
|
3
|
+
DEFAULT_HOME_SCRIPT,
|
|
4
|
+
DEFAULT_HOME_STYLE,
|
|
5
|
+
} from "./home.js";
|
|
6
|
+
import { escapeHtml } from "./page.js";
|
|
7
|
+
import { THEME_CSS } from "./theme.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The standing contract, injected into eligible root threads.
|
|
11
|
+
*
|
|
12
|
+
* It carries why the page matters and how to ask well, because those are what
|
|
13
|
+
* actually change an agent's behaviour. Everything mechanical - form wiring,
|
|
14
|
+
* styling, labels, blank handling - is absorbed by the runtime and the seed, so
|
|
15
|
+
* none of it is spent here. Anything deeper is one guide command away, paid for
|
|
16
|
+
* only by the sessions that need it.
|
|
17
|
+
*/
|
|
18
|
+
export const DEFAULT_AGENT_INSTRUCTION = `# The page is the conversation
|
|
19
|
+
|
|
20
|
+
The user does not read chat. Every turn you write or update one HTML page, they
|
|
21
|
+
read it and reply from inside it, and their answer arrives as your next message.
|
|
22
|
+
Everything they need must be on that page, and every action they might take must
|
|
23
|
+
be possible from it — including the ones you would rather they did not choose. A
|
|
24
|
+
page they cannot answer from is a dead end. Chat carries the link and one line.
|
|
25
|
+
|
|
26
|
+
Start every turn with \`bb thread-page init\`. It prints the page path and link.
|
|
27
|
+
Read an existing page before editing it; saving publishes it immediately and an
|
|
28
|
+
open page reloads itself. Update it on every turn, including small ones. If init
|
|
29
|
+
says SKIP this thread is a helper — answer in chat and stay off the page. When
|
|
30
|
+
you spawn threads of your own, parent them to yourself so they stay helpers.
|
|
31
|
+
|
|
32
|
+
## Every page ends with a way to answer
|
|
33
|
+
|
|
34
|
+
Any <form> is wired automatically: answers arrive as your next message. Write
|
|
35
|
+
plain semantic HTML — it is already styled, and there is nothing to remember.
|
|
36
|
+
<fieldset><legend> names a group, a wrapping <label> names one control, <small>
|
|
37
|
+
is a hint, and several <button name value> give one-click answers.
|
|
38
|
+
|
|
39
|
+
Asking well is most of the work. Answering should cost a click, not a paragraph:
|
|
40
|
+
buttons and radios for decisions, checkboxes for multi-select, free text only
|
|
41
|
+
where the answer is genuinely open. A range needs a scale that means something
|
|
42
|
+
and is easier to drag than to type — never a vague 1-to-5. Nothing is ever
|
|
43
|
+
required and blank is a real answer, so ask for everything that would help and
|
|
44
|
+
let them skip the rest. Always leave one open text field for what you failed to
|
|
45
|
+
anticipate: your form is their only way to redirect you, and a form that permits
|
|
46
|
+
only the answers you expect quietly takes the decision away from them.
|
|
47
|
+
|
|
48
|
+
## What belongs on the page
|
|
49
|
+
|
|
50
|
+
Only what they cannot skip: what you did, at the level they could explain it to
|
|
51
|
+
someone else; decisions that are genuinely theirs, with the options and your
|
|
52
|
+
recommendation; what only they can supply; anything they should sanity-check
|
|
53
|
+
because a wrong assumption of yours would be costly.
|
|
54
|
+
|
|
55
|
+
Every word necessary, nothing said twice. Each update leads with what changed and
|
|
56
|
+
has to stand alone, because they answer from that version without scrolling back.
|
|
57
|
+
Report failures, skipped steps and your own mistakes plainly. Conclusion first,
|
|
58
|
+
detail only if it changes what they do.
|
|
59
|
+
|
|
60
|
+
You own the work end to end: make the routine calls yourself, keep every file you
|
|
61
|
+
touch correct as you go, and escalate to the page rather than to chat.
|
|
62
|
+
|
|
63
|
+
## The home page
|
|
64
|
+
|
|
65
|
+
One thread's page is the home page, and every other page shows a Sessions link
|
|
66
|
+
back to it automatically — you never write that link yourself. Home is an
|
|
67
|
+
ordinary page: it should list the user's sessions with the threads.snapshot
|
|
68
|
+
capability and let them open, continue, or start one.
|
|
69
|
+
|
|
70
|
+
If the user asks for a home page, or asks where their sessions are, run
|
|
71
|
+
\`bb thread-page home\` in the thread that should own it and then build that page
|
|
72
|
+
against \`bb thread-page guide\`. Check whether one already exists before making
|
|
73
|
+
a second.
|
|
74
|
+
|
|
75
|
+
A page needing more than prose and a form — a chart, a branch, cards to swipe, a
|
|
76
|
+
file, live session control — runs \`bb thread-page guide\` first.`;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The default new-page document.
|
|
80
|
+
*
|
|
81
|
+
* The design system travels in the page rather than being injected at render
|
|
82
|
+
* time, so the agent can read and change every rule, and a later plugin update
|
|
83
|
+
* never restyles a page the user has already read. Plain semantic HTML inside
|
|
84
|
+
* <main> is fully styled by it; there are three class names in total.
|
|
85
|
+
*/
|
|
86
|
+
export const DEFAULT_PAGE_SEED = `<!doctype html>
|
|
87
|
+
<html lang="en" data-theme="volume" data-mode="system" data-atmos="on">
|
|
88
|
+
<head>
|
|
89
|
+
<meta charset="utf-8">
|
|
90
|
+
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
|
|
91
|
+
<title>{{TITLE}}</title>
|
|
92
|
+
<style>${THEME_CSS}</style>
|
|
93
|
+
</head>
|
|
94
|
+
<body>
|
|
95
|
+
<div class="atmosphere" aria-hidden="true"></div>
|
|
96
|
+
<div class="wrap">
|
|
97
|
+
|
|
98
|
+
<header class="brief-head">
|
|
99
|
+
<h1>{{TITLE}}</h1>
|
|
100
|
+
<p class="brief-meta"><span>{{DATE}}</span></p>
|
|
101
|
+
</header>
|
|
102
|
+
|
|
103
|
+
<!--
|
|
104
|
+
Write inside the main element below. Plain semantic HTML is already styled:
|
|
105
|
+
h2, p, ul, table, form, fieldset/legend, a wrapping label, small, details.
|
|
106
|
+
Three class names exist: .card boxes an aside, .needs-you flags a block
|
|
107
|
+
that is blocked on the reader, .label is a small uppercase tag.
|
|
108
|
+
|
|
109
|
+
data-theme is paper | terminal | atrium | volume | bloom.
|
|
110
|
+
data-mode is system | light | dark. data-atmos is on | off.
|
|
111
|
+
|
|
112
|
+
Any extra CSS goes in one more style block, everything inside
|
|
113
|
+
@scope (main), and colour and shape from var(--token) only — never a hex.
|
|
114
|
+
That is what keeps a bespoke page correct in all five worlds and in dark.
|
|
115
|
+
|
|
116
|
+
For charts, multi-screen flows, files, activity, or bridge methods:
|
|
117
|
+
bb thread-page guide
|
|
118
|
+
-->
|
|
119
|
+
<main>
|
|
120
|
+
<p>Replace this with what changed and what you need from the reader.</p>
|
|
121
|
+
|
|
122
|
+
<form data-title="{{TITLE}}">
|
|
123
|
+
<label>Reply
|
|
124
|
+
<textarea name="reply" rows="4"></textarea>
|
|
125
|
+
</label>
|
|
126
|
+
<button name="action" value="Reply">Reply</button>
|
|
127
|
+
</form>
|
|
128
|
+
</main>
|
|
129
|
+
|
|
130
|
+
</div>
|
|
131
|
+
</body>
|
|
132
|
+
</html>
|
|
133
|
+
`;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The home page document: the same shell and stylesheet as any other page, with
|
|
137
|
+
* a session hub already written into it. It is an ordinary page afterwards, so
|
|
138
|
+
* the owning agent can restyle or regroup it on request.
|
|
139
|
+
*/
|
|
140
|
+
export function renderHomeSeed(template: string, now: Date = new Date()): string {
|
|
141
|
+
const base = renderPageSeed(template, "Sessions", now);
|
|
142
|
+
const bodyStart = base.indexOf(" <header class=\"brief-head\">");
|
|
143
|
+
const bodyEnd = base.indexOf(" </div>\n</body>");
|
|
144
|
+
if (bodyStart < 0 || bodyEnd < 0 || bodyEnd <= bodyStart) {
|
|
145
|
+
// A custom seed we cannot splice: keep the user's own document rather than
|
|
146
|
+
// silently replacing it, and let the agent write the hub itself.
|
|
147
|
+
return base;
|
|
148
|
+
}
|
|
149
|
+
return (
|
|
150
|
+
base.slice(0, bodyStart) +
|
|
151
|
+
DEFAULT_HOME_BODY +
|
|
152
|
+
`\n <style>${DEFAULT_HOME_STYLE}</style>\n` +
|
|
153
|
+
` <script>${DEFAULT_HOME_SCRIPT}</script>\n\n` +
|
|
154
|
+
base.slice(bodyEnd)
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function renderPageSeed(
|
|
159
|
+
template: string,
|
|
160
|
+
title: string,
|
|
161
|
+
now: Date = new Date(),
|
|
162
|
+
): string {
|
|
163
|
+
const date = now.toLocaleDateString("en-GB", {
|
|
164
|
+
day: "numeric",
|
|
165
|
+
month: "long",
|
|
166
|
+
year: "numeric",
|
|
167
|
+
});
|
|
168
|
+
return template
|
|
169
|
+
.replaceAll("{{TITLE}}", escapeHtml(title))
|
|
170
|
+
.replaceAll("{{DATE}}", escapeHtml(date));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export const AUTHORING_GUIDE = `# Thread Pages authoring guide
|
|
174
|
+
|
|
175
|
+
Use the smallest page shape that makes the task easier. Plain semantic HTML is
|
|
176
|
+
the default; a Thread Page may also be a complete HTML/CSS/JavaScript mini-app.
|
|
177
|
+
Saving the file publishes it.
|
|
178
|
+
|
|
179
|
+
## Built in
|
|
180
|
+
|
|
181
|
+
- Every non-manual <form> replies to this thread. Add
|
|
182
|
+
data-thread-page-manual when your application owns submission.
|
|
183
|
+
- Blank answers are valid. Fieldset legends and labels become answer names.
|
|
184
|
+
- Multiple forms have independent pending and dirty state.
|
|
185
|
+
- A clicked submit button leads the message as Action.
|
|
186
|
+
- window.threadPage.setDirty(true|false) protects custom application state
|
|
187
|
+
from an automatic page reload.
|
|
188
|
+
- window.threadPage.invoke(method, params) calls an enabled, validated BB
|
|
189
|
+
capability. Run context.get to discover the current capability roster.
|
|
190
|
+
|
|
191
|
+
## What plain HTML already gives you
|
|
192
|
+
|
|
193
|
+
The seed carries the design system, so semantic HTML is already styled. You do
|
|
194
|
+
not need most of what follows; reach past prose only when the shape of the thing
|
|
195
|
+
genuinely is not prose.
|
|
196
|
+
|
|
197
|
+
h2, p, ul, table the page's type scale, rhythm, rules, tabular figures
|
|
198
|
+
form a panel, wiring to this thread, a status line
|
|
199
|
+
fieldset + legend a named group; the legend becomes the question
|
|
200
|
+
label wrapping one the label becomes that answer's name
|
|
201
|
+
small in a label a hint under the option
|
|
202
|
+
input type=range a slider with a live value readout
|
|
203
|
+
input type=file uploaded on submit, path sent to this thread
|
|
204
|
+
details/summary detail on demand; add name="x" for an accordion
|
|
205
|
+
div class=card a boxed aside
|
|
206
|
+
p class=needs-you a flagged block, for what is blocked on the reader
|
|
207
|
+
span class=label a small uppercase tag
|
|
208
|
+
|
|
209
|
+
Three class names. That is the whole vocabulary; everything else is selected by
|
|
210
|
+
what the element is.
|
|
211
|
+
|
|
212
|
+
## The look is three attributes
|
|
213
|
+
|
|
214
|
+
On <html>:
|
|
215
|
+
|
|
216
|
+
data-theme paper | terminal | atrium | volume | bloom
|
|
217
|
+
data-mode system | light | dark
|
|
218
|
+
data-atmos on | off
|
|
219
|
+
|
|
220
|
+
Each world sets a palette (both halves at once), a typeface, a shape language,
|
|
221
|
+
an atmosphere layer, and its own idea of what choosing and committing look like.
|
|
222
|
+
Changing the attribute reskins everything, including anything you built.
|
|
223
|
+
|
|
224
|
+
## The escape hatch
|
|
225
|
+
|
|
226
|
+
A page may carry one extra <style> block with two rules:
|
|
227
|
+
|
|
228
|
+
1. Everything inside @scope (main). The browser enforces it, so a page cannot
|
|
229
|
+
reach the shell.
|
|
230
|
+
2. Tokens only. No hex, no rgb(). Colour and shape come from var(--...).
|
|
231
|
+
|
|
232
|
+
Rule 2 is what keeps a bespoke page inside the system: dark mode still works and
|
|
233
|
+
switching world reskins your chart too. A page that writes #3b82f6 is wrong half
|
|
234
|
+
the time and nobody notices until night.
|
|
235
|
+
|
|
236
|
+
Tokens: --bg --surface --ink --ink-2 --ink-3 --rule --rule-soft --code-bg
|
|
237
|
+
--accent --accent-soft --accent-line --flag --ok --font-body --font-head
|
|
238
|
+
--radius --rule-w --shadow --measure --space --size --h1-size --label-case
|
|
239
|
+
--caps-track --dur --ease
|
|
240
|
+
|
|
241
|
+
## Prefer native HTML first
|
|
242
|
+
|
|
243
|
+
- details/summary (and details name="x") for disclosure and accordions.
|
|
244
|
+
- input type="range" for an eyeballed scale; Thread Pages adds a live output.
|
|
245
|
+
- CSS :has() for simple branches — real different content, not a hidden field.
|
|
246
|
+
- overflow-x:auto plus scroll-snap for swipeable cards: a real swipe on a
|
|
247
|
+
phone, a scrollbar on a desktop, arrow keys on a keyboard, in four lines.
|
|
248
|
+
- inline SVG for diagrams and charts; var(--accent) works inside it. Give a
|
|
249
|
+
zero a visible stub bar or the eye reads it as missing data.
|
|
250
|
+
- animation-timeline: view() for scroll-linked motion, wrapped in
|
|
251
|
+
@media (prefers-reduced-motion: no-preference) so still is the default.
|
|
252
|
+
- @starting-style with transition-behavior: allow-discrete for enter/exit.
|
|
253
|
+
- dialog, popover, container queries, color-mix(), and view transitions when
|
|
254
|
+
they clarify the task.
|
|
255
|
+
- Respect prefers-reduced-motion and keep every action keyboard reachable.
|
|
256
|
+
Never make something reachable only by pointer.
|
|
257
|
+
|
|
258
|
+
## Before you save
|
|
259
|
+
|
|
260
|
+
grep -o '#[0-9a-fA-F]\{3,8\}' page.html # inside your <style>: empty
|
|
261
|
+
grep -c '@scope (main)' page.html # 1 if you added a <style>
|
|
262
|
+
|
|
263
|
+
Then read it once at 320px wide, once in dark, once with reduced motion. Those
|
|
264
|
+
three are where a page that looks finished stops being one.
|
|
265
|
+
|
|
266
|
+
## Complete custom applications
|
|
267
|
+
|
|
268
|
+
Inline CSS and JavaScript, Web Components, SVG/canvas, internal routes, and
|
|
269
|
+
multi-step state are allowed inside the opaque sandbox. The page cannot read BB
|
|
270
|
+
cookies, the mutation token, parent DOM, localStorage, raw SDK/API, CLI, or
|
|
271
|
+
arbitrary files. Ordinary fetch and subresource networking are blocked unless a
|
|
272
|
+
confined resource is explicitly supplied.
|
|
273
|
+
|
|
274
|
+
Arbitrary JavaScript can still navigate its own sandboxed frame and encode
|
|
275
|
+
page/input data in the URL. The open mini-app model trusts authored code with
|
|
276
|
+
data already visible in its frame. Strong no-exfiltration requires a
|
|
277
|
+
declarative/no-authored-JavaScript page.
|
|
278
|
+
|
|
279
|
+
## The session hub, and the home page
|
|
280
|
+
|
|
281
|
+
One page is the home page; \`bb thread-page home\` designates the current
|
|
282
|
+
thread's. Every other page then shows a Sessions link back to it as chrome, so
|
|
283
|
+
no page writes that link. Home is an ordinary page — give it whatever design
|
|
284
|
+
suits, and render the list yourself:
|
|
285
|
+
|
|
286
|
+
const { threads } = await window.threadPage.invoke("threads.snapshot", { limit: 50 });
|
|
287
|
+
// each: id, title, projectId, parentThreadId, status, archived,
|
|
288
|
+
// page: { available, revision }, updatedAtMs
|
|
289
|
+
|
|
290
|
+
await window.threadPage.invoke("threads.openPage", { threadId }); // its page
|
|
291
|
+
await window.threadPage.invoke("threads.openBb", { threadId }); // in bb
|
|
292
|
+
await window.threadPage.invoke("threads.continue", { threadId, prompt });
|
|
293
|
+
await window.threadPage.invoke("threads.spawn", { projectId, prompt });
|
|
294
|
+
await window.threadPage.invoke("threads.archive", { threadId });
|
|
295
|
+
await window.threadPage.invoke("threads.stop", { threadId });
|
|
296
|
+
|
|
297
|
+
const { projects } = await window.threadPage.invoke("projects.list", {});
|
|
298
|
+
const { providers } = await window.threadPage.invoke("providers.list");
|
|
299
|
+
|
|
300
|
+
// Folder picker, then create a project from the opaque selection token.
|
|
301
|
+
const { selection } = await window.threadPage.invoke("projects.browse", {});
|
|
302
|
+
if (selection) await window.threadPage.invoke("projects.create",
|
|
303
|
+
{ selectionToken: selection.token, name: "My project" });
|
|
304
|
+
|
|
305
|
+
await window.threadPage.invoke("navigation.openExternal", { url, label });
|
|
306
|
+
|
|
307
|
+
// Small state that survives a reload, scoped to this page.
|
|
308
|
+
await window.threadPage.invoke("storage.set", { key: "wizard.step", value: 3 });
|
|
309
|
+
const state = await window.threadPage.invoke("storage.get", { key: "wizard.step" });
|
|
310
|
+
|
|
311
|
+
Anything that changes another thread, archives, stops, creates a project, or
|
|
312
|
+
leaves bb shows a confirmation in trusted chrome first. You do not build that
|
|
313
|
+
and cannot word it; a declined action rejects with code "cancelled". Handle it.
|
|
314
|
+
|
|
315
|
+
## Current bridge
|
|
316
|
+
|
|
317
|
+
const context = await window.threadPage.invoke("context.get");
|
|
318
|
+
const stop = window.threadPage.watch(
|
|
319
|
+
"thread.activity",
|
|
320
|
+
{ limit: 8 },
|
|
321
|
+
(value) => renderActivity(value),
|
|
322
|
+
{ intervalMs: 8000 }
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
await window.threadPage.invoke("thread.reply", {
|
|
326
|
+
title: "Diagram result",
|
|
327
|
+
mode: "queue", // or "steer"
|
|
328
|
+
result: { selectedNodes: ["a", "b"] },
|
|
329
|
+
idempotencyKey: "optional-stable-key"
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
Call stop() when a watched component unmounts. A page that never calls watch
|
|
333
|
+
does no bridge polling.
|
|
334
|
+
|
|
335
|
+
## Files the user sends you
|
|
336
|
+
|
|
337
|
+
An automatic form may contain input type="file" (including multiple). On submit
|
|
338
|
+
the bytes are uploaded first, stored under this thread's confined upload
|
|
339
|
+
directory, and reported to you in the form message as:
|
|
340
|
+
|
|
341
|
+
$BB_THREAD_STORAGE/thread-page-uploads/<generated-name>
|
|
342
|
+
|
|
343
|
+
Read them there with your normal tools. Each file must be under 24 MiB. Names
|
|
344
|
+
are generated by the plugin, so a hostile page cannot choose a path. Uploads
|
|
345
|
+
fail visibly on the page; they are never silently dropped.
|
|
346
|
+
|
|
347
|
+
## Files you show the user
|
|
348
|
+
|
|
349
|
+
Put sibling resources in:
|
|
350
|
+
|
|
351
|
+
$BB_THREAD_STORAGE/thread-page-assets/
|
|
352
|
+
|
|
353
|
+
Reference them relatively (<img src="chart.png">, <link href="page.css">) or
|
|
354
|
+
resolve one explicitly:
|
|
355
|
+
|
|
356
|
+
const url = window.threadPage.assetUrl("chart.png");
|
|
357
|
+
|
|
358
|
+
The directory is exposed to the page as one temporary, path-shaped preview and
|
|
359
|
+
is the only network origin the page's CSP allows. Names may use letters,
|
|
360
|
+
digits, dot, dash, and underscore only, with no subdirectories. When the
|
|
361
|
+
directory does not exist there is no asset base and assetUrl throws.
|
|
362
|
+
|
|
363
|
+
## Design ownership
|
|
364
|
+
|
|
365
|
+
The plugin does not impose a theme or component library. You may define any
|
|
366
|
+
task-specific visual system. Prefer CSS custom properties with light/dark
|
|
367
|
+
values so the page stays coherent, and test at a narrow mobile width.
|
|
368
|
+
`;
|