@oh-my-pi/pi-coding-agent 15.9.0 → 15.9.3
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 +67 -0
- package/dist/types/cli/dry-balance-cli.d.ts +104 -0
- package/dist/types/cli/update-cli.d.ts +15 -1
- package/dist/types/commands/dry-balance.d.ts +31 -0
- package/dist/types/config/append-only-context-mode.d.ts +8 -0
- package/dist/types/config/model-registry.d.ts +5 -0
- package/dist/types/config/models-config-schema.d.ts +18 -0
- package/dist/types/config/settings-schema.d.ts +3 -3
- package/dist/types/config/settings.d.ts +11 -0
- package/dist/types/discovery/helpers.d.ts +1 -0
- package/dist/types/exa/mcp-client.d.ts +2 -1
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +2 -3
- package/dist/types/hindsight/bank.d.ts +17 -9
- package/dist/types/hindsight/mental-models.d.ts +1 -1
- package/dist/types/hindsight/state.d.ts +9 -3
- package/dist/types/mcp/json-rpc.d.ts +6 -1
- package/dist/types/mcp/manager.d.ts +1 -1
- package/dist/types/mcp/tool-bridge.d.ts +4 -0
- package/dist/types/mnemopi/state.d.ts +2 -2
- package/dist/types/modes/components/agent-dashboard.d.ts +1 -0
- package/dist/types/modes/components/extensions/extension-dashboard.d.ts +1 -0
- package/dist/types/modes/components/plugin-settings.d.ts +40 -8
- package/dist/types/modes/components/session-selector.d.ts +8 -3
- package/dist/types/modes/components/settings-selector.d.ts +1 -1
- package/dist/types/modes/components/transcript-container.d.ts +3 -2
- package/dist/types/modes/utils/keybinding-matchers.d.ts +4 -0
- package/dist/types/session/agent-session.d.ts +13 -1
- package/dist/types/session/auth-storage.d.ts +2 -2
- package/dist/types/session/history-storage.d.ts +3 -4
- package/dist/types/session/messages.d.ts +1 -0
- package/dist/types/session/session-manager.d.ts +1 -0
- package/dist/types/slash-commands/types.d.ts +17 -4
- package/dist/types/task/types.d.ts +2 -0
- package/dist/types/tiny/text.d.ts +17 -0
- package/dist/types/tools/index.d.ts +16 -0
- package/dist/types/tools/path-utils.d.ts +11 -0
- package/dist/types/web/search/providers/base.d.ts +14 -0
- package/dist/types/web/search/providers/exa.d.ts +9 -0
- package/dist/types/web/search/providers/perplexity.d.ts +2 -2
- package/dist/types/web/search/types.d.ts +2 -1
- package/package.json +9 -9
- package/src/cli/dry-balance-cli.ts +823 -0
- package/src/cli/session-picker.ts +1 -0
- package/src/cli/update-cli.ts +54 -2
- package/src/cli-commands.ts +1 -0
- package/src/commands/completions.ts +1 -1
- package/src/commands/dry-balance.ts +43 -0
- package/src/config/append-only-context-mode.ts +37 -0
- package/src/config/model-registry.ts +6 -0
- package/src/config/models-config-schema.ts +3 -0
- package/src/config/settings-schema.ts +2 -2
- package/src/config/settings.ts +38 -0
- package/src/discovery/builtin-rules/ts-no-tiny-functions.md +1 -0
- package/src/discovery/github.ts +37 -1
- package/src/discovery/helpers.ts +3 -1
- package/src/exa/mcp-client.ts +11 -5
- package/src/extensibility/plugins/legacy-pi-compat.ts +245 -25
- package/src/hindsight/backend.ts +184 -35
- package/src/hindsight/bank.ts +32 -22
- package/src/hindsight/mental-models.ts +1 -1
- package/src/hindsight/state.ts +21 -7
- package/src/internal-urls/docs-index.generated.ts +5 -5
- package/src/internal-urls/omp-protocol.ts +8 -2
- package/src/main.ts +4 -2
- package/src/mcp/json-rpc.ts +8 -0
- package/src/mcp/manager.ts +40 -21
- package/src/mcp/render.ts +3 -0
- package/src/mcp/tool-bridge.ts +10 -2
- package/src/mcp/transports/http.ts +33 -16
- package/src/mnemopi/state.ts +4 -4
- package/src/modes/acp/acp-agent.ts +168 -3
- package/src/modes/components/agent-dashboard.ts +103 -31
- package/src/modes/components/extensions/extension-dashboard.ts +56 -10
- package/src/modes/components/history-search.ts +128 -14
- package/src/modes/components/plugin-settings.ts +270 -36
- package/src/modes/components/session-selector.ts +45 -14
- package/src/modes/components/settings-selector.ts +1 -1
- package/src/modes/components/tips.txt +5 -1
- package/src/modes/components/transcript-container.ts +35 -6
- package/src/modes/components/tree-selector.ts +29 -2
- package/src/modes/controllers/command-controller.ts +4 -3
- package/src/modes/controllers/input-controller.ts +18 -7
- package/src/modes/controllers/selector-controller.ts +30 -19
- package/src/modes/interactive-mode.ts +38 -3
- package/src/modes/setup-wizard/scenes/sign-in.ts +27 -7
- package/src/modes/utils/keybinding-matchers.ts +10 -0
- package/src/prompts/agents/explore.md +1 -0
- package/src/prompts/agents/librarian.md +1 -0
- package/src/prompts/dry-balance-bench.md +8 -0
- package/src/prompts/steering/user-interjection.md +10 -0
- package/src/prompts/system/agent-creation-architect.md +1 -26
- package/src/prompts/system/system-prompt.md +143 -145
- package/src/prompts/system/title-system.md +3 -2
- package/src/prompts/tools/browser.md +29 -29
- package/src/prompts/tools/render-mermaid.md +2 -2
- package/src/sdk.ts +87 -30
- package/src/session/agent-session.ts +96 -14
- package/src/session/auth-storage.ts +4 -0
- package/src/session/history-storage.ts +11 -18
- package/src/session/messages.ts +80 -0
- package/src/session/session-manager.ts +7 -1
- package/src/slash-commands/types.ts +27 -10
- package/src/task/executor.ts +6 -2
- package/src/task/index.ts +8 -7
- package/src/task/types.ts +2 -0
- package/src/tiny/text.ts +112 -1
- package/src/tools/bash.ts +3 -4
- package/src/tools/index.ts +16 -0
- package/src/tools/job.ts +3 -3
- package/src/tools/memory-recall.ts +1 -1
- package/src/tools/memory-reflect.ts +3 -3
- package/src/tools/path-utils.ts +21 -0
- package/src/tools/search.ts +18 -1
- package/src/tools/ssh.ts +26 -10
- package/src/tools/write.ts +14 -2
- package/src/tui/status-line.ts +15 -4
- package/src/utils/file-mentions.ts +7 -107
- package/src/utils/title-generator.ts +66 -38
- package/src/web/search/index.ts +3 -1
- package/src/web/search/provider.ts +1 -1
- package/src/web/search/providers/base.ts +17 -0
- package/src/web/search/providers/exa.ts +111 -7
- package/src/web/search/providers/perplexity.ts +8 -4
- package/src/web/search/types.ts +2 -1
|
@@ -31,7 +31,11 @@ export interface ParsedSlashCommand {
|
|
|
31
31
|
/**
|
|
32
32
|
* Result returned by a slash-command handler.
|
|
33
33
|
*
|
|
34
|
-
* - `
|
|
34
|
+
* - `undefined` (and the implicit `void` return) — command was handled and
|
|
35
|
+
* consumed; no further input. Handlers may simply omit a `return` rather than
|
|
36
|
+
* building `{ consumed: true }`; `void` is accepted in the handler signatures
|
|
37
|
+
* below so the contract typechecks under TypeScript 5.x (which does not
|
|
38
|
+
* coerce `() => void` to `() => T | undefined`) as well as 6.x / tsgo.
|
|
35
39
|
* - `{ consumed: true }` — explicit equivalent of the above (ACP shape).
|
|
36
40
|
* - `{ prompt: string }` — command handled, pass `prompt` through as the new
|
|
37
41
|
* user input (e.g. `/force <tool> <prompt>` keeps `<prompt>` as the message).
|
|
@@ -100,20 +104,33 @@ export interface SlashCommandSpec extends BuiltinSlashCommand {
|
|
|
100
104
|
/**
|
|
101
105
|
* Text/ACP-mode handler. The same body is invoked from the ACP dispatcher
|
|
102
106
|
* and, via the TUI adapter, when no `handleTui` override is provided.
|
|
107
|
+
*
|
|
108
|
+
* Expressed as a union of two function types — one returning a
|
|
109
|
+
* `SlashCommandResult`, one returning `void` — so handlers that simply
|
|
110
|
+
* `return` (or omit the return) typecheck under TypeScript 5.x as well as
|
|
111
|
+
* 6.x / tsgo. TS 5.x does not coerce a `() => void` value into a
|
|
112
|
+
* `() => T | undefined` slot, so the two return shapes must be siblings
|
|
113
|
+
* rather than a `T | void` union inside one function type (which Biome's
|
|
114
|
+
* `noConfusingVoidType` also rejects).
|
|
103
115
|
*/
|
|
104
|
-
handle?:
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
116
|
+
handle?:
|
|
117
|
+
| ((
|
|
118
|
+
command: ParsedSlashCommand,
|
|
119
|
+
runtime: SlashCommandRuntime,
|
|
120
|
+
) => SlashCommandResult | Promise<SlashCommandResult>)
|
|
121
|
+
| ((command: ParsedSlashCommand, runtime: SlashCommandRuntime) => void | Promise<void>);
|
|
108
122
|
/**
|
|
109
123
|
* TUI-only handler that supersedes `handle` when both are present. Use for
|
|
110
124
|
* selectors, wizards, dashboards, and anything else that requires
|
|
111
|
-
* `InteractiveModeContext`.
|
|
125
|
+
* `InteractiveModeContext`. See `handle` for the rationale behind the
|
|
126
|
+
* function-type union shape.
|
|
112
127
|
*/
|
|
113
|
-
handleTui?:
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
128
|
+
handleTui?:
|
|
129
|
+
| ((
|
|
130
|
+
command: ParsedSlashCommand,
|
|
131
|
+
runtime: TuiSlashCommandRuntime,
|
|
132
|
+
) => SlashCommandResult | Promise<SlashCommandResult>)
|
|
133
|
+
| ((command: ParsedSlashCommand, runtime: TuiSlashCommandRuntime) => void | Promise<void>);
|
|
117
134
|
}
|
|
118
135
|
|
|
119
136
|
/** Result returned by `executeAcpBuiltinSlashCommand`. */
|
package/src/task/executor.ts
CHANGED
|
@@ -531,7 +531,7 @@ function createMCPProxyTools(mcpManager: MCPManager): CustomTool[] {
|
|
|
531
531
|
});
|
|
532
532
|
}
|
|
533
533
|
|
|
534
|
-
function createSubagentSettings(baseSettings: Settings): Settings {
|
|
534
|
+
function createSubagentSettings(baseSettings: Settings, overrides?: Partial<Record<SettingPath, unknown>>): Settings {
|
|
535
535
|
const snapshot: Partial<Record<SettingPath, unknown>> = {};
|
|
536
536
|
for (const key of Object.keys(SETTINGS_SCHEMA) as SettingPath[]) {
|
|
537
537
|
snapshot[key] = baseSettings.get(key);
|
|
@@ -545,6 +545,7 @@ function createSubagentSettings(baseSettings: Settings): Settings {
|
|
|
545
545
|
// the parent task approval is the authorization boundary. Use yolo mode
|
|
546
546
|
// to preserve unattended subagent execution. User `tools.approval` policies still apply.
|
|
547
547
|
"tools.approvalMode": "yolo",
|
|
548
|
+
...overrides,
|
|
548
549
|
});
|
|
549
550
|
}
|
|
550
551
|
|
|
@@ -619,7 +620,10 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
619
620
|
}
|
|
620
621
|
|
|
621
622
|
const settings = options.settings ?? Settings.isolated();
|
|
622
|
-
const subagentSettings = createSubagentSettings(
|
|
623
|
+
const subagentSettings = createSubagentSettings(
|
|
624
|
+
settings,
|
|
625
|
+
agent.readSummarize === false ? { "read.summarize.enabled": false } : undefined,
|
|
626
|
+
);
|
|
623
627
|
const maxRecursionDepth = settings.get("task.maxRecursionDepth") ?? 2;
|
|
624
628
|
const maxRuntimeMs = Math.max(0, Math.trunc(Number(settings.get("task.maxRuntimeMs") ?? 0) || 0));
|
|
625
629
|
const parentDepth = options.taskDepth ?? 0;
|
package/src/task/index.ts
CHANGED
|
@@ -17,9 +17,8 @@ import * as os from "node:os";
|
|
|
17
17
|
import path from "node:path";
|
|
18
18
|
import type { AgentTool, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
19
19
|
import type { Usage } from "@oh-my-pi/pi-ai";
|
|
20
|
-
import { $env, prompt, Snowflake } from "@oh-my-pi/pi-utils";
|
|
20
|
+
import { $env, logger, prompt, Snowflake } from "@oh-my-pi/pi-utils";
|
|
21
21
|
import type { ToolSession } from "..";
|
|
22
|
-
import { AsyncJobManager } from "../async";
|
|
23
22
|
import { resolveAgentModelPatterns } from "../config/model-resolver";
|
|
24
23
|
import { MCPManager } from "../mcp/manager";
|
|
25
24
|
import type { Theme } from "../modes/theme/theme";
|
|
@@ -343,12 +342,14 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
343
342
|
return this.#executeSync(_toolCallId, params, signal, onUpdate);
|
|
344
343
|
}
|
|
345
344
|
|
|
346
|
-
const manager =
|
|
345
|
+
const manager = this.session.asyncJobManager;
|
|
347
346
|
if (!manager) {
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
347
|
+
// Async was requested but no manager is registered (e.g. an
|
|
348
|
+
// orphaned session whose host never wired one up). Falling back
|
|
349
|
+
// to the sync path keeps the tool usable; only background/job-poll
|
|
350
|
+
// semantics are lost.
|
|
351
|
+
logger.warn("task: async.enabled but no AsyncJobManager registered; falling back to sync execution");
|
|
352
|
+
return this.#executeSync(_toolCallId, params, signal, onUpdate);
|
|
352
353
|
}
|
|
353
354
|
|
|
354
355
|
const taskItems = params.tasks ?? [];
|
package/src/task/types.ts
CHANGED
|
@@ -174,6 +174,8 @@ export interface AgentDefinition {
|
|
|
174
174
|
output?: unknown;
|
|
175
175
|
blocking?: boolean;
|
|
176
176
|
autoloadSkills?: string[];
|
|
177
|
+
/** When `false`, the agent's `read` tool returns verbatim file content instead of structural summaries. */
|
|
178
|
+
readSummarize?: boolean;
|
|
177
179
|
source: AgentSource;
|
|
178
180
|
filePath?: string;
|
|
179
181
|
}
|
package/src/tiny/text.ts
CHANGED
|
@@ -43,6 +43,116 @@ export function formatTitleUserMessage(message: string): string {
|
|
|
43
43
|
return `<user-message>\n${prepareTitleInput(message)}\n</user-message>`;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Greeting / acknowledgement / filler tokens. A first user message composed
|
|
48
|
+
* entirely of these (or of bare numbers / punctuation / emoji) carries no
|
|
49
|
+
* concrete task, so titling is deferred to a later message instead of latching
|
|
50
|
+
* onto "hi". See {@link isLowSignalTitleInput}.
|
|
51
|
+
*/
|
|
52
|
+
const FILLER_TITLE_TOKENS = new Set<string>([
|
|
53
|
+
// greetings
|
|
54
|
+
"hi",
|
|
55
|
+
"hii",
|
|
56
|
+
"hiii",
|
|
57
|
+
"hiya",
|
|
58
|
+
"hey",
|
|
59
|
+
"heya",
|
|
60
|
+
"hello",
|
|
61
|
+
"helo",
|
|
62
|
+
"hullo",
|
|
63
|
+
"yo",
|
|
64
|
+
"ya",
|
|
65
|
+
"sup",
|
|
66
|
+
"wassup",
|
|
67
|
+
"whatsup",
|
|
68
|
+
"howdy",
|
|
69
|
+
"greetings",
|
|
70
|
+
"hola",
|
|
71
|
+
"ciao",
|
|
72
|
+
"aloha",
|
|
73
|
+
"gm",
|
|
74
|
+
"gn",
|
|
75
|
+
"good",
|
|
76
|
+
"morning",
|
|
77
|
+
"afternoon",
|
|
78
|
+
"evening",
|
|
79
|
+
"night",
|
|
80
|
+
"day",
|
|
81
|
+
// politeness / acknowledgement
|
|
82
|
+
"thanks",
|
|
83
|
+
"thank",
|
|
84
|
+
"thx",
|
|
85
|
+
"ty",
|
|
86
|
+
"tysm",
|
|
87
|
+
"cheers",
|
|
88
|
+
"please",
|
|
89
|
+
"pls",
|
|
90
|
+
"plz",
|
|
91
|
+
"ok",
|
|
92
|
+
"okay",
|
|
93
|
+
"okey",
|
|
94
|
+
"k",
|
|
95
|
+
"kk",
|
|
96
|
+
"yep",
|
|
97
|
+
"yes",
|
|
98
|
+
"yeah",
|
|
99
|
+
"yup",
|
|
100
|
+
"nope",
|
|
101
|
+
"no",
|
|
102
|
+
"nah",
|
|
103
|
+
"sure",
|
|
104
|
+
"cool",
|
|
105
|
+
"nice",
|
|
106
|
+
"great",
|
|
107
|
+
"awesome",
|
|
108
|
+
"perfect",
|
|
109
|
+
"lol",
|
|
110
|
+
"lmao",
|
|
111
|
+
"haha",
|
|
112
|
+
"hehe",
|
|
113
|
+
// poking the agent / fillers
|
|
114
|
+
"test",
|
|
115
|
+
"tests",
|
|
116
|
+
"testing",
|
|
117
|
+
"ping",
|
|
118
|
+
"pong",
|
|
119
|
+
"there",
|
|
120
|
+
"you",
|
|
121
|
+
"u",
|
|
122
|
+
"hmm",
|
|
123
|
+
"hmmm",
|
|
124
|
+
"um",
|
|
125
|
+
"uh",
|
|
126
|
+
"so",
|
|
127
|
+
"well",
|
|
128
|
+
"anyway",
|
|
129
|
+
]);
|
|
130
|
+
|
|
131
|
+
const TITLE_WORD = /[\p{L}\p{N}]+/gu;
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* True when a first user message is too low-signal to title (greeting, ack,
|
|
135
|
+
* bare number, or empty once code/punctuation/emoji are stripped).
|
|
136
|
+
*
|
|
137
|
+
* Deterministic pre-filter: the default tiny title model (~350M local) cannot
|
|
138
|
+
* reliably follow a "respond with none" instruction and tends to hallucinate a
|
|
139
|
+
* title for trivial input, so we never ask it — the caller defers titling to
|
|
140
|
+
* the next message instead.
|
|
141
|
+
*/
|
|
142
|
+
export function isLowSignalTitleInput(message: string): boolean {
|
|
143
|
+
const tokens = stripCodeBlocks(message).toLowerCase().match(TITLE_WORD);
|
|
144
|
+
if (!tokens) return true;
|
|
145
|
+
return tokens.every(token => FILLER_TITLE_TOKENS.has(token) || /^\d+$/.test(token));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Sentinel a capable title model may emit when a message carries no concrete
|
|
150
|
+
* task. Treated as "no title yet" so the caller can defer titling. Backstop for
|
|
151
|
+
* the deterministic {@link isLowSignalTitleInput} filter; kept in sync with the
|
|
152
|
+
* `none` instruction in `prompts/system/title-system.md`.
|
|
153
|
+
*/
|
|
154
|
+
export const NO_TITLE_SENTINEL = "none";
|
|
155
|
+
|
|
46
156
|
export function normalizeGeneratedTitle(value: string | null | undefined): string | null {
|
|
47
157
|
const firstLine = value?.trim().split(/\r?\n/, 1)[0]?.trim();
|
|
48
158
|
if (!firstLine) return null;
|
|
@@ -50,5 +160,6 @@ export function normalizeGeneratedTitle(value: string | null | undefined): strin
|
|
|
50
160
|
.replace(/^["']|["']$/g, "")
|
|
51
161
|
.replace(/[.!?]$/, "")
|
|
52
162
|
.trim();
|
|
53
|
-
|
|
163
|
+
if (!title || title.toLowerCase() === NO_TITLE_SENTINEL) return null;
|
|
164
|
+
return title;
|
|
54
165
|
}
|
package/src/tools/bash.ts
CHANGED
|
@@ -10,7 +10,6 @@ import type { Component } from "@oh-my-pi/pi-tui";
|
|
|
10
10
|
import { ImageProtocol, TERMINAL } from "@oh-my-pi/pi-tui";
|
|
11
11
|
import { getProjectDir, isEnoent, logger, prompt } from "@oh-my-pi/pi-utils";
|
|
12
12
|
import * as z from "zod/v4";
|
|
13
|
-
import { AsyncJobManager } from "../async";
|
|
14
13
|
import { type BashResult, executeBash } from "../exec/bash-executor";
|
|
15
14
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
16
15
|
import { InternalUrlRouter } from "../internal-urls";
|
|
@@ -489,7 +488,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
489
488
|
onUpdate?: AgentToolUpdateCallback<BashToolDetails>;
|
|
490
489
|
startBackgrounded: boolean;
|
|
491
490
|
}): ManagedBashJobHandle {
|
|
492
|
-
const manager =
|
|
491
|
+
const manager = this.session.asyncJobManager;
|
|
493
492
|
if (!manager) {
|
|
494
493
|
throw new ToolError("Background job manager unavailable for this session.");
|
|
495
494
|
}
|
|
@@ -716,7 +715,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
716
715
|
if (timeoutClampNotice) pendingNotices.push(timeoutClampNotice);
|
|
717
716
|
|
|
718
717
|
if (asyncRequested) {
|
|
719
|
-
if (!
|
|
718
|
+
if (!this.session.asyncJobManager) {
|
|
720
719
|
throw new ToolError("Async job manager unavailable for this session.");
|
|
721
720
|
}
|
|
722
721
|
const job = this.#startManagedBashJob({
|
|
@@ -737,7 +736,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
737
736
|
});
|
|
738
737
|
}
|
|
739
738
|
|
|
740
|
-
const autoBgManager =
|
|
739
|
+
const autoBgManager = this.session.asyncJobManager;
|
|
741
740
|
if (this.#autoBackgroundEnabled && !pty && autoBgManager) {
|
|
742
741
|
const autoBackgroundWaitMs = this.#resolveAutoBackgroundWaitMs(timeoutMs);
|
|
743
742
|
const startBackgrounded = autoBackgroundWaitMs === 0;
|
package/src/tools/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { InMemorySnapshotStore } from "@oh-my-pi/hashline";
|
|
|
2
2
|
import type { AgentTelemetryConfig, AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
3
3
|
import type { ToolChoice } from "@oh-my-pi/pi-ai";
|
|
4
4
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
5
|
+
import type { AsyncJobManager } from "../async/job-manager";
|
|
5
6
|
import type { PromptTemplate } from "../config/prompt-templates";
|
|
6
7
|
import type { Settings } from "../config/settings";
|
|
7
8
|
import { EditTool } from "../edit";
|
|
@@ -183,6 +184,21 @@ export interface ToolSession {
|
|
|
183
184
|
modelRegistry?: import("../config/model-registry").ModelRegistry;
|
|
184
185
|
/** Agent output manager for unique agent:// IDs across task invocations */
|
|
185
186
|
agentOutputManager?: AgentOutputManager;
|
|
187
|
+
/**
|
|
188
|
+
* Async job manager scoped to this session.
|
|
189
|
+
*
|
|
190
|
+
* - Top-level session that constructed one: its own manager.
|
|
191
|
+
* - Subagent (`parentTaskPrefix` set): the parent's manager, so background
|
|
192
|
+
* bash/task work and `onJobComplete` deliveries flow into the conversation
|
|
193
|
+
* that spawned it.
|
|
194
|
+
* - Secondary in-process top-level session that found a singleton already
|
|
195
|
+
* installed (issue #1923): `undefined`. Tools refuse async work rather
|
|
196
|
+
* than silently route completions into the owning session's `yieldQueue`.
|
|
197
|
+
*
|
|
198
|
+
* Tools MUST use this instead of `AsyncJobManager.instance()` so a secondary
|
|
199
|
+
* session never borrows the owning session's manager by accident.
|
|
200
|
+
*/
|
|
201
|
+
asyncJobManager?: AsyncJobManager;
|
|
186
202
|
/** MCP manager visible to subagents without relying on the process-global singleton. */
|
|
187
203
|
mcpManager?: MCPManager;
|
|
188
204
|
/** Local protocol root to propagate to nested subagents and eval-created agents. */
|
package/src/tools/job.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { Component } from "@oh-my-pi/pi-tui";
|
|
|
3
3
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
4
4
|
import { prompt } from "@oh-my-pi/pi-utils";
|
|
5
5
|
import * as z from "zod/v4";
|
|
6
|
-
import { type AsyncJob, AsyncJobManager, isBackgroundJobSupportEnabled } from "../async";
|
|
6
|
+
import { type AsyncJob, type AsyncJobManager, isBackgroundJobSupportEnabled } from "../async";
|
|
7
7
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
8
8
|
import type { Theme } from "../modes/theme/theme";
|
|
9
9
|
import jobDescription from "../prompts/tools/job.md" with { type: "text" };
|
|
@@ -90,7 +90,7 @@ export class JobTool implements AgentTool<typeof jobSchema, JobToolDetails> {
|
|
|
90
90
|
onUpdate?: AgentToolUpdateCallback<JobToolDetails>,
|
|
91
91
|
_context?: AgentToolContext,
|
|
92
92
|
): Promise<AgentToolResult<JobToolDetails>> {
|
|
93
|
-
const manager =
|
|
93
|
+
const manager = this.session.asyncJobManager;
|
|
94
94
|
if (!manager) {
|
|
95
95
|
return {
|
|
96
96
|
content: [{ type: "text", text: "Async execution is disabled; no background jobs are available." }],
|
|
@@ -254,7 +254,7 @@ export class JobTool implements AgentTool<typeof jobSchema, JobToolDetails> {
|
|
|
254
254
|
): JobSnapshot[] {
|
|
255
255
|
const now = Date.now();
|
|
256
256
|
return jobs.map(j => {
|
|
257
|
-
const current =
|
|
257
|
+
const current = this.session.asyncJobManager?.getJob(j.id);
|
|
258
258
|
const latest = current ?? j;
|
|
259
259
|
return {
|
|
260
260
|
id: latest.id,
|
|
@@ -38,7 +38,7 @@ export class MemoryRecallTool implements AgentTool<typeof memoryRecallSchema> {
|
|
|
38
38
|
throw new Error("Mnemopi backend is not initialised for this session.");
|
|
39
39
|
}
|
|
40
40
|
try {
|
|
41
|
-
const results = state.recallResultsScoped(params.query);
|
|
41
|
+
const results = await state.recallResultsScoped(params.query);
|
|
42
42
|
if (results.length === 0) {
|
|
43
43
|
return {
|
|
44
44
|
content: [{ type: "text", text: "No relevant memories found." }],
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AgentTool, AgentToolResult } from "@oh-my-pi/pi-agent-core";
|
|
2
2
|
import { logger, untilAborted } from "@oh-my-pi/pi-utils";
|
|
3
3
|
import * as z from "zod/v4";
|
|
4
|
-
import {
|
|
4
|
+
import { ensureBankExists } from "../hindsight/bank";
|
|
5
5
|
import reflectDescription from "../prompts/tools/reflect.md" with { type: "text" };
|
|
6
6
|
import type { ToolSession } from ".";
|
|
7
7
|
|
|
@@ -43,7 +43,7 @@ export class MemoryReflectTool implements AgentTool<typeof memoryReflectSchema>
|
|
|
43
43
|
const query = params.context?.trim()
|
|
44
44
|
? `${params.query.trim()}\n\nAdditional context:\n${params.context.trim()}`
|
|
45
45
|
: params.query;
|
|
46
|
-
const results = state.recallResultsScoped(query);
|
|
46
|
+
const results = await state.recallResultsScoped(query);
|
|
47
47
|
if (results.length === 0) {
|
|
48
48
|
return {
|
|
49
49
|
content: [{ type: "text", text: "No relevant information found to reflect on." }],
|
|
@@ -67,7 +67,7 @@ export class MemoryReflectTool implements AgentTool<typeof memoryReflectSchema>
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
try {
|
|
70
|
-
await
|
|
70
|
+
await ensureBankExists(state.client, state.bankId, state.config, state.banksSet);
|
|
71
71
|
const response = await state.client.reflect(state.bankId, params.query, {
|
|
72
72
|
context: params.context,
|
|
73
73
|
budget: state.config.recallBudget,
|
package/src/tools/path-utils.ts
CHANGED
|
@@ -217,6 +217,27 @@ export function parseLineRanges(sel: string): [LineRange, ...LineRange[]] | null
|
|
|
217
217
|
return merged as [LineRange, ...LineRange[]];
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
+
/**
|
|
221
|
+
* Extract the line-range component from a read-tool selector that may also
|
|
222
|
+
* carry a verbatim/index display mode (`raw`, `conflicts`) — alone or compounded
|
|
223
|
+
* with a range (`raw:50-100`, `50-100:raw`). Returns the parsed ranges when the
|
|
224
|
+
* selector names any, otherwise `undefined` (pure `raw`/`conflicts`/none).
|
|
225
|
+
*
|
|
226
|
+
* Used by content search, which honors line ranges as a match filter but has no
|
|
227
|
+
* use for verbatim/conflict display modes — so those selectors are accepted and
|
|
228
|
+
* treated as an unfiltered, whole-resource search rather than rejected.
|
|
229
|
+
*/
|
|
230
|
+
export function selectorLineRanges(sel: string | undefined): [LineRange, ...LineRange[]] | undefined {
|
|
231
|
+
if (!sel) return undefined;
|
|
232
|
+
for (const chunk of sel.split(":")) {
|
|
233
|
+
const lower = chunk.toLowerCase();
|
|
234
|
+
if (lower === "raw" || lower === "conflicts") continue;
|
|
235
|
+
const ranges = parseLineRanges(chunk);
|
|
236
|
+
if (ranges) return ranges;
|
|
237
|
+
}
|
|
238
|
+
return undefined;
|
|
239
|
+
}
|
|
240
|
+
|
|
220
241
|
/** Return `true` when `lineNumber` (1-indexed) falls in any of the supplied ranges. */
|
|
221
242
|
export function isLineInRanges(lineNumber: number, ranges: readonly LineRange[]): boolean {
|
|
222
243
|
for (const range of ranges) {
|
package/src/tools/search.ts
CHANGED
|
@@ -38,6 +38,8 @@ import {
|
|
|
38
38
|
type ResolvedSearchTarget,
|
|
39
39
|
resolveReadPath,
|
|
40
40
|
resolveToolSearchScope,
|
|
41
|
+
selectorLineRanges,
|
|
42
|
+
splitInternalUrlSel,
|
|
41
43
|
splitPathAndSel,
|
|
42
44
|
} from "./path-utils";
|
|
43
45
|
import {
|
|
@@ -109,6 +111,21 @@ interface SearchPathSpec {
|
|
|
109
111
|
function parsePathSpecs(rawEntries: readonly string[]): SearchPathSpec[] {
|
|
110
112
|
const specs: SearchPathSpec[] = [];
|
|
111
113
|
for (const entry of rawEntries) {
|
|
114
|
+
// Internal URLs (`artifact://`, `skill://`, …) use the URL-aware splitter,
|
|
115
|
+
// which peels selector-shaped tails only for selector-capable schemes and
|
|
116
|
+
// leaves opaque ones (`mcp://`) intact. Unlike filesystem paths, their
|
|
117
|
+
// verbatim/index display modes (`raw`, `conflicts`) carry no meaning for
|
|
118
|
+
// content search, so we accept them — searching the whole resource — and
|
|
119
|
+
// still honor any embedded line range as a match filter.
|
|
120
|
+
const internalSplit = splitInternalUrlSel(entry);
|
|
121
|
+
if (internalSplit.sel !== undefined) {
|
|
122
|
+
specs.push({
|
|
123
|
+
original: entry,
|
|
124
|
+
clean: internalSplit.path,
|
|
125
|
+
ranges: selectorLineRanges(internalSplit.sel),
|
|
126
|
+
});
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
112
129
|
const split = splitPathAndSel(entry);
|
|
113
130
|
let clean = entry;
|
|
114
131
|
let ranges: [LineRange, ...LineRange[]] | undefined;
|
|
@@ -259,7 +276,7 @@ interface IndexedContentLines {
|
|
|
259
276
|
}
|
|
260
277
|
|
|
261
278
|
const INTERNAL_URL_DISPLAY_RE = /^[a-z][a-z0-9+.-]*:\/\//i;
|
|
262
|
-
const OMP_ROOT_URL_RE = /^omp
|
|
279
|
+
const OMP_ROOT_URL_RE = /^omp:\/\/(?:\/?|docs\/?)$/i;
|
|
263
280
|
|
|
264
281
|
function normalizeSearchLine(line: string): string {
|
|
265
282
|
return line.endsWith("\r") ? line.slice(0, -1) : line;
|
package/src/tools/ssh.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
2
2
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
3
|
-
import { Text } from "@oh-my-pi/pi-tui";
|
|
4
3
|
import { prompt } from "@oh-my-pi/pi-utils";
|
|
5
4
|
import * as z from "zod/v4";
|
|
6
5
|
import type { SSHHost } from "../capability/ssh";
|
|
@@ -18,6 +17,7 @@ import { CachedOutputBlock } from "../tui/output-block";
|
|
|
18
17
|
import type { ToolSession } from ".";
|
|
19
18
|
import { truncateForPrompt } from "./approval";
|
|
20
19
|
import { formatStyledTruncationWarning, type OutputMeta, stripOutputNotice } from "./output-meta";
|
|
20
|
+
import { replaceTabs } from "./render-utils";
|
|
21
21
|
import { ToolError } from "./tool-errors";
|
|
22
22
|
import { toolResult } from "./tool-result";
|
|
23
23
|
import { clampTimeout } from "./tool-timeouts";
|
|
@@ -230,12 +230,30 @@ interface SshRenderContext {
|
|
|
230
230
|
totalVisualLines?: number;
|
|
231
231
|
}
|
|
232
232
|
|
|
233
|
+
function formatSshCommandLines(command: string, uiTheme: Theme): string[] {
|
|
234
|
+
const sanitized = replaceTabs(command);
|
|
235
|
+
const rawLines = sanitized.length > 0 ? sanitized.split("\n") : ["…"];
|
|
236
|
+
const prefix = uiTheme.fg("dim", "$ ");
|
|
237
|
+
return rawLines.map((line, i) => (i === 0 ? `${prefix}${line}` : line));
|
|
238
|
+
}
|
|
239
|
+
|
|
233
240
|
export const sshToolRenderer = {
|
|
234
241
|
renderCall(args: SshRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
|
|
235
242
|
const host = args.host || "…";
|
|
236
|
-
const command = args.command
|
|
237
|
-
const
|
|
238
|
-
|
|
243
|
+
const command = args.command ?? "";
|
|
244
|
+
const header = renderStatusLine({ icon: "pending", title: "SSH", description: `[${host}]` }, uiTheme);
|
|
245
|
+
const cmdLines = formatSshCommandLines(command, uiTheme);
|
|
246
|
+
const outputBlock = new CachedOutputBlock();
|
|
247
|
+
return {
|
|
248
|
+
render: (width: number): string[] =>
|
|
249
|
+
outputBlock.render(
|
|
250
|
+
{ header, state: "pending", sections: [{ lines: cmdLines }], width, animate: true },
|
|
251
|
+
uiTheme,
|
|
252
|
+
),
|
|
253
|
+
invalidate: () => {
|
|
254
|
+
outputBlock.invalidate();
|
|
255
|
+
},
|
|
256
|
+
};
|
|
239
257
|
},
|
|
240
258
|
|
|
241
259
|
renderResult(
|
|
@@ -249,11 +267,9 @@ export const sshToolRenderer = {
|
|
|
249
267
|
): Component {
|
|
250
268
|
const details = result.details;
|
|
251
269
|
const host = args?.host || "…";
|
|
252
|
-
const command = args?.command
|
|
253
|
-
const header = renderStatusLine(
|
|
254
|
-
|
|
255
|
-
uiTheme,
|
|
256
|
-
);
|
|
270
|
+
const command = args?.command ?? "";
|
|
271
|
+
const header = renderStatusLine({ icon: "success", title: "SSH", description: `[${host}]` }, uiTheme);
|
|
272
|
+
const cmdLines = formatSshCommandLines(command, uiTheme);
|
|
257
273
|
const textContent = result.content?.find(c => c.type === "text")?.text ?? "";
|
|
258
274
|
const outputBlock = new CachedOutputBlock();
|
|
259
275
|
|
|
@@ -303,7 +319,7 @@ export const sshToolRenderer = {
|
|
|
303
319
|
{
|
|
304
320
|
header,
|
|
305
321
|
state: "success",
|
|
306
|
-
sections: [{ label: uiTheme.fg("toolTitle", "Output"), lines: outputLines }],
|
|
322
|
+
sections: [{ lines: cmdLines }, { label: uiTheme.fg("toolTitle", "Output"), lines: outputLines }],
|
|
307
323
|
width,
|
|
308
324
|
},
|
|
309
325
|
uiTheme,
|
package/src/tools/write.ts
CHANGED
|
@@ -135,6 +135,15 @@ function maybeWriteSnapshotHeader(session: ToolSession, absolutePath: string, co
|
|
|
135
135
|
return formatHashlineHeader(formatPathRelativeToCwd(absolutePath, session.cwd), tag);
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
+
function shouldRouteWriteThroughBridge(session: ToolSession, requestedPath: string, absolutePath: string): boolean {
|
|
139
|
+
if (isInternalUrlPath(requestedPath)) return false;
|
|
140
|
+
|
|
141
|
+
const state = session.getPlanModeState?.();
|
|
142
|
+
if (!state?.enabled || !isInternalUrlPath(state.planFilePath)) return true;
|
|
143
|
+
|
|
144
|
+
return absolutePath !== resolvePlanPath(session, state.planFilePath);
|
|
145
|
+
}
|
|
146
|
+
|
|
138
147
|
/**
|
|
139
148
|
* Append a trailing note line to the first text block of a tool result.
|
|
140
149
|
* Mutates `result` in place (the result object is owned by this call).
|
|
@@ -845,8 +854,11 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
845
854
|
await assertEditableFile(absolutePath, path);
|
|
846
855
|
}
|
|
847
856
|
|
|
848
|
-
// Try ACP bridge first
|
|
849
|
-
|
|
857
|
+
// Try ACP bridge first for editor-visible filesystem paths. Internal
|
|
858
|
+
// artifacts such as local:// plans are owned by OMP, not the editor.
|
|
859
|
+
const bridgePromise = shouldRouteWriteThroughBridge(this.session, path, absolutePath)
|
|
860
|
+
? this.#routeWriteThroughBridge(absolutePath, cleanContent)
|
|
861
|
+
: undefined;
|
|
850
862
|
if (bridgePromise !== undefined) {
|
|
851
863
|
try {
|
|
852
864
|
await bridgePromise;
|
package/src/tui/status-line.ts
CHANGED
|
@@ -15,22 +15,33 @@ export interface StatusLineOptions {
|
|
|
15
15
|
meta?: string[];
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Flatten CR/LF runs in caller-supplied header fragments so a single newline
|
|
20
|
+
* embedded in `description` or `meta` cannot expand the status line into
|
|
21
|
+
* multiple rows — which would otherwise break the bordered output block the
|
|
22
|
+
* header sits on. Tab characters are left alone; tool renderers that need
|
|
23
|
+
* tab-safe text run `replaceTabs()` themselves.
|
|
24
|
+
*/
|
|
25
|
+
function flattenForHeader(text: string): string {
|
|
26
|
+
return text.replace(/\r\n?|\n/g, " ");
|
|
27
|
+
}
|
|
28
|
+
|
|
18
29
|
export function renderStatusLine(options: StatusLineOptions, theme: Theme): string {
|
|
19
30
|
const icon = options.icon ? formatStatusIcon(options.icon, theme, options.spinnerFrame) : "";
|
|
20
31
|
const titleColor = options.titleColor ?? "accent";
|
|
21
|
-
const title = theme.fg(titleColor, options.title);
|
|
32
|
+
const title = theme.fg(titleColor, flattenForHeader(options.title));
|
|
22
33
|
let line = icon ? `${icon} ${title}` : title;
|
|
23
34
|
|
|
24
35
|
if (options.description) {
|
|
25
|
-
line += `: ${theme.fg("muted", options.description)}`;
|
|
36
|
+
line += `: ${theme.fg("muted", flattenForHeader(options.description))}`;
|
|
26
37
|
}
|
|
27
38
|
|
|
28
39
|
if (options.badge) {
|
|
29
40
|
const { label, color } = options.badge;
|
|
30
|
-
line += ` ${theme.fg(color, `${theme.format.bracketLeft}${label}${theme.format.bracketRight}`)}`;
|
|
41
|
+
line += ` ${theme.fg(color, `${theme.format.bracketLeft}${flattenForHeader(label)}${theme.format.bracketRight}`)}`;
|
|
31
42
|
}
|
|
32
43
|
|
|
33
|
-
const meta = options.meta?.filter(value => value.trim().length > 0) ?? [];
|
|
44
|
+
const meta = options.meta?.map(flattenForHeader).filter(value => value.trim().length > 0) ?? [];
|
|
34
45
|
if (meta.length > 0) {
|
|
35
46
|
line += ` ${theme.fg("dim", meta.join(theme.sep.dot))}`;
|
|
36
47
|
}
|