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.
Files changed (61) hide show
  1. package/README.md +90 -27
  2. package/dist/index.js +9 -140
  3. package/dist/src/client/index.d.ts +7 -0
  4. package/dist/src/client/index.js +25 -0
  5. package/dist/src/client/types.d.ts +17 -0
  6. package/dist/src/client/types.js +1 -0
  7. package/dist/src/config/index.d.ts +28 -0
  8. package/dist/src/config/index.js +222 -0
  9. package/dist/src/index.d.ts +10 -6
  10. package/dist/src/index.js +10 -6
  11. package/dist/src/logger/config.d.ts +11 -0
  12. package/dist/src/logger/config.js +27 -0
  13. package/dist/src/logger/index.d.ts +20 -0
  14. package/dist/src/logger/index.js +76 -0
  15. package/dist/src/{tui-chrome.js → pane/chrome.js} +22 -13
  16. package/dist/src/pane/overlay.d.ts +75 -0
  17. package/dist/src/pane/overlay.js +492 -0
  18. package/dist/src/reminders/client.d.ts +10 -0
  19. package/dist/src/reminders/client.js +49 -0
  20. package/dist/src/reminders/config.d.ts +5 -6
  21. package/dist/src/reminders/config.js +9 -23
  22. package/dist/src/reminders/debug.js +2 -2
  23. package/dist/src/reminders/host.d.ts +5 -0
  24. package/dist/src/reminders/host.js +298 -0
  25. package/dist/src/reminders/index.d.ts +3 -5
  26. package/dist/src/reminders/index.js +3 -308
  27. package/dist/src/reminders/types.d.ts +3 -0
  28. package/dist/src/reminders/types.js +3 -0
  29. package/dist/src/ui/client.d.ts +22 -0
  30. package/dist/src/ui/client.js +17 -0
  31. package/dist/src/utils-config.d.ts +24 -0
  32. package/dist/src/utils-config.js +38 -0
  33. package/dist/src/widgets/client.d.ts +27 -0
  34. package/dist/src/widgets/client.js +130 -0
  35. package/dist/src/widgets/host.d.ts +2 -0
  36. package/dist/src/widgets/host.js +143 -0
  37. package/docs/README.md +11 -0
  38. package/docs/client.md +91 -0
  39. package/docs/config.md +86 -0
  40. package/docs/pane-overlay.md +88 -0
  41. package/docs/reference/reminders-spec.md +351 -0
  42. package/docs/reminders.md +68 -0
  43. package/docs/widgets.md +57 -0
  44. package/examples/README.md +23 -0
  45. package/examples/config.ts +31 -0
  46. package/examples/logger.ts +20 -0
  47. package/examples/pane-overlay.ts +47 -0
  48. package/examples/reminders.ts +25 -0
  49. package/examples/widget-coordinator.ts +18 -0
  50. package/package.json +12 -1
  51. package/dist/src/client.d.ts +0 -47
  52. package/dist/src/client.js +0 -194
  53. package/dist/src/logger.d.ts +0 -12
  54. package/dist/src/logger.js +0 -45
  55. /package/dist/src/{tui-chrome.d.ts → pane/chrome.d.ts} +0 -0
  56. /package/dist/src/{key-dispatch.d.ts → pane/key-dispatch.d.ts} +0 -0
  57. /package/dist/src/{key-dispatch.js → pane/key-dispatch.js} +0 -0
  58. /package/dist/src/{pane-state.d.ts → pane/state.d.ts} +0 -0
  59. /package/dist/src/{pane-state.js → pane/state.js} +0 -0
  60. /package/dist/src/{protocol.d.ts → widgets/protocol.d.ts} +0 -0
  61. /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
- // Reserve at least `─ ` + label + ` ─` (4 chars) and 1 dash on each side of the tail when present.
119
- const labelBudget = opts.width <= 4 ? 0 : opts.width - 4;
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 labelText = truncateToWidth(opts.label ?? "", labelBudget);
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
- const tailLen = visibleWidth(tailPlain);
130
- // Layout with tail: `─ <label> ──…── <tail> ─` => fixed = 6 + labelLen + tailLen.
131
- // Layout without tail: `─ <label> ──…────` => fixed = 3 + labelLen (one space + label + space).
132
- if (tailLen > 0) {
133
- const fillDashes = Math.max(1, opts.width - (labelLen + tailLen + 6));
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(1, opts.width - (labelLen + 3));
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>;