@aragon-agent/cli 0.6.12 → 0.6.13

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 (101) hide show
  1. package/README.md +81 -15
  2. package/dist/agent/controller.d.ts +29 -1
  3. package/dist/agent/controller.js +123 -22
  4. package/dist/agent/model-profile-settings.d.ts +31 -0
  5. package/dist/agent/model-profile-settings.js +199 -0
  6. package/dist/agent/queued-messages.d.ts +12 -0
  7. package/dist/agent/queued-messages.js +18 -0
  8. package/dist/agent/reducer.d.ts +13 -1
  9. package/dist/agent/reducer.js +51 -12
  10. package/dist/cli.js +15 -1
  11. package/dist/commands/builtins.js +66 -39
  12. package/dist/commands/queue-command.d.ts +3 -0
  13. package/dist/commands/queue-command.js +5 -0
  14. package/dist/commands/registry.d.ts +3 -0
  15. package/dist/compaction/child.d.ts +4 -2
  16. package/dist/compaction/child.js +4 -1
  17. package/dist/compaction/compactor.d.ts +7 -2
  18. package/dist/compaction/compactor.js +13 -11
  19. package/dist/compaction/context-command.js +5 -0
  20. package/dist/compaction/wiring.d.ts +4 -3
  21. package/dist/compaction/wiring.js +6 -1
  22. package/dist/config/cli-commands.d.ts +2 -0
  23. package/dist/config/cli-commands.js +16 -4
  24. package/dist/config/load.d.ts +2 -1
  25. package/dist/config/load.js +15 -8
  26. package/dist/config/model-profile-resolution.d.ts +20 -0
  27. package/dist/config/model-profile-resolution.js +81 -0
  28. package/dist/config/model-profile-store.d.ts +67 -0
  29. package/dist/config/model-profile-store.js +153 -0
  30. package/dist/config/model-profiles.d.ts +44 -0
  31. package/dist/config/model-profiles.js +121 -0
  32. package/dist/config/schema.d.ts +9 -0
  33. package/dist/config/store.d.ts +11 -1
  34. package/dist/config/store.js +43 -27
  35. package/dist/fast/resolve.d.ts +2 -1
  36. package/dist/fast/resolve.js +6 -4
  37. package/dist/fast/wiring.d.ts +5 -3
  38. package/dist/fast/wiring.js +8 -4
  39. package/dist/input/stdin-filter.js +3 -2
  40. package/dist/logging/install.js +4 -2
  41. package/dist/logging/secret-registry.d.ts +2 -0
  42. package/dist/logging/secret-registry.js +18 -0
  43. package/dist/team/runtime.d.ts +2 -1
  44. package/dist/team/subagent.d.ts +3 -1
  45. package/dist/team/subagent.js +3 -1
  46. package/dist/ui/ActivityLine.d.ts +6 -70
  47. package/dist/ui/ActivityLine.js +12 -34
  48. package/dist/ui/App.d.ts +1 -0
  49. package/dist/ui/App.js +113 -205
  50. package/dist/ui/BottomStatusRow.d.ts +6 -86
  51. package/dist/ui/BottomStatusRow.js +11 -26
  52. package/dist/ui/Composer.d.ts +16 -83
  53. package/dist/ui/Composer.js +9 -100
  54. package/dist/ui/PromptInput.d.ts +4 -1
  55. package/dist/ui/PromptInput.js +41 -34
  56. package/dist/ui/QueuePanel.d.ts +8 -0
  57. package/dist/ui/QueuePanel.js +9 -0
  58. package/dist/ui/QueueStatusRow.d.ts +2 -0
  59. package/dist/ui/QueueStatusRow.js +1 -1
  60. package/dist/ui/StatusBar.d.ts +13 -149
  61. package/dist/ui/StatusBar.js +27 -142
  62. package/dist/ui/clipboard-task.d.ts +29 -0
  63. package/dist/ui/clipboard-task.js +66 -0
  64. package/dist/ui/clipboard.d.ts +29 -50
  65. package/dist/ui/clipboard.js +141 -75
  66. package/dist/ui/composer-limits.d.ts +3 -4
  67. package/dist/ui/composer-limits.js +4 -9
  68. package/dist/ui/composer-rows.d.ts +1 -1
  69. package/dist/ui/composer-rows.js +24 -137
  70. package/dist/ui/editor-navigation.d.ts +45 -0
  71. package/dist/ui/editor-navigation.js +85 -0
  72. package/dist/ui/editor-reducer.d.ts +4 -0
  73. package/dist/ui/editor-reducer.js +29 -15
  74. package/dist/ui/interaction-copy.d.ts +77 -0
  75. package/dist/ui/interaction-copy.js +40 -0
  76. package/dist/ui/interaction-hints.d.ts +18 -0
  77. package/dist/ui/interaction-hints.js +92 -0
  78. package/dist/ui/layout/budget.js +1 -1
  79. package/dist/ui/layout/queue-layout.d.ts +19 -0
  80. package/dist/ui/layout/queue-layout.js +26 -0
  81. package/dist/ui/layout/status-layout.d.ts +55 -0
  82. package/dist/ui/layout/status-layout.js +109 -0
  83. package/dist/ui/layout/todo-layout.d.ts +1 -0
  84. package/dist/ui/layout/todo-layout.js +1 -1
  85. package/dist/ui/overlays/HelpOverlay.js +8 -8
  86. package/dist/ui/overlays/ModelProfileEditor.d.ts +12 -0
  87. package/dist/ui/overlays/ModelProfileEditor.js +103 -0
  88. package/dist/ui/overlays/ModelProfilePicker.d.ts +35 -0
  89. package/dist/ui/overlays/ModelProfilePicker.js +167 -0
  90. package/dist/ui/overlays/QueueOverlay.d.ts +22 -0
  91. package/dist/ui/overlays/QueueOverlay.js +79 -0
  92. package/dist/ui/overlays/SettingsScreen.d.ts +9 -2
  93. package/dist/ui/overlays/SettingsScreen.js +347 -3
  94. package/dist/ui/overlays/model-profile-state.d.ts +74 -0
  95. package/dist/ui/overlays/model-profile-state.js +119 -0
  96. package/dist/ui/run-status-row.d.ts +7 -19
  97. package/dist/ui/run-status-row.js +2 -18
  98. package/dist/ui/selection/selection-controller.d.ts +2 -2
  99. package/dist/ui/selection/selection-controller.js +1 -0
  100. package/dist/ui/use-wheel-routing.js +2 -1
  101. package/package.json +2 -2
@@ -1,42 +1,27 @@
1
- /**
2
- * Status bar (spec §4.8). Width-aware, borderless, EXACTLY one row: a left
3
- * cluster (run status, model, thinking level) and a right cluster (context
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
13
- *
14
- * Dropping the round border here buys back 3 rows of a 24-row terminal — an
15
- * eighth of the screen was being spent framing a single line of text.
16
- */
1
+ /** A single row of complete, measured status fields. */
17
2
  import React from 'react';
18
3
  import type { Theme } from './theme.js';
19
4
  import type { TermCapabilities } from './capabilities.js';
20
5
  import type { UsageTotal } from '../agent/reducer.js';
21
6
  import type { ContextUsageSnapshot } from '../compaction/types.js';
22
- import { type AgentMode } from '../agent/agent-mode.js';
7
+ import type { AgentMode } from '../agent/agent-mode.js';
23
8
  import type { PendingSteering } from '../agent/queued-messages.js';
24
- interface StatusBarProps {
9
+ import { type RunPhase } from './layout/status-layout.js';
10
+ export interface StatusBarProps {
11
+ runPhase?: RunPhase;
12
+ activeTool?: {
13
+ name: string;
14
+ toolCallId: string;
15
+ } | null;
16
+ runOutcome?: "none" | "ended" | "interrupted" | "failed";
17
+ reducedMotion?: boolean;
18
+ waitingForConfirmation?: boolean;
19
+ stopping?: boolean;
25
20
  pendingSteering?: readonly PendingSteering[];
26
21
  interruptHint?: string;
27
22
  model: string;
28
23
  provider: string;
29
24
  usageTotal: UsageTotal;
30
- /**
31
- * Context occupancy, as one object (context-usage-gauge-accuracy §4.3).
32
- *
33
- * IT REPLACED FOUR PARALLEL PROPS - `contextTokens`, `contextWindow`,
34
- * `contextWindowKnown`, `contextEstimated`. Four props describing one reading
35
- * are four chances for a caller to update three of them, which is the props-
36
- * layer form of the "many writers" defect this feature removed from the
37
- * reducer. `usageTotal` stays separate because it genuinely IS another
38
- * quantity: session spend, not occupancy.
39
- */
40
25
  context: ContextUsageSnapshot;
41
26
  status: 'idle' | 'running';
42
27
  elapsedMs: number;
@@ -44,157 +29,36 @@ interface StatusBarProps {
44
29
  tokPerSec: number;
45
30
  theme: Theme;
46
31
  caps: TermCapabilities;
47
- /**
48
- * Lines scrolled off the bottom; > 0 renders `↑N`. Inline mode has no
49
- * self-drawn viewport, so it simply never sends anything but 0 — the bar
50
- * needs no render-mode branch of its own.
51
- */
52
32
  scrolledLines?: number;
53
- /**
54
- * Ctrl+L redraw carrier — invariant I-5 (spec §4.13). See `redrawChar`.
55
- */
56
33
  redrawNonce?: number;
57
34
  agentMode?: AgentMode;
58
- /** Non-null while a deferred `plan -> build` waits for `agent_end`. */
59
35
  pendingAgentMode?: AgentMode | null;
60
- /**
61
- * Live subagent counts (team-subagents §6.2), or absent when no dispatch is
62
- * running — an ordinary session's bar is then unchanged.
63
- *
64
- * LEFT CLUSTER, and it degrades (D-20 / P2-1). The left cluster is
65
- * `flexShrink={0}`, so every column it takes comes out of the context gauge
66
- * and the cost readout opposite it; on an 80-column terminal the full
67
- * `agents 3/5` visibly squeezes them, and the gauge is how a user notices they
68
- * are about to run out of context. Below `TEAM_LIMITS.statusCompactCols` this
69
- * therefore renders as `[3]` — the running count alone.
70
- *
71
- * This does NOT contradict the "no `cols >= N` breakpoint" rule above: that
72
- * rule is written about `agentMode`, whose reason is that the mode must never
73
- * be unreportable. The team counter has a second, fuller home in `TeamPanel`
74
- * and a third in `TeamCard`; it is the one readout on this bar that can afford
75
- * to degrade.
76
- */
77
36
  teamActive?: {
78
37
  running: number;
79
38
  total: number;
80
39
  };
81
- /**
82
- * Live todo counts (todo-plan-execution §6.3), or absent.
83
- *
84
- * `App` sends this ONLY when a list exists AND the rail does not: with the
85
- * rail on screen the numbers are already there in a larger typeface, and
86
- * duplicating them costs the context gauge columns it cannot spare. This is
87
- * therefore the INLINE-MODE and NARROW-TERMINAL fallback for R-d, which is
88
- * exactly why it is here rather than being folded into the rail's own gate.
89
- *
90
- * Degrades below `TODO_LIMITS.statusCompactCols` from `todo 2/7` to `[2/7]`,
91
- * the same treatment and the same justification as the team counter's
92
- * `agents 3/5` -> `[3]`. See the note on `agentMode` for why that rule does
93
- * NOT apply to the mode word.
94
- */
95
40
  todoActive?: {
96
41
  done: number;
97
42
  total: number;
98
43
  };
99
- /**
100
- * Live background services (background-service-supervision §3.8), or absent
101
- * when none are running — an ordinary session's bar is then unchanged.
102
- *
103
- * LEFT CLUSTER, after the todo chip, and it DEGRADES rather than hiding below
104
- * `PROC_LIMITS.statusCompactCols`. That is the `todoActive` / `teamActive`
105
- * ladder rather than `compactionActive`'s hide-below-threshold one, and the
106
- * choice is deliberate: `compact` and `fast` describe something the CLI is
107
- * doing to itself, whereas this is a PROCESS ON THE USER'S MACHINE HOLDING A
108
- * PORT. A narrow terminal is a reason to shorten it, never a reason to stop
109
- * saying it.
110
- *
111
- * `live` counts `starting | ready | running`. The transcript records events
112
- * about a service (D-11); this is where "what is running right now" lives,
113
- * alongside `/bg` and `bash_output`.
114
- */
115
44
  servicesActive?: {
116
45
  live: number;
117
46
  };
118
- /**
119
- * Live retry state (llm-api-retry-backoff §6.6), or absent when nothing is
120
- * retrying — an ordinary session's bar is then unchanged.
121
- *
122
- * LEFT CLUSTER, after `teamActive`, and it DEGRADES. It is a run-state fact like
123
- * `agents 3/5`, and the left cluster is `flexShrink={0}` so every column it takes
124
- * comes out of the context gauge; below `RETRY_UI.statusCompactCols` it renders
125
- * as `[r3]`.
126
- *
127
- * This does NOT contradict the "no `cols >= N` breakpoint" rule on `agentMode`
128
- * above: that rule exists because the MODE must never be unreportable. The retry
129
- * has a second, fuller home in the transcript card, which is exactly why it is
130
- * the kind of readout that can afford to shrink.
131
- */
132
47
  retryActive?: {
133
48
  attempt: number;
134
49
  max: number;
135
50
  secondsLeft: number;
136
51
  };
137
- /**
138
- * Render-governor rung (tui-render-performance L4). `> 0` renders a muted
139
- * `eco` chip.
140
- *
141
- * I-L4-2 — THE GOVERNOR IS USER-VISIBLE, and this is where. A user who cannot
142
- * see why the stream got chunkier will conclude the model got slower, and
143
- * degrading silently is the one thing a claim of robustness cannot afford.
144
- * It sits in the RIGHT cluster next to the other derived numbers and is
145
- * suppressed on a narrow terminal, which is the `teamActive` treatment rather
146
- * than the `agentMode` one: the mode must never be unreportable, whereas an
147
- * eco chip on a 50-column terminal costs the context gauge more than it says.
148
- */
149
52
  ecoRung?: number;
150
- /**
151
- * The live fast tier (fast-model-tier §6), or absent when it is off for this
152
- * session — in which case an ordinary session's bar is byte-identical.
153
- *
154
- * RIGHT CLUSTER, next to `eco`, and it DEGRADES below
155
- * `FAST_LIMITS.statusCompactCols`. This is the `teamActive` / `ecoRung`
156
- * treatment rather than the `agentMode` one, and the distinction is the note
157
- * on `agentMode` above: the mode must never be unreportable, whereas the fast
158
- * tier has a fuller home in `/fast status` — which is the GUARANTEED reporting
159
- * surface — and a chip on a 60-column terminal costs the context gauge more
160
- * than it says.
161
- *
162
- * `inFlight` renders `fast*`, so a review that is taking a while is visible
163
- * without a spinner competing with the run's own.
164
- */
165
53
  fastActive?: {
166
54
  inFlight: boolean;
167
55
  };
168
- /**
169
- * Live context compaction (context-auto-compaction §6.2), or absent when it is
170
- * off for this session — in which case an ordinary session's bar is unchanged.
171
- *
172
- * RIGHT CLUSTER, next to `fast` and `eco`, and it DEGRADES below
173
- * `COMPACTION_LIMITS.statusCompactCols`. The `teamActive` / `ecoRung` /
174
- * `fastActive` treatment rather than the `agentMode` one, for the reason the
175
- * note on `agentMode` gives: the mode must never be unreportable, whereas
176
- * compaction has a fuller home in `/compact status` — the GUARANTEED reporting
177
- * surface (§6.5) — and a chip on a 60-column terminal costs the context gauge
178
- * more than it says.
179
- *
180
- * `inFlight` renders `compacting`, so a summarization that is taking a while is
181
- * visible on this row without a spinner competing with the activity line's.
182
- */
183
56
  compactionActive?: {
184
57
  inFlight: boolean;
185
58
  };
186
- /**
187
- * Where the gauge turns amber and red (§6.2).
188
- *
189
- * ABSENT means today's hardcoded `{ warn: 60, high: 85 }`, so a session with
190
- * compaction off renders a byte-identical bar. When present these are
191
- * `warnThreshold * 100` and `threshold * 100`, which is what makes the colour
192
- * the user sees agree with the number the trigger fires on.
193
- */
194
59
  gaugeMarks?: {
195
60
  warn: number;
196
61
  high: number;
197
62
  };
198
63
  }
199
64
  export declare function StatusBar(props: StatusBarProps): React.ReactElement;
200
- export {};
@@ -1,148 +1,33 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import stringWidth from 'string-width';
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /** A single row of complete, measured status fields. */
3
+ import React from 'react';
3
4
  import { Box, Text, useStdout } from 'ink';
4
5
  import { pickGlyphs } from './glyphs.js';
5
- import { formatCost, formatTokens, formatDuration, promptTokensOf } from '../agent/usage.js';
6
- import { buildGauge } from './gauge.js';
7
- import { MODE_LABEL } from '../agent/agent-mode.js';
8
- import { formatRetryChip } from '../agent/retry-view.js';
9
- import { FAST_LIMITS } from '../fast/limits.js';
10
- import { COMPACTION_LIMITS } from '../compaction/limits.js';
11
- import { TEAM_LIMITS } from '../team/limits.js';
12
- import { TODO_LIMITS } from '../todo/limits.js';
13
- import { PROC_LIMITS } from '../proc/limits.js';
14
- import { QueueStatusRow } from './QueueStatusRow.js';
15
- /** Below this the `eco` chip costs the context gauge more than it is worth. */
16
- const ECO_MIN_COLS = 72;
17
- /**
18
- * Where `86k/200k` starts being shown (context-usage-gauge-accuracy §3.6 / W5).
19
- *
20
- * IT TAKES THE COLUMNS THE SESSION TOTAL USED TO HAVE, and the ordering is the
21
- * point rather than a layout accident (D-6). "How full am I, out of how much" is
22
- * the question a user asks the gauge; "what has this session cost in tokens" is
23
- * a different one, and putting the second in front of the first spends the
24
- * scarce columns on the number the user is more likely to MISREAD - the total
25
- * looks like an occupancy until you notice it exceeds the window.
26
- */
27
- const ABSOLUTE_PAIR_MIN_COLS = 72;
28
- /**
29
- * Where the session total returns, now prefixed.
30
- *
31
- * `total ` IS AN ASCII WORD, NOT A SIGMA. `ui/**` is inside the glyph scanner's
32
- * scope, so a new symbol would need an entry in `glyphs.ts` with an ASCII
33
- * fallback for a legacy `cmd.exe` - a real cost for a three-character prefix
34
- * that reads worse.
35
- */
36
- const SESSION_TOTAL_MIN_COLS = 96;
37
- /**
38
- * I-5 — DO NOT "CLEAN THIS UP". Ink short-circuits identical output at TWO
39
- * gates (`ink.js:132` and `log-update.js:13`), so a fixed frame whose React
40
- * output has not changed is never repainted. `Ctrl+L` therefore cannot work by
41
- * writing an escape sequence; it works by making the rendered STRING differ.
42
- * This alternates the status bar's left gutter between U+0020 and U+00A0: both
43
- * render one invisible column, so there is zero visual or layout difference,
44
- * but the bytes differ and both gates open.
45
- *
46
- * It must sit at the START of the line: Ink `trimEnd()`s every row (output.js),
47
- * and JS `trimEnd` strips U+00A0 too — a trailing carrier would be deleted and
48
- * Ctrl+L would degrade to a silent no-op.
49
- */
50
- /** Built by code point so no formatter can normalize it back to a plain space. */
51
- const NBSP = String.fromCharCode(0x00a0);
52
- function redrawChar(nonce) {
53
- return nonce % 2 === 0 ? ' ' : NBSP;
54
- }
55
- function queueStateLabels(props, compact) {
56
- const mode = props.agentMode ?? 'build';
57
- const next = props.pendingAgentMode;
58
- const modeLabel = compact ? mode[0].toUpperCase() : mode;
59
- const nextLabel = next ? (compact ? next[0].toUpperCase() : next) : '';
60
- const stopping = /force|again/i.test(props.interruptHint ?? '');
61
- return [
62
- nextLabel ? `${modeLabel}>${nextLabel}` : modeLabel,
63
- stopping ? (compact ? 'E!' : 'Esc!')
64
- : props.status === 'running' ? (compact ? 'E2' : 'Esc x2') : '',
65
- props.servicesActive?.live ? (compact ? 'S+' : `svc ${props.servicesActive.live}`) : '',
66
- props.todoActive?.total ? (compact ? 'T+'
67
- : `todo ${props.todoActive.done}/${props.todoActive.total}`) : '',
68
- ].filter(Boolean).join(' ') + ' ';
69
- }
70
- function QueueStatusBar(props) {
71
- const pending = props.pendingSteering;
72
- const normal = queueStateLabels(props, false);
73
- const paused = props.status !== 'running';
74
- const suffix = pending.length > 1 ? ` (+${pending.length - 1} more)` : '';
75
- const minimum = stringWidth((paused ? 'Queue (paused): ' : 'Queue: ') + suffix)
76
- + stringWidth(pickGlyphs(props.caps).ellipsis);
77
- const compact = stringWidth(normal) + minimum + 1 > props.columns;
78
- const labels = compact ? queueStateLabels(props, true) : normal;
79
- const columns = props.columns - 1 - stringWidth(labels);
80
- return _jsxs(Box, { width: props.columns, height: 1, flexShrink: 0, children: [_jsx(Text, { children: redrawChar(props.redrawNonce ?? 0) }), _jsx(Text, { color: props.theme.noticeWarn, children: labels }), _jsx(QueueStatusRow, { pending: pending, paused: paused, columns: columns, compact: compact, theme: props.theme, caps: props.caps })] });
81
- }
6
+ import { liveSpinner } from './ActivityLine.js';
7
+ import { planStatusFields } from './layout/status-layout.js';
8
+ /** An invisible leading byte change forces Ink to repaint on Ctrl+L. */
9
+ const redrawChar = (nonce) => nonce % 2 ? String.fromCharCode(0x00a0) : ' ';
82
10
  export function StatusBar(props) {
83
- 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;
84
11
  const { stdout } = useStdout();
85
12
  const cols = stdout?.columns ?? 80;
86
- const running = status === 'running';
87
- if (props.pendingSteering?.length)
88
- return _jsx(QueueStatusBar, { ...props, columns: cols });
89
- const glyphs = pickGlyphs(caps);
90
- // THE PERCENTAGE IS NOT RECOMPUTED HERE ANY MORE. It arrives already divided
91
- // and clamped by the same code the compaction trigger reads, which is the
92
- // whole of AC-2: a user watching 78 % and a `/compact status` reporting 91 %
93
- // would rightly conclude one of them is lying.
94
- const pct = context.pct;
95
- // `gaugeMarks` is spread rather than passed as `undefined`, so a session with
96
- // compaction off takes `buildGauge`'s own default and the bar is byte-identical
97
- // to the pre-feature build.
98
- const gauge = gaugeMarks
99
- ? buildGauge(pct, cols < 72 ? 8 : 12, theme, caps, gaugeMarks)
100
- : buildGauge(pct, cols < 72 ? 8 : 12, theme, caps);
101
- // `~` BELONGS TO THE NUMERATOR, `?` TO THE DENOMINATOR (P2-7). One glyph used
102
- // to carry three meanings - the window is a guess, the occupancy is a guess,
103
- // the occupancy contains an appended-message estimate - and a user could not
104
- // tell from it which half to distrust. The `~` keeps every case it had: an
105
- // unknown window still produces it, so a session that never touches the new
106
- // absolute pair sees the identical string it saw before.
107
- const occupancyApproximate = context.source === 'estimate' || context.deltaTokens > 0;
108
- const pctLabel = `${context.windowKnown && !occupancyApproximate ? '' : '~'}${gauge.pct}%`;
109
- // `?` ON THE WINDOW, AND ONLY ON THE NEW READOUT. `windowKnown` is false
110
- // exactly when `buildRuntimeModel` invented the 128k placeholder, and saying
111
- // `128k?` is the difference between a denominator the user can act on and one
112
- // they will assume came from their model card.
113
- const windowLabel = `${formatTokens(context.window)}${context.windowKnown ? '' : '?'}`;
114
- const absolutePair = `${formatTokens(context.occupied)}/${windowLabel}`;
115
- // THE PROMPT-SIDE TOTAL, NOT `inputTokens` (P1-3 / AC-11). Anthropic reports
116
- // `input_tokens` EXCLUDING cached tokens, so the old expression under-reported
117
- // for anyone behind a caching gateway - on the same row as a `$` that has
118
- // always priced the cache. Any session with cache hits sees this number grow,
119
- // and that is the fix rather than a regression.
120
- const tokens = `total ${formatTokens(promptTokensOf(usageTotal))}${glyphs.arrowUp} ${formatTokens(usageTotal.outputTokens)}${glyphs.arrowDown}`;
121
- const statusGlyph = running ? theme.symbols.toolRunning : theme.symbols.toolPending;
122
- // Nothing at all in build mode with nothing pending: the default posture is
123
- // not news, and a permanent word here would be furniture.
124
- const modeWord = agentMode === 'build' && !pendingAgentMode
125
- ? ''
126
- : pendingAgentMode
127
- ? `${MODE_LABEL[agentMode]} ${glyphs.arrowRight} ${MODE_LABEL[pendingAgentMode]}`
128
- : MODE_LABEL[agentMode];
129
- const actionHints = [running ? (props.interruptHint ?? 'Esc abort') : '',
130
- servicesActive ? `Ctrl+C stop ${servicesActive.live}` : '',
131
- scrolledLines > 0 ? 'PgDn down' : ''].filter(Boolean).join(' | ');
132
- const hintCols = actionHints ? Math.min(Math.max(0, cols - 1), stringWidth(actionHints) + 2) : 0;
133
- const detailCols = Math.max(0, cols - 1 - hintCols);
134
- return (
135
- // `flexShrink={0}` on the left cluster is not cosmetic. When the two clusters
136
- // over-subscribe the row, yoga shrinks the flexible children and Ink's text
137
- // measurement drops a character from EACH of them — the bar renders "idl"
138
- // and "? hel" rather than truncating cleanly at one end. Pinning the left
139
- // cluster keeps the run status and model intact and makes the degradation
140
- // land in one predictable place.
141
- _jsxs(Box, { width: cols, flexDirection: "row", justifyContent: "space-between", flexShrink: 0, height: 1, overflow: "hidden", children: [_jsx(Box, { width: 1, flexShrink: 0, children: _jsx(Text, { children: redrawChar(redrawNonce) }) }), hintCols > 0 && (_jsx(Box, { width: hintCols, flexShrink: 0, overflow: "hidden", children: _jsxs(Text, { wrap: "truncate", color: theme.noticeWarn, children: [actionHints, ' '] }) })), _jsxs(Box, { display: detailCols > 0 ? 'flex' : 'none', width: detailCols, flexShrink: 0, flexDirection: "row", justifyContent: "space-between", overflow: "hidden", children: [_jsxs(Box, { flexDirection: "row", flexShrink: 0, overflow: "hidden", children: [_jsxs(Text, { color: running ? theme.toolRunning : theme.toolDone, children: [statusGlyph, " ", running ? 'running' : 'idle'] }), modeWord && (_jsxs(Text, { color: theme.accent, bold: true, children: [' ', modeWord] })), teamActive && (_jsxs(Text, { color: theme.toolRunning, bold: true, children: [' ', cols >= TEAM_LIMITS.statusCompactCols
142
- ? `agents ${teamActive.running}/${teamActive.total}`
143
- : `[${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
144
- ? `todo ${todoActive.done}/${todoActive.total}`
145
- : `[${todoActive.done}/${todoActive.total}]`] })), servicesActive && (_jsxs(Text, { color: theme.accent, children: [' ', cols >= PROC_LIMITS.statusCompactCols
146
- ? `svc ${servicesActive.live}`
147
- : `[${servicesActive.live}]`] })), cols >= 60 && (cols >= 110 || (!running && !servicesActive && scrolledLines === 0)) && (_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)] }))] })] })] }));
13
+ const phase = props.runPhase ?? (props.status === 'running' ? 'waiting' : 'idle');
14
+ const active = phase !== 'idle' || !!props.compactionActive?.inFlight;
15
+ const glyph = pickGlyphs(props.caps).spinnerStill;
16
+ const spinner = active ? liveSpinner(props.reducedMotion ?? false, props.caps) : null;
17
+ const fields = planStatusFields({
18
+ columns: cols, phase, pendingCount: props.pendingSteering?.length ?? 0,
19
+ context: props.context, usageTotal: props.usageTotal,
20
+ thinkingLevel: props.thinkingLevel, elapsedMs: props.elapsedMs, tokPerSec: props.tokPerSec,
21
+ activeTool: props.activeTool, runOutcome: props.runOutcome, spinner: active ? glyph : '',
22
+ waitingForConfirmation: props.waitingForConfirmation, stopping: props.stopping,
23
+ compacting: props.compactionActive?.inFlight, retryActive: props.retryActive,
24
+ mode: props.agentMode, pendingMode: props.pendingAgentMode, servicesActive: props.servicesActive,
25
+ teamActive: props.teamActive, todoActive: props.todoActive,
26
+ ecoRung: props.ecoRung, fastActive: props.fastActive, scrolledLines: props.scrolledLines,
27
+ });
28
+ const tones = { normal: props.theme.primary, muted: props.theme.muted,
29
+ warning: props.theme.noticeWarn, error: props.theme.noticeError };
30
+ return _jsxs(Box, { width: cols, height: 1, flexShrink: 0, flexDirection: "row", children: [_jsx(Box, { width: 1, flexShrink: 0, children: _jsx(Text, { children: redrawChar(props.redrawNonce ?? 0) }) }), fields.map((item, index) => _jsxs(React.Fragment, { children: [index > 0 && _jsx(Box, { width: 3, flexShrink: 0, children: _jsx(Text, { color: props.theme.muted, children: " | " }) }), _jsx(Box, { width: item.cells, flexShrink: 0, children: _jsx(Text, { color: tones[item.tone], children: item.id === 'phase' && active
31
+ ? _jsxs(_Fragment, { children: [spinner ?? glyph, item.text.slice(glyph.length)] })
32
+ : item.text }) })] }, item.id))] });
148
33
  }
@@ -0,0 +1,29 @@
1
+ import { type ClipboardOptions, type CopyResult } from './clipboard.js';
2
+ export interface CopyRequest {
3
+ text: string;
4
+ lines: number;
5
+ }
6
+ export type CopyRequestResult = CopyResult | {
7
+ status: 'busy';
8
+ };
9
+ export interface ClipboardState {
10
+ busy: boolean;
11
+ cleanupPending: boolean;
12
+ }
13
+ export interface ClipboardCoordinatorOptions {
14
+ clipboard?: ClipboardOptions;
15
+ /** Clears the previous Ctrl+C exit preparation for both copy entry points. */
16
+ onRequest?: () => void;
17
+ /** A repeated request is consumed; its selection must remain untouched. */
18
+ onBusy?: (state: ClipboardState) => void;
19
+ onResult?: (result: CopyResult, request: CopyRequest) => void;
20
+ onStateChange?: (state: ClipboardState) => void;
21
+ }
22
+ export interface ClipboardCoordinator {
23
+ requestCopy(request: CopyRequest): Promise<CopyRequestResult>;
24
+ isBusy(): boolean;
25
+ isCleanupPending(): boolean;
26
+ dispose(): void;
27
+ }
28
+ /** Create the App-owned copy port; use its live busy state before taking a selection. */
29
+ export declare function createClipboardCoordinator(options?: ClipboardCoordinatorOptions): ClipboardCoordinator;
@@ -0,0 +1,66 @@
1
+ import { startClipboardTask, } from './clipboard.js';
2
+ class SharedClipboardCoordinator {
3
+ options;
4
+ busy = false;
5
+ cleanupPending = false;
6
+ disposed = false;
7
+ abort = new AbortController();
8
+ constructor(options) {
9
+ this.options = options;
10
+ options.clipboard?.signal?.addEventListener('abort', this.dispose, { once: true });
11
+ if (options.clipboard?.signal?.aborted)
12
+ this.dispose();
13
+ }
14
+ isBusy() { return this.busy; }
15
+ isCleanupPending() { return this.cleanupPending; }
16
+ state() {
17
+ return { busy: this.busy, cleanupPending: this.cleanupPending };
18
+ }
19
+ notifyState() {
20
+ if (!this.disposed)
21
+ this.options.onStateChange?.(this.state());
22
+ }
23
+ requestCopy = (request) => {
24
+ if (this.disposed)
25
+ return Promise.resolve({ status: 'failed', reason: 'cancelled' });
26
+ if (this.busy) {
27
+ this.options.onRequest?.();
28
+ this.options.onBusy?.(this.state());
29
+ return Promise.resolve({ status: 'busy' });
30
+ }
31
+ // Claim synchronously, before callbacks or Promise work can re-enter this port.
32
+ this.busy = true;
33
+ this.options.onRequest?.();
34
+ this.notifyState();
35
+ const snapshot = { ...request };
36
+ const task = startClipboardTask(snapshot.text, {
37
+ ...this.options.clipboard, signal: this.abort.signal,
38
+ });
39
+ const result = task.result.then((value) => {
40
+ if (!this.disposed) {
41
+ this.cleanupPending = this.busy && value.status === 'failed';
42
+ this.notifyState();
43
+ this.options.onResult?.(value, snapshot);
44
+ }
45
+ return value;
46
+ });
47
+ // Failure feedback is not a release signal: the old native writer may still live.
48
+ void task.released.then(() => {
49
+ this.busy = false;
50
+ this.cleanupPending = false;
51
+ this.notifyState();
52
+ });
53
+ return result;
54
+ };
55
+ dispose = () => {
56
+ if (this.disposed)
57
+ return;
58
+ this.disposed = true;
59
+ this.options.clipboard?.signal?.removeEventListener('abort', this.dispose);
60
+ this.abort.abort();
61
+ };
62
+ }
63
+ /** Create the App-owned copy port; use its live busy state before taking a selection. */
64
+ export function createClipboardCoordinator(options = {}) {
65
+ return new SharedClipboardCoordinator(options);
66
+ }
@@ -1,52 +1,31 @@
1
- /**
2
- * Clipboard (tui-selection-and-scroll-follow §4.4.5) — the SINGLE clipboard path
3
- * for the package. `/copy` and the selection release both come through here.
4
- *
5
- * Two mechanisms, both attempted, and the return value names the better one so
6
- * the toast can say what was SENT rather than claiming a success neither one can
7
- * confirm:
8
- *
9
- * 1. **OSC 52.** It reaches the clipboard of the machine the USER is sitting
10
- * at, which is the only correct target over SSH, and it is the only mechanism
11
- * that works with no helper binary installed. `/copy` gained SSH support the
12
- * moment it moved onto this module.
13
- * 2. **The platform binary** (`clip` / `pbcopy` / `xclip`), moved here unchanged
14
- * from `commands/builtins.ts`, for the local case and for terminals that
15
- * ignore OSC 52 (tmux without `set -g set-clipboard on`, R-7).
16
- *
17
- * IT TAKES A WRITE *DOOR*, NOT A STREAM, and that is P1-6 rather than taste. In
18
- * full-screen mode stdout is a `Proxy` in front of the frame differ; an
19
- * unrecognised chunk there takes `passThrough(true)`, which increments
20
- * `fallbacks` and — on the first one — raises `onFirstFallback`, wired to
21
- * `console.warn(FRAME_FALLBACK_NOTICE)`. Sending OSC 52 through the proxy would
22
- * therefore print a diagnostic at a user who did nothing wrong the first time
23
- * they copied anything, and would permanently corrupt a counter documented to
24
- * mean "something wrote to stdout behind Ink's back". The caller supplies
25
- * `writeForeign` instead, and this module cannot pick the wrong door.
26
- */
27
- export type CopyVia = 'osc52' | 'native' | 'none';
28
- /**
29
- * Ceiling on an OSC 52 payload, in bytes of the ORIGINAL text.
30
- *
31
- * xterm's default limit is around 100 000 BASE64 characters and tmux / screen
32
- * are stricter, so 56 000 source bytes (~74 700 base64) stays inside all three
33
- * with room to spare. Past it the sequence is skipped rather than truncated: a
34
- * terminal that drops an over-long OSC 52 leaves the clipboard holding whatever
35
- * was there before, and half a selection is worse than none.
36
- */
1
+ export type CopyFailureReason = 'empty' | 'unavailable' | 'write' | 'timeout' | 'too-large' | 'cancelled';
2
+ export type CopyResult = {
3
+ status: 'confirmed';
4
+ via: 'native';
5
+ } | {
6
+ status: 'sent';
7
+ via: 'osc52';
8
+ fallbackReason?: CopyFailureReason;
9
+ } | {
10
+ status: 'failed';
11
+ reason: CopyFailureReason;
12
+ };
13
+ export interface ClipboardOptions {
14
+ /** Frame differ's foreign-write door, never the intercepted stdout stream. */
15
+ write?: (chunk: string) => void;
16
+ remote?: boolean;
17
+ timeoutMs?: number;
18
+ signal?: AbortSignal;
19
+ }
20
+ export interface ClipboardTask {
21
+ result: Promise<CopyResult>;
22
+ /** Resolves only after no writer can change the clipboard again. */
23
+ released: Promise<void>;
24
+ }
37
25
  export declare const MAX_OSC52_BYTES = 56000;
38
- /** `\x1b]52;c;<base64>\x07` — `c` is the CLIPBOARD selection, not PRIMARY. */
26
+ /** Encode the full clipboard selection as UTF-8, without truncating its payload. */
39
27
  export declare function osc52(text: string): string;
40
- /**
41
- * Put `text` on the clipboard and report the best mechanism that was reached.
42
- *
43
- * `write` is the foreign-write door (see the module header). Omit it and OSC 52
44
- * is skipped entirely, which is the correct behaviour for a caller that has no
45
- * terminal to write to.
46
- *
47
- * NEITHER MECHANISM IS DETECTABLE. `spawn` returning without throwing does not
48
- * mean `xclip` exists, and an OSC 52 sequence a terminal ignores is
49
- * indistinguishable from one it honours. The return value therefore names what
50
- * was ATTEMPTED, and every caller's user-facing text is worded accordingly.
51
- */
52
- export declare function copyText(text: string, write?: (chunk: string) => void): CopyVia;
28
+ /** Start a copy without rejecting; result and writer release are separate signals. */
29
+ export declare function startClipboardTask(text: string, options?: ClipboardOptions): ClipboardTask;
30
+ /** Promise convenience wrapper; interactive callers must use the shared coordinator. */
31
+ export declare function copyText(text: string, options?: ClipboardOptions): Promise<CopyResult>;