@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
@@ -0,0 +1,18 @@
1
+ export interface ActionHintInput {
2
+ cols: number;
3
+ interactionPhase: 'idle' | 'starting' | 'running';
4
+ interruptHint?: string;
5
+ selectionPending?: boolean;
6
+ copyInFlight?: boolean;
7
+ copyCleanupPending?: boolean;
8
+ completion?: 'none' | 'slash' | 'file';
9
+ services?: number;
10
+ overlay?: string | null;
11
+ hintsEnabled?: boolean;
12
+ toast?: string;
13
+ updateAvailable?: boolean;
14
+ exitHint?: string;
15
+ modeToggleKey?: string;
16
+ }
17
+ /** Esc describes its actual owner, including while transient feedback has the row. */
18
+ export declare function buildActionClauses(input: ActionHintInput): readonly string[];
@@ -0,0 +1,92 @@
1
+ import stringWidth from 'string-width';
2
+ import { interactionCopy as copy } from './interaction-copy.js';
3
+ import { cleanStatusText } from './layout/status-layout.js';
4
+ function escapeClause(input) {
5
+ if (input.overlay)
6
+ return copy.close;
7
+ if (input.completion && input.completion !== 'none')
8
+ return copy.closeCompletion;
9
+ if (input.interactionPhase === 'starting')
10
+ return /again|confirm/i.test(input.interruptHint ?? '')
11
+ ? copy.cancelStartAgain : copy.cancelStart;
12
+ if (input.interactionPhase !== 'running')
13
+ return '';
14
+ if (/force|\u5f3a\u5236/i.test(input.interruptHint ?? ''))
15
+ return copy.forceStop;
16
+ if (/again|confirm|\u518d\u6309/i.test(input.interruptHint ?? ''))
17
+ return copy.interruptAgain;
18
+ return copy.interrupt;
19
+ }
20
+ function contextClauses(input, escape) {
21
+ if (input.overlay) {
22
+ if (input.overlay === 'confirm')
23
+ return [escape, copy.approveTool, copy.rejectTool];
24
+ const selects = /question|model|session|settings/.test(input.overlay);
25
+ return [escape, selects ? copy.select : copy.scroll,
26
+ ...(selects ? [input.overlay === 'settings' ? copy.saveSettings : copy.confirm] : [])];
27
+ }
28
+ if (input.completion && input.completion !== 'none') {
29
+ const candidates = input.completion === 'slash'
30
+ ? [copy.executeCommand, escape, copy.complete, copy.select, copy.newline]
31
+ : [copy.complete, escape, copy.select, copy.newline];
32
+ if (input.completion === 'file' && input.interactionPhase !== 'starting')
33
+ candidates.push(input.interactionPhase === 'running' ? copy.enqueue : copy.send);
34
+ return candidates;
35
+ }
36
+ if (input.interactionPhase === 'starting')
37
+ return [copy.startingHint, escape];
38
+ const running = input.interactionPhase === 'running';
39
+ const narrow = input.cols < 72;
40
+ return [running ? (narrow ? copy.enqueue : copy.enqueueLong) : (narrow ? copy.send : copy.sendLong),
41
+ narrow ? copy.newline : copy.newlineLong, ...(escape ? [escape] : [])];
42
+ }
43
+ function fitClauses(candidates, budget) {
44
+ const result = [];
45
+ for (const text of candidates.filter(Boolean)) {
46
+ if (stringWidth([...result, text].join(' | ')) <= budget)
47
+ result.push(text);
48
+ }
49
+ return result;
50
+ }
51
+ /** Esc describes its actual owner, including while transient feedback has the row. */
52
+ export function buildActionClauses(input) {
53
+ const budget = Math.max(0, Math.floor(input.cols) - 1);
54
+ const escape = escapeClause(input);
55
+ const copyFeedback = input.copyCleanupPending
56
+ ? (input.toast?.includes(copy.copyFailed) ? input.toast : copy.copyCleanup)
57
+ : input.copyInFlight ? copy.copying : '';
58
+ const feedback = input.exitHint || copyFeedback || input.toast || '';
59
+ if (feedback) {
60
+ const reserved = escape ? [escape] : [];
61
+ const remaining = budget - stringWidth(reserved.join(' | ')) - (reserved.length ? 3 : 0);
62
+ let text = cleanStatusText(feedback);
63
+ if (stringWidth(text) > remaining) {
64
+ const minimum = text.includes(copy.copyFailed) ? copy.copyFailed : '';
65
+ let clipped = '';
66
+ for (const char of text) {
67
+ if (stringWidth(clipped + char) > remaining)
68
+ break;
69
+ clipped += char;
70
+ }
71
+ text = minimum && stringWidth(minimum) <= remaining ? minimum : clipped;
72
+ }
73
+ return fitClauses([text, ...reserved], budget);
74
+ }
75
+ const clauses = contextClauses(input, escape);
76
+ if (input.selectionPending)
77
+ clauses.unshift(copy.copy);
78
+ if (input.services && !input.selectionPending)
79
+ clauses.unshift(input.cols < 72
80
+ ? `${copy.stopServicesShort} ${input.services}` : `${copy.services} ${input.services} ${copy.stopServices}`);
81
+ if (input.updateAvailable && input.interactionPhase === 'idle' && !input.overlay)
82
+ clauses.unshift(copy.update);
83
+ if (!input.selectionPending && !input.services)
84
+ clauses.push(copy.exit);
85
+ if (input.hintsEnabled !== false && !input.overlay)
86
+ clauses.push(copy.shiftNewline, `${(input.modeToggleKey ?? 'shift+tab').split('+').map((key) => key.length === 1 ? key.toUpperCase() : key[0].toUpperCase() + key.slice(1).toLowerCase()).join('+')} ${copy.modeLabel}`, copy.help, '/ commands', '@ files');
87
+ // Reserve Esc before lower-priority clauses, but preserve the documented display order.
88
+ if (!escape)
89
+ return fitClauses(clauses, budget);
90
+ const selected = fitClauses(clauses.filter((clause) => clause !== escape), budget - stringWidth(escape) - 3);
91
+ return clauses.filter((clause) => clause === escape || selected.includes(clause));
92
+ }
@@ -18,7 +18,7 @@ export function chromeBudget(rows, draftRows = 1) {
18
18
  return {
19
19
  header: 1,
20
20
  toast: 1,
21
- composer: 2 + clampDraftRows(rows, draftRows) + (rows >= HINT_MIN_ROWS ? 1 : 0),
21
+ composer: 2 + clampDraftRows(rows, draftRows),
22
22
  status: 1,
23
23
  };
24
24
  }
@@ -0,0 +1,19 @@
1
+ import type { PendingSteering } from '../../agent/queued-messages.js';
2
+ export interface QueueLayoutInput {
3
+ pending: readonly PendingSteering[];
4
+ columns: number;
5
+ terminalRows: number;
6
+ availableRows: number;
7
+ paused: boolean;
8
+ }
9
+ export interface QueueLayout {
10
+ rows: number;
11
+ title: string;
12
+ items: readonly {
13
+ queueId: string;
14
+ label: string;
15
+ }[];
16
+ hiddenCount: number;
17
+ }
18
+ /** One allocation drives both footer measurement and rendered rows. */
19
+ export declare function buildQueueLayout(input: QueueLayoutInput): QueueLayout;
@@ -0,0 +1,26 @@
1
+ import stringWidth from 'string-width';
2
+ import { queueSummary, truncateSummary } from '../QueueStatusRow.js';
3
+ /** One allocation drives both footer measurement and rendered rows. */
4
+ export function buildQueueLayout(input) {
5
+ const count = input.pending.length;
6
+ const limit = Math.max(0, Math.min(input.terminalRows >= 20 ? 4 : 2, Math.floor(input.availableRows)));
7
+ if (!count || !limit)
8
+ return { rows: 0, title: '', items: [], hiddenCount: count };
9
+ const shown = Math.min(count, Math.max(1, limit - 1));
10
+ const items = input.pending.slice(0, shown).map((item, index) => {
11
+ const prefix = `${index + 1}. `;
12
+ const lines = item.text.split(/\r\n|[\r\n]/).length;
13
+ const suffix = lines > 1 ? ` (+${lines - 1}\u884c)` : '';
14
+ return { queueId: item.queueId, label: prefix + truncateSummary(queueSummary(item.text), Math.max(0, input.columns - stringWidth(prefix + suffix)), '...') + suffix };
15
+ });
16
+ const hiddenCount = count - items.length;
17
+ const paused = input.paused ? ' \u5df2\u6682\u505c' : '';
18
+ const hidden = hiddenCount ? ` \u53e6${hiddenCount}\u6761` : '';
19
+ const title = `Queue: \u5f85\u5904\u7406 ${count}${paused}${hidden} /queue`;
20
+ if (limit === 1)
21
+ return { rows: 1,
22
+ title: truncateSummary(`Queue ${count} /queue ${items[0].label}`, input.columns, '...'),
23
+ items, hiddenCount: count - items.length };
24
+ return { rows: 1 + items.length, title: stringWidth(title) <= input.columns
25
+ ? title : `Queue ${count} /queue`, items, hiddenCount };
26
+ }
@@ -0,0 +1,55 @@
1
+ import type { UsageTotal } from '../../agent/reducer.js';
2
+ import type { ContextUsageSnapshot } from '../../compaction/types.js';
3
+ export type RunPhase = 'idle' | 'starting' | 'waiting' | 'thinking' | 'generating' | 'preparing-tool' | 'tool';
4
+ export interface StatusLayoutInput {
5
+ columns: number;
6
+ phase: RunPhase;
7
+ pendingCount: number;
8
+ context: ContextUsageSnapshot;
9
+ usageTotal: UsageTotal;
10
+ thinkingLevel: string;
11
+ elapsedMs: number;
12
+ tokPerSec: number;
13
+ activeTool?: {
14
+ name: string;
15
+ toolCallId: string;
16
+ } | null;
17
+ runOutcome?: 'none' | 'ended' | 'interrupted' | 'failed';
18
+ spinner?: string;
19
+ waitingForConfirmation?: boolean;
20
+ stopping?: boolean;
21
+ compacting?: boolean;
22
+ retryActive?: {
23
+ attempt: number;
24
+ max: number;
25
+ secondsLeft: number;
26
+ };
27
+ mode?: string;
28
+ pendingMode?: string | null;
29
+ servicesActive?: {
30
+ live: number;
31
+ };
32
+ teamActive?: {
33
+ running: number;
34
+ total: number;
35
+ };
36
+ todoActive?: {
37
+ done: number;
38
+ total: number;
39
+ };
40
+ ecoRung?: number;
41
+ fastActive?: {
42
+ inFlight: boolean;
43
+ };
44
+ scrolledLines?: number;
45
+ }
46
+ export interface StatusField {
47
+ id: string;
48
+ text: string;
49
+ cells: number;
50
+ tone: 'normal' | 'muted' | 'warning' | 'error';
51
+ }
52
+ export declare function cleanStatusText(text: string): string;
53
+ export declare function phaseLabels(input: Pick<StatusLayoutInput, 'phase' | 'activeTool' | 'runOutcome' | 'waitingForConfirmation' | 'stopping' | 'compacting' | 'retryActive'>): readonly [string, string];
54
+ /** Allocate complete fields before rendering; only the redraw carrier lives outside this budget. */
55
+ export declare function planStatusFields(input: StatusLayoutInput): readonly StatusField[];
@@ -0,0 +1,110 @@
1
+ import stringWidth from 'string-width';
2
+ import stripAnsi from 'strip-ansi';
3
+ import { formatCost, formatDuration, formatTokens, promptTokensOf } from '../../agent/usage.js';
4
+ import { interactionCopy as copy } from '../interaction-copy.js';
5
+ export function cleanStatusText(text) {
6
+ // eslint-disable-next-line no-control-regex
7
+ return stripAnsi(text).replace(/[\x00-\x1f\x7f-\x9f\u2028\u2029]/g, ' ').trim();
8
+ }
9
+ export function phaseLabels(input) {
10
+ if (input.waitingForConfirmation)
11
+ return copy.confirming;
12
+ if (input.stopping)
13
+ return copy.stopping;
14
+ if (input.compacting)
15
+ return copy.compacting;
16
+ if (input.retryActive)
17
+ return [copy.retrying[0], `${copy.retrying[1]} ${Math.max(0, Math.ceil(input.retryActive.secondsLeft))}s`];
18
+ if (input.activeTool) {
19
+ const name = input.activeTool.name;
20
+ if (name === 'read_file')
21
+ return copy.readFile;
22
+ if (name === 'write_file' || name === 'edit_file')
23
+ return copy.writeFile;
24
+ if (name === 'bash')
25
+ return copy.command;
26
+ return [copy.tool[0], `${copy.tool[1]} ${cleanStatusText(name)}`];
27
+ }
28
+ if (input.phase === 'idle' && input.runOutcome && input.runOutcome !== 'none')
29
+ return copy[input.runOutcome];
30
+ if (input.phase === 'preparing-tool')
31
+ return copy.preparingTool;
32
+ return copy[input.phase];
33
+ }
34
+ const field = (id, text, tone = 'muted') => ({ id, text, cells: stringWidth(text), tone });
35
+ const width = (fields) => fields.reduce((sum, item) => sum + item.cells, 0) + Math.max(0, fields.length - 1) * 3;
36
+ const scientific = (value) => value.toExponential(1).replace(/\.0e/, 'e').replace('e+', 'e');
37
+ const compactNumber = (value) => value >= 1e9 ? scientific(value) : formatTokens(value).replace(/\.0([kM])$/, '$1');
38
+ function contextText(context, detail = false, exponent = false) {
39
+ if (![context.occupied, context.window, context.deltaTokens, context.pct].every(Number.isFinite)
40
+ || context.occupied < 0 || context.window <= 0)
41
+ return copy.contextUnknown;
42
+ const number = exponent ? scientific : detail ? formatTokens : compactNumber;
43
+ const approximate = context.source === 'estimate' || context.deltaTokens > 0 ? '~' : '';
44
+ const limit = context.windowKnown ? number(context.window) : '?';
45
+ return `${copy.context} ${approximate}${number(context.occupied)}/${limit} ${detail ? 'tokens' : 'tok'}`;
46
+ }
47
+ function secondaryFields(input) {
48
+ const list = [];
49
+ if ((input.mode && input.mode !== 'build') || input.pendingMode)
50
+ list.push(field('mode', input.pendingMode ? `${input.mode ?? 'build'}>${input.pendingMode}` : input.mode, 'normal'));
51
+ if (input.servicesActive?.live)
52
+ list.push(field('services', `${copy.services} ${input.servicesActive.live}`, 'warning'));
53
+ if (Number.isFinite(input.usageTotal.costUsd))
54
+ list.push(field('cost', `${copy.estimatedCost} ${formatCost(input.usageTotal.costUsd).slice(1)}`));
55
+ if (input.phase !== 'idle') {
56
+ if (input.tokPerSec > 0 && Number.isFinite(input.tokPerSec))
57
+ list.push(field('speed', `${copy.speed} ${input.tokPerSec} tok/s`));
58
+ if (Number.isFinite(input.elapsedMs))
59
+ list.push(field('elapsed', `${copy.elapsed} ${formatDuration(input.elapsedMs)}`));
60
+ }
61
+ if (input.thinkingLevel !== 'off')
62
+ list.push(field('thinking', `think:${cleanStatusText(input.thinkingLevel)}`));
63
+ list.push(field('usage', `${copy.sessionInput} ${compactNumber(promptTokensOf(input.usageTotal))} ${copy.output} ${compactNumber(input.usageTotal.outputTokens)} tok`));
64
+ if (input.context.windowKnown && Number.isFinite(input.context.pct))
65
+ list.push(field('percentage', `${input.context.source === 'estimate' || input.context.deltaTokens > 0 || !input.context.windowKnown ? '~' : ''}${Math.round(input.context.pct)}%`));
66
+ if (input.ecoRung)
67
+ list.push(field('eco', 'eco'));
68
+ if (input.fastActive)
69
+ list.push(field('fast', `fast${input.fastActive.inFlight ? '*' : ''}`));
70
+ if (input.teamActive)
71
+ list.push(field('team', `agents ${input.teamActive.running}/${input.teamActive.total}`));
72
+ if (input.todoActive)
73
+ list.push(field('todo', `todo ${input.todoActive.done}/${input.todoActive.total}`));
74
+ if (input.scrolledLines)
75
+ list.push(field('scroll', `^${input.scrolledLines}`, 'warning'));
76
+ return list;
77
+ }
78
+ /** Allocate complete fields before rendering; only the redraw carrier lives outside this budget. */
79
+ export function planStatusFields(input) {
80
+ const budget = Math.max(0, Math.floor(input.columns) - 1);
81
+ const [short, long] = phaseLabels(input);
82
+ const prefix = input.spinner ? `${input.spinner} ` : '';
83
+ const phaseTone = input.runOutcome === 'failed' ? 'error' : 'normal';
84
+ const fields = [field('phase', prefix + short, phaseTone)];
85
+ if (input.pendingCount > 0)
86
+ fields.push(field('queue', `${copy.queue} ${input.pendingCount}`, 'warning'));
87
+ fields.push(field('context', contextText(input.context)));
88
+ const contextIndex = fields.length - 1;
89
+ if (width(fields) > budget)
90
+ fields[contextIndex] = field('context', contextText(input.context, false, true));
91
+ if (width(fields) > budget)
92
+ fields[contextIndex] = field('context', '/context');
93
+ if (width(fields) > budget && input.pendingCount > 0)
94
+ fields[1] = field('queue', '/queue', 'warning');
95
+ if (width(fields) > budget)
96
+ return fields.filter((_f, index) => width(fields.slice(0, index + 1)) <= budget);
97
+ for (const candidate of secondaryFields(input)) {
98
+ if (width([...fields, candidate]) <= budget)
99
+ fields.push(candidate);
100
+ }
101
+ const upgrades = [field('phase', prefix + long, phaseTone),
102
+ field('queue', `${copy.pending} ${input.pendingCount}`, 'warning'),
103
+ field('context', contextText(input.context, true))];
104
+ for (const upgrade of upgrades) {
105
+ const index = fields.findIndex((f) => f.id === upgrade.id);
106
+ if (index >= 0 && width(fields) - fields[index].cells + upgrade.cells <= budget)
107
+ fields[index] = upgrade;
108
+ }
109
+ return fields;
110
+ }
@@ -5,6 +5,7 @@ export interface TodoRailLayoutInput {
5
5
  itemCount: number;
6
6
  viewportBudget: number;
7
7
  teamRows: number;
8
+ queueRows?: number;
8
9
  popupRows: number;
9
10
  composerBaseRows?: number;
10
11
  }
@@ -8,7 +8,7 @@ export function buildTodoRailLayout(input) {
8
8
  const cols = nonNegativeInt(input.cols);
9
9
  const budget = nonNegativeInt(input.viewportBudget);
10
10
  const teamRows = Math.min(budget, nonNegativeInt(input.teamRows));
11
- const popupMaxHeight = Math.max(0, budget - teamRows - TODO_LIMITS.panelMinRows
11
+ const popupMaxHeight = Math.max(0, budget - teamRows - nonNegativeInt(input.queueRows ?? 0) - 1
12
12
  - nonNegativeInt(input.composerBaseRows ?? 0));
13
13
  const popupRows = popupMaxHeight < 3 ? 0 : Math.min(nonNegativeInt(input.popupRows), popupMaxHeight);
14
14
  const rows = budget;
@@ -5,7 +5,7 @@ const KEY_PAD = 24;
5
5
  function keyRows(times) {
6
6
  return [
7
7
  ['Enter', 'Submit (idle) / queue steering (running); re-pins to the newest output'],
8
- ['Alt+Enter / Shift+Enter', 'Insert newline'],
8
+ ['Alt+Enter / Shift+Enter', 'Newline when supported; the host may intercept either binding'],
9
9
  ['Ctrl+J', 'Newline fallback; /terminal-setup explains Shift+Enter binding'],
10
10
  ['Esc', 'Close menu / overlay'],
11
11
  ['Esc twice', 'Interrupt run (within 1.5s)'],
@@ -28,15 +28,14 @@ function keyRows(times) {
28
28
  // a key, so it has to be named somewhere they can look it up. The one-shot
29
29
  // startup notice is the other place, and it scrolls away.
30
30
  ['Drag right edge', 'Scroll messages and input together; type to return'],
31
- ['Run status row', 'Spinner and queue / interrupt keys, above the input during a run'],
31
+ ['Bottom rows', 'Input, fixed action hints, then global runtime / queue / context status'],
32
32
  ['Drag text (left button)', 'Select text on screen; the highlight waits'],
33
33
  ['Ctrl+C (with a selection)', 'Copy the pending selection - does not arm exit'],
34
34
  ['Native terminal selection', 'Copy-on-select is controlled by your terminal settings'],
35
- ['Queue', 'Waits for a message receipt, not turn start; paused means not running'],
36
- ['P>B / B>P', 'A pending mode switch; P=plan, B=build'],
37
- ['E2 / E!', 'Esc twice to interrupt / Esc again to force-stop'],
38
- ['S+ / T+', 'Services / TODO present; /bg and /todo status show counts'],
39
- ['Queue(p) / (+99+)', 'Paused queue / at least 100 additional messages'],
35
+ ['Queue / /queue', 'Pending receipt; /queue opens full text, PgUp/PgDn pages, Esc closes'],
36
+ ['plan>build / build>plan', 'A mode switch pending until the current run ends'],
37
+ ['/bg / /todo status', 'Show full service and TODO counts when status fields are hidden'],
38
+ ['Queue paused', 'Unreceived messages persist after interruption until accepted or cancelled'],
40
39
  ['Up / Down', 'Prompt history (empty input)'],
41
40
  // Pasting is the one affordance here with no key of its own, so the only
42
41
  // place a user can learn what happened to their 218 lines is this row.
@@ -63,7 +62,8 @@ const COMMANDS = [
63
62
  ['/cwd [dir]', 'Show or change the tool working directory'],
64
63
  ['/save [file]', 'Save the session to JSON'],
65
64
  ['/resume [file]', 'Load a saved session'],
66
- ['/copy', 'Copy the last answer to the clipboard (OSC 52, so it works over ssh)'],
65
+ ['/copy', 'Copy last answer; native completion confirmed, OSC 52 request unconfirmed'],
66
+ ['/queue', 'Read every pending message in full; no replay or queue mutation'],
67
67
  ['/mouse [on|off]', 'Release the mouse to your terminal, or take it back'],
68
68
  ['/terminal-setup', 'Show newline key bindings without changing terminal settings'],
69
69
  ['/exit', 'Exit'],
@@ -0,0 +1,12 @@
1
+ import React from 'react';
2
+ import { type ProfileFrameProps } from './ModelProfilePicker.js';
3
+ import { type ProfileEditorDraft, type ModelProfileAction } from './model-profile-state.js';
4
+ interface ModelProfileEditorProps extends ProfileFrameProps {
5
+ editor: ProfileEditorDraft;
6
+ users: string;
7
+ error?: string;
8
+ dispatch: React.Dispatch<ModelProfileAction>;
9
+ }
10
+ /** Replacement key input starts empty; retention across targets is explicit. */
11
+ export declare function ModelProfileEditor(props: ModelProfileEditorProps): React.ReactElement;
12
+ export {};
@@ -0,0 +1,103 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { Text, useInput } from 'ink';
4
+ import { ADAPTER_PROVIDERS } from '../../config/schema.js';
5
+ import { registerSecret } from '../../logging/secret-registry.js';
6
+ import { OverlayFrame } from '../layout/OverlayFrame.js';
7
+ import { stripPasteFrames } from '../paste-frames.js';
8
+ import { stripEnterFrames } from '../enter-frames.js';
9
+ import { pickGlyphs } from '../glyphs.js';
10
+ import { profileEndpointSummary, useProfileScroll } from './ModelProfilePicker.js';
11
+ import { finishProfileEditor } from './model-profile-state.js';
12
+ const FIELDS = ['name', 'provider', 'model', 'baseUrl', 'credentialMode', 'apiKey'];
13
+ const LABELS = ['Name', 'Provider', 'Model', 'Base URL', 'Credential mode', 'API key'];
14
+ function useEditorInput(props, navigation) {
15
+ const { editor, dispatch } = props;
16
+ const { index, setIndex, retain, count } = navigation;
17
+ function commit() {
18
+ const result = finishProfileEditor(editor);
19
+ if (result.field) {
20
+ const at = FIELDS.indexOf(result.field);
21
+ if (at >= 0)
22
+ setIndex(at);
23
+ }
24
+ dispatch({ type: 'merge-editor' });
25
+ }
26
+ useInput((input, key) => {
27
+ if (key.escape) {
28
+ dispatch({ type: 'cancel-editor' });
29
+ return;
30
+ }
31
+ if (key.upArrow || (key.tab && key.shift)) {
32
+ setIndex((index - 1 + count) % count);
33
+ return;
34
+ }
35
+ if (key.downArrow || key.tab) {
36
+ setIndex((index + 1) % count);
37
+ return;
38
+ }
39
+ if (key.return) {
40
+ if (retain && index === FIELDS.length)
41
+ dispatch({ type: 'retain-key' });
42
+ else if (index === count - 1)
43
+ dispatch({ type: 'cancel-editor' });
44
+ else
45
+ commit();
46
+ return;
47
+ }
48
+ const field = FIELDS[index];
49
+ if (!field || key.ctrl || key.meta)
50
+ return;
51
+ if (field === 'provider' || field === 'credentialMode') {
52
+ if (!key.leftArrow && !key.rightArrow)
53
+ return;
54
+ const options = field === 'provider' ? ADAPTER_PROVIDERS
55
+ : editor.original.apiKey && !editor.endpointChanged ? ['keep', 'replace', 'shared']
56
+ : ['shared', 'replace'];
57
+ const current = options.indexOf(editor[field]);
58
+ dispatch({ type: 'editor-field', field,
59
+ value: options[(current + (key.leftArrow ? -1 : 1) + options.length) % options.length] });
60
+ return;
61
+ }
62
+ if (field === 'apiKey' && editor.credentialMode !== 'replace')
63
+ return;
64
+ const text = stripPasteFrames(stripEnterFrames(input));
65
+ const value = key.backspace || key.delete ? [...editor[field]].slice(0, -1).join('')
66
+ : editor[field] + text;
67
+ if (field === 'apiKey')
68
+ registerSecret(value);
69
+ if (key.backspace || key.delete || text)
70
+ dispatch({ type: 'editor-field', field, value });
71
+ });
72
+ }
73
+ function buildEditorRows(props, navigation) {
74
+ const { editor } = props;
75
+ const { index, retain } = navigation;
76
+ const glyphs = pickGlyphs(props.caps);
77
+ const values = FIELDS.map((field) => {
78
+ if (field === 'apiKey')
79
+ return editor.credentialMode === 'replace'
80
+ ? editor.apiKey ? 'Configured (replacement)' : '(type replacement)'
81
+ : editor.credentialMode === 'keep' ? 'Configured (unchanged)' : 'Shared / environment';
82
+ if (field === 'credentialMode')
83
+ return editor.credentialMode;
84
+ return editor[field] || (field === 'name' ? '(defaults to model)' : field === 'baseUrl'
85
+ ? '(provider default)' : '(required)');
86
+ });
87
+ const lines = LABELS.map((label, at) => `${label.padEnd(18)}${values[at]}`);
88
+ if (retain)
89
+ lines.push('Retain key for new endpoint');
90
+ lines.push('Apply to settings draft', 'Cancel editing');
91
+ 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));
92
+ rows.push(_jsxs(Text, { wrap: "truncate", color: props.theme.muted, children: ["Target: ", editor.provider, " ", profileEndpointSummary(editor.baseUrl)] }, "target"));
93
+ return rows;
94
+ }
95
+ /** Replacement key input starts empty; retention across targets is explicit. */
96
+ export function ModelProfileEditor(props) {
97
+ const [index, setIndex] = useState(0);
98
+ const retain = props.editor.endpointChanged && Boolean(props.editor.original.apiKey);
99
+ const navigation = { index, setIndex, retain, count: FIELDS.length + (retain ? 1 : 0) + 2 };
100
+ const offset = useProfileScroll(props, index);
101
+ useEditorInput(props, navigation);
102
+ return _jsx(OverlayFrame, { ...props, rows: buildEditorRows(props, navigation), scrollOffset: offset, title: props.error ?? `Edit profile${props.users ? ` | Used by ${props.users}` : ''}`, hint: "Enter apply to draft | Esc cancel" });
103
+ }
@@ -0,0 +1,35 @@
1
+ import React from 'react';
2
+ import type { ModelProfile, ModelProfilesConfig, ModelRole } from '../../config/model-profiles.js';
3
+ import type { Theme } from '../theme.js';
4
+ import type { TermCapabilities } from '../capabilities.js';
5
+ export interface ProfileFrameProps {
6
+ theme: Theme;
7
+ caps: TermCapabilities;
8
+ cols: number;
9
+ maxRows: number;
10
+ scrollOffset: number;
11
+ onScrollClamp?: (offset: number) => void;
12
+ }
13
+ /** Move focus into view while accepting App's independent wheel/page scrolling. */
14
+ export declare function useProfileScroll(props: ProfileFrameProps, focus: number): number;
15
+ /** Cut by terminal columns, preserving surrogate pairs and wide names. */
16
+ export declare function clipProfileText(value: string, columns: number): string;
17
+ export declare function profileEndpointSummary(baseUrl: string | null): string;
18
+ interface ModelProfilePickerProps extends ProfileFrameProps {
19
+ profiles: ModelProfilesConfig;
20
+ savedProfiles?: ModelProfilesConfig;
21
+ role: ModelRole;
22
+ manager?: boolean;
23
+ error?: string;
24
+ apiKeys: Record<string, string | undefined>;
25
+ customProfile?: ModelProfile;
26
+ onSelect: (id: string | null) => void;
27
+ onCreate: () => void;
28
+ onEdit: (profile: ModelProfile) => void;
29
+ onDuplicate: (id: string) => void;
30
+ onDelete: (id: string) => void;
31
+ onCancel: () => void;
32
+ }
33
+ /** Profile selection and explicit management actions share one input owner. */
34
+ export declare function ModelProfilePicker(props: ModelProfilePickerProps): React.ReactElement;
35
+ export {};