@aragon-agent/cli 0.6.3 → 0.6.7

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 (138) hide show
  1. package/CHANGELOG.md +142 -0
  2. package/README.md +203 -13
  3. package/dist/agent/controller.d.ts +158 -2
  4. package/dist/agent/controller.js +400 -1
  5. package/dist/agent/reducer.d.ts +102 -19
  6. package/dist/agent/reducer.js +162 -71
  7. package/dist/agent/system-prompt.d.ts +14 -0
  8. package/dist/agent/system-prompt.js +2 -0
  9. package/dist/agent/usage.d.ts +34 -0
  10. package/dist/agent/usage.js +43 -0
  11. package/dist/cli.js +132 -23
  12. package/dist/commands/builtins.js +101 -1
  13. package/dist/compaction/child.js +5 -0
  14. package/dist/compaction/command.js +14 -1
  15. package/dist/compaction/compactor.d.ts +94 -24
  16. package/dist/compaction/compactor.js +247 -98
  17. package/dist/compaction/context-command.d.ts +33 -0
  18. package/dist/compaction/context-command.js +166 -0
  19. package/dist/compaction/meter.d.ts +251 -0
  20. package/dist/compaction/meter.js +428 -0
  21. package/dist/compaction/pressure.d.ts +6 -0
  22. package/dist/compaction/pressure.js +4 -0
  23. package/dist/compaction/types.d.ts +77 -0
  24. package/dist/compaction/wiring.d.ts +56 -0
  25. package/dist/compaction/wiring.js +107 -25
  26. package/dist/config/cli-commands.js +5 -0
  27. package/dist/config/env.d.ts +1 -1
  28. package/dist/config/env.js +63 -2
  29. package/dist/config/load.d.ts +28 -0
  30. package/dist/config/load.js +83 -1
  31. package/dist/config/schema.d.ts +129 -0
  32. package/dist/config/schema.js +69 -0
  33. package/dist/config/store.js +47 -1
  34. package/dist/exec/events.d.ts +138 -1
  35. package/dist/exec/events.js +42 -0
  36. package/dist/exec/index.js +43 -6
  37. package/dist/exec/progress-reporter.d.ts +23 -0
  38. package/dist/exec/progress-reporter.js +88 -0
  39. package/dist/exec/runner.d.ts +34 -0
  40. package/dist/exec/runner.js +137 -25
  41. package/dist/fast/reviewer.d.ts +19 -1
  42. package/dist/fast/reviewer.js +22 -0
  43. package/dist/fast/types.d.ts +15 -0
  44. package/dist/fast/wiring.d.ts +10 -1
  45. package/dist/fast/wiring.js +18 -1
  46. package/dist/input/limits.d.ts +72 -0
  47. package/dist/input/limits.js +63 -0
  48. package/dist/input/paste-parse.d.ts +71 -0
  49. package/dist/input/paste-parse.js +95 -0
  50. package/dist/input/stdin-filter.d.ts +81 -0
  51. package/dist/input/stdin-filter.js +379 -0
  52. package/dist/logging/install.d.ts +2 -0
  53. package/dist/logging/install.js +53 -1
  54. package/dist/proc/classify.d.ts +52 -0
  55. package/dist/proc/classify.js +143 -0
  56. package/dist/proc/kill-tree.d.ts +42 -0
  57. package/dist/proc/kill-tree.js +119 -0
  58. package/dist/proc/limits.d.ts +84 -0
  59. package/dist/proc/limits.js +84 -0
  60. package/dist/proc/log-ring.d.ts +68 -0
  61. package/dist/proc/log-ring.js +102 -0
  62. package/dist/proc/prompt.d.ts +24 -0
  63. package/dist/proc/prompt.js +49 -0
  64. package/dist/proc/readiness.d.ts +102 -0
  65. package/dist/proc/readiness.js +219 -0
  66. package/dist/proc/supervisor.d.ts +103 -0
  67. package/dist/proc/supervisor.js +465 -0
  68. package/dist/proc/types.d.ts +175 -0
  69. package/dist/proc/types.js +15 -0
  70. package/dist/session/persist.js +36 -1
  71. package/dist/todo/limits.d.ts +4 -17
  72. package/dist/todo/limits.js +4 -17
  73. package/dist/todo/panel-rows.d.ts +8 -6
  74. package/dist/todo/panel-rows.js +19 -6
  75. package/dist/tools/bash-tool.d.ts +51 -2
  76. package/dist/tools/bash-tool.js +321 -51
  77. package/dist/tools/fs-tools.d.ts +17 -0
  78. package/dist/tools/index.d.ts +55 -2
  79. package/dist/tools/index.js +39 -1
  80. package/dist/tools/proc-tools.d.ts +25 -0
  81. package/dist/tools/proc-tools.js +102 -0
  82. package/dist/ui/App.d.ts +15 -2
  83. package/dist/ui/App.js +330 -69
  84. package/dist/ui/AutocompletePopup.d.ts +3 -1
  85. package/dist/ui/AutocompletePopup.js +9 -11
  86. package/dist/ui/BottomStatusRow.d.ts +1 -1
  87. package/dist/ui/Composer.d.ts +56 -1
  88. package/dist/ui/Composer.js +50 -6
  89. package/dist/ui/PromptInput.d.ts +52 -3
  90. package/dist/ui/PromptInput.js +201 -55
  91. package/dist/ui/StatusBar.d.ts +40 -15
  92. package/dist/ui/StatusBar.js +51 -9
  93. package/dist/ui/TeamPanel.d.ts +3 -1
  94. package/dist/ui/TeamPanel.js +13 -9
  95. package/dist/ui/TodoPanel.d.ts +5 -5
  96. package/dist/ui/TodoPanel.js +56 -24
  97. package/dist/ui/Transcript.js +29 -1
  98. package/dist/ui/UpdateLine.d.ts +1 -1
  99. package/dist/ui/capabilities.js +5 -5
  100. package/dist/ui/composer-limits.d.ts +31 -0
  101. package/dist/ui/composer-limits.js +50 -0
  102. package/dist/ui/composer-rows.d.ts +73 -0
  103. package/dist/ui/composer-rows.js +201 -0
  104. package/dist/ui/editor-reducer.d.ts +37 -0
  105. package/dist/ui/editor-reducer.js +88 -12
  106. package/dist/ui/entries/ServiceCard.d.ts +66 -0
  107. package/dist/ui/entries/ServiceCard.js +116 -0
  108. package/dist/ui/entries/UserEntry.d.ts +17 -3
  109. package/dist/ui/entries/UserEntry.js +34 -5
  110. package/dist/ui/exit-snapshot.d.ts +9 -0
  111. package/dist/ui/layout/ScrollViewport.d.ts +1 -1
  112. package/dist/ui/layout/autocomplete.d.ts +18 -0
  113. package/dist/ui/layout/autocomplete.js +26 -0
  114. package/dist/ui/layout/budget.d.ts +34 -6
  115. package/dist/ui/layout/budget.js +32 -7
  116. package/dist/ui/layout/rail.d.ts +4 -53
  117. package/dist/ui/layout/rail.js +10 -62
  118. package/dist/ui/layout/team-panel.d.ts +18 -0
  119. package/dist/ui/layout/team-panel.js +22 -0
  120. package/dist/ui/layout/todo-layout.d.ts +24 -0
  121. package/dist/ui/layout/todo-layout.js +27 -0
  122. package/dist/ui/layout/virtual-window.js +51 -1
  123. package/dist/ui/overlays/HelpOverlay.js +3 -0
  124. package/dist/ui/overlays/PlanReviewOverlay.js +10 -2
  125. package/dist/ui/overlays/QuestionOverlay.js +5 -1
  126. package/dist/ui/overlays/SettingsScreen.js +14 -1
  127. package/dist/ui/paste-frames.d.ts +87 -0
  128. package/dist/ui/paste-frames.js +112 -0
  129. package/dist/ui/paste-tokens.d.ts +90 -0
  130. package/dist/ui/paste-tokens.js +147 -0
  131. package/dist/ui/screen.d.ts +16 -0
  132. package/dist/ui/screen.js +45 -2
  133. package/dist/ui/selection/selection-controller.d.ts +1 -1
  134. package/dist/ui/transcript-text.js +55 -2
  135. package/dist/ui/use-wheel-routing.d.ts +1 -1
  136. package/package.json +2 -2
  137. package/dist/input/stdin-mouse-filter.d.ts +0 -47
  138. package/dist/input/stdin-mouse-filter.js +0 -153
@@ -3,15 +3,29 @@
3
3
  *
4
4
  * The role glyph, the gutter rail and the inter-entry spacing all belong to
5
5
  * `EntryFrame` now (§4.3), so this component renders content and nothing else.
6
- * That is also why it takes no `caps`: it has no glyph of its own left to
7
- * degrade.
6
+ *
7
+ * IT DOES TAKE `caps` AS OF tui-paste-handling (G7): the row cap below needs one
8
+ * glyph, and a hardcoded ellipsis is the exact failure `glyphs.ts` exists to
9
+ * prevent. That is the only reason the earlier "it has no glyph of its own left
10
+ * to degrade" note no longer holds.
8
11
  */
9
12
  import React from 'react';
10
13
  import type { Theme } from '../theme.js';
11
- declare function UserEntryImpl({ text, theme }: {
14
+ import type { TermCapabilities } from '../capabilities.js';
15
+ declare function UserEntryImpl({ text, theme, caps, }: {
12
16
  text: string;
13
17
  theme: Theme;
18
+ caps: TermCapabilities;
14
19
  }): React.ReactElement;
20
+ /**
21
+ * Rows this component renders for a given text.
22
+ *
23
+ * EXPORTED SO `virtual-window.ts` AND THIS FILE CANNOT DISAGREE (I-13). The
24
+ * render cap and the height estimate are ONE number; an over-estimated entry is
25
+ * never mounted, so it is never measured, so the estimate never self-corrects --
26
+ * which makes that particular disagreement permanent and silent.
27
+ */
28
+ export declare function userEntryRenderedRows(lineCount: number): number;
15
29
  /**
16
30
  * `React.memo` with the DEFAULT comparator (tui-render-performance L2 / R3).
17
31
  * `mapEntry` already preserves object identity for untouched entries, so the
@@ -1,16 +1,45 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /**
3
3
  * Render a user message entry.
4
4
  *
5
5
  * The role glyph, the gutter rail and the inter-entry spacing all belong to
6
6
  * `EntryFrame` now (§4.3), so this component renders content and nothing else.
7
- * That is also why it takes no `caps`: it has no glyph of its own left to
8
- * degrade.
7
+ *
8
+ * IT DOES TAKE `caps` AS OF tui-paste-handling (G7): the row cap below needs one
9
+ * glyph, and a hardcoded ellipsis is the exact failure `glyphs.ts` exists to
10
+ * prevent. That is the only reason the earlier "it has no glyph of its own left
11
+ * to degrade" note no longer holds.
9
12
  */
10
13
  import React from 'react';
11
14
  import { Box, Text } from 'ink';
12
- function UserEntryImpl({ text, theme }) {
13
- return (_jsx(Box, { flexDirection: "column", children: text.split('\n').map((line, i) => (_jsx(Text, { color: theme.user, bold: i === 0, children: line }, i))) }));
15
+ import { pickGlyphs } from '../glyphs.js';
16
+ import { USER_ENTRY_MAX_ROWS } from '../composer-limits.js';
17
+ function UserEntryImpl({ text, theme, caps, }) {
18
+ const glyphs = pickGlyphs(caps);
19
+ const all = text.split('\n');
20
+ // A submitted 5 000-line paste otherwise asks Yoga to lay out 5 000 nodes
21
+ // inside a measured entry -- the hazard `render-cache.ts:37` already records
22
+ // in the same words ("a multi-megabyte 'code block' is almost always a pasted
23
+ // log"). The full text stays in `ViewState` and in the exit transcript, which
24
+ // is deliberately NOT capped: a replay a user pipes into an issue must be
25
+ // complete.
26
+ const hidden = Math.max(0, all.length - USER_ENTRY_MAX_ROWS);
27
+ const shown = hidden > 0 ? all.slice(0, USER_ENTRY_MAX_ROWS) : all;
28
+ return (_jsxs(Box, { flexDirection: "column", children: [shown.map((line, i) => (_jsx(Text, { color: theme.user, bold: i === 0, children: line }, i))), hidden > 0 && (
29
+ // The tail NAMES the number of hidden rows rather than hiding them
30
+ // silently, which is what makes the cap honest (D-9 / R-10).
31
+ _jsx(Text, { color: theme.muted, children: `${glyphs.ellipsis} +${hidden} more lines` }))] }));
32
+ }
33
+ /**
34
+ * Rows this component renders for a given text.
35
+ *
36
+ * EXPORTED SO `virtual-window.ts` AND THIS FILE CANNOT DISAGREE (I-13). The
37
+ * render cap and the height estimate are ONE number; an over-estimated entry is
38
+ * never mounted, so it is never measured, so the estimate never self-corrects --
39
+ * which makes that particular disagreement permanent and silent.
40
+ */
41
+ export function userEntryRenderedRows(lineCount) {
42
+ return lineCount > USER_ENTRY_MAX_ROWS ? USER_ENTRY_MAX_ROWS + 1 : lineCount;
14
43
  }
15
44
  /**
16
45
  * `React.memo` with the DEFAULT comparator (tui-render-performance L2 / R3).
@@ -14,6 +14,15 @@
14
14
  import type { Entry, UsageTotal } from '../agent/reducer.js';
15
15
  export interface ExitSnapshot {
16
16
  entries: Entry[];
17
+ /**
18
+ * The session total, FORWARDED not copied.
19
+ *
20
+ * It gained `cacheReadTokens` / `cacheWriteTokens`
21
+ * (context-usage-gauge-accuracy §3.6) and this file needed no change for it,
22
+ * which is the point of referencing the reducer's type rather than restating
23
+ * its fields: `renderTranscriptText` reads the total through `promptTokensOf`,
24
+ * so the exit summary and the status bar cannot drift apart.
25
+ */
17
26
  usageTotal: UsageTotal;
18
27
  provider: string;
19
28
  model: string;
@@ -76,7 +76,7 @@ export interface ScrollViewportProps {
76
76
  * This component is the ONLY one inside the viewport subtree that reads
77
77
  * `stdout.columns` directly, and the place it uses the number — the
78
78
  * `MIN_INDICATOR_COLS` gate — is about how much room THIS BOX has, not how
79
- * wide the terminal is. Once the todo rail takes a fifth of the frame those two
79
+ * wide the terminal is. Once the todo rail takes its responsive share of the frame those two
80
80
  * stop being the same number. Optional so every existing caller (and every
81
81
  * test) is unchanged.
82
82
  */
@@ -0,0 +1,18 @@
1
+ export interface AutocompleteLayoutInput {
2
+ itemCount: number;
3
+ selected: number;
4
+ maxRows?: number;
5
+ maxHeight?: number;
6
+ }
7
+ export interface AutocompleteLayout {
8
+ start: number;
9
+ count: number;
10
+ moreBelow: number;
11
+ showMore: boolean;
12
+ rowCount: number;
13
+ }
14
+ /** Window suggestions within a total row budget including borders and footer.
15
+ * Only omitted maxHeight is unlimited. Explicit invalid numbers become zero.
16
+ * Selection is clamped to the available items; no input is mutated or rejected.
17
+ */
18
+ export declare function buildAutocompleteLayout(input: AutocompleteLayoutInput): AutocompleteLayout;
@@ -0,0 +1,26 @@
1
+ /** Window suggestions within a total row budget including borders and footer.
2
+ * Only omitted maxHeight is unlimited. Explicit invalid numbers become zero.
3
+ * Selection is clamped to the available items; no input is mutated or rejected.
4
+ */
5
+ export function buildAutocompleteLayout(input) {
6
+ const total = nonNegativeInt(input.itemCount);
7
+ const maxRows = input.maxRows === undefined ? 6 : nonNegativeInt(input.maxRows);
8
+ const height = input.maxHeight === undefined ? Infinity : nonNegativeInt(input.maxHeight);
9
+ if (total === 0 || maxRows === 0 || height < 3) {
10
+ return { start: 0, count: 0, moreBelow: 0, showMore: false, rowCount: 0 };
11
+ }
12
+ const selected = Math.min(total - 1, nonNegativeInt(input.selected));
13
+ let count = Math.min(total, maxRows, height - 2);
14
+ const windowStart = () => Math.max(0, Math.min(selected - count + 1, total - count));
15
+ let start = windowStart();
16
+ if (count >= 2 && start + count < total && count + 3 > height) {
17
+ count--;
18
+ start = windowStart();
19
+ }
20
+ const moreBelow = total - start - count;
21
+ const showMore = moreBelow > 0 && count + 3 <= height;
22
+ return { start, count, moreBelow, showMore, rowCount: 2 + count + Number(showMore) };
23
+ }
24
+ function nonNegativeInt(value) {
25
+ return Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0;
26
+ }
@@ -30,11 +30,26 @@ export interface ChromeBudget {
30
30
  /** Brand bar. Constant 1 in full-screen — this is what makes A-3 hold. */
31
31
  header: number;
32
32
  toast: number;
33
- /** Round border (2) + input row (1), plus the hint row when it fits. */
33
+ /**
34
+ * Round border (2) + the DRAFT's own rows, plus the hint row when it fits.
35
+ *
36
+ * The draft term used to be the constant 1 that the `3` / `4` below encode.
37
+ * That constant is what made a pasted 200-line draft draw the transcript 197
38
+ * rows shorter than every consumer of `viewportRows` believed -- the trap
39
+ * `BottomStatusRow.tsx:5-27` already documents at 1/200th the magnitude.
40
+ */
34
41
  composer: number;
35
42
  status: number;
36
43
  }
37
- export declare function chromeBudget(rows: number): ChromeBudget;
44
+ /**
45
+ * The composer's draft rows, clamped to what this terminal height allows.
46
+ *
47
+ * Exported so the reporter and the budget clamp with ONE function: I-8 requires
48
+ * the rendered row count and the number handed to `viewportRows` to be the same
49
+ * number, and two clamps are how they stop being.
50
+ */
51
+ export declare function clampDraftRows(rows: number, draftRows: number): number;
52
+ export declare function chromeBudget(rows: number, draftRows?: number): ChromeBudget;
38
53
  /**
39
54
  * Rows available to the viewport at a given terminal height.
40
55
  *
@@ -42,8 +57,21 @@ export declare function chromeBudget(rows: number): ChromeBudget;
42
57
  * that argument is the guarantee that the first submitted message cannot make
43
58
  * the layout jump (A-4). Do not add it back.
44
59
  *
45
- * Non-decreasing in `rows`: the one step where chrome grows (the hint row at
46
- * `HINT_MIN_ROWS`) coincides with `rows` growing by one, so the net change is 0
47
- * rather than -1.
60
+ * `draftRows` IS NOT THAT ARGUMENT, and the distinction is the whole of D-12.
61
+ * A-4 forbids making the budget depend on TRANSCRIPT content, so that the first
62
+ * submitted message cannot move the layout. `draftRows` depends only on what the
63
+ * user is typing right now, and its whole purpose is that the jump is ALREADY
64
+ * HAPPENING -- Yoga takes the rows out of the transcript whether or not this
65
+ * function was told. `App` adds the hysteresis (grow now, shrink one tick later)
66
+ * that keeps it from becoming per-keystroke jitter.
67
+ *
68
+ * It DEFAULTS TO 1, i.e. to the constant this function used to assume, so every
69
+ * pre-existing caller and every pre-existing assertion is byte-identical -- and
70
+ * `viewportRows.length` stays 1, because a parameter with a default does not
71
+ * count toward it.
72
+ *
73
+ * Non-decreasing in `rows` at a fixed `draftRows = 1`: the one step where chrome
74
+ * grows (the hint row at `HINT_MIN_ROWS`) coincides with `rows` growing by one,
75
+ * so the net change is 0 rather than -1.
48
76
  */
49
- export declare function viewportRows(rows: number): number;
77
+ export declare function viewportRows(rows: number, draftRows?: number): number;
@@ -24,14 +24,26 @@
24
24
  * make this comment a true-sounding statement about a row that is gone (AC-12,
25
25
  * and the `wheel-scrolls-transcript-only` RV-3 lesson).
26
26
  */
27
+ import { draftMaxRows } from '../composer-limits.js';
27
28
  import { frameHeight, MIN_FULLSCREEN_ROWS } from './frame.js';
28
29
  /** Below this many rows the composer's hint line is dropped to buy back a row. */
29
30
  export const HINT_MIN_ROWS = 20;
30
- export function chromeBudget(rows) {
31
+ /**
32
+ * The composer's draft rows, clamped to what this terminal height allows.
33
+ *
34
+ * Exported so the reporter and the budget clamp with ONE function: I-8 requires
35
+ * the rendered row count and the number handed to `viewportRows` to be the same
36
+ * number, and two clamps are how they stop being.
37
+ */
38
+ export function clampDraftRows(rows, draftRows) {
39
+ const wanted = Number.isFinite(draftRows) ? Math.floor(draftRows) : 1;
40
+ return Math.max(1, Math.min(wanted, draftMaxRows(rows)));
41
+ }
42
+ export function chromeBudget(rows, draftRows = 1) {
31
43
  return {
32
44
  header: 1,
33
45
  toast: 1,
34
- composer: rows >= HINT_MIN_ROWS ? 4 : 3,
46
+ composer: 2 + clampDraftRows(rows, draftRows) + (rows >= HINT_MIN_ROWS ? 1 : 0),
35
47
  status: 1,
36
48
  };
37
49
  }
@@ -42,13 +54,26 @@ export function chromeBudget(rows) {
42
54
  * that argument is the guarantee that the first submitted message cannot make
43
55
  * the layout jump (A-4). Do not add it back.
44
56
  *
45
- * Non-decreasing in `rows`: the one step where chrome grows (the hint row at
46
- * `HINT_MIN_ROWS`) coincides with `rows` growing by one, so the net change is 0
47
- * rather than -1.
57
+ * `draftRows` IS NOT THAT ARGUMENT, and the distinction is the whole of D-12.
58
+ * A-4 forbids making the budget depend on TRANSCRIPT content, so that the first
59
+ * submitted message cannot move the layout. `draftRows` depends only on what the
60
+ * user is typing right now, and its whole purpose is that the jump is ALREADY
61
+ * HAPPENING -- Yoga takes the rows out of the transcript whether or not this
62
+ * function was told. `App` adds the hysteresis (grow now, shrink one tick later)
63
+ * that keeps it from becoming per-keystroke jitter.
64
+ *
65
+ * It DEFAULTS TO 1, i.e. to the constant this function used to assume, so every
66
+ * pre-existing caller and every pre-existing assertion is byte-identical -- and
67
+ * `viewportRows.length` stays 1, because a parameter with a default does not
68
+ * count toward it.
69
+ *
70
+ * Non-decreasing in `rows` at a fixed `draftRows = 1`: the one step where chrome
71
+ * grows (the hint row at `HINT_MIN_ROWS`) coincides with `rows` growing by one,
72
+ * so the net change is 0 rather than -1.
48
73
  */
49
- export function viewportRows(rows) {
74
+ export function viewportRows(rows, draftRows = 1) {
50
75
  if (!Number.isFinite(rows) || rows < MIN_FULLSCREEN_ROWS)
51
76
  return 0;
52
- const c = chromeBudget(rows);
77
+ const c = chromeBudget(rows, draftRows);
53
78
  return Math.max(0, frameHeight(rows) - c.header - c.toast - c.composer - c.status);
54
79
  }
@@ -1,56 +1,7 @@
1
- /**
2
- * Rail geometry (todo-plan-execution §3.9) — the ONE place that says how wide
3
- * and how tall the todo rail is allowed to be.
4
- *
5
- * Pure and React-free, mirroring `budget.ts`, so `todo-rail.test.ts` can pin the
6
- * arithmetic without mounting anything. Deliberately NOT part of `budget.ts`:
7
- * that module answers "how many ROWS does the viewport get" and stays a function
8
- * of `rows` alone; the rail spends COLUMNS.
9
- *
10
- * NAMED FOR ITS POSITION, NOT ITS CONTENT, like `AppShellProps.rail` (D-18):
11
- * this is a layout primitive and `viewportRows` is its neighbour, not
12
- * `TodoPanel`.
13
- */
14
- /** Below this the marker + a two-word title do not fit and the column is noise. */
15
- export declare const TODO_RAIL_MIN_COLS = 18;
16
- /**
17
- * Above this, 20% is being spent on whitespace: at 36 columns a step title
18
- * already fits on one line, and the transcript is the surface that benefits from
19
- * a 200-column terminal. A deliberate, revertible deviation from a literal
20
- * reading of R-e; it only binds above 180 columns.
21
- */
1
+ export declare const TODO_RAIL_MIN_COLS = 14;
22
2
  export declare const TODO_RAIL_MAX_COLS = 36;
23
- /** Under 80 columns the transcript needs every column; no rail at all. */
24
- export declare const TODO_RAIL_MIN_TOTAL_COLS = 80;
25
- /** Below this rail width the 2-column index prefix is dropped for content. */
3
+ export declare const TODO_TRANSCRIPT_MIN_COLS = 62;
4
+ export declare const TODO_RAIL_MIN_TOTAL_COLS: number;
26
5
  export declare const TODO_RAIL_INDEX_MIN_COLS = 22;
27
- /**
28
- * Two properties are pinned by tests, for the reason `budget.ts` records about
29
- * the viewport being non-monotonic before anyone measured it:
30
- *
31
- * - NON-DECREASING in `cols` across [40, 300]. A user dragging a window wider
32
- * must never see the rail get narrower.
33
- * - `cols - todoRailWidth(cols) >= 62` for every `cols >= 80`. Every existing
34
- * width heuristic inside the viewport (`TERSE_HINT_COLS` 42,
35
- * `MIN_INDICATOR_COLS` 50, the status bar's 60/72 steps) sits below that
36
- * floor, so no existing behaviour changes because a rail appeared (R-3).
37
- */
6
+ /** Return a non-decreasing rail width; invalid or narrow terminals have no rail. */
38
7
  export declare function todoRailWidth(cols: number): number;
39
- /**
40
- * How many rows the rail may actually draw.
41
- *
42
- * IT IS NOT `viewportBudget`, AND THE DIFFERENCE IS NOT COSMETIC (P1-3 / D-23).
43
- * `viewportRows()` subtracts the STATIC chrome only, and says so; the live team
44
- * roster is mounted inside `AppShell`'s measured bottom box, where it costs a
45
- * header row, up to `TEAM_LIMITS.panelMaxRows` child rows, a `+N more` row and a
46
- * mail row that `viewportBudget` knows nothing about. `ScrollViewport` is immune
47
- * because it MEASURES itself; the rail does not measure - a second
48
- * measure -> setState -> measure loop is exactly what §1.2 refuses - so it has
49
- * to subtract.
50
- *
51
- * Ink clips overflow from the BOTTOM, so an over-request does not error: it
52
- * silently eats the last row, which is the `+N below` marker - the one row whose
53
- * absence is indistinguishable from "the list is short". That is why this is
54
- * arithmetic rather than a shrug at `overflow: hidden`.
55
- */
56
- export declare function todoRailRows(viewportBudget: number, teamActive: boolean): number;
@@ -1,68 +1,16 @@
1
- /**
2
- * Rail geometry (todo-plan-execution §3.9) — the ONE place that says how wide
3
- * and how tall the todo rail is allowed to be.
4
- *
5
- * Pure and React-free, mirroring `budget.ts`, so `todo-rail.test.ts` can pin the
6
- * arithmetic without mounting anything. Deliberately NOT part of `budget.ts`:
7
- * that module answers "how many ROWS does the viewport get" and stays a function
8
- * of `rows` alone; the rail spends COLUMNS.
9
- *
10
- * NAMED FOR ITS POSITION, NOT ITS CONTENT, like `AppShellProps.rail` (D-18):
11
- * this is a layout primitive and `viewportRows` is its neighbour, not
12
- * `TodoPanel`.
13
- */
14
- import { TODO_LIMITS } from '../../todo/limits.js';
15
- /** R-e, literally: the rail asks for a fifth of the terminal. */
16
- const RAIL_FRACTION = 0.2;
17
- /** Below this the marker + a two-word title do not fit and the column is noise. */
18
- export const TODO_RAIL_MIN_COLS = 18;
19
- /**
20
- * Above this, 20% is being spent on whitespace: at 36 columns a step title
21
- * already fits on one line, and the transcript is the surface that benefits from
22
- * a 200-column terminal. A deliberate, revertible deviation from a literal
23
- * reading of R-e; it only binds above 180 columns.
24
- */
1
+ /** Responsive TODO rail widths, measured in terminal columns including chrome. */
2
+ const RAIL_FRACTION = 0.15;
3
+ export const TODO_RAIL_MIN_COLS = 14;
25
4
  export const TODO_RAIL_MAX_COLS = 36;
26
- /** Under 80 columns the transcript needs every column; no rail at all. */
27
- export const TODO_RAIL_MIN_TOTAL_COLS = 80;
28
- /** Below this rail width the 2-column index prefix is dropped for content. */
5
+ export const TODO_TRANSCRIPT_MIN_COLS = 62;
6
+ export const TODO_RAIL_MIN_TOTAL_COLS = TODO_TRANSCRIPT_MIN_COLS + TODO_RAIL_MIN_COLS;
29
7
  export const TODO_RAIL_INDEX_MIN_COLS = 22;
30
- /**
31
- * Two properties are pinned by tests, for the reason `budget.ts` records about
32
- * the viewport being non-monotonic before anyone measured it:
33
- *
34
- * - NON-DECREASING in `cols` across [40, 300]. A user dragging a window wider
35
- * must never see the rail get narrower.
36
- * - `cols - todoRailWidth(cols) >= 62` for every `cols >= 80`. Every existing
37
- * width heuristic inside the viewport (`TERSE_HINT_COLS` 42,
38
- * `MIN_INDICATOR_COLS` 50, the status bar's 60/72 steps) sits below that
39
- * floor, so no existing behaviour changes because a rail appeared (R-3).
40
- */
8
+ /** Return a non-decreasing rail width; invalid or narrow terminals have no rail. */
41
9
  export function todoRailWidth(cols) {
42
- if (!Number.isFinite(cols) || cols < TODO_RAIL_MIN_TOTAL_COLS)
10
+ if (!Number.isFinite(cols))
43
11
  return 0;
44
- return Math.min(TODO_RAIL_MAX_COLS, Math.max(TODO_RAIL_MIN_COLS, Math.round(cols * RAIL_FRACTION)));
45
- }
46
- /**
47
- * How many rows the rail may actually draw.
48
- *
49
- * IT IS NOT `viewportBudget`, AND THE DIFFERENCE IS NOT COSMETIC (P1-3 / D-23).
50
- * `viewportRows()` subtracts the STATIC chrome only, and says so; the live team
51
- * roster is mounted inside `AppShell`'s measured bottom box, where it costs a
52
- * header row, up to `TEAM_LIMITS.panelMaxRows` child rows, a `+N more` row and a
53
- * mail row that `viewportBudget` knows nothing about. `ScrollViewport` is immune
54
- * because it MEASURES itself; the rail does not measure - a second
55
- * measure -> setState -> measure loop is exactly what §1.2 refuses - so it has
56
- * to subtract.
57
- *
58
- * Ink clips overflow from the BOTTOM, so an over-request does not error: it
59
- * silently eats the last row, which is the `+N below` marker - the one row whose
60
- * absence is indistinguishable from "the list is short". That is why this is
61
- * arithmetic rather than a shrug at `overflow: hidden`.
62
- */
63
- export function todoRailRows(viewportBudget, teamActive) {
64
- if (!Number.isFinite(viewportBudget))
12
+ const columns = Math.floor(cols);
13
+ if (columns < TODO_RAIL_MIN_TOTAL_COLS)
65
14
  return 0;
66
- const reserved = teamActive ? TODO_LIMITS.railReservedRows : 0;
67
- return Math.max(0, viewportBudget - reserved);
15
+ return Math.min(TODO_RAIL_MAX_COLS, Math.max(TODO_RAIL_MIN_COLS, Math.round(columns * RAIL_FRACTION)));
68
16
  }
@@ -0,0 +1,18 @@
1
+ import type { SubagentRun, TeamSnapshot } from '../../team/types.js';
2
+ export interface TeamPanelLayout {
3
+ collapsed: boolean;
4
+ rowCount: number;
5
+ visible: SubagentRun[];
6
+ hiddenTotal: number;
7
+ hiddenRunning: number;
8
+ }
9
+ export interface TeamPanelLayoutInput {
10
+ snapshot: TeamSnapshot | null;
11
+ terminalRows: number;
12
+ availableRows: number;
13
+ }
14
+ /** Project actual roster rows, preserving three middle rows when expanded.
15
+ * Invalid geometry becomes zero; legacy callers may pass positive Infinity
16
+ * for an unlimited available-row budget. Does not throw or mutate the snapshot.
17
+ */
18
+ export declare function buildTeamPanelLayout(input: TeamPanelLayoutInput): TeamPanelLayout;
@@ -0,0 +1,22 @@
1
+ import { TEAM_LIMITS } from '../../team/limits.js';
2
+ import { selectPanelRows } from '../../team/panel-rows.js';
3
+ import { TODO_LIMITS } from '../../todo/limits.js';
4
+ /** Project actual roster rows, preserving three middle rows when expanded.
5
+ * Invalid geometry becomes zero; legacy callers may pass positive Infinity
6
+ * for an unlimited available-row budget. Does not throw or mutate the snapshot.
7
+ */
8
+ export function buildTeamPanelLayout(input) {
9
+ const available = input.availableRows === Infinity ? Infinity : nonNegativeInt(input.availableRows);
10
+ if (!input.snapshot || available < 1) {
11
+ return { collapsed: true, rowCount: 0, visible: [], hiddenTotal: 0, hiddenRunning: 0 };
12
+ }
13
+ const selection = selectPanelRows(input.snapshot.runs, TEAM_LIMITS.panelMaxRows);
14
+ const natural = 1 + selection.visible.length + Number(selection.hiddenTotal > 0)
15
+ + Number(Boolean(input.snapshot.lastMessage));
16
+ const collapsed = nonNegativeInt(input.terminalRows) < TEAM_LIMITS.panelCollapseRows
17
+ || available - natural < TODO_LIMITS.panelMinRows;
18
+ return { ...selection, collapsed, rowCount: collapsed ? 1 : natural };
19
+ }
20
+ function nonNegativeInt(value) {
21
+ return Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0;
22
+ }
@@ -0,0 +1,24 @@
1
+ import type { RenderMode } from './frame.js';
2
+ export interface TodoRailLayoutInput {
3
+ mode: RenderMode;
4
+ cols: number;
5
+ panelEnabled: boolean;
6
+ overlayOpen: boolean;
7
+ itemCount: number;
8
+ viewportBudget: number;
9
+ teamRows: number;
10
+ popupRows: number;
11
+ }
12
+ export interface TodoRailLayout {
13
+ visible: boolean;
14
+ reason: 'visible' | 'inline' | 'disabled' | 'overlay' | 'empty' | 'narrow' | 'short';
15
+ width: number;
16
+ rows: number;
17
+ contentCols: number;
18
+ popupMaxHeight: number;
19
+ }
20
+ /** Allocate columns and rows from the same team/menu projections as the renderers.
21
+ * Invalid geometry becomes zero, excess team/menu rows are clamped. Menu capacity
22
+ * never depends on menu occupancy or rail visibility, preventing feedback loops.
23
+ */
24
+ export declare function buildTodoRailLayout(input: TodoRailLayoutInput): TodoRailLayout;
@@ -0,0 +1,27 @@
1
+ import { TODO_LIMITS } from '../../todo/limits.js';
2
+ import { todoRailWidth } from './rail.js';
3
+ /** Allocate columns and rows from the same team/menu projections as the renderers.
4
+ * Invalid geometry becomes zero, excess team/menu rows are clamped. Menu capacity
5
+ * never depends on menu occupancy or rail visibility, preventing feedback loops.
6
+ */
7
+ export function buildTodoRailLayout(input) {
8
+ const cols = nonNegativeInt(input.cols);
9
+ const budget = nonNegativeInt(input.viewportBudget);
10
+ const teamRows = Math.min(budget, nonNegativeInt(input.teamRows));
11
+ const popupMaxHeight = Math.max(0, budget - teamRows - TODO_LIMITS.panelMinRows);
12
+ const popupRows = popupMaxHeight < 3 ? 0 : Math.min(nonNegativeInt(input.popupRows), popupMaxHeight);
13
+ const rows = budget - teamRows - popupRows;
14
+ const width = todoRailWidth(cols);
15
+ const reason = input.mode !== 'fullscreen' ? 'inline'
16
+ : !input.panelEnabled ? 'disabled'
17
+ : input.overlayOpen ? 'overlay'
18
+ : nonNegativeInt(input.itemCount) === 0 ? 'empty'
19
+ : width === 0 ? 'narrow'
20
+ : rows < TODO_LIMITS.panelMinRows ? 'short'
21
+ : 'visible';
22
+ const visible = reason === 'visible';
23
+ return { visible, reason, width, rows, contentCols: cols - (visible ? width : 0), popupMaxHeight };
24
+ }
25
+ function nonNegativeInt(value) {
26
+ return Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0;
27
+ }
@@ -35,6 +35,7 @@
35
35
  import { FAST_LIMITS } from '../../fast/limits.js';
36
36
  import { COMPACTION_LIMITS } from '../../compaction/limits.js';
37
37
  import { GUTTER_WIDTH } from './Gutter.js';
38
+ import { USER_ENTRY_MAX_ROWS } from '../composer-limits.js';
38
39
  export const VIRTUAL_LIMITS = {
39
40
  /**
40
41
  * Entries kept mounted on each side of the visible band.
@@ -123,6 +124,22 @@ export function entryRevision(entry) {
123
124
  */
124
125
  case 'compaction':
125
126
  return `c${entry.live ? 1 : 0}.${entry.summary?.length ?? 0}.${entry.mode}.${entry.applied ? 1 : 0}`;
127
+ /**
128
+ * `rowsSeen`, AND NEVER `rows.length` (background-service-supervision P0-3).
129
+ *
130
+ * The service tail is a FIXED-SIZE RING: it evicts its oldest row while
131
+ * appending a new one, so the joined length is unchanged while the content is
132
+ * not. That is verbatim the NON-APPEND mutation I-L3-1 above says must extend
133
+ * the revision, and it is the same trap — and the same fix — as the `tool`
134
+ * branch's `liveSeq` two cases up. A length term would go on matching while
135
+ * the card changed underneath it, and the card would freeze at a stale height
136
+ * AND a stale rendered subtree with nothing anywhere reporting it.
137
+ *
138
+ * `status` and the presence of a URL are in because both change the rows the
139
+ * card draws; `exitCode` is in because the terminal row names it.
140
+ */
141
+ case 'service':
142
+ return `v${entry.status}.${entry.rowsSeen}.${entry.url ? 1 : 0}.${entry.exitCode ?? -1}.${entry.terminal ? 1 : 0}`;
126
143
  default:
127
144
  return 'x';
128
145
  }
@@ -164,6 +181,10 @@ const TOOL_COLLAPSED_LINES = 8;
164
181
  const DIFF_COLLAPSED_LINES = 12;
165
182
  /** Live tail rows in `ToolCard`; duplicated as a NUMBER, for the same reason. */
166
183
  const TOOL_LIVE_TAIL_ROWS = 8;
184
+ /** Tail rows a COLLAPSED `ServiceCard` draws; duplicated for the same reason. */
185
+ const SERVICE_COLLAPSED_TAIL_ROWS = 2;
186
+ /** Tail rows an EXPANDED `ServiceCard` draws; the snapshot cap (8). */
187
+ const SERVICE_EXPANDED_TAIL_ROWS = 8;
167
188
  /**
168
189
  * Pure UPPER-BOUND estimate of an entry's height, used until a real measurement
169
190
  * exists.
@@ -181,7 +202,14 @@ export function estimateEntryRows(entry, cols, density, expanded, thinkingVisibl
181
202
  const separation = density === 'comfortable' ? 1 : 0;
182
203
  switch (entry.kind) {
183
204
  case 'user':
184
- return separation + wrappedRows(entry.text, usable);
205
+ // MIRRORS `UserEntry`'s RENDER CAP (+1 for the `... +N more lines` tail).
206
+ // THESE TWO NUMBERS ARE ONE NUMBER (I-13). Capping the renderer alone
207
+ // would leave this function believing a 5 000-line message is 5 000 rows
208
+ // tall -- and unlike an ordinary over-estimate that failure does NOT
209
+ // self-correct: this file's own note above records that "an entry
210
+ // estimated at 400 rows is exactly the one that never gets mounted", and
211
+ // an unmounted entry is never measured.
212
+ return (separation + Math.min(wrappedRows(entry.text, usable), USER_ENTRY_MAX_ROWS + 1));
185
213
  case 'notice':
186
214
  return separation + wrappedRows(entry.text, usable);
187
215
  case 'assistant': {
@@ -277,6 +305,28 @@ export function estimateEntryRows(entry, cols, density, expanded, thinkingVisibl
277
305
  const footer = lines > body || (expanded && lines > COMPACTION_LIMITS.cardTextRows) ? 1 : 0;
278
306
  return separation + 1 + 1 + body + footer;
279
307
  }
308
+ /**
309
+ * THE THIRD SWITCH, and adding the case to only one of this file's two
310
+ * yields a transcript whose scroll arithmetic disagrees with what it drew —
311
+ * drift rather than an error (P0-3 / RV-13). Both `default` branches swallow
312
+ * an unknown kind silently, and `virtual-window.test.ts` hand-enumerates
313
+ * kinds, so neither omission turns anything red on its own.
314
+ *
315
+ * A terminal record is exactly one row. A live card is the status row, the
316
+ * URL row when there is one, and the tail — counted, not wrapped, which is
317
+ * legitimate only because every row is `wrap="truncate"` AND clipped at
318
+ * `LIVE_ROW_MAX_CHARS` on the way into the ring, the same coupling the
319
+ * `tool` branch above records for itself.
320
+ */
321
+ case 'service': {
322
+ if (entry.terminal)
323
+ return separation + 1;
324
+ const url = entry.url ? 1 : 0;
325
+ const tail = expanded
326
+ ? Math.min(entry.rows.length, SERVICE_EXPANDED_TAIL_ROWS)
327
+ : Math.min(entry.rows.length, SERVICE_COLLAPSED_TAIL_ROWS);
328
+ return separation + 1 + url + tail;
329
+ }
280
330
  default:
281
331
  return separation + 1;
282
332
  }
@@ -26,6 +26,9 @@ function keyRows(times) {
26
26
  // startup notice is the other place, and it scrolls away.
27
27
  ['Drag (left button)', 'Select text on screen; releasing copies it'],
28
28
  ['Up / Down', 'Prompt history (empty input)'],
29
+ // Pasting is the one affordance here with no key of its own, so the only
30
+ // place a user can learn what happened to their 218 lines is this row.
31
+ ['Paste', 'Inserted as-is up to 6 lines; larger becomes [Pasted text #N] (--no-paste)'],
29
32
  ];
30
33
  }
31
34
  const COMMANDS = [
@@ -27,6 +27,7 @@ import { Text, useInput } from 'ink';
27
27
  import { pickGlyphs } from '../glyphs.js';
28
28
  import { OverlayFrame } from '../layout/OverlayFrame.js';
29
29
  import { wrapToRows } from '../layout/wrap-rows.js';
30
+ import { stripPasteFrames } from '../paste-frames.js';
30
31
  /** The frame's border plus its one-column padding on each side. */
31
32
  const FRAME_CHROME_COLS = 4;
32
33
  export function PlanReviewOverlay({ plan, maxRows, cols, scrollOffset, onScrollClamp, theme, caps, onVerdict, }) {
@@ -55,8 +56,15 @@ export function PlanReviewOverlay({ plan, maxRows, cols, scrollOffset, onScrollC
55
56
  setFeedback(feedback.slice(0, -1));
56
57
  return;
57
58
  }
58
- if (input && !key.ctrl && !key.meta && !key.tab)
59
- setFeedback(feedback + input);
59
+ // `stripPasteFrames`, NOT `input` (I-12 / P0-2). This is the THIRD consumer
60
+ // that appends `input` to a string; the design's table listed it as
61
+ // "key-driven only, inert", which was true of the version that had no
62
+ // free-text field. Revision feedback is exactly the kind of thing a user
63
+ // pastes — a failing test, a stack trace — and a framed paste appended
64
+ // verbatim would put NULs into text the model then reads.
65
+ if (input && !key.ctrl && !key.meta && !key.tab) {
66
+ setFeedback(feedback + stripPasteFrames(input));
67
+ }
60
68
  return;
61
69
  }
62
70
  if (input === 'a' || input === 'A')