@yu_robotics/remote-cli 1.6.151 → 1.6.152

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 (69) hide show
  1. package/README.md +5 -3
  2. package/dist/client/MessageHandler.d.ts +14 -7
  3. package/dist/client/MessageHandler.d.ts.map +1 -1
  4. package/dist/client/MessageHandler.js +225 -96
  5. package/dist/client/MessageHandler.js.map +1 -1
  6. package/dist/client/WebSocketClient.d.ts.map +1 -1
  7. package/dist/client/WebSocketClient.js +1 -0
  8. package/dist/client/WebSocketClient.js.map +1 -1
  9. package/dist/executor/AcpExecutor.d.ts +6 -1
  10. package/dist/executor/AcpExecutor.d.ts.map +1 -1
  11. package/dist/executor/AcpExecutor.js +96 -7
  12. package/dist/executor/AcpExecutor.js.map +1 -1
  13. package/dist/executor/AgyExecutor.d.ts +9 -1
  14. package/dist/executor/AgyExecutor.d.ts.map +1 -1
  15. package/dist/executor/AgyExecutor.js +66 -0
  16. package/dist/executor/AgyExecutor.js.map +1 -1
  17. package/dist/executor/ClaudePersistentExecutor.d.ts +33 -6
  18. package/dist/executor/ClaudePersistentExecutor.d.ts.map +1 -1
  19. package/dist/executor/ClaudePersistentExecutor.js +308 -38
  20. package/dist/executor/ClaudePersistentExecutor.js.map +1 -1
  21. package/dist/executor/CodexAppServerExecutor.d.ts +2 -1
  22. package/dist/executor/CodexAppServerExecutor.d.ts.map +1 -1
  23. package/dist/executor/CodexAppServerExecutor.js +39 -4
  24. package/dist/executor/CodexAppServerExecutor.js.map +1 -1
  25. package/dist/executor/DshExecutor.js +2 -2
  26. package/dist/executor/DshExecutor.js.map +1 -1
  27. package/dist/executor/IExecutor.d.ts +20 -0
  28. package/dist/executor/IExecutor.d.ts.map +1 -1
  29. package/dist/executor/PiExecutor.d.ts +2 -1
  30. package/dist/executor/PiExecutor.d.ts.map +1 -1
  31. package/dist/executor/PiExecutor.js +70 -53
  32. package/dist/executor/PiExecutor.js.map +1 -1
  33. package/dist/executor/acp/AcpClient.d.ts.map +1 -1
  34. package/dist/executor/acp/AcpClient.js +23 -3
  35. package/dist/executor/acp/AcpClient.js.map +1 -1
  36. package/dist/executor/acp/AcpTypes.d.ts +8 -1
  37. package/dist/executor/acp/AcpTypes.d.ts.map +1 -1
  38. package/dist/executor/claude/ClaudeSettings.d.ts +28 -0
  39. package/dist/executor/claude/ClaudeSettings.d.ts.map +1 -0
  40. package/dist/executor/claude/ClaudeSettings.js +121 -0
  41. package/dist/executor/claude/ClaudeSettings.js.map +1 -0
  42. package/dist/executor/index.d.ts +1 -1
  43. package/dist/executor/index.d.ts.map +1 -1
  44. package/dist/executor/index.js +3 -3
  45. package/dist/executor/index.js.map +1 -1
  46. package/dist/maintenance/release-notes.json +1 -1
  47. package/dist/settings/SettingsAdmission.d.ts +12 -0
  48. package/dist/settings/SettingsAdmission.d.ts.map +1 -0
  49. package/dist/settings/SettingsAdmission.js +55 -0
  50. package/dist/settings/SettingsAdmission.js.map +1 -0
  51. package/dist/settings/SettingsCatalog.d.ts +23 -0
  52. package/dist/settings/SettingsCatalog.d.ts.map +1 -0
  53. package/dist/settings/SettingsCatalog.js +137 -0
  54. package/dist/settings/SettingsCatalog.js.map +1 -0
  55. package/dist/settings/SettingsService.d.ts +48 -0
  56. package/dist/settings/SettingsService.d.ts.map +1 -0
  57. package/dist/settings/SettingsService.js +218 -0
  58. package/dist/settings/SettingsService.js.map +1 -0
  59. package/dist/thread/ThreadExecutorPool.js +1 -1
  60. package/dist/thread/ThreadExecutorPool.js.map +1 -1
  61. package/dist/thread/types.d.ts +1 -1
  62. package/dist/thread/types.d.ts.map +1 -1
  63. package/dist/types/Settings.d.ts +64 -0
  64. package/dist/types/Settings.d.ts.map +1 -0
  65. package/dist/types/Settings.js +3 -0
  66. package/dist/types/Settings.js.map +1 -0
  67. package/dist/types/index.d.ts +2 -0
  68. package/dist/types/index.d.ts.map +1 -1
  69. package/package.json +1 -1
package/README.md CHANGED
@@ -171,10 +171,10 @@ Linux users upgrading from version 1.6.23 or earlier should run `remote-cli serv
171
171
  | `/new` | Alias for `/clear`; start a fresh conversation in this thread |
172
172
  | `/compact` | Compress history to save tokens |
173
173
  | `/cd <dir>` | Change directory; a different directory starts fresh conversations for this thread |
174
- | `/model [name]` | List models for the active backend or set this thread's model |
175
- | `/effort [auto|level]` | Show or set Codex/AGY/OpenCode/Kimi/ZCode/Pi reasoning effort |
174
+ | `/model [name]` | Open native model settings, or set the active backend model with a text argument |
175
+ | `/effort [auto|level]` | Open native effort settings, or set/clear the active backend effort with a text argument |
176
176
  | `/sandbox [on/off/read-only/default]` | Configure the current Codex or Claude Code thread sandbox, directory grants, and networking |
177
- | `/backend` | List backends and show the current thread's effective backend |
177
+ | `/backend` | Open backend settings; Current thread is the default and Confirm applies the selection |
178
178
  | `/delegation [on|off]` | Inspect or enable delegation between installed agent backends in this thread |
179
179
  | `/backend <index>` | Switch all threads and clear per-thread backend overrides |
180
180
  | `/backend <index> @` | Switch only the current thread |
@@ -183,6 +183,8 @@ Linux users upgrading from version 1.6.23 or earlier should run `remote-cli serv
183
183
  | `/unbind` | Unbind all devices |
184
184
  | `/device` | List and switch between bound devices |
185
185
 
186
+ CLI and Router 1.6.152 add standalone settings cards. `/backend` lists all eight stable backend choices, disables missing installations and applies only after Confirm; Current thread is the default and All threads on this device is opt-in. `/model` and `/effort` submit native options immediately, and show applied state only after a saved-setting acknowledgement. Their target-backend selector configures future worker launches without switching the coordinator. Busy work, expired cards and changed settings require refresh; older peers and parameterized text commands retain their existing behavior. See [Model and Reasoning Effort](../../README.md#model-and-reasoning-effort).
187
+
186
188
  ### Threads & Machines
187
189
 
188
190
  From CLI 1.6.121, new and existing threads without a saved preference enable
@@ -42,6 +42,10 @@ export declare class MessageHandler {
42
42
  private delegationProgressTextSupported;
43
43
  private activityProgressSupported;
44
44
  private workerContextResetSupported;
45
+ private settingsCardsSupported;
46
+ private readonly settingsAdmission;
47
+ private readonly settingsCatalogReader;
48
+ private readonly settingsService;
45
49
  private readonly pendingApprovalCards;
46
50
  private readonly delegation;
47
51
  private readonly delegationBridges;
@@ -58,6 +62,7 @@ export declare class MessageHandler {
58
62
  * Handle command message — route to the correct thread executor.
59
63
  */
60
64
  private handleCommandMessage;
65
+ private handleAdmittedCommandMessage;
61
66
  /**
62
67
  * Handle /abort command for a specific thread executor
63
68
  */
@@ -85,8 +90,8 @@ export declare class MessageHandler {
85
90
  private hasThreadQueueState;
86
91
  /**
87
92
  * Bare /model: show the thread's current model on the active backend plus
88
- * the available models. Listing source per backend (all verified live):
89
- * - claude: `claude --print /model` prints current + available aliases
93
+ * the available models, using each adapter's structured catalog:
94
+ * - claude: native control initialization exposes model aliases and capabilities
90
95
  * - agy: `agy models` prints "slug<TAB>Display Name" lines
91
96
  * - codex: app-server `model/list` returns the authenticated catalog
92
97
  * - opencode/kimi ACP: session config options expose enabled models
@@ -94,11 +99,6 @@ export declare class MessageHandler {
94
99
  * - pi: RPC `get_available_models` returns the configured Pi catalog
95
100
  */
96
101
  private handleModelList;
97
- /**
98
- * Run a short-lived CLI listing command with a 10s timeout.
99
- * Returns null on failure/timeout — callers degrade gracefully.
100
- */
101
- private runListingCommand;
102
102
  /**
103
103
  * Handle /thread subcommands
104
104
  */
@@ -182,6 +182,13 @@ export declare class MessageHandler {
182
182
  private sendImage;
183
183
  private sendStructuredContent;
184
184
  private sendResponse;
185
+ private executorConfiguration;
186
+ private settingsState;
187
+ private settingsBusy;
188
+ private settingsFallback;
189
+ /** Shared native/persistent mutation for cards and parameterized text commands. */
190
+ private applyPreference;
191
+ private applyBackendSetting;
185
192
  private checkCommand;
186
193
  private detectBackends;
187
194
  private handleBackendCommand;
@@ -1 +1 @@
1
- {"version":3,"file":"MessageHandler.d.ts","sourceRoot":"","sources":["../../src/client/MessageHandler.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"MessageHandler.d.ts","sourceRoot":"","sources":["../../src/client/MessageHandler.ts"],"names":[],"mappings":"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"}
@@ -32,8 +32,10 @@ const BackendCommand_1 = require("../utils/BackendCommand");
32
32
  const ClaudeStderrFilter_1 = require("../executor/claude/ClaudeStderrFilter");
33
33
  const ExecutionMetadata_1 = require("../executor/ExecutionMetadata");
34
34
  const ActivityProgress_1 = require("../utils/ActivityProgress");
35
- const EFFORT_BACKENDS = new Set(['codex', 'agy', 'opencode', 'kimi', 'zcode', 'pi', 'dsh']);
36
- const NATIVE_MODEL_LIST_BACKENDS = new Set(['codex', 'opencode', 'kimi', 'zcode', 'pi', 'dsh']);
35
+ const SettingsAdmission_1 = require("../settings/SettingsAdmission");
36
+ const SettingsCatalog_1 = require("../settings/SettingsCatalog");
37
+ const SettingsService_1 = require("../settings/SettingsService");
38
+ const EFFORT_BACKENDS = new Set(['claude', 'codex', 'agy', 'opencode', 'kimi', 'zcode', 'pi', 'dsh']);
37
39
  const SLASH_SESSION_BACKENDS = new Set(['opencode', 'kimi', 'zcode', 'pi']);
38
40
  const NATIVE_SKILLS_BACKENDS = new Set(['claude', 'agy', 'opencode', 'kimi', 'zcode', 'pi']);
39
41
  function backendDisplayName(key) {
@@ -114,6 +116,10 @@ class MessageHandler {
114
116
  delegationProgressTextSupported = false;
115
117
  activityProgressSupported = false;
116
118
  workerContextResetSupported = false;
119
+ settingsCardsSupported = false;
120
+ settingsAdmission = new SettingsAdmission_1.SettingsAdmission();
121
+ settingsCatalogReader;
122
+ settingsService;
117
123
  pendingApprovalCards = new Map();
118
124
  delegation;
119
125
  delegationBridges = new Map();
@@ -126,6 +132,33 @@ class MessageHandler {
126
132
  this.config = config;
127
133
  this.wsClient.onClose?.(() => this.fileInbox?.disconnect());
128
134
  this.delegation = new DelegationManager_1.DelegationManager(directoryGuard);
135
+ this.settingsCatalogReader = new SettingsCatalog_1.SettingsCatalogReader(directoryGuard);
136
+ this.settingsService = new SettingsService_1.SettingsService({
137
+ state: id => this.settingsState(id),
138
+ backends: async () => (await this.detectBackends()).map(entry => ({ ...entry, value: (0, config_1.backendKeyOf)(entry.id) })),
139
+ catalog: async (id, target, kind) => {
140
+ const thread = this.threadManager.getThread(id);
141
+ if (!thread)
142
+ throw new Error('This thread no longer exists.');
143
+ const config = this.executorConfiguration();
144
+ return this.settingsCatalogReader.read(config, thread, this.threadPool.getBackendKey(id), target, kind);
145
+ },
146
+ busy: (id, except, queue = true) => this.settingsBusy(id, except, queue),
147
+ threads: () => this.threadManager.listThreads().map(thread => thread.id),
148
+ exclusive: async (target, body) => {
149
+ try {
150
+ return await this.settingsAdmission.mutation(target, body);
151
+ }
152
+ finally {
153
+ for (const id of target === '*' ? this.threadManager.listThreads().map(thread => thread.id) : [target]) {
154
+ void this.startNextQueuedCommand(id);
155
+ }
156
+ }
157
+ },
158
+ backend: (id, target, scope, follow) => this.applyBackendSetting(id, target, scope, follow),
159
+ preference: (id, target, kind, value) => this.applyPreference(id, target, kind, value),
160
+ });
161
+ this.wsClient.onClose?.(() => this.settingsService.invalidate());
129
162
  this.notificationAdapter = new hooks_1.FeishuNotificationAdapter(wsClient);
130
163
  this.notificationAdapter.setThreadNameResolver((threadId) => this.threadManager.getThread(threadId)?.name);
131
164
  this.notificationAdapter.register();
@@ -162,6 +195,12 @@ class MessageHandler {
162
195
  case 'command':
163
196
  await this.handleCommandMessage(message, this.fileInbox?.mark());
164
197
  return;
198
+ case 'settings_action':
199
+ if (this.settingsCardsSupported) {
200
+ const result = await this.settingsService.action(message);
201
+ this.wsClient.send(result);
202
+ }
203
+ return;
165
204
  case 'file_pending':
166
205
  if (this.fileInbox)
167
206
  await this.fileInbox.accept(message);
@@ -202,6 +241,7 @@ class MessageHandler {
202
241
  this.activityProgressSupported = data.capabilities?.activityProgress === true;
203
242
  this.delegationProgressSupported = data.capabilities?.delegationProgress === true;
204
243
  this.workerContextResetSupported = this.delegationProgressSupported && data.capabilities?.workerContextReset === true;
244
+ this.settingsCardsSupported = data.capabilities?.settingsCards === true;
205
245
  this.delegationProgressTextSupported = this.delegationProgressSupported
206
246
  && data.capabilities?.delegationProgressText === true;
207
247
  for (const pending of this.pendingApprovalCards.values()) {
@@ -252,6 +292,34 @@ class MessageHandler {
252
292
  * Handle command message — route to the correct thread executor.
253
293
  */
254
294
  async handleCommandMessage(message, fileOrder) {
295
+ const threadId = message.threadId ?? this.threadManager.getDefaultThread().id;
296
+ const content = message.content?.trim() ?? '';
297
+ const body = () => this.handleAdmittedCommandMessage(message, fileOrder);
298
+ this.messageOpenIds.set(message.messageId, message.openId);
299
+ try {
300
+ // Abort and addressed human replies must remain available during native work.
301
+ if (content === '/abort' || /^(?:yes|no)(?:\s|$)/.test(content) || content.startsWith('/input '))
302
+ return await body();
303
+ if (/^\/(?:backend|model|effort)\s+\S/.test(content)) {
304
+ const global = /^\/backend\s/.test(content) && !/\s@$/.test(content);
305
+ await this.settingsAdmission.mutation(global ? '*' : threadId, async () => {
306
+ this.settingsService.invalidate(global ? undefined : threadId);
307
+ await body();
308
+ });
309
+ }
310
+ else {
311
+ await this.settingsAdmission.command(threadId, message.messageId, async () => {
312
+ if (/^\/(?:clear|new|cd|sandbox)(?:\s|$)/.test(content) || /^\/thread\s+delete(?:\s|$)/.test(content))
313
+ this.settingsService.invalidate(threadId);
314
+ await body();
315
+ });
316
+ }
317
+ }
318
+ catch (error) {
319
+ this.sendResponse(message.messageId, threadId, { success: false, error: error instanceof Error ? error.message : 'Command admission failed.' });
320
+ }
321
+ }
322
+ async handleAdmittedCommandMessage(message, fileOrder) {
255
323
  const { messageId, content, attachments, workingDirectory, openId, isSlashCommand, threadId } = message;
256
324
  this.currentOpenId = openId;
257
325
  this.messageOpenIds.set(message.messageId, openId);
@@ -275,6 +343,17 @@ class MessageHandler {
275
343
  });
276
344
  return;
277
345
  }
346
+ if (this.settingsCardsSupported && openId && /^\/(?:backend|model|effort)$/.test(content?.trim() ?? '')) {
347
+ const kind = content.trim().slice(1);
348
+ try {
349
+ const menu = await this.settingsService.open(kind, resolvedThreadId, openId, undefined, messageId);
350
+ this.sendResponse(messageId, resolvedThreadId, { success: true, output: this.settingsFallback(menu), settingsMenu: menu });
351
+ }
352
+ catch {
353
+ this.sendResponse(messageId, resolvedThreadId, { success: false, error: 'The settings menu could not be opened. Retry or use the parameterized text command.' });
354
+ }
355
+ return;
356
+ }
278
357
  const executor = this.threadPool.getExecutor(resolvedThreadId);
279
358
  this.sendStreamContext(messageId, resolvedThreadId, executor);
280
359
  // Handle /abort for this specific thread (bypasses busy check)
@@ -810,11 +889,11 @@ Use /compact to reduce conversation context or /clear to start a fresh context.`
810
889
  - /clear, /new - Start a fresh conversation in this thread
811
890
  - /compact - Compress conversation history to reduce context size
812
891
  - /cd <directory> - Change working directory for this thread
813
- - /model [name] - Show models for the active backend, or set this thread's model
814
- - /effort [auto|level] - Show or set effort for Codex/AGY/OpenCode/Kimi/ZCode/Pi/DSH (Claude Code is unsupported)
892
+ - /model [name] - Open model settings, or set the active backend model with a text argument
893
+ - /effort [auto|level] - Open effort settings, or set a native supported level with a text argument
815
894
  - /sandbox [on|off|read-only|default|allow <directory>|remove <directory>|network on|off] - Configure this thread's sandbox (Codex, Claude Code)
816
895
  - /delegation [on|off|reset [backend]] - Delegate tasks between installed agent backends; reset saved worker context when needed
817
- - /backend - List backends and show the current thread's effective backend
896
+ - /backend - Open backend settings (Current thread by default; Confirm applies)
818
897
  - /backend <index> - Switch all threads and clear per-thread backend overrides
819
898
  - /backend <index> @ - Switch only the current thread
820
899
  - /backend default @ - Clear the current thread override and follow the global backend
@@ -943,9 +1022,9 @@ You can also use natural language commands to control Claude Code CLI.`,
943
1022
  if ('listModels' in executor && typeof executor.listModels === 'function') {
944
1023
  try {
945
1024
  const models = await executor.listModels();
946
- const activeModel = models.find((entry) => entry.id === configuredModel)
947
- ?? models.find((entry) => entry.isDefault)
948
- ?? models[0];
1025
+ const activeModel = models.find((entry) => entry.isCurrent)
1026
+ ?? models.find((entry) => entry.id === configuredModel)
1027
+ ?? models.find((entry) => entry.isDefault);
949
1028
  if (activeModel) {
950
1029
  lines.push(`Model: ${activeModel.id}`);
951
1030
  lines.push(`Model default: ${activeModel.defaultReasoningEffort ?? 'unavailable'}`);
@@ -969,18 +1048,7 @@ You can also use natural language commands to control Claude Code CLI.`,
969
1048
  return true;
970
1049
  }
971
1050
  const effortArg = parts.slice(1).join(' ').toLowerCase();
972
- const result = await executor.setEffort(effortArg);
973
- if (result.success) {
974
- const current = this.threadManager.getThread(threadId);
975
- const efforts = { ...current?.efforts };
976
- if (effortArg === 'auto')
977
- delete efforts[key];
978
- else
979
- efforts[key] = effortArg;
980
- await this.threadManager.updateThread(threadId, {
981
- efforts: Object.keys(efforts).length > 0 ? efforts : undefined,
982
- });
983
- }
1051
+ const result = await this.applyPreference(threadId, key, 'effort', effortArg === 'auto' ? undefined : effortArg);
984
1052
  this.sendResponse(messageId, threadId, result.success
985
1053
  ? { success: true, output: result.output || `Reasoning effort set to ${effortArg}.` }
986
1054
  : { success: false, error: result.error || 'Failed to set reasoning effort' });
@@ -1000,20 +1068,7 @@ You can also use natural language commands to control Claude Code CLI.`,
1000
1068
  });
1001
1069
  return true;
1002
1070
  }
1003
- const result = await executor.setModel(modelArg, (chunk) => {
1004
- this.sendStreamChunk(messageId, threadId, chunk);
1005
- });
1006
- if (result.success) {
1007
- // Persist per-backend: model names are backend-specific (Claude's
1008
- // "opus" is rejected by agy), so selections live under
1009
- // thread.models[backendKey]. The legacy `model` field is kept in
1010
- // sync for the Claude backend only.
1011
- const executorConfig = this.config.get('executor') ?? { type: 'auto' };
1012
- const key = this.threadPool.getBackendKey(threadId);
1013
- const current = this.threadManager.getThread(threadId);
1014
- const models = { ...current?.models, [key]: modelArg };
1015
- await this.threadManager.updateThread(threadId, key === 'claude' ? { models, model: modelArg } : { models });
1016
- }
1071
+ const result = await this.applyPreference(threadId, this.threadPool.getBackendKey(threadId), 'model', modelArg, chunk => this.sendStreamChunk(messageId, threadId, chunk));
1017
1072
  this.sendResponse(messageId, threadId, result.success
1018
1073
  ? { success: true, output: result.output || `āœ… Model set to ${modelArg}` }
1019
1074
  : { success: false, error: result.error || 'Failed to set model' });
@@ -1162,6 +1217,8 @@ You can also use natural language commands to control Claude Code CLI.`,
1162
1217
  this.sendResponse(messageId, threadId, { success: true, output: lines.join('\n') });
1163
1218
  }
1164
1219
  async startNextQueuedCommand(threadId) {
1220
+ if (this.settingsAdmission.isBlocked(threadId))
1221
+ return;
1165
1222
  if (this.threadPool.isThreadBusy(threadId) || this.pausedQueues.has(threadId) || this.abortOperations.has(threadId))
1166
1223
  return;
1167
1224
  const queue = this.threadQueues.get(threadId);
@@ -1248,8 +1305,8 @@ You can also use natural language commands to control Claude Code CLI.`,
1248
1305
  }
1249
1306
  /**
1250
1307
  * Bare /model: show the thread's current model on the active backend plus
1251
- * the available models. Listing source per backend (all verified live):
1252
- * - claude: `claude --print /model` prints current + available aliases
1308
+ * the available models, using each adapter's structured catalog:
1309
+ * - claude: native control initialization exposes model aliases and capabilities
1253
1310
  * - agy: `agy models` prints "slug<TAB>Display Name" lines
1254
1311
  * - codex: app-server `model/list` returns the authenticated catalog
1255
1312
  * - opencode/kimi ACP: session config options expose enabled models
@@ -1257,7 +1314,6 @@ You can also use natural language commands to control Claude Code CLI.`,
1257
1314
  * - pi: RPC `get_available_models` returns the configured Pi catalog
1258
1315
  */
1259
1316
  async handleModelList(messageId, threadId) {
1260
- const executorConfig = this.config.get('executor') ?? { type: 'auto' };
1261
1317
  const key = this.threadPool.getBackendKey(threadId);
1262
1318
  const thread = this.threadManager.getThread(threadId);
1263
1319
  const current = thread?.models?.[key] ?? (key === 'claude' ? thread?.model : undefined);
@@ -1266,7 +1322,7 @@ You can also use natural language commands to control Claude Code CLI.`,
1266
1322
  `šŸŽÆ Backend: ${backendLabel}`,
1267
1323
  `Current model: ${current ?? 'backend default'}`,
1268
1324
  ];
1269
- if (NATIVE_MODEL_LIST_BACKENDS.has(key)) {
1325
+ {
1270
1326
  const executor = this.threadPool.getExecutor(threadId);
1271
1327
  if ('listModels' in executor && typeof executor.listModels === 'function') {
1272
1328
  try {
@@ -1290,57 +1346,9 @@ You can also use natural language commands to control Claude Code CLI.`,
1290
1346
  lines.push('', `Model listing is unavailable in this ${backendLabel} transport.`);
1291
1347
  }
1292
1348
  }
1293
- else {
1294
- const bin = (0, BackendCommand_1.getBackendCommand)(key === 'agy' ? 'agy' : 'claude', executorConfig);
1295
- const args = key === 'agy' ? ['models'] : ['/model', '--print'];
1296
- const listing = await this.runListingCommand(bin, args);
1297
- if (listing) {
1298
- lines.push('', 'Available models:', listing.trim());
1299
- }
1300
- else {
1301
- lines.push('', 'āš ļø Could not fetch the model list from the backend CLI.');
1302
- }
1303
- }
1304
1349
  lines.push('', 'Set with: /model <name>');
1305
1350
  this.sendResponse(messageId, threadId, { success: true, output: lines.join('\n') });
1306
1351
  }
1307
- /**
1308
- * Run a short-lived CLI listing command with a 10s timeout.
1309
- * Returns null on failure/timeout — callers degrade gracefully.
1310
- */
1311
- runListingCommand(bin, args) {
1312
- return new Promise((resolve) => {
1313
- let settled = false;
1314
- let timer;
1315
- const finish = (value) => {
1316
- if (settled)
1317
- return;
1318
- settled = true;
1319
- if (timer)
1320
- clearTimeout(timer);
1321
- resolve(value);
1322
- };
1323
- let child;
1324
- try {
1325
- child = (0, child_process_1.spawn)(bin, args, {
1326
- stdio: ['ignore', 'pipe', 'pipe'],
1327
- env: { ...process.env, CLAUDECODE: '' },
1328
- });
1329
- }
1330
- catch {
1331
- finish(null);
1332
- return;
1333
- }
1334
- timer = setTimeout(() => {
1335
- child.kill();
1336
- finish(null);
1337
- }, 10000);
1338
- const chunks = [];
1339
- child.stdout?.on('data', (d) => chunks.push(d.toString()));
1340
- child.on('exit', (code) => finish(code === 0 ? chunks.join('') : null));
1341
- child.on('error', () => finish(null));
1342
- });
1343
- }
1344
1352
  /**
1345
1353
  * Handle /thread subcommands
1346
1354
  */
@@ -2515,6 +2523,7 @@ You can also use natural language commands to control Claude Code CLI.`,
2515
2523
  threads: result.threads,
2516
2524
  queueConfirmation: result.queueConfirmation,
2517
2525
  ...(result.executionMetadata ? { executionMetadata: result.executionMetadata } : {}),
2526
+ ...(this.settingsCardsSupported && result.settingsMenu ? { settingsMenu: result.settingsMenu } : {}),
2518
2527
  cwd,
2519
2528
  timestamp: Date.now(),
2520
2529
  });
@@ -2525,6 +2534,127 @@ You can also use natural language commands to control Claude Code CLI.`,
2525
2534
  }
2526
2535
  }
2527
2536
  // ── Backend switching ─────────────────────────────────────────────────────
2537
+ executorConfiguration() {
2538
+ return this.config.get('executor') ?? { type: 'auto' };
2539
+ }
2540
+ settingsState(threadId) {
2541
+ const thread = this.threadManager.getThread(threadId);
2542
+ if (!thread)
2543
+ return undefined;
2544
+ const projection = (entry) => ({ id: entry.id, backend: entry.backend,
2545
+ cwd: entry.workingDirectory, generation: entry.delegationWorkspaceGeneration, model: entry.model,
2546
+ models: entry.models, efforts: entry.efforts });
2547
+ const all = this.threadManager.listThreads().map(projection).sort((a, b) => a.id.localeCompare(b.id));
2548
+ const configuredModels = { ...thread.models };
2549
+ if (configuredModels.claude === undefined && thread.model)
2550
+ configuredModels.claude = thread.model;
2551
+ return { threadId, threadName: thread.name, coordinatorBackend: this.threadPool.getBackendKey(threadId),
2552
+ followsGlobal: !thread.backend, configuredModels, configuredEfforts: { ...thread.efforts },
2553
+ revision: (0, crypto_1.createHash)('sha256').update(JSON.stringify({ thread: projection(thread), all, config: this.executorConfiguration() })).digest('hex') };
2554
+ }
2555
+ settingsBusy(threadId, exceptRequest, includeQueue = true) {
2556
+ return this.isDestroyed || this.automaticUpdateInProgress || this.threadPool.isThreadBusy(threadId)
2557
+ || this.delegation.hasActiveTasks(threadId) || this.abortOperations.has(threadId)
2558
+ || this.settingsAdmission.hasCommand(threadId, exceptRequest) || (includeQueue && this.hasThreadQueueState(threadId));
2559
+ }
2560
+ settingsFallback(menu) {
2561
+ const lines = [`${menu.kind === 'backend' ? 'Backend' : menu.kind === 'model' ? 'Model' : 'Reasoning effort'} settings`,
2562
+ `Thread: ${menu.threadName}`, `Target backend: ${menu.targetBackend}`,
2563
+ `Configured: ${menu.configuredValue ?? 'default'}`, `Effective: ${menu.effectiveValue ?? 'not reported'} (${menu.effectiveSource})`];
2564
+ if (menu.unavailableReason)
2565
+ lines.push(menu.unavailableReason);
2566
+ for (const choice of menu.choices)
2567
+ lines.push(`${choice.label}${choice.disabled ? ' (unavailable)' : ''}`);
2568
+ if (menu.kind === 'backend')
2569
+ lines.push('Card scope defaults to Current thread. Confirm to apply. Text commands: /backend <index> @ (current), /backend <index> (all).');
2570
+ else
2571
+ lines.push(`Use the card buttons, or /${menu.kind} <value> for the active backend.`);
2572
+ return lines.join('\n');
2573
+ }
2574
+ /** Shared native/persistent mutation for cards and parameterized text commands. */
2575
+ async applyPreference(threadId, backend, kind, value, onStream) {
2576
+ const thread = this.threadManager.getThread(threadId);
2577
+ if (!thread)
2578
+ return { success: false, error: 'This thread no longer exists.' };
2579
+ if (value !== undefined && (!value || value.length > 512 || /[\x00-\x1f\x7f]/.test(value)))
2580
+ return { success: false, error: 'Invalid settings value.' };
2581
+ const config = this.executorConfiguration();
2582
+ const coordinator = this.threadPool.getBackendKey(threadId);
2583
+ const apply = async (executor) => {
2584
+ if (kind === 'effort') {
2585
+ if (!executor.setEffort)
2586
+ return { success: false, error: 'This backend does not expose effort controls.' };
2587
+ return executor.setEffort(value ?? 'auto');
2588
+ }
2589
+ if (value === undefined) {
2590
+ const fallback = (0, SettingsCatalog_1.configuredBackendModel)(config, backend);
2591
+ if (fallback && executor.setModel)
2592
+ return executor.setModel(fallback, onStream);
2593
+ if (!executor.clearModel)
2594
+ return { success: false, error: 'This backend cannot safely clear its model override.' };
2595
+ await executor.clearModel();
2596
+ return { success: true, output: 'Model override cleared.' };
2597
+ }
2598
+ if (!executor.setModel)
2599
+ return { success: false, error: 'This backend does not expose model controls.' };
2600
+ return executor.setModel(value, onStream);
2601
+ };
2602
+ try {
2603
+ const result = backend === coordinator ? await apply(this.threadPool.getExecutor(threadId))
2604
+ : await this.settingsCatalogReader.withExecutor(config, thread, coordinator, backend, apply, value === undefined && kind === 'model');
2605
+ if (!result.success)
2606
+ return result;
2607
+ // Persistence uses the latest thread object so unrelated backend choices survive.
2608
+ const current = this.threadManager.getThread(threadId);
2609
+ if (!current)
2610
+ throw new Error('The thread was deleted during the settings operation.');
2611
+ if (kind === 'model') {
2612
+ const models = { ...current.models };
2613
+ if (value === undefined)
2614
+ delete models[backend];
2615
+ else
2616
+ models[backend] = value;
2617
+ await this.threadManager.updateThread(threadId, { models: Object.keys(models).length ? models : undefined,
2618
+ ...(backend === 'claude' ? { model: value } : {}) });
2619
+ }
2620
+ else {
2621
+ const efforts = { ...current.efforts };
2622
+ if (value === undefined)
2623
+ delete efforts[backend];
2624
+ else
2625
+ efforts[backend] = value;
2626
+ await this.threadManager.updateThread(threadId, { efforts: Object.keys(efforts).length ? efforts : undefined });
2627
+ }
2628
+ return result;
2629
+ }
2630
+ finally {
2631
+ this.settingsService.invalidate(threadId);
2632
+ }
2633
+ }
2634
+ async applyBackendSetting(threadId, backend, scope, followGlobal = false) {
2635
+ try {
2636
+ if (followGlobal) {
2637
+ await this.threadPool.destroyThread(threadId, { deleteData: false });
2638
+ await this.threadManager.updateThread(threadId, { backend: undefined });
2639
+ return { success: true, output: 'This thread now follows the global backend.', clearedCount: this.clearThreadQueue(threadId) };
2640
+ }
2641
+ if (scope === 'thread') {
2642
+ await this.threadPool.switchThreadBackend(threadId, backend);
2643
+ return { success: true, output: `Thread backend set to ${backend}.`, clearedCount: this.clearThreadQueue(threadId) };
2644
+ }
2645
+ if (this.threadPool.getSummaries().some(thread => thread.status === 'running') || this.delegation.hasActiveTasks(threadId)) {
2646
+ return { success: false, error: 'Cannot switch all backends while a thread is running. Send /abort first.', clearedCount: 0 };
2647
+ }
2648
+ const config = { ...this.executorConfiguration(), type: backend === 'claude' ? 'auto' : backend };
2649
+ await this.config.set('executor', config);
2650
+ await this.threadPool.switchBackend(config);
2651
+ await this.threadManager.clearBackendOverrides();
2652
+ return { success: true, output: `All threads will use ${backend} for future commands.`, clearedCount: this.clearAllQueues() };
2653
+ }
2654
+ finally {
2655
+ this.settingsService.invalidate(scope === 'all' ? undefined : threadId);
2656
+ }
2657
+ }
2528
2658
  checkCommand(cmd, args) {
2529
2659
  return new Promise((resolve) => {
2530
2660
  try {
@@ -2596,9 +2726,7 @@ You can also use natural language commands to control Claude Code CLI.`,
2596
2726
  });
2597
2727
  return;
2598
2728
  }
2599
- await this.threadPool.destroyThread(threadId, { deleteData: false });
2600
- await this.threadManager.updateThread(threadId, { backend: undefined });
2601
- const clearedCount = this.clearThreadQueue(threadId);
2729
+ const { clearedCount } = await this.applyBackendSetting(threadId, (0, config_1.backendKeyOf)(currentType), 'thread', true);
2602
2730
  this.sendResponse(messageId, threadId, {
2603
2731
  success: true,
2604
2732
  output: `āœ… Thread backend reset to the global backend (${(0, config_1.backendKeyOf)(currentType)}).${clearedCount ? `\nšŸ—‘ļø Cleared ${clearedCount} queued message${clearedCount === 1 ? '' : 's'}.` : ''}`,
@@ -2621,11 +2749,9 @@ You can also use natural language commands to control Claude Code CLI.`,
2621
2749
  });
2622
2750
  return;
2623
2751
  }
2624
- const newConfig = { ...currentConfig, type: target.id };
2625
2752
  if (isThreadOverride) {
2626
2753
  try {
2627
- await this.threadPool.switchThreadBackend(threadId, (0, config_1.backendKeyOf)(target.id));
2628
- const clearedCount = this.clearThreadQueue(threadId);
2754
+ const { clearedCount } = await this.applyBackendSetting(threadId, (0, config_1.backendKeyOf)(target.id), 'thread');
2629
2755
  this.sendResponse(messageId, threadId, {
2630
2756
  success: true,
2631
2757
  output: `āœ… This thread switched to: ${target.label}${clearedCount ? `\nšŸ—‘ļø Cleared ${clearedCount} queued message${clearedCount === 1 ? '' : 's'}.` : ''}\n\nUse /backend ${installed.indexOf(target) + 1} to switch all threads, or /backend default @ to follow the global backend again.`,
@@ -2647,10 +2773,12 @@ You can also use natural language commands to control Claude Code CLI.`,
2647
2773
  });
2648
2774
  return;
2649
2775
  }
2650
- await this.config.set('executor', newConfig);
2651
- await this.threadPool.switchBackend(newConfig);
2652
- await this.threadManager.clearBackendOverrides();
2653
- const clearedCount = this.clearAllQueues();
2776
+ const applied = await this.applyBackendSetting(threadId, (0, config_1.backendKeyOf)(target.id), 'all');
2777
+ if (!applied.success) {
2778
+ this.sendResponse(messageId, threadId, applied);
2779
+ return;
2780
+ }
2781
+ const { clearedCount } = applied;
2654
2782
  this.sendResponse(messageId, threadId, {
2655
2783
  success: true,
2656
2784
  output: `āœ… Backend switched to: ${target.label}${clearedCount ? `\nšŸ—‘ļø Cleared ${clearedCount} queued message${clearedCount === 1 ? '' : 's'}.` : ''}\n\nAll threads will use the new backend for future commands. Conversations on the previous backend are preserved — switch back to resume them.`,
@@ -2661,6 +2789,7 @@ You can also use natural language commands to control Claude Code CLI.`,
2661
2789
  * Destroy handler and all executors
2662
2790
  */
2663
2791
  async destroy() {
2792
+ this.settingsService.destroy();
2664
2793
  this.isDestroyed = true;
2665
2794
  await this.fileInbox?.destroy();
2666
2795
  this.notificationAdapter.unregister();