Files
orca/src/shared/plugins/events/plugin-events.ts
T
Neil 7b41b92062 feat(plugins): host API 1.7 — workspace ids, project/terminal list, terminal focus/close
Plugins that track workspaces and terminals across Orca had to read the private runtime
token or shell out to the CLI. Host API 1.7 gives them the same through consented methods:

- workspace.readContext (and workspace.changed) carry `workspaceId`, the id workspace.list
  gives; workspace.created/removed and agent.status.changed add `workspaceListId` beside
  their existing raw `workspaceId`, which keeps its meaning.
- workspace.list rows name their `projectId`; opt-in `includeStatus` joins each row's agent
  state, unread flag, live terminal count and last activity from worktree.ps.
- project.list (workspace:read), terminal.list (workspace:read), terminal.focus
  (workspace:write, desktop only, steers only this window) and terminal.close
  (terminal:send, reports whether the process is known to have stopped).
- Runtime refusals (unknown or ambiguous workspace, stale or exited terminal) reach plugins
  as fixed messages instead of runtime wording.
- Consent copy for workspace:read, workspace:write and terminal:send says what they now
  allow; terminal.focus and terminal.close are audited by terminal id.
- PLUGIN_SCHEMA_NEWEST_SINCE covers the editor typography and theme video floors it missed,
  and the provisional 1.7 floor, so development builds accept plugins requiring them.
2026-11-12 02:56:40 -08:00

113 lines
5.2 KiB
TypeScript

import { z } from 'zod'
import { AGENT_STATUS_STATES } from '../../agent-status/contracts/agent-status-types'
import { AGENT_TURN_OUTCOMES } from '../../agent-status/lifecycle/agent-turn-outcome'
import type { PluginEventName } from '../manifest/plugin-manifest'
import {
PLUGIN_TERMINAL_ID_MAX_LENGTH,
PLUGIN_WORKSPACE_LABEL_MAX_LENGTH,
pluginWorkspaceContextSchema
} from '../workers/plugin-host-api'
import { pluginEditorTypographySchema } from '../workers/plugin-host-api-editor-typography'
import { pluginWorkspaceIdSchema } from '../workers/plugin-host-api-workspace-methods'
/**
* Payload contracts for the v0 plugin event set (workspace lifecycle + agent
* status only). Payloads are bounded projections — never raw runtime
* objects — so nothing sensitive (absolute repo paths beyond the worktree's
* own, remotes, credentials) can leak through the event stream.
*/
/** Plugins see `main`, not the `refs/heads/main` Orca tracks internally. */
export function shortPluginBranchName(branch: string): string {
return branch.replace(/^refs\/heads\//, '')
}
/** The id `workspace.list` gives the workspace, which every workspace method takes; `workspaceId`
* is Orca's raw id, which they take too but which differs from it for most worktrees. Absent
* before host API 1.7. */
const workspaceListId = pluginWorkspaceIdSchema.optional()
const workspaceIdentity = {
workspaceId: z.string().min(1).max(2048),
workspaceListId,
/** Folder workspaces have no branch; only worktrees emit lifecycle events today. */
kind: z.enum(['worktree', 'folder'])
}
export const workspaceCreatedPayloadSchema = z.object({
...workspaceIdentity,
path: z
.string()
.min(1)
.max(32 * 1024),
branch: z.string().max(1024)
})
export const workspaceRemovedPayloadSchema = z.object({
...workspaceIdentity,
path: z
.string()
.min(1)
.max(32 * 1024)
})
export const agentStatusChangedPayloadSchema = z.object({
workspaceId: z.string().min(1).max(2048).nullable(),
/** Absent too while `workspaceId` is null. */
workspaceListId,
paneKey: z.string().min(1).max(2048),
/** The pane's terminal, as `workspace.readContext` and `terminal.sendText` name it; absent when
* Orca cannot tie the pane to a terminal it lists. */
terminalId: z.string().min(1).max(PLUGIN_TERMINAL_ID_MAX_LENGTH).optional(),
/** The pane's name, or its terminal title. */
title: z.string().max(PLUGIN_WORKSPACE_LABEL_MAX_LENGTH).optional(),
/** The combined status the user sees: `working` while the main agent or any live child works. */
state: z.enum(AGENT_STATUS_STATES),
receivedAt: z.number().finite().positive(),
/** Distinguish the main agent from combined child activity; absent on older hosts or without main-agent evidence. State keeps its meaning. */
mainAgent: z
.object({
state: z.enum(AGENT_STATUS_STATES),
/** The verdict on the main agent's last finished turn, the provider's or else the host-observed end
* (`interruption`, `unconfirmed`); present only while `state` is done. */
outcome: z.enum(AGENT_TURN_OUTCOMES).optional(),
// Why: the same bound the row normalizer applies; a stricter one here would reject the whole event.
/** Stamped by the host the agent runs on (an SSH host's own clock), unlike `receivedAt`. */
stateStartedAt: z.number().finite()
})
.optional()
})
export const workspaceChangedPayloadSchema = z.object({
/** What `workspace.readContext` answers now; null when no workspace is focused. */
context: pluginWorkspaceContextSchema
})
export const PLUGIN_EVENT_PAYLOAD_SCHEMAS = {
'workspace.created': workspaceCreatedPayloadSchema,
'workspace.removed': workspaceRemovedPayloadSchema,
'agent.status.changed': agentStatusChangedPayloadSchema,
'workspace.changed': workspaceChangedPayloadSchema,
'editor.typography.changed': pluginEditorTypographySchema
} satisfies Record<PluginEventName, z.ZodType>
/** One line per event for the generated reference and author typings. */
export const PLUGIN_EVENT_SUMMARIES: Record<PluginEventName, string> = {
'workspace.created':
'A worktree workspace was created; `workspaceListId` (host API 1.7) is the id `workspace.list` gives it.',
'workspace.removed':
'A worktree workspace was removed; `workspaceListId` (host API 1.7) is the id `workspace.list` gave it.',
'agent.status.changed':
'An agent in a terminal pane changed status; `workspaceId` is null when Orca cannot tie the pane to a workspace, `workspaceListId` (host API 1.7) is the id `workspace.list` gives that workspace, and `terminalId` names its terminal when Orca lists it.',
'workspace.changed':
'The focused workspace changed, or its terminals were opened, closed or renamed; `context` is what `workspace.readContext` now answers, its `workspaceId` included. Each new subscriber first gets the current context.',
'editor.typography.changed':
"What Orca's file editor uses changed (a setting or the editor zoom); the payload is what `editor.readTypography` now answers. Needs no capability."
}
export type PluginEventPayload<E extends PluginEventName> = z.infer<
(typeof PLUGIN_EVENT_PAYLOAD_SCHEMAS)[E]
>
export type PluginAgentStatusChangedPayload = z.infer<typeof agentStatusChangedPayloadSchema>