@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
@@ -1,8 +1,15 @@
1
1
  /**
2
2
  * Status bar (spec §4.8). Width-aware, borderless, EXACTLY one row: a left
3
3
  * cluster (run status, model, thinking level) and a right cluster (context
4
- * gauge, tokens, cost, tokens/sec + elapsed while running, off-bottom `↑N`).
5
- * Clusters drop out gracefully on narrow terminals.
4
+ * gauge, occupancy pair, session total, cost, tokens/sec + elapsed while
5
+ * running, off-bottom `↑N`). Clusters drop out gracefully on narrow terminals.
6
+ *
7
+ * THE RIGHT CLUSTER'S WIDTH LADDER (context-usage-gauge-accuracy §3.6):
8
+ *
9
+ * >= 96 [####....] 43% 86.0k/200.0k total 1.2M^ 48.0k v $3.21 12 tok/s 1m02s
10
+ * 72-95 [####....] 43% 86.0k/200.0k $3.21
11
+ * 60-71 [####....] 43% $3.21
12
+ * < 60 43% $3.21
6
13
  *
7
14
  * Dropping the round border here buys back 3 rows of a 24-row terminal — an
8
15
  * eighth of the screen was being spent framing a single line of text.
@@ -11,14 +18,23 @@ import React from 'react';
11
18
  import type { Theme } from './theme.js';
12
19
  import type { TermCapabilities } from './capabilities.js';
13
20
  import type { UsageTotal } from '../agent/reducer.js';
21
+ import type { ContextUsageSnapshot } from '../compaction/types.js';
14
22
  import { type AgentMode } from '../agent/agent-mode.js';
15
23
  interface StatusBarProps {
16
24
  model: string;
17
25
  provider: string;
18
26
  usageTotal: UsageTotal;
19
- contextTokens: number;
20
- contextWindow: number;
21
- contextWindowKnown: boolean;
27
+ /**
28
+ * Context occupancy, as one object (context-usage-gauge-accuracy §4.3).
29
+ *
30
+ * IT REPLACED FOUR PARALLEL PROPS - `contextTokens`, `contextWindow`,
31
+ * `contextWindowKnown`, `contextEstimated`. Four props describing one reading
32
+ * are four chances for a caller to update three of them, which is the props-
33
+ * layer form of the "many writers" defect this feature removed from the
34
+ * reducer. `usageTotal` stays separate because it genuinely IS another
35
+ * quantity: session spend, not occupancy.
36
+ */
37
+ context: ContextUsageSnapshot;
22
38
  status: 'idle' | 'running';
23
39
  elapsedMs: number;
24
40
  thinkingLevel: string;
@@ -88,6 +104,25 @@ interface StatusBarProps {
88
104
  done: number;
89
105
  total: number;
90
106
  };
107
+ /**
108
+ * Live background services (background-service-supervision §3.8), or absent
109
+ * when none are running — an ordinary session's bar is then unchanged.
110
+ *
111
+ * LEFT CLUSTER, after the todo chip, and it DEGRADES rather than hiding below
112
+ * `PROC_LIMITS.statusCompactCols`. That is the `todoActive` / `teamActive`
113
+ * ladder rather than `compactionActive`'s hide-below-threshold one, and the
114
+ * choice is deliberate: `compact` and `fast` describe something the CLI is
115
+ * doing to itself, whereas this is a PROCESS ON THE USER'S MACHINE HOLDING A
116
+ * PORT. A narrow terminal is a reason to shorten it, never a reason to stop
117
+ * saying it.
118
+ *
119
+ * `live` counts `starting | ready | running`. The transcript records events
120
+ * about a service (D-11); this is where "what is running right now" lives,
121
+ * alongside `/bg` and `bash_output`.
122
+ */
123
+ servicesActive?: {
124
+ live: number;
125
+ };
91
126
  /**
92
127
  * Live retry state (llm-api-retry-backoff §6.6), or absent when nothing is
93
128
  * retrying — an ordinary session's bar is then unchanged.
@@ -168,16 +203,6 @@ interface StatusBarProps {
168
203
  warn: number;
169
204
  high: number;
170
205
  };
171
- /**
172
- * Whether `contextTokens` is a DERIVED figure rather than a measured one.
173
- *
174
- * It WIDENS THE EXISTING `~`, it does not add a second marker. Today that
175
- * prefix means "the window is a guess" (`contextWindowKnown`); with compaction
176
- * it also has to mean "the occupancy is a guess", because the bar is written
177
- * from an estimate for one turn after every compaction so it can fall
178
- * immediately (§6.2 / AC-5).
179
- */
180
- contextEstimated?: boolean;
181
206
  }
182
207
  export declare function StatusBar(props: StatusBarProps): React.ReactElement;
183
208
  export {};
@@ -1,7 +1,7 @@
1
1
  import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Box, Text, useStdout } from 'ink';
3
3
  import { pickGlyphs } from './glyphs.js';
4
- import { formatCost, formatTokens, formatDuration } from '../agent/usage.js';
4
+ import { formatCost, formatTokens, formatDuration, promptTokensOf } from '../agent/usage.js';
5
5
  import { buildGauge } from './gauge.js';
6
6
  import { MODE_LABEL } from '../agent/agent-mode.js';
7
7
  import { formatRetryChip } from '../agent/retry-view.js';
@@ -9,8 +9,29 @@ import { FAST_LIMITS } from '../fast/limits.js';
9
9
  import { COMPACTION_LIMITS } from '../compaction/limits.js';
10
10
  import { TEAM_LIMITS } from '../team/limits.js';
11
11
  import { TODO_LIMITS } from '../todo/limits.js';
12
+ import { PROC_LIMITS } from '../proc/limits.js';
12
13
  /** Below this the `eco` chip costs the context gauge more than it is worth. */
13
14
  const ECO_MIN_COLS = 72;
15
+ /**
16
+ * Where `86k/200k` starts being shown (context-usage-gauge-accuracy §3.6 / W5).
17
+ *
18
+ * IT TAKES THE COLUMNS THE SESSION TOTAL USED TO HAVE, and the ordering is the
19
+ * point rather than a layout accident (D-6). "How full am I, out of how much" is
20
+ * the question a user asks the gauge; "what has this session cost in tokens" is
21
+ * a different one, and putting the second in front of the first spends the
22
+ * scarce columns on the number the user is more likely to MISREAD - the total
23
+ * looks like an occupancy until you notice it exceeds the window.
24
+ */
25
+ const ABSOLUTE_PAIR_MIN_COLS = 72;
26
+ /**
27
+ * Where the session total returns, now prefixed.
28
+ *
29
+ * `total ` IS AN ASCII WORD, NOT A SIGMA. `ui/**` is inside the glyph scanner's
30
+ * scope, so a new symbol would need an entry in `glyphs.ts` with an ASCII
31
+ * fallback for a legacy `cmd.exe` - a real cost for a three-character prefix
32
+ * that reads worse.
33
+ */
34
+ const SESSION_TOTAL_MIN_COLS = 96;
14
35
  /**
15
36
  * I-5 — DO NOT "CLEAN THIS UP". Ink short-circuits identical output at TWO
16
37
  * gates (`ink.js:132` and `log-update.js:13`), so a fixed frame whose React
@@ -30,23 +51,42 @@ function redrawChar(nonce) {
30
51
  return nonce % 2 === 0 ? ' ' : NBSP;
31
52
  }
32
53
  export function StatusBar(props) {
33
- const { model, provider, usageTotal, contextTokens, contextWindow, contextWindowKnown, status, elapsedMs, thinkingLevel, tokPerSec, theme, caps, scrolledLines = 0, redrawNonce = 0, agentMode = 'build', pendingAgentMode = null, teamActive, todoActive, retryActive, ecoRung = 0, fastActive, compactionActive, gaugeMarks, contextEstimated = false, } = props;
54
+ const { model, provider, usageTotal, context, status, elapsedMs, thinkingLevel, tokPerSec, theme, caps, scrolledLines = 0, redrawNonce = 0, agentMode = 'build', pendingAgentMode = null, teamActive, todoActive, servicesActive, retryActive, ecoRung = 0, fastActive, compactionActive, gaugeMarks, } = props;
34
55
  const { stdout } = useStdout();
35
56
  const cols = stdout?.columns ?? 80;
36
57
  const running = status === 'running';
37
58
  const glyphs = pickGlyphs(caps);
38
- const pct = contextWindow > 0 ? Math.min(100, Math.round((contextTokens / contextWindow) * 100)) : 0;
59
+ // THE PERCENTAGE IS NOT RECOMPUTED HERE ANY MORE. It arrives already divided
60
+ // and clamped by the same code the compaction trigger reads, which is the
61
+ // whole of AC-2: a user watching 78 % and a `/compact status` reporting 91 %
62
+ // would rightly conclude one of them is lying.
63
+ const pct = context.pct;
39
64
  // `gaugeMarks` is spread rather than passed as `undefined`, so a session with
40
65
  // compaction off takes `buildGauge`'s own default and the bar is byte-identical
41
66
  // to the pre-feature build.
42
67
  const gauge = gaugeMarks
43
68
  ? buildGauge(pct, cols < 72 ? 8 : 12, theme, caps, gaugeMarks)
44
69
  : buildGauge(pct, cols < 72 ? 8 : 12, theme, caps);
45
- // ONE `~` FOR TWO KINDS OF APPROXIMATION (§6.2). A second marker would be a
46
- // second thing to explain for a distinction the user does not act on
47
- // differently: either way the number is a guess.
48
- const pctLabel = `${contextWindowKnown && !contextEstimated ? '' : '~'}${gauge.pct}%`;
49
- const tokens = `${formatTokens(usageTotal.inputTokens)}${glyphs.arrowUp} ${formatTokens(usageTotal.outputTokens)}${glyphs.arrowDown}`;
70
+ // `~` BELONGS TO THE NUMERATOR, `?` TO THE DENOMINATOR (P2-7). One glyph used
71
+ // to carry three meanings - the window is a guess, the occupancy is a guess,
72
+ // the occupancy contains an appended-message estimate - and a user could not
73
+ // tell from it which half to distrust. The `~` keeps every case it had: an
74
+ // unknown window still produces it, so a session that never touches the new
75
+ // absolute pair sees the identical string it saw before.
76
+ const occupancyApproximate = context.source === 'estimate' || context.deltaTokens > 0;
77
+ const pctLabel = `${context.windowKnown && !occupancyApproximate ? '' : '~'}${gauge.pct}%`;
78
+ // `?` ON THE WINDOW, AND ONLY ON THE NEW READOUT. `windowKnown` is false
79
+ // exactly when `buildRuntimeModel` invented the 128k placeholder, and saying
80
+ // `128k?` is the difference between a denominator the user can act on and one
81
+ // they will assume came from their model card.
82
+ const windowLabel = `${formatTokens(context.window)}${context.windowKnown ? '' : '?'}`;
83
+ const absolutePair = `${formatTokens(context.occupied)}/${windowLabel}`;
84
+ // THE PROMPT-SIDE TOTAL, NOT `inputTokens` (P1-3 / AC-11). Anthropic reports
85
+ // `input_tokens` EXCLUDING cached tokens, so the old expression under-reported
86
+ // for anyone behind a caching gateway - on the same row as a `$` that has
87
+ // always priced the cache. Any session with cache hits sees this number grow,
88
+ // and that is the fix rather than a regression.
89
+ const tokens = `total ${formatTokens(promptTokensOf(usageTotal))}${glyphs.arrowUp} ${formatTokens(usageTotal.outputTokens)}${glyphs.arrowDown}`;
50
90
  const statusGlyph = running ? theme.symbols.toolRunning : theme.symbols.toolPending;
51
91
  // Nothing at all in build mode with nothing pending: the default posture is
52
92
  // not news, and a permanent word here would be furniture.
@@ -66,5 +106,7 @@ export function StatusBar(props) {
66
106
  ? `agents ${teamActive.running}/${teamActive.total}`
67
107
  : `[${teamActive.running}]`] })), retryActive && (_jsxs(Text, { color: theme.noticeWarn, bold: true, children: [' ', formatRetryChip(retryActive, cols)] })), todoActive && (_jsxs(Text, { color: theme.accent, children: [' ', cols >= TODO_LIMITS.statusCompactCols
68
108
  ? `todo ${todoActive.done}/${todoActive.total}`
69
- : `[${todoActive.done}/${todoActive.total}]`] })), cols >= 60 && (_jsxs(Text, { color: theme.muted, children: [' ', provider, ":", model] })), thinkingLevel !== 'off' && cols >= 72 && (_jsxs(Text, { color: theme.accent, children: [" think:", thinkingLevel] }))] }), _jsxs(Box, { flexDirection: "row", children: [compactionActive && cols >= COMPACTION_LIMITS.statusCompactCols && (_jsxs(Text, { color: compactionActive.inFlight ? theme.accent : theme.muted, children: [compactionActive.inFlight ? 'compacting' : 'compact', ' '] })), fastActive && cols >= FAST_LIMITS.statusCompactCols && (_jsxs(Text, { color: theme.muted, children: ["fast", fastActive.inFlight ? '*' : '', ' '] })), ecoRung > 0 && cols >= ECO_MIN_COLS && (_jsxs(Text, { color: theme.muted, children: ["eco", ' '] })), scrolledLines > 0 && (_jsxs(Text, { color: theme.noticeWarn, children: [glyphs.arrowUp, scrolledLines, ' '] })), cols >= 60 && (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: "[" }), _jsx(Text, { color: gauge.fillColor, children: gauge.filled }), _jsx(Text, { color: gauge.trackColor, children: gauge.empty }), _jsx(Text, { color: theme.muted, children: "] " })] })), _jsx(Text, { color: theme.muted, children: pctLabel }), cols >= 72 && _jsxs(Text, { color: theme.muted, children: [" ", tokens] }), _jsxs(Text, { color: theme.muted, children: [" ", formatCost(usageTotal.costUsd)] }), running && (_jsxs(Text, { color: theme.muted, children: [tokPerSec > 0 && cols >= 72 ? ` ${tokPerSec} tok/s` : '', " ", formatDuration(elapsedMs)] }))] })] }));
109
+ : `[${todoActive.done}/${todoActive.total}]`] })), servicesActive && (_jsxs(Text, { color: theme.accent, children: [' ', cols >= PROC_LIMITS.statusCompactCols
110
+ ? `svc ${servicesActive.live}`
111
+ : `[${servicesActive.live}]`] })), cols >= 60 && (_jsxs(Text, { color: theme.muted, children: [' ', provider, ":", model] })), thinkingLevel !== 'off' && cols >= 72 && (_jsxs(Text, { color: theme.accent, children: [" think:", thinkingLevel] }))] }), _jsxs(Box, { flexDirection: "row", children: [compactionActive && cols >= COMPACTION_LIMITS.statusCompactCols && (_jsxs(Text, { color: compactionActive.inFlight ? theme.accent : theme.muted, children: [compactionActive.inFlight ? 'compacting' : 'compact', ' '] })), fastActive && cols >= FAST_LIMITS.statusCompactCols && (_jsxs(Text, { color: theme.muted, children: ["fast", fastActive.inFlight ? '*' : '', ' '] })), ecoRung > 0 && cols >= ECO_MIN_COLS && (_jsxs(Text, { color: theme.muted, children: ["eco", ' '] })), scrolledLines > 0 && (_jsxs(Text, { color: theme.noticeWarn, children: [glyphs.arrowUp, scrolledLines, ' '] })), cols >= 60 && (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: "[" }), _jsx(Text, { color: gauge.fillColor, children: gauge.filled }), _jsx(Text, { color: gauge.trackColor, children: gauge.empty }), _jsx(Text, { color: theme.muted, children: "] " })] })), _jsx(Text, { color: theme.muted, children: pctLabel }), cols >= ABSOLUTE_PAIR_MIN_COLS && (_jsxs(Text, { color: theme.muted, children: [" ", absolutePair] })), cols >= SESSION_TOTAL_MIN_COLS && _jsxs(Text, { color: theme.muted, children: [" ", tokens] }), _jsxs(Text, { color: theme.muted, children: [" ", formatCost(usageTotal.costUsd)] }), running && (_jsxs(Text, { color: theme.muted, children: [tokPerSec > 0 && cols >= 72 ? ` ${tokPerSec} tok/s` : '', " ", formatDuration(elapsedMs)] }))] })] }));
70
112
  }
@@ -19,6 +19,7 @@
19
19
  import React from 'react';
20
20
  import type { Theme } from './theme.js';
21
21
  import type { TermCapabilities } from './capabilities.js';
22
+ import { type TeamPanelLayout } from './layout/team-panel.js';
22
23
  import type { SubagentRun, TeamSnapshot } from '../team/types.js';
23
24
  export interface TeamPanelProps {
24
25
  snapshot: TeamSnapshot;
@@ -35,6 +36,7 @@ export interface TeamPanelProps {
35
36
  caps: TermCapabilities;
36
37
  /** Injectable clock so the panel test does not depend on wall time. */
37
38
  now?: number;
39
+ layout?: TeamPanelLayout;
38
40
  }
39
41
  export declare function activityBudget(cols: number): number;
40
42
  /**
@@ -55,4 +57,4 @@ export declare function activityBudget(cols: number): number;
55
57
  * columns and break at 80.
56
58
  */
57
59
  export declare function activityLine(run: SubagentRun, cols: number): string;
58
- export declare function TeamPanel({ snapshot, rows, cols, reducedMotion, theme, caps, now, }: TeamPanelProps): React.ReactElement;
60
+ export declare function TeamPanel({ snapshot, rows, cols, reducedMotion, theme, caps, now, layout, }: TeamPanelProps): React.ReactElement | null;
@@ -5,7 +5,7 @@ import { pickGlyphs } from './glyphs.js';
5
5
  import { formatDuration } from '../agent/usage.js';
6
6
  import { describeToolActivity, sanitizeActivity } from '../team/activity.js';
7
7
  import { TEAM_LIMITS } from '../team/limits.js';
8
- import { selectPanelRows } from '../team/panel-rows.js';
8
+ import { buildTeamPanelLayout } from './layout/team-panel.js';
9
9
  function isRunning(run) {
10
10
  return run.phase === 'starting' || run.phase === 'thinking' || run.phase === 'tool' ||
11
11
  run.phase === 'waiting';
@@ -101,24 +101,28 @@ export function activityLine(run, cols) {
101
101
  return 'aborted';
102
102
  }
103
103
  }
104
- export function TeamPanel({ snapshot, rows, cols, reducedMotion, theme, caps, now, }) {
104
+ export function TeamPanel({ snapshot, rows, cols, reducedMotion, theme, caps, now, layout, }) {
105
+ const projection = layout ?? buildTeamPanelLayout({
106
+ snapshot, terminalRows: rows, availableRows: Infinity,
107
+ });
108
+ if (projection.rowCount === 0)
109
+ return null;
105
110
  const glyphs = pickGlyphs(caps);
106
111
  const at = now ?? Date.now();
107
112
  const running = snapshot.runs.filter(isRunning).length;
108
113
  const done = snapshot.runs.filter(isSettled).length;
109
114
  const elapsed = formatDuration(Math.max(0, at - snapshot.startedAt));
110
115
  const header = (_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Text, { wrap: "truncate", color: theme.accent, bold: true, children: [glyphs.teamAgent, " team", ' ', _jsxs(Text, { color: theme.muted, children: [running, " running ", glyphs.midDot, " ", done, " done ", glyphs.midDot, " ", elapsed] })] }), snapshot.messageCount > 0 && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [glyphs.teamMail, " ", snapshot.messageCount] }))] }));
111
- // Below 20 rows the roster is more expensive than the transcript it displaces,
112
- // so the panel degrades to the one line that still answers "is something
113
- // running, and for how long" (§6.4).
114
- if (rows < TEAM_LIMITS.panelCollapseRows) {
116
+ // Short terminals or a long draft leave only the shared header projection,
117
+ // which still answers whether anything is running and for how long.
118
+ if (projection.collapsed) {
115
119
  return _jsx(Box, { flexDirection: "column", flexShrink: 0, children: header });
116
120
  }
117
121
  // RANKED, not sliced by array index (F-2). The slot pool hands out indices in
118
122
  // order, so `slice(0, 5)` showed the five children that finished FIRST and hid
119
123
  // everything still working behind `+N more` - at exactly the fan-out widths
120
124
  // the requirement permits.
121
- const { visible, hiddenTotal, hiddenRunning } = selectPanelRows(snapshot.runs, TEAM_LIMITS.panelMaxRows);
125
+ const { visible, hiddenTotal, hiddenRunning } = projection;
122
126
  return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [header, visible.map((run) => {
123
127
  const color = statusColor(run, theme);
124
128
  // One spinner per RUNNING row, and the row cap is 5 precisely because
@@ -126,8 +130,8 @@ export function TeamPanel({ snapshot, rows, cols, reducedMotion, theme, caps, no
126
130
  // `reducedMotion` and a non-Unicode terminal fall back to the same
127
131
  // static marker, exactly as `AssistantEntry` and `ToolCard` already do.
128
132
  const animate = isRunning(run) && !reducedMotion && caps.unicode;
129
- return (_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: color, children: [' ', animate ? _jsx(Spinner, { type: "dots" }) : marker(run, glyphs), ' '] }), _jsx(Text, { color: theme.primary, children: `${run.label}${run.tier === 'fast' ? '~' : ''}`.padEnd(8).slice(0, 8) }), _jsx(Box, { flexGrow: 1, flexShrink: 1, overflow: "hidden", children: _jsx(Text, { wrap: "truncate", color: theme.muted, children: run.description }) }), _jsx(Box, { flexShrink: 1, overflow: "hidden", children: _jsxs(Text, { wrap: "truncate", color: color, children: [' ', activityLine(run, cols)] }) }), _jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', run.startedAt ? formatDuration(Math.max(0, (run.endedAt ?? at) - run.startedAt)) : ''] })] }, run.label));
130
- }), hiddenTotal > 0 && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', "+", hiddenTotal, " more", hiddenRunning > 0 ? ` (${hiddenRunning} running)` : ''] })), snapshot.lastMessage && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', glyphs.teamMail, " ", snapshot.lastMessage.from, " ", glyphs.arrowRight, " ", snapshot.lastMessage.to, ": ", snapshot.lastMessage.subject] }))] }));
133
+ return (_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: color, children: [' ', animate ? _jsx(Spinner, { type: "dots" }) : marker(run, glyphs), ' '] }), _jsx(Text, { color: theme.primary, children: `${run.label}${run.tier === 'fast' ? '~' : ''}`.padEnd(8).slice(0, 8) }), _jsx(Box, { flexGrow: 1, flexShrink: 1, overflow: "hidden", children: _jsx(Text, { wrap: "truncate", color: theme.muted, children: run.description.replace(/\s+/g, ' ') }) }), _jsx(Box, { flexShrink: 1, overflow: "hidden", children: _jsxs(Text, { wrap: "truncate", color: color, children: [' ', activityLine(run, cols)] }) }), _jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', run.startedAt ? formatDuration(Math.max(0, (run.endedAt ?? at) - run.startedAt)) : ''] })] }, run.label));
134
+ }), hiddenTotal > 0 && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', "+", hiddenTotal, " more", hiddenRunning > 0 ? ` (${hiddenRunning} running)` : ''] })), snapshot.lastMessage && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', glyphs.teamMail, " ", snapshot.lastMessage.from, " ", glyphs.arrowRight, " ", snapshot.lastMessage.to, ": ", snapshot.lastMessage.subject.replace(/\s+/g, ' ')] }))] }));
131
135
  }
132
136
  function marker(run, glyphs) {
133
137
  if (run.phase === 'done')
@@ -14,13 +14,13 @@
14
14
  * stop being a rail and start eating viewport height, growing and shrinking the
15
15
  * transcript every time the list changed length.
16
16
  *
17
- * WIDTH IS MEASURED IN COLUMNS, NEVER IN CODE UNITS (P2-3). This rail is 18-36
17
+ * WIDTH IS MEASURED IN COLUMNS, NEVER IN CODE UNITS (P2-3). This rail is 14-36
18
18
  * columns wide and the system prompt's last line is "Respond in the language the
19
19
  * user writes in", so CJK item text in an 18-column rail is the NORMAL case
20
20
  * rather than the edge case. Every text cell is `wrap="truncate"` and Ink's
21
21
  * truncation is display-width aware, so nothing can overflow the box — and NO
22
- * ARITHMETIC HERE MAY BUDGET USER TEXT BY `.length`. The `padEnd(3)` /
23
- * `padStart(2)` cells below are ASCII markers and indices, which is why they are
22
+ * ARITHMETIC HERE MAY BUDGET USER TEXT BY `.length`. The count string and
23
+ * `padStart(2)` cells below are ASCII counts and indices, which is why they are
24
24
  * safe; a "characters that fit" calculation over `content` would not be. That is
25
25
  * the whole of the lesson `TeamPanel`'s `WIDE_CHAR_ALLOWANCE` records at length,
26
26
  * and the mitigation here is to have no such budget at all.
@@ -34,7 +34,7 @@ export interface TodoPanelProps {
34
34
  /** From `todoRailWidth(cols)`. The rail's whole column budget, separator included. */
35
35
  width: number;
36
36
  /**
37
- * From `todoRailRows(viewportBudget, teamActive)` — NOT `viewportBudget`
37
+ * From `buildTodoRailLayout` after actual team/menu rows are subtracted.
38
38
  * (I-10 / P1-3). The gate that mounts this component and the number it lays
39
39
  * out against are ONE value computed once in `App`: a panel that mounts on one
40
40
  * number and lays out against another renders nothing and reports nothing.
@@ -46,4 +46,4 @@ export interface TodoPanelProps {
46
46
  theme: Theme;
47
47
  caps: TermCapabilities;
48
48
  }
49
- export declare function TodoPanel({ snapshot, width, rows, running, reducedMotion, theme, caps, }: TodoPanelProps): React.ReactElement;
49
+ export declare function TodoPanel({ snapshot, width, rows, running, reducedMotion, theme, caps, }: TodoPanelProps): React.ReactElement | null;
@@ -1,4 +1,32 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * TodoPanel — the right rail (todo-plan-execution §6.1).
4
+ *
5
+ * ASCII ONLY: every glyph comes from `pickGlyphs`; a literal here bypasses the
6
+ * capability probe and shows mojibake on a legacy console (`glyphs.ts` header).
7
+ *
8
+ * MOUNTED ONLY WHEN A LIST EXISTS. `App` passes `null` the rest of the time and
9
+ * this component is not rendered at all, so a session that never plans has zero
10
+ * columns and zero layout shift (R-g's "no furniture when idle" / AC-24).
11
+ *
12
+ * IT MUST BE MOUNTED IN `AppShell`'s MIDDLE BAND, NEVER IN THE BOTTOM CHROME
13
+ * (C-6 / I-4). This is a COLUMN BESIDE the viewport, and the middle band is the
14
+ * only row-wrapped region there is; moved down into the bottom chrome it would
15
+ * stop being a rail and start eating viewport height, growing and shrinking the
16
+ * transcript every time the list changed length.
17
+ *
18
+ * WIDTH IS MEASURED IN COLUMNS, NEVER IN CODE UNITS (P2-3). This rail is 14-36
19
+ * columns wide and the system prompt's last line is "Respond in the language the
20
+ * user writes in", so CJK item text in an 18-column rail is the NORMAL case
21
+ * rather than the edge case. Every text cell is `wrap="truncate"` and Ink's
22
+ * truncation is display-width aware, so nothing can overflow the box — and NO
23
+ * ARITHMETIC HERE MAY BUDGET USER TEXT BY `.length`. The count string and
24
+ * `padStart(2)` cells below are ASCII counts and indices, which is why they are
25
+ * safe; a "characters that fit" calculation over `content` would not be. That is
26
+ * the whole of the lesson `TeamPanel`'s `WIDE_CHAR_ALLOWANCE` records at length,
27
+ * and the mitigation here is to have no such budget at all.
28
+ */
29
+ import { useMemo } from 'react';
2
30
  import { Box, Text } from 'ink';
3
31
  import Spinner from 'ink-spinner';
4
32
  import { pickGlyphs } from './glyphs.js';
@@ -6,7 +34,7 @@ import { buildGauge } from './gauge.js';
6
34
  import { railBorderProps } from './layout/Gutter.js';
7
35
  import { TODO_RAIL_INDEX_MIN_COLS } from './layout/rail.js';
8
36
  import { TODO_LIMITS } from '../todo/limits.js';
9
- import { selectTodoRows } from '../todo/panel-rows.js';
37
+ import { selectCompactTodoRows, selectTodoRows } from '../todo/panel-rows.js';
10
38
  /** Header row + the blank row under the gauge. */
11
39
  const FIXED_ROWS = 2;
12
40
  /** The gauge is the one purely decorative element, so it is the first to go. */
@@ -19,8 +47,11 @@ const MARKER_COLS = 3;
19
47
  /** Index cell: two digits plus the space that separates it from the text. */
20
48
  const INDEX_COLS = 3;
21
49
  function marker(item, glyphs) {
22
- if (item.status === 'completed')
23
- return glyphs.todoDone;
50
+ // Ink's tokenizer otherwise treats the bare emoji-capable check as one cell,
51
+ // while string-width counts two, adding an extra visible space to each row.
52
+ if (item.status === 'completed') {
53
+ return glyphs.todoDone.length === 1 ? `${glyphs.todoDone}\uFE0F` : glyphs.todoDone;
54
+ }
24
55
  if (item.status === 'in_progress')
25
56
  return glyphs.todoActive;
26
57
  return glyphs.todoPending;
@@ -34,41 +65,42 @@ function rowColor(item, theme) {
34
65
  }
35
66
  export function TodoPanel({ snapshot, width, rows, running, reducedMotion, theme, caps, }) {
36
67
  const glyphs = pickGlyphs(caps);
68
+ // Ink reapplies all four Yoga borders when the borderStyle object changes,
69
+ // but unchanged borderTop/Bottom=false props are not reapplied on that update.
70
+ const border = useMemo(() => railBorderProps(glyphs.railVertical), [glyphs.railVertical]);
71
+ if (snapshot.items.length === 0 || rows < TODO_LIMITS.panelMinRows)
72
+ return null;
73
+ const compact = rows < TODO_LIMITS.panelFullRows;
37
74
  const showGauge = width >= GAUGE_MIN_COLS && rows >= GAUGE_MIN_ROWS;
38
- const showIndex = width >= TODO_RAIL_INDEX_MIN_COLS;
39
- const itemRows = Math.max(1, rows - FIXED_ROWS - (showGauge ? 1 : 0));
40
- const { visible, hiddenAbove, hiddenBelow } = selectTodoRows(snapshot.items, itemRows);
75
+ const showIndex = !compact && width >= TODO_RAIL_INDEX_MIN_COLS;
76
+ const compactFooter = compact && snapshot.items.length > rows - 1;
77
+ const itemRows = compact ? rows - 1 - Number(compactFooter)
78
+ : rows - FIXED_ROWS - Number(showGauge);
79
+ const { visible, hiddenAbove, hiddenBelow } = compact
80
+ ? selectCompactTodoRows(snapshot.items, itemRows)
81
+ : selectTodoRows(snapshot.items, itemRows);
41
82
  const pct = snapshot.total > 0 ? Math.round((snapshot.doneCount / snapshot.total) * 100) : 0;
42
83
  const gauge = buildGauge(pct, Math.max(4, width - CHROME_COLS - 2), theme, caps);
43
84
  const complete = snapshot.doneCount === snapshot.total;
85
+ const countText = `${snapshot.doneCount}/${snapshot.total}`;
86
+ const showDone = complete
87
+ && width - CHROME_COLS >= 'TODO '.length + countText.length + ' done'.length;
88
+ const count = `${countText}${showDone ? ' done' : ''}`;
44
89
  return (
45
90
  // `flexShrink={0}` IS LOAD-BEARING (P1-4). Ink's `Box.defaultProps` is
46
91
  // `{ flexWrap:'nowrap', flexDirection:'row', flexGrow:0, flexShrink:1 }`, so
47
92
  // a `width={n}` box inside a shrinking row is SQUEEZED under pressure — and
48
93
  // every piece of arithmetic in `rail.ts` silently becomes a suggestion at
49
94
  // exactly the narrow widths where the guarantee matters. `ScrollIndicator`
50
- // spells the same pair one level down. Cross-axis height needs no prop:
51
- // `alignItems` is unset in this tree and yoga's default is `stretch`.
52
- _jsxs(Box, { flexDirection: "column", flexShrink: 0, width: width, overflow: "hidden", paddingLeft: 1, ...railBorderProps(glyphs.railVertical, theme.border), children: [_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", children: [_jsx(Text, { wrap: "truncate", color: theme.accent, bold: true, children: "TODO" }), _jsxs(Text, { wrap: "truncate", color: complete ? theme.toolDone : theme.muted, children: [snapshot.doneCount, "/", snapshot.total, complete ? ' done' : ''] })] }), showGauge && (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: gauge.fillColor, children: gauge.filled }), _jsx(Text, { color: gauge.trackColor, children: gauge.empty })] })), _jsx(Text, { children: " " }), hiddenAbove > 0 && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', "+", hiddenAbove, " above"] })), visible.map(({ item, index }) => {
95
+ // spells the same pair one level down. Height uses the same row budget as
96
+ // selection, so content cannot silently enlarge the middle band.
97
+ _jsxs(Box, { flexDirection: "column", flexShrink: 0, width: width, height: rows, overflow: "hidden", paddingLeft: 1, ...border, borderColor: theme.border, children: [_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", children: [_jsx(Box, { flexGrow: 1, flexShrink: 1, overflow: "hidden", children: _jsx(Text, { wrap: "truncate", color: theme.accent, bold: true, children: "TODO" }) }), _jsx(Box, { width: count.length, flexShrink: 0, children: _jsx(Text, { wrap: "truncate", color: complete ? theme.toolDone : theme.muted, children: count }) })] }), showGauge && (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: gauge.fillColor, children: gauge.filled }), _jsx(Text, { color: gauge.trackColor, children: gauge.empty })] })), !compact && _jsx(Text, { children: " " }), !compact && hiddenAbove > 0 && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', "+", hiddenAbove, " above"] })), visible.map(({ item, index }) => {
53
98
  const active = item.status === 'in_progress';
54
99
  // The spinner is gated on `running`: a spinner while the agent is idle
55
100
  // is a lie about the state of the world, and this panel outlives the run
56
101
  // that filled it. Reduced motion and a non-Unicode terminal fall back to
57
102
  // the same static marker, exactly as `AssistantEntry` and `TeamPanel` do.
58
103
  const animate = active && running && !reducedMotion && caps.unicode;
59
- return (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { flexShrink: 0, width: MARKER_COLS, children: _jsx(Text, { color: rowColor(item, theme), bold: active, children: animate ? _jsx(Spinner, { type: "dots" }) : marker(item, glyphs) }) }), showIndex && (_jsx(Box, { flexShrink: 0, width: INDEX_COLS, children: _jsx(Text, { color: theme.muted, children: String(index + 1).padStart(2) }) })), _jsx(Box, { flexGrow: 1, flexShrink: 1, overflow: "hidden", children: _jsx(Text, { wrap: active ? 'wrap' : 'truncate', color: rowColor(item, theme), bold: active, children: active ? clampWrapped(item.activeForm, width) : item.content }) })] }, `${index}-${item.content}`));
60
- }), hiddenBelow > 0 && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', "+", hiddenBelow, " more"] }))] }));
61
- }
62
- /**
63
- * Keep the wrapping row inside `TODO_LIMITS.activeWrapRows`.
64
- *
65
- * A CHARACTER BUDGET IS NOT A COLUMN BUDGET (P2-3), so this is deliberately
66
- * generous rather than exact: it exists to stop a maximum-length `activeForm`
67
- * from claiming five rows of an 18-column rail, and Ink's display-width-aware
68
- * truncation is what actually keeps the row inside the box. Erring long costs
69
- * nothing — the surrounding `overflow="hidden"` clips it.
70
- */
71
- function clampWrapped(text, width) {
72
- const budget = Math.max(1, width - CHROME_COLS) * TODO_LIMITS.activeWrapRows;
73
- return text.length > budget ? text.slice(0, budget) : text;
104
+ return (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { flexShrink: 0, width: MARKER_COLS, children: _jsx(Text, { color: rowColor(item, theme), bold: active, children: animate ? _jsx(Spinner, { type: "dots" }) : marker(item, glyphs) }) }), showIndex && (_jsx(Box, { flexShrink: 0, width: INDEX_COLS, children: _jsx(Text, { color: theme.muted, children: String(index + 1).padStart(2) }) })), _jsx(Box, { flexGrow: 1, flexShrink: 1, overflow: "hidden", children: _jsx(Text, { wrap: "truncate", color: rowColor(item, theme), bold: active, children: active ? item.activeForm : item.content }) })] }, `${index}-${item.content}`));
105
+ }), compactFooter && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: ["-", hiddenAbove, " +", hiddenBelow] })), !compact && hiddenBelow > 0 && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', "+", hiddenBelow, " more"] }))] }, glyphs.railVertical));
74
106
  }
@@ -40,6 +40,7 @@ import { TodoCard, todoCardColor } from './entries/TodoCard.js';
40
40
  import { RetryCard, retryCardColor } from './entries/RetryCard.js';
41
41
  import { FastCard, fastCardColor } from './entries/FastCard.js';
42
42
  import { CompactionCard, compactionCardColor } from './entries/CompactionCard.js';
43
+ import { ServiceCard, serviceCardColor, serviceGlyph } from './entries/ServiceCard.js';
43
44
  import { MeasuredEntry } from './layout/MeasuredEntry.js';
44
45
  import { useViewportGeometry } from './layout/viewport-geometry.js';
45
46
  import { estimateEntryRows, heightKey, selectWindow, VIRTUAL_LIMITS, } from './layout/virtual-window.js';
@@ -107,6 +108,31 @@ export function computeSettledCount(entries, expandedToolIds) {
107
108
  // which is the other half.
108
109
  if (e.kind === 'compaction' && e.live)
109
110
  break;
111
+ /**
112
+ * A SERVICE BLOCKS ONLY WHILE IT IS `starting` (D-11 / P0-4).
113
+ *
114
+ * Every clause above is bounded by an OPERATION — a dispatch, a turn, one
115
+ * LLM call, one countdown — which is why each of them can safely pin this
116
+ * prefix scan: the wait is short and it ends. A service is bounded by the
117
+ * USER'S INTENT. A dev server left up for an hour is the normal case, not
118
+ * the pathological one, so treating it like the other five would pin the
119
+ * boundary at the card and re-render the whole transcript from there on
120
+ * EVERY FRAME, FOREVER, on exactly the long sessions where it is least
121
+ * likely to be noticed.
122
+ *
123
+ * `starting` is different, and it is different for the reason that makes
124
+ * every other clause here legitimate: it is bounded by `readyTimeoutMs`,
125
+ * after which the supervisor calls the service `running` whatever happened.
126
+ *
127
+ * The cost of the rest is that a printed card stops mirroring live state —
128
+ * which is what a transcript IS. A terminal transition appends a NEW one-row
129
+ * entry rather than rewriting a card `<Static>` has already drawn, and live
130
+ * state lives in the status chip, `/bg` and `bash_output`. Weakening this to
131
+ * "any non-terminal service blocks" is the change D-11 exists to prevent;
132
+ * see condition 5 of the design's review verdict before making it.
133
+ */
134
+ if (e.kind === 'service' && e.status === 'starting')
135
+ break;
110
136
  count = i + 1;
111
137
  }
112
138
  return count;
@@ -148,7 +174,7 @@ function EntryViewImpl({ entry, prev, expanded, thinkingVisible, reducedMotion,
148
174
  const frame = (glyph, color, children) => (_jsx(EntryFrame, { glyph: glyph, color: color, continuation: glyphs.railVertical, separation: separation, spacerColor: theme.border, children: children }));
149
175
  switch (entry.kind) {
150
176
  case 'user':
151
- return frame(glyphs.user, theme.user, _jsx(UserEntry, { text: entry.text, theme: theme }));
177
+ return frame(glyphs.user, theme.user, _jsx(UserEntry, { text: entry.text, theme: theme, caps: caps }));
152
178
  case 'assistant': {
153
179
  // While text streams, the spinner IS the role marker. Braille dots are
154
180
  // both an animation and a Unicode-only glyph, so reduced motion and an
@@ -208,6 +234,8 @@ function EntryViewImpl({ entry, prev, expanded, thinkingVisible, reducedMotion,
208
234
  elapsedMs: entry.live && nowSec !== undefined && entry.startedAt !== undefined
209
235
  ? Math.max(0, nowSec * 1000 - entry.startedAt)
210
236
  : undefined, live: entry.live, expanded: expanded, reducedMotion: reducedMotion, theme: theme, caps: caps }));
237
+ case 'service':
238
+ return frame(serviceGlyph(entry, glyphs), serviceCardColor(entry, theme), _jsx(ServiceCard, { serviceId: entry.serviceId, command: entry.command, status: entry.status, url: entry.url, port: entry.port, exitCode: entry.exitCode, startedAt: entry.startedAt, readyAt: entry.readyAt, endedAt: entry.endedAt, rows: entry.rows, terminal: entry.terminal, killIncomplete: entry.killIncomplete, expanded: expanded, reducedMotion: reducedMotion, theme: theme, caps: caps }));
211
239
  case 'notice':
212
240
  return frame(noticeSymbol(entry.level, theme), noticeColor(entry.level, theme), _jsx(Text, { color: noticeColor(entry.level, theme), children: entry.text }));
213
241
  default:
@@ -9,7 +9,7 @@
9
9
  * still truthy. Taking that branch would emit a box of ZERO rows - the frame
10
10
  * keeps its fixed height, Yoga hands the row to the transcript, and
11
11
  * `viewportRows()` goes on reporting the old number to `ScrollViewport`,
12
- * `selectWindow`, `overlayMaxRows`, `popupMaxRows` and `todoRailRows`. The
12
+ * `selectWindow`, `overlayMaxRows`, `popupMaxRows` and `buildTodoRailLayout`. The
13
13
  * transcript would gain a row while the updater is idle and lose it the moment
14
14
  * it has news. Presence is the CALLER's decision, through
15
15
  * `shouldRenderUpdateLine`, exactly as `App.tsx` already decides the activity
@@ -15,10 +15,6 @@ function detectColorLevel(env) {
15
15
  return 0;
16
16
  if (env.TERM === 'dumb')
17
17
  return 0;
18
- // A truecolor-capable terminal advertises itself.
19
- const colorterm = env.COLORTERM?.toLowerCase();
20
- if (colorterm === 'truecolor' || colorterm === '24bit')
21
- return 3;
22
18
  // FORCE_COLOR is an explicit override of the auto-detected depth.
23
19
  const force = env.FORCE_COLOR;
24
20
  if (force !== undefined) {
@@ -30,6 +26,10 @@ function detectColorLevel(env) {
30
26
  return 3;
31
27
  return 1; // '1' | 'true' | '' → at least basic color
32
28
  }
29
+ // A truecolor-capable terminal advertises itself.
30
+ const colorterm = env.COLORTERM?.toLowerCase();
31
+ if (colorterm === 'truecolor' || colorterm === '24bit')
32
+ return 3;
33
33
  // Fall back to chalk's own platform detection.
34
34
  const level = chalk.level;
35
35
  return level >= 0 && level <= 3 ? level : 0;
@@ -42,7 +42,7 @@ function detectUnicode(env) {
42
42
  if (env.WT_SESSION)
43
43
  return true; // Windows Terminal
44
44
  const program = env.TERM_PROGRAM;
45
- if (program === 'vscode' || program === 'iTerm.app' || program === 'Apple_Terminal')
45
+ if (program === 'aragonmesh' || program === 'vscode' || program === 'iTerm.app' || program === 'Apple_Terminal')
46
46
  return true;
47
47
  // Legacy cmd.exe (no TERM, no WT_SESSION) and unknown terminals fall back to ASCII.
48
48
  return false;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Composer and transcript row ceilings (tui-paste-handling section 5.5 / 5.6).
3
+ *
4
+ * Structural, not policy (D-10): the only user-facing key this feature adds is
5
+ * the boolean `paste`. Everything here is a bound that keeps the frame's row
6
+ * arithmetic true, and every one of them is switchable by returning a larger
7
+ * number rather than by a config flag.
8
+ */
9
+ /** At or below BOTH of these a paste is inserted verbatim rather than collapsed. */
10
+ export declare const PASTE_INLINE_MAX_LINES = 6;
11
+ export declare const PASTE_INLINE_MAX_CHARS = 400;
12
+ /**
13
+ * Rows one submitted user message may RENDER in the transcript, before the
14
+ * `... +N more lines` tail (G7).
15
+ *
16
+ * `ui/layout/virtual-window.ts` imports this rather than duplicating the number,
17
+ * unlike `DIFF_COLLAPSED_LINES` / `TOOL_LIVE_TAIL_ROWS` beside it: I-13 makes the
18
+ * render cap and the height estimate THE SAME NUMBER, and there is no
19
+ * import-cycle reason here for them to be two.
20
+ */
21
+ export declare const USER_ENTRY_MAX_ROWS = 40;
22
+ /**
23
+ * Ceiling on the composer's DRAFT rows at a given terminal height.
24
+ *
25
+ * Non-decreasing in `terminalRows`. Returning `Infinity` reduces the whole
26
+ * height-bound layer to the rung below it (section 12, S4), which is what makes
27
+ * that stage revertible on its own.
28
+ */
29
+ export declare function draftMaxRows(terminalRows: number): number;
30
+ /** The terminal height at which `draftMaxRows` steps from 3 to 6. Pinned by test. */
31
+ export declare const DRAFT_MAX_ROWS_HINT_TIER = 20;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Composer and transcript row ceilings (tui-paste-handling section 5.5 / 5.6).
3
+ *
4
+ * Structural, not policy (D-10): the only user-facing key this feature adds is
5
+ * the boolean `paste`. Everything here is a bound that keeps the frame's row
6
+ * arithmetic true, and every one of them is switchable by returning a larger
7
+ * number rather than by a config flag.
8
+ */
9
+ /** At or below BOTH of these a paste is inserted verbatim rather than collapsed. */
10
+ export const PASTE_INLINE_MAX_LINES = 6;
11
+ export const PASTE_INLINE_MAX_CHARS = 400;
12
+ /**
13
+ * Rows one submitted user message may RENDER in the transcript, before the
14
+ * `... +N more lines` tail (G7).
15
+ *
16
+ * `ui/layout/virtual-window.ts` imports this rather than duplicating the number,
17
+ * unlike `DIFF_COLLAPSED_LINES` / `TOOL_LIVE_TAIL_ROWS` beside it: I-13 makes the
18
+ * render cap and the height estimate THE SAME NUMBER, and there is no
19
+ * import-cycle reason here for them to be two.
20
+ */
21
+ export const USER_ENTRY_MAX_ROWS = 40;
22
+ /**
23
+ * The terminal height at which the composer's hint row starts to fit.
24
+ *
25
+ * DELIBERATELY A SECOND COPY of `ui/layout/budget.ts::HINT_MIN_ROWS` rather than
26
+ * an import: `budget.ts` calls `draftMaxRows` to clamp its composer term, so an
27
+ * import in this direction would close an ESM cycle whose only protection is
28
+ * that neither side reads the other at module scope -- true today, and a
29
+ * confusing TDZ crash the first time someone hoists a use. `budget.test.ts`
30
+ * asserts the two numbers are equal, so they cannot drift in silence.
31
+ */
32
+ const DRAFT_TIER_MIN_ROWS = 20;
33
+ /**
34
+ * Ceiling on the composer's DRAFT rows at a given terminal height.
35
+ *
36
+ * Non-decreasing in `terminalRows`. Returning `Infinity` reduces the whole
37
+ * height-bound layer to the rung below it (section 12, S4), which is what makes
38
+ * that stage revertible on its own.
39
+ */
40
+ export function draftMaxRows(terminalRows) {
41
+ if (!Number.isFinite(terminalRows))
42
+ return 3;
43
+ if (terminalRows >= 30)
44
+ return 10;
45
+ if (terminalRows >= DRAFT_TIER_MIN_ROWS)
46
+ return 6;
47
+ return 3;
48
+ }
49
+ /** The terminal height at which `draftMaxRows` steps from 3 to 6. Pinned by test. */
50
+ export const DRAFT_MAX_ROWS_HINT_TIER = DRAFT_TIER_MIN_ROWS;