@oh-my-pi/pi-coding-agent 16.1.19 → 16.1.21

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 (59) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/cli.js +3795 -3760
  3. package/dist/types/advisor/advise-tool.d.ts +3 -0
  4. package/dist/types/cli/gallery-cli.d.ts +6 -0
  5. package/dist/types/commands/gallery.d.ts +1 -1
  6. package/dist/types/config/service-tier.d.ts +34 -0
  7. package/dist/types/config/settings-schema.d.ts +36 -33
  8. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +2 -0
  9. package/dist/types/mcp/oauth-flow.d.ts +42 -5
  10. package/dist/types/modes/components/custom-editor.d.ts +32 -0
  11. package/dist/types/modes/components/tool-execution.d.ts +5 -5
  12. package/dist/types/modes/controllers/event-controller.d.ts +10 -0
  13. package/dist/types/modes/controllers/input-controller.d.ts +2 -1
  14. package/dist/types/session/agent-session.d.ts +2 -0
  15. package/dist/types/task/executor.d.ts +9 -1
  16. package/dist/types/task/parallel.d.ts +17 -1
  17. package/dist/types/tools/index.d.ts +3 -1
  18. package/dist/types/utils/clipboard.d.ts +10 -0
  19. package/package.json +13 -13
  20. package/scripts/generate-legacy-pi-bundled-registry.ts +10 -0
  21. package/src/advisor/__tests__/advisor.test.ts +44 -0
  22. package/src/advisor/advise-tool.ts +33 -0
  23. package/src/autolearn/controller.ts +17 -2
  24. package/src/cli/gallery-cli.ts +31 -2
  25. package/src/cli/gallery-fixtures/agentic.ts +13 -4
  26. package/src/commands/gallery.ts +11 -3
  27. package/src/config/service-tier.ts +87 -0
  28. package/src/config/settings-schema.ts +48 -23
  29. package/src/eval/agent-bridge.ts +2 -0
  30. package/src/extensibility/plugins/legacy-pi-bundled-keys.ts +1 -1
  31. package/src/extensibility/plugins/legacy-pi-bundled-registry.ts +4 -4
  32. package/src/extensibility/plugins/legacy-pi-compat.ts +193 -5
  33. package/src/main.ts +1 -0
  34. package/src/mcp/manager.ts +12 -3
  35. package/src/mcp/oauth-flow.ts +121 -7
  36. package/src/mcp/transports/stdio.ts +5 -0
  37. package/src/modes/components/chat-transcript-builder.ts +31 -0
  38. package/src/modes/components/custom-editor.test.ts +80 -0
  39. package/src/modes/components/custom-editor.ts +86 -6
  40. package/src/modes/components/tool-execution.ts +50 -25
  41. package/src/modes/controllers/event-controller.ts +57 -8
  42. package/src/modes/controllers/input-controller.ts +213 -93
  43. package/src/modes/controllers/mcp-command-controller.ts +18 -2
  44. package/src/modes/utils/ui-helpers.ts +40 -0
  45. package/src/prompts/system/autolearn-nudge-autocontinue.md +5 -0
  46. package/src/prompts/system/autolearn-nudge.md +4 -2
  47. package/src/prompts/tools/todo.md +1 -1
  48. package/src/sdk.ts +1 -0
  49. package/src/session/agent-session.ts +76 -15
  50. package/src/session/session-history-format.ts +21 -4
  51. package/src/task/executor.ts +79 -2
  52. package/src/task/index.ts +11 -6
  53. package/src/task/parallel.ts +59 -7
  54. package/src/tools/index.ts +3 -1
  55. package/src/tools/irc.ts +16 -2
  56. package/src/tools/todo.ts +20 -10
  57. package/src/utils/clipboard.ts +57 -0
  58. package/src/utils/shell-snapshot-fn-env.sh +60 -0
  59. package/src/utils/shell-snapshot.ts +77 -20
@@ -84,6 +84,7 @@ export declare function deriveAdvisorTelemetry(primaryTelemetry: AgentTelemetryC
84
84
  */
85
85
  export declare const ADVISOR_READONLY_TOOL_NAMES: ReadonlySet<string>;
86
86
  export declare class AdviseTool implements AgentTool<typeof adviseSchema, AdviseDetails> {
87
+ #private;
87
88
  private readonly onAdvice;
88
89
  readonly name = "advise";
89
90
  readonly label = "Advise";
@@ -94,6 +95,8 @@ export declare class AdviseTool implements AgentTool<typeof adviseSchema, Advise
94
95
  }, {}>;
95
96
  readonly intent: "omit";
96
97
  constructor(onAdvice: (note: string, severity?: AdviseDetails["severity"]) => void);
98
+ /** Clear delivered-note memory when the advisor starts a fresh conversation. */
99
+ resetDeliveredNotes(): void;
97
100
  execute(_toolCallId: string, args: AdviseParams, _signal?: AbortSignal, _onUpdate?: AgentToolUpdateCallback<AdviseDetails>, _context?: AgentToolContext): Promise<AgentToolResult<AdviseDetails>>;
98
101
  }
99
102
  export {};
@@ -2,6 +2,12 @@ import { type GalleryFixture } from "./gallery-fixtures";
2
2
  /** Lifecycle states the gallery renders, in display order. */
3
3
  export declare const GALLERY_STATES: readonly ["streaming", "progress", "success", "error"];
4
4
  export type GalleryState = (typeof GALLERY_STATES)[number];
5
+ /** User-facing labels printed above each rendered lifecycle state. */
6
+ export declare const GALLERY_STATE_LABELS: Record<GalleryState, string>;
7
+ /** Accepted `--state` tokens, including legacy lifecycle names and displayed labels. */
8
+ export declare const GALLERY_STATE_TOKENS: string[];
9
+ /** Normalize user-provided `--state` tokens to the internal gallery lifecycle states. */
10
+ export declare function parseGalleryStates(states: readonly string[] | undefined): GalleryState[] | undefined;
5
11
  export interface GalleryCommandArgs {
6
12
  /** Render width in columns (defaults to terminal width, clamped). */
7
13
  width?: number;
@@ -12,7 +12,7 @@ export default class Gallery extends Command {
12
12
  state: import("@oh-my-pi/pi-utils/cli").FlagDescriptor<"string"> & {
13
13
  char: string;
14
14
  description: string;
15
- options: ("error" | "progress" | "streaming" | "success")[];
15
+ options: string[];
16
16
  multiple: true;
17
17
  };
18
18
  width: import("@oh-my-pi/pi-utils/cli").FlagDescriptor<"integer"> & {
@@ -0,0 +1,34 @@
1
+ import type { ServiceTier } from "@oh-my-pi/pi-ai";
2
+ import type { SubmenuOption } from "./settings-schema";
3
+ /**
4
+ * Service-tier setting values shared by every "Service Tier" setting. `"none"`
5
+ * is the omit-the-parameter sentinel; the remaining values mirror
6
+ * {@link ServiceTier}.
7
+ */
8
+ export declare const SERVICE_TIER_SETTING_VALUES: readonly ["none", "auto", "default", "flex", "scale", "priority", "openai-only", "claude-only"];
9
+ export type ServiceTierSettingValue = (typeof SERVICE_TIER_SETTING_VALUES)[number];
10
+ /** Variant value set for scoped service-tier settings (subagent/advisor) that can defer to the main agent. */
11
+ export declare const SERVICE_TIER_INHERIT_SETTING_VALUES: readonly ["inherit", "none", "auto", "default", "flex", "scale", "priority", "openai-only", "claude-only"];
12
+ export type ServiceTierInheritSettingValue = (typeof SERVICE_TIER_INHERIT_SETTING_VALUES)[number];
13
+ /** Submenu descriptions shared by the base `serviceTier` setting. */
14
+ export declare const SERVICE_TIER_OPTIONS: ReadonlyArray<SubmenuOption<ServiceTierSettingValue>>;
15
+ /** Submenu descriptions for inherit-capable service-tier settings. */
16
+ export declare const SERVICE_TIER_INHERIT_OPTIONS: ReadonlyArray<SubmenuOption<ServiceTierInheritSettingValue>>;
17
+ /**
18
+ * Resolve a service-tier setting value to the wire {@link ServiceTier} (or
19
+ * `undefined` to omit). `"inherit"` defers to `inherited`; `"none"` omits.
20
+ */
21
+ export declare function resolveServiceTierSetting(value: string, inherited: ServiceTier | undefined): ServiceTier | undefined;
22
+ /**
23
+ * Resolve the `serviceTier` *setting value* to stamp onto a subagent's settings
24
+ * snapshot.
25
+ *
26
+ * - A concrete `subagentSetting` (`"none"` or a tier) wins outright.
27
+ * - `"inherit"` defers to the parent's live effective tier when the caller has a
28
+ * live session (`inherited` passed as `ServiceTier | null`, where `null` means
29
+ * the parent explicitly has no tier — e.g. `/fast off`). When no live session
30
+ * is available (`inherited === undefined`, e.g. cold subagent revive) it falls
31
+ * back to the parent's configured `serviceTier` setting so behavior matches a
32
+ * plain settings snapshot.
33
+ */
34
+ export declare function resolveSubagentServiceTier(subagentSetting: string, configuredTier: ServiceTierSettingValue, inherited: ServiceTier | null | undefined): ServiceTierSettingValue;
@@ -1153,39 +1153,32 @@ export declare const SETTINGS_SCHEMA: {
1153
1153
  readonly group: "Sampling";
1154
1154
  readonly label: "Service Tier";
1155
1155
  readonly description: 'Processing priority hint (none = omit). OpenAI accepts the tier values directly; Anthropic realizes `priority` as `speed: "fast"` on supported Opus models. Scoped values target one family.';
1156
- readonly options: readonly [{
1157
- readonly value: "none";
1158
- readonly label: "None";
1159
- readonly description: "Omit service_tier parameter";
1160
- }, {
1161
- readonly value: "auto";
1162
- readonly label: "Auto";
1163
- readonly description: "Use provider default tier selection (OpenAI)";
1164
- }, {
1165
- readonly value: "default";
1166
- readonly label: "Default";
1167
- readonly description: "Standard priority processing (OpenAI)";
1168
- }, {
1169
- readonly value: "flex";
1170
- readonly label: "Flex";
1171
- readonly description: "Flexible capacity tier when available (OpenAI)";
1172
- }, {
1173
- readonly value: "scale";
1174
- readonly label: "Scale";
1175
- readonly description: "Scale Tier credits when available (OpenAI)";
1176
- }, {
1177
- readonly value: "priority";
1178
- readonly label: "Priority";
1179
- readonly description: "Priority on every supported provider (OpenAI `service_tier`, Anthropic fast mode)";
1180
- }, {
1181
- readonly value: "openai-only";
1182
- readonly label: "Priority (OpenAI only)";
1183
- readonly description: "Priority on OpenAI/OpenAI-Codex requests; ignored elsewhere";
1184
- }, {
1185
- readonly value: "claude-only";
1186
- readonly label: "Priority (Claude only)";
1187
- readonly description: "Anthropic fast mode on direct Claude requests; ignored elsewhere (incl. Bedrock/Vertex)";
1188
- }];
1156
+ readonly options: readonly SubmenuOption<"auto" | "claude-only" | "default" | "flex" | "none" | "openai-only" | "priority" | "scale">[];
1157
+ };
1158
+ };
1159
+ readonly serviceTierSubagent: {
1160
+ readonly type: "enum";
1161
+ readonly values: readonly ["inherit", "none", "auto", "default", "flex", "scale", "priority", "openai-only", "claude-only"];
1162
+ readonly default: "inherit";
1163
+ readonly ui: {
1164
+ readonly tab: "model";
1165
+ readonly group: "Sampling";
1166
+ readonly label: "Service Tier - Subagent";
1167
+ readonly description: "Service Tier for spawned task/eval subagents. Inherit = match the main agent's live tier (tracks /fast); pick a value to scope subagents independently.";
1168
+ readonly options: readonly SubmenuOption<"auto" | "claude-only" | "default" | "flex" | "inherit" | "none" | "openai-only" | "priority" | "scale">[];
1169
+ };
1170
+ };
1171
+ readonly serviceTierAdvisor: {
1172
+ readonly type: "enum";
1173
+ readonly values: readonly ["inherit", "none", "auto", "default", "flex", "scale", "priority", "openai-only", "claude-only"];
1174
+ readonly default: "none";
1175
+ readonly ui: {
1176
+ readonly tab: "model";
1177
+ readonly group: "Sampling";
1178
+ readonly label: "Service Tier - Advisor";
1179
+ readonly description: "Service Tier for the advisor model. None = standard processing; Inherit = match the main agent's live tier; pick a value (e.g. Priority) to run the advisor on a faster serving path.";
1180
+ readonly options: readonly SubmenuOption<"auto" | "claude-only" | "default" | "flex" | "inherit" | "none" | "openai-only" | "priority" | "scale">[];
1181
+ readonly condition: "advisorEnabled";
1189
1182
  };
1190
1183
  };
1191
1184
  readonly fastModeScope: {
@@ -4243,6 +4236,16 @@ export declare const SETTINGS_SCHEMA: {
4243
4236
  readonly description: "Obfuscate secrets before sending to AI providers";
4244
4237
  };
4245
4238
  };
4239
+ readonly "providers.ollama-cloud.maxConcurrency": {
4240
+ readonly type: "number";
4241
+ readonly default: 3;
4242
+ readonly ui: {
4243
+ readonly tab: "providers";
4244
+ readonly group: "Services";
4245
+ readonly label: "Ollama Cloud Max Concurrency";
4246
+ readonly description: "Maximum concurrent Ollama Cloud subagent runs per process; 0 disables the provider-specific limit";
4247
+ };
4248
+ };
4246
4249
  readonly "providers.webSearch": {
4247
4250
  readonly type: "enum";
4248
4251
  readonly values: readonly ["auto", ...SearchProviderId[]];
@@ -61,6 +61,8 @@ export declare function __validateLegacyPiPackageRootOverrides(candidates: Recor
61
61
  * compiled-binary branch is verifiable from dev tests.
62
62
  */
63
63
  export declare function __buildLegacyPiPackageRootOverrides(isCompiled: boolean): Record<string, string>;
64
+ /** Test seam for compiled-binary legacy extension source rewriting. */
65
+ export declare function __rewriteLegacyExtensionSourceForTests(source: string, importerPath: string): Promise<string>;
64
66
  /**
65
67
  * Load a legacy Pi extension module from its real on-disk location.
66
68
  *
@@ -38,6 +38,14 @@ export interface MCPStoredOAuthCredential extends OAuthCredential {
38
38
  clientId?: string;
39
39
  clientSecret?: string;
40
40
  resource?: string;
41
+ /**
42
+ * Authorization-server URL (the issuer the grant was minted against). Used
43
+ * to filter same-origin resource indicators on refresh: RFC 8414 lets the
44
+ * authorize and token endpoints sit on different origins, so refresh
45
+ * cannot infer the original auth-server origin from `tokenUrl` alone.
46
+ * Unset on legacy credentials minted before issue #3502's fix.
47
+ */
48
+ authorizationUrl?: string;
41
49
  }
42
50
  export interface MCPOAuthConfig {
43
51
  /** Authorization endpoint URL */
@@ -67,6 +75,13 @@ export interface MCPOAuthConfig {
67
75
  callbackPath?: string;
68
76
  /** MCP resource URI for RFC 8707 resource indicators */
69
77
  resource?: string;
78
+ /**
79
+ * True when `resource` was synthesized from the server URL fallback rather
80
+ * than advertised by OAuth/protected-resource metadata. Fallback resources
81
+ * are stripped when same-origin with the authorization server; advertised
82
+ * path-scoped resources are preserved.
83
+ */
84
+ stripSameOriginResource?: boolean;
70
85
  /** Fetch implementation for token exchange and discovery requests. */
71
86
  fetch?: FetchImpl;
72
87
  }
@@ -93,18 +108,40 @@ export declare class MCPOAuthFlow extends OAuthCallbackFlow {
93
108
  */
94
109
  get registeredClientSecret(): string | undefined;
95
110
  get resource(): string | undefined;
111
+ /**
112
+ * Authorization-server URL the flow used. Persist alongside the credential
113
+ * so refresh can filter same-origin resource indicators against the issuer's
114
+ * origin even when `tokenUrl` lives on a different origin (RFC 8414 permits
115
+ * the split).
116
+ */
117
+ get authorizationUrl(): string;
96
118
  generateAuthUrl(state: string, redirectUri: string): Promise<{
97
119
  url: string;
98
120
  instructions?: string;
99
121
  }>;
100
122
  exchangeToken(code: string, _state: string, redirectUri: string): Promise<OAuthCredentials>;
101
123
  }
124
+ /**
125
+ * Options for {@link refreshMCPOAuthToken}. Carried via the trailing object
126
+ * so positional callers keep working.
127
+ */
128
+ export interface RefreshMCPOAuthTokenOptions {
129
+ fetch?: FetchImpl;
130
+ /**
131
+ * Authorization-server URL the original grant was minted against. Used to
132
+ * filter same-origin resource indicators on refresh. Defaults to `tokenUrl`'s
133
+ * origin when omitted for legacy credentials.
134
+ */
135
+ authorizationUrl?: string;
136
+ /**
137
+ * True when the refresh `resource` was synthesized from the server URL
138
+ * fallback because the credential/auth material carried no resource.
139
+ * Preserved advertised resources leave this false/undefined.
140
+ */
141
+ stripSameOriginResource?: boolean;
142
+ }
102
143
  /**
103
144
  * Refresh an MCP OAuth token using the standard refresh_token grant.
104
145
  * Returns updated credentials; preserves the old refresh token if the server doesn't rotate it.
105
146
  */
106
- export declare function refreshMCPOAuthToken(tokenUrl: string, refreshToken: string, clientId?: string, clientSecret?: string, resourceOrOpts?: string | {
107
- fetch?: FetchImpl;
108
- }, opts?: {
109
- fetch?: FetchImpl;
110
- }): Promise<OAuthCredentials>;
147
+ export declare function refreshMCPOAuthToken(tokenUrl: string, refreshToken: string, clientId?: string, clientSecret?: string, resourceOrOpts?: string | RefreshMCPOAuthTokenOptions, opts?: RefreshMCPOAuthTokenOptions): Promise<OAuthCredentials>;
@@ -19,9 +19,41 @@ export declare const SPACE_HOLD_MECHANICAL_RUN = 2;
19
19
  /** Idle gap (ms) after the last repeated space that counts as the space bar being released, ending
20
20
  * the push-to-talk recording. Must comfortably exceed the OS key-repeat interval. */
21
21
  export declare const SPACE_HOLD_RELEASE_MS = 250;
22
+ /**
23
+ * Extract image-or-other file paths from plain (un-bracketed) clipboard text.
24
+ * Mirrors {@link extractBracketedPastePaths} for terminals/handlers that
25
+ * already stripped the `\x1b[200~`…`\x1b[201~` markers (e.g. clipboard text
26
+ * read directly via `pbpaste`/PowerShell).
27
+ */
28
+ export declare function extractPastePathsFromText(text: string): string[] | undefined;
22
29
  export declare function extractBracketedPastePaths(data: string): string[] | undefined;
23
30
  export declare function extractBracketedImagePastePaths(data: string): string[] | undefined;
24
31
  export declare function extractBracketedImagePastePath(data: string): string | undefined;
32
+ /**
33
+ * Return a single image file path when `text` is exactly one explicit path
34
+ * pointing at a supported image extension (`.png`, `.jpg`/`.jpeg`, `.gif`,
35
+ * `.webp`). Used by the keybind-driven clipboard image paste path so a
36
+ * clipboard whose only payload is an image file (e.g. Finder `Cmd+C` on
37
+ * macOS) attaches the image instead of pasting the path as literal text.
38
+ *
39
+ * Two-stage detection:
40
+ *
41
+ * 1. Splitter pass (shared with the bracketed-paste handler) — handles
42
+ * quoted paths, shell-escaped spaces, and unambiguous single tokens.
43
+ * Returns the single image path when it parses cleanly; explicitly
44
+ * returns `undefined` when the splitter found multiple segments (so
45
+ * ambiguous multi-path clipboard text like `/tmp/a.png /tmp/b.png`
46
+ * still falls through to the text fallback instead of being mis-loaded
47
+ * as one giant path).
48
+ * 2. Whole-text-as-path pass — only reached when the splitter failed
49
+ * (every segment must look like an explicit path; an unescaped space in
50
+ * a real path breaks that). Restricted to inputs anchored by
51
+ * {@link ABSOLUTE_PATH_PREFIX_REGEX} so prose containing a path-shaped
52
+ * fragment ("see /tmp/x.png") never hijacks the smart fallback. This
53
+ * is what recovers macOS screenshot filenames like
54
+ * `/Users/me/Desktop/Screenshot 2026-06-25 at 1.23.45 PM.png`.
55
+ */
56
+ export declare function extractImagePathFromText(text: string): string | undefined;
25
57
  /**
26
58
  * Custom editor that handles configurable app-level shortcuts for coding-agent.
27
59
  */
@@ -107,13 +107,13 @@ export declare class ToolExecutionComponent extends Container implements NativeS
107
107
  */
108
108
  seal(): void;
109
109
  /**
110
- * Whether this block is a waiting `job` poll (every watched job still
111
- * running) that has not been sealed. Such a block never finalized, so none
112
- * of its rows entered native scrollback (the ticking spinner keeps the
113
- * stable-prefix ratchet at zero) and the whole block can be removed when a
114
- * follow-up `job` call supersedes it.
110
+ * Whether this block is a supersedable result snapshot that has not been
111
+ * sealed. Such a block never finalized, so none of its rows entered native
112
+ * scrollback and the whole block can be removed when a follow-up matching
113
+ * tool call supersedes it.
115
114
  */
116
115
  isDisplaceableBlock(): boolean;
116
+ canBeDisplacedBy(nextToolName: string | undefined): boolean;
117
117
  /**
118
118
  * Stop spinner animation and cleanup resources.
119
119
  */
@@ -1,3 +1,4 @@
1
+ import { ToolExecutionComponent } from "../../modes/components/tool-execution";
1
2
  import type { InteractiveModeContext } from "../../modes/types";
2
3
  import type { AgentSessionEvent } from "../../session/agent-session";
3
4
  export declare class EventController {
@@ -14,5 +15,14 @@ export declare class EventController {
14
15
  */
15
16
  resetTranscriptAnchors(): void;
16
17
  handleEvent(event: AgentSessionEvent): Promise<void>;
18
+ /**
19
+ * Adopt a rebuilt-tail todo snapshot as the controller's tracked live
20
+ * snapshot. Used by rebuild paths (settings/extensions overlay close, focus
21
+ * attach, /resume) to preserve displacement continuity when a turn is still
22
+ * active — without this, the next same-turn `todo` update would stack
23
+ * another panel because the controller's tracker was reset before rebuild.
24
+ * Drops the candidate when it is no longer a displaceable todo.
25
+ */
26
+ inheritDisplaceableTodo(component: ToolExecutionComponent | null | undefined): void;
17
27
  sendCompletionNotification(): void;
18
28
  }
@@ -1,6 +1,6 @@
1
1
  import { type AutocompleteProvider, type SlashCommand } from "@oh-my-pi/pi-tui";
2
2
  import type { InteractiveModeContext } from "../../modes/types";
3
- import { readImageFromClipboard, readTextFromClipboard } from "../../utils/clipboard";
3
+ import { readImageFromClipboard, readMacFileUrlsFromClipboard, readTextFromClipboard } from "../../utils/clipboard";
4
4
  /**
5
5
  * Slash commands that may carry secrets in their arguments should never be
6
6
  * persisted to history.
@@ -24,6 +24,7 @@ export declare class InputController {
24
24
  clipboard?: {
25
25
  readImage: typeof readImageFromClipboard;
26
26
  readText: typeof readTextFromClipboard;
27
+ readMacFileUrls?: typeof readMacFileUrlsFromClipboard;
27
28
  });
28
29
  setupKeyHandlers(): void;
29
30
  setupEditorSubmitHandler(): void;
@@ -760,6 +760,8 @@ export declare class AgentSession {
760
760
  abort(options?: {
761
761
  goalReason?: "interrupted" | "internal";
762
762
  reason?: string;
763
+ /** Internal `/compact` startup keeps the manual-compaction marker alive while aborting the active turn. */
764
+ preserveCompaction?: boolean;
763
765
  }): Promise<void>;
764
766
  /**
765
767
  * Start a new session, optionally with initial messages and parent tracking.
@@ -4,6 +4,7 @@
4
4
  * Runs each subagent on the main thread and forwards AgentEvents for progress tracking.
5
5
  */
6
6
  import type { AgentTelemetryConfig, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
7
+ import type { ServiceTier } from "@oh-my-pi/pi-ai";
7
8
  import type { Rule } from "../capability/rule";
8
9
  import { ModelRegistry } from "../config/model-registry";
9
10
  import type { PromptTemplate } from "../config/prompt-templates";
@@ -119,6 +120,13 @@ export interface ExecutorOptions {
119
120
  authStorage?: AuthStorage;
120
121
  modelRegistry?: ModelRegistry;
121
122
  settings?: Settings;
123
+ /**
124
+ * Parent session's live effective service tier, the source of truth for a
125
+ * subagent whose `serviceTierSubagent` is `"inherit"`. `null` = the parent
126
+ * explicitly has no tier (e.g. `/fast off`); omitted = no live session, so
127
+ * inherit falls back to the configured `serviceTier` setting.
128
+ */
129
+ parentServiceTier?: ServiceTier | null;
122
130
  /** Override local:// protocol options so subagent shares parent's local:// root */
123
131
  localProtocolOptions?: LocalProtocolOptions;
124
132
  /**
@@ -188,7 +196,7 @@ export declare function finalizeSubprocessOutput(args: FinalizeSubprocessOutputA
188
196
  * Create proxy tools that reuse the parent's MCP connections.
189
197
  */
190
198
  export declare function createMCPProxyTools(mcpManager: MCPManager): CustomTool[];
191
- export declare function createSubagentSettings(baseSettings: Settings, overrides?: Partial<Record<SettingPath, unknown>>): Settings;
199
+ export declare function createSubagentSettings(baseSettings: Settings, overrides?: Partial<Record<SettingPath, unknown>>, inheritedServiceTier?: ServiceTier | null): Settings;
192
200
  /**
193
201
  * Run a single agent in-process.
194
202
  */
@@ -33,6 +33,22 @@ export declare function mapWithConcurrencyLimit<T, R>(items: T[], concurrency: n
33
33
  export declare class Semaphore {
34
34
  #private;
35
35
  constructor(max: number);
36
- acquire(): Promise<void>;
36
+ /**
37
+ * Resolves when a slot is available. Pass an `AbortSignal` so callers that
38
+ * stop waiting (parent task cancelled, wall-clock budget elapsed) also stop
39
+ * occupying a queue slot — otherwise a later `release()` would resolve the
40
+ * abandoned waiter, permanently shrinking effective concurrency for the
41
+ * remaining lifetime of the process (issue #3464 review feedback).
42
+ */
43
+ acquire(signal?: AbortSignal): Promise<void>;
37
44
  release(): void;
45
+ /**
46
+ * Adjust the maximum concurrency in place. Raising the ceiling immediately
47
+ * admits queued waiters that now fit; lowering it lets in-flight holders
48
+ * drain naturally (new acquires keep blocking until `#current` falls below
49
+ * the new max). Resizing the single shared instance — instead of replacing
50
+ * it — keeps in-flight slots counted, so a runtime or mixed limit change can
51
+ * never push concurrency past the cap (issue #3464 review feedback).
52
+ */
53
+ resize(max: number): void;
38
54
  }
@@ -1,6 +1,6 @@
1
1
  import type { InMemorySnapshotStore } from "@oh-my-pi/hashline";
2
2
  import type { AgentTelemetryConfig, AgentTool } from "@oh-my-pi/pi-agent-core";
3
- import type { FetchImpl, ImageContent, Model, ToolChoice } from "@oh-my-pi/pi-ai";
3
+ import type { FetchImpl, ImageContent, Model, ServiceTier, ToolChoice } from "@oh-my-pi/pi-ai";
4
4
  import type { AsyncJobManager } from "../async/job-manager";
5
5
  import type { Rule } from "../capability/rule";
6
6
  import type { PromptTemplate } from "../config/prompt-templates";
@@ -191,6 +191,8 @@ export interface ToolSession {
191
191
  getActiveModelString?: () => string | undefined;
192
192
  /** Get the current session model object (provider/api capabilities), regardless of how it was chosen. */
193
193
  getActiveModel?: () => Model | undefined;
194
+ /** Get the session's live effective service tier (undefined = none). Source of truth for subagent `serviceTierSubagent: inherit`. */
195
+ getServiceTier?: () => ServiceTier | undefined;
194
196
  /** Auth storage for passing to subagents (avoids re-discovery) */
195
197
  authStorage?: import("../session/auth-storage").AuthStorage;
196
198
  /** Model registry for passing to subagents (avoids re-discovery) */
@@ -1,4 +1,14 @@
1
1
  import type { ClipboardImage } from "@oh-my-pi/pi-natives";
2
+ /**
3
+ * Read file paths from the macOS pasteboard's `public.file-url` representation.
4
+ *
5
+ * Used to reach the Finder `Cmd+C` pasteboard (which exposes only file URLs,
6
+ * no plain text or raw image bytes) so an image-file clipboard can be attached
7
+ * via {@link handleImagePathPaste} instead of falling through to "Clipboard is
8
+ * empty". Returns an empty array on non-darwin platforms, when AppleScript is
9
+ * unavailable, or when the pasteboard holds no file URLs.
10
+ */
11
+ export declare function readMacFileUrlsFromClipboard(): Promise<string[]>;
2
12
  /**
3
13
  * Copy text to the system clipboard.
4
14
  *
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-coding-agent",
4
- "version": "16.1.19",
4
+ "version": "16.1.21",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -35,7 +35,7 @@
35
35
  "check": "biome check . && bun run check:types",
36
36
  "check:types": "tsgo -p tsconfig.json --noEmit",
37
37
  "lint": "biome lint .",
38
- "test": "bun test --parallel=4 test src",
38
+ "test": "bun ../../scripts/ci-test-ts.ts coding-agent-heavy --full",
39
39
  "fix": "biome check --write --unsafe . && bun run format-prompts",
40
40
  "fmt": "biome format --write . && bun run format-prompts",
41
41
  "format-prompts": "bun scripts/format-prompts.ts",
@@ -48,17 +48,17 @@
48
48
  "@agentclientprotocol/sdk": "0.25.0",
49
49
  "@babel/parser": "^7.29.7",
50
50
  "@mozilla/readability": "^0.6.0",
51
- "@oh-my-pi/hashline": "16.1.19",
52
- "@oh-my-pi/omp-stats": "16.1.19",
53
- "@oh-my-pi/pi-agent-core": "16.1.19",
54
- "@oh-my-pi/pi-ai": "16.1.19",
55
- "@oh-my-pi/pi-catalog": "16.1.19",
56
- "@oh-my-pi/pi-mnemopi": "16.1.19",
57
- "@oh-my-pi/pi-natives": "16.1.19",
58
- "@oh-my-pi/pi-tui": "16.1.19",
59
- "@oh-my-pi/pi-utils": "16.1.19",
60
- "@oh-my-pi/pi-wire": "16.1.19",
61
- "@oh-my-pi/snapcompact": "16.1.19",
51
+ "@oh-my-pi/hashline": "16.1.21",
52
+ "@oh-my-pi/omp-stats": "16.1.21",
53
+ "@oh-my-pi/pi-agent-core": "16.1.21",
54
+ "@oh-my-pi/pi-ai": "16.1.21",
55
+ "@oh-my-pi/pi-catalog": "16.1.21",
56
+ "@oh-my-pi/pi-mnemopi": "16.1.21",
57
+ "@oh-my-pi/pi-natives": "16.1.21",
58
+ "@oh-my-pi/pi-tui": "16.1.21",
59
+ "@oh-my-pi/pi-utils": "16.1.21",
60
+ "@oh-my-pi/pi-wire": "16.1.21",
61
+ "@oh-my-pi/snapcompact": "16.1.21",
62
62
  "@opentelemetry/api": "^1.9.1",
63
63
  "@opentelemetry/context-async-hooks": "^2.7.1",
64
64
  "@opentelemetry/exporter-trace-otlp-proto": "^0.218.0",
@@ -117,6 +117,15 @@ function isSafeWildcardBasename(basename: string): boolean {
117
117
  return true;
118
118
  }
119
119
 
120
+ // Worker entry modules intentionally throw when imported outside a Worker. The
121
+ // bundled registry loads on the main thread during legacy extension validation,
122
+ // so these exported subpaths must stay out of the static registry.
123
+ const MAIN_THREAD_UNSAFE_WILDCARD_BASENAMES = new Set(["worker-entry"]);
124
+
125
+ function isMainThreadSafeWildcardBasename(basename: string): boolean {
126
+ return !MAIN_THREAD_UNSAFE_WILDCARD_BASENAMES.has(basename);
127
+ }
128
+
120
129
  interface WildcardPattern {
121
130
  readonly exportPrefix: string;
122
131
  readonly exportSuffix: string;
@@ -217,6 +226,7 @@ async function collectEntries(): Promise<RegistryEntry[]> {
217
226
  if (!match.endsWith(pattern.sourceSuffix)) continue;
218
227
  const basename = match.slice(0, match.length - pattern.sourceSuffix.length);
219
228
  if (!isSafeWildcardBasename(basename)) continue;
229
+ if (!isMainThreadSafeWildcardBasename(basename)) continue;
220
230
  if (basename.includes("/")) continue;
221
231
  const subpath = `${pattern.exportPrefix}${basename}${pattern.exportSuffix}`;
222
232
  const key = `${pkg.name}/${subpath}`;
@@ -199,6 +199,50 @@ describe("advisor", () => {
199
199
  expect(result.useless).toBe(true);
200
200
  });
201
201
 
202
+ it("suppresses duplicate advice notes from the same advisor session", async () => {
203
+ const onAdvice = vi.fn();
204
+ const tool = new AdviseTool(onAdvice);
205
+ const note = "I'll pause here and wait for the YAML revision.";
206
+
207
+ await tool.execute("tc-1", { note, severity: "nit" });
208
+ await tool.execute("tc-2", { note, severity: "nit" });
209
+
210
+ expect(onAdvice).toHaveBeenCalledTimes(1);
211
+ expect(onAdvice).toHaveBeenCalledWith(note, "nit");
212
+ });
213
+
214
+ it("allows the same advice after delivered-note memory resets", async () => {
215
+ const onAdvice = vi.fn();
216
+ const tool = new AdviseTool(onAdvice);
217
+ const note = "Acknowledged.";
218
+
219
+ await tool.execute("tc-1", { note, severity: "nit" });
220
+ tool.resetDeliveredNotes();
221
+ await tool.execute("tc-2", { note, severity: "nit" });
222
+
223
+ expect(onAdvice).toHaveBeenCalledTimes(2);
224
+ expect(onAdvice).toHaveBeenNthCalledWith(1, note, "nit");
225
+ expect(onAdvice).toHaveBeenNthCalledWith(2, note, "nit");
226
+ });
227
+
228
+ it("forwards escalations of an already-delivered note and suppresses downgrades", async () => {
229
+ const onAdvice = vi.fn();
230
+ const tool = new AdviseTool(onAdvice);
231
+ const note = "Rename collides with the existing helper.";
232
+
233
+ await tool.execute("tc-1", { note, severity: "nit" });
234
+ await tool.execute("tc-2", { note, severity: "concern" });
235
+ await tool.execute("tc-3", { note, severity: "blocker" });
236
+ // De-escalation back to nit or concern is treated as a duplicate.
237
+ await tool.execute("tc-4", { note, severity: "concern" });
238
+ await tool.execute("tc-5", { note, severity: "nit" });
239
+
240
+ expect(onAdvice).toHaveBeenCalledTimes(3);
241
+ expect(onAdvice).toHaveBeenNthCalledWith(1, note, "nit");
242
+ expect(onAdvice).toHaveBeenNthCalledWith(2, note, "concern");
243
+ expect(onAdvice).toHaveBeenNthCalledWith(3, note, "blocker");
244
+ });
245
+
202
246
  it("validates parameters using ArkType", () => {
203
247
  const onAdvice = vi.fn();
204
248
  const tool = new AdviseTool(onAdvice);
@@ -139,15 +139,37 @@ export function deriveAdvisorTelemetry(
139
139
  */
140
140
  export const ADVISOR_READONLY_TOOL_NAMES: ReadonlySet<string> = new Set(["read", "search", "find"]);
141
141
 
142
+ function advisorNoteDedupeKey(note: string): string {
143
+ return note.trim().replace(/\s+/g, " ");
144
+ }
145
+
146
+ /** Rank advisor severities so the dedupe state can detect a real escalation
147
+ * (nit → concern → blocker) versus a verbatim repeat. `undefined` defers to
148
+ * `nit` because the schema treats an omitted severity as a plain nit. */
149
+ const ADVISOR_SEVERITY_RANK: Record<AdvisorSeverity, number> = { nit: 1, concern: 2, blocker: 3 };
150
+ function advisorSeverityRank(severity: AdvisorSeverity | undefined): number {
151
+ return ADVISOR_SEVERITY_RANK[severity ?? "nit"];
152
+ }
153
+
142
154
  export class AdviseTool implements AgentTool<typeof adviseSchema, AdviseDetails> {
143
155
  readonly name = "advise";
144
156
  readonly label = "Advise";
145
157
  readonly description = adviseDescription;
146
158
  readonly parameters = adviseSchema;
147
159
  readonly intent = "omit" as const;
160
+ /** Highest delivered severity rank per normalized note. A new call passes
161
+ * through only when its rank strictly exceeds the recorded one (a real
162
+ * escalation: nit → concern → blocker), so an advisor cannot bypass dedupe
163
+ * by retagging the same text at a lower or equal severity. */
164
+ #deliveredNoteSeverities = new Map<string, number>();
148
165
 
149
166
  constructor(private readonly onAdvice: (note: string, severity?: AdviseDetails["severity"]) => void) {}
150
167
 
168
+ /** Clear delivered-note memory when the advisor starts a fresh conversation. */
169
+ resetDeliveredNotes(): void {
170
+ this.#deliveredNoteSeverities.clear();
171
+ }
172
+
151
173
  async execute(
152
174
  _toolCallId: string,
153
175
  args: AdviseParams,
@@ -155,6 +177,17 @@ export class AdviseTool implements AgentTool<typeof adviseSchema, AdviseDetails>
155
177
  _onUpdate?: AgentToolUpdateCallback<AdviseDetails>,
156
178
  _context?: AgentToolContext,
157
179
  ): Promise<AgentToolResult<AdviseDetails>> {
180
+ const key = advisorNoteDedupeKey(args.note);
181
+ const rank = advisorSeverityRank(args.severity);
182
+ const previousRank = this.#deliveredNoteSeverities.get(key) ?? 0;
183
+ if (rank <= previousRank) {
184
+ return {
185
+ content: [{ type: "text", text: "Duplicate advice ignored." }],
186
+ details: { note: args.note, severity: args.severity },
187
+ useless: true,
188
+ };
189
+ }
190
+ this.#deliveredNoteSeverities.set(key, rank);
158
191
  this.onAdvice(args.note, args.severity);
159
192
  return {
160
193
  content: [{ type: "text", text: "Recorded." }],