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