@oh-my-pi/pi-coding-agent 14.5.8 → 14.5.10
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 +56 -0
- package/package.json +7 -15
- package/scripts/build-binary.ts +1 -1
- package/src/cli/update-cli.ts +25 -1
- package/src/config/model-registry.ts +21 -19
- package/src/config/settings-schema.ts +14 -19
- package/src/discovery/claude-plugins.ts +28 -3
- package/src/edit/modes/atom.lark +7 -5
- package/src/edit/modes/atom.ts +510 -73
- package/src/edit/modes/hashline.ts +172 -91
- package/src/extensibility/extensions/runner.ts +34 -1
- package/src/extensibility/extensions/types.ts +8 -0
- package/src/lsp/client.ts +27 -35
- package/src/lsp/index.ts +2 -4
- package/src/lsp/render.ts +0 -3
- package/src/lsp/types.ts +1 -4
- package/src/lsp/utils.ts +18 -14
- package/src/memories/index.ts +5 -0
- package/src/modes/components/settings-defs.ts +1 -1
- package/src/modes/controllers/command-controller.ts +17 -0
- package/src/modes/controllers/input-controller.ts +7 -1
- package/src/modes/controllers/selector-controller.ts +2 -2
- package/src/modes/interactive-mode.ts +57 -26
- package/src/modes/theme/theme.ts +10 -1
- package/src/modes/types.ts +5 -3
- package/src/modes/utils/context-usage.ts +294 -0
- package/src/modes/utils/ui-helpers.ts +19 -6
- package/src/prompts/system/auto-continue.md +1 -0
- package/src/prompts/tools/atom.md +99 -44
- package/src/prompts/tools/exit-plan-mode.md +5 -39
- package/src/prompts/tools/github.md +3 -3
- package/src/prompts/tools/lsp.md +2 -3
- package/src/prompts/tools/{run-command.md → recipe.md} +1 -1
- package/src/prompts/tools/task.md +34 -147
- package/src/prompts/tools/todo-write.md +22 -64
- package/src/sdk.ts +13 -2
- package/src/session/agent-session.ts +175 -79
- package/src/session/compaction/compaction.ts +35 -22
- package/src/session/session-dump-format.ts +1 -0
- package/src/session/session-manager.ts +19 -2
- package/src/slash-commands/builtin-registry.ts +12 -5
- package/src/tools/bash.ts +9 -4
- package/src/tools/debug.ts +57 -70
- package/src/tools/gh.ts +267 -119
- package/src/tools/index.ts +7 -7
- package/src/tools/{run-command → recipe}/index.ts +19 -19
- package/src/tools/recipe/render.ts +19 -0
- package/src/tools/{run-command → recipe}/runner.ts +28 -7
- package/src/tools/{run-command → recipe}/runners/pkg.ts +23 -53
- package/src/tools/renderers.ts +2 -2
- package/src/utils/git.ts +61 -2
- package/src/web/search/providers/searxng.ts +71 -13
- package/src/tools/run-command/render.ts +0 -18
- /package/src/tools/{run-command → recipe}/runners/cargo.ts +0 -0
- /package/src/tools/{run-command → recipe}/runners/index.ts +0 -0
- /package/src/tools/{run-command → recipe}/runners/just.ts +0 -0
- /package/src/tools/{run-command → recipe}/runners/make.ts +0 -0
- /package/src/tools/{run-command → recipe}/runners/task.ts +0 -0
|
@@ -53,7 +53,7 @@ export type HashlineEdit =
|
|
|
53
53
|
// Accept both `|` (canonical) and `:` (legacy) so re-reads of older outputs still parse.
|
|
54
54
|
const HASHLINE_CONTENT_SEPARATOR_RE = "[:|]";
|
|
55
55
|
const HASHLINE_PREFIX_RE = new RegExp(
|
|
56
|
-
`^\\s*(?:>>>|>>)?\\s*(
|
|
56
|
+
`^\\s*(?:>>>|>>)?\\s*(?:[+*]\\s*)?\\d+${HASHLINE_BIGRAM_RE_SRC}${HASHLINE_CONTENT_SEPARATOR_RE}`,
|
|
57
57
|
);
|
|
58
58
|
const HASHLINE_PREFIX_PLUS_RE = new RegExp(
|
|
59
59
|
`^\\s*(?:>>>|>>)?\\s*\\+\\s*\\d+${HASHLINE_BIGRAM_RE_SRC}${HASHLINE_CONTENT_SEPARATOR_RE}`,
|
|
@@ -503,7 +503,7 @@ export function parseTag(ref: string): { line: number; hash: string } {
|
|
|
503
503
|
// 1. optional leading ">+-" markers and whitespace
|
|
504
504
|
// 2. line number (1+ digits)
|
|
505
505
|
// 3. hash (one BPE bigram from HASHLINE_BIGRAMS) directly adjacent (no separator)
|
|
506
|
-
const match = ref.match(new RegExp(`^\\s*[
|
|
506
|
+
const match = ref.match(new RegExp(`^\\s*[>+\\-*]*\\s*(\\d+)(${HASHLINE_BIGRAM_RE_SRC})`));
|
|
507
507
|
if (!match) {
|
|
508
508
|
throw new Error(`Invalid line reference. Expected ${formatFullAnchorRequirement(ref)}.`);
|
|
509
509
|
}
|
|
@@ -605,7 +605,6 @@ export class HashlineMismatchError extends Error {
|
|
|
605
605
|
`Edit rejected: ${mismatches.length} line${mismatches.length > 1 ? "s have" : " has"} changed since the last read (marked *).`,
|
|
606
606
|
"The edit was NOT applied, please use the updated file content shown below, and issue another edit tool-call.",
|
|
607
607
|
);
|
|
608
|
-
lines.push("");
|
|
609
608
|
|
|
610
609
|
let prevLine = -1;
|
|
611
610
|
for (const lineNum of sorted) {
|
|
@@ -650,7 +649,7 @@ export function validateLineRef(ref: { line: number; hash: string }, fileLines:
|
|
|
650
649
|
/**
|
|
651
650
|
* Default search window for {@link tryRebaseAnchor} (lines on each side of the requested anchor).
|
|
652
651
|
*/
|
|
653
|
-
export const ANCHOR_REBASE_WINDOW =
|
|
652
|
+
export const ANCHOR_REBASE_WINDOW = 5;
|
|
654
653
|
|
|
655
654
|
/**
|
|
656
655
|
* Look for the requested hash within ±`window` lines of `anchor.line`.
|
|
@@ -1008,23 +1007,40 @@ export interface CompactHashlineDiffPreview {
|
|
|
1008
1007
|
}
|
|
1009
1008
|
|
|
1010
1009
|
export interface CompactHashlineDiffOptions {
|
|
1010
|
+
/** Maximum entries kept on each side of an unchanged-context truncation (default: 2). */
|
|
1011
1011
|
maxUnchangedRun?: number;
|
|
1012
|
-
maxDeletionRun?: number;
|
|
1013
|
-
maxOutputLines?: number;
|
|
1014
1012
|
}
|
|
1015
1013
|
|
|
1016
1014
|
const NUMBERED_DIFF_LINE_RE = /^([ +-])(\s*\d+)\|(.*)$/;
|
|
1017
1015
|
const HASHLINE_PREVIEW_PLACEHOLDER = " ";
|
|
1016
|
+
const ELLIPSIS = "...";
|
|
1018
1017
|
|
|
1019
|
-
type
|
|
1020
|
-
type
|
|
1018
|
+
type DiffEntryKind = " " | "+" | "-" | "*";
|
|
1019
|
+
type RunKind = DiffEntryKind | "meta";
|
|
1020
|
+
|
|
1021
|
+
interface DiffEntry {
|
|
1022
|
+
kind: DiffEntryKind;
|
|
1023
|
+
oldLine: number;
|
|
1024
|
+
newLine: number;
|
|
1025
|
+
content: string;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
interface MetaEntry {
|
|
1029
|
+
kind: "meta";
|
|
1030
|
+
raw: string;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
type Entry = DiffEntry | MetaEntry;
|
|
1034
|
+
|
|
1035
|
+
interface Run {
|
|
1036
|
+
kind: RunKind;
|
|
1037
|
+
entries: Entry[];
|
|
1038
|
+
}
|
|
1021
1039
|
|
|
1022
1040
|
interface ParsedNumberedDiffLine {
|
|
1023
1041
|
kind: " " | "+" | "-";
|
|
1024
1042
|
lineNumber: number;
|
|
1025
|
-
lineWidth: number;
|
|
1026
1043
|
content: string;
|
|
1027
|
-
raw: string;
|
|
1028
1044
|
}
|
|
1029
1045
|
|
|
1030
1046
|
interface CompactPreviewCounters {
|
|
@@ -1039,11 +1055,10 @@ function parseNumberedDiffLine(line: string): ParsedNumberedDiffLine | undefined
|
|
|
1039
1055
|
const kind = match[1];
|
|
1040
1056
|
if (kind !== " " && kind !== "+" && kind !== "-") return undefined;
|
|
1041
1057
|
|
|
1042
|
-
const
|
|
1043
|
-
const lineNumber = Number(lineField.trim());
|
|
1058
|
+
const lineNumber = Number(match[2].trim());
|
|
1044
1059
|
if (!Number.isInteger(lineNumber)) return undefined;
|
|
1045
1060
|
|
|
1046
|
-
return { kind, lineNumber,
|
|
1061
|
+
return { kind, lineNumber, content: match[3] };
|
|
1047
1062
|
}
|
|
1048
1063
|
|
|
1049
1064
|
function syncOldLineCounters(counters: CompactPreviewCounters, lineNumber: number): void {
|
|
@@ -1070,105 +1085,169 @@ function syncNewLineCounters(counters: CompactPreviewCounters, lineNumber: numbe
|
|
|
1070
1085
|
counters.newLine = lineNumber;
|
|
1071
1086
|
}
|
|
1072
1087
|
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1088
|
+
/**
|
|
1089
|
+
* Parse a unified-diff-with-line-numbers blob into structured entries while
|
|
1090
|
+
* tracking both old- and new-file line numbers. `...` markers (emitted by
|
|
1091
|
+
* {@link generateDiffString} for collapsed context) sync counters but are
|
|
1092
|
+
* preserved as passthrough entries so the original ellipsis remains visible.
|
|
1093
|
+
*/
|
|
1094
|
+
function parseDiffEntries(lines: string[]): Entry[] {
|
|
1095
|
+
const entries: Entry[] = [];
|
|
1096
|
+
const counters: CompactPreviewCounters = {};
|
|
1076
1097
|
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1098
|
+
for (const line of lines) {
|
|
1099
|
+
const parsed = parseNumberedDiffLine(line);
|
|
1100
|
+
if (!parsed) {
|
|
1101
|
+
entries.push({ kind: "meta", raw: line });
|
|
1102
|
+
continue;
|
|
1103
|
+
}
|
|
1080
1104
|
|
|
1081
|
-
|
|
1082
|
-
const parsed = parseNumberedDiffLine(line);
|
|
1083
|
-
if (!parsed) return { kind: "meta", text: line };
|
|
1105
|
+
const isEllipsis = parsed.content === ELLIPSIS;
|
|
1084
1106
|
|
|
1085
|
-
if (parsed.content === "...") {
|
|
1086
1107
|
if (parsed.kind === "+") {
|
|
1087
1108
|
syncNewLineCounters(counters, parsed.lineNumber);
|
|
1088
|
-
|
|
1089
|
-
|
|
1109
|
+
const newLine = counters.newLine ?? parsed.lineNumber;
|
|
1110
|
+
const oldLine = counters.oldLine ?? parsed.lineNumber;
|
|
1111
|
+
entries.push({ kind: "+", oldLine, newLine, content: parsed.content });
|
|
1112
|
+
if (!isEllipsis) counters.newLine = newLine + 1;
|
|
1113
|
+
continue;
|
|
1090
1114
|
}
|
|
1091
|
-
return { kind: parsed.kind, text: parsed.raw };
|
|
1092
|
-
}
|
|
1093
1115
|
|
|
1094
|
-
|
|
1095
|
-
case "+": {
|
|
1096
|
-
syncNewLineCounters(counters, parsed.lineNumber);
|
|
1097
|
-
const newLine = counters.newLine;
|
|
1098
|
-
if (newLine === undefined) return { kind: "+", text: parsed.raw };
|
|
1099
|
-
const text = formatCompactHashlineLine("+", newLine, parsed.content);
|
|
1100
|
-
counters.newLine = newLine + 1;
|
|
1101
|
-
return { kind: "+", text };
|
|
1102
|
-
}
|
|
1103
|
-
case "-": {
|
|
1116
|
+
if (parsed.kind === "-") {
|
|
1104
1117
|
syncOldLineCounters(counters, parsed.lineNumber);
|
|
1105
|
-
const
|
|
1106
|
-
counters.
|
|
1107
|
-
|
|
1118
|
+
const oldLine = parsed.lineNumber;
|
|
1119
|
+
const newLine = counters.newLine ?? parsed.lineNumber;
|
|
1120
|
+
entries.push({ kind: "-", oldLine, newLine, content: parsed.content });
|
|
1121
|
+
if (!isEllipsis) counters.oldLine = oldLine + 1;
|
|
1122
|
+
continue;
|
|
1108
1123
|
}
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1124
|
+
|
|
1125
|
+
// Context line.
|
|
1126
|
+
syncOldLineCounters(counters, parsed.lineNumber);
|
|
1127
|
+
const oldLine = parsed.lineNumber;
|
|
1128
|
+
const newLine = counters.newLine ?? parsed.lineNumber;
|
|
1129
|
+
entries.push({ kind: " ", oldLine, newLine, content: parsed.content });
|
|
1130
|
+
if (!isEllipsis) {
|
|
1131
|
+
counters.oldLine = oldLine + 1;
|
|
1115
1132
|
counters.newLine = newLine + 1;
|
|
1116
|
-
return { kind: " ", text };
|
|
1117
1133
|
}
|
|
1118
1134
|
}
|
|
1119
|
-
}
|
|
1120
1135
|
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
const counters: CompactPreviewCounters = {};
|
|
1136
|
+
return entries;
|
|
1137
|
+
}
|
|
1124
1138
|
|
|
1125
|
-
|
|
1126
|
-
|
|
1139
|
+
function groupRuns(entries: Entry[]): Run[] {
|
|
1140
|
+
const runs: Run[] = [];
|
|
1141
|
+
for (const entry of entries) {
|
|
1127
1142
|
const prev = runs[runs.length - 1];
|
|
1128
|
-
if (prev && prev.kind ===
|
|
1129
|
-
prev.
|
|
1143
|
+
if (prev && prev.kind === entry.kind) {
|
|
1144
|
+
prev.entries.push(entry);
|
|
1130
1145
|
continue;
|
|
1131
1146
|
}
|
|
1132
|
-
runs.push({ kind:
|
|
1147
|
+
runs.push({ kind: entry.kind, entries: [entry] });
|
|
1133
1148
|
}
|
|
1134
|
-
|
|
1135
1149
|
return runs;
|
|
1136
1150
|
}
|
|
1137
1151
|
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1152
|
+
/**
|
|
1153
|
+
* Collapse adjacent `(-, +)` runs into a single `*` run for paired
|
|
1154
|
+
* modifications. The i-th removed line pairs with the i-th added line — in
|
|
1155
|
+
* unified-diff convention they replaced each other in place — and is shown as
|
|
1156
|
+
* `*<newLine><hash>|<newContent>` instead of two lines `-<old>` + `+<new>`.
|
|
1157
|
+
* Surplus removals or additions remain as their own runs after the paired
|
|
1158
|
+
* block, preserving the unified-diff `del-then-add` ordering.
|
|
1159
|
+
*/
|
|
1160
|
+
function pairModifications(runs: Run[]): Run[] {
|
|
1161
|
+
const isPairable = (entry: Entry): entry is DiffEntry => entry.kind !== "meta" && entry.content !== ELLIPSIS;
|
|
1162
|
+
|
|
1163
|
+
const out: Run[] = [];
|
|
1164
|
+
for (let i = 0; i < runs.length; i++) {
|
|
1165
|
+
const run = runs[i];
|
|
1166
|
+
const next = runs[i + 1];
|
|
1167
|
+
if (run.kind !== "-" || !next || next.kind !== "+") {
|
|
1168
|
+
out.push(run);
|
|
1169
|
+
continue;
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
const dels = run.entries.filter(isPairable);
|
|
1173
|
+
const adds = next.entries.filter(isPairable);
|
|
1174
|
+
const pairCount = Math.min(dels.length, adds.length);
|
|
1175
|
+
if (pairCount === 0) {
|
|
1176
|
+
out.push(run);
|
|
1177
|
+
continue;
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
const mods: Entry[] = [];
|
|
1181
|
+
for (let p = 0; p < pairCount; p++) {
|
|
1182
|
+
mods.push({
|
|
1183
|
+
kind: "*",
|
|
1184
|
+
oldLine: dels[p].oldLine,
|
|
1185
|
+
newLine: adds[p].newLine,
|
|
1186
|
+
content: adds[p].content,
|
|
1187
|
+
});
|
|
1188
|
+
}
|
|
1189
|
+
out.push({ kind: "*", entries: mods });
|
|
1190
|
+
|
|
1191
|
+
if (dels.length > pairCount) {
|
|
1192
|
+
out.push({ kind: "-", entries: dels.slice(pairCount) });
|
|
1193
|
+
}
|
|
1194
|
+
if (adds.length > pairCount) {
|
|
1195
|
+
out.push({ kind: "+", entries: adds.slice(pairCount) });
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
i++; // consume the `+` run
|
|
1199
|
+
}
|
|
1200
|
+
return out;
|
|
1142
1201
|
}
|
|
1143
1202
|
|
|
1144
|
-
function
|
|
1145
|
-
if (
|
|
1146
|
-
|
|
1147
|
-
|
|
1203
|
+
function formatEntry(entry: Entry): string {
|
|
1204
|
+
if (entry.kind === "meta") return entry.raw;
|
|
1205
|
+
|
|
1206
|
+
if (entry.content === ELLIPSIS) {
|
|
1207
|
+
// Preserve the `... <line>|...` ellipsis marker emitted by generateDiffString.
|
|
1208
|
+
const lineNum = entry.kind === "+" || entry.kind === "*" ? entry.newLine : entry.oldLine;
|
|
1209
|
+
const prefix = entry.kind === "*" ? "+" : entry.kind;
|
|
1210
|
+
return `${prefix}${lineNum}${HASHLINE_PREVIEW_PLACEHOLDER}${HASHLINE_CONTENT_SEPARATOR}${ELLIPSIS}`;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
switch (entry.kind) {
|
|
1214
|
+
case "+":
|
|
1215
|
+
return `+${entry.newLine}${computeLineHash(entry.newLine, entry.content)}${HASHLINE_CONTENT_SEPARATOR}${entry.content}`;
|
|
1216
|
+
case "-":
|
|
1217
|
+
return `-${entry.oldLine}${HASHLINE_PREVIEW_PLACEHOLDER}${HASHLINE_CONTENT_SEPARATOR}${entry.content}`;
|
|
1218
|
+
case " ":
|
|
1219
|
+
return ` ${entry.newLine}${computeLineHash(entry.newLine, entry.content)}${HASHLINE_CONTENT_SEPARATOR}${entry.content}`;
|
|
1220
|
+
case "*":
|
|
1221
|
+
return `*${entry.newLine}${computeLineHash(entry.newLine, entry.content)}${HASHLINE_CONTENT_SEPARATOR}${entry.content}`;
|
|
1222
|
+
}
|
|
1148
1223
|
}
|
|
1149
1224
|
|
|
1150
|
-
function
|
|
1151
|
-
if (
|
|
1152
|
-
const hidden =
|
|
1153
|
-
return [
|
|
1225
|
+
function collapseUnchangedMiddle(entries: Entry[], maxRun: number): string[] {
|
|
1226
|
+
if (entries.length <= maxRun * 2) return entries.map(formatEntry);
|
|
1227
|
+
const hidden = entries.length - maxRun * 2;
|
|
1228
|
+
return [
|
|
1229
|
+
...entries.slice(0, maxRun).map(formatEntry),
|
|
1230
|
+
` ... ${hidden} more unchanged lines`,
|
|
1231
|
+
...entries.slice(-maxRun).map(formatEntry),
|
|
1232
|
+
];
|
|
1154
1233
|
}
|
|
1155
1234
|
|
|
1156
1235
|
/**
|
|
1157
1236
|
* Build a compact diff preview suitable for model-visible tool responses.
|
|
1158
1237
|
*
|
|
1159
|
-
*
|
|
1160
|
-
*
|
|
1238
|
+
* Every changed line — added, removed, or modified — is shown in full. Only
|
|
1239
|
+
* unchanged context blocks between or around changes get truncated. Adjacent
|
|
1240
|
+
* `-`/`+` pairs are folded into single `*` modification lines so the common
|
|
1241
|
+
* 1:1 line-replacement case stays compact.
|
|
1161
1242
|
*/
|
|
1162
1243
|
export function buildCompactHashlineDiffPreview(
|
|
1163
1244
|
diff: string,
|
|
1164
1245
|
options: CompactHashlineDiffOptions = {},
|
|
1165
1246
|
): CompactHashlineDiffPreview {
|
|
1166
1247
|
const maxUnchangedRun = options.maxUnchangedRun ?? 2;
|
|
1167
|
-
const maxDeletionRun = options.maxDeletionRun ?? 2;
|
|
1168
|
-
const maxOutputLines = options.maxOutputLines ?? 16;
|
|
1169
1248
|
|
|
1170
1249
|
const inputLines = diff.length === 0 ? [] : diff.split("\n");
|
|
1171
|
-
const runs =
|
|
1250
|
+
const runs = pairModifications(groupRuns(parseDiffEntries(inputLines)));
|
|
1172
1251
|
|
|
1173
1252
|
const out: string[] = [];
|
|
1174
1253
|
let addedLines = 0;
|
|
@@ -1178,39 +1257,41 @@ export function buildCompactHashlineDiffPreview(
|
|
|
1178
1257
|
const run = runs[runIndex];
|
|
1179
1258
|
switch (run.kind) {
|
|
1180
1259
|
case "meta":
|
|
1181
|
-
out.push(
|
|
1260
|
+
for (const entry of run.entries) out.push(formatEntry(entry));
|
|
1182
1261
|
break;
|
|
1183
1262
|
case "+":
|
|
1184
|
-
|
|
1185
|
-
|
|
1263
|
+
for (const entry of run.entries) {
|
|
1264
|
+
if (entry.kind !== "meta" && entry.content !== ELLIPSIS) addedLines++;
|
|
1265
|
+
out.push(formatEntry(entry));
|
|
1266
|
+
}
|
|
1186
1267
|
break;
|
|
1187
1268
|
case "-":
|
|
1188
|
-
|
|
1189
|
-
|
|
1269
|
+
for (const entry of run.entries) {
|
|
1270
|
+
if (entry.kind !== "meta" && entry.content !== ELLIPSIS) removedLines++;
|
|
1271
|
+
out.push(formatEntry(entry));
|
|
1272
|
+
}
|
|
1273
|
+
break;
|
|
1274
|
+
case "*":
|
|
1275
|
+
for (const entry of run.entries) {
|
|
1276
|
+
addedLines++;
|
|
1277
|
+
removedLines++;
|
|
1278
|
+
out.push(formatEntry(entry));
|
|
1279
|
+
}
|
|
1190
1280
|
break;
|
|
1191
1281
|
case " ":
|
|
1192
1282
|
if (runIndex === 0) {
|
|
1193
|
-
out.push(...run.
|
|
1283
|
+
out.push(...run.entries.slice(-maxUnchangedRun).map(formatEntry));
|
|
1194
1284
|
break;
|
|
1195
1285
|
}
|
|
1196
1286
|
if (runIndex === runs.length - 1) {
|
|
1197
|
-
out.push(...run.
|
|
1287
|
+
out.push(...run.entries.slice(0, maxUnchangedRun).map(formatEntry));
|
|
1198
1288
|
break;
|
|
1199
1289
|
}
|
|
1200
|
-
out.push(...
|
|
1290
|
+
out.push(...collapseUnchangedMiddle(run.entries, maxUnchangedRun));
|
|
1201
1291
|
break;
|
|
1202
1292
|
}
|
|
1203
1293
|
}
|
|
1204
1294
|
|
|
1205
|
-
if (out.length > maxOutputLines) {
|
|
1206
|
-
const hidden = out.length - maxOutputLines;
|
|
1207
|
-
return {
|
|
1208
|
-
preview: [...out.slice(0, maxOutputLines), ` ... ${hidden} more preview lines`].join("\n"),
|
|
1209
|
-
addedLines,
|
|
1210
|
-
removedLines,
|
|
1211
|
-
};
|
|
1212
|
-
}
|
|
1213
|
-
|
|
1214
1295
|
return { preview: out.join("\n"), addedLines, removedLines };
|
|
1215
1296
|
}
|
|
1216
1297
|
|
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
* Extension runner - executes extensions and manages their lifecycle.
|
|
3
3
|
*/
|
|
4
4
|
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
5
|
-
import type { ImageContent, Model } from "@oh-my-pi/pi-ai";
|
|
5
|
+
import type { ImageContent, Model, ProviderResponseMetadata } from "@oh-my-pi/pi-ai";
|
|
6
6
|
import type { KeyId } from "@oh-my-pi/pi-tui";
|
|
7
7
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
8
8
|
import type { ModelRegistry } from "../../config/model-registry";
|
|
9
9
|
import { type Theme, theme } from "../../modes/theme/theme";
|
|
10
10
|
import type { SessionManager } from "../../session/session-manager";
|
|
11
11
|
import type {
|
|
12
|
+
AfterProviderResponseEvent,
|
|
12
13
|
BeforeAgentStartEvent,
|
|
13
14
|
BeforeAgentStartEventResult,
|
|
14
15
|
BeforeProviderRequestEvent,
|
|
@@ -70,6 +71,7 @@ type RunnerEmitEvent = Exclude<
|
|
|
70
71
|
| UserBashEvent
|
|
71
72
|
| ContextEvent
|
|
72
73
|
| BeforeProviderRequestEvent
|
|
74
|
+
| AfterProviderResponseEvent
|
|
73
75
|
| BeforeAgentStartEvent
|
|
74
76
|
| ResourcesDiscoverEvent
|
|
75
77
|
| InputEvent
|
|
@@ -759,6 +761,37 @@ export class ExtensionRunner {
|
|
|
759
761
|
return currentPayload;
|
|
760
762
|
}
|
|
761
763
|
|
|
764
|
+
async emitAfterProviderResponse(response: ProviderResponseMetadata, _model?: Model): Promise<void> {
|
|
765
|
+
const ctx = this.createContext();
|
|
766
|
+
|
|
767
|
+
for (const ext of this.extensions) {
|
|
768
|
+
const handlers = ext.handlers.get("after_provider_response");
|
|
769
|
+
if (!handlers || handlers.length === 0) continue;
|
|
770
|
+
|
|
771
|
+
for (const handler of handlers) {
|
|
772
|
+
try {
|
|
773
|
+
const event: AfterProviderResponseEvent = {
|
|
774
|
+
type: "after_provider_response",
|
|
775
|
+
status: response.status,
|
|
776
|
+
headers: response.headers,
|
|
777
|
+
requestId: response.requestId,
|
|
778
|
+
metadata: response.metadata,
|
|
779
|
+
};
|
|
780
|
+
await handler(event, ctx);
|
|
781
|
+
} catch (err) {
|
|
782
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
783
|
+
const stack = err instanceof Error ? err.stack : undefined;
|
|
784
|
+
this.emitError({
|
|
785
|
+
extensionPath: ext.path,
|
|
786
|
+
event: "after_provider_response",
|
|
787
|
+
error: message,
|
|
788
|
+
stack,
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
762
795
|
async emitBeforeAgentStart(
|
|
763
796
|
prompt: string,
|
|
764
797
|
images: ImageContent[] | undefined,
|
|
@@ -17,6 +17,7 @@ import type {
|
|
|
17
17
|
Model,
|
|
18
18
|
OAuthCredentials,
|
|
19
19
|
OAuthLoginCallbacks,
|
|
20
|
+
ProviderResponseMetadata,
|
|
20
21
|
SimpleStreamOptions,
|
|
21
22
|
TextContent,
|
|
22
23
|
ToolResultMessage,
|
|
@@ -482,6 +483,11 @@ export interface BeforeProviderRequestEvent {
|
|
|
482
483
|
payload: unknown;
|
|
483
484
|
}
|
|
484
485
|
|
|
486
|
+
/** Fired after a provider response is received, before its stream body is consumed. */
|
|
487
|
+
export interface AfterProviderResponseEvent extends ProviderResponseMetadata {
|
|
488
|
+
type: "after_provider_response";
|
|
489
|
+
}
|
|
490
|
+
|
|
485
491
|
/** Fired after user submits prompt but before agent loop. */
|
|
486
492
|
export interface BeforeAgentStartEvent {
|
|
487
493
|
type: "before_agent_start";
|
|
@@ -801,6 +807,7 @@ export type ExtensionEvent =
|
|
|
801
807
|
| SessionEvent
|
|
802
808
|
| ContextEvent
|
|
803
809
|
| BeforeProviderRequestEvent
|
|
810
|
+
| AfterProviderResponseEvent
|
|
804
811
|
| BeforeAgentStartEvent
|
|
805
812
|
| AgentStartEvent
|
|
806
813
|
| AgentEndEvent
|
|
@@ -981,6 +988,7 @@ export interface ExtensionAPI {
|
|
|
981
988
|
event: "before_provider_request",
|
|
982
989
|
handler: ExtensionHandler<BeforeProviderRequestEvent, BeforeProviderRequestEventResult>,
|
|
983
990
|
): void;
|
|
991
|
+
on(event: "after_provider_response", handler: ExtensionHandler<AfterProviderResponseEvent>): void;
|
|
984
992
|
on(event: "before_agent_start", handler: ExtensionHandler<BeforeAgentStartEvent, BeforeAgentStartEventResult>): void;
|
|
985
993
|
on(event: "agent_start", handler: ExtensionHandler<AgentStartEvent>): void;
|
|
986
994
|
on(event: "agent_end", handler: ExtensionHandler<AgentEndEvent>): void;
|
package/src/lsp/client.ts
CHANGED
|
@@ -47,7 +47,7 @@ function startIdleChecker(): void {
|
|
|
47
47
|
const now = Date.now();
|
|
48
48
|
for (const [key, client] of Array.from(clients.entries())) {
|
|
49
49
|
if (now - client.lastActivity > idleTimeoutMs) {
|
|
50
|
-
shutdownClient(key);
|
|
50
|
+
void shutdownClient(key);
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
}, IDLE_CHECK_INTERVAL_MS);
|
|
@@ -762,22 +762,25 @@ export async function refreshFile(client: LspClient, filePath: string, signal?:
|
|
|
762
762
|
/**
|
|
763
763
|
* Shutdown a specific client by key.
|
|
764
764
|
*/
|
|
765
|
-
|
|
766
|
-
const
|
|
767
|
-
if (!client) return;
|
|
768
|
-
|
|
769
|
-
// Reject all pending requests
|
|
765
|
+
async function shutdownClientInstance(client: LspClient): Promise<void> {
|
|
766
|
+
const err = new Error("LSP client shutdown");
|
|
770
767
|
for (const pending of Array.from(client.pendingRequests.values())) {
|
|
771
|
-
pending.reject(
|
|
768
|
+
pending.reject(err);
|
|
772
769
|
}
|
|
773
770
|
client.pendingRequests.clear();
|
|
774
771
|
|
|
775
|
-
|
|
776
|
-
sendRequest(client, "shutdown", null).catch(() => {});
|
|
777
|
-
|
|
778
|
-
// Kill process
|
|
772
|
+
const timeout = Bun.sleep(5_000);
|
|
773
|
+
const shutdown = sendRequest(client, "shutdown", null).catch(() => {});
|
|
774
|
+
await Promise.race([shutdown, timeout]);
|
|
779
775
|
client.proc.kill();
|
|
776
|
+
await Promise.race([client.proc.exited.catch(() => {}), Bun.sleep(1_000)]);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
export async function shutdownClient(key: string): Promise<void> {
|
|
780
|
+
const client = clients.get(key);
|
|
781
|
+
if (!client) return;
|
|
780
782
|
clients.delete(key);
|
|
783
|
+
await shutdownClientInstance(client);
|
|
781
784
|
}
|
|
782
785
|
|
|
783
786
|
// =============================================================================
|
|
@@ -890,27 +893,10 @@ export async function sendNotification(client: LspClient, method: string, params
|
|
|
890
893
|
/**
|
|
891
894
|
* Shutdown all LSP clients.
|
|
892
895
|
*/
|
|
893
|
-
export function shutdownAll(): void {
|
|
896
|
+
export async function shutdownAll(): Promise<void> {
|
|
894
897
|
const clientsToShutdown = Array.from(clients.values());
|
|
895
898
|
clients.clear();
|
|
896
|
-
|
|
897
|
-
const err = new Error("LSP client shutdown");
|
|
898
|
-
for (const client of clientsToShutdown) {
|
|
899
|
-
/// Reject all pending requests
|
|
900
|
-
const reqs = Array.from(client.pendingRequests.values());
|
|
901
|
-
client.pendingRequests.clear();
|
|
902
|
-
for (const pending of reqs) {
|
|
903
|
-
pending.reject(err);
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
void (async () => {
|
|
907
|
-
// Send shutdown request (best effort, don't wait)
|
|
908
|
-
const timeout = Bun.sleep(5_000);
|
|
909
|
-
const result = sendRequest(client, "shutdown", null).catch(() => {});
|
|
910
|
-
await Promise.race([result, timeout]);
|
|
911
|
-
client.proc.kill();
|
|
912
|
-
})().catch(() => {});
|
|
913
|
-
}
|
|
899
|
+
await Promise.allSettled(clientsToShutdown.map(client => shutdownClientInstance(client)));
|
|
914
900
|
}
|
|
915
901
|
|
|
916
902
|
/** Status of an LSP server */
|
|
@@ -938,13 +924,19 @@ export function getActiveClients(): LspServerStatus[] {
|
|
|
938
924
|
|
|
939
925
|
// Register cleanup on module unload
|
|
940
926
|
if (typeof process !== "undefined") {
|
|
941
|
-
process.on("beforeExit",
|
|
927
|
+
process.on("beforeExit", () => {
|
|
928
|
+
void shutdownAll();
|
|
929
|
+
});
|
|
942
930
|
process.on("SIGINT", () => {
|
|
943
|
-
|
|
944
|
-
|
|
931
|
+
void (async () => {
|
|
932
|
+
await shutdownAll();
|
|
933
|
+
process.exit(0);
|
|
934
|
+
})();
|
|
945
935
|
});
|
|
946
936
|
process.on("SIGTERM", () => {
|
|
947
|
-
|
|
948
|
-
|
|
937
|
+
void (async () => {
|
|
938
|
+
await shutdownAll();
|
|
939
|
+
process.exit(0);
|
|
940
|
+
})();
|
|
949
941
|
});
|
|
950
942
|
}
|
package/src/lsp/index.ts
CHANGED
|
@@ -1136,7 +1136,7 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
|
|
|
1136
1136
|
_onUpdate?: AgentToolUpdateCallback<LspToolDetails>,
|
|
1137
1137
|
_context?: AgentToolContext,
|
|
1138
1138
|
): Promise<AgentToolResult<LspToolDetails>> {
|
|
1139
|
-
const { action, file, line, symbol,
|
|
1139
|
+
const { action, file, line, symbol, query, new_name, apply, timeout } = params;
|
|
1140
1140
|
const timeoutSec = clampTimeout("lsp", timeout);
|
|
1141
1141
|
const timeoutSignal = AbortSignal.timeout(timeoutSec * 1000);
|
|
1142
1142
|
signal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
@@ -1449,9 +1449,7 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
|
|
|
1449
1449
|
|
|
1450
1450
|
const uri = targetFile ? fileToUri(targetFile) : "";
|
|
1451
1451
|
const resolvedLine = line ?? 1;
|
|
1452
|
-
const resolvedCharacter = targetFile
|
|
1453
|
-
? await resolveSymbolColumn(targetFile, resolvedLine, symbol, occurrence)
|
|
1454
|
-
: 0;
|
|
1452
|
+
const resolvedCharacter = targetFile ? await resolveSymbolColumn(targetFile, resolvedLine, symbol) : 0;
|
|
1455
1453
|
const position = { line: resolvedLine - 1, character: resolvedCharacter };
|
|
1456
1454
|
|
|
1457
1455
|
let output: string;
|
package/src/lsp/render.ts
CHANGED
|
@@ -131,9 +131,6 @@ export function renderResult(
|
|
|
131
131
|
}
|
|
132
132
|
if (request?.symbol) {
|
|
133
133
|
requestLines.push(theme.fg("dim", `symbol: ${sanitizeInlineText(request.symbol)}`));
|
|
134
|
-
if (request.occurrence !== undefined) {
|
|
135
|
-
requestLines.push(theme.fg("dim", `occurrence: ${request.occurrence}`));
|
|
136
|
-
}
|
|
137
134
|
}
|
|
138
135
|
if (request?.query) requestLines.push(theme.fg("dim", `query: ${request.query}`));
|
|
139
136
|
if (request?.new_name) requestLines.push(theme.fg("dim", `new name: ${request.new_name}`));
|
package/src/lsp/types.ts
CHANGED
|
@@ -25,10 +25,7 @@ export const lspSchema = Type.Object({
|
|
|
25
25
|
),
|
|
26
26
|
file: Type.Optional(Type.String({ description: "File path" })),
|
|
27
27
|
line: Type.Optional(Type.Number({ description: "Line number (1-indexed)" })),
|
|
28
|
-
symbol: Type.Optional(
|
|
29
|
-
Type.String({ description: "Symbol/substring to locate on the line (used to compute column)" }),
|
|
30
|
-
),
|
|
31
|
-
occurrence: Type.Optional(Type.Number({ description: "Symbol occurrence on line (1-indexed, default: 1)" })),
|
|
28
|
+
symbol: Type.Optional(Type.String({ description: "Symbol/substring to locate on the line" })),
|
|
32
29
|
query: Type.Optional(Type.String({ description: "Search query or SSR pattern" })),
|
|
33
30
|
new_name: Type.Optional(Type.String({ description: "New name for rename" })),
|
|
34
31
|
apply: Type.Optional(Type.Boolean({ description: "Apply edits (default: true)" })),
|