u-foo 2.2.3 → 2.3.0

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 (67) hide show
  1. package/SKILLS/ufoo/SKILL.md +56 -12
  2. package/SKILLS/uinit/SKILL.md +3 -2
  3. package/modules/AGENTS.template.md +2 -1
  4. package/modules/bus/README.md +1 -1
  5. package/modules/context/SKILLS/uctx/SKILL.md +6 -4
  6. package/package.json +1 -1
  7. package/src/agent/codexThreadProvider.js +2 -2
  8. package/src/agent/controllerToolExecutor.js +24 -1
  9. package/src/agent/credentials/claude.js +85 -16
  10. package/src/agent/credentials/codex.js +251 -23
  11. package/src/agent/defaultBootstrap.js +3 -1
  12. package/src/agent/directAuthStatus.js +264 -0
  13. package/src/agent/internalRunner.js +18 -12
  14. package/src/agent/loopObservability.js +10 -0
  15. package/src/agent/loopRuntime.js +19 -0
  16. package/src/agent/ufooAgent.js +43 -13
  17. package/src/agent/upstreamTransport.js +23 -8
  18. package/src/bus/index.js +13 -5
  19. package/src/bus/message.js +157 -9
  20. package/src/bus/nickname.js +14 -3
  21. package/src/bus/subscriber.js +30 -10
  22. package/src/chat/agentDirectory.js +2 -2
  23. package/src/chat/commandExecutor.js +190 -8
  24. package/src/chat/commands.js +23 -4
  25. package/src/chat/completionController.js +30 -7
  26. package/src/chat/daemonMessageRouter.js +2 -1
  27. package/src/chat/index.js +9 -8
  28. package/src/cli/groupCoreCommands.js +5 -0
  29. package/src/cli.js +309 -0
  30. package/src/code/UCODE_PROMPT.md +3 -2
  31. package/src/code/nativeRunner.js +2 -1
  32. package/src/code/prompts/ufoo.js +3 -2
  33. package/src/config.js +16 -3
  34. package/src/context/doctor.js +1 -1
  35. package/src/daemon/groupOrchestrator.js +13 -9
  36. package/src/daemon/index.js +35 -18
  37. package/src/daemon/nicknameScope.js +37 -0
  38. package/src/daemon/ops.js +22 -10
  39. package/src/daemon/promptRequest.js +11 -2
  40. package/src/daemon/reporting.js +2 -3
  41. package/src/daemon/soloBootstrap.js +15 -3
  42. package/src/daemon/status.js +5 -1
  43. package/src/group/bootstrap.js +1 -1
  44. package/src/group/promptProfiles.js +106 -22
  45. package/src/group/templates.js +1 -0
  46. package/src/init/index.js +4 -0
  47. package/src/memory/historySearch.js +308 -0
  48. package/src/memory/index.js +653 -8
  49. package/src/providerapi/redactor.js +4 -1
  50. package/src/status/index.js +26 -2
  51. package/src/tools/handlers/memory.js +168 -0
  52. package/src/tools/index.js +12 -0
  53. package/src/tools/registry.js +12 -0
  54. package/src/tools/schemaFixtures.js +213 -0
  55. package/src/tools/tier1/editMemory.js +14 -0
  56. package/src/tools/tier1/forget.js +14 -0
  57. package/src/tools/tier1/recall.js +14 -0
  58. package/src/tools/tier1/remember.js +14 -0
  59. package/src/tools/tier1/searchHistory.js +14 -0
  60. package/src/tools/tier1/searchMemory.js +14 -0
  61. package/templates/groups/build-lane.json +44 -6
  62. package/templates/groups/build-ultra.json +6 -5
  63. package/templates/groups/design-system.json +84 -0
  64. package/templates/groups/product-discovery.json +9 -4
  65. package/templates/groups/ui-plan-review.json +84 -0
  66. package/templates/groups/ui-polish.json +6 -2
  67. package/templates/groups/verify-ship.json +9 -4
@@ -175,20 +175,29 @@ class SubscriberManager {
175
175
  // 检查是否是重新加入(rejoin)
176
176
  const existingMeta = this.busData.agents[subscriber];
177
177
  let finalNickname = nickname;
178
+ let finalScopedNickname = typeof options.scopedNickname === "string"
179
+ ? options.scopedNickname.trim()
180
+ : (typeof process.env.UFOO_SCOPED_NICKNAME === "string" ? process.env.UFOO_SCOPED_NICKNAME.trim() : "");
178
181
 
179
- if (existingMeta && existingMeta.nickname) {
180
- // 重新加入,保留原昵称
181
- finalNickname = existingMeta.nickname;
182
- } else if (nickname) {
182
+ if (nickname) {
183
183
  // 新昵称,检查冲突
184
- if (nicknameManager.nicknameExists(nickname, subscriber)) {
184
+ const conflictTarget = finalScopedNickname || nickname;
185
+ if (nicknameManager.nicknameExists(conflictTarget, subscriber)) {
185
186
  throw new Error(`Nickname "${nickname}" already exists`);
186
187
  }
187
188
  finalNickname = nickname;
189
+ } else if (existingMeta && existingMeta.nickname) {
190
+ // 重新加入,保留原昵称
191
+ finalNickname = existingMeta.nickname;
192
+ finalScopedNickname = existingMeta.scoped_nickname || finalScopedNickname || finalNickname;
188
193
  } else {
189
194
  // 自动生成昵称(并标记占用,避免并发重复)
190
195
  finalNickname = nicknameManager.generateAutoNickname(agentType);
191
- nicknameManager.setNickname(subscriber, finalNickname);
196
+ nicknameManager.setNickname(subscriber, finalNickname, finalScopedNickname);
197
+ }
198
+
199
+ if (!finalScopedNickname) {
200
+ finalScopedNickname = existingMeta?.scoped_nickname || finalNickname;
192
201
  }
193
202
 
194
203
  const explicitLaunchMode = typeof options.launchMode === "string"
@@ -221,6 +230,7 @@ class SubscriberManager {
221
230
  const inheritedNickname = await this.cleanupDuplicateTty(subscriber, finalTty);
222
231
  if (inheritedNickname && !nickname && !existingMeta) {
223
232
  finalNickname = inheritedNickname;
233
+ if (!finalScopedNickname) finalScopedNickname = inheritedNickname;
224
234
  }
225
235
 
226
236
  // 更新订阅者信息(保留已有字段,如 provider_session_*)
@@ -252,6 +262,7 @@ class SubscriberManager {
252
262
  ...preserved,
253
263
  agent_type: agentType,
254
264
  nickname: finalNickname,
265
+ scoped_nickname: finalScopedNickname || finalNickname,
255
266
  status: "active",
256
267
  activity_state: "starting",
257
268
  activity_since: getTimestamp(),
@@ -311,7 +322,11 @@ class SubscriberManager {
311
322
  // 创建队列目录
312
323
  this.queueManager.ensureQueueDir(subscriber);
313
324
 
314
- return { subscriber, nickname: finalNickname };
325
+ return {
326
+ subscriber,
327
+ nickname: finalNickname,
328
+ scopedNickname: this.busData.agents[subscriber].scoped_nickname || finalNickname,
329
+ };
315
330
  }
316
331
 
317
332
  /**
@@ -333,22 +348,27 @@ class SubscriberManager {
333
348
  /**
334
349
  * 重命名订阅者
335
350
  */
336
- async rename(subscriber, newNickname) {
351
+ async rename(subscriber, newNickname, options = {}) {
337
352
  if (!this.busData.agents || !this.busData.agents[subscriber]) {
338
353
  throw new Error(`Subscriber "${subscriber}" not found`);
339
354
  }
340
355
 
341
356
  const nicknameManager = new NicknameManager(this.busData);
357
+ const scopedNickname = typeof options.scopedNickname === "string" && options.scopedNickname.trim()
358
+ ? options.scopedNickname.trim()
359
+ : newNickname;
342
360
 
343
361
  // 检查昵称冲突
344
- if (nicknameManager.nicknameExists(newNickname, subscriber)) {
362
+ if (nicknameManager.nicknameExists(scopedNickname, subscriber)) {
345
363
  throw new Error(`Nickname "${newNickname}" already exists`);
346
364
  }
347
365
 
348
366
  const oldNickname = this.busData.agents[subscriber].nickname;
367
+ const oldScopedNickname = this.busData.agents[subscriber].scoped_nickname || oldNickname;
349
368
  this.busData.agents[subscriber].nickname = newNickname;
369
+ this.busData.agents[subscriber].scoped_nickname = scopedNickname;
350
370
 
351
- return { subscriber, oldNickname, newNickname };
371
+ return { subscriber, oldNickname, newNickname, oldScopedNickname, newScopedNickname: scopedNickname };
352
372
  }
353
373
 
354
374
  /**
@@ -10,8 +10,8 @@ function buildAgentMaps(activeAgents = [], metaList = [], fallbackMap = null) {
10
10
 
11
11
  for (const id of activeAgents) {
12
12
  const meta = metaById.get(id);
13
- const label = meta && meta.nickname
14
- ? meta.nickname
13
+ const label = meta && (meta.display_nickname || meta.nickname)
14
+ ? (meta.display_nickname || meta.nickname)
15
15
  : (fallbackMap && fallbackMap.get(id)) || id;
16
16
  labelMap.set(id, label);
17
17
  if (meta) {
@@ -11,10 +11,15 @@ const {
11
11
  normalizeControllerMode,
12
12
  } = require("../config");
13
13
  const { resolveTransport } = require("../code/nativeRunner");
14
+ const { resolveDisplayNickname } = require("../daemon/nicknameScope");
14
15
  const { parseIntervalMs, formatIntervalMs } = require("./cronScheduler");
15
16
  const { isGlobalControllerProjectRoot, resolveGlobalControllerUfooDir } = require("../projects");
16
17
  const { loadPromptProfileRegistry } = require("../group/promptProfiles");
17
18
  const { resolveSoloAgentType } = require("../solo/commands");
19
+ const {
20
+ inspectDirectAuthStatus,
21
+ formatDirectAuthStatus,
22
+ } = require("../agent/directAuthStatus");
18
23
 
19
24
  function defaultCreateDoctor(projectRoot) {
20
25
  const UfooDoctor = require("../doctor");
@@ -38,6 +43,41 @@ function defaultResolveTerminalApp() {
38
43
  return "";
39
44
  }
40
45
 
46
+ const SETTINGS_MODEL_DEFAULTS = Object.freeze({
47
+ agent: Object.freeze({
48
+ codex: "gpt-5.5",
49
+ claude: "opus-4.7",
50
+ }),
51
+ router: Object.freeze({
52
+ codex: "gpt-5.4-mini",
53
+ claude: "sonnet-4.7",
54
+ }),
55
+ });
56
+
57
+ function normalizeSettingsProvider(value = "", fallback = "codex-cli") {
58
+ const text = String(value || "").trim().toLowerCase();
59
+ if (text === "claude" || text === "claude-cli" || text === "claude-code" || text === "anthropic") {
60
+ return "claude-cli";
61
+ }
62
+ if (text === "codex" || text === "codex-cli" || text === "codex-code" || text === "openai") {
63
+ return "codex-cli";
64
+ }
65
+ return fallback;
66
+ }
67
+
68
+ function agentProviderKey(value = "") {
69
+ return normalizeSettingsProvider(value) === "claude-cli" ? "claude" : "codex";
70
+ }
71
+
72
+ function defaultAgentModelForProvider(value = "") {
73
+ return SETTINGS_MODEL_DEFAULTS.agent[agentProviderKey(value)] || SETTINGS_MODEL_DEFAULTS.agent.codex;
74
+ }
75
+
76
+ function defaultGateModelForProvider(value = "") {
77
+ const key = agentProviderKey(value);
78
+ return SETTINGS_MODEL_DEFAULTS.router[key] || SETTINGS_MODEL_DEFAULTS.router.codex;
79
+ }
80
+
41
81
  function collectHostLaunchRequestContext(env = process.env) {
42
82
  const hostInjectSock = String(env.UFOO_HOST_INJECT_SOCK || env.HORIZON_INJECT_SOCK || "").trim();
43
83
  const hostDaemonSock = String(env.UFOO_HOST_DAEMON_SOCK || "").trim();
@@ -101,6 +141,8 @@ function createCommandExecutor(options = {}) {
101
141
  listCronTasks = () => [],
102
142
  stopCronTask = () => false,
103
143
  runGroupCore = runGroupCoreCommand,
144
+ inspectDirectAuth = inspectDirectAuthStatus,
145
+ formatDirectAuth = formatDirectAuthStatus,
104
146
  requestCron = null,
105
147
  globalMode = false,
106
148
  listProjects = () => [],
@@ -163,6 +205,14 @@ function createCommandExecutor(options = {}) {
163
205
  } else {
164
206
  logMessage("system", "{white-fg}✗{/white-fg} Daemon is not running");
165
207
  }
208
+
209
+ const authStatus = await inspectDirectAuth({
210
+ projectRoot: getActiveProjectRoot(),
211
+ autoRefresh: false,
212
+ });
213
+ for (const line of formatDirectAuth(authStatus, { compact: true })) {
214
+ logMessage("system", escapeBlessed(line));
215
+ }
166
216
  }
167
217
 
168
218
  async function handleDaemonCommand(args = []) {
@@ -319,7 +369,8 @@ function createCommandExecutor(options = {}) {
319
369
  } else {
320
370
  logMessage("system", "{cyan-fg}Active agents:{/cyan-fg}");
321
371
  for (const [id, meta] of subscribers) {
322
- const nickname = meta && meta.nickname ? ` (${meta.nickname})` : "";
372
+ const displayNickname = meta ? resolveDisplayNickname(projectRoot, meta) : "";
373
+ const nickname = displayNickname ? ` (${displayNickname})` : "";
323
374
  const status = meta && meta.status ? meta.status : "unknown";
324
375
  logMessage("system", ` • ${id}${nickname} {white-fg}[${status}]{/white-fg}`);
325
376
  }
@@ -1138,8 +1189,14 @@ function createCommandExecutor(options = {}) {
1138
1189
 
1139
1190
  async function handleSettingsCommand(args = []) {
1140
1191
  const section = String(args[0] || "").trim().toLowerCase();
1141
- if (!section) {
1142
- logMessage("error", "{white-fg}✗{/white-fg} Usage: /settings <router|ucode> ...");
1192
+ if (!section || section === "show" || section === "status") {
1193
+ await handleSettingsOverviewCommand();
1194
+ return;
1195
+ }
1196
+
1197
+ if (section === "agent" || section === "ufoo") {
1198
+ const subArgs = args.slice(1);
1199
+ await handleAgentSettingsCommand(subArgs);
1143
1200
  return;
1144
1201
  }
1145
1202
 
@@ -1159,7 +1216,116 @@ function createCommandExecutor(options = {}) {
1159
1216
  return;
1160
1217
  }
1161
1218
 
1162
- logMessage("error", "{white-fg}✗{/white-fg} Unknown settings section. Use: router, ucode");
1219
+ logMessage("error", "{white-fg}✗{/white-fg} Unknown settings section. Use: show, agent, router, ucode");
1220
+ }
1221
+
1222
+ async function handleSettingsOverviewCommand() {
1223
+ const config = loadConfig(projectRoot) || {};
1224
+ const agentProvider = normalizeSettingsProvider(config.agentProvider);
1225
+ const agentKey = agentProviderKey(agentProvider);
1226
+ const agentModel = String(config.agentModel || "").trim();
1227
+ const routerMode = normalizeControllerMode(config.controllerMode);
1228
+ const routerProvider = String(config.routerProvider || "").trim();
1229
+ const routerModel = String(config.routerModel || "").trim();
1230
+ const ucodeConfig = loadUcodeConfig() || {};
1231
+ const ucodeProvider = String(ucodeConfig.ucodeProvider || "").trim();
1232
+ const ucodeModel = String(ucodeConfig.ucodeModel || "").trim();
1233
+
1234
+ logMessage("system", "{cyan-fg}settings:{/cyan-fg}");
1235
+ logMessage("system", ` • agent: ${agentKey} · model ${agentModel || `(unset, recommended ${defaultAgentModelForProvider(agentProvider)})`}`);
1236
+ logMessage("system", ` • router: mode ${routerMode} · provider ${routerProvider || "(unset)"} · model ${routerModel || "(unset)"}`);
1237
+ logMessage("system", ` • ucode: provider ${ucodeProvider || "(unset)"} · model ${ucodeModel || "(unset)"}`);
1238
+ logMessage("system", " • use: /settings agent | /settings router | /settings ucode");
1239
+ }
1240
+
1241
+ async function handleAgentSettingsCommand(args = []) {
1242
+ const first = String(args[0] || "").trim().toLowerCase();
1243
+ const hasInlineKv = args.some((item) => String(item || "").includes("="));
1244
+ const action = !first ? "show" : (hasInlineKv ? "set" : first);
1245
+
1246
+ if (action === "show" || action === "status") {
1247
+ const config = loadConfig(projectRoot) || {};
1248
+ const provider = normalizeSettingsProvider(config.agentProvider);
1249
+ const key = agentProviderKey(provider);
1250
+ const model = String(config.agentModel || "").trim();
1251
+ logMessage("system", "{cyan-fg}ufoo-agent config:{/cyan-fg}");
1252
+ logMessage("system", ` • provider: ${key}`);
1253
+ logMessage("system", ` • model: ${model || `(unset, recommended ${defaultAgentModelForProvider(provider)})`}`);
1254
+ logMessage("system", ` • defaults: codex=${SETTINGS_MODEL_DEFAULTS.agent.codex}, claude=${SETTINGS_MODEL_DEFAULTS.agent.claude}`);
1255
+ logMessage("system", " • use: /settings agent set provider=<codex|claude> model=<id>");
1256
+ return;
1257
+ }
1258
+
1259
+ if (action === "codex" || action === "claude") {
1260
+ const kv = parseKeyValueArgs(args.slice(1));
1261
+ const provider = action === "claude" ? "claude-cli" : "codex-cli";
1262
+ const model = String(kv.model || defaultAgentModelForProvider(provider)).trim();
1263
+ saveConfig(projectRoot, {
1264
+ agentProvider: provider,
1265
+ agentModel: model,
1266
+ });
1267
+ logMessage("system", "{white-fg}✓{/white-fg} ufoo-agent config updated");
1268
+ logMessage("system", ` • provider: ${agentProviderKey(provider)}`);
1269
+ logMessage("system", ` • model: ${model}`);
1270
+ await restartDaemon(projectRoot);
1271
+ return;
1272
+ }
1273
+
1274
+ if (action === "set") {
1275
+ const kvArgs = hasInlineKv ? args : args.slice(1);
1276
+ const kv = parseKeyValueArgs(kvArgs);
1277
+ const updates = {};
1278
+ let nextProvider = "";
1279
+
1280
+ if (Object.prototype.hasOwnProperty.call(kv, "provider")) {
1281
+ nextProvider = normalizeSettingsProvider(kv.provider, "");
1282
+ if (!nextProvider) {
1283
+ logMessage("error", "{white-fg}✗{/white-fg} Usage: /settings agent set provider=<codex|claude> model=<id>");
1284
+ return;
1285
+ }
1286
+ updates.agentProvider = nextProvider;
1287
+ }
1288
+ if (Object.prototype.hasOwnProperty.call(kv, "model")) {
1289
+ updates.agentModel = String(kv.model || "").trim();
1290
+ } else if (nextProvider) {
1291
+ updates.agentModel = defaultAgentModelForProvider(nextProvider);
1292
+ }
1293
+
1294
+ if (Object.keys(updates).length === 0) {
1295
+ logMessage("error", "{white-fg}✗{/white-fg} Usage: /settings agent set provider=<codex|claude> model=<id>");
1296
+ return;
1297
+ }
1298
+
1299
+ saveConfig(projectRoot, updates);
1300
+ logMessage("system", "{white-fg}✓{/white-fg} ufoo-agent config updated");
1301
+ if (Object.prototype.hasOwnProperty.call(updates, "agentProvider")) {
1302
+ logMessage("system", ` • provider: ${agentProviderKey(updates.agentProvider)}`);
1303
+ }
1304
+ if (Object.prototype.hasOwnProperty.call(updates, "agentModel")) {
1305
+ logMessage("system", ` • model: ${updates.agentModel || "(unset)"}`);
1306
+ }
1307
+ await restartDaemon(projectRoot);
1308
+ return;
1309
+ }
1310
+
1311
+ if (action === "clear") {
1312
+ const fieldsRaw = args.slice(1).map((item) => String(item || "").trim().toLowerCase()).filter(Boolean);
1313
+ const fields = fieldsRaw.length === 0 ? ["model"] : fieldsRaw;
1314
+ const updates = {};
1315
+ const clearAll = fields.includes("all");
1316
+ if (clearAll || fields.includes("provider")) updates.agentProvider = "codex-cli";
1317
+ if (clearAll || fields.includes("model")) updates.agentModel = "";
1318
+ if (Object.keys(updates).length === 0) {
1319
+ logMessage("error", "{white-fg}✗{/white-fg} Usage: /settings agent clear [provider|model|all]");
1320
+ return;
1321
+ }
1322
+ saveConfig(projectRoot, updates);
1323
+ logMessage("system", "{white-fg}✓{/white-fg} ufoo-agent config cleared");
1324
+ await restartDaemon(projectRoot);
1325
+ return;
1326
+ }
1327
+
1328
+ logMessage("error", "{white-fg}✗{/white-fg} Unknown settings agent action. Use: show, set, clear, codex, claude");
1163
1329
  }
1164
1330
 
1165
1331
  async function handleRouterSettingsCommand(args = []) {
@@ -1170,11 +1336,27 @@ function createCommandExecutor(options = {}) {
1170
1336
  if (action === "show" || action === "status") {
1171
1337
  const config = loadConfig(projectRoot) || {};
1172
1338
  const mode = normalizeControllerMode(config.controllerMode);
1173
- logMessage("system", "{cyan-fg}router config:{/cyan-fg}");
1339
+ logMessage("system", "{cyan-fg}gate router config:{/cyan-fg}");
1174
1340
  logMessage("system", ` • controllerMode: ${mode}`);
1175
1341
  logMessage("system", ` • provider: ${String(config.routerProvider || "").trim() || "(unset)"}`);
1176
1342
  logMessage("system", ` • model: ${String(config.routerModel || "").trim() || "(unset)"}`);
1177
1343
  logMessage("system", " • allowed modes: main | loop | legacy | shadow");
1344
+ logMessage("system", ` • defaults: codex=${SETTINGS_MODEL_DEFAULTS.router.codex}, claude=${SETTINGS_MODEL_DEFAULTS.router.claude}`);
1345
+ return;
1346
+ }
1347
+
1348
+ if (action === "codex" || action === "claude") {
1349
+ const kv = parseKeyValueArgs(args.slice(1));
1350
+ const provider = action;
1351
+ const model = String(kv.model || defaultGateModelForProvider(provider)).trim();
1352
+ saveConfig(projectRoot, {
1353
+ routerProvider: provider,
1354
+ routerModel: model,
1355
+ });
1356
+ logMessage("system", "{white-fg}✓{/white-fg} gate router config updated");
1357
+ logMessage("system", ` • provider: ${provider}`);
1358
+ logMessage("system", ` • model: ${model}`);
1359
+ await restartDaemon(projectRoot);
1178
1360
  return;
1179
1361
  }
1180
1362
 
@@ -1200,7 +1382,7 @@ function createCommandExecutor(options = {}) {
1200
1382
  }
1201
1383
 
1202
1384
  saveConfig(projectRoot, updates);
1203
- logMessage("system", "{white-fg}✓{/white-fg} router config updated");
1385
+ logMessage("system", "{white-fg}✓{/white-fg} gate router config updated");
1204
1386
  if (Object.prototype.hasOwnProperty.call(updates, "controllerMode")) {
1205
1387
  logMessage("system", ` • controllerMode: ${updates.controllerMode}`);
1206
1388
  }
@@ -1227,7 +1409,7 @@ function createCommandExecutor(options = {}) {
1227
1409
  return;
1228
1410
  }
1229
1411
  saveConfig(projectRoot, updates);
1230
- logMessage("system", "{white-fg}✓{/white-fg} router config cleared");
1412
+ logMessage("system", "{white-fg}✓{/white-fg} gate router config cleared");
1231
1413
  await restartDaemon(projectRoot);
1232
1414
  return;
1233
1415
  }
@@ -1239,7 +1421,7 @@ function createCommandExecutor(options = {}) {
1239
1421
  }
1240
1422
 
1241
1423
  saveConfig(projectRoot, { controllerMode: nextMode });
1242
- logMessage("system", `{white-fg}✓{/white-fg} router mode set to ${nextMode}`);
1424
+ logMessage("system", `{white-fg}✓{/white-fg} gate router mode set to ${nextMode}`);
1243
1425
  await restartDaemon(projectRoot);
1244
1426
  }
1245
1427
 
@@ -87,20 +87,39 @@ const COMMAND_TREE = {
87
87
  "/settings": {
88
88
  desc: "Settings operations",
89
89
  children: {
90
+ show: {
91
+ desc: "Show settings overview",
92
+ order: 1,
93
+ },
94
+ agent: {
95
+ desc: "Manage main ufoo-agent/router provider/model",
96
+ order: 2,
97
+ children: {
98
+ show: { desc: "Show main agent provider/model", order: 1 },
99
+ set: { desc: "Set provider=<codex|claude> model=<id>", order: 2 },
100
+ clear: { desc: "Clear agent model or reset provider", order: 3 },
101
+ codex: { desc: "Use Codex default model (gpt-5.5)", order: 4 },
102
+ claude: { desc: "Use Claude default model (opus-4.7)", order: 5 },
103
+ },
104
+ },
90
105
  router: {
91
- desc: "Manage controller routing mode/provider/model",
106
+ desc: "Manage gate router mode/provider/model",
107
+ order: 3,
92
108
  children: {
93
- show: { desc: "Show router mode/provider/model", order: 1 },
94
- set: { desc: "Set router mode/provider/model", order: 2 },
95
- clear: { desc: "Clear router provider/model or reset mode", order: 3 },
109
+ show: { desc: "Show gate router mode/provider/model", order: 1 },
110
+ set: { desc: "Set mode/provider/model", order: 2 },
111
+ clear: { desc: "Clear gate router provider/model or reset mode", order: 3 },
96
112
  main: { desc: "Set router mode to main", order: 4 },
97
113
  loop: { desc: "Set router mode to loop", order: 5 },
98
114
  legacy: { desc: "Set router mode to legacy", order: 6 },
99
115
  shadow: { desc: "Set router mode to shadow", order: 7 },
116
+ codex: { desc: "Use Codex gate model (gpt-5.4-mini)", order: 8 },
117
+ claude: { desc: "Use Claude gate model (sonnet-4.7)", order: 9 },
100
118
  },
101
119
  },
102
120
  ucode: {
103
121
  desc: "Manage ucode model provider config",
122
+ order: 4,
104
123
  children: {
105
124
  show: { desc: "Show ucode provider/model/url/key", order: 1 },
106
125
  set: { desc: "Set ucode provider/model/url/key", order: 2 },
@@ -24,6 +24,34 @@ function mapSubcommandSuggestions(subs = [], parentCmd, tokenIndex, filterText =
24
24
  .sort(sortSubcommandEntries);
25
25
  }
26
26
 
27
+ function buildNestedSubcommandSuggestions(subs = [], mainCmd, parts = [], endsWithSpace = false) {
28
+ let currentSubs = Array.isArray(subs) ? subs : [];
29
+ if (currentSubs.length === 0) return [];
30
+
31
+ for (let index = 1; index < parts.length; index += 1) {
32
+ const token = String(parts[index] || "");
33
+ const exact = currentSubs.find((sub) =>
34
+ String(sub && sub.cmd ? sub.cmd : "").toLowerCase() === token.toLowerCase()
35
+ );
36
+ const isLastToken = index === parts.length - 1;
37
+
38
+ if (exact && Array.isArray(exact.subcommands) && exact.subcommands.length > 0) {
39
+ if (!isLastToken || endsWithSpace) {
40
+ currentSubs = exact.subcommands;
41
+ continue;
42
+ }
43
+ }
44
+
45
+ if (exact && isLastToken && endsWithSpace) {
46
+ return [];
47
+ }
48
+
49
+ return mapSubcommandSuggestions(currentSubs, mainCmd, index, token);
50
+ }
51
+
52
+ return mapSubcommandSuggestions(currentSubs, mainCmd, parts.length, "");
53
+ }
54
+
27
55
  function createCompletionController(options = {}) {
28
56
  const {
29
57
  input,
@@ -230,14 +258,9 @@ function createCompletionController(options = {}) {
230
258
  subs = Array.from(merged.values());
231
259
  }
232
260
  if (isLaunch) {
233
- return mapSubcommandSuggestions(subs, mainCmd, 1, "");
234
- }
235
- const selectedSub = subs.find((sub) => String(sub && sub.cmd ? sub.cmd : "").toLowerCase() === subFilter.toLowerCase());
236
- if (selectedSub && selectedSub.subcommands && (parts.length > 2 || endsWithSpace)) {
237
- const nestedFilter = parts[2] || "";
238
- return mapSubcommandSuggestions(selectedSub.subcommands, mainCmd, 2, nestedFilter);
261
+ return mapSubcommandSuggestions(subs, mainCmd, 1, subFilter);
239
262
  }
240
- return mapSubcommandSuggestions(subs, mainCmd, 1, subFilter);
263
+ return buildNestedSubcommandSuggestions(subs, mainCmd, parts, endsWithSpace);
241
264
  }
242
265
  return [];
243
266
  }
@@ -229,7 +229,8 @@ function createDaemonMessageRouter(options = {}) {
229
229
  if (recoverableList.length > 0) {
230
230
  logMessage("system", "{cyan-fg}Recoverable agents:{/cyan-fg}");
231
231
  recoverableList.forEach((item) => {
232
- const nickname = item.nickname ? ` (${item.nickname})` : "";
232
+ const displayNickname = item.display_nickname || item.nickname || "";
233
+ const nickname = displayNickname ? ` (${displayNickname})` : "";
233
234
  const meta = item.launchMode ? ` [${item.agent}/${item.launchMode}]` : ` [${item.agent}]`;
234
235
  logMessage("system", ` • ${escapeBlessed(`${item.id}${nickname}${meta}`)}`);
235
236
  });
package/src/chat/index.js CHANGED
@@ -15,6 +15,7 @@ const UfooInit = require("../init");
15
15
  const AgentActivator = require("../bus/activate");
16
16
  const { subscriberToSafeName } = require("../bus/utils");
17
17
  const { getUfooPaths } = require("../ufoo/paths");
18
+ const { resolveDisplayNickname } = require("../daemon/nicknameScope");
18
19
  const { startDaemon, stopDaemon, connectWithRetry } = require("./transport");
19
20
  const { escapeBlessed, stripBlessedTags, truncateText } = require("./text");
20
21
  const { COMMAND_REGISTRY, parseCommand, parseAtTarget } = require("./commands");
@@ -562,6 +563,7 @@ async function runChat(projectRoot, options = {}) {
562
563
  return registry.templates.map((item) => ({
563
564
  alias: item.alias,
564
565
  name: item.templateName || item.templateId || "",
566
+ desc: item.templateDescription || "",
565
567
  source: item.source || "",
566
568
  }));
567
569
  },
@@ -915,7 +917,7 @@ async function runChat(projectRoot, options = {}) {
915
917
  const busPath = getUfooPaths(activeProjectRoot).agentsFile;
916
918
  const bus = JSON.parse(fs.readFileSync(busPath, "utf8"));
917
919
  for (const [id, meta] of Object.entries(bus.agents || {})) {
918
- if (meta && meta.nickname === nickname) return id;
920
+ if (meta && (meta.nickname === nickname || meta.scoped_nickname === nickname)) return id;
919
921
  }
920
922
  } catch {
921
923
  // ignore lookup errors
@@ -934,7 +936,7 @@ async function runChat(projectRoot, options = {}) {
934
936
  const busPath = getUfooPaths(activeProjectRoot).agentsFile;
935
937
  const bus = JSON.parse(fs.readFileSync(busPath, "utf8"));
936
938
  const meta = bus.agents && bus.agents[id];
937
- if (meta && meta.nickname) return meta.nickname;
939
+ if (meta) return resolveDisplayNickname(activeProjectRoot, meta);
938
940
  } catch {
939
941
  // Keep original publisher ID
940
942
  }
@@ -1275,7 +1277,9 @@ async function runChat(projectRoot, options = {}) {
1275
1277
  const bus = JSON.parse(fs.readFileSync(busPath, "utf8"));
1276
1278
  fallbackMap = new Map();
1277
1279
  for (const [id, meta] of Object.entries(bus.agents || {})) {
1278
- if (meta && meta.nickname) fallbackMap.set(id, meta.nickname);
1280
+ if (!meta) continue;
1281
+ const displayNickname = resolveDisplayNickname(activeProjectRoot, meta);
1282
+ if (displayNickname) fallbackMap.set(id, displayNickname);
1279
1283
  }
1280
1284
  } catch {
1281
1285
  fallbackMap = null;
@@ -2100,12 +2104,9 @@ async function runChat(projectRoot, options = {}) {
2100
2104
  if (runtimeWatchDebounce) return;
2101
2105
  runtimeWatchDebounce = setTimeout(() => {
2102
2106
  runtimeWatchDebounce = null;
2103
- const prevCount = projectRuntimes.length;
2104
2107
  refreshProjectRuntimes();
2105
- if (projectRuntimes.length !== prevCount) {
2106
- renderDashboard();
2107
- screen.render();
2108
- }
2108
+ renderDashboard();
2109
+ screen.render();
2109
2110
  }, 300);
2110
2111
  });
2111
2112
  screen.on("destroy", () => watcher.close());
@@ -80,6 +80,9 @@ function printList({ templates, errors }, { write, json, cwd }) {
80
80
  const displayPath = formatDisplayPath(item.filePath, cwd);
81
81
  write(`- ${item.alias} [${item.source}]`);
82
82
  write(` name: ${nameLabel}`);
83
+ if (item.templateDescription) {
84
+ write(` desc: ${item.templateDescription}`);
85
+ }
83
86
  write(` id: ${idLabel} schema: ${verLabel}`);
84
87
  write(` file: ${displayPath}`);
85
88
  }
@@ -170,6 +173,7 @@ async function runGroupCoreCommand(subcmd, cmdArgs = [], options = {}) {
170
173
  filePath: item.filePath,
171
174
  templateId: item.templateId || "",
172
175
  templateName: item.templateName || "",
176
+ templateDescription: item.templateDescription || "",
173
177
  schemaVersion: item.schemaVersion,
174
178
  }));
175
179
  printList({ templates, errors: registry.errors }, { write, json, cwd });
@@ -190,6 +194,7 @@ async function runGroupCoreCommand(subcmd, cmdArgs = [], options = {}) {
190
194
  filePath: item.filePath,
191
195
  templateId: item.templateId || "",
192
196
  templateName: item.templateName || "",
197
+ templateDescription: item.templateDescription || "",
193
198
  schemaVersion: item.schemaVersion,
194
199
  }));
195
200
  printList({ templates, errors: registry.errors }, { write, json, cwd });