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

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 (91) hide show
  1. package/CHANGELOG.md +57 -1
  2. package/dist/{CHANGELOG-1p8yvde7.md → CHANGELOG-8yy8h3bc.md} +57 -1
  3. package/dist/cli.js +4974 -4968
  4. package/dist/types/async/job-manager.d.ts +12 -0
  5. package/dist/types/discovery/agents.d.ts +11 -0
  6. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +10 -0
  7. package/dist/types/hindsight/state.d.ts +0 -1
  8. package/dist/types/lsp/utils.d.ts +8 -0
  9. package/dist/types/modes/session-observer-registry.d.ts +2 -0
  10. package/dist/types/registry/agent-lifecycle.d.ts +1 -1
  11. package/dist/types/session/agent-session-types.d.ts +7 -0
  12. package/dist/types/session/agent-session.d.ts +5 -3
  13. package/dist/types/session/turn-recovery.d.ts +6 -2
  14. package/dist/types/task/executor.d.ts +4 -0
  15. package/dist/types/tools/browser/launch.d.ts +2 -0
  16. package/dist/types/tools/path-utils.d.ts +7 -0
  17. package/dist/types/tools/read.d.ts +2 -0
  18. package/dist/types/tools/shell-tokenize.d.ts +21 -3
  19. package/dist/types/utils/changelog.d.ts +3 -2
  20. package/dist/types/utils/late-cleanup.d.ts +2 -0
  21. package/dist/types/vibe/runtime.d.ts +2 -0
  22. package/package.json +13 -13
  23. package/scripts/omp +11 -2
  24. package/src/async/job-manager.ts +26 -0
  25. package/src/cli/stats-cli.ts +3 -3
  26. package/src/cli/update-cli.ts +2 -56
  27. package/src/config/config-file.ts +2 -2
  28. package/src/config/keybindings.ts +3 -3
  29. package/src/config/model-discovery.ts +36 -2
  30. package/src/config/model-registry.ts +4 -1
  31. package/src/discovery/agents.ts +71 -3
  32. package/src/discovery/claude.ts +8 -5
  33. package/src/discovery/cursor.ts +8 -5
  34. package/src/discovery/gemini.ts +11 -8
  35. package/src/discovery/vscode.ts +1 -0
  36. package/src/discovery/windsurf.ts +4 -2
  37. package/src/edit/hashline/filesystem.ts +7 -7
  38. package/src/eval/agent-bridge.ts +1 -3
  39. package/src/eval/jl/prelude.jl +4 -4
  40. package/src/eval/js/shared/prelude.txt +2 -2
  41. package/src/eval/py/prelude.py +0 -3
  42. package/src/eval/rb/prelude.rb +1 -2
  43. package/src/extensibility/legacy-pi-coding-agent-shim.ts +23 -0
  44. package/src/hindsight/state.ts +2 -22
  45. package/src/internal-urls/memory-protocol.ts +29 -0
  46. package/src/lsp/client.ts +2 -2
  47. package/src/lsp/utils.ts +29 -0
  48. package/src/modes/acp/acp-agent.ts +2 -1
  49. package/src/modes/components/agent-hub.ts +36 -20
  50. package/src/modes/components/agent-transcript-viewer.ts +1 -3
  51. package/src/modes/components/bordered-loader.ts +1 -1
  52. package/src/modes/components/custom-editor.ts +2 -2
  53. package/src/modes/components/hook-input.ts +1 -1
  54. package/src/modes/components/hook-selector.ts +2 -2
  55. package/src/modes/components/session-selector.ts +2 -2
  56. package/src/modes/controllers/event-controller.ts +159 -2
  57. package/src/modes/controllers/extension-ui-controller.ts +2 -1
  58. package/src/modes/print-mode.ts +29 -12
  59. package/src/modes/session-observer-registry.ts +5 -0
  60. package/src/registry/agent-lifecycle.ts +21 -3
  61. package/src/sdk.ts +11 -5
  62. package/src/session/agent-session-types.ts +8 -0
  63. package/src/session/agent-session.ts +156 -186
  64. package/src/session/session-paths.ts +68 -121
  65. package/src/session/settings-stream-fn.ts +7 -3
  66. package/src/session/turn-recovery.ts +234 -16
  67. package/src/session/unexpected-stop-classifier.ts +12 -3
  68. package/src/slash-commands/helpers/stats-dashboard.ts +2 -1
  69. package/src/task/agents.ts +1 -1
  70. package/src/task/executor.ts +105 -29
  71. package/src/task/isolation-runner.ts +19 -1
  72. package/src/task/structured-subagent.ts +17 -2
  73. package/src/tiny/worker.ts +1 -1
  74. package/src/tools/bash-interceptor.ts +8 -4
  75. package/src/tools/browser/launch.ts +37 -9
  76. package/src/tools/glob.ts +4 -2
  77. package/src/tools/path-utils.ts +13 -3
  78. package/src/tools/read.ts +6 -0
  79. package/src/tools/shell-tokenize.ts +38 -9
  80. package/src/utils/changelog.ts +5 -4
  81. package/src/utils/late-cleanup.ts +17 -0
  82. package/src/vibe/runtime.ts +197 -53
  83. package/src/web/scrapers/hackage.ts +1 -12
  84. package/src/web/search/providers/duckduckgo.ts +1 -1
  85. package/src/web/search/providers/ecosia.ts +1 -1
  86. package/src/web/search/providers/exa.ts +1 -1
  87. package/src/web/search/providers/firecrawl.ts +1 -1
  88. package/src/web/search/providers/mojeek.ts +1 -1
  89. package/src/web/search/providers/perplexity.ts +1 -1
  90. package/src/web/search/providers/public.ts +1 -1
  91. package/src/web/search/providers/startpage.ts +1 -1
@@ -51,6 +51,11 @@ export interface AsyncJobDeliveryState {
51
51
  nextRetryAt?: number;
52
52
  pendingJobIds: string[];
53
53
  }
54
+ export interface AsyncJobReapResult {
55
+ settled: boolean;
56
+ pendingJobIds: string[];
57
+ completion: Promise<void>;
58
+ }
54
59
  export interface AsyncJobRegisterOptions {
55
60
  id?: string;
56
61
  /** Registry id of the agent that owns this job; used to scope cancelAll. */
@@ -167,6 +172,13 @@ export declare class AsyncJobManager {
167
172
  timeoutMs?: number;
168
173
  excludeSuppressed?: boolean;
169
174
  }): Promise<boolean>;
175
+ /**
176
+ * Cancel every job owned by `ownerId`, then wait only until `deadlineAt`.
177
+ * The returned completion keeps waiting for actual process settlement when
178
+ * the deadline expires, so callers can move that cleanup out of the
179
+ * user-visible Task wait without losing ownership of the live work.
180
+ */
181
+ cancelAndReapOwnerJobs(ownerId: string, deadlineAt: number): Promise<AsyncJobReapResult>;
170
182
  drainDeliveries(options?: {
171
183
  timeoutMs?: number;
172
184
  filter?: AsyncJobFilter;
@@ -1,4 +1,14 @@
1
1
  import type { LoadContext } from "../capability/types.js";
2
+ interface UserPathCandidateOptions {
3
+ platform?: NodeJS.Platform;
4
+ env?: NodeJS.ProcessEnv;
5
+ windowsUserProfile?: () => string | undefined;
6
+ wslPath?: (windowsPath: string) => string | undefined;
7
+ }
8
+ /** Resolve the Windows host profile home exposed to WSL, if available. */
9
+ export declare function getWslWindowsHomeCandidate(options?: UserPathCandidateOptions): string | undefined;
10
+ /** User-level paths: ~/.agent[s]/<segments>, plus the Windows host profile under WSL. */
11
+ export declare function getUserPathCandidates(ctx: LoadContext, ...segments: string[]): string[];
2
12
  /**
3
13
  * Project-level paths: walk up from cwd to repoRoot, returning `.agent/<segments>`
4
14
  * and `.agents/<segments>` at each ancestor.
@@ -10,3 +20,4 @@ import type { LoadContext } from "../capability/types.js";
10
20
  * directory — see https://github.com/can1357/oh-my-pi/issues/1116.
11
21
  */
12
22
  export declare function getProjectPathCandidates(ctx: LoadContext, ...segments: string[]): string[];
23
+ export {};
@@ -92,6 +92,16 @@ export interface WriteOperations {
92
92
  export interface WriteToolOptions {
93
93
  operations?: WriteOperations;
94
94
  }
95
+ /**
96
+ * Convert an image attachment to PNG using the legacy package-root contract.
97
+ *
98
+ * Invalid or unsupported image data returns `null`, matching Pi's historical
99
+ * helper instead of surfacing Bun's decoder error to extensions.
100
+ */
101
+ export declare function convertToPng(base64Data: string, mimeType: string): Promise<{
102
+ data: string;
103
+ mimeType: string;
104
+ } | null>;
95
105
  /** Format the active shortcut for legacy extensions that render keybinding hints. */
96
106
  export declare function keyText(action: Keybinding): string;
97
107
  /** Parse frontmatter using the historical Pi package-root helper. */
@@ -93,7 +93,6 @@ export declare class HindsightSessionState {
93
93
  retainSession(messages: HindsightMessage[]): Promise<void>;
94
94
  maybeRetainOnAgentEnd(): Promise<void>;
95
95
  forceRetainCurrentSession(): Promise<void>;
96
- maybeRecallOnAgentStart(): Promise<void>;
97
96
  beforeAgentStartPrompt(promptText: string): Promise<string | undefined>;
98
97
  recallForCompaction(messages: HindsightMessage[]): Promise<string | undefined>;
99
98
  runMentalModelLoad(scope: BankScope): Promise<void>;
@@ -14,6 +14,14 @@ export declare function fileToUri(filePath: string): string;
14
14
  * Handles Windows drive letters correctly.
15
15
  */
16
16
  export declare function uriToFile(uri: string): string;
17
+ /** Map that treats equivalent file URI spellings as the same key. */
18
+ export declare class EquivalentUriMap<Value> extends Map<string, Value> {
19
+ #private;
20
+ delete(uri: string): boolean;
21
+ get(uri: string): Value | undefined;
22
+ has(uri: string): boolean;
23
+ set(uri: string, value: Value): this;
24
+ }
17
25
  /**
18
26
  * Convert diagnostic severity number to string name.
19
27
  */
@@ -28,6 +28,8 @@ export declare class SessionObserverRegistry {
28
28
  /** Add a change listener. Returns unsubscribe function. */
29
29
  onChange(cb: (kind: SessionObserverChangeKind) => void): () => void;
30
30
  setMainSession(sessionFile?: string): void;
31
+ /** Return one tracked session without copying or sorting the registry. */
32
+ getSession(id: string): ObservableSession | undefined;
31
33
  getSessions(): ObservableSession[];
32
34
  getActiveSubagentCount(): number;
33
35
  /** Clear all tracked sessions (e.g. on session switch). Keeps EventBus subscriptions and listeners. */
@@ -108,5 +108,5 @@ export declare class AgentLifecycleManager {
108
108
  tombstone?: boolean;
109
109
  }): Promise<boolean>;
110
110
  /** Teardown everything (process exit / main session dispose). */
111
- dispose(): Promise<void>;
111
+ dispose(deadlineAt?: number): Promise<void>;
112
112
  }
@@ -65,6 +65,13 @@ export interface UsageFallbackConfirmation {
65
65
  to: string;
66
66
  remainingPercent: number | undefined;
67
67
  }
68
+ /**
69
+ * Confirms whether a reserve-triggered model fallback may proceed.
70
+ *
71
+ * Interactive callers use the confirmation details to present the pending
72
+ * route change; aborting `signal` cancels that pending confirmation.
73
+ */
74
+ export type UsageFallbackConfirmer = (confirmation: UsageFallbackConfirmation, signal: AbortSignal) => Promise<boolean>;
68
75
  /** Identifies a retry fallback chain already entered during startup model resolution. */
69
76
  export interface InitialRetryFallbackState {
70
77
  /** Role whose configured primary was unavailable. */
@@ -20,7 +20,7 @@ import { type Effort } from "@oh-my-pi/pi-ai";
20
20
  import { type AdvisorConfig, type AdvisorRuntimeStatus } from "../advisor/index.js";
21
21
  import { AsyncJobManager } from "../async/index.js";
22
22
  import type { ModelRegistry } from "../config/model-registry.js";
23
- import { type ResolvedModelRoleValue } from "../config/model-resolver.js";
23
+ import type { ResolvedModelRoleValue } from "../config/model-resolver.js";
24
24
  import { type PromptTemplate } from "../config/prompt-templates.js";
25
25
  import type { Settings, SkillsSettings } from "../config/settings.js";
26
26
  import { RawSseDebugBuffer } from "../debug/raw-sse-buffer.js";
@@ -50,7 +50,7 @@ import type { TodoPhase } from "../tools/todo.js";
50
50
  import type { InspectImageMode } from "../utils/inspect-image-mode.js";
51
51
  import type { VibeModeState } from "../vibe/state.js";
52
52
  import type { AgentSessionEventListener } from "./agent-session-events.js";
53
- import type { AgentSessionConfig, AgentSessionDisposeOptions, AsyncJobSnapshot, CommandMetadataChangedListener, ContextUsageBreakdown, FollowUpOptions, FreshSessionResult, HandoffResult, ModelCycleResult, Prewalk, PromptOptions, ResetSessionContextResult, ResolvedRoleModel, RestoredQueuedMessage, RoleModelCycle, RoleModelCycleResult, SessionHandoffOptions, SessionOAuthAccountList, SessionStats, UsageFallbackConfirmation } from "./agent-session-types.js";
53
+ import type { AgentSessionConfig, AgentSessionDisposeOptions, AsyncJobSnapshot, CommandMetadataChangedListener, ContextUsageBreakdown, FollowUpOptions, FreshSessionResult, HandoffResult, ModelCycleResult, Prewalk, PromptOptions, ResetSessionContextResult, ResolvedRoleModel, RestoredQueuedMessage, RoleModelCycle, RoleModelCycleResult, SessionHandoffOptions, SessionOAuthAccountList, SessionStats, UsageFallbackConfirmer } from "./agent-session-types.js";
54
54
  import type { ClientBridge } from "./client-bridge.js";
55
55
  import { type CustomMessage, type CustomMessagePayload } from "./messages.js";
56
56
  import { type AdvisorStats } from "./session-advisors.js";
@@ -228,7 +228,7 @@ export declare class AgentSession {
228
228
  /** Resolved selector while retry routing is using a fallback model. */
229
229
  get retryFallbackModel(): string | undefined;
230
230
  /** Install the interactive decision surface for reserve-triggered model changes. */
231
- setUsageFallbackConfirmer(confirmer: ((confirmation: UsageFallbackConfirmation) => Promise<boolean>) | undefined): void;
231
+ setUsageFallbackConfirmer(confirmer: UsageFallbackConfirmer | undefined): void;
232
232
  /** Effective thinking level applied to the agent (the resolved level when `auto`). */
233
233
  get thinkingLevel(): ThinkingLevel | undefined;
234
234
  /** The selector the user configured: `auto` when auto mode is active, else the effective level. */
@@ -267,6 +267,8 @@ export declare class AgentSession {
267
267
  getLastAssistantMessage(): AssistantMessage | undefined;
268
268
  /** Current effective system prompt blocks (includes any per-turn extension modifications) */
269
269
  get systemPrompt(): string[];
270
+ /** Marks streamed text as committed or buffered for turn-recovery replay decisions. */
271
+ setTextOutputCommitted(committed: boolean): void;
270
272
  /** Current retry attempt (0 if not retrying) */
271
273
  get retryAttempt(): number;
272
274
  /** Names of tools currently exposed at the top level. */
@@ -4,7 +4,7 @@ import type { ModelRegistry } from "../config/model-registry.js";
4
4
  import type { Settings } from "../config/settings.js";
5
5
  import { type ConfiguredThinkingLevel } from "../thinking.js";
6
6
  import type { AgentSessionEvent } from "./agent-session-events.js";
7
- import type { InitialRetryFallbackState } from "./agent-session-types.js";
7
+ import type { InitialRetryFallbackState, UsageFallbackConfirmer } from "./agent-session-types.js";
8
8
  import { type RetryFallbackSelector } from "./retry-fallback-chains.js";
9
9
  import type { SessionManager } from "./session-manager.js";
10
10
  /** Result shape shared with automatic maintenance recovery. */
@@ -22,6 +22,8 @@ export interface TurnRecoveryHost {
22
22
  modelRegistry: ModelRegistry;
23
23
  configWarnings: string[];
24
24
  model(): Model | undefined;
25
+ /** Whether streamed text has already been committed to the active output sink. */
26
+ textOutputCommitted(): boolean;
25
27
  thinkingLevel(): ThinkingLevel | undefined;
26
28
  configuredThinkingLevel(): ConfiguredThinkingLevel | undefined;
27
29
  setThinkingLevel(level: ConfiguredThinkingLevel | undefined): void;
@@ -100,6 +102,8 @@ export declare class TurnRecovery {
100
102
  }): Promise<RecoveryCompactionResult>;
101
103
  /** Restores the configured primary after fallback cooldown expiry. */
102
104
  maybeRestoreRetryFallbackPrimary(): Promise<void>;
105
+ /** Applies model fallback policy from live usage health before a turn starts. */
106
+ maybeApplyUsageAwareFallback(signal: AbortSignal, confirmer?: UsageFallbackConfirmer): Promise<boolean>;
103
107
  /** Applies automatic retry, credential rotation, and model fallback policy. */
104
108
  handleRetryableError(message: AssistantMessage, options?: {
105
109
  allowModelFallback?: boolean;
@@ -181,7 +185,7 @@ export declare class TurnRecovery {
181
185
  pinFallback?: boolean;
182
186
  apiKey?: string;
183
187
  signal?: AbortSignal;
184
- }): Promise<void>;
188
+ }): Promise<boolean>;
185
189
  /**
186
190
  * True when the current turn failed on a Fireworks Fast (`-fast`) model in a
187
191
  * way that should degrade to the reliable base (Standard) model. Fast is a
@@ -209,6 +209,8 @@ export interface ExecutorOptions {
209
209
  * set this false so disposal unregisters them instead of leaving idle peers.
210
210
  */
211
211
  keepAlive?: boolean;
212
+ /** Internal ownership handoff for cleanup that outlives the visible Task result. */
213
+ onCleanupDeferred?: (completion: Promise<void>) => void;
212
214
  }
213
215
  interface FinalizeSubprocessOutputArgs {
214
216
  rawOutput: string;
@@ -294,6 +296,8 @@ export declare function finalizeSubagentLifecycle(args: {
294
296
  isolated: boolean;
295
297
  agentIdleTtlMs: number;
296
298
  reviveSession: AgentReviver | null;
299
+ cleanupDeadlineAt?: number;
300
+ onCleanupDeferred?: (completion: Promise<void>) => void;
297
301
  }): Promise<void>;
298
302
  /** Options for {@link runSubagentFollowUpTurn}. */
299
303
  export interface FollowUpTurnOptions {
@@ -115,6 +115,8 @@ export declare function applyStealthPatches(browser: Browser, page: Page, state:
115
115
  browserSession: CDPSession | null;
116
116
  override: UserAgentOverride | null;
117
117
  }): Promise<void>;
118
+ /** Exposes executable candidates for detection tests. */
119
+ export declare function systemChromiumCandidatesForTest(platform?: NodeJS.Platform, home?: string, which?: (name: string) => string | undefined): string[];
118
120
  export declare function stealthIgnoreDefaultArgsForTest(executablePath: string | undefined): string[];
119
121
  export declare function targetSupportsUserAgentOverrideForTest(target: Target): boolean;
120
122
  export declare function configureUserAgentTargetsForTest(browser: Browser, state: {
@@ -44,6 +44,13 @@ export declare function splitPathAndSel(rawPath: string): {
44
44
  * plus selector `1-2` (issue #4618). `lstat` inspects the entry itself, so a
45
45
  * dangling symlink is still detected as present; ambiguous errors resolve to
46
46
  * `"unknown"` so callers keep the raw path instead of guessing.
47
+ *
48
+ * `ENAMETOOLONG` resolves to `"missing"` rather than `"unknown"`: a path whose
49
+ * component or whole length exceeds the OS limit can never name a real single
50
+ * entry, so it is strictly stronger evidence of non-existence than `ENOENT`.
51
+ * Without this, a semicolon-joined `path` list long enough to trip the limit
52
+ * (bare filenames past `NAME_MAX`, or a total past `PATH_MAX`) was read as one
53
+ * literal path and the delimited split was suppressed (issue #7597).
47
54
  */
48
55
  export declare function probeLiteralPathExists(filePath: string, cwd: string): Promise<"exists" | "missing" | "unknown">;
49
56
  /**
@@ -28,6 +28,8 @@ export interface ReadToolDetails {
28
28
  meta?: OutputMeta;
29
29
  /** Full on-disk byte size recorded before applying a file range. */
30
30
  fileSize?: number;
31
+ /** Full source line count when the read reached EOF and the count is exact. */
32
+ totalLines?: number;
31
33
  /** Raw text + start line for user-visible TUI rendering, set when content is text-like.
32
34
  * Mirrors the same lines the model receives but without hashline/line-number prefixes,
33
35
  * so the TUI can render the file content with its own gutter without re-parsing the formatted text. */
@@ -13,13 +13,31 @@
13
13
  */
14
14
  export declare function tokenizeShellSegments(command: string): string[][];
15
15
  /**
16
- * Returns the original text of flat shell command segments. Unlike
16
+ * A flat shell command segment with the context needed to decide interception.
17
+ *
18
+ * @see extractFlatShellCommandSegments
19
+ */
20
+ export interface FlatShellCommandSegment {
21
+ /** Original segment text with quoting and escaping preserved. */
22
+ text: string;
23
+ /**
24
+ * True when this segment consumes the previous stage's stdout via an
25
+ * unquoted `|` or `|&`. Blank and comment-only continuation lines preserve
26
+ * the pending pipe state. Such a stage reads piped stdin, so path-based
27
+ * dedicated tools (read/grep/glob) cannot replace it. `||`, `;`, `&`, and
28
+ * `&&` start an independent command and leave this false.
29
+ */
30
+ pipedStdin: boolean;
31
+ }
32
+ /**
33
+ * Returns the flat shell command segments with the original text of each. Unlike
17
34
  * `tokenizeShellSegments`, this preserves quoting and escaping so the results
18
- * are safe to match against user-configured regular expressions.
35
+ * are safe to match against user-configured regular expressions, and flags
36
+ * segments that receive piped stdin.
19
37
  *
20
38
  * The extractor deliberately declines to split syntax whose execution context
21
39
  * cannot be determined with this small scanner (heredocs, command substitution,
22
40
  * backticks, grouping, and malformed quoting). Callers must still check the
23
41
  * complete input in that case.
24
42
  */
25
- export declare function extractFlatShellCommandSegments(command: string): string[];
43
+ export declare function extractFlatShellCommandSegments(command: string): FlatShellCommandSegment[];
@@ -39,9 +39,10 @@ export declare function formatStartupChangelogSummary(selection: StartupChangelo
39
39
  export declare function resolveBundledChangelogPath(assetPath: string, moduleUrl: string | URL): string | URL;
40
40
  export declare function parseChangelog(changelogPath: string | undefined): Promise<ChangelogEntry[]>;
41
41
  /**
42
- * Compare versions. Returns: -1 if v1 < v2, 0 if v1 === v2, 1 if v1 > v2
42
+ * Compare changelog entries by their parsed version parts.
43
+ * Returns: -1 if v1 < v2, 0 if v1 === v2, 1 if v1 > v2
43
44
  */
44
- export declare function compareVersions(v1: ChangelogEntry, v2: ChangelogEntry): number;
45
+ export declare function compareChangelogEntries(v1: ChangelogEntry, v2: ChangelogEntry): number;
45
46
  /**
46
47
  * Parse an omp changelog marker version into comparable parts.
47
48
  */
@@ -0,0 +1,2 @@
1
+ /** Keep timed-out cleanup reachable until its resources really settle. */
2
+ export declare function trackLateCleanup(work: Promise<void>, context: Record<string, unknown>): void;
@@ -113,6 +113,8 @@ export declare class VibeSessionRegistry {
113
113
  ownerId: string;
114
114
  state?: VibeSessionState;
115
115
  }): void;
116
+ /** Override the teardown grace period for deterministic lifecycle tests. */
117
+ setTeardownGraceForTesting(timeoutMs: number): void;
116
118
  ownerScope(session: VibeParentSession): VibeOwnerScope;
117
119
  /** Re-open spawn admission after an explicit Vibe-mode entry. */
118
120
  activateScope(scope: VibeOwnerScope): void;
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": "17.2.7",
4
+ "version": "17.2.9",
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",
@@ -52,18 +52,18 @@
52
52
  "@agentclientprotocol/sdk": "1.2.1",
53
53
  "@babel/parser": "^7.29.7",
54
54
  "@mozilla/readability": "^0.6.0",
55
- "@oh-my-pi/hashline": "17.2.7",
56
- "@oh-my-pi/omp-stats": "17.2.7",
57
- "@oh-my-pi/omptype": "17.2.7",
58
- "@oh-my-pi/pi-agent-core": "17.2.7",
59
- "@oh-my-pi/pi-ai": "17.2.7",
60
- "@oh-my-pi/pi-catalog": "17.2.7",
61
- "@oh-my-pi/pi-mnemopi": "17.2.7",
62
- "@oh-my-pi/pi-natives": "17.2.7",
63
- "@oh-my-pi/pi-tui": "17.2.7",
64
- "@oh-my-pi/pi-utils": "17.2.7",
65
- "@oh-my-pi/pi-wire": "17.2.7",
66
- "@oh-my-pi/snapcompact": "17.2.7",
55
+ "@oh-my-pi/hashline": "17.2.9",
56
+ "@oh-my-pi/omp-stats": "17.2.9",
57
+ "@oh-my-pi/omptype": "17.2.9",
58
+ "@oh-my-pi/pi-agent-core": "17.2.9",
59
+ "@oh-my-pi/pi-ai": "17.2.9",
60
+ "@oh-my-pi/pi-catalog": "17.2.9",
61
+ "@oh-my-pi/pi-mnemopi": "17.2.9",
62
+ "@oh-my-pi/pi-natives": "17.2.9",
63
+ "@oh-my-pi/pi-tui": "17.2.9",
64
+ "@oh-my-pi/pi-utils": "17.2.9",
65
+ "@oh-my-pi/pi-wire": "17.2.9",
66
+ "@oh-my-pi/snapcompact": "17.2.9",
67
67
  "@opentelemetry/api": "^1.9.1",
68
68
  "@opentelemetry/api-logs": "^0.220.0",
69
69
  "@opentelemetry/context-async-hooks": "^2.9.0",
package/scripts/omp CHANGED
@@ -36,7 +36,16 @@ mkdir -p "$launch_dir"
36
36
  OMP_LAUNCH_CWD=$PWD
37
37
  export OMP_LAUNCH_CWD
38
38
  cd "$launch_dir"
39
+ # Herdr 0.7.5 validates the foreground process name. The macOS shell can
40
+ # preserve OMP's identity while still running the development CLI through Bun.
41
+ run_bun() {
42
+ if [ "${HERDR_ENV:-}" = 1 ] && [ "$(uname -s)" = Darwin ]; then
43
+ exec -a omp bun "$@"
44
+ fi
45
+ exec bun "$@"
46
+ }
47
+
39
48
  if [ -n "${PI_TIMING:-}" ]; then
40
- exec bun --preload "$preload" --preload "$timing_preload" "$cli" "$@"
49
+ run_bun --preload "$preload" --preload "$timing_preload" "$cli" "$@"
41
50
  fi
42
- exec bun --preload "$preload" "$cli" "$@"
51
+ run_bun --preload "$preload" "$cli" "$@"
@@ -94,6 +94,12 @@ export interface AsyncJobDeliveryState {
94
94
  pendingJobIds: string[];
95
95
  }
96
96
 
97
+ export interface AsyncJobReapResult {
98
+ settled: boolean;
99
+ pendingJobIds: string[];
100
+ completion: Promise<void>;
101
+ }
102
+
97
103
  export interface AsyncJobRegisterOptions {
98
104
  id?: string;
99
105
  /** Registry id of the agent that owns this job; used to scope cancelAll. */
@@ -490,6 +496,26 @@ export class AsyncJobManager {
490
496
  }
491
497
  }
492
498
 
499
+ /**
500
+ * Cancel every job owned by `ownerId`, then wait only until `deadlineAt`.
501
+ * The returned completion keeps waiting for actual process settlement when
502
+ * the deadline expires, so callers can move that cleanup out of the
503
+ * user-visible Task wait without losing ownership of the live work.
504
+ */
505
+ async cancelAndReapOwnerJobs(ownerId: string, deadlineAt: number): Promise<AsyncJobReapResult> {
506
+ this.cancelAll({ ownerId });
507
+ const timeoutMs = Math.max(0, deadlineAt - Date.now());
508
+ const settled = await this.waitForOwnerJobs(ownerId, { timeoutMs });
509
+ if (settled) {
510
+ return { settled: true, pendingJobIds: [], completion: Promise.resolve() };
511
+ }
512
+ const pendingJobIds = this.getAllJobs({ ownerId })
513
+ .filter(job => job.status === "running" || job.status === "cancelled")
514
+ .map(job => job.id);
515
+ const completion = this.waitForOwnerJobs(ownerId).then(() => {});
516
+ return { settled: false, pendingJobIds, completion };
517
+ }
518
+
493
519
  async #waitForAllUntil(deadline: number): Promise<boolean> {
494
520
  const promises = Array.from(this.#jobs.values()).map(job => job.promise);
495
521
  if (promises.length === 0) return true;
@@ -136,11 +136,11 @@ export async function runStatsCommand(cmd: StatsCommandArgs): Promise<void> {
136
136
  }
137
137
 
138
138
  // Start the dashboard server
139
- const { port } = await startServer(cmd.port);
140
- console.log(chalk.green(`Dashboard available at: http://localhost:${port}`));
139
+ const { hostname, port } = await startServer(cmd.port);
140
+ const url = `http://${hostname}:${port}`;
141
+ console.log(chalk.green(`Dashboard available at: ${url}`));
141
142
 
142
143
  // Open browser
143
- const url = `http://localhost:${port}`;
144
144
  openPath(url);
145
145
 
146
146
  console.log("Press Ctrl+C to stop\n");
@@ -10,7 +10,7 @@ import * as os from "node:os";
10
10
  import * as path from "node:path";
11
11
  import { Transform } from "node:stream";
12
12
  import { pipeline } from "node:stream/promises";
13
- import { $env, $which, APP_NAME, isEnoent, VERSION } from "@oh-my-pi/pi-utils";
13
+ import { $env, $which, APP_NAME, compareVersions, isEnoent, VERSION } from "@oh-my-pi/pi-utils";
14
14
  import { $ } from "bun";
15
15
  import chalk from "chalk";
16
16
  import { theme } from "../modes/theme/theme";
@@ -498,24 +498,6 @@ async function getLatestRelease(): Promise<ReleaseInfo> {
498
498
  };
499
499
  }
500
500
 
501
- /**
502
- * Compare semver versions. Returns:
503
- * - negative if a < b
504
- * - 0 if a == b
505
- * - positive if a > b
506
- */
507
- function compareVersions(a: string, b: string): number {
508
- const pa = a.split(".").map(Number);
509
- const pb = b.split(".").map(Number);
510
-
511
- for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
512
- const na = pa[i] || 0;
513
- const nb = pb[i] || 0;
514
- if (na !== nb) return na - nb;
515
- }
516
- return 0;
517
- }
518
-
519
501
  interface BunInstallCachePruneResult {
520
502
  scannedPackages: number;
521
503
  removedEntries: number;
@@ -532,42 +514,6 @@ function stripBunCacheVersionSuffix(name: string): string {
532
514
  return metadataIndex === -1 ? name : name.slice(0, metadataIndex);
533
515
  }
534
516
 
535
- function compareSemverIdentifier(a: string, b: string): number {
536
- const aNumber = /^\d+$/.test(a);
537
- const bNumber = /^\d+$/.test(b);
538
- if (aNumber && bNumber) return Number(a) - Number(b);
539
- if (aNumber) return -1;
540
- if (bNumber) return 1;
541
- return a.localeCompare(b);
542
- }
543
-
544
- function compareSemverLikeVersions(a: string, b: string): number {
545
- const [aCoreWithPrerelease] = a.split("+", 1);
546
- const [bCoreWithPrerelease] = b.split("+", 1);
547
- const [aCore, aPrerelease] = aCoreWithPrerelease.split("-", 2);
548
- const [bCore, bPrerelease] = bCoreWithPrerelease.split("-", 2);
549
- const aParts = aCore.split(".");
550
- const bParts = bCore.split(".");
551
- for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) {
552
- const diff = Number(aParts[i] ?? 0) - Number(bParts[i] ?? 0);
553
- if (diff !== 0 && Number.isFinite(diff)) return diff;
554
- }
555
- if (!aPrerelease && !bPrerelease) return 0;
556
- if (!aPrerelease) return 1;
557
- if (!bPrerelease) return -1;
558
- const aPrereleaseParts = aPrerelease.split(".");
559
- const bPrereleaseParts = bPrerelease.split(".");
560
- for (let i = 0; i < Math.max(aPrereleaseParts.length, bPrereleaseParts.length); i++) {
561
- const aPart = aPrereleaseParts[i];
562
- const bPart = bPrereleaseParts[i];
563
- if (aPart === undefined) return -1;
564
- if (bPart === undefined) return 1;
565
- const diff = compareSemverIdentifier(aPart, bPart);
566
- if (diff !== 0) return diff;
567
- }
568
- return 0;
569
- }
570
-
571
517
  async function readdirIfExists(dir: string): Promise<fs.Dirent[]> {
572
518
  try {
573
519
  return await fs.promises.readdir(dir, { withFileTypes: true });
@@ -689,7 +635,7 @@ export async function pruneBunInstallCache(
689
635
  scannedPackages++;
690
636
  let latestVersion: string | undefined;
691
637
  for (const version of group.actualDirs.keys()) {
692
- if (!latestVersion || compareSemverLikeVersions(version, latestVersion) > 0) latestVersion = version;
638
+ if (!latestVersion || compareVersions(version, latestVersion) > 0) latestVersion = version;
693
639
  }
694
640
  if (!latestVersion) continue;
695
641
  for (const [version, paths] of group.actualDirs) {
@@ -109,11 +109,11 @@ export class ConfigError extends Error {
109
109
  this.#message = message;
110
110
  }
111
111
 
112
- get message(): string {
112
+ override get message(): string {
113
113
  return this.#message;
114
114
  }
115
115
 
116
- toString(): string {
116
+ override toString(): string {
117
117
  return this.message;
118
118
  }
119
119
  }
@@ -607,12 +607,12 @@ export class KeybindingsManager extends TuiKeybindingsManager {
607
607
  this.setUserBindings(mergeKeybindingsConfig(inheritedConfig, profileConfig));
608
608
  }
609
609
 
610
- setUserBindings(userBindings: KeybindingsConfig): void {
610
+ override setUserBindings(userBindings: KeybindingsConfig): void {
611
611
  this.#userBindings = userBindings;
612
612
  super.setUserBindings(userBindings);
613
613
  }
614
614
 
615
- getKeys(keybinding: Keybinding): KeyId[] {
615
+ override getKeys(keybinding: Keybinding): KeyId[] {
616
616
  const keys = super.getKeys(keybinding);
617
617
  const fallbackKey = getFallbackKey(keybinding);
618
618
  if (fallbackKey === undefined || this.#userBindings[keybinding] !== undefined) return keys;
@@ -620,7 +620,7 @@ export class KeybindingsManager extends TuiKeybindingsManager {
620
620
  return removeKey(keys, fallbackKey);
621
621
  }
622
622
 
623
- getResolvedBindings(): KeybindingsConfig {
623
+ override getResolvedBindings(): KeybindingsConfig {
624
624
  const resolved = super.getResolvedBindings();
625
625
  resolved[FOLLOW_UP_KEYBINDING] = keyConfigValue(this.getKeys(FOLLOW_UP_KEYBINDING));
626
626
  return resolved;
@@ -724,6 +724,31 @@ export async function discoverLlamaCppModelRuntimeMetadata(
724
724
  }
725
725
  }
726
726
 
727
+ /**
728
+ * Read image-input support from an OpenAI-compatible `/v1/models` row. Handles
729
+ * direct `input` arrays, Synthetic-style top-level `input_modalities`, and
730
+ * OpenRouter-style `architecture.input_modalities`; returns undefined when none
731
+ * is present so the bundled reference (or the `["text"]` default) can take over.
732
+ */
733
+ function extractOpenAIModelsListInputCapabilities(item: {
734
+ input?: unknown;
735
+ input_modalities?: unknown;
736
+ architecture?: unknown;
737
+ }): ("text" | "image")[] | undefined {
738
+ const modalities = new Set<string>();
739
+ const collect = (value: unknown): void => {
740
+ if (!Array.isArray(value)) return;
741
+ for (const entry of value) {
742
+ if (typeof entry === "string") modalities.add(entry.toLowerCase());
743
+ }
744
+ };
745
+ collect(item.input);
746
+ collect(item.input_modalities);
747
+ if (isRecord(item.architecture)) collect(item.architecture.input_modalities);
748
+ if (modalities.size === 0) return undefined;
749
+ return modalities.has("image") ? ["text", "image"] : ["text"];
750
+ }
751
+
727
752
  export async function discoverOpenAIModelsList(
728
753
  providerConfig: DiscoveryProviderConfig,
729
754
  ctx: DiscoveryContext,
@@ -752,7 +777,14 @@ export async function discoverOpenAIModelsList(
752
777
  }
753
778
  headers = h;
754
779
  return (await res.json()) as {
755
- data?: Array<{ id?: string; max_model_len?: unknown; context_length?: unknown }>;
780
+ data?: Array<{
781
+ id?: string;
782
+ max_model_len?: unknown;
783
+ context_length?: unknown;
784
+ input?: unknown;
785
+ input_modalities?: unknown;
786
+ architecture?: unknown;
787
+ }>;
756
788
  };
757
789
  }),
758
790
  nativeMetadataPromise,
@@ -796,7 +828,9 @@ export async function discoverOpenAIModelsList(
796
828
  baseUrl,
797
829
  reasoning: reference?.reasoning ?? false,
798
830
  thinking: inheritReferenceThinking(undefined, reference, providerConfig.provider),
799
- input: nativeMetadataForModel?.input ?? reference?.input ?? ["text"],
831
+ input: nativeMetadataForModel?.input ??
832
+ extractOpenAIModelsListInputCapabilities(item) ??
833
+ reference?.input ?? ["text"],
800
834
  ...(providerConfig.discovery.type === "lm-studio" ? { imageInputDecoder: "stb" as const } : {}),
801
835
  // Proxy/gateway pricing is provider-specific and rarely matches
802
836
  // upstream bundled catalogs, so keep costs local-unknown even
@@ -1715,7 +1715,10 @@ export class ModelRegistry {
1715
1715
  return resolveOllamaModelCacheProviderId(providerConfig.provider, providerConfig.baseUrl);
1716
1716
  }
1717
1717
  if (providerConfig.discovery.type === "openai-models-list") {
1718
- return `${providerConfig.provider}:openai-models-list-context-v2`;
1718
+ // context-v3 invalidates rows cached before server-advertised input
1719
+ // modalities were parsed from `/v1/models`; warm v2 rows pinned
1720
+ // vision-capable ids at `input: ["text"]` until a forced refresh.
1721
+ return `${providerConfig.provider}:openai-models-list-context-v3`;
1719
1722
  }
1720
1723
  if (providerConfig.discovery.type === "litellm") {
1721
1724
  // rich-v2 invalidates rows cached before reseller usage-suffix stripping