@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
package/src/lsp/edits.ts
CHANGED
|
@@ -24,27 +24,7 @@ import { uriToFile } from "./utils";
|
|
|
24
24
|
*/
|
|
25
25
|
export function applyTextEditsToString(content: string, edits: TextEdit[]): string {
|
|
26
26
|
const lines = content.split("\n");
|
|
27
|
-
|
|
28
|
-
// Sort edits in reverse order (bottom-to-top, right-to-left)
|
|
29
|
-
const sortedEdits = [...edits].sort((a, b) => {
|
|
30
|
-
if (a.range.start.line !== b.range.start.line) {
|
|
31
|
-
return b.range.start.line - a.range.start.line;
|
|
32
|
-
}
|
|
33
|
-
return b.range.start.character - a.range.start.character;
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
// Detect overlapping ranges: in reverse-sorted order, each edit's start
|
|
37
|
-
// must be >= the next edit's end. If not, the edits would clobber each other
|
|
38
|
-
// once applied bottom-up (typically a multi-server rename with stale positions).
|
|
39
|
-
for (let i = 0; i < sortedEdits.length - 1; i++) {
|
|
40
|
-
const later = sortedEdits[i].range;
|
|
41
|
-
const earlier = sortedEdits[i + 1].range;
|
|
42
|
-
if (comparePosition(earlier.end, later.start) > 0) {
|
|
43
|
-
throw new ToolError(
|
|
44
|
-
`overlapping LSP edits: ${formatRange(earlier)} conflicts with ${formatRange(later)}; multi-server rename produced inconsistent edits`,
|
|
45
|
-
);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
27
|
+
const sortedEdits = sortAndValidateTextEdits(edits);
|
|
48
28
|
|
|
49
29
|
for (const edit of sortedEdits) {
|
|
50
30
|
const { start, end } = edit.range;
|
|
@@ -78,6 +58,42 @@ export function rangesOverlap(a: Range, b: Range): boolean {
|
|
|
78
58
|
return comparePosition(a.start, b.end) < 0 && comparePosition(b.start, a.end) < 0;
|
|
79
59
|
}
|
|
80
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Sort edits bottom-to-top for in-place application and reject overlaps.
|
|
63
|
+
* Equal start positions tiebreak by original array index descending so that,
|
|
64
|
+
* applied bottom-up, inserts at the same position land in array order
|
|
65
|
+
* (LSP spec: the order of edits in the array defines the order in the result).
|
|
66
|
+
*/
|
|
67
|
+
export function sortAndValidateTextEdits(edits: TextEdit[]): TextEdit[] {
|
|
68
|
+
const sorted = edits
|
|
69
|
+
.map((edit, index) => ({ edit, index }))
|
|
70
|
+
.sort((a, b) => {
|
|
71
|
+
if (a.edit.range.start.line !== b.edit.range.start.line) {
|
|
72
|
+
return b.edit.range.start.line - a.edit.range.start.line;
|
|
73
|
+
}
|
|
74
|
+
if (a.edit.range.start.character !== b.edit.range.start.character) {
|
|
75
|
+
return b.edit.range.start.character - a.edit.range.start.character;
|
|
76
|
+
}
|
|
77
|
+
return b.index - a.index;
|
|
78
|
+
})
|
|
79
|
+
.map(entry => entry.edit);
|
|
80
|
+
|
|
81
|
+
// Detect overlapping ranges: in reverse-sorted order, each edit's start
|
|
82
|
+
// must be >= the next edit's end. If not, the edits would clobber each other
|
|
83
|
+
// once applied bottom-up (typically a multi-server rename with stale positions).
|
|
84
|
+
for (let i = 0; i < sorted.length - 1; i++) {
|
|
85
|
+
const later = sorted[i].range;
|
|
86
|
+
const earlier = sorted[i + 1].range;
|
|
87
|
+
if (comparePosition(earlier.end, later.start) > 0) {
|
|
88
|
+
throw new ToolError(
|
|
89
|
+
`overlapping LSP edits: ${formatRange(earlier)} conflicts with ${formatRange(later)}; multi-server rename produced inconsistent edits`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return sorted;
|
|
95
|
+
}
|
|
96
|
+
|
|
81
97
|
/**
|
|
82
98
|
* Flatten a WorkspaceEdit's text edits into a Map<uri, TextEdit[]>.
|
|
83
99
|
* Resource operations (create/rename/delete) are ignored — callers handle them separately.
|
|
@@ -120,92 +136,124 @@ export async function applyTextEdits(filePath: string, edits: TextEdit[]): Promi
|
|
|
120
136
|
// Workspace Edit Application
|
|
121
137
|
// =============================================================================
|
|
122
138
|
|
|
139
|
+
type WorkspaceEditOp =
|
|
140
|
+
| { kind: "text"; uri: string; edits: TextEdit[] }
|
|
141
|
+
| { kind: "create"; uri: string }
|
|
142
|
+
| { kind: "rename"; oldUri: string; newUri: string }
|
|
143
|
+
| { kind: "delete"; uri: string };
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Flatten documentChanges into an ordered op list. Text edits are accumulated
|
|
147
|
+
* per-URI and flushed before any resource op that touches the same URI (or,
|
|
148
|
+
* for folder rename/delete, any descendant URI) so that renames, creates, and
|
|
149
|
+
* deletes always see the correct prior file state.
|
|
150
|
+
*/
|
|
151
|
+
function planDocumentChanges(documentChanges: NonNullable<WorkspaceEdit["documentChanges"]>): WorkspaceEditOp[] {
|
|
152
|
+
const ops: WorkspaceEditOp[] = [];
|
|
153
|
+
const pending = new Map<string, TextEdit[]>();
|
|
154
|
+
|
|
155
|
+
const flushUri = (uri: string) => {
|
|
156
|
+
const edits = pending.get(uri);
|
|
157
|
+
if (!edits) return;
|
|
158
|
+
pending.delete(uri);
|
|
159
|
+
ops.push({ kind: "text", uri, edits });
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// Flush the exact URI plus every pending descendant (for folder-level
|
|
163
|
+
// resource ops where the queued edits target child files of the target).
|
|
164
|
+
const flushSubtree = (uri: string) => {
|
|
165
|
+
const prefix = uri.endsWith("/") ? uri : `${uri}/`;
|
|
166
|
+
const matches: string[] = [];
|
|
167
|
+
for (const candidate of pending.keys()) {
|
|
168
|
+
if (candidate === uri || candidate.startsWith(prefix)) matches.push(candidate);
|
|
169
|
+
}
|
|
170
|
+
for (const target of matches) {
|
|
171
|
+
flushUri(target);
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
for (const change of documentChanges) {
|
|
176
|
+
if ("textDocument" in change && change.textDocument && "edits" in change && change.edits) {
|
|
177
|
+
const tdc = change as TextDocumentEdit;
|
|
178
|
+
const uri = tdc.textDocument.uri;
|
|
179
|
+
const textEdits = tdc.edits.filter((e): e is TextEdit => "range" in e && "newText" in e);
|
|
180
|
+
if (textEdits.length > 0) {
|
|
181
|
+
const prev = pending.get(uri);
|
|
182
|
+
if (prev) prev.push(...textEdits);
|
|
183
|
+
else pending.set(uri, [...textEdits]);
|
|
184
|
+
}
|
|
185
|
+
} else if ("kind" in change && change.kind) {
|
|
186
|
+
if (change.kind === "create") {
|
|
187
|
+
const createOp = change as CreateFile;
|
|
188
|
+
flushUri(createOp.uri);
|
|
189
|
+
ops.push({ kind: "create", uri: createOp.uri });
|
|
190
|
+
} else if (change.kind === "rename") {
|
|
191
|
+
const renameOp = change as RenameFile;
|
|
192
|
+
// Per LSP §3.16.2 documentChanges are applied in declared order.
|
|
193
|
+
// Flush both the source subtree (so prior edits land before the move)
|
|
194
|
+
// AND the destination subtree (so prior edits land on whatever exists
|
|
195
|
+
// at newUri before the rename overwrites/replaces it — relevant under
|
|
196
|
+
// `options.overwrite` and `options.ignoreIfExists`).
|
|
197
|
+
flushSubtree(renameOp.oldUri);
|
|
198
|
+
flushSubtree(renameOp.newUri);
|
|
199
|
+
ops.push({ kind: "rename", oldUri: renameOp.oldUri, newUri: renameOp.newUri });
|
|
200
|
+
} else if (change.kind === "delete") {
|
|
201
|
+
const deleteOp = change as DeleteFile;
|
|
202
|
+
flushSubtree(deleteOp.uri);
|
|
203
|
+
ops.push({ kind: "delete", uri: deleteOp.uri });
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Flush text edits not followed by a resource op.
|
|
209
|
+
for (const uri of [...pending.keys()]) {
|
|
210
|
+
flushUri(uri);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return ops;
|
|
214
|
+
}
|
|
215
|
+
|
|
123
216
|
/**
|
|
124
217
|
* Apply a workspace edit (collection of file changes).
|
|
218
|
+
* All text-edit batches are overlap-validated before anything is written so a
|
|
219
|
+
* conflict throws without leaving the workspace half-applied.
|
|
125
220
|
* Returns array of applied change descriptions.
|
|
126
221
|
*/
|
|
127
222
|
export async function applyWorkspaceEdit(edit: WorkspaceEdit, cwd: string): Promise<string[]> {
|
|
128
223
|
const applied: string[] = [];
|
|
129
224
|
|
|
130
225
|
if (edit.documentChanges) {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
// always see the correct prior file state.
|
|
135
|
-
const pending = new Map<string, TextEdit[]>();
|
|
136
|
-
|
|
137
|
-
const flushUri = async (uri: string) => {
|
|
138
|
-
const edits = pending.get(uri);
|
|
139
|
-
if (!edits) return;
|
|
140
|
-
pending.delete(uri);
|
|
141
|
-
const filePath = uriToFile(uri);
|
|
142
|
-
await applyTextEdits(filePath, edits);
|
|
143
|
-
applied.push(`Applied ${edits.length} edit(s) to ${formatPathRelativeToCwd(filePath, cwd)}`);
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
// Flush the exact URI plus every pending descendant (for folder-level
|
|
147
|
-
// resource ops where the queued edits target child files of the target).
|
|
148
|
-
const flushSubtree = async (uri: string) => {
|
|
149
|
-
const prefix = uri.endsWith("/") ? uri : `${uri}/`;
|
|
150
|
-
const matches: string[] = [];
|
|
151
|
-
for (const candidate of pending.keys()) {
|
|
152
|
-
if (candidate === uri || candidate.startsWith(prefix)) matches.push(candidate);
|
|
153
|
-
}
|
|
154
|
-
for (const target of matches) {
|
|
155
|
-
await flushUri(target);
|
|
156
|
-
}
|
|
157
|
-
};
|
|
158
|
-
|
|
159
|
-
for (const change of edit.documentChanges) {
|
|
160
|
-
if ("textDocument" in change && change.textDocument && "edits" in change && change.edits) {
|
|
161
|
-
const tdc = change as TextDocumentEdit;
|
|
162
|
-
const uri = tdc.textDocument.uri;
|
|
163
|
-
const textEdits = tdc.edits.filter((e): e is TextEdit => "range" in e && "newText" in e);
|
|
164
|
-
if (textEdits.length > 0) {
|
|
165
|
-
const prev = pending.get(uri);
|
|
166
|
-
if (prev) prev.push(...textEdits);
|
|
167
|
-
else pending.set(uri, [...textEdits]);
|
|
168
|
-
}
|
|
169
|
-
} else if ("kind" in change && change.kind) {
|
|
170
|
-
if (change.kind === "create") {
|
|
171
|
-
const createOp = change as CreateFile;
|
|
172
|
-
await flushUri(createOp.uri);
|
|
173
|
-
const filePath = uriToFile(createOp.uri);
|
|
174
|
-
await Bun.write(filePath, "");
|
|
175
|
-
applied.push(`Created ${formatPathRelativeToCwd(filePath, cwd)}`);
|
|
176
|
-
} else if (change.kind === "rename") {
|
|
177
|
-
const renameOp = change as RenameFile;
|
|
178
|
-
// Per LSP §3.16.2 documentChanges are applied in declared order.
|
|
179
|
-
// Flush both the source subtree (so prior edits land before the move)
|
|
180
|
-
// AND the destination subtree (so prior edits land on whatever exists
|
|
181
|
-
// at newUri before the rename overwrites/replaces it — relevant under
|
|
182
|
-
// `options.overwrite` and `options.ignoreIfExists`).
|
|
183
|
-
await flushSubtree(renameOp.oldUri);
|
|
184
|
-
await flushSubtree(renameOp.newUri);
|
|
185
|
-
const oldPath = uriToFile(renameOp.oldUri);
|
|
186
|
-
const newPath = uriToFile(renameOp.newUri);
|
|
187
|
-
await fs.mkdir(path.dirname(newPath), { recursive: true });
|
|
188
|
-
await fs.rename(oldPath, newPath);
|
|
189
|
-
applied.push(
|
|
190
|
-
`Renamed ${formatPathRelativeToCwd(oldPath, cwd)} → ${formatPathRelativeToCwd(newPath, cwd)}`,
|
|
191
|
-
);
|
|
192
|
-
} else if (change.kind === "delete") {
|
|
193
|
-
const deleteOp = change as DeleteFile;
|
|
194
|
-
await flushSubtree(deleteOp.uri);
|
|
195
|
-
const filePath = uriToFile(deleteOp.uri);
|
|
196
|
-
await fs.rm(filePath, { recursive: true });
|
|
197
|
-
applied.push(`Deleted ${formatPathRelativeToCwd(filePath, cwd)}`);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
226
|
+
const ops = planDocumentChanges(edit.documentChanges);
|
|
227
|
+
for (const op of ops) {
|
|
228
|
+
if (op.kind === "text") sortAndValidateTextEdits(op.edits);
|
|
200
229
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
230
|
+
for (const op of ops) {
|
|
231
|
+
if (op.kind === "text") {
|
|
232
|
+
const filePath = uriToFile(op.uri);
|
|
233
|
+
await applyTextEdits(filePath, op.edits);
|
|
234
|
+
applied.push(`Applied ${op.edits.length} edit(s) to ${formatPathRelativeToCwd(filePath, cwd)}`);
|
|
235
|
+
} else if (op.kind === "create") {
|
|
236
|
+
const filePath = uriToFile(op.uri);
|
|
237
|
+
await Bun.write(filePath, "");
|
|
238
|
+
applied.push(`Created ${formatPathRelativeToCwd(filePath, cwd)}`);
|
|
239
|
+
} else if (op.kind === "rename") {
|
|
240
|
+
const oldPath = uriToFile(op.oldUri);
|
|
241
|
+
const newPath = uriToFile(op.newUri);
|
|
242
|
+
await fs.mkdir(path.dirname(newPath), { recursive: true });
|
|
243
|
+
await fs.rename(oldPath, newPath);
|
|
244
|
+
applied.push(`Renamed ${formatPathRelativeToCwd(oldPath, cwd)} → ${formatPathRelativeToCwd(newPath, cwd)}`);
|
|
245
|
+
} else {
|
|
246
|
+
const filePath = uriToFile(op.uri);
|
|
247
|
+
await fs.rm(filePath, { recursive: true });
|
|
248
|
+
applied.push(`Deleted ${formatPathRelativeToCwd(filePath, cwd)}`);
|
|
249
|
+
}
|
|
205
250
|
}
|
|
206
251
|
} else if (edit.changes) {
|
|
207
|
-
// Legacy changes-map path:
|
|
252
|
+
// Legacy changes-map path: validate every file's edits before writing any.
|
|
208
253
|
const changes = edit.changes;
|
|
254
|
+
for (const uri in changes) {
|
|
255
|
+
sortAndValidateTextEdits(changes[uri]);
|
|
256
|
+
}
|
|
209
257
|
for (const uri in changes) {
|
|
210
258
|
const textEdits = changes[uri];
|
|
211
259
|
if (textEdits.length === 0) continue;
|
package/src/lsp/index.ts
CHANGED
|
@@ -105,7 +105,7 @@ export const LSP_READONLY_ACTIONS: ReadonlySet<string> = new Set([
|
|
|
105
105
|
|
|
106
106
|
export interface LspStartupServerInfo {
|
|
107
107
|
name: string;
|
|
108
|
-
status: "connecting" | "ready" | "error";
|
|
108
|
+
status: "connecting" | "ready" | "error" | "available";
|
|
109
109
|
fileTypes: string[];
|
|
110
110
|
error?: string;
|
|
111
111
|
}
|
|
@@ -121,11 +121,14 @@ export interface LspWarmupOptions {
|
|
|
121
121
|
onConnecting?: (serverNames: string[]) => void;
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
export function discoverStartupLspServers(
|
|
124
|
+
export function discoverStartupLspServers(
|
|
125
|
+
cwd: string,
|
|
126
|
+
status: LspStartupServerInfo["status"] = "connecting",
|
|
127
|
+
): LspStartupServerInfo[] {
|
|
125
128
|
const config = loadConfig(cwd);
|
|
126
129
|
return getLspServers(config).map(([name, serverConfig]) => ({
|
|
127
130
|
name,
|
|
128
|
-
status
|
|
131
|
+
status,
|
|
129
132
|
fileTypes: serverConfig.fileTypes,
|
|
130
133
|
}));
|
|
131
134
|
}
|
|
@@ -454,21 +457,23 @@ function isMethodNotFoundError(err: unknown): boolean {
|
|
|
454
457
|
}
|
|
455
458
|
|
|
456
459
|
async function reloadServer(client: LspClient, serverName: string, signal?: AbortSignal): Promise<string> {
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
break;
|
|
464
|
-
} catch {
|
|
465
|
-
// Method not supported, try next
|
|
466
|
-
}
|
|
460
|
+
// rust-analyzer exposes a real reload request.
|
|
461
|
+
try {
|
|
462
|
+
await sendRequest(client, "rust-analyzer/reloadWorkspace", null, signal);
|
|
463
|
+
return `Reloaded ${serverName}`;
|
|
464
|
+
} catch {
|
|
465
|
+
// Method not supported — fall through.
|
|
467
466
|
}
|
|
468
|
-
|
|
467
|
+
// workspace/didChangeConfiguration is a notification per spec; sending it
|
|
468
|
+
// as a request hangs until the tool deadline on servers that route it to
|
|
469
|
+
// the notification handler and never respond.
|
|
470
|
+
try {
|
|
471
|
+
await sendNotification(client, "workspace/didChangeConfiguration", { settings: {} });
|
|
472
|
+
return `Reloaded ${serverName}`;
|
|
473
|
+
} catch {
|
|
469
474
|
client.proc.kill();
|
|
475
|
+
return `Restarted ${serverName}`;
|
|
470
476
|
}
|
|
471
|
-
return output;
|
|
472
477
|
}
|
|
473
478
|
|
|
474
479
|
interface WaitForDiagnosticsOptions {
|
|
@@ -636,12 +641,13 @@ interface GetDiagnosticsForFileOptions {
|
|
|
636
641
|
async function captureDiagnosticVersions(
|
|
637
642
|
cwd: string,
|
|
638
643
|
servers: Array<[string, ServerConfig]>,
|
|
644
|
+
initTimeoutMs?: number,
|
|
639
645
|
): Promise<ServerVersionMap> {
|
|
640
646
|
const versions = new Map<string, number>();
|
|
641
647
|
await Promise.allSettled(
|
|
642
648
|
servers.map(async ([serverName, serverConfig]) => {
|
|
643
649
|
if (serverConfig.createClient) return;
|
|
644
|
-
const client = await getOrCreateClient(serverConfig, cwd);
|
|
650
|
+
const client = await getOrCreateClient(serverConfig, cwd, initTimeoutMs);
|
|
645
651
|
versions.set(serverName, client.diagnosticsVersion);
|
|
646
652
|
}),
|
|
647
653
|
);
|
|
@@ -1118,7 +1124,9 @@ async function runLspWritethrough(
|
|
|
1118
1124
|
const useCustomFormatter = enableFormat && customLinterServers.length > 0;
|
|
1119
1125
|
|
|
1120
1126
|
// Capture diagnostic versions BEFORE syncing to detect stale diagnostics
|
|
1121
|
-
|
|
1127
|
+
// Bound client creation by the writethrough budget: a hung/broken server
|
|
1128
|
+
// must not add its full init wait (30s default) to every edit.
|
|
1129
|
+
const minVersions = enableDiagnostics ? await captureDiagnosticVersions(cwd, servers, 5_000) : undefined;
|
|
1122
1130
|
let expectedDocumentVersions: ServerVersionMap | undefined;
|
|
1123
1131
|
|
|
1124
1132
|
let formatter: FileFormatResult | undefined;
|
|
@@ -2311,11 +2319,12 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
|
|
|
2311
2319
|
break;
|
|
2312
2320
|
}
|
|
2313
2321
|
const parsedIndex = /^\d+$/.test(normalizedQuery) ? Number.parseInt(normalizedQuery, 10) : null;
|
|
2314
|
-
const selectedAction =
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2322
|
+
const selectedAction =
|
|
2323
|
+
parsedIndex !== null
|
|
2324
|
+
? result[parsedIndex]
|
|
2325
|
+
: result.find(actionItem =>
|
|
2326
|
+
actionItem.title.toLowerCase().includes(normalizedQuery.toLowerCase()),
|
|
2327
|
+
);
|
|
2319
2328
|
|
|
2320
2329
|
if (!selectedAction) {
|
|
2321
2330
|
const actionLines = result.map((actionItem, index) => ` ${formatCodeAction(actionItem, index)}`);
|
package/src/lsp/render.ts
CHANGED
|
@@ -139,7 +139,7 @@ export function renderResult(
|
|
|
139
139
|
const outputBlock = new CachedOutputBlock();
|
|
140
140
|
|
|
141
141
|
return markFramedBlockComponent({
|
|
142
|
-
render(width: number): string[] {
|
|
142
|
+
render(width: number): readonly string[] {
|
|
143
143
|
// Read mutable state at render time
|
|
144
144
|
const { expanded, isPartial, spinnerFrame } = options;
|
|
145
145
|
|
package/src/lsp/types.ts
CHANGED
|
@@ -416,6 +416,8 @@ export interface LspClient {
|
|
|
416
416
|
pendingRequests: Map<number, PendingRequest>;
|
|
417
417
|
messageBuffer: Uint8Array;
|
|
418
418
|
isReading: boolean;
|
|
419
|
+
/** Lifecycle state: "connecting" until initialize completes, then "ready"; "error" on init failure or reader death. */
|
|
420
|
+
status: "connecting" | "ready" | "error";
|
|
419
421
|
serverCapabilities?: LspServerCapabilities;
|
|
420
422
|
lastActivity: number;
|
|
421
423
|
/** Serializes outbound JSON-RPC writes to the server process. */
|
package/src/lsp/utils.ts
CHANGED
|
@@ -27,22 +27,17 @@ export { detectLanguageId } from "../utils/lang-from-path";
|
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
29
|
* Convert a file path to a file:// URI.
|
|
30
|
+
* Uses the URL machinery so special characters (`%`, `#`, `?`, spaces) are
|
|
31
|
+
* percent-encoded; plain concatenation produced URIs that broke round-trips.
|
|
30
32
|
* Handles Windows drive letters correctly.
|
|
31
33
|
*/
|
|
32
34
|
export function fileToUri(filePath: string): string {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (process.platform === "win32") {
|
|
36
|
-
// Windows: file:///C:/path/to/file
|
|
37
|
-
return `file:///${resolved.replace(/\\/g, "/")}`;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Unix: file:///path/to/file
|
|
41
|
-
return `file://${resolved}`;
|
|
35
|
+
return Bun.pathToFileURL(path.resolve(filePath)).href;
|
|
42
36
|
}
|
|
43
37
|
|
|
44
38
|
/**
|
|
45
39
|
* Convert a file:// URI to a file path.
|
|
40
|
+
* Tolerates both percent-encoded URIs and lax servers that send raw paths.
|
|
46
41
|
* Handles Windows drive letters correctly.
|
|
47
42
|
*/
|
|
48
43
|
export function uriToFile(uri: string): string {
|
|
@@ -50,7 +45,30 @@ export function uriToFile(uri: string): string {
|
|
|
50
45
|
return uri;
|
|
51
46
|
}
|
|
52
47
|
|
|
53
|
-
|
|
48
|
+
// A raw `#`/`?` parses *successfully* as fragment/query and silently
|
|
49
|
+
// truncates the path — it never reaches the catch below. LSP servers do
|
|
50
|
+
// not use fragments or queries on file URIs (encoded forms are %23/%3F),
|
|
51
|
+
// so raw occurrences mean a lax server sent an unencoded path.
|
|
52
|
+
if (uri.includes("#") || uri.includes("?")) {
|
|
53
|
+
return laxUriToFile(uri);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
return Bun.fileURLToPath(uri);
|
|
58
|
+
} catch {
|
|
59
|
+
// Not a well-formed file URL (unencoded characters, stray `%`, host
|
|
60
|
+
// component). Fall back to a lenient manual conversion.
|
|
61
|
+
return laxUriToFile(uri);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function laxUriToFile(uri: string): string {
|
|
66
|
+
let filePath = uri.slice(7);
|
|
67
|
+
try {
|
|
68
|
+
filePath = decodeURIComponent(filePath);
|
|
69
|
+
} catch {
|
|
70
|
+
// Invalid percent-encoding — treat as a literal path.
|
|
71
|
+
}
|
|
54
72
|
|
|
55
73
|
// Windows: file:///C:/path → C:/path (strip leading slash before drive letter)
|
|
56
74
|
if (process.platform === "win32" && filePath.startsWith("/") && /^[A-Za-z]:/.test(filePath.slice(1))) {
|