pi-repoprompt-mcp 0.6.2 → 0.7.0

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.
@@ -28,6 +28,7 @@ import type {
28
28
  RpTab,
29
29
  RpToolMeta,
30
30
  McpContent,
31
+ McpToolResult,
31
32
  AutoSelectionEntryData,
32
33
  AutoSelectionEntrySliceData,
33
34
  AutoSelectionEntryRangeData,
@@ -69,7 +70,7 @@ import { buildInvalidationV1 } from "./readcache/meta.js";
69
70
  import { clearReplayRuntimeState, createReplayRuntimeState } from "./readcache/replay.js";
70
71
  import type { RpReadcacheMetaV1, ScopeKey } from "./readcache/types.js";
71
72
  import { getStoreStats, pruneObjectsOlderThan } from "./readcache/object-store.js";
72
- import { resolveReadFilePath } from "./readcache/resolve.js";
73
+ import { clearRootsCache, resolveReadFilePath } from "./readcache/resolve.js";
73
74
 
74
75
  import {
75
76
  applyFullReadToSelectionState,
@@ -81,6 +82,13 @@ import {
81
82
  isWholeFileReadFromArgs,
82
83
  toPosixPath,
83
84
  } from "./auto-select.js";
85
+ import {
86
+ clearPendingTransitionSelectionState,
87
+ getPendingTransitionState,
88
+ setPendingTransitionSelectionState,
89
+ setPendingTransitionTargetState,
90
+ } from "./transition-state.js";
91
+ import type { PendingTransitionRetryMode, PendingTransitionTargetIdentity } from "./transition-state.js";
84
92
 
85
93
  function parseSummaryCount(value: string | undefined): number | undefined {
86
94
  if (!value) {
@@ -570,12 +578,34 @@ export default function repopromptMcp(pi: ExtensionAPI) {
570
578
  clearReplayRuntimeState(readcacheRuntimeState);
571
579
  };
572
580
 
581
+ type AutoSelectionSyncOptions = {
582
+ provisionTab?: boolean;
583
+ recoverClosedTab?: boolean;
584
+ reuseSoleEmptyTab?: boolean;
585
+ allowSyntheticSource?: boolean;
586
+ };
587
+
588
+ const STARTUP_AUTO_SELECTION_SYNC_OPTIONS: AutoSelectionSyncOptions = {
589
+ provisionTab: true,
590
+ recoverClosedTab: false,
591
+ reuseSoleEmptyTab: false,
592
+ allowSyntheticSource: true,
593
+ };
594
+
595
+ const TRANSITION_AUTO_SELECTION_SYNC_OPTIONS: AutoSelectionSyncOptions = {
596
+ provisionTab: false,
597
+ recoverClosedTab: true,
598
+ reuseSoleEmptyTab: true,
599
+ allowSyntheticSource: false,
600
+ };
601
+
573
602
  let activeAutoSelectionState: AutoSelectionEntryData | null = null;
574
603
  let autoSelectionUpdateQueue: Promise<void> = Promise.resolve();
604
+ let ownsLiveAutoSelection = false;
575
605
 
576
- function runAutoSelectionUpdate(task: () => Promise<void>): Promise<void> {
606
+ function runAutoSelectionUpdate<T>(task: () => Promise<T>): Promise<T> {
577
607
  const queued = autoSelectionUpdateQueue.then(task, task);
578
- autoSelectionUpdateQueue = queued.catch(() => {});
608
+ autoSelectionUpdateQueue = queued.then(() => undefined, () => undefined);
579
609
  return queued;
580
610
  }
581
611
 
@@ -804,12 +834,120 @@ export default function repopromptMcp(pi: ExtensionAPI) {
804
834
  return findAutoSelectionStateInEntries(entries, binding) ?? makeEmptyAutoSelectionState(binding);
805
835
  }
806
836
 
837
+ function resetAutoSelectionRuntimeState(): void {
838
+ activeAutoSelectionState = null;
839
+ autoSelectionUpdateQueue = Promise.resolve();
840
+ ownsLiveAutoSelection = false;
841
+ }
842
+
843
+ function commitLiveAutoSelectionState(state: AutoSelectionEntryData | null): void {
844
+ activeAutoSelectionState = state ? normalizeAutoSelectionState(state) : null;
845
+ ownsLiveAutoSelection = true;
846
+ }
847
+
848
+ function hasManagedAutoSelectionPaths(state: AutoSelectionEntryData | null): boolean {
849
+ return state !== null && autoSelectionManagedPaths(state).length > 0;
850
+ }
851
+
852
+ function updatePendingTransitionSelectionFromLiveState(): void {
853
+ if (!ownsLiveAutoSelection) {
854
+ return;
855
+ }
856
+
857
+ if (!hasManagedAutoSelectionPaths(activeAutoSelectionState)) {
858
+ clearPendingTransitionSelectionState();
859
+ return;
860
+ }
861
+
862
+ setPendingTransitionSelectionState(activeAutoSelectionState);
863
+ }
864
+
865
+ function autoSelectionRetryModeForSessionStartReason(
866
+ reason: "startup" | "reload" | "new" | "resume" | "fork"
867
+ ): PendingTransitionRetryMode {
868
+ return reason === "startup" || reason === "reload" ? "startup" : "transition";
869
+ }
870
+
871
+ function autoSelectionRetryModeForSyncOptions(
872
+ options: AutoSelectionSyncOptions
873
+ ): PendingTransitionRetryMode {
874
+ return options.provisionTab === false ? "transition" : "startup";
875
+ }
876
+
877
+ function autoSelectionSyncOptionsForRetryMode(
878
+ retryMode: PendingTransitionRetryMode
879
+ ): AutoSelectionSyncOptions {
880
+ return retryMode === "startup"
881
+ ? STARTUP_AUTO_SELECTION_SYNC_OPTIONS
882
+ : TRANSITION_AUTO_SELECTION_SYNC_OPTIONS;
883
+ }
884
+
885
+ function autoSelectionSyncOptionsForSessionStartReason(
886
+ reason: "startup" | "reload" | "new" | "resume" | "fork"
887
+ ): AutoSelectionSyncOptions {
888
+ return autoSelectionSyncOptionsForRetryMode(autoSelectionRetryModeForSessionStartReason(reason));
889
+ }
890
+
891
+ function reconnectAutoSelectionSyncOptions(): AutoSelectionSyncOptions {
892
+ return autoSelectionSyncOptionsForRetryMode(getPendingTransitionState()?.retryMode ?? "startup");
893
+ }
894
+
807
895
  function persistAutoSelectionState(state: AutoSelectionEntryData): void {
808
896
  const normalized = normalizeAutoSelectionState(state);
809
- activeAutoSelectionState = normalized;
897
+ commitLiveAutoSelectionState(normalized);
810
898
  pi.appendEntry(AUTO_SELECTION_ENTRY_TYPE, normalized);
811
899
  }
812
900
 
901
+ function getPendingTransitionTargetIdentity(ctx: ExtensionContext): PendingTransitionTargetIdentity {
902
+ return {
903
+ sessionFile: ctx.sessionManager.getSessionFile() ?? null,
904
+ sessionId: ctx.sessionManager.getSessionId(),
905
+ };
906
+ }
907
+
908
+ function samePendingTransitionTargetIdentity(
909
+ left: PendingTransitionTargetIdentity | null,
910
+ right: PendingTransitionTargetIdentity | null
911
+ ): boolean {
912
+ return left?.sessionFile === right?.sessionFile && left?.sessionId === right?.sessionId;
913
+ }
914
+
915
+ function seedPendingTransitionTargetForSessionStart(
916
+ ctx: ExtensionContext,
917
+ options: AutoSelectionSyncOptions
918
+ ): void {
919
+ const binding = getBinding();
920
+ const state = config.autoSelectReadSlices === true && binding?.tab
921
+ ? getAutoSelectionStateFromBranch(ctx, binding)
922
+ : null;
923
+
924
+ setPendingTransitionTargetState(
925
+ getPendingTransitionTargetIdentity(ctx),
926
+ binding,
927
+ state,
928
+ autoSelectionRetryModeForSyncOptions(options)
929
+ );
930
+ }
931
+
932
+ function throwOnMcpToolResultError(result: McpToolResult, fallbackMessage: string): void {
933
+ if (!result.isError) {
934
+ return;
935
+ }
936
+
937
+ throw new Error(extractTextContent(result.content) || fallbackMessage);
938
+ }
939
+
940
+ function isIgnorableOldBindingRemovalError(error: unknown): boolean {
941
+ const message = error instanceof Error ? error.message : String(error);
942
+ const lower = message.toLowerCase();
943
+
944
+ return (
945
+ (lower.includes("window") && lower.includes("not found")) ||
946
+ (lower.includes("tab") && lower.includes("not found")) ||
947
+ (lower.includes("context") && lower.includes("not found"))
948
+ );
949
+ }
950
+
813
951
  function bindingArgsForAutoSelectionState(state: AutoSelectionEntryData): Record<string, unknown> {
814
952
  return {
815
953
  _windowID: state.windowId,
@@ -836,11 +974,12 @@ export default function repopromptMcp(pi: ExtensionAPI) {
836
974
  return;
837
975
  }
838
976
 
839
- await client.callTool(manageSelectionToolName, {
977
+ const result = await client.callTool(manageSelectionToolName, {
840
978
  op: "remove",
841
979
  paths,
842
980
  ...bindingArgsForAutoSelectionState(state),
843
981
  });
982
+ throwOnMcpToolResultError(result, "RepoPrompt manage_selection remove failed");
844
983
  }
845
984
 
846
985
  async function addAutoSelectionFullPaths(
@@ -853,12 +992,13 @@ export default function repopromptMcp(pi: ExtensionAPI) {
853
992
  return;
854
993
  }
855
994
 
856
- await client.callTool(manageSelectionToolName, {
995
+ const result = await client.callTool(manageSelectionToolName, {
857
996
  op: "add",
858
997
  mode: "full",
859
998
  paths,
860
999
  ...bindingArgsForAutoSelectionState(state),
861
1000
  });
1001
+ throwOnMcpToolResultError(result, "RepoPrompt manage_selection add(full) failed");
862
1002
  }
863
1003
 
864
1004
  async function addAutoSelectionSlices(
@@ -871,11 +1011,12 @@ export default function repopromptMcp(pi: ExtensionAPI) {
871
1011
  return;
872
1012
  }
873
1013
 
874
- await client.callTool(manageSelectionToolName, {
1014
+ const result = await client.callTool(manageSelectionToolName, {
875
1015
  op: "add",
876
1016
  slices,
877
1017
  ...bindingArgsForAutoSelectionState(state),
878
1018
  });
1019
+ throwOnMcpToolResultError(result, "RepoPrompt manage_selection add(slices) failed");
879
1020
  }
880
1021
 
881
1022
  async function reconcileAutoSelectionWithinBinding(
@@ -1005,8 +1146,10 @@ export default function repopromptMcp(pi: ExtensionAPI) {
1005
1146
  currentState,
1006
1147
  autoSelectionManagedPaths(currentState)
1007
1148
  );
1008
- } catch {
1009
- // Old binding/window may no longer exist after RepoPrompt app restart
1149
+ } catch (error) {
1150
+ if (!isIgnorableOldBindingRemovalError(error)) {
1151
+ throw error;
1152
+ }
1010
1153
  }
1011
1154
 
1012
1155
  await addAutoSelectionFullPaths(client, manageSelectionToolName, desiredState, desiredState.fullPaths);
@@ -1022,8 +1165,10 @@ export default function repopromptMcp(pi: ExtensionAPI) {
1022
1165
  currentState,
1023
1166
  autoSelectionManagedPaths(currentState)
1024
1167
  );
1025
- } catch {
1026
- // Old binding/window may no longer exist after RepoPrompt app restart
1168
+ } catch (error) {
1169
+ if (!isIgnorableOldBindingRemovalError(error)) {
1170
+ throw error;
1171
+ }
1027
1172
  }
1028
1173
  return;
1029
1174
  }
@@ -1136,56 +1281,86 @@ export default function repopromptMcp(pi: ExtensionAPI) {
1136
1281
 
1137
1282
  async function syncAutoSelectionToCurrentBranch(
1138
1283
  ctx: ExtensionContext,
1139
- options: { provisionTab?: boolean; recoverClosedTab?: boolean; reuseSoleEmptyTab?: boolean } = {}
1284
+ options: AutoSelectionSyncOptions = reconnectAutoSelectionSyncOptions(),
1285
+ pendingTargetPolicy: "reuse" | "refresh" = "reuse"
1140
1286
  ): Promise<RpBinding | null> {
1141
- const previousBinding = getBinding();
1142
- const previousState = previousBinding
1143
- ? findAutoSelectionStateInEntries(ctx.sessionManager.getBranch(), previousBinding)
1144
- : null;
1145
-
1146
- const recoveryPaths = previousState ? autoSelectionManagedPaths(previousState) : [];
1147
- const hasRecoverableState = recoveryPaths.length > 0;
1148
- const binding = await ensureBindingTargetsLiveWindow(ctx, {
1149
- ...options,
1150
- hasRecoverableState,
1151
- recoveryPaths,
1152
- });
1153
-
1154
- if (config.autoSelectReadSlices !== true) {
1155
- activeAutoSelectionState = null;
1156
- return binding;
1157
- }
1287
+ return await runAutoSelectionUpdate(async () => {
1288
+ const transitionTargetIdentity = getPendingTransitionTargetIdentity(ctx);
1289
+ const pendingTransitionState = getPendingTransitionState();
1290
+ const pendingTargetMatchesCurrentSession = samePendingTransitionTargetIdentity(
1291
+ pendingTransitionState?.targetIdentity ?? null,
1292
+ transitionTargetIdentity
1293
+ );
1294
+ const reusePendingTarget = pendingTargetPolicy === "reuse" && pendingTargetMatchesCurrentSession;
1295
+
1296
+ const desiredBindingBeforeRecovery = reusePendingTarget
1297
+ ? pendingTransitionState?.targetBinding ?? getBinding()
1298
+ : getBinding();
1299
+ const desiredStateBeforeRecovery = reusePendingTarget
1300
+ ? pendingTransitionState?.targetState ?? null
1301
+ : config.autoSelectReadSlices === true && desiredBindingBeforeRecovery?.tab
1302
+ ? getAutoSelectionStateFromBranch(ctx, desiredBindingBeforeRecovery)
1303
+ : null;
1304
+
1305
+ if (!reusePendingTarget) {
1306
+ setPendingTransitionTargetState(
1307
+ transitionTargetIdentity,
1308
+ desiredBindingBeforeRecovery,
1309
+ desiredStateBeforeRecovery,
1310
+ autoSelectionRetryModeForSyncOptions(options)
1311
+ );
1312
+ }
1158
1313
 
1159
- let desiredState = binding?.tab ? getAutoSelectionStateFromBranch(ctx, binding) : null;
1160
- let recoveredState = false;
1314
+ const recoveryPaths = desiredStateBeforeRecovery ? autoSelectionManagedPaths(desiredStateBeforeRecovery) : [];
1315
+ const hasRecoverableState = recoveryPaths.length > 0;
1316
+ const liveBinding = await ensureBindingTargetsLiveWindow(ctx, {
1317
+ ...options,
1318
+ hasRecoverableState,
1319
+ recoveryPaths,
1320
+ });
1161
1321
 
1162
- if (binding?.tab && desiredState && autoSelectionManagedPaths(desiredState).length === 0) {
1163
- const recovered = recoverAutoSelectionStateForTabRecovery(previousState, previousBinding, binding);
1164
- if (recovered) {
1165
- desiredState = recovered;
1166
- recoveredState = true;
1322
+ if (config.autoSelectReadSlices !== true) {
1323
+ clearPendingTransitionSelectionState();
1324
+ activeAutoSelectionState = null;
1325
+ return liveBinding;
1326
+ }
1327
+
1328
+ const sourceState =
1329
+ pendingTransitionState?.sourceState ??
1330
+ activeAutoSelectionState ??
1331
+ (options.allowSyntheticSource === true ? desiredStateBeforeRecovery : null);
1332
+
1333
+ let desiredState = liveBinding?.tab ? getAutoSelectionStateFromBranch(ctx, liveBinding) : null;
1334
+ let recoveredState = false;
1335
+
1336
+ if (
1337
+ liveBinding?.tab &&
1338
+ desiredState &&
1339
+ desiredStateBeforeRecovery &&
1340
+ autoSelectionManagedPaths(desiredState).length === 0
1341
+ ) {
1342
+ const recovered = recoverAutoSelectionStateForTabRecovery(
1343
+ desiredStateBeforeRecovery,
1344
+ desiredBindingBeforeRecovery,
1345
+ liveBinding
1346
+ );
1347
+ if (recovered) {
1348
+ desiredState = recovered;
1349
+ recoveredState = true;
1350
+ }
1167
1351
  }
1168
- }
1169
1352
 
1170
- if (!binding?.tab) {
1171
- activeAutoSelectionState = desiredState;
1172
- return binding;
1173
- }
1174
-
1175
- let reconcileSucceeded = true;
1176
- try {
1177
- await reconcileAutoSelectionStates(activeAutoSelectionState, desiredState);
1178
- } catch {
1179
- reconcileSucceeded = false;
1180
- }
1353
+ await reconcileAutoSelectionStates(sourceState, desiredState);
1181
1354
 
1182
- if (recoveredState && desiredState && reconcileSucceeded) {
1183
- persistAutoSelectionState(desiredState);
1184
- return binding;
1185
- }
1355
+ if (recoveredState && desiredState) {
1356
+ persistAutoSelectionState(desiredState);
1357
+ } else {
1358
+ commitLiveAutoSelectionState(desiredState);
1359
+ }
1186
1360
 
1187
- activeAutoSelectionState = desiredState;
1188
- return binding;
1361
+ clearPendingTransitionSelectionState();
1362
+ return liveBinding;
1363
+ });
1189
1364
  }
1190
1365
 
1191
1366
  function getBaseAutoSelectionState(
@@ -1224,20 +1399,19 @@ export default function repopromptMcp(pi: ExtensionAPI) {
1224
1399
  // Lifecycle Events
1225
1400
  // ───────────────────────────────────────────────────────────────────────────
1226
1401
 
1227
- pi.on("session_start", async (_event, ctx) => {
1402
+ pi.on("session_start", async (event, ctx) => {
1228
1403
  shutdownRequested = false;
1229
1404
  extensionPaused = false;
1405
+ clearReadcacheCaches();
1406
+ clearRootsCache();
1407
+ resetAutoSelectionRuntimeState();
1230
1408
 
1231
1409
  if (ctx.hasUI) {
1232
1410
  // This extension used to set a status bar item; clear it to avoid persisting stale UI state
1233
1411
  ctx.ui.setStatus("rp", undefined);
1234
1412
  }
1235
1413
 
1236
- const restoredBinding = restoreBinding(ctx, config);
1237
- activeAutoSelectionState =
1238
- config.autoSelectReadSlices === true && restoredBinding
1239
- ? getAutoSelectionStateFromBranch(ctx, restoredBinding)
1240
- : null;
1414
+ restoreBinding(ctx, config);
1241
1415
 
1242
1416
  // Best-effort stale cache pruning (only when readcache is enabled)
1243
1417
  if (config.readcacheReadFile === true) {
@@ -1246,6 +1420,9 @@ export default function repopromptMcp(pi: ExtensionAPI) {
1246
1420
  });
1247
1421
  }
1248
1422
 
1423
+ const syncOptions = autoSelectionSyncOptionsForSessionStartReason(event.reason);
1424
+ seedPendingTransitionTargetForSessionStart(ctx, syncOptions);
1425
+
1249
1426
  // Non-blocking initialization
1250
1427
  const pendingInit = initializeExtension(pi, ctx, config);
1251
1428
  initPromise = pendingInit;
@@ -1257,8 +1434,8 @@ export default function repopromptMcp(pi: ExtensionAPI) {
1257
1434
  if (shutdownRequested) {
1258
1435
  return;
1259
1436
  }
1260
- await syncAutoSelectionToCurrentBranch(ctx);
1261
- }).catch(async (err) => {
1437
+ await syncAutoSelectionToCurrentBranch(ctx, syncOptions, "refresh");
1438
+ }).catch(async () => {
1262
1439
  if (initPromise === pendingInit) {
1263
1440
  initPromise = null;
1264
1441
  }
@@ -1273,8 +1450,9 @@ export default function repopromptMcp(pi: ExtensionAPI) {
1273
1450
  if (launched && !shutdownRequested) {
1274
1451
  try {
1275
1452
  await resetRpClient();
1453
+ clearRootsCache();
1276
1454
  await initializeExtension(pi, ctx, config);
1277
- await syncAutoSelectionToCurrentBranch(ctx);
1455
+ await syncAutoSelectionToCurrentBranch(ctx, syncOptions, "refresh");
1278
1456
  return;
1279
1457
  } catch {
1280
1458
  // Fall through to pause
@@ -1297,62 +1475,21 @@ export default function repopromptMcp(pi: ExtensionAPI) {
1297
1475
  pi.on("session_shutdown", async () => {
1298
1476
  shutdownRequested = true;
1299
1477
  initPromise = null;
1478
+ updatePendingTransitionSelectionFromLiveState();
1300
1479
 
1301
1480
  // Never block Pi shutdown on an MCP startup handshake that may be stuck waiting on the app
1481
+ clearBinding();
1302
1482
  clearReadcacheCaches();
1303
- activeAutoSelectionState = null;
1483
+ clearRootsCache();
1484
+ resetAutoSelectionRuntimeState();
1304
1485
  await resetRpClient();
1305
1486
  });
1306
1487
 
1307
- pi.on("session_switch", async (_event: unknown, ctx: ExtensionContext) => {
1488
+ pi.on("session_tree", async (_event, ctx) => {
1308
1489
  clearReadcacheCaches();
1490
+ clearRootsCache();
1309
1491
  restoreBinding(ctx, config);
1310
- await syncAutoSelectionToCurrentBranch(ctx, {
1311
- provisionTab: false,
1312
- recoverClosedTab: true,
1313
- reuseSoleEmptyTab: true,
1314
- });
1315
- if (ctx.hasUI) {
1316
- ctx.ui.setStatus("rp", undefined);
1317
- }
1318
- });
1319
-
1320
- // Restore binding from the current branch on /fork navigation
1321
- pi.on("session_fork", async (_event: unknown, ctx: ExtensionContext) => {
1322
- clearReadcacheCaches();
1323
- restoreBinding(ctx, config);
1324
- await syncAutoSelectionToCurrentBranch(ctx, {
1325
- provisionTab: false,
1326
- recoverClosedTab: true,
1327
- reuseSoleEmptyTab: true,
1328
- });
1329
- if (ctx.hasUI) {
1330
- ctx.ui.setStatus("rp", undefined);
1331
- }
1332
- });
1333
-
1334
- // Backwards compatibility (older pi versions)
1335
- (pi as any).on("session_branch", async (_event: unknown, ctx: ExtensionContext) => {
1336
- clearReadcacheCaches();
1337
- restoreBinding(ctx, config);
1338
- await syncAutoSelectionToCurrentBranch(ctx, {
1339
- provisionTab: false,
1340
- recoverClosedTab: true,
1341
- reuseSoleEmptyTab: true,
1342
- });
1343
- if (ctx.hasUI) {
1344
- ctx.ui.setStatus("rp", undefined);
1345
- }
1346
- });
1347
-
1348
- pi.on("session_tree", async (_event: unknown, ctx: ExtensionContext) => {
1349
- clearReadcacheCaches();
1350
- restoreBinding(ctx, config);
1351
- await syncAutoSelectionToCurrentBranch(ctx, {
1352
- provisionTab: false,
1353
- recoverClosedTab: true,
1354
- reuseSoleEmptyTab: true,
1355
- });
1492
+ await syncAutoSelectionToCurrentBranch(ctx, TRANSITION_AUTO_SELECTION_SYNC_OPTIONS, "refresh");
1356
1493
  if (ctx.hasUI) {
1357
1494
  ctx.ui.setStatus("rp", undefined);
1358
1495
  }
@@ -1619,9 +1756,10 @@ export default function repopromptMcp(pi: ExtensionAPI) {
1619
1756
  const wasPaused = extensionPaused;
1620
1757
  try {
1621
1758
  await resetRpClient();
1759
+ clearRootsCache();
1622
1760
  extensionPaused = false;
1623
1761
  await initializeExtension(pi, ctx, config);
1624
- await syncAutoSelectionToCurrentBranch(ctx);
1762
+ await syncAutoSelectionToCurrentBranch(ctx, reconnectAutoSelectionSyncOptions());
1625
1763
  ctx.ui.notify("RepoPrompt reconnected", "info");
1626
1764
 
1627
1765
  if (wasPaused) {
@@ -1890,7 +2028,7 @@ Mode priority: call > describe > search > windows > bind > status`,
1890
2028
 
1891
2029
  if (ctx) {
1892
2030
  try {
1893
- await syncAutoSelectionToCurrentBranch(ctx);
2031
+ await syncAutoSelectionToCurrentBranch(ctx, reconnectAutoSelectionSyncOptions());
1894
2032
  } catch {
1895
2033
  // Fail-open
1896
2034
  }
@@ -2141,7 +2279,7 @@ Mode priority: call > describe > search > windows > bind > status`,
2141
2279
  );
2142
2280
 
2143
2281
  if (autoSelectionStatesEqual(baseState, nextState)) {
2144
- activeAutoSelectionState = nextState;
2282
+ commitLiveAutoSelectionState(nextState);
2145
2283
  return;
2146
2284
  }
2147
2285
 
@@ -2170,7 +2308,7 @@ Mode priority: call > describe > search > windows > bind > status`,
2170
2308
  }
2171
2309
 
2172
2310
  if (!plan.desiredSlice) {
2173
- activeAutoSelectionState = plan.nextState;
2311
+ commitLiveAutoSelectionState(plan.nextState);
2174
2312
  return;
2175
2313
  }
2176
2314
 
@@ -2203,7 +2341,7 @@ Mode priority: call > describe > search > windows > bind > status`,
2203
2341
  );
2204
2342
 
2205
2343
  if (autoSelectionStatesEqual(baseState, nextState)) {
2206
- activeAutoSelectionState = nextState;
2344
+ commitLiveAutoSelectionState(nextState);
2207
2345
  return;
2208
2346
  }
2209
2347
 
@@ -0,0 +1,139 @@
1
+ import type { AutoSelectionEntryData, RpBinding } from "./types.js";
2
+
3
+ export type PendingTransitionRetryMode = "startup" | "transition";
4
+
5
+ export interface PendingTransitionTargetIdentity {
6
+ sessionFile: string | null;
7
+ sessionId: string;
8
+ }
9
+
10
+ export interface PendingTransitionSelectionState {
11
+ retryMode: PendingTransitionRetryMode | null;
12
+ sourceState: AutoSelectionEntryData | null;
13
+ targetIdentity: PendingTransitionTargetIdentity | null;
14
+ targetBinding: RpBinding | null;
15
+ targetState: AutoSelectionEntryData | null;
16
+ }
17
+
18
+ let pendingTransitionSelectionState: PendingTransitionSelectionState | null = null;
19
+
20
+ function cloneBinding(binding: RpBinding | null): RpBinding | null {
21
+ if (!binding) {
22
+ return null;
23
+ }
24
+
25
+ return {
26
+ windowId: binding.windowId,
27
+ tab: binding.tab,
28
+ workspace: binding.workspace,
29
+ autoDetected: binding.autoDetected,
30
+ };
31
+ }
32
+
33
+ function cloneIdentity(identity: PendingTransitionTargetIdentity | null): PendingTransitionTargetIdentity | null {
34
+ if (!identity) {
35
+ return null;
36
+ }
37
+
38
+ return {
39
+ sessionFile: identity.sessionFile,
40
+ sessionId: identity.sessionId,
41
+ };
42
+ }
43
+
44
+ function cloneRange(range: { start_line: number; end_line: number }) {
45
+ return {
46
+ start_line: range.start_line,
47
+ end_line: range.end_line,
48
+ };
49
+ }
50
+
51
+ function cloneState(state: AutoSelectionEntryData | null): AutoSelectionEntryData | null {
52
+ if (!state) {
53
+ return null;
54
+ }
55
+
56
+ return {
57
+ windowId: state.windowId,
58
+ tab: state.tab,
59
+ workspace: state.workspace,
60
+ fullPaths: [...state.fullPaths],
61
+ slicePaths: state.slicePaths.map((slice) => ({
62
+ path: slice.path,
63
+ ranges: slice.ranges.map(cloneRange),
64
+ })),
65
+ };
66
+ }
67
+
68
+ function clonePendingState(
69
+ state: PendingTransitionSelectionState | null
70
+ ): PendingTransitionSelectionState | null {
71
+ if (!state) {
72
+ return null;
73
+ }
74
+
75
+ return {
76
+ retryMode: state.retryMode,
77
+ sourceState: cloneState(state.sourceState),
78
+ targetIdentity: cloneIdentity(state.targetIdentity),
79
+ targetBinding: cloneBinding(state.targetBinding),
80
+ targetState: cloneState(state.targetState),
81
+ };
82
+ }
83
+
84
+ function setPendingState(state: PendingTransitionSelectionState | null): void {
85
+ const cloned = clonePendingState(state);
86
+ if (!cloned) {
87
+ pendingTransitionSelectionState = null;
88
+ return;
89
+ }
90
+
91
+ if (!cloned.sourceState && !cloned.targetBinding && !cloned.targetState) {
92
+ pendingTransitionSelectionState = null;
93
+ return;
94
+ }
95
+
96
+ pendingTransitionSelectionState = cloned;
97
+ }
98
+
99
+ export function getPendingTransitionState(): PendingTransitionSelectionState | null {
100
+ return clonePendingState(pendingTransitionSelectionState);
101
+ }
102
+
103
+ export function getPendingTransitionSelectionState(): AutoSelectionEntryData | null {
104
+ return cloneState(pendingTransitionSelectionState?.sourceState ?? null);
105
+ }
106
+
107
+ export function setPendingTransitionSelectionState(
108
+ state: AutoSelectionEntryData | null,
109
+ retryMode: PendingTransitionRetryMode | null = state
110
+ ? (pendingTransitionSelectionState?.retryMode ?? "transition")
111
+ : pendingTransitionSelectionState?.retryMode ?? null
112
+ ): void {
113
+ setPendingState({
114
+ retryMode,
115
+ sourceState: state,
116
+ targetIdentity: pendingTransitionSelectionState?.targetIdentity ?? null,
117
+ targetBinding: pendingTransitionSelectionState?.targetBinding ?? null,
118
+ targetState: pendingTransitionSelectionState?.targetState ?? null,
119
+ });
120
+ }
121
+
122
+ export function setPendingTransitionTargetState(
123
+ identity: PendingTransitionTargetIdentity | null,
124
+ binding: RpBinding | null,
125
+ state: AutoSelectionEntryData | null,
126
+ retryMode: PendingTransitionRetryMode | null = pendingTransitionSelectionState?.retryMode ?? null
127
+ ): void {
128
+ setPendingState({
129
+ retryMode,
130
+ sourceState: pendingTransitionSelectionState?.sourceState ?? null,
131
+ targetIdentity: identity,
132
+ targetBinding: binding,
133
+ targetState: state,
134
+ });
135
+ }
136
+
137
+ export function clearPendingTransitionSelectionState(): void {
138
+ pendingTransitionSelectionState = null;
139
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-repoprompt-mcp",
3
- "version": "0.6.2",
3
+ "version": "0.7.0",
4
4
  "description": "A token-efficient RepoPrompt integration for Pi with automated and branch-safe workspace management",
5
5
  "keywords": ["pi-package", "pi", "pi-coding-agent", "repoprompt", "mcp"],
6
6
  "license": "MIT",
@@ -22,7 +22,7 @@
22
22
  "diff": "^7.0.0"
23
23
  },
24
24
  "peerDependencies": {
25
- "@mariozechner/pi-coding-agent": "0.64.0",
25
+ "@mariozechner/pi-coding-agent": "0.65.0",
26
26
  "@mariozechner/pi-tui": "*",
27
27
  "@sinclair/typebox": "*"
28
28
  },