@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
@@ -23,6 +23,7 @@
23
23
  * Extracted from the component so it is unit-testable without mounting Ink.
24
24
  */
25
25
  import { expandRangeOverTokens, formatPasteToken, makePasteRecord, referencedIds, shouldCollapse, snapOutOfToken, } from './paste-tokens.js';
26
+ import { snapGrapheme, stepGrapheme } from './editor-navigation.js';
26
27
  const NO_PASTES = new Map();
27
28
  export const INITIAL_EDITOR_STATE = {
28
29
  buffer: '',
@@ -42,7 +43,9 @@ export const INITIAL_EDITOR_STATE = {
42
43
  * typing again, so suggestions should come back), and re-homes the selection
43
44
  * (the candidate list is about to be recomputed).
44
45
  */
45
- const DRAFT_FLAGS = { historyIndex: null, dismissed: false, sel: 0 };
46
+ const DRAFT_FLAGS = {
47
+ historyIndex: null, dismissed: false, sel: 0, preferredVisualColumn: undefined,
48
+ };
46
49
  /**
47
50
  * Drop payloads the buffer no longer references (I-7).
48
51
  *
@@ -87,12 +90,13 @@ export function editorReducer(state, action) {
87
90
  case 'insert': {
88
91
  // D-8: typing can never land INSIDE a token. A no-op on a buffer with no
89
92
  // tokens, which is every buffer until the first collapse.
90
- const cursor = snapOutOfToken(state.buffer, state.cursor);
93
+ const cursor = snapOutOfToken(state.buffer, snapGrapheme(state.buffer, state.cursor));
94
+ const buffer = state.buffer.slice(0, cursor) + action.text + state.buffer.slice(cursor);
91
95
  return withPrune({
92
96
  ...state,
93
97
  ...DRAFT_FLAGS,
94
- buffer: state.buffer.slice(0, cursor) + action.text + state.buffer.slice(cursor),
95
- cursor: cursor + action.text.length,
98
+ buffer,
99
+ cursor: snapGrapheme(buffer, cursor + action.text.length, true),
96
100
  });
97
101
  }
98
102
  case 'replace':
@@ -100,24 +104,29 @@ export function editorReducer(state, action) {
100
104
  ...state,
101
105
  ...DRAFT_FLAGS,
102
106
  buffer: action.buffer,
103
- cursor: action.cursor,
107
+ cursor: snapGrapheme(action.buffer, action.cursor),
104
108
  });
105
- case 'backspace': {
106
- if (state.cursor <= 0)
109
+ case 'backspace':
110
+ case 'delete': {
111
+ const cursor = snapGrapheme(state.buffer, state.cursor);
112
+ const backwards = action.type === 'backspace';
113
+ if (backwards ? cursor <= 0 : cursor >= state.buffer.length)
107
114
  return state;
108
115
  // G5: one `Backspace` immediately after `...lines]` removes the whole
109
116
  // token and releases its payload, in ONE dispatch.
110
- const range = expandRangeOverTokens(state.buffer, state.cursor - 1, state.cursor);
117
+ const other = stepGrapheme(state.buffer, cursor, backwards ? 'left' : 'right');
118
+ const range = expandRangeOverTokens(state.buffer, backwards ? other : cursor, backwards ? cursor : other);
119
+ const buffer = state.buffer.slice(0, range.from) + state.buffer.slice(range.to);
111
120
  return withPrune({
112
121
  ...state,
113
122
  ...DRAFT_FLAGS,
114
- buffer: state.buffer.slice(0, range.from) + state.buffer.slice(range.to),
115
- cursor: range.from,
123
+ buffer,
124
+ cursor: snapGrapheme(buffer, range.from),
116
125
  });
117
126
  }
118
127
  case 'input': {
119
128
  let buffer = state.buffer;
120
- let cursor = snapOutOfToken(buffer, state.cursor);
129
+ let cursor = snapOutOfToken(buffer, snapGrapheme(buffer, state.cursor));
121
130
  let pastes = null;
122
131
  for (const segment of action.segments) {
123
132
  const collapse = segment.kind === 'paste' && shouldCollapse(segment.text);
@@ -129,7 +138,7 @@ export function editorReducer(state, action) {
129
138
  pastes.set(record.id, record);
130
139
  }
131
140
  buffer = buffer.slice(0, cursor) + text + buffer.slice(cursor);
132
- cursor += text.length;
141
+ cursor = snapGrapheme(buffer, cursor + text.length, true);
133
142
  }
134
143
  return withPrune({
135
144
  ...state,
@@ -139,11 +148,15 @@ export function editorReducer(state, action) {
139
148
  ...(pastes ? { pastes } : {}),
140
149
  });
141
150
  }
142
- case 'moveCursor':
151
+ case 'moveCursor': {
143
152
  // NO DRAFT-FLAG RESET. Moving the caret is not an edit: clearing
144
153
  // `historyIndex` here would end a history walk the moment the user pressed
145
154
  // ← to look at what they recalled.
146
- return state.cursor === action.cursor ? state : { ...state, cursor: action.cursor };
155
+ const cursor = snapGrapheme(state.buffer, action.cursor);
156
+ const preferredVisualColumn = action.preferredVisualColumn;
157
+ return state.cursor === cursor && state.preferredVisualColumn === preferredVisualColumn
158
+ ? state : { ...state, cursor, preferredVisualColumn };
159
+ }
147
160
  case 'recall':
148
161
  // `historyIndex` is SET, not reset, which is why recall cannot reuse
149
162
  // `replace` — the two differ in exactly the field that keeps the walk
@@ -155,7 +168,8 @@ export function editorReducer(state, action) {
155
168
  return withPrune({
156
169
  ...state,
157
170
  buffer: action.buffer,
158
- cursor: action.cursor,
171
+ cursor: snapGrapheme(action.buffer, action.cursor),
172
+ preferredVisualColumn: undefined,
159
173
  historyIndex: action.historyIndex,
160
174
  });
161
175
  case 'select':
@@ -0,0 +1,77 @@
1
+ /** Product copy is centralized here; Unicode escapes keep UI source ASCII-only. */
2
+ export declare const interactionCopy: {
3
+ readonly copied: "已复制";
4
+ readonly lines: "行";
5
+ readonly copySent: "复制请求已发送,无法确认剪贴板";
6
+ readonly copyReasons: {
7
+ readonly empty: "没有可复制的内容";
8
+ readonly unavailable: "剪贴板工具不可用";
9
+ readonly write: "写入剪贴板失败";
10
+ readonly timeout: "剪贴板工具超时";
11
+ readonly 'too-large': "内容超过剪贴板限制";
12
+ readonly cancelled: "复制已取消";
13
+ };
14
+ readonly stopServicesShort: "^C 停服务";
15
+ readonly approveTool: "y 批准";
16
+ readonly rejectTool: "n/Enter 拒绝";
17
+ readonly saveSettings: "Enter 保存";
18
+ readonly modeLabel: "切换模式";
19
+ readonly idle: readonly ["空闲", "空闲"];
20
+ readonly starting: readonly ["启动", "启动中"];
21
+ readonly waiting: readonly ["等待", "等待模型"];
22
+ readonly thinking: readonly ["思考", "正在思考"];
23
+ readonly generating: readonly ["生成", "正在生成内容"];
24
+ readonly preparingTool: readonly ["工具", "正在准备工具"];
25
+ readonly tool: readonly ["工具", "正在运行工具"];
26
+ readonly confirming: readonly ["确认", "等待用户确认"];
27
+ readonly stopping: readonly ["停止中", "停止中"];
28
+ readonly compacting: readonly ["压缩", "正在压缩上下文"];
29
+ readonly retrying: readonly ["重试", "等待重试"];
30
+ readonly ended: readonly ["结束", "已结束"];
31
+ readonly interrupted: readonly ["中断", "已中断"];
32
+ readonly failed: readonly ["失败", "运行失败"];
33
+ readonly readFile: readonly ["读文件", "正在读取文件"];
34
+ readonly writeFile: readonly ["改文件", "正在修改文件"];
35
+ readonly command: readonly ["命令", "正在执行命令"];
36
+ readonly queue: "队";
37
+ readonly pending: "待处理";
38
+ readonly context: "上下文";
39
+ readonly contextUnknown: "上下文未知";
40
+ readonly sessionInput: "会话 输入";
41
+ readonly output: "输出";
42
+ readonly estimatedCost: "会话估算 USD";
43
+ readonly speed: "本轮均速";
44
+ readonly elapsed: "耗时";
45
+ readonly services: "服务";
46
+ readonly send: "Enter 发送";
47
+ readonly enqueue: "Enter 入队";
48
+ readonly newline: "^J 换行";
49
+ readonly sendLong: "Enter:发送";
50
+ readonly enqueueLong: "Enter:加入队列";
51
+ readonly newlineLong: "Ctrl+J:换行";
52
+ readonly interrupt: "Esc×2 中断";
53
+ readonly interruptAgain: "再按 Esc 中断";
54
+ readonly forceStop: "再按 Esc 强制停止";
55
+ readonly startingHint: "启动中,请稍候";
56
+ readonly cancelStart: "Esc×2 取消启动";
57
+ readonly cancelStartAgain: "再按 Esc 取消启动";
58
+ readonly copy: "Ctrl+C 复制";
59
+ readonly copying: "正在复制";
60
+ readonly copyFailed: "复制失败";
61
+ readonly copyCleanup: "复制失败,正在清理";
62
+ readonly close: "Esc 关闭";
63
+ readonly closeCompletion: "Esc 关闭补全";
64
+ readonly executeCommand: "Enter 执行命令";
65
+ readonly complete: "Tab 补全";
66
+ readonly select: "Up/Down 选择";
67
+ readonly scroll: "PgUp/PgDn 滚动";
68
+ readonly confirm: "Enter 确认";
69
+ readonly stopServices: "Ctrl+C 停止服务";
70
+ readonly exit: "Ctrl+C×2 退出";
71
+ readonly help: "/help 帮助";
72
+ readonly shiftNewline: "Shift+Enter 换行需终端支持";
73
+ readonly mode: "Shift+Tab 切换模式";
74
+ readonly update: "/update 更新";
75
+ readonly idlePlaceholder: "输入任务或问题…";
76
+ readonly runningPlaceholder: "输入补充说明…";
77
+ };
@@ -0,0 +1,40 @@
1
+ /** Product copy is centralized here; Unicode escapes keep UI source ASCII-only. */
2
+ export const interactionCopy = {
3
+ copied: '\u5df2\u590d\u5236', lines: '\u884c',
4
+ copySent: '\u590d\u5236\u8bf7\u6c42\u5df2\u53d1\u9001\uff0c\u65e0\u6cd5\u786e\u8ba4\u526a\u8d34\u677f',
5
+ copyReasons: {
6
+ empty: '\u6ca1\u6709\u53ef\u590d\u5236\u7684\u5185\u5bb9',
7
+ unavailable: '\u526a\u8d34\u677f\u5de5\u5177\u4e0d\u53ef\u7528',
8
+ write: '\u5199\u5165\u526a\u8d34\u677f\u5931\u8d25',
9
+ timeout: '\u526a\u8d34\u677f\u5de5\u5177\u8d85\u65f6',
10
+ 'too-large': '\u5185\u5bb9\u8d85\u8fc7\u526a\u8d34\u677f\u9650\u5236',
11
+ cancelled: '\u590d\u5236\u5df2\u53d6\u6d88',
12
+ },
13
+ stopServicesShort: '^C \u505c\u670d\u52a1',
14
+ approveTool: 'y \u6279\u51c6', rejectTool: 'n/Enter \u62d2\u7edd',
15
+ saveSettings: 'Enter \u4fdd\u5b58',
16
+ modeLabel: '\u5207\u6362\u6a21\u5f0f',
17
+ idle: ['\u7a7a\u95f2', '\u7a7a\u95f2'], starting: ['\u542f\u52a8', '\u542f\u52a8\u4e2d'], waiting: ['\u7b49\u5f85', '\u7b49\u5f85\u6a21\u578b'],
18
+ thinking: ['\u601d\u8003', '\u6b63\u5728\u601d\u8003'], generating: ['\u751f\u6210', '\u6b63\u5728\u751f\u6210\u5185\u5bb9'],
19
+ preparingTool: ['\u5de5\u5177', '\u6b63\u5728\u51c6\u5907\u5de5\u5177'], tool: ['\u5de5\u5177', '\u6b63\u5728\u8fd0\u884c\u5de5\u5177'],
20
+ confirming: ['\u786e\u8ba4', '\u7b49\u5f85\u7528\u6237\u786e\u8ba4'], stopping: ['\u505c\u6b62\u4e2d', '\u505c\u6b62\u4e2d'],
21
+ compacting: ['\u538b\u7f29', '\u6b63\u5728\u538b\u7f29\u4e0a\u4e0b\u6587'], retrying: ['\u91cd\u8bd5', '\u7b49\u5f85\u91cd\u8bd5'],
22
+ ended: ['\u7ed3\u675f', '\u5df2\u7ed3\u675f'], interrupted: ['\u4e2d\u65ad', '\u5df2\u4e2d\u65ad'], failed: ['\u5931\u8d25', '\u8fd0\u884c\u5931\u8d25'],
23
+ readFile: ['\u8bfb\u6587\u4ef6', '\u6b63\u5728\u8bfb\u53d6\u6587\u4ef6'], writeFile: ['\u6539\u6587\u4ef6', '\u6b63\u5728\u4fee\u6539\u6587\u4ef6'],
24
+ command: ['\u547d\u4ee4', '\u6b63\u5728\u6267\u884c\u547d\u4ee4'],
25
+ queue: '\u961f', pending: '\u5f85\u5904\u7406', context: '\u4e0a\u4e0b\u6587', contextUnknown: '\u4e0a\u4e0b\u6587\u672a\u77e5',
26
+ sessionInput: '\u4f1a\u8bdd \u8f93\u5165', output: '\u8f93\u51fa', estimatedCost: '\u4f1a\u8bdd\u4f30\u7b97 USD',
27
+ speed: '\u672c\u8f6e\u5747\u901f', elapsed: '\u8017\u65f6', services: '\u670d\u52a1',
28
+ send: 'Enter \u53d1\u9001', enqueue: 'Enter \u5165\u961f', newline: '^J \u6362\u884c',
29
+ sendLong: 'Enter\uff1a\u53d1\u9001', enqueueLong: 'Enter\uff1a\u52a0\u5165\u961f\u5217', newlineLong: 'Ctrl+J\uff1a\u6362\u884c',
30
+ interrupt: 'Esc\u00d72 \u4e2d\u65ad', interruptAgain: '\u518d\u6309 Esc \u4e2d\u65ad', forceStop: '\u518d\u6309 Esc \u5f3a\u5236\u505c\u6b62',
31
+ startingHint: '\u542f\u52a8\u4e2d\uff0c\u8bf7\u7a0d\u5019', cancelStart: 'Esc\u00d72 \u53d6\u6d88\u542f\u52a8',
32
+ cancelStartAgain: '\u518d\u6309 Esc \u53d6\u6d88\u542f\u52a8',
33
+ copy: 'Ctrl+C \u590d\u5236', copying: '\u6b63\u5728\u590d\u5236', copyFailed: '\u590d\u5236\u5931\u8d25', copyCleanup: '\u590d\u5236\u5931\u8d25\uff0c\u6b63\u5728\u6e05\u7406',
34
+ close: 'Esc \u5173\u95ed', closeCompletion: 'Esc \u5173\u95ed\u8865\u5168', executeCommand: 'Enter \u6267\u884c\u547d\u4ee4',
35
+ complete: 'Tab \u8865\u5168', select: 'Up/Down \u9009\u62e9', scroll: 'PgUp/PgDn \u6eda\u52a8',
36
+ confirm: 'Enter \u786e\u8ba4', stopServices: 'Ctrl+C \u505c\u6b62\u670d\u52a1', exit: 'Ctrl+C\u00d72 \u9000\u51fa',
37
+ help: '/help \u5e2e\u52a9', shiftNewline: 'Shift+Enter \u6362\u884c\u9700\u7ec8\u7aef\u652f\u6301',
38
+ mode: 'Shift+Tab \u5207\u6362\u6a21\u5f0f', update: '/update \u66f4\u65b0',
39
+ idlePlaceholder: '\u8f93\u5165\u4efb\u52a1\u6216\u95ee\u9898\u2026', runningPlaceholder: '\u8f93\u5165\u8865\u5145\u8bf4\u660e\u2026',
40
+ };
@@ -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,109 @@
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
+ return `${copy.context} ${approximate}${number(context.occupied)}/${number(context.window)}${context.windowKnown ? '' : '?'} ${detail ? 'tokens' : 'tok'}`;
45
+ }
46
+ function secondaryFields(input) {
47
+ const list = [];
48
+ if ((input.mode && input.mode !== 'build') || input.pendingMode)
49
+ list.push(field('mode', input.pendingMode ? `${input.mode ?? 'build'}>${input.pendingMode}` : input.mode, 'normal'));
50
+ if (input.servicesActive?.live)
51
+ list.push(field('services', `${copy.services} ${input.servicesActive.live}`, 'warning'));
52
+ if (Number.isFinite(input.usageTotal.costUsd))
53
+ list.push(field('cost', `${copy.estimatedCost} ${formatCost(input.usageTotal.costUsd).slice(1)}`));
54
+ if (input.phase !== 'idle') {
55
+ if (input.tokPerSec > 0 && Number.isFinite(input.tokPerSec))
56
+ list.push(field('speed', `${copy.speed} ${input.tokPerSec} tok/s`));
57
+ if (Number.isFinite(input.elapsedMs))
58
+ list.push(field('elapsed', `${copy.elapsed} ${formatDuration(input.elapsedMs)}`));
59
+ }
60
+ if (input.thinkingLevel !== 'off')
61
+ list.push(field('thinking', `think:${cleanStatusText(input.thinkingLevel)}`));
62
+ list.push(field('usage', `${copy.sessionInput} ${compactNumber(promptTokensOf(input.usageTotal))} ${copy.output} ${compactNumber(input.usageTotal.outputTokens)} tok`));
63
+ if (Number.isFinite(input.context.pct))
64
+ list.push(field('percentage', `${input.context.source === 'estimate' || input.context.deltaTokens > 0 || !input.context.windowKnown ? '~' : ''}${Math.round(input.context.pct)}%`));
65
+ if (input.ecoRung)
66
+ list.push(field('eco', 'eco'));
67
+ if (input.fastActive)
68
+ list.push(field('fast', `fast${input.fastActive.inFlight ? '*' : ''}`));
69
+ if (input.teamActive)
70
+ list.push(field('team', `agents ${input.teamActive.running}/${input.teamActive.total}`));
71
+ if (input.todoActive)
72
+ list.push(field('todo', `todo ${input.todoActive.done}/${input.todoActive.total}`));
73
+ if (input.scrolledLines)
74
+ list.push(field('scroll', `^${input.scrolledLines}`, 'warning'));
75
+ return list;
76
+ }
77
+ /** Allocate complete fields before rendering; only the redraw carrier lives outside this budget. */
78
+ export function planStatusFields(input) {
79
+ const budget = Math.max(0, Math.floor(input.columns) - 1);
80
+ const [short, long] = phaseLabels(input);
81
+ const prefix = input.spinner ? `${input.spinner} ` : '';
82
+ const phaseTone = input.runOutcome === 'failed' ? 'error' : 'normal';
83
+ const fields = [field('phase', prefix + short, phaseTone)];
84
+ if (input.pendingCount > 0)
85
+ fields.push(field('queue', `${copy.queue} ${input.pendingCount}`, 'warning'));
86
+ fields.push(field('context', contextText(input.context)));
87
+ const contextIndex = fields.length - 1;
88
+ if (width(fields) > budget)
89
+ fields[contextIndex] = field('context', contextText(input.context, false, true));
90
+ if (width(fields) > budget)
91
+ fields[contextIndex] = field('context', '/context');
92
+ if (width(fields) > budget && input.pendingCount > 0)
93
+ fields[1] = field('queue', '/queue', 'warning');
94
+ if (width(fields) > budget)
95
+ return fields.filter((_f, index) => width(fields.slice(0, index + 1)) <= budget);
96
+ for (const candidate of secondaryFields(input)) {
97
+ if (width([...fields, candidate]) <= budget)
98
+ fields.push(candidate);
99
+ }
100
+ const upgrades = [field('phase', prefix + long, phaseTone),
101
+ field('queue', `${copy.pending} ${input.pendingCount}`, 'warning'),
102
+ field('context', contextText(input.context, true))];
103
+ for (const upgrade of upgrades) {
104
+ const index = fields.findIndex((f) => f.id === upgrade.id);
105
+ if (index >= 0 && width(fields) - fields[index].cells + upgrade.cells <= budget)
106
+ fields[index] = upgrade;
107
+ }
108
+ return fields;
109
+ }
@@ -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 {};