@standardagents/code-plugin-sdk 1.0.0-alpha.1 → 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 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.0" },
15
- "devDependencies": { "@standardagents/code-plugin-sdk": "^1.0.0-alpha.0" },
14
+ "peerDependencies": { "@standardagents/code-plugin-sdk": "^1.0.0-alpha.10-rows.0" },
15
+ "devDependencies": { "@standardagents/code-plugin-sdk": "^1.0.0-alpha.10-rows.0" },
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's default export, its `testing` export,
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,15 +175,92 @@ 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
- Local `ctx.state` belongs to one machine.
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. `ctx.state.keys()` returns the sorted key names for this
181
+ plugin's account namespace. It returns an empty array when the plugin has no
182
+ keys and accepts only `RequestOptions`; it has no prefix or pagination
183
+ arguments. Another plugin cannot read these keys, even when it uses the same
184
+ account. Key names are 1 to 128 bytes. The first character is ASCII
185
+ alphanumeric or `_`; later characters are ASCII alphanumeric, `_`, `.`, `:`,
186
+ or `-`. A plugin holds at most 256 keys, and one value is at most 64 KiB of
187
+ JSON. The host applies the same account authority and revocation checks to
188
+ `keys()` as it applies to `get()` and `set()`.
165
189
  The public context declarations are in `src/index.d.ts`.
166
190
 
167
191
  ## Releases
168
192
 
169
193
  The Standard Code build workflow publishes this package when the version in
170
- `package.json` changes. Prereleases publish under the `next` dist-tag and
171
- releases under `latest`.
194
+ `package.json` changes. Main prereleases publish under `next` and main releases
195
+ under `latest`. Feature branches publish under their product branch tag. Authors
196
+ can install a published prerelease by its exact version.
197
+
198
+ ## Sidebar card titles
199
+
200
+ Sidebar card titles use muted, regular-weight text. A card declaration may
201
+ supply an `icon` containing one Nerd Font private-use glyph, for example
202
+ `"icon": "\uDB81\uDCC5"` (a gauge). The author chooses the card's topic icon.
203
+ The host shows it only on the viewer's Nerd Font tier. Other tiers omit the
204
+ icon and its spacing. Omitted icons have no fallback. This applies to bordered
205
+ and borderless cards. Empty titles remain empty.
206
+
207
+ A card view may replace its declared title through
208
+ `ui.view(root, { title: [{ text: 'Builds ' }, { text: '●', foreground: 'green' }] })`.
209
+ The optional `title` uses native `TextSpan` fields, including `#RRGGBB` and the terminal colors `black`, `red`, `green`,
210
+ `yellow`, `blue`, `magenta`, `cyan`, and `white`;
211
+ `ui.span()` is a body-view builder with a different shape. Titles accept 1–32
212
+ passive spans and at most 512 UTF-8 bytes of combined, nonblank single-line text.
213
+ Actions, control characters, and bidi formatting are rejected. Each replacement
214
+ updates the body and title together. Omitting `title` restores the manifest title.
215
+ This option belongs to card views. Plugins own the meaning and color of any
216
+ status light; the SDK adds no health indicator. Host title styling and supported
217
+ author-selected Nerd Font icons apply around the author-supplied spans.
172
218
 
173
219
  ## License
174
220
 
175
221
  MIT. See `LICENSE`.
222
+
223
+ ## Responsive pane rows
224
+
225
+ A pane header or footer slot can publish `rows` containing a `layout` and empty
226
+ `spans`. `layout.fields` is an ordered list of passive text fields. The host
227
+ resolves its geometry separately for every viewer, including pane resizes.
228
+ No resize event or producer repaint is needed.
229
+
230
+ ```js
231
+ slot.replace({ kind: 'rows', rows: [{ id: 'status', spans: [], layout: {
232
+ projectTint: true,
233
+ fields: [
234
+ { text: 'org/repo', bold: true },
235
+ { text: '~/src/repo', gap: 2, flex: true, tone: 'muted',
236
+ shrink: { priority: 1, min: 8, mode: 'middle' } },
237
+ { text: ' +24 ', gap: 2, flex: true, tone: 'ok', surface: true },
238
+ { text: ' −6 ', tone: 'error', surface: true },
239
+ ],
240
+ } }] })
241
+ ```
242
+
243
+ `gap` reserves 0–8 cells before a field. `flex` distributes spare width across
244
+ marked gaps; related fields without flexible gaps stay together. The first
245
+ field starts at the left edge and the final field reaches the right edge when
246
+ at least one visible flexible gap exists. `projectTint` fills the whole row
247
+ with the pane project's identity tint. `tone` accepts `muted`, `ok`, `error`,
248
+ `info`, `warning`, or `project`; semantic colors resolve against the viewer's
249
+ terminal palette. `surface` shades that field's background and `bold` emphasizes
250
+ its text. Include spaces in a field for padded color blocks.
251
+
252
+ `icon` is one Nerd Font private-use glyph. `fallback` is an optional ASCII
253
+ replacement (up to 16 bytes); viewers without Nerd Font support use the fallback.
254
+ The host supplies one space after an icon or fallback.
255
+
256
+ `shrink` policies run by increasing `priority` (0–255), then field order.
257
+ `min` is the retained text width in cells. `start`, `middle`, and `end` select
258
+ where an ellipsis replaces removed text. `hide` removes a field including its
259
+ icon and gap; all hide policies with equal priority are removed together.
260
+ Trimming preserves grapheme boundaries. If the declared minimum widths still
261
+ exceed the pane, remaining content clips at the right edge.
262
+
263
+ Layouts allow 1–32 fields, 2,048 UTF-8 bytes per text field and 8,192 total text
264
+ bytes. Controls and bidi formatting characters are rejected. Layouts are
265
+ supported only in pane header/footer slots. Ordinary rows and full/half slot
266
+ stacking retain their existing behavior.
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.