xtrm-tools 2.1.24 → 2.1.26

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/cli/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xtrm-cli",
3
- "version": "2.1.24",
3
+ "version": "2.1.26",
4
4
  "description": "Claude Code tools installer (skills, hooks, MCP servers)",
5
5
  "main": "./dist/index.js",
6
6
  "type": "module",
@@ -10,7 +10,10 @@ export default function (pi: ExtensionAPI) {
10
10
  const getCwd = (ctx: any) => ctx.cwd || process.cwd();
11
11
  const isBeadsProject = (cwd: string) => fs.existsSync(path.join(cwd, ".beads"));
12
12
 
13
- const getSessionClaim = async (sessionId: string, cwd: string): Promise<string | null> => {
13
+ // Pin session to PID stable for the life of the Pi process, consistent across all extension handlers
14
+ const sessionId = process.pid.toString();
15
+
16
+ const getSessionClaim = async (cwd: string): Promise<string | null> => {
14
17
  const result = await SubprocessRunner.run("bd", ["kv", "get", `claimed:${sessionId}`], { cwd });
15
18
  if (result.code === 0) return result.stdout.trim();
16
19
  return null;
@@ -33,10 +36,8 @@ export default function (pi: ExtensionAPI) {
33
36
  const cwd = getCwd(ctx);
34
37
  if (!isBeadsProject(cwd)) return undefined;
35
38
 
36
- const sessionId = ctx.sessionManager.sessionId;
37
-
38
39
  if (EventAdapter.isMutatingFileTool(event)) {
39
- const claim = await getSessionClaim(sessionId, cwd);
40
+ const claim = await getSessionClaim(cwd);
40
41
  if (!claim) {
41
42
  const hasWork = await hasTrackableWork(cwd);
42
43
  if (hasWork) {
@@ -45,7 +46,7 @@ export default function (pi: ExtensionAPI) {
45
46
  }
46
47
  return {
47
48
  block: true,
48
- reason: `No active issue claim for this session (${sessionId}).\n bd update <id> --claim\n bd kv set "claimed:${sessionId}" "<id>"`,
49
+ reason: `No active issue claim for this session (pid:${sessionId}).\n bd update <id> --claim\n bd kv set "claimed:${sessionId}" "<id>"`,
49
50
  };
50
51
  }
51
52
  }
@@ -54,7 +55,7 @@ export default function (pi: ExtensionAPI) {
54
55
  if (isToolCallEventType("bash", event)) {
55
56
  const command = event.input.command;
56
57
  if (command && /\bgit\s+commit\b/.test(command)) {
57
- const claim = await getSessionClaim(sessionId, cwd);
58
+ const claim = await getSessionClaim(cwd);
58
59
  if (claim) {
59
60
  return {
60
61
  block: true,
@@ -78,9 +79,8 @@ export default function (pi: ExtensionAPI) {
78
79
  if (issueMatch) {
79
80
  const issueId = issueMatch[1];
80
81
  const cwd = getCwd(ctx);
81
- const sessionId = ctx.sessionManager.sessionId;
82
82
  await SubprocessRunner.run("bd", ["kv", "set", `claimed:${sessionId}`, issueId], { cwd });
83
- const claimNotice = `\n\n✅ **Beads**: Session \`${sessionId}\` claimed issue \`${issueId}\`. File edits are now unblocked.`;
83
+ const claimNotice = `\n\n✅ **Beads**: Session \`pid:${sessionId}\` claimed issue \`${issueId}\`. File edits are now unblocked.`;
84
84
  return { content: [...event.content, { type: "text", text: claimNotice }] };
85
85
  }
86
86
  }
@@ -7,19 +7,107 @@
7
7
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
8
8
  import { truncateToWidth } from "@mariozechner/pi-tui";
9
9
 
10
+ import * as path from "node:path";
11
+ import * as fs from "node:fs";
12
+ import { SubprocessRunner } from "./core/lib";
13
+
10
14
  export default function (pi: ExtensionAPI) {
11
- let turnCount = 0;
15
+ // Pin session to PID — stable for the life of the Pi process, matches beads.ts
16
+ const SESSION_KEY = process.pid.toString();
17
+
18
+ interface BeadState {
19
+ claimId: string | null;
20
+ shortId: string | null;
21
+ status: string | null;
22
+ openCount: number;
23
+ lastFetch: number;
24
+ }
25
+
26
+ const STATUS_ICONS: Record<string, string> = {
27
+ open: "○",
28
+ in_progress: "◐",
29
+ blocked: "●",
30
+ closed: "✓",
31
+ };
32
+ const STATUS_BG: Record<string, string> = {
33
+ open: "\x1b[48;5;238m",
34
+ in_progress: "\x1b[48;5;28m",
35
+ blocked: "\x1b[48;5;88m",
36
+ };
37
+
38
+ let capturedCtx: any = null;
39
+ let beadState: BeadState = { claimId: null, shortId: null, status: null, openCount: 0, lastFetch: 0 };
40
+ let refreshing = false;
41
+ let requestRender: (() => void) | null = null;
42
+ const CACHE_TTL = 5000;
43
+
44
+ const getCwd = () => capturedCtx?.cwd || process.cwd();
45
+ const isBeadsProject = (cwd: string) => fs.existsSync(path.join(cwd, ".beads"));
46
+ const getShortId = (id: string) => id.split("-").pop() ?? id;
47
+
48
+ const refreshBeadState = async () => {
49
+ if (refreshing || Date.now() - beadState.lastFetch < CACHE_TTL) return;
50
+ const cwd = getCwd();
51
+ if (!isBeadsProject(cwd)) return;
52
+ refreshing = true;
53
+ try {
54
+ const claimResult = await SubprocessRunner.run("bd", ["kv", "get", `claimed:${SESSION_KEY}`], { cwd });
55
+ const claimId = claimResult.code === 0 ? claimResult.stdout.trim() || null : null;
56
+
57
+ let status: string | null = null;
58
+ if (claimId) {
59
+ const showResult = await SubprocessRunner.run("bd", ["show", claimId, "--json"], { cwd });
60
+ if (showResult.code === 0) {
61
+ try { status = JSON.parse(showResult.stdout).status ?? null; } catch {}
62
+ }
63
+ if (status === "closed") {
64
+ await SubprocessRunner.run("bd", ["kv", "clear", `claimed:${SESSION_KEY}`], { cwd });
65
+ beadState = { claimId: null, shortId: null, status: null, openCount: beadState.openCount, lastFetch: Date.now() };
66
+ requestRender?.();
67
+ return;
68
+ }
69
+ }
70
+
71
+ let openCount = 0;
72
+ const listResult = await SubprocessRunner.run("bd", ["list"], { cwd });
73
+ if (listResult.code === 0) {
74
+ const m = listResult.stdout.match(/\((\d+)\s+open/);
75
+ if (m) openCount = parseInt(m[1], 10);
76
+ }
77
+
78
+ beadState = { claimId, shortId: claimId ? getShortId(claimId) : null, status, openCount, lastFetch: Date.now() };
79
+ requestRender?.();
80
+ } catch {}
81
+ finally { refreshing = false; }
82
+ };
83
+
84
+ const buildBeadChip = (): string => {
85
+ const { claimId, shortId, status, openCount } = beadState;
86
+ if (claimId && shortId && status) {
87
+ const icon = STATUS_ICONS[status] ?? "?";
88
+ const bg = STATUS_BG[status] ?? "\x1b[48;5;238m";
89
+ return `${bg}\x1b[38;5;15m bd:${shortId}${icon} \x1b[0m`;
90
+ }
91
+ if (openCount > 0) {
92
+ return `\x1b[48;5;238m\x1b[38;5;15m bd:${openCount}${STATUS_ICONS.open} \x1b[0m`;
93
+ }
94
+ return "";
95
+ };
12
96
 
13
97
  pi.on("session_start", async (_event, ctx) => {
98
+ capturedCtx = ctx;
99
+
14
100
  ctx.ui.setFooter((tui, theme, footerData) => {
101
+ requestRender = () => tui.requestRender();
15
102
  const unsub = footerData.onBranchChange(() => tui.requestRender());
16
-
103
+
17
104
  return {
18
- dispose() { unsub(); },
105
+ dispose() { unsub(); requestRender = null; },
19
106
  invalidate() {},
20
107
  render(width: number): string[] {
21
- const brand = theme.fg("accent", "XTRM");
22
- const turns = theme.fg("dim", `[Turn ${turnCount}]`);
108
+ refreshBeadState().catch(() => {});
109
+
110
+ const brand = "\x1b[1m" + theme.fg("accent", "XTRM") + "\x1b[22m";
23
111
 
24
112
  const usage = ctx.getContextUsage();
25
113
  const pct = usage?.percent ?? 0;
@@ -37,11 +125,13 @@ export default function (pi: ExtensionAPI) {
37
125
  const modelStr = theme.fg("accent", modelId);
38
126
 
39
127
  const sep = theme.fg("dim", " | ");
40
-
41
- // Layout: XTRM [Turn 1] | model | 10% | ⌂ dir | ⎇ branch
42
- const leftParts = [`${brand} ${turns}`, modelStr, usageStr, cwdStr];
128
+
129
+ const leftParts = [brand, modelStr, usageStr, cwdStr];
43
130
  if (branchStr) leftParts.push(branchStr);
44
-
131
+
132
+ const beadChip = buildBeadChip();
133
+ if (beadChip) leftParts.push(beadChip);
134
+
45
135
  const left = leftParts.join(sep);
46
136
  return [truncateToWidth(left, width)];
47
137
  },
@@ -49,13 +139,13 @@ export default function (pi: ExtensionAPI) {
49
139
  });
50
140
  });
51
141
 
52
- pi.on("turn_start", async () => {
53
- turnCount++;
54
- });
55
-
56
- pi.on("session_switch", async (event, _ctx) => {
57
- if (event.reason === "new") {
58
- turnCount = 0;
142
+ // Bust the bead cache immediately after any bd write so the chip reflects the new state
143
+ pi.on("tool_result", async (event: any) => {
144
+ const cmd = event?.input?.command;
145
+ if (cmd && /\bbd\s+(close|update|create|claim)\b/.test(cmd)) {
146
+ beadState.lastFetch = 0;
147
+ setTimeout(() => refreshBeadState().catch(() => {}), 200);
59
148
  }
149
+ return undefined;
60
150
  });
61
151
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xtrm-tools",
3
- "version": "2.1.24",
3
+ "version": "2.1.26",
4
4
  "description": "Claude Code tools installer (skills, hooks, MCP servers)",
5
5
  "license": "MIT",
6
6
  "type": "module",