@oh-my-pi/pi-coding-agent 16.4.6 → 16.5.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.
Files changed (141) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/dist/cli.js +3312 -3254
  3. package/dist/types/cli/args.d.ts +5 -0
  4. package/dist/types/cli/gallery-fixtures/shell.d.ts +1 -1
  5. package/dist/types/commands/launch.d.ts +15 -0
  6. package/dist/types/config/model-resolver.d.ts +4 -3
  7. package/dist/types/config/model-roles.d.ts +8 -0
  8. package/dist/types/config/settings-schema.d.ts +46 -14
  9. package/dist/types/extensibility/extensions/types.d.ts +1 -1
  10. package/dist/types/launch/broker.d.ts +2 -0
  11. package/dist/types/launch/client.d.ts +22 -0
  12. package/dist/types/launch/paths.d.ts +4 -0
  13. package/dist/types/launch/presence.d.ts +8 -0
  14. package/dist/types/launch/protocol.d.ts +170 -0
  15. package/dist/types/launch/terminal-output.d.ts +7 -0
  16. package/dist/types/modes/components/agent-hub.d.ts +1 -1
  17. package/dist/types/modes/components/index.d.ts +1 -0
  18. package/dist/types/modes/components/model-browser.d.ts +28 -3
  19. package/dist/types/modes/components/model-hub.d.ts +0 -10
  20. package/dist/types/modes/components/model-picker.d.ts +44 -0
  21. package/dist/types/modes/components/plan-review-overlay.d.ts +2 -0
  22. package/dist/types/modes/components/status-line/types.d.ts +3 -0
  23. package/dist/types/modes/components/welcome.d.ts +4 -0
  24. package/dist/types/modes/print-mode.d.ts +14 -8
  25. package/dist/types/modes/print-mode.test.d.ts +1 -0
  26. package/dist/types/modes/theme/theme.d.ts +2 -1
  27. package/dist/types/sdk.d.ts +5 -1
  28. package/dist/types/session/agent-session.d.ts +42 -0
  29. package/dist/types/session/session-context.d.ts +9 -0
  30. package/dist/types/session/session-entries.d.ts +6 -0
  31. package/dist/types/thinking.d.ts +2 -2
  32. package/dist/types/tiny/models.d.ts +1 -1
  33. package/dist/types/tools/browser/launch.d.ts +1 -0
  34. package/dist/types/tools/browser/run-cancellation.d.ts +28 -2
  35. package/dist/types/tools/browser/tab-protocol.d.ts +6 -0
  36. package/dist/types/tools/browser/tab-worker.d.ts +6 -0
  37. package/dist/types/tools/builtin-names.d.ts +1 -1
  38. package/dist/types/tools/index.d.ts +1 -0
  39. package/dist/types/tools/launch.d.ts +121 -0
  40. package/dist/types/tools/render-utils.d.ts +2 -0
  41. package/dist/types/tools/terminal-output.d.ts +5 -0
  42. package/dist/types/vibe/runtime.d.ts +2 -2
  43. package/dist/types/web/search/types.d.ts +0 -8
  44. package/package.json +20 -20
  45. package/src/cli/args.ts +11 -0
  46. package/src/cli/flag-tables.ts +9 -0
  47. package/src/cli/gallery-fixtures/shell.ts +82 -1
  48. package/src/cli.ts +10 -0
  49. package/src/commands/launch.ts +17 -0
  50. package/src/config/model-resolver.ts +110 -31
  51. package/src/config/model-roles.ts +14 -0
  52. package/src/config/settings-schema.ts +71 -7
  53. package/src/edit/renderer.ts +13 -10
  54. package/src/eval/__tests__/agent-bridge.test.ts +2 -2
  55. package/src/eval/__tests__/completion-bridge.test.ts +1 -1
  56. package/src/eval/completion-bridge.ts +4 -4
  57. package/src/eval/js/shared/rewrite-imports.ts +31 -13
  58. package/src/export/ttsr.ts +0 -3
  59. package/src/extensibility/extensions/model-api.ts +1 -1
  60. package/src/extensibility/extensions/types.ts +1 -1
  61. package/src/internal-urls/docs-index.ts +4 -4
  62. package/src/launch/broker.ts +1017 -0
  63. package/src/launch/client.ts +344 -0
  64. package/src/launch/paths.ts +17 -0
  65. package/src/launch/presence.ts +82 -0
  66. package/src/launch/protocol.ts +386 -0
  67. package/src/launch/terminal-output.ts +46 -0
  68. package/src/main.ts +50 -1
  69. package/src/modes/acp/acp-agent.ts +8 -1
  70. package/src/modes/components/agent-hub.ts +101 -31
  71. package/src/modes/components/compaction-summary-message.ts +8 -2
  72. package/src/modes/components/index.ts +1 -0
  73. package/src/modes/components/model-browser.ts +152 -49
  74. package/src/modes/components/model-hub.ts +55 -142
  75. package/src/modes/components/model-picker.ts +233 -0
  76. package/src/modes/components/plan-review-overlay.ts +7 -0
  77. package/src/modes/components/snapcompact-shape-preview-doc.md +7 -11
  78. package/src/modes/components/status-line/component.test.ts +41 -2
  79. package/src/modes/components/status-line/component.ts +4 -0
  80. package/src/modes/components/status-line/segments.ts +6 -0
  81. package/src/modes/components/status-line/types.ts +3 -0
  82. package/src/modes/components/tips.txt +2 -1
  83. package/src/modes/components/welcome.ts +13 -14
  84. package/src/modes/controllers/command-controller.ts +9 -1
  85. package/src/modes/controllers/event-controller.ts +31 -32
  86. package/src/modes/controllers/selector-controller.ts +96 -22
  87. package/src/modes/controllers/tan-command-controller.ts +40 -1
  88. package/src/modes/interactive-mode.ts +20 -3
  89. package/src/modes/print-mode.test.ts +71 -0
  90. package/src/modes/print-mode.ts +51 -2
  91. package/src/modes/theme/theme.ts +9 -0
  92. package/src/modes/utils/ui-helpers.ts +25 -4
  93. package/src/prompts/agents/designer.md +1 -1
  94. package/src/prompts/agents/librarian.md +1 -1
  95. package/src/prompts/agents/reviewer.md +1 -1
  96. package/src/prompts/agents/scout.md +1 -1
  97. package/src/prompts/system/plan-yolo-handoff.md +5 -0
  98. package/src/prompts/system/prewalk-checklist.md +7 -0
  99. package/src/prompts/system/prewalk-continue.md +1 -0
  100. package/src/prompts/system/prewalk-plan.md +13 -0
  101. package/src/prompts/system/system-prompt.md +9 -7
  102. package/src/prompts/system/tan-context-switch.md +17 -0
  103. package/src/prompts/tools/bash.md +6 -4
  104. package/src/prompts/tools/browser.md +4 -4
  105. package/src/prompts/tools/launch.md +25 -0
  106. package/src/sdk.ts +8 -2
  107. package/src/session/agent-session.ts +550 -87
  108. package/src/session/session-context.test.ts +10 -5
  109. package/src/session/session-context.ts +25 -8
  110. package/src/session/session-entries.ts +6 -0
  111. package/src/slash-commands/builtin-registry.ts +25 -0
  112. package/src/task/agents.ts +2 -2
  113. package/src/thinking.ts +10 -3
  114. package/src/tiny/models.ts +7 -7
  115. package/src/tools/bash-interactive.ts +5 -8
  116. package/src/tools/bash.ts +38 -24
  117. package/src/tools/browser/cmux/cmux-tab.ts +17 -2
  118. package/src/tools/browser/launch.ts +8 -4
  119. package/src/tools/browser/run-cancellation.ts +66 -6
  120. package/src/tools/browser/tab-protocol.ts +6 -0
  121. package/src/tools/browser/tab-supervisor.ts +17 -1
  122. package/src/tools/browser/tab-worker.ts +140 -21
  123. package/src/tools/builtin-names.ts +1 -0
  124. package/src/tools/eval-render.ts +16 -14
  125. package/src/tools/index.ts +5 -0
  126. package/src/tools/inspect-image.ts +2 -2
  127. package/src/tools/launch.ts +643 -0
  128. package/src/tools/render-utils.ts +3 -0
  129. package/src/tools/renderers.ts +2 -0
  130. package/src/tools/terminal-output.ts +141 -0
  131. package/src/tts/speech-enhancer.ts +2 -2
  132. package/src/utils/image-vision-fallback.ts +3 -3
  133. package/src/vibe/runtime.ts +2 -2
  134. package/src/web/search/provider.ts +0 -10
  135. package/src/web/search/providers/perplexity.ts +18 -2
  136. package/src/web/search/providers/public.ts +2 -4
  137. package/src/web/search/types.ts +0 -10
  138. package/dist/types/web/search/providers/bing.d.ts +0 -14
  139. package/dist/types/web/search/providers/yahoo.d.ts +0 -14
  140. package/src/web/search/providers/bing.ts +0 -197
  141. package/src/web/search/providers/yahoo.ts +0 -179
@@ -0,0 +1,386 @@
1
+ /**
2
+ * Cross-process daemon broker protocol shared by the tool, client, and broker.
3
+ */
4
+ /** Hidden CLI selector used to re-enter the daemon broker worker. */
5
+ export const DAEMON_BROKER_WORKER_ARG = "__omp_worker_daemon_broker";
6
+
7
+ /** Fixed dimensions negotiated with every supervised PTY. */
8
+ export const DAEMON_PTY_COLUMNS = 120;
9
+ export const DAEMON_PTY_ROWS = 40;
10
+
11
+ /** Environment key carrying the broker's canonical project directory. */
12
+ export const DAEMON_PROJECT_DIR_ENV = "OMP_DAEMON_PROJECT_DIR";
13
+
14
+ /** Environment key carrying the broker's private runtime directory. */
15
+ export const DAEMON_RUNTIME_DIR_ENV = "OMP_DAEMON_RUNTIME_DIR";
16
+
17
+ /** Optional environment key overriding last-client shutdown grace. */
18
+ export const DAEMON_IDLE_GRACE_ENV = "OMP_DAEMON_IDLE_GRACE_MS";
19
+
20
+ /** Stable lifecycle states exposed by the launch tool. */
21
+ export type DaemonState = "starting" | "running" | "ready" | "restarting" | "stopping" | "exited" | "failed";
22
+
23
+ /** Restart behavior applied after an unexpected daemon exit. */
24
+ export type DaemonRestartPolicy = "no" | "on-failure" | "always";
25
+
26
+ /** Readiness conditions; every configured condition must pass. */
27
+ export interface DaemonReadySpec {
28
+ log?: string;
29
+ port?: number;
30
+ host?: string;
31
+ timeoutMs: number;
32
+ }
33
+
34
+ /** Immutable launch specification retained for restart and inspection. */
35
+ export interface DaemonSpec {
36
+ name: string;
37
+ application: string;
38
+ args: string[];
39
+ env: Record<string, string>;
40
+ cwd: string;
41
+ pty: boolean;
42
+ ready?: DaemonReadySpec;
43
+ restart: DaemonRestartPolicy;
44
+ persist: boolean;
45
+ detached: boolean;
46
+ }
47
+
48
+ /** Serializable daemon state visible to every client in one project directory. */
49
+ export interface DaemonSnapshot {
50
+ name: string;
51
+ id: string;
52
+ state: DaemonState;
53
+ pid?: number;
54
+ createdAt: number;
55
+ startedAt: number;
56
+ readyAt?: number;
57
+ exitedAt?: number;
58
+ exitCode?: number;
59
+ exitReason?: string;
60
+ restartCount: number;
61
+ outputBytes: number;
62
+ owner?: string;
63
+ readyMatch?: string;
64
+ /** Readiness conditions still unmet while `state` is `starting`; absent once ready or without a ready spec. */
65
+ readyPending?: ("log" | "port")[];
66
+ persist: boolean;
67
+ detached: boolean;
68
+ }
69
+
70
+ /** Signals accepted by daemon input operations. */
71
+ export type DaemonSignal = "SIGINT" | "SIGTERM" | "SIGHUP" | "SIGQUIT" | "SIGKILL";
72
+
73
+ /** Typed broker operation sent over the authenticated socket. */
74
+ export type DaemonOperation =
75
+ | { op: "ping" }
76
+ | { op: "start"; spec: DaemonSpec; owner?: string }
77
+ | { op: "list" }
78
+ | {
79
+ op: "logs";
80
+ name: string;
81
+ lines: number;
82
+ head: boolean;
83
+ grep?: string;
84
+ follow: boolean;
85
+ cursor?: number;
86
+ timeoutMs: number;
87
+ }
88
+ | { op: "wait"; name: string; for: "ready" | "exit"; pattern?: string; timeoutMs: number }
89
+ | { op: "send"; name: string; data?: string; signal?: DaemonSignal }
90
+ | { op: "stop"; name: string; timeoutMs: number }
91
+ | { op: "restart"; name: string }
92
+ | { op: "describe"; name: string }
93
+ | { op: "shutdown" };
94
+
95
+ /** Typed broker result decoded before it reaches tool code. */
96
+ export type DaemonRpcResult =
97
+ | { op: "ping"; projectDir: string }
98
+ | { op: "start"; daemon: DaemonSnapshot; readyTimedOut: boolean }
99
+ | { op: "list"; daemons: DaemonSnapshot[] }
100
+ | {
101
+ op: "logs";
102
+ name: string;
103
+ text: string;
104
+ /** Raw PTY byte stream used only to reconstruct the terminal screen. */
105
+ terminalText?: string;
106
+ cursor: number;
107
+ timedOut: boolean;
108
+ state: DaemonState;
109
+ }
110
+ | { op: "wait"; daemon: DaemonSnapshot; matched?: string; timedOut: boolean }
111
+ | { op: "send"; daemon: DaemonSnapshot }
112
+ | { op: "stop"; daemon: DaemonSnapshot }
113
+ | { op: "restart"; daemon: DaemonSnapshot }
114
+ | { op: "describe"; daemon: DaemonSnapshot; spec: DaemonSpec }
115
+ | { op: "shutdown" };
116
+
117
+ /** Authenticated request envelope used by socket clients. */
118
+ export interface DaemonWireRequest {
119
+ id: string;
120
+ token: string;
121
+ operation: DaemonOperation;
122
+ }
123
+
124
+ /** Response envelope kept raw until matched with its pending operation. */
125
+ export type DaemonWireResponse = { id: string; ok: true; result: unknown } | { id: string; ok: false; error: string };
126
+
127
+ function isRecord(value: unknown): value is Record<string, unknown> {
128
+ return typeof value === "object" && value !== null && !Array.isArray(value);
129
+ }
130
+
131
+ function record(value: unknown, label: string): Record<string, unknown> {
132
+ if (!isRecord(value)) throw new Error(`${label} must be an object`);
133
+ return value;
134
+ }
135
+
136
+ function stringValue(value: unknown, label: string): string {
137
+ if (typeof value !== "string" || value.length === 0) throw new Error(`${label} must be a non-empty string`);
138
+ return value;
139
+ }
140
+ function rawString(value: unknown, label: string): string {
141
+ if (typeof value !== "string") throw new Error(`${label} must be a string`);
142
+ return value;
143
+ }
144
+
145
+ function optionalString(value: unknown, label: string): string | undefined {
146
+ if (value === undefined) return undefined;
147
+ return stringValue(value, label);
148
+ }
149
+
150
+ function booleanValue(value: unknown, label: string): boolean {
151
+ if (typeof value !== "boolean") throw new Error(`${label} must be a boolean`);
152
+ return value;
153
+ }
154
+
155
+ function numberValue(value: unknown, label: string): number {
156
+ if (typeof value !== "number" || !Number.isFinite(value)) throw new Error(`${label} must be a finite number`);
157
+ return value;
158
+ }
159
+
160
+ function optionalNumber(value: unknown, label: string): number | undefined {
161
+ if (value === undefined) return undefined;
162
+ return numberValue(value, label);
163
+ }
164
+
165
+ function stringArray(value: unknown, label: string): string[] {
166
+ if (!Array.isArray(value)) throw new Error(`${label} must be an array of strings`);
167
+ const result: string[] = [];
168
+ for (const item of value) result.push(rawString(item, `${label} item`));
169
+ return result;
170
+ }
171
+
172
+ function stringRecord(value: unknown, label: string): Record<string, string> {
173
+ const source = record(value, label);
174
+ const result: Record<string, string> = {};
175
+ for (const key in source) result[key] = rawString(source[key], `${label}.${key}`);
176
+ return result;
177
+ }
178
+
179
+ function daemonState(value: unknown): DaemonState {
180
+ const state = stringValue(value, "daemon state");
181
+ if (state === "starting" || state === "running" || state === "ready" || state === "restarting") return state;
182
+ if (state === "stopping" || state === "exited" || state === "failed") return state;
183
+ throw new Error(`Unknown daemon state: ${state}`);
184
+ }
185
+
186
+ function restartPolicy(value: unknown): DaemonRestartPolicy {
187
+ const policy = stringValue(value, "restart policy");
188
+ if (policy === "no" || policy === "on-failure" || policy === "always") return policy;
189
+ throw new Error(`Unknown restart policy: ${policy}`);
190
+ }
191
+
192
+ function daemonSignal(value: unknown): DaemonSignal {
193
+ const signal = stringValue(value, "signal");
194
+ if (signal === "SIGINT" || signal === "SIGTERM" || signal === "SIGHUP") return signal;
195
+ if (signal === "SIGQUIT" || signal === "SIGKILL") return signal;
196
+ throw new Error(`Unknown daemon signal: ${signal}`);
197
+ }
198
+
199
+ function readyPendingList(value: unknown): ("log" | "port")[] {
200
+ if (!Array.isArray(value)) throw new Error("daemon.readyPending must be an array");
201
+ const result: ("log" | "port")[] = [];
202
+ for (const item of value) {
203
+ if (item !== "log" && item !== "port") throw new Error(`Unknown readiness condition: ${String(item)}`);
204
+ result.push(item);
205
+ }
206
+ return result;
207
+ }
208
+
209
+ function readySpec(value: unknown): DaemonReadySpec {
210
+ const source = record(value, "ready");
211
+ const log = optionalString(source.log, "ready.log");
212
+ const port = optionalNumber(source.port, "ready.port");
213
+ const host = optionalString(source.host, "ready.host");
214
+ const timeoutMs = numberValue(source.timeoutMs, "ready.timeoutMs");
215
+ if (!log && port === undefined) throw new Error("ready requires log or port");
216
+ return { log, port, host, timeoutMs };
217
+ }
218
+
219
+ /** Decode and validate a daemon launch specification. */
220
+ export function parseDaemonSpec(value: unknown): DaemonSpec {
221
+ const source = record(value, "daemon spec");
222
+ const detached = source.detached === undefined ? false : booleanValue(source.detached, "spec.detached");
223
+ return {
224
+ name: stringValue(source.name, "spec.name"),
225
+ application: stringValue(source.application, "spec.application"),
226
+ args: stringArray(source.args, "spec.args"),
227
+ env: stringRecord(source.env, "spec.env"),
228
+ cwd: stringValue(source.cwd, "spec.cwd"),
229
+ pty: booleanValue(source.pty, "spec.pty"),
230
+ ready: source.ready === undefined ? undefined : readySpec(source.ready),
231
+ restart: restartPolicy(source.restart),
232
+ persist: booleanValue(source.persist, "spec.persist") || detached,
233
+ detached,
234
+ };
235
+ }
236
+
237
+ /** Decode and validate one daemon snapshot. */
238
+ export function parseDaemonSnapshot(value: unknown): DaemonSnapshot {
239
+ const source = record(value, "daemon snapshot");
240
+ return {
241
+ name: stringValue(source.name, "daemon.name"),
242
+ id: stringValue(source.id, "daemon.id"),
243
+ state: daemonState(source.state),
244
+ pid: optionalNumber(source.pid, "daemon.pid"),
245
+ createdAt: numberValue(source.createdAt, "daemon.createdAt"),
246
+ startedAt: numberValue(source.startedAt, "daemon.startedAt"),
247
+ readyAt: optionalNumber(source.readyAt, "daemon.readyAt"),
248
+ exitedAt: optionalNumber(source.exitedAt, "daemon.exitedAt"),
249
+ exitCode: optionalNumber(source.exitCode, "daemon.exitCode"),
250
+ exitReason: optionalString(source.exitReason, "daemon.exitReason"),
251
+ restartCount: numberValue(source.restartCount, "daemon.restartCount"),
252
+ outputBytes: numberValue(source.outputBytes, "daemon.outputBytes"),
253
+ owner: optionalString(source.owner, "daemon.owner"),
254
+ readyMatch: optionalString(source.readyMatch, "daemon.readyMatch"),
255
+ readyPending: source.readyPending === undefined ? undefined : readyPendingList(source.readyPending),
256
+ persist: booleanValue(source.persist, "daemon.persist"),
257
+ detached: source.detached === undefined ? false : booleanValue(source.detached, "daemon.detached"),
258
+ };
259
+ }
260
+
261
+ /** Decode a socket request before the broker acts on it. */
262
+ export function parseDaemonWireRequest(value: unknown): DaemonWireRequest {
263
+ const source = record(value, "daemon request");
264
+ return {
265
+ id: stringValue(source.id, "request.id"),
266
+ token: stringValue(source.token, "request.token"),
267
+ operation: parseDaemonOperation(source.operation),
268
+ };
269
+ }
270
+
271
+ /** Decode a socket response envelope before resolving a pending call. */
272
+ export function parseDaemonWireResponse(value: unknown): DaemonWireResponse {
273
+ const source = record(value, "daemon response");
274
+ const id = stringValue(source.id, "response.id");
275
+ if (source.ok === true) return { id, ok: true, result: source.result };
276
+ if (source.ok === false) return { id, ok: false, error: stringValue(source.error, "response.error") };
277
+ throw new Error("response.ok must be a boolean");
278
+ }
279
+
280
+ function parseDaemonOperation(value: unknown): DaemonOperation {
281
+ const source = record(value, "daemon operation");
282
+ const op = stringValue(source.op, "operation.op");
283
+ switch (op) {
284
+ case "ping":
285
+ case "list":
286
+ case "shutdown":
287
+ return { op };
288
+ case "start":
289
+ return {
290
+ op,
291
+ spec: parseDaemonSpec(source.spec),
292
+ owner: optionalString(source.owner, "operation.owner"),
293
+ };
294
+ case "logs":
295
+ return {
296
+ op,
297
+ name: stringValue(source.name, "operation.name"),
298
+ lines: numberValue(source.lines, "operation.lines"),
299
+ head: booleanValue(source.head, "operation.head"),
300
+ grep: optionalString(source.grep, "operation.grep"),
301
+ follow: booleanValue(source.follow, "operation.follow"),
302
+ cursor: optionalNumber(source.cursor, "operation.cursor"),
303
+ timeoutMs: numberValue(source.timeoutMs, "operation.timeoutMs"),
304
+ };
305
+ case "wait": {
306
+ const target = stringValue(source.for, "operation.for");
307
+ if (target !== "ready" && target !== "exit") throw new Error("operation.for must be ready or exit");
308
+ return {
309
+ op,
310
+ name: stringValue(source.name, "operation.name"),
311
+ for: target,
312
+ pattern: optionalString(source.pattern, "operation.pattern"),
313
+ timeoutMs: numberValue(source.timeoutMs, "operation.timeoutMs"),
314
+ };
315
+ }
316
+ case "send":
317
+ return {
318
+ op,
319
+ name: stringValue(source.name, "operation.name"),
320
+ data: optionalString(source.data, "operation.data"),
321
+ signal: source.signal === undefined ? undefined : daemonSignal(source.signal),
322
+ };
323
+ case "stop":
324
+ return {
325
+ op,
326
+ name: stringValue(source.name, "operation.name"),
327
+ timeoutMs: numberValue(source.timeoutMs, "operation.timeoutMs"),
328
+ };
329
+ case "restart":
330
+ case "describe":
331
+ return { op, name: stringValue(source.name, "operation.name") };
332
+ default:
333
+ throw new Error(`Unknown daemon operation: ${op}`);
334
+ }
335
+ }
336
+
337
+ /** Decode a broker result using its pending operation as the discriminator. */
338
+ export function parseDaemonRpcResult(operation: DaemonOperation, value: unknown): DaemonRpcResult {
339
+ const source = record(value, `${operation.op} result`);
340
+ switch (operation.op) {
341
+ case "ping":
342
+ return { op: "ping", projectDir: stringValue(source.projectDir, "result.projectDir") };
343
+ case "start":
344
+ return {
345
+ op: "start",
346
+ daemon: parseDaemonSnapshot(source.daemon),
347
+ readyTimedOut: booleanValue(source.readyTimedOut, "result.readyTimedOut"),
348
+ };
349
+ case "list": {
350
+ if (!Array.isArray(source.daemons)) throw new Error("result.daemons must be an array");
351
+ return { op: "list", daemons: source.daemons.map(parseDaemonSnapshot) };
352
+ }
353
+ case "logs":
354
+ return {
355
+ op: "logs",
356
+ name: stringValue(source.name, "result.name"),
357
+ text: typeof source.text === "string" ? source.text : "",
358
+ terminalText:
359
+ source.terminalText === undefined ? undefined : rawString(source.terminalText, "result.terminalText"),
360
+ cursor: numberValue(source.cursor, "result.cursor"),
361
+ timedOut: booleanValue(source.timedOut, "result.timedOut"),
362
+ state: daemonState(source.state),
363
+ };
364
+ case "wait":
365
+ return {
366
+ op: "wait",
367
+ daemon: parseDaemonSnapshot(source.daemon),
368
+ matched: optionalString(source.matched, "result.matched"),
369
+ timedOut: booleanValue(source.timedOut, "result.timedOut"),
370
+ };
371
+ case "send":
372
+ return { op: "send", daemon: parseDaemonSnapshot(source.daemon) };
373
+ case "stop":
374
+ return { op: "stop", daemon: parseDaemonSnapshot(source.daemon) };
375
+ case "restart":
376
+ return { op: "restart", daemon: parseDaemonSnapshot(source.daemon) };
377
+ case "describe":
378
+ return {
379
+ op: "describe",
380
+ daemon: parseDaemonSnapshot(source.daemon),
381
+ spec: parseDaemonSpec(source.spec),
382
+ };
383
+ case "shutdown":
384
+ return { op: "shutdown" };
385
+ }
386
+ }
@@ -0,0 +1,46 @@
1
+ import { logger } from "@oh-my-pi/pi-utils";
2
+ import xterm, { type Terminal as XtermTerminal } from "@xterm/headless";
3
+ import { readTerminalRows } from "../tools/terminal-output";
4
+ import { DAEMON_PTY_COLUMNS, DAEMON_PTY_ROWS } from "./protocol";
5
+
6
+ const VIRTUAL_SCROLLBACK_ROWS = 4_096;
7
+
8
+ /** Controls which virtual terminal rows a launch log exposes. */
9
+ export interface TerminalOutputOptions {
10
+ head: boolean;
11
+ maxRows: number;
12
+ }
13
+
14
+ function writeTerminal(terminal: XtermTerminal, output: string): Promise<void> {
15
+ const { promise, resolve } = Promise.withResolvers<void>();
16
+ terminal.write(output, resolve);
17
+ return promise;
18
+ }
19
+
20
+ /** Replays daemon bytes with the same xterm screen renderer used by PTY mode. */
21
+ export async function renderTerminalOutput(
22
+ output: string,
23
+ options: TerminalOutputOptions,
24
+ ): Promise<string[] | undefined> {
25
+ if (!output) return [];
26
+ const maxRows = Math.max(1, Math.floor(options.maxRows));
27
+ const terminal = new xterm.Terminal({
28
+ cols: DAEMON_PTY_COLUMNS,
29
+ rows: DAEMON_PTY_ROWS,
30
+ scrollback: Math.max(VIRTUAL_SCROLLBACK_ROWS, maxRows),
31
+ allowProposedApi: true,
32
+ });
33
+ try {
34
+ await writeTerminal(terminal, output);
35
+ const rows = readTerminalRows(terminal, 0, terminal.buffer.active.length);
36
+ while (rows.at(-1) === "") rows.pop();
37
+ return options.head ? rows.slice(0, maxRows) : rows.slice(-maxRows);
38
+ } catch (error) {
39
+ logger.debug("Failed to render launch terminal output", {
40
+ error: error instanceof Error ? error.message : String(error),
41
+ });
42
+ return undefined;
43
+ } finally {
44
+ terminal.dispose();
45
+ }
46
+ }
package/src/main.ts CHANGED
@@ -31,6 +31,7 @@ import { applyStartupCwd } from "./cli/startup-cwd";
31
31
  import { findConfigFile } from "./config";
32
32
  import { ModelRegistry } from "./config/model-registry";
33
33
  import {
34
+ expandRoleAlias,
34
35
  getModelMatchPreferences,
35
36
  resolveCliModel,
36
37
  resolveModelRoleValue,
@@ -50,6 +51,7 @@ import { injectOmpExtensionCliRoots } from "./discovery/omp-extension-roots";
50
51
  import { ExtensionRunner } from "./extensibility/extensions/runner";
51
52
  import type { ExtensionUIContext } from "./extensibility/extensions/types";
52
53
  import { scheduleMarketplaceAutoUpdate } from "./extensibility/plugins/marketplace-auto-update";
54
+ import { registerDaemonProjectPresence } from "./launch/presence";
53
55
  import type { MCPManager } from "./mcp";
54
56
  import { InteractiveMode } from "./modes/interactive-mode";
55
57
  import type { PrintModeOptions } from "./modes/print-mode";
@@ -850,6 +852,7 @@ export async function buildSessionOptions(
850
852
  cliProvider: parsed.provider,
851
853
  cliModel: parsed.model,
852
854
  modelRegistry,
855
+ settings: activeSettings,
853
856
  preferences: modelMatchPreferences,
854
857
  });
855
858
  if (resolved.warning) {
@@ -903,6 +906,47 @@ export async function buildSessionOptions(
903
906
  if (!options.model) options.model = scopedModels[0].model;
904
907
  }
905
908
 
909
+ if (parsed.noPrewalk && (parsed.prewalk || parsed.prewalkInto !== undefined)) {
910
+ throw new Error("--no-prewalk cannot be combined with --prewalk or --prewalk-into");
911
+ }
912
+ const prewalkEnabled = parsed.noPrewalk
913
+ ? false
914
+ : parsed.prewalk === true || parsed.prewalkInto !== undefined
915
+ ? true
916
+ : activeSettings.get("prewalk.enabled");
917
+ if (prewalkEnabled) {
918
+ const rolePattern = expandRoleAlias(parsed.prewalkInto ?? "@smol", activeSettings);
919
+ const resolved = resolveCliModel({ cliModel: rolePattern, modelRegistry, preferences: modelMatchPreferences });
920
+ if (resolved.warning) {
921
+ process.stderr.write(`${chalk.yellow(`Warning: ${resolved.warning}`)}\n`);
922
+ }
923
+ if (resolved.error || !resolved.model) {
924
+ throw new Error(resolved.error ?? `Model "${parsed.prewalkInto ?? "@smol"}" not found`);
925
+ }
926
+ if (!modelRegistry.hasConfiguredAuth(resolved.model)) {
927
+ throw new Error(`No API key for ${resolved.model.provider}/${resolved.model.id}`);
928
+ }
929
+ options.prewalk = { target: resolved.model, thinkingLevel: resolved.thinkingLevel };
930
+ }
931
+
932
+ if (parsed.planYoloInto !== undefined && !parsed.planYolo) {
933
+ throw new Error("--plan-yolo-into requires --plan-yolo");
934
+ }
935
+ if (parsed.planYolo) {
936
+ const rolePattern = expandRoleAlias(parsed.planYoloInto ?? "@smol", activeSettings);
937
+ const resolved = resolveCliModel({ cliModel: rolePattern, modelRegistry, preferences: modelMatchPreferences });
938
+ if (resolved.warning) {
939
+ process.stderr.write(`${chalk.yellow(`Warning: ${resolved.warning}`)}\n`);
940
+ }
941
+ if (resolved.error || !resolved.model) {
942
+ throw new Error(resolved.error ?? `Model "${parsed.planYoloInto ?? "@smol"}" not found`);
943
+ }
944
+ if (!modelRegistry.hasConfiguredAuth(resolved.model)) {
945
+ throw new Error(`No API key for ${resolved.model.provider}/${resolved.model.id}`);
946
+ }
947
+ options.planYolo = { target: resolved.model, thinkingLevel: resolved.thinkingLevel };
948
+ }
949
+
906
950
  // Thinking level
907
951
  if (parsed.thinking) {
908
952
  options.thinkingLevel = parsed.thinking;
@@ -989,11 +1033,12 @@ interface RunRootCommandDependencies {
989
1033
  settings?: Settings;
990
1034
  forceSetupWizard?: boolean;
991
1035
  }
1036
+ const DEFAULT_RUN_ROOT_DEPENDENCIES: RunRootCommandDependencies = {};
992
1037
 
993
1038
  export async function runRootCommand(
994
1039
  parsed: Args,
995
1040
  rawArgs: string[],
996
- deps: RunRootCommandDependencies = {},
1041
+ deps: RunRootCommandDependencies = DEFAULT_RUN_ROOT_DEPENDENCIES,
997
1042
  ): Promise<void> {
998
1043
  logger.startTiming();
999
1044
  startStartupWatchdog();
@@ -1139,6 +1184,7 @@ export async function runRootCommand(
1139
1184
  modelPatterns,
1140
1185
  modelRegistry,
1141
1186
  modelMatchPreferences,
1187
+ settingsInstance,
1142
1188
  );
1143
1189
  }
1144
1190
 
@@ -1250,6 +1296,9 @@ export async function runRootCommand(
1250
1296
  }
1251
1297
 
1252
1298
  await pluginPreloadPromise;
1299
+ if (deps === DEFAULT_RUN_ROOT_DEPENDENCIES) {
1300
+ await logger.time("registerDaemonProjectPresence", registerDaemonProjectPresence, cwd);
1301
+ }
1253
1302
 
1254
1303
  scheduleMarketplaceAutoUpdate({
1255
1304
  autoUpdate: settingsInstance.get("marketplace.autoUpdate"),
@@ -351,12 +351,19 @@ async function elicitFromAcpClient(
351
351
  finish(undefined);
352
352
  });
353
353
  const response = await promise;
354
- if (response?.action !== "accept" || !response.content) {
354
+ if (!isAcceptedElicitation(response) || !response.content) {
355
355
  return undefined;
356
356
  }
357
357
  return response.content.value;
358
358
  }
359
359
 
360
+ /** Narrows a `CreateElicitationResponse` to the accepted-with-content branch; the SDK's `action: string` catch-all arm otherwise defeats literal narrowing on `action !== "accept"`. */
361
+ function isAcceptedElicitation(
362
+ response: CreateElicitationResponse | undefined,
363
+ ): response is Extract<CreateElicitationResponse, { action: "accept" }> {
364
+ return response?.action === "accept";
365
+ }
366
+
360
367
  /**
361
368
  * Build an {@link ExtensionUIContext} that translates skill/extension UI
362
369
  * requests into ACP elicitations against `connection` for the session