@aragon-agent/cli 0.6.12 → 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 +108 -31
- package/dist/agent/controller.d.ts +38 -2
- package/dist/agent/controller.js +185 -34
- package/dist/agent/model-profile-settings.d.ts +31 -0
- package/dist/agent/model-profile-settings.js +208 -0
- package/dist/agent/queued-messages.d.ts +12 -0
- package/dist/agent/queued-messages.js +18 -0
- package/dist/agent/reducer.d.ts +13 -1
- package/dist/agent/reducer.js +52 -12
- package/dist/boot/guard.d.ts +3 -3
- package/dist/boot/guard.js +3 -3
- package/dist/cli.js +48 -16
- package/dist/commands/builtins.js +66 -39
- package/dist/commands/queue-command.d.ts +3 -0
- package/dist/commands/queue-command.js +5 -0
- package/dist/commands/registry.d.ts +3 -0
- package/dist/compaction/child.d.ts +4 -2
- package/dist/compaction/child.js +8 -1
- package/dist/compaction/compactor.d.ts +7 -2
- package/dist/compaction/compactor.js +21 -16
- package/dist/compaction/context-command.js +10 -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.d.ts +4 -3
- package/dist/compaction/wiring.js +10 -2
- package/dist/config/cli-commands.d.ts +2 -0
- package/dist/config/cli-commands.js +16 -4
- package/dist/config/load.d.ts +5 -2
- package/dist/config/load.js +23 -12
- package/dist/config/model-profile-resolution.d.ts +20 -0
- package/dist/config/model-profile-resolution.js +81 -0
- package/dist/config/model-profile-store.d.ts +67 -0
- package/dist/config/model-profile-store.js +153 -0
- package/dist/config/model-profiles.d.ts +44 -0
- package/dist/config/model-profiles.js +121 -0
- package/dist/config/schema.d.ts +11 -2
- package/dist/config/store.d.ts +11 -1
- package/dist/config/store.js +43 -27
- package/dist/fast/resolve.d.ts +2 -1
- package/dist/fast/resolve.js +6 -4
- package/dist/fast/wiring.d.ts +5 -3
- package/dist/fast/wiring.js +8 -4
- package/dist/input/stdin-filter.js +3 -2
- package/dist/logging/install.js +4 -2
- package/dist/logging/secret-registry.d.ts +2 -0
- package/dist/logging/secret-registry.js +18 -0
- package/dist/team/runtime.d.ts +2 -1
- package/dist/team/subagent.d.ts +3 -1
- package/dist/team/subagent.js +3 -1
- package/dist/ui/ActivityLine.d.ts +6 -70
- package/dist/ui/ActivityLine.js +12 -34
- package/dist/ui/App.d.ts +1 -0
- package/dist/ui/App.js +116 -215
- package/dist/ui/BottomStatusRow.d.ts +6 -86
- package/dist/ui/BottomStatusRow.js +11 -26
- package/dist/ui/Composer.d.ts +16 -83
- package/dist/ui/Composer.js +9 -100
- package/dist/ui/Logo.d.ts +3 -5
- package/dist/ui/Logo.js +4 -8
- package/dist/ui/PromptInput.d.ts +4 -1
- package/dist/ui/PromptInput.js +44 -35
- package/dist/ui/QueuePanel.d.ts +8 -0
- package/dist/ui/QueuePanel.js +9 -0
- package/dist/ui/QueueStatusRow.d.ts +2 -0
- package/dist/ui/QueueStatusRow.js +1 -1
- package/dist/ui/StatusBar.d.ts +13 -149
- package/dist/ui/StatusBar.js +27 -142
- package/dist/ui/capabilities.d.ts +2 -0
- package/dist/ui/capabilities.js +6 -0
- package/dist/ui/clipboard-task.d.ts +29 -0
- package/dist/ui/clipboard-task.js +66 -0
- package/dist/ui/clipboard.d.ts +29 -50
- package/dist/ui/clipboard.js +141 -75
- package/dist/ui/color-runtime.d.ts +5 -0
- package/dist/ui/color-runtime.js +29 -0
- package/dist/ui/composer-limits.d.ts +3 -4
- package/dist/ui/composer-limits.js +4 -9
- package/dist/ui/composer-rows.d.ts +1 -1
- package/dist/ui/composer-rows.js +24 -137
- package/dist/ui/editor-navigation.d.ts +45 -0
- package/dist/ui/editor-navigation.js +85 -0
- package/dist/ui/editor-reducer.d.ts +4 -0
- package/dist/ui/editor-reducer.js +29 -15
- 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/interaction-copy.d.ts +77 -0
- package/dist/ui/interaction-copy.js +40 -0
- package/dist/ui/interaction-hints.d.ts +18 -0
- package/dist/ui/interaction-hints.js +92 -0
- package/dist/ui/layout/budget.js +1 -1
- package/dist/ui/layout/queue-layout.d.ts +19 -0
- package/dist/ui/layout/queue-layout.js +26 -0
- package/dist/ui/layout/status-layout.d.ts +55 -0
- package/dist/ui/layout/status-layout.js +110 -0
- package/dist/ui/layout/todo-layout.d.ts +1 -0
- package/dist/ui/layout/todo-layout.js +1 -1
- package/dist/ui/overlays/HelpOverlay.js +8 -8
- package/dist/ui/overlays/ModelProfileEditor.d.ts +12 -0
- package/dist/ui/overlays/ModelProfileEditor.js +103 -0
- package/dist/ui/overlays/ModelProfilePicker.d.ts +35 -0
- package/dist/ui/overlays/ModelProfilePicker.js +167 -0
- package/dist/ui/overlays/QueueOverlay.d.ts +22 -0
- package/dist/ui/overlays/QueueOverlay.js +79 -0
- package/dist/ui/overlays/SettingsScreen.d.ts +9 -2
- package/dist/ui/overlays/SettingsScreen.js +347 -3
- package/dist/ui/overlays/model-profile-state.d.ts +74 -0
- package/dist/ui/overlays/model-profile-state.js +119 -0
- package/dist/ui/render-cache.d.ts +2 -2
- package/dist/ui/render-cache.js +4 -3
- package/dist/ui/run-status-row.d.ts +7 -19
- package/dist/ui/run-status-row.js +2 -18
- package/dist/ui/selection/selection-controller.d.ts +2 -2
- package/dist/ui/selection/selection-controller.js +1 -0
- package/dist/ui/use-wheel-routing.js +2 -1
- package/package.json +2 -2
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { loadConfig } from '../config/load.js';
|
|
2
|
+
import { DEFAULT_CONFIG } from '../config/schema.js';
|
|
3
|
+
import { mergePersistedConfig } from '../config/store.js';
|
|
4
|
+
import { adaptLegacyConnectionPatch, assertLegacyFastTarget, commitModelSettings, readModelSettingsDisk, } from '../config/model-profile-store.js';
|
|
5
|
+
import { projectModelProfiles, resolveModelProfileState } from '../config/model-profile-resolution.js';
|
|
6
|
+
import { validateModelProfiles } from '../config/model-profiles.js';
|
|
7
|
+
import { registerProfileSecrets } from '../logging/secret-registry.js';
|
|
8
|
+
/** Capture both baselines when the editor opens, without migrating or writing anything. */
|
|
9
|
+
export function createModelSettingsDraft(live, liveRevision) {
|
|
10
|
+
const disk = readModelSettingsDisk();
|
|
11
|
+
return {
|
|
12
|
+
diskRevision: disk.revision, liveRevision,
|
|
13
|
+
baseline: structuredClone(disk.config ?? DEFAULT_CONFIG),
|
|
14
|
+
liveBaseline: structuredClone(live),
|
|
15
|
+
profiles: disk.config?.modelProfiles
|
|
16
|
+
? structuredClone(disk.config.modelProfiles) : undefined,
|
|
17
|
+
activateRoles: [], patch: {},
|
|
18
|
+
...(!disk.ok ? { readError: 'Cannot read config. Repair it with aragon config edit.' } : {}),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/** Map stable failure codes to safe UI text; never include parser or SDK error messages. */
|
|
22
|
+
export function modelSettingsResultMessage(result) {
|
|
23
|
+
if (result.status === 'saved_apply_failed') {
|
|
24
|
+
return result.persisted
|
|
25
|
+
? 'Saved, but this session could not apply all settings. Restart required.'
|
|
26
|
+
: 'Could not apply the reloaded settings completely. Restart required.';
|
|
27
|
+
}
|
|
28
|
+
if (result.status === 'restart_required') {
|
|
29
|
+
return 'Saved. Fast tier requires a restart; check its model and credentials.';
|
|
30
|
+
}
|
|
31
|
+
const messages = {
|
|
32
|
+
busy: 'Wait for the run, team, review, or compaction to finish before saving.',
|
|
33
|
+
conflict: 'Settings changed elsewhere. Reload before saving.',
|
|
34
|
+
read_failed: 'Cannot read config. Repair it with aragon config edit.',
|
|
35
|
+
write_failed: 'Could not write config. Your draft is preserved.',
|
|
36
|
+
invalid: 'Invalid model settings. Check the highlighted field or aragon config edit.',
|
|
37
|
+
profile_key_required: 'This profile has a dedicated key. Edit or copy it in /settings.',
|
|
38
|
+
legacy_conversion_unsupported: 'This endpoint needs a profile. Edit it in /settings.',
|
|
39
|
+
};
|
|
40
|
+
return result.ok ? 'Settings saved.' : messages[result.code ?? 'invalid'];
|
|
41
|
+
}
|
|
42
|
+
function rejected(code) {
|
|
43
|
+
return { ok: false, persisted: false, status: 'rejected', code };
|
|
44
|
+
}
|
|
45
|
+
function connectionChanged(draft, role) {
|
|
46
|
+
if (draft.profiles === undefined)
|
|
47
|
+
return false;
|
|
48
|
+
const before = draft.baseline.modelProfiles;
|
|
49
|
+
const after = draft.profiles;
|
|
50
|
+
if ((before?.[`${role}Id`] ?? null) !== (after?.[`${role}Id`] ?? null))
|
|
51
|
+
return true;
|
|
52
|
+
const id = after?.[`${role}Id`];
|
|
53
|
+
if (!id)
|
|
54
|
+
return false;
|
|
55
|
+
const a = before?.entries.find((entry) => entry.id === id);
|
|
56
|
+
const b = after?.entries.find((entry) => entry.id === id);
|
|
57
|
+
return ['provider', 'model', 'baseUrl', 'apiKey']
|
|
58
|
+
.some((key) => a?.[key] !== b?.[key]);
|
|
59
|
+
}
|
|
60
|
+
function hasConnectionPatch(patch, role) {
|
|
61
|
+
const section = role === 'main' ? patch : patch.fast;
|
|
62
|
+
return !!section && ['provider', 'model', 'baseUrl'].some((key) => key in section);
|
|
63
|
+
}
|
|
64
|
+
function liveCandidate(live, candidate, draft) {
|
|
65
|
+
const patch = draft.patch;
|
|
66
|
+
const normalizedPatch = {};
|
|
67
|
+
for (const key of Object.keys(patch)) {
|
|
68
|
+
normalizedPatch[key] = candidate[key];
|
|
69
|
+
}
|
|
70
|
+
const next = { ...live, ...normalizedPatch };
|
|
71
|
+
for (const section of ['apiKeys', 'fast', 'compaction', 'log']) {
|
|
72
|
+
const normalized = { ...live[section] };
|
|
73
|
+
for (const key of Object.keys(patch[section] ?? {})) {
|
|
74
|
+
normalized[key] = candidate[section][key];
|
|
75
|
+
}
|
|
76
|
+
Object.assign(next, { [section]: normalized });
|
|
77
|
+
}
|
|
78
|
+
if (patch.apiKeys && Object.hasOwn(patch.apiKeys, live.provider)) {
|
|
79
|
+
next.apiKeyOverride = undefined;
|
|
80
|
+
next.apiKeyOverrideTarget = undefined;
|
|
81
|
+
}
|
|
82
|
+
// These fields have intentionally different disk and runtime representations.
|
|
83
|
+
if ('maxTokens' in patch)
|
|
84
|
+
next.maxTokens = candidate.maxTokens ?? undefined;
|
|
85
|
+
if ('baseUrl' in patch)
|
|
86
|
+
next.baseUrl = candidate.baseUrl ?? undefined;
|
|
87
|
+
const projected = projectModelProfiles(candidate).file;
|
|
88
|
+
if (draft.profiles !== undefined || candidate.modelProfiles !== undefined) {
|
|
89
|
+
next.modelProfiles = structuredClone(projected.modelProfiles);
|
|
90
|
+
}
|
|
91
|
+
next.liveToolOutput = live.liveToolOutput;
|
|
92
|
+
for (const role of ['main', 'fast']) {
|
|
93
|
+
if (!draft.activateRoles.includes(role) && !connectionChanged(draft, role)
|
|
94
|
+
&& !hasConnectionPatch(patch, role))
|
|
95
|
+
continue;
|
|
96
|
+
if (role === 'main') {
|
|
97
|
+
next.provider = projected.provider;
|
|
98
|
+
next.model = projected.model;
|
|
99
|
+
next.baseUrl = projected.baseUrl ?? undefined;
|
|
100
|
+
next.apiKeyOverride = undefined;
|
|
101
|
+
next.apiKeyOverrideTarget = undefined;
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
next.fast = { ...next.fast, provider: projected.fast.provider,
|
|
105
|
+
model: projected.fast.model, baseUrl: projected.fast.baseUrl };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (next.apiKeyOverride && !next.apiKeyOverrideTarget
|
|
109
|
+
&& (next.modelProfiles?.mainId || next.modelProfiles?.fastId)) {
|
|
110
|
+
next.apiKeyOverrideTarget = { provider: live.provider, baseUrl: live.baseUrl ?? null };
|
|
111
|
+
}
|
|
112
|
+
next.modelProfileState = resolveModelProfileState(next);
|
|
113
|
+
return next;
|
|
114
|
+
}
|
|
115
|
+
/** Validate and prepare everything before the one synchronous, atomic disk commit. */
|
|
116
|
+
export function saveModelSettings(input) {
|
|
117
|
+
const { draft, controller } = input;
|
|
118
|
+
if (controller.isModelSettingsBusy())
|
|
119
|
+
return rejected('busy');
|
|
120
|
+
if (draft.liveRevision !== controller.getSettingsRevision())
|
|
121
|
+
return rejected('conflict');
|
|
122
|
+
const disk = readModelSettingsDisk();
|
|
123
|
+
if (!disk.ok || draft.readError)
|
|
124
|
+
return rejected('read_failed');
|
|
125
|
+
if (disk.revision !== draft.diskRevision)
|
|
126
|
+
return rejected('conflict');
|
|
127
|
+
const issues = validateModelProfiles(draft.profiles ?? disk.config?.modelProfiles);
|
|
128
|
+
if (issues.length)
|
|
129
|
+
return { ...rejected('invalid'), issues };
|
|
130
|
+
let snapshot;
|
|
131
|
+
let candidate;
|
|
132
|
+
try {
|
|
133
|
+
const current = disk.config ?? structuredClone(DEFAULT_CONFIG);
|
|
134
|
+
const profilesChanged = draft.activateRoles.length > 0
|
|
135
|
+
|| JSON.stringify(draft.profiles) !== JSON.stringify(draft.baseline.modelProfiles);
|
|
136
|
+
const implicitLegacy = draft.profiles === undefined || !profilesChanged;
|
|
137
|
+
const patch = adaptLegacyConnectionPatch({ current, patch: implicitLegacy
|
|
138
|
+
? draft.patch : { ...draft.patch, modelProfiles: draft.profiles } });
|
|
139
|
+
candidate = mergePersistedConfig(current, patch);
|
|
140
|
+
registerProfileSecrets(candidate);
|
|
141
|
+
const next = liveCandidate(controller.getConfig(), candidate, draft);
|
|
142
|
+
// Disk preferences cannot reveal startup/session gateways retained by the live main role.
|
|
143
|
+
if (implicitLegacy && current.modelProfiles?.fastId && hasConnectionPatch(draft.patch, 'fast')) {
|
|
144
|
+
assertLegacyFastTarget(next.fast, next);
|
|
145
|
+
}
|
|
146
|
+
snapshot = controller.prepareModelSettingsSnapshot(next);
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
const code = error.code ?? 'invalid';
|
|
150
|
+
return rejected(code);
|
|
151
|
+
}
|
|
152
|
+
const commit = commitModelSettings({ candidate, diskRevision: draft.diskRevision });
|
|
153
|
+
if (!commit.ok)
|
|
154
|
+
return rejected(commit.code ?? 'write_failed');
|
|
155
|
+
return applyPreparedSnapshot(controller, snapshot, true);
|
|
156
|
+
}
|
|
157
|
+
function applyPreparedSnapshot(controller, snapshot, persisted) {
|
|
158
|
+
try {
|
|
159
|
+
controller.applyModelSettingsSnapshot(snapshot);
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
controller.blockModelSettingsRequests();
|
|
163
|
+
return { ok: false, persisted, status: 'saved_apply_failed', code: 'apply_failed', restartRequired: true };
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
controller.applyModelSettingsEffects();
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
return { ok: false, persisted, status: 'saved_apply_failed', code: 'apply_failed', restartRequired: true };
|
|
170
|
+
}
|
|
171
|
+
const restartRequired = snapshot.config.fast.enabled && !controller.isFastRegistered();
|
|
172
|
+
return { ok: true, persisted, status: restartRequired ? 'restart_required' : 'applied',
|
|
173
|
+
restartRequired };
|
|
174
|
+
}
|
|
175
|
+
/** Reload the complete model snapshot; invalid disk content never replaces a live session. */
|
|
176
|
+
export function reloadModelSettings(controller) {
|
|
177
|
+
if (controller.isModelSettingsBusy())
|
|
178
|
+
return rejected('busy');
|
|
179
|
+
const disk = readModelSettingsDisk();
|
|
180
|
+
if (!disk.ok)
|
|
181
|
+
return rejected('read_failed');
|
|
182
|
+
if (validateModelProfiles(disk.config?.modelProfiles).length)
|
|
183
|
+
return rejected('invalid');
|
|
184
|
+
try {
|
|
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;
|
|
196
|
+
const after = readModelSettingsDisk();
|
|
197
|
+
if (!after.ok)
|
|
198
|
+
return rejected('read_failed');
|
|
199
|
+
if (after.revision !== disk.revision)
|
|
200
|
+
return rejected('conflict');
|
|
201
|
+
if (resolved.modelProfileState?.main.invalid)
|
|
202
|
+
return rejected('invalid');
|
|
203
|
+
return applyPreparedSnapshot(controller, controller.prepareModelSettingsSnapshot(resolved), false);
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
return rejected('invalid');
|
|
207
|
+
}
|
|
208
|
+
}
|
|
@@ -4,5 +4,17 @@ export interface PendingSteering {
|
|
|
4
4
|
readonly queueId: string;
|
|
5
5
|
readonly text: string;
|
|
6
6
|
}
|
|
7
|
+
/** Hide only active pending entries, using the same projection for measurement and paint. */
|
|
8
|
+
export declare function selectTranscriptEntries(entries: Entry[], pending: readonly PendingSteering[]): Entry[];
|
|
9
|
+
/** Restore trimmed messages in FIFO order before atomically accepting known receipts. */
|
|
10
|
+
export declare function reconcileSteeringReceipt(options: {
|
|
11
|
+
entries: Entry[];
|
|
12
|
+
pending: readonly PendingSteering[];
|
|
13
|
+
ids: readonly string[];
|
|
14
|
+
}): {
|
|
15
|
+
entries: Entry[];
|
|
16
|
+
pending: PendingSteering[];
|
|
17
|
+
changed: boolean;
|
|
18
|
+
};
|
|
7
19
|
/** Supplement trimmed/cleared history for save and exit without deduplicating text. */
|
|
8
20
|
export declare function mergePendingEntries(entries: Entry[], pending: readonly PendingSteering[]): Entry[];
|
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
/** Hide only active pending entries, using the same projection for measurement and paint. */
|
|
2
|
+
export function selectTranscriptEntries(entries, pending) {
|
|
3
|
+
if (pending.length === 0)
|
|
4
|
+
return entries;
|
|
5
|
+
const ids = new Set(pending.map((item) => item.queueId));
|
|
6
|
+
return entries.filter((entry) => entry.kind !== 'queued' || !ids.has(entry.queueId ?? ''));
|
|
7
|
+
}
|
|
8
|
+
/** Restore trimmed messages in FIFO order before atomically accepting known receipts. */
|
|
9
|
+
export function reconcileSteeringReceipt(options) {
|
|
10
|
+
const receipts = new Set(options.ids);
|
|
11
|
+
const ids = new Set(options.pending.filter((item) => receipts.has(item.queueId))
|
|
12
|
+
.map((item) => item.queueId));
|
|
13
|
+
if (!ids.size)
|
|
14
|
+
return { entries: options.entries, pending: [...options.pending], changed: false };
|
|
15
|
+
const entries = mergePendingEntries(options.entries, options.pending).map((entry) => entry.kind === 'queued' && ids.has(entry.queueId ?? '')
|
|
16
|
+
? { id: entry.id, kind: 'user', text: entry.text } : entry);
|
|
17
|
+
return { entries, pending: options.pending.filter((item) => !ids.has(item.queueId)), changed: true };
|
|
18
|
+
}
|
|
1
19
|
/** Supplement trimmed/cleared history for save and exit without deduplicating text. */
|
|
2
20
|
export function mergePendingEntries(entries, pending) {
|
|
3
21
|
if (pending.length === 0)
|
package/dist/agent/reducer.d.ts
CHANGED
|
@@ -22,7 +22,7 @@ import type { RetryPhase } from './retry-view.js';
|
|
|
22
22
|
import type { FilePatch } from '../tools/patch.js';
|
|
23
23
|
export type NoticeLevel = 'info' | 'warn' | 'error';
|
|
24
24
|
export type ToastLevel = NoticeLevel | 'success';
|
|
25
|
-
export type Overlay = null | 'settings' | 'model' | 'help' | 'confirm' | 'question' | 'plan';
|
|
25
|
+
export type Overlay = null | 'settings' | 'model' | 'help' | 'queue' | 'confirm' | 'question' | 'plan';
|
|
26
26
|
export type ToolStatus = 'pending' | 'running' | 'done' | 'error';
|
|
27
27
|
/** Ephemeral, auto-dismissed acknowledgement (spec §3.9). */
|
|
28
28
|
export interface Toast {
|
|
@@ -278,6 +278,13 @@ export interface RetrySnapshot {
|
|
|
278
278
|
export interface ViewState {
|
|
279
279
|
entries: Entry[];
|
|
280
280
|
pendingSteering: PendingSteering[];
|
|
281
|
+
seenSteeringIds: ReadonlySet<string>;
|
|
282
|
+
runPhase: 'idle' | 'starting' | 'waiting' | 'thinking' | 'generating' | 'preparing-tool' | 'tool';
|
|
283
|
+
activeTool?: {
|
|
284
|
+
toolCallId: string;
|
|
285
|
+
name: string;
|
|
286
|
+
};
|
|
287
|
+
runOutcome: 'none' | 'ended' | 'interrupted' | 'failed';
|
|
281
288
|
status: 'idle' | 'running';
|
|
282
289
|
usageTotal: UsageTotal;
|
|
283
290
|
/**
|
|
@@ -412,6 +419,10 @@ export type ViewAction = {
|
|
|
412
419
|
ids: readonly string[];
|
|
413
420
|
} | {
|
|
414
421
|
type: 'runStart';
|
|
422
|
+
} | {
|
|
423
|
+
type: 'runStarting';
|
|
424
|
+
} | {
|
|
425
|
+
type: 'runError';
|
|
415
426
|
} | {
|
|
416
427
|
type: 'turnStart';
|
|
417
428
|
} | {
|
|
@@ -438,6 +449,7 @@ export type ViewAction = {
|
|
|
438
449
|
} | {
|
|
439
450
|
type: 'toolExecStart';
|
|
440
451
|
toolCallId: string;
|
|
452
|
+
toolName?: string;
|
|
441
453
|
} | {
|
|
442
454
|
type: 'toolExecEnd';
|
|
443
455
|
toolCallId: string;
|
package/dist/agent/reducer.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* synthesizes a generic error notice (a swallowed throw is never a no-op).
|
|
12
12
|
*/
|
|
13
13
|
import process from 'node:process';
|
|
14
|
+
import { reconcileSteeringReceipt } from './queued-messages.js';
|
|
14
15
|
import { emptyContextUsage } from '../compaction/meter.js';
|
|
15
16
|
import { addUsage, computeCost } from './usage.js';
|
|
16
17
|
import { ENTRY_LIMITS, appendBounded, entryRetain, trimEntries } from './entry-limits.js';
|
|
@@ -47,6 +48,9 @@ export function initialViewState(seed = {}) {
|
|
|
47
48
|
return {
|
|
48
49
|
entries: [],
|
|
49
50
|
pendingSteering: [],
|
|
51
|
+
seenSteeringIds: new Set(),
|
|
52
|
+
runPhase: 'idle',
|
|
53
|
+
runOutcome: 'none',
|
|
50
54
|
status: 'idle',
|
|
51
55
|
usageTotal: EMPTY_USAGE_TOTAL,
|
|
52
56
|
context: emptyContextUsage(),
|
|
@@ -145,7 +149,7 @@ export function reduceEvent(event, cost, patches) {
|
|
|
145
149
|
case 'agent_end':
|
|
146
150
|
return [{ type: 'runEnd' }];
|
|
147
151
|
case 'tool_execution_start':
|
|
148
|
-
return [{ type: 'toolExecStart', toolCallId: event.toolCallId }];
|
|
152
|
+
return [{ type: 'toolExecStart', toolCallId: event.toolCallId, toolName: event.toolName }];
|
|
149
153
|
case 'tool_execution_end': {
|
|
150
154
|
const patch = patches?.take(event.toolCallId);
|
|
151
155
|
const preview = buildToolPreview(event.result);
|
|
@@ -200,8 +204,6 @@ function reduceStreamEvent(se) {
|
|
|
200
204
|
case 'text_delta':
|
|
201
205
|
return [{ type: 'textDelta', delta: se.delta }];
|
|
202
206
|
case 'tool_call_start':
|
|
203
|
-
if (SELF_RENDERING_TOOLS.has(se.toolName))
|
|
204
|
-
return [];
|
|
205
207
|
return [
|
|
206
208
|
{ type: 'toolCallStart', toolCallId: se.toolCallId, toolName: se.toolName },
|
|
207
209
|
];
|
|
@@ -213,6 +215,7 @@ function reduceStreamEvent(se) {
|
|
|
213
215
|
return [{ type: 'toolCallEnd', toolCallId: se.toolCallId, args: se.args }];
|
|
214
216
|
case 'error':
|
|
215
217
|
return [
|
|
218
|
+
{ type: 'runError' },
|
|
216
219
|
{
|
|
217
220
|
type: 'notice',
|
|
218
221
|
level: 'error',
|
|
@@ -405,7 +408,40 @@ function findToolEntryId(state, toolCallId) {
|
|
|
405
408
|
}
|
|
406
409
|
return undefined;
|
|
407
410
|
}
|
|
411
|
+
/** Runtime facts outlive transcript cards and are independent of command/clipboard notices. */
|
|
412
|
+
function reduceRunFacts(state, action) {
|
|
413
|
+
switch (action.type) {
|
|
414
|
+
case 'runStarting': return { ...state, runPhase: 'starting', runOutcome: 'none' };
|
|
415
|
+
case 'runStart': return { ...state, runPhase: 'waiting', runOutcome: 'none', activeTool: undefined };
|
|
416
|
+
case 'turnStart': return { ...state, runPhase: 'waiting' };
|
|
417
|
+
case 'thinkingStart':
|
|
418
|
+
case 'thinkingDelta': return { ...state, runPhase: 'thinking' };
|
|
419
|
+
case 'textDelta': return { ...state, runPhase: 'generating' };
|
|
420
|
+
case 'toolCallStart':
|
|
421
|
+
case 'toolCallDelta':
|
|
422
|
+
case 'toolCallEnd': return { ...state, runPhase: 'preparing-tool' };
|
|
423
|
+
case 'toolExecStart': {
|
|
424
|
+
const card = state.entries.find((entry) => entry.kind === 'tool' && entry.toolCallId === action.toolCallId);
|
|
425
|
+
return { ...state, runPhase: 'tool', activeTool: {
|
|
426
|
+
toolCallId: action.toolCallId,
|
|
427
|
+
name: action.toolName ?? (card?.kind === 'tool' ? card.name : 'tool'),
|
|
428
|
+
} };
|
|
429
|
+
}
|
|
430
|
+
case 'toolExecEnd': return state.activeTool?.toolCallId === action.toolCallId
|
|
431
|
+
? { ...state, runPhase: 'waiting', activeTool: undefined } : state;
|
|
432
|
+
case 'runError': return { ...state, runOutcome: 'failed' };
|
|
433
|
+
case 'abortMark': return { ...state, runOutcome: 'interrupted' };
|
|
434
|
+
case 'runEnd': return { ...state, runPhase: 'idle', activeTool: undefined,
|
|
435
|
+
runOutcome: state.runOutcome !== 'none' ? state.runOutcome
|
|
436
|
+
: !state.turnProduced && !state.errorNoticed && !state.aborted ? 'failed' : 'ended' };
|
|
437
|
+
case 'resetConversation':
|
|
438
|
+
case 'restoreEntries': return { ...state, runPhase: 'idle', activeTool: undefined,
|
|
439
|
+
runOutcome: 'none', seenSteeringIds: new Set() };
|
|
440
|
+
default: return state;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
408
443
|
export function viewReducer(state, action) {
|
|
444
|
+
state = reduceRunFacts(state, action);
|
|
409
445
|
switch (action.type) {
|
|
410
446
|
case 'submit': {
|
|
411
447
|
const { id, seq } = nextId(state);
|
|
@@ -429,6 +465,8 @@ export function viewReducer(state, action) {
|
|
|
429
465
|
};
|
|
430
466
|
}
|
|
431
467
|
case 'steerQueued': {
|
|
468
|
+
if (state.seenSteeringIds.has(action.queueId))
|
|
469
|
+
return state;
|
|
432
470
|
const { id, seq } = nextId(state);
|
|
433
471
|
const entry = { id, kind: 'queued', queueId: action.queueId, text: action.text };
|
|
434
472
|
// NOTHING ELSE RESETS. This is not a new turn -- `turnProduced`,
|
|
@@ -437,20 +475,19 @@ export function viewReducer(state, action) {
|
|
|
437
475
|
// guard misfire on a turn that produced plenty.
|
|
438
476
|
return {
|
|
439
477
|
...state, seq, ...appendEntry(state, state.entries, entry),
|
|
478
|
+
seenSteeringIds: new Set([...state.seenSteeringIds, action.queueId]),
|
|
440
479
|
pendingSteering: [...state.pendingSteering, { queueId: action.queueId, text: action.text }],
|
|
441
480
|
};
|
|
442
481
|
}
|
|
443
482
|
case 'steeringAccepted': {
|
|
444
|
-
const
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
if (
|
|
483
|
+
const receipt = reconcileSteeringReceipt({
|
|
484
|
+
entries: state.entries, pending: state.pendingSteering, ids: action.ids,
|
|
485
|
+
});
|
|
486
|
+
if (!receipt.changed)
|
|
448
487
|
return state;
|
|
449
|
-
const
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
: entry);
|
|
453
|
-
return { ...state, pendingSteering, entries };
|
|
488
|
+
const trimmed = trimEntries(receipt.entries, entryRetain());
|
|
489
|
+
return { ...state, pendingSteering: receipt.pending, entries: trimmed.entries,
|
|
490
|
+
droppedEntries: state.droppedEntries + trimmed.dropped };
|
|
454
491
|
}
|
|
455
492
|
case 'runStart':
|
|
456
493
|
return {
|
|
@@ -517,6 +554,8 @@ export function viewReducer(state, action) {
|
|
|
517
554
|
};
|
|
518
555
|
}
|
|
519
556
|
case 'toolCallStart': {
|
|
557
|
+
if (SELF_RENDERING_TOOLS.has(action.toolName))
|
|
558
|
+
return state;
|
|
520
559
|
const { id, seq } = nextId(state);
|
|
521
560
|
const entry = {
|
|
522
561
|
id,
|
|
@@ -1151,6 +1190,7 @@ export function viewReducer(state, action) {
|
|
|
1151
1190
|
prev.source === next.source &&
|
|
1152
1191
|
prev.deltaTokens === next.deltaTokens &&
|
|
1153
1192
|
prev.windowKnown === next.windowKnown &&
|
|
1193
|
+
prev.windowSource === next.windowSource &&
|
|
1154
1194
|
prev.windowOverridden === next.windowOverridden) {
|
|
1155
1195
|
return state;
|
|
1156
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,8 +16,9 @@ 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
|
+
import { ModelProfileConfigError } from './config/model-profile-store.js';
|
|
21
22
|
import { loadConfig } from './config/load.js';
|
|
22
23
|
import { getConfigPath, readConfigFile, updatePersistedConfig } from './config/store.js';
|
|
23
24
|
import { formatMigrationNotice, migrateLegacyState } from './config/migrate-legacy-state.js';
|
|
@@ -53,7 +54,7 @@ import { supportsWindowsVtInput } from './ui/win-vt-input.js';
|
|
|
53
54
|
import { forceWindowsVtInput } from './ui/win-vt-force.js';
|
|
54
55
|
import { readExitSnapshot } from './ui/exit-snapshot.js';
|
|
55
56
|
import { renderTranscriptText } from './ui/transcript-text.js';
|
|
56
|
-
import {
|
|
57
|
+
import { resolveTuiCapabilities } from './ui/capabilities.js';
|
|
57
58
|
import { pickGlyphs } from './ui/glyphs.js';
|
|
58
59
|
import { getTheme } from './ui/theme.js';
|
|
59
60
|
import { createSelectionController, } from './ui/selection/selection-controller.js';
|
|
@@ -223,7 +224,7 @@ function toFlags(opts) {
|
|
|
223
224
|
* instead of writing to disk unattended.
|
|
224
225
|
*/
|
|
225
226
|
function makeController(flags, opts = { interactive: true }) {
|
|
226
|
-
const config = loadConfig(flags);
|
|
227
|
+
const config = loadConfig(flags, { interactive: opts.interactive });
|
|
227
228
|
// The authoritative log level lands here (§4.4.5). `installLogging()` only
|
|
228
229
|
// resolved a bootstrap level — it ran before commander and before `.env` were
|
|
229
230
|
// readable — and without this hand-off the fully resolved one never applies.
|
|
@@ -282,6 +283,7 @@ function makeController(flags, opts = { interactive: true }) {
|
|
|
282
283
|
*/
|
|
283
284
|
function makeExecController(flags, deps) {
|
|
284
285
|
const { controller } = makeController(flags, { interactive: false, ...deps });
|
|
286
|
+
void controller.refreshModelMetadata();
|
|
285
287
|
const logger = getLogger();
|
|
286
288
|
logger.info('cli', 'run_exec', {});
|
|
287
289
|
logger.onFailure((reason) => process.stderr.write(`[log] ${reason}\n`));
|
|
@@ -387,9 +389,14 @@ function runInteractive(flags, extras = {}) {
|
|
|
387
389
|
return;
|
|
388
390
|
}
|
|
389
391
|
const { controller, confirmBridge, humanInputBridge } = makeController(flags);
|
|
392
|
+
void controller.refreshModelMetadata();
|
|
390
393
|
const config = controller.getConfig();
|
|
394
|
+
const caps = resolveTuiCapabilities({}, config.color);
|
|
391
395
|
const logger = getLogger();
|
|
392
|
-
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
|
+
});
|
|
393
400
|
attachAgentEvents(logger, controller);
|
|
394
401
|
// The team stream is CLI-local (D-10), so `attachAgentEvents` cannot see it.
|
|
395
402
|
// Without this a dispatch leaves NO trace in the log file at all — the wrong
|
|
@@ -458,7 +465,7 @@ function runInteractive(flags, extras = {}) {
|
|
|
458
465
|
// advising the user about a mode they just disabled.
|
|
459
466
|
const wantPaste = config.paste && !!process.stdin.isTTY;
|
|
460
467
|
const pasteBridge = { notify: null };
|
|
461
|
-
const filter =
|
|
468
|
+
const filter = process.stdin.isTTY
|
|
462
469
|
? tryCreateStdinFilter(process.stdin, { mouse: wantMouse, paste: wantPaste }, (reason) => logger.warn('cli', 'stdin_filter_failed', { reason }), pasteBridge)
|
|
463
470
|
: null;
|
|
464
471
|
/**
|
|
@@ -547,7 +554,7 @@ function runInteractive(flags, extras = {}) {
|
|
|
547
554
|
return; // Never published (instant exit) — skip, do not throw.
|
|
548
555
|
writeExitTranscript(process.stdout, renderTranscriptText(snapshot.entries, {
|
|
549
556
|
// Same terminal the TUI just left, so the same glyph tier applies.
|
|
550
|
-
glyphs: pickGlyphs(
|
|
557
|
+
glyphs: pickGlyphs(caps),
|
|
551
558
|
usageTotal: snapshot.usageTotal,
|
|
552
559
|
provider: snapshot.provider,
|
|
553
560
|
model: snapshot.model,
|
|
@@ -634,8 +641,8 @@ function runInteractive(flags, extras = {}) {
|
|
|
634
641
|
// the SAME `copyText` door and the SAME bridge `onCopied` toast
|
|
635
642
|
// funnel -- the controller keeps only the selection state machine.
|
|
636
643
|
// Read at PAINT time, not captured: `/theme` rebuilds the theme mid-session.
|
|
637
|
-
theme: () => getTheme(config.theme,
|
|
638
|
-
caps
|
|
644
|
+
theme: () => getTheme(config.theme, caps),
|
|
645
|
+
caps,
|
|
639
646
|
cols: () => process.stdout.columns ?? 80,
|
|
640
647
|
// The STATIC half of the gate. The dynamic half — "no overlay is open" —
|
|
641
648
|
// belongs to `App`, which owns that state and calls `setEnabled`.
|
|
@@ -648,6 +655,7 @@ function runInteractive(flags, extras = {}) {
|
|
|
648
655
|
};
|
|
649
656
|
const terminalBridge = {
|
|
650
657
|
mouseSelect: wantSelect,
|
|
658
|
+
deleteDisambiguated: filter !== null,
|
|
651
659
|
scrollbar,
|
|
652
660
|
...(mouseOn
|
|
653
661
|
? {
|
|
@@ -716,14 +724,15 @@ function runInteractive(flags, extras = {}) {
|
|
|
716
724
|
// every existing path is provably unchanged.
|
|
717
725
|
stdin: filter?.stdin ?? process.stdin,
|
|
718
726
|
stdout: frameObserver.stdout,
|
|
719
|
-
});
|
|
727
|
+
}, caps);
|
|
720
728
|
}
|
|
721
729
|
catch (error) {
|
|
722
730
|
failInteractive(error);
|
|
723
731
|
throw error;
|
|
724
732
|
}
|
|
725
|
-
// H1's primary disarm
|
|
726
|
-
//
|
|
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
|
|
727
736
|
// (cli-auto-update-hardening section 5.1.4). It is what makes a long session
|
|
728
737
|
// that is later `SIGKILL`ed - terminal window closed, machine slept badly, OOM
|
|
729
738
|
// killer - still count as healthy.
|
|
@@ -733,7 +742,8 @@ function runInteractive(flags, extras = {}) {
|
|
|
733
742
|
// never armed it this is one read and one string compare. Gating it would mean
|
|
734
743
|
// a user who switches `mode` to `off` between the install and the next launch
|
|
735
744
|
// leaves the guard armed with nothing able to disarm it.
|
|
736
|
-
|
|
745
|
+
setImmediate(() => { if (!interactiveClosed)
|
|
746
|
+
markBootHealthy(VERSION); });
|
|
737
747
|
if (updateEligible) {
|
|
738
748
|
// DYNAMIC, and fire-and-forget AFTER `render()` (D-25). Dynamic because a
|
|
739
749
|
// static import would make AC-1 false for `aragon -p`, which shares this
|
|
@@ -776,6 +786,9 @@ function runInteractive(flags, extras = {}) {
|
|
|
776
786
|
.waitUntilExit()
|
|
777
787
|
.then(() => {
|
|
778
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);
|
|
779
792
|
screen?.restore();
|
|
780
793
|
// BEFORE `disposeStdinFilter()`, which clears the listener set it subscribed to,
|
|
781
794
|
// and after `restore()` for the reason that call records: the terminal must
|
|
@@ -811,6 +824,7 @@ async function runOneShot(flags, prompt, quiet) {
|
|
|
811
824
|
// install. The gate must know that up front rather than discovering a null
|
|
812
825
|
// handler and defaulting to "yes" (§7.4 / AC-13).
|
|
813
826
|
const { controller } = makeController(flags, { interactive: false });
|
|
827
|
+
void controller.refreshModelMetadata();
|
|
814
828
|
const logger = getLogger();
|
|
815
829
|
logger.info('cli', 'run_oneshot', { quiet });
|
|
816
830
|
// Headless has no frame, so a logging failure can safely reach stderr — the
|
|
@@ -829,10 +843,16 @@ async function runOneShot(flags, prompt, quiet) {
|
|
|
829
843
|
// `followThrough` is read off the REAL controller here rather than through
|
|
830
844
|
// `HeadlessController`, which is deliberately the minimal surface a test can
|
|
831
845
|
// satisfy with an object literal (todo-plan-followthrough §3.6).
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
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
|
+
}
|
|
836
856
|
// Flushed HERE and not inside `runHeadless`: that module is deliberately
|
|
837
857
|
// injectable (its own controller interface, its own stdout/stderr) and has an
|
|
838
858
|
// early `return 2`, so a singleton dependency in there would be both a new
|
|
@@ -1009,6 +1029,18 @@ function runConfigSetContextWindow(value) {
|
|
|
1009
1029
|
process.stdout.write(`Set contextWindow = ${merged.contextWindow ?? 'auto'}\n`);
|
|
1010
1030
|
}
|
|
1011
1031
|
function runConfigSet(key, value) {
|
|
1032
|
+
try {
|
|
1033
|
+
runConfigSetValue(key, value);
|
|
1034
|
+
}
|
|
1035
|
+
catch (error) {
|
|
1036
|
+
const invalid = error instanceof ModelProfileConfigError
|
|
1037
|
+
&& !['read_failed', 'write_failed'].includes(error.code);
|
|
1038
|
+
process.stderr.write(error instanceof ModelProfileConfigError
|
|
1039
|
+
? `${error.message}\n` : 'Could not write config. Check file permissions.\n');
|
|
1040
|
+
process.exitCode = invalid ? 2 : 1;
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
function runConfigSetValue(key, value) {
|
|
1012
1044
|
if (!CONFIG_SET_KEYS.has(key)) {
|
|
1013
1045
|
process.stderr.write(`Unknown config key "${key}". Known keys: ${[...CONFIG_SET_KEYS].join(', ')}.\n`);
|
|
1014
1046
|
process.exitCode = 2;
|