@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
|
@@ -8,7 +8,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
8
8
|
* The API key is never printed: it renders as dots while editing and masked
|
|
9
9
|
* otherwise.
|
|
10
10
|
*/
|
|
11
|
-
import { useState } from 'react';
|
|
11
|
+
import { useEffect, useReducer, useRef, useState } from 'react';
|
|
12
12
|
import { Text, useInput } from 'ink';
|
|
13
13
|
import { DEFAULT_MAX_OUTPUT_TOKENS, resolveOutputTokens, } from '@aragon-agent/core';
|
|
14
14
|
import { ADAPTER_PROVIDERS, THINKING_LEVELS, maskSecret, parseMaxTokensInput, parseThresholdInput, } from '../../config/schema.js';
|
|
@@ -17,6 +17,11 @@ import { pickGlyphs } from '../glyphs.js';
|
|
|
17
17
|
import { OverlayFrame } from '../layout/OverlayFrame.js';
|
|
18
18
|
import { stripPasteFrames } from '../paste-frames.js';
|
|
19
19
|
import { stripEnterFrames } from '../enter-frames.js';
|
|
20
|
+
import { emptyModelProfiles } from '../../config/model-profiles.js';
|
|
21
|
+
import { registerSecret } from '../../logging/secret-registry.js';
|
|
22
|
+
import { ModelProfilePicker, useProfileScroll } from './ModelProfilePicker.js';
|
|
23
|
+
import { ModelProfileEditor } from './ModelProfileEditor.js';
|
|
24
|
+
import { createModelProfileState, createCustomProfile, modelProfileReducer, profileUsers } from './model-profile-state.js';
|
|
20
25
|
const SHOW_THINKING_OPTIONS = ['off', 'on'];
|
|
21
26
|
const LIVE_OUTPUT_OPTIONS = ['off', 'on'];
|
|
22
27
|
const FAST_ENABLED_OPTIONS = ['off', 'on'];
|
|
@@ -238,7 +243,11 @@ export function effectiveCapLine(values) {
|
|
|
238
243
|
return `Effective: ${resolution.value} (auto)`;
|
|
239
244
|
return `Effective: ${resolution.value}`;
|
|
240
245
|
}
|
|
241
|
-
export function SettingsScreen(
|
|
246
|
+
export function SettingsScreen(props) {
|
|
247
|
+
return props.draft ? _jsx(ProfileSettingsScreen, { ...props, draft: props.draft })
|
|
248
|
+
: _jsx(LegacySettingsScreen, { ...props });
|
|
249
|
+
}
|
|
250
|
+
function LegacySettingsScreen({ initial, apiKeys, maxRows, cols, scrollOffset, onScrollClamp, theme, caps, onSave, retrySummary, }) {
|
|
242
251
|
const [values, setValues] = useState(initial);
|
|
243
252
|
const [index, setIndex] = useState(0);
|
|
244
253
|
const glyphs = pickGlyphs(caps);
|
|
@@ -278,7 +287,7 @@ export function SettingsScreen({ initial, apiKeys, maxRows, cols, scrollOffset,
|
|
|
278
287
|
return;
|
|
279
288
|
}
|
|
280
289
|
if (key.return) {
|
|
281
|
-
onSave(values);
|
|
290
|
+
onSave?.(values);
|
|
282
291
|
return;
|
|
283
292
|
}
|
|
284
293
|
if (field.kind === 'enum') {
|
|
@@ -373,3 +382,338 @@ export function SettingsScreen({ initial, apiKeys, maxRows, cols, scrollOffset,
|
|
|
373
382
|
rows.push(_jsx(Text, { wrap: "truncate", color: theme.muted, children: "Enter saves. Keys persist to the config file (0600)." }, "__save"));
|
|
374
383
|
return (_jsx(OverlayFrame, { title: "Settings", hint: `${glyphs.arrowUp}${glyphs.arrowDown} field ${glyphs.midDot} ${glyphs.arrowLeft}${glyphs.arrowRight} change ${glyphs.midDot} Enter save ${glyphs.midDot} Esc close`, maxRows: maxRows, cols: cols, rows: rows, scrollOffset: scrollOffset, onScrollClamp: onScrollClamp, theme: theme, caps: caps }));
|
|
375
384
|
}
|
|
385
|
+
const PROFILE_FIELDS = FIELDS.filter((field) => ![
|
|
386
|
+
'provider', 'model', 'baseUrl', 'apiKey', 'fastProvider', 'fastModel', 'fastEnabled',
|
|
387
|
+
].includes(field.key));
|
|
388
|
+
function profileSettingsValues(initial, draft) {
|
|
389
|
+
const file = draft.baseline;
|
|
390
|
+
return { ...initial, provider: file.provider ?? initial.provider,
|
|
391
|
+
model: file.model ?? initial.model,
|
|
392
|
+
baseUrl: file.baseUrl ?? '', apiKey: '', fastModel: file.fast?.model ?? '',
|
|
393
|
+
fastProvider: file.fast?.provider || FAST_INHERIT_PROVIDER };
|
|
394
|
+
}
|
|
395
|
+
/** Only fields changed by the user belong in a settings transaction. */
|
|
396
|
+
export function readDirtySettings(initial, values) {
|
|
397
|
+
const patch = {};
|
|
398
|
+
const dirty = (key) => initial[key] !== values[key];
|
|
399
|
+
if (dirty('provider'))
|
|
400
|
+
patch.provider = values.provider;
|
|
401
|
+
if (dirty('model'))
|
|
402
|
+
patch.model = values.model.trim();
|
|
403
|
+
if (dirty('baseUrl'))
|
|
404
|
+
patch.baseUrl = values.baseUrl.trim();
|
|
405
|
+
if (dirty('apiKey') && values.apiKey.trim()) {
|
|
406
|
+
patch.apiKeys = { [values.provider]: values.apiKey.trim() };
|
|
407
|
+
}
|
|
408
|
+
if (dirty('thinkingLevel'))
|
|
409
|
+
patch.thinkingLevel = values.thinkingLevel;
|
|
410
|
+
if (dirty('showThinking'))
|
|
411
|
+
patch.showThinking = values.showThinking === 'on';
|
|
412
|
+
if (dirty('liveToolOutput'))
|
|
413
|
+
patch.liveToolOutput = values.liveToolOutput === 'on';
|
|
414
|
+
if (dirty('logLevel'))
|
|
415
|
+
patch.log = { level: values.logLevel };
|
|
416
|
+
if (dirty('maxTokens')) {
|
|
417
|
+
const parsed = parseMaxTokensInput(values.maxTokens);
|
|
418
|
+
if (parsed.kind === 'invalid')
|
|
419
|
+
throw new Error('Max tokens: use a number or auto.');
|
|
420
|
+
patch.maxTokens = parsed.kind === 'auto' ? null : parsed.value;
|
|
421
|
+
}
|
|
422
|
+
const fast = {};
|
|
423
|
+
const parsedFast = readFastSettings(values);
|
|
424
|
+
if (dirty('fastModel'))
|
|
425
|
+
fast.model = parsedFast.model;
|
|
426
|
+
if (dirty('fastProvider'))
|
|
427
|
+
fast.provider = parsedFast.provider;
|
|
428
|
+
if (dirty('fastEnabled'))
|
|
429
|
+
fast.enabled = parsedFast.enabled;
|
|
430
|
+
if (dirty('fastReview')) {
|
|
431
|
+
if (parsedFast.review === undefined)
|
|
432
|
+
throw new Error('Fast review: use a turn count or off.');
|
|
433
|
+
fast.review = parsedFast.review;
|
|
434
|
+
if (parsedFast.reviewEveryTurns !== undefined) {
|
|
435
|
+
fast.reviewEveryTurns = parsedFast.reviewEveryTurns;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
if (dirty('fastReviewBudget')) {
|
|
439
|
+
if (parsedFast.reviewMaxPerSession === undefined) {
|
|
440
|
+
throw new Error('Fast budget: use a number.');
|
|
441
|
+
}
|
|
442
|
+
fast.reviewMaxPerSession = parsedFast.reviewMaxPerSession;
|
|
443
|
+
}
|
|
444
|
+
if (Object.keys(fast).length)
|
|
445
|
+
patch.fast = fast;
|
|
446
|
+
const compaction = {};
|
|
447
|
+
const parsedCompaction = readCompactionSettings(values);
|
|
448
|
+
const mapping = { compactionEnabled: 'enabled', compactionThreshold: 'threshold',
|
|
449
|
+
compactionKeepTurns: 'keepRecentTurns', compactionSubagents: 'subagents',
|
|
450
|
+
compactionArchive: 'archive' };
|
|
451
|
+
for (const [field, key] of Object.entries(mapping)) {
|
|
452
|
+
if (!dirty(field))
|
|
453
|
+
continue;
|
|
454
|
+
const value = parsedCompaction[key];
|
|
455
|
+
if (value === undefined) {
|
|
456
|
+
const label = FIELDS.find((item) => item.key === field).label;
|
|
457
|
+
throw new Error(`${label}: use a number.`);
|
|
458
|
+
}
|
|
459
|
+
Object.assign(compaction, { [key]: value });
|
|
460
|
+
}
|
|
461
|
+
if (Object.keys(compaction).length)
|
|
462
|
+
patch.compaction = compaction;
|
|
463
|
+
return patch;
|
|
464
|
+
}
|
|
465
|
+
function saveError(result) {
|
|
466
|
+
if (result.persisted)
|
|
467
|
+
return 'Saved; not fully applied. Close and restart. Save disabled.';
|
|
468
|
+
const errors = {
|
|
469
|
+
busy: 'Busy. Wait for running work to finish, then save again.',
|
|
470
|
+
conflict: 'Config changed elsewhere. Reload before saving.',
|
|
471
|
+
read_failed: 'Cannot read config. Repair with aragon config edit, then reload.',
|
|
472
|
+
write_failed: 'Could not save. Your changes are still here; retry Save.',
|
|
473
|
+
invalid: 'Invalid settings. Review the highlighted field and retry.',
|
|
474
|
+
};
|
|
475
|
+
return errors[result.code ?? ''] ?? 'Could not save. Review settings and retry.';
|
|
476
|
+
}
|
|
477
|
+
function useProfileSettingsState(props) {
|
|
478
|
+
const [state, dispatch] = useReducer(modelProfileReducer, props.draft, createModelProfileState);
|
|
479
|
+
const [initial, setInitial] = useState(() => profileSettingsValues(props.initial, props.draft));
|
|
480
|
+
const [values, setValues] = useState(initial);
|
|
481
|
+
const [index, setIndex] = useState(0);
|
|
482
|
+
const [error, setError] = useState();
|
|
483
|
+
const [persisted, setPersisted] = useState(false);
|
|
484
|
+
const [confirmIndex, setConfirmIndex] = useState(0);
|
|
485
|
+
const [reloadPending, setReloadPending] = useState(false);
|
|
486
|
+
const offset = useProfileScroll(props, index);
|
|
487
|
+
const previousPage = useRef(state.page);
|
|
488
|
+
useEffect(() => {
|
|
489
|
+
const previous = previousPage.current;
|
|
490
|
+
previousPage.current = state.page;
|
|
491
|
+
if (state.page !== 'root' || previous === 'discard-confirm')
|
|
492
|
+
return;
|
|
493
|
+
setIndex(0);
|
|
494
|
+
props.onScrollClamp?.(0);
|
|
495
|
+
}, [state.page]);
|
|
496
|
+
const library = state.draft.profiles ?? emptyModelProfiles();
|
|
497
|
+
const glyphs = pickGlyphs(props.caps);
|
|
498
|
+
const dirty = state.dirty || Object.keys(values).some((key) => values[key] !== initial[key]);
|
|
499
|
+
const fields = [FIELDS.find((item) => item.key === 'fastEnabled'),
|
|
500
|
+
...FIELDS.filter((item) => library.mainId === null
|
|
501
|
+
&& ['provider', 'model', 'baseUrl', 'apiKey'].includes(item.key)),
|
|
502
|
+
...FIELDS.filter((item) => library.fastId === null
|
|
503
|
+
&& ['fastModel', 'fastProvider'].includes(item.key)), ...PROFILE_FIELDS];
|
|
504
|
+
const count = fields.length + 5;
|
|
505
|
+
const field = fields[index - 3];
|
|
506
|
+
const readError = state.draft.readError;
|
|
507
|
+
const confirming = state.page === 'delete-confirm' || state.page === 'discard-confirm';
|
|
508
|
+
return { state, dispatch, initial, setInitial, values, setValues, index, setIndex,
|
|
509
|
+
error, setError, persisted, setPersisted, confirmIndex, setConfirmIndex,
|
|
510
|
+
reloadPending, setReloadPending, offset, library, glyphs, dirty, fields, count,
|
|
511
|
+
field, readError, confirming };
|
|
512
|
+
}
|
|
513
|
+
function saveProfileSettings(props, view) {
|
|
514
|
+
const { persisted, readError, initial, values, fields, state, setError, setIndex, setPersisted } = view;
|
|
515
|
+
if (persisted || readError || !props.onProfileSave)
|
|
516
|
+
return;
|
|
517
|
+
let patch;
|
|
518
|
+
try {
|
|
519
|
+
patch = readDirtySettings(initial, values);
|
|
520
|
+
}
|
|
521
|
+
catch (failure) {
|
|
522
|
+
const message = failure.message;
|
|
523
|
+
setError(message);
|
|
524
|
+
const at = fields.findIndex((item) => message.startsWith(`${item.label}:`));
|
|
525
|
+
if (at >= 0)
|
|
526
|
+
setIndex(at + 3);
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
const result = props.onProfileSave({
|
|
530
|
+
...state.draft, patch: { ...state.draft.patch, ...patch },
|
|
531
|
+
});
|
|
532
|
+
if (!result.ok) {
|
|
533
|
+
setError(saveError(result));
|
|
534
|
+
setPersisted(result.persisted);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
function reloadProfileSettings(props, view) {
|
|
538
|
+
const { dispatch, setInitial, setValues, setError, setIndex, setReloadPending } = view;
|
|
539
|
+
if (!props.onReload)
|
|
540
|
+
return;
|
|
541
|
+
const next = props.onReload();
|
|
542
|
+
dispatch({ type: 'reload', draft: next });
|
|
543
|
+
const live = next.liveBaseline;
|
|
544
|
+
const refreshed = { ...props.initial, provider: live.provider, model: live.model,
|
|
545
|
+
baseUrl: live.baseUrl ?? '', thinkingLevel: live.thinkingLevel,
|
|
546
|
+
showThinking: live.showThinking ? 'on' : 'off',
|
|
547
|
+
liveToolOutput: live.liveToolOutput ? 'on' : 'off',
|
|
548
|
+
maxTokens: live.maxTokens === undefined ? '' : String(live.maxTokens),
|
|
549
|
+
logLevel: live.log?.level ?? props.initial.logLevel,
|
|
550
|
+
...(live.fast ? fastSettingsFrom(live.fast) : {}),
|
|
551
|
+
...(live.compaction ? compactionSettingsFrom(live.compaction) : {}),
|
|
552
|
+
};
|
|
553
|
+
const nextValues = profileSettingsValues(refreshed, next);
|
|
554
|
+
setInitial(nextValues);
|
|
555
|
+
setValues(nextValues);
|
|
556
|
+
setError(undefined);
|
|
557
|
+
setIndex(0);
|
|
558
|
+
setReloadPending(false);
|
|
559
|
+
}
|
|
560
|
+
function cancelProfileSettings(props, view, reloading = false) {
|
|
561
|
+
const { persisted, dirty, setReloadPending, setConfirmIndex, dispatch } = view;
|
|
562
|
+
if (persisted) {
|
|
563
|
+
props.onClose?.();
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
if (!dirty) {
|
|
567
|
+
if (reloading)
|
|
568
|
+
reloadProfileSettings(props, view);
|
|
569
|
+
else
|
|
570
|
+
props.onClose?.();
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
setReloadPending(reloading);
|
|
574
|
+
setConfirmIndex(0);
|
|
575
|
+
dispatch({ type: 'page', page: 'discard-confirm' });
|
|
576
|
+
}
|
|
577
|
+
function activateRootSetting(props, view) {
|
|
578
|
+
const { index, count, readError, persisted, dispatch } = view;
|
|
579
|
+
if (index === count - 1) {
|
|
580
|
+
cancelProfileSettings(props, view, true);
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
if (readError || persisted)
|
|
584
|
+
return;
|
|
585
|
+
if (index < 2)
|
|
586
|
+
dispatch({ type: 'page', page: 'picker', role: index === 0 ? 'main' : 'fast' });
|
|
587
|
+
else if (index === 2)
|
|
588
|
+
dispatch({ type: 'page', page: 'manager' });
|
|
589
|
+
else
|
|
590
|
+
saveProfileSettings(props, view);
|
|
591
|
+
}
|
|
592
|
+
function useRootSettingsInput(props, view) {
|
|
593
|
+
const { index, count, field, readError, persisted, values, state, setIndex, setValues } = view;
|
|
594
|
+
useInput((input, key) => {
|
|
595
|
+
if (key.escape) {
|
|
596
|
+
cancelProfileSettings(props, view);
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
if (key.ctrl && input === 's') {
|
|
600
|
+
saveProfileSettings(props, view);
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
if (key.upArrow || (key.tab && key.shift)) {
|
|
604
|
+
setIndex((index - 1 + count) % count);
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
if (key.downArrow || key.tab) {
|
|
608
|
+
setIndex((index + 1) % count);
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
if (key.return) {
|
|
612
|
+
activateRootSetting(props, view);
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
if (!field || readError || persisted || key.ctrl || key.meta)
|
|
616
|
+
return;
|
|
617
|
+
if (field.kind === 'enum') {
|
|
618
|
+
if (!key.leftArrow && !key.rightArrow)
|
|
619
|
+
return;
|
|
620
|
+
const options = field.options;
|
|
621
|
+
const at = options.indexOf(String(values[field.key]));
|
|
622
|
+
setValues({ ...values, ...(field.key === 'provider' ? { apiKey: '' } : {}),
|
|
623
|
+
[field.key]: options[(at + (key.leftArrow ? -1 : 1) + options.length) % options.length] });
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
const text = stripPasteFrames(stripEnterFrames(input));
|
|
627
|
+
const value = key.backspace || key.delete
|
|
628
|
+
? [...String(values[field.key])].slice(0, -1).join('') : String(values[field.key]) + text;
|
|
629
|
+
if (field.key === 'apiKey')
|
|
630
|
+
registerSecret(value);
|
|
631
|
+
if (key.backspace || key.delete || text)
|
|
632
|
+
setValues({ ...values, [field.key]: value });
|
|
633
|
+
}, { isActive: state.page === 'root' });
|
|
634
|
+
}
|
|
635
|
+
function useSettingsConfirmationInput(props, view) {
|
|
636
|
+
const { state, dispatch, confirmIndex, setConfirmIndex, reloadPending, confirming } = view;
|
|
637
|
+
useInput((_input, key) => {
|
|
638
|
+
if (key.escape) {
|
|
639
|
+
dispatch({ type: 'page', page: state.page === 'delete-confirm'
|
|
640
|
+
? 'manager' : 'root' });
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
if (key.upArrow || key.downArrow || key.leftArrow || key.rightArrow || key.tab) {
|
|
644
|
+
setConfirmIndex((confirmIndex + 1) % 2);
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
if (!key.return)
|
|
648
|
+
return;
|
|
649
|
+
if (state.page === 'delete-confirm') {
|
|
650
|
+
dispatch(confirmIndex === 1 ? { type: 'confirm-delete' }
|
|
651
|
+
: { type: 'page', page: 'manager' });
|
|
652
|
+
}
|
|
653
|
+
else if (confirmIndex === 0)
|
|
654
|
+
dispatch({ type: 'page', page: 'root' });
|
|
655
|
+
else if (reloadPending)
|
|
656
|
+
reloadProfileSettings(props, view);
|
|
657
|
+
else
|
|
658
|
+
props.onClose?.();
|
|
659
|
+
}, { isActive: confirming });
|
|
660
|
+
}
|
|
661
|
+
function renderSettingsConfirmation(props, view) {
|
|
662
|
+
const { state, library, confirmIndex, glyphs } = view;
|
|
663
|
+
const deleting = state.page === 'delete-confirm';
|
|
664
|
+
const name = library.entries.find((entry) => entry.id === state.deleteId)?.name ?? '';
|
|
665
|
+
const labels = deleting ? ['Cancel', 'Delete'] : ['Continue', 'Discard'];
|
|
666
|
+
return _jsx(OverlayFrame, { ...props, title: deleting ? `Delete ${name}?` : 'Discard unsaved changes?', hint: "Enter choose | Esc continue", rows: labels
|
|
667
|
+
.map((label, at) => _jsxs(Text, { wrap: "truncate", color: props.theme.primary, children: [confirmIndex === at ? `${glyphs.caret} ` : ' ', label] }, label)), scrollOffset: 0 });
|
|
668
|
+
}
|
|
669
|
+
function renderProfileSubpage(props, view) {
|
|
670
|
+
const { state, library, dispatch, setConfirmIndex } = view;
|
|
671
|
+
if (state.page === 'editor' && state.editor)
|
|
672
|
+
return _jsx(ModelProfileEditor, { ...props, editor: state.editor, users: profileUsers(library, state.editor.original.id), error: state.error, dispatch: dispatch });
|
|
673
|
+
if (state.page === 'picker' || state.page === 'manager')
|
|
674
|
+
return _jsx(ModelProfilePicker, { ...props, profiles: library, savedProfiles: state.draft.baseline.modelProfiles, role: state.role, customProfile: createCustomProfile(state.draft, state.role), manager: state.page === 'manager', error: state.error, onSelect: (id) => dispatch({ type: 'select', role: state.role, id }), onCreate: () => dispatch({ type: 'edit', profile: createCustomProfile(state.draft, state.role),
|
|
675
|
+
bindNew: state.page === 'picker' }), onEdit: (profile) => dispatch({ type: 'edit', profile }), onDuplicate: (id) => dispatch({ type: 'duplicate', id }), onDelete: (id) => { setConfirmIndex(0); dispatch({ type: 'delete', id }); }, onCancel: () => dispatch({ type: 'page', page: 'root' }) });
|
|
676
|
+
return null;
|
|
677
|
+
}
|
|
678
|
+
function buildRootSettingsRows(props, view) {
|
|
679
|
+
const { library, fields, values, persisted, readError, index, glyphs, state } = view;
|
|
680
|
+
const name = (id) => {
|
|
681
|
+
const entry = library.entries.find((candidate) => candidate.id === id);
|
|
682
|
+
if (!entry)
|
|
683
|
+
return 'Current custom';
|
|
684
|
+
const missing = !entry.apiKey && !props.apiKeys[entry.provider];
|
|
685
|
+
return `${entry.name} ${entry.provider}:${entry.model}${missing ? ' [Missing API key]' : ''}`;
|
|
686
|
+
};
|
|
687
|
+
const lines = [`Main profile ${name(library.mainId)}`,
|
|
688
|
+
`Fast profile ${name(library.fastId)}`,
|
|
689
|
+
`Manage profiles ${library.entries.length} saved`,
|
|
690
|
+
...fields.map((item) => `${item.label.padEnd(18)}${item.key === 'apiKey'
|
|
691
|
+
? values.apiKey ? 'Configured (replacement)' : props.apiKeys[values.provider]
|
|
692
|
+
? 'Configured (type to replace)' : '(type to set)'
|
|
693
|
+
: values[item.key] || (item.key === 'maxTokens' ? 'auto' : '(not set)')}`),
|
|
694
|
+
persisted || readError ? 'Save (disabled)' : 'Save settings', 'Reload settings'];
|
|
695
|
+
const rows = lines.map((line, at) => _jsxs(Text, { wrap: "truncate", color: at === index ? props.theme.accent : props.theme.muted, children: [at === index ? `${glyphs.caret} ` : ' ', line] }, at));
|
|
696
|
+
rows.push(_jsx(Text, { wrap: "truncate", color: props.theme.muted, children: effectiveCapLine(values) }, "effective"));
|
|
697
|
+
const overrides = state.draft.liveBaseline.modelProfileState;
|
|
698
|
+
if (overrides?.main.overriddenFields.length || overrides?.fast.overriddenFields.length) {
|
|
699
|
+
rows.push(_jsx(Text, { wrap: "truncate", color: props.theme.muted, children: "Startup overrides active; explicit selection applies now. Restart uses startup overrides." }, "overrides"));
|
|
700
|
+
}
|
|
701
|
+
if (props.retrySummary)
|
|
702
|
+
rows.push(_jsxs(Text, { wrap: "truncate", color: props.theme.muted, children: ["API retries: ", props.retrySummary] }, "retry"));
|
|
703
|
+
return rows;
|
|
704
|
+
}
|
|
705
|
+
function ProfileSettingsScreen(props) {
|
|
706
|
+
const view = useProfileSettingsState(props);
|
|
707
|
+
useRootSettingsInput(props, view);
|
|
708
|
+
useSettingsConfirmationInput(props, view);
|
|
709
|
+
if (view.confirming)
|
|
710
|
+
return renderSettingsConfirmation(props, view);
|
|
711
|
+
const subpage = renderProfileSubpage(props, view);
|
|
712
|
+
if (subpage)
|
|
713
|
+
return subpage;
|
|
714
|
+
const { error, readError, dirty, persisted, index, offset } = view;
|
|
715
|
+
return _jsx(OverlayFrame, { ...props, rows: buildRootSettingsRows(props, view), scrollOffset: offset, title: error ?? (readError ? 'Cannot read config. Repair with aragon config edit; Reload.'
|
|
716
|
+
: dirty ? 'Settings | Unsaved changes' : 'Settings'), hint: persisted ? 'Esc close; restart to apply' : index < 3
|
|
717
|
+
? 'Enter select/edit | Ctrl+S save | Esc cancel'
|
|
718
|
+
: 'Enter save/action | Ctrl+S save | Esc cancel' });
|
|
719
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { type ModelProfile, type ModelProfilesConfig, type ModelRole } from '../../config/model-profiles.js';
|
|
2
|
+
import type { ModelSettingsDraft } from '../../config/model-profile-store.js';
|
|
3
|
+
export type ProfilePage = 'root' | 'picker' | 'manager' | 'editor' | 'delete-confirm' | 'discard-confirm';
|
|
4
|
+
export interface ProfileEditorDraft {
|
|
5
|
+
original: ModelProfile;
|
|
6
|
+
name: string;
|
|
7
|
+
provider: string;
|
|
8
|
+
model: string;
|
|
9
|
+
baseUrl: string;
|
|
10
|
+
credentialMode: 'keep' | 'replace' | 'shared';
|
|
11
|
+
apiKey: string;
|
|
12
|
+
endpointChanged: boolean;
|
|
13
|
+
}
|
|
14
|
+
export interface ModelProfileState {
|
|
15
|
+
draft: ModelSettingsDraft;
|
|
16
|
+
page: ProfilePage;
|
|
17
|
+
role: ModelRole;
|
|
18
|
+
editor?: ProfileEditorDraft;
|
|
19
|
+
returnPage: 'picker' | 'manager';
|
|
20
|
+
bindNew: boolean;
|
|
21
|
+
dirty: boolean;
|
|
22
|
+
error?: string;
|
|
23
|
+
deleteId?: string;
|
|
24
|
+
}
|
|
25
|
+
export type ModelProfileAction = {
|
|
26
|
+
type: 'page';
|
|
27
|
+
page: ProfilePage;
|
|
28
|
+
role?: ModelRole;
|
|
29
|
+
} | {
|
|
30
|
+
type: 'select';
|
|
31
|
+
role: ModelRole;
|
|
32
|
+
id: string | null;
|
|
33
|
+
} | {
|
|
34
|
+
type: 'edit';
|
|
35
|
+
profile: ModelProfile;
|
|
36
|
+
bindNew?: boolean;
|
|
37
|
+
} | {
|
|
38
|
+
type: 'editor-field';
|
|
39
|
+
field: keyof Omit<ProfileEditorDraft, 'original' | 'endpointChanged'>;
|
|
40
|
+
value: string;
|
|
41
|
+
} | {
|
|
42
|
+
type: 'retain-key';
|
|
43
|
+
} | {
|
|
44
|
+
type: 'merge-editor';
|
|
45
|
+
} | {
|
|
46
|
+
type: 'cancel-editor';
|
|
47
|
+
} | {
|
|
48
|
+
type: 'duplicate';
|
|
49
|
+
id: string;
|
|
50
|
+
} | {
|
|
51
|
+
type: 'delete';
|
|
52
|
+
id: string;
|
|
53
|
+
} | {
|
|
54
|
+
type: 'confirm-delete';
|
|
55
|
+
} | {
|
|
56
|
+
type: 'reload';
|
|
57
|
+
draft: ModelSettingsDraft;
|
|
58
|
+
};
|
|
59
|
+
/** Keep the original secret separate from the empty replacement field. */
|
|
60
|
+
export declare function createProfileEditor(profile: ModelProfile): ProfileEditorDraft;
|
|
61
|
+
/** Validate locally before merging the child form into the root transaction. */
|
|
62
|
+
export declare function finishProfileEditor(editor: ProfileEditorDraft): {
|
|
63
|
+
profile?: ModelProfile;
|
|
64
|
+
error?: string;
|
|
65
|
+
field?: string;
|
|
66
|
+
};
|
|
67
|
+
/** Make an isolated root draft; opening the screen never mutates live config. */
|
|
68
|
+
export declare function createModelProfileState(draft: ModelSettingsDraft): ModelProfileState;
|
|
69
|
+
/** Describe references using text so ASCII and no-color modes remain usable. */
|
|
70
|
+
export declare function profileUsers(profiles: ModelProfilesConfig | undefined, id: string): string;
|
|
71
|
+
/** Adopt persisted fields only, resolving the old fast inheritance once. */
|
|
72
|
+
export declare function createCustomProfile(draft: ModelSettingsDraft, role: ModelRole): ModelProfile;
|
|
73
|
+
/** All child actions modify only the root draft; persistence belongs to Save. */
|
|
74
|
+
export declare function modelProfileReducer(state: ModelProfileState, action: ModelProfileAction): ModelProfileState;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { emptyModelProfiles, duplicateModelProfile, normalizeModelProfiles, validateModelProfiles } from '../../config/model-profiles.js';
|
|
3
|
+
/** Keep the original secret separate from the empty replacement field. */
|
|
4
|
+
export function createProfileEditor(profile) {
|
|
5
|
+
return { original: { ...profile }, name: profile.name, provider: profile.provider,
|
|
6
|
+
model: profile.model, baseUrl: profile.baseUrl ?? '', apiKey: '',
|
|
7
|
+
credentialMode: profile.apiKey ? 'keep' : 'shared', endpointChanged: false };
|
|
8
|
+
}
|
|
9
|
+
/** Validate locally before merging the child form into the root transaction. */
|
|
10
|
+
export function finishProfileEditor(editor) {
|
|
11
|
+
const profile = { id: editor.original.id, name: editor.name.trim(),
|
|
12
|
+
provider: editor.provider, model: editor.model.trim(), baseUrl: editor.baseUrl.trim() || null,
|
|
13
|
+
apiKey: editor.credentialMode === 'keep' ? editor.original.apiKey
|
|
14
|
+
: editor.credentialMode === 'replace' ? editor.apiKey.trim() : null };
|
|
15
|
+
const library = { ...emptyModelProfiles(), entries: [profile] };
|
|
16
|
+
const issue = validateModelProfiles(library)[0];
|
|
17
|
+
if (issue)
|
|
18
|
+
return { error: issue.message, field: issue.path.split('.').at(-1) };
|
|
19
|
+
return { profile: normalizeModelProfiles(library).entries[0] };
|
|
20
|
+
}
|
|
21
|
+
/** Make an isolated root draft; opening the screen never mutates live config. */
|
|
22
|
+
export function createModelProfileState(draft) {
|
|
23
|
+
const copy = structuredClone(draft);
|
|
24
|
+
if (validateModelProfiles(copy.profiles).length
|
|
25
|
+
|| validateModelProfiles(copy.baseline.modelProfiles).length) {
|
|
26
|
+
copy.profiles = undefined;
|
|
27
|
+
copy.readError = 'invalid_profiles';
|
|
28
|
+
}
|
|
29
|
+
return { draft: copy, page: 'root', role: 'main', returnPage: 'manager',
|
|
30
|
+
bindNew: false, dirty: false };
|
|
31
|
+
}
|
|
32
|
+
/** Describe references using text so ASCII and no-color modes remain usable. */
|
|
33
|
+
export function profileUsers(profiles, id) {
|
|
34
|
+
return [profiles?.mainId === id ? 'Main' : '', profiles?.fastId === id ? 'Fast' : '']
|
|
35
|
+
.filter(Boolean).join(' + ');
|
|
36
|
+
}
|
|
37
|
+
/** Adopt persisted fields only, resolving the old fast inheritance once. */
|
|
38
|
+
export function createCustomProfile(draft, role) {
|
|
39
|
+
const file = draft.baseline;
|
|
40
|
+
const provider = (role === 'fast' ? file.fast?.provider : undefined)
|
|
41
|
+
|| file.provider || 'anthropic';
|
|
42
|
+
const model = role === 'fast' ? file.fast?.model ?? '' : file.model ?? '';
|
|
43
|
+
const baseUrl = role === 'fast' && !model ? null
|
|
44
|
+
: (role === 'fast' ? file.fast?.baseUrl
|
|
45
|
+
|| (provider === (file.provider || 'anthropic') ? file.baseUrl : null)
|
|
46
|
+
: file.baseUrl) || null;
|
|
47
|
+
return { id: randomUUID(), name: '', provider, model, baseUrl,
|
|
48
|
+
apiKey: role === 'fast' && !model ? null : file.apiKeys?.[provider] || null };
|
|
49
|
+
}
|
|
50
|
+
function editField(state, action) {
|
|
51
|
+
if (!state.editor)
|
|
52
|
+
return state;
|
|
53
|
+
const editor = { ...state.editor, [action.field]: action.value };
|
|
54
|
+
if (action.field === 'provider' || action.field === 'baseUrl') {
|
|
55
|
+
editor.apiKey = '';
|
|
56
|
+
editor.credentialMode = 'shared';
|
|
57
|
+
editor.endpointChanged = true;
|
|
58
|
+
}
|
|
59
|
+
if (action.field === 'credentialMode' && action.value !== 'replace')
|
|
60
|
+
editor.apiKey = '';
|
|
61
|
+
return { ...state, editor, error: undefined };
|
|
62
|
+
}
|
|
63
|
+
function mergeEditor(state) {
|
|
64
|
+
if (!state.editor)
|
|
65
|
+
return state;
|
|
66
|
+
const result = finishProfileEditor(state.editor);
|
|
67
|
+
if (!result.profile)
|
|
68
|
+
return { ...state, error: result.error };
|
|
69
|
+
const library = state.draft.profiles ?? emptyModelProfiles();
|
|
70
|
+
const exists = library.entries.some((entry) => entry.id === result.profile.id);
|
|
71
|
+
if (!exists && library.entries.length >= 200)
|
|
72
|
+
return { ...state,
|
|
73
|
+
error: 'Maximum 200 profiles. Delete an unused profile first.' };
|
|
74
|
+
const profiles = { ...library, entries: exists
|
|
75
|
+
? library.entries.map((entry) => entry.id === result.profile.id ? result.profile : entry)
|
|
76
|
+
: [...library.entries, result.profile] };
|
|
77
|
+
const merged = { ...state, draft: { ...state.draft, profiles }, dirty: true,
|
|
78
|
+
editor: undefined, page: state.returnPage, error: undefined };
|
|
79
|
+
return state.bindNew ? modelProfileReducer(merged, { type: 'select', role: state.role, id: result.profile.id }) : merged;
|
|
80
|
+
}
|
|
81
|
+
/** All child actions modify only the root draft; persistence belongs to Save. */
|
|
82
|
+
export function modelProfileReducer(state, action) {
|
|
83
|
+
const library = state.draft.profiles ?? emptyModelProfiles();
|
|
84
|
+
switch (action.type) {
|
|
85
|
+
case 'reload': return createModelProfileState(action.draft);
|
|
86
|
+
case 'page': return { ...state, page: action.page, role: action.role ?? state.role,
|
|
87
|
+
error: undefined };
|
|
88
|
+
case 'select': return { ...state, page: 'root', dirty: true, error: undefined,
|
|
89
|
+
draft: { ...state.draft, profiles: { ...library, [`${action.role}Id`]: action.id },
|
|
90
|
+
activateRoles: [...new Set([...state.draft.activateRoles, action.role])] } };
|
|
91
|
+
case 'edit': return { ...state, editor: createProfileEditor(action.profile), page: 'editor',
|
|
92
|
+
returnPage: state.page === 'picker' ? 'picker' : 'manager',
|
|
93
|
+
bindNew: action.bindNew ?? false, error: undefined };
|
|
94
|
+
case 'editor-field': return editField(state, action);
|
|
95
|
+
case 'retain-key': return state.editor?.original.apiKey ? { ...state,
|
|
96
|
+
editor: { ...state.editor, credentialMode: 'keep', apiKey: '' } } : state;
|
|
97
|
+
case 'merge-editor': return mergeEditor(state);
|
|
98
|
+
case 'cancel-editor': return { ...state, editor: undefined, page: state.returnPage,
|
|
99
|
+
error: undefined };
|
|
100
|
+
case 'duplicate': {
|
|
101
|
+
if (library.entries.length >= 200)
|
|
102
|
+
return { ...state,
|
|
103
|
+
error: 'Maximum 200 profiles. Delete an unused profile first.' };
|
|
104
|
+
const original = library.entries.find((entry) => entry.id === action.id);
|
|
105
|
+
return original ? { ...state, dirty: true, draft: { ...state.draft,
|
|
106
|
+
profiles: { ...library, entries: [...library.entries, duplicateModelProfile(original)] } } }
|
|
107
|
+
: state;
|
|
108
|
+
}
|
|
109
|
+
case 'delete': {
|
|
110
|
+
const users = profileUsers(library, action.id);
|
|
111
|
+
return users ? { ...state, error: `Used by ${users}. Select another profile first.` }
|
|
112
|
+
: { ...state, page: 'delete-confirm', deleteId: action.id, error: undefined };
|
|
113
|
+
}
|
|
114
|
+
case 'confirm-delete': return { ...state, page: 'manager', dirty: true,
|
|
115
|
+
draft: { ...state.draft, profiles: { ...library,
|
|
116
|
+
entries: library.entries.filter((entry) => entry.id !== state.deleteId) } },
|
|
117
|
+
deleteId: undefined };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -1,24 +1,12 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* While a run is in flight, ONE row above the input box carries the spinner, the
|
|
5
|
-
* phrase and the `steer / interrupt / exit` hint clauses, and `planRunRow` decides
|
|
6
|
-
* what survives when the line is too narrow. It is a pure function with no React
|
|
7
|
-
* and no I/O so the whole ladder can be tested as arithmetic.
|
|
8
|
-
*
|
|
9
|
-
* THE ALLOCATION IS PER CLAUSE, NOT PER STRING, and that is the whole point.
|
|
10
|
-
* The idle hint row this one replaces owned the full line width and ended in
|
|
11
|
-
* `wrap="truncate"`; handing the label half the line and truncating the joined
|
|
12
|
-
* hint would cut `interrupt` to `inter...` at 38 columns - a regression of the
|
|
13
|
-
* "running hint is NEVER abbreviated" rule `Composer.hintText` documents. So:
|
|
14
|
-
*
|
|
15
|
-
* - clauses are dropped WHOLE, from the tail (`exit`, then `stop N`), and
|
|
16
|
-
* - `steer` and `interrupt` (REQUIRED_CLAUSES) are never dropped or cut mid-word
|
|
17
|
-
* while the row is enabled, and
|
|
18
|
-
* - the only thing allowed to end in an ellipsis is the label.
|
|
1
|
+
/** Compatibility planner for former composer run rows. The current UI uses
|
|
2
|
+
* status-layout.ts and interaction-hints.ts; keep this pure API for existing callers.
|
|
19
3
|
*/
|
|
20
|
-
|
|
4
|
+
import type { StatusLayoutInput } from './layout/status-layout.js';
|
|
5
|
+
/** Shared activity facts; omitted phase fields retain the legacy phrase behavior. */
|
|
21
6
|
export interface RunActivity {
|
|
7
|
+
phase?: StatusLayoutInput['phase'];
|
|
8
|
+
outcome?: StatusLayoutInput['runOutcome'];
|
|
9
|
+
activeTool?: StatusLayoutInput['activeTool'];
|
|
22
10
|
/** Epoch ms the current run began - the phrase sequence's seed. */
|
|
23
11
|
startedAt: number;
|
|
24
12
|
/** `Date.now() - startedAt`, already computed by App's 200 ms ticker. */
|
|
@@ -1,21 +1,5 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* While a run is in flight, ONE row above the input box carries the spinner, the
|
|
5
|
-
* phrase and the `steer / interrupt / exit` hint clauses, and `planRunRow` decides
|
|
6
|
-
* what survives when the line is too narrow. It is a pure function with no React
|
|
7
|
-
* and no I/O so the whole ladder can be tested as arithmetic.
|
|
8
|
-
*
|
|
9
|
-
* THE ALLOCATION IS PER CLAUSE, NOT PER STRING, and that is the whole point.
|
|
10
|
-
* The idle hint row this one replaces owned the full line width and ended in
|
|
11
|
-
* `wrap="truncate"`; handing the label half the line and truncating the joined
|
|
12
|
-
* hint would cut `interrupt` to `inter...` at 38 columns - a regression of the
|
|
13
|
-
* "running hint is NEVER abbreviated" rule `Composer.hintText` documents. So:
|
|
14
|
-
*
|
|
15
|
-
* - clauses are dropped WHOLE, from the tail (`exit`, then `stop N`), and
|
|
16
|
-
* - `steer` and `interrupt` (REQUIRED_CLAUSES) are never dropped or cut mid-word
|
|
17
|
-
* while the row is enabled, and
|
|
18
|
-
* - the only thing allowed to end in an ellipsis is the label.
|
|
1
|
+
/** Compatibility planner for former composer run rows. The current UI uses
|
|
2
|
+
* status-layout.ts and interaction-hints.ts; keep this pure API for existing callers.
|
|
19
3
|
*/
|
|
20
4
|
/** One leading space, matching the idle hint row's `' '` indent. */
|
|
21
5
|
export const RUN_ROW_LEAD = 1;
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
* than by a staleness check that can be wrong.
|
|
29
29
|
*/
|
|
30
30
|
import type { MouseSource } from '../../input/stdin-filter.js';
|
|
31
|
-
import type {
|
|
31
|
+
import type { CopyResult } from '../clipboard.js';
|
|
32
32
|
import type { Theme } from '../theme.js';
|
|
33
33
|
import type { TermCapabilities } from '../capabilities.js';
|
|
34
34
|
/**
|
|
@@ -98,7 +98,7 @@ export interface SelectionController {
|
|
|
98
98
|
*/
|
|
99
99
|
export interface SelectionBridge {
|
|
100
100
|
controller: SelectionController | null;
|
|
101
|
-
onCopied: ((
|
|
101
|
+
onCopied: ((result: CopyResult, lines: number, chars: number) => void) | null;
|
|
102
102
|
requestRedraw: (() => void) | null;
|
|
103
103
|
}
|
|
104
104
|
export declare function createSelectionController(options: SelectionControllerOptions): SelectionController;
|