pi-extension-utils 0.3.1 → 0.3.2
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/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.d.ts → widgets/protocol.d.ts} +0 -0
- /package/dist/src/{protocol.js → widgets/protocol.js} +0 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
export const LOG_LEVELS = ["debug", "info", "warn", "error"];
|
|
3
|
+
export const loggerLevelSchema = Type.Union([
|
|
4
|
+
Type.Literal("debug"),
|
|
5
|
+
Type.Literal("info"),
|
|
6
|
+
Type.Literal("warn"),
|
|
7
|
+
Type.Literal("error"),
|
|
8
|
+
Type.Literal("silent"),
|
|
9
|
+
], {
|
|
10
|
+
default: "info",
|
|
11
|
+
description: "Minimum log level to write.",
|
|
12
|
+
});
|
|
13
|
+
export const loggingConfigSchema = Type.Object({
|
|
14
|
+
level: loggerLevelSchema,
|
|
15
|
+
maxFiles: Type.Number({
|
|
16
|
+
default: 3,
|
|
17
|
+
minimum: 0,
|
|
18
|
+
description: "Number of rotated log files to retain.",
|
|
19
|
+
}),
|
|
20
|
+
maxBytes: Type.Number({
|
|
21
|
+
default: 1048576,
|
|
22
|
+
minimum: 0,
|
|
23
|
+
description: "Rotate a log file when it grows beyond this many bytes. Use 0 to disable rotation.",
|
|
24
|
+
}),
|
|
25
|
+
}, {
|
|
26
|
+
description: "Default logger settings used when createLogger() options omit a value.",
|
|
27
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type LoggerLevel, type LogLevel } from "./config.ts";
|
|
2
|
+
export { LOG_LEVELS, loggerLevelSchema, loggingConfigSchema, type LoggerLevel, type LoggingConfig, type LogLevel } from "./config.ts";
|
|
3
|
+
export interface LoggerOptions {
|
|
4
|
+
dir?: string;
|
|
5
|
+
maxBytes?: number;
|
|
6
|
+
/** Number of rotated files to retain: name.log.1, name.log.2, ... */
|
|
7
|
+
maxFiles?: number;
|
|
8
|
+
/** Minimum level to write. Defaults to utilsConfig.logging.level. */
|
|
9
|
+
level?: LoggerLevel;
|
|
10
|
+
}
|
|
11
|
+
export interface Logger {
|
|
12
|
+
debug(message: string): void;
|
|
13
|
+
info(message: string): void;
|
|
14
|
+
warn(message: string): void;
|
|
15
|
+
error(message: string): void;
|
|
16
|
+
log(level: LogLevel, message: string): void;
|
|
17
|
+
setLevel(level: LoggerLevel): void;
|
|
18
|
+
isEnabled(level: LogLevel): boolean;
|
|
19
|
+
}
|
|
20
|
+
export declare function createLogger(name: string, opts?: LoggerOptions): Logger;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { appendFileSync, existsSync, mkdirSync, renameSync, rmSync, statSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { readUtilsConfigOrDefaults } from "../utils-config.js";
|
|
5
|
+
export { LOG_LEVELS, loggerLevelSchema, loggingConfigSchema } from "./config.js";
|
|
6
|
+
const LEVEL_WEIGHT = {
|
|
7
|
+
debug: 10,
|
|
8
|
+
info: 20,
|
|
9
|
+
warn: 30,
|
|
10
|
+
error: 40,
|
|
11
|
+
silent: Number.POSITIVE_INFINITY,
|
|
12
|
+
};
|
|
13
|
+
export function createLogger(name, opts = {}) {
|
|
14
|
+
if (!name || name.includes("/") || name.includes("\\")) {
|
|
15
|
+
throw new Error("Logger name must not contain path separators");
|
|
16
|
+
}
|
|
17
|
+
const configured = opts.level === undefined || opts.maxBytes === undefined || opts.maxFiles === undefined
|
|
18
|
+
? readUtilsConfigOrDefaults().logging
|
|
19
|
+
: undefined;
|
|
20
|
+
const dir = opts.dir ?? join(getAgentDir(), "log");
|
|
21
|
+
const maxBytes = Math.max(0, Math.floor(opts.maxBytes ?? configured?.maxBytes ?? 1024 * 1024));
|
|
22
|
+
const maxFiles = Math.max(0, Math.floor(opts.maxFiles ?? configured?.maxFiles ?? 3));
|
|
23
|
+
let level = opts.level ?? configured?.level ?? "info";
|
|
24
|
+
const file = join(dir, `${name}.log`);
|
|
25
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
26
|
+
function rotateIfNeeded(nextBytes) {
|
|
27
|
+
if (maxBytes <= 0 || !existsSync(file))
|
|
28
|
+
return;
|
|
29
|
+
const size = statSync(file).size;
|
|
30
|
+
if (size + nextBytes <= maxBytes)
|
|
31
|
+
return;
|
|
32
|
+
if (maxFiles <= 0) {
|
|
33
|
+
rmSync(file);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const oldest = `${file}.${maxFiles}`;
|
|
37
|
+
if (existsSync(oldest))
|
|
38
|
+
rmSync(oldest);
|
|
39
|
+
for (let index = maxFiles - 1; index >= 1; index--) {
|
|
40
|
+
const from = `${file}.${index}`;
|
|
41
|
+
if (existsSync(from))
|
|
42
|
+
renameSync(from, `${file}.${index + 1}`);
|
|
43
|
+
}
|
|
44
|
+
renameSync(file, `${file}.1`);
|
|
45
|
+
}
|
|
46
|
+
function isEnabled(candidate) {
|
|
47
|
+
return LEVEL_WEIGHT[candidate] >= LEVEL_WEIGHT[level];
|
|
48
|
+
}
|
|
49
|
+
function log(candidate, message) {
|
|
50
|
+
if (!isEnabled(candidate))
|
|
51
|
+
return;
|
|
52
|
+
const line = `${new Date().toISOString()} ${candidate} ${message}\n`;
|
|
53
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
54
|
+
rotateIfNeeded(Buffer.byteLength(line));
|
|
55
|
+
appendFileSync(file, line);
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
debug(message) {
|
|
59
|
+
log("debug", message);
|
|
60
|
+
},
|
|
61
|
+
info(message) {
|
|
62
|
+
log("info", message);
|
|
63
|
+
},
|
|
64
|
+
warn(message) {
|
|
65
|
+
log("warn", message);
|
|
66
|
+
},
|
|
67
|
+
error(message) {
|
|
68
|
+
log("error", message);
|
|
69
|
+
},
|
|
70
|
+
log,
|
|
71
|
+
setLevel(nextLevel) {
|
|
72
|
+
level = nextLevel;
|
|
73
|
+
},
|
|
74
|
+
isEnabled,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { dispatchNavKeys } from "./key-dispatch.js";
|
|
2
|
-
import { computeFixedSidebarLayout, computeSplitPaneLayout, endCursor, endScrollOffset, ensureCursorVisible, homeCursor, homeScrollOffset, moveCursor, moveScrollOffset, pageCursor, pageScrollOffset, resizeSplitPane, } from "./
|
|
3
|
-
import { boxRow, clipStyled, flatRule, formatScrollInfo, padRight, renderKeyRow, titledBottomSegment, titledTopSegment, } from "./
|
|
2
|
+
import { computeFixedSidebarLayout, computeSplitPaneLayout, endCursor, endScrollOffset, ensureCursorVisible, homeCursor, homeScrollOffset, moveCursor, moveScrollOffset, pageCursor, pageScrollOffset, resizeSplitPane, } from "./state.js";
|
|
3
|
+
import { boxRow, clipStyled, flatRule, formatScrollInfo, padRight, renderKeyRow, titledBottomSegment, titledTopSegment, } from "./chrome.js";
|
|
4
4
|
import { matchesKey } from "@earendil-works/pi-tui";
|
|
5
5
|
const DEFAULT_CLOSE_KEYS = ["escape", "ctrl+c", "q"];
|
|
6
6
|
function resolveValue(value, ctx) {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { type ReminderAnnounceNowRequest, type ReminderIntent, type ReminderSnapshot } from "./types.ts";
|
|
3
|
+
export interface RemindersClient {
|
|
4
|
+
upsert(intent: ReminderIntent): void;
|
|
5
|
+
remove(source: string, key: string): void;
|
|
6
|
+
clearSource(source: string): void;
|
|
7
|
+
announceNow(payload?: ReminderAnnounceNowRequest): void;
|
|
8
|
+
list(source?: string): Promise<ReminderSnapshot>;
|
|
9
|
+
}
|
|
10
|
+
export declare function createRemindersClient(pi: ExtensionAPI, isDisposed: () => boolean): RemindersClient;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { REMINDER_ANNOUNCE_NOW_EVENT, REMINDER_CLEAR_SOURCE_EVENT, REMINDER_LIST_EVENT, REMINDER_REMOVE_EVENT, REMINDER_UPSERT_EVENT, } from "./types.js";
|
|
2
|
+
export function createRemindersClient(pi, isDisposed) {
|
|
3
|
+
return {
|
|
4
|
+
upsert(intent) {
|
|
5
|
+
if (isDisposed())
|
|
6
|
+
return;
|
|
7
|
+
pi.events.emit(REMINDER_UPSERT_EVENT, intent);
|
|
8
|
+
},
|
|
9
|
+
remove(source, key) {
|
|
10
|
+
if (isDisposed())
|
|
11
|
+
return;
|
|
12
|
+
pi.events.emit(REMINDER_REMOVE_EVENT, { source, id: key });
|
|
13
|
+
},
|
|
14
|
+
clearSource(source) {
|
|
15
|
+
if (isDisposed())
|
|
16
|
+
return;
|
|
17
|
+
pi.events.emit(REMINDER_CLEAR_SOURCE_EVENT, { source });
|
|
18
|
+
},
|
|
19
|
+
announceNow(payload = {}) {
|
|
20
|
+
if (isDisposed())
|
|
21
|
+
return;
|
|
22
|
+
pi.events.emit(REMINDER_ANNOUNCE_NOW_EVENT, payload);
|
|
23
|
+
},
|
|
24
|
+
list(source) {
|
|
25
|
+
if (isDisposed())
|
|
26
|
+
return Promise.resolve(emptyReminderSnapshot());
|
|
27
|
+
let settled = false;
|
|
28
|
+
let resolveSnapshot;
|
|
29
|
+
let rejectSnapshot;
|
|
30
|
+
const promise = new Promise((resolve, reject) => {
|
|
31
|
+
resolveSnapshot = (snapshot) => {
|
|
32
|
+
settled = true;
|
|
33
|
+
resolve(snapshot);
|
|
34
|
+
};
|
|
35
|
+
rejectSnapshot = (error) => {
|
|
36
|
+
settled = true;
|
|
37
|
+
reject(error);
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
pi.events.emit(REMINDER_LIST_EVENT, { source, resolve: resolveSnapshot, reject: rejectSnapshot });
|
|
41
|
+
if (!settled)
|
|
42
|
+
resolveSnapshot(emptyReminderSnapshot());
|
|
43
|
+
return promise;
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function emptyReminderSnapshot() {
|
|
48
|
+
return { reminders: [], count: 0 };
|
|
49
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
export
|
|
6
|
-
export declare function saveConfig(config: RemindersConfig, cwd: string): void;
|
|
1
|
+
import { Type, type Static } from "typebox";
|
|
2
|
+
export declare const remindersConfigSchema: Type.TObject<{
|
|
3
|
+
debugShowAllInTui: Type.TBoolean;
|
|
4
|
+
}>;
|
|
5
|
+
export type RemindersConfig = Static<typeof remindersConfigSchema>;
|
|
@@ -1,23 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
return {};
|
|
11
|
-
return {
|
|
12
|
-
debugShowAllInTui: typeof parsed.debugShowAllInTui === "boolean" ? parsed.debugShowAllInTui : undefined,
|
|
13
|
-
};
|
|
14
|
-
}
|
|
15
|
-
catch {
|
|
16
|
-
return {};
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
export function saveConfig(config, cwd) {
|
|
20
|
-
const filePath = configPath(cwd);
|
|
21
|
-
mkdirSync(dirname(filePath), { recursive: true });
|
|
22
|
-
writeFileSync(filePath, JSON.stringify(config, null, 2));
|
|
23
|
-
}
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
export const remindersConfigSchema = Type.Object({
|
|
3
|
+
debugShowAllInTui: Type.Boolean({
|
|
4
|
+
default: false,
|
|
5
|
+
description: "Show reminders with display:false in the transcript UI for debugging.",
|
|
6
|
+
}),
|
|
7
|
+
}, {
|
|
8
|
+
description: "Reminder host settings.",
|
|
9
|
+
});
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { appendFileSync, mkdirSync } from "node:fs";
|
|
3
|
-
import { homedir } from "node:os";
|
|
4
3
|
import { dirname, join } from "node:path";
|
|
4
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import { deriveLabel } from "./renderer.js";
|
|
6
|
-
const DEBUG_LOG_PATH = join(
|
|
6
|
+
const DEBUG_LOG_PATH = join(getAgentDir(), "log", "reminders.jsonl");
|
|
7
7
|
export function buildReminderCacheDiagnostics(input) {
|
|
8
8
|
const before = getProviderMessages(input.payload);
|
|
9
9
|
const after = getProviderMessages(input.resultPayload);
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { ReminderManager } from "./manager.ts";
|
|
3
|
+
/** Register the shared reminder host hooks and event listeners. */
|
|
4
|
+
export declare function registerReminderHost(pi: ExtensionAPI, manager?: ReminderManager): ReminderManager;
|
|
5
|
+
export default function reminderHost(pi: ExtensionAPI): void;
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { Text, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
2
|
+
import { readUtilsConfigOrDefaults, utilsConfig } from "../utils-config.js";
|
|
3
|
+
import { writeReminderDiagnostic } from "./debug.js";
|
|
4
|
+
import { ReminderManager } from "./manager.js";
|
|
5
|
+
import { DEBUG_REMINDER_SOURCE, LEGACY_REMINDER_MESSAGE_CUSTOM_TYPE, REMINDER_ANNOUNCE_NOW_EVENT, REMINDER_CLEAR_SOURCE_EVENT, REMINDER_LIST_EVENT, REMINDER_MESSAGE_CUSTOM_TYPE, REMINDER_REMOVE_EVENT, REMINDER_UPSERT_EVENT, } from "./types.js";
|
|
6
|
+
/** Register the shared reminder host hooks and event listeners. */
|
|
7
|
+
export function registerReminderHost(pi, manager = new ReminderManager()) {
|
|
8
|
+
const eventBus = pi.events;
|
|
9
|
+
let cfg = readUtilsConfigOrDefaults("reload").reminders;
|
|
10
|
+
const refreshConfig = () => {
|
|
11
|
+
cfg = readUtilsConfigOrDefaults("reload").reminders;
|
|
12
|
+
};
|
|
13
|
+
const debugShowAllInTui = () => isDebugModeEnabled(cfg);
|
|
14
|
+
writeReminderDiagnostic({
|
|
15
|
+
event: "reminder_extension_init",
|
|
16
|
+
});
|
|
17
|
+
writeReminderDiagnostic({
|
|
18
|
+
event: "reminder_extension_register_start",
|
|
19
|
+
});
|
|
20
|
+
eventBus.on(REMINDER_UPSERT_EVENT, (payload) => {
|
|
21
|
+
if (isRecord(payload))
|
|
22
|
+
manager.upsert(payload);
|
|
23
|
+
});
|
|
24
|
+
eventBus.on(REMINDER_REMOVE_EVENT, (payload) => {
|
|
25
|
+
if (isRecord(payload))
|
|
26
|
+
manager.remove(payload);
|
|
27
|
+
});
|
|
28
|
+
eventBus.on(REMINDER_CLEAR_SOURCE_EVENT, (payload) => {
|
|
29
|
+
if (isRecord(payload))
|
|
30
|
+
manager.clearSource(payload);
|
|
31
|
+
});
|
|
32
|
+
eventBus.on(REMINDER_ANNOUNCE_NOW_EVENT, (payload) => {
|
|
33
|
+
manager.forceAnnounce(isRecord(payload) ? payload : {});
|
|
34
|
+
});
|
|
35
|
+
eventBus.on(REMINDER_LIST_EVENT, (payload) => {
|
|
36
|
+
if (isRecord(payload) && typeof payload.resolve === "function") {
|
|
37
|
+
handleListRequest(manager, payload);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
if (typeof pi.registerMessageRenderer === "function") {
|
|
41
|
+
const renderer = (message, _options, theme) => {
|
|
42
|
+
const details = isRecord(message.details) ? message.details : undefined;
|
|
43
|
+
const content = typeof details?.displayText === "string"
|
|
44
|
+
? details.displayText
|
|
45
|
+
: stripSystemReminderWrapper(extractTextContent(message.content));
|
|
46
|
+
return new SystemReminderComponent(content, details, theme);
|
|
47
|
+
};
|
|
48
|
+
pi.registerMessageRenderer(REMINDER_MESSAGE_CUSTOM_TYPE, renderer);
|
|
49
|
+
pi.registerMessageRenderer(LEGACY_REMINDER_MESSAGE_CUSTOM_TYPE, renderer);
|
|
50
|
+
}
|
|
51
|
+
pi.on("session_start", (_event, ctx) => {
|
|
52
|
+
manager.clearSession();
|
|
53
|
+
refreshConfig();
|
|
54
|
+
writeReminderDiagnostic({
|
|
55
|
+
event: "reminder_session_start",
|
|
56
|
+
sessionId: getSessionId(ctx),
|
|
57
|
+
cwd: getCwd(ctx),
|
|
58
|
+
providerApi: getProviderApi(ctx),
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
pi.on("session_shutdown", () => {
|
|
62
|
+
manager.clearSession();
|
|
63
|
+
});
|
|
64
|
+
pi.on("turn_start", () => {
|
|
65
|
+
manager.advanceTurn();
|
|
66
|
+
});
|
|
67
|
+
pi.on("before_agent_start", (_event, ctx) => {
|
|
68
|
+
const announcement = manager.collectDueAnnouncement({ showHiddenInDisplay: debugShowAllInTui() });
|
|
69
|
+
if (!announcement)
|
|
70
|
+
return;
|
|
71
|
+
const message = buildReminderMessage(announcement, getContextUsage(ctx));
|
|
72
|
+
writeAnnouncementDiagnostic("reminder_history_message", message);
|
|
73
|
+
return { message };
|
|
74
|
+
});
|
|
75
|
+
pi.on("context", (_event, ctx) => {
|
|
76
|
+
const announcement = manager.collectDueAnnouncement({ showHiddenInDisplay: debugShowAllInTui() });
|
|
77
|
+
if (!announcement)
|
|
78
|
+
return;
|
|
79
|
+
const message = buildReminderMessage(announcement, getContextUsage(ctx));
|
|
80
|
+
writeAnnouncementDiagnostic("reminder_context_message", message);
|
|
81
|
+
pi.sendMessage(message, { deliverAs: "steer" });
|
|
82
|
+
});
|
|
83
|
+
pi.registerCommand("reminders", {
|
|
84
|
+
description: "Open reminders settings",
|
|
85
|
+
handler: async (_args, ctx) => {
|
|
86
|
+
refreshConfig();
|
|
87
|
+
const current = debugShowAllInTui() ? "on" : "off";
|
|
88
|
+
const choice = await ctx.ui.select("Reminders debug: show all in TUI", [`${current === "on" ? "[on]" : "on"}`, `${current === "off" ? "[off]" : "off"}`, "← Back"]);
|
|
89
|
+
if (!choice || choice === "← Back")
|
|
90
|
+
return;
|
|
91
|
+
cfg = utilsConfig.update({ reminders: { ...cfg, debugShowAllInTui: choice.includes("on") } }).reminders;
|
|
92
|
+
ctx.ui.notify(`Reminders debug show-all in TUI ${cfg.debugShowAllInTui ? "enabled" : "disabled"}`, "info");
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
pi.registerCommand("remind", {
|
|
96
|
+
description: "Add a debug system reminder",
|
|
97
|
+
handler: async (args, ctx) => {
|
|
98
|
+
const text = args.trim();
|
|
99
|
+
if (!text) {
|
|
100
|
+
ctx.ui.notify("Usage: /remind <text>", "warning");
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
manager.upsert({
|
|
104
|
+
source: DEBUG_REMINDER_SOURCE,
|
|
105
|
+
id: "debug-command",
|
|
106
|
+
label: "Reminder",
|
|
107
|
+
text,
|
|
108
|
+
priority: 100,
|
|
109
|
+
ttl: "once",
|
|
110
|
+
});
|
|
111
|
+
ctx.ui.notify("Reminder added", "info");
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
writeReminderDiagnostic({
|
|
115
|
+
event: "reminder_extension_registered",
|
|
116
|
+
hasEvents: eventBus !== undefined,
|
|
117
|
+
hasRegisterCommand: typeof pi.registerCommand === "function",
|
|
118
|
+
hasRegisterMessageRenderer: typeof pi.registerMessageRenderer === "function",
|
|
119
|
+
hasOn: typeof pi.on === "function",
|
|
120
|
+
});
|
|
121
|
+
return manager;
|
|
122
|
+
}
|
|
123
|
+
export default function reminderHost(pi) {
|
|
124
|
+
try {
|
|
125
|
+
registerReminderHost(pi);
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
writeReminderDiagnostic({
|
|
129
|
+
event: "reminder_extension_register_error",
|
|
130
|
+
errorName: error instanceof Error ? error.name : undefined,
|
|
131
|
+
errorMessage: error instanceof Error ? error.message : String(error),
|
|
132
|
+
errorStack: error instanceof Error ? error.stack : undefined,
|
|
133
|
+
});
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function handleListRequest(manager, request) {
|
|
138
|
+
try {
|
|
139
|
+
request.resolve(manager.snapshot(request.source));
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
request.reject?.(error);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function isDebugModeEnabled(config) {
|
|
146
|
+
const value = process.env.PI_REMINDERS_DEBUG?.trim().toLowerCase();
|
|
147
|
+
if (value)
|
|
148
|
+
return value === "1" || value === "true" || value === "yes" || value === "on";
|
|
149
|
+
return config.debugShowAllInTui === true;
|
|
150
|
+
}
|
|
151
|
+
function buildReminderMessage(announcement, usage) {
|
|
152
|
+
const sources = [...new Set(announcement.reminders.map((reminder) => reminder.source))].sort();
|
|
153
|
+
const modelBody = includesDcpReminder(announcement.reminders)
|
|
154
|
+
? appendContextUsageLine(stripSystemReminderWrapper(announcement.text), usage)
|
|
155
|
+
: stripSystemReminderWrapper(announcement.text);
|
|
156
|
+
const displayText = announcement.displayText && includesDcpReminder(announcement.displayReminders)
|
|
157
|
+
? appendContextUsageLine(announcement.displayText, usage)
|
|
158
|
+
: announcement.displayText;
|
|
159
|
+
const content = `<system-reminder>\n${modelBody}\n</system-reminder>`;
|
|
160
|
+
return {
|
|
161
|
+
customType: REMINDER_MESSAGE_CUSTOM_TYPE,
|
|
162
|
+
content,
|
|
163
|
+
display: displayText !== null,
|
|
164
|
+
details: {
|
|
165
|
+
reminderCount: announcement.reminders.length,
|
|
166
|
+
sources,
|
|
167
|
+
displayText: displayText ?? undefined,
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
function writeAnnouncementDiagnostic(event, message) {
|
|
172
|
+
const details = message.details;
|
|
173
|
+
writeReminderDiagnostic({
|
|
174
|
+
event,
|
|
175
|
+
activeReminderCount: details?.reminderCount,
|
|
176
|
+
sources: details?.sources,
|
|
177
|
+
trailerChars: message.content.length,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
function getContextUsage(ctx) {
|
|
181
|
+
if (!isRecord(ctx) || typeof ctx.getContextUsage !== "function")
|
|
182
|
+
return undefined;
|
|
183
|
+
try {
|
|
184
|
+
const usage = ctx.getContextUsage();
|
|
185
|
+
if (!isRecord(usage) || typeof usage.contextWindow !== "number")
|
|
186
|
+
return undefined;
|
|
187
|
+
const tokens = typeof usage.tokens === "number" ? usage.tokens : null;
|
|
188
|
+
return { tokens, contextWindow: usage.contextWindow };
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
return undefined;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function includesDcpReminder(reminders) {
|
|
195
|
+
return reminders.some((reminder) => {
|
|
196
|
+
const source = reminder.source.trim().toLowerCase();
|
|
197
|
+
return source === "dcp" || source === "pi-dynamic-context-pruning";
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
function appendContextUsageLine(displayText, usage) {
|
|
201
|
+
if (!usage)
|
|
202
|
+
return displayText;
|
|
203
|
+
const contextWindow = formatTokenCount(usage.contextWindow);
|
|
204
|
+
if (usage.tokens === null)
|
|
205
|
+
return `${displayText}\nContext: unknown / ${contextWindow} tokens.`;
|
|
206
|
+
const percent = usage.contextWindow > 0 ? Math.round((usage.tokens / usage.contextWindow) * 100) : 0;
|
|
207
|
+
return `${displayText}\nContext: ${formatTokenCount(usage.tokens)} / ${contextWindow} tokens (${percent}%).`;
|
|
208
|
+
}
|
|
209
|
+
function formatTokenCount(tokens) {
|
|
210
|
+
if (tokens >= 1_000_000)
|
|
211
|
+
return `${trimFixed(tokens / 1_000_000)}M`;
|
|
212
|
+
if (tokens >= 1_000)
|
|
213
|
+
return `${trimFixed(tokens / 1_000)}k`;
|
|
214
|
+
return String(tokens);
|
|
215
|
+
}
|
|
216
|
+
function trimFixed(value) {
|
|
217
|
+
return value.toFixed(value >= 10 ? 0 : 1).replace(/\.0$/, "");
|
|
218
|
+
}
|
|
219
|
+
function getProviderApi(ctx) {
|
|
220
|
+
return typeof ctx?.model?.api === "string" ? ctx.model.api : undefined;
|
|
221
|
+
}
|
|
222
|
+
function getSessionId(ctx) {
|
|
223
|
+
try {
|
|
224
|
+
return ctx?.sessionManager?.getSessionId?.();
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
return undefined;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
function getCwd(ctx) {
|
|
231
|
+
try {
|
|
232
|
+
return ctx?.sessionManager?.getCwd?.();
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
return undefined;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
function extractTextContent(content) {
|
|
239
|
+
if (typeof content === "string")
|
|
240
|
+
return content;
|
|
241
|
+
if (!Array.isArray(content))
|
|
242
|
+
return "";
|
|
243
|
+
return content
|
|
244
|
+
.filter((part) => isRecord(part) && part.type === "text" && typeof part.text === "string")
|
|
245
|
+
.map((part) => part.text)
|
|
246
|
+
.join("\n");
|
|
247
|
+
}
|
|
248
|
+
function stripSystemReminderWrapper(content) {
|
|
249
|
+
return content
|
|
250
|
+
.replace(/^\s*<system-reminder>\s*/i, "")
|
|
251
|
+
.replace(/\s*<\/system-reminder>\s*$/i, "")
|
|
252
|
+
.trim();
|
|
253
|
+
}
|
|
254
|
+
function padToWidth(line, width) {
|
|
255
|
+
return `${line}${" ".repeat(Math.max(0, width - visibleWidth(line)))}`;
|
|
256
|
+
}
|
|
257
|
+
const SYSTEM_REMINDER_MIN_BODY_WIDTH = 76;
|
|
258
|
+
const SYSTEM_REMINDER_BODY_WIDTH_RATIO = 0.9;
|
|
259
|
+
class SystemReminderComponent {
|
|
260
|
+
content;
|
|
261
|
+
details;
|
|
262
|
+
theme;
|
|
263
|
+
constructor(content, details, theme) {
|
|
264
|
+
this.content = content;
|
|
265
|
+
this.details = details;
|
|
266
|
+
this.theme = theme;
|
|
267
|
+
}
|
|
268
|
+
invalidate() { }
|
|
269
|
+
render(width) {
|
|
270
|
+
if (width < 3)
|
|
271
|
+
return [truncateToWidth("System Reminder", width)];
|
|
272
|
+
const availableBodyWidth = width - 2;
|
|
273
|
+
const preferredBodyWidth = Math.max(SYSTEM_REMINDER_MIN_BODY_WIDTH, Math.floor(availableBodyWidth * SYSTEM_REMINDER_BODY_WIDTH_RATIO));
|
|
274
|
+
const bodyWidth = Math.max(1, Math.min(availableBodyWidth, preferredBodyWidth));
|
|
275
|
+
const title = " System Reminder ";
|
|
276
|
+
const titleText = truncateToWidth(title, bodyWidth, "");
|
|
277
|
+
const border = (value) => this.theme.fg("accent", value);
|
|
278
|
+
const lines = [border(`╭${titleText}${"─".repeat(Math.max(0, bodyWidth - visibleWidth(titleText)))}╮`)];
|
|
279
|
+
const text = new Text(this.content, 0, 0);
|
|
280
|
+
for (const line of text.render(bodyWidth)) {
|
|
281
|
+
const truncated = truncateToWidth(line, bodyWidth, "");
|
|
282
|
+
lines.push(border("│") + padToWidth(truncated, bodyWidth) + border("│"));
|
|
283
|
+
}
|
|
284
|
+
const meta = [
|
|
285
|
+
this.details?.reminderCount !== undefined ? `${this.details.reminderCount} reminder${this.details.reminderCount === 1 ? "" : "s"}` : undefined,
|
|
286
|
+
this.details?.sources?.length ? this.details.sources.join(", ") : undefined,
|
|
287
|
+
].filter(Boolean).join(" · ");
|
|
288
|
+
if (meta) {
|
|
289
|
+
const metaLine = truncateToWidth(this.theme.fg("dim", meta), bodyWidth, "");
|
|
290
|
+
lines.push(border("│") + padToWidth(metaLine, bodyWidth) + border("│"));
|
|
291
|
+
}
|
|
292
|
+
lines.push(border(`╰${"─".repeat(bodyWidth)}╯`));
|
|
293
|
+
return lines;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
function isRecord(value) {
|
|
297
|
+
return typeof value === "object" && value !== null;
|
|
298
|
+
}
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
export declare function registerReminderHost(pi: ExtensionAPI, manager?: ReminderManager): ReminderManager;
|
|
5
|
-
export default function reminderHost(pi: ExtensionAPI): void;
|
|
1
|
+
export { default, registerReminderHost } from "./host.ts";
|
|
2
|
+
export { remindersConfigSchema, type RemindersConfig } from "./config.ts";
|
|
3
|
+
export * from "./types.ts";
|