@av-pi-studio/server 0.0.93 → 0.0.94

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.
@@ -1,5 +1,5 @@
1
- import type { AgentCapabilityFlags, AgentSessionConfig } from "@av-pi-studio/protocol";
2
- import type { AgentClient, AgentModeDefinition, AgentModelDefinition, AgentSession, CreateSessionOptions, LaunchContext, PersistenceHandle } from "../../provider-contract.js";
1
+ import type { AgentCapabilityFlags, AgentSessionConfig, AgentStreamEvent } from "@av-pi-studio/protocol";
2
+ import type { AgentClient, AgentCommandDefinition, AgentModeDefinition, AgentModelDefinition, AgentSession, CreateSessionOptions, LaunchContext, PendingPermission, PersistenceHandle, ProviderRuntimeInfo, ProviderUiRequest, ProviderUiResponse, Unsubscribe } from "../../provider-contract.js";
3
3
  /**
4
4
  * In-process `mock` provider (features/agent-providers.md § Provider entry — dev/test only). It
5
5
  * implements the `AgentClient`/`AgentSession` contracts in memory and emits a scripted turn. Never
@@ -10,6 +10,124 @@ export interface MockSessionOptions {
10
10
  /** Delay before a started turn completes (ms). Small but non-zero so `interrupt` can win. */
11
11
  turnDelayMs?: number;
12
12
  }
13
+ /** Exported so tests (this file's, and downstream sprint-066 task-003/004 daemon-level tests) can
14
+ * cast a created session to reach `emitUiRequest`/`uiResponses`, which are deliberately not part
15
+ * of the provider-neutral `AgentSession` contract. */
16
+ export declare class MockAgentSession implements AgentSession {
17
+ private readonly config;
18
+ readonly provider = "mock";
19
+ readonly id: `${string}-${string}-${string}-${string}-${string}`;
20
+ readonly capabilities: import("zod").objectOutputType<{
21
+ supportsStreaming: import("zod").ZodBoolean;
22
+ supportsSessionPersistence: import("zod").ZodBoolean;
23
+ supportsDynamicModes: import("zod").ZodBoolean;
24
+ supportsMcpServers: import("zod").ZodBoolean;
25
+ supportsReasoningStream: import("zod").ZodBoolean;
26
+ supportsToolInvocations: import("zod").ZodBoolean;
27
+ supportsRewindConversation: import("zod").ZodOptional<import("zod").ZodBoolean>;
28
+ supportsRewindFiles: import("zod").ZodOptional<import("zod").ZodBoolean>;
29
+ supportsRewindBoth: import("zod").ZodOptional<import("zod").ZodBoolean>;
30
+ supportsSteering: import("zod").ZodOptional<import("zod").ZodBoolean>;
31
+ supportsExtensionUi: import("zod").ZodOptional<import("zod").ZodBoolean>;
32
+ }, import("zod").ZodTypeAny, "passthrough">;
33
+ private readonly subscribers;
34
+ private readonly history;
35
+ private readonly turnDelayMs;
36
+ private activeTurn;
37
+ private completionTimer;
38
+ private mode;
39
+ private closed;
40
+ private readonly uiSubscribers;
41
+ readonly uiResponses: {
42
+ providerRequestId: string;
43
+ response: ProviderUiResponse;
44
+ }[];
45
+ private readonly pendingScriptedResponses;
46
+ constructor(config: AgentSessionConfig, options?: MockSessionOptions);
47
+ private emit;
48
+ subscribe(cb: (event: AgentStreamEvent) => void): Unsubscribe;
49
+ streamHistory(): AsyncGenerator<AgentStreamEvent>;
50
+ startTurn(prompt: string): Promise<{
51
+ turnId: string;
52
+ }>;
53
+ /** `#ui ...` prompt (task-001): raise the scripted dialog(s) through the same `uiSubscribers`
54
+ * channel `emitUiRequest` uses, instead of the normal echoed turn. Resolves immediately with the
55
+ * turn id, exactly like the normal path — the dialog(s) and the eventual echo/`turn_completed`
56
+ * happen asynchronously as `runUiScript` progresses. */
57
+ private startUiScriptTurn;
58
+ private runUiScript;
59
+ /** Raises one scripted step. A dialog (`expectsResponse: true`) returns a promise that resolves
60
+ * once `respondToUi` is called for its (mock-minted) `requestId`. A transient
61
+ * (`expectsResponse: false`, e.g. `notify`) resolves immediately with `null` — it is never
62
+ * answered, so waiting on `pendingScriptedResponses` for one would hang `runUiScript`'s
63
+ * `Promise.all` forever. Deliberately separate from the public `emitUiRequest` — that method
64
+ * fills defaults for ad-hoc test use and tracks nothing. */
65
+ private raiseScriptedDialog;
66
+ /** Convenience: start a turn and resolve when it reaches a terminal event. */
67
+ run(prompt: string): Promise<void>;
68
+ interrupt(): Promise<void>;
69
+ private readonly steeringQueue;
70
+ private readonly followUpQueue;
71
+ steer(message: string): Promise<void>;
72
+ followUp(message: string): Promise<void>;
73
+ getRuntimeInfo(): ProviderRuntimeInfo;
74
+ getAvailableModes(): AgentModeDefinition[];
75
+ getCurrentMode(): string | null;
76
+ setMode(id: string): Promise<void>;
77
+ getPendingPermissions(): PendingPermission[];
78
+ respondToPermission(): Promise<void>;
79
+ onUiRequest(cb: (req: ProviderUiRequest) => void): Unsubscribe;
80
+ respondToUi(providerRequestId: string, response: ProviderUiResponse): void;
81
+ /** Test-only: push a scripted UI request to every subscriber, with sensible defaults for any
82
+ * field the caller omits. Returns the request actually emitted (its `requestId` in particular),
83
+ * so a test can answer it via `respondToUi` or assert against it directly. */
84
+ emitUiRequest(partial?: Partial<ProviderUiRequest>): ProviderUiRequest;
85
+ describePersistence(): PersistenceHandle | null;
86
+ private sessionName;
87
+ getSessionStats(): Promise<{
88
+ sessionId: string;
89
+ totalMessages: number;
90
+ tokens: {
91
+ total: number;
92
+ };
93
+ }>;
94
+ compact(): Promise<{
95
+ summary: string;
96
+ firstKeptEntryId: string;
97
+ tokensBefore: number;
98
+ }>;
99
+ newSession(): Promise<{
100
+ cancelled: boolean;
101
+ }>;
102
+ switchSession(): Promise<{
103
+ cancelled: boolean;
104
+ }>;
105
+ fork(entryId: string): Promise<{
106
+ text: string;
107
+ cancelled: boolean;
108
+ }>;
109
+ getForkMessages(): Promise<{
110
+ entryId: string;
111
+ text: string;
112
+ }[]>;
113
+ clone(): Promise<{
114
+ cancelled: boolean;
115
+ }>;
116
+ setSessionName(name: string): Promise<void>;
117
+ cycleModel(): Promise<{
118
+ model: {
119
+ id: string;
120
+ };
121
+ thinkingLevel: string;
122
+ }>;
123
+ getLastAssistantText(): Promise<string | null>;
124
+ /** Command discovery (sprint-040): a deterministic, dependency-free multi-source list — one
125
+ * each of extension/prompt/skill — covering `agent_list_commands_request` without needing a
126
+ * real `pi` binary. */
127
+ listCommands(): Promise<AgentCommandDefinition[]>;
128
+ close(): Promise<void>;
129
+ isClosed(): boolean;
130
+ }
13
131
  export declare class MockAgentClient implements AgentClient {
14
132
  private readonly options;
15
133
  readonly provider = "mock";
@@ -24,6 +142,7 @@ export declare class MockAgentClient implements AgentClient {
24
142
  supportsRewindFiles: import("zod").ZodOptional<import("zod").ZodBoolean>;
25
143
  supportsRewindBoth: import("zod").ZodOptional<import("zod").ZodBoolean>;
26
144
  supportsSteering: import("zod").ZodOptional<import("zod").ZodBoolean>;
145
+ supportsExtensionUi: import("zod").ZodOptional<import("zod").ZodBoolean>;
27
146
  }, import("zod").ZodTypeAny, "passthrough">;
28
147
  constructor(options?: MockSessionOptions);
29
148
  createSession(config: AgentSessionConfig, _launchContext?: LaunchContext, _options?: CreateSessionOptions): Promise<AgentSession>;
@@ -1,4 +1,5 @@
1
1
  import { randomUUID } from "node:crypto";
2
+ import { getUiScriptHelpText, parseUiScript } from "./ui-script.js";
2
3
  /**
3
4
  * In-process `mock` provider (features/agent-providers.md § Provider entry — dev/test only). It
4
5
  * implements the `AgentClient`/`AgentSession` contracts in memory and emits a scripted turn. Never
@@ -12,12 +13,16 @@ export const MOCK_CAPABILITIES = {
12
13
  supportsReasoningStream: true,
13
14
  supportsToolInvocations: true,
14
15
  supportsSteering: true,
16
+ supportsExtensionUi: true,
15
17
  };
16
18
  const MOCK_MODES = [
17
19
  { id: "default", label: "Default" },
18
20
  { id: "plan", label: "Plan" },
19
21
  ];
20
- class MockAgentSession {
22
+ /** Exported so tests (this file's, and downstream sprint-066 task-003/004 daemon-level tests) can
23
+ * cast a created session to reach `emitUiRequest`/`uiResponses`, which are deliberately not part
24
+ * of the provider-neutral `AgentSession` contract. */
25
+ export class MockAgentSession {
21
26
  config;
22
27
  provider = "mock";
23
28
  id = randomUUID();
@@ -29,6 +34,16 @@ class MockAgentSession {
29
34
  completionTimer = null;
30
35
  mode = "default";
31
36
  closed = false;
37
+ // Extension UI (features/extension-ui-rpc.md, sprint-066): no `pi` process exists to script this
38
+ // family, so the mock provider exposes a scripted emitter (`emitUiRequest`) plus a recorder for
39
+ // `respondToUi` calls, letting tasks 003-004 drive/assert the whole family without a child process.
40
+ uiSubscribers = new Set();
41
+ uiResponses = [];
42
+ // #ui script support (sprint-068/task-001): a scripted dialog raised via `startTurn` is tracked
43
+ // here so `respondToUi` can resolve the promise `runUiScript` is awaiting, in addition to (not
44
+ // instead of) its existing `uiResponses` recording used by direct `emitUiRequest`/`respondToUi`
45
+ // callers.
46
+ pendingScriptedResponses = new Map();
32
47
  constructor(config, options = {}) {
33
48
  this.config = config;
34
49
  this.turnDelayMs = options.turnDelayMs ?? 5;
@@ -47,6 +62,9 @@ class MockAgentSession {
47
62
  yield event;
48
63
  }
49
64
  startTurn(prompt) {
65
+ const script = parseUiScript(prompt);
66
+ if (script !== null)
67
+ return this.startUiScriptTurn(script);
50
68
  const turnId = randomUUID();
51
69
  this.activeTurn = turnId;
52
70
  this.emit({ kind: "turn_started", turnId });
@@ -65,6 +83,75 @@ class MockAgentSession {
65
83
  }, this.turnDelayMs);
66
84
  return Promise.resolve({ turnId });
67
85
  }
86
+ /** `#ui ...` prompt (task-001): raise the scripted dialog(s) through the same `uiSubscribers`
87
+ * channel `emitUiRequest` uses, instead of the normal echoed turn. Resolves immediately with the
88
+ * turn id, exactly like the normal path — the dialog(s) and the eventual echo/`turn_completed`
89
+ * happen asynchronously as `runUiScript` progresses. */
90
+ startUiScriptTurn(steps) {
91
+ const turnId = randomUUID();
92
+ this.activeTurn = turnId;
93
+ this.emit({ kind: "turn_started", turnId });
94
+ if (steps.length === 0) {
95
+ // `#ui help` — no dialog, just the recipe list as assistant text.
96
+ setTimeout(() => {
97
+ if (this.activeTurn !== turnId)
98
+ return;
99
+ this.emit({
100
+ kind: "assistant_message",
101
+ messageId: randomUUID(),
102
+ text: getUiScriptHelpText(),
103
+ final: true,
104
+ });
105
+ this.emit({ kind: "turn_completed", turnId });
106
+ this.activeTurn = null;
107
+ }, this.turnDelayMs);
108
+ return Promise.resolve({ turnId });
109
+ }
110
+ void this.runUiScript(turnId, steps);
111
+ return Promise.resolve({ turnId });
112
+ }
113
+ async runUiScript(turnId, steps) {
114
+ const responses = await Promise.all(steps.map((step) => this.raiseScriptedDialog(step)));
115
+ if (this.activeTurn !== turnId)
116
+ return; // interrupted while dialogs were pending
117
+ for (let i = 0; i < steps.length; i++) {
118
+ const response = responses[i];
119
+ // `null`: a transient step (e.g. `notify`) — fire-and-forget, never resolved, nothing to
120
+ // echo. Only a real answered dialog gets an "ui X resolved: …" line.
121
+ if (response === null || response === undefined)
122
+ continue;
123
+ this.emit({
124
+ kind: "assistant_message",
125
+ messageId: randomUUID(),
126
+ text: `ui ${steps[i].method} resolved: ${describeUiResponse(response)}`,
127
+ final: true,
128
+ });
129
+ }
130
+ this.emit({ kind: "turn_completed", turnId });
131
+ this.activeTurn = null;
132
+ }
133
+ /** Raises one scripted step. A dialog (`expectsResponse: true`) returns a promise that resolves
134
+ * once `respondToUi` is called for its (mock-minted) `requestId`. A transient
135
+ * (`expectsResponse: false`, e.g. `notify`) resolves immediately with `null` — it is never
136
+ * answered, so waiting on `pendingScriptedResponses` for one would hang `runUiScript`'s
137
+ * `Promise.all` forever. Deliberately separate from the public `emitUiRequest` — that method
138
+ * fills defaults for ad-hoc test use and tracks nothing. */
139
+ raiseScriptedDialog(step) {
140
+ const req = {
141
+ requestId: randomUUID(),
142
+ method: step.method,
143
+ expectsResponse: step.expectsResponse,
144
+ payload: step.payload,
145
+ ...(step.timeoutMs !== undefined ? { timeoutMs: step.timeoutMs } : {}),
146
+ };
147
+ for (const cb of this.uiSubscribers)
148
+ cb(req);
149
+ if (!step.expectsResponse)
150
+ return Promise.resolve(null);
151
+ const { promise, resolve } = Promise.withResolvers();
152
+ this.pendingScriptedResponses.set(req.requestId, resolve);
153
+ return promise;
154
+ }
68
155
  /** Convenience: start a turn and resolve when it reaches a terminal event. */
69
156
  run(prompt) {
70
157
  return new Promise((resolve) => {
@@ -137,6 +224,35 @@ class MockAgentSession {
137
224
  respondToPermission() {
138
225
  return Promise.resolve();
139
226
  }
227
+ onUiRequest(cb) {
228
+ this.uiSubscribers.add(cb);
229
+ return () => this.uiSubscribers.delete(cb);
230
+ }
231
+ respondToUi(providerRequestId, response) {
232
+ this.uiResponses.push({ providerRequestId, response });
233
+ const resolve = this.pendingScriptedResponses.get(providerRequestId);
234
+ if (resolve) {
235
+ this.pendingScriptedResponses.delete(providerRequestId);
236
+ resolve(response);
237
+ }
238
+ }
239
+ /** Test-only: push a scripted UI request to every subscriber, with sensible defaults for any
240
+ * field the caller omits. Returns the request actually emitted (its `requestId` in particular),
241
+ * so a test can answer it via `respondToUi` or assert against it directly. */
242
+ emitUiRequest(partial = {}) {
243
+ const req = {
244
+ requestId: partial.requestId ?? randomUUID(),
245
+ method: partial.method ?? "confirm",
246
+ expectsResponse: partial.expectsResponse ?? true,
247
+ payload: partial.payload ?? {},
248
+ ...(partial.surfaceKey !== undefined ? { surfaceKey: partial.surfaceKey } : {}),
249
+ ...(partial.removed !== undefined ? { removed: partial.removed } : {}),
250
+ ...(partial.timeoutMs !== undefined ? { timeoutMs: partial.timeoutMs } : {}),
251
+ };
252
+ for (const cb of this.uiSubscribers)
253
+ cb(req);
254
+ return req;
255
+ }
140
256
  describePersistence() {
141
257
  return { provider: this.provider, sessionId: this.id, nativeHandle: `mock:${this.id}` };
142
258
  }
@@ -268,4 +384,17 @@ export class MockAgentClient {
268
384
  export function createMockClient(options) {
269
385
  return new MockAgentClient(options);
270
386
  }
387
+ /** Renders a scripted dialog's answer as short, human-readable assistant text (task-001: "the mock
388
+ * echoes what it received"). Dev/test-only tooling — unlike the web-client's presentation rules
389
+ * (sprint-068/task-004), this deliberately may name a typed value, since it exists to prove the
390
+ * round trip during manual verification. */
391
+ function describeUiResponse(response) {
392
+ if (response.cancelled)
393
+ return "cancelled: true";
394
+ if (response.confirmed !== undefined)
395
+ return `confirmed: ${response.confirmed}`;
396
+ if (response.value !== undefined)
397
+ return `value: ${JSON.stringify(response.value)}`;
398
+ return "no answer";
399
+ }
271
400
  //# sourceMappingURL=mock-provider.js.map
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Scripted UI-dialog trigger for the mock provider (sprint-068/task-001). Lets a browser-connected
3
+ * client raise every `agent_ui_*` dialog state by typing a `#ui ...` prompt, so the extension-UI
4
+ * dialog rendering shipped in this sprint can be visually signed off against a running dev daemon
5
+ * with no real `pi` process and no real interactive extension.
6
+ *
7
+ * `parseUiScript` is a **pure** parser with a cheap prefix check: it returns `null` for the
8
+ * overwhelmingly common case (an ordinary prompt, including one that merely mentions `#ui` later in
9
+ * the text), and a list of `UiScriptStep`s to raise for a recognised `#ui ...` prompt. Wiring the
10
+ * result into a turn — raising the dialogs, waiting for answers, echoing them as assistant text — is
11
+ * `MockAgentSession`'s job, not this module's; this module only decides *what* to raise.
12
+ *
13
+ * Grammar (also echoed verbatim by `#ui help`):
14
+ *
15
+ * #ui select one dialog, two short options (Allow / Block)
16
+ * #ui confirm title + message
17
+ * #ui input single-line field with a placeholder
18
+ * #ui editor multi-line field, prefilled
19
+ * #ui unknown a method Pi has never defined, still answerable (Cancel only)
20
+ * #ui select:9 nine options — past the § 12 stacking+scroll threshold
21
+ * #ui select:empty an empty `options` array
22
+ * #ui select:long self-numbered options, captured verbatim from a live run
23
+ * #ui input:multiline a title with a hard line break and a bracketed extension prefix
24
+ * #ui notify a transient info-level `notify` — no dialog, no answer, no echo
25
+ * #ui notify:warning a transient warning-level `notify`
26
+ * #ui notify:error a transient error-level `notify`
27
+ * #ui set_editor_text a transient `set_editor_text` — replaces the composer draft
28
+ * #ui <method> timeout=<s> adds a deadline in seconds (rejected for `editor` — see Notes)
29
+ *
30
+ * Notes: `editor` has no `timeout` field on Pi's real wire (the visual spec's § 00 wire table lists
31
+ * one in error — sprint-068/task-009 files the correction), so `#ui editor timeout=5` is rejected
32
+ * (parses to `null`, falling through to an ordinary echoed turn) rather than emit a field Pi could
33
+ * never actually send.
34
+ */
35
+ /** One dialog to raise. `payload` field names match the visual spec's § 00 wire table exactly. */
36
+ export interface UiScriptStep {
37
+ method: string;
38
+ payload: Record<string, unknown>;
39
+ expectsResponse: boolean;
40
+ timeoutMs?: number;
41
+ /** True: this dialog is raised and its answer is awaited before the turn can complete — the
42
+ * normal, single-dialog case. False: raised without individually waiting — used only by
43
+ * `#ui multi`, where every step is raised up front and the turn waits on all of them together.
44
+ * Unrelated to `expectsResponse: false` (a transient like `notify`) — those never produce an
45
+ * answer to await regardless of this field; see `mock-provider.ts`'s `raiseScriptedDialog`. */
46
+ await: boolean;
47
+ }
48
+ export declare function getUiScriptHelpText(): string;
49
+ /** Parses a `#ui ...` prompt into the dialog(s) it describes. Returns `null` when `prompt` is not a
50
+ * recognised script — the overwhelmingly common case, kept a cheap prefix check. */
51
+ export declare function parseUiScript(prompt: string): UiScriptStep[] | null;
52
+ //# sourceMappingURL=ui-script.d.ts.map
@@ -0,0 +1,224 @@
1
+ /**
2
+ * Scripted UI-dialog trigger for the mock provider (sprint-068/task-001). Lets a browser-connected
3
+ * client raise every `agent_ui_*` dialog state by typing a `#ui ...` prompt, so the extension-UI
4
+ * dialog rendering shipped in this sprint can be visually signed off against a running dev daemon
5
+ * with no real `pi` process and no real interactive extension.
6
+ *
7
+ * `parseUiScript` is a **pure** parser with a cheap prefix check: it returns `null` for the
8
+ * overwhelmingly common case (an ordinary prompt, including one that merely mentions `#ui` later in
9
+ * the text), and a list of `UiScriptStep`s to raise for a recognised `#ui ...` prompt. Wiring the
10
+ * result into a turn — raising the dialogs, waiting for answers, echoing them as assistant text — is
11
+ * `MockAgentSession`'s job, not this module's; this module only decides *what* to raise.
12
+ *
13
+ * Grammar (also echoed verbatim by `#ui help`):
14
+ *
15
+ * #ui select one dialog, two short options (Allow / Block)
16
+ * #ui confirm title + message
17
+ * #ui input single-line field with a placeholder
18
+ * #ui editor multi-line field, prefilled
19
+ * #ui unknown a method Pi has never defined, still answerable (Cancel only)
20
+ * #ui select:9 nine options — past the § 12 stacking+scroll threshold
21
+ * #ui select:empty an empty `options` array
22
+ * #ui select:long self-numbered options, captured verbatim from a live run
23
+ * #ui input:multiline a title with a hard line break and a bracketed extension prefix
24
+ * #ui notify a transient info-level `notify` — no dialog, no answer, no echo
25
+ * #ui notify:warning a transient warning-level `notify`
26
+ * #ui notify:error a transient error-level `notify`
27
+ * #ui set_editor_text a transient `set_editor_text` — replaces the composer draft
28
+ * #ui <method> timeout=<s> adds a deadline in seconds (rejected for `editor` — see Notes)
29
+ *
30
+ * Notes: `editor` has no `timeout` field on Pi's real wire (the visual spec's § 00 wire table lists
31
+ * one in error — sprint-068/task-009 files the correction), so `#ui editor timeout=5` is rejected
32
+ * (parses to `null`, falling through to an ordinary echoed turn) rather than emit a field Pi could
33
+ * never actually send.
34
+ */
35
+ const SELECT_ALLOW_BLOCK = {
36
+ title: "Allow this extension to modify /etc/hosts?",
37
+ options: ["Allow", "Block"],
38
+ };
39
+ const CONFIRM_CLEAR_SESSION = {
40
+ title: "Clear session?",
41
+ message: "All messages will be lost. The transcript can't be recovered afterwards.",
42
+ };
43
+ const INPUT_RELEASE_TAG = {
44
+ title: "Enter a release tag",
45
+ placeholder: "v2.4.1",
46
+ };
47
+ const EDITOR_COMMIT_MESSAGE = {
48
+ title: "Edit commit message before pushing",
49
+ prefill: "fix: retry dns lookups with backoff\n\nAdds exponential backoff to the connectivity\ncheck skill after repeated timeouts.",
50
+ };
51
+ // Verbatim from the visual spec § 05 (unrecognised-method card) — a method Pi has never defined.
52
+ const UNKNOWN_METHOD_PAYLOAD = {
53
+ title: "Select a window",
54
+ min: 0,
55
+ max: 240,
56
+ };
57
+ // Verbatim from § 12 "NINE OPTIONS · SCROLLS AT SIX".
58
+ const SELECT_NINE_OPTIONS = {
59
+ title: "Pick a target",
60
+ options: [
61
+ "staging-eu",
62
+ "staging-us",
63
+ "prod-eu",
64
+ "prod-us",
65
+ "prod-apac",
66
+ "canary-1",
67
+ "canary-2",
68
+ "local",
69
+ "dry-run",
70
+ ],
71
+ };
72
+ const SELECT_EMPTY_OPTIONS = {
73
+ title: "Pick a window",
74
+ options: [],
75
+ };
76
+ // Verbatim from § 03 "SELECT · STACKED, THE COMMON CASE" — a live capture: the extension numbered
77
+ // its own options and prefixed its own title. Never rewritten (§ 03/§ 12).
78
+ const SELECT_LONG_LABELS = {
79
+ title: "[Color] Which color do you pick?",
80
+ options: ["1. Red — Pick the color red.", "2. Blue — Pick the color blue.", "3. Type something."],
81
+ };
82
+ // Verbatim from § 03 "INPUT · HARD BREAK, BRACKETED PREFIX" — also a live capture.
83
+ const INPUT_MULTILINE = {
84
+ title: "[Color] Which color do you pick?\n\nType your answer:",
85
+ };
86
+ // § 00's wire table: `notify`'s `message` is required, `level` optional (absent ⇒ the client
87
+ // normalizes to "info" — `agent-ui-state.ts`'s `buildTransientEffects`). These three exercise all
88
+ // three levels § 11 treats distinctly.
89
+ const NOTIFY_INFO = {
90
+ message: "Sync complete.",
91
+ };
92
+ const NOTIFY_WARNING = {
93
+ message: "Rate limit approaching — 80% of quota used.",
94
+ level: "warning",
95
+ };
96
+ const NOTIFY_ERROR = {
97
+ message: "Failed to reach the remote index.",
98
+ level: "error",
99
+ };
100
+ // § 00's wire table: `set_editor_text` carries only `text` — captured verbatim from § 11's own
101
+ // mock ("REPLACEMENT LANDED").
102
+ const SET_EDITOR_TEXT = {
103
+ text: "retry the dns lookups with a 2s backoff",
104
+ };
105
+ const HELP_TEXT = `#ui script recipes:
106
+ #ui select one dialog, two short options (Allow / Block)
107
+ #ui confirm title + message
108
+ #ui input single-line field with a placeholder
109
+ #ui editor multi-line field, prefilled
110
+ #ui unknown a method Pi has never defined, still answerable (Cancel only)
111
+ #ui select:9 nine options — past the stacking+scroll threshold
112
+ #ui select:empty an empty options array
113
+ #ui select:long self-numbered options, captured verbatim from a live run
114
+ #ui input:multiline a title with a hard break and a bracketed extension prefix
115
+ #ui notify a transient info-level notify — no dialog, no answer, no echo
116
+ #ui notify:warning a transient warning-level notify
117
+ #ui notify:error a transient error-level notify
118
+ #ui set_editor_text a transient set_editor_text — replaces the composer draft
119
+ #ui <method> timeout=<s> adds a deadline in seconds (rejected for editor)
120
+ #ui multi <n> raises n dialogs at once, none awaited individually
121
+ #ui help this list`;
122
+ export function getUiScriptHelpText() {
123
+ return HELP_TEXT;
124
+ }
125
+ function buildStep(method, payload, timeoutMs) {
126
+ return {
127
+ method,
128
+ payload,
129
+ expectsResponse: true,
130
+ ...(timeoutMs !== undefined ? { timeoutMs } : {}),
131
+ await: true,
132
+ };
133
+ }
134
+ /** Like `buildStep`, but for a fire-and-forget transient (`notify`) — `expectsResponse: false`,
135
+ * no `timeoutMs` (transients have no deadline field on the real wire). */
136
+ function buildTransientStep(method, payload) {
137
+ return { method, payload, expectsResponse: false, await: true };
138
+ }
139
+ /** Parses a `#ui ...` prompt into the dialog(s) it describes. Returns `null` when `prompt` is not a
140
+ * recognised script — the overwhelmingly common case, kept a cheap prefix check. */
141
+ export function parseUiScript(prompt) {
142
+ const trimmed = prompt.trim();
143
+ if (!/^#ui(\s|$)/i.test(trimmed))
144
+ return null;
145
+ const rest = trimmed.slice(3).trim();
146
+ if (rest === "")
147
+ return null;
148
+ if (rest === "help")
149
+ return [];
150
+ const multiMatch = /^multi\s+(\d+)$/.exec(rest);
151
+ if (multiMatch) {
152
+ const n = Number(multiMatch[1]);
153
+ if (!Number.isInteger(n) || n < 1)
154
+ return null;
155
+ return Array.from({ length: n }, (_, i) => ({
156
+ method: "select",
157
+ payload: { title: `Question ${i + 1} of ${n}`, options: ["Allow", "Block"] },
158
+ expectsResponse: true,
159
+ await: false,
160
+ }));
161
+ }
162
+ const tokens = rest.split(/\s+/);
163
+ const recipe = tokens[0];
164
+ const extraTokens = tokens.slice(1);
165
+ if (extraTokens.some((t) => !/^timeout=\d+$/.test(t)))
166
+ return null;
167
+ const timeoutToken = extraTokens[0];
168
+ const timeoutMs = timeoutToken ? Number(timeoutToken.slice("timeout=".length)) * 1000 : undefined;
169
+ const [method, variant] = recipe.split(":");
170
+ switch (method) {
171
+ case "unknown":
172
+ if (variant !== undefined || timeoutMs !== undefined)
173
+ return null;
174
+ return [buildStep("pickRange", UNKNOWN_METHOD_PAYLOAD, undefined)];
175
+ case "editor":
176
+ // Pi's editor has no `timeout` field on the real wire — reject rather than fabricate one.
177
+ if (variant !== undefined || timeoutMs !== undefined)
178
+ return null;
179
+ return [buildStep("editor", EDITOR_COMMIT_MESSAGE, undefined)];
180
+ case "select": {
181
+ let payload = SELECT_ALLOW_BLOCK;
182
+ if (variant === "9")
183
+ payload = SELECT_NINE_OPTIONS;
184
+ else if (variant === "empty")
185
+ payload = SELECT_EMPTY_OPTIONS;
186
+ else if (variant === "long")
187
+ payload = SELECT_LONG_LABELS;
188
+ else if (variant !== undefined)
189
+ return null;
190
+ return [buildStep("select", payload, timeoutMs)];
191
+ }
192
+ case "confirm":
193
+ if (variant !== undefined)
194
+ return null;
195
+ return [buildStep("confirm", CONFIRM_CLEAR_SESSION, timeoutMs)];
196
+ case "input": {
197
+ let payload = INPUT_RELEASE_TAG;
198
+ if (variant === "multiline")
199
+ payload = INPUT_MULTILINE;
200
+ else if (variant !== undefined)
201
+ return null;
202
+ return [buildStep("input", payload, timeoutMs)];
203
+ }
204
+ case "notify": {
205
+ if (timeoutMs !== undefined)
206
+ return null; // transients have no deadline field on the wire
207
+ let payload = NOTIFY_INFO;
208
+ if (variant === "warning")
209
+ payload = NOTIFY_WARNING;
210
+ else if (variant === "error")
211
+ payload = NOTIFY_ERROR;
212
+ else if (variant !== undefined)
213
+ return null;
214
+ return [buildTransientStep("notify", payload)];
215
+ }
216
+ case "set_editor_text":
217
+ if (variant !== undefined || timeoutMs !== undefined)
218
+ return null;
219
+ return [buildTransientStep("set_editor_text", SET_EDITOR_TEXT)];
220
+ default:
221
+ return null;
222
+ }
223
+ }
224
+ //# sourceMappingURL=ui-script.js.map
@@ -51,6 +51,7 @@ export declare class PiAgentClient implements AgentClient {
51
51
  supportsRewindFiles: import("zod").ZodOptional<import("zod").ZodBoolean>;
52
52
  supportsRewindBoth: import("zod").ZodOptional<import("zod").ZodBoolean>;
53
53
  supportsSteering: import("zod").ZodOptional<import("zod").ZodBoolean>;
54
+ supportsExtensionUi: import("zod").ZodOptional<import("zod").ZodBoolean>;
54
55
  }, import("zod").ZodTypeAny, "passthrough">;
55
56
  private readonly command;
56
57
  private readonly factory;