@oh-my-pi/pi-coding-agent 17.2.11 → 17.2.12
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 +13 -0
- package/dist/{CHANGELOG-d9xenpn9.md → CHANGELOG-k9ghy5sn.md} +13 -0
- package/dist/cli.js +10795 -10791
- package/dist/types/advisor/runtime.d.ts +1 -1
- package/dist/types/config/custom-models.d.ts +31 -0
- package/dist/types/config/model-config-values.d.ts +19 -0
- package/dist/types/config/model-patch.d.ts +93 -0
- package/dist/types/config/model-provider-discovery.d.ts +51 -0
- package/dist/types/config/model-registry.d.ts +5 -52
- package/dist/types/config/settings.d.ts +5 -3
- package/dist/types/debug/raw-sse-buffer.d.ts +9 -0
- package/dist/types/discovery/agent-plugin-format.d.ts +6 -13
- package/dist/types/discovery/contained-path.d.ts +2 -3
- package/dist/types/eval/executor-base.d.ts +8 -2
- package/dist/types/eval/kernel-session-registry.d.ts +60 -0
- package/dist/types/export/share.d.ts +1 -1
- package/dist/types/lsp/diagnostics.d.ts +96 -0
- package/dist/types/lsp/index.d.ts +7 -128
- package/dist/types/lsp/servers.d.ts +72 -0
- package/dist/types/lsp/tool.d.ts +42 -0
- package/dist/types/lsp/workspace-diagnostics.d.ts +12 -0
- package/dist/types/lsp/writethrough.d.ts +33 -0
- package/dist/types/mcp/transports/header-policy.d.ts +2 -1
- package/dist/types/modes/components/agent-hub-projection.d.ts +2 -7
- package/dist/types/modes/components/agent-hub-renderer.d.ts +0 -7
- package/dist/types/modes/theme/color.d.ts +19 -0
- package/dist/types/modes/theme/loader.d.ts +19 -0
- package/dist/types/modes/theme/schema.d.ts +175 -0
- package/dist/types/modes/theme/symbols.d.ts +28 -0
- package/dist/types/modes/theme/theme-class.d.ts +244 -0
- package/dist/types/modes/theme/theme.d.ts +9 -280
- package/dist/types/modes/theme/tui-adapters.d.ts +13 -0
- package/dist/types/registry/agent-registry.d.ts +1 -3
- package/dist/types/secrets/index.d.ts +3 -1
- package/dist/types/secrets/message-transform.d.ts +40 -0
- package/dist/types/secrets/obfuscator.d.ts +0 -108
- package/dist/types/secrets/placeholder-scan.d.ts +95 -0
- package/dist/types/secrets/placeholder.d.ts +94 -0
- package/dist/types/secrets/replacement.d.ts +82 -0
- package/dist/types/session/agent-session-types.d.ts +6 -0
- package/dist/types/session/agent-session.d.ts +1 -1
- package/dist/types/session/session-handoff.d.ts +3 -3
- package/dist/types/session/session-manager.d.ts +32 -0
- package/dist/types/session/session-provider-boundary.d.ts +1 -1
- package/dist/types/session/turn-recovery.d.ts +2 -2
- package/dist/types/slash-commands/builtin-collaboration.d.ts +2 -0
- package/dist/types/slash-commands/builtin-completions.d.ts +36 -0
- package/dist/types/slash-commands/builtin-control.d.ts +2 -0
- package/dist/types/slash-commands/builtin-lifecycle.d.ts +3 -0
- package/dist/types/slash-commands/builtin-marketplace.d.ts +11 -0
- package/dist/types/slash-commands/builtin-modes.d.ts +5 -0
- package/dist/types/slash-commands/builtin-registry.d.ts +1 -1
- package/dist/types/slash-commands/builtin-session.d.ts +2 -0
- package/dist/types/tools/gh-common.d.ts +69 -0
- package/dist/types/tools/gh-pr-checkout.d.ts +84 -0
- package/dist/types/tools/gh-pr-diff.d.ts +102 -0
- package/dist/types/tools/gh-run-watch.d.ts +78 -0
- package/dist/types/tools/gh-search.d.ts +61 -0
- package/dist/types/tools/gh-types.d.ts +340 -0
- package/dist/types/tools/gh-view.d.ts +88 -0
- package/dist/types/tools/gh.d.ts +4 -201
- package/dist/types/tools/read-archive.d.ts +16 -0
- package/dist/types/tools/read-format.d.ts +104 -0
- package/dist/types/tools/read-path-resolution.d.ts +19 -0
- package/dist/types/tools/read-pdf-images.d.ts +12 -0
- package/dist/types/tools/read-renderer.d.ts +24 -0
- package/dist/types/tools/read-selector.d.ts +27 -0
- package/dist/types/tools/read-sqlite.d.ts +16 -0
- package/dist/types/tools/read-summary.d.ts +19 -0
- package/dist/types/tools/read.d.ts +1 -23
- package/dist/types/tools/shell-tokenize.d.ts +2 -1
- package/dist/types/utils/changelog.d.ts +0 -5
- package/package.json +13 -13
- package/src/advisor/runtime.ts +12 -7
- package/src/config/custom-models.ts +188 -0
- package/src/config/model-config-values.ts +128 -0
- package/src/config/model-patch.ts +252 -0
- package/src/config/model-provider-discovery.ts +132 -0
- package/src/config/model-registry.ts +64 -704
- package/src/config/settings.ts +7 -3
- package/src/debug/raw-sse-buffer.ts +20 -0
- package/src/discovery/agent-plugin-format.ts +7 -7
- package/src/discovery/contained-path.ts +2 -5
- package/src/edit/hashline/diff.ts +5 -1
- package/src/eval/executor-base.ts +39 -6
- package/src/eval/jl/executor.ts +82 -371
- package/src/eval/kernel-session-registry.ts +398 -0
- package/src/eval/py/executor.ts +53 -261
- package/src/eval/rb/executor.ts +36 -279
- package/src/export/share.ts +2 -1
- package/src/lsp/diagnostics.ts +516 -0
- package/src/lsp/index.ts +20 -2819
- package/src/lsp/servers.ts +296 -0
- package/src/lsp/tool.ts +1352 -0
- package/src/lsp/workspace-diagnostics.ts +170 -0
- package/src/lsp/writethrough.ts +561 -0
- package/src/mcp/transports/header-policy.ts +1 -1
- package/src/modes/components/agent-hub-projection.ts +2 -2
- package/src/modes/components/agent-hub-renderer.ts +3 -7
- package/src/modes/controllers/command-controller.ts +4 -1
- package/src/modes/theme/color.ts +133 -0
- package/src/modes/theme/loader.ts +178 -0
- package/src/modes/theme/schema.ts +263 -0
- package/src/modes/theme/symbols.ts +1000 -0
- package/src/modes/theme/theme-class.ts +611 -0
- package/src/modes/theme/theme.ts +27 -2453
- package/src/modes/theme/tui-adapters.ts +279 -0
- package/src/registry/agent-registry.ts +2 -2
- package/src/secrets/index.ts +6 -12
- package/src/secrets/message-transform.ts +287 -0
- package/src/secrets/obfuscator.ts +39 -1303
- package/src/secrets/placeholder-scan.ts +506 -0
- package/src/secrets/placeholder.ts +309 -0
- package/src/secrets/replacement.ts +216 -0
- package/src/session/agent-session-types.ts +6 -0
- package/src/session/agent-session.ts +113 -7
- package/src/session/indexed-session-storage.ts +7 -2
- package/src/session/session-advisors.ts +32 -34
- package/src/session/session-handoff.ts +39 -20
- package/src/session/session-manager.ts +67 -3
- package/src/session/session-provider-boundary.ts +3 -6
- package/src/session/turn-recovery.ts +21 -12
- package/src/slash-commands/builtin-collaboration.ts +504 -0
- package/src/slash-commands/builtin-completions.ts +291 -0
- package/src/slash-commands/builtin-control.ts +78 -0
- package/src/slash-commands/builtin-lifecycle.ts +506 -0
- package/src/slash-commands/builtin-marketplace.ts +567 -0
- package/src/slash-commands/builtin-modes.ts +518 -0
- package/src/slash-commands/builtin-registry.ts +21 -3000
- package/src/slash-commands/builtin-session.ts +592 -0
- package/src/task/executor.ts +17 -14
- package/src/tools/gh-common.ts +288 -0
- package/src/tools/gh-pr-checkout.ts +679 -0
- package/src/tools/gh-pr-diff.ts +473 -0
- package/src/tools/gh-run-watch.ts +1015 -0
- package/src/tools/gh-search.ts +500 -0
- package/src/tools/gh-types.ts +379 -0
- package/src/tools/gh-view.ts +612 -0
- package/src/tools/gh.ts +109 -3821
- package/src/tools/read-archive.ts +209 -0
- package/src/tools/read-format.ts +593 -0
- package/src/tools/read-path-resolution.ts +36 -0
- package/src/tools/read-pdf-images.ts +250 -0
- package/src/tools/read-renderer.ts +289 -0
- package/src/tools/read-selector.ts +84 -0
- package/src/tools/read-sqlite.ts +215 -0
- package/src/tools/read-summary.ts +199 -0
- package/src/tools/read.ts +77 -1820
- package/src/tools/shell-tokenize.ts +1 -1
- package/src/utils/changelog.ts +1 -1
- package/src/utils/title-generator.ts +3 -1
- package/src/web/search/providers/zai.ts +12 -3
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import { isEnoent, logger, once, untilAborted } from "@oh-my-pi/pi-utils";
|
|
3
|
+
import type { BunFile } from "bun";
|
|
4
|
+
import { FileChangeType, notifyWorkspaceWatchedFiles } from "./client";
|
|
5
|
+
import { getServersForFile } from "./config";
|
|
6
|
+
import {
|
|
7
|
+
captureDiagnosticVersions,
|
|
8
|
+
captureOpenFileVersions,
|
|
9
|
+
DEFERRED_DIAGNOSTICS_WAIT_TIMEOUT_MS,
|
|
10
|
+
type FileDiagnosticsResult,
|
|
11
|
+
FileFormatResult,
|
|
12
|
+
formatContent,
|
|
13
|
+
getDiagnosticsForFile,
|
|
14
|
+
INLINE_DIAGNOSTICS_WAIT_TIMEOUT_MS,
|
|
15
|
+
limitDiagnosticMessages,
|
|
16
|
+
type ServerVersionMap,
|
|
17
|
+
} from "./diagnostics";
|
|
18
|
+
import { getConfig, notifyFileSaved, splitServers, syncFileContent } from "./servers";
|
|
19
|
+
import type { ServerConfig } from "./types";
|
|
20
|
+
import { summarizeDiagnosticMessages } from "./utils";
|
|
21
|
+
|
|
22
|
+
/** Options for creating the LSP writethrough callback */
|
|
23
|
+
export interface WritethroughOptions {
|
|
24
|
+
/** Whether to format the file using LSP after writing */
|
|
25
|
+
enableFormat?: boolean;
|
|
26
|
+
/** Whether to get LSP diagnostics after writing */
|
|
27
|
+
enableDiagnostics?: boolean;
|
|
28
|
+
/** Called when diagnostics arrive after the main timeout. */
|
|
29
|
+
onDeferredDiagnostics?: (diagnostics: FileDiagnosticsResult) => void;
|
|
30
|
+
/** Signal to cancel a pending deferred diagnostics fetch. */
|
|
31
|
+
deferredSignal?: AbortSignal;
|
|
32
|
+
/** Transform diagnostics before surfacing them after a successful fetch. */
|
|
33
|
+
transformDiagnostics?: (absPath: string, result: FileDiagnosticsResult) => FileDiagnosticsResult;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Internal resolved form of {@link WritethroughOptions} that the writethrough machinery operates on. */
|
|
37
|
+
type ResolvedWritethroughOptions = {
|
|
38
|
+
enableFormat: boolean;
|
|
39
|
+
enableDiagnostics: boolean;
|
|
40
|
+
transformDiagnostics?: (absPath: string, result: FileDiagnosticsResult) => FileDiagnosticsResult;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/** Per-file deferred LSP diagnostics wiring for {@link WritethroughCallback}. */
|
|
44
|
+
export type WritethroughDeferredHandle = {
|
|
45
|
+
onDeferredDiagnostics: (diagnostics: FileDiagnosticsResult) => void;
|
|
46
|
+
signal: AbortSignal;
|
|
47
|
+
finalize: (diagnostics: FileDiagnosticsResult | undefined) => void;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** Callback type for the LSP writethrough */
|
|
51
|
+
export type WritethroughCallback = (
|
|
52
|
+
dst: string,
|
|
53
|
+
content: string,
|
|
54
|
+
signal?: AbortSignal,
|
|
55
|
+
file?: BunFile,
|
|
56
|
+
batch?: LspWritethroughBatchRequest,
|
|
57
|
+
getDeferred?: (dst: string) => WritethroughDeferredHandle | undefined,
|
|
58
|
+
) => Promise<FileDiagnosticsResult | undefined>;
|
|
59
|
+
|
|
60
|
+
/** No-op writethrough callback */
|
|
61
|
+
export async function writethroughNoop(
|
|
62
|
+
dst: string,
|
|
63
|
+
content: string,
|
|
64
|
+
_signal?: AbortSignal,
|
|
65
|
+
file?: BunFile,
|
|
66
|
+
_batch?: LspWritethroughBatchRequest,
|
|
67
|
+
_getDeferred?: (dst: string) => WritethroughDeferredHandle | undefined,
|
|
68
|
+
): Promise<FileDiagnosticsResult | undefined> {
|
|
69
|
+
if (file) {
|
|
70
|
+
await file.write(content);
|
|
71
|
+
} else {
|
|
72
|
+
await Bun.write(dst, content);
|
|
73
|
+
}
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
interface PendingWritethrough {
|
|
78
|
+
dst: string;
|
|
79
|
+
file?: BunFile;
|
|
80
|
+
changeType: FileChangeType;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
interface RunLspWritethroughOptions {
|
|
84
|
+
contentAlreadyWritten?: boolean;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
interface LspWritethroughBatchRequest {
|
|
88
|
+
id: string;
|
|
89
|
+
flush: boolean;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
interface LspWritethroughBatchState {
|
|
93
|
+
entries: Map<string, PendingWritethrough>;
|
|
94
|
+
options: ResolvedWritethroughOptions;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const writethroughBatches = new Map<string, LspWritethroughBatchState>();
|
|
98
|
+
|
|
99
|
+
function getOrCreateWritethroughBatch(id: string, options: ResolvedWritethroughOptions): LspWritethroughBatchState {
|
|
100
|
+
const existing = writethroughBatches.get(id);
|
|
101
|
+
if (existing) {
|
|
102
|
+
existing.options.enableFormat ||= options.enableFormat;
|
|
103
|
+
existing.options.enableDiagnostics ||= options.enableDiagnostics;
|
|
104
|
+
existing.options.transformDiagnostics ??= options.transformDiagnostics;
|
|
105
|
+
return existing;
|
|
106
|
+
}
|
|
107
|
+
const batch: LspWritethroughBatchState = {
|
|
108
|
+
entries: new Map<string, PendingWritethrough>(),
|
|
109
|
+
options: { ...options },
|
|
110
|
+
};
|
|
111
|
+
writethroughBatches.set(id, batch);
|
|
112
|
+
return batch;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function flushLspWritethroughBatch(
|
|
116
|
+
id: string,
|
|
117
|
+
cwd: string,
|
|
118
|
+
signal?: AbortSignal,
|
|
119
|
+
): Promise<FileDiagnosticsResult | undefined> {
|
|
120
|
+
const state = writethroughBatches.get(id);
|
|
121
|
+
if (!state) {
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
writethroughBatches.delete(id);
|
|
125
|
+
return flushWritethroughBatch(Array.from(state.entries.values()), cwd, state.options, signal);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function mergeDiagnostics(
|
|
129
|
+
results: Array<FileDiagnosticsResult | undefined>,
|
|
130
|
+
options: ResolvedWritethroughOptions,
|
|
131
|
+
): FileDiagnosticsResult | undefined {
|
|
132
|
+
const messages: string[] = [];
|
|
133
|
+
const servers = new Set<string>();
|
|
134
|
+
let hasResults = false;
|
|
135
|
+
let hasFormatter = false;
|
|
136
|
+
let formatted = false;
|
|
137
|
+
|
|
138
|
+
for (const result of results) {
|
|
139
|
+
if (!result) continue;
|
|
140
|
+
hasResults = true;
|
|
141
|
+
if (result.server) {
|
|
142
|
+
for (const server of result.server.split(",")) {
|
|
143
|
+
const trimmed = server.trim();
|
|
144
|
+
if (trimmed) {
|
|
145
|
+
servers.add(trimmed);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (result.messages.length > 0) {
|
|
150
|
+
messages.push(...result.messages);
|
|
151
|
+
}
|
|
152
|
+
if (result.formatter !== undefined) {
|
|
153
|
+
hasFormatter = true;
|
|
154
|
+
if (result.formatter === FileFormatResult.FORMATTED) {
|
|
155
|
+
formatted = true;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (!hasResults && !hasFormatter) {
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
let summary = options.enableDiagnostics ? "no issues" : "OK";
|
|
165
|
+
let errored = false;
|
|
166
|
+
let limitedMessages = messages;
|
|
167
|
+
if (messages.length > 0) {
|
|
168
|
+
const summaryInfo = summarizeDiagnosticMessages(messages);
|
|
169
|
+
summary = summaryInfo.summary;
|
|
170
|
+
errored = summaryInfo.errored;
|
|
171
|
+
limitedMessages = limitDiagnosticMessages(messages);
|
|
172
|
+
}
|
|
173
|
+
const formatter = hasFormatter ? (formatted ? FileFormatResult.FORMATTED : FileFormatResult.UNCHANGED) : undefined;
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
server: servers.size > 0 ? Array.from(servers).join(", ") : undefined,
|
|
177
|
+
messages: limitedMessages,
|
|
178
|
+
summary,
|
|
179
|
+
errored,
|
|
180
|
+
formatter,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function scheduleDeferredDiagnosticsFetch(args: {
|
|
185
|
+
dst: string;
|
|
186
|
+
cwd: string;
|
|
187
|
+
servers: Array<[string, ServerConfig]>;
|
|
188
|
+
minVersions: ServerVersionMap | undefined;
|
|
189
|
+
expectedDocumentVersions: ServerVersionMap | undefined;
|
|
190
|
+
signal: AbortSignal;
|
|
191
|
+
callback: (diagnostics: FileDiagnosticsResult) => void;
|
|
192
|
+
}): Promise<void> {
|
|
193
|
+
try {
|
|
194
|
+
const deferredTimeout = AbortSignal.timeout(25_000);
|
|
195
|
+
const combined = AbortSignal.any([args.signal, deferredTimeout]);
|
|
196
|
+
const diagnostics = await getDiagnosticsForFile(args.dst, args.cwd, args.servers, {
|
|
197
|
+
signal: combined,
|
|
198
|
+
minVersions: args.minVersions,
|
|
199
|
+
expectedDocumentVersions: args.expectedDocumentVersions,
|
|
200
|
+
timeoutMs: DEFERRED_DIAGNOSTICS_WAIT_TIMEOUT_MS,
|
|
201
|
+
});
|
|
202
|
+
if (args.signal.aborted || diagnostics === undefined) return;
|
|
203
|
+
args.callback(diagnostics);
|
|
204
|
+
} catch {
|
|
205
|
+
// Cancelled or LSP gave up; silently discard.
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Fetch post-write diagnostics without making the edit/write block on a slow
|
|
211
|
+
* language server.
|
|
212
|
+
*
|
|
213
|
+
* Blocks inline only briefly ({@link INLINE_DIAGNOSTICS_WAIT_TIMEOUT_MS}) for a
|
|
214
|
+
* fresh result. Freshness is enforced by the pre-edit `minVersions` baseline:
|
|
215
|
+
* exact document-version matches return immediately, and unversioned/mismatched
|
|
216
|
+
* publishes must settle with no newer publish before inline acceptance. If
|
|
217
|
+
* nothing fresh arrives in the inline window and a deferred
|
|
218
|
+
* channel is available, the in-flight fetch is handed off to deliver late via
|
|
219
|
+
* `onDeferredDiagnostics`, and this returns `undefined` so the tool result
|
|
220
|
+
* lands immediately. Without a deferred channel (direct/CI callers) it blocks
|
|
221
|
+
* for the standard budget so the result is still returned inline.
|
|
222
|
+
*/
|
|
223
|
+
async function fetchDiagnosticsWithDeferral(args: {
|
|
224
|
+
dst: string;
|
|
225
|
+
cwd: string;
|
|
226
|
+
servers: Array<[string, ServerConfig]>;
|
|
227
|
+
minVersions: ServerVersionMap | undefined;
|
|
228
|
+
expectedDocumentVersions: ServerVersionMap | undefined;
|
|
229
|
+
transformDiagnostics?: ResolvedWritethroughOptions["transformDiagnostics"];
|
|
230
|
+
deferred?: { onDeferredDiagnostics: (diagnostics: FileDiagnosticsResult) => void; signal: AbortSignal };
|
|
231
|
+
signal?: AbortSignal;
|
|
232
|
+
}): Promise<FileDiagnosticsResult | undefined> {
|
|
233
|
+
const { dst, cwd, servers, minVersions, expectedDocumentVersions, transformDiagnostics, deferred, signal } = args;
|
|
234
|
+
const apply = (d: FileDiagnosticsResult | undefined) =>
|
|
235
|
+
d && transformDiagnostics ? transformDiagnostics(dst, d) : d;
|
|
236
|
+
|
|
237
|
+
if (!deferred) {
|
|
238
|
+
// No late-injection channel: block for the standard budget and return inline.
|
|
239
|
+
return apply(
|
|
240
|
+
await getDiagnosticsForFile(dst, cwd, servers, {
|
|
241
|
+
signal,
|
|
242
|
+
minVersions,
|
|
243
|
+
expectedDocumentVersions,
|
|
244
|
+
}),
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// One background fetch with a generous inner budget; await it only briefly inline.
|
|
249
|
+
const fetchPromise = getDiagnosticsForFile(dst, cwd, servers, {
|
|
250
|
+
signal: deferred.signal,
|
|
251
|
+
minVersions,
|
|
252
|
+
expectedDocumentVersions,
|
|
253
|
+
timeoutMs: DEFERRED_DIAGNOSTICS_WAIT_TIMEOUT_MS,
|
|
254
|
+
});
|
|
255
|
+
const INLINE_TIMEOUT = Symbol("inline-diagnostics-timeout");
|
|
256
|
+
const raced = await Promise.race([
|
|
257
|
+
fetchPromise,
|
|
258
|
+
Bun.sleep(INLINE_DIAGNOSTICS_WAIT_TIMEOUT_MS).then(() => INLINE_TIMEOUT),
|
|
259
|
+
]);
|
|
260
|
+
if (raced !== INLINE_TIMEOUT) {
|
|
261
|
+
return apply(raced as FileDiagnosticsResult | undefined);
|
|
262
|
+
}
|
|
263
|
+
// Slow server: deliver late via the deferred channel; nothing inline. The
|
|
264
|
+
// deferred sink (edit tool) applies its own dedup, so pass the raw result.
|
|
265
|
+
void fetchPromise
|
|
266
|
+
.then(diagnostics => {
|
|
267
|
+
if (diagnostics && !deferred.signal.aborted) deferred.onDeferredDiagnostics(diagnostics);
|
|
268
|
+
})
|
|
269
|
+
.catch(() => {});
|
|
270
|
+
return undefined;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async function runLspWritethrough(
|
|
274
|
+
dst: string,
|
|
275
|
+
content: string,
|
|
276
|
+
cwd: string,
|
|
277
|
+
options: ResolvedWritethroughOptions,
|
|
278
|
+
changeType: FileChangeType,
|
|
279
|
+
signal?: AbortSignal,
|
|
280
|
+
file?: BunFile,
|
|
281
|
+
deferred?: {
|
|
282
|
+
onDeferredDiagnostics: (diagnostics: FileDiagnosticsResult) => void;
|
|
283
|
+
signal: AbortSignal;
|
|
284
|
+
},
|
|
285
|
+
runOptions?: RunLspWritethroughOptions,
|
|
286
|
+
): Promise<FileDiagnosticsResult | undefined> {
|
|
287
|
+
const { enableFormat, enableDiagnostics } = options;
|
|
288
|
+
const contentAlreadyWritten = runOptions?.contentAlreadyWritten ?? false;
|
|
289
|
+
|
|
290
|
+
let finalContent = content;
|
|
291
|
+
const writeContent = async (value: string) => (file ? file.write(value) : Bun.write(dst, value));
|
|
292
|
+
const getWritePromise = once(() =>
|
|
293
|
+
contentAlreadyWritten && finalContent === content ? Promise.resolve() : writeContent(finalContent),
|
|
294
|
+
);
|
|
295
|
+
let writeNotified = false;
|
|
296
|
+
const notifyWriteCommitted = async (notifySignal: AbortSignal | undefined = signal) => {
|
|
297
|
+
if (writeNotified) return;
|
|
298
|
+
writeNotified = true;
|
|
299
|
+
try {
|
|
300
|
+
await notifyWorkspaceWatchedFiles(cwd, [{ filePath: dst, type: changeType }], notifySignal);
|
|
301
|
+
} catch (error) {
|
|
302
|
+
if (notifySignal?.aborted && !signal?.aborted) {
|
|
303
|
+
// The operation budget died mid-notify while the caller is still
|
|
304
|
+
// live: allow the post-write retry below to re-announce with the
|
|
305
|
+
// caller's signal (didChangeWatchedFiles is idempotent).
|
|
306
|
+
writeNotified = false;
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
throw error;
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
if (!enableFormat && !enableDiagnostics) {
|
|
313
|
+
await getWritePromise();
|
|
314
|
+
await notifyWriteCommitted();
|
|
315
|
+
return undefined;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const config = getConfig(cwd);
|
|
319
|
+
const servers = getServersForFile(config, dst);
|
|
320
|
+
|
|
321
|
+
if (servers.length === 0) {
|
|
322
|
+
await getWritePromise();
|
|
323
|
+
await notifyWriteCommitted();
|
|
324
|
+
return undefined;
|
|
325
|
+
}
|
|
326
|
+
const { lspServers, customLinterServers } = splitServers(servers);
|
|
327
|
+
const useCustomFormatter = enableFormat && customLinterServers.length > 0;
|
|
328
|
+
|
|
329
|
+
// Capture diagnostic versions BEFORE syncing to detect stale diagnostics
|
|
330
|
+
// Bound client creation by the writethrough budget: a hung/broken server
|
|
331
|
+
// must not add its full init wait (30s default) to every edit.
|
|
332
|
+
const minVersionsPromise = enableDiagnostics ? captureDiagnosticVersions(cwd, servers, 5_000, signal) : undefined;
|
|
333
|
+
let minVersions = useCustomFormatter ? undefined : await minVersionsPromise;
|
|
334
|
+
let expectedDocumentVersions: ServerVersionMap | undefined;
|
|
335
|
+
|
|
336
|
+
let formatter: FileFormatResult | undefined;
|
|
337
|
+
let diagnostics: FileDiagnosticsResult | undefined;
|
|
338
|
+
let timedOut = false;
|
|
339
|
+
let synced = false;
|
|
340
|
+
let operationSignal: AbortSignal | undefined;
|
|
341
|
+
try {
|
|
342
|
+
const timeoutSignal = AbortSignal.timeout(5_000);
|
|
343
|
+
timeoutSignal.addEventListener(
|
|
344
|
+
"abort",
|
|
345
|
+
() => {
|
|
346
|
+
timedOut = true;
|
|
347
|
+
},
|
|
348
|
+
{ once: true },
|
|
349
|
+
);
|
|
350
|
+
operationSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
351
|
+
await untilAborted(operationSignal, async () => {
|
|
352
|
+
if (useCustomFormatter) {
|
|
353
|
+
// Custom linters operate on on-disk input; the shared pre-write also
|
|
354
|
+
// supports implementations that inspect the file before formatting.
|
|
355
|
+
if (!contentAlreadyWritten) await writeContent(content);
|
|
356
|
+
const [formattedContent, capturedVersions] = await Promise.all([
|
|
357
|
+
formatContent(dst, content, cwd, customLinterServers, operationSignal),
|
|
358
|
+
minVersionsPromise,
|
|
359
|
+
]);
|
|
360
|
+
finalContent = formattedContent;
|
|
361
|
+
minVersions = capturedVersions;
|
|
362
|
+
formatter = finalContent !== content ? FileFormatResult.FORMATTED : FileFormatResult.UNCHANGED;
|
|
363
|
+
if (!contentAlreadyWritten || finalContent !== content) await writeContent(finalContent);
|
|
364
|
+
await notifyWriteCommitted(operationSignal);
|
|
365
|
+
await syncFileContent(dst, finalContent, cwd, lspServers, operationSignal, enableDiagnostics);
|
|
366
|
+
} else {
|
|
367
|
+
// 1. Sync original content to LSP servers
|
|
368
|
+
await syncFileContent(dst, content, cwd, lspServers, operationSignal);
|
|
369
|
+
|
|
370
|
+
// 2. Format in-memory via LSP
|
|
371
|
+
if (enableFormat) {
|
|
372
|
+
finalContent = await formatContent(dst, content, cwd, lspServers, operationSignal);
|
|
373
|
+
formatter = finalContent !== content ? FileFormatResult.FORMATTED : FileFormatResult.UNCHANGED;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// 3. If formatted, sync formatted content to LSP servers
|
|
377
|
+
if (finalContent !== content) {
|
|
378
|
+
await syncFileContent(dst, finalContent, cwd, lspServers, operationSignal);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// 4. Write to disk
|
|
382
|
+
await getWritePromise();
|
|
383
|
+
await notifyWriteCommitted(operationSignal);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (enableDiagnostics) {
|
|
387
|
+
expectedDocumentVersions = await captureOpenFileVersions(dst, cwd, lspServers, operationSignal);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// 5. Notify saved to LSP servers
|
|
391
|
+
await notifyFileSaved(dst, cwd, lspServers, operationSignal, !useCustomFormatter || enableDiagnostics);
|
|
392
|
+
});
|
|
393
|
+
synced = true;
|
|
394
|
+
} catch {
|
|
395
|
+
if (timedOut) {
|
|
396
|
+
formatter = undefined;
|
|
397
|
+
diagnostics = undefined;
|
|
398
|
+
// Schedule background diagnostic fetch if caller wants deferred results
|
|
399
|
+
if (deferred && !deferred.signal.aborted && enableDiagnostics) {
|
|
400
|
+
void scheduleDeferredDiagnosticsFetch({
|
|
401
|
+
dst,
|
|
402
|
+
cwd,
|
|
403
|
+
servers,
|
|
404
|
+
minVersions,
|
|
405
|
+
expectedDocumentVersions,
|
|
406
|
+
signal: deferred.signal,
|
|
407
|
+
callback: deferred.onDeferredDiagnostics,
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
await getWritePromise();
|
|
412
|
+
// The write above committed even though the operation budget elapsed:
|
|
413
|
+
// announce it on the caller's signal — the dead `operationSignal` would
|
|
414
|
+
// abort the notify before it ever reaches the server.
|
|
415
|
+
await notifyWriteCommitted();
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (synced && enableDiagnostics) {
|
|
419
|
+
diagnostics = await fetchDiagnosticsWithDeferral({
|
|
420
|
+
dst,
|
|
421
|
+
cwd,
|
|
422
|
+
servers,
|
|
423
|
+
minVersions,
|
|
424
|
+
expectedDocumentVersions,
|
|
425
|
+
transformDiagnostics: options.transformDiagnostics,
|
|
426
|
+
deferred,
|
|
427
|
+
signal,
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (formatter !== undefined) {
|
|
432
|
+
diagnostics ??= {
|
|
433
|
+
server: servers.map(([name]) => name).join(", "),
|
|
434
|
+
messages: [],
|
|
435
|
+
summary: "OK",
|
|
436
|
+
errored: false,
|
|
437
|
+
};
|
|
438
|
+
diagnostics.formatter = formatter;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return diagnostics;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
async function flushWritethroughBatch(
|
|
445
|
+
batch: PendingWritethrough[],
|
|
446
|
+
cwd: string,
|
|
447
|
+
options: ResolvedWritethroughOptions,
|
|
448
|
+
signal?: AbortSignal,
|
|
449
|
+
getDeferred?: (dst: string) => WritethroughDeferredHandle | undefined,
|
|
450
|
+
): Promise<FileDiagnosticsResult | undefined> {
|
|
451
|
+
if (batch.length === 0) {
|
|
452
|
+
return undefined;
|
|
453
|
+
}
|
|
454
|
+
const results: Array<FileDiagnosticsResult | undefined> = [];
|
|
455
|
+
for (const entry of batch) {
|
|
456
|
+
const bundle = getDeferred?.(entry.dst);
|
|
457
|
+
let content: string;
|
|
458
|
+
try {
|
|
459
|
+
content = await fs.promises.readFile(entry.dst, "utf8");
|
|
460
|
+
} catch (error) {
|
|
461
|
+
if (!isEnoent(error)) throw error;
|
|
462
|
+
bundle?.finalize(undefined);
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
const deferredInner =
|
|
466
|
+
bundle &&
|
|
467
|
+
({
|
|
468
|
+
onDeferredDiagnostics: bundle.onDeferredDiagnostics,
|
|
469
|
+
signal: bundle.signal,
|
|
470
|
+
} as const);
|
|
471
|
+
const diag = await runLspWritethrough(
|
|
472
|
+
entry.dst,
|
|
473
|
+
content,
|
|
474
|
+
cwd,
|
|
475
|
+
options,
|
|
476
|
+
entry.changeType,
|
|
477
|
+
signal,
|
|
478
|
+
entry.file,
|
|
479
|
+
deferredInner,
|
|
480
|
+
{ contentAlreadyWritten: true },
|
|
481
|
+
);
|
|
482
|
+
bundle?.finalize(diag);
|
|
483
|
+
results.push(diag);
|
|
484
|
+
}
|
|
485
|
+
return mergeDiagnostics(results, options);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/** Create a writethrough callback for LSP aware write operations */
|
|
489
|
+
export function createLspWritethrough(cwd: string, options?: WritethroughOptions): WritethroughCallback {
|
|
490
|
+
const resolvedOptions: ResolvedWritethroughOptions = {
|
|
491
|
+
enableFormat: options?.enableFormat ?? false,
|
|
492
|
+
enableDiagnostics: options?.enableDiagnostics ?? false,
|
|
493
|
+
transformDiagnostics: options?.transformDiagnostics,
|
|
494
|
+
};
|
|
495
|
+
return async (
|
|
496
|
+
dst: string,
|
|
497
|
+
content: string,
|
|
498
|
+
signal?: AbortSignal,
|
|
499
|
+
file?: BunFile,
|
|
500
|
+
batch?: LspWritethroughBatchRequest,
|
|
501
|
+
getDeferred?: (dst: string) => WritethroughDeferredHandle | undefined,
|
|
502
|
+
) => {
|
|
503
|
+
const changeType = (await Bun.file(dst).exists()) ? FileChangeType.Changed : FileChangeType.Created;
|
|
504
|
+
if (!batch) {
|
|
505
|
+
const bundle = getDeferred?.(dst);
|
|
506
|
+
const deferredInner =
|
|
507
|
+
bundle &&
|
|
508
|
+
({
|
|
509
|
+
onDeferredDiagnostics: bundle.onDeferredDiagnostics,
|
|
510
|
+
signal: bundle.signal,
|
|
511
|
+
} as const);
|
|
512
|
+
const diagnostics = await runLspWritethrough(
|
|
513
|
+
dst,
|
|
514
|
+
content,
|
|
515
|
+
cwd,
|
|
516
|
+
resolvedOptions,
|
|
517
|
+
changeType,
|
|
518
|
+
signal,
|
|
519
|
+
file,
|
|
520
|
+
deferredInner,
|
|
521
|
+
);
|
|
522
|
+
bundle?.finalize(diagnostics);
|
|
523
|
+
return diagnostics;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// File commits are never deferred: the batch owns only LSP post-processing,
|
|
527
|
+
// so a later flush cannot replay an obsolete whole-file snapshot.
|
|
528
|
+
try {
|
|
529
|
+
await writethroughNoop(dst, content, signal, file);
|
|
530
|
+
} catch (error) {
|
|
531
|
+
if (batch.flush) {
|
|
532
|
+
const pending = writethroughBatches.get(batch.id);
|
|
533
|
+
if (pending) {
|
|
534
|
+
writethroughBatches.delete(batch.id);
|
|
535
|
+
try {
|
|
536
|
+
await flushWritethroughBatch(
|
|
537
|
+
Array.from(pending.entries.values()),
|
|
538
|
+
cwd,
|
|
539
|
+
pending.options,
|
|
540
|
+
signal,
|
|
541
|
+
getDeferred,
|
|
542
|
+
);
|
|
543
|
+
} catch (flushError) {
|
|
544
|
+
logger.warn("Failed to flush pending LSP batch after final write failure", {
|
|
545
|
+
batchId: batch.id,
|
|
546
|
+
error: flushError instanceof Error ? flushError.message : String(flushError),
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
throw error;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const state = getOrCreateWritethroughBatch(batch.id, resolvedOptions);
|
|
555
|
+
state.entries.set(dst, { dst, file, changeType });
|
|
556
|
+
if (!batch.flush) return undefined;
|
|
557
|
+
|
|
558
|
+
writethroughBatches.delete(batch.id);
|
|
559
|
+
return flushWritethroughBatch(Array.from(state.entries.values()), cwd, state.options, signal, getDeferred);
|
|
560
|
+
};
|
|
561
|
+
}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
/** Header buckets for one MCP HTTP request. */
|
|
17
|
-
|
|
17
|
+
interface MCPHeaderSources {
|
|
18
18
|
/** Client-generated HTTP/MCP/authorization headers; win case-insensitively. */
|
|
19
19
|
generated: Record<string, string>;
|
|
20
20
|
/** Configured headers from the server entry (package or user config). */
|
|
@@ -10,7 +10,7 @@ export interface AggregateMetrics extends AgentMetrics {
|
|
|
10
10
|
activeDurationAgents: number;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
interface AgentTreeProjection {
|
|
14
14
|
rows: AgentRef[];
|
|
15
15
|
depthById: Map<string, number>;
|
|
16
16
|
parentById: Map<string, string>;
|
|
@@ -65,7 +65,7 @@ export function progressMetrics(observed: ObservableSession | undefined): AgentM
|
|
|
65
65
|
* usage embedded in completed `task` tool results, so using it for a parent
|
|
66
66
|
* row would double-count child rows in the aggregate.
|
|
67
67
|
*/
|
|
68
|
-
|
|
68
|
+
function readSessionMetrics(session: NonNullable<AgentRef["session"]>): AgentMetrics | undefined {
|
|
69
69
|
try {
|
|
70
70
|
const stats = session.getSessionStats();
|
|
71
71
|
const messages = session.agent?.state?.messages;
|
|
@@ -16,7 +16,7 @@ export interface RosterRender {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
/** Legacy progress snapshots may omit counters; snapshot absence remains distinct. */
|
|
19
|
-
|
|
19
|
+
function metricNumber(value: number | undefined): number {
|
|
20
20
|
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
21
21
|
}
|
|
22
22
|
|
|
@@ -67,7 +67,7 @@ export function statusText(status: AgentRef["status"], text: string): string {
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
/** Model id + thinking level (`sonnet-4-6 ◒ high`), level colored per theme. */
|
|
70
|
-
|
|
70
|
+
function formatModelBadge(modelId: string, level: ThinkingLevel | undefined): string {
|
|
71
71
|
const model = theme.fg("muted", sanitizeDisplayText(modelId));
|
|
72
72
|
if (!level || level === ThinkingLevel.Off || level === ThinkingLevel.Inherit) return model;
|
|
73
73
|
const display = theme.thinking[level as keyof typeof theme.thinking] ?? level;
|
|
@@ -81,11 +81,7 @@ export function formatRoleBadge(role: string, settings: Settings): string {
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
/** Format a resolved selector, preserving provider identity when requested. */
|
|
84
|
-
|
|
85
|
-
resolved: string,
|
|
86
|
-
preserveProvider = false,
|
|
87
|
-
fallbackLevel?: ThinkingLevel,
|
|
88
|
-
): string {
|
|
84
|
+
function formatResolvedModelBadge(resolved: string, preserveProvider = false, fallbackLevel?: ThinkingLevel): string {
|
|
89
85
|
const cleanResolved = sanitizeDisplayText(resolved);
|
|
90
86
|
// Model ids may themselves contain colons (`qwen3:14b`), so only treat the
|
|
91
87
|
// suffix as a thinking level when it parses as one.
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
type UsageReport,
|
|
12
12
|
} from "@oh-my-pi/pi-ai";
|
|
13
13
|
import { Loader, Markdown, padding, Spacer, Text, visibleWidth } from "@oh-my-pi/pi-tui";
|
|
14
|
-
import { formatDuration, Snowflake, sanitizeText } from "@oh-my-pi/pi-utils";
|
|
14
|
+
import { formatDuration, logger, Snowflake, sanitizeText } from "@oh-my-pi/pi-utils";
|
|
15
15
|
import { shouldEnableAppendOnlyContext } from "../../config/append-only-context-mode";
|
|
16
16
|
import { type BashResult, isPersistentShellCdCommand } from "../../exec/bash-executor";
|
|
17
17
|
import { type LoadedCustomShare, loadCustomShare } from "../../export/custom-share";
|
|
@@ -1442,6 +1442,9 @@ export class CommandController {
|
|
|
1442
1442
|
if (message === "Handoff cancelled") {
|
|
1443
1443
|
this.ctx.showError("Handoff cancelled");
|
|
1444
1444
|
} else {
|
|
1445
|
+
// Persist the real failure so it is debuggable after the transient
|
|
1446
|
+
// TUI error clears (#7993).
|
|
1447
|
+
logger.error("Handoff failed", { error: message });
|
|
1445
1448
|
this.ctx.showError(`Handoff failed: ${message}`);
|
|
1446
1449
|
}
|
|
1447
1450
|
} finally {
|