@narumitw/pi-langfuse 0.35.0 → 0.39.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
@@ -144,7 +144,11 @@ Completed observations are exported in batches while Pi remains live. Neither `a
144
144
  /langfuse
145
145
  ```
146
146
 
147
- The command opens one context-aware menu. Its title shows the current session's tracing state, endpoint, content-capture mode, initialization failure when applicable, and private configuration path. It never displays credentials.
147
+ The command opens one context-aware standard menu in TUI or RPC mode. Its state lines show the
148
+ current session's tracing state, endpoint, content-capture mode, initialization failure when
149
+ applicable, and private configuration path. It never displays credentials. Escape closes the menu.
150
+ Credential and endpoint text inputs remain extension-owned because they enforce secret-preserving
151
+ setup/update semantics rather than standard navigation.
148
152
 
149
153
  Available actions depend on that state:
150
154
 
@@ -153,7 +157,10 @@ Available actions depend on that state:
153
157
  - **Update Langfuse for this Pi agent directory** appears when a valid config exists.
154
158
  - **Show setup and privacy help** explains the agent-directory scope, manual configuration path, and content-capture risk.
155
159
 
156
- Connection actions state their agent-directory scope and per-process restart requirement before selection. Command arguments are intentionally ignored so remembered subcommands cannot silently bypass the menu. In non-interactive modes, the command reports that the menu is unavailable and points to the manual config path.
160
+ Connection actions state their agent-directory scope and per-process restart requirement before
161
+ selection. Command arguments are intentionally ignored so remembered subcommands cannot silently
162
+ bypass the menu. Print and JSON modes reject the interactive command with an observable error that
163
+ includes current tracing state and the manual configuration path.
157
164
 
158
165
  ## 🔐 Privacy
159
166
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@narumitw/pi-langfuse",
3
- "version": "0.35.0",
3
+ "version": "0.39.0",
4
4
  "description": "Pi extension that traces LLM generations and tool activity to Langfuse.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -31,15 +31,16 @@
31
31
  "dependencies": {
32
32
  "@langfuse/otel": "^5.9.1",
33
33
  "@langfuse/tracing": "^5.9.1",
34
+ "@narumitw/pi-tui-kit": "<1",
34
35
  "@opentelemetry/api": "^1.9.1",
35
36
  "@opentelemetry/exporter-trace-otlp-http": "^0.221.0",
36
37
  "@opentelemetry/sdk-trace-base": "^2.10.0",
37
38
  "@opentelemetry/sdk-trace-node": "^2.10.0"
38
39
  },
39
40
  "devDependencies": {
40
- "@biomejs/biome": "2.5.5",
41
- "@earendil-works/pi-coding-agent": "0.82.1",
42
- "@types/node": "26.1.1",
41
+ "@biomejs/biome": "2.5.6",
42
+ "@earendil-works/pi-coding-agent": "0.83.0",
43
+ "@types/node": "26.1.2",
43
44
  "typescript": "7.0.2"
44
45
  },
45
46
  "repository": {
package/src/langfuse.ts CHANGED
@@ -3,6 +3,7 @@ import type {
3
3
  ExtensionCommandContext,
4
4
  ExtensionContext,
5
5
  } from "@earendil-works/pi-coding-agent";
6
+ import { defineMenu, runMenu } from "@narumitw/pi-tui-kit";
6
7
  import {
7
8
  DEFAULT_BASE_URL,
8
9
  type LangfuseConfig,
@@ -55,89 +56,118 @@ export function createLangfuseExtension(
55
56
  let hasStoredConfig = false;
56
57
  let configurationNotice: string | undefined;
57
58
  let sessionGeneration = 0;
59
+ let menuController = new AbortController();
58
60
  let configWriteQueue = Promise.resolve();
59
61
  let nextAttemptReason: string | undefined;
60
62
 
61
- pi.registerCommand("langfuse", {
62
- description: "Open interactive Langfuse tracing controls",
63
- handler: async (_args, ctx) => {
64
- if (!ctx.hasUI) {
65
- ctx.ui.notify(
66
- formatNonInteractiveStatus(activeConfig, configPath, initializationError),
67
- "warning",
68
- );
69
- return;
70
- }
71
-
72
- const menuGeneration = sessionGeneration;
73
- const menuRecorder = recorder;
74
- const menuConfig = activeConfig;
75
- const choice = await ctx.ui.select(
76
- formatMenuTitle(activeConfig, configPath, initializationError, configurationNotice),
77
- [
78
- ...(menuRecorder ? [FLUSH_ACTION] : []),
79
- hasStoredConfig ? UPDATE_ACTION : SET_UP_ACTION,
80
- HELP_ACTION,
81
- ],
63
+ async function showLangfuseMenu(ctx: ExtensionCommandContext) {
64
+ if (!ctx.hasUI) {
65
+ ctx.ui.notify(
66
+ formatNonInteractiveStatus(activeConfig, configPath, initializationError),
67
+ "warning",
82
68
  );
83
- if (!choice || menuGeneration !== sessionGeneration) return;
84
-
85
- if (choice === FLUSH_ACTION) {
86
- if (!menuRecorder) {
87
- ctx.ui.notify("Langfuse tracing is not enabled for this session.", "warning");
88
- return;
89
- }
90
- try {
91
- await menuRecorder.flush();
92
- if (menuGeneration !== sessionGeneration) return;
93
- ctx.ui.notify("Langfuse traces flushed for this session.", "info");
94
- } catch (error) {
95
- if (menuGeneration !== sessionGeneration) return;
96
- ctx.ui.notify(`Langfuse flush failed: ${formatError(error, menuConfig)}`, "error");
97
- }
98
- return;
99
- }
100
-
101
- if (choice === SET_UP_ACTION || choice === UPDATE_ACTION) {
102
- const loaded = await loadConfig(configPath);
103
- if (menuGeneration !== sessionGeneration) return;
104
- configPath = loaded.path;
105
- const next = await promptForConfig(
106
- ctx,
107
- loaded.ok ? loaded.config : undefined,
108
- () => menuGeneration === sessionGeneration,
109
- );
110
- if (!next || menuGeneration !== sessionGeneration) return;
111
- try {
112
- const write = configWriteQueue.then(() => writeConfig(next, loaded.path));
113
- configWriteQueue = write.then(
114
- () => undefined,
115
- () => undefined,
116
- );
117
- await write;
118
- if (menuGeneration !== sessionGeneration) return;
119
- hasStoredConfig = true;
120
- configurationNotice =
121
- "Saved; restart each Pi process to use it in subsequent sessions.";
122
- ctx.ui.notify(
123
- `Saved Langfuse config to ${loaded.path} for this Pi agent directory. Restart each Pi process to apply it to subsequent sessions.`,
124
- "info",
69
+ return;
70
+ }
71
+ const menuGeneration = sessionGeneration;
72
+ type Screen = "main";
73
+ type Action = "flush" | "configure" | "help";
74
+ const menu = defineMenu<undefined, Screen, Action>({
75
+ start: "main",
76
+ screens: {
77
+ main: () => ({
78
+ kind: "actions",
79
+ title: "Langfuse",
80
+ lines: formatMenuTitle(
81
+ activeConfig,
82
+ configPath,
83
+ initializationError,
84
+ configurationNotice,
85
+ )
86
+ .split("\n")
87
+ .slice(1),
88
+ items: [
89
+ ...(recorder ? [{ id: "flush", label: FLUSH_ACTION, action: "flush" as const }] : []),
90
+ {
91
+ id: "configure",
92
+ label: hasStoredConfig ? UPDATE_ACTION : SET_UP_ACTION,
93
+ action: "configure",
94
+ },
95
+ { id: "help", label: HELP_ACTION, action: "help" },
96
+ ],
97
+ hint: "close",
98
+ }),
99
+ },
100
+ actions: {
101
+ flush: async () => {
102
+ const menuRecorder = recorder;
103
+ const menuConfig = activeConfig;
104
+ if (!menuRecorder) {
105
+ ctx.ui.notify("Langfuse tracing is not enabled for this session.", "warning");
106
+ return { kind: "close" };
107
+ }
108
+ try {
109
+ await menuRecorder.flush();
110
+ if (menuGeneration !== sessionGeneration) return { kind: "close" };
111
+ ctx.ui.notify("Langfuse traces flushed for this session.", "info");
112
+ } catch (error) {
113
+ if (menuGeneration !== sessionGeneration) return { kind: "close" };
114
+ ctx.ui.notify(`Langfuse flush failed: ${formatError(error, menuConfig)}`, "error");
115
+ }
116
+ return { kind: "close" };
117
+ },
118
+ configure: async () => {
119
+ const loaded = await loadConfig(configPath);
120
+ if (menuGeneration !== sessionGeneration) return { kind: "close" };
121
+ configPath = loaded.path;
122
+ const next = await promptForConfig(
123
+ ctx,
124
+ loaded.ok ? loaded.config : undefined,
125
+ () => menuGeneration === sessionGeneration,
125
126
  );
126
- } catch (error) {
127
- if (menuGeneration !== sessionGeneration) return;
128
- ctx.ui.notify(`Failed to save Langfuse config: ${formatError(error, next)}`, "error");
129
- }
130
- return;
131
- }
127
+ if (!next || menuGeneration !== sessionGeneration) return { kind: "close" };
128
+ try {
129
+ const write = configWriteQueue.then(() => writeConfig(next, loaded.path));
130
+ configWriteQueue = write.then(
131
+ () => undefined,
132
+ () => undefined,
133
+ );
134
+ await write;
135
+ if (menuGeneration !== sessionGeneration) return { kind: "close" };
136
+ hasStoredConfig = true;
137
+ configurationNotice =
138
+ "Saved; restart each Pi process to use it in subsequent sessions.";
139
+ ctx.ui.notify(
140
+ `Saved Langfuse config to ${loaded.path} for this Pi agent directory. Restart each Pi process to apply it to subsequent sessions.`,
141
+ "info",
142
+ );
143
+ } catch (error) {
144
+ if (menuGeneration !== sessionGeneration) return { kind: "close" };
145
+ ctx.ui.notify(`Failed to save Langfuse config: ${formatError(error, next)}`, "error");
146
+ }
147
+ return { kind: "close" };
148
+ },
149
+ help: async () => {
150
+ ctx.ui.notify(formatHelp(configPath), "info");
151
+ return { kind: "close" };
152
+ },
153
+ },
154
+ });
155
+ await runMenu(ctx, menu, {
156
+ getState: () => undefined,
157
+ signal: menuController.signal,
158
+ isCurrent: () => menuGeneration === sessionGeneration,
159
+ });
160
+ }
132
161
 
133
- if (choice === HELP_ACTION) {
134
- ctx.ui.notify(formatHelp(configPath), "info");
135
- }
136
- },
162
+ pi.registerCommand("langfuse", {
163
+ description: "Open interactive Langfuse tracing controls",
164
+ handler: async (_args, ctx) => showLangfuseMenu(ctx),
137
165
  });
138
166
 
139
167
  pi.on("session_start", async (_event, ctx) => {
140
168
  const generation = ++sessionGeneration;
169
+ menuController.abort(new DOMException("Langfuse session replaced", "AbortError"));
170
+ menuController = new AbortController();
141
171
  recorder = undefined;
142
172
  activeConfig = undefined;
143
173
  configPath = undefined;
@@ -300,6 +330,7 @@ export function createLangfuseExtension(
300
330
 
301
331
  pi.on("session_shutdown", async (event, ctx) => {
302
332
  const generation = ++sessionGeneration;
333
+ menuController.abort(new DOMException("Langfuse session shut down", "AbortError"));
303
334
  const activeRecorder = recorder;
304
335
  const shutdownConfig = activeConfig;
305
336
  const pendingConfigWrite = configWriteQueue;