pi-repoprompt-mcp 0.5.4 → 0.6.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.
package/README.md CHANGED
@@ -53,7 +53,7 @@ Forked sessions inherit the parent session-plus-node's window, tab, and auto-sel
53
53
  <img width="270" height="936" alt="Collapsed call/result summaries" src="https://raw.githubusercontent.com/w-winter/dot314/main/packages/pi-repoprompt-mcp/docs/images/collapsed-summaries.png" />
54
54
  </p>
55
55
 
56
- - RepoPrompt `apply_edits` calls are forwarded with `verbose: true` by default (unless `raw: true`), while the returned diff is normalized into `details.diff` and presented to the agent as a terse summary. The same is done for `file_actions create/delete` outputs, so you see all edited/created/deleted LOC with rich rendering but the extension prevents the context window from getting bloated by round-tripping tool I/O tokens
56
+ - RepoPrompt `apply_edits` calls are forwarded with `verbose: true` by default, while the returned diff is normalized into `details.diff` and presented to the agent as a terse summary. The same is done for `file_actions create/delete` outputs, so you see all edited/created/deleted LOC with rich rendering but the extension prevents the context window from getting bloated by round-tripping tool I/O tokens
57
57
  - Adaptive diff rendering for RepoPrompt `git` and `apply_edits` outputs by default (`diffViewMode: "auto"` picks split, unified, compact, or summary at render time based on pane width). This uses the active Pi theme's `toolDiffAdded`, `toolDiffRemoved`, and `toolDiffContext` colors (typically mapped to chosen hues for green and red), and its visual design and rendering logic are indebted to [MasuRii/pi-tool-display](https://github.com/MasuRii/pi-tool-display). Two different examples at different pane widths:
58
58
 
59
59
  <p align="center">
@@ -207,7 +207,6 @@ but this is best-effort
207
207
 
208
208
  ## Readcache gotchas
209
209
 
210
- - `raw: true` disables readcache (and rendering). Don't use unless debugging
211
210
  - Need full content? use `bypass_cache: true` in `read_file` args
212
211
  - Multi-root: use absolute or specific relative paths (MCP `read_file` has no `RootName:` disambiguation)
213
212
 
@@ -754,7 +754,7 @@ function parseTabFromJson(raw: unknown): RpTab | null {
754
754
  }
755
755
 
756
756
  const obj = raw as Record<string, unknown>;
757
- const idRaw = obj.id ?? obj.tabId ?? obj.tab_id ?? obj.uuid;
757
+ const idRaw = obj.contextId ?? obj.context_id ?? obj.id ?? obj.tabId ?? obj.tab_id ?? obj.uuid;
758
758
  if (typeof idRaw !== "string" || !idRaw.trim()) {
759
759
  return null;
760
760
  }
@@ -786,7 +786,20 @@ function collectTabsFromJson(raw: unknown): RpTab[] {
786
786
  }
787
787
 
788
788
  const obj = raw as Record<string, unknown>;
789
- const containers = [obj.tabs, obj.tab, obj.createdTab, obj.created_tab, obj.selectedTab, obj.selected_tab];
789
+ const containers = [
790
+ obj.tabs,
791
+ obj.contexts,
792
+ obj.tab,
793
+ obj.context,
794
+ obj.createdTab,
795
+ obj.created_tab,
796
+ obj.createdContext,
797
+ obj.created_context,
798
+ obj.selectedTab,
799
+ obj.selected_tab,
800
+ obj.selectedContext,
801
+ obj.selected_context,
802
+ ];
790
803
 
791
804
  for (const candidate of containers) {
792
805
  const parsed = collectTabsFromJson(candidate);
@@ -835,7 +848,26 @@ function dedupeTabs(tabs: RpTab[]): RpTab[] {
835
848
 
836
849
  function parseTabLine(line: string): RpTab | null {
837
850
  const trimmed = line.trim();
838
- if (!trimmed || !trimmed.includes("`")) {
851
+ if (!trimmed) {
852
+ return null;
853
+ }
854
+
855
+ const contextMatch = trimmed.match(/^[\u2022-]\s*(.+?)(?:\s+\[([^\]]+)\])?\s+—\s+context_id:\s*`([^`]+)`/i);
856
+ if (contextMatch) {
857
+ const state = contextMatch[2] ?? "";
858
+ return {
859
+ id: contextMatch[3].trim(),
860
+ name: stripTrailingTabStateAnnotations(contextMatch[1].trim()) || contextMatch[3].trim(),
861
+ isActive: /\bactive\b|\bin-focus\b/i.test(state)
862
+ ? true
863
+ : /\bout-of-focus\b/i.test(state)
864
+ ? false
865
+ : undefined,
866
+ isBound: /\bbound\b/i.test(state) ? true : undefined,
867
+ };
868
+ }
869
+
870
+ if (!trimmed.includes("`")) {
839
871
  return null;
840
872
  }
841
873
 
@@ -869,6 +901,17 @@ function parseTabLine(line: string): RpTab | null {
869
901
  };
870
902
  }
871
903
 
904
+ function parseTabsFromBindContextText(text: string): RpTab[] {
905
+ return dedupeTabs(
906
+ text
907
+ .split("\n")
908
+ .map((line) => line.trim())
909
+ .filter((line) => /—\s+context_id:\s*`[^`]+`/i.test(line))
910
+ .map(parseTabLine)
911
+ .filter((tab): tab is RpTab => tab !== null)
912
+ );
913
+ }
914
+
872
915
  export function parseTabList(text: string): RpTab[] {
873
916
  const tabs: RpTab[] = [];
874
917
  let lastTab: RpTab | null = null;
@@ -899,132 +942,129 @@ function parseTabsFromJson(value: unknown): RpTab[] | null {
899
942
  return tabs.length > 0 ? dedupeTabs(tabs) : null;
900
943
  }
901
944
 
902
- function parseChatCountFromJson(value: unknown): number | undefined {
903
- if (!value) {
904
- return undefined;
905
- }
906
-
907
- if (Array.isArray(value)) {
908
- return value.length;
945
+ function findLiveTab(tabs: RpTab[], reference: string | undefined): RpTab | null {
946
+ if (!reference) {
947
+ return null;
909
948
  }
910
949
 
911
- if (typeof value !== "object") {
912
- return undefined;
913
- }
950
+ return tabs.find((tab) => tab.id === reference || tab.name === reference) ?? null;
951
+ }
914
952
 
915
- const obj = value as Record<string, unknown>;
916
- const directCount = parseCountMaybe(
917
- obj.count ?? obj.chatCount ?? obj.chat_count ?? obj.total ?? obj.totalCount ?? obj.total_count
918
- );
919
- if (directCount !== undefined) {
920
- return directCount;
921
- }
953
+ function isExplicitlyEmptyTab(tab: RpTab): boolean {
954
+ return tab.selectedFileCount === 0;
955
+ }
922
956
 
923
- for (const key of ["chats", "sessions", "items", "results"]) {
924
- const candidate = obj[key];
925
- if (Array.isArray(candidate)) {
926
- return candidate.length;
927
- }
928
- }
957
+ function orderReusableTabCandidates(tabs: RpTab[]): RpTab[] {
958
+ const ordered = [
959
+ ...tabs.filter((tab) => tab.isBound === true),
960
+ ...tabs.filter((tab) => tab.isBound !== true && tab.isActive === true),
961
+ ...tabs.filter((tab) => tab.isBound !== true && tab.isActive !== true),
962
+ ];
929
963
 
930
- return undefined;
964
+ return ordered.filter((tab, index) => ordered.findIndex((candidate) => candidate.id === tab.id) === index);
931
965
  }
932
966
 
933
- function parseChatCountFromText(text: string): number | undefined {
934
- const countMatch = text.match(/\bCount\b[^\d]*([\d,]+)/i);
935
- if (countMatch?.[1]) {
936
- return parseCountMaybe(countMatch[1]);
937
- }
967
+ function parseOracleSessionTabPrefixes(text: string): Set<string> {
968
+ const prefixes = new Set<string>();
938
969
 
939
- if (/\bNo chats\b/i.test(text)) {
940
- return 0;
970
+ for (const match of text.matchAll(/\btab=([A-F0-9-]{6,})(?:…|\b)/gi)) {
971
+ const prefix = match[1]?.trim().toUpperCase();
972
+ if (prefix) {
973
+ prefixes.add(prefix);
974
+ }
941
975
  }
942
976
 
943
- const sessionCount = text
944
- .split("\n")
945
- .map((line) => line.trim())
946
- .filter((line) => /^•\s*\[[^\]]+\]/.test(line)).length;
947
-
948
- return sessionCount > 0 ? sessionCount : undefined;
977
+ return prefixes;
949
978
  }
950
979
 
951
- async function fetchTabChatCount(
952
- tabId: string,
980
+ async function fetchOracleSessionTabPrefixes(
981
+ windowId: number,
953
982
  client: ReturnType<typeof getRpClient> = getRpClient()
954
- ): Promise<number | undefined> {
955
- if (!client.isConnected) {
956
- return undefined;
957
- }
958
-
959
- const chatsToolName = resolveToolName(client.tools, "chats");
960
- if (!chatsToolName) {
961
- return undefined;
983
+ ): Promise<Set<string> | null> {
984
+ const oracleUtilsToolName = resolveToolName(client.tools, "oracle_utils");
985
+ if (!oracleUtilsToolName) {
986
+ return null;
962
987
  }
963
988
 
964
- const result = await client.callTool(chatsToolName, {
965
- action: "list",
966
- scope: "tab",
967
- tab_id: tabId,
968
- limit: 1,
989
+ const result = await client.callTool(oracleUtilsToolName, {
990
+ op: "sessions",
991
+ limit: 200,
992
+ _windowID: windowId,
969
993
  });
970
994
 
971
995
  if (result.isError) {
972
- return undefined;
973
- }
974
-
975
- const countFromJson = parseChatCountFromJson(extractJsonContent(result.content));
976
- if (countFromJson !== undefined) {
977
- return countFromJson;
996
+ return null;
978
997
  }
979
998
 
980
- return parseChatCountFromText(extractTextContent(result.content));
999
+ return parseOracleSessionTabPrefixes(extractTextContent(result.content));
981
1000
  }
982
1001
 
983
- function findLiveTab(tabs: RpTab[], reference: string | undefined): RpTab | null {
984
- if (!reference) {
985
- return null;
1002
+ function tabHasOracleHistory(tabId: string, sessionTabPrefixes: Set<string> | null): boolean {
1003
+ if (!sessionTabPrefixes || sessionTabPrefixes.size === 0) {
1004
+ return false;
986
1005
  }
987
1006
 
988
- return tabs.find((tab) => tab.id === reference || tab.name === reference) ?? null;
989
- }
1007
+ const normalizedTabId = tabId.trim().toUpperCase();
1008
+ for (const prefix of sessionTabPrefixes) {
1009
+ if (normalizedTabId.startsWith(prefix)) {
1010
+ return true;
1011
+ }
1012
+ }
990
1013
 
991
- function isExplicitlyEmptyTab(tab: RpTab): boolean {
992
- return tab.selectedFileCount === 0;
1014
+ return false;
993
1015
  }
994
1016
 
995
- async function isSafeReusableTab(
1017
+ async function hasEmptySelection(
1018
+ windowId: number,
996
1019
  tab: RpTab,
997
1020
  client: ReturnType<typeof getRpClient> = getRpClient()
998
1021
  ): Promise<boolean> {
999
- if (!isExplicitlyEmptyTab(tab)) {
1022
+ if (isExplicitlyEmptyTab(tab)) {
1023
+ return true;
1024
+ }
1025
+
1026
+ const manageSelectionToolName = resolveToolName(client.tools, "manage_selection");
1027
+ if (!manageSelectionToolName) {
1000
1028
  return false;
1001
1029
  }
1002
1030
 
1003
- const chatCount = await fetchTabChatCount(tab.id, client);
1004
- return chatCount === 0;
1005
- }
1031
+ const result = await client.callTool(manageSelectionToolName, {
1032
+ op: "get",
1033
+ view: "summary",
1034
+ _windowID: windowId,
1035
+ context_id: tab.id,
1036
+ });
1006
1037
 
1007
- function orderReusableEmptyTabs(tabs: RpTab[]): RpTab[] {
1008
- const emptyTabs = tabs.filter(isExplicitlyEmptyTab);
1009
- if (emptyTabs.length === 0) {
1010
- return [];
1038
+ if (result.isError) {
1039
+ return false;
1011
1040
  }
1012
1041
 
1013
- const ordered = [
1014
- ...emptyTabs.filter((tab) => tab.isBound === true),
1015
- ...emptyTabs.filter((tab) => tab.isBound !== true && tab.isActive === true),
1016
- ...emptyTabs.filter((tab) => tab.isBound !== true && tab.isActive !== true),
1017
- ];
1042
+ const text = extractTextContent(result.content);
1043
+ return /\b0 total tokens\b/i.test(text);
1044
+ }
1018
1045
 
1019
- return ordered.filter((tab, index) => ordered.findIndex((candidate) => candidate.id === tab.id) === index);
1046
+ async function isSafeReusableTab(
1047
+ windowId: number,
1048
+ tab: RpTab,
1049
+ sessionTabPrefixes: Set<string> | null,
1050
+ client: ReturnType<typeof getRpClient> = getRpClient()
1051
+ ): Promise<boolean> {
1052
+ if (tabHasOracleHistory(tab.id, sessionTabPrefixes)) {
1053
+ return false;
1054
+ }
1055
+
1056
+ return await hasEmptySelection(windowId, tab, client);
1020
1057
  }
1021
1058
 
1022
1059
  async function findReusableSafeTab(
1060
+ windowId: number,
1023
1061
  tabs: RpTab[],
1024
1062
  client: ReturnType<typeof getRpClient> = getRpClient()
1025
1063
  ): Promise<RpTab | null> {
1026
- for (const tab of orderReusableEmptyTabs(tabs)) {
1027
- if (await isSafeReusableTab(tab, client)) {
1064
+ const sessionTabPrefixes = await fetchOracleSessionTabPrefixes(windowId, client);
1065
+
1066
+ for (const tab of orderReusableTabCandidates(tabs)) {
1067
+ if (await isSafeReusableTab(windowId, tab, sessionTabPrefixes, client)) {
1028
1068
  return tab;
1029
1069
  }
1030
1070
  }
@@ -1066,14 +1106,14 @@ export async function fetchWindowTabs(
1066
1106
  throw new Error("Not connected to RepoPrompt");
1067
1107
  }
1068
1108
 
1069
- const manageWorkspacesToolName = resolveToolName(client.tools, "manage_workspaces");
1070
- if (!manageWorkspacesToolName) {
1109
+ const bindContextToolName = resolveToolName(client.tools, "bind_context");
1110
+ if (!bindContextToolName) {
1071
1111
  return [];
1072
1112
  }
1073
1113
 
1074
- const result = await client.callTool(manageWorkspacesToolName, {
1075
- action: "list_tabs",
1076
- ...bindingWindowArgs(windowId),
1114
+ const result = await client.callTool(bindContextToolName, {
1115
+ op: "list",
1116
+ window_id: windowId,
1077
1117
  });
1078
1118
 
1079
1119
  if (result.isError) {
@@ -1086,7 +1126,7 @@ export async function fetchWindowTabs(
1086
1126
  return tabsFromJson;
1087
1127
  }
1088
1128
 
1089
- return parseTabList(extractTextContent(result.content));
1129
+ return parseTabsFromBindContextText(extractTextContent(result.content));
1090
1130
  }
1091
1131
 
1092
1132
  async function selectTab(
@@ -1094,16 +1134,15 @@ async function selectTab(
1094
1134
  tabId: string,
1095
1135
  client: ReturnType<typeof getRpClient> = getRpClient()
1096
1136
  ): Promise<void> {
1097
- const manageWorkspacesToolName = resolveToolName(client.tools, "manage_workspaces");
1098
- if (!manageWorkspacesToolName) {
1137
+ const bindContextToolName = resolveToolName(client.tools, "bind_context");
1138
+ if (!bindContextToolName) {
1099
1139
  return;
1100
1140
  }
1101
1141
 
1102
- const result = await client.callTool(manageWorkspacesToolName, {
1103
- action: "select_tab",
1104
- tab: tabId,
1105
- focus: false,
1106
- ...bindingWindowArgs(windowId),
1142
+ const result = await client.callTool(bindContextToolName, {
1143
+ op: "bind",
1144
+ window_id: windowId,
1145
+ context_id: tabId,
1107
1146
  });
1108
1147
 
1109
1148
  if (result.isError) {
@@ -1290,7 +1329,7 @@ export async function ensureBindingHasTab(
1290
1329
  }
1291
1330
 
1292
1331
  if (!binding.tab || reuseSoleEmptyTab || recoverIfMissing) {
1293
- const reusableSafeTab = await findReusableSafeTab(liveTabs, client);
1332
+ const reusableSafeTab = await findReusableSafeTab(binding.windowId, liveTabs, client);
1294
1333
  if (reusableSafeTab) {
1295
1334
  return await adoptTab(reusableSafeTab, true);
1296
1335
  }
@@ -1498,7 +1537,7 @@ export function getBindingArgs(): Record<string, unknown> {
1498
1537
  };
1499
1538
 
1500
1539
  if (currentBinding.tab) {
1501
- args._tabID = currentBinding.tab;
1540
+ args.context_id = currentBinding.tab;
1502
1541
  }
1503
1542
 
1504
1543
  return args;
@@ -546,9 +546,6 @@ const RpToolSchema = Type.Object({
546
546
  confirmEdits: Type.Optional(
547
547
  Type.Boolean({ description: "Confirm edit-like operations (required when confirmEdits is enabled)" })
548
548
  ),
549
-
550
- // Formatting
551
- raw: Type.Optional(Type.Boolean({ description: "Return raw output without formatting" })),
552
549
  });
553
550
 
554
551
  // ─────────────────────────────────────────────────────────────────────────────
@@ -816,7 +813,7 @@ export default function repopromptMcp(pi: ExtensionAPI) {
816
813
  function bindingArgsForAutoSelectionState(state: AutoSelectionEntryData): Record<string, unknown> {
817
814
  return {
818
815
  _windowID: state.windowId,
819
- ...(state.tab ? { _tabID: state.tab } : {}),
816
+ ...(state.tab ? { context_id: state.tab } : {}),
820
817
  };
821
818
  }
822
819
 
@@ -1585,9 +1582,9 @@ export default function repopromptMcp(pi: ExtensionAPI) {
1585
1582
  try {
1586
1583
  await ensureTabScopedBinding(ctx, "RepoPrompt binding has no tab. Use /rp bind or /rp tab new first.");
1587
1584
 
1588
- const chatSendToolName = resolveToolName(client.tools, "chat_send");
1589
- if (!chatSendToolName) {
1590
- ctx.ui.notify("RepoPrompt tool 'chat_send' not available", "error");
1585
+ const oracleSendToolName = resolveToolName(client.tools, "oracle_send");
1586
+ if (!oracleSendToolName) {
1587
+ ctx.ui.notify("RepoPrompt tool 'oracle_send' not available", "error");
1591
1588
  return;
1592
1589
  }
1593
1590
 
@@ -1601,7 +1598,7 @@ export default function repopromptMcp(pi: ExtensionAPI) {
1601
1598
  if (chatName) callArgs.chat_name = chatName;
1602
1599
  if (chatId) callArgs.chat_id = chatId;
1603
1600
 
1604
- const result = await client.callTool(chatSendToolName, callArgs);
1601
+ const result = await client.callTool(oracleSendToolName, callArgs);
1605
1602
 
1606
1603
  const text = extractTextContent(result.content);
1607
1604
 
@@ -1783,10 +1780,6 @@ Mode priority: call > describe > search > windows > bind > status`,
1783
1780
  return new Text(theme.fg("error", "↳ " + textContent), 0, 0);
1784
1781
  }
1785
1782
 
1786
- if (details.raw) {
1787
- return new Text(textContent, 0, 0);
1788
- }
1789
-
1790
1783
  const successPrefix = theme.fg("success", "↳ ");
1791
1784
  const collapsedMaxLines = config.collapsedMaxLines ?? 15;
1792
1785
  const normalizedToolName = typeof details.tool === "string" ? normalizeToolName(details.tool) : undefined;
@@ -2577,14 +2570,12 @@ Mode priority: call > describe > search > windows > bind > status`,
2577
2570
  const forwardedUserArgs = buildForwardedUserArgs({
2578
2571
  toolName: normalizedTool,
2579
2572
  userArgs,
2580
- raw: params.raw,
2581
2573
  });
2582
2574
 
2583
2575
  const mergedArgs = { ...forwardedUserArgs, ...bindingArgs };
2584
2576
 
2585
2577
  const fileActionDeleteSnapshot = normalizedTool === "file_actions"
2586
2578
  && userArgs.action === "delete"
2587
- && params.raw !== true
2588
2579
  && typeof userArgs.path === "string"
2589
2580
  ? (() => {
2590
2581
  try {
@@ -2611,7 +2602,6 @@ Mode priority: call > describe > search > windows > bind > status`,
2611
2602
 
2612
2603
  const shouldReadcache =
2613
2604
  config.readcacheReadFile === true &&
2614
- params.raw !== true &&
2615
2605
  normalizedTool === "read_file" &&
2616
2606
  typeof userArgs.path === "string" &&
2617
2607
  ctx !== undefined;
@@ -2668,9 +2658,8 @@ Mode priority: call > describe > search > windows > bind > status`,
2668
2658
  : normalizeToolResultText({
2669
2659
  toolName: normalizedTool,
2670
2660
  text: textContent,
2671
- raw: params.raw,
2672
2661
  });
2673
- const normalizedFileActionResult = result.isError || params.raw === true
2662
+ const normalizedFileActionResult = result.isError
2674
2663
  ? null
2675
2664
  : normalizeFileActionResult({
2676
2665
  action: userArgs.action,
@@ -2733,7 +2722,6 @@ Mode priority: call > describe > search > windows > bind > status`,
2733
2722
  warning: guardResult.warning,
2734
2723
  editNoop,
2735
2724
  rpReadcache: rpReadcache ?? undefined,
2736
- raw: params.raw,
2737
2725
  ...(normalizedTextResult ? normalizedTextResult.details : {}),
2738
2726
  ...(normalizedFileActionResult ?? {}),
2739
2727
  },
@@ -39,9 +39,8 @@ function normalizeDiffBlockCode(code: string): string {
39
39
  export function normalizeToolResultText(args: {
40
40
  toolName: string | undefined;
41
41
  text: string;
42
- raw: boolean | undefined;
43
42
  }): ToolResultNormalization | null {
44
- if (args.toolName !== "apply_edits" || args.raw === true || !args.text.includes("```diff")) {
43
+ if (args.toolName !== "apply_edits" || !args.text.includes("```diff")) {
45
44
  return null;
46
45
  }
47
46
 
@@ -1,7 +1,6 @@
1
1
  export function buildForwardedUserArgs(args: {
2
2
  toolName: string | undefined;
3
3
  userArgs: Record<string, unknown>;
4
- raw: boolean | undefined;
5
4
  }): Record<string, unknown> {
6
5
  const forwardedUserArgs: Record<string, unknown> = { ...args.userArgs };
7
6
 
@@ -9,7 +8,7 @@ export function buildForwardedUserArgs(args: {
9
8
  delete forwardedUserArgs.bypass_cache;
10
9
  }
11
10
 
12
- if (args.toolName === "apply_edits" && args.raw !== true) {
11
+ if (args.toolName === "apply_edits") {
13
12
  forwardedUserArgs.verbose = true;
14
13
  }
15
14
 
@@ -152,9 +152,6 @@ export interface RpToolParams {
152
152
  // Safety overrides
153
153
  allowDelete?: boolean; // Allow delete operations
154
154
  confirmEdits?: boolean; // Confirm edit-like operations when confirmEdits is enabled
155
-
156
- // Formatting
157
- raw?: boolean; // Return raw output without formatting
158
155
  }
159
156
 
160
157
  // ─────────────────────────────────────────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-repoprompt-mcp",
3
- "version": "0.5.4",
3
+ "version": "0.6.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",