@oh-my-pi/pi-coding-agent 15.0.0 → 15.0.1
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 +41 -0
- package/examples/extensions/plan-mode.ts +0 -1
- package/package.json +9 -9
- package/scripts/build-binary.ts +5 -0
- package/src/autoresearch/helpers.ts +17 -0
- package/src/autoresearch/tools/log-experiment.ts +9 -17
- package/src/autoresearch/tools/run-experiment.ts +2 -17
- package/src/capability/skill.ts +7 -0
- package/src/cli/list-models.ts +1 -1
- package/src/cli/shell-cli.ts +3 -13
- package/src/cli/update-cli.ts +1 -1
- package/src/cli.ts +10 -29
- package/src/commit/agentic/tools/propose-changelog.ts +8 -1
- package/src/commit/analysis/conventional.ts +8 -66
- package/src/commit/map-reduce/reduce-phase.ts +6 -65
- package/src/commit/pipeline.ts +2 -2
- package/src/commit/shared-llm.ts +89 -0
- package/src/config/config-file.ts +210 -0
- package/src/config/model-equivalence.ts +8 -11
- package/src/config/model-registry.ts +13 -2
- package/src/config/model-resolver.ts +1 -4
- package/src/config/settings-schema.ts +71 -1
- package/src/config/settings.ts +1 -1
- package/src/config.ts +3 -219
- package/src/edit/renderer.ts +7 -1
- package/src/eval/js/executor.ts +3 -0
- package/src/eval/js/shared/rewrite-imports.ts +2 -2
- package/src/eval/py/executor.ts +5 -0
- package/src/exa/factory.ts +2 -2
- package/src/exa/mcp-client.ts +74 -1
- package/src/exec/bash-executor.ts +5 -1
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +0 -11
- package/src/extensibility/extensions/runner.ts +1 -1
- package/src/extensibility/extensions/types.ts +89 -223
- package/src/extensibility/hooks/types.ts +89 -314
- package/src/extensibility/shared-events.ts +343 -0
- package/src/extensibility/skills.ts +9 -0
- package/src/goals/index.ts +3 -0
- package/src/goals/runtime.ts +500 -0
- package/src/goals/state.ts +37 -0
- package/src/goals/tools/goal-tool.ts +237 -0
- package/src/hashline/anchors.ts +2 -2
- package/src/hindsight/mental-models.ts +1 -1
- package/src/internal-urls/agent-protocol.ts +1 -20
- package/src/internal-urls/artifact-protocol.ts +1 -19
- package/src/internal-urls/docs-index.generated.ts +5 -6
- package/src/internal-urls/registry-helpers.ts +25 -0
- package/src/main.ts +11 -2
- package/src/mcp/oauth-flow.ts +20 -0
- package/src/modes/acp/acp-agent.ts +79 -45
- package/src/modes/components/assistant-message.ts +14 -8
- package/src/modes/components/bash-execution.ts +24 -63
- package/src/modes/components/custom-message.ts +14 -40
- package/src/modes/components/eval-execution.ts +27 -57
- package/src/modes/components/execution-shared.ts +102 -0
- package/src/modes/components/hook-message.ts +17 -49
- package/src/modes/components/mcp-add-wizard.ts +26 -5
- package/src/modes/components/message-frame.ts +88 -0
- package/src/modes/components/model-selector.ts +1 -1
- package/src/modes/components/session-observer-overlay.ts +6 -2
- package/src/modes/components/session-selector.ts +1 -1
- package/src/modes/components/status-line/segments.ts +55 -4
- package/src/modes/components/status-line/types.ts +4 -0
- package/src/modes/components/status-line.ts +28 -10
- package/src/modes/components/tool-execution.ts +7 -8
- package/src/modes/controllers/command-controller-shared.ts +108 -0
- package/src/modes/controllers/command-controller.ts +13 -4
- package/src/modes/controllers/event-controller.ts +36 -7
- package/src/modes/controllers/input-controller.ts +13 -0
- package/src/modes/controllers/mcp-command-controller.ts +56 -61
- package/src/modes/controllers/ssh-command-controller.ts +18 -57
- package/src/modes/interactive-mode.ts +624 -52
- package/src/modes/print-mode.ts +16 -86
- package/src/modes/rpc/rpc-mode.ts +14 -87
- package/src/modes/runtime-init.ts +115 -0
- package/src/modes/theme/defaults/dark-poimandres.json +2 -0
- package/src/modes/theme/defaults/light-poimandres.json +2 -0
- package/src/modes/theme/theme.ts +18 -6
- package/src/modes/types.ts +14 -3
- package/src/modes/utils/context-usage.ts +13 -13
- package/src/modes/utils/ui-helpers.ts +10 -3
- package/src/plan-mode/approved-plan.ts +35 -1
- package/src/prompts/goals/goal-budget-limit.md +16 -0
- package/src/prompts/goals/goal-continuation.md +28 -0
- package/src/prompts/goals/goal-mode-active.md +23 -0
- package/src/prompts/system/plan-mode-active.md +5 -5
- package/src/prompts/system/plan-mode-tool-decision-reminder.md +1 -1
- package/src/prompts/tools/bash.md +6 -0
- package/src/prompts/tools/goal.md +13 -0
- package/src/prompts/tools/hashline.md +102 -114
- package/src/prompts/tools/read.md +1 -0
- package/src/prompts/tools/resolve.md +6 -5
- package/src/sdk.ts +12 -5
- package/src/session/agent-session.ts +428 -106
- package/src/session/blob-store.ts +36 -3
- package/src/session/messages.ts +67 -2
- package/src/session/session-manager.ts +131 -12
- package/src/session/session-storage.ts +33 -15
- package/src/session/streaming-output.ts +309 -13
- package/src/slash-commands/builtin-registry.ts +18 -0
- package/src/ssh/ssh-executor.ts +5 -0
- package/src/system-prompt.ts +4 -2
- package/src/task/executor.ts +17 -7
- package/src/task/index.ts +3 -0
- package/src/task/render.ts +21 -15
- package/src/task/types.ts +4 -0
- package/src/tools/ast-edit.ts +21 -120
- package/src/tools/ast-grep.ts +21 -119
- package/src/tools/bash-interactive.ts +9 -1
- package/src/tools/bash.ts +27 -4
- package/src/tools/browser/attach.ts +3 -3
- package/src/tools/browser/launch.ts +81 -18
- package/src/tools/browser/registry.ts +1 -5
- package/src/tools/browser/tab-supervisor.ts +51 -14
- package/src/tools/conflict-detect.ts +15 -4
- package/src/tools/eval.ts +3 -1
- package/src/tools/find.ts +20 -38
- package/src/tools/gh.ts +7 -6
- package/src/tools/index.ts +22 -11
- package/src/tools/inspect-image.ts +3 -10
- package/src/tools/output-meta.ts +176 -37
- package/src/tools/path-utils.ts +125 -2
- package/src/tools/read.ts +516 -233
- package/src/tools/render-utils.ts +92 -0
- package/src/tools/renderers.ts +2 -0
- package/src/tools/resolve.ts +72 -44
- package/src/tools/search.ts +120 -186
- package/src/tools/write.ts +44 -9
- package/src/utils/file-mentions.ts +1 -1
- package/src/utils/image-loading.ts +7 -3
- package/src/utils/image-resize.ts +32 -43
- package/src/vim/parser.ts +0 -17
- package/src/vim/render.ts +1 -1
- package/src/vim/types.ts +1 -1
- package/src/web/search/providers/gemini.ts +35 -95
- package/src/prompts/tools/exit-plan-mode.md +0 -6
- package/src/tools/exit-plan-mode.ts +0 -97
- package/src/utils/fuzzy.ts +0 -108
- package/src/utils/image-convert.ts +0 -27
|
@@ -3,7 +3,7 @@ import * as os from "node:os";
|
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { $which, getPuppeteerDir, logger } from "@oh-my-pi/pi-utils";
|
|
5
5
|
import * as browsers from "@puppeteer/browsers";
|
|
6
|
-
import type { Browser, CDPSession, Page, default as Puppeteer } from "puppeteer-core";
|
|
6
|
+
import type { Browser, CDPSession, Page, default as Puppeteer, Target } from "puppeteer-core";
|
|
7
7
|
import { PUPPETEER_REVISIONS } from "puppeteer-core/internal/revisions.js";
|
|
8
8
|
import stealthTamperingScript from "../puppeteer/00_stealth_tampering.txt" with { type: "text" };
|
|
9
9
|
import stealthActivityScript from "../puppeteer/01_stealth_activity.txt" with { type: "text" };
|
|
@@ -30,13 +30,15 @@ export const DEFAULT_VIEWPORT = { width: 1365, height: 768, deviceScaleFactor: 1
|
|
|
30
30
|
* connection dropped, etc.).
|
|
31
31
|
*/
|
|
32
32
|
export const BROWSER_PROTOCOL_TIMEOUT_MS = 60_000;
|
|
33
|
-
|
|
33
|
+
const STEALTH_IGNORE_DEFAULT_ARGS = [
|
|
34
34
|
"--disable-extensions",
|
|
35
35
|
"--disable-default-apps",
|
|
36
36
|
"--disable-component-extensions-with-background-pages",
|
|
37
37
|
];
|
|
38
|
-
|
|
38
|
+
const STEALTH_ACCEPT_LANGUAGE = "en-US,en";
|
|
39
39
|
|
|
40
|
+
const USER_AGENT_TARGET_TIMEOUT_MS = 5_000;
|
|
41
|
+
const USER_AGENT_TARGET_TYPES = new Set(["page", "webview", "background_page"]);
|
|
40
42
|
const PUPPETEER_SOURCE_URL_SUFFIX = "//# sourceURL=__puppeteer_evaluation_script__";
|
|
41
43
|
|
|
42
44
|
/**
|
|
@@ -82,7 +84,7 @@ export async function loadPuppeteerInWorker(safeDir: string): Promise<typeof Pup
|
|
|
82
84
|
* The browser is cached under ~/.omp/puppeteer (getPuppeteerDir).
|
|
83
85
|
*/
|
|
84
86
|
let chromiumExecutablePromise: Promise<string | undefined> | undefined;
|
|
85
|
-
|
|
87
|
+
async function ensureChromiumExecutable(): Promise<string | undefined> {
|
|
86
88
|
const sysChrome = resolveSystemChromium();
|
|
87
89
|
if (sysChrome) return sysChrome;
|
|
88
90
|
const envPath = process.env.PUPPETEER_EXECUTABLE_PATH;
|
|
@@ -138,7 +140,7 @@ export async function ensureChromiumExecutable(): Promise<string | undefined> {
|
|
|
138
140
|
return chromiumExecutablePromise;
|
|
139
141
|
}
|
|
140
142
|
|
|
141
|
-
let
|
|
143
|
+
let resolvedChromium: string | null | undefined; // undefined = unchecked; null = not found
|
|
142
144
|
|
|
143
145
|
function isExecutableFile(p: string): boolean {
|
|
144
146
|
try {
|
|
@@ -209,19 +211,19 @@ function systemChromiumCandidates(): string[] {
|
|
|
209
211
|
return candidates;
|
|
210
212
|
}
|
|
211
213
|
|
|
212
|
-
|
|
213
|
-
if (
|
|
214
|
+
function resolveSystemChromium(): string | undefined {
|
|
215
|
+
if (resolvedChromium !== undefined) return resolvedChromium ?? undefined;
|
|
214
216
|
const seen = new Set<string>();
|
|
215
217
|
for (const candidate of systemChromiumCandidates()) {
|
|
216
218
|
if (!candidate || seen.has(candidate)) continue;
|
|
217
219
|
seen.add(candidate);
|
|
218
220
|
if (isExecutableFile(candidate)) {
|
|
219
|
-
|
|
221
|
+
resolvedChromium = candidate;
|
|
220
222
|
logger.debug("Using system Chrome/Chromium", { path: candidate });
|
|
221
223
|
return candidate;
|
|
222
224
|
}
|
|
223
225
|
}
|
|
224
|
-
|
|
226
|
+
resolvedChromium = null;
|
|
225
227
|
return undefined;
|
|
226
228
|
}
|
|
227
229
|
|
|
@@ -463,6 +465,7 @@ export interface UserAgentSession {
|
|
|
463
465
|
async function configureUserAgentTargets(
|
|
464
466
|
browser: Browser,
|
|
465
467
|
state: { browserSession: CDPSession | null; override: UserAgentOverride },
|
|
468
|
+
targetTimeoutMs = USER_AGENT_TARGET_TIMEOUT_MS,
|
|
466
469
|
): Promise<void> {
|
|
467
470
|
if (!state.browserSession) {
|
|
468
471
|
state.browserSession = await browser.target().createCDPSession();
|
|
@@ -471,23 +474,72 @@ async function configureUserAgentTargets(
|
|
|
471
474
|
waitForDebuggerOnStart: false,
|
|
472
475
|
flatten: true,
|
|
473
476
|
});
|
|
474
|
-
state.browserSession.on(
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
477
|
+
state.browserSession.on(
|
|
478
|
+
"Target.attachedToTarget",
|
|
479
|
+
async (event: { sessionId: string; targetInfo?: { type?: string } }) => {
|
|
480
|
+
if (!targetInfoSupportsUserAgentOverride(event.targetInfo)) return;
|
|
481
|
+
const connection = state.browserSession?.connection();
|
|
482
|
+
const session = connection?.session(event.sessionId);
|
|
483
|
+
if (!session) return;
|
|
484
|
+
await withSoftTimeout(
|
|
485
|
+
sendUserAgentOverride(wrapSession(session), state.override),
|
|
486
|
+
targetTimeoutMs,
|
|
487
|
+
"new target user-agent override",
|
|
488
|
+
);
|
|
489
|
+
},
|
|
490
|
+
);
|
|
480
491
|
}
|
|
481
492
|
|
|
482
|
-
const targets = browser.targets();
|
|
493
|
+
const targets = browser.targets().filter(targetSupportsUserAgentOverride);
|
|
483
494
|
await Promise.all(
|
|
484
495
|
targets.map(async target => {
|
|
485
|
-
|
|
486
|
-
|
|
496
|
+
await withSoftTimeout(
|
|
497
|
+
applyTargetUserAgentOverride(target, state.override),
|
|
498
|
+
targetTimeoutMs,
|
|
499
|
+
"target user-agent override",
|
|
500
|
+
);
|
|
487
501
|
}),
|
|
488
502
|
);
|
|
489
503
|
}
|
|
490
504
|
|
|
505
|
+
function targetSupportsUserAgentOverride(target: Target): boolean {
|
|
506
|
+
return targetInfoSupportsUserAgentOverride({ type: target.type() });
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function targetInfoSupportsUserAgentOverride(targetInfo: { type?: string } | undefined): boolean {
|
|
510
|
+
return Boolean(targetInfo?.type && USER_AGENT_TARGET_TYPES.has(targetInfo.type));
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
async function applyTargetUserAgentOverride(target: Target, override: UserAgentOverride): Promise<void> {
|
|
514
|
+
const session = await target.createCDPSession();
|
|
515
|
+
try {
|
|
516
|
+
await sendUserAgentOverride(wrapSession(session), override);
|
|
517
|
+
} finally {
|
|
518
|
+
await session.detach().catch(() => undefined);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
async function withSoftTimeout<T>(promise: Promise<T>, timeoutMs: number, label: string): Promise<T | undefined> {
|
|
523
|
+
let timeout: NodeJS.Timeout | undefined;
|
|
524
|
+
const timeoutPromise = new Promise<undefined>(resolve => {
|
|
525
|
+
timeout = setTimeout(() => {
|
|
526
|
+
logger.debug(`Timed out applying ${label}`);
|
|
527
|
+
resolve(undefined);
|
|
528
|
+
}, timeoutMs);
|
|
529
|
+
});
|
|
530
|
+
try {
|
|
531
|
+
return await Promise.race([
|
|
532
|
+
promise.catch(error => {
|
|
533
|
+
logger.debug(`Failed to apply ${label}`, { error: error instanceof Error ? error.message : String(error) });
|
|
534
|
+
return undefined;
|
|
535
|
+
}),
|
|
536
|
+
timeoutPromise,
|
|
537
|
+
]);
|
|
538
|
+
} finally {
|
|
539
|
+
if (timeout) clearTimeout(timeout);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
491
543
|
async function injectStealthScripts(page: Page): Promise<void> {
|
|
492
544
|
const scripts = [
|
|
493
545
|
stealthTamperingScript,
|
|
@@ -574,3 +626,14 @@ export async function applyStealthPatches(
|
|
|
574
626
|
state.browserSession = targetState.browserSession;
|
|
575
627
|
await injectStealthScripts(page);
|
|
576
628
|
}
|
|
629
|
+
|
|
630
|
+
export function targetSupportsUserAgentOverrideForTest(target: Target): boolean {
|
|
631
|
+
return targetSupportsUserAgentOverride(target);
|
|
632
|
+
}
|
|
633
|
+
export async function configureUserAgentTargetsForTest(
|
|
634
|
+
browser: Browser,
|
|
635
|
+
state: { browserSession: CDPSession | null; override: UserAgentOverride },
|
|
636
|
+
targetTimeoutMs?: number,
|
|
637
|
+
): Promise<void> {
|
|
638
|
+
await configureUserAgentTargets(browser, state, targetTimeoutMs);
|
|
639
|
+
}
|
|
@@ -26,10 +26,6 @@ export interface BrowserHandle {
|
|
|
26
26
|
|
|
27
27
|
const browsers = new Map<string, BrowserHandle>();
|
|
28
28
|
|
|
29
|
-
export function listBrowsers(): BrowserHandle[] {
|
|
30
|
-
return [...browsers.values()];
|
|
31
|
-
}
|
|
32
|
-
|
|
33
29
|
function browserKey(kind: BrowserKind): string {
|
|
34
30
|
switch (kind.kind) {
|
|
35
31
|
case "headless":
|
|
@@ -166,7 +162,7 @@ export async function releaseBrowser(handle: BrowserHandle, opts: { kill: boolea
|
|
|
166
162
|
}
|
|
167
163
|
}
|
|
168
164
|
|
|
169
|
-
|
|
165
|
+
async function disposeBrowserHandle(handle: BrowserHandle, opts: { kill: boolean }): Promise<void> {
|
|
170
166
|
if (handle.kind.kind === "headless") {
|
|
171
167
|
if (handle.browser.connected) {
|
|
172
168
|
try {
|
|
@@ -30,6 +30,7 @@ import type {
|
|
|
30
30
|
interface WorkerHandle {
|
|
31
31
|
send(msg: WorkerInbound, transferList?: Transferable[]): void;
|
|
32
32
|
onMessage(handler: (msg: WorkerOutbound) => void): () => void;
|
|
33
|
+
onError(handler: (error: Error) => void): () => void;
|
|
33
34
|
terminate(): Promise<void>;
|
|
34
35
|
readonly mode: "worker" | "inline";
|
|
35
36
|
}
|
|
@@ -89,10 +90,6 @@ export function getTab(name: string): TabSession | undefined {
|
|
|
89
90
|
return tabs.get(name);
|
|
90
91
|
}
|
|
91
92
|
|
|
92
|
-
export function listTabs(): TabSession[] {
|
|
93
|
-
return [...tabs.values()];
|
|
94
|
-
}
|
|
95
|
-
|
|
96
93
|
export async function acquireTab(
|
|
97
94
|
name: string,
|
|
98
95
|
browser: BrowserHandle,
|
|
@@ -124,23 +121,14 @@ export async function acquireTab(
|
|
|
124
121
|
|
|
125
122
|
const initPayload = await buildInitPayload(browser, opts);
|
|
126
123
|
const worker = await spawnTabWorker();
|
|
127
|
-
const { promise, resolve, reject } = Promise.withResolvers<ReadyInfo>();
|
|
128
|
-
const unlisten = worker.onMessage(msg => {
|
|
129
|
-
if (msg.type === "ready") resolve(msg.info);
|
|
130
|
-
else if (msg.type === "init-failed") reject(errorFromPayload(msg.error));
|
|
131
|
-
else if (msg.type === "log") logWorkerMessage(msg);
|
|
132
|
-
});
|
|
133
124
|
let info: ReadyInfo;
|
|
134
125
|
try {
|
|
135
|
-
|
|
136
|
-
info = await raceWithTimeout(promise, opts.timeoutMs + GRACE_MS, "Timed out initializing browser tab worker");
|
|
126
|
+
info = await initializeTabWorker(worker, initPayload, opts.timeoutMs + GRACE_MS);
|
|
137
127
|
} catch (error) {
|
|
138
|
-
unlisten();
|
|
139
128
|
await worker.terminate().catch(() => undefined);
|
|
140
129
|
if (browser.refCount === 0) await releaseBrowser(browser, { kill: false });
|
|
141
130
|
throw error;
|
|
142
131
|
}
|
|
143
|
-
unlisten();
|
|
144
132
|
|
|
145
133
|
holdBrowser(browser);
|
|
146
134
|
const tab: TabSession = {
|
|
@@ -477,6 +465,17 @@ function wrapBunWorker(worker: Worker): WorkerHandle {
|
|
|
477
465
|
worker.addEventListener("message", wrap);
|
|
478
466
|
return () => worker.removeEventListener("message", wrap);
|
|
479
467
|
},
|
|
468
|
+
onError(handler) {
|
|
469
|
+
const onError = (event: ErrorEvent): void => handler(errorFromWorkerEvent(event));
|
|
470
|
+
const onMessageError = (event: MessageEvent): void =>
|
|
471
|
+
handler(new ToolError(`Tab worker message error: ${String(event.data)}`));
|
|
472
|
+
worker.addEventListener("error", onError);
|
|
473
|
+
worker.addEventListener("messageerror", onMessageError);
|
|
474
|
+
return () => {
|
|
475
|
+
worker.removeEventListener("error", onError);
|
|
476
|
+
worker.removeEventListener("messageerror", onMessageError);
|
|
477
|
+
};
|
|
478
|
+
},
|
|
480
479
|
async terminate() {
|
|
481
480
|
worker.terminate();
|
|
482
481
|
},
|
|
@@ -515,6 +514,44 @@ async function spawnInlineWorker(): Promise<WorkerHandle> {
|
|
|
515
514
|
hostListeners.add(handler);
|
|
516
515
|
return () => hostListeners.delete(handler);
|
|
517
516
|
},
|
|
517
|
+
onError: () => () => {},
|
|
518
518
|
async terminate() {},
|
|
519
519
|
};
|
|
520
520
|
}
|
|
521
|
+
|
|
522
|
+
async function initializeTabWorker(
|
|
523
|
+
worker: WorkerHandle,
|
|
524
|
+
payload: WorkerInitPayload,
|
|
525
|
+
timeoutMs: number,
|
|
526
|
+
): Promise<ReadyInfo> {
|
|
527
|
+
const { promise, resolve, reject } = Promise.withResolvers<ReadyInfo>();
|
|
528
|
+
const unlisten = worker.onMessage(msg => {
|
|
529
|
+
if (msg.type === "ready") resolve(msg.info);
|
|
530
|
+
else if (msg.type === "init-failed") reject(errorFromPayload(msg.error));
|
|
531
|
+
else if (msg.type === "log") logWorkerMessage(msg);
|
|
532
|
+
});
|
|
533
|
+
const unlistenError = worker.onError(error => {
|
|
534
|
+
reject(new ToolError(`Tab worker failed during startup: ${error.message}`));
|
|
535
|
+
});
|
|
536
|
+
try {
|
|
537
|
+
worker.send({ type: "init", payload });
|
|
538
|
+
return await raceWithTimeout(promise, timeoutMs, "Timed out initializing browser tab worker");
|
|
539
|
+
} finally {
|
|
540
|
+
unlisten();
|
|
541
|
+
unlistenError();
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
export function initializeTabWorkerForTest(
|
|
546
|
+
worker: WorkerHandle,
|
|
547
|
+
payload: WorkerInitPayload,
|
|
548
|
+
timeoutMs: number,
|
|
549
|
+
): Promise<ReadyInfo> {
|
|
550
|
+
return initializeTabWorker(worker, payload, timeoutMs);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function errorFromWorkerEvent(event: ErrorEvent): Error {
|
|
554
|
+
if (event.error instanceof Error) return event.error;
|
|
555
|
+
if (event.message) return new Error(event.message);
|
|
556
|
+
return new Error("Unknown tab worker error");
|
|
557
|
+
}
|
|
@@ -250,9 +250,19 @@ export interface ParsedConflictUri {
|
|
|
250
250
|
/** `"*"` selects every currently-registered conflict (bulk write only). */
|
|
251
251
|
id: number | "*";
|
|
252
252
|
scope?: ConflictScope;
|
|
253
|
+
/**
|
|
254
|
+
* When `raw` was a malformed `<file-prefix>:conflict://…` path, the
|
|
255
|
+
* stripped prefix is preserved here so callers can surface a gentle
|
|
256
|
+
* "you don't need the file path" note. `undefined` for clean URIs.
|
|
257
|
+
*/
|
|
258
|
+
recoveredPrefix?: string;
|
|
253
259
|
}
|
|
254
260
|
|
|
255
|
-
|
|
261
|
+
// Accept an optional `<prefix>:` before the scheme so paths like
|
|
262
|
+
// `path/to/file.ts:conflict://3` (where the agent mixed the `:conflicts`
|
|
263
|
+
// read selector with the `conflict://` scheme) still resolve. The prefix
|
|
264
|
+
// is greedy so the LAST `:conflict://` wins for multi-colon inputs.
|
|
265
|
+
const CONFLICT_URI_RE = /^(?:(.+):)?conflict:\/\/(.+)$/;
|
|
256
266
|
|
|
257
267
|
/**
|
|
258
268
|
* Parse a `conflict://<N>`, `conflict://<N>/<scope>`, or `conflict://*` URI.
|
|
@@ -269,7 +279,8 @@ const CONFLICT_URI_RE = /^conflict:\/\/(.+)$/;
|
|
|
269
279
|
export function parseConflictUri(raw: string): ParsedConflictUri | null {
|
|
270
280
|
const match = raw.match(CONFLICT_URI_RE);
|
|
271
281
|
if (!match) return null;
|
|
272
|
-
const
|
|
282
|
+
const recoveredPrefix = match[1];
|
|
283
|
+
const tail = match[2];
|
|
273
284
|
const slashIdx = tail.indexOf("/");
|
|
274
285
|
const idPart = slashIdx === -1 ? tail : tail.slice(0, slashIdx);
|
|
275
286
|
const scopePart = slashIdx === -1 ? undefined : tail.slice(slashIdx + 1);
|
|
@@ -280,7 +291,7 @@ export function parseConflictUri(raw: string): ParsedConflictUri | null {
|
|
|
280
291
|
`Invalid conflict URI '${raw}': wildcard 'conflict://*' does not accept a scope segment. Drop '/${scopePart}' or use a numeric id.`,
|
|
281
292
|
);
|
|
282
293
|
}
|
|
283
|
-
return { id: "*" };
|
|
294
|
+
return recoveredPrefix !== undefined ? { id: "*", recoveredPrefix } : { id: "*" };
|
|
284
295
|
}
|
|
285
296
|
|
|
286
297
|
if (!/^\d+$/.test(idPart)) {
|
|
@@ -303,7 +314,7 @@ export function parseConflictUri(raw: string): ParsedConflictUri | null {
|
|
|
303
314
|
scope = scopePart as ConflictScope;
|
|
304
315
|
}
|
|
305
316
|
|
|
306
|
-
return { id, scope };
|
|
317
|
+
return recoveredPrefix !== undefined ? { id, scope, recoveredPrefix } : { id, scope };
|
|
307
318
|
}
|
|
308
319
|
|
|
309
320
|
/**
|
package/src/tools/eval.ts
CHANGED
|
@@ -16,7 +16,7 @@ import evalDescription from "../prompts/tools/eval.md" with { type: "text" };
|
|
|
16
16
|
import { DEFAULT_MAX_BYTES, OutputSink, type OutputSummary, TailBuffer } from "../session/streaming-output";
|
|
17
17
|
import { getTreeBranch, getTreeContinuePrefix, renderCodeCell } from "../tui";
|
|
18
18
|
import { resolveEvalBackends, type ToolSession } from ".";
|
|
19
|
-
import { formatStyledTruncationWarning } from "./output-meta";
|
|
19
|
+
import { formatStyledTruncationWarning, resolveOutputMaxColumns, resolveOutputSinkHeadBytes } from "./output-meta";
|
|
20
20
|
import { formatTitle, replaceTabs, shortenPath, truncateToWidth, wrapBrackets } from "./render-utils";
|
|
21
21
|
import { ToolAbortError, ToolError } from "./tool-errors";
|
|
22
22
|
import { toolResult } from "./tool-result";
|
|
@@ -358,6 +358,8 @@ export class EvalTool implements AgentTool<typeof evalSchema> {
|
|
|
358
358
|
outputSink = new OutputSink({
|
|
359
359
|
artifactPath,
|
|
360
360
|
artifactId,
|
|
361
|
+
headBytes: resolveOutputSinkHeadBytes(session.settings),
|
|
362
|
+
maxColumns: resolveOutputMaxColumns(session.settings),
|
|
361
363
|
onChunk: chunk => {
|
|
362
364
|
appendTail(chunk);
|
|
363
365
|
pushUpdate();
|
package/src/tools/find.ts
CHANGED
|
@@ -12,15 +12,7 @@ import { InternalUrlRouter } from "../internal-urls";
|
|
|
12
12
|
import type { Theme } from "../modes/theme/theme";
|
|
13
13
|
import findDescription from "../prompts/tools/find.md" with { type: "text" };
|
|
14
14
|
import { type TruncationResult, truncateHead } from "../session/streaming-output";
|
|
15
|
-
import {
|
|
16
|
-
Ellipsis,
|
|
17
|
-
Hasher,
|
|
18
|
-
type RenderCache,
|
|
19
|
-
renderFileList,
|
|
20
|
-
renderStatusLine,
|
|
21
|
-
renderTreeList,
|
|
22
|
-
truncateToWidth,
|
|
23
|
-
} from "../tui";
|
|
15
|
+
import { Ellipsis, renderFileList, renderStatusLine, renderTreeList, truncateToWidth } from "../tui";
|
|
24
16
|
import type { ToolSession } from ".";
|
|
25
17
|
import { applyListLimit } from "./list-limit";
|
|
26
18
|
import { formatFullOutputReference, type OutputMeta } from "./output-meta";
|
|
@@ -33,7 +25,13 @@ import {
|
|
|
33
25
|
resolveExplicitFindPatterns,
|
|
34
26
|
resolveToCwd,
|
|
35
27
|
} from "./path-utils";
|
|
36
|
-
import {
|
|
28
|
+
import {
|
|
29
|
+
createCachedComponent,
|
|
30
|
+
formatCount,
|
|
31
|
+
formatEmptyMessage,
|
|
32
|
+
formatErrorMessage,
|
|
33
|
+
PREVIEW_LIMITS,
|
|
34
|
+
} from "./render-utils";
|
|
37
35
|
import { ToolAbortError, ToolError, throwIfAborted } from "./tool-errors";
|
|
38
36
|
import { toolResult } from "./tool-result";
|
|
39
37
|
|
|
@@ -401,30 +399,22 @@ export const findToolRenderer = {
|
|
|
401
399
|
},
|
|
402
400
|
uiTheme,
|
|
403
401
|
);
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
const { expanded } = options;
|
|
408
|
-
const key = new Hasher().bool(expanded).u32(width).digest();
|
|
409
|
-
if (cached?.key === key) return cached.lines;
|
|
402
|
+
return createCachedComponent(
|
|
403
|
+
() => options.expanded,
|
|
404
|
+
width => {
|
|
410
405
|
const listLines = renderTreeList(
|
|
411
406
|
{
|
|
412
407
|
items: lines,
|
|
413
|
-
expanded,
|
|
408
|
+
expanded: options.expanded,
|
|
414
409
|
maxCollapsed: COLLAPSED_LIST_LIMIT,
|
|
415
410
|
itemType: "file",
|
|
416
411
|
renderItem: line => uiTheme.fg("accent", line),
|
|
417
412
|
},
|
|
418
413
|
uiTheme,
|
|
419
414
|
);
|
|
420
|
-
|
|
421
|
-
cached = { key, lines: result };
|
|
422
|
-
return result;
|
|
415
|
+
return [header, ...listLines].map(l => truncateToWidth(l, width, Ellipsis.Omit));
|
|
423
416
|
},
|
|
424
|
-
|
|
425
|
-
cached = undefined;
|
|
426
|
-
},
|
|
427
|
-
};
|
|
417
|
+
);
|
|
428
418
|
}
|
|
429
419
|
|
|
430
420
|
const fileCount = details?.fileCount ?? 0;
|
|
@@ -467,28 +457,20 @@ export const findToolRenderer = {
|
|
|
467
457
|
}
|
|
468
458
|
if (missingNote) extraLines.push(missingNote);
|
|
469
459
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
const { expanded } = options;
|
|
474
|
-
const key = new Hasher().bool(expanded).u32(width).digest();
|
|
475
|
-
if (cached?.key === key) return cached.lines;
|
|
460
|
+
return createCachedComponent(
|
|
461
|
+
() => options.expanded,
|
|
462
|
+
width => {
|
|
476
463
|
const fileLines = renderFileList(
|
|
477
464
|
{
|
|
478
465
|
files: files.map(entry => ({ path: entry, isDirectory: entry.endsWith("/") })),
|
|
479
|
-
expanded,
|
|
466
|
+
expanded: options.expanded,
|
|
480
467
|
maxCollapsed: COLLAPSED_LIST_LIMIT,
|
|
481
468
|
},
|
|
482
469
|
uiTheme,
|
|
483
470
|
);
|
|
484
|
-
|
|
485
|
-
cached = { key, lines: result };
|
|
486
|
-
return result;
|
|
487
|
-
},
|
|
488
|
-
invalidate() {
|
|
489
|
-
cached = undefined;
|
|
471
|
+
return [header, ...fileLines, ...extraLines].map(l => truncateToWidth(l, width, Ellipsis.Omit));
|
|
490
472
|
},
|
|
491
|
-
|
|
473
|
+
);
|
|
492
474
|
},
|
|
493
475
|
mergeCallAndResult: true,
|
|
494
476
|
};
|
package/src/tools/gh.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
+
import { scheduler } from "node:timers/promises";
|
|
4
5
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
5
6
|
import { StringEnum } from "@oh-my-pi/pi-ai";
|
|
6
|
-
import {
|
|
7
|
+
import { getWorktreesDir, isEnoent, prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
7
8
|
import { type Static, Type } from "@sinclair/typebox";
|
|
8
9
|
import type { Settings } from "../config/settings";
|
|
9
10
|
import githubDescription from "../prompts/tools/github.md" with { type: "text" };
|
|
@@ -3400,7 +3401,7 @@ async function executeRunWatch(
|
|
|
3400
3401
|
note,
|
|
3401
3402
|
}),
|
|
3402
3403
|
});
|
|
3403
|
-
await
|
|
3404
|
+
await scheduler.wait(graceSeconds * 1000, { signal });
|
|
3404
3405
|
run = await fetchRunSnapshot(session.cwd, repo, runId, signal);
|
|
3405
3406
|
}
|
|
3406
3407
|
|
|
@@ -3435,7 +3436,7 @@ async function executeRunWatch(
|
|
|
3435
3436
|
return buildTextResult(formatRunWatchResult(repo, run, [], tail), run.url, finalDetails);
|
|
3436
3437
|
}
|
|
3437
3438
|
|
|
3438
|
-
await
|
|
3439
|
+
await scheduler.wait(intervalSeconds * 1000, { signal });
|
|
3439
3440
|
}
|
|
3440
3441
|
}
|
|
3441
3442
|
|
|
@@ -3477,7 +3478,7 @@ async function executeRunWatch(
|
|
|
3477
3478
|
note,
|
|
3478
3479
|
}),
|
|
3479
3480
|
});
|
|
3480
|
-
await
|
|
3481
|
+
await scheduler.wait(graceSeconds * 1000, { signal });
|
|
3481
3482
|
runs = await fetchRunsForCommit(session.cwd, repo, headSha, branch, signal);
|
|
3482
3483
|
}
|
|
3483
3484
|
|
|
@@ -3533,11 +3534,11 @@ async function executeRunWatch(
|
|
|
3533
3534
|
note,
|
|
3534
3535
|
}),
|
|
3535
3536
|
});
|
|
3536
|
-
await
|
|
3537
|
+
await scheduler.wait(intervalSeconds * 1000, { signal });
|
|
3537
3538
|
continue;
|
|
3538
3539
|
}
|
|
3539
3540
|
|
|
3540
3541
|
settledSuccessSignature = undefined;
|
|
3541
|
-
await
|
|
3542
|
+
await scheduler.wait(intervalSeconds * 1000, { signal });
|
|
3542
3543
|
}
|
|
3543
3544
|
}
|
package/src/tools/index.ts
CHANGED
|
@@ -6,6 +6,8 @@ import type { Settings } from "../config/settings";
|
|
|
6
6
|
import { EditTool } from "../edit";
|
|
7
7
|
import { checkPythonKernelAvailability } from "../eval/py/kernel";
|
|
8
8
|
import type { Skill } from "../extensibility/skills";
|
|
9
|
+
import type { GoalModeState, GoalRuntime } from "../goals";
|
|
10
|
+
import { GoalTool } from "../goals/tools/goal-tool";
|
|
9
11
|
import type { HindsightSessionState } from "../hindsight/state";
|
|
10
12
|
import { LspTool } from "../lsp";
|
|
11
13
|
import type { PlanModeState } from "../plan-mode/state";
|
|
@@ -29,7 +31,6 @@ import { CalculatorTool } from "./calculator";
|
|
|
29
31
|
import { type CheckpointState, CheckpointTool, RewindTool } from "./checkpoint";
|
|
30
32
|
import { DebugTool } from "./debug";
|
|
31
33
|
import { EvalTool } from "./eval";
|
|
32
|
-
import { ExitPlanModeTool } from "./exit-plan-mode";
|
|
33
34
|
import { FindTool } from "./find";
|
|
34
35
|
import { GithubTool } from "./gh";
|
|
35
36
|
import { HindsightRecallTool } from "./hindsight-recall";
|
|
@@ -57,6 +58,7 @@ import { YieldTool } from "./yield";
|
|
|
57
58
|
export * from "../edit";
|
|
58
59
|
export * from "../exa";
|
|
59
60
|
export type * from "../exa/types";
|
|
61
|
+
export * from "../goals";
|
|
60
62
|
export * from "../lsp";
|
|
61
63
|
export * from "../session/streaming-output";
|
|
62
64
|
export * from "../task";
|
|
@@ -70,7 +72,6 @@ export * from "./calculator";
|
|
|
70
72
|
export * from "./checkpoint";
|
|
71
73
|
export * from "./debug";
|
|
72
74
|
export * from "./eval";
|
|
73
|
-
export * from "./exit-plan-mode";
|
|
74
75
|
export * from "./find";
|
|
75
76
|
export * from "./gh";
|
|
76
77
|
export * from "./hindsight-recall";
|
|
@@ -179,6 +180,10 @@ export interface ToolSession {
|
|
|
179
180
|
settings: Settings;
|
|
180
181
|
/** Plan mode state (if active) */
|
|
181
182
|
getPlanModeState?: () => PlanModeState | undefined;
|
|
183
|
+
/** Goal mode state (if active or paused) */
|
|
184
|
+
getGoalModeState?: () => GoalModeState | undefined;
|
|
185
|
+
/** Goal runtime for the active agent session. */
|
|
186
|
+
getGoalRuntime?: () => GoalRuntime | undefined;
|
|
182
187
|
/** Bridge to the connected client (e.g. ACP editor host). Tools should route fs/terminal/permission requests through this when available. */
|
|
183
188
|
getClientBridge?: () => ClientBridge | undefined;
|
|
184
189
|
/** Get compact conversation context for subagents (excludes tool results, system prompts) */
|
|
@@ -220,6 +225,12 @@ export interface ToolSession {
|
|
|
220
225
|
steer?(message: { customType: string; content: string; details?: unknown }): void;
|
|
221
226
|
/** Peek the currently in-flight tool-choice queue directive's invocation handler. Used by the `resolve` tool to dispatch to the pending action. */
|
|
222
227
|
peekQueueInvoker?(): ((input: unknown) => Promise<unknown> | unknown) | undefined;
|
|
228
|
+
/** Peek the long-lived "standing" resolve handler registered by a mode (e.g. plan mode).
|
|
229
|
+
* Consulted by the `resolve` tool as a fallback when no queue invoker is in flight,
|
|
230
|
+
* letting modes accept `resolve` invocations without forcing the tool choice every turn. */
|
|
231
|
+
peekStandingResolveHandler?(): ((input: unknown) => Promise<unknown> | unknown) | undefined;
|
|
232
|
+
/** Register or clear the standing resolve handler. Passing `null` clears it. */
|
|
233
|
+
setStandingResolveHandler?(handler: ((input: unknown) => Promise<unknown> | unknown) | null): void;
|
|
223
234
|
/** Get active checkpoint state if any. */
|
|
224
235
|
getCheckpointState?: () => CheckpointState | undefined;
|
|
225
236
|
/** Set or clear active checkpoint state. */
|
|
@@ -303,8 +314,8 @@ export const HIDDEN_TOOLS: Record<string, ToolFactory> = {
|
|
|
303
314
|
yield: s => new YieldTool(s),
|
|
304
315
|
report_finding: () => reportFindingTool,
|
|
305
316
|
report_tool_issue: s => createReportToolIssueTool(s),
|
|
306
|
-
exit_plan_mode: s => new ExitPlanModeTool(s),
|
|
307
317
|
resolve: s => new ResolveTool(s),
|
|
318
|
+
goal: s => new GoalTool(s),
|
|
308
319
|
};
|
|
309
320
|
|
|
310
321
|
export type ToolName = keyof typeof BUILTIN_TOOLS;
|
|
@@ -351,11 +362,12 @@ export function resolveEvalBackends(session: ToolSession): EvalBackendsAllowance
|
|
|
351
362
|
export async function createTools(session: ToolSession, toolNames?: string[]): Promise<Tool[]> {
|
|
352
363
|
const includeYield = session.requireYieldTool === true;
|
|
353
364
|
const enableLsp = session.enableLsp ?? true;
|
|
354
|
-
|
|
365
|
+
let requestedTools =
|
|
355
366
|
toolNames && toolNames.length > 0 ? [...new Set(toolNames.map(name => name.toLowerCase()))] : undefined;
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
|
|
367
|
+
const goalEnabled = session.settings.get("goal.enabled");
|
|
368
|
+
const goalModeActive = goalEnabled && session.getGoalModeState?.()?.enabled === true;
|
|
369
|
+
if (goalModeActive && requestedTools && !requestedTools.includes("goal")) {
|
|
370
|
+
requestedTools = [...requestedTools, "goal"];
|
|
359
371
|
}
|
|
360
372
|
const backends = resolveEvalBackends(session);
|
|
361
373
|
const allowPython = backends.python;
|
|
@@ -428,7 +440,7 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
428
440
|
|
|
429
441
|
const allTools: Record<string, ToolFactory> = { ...BUILTIN_TOOLS, ...HIDDEN_TOOLS };
|
|
430
442
|
const isToolAllowed = (name: string) => {
|
|
431
|
-
if (name === "
|
|
443
|
+
if (name === "goal") return goalEnabled && goalModeActive;
|
|
432
444
|
if (name === "lsp") return enableLsp && session.settings.get("lsp.enabled");
|
|
433
445
|
if (name === "bash") return true;
|
|
434
446
|
if (name === "eval") return allowEval;
|
|
@@ -478,7 +490,7 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
478
490
|
.filter(([name]) => isToolAllowed(name))
|
|
479
491
|
.map(([name, factory]) => [name, factory] as const),
|
|
480
492
|
...(includeYield ? ([["yield", HIDDEN_TOOLS.yield]] as const) : []),
|
|
481
|
-
...(
|
|
493
|
+
...(goalModeActive ? ([["goal", HIDDEN_TOOLS.goal]] as const) : []),
|
|
482
494
|
];
|
|
483
495
|
|
|
484
496
|
const baseResults = await Promise.all(
|
|
@@ -488,8 +500,7 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
488
500
|
}),
|
|
489
501
|
);
|
|
490
502
|
const tools = baseResults.filter((r): r is Tool => r !== null);
|
|
491
|
-
|
|
492
|
-
if (hasDeferrableTools && !tools.some(tool => tool.name === "resolve")) {
|
|
503
|
+
if (!tools.some(tool => tool.name === "resolve")) {
|
|
493
504
|
const resolveTool = await logger.time("createTools:resolve", HIDDEN_TOOLS.resolve, session);
|
|
494
505
|
if (resolveTool) {
|
|
495
506
|
tools.push(wrapToolWithMetaNotice(resolveTool));
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
2
|
-
import { type Api,
|
|
2
|
+
import { type Api, completeSimple, type Model } from "@oh-my-pi/pi-ai";
|
|
3
3
|
import { prompt } from "@oh-my-pi/pi-utils";
|
|
4
4
|
import { type Static, Type } from "@sinclair/typebox";
|
|
5
|
+
import { extractTextContent } from "../commit/utils";
|
|
5
6
|
import { expandRoleAlias, resolveModelFromString } from "../config/model-resolver";
|
|
6
7
|
import inspectImageDescription from "../prompts/tools/inspect-image.md" with { type: "text" };
|
|
7
8
|
import inspectImageSystemPromptTemplate from "../prompts/tools/inspect-image-system.md" with { type: "text" };
|
|
@@ -30,14 +31,6 @@ export interface InspectImageToolDetails {
|
|
|
30
31
|
mimeType: string;
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
function extractResponseText(message: AssistantMessage): string {
|
|
34
|
-
return message.content
|
|
35
|
-
.filter(content => content.type === "text")
|
|
36
|
-
.map(content => content.text)
|
|
37
|
-
.join("")
|
|
38
|
-
.trim();
|
|
39
|
-
}
|
|
40
|
-
|
|
41
34
|
export class InspectImageTool implements AgentTool<typeof inspectImageSchema, InspectImageToolDetails> {
|
|
42
35
|
readonly name = "inspect_image";
|
|
43
36
|
readonly label = "InspectImage";
|
|
@@ -151,7 +144,7 @@ export class InspectImageTool implements AgentTool<typeof inspectImageSchema, In
|
|
|
151
144
|
throw new ToolError("inspect_image request aborted.");
|
|
152
145
|
}
|
|
153
146
|
|
|
154
|
-
const text =
|
|
147
|
+
const text = extractTextContent(response);
|
|
155
148
|
if (!text) {
|
|
156
149
|
throw new ToolError("inspect_image model returned no text output.");
|
|
157
150
|
}
|