@braintrust/pi-extension 0.1.0 → 0.3.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
@@ -108,6 +108,8 @@ Example:
108
108
  | `additional_metadata` | `BRAINTRUST_ADDITIONAL_METADATA` | `{}` |
109
109
  | `log_file` | `BRAINTRUST_LOG_FILE` | unset |
110
110
  | `state_dir` | `BRAINTRUST_STATE_DIR` | `~/.pi/agent/state/braintrust-pi-extension` |
111
+ | `show_ui` | `BRAINTRUST_SHOW_UI` | `true` |
112
+ | `show_trace_link` | `BRAINTRUST_SHOW_TRACE_LINK` | `true` |
111
113
  | `parent_span_id` | `PI_PARENT_SPAN_ID` | unset |
112
114
  | `root_span_id` | `PI_ROOT_SPAN_ID` | unset |
113
115
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@braintrust/pi-extension",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Braintrust extension for pi. Includes automatic tracing for pi sessions, turns, LLM calls, and tool executions to Braintrust.",
5
5
  "keywords": [
6
6
  "braintrust",
@@ -24,17 +24,16 @@
24
24
  "access": "public"
25
25
  },
26
26
  "dependencies": {
27
- "braintrust": "^3.7.0",
27
+ "braintrust": "^3.8.0",
28
28
  "valibot": "^1.3.1"
29
29
  },
30
30
  "devDependencies": {
31
- "@mariozechner/pi-ai": "^0.64.0",
32
- "@mariozechner/pi-coding-agent": "^0.64.0",
33
- "@types/node": "^25.5.0",
34
- "tsx": "^4.21.0",
31
+ "@mariozechner/pi-ai": "^0.66.1",
32
+ "@mariozechner/pi-coding-agent": "^0.66.1",
33
+ "@types/node": "^25.6.0",
35
34
  "typescript": "^6.0.2",
36
- "vite-plus": "^0.1.14",
37
- "vitest": "^4.1.2"
35
+ "vite-plus": "^0.1.16",
36
+ "vitest": "^4.1.4"
38
37
  },
39
38
  "peerDependencies": {
40
39
  "@mariozechner/pi-coding-agent": "*"
@@ -66,6 +65,6 @@
66
65
  "test:integration": "vitest run src/index.integration.test.ts",
67
66
  "test:watch": "vitest",
68
67
  "typecheck": "vp check",
69
- "smoke": "tsx -e \"import('./src/index.ts')\""
68
+ "smoke": "vp exec node -e \"import('./src/index.ts')\""
70
69
  }
71
70
  }
@@ -16,6 +16,8 @@ const ENV_KEYS = [
16
16
  "BRAINTRUST_DEBUG",
17
17
  "BRAINTRUST_LOG_FILE",
18
18
  "BRAINTRUST_STATE_DIR",
19
+ "BRAINTRUST_SHOW_UI",
20
+ "BRAINTRUST_SHOW_TRACE_LINK",
19
21
  "PI_PARENT_SPAN_ID",
20
22
  "PI_ROOT_SPAN_ID",
21
23
  "BRAINTRUST_ADDITIONAL_METADATA",
@@ -257,6 +259,53 @@ describe("loadConfig", () => {
257
259
  });
258
260
  });
259
261
 
262
+ it("defaults showUi and showTraceLink to true", () => {
263
+ const home = makeTempDir("pi-extension-home-");
264
+ process.env.HOME = home;
265
+ process.env.BRAINTRUST_STATE_DIR = join(home, "state");
266
+
267
+ const config = loadConfig(home);
268
+
269
+ expect(config.showUi).toBe(true);
270
+ expect(config.showTraceLink).toBe(true);
271
+ });
272
+
273
+ it("applies show_ui and show_trace_link from config files", () => {
274
+ const home = makeTempDir("pi-extension-home-");
275
+ const cwd = join(home, "workspace");
276
+
277
+ process.env.HOME = home;
278
+ process.env.BRAINTRUST_STATE_DIR = join(home, "state");
279
+
280
+ writeJson(join(home, ".pi", "agent", "braintrust.json"), {
281
+ show_ui: false,
282
+ });
283
+
284
+ const config = loadConfig(cwd);
285
+
286
+ expect(config.showUi).toBe(false);
287
+ expect(config.showTraceLink).toBe(true);
288
+ });
289
+
290
+ it("overrides show_ui and show_trace_link from environment variables", () => {
291
+ const home = makeTempDir("pi-extension-home-");
292
+ const cwd = join(home, "workspace");
293
+
294
+ process.env.HOME = home;
295
+ process.env.BRAINTRUST_STATE_DIR = join(home, "state");
296
+ process.env.BRAINTRUST_SHOW_UI = "true";
297
+ process.env.BRAINTRUST_SHOW_TRACE_LINK = "false";
298
+
299
+ writeJson(join(home, ".pi", "agent", "braintrust.json"), {
300
+ show_ui: false,
301
+ });
302
+
303
+ const config = loadConfig(cwd);
304
+
305
+ expect(config.showUi).toBe(true);
306
+ expect(config.showTraceLink).toBe(false);
307
+ });
308
+
260
309
  it("warns when tracing is enabled without an API key", () => {
261
310
  const home = makeTempDir("pi-extension-home-");
262
311
  process.env.HOME = home;
@@ -290,6 +339,8 @@ describe("createLogger", () => {
290
339
  additionalMetadata: {},
291
340
  parentSpanId: undefined,
292
341
  rootSpanId: undefined,
342
+ showUi: true,
343
+ showTraceLink: true,
293
344
  configIssues: [],
294
345
  };
295
346
 
package/src/config.ts CHANGED
@@ -218,6 +218,17 @@ function applyConfig(
218
218
  );
219
219
  if (additionalMetadata !== undefined) target.additionalMetadata = additionalMetadata;
220
220
 
221
+ const showUi = validateOptionalBoolean(source.show_ui, issues, path, "show_ui");
222
+ if (showUi !== undefined) target.showUi = showUi;
223
+
224
+ const showTraceLink = validateOptionalBoolean(
225
+ source.show_trace_link,
226
+ issues,
227
+ path,
228
+ "show_trace_link",
229
+ );
230
+ if (showTraceLink !== undefined) target.showTraceLink = showTraceLink;
231
+
221
232
  return {
222
233
  parentSpanConfigured: parentSpanId !== undefined,
223
234
  rootSpanConfigured: rootSpanId !== undefined,
@@ -238,6 +249,8 @@ export function loadConfig(cwd = process.cwd()): TraceConfig {
238
249
  additionalMetadata: {},
239
250
  parentSpanId: undefined,
240
251
  rootSpanId: undefined,
252
+ showUi: true,
253
+ showTraceLink: true,
241
254
  configIssues: [],
242
255
  };
243
256
 
@@ -344,6 +357,22 @@ export function loadConfig(cwd = process.cwd()): TraceConfig {
344
357
  );
345
358
  if (envStateDir !== undefined) config.stateDir = envStateDir;
346
359
 
360
+ const envShowUi = validateOptionalBoolean(
361
+ process.env.BRAINTRUST_SHOW_UI,
362
+ config.configIssues,
363
+ "BRAINTRUST_SHOW_UI",
364
+ "BRAINTRUST_SHOW_UI",
365
+ );
366
+ if (envShowUi !== undefined) config.showUi = envShowUi;
367
+
368
+ const envShowTraceLink = validateOptionalBoolean(
369
+ process.env.BRAINTRUST_SHOW_TRACE_LINK,
370
+ config.configIssues,
371
+ "BRAINTRUST_SHOW_TRACE_LINK",
372
+ "BRAINTRUST_SHOW_TRACE_LINK",
373
+ );
374
+ if (envShowTraceLink !== undefined) config.showTraceLink = envShowTraceLink;
375
+
347
376
  const envParentSpanId = validateOptionalNonEmptyString(
348
377
  process.env.PI_PARENT_SPAN_ID,
349
378
  config.configIssues,
@@ -433,6 +433,9 @@ async function createHarness(options?: {
433
433
  return { agentDir, cwd, session, stateDir };
434
434
  }
435
435
 
436
+ // TODO: Remove this legacy fallback once our supported pi compatibility window no
437
+ // longer includes pi <0.65.0, which introduced the session runtime API and the
438
+ // session_start-only post-transition model.
436
439
  const resourceLoader = new DefaultResourceLoader({
437
440
  cwd,
438
441
  agentDir,
@@ -447,26 +450,32 @@ async function createHarness(options?: {
447
450
  resourceLoader,
448
451
  sessionManager,
449
452
  });
453
+ const legacyRuntimeSession = legacySession as typeof legacySession & {
454
+ newSession(): Promise<boolean>;
455
+ switchSession(sessionPath: string): Promise<boolean>;
456
+ fork(entryId: string): Promise<{ cancelled: boolean; selectedText?: string }>;
457
+ sessionManager: SessionManager;
458
+ };
450
459
 
451
460
  const session: TestSessionController = {
452
- prompt: (text) => legacySession.prompt(text),
453
- newSession: () => legacySession.newSession(),
454
- switchSession: (sessionPath) => legacySession.switchSession(sessionPath),
461
+ prompt: (text) => legacyRuntimeSession.prompt(text),
462
+ newSession: () => legacyRuntimeSession.newSession(),
463
+ switchSession: (sessionPath) => legacyRuntimeSession.switchSession(sessionPath),
455
464
  fork: async (entryId) => {
456
- const result = await legacySession.fork(entryId);
465
+ const result = await legacyRuntimeSession.fork(entryId);
457
466
  return {
458
467
  cancelled: result.cancelled,
459
- selectedText: result.selectedText,
468
+ selectedText: result.selectedText ?? "",
460
469
  };
461
470
  },
462
471
  dispose: async () => {
463
- legacySession.dispose();
472
+ legacyRuntimeSession.dispose();
464
473
  },
465
474
  get sessionFile() {
466
- return legacySession.sessionFile;
475
+ return legacyRuntimeSession.sessionFile;
467
476
  },
468
477
  get sessionManager() {
469
- return legacySession.sessionManager;
478
+ return legacyRuntimeSession.sessionManager;
470
479
  },
471
480
  };
472
481
 
package/src/index.test.ts CHANGED
@@ -22,6 +22,8 @@ const mockState = vi.hoisted(() => ({
22
22
  additionalMetadata: {},
23
23
  parentSpanId: undefined,
24
24
  rootSpanId: undefined,
25
+ showUi: true,
26
+ showTraceLink: true,
25
27
  configIssues: [] as Array<{ path: string; message: string; severity: "error" | "warning" }>,
26
28
  },
27
29
  }));
@@ -117,6 +119,8 @@ beforeEach(() => {
117
119
  additionalMetadata: {},
118
120
  parentSpanId: undefined,
119
121
  rootSpanId: undefined,
122
+ showUi: true,
123
+ showTraceLink: true,
120
124
  configIssues: [],
121
125
  };
122
126
  vi.resetModules();
@@ -406,4 +410,49 @@ describe("braintrustPiExtension", () => {
406
410
  undefined,
407
411
  );
408
412
  });
413
+
414
+ it("hides all UI when showUi is false", async () => {
415
+ mockState.config.showUi = false;
416
+
417
+ const { emit } = await createHarness();
418
+
419
+ await emit("session_start");
420
+ await emit("before_agent_start", {
421
+ prompt: "Inspect the package",
422
+ images: [],
423
+ });
424
+
425
+ const statusUpdates = mockState.statuses.filter(
426
+ (s) => s.key === "braintrust-tracing" && s.text !== undefined,
427
+ );
428
+ const widgetUpdates = mockState.widgets.filter(
429
+ (w) => w.key === "braintrust-trace-link" && w.content !== undefined,
430
+ );
431
+
432
+ expect(statusUpdates).toEqual([]);
433
+ expect(widgetUpdates).toEqual([]);
434
+ });
435
+
436
+ it("hides just the trace link when showTraceLink is false", async () => {
437
+ mockState.config.showTraceLink = false;
438
+
439
+ const { emit } = await createHarness();
440
+
441
+ await emit("session_start");
442
+ await emit("before_agent_start", {
443
+ prompt: "Inspect the package",
444
+ images: [],
445
+ });
446
+
447
+ const statusUpdates = mockState.statuses.filter(
448
+ (s) => s.key === "braintrust-tracing" && s.text !== undefined,
449
+ );
450
+ expect(statusUpdates.length).toBeGreaterThan(0);
451
+ expect(statusUpdates[0]?.text).toContain("Braintrust");
452
+
453
+ const widgetUpdates = mockState.widgets.filter(
454
+ (w) => w.key === "braintrust-trace-link" && w.content !== undefined,
455
+ );
456
+ expect(widgetUpdates).toEqual([]);
457
+ });
409
458
  });
package/src/index.ts CHANGED
@@ -193,7 +193,10 @@ function setTracingStatus(
193
193
  configIssue?: ConfigIssue;
194
194
  },
195
195
  ): void {
196
- if (!ctx.hasUI) return;
196
+ if (!ctx.hasUI || !config.showUi) {
197
+ if (ctx.hasUI) ctx.ui.setStatus(TRACING_STATUS_KEY, undefined);
198
+ return;
199
+ }
197
200
 
198
201
  const theme = ctx.ui.theme;
199
202
 
@@ -269,15 +272,19 @@ function displayPath(path: string): string {
269
272
 
270
273
  function setTraceWidget(
271
274
  ctx: ExtensionContext,
275
+ config: TraceConfig,
272
276
  traceUrl: string | undefined,
273
277
  configIssue: ConfigIssue | undefined,
274
278
  ): void {
275
- if (!ctx.hasUI) return;
279
+ if (!ctx.hasUI || !config.showUi) {
280
+ if (ctx.hasUI) ctx.ui.setWidget(TRACING_WIDGET_KEY, undefined);
281
+ return;
282
+ }
276
283
 
277
284
  const theme = ctx.ui.theme;
278
285
  const lines: string[] = [];
279
286
 
280
- if (traceUrl) {
287
+ if (traceUrl && config.showTraceLink) {
281
288
  const label = makeHyperlink(
282
289
  traceUrl,
283
290
  theme.fg("accent", theme.underline("Braintrust trace ↗")),
@@ -334,7 +341,7 @@ export default function braintrustPiExtension(pi: ExtensionAPI): void {
334
341
  missingApiKey: Boolean(config.enabled && !config.apiKey),
335
342
  configIssue,
336
343
  });
337
- setTraceWidget(ctx, activeSession?.traceUrl, configIssue);
344
+ setTraceWidget(ctx, config, activeSession?.traceUrl, configIssue);
338
345
  }
339
346
 
340
347
  function persistTraceUrl(session: ActiveSession, traceUrl: string): void {
@@ -615,12 +622,21 @@ export default function braintrustPiExtension(pi: ExtensionAPI): void {
615
622
  });
616
623
  });
617
624
 
618
- pi.on("session_switch", async (event, ctx) => {
625
+ // TODO: Remove these legacy transition listeners once our compatibility window
626
+ // no longer includes pi <0.65.0.
627
+ const legacyPi = pi as ExtensionAPI & {
628
+ on(
629
+ event: "session_switch" | "session_fork",
630
+ handler: (event: unknown, ctx: ExtensionContext) => Promise<void> | void,
631
+ ): void;
632
+ };
633
+
634
+ legacyPi.on("session_switch", async (event, ctx) => {
619
635
  refreshTracingUi(ctx);
620
636
  await rolloverSession(ctx, "session_switch", getPreviousSessionFile(event));
621
637
  });
622
638
 
623
- pi.on("session_fork", async (event, ctx) => {
639
+ legacyPi.on("session_fork", async (event, ctx) => {
624
640
  refreshTracingUi(ctx);
625
641
  await rolloverSession(ctx, "session_fork", getPreviousSessionFile(event));
626
642
  });
package/src/types.ts CHANGED
@@ -26,6 +26,8 @@ export interface TraceConfig {
26
26
  additionalMetadata: JsonObject;
27
27
  parentSpanId?: string;
28
28
  rootSpanId?: string;
29
+ showUi: boolean;
30
+ showTraceLink: boolean;
29
31
  configIssues: ConfigIssue[];
30
32
  }
31
33