@aragon-agent/cli 0.6.12 → 0.6.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +108 -31
  3. package/dist/agent/controller.d.ts +38 -2
  4. package/dist/agent/controller.js +185 -34
  5. package/dist/agent/model-profile-settings.d.ts +31 -0
  6. package/dist/agent/model-profile-settings.js +208 -0
  7. package/dist/agent/queued-messages.d.ts +12 -0
  8. package/dist/agent/queued-messages.js +18 -0
  9. package/dist/agent/reducer.d.ts +13 -1
  10. package/dist/agent/reducer.js +52 -12
  11. package/dist/boot/guard.d.ts +3 -3
  12. package/dist/boot/guard.js +3 -3
  13. package/dist/cli.js +48 -16
  14. package/dist/commands/builtins.js +66 -39
  15. package/dist/commands/queue-command.d.ts +3 -0
  16. package/dist/commands/queue-command.js +5 -0
  17. package/dist/commands/registry.d.ts +3 -0
  18. package/dist/compaction/child.d.ts +4 -2
  19. package/dist/compaction/child.js +8 -1
  20. package/dist/compaction/compactor.d.ts +7 -2
  21. package/dist/compaction/compactor.js +21 -16
  22. package/dist/compaction/context-command.js +10 -3
  23. package/dist/compaction/meter.js +2 -0
  24. package/dist/compaction/pressure.d.ts +1 -0
  25. package/dist/compaction/pressure.js +1 -0
  26. package/dist/compaction/types.d.ts +2 -0
  27. package/dist/compaction/wiring.d.ts +4 -3
  28. package/dist/compaction/wiring.js +10 -2
  29. package/dist/config/cli-commands.d.ts +2 -0
  30. package/dist/config/cli-commands.js +16 -4
  31. package/dist/config/load.d.ts +5 -2
  32. package/dist/config/load.js +23 -12
  33. package/dist/config/model-profile-resolution.d.ts +20 -0
  34. package/dist/config/model-profile-resolution.js +81 -0
  35. package/dist/config/model-profile-store.d.ts +67 -0
  36. package/dist/config/model-profile-store.js +153 -0
  37. package/dist/config/model-profiles.d.ts +44 -0
  38. package/dist/config/model-profiles.js +121 -0
  39. package/dist/config/schema.d.ts +11 -2
  40. package/dist/config/store.d.ts +11 -1
  41. package/dist/config/store.js +43 -27
  42. package/dist/fast/resolve.d.ts +2 -1
  43. package/dist/fast/resolve.js +6 -4
  44. package/dist/fast/wiring.d.ts +5 -3
  45. package/dist/fast/wiring.js +8 -4
  46. package/dist/input/stdin-filter.js +3 -2
  47. package/dist/logging/install.js +4 -2
  48. package/dist/logging/secret-registry.d.ts +2 -0
  49. package/dist/logging/secret-registry.js +18 -0
  50. package/dist/team/runtime.d.ts +2 -1
  51. package/dist/team/subagent.d.ts +3 -1
  52. package/dist/team/subagent.js +3 -1
  53. package/dist/ui/ActivityLine.d.ts +6 -70
  54. package/dist/ui/ActivityLine.js +12 -34
  55. package/dist/ui/App.d.ts +1 -0
  56. package/dist/ui/App.js +116 -215
  57. package/dist/ui/BottomStatusRow.d.ts +6 -86
  58. package/dist/ui/BottomStatusRow.js +11 -26
  59. package/dist/ui/Composer.d.ts +16 -83
  60. package/dist/ui/Composer.js +9 -100
  61. package/dist/ui/Logo.d.ts +3 -5
  62. package/dist/ui/Logo.js +4 -8
  63. package/dist/ui/PromptInput.d.ts +4 -1
  64. package/dist/ui/PromptInput.js +44 -35
  65. package/dist/ui/QueuePanel.d.ts +8 -0
  66. package/dist/ui/QueuePanel.js +9 -0
  67. package/dist/ui/QueueStatusRow.d.ts +2 -0
  68. package/dist/ui/QueueStatusRow.js +1 -1
  69. package/dist/ui/StatusBar.d.ts +13 -149
  70. package/dist/ui/StatusBar.js +27 -142
  71. package/dist/ui/capabilities.d.ts +2 -0
  72. package/dist/ui/capabilities.js +6 -0
  73. package/dist/ui/clipboard-task.d.ts +29 -0
  74. package/dist/ui/clipboard-task.js +66 -0
  75. package/dist/ui/clipboard.d.ts +29 -50
  76. package/dist/ui/clipboard.js +141 -75
  77. package/dist/ui/color-runtime.d.ts +5 -0
  78. package/dist/ui/color-runtime.js +29 -0
  79. package/dist/ui/composer-limits.d.ts +3 -4
  80. package/dist/ui/composer-limits.js +4 -9
  81. package/dist/ui/composer-rows.d.ts +1 -1
  82. package/dist/ui/composer-rows.js +24 -137
  83. package/dist/ui/editor-navigation.d.ts +45 -0
  84. package/dist/ui/editor-navigation.js +85 -0
  85. package/dist/ui/editor-reducer.d.ts +4 -0
  86. package/dist/ui/editor-reducer.js +29 -15
  87. package/dist/ui/glyphs.js +10 -8
  88. package/dist/ui/ink-runtime.d.ts +3 -0
  89. package/dist/ui/ink-runtime.js +30 -0
  90. package/dist/ui/interaction-copy.d.ts +77 -0
  91. package/dist/ui/interaction-copy.js +40 -0
  92. package/dist/ui/interaction-hints.d.ts +18 -0
  93. package/dist/ui/interaction-hints.js +92 -0
  94. package/dist/ui/layout/budget.js +1 -1
  95. package/dist/ui/layout/queue-layout.d.ts +19 -0
  96. package/dist/ui/layout/queue-layout.js +26 -0
  97. package/dist/ui/layout/status-layout.d.ts +55 -0
  98. package/dist/ui/layout/status-layout.js +110 -0
  99. package/dist/ui/layout/todo-layout.d.ts +1 -0
  100. package/dist/ui/layout/todo-layout.js +1 -1
  101. package/dist/ui/overlays/HelpOverlay.js +8 -8
  102. package/dist/ui/overlays/ModelProfileEditor.d.ts +12 -0
  103. package/dist/ui/overlays/ModelProfileEditor.js +103 -0
  104. package/dist/ui/overlays/ModelProfilePicker.d.ts +35 -0
  105. package/dist/ui/overlays/ModelProfilePicker.js +167 -0
  106. package/dist/ui/overlays/QueueOverlay.d.ts +22 -0
  107. package/dist/ui/overlays/QueueOverlay.js +79 -0
  108. package/dist/ui/overlays/SettingsScreen.d.ts +9 -2
  109. package/dist/ui/overlays/SettingsScreen.js +347 -3
  110. package/dist/ui/overlays/model-profile-state.d.ts +74 -0
  111. package/dist/ui/overlays/model-profile-state.js +119 -0
  112. package/dist/ui/render-cache.d.ts +2 -2
  113. package/dist/ui/render-cache.js +4 -3
  114. package/dist/ui/run-status-row.d.ts +7 -19
  115. package/dist/ui/run-status-row.js +2 -18
  116. package/dist/ui/selection/selection-controller.d.ts +2 -2
  117. package/dist/ui/selection/selection-controller.js +1 -0
  118. package/dist/ui/use-wheel-routing.js +2 -1
  119. 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
+ }
@@ -12,7 +12,7 @@
12
12
  * - `Markdown` re-split and re-regexed the whole answer into one React element
13
13
  * per line on every render.
14
14
  *
15
- * Everything cached here is a function of its INPUT STRING ALONE. Theme and
15
+ * Cached text depends on its input and, for highlighting, the color level. Theme and
16
16
  * glyphs are applied at render time and are never cached, because a themed
17
17
  * React element in a cache makes `/theme` a no-op until the entry evicts (K-5) —
18
18
  * exactly the class of silent bug this package writes comments to avoid.
@@ -50,7 +50,7 @@ export interface RenderCacheStats {
50
50
  misses: number;
51
51
  }
52
52
  /**
53
- * Syntax-highlight `code`, memoised on `(lang, code)`.
53
+ * Syntax-highlight `code`, memoised on `(colorLevel, lang, code)`.
54
54
  *
55
55
  * `cli-highlight` emits chalk-level ANSI that does not depend on `theme.*`, so
56
56
  * unlike a rendered element this string IS safe to cache across a `/theme`
@@ -12,7 +12,7 @@
12
12
  * - `Markdown` re-split and re-regexed the whole answer into one React element
13
13
  * per line on every render.
14
14
  *
15
- * Everything cached here is a function of its INPUT STRING ALONE. Theme and
15
+ * Cached text depends on its input and, for highlighting, the color level. Theme and
16
16
  * glyphs are applied at render time and are never cached, because a themed
17
17
  * React element in a cache makes `/theme` a no-op until the entry evicts (K-5) —
18
18
  * exactly the class of silent bug this package writes comments to avoid.
@@ -22,6 +22,7 @@
22
22
  * caches would miss precisely when a scroll brings an old entry back.
23
23
  */
24
24
  import { highlight } from 'cli-highlight';
25
+ import { syntaxColorLevel } from './color-runtime.js';
25
26
  import { parseMarkdownBlocks } from './markdown-blocks.js';
26
27
  import { pickGlyphs } from './glyphs.js';
27
28
  export const RENDER_CACHE_LIMITS = {
@@ -113,7 +114,7 @@ const markdownCache = new LruCache(RENDER_CACHE_LIMITS.markdownEntries, RENDER_C
113
114
  */
114
115
  const lineCache = new LruCache(RENDER_CACHE_LIMITS.linesEntries, RENDER_CACHE_LIMITS.markdownBytes);
115
116
  /**
116
- * Syntax-highlight `code`, memoised on `(lang, code)`.
117
+ * Syntax-highlight `code`, memoised on `(colorLevel, lang, code)`.
117
118
  *
118
119
  * `cli-highlight` emits chalk-level ANSI that does not depend on `theme.*`, so
119
120
  * unlike a rendered element this string IS safe to cache across a `/theme`
@@ -129,7 +130,7 @@ export function highlightCached(code, lang) {
129
130
  // ripgrep, which silently hides every definition in it from the tool this
130
131
  // codebase navigates itself with. NUL rather than a space because a space
131
132
  // still lets ("a b", "ts") and ("b", "ts a") collide on one key.
132
- const key = `${lang}\u0000${code}`;
133
+ const key = `${syntaxColorLevel()}\u0000${lang}\u0000${code}`;
133
134
  const hit = highlightCache.get(key);
134
135
  if (hit !== undefined)
135
136
  return hit;
@@ -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. */