@standardagents/code-plugin-sdk 1.0.0-alpha.4-hover.1 → 1.0.0-alpha.5-usage.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.4-hover.1" },
15
- "devDependencies": { "@standardagents/code-plugin-sdk": "^1.0.0-alpha.4-hover.1" },
14
+ "peerDependencies": { "@standardagents/code-plugin-sdk": "^1.0.0-alpha.5-usage.0" },
15
+ "devDependencies": { "@standardagents/code-plugin-sdk": "^1.0.0-alpha.5-usage.0" },
16
16
  "standardPlugin": {
17
17
  "apiVersion": 1,
18
18
  "id": "example-status",
@@ -46,9 +46,9 @@ plugin's own test suite.
46
46
 
47
47
  The complete rendering contract is in [REFERENCE.md](./REFERENCE.md). It
48
48
  covers rows, styled text, badges, cards, slots, panels, overlays, host-rendered
49
- views, ANSI canvases, canvas hover text, input focus, popover/column/pane
50
- presentations, remote read-only behavior, and the frontend surfaces that are
51
- still planned.
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
52
 
53
53
  Plugin source imports the default package entry. Plugin tests may import
54
54
  `@standardagents/code-plugin-sdk/testing` for `createHarness`. Runner internals,
@@ -177,8 +177,15 @@ methods.
177
177
 
178
178
  `ctx.state` is per-plugin key and value storage. Standard Code stores these
179
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.
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()`.
182
189
  The public context declarations are in `src/index.d.ts`.
183
190
 
184
191
  ## Releases
package/REFERENCE.md CHANGED
@@ -5,7 +5,7 @@ The public declarations live in
5
5
  plugin authors.
6
6
 
7
7
  The bundled SDK and the npm package use the same public declarations. This
8
- reference describes SDK `1.0.0-alpha.4-hover.1`. Plugins should declare the
8
+ reference describes SDK `1.0.0-alpha.5-usage.0`. Plugins should declare the
9
9
  version published for their product build in both `peerDependencies` and
10
10
  `devDependencies`; the manifest `apiVersion` remains `1`.
11
11
 
@@ -44,6 +44,12 @@ panels:
44
44
 
45
45
  `PRESENTATIONS` lists the accepted presentation values.
46
46
 
47
+ Account-level sidebar cards prefer a live producer on the viewing machine.
48
+ This keeps card actions and attached panels local to that viewer. If no live
49
+ local producer exists, the host uses its deterministic remote-source fallback;
50
+ remote source authority restrictions still apply. Plugins aggregate shared
51
+ account data themselves through the public state API.
52
+
47
53
  An anchor determines the entity that owns a contribution. Machine anchors use
48
54
  a machine entity, project anchors use a project entity, and pane anchors use a
49
55
  pane entity. The `plugins` and `account` anchors are account-level surfaces;
@@ -139,11 +145,15 @@ wraps or scrolls. Wide characters take two cells. Cells in default colours
139
145
  take the viewer's theme, and a blank default cell shows the surface under the
140
146
  canvas.
141
147
 
142
- `hover` is an optional one-line string. It must be non-empty after trimming,
143
- fit within 512 UTF-8 bytes, and contain no C0, C1, DEL, or bidirectional
144
- formatting character. `Canvas.replace` publishes the canvas metadata, so a
145
- plugin can update the hover text with another `replace` call while retaining
146
- the same canvas grid.
148
+ `hover` is an optional one-line string or an array of styled `TextSpan` values.
149
+ The string must be non-empty after trimming, fit within 512 UTF-8 bytes, and
150
+ contain no C0, C1, DEL, or bidirectional formatting character. Styled hover
151
+ values contain at most 32 spans whose joined text follows the same bounds and
152
+ must be non-empty. Span styling supports `foreground`, `background`, `bold`,
153
+ `italic`, and `underline`; `actionId` is rejected because hover text is
154
+ passive. `Canvas.replace` publishes the canvas metadata, so a plugin can update
155
+ the hover text with another `replace` call while retaining the same canvas
156
+ grid.
147
157
 
148
158
  On the current frontend, live canvases in `machine.before` and
149
159
  `machine.after` slots are hover targets. The viewer waits 300 milliseconds
@@ -152,6 +162,8 @@ line owned by the host. Leaving the canvas dismisses it immediately. Hover
152
162
  does not focus the canvas or change its geometry. The dwell timer and pointer
153
163
  state belong to each viewer, so remote viewers can see hover text for a
154
164
  machine slot independently.
165
+ Hover overlays for these machine slots use the same 22% sidebar machine colour
166
+ blend as the machine name bar over the viewer's background.
155
167
 
156
168
  `focus(true)` asks for keyboard input and needs `captureInput: true` in the
157
169
  canvas spec. While the canvas is shown in a pop-over, column, or plugin pane
@@ -299,17 +311,48 @@ export default definePlugin({
299
311
  `packages/plugin-sdk/test/fixtures/view-builds-monitor.json` holds a complete
300
312
  view that matches the protocol's round-trip test.
301
313
 
314
+ ### Pane header and footer slots
315
+
316
+ A `slot` on `pane.header` or `pane.footer` contributes rows inside the pane
317
+ frame, above or below its terminal body. Declarations use `width: "full"`
318
+ (the default) or `width: "half"`. Contributions stack in ascending plugin
319
+ order, then plugin ID and contribution ID. Full contributions occupy their
320
+ own rows. Consecutive half contributions share a row, with the odd column
321
+ assigned to the left. An unpaired half leaves the right half empty.
322
+ Multiline half contributions share a block as tall as the taller contribution.
323
+
324
+ Rows, styled text, and non-interactive canvas cells render in these slots.
325
+ Each slot is bounded to 32 rows. Small panes clip slot rows while preserving
326
+ terminal space. Slot height participates in the same geometry used for terminal
327
+ painting, pointer input, graphics clipping, and controller-owned resizing.
328
+ Remote viewers receive the same contributions. Missing panes, stale producer
329
+ snapshots, and mismatched pane generations withdraw their rows.
330
+
331
+ Pane-slot actions, input capture, hover popovers, and persisted slot collapse
332
+ remain unconnected. They are separate from the implemented row rendering.
333
+
334
+ `ctx.context.get()` exposes `projects`, `panes`, and `complete` for the owning
335
+ machine. Project records contain `{ entity, name, path }`; pane records contain
336
+ `{ entity, projectId, name, cwd }`. Entities include machine ID and a decimal
337
+ string generation. The pane `cwd` is its configured project launch directory.
338
+ Live shell-directory observations are a separate future capability. Each list
339
+ has at most 128 records, and their combined JSON is bounded to 32 KiB. `complete`
340
+ is false when a local path could not be decoded or a bound omitted records.
341
+ Commands, launch environment, prompts, and encrypted payloads are excluded.
342
+ Plugins must perform filesystem work on their owning machine and clear
343
+ contributions when their target disappears or changes generation.
344
+
302
345
  ### Current frontend coverage
303
346
 
304
- Sections, plugin cards, machine slots, canvas surfaces, host-rendered views,
347
+ Sections, plugin cards, machine slots, stacked pane header/footer slots, canvas surfaces, host-rendered views,
305
348
  global command palette entries, and the three
306
349
  panel presentations are implemented. A popover is content-sized within the
307
350
  modal area. A column attaches to the sidebar and resizes the workspace. A
308
351
  plugin pane has a tab and focus, drag, close, and reveal behavior; its width
309
352
  follows content and it does not create a tmux pane or resize a terminal.
310
353
 
311
- Project slot anchors, standalone badges attached to hierarchy rows, pane
312
- header/footer anchors, menu registrations, key registrations, link registrations,
354
+ Project slot anchors, standalone badges attached to hierarchy rows,
355
+ menu registrations, key registrations, link registrations,
313
356
  and entity-scoped command palette entries have protocol and composition support
314
357
  but do not have connected production frontend surfaces. Badge nodes inside a
315
358
  view tree are rendered. The `overlay`
@@ -342,6 +385,18 @@ write and focus, subscription changes, health, and webhook acknowledgements.
342
385
  Each operation has a typed input and output. `RequestOptions` accepts an abort
343
386
  signal and a timeout.
344
387
 
388
+ `ctx.state` provides account-backed `get(key)`, `set(key, value)`, and
389
+ `keys()` methods. State is isolated by the plugin ID. `keys()` takes no input
390
+ besides optional `RequestOptions` and returns a sorted `string[]` containing
391
+ the plugin's key names. The result is empty when no keys exist, is limited to
392
+ 256 keys, and has no prefix or pagination parameters. A key written by one
393
+ machine is visible to the other machines signed into the same account, so a
394
+ plugin can use distinct machine-specific key names without maintaining a
395
+ shared index. Key names are 1 to 128 bytes. The first character is ASCII
396
+ alphanumeric or `_`; later characters are ASCII alphanumeric, `_`, `.`, `:`,
397
+ or `-`. Values remain limited to 64 KiB of JSON. The host applies the same
398
+ account authority and revocation checks to all three methods.
399
+
345
400
  The public SDK applies the same capability checks and request bounds to every
346
401
  plugin. Plugin IDs do not grant fetch, pane, project, or secret exceptions.
347
402
  The current native host connects fetch, secrets, configuration, account state,
@@ -367,6 +422,9 @@ capabilities govern SDK/host operations and do not provide OS sandboxing.
367
422
  {
368
423
  "accountId": "account-id",
369
424
  "machineId": "machine-id",
425
+ "projects": [],
426
+ "panes": [],
427
+ "complete": true,
370
428
  "build": {
371
429
  "version": "0.14.1-branch.3bd7f23c.98a422",
372
430
  "commit": "<40 hex digits>",
@@ -415,6 +473,21 @@ the daemon does not deliver it when a plugin starts. Read `context.get` at
415
473
  activation for the initial values. The daemon pushes each change, so a plugin
416
474
  needs no schedule to track the followed channel.
417
475
 
476
+ Plugins with `events` can subscribe to `onEvent('pane-context', handler)`.
477
+ Its `data` replaces the local `machineId`, `projects`, `panes`, and `complete`
478
+ fields from `context.get`. Only a changed local context produces an event.
479
+ Subscribe before the initial `context.get` request and discard that response
480
+ if an event arrives while it is pending. Pane removal and generation changes
481
+ must withdraw publishers for the old entity. Project launch directories are
482
+ configured paths; shell `cd` changes are outside this event's current contract.
483
+
484
+ A plugin can watch these directories with Node's native filesystem events.
485
+ Debounce bursts with a one-shot timer, retain changes that arrive during a read,
486
+ and close watchers on disposal or target removal. Do not schedule directory
487
+ or Git refresh polling. Watch linked-worktree Git metadata and shared refs as
488
+ well as the working tree. Filesystem notification errors should report degraded
489
+ health; they must not silently switch to polling.
490
+
418
491
  ## Hooks and delivery
419
492
 
420
493
  Hook dispatch and durable webhook delivery are planned host integrations.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@standardagents/code-plugin-sdk",
3
- "version": "1.0.0-alpha.4-hover.1",
3
+ "version": "1.0.0-alpha.5-usage.0",
4
4
  "type": "module",
5
5
  "description": "Standard Code plugin authoring SDK",
6
6
  "license": "MIT",
package/src/index.d.ts CHANGED
@@ -90,8 +90,8 @@ export interface CanvasSpec {
90
90
  transparent?: boolean;
91
91
  shade?: number;
92
92
  captureInput?: boolean;
93
- /** A bounded one-line string shown while the pointer rests over the canvas. */
94
- hover?: string;
93
+ /** A bounded one-line string or styled spans shown while the pointer rests over the canvas. */
94
+ hover?: string | TextSpan[];
95
95
  }
96
96
  export type CanvasContent = { kind: 'canvas'; canvas: CanvasSpec };
97
97
  /**
@@ -235,13 +235,27 @@ export type FleetPolicy = {
235
235
  /** The pinned or target version, or null when it is not known. */
236
236
  version: string | null;
237
237
  };
238
+ /** A local project available to plugins on its owning machine. */
239
+ export interface PluginProjectContext { entity: EntityRef; name: string; path: string }
240
+ /** Pane context with its configured project launch directory. */
241
+ export interface PluginPaneContext { entity: EntityRef; projectId: string; name: string; cwd: string }
238
242
  /** The `context.get` result. `build` and `fleet` are null when unknown. */
239
243
  export type PluginContextInfo = {
240
244
  accountId: string;
241
245
  machineId: string;
246
+ projects: PluginProjectContext[];
247
+ panes: PluginPaneContext[];
248
+ /** False when local records could not be included or decoded. */
249
+ complete: boolean;
242
250
  build?: BuildInfo | null;
243
251
  fleet?: FleetPolicy | null;
244
252
  };
253
+ /** Replaces the local pane/project context after an account change. */
254
+ export type PaneContextEventData = Pick<PluginContextInfo, 'machineId' | 'projects' | 'panes' | 'complete'>;
255
+ export interface PaneContextEvent extends PluginEvent {
256
+ name: 'pane-context';
257
+ data: PaneContextEventData;
258
+ }
245
259
  /** The `data` of a `build-context` event. */
246
260
  export type BuildContextEventData = {
247
261
  build: BuildInfo | null;
@@ -275,6 +289,7 @@ export interface OperationMap {
275
289
  'config.get': { input: Record<string, never>; output: Record<string, Json> };
276
290
  'state.get': { input: { key: string }; output: Json };
277
291
  'state.set': { input: { key: string; value: Json }; output: null };
292
+ 'state.keys': { input: Record<string, never>; output: string[] };
278
293
  'context.get': { input: Record<string, never>; output: PluginContextInfo };
279
294
  'popover.open': { input: Popover; output: { choiceId?: string; values?: Record<string, Json>; confirmationId?: string } | null };
280
295
  'canvas.write': { input: { key: ContributionKey; ansi: string }; output: null };
@@ -331,6 +346,7 @@ export interface PluginContext {
331
346
  key(id: string, handler: ActionHandler): KeyRegistration;
332
347
  link(id: string, options: LinkOptions, handler: LinkHandler): Subscription;
333
348
  link(id: string, handler: LinkHandler): Subscription;
349
+ onEvent(name: 'pane-context', handler: (event: PaneContextEvent, context: HandlerContext) => void | Promise<void>, condition?: Condition): Subscription;
334
350
  onEvent(name: 'build-context', handler: (event: BuildContextEvent, context: HandlerContext) => void | Promise<void>, condition?: Condition): Subscription;
335
351
  onEvent(name: string, handler: (event: PluginEvent, context: HandlerContext) => void | Promise<void>, condition?: Condition): Subscription;
336
352
  onHook(name: string, handler: (event: HookEvent, context: HandlerContext) => HookResult | Promise<HookResult>): Subscription;
@@ -357,8 +373,8 @@ export interface PluginContext {
357
373
  fetch(args: OperationMap['fetch']['input'], options?: RequestOptions): Promise<OperationMap['fetch']['output']>;
358
374
  secrets: { get(name: string, options?: RequestOptions): Promise<string | null> };
359
375
  config: { get(options?: RequestOptions): Promise<Record<string, Json>> };
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. */
361
- state: { get(key: string, options?: RequestOptions): Promise<Json>; set(key: string, value: Json, options?: RequestOptions): Promise<null> };
376
+ /** 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. `keys()` returns the sorted key names. */
377
+ state: { get(key: string, options?: RequestOptions): Promise<Json>; set(key: string, value: Json, options?: RequestOptions): Promise<null>; keys(options?: RequestOptions): Promise<string[]> };
362
378
  context: { get(options?: RequestOptions): Promise<PluginContextInfo> };
363
379
  popover: { open(args: Popover, options?: RequestOptions): Promise<OperationMap['popover.open']['output']> };
364
380
  health: { set(args: OperationMap['health.set']['input'], options?: RequestOptions): Promise<null> };
package/src/protocol.mjs CHANGED
@@ -8,7 +8,7 @@ export const OPERATIONS = Object.freeze({
8
8
  'pane.input': 'panes', 'pane.focus': 'panes', 'pane.wait': 'panes',
9
9
  'project.create': 'projects', 'project.remove': 'projects',
10
10
  'notification.show': 'notifications', 'url.open': 'url', fetch: 'fetch',
11
- 'secret.get': 'secrets', 'config.get': null, 'state.get': null, 'state.set': null,
11
+ 'secret.get': 'secrets', 'config.get': null, 'state.get': null, 'state.set': null, 'state.keys': null,
12
12
  'context.get': null, 'popover.open': 'surfaces', 'canvas.write': 'surfaces',
13
13
  'canvas.focus': 'surfaces', 'subscription.add': null, 'subscription.remove': null,
14
14
  'health.set': null, 'webhook.ack': 'webhook',
package/src/runtime.mjs CHANGED
@@ -12,9 +12,39 @@ function boundedText(value, label, limit = 512) {
12
12
  }
13
13
  const HOVER_FORBIDDEN = /[\u0000-\u001f\u007f-\u009f\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069]/
14
14
  function validateCanvasHover(value) {
15
- ensure(typeof value === 'string' && value.trim().length > 0 && !HOVER_FORBIDDEN.test(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),
16
46
  'invalid_payload', 'Invalid canvas hover text')
17
- ensure(Buffer.byteLength(value) <= LIMITS.canvasHoverBytes,
47
+ ensure(Buffer.byteLength(text) <= LIMITS.canvasHoverBytes,
18
48
  'payload_too_large', `Canvas hover text exceeds ${LIMITS.canvasHoverBytes} bytes`)
19
49
  }
20
50
  function validateEntity(entity) {
@@ -342,7 +372,8 @@ export function createRuntime({ manifest: input, producer, send, clock = realClo
342
372
  secrets: Object.freeze({ get: (name, options) => request('secret.get', { name }, options) }),
343
373
  config: Object.freeze({ get: options => request('config.get', {}, options) }),
344
374
  state: Object.freeze({ get: (key, options) => request('state.get', { key }, options),
345
- set: (key, value, options) => request('state.set', { key, value }, options) }),
375
+ set: (key, value, options) => request('state.set', { key, value }, options),
376
+ keys: options => request('state.keys', {}, options) }),
346
377
  context: Object.freeze({ get: options => request('context.get', {}, options) }),
347
378
  popover: Object.freeze({ open: method('popover.open') }),
348
379
  health: Object.freeze({ set: method('health.set') }),
package/src/testing.mjs CHANGED
@@ -43,8 +43,9 @@ export function createHarness({ manifest: input, machineId = 'test-machine', epo
43
43
  ensure(accountState.has(args.key) || accountState.size < LIMITS.stateKeys, 'queue_full', 'Harness state holds 256 keys for this plugin')
44
44
  jsonBytes(args.value, LIMITS.stateValueBytes)
45
45
  accountState.set(args.key, structuredClone(args.value)); return null
46
+ case 'state.keys': return [...accountState.keys()].sort()
46
47
  case 'config.get': return {}
47
- case 'context.get': return { accountId: 'test-account', machineId, build: null, fleet: null }
48
+ case 'context.get': return { accountId: 'test-account', machineId, build: null, fleet: null, projects: [], panes: [], complete: true }
48
49
  case 'health.set': return null
49
50
  default: throw new PluginError('missing_fixture', `Provide a harness handler for ${operation.op}`)
50
51
  }