@frockbot/plugin-applets 0.0.0 → 0.3.13
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/frockbot.json +144 -0
- package/package.json +28 -6
- package/skills/applets.md +187 -0
- package/src/manifest.ts +3 -0
- package/src/package.test.ts +345 -0
- package/src/package.ts +394 -0
- package/src/pages/canvas.html +128 -0
- package/src/pages/list.html +180 -0
- package/src/root.test.ts +93 -0
- package/src/root.ts +111 -0
- package/src/template.generated.ts +37 -0
- package/README.md +0 -3
package/frockbot.json
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 5,
|
|
3
|
+
"id": "applets",
|
|
4
|
+
"displayName": "Applets",
|
|
5
|
+
"version": "0.0.1",
|
|
6
|
+
"compatibility": { "frockbot": ">=0.0.1" },
|
|
7
|
+
"dependencies": {},
|
|
8
|
+
"defaultEnablement": "enabled",
|
|
9
|
+
"contributions": {
|
|
10
|
+
"runtime": { "entry": "./package", "host": "bot-isolate" },
|
|
11
|
+
"client": {
|
|
12
|
+
"kind": "iframe",
|
|
13
|
+
"pages": [
|
|
14
|
+
{
|
|
15
|
+
"id": "list",
|
|
16
|
+
"artifact": {
|
|
17
|
+
"contentHash": "db52548255ebe482df18a1be2a22034fbaf40b97464d95729b053b1ca94aaf98",
|
|
18
|
+
"size": 7253,
|
|
19
|
+
"mediaType": "text/html",
|
|
20
|
+
"bundlerVersion": "frockbot-inline-html@1"
|
|
21
|
+
},
|
|
22
|
+
"mounts": [{ "slot": "frockbot.surface:list" }]
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"id": "canvas",
|
|
26
|
+
"artifact": {
|
|
27
|
+
"contentHash": "3d9c78811f638e7ee65a695cf325fdfbe757837bff18b6625e7f709eef896f6b",
|
|
28
|
+
"size": 6017,
|
|
29
|
+
"mediaType": "text/html",
|
|
30
|
+
"bundlerVersion": "frockbot-inline-html@1"
|
|
31
|
+
},
|
|
32
|
+
"mounts": [{ "slot": "frockbot.right-panel" }]
|
|
33
|
+
}
|
|
34
|
+
],
|
|
35
|
+
"entries": [
|
|
36
|
+
{
|
|
37
|
+
"id": "open",
|
|
38
|
+
"slot": "frockbot.sidebar-actions",
|
|
39
|
+
"order": 5,
|
|
40
|
+
"label": "Applets",
|
|
41
|
+
"icon": "applets",
|
|
42
|
+
"opens": { "kind": "surface", "page": "list" }
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"permissions": ["applets:manage", "workspace:write"],
|
|
48
|
+
"roots": [{ "id": "source", "scope": "user" }],
|
|
49
|
+
"tools": [
|
|
50
|
+
{
|
|
51
|
+
"name": "applet_list",
|
|
52
|
+
"description": "List this User's Applets: the small real-time apps that appear beside the conversation. Every Bot of this User sees every Applet. Call this before creating one, so you extend an Applet that already exists instead of building a second one.",
|
|
53
|
+
"inputSchema": {
|
|
54
|
+
"type": "object",
|
|
55
|
+
"properties": {},
|
|
56
|
+
"additionalProperties": false
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"name": "applet_create",
|
|
61
|
+
"description": "Create a new Applet and scaffold its source. This makes the directory entry, writes a working todo-list starting point into the Applet's source directory on this User's Computer, and focuses it so the User watches you build it. It does not publish anything: edit the files, run `applet check` and `applet build` on the Computer, then call applet_publish. Load the `applets` Skill before you start editing.",
|
|
62
|
+
"inputSchema": {
|
|
63
|
+
"type": "object",
|
|
64
|
+
"properties": {
|
|
65
|
+
"displayName": {
|
|
66
|
+
"type": "string",
|
|
67
|
+
"description": "What the User will call this Applet, in their words. 1-128 characters.",
|
|
68
|
+
"minLength": 1,
|
|
69
|
+
"maxLength": 128
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
"required": ["displayName"],
|
|
73
|
+
"additionalProperties": false
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"name": "applet_publish",
|
|
78
|
+
"description": "Publish what `applet build` last wrote for this Applet. Reads dist/server.js, dist/ui.html and dist/manifest.json from the Applet's source directory, records an immutable generation, mounts it, and offers its tools to every Bot of this User from the next Turn. Run `applet check` and `applet build` on the Computer first; a publish of a stale or failing build is refused and tells you why.",
|
|
79
|
+
"inputSchema": {
|
|
80
|
+
"type": "object",
|
|
81
|
+
"properties": {
|
|
82
|
+
"appletId": { "type": "string", "description": "The Applet's id." }
|
|
83
|
+
},
|
|
84
|
+
"required": ["appletId"],
|
|
85
|
+
"additionalProperties": false
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"name": "applet_revert",
|
|
90
|
+
"description": "Move an Applet back to an earlier generation. The revert is itself recorded as a generation, and the Applet's stored data is untouched — reverting the code never clears what the User put in it. Use applet_generations to find the id.",
|
|
91
|
+
"inputSchema": {
|
|
92
|
+
"type": "object",
|
|
93
|
+
"properties": {
|
|
94
|
+
"appletId": { "type": "string", "description": "The Applet's id." },
|
|
95
|
+
"generationId": {
|
|
96
|
+
"type": "string",
|
|
97
|
+
"description": "The generation to make current again."
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
"required": ["appletId", "generationId"],
|
|
101
|
+
"additionalProperties": false
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"name": "applet_delete",
|
|
106
|
+
"description": "Delete an Applet permanently: its stored data, its versions, and its entry. This cannot be undone and it is the User's decision, not yours — ask before calling it.",
|
|
107
|
+
"inputSchema": {
|
|
108
|
+
"type": "object",
|
|
109
|
+
"properties": {
|
|
110
|
+
"appletId": { "type": "string", "description": "The Applet's id." }
|
|
111
|
+
},
|
|
112
|
+
"required": ["appletId"],
|
|
113
|
+
"additionalProperties": false
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
"name": "applet_focus",
|
|
118
|
+
"description": "Show one Applet in the panel beside this conversation, or clear it. Pass null to close the panel. Creating and publishing already focus the Applet, so use this when the User asks to look at a different one.",
|
|
119
|
+
"inputSchema": {
|
|
120
|
+
"type": "object",
|
|
121
|
+
"properties": {
|
|
122
|
+
"appletId": {
|
|
123
|
+
"type": ["string", "null"],
|
|
124
|
+
"description": "The Applet to show, or null to close the panel."
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
"required": ["appletId"],
|
|
128
|
+
"additionalProperties": false
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
"name": "applet_generations",
|
|
133
|
+
"description": "List an Applet's version history, newest first: which generation is current, which failed, and what tools each one offered. Read this before applet_revert.",
|
|
134
|
+
"inputSchema": {
|
|
135
|
+
"type": "object",
|
|
136
|
+
"properties": {
|
|
137
|
+
"appletId": { "type": "string", "description": "The Applet's id." }
|
|
138
|
+
},
|
|
139
|
+
"required": ["appletId"],
|
|
140
|
+
"additionalProperties": false
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
]
|
|
144
|
+
}
|
package/package.json
CHANGED
|
@@ -1,14 +1,36 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frockbot/plugin-applets",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.3.13",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
"./manifest": "./src/manifest.ts",
|
|
8
|
+
"./root": "./src/root.ts",
|
|
9
|
+
"./package.json": "./package.json"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"test": "bun test src",
|
|
13
|
+
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@frockbot/kernel-contracts": "0.3.13",
|
|
17
|
+
"@frockbot/template-core": "0.3.13"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@types/bun": "1.4.0",
|
|
21
|
+
"typescript": "^7.0.2"
|
|
22
|
+
},
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
},
|
|
6
26
|
"repository": {
|
|
7
27
|
"type": "git",
|
|
8
28
|
"url": "git+https://github.com/timoconnellaus/frockbot.git",
|
|
9
29
|
"directory": "packages/plugin-applets"
|
|
10
30
|
},
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
|
|
31
|
+
"files": [
|
|
32
|
+
"frockbot.json",
|
|
33
|
+
"skills",
|
|
34
|
+
"src"
|
|
35
|
+
]
|
|
14
36
|
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Build an Applet
|
|
3
|
+
description: Use this whenever you are creating or changing an Applet — a small real-time app with its own data, its own page beside the conversation, and tools you can call. It is the reference for the Applets SDK, the file layout, the CLI, and every rule the linter enforces.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Build an Applet
|
|
7
|
+
|
|
8
|
+
An Applet is a real application. It has its own SQLite storage that survives
|
|
9
|
+
every code change, a React page the User opens beside this conversation, and
|
|
10
|
+
tools every Bot of this User can call. You write it in TypeScript on the
|
|
11
|
+
Computer, check it, build it, and publish it; the published code runs in the
|
|
12
|
+
kernel's loader, never on the Computer.
|
|
13
|
+
|
|
14
|
+
Two files are yours: `server.ts` (the tables and the tools) and `ui.tsx` (the
|
|
15
|
+
page). Nothing else.
|
|
16
|
+
|
|
17
|
+
## The loop
|
|
18
|
+
|
|
19
|
+
1. **`applet_create`** with a display name. It makes the Applet, scaffolds a
|
|
20
|
+
working todo list into
|
|
21
|
+
`/home/box/agent-data/user-packages/applets/source/<appletId>/`, and puts it
|
|
22
|
+
in the panel beside the conversation. Do not create a second Applet for a
|
|
23
|
+
change to an existing one — `applet_list` first.
|
|
24
|
+
2. **Edit** `server.ts` and `ui.tsx` in that directory with the ordinary file
|
|
25
|
+
tools. The scaffold already builds; change it rather than starting empty.
|
|
26
|
+
3. **`applet check`** in that directory. It type-checks and lints, and prints
|
|
27
|
+
every problem as `path:line:col message`. Fix all of them. Do not publish
|
|
28
|
+
over a failing check — the publish will be refused and you will have spent a
|
|
29
|
+
Turn learning what `applet check` would have told you.
|
|
30
|
+
4. **`applet build`** in that directory. It writes `dist/server.js`,
|
|
31
|
+
`dist/ui.html`, and `dist/manifest.json`. The tool list in the manifest is
|
|
32
|
+
derived by _running_ your server, so a tool that does not boot is a build
|
|
33
|
+
failure, not a surprise later.
|
|
34
|
+
5. **`applet dev`** if you want to look at it. It prints a local URL and opens
|
|
35
|
+
nothing; open that URL in the Computer's browser and screenshot it. Models
|
|
36
|
+
are unavailable in `applet dev`.
|
|
37
|
+
6. **`applet_publish`** with the Applet's id. It reads what `applet build`
|
|
38
|
+
wrote, records an immutable generation, mounts it, and offers its tools to
|
|
39
|
+
every Bot of this User from your next Turn — not this one.
|
|
40
|
+
|
|
41
|
+
`applet_generations` lists the history; `applet_revert` moves back to an
|
|
42
|
+
earlier generation and is itself recorded. Reverting code never touches the
|
|
43
|
+
Applet's data. `applet_delete` destroys the data too, so ask the User first.
|
|
44
|
+
|
|
45
|
+
## `server.ts`
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import { Applet, t, table } from "@frockbot/applet-sdk/server";
|
|
49
|
+
|
|
50
|
+
const tables = {
|
|
51
|
+
todos: table({
|
|
52
|
+
id: t.id(),
|
|
53
|
+
title: t.text(),
|
|
54
|
+
done: t.boolean().default(false),
|
|
55
|
+
createdAt: t.timestamp(),
|
|
56
|
+
}),
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export default class TodoApplet extends Applet<typeof tables> {
|
|
60
|
+
tables = tables;
|
|
61
|
+
|
|
62
|
+
tools = {
|
|
63
|
+
add_todo: this.tool(
|
|
64
|
+
{ description: "Add a todo to the list", input: { title: t.text() } },
|
|
65
|
+
({ title }) => {
|
|
66
|
+
this.db.todos.insert({ title, createdAt: new Date().toISOString() });
|
|
67
|
+
return `Added "${title}".`;
|
|
68
|
+
},
|
|
69
|
+
),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
- `tables` must be an object literal of `table({ … })` calls, declared once.
|
|
75
|
+
The schema becomes the SQLite tables, the wire format, and the client's
|
|
76
|
+
collections. Column types: `t.id()`, `t.text()`, `t.boolean()`,
|
|
77
|
+
`t.timestamp()`, each with optional `.default(value)` and `.optional()`.
|
|
78
|
+
- `tools` must be an object literal of `this.tool({ description, input }, fn)`
|
|
79
|
+
calls. A tool name is `^[a-z][a-z0-9_]{0,63}$` and the description is what a
|
|
80
|
+
model reads before calling it, so write it for a model.
|
|
81
|
+
- `this.db.<table>` is the only way to read or write:
|
|
82
|
+
`insert(values)`, `update(key, patch)`, `delete(key)`, `select(filter?)`.
|
|
83
|
+
Each call is atomic.
|
|
84
|
+
- A tool returns a string. That string is what the calling Bot sees.
|
|
85
|
+
- **Schema changes.** Adding a column with `.default(…)` or `.optional()` is
|
|
86
|
+
applied on the next mount and the rows are kept. Anything else — a rename, a
|
|
87
|
+
value rewrite — needs `async migrate(from: number)` on the class. It runs
|
|
88
|
+
once, before the Applet serves anything, and throwing fails the activation
|
|
89
|
+
back to the last known-good generation with the old data still resident.
|
|
90
|
+
|
|
91
|
+
## `ui.tsx`
|
|
92
|
+
|
|
93
|
+
```tsx
|
|
94
|
+
import { useState } from "react";
|
|
95
|
+
import { createApplet, mount, newId } from "@frockbot/applet-sdk/client";
|
|
96
|
+
import { Button, Input, List, ListItem, Stack } from "@frockbot/applet-sdk/kit";
|
|
97
|
+
import type TodoApplet from "./server";
|
|
98
|
+
|
|
99
|
+
const applet = createApplet<TodoApplet>();
|
|
100
|
+
|
|
101
|
+
function App() {
|
|
102
|
+
const { data: todos } = applet.useLiveQuery((query) =>
|
|
103
|
+
query
|
|
104
|
+
.from({ todo: applet.tables.todos })
|
|
105
|
+
.orderBy(({ todo }) => todo.createdAt),
|
|
106
|
+
);
|
|
107
|
+
// Optimistic: the row appears at once and rolls back if the server rejects it.
|
|
108
|
+
const add = (title: string) =>
|
|
109
|
+
applet.tables.todos.insert({
|
|
110
|
+
id: newId(),
|
|
111
|
+
title,
|
|
112
|
+
done: false,
|
|
113
|
+
createdAt: new Date().toISOString(),
|
|
114
|
+
});
|
|
115
|
+
return (
|
|
116
|
+
<Stack root gap="large">
|
|
117
|
+
…
|
|
118
|
+
</Stack>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
mount(<App />);
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
- `createApplet<TServer>()` connects on its own when the host sends `init`.
|
|
126
|
+
There is no loading wiring to write and no URL to fetch.
|
|
127
|
+
- `applet.useApplet()` gives `{ status }` if you want to show the connection.
|
|
128
|
+
- Mutations are per-row and optimistic; a rejection rolls the row back. Do not
|
|
129
|
+
write your own retry.
|
|
130
|
+
- Call `mount(<App />)` exactly once, at the bottom of the file.
|
|
131
|
+
|
|
132
|
+
## The component kit
|
|
133
|
+
|
|
134
|
+
`import { … } from "@frockbot/applet-sdk/kit"`. Fourteen components; they are
|
|
135
|
+
the whole visual vocabulary. There is no CSS file to write and no colour to
|
|
136
|
+
choose.
|
|
137
|
+
|
|
138
|
+
| Component | Props that matter |
|
|
139
|
+
| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
140
|
+
| `Stack` | `direction` `"row" \| "column"`, `gap` `"none" \| "small" \| "medium" \| "large"`, `align`, `justify`, `wrap`, `root` (exactly one, at the top) |
|
|
141
|
+
| `Text` | `size` `"title" \| "heading" \| "body" \| "small"`, `tone` `"default" \| "muted"`, `as` |
|
|
142
|
+
| `Button` | `variant` `"default" \| "primary" \| "ghost"`, `onClick`, `disabled`. Never submits a form by accident. |
|
|
143
|
+
| `Input`, `Textarea` | `label`, `error`, `value`, `onValueChange(value)`, `placeholder`; native attributes pass through |
|
|
144
|
+
| `Select` | `Input` plus `options: Array<{ value; label }>` |
|
|
145
|
+
| `Checkbox` | `checked`, `onChange(checked)`, `label` or `ariaLabel` (one is required), `disabled` |
|
|
146
|
+
| `Card` | `title?`, children — a bordered panel |
|
|
147
|
+
| `Toolbar` | children lead, `end?` trails — the Applet's title and its status or primary action |
|
|
148
|
+
| `List`, `ListItem` | `List`: `bordered?` (default true). `ListItem`: `start?`, `end?`, `onClick?`, children |
|
|
149
|
+
| `Badge` | `tone` `"default" \| "accent"` |
|
|
150
|
+
| `EmptyState` | `title`, `description?`, `action?` — show it whenever a live query is empty |
|
|
151
|
+
| `Dialog` | `open`, `onClose` (Escape and backdrop), `title`, `actions` |
|
|
152
|
+
|
|
153
|
+
The kit reads the nine tokens the host injects: `surface`, `surface-raised`,
|
|
154
|
+
`surface-subtle`, `text`, `text-muted`, `border`, `accent-surface`,
|
|
155
|
+
`accent-text`, `radius-card`. Read them yourself only as
|
|
156
|
+
`var(--frockbot-<name>)`.
|
|
157
|
+
|
|
158
|
+
## The rules the linter enforces
|
|
159
|
+
|
|
160
|
+
Every one of these is an error from `applet check`, not a warning.
|
|
161
|
+
|
|
162
|
+
- **`applet/no-raw-colors`** — no `#hex`, `rgb()`, `rgba()`, `hsl()`, `hsla()`,
|
|
163
|
+
`color-mix()`, or a CSS colour name, in `.ts`, `.tsx`, or `.css`. Use the
|
|
164
|
+
tokens. If the kit cannot express what you want, say so to the User rather
|
|
165
|
+
than styling around it.
|
|
166
|
+
- **`applet/no-network`** — no `fetch`, `XMLHttpRequest`, `WebSocket`,
|
|
167
|
+
`EventSource`, or `navigator.sendBeacon`. An Applet has no outbound network.
|
|
168
|
+
Reach the world through a tool on the server, which the Bot calls.
|
|
169
|
+
- **`applet/allowed-imports`** — only relative imports, `react`, and
|
|
170
|
+
`@frockbot/applet-sdk/*`. There is no npm install.
|
|
171
|
+
- **`applet/tables-via-table`** — `tables` is an object literal of `table({…})`.
|
|
172
|
+
- **`applet/tools-via-this-tool`** — `tools` is an object literal of
|
|
173
|
+
`this.tool(…)`.
|
|
174
|
+
|
|
175
|
+
## When it goes wrong
|
|
176
|
+
|
|
177
|
+
| What you see | What it means |
|
|
178
|
+
| --------------------------------------------------- | -------------------------------------------------------------------------------------- |
|
|
179
|
+
| `"dist/server.js" is …: run \`applet build\` …` | you published without building, or built in the wrong directory |
|
|
180
|
+
| `dist/manifest.json does not match the built files` | you edited a file after building; run `applet build` again |
|
|
181
|
+
| `applet check: N error(s)` | fix every line it printed before doing anything else |
|
|
182
|
+
| a publish reports `failed` with diagnostics | the generation did not mount; the previous one is still live and its data is untouched |
|
|
183
|
+
| the tools do not appear | a published generation activates on your **next** Turn, not the one that published it |
|
|
184
|
+
|
|
185
|
+
Report a publish failure to the User with the diagnostics as they were
|
|
186
|
+
printed. Never claim an Applet is working because the build passed: publishing
|
|
187
|
+
is what makes it real, and only a `published` result means it did.
|
package/src/manifest.ts
ADDED