pi-famulus 0.2.1 → 0.2.2-beta.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.
@@ -0,0 +1,130 @@
1
+ /**
2
+ * TUI widgets for intercom message cards.
3
+ *
4
+ * Upstream baseline: agent-intercom @ 98183fb, plugin/pi — verbatim port of
5
+ * the plugin's ChatText/ChatCard renderers and text-measurement helpers.
6
+ */
7
+
8
+ /** Rough display width: CJK ideographs and wide chars count as 2. */
9
+ export function stringWidth(s: string): number {
10
+ let w = 0;
11
+ for (const ch of s) {
12
+ const cp = ch.codePointAt(0) ?? 0;
13
+ w += cp >= 0x2e80 ? 2 : 1;
14
+ }
15
+ return w;
16
+ }
17
+
18
+ /** Word-wrap one line, treating ANSI SGR sequences as zero-width. */
19
+ export function wrapAnsiLine(line: string, width: number): string[] {
20
+ if (stringWidth(line.replace(/\x1b\[[0-9;]*m/g, "")) <= width) return [line];
21
+ const tokens = line.match(/(\x1b\[[0-9;]*m|\s+|[^\s\x1b]+)/g) ?? [line];
22
+ const lines: string[] = [];
23
+ let cur = "";
24
+ let curWidth = 0;
25
+ const push = () => {
26
+ lines.push(cur.trimEnd());
27
+ cur = "";
28
+ curWidth = 0;
29
+ };
30
+ for (const token of tokens) {
31
+ if (token.startsWith("\x1b")) {
32
+ cur += token;
33
+ continue;
34
+ }
35
+ if (/^\s+$/.test(token)) {
36
+ if (cur && curWidth + 1 <= width) {
37
+ cur += " ";
38
+ curWidth += 1;
39
+ }
40
+ continue;
41
+ }
42
+ let piece = token;
43
+ let pieceWidth = stringWidth(piece);
44
+ if (curWidth > 0 && curWidth + pieceWidth > width) push();
45
+ while (pieceWidth > width) {
46
+ let cut = 0;
47
+ let cutWidth = 0;
48
+ for (const ch of piece) {
49
+ const w = stringWidth(ch);
50
+ if (cutWidth + w > width) break;
51
+ cutWidth += w;
52
+ cut += ch.length;
53
+ }
54
+ cur += piece.slice(0, cut);
55
+ push();
56
+ piece = piece.slice(cut);
57
+ pieceWidth = stringWidth(piece);
58
+ }
59
+ cur += piece;
60
+ curWidth += pieceWidth;
61
+ }
62
+ if (cur.trimEnd()) push();
63
+ return lines.length ? lines : [""];
64
+ }
65
+
66
+ /** Local wall-clock `YYYY-mm-dd HH:ss`. */
67
+ export function formatTimestamp(d: Date): string {
68
+ const p = (n: number) => String(n).padStart(2, "0");
69
+ return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
70
+ }
71
+
72
+ /** Minimal Text component satisfying pi's Component contract; no pi-tui dependency. */
73
+ export class ChatText {
74
+ private text: string;
75
+ private paddingX: number;
76
+
77
+ constructor(text: string, paddingX = 1) {
78
+ this.text = text;
79
+ this.paddingX = paddingX;
80
+ }
81
+
82
+ invalidate(): void {}
83
+
84
+ render(width: number): string[] {
85
+ const padding = Math.min(this.paddingX, Math.max(0, Math.floor((width - 1) / 2)));
86
+ const pad = " ".repeat(padding);
87
+ const contentWidth = Math.max(1, width - padding * 2);
88
+ const out: string[] = [];
89
+ for (const line of this.text.split("\n")) {
90
+ for (const wrapped of wrapAnsiLine(line, contentWidth)) {
91
+ out.push(pad + wrapped);
92
+ }
93
+ }
94
+ return out;
95
+ }
96
+ }
97
+
98
+ /** Boxed card with a full-width background, matching pi's custom message look. */
99
+ export class ChatCard {
100
+ private header: string;
101
+ private body: string;
102
+ private bg: (text: string) => string;
103
+
104
+ constructor(header: string, body: string, bg: (text: string) => string) {
105
+ this.header = header;
106
+ this.body = body;
107
+ this.bg = bg;
108
+ }
109
+
110
+ invalidate(): void {}
111
+
112
+ render(width: number): string[] {
113
+ const px = 1;
114
+ const contentWidth = Math.max(1, width - px * 2);
115
+ const out: string[] = [this.bg(" ".repeat(width))];
116
+ const padLine = (s: string) => {
117
+ const visible = stringWidth(s.replace(/\x1b\[[0-9;]*m/g, ""));
118
+ return this.bg(" ".repeat(px) + s + " ".repeat(Math.max(0, width - px - visible)));
119
+ };
120
+ for (const line of `${this.header}\n${this.body}`.split("\n")) {
121
+ for (const wrapped of wrapAnsiLine(line, contentWidth)) {
122
+ out.push(padLine(wrapped));
123
+ }
124
+ }
125
+ out.push(this.bg(" ".repeat(width)));
126
+ // One transparent line below the card as extra bottom margin.
127
+ out.push("");
128
+ return out;
129
+ }
130
+ }
@@ -18,10 +18,11 @@ import { famulusPaths } from "./config";
18
18
  import { realClock, type Clock, type ClockTimer } from "./clock";
19
19
 
20
20
  const MAX_FRAME_BYTES = 4 * 1024 * 1024; // 4 MiB (§3.3)
21
- const EXTENSION_VERSION = "0.2.1";
21
+ const EXTENSION_VERSION = "0.2.2-beta.0";
22
22
  export { EXTENSION_VERSION };
23
- // 5: speaks per-kind agent admission and queued acquire.
24
- const PROTOCOL = 6;
23
+ // 6: hosted subagents. 7: the extension's messaging bridge (`intercom`
24
+ // request plus `intercom`/`intercom_reset` events).
25
+ const PROTOCOL = 7;
25
26
  const HELLO_TIMEOUT_MS = 5000;
26
27
  const RECONNECT_HELLO_TIMEOUT_MS = 25_000;
27
28
  const DEFAULT_REQUEST_TIMEOUT_MS = 30000;
@@ -108,7 +109,14 @@ export interface MonitorOutputRecord {
108
109
 
109
110
  /** Server-pushed event (§3.3). Fields beyond `event` depend on the event kind. */
110
111
  export interface ManagerEvent {
111
- event: "task_started" | "output" | "task_exited" | "session_rebound" | string;
112
+ event:
113
+ | "task_started"
114
+ | "output"
115
+ | "task_exited"
116
+ | "session_rebound"
117
+ | "intercom"
118
+ | "intercom_reset"
119
+ | string;
112
120
  task_id?: string;
113
121
  kind?: string;
114
122
  command?: string;
@@ -126,6 +134,8 @@ export interface ManagerEvent {
126
134
  ts?: number;
127
135
  /** Set on task_started when the daemon recorded who asked for the task. */
128
136
  origin?: { via: string; child_id?: string; run_id?: string };
137
+ /** An intercom bridge frame (deliver/receipt/idle_notice) for this session. */
138
+ frame?: Record<string, unknown>;
129
139
  }
130
140
 
131
141
  export class ManagerError extends Error {
@@ -608,6 +618,19 @@ export class ManagerClient {
608
618
  });
609
619
  }
610
620
 
621
+ /** The extension's messaging bridge: one intercom client-protocol op
622
+ * (register/send/list/receipt/…). Protocol 7+; rejected by older managers. */
623
+ async intercom(op: Record<string, unknown>): Promise<Record<string, unknown>> {
624
+ if (this.protocolLevel_ < 7) {
625
+ throw new ManagerError(
626
+ "E_VERSION",
627
+ "this pi-famulus daemon does not serve cross-session messaging (protocol " +
628
+ `${this.protocolLevel_}; need 7). Restart pi so it starts the current daemon.`,
629
+ );
630
+ }
631
+ return this.request({ type: "intercom", op });
632
+ }
633
+
611
634
  /** Daemon status, including optional in-place upgrade metadata. */
612
635
  async status(): Promise<ManagerStatusResponse> {
613
636
  const res = await this.request({ type: "status" });
@@ -29,6 +29,7 @@ const REGISTRATIONS = new Set<PropertyKey>([
29
29
  "registerShortcut",
30
30
  "registerFlag",
31
31
  "registerMessageRenderer",
32
+ "registerEntryRenderer",
32
33
  "registerProvider",
33
34
  ]);
34
35
 
@@ -51,8 +52,13 @@ export function createPiBinding(): PiBinding {
51
52
  let current: ExtensionAPI | null = null;
52
53
  let live = false;
53
54
 
54
- const call = (pi: ExtensionAPI, method: PropertyKey, args: unknown[]): unknown =>
55
- (Reflect.get(pi, method) as (...a: unknown[]) => unknown).apply(pi, args);
55
+ const call = (pi: ExtensionAPI, method: PropertyKey, args: unknown[]): unknown => {
56
+ const fn = Reflect.get(pi, method) as unknown;
57
+ // A host surface may not implement every registration kind (hosted
58
+ // child fakes, older pi); skipping beats crashing the replay.
59
+ if (typeof fn !== "function") return undefined;
60
+ return (fn as (...a: unknown[]) => unknown).apply(pi, args);
61
+ };
56
62
 
57
63
  const api = new Proxy({} as ExtensionAPI, {
58
64
  get(_target, key) {