pi-extension-utils 0.3.1 → 0.3.3
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 +91 -45
- package/dist/index.js +9 -140
- package/dist/src/client/index.d.ts +7 -0
- package/dist/src/client/index.js +25 -0
- package/dist/src/client/types.d.ts +17 -0
- package/dist/src/client/types.js +1 -0
- package/dist/src/config/index.d.ts +28 -0
- package/dist/src/config/index.js +222 -0
- package/dist/src/index.d.ts +10 -7
- package/dist/src/index.js +10 -7
- package/dist/src/logger/config.d.ts +11 -0
- package/dist/src/logger/config.js +27 -0
- package/dist/src/logger/index.d.ts +20 -0
- package/dist/src/logger/index.js +76 -0
- package/dist/src/{pane-overlay.d.ts → pane/overlay.d.ts} +1 -1
- package/dist/src/{pane-overlay.js → pane/overlay.js} +2 -2
- package/dist/src/reminders/client.d.ts +10 -0
- package/dist/src/reminders/client.js +49 -0
- package/dist/src/reminders/config.d.ts +5 -6
- package/dist/src/reminders/config.js +9 -23
- package/dist/src/reminders/debug.js +2 -2
- package/dist/src/reminders/host.d.ts +5 -0
- package/dist/src/reminders/host.js +298 -0
- package/dist/src/reminders/index.d.ts +3 -5
- package/dist/src/reminders/index.js +3 -308
- package/dist/src/reminders/types.d.ts +3 -0
- package/dist/src/reminders/types.js +3 -0
- package/dist/src/ui/client.d.ts +22 -0
- package/dist/src/ui/client.js +17 -0
- package/dist/src/utils-config.d.ts +24 -0
- package/dist/src/utils-config.js +38 -0
- package/dist/src/widgets/client.d.ts +27 -0
- package/dist/src/widgets/client.js +130 -0
- package/dist/src/widgets/host.d.ts +2 -0
- package/dist/src/widgets/host.js +143 -0
- package/dist/src/{protocol.d.ts → widgets/protocol.d.ts} +2 -6
- package/docs/README.md +11 -0
- package/docs/client.md +91 -0
- package/docs/config.md +86 -0
- package/docs/pane-overlay.md +88 -0
- package/docs/reference/reminders-spec.md +351 -0
- package/docs/reminders.md +68 -0
- package/docs/widgets.md +57 -0
- package/examples/README.md +23 -0
- package/examples/config.ts +31 -0
- package/examples/logger.ts +20 -0
- package/examples/pane-overlay.ts +47 -0
- package/examples/reminders.ts +25 -0
- package/examples/widget-coordinator.ts +18 -0
- package/package.json +12 -1
- package/dist/src/client.d.ts +0 -47
- package/dist/src/client.js +0 -194
- package/dist/src/logger.d.ts +0 -12
- package/dist/src/logger.js +0 -45
- /package/dist/src/{tui-chrome.d.ts → pane/chrome.d.ts} +0 -0
- /package/dist/src/{tui-chrome.js → pane/chrome.js} +0 -0
- /package/dist/src/{key-dispatch.d.ts → pane/key-dispatch.d.ts} +0 -0
- /package/dist/src/{key-dispatch.js → pane/key-dispatch.js} +0 -0
- /package/dist/src/{pane-state.d.ts → pane/state.d.ts} +0 -0
- /package/dist/src/{pane-state.js → pane/state.js} +0 -0
- /package/dist/src/{protocol.js → widgets/protocol.js} +0 -0
|
@@ -1,308 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import { ReminderManager } from "./manager.js";
|
|
5
|
-
import { REMINDER_ANNOUNCE_NOW_EVENT, REMINDER_CLEAR_SOURCE_EVENT, REMINDER_LIST_EVENT, REMINDER_REMOVE_EVENT, REMINDER_UPSERT_EVENT, } from "./types.js";
|
|
6
|
-
/** Register the pi-reminders extension hooks and event listeners. */
|
|
7
|
-
export function registerReminderHost(pi, manager = new ReminderManager()) {
|
|
8
|
-
const eventBus = pi.events;
|
|
9
|
-
const cfg = {};
|
|
10
|
-
const refreshConfig = (cwd) => {
|
|
11
|
-
delete cfg.debugShowAllInTui;
|
|
12
|
-
Object.assign(cfg, loadConfig(cwd));
|
|
13
|
-
};
|
|
14
|
-
const debugShowAllInTui = (ctx) => {
|
|
15
|
-
refreshConfig(resolveCwd(ctx));
|
|
16
|
-
return isDebugModeEnabled(cfg);
|
|
17
|
-
};
|
|
18
|
-
writeReminderDiagnostic({
|
|
19
|
-
event: "reminder_extension_init",
|
|
20
|
-
});
|
|
21
|
-
writeReminderDiagnostic({
|
|
22
|
-
event: "reminder_extension_register_start",
|
|
23
|
-
});
|
|
24
|
-
eventBus.on(REMINDER_UPSERT_EVENT, (payload) => {
|
|
25
|
-
if (isRecord(payload))
|
|
26
|
-
manager.upsert(payload);
|
|
27
|
-
});
|
|
28
|
-
eventBus.on(REMINDER_REMOVE_EVENT, (payload) => {
|
|
29
|
-
if (isRecord(payload))
|
|
30
|
-
manager.remove(payload);
|
|
31
|
-
});
|
|
32
|
-
eventBus.on(REMINDER_CLEAR_SOURCE_EVENT, (payload) => {
|
|
33
|
-
if (isRecord(payload))
|
|
34
|
-
manager.clearSource(payload);
|
|
35
|
-
});
|
|
36
|
-
eventBus.on(REMINDER_ANNOUNCE_NOW_EVENT, (payload) => {
|
|
37
|
-
manager.forceAnnounce(isRecord(payload) ? payload : {});
|
|
38
|
-
});
|
|
39
|
-
eventBus.on(REMINDER_LIST_EVENT, (payload) => {
|
|
40
|
-
if (isRecord(payload) && typeof payload.resolve === "function") {
|
|
41
|
-
handleListRequest(manager, payload);
|
|
42
|
-
}
|
|
43
|
-
});
|
|
44
|
-
if (typeof pi.registerMessageRenderer === "function") {
|
|
45
|
-
pi.registerMessageRenderer("pi-reminders", (message, _options, theme) => {
|
|
46
|
-
const details = isRecord(message.details) ? message.details : undefined;
|
|
47
|
-
const content = typeof details?.displayText === "string"
|
|
48
|
-
? details.displayText
|
|
49
|
-
: stripSystemReminderWrapper(extractTextContent(message.content));
|
|
50
|
-
return new SystemReminderComponent(content, details, theme);
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
pi.on("session_start", (_event, ctx) => {
|
|
54
|
-
manager.clearSession();
|
|
55
|
-
refreshConfig(resolveCwd(ctx));
|
|
56
|
-
writeReminderDiagnostic({
|
|
57
|
-
event: "reminder_session_start",
|
|
58
|
-
sessionId: getSessionId(ctx),
|
|
59
|
-
cwd: getCwd(ctx),
|
|
60
|
-
providerApi: getProviderApi(ctx),
|
|
61
|
-
});
|
|
62
|
-
});
|
|
63
|
-
pi.on("session_shutdown", () => {
|
|
64
|
-
manager.clearSession();
|
|
65
|
-
});
|
|
66
|
-
pi.on("turn_start", () => {
|
|
67
|
-
manager.advanceTurn();
|
|
68
|
-
});
|
|
69
|
-
pi.on("before_agent_start", (_event, ctx) => {
|
|
70
|
-
const announcement = manager.collectDueAnnouncement({ showHiddenInDisplay: debugShowAllInTui(ctx) });
|
|
71
|
-
if (!announcement)
|
|
72
|
-
return;
|
|
73
|
-
const message = buildReminderMessage(announcement, getContextUsage(ctx));
|
|
74
|
-
writeAnnouncementDiagnostic("reminder_history_message", message);
|
|
75
|
-
return { message };
|
|
76
|
-
});
|
|
77
|
-
pi.on("context", (_event, ctx) => {
|
|
78
|
-
const announcement = manager.collectDueAnnouncement({ showHiddenInDisplay: debugShowAllInTui(ctx) });
|
|
79
|
-
if (!announcement)
|
|
80
|
-
return;
|
|
81
|
-
const message = buildReminderMessage(announcement, getContextUsage(ctx));
|
|
82
|
-
writeAnnouncementDiagnostic("reminder_context_message", message);
|
|
83
|
-
pi.sendMessage(message, { deliverAs: "steer" });
|
|
84
|
-
});
|
|
85
|
-
pi.registerCommand("reminders", {
|
|
86
|
-
description: "Open pi-reminders settings",
|
|
87
|
-
handler: async (_args, ctx) => {
|
|
88
|
-
const cwd = resolveCwd(ctx);
|
|
89
|
-
refreshConfig(cwd);
|
|
90
|
-
const current = debugShowAllInTui(ctx) ? "on" : "off";
|
|
91
|
-
const choice = await ctx.ui.select("Reminders debug: show all in TUI", [`${current === "on" ? "[on]" : "on"}`, `${current === "off" ? "[off]" : "off"}`, "← Back"]);
|
|
92
|
-
if (!choice || choice === "← Back")
|
|
93
|
-
return;
|
|
94
|
-
cfg.debugShowAllInTui = choice.includes("on");
|
|
95
|
-
saveConfig(cfg, cwd);
|
|
96
|
-
ctx.ui.notify(`Reminders debug show-all in TUI ${cfg.debugShowAllInTui ? "enabled" : "disabled"}`, "info");
|
|
97
|
-
},
|
|
98
|
-
});
|
|
99
|
-
pi.registerCommand("remind", {
|
|
100
|
-
description: "Add a debug system reminder",
|
|
101
|
-
handler: async (args, ctx) => {
|
|
102
|
-
const text = args.trim();
|
|
103
|
-
if (!text) {
|
|
104
|
-
ctx.ui.notify("Usage: /remind <text>", "warning");
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
manager.upsert({
|
|
108
|
-
source: "pi-reminders",
|
|
109
|
-
id: "debug-command",
|
|
110
|
-
label: "Reminder",
|
|
111
|
-
text,
|
|
112
|
-
priority: 100,
|
|
113
|
-
ttl: "once",
|
|
114
|
-
});
|
|
115
|
-
ctx.ui.notify("Reminder added", "info");
|
|
116
|
-
},
|
|
117
|
-
});
|
|
118
|
-
writeReminderDiagnostic({
|
|
119
|
-
event: "reminder_extension_registered",
|
|
120
|
-
hasEvents: eventBus !== undefined,
|
|
121
|
-
hasRegisterCommand: typeof pi.registerCommand === "function",
|
|
122
|
-
hasRegisterMessageRenderer: typeof pi.registerMessageRenderer === "function",
|
|
123
|
-
hasOn: typeof pi.on === "function",
|
|
124
|
-
});
|
|
125
|
-
return manager;
|
|
126
|
-
}
|
|
127
|
-
export default function reminderHost(pi) {
|
|
128
|
-
try {
|
|
129
|
-
registerReminderHost(pi);
|
|
130
|
-
}
|
|
131
|
-
catch (error) {
|
|
132
|
-
writeReminderDiagnostic({
|
|
133
|
-
event: "reminder_extension_register_error",
|
|
134
|
-
errorName: error instanceof Error ? error.name : undefined,
|
|
135
|
-
errorMessage: error instanceof Error ? error.message : String(error),
|
|
136
|
-
errorStack: error instanceof Error ? error.stack : undefined,
|
|
137
|
-
});
|
|
138
|
-
throw error;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
function handleListRequest(manager, request) {
|
|
142
|
-
try {
|
|
143
|
-
request.resolve(manager.snapshot(request.source));
|
|
144
|
-
}
|
|
145
|
-
catch (error) {
|
|
146
|
-
request.reject?.(error);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
function isDebugModeEnabled(config) {
|
|
150
|
-
const value = process.env.PI_REMINDERS_DEBUG?.trim().toLowerCase();
|
|
151
|
-
if (value)
|
|
152
|
-
return value === "1" || value === "true" || value === "yes" || value === "on";
|
|
153
|
-
return config.debugShowAllInTui === true;
|
|
154
|
-
}
|
|
155
|
-
function buildReminderMessage(announcement, usage) {
|
|
156
|
-
const sources = [...new Set(announcement.reminders.map((reminder) => reminder.source))].sort();
|
|
157
|
-
const modelBody = includesDcpReminder(announcement.reminders)
|
|
158
|
-
? appendContextUsageLine(stripSystemReminderWrapper(announcement.text), usage)
|
|
159
|
-
: stripSystemReminderWrapper(announcement.text);
|
|
160
|
-
const displayText = announcement.displayText && includesDcpReminder(announcement.displayReminders)
|
|
161
|
-
? appendContextUsageLine(announcement.displayText, usage)
|
|
162
|
-
: announcement.displayText;
|
|
163
|
-
const content = `<system-reminder>\n${modelBody}\n</system-reminder>`;
|
|
164
|
-
return {
|
|
165
|
-
customType: "pi-reminders",
|
|
166
|
-
content,
|
|
167
|
-
display: displayText !== null,
|
|
168
|
-
details: {
|
|
169
|
-
reminderCount: announcement.reminders.length,
|
|
170
|
-
sources,
|
|
171
|
-
displayText: displayText ?? undefined,
|
|
172
|
-
},
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
function writeAnnouncementDiagnostic(event, message) {
|
|
176
|
-
const details = message.details;
|
|
177
|
-
writeReminderDiagnostic({
|
|
178
|
-
event,
|
|
179
|
-
activeReminderCount: details?.reminderCount,
|
|
180
|
-
sources: details?.sources,
|
|
181
|
-
trailerChars: message.content.length,
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
function getContextUsage(ctx) {
|
|
185
|
-
if (!isRecord(ctx) || typeof ctx.getContextUsage !== "function")
|
|
186
|
-
return undefined;
|
|
187
|
-
try {
|
|
188
|
-
const usage = ctx.getContextUsage();
|
|
189
|
-
if (!isRecord(usage) || typeof usage.contextWindow !== "number")
|
|
190
|
-
return undefined;
|
|
191
|
-
const tokens = typeof usage.tokens === "number" ? usage.tokens : null;
|
|
192
|
-
return { tokens, contextWindow: usage.contextWindow };
|
|
193
|
-
}
|
|
194
|
-
catch {
|
|
195
|
-
return undefined;
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
function includesDcpReminder(reminders) {
|
|
199
|
-
return reminders.some((reminder) => {
|
|
200
|
-
const source = reminder.source.trim().toLowerCase();
|
|
201
|
-
return source === "dcp" || source === "pi-dynamic-context-pruning";
|
|
202
|
-
});
|
|
203
|
-
}
|
|
204
|
-
function appendContextUsageLine(displayText, usage) {
|
|
205
|
-
if (!usage)
|
|
206
|
-
return displayText;
|
|
207
|
-
const contextWindow = formatTokenCount(usage.contextWindow);
|
|
208
|
-
if (usage.tokens === null)
|
|
209
|
-
return `${displayText}\nContext: unknown / ${contextWindow} tokens.`;
|
|
210
|
-
const percent = usage.contextWindow > 0 ? Math.round((usage.tokens / usage.contextWindow) * 100) : 0;
|
|
211
|
-
return `${displayText}\nContext: ${formatTokenCount(usage.tokens)} / ${contextWindow} tokens (${percent}%).`;
|
|
212
|
-
}
|
|
213
|
-
function formatTokenCount(tokens) {
|
|
214
|
-
if (tokens >= 1_000_000)
|
|
215
|
-
return `${trimFixed(tokens / 1_000_000)}M`;
|
|
216
|
-
if (tokens >= 1_000)
|
|
217
|
-
return `${trimFixed(tokens / 1_000)}k`;
|
|
218
|
-
return String(tokens);
|
|
219
|
-
}
|
|
220
|
-
function trimFixed(value) {
|
|
221
|
-
return value.toFixed(value >= 10 ? 0 : 1).replace(/\.0$/, "");
|
|
222
|
-
}
|
|
223
|
-
function getProviderApi(ctx) {
|
|
224
|
-
return typeof ctx?.model?.api === "string" ? ctx.model.api : undefined;
|
|
225
|
-
}
|
|
226
|
-
function getSessionId(ctx) {
|
|
227
|
-
try {
|
|
228
|
-
return ctx?.sessionManager?.getSessionId?.();
|
|
229
|
-
}
|
|
230
|
-
catch {
|
|
231
|
-
return undefined;
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
function getCwd(ctx) {
|
|
235
|
-
try {
|
|
236
|
-
return ctx?.sessionManager?.getCwd?.();
|
|
237
|
-
}
|
|
238
|
-
catch {
|
|
239
|
-
return undefined;
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
function resolveCwd(ctx) {
|
|
243
|
-
if (isRecord(ctx) && typeof ctx.cwd === "string" && ctx.cwd.trim())
|
|
244
|
-
return ctx.cwd;
|
|
245
|
-
const sessionCwd = getCwd(ctx);
|
|
246
|
-
return sessionCwd ?? process.env.PWD ?? process.cwd();
|
|
247
|
-
}
|
|
248
|
-
function extractTextContent(content) {
|
|
249
|
-
if (typeof content === "string")
|
|
250
|
-
return content;
|
|
251
|
-
if (!Array.isArray(content))
|
|
252
|
-
return "";
|
|
253
|
-
return content
|
|
254
|
-
.filter((part) => isRecord(part) && part.type === "text" && typeof part.text === "string")
|
|
255
|
-
.map((part) => part.text)
|
|
256
|
-
.join("\n");
|
|
257
|
-
}
|
|
258
|
-
function stripSystemReminderWrapper(content) {
|
|
259
|
-
return content
|
|
260
|
-
.replace(/^\s*<system-reminder>\s*/i, "")
|
|
261
|
-
.replace(/\s*<\/system-reminder>\s*$/i, "")
|
|
262
|
-
.trim();
|
|
263
|
-
}
|
|
264
|
-
function padToWidth(line, width) {
|
|
265
|
-
return `${line}${" ".repeat(Math.max(0, width - visibleWidth(line)))}`;
|
|
266
|
-
}
|
|
267
|
-
const SYSTEM_REMINDER_MIN_BODY_WIDTH = 76;
|
|
268
|
-
const SYSTEM_REMINDER_BODY_WIDTH_RATIO = 0.9;
|
|
269
|
-
class SystemReminderComponent {
|
|
270
|
-
content;
|
|
271
|
-
details;
|
|
272
|
-
theme;
|
|
273
|
-
constructor(content, details, theme) {
|
|
274
|
-
this.content = content;
|
|
275
|
-
this.details = details;
|
|
276
|
-
this.theme = theme;
|
|
277
|
-
}
|
|
278
|
-
invalidate() { }
|
|
279
|
-
render(width) {
|
|
280
|
-
if (width < 3)
|
|
281
|
-
return [truncateToWidth("System Reminder", width)];
|
|
282
|
-
const availableBodyWidth = width - 2;
|
|
283
|
-
const preferredBodyWidth = Math.max(SYSTEM_REMINDER_MIN_BODY_WIDTH, Math.floor(availableBodyWidth * SYSTEM_REMINDER_BODY_WIDTH_RATIO));
|
|
284
|
-
const bodyWidth = Math.max(1, Math.min(availableBodyWidth, preferredBodyWidth));
|
|
285
|
-
const title = " System Reminder ";
|
|
286
|
-
const titleText = truncateToWidth(title, bodyWidth, "");
|
|
287
|
-
const border = (value) => this.theme.fg("accent", value);
|
|
288
|
-
const lines = [border(`╭${titleText}${"─".repeat(Math.max(0, bodyWidth - visibleWidth(titleText)))}╮`)];
|
|
289
|
-
const text = new Text(this.content, 0, 0);
|
|
290
|
-
for (const line of text.render(bodyWidth)) {
|
|
291
|
-
const truncated = truncateToWidth(line, bodyWidth, "");
|
|
292
|
-
lines.push(border("│") + padToWidth(truncated, bodyWidth) + border("│"));
|
|
293
|
-
}
|
|
294
|
-
const meta = [
|
|
295
|
-
this.details?.reminderCount !== undefined ? `${this.details.reminderCount} reminder${this.details.reminderCount === 1 ? "" : "s"}` : undefined,
|
|
296
|
-
this.details?.sources?.length ? this.details.sources.join(", ") : undefined,
|
|
297
|
-
].filter(Boolean).join(" · ");
|
|
298
|
-
if (meta) {
|
|
299
|
-
const metaLine = truncateToWidth(this.theme.fg("dim", meta), bodyWidth, "");
|
|
300
|
-
lines.push(border("│") + padToWidth(metaLine, bodyWidth) + border("│"));
|
|
301
|
-
}
|
|
302
|
-
lines.push(border(`╰${"─".repeat(bodyWidth)}╯`));
|
|
303
|
-
return lines;
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
function isRecord(value) {
|
|
307
|
-
return typeof value === "object" && value !== null;
|
|
308
|
-
}
|
|
1
|
+
export { default, registerReminderHost } from "./host.js";
|
|
2
|
+
export { remindersConfigSchema } from "./config.js";
|
|
3
|
+
export * from "./types.js";
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
export declare const REMINDER_MESSAGE_CUSTOM_TYPE = "pi-extension-utils:reminders";
|
|
2
|
+
export declare const LEGACY_REMINDER_MESSAGE_CUSTOM_TYPE = "pi-reminders";
|
|
3
|
+
export declare const DEBUG_REMINDER_SOURCE = "pi-extension-utils";
|
|
1
4
|
export declare const REMINDER_UPSERT_EVENT = "reminder:upsert";
|
|
2
5
|
export declare const REMINDER_REMOVE_EVENT = "reminder:remove";
|
|
3
6
|
export declare const REMINDER_CLEAR_SOURCE_EVENT = "reminder:clear-source";
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
export const REMINDER_MESSAGE_CUSTOM_TYPE = "pi-extension-utils:reminders";
|
|
2
|
+
export const LEGACY_REMINDER_MESSAGE_CUSTOM_TYPE = "pi-reminders";
|
|
3
|
+
export const DEBUG_REMINDER_SOURCE = "pi-extension-utils";
|
|
1
4
|
export const REMINDER_UPSERT_EVENT = "reminder:upsert";
|
|
2
5
|
export const REMINDER_REMOVE_EVENT = "reminder:remove";
|
|
3
6
|
export const REMINDER_CLEAR_SOURCE_EVENT = "reminder:clear-source";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface FullscreenLeaseLike {
|
|
2
|
+
release(): void;
|
|
3
|
+
}
|
|
4
|
+
export interface FullscreenController {
|
|
5
|
+
acquire(): FullscreenLeaseLike;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Factory signature accepted by `ctx.ui.custom`. Typed structurally so the
|
|
9
|
+
* helper does not depend on pi-tui types directly.
|
|
10
|
+
*/
|
|
11
|
+
export type FullscreenComponentFactory<T> = (tui: unknown, theme: unknown, keybindings: unknown, done: (result: T) => void) => unknown;
|
|
12
|
+
export interface UiClient {
|
|
13
|
+
/**
|
|
14
|
+
* Run a full-screen custom UI: acquires a fullscreen lease (blanking
|
|
15
|
+
* coordinated widgets), shows the component via `ctx.ui.custom`, and
|
|
16
|
+
* releases the lease in a finally — even if the component throws.
|
|
17
|
+
*/
|
|
18
|
+
fullscreen<T>(factory: FullscreenComponentFactory<T>): Promise<T>;
|
|
19
|
+
}
|
|
20
|
+
export declare function createUiClient(fullscreen: FullscreenController, ctx: {
|
|
21
|
+
ui: unknown;
|
|
22
|
+
}): UiClient;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function createUiClient(fullscreen, ctx) {
|
|
2
|
+
return {
|
|
3
|
+
async fullscreen(factory) {
|
|
4
|
+
const ui = ctx.ui;
|
|
5
|
+
if (typeof ui.custom !== "function") {
|
|
6
|
+
throw new Error("pi-extension-utils: ui.fullscreen requires an interactive UI (ctx.ui.custom is unavailable)");
|
|
7
|
+
}
|
|
8
|
+
const lease = fullscreen.acquire();
|
|
9
|
+
try {
|
|
10
|
+
return await ui.custom(factory);
|
|
11
|
+
}
|
|
12
|
+
finally {
|
|
13
|
+
lease.release();
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Type, type Static } from "typebox";
|
|
2
|
+
export declare const utilsConfigSchema: Type.TObject<{
|
|
3
|
+
logging: Type.TObject<{
|
|
4
|
+
level: Type.TUnion<[Type.TLiteral<"debug">, Type.TLiteral<"info">, Type.TLiteral<"warn">, Type.TLiteral<"error">, Type.TLiteral<"silent">]>;
|
|
5
|
+
maxFiles: Type.TNumber;
|
|
6
|
+
maxBytes: Type.TNumber;
|
|
7
|
+
}>;
|
|
8
|
+
reminders: Type.TObject<{
|
|
9
|
+
debugShowAllInTui: Type.TBoolean;
|
|
10
|
+
}>;
|
|
11
|
+
}>;
|
|
12
|
+
export type UtilsConfig = Static<typeof utilsConfigSchema>;
|
|
13
|
+
export declare const DEFAULT_UTILS_CONFIG: UtilsConfig;
|
|
14
|
+
export declare const utilsConfig: import("./index.ts").DefinedConfig<Type.TObject<{
|
|
15
|
+
logging: Type.TObject<{
|
|
16
|
+
level: Type.TUnion<[Type.TLiteral<"debug">, Type.TLiteral<"info">, Type.TLiteral<"warn">, Type.TLiteral<"error">, Type.TLiteral<"silent">]>;
|
|
17
|
+
maxFiles: Type.TNumber;
|
|
18
|
+
maxBytes: Type.TNumber;
|
|
19
|
+
}>;
|
|
20
|
+
reminders: Type.TObject<{
|
|
21
|
+
debugShowAllInTui: Type.TBoolean;
|
|
22
|
+
}>;
|
|
23
|
+
}>>;
|
|
24
|
+
export declare function readUtilsConfigOrDefaults(mode?: "get" | "reload"): UtilsConfig;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import { defineConfig } from "./config/index.js";
|
|
3
|
+
import { loggingConfigSchema } from "./logger/config.js";
|
|
4
|
+
import { remindersConfigSchema } from "./reminders/config.js";
|
|
5
|
+
export const utilsConfigSchema = Type.Object({
|
|
6
|
+
logging: loggingConfigSchema,
|
|
7
|
+
reminders: remindersConfigSchema,
|
|
8
|
+
});
|
|
9
|
+
export const DEFAULT_UTILS_CONFIG = {
|
|
10
|
+
logging: {
|
|
11
|
+
level: "info",
|
|
12
|
+
maxFiles: 3,
|
|
13
|
+
maxBytes: 1048576,
|
|
14
|
+
},
|
|
15
|
+
reminders: {
|
|
16
|
+
debugShowAllInTui: false,
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
export const utilsConfig = defineConfig({
|
|
20
|
+
name: "utils",
|
|
21
|
+
schema: utilsConfigSchema,
|
|
22
|
+
});
|
|
23
|
+
let warnedAboutInvalidConfig = false;
|
|
24
|
+
export function readUtilsConfigOrDefaults(mode = "get") {
|
|
25
|
+
try {
|
|
26
|
+
return mode === "reload" ? utilsConfig.reload() : utilsConfig.get();
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
if (!warnedAboutInvalidConfig) {
|
|
30
|
+
warnedAboutInvalidConfig = true;
|
|
31
|
+
console.warn("pi-extension-utils: invalid utils config; using defaults", error);
|
|
32
|
+
}
|
|
33
|
+
return cloneDefaultUtilsConfig();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function cloneDefaultUtilsConfig() {
|
|
37
|
+
return JSON.parse(JSON.stringify(DEFAULT_UTILS_CONFIG));
|
|
38
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { type WidgetFactory, type WidgetPlacement } from "./protocol.ts";
|
|
3
|
+
export interface WidgetSetOptions {
|
|
4
|
+
order?: number;
|
|
5
|
+
}
|
|
6
|
+
export interface FullscreenLease {
|
|
7
|
+
release(): void;
|
|
8
|
+
}
|
|
9
|
+
export interface WidgetsClient {
|
|
10
|
+
set(placement: WidgetPlacement, key: string, factory: WidgetFactory, opts?: WidgetSetOptions): void;
|
|
11
|
+
remove(placement: WidgetPlacement, key: string): void;
|
|
12
|
+
}
|
|
13
|
+
export interface FullscreenClient {
|
|
14
|
+
acquire(): FullscreenLease;
|
|
15
|
+
}
|
|
16
|
+
export interface WidgetCoordinatorClient {
|
|
17
|
+
readonly mode: "fallback" | "coordinated";
|
|
18
|
+
widgets: WidgetsClient;
|
|
19
|
+
fullscreen: FullscreenClient;
|
|
20
|
+
dispose(): void;
|
|
21
|
+
}
|
|
22
|
+
interface WidgetCoordinatorClientOptions {
|
|
23
|
+
ctx: ExtensionContext;
|
|
24
|
+
clientId: string;
|
|
25
|
+
}
|
|
26
|
+
export declare function connectWidgetCoordinator(pi: ExtensionAPI, opts: WidgetCoordinatorClientOptions): WidgetCoordinatorClient;
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { basePayload, EVENTS, PROTOCOL_VERSION } from "./protocol.js";
|
|
2
|
+
let nextLeaseId = 1;
|
|
3
|
+
export function connectWidgetCoordinator(pi, opts) {
|
|
4
|
+
const { clientId } = opts;
|
|
5
|
+
const widgets = new Map();
|
|
6
|
+
const leases = new Set();
|
|
7
|
+
let disposed = false;
|
|
8
|
+
let coordinated = false;
|
|
9
|
+
const widgetId = (placement, key) => `${placement}:${key}`;
|
|
10
|
+
function emitRegister(record) {
|
|
11
|
+
pi.events.emit(EVENTS.registerWidget, {
|
|
12
|
+
...basePayload(clientId),
|
|
13
|
+
placement: record.placement,
|
|
14
|
+
key: record.key,
|
|
15
|
+
order: record.order,
|
|
16
|
+
factory: record.factory,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
function clearFallback(record) {
|
|
20
|
+
opts.ctx.ui.setWidget(record.key, undefined, { placement: record.placement });
|
|
21
|
+
}
|
|
22
|
+
function restoreFallback(record) {
|
|
23
|
+
opts.ctx.ui.setWidget(record.key, record.factory, { placement: record.placement });
|
|
24
|
+
}
|
|
25
|
+
function attach() {
|
|
26
|
+
if (disposed || coordinated)
|
|
27
|
+
return;
|
|
28
|
+
coordinated = true;
|
|
29
|
+
for (const record of widgets.values()) {
|
|
30
|
+
clearFallback(record);
|
|
31
|
+
emitRegister(record);
|
|
32
|
+
}
|
|
33
|
+
for (const token of leases) {
|
|
34
|
+
pi.events.emit(EVENTS.fullscreenAcquire, { ...basePayload(clientId), token });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const offReady = pi.events.on(EVENTS.ready, (data) => {
|
|
38
|
+
if (!isReadyPayload(data))
|
|
39
|
+
return;
|
|
40
|
+
attach();
|
|
41
|
+
});
|
|
42
|
+
pi.events.emit(EVENTS.hello, basePayload(clientId));
|
|
43
|
+
const fullscreen = {
|
|
44
|
+
acquire() {
|
|
45
|
+
const token = `${clientId}-lease-${nextLeaseId++}`;
|
|
46
|
+
let released = false;
|
|
47
|
+
const wasFallbackVisible = !coordinated && leases.size === 0;
|
|
48
|
+
leases.add(token);
|
|
49
|
+
if (coordinated) {
|
|
50
|
+
pi.events.emit(EVENTS.fullscreenAcquire, { ...basePayload(clientId), token });
|
|
51
|
+
}
|
|
52
|
+
else if (wasFallbackVisible) {
|
|
53
|
+
for (const record of widgets.values())
|
|
54
|
+
clearFallback(record);
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
release() {
|
|
58
|
+
if (released)
|
|
59
|
+
return;
|
|
60
|
+
released = true;
|
|
61
|
+
leases.delete(token);
|
|
62
|
+
if (coordinated && !disposed) {
|
|
63
|
+
pi.events.emit(EVENTS.fullscreenRelease, { ...basePayload(clientId), token });
|
|
64
|
+
}
|
|
65
|
+
else if (!disposed && leases.size === 0) {
|
|
66
|
+
for (const record of widgets.values())
|
|
67
|
+
restoreFallback(record);
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
return {
|
|
74
|
+
get mode() {
|
|
75
|
+
return coordinated ? "coordinated" : "fallback";
|
|
76
|
+
},
|
|
77
|
+
widgets: {
|
|
78
|
+
set(placement, key, factory, setOpts = {}) {
|
|
79
|
+
if (disposed)
|
|
80
|
+
return;
|
|
81
|
+
const record = { placement, key, factory, order: setOpts.order ?? 0 };
|
|
82
|
+
widgets.set(widgetId(placement, key), record);
|
|
83
|
+
if (coordinated) {
|
|
84
|
+
emitRegister(record);
|
|
85
|
+
}
|
|
86
|
+
else if (leases.size === 0) {
|
|
87
|
+
opts.ctx.ui.setWidget(key, factory, { placement });
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
remove(placement, key) {
|
|
91
|
+
const id = widgetId(placement, key);
|
|
92
|
+
const record = widgets.get(id);
|
|
93
|
+
if (!record)
|
|
94
|
+
return;
|
|
95
|
+
widgets.delete(id);
|
|
96
|
+
if (coordinated) {
|
|
97
|
+
pi.events.emit(EVENTS.unregisterWidget, { ...basePayload(clientId), placement, key });
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
clearFallback(record);
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
fullscreen,
|
|
105
|
+
dispose() {
|
|
106
|
+
if (disposed)
|
|
107
|
+
return;
|
|
108
|
+
disposed = true;
|
|
109
|
+
offReady();
|
|
110
|
+
if (coordinated) {
|
|
111
|
+
pi.events.emit(EVENTS.unregisterWidget, { ...basePayload(clientId), all: true });
|
|
112
|
+
for (const token of leases) {
|
|
113
|
+
pi.events.emit(EVENTS.fullscreenRelease, { ...basePayload(clientId), token });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
for (const record of widgets.values())
|
|
118
|
+
clearFallback(record);
|
|
119
|
+
}
|
|
120
|
+
leases.clear();
|
|
121
|
+
widgets.clear();
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function isReadyPayload(data) {
|
|
126
|
+
if (!data || typeof data !== "object")
|
|
127
|
+
return false;
|
|
128
|
+
const payload = data;
|
|
129
|
+
return typeof payload.protocolVersion === "number" && payload.protocolVersion <= PROTOCOL_VERSION && typeof payload.clientId === "string";
|
|
130
|
+
}
|