u-foo 1.7.4 → 1.7.5

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "u-foo",
3
- "version": "1.7.4",
3
+ "version": "1.7.5",
4
4
  "description": "Multi-Agent Workspace Protocol. Just add u. claude → uclaude, codex → ucodex.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "homepage": "https://ufoo.dev",
@@ -275,7 +275,7 @@ function buildSystemPrompt(context) {
275
275
  ' "reply": "string",',
276
276
  ` "assistant_call": {"kind":"explore|bash|mixed","task":"string","context":"optional","expect":"optional","provider":"codex|claude|ufoo (optional)","model":"optional","timeout_ms":${DEFAULT_ASSISTANT_TIMEOUT_MS}},`,
277
277
  ' "dispatch": [{"target":"broadcast|<agent-id>|<nickname>","message":"string","injection_mode":"immediate|queued (optional)","source":"optional"}],',
278
- ' "ops": [{"action":"launch|close|rename|cron","agent":"codex|claude|ucode","count":1,"agent_id":"id","nickname":"optional","operation":"start|list|stop","every":"30m","interval_ms":1800000,"target":"agent-id|nickname|csv","targets":["agent-id"],"prompt":"message","id":"task-id|all"}],',
278
+ ' "ops": [{"action":"launch|close|rename|cron","agent":"codex|claude|ucode","count":1,"agent_id":"id","nickname":"optional","operation":"start|list|stop","every":"30m","interval_ms":1800000,"at":"YYYY-MM-DD HH:mm","once_at_ms":1700000000000,"target":"agent-id|nickname|csv","targets":["agent-id"],"title":"optional short title","prompt":"message","id":"task-id|all"}],',
279
279
  ' "disambiguate": {"prompt":"string","candidates":[{"agent_id":"id","reason":"string"}]}',
280
280
  "}",
281
281
  "Rules:",
@@ -283,7 +283,7 @@ function buildSystemPrompt(context) {
283
283
  "- If multiple possible agents, use disambiguate with candidates and no dispatch.",
284
284
  "- If user specifies a nickname for a new agent, include ops.launch with nickname so daemon can rename.",
285
285
  "- If user requests rename, use ops.rename with agent_id and nickname (do NOT launch).",
286
- "- For scheduled follow-up (cron), use ops.cron with operation=start and include every+target(s)+prompt (or at for one-time).",
286
+ "- For scheduled follow-up (cron), use ops.cron with operation=start and include target(s)+prompt, plus optional title; use every/interval_ms for recurring or at/once_at_ms for one-time.",
287
287
  "- To check scheduled tasks, use ops.cron with operation=list.",
288
288
  "- To stop scheduled tasks, use ops.cron with operation=stop and id (or id=all).",
289
289
  "- Use top-level assistant_call for project exploration, temporary shell tasks, and quick execution support.",
@@ -18,16 +18,11 @@ function resolveAssistantEngine({
18
18
  } = {}) {
19
19
  const config = loadConfig(projectRoot);
20
20
 
21
- const hasRequestedProvider = String(requestedProvider || "").trim().length > 0;
22
21
  const requested = normalizeAssistantEngine(requestedProvider);
23
- const configEngine = normalizeAssistantEngine(config.assistantEngine);
24
22
  const fallback = normalizeAssistantEngine(fallbackProvider) || "codex";
25
23
 
26
24
  let selected = requested;
27
- if (selected === "auto") {
28
- // Explicit assistant_call provider=auto should inherit current main agent provider.
29
- selected = hasRequestedProvider ? fallback : configEngine;
30
- }
25
+ // Omitted/auto assistant providers inherit the active ufoo-agent provider.
31
26
  if (selected === "auto") selected = fallback;
32
27
  if (selected === "auto") selected = "codex";
33
28
 
@@ -688,6 +688,9 @@ function createCommandExecutor(options = {}) {
688
688
  const targetsRaw = String(
689
689
  kv.target || kv.targets || kv.agent || kv.agents || ""
690
690
  ).trim();
691
+ const title = String(
692
+ kv.title || kv.name || kv.label || ""
693
+ ).trim();
691
694
  const prompt = String(
692
695
  kv.prompt || kv.message || kv.msg || nonKvParts.join(" ") || ""
693
696
  ).trim();
@@ -695,7 +698,7 @@ function createCommandExecutor(options = {}) {
695
698
  if ((!intervalRaw && !atRaw) || !targetsRaw || !prompt) {
696
699
  logMessage(
697
700
  "error",
698
- "{white-fg}✗{/white-fg} Usage: /cron start every=<10s|5m> or at=\"YYYY-MM-DD HH:mm\" target=<agent1,agent2> prompt=\"...\""
701
+ "{white-fg}✗{/white-fg} Usage: /cron start every=<10s|5m> or at=\"YYYY-MM-DD HH:mm\" target=<agent1,agent2> [title=\"...\"] prompt=\"...\""
699
702
  );
700
703
  return;
701
704
  }
@@ -728,21 +731,18 @@ function createCommandExecutor(options = {}) {
728
731
  }
729
732
 
730
733
  if (typeof requestCron === "function") {
734
+ const request = {
735
+ operation: "start",
736
+ targets,
737
+ prompt,
738
+ };
739
+ if (title) request.title = title;
731
740
  if (atMs > 0) {
732
- requestCron({
733
- operation: "start",
734
- once_at_ms: atMs,
735
- targets,
736
- prompt,
737
- });
741
+ request.once_at_ms = atMs;
738
742
  } else {
739
- requestCron({
740
- operation: "start",
741
- interval_ms: intervalMs,
742
- targets,
743
- prompt,
744
- });
743
+ request.interval_ms = intervalMs;
745
744
  }
745
+ requestCron(request);
746
746
  schedule(requestStatus, 200);
747
747
  return;
748
748
  }
@@ -752,11 +752,13 @@ function createCommandExecutor(options = {}) {
752
752
  return;
753
753
  }
754
754
 
755
- const task = createCronTask({
755
+ const taskPayload = {
756
756
  intervalMs,
757
757
  targets,
758
758
  prompt,
759
- });
759
+ };
760
+ if (title) taskPayload.title = title;
761
+ const task = createCronTask(taskPayload);
760
762
  if (!task) {
761
763
  logMessage("error", "{white-fg}✗{/white-fg} Failed to create cron task");
762
764
  return;
@@ -764,7 +766,7 @@ function createCommandExecutor(options = {}) {
764
766
 
765
767
  logMessage(
766
768
  "system",
767
- `{white-fg}✓{/white-fg} Cron started ${task.id}: ${atMs > 0 ? `at ${formatCronAt(atMs)}` : `every ${formatIntervalMs(intervalMs)}`} -> ${targets.join(", ")}`
769
+ `{white-fg}✓{/white-fg} Cron started ${task.id}: ${task.label || `${atMs > 0 ? `at ${formatCronAt(atMs)}` : `every ${formatIntervalMs(intervalMs)}`} -> ${targets.join(", ")}`}`
768
770
  );
769
771
  }
770
772
 
@@ -30,7 +30,7 @@ const COMMAND_TREE = {
30
30
  "/cron": {
31
31
  desc: "Cron scheduler operations",
32
32
  children: {
33
- start: { desc: "Create cron task" },
33
+ start: { desc: "Create cron task (optional title)" },
34
34
  list: { desc: "List cron tasks" },
35
35
  stop: { desc: "Stop cron task by id or all" },
36
36
  },
@@ -29,13 +29,39 @@ function sanitizeSummaryText(value = "") {
29
29
  .trim();
30
30
  }
31
31
 
32
+ function truncateCronText(value = "", maxLength = 24) {
33
+ const text = String(value || "").trim();
34
+ if (!text) return "";
35
+ if (text.length <= maxLength) return text;
36
+ return `${text.slice(0, Math.max(1, maxLength - 3))}...`;
37
+ }
38
+
39
+ function resolveTaskTitle(task = {}) {
40
+ const explicitTitle = truncateCronText(
41
+ sanitizeSummaryText(task.title || "").replace(/:/g, "-"),
42
+ 24
43
+ );
44
+ if (explicitTitle) return explicitTitle;
45
+ const fallbackTitle = truncateCronText(
46
+ sanitizeSummaryText(task.prompt || "").replace(/:/g, "-"),
47
+ 24
48
+ );
49
+ return fallbackTitle || "(empty)";
50
+ }
51
+
52
+ function buildTaskLabel(task = {}) {
53
+ const targets = Array.isArray(task.targets) && task.targets.length > 0
54
+ ? task.targets.join("+")
55
+ : "unknown";
56
+ const title = resolveTaskTitle(task);
57
+ const interval = formatIntervalMs(task.intervalMs || 0);
58
+ return `${targets}:${title}:${interval}`;
59
+ }
60
+
32
61
  function summarizeTask(task = {}) {
33
62
  const id = String(task.id || "");
34
- const interval = formatIntervalMs(task.intervalMs || 0);
35
- const targets = Array.isArray(task.targets) ? task.targets.join("+") : "";
36
- const promptRaw = sanitizeSummaryText(task.prompt || "");
37
- const prompt = promptRaw.length > 24 ? `${promptRaw.slice(0, 24)}...` : promptRaw;
38
- return `${id}@${interval}->${targets}: ${prompt || "(empty)"}`;
63
+ const label = buildTaskLabel(task);
64
+ return id ? `${id} ${label}` : label;
39
65
  }
40
66
 
41
67
  function createCronScheduler(options = {}) {
@@ -58,7 +84,7 @@ function createCronScheduler(options = {}) {
58
84
  }
59
85
  }
60
86
 
61
- function addTask({ intervalMs = 0, targets = [], prompt = "" } = {}) {
87
+ function addTask({ intervalMs = 0, targets = [], prompt = "", title = "" } = {}) {
62
88
  const safeInterval = Number.parseInt(intervalMs, 10);
63
89
  const safeTargets = Array.isArray(targets)
64
90
  ? targets.map((item) => String(item || "").trim()).filter(Boolean)
@@ -74,6 +100,7 @@ function createCronScheduler(options = {}) {
74
100
  intervalMs: safeInterval,
75
101
  targets: Array.from(new Set(safeTargets)),
76
102
  prompt: safePrompt,
103
+ title: resolveTaskTitle({ title, prompt: safePrompt }),
77
104
  createdAt: nowFn(),
78
105
  lastRunAt: 0,
79
106
  tickCount: 0,
@@ -100,6 +127,7 @@ function createCronScheduler(options = {}) {
100
127
  notifyChange();
101
128
  return {
102
129
  ...task,
130
+ label: buildTaskLabel(task),
103
131
  summary: summarizeTask(task),
104
132
  };
105
133
  }
@@ -110,9 +138,11 @@ function createCronScheduler(options = {}) {
110
138
  intervalMs: task.intervalMs,
111
139
  targets: task.targets.slice(),
112
140
  prompt: task.prompt,
141
+ title: task.title,
113
142
  createdAt: task.createdAt,
114
143
  lastRunAt: task.lastRunAt,
115
144
  tickCount: task.tickCount,
145
+ label: buildTaskLabel(task),
116
146
  summary: summarizeTask(task),
117
147
  }));
118
148
  }
@@ -155,6 +185,7 @@ function createCronScheduler(options = {}) {
155
185
  module.exports = {
156
186
  parseIntervalMs,
157
187
  formatIntervalMs,
188
+ buildTaskLabel,
158
189
  summarizeTask,
159
190
  createCronScheduler,
160
191
  };
@@ -254,12 +254,12 @@ function createDaemonMessageRouter(options = {}) {
254
254
  if (task.mode === "once") {
255
255
  logMessage(
256
256
  "system",
257
- `{white-fg}✓{/white-fg} Cron scheduled ${escapeBlessed(task.id)} at ${escapeBlessed(task.onceAt || String(task.onceAtMs || ""))}`
257
+ `{white-fg}✓{/white-fg} Cron scheduled ${escapeBlessed(task.id)}: ${escapeBlessed(task.label || task.onceAt || String(task.onceAtMs || ""))}`
258
258
  );
259
259
  } else {
260
260
  logMessage(
261
261
  "system",
262
- `{white-fg}✓{/white-fg} Cron started ${escapeBlessed(task.id)}: every ${escapeBlessed(task.interval || String(task.intervalMs || ""))}`
262
+ `{white-fg}✓{/white-fg} Cron started ${escapeBlessed(task.id)}: ${escapeBlessed(task.label || task.interval || String(task.intervalMs || ""))}`
263
263
  );
264
264
  }
265
265
  } else if (operation === "stop") {
@@ -18,7 +18,6 @@ function createDashboardKeyController(options = {}) {
18
18
  exitDashboardMode = () => {},
19
19
  setLaunchMode = () => {},
20
20
  setAgentProvider = () => {},
21
- setAssistantEngine = () => {},
22
21
  setAutoResume = () => {},
23
22
  clampAgentWindow = () => {},
24
23
  clampAgentWindowWithSelection = () => {},
@@ -232,10 +231,7 @@ function createDashboardKeyController(options = {}) {
232
231
  }
233
232
 
234
233
  if (key.name === "down") {
235
- state.dashboardView = "assistant";
236
- const list = Array.isArray(state.assistantOptions) ? state.assistantOptions : [];
237
- const nextIndex = list.findIndex((opt) => opt.value === state.assistantEngine);
238
- state.selectedAssistantIndex = nextIndex >= 0 ? nextIndex : 0;
234
+ state.dashboardView = "cron";
239
235
  renderDashboardAndScreen();
240
236
  return true;
241
237
  }
@@ -261,67 +257,9 @@ function createDashboardKeyController(options = {}) {
261
257
  return true;
262
258
  }
263
259
 
264
- function handleAssistantKey(key) {
265
- const options = Array.isArray(state.assistantOptions) ? state.assistantOptions : [];
266
- if (options.length === 0) {
267
- if (key.name === "up") {
268
- state.dashboardView = "provider";
269
- renderDashboardAndScreen();
270
- return true;
271
- }
272
- if (key.name === "escape" || key.name === "enter" || key.name === "return") {
273
- exitDashboardMode(false);
274
- return true;
275
- }
276
- return true;
277
- }
278
-
279
- if (key.name === "left") {
280
- state.selectedAssistantIndex = state.selectedAssistantIndex <= 0
281
- ? options.length - 1
282
- : state.selectedAssistantIndex - 1;
283
- renderDashboardAndScreen();
284
- return true;
285
- }
286
-
287
- if (key.name === "right") {
288
- state.selectedAssistantIndex = state.selectedAssistantIndex >= options.length - 1
289
- ? 0
290
- : state.selectedAssistantIndex + 1;
291
- renderDashboardAndScreen();
292
- return true;
293
- }
294
-
295
- if (key.name === "up") {
296
- state.dashboardView = "provider";
297
- renderDashboardAndScreen();
298
- return true;
299
- }
300
-
301
- if (key.name === "down") {
302
- state.dashboardView = "cron";
303
- renderDashboardAndScreen();
304
- return true;
305
- }
306
-
307
- if (key.name === "enter" || key.name === "return") {
308
- const selected = options[state.selectedAssistantIndex];
309
- if (selected) setAssistantEngine(selected.value);
310
- exitDashboardMode(false);
311
- return true;
312
- }
313
-
314
- if (key.name === "escape") {
315
- exitDashboardMode(false);
316
- return true;
317
- }
318
-
319
- return true;
320
- }
321
-
322
260
  function handleCronKey(key) {
323
261
  if (key.name === "up") {
324
- state.dashboardView = "assistant";
262
+ state.dashboardView = "provider";
325
263
  renderDashboardAndScreen();
326
264
  return true;
327
265
  }
@@ -552,7 +490,6 @@ function createDashboardKeyController(options = {}) {
552
490
 
553
491
  if (state.dashboardView === "mode") return handleModeKey(key);
554
492
  if (state.dashboardView === "provider") return handleProviderKey(key);
555
- if (state.dashboardView === "assistant") return handleAssistantKey(key);
556
493
  if (state.dashboardView === "resume") return handleResumeKey(key);
557
494
  if (state.dashboardView === "cron") return handleCronKey(key);
558
495
 
@@ -8,14 +8,6 @@ function providerLabel(value) {
8
8
  return "codex";
9
9
  }
10
10
 
11
- function assistantLabel(value) {
12
- if (value === "codex") return "codex";
13
- if (value === "claude") return "claude";
14
- if (value === "ufoo") return "ucode";
15
- if (value === "ucode") return "ucode";
16
- return "auto";
17
- }
18
-
19
11
  function ensureAtPrefix(value) {
20
12
  const text = String(value || "").trim();
21
13
  if (!text) return text;
@@ -43,7 +35,6 @@ function buildSummaryLine(options = {}) {
43
35
  getAgentState = () => "",
44
36
  launchMode = "terminal",
45
37
  agentProvider = "codex-cli",
46
- assistantEngine = "auto",
47
38
  cronTasks = [],
48
39
  } = options;
49
40
  const agents = activeAgents.length > 0
@@ -55,7 +46,6 @@ function buildSummaryLine(options = {}) {
55
46
  return `{gray-fg}Agents:{/gray-fg} {cyan-fg}${agents}{/cyan-fg}`
56
47
  + ` {gray-fg}Mode:{/gray-fg} {cyan-fg}${launchMode}{/cyan-fg}`
57
48
  + ` {gray-fg}Agent:{/gray-fg} {cyan-fg}${providerLabel(agentProvider)}{/cyan-fg}`
58
- + ` {gray-fg}Assistant:{/gray-fg} {cyan-fg}${assistantLabel(assistantEngine)}{/cyan-fg}`
59
49
  + ` {gray-fg}Cron:{/gray-fg} {cyan-fg}${Array.isArray(cronTasks) ? cronTasks.length : 0}{/cyan-fg}`;
60
50
  }
61
51
 
@@ -134,11 +124,9 @@ function buildDashboardDetailLine(options = {}) {
134
124
  getAgentState = () => "",
135
125
  selectedModeIndex = 0,
136
126
  selectedProviderIndex = 0,
137
- selectedAssistantIndex = 0,
138
127
  selectedResumeIndex = 0,
139
128
  cronTasks = [],
140
129
  providerOptions = [],
141
- assistantOptions = [],
142
130
  resumeOptions = [],
143
131
  dashHints = {},
144
132
  modeOptions = DEFAULT_MODE_OPTIONS,
@@ -171,18 +159,6 @@ function buildDashboardDetailLine(options = {}) {
171
159
  return { content, windowStart };
172
160
  }
173
161
 
174
- if (dashboardView === "assistant") {
175
- const assistantParts = assistantOptions.map((opt, i) => {
176
- if (i === selectedAssistantIndex) {
177
- return `{inverse}${opt.label}{/inverse}`;
178
- }
179
- return `{cyan-fg}${opt.label}{/cyan-fg}`;
180
- });
181
- content += `{gray-fg}Assistant:{/gray-fg} ${assistantParts.join(" ")}`;
182
- content += ` {gray-fg}│ ${dashHints.assistant || ""}{/gray-fg}`;
183
- return { content, windowStart };
184
- }
185
-
186
162
  if (dashboardView === "resume") {
187
163
  const resumeParts = resumeOptions.map((opt, i) => {
188
164
  if (i === selectedResumeIndex) {
@@ -198,9 +174,9 @@ function buildDashboardDetailLine(options = {}) {
198
174
  if (dashboardView === "cron") {
199
175
  const items = Array.isArray(cronTasks) ? cronTasks : [];
200
176
  const summary = items.length > 0
201
- ? items.map((item) => item.summary || item.id || "").filter(Boolean).join(", ")
177
+ ? items.map((item) => item.label || item.summary || item.id || "").filter(Boolean).join(", ")
202
178
  : "none";
203
- content += `{gray-fg}Cron:{/gray-fg} {cyan-fg}${summary}{/cyan-fg}`;
179
+ content += `{gray-fg}Cron:{/gray-fg} {inverse}${summary}{/inverse}`;
204
180
  content += ` {gray-fg}│ ${dashHints.cron || ""}{/gray-fg}`;
205
181
  return { content, windowStart };
206
182
  }
@@ -259,14 +235,11 @@ function computeDashboardContent(options = {}) {
259
235
  getAgentState = () => "",
260
236
  launchMode = "terminal",
261
237
  agentProvider = "codex-cli",
262
- assistantEngine = "auto",
263
238
  selectedModeIndex = 0,
264
239
  selectedProviderIndex = 0,
265
- selectedAssistantIndex = 0,
266
240
  selectedResumeIndex = 0,
267
241
  cronTasks = [],
268
242
  providerOptions = [],
269
- assistantOptions = [],
270
243
  resumeOptions = [],
271
244
  dashHints = {},
272
245
  modeOptions = DEFAULT_MODE_OPTIONS,
@@ -297,7 +270,6 @@ function computeDashboardContent(options = {}) {
297
270
  getAgentState,
298
271
  launchMode,
299
272
  agentProvider,
300
- assistantEngine,
301
273
  cronTasks,
302
274
  });
303
275
  return {
@@ -317,11 +289,9 @@ function computeDashboardContent(options = {}) {
317
289
  getAgentState,
318
290
  selectedModeIndex,
319
291
  selectedProviderIndex,
320
- selectedAssistantIndex,
321
292
  selectedResumeIndex,
322
293
  cronTasks,
323
294
  providerOptions,
324
- assistantOptions,
325
295
  resumeOptions,
326
296
  dashHints,
327
297
  modeOptions,
@@ -344,11 +314,9 @@ function computeDashboardContent(options = {}) {
344
314
  getAgentState,
345
315
  selectedModeIndex,
346
316
  selectedProviderIndex,
347
- selectedAssistantIndex,
348
317
  selectedResumeIndex,
349
318
  cronTasks,
350
319
  providerOptions,
351
- assistantOptions,
352
320
  resumeOptions,
353
321
  dashHints,
354
322
  modeOptions,
@@ -362,7 +330,6 @@ function computeDashboardContent(options = {}) {
362
330
  getAgentState,
363
331
  launchMode,
364
332
  agentProvider,
365
- assistantEngine,
366
333
  cronTasks,
367
334
  });
368
335
 
@@ -372,5 +339,4 @@ function computeDashboardContent(options = {}) {
372
339
  module.exports = {
373
340
  computeDashboardContent,
374
341
  providerLabel,
375
- assistantLabel,
376
342
  };
package/src/chat/index.js CHANGED
@@ -9,7 +9,6 @@ const {
9
9
  saveConfig,
10
10
  normalizeLaunchMode,
11
11
  normalizeAgentProvider,
12
- normalizeAssistantEngine,
13
12
  } = require("../config");
14
13
  const { socketPath, isRunning } = require("../daemon");
15
14
  const UfooInit = require("../init");
@@ -104,7 +103,6 @@ async function runChat(projectRoot, options = {}) {
104
103
  const config = loadConfig(projectRoot);
105
104
  let launchMode = config.launchMode;
106
105
  let agentProvider = config.agentProvider;
107
- let assistantEngine = normalizeAssistantEngine(config.assistantEngine);
108
106
  let autoResume = config.autoResume !== false;
109
107
  let cronTasks = [];
110
108
 
@@ -675,7 +673,7 @@ async function runChat(projectRoot, options = {}) {
675
673
  let selectedAgentIndex = -1; // -1 = not in dashboard selection mode
676
674
  let targetAgent = null; // Selected agent for direct messaging
677
675
  let focusMode = "input"; // "input" or "dashboard"
678
- let dashboardView = "agents"; // "projects" | "agents" | "mode" | "provider" | "assistant" | "cron"
676
+ let dashboardView = "agents"; // "projects" | "agents" | "mode" | "provider" | "cron"
679
677
  let reportPendingTotal = 0;
680
678
  let selectedModeIndex = Math.max(0, MODE_OPTIONS.indexOf(launchMode));
681
679
  const providerOptions = [
@@ -684,16 +682,6 @@ async function runChat(projectRoot, options = {}) {
684
682
  { label: "ucode", value: "ucode" },
685
683
  ];
686
684
  let selectedProviderIndex = Math.max(0, providerOptions.findIndex((opt) => opt.value === agentProvider));
687
- const assistantOptions = [
688
- { label: "auto", value: "auto" },
689
- { label: "codex", value: "codex" },
690
- { label: "claude", value: "claude" },
691
- { label: "ucode", value: "ufoo" },
692
- ];
693
- let selectedAssistantIndex = Math.max(
694
- 0,
695
- assistantOptions.findIndex((opt) => opt.value === assistantEngine)
696
- );
697
685
  const resumeOptions = [
698
686
  { label: "Resume previous session", value: true },
699
687
  { label: "Start new session", value: false },
@@ -704,8 +692,7 @@ async function runChat(projectRoot, options = {}) {
704
692
  agentsGlobal: "←/→ select · Enter · ↓ mode · ↑ projects",
705
693
  agentsEmpty: "↓ mode · ↑ back",
706
694
  mode: "←/→ select · Enter · ↓ provider · ↑ back",
707
- provider: "←/→ select · Enter · ↓ assistant · ↑ back",
708
- assistant: "←/→ select · Enter · ↓ cron · ↑ back",
695
+ provider: "←/→ select · Enter · ↓ cron · ↑ back",
709
696
  cron: "Ctrl+X close · ↑ back",
710
697
  resume: "",
711
698
  projects: "Use /project switch <index|path>",
@@ -1081,12 +1068,6 @@ async function runChat(projectRoot, options = {}) {
1081
1068
  }
1082
1069
  }
1083
1070
 
1084
- function setAssistantEngine(value) {
1085
- if (settingsController) {
1086
- settingsController.setAssistantEngine(value);
1087
- }
1088
- }
1089
-
1090
1071
  function setAutoResume(value) {
1091
1072
  if (settingsController) {
1092
1073
  settingsController.setAutoResume(value);
@@ -1103,7 +1084,6 @@ async function runChat(projectRoot, options = {}) {
1103
1084
  saveConfig,
1104
1085
  normalizeLaunchMode,
1105
1086
  normalizeAgentProvider,
1106
- normalizeAssistantEngine,
1107
1087
  fsModule: fs,
1108
1088
  getUfooPaths,
1109
1089
  logMessage,
@@ -1124,14 +1104,6 @@ async function runChat(projectRoot, options = {}) {
1124
1104
  setSelectedProviderIndex: (value) => {
1125
1105
  selectedProviderIndex = value;
1126
1106
  },
1127
- getAssistantEngine: () => assistantEngine,
1128
- setAssistantEngineState: (value) => {
1129
- assistantEngine = value;
1130
- },
1131
- setSelectedAssistantIndex: (value) => {
1132
- selectedAssistantIndex = value;
1133
- },
1134
- assistantOptions,
1135
1107
  providerOptions,
1136
1108
  modeOptions: MODE_OPTIONS,
1137
1109
  getAutoResume: () => autoResume,
@@ -1180,15 +1152,12 @@ async function runChat(projectRoot, options = {}) {
1180
1152
  },
1181
1153
  launchMode,
1182
1154
  agentProvider,
1183
- assistantEngine,
1184
1155
  autoResume,
1185
1156
  selectedModeIndex,
1186
1157
  selectedProviderIndex,
1187
- selectedAssistantIndex,
1188
1158
  selectedResumeIndex,
1189
1159
  cronTasks,
1190
1160
  providerOptions,
1191
- assistantOptions,
1192
1161
  resumeOptions,
1193
1162
  pendingReports: reportPendingTotal,
1194
1163
  dashHints: DASH_HINTS,
@@ -1337,10 +1306,6 @@ async function runChat(projectRoot, options = {}) {
1337
1306
  }
1338
1307
  selectedModeIndex = Math.max(0, MODE_OPTIONS.indexOf(launchMode));
1339
1308
  selectedProviderIndex = Math.max(0, providerOptions.findIndex((opt) => opt.value === agentProvider));
1340
- selectedAssistantIndex = Math.max(
1341
- 0,
1342
- assistantOptions.findIndex((opt) => opt.value === assistantEngine)
1343
- );
1344
1309
  selectedResumeIndex = autoResume ? 0 : 1;
1345
1310
  // Immediately set @target when first agent is selected.
1346
1311
  if (!globalMode && selectedAgentIndex >= 0 && selectedAgentIndex < activeAgents.length) {
@@ -1368,15 +1333,12 @@ async function runChat(projectRoot, options = {}) {
1368
1333
  activeAgentMetaMap: { get: () => activeAgentMetaMap },
1369
1334
  selectedModeIndex: { get: () => selectedModeIndex, set: (value) => { selectedModeIndex = value; } },
1370
1335
  selectedProviderIndex: { get: () => selectedProviderIndex, set: (value) => { selectedProviderIndex = value; } },
1371
- selectedAssistantIndex: { get: () => selectedAssistantIndex, set: (value) => { selectedAssistantIndex = value; } },
1372
1336
  selectedResumeIndex: { get: () => selectedResumeIndex, set: (value) => { selectedResumeIndex = value; } },
1373
1337
  launchMode: { get: () => launchMode },
1374
1338
  agentProvider: { get: () => agentProvider },
1375
- assistantEngine: { get: () => assistantEngine },
1376
1339
  autoResume: { get: () => autoResume },
1377
1340
  cronTasks: { get: () => cronTasks },
1378
1341
  providerOptions: { get: () => providerOptions },
1379
- assistantOptions: { get: () => assistantOptions },
1380
1342
  resumeOptions: { get: () => resumeOptions },
1381
1343
  agentOutputSuppressed: {
1382
1344
  get: () => getAgentOutputSuppressed(),
@@ -1415,7 +1377,6 @@ async function runChat(projectRoot, options = {}) {
1415
1377
  exitDashboardMode,
1416
1378
  setLaunchMode,
1417
1379
  setAgentProvider,
1418
- setAssistantEngine,
1419
1380
  setAutoResume,
1420
1381
  clampAgentWindow,
1421
1382
  clampAgentWindowWithSelection,
@@ -6,7 +6,6 @@ function createSettingsController(options = {}) {
6
6
  saveConfig = () => {},
7
7
  normalizeLaunchMode = (value) => value,
8
8
  normalizeAgentProvider = (value) => value,
9
- normalizeAssistantEngine = (value) => value,
10
9
  fsModule,
11
10
  getUfooPaths = () => ({ agentDir: "" }),
12
11
  logMessage = () => {},
@@ -19,10 +18,6 @@ function createSettingsController(options = {}) {
19
18
  getAgentProvider = () => "codex-cli",
20
19
  setAgentProviderState = () => {},
21
20
  setSelectedProviderIndex = () => {},
22
- getAssistantEngine = () => "auto",
23
- setAssistantEngineState = () => {},
24
- setSelectedAssistantIndex = () => {},
25
- assistantOptions = [],
26
21
  providerOptions = [],
27
22
  modeOptions = [],
28
23
  getAutoResume = () => true,
@@ -41,14 +36,6 @@ function createSettingsController(options = {}) {
41
36
  return value === "claude-cli" ? "claude" : "codex";
42
37
  }
43
38
 
44
- function assistantLabel(value) {
45
- const normalized = normalizeAssistantEngine(value);
46
- if (normalized === "codex") return "codex";
47
- if (normalized === "claude") return "claude";
48
- if (normalized === "ufoo") return "ufoo";
49
- return "auto";
50
- }
51
-
52
39
  function clearUfooAgentIdentity() {
53
40
  const agentDir = getUfooPaths(projectRoot).agentDir;
54
41
  const stateFile = path.join(agentDir, "ufoo-agent.json");
@@ -106,26 +93,11 @@ function createSettingsController(options = {}) {
106
93
  return true;
107
94
  }
108
95
 
109
- function setAssistantEngine(engine) {
110
- const next = normalizeAssistantEngine(engine);
111
- if (next === getAssistantEngine()) return false;
112
- setAssistantEngineState(next);
113
- const idx = assistantOptions.findIndex((opt) => opt && opt.value === next);
114
- setSelectedAssistantIndex(idx >= 0 ? idx : 0);
115
- saveConfig(projectRoot, { assistantEngine: next });
116
- logMessage("status", `{white-fg}⚙{/white-fg} assistant-engine: ${assistantLabel(next)}`);
117
- renderDashboard();
118
- renderScreen();
119
- return true;
120
- }
121
-
122
96
  return {
123
97
  providerLabel,
124
- assistantLabel,
125
98
  clearUfooAgentIdentity,
126
99
  setLaunchMode,
127
100
  setAgentProvider,
128
- setAssistantEngine,
129
101
  setAutoResume,
130
102
  };
131
103
  }
@@ -128,18 +128,41 @@ function sanitizeSummaryText(value = "") {
128
128
  .trim();
129
129
  }
130
130
 
131
- function summarizeCronTask(task = {}) {
132
- const id = String(task.id || "");
133
- const targets = Array.isArray(task.targets) ? task.targets.join("+") : "";
134
- const promptRaw = sanitizeSummaryText(task.prompt || "");
135
- const prompt = promptRaw.length > 24 ? `${promptRaw.slice(0, 24)}...` : promptRaw;
131
+ function truncateCronText(value = "", maxLength = 24) {
132
+ const text = String(value || "").trim();
133
+ if (!text) return "";
134
+ if (text.length <= maxLength) return text;
135
+ return `${text.slice(0, Math.max(1, maxLength - 3))}...`;
136
+ }
136
137
 
137
- if (Number(task.onceAtMs) > 0) {
138
- return `${id}@once(${formatCronAtMs(task.onceAtMs)})->${targets}: ${prompt || "(empty)"}`;
139
- }
138
+ function normalizeCronTitle(value = "", prompt = "") {
139
+ const explicitTitle = truncateCronText(
140
+ sanitizeSummaryText(value || "").replace(/:/g, "-"),
141
+ 24
142
+ );
143
+ if (explicitTitle) return explicitTitle;
144
+ const fallbackTitle = truncateCronText(
145
+ sanitizeSummaryText(prompt || "").replace(/:/g, "-"),
146
+ 24
147
+ );
148
+ return fallbackTitle || "(empty)";
149
+ }
140
150
 
141
- const interval = formatIntervalMs(task.intervalMs || 0);
142
- return `${id}@${interval}->${targets}: ${prompt || "(empty)"}`;
151
+ function buildCronLabel(task = {}) {
152
+ const targets = Array.isArray(task.targets) && task.targets.length > 0
153
+ ? task.targets.join("+")
154
+ : "unknown";
155
+ const title = normalizeCronTitle(task.title || "", task.prompt || "");
156
+ const schedule = Number(task.onceAtMs) > 0
157
+ ? formatCronAtMs(task.onceAtMs)
158
+ : formatIntervalMs(task.intervalMs || 0);
159
+ return `${targets}:${title}:${schedule || "0s"}`;
160
+ }
161
+
162
+ function summarizeCronTask(task = {}) {
163
+ const id = String(task.id || "");
164
+ const label = buildCronLabel(task);
165
+ return id ? `${id} ${label}` : label;
143
166
  }
144
167
 
145
168
  function formatCronTask(task = {}) {
@@ -153,6 +176,8 @@ function formatCronTask(task = {}) {
153
176
  onceAt: onceAtMs > 0 ? formatCronAtMs(onceAtMs) : "",
154
177
  targets: Array.isArray(task.targets) ? task.targets.slice() : [],
155
178
  prompt: String(task.prompt || ""),
179
+ title: normalizeCronTitle(task.title || "", task.prompt || ""),
180
+ label: buildCronLabel(task),
156
181
  createdAt: Number(task.createdAt) || 0,
157
182
  lastRunAt: Number(task.lastRunAt) || 0,
158
183
  tickCount: Number(task.tickCount) || 0,
@@ -160,6 +185,10 @@ function formatCronTask(task = {}) {
160
185
  };
161
186
  }
162
187
 
188
+ function resolveCronTitle(op = {}) {
189
+ return String(op.title || op.name || op.label || "").trim();
190
+ }
191
+
163
192
  function createDaemonCronController(options = {}) {
164
193
  const {
165
194
  projectRoot = "",
@@ -198,6 +227,7 @@ function createDaemonCronController(options = {}) {
198
227
  onceAtMs: task.onceAtMs,
199
228
  targets: task.targets.slice(),
200
229
  prompt: task.prompt,
230
+ title: task.title,
201
231
  createdAt: task.createdAt,
202
232
  lastRunAt: task.lastRunAt,
203
233
  tickCount: task.tickCount,
@@ -275,13 +305,14 @@ function createDaemonCronController(options = {}) {
275
305
  }, task.intervalMs);
276
306
  }
277
307
 
278
- function addTask({ intervalMs = 0, onceAtMs = 0, targets = [], prompt = "" } = {}) {
308
+ function addTask({ intervalMs = 0, onceAtMs = 0, targets = [], prompt = "", title = "" } = {}) {
279
309
  const safeInterval = Number.parseInt(intervalMs, 10);
280
310
  const safeOnceAt = Number.parseInt(onceAtMs, 10);
281
311
  const safeTargets = Array.isArray(targets)
282
312
  ? targets.map((item) => String(item || "").trim()).filter(Boolean)
283
313
  : [];
284
314
  const safePrompt = String(prompt || "").trim();
315
+ const safeTitle = normalizeCronTitle(title, safePrompt);
285
316
 
286
317
  if (!safePrompt || safeTargets.length === 0) return null;
287
318
 
@@ -296,6 +327,7 @@ function createDaemonCronController(options = {}) {
296
327
  onceAtMs: useOnce ? safeOnceAt : 0,
297
328
  targets: Array.from(new Set(safeTargets)),
298
329
  prompt: safePrompt,
330
+ title: safeTitle,
299
331
  createdAt: nowFn(),
300
332
  lastRunAt: 0,
301
333
  tickCount: 0,
@@ -367,6 +399,7 @@ function createDaemonCronController(options = {}) {
367
399
  ? item.targets.map((v) => String(v || "").trim()).filter(Boolean)
368
400
  : [];
369
401
  const prompt = String(item && item.prompt ? item.prompt : "").trim();
402
+ const title = normalizeCronTitle(item && item.title ? item.title : "", prompt);
370
403
 
371
404
  if (!prompt || targets.length === 0) {
372
405
  changed = true;
@@ -389,6 +422,7 @@ function createDaemonCronController(options = {}) {
389
422
  onceAtMs: Number.isFinite(onceAtMs) ? Math.floor(onceAtMs) : 0,
390
423
  targets: Array.from(new Set(targets)),
391
424
  prompt,
425
+ title,
392
426
  createdAt: Number(item && item.createdAt) || now,
393
427
  lastRunAt: Number(item && item.lastRunAt) || 0,
394
428
  tickCount: Number(item && item.tickCount) || 0,
@@ -535,6 +569,7 @@ function createDaemonCronController(options = {}) {
535
569
  onceAtMs,
536
570
  targets,
537
571
  prompt,
572
+ title: resolveCronTitle(op),
538
573
  });
539
574
 
540
575
  if (!task) {
@@ -567,8 +602,10 @@ module.exports = {
567
602
  resolveCronOperation,
568
603
  resolveCronIntervalMs,
569
604
  resolveCronOnceAtMs,
605
+ resolveCronTitle,
570
606
  resolveCronPrompt,
571
607
  resolveCronTaskId,
572
608
  parseCronAtMs,
609
+ buildCronLabel,
573
610
  formatCronTask,
574
611
  };
@@ -915,9 +915,9 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
915
915
  }
916
916
  } else if (result.operation === "start" && result.task) {
917
917
  if (result.task.mode === "once") {
918
- reply = `Cron scheduled ${result.task.id} at ${result.task.onceAt || result.task.onceAtMs}`;
918
+ reply = `Cron scheduled ${result.task.id}: ${result.task.label || result.task.onceAt || result.task.onceAtMs}`;
919
919
  } else {
920
- reply = `Cron started ${result.task.id}: every ${result.task.interval || result.task.intervalMs}`;
920
+ reply = `Cron started ${result.task.id}: ${result.task.label || result.task.interval || result.task.intervalMs}`;
921
921
  }
922
922
  } else {
923
923
  reply = "Cron updated";
@@ -72,6 +72,8 @@ function normalizeCronTasks(raw = []) {
72
72
  onceAt: String(task && task.onceAt ? task.onceAt : ""),
73
73
  targets: Array.isArray(task && task.targets) ? task.targets.slice() : [],
74
74
  prompt: String(task && task.prompt ? task.prompt : ""),
75
+ title: String(task && task.title ? task.title : ""),
76
+ label: String(task && task.label ? task.label : ""),
75
77
  summary: String(task && task.summary ? task.summary : ""),
76
78
  createdAt: Number(task && task.createdAt ? task.createdAt : 0) || 0,
77
79
  lastRunAt: Number(task && task.lastRunAt ? task.lastRunAt : 0) || 0,