pi-repoprompt-mcp 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -50,17 +50,17 @@ Forked sessions inherit the parent session-plus-node's window, tab, and auto-sel
50
50
  - Common non-mutating RepoPrompt actions (`read_file`, `file_search`, `get_file_tree`, `get_code_structure`, `workspace_context`, routing helpers like `manage_workspaces`, and control/discovery actions like `windows`/`bind`/`status`/`search`/`describe`) get concise request-driven call/result summaries in collapsed mode. The call line carries intent while the result line carries outcome, so the transcript stays compact without echoing the same label twice. These summaries are derived from the arguments Pi sent, not by parsing RepoPrompt's prose output, and unknown tools fall back to normal collapsed rendering
51
51
 
52
52
  <p align="center">
53
- <img width="270" height="936" alt="Image" src="https://github.com/user-attachments/assets/142ca6c2-c1cf-4f0b-b41b-3d52d623c78c" />
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
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
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">
60
- <img width="1027" height="256" alt="horizontal" src="https://github.com/user-attachments/assets/31943d5b-475c-4254-813b-18bf9bd79d60" />
60
+ <img width="1027" height="256" alt="Split diff rendering" src="https://raw.githubusercontent.com/w-winter/dot314/main/packages/pi-repoprompt-mcp/docs/images/diff-split.png" />
61
61
  </p>
62
62
  <p align="center">
63
- <img width="629" height="302" alt="vertical" src="https://github.com/user-attachments/assets/fe4fc253-6bda-49e3-a37e-918244eb9e05" />
63
+ <img width="629" height="302" alt="Unified diff rendering" src="https://raw.githubusercontent.com/w-winter/dot314/main/packages/pi-repoprompt-mcp/docs/images/diff-unified.png" />
64
64
  </p>
65
65
 
66
66
  - Generic fenced diff blocks, and adaptive-diff parse failures, fall back to a simpler diff renderer, which uses `delta` if installed or otherwise the built-in highlighter
@@ -99,7 +99,7 @@ If RepoPrompt renames/removes these tools or changes their required parameters/o
99
99
  - `/rp status` — show status (connection + binding), including the currently bound tab name and a label like `[bound, in-focus]` or `[bound, out-of-focus]`, plus current selected file counts and estimated token counts
100
100
 
101
101
  <p align="center">
102
- <img width="210" alt="status" src="https://github.com/user-attachments/assets/bd59af9e-7df1-4572-8baf-edb6f8f7a0df" />
102
+ <img width="210" alt="Status display" src="https://raw.githubusercontent.com/w-winter/dot314/main/packages/pi-repoprompt-mcp/docs/images/status.png" />
103
103
  </p>
104
104
 
105
105
  - `/rp windows` — list available RepoPrompt windows
@@ -219,6 +219,8 @@ but this is best-effort
219
219
  ### Pi becomes unresponsive after closing/restarting RepoPrompt
220
220
  If the RepoPrompt MCP server stops responding (for example, if the RepoPrompt app is closed while Pi stays open), tool calls may time out. When that happens, the extension will drop the connection and you can recover with `/rp reconnect`.
221
221
 
222
+ If RepoPrompt is not running when Pi starts, the extension auto-pauses itself after a quick connection timeout. While paused, the `rp` tool returns a short error directing the agent to use native tools. Run `/rp reconnect` once RepoPrompt is open to resume, and the agent will be notified that `rp` is available again.
223
+
222
224
  ### "No matching window found"
223
225
  - Your `cwd` may not match any RepoPrompt workspace root
224
226
  - Use `/rp windows` to list windows
@@ -15,12 +15,32 @@ const CLIENT_INFO = {
15
15
  version: "1.0.0",
16
16
  };
17
17
 
18
+ const DEFAULT_CONNECT_TIMEOUT_MS = 6_000;
18
19
  const DEFAULT_LIST_TOOLS_TIMEOUT_MS = 10_000;
19
20
 
20
21
  // Keep parity with the rp-cli integration default (15 minutes). Some RepoPrompt tools
21
22
  // (notably context_builder and chat_send) can legitimately take longer than 10s
22
23
  const DEFAULT_TOOL_CALL_TIMEOUT_MS = 15 * 60 * 1000;
23
24
 
25
+ async function withTimeout<T>(promise: Promise<T>, timeoutMs: number, message: string): Promise<T> {
26
+ let timeoutId: ReturnType<typeof setTimeout> | undefined;
27
+
28
+ try {
29
+ return await Promise.race([
30
+ promise,
31
+ new Promise<T>((_, reject) => {
32
+ timeoutId = setTimeout(() => {
33
+ reject(new Error(message));
34
+ }, timeoutMs);
35
+ }),
36
+ ]);
37
+ } finally {
38
+ if (timeoutId) {
39
+ clearTimeout(timeoutId);
40
+ }
41
+ }
42
+ }
43
+
24
44
 
25
45
  /**
26
46
  * Manages the MCP connection to RepoPrompt server
@@ -31,23 +51,23 @@ export class RpClient {
31
51
  private _status: ConnectionStatus = "disconnected";
32
52
  private _tools: RpToolMeta[] = [];
33
53
  private _error: string | undefined;
34
-
54
+
35
55
  get status(): ConnectionStatus {
36
56
  return this._status;
37
57
  }
38
-
58
+
39
59
  get tools(): RpToolMeta[] {
40
60
  return this._tools;
41
61
  }
42
-
62
+
43
63
  get error(): string | undefined {
44
64
  return this._error;
45
65
  }
46
-
66
+
47
67
  get isConnected(): boolean {
48
68
  return this._status === "connected" && this.client !== null;
49
69
  }
50
-
70
+
51
71
  /**
52
72
  * Connect to the RepoPrompt MCP server
53
73
  */
@@ -59,13 +79,13 @@ export class RpClient {
59
79
  if (this._status === "connecting") {
60
80
  throw new Error("Connection already in progress");
61
81
  }
62
-
82
+
63
83
  // Close existing connection if any
64
84
  await this.close();
65
-
85
+
66
86
  this._status = "connecting";
67
87
  this._error = undefined;
68
-
88
+
69
89
  try {
70
90
  // Create transport
71
91
  const mergedEnv: Record<string, string> = {};
@@ -77,36 +97,40 @@ export class RpClient {
77
97
  if (env) {
78
98
  Object.assign(mergedEnv, env);
79
99
  }
80
-
100
+
81
101
  this.transport = new StdioClientTransport({
82
102
  command,
83
103
  args,
84
104
  env: Object.keys(mergedEnv).length > 0 ? mergedEnv : undefined,
85
105
  });
86
-
106
+
87
107
  // Create client
88
108
  this.client = new Client(CLIENT_INFO, {
89
109
  capabilities: {},
90
110
  });
91
-
111
+
92
112
  // Connect
93
- await this.client.connect(this.transport);
94
-
113
+ await withTimeout(
114
+ this.client.connect(this.transport),
115
+ DEFAULT_CONNECT_TIMEOUT_MS,
116
+ `Timed out connecting to RepoPrompt MCP server after ${DEFAULT_CONNECT_TIMEOUT_MS}ms`
117
+ );
118
+
95
119
  // Fetch available tools
96
120
  await this.refreshTools();
97
-
121
+
98
122
  this._status = "connected";
99
123
  } catch (error) {
100
124
  this._status = "error";
101
125
  this._error = error instanceof Error ? error.message : String(error);
102
-
126
+
103
127
  // Clean up on error
104
128
  await this.close();
105
-
129
+
106
130
  throw error;
107
131
  }
108
132
  }
109
-
133
+
110
134
  /**
111
135
  * Refresh the list of available tools
112
136
  */
@@ -125,7 +149,7 @@ export class RpClient {
125
149
 
126
150
  return this._tools;
127
151
  }
128
-
152
+
129
153
  /**
130
154
  * Call a tool on the RepoPrompt MCP server
131
155
  */
@@ -187,33 +211,38 @@ export class RpClient {
187
211
  isError: Boolean(result.isError),
188
212
  };
189
213
  }
190
-
214
+
191
215
  /**
192
216
  * Close the connection
193
217
  */
194
218
  async close(): Promise<void> {
195
- if (this.client) {
219
+ const client = this.client;
220
+ const transport = this.transport;
221
+ const wasConnecting = this._status === "connecting";
222
+
223
+ this.client = null;
224
+ this.transport = null;
225
+ this._status = "disconnected";
226
+ this._tools = [];
227
+
228
+ // If connect() never completed, skip the graceful MCP close and tear down the transport directly
229
+ if (client && !wasConnecting) {
196
230
  try {
197
- await this.client.close();
231
+ await client.close();
198
232
  } catch {
199
233
  // Ignore close errors
200
234
  }
201
- this.client = null;
202
235
  }
203
236
 
204
- if (this.transport) {
237
+ if (transport) {
205
238
  try {
206
- await this.transport.close();
239
+ await transport.close();
207
240
  } catch {
208
241
  // Ignore close errors
209
242
  }
210
- this.transport = null;
211
243
  }
212
-
213
- this._status = "disconnected";
214
- this._tools = [];
215
244
  }
216
-
245
+
217
246
  /**
218
247
  * Get connection info for debugging
219
248
  */
@@ -221,7 +250,7 @@ export class RpClient {
221
250
  if (!this.client || !this.transport) {
222
251
  return null;
223
252
  }
224
-
253
+
225
254
  return {
226
255
  client: this.client,
227
256
  transport: this.transport,
@@ -557,6 +557,8 @@ const RpToolSchema = Type.Object({
557
557
  export default function repopromptMcp(pi: ExtensionAPI) {
558
558
  let config: RpConfig = loadConfig();
559
559
  let initPromise: Promise<void> | null = null;
560
+ let shutdownRequested = false;
561
+ let extensionPaused = false;
560
562
 
561
563
  pi.on("before_agent_start", async () => {
562
564
  // Reload config so display knobs (collapsedMaxLines etc.) apply without requiring /reload
@@ -1225,6 +1227,9 @@ export default function repopromptMcp(pi: ExtensionAPI) {
1225
1227
  // ───────────────────────────────────────────────────────────────────────────
1226
1228
 
1227
1229
  pi.on("session_start", async (_event, ctx) => {
1230
+ shutdownRequested = false;
1231
+ extensionPaused = false;
1232
+
1228
1233
  if (ctx.hasUI) {
1229
1234
  // This extension used to set a status bar item; clear it to avoid persisting stale UI state
1230
1235
  ctx.ui.setStatus("rp", undefined);
@@ -1244,16 +1249,27 @@ export default function repopromptMcp(pi: ExtensionAPI) {
1244
1249
  }
1245
1250
 
1246
1251
  // Non-blocking initialization
1247
- initPromise = initializeExtension(pi, ctx, config);
1252
+ const pendingInit = initializeExtension(pi, ctx, config);
1253
+ initPromise = pendingInit;
1248
1254
 
1249
- initPromise.then(async () => {
1250
- initPromise = null;
1255
+ pendingInit.then(async () => {
1256
+ if (initPromise === pendingInit) {
1257
+ initPromise = null;
1258
+ }
1259
+ if (shutdownRequested) {
1260
+ return;
1261
+ }
1251
1262
  await syncAutoSelectionToCurrentBranch(ctx);
1252
1263
  }).catch((err) => {
1253
- console.error("RepoPrompt MCP initialization failed:", err);
1254
- initPromise = null;
1264
+ if (initPromise === pendingInit) {
1265
+ initPromise = null;
1266
+ }
1267
+ if (shutdownRequested) {
1268
+ return;
1269
+ }
1270
+ extensionPaused = true;
1255
1271
  if (ctx.hasUI) {
1256
- ctx.ui.notify(`RepoPrompt: ${err.message}`, "error");
1272
+ ctx.ui.notify("RepoPrompt unavailable — extension paused. Use /rp reconnect when ready.", "warning");
1257
1273
  }
1258
1274
  });
1259
1275
  });
@@ -1263,14 +1279,10 @@ export default function repopromptMcp(pi: ExtensionAPI) {
1263
1279
  });
1264
1280
 
1265
1281
  pi.on("session_shutdown", async () => {
1266
- if (initPromise) {
1267
- try {
1268
- await initPromise;
1269
- } catch {
1270
- // Ignore
1271
- }
1272
- }
1282
+ shutdownRequested = true;
1283
+ initPromise = null;
1273
1284
 
1285
+ // Never block Pi shutdown on an MCP startup handshake that may be stuck waiting on the app
1274
1286
  clearReadcacheCaches();
1275
1287
  activeAutoSelectionState = null;
1276
1288
  await resetRpClient();
@@ -1340,15 +1352,15 @@ export default function repopromptMcp(pi: ExtensionAPI) {
1340
1352
  const parts = args?.trim().split(/\s+/) ?? [];
1341
1353
  const subcommand = parts[0]?.toLowerCase() ?? "status";
1342
1354
 
1343
- // Allow status/readcache-status/readcache-refresh/reconnect while disconnected
1344
- if (
1345
- subcommand !== "reconnect" &&
1346
- subcommand !== "status" &&
1347
- subcommand !== "readcache-status" &&
1348
- subcommand !== "readcache_status" &&
1349
- subcommand !== "readcache-refresh" &&
1350
- subcommand !== "readcache_refresh"
1351
- ) {
1355
+ // Allow status/reconnect while disconnected or paused
1356
+ const alwaysAllowed = new Set(["reconnect", "status", "readcache-status", "readcache_status", "readcache-refresh", "readcache_refresh"]);
1357
+
1358
+ if (extensionPaused && !alwaysAllowed.has(subcommand)) {
1359
+ ctx.ui.notify("RepoPrompt extension is paused. Use /rp reconnect to resume.", "warning");
1360
+ return;
1361
+ }
1362
+
1363
+ if (!alwaysAllowed.has(subcommand)) {
1352
1364
  await ensureConnected(ctx);
1353
1365
  }
1354
1366
 
@@ -1587,16 +1599,31 @@ export default function repopromptMcp(pi: ExtensionAPI) {
1587
1599
  break;
1588
1600
  }
1589
1601
 
1590
- case "reconnect":
1602
+ case "reconnect": {
1603
+ const wasPaused = extensionPaused;
1591
1604
  try {
1592
1605
  await resetRpClient();
1606
+ extensionPaused = false;
1593
1607
  await initializeExtension(pi, ctx, config);
1594
1608
  await syncAutoSelectionToCurrentBranch(ctx);
1595
1609
  ctx.ui.notify("RepoPrompt reconnected", "info");
1610
+
1611
+ if (wasPaused) {
1612
+ pi.sendMessage(
1613
+ {
1614
+ customType: "rp-availability",
1615
+ content: "RepoPrompt (`rp` tool) is now available.",
1616
+ display: false,
1617
+ },
1618
+ { triggerTurn: false },
1619
+ );
1620
+ }
1596
1621
  } catch (err) {
1622
+ extensionPaused = true;
1597
1623
  ctx.ui.notify(`Reconnection failed: ${err instanceof Error ? err.message : err}`, "error");
1598
1624
  }
1599
1625
  break;
1626
+ }
1600
1627
 
1601
1628
  default:
1602
1629
  ctx.ui.notify(
@@ -1640,6 +1667,13 @@ Mode priority: call > describe > search > windows > bind > status`,
1640
1667
  parameters: RpToolSchema,
1641
1668
 
1642
1669
  async execute(_toolCallId, params: RpToolParams, _signal, onUpdate, _ctx) {
1670
+ if (extensionPaused) {
1671
+ throw new Error(
1672
+ "The rp tool is not currently available due to a connection issue. " +
1673
+ "The user can run /rp reconnect when the RepoPrompt app is running."
1674
+ );
1675
+ }
1676
+
1643
1677
  // Provide a no-op if onUpdate is undefined
1644
1678
  const safeOnUpdate = onUpdate ?? (() => {});
1645
1679
 
@@ -2176,6 +2210,9 @@ Mode priority: call > describe > search > windows > bind > status`,
2176
2210
 
2177
2211
  let msg = `RepoPrompt Status\n`;
2178
2212
  msg += `─────────────────\n`;
2213
+ if (extensionPaused) {
2214
+ msg += `Extension: ⏸ paused (use /rp reconnect to resume)\n`;
2215
+ }
2179
2216
  msg += `Connection: ${client.isConnected ? "✓ connected" : "✗ disconnected"}\n`;
2180
2217
  msg += `Tools: ${client.tools.length}\n`;
2181
2218
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-repoprompt-mcp",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
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",