pi-repoprompt-cli 0.2.6 → 0.2.8
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/README.md +6 -1
- package/extensions/repoprompt-cli/README.md +20 -1
- package/extensions/repoprompt-cli/auto-select.ts +288 -0
- package/extensions/repoprompt-cli/config.json.example +2 -1
- package/extensions/repoprompt-cli/config.ts +1 -0
- package/extensions/repoprompt-cli/index.ts +1656 -192
- package/extensions/repoprompt-cli/types.ts +34 -2
- package/package.json +5 -1
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
|
|
1
4
|
import type { ExtensionAPI, ExtensionContext, ToolRenderResultOptions } from "@mariozechner/pi-coding-agent";
|
|
2
5
|
import { highlightCode, Theme } from "@mariozechner/pi-coding-agent";
|
|
3
6
|
import { Text } from "@mariozechner/pi-tui";
|
|
@@ -5,13 +8,25 @@ import { Type } from "@sinclair/typebox";
|
|
|
5
8
|
import * as Diff from "diff";
|
|
6
9
|
|
|
7
10
|
import { loadConfig } from "./config.js";
|
|
11
|
+
import {
|
|
12
|
+
computeSliceRangeFromReadArgs,
|
|
13
|
+
countFileLines,
|
|
14
|
+
inferSelectionStatus,
|
|
15
|
+
toPosixPath,
|
|
16
|
+
} from "./auto-select.js";
|
|
8
17
|
import { RP_READCACHE_CUSTOM_TYPE, SCOPE_FULL, scopeRange } from "./readcache/constants.js";
|
|
9
18
|
import { buildInvalidationV1 } from "./readcache/meta.js";
|
|
10
19
|
import { getStoreStats, pruneObjectsOlderThan } from "./readcache/object-store.js";
|
|
11
20
|
import { readFileWithCache } from "./readcache/read-file.js";
|
|
12
21
|
import { clearReplayRuntimeState, createReplayRuntimeState } from "./readcache/replay.js";
|
|
13
|
-
import { resolveReadFilePath } from "./readcache/resolve.js";
|
|
22
|
+
import { clearRootsCache, resolveReadFilePath } from "./readcache/resolve.js";
|
|
14
23
|
import type { RpReadcacheMetaV1, ScopeKey } from "./readcache/types.js";
|
|
24
|
+
import type {
|
|
25
|
+
AutoSelectionEntryData,
|
|
26
|
+
AutoSelectionEntryRangeData,
|
|
27
|
+
AutoSelectionEntrySliceData,
|
|
28
|
+
RpCliBindingEntryData,
|
|
29
|
+
} from "./types.js";
|
|
15
30
|
|
|
16
31
|
let parseBash: ((input: string) => any) | null = null;
|
|
17
32
|
let justBashLoadPromise: Promise<void> | null = null;
|
|
@@ -239,12 +254,22 @@ function hasPipeOutsideQuotes(script: string): boolean {
|
|
|
239
254
|
* - Provide actionable error messages when blocked
|
|
240
255
|
* - For best command parsing (AST-based), install `just-bash` >= 2; otherwise it falls back to a legacy splitter
|
|
241
256
|
* - Syntax-highlight fenced code blocks in output (read, structure, etc.)
|
|
242
|
-
* -
|
|
257
|
+
* - Delta-powered diff highlighting (with graceful fallback when delta is unavailable)
|
|
243
258
|
*/
|
|
244
259
|
|
|
245
260
|
const DEFAULT_TIMEOUT_MS = 15 * 60 * 1000;
|
|
246
261
|
const DEFAULT_MAX_OUTPUT_CHARS = 12000;
|
|
247
262
|
const BINDING_CUSTOM_TYPE = "repoprompt-binding";
|
|
263
|
+
const AUTO_SELECTION_CUSTOM_TYPE = "repoprompt-cli-auto-selection";
|
|
264
|
+
const WINDOWS_CACHE_TTL_MS = 5000;
|
|
265
|
+
const BINDING_VALIDATION_TTL_MS = 5000;
|
|
266
|
+
|
|
267
|
+
interface RpCliWindow {
|
|
268
|
+
windowId: number;
|
|
269
|
+
workspaceId?: string;
|
|
270
|
+
workspaceName?: string;
|
|
271
|
+
rootFolderPaths?: string[];
|
|
272
|
+
}
|
|
248
273
|
|
|
249
274
|
const BindParams = Type.Object({
|
|
250
275
|
windowId: Type.Number({ description: "RepoPrompt window id (from `rp-cli -e windows`)" }),
|
|
@@ -954,6 +979,88 @@ function parseFencedBlocks(text: string): FencedBlock[] {
|
|
|
954
979
|
return blocks;
|
|
955
980
|
}
|
|
956
981
|
|
|
982
|
+
const ANSI_ESCAPE_RE = /\x1b\[[0-9;]*m/g;
|
|
983
|
+
const DELTA_TIMEOUT_MS = 5000;
|
|
984
|
+
const DELTA_MAX_BUFFER = 8 * 1024 * 1024;
|
|
985
|
+
const DELTA_CACHE_MAX_ENTRIES = 200;
|
|
986
|
+
|
|
987
|
+
let deltaAvailable: boolean | null = null;
|
|
988
|
+
const deltaDiffCache = new Map<string, string | null>();
|
|
989
|
+
|
|
990
|
+
function isDeltaInstalled(): boolean {
|
|
991
|
+
if (deltaAvailable !== null) {
|
|
992
|
+
return deltaAvailable;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
const check = spawnSync("delta", ["--version"], {
|
|
996
|
+
stdio: "ignore",
|
|
997
|
+
timeout: 1000,
|
|
998
|
+
});
|
|
999
|
+
|
|
1000
|
+
deltaAvailable = !check.error && check.status === 0;
|
|
1001
|
+
return deltaAvailable;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
function runDelta(diffText: string): string | null {
|
|
1005
|
+
const result = spawnSync("delta", ["--color-only", "--paging=never"], {
|
|
1006
|
+
encoding: "utf-8",
|
|
1007
|
+
input: diffText,
|
|
1008
|
+
timeout: DELTA_TIMEOUT_MS,
|
|
1009
|
+
maxBuffer: DELTA_MAX_BUFFER,
|
|
1010
|
+
});
|
|
1011
|
+
|
|
1012
|
+
if (result.error || result.status !== 0) {
|
|
1013
|
+
return null;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
return typeof result.stdout === "string" ? result.stdout : null;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
function stripSyntheticHeader(deltaOutput: string): string {
|
|
1020
|
+
const outputLines = deltaOutput.split("\n");
|
|
1021
|
+
const bodyStart = outputLines.findIndex((line) => line.replace(ANSI_ESCAPE_RE, "").startsWith("@@"));
|
|
1022
|
+
|
|
1023
|
+
if (bodyStart >= 0) {
|
|
1024
|
+
return outputLines.slice(bodyStart + 1).join("\n");
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
return deltaOutput;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
function renderDiffBlockWithDelta(code: string): string | null {
|
|
1031
|
+
if (!isDeltaInstalled()) {
|
|
1032
|
+
return null;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
const cached = deltaDiffCache.get(code);
|
|
1036
|
+
if (cached !== undefined) {
|
|
1037
|
+
return cached;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
let rendered = runDelta(code);
|
|
1041
|
+
|
|
1042
|
+
if (!rendered) {
|
|
1043
|
+
const syntheticDiff = [
|
|
1044
|
+
"--- a/file",
|
|
1045
|
+
"+++ b/file",
|
|
1046
|
+
"@@ -1,1 +1,1 @@",
|
|
1047
|
+
code,
|
|
1048
|
+
].join("\n");
|
|
1049
|
+
|
|
1050
|
+
const syntheticRendered = runDelta(syntheticDiff);
|
|
1051
|
+
if (syntheticRendered) {
|
|
1052
|
+
rendered = stripSyntheticHeader(syntheticRendered);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
if (deltaDiffCache.size >= DELTA_CACHE_MAX_ENTRIES) {
|
|
1057
|
+
deltaDiffCache.clear();
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
deltaDiffCache.set(code, rendered);
|
|
1061
|
+
return rendered;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
957
1064
|
/**
|
|
958
1065
|
* Compute word-level diff with inverse highlighting on changed parts
|
|
959
1066
|
*/
|
|
@@ -1005,6 +1112,11 @@ function renderIntraLineDiff(
|
|
|
1005
1112
|
* Render diff lines with syntax highlighting (red/green, word-level inverse)
|
|
1006
1113
|
*/
|
|
1007
1114
|
function renderDiffBlock(code: string, theme: Theme): string {
|
|
1115
|
+
const deltaRendered = renderDiffBlockWithDelta(code);
|
|
1116
|
+
if (deltaRendered !== null) {
|
|
1117
|
+
return deltaRendered;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1008
1120
|
const lines = code.split("\n");
|
|
1009
1121
|
const result: string[] = [];
|
|
1010
1122
|
|
|
@@ -1094,7 +1206,7 @@ function renderDiffBlock(code: string, theme: Theme): string {
|
|
|
1094
1206
|
|
|
1095
1207
|
/**
|
|
1096
1208
|
* Render rp_exec output with syntax highlighting for fenced code blocks.
|
|
1097
|
-
* - ```diff blocks
|
|
1209
|
+
* - ```diff blocks use delta when available, with word-level fallback
|
|
1098
1210
|
* - Other fenced blocks get syntax highlighting via Pi's highlightCode
|
|
1099
1211
|
* - Non-fenced content is rendered dim (no markdown parsing)
|
|
1100
1212
|
*/
|
|
@@ -1161,249 +1273,1577 @@ export default function (pi: ExtensionAPI) {
|
|
|
1161
1273
|
clearReplayRuntimeState(readcacheRuntimeState);
|
|
1162
1274
|
};
|
|
1163
1275
|
|
|
1276
|
+
let activeAutoSelectionState: AutoSelectionEntryData | null = null;
|
|
1277
|
+
|
|
1164
1278
|
let boundWindowId: number | undefined;
|
|
1165
1279
|
let boundTab: string | undefined;
|
|
1280
|
+
let boundWorkspaceId: string | undefined;
|
|
1281
|
+
let boundWorkspaceName: string | undefined;
|
|
1282
|
+
let boundWorkspaceRoots: string[] | undefined;
|
|
1166
1283
|
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
boundTab = tab;
|
|
1170
|
-
};
|
|
1284
|
+
let windowsCache: { windows: RpCliWindow[]; fetchedAtMs: number } | null = null;
|
|
1285
|
+
let lastBindingValidationAtMs = 0;
|
|
1171
1286
|
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1287
|
+
function sameOptionalText(a?: string, b?: string): boolean {
|
|
1288
|
+
return (a ?? undefined) === (b ?? undefined);
|
|
1289
|
+
}
|
|
1175
1290
|
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
}
|
|
1291
|
+
function clearWindowsCache(): void {
|
|
1292
|
+
windowsCache = null;
|
|
1293
|
+
}
|
|
1179
1294
|
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
boundWindowId = undefined;
|
|
1184
|
-
boundTab = undefined;
|
|
1295
|
+
function markBindingValidationStale(): void {
|
|
1296
|
+
lastBindingValidationAtMs = 0;
|
|
1297
|
+
}
|
|
1185
1298
|
|
|
1186
|
-
|
|
1187
|
-
|
|
1299
|
+
function shouldRevalidateBinding(): boolean {
|
|
1300
|
+
const now = Date.now();
|
|
1301
|
+
return (now - lastBindingValidationAtMs) >= BINDING_VALIDATION_TTL_MS;
|
|
1302
|
+
}
|
|
1188
1303
|
|
|
1189
|
-
|
|
1190
|
-
|
|
1304
|
+
function markBindingValidatedNow(): void {
|
|
1305
|
+
lastBindingValidationAtMs = Date.now();
|
|
1306
|
+
}
|
|
1191
1307
|
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
if (windowId !== undefined && tab) {
|
|
1196
|
-
reconstructedWindowId = windowId;
|
|
1197
|
-
reconstructedTab = tab;
|
|
1198
|
-
}
|
|
1308
|
+
function normalizeWorkspaceRoots(roots: string[] | undefined): string[] {
|
|
1309
|
+
if (!Array.isArray(roots)) {
|
|
1310
|
+
return [];
|
|
1199
1311
|
}
|
|
1200
1312
|
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1313
|
+
return [...new Set(roots.map((root) => toPosixPath(String(root).trim())).filter(Boolean))].sort();
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
function workspaceRootsEqual(left: string[] | undefined, right: string[] | undefined): boolean {
|
|
1317
|
+
const leftNormalized = normalizeWorkspaceRoots(left);
|
|
1318
|
+
const rightNormalized = normalizeWorkspaceRoots(right);
|
|
1319
|
+
return JSON.stringify(leftNormalized) === JSON.stringify(rightNormalized);
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
function workspaceIdentityMatches(
|
|
1323
|
+
left: { workspaceId?: string; workspaceName?: string; workspaceRoots?: string[] },
|
|
1324
|
+
right: { workspaceId?: string; workspaceName?: string; workspaceRoots?: string[] }
|
|
1325
|
+
): boolean {
|
|
1326
|
+
if (left.workspaceId && right.workspaceId) {
|
|
1327
|
+
return left.workspaceId === right.workspaceId;
|
|
1204
1328
|
}
|
|
1205
1329
|
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
const msg = entry.message;
|
|
1209
|
-
if (msg.role !== "toolResult" || msg.toolName !== "rp_bind") continue;
|
|
1330
|
+
const leftRoots = normalizeWorkspaceRoots(left.workspaceRoots);
|
|
1331
|
+
const rightRoots = normalizeWorkspaceRoots(right.workspaceRoots);
|
|
1210
1332
|
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
persistBinding(details.windowId, details.tab);
|
|
1214
|
-
}
|
|
1333
|
+
if (leftRoots.length > 0 && rightRoots.length > 0) {
|
|
1334
|
+
return JSON.stringify(leftRoots) === JSON.stringify(rightRoots);
|
|
1215
1335
|
}
|
|
1216
|
-
};
|
|
1217
1336
|
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
clearReadcacheCaches();
|
|
1221
|
-
if (config.readcacheReadFile === true) {
|
|
1222
|
-
void pruneObjectsOlderThan(ctx.cwd).catch(() => {
|
|
1223
|
-
// Fail-open
|
|
1224
|
-
});
|
|
1337
|
+
if (left.workspaceName && right.workspaceName) {
|
|
1338
|
+
return left.workspaceName === right.workspaceName;
|
|
1225
1339
|
}
|
|
1226
|
-
reconstructBinding(ctx);
|
|
1227
|
-
});
|
|
1228
1340
|
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
clearReadcacheCaches();
|
|
1232
|
-
reconstructBinding(ctx);
|
|
1233
|
-
});
|
|
1341
|
+
return false;
|
|
1342
|
+
}
|
|
1234
1343
|
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
reconstructBinding(ctx);
|
|
1240
|
-
});
|
|
1344
|
+
function getCurrentBinding(): RpCliBindingEntryData | null {
|
|
1345
|
+
if (boundWindowId === undefined || !boundTab) {
|
|
1346
|
+
return null;
|
|
1347
|
+
}
|
|
1241
1348
|
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1349
|
+
return {
|
|
1350
|
+
windowId: boundWindowId,
|
|
1351
|
+
tab: boundTab,
|
|
1352
|
+
workspaceId: boundWorkspaceId,
|
|
1353
|
+
workspaceName: boundWorkspaceName,
|
|
1354
|
+
workspaceRoots: boundWorkspaceRoots,
|
|
1355
|
+
};
|
|
1356
|
+
}
|
|
1247
1357
|
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1358
|
+
function normalizeBindingEntry(binding: RpCliBindingEntryData): RpCliBindingEntryData {
|
|
1359
|
+
return {
|
|
1360
|
+
windowId: binding.windowId,
|
|
1361
|
+
tab: binding.tab,
|
|
1362
|
+
workspaceId: typeof binding.workspaceId === "string" ? binding.workspaceId : undefined,
|
|
1363
|
+
workspaceName: typeof binding.workspaceName === "string" ? binding.workspaceName : undefined,
|
|
1364
|
+
workspaceRoots: normalizeWorkspaceRoots(binding.workspaceRoots),
|
|
1365
|
+
};
|
|
1366
|
+
}
|
|
1253
1367
|
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1368
|
+
function bindingEntriesEqual(a: RpCliBindingEntryData | null, b: RpCliBindingEntryData | null): boolean {
|
|
1369
|
+
if (!a && !b) {
|
|
1370
|
+
return true;
|
|
1371
|
+
}
|
|
1257
1372
|
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1373
|
+
if (!a || !b) {
|
|
1374
|
+
return false;
|
|
1375
|
+
}
|
|
1261
1376
|
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
handler: async (args, ctx) => {
|
|
1265
|
-
const parsed = parseRpbindArgs(args);
|
|
1266
|
-
if ("error" in parsed) {
|
|
1267
|
-
ctx.ui.notify(parsed.error, "error");
|
|
1268
|
-
return;
|
|
1269
|
-
}
|
|
1377
|
+
const left = normalizeBindingEntry(a);
|
|
1378
|
+
const right = normalizeBindingEntry(b);
|
|
1270
1379
|
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1380
|
+
return (
|
|
1381
|
+
left.windowId === right.windowId &&
|
|
1382
|
+
left.tab === right.tab &&
|
|
1383
|
+
sameOptionalText(left.workspaceId, right.workspaceId) &&
|
|
1384
|
+
sameOptionalText(left.workspaceName, right.workspaceName) &&
|
|
1385
|
+
workspaceRootsEqual(left.workspaceRoots, right.workspaceRoots)
|
|
1386
|
+
);
|
|
1387
|
+
}
|
|
1275
1388
|
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
handler: async (_args, ctx) => {
|
|
1279
|
-
config = loadConfig();
|
|
1389
|
+
function setBinding(binding: RpCliBindingEntryData | null): void {
|
|
1390
|
+
const previousWindowId = boundWindowId;
|
|
1280
1391
|
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1392
|
+
if (!binding) {
|
|
1393
|
+
boundWindowId = undefined;
|
|
1394
|
+
boundTab = undefined;
|
|
1395
|
+
boundWorkspaceId = undefined;
|
|
1396
|
+
boundWorkspaceName = undefined;
|
|
1397
|
+
boundWorkspaceRoots = undefined;
|
|
1398
|
+
} else {
|
|
1399
|
+
const normalized = normalizeBindingEntry(binding);
|
|
1400
|
+
boundWindowId = normalized.windowId;
|
|
1401
|
+
boundTab = normalized.tab;
|
|
1402
|
+
boundWorkspaceId = normalized.workspaceId;
|
|
1403
|
+
boundWorkspaceName = normalized.workspaceName;
|
|
1404
|
+
boundWorkspaceRoots = normalized.workspaceRoots;
|
|
1405
|
+
}
|
|
1284
1406
|
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
ctx.ui.notify(msg, "info");
|
|
1289
|
-
return;
|
|
1407
|
+
if (previousWindowId !== boundWindowId) {
|
|
1408
|
+
if (previousWindowId !== undefined) {
|
|
1409
|
+
clearRootsCache(previousWindowId);
|
|
1290
1410
|
}
|
|
1291
1411
|
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
msg += `\nObject store (under ${ctx.cwd}/.pi/readcache):\n`;
|
|
1295
|
-
msg += ` Objects: ${stats.objects}\n`;
|
|
1296
|
-
msg += ` Bytes: ${stats.bytes}\n`;
|
|
1297
|
-
} catch {
|
|
1298
|
-
msg += "\nObject store: unavailable\n";
|
|
1412
|
+
if (boundWindowId !== undefined) {
|
|
1413
|
+
clearRootsCache(boundWindowId);
|
|
1299
1414
|
}
|
|
1300
1415
|
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
msg += "- Use bypass_cache=true in the read command to force baseline output\n";
|
|
1416
|
+
clearWindowsCache();
|
|
1417
|
+
}
|
|
1304
1418
|
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
});
|
|
1419
|
+
markBindingValidationStale();
|
|
1420
|
+
}
|
|
1308
1421
|
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1422
|
+
function parseBindingEntryData(value: unknown): RpCliBindingEntryData | null {
|
|
1423
|
+
if (!value || typeof value !== "object") {
|
|
1424
|
+
return null;
|
|
1425
|
+
}
|
|
1313
1426
|
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1427
|
+
const obj = value as Record<string, unknown>;
|
|
1428
|
+
|
|
1429
|
+
const windowId = typeof obj.windowId === "number" ? obj.windowId : undefined;
|
|
1430
|
+
const tab = typeof obj.tab === "string" ? obj.tab : undefined;
|
|
1431
|
+
|
|
1432
|
+
if (windowId === undefined || !tab) {
|
|
1433
|
+
return null;
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
const workspaceId = typeof obj.workspaceId === "string"
|
|
1437
|
+
? obj.workspaceId
|
|
1438
|
+
: (typeof obj.workspaceID === "string" ? obj.workspaceID : undefined);
|
|
1439
|
+
|
|
1440
|
+
const workspaceName = typeof obj.workspaceName === "string"
|
|
1441
|
+
? obj.workspaceName
|
|
1442
|
+
: (typeof obj.workspace === "string" ? obj.workspace : undefined);
|
|
1443
|
+
|
|
1444
|
+
const workspaceRoots = Array.isArray(obj.workspaceRoots)
|
|
1445
|
+
? obj.workspaceRoots.filter((root): root is string => typeof root === "string")
|
|
1446
|
+
: (Array.isArray(obj.rootFolderPaths)
|
|
1447
|
+
? obj.rootFolderPaths.filter((root): root is string => typeof root === "string")
|
|
1448
|
+
: undefined);
|
|
1449
|
+
|
|
1450
|
+
return normalizeBindingEntry({
|
|
1451
|
+
windowId,
|
|
1452
|
+
tab,
|
|
1453
|
+
workspaceId,
|
|
1454
|
+
workspaceName,
|
|
1455
|
+
workspaceRoots,
|
|
1456
|
+
});
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
function persistBinding(binding: RpCliBindingEntryData): void {
|
|
1460
|
+
const normalized = normalizeBindingEntry(binding);
|
|
1461
|
+
const current = getCurrentBinding();
|
|
1462
|
+
|
|
1463
|
+
if (bindingEntriesEqual(current, normalized)) {
|
|
1464
|
+
return;
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
setBinding(normalized);
|
|
1468
|
+
pi.appendEntry(BINDING_CUSTOM_TYPE, normalized);
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
function reconstructBinding(ctx: ExtensionContext): void {
|
|
1472
|
+
// Prefer persisted binding (appendEntry) from the *current branch*, then fall back to prior rp_bind tool results
|
|
1473
|
+
// Branch semantics: if the current branch has no binding state, stay unbound
|
|
1474
|
+
setBinding(null);
|
|
1475
|
+
|
|
1476
|
+
let reconstructed: RpCliBindingEntryData | null = null;
|
|
1477
|
+
|
|
1478
|
+
for (const entry of ctx.sessionManager.getBranch()) {
|
|
1479
|
+
if (entry.type !== "custom" || entry.customType !== BINDING_CUSTOM_TYPE) {
|
|
1480
|
+
continue;
|
|
1317
1481
|
}
|
|
1318
1482
|
|
|
1319
|
-
const
|
|
1320
|
-
if (
|
|
1321
|
-
|
|
1322
|
-
return;
|
|
1483
|
+
const parsed = parseBindingEntryData(entry.data);
|
|
1484
|
+
if (parsed) {
|
|
1485
|
+
reconstructed = parsed;
|
|
1323
1486
|
}
|
|
1487
|
+
}
|
|
1324
1488
|
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1489
|
+
if (reconstructed) {
|
|
1490
|
+
setBinding(reconstructed);
|
|
1491
|
+
return;
|
|
1492
|
+
}
|
|
1328
1493
|
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1494
|
+
for (const entry of ctx.sessionManager.getBranch()) {
|
|
1495
|
+
if (entry.type !== "message") {
|
|
1496
|
+
continue;
|
|
1332
1497
|
}
|
|
1333
1498
|
|
|
1334
|
-
const
|
|
1335
|
-
|
|
1499
|
+
const msg = entry.message;
|
|
1500
|
+
if (msg.role !== "toolResult" || msg.toolName !== "rp_bind") {
|
|
1501
|
+
continue;
|
|
1502
|
+
}
|
|
1336
1503
|
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1504
|
+
const parsed = parseBindingEntryData(msg.details);
|
|
1505
|
+
if (parsed) {
|
|
1506
|
+
persistBinding(parsed);
|
|
1340
1507
|
}
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1341
1510
|
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
return;
|
|
1348
|
-
}
|
|
1511
|
+
function parseWindowsRawJson(raw: string): RpCliWindow[] {
|
|
1512
|
+
const trimmed = raw.trim();
|
|
1513
|
+
if (!trimmed) {
|
|
1514
|
+
return [];
|
|
1515
|
+
}
|
|
1349
1516
|
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1517
|
+
let parsed: unknown;
|
|
1518
|
+
try {
|
|
1519
|
+
parsed = JSON.parse(trimmed);
|
|
1520
|
+
} catch {
|
|
1521
|
+
return [];
|
|
1522
|
+
}
|
|
1356
1523
|
|
|
1357
|
-
|
|
1524
|
+
const pickRows = (value: unknown): unknown[] => {
|
|
1525
|
+
if (Array.isArray(value)) {
|
|
1526
|
+
return value;
|
|
1358
1527
|
}
|
|
1359
1528
|
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
ctx.ui.notify(`Could not resolve path: ${pathInput}`, "error");
|
|
1363
|
-
return;
|
|
1529
|
+
if (!value || typeof value !== "object") {
|
|
1530
|
+
return [];
|
|
1364
1531
|
}
|
|
1365
1532
|
|
|
1366
|
-
|
|
1367
|
-
clearReadcacheCaches();
|
|
1533
|
+
const obj = value as Record<string, unknown>;
|
|
1368
1534
|
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1535
|
+
const directArrayKeys = ["windows", "items", "data", "result"];
|
|
1536
|
+
for (const key of directArrayKeys) {
|
|
1537
|
+
const candidate = obj[key];
|
|
1538
|
+
if (Array.isArray(candidate)) {
|
|
1539
|
+
return candidate;
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1375
1542
|
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1543
|
+
const nested = obj.data;
|
|
1544
|
+
if (nested && typeof nested === "object") {
|
|
1545
|
+
const nestedObj = nested as Record<string, unknown>;
|
|
1546
|
+
if (Array.isArray(nestedObj.windows)) {
|
|
1547
|
+
return nestedObj.windows;
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1381
1550
|
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
maybeWarnAstUnavailable(ctx);
|
|
1385
|
-
persistBinding(params.windowId, params.tab);
|
|
1551
|
+
return [];
|
|
1552
|
+
};
|
|
1386
1553
|
|
|
1387
|
-
|
|
1388
|
-
content: [{ type: "text", text: `Bound rp_exec → window ${boundWindowId}, tab "${boundTab}"` }],
|
|
1389
|
-
details: { windowId: boundWindowId, tab: boundTab },
|
|
1390
|
-
};
|
|
1391
|
-
},
|
|
1392
|
-
});
|
|
1554
|
+
const rows = pickRows(parsed);
|
|
1393
1555
|
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1556
|
+
return rows
|
|
1557
|
+
.map((row) => {
|
|
1558
|
+
if (!row || typeof row !== "object") {
|
|
1559
|
+
return null;
|
|
1560
|
+
}
|
|
1399
1561
|
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1562
|
+
const obj = row as Record<string, unknown>;
|
|
1563
|
+
const windowId = typeof obj.windowID === "number"
|
|
1564
|
+
? obj.windowID
|
|
1565
|
+
: (typeof obj.windowId === "number" ? obj.windowId : undefined);
|
|
1404
1566
|
|
|
1405
|
-
|
|
1406
|
-
|
|
1567
|
+
if (windowId === undefined) {
|
|
1568
|
+
return null;
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
const workspaceId = typeof obj.workspaceID === "string"
|
|
1572
|
+
? obj.workspaceID
|
|
1573
|
+
: (typeof obj.workspaceId === "string" ? obj.workspaceId : undefined);
|
|
1574
|
+
|
|
1575
|
+
const workspaceName = typeof obj.workspaceName === "string"
|
|
1576
|
+
? obj.workspaceName
|
|
1577
|
+
: (typeof obj.workspace === "string" ? obj.workspace : undefined);
|
|
1578
|
+
|
|
1579
|
+
const rootFolderPaths = Array.isArray(obj.rootFolderPaths)
|
|
1580
|
+
? obj.rootFolderPaths.filter((root): root is string => typeof root === "string")
|
|
1581
|
+
: (Array.isArray(obj.roots)
|
|
1582
|
+
? obj.roots.filter((root): root is string => typeof root === "string")
|
|
1583
|
+
: undefined);
|
|
1584
|
+
|
|
1585
|
+
return {
|
|
1586
|
+
windowId,
|
|
1587
|
+
workspaceId,
|
|
1588
|
+
workspaceName,
|
|
1589
|
+
rootFolderPaths,
|
|
1590
|
+
} as RpCliWindow;
|
|
1591
|
+
})
|
|
1592
|
+
.filter((window): window is RpCliWindow => window !== null);
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
async function fetchWindowsFromCli(forceRefresh = false): Promise<RpCliWindow[]> {
|
|
1596
|
+
const now = Date.now();
|
|
1597
|
+
|
|
1598
|
+
if (!forceRefresh && windowsCache && (now - windowsCache.fetchedAtMs) < WINDOWS_CACHE_TTL_MS) {
|
|
1599
|
+
return windowsCache.windows;
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
try {
|
|
1603
|
+
const result = await pi.exec("rp-cli", ["--raw-json", "-q", "-e", "windows"], { timeout: 10_000 });
|
|
1604
|
+
|
|
1605
|
+
if ((result.code ?? 0) !== 0) {
|
|
1606
|
+
return windowsCache?.windows ?? [];
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
const stdout = result.stdout ?? "";
|
|
1610
|
+
const stderr = result.stderr ?? "";
|
|
1611
|
+
|
|
1612
|
+
const fromStdout = parseWindowsRawJson(stdout);
|
|
1613
|
+
const parsed = fromStdout.length > 0 ? fromStdout : parseWindowsRawJson(stderr);
|
|
1614
|
+
|
|
1615
|
+
windowsCache = {
|
|
1616
|
+
windows: parsed,
|
|
1617
|
+
fetchedAtMs: now,
|
|
1618
|
+
};
|
|
1619
|
+
|
|
1620
|
+
return parsed;
|
|
1621
|
+
} catch {
|
|
1622
|
+
return windowsCache?.windows ?? [];
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
function windowToBinding(window: RpCliWindow, tab: string): RpCliBindingEntryData {
|
|
1627
|
+
return normalizeBindingEntry({
|
|
1628
|
+
windowId: window.windowId,
|
|
1629
|
+
tab,
|
|
1630
|
+
workspaceId: window.workspaceId,
|
|
1631
|
+
workspaceName: window.workspaceName,
|
|
1632
|
+
workspaceRoots: window.rootFolderPaths,
|
|
1633
|
+
});
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
async function enrichBinding(windowId: number, tab: string): Promise<RpCliBindingEntryData> {
|
|
1637
|
+
const windows = await fetchWindowsFromCli();
|
|
1638
|
+
const match = windows.find((window) => window.windowId === windowId);
|
|
1639
|
+
|
|
1640
|
+
if (!match) {
|
|
1641
|
+
return normalizeBindingEntry({ windowId, tab });
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
return windowToBinding(match, tab);
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
async function ensureBindingTargetsLiveWindow(
|
|
1648
|
+
ctx: ExtensionContext,
|
|
1649
|
+
forceRefresh = false
|
|
1650
|
+
): Promise<RpCliBindingEntryData | null> {
|
|
1651
|
+
const binding = getCurrentBinding();
|
|
1652
|
+
if (!binding) {
|
|
1653
|
+
return null;
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
const windows = await fetchWindowsFromCli(forceRefresh);
|
|
1657
|
+
if (windows.length === 0) {
|
|
1658
|
+
return binding;
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
const sameWindow = windows.find((window) => window.windowId === binding.windowId);
|
|
1662
|
+
if (sameWindow) {
|
|
1663
|
+
const hydrated = windowToBinding(sameWindow, binding.tab);
|
|
1664
|
+
|
|
1665
|
+
if (binding.workspaceId && hydrated.workspaceId && binding.workspaceId !== hydrated.workspaceId) {
|
|
1666
|
+
// Window IDs were recycled to a different workspace. Fall through to workspace-based remap
|
|
1667
|
+
} else {
|
|
1668
|
+
if (!bindingEntriesEqual(binding, hydrated)) {
|
|
1669
|
+
persistBinding(hydrated);
|
|
1670
|
+
return hydrated;
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
setBinding(hydrated);
|
|
1674
|
+
return hydrated;
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
const workspaceCandidates = windows.filter((window) => workspaceIdentityMatches(
|
|
1679
|
+
{
|
|
1680
|
+
workspaceId: binding.workspaceId,
|
|
1681
|
+
workspaceName: binding.workspaceName,
|
|
1682
|
+
workspaceRoots: binding.workspaceRoots,
|
|
1683
|
+
},
|
|
1684
|
+
{
|
|
1685
|
+
workspaceId: window.workspaceId,
|
|
1686
|
+
workspaceName: window.workspaceName,
|
|
1687
|
+
workspaceRoots: window.rootFolderPaths,
|
|
1688
|
+
}
|
|
1689
|
+
));
|
|
1690
|
+
|
|
1691
|
+
if (workspaceCandidates.length === 1) {
|
|
1692
|
+
const rebound = windowToBinding(workspaceCandidates[0], binding.tab);
|
|
1693
|
+
persistBinding(rebound);
|
|
1694
|
+
return rebound;
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
setBinding(null);
|
|
1698
|
+
|
|
1699
|
+
if (ctx.hasUI) {
|
|
1700
|
+
const workspaceLabel = binding.workspaceName ?? binding.workspaceId ?? `window ${binding.windowId}`;
|
|
1701
|
+
|
|
1702
|
+
if (workspaceCandidates.length > 1) {
|
|
1703
|
+
ctx.ui.notify(
|
|
1704
|
+
`repoprompt-cli: binding for ${workspaceLabel} is ambiguous after restart. Re-bind with /rpbind`,
|
|
1705
|
+
"warning"
|
|
1706
|
+
);
|
|
1707
|
+
} else {
|
|
1708
|
+
ctx.ui.notify(
|
|
1709
|
+
`repoprompt-cli: ${workspaceLabel} not found after restart. Re-bind with /rpbind`,
|
|
1710
|
+
"warning"
|
|
1711
|
+
);
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
return null;
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
async function maybeEnsureBindingTargetsLiveWindow(ctx: ExtensionContext): Promise<RpCliBindingEntryData | null> {
|
|
1719
|
+
const binding = getCurrentBinding();
|
|
1720
|
+
if (!binding) {
|
|
1721
|
+
return null;
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
if (!shouldRevalidateBinding()) {
|
|
1725
|
+
return binding;
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
const validated = await ensureBindingTargetsLiveWindow(ctx, true);
|
|
1729
|
+
|
|
1730
|
+
if (validated) {
|
|
1731
|
+
markBindingValidatedNow();
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
return validated;
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
function sameBindingForAutoSelection(
|
|
1738
|
+
binding: RpCliBindingEntryData | null,
|
|
1739
|
+
state: AutoSelectionEntryData | null
|
|
1740
|
+
): boolean {
|
|
1741
|
+
if (!binding || !state) {
|
|
1742
|
+
return false;
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
if (!sameOptionalText(binding.tab, state.tab)) {
|
|
1746
|
+
return false;
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
if (binding.windowId === state.windowId) {
|
|
1750
|
+
return true;
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
return workspaceIdentityMatches(
|
|
1754
|
+
{
|
|
1755
|
+
workspaceId: binding.workspaceId,
|
|
1756
|
+
workspaceName: binding.workspaceName,
|
|
1757
|
+
workspaceRoots: binding.workspaceRoots,
|
|
1758
|
+
},
|
|
1759
|
+
{
|
|
1760
|
+
workspaceId: state.workspaceId,
|
|
1761
|
+
workspaceName: state.workspaceName,
|
|
1762
|
+
workspaceRoots: state.workspaceRoots,
|
|
1763
|
+
}
|
|
1764
|
+
);
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
function makeEmptyAutoSelectionState(binding: RpCliBindingEntryData): AutoSelectionEntryData {
|
|
1768
|
+
return {
|
|
1769
|
+
windowId: binding.windowId,
|
|
1770
|
+
tab: binding.tab,
|
|
1771
|
+
workspaceId: binding.workspaceId,
|
|
1772
|
+
workspaceName: binding.workspaceName,
|
|
1773
|
+
workspaceRoots: normalizeWorkspaceRoots(binding.workspaceRoots),
|
|
1774
|
+
fullPaths: [],
|
|
1775
|
+
slicePaths: [],
|
|
1776
|
+
};
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
function normalizeAutoSelectionRanges(ranges: AutoSelectionEntryRangeData[]): AutoSelectionEntryRangeData[] {
|
|
1780
|
+
const normalized = ranges
|
|
1781
|
+
.map((range) => ({
|
|
1782
|
+
start_line: Number(range.start_line),
|
|
1783
|
+
end_line: Number(range.end_line),
|
|
1784
|
+
}))
|
|
1785
|
+
.filter((range) => Number.isFinite(range.start_line) && Number.isFinite(range.end_line))
|
|
1786
|
+
.filter((range) => range.start_line > 0 && range.end_line >= range.start_line)
|
|
1787
|
+
.sort((a, b) => {
|
|
1788
|
+
if (a.start_line !== b.start_line) {
|
|
1789
|
+
return a.start_line - b.start_line;
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
return a.end_line - b.end_line;
|
|
1793
|
+
});
|
|
1794
|
+
|
|
1795
|
+
const merged: AutoSelectionEntryRangeData[] = [];
|
|
1796
|
+
for (const range of normalized) {
|
|
1797
|
+
const last = merged[merged.length - 1];
|
|
1798
|
+
if (!last) {
|
|
1799
|
+
merged.push(range);
|
|
1800
|
+
continue;
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
if (range.start_line <= last.end_line + 1) {
|
|
1804
|
+
last.end_line = Math.max(last.end_line, range.end_line);
|
|
1805
|
+
continue;
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
merged.push(range);
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
return merged;
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
function normalizeAutoSelectionState(state: AutoSelectionEntryData): AutoSelectionEntryData {
|
|
1815
|
+
const fullPaths = [...new Set(state.fullPaths.map((p) => toPosixPath(String(p).trim())).filter(Boolean))].sort();
|
|
1816
|
+
|
|
1817
|
+
const fullSet = new Set(fullPaths);
|
|
1818
|
+
|
|
1819
|
+
const sliceMap = new Map<string, AutoSelectionEntryRangeData[]>();
|
|
1820
|
+
for (const item of state.slicePaths) {
|
|
1821
|
+
const pathKey = toPosixPath(String(item.path ?? "").trim());
|
|
1822
|
+
if (!pathKey || fullSet.has(pathKey)) {
|
|
1823
|
+
continue;
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
const existing = sliceMap.get(pathKey) ?? [];
|
|
1827
|
+
existing.push(...normalizeAutoSelectionRanges(item.ranges ?? []));
|
|
1828
|
+
sliceMap.set(pathKey, existing);
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
const slicePaths: AutoSelectionEntrySliceData[] = [...sliceMap.entries()]
|
|
1832
|
+
.map(([pathKey, ranges]) => ({
|
|
1833
|
+
path: pathKey,
|
|
1834
|
+
ranges: normalizeAutoSelectionRanges(ranges),
|
|
1835
|
+
}))
|
|
1836
|
+
.filter((item) => item.ranges.length > 0)
|
|
1837
|
+
.sort((a, b) => a.path.localeCompare(b.path));
|
|
1838
|
+
|
|
1839
|
+
return {
|
|
1840
|
+
windowId: state.windowId,
|
|
1841
|
+
tab: state.tab,
|
|
1842
|
+
workspaceId: typeof state.workspaceId === "string" ? state.workspaceId : undefined,
|
|
1843
|
+
workspaceName: typeof state.workspaceName === "string" ? state.workspaceName : undefined,
|
|
1844
|
+
workspaceRoots: normalizeWorkspaceRoots(state.workspaceRoots),
|
|
1845
|
+
fullPaths,
|
|
1846
|
+
slicePaths,
|
|
1847
|
+
};
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
function autoSelectionStatesEqual(a: AutoSelectionEntryData | null, b: AutoSelectionEntryData | null): boolean {
|
|
1851
|
+
if (!a && !b) {
|
|
1852
|
+
return true;
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
if (!a || !b) {
|
|
1856
|
+
return false;
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
const left = normalizeAutoSelectionState(a);
|
|
1860
|
+
const right = normalizeAutoSelectionState(b);
|
|
1861
|
+
|
|
1862
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
function parseAutoSelectionEntryData(
|
|
1866
|
+
value: unknown,
|
|
1867
|
+
binding: RpCliBindingEntryData
|
|
1868
|
+
): AutoSelectionEntryData | null {
|
|
1869
|
+
if (!value || typeof value !== "object") {
|
|
1870
|
+
return null;
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
const obj = value as Record<string, unknown>;
|
|
1874
|
+
|
|
1875
|
+
const windowId = typeof obj.windowId === "number" ? obj.windowId : undefined;
|
|
1876
|
+
const tab = typeof obj.tab === "string" ? obj.tab : undefined;
|
|
1877
|
+
|
|
1878
|
+
const workspaceId = typeof obj.workspaceId === "string"
|
|
1879
|
+
? obj.workspaceId
|
|
1880
|
+
: (typeof obj.workspaceID === "string" ? obj.workspaceID : undefined);
|
|
1881
|
+
|
|
1882
|
+
const workspaceName = typeof obj.workspaceName === "string"
|
|
1883
|
+
? obj.workspaceName
|
|
1884
|
+
: (typeof obj.workspace === "string" ? obj.workspace : undefined);
|
|
1885
|
+
|
|
1886
|
+
const workspaceRoots = Array.isArray(obj.workspaceRoots)
|
|
1887
|
+
? obj.workspaceRoots.filter((root): root is string => typeof root === "string")
|
|
1888
|
+
: (Array.isArray(obj.rootFolderPaths)
|
|
1889
|
+
? obj.rootFolderPaths.filter((root): root is string => typeof root === "string")
|
|
1890
|
+
: undefined);
|
|
1891
|
+
|
|
1892
|
+
const tabMatches = sameOptionalText(tab, binding.tab);
|
|
1893
|
+
const windowMatches = windowId === binding.windowId;
|
|
1894
|
+
const workspaceMatches = workspaceIdentityMatches(
|
|
1895
|
+
{
|
|
1896
|
+
workspaceId: binding.workspaceId,
|
|
1897
|
+
workspaceName: binding.workspaceName,
|
|
1898
|
+
workspaceRoots: binding.workspaceRoots,
|
|
1899
|
+
},
|
|
1900
|
+
{
|
|
1901
|
+
workspaceId,
|
|
1902
|
+
workspaceName,
|
|
1903
|
+
workspaceRoots,
|
|
1904
|
+
}
|
|
1905
|
+
);
|
|
1906
|
+
|
|
1907
|
+
if (!tabMatches || (!windowMatches && !workspaceMatches)) {
|
|
1908
|
+
return null;
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
const fullPaths = Array.isArray(obj.fullPaths)
|
|
1912
|
+
? obj.fullPaths.filter((item): item is string => typeof item === "string")
|
|
1913
|
+
: [];
|
|
1914
|
+
|
|
1915
|
+
const slicePathsRaw = Array.isArray(obj.slicePaths) ? obj.slicePaths : [];
|
|
1916
|
+
const slicePaths: AutoSelectionEntrySliceData[] = slicePathsRaw
|
|
1917
|
+
.map((raw) => {
|
|
1918
|
+
if (!raw || typeof raw !== "object") {
|
|
1919
|
+
return null;
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
const row = raw as Record<string, unknown>;
|
|
1923
|
+
const pathValue = typeof row.path === "string" ? row.path : null;
|
|
1924
|
+
const rangesRaw = Array.isArray(row.ranges) ? row.ranges : [];
|
|
1925
|
+
|
|
1926
|
+
if (!pathValue) {
|
|
1927
|
+
return null;
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
const ranges: AutoSelectionEntryRangeData[] = rangesRaw
|
|
1931
|
+
.map((rangeRaw) => {
|
|
1932
|
+
if (!rangeRaw || typeof rangeRaw !== "object") {
|
|
1933
|
+
return null;
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
const rangeObj = rangeRaw as Record<string, unknown>;
|
|
1937
|
+
const start = typeof rangeObj.start_line === "number" ? rangeObj.start_line : NaN;
|
|
1938
|
+
const end = typeof rangeObj.end_line === "number" ? rangeObj.end_line : NaN;
|
|
1939
|
+
|
|
1940
|
+
if (!Number.isFinite(start) || !Number.isFinite(end)) {
|
|
1941
|
+
return null;
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
return {
|
|
1945
|
+
start_line: start,
|
|
1946
|
+
end_line: end,
|
|
1947
|
+
};
|
|
1948
|
+
})
|
|
1949
|
+
.filter((range): range is AutoSelectionEntryRangeData => range !== null);
|
|
1950
|
+
|
|
1951
|
+
return {
|
|
1952
|
+
path: pathValue,
|
|
1953
|
+
ranges,
|
|
1954
|
+
};
|
|
1955
|
+
})
|
|
1956
|
+
.filter((item): item is AutoSelectionEntrySliceData => item !== null);
|
|
1957
|
+
|
|
1958
|
+
return normalizeAutoSelectionState({
|
|
1959
|
+
windowId: binding.windowId,
|
|
1960
|
+
tab: binding.tab,
|
|
1961
|
+
workspaceId: binding.workspaceId ?? workspaceId,
|
|
1962
|
+
workspaceName: binding.workspaceName ?? workspaceName,
|
|
1963
|
+
workspaceRoots: binding.workspaceRoots ?? workspaceRoots,
|
|
1964
|
+
fullPaths,
|
|
1965
|
+
slicePaths,
|
|
1966
|
+
});
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
function getAutoSelectionStateFromBranch(
|
|
1970
|
+
ctx: ExtensionContext,
|
|
1971
|
+
binding: RpCliBindingEntryData
|
|
1972
|
+
): AutoSelectionEntryData {
|
|
1973
|
+
const entries = ctx.sessionManager.getBranch();
|
|
1974
|
+
|
|
1975
|
+
for (let i = entries.length - 1; i >= 0; i -= 1) {
|
|
1976
|
+
const entry = entries[i];
|
|
1977
|
+
if (entry.type !== "custom" || entry.customType !== AUTO_SELECTION_CUSTOM_TYPE) {
|
|
1978
|
+
continue;
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
const parsed = parseAutoSelectionEntryData(entry.data, binding);
|
|
1982
|
+
if (parsed) {
|
|
1983
|
+
return parsed;
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
return makeEmptyAutoSelectionState(binding);
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1990
|
+
function persistAutoSelectionState(state: AutoSelectionEntryData): void {
|
|
1991
|
+
const normalized = normalizeAutoSelectionState(state);
|
|
1992
|
+
activeAutoSelectionState = normalized;
|
|
1993
|
+
pi.appendEntry(AUTO_SELECTION_CUSTOM_TYPE, normalized);
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
function autoSelectionManagedPaths(state: AutoSelectionEntryData): string[] {
|
|
1997
|
+
const fromSlices = state.slicePaths.map((item) => item.path);
|
|
1998
|
+
return [...new Set([...state.fullPaths, ...fromSlices])];
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
function autoSelectionSliceKey(item: AutoSelectionEntrySliceData): string {
|
|
2002
|
+
return JSON.stringify(normalizeAutoSelectionRanges(item.ranges));
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
function bindingForAutoSelectionState(state: AutoSelectionEntryData): RpCliBindingEntryData {
|
|
2006
|
+
return {
|
|
2007
|
+
windowId: state.windowId,
|
|
2008
|
+
tab: state.tab ?? "Compose",
|
|
2009
|
+
workspaceId: state.workspaceId,
|
|
2010
|
+
workspaceName: state.workspaceName,
|
|
2011
|
+
workspaceRoots: state.workspaceRoots,
|
|
2012
|
+
};
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
async function runRpCliForBinding(
|
|
2016
|
+
binding: RpCliBindingEntryData,
|
|
2017
|
+
cmd: string,
|
|
2018
|
+
rawJson = false,
|
|
2019
|
+
timeout = 10_000
|
|
2020
|
+
): Promise<{ stdout: string; stderr: string; exitCode: number; output: string }> {
|
|
2021
|
+
const args: string[] = ["-w", String(binding.windowId)];
|
|
2022
|
+
|
|
2023
|
+
if (binding.tab) {
|
|
2024
|
+
args.push("-t", binding.tab);
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
args.push("-q", "--fail-fast");
|
|
2028
|
+
|
|
2029
|
+
if (rawJson) {
|
|
2030
|
+
args.push("--raw-json");
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
args.push("-e", cmd);
|
|
2034
|
+
|
|
2035
|
+
const result = await pi.exec("rp-cli", args, { timeout });
|
|
2036
|
+
|
|
2037
|
+
const stdout = result.stdout ?? "";
|
|
2038
|
+
const stderr = result.stderr ?? "";
|
|
2039
|
+
const exitCode = result.code ?? 0;
|
|
2040
|
+
const output = [stdout, stderr].filter(Boolean).join("\n").trim();
|
|
2041
|
+
|
|
2042
|
+
if (exitCode !== 0) {
|
|
2043
|
+
throw new Error(output || `rp-cli exited with status ${exitCode}`);
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
return { stdout, stderr, exitCode, output };
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
async function callManageSelection(binding: RpCliBindingEntryData, args: Record<string, unknown>): Promise<string> {
|
|
2050
|
+
const cmd = `call manage_selection ${JSON.stringify(args)}`;
|
|
2051
|
+
const result = await runRpCliForBinding(binding, cmd, false);
|
|
2052
|
+
return result.output;
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
async function getSelectionFilesText(binding: RpCliBindingEntryData): Promise<string | null> {
|
|
2056
|
+
try {
|
|
2057
|
+
const text = await callManageSelection(binding, {
|
|
2058
|
+
op: "get",
|
|
2059
|
+
view: "files",
|
|
2060
|
+
});
|
|
2061
|
+
|
|
2062
|
+
return text.length > 0 ? text : null;
|
|
2063
|
+
} catch {
|
|
2064
|
+
return null;
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
function buildSelectionPathFromResolved(
|
|
2069
|
+
inputPath: string,
|
|
2070
|
+
resolved: { absolutePath: string | null; repoRoot: string | null }
|
|
2071
|
+
): string {
|
|
2072
|
+
if (!resolved.absolutePath || !resolved.repoRoot) {
|
|
2073
|
+
return inputPath;
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
const rel = path.relative(resolved.repoRoot, resolved.absolutePath);
|
|
2077
|
+
if (!rel || rel.startsWith("..") || path.isAbsolute(rel)) {
|
|
2078
|
+
return inputPath;
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
const rootHint = path.basename(resolved.repoRoot);
|
|
2082
|
+
const relPosix = rel.split(path.sep).join("/");
|
|
2083
|
+
|
|
2084
|
+
return `${rootHint}/${relPosix}`;
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
function updateAutoSelectionStateAfterFullRead(binding: RpCliBindingEntryData, selectionPath: string): void {
|
|
2088
|
+
const normalizedPath = toPosixPath(selectionPath);
|
|
2089
|
+
|
|
2090
|
+
const baseState = sameBindingForAutoSelection(binding, activeAutoSelectionState)
|
|
2091
|
+
? (activeAutoSelectionState as AutoSelectionEntryData)
|
|
2092
|
+
: makeEmptyAutoSelectionState(binding);
|
|
2093
|
+
|
|
2094
|
+
const nextState: AutoSelectionEntryData = {
|
|
2095
|
+
...baseState,
|
|
2096
|
+
fullPaths: [...baseState.fullPaths, normalizedPath],
|
|
2097
|
+
slicePaths: baseState.slicePaths.filter((entry) => entry.path !== normalizedPath),
|
|
2098
|
+
};
|
|
2099
|
+
|
|
2100
|
+
const normalizedNext = normalizeAutoSelectionState(nextState);
|
|
2101
|
+
if (autoSelectionStatesEqual(baseState, normalizedNext)) {
|
|
2102
|
+
activeAutoSelectionState = normalizedNext;
|
|
2103
|
+
return;
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2106
|
+
persistAutoSelectionState(normalizedNext);
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
function existingSliceRangesForRead(
|
|
2110
|
+
binding: RpCliBindingEntryData,
|
|
2111
|
+
selectionPath: string
|
|
2112
|
+
): AutoSelectionEntryRangeData[] | null {
|
|
2113
|
+
const normalizedPath = toPosixPath(selectionPath);
|
|
2114
|
+
|
|
2115
|
+
const baseState = sameBindingForAutoSelection(binding, activeAutoSelectionState)
|
|
2116
|
+
? (activeAutoSelectionState as AutoSelectionEntryData)
|
|
2117
|
+
: makeEmptyAutoSelectionState(binding);
|
|
2118
|
+
|
|
2119
|
+
if (baseState.fullPaths.includes(normalizedPath)) {
|
|
2120
|
+
return null;
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2123
|
+
const existing = baseState.slicePaths.find((entry) => entry.path === normalizedPath);
|
|
2124
|
+
return normalizeAutoSelectionRanges(existing?.ranges ?? []);
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
function subtractCoveredRanges(
|
|
2128
|
+
incomingRange: AutoSelectionEntryRangeData,
|
|
2129
|
+
existingRanges: AutoSelectionEntryRangeData[]
|
|
2130
|
+
): AutoSelectionEntryRangeData[] {
|
|
2131
|
+
let pending: AutoSelectionEntryRangeData[] = [incomingRange];
|
|
2132
|
+
|
|
2133
|
+
for (const existing of normalizeAutoSelectionRanges(existingRanges)) {
|
|
2134
|
+
const nextPending: AutoSelectionEntryRangeData[] = [];
|
|
2135
|
+
|
|
2136
|
+
for (const candidate of pending) {
|
|
2137
|
+
const overlapStart = Math.max(candidate.start_line, existing.start_line);
|
|
2138
|
+
const overlapEnd = Math.min(candidate.end_line, existing.end_line);
|
|
2139
|
+
|
|
2140
|
+
if (overlapStart > overlapEnd) {
|
|
2141
|
+
nextPending.push(candidate);
|
|
2142
|
+
continue;
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
if (candidate.start_line < overlapStart) {
|
|
2146
|
+
nextPending.push({
|
|
2147
|
+
start_line: candidate.start_line,
|
|
2148
|
+
end_line: overlapStart - 1,
|
|
2149
|
+
});
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
if (candidate.end_line > overlapEnd) {
|
|
2153
|
+
nextPending.push({
|
|
2154
|
+
start_line: overlapEnd + 1,
|
|
2155
|
+
end_line: candidate.end_line,
|
|
2156
|
+
});
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
|
|
2160
|
+
pending = nextPending;
|
|
2161
|
+
if (pending.length === 0) {
|
|
2162
|
+
return [];
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
return normalizeAutoSelectionRanges(pending);
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
function updateAutoSelectionStateAfterSliceRead(
|
|
2170
|
+
binding: RpCliBindingEntryData,
|
|
2171
|
+
selectionPath: string,
|
|
2172
|
+
range: AutoSelectionEntryRangeData
|
|
2173
|
+
): void {
|
|
2174
|
+
const normalizedPath = toPosixPath(selectionPath);
|
|
2175
|
+
|
|
2176
|
+
const baseState = sameBindingForAutoSelection(binding, activeAutoSelectionState)
|
|
2177
|
+
? (activeAutoSelectionState as AutoSelectionEntryData)
|
|
2178
|
+
: makeEmptyAutoSelectionState(binding);
|
|
2179
|
+
|
|
2180
|
+
if (baseState.fullPaths.includes(normalizedPath)) {
|
|
2181
|
+
return;
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
const existing = baseState.slicePaths.find((entry) => entry.path === normalizedPath);
|
|
2185
|
+
|
|
2186
|
+
const nextSlicePaths = baseState.slicePaths.filter((entry) => entry.path !== normalizedPath);
|
|
2187
|
+
nextSlicePaths.push({
|
|
2188
|
+
path: normalizedPath,
|
|
2189
|
+
ranges: [...(existing?.ranges ?? []), range],
|
|
2190
|
+
});
|
|
2191
|
+
|
|
2192
|
+
const nextState: AutoSelectionEntryData = {
|
|
2193
|
+
...baseState,
|
|
2194
|
+
fullPaths: [...baseState.fullPaths],
|
|
2195
|
+
slicePaths: nextSlicePaths,
|
|
2196
|
+
};
|
|
2197
|
+
|
|
2198
|
+
const normalizedNext = normalizeAutoSelectionState(nextState);
|
|
2199
|
+
if (autoSelectionStatesEqual(baseState, normalizedNext)) {
|
|
2200
|
+
activeAutoSelectionState = normalizedNext;
|
|
2201
|
+
return;
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
persistAutoSelectionState(normalizedNext);
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
async function removeAutoSelectionPaths(state: AutoSelectionEntryData, paths: string[]): Promise<void> {
|
|
2208
|
+
if (paths.length === 0) {
|
|
2209
|
+
return;
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
const binding = bindingForAutoSelectionState(state);
|
|
2213
|
+
await callManageSelection(binding, { op: "remove", paths });
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2216
|
+
async function addAutoSelectionFullPaths(state: AutoSelectionEntryData, paths: string[]): Promise<void> {
|
|
2217
|
+
if (paths.length === 0) {
|
|
2218
|
+
return;
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
const binding = bindingForAutoSelectionState(state);
|
|
2222
|
+
await callManageSelection(binding, {
|
|
2223
|
+
op: "add",
|
|
2224
|
+
mode: "full",
|
|
2225
|
+
paths,
|
|
2226
|
+
});
|
|
2227
|
+
}
|
|
2228
|
+
|
|
2229
|
+
async function addAutoSelectionSlices(
|
|
2230
|
+
state: AutoSelectionEntryData,
|
|
2231
|
+
slices: AutoSelectionEntrySliceData[]
|
|
2232
|
+
): Promise<void> {
|
|
2233
|
+
if (slices.length === 0) {
|
|
2234
|
+
return;
|
|
2235
|
+
}
|
|
2236
|
+
|
|
2237
|
+
const binding = bindingForAutoSelectionState(state);
|
|
2238
|
+
await callManageSelection(binding, {
|
|
2239
|
+
op: "add",
|
|
2240
|
+
slices,
|
|
2241
|
+
});
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
async function reconcileAutoSelectionWithinBinding(
|
|
2245
|
+
currentState: AutoSelectionEntryData,
|
|
2246
|
+
desiredState: AutoSelectionEntryData
|
|
2247
|
+
): Promise<void> {
|
|
2248
|
+
const currentModeByPath = new Map<string, "full" | "slices">();
|
|
2249
|
+
for (const p of currentState.fullPaths) {
|
|
2250
|
+
currentModeByPath.set(p, "full");
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2253
|
+
for (const s of currentState.slicePaths) {
|
|
2254
|
+
if (!currentModeByPath.has(s.path)) {
|
|
2255
|
+
currentModeByPath.set(s.path, "slices");
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2259
|
+
const desiredModeByPath = new Map<string, "full" | "slices">();
|
|
2260
|
+
for (const p of desiredState.fullPaths) {
|
|
2261
|
+
desiredModeByPath.set(p, "full");
|
|
2262
|
+
}
|
|
2263
|
+
|
|
2264
|
+
for (const s of desiredState.slicePaths) {
|
|
2265
|
+
if (!desiredModeByPath.has(s.path)) {
|
|
2266
|
+
desiredModeByPath.set(s.path, "slices");
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2270
|
+
const desiredSliceByPath = new Map<string, AutoSelectionEntrySliceData>();
|
|
2271
|
+
for (const s of desiredState.slicePaths) {
|
|
2272
|
+
desiredSliceByPath.set(s.path, s);
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
const currentSliceByPath = new Map<string, AutoSelectionEntrySliceData>();
|
|
2276
|
+
for (const s of currentState.slicePaths) {
|
|
2277
|
+
currentSliceByPath.set(s.path, s);
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
const removePaths = new Set<string>();
|
|
2281
|
+
const addFullPaths: string[] = [];
|
|
2282
|
+
const addSlices: AutoSelectionEntrySliceData[] = [];
|
|
2283
|
+
|
|
2284
|
+
for (const [pathKey] of currentModeByPath) {
|
|
2285
|
+
if (!desiredModeByPath.has(pathKey)) {
|
|
2286
|
+
removePaths.add(pathKey);
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
|
|
2290
|
+
for (const [pathKey, mode] of desiredModeByPath) {
|
|
2291
|
+
const currentMode = currentModeByPath.get(pathKey);
|
|
2292
|
+
|
|
2293
|
+
if (mode === "full") {
|
|
2294
|
+
if (currentMode === "full") {
|
|
2295
|
+
continue;
|
|
2296
|
+
}
|
|
2297
|
+
|
|
2298
|
+
if (currentMode === "slices") {
|
|
2299
|
+
removePaths.add(pathKey);
|
|
2300
|
+
}
|
|
2301
|
+
|
|
2302
|
+
addFullPaths.push(pathKey);
|
|
2303
|
+
continue;
|
|
2304
|
+
}
|
|
2305
|
+
|
|
2306
|
+
const desiredSlice = desiredSliceByPath.get(pathKey);
|
|
2307
|
+
if (!desiredSlice) {
|
|
2308
|
+
continue;
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2311
|
+
if (currentMode === "full") {
|
|
2312
|
+
removePaths.add(pathKey);
|
|
2313
|
+
addSlices.push(desiredSlice);
|
|
2314
|
+
continue;
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2317
|
+
if (currentMode === "slices") {
|
|
2318
|
+
const currentSlice = currentSliceByPath.get(pathKey);
|
|
2319
|
+
if (currentSlice && autoSelectionSliceKey(currentSlice) === autoSelectionSliceKey(desiredSlice)) {
|
|
2320
|
+
continue;
|
|
2321
|
+
}
|
|
2322
|
+
|
|
2323
|
+
removePaths.add(pathKey);
|
|
2324
|
+
addSlices.push(desiredSlice);
|
|
2325
|
+
continue;
|
|
2326
|
+
}
|
|
2327
|
+
|
|
2328
|
+
addSlices.push(desiredSlice);
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2331
|
+
await removeAutoSelectionPaths(currentState, [...removePaths]);
|
|
2332
|
+
await addAutoSelectionFullPaths(desiredState, addFullPaths);
|
|
2333
|
+
await addAutoSelectionSlices(desiredState, addSlices);
|
|
2334
|
+
}
|
|
2335
|
+
|
|
2336
|
+
async function reconcileAutoSelectionStates(
|
|
2337
|
+
currentState: AutoSelectionEntryData | null,
|
|
2338
|
+
desiredState: AutoSelectionEntryData | null
|
|
2339
|
+
): Promise<void> {
|
|
2340
|
+
if (autoSelectionStatesEqual(currentState, desiredState)) {
|
|
2341
|
+
return;
|
|
2342
|
+
}
|
|
2343
|
+
|
|
2344
|
+
if (currentState && desiredState) {
|
|
2345
|
+
const sameBinding =
|
|
2346
|
+
currentState.windowId === desiredState.windowId &&
|
|
2347
|
+
sameOptionalText(currentState.tab, desiredState.tab);
|
|
2348
|
+
|
|
2349
|
+
if (sameBinding) {
|
|
2350
|
+
await reconcileAutoSelectionWithinBinding(currentState, desiredState);
|
|
2351
|
+
return;
|
|
2352
|
+
}
|
|
2353
|
+
|
|
2354
|
+
try {
|
|
2355
|
+
await removeAutoSelectionPaths(currentState, autoSelectionManagedPaths(currentState));
|
|
2356
|
+
} catch {
|
|
2357
|
+
// Old binding/window may no longer exist after RepoPrompt app restart
|
|
2358
|
+
}
|
|
2359
|
+
|
|
2360
|
+
await addAutoSelectionFullPaths(desiredState, desiredState.fullPaths);
|
|
2361
|
+
await addAutoSelectionSlices(desiredState, desiredState.slicePaths);
|
|
2362
|
+
return;
|
|
2363
|
+
}
|
|
2364
|
+
|
|
2365
|
+
if (currentState && !desiredState) {
|
|
2366
|
+
try {
|
|
2367
|
+
await removeAutoSelectionPaths(currentState, autoSelectionManagedPaths(currentState));
|
|
2368
|
+
} catch {
|
|
2369
|
+
// Old binding/window may no longer exist after RepoPrompt app restart
|
|
2370
|
+
}
|
|
2371
|
+
return;
|
|
2372
|
+
}
|
|
2373
|
+
|
|
2374
|
+
if (!currentState && desiredState) {
|
|
2375
|
+
await addAutoSelectionFullPaths(desiredState, desiredState.fullPaths);
|
|
2376
|
+
await addAutoSelectionSlices(desiredState, desiredState.slicePaths);
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
|
|
2380
|
+
async function syncAutoSelectionToCurrentBranch(ctx: ExtensionContext): Promise<void> {
|
|
2381
|
+
if (config.autoSelectReadSlices !== true) {
|
|
2382
|
+
activeAutoSelectionState = null;
|
|
2383
|
+
return;
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2386
|
+
const binding = await ensureBindingTargetsLiveWindow(ctx);
|
|
2387
|
+
const desiredState = binding ? getAutoSelectionStateFromBranch(ctx, binding) : null;
|
|
2388
|
+
|
|
2389
|
+
try {
|
|
2390
|
+
await reconcileAutoSelectionStates(activeAutoSelectionState, desiredState);
|
|
2391
|
+
} catch {
|
|
2392
|
+
// Fail-open
|
|
2393
|
+
}
|
|
2394
|
+
|
|
2395
|
+
activeAutoSelectionState = desiredState;
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2398
|
+
function parseReadOutputHints(readOutputText: string | undefined): {
|
|
2399
|
+
selectionPath: string | null;
|
|
2400
|
+
totalLines: number | null;
|
|
2401
|
+
} {
|
|
2402
|
+
if (!readOutputText) {
|
|
2403
|
+
return { selectionPath: null, totalLines: null };
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2406
|
+
const pathMatch =
|
|
2407
|
+
/\*\*Path\*\*:\s*`([^`]+)`/i.exec(readOutputText) ??
|
|
2408
|
+
/\*\*Path\*\*:\s*([^\n]+)$/im.exec(readOutputText);
|
|
2409
|
+
|
|
2410
|
+
const selectionPath = pathMatch?.[1]?.trim() ?? null;
|
|
2411
|
+
|
|
2412
|
+
const linesRegexes = [
|
|
2413
|
+
/\*\*Lines\*\*:\s*(\d+)\s*[–—-]\s*(\d+)\s+of\s+(\d+)/i,
|
|
2414
|
+
/Lines(?:\s*:)?\s*(\d+)\s*[–—-]\s*(\d+)\s+of\s+(\d+)/i,
|
|
2415
|
+
];
|
|
2416
|
+
|
|
2417
|
+
let totalLines: number | null = null;
|
|
2418
|
+
|
|
2419
|
+
for (const rx of linesRegexes) {
|
|
2420
|
+
const match = rx.exec(readOutputText);
|
|
2421
|
+
if (!match) {
|
|
2422
|
+
continue;
|
|
2423
|
+
}
|
|
2424
|
+
|
|
2425
|
+
const parsed = Number.parseInt(match[3] ?? "", 10);
|
|
2426
|
+
if (Number.isFinite(parsed)) {
|
|
2427
|
+
totalLines = parsed;
|
|
2428
|
+
break;
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
|
|
2432
|
+
return { selectionPath, totalLines };
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
async function autoSelectReadFileInRepoPromptSelection(
|
|
2436
|
+
ctx: ExtensionContext,
|
|
2437
|
+
inputPath: string,
|
|
2438
|
+
startLine: number | undefined,
|
|
2439
|
+
limit: number | undefined,
|
|
2440
|
+
readOutputText: string | undefined
|
|
2441
|
+
): Promise<void> {
|
|
2442
|
+
if (config.autoSelectReadSlices !== true) {
|
|
2443
|
+
return;
|
|
2444
|
+
}
|
|
2445
|
+
|
|
2446
|
+
const outputHints = parseReadOutputHints(readOutputText);
|
|
2447
|
+
|
|
2448
|
+
const binding = await maybeEnsureBindingTargetsLiveWindow(ctx);
|
|
2449
|
+
if (!binding) {
|
|
2450
|
+
return;
|
|
2451
|
+
}
|
|
2452
|
+
|
|
2453
|
+
const selectionText = await getSelectionFilesText(binding);
|
|
2454
|
+
if (selectionText === null) {
|
|
2455
|
+
return;
|
|
2456
|
+
}
|
|
2457
|
+
|
|
2458
|
+
const resolved = await resolveReadFilePath(pi, inputPath, ctx.cwd, binding.windowId, binding.tab);
|
|
2459
|
+
|
|
2460
|
+
const resolvedSelectionPath = buildSelectionPathFromResolved(inputPath, resolved);
|
|
2461
|
+
const selectionPath =
|
|
2462
|
+
outputHints.selectionPath && outputHints.selectionPath.trim().length > 0
|
|
2463
|
+
? outputHints.selectionPath
|
|
2464
|
+
: resolvedSelectionPath;
|
|
2465
|
+
|
|
2466
|
+
const candidatePaths = new Set<string>();
|
|
2467
|
+
candidatePaths.add(toPosixPath(selectionPath));
|
|
2468
|
+
candidatePaths.add(toPosixPath(resolvedSelectionPath));
|
|
2469
|
+
candidatePaths.add(toPosixPath(inputPath));
|
|
2470
|
+
|
|
2471
|
+
if (outputHints.selectionPath) {
|
|
2472
|
+
candidatePaths.add(toPosixPath(outputHints.selectionPath));
|
|
2473
|
+
}
|
|
2474
|
+
|
|
2475
|
+
if (resolved.absolutePath) {
|
|
2476
|
+
candidatePaths.add(toPosixPath(resolved.absolutePath));
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2479
|
+
if (resolved.absolutePath && resolved.repoRoot) {
|
|
2480
|
+
const rel = path.relative(resolved.repoRoot, resolved.absolutePath);
|
|
2481
|
+
if (rel && !rel.startsWith("..") && !path.isAbsolute(rel)) {
|
|
2482
|
+
candidatePaths.add(toPosixPath(rel.split(path.sep).join("/")));
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2486
|
+
let selectionStatus: ReturnType<typeof inferSelectionStatus> = null;
|
|
2487
|
+
|
|
2488
|
+
for (const candidate of candidatePaths) {
|
|
2489
|
+
const status = inferSelectionStatus(selectionText, candidate);
|
|
2490
|
+
if (!status) {
|
|
2491
|
+
continue;
|
|
2492
|
+
}
|
|
2493
|
+
|
|
2494
|
+
if (status.mode === "full") {
|
|
2495
|
+
selectionStatus = status;
|
|
2496
|
+
break;
|
|
2497
|
+
}
|
|
2498
|
+
|
|
2499
|
+
if (status.mode === "codemap_only" && status.codemapManual === true) {
|
|
2500
|
+
selectionStatus = status;
|
|
2501
|
+
break;
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2504
|
+
if (selectionStatus === null) {
|
|
2505
|
+
selectionStatus = status;
|
|
2506
|
+
continue;
|
|
2507
|
+
}
|
|
2508
|
+
|
|
2509
|
+
if (selectionStatus.mode === "codemap_only" && status.mode === "slices") {
|
|
2510
|
+
selectionStatus = status;
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2514
|
+
if (selectionStatus?.mode === "full") {
|
|
2515
|
+
return;
|
|
2516
|
+
}
|
|
2517
|
+
|
|
2518
|
+
if (selectionStatus?.mode === "codemap_only" && selectionStatus.codemapManual === true) {
|
|
2519
|
+
return;
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2522
|
+
let totalLines: number | undefined;
|
|
2523
|
+
|
|
2524
|
+
if (typeof startLine === "number" && startLine < 0) {
|
|
2525
|
+
if (resolved.absolutePath) {
|
|
2526
|
+
try {
|
|
2527
|
+
totalLines = await countFileLines(resolved.absolutePath);
|
|
2528
|
+
} catch {
|
|
2529
|
+
totalLines = undefined;
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2532
|
+
|
|
2533
|
+
if (totalLines === undefined && typeof outputHints.totalLines === "number") {
|
|
2534
|
+
totalLines = outputHints.totalLines;
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
|
|
2538
|
+
const sliceRange = computeSliceRangeFromReadArgs(startLine, limit, totalLines);
|
|
2539
|
+
|
|
2540
|
+
|
|
2541
|
+
if (sliceRange) {
|
|
2542
|
+
const existingRanges = existingSliceRangesForRead(binding, selectionPath);
|
|
2543
|
+
if (!existingRanges) {
|
|
2544
|
+
return;
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2547
|
+
const uncoveredRanges = subtractCoveredRanges(sliceRange, existingRanges);
|
|
2548
|
+
|
|
2549
|
+
if (uncoveredRanges.length === 0) {
|
|
2550
|
+
updateAutoSelectionStateAfterSliceRead(binding, selectionPath, sliceRange);
|
|
2551
|
+
return;
|
|
2552
|
+
}
|
|
2553
|
+
|
|
2554
|
+
// Add only uncovered ranges to avoid touching unrelated selection state
|
|
2555
|
+
// (and to avoid global set semantics)
|
|
2556
|
+
const payload = {
|
|
2557
|
+
op: "add",
|
|
2558
|
+
slices: [
|
|
2559
|
+
{
|
|
2560
|
+
path: toPosixPath(selectionPath),
|
|
2561
|
+
ranges: uncoveredRanges,
|
|
2562
|
+
},
|
|
2563
|
+
],
|
|
2564
|
+
};
|
|
2565
|
+
|
|
2566
|
+
try {
|
|
2567
|
+
await callManageSelection(binding, payload);
|
|
2568
|
+
} catch {
|
|
2569
|
+
// Fail-open
|
|
2570
|
+
return;
|
|
2571
|
+
}
|
|
2572
|
+
|
|
2573
|
+
updateAutoSelectionStateAfterSliceRead(binding, selectionPath, sliceRange);
|
|
2574
|
+
return;
|
|
2575
|
+
}
|
|
2576
|
+
|
|
2577
|
+
const payload = {
|
|
2578
|
+
op: "add",
|
|
2579
|
+
mode: "full",
|
|
2580
|
+
paths: [toPosixPath(selectionPath)],
|
|
2581
|
+
};
|
|
2582
|
+
|
|
2583
|
+
try {
|
|
2584
|
+
await callManageSelection(binding, payload);
|
|
2585
|
+
} catch {
|
|
2586
|
+
// Fail-open
|
|
2587
|
+
return;
|
|
2588
|
+
}
|
|
2589
|
+
|
|
2590
|
+
updateAutoSelectionStateAfterFullRead(binding, selectionPath);
|
|
2591
|
+
}
|
|
2592
|
+
|
|
2593
|
+
|
|
2594
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
2595
|
+
config = loadConfig();
|
|
2596
|
+
clearReadcacheCaches();
|
|
2597
|
+
clearWindowsCache();
|
|
2598
|
+
markBindingValidationStale();
|
|
2599
|
+
reconstructBinding(ctx);
|
|
2600
|
+
|
|
2601
|
+
const binding = getCurrentBinding();
|
|
2602
|
+
activeAutoSelectionState =
|
|
2603
|
+
config.autoSelectReadSlices === true && binding
|
|
2604
|
+
? getAutoSelectionStateFromBranch(ctx, binding)
|
|
2605
|
+
: null;
|
|
2606
|
+
|
|
2607
|
+
if (config.readcacheReadFile === true) {
|
|
2608
|
+
void pruneObjectsOlderThan(ctx.cwd).catch(() => {
|
|
2609
|
+
// Fail-open
|
|
2610
|
+
});
|
|
2611
|
+
}
|
|
2612
|
+
|
|
2613
|
+
try {
|
|
2614
|
+
await syncAutoSelectionToCurrentBranch(ctx);
|
|
2615
|
+
} catch {
|
|
2616
|
+
// Fail-open
|
|
2617
|
+
}
|
|
2618
|
+
});
|
|
2619
|
+
|
|
2620
|
+
pi.on("session_switch", async (_event, ctx) => {
|
|
2621
|
+
config = loadConfig();
|
|
2622
|
+
clearReadcacheCaches();
|
|
2623
|
+
clearWindowsCache();
|
|
2624
|
+
markBindingValidationStale();
|
|
2625
|
+
reconstructBinding(ctx);
|
|
2626
|
+
await syncAutoSelectionToCurrentBranch(ctx);
|
|
2627
|
+
});
|
|
2628
|
+
|
|
2629
|
+
// session_fork is the current event name; keep session_branch for backwards compatibility
|
|
2630
|
+
pi.on("session_fork", async (_event, ctx) => {
|
|
2631
|
+
config = loadConfig();
|
|
2632
|
+
clearReadcacheCaches();
|
|
2633
|
+
clearWindowsCache();
|
|
2634
|
+
markBindingValidationStale();
|
|
2635
|
+
reconstructBinding(ctx);
|
|
2636
|
+
await syncAutoSelectionToCurrentBranch(ctx);
|
|
2637
|
+
});
|
|
2638
|
+
|
|
2639
|
+
pi.on("session_branch", async (_event, ctx) => {
|
|
2640
|
+
config = loadConfig();
|
|
2641
|
+
clearReadcacheCaches();
|
|
2642
|
+
clearWindowsCache();
|
|
2643
|
+
markBindingValidationStale();
|
|
2644
|
+
reconstructBinding(ctx);
|
|
2645
|
+
await syncAutoSelectionToCurrentBranch(ctx);
|
|
2646
|
+
});
|
|
2647
|
+
|
|
2648
|
+
pi.on("session_tree", async (_event, ctx) => {
|
|
2649
|
+
config = loadConfig();
|
|
2650
|
+
clearReadcacheCaches();
|
|
2651
|
+
clearWindowsCache();
|
|
2652
|
+
markBindingValidationStale();
|
|
2653
|
+
reconstructBinding(ctx);
|
|
2654
|
+
await syncAutoSelectionToCurrentBranch(ctx);
|
|
2655
|
+
});
|
|
2656
|
+
|
|
2657
|
+
pi.on("session_compact", async () => {
|
|
2658
|
+
clearReadcacheCaches();
|
|
2659
|
+
});
|
|
2660
|
+
|
|
2661
|
+
pi.on("session_shutdown", async () => {
|
|
2662
|
+
clearReadcacheCaches();
|
|
2663
|
+
clearWindowsCache();
|
|
2664
|
+
activeAutoSelectionState = null;
|
|
2665
|
+
setBinding(null);
|
|
2666
|
+
});
|
|
2667
|
+
|
|
2668
|
+
pi.registerCommand("rpbind", {
|
|
2669
|
+
description: "Bind rp_exec to RepoPrompt: /rpbind <window_id> <tab>",
|
|
2670
|
+
handler: async (args, ctx) => {
|
|
2671
|
+
const parsed = parseRpbindArgs(args);
|
|
2672
|
+
if ("error" in parsed) {
|
|
2673
|
+
ctx.ui.notify(parsed.error, "error");
|
|
2674
|
+
return;
|
|
2675
|
+
}
|
|
2676
|
+
|
|
2677
|
+
const binding = await enrichBinding(parsed.windowId, parsed.tab);
|
|
2678
|
+
persistBinding(binding);
|
|
2679
|
+
|
|
2680
|
+
try {
|
|
2681
|
+
await syncAutoSelectionToCurrentBranch(ctx);
|
|
2682
|
+
} catch {
|
|
2683
|
+
// Fail-open
|
|
2684
|
+
}
|
|
2685
|
+
|
|
2686
|
+
ctx.ui.notify(
|
|
2687
|
+
`Bound rp_exec → window ${boundWindowId}, tab "${boundTab}"` +
|
|
2688
|
+
(boundWorkspaceName ? ` (${boundWorkspaceName})` : ""),
|
|
2689
|
+
"success"
|
|
2690
|
+
);
|
|
2691
|
+
},
|
|
2692
|
+
});
|
|
2693
|
+
|
|
2694
|
+
|
|
2695
|
+
pi.registerCommand("rpcli-readcache-status", {
|
|
2696
|
+
description: "Show repoprompt-cli read_file cache status",
|
|
2697
|
+
handler: async (_args, ctx) => {
|
|
2698
|
+
config = loadConfig();
|
|
2699
|
+
|
|
2700
|
+
let msg = "repoprompt-cli read_file cache\n";
|
|
2701
|
+
msg += "──────────────────────────\n";
|
|
2702
|
+
msg += `Enabled: ${config.readcacheReadFile === true ? "✓" : "✗"}\n`;
|
|
2703
|
+
msg += `Auto-select reads: ${config.autoSelectReadSlices === true ? "✓" : "✗"}\n`;
|
|
2704
|
+
|
|
2705
|
+
if (config.readcacheReadFile !== true) {
|
|
2706
|
+
msg += "\nEnable by creating ~/.pi/agent/extensions/repoprompt-cli/config.json\n";
|
|
2707
|
+
msg += "\nwith:\n { \"readcacheReadFile\": true }\n";
|
|
2708
|
+
ctx.ui.notify(msg, "info");
|
|
2709
|
+
return;
|
|
2710
|
+
}
|
|
2711
|
+
|
|
2712
|
+
try {
|
|
2713
|
+
const stats = await getStoreStats(ctx.cwd);
|
|
2714
|
+
msg += `\nObject store (under ${ctx.cwd}/.pi/readcache):\n`;
|
|
2715
|
+
msg += ` Objects: ${stats.objects}\n`;
|
|
2716
|
+
msg += ` Bytes: ${stats.bytes}\n`;
|
|
2717
|
+
} catch {
|
|
2718
|
+
msg += "\nObject store: unavailable\n";
|
|
2719
|
+
}
|
|
2720
|
+
|
|
2721
|
+
msg += "\nNotes:\n";
|
|
2722
|
+
msg += "- Cache applies only to simple rp_exec reads (read/cat/read_file)\n";
|
|
2723
|
+
msg += "- Use bypass_cache=true in the read command to force baseline output\n";
|
|
2724
|
+
|
|
2725
|
+
ctx.ui.notify(msg, "info");
|
|
2726
|
+
},
|
|
2727
|
+
});
|
|
2728
|
+
|
|
2729
|
+
pi.registerCommand("rpcli-readcache-refresh", {
|
|
2730
|
+
description: "Invalidate repoprompt-cli read_file cache trust for a path and optional line range",
|
|
2731
|
+
handler: async (args, ctx) => {
|
|
2732
|
+
config = loadConfig();
|
|
2733
|
+
|
|
2734
|
+
if (config.readcacheReadFile !== true) {
|
|
2735
|
+
ctx.ui.notify("readcacheReadFile is disabled in config", "error");
|
|
2736
|
+
return;
|
|
2737
|
+
}
|
|
2738
|
+
|
|
2739
|
+
const trimmed = args.trim();
|
|
2740
|
+
if (!trimmed) {
|
|
2741
|
+
ctx.ui.notify("Usage: /rpcli-readcache-refresh <path> [start-end]", "error");
|
|
2742
|
+
return;
|
|
2743
|
+
}
|
|
2744
|
+
|
|
2745
|
+
const parts = trimmed.split(/\s+/);
|
|
2746
|
+
const pathInput = parts[0];
|
|
2747
|
+
const rangeInput = parts[1];
|
|
2748
|
+
|
|
2749
|
+
if (!pathInput) {
|
|
2750
|
+
ctx.ui.notify("Usage: /rpcli-readcache-refresh <path> [start-end]", "error");
|
|
2751
|
+
return;
|
|
2752
|
+
}
|
|
2753
|
+
|
|
2754
|
+
const binding = await ensureBindingTargetsLiveWindow(ctx);
|
|
2755
|
+
if (!binding) {
|
|
2756
|
+
ctx.ui.notify("rp_exec is not bound. Bind first via /rpbind or rp_bind", "error");
|
|
2757
|
+
return;
|
|
2758
|
+
}
|
|
2759
|
+
|
|
2760
|
+
let scopeKey: ScopeKey = SCOPE_FULL;
|
|
2761
|
+
if (rangeInput) {
|
|
2762
|
+
const match = rangeInput.match(/^(\d+)-(\d+)$/);
|
|
2763
|
+
if (!match) {
|
|
2764
|
+
ctx.ui.notify("Invalid range. Use <start-end> like 1-120", "error");
|
|
2765
|
+
return;
|
|
2766
|
+
}
|
|
2767
|
+
|
|
2768
|
+
const start = parseInt(match[1] ?? "", 10);
|
|
2769
|
+
const end = parseInt(match[2] ?? "", 10);
|
|
2770
|
+
if (!Number.isFinite(start) || !Number.isFinite(end) || start <= 0 || end < start) {
|
|
2771
|
+
ctx.ui.notify("Invalid range. Use <start-end> like 1-120", "error");
|
|
2772
|
+
return;
|
|
2773
|
+
}
|
|
2774
|
+
|
|
2775
|
+
scopeKey = scopeRange(start, end);
|
|
2776
|
+
}
|
|
2777
|
+
|
|
2778
|
+
const resolved = await resolveReadFilePath(pi, pathInput, ctx.cwd, binding.windowId, binding.tab);
|
|
2779
|
+
if (!resolved.absolutePath) {
|
|
2780
|
+
ctx.ui.notify(`Could not resolve path: ${pathInput}`, "error");
|
|
2781
|
+
return;
|
|
2782
|
+
}
|
|
2783
|
+
|
|
2784
|
+
pi.appendEntry(RP_READCACHE_CUSTOM_TYPE, buildInvalidationV1(resolved.absolutePath, scopeKey));
|
|
2785
|
+
clearReadcacheCaches();
|
|
2786
|
+
|
|
2787
|
+
ctx.ui.notify(
|
|
2788
|
+
`Invalidated readcache for ${resolved.absolutePath}` + (scopeKey === SCOPE_FULL ? "" : ` (${scopeKey})`),
|
|
2789
|
+
"info"
|
|
2790
|
+
);
|
|
2791
|
+
},
|
|
2792
|
+
});
|
|
2793
|
+
|
|
2794
|
+
pi.registerTool({
|
|
2795
|
+
name: "rp_bind",
|
|
2796
|
+
label: "RepoPrompt Bind",
|
|
2797
|
+
description: "Bind rp_exec to a specific RepoPrompt window and compose tab",
|
|
2798
|
+
parameters: BindParams,
|
|
2799
|
+
|
|
2800
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
2801
|
+
await ensureJustBashLoaded();
|
|
2802
|
+
maybeWarnAstUnavailable(ctx);
|
|
2803
|
+
|
|
2804
|
+
const binding = await enrichBinding(params.windowId, params.tab);
|
|
2805
|
+
persistBinding(binding);
|
|
2806
|
+
|
|
2807
|
+
try {
|
|
2808
|
+
await syncAutoSelectionToCurrentBranch(ctx);
|
|
2809
|
+
} catch {
|
|
2810
|
+
// Fail-open
|
|
2811
|
+
}
|
|
2812
|
+
|
|
2813
|
+
return {
|
|
2814
|
+
content: [{ type: "text", text: `Bound rp_exec → window ${boundWindowId}, tab "${boundTab}"` }],
|
|
2815
|
+
details: {
|
|
2816
|
+
windowId: boundWindowId,
|
|
2817
|
+
tab: boundTab,
|
|
2818
|
+
workspaceId: boundWorkspaceId,
|
|
2819
|
+
workspaceName: boundWorkspaceName,
|
|
2820
|
+
workspaceRoots: boundWorkspaceRoots,
|
|
2821
|
+
},
|
|
2822
|
+
};
|
|
2823
|
+
},
|
|
2824
|
+
});
|
|
2825
|
+
|
|
2826
|
+
pi.registerTool({
|
|
2827
|
+
name: "rp_exec",
|
|
2828
|
+
label: "RepoPrompt Exec",
|
|
2829
|
+
description: "Run rp-cli in the bound RepoPrompt window/tab, with quiet defaults and output truncation",
|
|
2830
|
+
parameters: ExecParams,
|
|
2831
|
+
|
|
2832
|
+
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
2833
|
+
// Routing: prefer call-time overrides, otherwise fall back to the last persisted binding
|
|
2834
|
+
await ensureJustBashLoaded();
|
|
2835
|
+
maybeWarnAstUnavailable(ctx);
|
|
2836
|
+
|
|
2837
|
+
if (params.windowId === undefined && params.tab === undefined) {
|
|
2838
|
+
try {
|
|
2839
|
+
await maybeEnsureBindingTargetsLiveWindow(ctx);
|
|
2840
|
+
} catch {
|
|
2841
|
+
// Fail-open
|
|
2842
|
+
}
|
|
2843
|
+
}
|
|
2844
|
+
|
|
2845
|
+
const windowId = params.windowId ?? boundWindowId;
|
|
2846
|
+
const tab = params.tab ?? boundTab;
|
|
1407
2847
|
const rawJson = params.rawJson ?? false;
|
|
1408
2848
|
const quiet = params.quiet ?? true;
|
|
1409
2849
|
const failFast = params.failFast ?? true;
|
|
@@ -1504,10 +2944,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
1504
2944
|
let rpReadcache: RpReadcacheMetaV1 | null = null;
|
|
1505
2945
|
|
|
1506
2946
|
if (
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
2947
|
+
config.readcacheReadFile === true &&
|
|
2948
|
+
readRequest !== null &&
|
|
2949
|
+
readRequest.cacheable === true &&
|
|
2950
|
+
!execError &&
|
|
1511
2951
|
exitCode === 0 &&
|
|
1512
2952
|
windowId !== undefined &&
|
|
1513
2953
|
tab !== undefined
|
|
@@ -1538,6 +2978,30 @@ export default function (pi: ExtensionAPI) {
|
|
|
1538
2978
|
}
|
|
1539
2979
|
}
|
|
1540
2980
|
|
|
2981
|
+
const shouldAutoSelectRead =
|
|
2982
|
+
config.autoSelectReadSlices === true &&
|
|
2983
|
+
readRequest !== null &&
|
|
2984
|
+
!execError &&
|
|
2985
|
+
exitCode === 0 &&
|
|
2986
|
+
windowId !== undefined &&
|
|
2987
|
+
tab !== undefined &&
|
|
2988
|
+
params.windowId === undefined &&
|
|
2989
|
+
params.tab === undefined;
|
|
2990
|
+
|
|
2991
|
+
if (shouldAutoSelectRead) {
|
|
2992
|
+
try {
|
|
2993
|
+
await autoSelectReadFileInRepoPromptSelection(
|
|
2994
|
+
ctx,
|
|
2995
|
+
readRequest.path,
|
|
2996
|
+
readRequest.startLine,
|
|
2997
|
+
readRequest.limit,
|
|
2998
|
+
combinedOutput,
|
|
2999
|
+
);
|
|
3000
|
+
} catch {
|
|
3001
|
+
// Fail-open
|
|
3002
|
+
}
|
|
3003
|
+
}
|
|
3004
|
+
|
|
1541
3005
|
const editNoop =
|
|
1542
3006
|
!execError &&
|
|
1543
3007
|
exitCode === 0 &&
|