pi-fluency 0.1.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/LICENSE +21 -0
- package/README.md +171 -0
- package/extensions/pi-fluency/analytics.ts +253 -0
- package/extensions/pi-fluency/analyzer.ts +171 -0
- package/extensions/pi-fluency/collector.ts +102 -0
- package/extensions/pi-fluency/context.ts +65 -0
- package/extensions/pi-fluency/diff.ts +147 -0
- package/extensions/pi-fluency/generation-marker.ts +51 -0
- package/extensions/pi-fluency/history-codec.ts +266 -0
- package/extensions/pi-fluency/index.ts +459 -0
- package/extensions/pi-fluency/overlay.ts +637 -0
- package/extensions/pi-fluency/retention.ts +48 -0
- package/extensions/pi-fluency/sanitize.ts +40 -0
- package/extensions/pi-fluency/setup.ts +29 -0
- package/extensions/pi-fluency/state-reducer.ts +192 -0
- package/extensions/pi-fluency/status.ts +39 -0
- package/extensions/pi-fluency/store.ts +589 -0
- package/extensions/pi-fluency/taxonomy.ts +73 -0
- package/extensions/pi-fluency/types.ts +138 -0
- package/extensions/pi-fluency/worker.ts +144 -0
- package/package.json +63 -0
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import type {
|
|
5
|
+
ExtensionAPI,
|
|
6
|
+
ExtensionCommandContext,
|
|
7
|
+
ExtensionContext,
|
|
8
|
+
} from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import { Key } from "@earendil-works/pi-tui";
|
|
10
|
+
import { AnalyzerConfigurationError, ModelAnalyzer, type Analyzer } from "./analyzer.js";
|
|
11
|
+
import { computeFluencyAnalytics } from "./analytics.js";
|
|
12
|
+
import { collectPrompt } from "./collector.js";
|
|
13
|
+
import { showFluencyOverlay, type FluencyView } from "./overlay.js";
|
|
14
|
+
import { sanitizeTerminalLabel } from "./sanitize.js";
|
|
15
|
+
import { runSetup } from "./setup.js";
|
|
16
|
+
import { formatStatus, type StatusErrorReason, type StatusState } from "./status.js";
|
|
17
|
+
import { FluencyStore } from "./store.js";
|
|
18
|
+
import type { FluencySettings } from "./types.js";
|
|
19
|
+
import { FluencyWorker } from "./worker.js";
|
|
20
|
+
|
|
21
|
+
const STATUS_KEY = "pi-fluency";
|
|
22
|
+
const USAGE = "Usage: /fluency [pause|resume|status|model|clear|stats]";
|
|
23
|
+
|
|
24
|
+
export interface OpenInboxOptions {
|
|
25
|
+
signal: AbortSignal;
|
|
26
|
+
initialView?: FluencyView;
|
|
27
|
+
onProgressChanged?: () => void;
|
|
28
|
+
onMutationError?: (error: unknown) => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Overlay seam shared by command and shortcut contexts; implementations need only common UI/mode APIs. */
|
|
32
|
+
export type OpenInbox = (
|
|
33
|
+
ctx: ExtensionContext,
|
|
34
|
+
store: FluencyStore,
|
|
35
|
+
options: OpenInboxOptions,
|
|
36
|
+
) => Promise<void> | void;
|
|
37
|
+
|
|
38
|
+
export interface ExtensionDependencies {
|
|
39
|
+
rootDir?: string;
|
|
40
|
+
analyzerFactory?: (ctx: ExtensionContext, store: FluencyStore) => Analyzer;
|
|
41
|
+
now?: () => number;
|
|
42
|
+
openInbox?: OpenInbox;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface ResolvedDependencies extends ExtensionDependencies {
|
|
46
|
+
rootDir: string;
|
|
47
|
+
now: () => number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function hasConfiguredIdentity(settings: FluencySettings): boolean {
|
|
51
|
+
return settings.enabled
|
|
52
|
+
&& typeof settings.consentedAt === "number"
|
|
53
|
+
&& Number.isFinite(settings.consentedAt)
|
|
54
|
+
&& settings.consentedAt > 0
|
|
55
|
+
&& typeof settings.provider === "string"
|
|
56
|
+
&& settings.provider.trim().length > 0
|
|
57
|
+
&& typeof settings.modelId === "string"
|
|
58
|
+
&& settings.modelId.trim().length > 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function hasValidConfiguration(settings: FluencySettings, ctx: ExtensionContext): boolean {
|
|
62
|
+
if (!hasConfiguredIdentity(settings)) return false;
|
|
63
|
+
return ctx.modelRegistry.find(settings.provider!, settings.modelId!) !== undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function registerHandlers(pi: ExtensionAPI, dependencies: ResolvedDependencies): void {
|
|
67
|
+
let storeRef: FluencyStore | undefined;
|
|
68
|
+
let storePromise: Promise<FluencyStore> | undefined;
|
|
69
|
+
let workerRef: FluencyWorker | undefined;
|
|
70
|
+
let ctxRef: ExtensionContext | undefined;
|
|
71
|
+
let shutdownPromise: Promise<void> | undefined;
|
|
72
|
+
let shuttingDown = false;
|
|
73
|
+
let overlayOpen: Promise<void> | undefined;
|
|
74
|
+
let overlayController: AbortController | undefined;
|
|
75
|
+
const notifiedErrors = new Set<string>();
|
|
76
|
+
const inputSessionId = randomUUID();
|
|
77
|
+
let inputSequence = 0;
|
|
78
|
+
|
|
79
|
+
const publishStatus = (ctx: ExtensionContext, state: StatusState): void => {
|
|
80
|
+
const text = formatStatus(state);
|
|
81
|
+
ctx.ui.setStatus(STATUS_KEY, text);
|
|
82
|
+
if (text === undefined) {
|
|
83
|
+
pi.events.emit("powerbar:update", { id: STATUS_KEY, text: undefined });
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const separator = text.indexOf(" ");
|
|
87
|
+
const icon = separator < 0 ? text : text.slice(0, separator);
|
|
88
|
+
const powerbarText = separator < 0 ? "" : text.slice(separator + 1);
|
|
89
|
+
const color = state.kind === "initial-loading"
|
|
90
|
+
? "muted"
|
|
91
|
+
: state.kind === "error"
|
|
92
|
+
? "error"
|
|
93
|
+
: state.kind === "progress" && state.pendingOccurrences > 0 ? "warning" : "success";
|
|
94
|
+
pi.events.emit("powerbar:update", { id: STATUS_KEY, text: powerbarText, icon, color });
|
|
95
|
+
};
|
|
96
|
+
const clearStatus = (ctx: ExtensionContext): void => publishStatus(ctx, { kind: "hidden" });
|
|
97
|
+
|
|
98
|
+
const notifyError = (ctx: ExtensionContext, error: unknown): void => {
|
|
99
|
+
const normalized = error instanceof Error ? error : new Error(String(error));
|
|
100
|
+
const detail = sanitizeTerminalLabel(normalized.message) || "Unknown error";
|
|
101
|
+
if (!notifiedErrors.has(detail)) {
|
|
102
|
+
notifiedErrors.add(detail);
|
|
103
|
+
ctx.ui.notify(`Pi Fluency: ${detail}`, "error");
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const setError = (ctx: ExtensionContext, reason: StatusErrorReason, error: unknown): void => {
|
|
108
|
+
const store = storeRef;
|
|
109
|
+
if (shuttingDown || !store || !store.getSettings().enabled) {
|
|
110
|
+
clearStatus(ctx);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
publishStatus(ctx, { kind: "error", reason });
|
|
114
|
+
notifyError(ctx, error);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const publishProgress = (ctx: ExtensionContext, store: FluencyStore): void => {
|
|
118
|
+
const migrationWarning = store.getWarnings().find((warning) => warning.toLowerCase().includes("migration"));
|
|
119
|
+
if (migrationWarning) {
|
|
120
|
+
setError(ctx, "migrate", new Error(migrationWarning));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const snapshot = store.getAnalyticsSnapshot();
|
|
124
|
+
const analytics = computeFluencyAnalytics({
|
|
125
|
+
observations: snapshot.observations,
|
|
126
|
+
occurrences: snapshot.occurrences,
|
|
127
|
+
patterns: snapshot.patterns,
|
|
128
|
+
ignoredPatternKeys: new Set(snapshot.ignoredPatternKeys),
|
|
129
|
+
ignoredCategories: new Set(snapshot.ignoredCategories),
|
|
130
|
+
now: dependencies.now(),
|
|
131
|
+
});
|
|
132
|
+
publishStatus(ctx, {
|
|
133
|
+
kind: "progress",
|
|
134
|
+
pendingOccurrences: analytics.pendingOccurrences,
|
|
135
|
+
activeRules: analytics.activeRules,
|
|
136
|
+
sparkline: analytics.toolbarSparkline,
|
|
137
|
+
ratePerThousand: analytics.currentRatePerThousand,
|
|
138
|
+
});
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const getStore = async (): Promise<FluencyStore> => {
|
|
142
|
+
storePromise ??= FluencyStore.open(dependencies.rootDir).then((store) => {
|
|
143
|
+
storeRef = store;
|
|
144
|
+
return store;
|
|
145
|
+
});
|
|
146
|
+
return storePromise;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const analyzerErrorReason = (error: unknown): StatusErrorReason => {
|
|
150
|
+
if (!(error instanceof AnalyzerConfigurationError)) return "analyze";
|
|
151
|
+
return /auth|api[- ]?key|credential|token/i.test(error.message) ? "auth" : "model";
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const publishConfigurationFailureOrClear = (ctx: ExtensionContext, store: FluencyStore): void => {
|
|
155
|
+
const settings = store.getSettings();
|
|
156
|
+
if (hasConfiguredIdentity(settings)
|
|
157
|
+
&& ctx.modelRegistry.find(settings.provider!, settings.modelId!) === undefined) {
|
|
158
|
+
setError(ctx, "model", new AnalyzerConfigurationError("Configured Pi Fluency model is unavailable"));
|
|
159
|
+
} else {
|
|
160
|
+
clearStatus(ctx);
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const createAnalyzer = (ctx: ExtensionContext, store: FluencyStore): Analyzer => {
|
|
165
|
+
if (dependencies.analyzerFactory) return dependencies.analyzerFactory(ctx, store);
|
|
166
|
+
const settings = store.getSettings();
|
|
167
|
+
const model = settings.provider && settings.modelId
|
|
168
|
+
? ctx.modelRegistry.find(settings.provider, settings.modelId)
|
|
169
|
+
: undefined;
|
|
170
|
+
if (!model) throw new AnalyzerConfigurationError("Configured Pi Fluency model is unavailable");
|
|
171
|
+
return new ModelAnalyzer({
|
|
172
|
+
model,
|
|
173
|
+
registry: ctx.modelRegistry,
|
|
174
|
+
minimumConfidence: settings.minimumConfidence,
|
|
175
|
+
});
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const getWorker = (ctx: ExtensionContext, store: FluencyStore): FluencyWorker => {
|
|
179
|
+
ctxRef = ctx;
|
|
180
|
+
workerRef ??= new FluencyWorker({
|
|
181
|
+
analyzer: createAnalyzer(ctx, store),
|
|
182
|
+
isIdle: () => ctxRef?.isIdle() ?? false,
|
|
183
|
+
getPatterns: () => store.listKnownPatterns(),
|
|
184
|
+
onResult: async (prompt, result) => {
|
|
185
|
+
try {
|
|
186
|
+
await store.appendAnalysis(prompt, result);
|
|
187
|
+
} catch (error) {
|
|
188
|
+
if (ctxRef) setError(ctxRef, "store", error);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const currentCtx = ctxRef;
|
|
192
|
+
if (shuttingDown) return;
|
|
193
|
+
if (currentCtx && hasValidConfiguration(store.getSettings(), currentCtx)) {
|
|
194
|
+
if (shuttingDown) return;
|
|
195
|
+
publishProgress(currentCtx, store);
|
|
196
|
+
} else if (currentCtx) {
|
|
197
|
+
clearStatus(currentCtx);
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
onError: (error) => {
|
|
201
|
+
if (!ctxRef) return;
|
|
202
|
+
if (!hasValidConfiguration(store.getSettings(), ctxRef)) {
|
|
203
|
+
publishConfigurationFailureOrClear(ctxRef, store);
|
|
204
|
+
} else {
|
|
205
|
+
setError(ctxRef, analyzerErrorReason(error), error);
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
onOverflow: () => undefined,
|
|
209
|
+
});
|
|
210
|
+
return workerRef;
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const openInbox = async (
|
|
214
|
+
ctx: ExtensionContext,
|
|
215
|
+
store: FluencyStore,
|
|
216
|
+
initialView: FluencyView = "inbox",
|
|
217
|
+
): Promise<void> => {
|
|
218
|
+
if (shuttingDown) return;
|
|
219
|
+
if (overlayOpen) {
|
|
220
|
+
await overlayOpen;
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const controller = new AbortController();
|
|
224
|
+
overlayController = controller;
|
|
225
|
+
const onProgressChanged = (): void => {
|
|
226
|
+
if (!shuttingDown && hasValidConfiguration(store.getSettings(), ctx)) publishProgress(ctx, store);
|
|
227
|
+
};
|
|
228
|
+
const onMutationError = (error: unknown): void => setError(ctx, "store", error);
|
|
229
|
+
const current = Promise.resolve(
|
|
230
|
+
dependencies.openInbox
|
|
231
|
+
? dependencies.openInbox(ctx, store, {
|
|
232
|
+
signal: controller.signal,
|
|
233
|
+
initialView,
|
|
234
|
+
onProgressChanged,
|
|
235
|
+
onMutationError,
|
|
236
|
+
})
|
|
237
|
+
: showFluencyOverlay(
|
|
238
|
+
ctx,
|
|
239
|
+
store,
|
|
240
|
+
controller.signal,
|
|
241
|
+
onProgressChanged,
|
|
242
|
+
onMutationError,
|
|
243
|
+
initialView,
|
|
244
|
+
dependencies.now,
|
|
245
|
+
),
|
|
246
|
+
);
|
|
247
|
+
overlayOpen = current;
|
|
248
|
+
try {
|
|
249
|
+
await current;
|
|
250
|
+
} finally {
|
|
251
|
+
if (overlayOpen === current) {
|
|
252
|
+
overlayOpen = undefined;
|
|
253
|
+
overlayController = undefined;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const selectModel = async (ctx: ExtensionCommandContext, store: FluencyStore): Promise<boolean> => {
|
|
259
|
+
const changed = await runSetup(ctx, store, { enable: false, now: dependencies.now });
|
|
260
|
+
if (!changed) return false;
|
|
261
|
+
await workerRef?.shutdown();
|
|
262
|
+
workerRef = undefined;
|
|
263
|
+
if (hasValidConfiguration(store.getSettings(), ctx)) publishProgress(ctx, store);
|
|
264
|
+
else clearStatus(ctx);
|
|
265
|
+
const settings = store.getSettings();
|
|
266
|
+
const provider = sanitizeTerminalLabel(settings.provider, 100) || "unknown-provider";
|
|
267
|
+
const modelId = sanitizeTerminalLabel(settings.modelId, 100) || "unknown-model";
|
|
268
|
+
ctx.ui.notify(`Pi Fluency model: ${provider}/${modelId}`, "info");
|
|
269
|
+
return true;
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
const command = async (args: string, ctx: ExtensionCommandContext): Promise<void> => {
|
|
273
|
+
if (shuttingDown) return;
|
|
274
|
+
ctxRef = ctx;
|
|
275
|
+
const store = await getStore();
|
|
276
|
+
const action = args.trim();
|
|
277
|
+
if (action === "") {
|
|
278
|
+
if (hasValidConfiguration(store.getSettings(), ctx)) await openInbox(ctx, store);
|
|
279
|
+
else {
|
|
280
|
+
clearStatus(ctx);
|
|
281
|
+
await runSetup(ctx, store, { now: dependencies.now });
|
|
282
|
+
if (hasValidConfiguration(store.getSettings(), ctx)) publishProgress(ctx, store);
|
|
283
|
+
else clearStatus(ctx);
|
|
284
|
+
}
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
if (action === "stats") {
|
|
288
|
+
await openInbox(ctx, store, "stats");
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
if (action === "pause") {
|
|
292
|
+
await store.updateSettings({ enabled: false });
|
|
293
|
+
await workerRef?.shutdown();
|
|
294
|
+
workerRef = undefined;
|
|
295
|
+
clearStatus(ctx);
|
|
296
|
+
ctx.ui.notify("Pi Fluency paused", "info");
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (action === "resume") {
|
|
300
|
+
const settings = store.getSettings();
|
|
301
|
+
const resumedSettings = { ...settings, enabled: true };
|
|
302
|
+
if (!hasValidConfiguration(resumedSettings, ctx)) {
|
|
303
|
+
clearStatus(ctx);
|
|
304
|
+
ctx.ui.notify("Pi Fluency needs consent and an available model. Run /fluency.", "warning");
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
await store.updateSettings({ enabled: true });
|
|
308
|
+
publishProgress(ctx, store);
|
|
309
|
+
ctx.ui.notify("Pi Fluency resumed", "info");
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
if (action === "status") {
|
|
313
|
+
const settings = store.getSettings();
|
|
314
|
+
const snapshot = workerRef?.getSnapshot() ?? { queued: 0, dropped: 0 };
|
|
315
|
+
const sanitize = (value: string | undefined): string | undefined => sanitizeTerminalLabel(value, 100) || undefined;
|
|
316
|
+
const provider = sanitize(settings.provider);
|
|
317
|
+
const modelId = sanitize(settings.modelId);
|
|
318
|
+
const model = provider && modelId ? `${provider}/${modelId}` : "not selected";
|
|
319
|
+
const active = hasValidConfiguration(settings, ctx);
|
|
320
|
+
const state = active
|
|
321
|
+
? "enabled"
|
|
322
|
+
: settings.enabled ? "inactive (configuration invalid)" : "paused";
|
|
323
|
+
if (!active) clearStatus(ctx);
|
|
324
|
+
ctx.ui.notify(
|
|
325
|
+
`Pi Fluency: ${state}; model=${model}; queued=${snapshot.queued}; dropped=${snapshot.dropped}; warnings=${store.getWarnings().length}`,
|
|
326
|
+
"info",
|
|
327
|
+
);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
if (action === "model") {
|
|
331
|
+
await selectModel(ctx, store);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
if (action === "clear") {
|
|
335
|
+
if (await ctx.ui.confirm("Clear Pi Fluency history?", "This removes all recorded coaching history.")) {
|
|
336
|
+
await workerRef?.shutdown();
|
|
337
|
+
workerRef = undefined;
|
|
338
|
+
await store.clear();
|
|
339
|
+
if (hasValidConfiguration(store.getSettings(), ctx)) publishProgress(ctx, store);
|
|
340
|
+
else clearStatus(ctx);
|
|
341
|
+
ctx.ui.notify("Pi Fluency history cleared", "info");
|
|
342
|
+
}
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
ctx.ui.notify(USAGE, "warning");
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
349
|
+
ctxRef = ctx;
|
|
350
|
+
publishStatus(ctx, { kind: "initial-loading" });
|
|
351
|
+
try {
|
|
352
|
+
const store = await getStore();
|
|
353
|
+
if (shuttingDown) {
|
|
354
|
+
clearStatus(ctx);
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
if (!hasValidConfiguration(store.getSettings(), ctx)) {
|
|
358
|
+
publishConfigurationFailureOrClear(ctx, store);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
publishProgress(ctx, store);
|
|
362
|
+
} catch (error) {
|
|
363
|
+
if (shuttingDown) clearStatus(ctx);
|
|
364
|
+
else {
|
|
365
|
+
publishStatus(ctx, { kind: "error", reason: "store" });
|
|
366
|
+
notifyError(ctx, error);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
pi.on("input", async (event, ctx) => {
|
|
372
|
+
if (shuttingDown || event.source !== "interactive") return;
|
|
373
|
+
ctxRef = ctx;
|
|
374
|
+
const store = await getStore();
|
|
375
|
+
if (shuttingDown) return;
|
|
376
|
+
if (!hasValidConfiguration(store.getSettings(), ctx)) {
|
|
377
|
+
publishConfigurationFailureOrClear(ctx, store);
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
const collected = collectPrompt(event.text, dependencies.now());
|
|
381
|
+
if (!collected) return;
|
|
382
|
+
const prompt = {
|
|
383
|
+
...collected,
|
|
384
|
+
promptHash: createHash("sha256")
|
|
385
|
+
.update(`${collected.promptHash}\0${inputSessionId}\0${inputSequence++}`)
|
|
386
|
+
.digest("hex"),
|
|
387
|
+
};
|
|
388
|
+
if (store.hasProcessedPromptHash(prompt.promptHash)) return;
|
|
389
|
+
try {
|
|
390
|
+
getWorker(ctx, store).enqueue(prompt);
|
|
391
|
+
} catch (error) {
|
|
392
|
+
setError(ctx, analyzerErrorReason(error), error);
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
pi.on("agent_settled", (_event, ctx) => {
|
|
397
|
+
ctxRef = ctx;
|
|
398
|
+
const store = storeRef;
|
|
399
|
+
if (shuttingDown || !store) {
|
|
400
|
+
clearStatus(ctx);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
if (!hasValidConfiguration(store.getSettings(), ctx)) {
|
|
404
|
+
publishConfigurationFailureOrClear(ctx, store);
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
try {
|
|
408
|
+
void getWorker(ctx, store).drain().catch((error) => setError(ctx, analyzerErrorReason(error), error));
|
|
409
|
+
} catch (error) {
|
|
410
|
+
setError(ctx, analyzerErrorReason(error), error);
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
pi.on("session_shutdown", () => {
|
|
415
|
+
shutdownPromise ??= (async () => {
|
|
416
|
+
shuttingDown = true;
|
|
417
|
+
const shutdownCtx = ctxRef;
|
|
418
|
+
if (shutdownCtx) clearStatus(shutdownCtx);
|
|
419
|
+
overlayController?.abort();
|
|
420
|
+
await Promise.all([workerRef?.shutdown(), overlayOpen]);
|
|
421
|
+
if (shutdownCtx) clearStatus(shutdownCtx);
|
|
422
|
+
workerRef = undefined;
|
|
423
|
+
overlayOpen = undefined;
|
|
424
|
+
overlayController = undefined;
|
|
425
|
+
ctxRef = undefined;
|
|
426
|
+
})();
|
|
427
|
+
return shutdownPromise;
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
pi.registerCommand("fluency", {
|
|
431
|
+
description: "Configure Pi Fluency and open coaching inbox",
|
|
432
|
+
handler: command,
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
pi.registerShortcut(Key.ctrlShift("l"), {
|
|
436
|
+
description: "Open Pi Fluency inbox",
|
|
437
|
+
handler: async (ctx) => {
|
|
438
|
+
if (shuttingDown) return;
|
|
439
|
+
ctxRef = ctx;
|
|
440
|
+
const store = await getStore();
|
|
441
|
+
if (hasValidConfiguration(store.getSettings(), ctx)) await openInbox(ctx, store);
|
|
442
|
+
else {
|
|
443
|
+
clearStatus(ctx);
|
|
444
|
+
ctx.ui.notify("Run /fluency to enable Pi Fluency", "info");
|
|
445
|
+
}
|
|
446
|
+
},
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
export function createFluencyExtension(dependencies: ExtensionDependencies = {}) {
|
|
451
|
+
return function register(pi: ExtensionAPI): void {
|
|
452
|
+
const rootDir = dependencies.rootDir ?? join(homedir(), ".pi", "agent", "pi-fluency");
|
|
453
|
+
const now = dependencies.now ?? Date.now;
|
|
454
|
+
registerHandlers(pi, { ...dependencies, rootDir, now });
|
|
455
|
+
pi.events.emit("powerbar:register-segment", { id: STATUS_KEY, label: "Pi Fluency" });
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
export default createFluencyExtension();
|