Skip to content

Commit b106d63

Browse files
authored
feat(extensions): add cmux sidebar integration extension (#45)
* feat(extensions): add cmux sidebar integration extension * docs: update changelog
1 parent 0798c3c commit b106d63

3 files changed

Lines changed: 189 additions & 64 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,11 @@ All notable changes to agent-stuff are documented here.
7777

7878

7979

80+
81+
82+
## feat/cmux-sidebar-integration
83+
84+
Introduces a new cmux sidebar integration extension (#45) that pushes pi agent state—including model, thinking mode, token usage, and session cost—into the cmux multiplexer's sidebar in real-time via lifecycle event hooks. The extension is fire-and-forget, silently ignoring cmux errors to prevent agent disruption, and operates only when running inside cmux (detected via `CMUX_SOCKET_PATH` environment variable). Replaces the terminal progress indicator with this richer sidebar-based status system, providing better visibility into agent activity, resource consumption, and tool execution within the cmux environment.
8085

8186
## feat/ruby-extension-support
8287

pi-extensions/cmux.ts

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
/**
2+
* cmux — Push pi agent state into the cmux sidebar.
3+
*
4+
* Hooks into pi lifecycle events and fires cmux CLI commands to update
5+
* sidebar status keys, progress, and notifications. Fire-and-forget —
6+
* errors are silently ignored so cmux issues never affect pi.
7+
*
8+
* No-op when CMUX_SOCKET_PATH is not set (i.e. not running inside cmux).
9+
*
10+
* Source: https://github.com/HazAT/pi-config/tree/main/extensions/cmux
11+
*/
12+
13+
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
14+
15+
const CMUX_SOCKET = process.env.CMUX_SOCKET_PATH;
16+
17+
const GREEN = "#22C55E";
18+
const AMBER = "#F59E0B";
19+
const PURPLE = "#8B5CF6";
20+
const BLUE = "#3B82F6";
21+
const GRAY = "#6B7280";
22+
23+
/** Status keys owned by this extension — cleared on shutdown. */
24+
const STATUS_KEYS = [
25+
"pi_state",
26+
"pi_model",
27+
"pi_thinking",
28+
"pi_tokens",
29+
"pi_cost",
30+
"pi_tool",
31+
];
32+
33+
/** Format a token count into a compact human-readable string. */
34+
function formatTokens(n: number): string {
35+
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
36+
if (n >= 10_000) return `${Math.round(n / 1000)}k`;
37+
if (n >= 1_000) return `${(n / 1000).toFixed(1)}k`;
38+
return String(n);
39+
}
40+
41+
/** Format a cost value as a dollar string. */
42+
function formatCost(n: number): string {
43+
return `$${n.toFixed(2)}`;
44+
}
45+
46+
/** Strip common prefixes/suffixes from model IDs for display. */
47+
function shortModel(id: string): string {
48+
return id.replace(/^claude-/, "").replace(/-\d{8}$/, "");
49+
}
50+
51+
/** cmux sidebar integration — pushes agent state to cmux when running inside it. */
52+
export default function cmuxExtension(pi: ExtensionAPI): void {
53+
if (!CMUX_SOCKET) return;
54+
55+
let sessionCost = 0;
56+
let hasUI = false;
57+
58+
function run(...args: string[]): void {
59+
if (!hasUI) return;
60+
pi.exec("cmux", args, { timeout: 2000 }).catch(() => {});
61+
}
62+
63+
function setStatus(key: string, value: string, icon: string, color: string): void {
64+
run("set-status", key, value, "--icon", icon, "--color", color);
65+
}
66+
67+
function clearStatus(key: string): void {
68+
run("clear-status", key);
69+
}
70+
71+
// --- Session lifecycle ---
72+
73+
pi.on("session_start", async (_event, ctx) => {
74+
hasUI = ctx.hasUI;
75+
if (!hasUI) return;
76+
77+
// Reconstruct session cost from existing entries
78+
sessionCost = 0;
79+
for (const entry of ctx.sessionManager.getBranch()) {
80+
if (
81+
entry.type === "message" &&
82+
entry.message.role === "assistant" &&
83+
(entry.message as any).usage?.cost?.total
84+
) {
85+
sessionCost += (entry.message as any).usage.cost.total;
86+
}
87+
}
88+
89+
setStatus("pi_state", "Idle", "checkmark.circle", GREEN);
90+
91+
if (ctx.model?.id) {
92+
setStatus("pi_model", shortModel(ctx.model.id), "brain", PURPLE);
93+
}
94+
95+
const thinking = pi.getThinkingLevel();
96+
if (thinking && thinking !== "off") {
97+
setStatus("pi_thinking", thinking, "sparkles", AMBER);
98+
}
99+
100+
if (sessionCost > 0) {
101+
setStatus("pi_cost", formatCost(sessionCost), "dollarsign.circle", GREEN);
102+
}
103+
104+
const usage = ctx.getContextUsage();
105+
if (usage && usage.tokens > 0) {
106+
setStatus("pi_tokens", formatTokens(usage.tokens), "number", BLUE);
107+
}
108+
});
109+
110+
pi.on("session_shutdown", async (_event, ctx) => {
111+
if (!ctx.hasUI) return;
112+
for (const key of STATUS_KEYS) {
113+
clearStatus(key);
114+
}
115+
});
116+
117+
// --- Agent working state ---
118+
119+
pi.on("agent_start", async (_event, ctx) => {
120+
if (!ctx.hasUI) return;
121+
setStatus("pi_state", "Working", "arrow.circlepath", AMBER);
122+
});
123+
124+
pi.on("agent_end", async (_event, ctx) => {
125+
if (!ctx.hasUI) return;
126+
setStatus("pi_state", "Idle", "checkmark.circle", GREEN);
127+
clearStatus("pi_tool");
128+
129+
const usage = ctx.getContextUsage();
130+
if (usage && usage.tokens > 0) {
131+
setStatus("pi_tokens", formatTokens(usage.tokens), "number", BLUE);
132+
}
133+
134+
if (sessionCost > 0) {
135+
setStatus("pi_cost", formatCost(sessionCost), "dollarsign.circle", GREEN);
136+
}
137+
138+
// Notify user — empty body triggers blue ring + tab highlight
139+
run("notify", "--title", "Needs attention");
140+
});
141+
142+
// --- Turn tracking (tokens + cost) ---
143+
144+
pi.on("turn_end", async (event, ctx) => {
145+
if (!ctx.hasUI) return;
146+
147+
const msg = event.message;
148+
if (msg?.role === "assistant" && (msg as any).usage?.cost?.total) {
149+
sessionCost += (msg as any).usage.cost.total;
150+
setStatus("pi_cost", formatCost(sessionCost), "dollarsign.circle", GREEN);
151+
}
152+
153+
const usage = ctx.getContextUsage();
154+
if (usage && usage.tokens > 0) {
155+
setStatus("pi_tokens", formatTokens(usage.tokens), "number", BLUE);
156+
}
157+
});
158+
159+
// --- Model / thinking changes ---
160+
161+
pi.on("model_select", async (event, ctx) => {
162+
if (!ctx.hasUI) return;
163+
setStatus("pi_model", shortModel(event.model.id), "brain", PURPLE);
164+
const thinking = pi.getThinkingLevel();
165+
setStatus(
166+
"pi_thinking",
167+
thinking === "off" ? "off" : thinking,
168+
"sparkles",
169+
thinking === "off" ? GRAY : AMBER,
170+
);
171+
});
172+
173+
// --- Tool execution tracking ---
174+
175+
pi.on("tool_execution_start", async (event, ctx) => {
176+
if (!ctx.hasUI) return;
177+
setStatus("pi_tool", event.toolName, "wrench", GRAY);
178+
});
179+
180+
pi.on("tool_execution_end", async (_event, ctx) => {
181+
if (!ctx.hasUI) return;
182+
clearStatus("pi_tool");
183+
});
184+
}

pi-extensions/terminal-progress.ts

Lines changed: 0 additions & 64 deletions
This file was deleted.

0 commit comments

Comments
 (0)