pi-extension-utils 0.3.0 → 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 +90 -27
- 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 -6
- package/dist/src/index.js +10 -6
- 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/{tui-chrome.js → pane/chrome.js} +22 -13
- package/dist/src/pane/overlay.d.ts +75 -0
- package/dist/src/pane/overlay.js +492 -0
- 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/{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
|
+
}
|
|
@@ -113,28 +113,37 @@ export function titledTopSegment(theme, opts) {
|
|
|
113
113
|
const fillDashes = Math.max(1, width - labelLen - 3);
|
|
114
114
|
return clipStyled(`${dash(1)} ${labelStyled} ${dash(fillDashes)}`, width);
|
|
115
115
|
}
|
|
116
|
+
const width = Math.max(0, opts.width);
|
|
117
|
+
if (width <= 0)
|
|
118
|
+
return "";
|
|
116
119
|
const labelColor = opts.labelColor ?? "text";
|
|
117
120
|
const tailColor = opts.tailColor ?? "dim";
|
|
118
|
-
|
|
119
|
-
const
|
|
121
|
+
const tailPlain = opts.tailPlain ?? opts.tail ?? "";
|
|
122
|
+
const rawTailRendered = opts.tailRendered ?? (opts.tail !== undefined ? theme.fg(tailColor, opts.tail) : "");
|
|
123
|
+
const tailLen = visibleWidth(tailPlain);
|
|
124
|
+
// With a tail, the minimum chrome is:
|
|
125
|
+
// `─ ` + label + ` ` + `─` + ` ` + tail + ` ` + `─` = label + tail + 7.
|
|
126
|
+
// The previous math reserved only 6 columns, so long label+tail combinations
|
|
127
|
+
// could render one column too wide and crash Pi's TUI at terminal width.
|
|
128
|
+
const tailFits = tailLen > 0 && width >= tailLen + 7;
|
|
129
|
+
const labelBudget = tailFits ? Math.max(0, width - tailLen - 7) : Math.max(0, width - 3);
|
|
120
130
|
// Defensive: callers occasionally pass undefined labels for transient/legacy
|
|
121
131
|
// runs that lack agent+mode+label; treat as empty rather than crashing pi.
|
|
122
|
-
const
|
|
132
|
+
const rawLabel = opts.label ?? "";
|
|
133
|
+
const labelText = truncateToWidth(rawLabel, labelBudget, "").replace(/\u001b\[[0-9;]*m/g, "");
|
|
123
134
|
const labelStyled = opts.labelBold && theme.bold
|
|
124
135
|
? theme.bold(theme.fg(labelColor, labelText))
|
|
125
136
|
: theme.fg(labelColor, labelText);
|
|
126
|
-
const tailPlain = opts.tailPlain ?? opts.tail ?? "";
|
|
127
|
-
const tailRendered = opts.tailRendered ?? (opts.tail !== undefined ? theme.fg(tailColor, opts.tail) : "");
|
|
128
137
|
const labelLen = visibleWidth(labelText);
|
|
129
|
-
|
|
130
|
-
// Layout
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const fillDashes = Math.max(1,
|
|
134
|
-
return `${dash(1)} ${labelStyled} ${dash(fillDashes)} ${tailRendered} ${dash(1)}
|
|
138
|
+
// Layout with tail: `─ <label> ──…── <tail> ─`.
|
|
139
|
+
// Layout without tail: `─ <label> ──…────`.
|
|
140
|
+
if (tailFits) {
|
|
141
|
+
const tailRendered = visibleWidth(rawTailRendered) > tailLen ? clipStyled(rawTailRendered, tailLen) : rawTailRendered;
|
|
142
|
+
const fillDashes = Math.max(1, width - (labelLen + tailLen + 6));
|
|
143
|
+
return clipStyled(`${dash(1)} ${labelStyled} ${dash(fillDashes)} ${tailRendered} ${dash(1)}`, width);
|
|
135
144
|
}
|
|
136
|
-
const fillDashes = Math.max(
|
|
137
|
-
return `${dash(1)} ${labelStyled} ${dash(fillDashes)}
|
|
145
|
+
const fillDashes = Math.max(0, width - (labelLen + 3));
|
|
146
|
+
return clipStyled(`${dash(1)} ${labelStyled} ${dash(fillDashes)}`, width);
|
|
138
147
|
}
|
|
139
148
|
/**
|
|
140
149
|
* Build one half of a bottom border with an embedded hint string.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { FullscreenComponentFactory } from "../ui/client.ts";
|
|
2
|
+
export interface PaneOverlayContext<T = unknown, Row = unknown> {
|
|
3
|
+
tui: unknown;
|
|
4
|
+
primaryFocus: boolean;
|
|
5
|
+
detailFocus: boolean;
|
|
6
|
+
selectedRow: Row | undefined;
|
|
7
|
+
selectedIndex: number;
|
|
8
|
+
selectedKey: string;
|
|
9
|
+
primary: {
|
|
10
|
+
mode: "cursor" | "scroll";
|
|
11
|
+
cursor: number;
|
|
12
|
+
scrollOffset: number;
|
|
13
|
+
};
|
|
14
|
+
detail: {
|
|
15
|
+
scrollOffset: number;
|
|
16
|
+
};
|
|
17
|
+
close(result?: T): void;
|
|
18
|
+
requestRender(): void;
|
|
19
|
+
}
|
|
20
|
+
export interface PaneOverlayCustomAction<T = unknown, Row = unknown> {
|
|
21
|
+
keys: string | readonly string[];
|
|
22
|
+
label: string;
|
|
23
|
+
run(ctx: PaneOverlayContext<T, Row>): void;
|
|
24
|
+
when?(ctx: PaneOverlayContext<T, Row>): boolean;
|
|
25
|
+
showInLegend?: boolean;
|
|
26
|
+
}
|
|
27
|
+
export interface PrimaryPaneOptions<Row = unknown> {
|
|
28
|
+
mode?: "cursor" | "scroll";
|
|
29
|
+
rows: Row[] | ((ctx: PaneOverlayContext<unknown, Row>) => Row[]);
|
|
30
|
+
renderRow?(row: Row, ctx: PaneOverlayContext<unknown, Row>): string;
|
|
31
|
+
selectionKey?(row: Row, index: number): string;
|
|
32
|
+
onSelectionChange?(row: Row | undefined, index: number, key: string, ctx: PaneOverlayContext<unknown, Row>): void;
|
|
33
|
+
title?: string | ((ctx: PaneOverlayContext<unknown, Row>) => string);
|
|
34
|
+
footer?: string | ((ctx: PaneOverlayContext<unknown, Row>) => string);
|
|
35
|
+
}
|
|
36
|
+
export interface DetailPaneOptions<Row = unknown> {
|
|
37
|
+
rows(ctx: PaneOverlayContext<unknown, Row>): string[];
|
|
38
|
+
title?: string | ((ctx: PaneOverlayContext<unknown, Row>) => string);
|
|
39
|
+
footer?: string | ((ctx: PaneOverlayContext<unknown, Row>) => string);
|
|
40
|
+
}
|
|
41
|
+
export interface PaneOverlaySplitOptions {
|
|
42
|
+
initialFraction?: number;
|
|
43
|
+
minPrimaryWidth?: number;
|
|
44
|
+
minDetailWidth?: number;
|
|
45
|
+
maxPrimaryWidth?: number;
|
|
46
|
+
stepCols?: number;
|
|
47
|
+
minFraction?: number;
|
|
48
|
+
maxFraction?: number;
|
|
49
|
+
fractionBasis?: "total" | "interior";
|
|
50
|
+
}
|
|
51
|
+
export interface PaneCollapseOptions {
|
|
52
|
+
key?: string;
|
|
53
|
+
collapsedWidth?: number;
|
|
54
|
+
label?: string;
|
|
55
|
+
}
|
|
56
|
+
export interface PaneOverlayOptions<T = unknown, Row = unknown> {
|
|
57
|
+
primary: PrimaryPaneOptions<Row>;
|
|
58
|
+
detail: DetailPaneOptions<Row>;
|
|
59
|
+
height?: number | ((tui: unknown) => number);
|
|
60
|
+
closeKeys?: readonly string[];
|
|
61
|
+
closeResult?: T;
|
|
62
|
+
customActions?: readonly PaneOverlayCustomAction<T, Row>[];
|
|
63
|
+
bannedKeys?: readonly string[];
|
|
64
|
+
split?: PaneOverlaySplitOptions;
|
|
65
|
+
legendPlacement?: "footer" | "primary";
|
|
66
|
+
collapse?: PaneCollapseOptions;
|
|
67
|
+
perSelectionScroll?: boolean;
|
|
68
|
+
stickyBottom?: boolean;
|
|
69
|
+
}
|
|
70
|
+
export interface PaneOverlayComponent {
|
|
71
|
+
render(width: number): string[];
|
|
72
|
+
handleInput(data: string): void;
|
|
73
|
+
dispose(): void;
|
|
74
|
+
}
|
|
75
|
+
export declare function paneOverlay<T = undefined, Row = unknown>(options: PaneOverlayOptions<T, Row>): FullscreenComponentFactory<T>;
|