@oh-my-pi/pi-coding-agent 17.1.5 → 17.1.6

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 (207) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/{CHANGELOG-qs3vd6xf.md → CHANGELOG-x9zt79k8.md} +31 -0
  3. package/dist/cli.js +2989 -2978
  4. package/dist/types/capability/index.d.ts +1 -1
  5. package/dist/types/capability/types.d.ts +23 -1
  6. package/dist/types/config/settings-schema.d.ts +23 -1
  7. package/dist/types/internal-urls/mcp-protocol.d.ts +3 -2
  8. package/dist/types/internal-urls/parse.d.ts +12 -0
  9. package/dist/types/internal-urls/router.d.ts +6 -0
  10. package/dist/types/internal-urls/types.d.ts +6 -0
  11. package/dist/types/lsp/config.d.ts +1 -0
  12. package/dist/types/lsp/types.d.ts +2 -0
  13. package/dist/types/mcp/manager.d.ts +5 -0
  14. package/dist/types/mcp/tool-bridge.d.ts +13 -0
  15. package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -1
  16. package/dist/types/modes/rpc/rpc-mode.d.ts +2 -0
  17. package/dist/types/sdk.d.ts +3 -1
  18. package/dist/types/session/agent-session-types.d.ts +3 -1
  19. package/dist/types/session/agent-session.d.ts +2 -0
  20. package/dist/types/session/model-controls.d.ts +3 -0
  21. package/dist/types/session/session-advisors.d.ts +7 -1
  22. package/dist/types/session/tool-choice-queue.d.ts +6 -4
  23. package/dist/types/session/turn-recovery.d.ts +4 -2
  24. package/dist/types/task/index.d.ts +1 -1
  25. package/dist/types/task/types.d.ts +3 -11
  26. package/dist/types/thinking.d.ts +21 -2
  27. package/dist/types/utils/title-generator.d.ts +17 -16
  28. package/package.json +12 -12
  29. package/src/capability/index.ts +43 -12
  30. package/src/capability/mcp.ts +21 -0
  31. package/src/capability/types.ts +20 -1
  32. package/src/cli/read-cli.ts +44 -2
  33. package/src/config/settings-schema.ts +27 -1
  34. package/src/eval/py/runner.py +16 -2
  35. package/src/extensibility/extensions/runner.ts +91 -5
  36. package/src/extensibility/extensions/types.ts +0 -1
  37. package/src/extensibility/hooks/types.ts +0 -1
  38. package/src/internal-urls/mcp-protocol.ts +17 -3
  39. package/src/internal-urls/parse.ts +31 -0
  40. package/src/internal-urls/router.ts +24 -4
  41. package/src/internal-urls/types.ts +6 -0
  42. package/src/live/transport.ts +2 -2
  43. package/src/lsp/client.ts +2 -2
  44. package/src/lsp/config.ts +4 -0
  45. package/src/lsp/types.ts +2 -0
  46. package/src/mcp/config.ts +26 -14
  47. package/src/mcp/manager.ts +26 -9
  48. package/src/mcp/tool-bridge.ts +52 -1
  49. package/src/memories/index.ts +25 -6
  50. package/src/modes/components/status-line/segments.ts +3 -1
  51. package/src/modes/controllers/command-controller.ts +10 -10
  52. package/src/modes/controllers/extension-ui-controller.ts +3 -3
  53. package/src/modes/rpc/rpc-mode.ts +60 -47
  54. package/src/prompts/steering/user-interjection.md +2 -5
  55. package/src/prompts/tools/task.md +4 -2
  56. package/src/sdk.ts +17 -8
  57. package/src/session/agent-session-types.ts +3 -0
  58. package/src/session/agent-session.ts +21 -4
  59. package/src/session/model-controls.ts +43 -7
  60. package/src/session/session-advisors.ts +30 -14
  61. package/src/session/session-tools.ts +4 -2
  62. package/src/session/tool-choice-queue.ts +19 -4
  63. package/src/session/turn-recovery.ts +21 -2
  64. package/src/task/executor.ts +11 -3
  65. package/src/task/index.ts +43 -32
  66. package/src/task/types.ts +12 -17
  67. package/src/thinking.ts +68 -5
  68. package/src/tools/read.ts +2 -2
  69. package/src/utils/title-generator.ts +88 -34
  70. package/dist/types/advisor/__tests__/advisor.test.d.ts +0 -1
  71. package/dist/types/advisor/__tests__/config.test.d.ts +0 -1
  72. package/dist/types/advisor/__tests__/emission-guard.test.d.ts +0 -1
  73. package/dist/types/cli/__tests__/auth-gateway-catalog.test.d.ts +0 -1
  74. package/dist/types/cli/update-cli.test.d.ts +0 -1
  75. package/dist/types/config/__tests__/model-registry.test.d.ts +0 -1
  76. package/dist/types/eval/__tests__/agent-bridge.test.d.ts +0 -1
  77. package/dist/types/eval/__tests__/bridge-timeout.test.d.ts +0 -1
  78. package/dist/types/eval/__tests__/budget-bridge.test.d.ts +0 -1
  79. package/dist/types/eval/__tests__/completion-bridge.test.d.ts +0 -1
  80. package/dist/types/eval/__tests__/helpers-local-roots.test.d.ts +0 -1
  81. package/dist/types/eval/__tests__/idle-timeout.test.d.ts +0 -1
  82. package/dist/types/eval/__tests__/js-context-manager.test.d.ts +0 -1
  83. package/dist/types/eval/__tests__/julia-prelude.test.d.ts +0 -1
  84. package/dist/types/eval/__tests__/kernel-spawn.test.d.ts +0 -1
  85. package/dist/types/eval/__tests__/prelude-agent.test.d.ts +0 -1
  86. package/dist/types/eval/__tests__/process-entry-import.test.d.ts +0 -1
  87. package/dist/types/eval/py/__tests__/prelude.test.d.ts +0 -1
  88. package/dist/types/eval/py/__tests__/runner-shell-output.test.d.ts +0 -1
  89. package/dist/types/hindsight/client.test.d.ts +0 -1
  90. package/dist/types/internal-urls/__tests__/agent-protocol-nested.test.d.ts +0 -1
  91. package/dist/types/internal-urls/__tests__/ssh-protocol.test.d.ts +0 -1
  92. package/dist/types/launch/broker-list-order.test.d.ts +0 -1
  93. package/dist/types/launch/broker-output-snapshot.test.d.ts +0 -1
  94. package/dist/types/launch/protocol.test.d.ts +0 -1
  95. package/dist/types/launch/spawn-options.test.d.ts +0 -1
  96. package/dist/types/launch/terminal-output.test.d.ts +0 -1
  97. package/dist/types/live/protocol.test.d.ts +0 -1
  98. package/dist/types/mcp/config-writer.test.d.ts +0 -1
  99. package/dist/types/mcp/smithery-auth.test.d.ts +0 -1
  100. package/dist/types/mcp/smithery-registry.test.d.ts +0 -1
  101. package/dist/types/mcp/transports/stdio.test.d.ts +0 -1
  102. package/dist/types/modes/components/__tests__/dynamic-border.test.d.ts +0 -1
  103. package/dist/types/modes/components/__tests__/move-overlay.test.d.ts +0 -1
  104. package/dist/types/modes/components/__tests__/pause-screen.test.d.ts +0 -1
  105. package/dist/types/modes/components/__tests__/skill-message.test.d.ts +0 -1
  106. package/dist/types/modes/components/custom-editor-plugin-ctor.test.d.ts +0 -1
  107. package/dist/types/modes/components/custom-editor.test.d.ts +0 -1
  108. package/dist/types/modes/components/login-dialog.test.d.ts +0 -1
  109. package/dist/types/modes/components/status-line/component.jj-cache.test.d.ts +0 -1
  110. package/dist/types/modes/components/status-line/component.test.d.ts +0 -1
  111. package/dist/types/modes/components/tool-execution.test.d.ts +0 -1
  112. package/dist/types/modes/controllers/extension-ui-controller.test.d.ts +0 -1
  113. package/dist/types/modes/noninteractive-dispose.test.d.ts +0 -1
  114. package/dist/types/modes/print-mode.test.d.ts +0 -1
  115. package/dist/types/modes/session-teardown.test.d.ts +0 -1
  116. package/dist/types/modes/theme/mermaid-rendering.test.d.ts +0 -1
  117. package/dist/types/modes/utils/transcript-render-helpers.test.d.ts +0 -1
  118. package/dist/types/modes/warp-events.test.d.ts +0 -1
  119. package/dist/types/plan-mode/approved-plan-prompt.test.d.ts +0 -1
  120. package/dist/types/plan-mode/model-transition.test.d.ts +0 -1
  121. package/dist/types/plan-mode/reentry-prompt.test.d.ts +0 -1
  122. package/dist/types/session/agent-session-error-log.test.d.ts +0 -1
  123. package/dist/types/session/blob-store.test.d.ts +0 -1
  124. package/dist/types/session/messages.test.d.ts +0 -1
  125. package/dist/types/session/session-context.test.d.ts +0 -1
  126. package/dist/types/ssh/__tests__/connection-manager-args.test.d.ts +0 -1
  127. package/dist/types/ssh/__tests__/connection-manager-timeout.test.d.ts +0 -1
  128. package/dist/types/ssh/__tests__/file-transfer-posix-guard.test.d.ts +0 -1
  129. package/dist/types/ssh/__tests__/sshfs-mount.test.d.ts +0 -1
  130. package/dist/types/system-prompt.test.d.ts +0 -1
  131. package/dist/types/task/render.test.d.ts +0 -1
  132. package/dist/types/task/spawn-policy.test.d.ts +0 -1
  133. package/dist/types/tools/__tests__/eval-description.test.d.ts +0 -1
  134. package/dist/types/tools/__tests__/glob.test.d.ts +0 -1
  135. package/dist/types/tools/__tests__/json-tree.test.d.ts +0 -1
  136. package/dist/types/tools/__tests__/vibe-render.test.d.ts +0 -1
  137. package/dist/types/tools/hub/launch-compat.test.d.ts +0 -1
  138. package/dist/types/vibe/__tests__/token-rate.test.d.ts +0 -1
  139. package/src/advisor/__tests__/advisor.test.ts +0 -4889
  140. package/src/advisor/__tests__/config.test.ts +0 -349
  141. package/src/advisor/__tests__/emission-guard.test.ts +0 -147
  142. package/src/cli/__tests__/auth-gateway-catalog.test.ts +0 -111
  143. package/src/cli/update-cli.test.ts +0 -28
  144. package/src/config/__tests__/model-registry.test.ts +0 -182
  145. package/src/eval/__tests__/agent-bridge.test.ts +0 -1509
  146. package/src/eval/__tests__/bridge-timeout.test.ts +0 -170
  147. package/src/eval/__tests__/budget-bridge.test.ts +0 -80
  148. package/src/eval/__tests__/completion-bridge.test.ts +0 -412
  149. package/src/eval/__tests__/helpers-local-roots.test.ts +0 -55
  150. package/src/eval/__tests__/idle-timeout.test.ts +0 -80
  151. package/src/eval/__tests__/js-context-manager.test.ts +0 -456
  152. package/src/eval/__tests__/julia-prelude.test.ts +0 -66
  153. package/src/eval/__tests__/kernel-spawn.test.ts +0 -115
  154. package/src/eval/__tests__/prelude-agent.test.ts +0 -156
  155. package/src/eval/__tests__/process-entry-import.test.ts +0 -137
  156. package/src/eval/py/__tests__/prelude.test.ts +0 -104
  157. package/src/eval/py/__tests__/runner-shell-output.test.ts +0 -157
  158. package/src/hindsight/client.test.ts +0 -75
  159. package/src/internal-urls/__tests__/agent-protocol-nested.test.ts +0 -141
  160. package/src/internal-urls/__tests__/ssh-protocol.test.ts +0 -331
  161. package/src/launch/broker-list-order.test.ts +0 -89
  162. package/src/launch/broker-output-snapshot.test.ts +0 -126
  163. package/src/launch/protocol.test.ts +0 -59
  164. package/src/launch/spawn-options.test.ts +0 -31
  165. package/src/launch/terminal-output.test.ts +0 -107
  166. package/src/live/protocol.test.ts +0 -140
  167. package/src/mcp/config-writer.test.ts +0 -43
  168. package/src/mcp/smithery-auth.test.ts +0 -29
  169. package/src/mcp/smithery-registry.test.ts +0 -51
  170. package/src/mcp/transports/stdio.test.ts +0 -427
  171. package/src/modes/components/__tests__/dynamic-border.test.ts +0 -55
  172. package/src/modes/components/__tests__/move-overlay.test.ts +0 -252
  173. package/src/modes/components/__tests__/pause-screen.test.ts +0 -143
  174. package/src/modes/components/__tests__/skill-message.test.ts +0 -94
  175. package/src/modes/components/custom-editor-plugin-ctor.test.ts +0 -36
  176. package/src/modes/components/custom-editor.test.ts +0 -510
  177. package/src/modes/components/login-dialog.test.ts +0 -56
  178. package/src/modes/components/status-line/component.jj-cache.test.ts +0 -229
  179. package/src/modes/components/status-line/component.test.ts +0 -84
  180. package/src/modes/components/tool-execution.test.ts +0 -162
  181. package/src/modes/controllers/extension-ui-controller.test.ts +0 -250
  182. package/src/modes/noninteractive-dispose.test.ts +0 -73
  183. package/src/modes/print-mode.test.ts +0 -71
  184. package/src/modes/session-teardown.test.ts +0 -219
  185. package/src/modes/theme/mermaid-rendering.test.ts +0 -53
  186. package/src/modes/utils/transcript-render-helpers.test.ts +0 -38
  187. package/src/modes/warp-events.test.ts +0 -794
  188. package/src/plan-mode/approved-plan-prompt.test.ts +0 -36
  189. package/src/plan-mode/model-transition.test.ts +0 -60
  190. package/src/plan-mode/reentry-prompt.test.ts +0 -41
  191. package/src/session/agent-session-error-log.test.ts +0 -59
  192. package/src/session/blob-store.test.ts +0 -56
  193. package/src/session/messages.test.ts +0 -282
  194. package/src/session/session-context.test.ts +0 -384
  195. package/src/ssh/__tests__/connection-manager-args.test.ts +0 -191
  196. package/src/ssh/__tests__/connection-manager-timeout.test.ts +0 -61
  197. package/src/ssh/__tests__/file-transfer-posix-guard.test.ts +0 -105
  198. package/src/ssh/__tests__/sshfs-mount.test.ts +0 -13
  199. package/src/system-prompt.test.ts +0 -236
  200. package/src/task/render.test.ts +0 -290
  201. package/src/task/spawn-policy.test.ts +0 -62
  202. package/src/tools/__tests__/eval-description.test.ts +0 -18
  203. package/src/tools/__tests__/glob.test.ts +0 -37
  204. package/src/tools/__tests__/json-tree.test.ts +0 -35
  205. package/src/tools/__tests__/vibe-render.test.ts +0 -210
  206. package/src/tools/hub/launch-compat.test.ts +0 -40
  207. package/src/vibe/__tests__/token-rate.test.ts +0 -96
package/src/task/index.ts CHANGED
@@ -156,27 +156,31 @@ export function formatResultOutputFallback(result: Pick<SingleResult, "output" |
156
156
  return result.requests > 0 ? `(no output) after ${result.requests} req` : "(no output)";
157
157
  }
158
158
 
159
- /**
160
- * Render the tool description from a cached agent list and current settings.
161
- */
162
- function renderDescription(
163
- agents: AgentDefinition[],
164
- isolationEnabled: boolean,
165
- applyIsolatedChanges: boolean,
166
- disabledAgents: string[],
167
- batchEnabled: boolean,
168
- asyncEnabled: boolean,
169
- ircEnabled: boolean,
170
- parentSpawns: string,
171
- ): string {
172
- const spawnPolicy = resolveSpawnPolicy(parentSpawns);
159
+ interface TaskDescriptionOptions {
160
+ agents: AgentDefinition[];
161
+ isolationEnabled: boolean;
162
+ applyIsolatedChanges: boolean;
163
+ disabledAgents: string[];
164
+ batchEnabled: boolean;
165
+ effortEnabled: boolean;
166
+ asyncEnabled: boolean;
167
+ ircEnabled: boolean;
168
+ parentSpawns: string;
169
+ }
170
+
171
+ /** Render the tool description from a cached agent list and current settings. */
172
+ function renderDescription(options: TaskDescriptionOptions): string {
173
+ const spawnPolicy = resolveSpawnPolicy(options.parentSpawns);
173
174
  const spawningDisabled = !spawnPolicy.enabled;
174
- let filteredAgents = disabledAgents.length > 0 ? agents.filter(a => !disabledAgents.includes(a.name)) : agents;
175
+ let filteredAgents =
176
+ options.disabledAgents.length > 0
177
+ ? options.agents.filter(agent => !options.disabledAgents.includes(agent.name))
178
+ : options.agents;
175
179
  if (spawningDisabled) {
176
180
  filteredAgents = [];
177
181
  } else if (spawnPolicy.allowedAgents !== null) {
178
182
  const allowed = new Set(spawnPolicy.allowedAgents);
179
- filteredAgents = filteredAgents.filter(a => allowed.has(a.name));
183
+ filteredAgents = filteredAgents.filter(agent => allowed.has(agent.name));
180
184
  }
181
185
  const renderedAgents = filteredAgents.map(agent => ({
182
186
  name: agent.name,
@@ -188,12 +192,13 @@ function renderDescription(
188
192
  agents: renderedAgents,
189
193
  spawningDisabled,
190
194
  defaultAgent: spawnPolicy.defaultAgent,
191
- isolationEnabled,
192
- applyIsolatedChanges,
193
- batchEnabled,
194
- asyncEnabled,
195
+ isolationEnabled: options.isolationEnabled,
196
+ applyIsolatedChanges: options.applyIsolatedChanges,
197
+ batchEnabled: options.batchEnabled,
198
+ effortEnabled: options.effortEnabled,
199
+ asyncEnabled: options.asyncEnabled,
195
200
  hasBlockingAgents: renderedAgents.some(agent => agent.blocking),
196
- ircEnabled,
201
+ ircEnabled: options.ircEnabled,
197
202
  });
198
203
  }
199
204
 
@@ -581,28 +586,34 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
581
586
  const planMode = this.session.getPlanModeState?.()?.enabled === true;
582
587
  const isolationEnabled = !planMode && this.session.settings.get("task.isolation.mode") !== "none";
583
588
  const defaultAgent = resolveSpawnPolicy(this.session.getSessionSpawns()).defaultAgent;
584
- return getTaskSchema({ isolationEnabled, batchEnabled: this.#isBatchEnabled(), defaultAgent });
589
+ return getTaskSchema({
590
+ isolationEnabled,
591
+ batchEnabled: this.#isBatchEnabled(),
592
+ effortEnabled: this.session.settings.get("task.enableEffort"),
593
+ defaultAgent,
594
+ });
585
595
  }
586
596
 
587
597
  renderCall(args: unknown, options: Parameters<typeof renderTaskCall>[1], theme: Theme) {
588
598
  return renderTaskCall(repairTaskParams(args as TaskParams), options, theme);
589
599
  }
590
600
 
591
- /** Dynamic description that reflects current disabled-agent settings */
601
+ /** Dynamic description that reflects current task settings. */
592
602
  get description(): string {
593
603
  const disabledAgents = this.session.settings.get("task.disabledAgents") as string[];
594
604
  const planMode = this.session.getPlanModeState?.()?.enabled === true;
595
605
  const isolationMode = this.session.settings.get("task.isolation.mode");
596
- return renderDescription(
597
- this.#discoveredAgents,
598
- !planMode && isolationMode !== "none",
599
- this.session.settings.get("task.isolation.apply"),
606
+ return renderDescription({
607
+ agents: this.#discoveredAgents,
608
+ isolationEnabled: !planMode && isolationMode !== "none",
609
+ applyIsolatedChanges: this.session.settings.get("task.isolation.apply"),
600
610
  disabledAgents,
601
- this.#isBatchEnabled(),
602
- this.session.settings.get("async.enabled"),
603
- isIrcEnabled(this.session.settings, this.session.taskDepth ?? 0),
604
- this.session.getSessionSpawns() ?? "*",
605
- );
611
+ batchEnabled: this.#isBatchEnabled(),
612
+ effortEnabled: this.session.settings.get("task.enableEffort"),
613
+ asyncEnabled: this.session.settings.get("async.enabled"),
614
+ ircEnabled: isIrcEnabled(this.session.settings, this.session.taskDepth ?? 0),
615
+ parentSpawns: this.session.getSessionSpawns() ?? "*",
616
+ });
606
617
  }
607
618
  private constructor(
608
619
  private readonly session: ToolSession,
package/src/task/types.ts CHANGED
@@ -115,7 +115,6 @@ export const taskItemSchema = type({
115
115
  "name?": "string",
116
116
  agent: "string = 'task'",
117
117
  task: "string",
118
- "effort?": effortRule,
119
118
  "outputSchema?": outputSchemaInputSchema,
120
119
  "schemaMode?": '"permissive" | "strict"',
121
120
  "+": "delete",
@@ -124,7 +123,6 @@ const taskItemSchemaIsolated = type({
124
123
  "name?": "string",
125
124
  agent: "string = 'task'",
126
125
  task: "string",
127
- "effort?": effortRule,
128
126
  "outputSchema?": outputSchemaInputSchema,
129
127
  "schemaMode?": '"permissive" | "strict"',
130
128
  "isolated?": "boolean",
@@ -153,7 +151,6 @@ export const taskSchema = type({
153
151
  "name?": "string",
154
152
  agent: "string = 'task'",
155
153
  task: "string",
156
- "effort?": effortRule,
157
154
  "outputSchema?": outputSchemaInputSchema,
158
155
  "schemaMode?": '"permissive" | "strict"',
159
156
  "isolated?": "boolean",
@@ -163,7 +160,6 @@ const taskSchemaNoIsolation = type({
163
160
  "name?": "string",
164
161
  agent: "string = 'task'",
165
162
  task: "string",
166
- "effort?": effortRule,
167
163
  "outputSchema?": outputSchemaInputSchema,
168
164
  "schemaMode?": '"permissive" | "strict"',
169
165
  "+": "delete",
@@ -200,15 +196,17 @@ function createTaskSchema(options: {
200
196
  isolationEnabled: boolean;
201
197
  batchEnabled: boolean;
202
198
  defaultAgent: string;
199
+ effortEnabled: boolean;
203
200
  }): BaseType {
204
201
  const agent = taskAgentSchemaRule(options.defaultAgent);
202
+ const effortField = options.effortEnabled ? { "effort?": effortRule } : {};
205
203
  if (options.batchEnabled) {
206
204
  if (options.isolationEnabled) {
207
205
  const item = type.raw({
208
206
  "name?": "string",
209
207
  agent,
210
208
  task: "string",
211
- "effort?": effortRule,
209
+ ...effortField,
212
210
  "outputSchema?": outputSchemaInputSchema,
213
211
  "schemaMode?": '"permissive" | "strict"',
214
212
  "isolated?": "boolean",
@@ -224,7 +222,7 @@ function createTaskSchema(options: {
224
222
  "name?": "string",
225
223
  agent,
226
224
  task: "string",
227
- "effort?": effortRule,
225
+ ...effortField,
228
226
  "outputSchema?": outputSchemaInputSchema,
229
227
  "schemaMode?": '"permissive" | "strict"',
230
228
  "+": "delete",
@@ -240,7 +238,7 @@ function createTaskSchema(options: {
240
238
  "name?": "string",
241
239
  agent,
242
240
  task: "string",
243
- "effort?": effortRule,
241
+ ...effortField,
244
242
  "outputSchema?": outputSchemaInputSchema,
245
243
  "schemaMode?": '"permissive" | "strict"',
246
244
  "isolated?": "boolean",
@@ -251,33 +249,30 @@ function createTaskSchema(options: {
251
249
  "name?": "string",
252
250
  agent,
253
251
  task: "string",
254
- "effort?": effortRule,
252
+ ...effortField,
255
253
  "outputSchema?": outputSchemaInputSchema,
256
254
  "schemaMode?": '"permissive" | "strict"',
257
255
  "+": "delete",
258
256
  });
259
257
  }
260
258
 
261
- export function getTaskSchema(options: { isolationEnabled: boolean; batchEnabled: boolean }): DynamicTaskSchema;
262
- export function getTaskSchema(options: {
263
- isolationEnabled: boolean;
264
- batchEnabled: boolean;
265
- defaultAgent: string;
266
- }): TaskToolSchemaInstance;
259
+ /** Build the task wire schema for the current settings and spawn policy. */
267
260
  export function getTaskSchema(options: {
268
261
  isolationEnabled: boolean;
269
262
  batchEnabled: boolean;
263
+ effortEnabled?: boolean;
270
264
  defaultAgent?: string;
271
265
  }): TaskToolSchemaInstance {
272
266
  const defaultAgent = options.defaultAgent ?? "task";
273
- if (defaultAgent === "task") {
267
+ const effortEnabled = options.effortEnabled ?? false;
268
+ if (defaultAgent === "task" && !effortEnabled) {
274
269
  if (options.batchEnabled) return options.isolationEnabled ? taskSchemaBatch : taskSchemaBatchNoIsolation;
275
270
  return options.isolationEnabled ? taskSchema : taskSchemaNoIsolation;
276
271
  }
277
- const key = `${options.isolationEnabled ? "iso" : "flat"}:${options.batchEnabled ? "batch" : "single"}:${defaultAgent}`;
272
+ const key = `${options.isolationEnabled ? "iso" : "flat"}:${options.batchEnabled ? "batch" : "single"}:${effortEnabled ? "effort" : "default"}:${defaultAgent}`;
278
273
  const cached = taskSchemaCache.get(key);
279
274
  if (cached) return cached;
280
- const schema = createTaskSchema({ ...options, defaultAgent });
275
+ const schema = createTaskSchema({ ...options, effortEnabled, defaultAgent });
281
276
  taskSchemaCache.set(key, schema);
282
277
  return schema;
283
278
  }
package/src/thinking.ts CHANGED
@@ -271,19 +271,82 @@ export type TaskEffort = (typeof TASK_EFFORTS)[number];
271
271
  * at — high, xhigh, or max), `med` = the middle (lower of the two middles for
272
272
  * an even-sized range). Without a model, maps over the full canonical range.
273
273
  * Returns `undefined` when the model has no controllable effort surface, so
274
- * callers fall back to their default selector (e.g. `auto`).
274
+ * callers fall back to their default selector (e.g. `auto`). Throws when the
275
+ * configured ceiling is below the model's lowest supported effort.
275
276
  */
276
- export function resolveTaskEffortLevel(model: Model | undefined, effort: TaskEffort): Effort | undefined {
277
+ export function resolveTaskEffortLevel(
278
+ model: Model | undefined,
279
+ effort: TaskEffort,
280
+ maxEffort?: Effort,
281
+ ): Effort | undefined {
277
282
  const supported = model ? getSupportedEfforts(model) : THINKING_EFFORTS;
278
283
  if (supported.length === 0) return undefined;
284
+ let resolved: Effort;
279
285
  switch (effort) {
280
286
  case "lo":
281
- return supported[0];
287
+ resolved = supported[0];
288
+ break;
282
289
  case "med":
283
- return supported[(supported.length - 1) >> 1];
290
+ resolved = supported[(supported.length - 1) >> 1];
291
+ break;
284
292
  case "hi":
285
- return supported[supported.length - 1];
293
+ resolved = supported[supported.length - 1];
294
+ break;
295
+ }
296
+ if (maxEffort === undefined) return resolved;
297
+ const maxIndex = THINKING_EFFORTS.indexOf(maxEffort);
298
+ const ceiling = supported.findLast(candidate => THINKING_EFFORTS.indexOf(candidate) <= maxIndex);
299
+ if (ceiling === undefined) {
300
+ const modelName = model ? `${model.provider}/${model.id}` : "Selected model";
301
+ throw new RangeError(`${modelName} has no supported thinking effort at or below task.maxEffort=${maxEffort}`);
286
302
  }
303
+ return THINKING_EFFORTS.indexOf(resolved) > THINKING_EFFORTS.indexOf(ceiling) ? ceiling : resolved;
304
+ }
305
+
306
+ /**
307
+ * Clamps a concrete thinking selector to a per-session effort ceiling (e.g. a
308
+ * task spawn's `task.maxEffort`-capped effort hint). `off`/`inherit`/
309
+ * `undefined` pass through, as do levels already at or below the ceiling.
310
+ * Levels above it snap to the highest model-supported effort at or below the
311
+ * ceiling. A model whose floor exceeds the ceiling has nothing valid to snap
312
+ * to; the requested level is returned unchanged and the caller is responsible
313
+ * for rejecting or skipping such models (see {@link modelSupportsEffortCeiling}).
314
+ */
315
+ export function clampThinkingLevelToCeiling(
316
+ model: Model | undefined,
317
+ level: Effort | undefined,
318
+ ceiling: Effort | undefined,
319
+ ): Effort | undefined;
320
+ export function clampThinkingLevelToCeiling(
321
+ model: Model | undefined,
322
+ level: ThinkingLevel | undefined,
323
+ ceiling: Effort | undefined,
324
+ ): ThinkingLevel | undefined;
325
+ export function clampThinkingLevelToCeiling(
326
+ model: Model | undefined,
327
+ level: ThinkingLevel | undefined,
328
+ ceiling: Effort | undefined,
329
+ ): ThinkingLevel | undefined {
330
+ if (ceiling === undefined || level === undefined || level === ThinkingLevel.Off || level === ThinkingLevel.Inherit) {
331
+ return level;
332
+ }
333
+ const maxIndex = THINKING_EFFORTS.indexOf(ceiling);
334
+ if (THINKING_EFFORTS.indexOf(level) <= maxIndex) return level;
335
+ const supported = model ? getSupportedEfforts(model) : THINKING_EFFORTS;
336
+ return supported.findLast(candidate => THINKING_EFFORTS.indexOf(candidate) <= maxIndex) ?? level;
337
+ }
338
+
339
+ /**
340
+ * True when `model` can honor a thinking-effort ceiling: it either has no
341
+ * controllable effort surface (nothing to cap) or supports at least one effort
342
+ * at or below the ceiling. Retry-fallback candidate filtering uses this to
343
+ * skip models whose floor would force the session above the ceiling.
344
+ */
345
+ export function modelSupportsEffortCeiling(model: Model, ceiling: Effort): boolean {
346
+ const supported = getSupportedEfforts(model);
347
+ if (supported.length === 0) return true;
348
+ const maxIndex = THINKING_EFFORTS.indexOf(ceiling);
349
+ return supported.some(candidate => THINKING_EFFORTS.indexOf(candidate) <= maxIndex);
287
350
  }
288
351
 
289
352
  /**
package/src/tools/read.ts CHANGED
@@ -2250,12 +2250,12 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
2250
2250
  return executeReadUrl(this.session, { path: parsedUrlTarget.path, raw: urlRaw }, signal);
2251
2251
  }
2252
2252
 
2253
- // Handle internal URLs (agent://, artifact://, memory://, skill://, rule://, local://, mcp://, omp://, issue://, pr://).
2253
+ // Handle native OMP URLs and custom-scheme resources advertised by MCP servers.
2254
2254
  // Use the internal-URL-aware splitter so malformed selectors are peeled
2255
2255
  // off the URL and surfaced via parseSel rather than confusing handlers.
2256
2256
  const internalRouter = InternalUrlRouter.instance();
2257
2257
  let promotedSelector: string | undefined;
2258
- if (internalRouter.canHandle(readPath)) {
2258
+ if (internalRouter.canResolve(readPath)) {
2259
2259
  const internalTarget = splitInternalUrlSel(readPath);
2260
2260
  const parsed = parseSel(internalTarget.sel);
2261
2261
  if (internalTarget.sel !== undefined && parsed.kind === "none") {
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Generate session titles using a smol, fast model.
3
3
  */
4
+ import { dlopen, FFIType, ptr } from "bun:ffi";
4
5
  import * as path from "node:path";
5
6
 
6
7
  import { type Api, type AssistantMessage, completeSimple, type Model } from "@oh-my-pi/pi-ai";
@@ -23,6 +24,59 @@ const TITLE_MARKER_INSTRUCTION = prompt.render(titleMarkerInstruction);
23
24
  const DEFAULT_TERMINAL_TITLE = "π";
24
25
  const TERMINAL_TITLE_CONTROL_CHARS = /[\u0000-\u001f\u007f-\u009f]/g;
25
26
 
27
+ interface WindowsConsoleTitleApi {
28
+ set(title: string): boolean;
29
+ close(): void;
30
+ }
31
+
32
+ let windowsConsoleTitleApi: WindowsConsoleTitleApi | null | undefined;
33
+ let lastTerminalTitle: string | undefined;
34
+
35
+ function getWindowsConsoleTitleApi(): WindowsConsoleTitleApi | null {
36
+ if (process.platform !== "win32") return null;
37
+ if (windowsConsoleTitleApi !== undefined) return windowsConsoleTitleApi;
38
+ try {
39
+ const kernel32 = dlopen("kernel32.dll", {
40
+ SetConsoleTitleW: { args: [FFIType.ptr], returns: FFIType.bool },
41
+ });
42
+ windowsConsoleTitleApi = {
43
+ set(title) {
44
+ const wideTitle = Buffer.from(`${title}\0`, "utf16le");
45
+ return kernel32.symbols.SetConsoleTitleW(ptr(wideTitle));
46
+ },
47
+ close: () => kernel32.close(),
48
+ };
49
+ } catch {
50
+ windowsConsoleTitleApi = null;
51
+ }
52
+ return windowsConsoleTitleApi;
53
+ }
54
+
55
+ function setWindowsConsoleTitle(title: string): boolean {
56
+ const api = getWindowsConsoleTitleApi();
57
+ if (!api) return false;
58
+ try {
59
+ return api.set(title);
60
+ } catch {
61
+ try {
62
+ api.close();
63
+ } catch {
64
+ // Ignore cleanup failures after the native title path has already failed.
65
+ }
66
+ windowsConsoleTitleApi = null;
67
+ return false;
68
+ }
69
+ }
70
+
71
+ function disposeWindowsConsoleTitleApi(): void {
72
+ try {
73
+ windowsConsoleTitleApi?.close();
74
+ } catch {
75
+ // Terminal teardown must remain best-effort.
76
+ }
77
+ windowsConsoleTitleApi = undefined;
78
+ }
79
+
26
80
  // Cover the "backend ignores `disableReasoning`" case unconditionally: the
27
81
  // static `model.reasoning` catalog flag can't distinguish a thinking model that
28
82
  // was declared with `reasoning: false` (e.g. Qwen3 served locally via llama.cpp,
@@ -379,11 +433,16 @@ export function formatSessionTerminalTitle(sessionName: string | undefined, cwd?
379
433
  }
380
434
 
381
435
  /**
382
- * Set the terminal title using OSC 0 (sets both tab and window title). Unsupported terminals ignore it.
436
+ * Set the terminal title through the native Win32 API or OSC 0.
437
+ *
438
+ * Repeating the same sanitized title is a no-op on every platform.
383
439
  */
384
440
  export function setTerminalTitle(title: string): void {
385
441
  if (!process.stdout.isTTY || isTerminalHeadless()) return;
386
- process.stdout.write(`\x1b]0;${sanitizeTerminalTitlePart(title) ?? DEFAULT_TERMINAL_TITLE}\x07`);
442
+ const next = sanitizeTerminalTitlePart(title) ?? DEFAULT_TERMINAL_TITLE;
443
+ if (next === lastTerminalTitle) return;
444
+ if (!setWindowsConsoleTitle(next)) process.stdout.write(`\x1b]0;${next}\x07`);
445
+ lastTerminalTitle = next;
387
446
  }
388
447
 
389
448
  export function setSessionTerminalTitle(sessionName: string | undefined, cwd?: string): void {
@@ -396,9 +455,9 @@ export function setSessionTerminalTitle(sessionName: string | undefined, cwd?: s
396
455
 
397
456
  /**
398
457
  * Set a terminal title from an extension's `setTitle()`. Unlike the session base
399
- * title, this owns the terminal verbatim: the run-state separator will not rewrite
400
- * it on the next spinner tick or state change. Cleared when the app next sets an
401
- * authoritative session title via {@link setSessionTerminalTitle}.
458
+ * title, this owns the terminal verbatim: periodic and run-state updates will not
459
+ * rewrite it. Cleared when the app next sets an authoritative session title via
460
+ * {@link setSessionTerminalTitle}.
402
461
  */
403
462
  export function setExtensionTerminalTitle(title: string): void {
404
463
  terminalTitleRuntime.extensionOverride = title;
@@ -407,12 +466,9 @@ export function setExtensionTerminalTitle(title: string): void {
407
466
 
408
467
  export type TerminalTitleState = "idle" | "working" | "attention";
409
468
 
410
- /** Separator glyphs carrying the run state between the `π` brand and the session
411
- * label the brand itself stays bare (no prefix glyph). Spinner frames animate
412
- * the separator while working; they are self-contained (not the theme's symbol
413
- * set) to avoid a utils→modes import cycle; OSC titles render in tab/window bars
414
- * that handle Unicode. */
415
- const TITLE_SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] as const;
469
+ /** Windows uses a static working separator instead of scheduling title animation. */
470
+ const WINDOWS_TITLE_WORKING_SEPARATOR = ":";
471
+ const TITLE_SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
416
472
  const TITLE_SPINNER_INTERVAL_MS = 80;
417
473
  /** The user's turn: the title reads like a shell prompt awaiting input. */
418
474
  const TITLE_IDLE_SEPARATOR = ">";
@@ -425,7 +481,6 @@ const terminalTitleRuntime: {
425
481
  frame: number;
426
482
  enabled: boolean;
427
483
  timer: NodeJS.Timeout | undefined;
428
- lastEmitted: string | undefined;
429
484
  /** A title an extension set via `setTitle()`. While set, it owns the terminal
430
485
  * title verbatim: the run-state separator never rewrites it. Cleared when the
431
486
  * app next establishes an authoritative session title (rename, new session,
@@ -437,19 +492,16 @@ const terminalTitleRuntime: {
437
492
  frame: 0,
438
493
  enabled: true,
439
494
  timer: undefined,
440
- lastEmitted: undefined,
441
495
  extensionOverride: undefined,
442
496
  };
443
497
 
444
498
  /**
445
- * Compose the terminal title from the `π` brand, a state-carrying SEPARATOR, and
446
- * the session label. The brand never gains a prefix glyph the separator slot
447
- * expresses the run state instead. Pure (no I/O) so the state→separator contract
448
- * is unit-testable:
449
- * - `idle` (user's turn): > label` — reads like a prompt awaiting input;
450
- * - `working`: `π label` — spinner frames animate the separator;
451
- * - `attention`: `π ! label` — agent blocked on the user;
452
- * - disabled: `π: label` — the pre-state layout.
499
+ * Compose the terminal title from the `π` brand, a state-carrying separator, and
500
+ * the session label. Pure (no I/O) so the state→separator contract is testable:
501
+ * - `idle` (user's turn): `π > label`;
502
+ * - `working`: `π ⠋ label` (`π : label` on Windows);
503
+ * - `attention`: ! label`;
504
+ * - disabled: `π: label`.
453
505
  * Without a label the separator trails the brand (`π >`) so the state stays visible.
454
506
  */
455
507
  export function buildTerminalTitleWithState(
@@ -457,11 +509,14 @@ export function buildTerminalTitleWithState(
457
509
  state: TerminalTitleState,
458
510
  frame: number,
459
511
  enabled: boolean,
512
+ platform: NodeJS.Platform = process.platform,
460
513
  ): string {
461
514
  if (!enabled) return label ? `${DEFAULT_TERMINAL_TITLE}: ${label}` : DEFAULT_TERMINAL_TITLE;
462
515
  const separator =
463
516
  state === "working"
464
- ? TITLE_SPINNER_FRAMES[frame % TITLE_SPINNER_FRAMES.length]
517
+ ? platform === "win32"
518
+ ? WINDOWS_TITLE_WORKING_SEPARATOR
519
+ : TITLE_SPINNER_FRAMES[frame % TITLE_SPINNER_FRAMES.length]
465
520
  : state === "attention"
466
521
  ? TITLE_ATTENTION_SEPARATOR
467
522
  : TITLE_IDLE_SEPARATOR;
@@ -469,8 +524,8 @@ export function buildTerminalTitleWithState(
469
524
  }
470
525
 
471
526
  function emitTerminalTitle(): void {
472
- // An extension override owns the terminal verbatim; the run-state separator and
473
- // spinner ticks must not clobber it (still deduped via lastEmitted).
527
+ // An extension override owns the terminal verbatim; the terminal sink
528
+ // deduplicates repeated state updates.
474
529
  const next =
475
530
  terminalTitleRuntime.extensionOverride ??
476
531
  buildTerminalTitleWithState(
@@ -479,20 +534,16 @@ function emitTerminalTitle(): void {
479
534
  terminalTitleRuntime.frame,
480
535
  terminalTitleRuntime.enabled,
481
536
  );
482
- if (next === terminalTitleRuntime.lastEmitted) return;
483
- terminalTitleRuntime.lastEmitted = next;
484
537
  setTerminalTitle(next);
485
538
  }
486
539
 
487
540
  function stopTerminalTitleSpinner(): void {
488
- if (terminalTitleRuntime.timer) {
489
- clearInterval(terminalTitleRuntime.timer);
490
- terminalTitleRuntime.timer = undefined;
491
- }
541
+ clearInterval(terminalTitleRuntime.timer);
542
+ terminalTitleRuntime.timer = undefined;
492
543
  }
493
544
 
494
545
  function startTerminalTitleSpinner(): void {
495
- if (terminalTitleRuntime.timer || !process.stdout.isTTY) return;
546
+ if (process.platform === "win32" || terminalTitleRuntime.timer || !process.stdout.isTTY) return;
496
547
  terminalTitleRuntime.timer = setInterval(() => {
497
548
  terminalTitleRuntime.frame = (terminalTitleRuntime.frame + 1) % TITLE_SPINNER_FRAMES.length;
498
549
  emitTerminalTitle();
@@ -503,8 +554,9 @@ function startTerminalTitleSpinner(): void {
503
554
 
504
555
  /**
505
556
  * Reflect the agent run state in the terminal title's separator: `working`
506
- * animates spinner frames in the separator slot, `idle` shows `>` (your turn),
507
- * `attention` shows `!` (agent blocked on you). Gated off by `tui.titleState`.
557
+ * animates outside Windows and stays `:` on Windows, `idle` shows `>` (your
558
+ * turn), and `attention` shows `!` (agent blocked on you). Gated off by
559
+ * `tui.titleState`.
508
560
  */
509
561
  export function setTerminalTitleState(state: TerminalTitleState): void {
510
562
  terminalTitleRuntime.state = state;
@@ -521,9 +573,11 @@ export function setTerminalTitleStateEnabled(enabled: boolean): void {
521
573
  emitTerminalTitle();
522
574
  }
523
575
 
524
- /** Stop the spinner timer; call on session/UI teardown. */
576
+ /** Release terminal-title runtime resources. */
525
577
  export function disposeTerminalTitleState(): void {
526
578
  stopTerminalTitleSpinner();
579
+ disposeWindowsConsoleTitleApi();
580
+ lastTerminalTitle = undefined;
527
581
  }
528
582
 
529
583
  /**
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};