@standardagents/code-plugin-sdk 1.0.0-alpha.3 → 1.0.0-alpha.4-hover.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/README.md +24 -6
- package/REFERENCE.md +439 -0
- package/package.json +2 -2
- package/src/index.d.ts +4 -2
- package/src/manifest.mjs +2 -1
- package/src/runtime.mjs +39 -1
- package/src/testing.mjs +8 -5
- package/src/view.mjs +18 -7
package/README.md
CHANGED
|
@@ -11,8 +11,8 @@ Its `package.json` includes a static `standardPlugin` manifest.
|
|
|
11
11
|
{
|
|
12
12
|
"name": "example-status",
|
|
13
13
|
"type": "module",
|
|
14
|
-
"peerDependencies": { "@standardagents/code-plugin-sdk": "^1.0.0-alpha.
|
|
15
|
-
"devDependencies": { "@standardagents/code-plugin-sdk": "^1.0.0-alpha.
|
|
14
|
+
"peerDependencies": { "@standardagents/code-plugin-sdk": "^1.0.0-alpha.4-hover.2" },
|
|
15
|
+
"devDependencies": { "@standardagents/code-plugin-sdk": "^1.0.0-alpha.4-hover.2" },
|
|
16
16
|
"standardPlugin": {
|
|
17
17
|
"apiVersion": 1,
|
|
18
18
|
"id": "example-status",
|
|
@@ -44,6 +44,20 @@ package, such as `@standardagents/code-plugin-sdk/testing`, are refused when
|
|
|
44
44
|
the plugin runs inside Standard Code. The `testing` export serves the
|
|
45
45
|
plugin's own test suite.
|
|
46
46
|
|
|
47
|
+
The complete rendering contract is in [REFERENCE.md](./REFERENCE.md). It
|
|
48
|
+
covers rows, styled text, badges, cards, slots, panels, overlays, host-rendered
|
|
49
|
+
views, ANSI canvases, styled canvas hover text, input focus,
|
|
50
|
+
popover/column/pane presentations, remote read-only behavior, and the frontend
|
|
51
|
+
surfaces that are still planned.
|
|
52
|
+
|
|
53
|
+
Plugin source imports the default package entry. Plugin tests may import
|
|
54
|
+
`@standardagents/code-plugin-sdk/testing` for `createHarness`. Runner internals,
|
|
55
|
+
raw host request envelopes, and internal module paths are outside the public
|
|
56
|
+
package exports. First-party plugins use these same public entries and receive
|
|
57
|
+
the same manifest validation, capability checks, request bounds, and remote
|
|
58
|
+
ownership rules as third-party plugins. A plugin ID does not grant a host
|
|
59
|
+
operation or fetch exception.
|
|
60
|
+
|
|
47
61
|
The runtime disposes publishers, subscriptions, schedules and pending requests.
|
|
48
62
|
Plugins register additional cleanup through `ctx.onDispose` or an activation return value.
|
|
49
63
|
Handlers receive an abort signal.
|
|
@@ -148,7 +162,7 @@ The harness starts no subprocesses.
|
|
|
148
162
|
|
|
149
163
|
## Supported interface
|
|
150
164
|
|
|
151
|
-
The public interface is the package
|
|
165
|
+
The public interface is the package root export, its `testing` export,
|
|
152
166
|
the declarations in `src/index.d.ts` and `src/testing.d.ts`, and the
|
|
153
167
|
`standard-plugin` command's `check` and `pack` behavior.
|
|
154
168
|
|
|
@@ -161,14 +175,18 @@ without notice. Plugin code and plugin tests should treat recorded frames as
|
|
|
161
175
|
opaque values and drive the plugin through `PluginContext` and the harness
|
|
162
176
|
methods.
|
|
163
177
|
|
|
164
|
-
|
|
178
|
+
`ctx.state` is per-plugin key and value storage. Standard Code stores these
|
|
179
|
+
values in the user's account, so every machine the user has reads and writes
|
|
180
|
+
the same keys. A plugin holds at most 256 keys, and one value is at most
|
|
181
|
+
64 KiB of JSON.
|
|
165
182
|
The public context declarations are in `src/index.d.ts`.
|
|
166
183
|
|
|
167
184
|
## Releases
|
|
168
185
|
|
|
169
186
|
The Standard Code build workflow publishes this package when the version in
|
|
170
|
-
`package.json` changes.
|
|
171
|
-
|
|
187
|
+
`package.json` changes. Main prereleases publish under `next` and main releases
|
|
188
|
+
under `latest`. Feature branches publish under their product branch tag. Authors
|
|
189
|
+
can install a published prerelease by its exact version.
|
|
172
190
|
|
|
173
191
|
## License
|
|
174
192
|
|
package/REFERENCE.md
ADDED
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
# Plugin SDK reference
|
|
2
|
+
|
|
3
|
+
The public declarations live in
|
|
4
|
+
`packages/plugin-sdk/src/index.d.ts`. This page groups the stable concepts for
|
|
5
|
+
plugin authors.
|
|
6
|
+
|
|
7
|
+
The bundled SDK and the npm package use the same public declarations. This
|
|
8
|
+
reference describes SDK `1.0.0-alpha.4-hover.2`. Plugins should declare the
|
|
9
|
+
version published for their product build in both `peerDependencies` and
|
|
10
|
+
`devDependencies`; the manifest `apiVersion` remains `1`.
|
|
11
|
+
|
|
12
|
+
## Definition and context
|
|
13
|
+
|
|
14
|
+
`definePlugin({ id, activate })` creates the default plugin export. The
|
|
15
|
+
activation function receives a `PluginContext` with the immutable manifest,
|
|
16
|
+
producer identity, abort signal, typed request function, surface publishers,
|
|
17
|
+
subscriptions, schedules, lifecycle operations, and host capabilities.
|
|
18
|
+
|
|
19
|
+
`section`, `card`, `slot`, `badge`, `panel`, and `overlay` each return a
|
|
20
|
+
publisher for a declared contribution of that kind; `canvas(id, spec)`
|
|
21
|
+
returns a canvas publisher for any declaration that draws content.
|
|
22
|
+
Publishers expose `replace(content)` and `clear()`. A canvas publisher also
|
|
23
|
+
exposes `write(ansi)` and `focus(capture)`. Registration objects return a
|
|
24
|
+
disposable subscription. Key registrations support one pending chord update.
|
|
25
|
+
|
|
26
|
+
## Manifest
|
|
27
|
+
|
|
28
|
+
`PluginManifest` contains `apiVersion`, `id`, `name`, `version`, `entry`,
|
|
29
|
+
`singleton`, `order`, `capabilities`, `contributions`, and `hookTimeoutMs`.
|
|
30
|
+
Contribution declarations contain a stable ID, surface kind, anchor, title,
|
|
31
|
+
merge mode, width, menu position, palette group, chord, URL pattern, and
|
|
32
|
+
action ID as appropriate.
|
|
33
|
+
|
|
34
|
+
A `card` declaration draws a boxed card in the sidebar. `validateManifest`
|
|
35
|
+
refuses a card whose anchor is not `plugins`. Two optional fields control
|
|
36
|
+
panels:
|
|
37
|
+
|
|
38
|
+
- `presentation` is `popover` (the default), `column`, or `pane`. On a panel
|
|
39
|
+
it sets how the panel opens. On a card or command it sets how the panel
|
|
40
|
+
named by `opens` opens. Other kinds cannot declare a presentation.
|
|
41
|
+
- `opens` is the ID of a panel that a card or command opens when the viewer
|
|
42
|
+
activates it. `validateManifest` refuses an `opens` value that does not name
|
|
43
|
+
a declared panel.
|
|
44
|
+
|
|
45
|
+
`PRESENTATIONS` lists the accepted presentation values.
|
|
46
|
+
|
|
47
|
+
An anchor determines the entity that owns a contribution. Machine anchors use
|
|
48
|
+
a machine entity, project anchors use a project entity, and pane anchors use a
|
|
49
|
+
pane entity. The `plugins` and `account` anchors are account-level surfaces;
|
|
50
|
+
`section` may use a section entity. A machine contribution normally omits its
|
|
51
|
+
generation so the host resolves the current generation. The host rejects a
|
|
52
|
+
contribution whose entity belongs to another machine or project.
|
|
53
|
+
|
|
54
|
+
## Sources and collections
|
|
55
|
+
|
|
56
|
+
A plugin installs from a Git repository or an npm package. A source is one
|
|
57
|
+
plugin or a collection. `PluginCollection` is the parsed
|
|
58
|
+
`standard-plugins.json` at the source root: `schema` is `1` and `plugins`
|
|
59
|
+
holds 1 to 256 `PluginCollectionEntry` values. Each entry has an `id` in the
|
|
60
|
+
plugin id pattern and a relative POSIX `path` with no leading slash, no
|
|
61
|
+
backslash, and no empty, `.`, or `..` segment. Ids and paths are unique.
|
|
62
|
+
`standard plugin install <source> --plugin <id>` selects one entry.
|
|
63
|
+
|
|
64
|
+
`validateCollection(value)` returns a frozen collection or throws a
|
|
65
|
+
`PluginError` with code `invalid_collection`.
|
|
66
|
+
`resolveCollection({ collection, packageJson })` returns the validated
|
|
67
|
+
collection when a file is present; otherwise a valid `standardPlugin`
|
|
68
|
+
manifest in `packageJson` yields one entry at path `""`. `COLLECTION_FILE`
|
|
69
|
+
is the file name.
|
|
70
|
+
|
|
71
|
+
## Dependencies and publishing
|
|
72
|
+
|
|
73
|
+
A plugin with `dependencies` or `optionalDependencies` ships a lockfile
|
|
74
|
+
beside its `package.json`. `lockfileRequirement({ packageJson, sourceKind })`
|
|
75
|
+
returns `{ required, dependencies, lockfiles }`: for `"npm"` the accepted
|
|
76
|
+
name is `npm-shrinkwrap.json`; for `"git"` it is `package-lock.json` or
|
|
77
|
+
`npm-shrinkwrap.json`. The SDK itself stays under `peerDependencies`.
|
|
78
|
+
|
|
79
|
+
`checkPackageForPublish({ packageJson, files, sourceKind, requireManifest,
|
|
80
|
+
expectedId })` is pure and returns `PublishProblem` values with a `code` and
|
|
81
|
+
a `message`. Codes cover a missing lockfile, the SDK under `dependencies`, a
|
|
82
|
+
missing peer declaration, a missing or invalid manifest, an entry absent from
|
|
83
|
+
`files`, an id that differs from `expectedId`, and a private package.
|
|
84
|
+
|
|
85
|
+
The package's `standard-plugin` command applies these rules to a directory:
|
|
86
|
+
`check [dir] [--source npm|git]` prints problems and exits with status 1
|
|
87
|
+
when it finds one, and `pack [dir]` runs `check`, runs `npm shrinkwrap` when
|
|
88
|
+
dependencies lack `npm-shrinkwrap.json`, and then prints the
|
|
89
|
+
`npm pack --dry-run` file list.
|
|
90
|
+
|
|
91
|
+
## Content
|
|
92
|
+
|
|
93
|
+
Native content uses rows, styled text, or badges. A row can carry an identity,
|
|
94
|
+
provider revision, styled spans, an action ID, a meter, spark samples, and a
|
|
95
|
+
divider marker. Canvas content declares canonical columns, rows, transparency,
|
|
96
|
+
shade, input capture, and optional hover metadata.
|
|
97
|
+
|
|
98
|
+
View content, `{ kind: 'view', root }`, is a tree of host-rendered nodes. The
|
|
99
|
+
host draws it with the viewer's theme, symbols, focus, and hit testing.
|
|
100
|
+
|
|
101
|
+
Each contribution kind accepts a fixed set of content kinds, and
|
|
102
|
+
`replace(content)` throws a `PluginError` for any other pairing:
|
|
103
|
+
|
|
104
|
+
| Contribution | Rows, text | Canvas | View | Badge |
|
|
105
|
+
|---|---|---|---|---|
|
|
106
|
+
| `section`, `card`, `panel` | yes | yes | yes | no |
|
|
107
|
+
| `slot`, `overlay` | yes | yes | no | no |
|
|
108
|
+
| `badge` | no | no | no | yes |
|
|
109
|
+
| `menu`, `command`, `key`, `link` | no | no | no | no |
|
|
110
|
+
|
|
111
|
+
### Custom interfaces with canvas
|
|
112
|
+
|
|
113
|
+
Canvas is the fully supported path for a plugin that draws its own
|
|
114
|
+
interface. A section, card, or panel can show a canvas in every
|
|
115
|
+
presentation: a sidebar card, a pop-over, a column, or a plugin pane.
|
|
116
|
+
`canvas.columns` is the intrinsic width that the host uses to size the card,
|
|
117
|
+
column, or plugin pane. The view helpers below are optional, and a plugin can
|
|
118
|
+
combine a view card with a canvas panel.
|
|
119
|
+
|
|
120
|
+
`write(ansi)` draws into a grid that the host keeps for the canvas. The grid
|
|
121
|
+
keeps its cells between writes, so a plugin that redraws a whole frame starts
|
|
122
|
+
it with `\x1b[2J\x1b[H`. The host honours these sequences:
|
|
123
|
+
|
|
124
|
+
- SGR (`CSI … m`): reset, bold, dim, italic, underline, inverse, hidden,
|
|
125
|
+
strikethrough, their resets, the 8 and 16 colour forms, and `38`/`48`
|
|
126
|
+
with `5;n` or `2;r;g;b` (colon forms too).
|
|
127
|
+
- Cursor positioning: `CUP`/`HVP` (`H`, `f`), `CUU`, `CUD`, `CUF`, `CUB`,
|
|
128
|
+
`CNL`, `CPL`, `CHA`, and `VPA`, clamped to the canvas.
|
|
129
|
+
- Erasing: `ED` (`J`), `EL` (`K`), and `ECH` (`X`), with the current
|
|
130
|
+
background colour.
|
|
131
|
+
- Carriage return, line feed (which also returns to the first column),
|
|
132
|
+
tab, and backspace.
|
|
133
|
+
|
|
134
|
+
The host drops every other escape and control sequence, including private
|
|
135
|
+
modes, OSC (titles, hyperlinks, clipboard), DCS, APC, PM, and SOS, and every
|
|
136
|
+
C0, C1, and bidirectional formatting character. Text past the right edge is
|
|
137
|
+
clipped, and a line feed on the last row stays on that row: a canvas never
|
|
138
|
+
wraps or scrolls. Wide characters take two cells. Cells in default colours
|
|
139
|
+
take the viewer's theme, and a blank default cell shows the surface under the
|
|
140
|
+
canvas.
|
|
141
|
+
|
|
142
|
+
`hover` is an optional one-line string or an array of styled `TextSpan` values.
|
|
143
|
+
The string must be non-empty after trimming, fit within 512 UTF-8 bytes, and
|
|
144
|
+
contain no C0, C1, DEL, or bidirectional formatting character. Styled hover
|
|
145
|
+
values contain at most 32 spans whose joined text follows the same bounds and
|
|
146
|
+
must be non-empty. Span styling supports `foreground`, `background`, `bold`,
|
|
147
|
+
`italic`, and `underline`; `actionId` is rejected because hover text is
|
|
148
|
+
passive. `Canvas.replace` publishes the canvas metadata, so a plugin can update
|
|
149
|
+
the hover text with another `replace` call while retaining the same canvas
|
|
150
|
+
grid.
|
|
151
|
+
|
|
152
|
+
On the current frontend, live canvases in `machine.before` and
|
|
153
|
+
`machine.after` slots are hover targets. The viewer waits 300 milliseconds
|
|
154
|
+
while the pointer remains over the canvas, then shows the text in a single
|
|
155
|
+
line owned by the host. Leaving the canvas dismisses it immediately. Hover
|
|
156
|
+
does not focus the canvas or change its geometry. The dwell timer and pointer
|
|
157
|
+
state belong to each viewer, so remote viewers can see hover text for a
|
|
158
|
+
machine slot independently.
|
|
159
|
+
Hover overlays for these machine slots use the same 22% sidebar machine colour
|
|
160
|
+
blend as the machine name bar over the viewer's background.
|
|
161
|
+
|
|
162
|
+
`focus(true)` asks for keyboard input and needs `captureInput: true` in the
|
|
163
|
+
canvas spec. While the canvas is shown in a pop-over, column, or plugin pane
|
|
164
|
+
and holds focus, the host sends every key except Escape and
|
|
165
|
+
Control, Alt, or Super chords to `onInput(id, handler)` subscriptions as
|
|
166
|
+
`{ kind: 'key', key, phase, contributionId, entity? }`. `key` is the typed
|
|
167
|
+
character or one of `up`, `down`, `left`, `right`, `enter`, `tab`,
|
|
168
|
+
`backspace`, `delete`, `escape`, `space`, `home`, `end`, `pageup`,
|
|
169
|
+
`pagedown`, or `f1`-`f12`; `phase` is `press` or `repeat`. When the canvas
|
|
170
|
+
stops being shown, for example after Escape, input subscriptions receive
|
|
171
|
+
`{ kind: 'focus', focused: false }` and `onDeactivate(id, handler)`
|
|
172
|
+
subscriptions run. The plugin calls `focus(true)` again to take keys back.
|
|
173
|
+
Sidebar cards and machine slots show only canvases without `captureInput`.
|
|
174
|
+
Local frontends receive canvas cells and send input. Remote viewers receive
|
|
175
|
+
the canvas grids and hover metadata, but remote actions, commands, panel
|
|
176
|
+
visibility changes, and canvas input are read-only until transport support is
|
|
177
|
+
added.
|
|
178
|
+
|
|
179
|
+
## View nodes
|
|
180
|
+
|
|
181
|
+
Every node has a `type`. Field names use camelCase, and tone, weight,
|
|
182
|
+
alignment, and width keywords are lowercase.
|
|
183
|
+
|
|
184
|
+
| Type | Fields |
|
|
185
|
+
|---|---|
|
|
186
|
+
| `stack` | `gap?`, `children` |
|
|
187
|
+
| `row` | `children`, `align?` |
|
|
188
|
+
| `divider` | `label?` |
|
|
189
|
+
| `text` | `text?` or `spans?`, `tone?`, `weight?`, `mono?` |
|
|
190
|
+
| `badge` | `label`, `tone?` |
|
|
191
|
+
| `dot` | `tone?` |
|
|
192
|
+
| `progress` | `value`, `max`, `tone?`, `label?` |
|
|
193
|
+
| `segments` | `items`: `{ value, max, tone?, label? }` |
|
|
194
|
+
| `card` | `title?`, `tone?`, `children` |
|
|
195
|
+
| `stat` | `label`, `value`, `tone?`, `hint?` |
|
|
196
|
+
| `kv` | `items`: `{ label, value, mono?, copy? }` |
|
|
197
|
+
| `tabs` | `id`, `items`, `filters?`, `action?` |
|
|
198
|
+
| `select` | `id`, `label`, `options`, `filters?`, `action?` |
|
|
199
|
+
| `table` | `id`, `columns`, `rows` |
|
|
200
|
+
| `log` | `lines` |
|
|
201
|
+
| `button` | `label`, `action` |
|
|
202
|
+
|
|
203
|
+
Tones are `ok`, `info`, `warn`, `error`, `muted`, `accent`, `pending`, and
|
|
204
|
+
`bright` (`TONES`). Weights are `normal`, `bold`, and `dim`. Alignments are
|
|
205
|
+
`start`, `center`, and `end`.
|
|
206
|
+
|
|
207
|
+
A table column has an `id`, an optional `label`, a `width` of `'fill'` or a
|
|
208
|
+
cell count, an optional `maxWidth` and `align`, and a `priority`. The host
|
|
209
|
+
drops the columns with the highest `priority` values first on a narrow
|
|
210
|
+
surface. A table row has an `id`, `cells` keyed by column ID, and optional
|
|
211
|
+
`tone`, `note` spans, `tags`, and `action`.
|
|
212
|
+
|
|
213
|
+
Tab items and select options carry an `id`, a `label`, an optional `count`,
|
|
214
|
+
and an optional `tag`. When `filters` names a table in the same view, the
|
|
215
|
+
host shows the rows whose `tags` contain the chosen item's `tag`. An item
|
|
216
|
+
without a `tag` shows every row. The host filters and counts for each viewer
|
|
217
|
+
without a plugin round trip.
|
|
218
|
+
|
|
219
|
+
A view action is `{ actionId, value?, opens? }`. The host sends `actionId`
|
|
220
|
+
and the string `value` to the plugin, so an `onAction(actionId, handler)`
|
|
221
|
+
handler receives `event.value`. One action ID with a per-row value replaces
|
|
222
|
+
one subscription per row. When `opens` names a panel, the host opens that
|
|
223
|
+
panel after the plugin accepts the action. A `tabs` or `select` action value
|
|
224
|
+
defaults to the chosen item ID.
|
|
225
|
+
|
|
226
|
+
A node type that the host does not know draws nothing, so a newer SDK can
|
|
227
|
+
add node types.
|
|
228
|
+
|
|
229
|
+
### Card views
|
|
230
|
+
|
|
231
|
+
A card shows a short summary. Its view can contain `stack`, `row`, `text`,
|
|
232
|
+
`badge`, `dot`, `progress`, `segments`, `stat`, and `divider` nodes, and it
|
|
233
|
+
is at most 6 lines tall. A `stack` is as tall as its children plus `gap`
|
|
234
|
+
lines between them. A `row` is as tall as its tallest child. Each other
|
|
235
|
+
allowed node is 1 line, and an unknown node is 0 lines. Put `card`, `kv`,
|
|
236
|
+
`tabs`, `select`, `table`, `log`, and `button` nodes in a panel. A card that
|
|
237
|
+
shows a canvas uses the general canvas bounds.
|
|
238
|
+
|
|
239
|
+
### Bounds
|
|
240
|
+
|
|
241
|
+
`validateView(root)` applies the bounds that the host applies, and
|
|
242
|
+
`replace(content)` calls it for view content. `VIEW_LIMITS` holds the
|
|
243
|
+
numbers.
|
|
244
|
+
|
|
245
|
+
- A tree nests at most 8 nodes deep and holds at most 4,096 nodes. Table
|
|
246
|
+
cells count toward both limits.
|
|
247
|
+
- A table has at most 512 rows and 12 columns.
|
|
248
|
+
- No string contains a C0 or C1 control character, DEL, or a bidirectional
|
|
249
|
+
formatting character.
|
|
250
|
+
- Every number is finite.
|
|
251
|
+
- The serialized tree fits in one 256 KiB frame.
|
|
252
|
+
- An action ID and an action `opens` value are plugin identifiers, and an
|
|
253
|
+
action `value` is at most 512 UTF-8 bytes.
|
|
254
|
+
- An action `opens` value names a panel that the plugin declares.
|
|
255
|
+
- A card view follows the card rules above.
|
|
256
|
+
|
|
257
|
+
`validateView(root, { kind, manifest })` applies the card rules when `kind`
|
|
258
|
+
is `'card'` and the `opens` rule when `manifest` is present.
|
|
259
|
+
|
|
260
|
+
### View helpers
|
|
261
|
+
|
|
262
|
+
`ui` builds nodes as plain JSON. The helpers are optional; hand-written JSON
|
|
263
|
+
with the same fields is equivalent. Options that are `undefined` are left
|
|
264
|
+
out.
|
|
265
|
+
|
|
266
|
+
```js
|
|
267
|
+
import { definePlugin, ui } from '@standardagents/code-plugin-sdk'
|
|
268
|
+
|
|
269
|
+
export default definePlugin({
|
|
270
|
+
id: 'builds-monitor',
|
|
271
|
+
activate(ctx) {
|
|
272
|
+
const card = ctx.card('summary')
|
|
273
|
+
card.replace(ui.view(ui.stack([
|
|
274
|
+
ui.stat('Queue', '3', { tone: 'pending' }),
|
|
275
|
+
ui.table('builds', [{ id: 'version', label: 'Version' }], [{
|
|
276
|
+
id: 'b-1',
|
|
277
|
+
cells: { version: ui.text('0.4.12', { weight: 'bold' }) },
|
|
278
|
+
action: ui.action('open-build', { value: 'b-1', opens: 'build-detail' }),
|
|
279
|
+
}]),
|
|
280
|
+
ui.button('Open dashboard', 'open-dashboard'),
|
|
281
|
+
])))
|
|
282
|
+
ctx.onAction('open-build', event => { /* event.value is 'b-1' */ })
|
|
283
|
+
},
|
|
284
|
+
})
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
| Helper | Result |
|
|
288
|
+
|---|---|
|
|
289
|
+
| `ui.view(root)` | `{ kind: 'view', root }` |
|
|
290
|
+
| `ui.action(actionId, { value, opens })` | an action |
|
|
291
|
+
| `ui.span(text, { tone, weight, mono })` | a span |
|
|
292
|
+
| `ui.stack(children, { gap })`, `ui.row(children, { align })` | a layout node |
|
|
293
|
+
| `ui.card(children, { title, tone })` | a card node |
|
|
294
|
+
| `ui.divider(label)` | a divider |
|
|
295
|
+
| `ui.text(textOrSpans, { tone, weight, mono })` | a text node; an array sets `spans` |
|
|
296
|
+
| `ui.badge(label, tone)`, `ui.dot(tone)` | a badge or dot |
|
|
297
|
+
| `ui.progress(value, max, { tone, label })`, `ui.segments(items)` | a meter |
|
|
298
|
+
| `ui.stat(label, value, { tone, hint })`, `ui.kv(items)` | a value display |
|
|
299
|
+
| `ui.tabs(id, items, { filters, action })` | a tab strip |
|
|
300
|
+
| `ui.select(id, label, options, { filters, action })` | a drop-down |
|
|
301
|
+
| `ui.table(id, columns, rows)` | a table |
|
|
302
|
+
| `ui.log(lines)` | a log |
|
|
303
|
+
| `ui.button(label, action)` | a button; `action` is an action or an action ID |
|
|
304
|
+
|
|
305
|
+
`packages/plugin-sdk/test/fixtures/view-builds-monitor.json` holds a complete
|
|
306
|
+
view that matches the protocol's round-trip test.
|
|
307
|
+
|
|
308
|
+
### Current frontend coverage
|
|
309
|
+
|
|
310
|
+
Sections, plugin cards, machine slots, canvas surfaces, host-rendered views,
|
|
311
|
+
global command palette entries, and the three
|
|
312
|
+
panel presentations are implemented. A popover is content-sized within the
|
|
313
|
+
modal area. A column attaches to the sidebar and resizes the workspace. A
|
|
314
|
+
plugin pane has a tab and focus, drag, close, and reveal behavior; its width
|
|
315
|
+
follows content and it does not create a tmux pane or resize a terminal.
|
|
316
|
+
|
|
317
|
+
Project slot anchors, standalone badges attached to hierarchy rows, pane
|
|
318
|
+
header/footer anchors, menu registrations, key registrations, link registrations,
|
|
319
|
+
and entity-scoped command palette entries have protocol and composition support
|
|
320
|
+
but do not have connected production frontend surfaces. Badge nodes inside a
|
|
321
|
+
view tree are rendered. The `overlay`
|
|
322
|
+
contribution kind has protocol validation, while general workspace overlay
|
|
323
|
+
placement and input remain unfinished. Use a supported panel presentation for
|
|
324
|
+
current interactive canvases.
|
|
325
|
+
|
|
326
|
+
## Actions and subscriptions
|
|
327
|
+
|
|
328
|
+
Commands, menus, keys, and links carry stable action IDs. Actions receive a
|
|
329
|
+
selection and handler context with an abort signal. Link handlers return a
|
|
330
|
+
boolean that controls matching-handler traversal.
|
|
331
|
+
|
|
332
|
+
Global command registrations appear in the command palette. Menu, key, and
|
|
333
|
+
link registrations are accepted and stored by the SDK and daemon, while their
|
|
334
|
+
frontend menus, global key dispatch, and link list are still planned. An
|
|
335
|
+
entity-scoped command also has no current palette surface. `url.open` is the
|
|
336
|
+
supported host operation for opening a URL.
|
|
337
|
+
|
|
338
|
+
Event, hook, input, select, resize, activate, deactivate, and visibility
|
|
339
|
+
subscriptions use bounded conditions. Schedules allow `always`,
|
|
340
|
+
`section-visible`, `slot-visible`, or `panel-open` conditions. One schedule
|
|
341
|
+
invocation runs at a time.
|
|
342
|
+
|
|
343
|
+
## Typed requests
|
|
344
|
+
|
|
345
|
+
The operation map covers pane and project lifecycle, notifications, URL and
|
|
346
|
+
fetch access, secrets, configuration, account state, context, popovers, canvas
|
|
347
|
+
write and focus, subscription changes, health, and webhook acknowledgements.
|
|
348
|
+
Each operation has a typed input and output. `RequestOptions` accepts an abort
|
|
349
|
+
signal and a timeout.
|
|
350
|
+
|
|
351
|
+
The public SDK applies the same capability checks and request bounds to every
|
|
352
|
+
plugin. Plugin IDs do not grant fetch, pane, project, or secret exceptions.
|
|
353
|
+
The current native host connects fetch, secrets, configuration, account state,
|
|
354
|
+
context, URL opening, subscription changes, health, canvas writes, and canvas
|
|
355
|
+
focus. Pane/project lifecycle requests, notifications, `popover.open`, and
|
|
356
|
+
webhook acknowledgements have public types but currently return an unsupported
|
|
357
|
+
operation error through the production context. Declaring a capability does
|
|
358
|
+
not supply an unfinished host adapter.
|
|
359
|
+
|
|
360
|
+
The shared fetch policy currently permits HTTPS GET requests to
|
|
361
|
+
`builds.standardcode.ai` on port 443 under `/admin/api/` or `/builds/`. Requests
|
|
362
|
+
have no body, user-info, or fragment. Allowed request headers are authorization,
|
|
363
|
+
range, accept, and cache-control. The response body limit is 16 MiB. These
|
|
364
|
+
bounds apply to every fetch-authorized plugin. Plugin code retains normal
|
|
365
|
+
Node.js access to files, networking, and subprocesses under the user's identity;
|
|
366
|
+
capabilities govern SDK/host operations and do not provide OS sandboxing.
|
|
367
|
+
|
|
368
|
+
## Build context
|
|
369
|
+
|
|
370
|
+
`context.get` returns a `PluginContextInfo`:
|
|
371
|
+
|
|
372
|
+
```json
|
|
373
|
+
{
|
|
374
|
+
"accountId": "account-id",
|
|
375
|
+
"machineId": "machine-id",
|
|
376
|
+
"build": {
|
|
377
|
+
"version": "0.14.1-branch.3bd7f23c.98a422",
|
|
378
|
+
"commit": "<40 hex digits>",
|
|
379
|
+
"ref": "refs/heads/feat/sidebar-plugins",
|
|
380
|
+
"channel": "branch"
|
|
381
|
+
},
|
|
382
|
+
"fleet": {
|
|
383
|
+
"mode": "follow",
|
|
384
|
+
"npmTag": "branch-feat-sidebar-plugins-3bd7f23c-b-6e049b24f5928544",
|
|
385
|
+
"ref": "refs/heads/feat/sidebar-plugins",
|
|
386
|
+
"version": "0.14.1-branch.3bd7f23c.98a422"
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
`build` describes the native release that this machine runs. It is `null` for
|
|
392
|
+
a development binary without release metadata.
|
|
393
|
+
|
|
394
|
+
- `channel` is `branch` for branch and issue builds, `canary` for `main`
|
|
395
|
+
builds, `production` for a stable version, and `team` for a legacy team
|
|
396
|
+
release.
|
|
397
|
+
- `ref` is `refs/heads/main` for canary and team builds and
|
|
398
|
+
`refs/tags/vX.Y.Z` for production builds. A branch build reports its sealed
|
|
399
|
+
branch name.
|
|
400
|
+
|
|
401
|
+
`fleet` describes the account build policy. It is `null` when the account has
|
|
402
|
+
no build policy.
|
|
403
|
+
|
|
404
|
+
- `mode` is `follow` for a followed npm tag, `production` for the `latest`
|
|
405
|
+
tag, and `pin` for an exact version.
|
|
406
|
+
- `npmTag` is the followed tag, or `null` for a pin.
|
|
407
|
+
- `version` is the pinned version, or the target of the current rollout, or
|
|
408
|
+
the active account version. It is `null` when none is known.
|
|
409
|
+
- `ref` is `refs/heads/main` for `canary`. A branch or issue tag has a `ref`
|
|
410
|
+
only when it names the branch of the running build. The daemon does not
|
|
411
|
+
read the network to resolve other branch names, so `ref` is `null` for them.
|
|
412
|
+
|
|
413
|
+
Older daemons return only `accountId` and `machineId`. Treat a missing
|
|
414
|
+
`build` or `fleet` as `null`.
|
|
415
|
+
|
|
416
|
+
A plugin with the `events` capability can subscribe with
|
|
417
|
+
`onEvent('build-context', handler)`. The daemon delivers a `BuildContextEvent`
|
|
418
|
+
when `build` or `fleet` changes. Its `data` holds the new `build` and `fleet`
|
|
419
|
+
values in the shape above. Other account changes do not deliver the event, and
|
|
420
|
+
the daemon does not deliver it when a plugin starts. Read `context.get` at
|
|
421
|
+
activation for the initial values. The daemon pushes each change, so a plugin
|
|
422
|
+
needs no schedule to track the followed channel.
|
|
423
|
+
|
|
424
|
+
## Hooks and delivery
|
|
425
|
+
|
|
426
|
+
Hook dispatch and durable webhook delivery are planned host integrations.
|
|
427
|
+
The public contract defines hook events with an entity, launch specification,
|
|
428
|
+
ancestry, operation ID, and deadline. A hook returns `proceed` with an optional replacement launch or
|
|
429
|
+
`cancel` with a bounded reason. Webhook events carry a delivery ID for
|
|
430
|
+
acknowledgement and replay-safe handling.
|
|
431
|
+
|
|
432
|
+
## Testing harness
|
|
433
|
+
|
|
434
|
+
`@standardagents/code-plugin-sdk/testing` provides `createHarness`. A harness drives
|
|
435
|
+
activation, event delivery, visibility, manual time, flushing, and disposal.
|
|
436
|
+
It records traces, surface replacement, subscriptions, and resources without
|
|
437
|
+
starting subprocesses. `harness.surface(id, entity?)` returns the current
|
|
438
|
+
content of a published contribution, including view and canvas content, or
|
|
439
|
+
`undefined` after the contribution clears.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@standardagents/code-plugin-sdk",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.4-hover.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Standard Code plugin authoring SDK",
|
|
6
6
|
"license": "MIT",
|
|
@@ -16,6 +16,6 @@
|
|
|
16
16
|
"./testing": { "types": "./src/testing.d.ts", "import": "./src/testing.mjs" }
|
|
17
17
|
},
|
|
18
18
|
"bin": { "standard-plugin": "./bin/standard-plugin.mjs" },
|
|
19
|
-
"files": ["src", "bin", "README.md", "LICENSE"],
|
|
19
|
+
"files": ["src", "bin", "README.md", "REFERENCE.md", "LICENSE"],
|
|
20
20
|
"scripts": { "test": "node --test test/*.test.mjs" }
|
|
21
21
|
}
|
package/src/index.d.ts
CHANGED
|
@@ -90,6 +90,8 @@ export interface CanvasSpec {
|
|
|
90
90
|
transparent?: boolean;
|
|
91
91
|
shade?: number;
|
|
92
92
|
captureInput?: boolean;
|
|
93
|
+
/** A bounded one-line string or styled spans shown while the pointer rests over the canvas. */
|
|
94
|
+
hover?: string | TextSpan[];
|
|
93
95
|
}
|
|
94
96
|
export type CanvasContent = { kind: 'canvas'; canvas: CanvasSpec };
|
|
95
97
|
/**
|
|
@@ -355,7 +357,7 @@ export interface PluginContext {
|
|
|
355
357
|
fetch(args: OperationMap['fetch']['input'], options?: RequestOptions): Promise<OperationMap['fetch']['output']>;
|
|
356
358
|
secrets: { get(name: string, options?: RequestOptions): Promise<string | null> };
|
|
357
359
|
config: { get(options?: RequestOptions): Promise<Record<string, Json>> };
|
|
358
|
-
/**
|
|
360
|
+
/** Per-plugin key and value storage, held in the user's account and shared across their machines: 256 keys per plugin, 64 KiB per value. */
|
|
359
361
|
state: { get(key: string, options?: RequestOptions): Promise<Json>; set(key: string, value: Json, options?: RequestOptions): Promise<null> };
|
|
360
362
|
context: { get(options?: RequestOptions): Promise<PluginContextInfo> };
|
|
361
363
|
popover: { open(args: Popover, options?: RequestOptions): Promise<OperationMap['popover.open']['output']> };
|
|
@@ -371,7 +373,7 @@ export function validateManifest(value: unknown): Readonly<PluginManifest>;
|
|
|
371
373
|
export class PluginError extends Error { code: string; constructor(code: string, message: string) }
|
|
372
374
|
export const PRESENTATIONS: readonly Presentation[];
|
|
373
375
|
export const TONES: readonly Tone[];
|
|
374
|
-
export const VIEW_LIMITS: Readonly<{ depth: 8; nodes: 4096; tableRows: 512; tableColumns: 12; cardLines: 6; actionValueBytes: 512 }>;
|
|
376
|
+
export const VIEW_LIMITS: Readonly<{ depth: 8; nodes: 4096; tableRows: 512; tableColumns: 12; items: 512; logLines: 2048; cardLines: 6; actionValueBytes: 512 }>;
|
|
375
377
|
/**
|
|
376
378
|
* Throws a PluginError when a view tree breaks a protocol bound or a daemon rule. Unknown node types pass.
|
|
377
379
|
* `kind: 'card'` adds the card rules; `manifest` requires each action `opens` to name one of its panels.
|
package/src/manifest.mjs
CHANGED
|
@@ -12,7 +12,8 @@ export const LIMITS = Object.freeze({ manifestBytes: 65536, frameBytes: 262144,
|
|
|
12
12
|
responseFrameBytes: 64 * 1024 * 1024,
|
|
13
13
|
pendingRequests: 128, subscriptions: 256, schedules: 128, contributions: 256,
|
|
14
14
|
queuedBytes: 4 * 1024 * 1024, hookTimeoutMs: 60000, requestTimeoutMs: 30000,
|
|
15
|
-
canvasColumns: 512, canvasRows: 256
|
|
15
|
+
canvasColumns: 512, canvasRows: 256, canvasHoverBytes: 512,
|
|
16
|
+
stateKeys: 256, stateValueBytes: 64 * 1024 })
|
|
16
17
|
|
|
17
18
|
export class PluginError extends Error {
|
|
18
19
|
constructor(code, message) { super(message); this.name = 'PluginError'; this.code = code }
|
package/src/runtime.mjs
CHANGED
|
@@ -10,6 +10,43 @@ function boundedText(value, label, limit = 512) {
|
|
|
10
10
|
!/[\u0000-\u001f\u007f]/.test(value), 'invalid_payload', `Invalid ${label}`)
|
|
11
11
|
return value
|
|
12
12
|
}
|
|
13
|
+
const HOVER_FORBIDDEN = /[\u0000-\u001f\u007f-\u009f\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069]/
|
|
14
|
+
function validateCanvasHover(value) {
|
|
15
|
+
if (typeof value === 'string') {
|
|
16
|
+
ensure(value.trim().length > 0 && !HOVER_FORBIDDEN.test(value),
|
|
17
|
+
'invalid_payload', 'Invalid canvas hover text')
|
|
18
|
+
ensure(Buffer.byteLength(value) <= LIMITS.canvasHoverBytes,
|
|
19
|
+
'payload_too_large', `Canvas hover text exceeds ${LIMITS.canvasHoverBytes} bytes`)
|
|
20
|
+
return
|
|
21
|
+
}
|
|
22
|
+
ensure(Array.isArray(value), 'invalid_payload', 'Invalid canvas hover spans')
|
|
23
|
+
ensure(value.length > 0, 'invalid_payload', 'Canvas hover spans must contain text')
|
|
24
|
+
ensure(value.length <= 32, 'payload_too_large', 'Canvas hover spans exceed 32 spans')
|
|
25
|
+
let text = ''
|
|
26
|
+
for (const span of value) {
|
|
27
|
+
ensure(object(span), 'invalid_payload', 'Invalid canvas hover span')
|
|
28
|
+
ensure(typeof span.text === 'string' && !HOVER_FORBIDDEN.test(span.text),
|
|
29
|
+
'invalid_payload', 'Invalid canvas hover span text')
|
|
30
|
+
for (const field of ['foreground', 'background']) {
|
|
31
|
+
if (span[field] !== undefined) {
|
|
32
|
+
ensure(typeof span[field] === 'string' && span[field].trim().length > 0 &&
|
|
33
|
+
!HOVER_FORBIDDEN.test(span[field]), 'invalid_payload', `Invalid canvas hover span ${field}`)
|
|
34
|
+
ensure(Buffer.byteLength(span[field]) <= 64,
|
|
35
|
+
'payload_too_large', `Canvas hover span ${field} exceeds 64 bytes`)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
for (const field of ['bold', 'italic', 'underline']) {
|
|
39
|
+
ensure(span[field] === undefined || typeof span[field] === 'boolean',
|
|
40
|
+
'invalid_payload', `Invalid canvas hover span ${field}`)
|
|
41
|
+
}
|
|
42
|
+
ensure(span.actionId === undefined, 'invalid_payload', 'Canvas hover spans cannot contain actionId')
|
|
43
|
+
text += span.text
|
|
44
|
+
}
|
|
45
|
+
ensure(text.trim().length > 0 && !HOVER_FORBIDDEN.test(text),
|
|
46
|
+
'invalid_payload', 'Invalid canvas hover text')
|
|
47
|
+
ensure(Buffer.byteLength(text) <= LIMITS.canvasHoverBytes,
|
|
48
|
+
'payload_too_large', `Canvas hover text exceeds ${LIMITS.canvasHoverBytes} bytes`)
|
|
49
|
+
}
|
|
13
50
|
function validateEntity(entity) {
|
|
14
51
|
ensure(object(entity) && entityKinds.includes(entity.kind), 'invalid_payload', 'Invalid registration entity')
|
|
15
52
|
boundedText(entity.id, 'entity id', 128)
|
|
@@ -57,10 +94,11 @@ function validateContent(content, declaration, manifest) {
|
|
|
57
94
|
if (content.kind === 'view') validateView(content.root, { kind: declaration.kind, manifest })
|
|
58
95
|
jsonBytes(content)
|
|
59
96
|
if (content.kind === 'canvas') {
|
|
60
|
-
const { columns, rows, shade = 0 } = content.canvas ?? {}
|
|
97
|
+
const { columns, rows, shade = 0, hover } = content.canvas ?? {}
|
|
61
98
|
ensure(Number.isSafeInteger(columns) && columns > 0 && columns <= LIMITS.canvasColumns &&
|
|
62
99
|
Number.isSafeInteger(rows) && rows > 0 && rows <= LIMITS.canvasRows &&
|
|
63
100
|
Number.isFinite(shade) && shade >= 0 && shade <= 1, 'invalid_payload', 'Invalid canvas dimensions or shade')
|
|
101
|
+
if (hover !== undefined) validateCanvasHover(hover)
|
|
64
102
|
}
|
|
65
103
|
}
|
|
66
104
|
|
package/src/testing.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createRuntime } from './runtime.mjs'
|
|
2
2
|
import { RpcPeer } from './protocol.mjs'
|
|
3
|
-
import { PluginError, ensure, LIMITS, validateManifest } from './manifest.mjs'
|
|
3
|
+
import { PluginError, ensure, jsonBytes, LIMITS, validateManifest } from './manifest.mjs'
|
|
4
4
|
|
|
5
5
|
/** Explicit time and fixture-owned responses; this harness starts no threads or subprocesses. */
|
|
6
6
|
export function createHarness({ manifest: input, machineId = 'test-machine', epoch = '1', handlers = {}, now = 0 } = {}) {
|
|
@@ -9,7 +9,7 @@ export function createHarness({ manifest: input, machineId = 'test-machine', epo
|
|
|
9
9
|
const trace = []
|
|
10
10
|
const surfaces = new Map()
|
|
11
11
|
const subscriptions = new Map()
|
|
12
|
-
const
|
|
12
|
+
const accountState = new Map()
|
|
13
13
|
const timers = new Map()
|
|
14
14
|
let timerId = 0
|
|
15
15
|
let disposed = false
|
|
@@ -36,10 +36,13 @@ export function createHarness({ manifest: input, machineId = 'test-machine', epo
|
|
|
36
36
|
switch (operation.op) {
|
|
37
37
|
case 'subscription.add': subscriptions.set(args.id, structuredClone(args)); return null
|
|
38
38
|
case 'subscription.remove': subscriptions.delete(args.id); return null
|
|
39
|
-
case 'state.get': return structuredClone(
|
|
39
|
+
case 'state.get': return structuredClone(accountState.get(args.key) ?? null)
|
|
40
40
|
case 'state.set':
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
// The product stores these values in the account: 256 keys per
|
|
42
|
+
// plugin, 64 KiB per value.
|
|
43
|
+
ensure(accountState.has(args.key) || accountState.size < LIMITS.stateKeys, 'queue_full', 'Harness state holds 256 keys for this plugin')
|
|
44
|
+
jsonBytes(args.value, LIMITS.stateValueBytes)
|
|
45
|
+
accountState.set(args.key, structuredClone(args.value)); return null
|
|
43
46
|
case 'config.get': return {}
|
|
44
47
|
case 'context.get': return { accountId: 'test-account', machineId, build: null, fleet: null }
|
|
45
48
|
case 'health.set': return null
|
package/src/view.mjs
CHANGED
|
@@ -2,8 +2,10 @@ import { LIMITS, ensure, identifier, jsonBytes, object } from './manifest.mjs'
|
|
|
2
2
|
|
|
3
3
|
// Mirrors crates/standard-protocol/src/plugin_view.rs. The host applies the
|
|
4
4
|
// same bounds; checking here reports a mistake at the replace() call.
|
|
5
|
+
// items and logLines bound the list-shaped nodes: the host lays every entry
|
|
6
|
+
// out again on each paint, so a long list costs a frame, not just memory.
|
|
5
7
|
export const VIEW_LIMITS = Object.freeze({ depth: 8, nodes: 4096, tableRows: 512, tableColumns: 12,
|
|
6
|
-
cardLines: 6, actionValueBytes: 512 })
|
|
8
|
+
items: 512, logLines: 2048, cardLines: 6, actionValueBytes: 512 })
|
|
7
9
|
export const TONES = Object.freeze(['ok', 'info', 'warn', 'error', 'muted', 'accent', 'pending', 'bright'])
|
|
8
10
|
const WEIGHTS = ['normal', 'bold', 'dim']
|
|
9
11
|
const ALIGNS = ['start', 'center', 'end']
|
|
@@ -38,6 +40,15 @@ function entries(value, field, optional, check) {
|
|
|
38
40
|
check(item)
|
|
39
41
|
}
|
|
40
42
|
}
|
|
43
|
+
function bounded(value, field, limit, optional = false) {
|
|
44
|
+
const values = list(value, field, optional)
|
|
45
|
+
ensure(values.length <= limit, 'invalid_payload', `Plugin view ${field} has more than ${limit} entries`)
|
|
46
|
+
return values
|
|
47
|
+
}
|
|
48
|
+
// A list-shaped node's entries, held to the bound the host applies.
|
|
49
|
+
function boundedEntries(value, field, check) {
|
|
50
|
+
entries(bounded(value, field, VIEW_LIMITS.items), field, false, check)
|
|
51
|
+
}
|
|
41
52
|
function spans(value, optional) {
|
|
42
53
|
entries(value, 'spans', optional, span => {
|
|
43
54
|
text(span.text, 'span text')
|
|
@@ -77,7 +88,7 @@ const NODES = {
|
|
|
77
88
|
badge(node) { text(node.label, 'label'); tone(node.tone) },
|
|
78
89
|
dot(node) { tone(node.tone) },
|
|
79
90
|
progress: meter,
|
|
80
|
-
segments(node) {
|
|
91
|
+
segments(node) { boundedEntries(node.items, 'items', meter) },
|
|
81
92
|
card(node, depth, count) { optionalText(node.title, 'title'); tone(node.tone); children(node, depth, count) },
|
|
82
93
|
stat(node) {
|
|
83
94
|
text(node.label, 'label')
|
|
@@ -86,7 +97,7 @@ const NODES = {
|
|
|
86
97
|
optionalText(node.hint, 'hint')
|
|
87
98
|
},
|
|
88
99
|
kv(node) {
|
|
89
|
-
|
|
100
|
+
boundedEntries(node.items, 'items', item => {
|
|
90
101
|
text(item.label, 'label')
|
|
91
102
|
text(item.value, 'value')
|
|
92
103
|
flag(item.mono, 'mono')
|
|
@@ -97,7 +108,7 @@ const NODES = {
|
|
|
97
108
|
text(node.id, 'id')
|
|
98
109
|
optionalText(node.filters, 'filters')
|
|
99
110
|
action(node.action)
|
|
100
|
-
|
|
111
|
+
boundedEntries(node.items, 'items', item => {
|
|
101
112
|
text(item.id, 'id')
|
|
102
113
|
text(item.label, 'label')
|
|
103
114
|
optionalText(item.sublabel, 'sublabel')
|
|
@@ -111,7 +122,7 @@ const NODES = {
|
|
|
111
122
|
text(node.label, 'label')
|
|
112
123
|
optionalText(node.filters, 'filters')
|
|
113
124
|
action(node.action)
|
|
114
|
-
|
|
125
|
+
boundedEntries(node.options, 'options', option => {
|
|
115
126
|
text(option.id, 'id')
|
|
116
127
|
text(option.label, 'label')
|
|
117
128
|
optionalText(option.group, 'group')
|
|
@@ -137,7 +148,7 @@ const NODES = {
|
|
|
137
148
|
text(row.id, 'row id')
|
|
138
149
|
tone(row.tone)
|
|
139
150
|
spans(row.note, true)
|
|
140
|
-
for (const tag of
|
|
151
|
+
for (const tag of bounded(row.tags, 'tags', VIEW_LIMITS.items, true)) text(tag, 'tag')
|
|
141
152
|
action(row.action)
|
|
142
153
|
ensure(row.cells === undefined || object(row.cells), 'invalid_payload', 'Plugin table cells must be an object')
|
|
143
154
|
for (const [column, cell] of Object.entries(row.cells ?? {})) {
|
|
@@ -146,7 +157,7 @@ const NODES = {
|
|
|
146
157
|
}
|
|
147
158
|
})
|
|
148
159
|
},
|
|
149
|
-
log(node) { for (const line of
|
|
160
|
+
log(node) { for (const line of bounded(node.lines, 'lines', VIEW_LIMITS.logLines)) text(line, 'log line') },
|
|
150
161
|
button(node) { text(node.label, 'label'); action(node.action, false) },
|
|
151
162
|
}
|
|
152
163
|
|