@gonrocca/zero-pi 0.1.32 → 0.1.34

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.
@@ -125,27 +125,75 @@ function ornament(width: number): string {
125
125
  return center(line, width);
126
126
  }
127
127
 
128
- /** The full banner block, centered to the given width. */
128
+ /**
129
+ * The full banner block, centered to the given width.
130
+ *
131
+ * Wide layout = exactly **10 lines**: top ornament + 7 logo rows (6 + cast
132
+ * shadow) + tag + bottom ornament. Pi's `setWidget` caps managed widgets at
133
+ * `MAX_WIDGET_LINES = 10` (hardcoded in interactive-mode.js); anything taller
134
+ * is rendered with a `... (widget truncated)` line. Inner blank-line padding
135
+ * was removed to fit that cap exactly — adding blanks back would re-truncate.
136
+ */
129
137
  export function bannerBlock(width: number): string[] {
130
138
  if (width < 64) {
131
139
  return [center(fg(VIOLET, "ZERO SDD"), width), center(fg(MUTED, "pi.dev · spec-driven work"), width)];
132
140
  }
133
141
  const tag = fg(VIOLET, "ZERO SDD") + fg(MUTED, " explore → plan → build → veredicto");
134
- return [ornament(width), "", ...renderLogo(width), "", center(tag, width), ornament(width)];
142
+ return [ornament(width), ...renderLogo(width), center(tag, width), ornament(width)];
143
+ }
144
+
145
+ /** The slice of pi's UI surface this extension uses. */
146
+ interface PiUI {
147
+ setWidget(id: string, lines: string[]): void;
148
+ }
149
+ interface PiSessionContext {
150
+ ui?: PiUI;
151
+ }
152
+ interface PiAPI {
153
+ on(event: string, handler: (event: unknown, ctx: PiSessionContext) => void): void;
154
+ }
155
+
156
+ /** Stable widget id — re-installing with the same id replaces the previous lines. */
157
+ const WIDGET_ID = "zero-banner";
158
+
159
+ /**
160
+ * Install (or refresh) the banner as a pi-managed widget above the editor.
161
+ *
162
+ * Using `ctx.ui.setWidget` instead of a one-shot `stdout.write` means pi owns
163
+ * the lines and redraws them on every resize, every reload, every session
164
+ * restart — so the banner survives maximize/minimize instead of vanishing into
165
+ * scrollback. The crash this file's earlier comment warned about was the
166
+ * animated 140 ms re-render loop; a static widget set once per `session_start`
167
+ * has no timer and re-uses the same id, so it never spams.
168
+ */
169
+ function installWidget(ctx: PiSessionContext): void {
170
+ if (!ctx?.ui || typeof ctx.ui.setWidget !== "function") return;
171
+ if (process.env.NO_COLOR) return;
172
+ const cols = process.stdout?.columns && process.stdout.columns > 0 ? process.stdout.columns : 80;
173
+ ctx.ui.setWidget(WIDGET_ID, bannerBlock(cols));
135
174
  }
136
175
 
137
176
  /**
138
- * The pi extension entry point. Renders the banner exactly once, synchronously,
139
- * at load time. Any failure is swallowed — a banner must never break a session.
177
+ * The pi extension entry point.
178
+ *
179
+ * Hooks `session_start` (fires on startup, reload, new, resume, fork) and
180
+ * installs the banner as a pi-managed widget above the editor — so pi keeps it
181
+ * rendered and redraws it on resize. Disabled with `ZERO_HEADER=off`.
182
+ * Defensive: any failure of registration or the widget install is swallowed —
183
+ * a banner must never break a pi session.
140
184
  */
141
- export default function register(_pi?: unknown): void {
185
+ export default function register(pi?: unknown): void {
142
186
  try {
143
187
  if ((process.env.ZERO_HEADER ?? "").trim().toLowerCase() === "off") return;
144
- const stream = process.stdout;
145
- if (!stream || !stream.isTTY || process.env.NO_COLOR) return;
146
- const width = stream.columns && stream.columns > 0 ? stream.columns : 80;
147
- stream.write("\n" + bannerBlock(width).join("\n") + "\n\n");
188
+ if (!pi || typeof (pi as PiAPI).on !== "function") return;
189
+ (pi as PiAPI).on("session_start", (_event, ctx) => {
190
+ try {
191
+ installWidget(ctx);
192
+ } catch {
193
+ // A widget install failure must never break a pi session.
194
+ }
195
+ });
148
196
  } catch {
149
- // A banner failure must never break a pi session.
197
+ // Registration itself must never break a pi session.
150
198
  }
151
199
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gonrocca/zero-pi",
3
- "version": "0.1.32",
3
+ "version": "0.1.34",
4
4
  "description": "zero-pi — an installable layer for pi (pi.dev): the zero spec-driven development workflow, per-phase model autotune, and skill auto-learning. Adds capability to pi without modifying pi.",
5
5
  "type": "module",
6
6
  "keywords": [