@oh-my-pi/pi-coding-agent 14.9.9 → 15.0.0
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 +82 -0
- package/package.json +7 -7
- package/scripts/format-prompts.ts +1 -1
- package/src/cli/args.ts +2 -2
- package/src/cli.ts +1 -0
- package/src/commands/acp.ts +24 -0
- package/src/commands/launch.ts +6 -4
- package/src/commit/agentic/prompts/system.md +1 -1
- package/src/config/model-resolver.ts +30 -0
- package/src/config/settings-schema.ts +31 -0
- package/src/edit/index.ts +22 -1
- package/src/edit/modes/patch.ts +10 -0
- package/src/edit/modes/replace.ts +3 -0
- package/src/edit/renderer.ts +10 -0
- package/src/eval/js/context-manager.ts +1 -1
- package/src/eval/js/shared/rewrite-imports.ts +120 -48
- package/src/eval/js/shared/runtime.ts +31 -4
- package/src/eval/js/tool-bridge.ts +43 -21
- package/src/extensibility/extensions/runner.ts +54 -1
- package/src/extensibility/extensions/types.ts +11 -0
- package/src/extensibility/skills.ts +33 -1
- package/src/internal-urls/docs-index.generated.ts +6 -6
- package/src/internal-urls/index.ts +1 -0
- package/src/internal-urls/issue-pr-protocol.ts +577 -0
- package/src/internal-urls/router.ts +6 -3
- package/src/internal-urls/types.ts +22 -1
- package/src/main.ts +13 -9
- package/src/modes/acp/acp-agent.ts +361 -54
- package/src/modes/acp/acp-client-bridge.ts +152 -0
- package/src/modes/acp/acp-event-mapper.ts +180 -15
- package/src/modes/acp/terminal-auth.ts +37 -0
- package/src/modes/components/read-tool-group.ts +29 -1
- package/src/modes/controllers/command-controller.ts +14 -6
- package/src/modes/controllers/event-controller.ts +24 -11
- package/src/modes/controllers/extension-ui-controller.ts +8 -2
- package/src/modes/controllers/input-controller.ts +72 -39
- package/src/modes/interactive-mode.ts +71 -7
- package/src/modes/rpc/rpc-mode.ts +17 -2
- package/src/modes/types.ts +6 -2
- package/src/modes/utils/ui-helpers.ts +15 -3
- package/src/prompts/agents/designer.md +5 -5
- package/src/prompts/agents/explore.md +7 -7
- package/src/prompts/agents/init.md +9 -9
- package/src/prompts/agents/librarian.md +14 -14
- package/src/prompts/agents/plan.md +4 -4
- package/src/prompts/agents/reviewer.md +5 -5
- package/src/prompts/agents/task.md +10 -10
- package/src/prompts/commands/orchestrate.md +2 -2
- package/src/prompts/compaction/branch-summary.md +3 -3
- package/src/prompts/compaction/compaction-short-summary.md +7 -7
- package/src/prompts/compaction/compaction-summary-context.md +1 -1
- package/src/prompts/compaction/compaction-summary.md +5 -5
- package/src/prompts/compaction/compaction-turn-prefix.md +3 -3
- package/src/prompts/compaction/compaction-update-summary.md +11 -11
- package/src/prompts/memories/consolidation.md +2 -2
- package/src/prompts/memories/read-path.md +1 -1
- package/src/prompts/memories/stage_one_input.md +1 -1
- package/src/prompts/memories/stage_one_system.md +5 -5
- package/src/prompts/review-request.md +4 -4
- package/src/prompts/system/agent-creation-architect.md +17 -17
- package/src/prompts/system/agent-creation-user.md +2 -2
- package/src/prompts/system/commit-message-system.md +2 -2
- package/src/prompts/system/custom-system-prompt.md +2 -2
- package/src/prompts/system/eager-todo.md +6 -6
- package/src/prompts/system/handoff-document.md +1 -1
- package/src/prompts/system/plan-mode-active.md +22 -21
- package/src/prompts/system/plan-mode-approved.md +4 -4
- package/src/prompts/system/plan-mode-compact-instructions.md +16 -0
- package/src/prompts/system/plan-mode-reference.md +2 -2
- package/src/prompts/system/plan-mode-subagent.md +8 -8
- package/src/prompts/system/plan-mode-tool-decision-reminder.md +2 -2
- package/src/prompts/system/project-prompt.md +4 -4
- package/src/prompts/system/subagent-system-prompt.md +7 -7
- package/src/prompts/system/subagent-yield-reminder.md +4 -4
- package/src/prompts/system/system-prompt.md +72 -71
- package/src/prompts/system/ttsr-interrupt.md +1 -1
- package/src/prompts/tools/apply-patch.md +1 -1
- package/src/prompts/tools/ast-edit.md +3 -3
- package/src/prompts/tools/ast-grep.md +3 -3
- package/src/prompts/tools/browser.md +3 -3
- package/src/prompts/tools/checkpoint.md +3 -3
- package/src/prompts/tools/exit-plan-mode.md +2 -2
- package/src/prompts/tools/find.md +3 -3
- package/src/prompts/tools/github.md +2 -5
- package/src/prompts/tools/hashline.md +6 -6
- package/src/prompts/tools/image-gen.md +3 -3
- package/src/prompts/tools/irc.md +1 -1
- package/src/prompts/tools/lsp.md +2 -2
- package/src/prompts/tools/patch.md +6 -6
- package/src/prompts/tools/read.md +7 -7
- package/src/prompts/tools/replace.md +5 -5
- package/src/prompts/tools/retain.md +1 -1
- package/src/prompts/tools/rewind.md +2 -2
- package/src/prompts/tools/search.md +2 -2
- package/src/prompts/tools/ssh.md +2 -2
- package/src/prompts/tools/task.md +12 -6
- package/src/prompts/tools/web-search.md +2 -2
- package/src/prompts/tools/write.md +3 -3
- package/src/sdk.ts +69 -12
- package/src/session/agent-session.ts +231 -22
- package/src/session/client-bridge.ts +81 -0
- package/src/session/compaction/errors.ts +31 -0
- package/src/session/compaction/index.ts +1 -0
- package/src/slash-commands/acp-builtins.ts +46 -0
- package/src/slash-commands/builtin-registry.ts +699 -116
- package/src/slash-commands/helpers/context-report.ts +39 -0
- package/src/slash-commands/helpers/format.ts +23 -0
- package/src/slash-commands/helpers/marketplace-manager.ts +25 -0
- package/src/slash-commands/helpers/mcp.ts +532 -0
- package/src/slash-commands/helpers/parse.ts +85 -0
- package/src/slash-commands/helpers/ssh.ts +193 -0
- package/src/slash-commands/helpers/todo.ts +279 -0
- package/src/slash-commands/helpers/usage-report.ts +91 -0
- package/src/slash-commands/types.ts +126 -0
- package/src/task/executor.ts +10 -3
- package/src/task/index.ts +17 -1
- package/src/task/render.ts +6 -3
- package/src/tools/bash.ts +176 -2
- package/src/tools/conflict-detect.ts +6 -6
- package/src/tools/fetch.ts +15 -4
- package/src/tools/find.ts +19 -1
- package/src/tools/gh-renderer.ts +0 -12
- package/src/tools/gh.ts +682 -176
- package/src/tools/github-cache.ts +548 -0
- package/src/tools/index.ts +3 -0
- package/src/tools/read.ts +110 -27
- package/src/tools/write.ts +23 -1
- package/src/tui/code-cell.ts +70 -2
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { Settings } from "../config/settings";
|
|
2
|
+
import type { InteractiveModeContext } from "../modes/types";
|
|
3
|
+
import type { AgentSession } from "../session/agent-session";
|
|
4
|
+
import type { SessionManager } from "../session/session-manager";
|
|
5
|
+
|
|
6
|
+
/** Declarative subcommand definition for commands like /mcp. */
|
|
7
|
+
export interface SubcommandDef {
|
|
8
|
+
name: string;
|
|
9
|
+
description: string;
|
|
10
|
+
/** Usage hint shown as dim ghost text, e.g. "<name> [--scope project|user]". */
|
|
11
|
+
usage?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Declarative builtin slash command metadata used by autocomplete and help UI. */
|
|
15
|
+
export interface BuiltinSlashCommand {
|
|
16
|
+
name: string;
|
|
17
|
+
description: string;
|
|
18
|
+
/** Subcommands for dropdown completion (e.g. /mcp add, /mcp list). */
|
|
19
|
+
subcommands?: SubcommandDef[];
|
|
20
|
+
/** Static inline hint when command takes a simple argument (no subcommands). */
|
|
21
|
+
inlineHint?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Parsed slash-command text after stripping the leading "/". */
|
|
25
|
+
export interface ParsedSlashCommand {
|
|
26
|
+
name: string;
|
|
27
|
+
args: string;
|
|
28
|
+
text: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Result returned by a slash-command handler.
|
|
33
|
+
*
|
|
34
|
+
* - `void` / `undefined` — command was handled and consumed; no further input.
|
|
35
|
+
* - `{ consumed: true }` — explicit equivalent of the above (ACP shape).
|
|
36
|
+
* - `{ prompt: string }` — command handled, pass `prompt` through as the new
|
|
37
|
+
* user input (e.g. `/force <tool> <prompt>` keeps `<prompt>` as the message).
|
|
38
|
+
*/
|
|
39
|
+
export type SlashCommandResult = undefined | { consumed: true } | { prompt: string };
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Runtime visible to slash-command handlers that run in text/ACP mode.
|
|
43
|
+
*
|
|
44
|
+
* Both the TUI dispatcher (when invoking a `handle` via its adapter) and the
|
|
45
|
+
* ACP dispatcher pass this shape. Implementations MUST NOT depend on TUI-only
|
|
46
|
+
* state (editor, selectors, status line).
|
|
47
|
+
*/
|
|
48
|
+
export interface SlashCommandRuntime {
|
|
49
|
+
session: AgentSession;
|
|
50
|
+
sessionManager: SessionManager;
|
|
51
|
+
settings: Settings;
|
|
52
|
+
cwd: string;
|
|
53
|
+
/** Emit text to the operator. TUI maps to `ctx.showStatus`, ACP to `sessionUpdate`. */
|
|
54
|
+
output: (text: string) => Promise<void> | void;
|
|
55
|
+
/** Re-advertise the available command list (no-op outside ACP). */
|
|
56
|
+
refreshCommands: () => Promise<void> | void;
|
|
57
|
+
/**
|
|
58
|
+
* Reload plugin state (caches, slash command registry, project registries)
|
|
59
|
+
* and re-emit available commands. Used by `/reload-plugins`, `/move`, and
|
|
60
|
+
* `/marketplace`/`/plugins` mutations so the session sees a consistent view
|
|
61
|
+
* after plugin or project-scope changes.
|
|
62
|
+
*/
|
|
63
|
+
reloadPlugins: () => Promise<void>;
|
|
64
|
+
notifyTitleChanged?: () => Promise<void> | void;
|
|
65
|
+
notifyConfigChanged?: () => Promise<void> | void;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Runtime visible to TUI-only handlers (`handleTui`). Carries the interactive
|
|
70
|
+
* mode context plus the background-detach hook. Intentionally narrower than
|
|
71
|
+
* `SlashCommandRuntime` so existing callers can keep building it from just
|
|
72
|
+
* `{ ctx, handleBackgroundCommand }`; when the TUI dispatcher needs to invoke
|
|
73
|
+
* a `handle` (no `handleTui` override), it synthesizes a `SlashCommandRuntime`
|
|
74
|
+
* from `ctx`.
|
|
75
|
+
*/
|
|
76
|
+
export interface TuiSlashCommandRuntime {
|
|
77
|
+
ctx: InteractiveModeContext;
|
|
78
|
+
handleBackgroundCommand: () => void;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Unified slash-command spec consumed by both TUI and ACP dispatchers. */
|
|
82
|
+
export interface SlashCommandSpec extends BuiltinSlashCommand {
|
|
83
|
+
aliases?: string[];
|
|
84
|
+
/** When false, the dispatcher refuses to handle invocations that include arguments. */
|
|
85
|
+
allowArgs?: boolean;
|
|
86
|
+
/**
|
|
87
|
+
* ACP-specific override for `description`. Used by `ACP_BUILTIN_SLASH_COMMANDS`
|
|
88
|
+
* when building `available_commands_update` payloads so the client receives
|
|
89
|
+
* mode-appropriate copy (e.g. `/dump` advertises "Return full transcript as
|
|
90
|
+
* plain text" in ACP rather than the TUI's clipboard-centric copy).
|
|
91
|
+
*/
|
|
92
|
+
acpDescription?: string;
|
|
93
|
+
/**
|
|
94
|
+
* ACP-specific override for the advertised input hint. `subcommands`-only
|
|
95
|
+
* specs that historically advertised `<subcommand>` / `[on|off|status]` /
|
|
96
|
+
* `info|delete` to ACP clients carry the hint here so the unification does
|
|
97
|
+
* not silently drop it from `available_commands_update`.
|
|
98
|
+
*/
|
|
99
|
+
acpInputHint?: string;
|
|
100
|
+
/**
|
|
101
|
+
* Text/ACP-mode handler. The same body is invoked from the ACP dispatcher
|
|
102
|
+
* and, via the TUI adapter, when no `handleTui` override is provided.
|
|
103
|
+
*/
|
|
104
|
+
handle?: (
|
|
105
|
+
command: ParsedSlashCommand,
|
|
106
|
+
runtime: SlashCommandRuntime,
|
|
107
|
+
) => Promise<SlashCommandResult> | SlashCommandResult;
|
|
108
|
+
/**
|
|
109
|
+
* TUI-only handler that supersedes `handle` when both are present. Use for
|
|
110
|
+
* selectors, wizards, dashboards, and anything else that requires
|
|
111
|
+
* `InteractiveModeContext`.
|
|
112
|
+
*/
|
|
113
|
+
handleTui?: (
|
|
114
|
+
command: ParsedSlashCommand,
|
|
115
|
+
runtime: TuiSlashCommandRuntime,
|
|
116
|
+
) => Promise<SlashCommandResult> | SlashCommandResult;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* @deprecated Use `SlashCommandRuntime` directly. Retained as an alias so
|
|
121
|
+
* downstream code that imported the ACP-specific name keeps compiling.
|
|
122
|
+
*/
|
|
123
|
+
export type AcpBuiltinCommandRuntime = SlashCommandRuntime;
|
|
124
|
+
|
|
125
|
+
/** Result returned by `executeAcpBuiltinSlashCommand`. */
|
|
126
|
+
export type AcpBuiltinSlashCommandResult = false | { consumed: true } | { prompt: string };
|
package/src/task/executor.ts
CHANGED
|
@@ -947,10 +947,17 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
947
947
|
|
|
948
948
|
try {
|
|
949
949
|
checkAbort();
|
|
950
|
-
|
|
951
|
-
checkAbort();
|
|
950
|
+
// Pin authStorage to modelRegistry.authStorage — mirrors the createAgentSession invariant.
|
|
952
951
|
const registryFromParent = options.modelRegistry !== undefined;
|
|
953
|
-
const modelRegistry =
|
|
952
|
+
const modelRegistry =
|
|
953
|
+
options.modelRegistry ?? new ModelRegistry(options.authStorage ?? (await discoverAuthStorage()));
|
|
954
|
+
const authStorage = modelRegistry.authStorage;
|
|
955
|
+
if (options.authStorage && options.authStorage !== authStorage) {
|
|
956
|
+
throw new Error(
|
|
957
|
+
"options.authStorage and options.modelRegistry.authStorage must be the same instance when both are provided",
|
|
958
|
+
);
|
|
959
|
+
}
|
|
960
|
+
checkAbort();
|
|
954
961
|
if (!registryFromParent) {
|
|
955
962
|
await modelRegistry.refresh();
|
|
956
963
|
} else {
|
package/src/task/index.ts
CHANGED
|
@@ -483,11 +483,27 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
|
|
|
483
483
|
? ` Failed to schedule ${failedSchedules.length} task${failedSchedules.length === 1 ? "" : "s"}.`
|
|
484
484
|
: "";
|
|
485
485
|
|
|
486
|
+
const ircEnabled = this.session.settings.get("irc.enabled") === true;
|
|
487
|
+
const taskIdByItemId = new Map<string, string>();
|
|
488
|
+
for (let i = 0; i < taskItems.length; i++) {
|
|
489
|
+
taskIdByItemId.set(taskItems[i].id, uniqueIds[i]);
|
|
490
|
+
}
|
|
491
|
+
const startedListing = startedJobs
|
|
492
|
+
.map(({ taskId }) => {
|
|
493
|
+
const id = taskIdByItemId.get(taskId) ?? taskId;
|
|
494
|
+
const desc = progressByTaskId.get(taskId)?.description;
|
|
495
|
+
return desc ? `- \`${id}\` — ${desc}` : `- \`${id}\``;
|
|
496
|
+
})
|
|
497
|
+
.join("\n");
|
|
498
|
+
const coordinationHint = ircEnabled
|
|
499
|
+
? ` DM these ids via \`irc\` to coordinate while they run; reach for \`job\` only to inspect (\`list\`), wait (\`poll\`), or cancel a stuck task.`
|
|
500
|
+
: ` Use \`job\` to inspect (\`list\`), wait (\`poll\`), or cancel a stuck task by id.`;
|
|
501
|
+
|
|
486
502
|
return {
|
|
487
503
|
content: [
|
|
488
504
|
{
|
|
489
505
|
type: "text",
|
|
490
|
-
text: `Started ${startedJobs.length} background task job${startedJobs.length === 1 ? "" : "s"} using ${params.agent}.${scheduleFailureSummary} Results will be delivered when complete
|
|
506
|
+
text: `Started ${startedJobs.length} background task job${startedJobs.length === 1 ? "" : "s"} using ${params.agent}.${scheduleFailureSummary} Results will be delivered when complete.\n${startedListing}\n${coordinationHint}`,
|
|
491
507
|
},
|
|
492
508
|
],
|
|
493
509
|
details: {
|
package/src/task/render.ts
CHANGED
|
@@ -661,8 +661,10 @@ function renderReviewResult(
|
|
|
661
661
|
lines.push(`${continuePrefix} ${theme.fg("dim", replaceTabs(line))}`);
|
|
662
662
|
}
|
|
663
663
|
} else {
|
|
664
|
-
// Preview: first sentence or ~100 chars
|
|
665
|
-
const
|
|
664
|
+
// Preview: first sentence or ~100 chars (flatten tabs/newlines first)
|
|
665
|
+
const flat = replaceTabs(summary.explanation).replace(/[\r\n]+/g, " ");
|
|
666
|
+
const firstSentence = flat.split(/[.!?]/)[0].trim();
|
|
667
|
+
const preview = truncateToWidth(`${firstSentence}.`, 100);
|
|
666
668
|
lines.push(`${continuePrefix}${theme.fg("dim", preview)}`);
|
|
667
669
|
}
|
|
668
670
|
}
|
|
@@ -701,7 +703,8 @@ function renderFindings(
|
|
|
701
703
|
const findingContinue = isLastFinding ? " " : `${theme.tree.vertical} `;
|
|
702
704
|
|
|
703
705
|
const { color } = getPriorityInfo(finding.priority);
|
|
704
|
-
const
|
|
706
|
+
const rawTitle = finding.title?.replace(/^\[P\d\]\s*/, "") ?? "Untitled";
|
|
707
|
+
const titleText = replaceTabs(rawTitle).replace(/[\r\n]+/g, " ");
|
|
705
708
|
const loc = `${path.basename(finding.file_path || "<unknown>")}:${finding.line_start}`;
|
|
706
709
|
|
|
707
710
|
lines.push(
|
package/src/tools/bash.ts
CHANGED
|
@@ -2,7 +2,7 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
3
3
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
4
4
|
import { ImageProtocol, TERMINAL, Text } from "@oh-my-pi/pi-tui";
|
|
5
|
-
import { $env, getProjectDir, isEnoent, prompt } from "@oh-my-pi/pi-utils";
|
|
5
|
+
import { $env, getProjectDir, isEnoent, logger, prompt } from "@oh-my-pi/pi-utils";
|
|
6
6
|
import { Type } from "@sinclair/typebox";
|
|
7
7
|
import { AsyncJobManager } from "../async";
|
|
8
8
|
import { type BashResult, executeBash } from "../exec/bash-executor";
|
|
@@ -11,6 +11,7 @@ import { InternalUrlRouter } from "../internal-urls";
|
|
|
11
11
|
import { truncateToVisualLines } from "../modes/components/visual-truncate";
|
|
12
12
|
import type { Theme } from "../modes/theme/theme";
|
|
13
13
|
import bashDescription from "../prompts/tools/bash.md" with { type: "text" };
|
|
14
|
+
import type { ClientBridgeTerminalExitStatus, ClientBridgeTerminalOutput } from "../session/client-bridge";
|
|
14
15
|
import { DEFAULT_MAX_BYTES, streamTailUpdates, TailBuffer } from "../session/streaming-output";
|
|
15
16
|
import { renderStatusLine } from "../tui";
|
|
16
17
|
import { CachedOutputBlock } from "../tui/output-block";
|
|
@@ -84,6 +85,7 @@ export interface BashToolDetails {
|
|
|
84
85
|
meta?: OutputMeta;
|
|
85
86
|
timeoutSeconds?: number;
|
|
86
87
|
requestedTimeoutSeconds?: number;
|
|
88
|
+
terminalId?: string;
|
|
87
89
|
async?: {
|
|
88
90
|
state: "running" | "completed" | "failed";
|
|
89
91
|
jobId: string;
|
|
@@ -289,7 +291,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
289
291
|
#buildCompletedResult(
|
|
290
292
|
result: BashResult | BashInteractiveResult,
|
|
291
293
|
timeoutSec: number,
|
|
292
|
-
options: { requestedTimeoutSec?: number; notices?: string[] } = {},
|
|
294
|
+
options: { requestedTimeoutSec?: number; notices?: string[]; terminalId?: string } = {},
|
|
293
295
|
): AgentToolResult<BashToolDetails> {
|
|
294
296
|
const outputLines = [this.#formatResultOutput(result)];
|
|
295
297
|
const notices = options.notices?.filter(Boolean) ?? [];
|
|
@@ -299,6 +301,9 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
299
301
|
if (options.requestedTimeoutSec !== undefined && options.requestedTimeoutSec !== timeoutSec) {
|
|
300
302
|
details.requestedTimeoutSeconds = options.requestedTimeoutSec;
|
|
301
303
|
}
|
|
304
|
+
if (options.terminalId !== undefined) {
|
|
305
|
+
details.terminalId = options.terminalId;
|
|
306
|
+
}
|
|
302
307
|
const resultBuilder = toolResult(details).text(outputText).truncationFromSummary(result, { direction: "tail" });
|
|
303
308
|
this.#buildResultText(result, timeoutSec, outputText);
|
|
304
309
|
return resultBuilder.done();
|
|
@@ -618,6 +623,175 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
618
623
|
});
|
|
619
624
|
}
|
|
620
625
|
|
|
626
|
+
// Route through the client terminal when the client advertises the terminal capability.
|
|
627
|
+
// Skip when pty=true (PTY needs the local terminal UI).
|
|
628
|
+
const clientBridge = this.session.getClientBridge?.();
|
|
629
|
+
if (clientBridge?.capabilities.terminal && clientBridge.createTerminal && !pty) {
|
|
630
|
+
const handle = await clientBridge.createTerminal({
|
|
631
|
+
command,
|
|
632
|
+
cwd: commandCwd,
|
|
633
|
+
env: resolvedEnv
|
|
634
|
+
? Object.entries(resolvedEnv).map(([name, value]) => ({ name, value: value as string }))
|
|
635
|
+
: undefined,
|
|
636
|
+
outputByteLimit: DEFAULT_MAX_BYTES,
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
// Emit partial update so the editor can embed the live terminal card.
|
|
640
|
+
onUpdate?.({ content: [], details: { terminalId: handle.terminalId } });
|
|
641
|
+
|
|
642
|
+
const exitPromise = handle.waitForExit();
|
|
643
|
+
let exitStatus!: ClientBridgeTerminalExitStatus;
|
|
644
|
+
|
|
645
|
+
type BridgeRaceResult =
|
|
646
|
+
| { kind: "exit"; status: ClientBridgeTerminalExitStatus }
|
|
647
|
+
| { kind: "poll" }
|
|
648
|
+
| { kind: "timeout" }
|
|
649
|
+
| { kind: "aborted" };
|
|
650
|
+
|
|
651
|
+
// Set up abort listener before entering the poll loop. The listener
|
|
652
|
+
// kicks off `handle.kill()` synchronously so a `session/cancel`
|
|
653
|
+
// arriving mid-poll terminates the remote command immediately,
|
|
654
|
+
// instead of waiting for the next `currentOutput()` to return.
|
|
655
|
+
const { promise: abortedP, resolve: resolveAborted } = Promise.withResolvers<void>();
|
|
656
|
+
let killStarted = false;
|
|
657
|
+
const fireKill = (): Promise<void> => {
|
|
658
|
+
if (killStarted) return Promise.resolve();
|
|
659
|
+
killStarted = true;
|
|
660
|
+
return handle.kill().catch((error: unknown) => {
|
|
661
|
+
logger.warn("ACP terminal kill failed", { terminalId: handle.terminalId, error });
|
|
662
|
+
});
|
|
663
|
+
};
|
|
664
|
+
const onAbortSignal = () => {
|
|
665
|
+
resolveAborted();
|
|
666
|
+
void fireKill();
|
|
667
|
+
};
|
|
668
|
+
signal?.addEventListener("abort", onAbortSignal, { once: true });
|
|
669
|
+
|
|
670
|
+
try {
|
|
671
|
+
try {
|
|
672
|
+
if (signal?.aborted) {
|
|
673
|
+
await fireKill();
|
|
674
|
+
throw new ToolAbortError("Command aborted");
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
const timeoutPromise = Bun.sleep(timeoutMs).then(() => ({ kind: "timeout" as const }));
|
|
678
|
+
// Poll until the process exits, times out, or the caller aborts.
|
|
679
|
+
for (;;) {
|
|
680
|
+
const racers: Array<Promise<BridgeRaceResult>> = [
|
|
681
|
+
exitPromise.then(s => ({ kind: "exit" as const, status: s })),
|
|
682
|
+
timeoutPromise,
|
|
683
|
+
Bun.sleep(250).then(() => ({ kind: "poll" as const })),
|
|
684
|
+
];
|
|
685
|
+
if (signal) {
|
|
686
|
+
racers.push(abortedP.then(() => ({ kind: "aborted" as const })));
|
|
687
|
+
}
|
|
688
|
+
const raced = await Promise.race(racers);
|
|
689
|
+
|
|
690
|
+
if (raced.kind === "aborted" || signal?.aborted) {
|
|
691
|
+
await fireKill();
|
|
692
|
+
throw new ToolAbortError("Command aborted");
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
if (raced.kind === "timeout") {
|
|
696
|
+
// Kill before reading final output so a slow `terminal/output`
|
|
697
|
+
// RPC cannot let a timed-out command keep running past the
|
|
698
|
+
// enforced timeout. The handle stays valid post-kill so the
|
|
699
|
+
// buffered output is still readable.
|
|
700
|
+
await fireKill();
|
|
701
|
+
let current = { output: "", truncated: false };
|
|
702
|
+
try {
|
|
703
|
+
current = await handle.currentOutput();
|
|
704
|
+
} catch (error) {
|
|
705
|
+
logger.warn("ACP terminal final output read failed", {
|
|
706
|
+
terminalId: handle.terminalId,
|
|
707
|
+
error,
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
const timedOutResult: BashInteractiveResult = {
|
|
711
|
+
output: current.output,
|
|
712
|
+
exitCode: undefined,
|
|
713
|
+
cancelled: false,
|
|
714
|
+
timedOut: true,
|
|
715
|
+
truncated: current.truncated,
|
|
716
|
+
totalLines: current.output.length > 0 ? current.output.split("\n").length : 0,
|
|
717
|
+
totalBytes: current.output.length,
|
|
718
|
+
outputLines: current.output.length > 0 ? current.output.split("\n").length : 0,
|
|
719
|
+
outputBytes: current.output.length,
|
|
720
|
+
};
|
|
721
|
+
return this.#buildCompletedResult(timedOutResult, timeoutSec, {
|
|
722
|
+
requestedTimeoutSec,
|
|
723
|
+
notices: [timeoutClampNotice].filter((notice): notice is string => Boolean(notice)),
|
|
724
|
+
terminalId: handle.terminalId,
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
if (raced.kind === "exit") {
|
|
729
|
+
exitStatus = raced.status;
|
|
730
|
+
break;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// Poll tick: push current output so agent-loop transcript stays consistent.
|
|
734
|
+
// Race the read against abort so a stuck `terminal/output` RPC does not
|
|
735
|
+
// delay cancellation.
|
|
736
|
+
const pollOutput = await Promise.race([
|
|
737
|
+
handle.currentOutput(),
|
|
738
|
+
abortedP.then(() => undefined as ClientBridgeTerminalOutput | undefined),
|
|
739
|
+
]);
|
|
740
|
+
if (pollOutput === undefined) {
|
|
741
|
+
// Abort fired during the poll-tick read; let the next loop iteration
|
|
742
|
+
// observe `signal?.aborted` and exit via the abort branch.
|
|
743
|
+
continue;
|
|
744
|
+
}
|
|
745
|
+
onUpdate?.({
|
|
746
|
+
content: [{ type: "text", text: pollOutput.output }],
|
|
747
|
+
details: { terminalId: handle.terminalId },
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
} finally {
|
|
751
|
+
signal?.removeEventListener("abort", onAbortSignal);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// Fetch final output; the terminal is released in the outer finally.
|
|
755
|
+
const finalOutput = await handle.currentOutput();
|
|
756
|
+
|
|
757
|
+
// Map exit status: null exitCode with a signal → treat as signal kill (137).
|
|
758
|
+
const rawExitCode = exitStatus.exitCode;
|
|
759
|
+
const exitCode: number | undefined =
|
|
760
|
+
rawExitCode != null ? rawExitCode : exitStatus.signal ? 137 : undefined;
|
|
761
|
+
|
|
762
|
+
const outputText = finalOutput.output;
|
|
763
|
+
const outputByteLen = outputText.length;
|
|
764
|
+
const outputLineCount = outputText.length > 0 ? outputText.split("\n").length : 0;
|
|
765
|
+
|
|
766
|
+
const bridgeResult: BashResult = {
|
|
767
|
+
output: outputText,
|
|
768
|
+
exitCode,
|
|
769
|
+
cancelled: false,
|
|
770
|
+
truncated: finalOutput.truncated,
|
|
771
|
+
totalLines: outputLineCount,
|
|
772
|
+
totalBytes: outputByteLen,
|
|
773
|
+
outputLines: outputLineCount,
|
|
774
|
+
outputBytes: outputByteLen,
|
|
775
|
+
};
|
|
776
|
+
|
|
777
|
+
const bridgeNotices: string[] = [];
|
|
778
|
+
if (finalOutput.truncated) bridgeNotices.push("(output truncated)");
|
|
779
|
+
if (timeoutClampNotice) bridgeNotices.push(timeoutClampNotice);
|
|
780
|
+
|
|
781
|
+
return this.#buildCompletedResult(bridgeResult, timeoutSec, {
|
|
782
|
+
requestedTimeoutSec,
|
|
783
|
+
notices: bridgeNotices,
|
|
784
|
+
terminalId: handle.terminalId,
|
|
785
|
+
});
|
|
786
|
+
} finally {
|
|
787
|
+
try {
|
|
788
|
+
await handle.release();
|
|
789
|
+
} catch (error) {
|
|
790
|
+
logger.warn("ACP terminal release failed", { terminalId: handle.terminalId, error });
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
621
795
|
// Track output for streaming updates (tail only)
|
|
622
796
|
const tailBuffer = new TailBuffer(DEFAULT_MAX_BYTES);
|
|
623
797
|
|
|
@@ -240,7 +240,7 @@ export function getConflictHistory(session: ToolSession): ConflictHistory {
|
|
|
240
240
|
return session.conflictHistory;
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
-
/** A side of a conflict block that `read conflict://N/<scope
|
|
243
|
+
/** A side of a conflict block that the `read` tool can render via `conflict://N/<scope>`. */
|
|
244
244
|
export type ConflictScope = "ours" | "theirs" | "base";
|
|
245
245
|
|
|
246
246
|
const CONFLICT_SCOPES = new Set<ConflictScope>(["ours", "theirs", "base"]);
|
|
@@ -440,7 +440,7 @@ function markerLine(prefix: string, label: string | undefined): string {
|
|
|
440
440
|
}
|
|
441
441
|
|
|
442
442
|
/**
|
|
443
|
-
* Materialise a conflict block for `
|
|
443
|
+
* Materialise a conflict block for `conflict://<N>` reads (and their
|
|
444
444
|
* `/ours` / `/theirs` / `/base` scopes).
|
|
445
445
|
*
|
|
446
446
|
* Returns:
|
|
@@ -534,7 +534,7 @@ export function formatConflictWarning(
|
|
|
534
534
|
if (partial) {
|
|
535
535
|
const hintPath = options.displayPath ?? "<file>";
|
|
536
536
|
out.push(
|
|
537
|
-
`⚠ ${entries.length} of ${total} unresolved ${word} visible in this window (
|
|
537
|
+
`⚠ ${entries.length} of ${total} unresolved ${word} visible in this window (read \`${hintPath}:conflicts\` for the full list).`,
|
|
538
538
|
);
|
|
539
539
|
} else {
|
|
540
540
|
out.push(`⚠ ${total} unresolved ${word} detected`);
|
|
@@ -551,7 +551,7 @@ export function formatConflictWarning(
|
|
|
551
551
|
if (theirsLabel) out.push(`- theirs = ${theirsLabel}`);
|
|
552
552
|
if (anyBase) out.push(`- base = ${baseLabel ?? "(no label)"}`);
|
|
553
553
|
out.push(
|
|
554
|
-
'NOTICE: Inspect a block
|
|
554
|
+
'NOTICE: Inspect a block by reading `conflict://<N>` (add `/ours` / `/theirs` / `/base` to render a single side). Resolve with `write({ path: "conflict://<N>", content })`, or bulk-resolve every registered conflict with `write({ path: "conflict://*", content })`. Writes replace the whole conflict region (markers + all sides).',
|
|
555
555
|
);
|
|
556
556
|
out.push(
|
|
557
557
|
'`content` shorthand: a line that is exactly `@ours` / `@theirs` / `@base` / `@both` expands to that recorded section. `@both` is ours-then-theirs with no separator. Lines that are not a token pass through verbatim, so `"// keep both\\n@ours\\n@theirs"` literally writes the comment, then ours, then theirs.',
|
|
@@ -592,7 +592,7 @@ export function formatConflictWarning(
|
|
|
592
592
|
|
|
593
593
|
/**
|
|
594
594
|
* Render a single-line-per-block index of every conflict in a file.
|
|
595
|
-
* Used by
|
|
595
|
+
* Used by the `<path>:conflicts` read selector to give the agent a cheap overview
|
|
596
596
|
* of a heavily-conflicted file without dumping every body.
|
|
597
597
|
*/
|
|
598
598
|
export function formatConflictSummary(
|
|
@@ -614,7 +614,7 @@ export function formatConflictSummary(
|
|
|
614
614
|
if (theirsLabel) lines.push(`- theirs = ${theirsLabel}`);
|
|
615
615
|
if (anyBase) lines.push(`- base = ${baseLabel ?? "(no label)"}`);
|
|
616
616
|
lines.push(
|
|
617
|
-
'NOTICE: Bulk-resolve with `write({ path: "conflict://*", content })`, or address a single block with `write({ path: "conflict://<N>", content })`. Inspect a block
|
|
617
|
+
'NOTICE: Bulk-resolve with `write({ path: "conflict://*", content })`, or address a single block with `write({ path: "conflict://<N>", content })`. Inspect a block by reading `conflict://<N>` (add `/ours` / `/theirs` / `/base` for a single side).',
|
|
618
618
|
);
|
|
619
619
|
lines.push(
|
|
620
620
|
"`content` shorthand: `@ours` / `@theirs` / `@base` / `@both` lines expand to the recorded sections; `@both` = ours-then-theirs. Non-token lines pass through verbatim.",
|
package/src/tools/fetch.ts
CHANGED
|
@@ -22,7 +22,7 @@ import { finalizeOutput, loadPage, looksLikeHtml, MAX_OUTPUT_CHARS } from "../we
|
|
|
22
22
|
import { convertWithMarkit, fetchBinary } from "../web/scrapers/utils";
|
|
23
23
|
import { applyListLimit } from "./list-limit";
|
|
24
24
|
import { formatStyledArtifactReference, type OutputMeta } from "./output-meta";
|
|
25
|
-
import { formatExpandHint, getDomain } from "./render-utils";
|
|
25
|
+
import { formatExpandHint, getDomain, replaceTabs } from "./render-utils";
|
|
26
26
|
import { ToolAbortError, ToolError } from "./tool-errors";
|
|
27
27
|
import { toolResult } from "./tool-result";
|
|
28
28
|
import { clampTimeout } from "./tool-timeouts";
|
|
@@ -1362,14 +1362,25 @@ export function renderReadUrlCall(
|
|
|
1362
1362
|
|
|
1363
1363
|
/** Render URL read result with tree-based layout */
|
|
1364
1364
|
export function renderReadUrlResult(
|
|
1365
|
-
result: { content: Array<{ type: string; text?: string }>; details?: ReadUrlToolDetails },
|
|
1365
|
+
result: { content: Array<{ type: string; text?: string }>; details?: ReadUrlToolDetails; isError?: boolean },
|
|
1366
1366
|
options: RenderResultOptions,
|
|
1367
1367
|
uiTheme: Theme = theme,
|
|
1368
1368
|
): Component {
|
|
1369
1369
|
const details = result.details;
|
|
1370
1370
|
|
|
1371
|
-
if (!details) {
|
|
1372
|
-
|
|
1371
|
+
if (result.isError || !details) {
|
|
1372
|
+
const rawErrorText = result.content?.find(c => c.type === "text")?.text ?? "";
|
|
1373
|
+
const errorText = (rawErrorText || "No response data").replace(/^Error:\s*/, "");
|
|
1374
|
+
const urlText = details?.finalUrl ?? details?.url ?? "";
|
|
1375
|
+
const description = urlText ? `${getDomain(urlText)}${urlText.replace(/^https?:\/\/[^/]+/, "")}` : undefined;
|
|
1376
|
+
const header = renderStatusLine({ icon: "error", title: "Read", description }, uiTheme);
|
|
1377
|
+
const errorLines = errorText.split("\n").map(line => uiTheme.fg("error", replaceTabs(line)));
|
|
1378
|
+
const outputBlock = new CachedOutputBlock();
|
|
1379
|
+
return {
|
|
1380
|
+
render: (width: number) =>
|
|
1381
|
+
outputBlock.render({ header, state: "error", sections: [{ lines: errorLines }], width }, uiTheme),
|
|
1382
|
+
invalidate: () => outputBlock.invalidate(),
|
|
1383
|
+
};
|
|
1373
1384
|
}
|
|
1374
1385
|
|
|
1375
1386
|
const domain = getDomain(details.finalUrl);
|
package/src/tools/find.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { isEnoent, prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
|
8
8
|
import type { Static } from "@sinclair/typebox";
|
|
9
9
|
import { Type } from "@sinclair/typebox";
|
|
10
10
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
11
|
+
import { InternalUrlRouter } from "../internal-urls";
|
|
11
12
|
import type { Theme } from "../modes/theme/theme";
|
|
12
13
|
import findDescription from "../prompts/tools/find.md" with { type: "text" };
|
|
13
14
|
import { type TruncationResult, truncateHead } from "../session/streaming-output";
|
|
@@ -25,6 +26,7 @@ import { applyListLimit } from "./list-limit";
|
|
|
25
26
|
import { formatFullOutputReference, type OutputMeta } from "./output-meta";
|
|
26
27
|
import {
|
|
27
28
|
formatPathRelativeToCwd,
|
|
29
|
+
hasGlobPathChars,
|
|
28
30
|
normalizePathLikeInput,
|
|
29
31
|
parseFindPattern,
|
|
30
32
|
partitionExistingPaths,
|
|
@@ -116,7 +118,23 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
116
118
|
|
|
117
119
|
return untilAborted(signal, async () => {
|
|
118
120
|
const formatScopePath = (targetPath: string): string => formatPathRelativeToCwd(targetPath, this.session.cwd);
|
|
119
|
-
const
|
|
121
|
+
const rawPatterns = paths.map(input => normalizePathLikeInput(input).replace(/\\/g, "/"));
|
|
122
|
+
const internalRouter = InternalUrlRouter.instance();
|
|
123
|
+
const normalizedPatterns: string[] = [];
|
|
124
|
+
for (const rawPattern of rawPatterns) {
|
|
125
|
+
if (!internalRouter.canHandle(rawPattern)) {
|
|
126
|
+
normalizedPatterns.push(rawPattern);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (hasGlobPathChars(rawPattern)) {
|
|
130
|
+
throw new ToolError(`Glob patterns are not supported for internal URLs: ${rawPattern}`);
|
|
131
|
+
}
|
|
132
|
+
const resource = await internalRouter.resolve(rawPattern);
|
|
133
|
+
if (!resource.sourcePath) {
|
|
134
|
+
throw new ToolError(`Cannot find internal URL without a backing file: ${rawPattern}`);
|
|
135
|
+
}
|
|
136
|
+
normalizedPatterns.push(resource.sourcePath);
|
|
137
|
+
}
|
|
120
138
|
if (normalizedPatterns.some(pattern => pattern.length === 0)) {
|
|
121
139
|
throw new ToolError("`paths` must contain non-empty globs or paths");
|
|
122
140
|
}
|
package/src/tools/gh-renderer.ts
CHANGED
|
@@ -27,7 +27,6 @@ type GithubToolRenderArgs = {
|
|
|
27
27
|
run?: string;
|
|
28
28
|
branch?: string;
|
|
29
29
|
repo?: string;
|
|
30
|
-
issue?: string;
|
|
31
30
|
pr?: string | string[];
|
|
32
31
|
query?: string;
|
|
33
32
|
};
|
|
@@ -40,9 +39,6 @@ const FALLBACK_WIDTH = 80;
|
|
|
40
39
|
|
|
41
40
|
const OP_TITLES: Record<string, string> = {
|
|
42
41
|
repo_view: "GitHub Repo",
|
|
43
|
-
issue_view: "GitHub Issue",
|
|
44
|
-
pr_view: "GitHub PR",
|
|
45
|
-
pr_diff: "GitHub PR Diff",
|
|
46
42
|
pr_checkout: "GitHub PR Checkout",
|
|
47
43
|
pr_push: "GitHub PR Push",
|
|
48
44
|
search_issues: "GitHub Search Issues",
|
|
@@ -85,14 +81,6 @@ function buildOpMeta(args: GithubToolRenderArgs): string[] {
|
|
|
85
81
|
const meta: string[] = [];
|
|
86
82
|
const op = args.op;
|
|
87
83
|
switch (op) {
|
|
88
|
-
case "issue_view": {
|
|
89
|
-
const id = extractIssueId(args.issue);
|
|
90
|
-
if (id) meta.push(id);
|
|
91
|
-
if (args.repo) meta.push(args.repo);
|
|
92
|
-
break;
|
|
93
|
-
}
|
|
94
|
-
case "pr_view":
|
|
95
|
-
case "pr_diff":
|
|
96
84
|
case "pr_checkout":
|
|
97
85
|
case "pr_push": {
|
|
98
86
|
const id = formatPrIdentifier(args.pr);
|