@oh-my-pi/pi-coding-agent 14.0.4 → 14.0.5
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 +42 -0
- package/package.json +11 -8
- package/src/config/model-registry.ts +3 -2
- package/src/config/model-resolver.ts +33 -25
- package/src/config/settings.ts +9 -2
- package/src/dap/session.ts +31 -39
- package/src/debug/log-formatting.ts +2 -2
- package/src/edit/modes/chunk.ts +8 -3
- package/src/lsp/client.ts +4 -2
- package/src/lsp/index.ts +4 -9
- package/src/lsp/utils.ts +26 -0
- package/src/modes/components/diff.ts +1 -1
- package/src/modes/controllers/event-controller.ts +438 -426
- package/src/modes/theme/mermaid-cache.ts +5 -7
- package/src/priority.json +8 -0
- package/src/prompts/agents/designer.md +1 -2
- package/src/prompts/tools/chunk-edit.md +39 -40
- package/src/prompts/tools/read-chunk.md +4 -1
- package/src/session/agent-session.ts +10 -0
- package/src/session/compaction/compaction.ts +1 -1
- package/src/tools/browser.ts +84 -21
- package/src/tools/fetch.ts +1 -1
- package/src/tools/find.ts +40 -94
- package/src/tools/gemini-image.ts +1 -0
- package/src/tools/render-utils.ts +1 -1
- package/src/utils/image-resize.ts +73 -37
- package/src/web/scrapers/types.ts +50 -32
- package/src/web/search/providers/codex.ts +21 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,48 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [14.0.5] - 2026-04-11
|
|
6
|
+
### Added
|
|
7
|
+
|
|
8
|
+
- Added `designer` model role for UI/UX design tasks with Gemini 3.1 Pro as default model
|
|
9
|
+
- Added support for model role fallback lists — roles can now resolve to multiple model patterns with automatic fallback to next available model
|
|
10
|
+
- Added `extractReadableFromHtml` utility function to extract readable content from HTML with Readability article extraction and CSS selector fallback
|
|
11
|
+
- Added support for GFM (GitHub Flavored Markdown) features including tables, strikethrough, and task lists in HTML-to-markdown conversion
|
|
12
|
+
- Added `resolveDiagnosticTargets` utility function to handle glob pattern resolution with fallback to literal file paths for bracket-style paths
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- Clarified fenced code block editing behavior in markdown — the tool now preserves literal indentation inside fenced blocks, with content written verbatim as supplied
|
|
17
|
+
- Updated guidance for inserting content after markdown section headings to use `after` on the heading chunk rather than `before`/`prepend` on the section itself
|
|
18
|
+
- Reduced default image resize limits to 1568px (from 2000px) and 500KB (from 4.5MB) to match Anthropic's internal downscaling threshold and reduce payload sizes in tool calls
|
|
19
|
+
- Adjusted screenshot compression to use 1024px max dimensions and 150KB budget for more aggressive optimization of browser screenshots in LLM requests
|
|
20
|
+
- Updated JPEG quality defaults from 80 to 75 and refined quality ladder steps (70, 60, 50, 40) for tighter byte budgets
|
|
21
|
+
- Improved image resize fast-path to skip re-encoding when images are already within dimensions and at ≤25% of byte budget, avoiding unnecessary processing of small icons and diagrams
|
|
22
|
+
- Clarified that chunk names are truncated and must be copied from `read` or `?` output rather than constructed from source identifiers
|
|
23
|
+
- Enhanced guidance for editing fenced code blocks in markdown to preserve exact whitespace using `raw` reads, as the tool normalizes tabs to spaces which can damage indentation-sensitive content
|
|
24
|
+
- Updated designer agent to use `pi/designer` role alias instead of explicit model list
|
|
25
|
+
- Refactored model role resolution to support multiple fallback patterns per role, improving model availability handling
|
|
26
|
+
- Replaced regex-based HTML-to-markdown conversion with Turndown library and GFM plugin for more accurate formatting of complex HTML structures
|
|
27
|
+
- Simplified no-changes response to omit redundant response text when chunk content already matches
|
|
28
|
+
- Clarified region suffix behavior on leaf and compound statement chunks — `~` and `^` now fall back to whole-chunk replacement with explicit guidance to supply complete structural content
|
|
29
|
+
- Updated CRC refresh guidance to direct users to use CRCs from edit responses or run `read(path="file", sel="?")`
|
|
30
|
+
- Added clarification that region suffixes fall back to whole-chunk replacement for prose and data formats (markdown, YAML, JSON, fenced code blocks, frontmatter)
|
|
31
|
+
- Documented `L20` shorthand syntax for single-line reads extending to end-of-file, with `L20-L20` for one-line windows
|
|
32
|
+
- Refactored diagnostic target resolution to use new `resolveDiagnosticTargets` function, consolidating glob pattern detection and file matching logic
|
|
33
|
+
- Updated chunk selector syntax from `@region` format to `~` (body) and `^` (head) suffixes for more concise region targeting
|
|
34
|
+
- Simplified chunk edit documentation to use new `~` and `^` region syntax instead of `@head`, `@body`, `@tail`, `@decl` keywords
|
|
35
|
+
- Replaced internal `raceAbort` function with imported `raceWithAbort` utility from pi-utils
|
|
36
|
+
- Refactored cleanup timer to use async iterator pattern with `timers.setInterval` instead of `setInterval`
|
|
37
|
+
- Made `#cleanupIdleSessions` synchronous and moved async cleanup loop logic to new `#runCleanupLoop` method
|
|
38
|
+
- Replaced regex-based `htmlToBasicMarkdown` with a Turndown + GFM plugin pipeline (tables, strikethrough, task lists, nested lists now convert correctly). Added direct `turndown` and `turndown-plugin-gfm` dependencies
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
|
|
42
|
+
- Fixed chunk edit tool to report file-not-found error distinctly when attempting to use chunk selectors on non-existent files, with guidance to use write tool or verify the path
|
|
43
|
+
- Fixed stale child selector reuse to correctly match chunks by checksum when multiple sibling chunks with the same name exist under the same parent
|
|
44
|
+
- Fixed stale diagnostics being reused after unrelated file publishes by clearing cached diagnostics before refreshing file state
|
|
45
|
+
- Fixed Codex search to use streamed answer text when final answer is an image placeholder or empty
|
|
46
|
+
|
|
5
47
|
## [14.0.4] - 2026-04-10
|
|
6
48
|
### Added
|
|
7
49
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
4
|
-
"version": "14.0.
|
|
4
|
+
"version": "14.0.5",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://github.com/can1357/oh-my-pi",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -46,12 +46,12 @@
|
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@agentclientprotocol/sdk": "0.16.1",
|
|
48
48
|
"@mozilla/readability": "^0.6",
|
|
49
|
-
"@oh-my-pi/omp-stats": "14.0.
|
|
50
|
-
"@oh-my-pi/pi-agent-core": "14.0.
|
|
51
|
-
"@oh-my-pi/pi-ai": "14.0.
|
|
52
|
-
"@oh-my-pi/pi-natives": "14.0.
|
|
53
|
-
"@oh-my-pi/pi-tui": "14.0.
|
|
54
|
-
"@oh-my-pi/pi-utils": "14.0.
|
|
49
|
+
"@oh-my-pi/omp-stats": "14.0.5",
|
|
50
|
+
"@oh-my-pi/pi-agent-core": "14.0.5",
|
|
51
|
+
"@oh-my-pi/pi-ai": "14.0.5",
|
|
52
|
+
"@oh-my-pi/pi-natives": "14.0.5",
|
|
53
|
+
"@oh-my-pi/pi-tui": "14.0.5",
|
|
54
|
+
"@oh-my-pi/pi-utils": "14.0.5",
|
|
55
55
|
"@sinclair/typebox": "^0.34",
|
|
56
56
|
"@xterm/headless": "^6.0",
|
|
57
57
|
"ajv": "^8.18",
|
|
@@ -63,10 +63,13 @@
|
|
|
63
63
|
"lru-cache": "11.3.1",
|
|
64
64
|
"markit-ai": "0.5.0",
|
|
65
65
|
"puppeteer": "^24.37",
|
|
66
|
+
"turndown": "7.2.4",
|
|
67
|
+
"turndown-plugin-gfm": "1.0.2",
|
|
66
68
|
"zod": "4.3.6"
|
|
67
69
|
},
|
|
68
70
|
"devDependencies": {
|
|
69
|
-
"@types/bun": "^1.3"
|
|
71
|
+
"@types/bun": "^1.3",
|
|
72
|
+
"@types/turndown": "5.0.6"
|
|
70
73
|
},
|
|
71
74
|
"engines": {
|
|
72
75
|
"bun": ">=1.3.7"
|
|
@@ -39,7 +39,7 @@ export function isAuthenticated(apiKey: string | undefined | null): apiKey is st
|
|
|
39
39
|
return Boolean(apiKey) && apiKey !== kNoAuth;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
export type ModelRole = "default" | "smol" | "slow" | "vision" | "plan" | "commit" | "task";
|
|
42
|
+
export type ModelRole = "default" | "smol" | "slow" | "vision" | "plan" | "designer" | "commit" | "task";
|
|
43
43
|
|
|
44
44
|
export interface ModelRoleInfo {
|
|
45
45
|
tag?: string;
|
|
@@ -53,11 +53,12 @@ export const MODEL_ROLES: Record<ModelRole, ModelRoleInfo> = {
|
|
|
53
53
|
slow: { tag: "SLOW", name: "Thinking", color: "accent" },
|
|
54
54
|
vision: { tag: "VISION", name: "Vision", color: "error" },
|
|
55
55
|
plan: { tag: "PLAN", name: "Architect", color: "muted" },
|
|
56
|
+
designer: { tag: "DESIGNER", name: "Designer", color: "muted" },
|
|
56
57
|
commit: { tag: "COMMIT", name: "Commit", color: "dim" },
|
|
57
58
|
task: { tag: "TASK", name: "Subtask", color: "muted" },
|
|
58
59
|
};
|
|
59
60
|
|
|
60
|
-
export const MODEL_ROLE_IDS: ModelRole[] = ["default", "smol", "slow", "vision", "plan", "commit", "task"];
|
|
61
|
+
export const MODEL_ROLE_IDS: ModelRole[] = ["default", "smol", "slow", "vision", "plan", "designer", "commit", "task"];
|
|
61
62
|
|
|
62
63
|
/** Alias for ModelRoleInfo - used for both built-in and custom roles */
|
|
63
64
|
export type RoleInfo = ModelRoleInfo;
|
|
@@ -387,7 +387,7 @@ function isSessionInheritedAgentPattern(value: string): boolean {
|
|
|
387
387
|
return value === DEFAULT_MODEL_ROLE || value === `${PREFIX_MODEL_ROLE}${DEFAULT_MODEL_ROLE}` || value === "pi/task";
|
|
388
388
|
}
|
|
389
389
|
|
|
390
|
-
function resolveConfiguredRolePattern(value: string, settings?: Settings): string | undefined {
|
|
390
|
+
function resolveConfiguredRolePattern(value: string, settings?: Settings): string[] | undefined {
|
|
391
391
|
const normalized = value.trim();
|
|
392
392
|
if (!normalized) return undefined;
|
|
393
393
|
|
|
@@ -396,11 +396,16 @@ function resolveConfiguredRolePattern(value: string, settings?: Settings): strin
|
|
|
396
396
|
lastColonIndex > PREFIX_MODEL_ROLE.length ? parseThinkingLevel(normalized.slice(lastColonIndex + 1)) : undefined;
|
|
397
397
|
const aliasCandidate = thinkingLevel ? normalized.slice(0, lastColonIndex) : normalized;
|
|
398
398
|
const role = getModelRoleAlias(aliasCandidate);
|
|
399
|
-
if (!role) return normalized;
|
|
399
|
+
if (!role) return [normalized];
|
|
400
400
|
|
|
401
401
|
const configured = settings?.getModelRole(role)?.trim();
|
|
402
|
-
|
|
403
|
-
|
|
402
|
+
const roleDefaults = normalizeModelPatternList(MODEL_PRIO[role as keyof typeof MODEL_PRIO]);
|
|
403
|
+
const resolved = configured ? normalizeModelPatternList(configured) : roleDefaults;
|
|
404
|
+
if (!resolved || resolved.length === 0) {
|
|
405
|
+
return undefined;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return thinkingLevel ? resolved.map(pattern => `${pattern}:${thinkingLevel}`) : resolved;
|
|
404
409
|
}
|
|
405
410
|
|
|
406
411
|
/**
|
|
@@ -412,7 +417,7 @@ export function expandRoleAlias(value: string, settings?: Settings): string {
|
|
|
412
417
|
return settings?.getModelRole("default") ?? value;
|
|
413
418
|
}
|
|
414
419
|
|
|
415
|
-
const resolved = resolveConfiguredRolePattern(value, settings);
|
|
420
|
+
const resolved = resolveConfiguredRolePattern(value, settings)?.[0];
|
|
416
421
|
return resolved ?? value;
|
|
417
422
|
}
|
|
418
423
|
|
|
@@ -420,10 +425,9 @@ export function resolveConfiguredModelPatterns(value: string | string[] | undefi
|
|
|
420
425
|
const patterns = normalizeModelPatternList(value);
|
|
421
426
|
return patterns.flatMap(pattern => {
|
|
422
427
|
const resolved = resolveConfiguredRolePattern(pattern, settings);
|
|
423
|
-
return resolved
|
|
428
|
+
return resolved ?? [];
|
|
424
429
|
});
|
|
425
430
|
}
|
|
426
|
-
|
|
427
431
|
export interface AgentModelPatternResolutionOptions {
|
|
428
432
|
settingsOverride?: string | string[];
|
|
429
433
|
agentModel?: string | string[];
|
|
@@ -477,28 +481,32 @@ export function resolveModelRoleValue(
|
|
|
477
481
|
}
|
|
478
482
|
|
|
479
483
|
const lastColonIndex = normalized.lastIndexOf(":");
|
|
480
|
-
const
|
|
484
|
+
const _thinkingSelector =
|
|
481
485
|
lastColonIndex > PREFIX_MODEL_ROLE.length ? parseThinkingLevel(normalized.slice(lastColonIndex + 1)) : undefined;
|
|
482
|
-
const
|
|
483
|
-
|
|
484
|
-
if (!effectivePattern) {
|
|
486
|
+
const effectivePatterns = resolveConfiguredRolePattern(normalized, options?.settings);
|
|
487
|
+
if (!effectivePatterns || effectivePatterns.length === 0) {
|
|
485
488
|
return { model: undefined, thinkingLevel: undefined, explicitThinkingLevel: false, warning: undefined };
|
|
486
489
|
}
|
|
487
|
-
const patternWithSuffix = thinkingSelector ? `${effectivePattern}:${thinkingSelector}` : effectivePattern;
|
|
488
|
-
const { model, thinkingLevel, warning, explicitThinkingLevel } = parseModelPattern(
|
|
489
|
-
patternWithSuffix,
|
|
490
|
-
availableModels,
|
|
491
|
-
options?.matchPreferences,
|
|
492
|
-
);
|
|
493
490
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
491
|
+
let warning: string | undefined;
|
|
492
|
+
for (const effectivePattern of effectivePatterns) {
|
|
493
|
+
const resolved = parseModelPattern(effectivePattern, availableModels, options?.matchPreferences);
|
|
494
|
+
if (resolved.model) {
|
|
495
|
+
return {
|
|
496
|
+
model: resolved.model,
|
|
497
|
+
thinkingLevel: resolved.explicitThinkingLevel
|
|
498
|
+
? (resolveThinkingLevelForModel(resolved.model, resolved.thinkingLevel) ?? resolved.thinkingLevel)
|
|
499
|
+
: resolved.thinkingLevel,
|
|
500
|
+
explicitThinkingLevel: resolved.explicitThinkingLevel,
|
|
501
|
+
warning: resolved.warning,
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
if (!warning && resolved.warning) {
|
|
505
|
+
warning = resolved.warning;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
return { model: undefined, thinkingLevel: undefined, explicitThinkingLevel: false, warning };
|
|
502
510
|
}
|
|
503
511
|
|
|
504
512
|
export function extractExplicitThinkingSelector(
|
package/src/config/settings.ts
CHANGED
|
@@ -13,8 +13,15 @@
|
|
|
13
13
|
|
|
14
14
|
import * as fs from "node:fs";
|
|
15
15
|
import * as path from "node:path";
|
|
16
|
-
import {
|
|
17
|
-
|
|
16
|
+
import {
|
|
17
|
+
getAgentDbPath,
|
|
18
|
+
getAgentDir,
|
|
19
|
+
getProjectDir,
|
|
20
|
+
isEnoent,
|
|
21
|
+
logger,
|
|
22
|
+
procmgr,
|
|
23
|
+
setDefaultTabWidth,
|
|
24
|
+
} from "@oh-my-pi/pi-utils";
|
|
18
25
|
import { YAML } from "bun";
|
|
19
26
|
import { type Settings as SettingsCapabilityItem, settingsCapability } from "../capability/settings";
|
|
20
27
|
import type { ModelRole } from "../config/model-registry";
|
package/src/dap/session.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
|
-
import
|
|
2
|
+
import * as timers from "node:timers/promises";
|
|
3
|
+
import { logger, ptree, untilAborted } from "@oh-my-pi/pi-utils";
|
|
3
4
|
import { NON_INTERACTIVE_ENV } from "../exec/non-interactive-env";
|
|
4
5
|
import { DapClient } from "./client";
|
|
5
6
|
import type {
|
|
@@ -154,27 +155,10 @@ function buildSummary(session: DapSession): DapSessionSummary {
|
|
|
154
155
|
};
|
|
155
156
|
}
|
|
156
157
|
|
|
157
|
-
async function raceAbort<T>(promise: Promise<T>, signal?: AbortSignal): Promise<T> {
|
|
158
|
-
if (!signal) return promise;
|
|
159
|
-
if (signal.aborted) {
|
|
160
|
-
throw signal.reason instanceof Error ? signal.reason : new Error("Operation aborted");
|
|
161
|
-
}
|
|
162
|
-
const { promise: abortPromise, reject } = Promise.withResolvers<never>();
|
|
163
|
-
const onAbort = () => {
|
|
164
|
-
reject(signal.reason instanceof Error ? signal.reason : new Error("Operation aborted"));
|
|
165
|
-
};
|
|
166
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
167
|
-
try {
|
|
168
|
-
return await Promise.race([promise, abortPromise]);
|
|
169
|
-
} finally {
|
|
170
|
-
signal.removeEventListener("abort", onAbort);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
158
|
export class DapSessionManager {
|
|
175
159
|
#sessions = new Map<string, DapSession>();
|
|
176
160
|
#activeSessionId: string | null = null;
|
|
177
|
-
#
|
|
161
|
+
#cleanupLoopPromise?: Promise<void>;
|
|
178
162
|
#nextId = 0;
|
|
179
163
|
|
|
180
164
|
constructor() {
|
|
@@ -235,7 +219,7 @@ export class DapSessionManager {
|
|
|
235
219
|
// Try to capture initial stopped state (e.g. stopOnEntry).
|
|
236
220
|
// Timeout is acceptable — the program may simply be running.
|
|
237
221
|
try {
|
|
238
|
-
await
|
|
222
|
+
await untilAborted(signal, initialStopPromise);
|
|
239
223
|
if (session.status === "stopped") {
|
|
240
224
|
await this.#fetchTopFrame(session, signal, Math.min(timeoutMs, STOP_CAPTURE_TIMEOUT_MS));
|
|
241
225
|
}
|
|
@@ -283,7 +267,7 @@ export class DapSessionManager {
|
|
|
283
267
|
await this.#completeConfigurationHandshake(session, signal, timeoutMs);
|
|
284
268
|
await attachPromise;
|
|
285
269
|
try {
|
|
286
|
-
await
|
|
270
|
+
await untilAborted(signal, initialStopPromise);
|
|
287
271
|
if (session.status === "stopped") {
|
|
288
272
|
await this.#fetchTopFrame(session, signal, Math.min(timeoutMs, STOP_CAPTURE_TIMEOUT_MS));
|
|
289
273
|
}
|
|
@@ -696,9 +680,9 @@ export class DapSessionManager {
|
|
|
696
680
|
// between the request and here. Wait for it, but tolerate timeout if the
|
|
697
681
|
// session already transitioned.
|
|
698
682
|
try {
|
|
699
|
-
await
|
|
700
|
-
session.client.waitForEvent<DapStoppedEventBody>("stopped", undefined, signal, timeoutMs),
|
|
683
|
+
await untilAborted(
|
|
701
684
|
signal,
|
|
685
|
+
session.client.waitForEvent<DapStoppedEventBody>("stopped", undefined, signal, timeoutMs),
|
|
702
686
|
);
|
|
703
687
|
} catch {
|
|
704
688
|
// Timeout or abort — report current state regardless
|
|
@@ -833,16 +817,16 @@ export class DapSessionManager {
|
|
|
833
817
|
session.lastUsedAt = Date.now();
|
|
834
818
|
if (session.status !== "terminated") {
|
|
835
819
|
if (session.capabilities?.supportsTerminateRequest) {
|
|
836
|
-
await
|
|
837
|
-
session.client.sendRequest("terminate", undefined, signal, timeoutMs).catch(() => undefined),
|
|
820
|
+
await untilAborted(
|
|
838
821
|
signal,
|
|
822
|
+
session.client.sendRequest("terminate", undefined, signal, timeoutMs).catch(() => undefined),
|
|
839
823
|
);
|
|
840
824
|
}
|
|
841
|
-
await
|
|
825
|
+
await untilAborted(
|
|
826
|
+
signal,
|
|
842
827
|
session.client
|
|
843
828
|
.sendRequest("disconnect", { terminateDebuggee: true }, signal, timeoutMs)
|
|
844
829
|
.catch(() => undefined),
|
|
845
|
-
signal,
|
|
846
830
|
);
|
|
847
831
|
}
|
|
848
832
|
session.status = "terminated";
|
|
@@ -852,22 +836,30 @@ export class DapSessionManager {
|
|
|
852
836
|
}
|
|
853
837
|
|
|
854
838
|
#startCleanupTimer(): void {
|
|
855
|
-
if (this.#
|
|
856
|
-
this.#
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
839
|
+
if (this.#cleanupLoopPromise) return;
|
|
840
|
+
this.#cleanupLoopPromise = this.#runCleanupLoop();
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
async #runCleanupLoop(): Promise<void> {
|
|
844
|
+
for await (const _ of timers.setInterval(CLEANUP_INTERVAL_MS, null, { ref: false })) {
|
|
845
|
+
try {
|
|
846
|
+
this.#cleanupIdleSessions();
|
|
847
|
+
} catch (error) {
|
|
848
|
+
logger.error("DAP idle session cleanup failed", { error: toErrorMessage(error) });
|
|
849
|
+
}
|
|
850
|
+
}
|
|
860
851
|
}
|
|
861
852
|
|
|
862
|
-
|
|
853
|
+
#cleanupIdleSessions(): void {
|
|
854
|
+
if (this.#sessions.size === 0) return;
|
|
863
855
|
const now = Date.now();
|
|
864
|
-
for (const session of
|
|
856
|
+
for (const session of this.#sessions.values()) {
|
|
865
857
|
if (
|
|
866
858
|
session.status === "terminated" ||
|
|
867
859
|
now - session.lastUsedAt > IDLE_TIMEOUT_MS ||
|
|
868
860
|
!session.client.isAlive()
|
|
869
861
|
) {
|
|
870
|
-
|
|
862
|
+
this.#disposeSession(session);
|
|
871
863
|
}
|
|
872
864
|
}
|
|
873
865
|
}
|
|
@@ -1006,7 +998,7 @@ export class DapSessionManager {
|
|
|
1006
998
|
// Wait for the initialized event if we haven't seen it yet.
|
|
1007
999
|
if (!session.initializedSeen) {
|
|
1008
1000
|
try {
|
|
1009
|
-
await
|
|
1001
|
+
await untilAborted(signal, session.client.waitForEvent("initialized", undefined, signal, timeoutMs));
|
|
1010
1002
|
} catch {
|
|
1011
1003
|
// Adapter may not send initialized (e.g. it already terminated).
|
|
1012
1004
|
// Proceed anyway — the launch/attach response will surface any real error.
|
|
@@ -1100,7 +1092,7 @@ export class DapSessionManager {
|
|
|
1100
1092
|
timeoutMs: number = 30_000,
|
|
1101
1093
|
): Promise<DapContinueOutcome> {
|
|
1102
1094
|
try {
|
|
1103
|
-
await
|
|
1095
|
+
await untilAborted(signal, outcomePromise);
|
|
1104
1096
|
if (session.status === "stopped") {
|
|
1105
1097
|
await this.#fetchTopFrame(session, signal, Math.min(timeoutMs, 5_000));
|
|
1106
1098
|
}
|
|
@@ -1243,12 +1235,12 @@ export class DapSessionManager {
|
|
|
1243
1235
|
return session;
|
|
1244
1236
|
}
|
|
1245
1237
|
|
|
1246
|
-
|
|
1238
|
+
#disposeSession(session: DapSession) {
|
|
1247
1239
|
if (this.#activeSessionId === session.id) {
|
|
1248
1240
|
this.#activeSessionId = null;
|
|
1249
1241
|
}
|
|
1250
1242
|
this.#sessions.delete(session.id);
|
|
1251
|
-
|
|
1243
|
+
void session.client.dispose().catch(() => {});
|
|
1252
1244
|
}
|
|
1253
1245
|
}
|
|
1254
1246
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { sanitizeText
|
|
2
|
-
import { replaceTabs, truncateToWidth } from "../tools/render-utils";
|
|
1
|
+
import { sanitizeText } from "@oh-my-pi/pi-natives";
|
|
2
|
+
import { replaceTabs, truncateToWidth, wrapTextWithAnsi } from "../tools/render-utils";
|
|
3
3
|
|
|
4
4
|
export function formatDebugLogLine(line: string, maxWidth: number): string {
|
|
5
5
|
const sanitized = sanitizeText(line);
|
package/src/edit/modes/chunk.ts
CHANGED
|
@@ -332,7 +332,7 @@ export const chunkToolEditSchema = Type.Object({
|
|
|
332
332
|
op: StringEnum(CHUNK_OP_VALUES),
|
|
333
333
|
sel: Type.String({
|
|
334
334
|
description:
|
|
335
|
-
"Chunk selector.
|
|
335
|
+
"Chunk selector. Use 'path~' or 'path^' for insertions, 'path#CRC~' or 'path#CRC^' for replace, or omit the suffix to target the full chunk.",
|
|
336
336
|
}),
|
|
337
337
|
content: Type.String({
|
|
338
338
|
description:
|
|
@@ -443,6 +443,12 @@ export async function executeChunkMode(
|
|
|
443
443
|
}
|
|
444
444
|
const normalizedOperations = normalizeChunkEditOperations(edits);
|
|
445
445
|
|
|
446
|
+
if (!sourceExists && normalizedOperations.some(op => op.sel)) {
|
|
447
|
+
throw new Error(
|
|
448
|
+
`File does not exist: ${path}. Cannot resolve chunk selectors on a non-existent file. Use the write tool to create a new file, or check the path for typos.`,
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
|
|
446
452
|
const chunkResult = applyChunkEdits({
|
|
447
453
|
source: rawContent,
|
|
448
454
|
language: chunkLanguage,
|
|
@@ -453,9 +459,8 @@ export async function executeChunkMode(
|
|
|
453
459
|
});
|
|
454
460
|
|
|
455
461
|
if (!chunkResult.changed) {
|
|
456
|
-
const responseText = `[No changes needed — content already matches.]\n\n${chunkResult.responseText}`;
|
|
457
462
|
return {
|
|
458
|
-
content: [{ type: "text", text:
|
|
463
|
+
content: [{ type: "text", text: "[No changes needed \u2014 content already matches.]" }],
|
|
459
464
|
details: {
|
|
460
465
|
diff: "",
|
|
461
466
|
op: sourceExists ? "update" : "create",
|
package/src/lsp/client.ts
CHANGED
|
@@ -638,15 +638,17 @@ export async function refreshFile(client: LspClient, filePath: string, signal?:
|
|
|
638
638
|
const uri = fileToUri(filePath);
|
|
639
639
|
const lockKey = `${client.name}:${uri}`;
|
|
640
640
|
|
|
641
|
-
// Check if another operation is in progress
|
|
642
641
|
const existingLock = fileOperationLocks.get(lockKey);
|
|
643
642
|
if (existingLock) {
|
|
644
643
|
await untilAborted(signal, () => existingLock);
|
|
645
644
|
}
|
|
646
645
|
|
|
647
|
-
// Lock and refresh file
|
|
648
646
|
const refreshPromise = (async () => {
|
|
649
647
|
throwIfAborted(signal);
|
|
648
|
+
// Drop cached diagnostics for this URI before asking the server to recompute.
|
|
649
|
+
// Otherwise an unrelated publishDiagnostics notification can advance the global
|
|
650
|
+
// diagnostics version and cause waiters to accept stale unversioned diagnostics.
|
|
651
|
+
client.diagnostics.delete(uri);
|
|
650
652
|
const info = client.openFiles.get(uri);
|
|
651
653
|
|
|
652
654
|
if (!info) {
|
package/src/lsp/index.ts
CHANGED
|
@@ -47,7 +47,6 @@ import {
|
|
|
47
47
|
} from "./types";
|
|
48
48
|
import {
|
|
49
49
|
applyCodeAction,
|
|
50
|
-
collectGlobMatches,
|
|
51
50
|
dedupeWorkspaceSymbols,
|
|
52
51
|
extractHoverText,
|
|
53
52
|
fileToUri,
|
|
@@ -60,8 +59,8 @@ import {
|
|
|
60
59
|
formatLocation,
|
|
61
60
|
formatSymbolInformation,
|
|
62
61
|
formatWorkspaceEdit,
|
|
63
|
-
hasGlobPattern,
|
|
64
62
|
readLocationContext,
|
|
63
|
+
resolveDiagnosticTargets,
|
|
65
64
|
resolveSymbolColumn,
|
|
66
65
|
sortDiagnostics,
|
|
67
66
|
symbolKindToIcon,
|
|
@@ -1172,13 +1171,9 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
|
|
|
1172
1171
|
|
|
1173
1172
|
let targets: string[];
|
|
1174
1173
|
let truncatedGlobTargets = false;
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
truncatedGlobTargets = globMatches.truncated;
|
|
1179
|
-
} else {
|
|
1180
|
-
targets = [file];
|
|
1181
|
-
}
|
|
1174
|
+
const resolvedTargets = await resolveDiagnosticTargets(file, this.session.cwd, MAX_GLOB_DIAGNOSTIC_TARGETS);
|
|
1175
|
+
targets = resolvedTargets.matches;
|
|
1176
|
+
truncatedGlobTargets = resolvedTargets.truncated;
|
|
1182
1177
|
|
|
1183
1178
|
if (targets.length === 0) {
|
|
1184
1179
|
return {
|
package/src/lsp/utils.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
export { truncate } from "@oh-my-pi/pi-utils";
|
|
2
2
|
|
|
3
|
+
import * as fs from "node:fs/promises";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { isEnoent } from "@oh-my-pi/pi-utils";
|
|
5
6
|
import { type Theme, theme } from "../modes/theme/theme";
|
|
7
|
+
import { resolveToCwd } from "../tools/path-utils";
|
|
6
8
|
import type {
|
|
7
9
|
CodeAction,
|
|
8
10
|
Command,
|
|
@@ -550,6 +552,30 @@ export async function collectGlobMatches(
|
|
|
550
552
|
}
|
|
551
553
|
return { matches, truncated: false };
|
|
552
554
|
}
|
|
555
|
+
|
|
556
|
+
export async function resolveDiagnosticTargets(
|
|
557
|
+
file: string,
|
|
558
|
+
cwd: string,
|
|
559
|
+
maxMatches: number,
|
|
560
|
+
): Promise<{ matches: string[]; truncated: boolean }> {
|
|
561
|
+
if (!hasGlobPattern(file)) {
|
|
562
|
+
return { matches: [file], truncated: false };
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const resolved = resolveToCwd(file, cwd);
|
|
566
|
+
try {
|
|
567
|
+
const stat = await fs.stat(resolved);
|
|
568
|
+
if (stat.isFile()) {
|
|
569
|
+
return { matches: [file], truncated: false };
|
|
570
|
+
}
|
|
571
|
+
} catch (error) {
|
|
572
|
+
if (!isEnoent(error)) {
|
|
573
|
+
throw error;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
return collectGlobMatches(file, cwd, maxMatches);
|
|
578
|
+
}
|
|
553
579
|
// =============================================================================
|
|
554
580
|
// Hover Content Extraction
|
|
555
581
|
// =============================================================================
|