@oh-my-pi/pi-coding-agent 14.5.9 → 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 +34 -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 +11 -16
- package/src/discovery/claude-plugins.ts +28 -3
- package/src/edit/modes/atom.ts +50 -19
- package/src/edit/modes/hashline.ts +171 -110
- 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/memories/index.ts +5 -0
- package/src/modes/components/settings-defs.ts +1 -1
- package/src/modes/controllers/selector-controller.ts +2 -2
- package/src/modes/interactive-mode.ts +27 -3
- package/src/modes/theme/theme.ts +10 -1
- package/src/modes/types.ts +1 -1
- package/src/modes/utils/ui-helpers.ts +19 -6
- package/src/prompts/system/auto-continue.md +1 -0
- package/src/prompts/tools/github.md +3 -3
- package/src/sdk.ts +13 -2
- package/src/session/agent-session.ts +175 -79
- package/src/session/session-manager.ts +19 -2
- package/src/tools/bash.ts +9 -4
- package/src/tools/gh.ts +267 -119
- package/src/utils/git.ts +61 -2
- package/src/web/search/providers/searxng.ts +71 -13
|
@@ -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
|
}
|
|
@@ -606,27 +606,6 @@ export class HashlineMismatchError extends Error {
|
|
|
606
606
|
"The edit was NOT applied, please use the updated file content shown below, and issue another edit tool-call.",
|
|
607
607
|
);
|
|
608
608
|
|
|
609
|
-
// Content-based recovery hint: the two-letter hash is weak, so a
|
|
610
|
-
// unique match elsewhere is only a candidate. Keep this advisory; never
|
|
611
|
-
// silently retarget stale edits based on a whole-file hash-only match.
|
|
612
|
-
const hints: string[] = [];
|
|
613
|
-
for (const m of mismatches) {
|
|
614
|
-
const matches: number[] = [];
|
|
615
|
-
for (let line = 1; line <= fileLines.length; line++) {
|
|
616
|
-
if (computeLineHash(line, fileLines[line - 1]) === m.expected) matches.push(line);
|
|
617
|
-
if (matches.length > 1) break;
|
|
618
|
-
}
|
|
619
|
-
if (matches.length === 1 && matches[0] !== m.line) {
|
|
620
|
-
hints.push(` ${m.line}${m.expected} → ${matches[0]}${m.expected}`);
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
if (hints.length > 0) {
|
|
624
|
-
lines.push("Hash-only shifted candidate; verify content/context before using:");
|
|
625
|
-
lines.push(...hints);
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
lines.push("");
|
|
629
|
-
|
|
630
609
|
let prevLine = -1;
|
|
631
610
|
for (const lineNum of sorted) {
|
|
632
611
|
// Gap separator between non-contiguous regions
|
|
@@ -1028,23 +1007,40 @@ export interface CompactHashlineDiffPreview {
|
|
|
1028
1007
|
}
|
|
1029
1008
|
|
|
1030
1009
|
export interface CompactHashlineDiffOptions {
|
|
1010
|
+
/** Maximum entries kept on each side of an unchanged-context truncation (default: 2). */
|
|
1031
1011
|
maxUnchangedRun?: number;
|
|
1032
|
-
maxDeletionRun?: number;
|
|
1033
|
-
maxOutputLines?: number;
|
|
1034
1012
|
}
|
|
1035
1013
|
|
|
1036
1014
|
const NUMBERED_DIFF_LINE_RE = /^([ +-])(\s*\d+)\|(.*)$/;
|
|
1037
1015
|
const HASHLINE_PREVIEW_PLACEHOLDER = " ";
|
|
1016
|
+
const ELLIPSIS = "...";
|
|
1038
1017
|
|
|
1039
|
-
type
|
|
1040
|
-
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
|
+
}
|
|
1041
1039
|
|
|
1042
1040
|
interface ParsedNumberedDiffLine {
|
|
1043
1041
|
kind: " " | "+" | "-";
|
|
1044
1042
|
lineNumber: number;
|
|
1045
|
-
lineWidth: number;
|
|
1046
1043
|
content: string;
|
|
1047
|
-
raw: string;
|
|
1048
1044
|
}
|
|
1049
1045
|
|
|
1050
1046
|
interface CompactPreviewCounters {
|
|
@@ -1059,11 +1055,10 @@ function parseNumberedDiffLine(line: string): ParsedNumberedDiffLine | undefined
|
|
|
1059
1055
|
const kind = match[1];
|
|
1060
1056
|
if (kind !== " " && kind !== "+" && kind !== "-") return undefined;
|
|
1061
1057
|
|
|
1062
|
-
const
|
|
1063
|
-
const lineNumber = Number(lineField.trim());
|
|
1058
|
+
const lineNumber = Number(match[2].trim());
|
|
1064
1059
|
if (!Number.isInteger(lineNumber)) return undefined;
|
|
1065
1060
|
|
|
1066
|
-
return { kind, lineNumber,
|
|
1061
|
+
return { kind, lineNumber, content: match[3] };
|
|
1067
1062
|
}
|
|
1068
1063
|
|
|
1069
1064
|
function syncOldLineCounters(counters: CompactPreviewCounters, lineNumber: number): void {
|
|
@@ -1090,105 +1085,169 @@ function syncNewLineCounters(counters: CompactPreviewCounters, lineNumber: numbe
|
|
|
1090
1085
|
counters.newLine = lineNumber;
|
|
1091
1086
|
}
|
|
1092
1087
|
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
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 = {};
|
|
1096
1097
|
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1098
|
+
for (const line of lines) {
|
|
1099
|
+
const parsed = parseNumberedDiffLine(line);
|
|
1100
|
+
if (!parsed) {
|
|
1101
|
+
entries.push({ kind: "meta", raw: line });
|
|
1102
|
+
continue;
|
|
1103
|
+
}
|
|
1100
1104
|
|
|
1101
|
-
|
|
1102
|
-
const parsed = parseNumberedDiffLine(line);
|
|
1103
|
-
if (!parsed) return { kind: "meta", text: line };
|
|
1105
|
+
const isEllipsis = parsed.content === ELLIPSIS;
|
|
1104
1106
|
|
|
1105
|
-
if (parsed.content === "...") {
|
|
1106
1107
|
if (parsed.kind === "+") {
|
|
1107
1108
|
syncNewLineCounters(counters, parsed.lineNumber);
|
|
1108
|
-
|
|
1109
|
-
|
|
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;
|
|
1110
1114
|
}
|
|
1111
|
-
return { kind: parsed.kind, text: parsed.raw };
|
|
1112
|
-
}
|
|
1113
1115
|
|
|
1114
|
-
|
|
1115
|
-
case "+": {
|
|
1116
|
-
syncNewLineCounters(counters, parsed.lineNumber);
|
|
1117
|
-
const newLine = counters.newLine;
|
|
1118
|
-
if (newLine === undefined) return { kind: "+", text: parsed.raw };
|
|
1119
|
-
const text = formatCompactHashlineLine("+", newLine, parsed.content);
|
|
1120
|
-
counters.newLine = newLine + 1;
|
|
1121
|
-
return { kind: "+", text };
|
|
1122
|
-
}
|
|
1123
|
-
case "-": {
|
|
1116
|
+
if (parsed.kind === "-") {
|
|
1124
1117
|
syncOldLineCounters(counters, parsed.lineNumber);
|
|
1125
|
-
const
|
|
1126
|
-
counters.
|
|
1127
|
-
|
|
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;
|
|
1128
1123
|
}
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
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;
|
|
1135
1132
|
counters.newLine = newLine + 1;
|
|
1136
|
-
return { kind: " ", text };
|
|
1137
1133
|
}
|
|
1138
1134
|
}
|
|
1139
|
-
}
|
|
1140
1135
|
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
const counters: CompactPreviewCounters = {};
|
|
1136
|
+
return entries;
|
|
1137
|
+
}
|
|
1144
1138
|
|
|
1145
|
-
|
|
1146
|
-
|
|
1139
|
+
function groupRuns(entries: Entry[]): Run[] {
|
|
1140
|
+
const runs: Run[] = [];
|
|
1141
|
+
for (const entry of entries) {
|
|
1147
1142
|
const prev = runs[runs.length - 1];
|
|
1148
|
-
if (prev && prev.kind ===
|
|
1149
|
-
prev.
|
|
1143
|
+
if (prev && prev.kind === entry.kind) {
|
|
1144
|
+
prev.entries.push(entry);
|
|
1150
1145
|
continue;
|
|
1151
1146
|
}
|
|
1152
|
-
runs.push({ kind:
|
|
1147
|
+
runs.push({ kind: entry.kind, entries: [entry] });
|
|
1153
1148
|
}
|
|
1154
|
-
|
|
1155
1149
|
return runs;
|
|
1156
1150
|
}
|
|
1157
1151
|
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
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;
|
|
1162
1201
|
}
|
|
1163
1202
|
|
|
1164
|
-
function
|
|
1165
|
-
if (
|
|
1166
|
-
|
|
1167
|
-
|
|
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
|
+
}
|
|
1168
1223
|
}
|
|
1169
1224
|
|
|
1170
|
-
function
|
|
1171
|
-
if (
|
|
1172
|
-
const hidden =
|
|
1173
|
-
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
|
+
];
|
|
1174
1233
|
}
|
|
1175
1234
|
|
|
1176
1235
|
/**
|
|
1177
1236
|
* Build a compact diff preview suitable for model-visible tool responses.
|
|
1178
1237
|
*
|
|
1179
|
-
*
|
|
1180
|
-
*
|
|
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.
|
|
1181
1242
|
*/
|
|
1182
1243
|
export function buildCompactHashlineDiffPreview(
|
|
1183
1244
|
diff: string,
|
|
1184
1245
|
options: CompactHashlineDiffOptions = {},
|
|
1185
1246
|
): CompactHashlineDiffPreview {
|
|
1186
1247
|
const maxUnchangedRun = options.maxUnchangedRun ?? 2;
|
|
1187
|
-
const maxDeletionRun = options.maxDeletionRun ?? 2;
|
|
1188
|
-
const maxOutputLines = options.maxOutputLines ?? 16;
|
|
1189
1248
|
|
|
1190
1249
|
const inputLines = diff.length === 0 ? [] : diff.split("\n");
|
|
1191
|
-
const runs =
|
|
1250
|
+
const runs = pairModifications(groupRuns(parseDiffEntries(inputLines)));
|
|
1192
1251
|
|
|
1193
1252
|
const out: string[] = [];
|
|
1194
1253
|
let addedLines = 0;
|
|
@@ -1198,39 +1257,41 @@ export function buildCompactHashlineDiffPreview(
|
|
|
1198
1257
|
const run = runs[runIndex];
|
|
1199
1258
|
switch (run.kind) {
|
|
1200
1259
|
case "meta":
|
|
1201
|
-
out.push(
|
|
1260
|
+
for (const entry of run.entries) out.push(formatEntry(entry));
|
|
1202
1261
|
break;
|
|
1203
1262
|
case "+":
|
|
1204
|
-
|
|
1205
|
-
|
|
1263
|
+
for (const entry of run.entries) {
|
|
1264
|
+
if (entry.kind !== "meta" && entry.content !== ELLIPSIS) addedLines++;
|
|
1265
|
+
out.push(formatEntry(entry));
|
|
1266
|
+
}
|
|
1206
1267
|
break;
|
|
1207
1268
|
case "-":
|
|
1208
|
-
|
|
1209
|
-
|
|
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
|
+
}
|
|
1210
1280
|
break;
|
|
1211
1281
|
case " ":
|
|
1212
1282
|
if (runIndex === 0) {
|
|
1213
|
-
out.push(...run.
|
|
1283
|
+
out.push(...run.entries.slice(-maxUnchangedRun).map(formatEntry));
|
|
1214
1284
|
break;
|
|
1215
1285
|
}
|
|
1216
1286
|
if (runIndex === runs.length - 1) {
|
|
1217
|
-
out.push(...run.
|
|
1287
|
+
out.push(...run.entries.slice(0, maxUnchangedRun).map(formatEntry));
|
|
1218
1288
|
break;
|
|
1219
1289
|
}
|
|
1220
|
-
out.push(...
|
|
1290
|
+
out.push(...collapseUnchangedMiddle(run.entries, maxUnchangedRun));
|
|
1221
1291
|
break;
|
|
1222
1292
|
}
|
|
1223
1293
|
}
|
|
1224
1294
|
|
|
1225
|
-
if (out.length > maxOutputLines) {
|
|
1226
|
-
const hidden = out.length - maxOutputLines;
|
|
1227
|
-
return {
|
|
1228
|
-
preview: [...out.slice(0, maxOutputLines), ` ... ${hidden} more preview lines`].join("\n"),
|
|
1229
|
-
addedLines,
|
|
1230
|
-
removedLines,
|
|
1231
|
-
};
|
|
1232
|
-
}
|
|
1233
|
-
|
|
1234
1295
|
return { preview: out.join("\n"), addedLines, removedLines };
|
|
1235
1296
|
}
|
|
1236
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/memories/index.ts
CHANGED
|
@@ -277,6 +277,11 @@ async function runPhase1(options: {
|
|
|
277
277
|
});
|
|
278
278
|
|
|
279
279
|
if (result.kind === "failed") {
|
|
280
|
+
logger.error("Memory phase1 stage1 job failed", {
|
|
281
|
+
threadId: claim.threadId,
|
|
282
|
+
rolloutPath: claim.rolloutPath,
|
|
283
|
+
reason: result.reason,
|
|
284
|
+
});
|
|
280
285
|
markStage1Failed(db, {
|
|
281
286
|
threadId: claim.threadId,
|
|
282
287
|
ownershipToken: claim.ownershipToken,
|
|
@@ -348,7 +348,7 @@ const OPTION_PROVIDERS: Partial<Record<SettingPath, OptionProvider>> = {
|
|
|
348
348
|
{ value: "kagi", label: "Kagi", description: "Requires KAGI_API_KEY and Kagi Search API beta access" },
|
|
349
349
|
{ value: "synthetic", label: "Synthetic", description: "Requires SYNTHETIC_API_KEY" },
|
|
350
350
|
{ value: "parallel", label: "Parallel", description: "Requires PARALLEL_API_KEY" },
|
|
351
|
-
{ value: "searxng", label: "SearXNG", description: "
|
|
351
|
+
{ value: "searxng", label: "SearXNG", description: "Requires searxng.endpoint" },
|
|
352
352
|
],
|
|
353
353
|
"providers.image": [
|
|
354
354
|
{
|
|
@@ -659,9 +659,9 @@ export class SelectorController {
|
|
|
659
659
|
return;
|
|
660
660
|
}
|
|
661
661
|
|
|
662
|
-
// Update UI
|
|
662
|
+
// Update UI — pass the context built by navigateTree to skip a second O(N) walk.
|
|
663
663
|
this.ctx.chatContainer.clear();
|
|
664
|
-
this.ctx.renderInitialMessages();
|
|
664
|
+
this.ctx.renderInitialMessages(result.sessionContext);
|
|
665
665
|
await this.ctx.reloadTodos();
|
|
666
666
|
if (result.editorText && !this.ctx.editor.getText().trim()) {
|
|
667
667
|
this.ctx.editor.setText(result.editorText);
|