@aragon-agent/cli 0.6.38 → 0.6.40

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 (209) hide show
  1. package/CHANGELOG.md +263 -0
  2. package/README.md +544 -11
  3. package/dist/agent/controller.d.ts +245 -10
  4. package/dist/agent/controller.js +367 -22
  5. package/dist/agent/model-profile-settings.js +12 -1
  6. package/dist/agent/plan-prompt.d.ts +1 -3
  7. package/dist/agent/plan-prompt.js +13 -7
  8. package/dist/agent/reducer.d.ts +70 -2
  9. package/dist/agent/reducer.js +59 -6
  10. package/dist/agent/skill-triage-prompt.d.ts +42 -0
  11. package/dist/agent/skill-triage-prompt.js +74 -0
  12. package/dist/agent/system-prompt.d.ts +28 -2
  13. package/dist/agent/system-prompt.js +12 -4
  14. package/dist/cli.js +106 -32
  15. package/dist/commands/builtins.js +24 -3
  16. package/dist/commands/keys-command.d.ts +23 -0
  17. package/dist/commands/keys-command.js +38 -0
  18. package/dist/commands/registry.d.ts +39 -0
  19. package/dist/commands/skills.js +86 -1
  20. package/dist/commands/subagent-command.d.ts +25 -0
  21. package/dist/commands/subagent-command.js +242 -0
  22. package/dist/commands/task-command.d.ts +39 -0
  23. package/dist/commands/task-command.js +102 -0
  24. package/dist/compaction/command.js +16 -3
  25. package/dist/compaction/compactor.d.ts +25 -0
  26. package/dist/compaction/compactor.js +111 -10
  27. package/dist/compaction/digest.d.ts +24 -1
  28. package/dist/compaction/digest.js +139 -17
  29. package/dist/compaction/limits.d.ts +21 -0
  30. package/dist/compaction/limits.js +21 -0
  31. package/dist/compaction/reason-text.d.ts +36 -0
  32. package/dist/compaction/reason-text.js +78 -0
  33. package/dist/compaction/types.d.ts +82 -14
  34. package/dist/compaction/wiring.d.ts +13 -8
  35. package/dist/compaction/wiring.js +1 -0
  36. package/dist/config/cli-commands.d.ts +23 -0
  37. package/dist/config/cli-commands.js +36 -0
  38. package/dist/config/env.js +17 -4
  39. package/dist/config/load.js +55 -3
  40. package/dist/config/schema.d.ts +125 -10
  41. package/dist/config/schema.js +138 -14
  42. package/dist/config/store.js +23 -3
  43. package/dist/diagnostics/doctor.d.ts +9 -0
  44. package/dist/diagnostics/doctor.js +34 -0
  45. package/dist/diagnostics/info.d.ts +71 -0
  46. package/dist/diagnostics/info.js +91 -5
  47. package/dist/exec/bridge/caller-tools.d.ts +96 -0
  48. package/dist/exec/bridge/caller-tools.js +213 -0
  49. package/dist/exec/bridge/decisions.d.ts +78 -0
  50. package/dist/exec/bridge/decisions.js +116 -0
  51. package/dist/exec/bridge/human-answers.d.ts +38 -0
  52. package/dist/exec/bridge/human-answers.js +164 -0
  53. package/dist/exec/bridge/limits.d.ts +68 -0
  54. package/dist/exec/bridge/limits.js +68 -0
  55. package/dist/exec/bridge/ndjson.d.ts +17 -0
  56. package/dist/exec/bridge/ndjson.js +83 -0
  57. package/dist/exec/bridge/pending.d.ts +70 -0
  58. package/dist/exec/bridge/pending.js +132 -0
  59. package/dist/exec/bridge/protocol.d.ts +346 -0
  60. package/dist/exec/bridge/protocol.js +256 -0
  61. package/dist/exec/bridge/server.d.ts +142 -0
  62. package/dist/exec/bridge/server.js +502 -0
  63. package/dist/exec/bridge/source.d.ts +58 -0
  64. package/dist/exec/bridge/source.js +145 -0
  65. package/dist/exec/bridge/writer.d.ts +26 -0
  66. package/dist/exec/bridge/writer.js +156 -0
  67. package/dist/exec/cli-commands.js +30 -2
  68. package/dist/exec/emitter.d.ts +16 -2
  69. package/dist/exec/emitter.js +26 -6
  70. package/dist/exec/events.d.ts +465 -10
  71. package/dist/exec/events.js +47 -1
  72. package/dist/exec/hooks/dispatch.d.ts +76 -0
  73. package/dist/exec/hooks/dispatch.js +169 -0
  74. package/dist/exec/hooks/index.d.ts +83 -0
  75. package/dist/exec/hooks/index.js +205 -0
  76. package/dist/exec/hooks/limits.d.ts +56 -0
  77. package/dist/exec/hooks/limits.js +49 -0
  78. package/dist/exec/hooks/registry.d.ts +49 -0
  79. package/dist/exec/hooks/registry.js +91 -0
  80. package/dist/exec/hooks/tool-wrapper.d.ts +94 -0
  81. package/dist/exec/hooks/tool-wrapper.js +154 -0
  82. package/dist/exec/hooks/turn.d.ts +61 -0
  83. package/dist/exec/hooks/turn.js +59 -0
  84. package/dist/exec/hooks/validate.d.ts +85 -0
  85. package/dist/exec/hooks/validate.js +310 -0
  86. package/dist/exec/index.d.ts +130 -4
  87. package/dist/exec/index.js +501 -10
  88. package/dist/exec/options.d.ts +124 -2
  89. package/dist/exec/options.js +413 -6
  90. package/dist/exec/permission.d.ts +9 -1
  91. package/dist/exec/permission.js +7 -1
  92. package/dist/exec/request/limits.d.ts +51 -0
  93. package/dist/exec/request/limits.js +49 -0
  94. package/dist/exec/request/scope.d.ts +106 -0
  95. package/dist/exec/request/scope.js +149 -0
  96. package/dist/exec/result/limits.d.ts +74 -0
  97. package/dist/exec/result/limits.js +72 -0
  98. package/dist/exec/result/prompt.d.ts +28 -0
  99. package/dist/exec/result/prompt.js +74 -0
  100. package/dist/exec/result/schema.d.ts +99 -0
  101. package/dist/exec/result/schema.js +464 -0
  102. package/dist/exec/result/sink.d.ts +56 -0
  103. package/dist/exec/result/sink.js +50 -0
  104. package/dist/exec/result/tool.d.ts +37 -0
  105. package/dist/exec/result/tool.js +118 -0
  106. package/dist/exec/runner.d.ts +395 -1
  107. package/dist/exec/runner.js +860 -8
  108. package/dist/exec/threads/aggregate.d.ts +34 -0
  109. package/dist/exec/threads/aggregate.js +30 -0
  110. package/dist/exec/threads/capacity.d.ts +23 -0
  111. package/dist/exec/threads/capacity.js +38 -0
  112. package/dist/exec/threads/dispatch-source.d.ts +60 -0
  113. package/dist/exec/threads/dispatch-source.js +106 -0
  114. package/dist/exec/threads/execute-threads.d.ts +53 -0
  115. package/dist/exec/threads/execute-threads.js +234 -0
  116. package/dist/exec/threads/gate.d.ts +42 -0
  117. package/dist/exec/threads/gate.js +81 -0
  118. package/dist/exec/threads/identity.d.ts +36 -0
  119. package/dist/exec/threads/identity.js +67 -0
  120. package/dist/exec/threads/interrupt.d.ts +29 -0
  121. package/dist/exec/threads/interrupt.js +79 -0
  122. package/dist/exec/threads/limits.d.ts +58 -0
  123. package/dist/exec/threads/limits.js +35 -0
  124. package/dist/exec/threads/outcome.d.ts +47 -0
  125. package/dist/exec/threads/outcome.js +26 -0
  126. package/dist/exec/threads/registry.d.ts +81 -0
  127. package/dist/exec/threads/registry.js +427 -0
  128. package/dist/exec/threads/runtime.d.ts +107 -0
  129. package/dist/exec/threads/runtime.js +523 -0
  130. package/dist/exec/threads/thread-source.d.ts +20 -0
  131. package/dist/exec/threads/thread-source.js +134 -0
  132. package/dist/fast/limits.d.ts +54 -0
  133. package/dist/fast/limits.js +54 -0
  134. package/dist/fast/summary-call.d.ts +72 -0
  135. package/dist/fast/summary-call.js +133 -0
  136. package/dist/fast/summary-prompt.d.ts +106 -0
  137. package/dist/fast/summary-prompt.js +320 -0
  138. package/dist/fast/task-summary-types.d.ts +66 -0
  139. package/dist/fast/task-summary-types.js +17 -0
  140. package/dist/logging/logger.d.ts +1 -1
  141. package/dist/proc/limits.d.ts +7 -0
  142. package/dist/proc/limits.js +7 -0
  143. package/dist/proc/log-mirror.d.ts +21 -4
  144. package/dist/proc/log-mirror.js +29 -7
  145. package/dist/proc/output-streams.d.ts +50 -0
  146. package/dist/proc/output-streams.js +54 -0
  147. package/dist/skills/service.d.ts +41 -0
  148. package/dist/skills/service.js +62 -0
  149. package/dist/skills/triage.d.ts +194 -0
  150. package/dist/skills/triage.js +393 -0
  151. package/dist/team/forced-dispatch.d.ts +95 -0
  152. package/dist/team/forced-dispatch.js +208 -0
  153. package/dist/team/limits.d.ts +21 -1
  154. package/dist/team/limits.js +21 -1
  155. package/dist/team/normalize.d.ts +15 -0
  156. package/dist/team/normalize.js +21 -4
  157. package/dist/team/prompt.d.ts +6 -3
  158. package/dist/team/prompt.js +9 -5
  159. package/dist/team/runtime.d.ts +9 -0
  160. package/dist/team/runtime.js +8 -0
  161. package/dist/team/subagent.d.ts +15 -0
  162. package/dist/team/subagent.js +45 -1
  163. package/dist/team/task-tool.js +11 -7
  164. package/dist/tools/bash-tool.d.ts +10 -0
  165. package/dist/tools/bash-tool.js +71 -6
  166. package/dist/tools/human-input.d.ts +35 -2
  167. package/dist/tools/human-input.js +38 -0
  168. package/dist/tools/index.d.ts +93 -1
  169. package/dist/tools/index.js +112 -3
  170. package/dist/tools/plan-tools.d.ts +20 -12
  171. package/dist/tools/plan-tools.js +24 -17
  172. package/dist/tools/tool-gate.d.ts +100 -0
  173. package/dist/tools/tool-gate.js +87 -0
  174. package/dist/ui/App.js +238 -12
  175. package/dist/ui/Header.d.ts +28 -16
  176. package/dist/ui/Header.js +21 -19
  177. package/dist/ui/StatusBar.d.ts +1 -0
  178. package/dist/ui/StatusBar.js +1 -0
  179. package/dist/ui/Transcript.js +1 -1
  180. package/dist/ui/entries/CompactionCard.d.ts +11 -1
  181. package/dist/ui/entries/CompactionCard.js +65 -4
  182. package/dist/ui/interaction-hints.js +6 -0
  183. package/dist/ui/layout/OverlayFrame.d.ts +18 -0
  184. package/dist/ui/layout/OverlayFrame.js +19 -4
  185. package/dist/ui/layout/header-hints.d.ts +55 -62
  186. package/dist/ui/layout/header-hints.js +59 -166
  187. package/dist/ui/layout/header-left.d.ts +115 -0
  188. package/dist/ui/layout/header-left.js +206 -0
  189. package/dist/ui/layout/shortcut-panel-layout.d.ts +191 -0
  190. package/dist/ui/layout/shortcut-panel-layout.js +321 -0
  191. package/dist/ui/layout/status-detail-layout.js +5 -1
  192. package/dist/ui/layout/status-layout.d.ts +10 -0
  193. package/dist/ui/layout/status-layout.js +18 -1
  194. package/dist/ui/layout/task-summary-cell.d.ts +61 -0
  195. package/dist/ui/layout/task-summary-cell.js +65 -0
  196. package/dist/ui/overlays/HelpOverlay.d.ts +12 -0
  197. package/dist/ui/overlays/HelpOverlay.js +29 -2
  198. package/dist/ui/overlays/SettingsScreen.js +29 -5
  199. package/dist/ui/overlays/ShortcutPanel.d.ts +39 -0
  200. package/dist/ui/overlays/ShortcutPanel.js +106 -0
  201. package/dist/ui/overlays/TeamFanoutPage.d.ts +35 -0
  202. package/dist/ui/overlays/TeamFanoutPage.js +78 -0
  203. package/dist/ui/overlays/model-profile-state.d.ts +19 -1
  204. package/dist/ui/overlays/model-profile-state.js +12 -0
  205. package/dist/ui/overlays/team-fanout-state.d.ts +73 -0
  206. package/dist/ui/overlays/team-fanout-state.js +97 -0
  207. package/dist/ui/shortcuts.d.ts +81 -0
  208. package/dist/ui/shortcuts.js +221 -0
  209. package/package.json +2 -2
package/dist/ui/Header.js CHANGED
@@ -2,24 +2,26 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  import { basename } from 'node:path';
3
3
  import { Box, Text } from 'ink';
4
4
  import { Logo } from './Logo.js';
5
- import { planHeaderHints, HEADER_HINT_LIMITS } from './layout/header-hints.js';
6
- export function Header({ cwd, provider, model, hasKey, variant, theme, caps, columns = 80, statusExpanded = false, scrolledLines = 0, overlayOpen = false, kitHint = true, promptsHint = true }) {
7
- // The right-hand hints are planned, not assembled (tui-branch-terminal-shortcut
8
- // 3.9 / D-11). `plan.text` is exactly `plan.cells` wide and already carries the
9
- // scroll chip's own trailing spacing, so this component can no longer clip a
10
- // hint by guessing one cell short - and the ladder that decides which hints
11
- // survive a narrow terminal lives in a pure function with its own tests.
12
- //
13
- // `kitHint` is the `^K switch kit` clause (agent-model-kits §4.7): `App`
14
- // passes `true` always, because the requirement is that `ctrl+G` reveals the
15
- // key - and the user with no kits needs that hint most.
16
- const plan = planHeaderHints({
17
- columns, statusExpanded, overlayOpen, scrolledLines, kitHint, promptsHint,
5
+ import { pickGlyphs } from './glyphs.js';
6
+ import { planHeaderHints, HEADER_HINT_LIMITS, } from './layout/header-hints.js';
7
+ import { planHeaderLeft } from './layout/header-left.js';
8
+ export function Header({ cwd, provider, model, hasKey, variant, theme, caps, columns = 80, panel = 'none', scrolledLines = 0, taskSummary = null, summaryEnabled = true, }) {
9
+ const glyphs = pickGlyphs(caps);
10
+ // THE RIGHT COLUMN FIRST: it is the only clause that may never be dropped, so
11
+ // the left half is planned against what is left after it (defect 12).
12
+ const plan = planHeaderHints({ columns, panel, scrolledLines });
13
+ const leftCells = Math.max(0, columns - plan.cells - HEADER_HINT_LIMITS.separatorCells);
14
+ const left = planHeaderLeft({
15
+ leftCells,
16
+ provider,
17
+ model,
18
+ cwd: basename(cwd) || cwd,
19
+ hasKey,
20
+ summary: taskSummary,
21
+ summaryEnabled,
22
+ ellipsis: glyphs.ellipsis,
18
23
  });
19
- // THE TWO HALVES ARE SEPARATED BY CELLS, NOT BY LUCK (defect 12). The left half
20
- // is `wrap="truncate"`, which draws its ellipsis in the LAST cell it owns; with
21
- // the right half starting in the very next cell a narrow terminal read
22
- // `aragon...Details off` as one string. The blank column comes out of the left
23
- // half's width, so the row still spans exactly `columns`.
24
- return _jsxs(Box, { flexDirection: "row", height: 1, width: columns, flexShrink: 0, overflow: "hidden", children: [_jsx(Box, { width: Math.max(0, columns - plan.cells - HEADER_HINT_LIMITS.separatorCells), flexShrink: 0, overflow: "hidden", children: _jsxs(Text, { wrap: "truncate", children: [_jsx(Logo, { variant: variant === 'mini' ? 'mini' : 'bar', theme: theme, caps: caps }), variant !== 'mini' && _jsxs(_Fragment, { children: [_jsxs(Text, { color: theme.accent, children: [' ', provider, ":", model] }), _jsxs(Text, { color: theme.muted, children: [' ', basename(cwd) || cwd] }), _jsxs(Text, { color: hasKey ? theme.toolDone : theme.toolRunning, children: [' ', hasKey ? theme.symbols.keyOn : theme.symbols.keyOff] })] })] }) }), _jsx(Box, { width: HEADER_HINT_LIMITS.separatorCells, flexShrink: 0 }), _jsx(Box, { width: plan.cells, flexShrink: 0, justifyContent: "flex-end", children: _jsx(Text, { color: theme.muted, wrap: "truncate", children: plan.text }) })] });
24
+ return _jsxs(Box, { flexDirection: "row", height: 1, width: columns, flexShrink: 0, overflow: "hidden", children: [_jsx(Box, { width: leftCells, flexShrink: 0, overflow: "hidden", children: _jsxs(Text, { wrap: "truncate", children: [_jsx(Logo, { variant: variant === 'mini' ? 'mini' : 'bar', theme: theme, caps: caps }), variant !== 'mini' && _jsxs(_Fragment, { children: [left.text.model && _jsxs(Text, { color: theme.accent, children: [' ', left.text.model] }), left.text.cwd && _jsxs(Text, { color: theme.muted, children: [' ', left.text.cwd] }), left.text.summary && _jsxs(Text, { color: theme.muted, children: [' ', glyphs.midDot, ' '] }), left.text.summary && (taskSummary?.tone === 'fallback'
25
+ ? _jsx(Text, { color: theme.muted, dimColor: true, children: left.text.summary })
26
+ : _jsx(Text, { color: theme.primary, children: left.text.summary })), _jsxs(Text, { color: hasKey ? theme.toolDone : theme.toolRunning, children: [' ', hasKey ? theme.symbols.keyOn : theme.symbols.keyOff] })] })] }) }), _jsx(Box, { width: HEADER_HINT_LIMITS.separatorCells, flexShrink: 0 }), _jsx(Box, { width: plan.cells, flexShrink: 0, justifyContent: "flex-end", children: _jsx(Text, { color: theme.muted, wrap: "truncate", children: plan.text }) })] });
25
27
  }
@@ -92,6 +92,7 @@ export interface StatusBarProps {
92
92
  };
93
93
  compactionActive?: {
94
94
  inFlight: boolean;
95
+ selfDisabled?: boolean;
95
96
  };
96
97
  gaugeMarks?: {
97
98
  warn: number;
@@ -39,6 +39,7 @@ export function StatusBar(props) {
39
39
  interruptPhase: props.interruptPhase,
40
40
  waitingForConfirmation: props.waitingForConfirmation, stopping: props.stopping,
41
41
  compacting: props.compactionActive?.inFlight, retryActive: props.retryActive,
42
+ compactionPaused: props.compactionActive?.selfDisabled === true,
42
43
  mode: props.agentMode, pendingMode: props.pendingAgentMode, servicesActive: props.servicesActive,
43
44
  teamActive: props.teamActive, todoActive: props.todoActive,
44
45
  ...(props.acceptanceActive ? { acceptanceActive: props.acceptanceActive } : {}),
@@ -137,7 +137,7 @@ function EntryViewImpl({ entry, prev, expanded, thinkingVisible, reducedMotion,
137
137
  case 'fast':
138
138
  return frame(entry.status === 'failed' ? glyphs.toolError : glyphs.teamAgent, fastCardColor(entry.status, theme), _jsx(FastCard, { reviewIndex: entry.reviewIndex, model: entry.model, status: entry.status, text: entry.text, detail: entry.detail, turn: entry.turn, durationMs: entry.durationMs, live: entry.live, reducedMotion: reducedMotion, theme: theme, caps: caps }));
139
139
  case 'compaction':
140
- return frame(entry.applied || entry.live ? glyphs.compaction : glyphs.toolError, compactionCardColor(entry, theme), _jsx(CompactionCard, { index: entry.index, decision: entry.decision, memoryVersion: entry.memoryVersion, trigger: entry.trigger, mode: entry.mode, applied: entry.applied, reason: entry.reason, messagesBefore: entry.messagesBefore, messagesAfter: entry.messagesAfter, tokensBefore: entry.tokensBefore, tokensAfter: entry.tokensAfter, summary: entry.summary, model: entry.model, durationMs: entry.durationMs, tailRelief: entry.tailRelief,
140
+ return frame(entry.applied || entry.live ? glyphs.compaction : glyphs.toolError, compactionCardColor(entry, theme), _jsx(CompactionCard, { index: entry.index, decision: entry.decision, memoryVersion: entry.memoryVersion, trigger: entry.trigger, mode: entry.mode, applied: entry.applied, reason: entry.reason, messagesBefore: entry.messagesBefore, messagesAfter: entry.messagesAfter, tokensBefore: entry.tokensBefore, tokensAfter: entry.tokensAfter, summary: entry.summary, model: entry.model, durationMs: entry.durationMs, tailRelief: entry.tailRelief, phase: entry.phase, keepRelaxed: entry.keepRelaxed, retainedTurns: entry.retainedTurns, retainedMessages: entry.retainedMessages, keepRecentTurnsUsed: entry.keepRecentTurnsUsed, elidedMessages: entry.elidedMessages,
141
141
  // ONLY WHILE LIVE, and only when the clock has ticked at least once:
142
142
  // `nowSec` is `undefined` until then, and a settled card must keep
143
143
  // rendering `durationMs` rather than a figure that would drift.
@@ -23,7 +23,7 @@
23
23
  import React from 'react';
24
24
  import type { Theme } from '../theme.js';
25
25
  import type { TermCapabilities } from '../capabilities.js';
26
- import type { CompactionDecision, CompactionMode, CompactionUiTrigger } from '../../compaction/types.js';
26
+ import type { CompactionDecision, CompactionMode, CompactionPhase, CompactionUiTrigger } from '../../compaction/types.js';
27
27
  export interface CompactionCardProps {
28
28
  decision?: CompactionDecision;
29
29
  memoryVersion?: 2;
@@ -39,6 +39,16 @@ export interface CompactionCardProps {
39
39
  summary?: string;
40
40
  model: string;
41
41
  durationMs?: number;
42
+ /** The live card's stage, so "compacting" also says WHAT (never-fires C). */
43
+ phase?: CompactionPhase;
44
+ /** Set when this cut existed only because `keepRecentTurns` was relaxed (B). */
45
+ keepRelaxed?: boolean;
46
+ retainedTurns?: number;
47
+ /** Messages the relaxed cut left verbatim, which is not the same as turns. */
48
+ retainedMessages?: number;
49
+ keepRecentTurnsUsed?: number;
50
+ /** Messages the digest left out and therefore cannot be quoted (A1). */
51
+ elidedMessages?: number;
42
52
  /**
43
53
  * Wall-clock since the card opened, LIVE ONLY
44
54
  * (context-auto-compaction-hardening §3.6 / W5).
@@ -28,6 +28,7 @@ import { pickGlyphs } from '../glyphs.js';
28
28
  import { railBorderProps } from '../layout/Gutter.js';
29
29
  import { formatDuration, formatTokens } from '../../agent/usage.js';
30
30
  import { COMPACTION_LIMITS } from '../../compaction/limits.js';
31
+ import { compactionPhaseLabel, compactionReasonLine } from '../../compaction/reason-text.js';
31
32
  /**
32
33
  * The card's colour.
33
34
  *
@@ -56,12 +57,19 @@ function headline(props, arrow, dot) {
56
57
  // second 45 this was the only changing information on the card, and there was
57
58
  // none.
58
59
  const elapsed = props.elapsedMs === undefined ? '' : `${dot}${formatDuration(props.elapsedMs)}`;
60
+ // THE STAGE, NOT JUST "COMPACTING" (never-fires C). Planning and digesting run
61
+ // before any model call, and a user watching a card for twenty seconds cannot
62
+ // tell "thinking hard" from "stuck in a gate" without it.
63
+ const stage = props.phase === undefined ? '' : `${dot}${compactionPhaseLabel(props.phase)}`;
59
64
  return props.model
60
- ? `compacting context #${index}${dot}${props.model}${elapsed}`
61
- : `compacting context #${index}${elapsed}`;
65
+ ? `compacting context #${index}${dot}${props.model}${stage}${elapsed}`
66
+ : `compacting context #${index}${stage}${elapsed}`;
62
67
  }
63
68
  if (!props.applied || props.tokensAfter >= props.tokensBefore) {
64
- const why = props.reason ? `: ${props.reason}` : '';
69
+ // THE REASON IN WORDS (never-fires C). `digest_budget_exceeded: $.digest` was
70
+ // a code the user could not act on; `compactionReasonLine` renders the same
71
+ // fact as a clause plus, where there is one, the remedy.
72
+ const why = props.reason ? `: ${compactionReasonLine(props.reason)}` : '';
65
73
  return `context not compacted #${index}${why}`;
66
74
  }
67
75
  const messages = `${props.messagesBefore} ${arrow} ${props.messagesAfter} messages`;
@@ -87,8 +95,15 @@ function headline(props, arrow, dot) {
87
95
  */
88
96
  function detailLine(props) {
89
97
  if (props.live) {
98
+ // THE ESCAPE, NOT THE STAGE (never-fires C, as corrected in review): the live
99
+ // headline already carries the stage, and a two-line card that names it twice
100
+ // is the one-fact-one-surface rule this file keeps having to restore. With no
101
+ // model resolved there is no call to cancel, so the whole line goes - the same
102
+ // rule the settled branch applies.
90
103
  if (!props.model)
91
104
  return '';
105
+ if (props.phase !== undefined)
106
+ return 'esc to cancel';
92
107
  return `summarizing with ${props.model} - esc to cancel`;
93
108
  }
94
109
  if (!props.applied || props.tokensAfter >= props.tokensBefore)
@@ -104,6 +119,50 @@ function detailLine(props) {
104
119
  const via = props.trigger === 'overflow' ? ' after a context overflow' : '';
105
120
  return `summarized ${dropped} messages with ${props.model}${via}`;
106
121
  }
122
+ /**
123
+ * The relaxation disclosure (never-fires B).
124
+ *
125
+ * `planCompaction`'s second pass trades "the last N user turns are verbatim" for
126
+ * "a cut exists at all". That trade must be readable, because the difference
127
+ * between the two compactions is exactly what the user asked for: the tail is the
128
+ * part they care most about keeping word for word.
129
+ */
130
+ function relaxedLine(props) {
131
+ if (props.live || !props.applied || props.keepRelaxed !== true)
132
+ return '';
133
+ const turns = props.retainedTurns ?? 0;
134
+ const wanted = props.keepRecentTurnsUsed;
135
+ const messages = props.retainedMessages;
136
+ // THE TAIL IS MESSAGES, NOT TURNS (review). The dominant relaxed shape is one
137
+ // prompt plus a hundred tool calls, so `retainedTurns` is 0 while forty messages
138
+ // were kept verbatim: a line reading "kept 0 recent turns" would report a total
139
+ // loss that did not happen. Whenever the count is available it leads, and the
140
+ // turn arithmetic follows as the explanation rather than as the headline.
141
+ if (messages !== undefined && wanted !== undefined) {
142
+ return `kept the last ${messages} messages - ${turns} of the ${wanted} requested turns, ` +
143
+ 'since this run had too few user turns';
144
+ }
145
+ if (messages !== undefined)
146
+ return `kept the last ${messages} messages verbatim`;
147
+ return wanted === undefined
148
+ ? `kept the last ${turns} turns - this run had too few user turns to keep more`
149
+ : `kept ${turns} of the ${wanted} requested turns - this run had too few user turns`;
150
+ }
151
+ /**
152
+ * The digest-elision disclosure (never-fires A1).
153
+ *
154
+ * The digest never rejects for length any more; it keeps the first and last
155
+ * entries verbatim and elides the middle. The elided messages cannot be quoted by
156
+ * the summarizer, so the card says so rather than presenting a summary as if it
157
+ * had seen everything.
158
+ */
159
+ function elidedLine(props) {
160
+ const n = props.elidedMessages ?? 0;
161
+ if (props.live || n <= 0)
162
+ return '';
163
+ return `elided ${n} ${n === 1 ? 'message' : 'messages'} from the summary input - ` +
164
+ 'the middle is summarized without them';
165
+ }
107
166
  /**
108
167
  * The relief disclosure, on a settled card that ALSO summarized.
109
168
  *
@@ -139,6 +198,8 @@ function CompactionCardImpl(props) {
139
198
  const head = headline(props, glyphs.arrowRight, dot);
140
199
  const detail = detailLine(props);
141
200
  const relief = reliefLine(props);
201
+ const relaxed = relaxedLine(props);
202
+ const elided = elidedLine(props);
142
203
  const showMemory = props.memoryVersion !== 2 || expanded === true;
143
204
  const rows = summary && showMemory ? summaryRows(summary, expanded === true) : [];
144
205
  const totalRows = summary ? summary.split('\n').filter((l) => l.trim().length > 0).length : 0;
@@ -150,7 +211,7 @@ function CompactionCardImpl(props) {
150
211
  // the whole run, losing the status colour along with the motion.
151
212
  const animate = live && reducedMotion !== true && caps.unicode;
152
213
  return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: theme.accent, bold: true, children: props.trigger === 'manual' ? 'Manual compaction' : props.trigger === 'overflow'
153
- ? 'Overflow: threshold reached' : 'Automatic: threshold reached' }), _jsxs(Text, { color: live ? color : theme.muted, wrap: caps.unicode ? 'truncate' : 'wrap', children: [animate ? (_jsxs(_Fragment, { children: [_jsx(Spinner, { type: "dots" }), ' '] })) : live ? (`${glyphs.spinnerStill} `) : (''), head] })] }), props.memoryVersion === 2 && (_jsxs(Text, { color: theme.muted, children: ["Task memory v2", expanded ? '' : ' (ctrl+o to expand)'] })), detail.length > 0 && (_jsx(Box, { flexDirection: "column", flexShrink: 0, paddingLeft: 1, children: _jsx(Text, { color: color, wrap: caps.unicode ? 'truncate' : 'wrap', children: detail }) })), relief.length > 0 && (_jsx(Box, { flexDirection: "column", flexShrink: 0, paddingLeft: 1, children: _jsx(Text, { color: theme.noticeWarn, children: relief }) })), rows.length > 0 && (_jsxs(Box, { flexDirection: "column", flexShrink: 0, paddingLeft: 1, ...railBorderProps(glyphs.railVertical, theme.border), children: [rows.map((line, index) => (_jsx(Text, { color: theme.primary, children: line }, index))), hidden > 0 && (_jsxs(Text, { color: theme.muted, children: [glyphs.railEnd, " +", hidden, " more lines (ctrl+o to expand)"] })), hidden === 0 && expanded === true && totalRows > COMPACTION_LIMITS.cardTextRows && (_jsxs(Text, { color: theme.muted, children: [glyphs.railEnd, " ctrl+o to collapse"] }))] }))] }));
214
+ ? 'Overflow: threshold reached' : 'Automatic: threshold reached' }), _jsxs(Text, { color: live ? color : theme.muted, wrap: caps.unicode ? 'truncate' : 'wrap', children: [animate ? (_jsxs(_Fragment, { children: [_jsx(Spinner, { type: "dots" }), ' '] })) : live ? (`${glyphs.spinnerStill} `) : (''), head] })] }), props.memoryVersion === 2 && (_jsxs(Text, { color: theme.muted, children: ["Task memory v2", expanded ? '' : ' (ctrl+o to expand)'] })), detail.length > 0 && (_jsx(Box, { flexDirection: "column", flexShrink: 0, paddingLeft: 1, children: _jsx(Text, { color: color, wrap: caps.unicode ? 'truncate' : 'wrap', children: detail }) })), relief.length > 0 && (_jsx(Box, { flexDirection: "column", flexShrink: 0, paddingLeft: 1, children: _jsx(Text, { color: theme.noticeWarn, children: relief }) })), relaxed.length > 0 && (_jsx(Box, { flexDirection: "column", flexShrink: 0, paddingLeft: 1, children: _jsx(Text, { color: theme.muted, children: relaxed }) })), elided.length > 0 && (_jsx(Box, { flexDirection: "column", flexShrink: 0, paddingLeft: 1, children: _jsx(Text, { color: theme.muted, children: elided }) })), rows.length > 0 && (_jsxs(Box, { flexDirection: "column", flexShrink: 0, paddingLeft: 1, ...railBorderProps(glyphs.railVertical, theme.border), children: [rows.map((line, index) => (_jsx(Text, { color: theme.primary, children: line }, index))), hidden > 0 && (_jsxs(Text, { color: theme.muted, children: [glyphs.railEnd, " +", hidden, " more lines (ctrl+o to expand)"] })), hidden === 0 && expanded === true && totalRows > COMPACTION_LIMITS.cardTextRows && (_jsxs(Text, { color: theme.muted, children: [glyphs.railEnd, " ctrl+o to collapse"] }))] }))] }));
154
215
  }
155
216
  /**
156
217
  * `React.memo` with the DEFAULT comparator (tui-render-performance L2 / R3).
@@ -29,6 +29,12 @@ export function actionCandidates(input) {
29
29
  if (input.overlay) {
30
30
  if (input.overlay === 'confirm')
31
31
  return [copy.approveTool, copy.rejectTool];
32
+ // THE DRAWER SELECTS, IT DOES NOT SCROLL (tui-shortcut-drawer-and-task-summary
33
+ // §5). It is a list with a cursor, so the generic `copy.scroll` fallback would
34
+ // describe the wrong interaction - and the drawer's own footer already names
35
+ // every key, so one clause is the whole line.
36
+ if (input.overlay === 'shortcuts')
37
+ return [copy.select];
32
38
  const selects = ['question', 'model', 'session', 'settings'].includes(input.overlay);
33
39
  return [selects ? copy.select : copy.scroll,
34
40
  ...(selects ? [input.overlay === 'settings' ? copy.saveSettings : copy.confirm] : [])];
@@ -10,6 +10,24 @@ interface OverlayFrameBase {
10
10
  cols?: number;
11
11
  theme: Theme;
12
12
  caps: TermCapabilities;
13
+ /**
14
+ * Where the frame sits on the row when `width` is given.
15
+ *
16
+ * DEFAULT `'left'` AND NO WRAPPER BOX: with neither new prop set, the render
17
+ * tree is byte-identical to the pre-`shortcut-drawer` one, which is what keeps
18
+ * the eight existing overlays' layout assertions green (`R-16`).
19
+ */
20
+ align?: 'left' | 'right';
21
+ /**
22
+ * An explicit width for a frame that is NOT the full terminal width - the
23
+ * shortcut drawer's right-hand bar.
24
+ *
25
+ * THE FRAME STILL DECIDES ITS OWN FORM from `cols` (`BORDERLESS_COLS`), so the
26
+ * caller passes the TERMINAL width here and the bar's width separately. Passing
27
+ * the bar's width as `cols` would put a 46-cell drawer on a 200-column terminal
28
+ * into the rail form for ever.
29
+ */
30
+ width?: number;
13
31
  }
14
32
  interface OverlayFrameControlled extends OverlayFrameBase {
15
33
  /** One element per row, each already `wrap="truncate"`. */
@@ -60,14 +60,29 @@ export function OverlayFrame(props) {
60
60
  onScrollClamp?.(overflowOffset);
61
61
  }, [overflowOffset, onScrollClamp]);
62
62
  const inner = (_jsxs(_Fragment, { children: [_jsxs(Box, { flexDirection: "row", height: 1, flexShrink: 0, children: [_jsx(Box, { flexGrow: 1, flexShrink: 1, children: _jsx(Text, { wrap: "truncate", color: theme.primary, bold: true, children: title }) }), position && _jsx(Box, { width: stringWidth(position), flexShrink: 0, children: _jsx(Text, { color: theme.muted, children: position }) })] }), _jsx(Box, { flexDirection: "column", flexShrink: 0, height: unbounded ? undefined : Math.max(1, maxRows - (compact ? 2 : bordered ? 5 : 3)), overflowY: "hidden", children: body }), _jsx(Text, { wrap: "truncate", color: theme.hintFg ?? theme.muted, children: hint })] }));
63
- if (compact)
64
- return _jsx(Box, { width: cols, height: Math.max(0, maxRows), flexDirection: "column", flexShrink: 0, overflowY: "hidden", children: inner });
63
+ // THE NEW PROPS ARE OPTIONAL AND THEIR DEFAULT IS TODAY'S BEHAVIOUR (R-16): the
64
+ // three branches below return exactly what they always returned unless the
65
+ // caller asked for a width or a right-hand placement. When it did, the SAME box
66
+ // is built and then placed inside one row-wide wrapper, which is the only way
67
+ // Ink can push a fixed-width child to the right edge (a `marginLeft: 'auto'`
68
+ // does not exist here).
69
+ const width = props.width;
70
+ const align = props.align ?? 'left';
71
+ const placed = (node) => {
72
+ if (width === undefined && align === 'left')
73
+ return node;
74
+ return (_jsx(Box, { width: cols, flexDirection: "row", flexShrink: 0, justifyContent: align === 'right' ? 'flex-end' : 'flex-start', children: node }));
75
+ };
76
+ const boxWidth = width === undefined ? {} : { width };
77
+ if (compact) {
78
+ return placed(_jsx(Box, { ...boxWidth, width: width ?? cols, height: Math.max(0, maxRows), flexDirection: "column", flexShrink: 0, overflowY: "hidden", children: inner }));
79
+ }
65
80
  if (!bordered) {
66
81
  // Narrow terminals trade the box for a rail: same grouping, 2 fewer rows
67
82
  // and 4 fewer columns (§5.2).
68
- return (_jsx(Box, { flexDirection: "column", flexShrink: 0, marginTop: 1, children: _jsx(Box, { flexDirection: "column", flexShrink: 0, borderStyle: railBorderStyle, borderTop: false, borderBottom: false, borderRight: false, borderColor: theme.primary, paddingLeft: 1, children: inner }) }));
83
+ return placed(_jsx(Box, { flexDirection: "column", flexShrink: 0, marginTop: 1, ...boxWidth, children: _jsx(Box, { flexDirection: "column", flexShrink: 0, borderStyle: railBorderStyle, borderTop: false, borderBottom: false, borderRight: false, borderColor: theme.primary, paddingLeft: 1, children: inner }) }));
69
84
  }
70
- return (_jsx(Box, { flexDirection: "column", flexShrink: 0, borderStyle: glyphs.boxStyle, borderColor: theme.primary, paddingX: 1, marginTop: 1, children: inner }));
85
+ return placed(_jsx(Box, { flexDirection: "column", flexShrink: 0, borderStyle: glyphs.boxStyle, borderColor: theme.primary, paddingX: 1, marginTop: 1, ...boxWidth, children: inner }));
71
86
  }
72
87
  /**
73
88
  * Rows one PgUp/PgDn - and one Shift+wheel notch - turns inside a controlled
@@ -1,25 +1,21 @@
1
1
  /**
2
- * The right-hand hint budget for the header row (tui-branch-terminal-shortcut 3.8).
2
+ * The right-hand hint budget for the header row
3
+ * (tui-shortcut-drawer-and-task-summary §4.2).
3
4
  *
4
5
  * ASCII ONLY - `src/ui/**` is inside `glyphs.test.ts::inScope`.
5
6
  *
6
- * WHY THIS IS A PLANNER AND NOT ANOTHER TERNARY IN THE JSX (R-0-4): the header's
7
- * right side used to be one hand-computed width constant (`detailCells = indexHint
8
- * ? 32 : 18`). Adding a third affordance to that arithmetic produces either a
9
- * clipped hint or a pushed-out word mark on a 72-column terminal, and NO TEST
10
- * WOULD SEE IT - the only thing lost is trailing characters. `ui/layout/session-chip.ts`
11
- * already set the precedent for the same problem: the chip has to participate in
12
- * one width budget rather than be appended and hoped for.
7
+ * THIS FILE USED TO BE A SIX-RUNG LADDER. `^L branch`, `^P prompts`, `^I Index`,
8
+ * `^K switch kit`, `^G more` and the expanded variants competed for a width
9
+ * budget, so what a user saw was not "my shortcuts" but "which clauses happen to
10
+ * fit at this width" - and on an 80-column terminal that was three of six. The
11
+ * requirement replaces the whole idea with ONE entry point (`ctrl+G more`) whose
12
+ * panel lists every shortcut, so the ladder is gone and what remains is a
13
+ * three-state single clause.
13
14
  *
14
- * THE RULE IS ONE COMPARISON. `budget = columns - minLeftCells`, then take the
15
- * first ladder rung whose `cells` fits. There is no second magic constant, and the
16
- * threshold table in the spec is what that comparison WORKS OUT TO, not an extra
17
- * branch.
18
- *
19
- * WHY `minLeftCells` IS 24: the word mark is 13 cells and `provider:model` + cwd +
20
- * the key symbol need at least 11 more to stay readable. It is lower than
21
- * `status-layout`'s 34 because the top-left half has a brand to lose while the
22
- * bottom-left half has a phase that must not disappear.
15
+ * WHY A PLANNER AND NOT A TERNARY IN THE JSX (R-0-4, unchanged): the header's
16
+ * right side was once one hand-computed width constant, and every later addition
17
+ * to it produced either a clipped hint or a pushed-out word mark with no test
18
+ * able to see it - the only thing lost is trailing characters.
23
19
  */
24
20
  /**
25
21
  * Bounds for the hint budget.
@@ -27,71 +23,68 @@
27
23
  * WHY THEY LIVE IN THIS FILE (R-21, accepted): `CLAUDE.md`'s general rule is that
28
24
  * structural bounds live in the subsystem's `limits.ts`, and `ui/layout/` has none
29
25
  * - `budget.ts` keeps `HINT_MIN_ROWS` beside its own module for the same reason.
30
- * This module has exactly one consumer. If `ui/layout/` ever grows a real
31
- * `limits.ts`, these three constants should move there together; this comment is
32
- * that future migration's note.
33
26
  */
34
27
  export declare const HEADER_HINT_LIMITS: {
35
- /** Cells the LEFT half of the header must keep (`AragonAgent` is 13 of them). */
28
+ /**
29
+ * Cells the LEFT half of the header must keep.
30
+ *
31
+ * 24 = the 13-cell word mark plus at least 11 for `provider:model`, the
32
+ * directory and the key symbol. Lower than `status-layout`'s 34 because the top
33
+ * row has a brand to lose while the bottom row has a phase that must not
34
+ * disappear.
35
+ */
36
36
  readonly minLeftCells: 24;
37
- /** Fixed spacing between two hint clauses. */
38
- readonly gapCells: 2;
39
37
  /**
40
38
  * Blank cells between the header's left half and this hint column (defect 12).
41
39
  *
42
- * A SEPARATE CONSTANT FROM `gapCells`. That one spaces two hint clauses INSIDE
43
- * the right column (`buildPlan` below); this one separates the session
44
- * description from that column. One constant carrying two meanings is how a
45
- * later change to clause spacing silently re-lays-out the whole row.
40
+ * A SEPARATE CONCEPT FROM CLAUSE SPACING: this one separates the two HALVES of
41
+ * the row, and it is spent out of the same `columns` budget so the row still
42
+ * spans exactly the terminal width.
46
43
  */
47
44
  readonly separatorCells: 2;
48
45
  };
49
- export type HeaderHintId = 'branch' | 'prompts' | 'index' | 'kit' | 'detail';
50
- export interface HeaderHintClause {
51
- id: HeaderHintId;
52
- text: string;
53
- }
46
+ /**
47
+ * What the right-hand column says. THREE STATES, NO LADDER.
48
+ *
49
+ * `none` - no overlay: `^G more`, the only shortcut hint the header teaches.
50
+ * `shortcuts` - this panel is open: `^G close`, the key that closes it.
51
+ * `other` - some other overlay owns the keyboard: `Esc close`, which is true
52
+ * for every overlay and is the only key that works in all of them.
53
+ */
54
+ export type HeaderPanelState = 'none' | 'shortcuts' | 'other';
54
55
  export interface HeaderHintPlan {
55
- /** Clauses left to right (including the overlay's `Details on/off` replacement). */
56
- clauses: HeaderHintClause[];
57
56
  /** The whole right-hand string, scroll chip included with its trailing spacing. */
58
57
  text: string;
59
- /** Total cells of the right column; `Header` uses it to size the left one. */
58
+ /** Total cells of the right column; `Header` sizes the left one from it. */
60
59
  cells: number;
61
60
  /** Cells taken by the scroll chip (0 when there is none). */
62
61
  scrollCells: number;
63
- /** True while an overlay owns the keyboard: only the detail word remains. */
64
- detailsOnly: boolean;
65
62
  }
63
+ /** The verbatim text of each state. Exported so a test can name it instead of guessing. */
64
+ export declare const HEADER_HINTS: Readonly<Record<HeaderPanelState, string>>;
66
65
  /**
67
- * Plan the right-hand hints.
66
+ * Plan the right-hand hint.
68
67
  *
69
- * The inputs are the props `Header` receives plus the two switches that decide which
70
- * clauses exist at all - the call site does not change, only the arithmetic behind it
71
- * (spec 3.11 step 5).
68
+ * NO WIDTH BRANCHING: the clause is always rendered. When the row cannot hold it
69
+ * the LEFT half is what shrinks (to zero) and `overflow="hidden"` keeps that from
70
+ * spilling - the pre-feature behaviour, which is what makes the header safe at 20
71
+ * columns without a ladder. `headerHintFits` below exists only so the threshold
72
+ * is documented and testable, not so the renderer can branch on it.
72
73
  */
73
74
  export declare function planHeaderHints(input: {
74
75
  columns: number;
75
- statusExpanded: boolean;
76
- overlayOpen: boolean;
76
+ panel: HeaderPanelState;
77
77
  scrolledLines: number;
78
- /**
79
- * Whether the `^K switch kit` clause exists at all (default `true`).
80
- *
81
- * `App` ALWAYS PASSES TRUE, including for a user with no kits: the whole point
82
- * of the requirement is discoverability of `ctrl+K`, and an empty library is
83
- * exactly the state that needs it (the picker's own `No kits yet` row tells
84
- * them where to create one). `false` exists for tests and for the future
85
- * switch, and it degrades the six expanded rungs back into today's four.
86
- */
87
- kitHint?: boolean;
88
- /**
89
- * Whether the `^P prompts` clause exists at all (default `true`).
90
- *
91
- * `false` is the `prompts.hint` config key and the only reason the ladder is
92
- * parameterised: a user who turned the header hint off must get exactly the row
93
- * they had before this feature, which is what the unfiltered ladder below
94
- * degenerates into.
95
- */
96
- promptsHint?: boolean;
97
78
  }): HeaderHintPlan;
79
+ /**
80
+ * Whether the row can hold the hint column without squeezing the left half below
81
+ * `minLeftCells`.
82
+ *
83
+ * A FORMULA, NOT A TABLE OF MAGIC NUMBERS: `columns - minLeftCells -
84
+ * separatorCells >= scrollCells + bodyCells`. Substituting the three states gives
85
+ * 32 / 34 / 35 columns with no chip, plus up to 8 more for a saturated `^9999+ `.
86
+ * v1 claimed "any width >= 35", which forgot the chip entirely.
87
+ */
88
+ export declare function headerHintFits(columns: number, panel: HeaderPanelState): boolean;
89
+ /** The narrowest width at which each state fits with no scroll chip. */
90
+ export declare function headerHintThreshold(panel: HeaderPanelState): number;