@oh-my-pi/pi-coding-agent 15.10.9 → 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 +117 -0
- 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 +20 -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 -16
- 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/debug/terminal-info.d.ts +0 -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 +31 -26
- 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/discovery.d.ts +1 -2
- package/dist/types/task/parallel.d.ts +2 -2
- package/dist/types/task/worktree.d.ts +2 -0
- package/dist/types/tiny/title-client.d.ts +1 -1
- 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/tools/todo.d.ts +2 -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 +7 -12
- 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 +308 -1025
- 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 -14
- 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/debug/terminal-info.ts +0 -3
- package/src/edit/diff.ts +95 -18
- 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 +49 -23
- 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 +10 -10
- 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 +66 -54
- 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 +373 -141
- 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 -49
- package/src/modes/controllers/input-controller.ts +5 -5
- package/src/modes/controllers/mcp-command-controller.ts +1 -1
- package/src/modes/controllers/selector-controller.ts +1 -5
- package/src/modes/controllers/streaming-reveal.ts +85 -18
- package/src/modes/interactive-mode.ts +5 -19
- 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 +15 -26
- 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 +8 -10
- 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 +6 -2
- 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/discovery.ts +17 -24
- 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 +32 -14
- 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 +51 -12
- 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/anthropic.ts +8 -2
- 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
|
@@ -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
|
}
|
|
@@ -66,7 +66,7 @@ function dropTrailingBlankLines(text: string): string {
|
|
|
66
66
|
function appendLine(component: Component, line: string | undefined): Component {
|
|
67
67
|
if (!line) return component;
|
|
68
68
|
const wrapped = {
|
|
69
|
-
render: (width: number): string[] => {
|
|
69
|
+
render: (width: number): readonly string[] => {
|
|
70
70
|
const base = component.render(width);
|
|
71
71
|
return [...base, line];
|
|
72
72
|
},
|
|
@@ -95,7 +95,7 @@ function renderRunCell(
|
|
|
95
95
|
|
|
96
96
|
let cached: { key: bigint; width: number; lines: string[] } | undefined;
|
|
97
97
|
return markFramedBlockComponent({
|
|
98
|
-
render: (width: number): string[] => {
|
|
98
|
+
render: (width: number): readonly string[] => {
|
|
99
99
|
const expanded = options.renderContext?.expanded ?? options.expanded;
|
|
100
100
|
const previewLines = options.renderContext?.previewLines ?? BROWSER_DEFAULT_PREVIEW_LINES;
|
|
101
101
|
const key = new Hasher()
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getPuppeteerDir,
|
|
1
|
+
import { getPuppeteerDir, logger, Snowflake, workerHostEntry } from "@oh-my-pi/pi-utils";
|
|
2
2
|
import type { Page, Target } from "puppeteer-core";
|
|
3
3
|
import { callSessionTool } from "../../eval/js/tool-bridge";
|
|
4
4
|
import type { ToolSession } from "../../sdk";
|
|
@@ -18,14 +18,8 @@ import type {
|
|
|
18
18
|
WorkerOutbound,
|
|
19
19
|
} from "./tab-protocol";
|
|
20
20
|
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
// (registered as an additional entrypoint in `scripts/build-binary.ts`); in
|
|
24
|
-
// dev we resolve the same source via `import.meta.url`. Replaces the older
|
|
25
|
-
// `with { type: "file" }` pattern, which only copied the entry as a raw
|
|
26
|
-
// asset and could not resolve the worker's relative imports inside a
|
|
27
|
-
// compiled binary (issue #1011 was a false-positive fix — the regression
|
|
28
|
-
// test only checked emission, not actual worker startup).
|
|
21
|
+
// Coding-agent binary/bundle workers route through the CLI entrypoint with a
|
|
22
|
+
// hidden argv mode, so compiled/npm builds only need one JavaScript entry.
|
|
29
23
|
|
|
30
24
|
interface WorkerHandle {
|
|
31
25
|
send(msg: WorkerInbound, transferList?: Transferable[]): void;
|
|
@@ -84,21 +78,51 @@ export interface ReleaseTabOptions {
|
|
|
84
78
|
}
|
|
85
79
|
|
|
86
80
|
const tabs = new Map<string, TabSession>();
|
|
81
|
+
// Per-name acquisition chain: serializes concurrent `acquireTab` calls for the
|
|
82
|
+
// same tab name so the existence check and `tabs.set` (separated by several
|
|
83
|
+
// awaits) cannot interleave and leak a worker + browser refCount.
|
|
84
|
+
const acquireChains = new Map<string, Promise<void>>();
|
|
87
85
|
const GRACE_MS = 750;
|
|
88
86
|
|
|
89
87
|
export function getTab(name: string): TabSession | undefined {
|
|
90
88
|
return tabs.get(name);
|
|
91
89
|
}
|
|
92
90
|
|
|
93
|
-
export
|
|
91
|
+
export function acquireTab(name: string, browser: BrowserHandle, opts: AcquireTabOptions): Promise<AcquireTabResult> {
|
|
92
|
+
const prior = acquireChains.get(name) ?? Promise.resolve();
|
|
93
|
+
const result = prior.then(() => acquireTabImpl(name, browser, opts));
|
|
94
|
+
const tail = result.then(
|
|
95
|
+
() => undefined,
|
|
96
|
+
() => undefined,
|
|
97
|
+
);
|
|
98
|
+
acquireChains.set(name, tail);
|
|
99
|
+
void tail.then(() => {
|
|
100
|
+
if (acquireChains.get(name) === tail) acquireChains.delete(name);
|
|
101
|
+
});
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function acquireTabImpl(
|
|
94
106
|
name: string,
|
|
95
107
|
browser: BrowserHandle,
|
|
96
108
|
opts: AcquireTabOptions,
|
|
97
109
|
): Promise<AcquireTabResult> {
|
|
110
|
+
// Serialized opens can sit behind a slow predecessor in the per-name
|
|
111
|
+
// chain; honor an abort at dequeue instead of spawning a worker and
|
|
112
|
+
// browser hold nobody is waiting for.
|
|
113
|
+
if (opts.signal?.aborted) {
|
|
114
|
+
throw new ToolAbortError("Browser tab open aborted");
|
|
115
|
+
}
|
|
116
|
+
// Temporary refCount hold so releasing an existing tab on the SAME browser
|
|
117
|
+
// below cannot drop it to refCount 0 and dispose the instance we are about
|
|
118
|
+
// to reuse (e.g. reopening the sole tab with a different dialogs policy).
|
|
119
|
+
let tempHold = false;
|
|
98
120
|
const existing = tabs.get(name);
|
|
99
121
|
if (existing) {
|
|
100
122
|
if (existing.browser === browser && existing.state === "alive") {
|
|
101
123
|
if (opts.dialogs !== undefined && opts.dialogs !== existing.dialogPolicy) {
|
|
124
|
+
holdBrowser(browser);
|
|
125
|
+
tempHold = true;
|
|
102
126
|
await releaseTab(name, { kill: false });
|
|
103
127
|
} else {
|
|
104
128
|
const reuseSteps: string[] = [];
|
|
@@ -127,12 +151,25 @@ export async function acquireTab(
|
|
|
127
151
|
return { tab: tabs.get(name)!, created: false };
|
|
128
152
|
}
|
|
129
153
|
} else {
|
|
154
|
+
if (existing.browser === browser) {
|
|
155
|
+
holdBrowser(browser);
|
|
156
|
+
tempHold = true;
|
|
157
|
+
}
|
|
130
158
|
await releaseTab(name, { kill: false });
|
|
131
159
|
}
|
|
132
160
|
}
|
|
133
161
|
|
|
134
|
-
|
|
135
|
-
let worker
|
|
162
|
+
let initPayload: WorkerInitPayload;
|
|
163
|
+
let worker: WorkerHandle;
|
|
164
|
+
try {
|
|
165
|
+
initPayload = await buildInitPayload(browser, opts);
|
|
166
|
+
worker = await spawnTabWorker();
|
|
167
|
+
} catch (error) {
|
|
168
|
+
// Failing before the worker took its own hold must release the
|
|
169
|
+
// temporary one, or the browser's refCount never reaches 0 again.
|
|
170
|
+
if (tempHold || browser.refCount === 0) await releaseBrowser(browser, { kill: false });
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
136
173
|
let info: ReadyInfo;
|
|
137
174
|
try {
|
|
138
175
|
info = await initializeTabWorker(worker, initPayload, opts.timeoutMs + GRACE_MS);
|
|
@@ -142,7 +179,7 @@ export async function acquireTab(
|
|
|
142
179
|
// the inline worker here so module-resolution failures don't poison every tab open.
|
|
143
180
|
await worker.terminate().catch(() => undefined);
|
|
144
181
|
if (worker.mode === "inline") {
|
|
145
|
-
if (browser.refCount === 0) await releaseBrowser(browser, { kill: false });
|
|
182
|
+
if (tempHold || browser.refCount === 0) await releaseBrowser(browser, { kill: false });
|
|
146
183
|
throw error;
|
|
147
184
|
}
|
|
148
185
|
logger.warn("Tab worker init failed; retrying with inline tab worker (no sync-loop guard)", {
|
|
@@ -153,7 +190,7 @@ export async function acquireTab(
|
|
|
153
190
|
info = await initializeTabWorker(worker, initPayload, opts.timeoutMs + GRACE_MS);
|
|
154
191
|
} catch (inlineError) {
|
|
155
192
|
await worker.terminate().catch(() => undefined);
|
|
156
|
-
if (browser.refCount === 0) await releaseBrowser(browser, { kill: false });
|
|
193
|
+
if (tempHold || browser.refCount === 0) await releaseBrowser(browser, { kill: false });
|
|
157
194
|
const finalError = new ToolError(
|
|
158
195
|
`Failed to start browser tab worker (inline fallback also failed): ${inlineError instanceof Error ? inlineError.message : String(inlineError)}`,
|
|
159
196
|
);
|
|
@@ -163,6 +200,7 @@ export async function acquireTab(
|
|
|
163
200
|
}
|
|
164
201
|
|
|
165
202
|
holdBrowser(browser);
|
|
203
|
+
if (tempHold) await releaseBrowser(browser, { kill: false });
|
|
166
204
|
const tab: TabSession = {
|
|
167
205
|
name,
|
|
168
206
|
browser,
|
|
@@ -474,8 +512,9 @@ async function raceWithTimeout<T>(
|
|
|
474
512
|
|
|
475
513
|
async function spawnTabWorker(): Promise<WorkerHandle> {
|
|
476
514
|
try {
|
|
477
|
-
const
|
|
478
|
-
|
|
515
|
+
const hostEntry = workerHostEntry();
|
|
516
|
+
const worker = hostEntry
|
|
517
|
+
? new Worker(hostEntry, { type: "module", argv: ["__omp_tab_worker"] })
|
|
479
518
|
: new Worker(new URL("./tab-worker-entry.ts", import.meta.url).href, { type: "module" });
|
|
480
519
|
return wrapBunWorker(worker);
|
|
481
520
|
} catch (err) {
|
|
@@ -68,7 +68,9 @@ export function scanConflictLines(lines: readonly string[], firstLineNumber: num
|
|
|
68
68
|
} | null = null;
|
|
69
69
|
|
|
70
70
|
for (let i = 0; i < lines.length; i++) {
|
|
71
|
-
|
|
71
|
+
// Strip a trailing \r so CRLF checkouts match the same markers; stored
|
|
72
|
+
// section lines are LF-normalized (splice re-applies \r on write).
|
|
73
|
+
const line = stripTrailingCr(lines[i]);
|
|
72
74
|
const ln = firstLineNumber + i;
|
|
73
75
|
|
|
74
76
|
const oursLabel = matchMarker(line, OURS_PREFIX);
|
|
@@ -338,13 +340,22 @@ export function spliceConflict(originalText: string, entry: ConflictEntry, repla
|
|
|
338
340
|
}
|
|
339
341
|
|
|
340
342
|
const trimmed = normalizeTrailingNewline(replacement);
|
|
341
|
-
|
|
343
|
+
let replacementLines = trimmed.split("\n").map(stripTrailingCr);
|
|
344
|
+
// Round-trip fidelity for CRLF files: recorded sections are LF-normalized,
|
|
345
|
+
// so re-apply \r to spliced lines when the matched region used CRLF. The
|
|
346
|
+
// final replacement line only carries \r when another line follows it.
|
|
347
|
+
if (lines[match.startIdx]!.endsWith("\r")) {
|
|
348
|
+
const hasFollowingLine = match.endIdx + 1 < lines.length;
|
|
349
|
+
replacementLines = replacementLines.map((l, i) =>
|
|
350
|
+
i < replacementLines.length - 1 || hasFollowingLine ? `${l}\r` : l,
|
|
351
|
+
);
|
|
352
|
+
}
|
|
342
353
|
const next = [...lines.slice(0, match.startIdx), ...replacementLines, ...lines.slice(match.endIdx + 1)];
|
|
343
354
|
return next.join("\n");
|
|
344
355
|
}
|
|
345
356
|
|
|
346
357
|
/** Reconstruct the recorded marker block as it should appear in the file. */
|
|
347
|
-
function buildRecordedRegion(entry:
|
|
358
|
+
function buildRecordedRegion(entry: ConflictBlock): string[] {
|
|
348
359
|
const out: string[] = [];
|
|
349
360
|
out.push(entry.oursLabel ? `${OURS_PREFIX} ${entry.oursLabel}` : OURS_PREFIX);
|
|
350
361
|
out.push(...entry.oursLines);
|
|
@@ -358,6 +369,36 @@ function buildRecordedRegion(entry: ConflictEntry): string[] {
|
|
|
358
369
|
return out;
|
|
359
370
|
}
|
|
360
371
|
|
|
372
|
+
/**
|
|
373
|
+
* True when two registered blocks record the same marker-block content
|
|
374
|
+
* (labels and all sides). Out-of-band edits can shift a block's line
|
|
375
|
+
* numbers between reads, registering a fresh id while the stale one
|
|
376
|
+
* persists; callers use content identity to treat a locate-miss for the
|
|
377
|
+
* stale twin as "already resolved" instead of a hard failure.
|
|
378
|
+
*/
|
|
379
|
+
export function conflictRegionsEqual(a: ConflictBlock, b: ConflictBlock): boolean {
|
|
380
|
+
const ra = buildRecordedRegion(a);
|
|
381
|
+
const rb = buildRecordedRegion(b);
|
|
382
|
+
if (ra.length !== rb.length) return false;
|
|
383
|
+
for (let i = 0; i < ra.length; i++) {
|
|
384
|
+
if (ra[i] !== rb[i]) return false;
|
|
385
|
+
}
|
|
386
|
+
return true;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* True when the entry's recorded marker block still occurs in `content`
|
|
391
|
+
* (LF-normalized — recorded sections are stored LF). Distinguishes a stale
|
|
392
|
+
* re-registration of a just-resolved region (no longer present) from a
|
|
393
|
+
* DISTINCT conflict block that happens to be byte-identical (still present
|
|
394
|
+
* elsewhere in the file and must stay addressable).
|
|
395
|
+
*/
|
|
396
|
+
export function conflictRegionPresent(content: string, entry: ConflictBlock): boolean {
|
|
397
|
+
const region = buildRecordedRegion(entry).join("\n");
|
|
398
|
+
const normalized = content.includes("\r") ? content.replace(/\r\n/g, "\n") : content;
|
|
399
|
+
return normalized.includes(region);
|
|
400
|
+
}
|
|
401
|
+
|
|
361
402
|
/**
|
|
362
403
|
* Find a contiguous match of `expected` inside `lines`, preferring the
|
|
363
404
|
* occurrence closest to `preferredIdx` to disambiguate when an identical
|
|
@@ -391,11 +432,16 @@ function locateRegion(
|
|
|
391
432
|
function matchesAt(lines: readonly string[], startIdx: number, expected: readonly string[]): boolean {
|
|
392
433
|
if (startIdx < 0 || startIdx + expected.length > lines.length) return false;
|
|
393
434
|
for (let i = 0; i < expected.length; i++) {
|
|
394
|
-
|
|
435
|
+
// Recorded lines are LF-normalized; tolerate CRLF on-disk lines.
|
|
436
|
+
if (stripTrailingCr(lines[startIdx + i]!) !== expected[i]) return false;
|
|
395
437
|
}
|
|
396
438
|
return true;
|
|
397
439
|
}
|
|
398
440
|
|
|
441
|
+
function stripTrailingCr(line: string): string {
|
|
442
|
+
return line.endsWith("\r") ? line.slice(0, -1) : line;
|
|
443
|
+
}
|
|
444
|
+
|
|
399
445
|
function normalizeTrailingNewline(replacement: string): string {
|
|
400
446
|
if (replacement.endsWith("\r\n")) return replacement.slice(0, -2);
|
|
401
447
|
if (replacement.endsWith("\n")) return replacement.slice(0, -1);
|
package/src/tools/debug.ts
CHANGED
|
@@ -592,7 +592,7 @@ export const debugToolRenderer = {
|
|
|
592
592
|
): Component {
|
|
593
593
|
const outputBlock = new CachedOutputBlock();
|
|
594
594
|
return markFramedBlockComponent({
|
|
595
|
-
render(width: number): string[] {
|
|
595
|
+
render(width: number): readonly string[] {
|
|
596
596
|
const action = (args?.action ?? result.details?.action ?? "debug").replaceAll("_", " ");
|
|
597
597
|
const success = !options.isPartial && !result.isError;
|
|
598
598
|
const statusIcon = success
|