@aragon-agent/cli 0.6.12 → 0.6.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/README.md +81 -15
  2. package/dist/agent/controller.d.ts +29 -1
  3. package/dist/agent/controller.js +123 -22
  4. package/dist/agent/model-profile-settings.d.ts +31 -0
  5. package/dist/agent/model-profile-settings.js +199 -0
  6. package/dist/agent/queued-messages.d.ts +12 -0
  7. package/dist/agent/queued-messages.js +18 -0
  8. package/dist/agent/reducer.d.ts +13 -1
  9. package/dist/agent/reducer.js +51 -12
  10. package/dist/cli.js +15 -1
  11. package/dist/commands/builtins.js +66 -39
  12. package/dist/commands/queue-command.d.ts +3 -0
  13. package/dist/commands/queue-command.js +5 -0
  14. package/dist/commands/registry.d.ts +3 -0
  15. package/dist/compaction/child.d.ts +4 -2
  16. package/dist/compaction/child.js +4 -1
  17. package/dist/compaction/compactor.d.ts +7 -2
  18. package/dist/compaction/compactor.js +13 -11
  19. package/dist/compaction/context-command.js +5 -0
  20. package/dist/compaction/wiring.d.ts +4 -3
  21. package/dist/compaction/wiring.js +6 -1
  22. package/dist/config/cli-commands.d.ts +2 -0
  23. package/dist/config/cli-commands.js +16 -4
  24. package/dist/config/load.d.ts +2 -1
  25. package/dist/config/load.js +15 -8
  26. package/dist/config/model-profile-resolution.d.ts +20 -0
  27. package/dist/config/model-profile-resolution.js +81 -0
  28. package/dist/config/model-profile-store.d.ts +67 -0
  29. package/dist/config/model-profile-store.js +153 -0
  30. package/dist/config/model-profiles.d.ts +44 -0
  31. package/dist/config/model-profiles.js +121 -0
  32. package/dist/config/schema.d.ts +9 -0
  33. package/dist/config/store.d.ts +11 -1
  34. package/dist/config/store.js +43 -27
  35. package/dist/fast/resolve.d.ts +2 -1
  36. package/dist/fast/resolve.js +6 -4
  37. package/dist/fast/wiring.d.ts +5 -3
  38. package/dist/fast/wiring.js +8 -4
  39. package/dist/input/stdin-filter.js +3 -2
  40. package/dist/logging/install.js +4 -2
  41. package/dist/logging/secret-registry.d.ts +2 -0
  42. package/dist/logging/secret-registry.js +18 -0
  43. package/dist/team/runtime.d.ts +2 -1
  44. package/dist/team/subagent.d.ts +3 -1
  45. package/dist/team/subagent.js +3 -1
  46. package/dist/ui/ActivityLine.d.ts +6 -70
  47. package/dist/ui/ActivityLine.js +12 -34
  48. package/dist/ui/App.d.ts +1 -0
  49. package/dist/ui/App.js +113 -205
  50. package/dist/ui/BottomStatusRow.d.ts +6 -86
  51. package/dist/ui/BottomStatusRow.js +11 -26
  52. package/dist/ui/Composer.d.ts +16 -83
  53. package/dist/ui/Composer.js +9 -100
  54. package/dist/ui/PromptInput.d.ts +4 -1
  55. package/dist/ui/PromptInput.js +41 -34
  56. package/dist/ui/QueuePanel.d.ts +8 -0
  57. package/dist/ui/QueuePanel.js +9 -0
  58. package/dist/ui/QueueStatusRow.d.ts +2 -0
  59. package/dist/ui/QueueStatusRow.js +1 -1
  60. package/dist/ui/StatusBar.d.ts +13 -149
  61. package/dist/ui/StatusBar.js +27 -142
  62. package/dist/ui/clipboard-task.d.ts +29 -0
  63. package/dist/ui/clipboard-task.js +66 -0
  64. package/dist/ui/clipboard.d.ts +29 -50
  65. package/dist/ui/clipboard.js +141 -75
  66. package/dist/ui/composer-limits.d.ts +3 -4
  67. package/dist/ui/composer-limits.js +4 -9
  68. package/dist/ui/composer-rows.d.ts +1 -1
  69. package/dist/ui/composer-rows.js +24 -137
  70. package/dist/ui/editor-navigation.d.ts +45 -0
  71. package/dist/ui/editor-navigation.js +85 -0
  72. package/dist/ui/editor-reducer.d.ts +4 -0
  73. package/dist/ui/editor-reducer.js +29 -15
  74. package/dist/ui/interaction-copy.d.ts +77 -0
  75. package/dist/ui/interaction-copy.js +40 -0
  76. package/dist/ui/interaction-hints.d.ts +18 -0
  77. package/dist/ui/interaction-hints.js +92 -0
  78. package/dist/ui/layout/budget.js +1 -1
  79. package/dist/ui/layout/queue-layout.d.ts +19 -0
  80. package/dist/ui/layout/queue-layout.js +26 -0
  81. package/dist/ui/layout/status-layout.d.ts +55 -0
  82. package/dist/ui/layout/status-layout.js +109 -0
  83. package/dist/ui/layout/todo-layout.d.ts +1 -0
  84. package/dist/ui/layout/todo-layout.js +1 -1
  85. package/dist/ui/overlays/HelpOverlay.js +8 -8
  86. package/dist/ui/overlays/ModelProfileEditor.d.ts +12 -0
  87. package/dist/ui/overlays/ModelProfileEditor.js +103 -0
  88. package/dist/ui/overlays/ModelProfilePicker.d.ts +35 -0
  89. package/dist/ui/overlays/ModelProfilePicker.js +167 -0
  90. package/dist/ui/overlays/QueueOverlay.d.ts +22 -0
  91. package/dist/ui/overlays/QueueOverlay.js +79 -0
  92. package/dist/ui/overlays/SettingsScreen.d.ts +9 -2
  93. package/dist/ui/overlays/SettingsScreen.js +347 -3
  94. package/dist/ui/overlays/model-profile-state.d.ts +74 -0
  95. package/dist/ui/overlays/model-profile-state.js +119 -0
  96. package/dist/ui/run-status-row.d.ts +7 -19
  97. package/dist/ui/run-status-row.js +2 -18
  98. package/dist/ui/selection/selection-controller.d.ts +2 -2
  99. package/dist/ui/selection/selection-controller.js +1 -0
  100. package/dist/ui/use-wheel-routing.js +2 -1
  101. package/package.json +2 -2
@@ -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({ initial, apiKeys, maxRows, cols, scrollOffset, onScrollClamp, theme, caps, onSave, retrySummary, }) {
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
- * The run status row's width plan (tui-scrollbar-edge-and-run-row §3.3.3).
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
- /** Everything the label needs, kept in one shape so there is one place that names it. */
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
- * The run status row's width plan (tui-scrollbar-edge-and-run-row §3.3.3).
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 { CopyVia } from '../clipboard.js';
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: ((via: CopyVia, lines: number, chars: number) => void) | null;
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;
@@ -209,6 +209,7 @@ export function createSelectionController(options) {
209
209
  // key, resize, overlay, /mouse off).
210
210
  settled = true;
211
211
  paint(true);
212
+ options.requestRedraw();
212
213
  };
213
214
  const onMouse = (event) => {
214
215
  if (disposed)