@yaag/tui 0.4.0 → 0.5.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yaag/tui",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -19,6 +19,6 @@
19
19
  },
20
20
  "dependencies": {
21
21
  "@earendil-works/pi-tui": "^0.84.0",
22
- "@yaag/runtime": "0.4.0"
22
+ "@yaag/runtime": "0.5.0"
23
23
  }
24
24
  }
@@ -21,12 +21,9 @@ import {
21
21
  import type { NamedKeybindings } from "./key-router.ts";
22
22
  import { renderActionsMenu } from "./node-actions.ts";
23
23
  import { agentOfPath } from "./node-path-parse.ts";
24
+ import { overlayContentRows, overlayPanelRows } from "./overlay-geometry.ts";
24
25
  import { extractSystemPrompt } from "./session-transcript.ts";
25
- import {
26
- renderSystemPromptOverlay,
27
- systemPromptContentRows,
28
- systemPromptLines,
29
- } from "./system-prompt-overlay.ts";
26
+ import { renderSystemPromptOverlay, systemPromptLines } from "./system-prompt-overlay.ts";
30
27
  import { nodeTranscriptLines } from "./transcript-content.ts";
31
28
  import { renderTranscriptOverlay } from "./transcript-overlay.ts";
32
29
  import { NO_TRANSCRIPT_STUB } from "./transcript-render.ts";
@@ -48,6 +45,12 @@ export interface DrillHost {
48
45
  agentInfo(agent: string): AgentInfo | undefined;
49
46
  /** Reads an Agent's session file body, or null when it is missing. */
50
47
  readSession(agent: string): Promise<string | null>;
48
+ /**
49
+ * Reads the system-prompt sidecar the Agent's own pi wrote, or null when
50
+ * there is none — an Agent of a replayed Run, or a session that predates the
51
+ * sidecar.
52
+ */
53
+ readSystemPromptSidecar(agent: string): Promise<string | null>;
51
54
  /** An Agent's session file path, or null when the spawn reported none. */
52
55
  sessionPath(agent: string): string | null;
53
56
  copyPath(text: string): Promise<void>;
@@ -105,7 +108,7 @@ export class DrillController {
105
108
  nodePath: state.path,
106
109
  content: this.#linesFor(state),
107
110
  scroll: state.scroll,
108
- rows: this.#host.rows(),
111
+ rows: this.#panelRows(),
109
112
  width,
110
113
  });
111
114
  case "systemPrompt":
@@ -113,12 +116,20 @@ export class DrillController {
113
116
  nodePath: state.path,
114
117
  content: this.#linesFor(state),
115
118
  scroll: state.scroll,
116
- rows: this.#host.rows(),
119
+ rows: this.#panelRows(),
117
120
  width,
118
121
  });
119
122
  }
120
123
  }
121
124
 
125
+ /**
126
+ * The total panel height both overlays draw at, derived from the host's row
127
+ * budget by the shared geometry so pi's own chrome keeps its rows.
128
+ */
129
+ #panelRows(): number {
130
+ return overlayPanelRows(this.#host.rows());
131
+ }
132
+
122
133
  /** Routes one input byte string; returns whether a layer consumed it. */
123
134
  handleInput(data: string): boolean {
124
135
  const action = this.#route(data);
@@ -226,12 +237,14 @@ export class DrillController {
226
237
  }
227
238
 
228
239
  /**
229
- * The page height the open layer scrolls by. The system-prompt modal spends
230
- * three rows on its frame, so its page is smaller than the row budget.
240
+ * The page height the open layer scrolls by. Both overlays draw the same
241
+ * panel and spend the same frame rows, so both page by its content rows and
242
+ * scrolling matches what is drawn.
231
243
  */
232
244
  #pageRows(state: DrillState): number {
233
- const rows = this.#host.rows();
234
- return state.kind === "systemPrompt" ? systemPromptContentRows(rows) : rows;
245
+ return state.kind === "tree" || state.kind === "menu"
246
+ ? this.#host.rows()
247
+ : overlayContentRows(this.#panelRows());
235
248
  }
236
249
 
237
250
  /** Drops retained content and voids every in-flight read. */
@@ -266,8 +279,8 @@ export class DrillController {
266
279
  * closed or replaced modal.
267
280
  */
268
281
  async #loadSystemPrompt(nodePath: string, generation: number): Promise<void> {
269
- const session = await this.#readSession(agentOfPath(nodePath));
270
- const prompt = session === null ? null : extractSystemPrompt(session);
282
+ const agent = agentOfPath(nodePath);
283
+ const prompt = await this.#readPrompt(agent);
271
284
  if (generation !== this.#generation) return;
272
285
  const state = this.#state;
273
286
  if (state.kind !== "systemPrompt" || state.path !== nodePath) return;
@@ -275,6 +288,29 @@ export class DrillController {
275
288
  this.#host.requestRender();
276
289
  }
277
290
 
291
+ /**
292
+ * The Agent's system prompt, from the sidecar its own pi wrote, or else from
293
+ * the session file, or null when neither records one.
294
+ *
295
+ * The sidecar wins because it holds the prompt pi really assembled; the
296
+ * session file is read only for a session written before the sidecar existed.
297
+ */
298
+ async #readPrompt(agent: string): Promise<string | null> {
299
+ const sidecar = await this.#readSidecar(agent);
300
+ if (sidecar !== null && sidecar !== "") return sidecar;
301
+ const session = await this.#readSession(agent);
302
+ return session === null ? null : extractSystemPrompt(session);
303
+ }
304
+
305
+ /** A sidecar that cannot be read degrades to the session file. */
306
+ async #readSidecar(agent: string): Promise<string | null> {
307
+ try {
308
+ return await this.#host.readSystemPromptSidecar(agent);
309
+ } catch {
310
+ return null;
311
+ }
312
+ }
313
+
278
314
  /** A session file that cannot be read degrades to the labelled stub. */
279
315
  async #readSession(agent: string): Promise<string | null> {
280
316
  try {
package/src/index.ts CHANGED
@@ -40,6 +40,11 @@ export {
40
40
  export { agentOfPath, type NodePathSegment, parseNodePath } from "./node-path-parse.ts";
41
41
  export { renderNodeTable } from "./node-table.ts";
42
42
  export { type FramedBoxOptions, renderFramedBox } from "./overlay-frame.ts";
43
+ export {
44
+ OVERLAY_CHROME_ROWS,
45
+ overlayContentRows,
46
+ overlayPanelRows,
47
+ } from "./overlay-geometry.ts";
43
48
  export {
44
49
  initialScroll,
45
50
  maxOffset,
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Shared height geometry for the drill-in overlays (ADR-0034).
3
+ *
4
+ * The Run view is a non-overlay `ctx.ui.custom()` component: pi draws its own
5
+ * chrome — the editor box and the status line — below the component, and it
6
+ * gives the component no height budget. A panel as tall as the terminal
7
+ * therefore pushes that chrome off the screen. These two pure helpers give
8
+ * every overlay one contract: `overlayPanelRows` turns a terminal row budget
9
+ * into the TOTAL panel height, and `overlayContentRows` turns that total into
10
+ * the content rows the panel can draw.
11
+ */
12
+
13
+ /** Frame rows a panel spends on its title, its footer, and its bottom border. */
14
+ export const OVERLAY_CHROME_ROWS = 3;
15
+
16
+ /** Fraction of the row budget a panel takes on a tall terminal. */
17
+ const HEIGHT_FRACTION = 0.9;
18
+
19
+ /** Rows kept free for pi chrome below the component, on a short terminal. */
20
+ const HOST_CHROME_ROWS = 6;
21
+
22
+ /** The shortest panel that still draws one content row. */
23
+ const MIN_PANEL_ROWS = 4;
24
+
25
+ /**
26
+ * The total panel height for a terminal row budget of `budget`.
27
+ *
28
+ * The fraction keeps a margin on a tall terminal; the absolute reserve keeps
29
+ * one on a short terminal, where a fraction alone leaves too little. A budget
30
+ * smaller than both floors to the shortest drawable panel. Failure mode: a
31
+ * budget that is not a finite number degrades to that same floor.
32
+ */
33
+ export function overlayPanelRows(budget: number): number {
34
+ if (!Number.isFinite(budget)) return MIN_PANEL_ROWS;
35
+ const rows = Math.floor(budget);
36
+ const fitted = Math.min(Math.round(rows * HEIGHT_FRACTION), rows - HOST_CHROME_ROWS);
37
+ return Math.max(MIN_PANEL_ROWS, fitted);
38
+ }
39
+
40
+ /** The content rows a panel of `panelRows` total height draws. */
41
+ export function overlayContentRows(panelRows: number): number {
42
+ const rows = Number.isFinite(panelRows) ? Math.floor(panelRows) : 0;
43
+ return Math.max(1, rows - OVERLAY_CHROME_ROWS);
44
+ }
@@ -12,6 +12,7 @@
12
12
  */
13
13
 
14
14
  import { renderFramedBox } from "./overlay-frame.ts";
15
+ import { overlayContentRows } from "./overlay-geometry.ts";
15
16
  import { resolveOffset, type Scroll } from "./overlay-scroll.ts";
16
17
  import { wrapLines } from "./wrap-text.ts";
17
18
 
@@ -20,9 +21,6 @@ const FOOTER = " ↑↓ scroll c copy path esc back";
20
21
  /** Fraction of the terminal width the centered panel takes. */
21
22
  const WIDTH_FRACTION = 0.9;
22
23
 
23
- /** Frame rows the panel spends on its title, footer, and bottom border. */
24
- const CHROME_ROWS = 3;
25
-
26
24
  /** Frame columns one content row loses to the borders and their padding. */
27
25
  const CHROME_COLUMNS = 4;
28
26
 
@@ -49,11 +47,6 @@ export interface SystemPromptOverlayOptions {
49
47
  readonly width: number;
50
48
  }
51
49
 
52
- /** The content rows a panel of `rows` total height draws. */
53
- export function systemPromptContentRows(rows: number): number {
54
- return Math.max(1, floor(rows) - CHROME_ROWS);
55
- }
56
-
57
50
  /**
58
51
  * Wraps a prompt body to the panel drawn at `width`.
59
52
  *
@@ -70,7 +63,7 @@ export function renderSystemPromptOverlay(options: SystemPromptOverlayOptions):
70
63
  const width = frameWidth(options.width);
71
64
  const panel = panelWidth(width);
72
65
  const margin = " ".repeat(Math.max(0, Math.floor((width - panel) / 2)));
73
- const rows = systemPromptContentRows(options.rows);
66
+ const rows = overlayContentRows(options.rows);
74
67
  const offset = resolveOffset(options.scroll, options.content.length, rows);
75
68
  const visible = options.content.slice(offset, offset + rows);
76
69
  return renderFramedBox({
@@ -104,7 +97,3 @@ function blankRows(count: number): readonly string[] {
104
97
  function padding(columns: number): string {
105
98
  return " ".repeat(Math.max(0, columns));
106
99
  }
107
-
108
- function floor(value: number): number {
109
- return Math.floor(Number.isFinite(value) ? value : 0);
110
- }
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import { renderFramedBox } from "./overlay-frame.ts";
9
+ import { overlayContentRows } from "./overlay-geometry.ts";
9
10
  import { resolveOffset, type Scroll } from "./overlay-scroll.ts";
10
11
 
11
12
  const FOOTER = " ↑↓ scroll c copy path esc back";
@@ -14,7 +15,8 @@ const FOOTER = " ↑↓ scroll c copy path esc back";
14
15
  * One transcript-overlay render request.
15
16
  *
16
17
  * Contract: `content` and `scroll` decide the visible slice; `rows` is the
17
- * content row budget; `width` is the exact visible width of every returned
18
+ * TOTAL panel height, frame chrome included, and the overlay derives its
19
+ * content rows from it; `width` is the exact visible width of every returned
18
20
  * line. Failure mode: empty `content`, a `rows` of zero or less, and a scroll
19
21
  * offset past the end all draw a valid frame instead of throwing.
20
22
  */
@@ -24,14 +26,14 @@ export interface TranscriptOverlayOptions {
24
26
  /** Already-rendered content lines, from `nodeTranscriptLines`. */
25
27
  readonly content: readonly string[];
26
28
  readonly scroll: Scroll;
27
- /** Content rows the overlay may draw; a zero or negative value draws one. */
29
+ /** Panel height, chrome included; a value under four draws one content row. */
28
30
  readonly rows: number;
29
31
  readonly width: number;
30
32
  }
31
33
 
32
34
  /** Draws the framed, scrolled overlay for one node's transcript. */
33
35
  export function renderTranscriptOverlay(options: TranscriptOverlayOptions): readonly string[] {
34
- const rows = Math.max(1, Math.floor(options.rows));
36
+ const rows = overlayContentRows(options.rows);
35
37
  const offset = resolveOffset(options.scroll, options.content.length, rows);
36
38
  return renderFramedBox({
37
39
  title: `${options.nodePath} ─ transcript`,