@oh-my-pi/pi-coding-agent 17.2.5 → 17.2.7

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 (201) hide show
  1. package/CHANGELOG.md +35 -1
  2. package/dist/{CHANGELOG-q2w43aw3.md → CHANGELOG-1p8yvde7.md} +35 -1
  3. package/dist/cli.js +3340 -3339
  4. package/dist/types/advisor/advise-tool.d.ts +10 -4
  5. package/dist/types/autoresearch/tools/init-experiment.d.ts +19 -7
  6. package/dist/types/autoresearch/tools/log-experiment.d.ts +19 -11
  7. package/dist/types/autoresearch/tools/run-experiment.d.ts +4 -2
  8. package/dist/types/autoresearch/tools/update-notes.d.ts +5 -2
  9. package/dist/types/cli/gc-cli.d.ts +1 -0
  10. package/dist/types/commit/agentic/tools/analyze-file.d.ts +5 -2
  11. package/dist/types/commit/agentic/tools/git-file-diff.d.ts +5 -2
  12. package/dist/types/commit/agentic/tools/git-hunk.d.ts +6 -2
  13. package/dist/types/commit/agentic/tools/git-overview.d.ts +5 -2
  14. package/dist/types/commit/agentic/tools/propose-changelog.d.ts +29 -7
  15. package/dist/types/commit/agentic/tools/propose-commit.d.ts +14 -4
  16. package/dist/types/commit/agentic/tools/recent-commits.d.ts +4 -2
  17. package/dist/types/commit/agentic/tools/schemas.d.ts +7 -3
  18. package/dist/types/commit/agentic/tools/split-commit.d.ts +37 -10
  19. package/dist/types/commit/changelog/generate.d.ts +13 -3
  20. package/dist/types/commit/shared-llm.d.ts +13 -4
  21. package/dist/types/config/config-file.d.ts +1 -1
  22. package/dist/types/config/models-config-schema-bundle.d.ts +1992 -981
  23. package/dist/types/config/models-config-schema.d.ts +1201 -580
  24. package/dist/types/config/models-config.d.ts +355 -371
  25. package/dist/types/edit/hashline/params.d.ts +4 -2
  26. package/dist/types/edit/modes/apply-patch.d.ts +4 -2
  27. package/dist/types/edit/modes/patch.d.ts +16 -5
  28. package/dist/types/edit/modes/replace.d.ts +8 -3
  29. package/dist/types/eval/bridge-timeout.d.ts +5 -2
  30. package/dist/types/eval/completion-bridge.d.ts +13 -0
  31. package/dist/types/eval/py/tool-bridge.d.ts +12 -0
  32. package/dist/types/extensibility/custom-commands/types.d.ts +4 -4
  33. package/dist/types/extensibility/custom-tools/types.d.ts +4 -4
  34. package/dist/types/extensibility/extensions/types.d.ts +4 -4
  35. package/dist/types/extensibility/hooks/types.d.ts +4 -4
  36. package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +1 -1
  37. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +1 -1
  38. package/dist/types/extensibility/legacy-typebox.d.ts +8 -0
  39. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +6 -13
  40. package/dist/types/goals/tools/goal-tool.d.ts +12 -4
  41. package/dist/types/launch/client.d.ts +9 -1
  42. package/dist/types/launch/protocol.d.ts +17 -0
  43. package/dist/types/lsp/index.d.ts +15 -5
  44. package/dist/types/lsp/types.d.ts +15 -5
  45. package/dist/types/modes/components/btw-panel.d.ts +3 -0
  46. package/dist/types/modes/controllers/btw-controller.d.ts +2 -0
  47. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  48. package/dist/types/modes/interactive-mode.d.ts +4 -1
  49. package/dist/types/modes/types.d.ts +3 -1
  50. package/dist/types/security/contracts/schemas.d.ts +1115 -537
  51. package/dist/types/security/publication.d.ts +92 -34
  52. package/dist/types/session/agent-session-types.d.ts +5 -0
  53. package/dist/types/session/agent-session.d.ts +26 -2
  54. package/dist/types/session/launch-completion.d.ts +10 -0
  55. package/dist/types/session/session-entries.d.ts +15 -1
  56. package/dist/types/session/session-manager.d.ts +7 -0
  57. package/dist/types/session/yield-queue.d.ts +6 -1
  58. package/dist/types/task/types.d.ts +63 -18
  59. package/dist/types/tools/ask.d.ts +34 -8
  60. package/dist/types/tools/ast-edit.d.ts +16 -4
  61. package/dist/types/tools/ast-grep.d.ts +12 -4
  62. package/dist/types/tools/bash.d.ts +20 -11
  63. package/dist/types/tools/browser.d.ts +64 -20
  64. package/dist/types/tools/checkpoint.d.ts +16 -8
  65. package/dist/types/tools/computer.d.ts +1 -1
  66. package/dist/types/tools/debug.d.ts +118 -48
  67. package/dist/types/tools/eval.d.ts +11 -5
  68. package/dist/types/tools/gh.d.ts +74 -26
  69. package/dist/types/tools/glob.d.ts +16 -6
  70. package/dist/types/tools/grep.d.ts +18 -6
  71. package/dist/types/tools/hub/index.d.ts +116 -46
  72. package/dist/types/tools/image-gen.d.ts +25 -8
  73. package/dist/types/tools/index.d.ts +9 -0
  74. package/dist/types/tools/inspect-image.d.ts +10 -4
  75. package/dist/types/tools/learn.d.ts +26 -8
  76. package/dist/types/tools/manage-skill.d.ts +16 -6
  77. package/dist/types/tools/memory-edit.d.ts +18 -6
  78. package/dist/types/tools/memory-recall.d.ts +8 -4
  79. package/dist/types/tools/memory-reflect.d.ts +10 -4
  80. package/dist/types/tools/memory-retain.d.ts +14 -4
  81. package/dist/types/tools/read.d.ts +8 -4
  82. package/dist/types/tools/security-scan.d.ts +82 -30
  83. package/dist/types/tools/todo.d.ts +30 -10
  84. package/dist/types/tools/tts.d.ts +12 -5
  85. package/dist/types/tools/vibe.d.ts +42 -18
  86. package/dist/types/tools/write.d.ts +10 -4
  87. package/dist/types/web/search/index.d.ts +20 -6
  88. package/package.json +13 -13
  89. package/scripts/legacy-pi-virtual-module.ts +2 -2
  90. package/src/advisor/advise-tool.ts +1 -1
  91. package/src/advisor/config.ts +1 -1
  92. package/src/autoresearch/tools/init-experiment.ts +1 -2
  93. package/src/autoresearch/tools/log-experiment.ts +1 -2
  94. package/src/autoresearch/tools/run-experiment.ts +1 -1
  95. package/src/autoresearch/tools/update-notes.ts +1 -1
  96. package/src/cli/gc-cli.ts +641 -21
  97. package/src/commit/agentic/tools/analyze-file.ts +1 -1
  98. package/src/commit/agentic/tools/git-file-diff.ts +1 -1
  99. package/src/commit/agentic/tools/git-hunk.ts +1 -1
  100. package/src/commit/agentic/tools/git-overview.ts +1 -1
  101. package/src/commit/agentic/tools/propose-changelog.ts +1 -1
  102. package/src/commit/agentic/tools/propose-commit.ts +1 -1
  103. package/src/commit/agentic/tools/recent-commits.ts +1 -1
  104. package/src/commit/agentic/tools/schemas.ts +1 -1
  105. package/src/commit/agentic/tools/split-commit.ts +1 -1
  106. package/src/commit/analysis/summary.ts +1 -1
  107. package/src/commit/changelog/generate.ts +1 -1
  108. package/src/config/config-file.ts +2 -2
  109. package/src/config/model-discovery.ts +14 -14
  110. package/src/config/model-registry.ts +10 -8
  111. package/src/config/models-config-schema-bundle.ts +1 -8
  112. package/src/config/settings.ts +1 -1
  113. package/src/discovery/codex.ts +22 -9
  114. package/src/edit/hashline/params.ts +1 -1
  115. package/src/edit/modes/apply-patch.ts +1 -1
  116. package/src/edit/modes/patch.ts +1 -1
  117. package/src/edit/modes/replace.ts +2 -1
  118. package/src/eval/agent-bridge.ts +4 -2
  119. package/src/eval/bridge-timeout.ts +5 -2
  120. package/src/eval/completion-bridge.ts +3 -2
  121. package/src/eval/executor-base.ts +20 -1
  122. package/src/eval/js/context-manager.ts +94 -6
  123. package/src/eval/py/tool-bridge.ts +29 -9
  124. package/src/export/html/index.ts +10 -3
  125. package/src/export/share.ts +4 -0
  126. package/src/extensibility/custom-commands/loader.ts +3 -3
  127. package/src/extensibility/custom-commands/types.ts +4 -4
  128. package/src/extensibility/custom-tools/loader.ts +3 -3
  129. package/src/extensibility/custom-tools/types.ts +5 -4
  130. package/src/extensibility/extensions/loader.ts +3 -3
  131. package/src/extensibility/extensions/types.ts +5 -4
  132. package/src/extensibility/hooks/loader.ts +3 -3
  133. package/src/extensibility/hooks/types.ts +4 -4
  134. package/src/extensibility/legacy-pi-ai-shim.ts +1 -1
  135. package/src/extensibility/legacy-pi-coding-agent-shim.ts +2 -2
  136. package/src/extensibility/legacy-typebox.ts +101 -0
  137. package/src/extensibility/plugins/legacy-pi-compat.ts +13 -22
  138. package/src/goals/tools/goal-tool.ts +1 -1
  139. package/src/launch/broker.ts +222 -6
  140. package/src/launch/client.ts +161 -9
  141. package/src/launch/protocol.ts +52 -0
  142. package/src/lsp/types.ts +1 -1
  143. package/src/main.ts +13 -6
  144. package/src/mcp/config-writer.ts +1 -1
  145. package/src/modes/components/btw-panel.ts +41 -4
  146. package/src/modes/controllers/btw-controller.ts +55 -7
  147. package/src/modes/controllers/command-controller.ts +31 -0
  148. package/src/modes/controllers/input-controller.ts +24 -7
  149. package/src/modes/interactive-mode.ts +16 -2
  150. package/src/modes/theme/theme.ts +3 -3
  151. package/src/modes/types.ts +8 -1
  152. package/src/prompts/session/launch-completion.md +1 -0
  153. package/src/sdk.ts +15 -0
  154. package/src/security/contracts/schemas.ts +202 -184
  155. package/src/security/contracts/validation.ts +6 -2
  156. package/src/security/publication.ts +1 -1
  157. package/src/security/store.ts +2 -2
  158. package/src/session/agent-session-types.ts +6 -0
  159. package/src/session/agent-session.ts +190 -14
  160. package/src/session/launch-completion.ts +37 -0
  161. package/src/session/session-context.ts +26 -0
  162. package/src/session/session-entries.ts +17 -1
  163. package/src/session/session-manager.ts +21 -0
  164. package/src/session/yield-queue.ts +121 -16
  165. package/src/slash-commands/builtin-registry.ts +10 -1
  166. package/src/task/types.ts +1 -1
  167. package/src/tools/ask.ts +1 -1
  168. package/src/tools/ast-edit.ts +1 -1
  169. package/src/tools/ast-grep.ts +1 -1
  170. package/src/tools/bash.ts +61 -3
  171. package/src/tools/browser/cmux/cmux-tab.ts +92 -4
  172. package/src/tools/browser.ts +1 -1
  173. package/src/tools/checkpoint.ts +1 -1
  174. package/src/tools/computer.ts +1 -1
  175. package/src/tools/debug.ts +1 -1
  176. package/src/tools/eval.ts +1 -1
  177. package/src/tools/gh.ts +1 -2
  178. package/src/tools/glob.ts +1 -1
  179. package/src/tools/grep.ts +1 -1
  180. package/src/tools/hub/index.ts +1 -1
  181. package/src/tools/hub/launch.ts +122 -6
  182. package/src/tools/image-gen.ts +1 -1
  183. package/src/tools/index.ts +9 -0
  184. package/src/tools/inspect-image.ts +1 -1
  185. package/src/tools/learn.ts +1 -1
  186. package/src/tools/manage-skill.ts +1 -1
  187. package/src/tools/memory-edit.ts +1 -1
  188. package/src/tools/memory-recall.ts +1 -1
  189. package/src/tools/memory-reflect.ts +1 -1
  190. package/src/tools/memory-retain.ts +1 -1
  191. package/src/tools/read.ts +1 -1
  192. package/src/tools/security-scan.ts +1 -1
  193. package/src/tools/todo.ts +1 -1
  194. package/src/tools/tts.ts +1 -1
  195. package/src/tools/vibe.ts +2 -1
  196. package/src/tools/write.ts +1 -1
  197. package/src/web/search/index.ts +2 -1
  198. package/dist/types/config/file-lock.d.ts +0 -29
  199. package/dist/types/extensibility/typebox.d.ts +0 -181
  200. package/src/config/file-lock.ts +0 -164
  201. package/src/extensibility/typebox.ts +0 -958
@@ -1,4 +1,4 @@
1
- import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
1
+ import { type AgentMessage, ASIDE_MESSAGE_COMMIT, ASIDE_MESSAGE_DISCARD } from "@oh-my-pi/pi-agent-core";
2
2
  import { logger } from "@oh-my-pi/pi-utils";
3
3
 
4
4
  export interface YieldDispatcher<P> {
@@ -25,6 +25,17 @@ interface StoredDispatcher {
25
25
  skipIdleFlush?: boolean;
26
26
  }
27
27
 
28
+ interface StoredEntry {
29
+ value: unknown;
30
+ resolve?: () => void;
31
+ reject?: (error: Error) => void;
32
+ }
33
+
34
+ interface BuiltMessage {
35
+ message: AgentMessage;
36
+ entries: StoredEntry[];
37
+ }
38
+
28
39
  function formatError(error: unknown): string {
29
40
  return error instanceof Error ? error.message : String(error);
30
41
  }
@@ -32,7 +43,7 @@ function formatError(error: unknown): string {
32
43
  export class YieldQueue {
33
44
  readonly #options: YieldQueueOptions;
34
45
  readonly #dispatchers = new Map<string, StoredDispatcher>();
35
- readonly #entries = new Map<string, unknown[]>();
46
+ readonly #entries = new Map<string, StoredEntry[]>();
36
47
  #idleFlushPending = false;
37
48
 
38
49
  constructor(options: YieldQueueOptions) {
@@ -49,14 +60,27 @@ export class YieldQueue {
49
60
  return () => {
50
61
  if (this.#dispatchers.get(kind) !== stored) return;
51
62
  this.#dispatchers.delete(kind);
63
+ this.#rejectEntries(this.#entries.get(kind) ?? [], new Error(`Yield queue dispatcher removed: ${kind}`));
52
64
  this.#entries.delete(kind);
53
65
  };
54
66
  }
55
67
 
56
68
  enqueue<P>(kind: string, entry: P): void {
69
+ this.#enqueue(kind, { value: entry });
70
+ }
71
+
72
+ enqueueWithReceipt<P>(kind: string, entry: P): Promise<void> {
73
+ const { promise, resolve, reject } = Promise.withResolvers<void>();
74
+ if (!this.#enqueue(kind, { value: entry, resolve, reject })) {
75
+ reject(new Error(`Yield queue entry ignored for unregistered kind: ${kind}`));
76
+ }
77
+ return promise;
78
+ }
79
+
80
+ #enqueue(kind: string, entry: StoredEntry): boolean {
57
81
  if (!this.#dispatchers.has(kind)) {
58
82
  logger.warn("Yield queue entry ignored for unregistered kind", { kind });
59
- return;
83
+ return false;
60
84
  }
61
85
  let entries = this.#entries.get(kind);
62
86
  if (!entries) {
@@ -67,6 +91,7 @@ export class YieldQueue {
67
91
  if (!this.#options.isStreaming() && !this.#dispatchers.get(kind)!.skipIdleFlush) {
68
92
  this.#scheduleIdleFlush();
69
93
  }
94
+ return true;
70
95
  }
71
96
 
72
97
  has(kind?: string): boolean {
@@ -77,30 +102,55 @@ export class YieldQueue {
77
102
  return false;
78
103
  }
79
104
 
105
+ /** Arrange an idle flush for entries queued near the end of a streaming run. */
106
+ requestIdleFlush(): void {
107
+ for (const [kind, dispatcher] of this.#dispatchers) {
108
+ if (!dispatcher.skipIdleFlush && this.has(kind)) {
109
+ this.#scheduleIdleFlush();
110
+ return;
111
+ }
112
+ }
113
+ }
114
+
80
115
  async flush(mode: YieldFlushMode): Promise<void> {
81
116
  if (mode === "idle") {
82
117
  this.#idleFlushPending = false;
83
118
  }
84
- const idleMessages: AgentMessage[] = [];
119
+ const idleMessages: BuiltMessage[] = [];
85
120
  for (const [kind, dispatcher] of this.#dispatchers) {
121
+ if (mode === "idle" && dispatcher.skipIdleFlush) continue;
86
122
  const entries = this.#drain(kind);
87
123
  if (entries.length === 0) continue;
88
- const message = this.#build(kind, dispatcher, entries);
89
- if (!message) continue;
124
+ const built = this.#build(kind, dispatcher, entries);
125
+ if (!built) continue;
90
126
  if (mode === "streaming") {
91
127
  try {
92
- this.#options.injectStreaming?.(message);
128
+ if (!this.#options.injectStreaming) throw new Error("Streaming injection is unavailable");
129
+ this.#options.injectStreaming(built.message);
130
+ this.#resolveEntries(built.entries);
93
131
  } catch (error) {
132
+ const dispatchError = error instanceof Error ? error : new Error(String(error));
133
+ this.#rejectEntries(built.entries, dispatchError);
94
134
  logger.warn("Yield queue streaming dispatch failed", { kind, error: formatError(error) });
95
135
  }
96
136
  } else {
97
- idleMessages.push(message);
137
+ idleMessages.push(built);
98
138
  }
99
139
  }
100
140
  if (mode === "idle" && idleMessages.length > 0) {
141
+ for (const item of idleMessages) this.#attachEntrySettlement(item);
101
142
  try {
102
- await this.#options.injectIdle(idleMessages);
143
+ await this.#options.injectIdle(idleMessages.map(item => item.message));
144
+ for (const item of idleMessages) {
145
+ (item.message as AgentMessage & { [ASIDE_MESSAGE_COMMIT]?: () => void })[ASIDE_MESSAGE_COMMIT]?.();
146
+ }
103
147
  } catch (error) {
148
+ const dispatchError = error instanceof Error ? error : new Error(String(error));
149
+ for (const item of idleMessages) {
150
+ (item.message as AgentMessage & { [ASIDE_MESSAGE_DISCARD]?: (error: Error) => void })[
151
+ ASIDE_MESSAGE_DISCARD
152
+ ]?.(dispatchError);
153
+ }
104
154
  logger.warn("Yield queue idle dispatch failed", { error: formatError(error) });
105
155
  }
106
156
  }
@@ -119,7 +169,12 @@ export class YieldQueue {
119
169
  for (const [kind, dispatcher] of this.#dispatchers) {
120
170
  const entries = this.#drain(kind);
121
171
  if (entries.length === 0) continue;
122
- thunks.push(() => this.#build(kind, dispatcher, entries));
172
+ thunks.push(() => {
173
+ const built = this.#build(kind, dispatcher, entries);
174
+ if (!built) return null;
175
+ this.#attachEntrySettlement(built);
176
+ return built.message;
177
+ });
123
178
  }
124
179
  return thunks;
125
180
  }
@@ -127,14 +182,22 @@ export class YieldQueue {
127
182
  /** Drop queued entries. With `kind`, drop only that kind's entries (leaving
128
183
  * any pending idle-flush for other kinds intact); otherwise drop everything. */
129
184
  clear(kind?: string): void {
185
+ const error = new Error("Yield queue entry cleared before dispatch");
130
186
  if (kind !== undefined) {
187
+ this.#rejectEntries(this.#entries.get(kind) ?? [], error);
131
188
  this.#entries.delete(kind);
132
189
  return;
133
190
  }
191
+ for (const entries of this.#entries.values()) this.#rejectEntries(entries, error);
134
192
  this.#entries.clear();
135
193
  this.#idleFlushPending = false;
136
194
  }
137
195
 
196
+ /** Clear a scheduled-flush latch when its host task is cancelled before running. */
197
+ cancelIdleFlushScheduling(): void {
198
+ this.#idleFlushPending = false;
199
+ }
200
+
138
201
  #scheduleIdleFlush(): void {
139
202
  if (this.#idleFlushPending) return;
140
203
  this.#idleFlushPending = true;
@@ -150,34 +213,76 @@ export class YieldQueue {
150
213
  }
151
214
  }
152
215
 
153
- #drain(kind: string): unknown[] {
216
+ #drain(kind: string): StoredEntry[] {
154
217
  const entries = this.#entries.get(kind);
155
218
  if (!entries || entries.length === 0) return [];
156
219
  this.#entries.delete(kind);
157
220
  return entries;
158
221
  }
159
222
 
160
- #build(kind: string, dispatcher: StoredDispatcher, entries: unknown[]): AgentMessage | null {
161
- const survivors: unknown[] = [];
223
+ #build(kind: string, dispatcher: StoredDispatcher, entries: StoredEntry[]): BuiltMessage | null {
224
+ const survivors: StoredEntry[] = [];
162
225
  for (const entry of entries) {
163
226
  if (dispatcher.isStale) {
164
227
  let stale: boolean;
165
228
  try {
166
- stale = dispatcher.isStale(entry);
229
+ stale = dispatcher.isStale(entry.value);
167
230
  } catch (error) {
231
+ const staleError = error instanceof Error ? error : new Error(String(error));
232
+ entry.reject?.(staleError);
168
233
  logger.warn("Yield queue stale check failed", { kind, error: formatError(error) });
169
234
  continue;
170
235
  }
171
- if (stale) continue;
236
+ if (stale) {
237
+ entry.reject?.(new Error(`Yield queue entry became stale: ${kind}`));
238
+ continue;
239
+ }
172
240
  }
173
241
  survivors.push(entry);
174
242
  }
175
243
  if (survivors.length === 0) return null;
176
244
  try {
177
- return dispatcher.build(survivors);
245
+ const message = dispatcher.build(survivors.map(entry => entry.value));
246
+ if (!message) {
247
+ this.#rejectEntries(survivors, new Error(`Yield queue dispatcher skipped entry: ${kind}`));
248
+ return null;
249
+ }
250
+ return { message, entries: survivors };
178
251
  } catch (error) {
252
+ const buildError = error instanceof Error ? error : new Error(String(error));
253
+ this.#rejectEntries(survivors, buildError);
179
254
  logger.warn("Yield queue build failed", { kind, error: formatError(error) });
180
255
  return null;
181
256
  }
182
257
  }
258
+
259
+ #attachEntrySettlement(built: BuiltMessage): void {
260
+ let settled = false;
261
+ Object.defineProperties(built.message, {
262
+ [ASIDE_MESSAGE_COMMIT]: {
263
+ configurable: true,
264
+ value: () => {
265
+ if (settled) return;
266
+ settled = true;
267
+ this.#resolveEntries(built.entries);
268
+ },
269
+ },
270
+ [ASIDE_MESSAGE_DISCARD]: {
271
+ configurable: true,
272
+ value: (error: Error) => {
273
+ if (settled) return;
274
+ settled = true;
275
+ this.#rejectEntries(built.entries, error);
276
+ },
277
+ },
278
+ });
279
+ }
280
+
281
+ #resolveEntries(entries: StoredEntry[]): void {
282
+ for (const entry of entries) entry.resolve?.();
283
+ }
284
+
285
+ #rejectEntries(entries: StoredEntry[], error: Error): void {
286
+ for (const entry of entries) entry.reject?.(error);
287
+ }
183
288
  }
@@ -1691,7 +1691,6 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
1691
1691
  },
1692
1692
  {
1693
1693
  name: "new",
1694
- aliases: ["clear"],
1695
1694
  description: "Start a new session",
1696
1695
  handleTui: async (_command, runtime) => {
1697
1696
  runtime.ctx.editor.setText("");
@@ -1719,6 +1718,16 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
1719
1718
  await runtime.ctx.handleFreshCommand();
1720
1719
  },
1721
1720
  },
1721
+ {
1722
+ name: "clear",
1723
+ description: "Clear the conversation context in place, keeping the session",
1724
+ getTuiAutocompleteDescription: runtime =>
1725
+ runtime.ctx.session.isStreaming ? "Clear: unavailable while streaming" : "Clear: drop context, keep session",
1726
+ handleTui: async (_command, runtime) => {
1727
+ runtime.ctx.editor.setText("");
1728
+ await runtime.ctx.handleResetContextCommand();
1729
+ },
1730
+ },
1722
1731
  {
1723
1732
  name: "drop",
1724
1733
  description: "Delete the current session and start a new one",
package/src/task/types.ts CHANGED
@@ -1,6 +1,6 @@
1
+ import { type BaseType, type } from "@oh-my-pi/omptype";
1
2
  import type { Usage } from "@oh-my-pi/pi-ai";
2
3
  import { $env } from "@oh-my-pi/pi-utils";
3
- import { type BaseType, type } from "arktype";
4
4
  import type { AgentSessionEvent } from "../session/agent-session";
5
5
  import type { ConfiguredThinkingLevel, TaskEffort } from "../thinking";
6
6
  import type { NestedRepoPatch } from "./worktree";
package/src/tools/ask.ts CHANGED
@@ -15,6 +15,7 @@
15
15
  * - Questions may time out and auto-select the recommended option (configurable, disabled in plan mode)
16
16
  */
17
17
 
18
+ import { type as arkType } from "@oh-my-pi/omptype";
18
19
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
19
20
  import type { ToolExample } from "@oh-my-pi/pi-ai";
20
21
  import {
@@ -30,7 +31,6 @@ import {
30
31
  visibleWidth,
31
32
  } from "@oh-my-pi/pi-tui";
32
33
  import { prompt, untilAborted } from "@oh-my-pi/pi-utils";
33
- import { type as arkType } from "arktype";
34
34
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
35
35
  import type { ExtensionUISelectItem } from "../extensibility/extensions";
36
36
  import { getMarkdownTheme, type Theme, theme } from "../modes/theme/theme";
@@ -1,12 +1,12 @@
1
1
  import * as path from "node:path";
2
2
  import { formatHashlineHeader } from "@oh-my-pi/hashline";
3
+ import { type } from "@oh-my-pi/omptype";
3
4
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
4
5
  import type { ToolExample } from "@oh-my-pi/pi-ai";
5
6
  import { type AstReplaceChange, type AstReplaceFileChange, astEdit } from "@oh-my-pi/pi-natives";
6
7
  import type { Component } from "@oh-my-pi/pi-tui";
7
8
  import { replaceTabs, Text } from "@oh-my-pi/pi-tui";
8
9
  import { $envpos, prompt, untilAborted } from "@oh-my-pi/pi-utils";
9
- import { type } from "arktype";
10
10
  import { canonicalSnapshotKey, getFileSnapshotStore } from "../edit/file-snapshot-store";
11
11
  import { normalizeToLF } from "../edit/normalize";
12
12
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
@@ -1,12 +1,12 @@
1
1
  import * as path from "node:path";
2
2
  import { formatHashlineHeader } from "@oh-my-pi/hashline";
3
+ import { type } from "@oh-my-pi/omptype";
3
4
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
4
5
  import type { ToolExample } from "@oh-my-pi/pi-ai";
5
6
  import { type AstFindMatch, astGrep } from "@oh-my-pi/pi-natives";
6
7
  import type { Component } from "@oh-my-pi/pi-tui";
7
8
  import { Text } from "@oh-my-pi/pi-tui";
8
9
  import { prompt, untilAborted } from "@oh-my-pi/pi-utils";
9
- import { type } from "arktype";
10
10
  import { recordFileSnapshot, recordSeenLinesFromBody } from "../edit/file-snapshot-store";
11
11
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
12
12
  import type { Theme } from "../modes/theme/theme";
package/src/tools/bash.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import * as fs from "node:fs";
2
+ import { type } from "@oh-my-pi/omptype";
2
3
  import type {
3
4
  AgentTool,
4
5
  AgentToolContext,
@@ -9,7 +10,6 @@ import type {
9
10
  import type { Component } from "@oh-my-pi/pi-tui";
10
11
  import { ImageProtocol, TERMINAL } from "@oh-my-pi/pi-tui";
11
12
  import { getProjectDir, isEnoent, logger, prompt } from "@oh-my-pi/pi-utils";
12
- import { type } from "arktype";
13
13
  import type { Settings } from "../config/settings";
14
14
  import { applyDirenvPreflight, type BashResult, executeBash } from "../exec/bash-executor";
15
15
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
@@ -58,7 +58,65 @@ export const BASH_DEFAULT_PREVIEW_LINES = DEFAULT_TERMINAL_PREVIEW_LINES;
58
58
 
59
59
  const BASH_ENV_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
60
60
  const DEFAULT_AUTO_BACKGROUND_THRESHOLD_MS = 60_000;
61
- const BASH_APPROVAL_SHELL_CONTROL_RE = /[\n\r;&|<>`$()]/u;
61
+ const BASH_APPROVAL_SHELL_CONTROL_CHARS: Record<string, true> = {
62
+ "\n": true,
63
+ "\r": true,
64
+ ";": true,
65
+ "&": true,
66
+ "|": true,
67
+ "<": true,
68
+ ">": true,
69
+ "`": true,
70
+ $: true,
71
+ "(": true,
72
+ ")": true,
73
+ };
74
+ const BASH_APPROVAL_REINTERPRETED_ARGUMENT_RE = /(?:^|[ \t])(?:-[^-]*[ce]|--(?:command|eval))(?:[= \t]|$)/u;
75
+
76
+ function hasBashApprovalShellControl(command: string): boolean {
77
+ let quote: "'" | '"' | undefined;
78
+ let hasReinterpretableShellControl = false;
79
+ for (let i = 0; i < command.length; i++) {
80
+ const ch = command[i];
81
+ if (quote === "'") {
82
+ if (ch === "'") {
83
+ quote = undefined;
84
+ } else if (Object.hasOwn(BASH_APPROVAL_SHELL_CONTROL_CHARS, ch)) {
85
+ hasReinterpretableShellControl = true;
86
+ }
87
+ continue;
88
+ }
89
+ if (ch === "\\") {
90
+ const escaped = command[i + 1];
91
+ if (escaped && Object.hasOwn(BASH_APPROVAL_SHELL_CONTROL_CHARS, escaped)) {
92
+ hasReinterpretableShellControl = true;
93
+ }
94
+ i++;
95
+ continue;
96
+ }
97
+ if (quote === '"') {
98
+ if (ch === '"') {
99
+ quote = undefined;
100
+ continue;
101
+ }
102
+ // Expansion is active inside double quotes even in the original line.
103
+ if (ch === "`" || ch === "$") return true;
104
+ // Other control characters are literal here but become executable if a
105
+ // `-c`/`-e` option reinterprets the argument through another shell.
106
+ if (Object.hasOwn(BASH_APPROVAL_SHELL_CONTROL_CHARS, ch)) hasReinterpretableShellControl = true;
107
+ continue;
108
+ }
109
+ if (ch === "'" || ch === '"') {
110
+ quote = ch;
111
+ continue;
112
+ }
113
+ if (Object.hasOwn(BASH_APPROVAL_SHELL_CONTROL_CHARS, ch)) return true;
114
+ }
115
+ // Options such as `git -c alias.x='!...'` and `sh -c "..."` reinterpret
116
+ // otherwise literal quoted or escaped arguments as executable code.
117
+ return hasReinterpretableShellControl && BASH_APPROVAL_REINTERPRETED_ARGUMENT_RE.test(command);
118
+ }
119
+
62
120
  const BASH_PATTERN_APPROVAL_VALUES = new Set(["allow", "deny", "prompt"]);
63
121
 
64
122
  /**
@@ -218,7 +276,7 @@ function commandSegmentMatchesBashApprovalPattern(command: string, pattern: stri
218
276
  // `prompt` fire on any matching segment so they mean what they appear to.
219
277
  function bashApprovalRuleMatches(command: string, rule: BashApprovalPatternRule): boolean {
220
278
  if (rule.approval === "allow") {
221
- if (BASH_APPROVAL_SHELL_CONTROL_RE.test(command)) return false;
279
+ if (hasBashApprovalShellControl(command)) return false;
222
280
  return commandMatchesBashApprovalPattern(command, rule.match);
223
281
  }
224
282
  return commandSegmentMatchesBashApprovalPattern(command, rule.match);
@@ -254,6 +254,65 @@ export interface RunCmuxCodeOptions {
254
254
  snapshot: SessionSnapshot;
255
255
  }
256
256
 
257
+ interface ActiveCmuxRun {
258
+ filename: string;
259
+ floatingRejections: unknown[];
260
+ }
261
+
262
+ const RECENT_CMUX_RUN_FILES_MAX = 256;
263
+ const activeCmuxRuns = new Map<string, ActiveCmuxRun>();
264
+ const recentCmuxRunFiles = new Set<string>();
265
+
266
+ function consumeCmuxRunRejection(reason: unknown): boolean {
267
+ // cmux runs guest JS in the shared main-process realm (TTS/STT/MCP and other
268
+ // subsystems live here too), so — like the eval inline fallback — only a
269
+ // guest-file stack frame can safely attribute a rejection. A stackless or
270
+ // non-run-stack reason is indistinguishable from a subsystem failure and
271
+ // keeps the default fatal path; worker isolation is the long-term fix.
272
+ const stack = reason instanceof Error && typeof reason.stack === "string" ? reason.stack : undefined;
273
+ if (!stack) return false;
274
+
275
+ let owner: ActiveCmuxRun | undefined;
276
+ let ownerIndex = -1;
277
+ for (const run of activeCmuxRuns.values()) {
278
+ const index = stack.lastIndexOf(run.filename);
279
+ if (index > ownerIndex) {
280
+ ownerIndex = index;
281
+ owner = run;
282
+ }
283
+ }
284
+ if (owner) {
285
+ owner.floatingRejections.push(reason);
286
+ return true;
287
+ }
288
+
289
+ let recent: string | undefined;
290
+ let recentIndex = -1;
291
+ for (const filename of recentCmuxRunFiles) {
292
+ const index = stack.lastIndexOf(filename);
293
+ if (index > recentIndex) {
294
+ recentIndex = index;
295
+ recent = filename;
296
+ }
297
+ }
298
+ if (!recent) return false;
299
+ logger.warn("Unhandled rejection from a finished cmux browser run (missing await?)", {
300
+ filename: recent,
301
+ error: reason,
302
+ });
303
+ return true;
304
+ }
305
+
306
+ function rememberCmuxRunFile(filename: string): void {
307
+ recentCmuxRunFiles.delete(filename);
308
+ recentCmuxRunFiles.add(filename);
309
+ if (recentCmuxRunFiles.size <= RECENT_CMUX_RUN_FILES_MAX) return;
310
+ const oldest = recentCmuxRunFiles.values().next().value;
311
+ if (oldest !== undefined) recentCmuxRunFiles.delete(oldest);
312
+ }
313
+
314
+ postmortem.interceptUnhandledRejections(consumeCmuxRunRejection);
315
+
257
316
  export class CmuxTab {
258
317
  readonly #client: CmuxSocketClient;
259
318
  readonly #surfaceId: string;
@@ -1312,6 +1371,9 @@ export async function runCmuxCode(tab: CmuxTab, opts: RunCmuxCodeOptions): Promi
1312
1371
  const output = new RunOutput();
1313
1372
  const screenshots: ScreenshotResult[] = [];
1314
1373
  const runId = crypto.randomUUID();
1374
+ const filename = `cmux-run-${runId}.js`;
1375
+ const activeRun: ActiveCmuxRun = { filename, floatingRejections: [] };
1376
+ activeCmuxRuns.set(filename, activeRun);
1315
1377
  tab.setRunContext({ session: opts.snapshot, output, screenshots, signal, timeoutMs: opts.timeoutMs });
1316
1378
 
1317
1379
  const { promise: cancelRejection, reject } = Promise.withResolvers<never>();
@@ -1378,14 +1440,40 @@ export async function runCmuxCode(tab: CmuxTab, opts: RunCmuxCodeOptions): Promi
1378
1440
  };
1379
1441
  // Like the inline worker fallback, cmux runs user JS in-process: awaited cmux/tool calls
1380
1442
  // observe this abort signal, but a synchronous infinite loop cannot be interrupted here.
1381
- const returnValue = await Promise.race([
1382
- runtime.run(opts.code, `cmux-run-${runId}.js`, hooks, { runId, cwd: opts.snapshot.cwd }),
1383
- cancelRejection,
1384
- ]);
1443
+ let returnValue: unknown;
1444
+ let runError: unknown;
1445
+ let runFailed = false;
1446
+ try {
1447
+ returnValue = await Promise.race([
1448
+ runtime.run(opts.code, filename, hooks, { runId, cwd: opts.snapshot.cwd }),
1449
+ cancelRejection,
1450
+ ]);
1451
+ } catch (error) {
1452
+ runFailed = true;
1453
+ runError = error;
1454
+ }
1455
+ // Let rejection callbacks run while this run can still own guest-created promises.
1456
+ await Bun.sleep(0);
1457
+ if (runFailed) {
1458
+ for (const reason of activeRun.floatingRejections) {
1459
+ logger.warn("Unhandled rejection accompanied a failed cmux browser run", { filename, error: reason });
1460
+ }
1461
+ throw runError;
1462
+ }
1463
+ if (activeRun.floatingRejections.length > 0) {
1464
+ const messages = activeRun.floatingRejections.map(reason =>
1465
+ reason instanceof Error ? reason.message : String(reason),
1466
+ );
1467
+ throw new ToolError(`Unhandled rejection (missing await?): ${messages.join("\n[unhandled rejection] ")}`, {
1468
+ rejections: activeRun.floatingRejections,
1469
+ });
1470
+ }
1385
1471
  return { displays: output.finish(), returnValue: cloneSafe(returnValue), screenshots };
1386
1472
  } finally {
1387
1473
  signal.removeEventListener("abort", onAbort);
1388
1474
  runAc.abort(postmortem.markExpectedCleanupError(new ToolAbortError("Browser run ended")));
1475
+ activeCmuxRuns.delete(filename);
1476
+ rememberCmuxRunFile(filename);
1389
1477
  tab.clearRunContext();
1390
1478
  }
1391
1479
  }
@@ -1,7 +1,7 @@
1
+ import { type } from "@oh-my-pi/omptype";
1
2
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
2
3
  import type { ToolExample } from "@oh-my-pi/pi-ai";
3
4
  import { prompt, untilAborted } from "@oh-my-pi/pi-utils";
4
- import { type } from "arktype";
5
5
  import browserDescription from "../prompts/tools/browser.md" with { type: "text" };
6
6
  import type { ToolSession } from "../sdk";
7
7
  import { enforceInlineByteCap } from "../session/streaming-output";
@@ -1,6 +1,6 @@
1
+ import { type } from "@oh-my-pi/omptype";
1
2
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
2
3
  import { prompt } from "@oh-my-pi/pi-utils";
3
- import { type } from "arktype";
4
4
  import checkpointDescription from "../prompts/tools/checkpoint.md" with { type: "text" };
5
5
  import rewindDescription from "../prompts/tools/rewind.md" with { type: "text" };
6
6
  import type { ToolSession } from ".";
@@ -1,3 +1,4 @@
1
+ import { type Type, type } from "@oh-my-pi/omptype";
1
2
  import type {
2
3
  AgentTool,
3
4
  AgentToolContext,
@@ -9,7 +10,6 @@ import type { Model } from "@oh-my-pi/pi-ai";
9
10
  import { isClaudeModelId } from "@oh-my-pi/pi-catalog/identity";
10
11
  import type { DesktopCapabilities } from "@oh-my-pi/pi-natives";
11
12
  import { once, prompt } from "@oh-my-pi/pi-utils";
12
- import { type Type, type } from "arktype";
13
13
  import { callSessionTool } from "../eval/js/tool-bridge";
14
14
  import computerDescription from "../prompts/tools/computer.md" with { type: "text" };
15
15
  import { enforceInlineByteCap } from "../session/streaming-output";
@@ -1,4 +1,5 @@
1
1
  import * as fs from "node:fs/promises";
2
+ import { type } from "@oh-my-pi/omptype";
2
3
  import type {
3
4
  AgentTool,
4
5
  AgentToolContext,
@@ -10,7 +11,6 @@ import type {
10
11
  import type { ToolExample } from "@oh-my-pi/pi-ai";
11
12
  import { type Component, Text } from "@oh-my-pi/pi-tui";
12
13
  import { isEnoent, prompt } from "@oh-my-pi/pi-utils";
13
- import { type } from "arktype";
14
14
  import {
15
15
  type DapBreakpointRecord,
16
16
  type DapCapabilities,
package/src/tools/eval.ts CHANGED
@@ -1,7 +1,7 @@
1
+ import { type } from "@oh-my-pi/omptype";
1
2
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
2
3
  import type { ImageContent, ToolExample } from "@oh-my-pi/pi-ai";
3
4
  import { prompt } from "@oh-my-pi/pi-utils";
4
- import { type } from "arktype";
5
5
  import { jsBackend, juliaBackend, pythonBackend, rubyBackend } from "../eval";
6
6
  import type { ExecutorBackend, ExecutorBackendResult } from "../eval/backend";
7
7
  import { EVAL_TIMEOUT_PAUSE_OP, EVAL_TIMEOUT_RESUME_OP } from "../eval/bridge-timeout";
package/src/tools/gh.ts CHANGED
@@ -2,6 +2,7 @@ import * as fs from "node:fs/promises";
2
2
  import * as os from "node:os";
3
3
  import * as path from "node:path";
4
4
  import { scheduler } from "node:timers/promises";
5
+ import { type } from "@oh-my-pi/omptype";
5
6
  import type {
6
7
  AgentTool,
7
8
  AgentToolContext,
@@ -9,9 +10,7 @@ import type {
9
10
  AgentToolUpdateCallback,
10
11
  ToolApprovalDecision,
11
12
  } from "@oh-my-pi/pi-agent-core";
12
-
13
13
  import { getWorktreeDir, hashPath, isEnoent, logger, prompt, untilAborted } from "@oh-my-pi/pi-utils";
14
- import { type } from "arktype";
15
14
  import type { Settings } from "../config/settings";
16
15
  import githubDescription from "../prompts/tools/github.md" with { type: "text" };
17
16
  import * as git from "../utils/git";
package/src/tools/glob.ts CHANGED
@@ -1,12 +1,12 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
+ import { type } from "@oh-my-pi/omptype";
3
4
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
4
5
  import type { ToolExample } from "@oh-my-pi/pi-ai";
5
6
  import * as natives from "@oh-my-pi/pi-natives";
6
7
  import type { Component } from "@oh-my-pi/pi-tui";
7
8
  import { Text } from "@oh-my-pi/pi-tui";
8
9
  import { formatGroupedPaths, isEnoent, prompt, untilAborted } from "@oh-my-pi/pi-utils";
9
- import { type } from "arktype";
10
10
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
11
11
  import { InternalUrlRouter } from "../internal-urls";
12
12
  import { splitMemoryGlobPattern } from "../internal-urls/memory-protocol";
package/src/tools/grep.ts CHANGED
@@ -2,6 +2,7 @@ import { mkdtemp, rm, stat, writeFile } from "node:fs/promises";
2
2
  import { tmpdir } from "node:os";
3
3
  import * as path from "node:path";
4
4
  import { formatHashlineHeader } from "@oh-my-pi/hashline";
5
+ import { type } from "@oh-my-pi/omptype";
5
6
  import type {
6
7
  AgentTool,
7
8
  AgentToolContext,
@@ -13,7 +14,6 @@ import { type GrepMatch, GrepOutputMode, type GrepResult, grep } from "@oh-my-pi
13
14
  import type { Component } from "@oh-my-pi/pi-tui";
14
15
  import { Text } from "@oh-my-pi/pi-tui";
15
16
  import { prompt, untilAborted } from "@oh-my-pi/pi-utils";
16
- import { type } from "arktype";
17
17
  import { recordFileSnapshot, recordSeenLinesFromBody } from "../edit/file-snapshot-store";
18
18
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
19
19
  import type { LocalProtocolOptions } from "../internal-urls/local-protocol";
@@ -15,6 +15,7 @@
15
15
  * when the agent has nothing else to do.
16
16
  */
17
17
 
18
+ import { type } from "@oh-my-pi/omptype";
18
19
  import type {
19
20
  AgentTool,
20
21
  AgentToolContext,
@@ -25,7 +26,6 @@ import type {
25
26
  import type { ToolExample } from "@oh-my-pi/pi-ai";
26
27
  import type { Component } from "@oh-my-pi/pi-tui";
27
28
  import { prompt } from "@oh-my-pi/pi-utils";
28
- import { type } from "arktype";
29
29
  import type { RenderResultOptions } from "../../extensibility/custom-tools/types";
30
30
  import { IrcBus } from "../../irc/bus";
31
31
  import type { Theme } from "../../modes/theme/theme";