@oh-my-pi/pi-coding-agent 15.1.9 → 15.2.2
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.
- package/CHANGELOG.md +36 -1
- package/dist/types/config/settings-schema.d.ts +10 -0
- package/dist/types/eval/py/kernel.d.ts +6 -0
- package/dist/types/goals/state.d.ts +1 -1
- package/dist/types/goals/tools/goal-tool.d.ts +4 -0
- package/dist/types/hashline/parser.d.ts +6 -2
- package/dist/types/internal-urls/memory-protocol.d.ts +6 -0
- package/dist/types/modes/theme/shimmer.d.ts +27 -0
- package/dist/types/slash-commands/helpers/format.d.ts +4 -1
- package/dist/types/tools/ast-edit.d.ts +3 -0
- package/dist/types/tools/ast-grep.d.ts +3 -0
- package/dist/types/tools/browser/launch.d.ts +2 -0
- package/dist/types/tools/find.d.ts +3 -0
- package/dist/types/tools/search.d.ts +3 -0
- package/dist/types/tui/file-list.d.ts +6 -0
- package/dist/types/tui/hyperlink.d.ts +42 -0
- package/dist/types/tui/index.d.ts +1 -0
- package/dist/types/web/search/providers/utils.d.ts +2 -1
- package/package.json +7 -7
- package/src/config/settings-schema.ts +12 -0
- package/src/config/settings.ts +28 -5
- package/src/discovery/builtin.ts +30 -0
- package/src/edit/renderer.ts +5 -3
- package/src/eval/py/executor.ts +12 -1
- package/src/eval/py/kernel.ts +24 -8
- package/src/extensibility/plugins/legacy-pi-compat.ts +2 -2
- package/src/goals/runtime.ts +9 -3
- package/src/goals/state.ts +1 -1
- package/src/goals/tools/goal-tool.ts +12 -2
- package/src/hashline/diff.ts +1 -1
- package/src/hashline/execute.ts +2 -2
- package/src/hashline/parser.ts +87 -12
- package/src/internal-urls/memory-protocol.ts +1 -1
- package/src/modes/interactive-mode.ts +29 -1
- package/src/modes/theme/shimmer.ts +79 -0
- package/src/prompts/tools/goal.md +7 -2
- package/src/session/agent-session.ts +18 -75
- package/src/slash-commands/helpers/format.ts +23 -3
- package/src/task/executor.ts +115 -19
- package/src/tools/ast-edit.ts +39 -6
- package/src/tools/ast-grep.ts +38 -6
- package/src/tools/browser/launch.ts +63 -51
- package/src/tools/find.ts +13 -2
- package/src/tools/read.ts +46 -6
- package/src/tools/search.ts +447 -265
- package/src/tui/file-list.ts +10 -2
- package/src/tui/hyperlink.ts +126 -0
- package/src/tui/index.ts +1 -0
- package/src/web/search/index.ts +13 -9
- package/src/web/search/providers/anthropic.ts +3 -1
- package/src/web/search/providers/brave.ts +3 -1
- package/src/web/search/providers/codex.ts +3 -1
- package/src/web/search/providers/exa.ts +3 -1
- package/src/web/search/providers/gemini.ts +3 -1
- package/src/web/search/providers/jina.ts +3 -1
- package/src/web/search/providers/kagi.ts +5 -1
- package/src/web/search/providers/kimi.ts +3 -1
- package/src/web/search/providers/parallel.ts +5 -1
- package/src/web/search/providers/perplexity.ts +5 -1
- package/src/web/search/providers/searxng.ts +3 -1
- package/src/web/search/providers/synthetic.ts +3 -1
- package/src/web/search/providers/tavily.ts +3 -1
- package/src/web/search/providers/utils.ts +33 -1
- package/src/web/search/providers/zai.ts +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,37 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [15.2.2] - 2026-05-22
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Fixed `RULES.md` not being injected. The documented sticky-rules file at `~/.omp/agent/RULES.md` and `<repo>/.omp/RULES.md` was never read by any discovery provider; only `.omp/rules/*.md` was scanned. The native provider now loads both as always-apply rules so they re-attach every turn as documented ([#1266](https://github.com/can1357/oh-my-pi/issues/1266)).
|
|
10
|
+
|
|
11
|
+
## [15.2.1] - 2026-05-21
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- Fixed compaction routing to the wrong provider when `modelRoles.default` is set to a different model than the active chat. Auto- and manual compaction now prefer the active session's model and only fall back to role-based candidates when the current model has no usable credentials. Previously, an Anthropic chat with `modelRoles.default = openai/gpt-5` would compact through OpenAI (including the remote-compaction endpoint), even though the live conversation never used OpenAI.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- Fixed context overflow not being detected when the session's model (e.g. `hf:zai-org/GLM-5.1` via `synthetic` provider) returns a 400 with the upstream "400 status code (no body)" message wrapped inside a JSON envelope. The `isContextOverflow` check now matches the no-body status phrase anywhere in the error string rather than requiring it at the very start, so auto-compaction fires correctly instead of leaving the session silently stuck ([#1251](https://github.com/can1357/oh-my-pi/issues/1251)).
|
|
20
|
+
- Fixed `formatCapturedHttpError` not extracting the error text from `{"error":"string"}` response bodies (only object-valued `error` fields were previously handled), resulting in raw JSON in error messages instead of the human-readable string.
|
|
21
|
+
|
|
22
|
+
## [15.2.0] - 2026-05-21
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- Changed the interactive working loader and slash-command progress bars to use the active theme's accent shimmer instead of static muted text.
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- Fixed false-positive hashline `~ TEXT` separator-padding warning firing on YAML, JSON, Python, Markdown, TOML, and other indent-sensitive file edits. The padding check is now skipped entirely for indentation-sensitive extensions (`.py`, `.yml`/`.yaml`, `.md`/`.mdx`, `.json`/`.jsonc`/`.json5`, `.toml`, `.rst`, `.tf`, `.nix`, `.coffee`, `.haml`/`.slim`/`.pug`, `.sass`/`.styl`, `.nim`, `.cr`, `.elm`, `.fs`, …), and tightened on every other extension to flag only the `~ beta` typo shape (exactly one leading space before non-space content) rather than any leading-space payload.
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
|
|
34
|
+
- Fixed goal state machine: `get` now returns paused goals (was returning null for `enabled=false`), `complete` now works on paused goals after interrupts, `create` is allowed after a previous goal reaches `complete` status, and the goal tool is re-added to the active tool set on session reload when a paused goal is persisted. Added `resume` and `drop` ops to the goal tool so the agent can re-engage or discard a paused goal without requiring user slash commands. ([#1249](https://github.com/can1357/oh-my-pi/issues/1249))
|
|
35
|
+
|
|
5
36
|
## [15.1.9] - 2026-05-21
|
|
6
37
|
|
|
7
38
|
### Fixed
|
|
@@ -24,6 +55,10 @@
|
|
|
24
55
|
|
|
25
56
|
- Fixed `web_search` freezing the session when an upstream provider stalled. Bun's WinHTTP backend on Windows can silently drop `AbortSignal` once a TCP/TLS connection hangs (oven-sh/bun#15275, oven-sh/bun#18536), so Esc never reached the in-flight fetch and the only recovery was Ctrl+C + `omp --resume`. Every web-search provider's outbound `fetch` (anthropic, brave, codex, exa, gemini, jina, kagi, kimi, parallel, perplexity, searxng, synthetic, tavily, z.ai) now composes the caller signal with a 60s hard timeout via a shared `withHardTimeout` helper, guaranteeing the request settles within a minute even when Bun's abort fails to propagate. Independently, `executeSearch`'s provider-fallback loop was masking real cancellations as ordinary provider errors and returning "All web search providers failed"; it now re-throws as `ToolAbortError` the moment the caller's signal aborts, so the session sees a clean cancel on every platform. ([#1221](https://github.com/can1357/oh-my-pi/issues/1221))
|
|
26
57
|
|
|
58
|
+
### Added
|
|
59
|
+
|
|
60
|
+
- Added OSC 8 terminal hyperlink support for file paths in tool output. When the terminal supports hyperlinks (kitty, Ghostty, WezTerm, iTerm2, Alacritty, VS Code) and the new `tui.hyperlinks` setting is `auto` (default) or `always`, OMP wraps file paths emitted by `read`, `find`, `search`, `edit`, `ast_grep`, and `ast_edit` renderers in `file:///abs/path` hyperlinks. `local://` and other fs-backed internal URLs resolve to their backing path. Set `tui.hyperlinks: off` to disable. ([#1244](https://github.com/can1357/oh-my-pi/issues/1244))
|
|
61
|
+
|
|
27
62
|
## [15.1.8] - 2026-05-20
|
|
28
63
|
|
|
29
64
|
### Fixed
|
|
@@ -8476,4 +8511,4 @@ Initial public release.
|
|
|
8476
8511
|
- Git branch display in footer
|
|
8477
8512
|
- Message queueing during streaming responses
|
|
8478
8513
|
- OAuth integration for Gmail and Google Calendar access
|
|
8479
|
-
- HTML export with syntax highlighting and collapsible sections
|
|
8514
|
+
- HTML export with syntax highlighting and collapsible sections
|
|
@@ -634,6 +634,16 @@ export declare const SETTINGS_SCHEMA: {
|
|
|
634
634
|
readonly default: 20;
|
|
635
635
|
readonly description: "Maximum height in terminal rows for inline images (default 20). Set to 0 to use only the viewport-based limit (60% of terminal height).";
|
|
636
636
|
};
|
|
637
|
+
readonly "tui.hyperlinks": {
|
|
638
|
+
readonly type: "enum";
|
|
639
|
+
readonly values: readonly ["off", "auto", "always"];
|
|
640
|
+
readonly default: "auto";
|
|
641
|
+
readonly ui: {
|
|
642
|
+
readonly tab: "appearance";
|
|
643
|
+
readonly label: "Terminal Hyperlinks";
|
|
644
|
+
readonly description: "Wrap file paths in OSC 8 hyperlinks for terminal-native click-to-open (auto: detect support; off: never; always: unconditional)";
|
|
645
|
+
};
|
|
646
|
+
};
|
|
637
647
|
readonly "display.tabWidth": {
|
|
638
648
|
readonly type: "number";
|
|
639
649
|
readonly default: 3;
|
|
@@ -21,6 +21,12 @@ export interface KernelExecuteResult {
|
|
|
21
21
|
cancelled: boolean;
|
|
22
22
|
timedOut: boolean;
|
|
23
23
|
stdinRequested: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* True when the kernel subprocess was killed as part of settling this
|
|
26
|
+
* execution (e.g. SIGINT was ignored and we escalated to shutdown, or the
|
|
27
|
+
* kernel died unexpectedly). When false, the kernel remains reusable.
|
|
28
|
+
*/
|
|
29
|
+
kernelKilled?: boolean;
|
|
24
30
|
}
|
|
25
31
|
export interface KernelShutdownResult {
|
|
26
32
|
confirmed: boolean;
|
|
@@ -17,7 +17,7 @@ export interface GoalModeState {
|
|
|
17
17
|
goal: Goal;
|
|
18
18
|
}
|
|
19
19
|
export interface GoalToolDetails {
|
|
20
|
-
op: "create" | "get" | "complete";
|
|
20
|
+
op: "create" | "get" | "complete" | "resume" | "drop";
|
|
21
21
|
goal?: Goal | null;
|
|
22
22
|
remainingTokens?: number | null;
|
|
23
23
|
completionBudgetReport?: string | null;
|
|
@@ -9,7 +9,9 @@ declare const goalSchema: z.ZodObject<{
|
|
|
9
9
|
op: z.ZodEnum<{
|
|
10
10
|
complete: "complete";
|
|
11
11
|
create: "create";
|
|
12
|
+
drop: "drop";
|
|
12
13
|
get: "get";
|
|
14
|
+
resume: "resume";
|
|
13
15
|
}>;
|
|
14
16
|
objective: z.ZodOptional<z.ZodString>;
|
|
15
17
|
token_budget: z.ZodOptional<z.ZodNumber>;
|
|
@@ -32,7 +34,9 @@ export declare class GoalTool implements AgentTool<typeof goalSchema, GoalToolDe
|
|
|
32
34
|
op: z.ZodEnum<{
|
|
33
35
|
complete: "complete";
|
|
34
36
|
create: "create";
|
|
37
|
+
drop: "drop";
|
|
35
38
|
get: "get";
|
|
39
|
+
resume: "resume";
|
|
36
40
|
}>;
|
|
37
41
|
objective: z.ZodOptional<z.ZodString>;
|
|
38
42
|
token_budget: z.ZodOptional<z.ZodNumber>;
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import type { HashlineCursor, HashlineEdit } from "./types";
|
|
2
2
|
export declare function cloneCursor(cursor: HashlineCursor): HashlineCursor;
|
|
3
|
-
export declare function parseHashline(diff: string): HashlineEdit[];
|
|
4
|
-
export
|
|
3
|
+
export declare function parseHashline(diff: string, opts?: ParseHashlineOptions): HashlineEdit[];
|
|
4
|
+
export interface ParseHashlineOptions {
|
|
5
|
+
/** File path the diff targets. Used to suppress indent-sensitive false-positive warnings. */
|
|
6
|
+
path?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function parseHashlineWithWarnings(diff: string, opts?: ParseHashlineOptions): {
|
|
5
9
|
edits: HashlineEdit[];
|
|
6
10
|
warnings: string[];
|
|
7
11
|
};
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import type { InternalResource, InternalUrl, ProtocolHandler } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* Snapshot of memory roots for every registered session, deduped.
|
|
4
|
+
* Each session has its own cwd (possibly a worktree), so subagents and main
|
|
5
|
+
* may see different roots.
|
|
6
|
+
*/
|
|
7
|
+
export declare function memoryRootsFromRegistry(): string[];
|
|
2
8
|
/**
|
|
3
9
|
* Resolve a memory:// URL to an absolute filesystem path under memory root.
|
|
4
10
|
*/
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Theme, ThemeColor } from "./theme";
|
|
2
|
+
type ShimmerTheme = Pick<Theme, "bold" | "fg">;
|
|
3
|
+
/** Three-tier color stack a shimmer character cycles through as the band sweeps. */
|
|
4
|
+
export interface ShimmerPalette {
|
|
5
|
+
/** Color for chars outside / at the edge of the band (intensity < 0.2). */
|
|
6
|
+
low: ThemeColor;
|
|
7
|
+
/** Color for chars approaching the crest (0.2 <= intensity < 0.6). */
|
|
8
|
+
mid: ThemeColor;
|
|
9
|
+
/** Color at the band's crest (intensity >= 0.6). */
|
|
10
|
+
high: ThemeColor;
|
|
11
|
+
/** Whether to bold the crest tier. Default `false`. */
|
|
12
|
+
bold?: boolean;
|
|
13
|
+
}
|
|
14
|
+
/** One run of text that shares a palette inside a larger shimmer sweep. */
|
|
15
|
+
export interface ShimmerSegment {
|
|
16
|
+
text: string;
|
|
17
|
+
palette?: ShimmerPalette;
|
|
18
|
+
}
|
|
19
|
+
export declare const DEFAULT_SHIMMER_PALETTE: ShimmerPalette;
|
|
20
|
+
/**
|
|
21
|
+
* Apply a shimmer sweep across one or more segments, treating them as a single
|
|
22
|
+
* continuous string for band positioning. Each segment can supply its own
|
|
23
|
+
* palette so the gradient stays in lockstep while the colors differ.
|
|
24
|
+
*/
|
|
25
|
+
export declare function shimmerSegments(segments: readonly ShimmerSegment[], theme: ShimmerTheme): string;
|
|
26
|
+
export declare function shimmerText(text: string, theme: ShimmerTheme, palette?: ShimmerPalette): string;
|
|
27
|
+
export {};
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import { type Theme } from "../../modes/theme/theme";
|
|
1
2
|
/** Format a millisecond duration as a coarse-grained human label. */
|
|
2
3
|
export declare function formatDuration(ms: number): string;
|
|
4
|
+
type ProgressBarTheme = Pick<Theme, "bold" | "fg">;
|
|
3
5
|
/**
|
|
4
6
|
* Render an ASCII progress bar with a trailing percent label.
|
|
5
7
|
* `fraction` is clamped to `[0, 1]`. `undefined` renders a dotted placeholder.
|
|
6
8
|
*/
|
|
7
|
-
export declare function renderAsciiBar(fraction: number | undefined, width?: number): string;
|
|
9
|
+
export declare function renderAsciiBar(fraction: number | undefined, width?: number, uiTheme?: ProgressBarTheme): string;
|
|
10
|
+
export {};
|
|
@@ -31,6 +31,9 @@ export interface AstEditToolDetails {
|
|
|
31
31
|
/** Pre-formatted text for the user-visible TUI render. Mirrors `result.text` lines but uses
|
|
32
32
|
* a `│` gutter (no model-only hashline anchors). The TUI uses this directly so it never parses model-facing text. */
|
|
33
33
|
displayContent?: string;
|
|
34
|
+
/** Absolute base directory used during the edit. Used by the renderer to resolve
|
|
35
|
+
* display-relative paths to absolute paths for OSC 8 hyperlinks. */
|
|
36
|
+
searchPath?: string;
|
|
34
37
|
}
|
|
35
38
|
export declare class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolDetails> {
|
|
36
39
|
private readonly session;
|
|
@@ -28,6 +28,9 @@ export interface AstGrepToolDetails {
|
|
|
28
28
|
/** Pre-formatted text for the user-visible TUI render. Mirrors `result.text` lines but uses
|
|
29
29
|
* a `│` gutter and `*` to mark match lines. The TUI uses this directly so it never parses model-facing text. */
|
|
30
30
|
displayContent?: string;
|
|
31
|
+
/** Absolute base directory used during search. Used by the renderer to resolve
|
|
32
|
+
* display-relative paths to absolute paths for OSC 8 hyperlinks. */
|
|
33
|
+
searchPath?: string;
|
|
31
34
|
}
|
|
32
35
|
export declare class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolDetails> {
|
|
33
36
|
private readonly session;
|
|
@@ -48,6 +48,8 @@ export interface UserAgentSession {
|
|
|
48
48
|
override: UserAgentOverride;
|
|
49
49
|
browserSession: CDPSession | null;
|
|
50
50
|
}
|
|
51
|
+
/** Builds the browser-page stealth bootstrap source for regression tests. */
|
|
52
|
+
export declare function buildStealthInjectionScriptForTest(scripts?: readonly string[]): string;
|
|
51
53
|
/** Apply stealth patches + UA override to a headless page. Idempotent within a tab. */
|
|
52
54
|
export declare function applyStealthPatches(browser: Browser, page: Page, state: {
|
|
53
55
|
browserSession: CDPSession | null;
|
|
@@ -23,6 +23,9 @@ export interface FindToolDetails {
|
|
|
23
23
|
files?: string[];
|
|
24
24
|
truncated?: boolean;
|
|
25
25
|
error?: string;
|
|
26
|
+
/** Working directory at search time. Used by the renderer to resolve relative
|
|
27
|
+
* file paths to absolute paths for OSC 8 hyperlinks. */
|
|
28
|
+
cwd?: string;
|
|
26
29
|
/** User-supplied paths whose base directory was missing on disk. The tool
|
|
27
30
|
* skipped these and continued with the surviving entries; surfaced as a
|
|
28
31
|
* non-fatal warning in the renderer and in the model-facing text. */
|
|
@@ -43,6 +43,9 @@ export interface SearchToolDetails {
|
|
|
43
43
|
* `result.text` lines but uses a `│` gutter and `*` to mark match lines (vs space for
|
|
44
44
|
* context). The TUI uses this directly so it never parses model-facing hashline anchors. */
|
|
45
45
|
displayContent?: string;
|
|
46
|
+
/** Absolute base directory used during search. Used by the renderer to resolve
|
|
47
|
+
* display-relative paths to absolute paths for OSC 8 hyperlinks. */
|
|
48
|
+
searchPath?: string;
|
|
46
49
|
/** User-supplied paths whose base directory was missing on disk. The tool
|
|
47
50
|
* skipped these and continued with the surviving entries; surfaced as a
|
|
48
51
|
* non-fatal warning in the renderer and in the model-facing text. */
|
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
import type { Theme } from "../modes/theme/theme";
|
|
5
5
|
export interface FileEntry {
|
|
6
6
|
path: string;
|
|
7
|
+
/** Absolute filesystem path. When provided together with {@link FileListOptions.hyperlinkFn}, the
|
|
8
|
+
* rendered path text is wrapped in an OSC 8 hyperlink. */
|
|
9
|
+
absPath?: string;
|
|
7
10
|
isDirectory?: boolean;
|
|
8
11
|
meta?: string;
|
|
9
12
|
}
|
|
@@ -12,5 +15,8 @@ export interface FileListOptions {
|
|
|
12
15
|
expanded?: boolean;
|
|
13
16
|
maxCollapsed?: number;
|
|
14
17
|
showIcons?: boolean;
|
|
18
|
+
/** When provided, called with the entry's absolute path and the ANSI-styled display string to
|
|
19
|
+
* optionally wrap the path in an OSC 8 hyperlink. Only invoked when {@link FileEntry.absPath} is set. */
|
|
20
|
+
hyperlinkFn?: (absPath: string, displayText: string) => string;
|
|
15
21
|
}
|
|
16
22
|
export declare function renderFileList(options: FileListOptions, theme: Theme): string[];
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns true when OSC 8 hyperlinks should be emitted.
|
|
3
|
+
*
|
|
4
|
+
* Respects `tui.hyperlinks` setting:
|
|
5
|
+
* - `"off"`: never
|
|
6
|
+
* - `"auto"`: when `process.stdout.isTTY`, `NO_COLOR` is unset, and the detected terminal reports hyperlink support
|
|
7
|
+
* - `"always"`: unconditionally (useful for viewers that support OSC 8 without advertising it)
|
|
8
|
+
*/
|
|
9
|
+
export declare function isHyperlinkEnabled(): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Wrap `displayText` in an OSC 8 hyperlink pointing at the given absolute file path.
|
|
12
|
+
*
|
|
13
|
+
* Returns `displayText` unchanged when hyperlinks are disabled or when
|
|
14
|
+
* the text already contains an OSC 8 sequence (prevents double-wrapping).
|
|
15
|
+
*
|
|
16
|
+
* The caller is responsible for passing an absolute path. Relative paths
|
|
17
|
+
* produce invalid `file://` URIs and are accepted silently to avoid runtime
|
|
18
|
+
* errors in renderer hot paths.
|
|
19
|
+
*
|
|
20
|
+
* @param absPath - Absolute filesystem path
|
|
21
|
+
* @param displayText - Text to render as the hyperlink anchor (may contain ANSI codes)
|
|
22
|
+
* @param opts - Optional line/col position appended as `?line=N&col=M` query params
|
|
23
|
+
*/
|
|
24
|
+
export declare function fileHyperlink(absPath: string, displayText: string, opts?: {
|
|
25
|
+
line?: number;
|
|
26
|
+
col?: number;
|
|
27
|
+
}): string;
|
|
28
|
+
/**
|
|
29
|
+
* Synchronously resolve a filesystem-backed internal URL (e.g. `local://foo.md`,
|
|
30
|
+
* `memory://root/notes.md`) to its absolute filesystem path. Returns `undefined`
|
|
31
|
+
* for inputs that aren't fs-backed, aren't resolvable in the current session
|
|
32
|
+
* registry, or fail to parse.
|
|
33
|
+
*
|
|
34
|
+
* Used by renderers to wrap fs-backed internal URLs in OSC 8 hyperlinks even
|
|
35
|
+
* when the resolved path isn't yet available from tool result details (e.g.
|
|
36
|
+
* during the call/streaming phase before a result lands).
|
|
37
|
+
*
|
|
38
|
+
* Async-resolved schemes (`artifact://`, `agent://`, `skill://`, `rule://`,
|
|
39
|
+
* `omp://`) are not handled here — those rely on `details.resolvedPath` set
|
|
40
|
+
* by the read tool's router resolution.
|
|
41
|
+
*/
|
|
42
|
+
export declare function tryResolveInternalUrlSync(input: string): string | undefined;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { SearchProviderError, type SearchProviderId, type SearchSource } from "../../../web/search/types";
|
|
2
2
|
/**
|
|
3
3
|
* Search for an API credential by checking an env-derived key first,
|
|
4
4
|
* then falling back to agent.db stored credentials for the given providers.
|
|
@@ -47,3 +47,4 @@ export declare function toSearchSources(sources: ReadonlyArray<{
|
|
|
47
47
|
snippet?: string;
|
|
48
48
|
publishedDate?: string;
|
|
49
49
|
}>, numResults: number): SearchSource[];
|
|
50
|
+
export declare function classifyProviderHttpError(provider: SearchProviderId, status: number, body: string): SearchProviderError | null;
|
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": "15.
|
|
4
|
+
"version": "15.2.2",
|
|
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",
|
|
@@ -47,12 +47,12 @@
|
|
|
47
47
|
"@agentclientprotocol/sdk": "0.21.0",
|
|
48
48
|
"@babel/parser": "^7.29.3",
|
|
49
49
|
"@mozilla/readability": "^0.6.0",
|
|
50
|
-
"@oh-my-pi/omp-stats": "15.
|
|
51
|
-
"@oh-my-pi/pi-agent-core": "15.
|
|
52
|
-
"@oh-my-pi/pi-ai": "15.
|
|
53
|
-
"@oh-my-pi/pi-natives": "15.
|
|
54
|
-
"@oh-my-pi/pi-tui": "15.
|
|
55
|
-
"@oh-my-pi/pi-utils": "15.
|
|
50
|
+
"@oh-my-pi/omp-stats": "15.2.2",
|
|
51
|
+
"@oh-my-pi/pi-agent-core": "15.2.2",
|
|
52
|
+
"@oh-my-pi/pi-ai": "15.2.2",
|
|
53
|
+
"@oh-my-pi/pi-natives": "15.2.2",
|
|
54
|
+
"@oh-my-pi/pi-tui": "15.2.2",
|
|
55
|
+
"@oh-my-pi/pi-utils": "15.2.2",
|
|
56
56
|
"@puppeteer/browsers": "^2.13.0",
|
|
57
57
|
"@types/turndown": "5.0.6",
|
|
58
58
|
"@xterm/headless": "^6.0.0",
|
|
@@ -583,6 +583,18 @@ export const SETTINGS_SCHEMA = {
|
|
|
583
583
|
description:
|
|
584
584
|
"Maximum height in terminal rows for inline images (default 20). Set to 0 to use only the viewport-based limit (60% of terminal height).",
|
|
585
585
|
},
|
|
586
|
+
|
|
587
|
+
"tui.hyperlinks": {
|
|
588
|
+
type: "enum",
|
|
589
|
+
values: ["off", "auto", "always"] as const,
|
|
590
|
+
default: "auto",
|
|
591
|
+
ui: {
|
|
592
|
+
tab: "appearance",
|
|
593
|
+
label: "Terminal Hyperlinks",
|
|
594
|
+
description:
|
|
595
|
+
"Wrap file paths in OSC 8 hyperlinks for terminal-native click-to-open (auto: detect support; off: never; always: unconditional)",
|
|
596
|
+
},
|
|
597
|
+
},
|
|
586
598
|
// Display rendering
|
|
587
599
|
"display.tabWidth": {
|
|
588
600
|
type: "number",
|
package/src/config/settings.ts
CHANGED
|
@@ -129,6 +129,18 @@ function stringArrayFromUnknown(value: unknown): string[] {
|
|
|
129
129
|
return [];
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
function shallowStringRecord(value: unknown): Record<string, string> {
|
|
133
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
134
|
+
|
|
135
|
+
const result: Record<string, string> = {};
|
|
136
|
+
for (const [key, item] of Object.entries(value)) {
|
|
137
|
+
if (typeof item === "string") {
|
|
138
|
+
result[key] = item;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
|
|
132
144
|
function resolvePathScopedStringArray(settingPath: SettingPath, value: unknown, cwd: string): string[] | undefined {
|
|
133
145
|
if (!PATH_SCOPED_ARRAY_SETTINGS.has(settingPath) || !Array.isArray(value)) return undefined;
|
|
134
146
|
|
|
@@ -424,8 +436,19 @@ export class Settings {
|
|
|
424
436
|
* Set a model role (helper for modelRoles record).
|
|
425
437
|
*/
|
|
426
438
|
setModelRole(role: ModelRole | string, modelId: string): void {
|
|
427
|
-
const current = this
|
|
439
|
+
const current = shallowStringRecord(getByPath(this.#global, ["modelRoles"]));
|
|
440
|
+
const runtimeOverrides = getByPath(this.#overrides, ["modelRoles"]);
|
|
441
|
+
const updateRuntimeOverride =
|
|
442
|
+
!!runtimeOverrides &&
|
|
443
|
+
typeof runtimeOverrides === "object" &&
|
|
444
|
+
!Array.isArray(runtimeOverrides) &&
|
|
445
|
+
Object.hasOwn(runtimeOverrides, role);
|
|
446
|
+
|
|
428
447
|
this.set("modelRoles", { ...current, [role]: modelId });
|
|
448
|
+
|
|
449
|
+
if (updateRuntimeOverride) {
|
|
450
|
+
this.override("modelRoles", { ...shallowStringRecord(runtimeOverrides), [role]: modelId });
|
|
451
|
+
}
|
|
429
452
|
}
|
|
430
453
|
|
|
431
454
|
/**
|
|
@@ -440,20 +463,20 @@ export class Settings {
|
|
|
440
463
|
* Get all model roles (helper for modelRoles record).
|
|
441
464
|
*/
|
|
442
465
|
getModelRoles(): ReadOnlyDict<string> {
|
|
443
|
-
return this.get("modelRoles");
|
|
466
|
+
return { ...this.get("modelRoles") };
|
|
444
467
|
}
|
|
445
468
|
|
|
446
469
|
/*
|
|
447
470
|
* Override model roles (helper for modelRoles record).
|
|
448
471
|
*/
|
|
449
472
|
overrideModelRoles(roles: ReadOnlyDict<string>): void {
|
|
450
|
-
const
|
|
473
|
+
const next = shallowStringRecord(getByPath(this.#overrides, ["modelRoles"]));
|
|
451
474
|
for (const [role, modelId] of Object.entries(roles)) {
|
|
452
475
|
if (modelId) {
|
|
453
|
-
|
|
476
|
+
next[role] = modelId;
|
|
454
477
|
}
|
|
455
478
|
}
|
|
456
|
-
this.override("modelRoles",
|
|
479
|
+
this.override("modelRoles", next);
|
|
457
480
|
}
|
|
458
481
|
|
|
459
482
|
/**
|
package/src/discovery/builtin.ts
CHANGED
|
@@ -346,9 +346,39 @@ async function loadRules(ctx: LoadContext): Promise<LoadResult<Rule>> {
|
|
|
346
346
|
if (result.warnings) warnings.push(...result.warnings);
|
|
347
347
|
}
|
|
348
348
|
|
|
349
|
+
// Top-level RULES.md is a sticky always-apply rule. Documented in
|
|
350
|
+
// https://omp.sh/docs/context-files as the file that gets "re-injected near
|
|
351
|
+
// the current turn so they keep hold across long conversations".
|
|
352
|
+
// User scope: ~/.omp/agent/RULES.md
|
|
353
|
+
// Project scope: nearest .omp/RULES.md walking up from cwd to repoRoot
|
|
354
|
+
const userRulesFile = path.join(ctx.home, PATHS.userAgent, "RULES.md");
|
|
355
|
+
const userRule = await loadStickyRulesFile(userRulesFile, "user");
|
|
356
|
+
if (userRule) items.push(userRule);
|
|
357
|
+
|
|
358
|
+
const nearestProjectConfigDir = await findNearestProjectConfigDir(ctx.cwd, ctx.repoRoot);
|
|
359
|
+
if (nearestProjectConfigDir) {
|
|
360
|
+
const projectRulesFile = path.join(nearestProjectConfigDir.dir, "RULES.md");
|
|
361
|
+
const projectRule = await loadStickyRulesFile(projectRulesFile, "project");
|
|
362
|
+
if (projectRule) items.push(projectRule);
|
|
363
|
+
}
|
|
364
|
+
|
|
349
365
|
return { items, warnings };
|
|
350
366
|
}
|
|
351
367
|
|
|
368
|
+
/**
|
|
369
|
+
* Read a top-level `RULES.md` and synthesize an always-apply rule.
|
|
370
|
+
* Returns null when the file is absent or empty so callers can short-circuit.
|
|
371
|
+
*/
|
|
372
|
+
async function loadStickyRulesFile(filePath: string, level: "user" | "project"): Promise<Rule | null> {
|
|
373
|
+
const content = await readFile(filePath);
|
|
374
|
+
if (!content) return null;
|
|
375
|
+
const source = createSourceMeta(PROVIDER_ID, filePath, level);
|
|
376
|
+
const rule = buildRuleFromMarkdown("RULES.md", content, filePath, source, { ruleName: "RULES" });
|
|
377
|
+
// Force alwaysApply regardless of frontmatter — the whole point of RULES.md
|
|
378
|
+
// is to be reattached every turn.
|
|
379
|
+
return { ...rule, alwaysApply: true };
|
|
380
|
+
}
|
|
381
|
+
|
|
352
382
|
registerProvider<Rule>(ruleCapability.id, {
|
|
353
383
|
id: PROVIDER_ID,
|
|
354
384
|
displayName: DISPLAY_NAME,
|
package/src/edit/renderer.ts
CHANGED
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
truncateDiffByHunk,
|
|
26
26
|
} from "../tools/render-utils";
|
|
27
27
|
import { type VimRenderArgs, vimToolRenderer } from "../tools/vim";
|
|
28
|
-
import { Hasher, type RenderCache, renderStatusLine, truncateToWidth } from "../tui";
|
|
28
|
+
import { fileHyperlink, Hasher, type RenderCache, renderStatusLine, truncateToWidth } from "../tui";
|
|
29
29
|
import type { EditMode } from "../utils/edit-mode";
|
|
30
30
|
import type { VimToolDetails } from "../vim/types";
|
|
31
31
|
import type { DiffError, DiffResult } from "./diff";
|
|
@@ -219,14 +219,16 @@ function formatEditPathDisplay(
|
|
|
219
219
|
uiTheme: Theme,
|
|
220
220
|
options?: { rename?: string; firstChangedLine?: number },
|
|
221
221
|
): string {
|
|
222
|
-
let pathDisplay = rawPath
|
|
222
|
+
let pathDisplay = rawPath
|
|
223
|
+
? fileHyperlink(rawPath, uiTheme.fg("accent", shortenPath(rawPath)))
|
|
224
|
+
: uiTheme.fg("toolOutput", "…");
|
|
223
225
|
|
|
224
226
|
if (options?.firstChangedLine) {
|
|
225
227
|
pathDisplay += uiTheme.fg("warning", `:${options.firstChangedLine}`);
|
|
226
228
|
}
|
|
227
229
|
|
|
228
230
|
if (options?.rename) {
|
|
229
|
-
pathDisplay += ` ${uiTheme.fg("dim", "→")} ${uiTheme.fg("accent", shortenPath(options.rename))}`;
|
|
231
|
+
pathDisplay += ` ${uiTheme.fg("dim", "→")} ${fileHyperlink(options.rename, uiTheme.fg("accent", shortenPath(options.rename)))}`;
|
|
230
232
|
}
|
|
231
233
|
|
|
232
234
|
return pathDisplay;
|
package/src/eval/py/executor.ts
CHANGED
|
@@ -209,6 +209,15 @@ function formatTimeoutAnnotation(timeoutMs?: number): string | undefined {
|
|
|
209
209
|
return `Command timed out after ${secs} seconds`;
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
+
function formatKernelTimeoutAnnotation(timeoutMs: number | undefined, kernelKilled: boolean): string {
|
|
213
|
+
const secs = timeoutMs === undefined ? undefined : Math.max(1, Math.round(timeoutMs / 1000));
|
|
214
|
+
if (kernelKilled) {
|
|
215
|
+
return "eval cell timed out and the kernel was unresponsive to interrupt; the kernel has been killed and will be recreated on the next call.";
|
|
216
|
+
}
|
|
217
|
+
const duration = secs === undefined ? "the configured timeout" : `${secs}s`;
|
|
218
|
+
return `eval cell timed out after ${duration}; kernel interrupted but remains running. Reset the kernel via { reset: true } if state appears corrupted.`;
|
|
219
|
+
}
|
|
220
|
+
|
|
212
221
|
function createCancelledPythonResult(timedOut: boolean, timeoutMs?: number): PythonResult {
|
|
213
222
|
const output = timedOut ? (formatTimeoutAnnotation(timeoutMs) ?? "Command timed out") : "";
|
|
214
223
|
const outputBytes = Buffer.byteLength(output, "utf-8");
|
|
@@ -434,7 +443,9 @@ async function executeWithKernel(
|
|
|
434
443
|
});
|
|
435
444
|
|
|
436
445
|
if (result.cancelled) {
|
|
437
|
-
const annotation = result.timedOut
|
|
446
|
+
const annotation = result.timedOut
|
|
447
|
+
? formatKernelTimeoutAnnotation(executionTimeoutMs, result.kernelKilled ?? false)
|
|
448
|
+
: undefined;
|
|
438
449
|
return {
|
|
439
450
|
exitCode: undefined,
|
|
440
451
|
cancelled: true,
|
package/src/eval/py/kernel.ts
CHANGED
|
@@ -46,8 +46,10 @@ const STARTUP_TIMEOUT_MS = 10_000;
|
|
|
46
46
|
// How long to wait after SIGINT for the runner to emit `done`. If the cell is
|
|
47
47
|
// stuck in code that ignores Python signals (e.g. a C extension holding the
|
|
48
48
|
// GIL), we escalate to a full subprocess shutdown so the host queue unblocks
|
|
49
|
-
// instead of hanging the session forever.
|
|
50
|
-
|
|
49
|
+
// instead of hanging the session forever. The grace window is intentionally
|
|
50
|
+
// generous: a clean interrupt is far preferable to losing the persistent
|
|
51
|
+
// kernel's state, so we only kill as a last-resort recovery path.
|
|
52
|
+
const INTERRUPT_ESCALATION_MS = 5_000;
|
|
51
53
|
|
|
52
54
|
export interface KernelExecuteOptions {
|
|
53
55
|
signal?: AbortSignal;
|
|
@@ -66,6 +68,12 @@ export interface KernelExecuteResult {
|
|
|
66
68
|
cancelled: boolean;
|
|
67
69
|
timedOut: boolean;
|
|
68
70
|
stdinRequested: boolean;
|
|
71
|
+
/**
|
|
72
|
+
* True when the kernel subprocess was killed as part of settling this
|
|
73
|
+
* execution (e.g. SIGINT was ignored and we escalated to shutdown, or the
|
|
74
|
+
* kernel died unexpectedly). When false, the kernel remains reusable.
|
|
75
|
+
*/
|
|
76
|
+
kernelKilled?: boolean;
|
|
69
77
|
}
|
|
70
78
|
|
|
71
79
|
export interface KernelShutdownResult {
|
|
@@ -162,6 +170,7 @@ interface PendingExecution {
|
|
|
162
170
|
cancelled: boolean;
|
|
163
171
|
timedOut: boolean;
|
|
164
172
|
stdinRequested: boolean;
|
|
173
|
+
kernelKilled: boolean;
|
|
165
174
|
settled: boolean;
|
|
166
175
|
escalationTimer?: NodeJS.Timeout;
|
|
167
176
|
}
|
|
@@ -222,7 +231,7 @@ export class PythonKernel {
|
|
|
222
231
|
kernel.#exitedPromise = proc.exited;
|
|
223
232
|
void kernel.#exitedPromise.then(code => {
|
|
224
233
|
kernel.#alive = false;
|
|
225
|
-
kernel.#abortPendingExecutions(`Python kernel exited with code ${code}
|
|
234
|
+
kernel.#abortPendingExecutions(`Python kernel exited with code ${code}`, { kernelKilled: true });
|
|
226
235
|
});
|
|
227
236
|
|
|
228
237
|
kernel.#startReader(proc.stdout as ReadableStream<Uint8Array>);
|
|
@@ -261,6 +270,7 @@ export class PythonKernel {
|
|
|
261
270
|
timedOut: false,
|
|
262
271
|
stdinRequested: false,
|
|
263
272
|
settled: false,
|
|
273
|
+
kernelKilled: false,
|
|
264
274
|
};
|
|
265
275
|
this.#pending.set(msgId, pending);
|
|
266
276
|
|
|
@@ -276,6 +286,7 @@ export class PythonKernel {
|
|
|
276
286
|
cancelled: pending.cancelled,
|
|
277
287
|
timedOut: pending.timedOut,
|
|
278
288
|
stdinRequested: pending.stdinRequested,
|
|
289
|
+
kernelKilled: pending.kernelKilled,
|
|
279
290
|
});
|
|
280
291
|
};
|
|
281
292
|
|
|
@@ -287,9 +298,12 @@ export class PythonKernel {
|
|
|
287
298
|
logger.warn("Python runner did not respond to SIGINT; terminating subprocess", {
|
|
288
299
|
kernelId: this.id,
|
|
289
300
|
});
|
|
290
|
-
//
|
|
291
|
-
//
|
|
292
|
-
//
|
|
301
|
+
// SIGINT was ignored; mark the cell as kernel-killed so callers can
|
|
302
|
+
// surface the harsher recovery message. `shutdown()` aborts pending
|
|
303
|
+
// executions immediately and escalates to SIGTERM/SIGKILL, so the
|
|
304
|
+
// host queue unblocks even if the runner is stuck in a
|
|
305
|
+
// non-interruptible state.
|
|
306
|
+
pending.kernelKilled = true;
|
|
293
307
|
void this.shutdown();
|
|
294
308
|
}, INTERRUPT_ESCALATION_MS);
|
|
295
309
|
escalation.unref?.();
|
|
@@ -363,7 +377,7 @@ export class PythonKernel {
|
|
|
363
377
|
if (this.#shutdownConfirmed) return { confirmed: true };
|
|
364
378
|
|
|
365
379
|
this.#alive = false;
|
|
366
|
-
this.#abortPendingExecutions("Python kernel shutdown");
|
|
380
|
+
this.#abortPendingExecutions("Python kernel shutdown", { kernelKilled: true });
|
|
367
381
|
|
|
368
382
|
const timeoutMs = options?.timeoutMs ?? SHUTDOWN_GRACE_MS;
|
|
369
383
|
const proc = this.#proc;
|
|
@@ -410,10 +424,11 @@ export class PythonKernel {
|
|
|
410
424
|
return { confirmed };
|
|
411
425
|
}
|
|
412
426
|
|
|
413
|
-
#abortPendingExecutions(reason: string): void {
|
|
427
|
+
#abortPendingExecutions(reason: string, options?: { kernelKilled?: boolean }): void {
|
|
414
428
|
if (this.#pending.size === 0) return;
|
|
415
429
|
const pending = Array.from(this.#pending.values());
|
|
416
430
|
this.#pending.clear();
|
|
431
|
+
const kernelKilledDefault = options?.kernelKilled ?? false;
|
|
417
432
|
for (const entry of pending) {
|
|
418
433
|
if (entry.settled) continue;
|
|
419
434
|
entry.settled = true;
|
|
@@ -425,6 +440,7 @@ export class PythonKernel {
|
|
|
425
440
|
stdinRequested: entry.stdinRequested,
|
|
426
441
|
executionCount: entry.executionCount,
|
|
427
442
|
error: entry.error,
|
|
443
|
+
kernelKilled: entry.kernelKilled || kernelKilledDefault,
|
|
428
444
|
});
|
|
429
445
|
}
|
|
430
446
|
}
|