@aragon-agent/cli 0.6.13 → 0.6.14
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.
- package/CHANGELOG.md +27 -0
- package/README.md +27 -16
- package/dist/agent/controller.d.ts +9 -1
- package/dist/agent/controller.js +62 -12
- package/dist/agent/model-profile-settings.js +11 -2
- package/dist/agent/reducer.js +1 -0
- package/dist/boot/guard.d.ts +3 -3
- package/dist/boot/guard.js +3 -3
- package/dist/cli.js +33 -15
- package/dist/compaction/child.js +4 -0
- package/dist/compaction/compactor.js +8 -5
- package/dist/compaction/context-command.js +5 -3
- package/dist/compaction/meter.js +2 -0
- package/dist/compaction/pressure.d.ts +1 -0
- package/dist/compaction/pressure.js +1 -0
- package/dist/compaction/types.d.ts +2 -0
- package/dist/compaction/wiring.js +4 -1
- package/dist/config/load.d.ts +3 -1
- package/dist/config/load.js +8 -4
- package/dist/config/schema.d.ts +2 -2
- package/dist/ui/App.js +3 -10
- package/dist/ui/Logo.d.ts +3 -5
- package/dist/ui/Logo.js +4 -8
- package/dist/ui/PromptInput.js +3 -1
- package/dist/ui/capabilities.d.ts +2 -0
- package/dist/ui/capabilities.js +6 -0
- package/dist/ui/color-runtime.d.ts +5 -0
- package/dist/ui/color-runtime.js +29 -0
- package/dist/ui/glyphs.js +10 -8
- package/dist/ui/ink-runtime.d.ts +3 -0
- package/dist/ui/ink-runtime.js +30 -0
- package/dist/ui/layout/status-layout.js +3 -2
- package/dist/ui/render-cache.d.ts +2 -2
- package/dist/ui/render-cache.js +4 -3
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,33 @@ were written.
|
|
|
7
7
|
|
|
8
8
|
## Unreleased
|
|
9
9
|
|
|
10
|
+
### Consistent TUI styling
|
|
11
|
+
|
|
12
|
+
- Interactive sessions always use Unicode and default to truecolor, including
|
|
13
|
+
PowerShell sessions without terminal or UTF locale markers. Ink's actual color
|
|
14
|
+
output follows the same policy, with renderer state restored on exit.
|
|
15
|
+
- Explicit color opt-outs keep Unicode borders and icons; the opening logo now
|
|
16
|
+
depends only on available space. Headless terminal detection is unchanged.
|
|
17
|
+
- Reloading model settings preserves session display preferences. Syntax
|
|
18
|
+
highlighting follows the same color policy, and status icons use consistent
|
|
19
|
+
single-column glyphs to prevent narrow-frame overflow.
|
|
20
|
+
- Hiding the editor caret while scrolling preserves draft geometry, preventing
|
|
21
|
+
a layout update loop when entering text above the full startup logo.
|
|
22
|
+
- Register exit cleanup before mounting the application, so immediate exits and
|
|
23
|
+
initial render failures restore terminal and color state reliably. Failed
|
|
24
|
+
mounts no longer mark an auto-installed build as healthy.
|
|
25
|
+
|
|
26
|
+
### Model context window
|
|
27
|
+
|
|
28
|
+
- Discover context limits at startup and after connection changes without blocking
|
|
29
|
+
input. The status bar and compaction use the discovered limit; `/context` names
|
|
30
|
+
API versus catalog metadata. Unknown windows show `?`, not a fabricated 128k
|
|
31
|
+
capacity or percentage. Manual `contextWindow` overrides retain precedence.
|
|
32
|
+
- Window overrides now apply to tail relief and compaction progress as well as
|
|
33
|
+
the gauge. Workers on different models/endpoints do not inherit the lead's
|
|
34
|
+
override. API-key changes refresh metadata, and ending a session cancels its
|
|
35
|
+
pending model queries. `/context` also hides unknown capacities/percentages.
|
|
36
|
+
|
|
10
37
|
### TUI 输入与排队可靠性
|
|
11
38
|
|
|
12
39
|
- Shift+Enter 与粘贴按输入顺序处理;普通 Enter 保持弹层兼容,混合输入最多提交一次,
|
package/README.md
CHANGED
|
@@ -1316,9 +1316,9 @@ The readouts drop as the terminal narrows, in that order of priority:
|
|
|
1316
1316
|
either no completed turn has reported usage yet (right after `/resume`, or
|
|
1317
1317
|
right after a compaction), or tool results have been appended since the last
|
|
1318
1318
|
measurement and their size is estimated.
|
|
1319
|
-
- `?`
|
|
1320
|
-
|
|
1321
|
-
`contextWindow` below.
|
|
1319
|
+
- `?` replaces the **window** when neither the API nor the catalog provides a
|
|
1320
|
+
limit. No percentage is shown in that case. The internal 128 000-token fallback
|
|
1321
|
+
remains a compaction budget, not a claim about model capacity. See `contextWindow` below.
|
|
1322
1322
|
|
|
1323
1323
|
**The percentage keeps moving during a turn.** Every tool result nudges it,
|
|
1324
1324
|
whether or not compaction is enabled — it is not a compaction feature, and a
|
|
@@ -1343,7 +1343,7 @@ Context
|
|
|
1343
1343
|
```
|
|
1344
1344
|
|
|
1345
1345
|
Two lines are worth reading closely. **`Window`** always names where the number
|
|
1346
|
-
came from — the model table, your own `contextWindow`, or the invented
|
|
1346
|
+
came from — the model API, the model table, your own `contextWindow`, or the invented
|
|
1347
1347
|
placeholder — because those are indistinguishable everywhere else. And
|
|
1348
1348
|
**`Compaction`** reports what is actually true of this session, including
|
|
1349
1349
|
`off - not registered for this session (started with --no-compaction)`; it never
|
|
@@ -1351,13 +1351,21 @@ promises a threshold that nothing will act on.
|
|
|
1351
1351
|
|
|
1352
1352
|
### `contextWindow`
|
|
1353
1353
|
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1354
|
+
Window resolution uses your explicit override first, then API metadata from the
|
|
1355
|
+
configured endpoint, then the built-in catalog (including known aliases and dated
|
|
1356
|
+
snapshots). Startup, connection changes, and subsequent prompts refresh discovery
|
|
1357
|
+
in the background, with a five-minute cache. OpenAI-compatible endpoints may expose
|
|
1358
|
+
`context_length` or `context_window`; Anthropic uses `max_input_tokens`; Google uses
|
|
1359
|
+
`inputTokenLimit`. An ID-only models response cannot establish the real limit.
|
|
1360
|
+
Google/Anthropic lists are paginated (up to 20 pages within one 15-second deadline).
|
|
1361
|
+
Changing the connection or ending the session cancels its pending lookup.
|
|
1362
|
+
|
|
1363
|
+
If neither the endpoint nor the catalog knows your model, the status bar shows `?`.
|
|
1364
|
+
Set a verified provider limit explicitly:
|
|
1357
1365
|
|
|
1358
1366
|
```bash
|
|
1359
1367
|
aragon config set contextWindow 1000000 # persisted; prints "Set contextWindow = 1000000"
|
|
1360
|
-
aragon config set contextWindow auto # back to
|
|
1368
|
+
aragon config set contextWindow auto # back to API discovery / catalog
|
|
1361
1369
|
ARAGON_CONTEXT_WINDOW=1000000 aragon # one session
|
|
1362
1370
|
```
|
|
1363
1371
|
|
|
@@ -1890,7 +1898,7 @@ env / `.env` → CLI flags**.
|
|
|
1890
1898
|
| Config key | Default | Meaning |
|
|
1891
1899
|
| --- | --- | --- |
|
|
1892
1900
|
| `maxTokens` | `64000` | Output token cap, clamped to `[256, 200000]`. `null` means **auto** — use each model's own ceiling, never above 64000. Absent means the default. See [Output token limits](#output-token-limits). |
|
|
1893
|
-
| `contextWindow` | `null` | The window the context gauge measures against, clamped to `[8000, 5000000]`. `null` means **auto** — the built-in
|
|
1901
|
+
| `contextWindow` | `null` | The window the context gauge measures against, clamped to `[8000, 5000000]`. `null` means **auto** — API metadata, then the built-in catalog. Unknown windows show `?`; the internal fallback is 128000. See [The context gauge](#the-context-gauge). |
|
|
1894
1902
|
| `showThinking` | `false` | Draw the reasoning the model returns. `thinkingLevel` is the effort the provider is asked to **spend**; this is whether the terminal **shows** it. Off by default: a settled turn that thought leaves one muted `thought for 12s` row in its place, so nothing is hidden silently. |
|
|
1895
1903
|
| `liveToolOutput` | `true` | Draw up to eight sanitised rows of a **running** tool's output on its card, plus a `no output for Ns` row when the child goes quiet. On by default, unlike `showThinking`: this ADDS the information a long `bash` call otherwise hides, and its cost is bounded by construction — eight rows per call, sixteen calls, whatever the command emits. Turn it off and the card is a single `running` row again, with no store allocated and no recorder attached. |
|
|
1896
1904
|
| `exitTranscript` | `true` | Replay a plain-text session summary after exiting (full-screen only). |
|
|
@@ -1968,7 +1976,7 @@ aragon config set maxTokens 32000 # persisted; prints "Set maxTokens = 32000"
|
|
|
1968
1976
|
aragon config set maxTokens auto # persisted as null
|
|
1969
1977
|
aragon config set maxTokens default # back to 64000
|
|
1970
1978
|
aragon config set contextWindow 1000000 # the gauge's denominator
|
|
1971
|
-
aragon config set contextWindow auto # back to
|
|
1979
|
+
aragon config set contextWindow auto # back to API discovery / catalog
|
|
1972
1980
|
aragon config get maxTokens # "64000" | "auto"
|
|
1973
1981
|
```
|
|
1974
1982
|
|
|
@@ -2272,12 +2280,15 @@ quiet output is required.
|
|
|
2272
2280
|
`--theme` / `config.theme` / `/theme` pick a palette: `warm`, `cool`, `light`, or
|
|
2273
2281
|
`auto` (which resolves to `warm` — terminals can't reliably report their
|
|
2274
2282
|
background). `dark` remains accepted everywhere as an alias for `cool`.
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2283
|
+
Interactive sessions always use Unicode borders, icons and scrollbars, with
|
|
2284
|
+
truecolor and gradients by default. Missing terminal/locale markers, `TERM=dumb`,
|
|
2285
|
+
and low `FORCE_COLOR` levels do not simplify the TUI. `--no-color`, `NO_COLOR`
|
|
2286
|
+
(including an empty value), or `FORCE_COLOR=0/false` explicitly disable color;
|
|
2287
|
+
Unicode stays enabled. The opening logo adapts only to available space and
|
|
2288
|
+
remains visible in monochrome. `reducedMotion` (also `ARAGON_REDUCED_MOTION`,
|
|
2289
|
+
and implied by disabling color) replaces spinners with a static glyph.
|
|
2290
|
+
Headless commands retain their existing terminal detection. The terminal and
|
|
2291
|
+
font still need to support the characters and colors the TUI emits.
|
|
2281
2292
|
|
|
2282
2293
|
### Timeout invariant
|
|
2283
2294
|
|
|
@@ -123,6 +123,10 @@ export declare class AgentController {
|
|
|
123
123
|
private readonly agent;
|
|
124
124
|
private readonly providerRegistry;
|
|
125
125
|
private readonly modelRegistry;
|
|
126
|
+
private modelMetadataEnabled;
|
|
127
|
+
private modelMetadataGeneration;
|
|
128
|
+
private modelMetadataAbort;
|
|
129
|
+
private disposed;
|
|
126
130
|
private readonly tools;
|
|
127
131
|
private readonly skills;
|
|
128
132
|
private readonly skillsEnabled;
|
|
@@ -608,7 +612,11 @@ export declare class AgentController {
|
|
|
608
612
|
* cost table for the model that actually billed" cannot drift from "the cost
|
|
609
613
|
* table for the session's model" as two separate bodies.
|
|
610
614
|
*/
|
|
611
|
-
getModelInfoFor(ref: Pick<ModelRef, 'providerId' | 'modelId'>): ModelInfo;
|
|
615
|
+
getModelInfoFor(ref: Pick<ModelRef, 'providerId' | 'modelId' | 'baseUrl'>): ModelInfo;
|
|
616
|
+
/** Best-effort metadata lookup; never block typing or a model request. */
|
|
617
|
+
refreshModelMetadata(): Promise<void>;
|
|
618
|
+
/** Invalidate an old connection without restarting a lookup on every prompt. */
|
|
619
|
+
private resetModelMetadataRequest;
|
|
612
620
|
/**
|
|
613
621
|
* Whether the STATIC table knows this model's price (C-11 / RV-4).
|
|
614
622
|
*
|
package/dist/agent/controller.js
CHANGED
|
@@ -51,6 +51,10 @@ export class AgentController {
|
|
|
51
51
|
agent;
|
|
52
52
|
providerRegistry;
|
|
53
53
|
modelRegistry;
|
|
54
|
+
modelMetadataEnabled = false;
|
|
55
|
+
modelMetadataGeneration = 0;
|
|
56
|
+
modelMetadataAbort = new AbortController();
|
|
57
|
+
disposed = false;
|
|
54
58
|
tools;
|
|
55
59
|
skills;
|
|
56
60
|
skillsEnabled;
|
|
@@ -529,15 +533,8 @@ export class AgentController {
|
|
|
529
533
|
getMessages: () => this.agent.state.messages,
|
|
530
534
|
getSystemPrompt: () => this.agent.state.systemPrompt,
|
|
531
535
|
getModelInfo: () => this.getModelInfo(),
|
|
532
|
-
//
|
|
533
|
-
|
|
534
|
-
// back to `buildRuntimeModel` - a 128k placeholder AND a zero cost table -
|
|
535
|
-
// for exactly the models it answers `false` for. The two are the same
|
|
536
|
-
// question about the same static table, not a coincidence.
|
|
537
|
-
isWindowKnown: () => this.isPricedModel({
|
|
538
|
-
providerId: this.config.provider,
|
|
539
|
-
modelId: this.config.model,
|
|
540
|
-
}),
|
|
536
|
+
// Context discovery does not imply that the model's price is known.
|
|
537
|
+
isWindowKnown: () => this.getModelInfo().contextWindowSource !== 'fallback',
|
|
541
538
|
// LIVE, so a settings-screen edit moves the denominator without a relaunch.
|
|
542
539
|
getWindowOverride: () => this.config.contextWindow,
|
|
543
540
|
});
|
|
@@ -863,6 +860,8 @@ export class AgentController {
|
|
|
863
860
|
if (this.modelSettingsBlocked) {
|
|
864
861
|
return { status: 'not-started', reason: 'failed' };
|
|
865
862
|
}
|
|
863
|
+
if (this.modelMetadataEnabled)
|
|
864
|
+
void this.refreshModelMetadata();
|
|
866
865
|
const request = ++this.startupSequence;
|
|
867
866
|
try {
|
|
868
867
|
if (this.agent.state.isRunning) {
|
|
@@ -1257,6 +1256,9 @@ export class AgentController {
|
|
|
1257
1256
|
}
|
|
1258
1257
|
/** Abort every child and clear timers. Idempotent; safe to call twice. */
|
|
1259
1258
|
dispose() {
|
|
1259
|
+
this.disposed = true;
|
|
1260
|
+
this.modelMetadataGeneration += 1;
|
|
1261
|
+
this.modelMetadataAbort.abort();
|
|
1260
1262
|
this.teamRuntime?.dispose();
|
|
1261
1263
|
this.fast?.dispose();
|
|
1262
1264
|
this.compaction?.dispose();
|
|
@@ -1419,6 +1421,12 @@ export class AgentController {
|
|
|
1419
1421
|
this.agent.setModel(snapshot.model);
|
|
1420
1422
|
this.agent.setThinkingLevel(snapshot.config.thinkingLevel);
|
|
1421
1423
|
this.agent.setMaxTokens(snapshot.config.maxTokens);
|
|
1424
|
+
// Credentials may change even when endpoint/model do not. Invalidate
|
|
1425
|
+
// before publishing a window, including when the new profile has no key.
|
|
1426
|
+
if (this.modelMetadataEnabled) {
|
|
1427
|
+
this.resetModelMetadataRequest();
|
|
1428
|
+
this.modelRegistry.clearCache();
|
|
1429
|
+
}
|
|
1422
1430
|
this.fast?.onConfigChanged(false, snapshot.config.fast.enabled);
|
|
1423
1431
|
this.compaction?.onConfigChanged(snapshot.config.compaction.enabled);
|
|
1424
1432
|
this.contextMeter.onWindowChanged();
|
|
@@ -1427,6 +1435,8 @@ export class AgentController {
|
|
|
1427
1435
|
finally {
|
|
1428
1436
|
this.preparedSystemPrompt = undefined;
|
|
1429
1437
|
}
|
|
1438
|
+
if (this.modelMetadataEnabled)
|
|
1439
|
+
void this.refreshModelMetadata();
|
|
1430
1440
|
}
|
|
1431
1441
|
/** Potentially failing ordinary-setting side effects are isolated from model application. */
|
|
1432
1442
|
applyModelSettingsEffects() { getLogger().reconfigure(this.config.log); }
|
|
@@ -1434,12 +1444,15 @@ export class AgentController {
|
|
|
1434
1444
|
blockModelSettingsRequests() {
|
|
1435
1445
|
this.modelSettingsBlocked = true;
|
|
1436
1446
|
this.config.modelSettingsRestartRequired = true;
|
|
1447
|
+
this.resetModelMetadataRequest();
|
|
1437
1448
|
}
|
|
1438
1449
|
/** The UI uses this before accepting or draining queued prompts. */
|
|
1439
1450
|
areModelSettingsBlocked() { return this.modelSettingsBlocked; }
|
|
1440
1451
|
setModel(provider, model, baseUrl) {
|
|
1441
1452
|
if (this.isModelSettingsBusy())
|
|
1442
1453
|
throw new Error('Model settings are busy.');
|
|
1454
|
+
if (this.modelMetadataEnabled)
|
|
1455
|
+
this.resetModelMetadataRequest();
|
|
1443
1456
|
this.settingsRevision += 1;
|
|
1444
1457
|
this.config = { ...this.config, provider, model, baseUrl };
|
|
1445
1458
|
this.config.modelProfileState = resolveModelProfileState(this.config);
|
|
@@ -1462,6 +1475,8 @@ export class AgentController {
|
|
|
1462
1475
|
// numerator's calibration is one turn stale until the next `turn_end`
|
|
1463
1476
|
// (RV-13), which is the trade this call names.
|
|
1464
1477
|
this.contextMeter.onWindowChanged();
|
|
1478
|
+
if (this.modelMetadataEnabled)
|
|
1479
|
+
void this.refreshModelMetadata();
|
|
1465
1480
|
}
|
|
1466
1481
|
setThinkingLevel(level) {
|
|
1467
1482
|
this.settingsRevision += 1;
|
|
@@ -1481,6 +1496,12 @@ export class AgentController {
|
|
|
1481
1496
|
...this.config,
|
|
1482
1497
|
apiKeys: { ...this.config.apiKeys, [provider]: key },
|
|
1483
1498
|
};
|
|
1499
|
+
if (this.modelMetadataEnabled) {
|
|
1500
|
+
this.resetModelMetadataRequest();
|
|
1501
|
+
this.modelRegistry.clearCache(provider);
|
|
1502
|
+
this.contextMeter.onWindowChanged();
|
|
1503
|
+
void this.refreshModelMetadata();
|
|
1504
|
+
}
|
|
1484
1505
|
// Rule 5 of §3.2 reads `apiKeys`, so a key edit is a tier event: this is the
|
|
1485
1506
|
// path that turns `no_key` back into a working tier without a relaunch.
|
|
1486
1507
|
this.fast?.onConfigChanged();
|
|
@@ -1639,9 +1660,38 @@ export class AgentController {
|
|
|
1639
1660
|
*/
|
|
1640
1661
|
getModelInfoFor(ref) {
|
|
1641
1662
|
const found = this.modelRegistry.getModel(ref.providerId, ref.modelId);
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
return
|
|
1663
|
+
const baseUrl = ref.baseUrl ?? (ref.providerId === this.config.provider && ref.modelId === this.config.model
|
|
1664
|
+
? this.config.baseUrl : undefined);
|
|
1665
|
+
return {
|
|
1666
|
+
...(found ?? this.modelRegistry.buildRuntimeModel(ref.providerId, ref.modelId)),
|
|
1667
|
+
...this.modelRegistry.getContextWindow(ref.providerId, ref.modelId, baseUrl),
|
|
1668
|
+
};
|
|
1669
|
+
}
|
|
1670
|
+
/** Best-effort metadata lookup; never block typing or a model request. */
|
|
1671
|
+
async refreshModelMetadata() {
|
|
1672
|
+
if (this.disposed || this.modelSettingsBlocked)
|
|
1673
|
+
return;
|
|
1674
|
+
this.modelMetadataEnabled = true;
|
|
1675
|
+
const generation = ++this.modelMetadataGeneration;
|
|
1676
|
+
const { provider, baseUrl } = this.config;
|
|
1677
|
+
try {
|
|
1678
|
+
const key = this.resolveKey(provider);
|
|
1679
|
+
if (!key)
|
|
1680
|
+
return;
|
|
1681
|
+
await this.modelRegistry.discoverModels(provider, key, baseUrl, this.modelMetadataAbort.signal);
|
|
1682
|
+
if (this.disposed || generation !== this.modelMetadataGeneration)
|
|
1683
|
+
return;
|
|
1684
|
+
this.contextMeter.onWindowChanged();
|
|
1685
|
+
}
|
|
1686
|
+
catch {
|
|
1687
|
+
// Offline or unsupported discovery leaves catalog/unknown metadata intact.
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
/** Invalidate an old connection without restarting a lookup on every prompt. */
|
|
1691
|
+
resetModelMetadataRequest() {
|
|
1692
|
+
this.modelMetadataGeneration += 1;
|
|
1693
|
+
this.modelMetadataAbort.abort();
|
|
1694
|
+
this.modelMetadataAbort = new AbortController();
|
|
1645
1695
|
}
|
|
1646
1696
|
/**
|
|
1647
1697
|
* Whether the STATIC table knows this model's price (C-11 / RV-4).
|
|
@@ -182,8 +182,17 @@ export function reloadModelSettings(controller) {
|
|
|
182
182
|
if (validateModelProfiles(disk.config?.modelProfiles).length)
|
|
183
183
|
return rejected('invalid');
|
|
184
184
|
try {
|
|
185
|
-
const
|
|
186
|
-
resolved
|
|
185
|
+
const live = controller.getConfig();
|
|
186
|
+
const resolved = loadConfig({ cwd: live.cwd });
|
|
187
|
+
resolved.liveToolOutput = live.liveToolOutput;
|
|
188
|
+
// The mounted renderer owns these session preferences. Reloading model
|
|
189
|
+
// settings must not re-detect the shell or discard a startup --no-color.
|
|
190
|
+
resolved.color = live.color;
|
|
191
|
+
if (live.colorLevel !== undefined)
|
|
192
|
+
resolved.colorLevel = live.colorLevel;
|
|
193
|
+
if (live.unicode !== undefined)
|
|
194
|
+
resolved.unicode = live.unicode;
|
|
195
|
+
resolved.reducedMotion ||= !live.color;
|
|
187
196
|
const after = readModelSettingsDisk();
|
|
188
197
|
if (!after.ok)
|
|
189
198
|
return rejected('read_failed');
|
package/dist/agent/reducer.js
CHANGED
|
@@ -1190,6 +1190,7 @@ export function viewReducer(state, action) {
|
|
|
1190
1190
|
prev.source === next.source &&
|
|
1191
1191
|
prev.deltaTokens === next.deltaTokens &&
|
|
1192
1192
|
prev.windowKnown === next.windowKnown &&
|
|
1193
|
+
prev.windowSource === next.windowSource &&
|
|
1193
1194
|
prev.windowOverridden === next.windowOverridden) {
|
|
1194
1195
|
return state;
|
|
1195
1196
|
}
|
package/dist/boot/guard.d.ts
CHANGED
|
@@ -109,9 +109,9 @@ export declare function runBootGuard(deps?: BootGuardDeps): void;
|
|
|
109
109
|
*
|
|
110
110
|
* Called from two places, and both matter:
|
|
111
111
|
*
|
|
112
|
-
* - `cli.tsx::runInteractive`,
|
|
113
|
-
*
|
|
114
|
-
* loaded, config resolved, Ink mounted. This
|
|
112
|
+
* - `cli.tsx::runInteractive`, after mount-time exit promises settle, while
|
|
113
|
+
* the app is still live (or has exited successfully): the module graph
|
|
114
|
+
* loaded, config resolved, Ink mounted successfully. This primary path
|
|
115
115
|
* makes a long session that is later `SIGKILL`ed (terminal window closed,
|
|
116
116
|
* machine slept badly, OOM killer) still count as healthy.
|
|
117
117
|
* - the exit hook installed by `runBootGuard`, when the code is `0` - the
|
package/dist/boot/guard.js
CHANGED
|
@@ -178,9 +178,9 @@ export function runBootGuard(deps = {}) {
|
|
|
178
178
|
*
|
|
179
179
|
* Called from two places, and both matter:
|
|
180
180
|
*
|
|
181
|
-
* - `cli.tsx::runInteractive`,
|
|
182
|
-
*
|
|
183
|
-
* loaded, config resolved, Ink mounted. This
|
|
181
|
+
* - `cli.tsx::runInteractive`, after mount-time exit promises settle, while
|
|
182
|
+
* the app is still live (or has exited successfully): the module graph
|
|
183
|
+
* loaded, config resolved, Ink mounted successfully. This primary path
|
|
184
184
|
* makes a long session that is later `SIGKILL`ed (terminal window closed,
|
|
185
185
|
* machine slept badly, OOM killer) still count as healthy.
|
|
186
186
|
* - the exit hook installed by `runBootGuard`, when the code is `0` - the
|
package/dist/cli.js
CHANGED
|
@@ -16,7 +16,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
16
16
|
import process from 'node:process';
|
|
17
17
|
import '../runtime/insecure-tls-warning.cjs';
|
|
18
18
|
// Must precede every other dependency that imports Ink: CI detection is cached.
|
|
19
|
-
import { render } from './ui/ink-runtime.js';
|
|
19
|
+
import { renderTui as render } from './ui/ink-runtime.js';
|
|
20
20
|
import { Command } from 'commander';
|
|
21
21
|
import { ModelProfileConfigError } from './config/model-profile-store.js';
|
|
22
22
|
import { loadConfig } from './config/load.js';
|
|
@@ -54,7 +54,7 @@ import { supportsWindowsVtInput } from './ui/win-vt-input.js';
|
|
|
54
54
|
import { forceWindowsVtInput } from './ui/win-vt-force.js';
|
|
55
55
|
import { readExitSnapshot } from './ui/exit-snapshot.js';
|
|
56
56
|
import { renderTranscriptText } from './ui/transcript-text.js';
|
|
57
|
-
import {
|
|
57
|
+
import { resolveTuiCapabilities } from './ui/capabilities.js';
|
|
58
58
|
import { pickGlyphs } from './ui/glyphs.js';
|
|
59
59
|
import { getTheme } from './ui/theme.js';
|
|
60
60
|
import { createSelectionController, } from './ui/selection/selection-controller.js';
|
|
@@ -224,7 +224,7 @@ function toFlags(opts) {
|
|
|
224
224
|
* instead of writing to disk unattended.
|
|
225
225
|
*/
|
|
226
226
|
function makeController(flags, opts = { interactive: true }) {
|
|
227
|
-
const config = loadConfig(flags);
|
|
227
|
+
const config = loadConfig(flags, { interactive: opts.interactive });
|
|
228
228
|
// The authoritative log level lands here (§4.4.5). `installLogging()` only
|
|
229
229
|
// resolved a bootstrap level — it ran before commander and before `.env` were
|
|
230
230
|
// readable — and without this hand-off the fully resolved one never applies.
|
|
@@ -283,6 +283,7 @@ function makeController(flags, opts = { interactive: true }) {
|
|
|
283
283
|
*/
|
|
284
284
|
function makeExecController(flags, deps) {
|
|
285
285
|
const { controller } = makeController(flags, { interactive: false, ...deps });
|
|
286
|
+
void controller.refreshModelMetadata();
|
|
286
287
|
const logger = getLogger();
|
|
287
288
|
logger.info('cli', 'run_exec', {});
|
|
288
289
|
logger.onFailure((reason) => process.stderr.write(`[log] ${reason}\n`));
|
|
@@ -388,9 +389,14 @@ function runInteractive(flags, extras = {}) {
|
|
|
388
389
|
return;
|
|
389
390
|
}
|
|
390
391
|
const { controller, confirmBridge, humanInputBridge } = makeController(flags);
|
|
392
|
+
void controller.refreshModelMetadata();
|
|
391
393
|
const config = controller.getConfig();
|
|
394
|
+
const caps = resolveTuiCapabilities({}, config.color);
|
|
392
395
|
const logger = getLogger();
|
|
393
|
-
logger.info('cli', 'run_interactive', {
|
|
396
|
+
logger.info('cli', 'run_interactive', {
|
|
397
|
+
mode: 'fullscreen', provider: config.provider, model: config.model,
|
|
398
|
+
unicode: caps.unicode, colorLevel: caps.colorLevel,
|
|
399
|
+
});
|
|
394
400
|
attachAgentEvents(logger, controller);
|
|
395
401
|
// The team stream is CLI-local (D-10), so `attachAgentEvents` cannot see it.
|
|
396
402
|
// Without this a dispatch leaves NO trace in the log file at all — the wrong
|
|
@@ -548,7 +554,7 @@ function runInteractive(flags, extras = {}) {
|
|
|
548
554
|
return; // Never published (instant exit) — skip, do not throw.
|
|
549
555
|
writeExitTranscript(process.stdout, renderTranscriptText(snapshot.entries, {
|
|
550
556
|
// Same terminal the TUI just left, so the same glyph tier applies.
|
|
551
|
-
glyphs: pickGlyphs(
|
|
557
|
+
glyphs: pickGlyphs(caps),
|
|
552
558
|
usageTotal: snapshot.usageTotal,
|
|
553
559
|
provider: snapshot.provider,
|
|
554
560
|
model: snapshot.model,
|
|
@@ -635,8 +641,8 @@ function runInteractive(flags, extras = {}) {
|
|
|
635
641
|
// the SAME `copyText` door and the SAME bridge `onCopied` toast
|
|
636
642
|
// funnel -- the controller keeps only the selection state machine.
|
|
637
643
|
// Read at PAINT time, not captured: `/theme` rebuilds the theme mid-session.
|
|
638
|
-
theme: () => getTheme(config.theme,
|
|
639
|
-
caps
|
|
644
|
+
theme: () => getTheme(config.theme, caps),
|
|
645
|
+
caps,
|
|
640
646
|
cols: () => process.stdout.columns ?? 80,
|
|
641
647
|
// The STATIC half of the gate. The dynamic half — "no overlay is open" —
|
|
642
648
|
// belongs to `App`, which owns that state and calls `setEnabled`.
|
|
@@ -718,14 +724,15 @@ function runInteractive(flags, extras = {}) {
|
|
|
718
724
|
// every existing path is provably unchanged.
|
|
719
725
|
stdin: filter?.stdin ?? process.stdin,
|
|
720
726
|
stdout: frameObserver.stdout,
|
|
721
|
-
});
|
|
727
|
+
}, caps);
|
|
722
728
|
}
|
|
723
729
|
catch (error) {
|
|
724
730
|
failInteractive(error);
|
|
725
731
|
throw error;
|
|
726
732
|
}
|
|
727
|
-
// H1's primary disarm
|
|
728
|
-
//
|
|
733
|
+
// H1's primary disarm. Let mount-time exit promises settle first: Ink may
|
|
734
|
+
// return from render after its error boundary has already unmounted the app.
|
|
735
|
+
// A live app on the next turn has loaded, resolved config, and mounted
|
|
729
736
|
// (cli-auto-update-hardening section 5.1.4). It is what makes a long session
|
|
730
737
|
// that is later `SIGKILL`ed - terminal window closed, machine slept badly, OOM
|
|
731
738
|
// killer - still count as healthy.
|
|
@@ -735,7 +742,8 @@ function runInteractive(flags, extras = {}) {
|
|
|
735
742
|
// never armed it this is one read and one string compare. Gating it would mean
|
|
736
743
|
// a user who switches `mode` to `off` between the install and the next launch
|
|
737
744
|
// leaves the guard armed with nothing able to disarm it.
|
|
738
|
-
|
|
745
|
+
setImmediate(() => { if (!interactiveClosed)
|
|
746
|
+
markBootHealthy(VERSION); });
|
|
739
747
|
if (updateEligible) {
|
|
740
748
|
// DYNAMIC, and fire-and-forget AFTER `render()` (D-25). Dynamic because a
|
|
741
749
|
// static import would make AC-1 false for `aragon -p`, which shares this
|
|
@@ -778,6 +786,9 @@ function runInteractive(flags, extras = {}) {
|
|
|
778
786
|
.waitUntilExit()
|
|
779
787
|
.then(() => {
|
|
780
788
|
interactiveClosed = true;
|
|
789
|
+
// Also cover a successful immediate exit when launched via dist/cli.js,
|
|
790
|
+
// which bypasses the launcher's code-zero boot-guard backstop.
|
|
791
|
+
markBootHealthy(VERSION);
|
|
781
792
|
screen?.restore();
|
|
782
793
|
// BEFORE `disposeStdinFilter()`, which clears the listener set it subscribed to,
|
|
783
794
|
// and after `restore()` for the reason that call records: the terminal must
|
|
@@ -813,6 +824,7 @@ async function runOneShot(flags, prompt, quiet) {
|
|
|
813
824
|
// install. The gate must know that up front rather than discovering a null
|
|
814
825
|
// handler and defaulting to "yes" (§7.4 / AC-13).
|
|
815
826
|
const { controller } = makeController(flags, { interactive: false });
|
|
827
|
+
void controller.refreshModelMetadata();
|
|
816
828
|
const logger = getLogger();
|
|
817
829
|
logger.info('cli', 'run_oneshot', { quiet });
|
|
818
830
|
// Headless has no frame, so a logging failure can safely reach stderr — the
|
|
@@ -831,10 +843,16 @@ async function runOneShot(flags, prompt, quiet) {
|
|
|
831
843
|
// `followThrough` is read off the REAL controller here rather than through
|
|
832
844
|
// `HeadlessController`, which is deliberately the minimal surface a test can
|
|
833
845
|
// satisfy with an object literal (todo-plan-followthrough §3.6).
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
846
|
+
let code;
|
|
847
|
+
try {
|
|
848
|
+
code = await runHeadless(controller, prompt, {
|
|
849
|
+
quiet,
|
|
850
|
+
followThrough: controller.getTodoConfig().followThrough,
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
finally {
|
|
854
|
+
controller.dispose();
|
|
855
|
+
}
|
|
838
856
|
// Flushed HERE and not inside `runHeadless`: that module is deliberately
|
|
839
857
|
// injectable (its own controller interface, its own stdout/stderr) and has an
|
|
840
858
|
// early `return 2`, so a singleton dependency in there would be both a new
|
package/dist/compaction/child.js
CHANGED
|
@@ -53,11 +53,15 @@ export function createChildContextManager(req, deps) {
|
|
|
53
53
|
*/
|
|
54
54
|
const childConfig = () => {
|
|
55
55
|
const base = deps.getConfig();
|
|
56
|
+
const sameConnection = base.provider === req.model.providerId &&
|
|
57
|
+
base.model === req.model.modelId && base.baseUrl === req.model.baseUrl;
|
|
56
58
|
return {
|
|
57
59
|
...base,
|
|
58
60
|
provider: req.model.providerId,
|
|
59
61
|
model: req.model.modelId,
|
|
60
62
|
baseUrl: req.model.baseUrl,
|
|
63
|
+
// The lead's manual limit describes its connection, not every worker.
|
|
64
|
+
contextWindow: sameConnection ? base.contextWindow : null,
|
|
61
65
|
compaction: {
|
|
62
66
|
...base.compaction,
|
|
63
67
|
keepRecentTurns: COMPACTION_LIMITS.childKeepRecentTurns,
|
|
@@ -468,7 +468,7 @@ export class Compactor {
|
|
|
468
468
|
from,
|
|
469
469
|
systemPrompt,
|
|
470
470
|
budget: tailBudgetTokens({
|
|
471
|
-
contextWindow: this.
|
|
471
|
+
contextWindow: this.meter().current().contextWindow,
|
|
472
472
|
maxOutputTokens: this.effectiveMaxOutputTokens(),
|
|
473
473
|
}),
|
|
474
474
|
log: (event, fields) => this.log.info(event, fields),
|
|
@@ -569,7 +569,7 @@ export class Compactor {
|
|
|
569
569
|
*/
|
|
570
570
|
checkProgress(ctx, uiTrigger, after) {
|
|
571
571
|
const config = this.deps.getConfig().compaction;
|
|
572
|
-
const
|
|
572
|
+
const pressure = this.meter().current();
|
|
573
573
|
// THE OFFSET NOW LIVES ON THE METER (W1), and it is read from the last
|
|
574
574
|
// PUBLISHED pressure rather than re-derived: `Pressure.estimateOffset` is
|
|
575
575
|
// carried on every measurement, so this is the same number the field used to
|
|
@@ -578,8 +578,8 @@ export class Compactor {
|
|
|
578
578
|
const projected = computePressure({
|
|
579
579
|
messages: after,
|
|
580
580
|
systemPrompt: ctx.systemPrompt,
|
|
581
|
-
contextWindow:
|
|
582
|
-
windowKnown:
|
|
581
|
+
contextWindow: pressure.contextWindow,
|
|
582
|
+
windowKnown: pressure.windowKnown,
|
|
583
583
|
...(estimateOffset !== undefined ? { estimateOffset } : {}),
|
|
584
584
|
});
|
|
585
585
|
const target = config.threshold - COMPACTION_LIMITS.minReclaimRatio;
|
|
@@ -792,7 +792,10 @@ export class Compactor {
|
|
|
792
792
|
getMessages: () => this.deps.getMessages(),
|
|
793
793
|
getSystemPrompt: () => this.deps.getSystemPrompt(),
|
|
794
794
|
getModelInfo: () => this.modelInfo(),
|
|
795
|
-
isWindowKnown: () =>
|
|
795
|
+
isWindowKnown: () => {
|
|
796
|
+
const source = this.modelInfo().contextWindowSource;
|
|
797
|
+
return source === undefined ? this.deps.isPricedModel(this.mainRef()) : source !== 'fallback';
|
|
798
|
+
},
|
|
796
799
|
getWindowOverride: () => this.deps.getConfig().contextWindow,
|
|
797
800
|
});
|
|
798
801
|
}
|
|
@@ -69,6 +69,8 @@ function occupancyLine(usage) {
|
|
|
69
69
|
: usage.deltaTokens > 0
|
|
70
70
|
? `[measured ${formatTokens(measured)} + ${formatTokens(usage.deltaTokens)} estimated]`
|
|
71
71
|
: `[measured ${formatTokens(measured)}]`;
|
|
72
|
+
if (!usage.windowKnown)
|
|
73
|
+
return `${formatTokens(usage.occupied)} of ? ${breakdown}`;
|
|
72
74
|
return `${pct} ${formatTokens(usage.occupied)} of ${formatTokens(usage.window)} ${breakdown}`;
|
|
73
75
|
}
|
|
74
76
|
/**
|
|
@@ -83,13 +85,13 @@ function occupancyLine(usage) {
|
|
|
83
85
|
*/
|
|
84
86
|
function windowLine(usage) {
|
|
85
87
|
if (usage.windowOverridden) {
|
|
86
|
-
return `${usage.window} from contextWindow
|
|
88
|
+
return `${usage.window} from contextWindow configuration (overrides API and model table)`;
|
|
87
89
|
}
|
|
88
90
|
if (!usage.windowKnown) {
|
|
89
|
-
return (
|
|
91
|
+
return (`UNKNOWN - no verified model limit; internal fallback budget ${usage.window}; ` +
|
|
90
92
|
'set contextWindow to correct it');
|
|
91
93
|
}
|
|
92
|
-
return `${usage.window} from the model table`;
|
|
94
|
+
return `${usage.window} from ${usage.windowSource === 'api' ? 'the model API' : 'the model table'}`;
|
|
93
95
|
}
|
|
94
96
|
/**
|
|
95
97
|
* How stale the measured half is.
|
package/dist/compaction/meter.js
CHANGED
|
@@ -53,6 +53,7 @@ export const CONTEXT_METER_TICK_MS = 400;
|
|
|
53
53
|
export function toContextUsage(p) {
|
|
54
54
|
return {
|
|
55
55
|
occupied: p.occupied,
|
|
56
|
+
...(p.windowSource ? { windowSource: p.windowSource } : {}),
|
|
56
57
|
window: p.contextWindow,
|
|
57
58
|
// FROM `ratio`, NOT FROM A SECOND DIVISION. `computePressure` already
|
|
58
59
|
// clamped it to [0, 1] and already decided what a zero window means.
|
|
@@ -380,6 +381,7 @@ export class ContextMeter {
|
|
|
380
381
|
messages: this.deps.getMessages(),
|
|
381
382
|
systemPrompt: this.readSystemPrompt(),
|
|
382
383
|
contextWindow: window.contextWindow,
|
|
384
|
+
windowSource: this.deps.getModelInfo().contextWindowSource,
|
|
383
385
|
windowKnown: window.windowKnown,
|
|
384
386
|
windowOverridden: window.windowOverridden,
|
|
385
387
|
...(this.estimateOffset !== undefined ? { estimateOffset: this.estimateOffset } : {}),
|
|
@@ -50,6 +50,7 @@ export declare function occupiedTokens(usage: TokenUsage): number;
|
|
|
50
50
|
*/
|
|
51
51
|
export declare function computeEstimateOffset(usage: TokenUsage, messages: readonly Message[], systemPrompt: string): number;
|
|
52
52
|
export interface PressureInput {
|
|
53
|
+
windowSource?: Pressure['windowSource'];
|
|
53
54
|
/** Authoritative usage of the last completed turn, or `undefined` (§3.4.2). */
|
|
54
55
|
lastUsage?: TokenUsage;
|
|
55
56
|
messages: readonly Message[];
|
|
@@ -133,6 +133,7 @@ export function computePressure(input) {
|
|
|
133
133
|
const ratio = window > 0 ? Math.max(0, Math.min(1, occupied / window)) : 0;
|
|
134
134
|
return {
|
|
135
135
|
occupied,
|
|
136
|
+
...(input.windowSource ? { windowSource: input.windowSource } : {}),
|
|
136
137
|
contextWindow: window,
|
|
137
138
|
ratio,
|
|
138
139
|
headroom: Math.max(0, window - occupied),
|
|
@@ -51,6 +51,7 @@ export type CompactionUiTrigger = 'pressure' | 'overflow' | 'manual';
|
|
|
51
51
|
/** Whether a figure was measured by the provider or estimated by us. */
|
|
52
52
|
export type PressureSource = 'usage' | 'estimate';
|
|
53
53
|
export interface Pressure {
|
|
54
|
+
windowSource?: 'api' | 'catalog' | 'fallback';
|
|
54
55
|
occupied: number;
|
|
55
56
|
contextWindow: number;
|
|
56
57
|
/** `occupied / contextWindow`, clamped to [0, 1]. */
|
|
@@ -115,6 +116,7 @@ export interface Pressure {
|
|
|
115
116
|
* view state, and `toContextUsage` in `meter.ts` is the ONLY converter.
|
|
116
117
|
*/
|
|
117
118
|
export interface ContextUsageSnapshot {
|
|
119
|
+
windowSource?: Pressure['windowSource'];
|
|
118
120
|
/** Occupied tokens (measured, plus an estimate of anything appended since). */
|
|
119
121
|
occupied: number;
|
|
120
122
|
/** The denominator actually used. */
|
|
@@ -135,7 +135,10 @@ export class CompactionWiring {
|
|
|
135
135
|
getMessages: deps.getMessages,
|
|
136
136
|
getSystemPrompt: deps.getSystemPrompt,
|
|
137
137
|
getModelInfo: () => deps.getModelInfoFor(this.mainRef()),
|
|
138
|
-
isWindowKnown: () =>
|
|
138
|
+
isWindowKnown: () => {
|
|
139
|
+
const source = deps.getModelInfoFor(this.mainRef()).contextWindowSource;
|
|
140
|
+
return source === undefined ? deps.isPricedModel(this.mainRef()) : source !== 'fallback';
|
|
141
|
+
},
|
|
139
142
|
getWindowOverride: () => deps.getConfig().contextWindow,
|
|
140
143
|
});
|
|
141
144
|
this.compactor = new Compactor({
|
package/dist/config/load.d.ts
CHANGED
|
@@ -241,7 +241,9 @@ export interface CliFlags {
|
|
|
241
241
|
* Resolve the effective config by merging all layers, then derive the runtime
|
|
242
242
|
* (non-persisted) fields and enforce the timeout invariant.
|
|
243
243
|
*/
|
|
244
|
-
export declare function loadConfig(flags?: CliFlags
|
|
244
|
+
export declare function loadConfig(flags?: CliFlags, options?: {
|
|
245
|
+
interactive?: boolean;
|
|
246
|
+
}): CliConfig;
|
|
245
247
|
/**
|
|
246
248
|
* Build the `getApiKey(providerId)` resolver the core `Agent` consumes.
|
|
247
249
|
* The one-shot `--api-key` override wins, but only for the active provider.
|
package/dist/config/load.js
CHANGED
|
@@ -16,7 +16,7 @@ import { setEntryRetain } from '../agent/entry-limits.js';
|
|
|
16
16
|
import { readSubmitCount } from './ui-state.js';
|
|
17
17
|
import { getLogger } from '../logging/logger.js';
|
|
18
18
|
import { registerSecret, registerSecretsFrom } from '../logging/secret-registry.js';
|
|
19
|
-
import { detectCapabilities } from '../ui/capabilities.js';
|
|
19
|
+
import { detectCapabilities, resolveTuiCapabilities } from '../ui/capabilities.js';
|
|
20
20
|
/** Interpret a boolean-ish env value (`1/true/on/yes`) as `true`. */
|
|
21
21
|
function envFlagTrue(value) {
|
|
22
22
|
if (!value)
|
|
@@ -436,7 +436,7 @@ function resolveContextWindow(env, file) {
|
|
|
436
436
|
* Resolve the effective config by merging all layers, then derive the runtime
|
|
437
437
|
* (non-persisted) fields and enforce the timeout invariant.
|
|
438
438
|
*/
|
|
439
|
-
export function loadConfig(flags = {}) {
|
|
439
|
+
export function loadConfig(flags = {}, options = {}) {
|
|
440
440
|
const cwd = flags.cwd ? flags.cwd : process.cwd();
|
|
441
441
|
// `.env` must be loaded before reading env overrides.
|
|
442
442
|
loadDotenv(cwd);
|
|
@@ -519,13 +519,17 @@ export function loadConfig(flags = {}) {
|
|
|
519
519
|
// registering twice costs a `Set.add`, missing once costs a credential.
|
|
520
520
|
registerSecretsFrom(apiKeys);
|
|
521
521
|
registerSecret(flags.apiKey);
|
|
522
|
-
const
|
|
522
|
+
const caps = options.interactive
|
|
523
|
+
? resolveTuiCapabilities(process.env, flags.color)
|
|
524
|
+
: detectCapabilities(process.env, process.stdout);
|
|
525
|
+
const color = options.interactive
|
|
526
|
+
? caps.colorLevel !== 0
|
|
527
|
+
: flags.color !== undefined ? flags.color : !process.env.NO_COLOR;
|
|
523
528
|
// `--no-color` implies calmer chrome; env / file may also opt in explicitly.
|
|
524
529
|
const reducedMotion = envFlagTrue(process.env.ARAGON_REDUCED_MOTION) ||
|
|
525
530
|
(file.reducedMotion ?? DEFAULT_CONFIG.reducedMotion) ||
|
|
526
531
|
!color;
|
|
527
532
|
// Detect terminal capabilities once; force monochrome when color is disabled.
|
|
528
|
-
const caps = detectCapabilities(process.env, process.stdout);
|
|
529
533
|
const colorLevel = color ? caps.colorLevel : 0;
|
|
530
534
|
// No flag and no env var: a privacy switch you set once, not a per-run knob.
|
|
531
535
|
const historyEnabled = file.historyEnabled ?? DEFAULT_CONFIG.historyEnabled;
|
package/dist/config/schema.d.ts
CHANGED
|
@@ -1097,9 +1097,9 @@ export interface CliConfig {
|
|
|
1097
1097
|
cwd: string;
|
|
1098
1098
|
/** Whether ANSI color is enabled. */
|
|
1099
1099
|
color: boolean;
|
|
1100
|
-
/**
|
|
1100
|
+
/** Resolved color depth: TUI policy or non-interactive terminal detection. */
|
|
1101
1101
|
colorLevel?: 0 | 1 | 2 | 3;
|
|
1102
|
-
/**
|
|
1102
|
+
/** TUI always enables Unicode; non-interactive output uses detection. */
|
|
1103
1103
|
unicode?: boolean;
|
|
1104
1104
|
/** One-shot `--api-key` override; applies to the active provider only. */
|
|
1105
1105
|
apiKeyOverride?: string;
|
package/dist/ui/App.js
CHANGED
|
@@ -8,7 +8,6 @@ import { selectTranscriptEntries, mergePendingEntries } from '../agent/queued-me
|
|
|
8
8
|
* input / toast stack / status bar / overlays.
|
|
9
9
|
*/
|
|
10
10
|
import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react';
|
|
11
|
-
import process from 'node:process';
|
|
12
11
|
import { Text, useApp, useInput, useStdout } from 'ink';
|
|
13
12
|
import { advanceInterruptGesture, cancelInterruptConfirmation, } from '../input/interrupt-gesture.js';
|
|
14
13
|
import { initialViewState, reduceEvent, viewReducer, } from '../agent/reducer.js';
|
|
@@ -22,7 +21,7 @@ import { getLogger } from '../logging/logger.js';
|
|
|
22
21
|
import { isTerminalStatus } from '../proc/types.js';
|
|
23
22
|
import { getSecrets } from '../logging/secret-registry.js';
|
|
24
23
|
import { redactText } from '../logging/redact.js';
|
|
25
|
-
import {
|
|
24
|
+
import { resolveTuiCapabilities } from './capabilities.js';
|
|
26
25
|
import { useStartupNotices, MOUSE_NOTICE_VERSION, VT_INPUT_NOTICE_VERSION, } from './use-startup-notices.js';
|
|
27
26
|
import { getTheme } from './theme.js';
|
|
28
27
|
import { pickGlyphs } from './glyphs.js';
|
|
@@ -206,13 +205,7 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
206
205
|
// the completion list are memoized against it, so a skill installed mid-session
|
|
207
206
|
// gets its `/<name>` command and its autocomplete entry immediately.
|
|
208
207
|
const [skillsNonce, setSkillsNonce] = useState(0);
|
|
209
|
-
const caps = useMemo(() => {
|
|
210
|
-
const detected = detectCapabilities(process.env, stdout);
|
|
211
|
-
return {
|
|
212
|
-
colorLevel: cfg.color === false ? 0 : cfg.colorLevel ?? detected.colorLevel,
|
|
213
|
-
unicode: cfg.unicode ?? detected.unicode,
|
|
214
|
-
};
|
|
215
|
-
}, [cfg.color, cfg.colorLevel, cfg.unicode, stdout]);
|
|
208
|
+
const caps = useMemo(() => resolveTuiCapabilities({}, cfg.color), [cfg.color]);
|
|
216
209
|
const theme = useMemo(() => getTheme(cfg.theme, caps), [cfg.theme, caps]);
|
|
217
210
|
const glyphs = useMemo(() => pickGlyphs(caps), [caps]);
|
|
218
211
|
const reducedMotion = cfg.reducedMotion ?? false;
|
|
@@ -1956,7 +1949,7 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
1956
1949
|
// The wordmark + getting-started card now live INSIDE the viewport, as its
|
|
1957
1950
|
// first block of content, so they scroll away instead of being yanked out of
|
|
1958
1951
|
// the header the moment the first message lands.
|
|
1959
|
-
const opener = empty ? (_jsx(SessionOpener, { variant: pickOpenerVariant(viewportBudget, contentCols
|
|
1952
|
+
const opener = empty ? (_jsx(SessionOpener, { variant: pickOpenerVariant(viewportBudget, contentCols), version: version, cwd: controller.getCwd(), hasKey: hasKey, viewportRows: viewportBudget, theme: theme, caps: caps })) : null;
|
|
1960
1953
|
const density = cfg.density;
|
|
1961
1954
|
const transcriptWindow = cfg.transcriptWindow ?? DEFAULT_TRANSCRIPT_WINDOW;
|
|
1962
1955
|
// The fixed status bar remains visible underneath every overlay and toast.
|
package/dist/ui/Logo.d.ts
CHANGED
|
@@ -19,12 +19,10 @@ export declare function pickHeaderVariant(cols: number): HeaderVariant;
|
|
|
19
19
|
* now spends the viewport's budget, not the frame's: the test has to be against
|
|
20
20
|
* the account being charged.
|
|
21
21
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* for a wordmark in the first place. `banner` still carries the brand there, and
|
|
25
|
-
* the always-present header bar carries it even at `none` (R1).
|
|
22
|
+
* Color preferences must not change geometry: a monochrome TUI gets the same
|
|
23
|
+
* wordmark as a colored one when there is room for it.
|
|
26
24
|
*/
|
|
27
|
-
export declare function pickOpenerVariant(viewportRows: number, cols: number
|
|
25
|
+
export declare function pickOpenerVariant(viewportRows: number, cols: number): OpenerVariant;
|
|
28
26
|
export interface LogoProps {
|
|
29
27
|
/** `art` / `banner` come from `SessionOpener`; `bar` / `mini` from `Header`. */
|
|
30
28
|
variant: HeaderVariant | OpenerVariant;
|
package/dist/ui/Logo.js
CHANGED
|
@@ -32,16 +32,12 @@ export function pickHeaderVariant(cols) {
|
|
|
32
32
|
* now spends the viewport's budget, not the frame's: the test has to be against
|
|
33
33
|
* the account being charged.
|
|
34
34
|
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* for a wordmark in the first place. `banner` still carries the brand there, and
|
|
38
|
-
* the always-present header bar carries it even at `none` (R1).
|
|
35
|
+
* Color preferences must not change geometry: a monochrome TUI gets the same
|
|
36
|
+
* wordmark as a colored one when there is room for it.
|
|
39
37
|
*/
|
|
40
|
-
export function pickOpenerVariant(viewportRows, cols
|
|
38
|
+
export function pickOpenerVariant(viewportRows, cols) {
|
|
41
39
|
if (viewportRows >= ART_MIN_VIEWPORT_ROWS &&
|
|
42
|
-
cols >= ART_MIN_COLS
|
|
43
|
-
caps.unicode &&
|
|
44
|
-
caps.colorLevel >= 2) {
|
|
40
|
+
cols >= ART_MIN_COLS) {
|
|
45
41
|
return 'art';
|
|
46
42
|
}
|
|
47
43
|
if (cols >= BANNER_MIN_COLS)
|
package/dist/ui/PromptInput.js
CHANGED
|
@@ -334,7 +334,9 @@ export function PromptInput({ cols: colsProp, cursorVisible = true, onInteractio
|
|
|
334
334
|
cursor,
|
|
335
335
|
cols: wrapCols,
|
|
336
336
|
maxRows: draftMaxRows(terminalRows),
|
|
337
|
-
|
|
337
|
+
// Scrolling may hide the caret, but must not change the measured draft.
|
|
338
|
+
// Otherwise visibility can toggle wrapping and feed back into scrolling.
|
|
339
|
+
active: isActive,
|
|
338
340
|
});
|
|
339
341
|
// TWO PASSES, AND THE SECOND ONE IS LOAD-BEARING (I-8). The overflow indicator
|
|
340
342
|
// rides the last rendered row INSIDE the box the draft wraps in, so its cells
|
|
@@ -22,4 +22,6 @@ interface StdoutLike {
|
|
|
22
22
|
* for future TTY-aware refinements and kept in the signature for callers.
|
|
23
23
|
*/
|
|
24
24
|
export declare function detectCapabilities(env: EnvLike, _stdout?: StdoutLike): TermCapabilities;
|
|
25
|
+
/** Full TUI styling is a policy, not a guess based on inherited shell markers. */
|
|
26
|
+
export declare function resolveTuiCapabilities(env: EnvLike, color?: boolean): TermCapabilities;
|
|
25
27
|
export {};
|
package/dist/ui/capabilities.js
CHANGED
|
@@ -57,3 +57,9 @@ export function detectCapabilities(env, _stdout) {
|
|
|
57
57
|
unicode: detectUnicode(env),
|
|
58
58
|
};
|
|
59
59
|
}
|
|
60
|
+
/** Full TUI styling is a policy, not a guess based on inherited shell markers. */
|
|
61
|
+
export function resolveTuiCapabilities(env, color) {
|
|
62
|
+
const disabled = color === false || env.NO_COLOR !== undefined ||
|
|
63
|
+
env.FORCE_COLOR === '0' || env.FORCE_COLOR === 'false';
|
|
64
|
+
return { unicode: true, colorLevel: disabled ? 0 : 3 };
|
|
65
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { TermCapabilities } from './capabilities.js';
|
|
2
|
+
/** Highlight cache entries must not cross color preferences between sessions. */
|
|
3
|
+
export declare function syntaxColorLevel(): number;
|
|
4
|
+
/** Apply session colors without changing environment inherited by agent tools. */
|
|
5
|
+
export declare function useTuiColors(level: TermCapabilities['colorLevel']): () => void;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import { pathToFileURL } from 'node:url';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
const require = createRequire(import.meta.url);
|
|
5
|
+
/** Resolve the actual dependency instance even when npm nests its Chalk. */
|
|
6
|
+
async function dependencyChalk(packageName) {
|
|
7
|
+
const dependency = createRequire(require.resolve(packageName));
|
|
8
|
+
return (await import(pathToFileURL(dependency.resolve('chalk')).href)).default;
|
|
9
|
+
}
|
|
10
|
+
const [inkChalk, highlightChalk] = await Promise.all([
|
|
11
|
+
dependencyChalk('ink'), dependencyChalk('cli-highlight'),
|
|
12
|
+
]);
|
|
13
|
+
/** Highlight cache entries must not cross color preferences between sessions. */
|
|
14
|
+
export function syntaxColorLevel() { return highlightChalk.level; }
|
|
15
|
+
/** Apply session colors without changing environment inherited by agent tools. */
|
|
16
|
+
export function useTuiColors(level) {
|
|
17
|
+
const renderers = new Set([chalk, inkChalk, highlightChalk]);
|
|
18
|
+
const previous = new Map([...renderers].map(renderer => [renderer, renderer.level]));
|
|
19
|
+
for (const renderer of renderers)
|
|
20
|
+
renderer.level = level;
|
|
21
|
+
let restored = false;
|
|
22
|
+
return () => {
|
|
23
|
+
if (restored)
|
|
24
|
+
return;
|
|
25
|
+
restored = true;
|
|
26
|
+
for (const [renderer, original] of previous)
|
|
27
|
+
renderer.level = original;
|
|
28
|
+
};
|
|
29
|
+
}
|
package/dist/ui/glyphs.js
CHANGED
|
@@ -24,18 +24,20 @@ const UNICODE_GLYPHS = {
|
|
|
24
24
|
thinking: '✱',
|
|
25
25
|
toolPending: '◦',
|
|
26
26
|
toolRunning: '◍',
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
// U+2714 has conflicting emoji widths in Ink and string-width. U+2713 is
|
|
28
|
+
// consistently one cell, including beside the right-edge scrollbar.
|
|
29
|
+
toolDone: '✓',
|
|
30
|
+
toolError: '×',
|
|
31
|
+
info: 'ⓘ',
|
|
30
32
|
warn: '▲',
|
|
31
|
-
error: '
|
|
33
|
+
error: '×',
|
|
32
34
|
bullet: '•',
|
|
33
35
|
spinnerStill: '·',
|
|
34
36
|
gaugeFull: '█',
|
|
35
37
|
gaugeEmpty: '░',
|
|
36
38
|
keyOn: '●',
|
|
37
39
|
keyOff: '○',
|
|
38
|
-
boxChecked: '
|
|
40
|
+
boxChecked: '☒',
|
|
39
41
|
boxEmpty: '☐',
|
|
40
42
|
wordmark: '◇',
|
|
41
43
|
ellipsis: '…',
|
|
@@ -60,10 +62,10 @@ const UNICODE_GLYPHS = {
|
|
|
60
62
|
maskDot: '•',
|
|
61
63
|
boxStyle: 'round',
|
|
62
64
|
teamAgent: '◆',
|
|
63
|
-
teamMail: '
|
|
65
|
+
teamMail: '⇄',
|
|
64
66
|
todoPending: '○',
|
|
65
67
|
todoActive: '▸',
|
|
66
|
-
todoDone: '
|
|
68
|
+
todoDone: '✓',
|
|
67
69
|
retry: '↻',
|
|
68
70
|
compaction: '⤓',
|
|
69
71
|
queued: '◷',
|
|
@@ -78,7 +80,7 @@ const UNICODE_GLYPHS = {
|
|
|
78
80
|
ask_user: '?',
|
|
79
81
|
submit_plan: '◈',
|
|
80
82
|
task: '◆',
|
|
81
|
-
todo_write: '
|
|
83
|
+
todo_write: '≡',
|
|
82
84
|
},
|
|
83
85
|
toolDefault: '•',
|
|
84
86
|
};
|
package/dist/ui/ink-runtime.d.ts
CHANGED
|
@@ -1 +1,4 @@
|
|
|
1
|
+
import type { TermCapabilities } from './capabilities.js';
|
|
1
2
|
export declare const render: (node: import("react").ReactNode, options?: NodeJS.WriteStream | import("ink").RenderOptions) => import("ink").Instance;
|
|
3
|
+
/** Mount the TUI with its resolved colors, restoring all renderers on exit. */
|
|
4
|
+
export declare function renderTui(node: Parameters<typeof render>[0], options: Parameters<typeof render>[1], caps: TermCapabilities): ReturnType<typeof render>;
|
package/dist/ui/ink-runtime.js
CHANGED
|
@@ -19,3 +19,33 @@ async function loadInk() {
|
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
21
|
export const { render } = await loadInk();
|
|
22
|
+
// Keep asynchronous dependency resolution after Ink's CI initialization. A
|
|
23
|
+
// static import here would let sibling UI modules load Ink before CI is reset.
|
|
24
|
+
const { useTuiColors } = await import('./color-runtime.js');
|
|
25
|
+
/** Mount the TUI with its resolved colors, restoring all renderers on exit. */
|
|
26
|
+
export function renderTui(node, options, caps) {
|
|
27
|
+
const restore = useTuiColors(caps.colorLevel);
|
|
28
|
+
try {
|
|
29
|
+
// Ink creates its exit promise lazily and does not replay earlier exits.
|
|
30
|
+
// Mount the host first, then subscribe before application render or layout
|
|
31
|
+
// effects can fail or call useApp().exit().
|
|
32
|
+
const instance = render(null, options);
|
|
33
|
+
void instance.waitUntilExit().then(restore, restore);
|
|
34
|
+
instance.rerender(node);
|
|
35
|
+
return {
|
|
36
|
+
...instance,
|
|
37
|
+
unmount() {
|
|
38
|
+
try {
|
|
39
|
+
instance.unmount();
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
restore();
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
restore();
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -41,7 +41,8 @@ function contextText(context, detail = false, exponent = false) {
|
|
|
41
41
|
return copy.contextUnknown;
|
|
42
42
|
const number = exponent ? scientific : detail ? formatTokens : compactNumber;
|
|
43
43
|
const approximate = context.source === 'estimate' || context.deltaTokens > 0 ? '~' : '';
|
|
44
|
-
|
|
44
|
+
const limit = context.windowKnown ? number(context.window) : '?';
|
|
45
|
+
return `${copy.context} ${approximate}${number(context.occupied)}/${limit} ${detail ? 'tokens' : 'tok'}`;
|
|
45
46
|
}
|
|
46
47
|
function secondaryFields(input) {
|
|
47
48
|
const list = [];
|
|
@@ -60,7 +61,7 @@ function secondaryFields(input) {
|
|
|
60
61
|
if (input.thinkingLevel !== 'off')
|
|
61
62
|
list.push(field('thinking', `think:${cleanStatusText(input.thinkingLevel)}`));
|
|
62
63
|
list.push(field('usage', `${copy.sessionInput} ${compactNumber(promptTokensOf(input.usageTotal))} ${copy.output} ${compactNumber(input.usageTotal.outputTokens)} tok`));
|
|
63
|
-
if (Number.isFinite(input.context.pct))
|
|
64
|
+
if (input.context.windowKnown && Number.isFinite(input.context.pct))
|
|
64
65
|
list.push(field('percentage', `${input.context.source === 'estimate' || input.context.deltaTokens > 0 || !input.context.windowKnown ? '~' : ''}${Math.round(input.context.pct)}%`));
|
|
65
66
|
if (input.ecoRung)
|
|
66
67
|
list.push(field('eco', 'eco'));
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* - `Markdown` re-split and re-regexed the whole answer into one React element
|
|
13
13
|
* per line on every render.
|
|
14
14
|
*
|
|
15
|
-
*
|
|
15
|
+
* Cached text depends on its input and, for highlighting, the color level. Theme and
|
|
16
16
|
* glyphs are applied at render time and are never cached, because a themed
|
|
17
17
|
* React element in a cache makes `/theme` a no-op until the entry evicts (K-5) —
|
|
18
18
|
* exactly the class of silent bug this package writes comments to avoid.
|
|
@@ -50,7 +50,7 @@ export interface RenderCacheStats {
|
|
|
50
50
|
misses: number;
|
|
51
51
|
}
|
|
52
52
|
/**
|
|
53
|
-
* Syntax-highlight `code`, memoised on `(lang, code)`.
|
|
53
|
+
* Syntax-highlight `code`, memoised on `(colorLevel, lang, code)`.
|
|
54
54
|
*
|
|
55
55
|
* `cli-highlight` emits chalk-level ANSI that does not depend on `theme.*`, so
|
|
56
56
|
* unlike a rendered element this string IS safe to cache across a `/theme`
|
package/dist/ui/render-cache.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* - `Markdown` re-split and re-regexed the whole answer into one React element
|
|
13
13
|
* per line on every render.
|
|
14
14
|
*
|
|
15
|
-
*
|
|
15
|
+
* Cached text depends on its input and, for highlighting, the color level. Theme and
|
|
16
16
|
* glyphs are applied at render time and are never cached, because a themed
|
|
17
17
|
* React element in a cache makes `/theme` a no-op until the entry evicts (K-5) —
|
|
18
18
|
* exactly the class of silent bug this package writes comments to avoid.
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
* caches would miss precisely when a scroll brings an old entry back.
|
|
23
23
|
*/
|
|
24
24
|
import { highlight } from 'cli-highlight';
|
|
25
|
+
import { syntaxColorLevel } from './color-runtime.js';
|
|
25
26
|
import { parseMarkdownBlocks } from './markdown-blocks.js';
|
|
26
27
|
import { pickGlyphs } from './glyphs.js';
|
|
27
28
|
export const RENDER_CACHE_LIMITS = {
|
|
@@ -113,7 +114,7 @@ const markdownCache = new LruCache(RENDER_CACHE_LIMITS.markdownEntries, RENDER_C
|
|
|
113
114
|
*/
|
|
114
115
|
const lineCache = new LruCache(RENDER_CACHE_LIMITS.linesEntries, RENDER_CACHE_LIMITS.markdownBytes);
|
|
115
116
|
/**
|
|
116
|
-
* Syntax-highlight `code`, memoised on `(lang, code)`.
|
|
117
|
+
* Syntax-highlight `code`, memoised on `(colorLevel, lang, code)`.
|
|
117
118
|
*
|
|
118
119
|
* `cli-highlight` emits chalk-level ANSI that does not depend on `theme.*`, so
|
|
119
120
|
* unlike a rendered element this string IS safe to cache across a `/theme`
|
|
@@ -129,7 +130,7 @@ export function highlightCached(code, lang) {
|
|
|
129
130
|
// ripgrep, which silently hides every definition in it from the tool this
|
|
130
131
|
// codebase navigates itself with. NUL rather than a space because a space
|
|
131
132
|
// still lets ("a b", "ts") and ("b", "ts a") collide on one key.
|
|
132
|
-
const key = `${lang}\u0000${code}`;
|
|
133
|
+
const key = `${syntaxColorLevel()}\u0000${lang}\u0000${code}`;
|
|
133
134
|
const hit = highlightCache.get(key);
|
|
134
135
|
if (hit !== undefined)
|
|
135
136
|
return hit;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aragon-agent/cli",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.14",
|
|
4
4
|
"description": "AragonAgent CLI — a Claude-Code / Codex-style interactive terminal UI (TUI) on top of the @aragon-agent/core engine. Installable and runnable via npm / npx.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "AragonAgent contributors",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"prepublishOnly": "npm run build"
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
|
-
"@aragon-agent/core": "^0.2.
|
|
59
|
+
"@aragon-agent/core": "^0.2.26",
|
|
60
60
|
"chalk": "^5.6.2",
|
|
61
61
|
"cli-highlight": "^2.0.0",
|
|
62
62
|
"commander": "^12.1.0",
|