@oh-my-pi/pi-coding-agent 15.10.10 → 15.10.11
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 +95 -4
- package/dist/cli.js +23087 -0
- package/dist/tokenizers.linux-x64-gnu-xcjh3jwk.node +0 -0
- package/dist/types/async/job-manager.d.ts +18 -0
- package/dist/types/cli/args.d.ts +1 -1
- package/dist/types/cli/dry-balance-cli.d.ts +1 -1
- package/dist/types/cli/gallery-cli.d.ts +1 -1
- package/dist/types/cli/gallery-fixtures/types.d.ts +1 -1
- package/dist/types/cli/usage-cli.d.ts +72 -0
- package/dist/types/commands/launch.d.ts +1 -1
- package/dist/types/commands/read.d.ts +1 -1
- package/dist/types/commands/usage.d.ts +25 -0
- package/dist/types/config/append-only-context-mode.d.ts +2 -1
- package/dist/types/config/model-discovery.d.ts +55 -0
- package/dist/types/config/model-registry.d.ts +7 -219
- package/dist/types/config/model-resolver.d.ts +16 -10
- package/dist/types/config/model-roles.d.ts +28 -0
- package/dist/types/config/models-config-schema.d.ts +523 -42
- package/dist/types/config/models-config.d.ts +385 -0
- package/dist/types/config/settings-schema.d.ts +12 -7
- package/dist/types/config/settings.d.ts +1 -1
- package/dist/types/debug/log-viewer.d.ts +1 -1
- package/dist/types/debug/raw-sse.d.ts +1 -1
- package/dist/types/eval/backend.d.ts +0 -2
- package/dist/types/eval/idle-timeout.d.ts +0 -4
- package/dist/types/eval/js/shared/rewrite-imports.d.ts +6 -6
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/extensibility/extensions/types.d.ts +3 -3
- package/dist/types/hindsight/mental-models.d.ts +17 -8
- package/dist/types/internal-urls/artifact-protocol.d.ts +2 -2
- package/dist/types/internal-urls/types.d.ts +1 -1
- package/dist/types/lsp/edits.d.ts +9 -0
- package/dist/types/lsp/index.d.ts +2 -2
- package/dist/types/lsp/types.d.ts +2 -0
- package/dist/types/lsp/utils.d.ts +3 -0
- package/dist/types/mcp/json-rpc.d.ts +5 -0
- package/dist/types/mnemopi/state.d.ts +11 -1
- package/dist/types/modes/components/agent-dashboard.d.ts +1 -1
- package/dist/types/modes/components/assistant-message.d.ts +3 -1
- package/dist/types/modes/components/bash-execution.d.ts +1 -1
- package/dist/types/modes/components/copy-selector.d.ts +1 -1
- package/dist/types/modes/components/dynamic-border.d.ts +1 -1
- package/dist/types/modes/components/extensions/extension-dashboard.d.ts +1 -1
- package/dist/types/modes/components/extensions/extension-list.d.ts +1 -1
- package/dist/types/modes/components/extensions/inspector-panel.d.ts +1 -1
- package/dist/types/modes/components/footer.d.ts +1 -1
- package/dist/types/modes/components/hook-editor.d.ts +5 -0
- package/dist/types/modes/components/hook-input.d.ts +4 -0
- package/dist/types/modes/components/hook-selector.d.ts +1 -1
- package/dist/types/modes/components/model-selector.d.ts +1 -1
- package/dist/types/modes/components/plan-review-overlay.d.ts +1 -1
- package/dist/types/modes/components/session-observer-overlay.d.ts +1 -1
- package/dist/types/modes/components/session-selector.d.ts +1 -1
- package/dist/types/modes/components/status-line/component.d.ts +1 -1
- package/dist/types/modes/components/tiny-title-download-progress.d.ts +1 -1
- package/dist/types/modes/components/transcript-container.d.ts +25 -6
- package/dist/types/modes/components/tree-selector.d.ts +1 -1
- package/dist/types/modes/components/user-message-selector.d.ts +1 -1
- package/dist/types/modes/components/user-message.d.ts +2 -1
- package/dist/types/modes/components/visual-truncate.d.ts +1 -1
- package/dist/types/modes/components/welcome.d.ts +19 -3
- package/dist/types/modes/controllers/mcp-command-controller.d.ts +1 -1
- package/dist/types/modes/controllers/streaming-reveal.d.ts +1 -1
- package/dist/types/modes/interactive-mode.d.ts +1 -1
- package/dist/types/modes/setup-wizard/scenes/sign-in.d.ts +1 -1
- package/dist/types/modes/setup-wizard/scenes/types.d.ts +1 -1
- package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +1 -1
- package/dist/types/modes/setup-wizard/wizard-overlay.d.ts +1 -1
- package/dist/types/modes/types.d.ts +2 -1
- package/dist/types/session/agent-session.d.ts +1 -1
- package/dist/types/session/auth-broker-config.d.ts +4 -0
- package/dist/types/session/session-manager.d.ts +1 -1
- package/dist/types/slash-commands/helpers/stats-dashboard.d.ts +13 -0
- package/dist/types/ssh/connection-manager.d.ts +8 -0
- package/dist/types/task/parallel.d.ts +2 -2
- package/dist/types/task/worktree.d.ts +2 -0
- package/dist/types/tools/ask.d.ts +4 -0
- package/dist/types/tools/conflict-detect.d.ts +16 -0
- package/dist/types/tools/github-cache.d.ts +7 -0
- package/dist/types/tools/sqlite-reader.d.ts +3 -0
- package/dist/types/tui/output-block.d.ts +3 -3
- package/dist/types/utils/changelog.d.ts +8 -0
- package/dist/types/web/scrapers/readthedocs.d.ts +3 -0
- package/dist/types/web/scrapers/types.d.ts +12 -0
- package/dist/types/web/search/providers/codex.d.ts +1 -1
- package/dist/types/web/search/providers/gemini.d.ts +1 -1
- package/examples/extensions/tools.ts +5 -4
- package/package.json +14 -11
- package/scripts/build-binary.ts +18 -23
- package/scripts/bundle-dist.ts +81 -0
- package/scripts/{dev-launch → omp} +1 -1
- package/scripts/{dev-launch-preload.ts → omp.ts} +1 -1
- package/src/async/job-manager.ts +57 -3
- package/src/autoresearch/dashboard.ts +1 -1
- package/src/autoresearch/prompt-setup.md +6 -6
- package/src/autoresearch/prompt.md +6 -6
- package/src/capability/fs.ts +10 -0
- package/src/cli/args.ts +1 -1
- package/src/cli/auth-gateway-cli.ts +1 -3
- package/src/cli/dry-balance-cli.ts +1 -1
- package/src/cli/gallery-cli.ts +1 -1
- package/src/cli/gallery-fixtures/fs.ts +1 -1
- package/src/cli/gallery-fixtures/types.ts +5 -1
- package/src/cli/list-models.ts +2 -1
- package/src/cli/usage-cli.ts +603 -0
- package/src/cli-commands.ts +1 -0
- package/src/cli.ts +69 -5
- package/src/commands/complete.ts +1 -1
- package/src/commands/launch.ts +1 -1
- package/src/commands/read.ts +6 -3
- package/src/commands/usage.ts +35 -0
- package/src/commit/agentic/agent.ts +1 -1
- package/src/commit/model-selection.ts +1 -1
- package/src/config/append-only-context-mode.ts +6 -12
- package/src/config/model-discovery.ts +554 -0
- package/src/config/model-registry.ts +231 -1019
- package/src/config/model-resolver.ts +113 -156
- package/src/config/model-roles.ts +74 -0
- package/src/config/models-config-schema.ts +57 -8
- package/src/config/models-config.ts +129 -0
- package/src/config/settings-schema.ts +18 -4
- package/src/config/settings.ts +37 -1
- package/src/dap/client.ts +124 -37
- package/src/dap/session.ts +259 -158
- package/src/debug/log-viewer.ts +1 -1
- package/src/debug/raw-sse.ts +1 -1
- package/src/edit/diff.ts +47 -3
- package/src/edit/hashline/block-resolver.ts +20 -1
- package/src/edit/hashline/diff.ts +36 -1
- package/src/edit/hashline/execute.ts +8 -2
- package/src/edit/index.ts +16 -1
- package/src/edit/modes/patch.ts +52 -0
- package/src/edit/modes/replace.ts +56 -22
- package/src/edit/notebook.ts +22 -2
- package/src/edit/renderer.ts +36 -10
- package/src/eval/__tests__/completion-bridge.test.ts +1 -1
- package/src/eval/backend.ts +0 -2
- package/src/eval/completion-bridge.ts +2 -1
- package/src/eval/idle-timeout.ts +2 -9
- package/src/eval/js/context-manager.ts +6 -8
- package/src/eval/js/executor.ts +6 -2
- package/src/eval/js/index.ts +0 -2
- package/src/eval/js/shared/helpers.ts +5 -6
- package/src/eval/js/shared/local-module-loader.ts +1 -1
- package/src/eval/js/shared/prelude.txt +62 -1
- package/src/eval/js/shared/rewrite-imports.ts +40 -22
- package/src/eval/js/shared/runtime.ts +1 -1
- package/src/eval/py/index.ts +0 -2
- package/src/eval/py/kernel.ts +19 -0
- package/src/eval/py/runner.py +107 -3
- package/src/exec/bash-executor.ts +3 -1
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +3 -1
- package/src/extensibility/extensions/types.ts +3 -2
- package/src/extensibility/plugins/legacy-pi-compat.ts +20 -3
- package/src/hindsight/mental-models.ts +59 -12
- package/src/hindsight/state.ts +6 -1
- package/src/internal-urls/artifact-protocol.ts +11 -2
- package/src/internal-urls/docs-index.generated.ts +8 -8
- package/src/internal-urls/issue-pr-protocol.ts +12 -5
- package/src/internal-urls/router.ts +1 -1
- package/src/internal-urls/types.ts +1 -1
- package/src/lib/xai-http.ts +1 -1
- package/src/lsp/client.ts +118 -38
- package/src/lsp/clients/biome-client.ts +101 -39
- package/src/lsp/edits.ts +143 -95
- package/src/lsp/index.ts +31 -22
- package/src/lsp/render.ts +1 -1
- package/src/lsp/types.ts +2 -0
- package/src/lsp/utils.ts +28 -10
- package/src/main.ts +165 -17
- package/src/mcp/json-rpc.ts +35 -5
- package/src/mcp/transports/stdio.ts +7 -1
- package/src/memories/index.ts +2 -1
- package/src/mnemopi/backend.ts +25 -3
- package/src/mnemopi/state.ts +38 -2
- package/src/modes/components/agent-dashboard.ts +10 -7
- package/src/modes/components/assistant-message.ts +19 -13
- package/src/modes/components/bash-execution.ts +1 -1
- package/src/modes/components/copy-selector.ts +1 -1
- package/src/modes/components/diff.ts +13 -2
- package/src/modes/components/dynamic-border.ts +12 -3
- package/src/modes/components/extensions/extension-dashboard.ts +8 -5
- package/src/modes/components/extensions/extension-list.ts +1 -1
- package/src/modes/components/extensions/inspector-panel.ts +1 -1
- package/src/modes/components/footer.ts +1 -1
- package/src/modes/components/history-search.ts +1 -1
- package/src/modes/components/hook-editor.ts +8 -0
- package/src/modes/components/hook-input.ts +8 -0
- package/src/modes/components/hook-selector.ts +2 -2
- package/src/modes/components/model-selector.ts +4 -2
- package/src/modes/components/plan-review-overlay.ts +1 -1
- package/src/modes/components/session-observer-overlay.ts +2 -2
- package/src/modes/components/session-selector.ts +1 -1
- package/src/modes/components/settings-selector.ts +5 -1
- package/src/modes/components/status-line/component.ts +1 -1
- package/src/modes/components/tiny-title-download-progress.ts +1 -1
- package/src/modes/components/transcript-container.ts +258 -53
- package/src/modes/components/tree-selector.ts +3 -3
- package/src/modes/components/user-message-selector.ts +1 -1
- package/src/modes/components/user-message.ts +17 -5
- package/src/modes/components/visual-truncate.ts +1 -1
- package/src/modes/components/welcome.ts +108 -26
- package/src/modes/controllers/command-controller.ts +10 -3
- package/src/modes/controllers/event-controller.ts +73 -4
- package/src/modes/controllers/input-controller.ts +1 -1
- package/src/modes/controllers/mcp-command-controller.ts +1 -1
- package/src/modes/controllers/selector-controller.ts +1 -1
- package/src/modes/controllers/streaming-reveal.ts +85 -18
- package/src/modes/interactive-mode.ts +3 -9
- package/src/modes/setup-wizard/scenes/glyph.ts +1 -1
- package/src/modes/setup-wizard/scenes/providers.ts +1 -1
- package/src/modes/setup-wizard/scenes/sign-in.ts +1 -1
- package/src/modes/setup-wizard/scenes/theme.ts +1 -1
- package/src/modes/setup-wizard/scenes/types.ts +1 -1
- package/src/modes/setup-wizard/scenes/web-search.ts +1 -1
- package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
- package/src/modes/types.ts +2 -1
- package/src/prompts/agents/explore.md +2 -2
- package/src/prompts/agents/librarian.md +1 -2
- package/src/prompts/agents/oracle.md +1 -1
- package/src/prompts/agents/plan.md +5 -5
- package/src/prompts/agents/task.md +5 -5
- package/src/prompts/ci-green-request.md +5 -7
- package/src/prompts/goals/goal-budget-limit.md +2 -2
- package/src/prompts/goals/goal-continuation.md +4 -4
- package/src/prompts/goals/goal-mode-active.md +1 -1
- package/src/prompts/memories/read-path.md +1 -1
- package/src/prompts/memories/stage_one_system.md +2 -2
- package/src/prompts/review-custom-request.md +1 -1
- package/src/prompts/system/agent-creation-architect.md +2 -2
- package/src/prompts/system/auto-continue.md +1 -1
- package/src/prompts/system/background-tan-dispatch.md +1 -1
- package/src/prompts/system/btw-user.md +2 -2
- package/src/prompts/system/commit-message-system.md +13 -1
- package/src/prompts/system/custom-system-prompt.md +1 -1
- package/src/prompts/system/eager-todo.md +2 -2
- package/src/prompts/system/irc-incoming.md +1 -1
- package/src/prompts/system/manual-continue.md +1 -1
- package/src/prompts/system/omfg-user.md +3 -4
- package/src/prompts/system/orchestrate-notice.md +9 -9
- package/src/prompts/system/plan-mode-active.md +4 -4
- package/src/prompts/system/plan-mode-subagent.md +4 -5
- package/src/prompts/system/plan-mode-tool-decision-reminder.md +1 -1
- package/src/prompts/system/project-prompt.md +2 -2
- package/src/prompts/system/subagent-system-prompt.md +4 -4
- package/src/prompts/system/system-prompt.md +13 -24
- package/src/prompts/system/title-system.md +2 -2
- package/src/prompts/system/ttsr-tool-reminder.md +1 -1
- package/src/prompts/system/workflow-notice.md +1 -1
- package/src/prompts/tools/ast-edit.md +1 -1
- package/src/prompts/tools/ast-grep.md +2 -2
- package/src/prompts/tools/bash.md +5 -7
- package/src/prompts/tools/browser.md +7 -7
- package/src/prompts/tools/debug.md +1 -1
- package/src/prompts/tools/eval.md +3 -3
- package/src/prompts/tools/find.md +0 -1
- package/src/prompts/tools/github.md +8 -7
- package/src/prompts/tools/goal.md +1 -1
- package/src/prompts/tools/image-gen.md +1 -1
- package/src/prompts/tools/inspect-image-system.md +1 -1
- package/src/prompts/tools/irc.md +15 -15
- package/src/prompts/tools/lsp.md +2 -2
- package/src/prompts/tools/patch.md +2 -2
- package/src/prompts/tools/read.md +3 -4
- package/src/prompts/tools/recall.md +1 -1
- package/src/prompts/tools/reflect.md +1 -1
- package/src/prompts/tools/render-mermaid.md +2 -2
- package/src/prompts/tools/replace.md +4 -10
- package/src/prompts/tools/rewind.md +2 -2
- package/src/prompts/tools/search-tool-bm25.md +1 -9
- package/src/prompts/tools/search.md +0 -1
- package/src/prompts/tools/ssh.md +0 -4
- package/src/prompts/tools/task.md +2 -3
- package/src/prompts/tools/todo.md +1 -1
- package/src/sdk.ts +23 -10
- package/src/session/agent-session.ts +44 -10
- package/src/session/auth-broker-config.ts +30 -1
- package/src/session/session-manager.ts +2 -2
- package/src/session/streaming-output.ts +23 -2
- package/src/slash-commands/builtin-registry.ts +20 -0
- package/src/slash-commands/helpers/stats-dashboard.ts +85 -0
- package/src/ssh/connection-manager.ts +27 -0
- package/src/task/commands.ts +2 -1
- package/src/task/executor.ts +61 -53
- package/src/task/index.ts +137 -60
- package/src/task/parallel.ts +3 -3
- package/src/task/render.ts +2 -2
- package/src/task/worktree.ts +64 -56
- package/src/thinking.ts +2 -1
- package/src/tiny/title-client.ts +26 -11
- package/src/tools/archive-reader.ts +30 -2
- package/src/tools/ask.ts +104 -21
- package/src/tools/ast-edit.ts +25 -5
- package/src/tools/auto-generated-guard.ts +20 -3
- package/src/tools/bash-interactive.ts +27 -7
- package/src/tools/bash.ts +54 -13
- package/src/tools/browser/launch.ts +11 -2
- package/src/tools/browser/readable.ts +19 -2
- package/src/tools/browser/registry.ts +4 -1
- package/src/tools/browser/render.ts +2 -2
- package/src/tools/browser/tab-supervisor.ts +55 -16
- package/src/tools/conflict-detect.ts +50 -4
- package/src/tools/debug.ts +1 -1
- package/src/tools/eval-render.ts +5 -5
- package/src/tools/eval.ts +0 -2
- package/src/tools/fetch.ts +33 -10
- package/src/tools/gh-cache-invalidation.ts +63 -8
- package/src/tools/gh-renderer.ts +1 -1
- package/src/tools/gh.ts +172 -29
- package/src/tools/github-cache.ts +70 -6
- package/src/tools/image-gen.ts +3 -9
- package/src/tools/irc.ts +5 -1
- package/src/tools/job.ts +1 -1
- package/src/tools/read.ts +202 -61
- package/src/tools/render-utils.ts +3 -3
- package/src/tools/resolve.ts +1 -1
- package/src/tools/search.ts +92 -29
- package/src/tools/sqlite-reader.ts +17 -5
- package/src/tools/ssh.ts +8 -8
- package/src/tools/todo.ts +38 -8
- package/src/tools/write.ts +118 -18
- package/src/tui/output-block.ts +4 -4
- package/src/utils/changelog.ts +27 -1
- package/src/utils/file-mentions.ts +2 -1
- package/src/web/scrapers/arxiv.ts +1 -1
- package/src/web/scrapers/go-pkg.ts +1 -1
- package/src/web/scrapers/iacr.ts +1 -1
- package/src/web/scrapers/readthedocs.ts +1 -1
- package/src/web/scrapers/twitter.ts +2 -1
- package/src/web/scrapers/types.ts +87 -8
- package/src/web/scrapers/wikipedia.ts +1 -1
- package/src/web/scrapers/youtube.ts +6 -1
- package/src/web/search/index.ts +1 -1
- package/src/web/search/providers/codex.ts +2 -1
- package/src/web/search/providers/gemini.ts +2 -3
- package/src/web/search/render.ts +8 -6
- package/dist/types/config/model-equivalence.d.ts +0 -24
- package/dist/types/config/model-id-affixes.d.ts +0 -12
- package/dist/types/config/model-provider-priority.d.ts +0 -1
- package/dist/types/exec/idle-timeout-watchdog.d.ts +0 -18
- package/src/config/model-equivalence.ts +0 -875
- package/src/config/model-id-affixes.ts +0 -81
- package/src/config/model-provider-priority.ts +0 -56
- package/src/exec/idle-timeout-watchdog.ts +0 -126
package/src/tools/ask.ts
CHANGED
|
@@ -59,6 +59,8 @@ export interface QuestionResult {
|
|
|
59
59
|
multi: boolean;
|
|
60
60
|
selectedOptions: string[];
|
|
61
61
|
customInput?: string;
|
|
62
|
+
/** True when the answer was auto-selected because the dialog timed out. */
|
|
63
|
+
timedOut?: boolean;
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
export interface AskToolDetails {
|
|
@@ -67,6 +69,8 @@ export interface AskToolDetails {
|
|
|
67
69
|
multi?: boolean;
|
|
68
70
|
selectedOptions?: string[];
|
|
69
71
|
customInput?: string;
|
|
72
|
+
/** True when the answer was auto-selected because the dialog timed out. */
|
|
73
|
+
timedOut?: boolean;
|
|
70
74
|
/** Multi-part question mode */
|
|
71
75
|
results?: QuestionResult[];
|
|
72
76
|
}
|
|
@@ -94,6 +98,10 @@ function toSelectOption(option: AskOption, label = option.label): ExtensionUISel
|
|
|
94
98
|
|
|
95
99
|
const OTHER_OPTION = "Other (type your own)";
|
|
96
100
|
const RECOMMENDED_SUFFIX = " (Recommended)";
|
|
101
|
+
// Window after the timeout deadline within which an `undefined` selection is
|
|
102
|
+
// attributed to a UI-enforced timeout (for surfaces that close the dialog at
|
|
103
|
+
// the deadline but never invoke `onTimeout`). Cancels beyond it are user Esc.
|
|
104
|
+
const TIMEOUT_DETECTION_TOLERANCE_MS = 1_000;
|
|
97
105
|
|
|
98
106
|
function getDoneOptionLabel(): string {
|
|
99
107
|
return `${theme.symbol("tool.ask")} Done selecting`;
|
|
@@ -230,7 +238,12 @@ async function askSingleQuestion(
|
|
|
230
238
|
? await untilAborted(signal, () => ui.select(prompt, optionsToShow, dialogOptions))
|
|
231
239
|
: await ui.select(prompt, optionsToShow, dialogOptions);
|
|
232
240
|
if (!timeoutTriggered && choice === undefined && typeof timeout === "number") {
|
|
233
|
-
|
|
241
|
+
// Fallback for UI surfaces that enforce `timeout` without invoking
|
|
242
|
+
// `onTimeout`: their auto-cancel resolves right at the deadline. A
|
|
243
|
+
// cancel arriving well past the deadline is a deliberate user Esc on
|
|
244
|
+
// a surface that kept the dialog open — keep treating it as a cancel.
|
|
245
|
+
const elapsed = Date.now() - startMs;
|
|
246
|
+
timeoutTriggered = elapsed >= timeout && elapsed <= timeout + TIMEOUT_DETECTION_TOLERANCE_MS;
|
|
234
247
|
}
|
|
235
248
|
return { choice, timedOut: timeoutTriggered, navigation: navigationAction };
|
|
236
249
|
};
|
|
@@ -380,9 +393,10 @@ function formatQuestionResult(result: QuestionResult): string {
|
|
|
380
393
|
return `${result.id}: "${result.customInput}"`;
|
|
381
394
|
}
|
|
382
395
|
if (result.selectedOptions.length > 0) {
|
|
396
|
+
const suffix = result.timedOut ? " (auto-selected after timeout)" : "";
|
|
383
397
|
return result.multi
|
|
384
|
-
? `${result.id}: [${result.selectedOptions.join(", ")}]`
|
|
385
|
-
: `${result.id}: ${result.selectedOptions[0]}`;
|
|
398
|
+
? `${result.id}: [${result.selectedOptions.join(", ")}]${suffix}`
|
|
399
|
+
: `${result.id}: ${result.selectedOptions[0]}${suffix}`;
|
|
386
400
|
}
|
|
387
401
|
return `${result.id}: (cancelled)`;
|
|
388
402
|
}
|
|
@@ -519,13 +533,15 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
|
|
|
519
533
|
multi: q.multi ?? false,
|
|
520
534
|
selectedOptions,
|
|
521
535
|
customInput,
|
|
536
|
+
timedOut: timedOut || undefined,
|
|
522
537
|
};
|
|
523
538
|
|
|
524
539
|
const responseParts: string[] = [];
|
|
525
540
|
if (selectedOptions.length > 0) {
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
541
|
+
const selectedText = q.multi
|
|
542
|
+
? `User selected: ${selectedOptions.join(", ")}`
|
|
543
|
+
: `User selected: ${selectedOptions[0]}`;
|
|
544
|
+
responseParts.push(timedOut ? `${selectedText} (auto-selected after timeout)` : selectedText);
|
|
529
545
|
}
|
|
530
546
|
if (customInput !== undefined) {
|
|
531
547
|
responseParts.push(
|
|
@@ -573,6 +589,7 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
|
|
|
573
589
|
multi: q.multi ?? false,
|
|
574
590
|
selectedOptions,
|
|
575
591
|
customInput,
|
|
592
|
+
timedOut: timedOut || undefined,
|
|
576
593
|
};
|
|
577
594
|
|
|
578
595
|
if (navAction === "back") {
|
|
@@ -624,6 +641,56 @@ interface AskRenderArgs {
|
|
|
624
641
|
}>;
|
|
625
642
|
}
|
|
626
643
|
|
|
644
|
+
/**
|
|
645
|
+
* Coerce an untrusted option list (streamed or model-mangled call args) into
|
|
646
|
+
* well-formed render options. Bare strings become labels; entries without a
|
|
647
|
+
* string label are dropped.
|
|
648
|
+
*/
|
|
649
|
+
function normalizeRenderOptions(raw: unknown): AskRenderOption[] | undefined {
|
|
650
|
+
if (!Array.isArray(raw)) return undefined;
|
|
651
|
+
const out: AskRenderOption[] = [];
|
|
652
|
+
for (const entry of raw) {
|
|
653
|
+
if (typeof entry === "string") {
|
|
654
|
+
out.push({ label: entry });
|
|
655
|
+
continue;
|
|
656
|
+
}
|
|
657
|
+
if (!entry || typeof entry !== "object") continue;
|
|
658
|
+
const { label, description } = entry as Partial<AskRenderOption>;
|
|
659
|
+
if (typeof label !== "string") continue;
|
|
660
|
+
out.push(typeof description === "string" ? { label, description } : { label });
|
|
661
|
+
}
|
|
662
|
+
return out;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Coerce untrusted `questions` call args into a renderable array. Models
|
|
667
|
+
* occasionally double-encode the array as a JSON string — a bare string passes
|
|
668
|
+
* a truthy `.length` check but has no `.map`, which used to crash the TUI
|
|
669
|
+
* render loop. Partially streamed args can also be missing fields.
|
|
670
|
+
*/
|
|
671
|
+
function normalizeRenderQuestions(raw: unknown): NonNullable<AskRenderArgs["questions"]> | undefined {
|
|
672
|
+
if (typeof raw === "string") {
|
|
673
|
+
try {
|
|
674
|
+
raw = JSON.parse(raw);
|
|
675
|
+
} catch {
|
|
676
|
+
return undefined;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
if (!Array.isArray(raw)) return undefined;
|
|
680
|
+
const out: NonNullable<AskRenderArgs["questions"]> = [];
|
|
681
|
+
for (const entry of raw) {
|
|
682
|
+
if (!entry || typeof entry !== "object") continue;
|
|
683
|
+
const q = entry as Partial<NonNullable<AskRenderArgs["questions"]>[number]>;
|
|
684
|
+
out.push({
|
|
685
|
+
id: typeof q.id === "string" ? q.id : "?",
|
|
686
|
+
question: typeof q.question === "string" ? q.question : "",
|
|
687
|
+
options: normalizeRenderOptions(q.options) ?? [],
|
|
688
|
+
multi: q.multi === true,
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
return out;
|
|
692
|
+
}
|
|
693
|
+
|
|
627
694
|
/** Render a custom free-text answer as a status line plus indented continuation rows. */
|
|
628
695
|
function renderCustomInputLines(uiTheme: Theme, customInput: string): string[] {
|
|
629
696
|
const lines = customInput.split("\n");
|
|
@@ -707,8 +774,10 @@ export const askToolRenderer = {
|
|
|
707
774
|
new Markdown(text, 1, 0, mdTheme, accentStyle).render(Math.max(1, width - 3 + 1));
|
|
708
775
|
|
|
709
776
|
// Multi-part questions: one divider-labelled section per question.
|
|
710
|
-
|
|
711
|
-
|
|
777
|
+
// Call args are untrusted (partially streamed or model-mangled) and a
|
|
778
|
+
// throw here takes down the whole TUI render loop — normalize first.
|
|
779
|
+
const questions = normalizeRenderQuestions(args.questions);
|
|
780
|
+
if (questions && questions.length > 0) {
|
|
712
781
|
const header = `${label} ${uiTheme.fg("muted", `${questions.length} questions`)}`;
|
|
713
782
|
return framedBlock(uiTheme, width => {
|
|
714
783
|
const sections = questions.map(q => {
|
|
@@ -716,8 +785,11 @@ export const askToolRenderer = {
|
|
|
716
785
|
if (q.multi) meta.push("multi");
|
|
717
786
|
if (q.options?.length) meta.push(`options:${q.options.length}`);
|
|
718
787
|
const metaStr = meta.length > 0 ? uiTheme.fg("dim", ` · ${meta.join(" · ")}`) : "";
|
|
719
|
-
|
|
720
|
-
|
|
788
|
+
// md() returns a shared cached array (module-level Markdown LRU) — copy before appending.
|
|
789
|
+
const mdLines = md(q.question, width);
|
|
790
|
+
const lines = q.options?.length
|
|
791
|
+
? [...mdLines, ...renderQuestionOptionLines(uiTheme, mdTheme, q.options, q.multi)]
|
|
792
|
+
: mdLines;
|
|
721
793
|
return { label: `${uiTheme.fg("dim", `[${q.id}]`)}${metaStr}`, lines };
|
|
722
794
|
});
|
|
723
795
|
return { header, sections, state: "pending", borderColor: "borderMuted", width };
|
|
@@ -725,7 +797,7 @@ export const askToolRenderer = {
|
|
|
725
797
|
}
|
|
726
798
|
|
|
727
799
|
// Single question
|
|
728
|
-
if (!args.question) {
|
|
800
|
+
if (typeof args.question !== "string" || !args.question) {
|
|
729
801
|
const errorLine = formatErrorMessage("No question provided", uiTheme);
|
|
730
802
|
return framedBlock(uiTheme, width => ({
|
|
731
803
|
header: errorLine,
|
|
@@ -739,14 +811,16 @@ export const askToolRenderer = {
|
|
|
739
811
|
const question = args.question;
|
|
740
812
|
const meta: string[] = [];
|
|
741
813
|
if (args.multi) meta.push("multi");
|
|
742
|
-
|
|
814
|
+
const questionOptions = normalizeRenderOptions(args.options);
|
|
815
|
+
if (questionOptions?.length) meta.push(`options:${questionOptions.length}`);
|
|
743
816
|
const header = `${label}${formatMeta(meta, uiTheme)}`;
|
|
744
|
-
const questionOptions = args.options;
|
|
745
817
|
const multi = args.multi;
|
|
746
818
|
return framedBlock(uiTheme, width => {
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
819
|
+
// md() returns a shared cached array (module-level Markdown LRU) — copy before appending.
|
|
820
|
+
const mdLines = md(question, width);
|
|
821
|
+
const bodyLines = questionOptions?.length
|
|
822
|
+
? [...mdLines, ...renderQuestionOptionLines(uiTheme, mdTheme, questionOptions, multi)]
|
|
823
|
+
: mdLines;
|
|
750
824
|
return {
|
|
751
825
|
header,
|
|
752
826
|
sections: bodyLines.length > 0 ? [{ lines: bodyLines }] : [],
|
|
@@ -792,10 +866,11 @@ export const askToolRenderer = {
|
|
|
792
866
|
);
|
|
793
867
|
return framedBlock(uiTheme, width => {
|
|
794
868
|
const sections = results.map(r => {
|
|
795
|
-
|
|
796
|
-
lines
|
|
869
|
+
// md() returns a shared cached array (module-level Markdown LRU) — copy before appending.
|
|
870
|
+
const lines = [
|
|
871
|
+
...md(r.question, width),
|
|
797
872
|
...renderAnswerOptionLines(uiTheme, mdTheme, r.options, r.selectedOptions, r.multi, r.customInput),
|
|
798
|
-
|
|
873
|
+
];
|
|
799
874
|
return { label: uiTheme.fg("dim", `[${r.id}]`), lines };
|
|
800
875
|
});
|
|
801
876
|
return {
|
|
@@ -828,9 +903,17 @@ export const askToolRenderer = {
|
|
|
828
903
|
const dSelected = details.selectedOptions;
|
|
829
904
|
const dMulti = details.multi;
|
|
830
905
|
const dCustom = details.customInput;
|
|
906
|
+
const dTimedOut = details.timedOut;
|
|
831
907
|
return framedBlock(uiTheme, width => {
|
|
832
|
-
|
|
833
|
-
bodyLines
|
|
908
|
+
// md() returns a shared cached array (module-level Markdown LRU) — copy before appending.
|
|
909
|
+
const bodyLines = [
|
|
910
|
+
...md(question, width),
|
|
911
|
+
...renderAnswerOptionLines(uiTheme, mdTheme, dOptions, dSelected, dMulti, dCustom),
|
|
912
|
+
];
|
|
913
|
+
if (dTimedOut) {
|
|
914
|
+
// Distinguish auto-selection from a real user choice in the transcript.
|
|
915
|
+
bodyLines.push(uiTheme.fg("dim", "auto-selected after timeout — not a user choice"));
|
|
916
|
+
}
|
|
834
917
|
return {
|
|
835
918
|
header,
|
|
836
919
|
sections: bodyLines.length > 0 ? [{ lines: bodyLines }] : [],
|
package/src/tools/ast-edit.ts
CHANGED
|
@@ -6,7 +6,7 @@ import type { Component } from "@oh-my-pi/pi-tui";
|
|
|
6
6
|
import { replaceTabs, Text } from "@oh-my-pi/pi-tui";
|
|
7
7
|
import { $envpos, prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
8
8
|
import * as z from "zod/v4";
|
|
9
|
-
import { getFileSnapshotStore } from "../edit/file-snapshot-store";
|
|
9
|
+
import { canonicalSnapshotKey, getFileSnapshotStore } from "../edit/file-snapshot-store";
|
|
10
10
|
import { normalizeToLF } from "../edit/normalize";
|
|
11
11
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
12
12
|
import type { Theme } from "../modes/theme/theme";
|
|
@@ -295,7 +295,7 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
|
|
295
295
|
const absolutePath = path.resolve(this.session.cwd, relativePath);
|
|
296
296
|
try {
|
|
297
297
|
const fullText = normalizeToLF(await Bun.file(absolutePath).text());
|
|
298
|
-
const tag = snapshotStore.record(absolutePath, fullText);
|
|
298
|
+
const tag = snapshotStore.record(canonicalSnapshotKey(absolutePath), fullText);
|
|
299
299
|
hashContexts.set(relativePath, { tag });
|
|
300
300
|
} catch {
|
|
301
301
|
// Best-effort: if a file disappears between ast-edit and rendering, emit plain line output.
|
|
@@ -402,6 +402,23 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
|
|
402
402
|
for (const change of applyResult.changes) {
|
|
403
403
|
recordAppliedFile(formatPath(change.path));
|
|
404
404
|
}
|
|
405
|
+
// The preview minted tags from pre-apply content; the rewrite just
|
|
406
|
+
// invalidated them. Re-record post-apply snapshots (canonical keys)
|
|
407
|
+
// so the model's next hashline edit anchors against fresh tags.
|
|
408
|
+
const freshTagLines: string[] = [];
|
|
409
|
+
if (useHashLines) {
|
|
410
|
+
const snapshotStore = getFileSnapshotStore(this.session);
|
|
411
|
+
for (const relativePath of appliedFileList) {
|
|
412
|
+
const appliedAbsolutePath = path.resolve(this.session.cwd, relativePath);
|
|
413
|
+
try {
|
|
414
|
+
const fullText = normalizeToLF(await Bun.file(appliedAbsolutePath).text());
|
|
415
|
+
const freshTag = snapshotStore.record(canonicalSnapshotKey(appliedAbsolutePath), fullText);
|
|
416
|
+
freshTagLines.push(formatHashlineHeader(relativePath, freshTag));
|
|
417
|
+
} catch {
|
|
418
|
+
// File disappeared between apply and re-read; skip its tag.
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
405
422
|
const appliedFileReplacements = appliedFileList.map(filePath => ({
|
|
406
423
|
path: filePath,
|
|
407
424
|
count: appliedFileReplacementCounts.get(filePath) ?? 0,
|
|
@@ -429,17 +446,20 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
|
|
429
446
|
filePath => fileReplacementCounts.get(filePath) !== appliedFileReplacementCounts.get(filePath),
|
|
430
447
|
);
|
|
431
448
|
if (stalePreview) {
|
|
432
|
-
const
|
|
449
|
+
const staleText =
|
|
433
450
|
applyResult.totalReplacements === 0
|
|
434
451
|
? `Preview is stale / no longer matches; no replacements were applied. Preview expected ${result.totalReplacements} replacement${previewReplacementPlural} in ${result.filesTouched} file${previewFilePlural}.`
|
|
435
452
|
: applyResult.totalReplacements < result.totalReplacements
|
|
436
453
|
? `Preview is stale / no longer matches; only ${applyResult.totalReplacements} of ${result.totalReplacements} replacements were applied in ${applyResult.filesTouched} of ${result.filesTouched} files.`
|
|
437
454
|
: `Preview is stale / no longer matches; applied ${applyResult.totalReplacements} replacements but preview expected ${result.totalReplacements}.`;
|
|
438
|
-
|
|
455
|
+
const staleWithTags =
|
|
456
|
+
freshTagLines.length > 0 ? `${staleText}\n${freshTagLines.join("\n")}` : staleText;
|
|
457
|
+
return { ...toolResult(appliedDetails).text(staleWithTags).done(), isError: true };
|
|
439
458
|
}
|
|
440
459
|
const appliedReplacementPlural = applyResult.totalReplacements !== 1 ? "s" : "";
|
|
441
460
|
const appliedFilePlural = applyResult.filesTouched !== 1 ? "s" : "";
|
|
442
|
-
const
|
|
461
|
+
const appliedText = `Applied ${applyResult.totalReplacements} replacement${appliedReplacementPlural} in ${applyResult.filesTouched} file${appliedFilePlural}.`;
|
|
462
|
+
const text = freshTagLines.length > 0 ? `${appliedText}\n${freshTagLines.join("\n")}` : appliedText;
|
|
443
463
|
return toolResult(appliedDetails).text(text).done();
|
|
444
464
|
},
|
|
445
465
|
});
|
|
@@ -241,15 +241,32 @@ function buildAutoGeneratedError(displayPath: string, detected: string): ToolErr
|
|
|
241
241
|
|
|
242
242
|
const decoder = new TextDecoder("utf-8");
|
|
243
243
|
|
|
244
|
-
const autoGeneratedMap = new LRUCache<string, { marker: string | undefined }>({
|
|
244
|
+
const autoGeneratedMap = new LRUCache<string, { mtimeMs: number; size: number; marker: string | undefined }>({
|
|
245
|
+
max: 10,
|
|
246
|
+
});
|
|
245
247
|
|
|
246
248
|
async function getAutoGeneratedMarker(filePath: string): Promise<string | undefined> {
|
|
247
249
|
if (isAutoGeneratedFileName(filePath)) {
|
|
248
250
|
return filePath.split("/").pop() ?? "";
|
|
249
251
|
}
|
|
250
252
|
|
|
253
|
+
// Key the cache on (mtime, size) so a file rewritten after the first
|
|
254
|
+
// check (generator added/removed) is re-scanned instead of served stale.
|
|
255
|
+
let mtimeMs: number;
|
|
256
|
+
let size: number;
|
|
257
|
+
try {
|
|
258
|
+
const stat = await Bun.file(filePath).stat();
|
|
259
|
+
mtimeMs = stat.mtimeMs;
|
|
260
|
+
size = stat.size;
|
|
261
|
+
} catch (err) {
|
|
262
|
+
if (isEnoent(err)) {
|
|
263
|
+
return undefined;
|
|
264
|
+
}
|
|
265
|
+
throw err;
|
|
266
|
+
}
|
|
267
|
+
|
|
251
268
|
const cached = autoGeneratedMap.get(filePath);
|
|
252
|
-
if (cached) return cached.marker;
|
|
269
|
+
if (cached && cached.mtimeMs === mtimeMs && cached.size === size) return cached.marker;
|
|
253
270
|
|
|
254
271
|
let marker: string | undefined;
|
|
255
272
|
try {
|
|
@@ -262,7 +279,7 @@ async function getAutoGeneratedMarker(filePath: string): Promise<string | undefi
|
|
|
262
279
|
throw err;
|
|
263
280
|
}
|
|
264
281
|
|
|
265
|
-
autoGeneratedMap.set(filePath, { marker });
|
|
282
|
+
autoGeneratedMap.set(filePath, { mtimeMs, size, marker });
|
|
266
283
|
return marker;
|
|
267
284
|
}
|
|
268
285
|
|
|
@@ -11,10 +11,9 @@ import {
|
|
|
11
11
|
visibleWidth,
|
|
12
12
|
} from "@oh-my-pi/pi-tui";
|
|
13
13
|
import { sanitizeText } from "@oh-my-pi/pi-utils";
|
|
14
|
+
import type * as XtermModule from "@xterm/headless";
|
|
14
15
|
import type { Terminal as XtermTerminalType } from "@xterm/headless";
|
|
15
|
-
import xterm from "@xterm/headless";
|
|
16
16
|
import { Settings } from "../config/settings";
|
|
17
|
-
import { NON_INTERACTIVE_ENV } from "../exec/non-interactive-env";
|
|
18
17
|
import type { Theme } from "../modes/theme/theme";
|
|
19
18
|
import { OutputSink, type OutputSummary } from "../session/streaming-output";
|
|
20
19
|
import { sanitizeWithOptionalSixelPassthrough } from "../utils/sixel";
|
|
@@ -32,7 +31,17 @@ function normalizeCaptureChunk(chunk: string): string {
|
|
|
32
31
|
return sanitizeWithOptionalSixelPassthrough(normalized, sanitizeText);
|
|
33
32
|
}
|
|
34
33
|
|
|
35
|
-
|
|
34
|
+
// @xterm/headless is only needed once an interactive PTY session actually starts,
|
|
35
|
+
// so it is loaded lazily (and memoized) instead of weighing down CLI startup.
|
|
36
|
+
let xtermTerminalCtor: typeof XtermModule.Terminal | undefined;
|
|
37
|
+
|
|
38
|
+
async function loadXtermTerminal(): Promise<typeof XtermModule.Terminal> {
|
|
39
|
+
if (!xtermTerminalCtor) {
|
|
40
|
+
const mod = (await import("@xterm/headless")) as typeof XtermModule & { default?: typeof XtermModule };
|
|
41
|
+
xtermTerminalCtor = (mod.default ?? mod).Terminal;
|
|
42
|
+
}
|
|
43
|
+
return xtermTerminalCtor;
|
|
44
|
+
}
|
|
36
45
|
|
|
37
46
|
function normalizeInputForPty(data: string, applicationCursorKeysMode: boolean): string {
|
|
38
47
|
const kitty = parseKittySequence(data);
|
|
@@ -113,8 +122,9 @@ class BashInteractiveOverlayComponent implements Component {
|
|
|
113
122
|
private readonly command: string,
|
|
114
123
|
private readonly uiTheme: Theme,
|
|
115
124
|
private readonly getTerminalRows: () => number,
|
|
125
|
+
terminalCtor: typeof XtermModule.Terminal,
|
|
116
126
|
) {
|
|
117
|
-
this.#terminal = new
|
|
127
|
+
this.#terminal = new terminalCtor({
|
|
118
128
|
cols: 120,
|
|
119
129
|
rows: 40,
|
|
120
130
|
disableStdin: true,
|
|
@@ -224,7 +234,7 @@ class BashInteractiveOverlayComponent implements Component {
|
|
|
224
234
|
}
|
|
225
235
|
return visibleLines;
|
|
226
236
|
}
|
|
227
|
-
render(width: number): string[] {
|
|
237
|
+
render(width: number): readonly string[] {
|
|
228
238
|
const safeWidth = Math.max(20, width);
|
|
229
239
|
const innerWidth = Math.max(1, safeWidth - 2);
|
|
230
240
|
const maxOverlayRows = Math.max(5, Math.floor(this.getTerminalRows() * 0.8));
|
|
@@ -298,6 +308,8 @@ export async function runInteractiveBashPty(
|
|
|
298
308
|
},
|
|
299
309
|
): Promise<BashInteractiveResult> {
|
|
300
310
|
const settings = await Settings.init();
|
|
311
|
+
// Load the xterm Terminal ctor here (async boundary) — the ui.custom factory below is sync.
|
|
312
|
+
const XtermTerminal = await loadXtermTerminal();
|
|
301
313
|
const { shell: resolvedShell } = settings.getShellConfig();
|
|
302
314
|
const sink = new OutputSink({
|
|
303
315
|
artifactPath: options.artifactPath,
|
|
@@ -308,7 +320,12 @@ export async function runInteractiveBashPty(
|
|
|
308
320
|
const result = await ui.custom<BashInteractiveResult>(
|
|
309
321
|
(tui, uiTheme, _keybindings, done) => {
|
|
310
322
|
const session = new PtySession();
|
|
311
|
-
const component = new BashInteractiveOverlayComponent(
|
|
323
|
+
const component = new BashInteractiveOverlayComponent(
|
|
324
|
+
options.command,
|
|
325
|
+
uiTheme,
|
|
326
|
+
() => tui.terminal.rows,
|
|
327
|
+
XtermTerminal,
|
|
328
|
+
);
|
|
312
329
|
component.setSession(session);
|
|
313
330
|
let finished = false;
|
|
314
331
|
const finalize = (run: PtyRunResult) => {
|
|
@@ -358,8 +375,11 @@ export async function runInteractiveBashPty(
|
|
|
358
375
|
command: options.command,
|
|
359
376
|
cwd: options.cwd,
|
|
360
377
|
timeoutMs: options.timeoutMs,
|
|
378
|
+
// Interactive PTY: inherit the user's environment (the Rust side
|
|
379
|
+
// applies these as overrides), with a real TERM so editors,
|
|
380
|
+
// pagers, and TUIs behave like a normal terminal.
|
|
361
381
|
env: {
|
|
362
|
-
|
|
382
|
+
TERM: "xterm-256color",
|
|
363
383
|
...options.env,
|
|
364
384
|
},
|
|
365
385
|
signal: options.signal,
|
package/src/tools/bash.ts
CHANGED
|
@@ -410,10 +410,19 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
410
410
|
*/
|
|
411
411
|
#throwIfUnfinished(result: BashResult | BashInteractiveResult, timeoutSec: number, outputText: string): void {
|
|
412
412
|
if (result.cancelled) {
|
|
413
|
-
|
|
413
|
+
// executeBash output already carries a `[Command cancelled]` notice from
|
|
414
|
+
// the sink; PTY/bridge interactive output does not, so annotate it here.
|
|
415
|
+
const out = normalizeResultOutput(result);
|
|
416
|
+
const annotated = isInteractiveResult(result) && out ? `${out}\n\n[Command aborted]` : out;
|
|
417
|
+
throw new ToolError(annotated || "Command aborted");
|
|
414
418
|
}
|
|
415
419
|
if (isInteractiveResult(result) && result.timedOut) {
|
|
416
|
-
|
|
420
|
+
const out = normalizeResultOutput(result);
|
|
421
|
+
throw new ToolError(
|
|
422
|
+
out
|
|
423
|
+
? `${out}\n\n[Command timed out after ${timeoutSec} seconds]`
|
|
424
|
+
: `Command timed out after ${timeoutSec} seconds`,
|
|
425
|
+
);
|
|
417
426
|
}
|
|
418
427
|
if (result.exitCode === undefined) {
|
|
419
428
|
throw new ToolError(`${outputText}\n\nCommand failed: missing exit status`);
|
|
@@ -669,7 +678,10 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
669
678
|
// script can't pull the entire script into the "cwd" capture.
|
|
670
679
|
if (!cwd) {
|
|
671
680
|
const cdMatch = command.match(/^cd[ \t]+((?:[^&\\\n\r]|\\.)+?)[ \t]*&&[ \t]*/);
|
|
672
|
-
|
|
681
|
+
// Skip extraction when the path needs shell expansion ($VAR, $(...),
|
|
682
|
+
// backticks) — resolveToCwd only expands `~`, so routing those through
|
|
683
|
+
// cwd would reject commands the shell itself handles fine.
|
|
684
|
+
if (cdMatch && !/[$`(]/.test(cdMatch[1])) {
|
|
673
685
|
cwd = cdMatch[1].trim().replace(/^["']|["']$/g, "");
|
|
674
686
|
command = command.slice(cdMatch[0].length);
|
|
675
687
|
}
|
|
@@ -771,8 +783,24 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
771
783
|
});
|
|
772
784
|
}
|
|
773
785
|
|
|
786
|
+
// The client-bridge terminal provides a live terminal card in the editor;
|
|
787
|
+
// when available it wins over auto-backgrounding (both are opt-in, and
|
|
788
|
+
// auto-background would otherwise silently disable the terminal route).
|
|
789
|
+
const clientBridge = this.session.getClientBridge?.();
|
|
790
|
+
const bridgeTerminalAvailable = Boolean(
|
|
791
|
+
clientBridge?.capabilities.terminal && clientBridge.createTerminal && !pty,
|
|
792
|
+
);
|
|
793
|
+
|
|
774
794
|
const autoBgManager = this.session.asyncJobManager;
|
|
775
|
-
|
|
795
|
+
// At the running-job cap, fall through to direct foreground execution
|
|
796
|
+
// instead of failing every bash call until a slot frees up.
|
|
797
|
+
if (
|
|
798
|
+
this.#autoBackgroundEnabled &&
|
|
799
|
+
!pty &&
|
|
800
|
+
!bridgeTerminalAvailable &&
|
|
801
|
+
autoBgManager &&
|
|
802
|
+
!autoBgManager.atCapacity
|
|
803
|
+
) {
|
|
776
804
|
const autoBackgroundWaitMs = this.#resolveAutoBackgroundWaitMs(timeoutMs);
|
|
777
805
|
const startBackgrounded = autoBackgroundWaitMs === 0;
|
|
778
806
|
const job = this.#startManagedBashJob({
|
|
@@ -793,21 +821,23 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
793
821
|
notices: pendingNotices,
|
|
794
822
|
});
|
|
795
823
|
}
|
|
824
|
+
// Suppress the completion delivery up front so a job finishing while we
|
|
825
|
+
// foreground-wait cannot also be injected by the delivery loop. Lifted
|
|
826
|
+
// via resumeDeliveries() if we end up backgrounding after all.
|
|
827
|
+
autoBgManager.acknowledgeDeliveries([job.jobId]);
|
|
796
828
|
const waitResult = await this.#waitForManagedBashJob(job, autoBackgroundWaitMs, signal);
|
|
797
829
|
if (waitResult.kind === "completed") {
|
|
798
|
-
autoBgManager.acknowledgeDeliveries([job.jobId]);
|
|
799
830
|
return waitResult.result;
|
|
800
831
|
}
|
|
801
832
|
if (waitResult.kind === "failed") {
|
|
802
|
-
autoBgManager.acknowledgeDeliveries([job.jobId]);
|
|
803
833
|
throw waitResult.error;
|
|
804
834
|
}
|
|
805
835
|
if (waitResult.kind === "aborted") {
|
|
806
836
|
autoBgManager.cancel(job.jobId);
|
|
807
|
-
autoBgManager.acknowledgeDeliveries([job.jobId]);
|
|
808
837
|
throw new ToolAbortError(job.getLatestText() || "Command aborted");
|
|
809
838
|
}
|
|
810
839
|
job.setBackgrounded(true);
|
|
840
|
+
autoBgManager.resumeDeliveries([job.jobId]);
|
|
811
841
|
return this.#buildBackgroundStartResult(job.jobId, job.label, job.getLatestText(), timeoutSec, {
|
|
812
842
|
requestedTimeoutSec,
|
|
813
843
|
notices: pendingNotices,
|
|
@@ -816,7 +846,6 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
816
846
|
|
|
817
847
|
// Route through the client terminal when the client advertises the terminal capability.
|
|
818
848
|
// Skip when pty=true (PTY needs the local terminal UI).
|
|
819
|
-
const clientBridge = this.session.getClientBridge?.();
|
|
820
849
|
if (clientBridge?.capabilities.terminal && clientBridge.createTerminal && !pty) {
|
|
821
850
|
const bridgeWallTimeStart = performance.now();
|
|
822
851
|
const handle = await clientBridge.createTerminal({
|
|
@@ -993,6 +1022,9 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
993
1022
|
const { path: artifactPath, id: artifactId } = (await this.session.allocateOutputArtifact?.("bash")) ?? {};
|
|
994
1023
|
|
|
995
1024
|
const interactiveUi = canUseInteractiveBashPty(pty, ctx) ? ctx?.ui : undefined;
|
|
1025
|
+
if (pty && !interactiveUi) {
|
|
1026
|
+
pendingNotices.push("pty requested but unavailable in this environment; ran without a terminal");
|
|
1027
|
+
}
|
|
996
1028
|
const wallTimeStart = performance.now();
|
|
997
1029
|
const result: BashResult | BashInteractiveResult = interactiveUi
|
|
998
1030
|
? await runInteractiveBashPty(interactiveUi, {
|
|
@@ -1017,13 +1049,22 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
1017
1049
|
});
|
|
1018
1050
|
const wallTimeMs = performance.now() - wallTimeStart;
|
|
1019
1051
|
if (result.cancelled) {
|
|
1052
|
+
const out = normalizeResultOutput(result);
|
|
1053
|
+
// PTY output carries no cancel/timeout notice of its own; annotate so
|
|
1054
|
+
// the model can tell an abort from a plain failure.
|
|
1055
|
+
const message = isInteractiveResult(result) && out ? `${out}\n\n[Command aborted]` : out || "Command aborted";
|
|
1020
1056
|
if (signal?.aborted) {
|
|
1021
|
-
throw new ToolAbortError(
|
|
1057
|
+
throw new ToolAbortError(message);
|
|
1022
1058
|
}
|
|
1023
|
-
throw new ToolError(
|
|
1059
|
+
throw new ToolError(message);
|
|
1024
1060
|
}
|
|
1025
1061
|
if (isInteractiveResult(result) && result.timedOut) {
|
|
1026
|
-
|
|
1062
|
+
const out = normalizeResultOutput(result);
|
|
1063
|
+
throw new ToolError(
|
|
1064
|
+
out
|
|
1065
|
+
? `${out}\n\n[Command timed out after ${timeoutSec} seconds]`
|
|
1066
|
+
: `Command timed out after ${timeoutSec} seconds`,
|
|
1067
|
+
);
|
|
1027
1068
|
}
|
|
1028
1069
|
return this.#buildCompletedResult(result, timeoutSec, {
|
|
1029
1070
|
requestedTimeoutSec,
|
|
@@ -1122,7 +1163,7 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
|
|
|
1122
1163
|
: renderStatusLine({ icon: "pending", title: config.resolveTitle(args, options) }, uiTheme);
|
|
1123
1164
|
const outputBlock = new CachedOutputBlock();
|
|
1124
1165
|
return markFramedBlockComponent({
|
|
1125
|
-
render: (width: number): string[] =>
|
|
1166
|
+
render: (width: number): readonly string[] =>
|
|
1126
1167
|
outputBlock.render(
|
|
1127
1168
|
{
|
|
1128
1169
|
header,
|
|
@@ -1172,7 +1213,7 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
|
|
|
1172
1213
|
const outputBlock = new CachedOutputBlock();
|
|
1173
1214
|
|
|
1174
1215
|
return markFramedBlockComponent({
|
|
1175
|
-
render: (width: number): string[] => {
|
|
1216
|
+
render: (width: number): readonly string[] => {
|
|
1176
1217
|
// REACTIVE: read mutable options at render time
|
|
1177
1218
|
const { renderContext } = options;
|
|
1178
1219
|
const expanded = renderContext?.expanded ?? options.expanded;
|
|
@@ -2,9 +2,8 @@ import * as fs from "node:fs";
|
|
|
2
2
|
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
|
-
import * as
|
|
5
|
+
import type * as BrowsersNs from "@puppeteer/browsers";
|
|
6
6
|
import type { Browser, CDPSession, Page, default as Puppeteer, Target } from "puppeteer-core";
|
|
7
|
-
import { PUPPETEER_REVISIONS } from "puppeteer-core/internal/revisions.js";
|
|
8
7
|
import stealthTamperingScript from "../puppeteer/00_stealth_tampering.txt" with { type: "text" };
|
|
9
8
|
import stealthActivityScript from "../puppeteer/01_stealth_activity.txt" with { type: "text" };
|
|
10
9
|
import stealthHairlineScript from "../puppeteer/02_stealth_hairline.txt" with { type: "text" };
|
|
@@ -78,6 +77,14 @@ export async function loadPuppeteerInWorker(safeDir: string): Promise<typeof Pup
|
|
|
78
77
|
}
|
|
79
78
|
}
|
|
80
79
|
|
|
80
|
+
let browsersModule: typeof BrowsersNs | undefined;
|
|
81
|
+
async function loadBrowsers(): Promise<typeof BrowsersNs> {
|
|
82
|
+
if (!browsersModule) {
|
|
83
|
+
browsersModule = await import("@puppeteer/browsers");
|
|
84
|
+
}
|
|
85
|
+
return browsersModule;
|
|
86
|
+
}
|
|
87
|
+
|
|
81
88
|
/**
|
|
82
89
|
* Lazily download Chromium on first browser launch via @puppeteer/browsers.
|
|
83
90
|
* Skipped when a system Chromium (NixOS) or PUPPETEER_EXECUTABLE_PATH is set.
|
|
@@ -92,12 +99,14 @@ async function ensureChromiumExecutable(): Promise<string | undefined> {
|
|
|
92
99
|
if (chromiumExecutablePromise) return chromiumExecutablePromise;
|
|
93
100
|
|
|
94
101
|
chromiumExecutablePromise = (async () => {
|
|
102
|
+
const browsers = await loadBrowsers();
|
|
95
103
|
const platform = browsers.detectBrowserPlatform();
|
|
96
104
|
if (!platform) {
|
|
97
105
|
logger.warn("Could not detect browser platform; relying on puppeteer default resolution");
|
|
98
106
|
return undefined;
|
|
99
107
|
}
|
|
100
108
|
const cacheDir = getPuppeteerDir();
|
|
109
|
+
const { PUPPETEER_REVISIONS } = await import("puppeteer-core/internal/revisions.js");
|
|
101
110
|
const buildId = await browsers.resolveBuildId(browsers.Browser.CHROME, platform, PUPPETEER_REVISIONS.chrome);
|
|
102
111
|
const executablePath = browsers.computeExecutablePath({
|
|
103
112
|
browser: browsers.Browser.CHROME,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import type * as ReadabilityNs from "@mozilla/readability";
|
|
2
|
+
import type * as LinkedomNs from "linkedom";
|
|
3
3
|
import { htmlToBasicMarkdown } from "../../web/scrapers/types";
|
|
4
4
|
|
|
5
5
|
export type ReadableFormat = "text" | "markdown";
|
|
@@ -20,6 +20,22 @@ function normalize(text: string | null | undefined): string | undefined {
|
|
|
20
20
|
return trimmed || undefined;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
let readabilityModule: typeof ReadabilityNs | undefined;
|
|
24
|
+
async function loadReadability(): Promise<typeof ReadabilityNs> {
|
|
25
|
+
if (!readabilityModule) {
|
|
26
|
+
readabilityModule = await import("@mozilla/readability");
|
|
27
|
+
}
|
|
28
|
+
return readabilityModule;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let linkedomModule: typeof LinkedomNs | undefined;
|
|
32
|
+
async function loadLinkedom(): Promise<typeof LinkedomNs> {
|
|
33
|
+
if (!linkedomModule) {
|
|
34
|
+
linkedomModule = await import("linkedom");
|
|
35
|
+
}
|
|
36
|
+
return linkedomModule;
|
|
37
|
+
}
|
|
38
|
+
|
|
23
39
|
/**
|
|
24
40
|
* Extract readable content from raw HTML.
|
|
25
41
|
* Tries Readability (article-isolation scoring) first, then falls back to a
|
|
@@ -31,6 +47,7 @@ export async function extractReadableFromHtml(
|
|
|
31
47
|
url: string,
|
|
32
48
|
format: ReadableFormat,
|
|
33
49
|
): Promise<ReadableResult | null> {
|
|
50
|
+
const [{ parseHTML }, { Readability }] = await Promise.all([loadLinkedom(), loadReadability()]);
|
|
34
51
|
const { document } = parseHTML(html);
|
|
35
52
|
|
|
36
53
|
// --- Primary: Readability article extraction ---
|
|
@@ -157,7 +157,10 @@ export function holdBrowser(handle: BrowserHandle): void {
|
|
|
157
157
|
export async function releaseBrowser(handle: BrowserHandle, opts: { kill: boolean }): Promise<void> {
|
|
158
158
|
handle.refCount = Math.max(0, handle.refCount - 1);
|
|
159
159
|
if (handle.refCount === 0) {
|
|
160
|
-
|
|
160
|
+
// Only evict if the registry still points at THIS handle. After a disconnect,
|
|
161
|
+
// `acquireBrowser` may have already replaced the entry with a fresh live handle
|
|
162
|
+
// under the same key; deleting blindly would orphan that new browser.
|
|
163
|
+
if (browsers.get(handle.key) === handle) browsers.delete(handle.key);
|
|
161
164
|
await disposeBrowserHandle(handle, opts);
|
|
162
165
|
}
|
|
163
166
|
}
|