@aragon-agent/cli 0.6.12 → 0.6.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +81 -15
- package/dist/agent/controller.d.ts +29 -1
- package/dist/agent/controller.js +123 -22
- package/dist/agent/model-profile-settings.d.ts +31 -0
- package/dist/agent/model-profile-settings.js +199 -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 +51 -12
- package/dist/cli.js +15 -1
- 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 +4 -1
- package/dist/compaction/compactor.d.ts +7 -2
- package/dist/compaction/compactor.js +13 -11
- package/dist/compaction/context-command.js +5 -0
- package/dist/compaction/wiring.d.ts +4 -3
- package/dist/compaction/wiring.js +6 -1
- package/dist/config/cli-commands.d.ts +2 -0
- package/dist/config/cli-commands.js +16 -4
- package/dist/config/load.d.ts +2 -1
- package/dist/config/load.js +15 -8
- 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 +9 -0
- 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 +113 -205
- 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/PromptInput.d.ts +4 -1
- package/dist/ui/PromptInput.js +41 -34
- 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/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/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/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 +109 -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/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,199 @@
|
|
|
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 resolved = loadConfig({ cwd: controller.getConfig().cwd });
|
|
186
|
+
resolved.liveToolOutput = controller.getConfig().liveToolOutput;
|
|
187
|
+
const after = readModelSettingsDisk();
|
|
188
|
+
if (!after.ok)
|
|
189
|
+
return rejected('read_failed');
|
|
190
|
+
if (after.revision !== disk.revision)
|
|
191
|
+
return rejected('conflict');
|
|
192
|
+
if (resolved.modelProfileState?.main.invalid)
|
|
193
|
+
return rejected('invalid');
|
|
194
|
+
return applyPreparedSnapshot(controller, controller.prepareModelSettingsSnapshot(resolved), false);
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
return rejected('invalid');
|
|
198
|
+
}
|
|
199
|
+
}
|
|
@@ -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,
|
package/dist/cli.js
CHANGED
|
@@ -18,6 +18,7 @@ import '../runtime/insecure-tls-warning.cjs';
|
|
|
18
18
|
// Must precede every other dependency that imports Ink: CI detection is cached.
|
|
19
19
|
import { 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';
|
|
@@ -458,7 +459,7 @@ function runInteractive(flags, extras = {}) {
|
|
|
458
459
|
// advising the user about a mode they just disabled.
|
|
459
460
|
const wantPaste = config.paste && !!process.stdin.isTTY;
|
|
460
461
|
const pasteBridge = { notify: null };
|
|
461
|
-
const filter =
|
|
462
|
+
const filter = process.stdin.isTTY
|
|
462
463
|
? tryCreateStdinFilter(process.stdin, { mouse: wantMouse, paste: wantPaste }, (reason) => logger.warn('cli', 'stdin_filter_failed', { reason }), pasteBridge)
|
|
463
464
|
: null;
|
|
464
465
|
/**
|
|
@@ -648,6 +649,7 @@ function runInteractive(flags, extras = {}) {
|
|
|
648
649
|
};
|
|
649
650
|
const terminalBridge = {
|
|
650
651
|
mouseSelect: wantSelect,
|
|
652
|
+
deleteDisambiguated: filter !== null,
|
|
651
653
|
scrollbar,
|
|
652
654
|
...(mouseOn
|
|
653
655
|
? {
|
|
@@ -1009,6 +1011,18 @@ function runConfigSetContextWindow(value) {
|
|
|
1009
1011
|
process.stdout.write(`Set contextWindow = ${merged.contextWindow ?? 'auto'}\n`);
|
|
1010
1012
|
}
|
|
1011
1013
|
function runConfigSet(key, value) {
|
|
1014
|
+
try {
|
|
1015
|
+
runConfigSetValue(key, value);
|
|
1016
|
+
}
|
|
1017
|
+
catch (error) {
|
|
1018
|
+
const invalid = error instanceof ModelProfileConfigError
|
|
1019
|
+
&& !['read_failed', 'write_failed'].includes(error.code);
|
|
1020
|
+
process.stderr.write(error instanceof ModelProfileConfigError
|
|
1021
|
+
? `${error.message}\n` : 'Could not write config. Check file permissions.\n');
|
|
1022
|
+
process.exitCode = invalid ? 2 : 1;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
function runConfigSetValue(key, value) {
|
|
1012
1026
|
if (!CONFIG_SET_KEYS.has(key)) {
|
|
1013
1027
|
process.stderr.write(`Unknown config key "${key}". Known keys: ${[...CONFIG_SET_KEYS].join(', ')}.\n`);
|
|
1014
1028
|
process.exitCode = 2;
|