@standardagents/code-plugin-sdk 1.0.0-alpha.0 → 1.0.0-alpha.10-rows.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +182 -7
- package/REFERENCE.md +536 -0
- package/bin/standard-plugin.mjs +149 -0
- package/package.json +3 -2
- package/src/collection.mjs +0 -0
- package/src/index.d.ts +250 -16
- package/src/index.mjs +7 -2
- package/src/internal.d.ts +2 -1
- package/src/manifest.mjs +22 -3
- package/src/protocol.mjs +8 -4
- package/src/publish.mjs +62 -0
- package/src/row-layout.mjs +29 -0
- package/src/runtime.mjs +81 -6
- package/src/testing.d.ts +3 -1
- package/src/testing.mjs +17 -6
- package/src/view.mjs +257 -0
package/REFERENCE.md
ADDED
|
@@ -0,0 +1,536 @@
|
|
|
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.5-usage.0`. 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 sidebar card. Its optional `border` boolean
|
|
35
|
+
defaults to `true`. Set `border: false` for a borderless group: the host
|
|
36
|
+
retains the author title, content padding, focus indication, and activation
|
|
37
|
+
behavior, and removes the outline and bottom border row. This preference
|
|
38
|
+
is declared by each plugin on its own cards; other surface kinds reject it.
|
|
39
|
+
The nested view node named `card` is a separate content element.
|
|
40
|
+
|
|
41
|
+
`validateManifest`
|
|
42
|
+
refuses a card whose anchor is not `plugins`. Two optional fields control
|
|
43
|
+
panels:
|
|
44
|
+
|
|
45
|
+
- `presentation` is `popover` (the default), `column`, or `pane`. On a panel
|
|
46
|
+
it sets how the panel opens. On a card or command it sets how the panel
|
|
47
|
+
named by `opens` opens. Other kinds cannot declare a presentation.
|
|
48
|
+
- `opens` is the ID of a panel that a card or command opens when the viewer
|
|
49
|
+
activates it. `validateManifest` refuses an `opens` value that does not name
|
|
50
|
+
a declared panel.
|
|
51
|
+
|
|
52
|
+
`PRESENTATIONS` lists the accepted presentation values.
|
|
53
|
+
|
|
54
|
+
Account-level sidebar cards prefer a live producer on the viewing machine.
|
|
55
|
+
This keeps card actions and attached panels local to that viewer. If no live
|
|
56
|
+
local producer exists, the host uses its deterministic remote-source fallback;
|
|
57
|
+
remote source authority restrictions still apply. Plugins aggregate shared
|
|
58
|
+
account data themselves through the public state API.
|
|
59
|
+
|
|
60
|
+
An anchor determines the entity that owns a contribution. Machine anchors use
|
|
61
|
+
a machine entity, project anchors use a project entity, and pane anchors use a
|
|
62
|
+
pane entity. The `plugins` and `account` anchors are account-level surfaces;
|
|
63
|
+
`section` may use a section entity. A machine contribution normally omits its
|
|
64
|
+
generation so the host resolves the current generation. The host rejects a
|
|
65
|
+
contribution whose entity belongs to another machine or project.
|
|
66
|
+
|
|
67
|
+
## Sources and collections
|
|
68
|
+
|
|
69
|
+
A plugin installs from a Git repository or an npm package. A source is one
|
|
70
|
+
plugin or a collection. `PluginCollection` is the parsed
|
|
71
|
+
`standard-plugins.json` at the source root: `schema` is `1` and `plugins`
|
|
72
|
+
holds 1 to 256 `PluginCollectionEntry` values. Each entry has an `id` in the
|
|
73
|
+
plugin id pattern and a relative POSIX `path` with no leading slash, no
|
|
74
|
+
backslash, and no empty, `.`, or `..` segment. Ids and paths are unique.
|
|
75
|
+
`standard plugin install <source> --plugin <id>` selects one entry.
|
|
76
|
+
|
|
77
|
+
`validateCollection(value)` returns a frozen collection or throws a
|
|
78
|
+
`PluginError` with code `invalid_collection`.
|
|
79
|
+
`resolveCollection({ collection, packageJson })` returns the validated
|
|
80
|
+
collection when a file is present; otherwise a valid `standardPlugin`
|
|
81
|
+
manifest in `packageJson` yields one entry at path `""`. `COLLECTION_FILE`
|
|
82
|
+
is the file name.
|
|
83
|
+
|
|
84
|
+
## Dependencies and publishing
|
|
85
|
+
|
|
86
|
+
A plugin with `dependencies` or `optionalDependencies` ships a lockfile
|
|
87
|
+
beside its `package.json`. `lockfileRequirement({ packageJson, sourceKind })`
|
|
88
|
+
returns `{ required, dependencies, lockfiles }`: for `"npm"` the accepted
|
|
89
|
+
name is `npm-shrinkwrap.json`; for `"git"` it is `package-lock.json` or
|
|
90
|
+
`npm-shrinkwrap.json`. The SDK itself stays under `peerDependencies`.
|
|
91
|
+
|
|
92
|
+
`checkPackageForPublish({ packageJson, files, sourceKind, requireManifest,
|
|
93
|
+
expectedId })` is pure and returns `PublishProblem` values with a `code` and
|
|
94
|
+
a `message`. Codes cover a missing lockfile, the SDK under `dependencies`, a
|
|
95
|
+
missing peer declaration, a missing or invalid manifest, an entry absent from
|
|
96
|
+
`files`, an id that differs from `expectedId`, and a private package.
|
|
97
|
+
|
|
98
|
+
The package's `standard-plugin` command applies these rules to a directory:
|
|
99
|
+
`check [dir] [--source npm|git]` prints problems and exits with status 1
|
|
100
|
+
when it finds one, and `pack [dir]` runs `check`, runs `npm shrinkwrap` when
|
|
101
|
+
dependencies lack `npm-shrinkwrap.json`, and then prints the
|
|
102
|
+
`npm pack --dry-run` file list.
|
|
103
|
+
|
|
104
|
+
## Content
|
|
105
|
+
|
|
106
|
+
Native content uses rows, styled text, or badges. A row can carry an identity,
|
|
107
|
+
provider revision, styled spans, an action ID, a meter, spark samples, and a
|
|
108
|
+
divider marker. Canvas content declares canonical columns, rows, transparency,
|
|
109
|
+
shade, input capture, and optional hover metadata.
|
|
110
|
+
|
|
111
|
+
View content, `{ kind: 'view', root }`, is a tree of host-rendered nodes. The
|
|
112
|
+
host draws it with the viewer's theme, symbols, focus, and hit testing.
|
|
113
|
+
|
|
114
|
+
Each contribution kind accepts a fixed set of content kinds, and
|
|
115
|
+
`replace(content)` throws a `PluginError` for any other pairing:
|
|
116
|
+
|
|
117
|
+
| Contribution | Rows, text | Canvas | View | Badge |
|
|
118
|
+
|---|---|---|---|---|
|
|
119
|
+
| `section`, `card`, `panel` | yes | yes | yes | no |
|
|
120
|
+
| `slot`, `overlay` | yes | yes | no | no |
|
|
121
|
+
| `badge` | no | no | no | yes |
|
|
122
|
+
| `menu`, `command`, `key`, `link` | no | no | no | no |
|
|
123
|
+
|
|
124
|
+
### Custom interfaces with canvas
|
|
125
|
+
|
|
126
|
+
Canvas is the fully supported path for a plugin that draws its own
|
|
127
|
+
interface. A section, card, or panel can show a canvas in every
|
|
128
|
+
presentation: a sidebar card, a pop-over, a column, or a plugin pane.
|
|
129
|
+
`canvas.columns` is the intrinsic width that the host uses to size the card,
|
|
130
|
+
column, or plugin pane. The view helpers below are optional, and a plugin can
|
|
131
|
+
combine a view card with a canvas panel.
|
|
132
|
+
|
|
133
|
+
`write(ansi)` draws into a grid that the host keeps for the canvas. The grid
|
|
134
|
+
keeps its cells between writes, so a plugin that redraws a whole frame starts
|
|
135
|
+
it with `\x1b[2J\x1b[H`. The host honours these sequences:
|
|
136
|
+
|
|
137
|
+
- SGR (`CSI … m`): reset, bold, dim, italic, underline, inverse, hidden,
|
|
138
|
+
strikethrough, their resets, the 8 and 16 colour forms, and `38`/`48`
|
|
139
|
+
with `5;n` or `2;r;g;b` (colon forms too).
|
|
140
|
+
- Cursor positioning: `CUP`/`HVP` (`H`, `f`), `CUU`, `CUD`, `CUF`, `CUB`,
|
|
141
|
+
`CNL`, `CPL`, `CHA`, and `VPA`, clamped to the canvas.
|
|
142
|
+
- Erasing: `ED` (`J`), `EL` (`K`), and `ECH` (`X`), with the current
|
|
143
|
+
background colour.
|
|
144
|
+
- Carriage return, line feed (which also returns to the first column),
|
|
145
|
+
tab, and backspace.
|
|
146
|
+
|
|
147
|
+
The host drops every other escape and control sequence, including private
|
|
148
|
+
modes, OSC (titles, hyperlinks, clipboard), DCS, APC, PM, and SOS, and every
|
|
149
|
+
C0, C1, and bidirectional formatting character. Text past the right edge is
|
|
150
|
+
clipped, and a line feed on the last row stays on that row: a canvas never
|
|
151
|
+
wraps or scrolls. Wide characters take two cells. Cells in default colours
|
|
152
|
+
take the viewer's theme, and a blank default cell shows the surface under the
|
|
153
|
+
canvas.
|
|
154
|
+
|
|
155
|
+
`hover` is an optional one-line string or an array of styled `TextSpan` values.
|
|
156
|
+
The string must be non-empty after trimming, fit within 512 UTF-8 bytes, and
|
|
157
|
+
contain no C0, C1, DEL, or bidirectional formatting character. Styled hover
|
|
158
|
+
values contain at most 32 spans whose joined text follows the same bounds and
|
|
159
|
+
must be non-empty. Span styling supports `foreground`, `background`, `bold`,
|
|
160
|
+
`italic`, and `underline`; `actionId` is rejected because hover text is
|
|
161
|
+
passive. `Canvas.replace` publishes the canvas metadata, so a plugin can update
|
|
162
|
+
the hover text with another `replace` call while retaining the same canvas
|
|
163
|
+
grid.
|
|
164
|
+
|
|
165
|
+
On the current frontend, live canvases in `machine.before` and
|
|
166
|
+
`machine.after` slots are hover targets. The viewer waits 300 milliseconds
|
|
167
|
+
while the pointer remains over the canvas, then shows the text in a single
|
|
168
|
+
line owned by the host. Leaving the canvas dismisses it immediately. Hover
|
|
169
|
+
does not focus the canvas or change its geometry. The dwell timer and pointer
|
|
170
|
+
state belong to each viewer, so remote viewers can see hover text for a
|
|
171
|
+
machine slot independently.
|
|
172
|
+
Hover overlays for these machine slots use the same 22% sidebar machine colour
|
|
173
|
+
blend as the machine name bar over the viewer's background.
|
|
174
|
+
|
|
175
|
+
`focus(true)` asks for keyboard input and needs `captureInput: true` in the
|
|
176
|
+
canvas spec. While the canvas is shown in a pop-over, column, or plugin pane
|
|
177
|
+
and holds focus, the host sends every key except Escape and
|
|
178
|
+
Control, Alt, or Super chords to `onInput(id, handler)` subscriptions as
|
|
179
|
+
`{ kind: 'key', key, phase, contributionId, entity? }`. `key` is the typed
|
|
180
|
+
character or one of `up`, `down`, `left`, `right`, `enter`, `tab`,
|
|
181
|
+
`backspace`, `delete`, `escape`, `space`, `home`, `end`, `pageup`,
|
|
182
|
+
`pagedown`, or `f1`-`f12`; `phase` is `press` or `repeat`. When the canvas
|
|
183
|
+
stops being shown, for example after Escape, input subscriptions receive
|
|
184
|
+
`{ kind: 'focus', focused: false }` and `onDeactivate(id, handler)`
|
|
185
|
+
subscriptions run. The plugin calls `focus(true)` again to take keys back.
|
|
186
|
+
Sidebar cards and machine slots show only canvases without `captureInput`.
|
|
187
|
+
Local frontends receive canvas cells and send input. Remote viewers receive
|
|
188
|
+
the canvas grids and hover metadata, but remote actions, commands, panel
|
|
189
|
+
visibility changes, and canvas input are read-only until transport support is
|
|
190
|
+
added.
|
|
191
|
+
|
|
192
|
+
## View nodes
|
|
193
|
+
|
|
194
|
+
Every node has a `type`. Field names use camelCase, and tone, weight,
|
|
195
|
+
alignment, and width keywords are lowercase.
|
|
196
|
+
|
|
197
|
+
| Type | Fields |
|
|
198
|
+
|---|---|
|
|
199
|
+
| `stack` | `gap?`, `children` |
|
|
200
|
+
| `row` | `children`, `align?` |
|
|
201
|
+
| `divider` | `label?` |
|
|
202
|
+
| `text` | `text?` or `spans?`, `tone?`, `weight?`, `mono?` |
|
|
203
|
+
| `badge` | `label`, `tone?` |
|
|
204
|
+
| `dot` | `tone?` |
|
|
205
|
+
| `progress` | `value`, `max`, `tone?`, `label?` |
|
|
206
|
+
| `segments` | `items`: `{ value, max, tone?, label? }` |
|
|
207
|
+
| `card` | `title?`, `tone?`, `children` |
|
|
208
|
+
| `stat` | `label`, `value`, `tone?`, `hint?` |
|
|
209
|
+
| `kv` | `items`: `{ label, value, mono?, copy? }` |
|
|
210
|
+
| `tabs` | `id`, `items`, `filters?`, `action?` |
|
|
211
|
+
| `select` | `id`, `label`, `options`, `filters?`, `action?` |
|
|
212
|
+
| `table` | `id`, `columns`, `rows` |
|
|
213
|
+
| `log` | `lines` |
|
|
214
|
+
| `button` | `label`, `action` |
|
|
215
|
+
|
|
216
|
+
Tones are `ok`, `info`, `warn`, `error`, `muted`, `accent`, `pending`, and
|
|
217
|
+
`bright` (`TONES`). Weights are `normal`, `bold`, and `dim`. Alignments are
|
|
218
|
+
`start`, `center`, and `end`.
|
|
219
|
+
|
|
220
|
+
A table column has an `id`, an optional `label`, a `width` of `'fill'` or a
|
|
221
|
+
cell count, an optional `maxWidth` and `align`, and a `priority`. The host
|
|
222
|
+
drops the columns with the highest `priority` values first on a narrow
|
|
223
|
+
surface. A table row has an `id`, `cells` keyed by column ID, and optional
|
|
224
|
+
`tone`, `note` spans, `tags`, and `action`.
|
|
225
|
+
|
|
226
|
+
Tab items and select options carry an `id`, a `label`, an optional `count`,
|
|
227
|
+
and an optional `tag`. When `filters` names a table in the same view, the
|
|
228
|
+
host shows the rows whose `tags` contain the chosen item's `tag`. An item
|
|
229
|
+
without a `tag` shows every row. The host filters and counts for each viewer
|
|
230
|
+
without a plugin round trip.
|
|
231
|
+
|
|
232
|
+
A view action is `{ actionId, value?, opens? }`. The host sends `actionId`
|
|
233
|
+
and the string `value` to the plugin, so an `onAction(actionId, handler)`
|
|
234
|
+
handler receives `event.value`. One action ID with a per-row value replaces
|
|
235
|
+
one subscription per row. When `opens` names a panel, the host opens that
|
|
236
|
+
panel after the plugin accepts the action. A `tabs` or `select` action value
|
|
237
|
+
defaults to the chosen item ID.
|
|
238
|
+
|
|
239
|
+
A node type that the host does not know draws nothing, so a newer SDK can
|
|
240
|
+
add node types.
|
|
241
|
+
|
|
242
|
+
### Card views
|
|
243
|
+
|
|
244
|
+
Titles come from the contribution's `title`. The host adds no status dot,
|
|
245
|
+
health text, or machine label to a card title or panel. Authors can include
|
|
246
|
+
indicators in their own title or content. Runtime health remains available in
|
|
247
|
+
plugin management through `ctx.health.set()`.
|
|
248
|
+
|
|
249
|
+
A card shows a short summary. Its view can contain `stack`, `row`, `text`,
|
|
250
|
+
`badge`, `dot`, `progress`, `segments`, `stat`, and `divider` nodes, and it
|
|
251
|
+
is at most 6 lines tall. A `stack` is as tall as its children plus `gap`
|
|
252
|
+
lines between them. A `row` is as tall as its tallest child. Each other
|
|
253
|
+
allowed node is 1 line, and an unknown node is 0 lines. Put `card`, `kv`,
|
|
254
|
+
`tabs`, `select`, `table`, `log`, and `button` nodes in a panel. A card that
|
|
255
|
+
shows a canvas uses the general canvas bounds.
|
|
256
|
+
|
|
257
|
+
### Bounds
|
|
258
|
+
|
|
259
|
+
`validateView(root)` applies the bounds that the host applies, and
|
|
260
|
+
`replace(content)` calls it for view content. `VIEW_LIMITS` holds the
|
|
261
|
+
numbers.
|
|
262
|
+
|
|
263
|
+
- A tree nests at most 8 nodes deep and holds at most 4,096 nodes. Table
|
|
264
|
+
cells count toward both limits.
|
|
265
|
+
- A table has at most 512 rows and 12 columns.
|
|
266
|
+
- No string contains a C0 or C1 control character, DEL, or a bidirectional
|
|
267
|
+
formatting character.
|
|
268
|
+
- Every number is finite.
|
|
269
|
+
- The serialized tree fits in one 256 KiB frame.
|
|
270
|
+
- An action ID and an action `opens` value are plugin identifiers, and an
|
|
271
|
+
action `value` is at most 512 UTF-8 bytes.
|
|
272
|
+
- An action `opens` value names a panel that the plugin declares.
|
|
273
|
+
- A card view follows the card rules above.
|
|
274
|
+
|
|
275
|
+
`validateView(root, { kind, manifest })` applies the card rules when `kind`
|
|
276
|
+
is `'card'` and the `opens` rule when `manifest` is present.
|
|
277
|
+
|
|
278
|
+
### View helpers
|
|
279
|
+
|
|
280
|
+
`ui` builds nodes as plain JSON. The helpers are optional; hand-written JSON
|
|
281
|
+
with the same fields is equivalent. Options that are `undefined` are left
|
|
282
|
+
out.
|
|
283
|
+
|
|
284
|
+
```js
|
|
285
|
+
import { definePlugin, ui } from '@standardagents/code-plugin-sdk'
|
|
286
|
+
|
|
287
|
+
export default definePlugin({
|
|
288
|
+
id: 'builds-monitor',
|
|
289
|
+
activate(ctx) {
|
|
290
|
+
const card = ctx.card('summary')
|
|
291
|
+
card.replace(ui.view(ui.stack([
|
|
292
|
+
ui.stat('Queue', '3', { tone: 'pending' }),
|
|
293
|
+
ui.table('builds', [{ id: 'version', label: 'Version' }], [{
|
|
294
|
+
id: 'b-1',
|
|
295
|
+
cells: { version: ui.text('0.4.12', { weight: 'bold' }) },
|
|
296
|
+
action: ui.action('open-build', { value: 'b-1', opens: 'build-detail' }),
|
|
297
|
+
}]),
|
|
298
|
+
ui.button('Open dashboard', 'open-dashboard'),
|
|
299
|
+
])))
|
|
300
|
+
ctx.onAction('open-build', event => { /* event.value is 'b-1' */ })
|
|
301
|
+
},
|
|
302
|
+
})
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
| Helper | Result |
|
|
306
|
+
|---|---|
|
|
307
|
+
| `ui.view(root)` | `{ kind: 'view', root }` |
|
|
308
|
+
| `ui.action(actionId, { value, opens })` | an action |
|
|
309
|
+
| `ui.span(text, { tone, weight, mono })` | a span |
|
|
310
|
+
| `ui.stack(children, { gap })`, `ui.row(children, { align })` | a layout node |
|
|
311
|
+
| `ui.card(children, { title, tone })` | a card node |
|
|
312
|
+
| `ui.divider(label)` | a divider |
|
|
313
|
+
| `ui.text(textOrSpans, { tone, weight, mono })` | a text node; an array sets `spans` |
|
|
314
|
+
| `ui.badge(label, tone)`, `ui.dot(tone)` | a badge or dot |
|
|
315
|
+
| `ui.progress(value, max, { tone, label })`, `ui.segments(items)` | a meter |
|
|
316
|
+
| `ui.stat(label, value, { tone, hint })`, `ui.kv(items)` | a value display |
|
|
317
|
+
| `ui.tabs(id, items, { filters, action })` | a tab strip |
|
|
318
|
+
| `ui.select(id, label, options, { filters, action })` | a drop-down |
|
|
319
|
+
| `ui.table(id, columns, rows)` | a table |
|
|
320
|
+
| `ui.log(lines)` | a log |
|
|
321
|
+
| `ui.button(label, action)` | a button; `action` is an action or an action ID |
|
|
322
|
+
|
|
323
|
+
`packages/plugin-sdk/test/fixtures/view-builds-monitor.json` holds a complete
|
|
324
|
+
view that matches the protocol's round-trip test.
|
|
325
|
+
|
|
326
|
+
### Pane header and footer slots
|
|
327
|
+
|
|
328
|
+
A `slot` on `pane.header` or `pane.footer` contributes rows inside the pane
|
|
329
|
+
frame, above or below its terminal body. Declarations use `width: "full"`
|
|
330
|
+
(the default) or `width: "half"`. Contributions stack in ascending plugin
|
|
331
|
+
order, then plugin ID and contribution ID. Full contributions occupy their
|
|
332
|
+
own rows. Consecutive half contributions share a row, with the odd column
|
|
333
|
+
assigned to the left. An unpaired half leaves the right half empty.
|
|
334
|
+
Multiline half contributions share a block as tall as the taller contribution.
|
|
335
|
+
|
|
336
|
+
Rows, styled text, and non-interactive canvas cells render in these slots.
|
|
337
|
+
Each slot is bounded to 32 rows. Small panes clip slot rows while preserving
|
|
338
|
+
terminal space. Slot height participates in the same geometry used for terminal
|
|
339
|
+
painting, pointer input, graphics clipping, and controller-owned resizing.
|
|
340
|
+
Remote viewers receive the same contributions. Missing panes, stale producer
|
|
341
|
+
snapshots, and mismatched pane generations withdraw their rows.
|
|
342
|
+
|
|
343
|
+
Pane-slot actions, input capture, hover popovers, and persisted slot collapse
|
|
344
|
+
remain unconnected. They are separate from the implemented row rendering.
|
|
345
|
+
|
|
346
|
+
`ctx.context.get()` exposes `projects`, `panes`, and `complete` for the owning
|
|
347
|
+
machine. Project records contain `{ entity, name, path }`; pane records contain
|
|
348
|
+
`{ entity, projectId, name, cwd }`. Entities include machine ID and a decimal
|
|
349
|
+
string generation. The pane `cwd` is its configured project launch directory.
|
|
350
|
+
Live shell-directory observations are a separate future capability. Each list
|
|
351
|
+
has at most 128 records, and their combined JSON is bounded to 32 KiB. `complete`
|
|
352
|
+
is false when a local path could not be decoded or a bound omitted records.
|
|
353
|
+
Commands, launch environment, prompts, and encrypted payloads are excluded.
|
|
354
|
+
Plugins must perform filesystem work on their owning machine and clear
|
|
355
|
+
contributions when their target disappears or changes generation.
|
|
356
|
+
|
|
357
|
+
### Current frontend coverage
|
|
358
|
+
|
|
359
|
+
Sections, plugin cards, machine slots, stacked pane header/footer slots, canvas surfaces, host-rendered views,
|
|
360
|
+
global command palette entries, and the three
|
|
361
|
+
panel presentations are implemented. A popover follows the content width and height within the
|
|
362
|
+
modal area. Its title is the contribution title and its body starts beneath it. A column attaches to the sidebar and resizes the workspace. A
|
|
363
|
+
plugin pane has a tab and focus, drag, close, and reveal behavior; its width
|
|
364
|
+
follows content and it does not create a tmux pane or resize a terminal.
|
|
365
|
+
|
|
366
|
+
Project slot anchors, standalone badges attached to hierarchy rows,
|
|
367
|
+
menu registrations, key registrations, link registrations,
|
|
368
|
+
and entity-scoped command palette entries have protocol and composition support
|
|
369
|
+
but do not have connected production frontend surfaces. Badge nodes inside a
|
|
370
|
+
view tree are rendered. The `overlay`
|
|
371
|
+
contribution kind has protocol validation, while general workspace overlay
|
|
372
|
+
placement and input remain unfinished. Use a supported panel presentation for
|
|
373
|
+
current interactive canvases.
|
|
374
|
+
|
|
375
|
+
## Actions and subscriptions
|
|
376
|
+
|
|
377
|
+
Commands, menus, keys, and links carry stable action IDs. Actions receive a
|
|
378
|
+
selection and handler context with an abort signal. Link handlers return a
|
|
379
|
+
boolean that controls matching-handler traversal.
|
|
380
|
+
|
|
381
|
+
Global command registrations appear in the command palette. Menu, key, and
|
|
382
|
+
link registrations are accepted and stored by the SDK and daemon, while their
|
|
383
|
+
frontend menus, global key dispatch, and link list are still planned. An
|
|
384
|
+
entity-scoped command also has no current palette surface. `url.open` is the
|
|
385
|
+
supported host operation for opening a URL.
|
|
386
|
+
|
|
387
|
+
Event, hook, input, select, resize, activate, deactivate, and visibility
|
|
388
|
+
subscriptions use bounded conditions. Schedules allow `always`,
|
|
389
|
+
`section-visible`, `slot-visible`, or `panel-open` conditions. One schedule
|
|
390
|
+
invocation runs at a time.
|
|
391
|
+
|
|
392
|
+
## Typed requests
|
|
393
|
+
|
|
394
|
+
The operation map covers pane and project lifecycle, notifications, URL and
|
|
395
|
+
fetch access, secrets, configuration, account state, context, popovers, canvas
|
|
396
|
+
write and focus, subscription changes, health, and webhook acknowledgements.
|
|
397
|
+
Each operation has a typed input and output. `RequestOptions` accepts an abort
|
|
398
|
+
signal and a timeout.
|
|
399
|
+
|
|
400
|
+
`ctx.state` provides account-backed `get(key)`, `set(key, value)`, and
|
|
401
|
+
`keys()` methods. State is isolated by the plugin ID. `keys()` takes no input
|
|
402
|
+
besides optional `RequestOptions` and returns a sorted `string[]` containing
|
|
403
|
+
the plugin's key names. The result is empty when no keys exist, is limited to
|
|
404
|
+
256 keys, and has no prefix or pagination parameters. A key written by one
|
|
405
|
+
machine is visible to the other machines signed into the same account, so a
|
|
406
|
+
plugin can use distinct machine-specific key names without maintaining a
|
|
407
|
+
shared index. Key names are 1 to 128 bytes. The first character is ASCII
|
|
408
|
+
alphanumeric or `_`; later characters are ASCII alphanumeric, `_`, `.`, `:`,
|
|
409
|
+
or `-`. Values remain limited to 64 KiB of JSON. The host applies the same
|
|
410
|
+
account authority and revocation checks to all three methods.
|
|
411
|
+
|
|
412
|
+
The public SDK applies the same capability checks and request bounds to every
|
|
413
|
+
plugin. Plugin IDs do not grant fetch, pane, project, or secret exceptions.
|
|
414
|
+
The current native host connects fetch, secrets, configuration, account state,
|
|
415
|
+
context, URL opening, subscription changes, health, canvas writes, and canvas
|
|
416
|
+
focus. Pane/project lifecycle requests, notifications, `popover.open`, and
|
|
417
|
+
webhook acknowledgements have public types but currently return an unsupported
|
|
418
|
+
operation error through the production context. Declaring a capability does
|
|
419
|
+
not supply an unfinished host adapter.
|
|
420
|
+
|
|
421
|
+
The shared fetch policy currently permits HTTPS GET requests to
|
|
422
|
+
`builds.standardcode.ai` on port 443 under `/admin/api/` or `/builds/`. Requests
|
|
423
|
+
have no body, user-info, or fragment. Allowed request headers are authorization,
|
|
424
|
+
range, accept, and cache-control. The response body limit is 16 MiB. These
|
|
425
|
+
bounds apply to every fetch-authorized plugin. Plugin code retains normal
|
|
426
|
+
Node.js access to files, networking, and subprocesses under the user's identity;
|
|
427
|
+
capabilities govern SDK/host operations and do not provide OS sandboxing.
|
|
428
|
+
|
|
429
|
+
## Build context
|
|
430
|
+
|
|
431
|
+
`context.get` returns a `PluginContextInfo`:
|
|
432
|
+
|
|
433
|
+
```json
|
|
434
|
+
{
|
|
435
|
+
"accountId": "account-id",
|
|
436
|
+
"machineId": "machine-id",
|
|
437
|
+
"projects": [],
|
|
438
|
+
"panes": [],
|
|
439
|
+
"complete": true,
|
|
440
|
+
"build": {
|
|
441
|
+
"version": "0.14.1-branch.3bd7f23c.98a422",
|
|
442
|
+
"commit": "<40 hex digits>",
|
|
443
|
+
"ref": "refs/heads/feat/sidebar-plugins",
|
|
444
|
+
"channel": "branch"
|
|
445
|
+
},
|
|
446
|
+
"fleet": {
|
|
447
|
+
"mode": "follow",
|
|
448
|
+
"npmTag": "branch-feat-sidebar-plugins-3bd7f23c-b-6e049b24f5928544",
|
|
449
|
+
"ref": "refs/heads/feat/sidebar-plugins",
|
|
450
|
+
"version": "0.14.1-branch.3bd7f23c.98a422"
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
`build` describes the native release that this machine runs. It is `null` for
|
|
456
|
+
a development binary without release metadata.
|
|
457
|
+
|
|
458
|
+
- `channel` is `branch` for branch and issue builds, `canary` for `main`
|
|
459
|
+
builds, `production` for a stable version, and `team` for a legacy team
|
|
460
|
+
release.
|
|
461
|
+
- `ref` is `refs/heads/main` for canary and team builds and
|
|
462
|
+
`refs/tags/vX.Y.Z` for production builds. A branch build reports its sealed
|
|
463
|
+
branch name.
|
|
464
|
+
|
|
465
|
+
`fleet` describes the account build policy. It is `null` when the account has
|
|
466
|
+
no build policy.
|
|
467
|
+
|
|
468
|
+
- `mode` is `follow` for a followed npm tag, `production` for the `latest`
|
|
469
|
+
tag, and `pin` for an exact version.
|
|
470
|
+
- `npmTag` is the followed tag, or `null` for a pin.
|
|
471
|
+
- `version` is the pinned version, or the target of the current rollout, or
|
|
472
|
+
the active account version. It is `null` when none is known.
|
|
473
|
+
- `ref` is `refs/heads/main` for `canary`. A branch or issue tag has a `ref`
|
|
474
|
+
only when it names the branch of the running build. The daemon does not
|
|
475
|
+
read the network to resolve other branch names, so `ref` is `null` for them.
|
|
476
|
+
|
|
477
|
+
Older daemons return only `accountId` and `machineId`. Treat a missing
|
|
478
|
+
`build` or `fleet` as `null`.
|
|
479
|
+
|
|
480
|
+
A plugin with the `events` capability can subscribe with
|
|
481
|
+
`onEvent('build-context', handler)`. The daemon delivers a `BuildContextEvent`
|
|
482
|
+
when `build` or `fleet` changes. Its `data` holds the new `build` and `fleet`
|
|
483
|
+
values in the shape above. Other account changes do not deliver the event, and
|
|
484
|
+
the daemon does not deliver it when a plugin starts. Read `context.get` at
|
|
485
|
+
activation for the initial values. The daemon pushes each change, so a plugin
|
|
486
|
+
needs no schedule to track the followed channel.
|
|
487
|
+
|
|
488
|
+
Plugins with `events` can subscribe to `onEvent('pane-context', handler)`.
|
|
489
|
+
Its `data` replaces the local `machineId`, `projects`, `panes`, and `complete`
|
|
490
|
+
fields from `context.get`. Only a changed local context produces an event.
|
|
491
|
+
Subscribe before the initial `context.get` request and discard that response
|
|
492
|
+
if an event arrives while it is pending. Pane removal and generation changes
|
|
493
|
+
must withdraw publishers for the old entity. Project launch directories are
|
|
494
|
+
configured paths; shell `cd` changes are outside this event's current contract.
|
|
495
|
+
|
|
496
|
+
A plugin can watch these directories with Node's native filesystem events.
|
|
497
|
+
Debounce bursts with a one-shot timer, retain changes that arrive during a read,
|
|
498
|
+
and close watchers on disposal or target removal. Do not schedule directory
|
|
499
|
+
or Git refresh polling. Watch linked-worktree Git metadata and shared refs as
|
|
500
|
+
well as the working tree. Filesystem notification errors should report degraded
|
|
501
|
+
health; they must not silently switch to polling.
|
|
502
|
+
|
|
503
|
+
## Hooks and delivery
|
|
504
|
+
|
|
505
|
+
Hook dispatch and durable webhook delivery are planned host integrations.
|
|
506
|
+
The public contract defines hook events with an entity, launch specification,
|
|
507
|
+
ancestry, operation ID, and deadline. A hook returns `proceed` with an optional replacement launch or
|
|
508
|
+
`cancel` with a bounded reason. Webhook events carry a delivery ID for
|
|
509
|
+
acknowledgement and replay-safe handling.
|
|
510
|
+
|
|
511
|
+
## Testing harness
|
|
512
|
+
|
|
513
|
+
`@standardagents/code-plugin-sdk/testing` provides `createHarness`. A harness drives
|
|
514
|
+
activation, event delivery, visibility, manual time, flushing, and disposal.
|
|
515
|
+
It records traces, surface replacement, subscriptions, and resources without
|
|
516
|
+
starting subprocesses. `harness.surface(id, entity?)` returns the current
|
|
517
|
+
content of a published contribution, including view and canvas content, or
|
|
518
|
+
`undefined` after the contribution clears.
|
|
519
|
+
|
|
520
|
+
### Canvas theme colors
|
|
521
|
+
|
|
522
|
+
`CanvasSpec.themeColors` maps up to 32 canvas-local ANSI indexes (0–255) to
|
|
523
|
+
recipes. Each recipe has `source` (optional ANSI index 0–15, omitted for the
|
|
524
|
+
default foreground), `mix` (optional second ANSI index mixed equally), and
|
|
525
|
+
`opacity` (0–1, blended against the viewer background). Both foreground and
|
|
526
|
+
background SGR references to that index resolve through the same recipe.
|
|
527
|
+
For example, `{ 16: { source: 4, opacity: 0.72 }, 20: { opacity: 0.4 } }`
|
|
528
|
+
provides muted terminal blue at index 16 and a subtle foreground at index 20.
|
|
529
|
+
|
|
530
|
+
Resolution occurs in the viewer for sidebar, pane, slot, column, and popover
|
|
531
|
+
canvases. Remote producers do not read or bake another machine's colors.
|
|
532
|
+
Recipes remain unchanged across terminal theme updates. Unavailable source
|
|
533
|
+
colors fall back to the named ANSI index or default foreground; unavailable
|
|
534
|
+
background colors leave opacity unapplied. OSC queries remain unsupported
|
|
535
|
+
in plugin canvas streams. This capability is public and identical for all
|
|
536
|
+
plugins.
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Authoring tool for plugin packages. The SDK entry never imports this file.
|
|
3
|
+
import { spawnSync } from 'node:child_process'
|
|
4
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs'
|
|
5
|
+
import { join, resolve } from 'node:path'
|
|
6
|
+
import { COLLECTION_FILE, PluginError, checkPackageForPublish, validateCollection } from '../src/index.mjs'
|
|
7
|
+
|
|
8
|
+
const USAGE = `usage: standard-plugin check [dir] [--source npm|git]
|
|
9
|
+
standard-plugin pack [dir]
|
|
10
|
+
|
|
11
|
+
check validates package.json, ${COLLECTION_FILE}, each collection entry, and lockfiles
|
|
12
|
+
pack runs check, creates npm-shrinkwrap.json when dependencies need one, then runs npm pack --dry-run`
|
|
13
|
+
const IGNORED_DIRECTORIES = new Set(['node_modules', '.git'])
|
|
14
|
+
const FILE_LIMIT = 50000
|
|
15
|
+
|
|
16
|
+
function readJson(path) {
|
|
17
|
+
let text
|
|
18
|
+
try { text = readFileSync(path, 'utf8') } catch (error) {
|
|
19
|
+
if (error.code === 'ENOENT') return undefined
|
|
20
|
+
throw error
|
|
21
|
+
}
|
|
22
|
+
return JSON.parse(text)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Relative POSIX paths of regular files under root, without following links. */
|
|
26
|
+
function listFiles(root) {
|
|
27
|
+
const files = []
|
|
28
|
+
const pending = ['']
|
|
29
|
+
while (pending.length) {
|
|
30
|
+
const directory = pending.pop()
|
|
31
|
+
for (const entry of readdirSync(join(root, directory), { withFileTypes: true })) {
|
|
32
|
+
const path = directory ? `${directory}/${entry.name}` : entry.name
|
|
33
|
+
if (entry.isDirectory()) { if (!IGNORED_DIRECTORIES.has(entry.name)) pending.push(path) }
|
|
34
|
+
else if (entry.isFile()) files.push(path)
|
|
35
|
+
if (files.length > FILE_LIMIT) throw new Error(`More than ${FILE_LIMIT} files under ${root}`)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return files
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function tryJson(root, name, problems, location) {
|
|
42
|
+
try { return readJson(join(root, name)) } catch (error) {
|
|
43
|
+
problems.push({ location, code: 'invalid_json', message: `${name}: ${error.message}` })
|
|
44
|
+
return undefined
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function collect(problems, location, list) {
|
|
49
|
+
for (const problem of list) problems.push({ location, ...problem })
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Checks one source directory. Returns problems with the directory each one belongs to. */
|
|
53
|
+
function checkDirectory(root, { sourceKind = 'npm' } = {}) {
|
|
54
|
+
const problems = []
|
|
55
|
+
const files = listFiles(root)
|
|
56
|
+
const packageJson = tryJson(root, 'package.json', problems, '.')
|
|
57
|
+
const collectionInput = tryJson(root, COLLECTION_FILE, problems, '.')
|
|
58
|
+
if (collectionInput === undefined) {
|
|
59
|
+
if (packageJson === undefined) problems.push({ location: '.', code: 'package_missing', message: `package.json or ${COLLECTION_FILE} is required` })
|
|
60
|
+
else collect(problems, '.', checkPackageForPublish({ packageJson, files, sourceKind }))
|
|
61
|
+
return problems
|
|
62
|
+
}
|
|
63
|
+
let collection
|
|
64
|
+
try { collection = validateCollection(collectionInput) } catch (error) {
|
|
65
|
+
if (!(error instanceof PluginError)) throw error
|
|
66
|
+
problems.push({ location: '.', code: error.code, message: `${COLLECTION_FILE}: ${error.message}` })
|
|
67
|
+
return problems
|
|
68
|
+
}
|
|
69
|
+
if (packageJson !== undefined) collect(problems, '.', checkPackageForPublish({ packageJson, files, sourceKind, requireManifest: false }))
|
|
70
|
+
else if (sourceKind === 'npm') problems.push({ location: '.', code: 'package_missing', message: 'an npm package needs a root package.json' })
|
|
71
|
+
for (const entry of collection.plugins) {
|
|
72
|
+
const entryPackage = tryJson(join(root, entry.path), 'package.json', problems, entry.path)
|
|
73
|
+
if (entryPackage === undefined) {
|
|
74
|
+
if (!problems.some(problem => problem.location === entry.path)) {
|
|
75
|
+
problems.push({ location: entry.path, code: 'package_missing', message: `collection entry ${entry.id} has no package.json` })
|
|
76
|
+
}
|
|
77
|
+
continue
|
|
78
|
+
}
|
|
79
|
+
const prefix = `${entry.path}/`
|
|
80
|
+
const entryFiles = files.filter(file => file.startsWith(prefix)).map(file => file.slice(prefix.length))
|
|
81
|
+
collect(problems, entry.path, checkPackageForPublish({ packageJson: entryPackage, files: entryFiles, sourceKind, expectedId: entry.id }))
|
|
82
|
+
}
|
|
83
|
+
return problems
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function report(problems) {
|
|
87
|
+
for (const problem of problems) console.log(`${problem.location}: ${problem.code}: ${problem.message}`)
|
|
88
|
+
console.log(problems.length ? `${problems.length} problem${problems.length === 1 ? '' : 's'}` : 'ok')
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function npm(args, cwd) {
|
|
92
|
+
const result = spawnSync('npm', args, { cwd, stdio: 'inherit', shell: process.platform === 'win32' })
|
|
93
|
+
if (result.error) throw result.error
|
|
94
|
+
return result.status ?? 1
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function parse(argv) {
|
|
98
|
+
const [command, ...rest] = argv
|
|
99
|
+
const options = { directory: '.', sourceKind: 'npm' }
|
|
100
|
+
for (let index = 0; index < rest.length; index++) {
|
|
101
|
+
const argument = rest[index]
|
|
102
|
+
if (argument === '--source') {
|
|
103
|
+
options.sourceKind = rest[++index]
|
|
104
|
+
if (!['npm', 'git'].includes(options.sourceKind)) throw new Error('--source takes npm or git')
|
|
105
|
+
} else if (argument.startsWith('-')) throw new Error(`unknown option ${argument}`)
|
|
106
|
+
else options.directory = argument
|
|
107
|
+
}
|
|
108
|
+
return { command, ...options, directory: resolve(options.directory) }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function check(options) {
|
|
112
|
+
const problems = checkDirectory(options.directory, options)
|
|
113
|
+
report(problems)
|
|
114
|
+
return problems.length ? 1 : 0
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function pack(options) {
|
|
118
|
+
if (options.sourceKind !== 'npm') throw new Error('pack checks an npm package; --source git does not apply')
|
|
119
|
+
let problems = checkDirectory(options.directory, options)
|
|
120
|
+
const missing = problems.filter(problem => problem.code === 'lockfile_missing')
|
|
121
|
+
for (const problem of missing) {
|
|
122
|
+
const directory = resolve(options.directory, problem.location)
|
|
123
|
+
if (!existsSync(join(directory, 'package-lock.json'))) {
|
|
124
|
+
console.log(`${problem.location}: run npm install first so npm shrinkwrap has a package-lock.json to convert`)
|
|
125
|
+
continue
|
|
126
|
+
}
|
|
127
|
+
console.log(`${problem.location}: creating npm-shrinkwrap.json from package-lock.json`)
|
|
128
|
+
const status = npm(['shrinkwrap'], directory)
|
|
129
|
+
if (status !== 0) return status
|
|
130
|
+
console.log(`${problem.location}: commit npm-shrinkwrap.json; npm publishes it with the package`)
|
|
131
|
+
}
|
|
132
|
+
if (missing.length) problems = checkDirectory(options.directory, options)
|
|
133
|
+
report(problems)
|
|
134
|
+
if (problems.length) return 1
|
|
135
|
+
return npm(['pack', '--dry-run'], options.directory)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function main(argv) {
|
|
139
|
+
let options
|
|
140
|
+
try { options = parse(argv) } catch (error) { console.error(error.message); console.error(USAGE); return 2 }
|
|
141
|
+
try {
|
|
142
|
+
if (options.command === 'check') return check(options)
|
|
143
|
+
if (options.command === 'pack') return pack(options)
|
|
144
|
+
} catch (error) { console.error(`standard-plugin: ${error.message}`); return 2 }
|
|
145
|
+
console.error(USAGE)
|
|
146
|
+
return 2
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
process.exitCode = main(process.argv.slice(2))
|