@oh-my-pi/pi-coding-agent 16.1.22 → 16.2.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 +99 -1
- package/dist/cli.js +4387 -4164
- package/dist/types/advisor/__tests__/emission-guard.test.d.ts +1 -0
- package/dist/types/advisor/emission-guard.d.ts +73 -0
- package/dist/types/advisor/index.d.ts +1 -0
- package/dist/types/advisor/runtime.d.ts +7 -0
- package/dist/types/advisor/watchdog.d.ts +2 -0
- package/dist/types/cli/flag-tables.d.ts +1 -0
- package/dist/types/cli/gallery-fixtures/search.d.ts +1 -1
- package/dist/types/cli/gc-cli.d.ts +58 -0
- package/dist/types/collab/display-name.d.ts +3 -0
- package/dist/types/collab/host.d.ts +0 -2
- package/dist/types/commands/gc.d.ts +37 -0
- package/dist/types/commands/worktree.d.ts +0 -3
- package/dist/types/config/inline-tool-descriptors-mode.d.ts +13 -0
- package/dist/types/config/model-discovery.d.ts +6 -1
- package/dist/types/config/model-registry.d.ts +6 -2
- package/dist/types/config/model-resolver.d.ts +12 -0
- package/dist/types/config/models-config-schema.d.ts +29 -2
- package/dist/types/config/models-config.d.ts +22 -1
- package/dist/types/config/settings-schema.d.ts +145 -21
- package/dist/types/config/settings.d.ts +13 -0
- package/dist/types/dap/config.d.ts +1 -1
- package/dist/types/edit/hashline/filesystem.d.ts +4 -2
- package/dist/types/edit/renderer.d.ts +4 -0
- package/dist/types/extensibility/extensions/types.d.ts +17 -17
- package/dist/types/extensibility/hooks/types.d.ts +10 -10
- package/dist/types/extensibility/plugins/marketplace/types.d.ts +1 -0
- package/dist/types/internal-urls/__tests__/ssh-protocol.test.d.ts +1 -0
- package/dist/types/internal-urls/index.d.ts +1 -0
- package/dist/types/internal-urls/registry-helpers.d.ts +2 -0
- package/dist/types/internal-urls/router.d.ts +1 -1
- package/dist/types/internal-urls/ssh-protocol.d.ts +10 -0
- package/dist/types/internal-urls/types.d.ts +19 -2
- package/dist/types/irc/bus.d.ts +6 -0
- package/dist/types/mcp/transports/stdio.d.ts +25 -1
- package/dist/types/modes/components/__tests__/move-overlay.test.d.ts +1 -0
- package/dist/types/modes/components/custom-editor.d.ts +7 -0
- package/dist/types/modes/components/move-overlay.d.ts +23 -0
- package/dist/types/modes/components/plugin-selector.d.ts +2 -1
- package/dist/types/modes/components/queue-mode-selector.d.ts +2 -1
- package/dist/types/modes/components/select-list-mouse-routing.d.ts +2 -0
- package/dist/types/modes/components/settings-defs.d.ts +4 -1
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/modes/components/show-images-selector.d.ts +2 -1
- package/dist/types/modes/components/status-line/component.d.ts +2 -0
- package/dist/types/modes/components/status-line/types.d.ts +2 -0
- package/dist/types/modes/components/theme-selector.d.ts +2 -1
- package/dist/types/modes/components/thinking-selector.d.ts +2 -1
- package/dist/types/modes/controllers/command-controller.d.ts +10 -1
- package/dist/types/modes/controllers/streaming-reveal.d.ts +6 -0
- package/dist/types/modes/interactive-mode.d.ts +10 -1
- package/dist/types/modes/internal-url-autocomplete.d.ts +1 -1
- package/dist/types/modes/running-subagent-badge.d.ts +6 -0
- package/dist/types/modes/theme/mermaid-rendering.test.d.ts +1 -0
- package/dist/types/modes/theme/theme.d.ts +2 -1
- package/dist/types/modes/types.d.ts +9 -1
- package/dist/types/sdk.d.ts +2 -2
- package/dist/types/session/agent-session.d.ts +2 -1
- package/dist/types/session/session-listing.d.ts +10 -1
- package/dist/types/session/session-manager.d.ts +13 -0
- package/dist/types/slash-commands/builtin-registry.d.ts +1 -1
- package/dist/types/ssh/__tests__/connection-manager-args.test.d.ts +1 -0
- package/dist/types/ssh/__tests__/file-transfer-posix-guard.test.d.ts +1 -0
- package/dist/types/ssh/connection-manager.d.ts +2 -0
- package/dist/types/ssh/file-transfer.d.ts +79 -0
- package/dist/types/ssh/utils.d.ts +6 -0
- package/dist/types/system-prompt.d.ts +5 -0
- package/dist/types/task/executor.d.ts +16 -0
- package/dist/types/tiny/text.d.ts +1 -1
- package/dist/types/tools/builtin-names.d.ts +5 -1
- package/dist/types/tools/{find.d.ts → glob.d.ts} +15 -15
- package/dist/types/tools/{search.d.ts → grep.d.ts} +14 -14
- package/dist/types/tools/index.d.ts +3 -3
- package/dist/types/tools/path-utils.d.ts +29 -0
- package/dist/types/tools/plan-mode-guard.d.ts +7 -0
- package/dist/types/tools/read.d.ts +2 -2
- package/dist/types/tools/render-utils.d.ts +8 -0
- package/dist/types/tools/renderers.d.ts +11 -0
- package/dist/types/tools/ssh.d.ts +2 -0
- package/dist/types/tools/todo.d.ts +0 -16
- package/dist/types/tools/write.d.ts +2 -2
- package/dist/types/utils/active-repo-context.d.ts +8 -0
- package/dist/types/utils/image-resize.d.ts +1 -0
- package/dist/types/utils/markit-cache.d.ts +23 -0
- package/dist/types/utils/markit.d.ts +5 -1
- package/dist/types/utils/prompt-path.d.ts +1 -0
- package/dist/types/web/search/providers/duckduckgo.d.ts +14 -0
- package/dist/types/web/search/providers/firecrawl.d.ts +28 -0
- package/dist/types/web/search/providers/tinyfish.d.ts +29 -0
- package/dist/types/web/search/providers/xai.d.ts +13 -0
- package/dist/types/web/search/types.d.ts +18 -2
- package/package.json +30 -15
- package/scripts/bench-guard.ts +1 -1
- package/scripts/build-binary.ts +9 -9
- package/scripts/bundle-dist.ts +2 -2
- package/src/advisor/__tests__/advisor.test.ts +40 -4
- package/src/advisor/__tests__/emission-guard.test.ts +147 -0
- package/src/advisor/advise-tool.ts +1 -1
- package/src/advisor/emission-guard.ts +172 -0
- package/src/advisor/index.ts +1 -0
- package/src/advisor/runtime.ts +11 -0
- package/src/advisor/watchdog.ts +12 -1
- package/src/cli/args.ts +5 -2
- package/src/cli/auth-broker-cli.ts +17 -0
- package/src/cli/config-cli.ts +4 -0
- package/src/cli/flag-tables.ts +7 -4
- package/src/cli/gallery-cli.ts +14 -2
- package/src/cli/gallery-fixtures/edit.ts +60 -0
- package/src/cli/gallery-fixtures/fs.ts +17 -17
- package/src/cli/gallery-fixtures/search.ts +4 -4
- package/src/cli/gc-cli.ts +939 -0
- package/src/cli/usage-cli.ts +20 -1
- package/src/cli/web-search-cli.ts +1 -1
- package/src/cli-commands.ts +1 -0
- package/src/collab/display-name.ts +13 -0
- package/src/collab/guest.ts +5 -1
- package/src/collab/host.ts +2 -13
- package/src/commands/gc.ts +46 -0
- package/src/commands/worktree.ts +6 -0
- package/src/config/inline-tool-descriptors-mode.ts +27 -0
- package/src/config/model-discovery.ts +77 -8
- package/src/config/model-registry.ts +89 -11
- package/src/config/model-resolver.ts +39 -1
- package/src/config/models-config-schema.ts +41 -5
- package/src/config/models-config.ts +4 -1
- package/src/config/settings-schema.ts +130 -27
- package/src/config/settings.ts +216 -7
- package/src/cursor.ts +1 -1
- package/src/dap/config.ts +152 -8
- package/src/dap/session.ts +1 -1
- package/src/discovery/helpers.ts +3 -1
- package/src/edit/hashline/diff.ts +14 -3
- package/src/edit/hashline/execute.ts +42 -6
- package/src/edit/hashline/filesystem.ts +49 -8
- package/src/edit/index.ts +1 -0
- package/src/edit/modes/patch.ts +11 -1
- package/src/edit/renderer.ts +140 -13
- package/src/eval/__tests__/agent-bridge.test.ts +101 -0
- package/src/eval/__tests__/julia-prelude.test.ts +18 -0
- package/src/eval/agent-bridge.ts +26 -3
- package/src/eval/jl/runner.jl +7 -1
- package/src/eval/js/tool-bridge.ts +2 -2
- package/src/export/html/index.ts +1 -1
- package/src/export/html/template.js +3 -1
- package/src/export/html/tool-views.generated.js +20 -20
- package/src/extensibility/extensions/types.ts +22 -22
- package/src/extensibility/hooks/types.ts +11 -11
- package/src/extensibility/plugins/legacy-pi-bundled-keys.ts +10 -3
- package/src/extensibility/plugins/legacy-pi-bundled-registry.ts +31 -10
- package/src/extensibility/plugins/marketplace/manager.ts +22 -0
- package/src/extensibility/plugins/marketplace/types.ts +1 -0
- package/src/internal-urls/__tests__/ssh-protocol.test.ts +331 -0
- package/src/internal-urls/docs-index.generated.txt +2 -2
- package/src/internal-urls/docs-index.ts +2 -3
- package/src/internal-urls/index.ts +1 -0
- package/src/internal-urls/registry-helpers.ts +19 -4
- package/src/internal-urls/router.ts +5 -3
- package/src/internal-urls/ssh-protocol.ts +367 -0
- package/src/internal-urls/types.ts +19 -2
- package/src/irc/bus.ts +11 -3
- package/src/lsp/index.ts +8 -1
- package/src/mcp/oauth-discovery.ts +20 -20
- package/src/mcp/tool-bridge.ts +32 -2
- package/src/mcp/transports/stdio.test.ts +20 -3
- package/src/mcp/transports/stdio.ts +45 -14
- package/src/memories/index.ts +1 -1
- package/src/modes/acp/acp-event-mapper.ts +2 -2
- package/src/modes/components/__tests__/move-overlay.test.ts +166 -0
- package/src/modes/components/agent-hub.ts +3 -0
- package/src/modes/components/agent-transcript-viewer.ts +9 -7
- package/src/modes/components/custom-editor.ts +90 -5
- package/src/modes/components/move-overlay.ts +282 -0
- package/src/modes/components/plan-review-overlay.ts +35 -35
- package/src/modes/components/plugin-selector.ts +6 -1
- package/src/modes/components/queue-mode-selector.ts +6 -1
- package/src/modes/components/select-list-mouse-routing.ts +35 -0
- package/src/modes/components/session-selector.ts +11 -10
- package/src/modes/components/settings-defs.ts +14 -1
- package/src/modes/components/settings-selector.ts +196 -29
- package/src/modes/components/show-images-selector.ts +6 -1
- package/src/modes/components/status-line/component.ts +108 -28
- package/src/modes/components/status-line/segments.ts +5 -1
- package/src/modes/components/status-line/types.ts +2 -0
- package/src/modes/components/theme-selector.ts +6 -1
- package/src/modes/components/thinking-selector.ts +6 -1
- package/src/modes/components/tool-execution.ts +25 -9
- package/src/modes/components/tree-selector.ts +5 -5
- package/src/modes/controllers/command-controller.ts +140 -47
- package/src/modes/controllers/event-controller.ts +59 -3
- package/src/modes/controllers/input-controller.ts +70 -4
- package/src/modes/controllers/selector-controller.ts +15 -2
- package/src/modes/controllers/streaming-reveal.ts +17 -0
- package/src/modes/controllers/tool-args-reveal.ts +1 -1
- package/src/modes/interactive-mode.ts +142 -59
- package/src/modes/internal-url-autocomplete.ts +17 -2
- package/src/modes/prompt-action-autocomplete.ts +3 -1
- package/src/modes/running-subagent-badge.ts +13 -0
- package/src/modes/setup-wizard/scenes/glyph.ts +2 -13
- package/src/modes/setup-wizard/scenes/providers.ts +2 -1
- package/src/modes/setup-wizard/scenes/theme.ts +6 -12
- package/src/modes/setup-wizard/scenes/web-search.ts +8 -13
- package/src/modes/setup-wizard/wizard-overlay.ts +6 -5
- package/src/modes/theme/mermaid-rendering.test.ts +53 -0
- package/src/modes/theme/theme.ts +42 -15
- package/src/modes/types.ts +9 -1
- package/src/modes/utils/interactive-context-helpers.ts +1 -1
- package/src/priority.json +15 -0
- package/src/prompts/advisor/active-repo-watchdog.md +6 -0
- package/src/prompts/advisor/system.md +21 -7
- package/src/prompts/agents/designer.md +1 -1
- package/src/prompts/agents/explore.md +1 -1
- package/src/prompts/agents/librarian.md +2 -2
- package/src/prompts/agents/plan.md +2 -2
- package/src/prompts/agents/reviewer.md +1 -1
- package/src/prompts/agents/task.md +2 -2
- package/src/prompts/system/active-repo-context.md +4 -0
- package/src/prompts/system/gemini-tool-call-reminder.md +9 -0
- package/src/prompts/system/plan-mode-active.md +12 -3
- package/src/prompts/system/project-prompt.md +2 -2
- package/src/prompts/system/system-prompt.md +8 -6
- package/src/prompts/tools/bash.md +2 -2
- package/src/prompts/tools/checkpoint.md +1 -1
- package/src/prompts/tools/{find.md → glob.md} +1 -1
- package/src/prompts/tools/{search.md → grep.md} +3 -3
- package/src/prompts/tools/read.md +4 -2
- package/src/sdk.ts +98 -29
- package/src/session/agent-session.ts +828 -191
- package/src/session/session-history-format.ts +2 -2
- package/src/session/session-listing.ts +35 -2
- package/src/session/session-manager.ts +46 -0
- package/src/slash-commands/builtin-registry.ts +158 -22
- package/src/slash-commands/helpers/usage-report.ts +23 -2
- package/src/ssh/__tests__/connection-manager-args.test.ts +69 -0
- package/src/ssh/__tests__/file-transfer-posix-guard.test.ts +68 -0
- package/src/ssh/connection-manager.ts +12 -15
- package/src/ssh/file-transfer.ts +209 -0
- package/src/ssh/utils.ts +24 -0
- package/src/system-prompt.ts +60 -28
- package/src/task/executor.ts +96 -36
- package/src/task/index.ts +3 -3
- package/src/task/render.ts +14 -13
- package/src/task/worktree.ts +38 -1
- package/src/tiny/text.ts +49 -4
- package/src/tiny/worker.ts +3 -3
- package/src/tools/acp-bridge.ts +6 -1
- package/src/tools/bash.ts +2 -2
- package/src/tools/builtin-names.ts +26 -2
- package/src/tools/{find.ts → glob.ts} +48 -42
- package/src/tools/{search.ts → grep.ts} +311 -129
- package/src/tools/index.ts +13 -14
- package/src/tools/irc.ts +6 -2
- package/src/tools/path-utils.ts +64 -1
- package/src/tools/plan-mode-guard.ts +26 -13
- package/src/tools/read.ts +35 -6
- package/src/tools/render-utils.ts +14 -0
- package/src/tools/renderers.ts +15 -4
- package/src/tools/ssh.ts +17 -2
- package/src/tools/todo.ts +4 -26
- package/src/tools/write.ts +25 -9
- package/src/utils/active-repo-context.ts +143 -0
- package/src/utils/edit-mode.ts +19 -2
- package/src/utils/image-resize.ts +88 -7
- package/src/utils/lang-from-path.ts +3 -3
- package/src/utils/markit-cache.ts +166 -0
- package/src/utils/markit.ts +86 -18
- package/src/utils/prompt-path.ts +3 -0
- package/src/utils/shell-snapshot.ts +1 -1
- package/src/utils/title-generator.ts +1 -1
- package/src/web/search/provider.ts +54 -34
- package/src/web/search/providers/duckduckgo.ts +140 -0
- package/src/web/search/providers/firecrawl.ts +144 -0
- package/src/web/search/providers/tinyfish.ts +159 -0
- package/src/web/search/providers/xai.ts +292 -0
- package/src/web/search/providers/zai.ts +142 -56
- package/src/web/search/types.ts +6 -2
package/src/tools/index.ts
CHANGED
|
@@ -39,13 +39,14 @@ import { AstEditTool } from "./ast-edit";
|
|
|
39
39
|
import { AstGrepTool } from "./ast-grep";
|
|
40
40
|
import { BashTool } from "./bash";
|
|
41
41
|
import { BrowserTool } from "./browser";
|
|
42
|
-
import type
|
|
42
|
+
import { type BuiltinToolName, normalizeToolNames } from "./builtin-names";
|
|
43
43
|
import { type CheckpointState, CheckpointTool, RewindTool } from "./checkpoint";
|
|
44
44
|
import { DebugTool } from "./debug";
|
|
45
45
|
import { EvalTool } from "./eval";
|
|
46
46
|
import { resolveEvalBackends } from "./eval-backends";
|
|
47
|
-
import { FindTool } from "./find";
|
|
48
47
|
import { GithubTool } from "./gh";
|
|
48
|
+
import { GlobTool } from "./glob";
|
|
49
|
+
import { GrepTool } from "./grep";
|
|
49
50
|
import { InspectImageTool } from "./inspect-image";
|
|
50
51
|
import { IrcTool, isIrcEnabled } from "./irc";
|
|
51
52
|
import { JobTool } from "./job";
|
|
@@ -60,7 +61,6 @@ import { ReadTool } from "./read";
|
|
|
60
61
|
import { createReportToolIssueTool, isAutoQaEnabled } from "./report-tool-issue";
|
|
61
62
|
import { ResolveTool } from "./resolve";
|
|
62
63
|
import { reportFindingTool } from "./review";
|
|
63
|
-
import { SearchTool } from "./search";
|
|
64
64
|
import { SearchToolBm25Tool } from "./search-tool-bm25";
|
|
65
65
|
import { loadSshTool } from "./ssh";
|
|
66
66
|
import { type TodoPhase, TodoTool } from "./todo";
|
|
@@ -82,8 +82,9 @@ export * from "./checkpoint";
|
|
|
82
82
|
export * from "./debug";
|
|
83
83
|
export * from "./eval";
|
|
84
84
|
export * from "./eval-backends";
|
|
85
|
-
export * from "./find";
|
|
86
85
|
export * from "./gh";
|
|
86
|
+
export * from "./glob";
|
|
87
|
+
export * from "./grep";
|
|
87
88
|
export * from "./image-gen";
|
|
88
89
|
export * from "./inspect-image";
|
|
89
90
|
export * from "./irc";
|
|
@@ -98,7 +99,6 @@ export * from "./read";
|
|
|
98
99
|
export * from "./report-tool-issue";
|
|
99
100
|
export * from "./resolve";
|
|
100
101
|
export * from "./review";
|
|
101
|
-
export * from "./search";
|
|
102
102
|
export * from "./search-tool-bm25";
|
|
103
103
|
export * from "./ssh";
|
|
104
104
|
export * from "./todo";
|
|
@@ -384,7 +384,7 @@ export const DEFAULT_ESSENTIAL_TOOL_NAMES: readonly string[] = [
|
|
|
384
384
|
"bash",
|
|
385
385
|
"edit",
|
|
386
386
|
"write",
|
|
387
|
-
"
|
|
387
|
+
"glob",
|
|
388
388
|
"eval",
|
|
389
389
|
] as const;
|
|
390
390
|
|
|
@@ -395,7 +395,7 @@ export const DEFAULT_ESSENTIAL_TOOL_NAMES: readonly string[] = [
|
|
|
395
395
|
*/
|
|
396
396
|
export function computeEssentialBuiltinNames(settings: Settings): string[] {
|
|
397
397
|
const override = settings.get("tools.essentialOverride") ?? [];
|
|
398
|
-
const cleaned = override.map(name => name.trim()).filter(Boolean);
|
|
398
|
+
const cleaned = normalizeToolNames(override.map(name => name.trim()).filter(Boolean));
|
|
399
399
|
if (cleaned.length > 0) {
|
|
400
400
|
return cleaned.filter(name => name in BUILTIN_TOOLS);
|
|
401
401
|
}
|
|
@@ -449,8 +449,8 @@ export const BUILTIN_TOOLS: Record<BuiltinToolName, ToolFactory> = {
|
|
|
449
449
|
eval: s => new EvalTool(s),
|
|
450
450
|
ssh: loadSshTool,
|
|
451
451
|
github: GithubTool.createIf,
|
|
452
|
-
|
|
453
|
-
|
|
452
|
+
glob: s => new GlobTool(s),
|
|
453
|
+
grep: s => new GrepTool(s),
|
|
454
454
|
lsp: LspTool.createIf,
|
|
455
455
|
inspect_image: s => new InspectImageTool(s),
|
|
456
456
|
browser: s => new BrowserTool(s),
|
|
@@ -487,8 +487,7 @@ export type ToolName = BuiltinToolName;
|
|
|
487
487
|
export async function createTools(session: ToolSession, toolNames?: string[]): Promise<Tool[]> {
|
|
488
488
|
const includeYield = session.requireYieldTool === true;
|
|
489
489
|
const enableLsp = session.enableLsp ?? true;
|
|
490
|
-
let requestedTools =
|
|
491
|
-
toolNames && toolNames.length > 0 ? [...new Set(toolNames.map(name => name.toLowerCase()))] : undefined;
|
|
490
|
+
let requestedTools = toolNames && toolNames.length > 0 ? normalizeToolNames(toolNames) : undefined;
|
|
492
491
|
const goalEnabled = session.settings.get("goal.enabled");
|
|
493
492
|
const goalModeActive = goalEnabled && session.getGoalModeState?.()?.enabled === true;
|
|
494
493
|
if (goalModeActive && requestedTools && !requestedTools.includes("goal")) {
|
|
@@ -552,7 +551,7 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
552
551
|
// Auto-include AST counterparts when their text-based sibling is present
|
|
553
552
|
if (requestedTools) {
|
|
554
553
|
if (
|
|
555
|
-
requestedTools.includes("
|
|
554
|
+
requestedTools.includes("grep") &&
|
|
556
555
|
!requestedTools.includes("ast_grep") &&
|
|
557
556
|
session.settings.get("astGrep.enabled")
|
|
558
557
|
) {
|
|
@@ -602,8 +601,8 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
602
601
|
if (name === "eval") return allowEval;
|
|
603
602
|
if (name === "debug") return session.settings.get("debug.enabled");
|
|
604
603
|
if (name === "todo") return !includeYield && session.settings.get("todo.enabled");
|
|
605
|
-
if (name === "
|
|
606
|
-
if (name === "
|
|
604
|
+
if (name === "glob") return session.settings.get("glob.enabled");
|
|
605
|
+
if (name === "grep") return session.settings.get("grep.enabled");
|
|
607
606
|
if (name === "github") return session.settings.get("github.enabled");
|
|
608
607
|
if (name === "ast_grep") return session.settings.get("astGrep.enabled");
|
|
609
608
|
if (name === "ast_edit") return session.settings.get("astEdit.enabled");
|
package/src/tools/irc.ts
CHANGED
|
@@ -19,7 +19,7 @@ import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
|
19
19
|
import { IrcBus, type IrcDeliveryReceipt, type IrcMessage } from "../irc/bus";
|
|
20
20
|
import type { Theme } from "../modes/theme/theme";
|
|
21
21
|
import ircDescription from "../prompts/tools/irc.md" with { type: "text" };
|
|
22
|
-
import type
|
|
22
|
+
import { type AgentRegistry, MAIN_AGENT_ID } from "../registry/agent-registry";
|
|
23
23
|
import { canSpawnAtDepth } from "../task/types";
|
|
24
24
|
import { Ellipsis, renderStatusLine, renderTreeList, truncateToWidth } from "../tui";
|
|
25
25
|
import type { ToolSession } from ".";
|
|
@@ -280,6 +280,10 @@ export class IrcTool implements AgentTool<typeof ircSchema, IrcDetails> {
|
|
|
280
280
|
// parked agent on a broadcast would be a stampede. Direct sends go
|
|
281
281
|
// through the bus unfiltered so parked recipients are revived.
|
|
282
282
|
const targets = isBroadcast ? registry.listVisibleTo(senderId).map(ref => ref.id) : [to];
|
|
283
|
+
// A broadcast that also reaches the main agent delivers the body to it
|
|
284
|
+
// directly (its own incoming card); relaying the sibling legs to the
|
|
285
|
+
// main UI would then show the same body once per other recipient.
|
|
286
|
+
const suppressRelay = isBroadcast && targets.includes(MAIN_AGENT_ID);
|
|
283
287
|
const receipts = await Promise.all(
|
|
284
288
|
targets.map(target =>
|
|
285
289
|
bus.send(
|
|
@@ -287,7 +291,7 @@ export class IrcTool implements AgentTool<typeof ircSchema, IrcDetails> {
|
|
|
287
291
|
// Awaited sends mark the sender as blocked on an answer so a
|
|
288
292
|
// busy recipient that cannot reach a step boundary (async
|
|
289
293
|
// disabled) auto-replies instead of stranding the sender.
|
|
290
|
-
params.await
|
|
294
|
+
{ expectsReply: params.await || undefined, suppressRelay: suppressRelay || undefined },
|
|
291
295
|
),
|
|
292
296
|
),
|
|
293
297
|
);
|
package/src/tools/path-utils.ts
CHANGED
|
@@ -26,7 +26,11 @@ const INTERNAL_URL_SELECTOR_PART_RE = new RegExp(
|
|
|
26
26
|
);
|
|
27
27
|
// Schemes whose host grammar is identifier-shaped, so any trailing
|
|
28
28
|
// `:<selector-chunk>` is unambiguously a read-tool selector. `mcp://` is
|
|
29
|
-
// excluded because mcp resource URIs may legitimately contain colons.
|
|
29
|
+
// excluded because mcp resource URIs may legitimately contain colons. `ssh://`
|
|
30
|
+
// is included despite an optional `:port`; `splitInternalUrlSel` skips the peel
|
|
31
|
+
// for an `ssh://host:port` that has no `/path`, so the port colon is never
|
|
32
|
+
// mistaken for a selector (a real ssh selector trails the `/path`, e.g.
|
|
33
|
+
// `ssh://h/f:1-5`).
|
|
30
34
|
const INTERNAL_SCHEMES_WITH_SELECTORS: Record<string, true> = {
|
|
31
35
|
agent: true,
|
|
32
36
|
artifact: true,
|
|
@@ -37,6 +41,7 @@ const INTERNAL_SCHEMES_WITH_SELECTORS: Record<string, true> = {
|
|
|
37
41
|
pr: true,
|
|
38
42
|
rule: true,
|
|
39
43
|
skill: true,
|
|
44
|
+
ssh: true,
|
|
40
45
|
vault: true,
|
|
41
46
|
};
|
|
42
47
|
// Schemes whose resource URIs are server-defined and may legitimately end
|
|
@@ -55,6 +60,7 @@ const TOP_LEVEL_INTERNAL_URL_PREFIXES = [
|
|
|
55
60
|
"rule://",
|
|
56
61
|
"local://",
|
|
57
62
|
"mcp://",
|
|
63
|
+
"ssh://",
|
|
58
64
|
"vault://",
|
|
59
65
|
] as const;
|
|
60
66
|
|
|
@@ -337,6 +343,13 @@ export function splitInternalUrlSel(rawPath: string): { path: string; sel?: stri
|
|
|
337
343
|
if (!INTERNAL_SCHEMES_WITH_SELECTORS[scheme]) return { path: rawPath };
|
|
338
344
|
|
|
339
345
|
const schemeEnd = schemeMatch[0].length;
|
|
346
|
+
// ssh:// authority carries an optional `:port`; with no `/path` after the
|
|
347
|
+
// authority, a trailing `:NNNN` is the port, not a read selector
|
|
348
|
+
// (e.g. ssh://host:2222). Other schemes' authority-trailing selectors
|
|
349
|
+
// (artifact://5:1-50) still peel, so this guard is ssh-specific.
|
|
350
|
+
if (scheme === "ssh" && rawPath.indexOf("/", schemeEnd) === -1) {
|
|
351
|
+
return { path: rawPath };
|
|
352
|
+
}
|
|
340
353
|
let path = rawPath;
|
|
341
354
|
const chunks: string[] = [];
|
|
342
355
|
while (true) {
|
|
@@ -352,6 +365,27 @@ export function splitInternalUrlSel(rawPath: string): { path: string; sel?: stri
|
|
|
352
365
|
return { path, sel: chunks.join(":") };
|
|
353
366
|
}
|
|
354
367
|
|
|
368
|
+
/**
|
|
369
|
+
* Peel a read-tool selector off an internal-URL write target so `write` resolves
|
|
370
|
+
* the same file `read` does (e.g. `ssh://h/f:raw` -> `ssh://h/f`). Only the
|
|
371
|
+
* whole-file display modes `raw`/`conflicts` are accepted (they do not change
|
|
372
|
+
* which bytes are written); any other selector-shaped tail `splitInternalUrlSel`
|
|
373
|
+
* peels — a line range, a compound like `raw:1-20`, or a malformed `:-N` — throws,
|
|
374
|
+
* because `write` addresses a whole file, not a partial range, and silently
|
|
375
|
+
* stripping it would write to a path the caller never named. Non-URL paths and
|
|
376
|
+
* URLs without a selector pass through unchanged.
|
|
377
|
+
*/
|
|
378
|
+
export function peelWriteUrlSelector(rawPath: string): string {
|
|
379
|
+
const { path, sel } = splitInternalUrlSel(rawPath);
|
|
380
|
+
if (sel === undefined) return rawPath;
|
|
381
|
+
// Case-insensitive to match read's selector grammar (parseSel + the /i regexes above).
|
|
382
|
+
if (/^(?:raw|conflicts)$/i.test(sel)) return path;
|
|
383
|
+
throw new ToolError(
|
|
384
|
+
`write does not accept the trailing selector ":${sel}" — it writes a whole file. ` +
|
|
385
|
+
`Remove ":${sel}", or if the filename truly ends with it, percent-encode the ":" as %3A.`,
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
|
|
355
389
|
function assertNotInternalUrl(expanded: string, original: string): void {
|
|
356
390
|
for (const prefix of TOP_LEVEL_INTERNAL_URL_PREFIXES) {
|
|
357
391
|
if (expanded.startsWith(prefix)) {
|
|
@@ -375,6 +409,30 @@ export function isInternalUrlPath(filePath: string): boolean {
|
|
|
375
409
|
return false;
|
|
376
410
|
}
|
|
377
411
|
|
|
412
|
+
/**
|
|
413
|
+
* True when a tool path argument references the `ssh://` scheme anywhere.
|
|
414
|
+
*
|
|
415
|
+
* Substring (not anchored) on purpose: it feeds the read/search/write approval
|
|
416
|
+
* tier, which runs synchronously on the raw args. `search` only flattens a
|
|
417
|
+
* delimited `paths: "a,ssh://h/x"` into separate entries *after* approval, so an
|
|
418
|
+
* anchored check would let an embedded `ssh://` slip through at the read tier.
|
|
419
|
+
* Matching the literal `ssh://` substring also tracks exactly what routes to the
|
|
420
|
+
* SSH handler; over-matching only over-prompts (fail-closed).
|
|
421
|
+
*/
|
|
422
|
+
export function pathTargetsSsh(path: string): boolean {
|
|
423
|
+
return /ssh:\/\//i.test(path);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* True when a path is specifically an `ssh://` URL (anchored scheme match).
|
|
428
|
+
* Unlike {@link pathTargetsSsh} (substring, for the pre-expansion approval
|
|
429
|
+
* scan), this is the exact per-entry check used to reject `ssh://` *before* a
|
|
430
|
+
* side-effecting `InternalUrlRouter.resolve` in tools that need a local file.
|
|
431
|
+
*/
|
|
432
|
+
export function isSshUrl(path: string): boolean {
|
|
433
|
+
return /^ssh:\/\//i.test(path.trim());
|
|
434
|
+
}
|
|
435
|
+
|
|
378
436
|
/**
|
|
379
437
|
* Resolve a path relative to the given cwd.
|
|
380
438
|
* Handles ~ expansion and absolute paths.
|
|
@@ -1037,6 +1095,11 @@ export async function resolveToolSearchScope(opts: ToolScopeOptions): Promise<To
|
|
|
1037
1095
|
resolvedPathInputs.push(rawPath);
|
|
1038
1096
|
continue;
|
|
1039
1097
|
}
|
|
1098
|
+
if (isSshUrl(rawPath)) {
|
|
1099
|
+
throw new ToolError(
|
|
1100
|
+
`Cannot ${internalUrlAction} a remote ssh:// path (no local file): ${rawPath}. Use \`read ${rawPath}\` to view it, or the \`search\` tool to grep remote files.`,
|
|
1101
|
+
);
|
|
1102
|
+
}
|
|
1040
1103
|
if (hasGlobPathChars(rawPath)) {
|
|
1041
1104
|
throw new ToolError(`Glob patterns are not supported for internal URLs: ${rawPath}`);
|
|
1042
1105
|
}
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { HL_FILE_HASH_LENGTH, HL_FILE_HASH_SEP, HL_FILE_PREFIX, HL_FILE_SUFFIX } from "@oh-my-pi/hashline";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
type LocalProtocolOptions,
|
|
6
|
+
resolveLocalRoot,
|
|
7
|
+
resolveLocalUrlToPath,
|
|
8
|
+
resolveVaultUrlToPath,
|
|
9
|
+
} from "../internal-urls";
|
|
5
10
|
import type { ToolSession } from ".";
|
|
6
11
|
import { normalizeLocalScheme, resolveToCwd } from "./path-utils";
|
|
7
12
|
import { ToolError } from "./tool-errors";
|
|
@@ -10,16 +15,27 @@ const VAULT_SCHEME_PREFIX = "vault:";
|
|
|
10
15
|
const LOCAL_SCHEME_PREFIX = "local:";
|
|
11
16
|
const HL_TRAILING_TAG_RE = new RegExp(`${HL_FILE_HASH_SEP}[0-9A-Fa-f]{${HL_FILE_HASH_LENGTH}}$`);
|
|
12
17
|
|
|
18
|
+
/** Resolve the `local://` options the session uses, preferring its own
|
|
19
|
+
* {@link LocalProtocolOptions} (the mapping `read`/`write`/`eval` resolve
|
|
20
|
+
* through) over the bare `getArtifactsDir`/`getSessionId` pair. Subagents and
|
|
21
|
+
* multi-session hosts (cmux/ACP, embedded SDK) pin `local://` to a parent/foreign
|
|
22
|
+
* root via `localProtocolOptions`; the sandbox root the plan-mode guard derives
|
|
23
|
+
* must match where the artifact actually lives, or it rejects a legitimate plan
|
|
24
|
+
* edit (and tag-based path recovery onto the sandbox would miss it). */
|
|
25
|
+
function planLocalProtocolOptions(session: ToolSession): LocalProtocolOptions {
|
|
26
|
+
return (
|
|
27
|
+
session.localProtocolOptions ?? {
|
|
28
|
+
getArtifactsDir: () => session.getArtifactsDir?.() ?? null,
|
|
29
|
+
getSessionId: () => session.getSessionId?.() ?? null,
|
|
30
|
+
}
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
13
34
|
/** Resolve the absolute path of the session's `local://` artifact sandbox.
|
|
14
35
|
* Returns `null` when the session has no artifact wiring (e.g. tests). */
|
|
15
36
|
function localSandboxRoot(session: ToolSession): string | null {
|
|
16
37
|
try {
|
|
17
|
-
return path.resolve(
|
|
18
|
-
resolveLocalRoot({
|
|
19
|
-
getArtifactsDir: session.getArtifactsDir,
|
|
20
|
-
getSessionId: session.getSessionId,
|
|
21
|
-
}),
|
|
22
|
-
);
|
|
38
|
+
return path.resolve(resolveLocalRoot(planLocalProtocolOptions(session)));
|
|
23
39
|
} catch {
|
|
24
40
|
return null;
|
|
25
41
|
}
|
|
@@ -62,8 +78,8 @@ export function unwrapHashlineHeaderPath(targetPath: string): string {
|
|
|
62
78
|
* always agree on the absolute target (including bracketed hashline headers,
|
|
63
79
|
* `local://` URLs, and bare absolute paths). Files inside the sandbox are not
|
|
64
80
|
* part of the working tree, so plan mode treats them as freely writable
|
|
65
|
-
* scratch/plan space. */
|
|
66
|
-
function targetsLocalSandbox(session: ToolSession, targetPath: string): boolean {
|
|
81
|
+
* scratch/plan space — and tag-based path recovery may rebind onto them. */
|
|
82
|
+
export function targetsLocalSandbox(session: ToolSession, targetPath: string): boolean {
|
|
67
83
|
const root = localSandboxRoot(session);
|
|
68
84
|
if (!root) return false;
|
|
69
85
|
let resolved: string;
|
|
@@ -99,10 +115,7 @@ export function resolvePlanPath(session: ToolSession, targetPath: string): strin
|
|
|
99
115
|
const unwrapped = unwrapHashlineHeaderPath(targetPath);
|
|
100
116
|
const normalized = normalizeLocalScheme(unwrapped);
|
|
101
117
|
if (normalized.startsWith(LOCAL_SCHEME_PREFIX)) {
|
|
102
|
-
return resolveLocalUrlToPath(normalized,
|
|
103
|
-
getArtifactsDir: session.getArtifactsDir,
|
|
104
|
-
getSessionId: session.getSessionId,
|
|
105
|
-
});
|
|
118
|
+
return resolveLocalUrlToPath(normalized, planLocalProtocolOptions(session));
|
|
106
119
|
}
|
|
107
120
|
|
|
108
121
|
if (normalized.startsWith(VAULT_SCHEME_PREFIX)) {
|
package/src/tools/read.ts
CHANGED
|
@@ -3,7 +3,13 @@ import * as fs from "node:fs/promises";
|
|
|
3
3
|
import * as os from "node:os";
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import { formatHashlineHeader, formatNumberedLine, formatNumberedLines } from "@oh-my-pi/hashline";
|
|
6
|
-
import type {
|
|
6
|
+
import type {
|
|
7
|
+
AgentTool,
|
|
8
|
+
AgentToolContext,
|
|
9
|
+
AgentToolResult,
|
|
10
|
+
AgentToolUpdateCallback,
|
|
11
|
+
ToolTier,
|
|
12
|
+
} from "@oh-my-pi/pi-agent-core";
|
|
7
13
|
import type { ImageContent, TextContent } from "@oh-my-pi/pi-ai";
|
|
8
14
|
import { glob, type SummaryResult, summarizeCode } from "@oh-my-pi/pi-natives";
|
|
9
15
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
@@ -83,6 +89,7 @@ import {
|
|
|
83
89
|
formatPathRelativeToCwd,
|
|
84
90
|
type LineRange,
|
|
85
91
|
parseLineRanges,
|
|
92
|
+
pathTargetsSsh,
|
|
86
93
|
resolveReadPath,
|
|
87
94
|
splitDelimitedPathEntry,
|
|
88
95
|
splitInternalUrlSel,
|
|
@@ -170,6 +177,10 @@ interface HashlineHeaderContext {
|
|
|
170
177
|
fullText?: string;
|
|
171
178
|
}
|
|
172
179
|
|
|
180
|
+
function formatReadHashlineHeader(displayPath: string, tag: string): string {
|
|
181
|
+
return formatHashlineHeader(path.basename(displayPath), tag);
|
|
182
|
+
}
|
|
183
|
+
|
|
173
184
|
function recordFullHashlineContext(
|
|
174
185
|
session: ToolSession,
|
|
175
186
|
absolutePath: string | undefined,
|
|
@@ -180,7 +191,7 @@ function recordFullHashlineContext(
|
|
|
180
191
|
const normalized = normalizeToLF(fullText);
|
|
181
192
|
const tag = getFileSnapshotStore(session).record(canonicalSnapshotKey(absolutePath), normalized);
|
|
182
193
|
return {
|
|
183
|
-
header:
|
|
194
|
+
header: formatReadHashlineHeader(displayPath, tag),
|
|
184
195
|
tag,
|
|
185
196
|
fullText: normalized,
|
|
186
197
|
};
|
|
@@ -203,7 +214,7 @@ async function readHashlineHeaderContext(
|
|
|
203
214
|
}
|
|
204
215
|
|
|
205
216
|
function hashlineHeaderContext(displayPath: string, tag: string): HashlineHeaderContext {
|
|
206
|
-
return { header:
|
|
217
|
+
return { header: formatReadHashlineHeader(displayPath, tag), tag };
|
|
207
218
|
}
|
|
208
219
|
|
|
209
220
|
function prependHashlineHeader(text: string, context: HashlineHeaderContext | undefined): string {
|
|
@@ -742,11 +753,27 @@ function isMultiRange(parsed: ParsedSelector): boolean {
|
|
|
742
753
|
return parsed.kind === "lines" && parsed.ranges.length > 1;
|
|
743
754
|
}
|
|
744
755
|
|
|
756
|
+
function selectorChunkLooksReadLike(chunk: string): boolean {
|
|
757
|
+
const lower = chunk.toLowerCase();
|
|
758
|
+
return (
|
|
759
|
+
lower === "raw" || lower === "conflicts" || /^-\d+(?:[-+]\d+)?$/.test(chunk) || parseLineRanges(chunk) !== null
|
|
760
|
+
);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
function invalidSelector(sel: string): ToolError {
|
|
764
|
+
return new ToolError(
|
|
765
|
+
`Invalid selector ':${sel}'. Use :N, :N-M, :N+K, :N- (open-ended), a comma-separated list of ranges, :raw, or a range combined with raw (e.g. :raw:50-100).`,
|
|
766
|
+
);
|
|
767
|
+
}
|
|
768
|
+
|
|
745
769
|
function parseSel(sel: string | undefined): ParsedSelector {
|
|
746
770
|
if (!sel || sel.length === 0) return { kind: "none" };
|
|
747
771
|
|
|
748
772
|
// Compound selector: `1-50:raw` or `raw:1-50`. Split into chunks and accept
|
|
749
|
-
//
|
|
773
|
+
// exactly one line range (possibly multi) plus the literal `raw`. Selector-like
|
|
774
|
+
// compounds that are not in that accepted set are invalid rather than "none";
|
|
775
|
+
// otherwise `read` can silently widen a malformed selector like
|
|
776
|
+
// `artifact://5:conflicts:1-1` while `grep` rejects it.
|
|
750
777
|
if (sel.includes(":")) {
|
|
751
778
|
const chunks = sel.split(":");
|
|
752
779
|
if (chunks.length === 2) {
|
|
@@ -762,6 +789,7 @@ function parseSel(sel: string | undefined): ParsedSelector {
|
|
|
762
789
|
}
|
|
763
790
|
}
|
|
764
791
|
}
|
|
792
|
+
if (chunks.every(selectorChunkLooksReadLike)) throw invalidSelector(sel);
|
|
765
793
|
// Unrecognized compound — fall through (sqlite/archive/url consume their own colon syntax).
|
|
766
794
|
return { kind: "none" };
|
|
767
795
|
}
|
|
@@ -814,7 +842,8 @@ type SuffixMatchCache = Map<string, { absolutePath: string; displayPath: string
|
|
|
814
842
|
*/
|
|
815
843
|
export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
816
844
|
readonly name = "read";
|
|
817
|
-
readonly approval =
|
|
845
|
+
readonly approval = (args: unknown): ToolTier =>
|
|
846
|
+
pathTargetsSsh(String((args as { path?: unknown }).path ?? "")) ? "exec" : "read";
|
|
818
847
|
readonly label = "Read";
|
|
819
848
|
readonly loadMode = "essential";
|
|
820
849
|
readonly description: string;
|
|
@@ -1598,7 +1627,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1598
1627
|
const tag = await recordFileSnapshot(this.session, absolutePath);
|
|
1599
1628
|
if (tag) {
|
|
1600
1629
|
recordSeenLinesFromBody(this.session, absolutePath, tag, outputText);
|
|
1601
|
-
outputText = `${
|
|
1630
|
+
outputText = `${formatReadHashlineHeader(formatPathRelativeToCwd(absolutePath, this.session.cwd), tag)}\n${outputText}`;
|
|
1602
1631
|
}
|
|
1603
1632
|
}
|
|
1604
1633
|
if (notices.length > 0) {
|
|
@@ -99,6 +99,17 @@ export function getPreviewLines(text: string, maxLines: number, maxLineLen: numb
|
|
|
99
99
|
return lines.slice(0, maxLines).map(l => truncateToWidth(l.trim(), maxLineLen, ellipsis));
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Collapse a possibly multi-line string into a single line, then truncate it to
|
|
104
|
+
* `maxWidth` display cells. {@link truncateToWidth} alone caps width but
|
|
105
|
+
* newlines are zero-width, so multi-line content (markdown briefs, tool args,
|
|
106
|
+
* provider errors) would otherwise spill a single status row across several
|
|
107
|
+
* visual lines. Whitespace runs collapse to one space, so tabs are handled too.
|
|
108
|
+
*/
|
|
109
|
+
export function previewLine(text: string, maxWidth: number, ellipsis?: Ellipsis): string {
|
|
110
|
+
return truncateToWidth(text.replace(/\s+/g, " ").trim(), maxWidth, ellipsis);
|
|
111
|
+
}
|
|
112
|
+
|
|
102
113
|
// =============================================================================
|
|
103
114
|
// URL Utilities
|
|
104
115
|
// =============================================================================
|
|
@@ -698,6 +709,9 @@ export function formatScreenshot(opts: {
|
|
|
698
709
|
lines.push(`Format: ${opts.resized.mimeType} (${(opts.resized.buffer.length / 1024).toFixed(2)} KB)`);
|
|
699
710
|
lines.push(`Dimensions: ${opts.resized.width}x${opts.resized.height}`);
|
|
700
711
|
}
|
|
712
|
+
if (opts.resized.decodeFailed) {
|
|
713
|
+
lines.push("Resize: image decoder failed; using original image bytes");
|
|
714
|
+
}
|
|
701
715
|
const dimensionNote = formatDimensionNote(opts.resized);
|
|
702
716
|
if (dimensionNote) {
|
|
703
717
|
lines.push(dimensionNote);
|
package/src/tools/renderers.ts
CHANGED
|
@@ -18,15 +18,15 @@ import { bashToolRenderer } from "./bash";
|
|
|
18
18
|
import { browserToolRenderer } from "./browser/render";
|
|
19
19
|
import { debugToolRenderer } from "./debug";
|
|
20
20
|
import { evalToolRenderer } from "./eval-render";
|
|
21
|
-
import { findToolRenderer } from "./find";
|
|
22
21
|
import { githubToolRenderer } from "./gh-renderer";
|
|
22
|
+
import { globToolRenderer } from "./glob";
|
|
23
|
+
import { grepToolRenderer } from "./grep";
|
|
23
24
|
import { inspectImageToolRenderer } from "./inspect-image-renderer";
|
|
24
25
|
import { ircToolRenderer } from "./irc";
|
|
25
26
|
import { jobToolRenderer } from "./job";
|
|
26
27
|
import { recallToolRenderer, reflectToolRenderer, retainToolRenderer } from "./memory-render";
|
|
27
28
|
import { readToolRenderer } from "./read";
|
|
28
29
|
import { resolveToolRenderer } from "./resolve";
|
|
29
|
-
import { searchToolRenderer } from "./search";
|
|
30
30
|
import { searchToolBm25Renderer } from "./search-tool-bm25";
|
|
31
31
|
import { sshToolRenderer } from "./ssh";
|
|
32
32
|
import { todoToolRenderer } from "./todo";
|
|
@@ -52,6 +52,17 @@ export type ToolRenderer = {
|
|
|
52
52
|
* streams rows the result render preserves.
|
|
53
53
|
*/
|
|
54
54
|
provisionalPendingPreview?: boolean | "collapsed";
|
|
55
|
+
/**
|
|
56
|
+
* Whether the partial-result render is provisional: chrome rows (header
|
|
57
|
+
* glyph, frame state) that change between `options.isPartial === true` and
|
|
58
|
+
* the final result render. When `true`, the block is treated as
|
|
59
|
+
* commit-unstable while a partial result is in flight, so the
|
|
60
|
+
* stable-prefix ratchet in `deriveLiveCommitState` cannot promote the
|
|
61
|
+
* partial chrome to native scrollback only to have the final render strand
|
|
62
|
+
* it above the settled frame. Absent = the partial render is byte-stable
|
|
63
|
+
* with the final render and may commit like any settled stream.
|
|
64
|
+
*/
|
|
65
|
+
provisionalPartialResult?: boolean;
|
|
55
66
|
};
|
|
56
67
|
|
|
57
68
|
export const toolRenderers: Record<string, ToolRenderer> = {
|
|
@@ -64,8 +75,8 @@ export const toolRenderers: Record<string, ToolRenderer> = {
|
|
|
64
75
|
eval: evalToolRenderer as ToolRenderer,
|
|
65
76
|
edit: editToolRenderer as ToolRenderer,
|
|
66
77
|
apply_patch: editToolRenderer as ToolRenderer,
|
|
67
|
-
|
|
68
|
-
|
|
78
|
+
glob: globToolRenderer as ToolRenderer,
|
|
79
|
+
grep: grepToolRenderer as ToolRenderer,
|
|
69
80
|
lsp: lspToolRenderer as ToolRenderer,
|
|
70
81
|
inspect_image: inspectImageToolRenderer as ToolRenderer,
|
|
71
82
|
irc: ircToolRenderer as ToolRenderer,
|
package/src/tools/ssh.ts
CHANGED
|
@@ -281,6 +281,7 @@ export const sshToolRenderer = {
|
|
|
281
281
|
result: {
|
|
282
282
|
content: Array<{ type: string; text?: string }>;
|
|
283
283
|
details?: SSHToolDetails;
|
|
284
|
+
isError?: boolean;
|
|
284
285
|
},
|
|
285
286
|
options: RenderResultOptions & { renderContext?: SshRenderContext },
|
|
286
287
|
uiTheme: Theme,
|
|
@@ -289,8 +290,14 @@ export const sshToolRenderer = {
|
|
|
289
290
|
const details = result.details;
|
|
290
291
|
const host = args?.host || "…";
|
|
291
292
|
const command = args?.command ?? "";
|
|
293
|
+
const isError = result.isError === true;
|
|
294
|
+
const isPartial = options.isPartial === true;
|
|
292
295
|
const header = renderStatusLine(
|
|
293
|
-
|
|
296
|
+
isPartial
|
|
297
|
+
? { icon: "pending", title: "SSH", description: `[${host}]` }
|
|
298
|
+
: isError
|
|
299
|
+
? { icon: "error", title: "SSH", description: `[${host}]` }
|
|
300
|
+
: { iconOverride: uiTheme.styledSymbol("tool.ssh", "accent"), title: "SSH", description: `[${host}]` },
|
|
294
301
|
uiTheme,
|
|
295
302
|
);
|
|
296
303
|
const cmdLines = formatSshCommandLines(command, uiTheme);
|
|
@@ -342,7 +349,7 @@ export const sshToolRenderer = {
|
|
|
342
349
|
return outputBlock.render(
|
|
343
350
|
{
|
|
344
351
|
header,
|
|
345
|
-
state: "success",
|
|
352
|
+
state: isPartial ? "pending" : isError ? "error" : "success",
|
|
346
353
|
sections: [
|
|
347
354
|
{
|
|
348
355
|
// Viewport-sized tail window in every state — streaming and final
|
|
@@ -366,4 +373,12 @@ export const sshToolRenderer = {
|
|
|
366
373
|
// that shifts while args stream. Expanded output is top-anchored enough for
|
|
367
374
|
// the transcript to commit its settled prefix.
|
|
368
375
|
provisionalPendingPreview: "collapsed",
|
|
376
|
+
// Partial-result chrome (pending icon and frame state) differs from the
|
|
377
|
+
// final SSH glyph/state, so the block stays commit-unstable while
|
|
378
|
+
// `options.isPartial` holds. Without this, a long-running SSH command's
|
|
379
|
+
// stable pending header would be promoted by the stable-prefix ratchet and
|
|
380
|
+
// committed to native scrollback, then the final render's SSH glyph would
|
|
381
|
+
// land below and strand a duplicate pending header above the final frame
|
|
382
|
+
// ([#3177](https://github.com/can1357/oh-my-pi/issues/3177)).
|
|
383
|
+
provisionalPartialResult: true,
|
|
369
384
|
};
|
package/src/tools/todo.ts
CHANGED
|
@@ -57,7 +57,10 @@ const todoSchema = type({
|
|
|
57
57
|
"list?": InitListEntry.array().describe("phased task list (init)"),
|
|
58
58
|
"task?": type("string").describe("task content"),
|
|
59
59
|
"phase?": type("string").describe("phase name"),
|
|
60
|
-
|
|
60
|
+
// No `atLeastLength(1)` here: `items` is only meaningful for `init`/`append`,
|
|
61
|
+
// and both enforce non-empty with op-specific errors. A stray `items: []` on
|
|
62
|
+
// an op that ignores it (e.g. `view`) must not be a hard schema rejection.
|
|
63
|
+
"items?": type("string").describe("task content").array().describe("tasks to append"),
|
|
61
64
|
}).describe("apply a single todo operation");
|
|
62
65
|
|
|
63
66
|
type TodoParams = TodoSchema;
|
|
@@ -156,31 +159,6 @@ export function getLatestTodoPhasesFromEntries(entries: SessionEntry[]): TodoPha
|
|
|
156
159
|
return [];
|
|
157
160
|
}
|
|
158
161
|
|
|
159
|
-
/**
|
|
160
|
-
* Pick the actionable window of tasks to display in the sticky todo panel.
|
|
161
|
-
*
|
|
162
|
-
* Returns up to `maxVisible` open (pending / in_progress) tasks in their
|
|
163
|
-
* original phase order, plus the count of remaining open tasks not shown so
|
|
164
|
-
* the caller can render a `+N more` hint. When every task in `tasks` is
|
|
165
|
-
* closed (completed or abandoned), returns the trailing `maxVisible` tasks
|
|
166
|
-
* with `hiddenOpenCount = 0`, so the panel keeps useful context until the
|
|
167
|
-
* active-phase pointer advances on the next `todo`.
|
|
168
|
-
*
|
|
169
|
-
* Task identity and order are preserved — this is a slice, never a sort.
|
|
170
|
-
*/
|
|
171
|
-
export function selectStickyTodoWindow(
|
|
172
|
-
tasks: TodoItem[],
|
|
173
|
-
maxVisible = 5,
|
|
174
|
-
): { visible: TodoItem[]; hiddenOpenCount: number } {
|
|
175
|
-
const openTasks = tasks.filter(t => t.status === "pending" || t.status === "in_progress");
|
|
176
|
-
if (openTasks.length > 0) {
|
|
177
|
-
const visible = openTasks.slice(0, maxVisible);
|
|
178
|
-
return { visible, hiddenOpenCount: openTasks.length - visible.length };
|
|
179
|
-
}
|
|
180
|
-
const start = Math.max(0, tasks.length - maxVisible);
|
|
181
|
-
return { visible: tasks.slice(start), hiddenOpenCount: 0 };
|
|
182
|
-
}
|
|
183
|
-
|
|
184
162
|
/** Minimum overlap (after normalization) required for a substring match.
|
|
185
163
|
* Picked at six chars to admit single-word identifiers like "review" /
|
|
186
164
|
* "Sonnet" without admitting tiny common substrings like "test" / "fix"
|
package/src/tools/write.ts
CHANGED
|
@@ -3,7 +3,13 @@ import * as fs from "node:fs/promises";
|
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
|
|
5
5
|
import { formatHashlineHeader, stripHashlinePrefixes } from "@oh-my-pi/hashline";
|
|
6
|
-
import type {
|
|
6
|
+
import type {
|
|
7
|
+
AgentTool,
|
|
8
|
+
AgentToolContext,
|
|
9
|
+
AgentToolResult,
|
|
10
|
+
AgentToolUpdateCallback,
|
|
11
|
+
ToolTier,
|
|
12
|
+
} from "@oh-my-pi/pi-agent-core";
|
|
7
13
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
8
14
|
import { isEnoent, isRecord, prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
9
15
|
import { type } from "arktype";
|
|
@@ -41,7 +47,7 @@ import {
|
|
|
41
47
|
} from "./conflict-detect";
|
|
42
48
|
import { invalidateFsScanAfterWrite } from "./fs-cache-invalidation";
|
|
43
49
|
import { type OutputMeta, outputMeta } from "./output-meta";
|
|
44
|
-
import { formatPathRelativeToCwd, isInternalUrlPath } from "./path-utils";
|
|
50
|
+
import { formatPathRelativeToCwd, isInternalUrlPath, pathTargetsSsh, peelWriteUrlSelector } from "./path-utils";
|
|
45
51
|
import { enforcePlanModeWrite, resolvePlanPath, unwrapHashlineHeaderPath } from "./plan-mode-guard";
|
|
46
52
|
import {
|
|
47
53
|
cachedRenderedString,
|
|
@@ -263,14 +269,22 @@ function parseSqliteWriteTarget(subPath: string, queryString: string): { table:
|
|
|
263
269
|
*/
|
|
264
270
|
export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails> {
|
|
265
271
|
readonly name = "write";
|
|
266
|
-
readonly approval = (args: unknown) => {
|
|
272
|
+
readonly approval = (args: unknown): ToolTier => {
|
|
267
273
|
const rawPath = (args as Partial<WriteParams>).path;
|
|
268
|
-
if (typeof rawPath !== "string"
|
|
274
|
+
if (typeof rawPath !== "string") return "write";
|
|
275
|
+
// Unwrap a hashline `[path#TAG]` wrapper first (parity with execute) so a
|
|
276
|
+
// wrapped `[ssh://h/x#ABCD]` can't dodge scheme detection and the tier checks below.
|
|
277
|
+
const path = unwrapHashlineHeaderPath(rawPath);
|
|
278
|
+
// Remote SSH writes open an outbound connection and run a remote shell —
|
|
279
|
+
// gate them like the exec-tier `ssh` tool, ahead of the handler-write
|
|
280
|
+
// logic. Substring match also covers selector-suffixed targets.
|
|
281
|
+
if (pathTargetsSsh(path)) return "exec";
|
|
282
|
+
if (!isInternalUrlPath(path)) return "write";
|
|
269
283
|
// Internal URLs are usually session-local artifacts (read tier), but a
|
|
270
|
-
// scheme whose handler exposes a `write` hook mutates handler-owned
|
|
271
|
-
//
|
|
272
|
-
//
|
|
273
|
-
const match = /^([a-z][a-z0-9+.-]*):\/\//i.exec(
|
|
284
|
+
// scheme whose handler exposes a `write` hook mutates handler-owned user
|
|
285
|
+
// data (e.g. vault:// notes) and must take the write tier so always-ask
|
|
286
|
+
// mode actually prompts.
|
|
287
|
+
const match = /^([a-z][a-z0-9+.-]*):\/\//i.exec(path.trim());
|
|
274
288
|
const handler = match ? InternalUrlRouter.instance().getHandler(match[1]!.toLowerCase()) : undefined;
|
|
275
289
|
return handler?.write ? "write" : "read";
|
|
276
290
|
};
|
|
@@ -778,7 +792,9 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
778
792
|
// (which fails on a leading `[`) and the bridge router would send a
|
|
779
793
|
// `[local://scratch.md#ABCD]` write to the editor instead of the
|
|
780
794
|
// session-local sandbox.
|
|
781
|
-
|
|
795
|
+
// Peel a read-tool selector (`:raw`, `:1-20`, …) so the write target matches
|
|
796
|
+
// what `read` resolves for the same URL; line-range/malformed selectors throw.
|
|
797
|
+
const path = peelWriteUrlSelector(unwrapHashlineHeaderPath(rawPath));
|
|
782
798
|
return untilAborted(signal, async () => {
|
|
783
799
|
// Strip hashline display prefixes ([PATH#HASH] + LINE:) if the model copied them from read output
|
|
784
800
|
const { text: cleanContent, stripped } = stripWriteContent(this.session, content);
|