mirror of
https://github.com/stablyai/orca.git
synced 2026-11-15 18:37:24 +00:00
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.
113 lines
4.0 KiB
TypeScript
113 lines
4.0 KiB
TypeScript
import type { z } from 'zod'
|
|
import { shortPluginBranchName } from '../../../../shared/plugins/events/plugin-events'
|
|
import {
|
|
PLUGIN_TERMINAL_ID_MAX_LENGTH,
|
|
PLUGIN_WORKSPACE_LABEL_MAX_LENGTH,
|
|
PLUGIN_WORKSPACE_TERMINAL_LIMIT,
|
|
type pluginWorkspaceContextSchema
|
|
} from '../../../../shared/plugins/workers/plugin-host-api'
|
|
import { PLUGIN_AGENT_LIST_LIMIT } from '../../../../shared/plugins/workers/plugin-host-api-panel-methods'
|
|
import {
|
|
PLUGIN_HOST_ID_MAX_LENGTH,
|
|
pluginWorkspaceId
|
|
} from '../../../../shared/plugins/workers/plugin-host-api-workspace-methods'
|
|
import type { AgentStatusState } from '../../../../shared/agent-status/contracts/agent-status-types'
|
|
|
|
/** One terminal of a workspace, as main's runtime lists it. */
|
|
export type PluginWorkspaceTerminal = {
|
|
/** The terminal handle plugins call `terminalId`. */
|
|
id: string
|
|
title?: string
|
|
/** `tabId:leafId`, which agent statuses carry. */
|
|
paneKey?: string
|
|
}
|
|
|
|
/** One agent row from the hook server's status store (the fields plugins may see). */
|
|
export type PluginAgentStatusRow = {
|
|
paneKey: string
|
|
worktreeId?: string
|
|
terminalHandle?: string
|
|
state: AgentStatusState
|
|
agentType?: string
|
|
stateStartedAt: number
|
|
providerSessionOnly?: boolean
|
|
}
|
|
|
|
export type PluginWorkspaceContext = z.output<typeof pluginWorkspaceContextSchema>
|
|
|
|
type Context = { worktreeId: string; identityKey?: string; branch: string; displayName: string }
|
|
|
|
/** A pane's own name wins over the title its program sets. */
|
|
export function pluginTerminalTitle(terminal: {
|
|
title: string | null
|
|
paneTitle?: string
|
|
}): string | undefined {
|
|
const title = (terminal.paneTitle ?? terminal.title ?? '').trim()
|
|
return title ? title.slice(0, PLUGIN_WORKSPACE_LABEL_MAX_LENGTH) : undefined
|
|
}
|
|
|
|
/** The id `workspace.list` gives, or undefined when it exceeds the public bound. */
|
|
export function pluginListedWorkspaceId(row: {
|
|
id: string
|
|
identityKey?: string | null
|
|
}): string | undefined {
|
|
const id = pluginWorkspaceId(row)
|
|
return id.length <= PLUGIN_HOST_ID_MAX_LENGTH ? id : undefined
|
|
}
|
|
|
|
/** Only these public fields reach plugins; the id is the one `workspace.list` gives. */
|
|
export function projectPluginWorkspaceContext(
|
|
context: Context | null,
|
|
terminals: readonly PluginWorkspaceTerminal[]
|
|
): PluginWorkspaceContext {
|
|
if (!context) {
|
|
return null
|
|
}
|
|
const workspaceId = pluginListedWorkspaceId({
|
|
id: context.worktreeId,
|
|
identityKey: context.identityKey
|
|
})
|
|
return {
|
|
...(workspaceId ? { workspaceId } : {}),
|
|
branch: shortPluginBranchName(context.branch).slice(0, PLUGIN_WORKSPACE_LABEL_MAX_LENGTH),
|
|
displayName: context.displayName.slice(0, PLUGIN_WORKSPACE_LABEL_MAX_LENGTH),
|
|
terminals: terminals
|
|
.filter(({ id }) => id.length > 0 && id.length <= PLUGIN_TERMINAL_ID_MAX_LENGTH)
|
|
.slice(0, PLUGIN_WORKSPACE_TERMINAL_LIMIT)
|
|
.map(({ id, title }) => (title ? { id, title } : { id }))
|
|
}
|
|
}
|
|
|
|
/** The terminal an agent's pane is: by the handle its status names, else by pane key. */
|
|
export function terminalForPane(
|
|
terminals: readonly PluginWorkspaceTerminal[],
|
|
row: Pick<PluginAgentStatusRow, 'paneKey' | 'terminalHandle'>
|
|
): PluginWorkspaceTerminal | undefined {
|
|
return (
|
|
terminals.find((terminal) => terminal.id === row.terminalHandle) ??
|
|
terminals.find((terminal) => terminal.paneKey === row.paneKey)
|
|
)
|
|
}
|
|
|
|
/** `agents.list`: the focused workspace's agents, newest state first. */
|
|
export function projectPluginWorkspaceAgents(
|
|
worktreeId: string,
|
|
terminals: readonly PluginWorkspaceTerminal[],
|
|
rows: readonly PluginAgentStatusRow[]
|
|
) {
|
|
return rows
|
|
.filter((row) => row.worktreeId === worktreeId && row.providerSessionOnly !== true)
|
|
.toSorted((a, b) => b.stateStartedAt - a.stateStartedAt)
|
|
.slice(0, PLUGIN_AGENT_LIST_LIMIT)
|
|
.map((row) => {
|
|
const terminal = terminalForPane(terminals, row)
|
|
return {
|
|
terminalId: terminal?.id ?? null,
|
|
title: terminal?.title ?? null,
|
|
agent: row.agentType?.slice(0, 128) ?? null,
|
|
state: row.state,
|
|
stateStartedAt: row.stateStartedAt
|
|
}
|
|
})
|
|
}
|