@oh-my-pi/pi-coding-agent 14.7.0 → 14.7.2
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 +24 -0
- package/package.json +12 -12
- package/src/cli/grep-cli.ts +1 -1
- package/src/config/model-equivalence.ts +1 -0
- package/src/config/model-registry.ts +108 -22
- package/src/config/settings-schema.ts +46 -1
- package/src/config/settings.ts +71 -1
- package/src/dap/client.ts +1 -0
- package/src/discovery/builtin.ts +34 -9
- package/src/discovery/helpers.ts +4 -3
- package/src/edit/index.ts +1 -0
- package/src/edit/modes/hashline.ts +212 -63
- package/src/eval/py/gateway-coordinator.ts +2 -3
- package/src/eval/py/runtime.ts +1 -0
- package/src/internal-urls/docs-index.generated.ts +2 -2
- package/src/lsp/index.ts +2 -0
- package/src/main.ts +10 -15
- package/src/mcp/discoverable-tool-metadata.ts +24 -202
- package/src/modes/components/extensions/extension-dashboard.ts +26 -2
- package/src/modes/components/extensions/state-manager.ts +41 -0
- package/src/modes/controllers/selector-controller.ts +3 -0
- package/src/modes/interactive-mode.ts +45 -13
- package/src/prompts/system/plan-mode-active.md +7 -3
- package/src/prompts/system/plan-mode-approved.md +5 -0
- package/src/prompts/tools/search-tool-bm25.md +14 -14
- package/src/prompts/tools/todo-write.md +1 -0
- package/src/sdk.ts +69 -8
- package/src/session/agent-session.ts +177 -1
- package/src/slash-commands/builtin-registry.ts +13 -2
- package/src/task/index.ts +2 -0
- package/src/task/isolation-backend.ts +22 -0
- package/src/tool-discovery/tool-index.ts +377 -0
- package/src/tools/ask.ts +2 -0
- package/src/tools/ast-edit.ts +2 -0
- package/src/tools/ast-grep.ts +2 -0
- package/src/tools/bash.ts +1 -0
- package/src/tools/browser.ts +2 -0
- package/src/tools/calculator.ts +2 -0
- package/src/tools/checkpoint.ts +4 -0
- package/src/tools/debug.ts +2 -0
- package/src/tools/eval.ts +2 -0
- package/src/tools/find.ts +2 -0
- package/src/tools/gh.ts +2 -0
- package/src/tools/hindsight-recall.ts +2 -0
- package/src/tools/hindsight-reflect.ts +2 -0
- package/src/tools/hindsight-retain.ts +2 -0
- package/src/tools/index.ts +74 -14
- package/src/tools/inspect-image.ts +2 -0
- package/src/tools/irc.ts +2 -1
- package/src/tools/job.ts +2 -1
- package/src/tools/notebook.ts +2 -0
- package/src/tools/read.ts +7 -1
- package/src/tools/recipe/index.ts +2 -0
- package/src/tools/render-mermaid.ts +2 -0
- package/src/tools/search-tool-bm25.ts +128 -42
- package/src/tools/search.ts +2 -0
- package/src/tools/ssh.ts +2 -0
- package/src/tools/todo-write.ts +2 -1
- package/src/tools/write.ts +2 -0
- package/src/web/search/index.ts +2 -0
- package/src/web/search/providers/searxng.ts +8 -0
|
@@ -981,6 +981,110 @@ function countMatchingSuffixBlock(fileLines: string[], endLine: number, replacem
|
|
|
981
981
|
return 0;
|
|
982
982
|
}
|
|
983
983
|
|
|
984
|
+
// Single-line duplicate absorption is limited to structural closing delimiters.
|
|
985
|
+
// General one-line context is too easy to delete incorrectly, but duplicated
|
|
986
|
+
// `};` / `)` / `]` boundaries usually indicate a replacement range stopped one
|
|
987
|
+
// line early and would otherwise produce a syntax error.
|
|
988
|
+
const STRUCTURAL_CLOSING_BOUNDARY_RE = /^\s*[\])}]+[;,]?\s*$/;
|
|
989
|
+
|
|
990
|
+
function isStructuralClosingBoundaryLine(line: string): boolean {
|
|
991
|
+
return STRUCTURAL_CLOSING_BOUNDARY_RE.test(line);
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
interface DelimiterBalance {
|
|
995
|
+
paren: number;
|
|
996
|
+
bracket: number;
|
|
997
|
+
brace: number;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
const ZERO_DELIMITER_BALANCE: DelimiterBalance = { paren: 0, bracket: 0, brace: 0 };
|
|
1001
|
+
|
|
1002
|
+
/**
|
|
1003
|
+
* Naive bracket counter — does NOT skip string/template/comment contents. The
|
|
1004
|
+
* single-line structural absorb relies on this being safe-by-asymmetry: the
|
|
1005
|
+
* candidate boundary line is constrained by `STRUCTURAL_CLOSING_BOUNDARY_RE`
|
|
1006
|
+
* to be pure delimiters, so noise in deleted lines or non-boundary kept payload
|
|
1007
|
+
* tends to push `expected !== kept` and biases the heuristic toward NOT
|
|
1008
|
+
* absorbing (the safe direction). If we ever extend this to opening boundaries
|
|
1009
|
+
* or non-structural single lines, swap this for a real tokenizer.
|
|
1010
|
+
*/
|
|
1011
|
+
function computeDelimiterBalance(lines: string[]): DelimiterBalance {
|
|
1012
|
+
const balance: DelimiterBalance = { paren: 0, bracket: 0, brace: 0 };
|
|
1013
|
+
for (const line of lines) {
|
|
1014
|
+
for (const char of line) {
|
|
1015
|
+
switch (char) {
|
|
1016
|
+
case "(":
|
|
1017
|
+
balance.paren++;
|
|
1018
|
+
break;
|
|
1019
|
+
case ")":
|
|
1020
|
+
balance.paren--;
|
|
1021
|
+
break;
|
|
1022
|
+
case "[":
|
|
1023
|
+
balance.bracket++;
|
|
1024
|
+
break;
|
|
1025
|
+
case "]":
|
|
1026
|
+
balance.bracket--;
|
|
1027
|
+
break;
|
|
1028
|
+
case "{":
|
|
1029
|
+
balance.brace++;
|
|
1030
|
+
break;
|
|
1031
|
+
case "}":
|
|
1032
|
+
balance.brace--;
|
|
1033
|
+
break;
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
return balance;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
function delimiterBalancesEqual(a: DelimiterBalance, b: DelimiterBalance): boolean {
|
|
1041
|
+
return a.paren === b.paren && a.bracket === b.bracket && a.brace === b.brace;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
/**
|
|
1045
|
+
* Decides whether the structural-boundary candidate should be dropped: the
|
|
1046
|
+
* `keptPayload` (full payload with the boundary line removed) must restore the
|
|
1047
|
+
* caller's `expectedBalance`, while the `fullPayload` (boundary line still
|
|
1048
|
+
* present) must NOT. For replacements `expectedBalance` is the deleted
|
|
1049
|
+
* region's net delimiter balance; for pure inserts it is zero.
|
|
1050
|
+
*/
|
|
1051
|
+
function shouldDropSingleStructuralBoundary(
|
|
1052
|
+
fullPayload: string[],
|
|
1053
|
+
keptPayload: string[],
|
|
1054
|
+
expectedBalance: DelimiterBalance,
|
|
1055
|
+
): boolean {
|
|
1056
|
+
return (
|
|
1057
|
+
delimiterBalancesEqual(computeDelimiterBalance(keptPayload), expectedBalance) &&
|
|
1058
|
+
!delimiterBalancesEqual(computeDelimiterBalance(fullPayload), expectedBalance)
|
|
1059
|
+
);
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
function countMatchingSingleStructuralPrefixBoundary(
|
|
1063
|
+
fileLines: string[],
|
|
1064
|
+
startLine: number,
|
|
1065
|
+
replacement: string[],
|
|
1066
|
+
expectedBalance: DelimiterBalance,
|
|
1067
|
+
): number {
|
|
1068
|
+
if (replacement.length === 0 || startLine <= 1) return 0;
|
|
1069
|
+
const line = replacement[0];
|
|
1070
|
+
if (!isStructuralClosingBoundaryLine(line)) return 0;
|
|
1071
|
+
if (fileLines[startLine - 2] !== line) return 0;
|
|
1072
|
+
return shouldDropSingleStructuralBoundary(replacement, replacement.slice(1), expectedBalance) ? 1 : 0;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
function countMatchingSingleStructuralSuffixBoundary(
|
|
1076
|
+
fileLines: string[],
|
|
1077
|
+
endLine: number,
|
|
1078
|
+
replacement: string[],
|
|
1079
|
+
expectedBalance: DelimiterBalance,
|
|
1080
|
+
): number {
|
|
1081
|
+
if (replacement.length === 0 || endLine >= fileLines.length) return 0;
|
|
1082
|
+
const line = replacement[replacement.length - 1];
|
|
1083
|
+
if (!isStructuralClosingBoundaryLine(line)) return 0;
|
|
1084
|
+
if (fileLines[endLine] !== line) return 0;
|
|
1085
|
+
return shouldDropSingleStructuralBoundary(replacement, replacement.slice(0, -1), expectedBalance) ? 1 : 0;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
984
1088
|
function hasExternalTargets(lines: Iterable<number>, externalTargetLines: Set<number>): boolean {
|
|
985
1089
|
for (const line of lines) {
|
|
986
1090
|
if (externalTargetLines.has(line)) return true;
|
|
@@ -1087,49 +1191,80 @@ interface PureInsertAbsorbResult {
|
|
|
1087
1191
|
* Mirror of replacement-absorb's prefix/suffix block check, but for pure
|
|
1088
1192
|
* inserts: drop payload lines that exactly duplicate the file lines
|
|
1089
1193
|
* immediately above (leading) or immediately below (trailing) the insertion
|
|
1090
|
-
* point.
|
|
1091
|
-
*
|
|
1194
|
+
* point. Generic context echo absorption requires a minimum run of 2, but a
|
|
1195
|
+
* single structural closing delimiter is absorbed because duplicated `}` /
|
|
1196
|
+
* `});`-style boundaries almost always mean the insert included adjacent
|
|
1197
|
+
* context.
|
|
1092
1198
|
*/
|
|
1093
|
-
function tryAbsorbPureInsertGroup(
|
|
1199
|
+
function tryAbsorbPureInsertGroup(
|
|
1200
|
+
group: HashlinePureInsertGroup,
|
|
1201
|
+
fileLines: string[],
|
|
1202
|
+
allowGenericBoundaryAbsorb: boolean,
|
|
1203
|
+
): PureInsertAbsorbResult {
|
|
1094
1204
|
const empty: PureInsertAbsorbResult = { keptPayload: group.payload, absorbedLeading: 0, absorbedTrailing: 0 };
|
|
1095
|
-
if (group.payload.length
|
|
1205
|
+
if (group.payload.length === 0) return empty;
|
|
1096
1206
|
|
|
1097
1207
|
const { aboveEndIdx, belowStartIdx } = pureInsertNeighborhood(group.cursor, fileLines);
|
|
1098
1208
|
|
|
1099
1209
|
// Leading: payload[0..k-1] vs fileLines[aboveEndIdx-k+1 .. aboveEndIdx].
|
|
1100
1210
|
let absorbedLeading = 0;
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
let
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1211
|
+
if (allowGenericBoundaryAbsorb) {
|
|
1212
|
+
const maxLead = Math.min(group.payload.length, aboveEndIdx + 1);
|
|
1213
|
+
for (let count = maxLead; count >= 2; count--) {
|
|
1214
|
+
let ok = true;
|
|
1215
|
+
for (let offset = 0; offset < count; offset++) {
|
|
1216
|
+
if (group.payload[offset] !== fileLines[aboveEndIdx - count + 1 + offset]) {
|
|
1217
|
+
ok = false;
|
|
1218
|
+
break;
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
if (ok) {
|
|
1222
|
+
absorbedLeading = count;
|
|
1107
1223
|
break;
|
|
1108
1224
|
}
|
|
1109
1225
|
}
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1226
|
+
}
|
|
1227
|
+
if (
|
|
1228
|
+
absorbedLeading === 0 &&
|
|
1229
|
+
group.payload.length > 0 &&
|
|
1230
|
+
aboveEndIdx >= 0 &&
|
|
1231
|
+
isStructuralClosingBoundaryLine(group.payload[0]) &&
|
|
1232
|
+
group.payload[0] === fileLines[aboveEndIdx] &&
|
|
1233
|
+
shouldDropSingleStructuralBoundary(group.payload, group.payload.slice(1), ZERO_DELIMITER_BALANCE)
|
|
1234
|
+
) {
|
|
1235
|
+
absorbedLeading = 1;
|
|
1114
1236
|
}
|
|
1115
1237
|
|
|
1116
1238
|
// Trailing: payload[len-k..len-1] vs fileLines[belowStartIdx..belowStartIdx+k-1].
|
|
1117
1239
|
// Don't double-count payload lines already absorbed as leading.
|
|
1118
1240
|
let absorbedTrailing = 0;
|
|
1119
|
-
const
|
|
1120
|
-
const
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
for (let
|
|
1124
|
-
|
|
1125
|
-
|
|
1241
|
+
const remainingPayload = group.payload.slice(absorbedLeading);
|
|
1242
|
+
const remaining = remainingPayload.length;
|
|
1243
|
+
if (allowGenericBoundaryAbsorb) {
|
|
1244
|
+
const maxTrail = Math.min(remaining, fileLines.length - belowStartIdx);
|
|
1245
|
+
for (let count = maxTrail; count >= 2; count--) {
|
|
1246
|
+
let ok = true;
|
|
1247
|
+
for (let offset = 0; offset < count; offset++) {
|
|
1248
|
+
if (group.payload[group.payload.length - count + offset] !== fileLines[belowStartIdx + offset]) {
|
|
1249
|
+
ok = false;
|
|
1250
|
+
break;
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
if (ok) {
|
|
1254
|
+
absorbedTrailing = count;
|
|
1126
1255
|
break;
|
|
1127
1256
|
}
|
|
1128
1257
|
}
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1258
|
+
}
|
|
1259
|
+
if (
|
|
1260
|
+
absorbedTrailing === 0 &&
|
|
1261
|
+
remaining > 0 &&
|
|
1262
|
+
belowStartIdx < fileLines.length &&
|
|
1263
|
+
isStructuralClosingBoundaryLine(remainingPayload[remainingPayload.length - 1]) &&
|
|
1264
|
+
remainingPayload[remainingPayload.length - 1] === fileLines[belowStartIdx] &&
|
|
1265
|
+
shouldDropSingleStructuralBoundary(remainingPayload, remainingPayload.slice(0, -1), ZERO_DELIMITER_BALANCE)
|
|
1266
|
+
) {
|
|
1267
|
+
absorbedTrailing = 1;
|
|
1133
1268
|
}
|
|
1134
1269
|
|
|
1135
1270
|
if (absorbedLeading === 0 && absorbedTrailing === 0) return empty;
|
|
@@ -1164,46 +1299,53 @@ function absorbReplacementBoundaryDuplicates(
|
|
|
1164
1299
|
for (let index = 0; index < edits.length; index++) {
|
|
1165
1300
|
const group = findReplacementGroup(edits, index);
|
|
1166
1301
|
if (!group) {
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
const { start, end } = result.trailingFileRange;
|
|
1185
|
-
const key = `pure-insert-trailing:${start}..${end}`;
|
|
1186
|
-
if (!emittedAbsorbKeys.has(key)) {
|
|
1187
|
-
emittedAbsorbKeys.add(key);
|
|
1188
|
-
warnings.push(
|
|
1189
|
-
`Auto-dropped ${result.absorbedTrailing} duplicate line(s) at the end of insert at line ${pureInsert.sourceLineNum} ` +
|
|
1190
|
-
`(file lines ${start}..${end} already match the payload's trailing lines).`,
|
|
1191
|
-
);
|
|
1192
|
-
}
|
|
1302
|
+
const pureInsert = findPureInsertGroup(edits, index);
|
|
1303
|
+
if (pureInsert) {
|
|
1304
|
+
const result = tryAbsorbPureInsertGroup(
|
|
1305
|
+
pureInsert,
|
|
1306
|
+
fileLines,
|
|
1307
|
+
options.autoDropPureInsertDuplicates === true,
|
|
1308
|
+
);
|
|
1309
|
+
if (result.absorbedLeading > 0 || result.absorbedTrailing > 0) {
|
|
1310
|
+
if (result.leadingFileRange) {
|
|
1311
|
+
const { start, end } = result.leadingFileRange;
|
|
1312
|
+
const key = `pure-insert-leading:${start}..${end}`;
|
|
1313
|
+
if (!emittedAbsorbKeys.has(key)) {
|
|
1314
|
+
emittedAbsorbKeys.add(key);
|
|
1315
|
+
warnings.push(
|
|
1316
|
+
`Auto-dropped ${result.absorbedLeading} duplicate line(s) at the start of insert at line ${pureInsert.sourceLineNum} ` +
|
|
1317
|
+
`(file lines ${start}..${end} already match the payload's leading lines).`,
|
|
1318
|
+
);
|
|
1193
1319
|
}
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1320
|
+
}
|
|
1321
|
+
if (result.trailingFileRange) {
|
|
1322
|
+
const { start, end } = result.trailingFileRange;
|
|
1323
|
+
const key = `pure-insert-trailing:${start}..${end}`;
|
|
1324
|
+
if (!emittedAbsorbKeys.has(key)) {
|
|
1325
|
+
emittedAbsorbKeys.add(key);
|
|
1326
|
+
warnings.push(
|
|
1327
|
+
`Auto-dropped ${result.absorbedTrailing} duplicate line(s) at the end of insert at line ${pureInsert.sourceLineNum} ` +
|
|
1328
|
+
`(file lines ${start}..${end} already match the payload's trailing lines).`,
|
|
1329
|
+
);
|
|
1202
1330
|
}
|
|
1203
|
-
index = pureInsert.endIndex;
|
|
1204
|
-
continue;
|
|
1205
1331
|
}
|
|
1332
|
+
for (const text of result.keptPayload) {
|
|
1333
|
+
absorbed.push({
|
|
1334
|
+
kind: "insert",
|
|
1335
|
+
cursor: cloneCursor(pureInsert.cursor),
|
|
1336
|
+
text,
|
|
1337
|
+
lineNum: pureInsert.sourceLineNum,
|
|
1338
|
+
index: nextSyntheticIndex++,
|
|
1339
|
+
});
|
|
1340
|
+
}
|
|
1341
|
+
index = pureInsert.endIndex;
|
|
1342
|
+
continue;
|
|
1343
|
+
}
|
|
1344
|
+
for (let groupIndex = pureInsert.startIndex; groupIndex <= pureInsert.endIndex; groupIndex++) {
|
|
1345
|
+
absorbed.push(edits[groupIndex]);
|
|
1206
1346
|
}
|
|
1347
|
+
index = pureInsert.endIndex;
|
|
1348
|
+
continue;
|
|
1207
1349
|
}
|
|
1208
1350
|
absorbed.push(edits[index]);
|
|
1209
1351
|
continue;
|
|
@@ -1212,8 +1354,15 @@ function absorbReplacementBoundaryDuplicates(
|
|
|
1212
1354
|
const startLine = group.deletes[0].anchor.line;
|
|
1213
1355
|
const endLine = group.deletes[group.deletes.length - 1].anchor.line;
|
|
1214
1356
|
|
|
1215
|
-
const
|
|
1216
|
-
|
|
1357
|
+
const deletedBalance = computeDelimiterBalance(
|
|
1358
|
+
group.deletes.map(deleteEdit => fileLines[deleteEdit.anchor.line - 1] ?? ""),
|
|
1359
|
+
);
|
|
1360
|
+
const prefixCount =
|
|
1361
|
+
countMatchingPrefixBlock(fileLines, startLine, group.replacement) ||
|
|
1362
|
+
countMatchingSingleStructuralPrefixBoundary(fileLines, startLine, group.replacement, deletedBalance);
|
|
1363
|
+
const suffixCount =
|
|
1364
|
+
countMatchingSuffixBlock(fileLines, endLine, group.replacement) ||
|
|
1365
|
+
countMatchingSingleStructuralSuffixBoundary(fileLines, endLine, group.replacement, deletedBalance);
|
|
1217
1366
|
const prefixLines = contiguousRange(startLine - prefixCount, prefixCount);
|
|
1218
1367
|
const suffixLines = contiguousRange(endLine + 1, suffixCount);
|
|
1219
1368
|
const safePrefixCount = hasExternalTargets(prefixLines, allTargetLines) ? 0 : prefixCount;
|
|
@@ -2,13 +2,12 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import { createServer } from "node:net";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { Process } from "@oh-my-pi/pi-natives";
|
|
5
|
-
import {
|
|
5
|
+
import { getPythonGatewayDir, isEnoent, logger, procmgr } from "@oh-my-pi/pi-utils";
|
|
6
6
|
import type { Subprocess } from "bun";
|
|
7
7
|
import { Settings } from "../../config/settings";
|
|
8
8
|
import { getOrCreateSnapshot } from "../../utils/shell-snapshot";
|
|
9
9
|
import { filterEnv, resolvePythonRuntime } from "./runtime";
|
|
10
10
|
|
|
11
|
-
const GATEWAY_DIR_NAME = "python-gateway";
|
|
12
11
|
const GATEWAY_INFO_FILE = "gateway.json";
|
|
13
12
|
const GATEWAY_LOCK_FILE = "gateway.lock";
|
|
14
13
|
const GATEWAY_STARTUP_TIMEOUT_MS = 30000;
|
|
@@ -66,7 +65,7 @@ async function allocatePort(): Promise<number> {
|
|
|
66
65
|
}
|
|
67
66
|
|
|
68
67
|
function getGatewayDir(): string {
|
|
69
|
-
return
|
|
68
|
+
return getPythonGatewayDir();
|
|
70
69
|
}
|
|
71
70
|
|
|
72
71
|
function getGatewayInfoPath(): string {
|