@jaggerxtrm/pi-extensions 0.7.23 → 0.7.24

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
@@ -48,3 +48,4 @@ Notable bundled extensions include:
48
48
 
49
49
  - `xtrm-ui` — XTRM Pi chrome, native tool summaries, selectable external tool chrome (`/xtrm-ui chrome background|box`).
50
50
  - `sp-terminal-overlay` — `/sp-feed` streaming overlay, `/sp-ps` snapshot overlay, and `/xtrm-terminal` shell overlay for specialist monitoring.
51
+ - `serena-pool` — shared Serena MCP daemon per repo root; sets `SERENA_MCP_PORT` for `pi-serena-tools`, reuses daemons across sessions, and safely cleans up owned orphan process groups.
@@ -15,3 +15,21 @@ Commands:
15
15
  - `/xtrm-terminal <command>` — opens an arbitrary shell command in an overlay.
16
16
 
17
17
  Keys: `Esc`/`q` close, `r` restart, arrows/page keys scroll.
18
+
19
+ ## serena-pool
20
+
21
+ Shared Serena daemon pool for Pi sessions.
22
+
23
+ Behavior:
24
+
25
+ - Resolves the current git repo root on `session_start`.
26
+ - Maps each repo root to a deterministic local port.
27
+ - Reuses an existing Serena MCP daemon when the port is already listening.
28
+ - Spawns Serena via `uvx` when no daemon is listening and exports `SERENA_MCP_PORT` for `pi-serena-tools`.
29
+ - Persists ownership state under `/tmp/serena-pool` and reaps only owned orphan process groups from dead recorded daemons.
30
+
31
+ Debugging:
32
+
33
+ ```bash
34
+ DEBUG=serena-pool pi
35
+ ```
@@ -258,6 +258,8 @@ export function formatLineLabel(count: number, noun: string): string {
258
258
  return `${count} ${noun}${count === 1 ? "" : "s"}`;
259
259
  }
260
260
 
261
+ export const TOOL_ROW_MARKER = "›";
262
+
261
263
  const TOOL_SUMMARY_SUBJECT_MAX = 34;
262
264
  const TOOL_SUMMARY_META_MAX = 34;
263
265
 
@@ -275,7 +277,7 @@ export function renderToolSummary(
275
277
  : "muted";
276
278
  const compactSubject = subject ? shortenCommand(subject, TOOL_SUMMARY_SUBJECT_MAX) : undefined;
277
279
  const compactMeta = meta ? shortenCommand(meta, TOOL_SUMMARY_META_MAX) : undefined;
278
- let text = `${theme.fg(color, "•")} ${theme.fg("toolTitle", theme.bold(label))}`;
280
+ let text = `${theme.fg(color, TOOL_ROW_MARKER)} ${theme.fg("toolTitle", theme.bold(label))}`;
279
281
  if (compactSubject) text += ` ${theme.fg("accent", compactSubject)}`;
280
282
  if (compactMeta) text += theme.fg("muted", ` · ${compactMeta}`);
281
283
  return text;
@@ -47,6 +47,7 @@ import {
47
47
  previewLines,
48
48
  renderRichDiffPreview,
49
49
  renderToolSummary,
50
+ TOOL_ROW_MARKER,
50
51
  shortenCommand,
51
52
  shortenPath,
52
53
  } from "./format";
@@ -252,7 +253,7 @@ type PatchableToolExecutionComponent = {
252
253
  type ExternalToolFrameKind = "serena" | "gitnexus" | "structured" | "process" | "external";
253
254
 
254
255
  const PATCHED_EXTERNAL_TOOL_FRAME = "__xtrmUiExternalToolFrame";
255
- const EXTERNAL_TOOL_FRAME_PATCH_VERSION = 11;
256
+ const EXTERNAL_TOOL_FRAME_PATCH_VERSION = 12;
256
257
  const ANSI_PATTERN = /\x1b\[[0-9;?]*[ -/]*[@-~]/g;
257
258
 
258
259
  function stripAnsi(text: string): string {
@@ -291,21 +292,21 @@ function getToolArgs(component: PatchableToolExecutionComponent): Record<string,
291
292
  function summarizeExternalToolPending(toolName: string | undefined, input: Record<string, unknown>): string {
292
293
  const name = toolName ?? "tool";
293
294
  if (name === "structured_return") {
294
- return `• structured_return ${shortenCommand(String(input.command ?? "running"), 38)}`;
295
+ return `${TOOL_ROW_MARKER} structured_return ${shortenCommand(String(input.command ?? "running"), 38)}`;
295
296
  }
296
297
  if (name === "process") {
297
- return `• process ${String(input.action ?? "running")}`;
298
+ return `${TOOL_ROW_MARKER} process ${String(input.action ?? "running")}`;
298
299
  }
299
300
  if (name.startsWith("gitnexus_")) {
300
301
  const subject = summarizeSerenaSubject(name, input) ?? summarizeToolSubject(name, input);
301
- return `• ${normalizeToolLabel(name)}${subject ? ` ${subject}` : ""}`;
302
+ return `${TOOL_ROW_MARKER} ${normalizeToolLabel(name)}${subject ? ` ${subject}` : ""}`;
302
303
  }
303
304
  if (SERENA_COMPACT_TOOLS.has(name)) {
304
305
  const subject = summarizeSerenaSubject(name, input);
305
- return `• serena ${name}${subject ? ` ${subject}` : ""}`;
306
+ return `${TOOL_ROW_MARKER} serena ${name}${subject ? ` ${subject}` : ""}`;
306
307
  }
307
308
  const subject = summarizeToolSubject(name, input) ?? summarizeSerenaSubject(name, input);
308
- return `• ${normalizeToolLabel(name)}${subject ? ` ${subject}` : ""}`;
309
+ return `${TOOL_ROW_MARKER} ${normalizeToolLabel(name)}${subject ? ` ${subject}` : ""}`;
309
310
  }
310
311
 
311
312
  function extractResultTextLines(component: PatchableToolExecutionComponent): string[] | undefined {
@@ -339,7 +340,7 @@ function externalToolBadgeColor(kind: ExternalToolFrameKind, text: string): stri
339
340
  }
340
341
 
341
342
  function highlightExternalToolBadge(kind: ExternalToolFrameKind, line: string): string {
342
- const match = line.match(/^(•\s+)(\S+)/u);
343
+ const match = line.match(/^([•›]\s+)(\S+)/u);
343
344
  if (!match?.[1] || !match[2]) return line;
344
345
  return match[1] + externalToolBadgeColor(kind, match[2]) + line.slice(match[1].length + match[2].length);
345
346
  }
@@ -1140,28 +1141,28 @@ function summarizeSerenaToolResult(
1140
1141
  case "jet_brains_find_symbol":
1141
1142
  case "jet_brains_find_referencing_symbols": {
1142
1143
  const count = countJsonItems(payload) ?? (text.match(/"name_path"\s*:/g)?.length ?? 0);
1143
- return `• serena ${toolName} ${subject ?? "symbol"}${meta(formatLineLabel(count, "result"), duration)}`;
1144
+ return `${TOOL_ROW_MARKER} serena ${toolName} ${subject ?? "symbol"}${meta(formatLineLabel(count, "result"), duration)}`;
1144
1145
  }
1145
1146
  case "get_symbols_overview":
1146
1147
  case "jet_brains_get_symbols_overview":
1147
1148
  case "jet_brains_type_hierarchy": {
1148
1149
  const count = Math.max(countOverviewSymbols(payload), text.match(/"name_path"\s*:/g)?.length ?? 0);
1149
- return `• serena ${toolName} ${subject ?? "file"}${meta(formatLineLabel(count, "symbol"), duration)}`;
1150
+ return `${TOOL_ROW_MARKER} serena ${toolName} ${subject ?? "file"}${meta(formatLineLabel(count, "symbol"), duration)}`;
1150
1151
  }
1151
1152
  case "search_for_pattern": {
1152
1153
  const count = countSearchMatches(payload) ?? (text.match(/^\s*>\s*\d+:/gm)?.length ?? 0);
1153
- return `• serena search ${subject ?? "pattern"}${meta(formatLineLabel(count, "match"), duration)}`;
1154
+ return `${TOOL_ROW_MARKER} serena search ${subject ?? "pattern"}${meta(formatLineLabel(count, "match"), duration)}`;
1154
1155
  }
1155
1156
  case "read_file": {
1156
- return `• serena read ${subject ?? "file"}${meta(formatLineLabel(countLines(text), "line"), duration)}`;
1157
+ return `${TOOL_ROW_MARKER} serena read ${subject ?? "file"}${meta(formatLineLabel(countLines(text), "line"), duration)}`;
1157
1158
  }
1158
1159
  case "list_dir": {
1159
1160
  const count = countJsonItems(payload) ?? countLines(text);
1160
- return `• serena list_dir ${subject ?? "."}${meta(formatLineLabel(count, "entry"), duration)}`;
1161
+ return `${TOOL_ROW_MARKER} serena list_dir ${subject ?? "."}${meta(formatLineLabel(count, "entry"), duration)}`;
1161
1162
  }
1162
1163
  case "find_file": {
1163
1164
  const count = countJsonItems(payload) ?? countLines(text);
1164
- return `• serena find_file ${String(input.file_mask ?? "")}${meta(formatLineLabel(count, "match"), duration)}`;
1165
+ return `${TOOL_ROW_MARKER} serena find_file ${String(input.file_mask ?? "")}${meta(formatLineLabel(count, "match"), duration)}`;
1165
1166
  }
1166
1167
  case "replace_symbol_body":
1167
1168
  case "insert_after_symbol":
@@ -1182,14 +1183,14 @@ function summarizeSerenaToolResult(
1182
1183
  case "restart_language_server":
1183
1184
  case "onboarding":
1184
1185
  case "serena_mcp_reset":
1185
- return `• serena ${toolName}${subject ? ` ${subject}` : ""}${meta(duration)}`;
1186
+ return `${TOOL_ROW_MARKER} serena ${toolName}${subject ? ` ${subject}` : ""}${meta(duration)}`;
1186
1187
  case "execute_shell_command": {
1187
1188
  const count = countLines(text);
1188
- return `• serena shell ${subject ?? "command"}${meta(formatLineLabel(count, "line"), duration)}`;
1189
+ return `${TOOL_ROW_MARKER} serena shell ${subject ?? "command"}${meta(formatLineLabel(count, "line"), duration)}`;
1189
1190
  }
1190
1191
  default: {
1191
1192
  const count = countJsonItems(payload) ?? countLines(text);
1192
- return `• serena ${toolName}${subject ? ` ${subject}` : ""}${meta(formatLineLabel(count, "item"), duration)}`;
1193
+ return `${TOOL_ROW_MARKER} serena ${toolName}${subject ? ` ${subject}` : ""}${meta(formatLineLabel(count, "item"), duration)}`;
1193
1194
  }
1194
1195
  }
1195
1196
  }
@@ -1257,7 +1258,7 @@ function summarizeGenericToolResult(
1257
1258
  const label = formatLineLabel(count, "line");
1258
1259
  const joined = joinMeta([label, duration]);
1259
1260
  const normalized = normalizeToolLabel(toolName);
1260
- return `• ${normalized}${subject ? ` ${subject}` : ""}${joined ? ` · ${joined}` : ""}`;
1261
+ return `${TOOL_ROW_MARKER} ${normalized}${subject ? ` ${subject}` : ""}${joined ? ` · ${joined}` : ""}`;
1261
1262
  }
1262
1263
 
1263
1264
  function summarizeStructuredReturnToolResult(
@@ -1275,7 +1276,7 @@ function summarizeStructuredReturnToolResult(
1275
1276
  const exitCode = typeof record?.exitCode === "number" ? `exit ${record.exitCode}` : undefined;
1276
1277
  const duration = formatDuration(durationMs);
1277
1278
  const meta = joinMeta([summary ? shortenCommand(summary, 72) : undefined, parser, exitCode, duration]);
1278
- return `• structured_return ${command}${meta ? ` · ${meta}` : ""}`;
1279
+ return `${TOOL_ROW_MARKER} structured_return ${command}${meta ? ` · ${meta}` : ""}`;
1279
1280
  }
1280
1281
 
1281
1282
  function summarizeProcessToolResult(
@@ -1297,7 +1298,7 @@ function summarizeProcessToolResult(
1297
1298
  const name = String(proc?.name ?? input.name ?? "process");
1298
1299
  const id = proc?.id ? String(proc.id) : undefined;
1299
1300
  const pid = proc?.pid != null ? `pid ${String(proc.pid)}` : undefined;
1300
- return `• process start "${name}"${meta(id, pid)}`;
1301
+ return `${TOOL_ROW_MARKER} process start "${name}"${meta(id, pid)}`;
1301
1302
  }
1302
1303
 
1303
1304
  if (action === "list") {
@@ -1306,25 +1307,25 @@ function summarizeProcessToolResult(
1306
1307
  const proc = asRecord(item);
1307
1308
  return proc?.status === "running" || proc?.status === "terminating";
1308
1309
  }).length;
1309
- return `• process list${meta(`${processes.length} ${processes.length === 1 ? "process" : "processes"}`, `${running} running`)}`;
1310
+ return `${TOOL_ROW_MARKER} process list${meta(`${processes.length} ${processes.length === 1 ? "process" : "processes"}`, `${running} running`)}`;
1310
1311
  }
1311
1312
 
1312
1313
  if (action === "output") {
1313
1314
  const output = asRecord(record?.output);
1314
1315
  const stdout = Array.isArray(output?.stdout) ? output.stdout.length : undefined;
1315
1316
  const stderr = Array.isArray(output?.stderr) ? output.stderr.length : undefined;
1316
- return `• process output ${String(input.id ?? "process")}${meta(
1317
+ return `${TOOL_ROW_MARKER} process output ${String(input.id ?? "process")}${meta(
1317
1318
  stdout != null ? `${stdout} stdout` : undefined,
1318
1319
  stderr != null ? `${stderr} stderr` : undefined,
1319
1320
  )}`;
1320
1321
  }
1321
1322
 
1322
1323
  if (action === "logs") {
1323
- return `• process logs ${String(input.id ?? "process")}${meta("log paths")}`;
1324
+ return `${TOOL_ROW_MARKER} process logs ${String(input.id ?? "process")}${meta("log paths")}`;
1324
1325
  }
1325
1326
 
1326
1327
  const message = typeof record?.message === "string" ? record.message : text.split("\n")[0];
1327
- return `• process ${action}${message ? ` · ${shortenCommand(message, 38)}` : ""}${duration ? ` · ${duration}` : ""}`;
1328
+ return `${TOOL_ROW_MARKER} process ${action}${message ? ` · ${shortenCommand(message, 38)}` : ""}${duration ? ` · ${duration}` : ""}`;
1328
1329
  }
1329
1330
 
1330
1331
  function summarizeExternalToolResult(
@@ -1452,13 +1453,13 @@ function registerXtrmUiTools(pi: ExtensionAPI, getPrefs: () => XtrmUiPrefs): voi
1452
1453
  const meta = getXtrmMeta<BashToolDetails, Record<string, unknown>>(details);
1453
1454
  const command = shortenCommand(String(meta?.args.command ?? ""), 38);
1454
1455
  if (isPartial) {
1455
- return toolRowText(theme, `${theme.fg("accent", "•")} ${theme.fg("toolTitle", "Running ")}${theme.fg("accent", command)}${theme.fg("toolTitle", " in bash")}`);
1456
+ return toolRowText(theme, `${theme.fg("accent", TOOL_ROW_MARKER)} ${theme.fg("toolTitle", "Running ")}${theme.fg("accent", command)}${theme.fg("toolTitle", " in bash")}`);
1456
1457
  }
1457
1458
  const output = getTextContent(result as any);
1458
1459
  const outputLines = cleanOutputLines(output);
1459
1460
  const exitMatch = output.match(/exit code:\s*(-?\d+)/i);
1460
1461
  const exitCode = exitMatch ? Number.parseInt(exitMatch[1] ?? "0", 10) : 0;
1461
- const bullet = exitCode === 0 ? theme.fg("success", "•") : theme.fg("error", "•");
1462
+ const bullet = exitCode === 0 ? theme.fg("success", TOOL_ROW_MARKER) : theme.fg("error", TOOL_ROW_MARKER);
1462
1463
  const summary = joinMeta([formatLineLabel(outputLines.length, "line"), formatDuration(meta?.durationMs), details.truncation?.truncated ? "truncated" : undefined]);
1463
1464
  let text = `${bullet} ${theme.fg("toolTitle", "Ran ")}${theme.fg("accent", command)}`;
1464
1465
  if (summary) text += theme.fg("dim", ` · ${summary}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jaggerxtrm/pi-extensions",
3
- "version": "0.7.23",
3
+ "version": "0.7.24",
4
4
  "description": "Unified Pi extension entrypoint for xtrm-managed extensions",
5
5
  "type": "module",
6
6
  "publishConfig": {