@aragon-agent/cli 0.6.38 → 0.6.39

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 (156) hide show
  1. package/CHANGELOG.md +2523 -2326
  2. package/README.md +3614 -3173
  3. package/dist/agent/controller.d.ts +142 -1
  4. package/dist/agent/controller.js +171 -3
  5. package/dist/agent/reducer.d.ts +41 -1
  6. package/dist/agent/reducer.js +25 -0
  7. package/dist/agent/system-prompt.d.ts +14 -0
  8. package/dist/agent/system-prompt.js +5 -0
  9. package/dist/cli.js +85 -27
  10. package/dist/commands/builtins.js +13 -1
  11. package/dist/commands/keys-command.d.ts +23 -0
  12. package/dist/commands/keys-command.js +38 -0
  13. package/dist/commands/registry.d.ts +39 -0
  14. package/dist/commands/task-command.d.ts +39 -0
  15. package/dist/commands/task-command.js +102 -0
  16. package/dist/config/cli-commands.d.ts +23 -0
  17. package/dist/config/cli-commands.js +36 -0
  18. package/dist/config/env.js +17 -4
  19. package/dist/config/load.js +6 -1
  20. package/dist/config/schema.d.ts +43 -4
  21. package/dist/config/schema.js +31 -5
  22. package/dist/config/store.js +17 -2
  23. package/dist/diagnostics/doctor.d.ts +9 -0
  24. package/dist/diagnostics/doctor.js +34 -0
  25. package/dist/diagnostics/info.d.ts +71 -0
  26. package/dist/diagnostics/info.js +91 -5
  27. package/dist/exec/bridge/caller-tools.d.ts +96 -0
  28. package/dist/exec/bridge/caller-tools.js +213 -0
  29. package/dist/exec/bridge/decisions.d.ts +78 -0
  30. package/dist/exec/bridge/decisions.js +116 -0
  31. package/dist/exec/bridge/human-answers.d.ts +38 -0
  32. package/dist/exec/bridge/human-answers.js +164 -0
  33. package/dist/exec/bridge/limits.d.ts +68 -0
  34. package/dist/exec/bridge/limits.js +68 -0
  35. package/dist/exec/bridge/ndjson.d.ts +17 -0
  36. package/dist/exec/bridge/ndjson.js +83 -0
  37. package/dist/exec/bridge/pending.d.ts +70 -0
  38. package/dist/exec/bridge/pending.js +132 -0
  39. package/dist/exec/bridge/protocol.d.ts +346 -0
  40. package/dist/exec/bridge/protocol.js +256 -0
  41. package/dist/exec/bridge/server.d.ts +142 -0
  42. package/dist/exec/bridge/server.js +502 -0
  43. package/dist/exec/bridge/source.d.ts +58 -0
  44. package/dist/exec/bridge/source.js +145 -0
  45. package/dist/exec/bridge/writer.d.ts +26 -0
  46. package/dist/exec/bridge/writer.js +156 -0
  47. package/dist/exec/cli-commands.js +30 -2
  48. package/dist/exec/emitter.d.ts +16 -2
  49. package/dist/exec/emitter.js +26 -6
  50. package/dist/exec/events.d.ts +465 -10
  51. package/dist/exec/events.js +47 -1
  52. package/dist/exec/hooks/dispatch.d.ts +76 -0
  53. package/dist/exec/hooks/dispatch.js +169 -0
  54. package/dist/exec/hooks/index.d.ts +83 -0
  55. package/dist/exec/hooks/index.js +205 -0
  56. package/dist/exec/hooks/limits.d.ts +56 -0
  57. package/dist/exec/hooks/limits.js +49 -0
  58. package/dist/exec/hooks/registry.d.ts +49 -0
  59. package/dist/exec/hooks/registry.js +91 -0
  60. package/dist/exec/hooks/tool-wrapper.d.ts +94 -0
  61. package/dist/exec/hooks/tool-wrapper.js +154 -0
  62. package/dist/exec/hooks/turn.d.ts +61 -0
  63. package/dist/exec/hooks/turn.js +59 -0
  64. package/dist/exec/hooks/validate.d.ts +85 -0
  65. package/dist/exec/hooks/validate.js +310 -0
  66. package/dist/exec/index.d.ts +130 -4
  67. package/dist/exec/index.js +501 -10
  68. package/dist/exec/options.d.ts +124 -2
  69. package/dist/exec/options.js +413 -6
  70. package/dist/exec/permission.d.ts +9 -1
  71. package/dist/exec/permission.js +7 -1
  72. package/dist/exec/request/limits.d.ts +51 -0
  73. package/dist/exec/request/limits.js +49 -0
  74. package/dist/exec/request/scope.d.ts +106 -0
  75. package/dist/exec/request/scope.js +149 -0
  76. package/dist/exec/result/limits.d.ts +74 -0
  77. package/dist/exec/result/limits.js +72 -0
  78. package/dist/exec/result/prompt.d.ts +28 -0
  79. package/dist/exec/result/prompt.js +74 -0
  80. package/dist/exec/result/schema.d.ts +99 -0
  81. package/dist/exec/result/schema.js +464 -0
  82. package/dist/exec/result/sink.d.ts +56 -0
  83. package/dist/exec/result/sink.js +50 -0
  84. package/dist/exec/result/tool.d.ts +37 -0
  85. package/dist/exec/result/tool.js +118 -0
  86. package/dist/exec/runner.d.ts +395 -1
  87. package/dist/exec/runner.js +860 -8
  88. package/dist/exec/threads/aggregate.d.ts +34 -0
  89. package/dist/exec/threads/aggregate.js +30 -0
  90. package/dist/exec/threads/capacity.d.ts +23 -0
  91. package/dist/exec/threads/capacity.js +38 -0
  92. package/dist/exec/threads/dispatch-source.d.ts +60 -0
  93. package/dist/exec/threads/dispatch-source.js +106 -0
  94. package/dist/exec/threads/execute-threads.d.ts +53 -0
  95. package/dist/exec/threads/execute-threads.js +234 -0
  96. package/dist/exec/threads/gate.d.ts +42 -0
  97. package/dist/exec/threads/gate.js +81 -0
  98. package/dist/exec/threads/identity.d.ts +36 -0
  99. package/dist/exec/threads/identity.js +67 -0
  100. package/dist/exec/threads/interrupt.d.ts +29 -0
  101. package/dist/exec/threads/interrupt.js +79 -0
  102. package/dist/exec/threads/limits.d.ts +58 -0
  103. package/dist/exec/threads/limits.js +35 -0
  104. package/dist/exec/threads/outcome.d.ts +47 -0
  105. package/dist/exec/threads/outcome.js +26 -0
  106. package/dist/exec/threads/registry.d.ts +81 -0
  107. package/dist/exec/threads/registry.js +427 -0
  108. package/dist/exec/threads/runtime.d.ts +107 -0
  109. package/dist/exec/threads/runtime.js +523 -0
  110. package/dist/exec/threads/thread-source.d.ts +20 -0
  111. package/dist/exec/threads/thread-source.js +134 -0
  112. package/dist/fast/limits.d.ts +54 -0
  113. package/dist/fast/limits.js +54 -0
  114. package/dist/fast/summary-call.d.ts +72 -0
  115. package/dist/fast/summary-call.js +133 -0
  116. package/dist/fast/summary-prompt.d.ts +106 -0
  117. package/dist/fast/summary-prompt.js +320 -0
  118. package/dist/fast/task-summary-types.d.ts +66 -0
  119. package/dist/fast/task-summary-types.js +17 -0
  120. package/dist/logging/logger.d.ts +1 -1
  121. package/dist/proc/limits.d.ts +7 -0
  122. package/dist/proc/limits.js +7 -0
  123. package/dist/proc/output-streams.d.ts +50 -0
  124. package/dist/proc/output-streams.js +54 -0
  125. package/dist/team/runtime.d.ts +9 -0
  126. package/dist/team/runtime.js +8 -0
  127. package/dist/team/subagent.d.ts +15 -0
  128. package/dist/team/subagent.js +45 -1
  129. package/dist/tools/bash-tool.d.ts +10 -0
  130. package/dist/tools/bash-tool.js +71 -6
  131. package/dist/tools/index.d.ts +93 -1
  132. package/dist/tools/index.js +112 -3
  133. package/dist/tools/tool-gate.d.ts +100 -0
  134. package/dist/tools/tool-gate.js +87 -0
  135. package/dist/ui/App.js +224 -11
  136. package/dist/ui/Header.d.ts +28 -16
  137. package/dist/ui/Header.js +21 -19
  138. package/dist/ui/interaction-hints.js +6 -0
  139. package/dist/ui/layout/OverlayFrame.d.ts +18 -0
  140. package/dist/ui/layout/OverlayFrame.js +19 -4
  141. package/dist/ui/layout/header-hints.d.ts +55 -62
  142. package/dist/ui/layout/header-hints.js +59 -166
  143. package/dist/ui/layout/header-left.d.ts +115 -0
  144. package/dist/ui/layout/header-left.js +206 -0
  145. package/dist/ui/layout/shortcut-panel-layout.d.ts +191 -0
  146. package/dist/ui/layout/shortcut-panel-layout.js +321 -0
  147. package/dist/ui/layout/status-detail-layout.js +5 -1
  148. package/dist/ui/layout/task-summary-cell.d.ts +61 -0
  149. package/dist/ui/layout/task-summary-cell.js +65 -0
  150. package/dist/ui/overlays/HelpOverlay.d.ts +12 -0
  151. package/dist/ui/overlays/HelpOverlay.js +29 -2
  152. package/dist/ui/overlays/ShortcutPanel.d.ts +39 -0
  153. package/dist/ui/overlays/ShortcutPanel.js +106 -0
  154. package/dist/ui/shortcuts.d.ts +81 -0
  155. package/dist/ui/shortcuts.js +221 -0
  156. package/package.json +2 -2
@@ -0,0 +1,206 @@
1
+ /**
2
+ * The header's LEFT half budget (tui-shortcut-drawer-and-task-summary §4.3).
3
+ *
4
+ * ASCII ONLY: `src/ui/**` is inside the glyph scanner's scope.
5
+ *
6
+ * WHY A PLANNER (the same argument `header-hints.ts` makes one file over): the
7
+ * left half used to be a `<Text wrap="truncate">` over four clauses with no
8
+ * clause-level arithmetic anywhere, so the only thing that ever decided what got
9
+ * cut was the terminal. Adding a fifth clause - the task summary - to that shape
10
+ * produces a header where the summary silently eats the directory name on an
11
+ * 80-column terminal and NO ASSERTION WOULD SEE IT, because losing trailing
12
+ * characters is invisible to a snapshot test.
13
+ *
14
+ * THE PRIORITY ORDER IS THE DESIGN: brand, then `provider:model`, then cwd, then
15
+ * the summary, with the key symbol reserved up front. The summary is the only
16
+ * clause that is allowed to disappear entirely - it is the newest and the least
17
+ * load-bearing (D-2) - and it is dropped rather than rendered as `...`, because a
18
+ * cell holding three dots spends eight columns to say nothing.
19
+ */
20
+ import { cellWidth, fitCells } from './cells.js';
21
+ import { TASK_SUMMARY_LIMITS } from '../../fast/limits.js';
22
+ export const HEADER_LEFT_LIMITS = {
23
+ /** The `AragonAgent` word mark, the same 13 `header-hints.ts` documents. */
24
+ brandCells: 13,
25
+ /** Blank cells between two left clauses. */
26
+ gapCells: 2,
27
+ /** The has-key symbol (1 cell) plus its gap, always reserved. */
28
+ keyCells: 3,
29
+ /** Below this the model clause is dropped rather than rendered as `a...`. */
30
+ modelMinCells: 8,
31
+ /** Below this the cwd clause is dropped. */
32
+ cwdMinCells: 8,
33
+ /** Below this the summary is dropped instead of rendered as `...`. */
34
+ summaryMinCells: 8,
35
+ /**
36
+ * The separator that belongs to the SUMMARY rather than to the general gap:
37
+ * two spaces, the mid dot and one space. It is reserved together with the label
38
+ * so a summary can never be drawn without its dot, or the dot without it.
39
+ */
40
+ summaryGapCells: 4,
41
+ };
42
+ /** `provider:model`, the clause `Header` prints after the word mark. */
43
+ export function modelClause(provider, model) {
44
+ return `${provider}:${model}`;
45
+ }
46
+ /** The directory name, falling back to the whole path for a bare root. */
47
+ export function cwdClause(cwd, basename) {
48
+ return basename(cwd) || cwd;
49
+ }
50
+ /**
51
+ * Allocate the left half.
52
+ *
53
+ * THE ORDER OF THE FOUR STEPS IS THE PRIORITY (D-2):
54
+ * 1. reserve the word mark, its gap and the key symbol - they are never cut;
55
+ * 2. `provider:model`, down to `modelMinCells`, then dropped;
56
+ * 3. cwd, down to `cwdMinCells`, then dropped;
57
+ * 4. the summary, capped at `TASK_SUMMARY_LIMITS.maxCells`, dropped below
58
+ * `summaryMinCells` and reported as `narrow`.
59
+ *
60
+ * THE LADDER IS EXPLICIT RATHER THAN GREEDY, and that is a correction to v2's
61
+ * algorithm, which funded the summary out of the reserved budget and could therefore
62
+ * show a label on a row where the model name had already been dropped. Four
63
+ * presence sets are tried from most to least complete; within a set the LAST clause
64
+ * gives way first, each only down to its own minimum. The first set that fits wins.
65
+ *
66
+ * Every returned text already MEASURES its reported width, so `Header` renders
67
+ * cells rather than hoping for them.
68
+ */
69
+ export function planHeaderLeft(input) {
70
+ const leftCells = Number.isFinite(input.leftCells) ? Math.max(0, Math.floor(input.leftCells)) : 0;
71
+ const empty = {
72
+ text: { model: '', cwd: '', summary: '' },
73
+ cells: { model: 0, cwd: 0, summary: 0 },
74
+ summaryReason: input.summaryEnabled ? 'empty' : 'disabled',
75
+ };
76
+ const gap = HEADER_LEFT_LIMITS.gapCells;
77
+ // Everything the planner never touches, taken off the top so no later step can
78
+ // borrow from it.
79
+ // `keyCells` already contains the gap before the symbol, so this is the whole fixed
80
+ // cost and every clause below pays its own gap.
81
+ const budget = leftCells - HEADER_LEFT_LIMITS.brandCells - HEADER_LEFT_LIMITS.keyCells;
82
+ if (budget <= 0) {
83
+ return {
84
+ ...empty,
85
+ summaryReason: input.summaryEnabled
86
+ ? (input.summary === null ? 'empty' : 'narrow')
87
+ : 'disabled',
88
+ };
89
+ }
90
+ const model = modelClause(input.provider, input.model);
91
+ const summaryText = input.summary?.text ?? '';
92
+ const wantsSummary = input.summaryEnabled && input.summary !== null;
93
+ /** Desired width and the floor this clause may be cut to. */
94
+ const clause = (text, floor) => ({
95
+ want: cellWidth(text),
96
+ floor: Math.min(cellWidth(text), floor),
97
+ });
98
+ const modelWanted = clause(model, HEADER_LEFT_LIMITS.modelMinCells);
99
+ const cwdWanted = clause(input.cwd, HEADER_LEFT_LIMITS.cwdMinCells);
100
+ const summaryWanted = {
101
+ want: Math.min(cellWidth(summaryText), TASK_SUMMARY_LIMITS.maxCells),
102
+ // A label is never shown below its minimum: a three-cell label is a fragment,
103
+ // not a label, and the cell is dropped instead.
104
+ floor: HEADER_LEFT_LIMITS.summaryMinCells,
105
+ };
106
+ const sets = [
107
+ ['model', 'cwd', ...(wantsSummary ? ['summary'] : [])],
108
+ ['model', 'cwd'],
109
+ ['model'],
110
+ [],
111
+ ];
112
+ const wanted = { model: modelWanted, cwd: cwdWanted, summary: summaryWanted };
113
+ for (const set of sets) {
114
+ const assigned = fitClauses(set, wanted, budget, gap);
115
+ if (assigned === null)
116
+ continue;
117
+ const modelCells = assigned.model ?? 0;
118
+ const cwdCells = assigned.cwd ?? 0;
119
+ const summaryCells = assigned.summary ?? 0;
120
+ let summaryReason = 'empty';
121
+ if (!input.summaryEnabled)
122
+ summaryReason = 'disabled';
123
+ else if (input.summary === null)
124
+ summaryReason = 'empty';
125
+ else if (summaryCells === 0)
126
+ summaryReason = 'narrow';
127
+ else
128
+ summaryReason = 'ok';
129
+ return {
130
+ text: {
131
+ model: modelCells > 0 ? fitCells(model, modelCells, input.ellipsis) : '',
132
+ cwd: cwdCells > 0 ? fitCells(input.cwd, cwdCells, input.ellipsis) : '',
133
+ summary: summaryCells > 0 ? fitCells(summaryText, summaryCells, input.ellipsis) : '',
134
+ },
135
+ cells: { model: modelCells, cwd: cwdCells, summary: summaryCells },
136
+ summaryReason,
137
+ };
138
+ }
139
+ return empty;
140
+ }
141
+ /**
142
+ * Fit one presence set into `budget`, cutting from the LAST clause inward.
143
+ *
144
+ * Returns `null` when the set cannot fit even at every clause's floor, which is the
145
+ * signal to try the next (smaller) set. `null` rather than a partial result because
146
+ * "the summary was dropped" and "the summary does not fit" must be the same fact to
147
+ * the caller, and only the set composition may express it.
148
+ */
149
+ function fitClauses(set, wanted, budget, gap) {
150
+ const widths = { model: 0, cwd: 0, summary: 0 };
151
+ let cost = 0;
152
+ for (const key of set) {
153
+ widths[key] = wanted[key].want;
154
+ // EVERY CLAUSE PAYS ITS OWN SEPARATOR, including the first: the row renders
155
+ // `word mark, gap, clause, gap, clause, gap, key`, so a one-clause plan still
156
+ // spends a gap on each side of that clause.
157
+ cost += (key === 'summary' ? HEADER_LEFT_LIMITS.summaryGapCells : gap)
158
+ + wanted[key].want;
159
+ }
160
+ let excess = cost - budget;
161
+ for (let index = set.length - 1; index >= 0 && excess > 0; index -= 1) {
162
+ const key = set[index];
163
+ const room = widths[key] - wanted[key].floor;
164
+ if (room <= 0)
165
+ continue;
166
+ const cut = Math.min(room, excess);
167
+ widths[key] -= cut;
168
+ excess -= cut;
169
+ }
170
+ return excess > 0 ? null : widths;
171
+ }
172
+ /**
173
+ * Cells the plan actually spends, for the row invariant:
174
+ * `brand + gap + model + gap + cwd + gap + summary + key <= leftCells`.
175
+ *
176
+ * Exported so `header-left.test.ts` can assert the inequality against the SAME
177
+ * arithmetic the renderer uses instead of re-deriving it in the test (which is how
178
+ * the two drift apart).
179
+ */
180
+ export function planHeaderLeftCells(plan) {
181
+ const { gapCells, brandCells, keyCells, summaryGapCells } = HEADER_LEFT_LIMITS;
182
+ // The word mark and the key symbol are always drawn; every clause pays its own gap.
183
+ // The gap before the key is INSIDE `keyCells`, so it is counted once here and never
184
+ // again after a clause - the double count that made a one-clause plan read two cells
185
+ // wider than it renders.
186
+ let cells = brandCells + keyCells;
187
+ if (plan.cells.model > 0)
188
+ cells += gapCells + plan.cells.model;
189
+ if (plan.cells.cwd > 0)
190
+ cells += gapCells + plan.cells.cwd;
191
+ // The summary's separator carries the dot, so it is wider than the general gap.
192
+ if (plan.cells.summary > 0)
193
+ cells += summaryGapCells + plan.cells.summary;
194
+ return cells;
195
+ }
196
+ /**
197
+ * The fixed cells a plan needs before any clause can appear: the word mark plus the
198
+ * key symbol and its gap.
199
+ *
200
+ * Below this the row does not "spend too much", it simply cannot show a clause - the
201
+ * renderer truncates the overflow and `Header`'s own `wrap="truncate"` keeps that from
202
+ * spilling. Stating it as a constant keeps the invariant in the tests honest:
203
+ * `planHeaderLeftCells(plan) <= max(leftCells, HEADER_LEFT_BASELINE)`.
204
+ */
205
+ export const HEADER_LEFT_BASELINE = HEADER_LEFT_LIMITS.brandCells
206
+ + HEADER_LEFT_LIMITS.keyCells;
@@ -0,0 +1,191 @@
1
+ /**
2
+ * The shortcut drawer's geometry and row layout
3
+ * (tui-shortcut-drawer-and-task-summary §4.5).
4
+ *
5
+ * ASCII ONLY: `src/ui/**` is inside the glyph scanner's scope, so every symbol
6
+ * this module emits arrives as a `glyphs` field and is never spelled here.
7
+ *
8
+ * PURE AND TOTAL. No Ink, no clock, no terminal: the drawer component renders
9
+ * what this returns, `OverlayFrame` slices the window this function DERIVED, and
10
+ * `shortcut-panel-layout.test.ts` pins the arithmetic directly. That split is the
11
+ * whole defence against P0-1, which was not a rendering bug: v1 hand-fixed the key
12
+ * column at 21 cells, which left six cells for the keyword at 80 columns, so every
13
+ * keyword was truncated to `promp...` while a "does not overflow" assertion stayed
14
+ * green. The column is therefore DERIVED from the table, and a keyword that does
15
+ * not fit makes its row STACK rather than be cut.
16
+ *
17
+ * THERE IS EXACTLY ONE WINDOW AUTHORITY: `cursor`. v1 took both `cursor` and
18
+ * `scrollOffset`, which meant paging could leave the selected row - and the detail
19
+ * line that belongs to it - off screen. Paging now moves the CURSOR by a page and
20
+ * the window is the smallest one containing it.
21
+ */
22
+ import { type OverlayWindow } from './overlay-window.js';
23
+ import { type ShortcutEntry } from '../shortcuts.js';
24
+ export declare const SHORTCUT_PANEL_LIMITS: {
25
+ /** Below this the drawer is full width: a 30-cell bar on a 40-cell screen is noise. */
26
+ readonly minSplitCols: 64;
27
+ /** Fraction of the terminal the drawer takes when it IS a drawer. */
28
+ readonly widthFraction: 0.34;
29
+ readonly minWidth: 30;
30
+ readonly maxWidth: 46;
31
+ /** Rail (1 cell) + `paddingLeft` (1 cell) that `OverlayFrame` spends below 48 columns. */
32
+ readonly railCells: 2;
33
+ /** Border (2 cells) + `paddingX` (2 cells) that `OverlayFrame` spends at or above 48. */
34
+ readonly boxCells: 4;
35
+ /**
36
+ * The key column is DERIVED: `min(longestKeys, ceil(listCells * 0.55))`.
37
+ *
38
+ * A hard 21 is what turned `prompt library` into `promp...` at 80 columns. The
39
+ * share is a ceiling, not a target: a table of short keys gets a narrow key
40
+ * column and the leftover width goes to the keyword.
41
+ */
42
+ readonly keyColumnShare: 0.55;
43
+ /** The keyword column must be at least this wide, or the row stacks. */
44
+ readonly keywordMinCells: 8;
45
+ /** Hard ceiling for a keyword string; a longer one is a spec bug, not a width problem. */
46
+ readonly keywordMaxCells: 10;
47
+ /** Gap between the key column and the keyword column. */
48
+ readonly keyGapCells: 1;
49
+ /** Indent for the selected row's detail lines, and for a stacked keyword. */
50
+ readonly detailIndent: 2;
51
+ /** Title + footer rows are chrome and are spent out of the row budget. */
52
+ readonly chromeRows: 2;
53
+ /** The task section is capped so the key list always gets the majority of the panel. */
54
+ readonly taskMaxRows: 2;
55
+ /**
56
+ * ABOVE THIS BODY HEIGHT THE GROUP HEADERS GET THEIR OWN ROWS.
57
+ *
58
+ * Below it they are dropped entirely - not compressed into `---- GROUP ----`
59
+ * separators, which was v2's rule and left FOUR entry rows visible on an
60
+ * 80x24 terminal (title + task + rule + five separators = 9 of 13 body rows).
61
+ * See `spec.md`'s `## 实施过程发现的方案缺陷` note 1: the ordering still runs
62
+ * COMPOSE -> RUN -> PANELS -> SESSION -> VIEW, and the requirement ("list every
63
+ * shortcut from top to bottom") is met by the entries themselves.
64
+ */
65
+ readonly groupHeaderMinRows: 18;
66
+ /** The longest detail line the selected row may grow to. */
67
+ readonly detailMaxRows: 2;
68
+ };
69
+ /**
70
+ * The drawer's width in columns.
71
+ *
72
+ * `cols < 64` is FULL WIDTH on purpose: at 40 columns a right-aligned 30-cell bar
73
+ * spends 10 of the user's columns on empty space. The clamp mirrors
74
+ * `rail.ts::todoRailWidth` (same shape, same reason).
75
+ */
76
+ export declare function shortcutPanelWidth(cols: number): number;
77
+ /**
78
+ * Cells the drawer may draw on INSIDE its frame.
79
+ *
80
+ * THE FORM IS DECIDED BY THE TERMINAL, exactly as `OverlayFrame` decides it
81
+ * (`cols >= BORDERLESS_COLS`), so this function takes the same `cols` the frame
82
+ * receives. Passing the panel's own width instead would make a 46-cell drawer on
83
+ * a 200-column terminal fall back to the rail form for ever, because 46 < 48.
84
+ */
85
+ export declare function shortcutPanelContentCells(cols: number): number;
86
+ /** One rendered row. `text` already measures at most the panel's content width. */
87
+ export interface ShortcutRow {
88
+ /** `entry` = key + keyword on one row; `stacked` = key alone, keyword below. */
89
+ kind: 'task' | 'rule' | 'group' | 'entry' | 'stacked' | 'keyword' | 'detail';
90
+ text: string;
91
+ /** Which entry a `detail` / `keyword` row belongs to; `null` for every other kind. */
92
+ entryId: string | null;
93
+ tone: 'accent' | 'primary' | 'muted' | 'dim';
94
+ }
95
+ /** The task section's inputs, already flattened by the caller. */
96
+ export interface ShortcutTaskSection {
97
+ /** The label, or `null` when there is none yet. */
98
+ text: string | null;
99
+ /** `model` / `fallback`, so the row can be coloured by provenance. */
100
+ tone: 'model' | 'fallback' | null;
101
+ /** Why it is a fallback, or `null`. */
102
+ reason: string | null;
103
+ /** `provider:model`, or `''`. */
104
+ by: string;
105
+ /** Age in ms, or `null` when unknown. */
106
+ ageMs: number | null;
107
+ /** `false` when `taskSummary.enabled` is off. */
108
+ enabled: boolean;
109
+ /** A call is in flight. */
110
+ pending: boolean;
111
+ }
112
+ export interface BuildShortcutRowsInput {
113
+ cols: number;
114
+ /** Body rows available INSIDE the frame (`OverlayFrame` already subtracted its chrome). */
115
+ rows: number;
116
+ /** Index into the FILTERED entry list. The one authority for the window. */
117
+ cursor: number;
118
+ /**
119
+ * THE ENTRIES ARRIVE FILTERED AND WITH THEIR AVAILABILITY, so this module takes
120
+ * neither a `query` nor a `context` for them (review fix RF-2). It used to accept
121
+ * both - and `detailsExpanded` as well - and read none of them, which is the exact
122
+ * "second source of truth that silently does nothing" its own 0-3 rule forbids: a
123
+ * caller could pass `query: 'git'` and get the unfiltered list with nothing
124
+ * failing.
125
+ */
126
+ entries: readonly ShortcutEntry[];
127
+ task: ShortcutTaskSection;
128
+ /** Per-entry availability, so an unavailable row renders dim and explains itself. */
129
+ entryAvailable?: (entry: ShortcutEntry) => boolean;
130
+ /** The sentence appended to an unavailable row's detail line. */
131
+ unavailableReason?: string;
132
+ glyphs: {
133
+ ellipsis: string;
134
+ cursor: string;
135
+ hrule: string;
136
+ midDot: string;
137
+ };
138
+ }
139
+ export interface BuildShortcutRowsResult {
140
+ rows: ShortcutRow[];
141
+ window: OverlayWindow;
142
+ /** Row index of the selected entry; equal to `window.start + 0` at the top. */
143
+ cursorRow: number;
144
+ /** Total rows the panel holds, which is what the frame's position indicator counts. */
145
+ total: number;
146
+ /** The footer string; `OverlayFrame`'s `hint`, and chrome rather than a row. */
147
+ hint: string;
148
+ }
149
+ /** Column widths for one geometry. Exported so the tests can assert them directly. */
150
+ export interface ShortcutColumns {
151
+ /** The panel's own width. */
152
+ width: number;
153
+ /** Content cells inside the frame. */
154
+ available: number;
155
+ /** Cells left for a list row after the cursor column is spent. */
156
+ listCells: number;
157
+ keyCells: number;
158
+ keywordCells: number;
159
+ /** True when no entry fits the two-column form and every row stacks. */
160
+ stacked: boolean;
161
+ }
162
+ /**
163
+ * Derive the two columns (P0-1).
164
+ *
165
+ * `stacked` is a GLOBAL flag only when the keyword column is too narrow for any
166
+ * keyword at all; otherwise stacking is decided per row, so one long binding
167
+ * (`Shift+Up / Shift+Down`, 20 cells) does not cost the other thirty rows a line.
168
+ */
169
+ export declare function shortcutColumns(cols: number, entries: readonly ShortcutEntry[], glyphs: {
170
+ cursor: string;
171
+ }): ShortcutColumns;
172
+ /**
173
+ * The footer, built by DROPPING clauses from the right (never by re-wording).
174
+ *
175
+ * `Esc close` is never dropped: it is the one thing a user must be able to find on
176
+ * a terminal too narrow to explain anything else. The rest go in a fixed order so
177
+ * the footer is stable across resizes instead of shuffling.
178
+ */
179
+ export declare function shortcutFooter(cols: number, glyphs: {
180
+ midDot: string;
181
+ }, opts?: {
182
+ canRefresh: boolean;
183
+ }): string;
184
+ /**
185
+ * Build every row the drawer can show, and derive the window from the cursor.
186
+ *
187
+ * @param input.cols Terminal columns (the frame receives the same number).
188
+ * @param input.rows Body rows inside the frame - `overlayBodyRows(maxRows, cols)`.
189
+ * @param input.cursor Index into `input.entries`; clamped, never trusted.
190
+ */
191
+ export declare function buildShortcutRows(input: BuildShortcutRowsInput): BuildShortcutRowsResult;