u-foo 2.3.1 → 2.3.3

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": "2.3.1",
3
+ "version": "2.3.3",
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",
@@ -2,6 +2,7 @@
2
2
 
3
3
  const { executeControllerTool } = require("./controllerToolExecutor");
4
4
  const { createLoopObserver } = require("./loopObservability");
5
+ const { finalizeRouterPayload } = require("../controller/routerFinalize");
5
6
 
6
7
  const DEFAULT_LOOP_OPTIONS = {
7
8
  enabled: false,
@@ -126,34 +127,23 @@ function buildLoopContinuationPrompt({
126
127
  async function finalizeLoopRun({
127
128
  projectRoot,
128
129
  payload,
130
+ prompt = "",
129
131
  processManager,
130
132
  dispatchMessages,
131
133
  handleOps,
132
134
  markPending,
133
135
  finalizeLocally = true,
134
136
  }) {
135
- if (finalizeLocally === false) {
136
- return { ok: true, payload, opsResults: [] };
137
- }
138
-
139
- const dispatch = Array.isArray(payload.dispatch) ? payload.dispatch : [];
140
- const ops = Array.isArray(payload.ops) ? payload.ops : [];
141
-
142
- for (const item of dispatch) {
143
- if (item && item.target && item.target !== "broadcast" && typeof markPending === "function") {
144
- markPending(item.target);
145
- }
146
- }
147
-
148
- if (typeof dispatchMessages === "function") {
149
- await dispatchMessages(projectRoot, dispatch);
150
- }
151
-
152
- const opsResults = typeof handleOps === "function"
153
- ? await handleOps(projectRoot, ops, processManager || null)
154
- : [];
155
-
156
- return { ok: true, payload, opsResults };
137
+ return finalizeRouterPayload({
138
+ projectRoot,
139
+ payload,
140
+ prompt,
141
+ processManager: processManager || null,
142
+ dispatchMessages,
143
+ handleOps,
144
+ markPending,
145
+ finalizeLocally,
146
+ });
157
147
  }
158
148
 
159
149
  function buildTerminalPayload(reason, lastPayload, rounds, toolCalls, toolErrors, totals = {}) {
@@ -248,6 +238,7 @@ async function runPromptWithControllerLoop({
248
238
  return finalizeLoopRun({
249
239
  projectRoot,
250
240
  payload,
241
+ prompt: currentPrompt,
251
242
  processManager,
252
243
  dispatchMessages,
253
244
  handleOps,
@@ -261,6 +252,7 @@ async function runPromptWithControllerLoop({
261
252
  return finalizeLoopRun({
262
253
  projectRoot,
263
254
  payload,
255
+ prompt: currentPrompt,
264
256
  processManager,
265
257
  dispatchMessages,
266
258
  handleOps,
@@ -357,6 +349,7 @@ async function runPromptWithControllerLoop({
357
349
  return finalizeLoopRun({
358
350
  projectRoot,
359
351
  payload: finalPayload,
352
+ prompt: currentPrompt,
360
353
  processManager,
361
354
  dispatchMessages,
362
355
  handleOps,
@@ -370,6 +363,7 @@ async function runPromptWithControllerLoop({
370
363
  return finalizeLoopRun({
371
364
  projectRoot,
372
365
  payload: finalPayload,
366
+ prompt: currentPrompt,
373
367
  processManager,
374
368
  dispatchMessages,
375
369
  handleOps,
@@ -430,6 +424,7 @@ async function runPromptWithControllerLoop({
430
424
  return finalizeLoopRun({
431
425
  projectRoot,
432
426
  payload: finalPayload,
427
+ prompt: currentPrompt,
433
428
  processManager,
434
429
  dispatchMessages,
435
430
  handleOps,
@@ -456,6 +451,7 @@ async function runPromptWithControllerLoop({
456
451
  return finalizeLoopRun({
457
452
  projectRoot,
458
453
  payload,
454
+ prompt: currentPrompt,
459
455
  processManager,
460
456
  dispatchMessages,
461
457
  handleOps,
@@ -9,6 +9,7 @@ const { normalizeProvider, sendUpstreamPrompt } = require("./upstreamTransport")
9
9
  const { normalizeAgentTypeAlias } = require("../bus/utils");
10
10
  const { buildCachedMemoryPrefix } = require("../memory");
11
11
  const { listProjectRuntimes, isGlobalControllerProjectRoot } = require("../projects");
12
+ const { assignMissingLaunchNicknames } = require("../controller/launchRouting");
12
13
  const {
13
14
  CONTROLLER_MODES,
14
15
  resolveControllerMode,
@@ -462,6 +463,8 @@ function buildSystemPrompt(context, options = {}) {
462
463
  "- When returning tool_call, set done=false and keep dispatch/ops empty for that round.",
463
464
  "- Use dispatch_message for direct bus delivery, ack_bus for controller queue acknowledgement, and launch_agent for bounded worker launches.",
464
465
  "- When you have enough information, omit tool_call and return the final reply/dispatch/ops with done=true.",
466
+ "- When launching a new coding agent for a user task, include a short task-specific nickname and include a dispatch to that launched nickname with the task.",
467
+ "- Do not emit launch-only ops for delegated work; a launched worker must receive a task in dispatch.",
465
468
  "- Do not emit assistant_call or ops.assistant_call; that legacy helper path has been removed.",
466
469
  `- Round budget: maxRounds=${loopRuntime.maxRounds || ""}, remainingToolCalls=${loopRuntime.remainingToolCalls || 0}.`,
467
470
  agentGuidance,
@@ -505,6 +508,8 @@ function buildSystemPrompt(context, options = {}) {
505
508
  "- For short-lived exploration, prefer a dispatch to an online agent or reply with a clarification.",
506
509
  "- Primary routing signal is semantic continuity from agent_prompt_history; prefer the agent that already handled similar prompts.",
507
510
  "- Launch a new coding agent when the request is a new topic without clear ownership in existing histories.",
511
+ "- When launching a new coding agent for a user task, include a short task-specific nickname and include a dispatch to that launched nickname with the task.",
512
+ "- Do not emit launch-only ops for delegated work; a launched worker must receive a task in dispatch.",
508
513
  "- dispatch.injection_mode defaults to immediate when omitted.",
509
514
  "- Use queued only when routing a chat-dialog request that is clearly a new unrelated task for an agent whose recent prompt history shows a different ongoing thread.",
510
515
  "- If the new request strongly continues the target agent's recent prompt history, keep injection_mode immediate even when that agent is busy.",
@@ -588,21 +593,6 @@ function buildRouteAgentSystemPrompt(context, options = {}) {
588
593
  ].join("\n");
589
594
  }
590
595
 
591
- function extractNickname(prompt) {
592
- if (!prompt) return "";
593
- const patterns = [
594
- /(?:叫|名为|叫做|取名|昵称)\s*([A-Za-z0-9_-]{1,32})/i,
595
- /(?:named|name)\s+([A-Za-z0-9_-]{1,32})/i,
596
- ];
597
- for (const re of patterns) {
598
- const match = prompt.match(re);
599
- if (match && match[1]) return match[1];
600
- }
601
- const quoted = prompt.match(/[“"']([^“"'\\]{1,32})[”"']/);
602
- if (quoted && quoted[1]) return quoted[1];
603
- return "";
604
- }
605
-
606
596
  function shouldUseDirectProvider(value = "") {
607
597
  const provider = normalizeProvider(value);
608
598
  return provider === "ucode" || provider === "codex" || provider === "claude";
@@ -733,15 +723,7 @@ async function runUfooAgent({
733
723
  payload = { reply: text, dispatch: [], ops: [] };
734
724
  }
735
725
 
736
- const fallbackNickname = extractNickname(prompt);
737
- if (fallbackNickname && payload && Array.isArray(payload.ops)) {
738
- for (const op of payload.ops) {
739
- if (op && (op.action === "launch" || op.action === "rename") && !op.nickname) {
740
- op.nickname = fallbackNickname;
741
- break;
742
- }
743
- }
744
- }
726
+ payload = assignMissingLaunchNicknames(payload, { prompt, context: bus });
745
727
 
746
728
  saveSessionState(projectRoot, {
747
729
  provider,
@@ -122,10 +122,11 @@ class SubscriberManager {
122
122
  }
123
123
  }
124
124
 
125
- async cleanupDuplicateTty(currentSubscriber, ttyPath) {
125
+ async cleanupDuplicateTty(currentSubscriber, ttyPath, options = {}) {
126
126
  if (!ttyPath) return null;
127
127
  if (!this.busData.agents) return null;
128
128
 
129
+ const currentAgentType = String(options.agentType || "").trim();
129
130
  let inheritedNickname = null;
130
131
  const entries = Object.entries(this.busData.agents);
131
132
  for (const [id, meta] of entries) {
@@ -136,8 +137,9 @@ class SubscriberManager {
136
137
  : (await this.queueManager.readTty(id));
137
138
  if (!metaTty) continue;
138
139
  if (metaTty === ttyPath) {
139
- // Inherit user-set nickname from the displaced entry
140
- if (meta.nickname && !inheritedNickname) {
140
+ const sameAgentType = !currentAgentType || meta?.agent_type === currentAgentType;
141
+ // Inherit user-set nickname only when replacing the same agent type.
142
+ if (sameAgentType && meta.nickname && !inheritedNickname) {
141
143
  inheritedNickname = meta.nickname;
142
144
  }
143
145
  // Remove stale subscriber using same tty
@@ -227,7 +229,7 @@ class SubscriberManager {
227
229
  // 清理同一 tty 的旧订阅者(避免重复启动污染)
228
230
  // Inherit nickname from displaced entry when this is a new subscriber
229
231
  // with no explicit nickname (e.g. session restart on same TTY)
230
- const inheritedNickname = await this.cleanupDuplicateTty(subscriber, finalTty);
232
+ const inheritedNickname = await this.cleanupDuplicateTty(subscriber, finalTty, { agentType });
231
233
  if (inheritedNickname && !nickname && !existingMeta) {
232
234
  finalNickname = inheritedNickname;
233
235
  if (!finalScopedNickname) finalScopedNickname = inheritedNickname;
@@ -117,6 +117,7 @@ function createCommandExecutor(options = {}) {
117
117
  parseCommand = () => null,
118
118
  escapeBlessed = (value) => String(value || ""),
119
119
  logMessage = () => {},
120
+ resolveStatusLine = null,
120
121
  renderScreen = () => {},
121
122
  getActiveAgents = () => [],
122
123
  getActiveAgentMetaMap = () => new Map(),
@@ -157,6 +158,10 @@ function createCommandExecutor(options = {}) {
157
158
  throw new Error("createCommandExecutor requires projectRoot");
158
159
  }
159
160
 
161
+ const statusMsg = typeof resolveStatusLine === "function"
162
+ ? resolveStatusLine
163
+ : (text) => logMessage("status", text);
164
+
160
165
  async function handleDoctorCommand() {
161
166
  logMessage("system", "{white-fg}⚙{/white-fg} Running health check...");
162
167
 
@@ -221,51 +226,51 @@ function createCommandExecutor(options = {}) {
221
226
 
222
227
  if (subcommand === "start") {
223
228
  if (isDaemonRunning(targetRoot)) {
224
- logMessage("system", "{white-fg}⚠{/white-fg} Daemon already running");
229
+ statusMsg("{gray-fg}⚠{/gray-fg} Daemon already running");
225
230
  } else {
226
- logMessage("system", "{white-fg}⚙{/white-fg} Starting daemon...");
231
+ statusMsg("{gray-fg}⚙{/gray-fg} Starting daemon...");
227
232
  startDaemon(targetRoot);
228
233
  await sleep(1000);
229
234
  if (isDaemonRunning(targetRoot)) {
230
- logMessage("system", "{white-fg}✓{/white-fg} Daemon started");
235
+ statusMsg("{gray-fg}✓{/gray-fg} Daemon started");
231
236
  } else {
232
- logMessage("error", "{white-fg}✗{/white-fg} Failed to start daemon");
237
+ statusMsg("{gray-fg}✗{/gray-fg} Failed to start daemon");
233
238
  }
234
239
  }
235
240
  return;
236
241
  }
237
242
 
238
243
  if (subcommand === "stop") {
239
- logMessage("system", "{white-fg}⚙{/white-fg} Stopping daemon...");
244
+ statusMsg("{gray-fg}⚙{/gray-fg} Stopping daemon...");
240
245
  stopDaemon(targetRoot);
241
246
  await sleep(1000);
242
247
  if (!isDaemonRunning(targetRoot)) {
243
- logMessage("system", "{white-fg}✓{/white-fg} Daemon stopped");
248
+ statusMsg("{gray-fg}✓{/gray-fg} Daemon stopped");
244
249
  } else {
245
- logMessage("error", "{white-fg}✗{/white-fg} Failed to stop daemon");
250
+ statusMsg("{gray-fg}✗{/gray-fg} Failed to stop daemon");
246
251
  }
247
252
  return;
248
253
  }
249
254
 
250
255
  if (subcommand === "restart") {
251
- logMessage("system", "{white-fg}⚙{/white-fg} Restarting daemon...");
256
+ statusMsg("{gray-fg}⚙{/gray-fg} Restarting daemon...");
252
257
  stopDaemon(targetRoot);
253
258
  await sleep(500);
254
259
  startDaemon(targetRoot);
255
260
  await sleep(1000);
256
261
  if (isDaemonRunning(targetRoot)) {
257
- logMessage("system", "{white-fg}✓{/white-fg} Daemon restarted");
262
+ statusMsg("{gray-fg}✓{/gray-fg} Daemon restarted");
258
263
  } else {
259
- logMessage("error", "{white-fg}✗{/white-fg} Failed to restart daemon");
264
+ statusMsg("{gray-fg}✗{/gray-fg} Failed to restart daemon");
260
265
  }
261
266
  return;
262
267
  }
263
268
 
264
269
  if (subcommand === "status") {
265
270
  if (isDaemonRunning(targetRoot)) {
266
- logMessage("system", "{white-fg}✓{/white-fg} Daemon is running");
271
+ statusMsg("{gray-fg}✓{/gray-fg} Daemon is running");
267
272
  } else {
268
- logMessage("system", "{white-fg}✗{/white-fg} Daemon is not running");
273
+ statusMsg("{gray-fg}✗{/gray-fg} Daemon is not running");
269
274
  }
270
275
  return;
271
276
  }
@@ -37,6 +37,14 @@ function createDaemonMessageRouter(options = {}) {
37
37
  return text.includes(":") && !text.includes(" ");
38
38
  }
39
39
 
40
+ function speakerPrefix(label, color = "cyan") {
41
+ const escapedLabel = escapeBlessed(label);
42
+ if (color === "white") {
43
+ return `{white-fg}${escapedLabel}{/white-fg} {gray-fg}·{/gray-fg} `;
44
+ }
45
+ return `{cyan-fg}${escapedLabel}{/cyan-fg} {gray-fg}·{/gray-fg} `;
46
+ }
47
+
40
48
  function normalizeDisplayMessage(raw) {
41
49
  let displayMessage = raw || "";
42
50
  let streamPayload = null;
@@ -77,9 +85,9 @@ function createDaemonMessageRouter(options = {}) {
77
85
  resolveBusStatus(item);
78
86
  if (text) {
79
87
  const prefix = data.phase === BUS_STATUS_PHASES.ERROR
80
- ? "{white-fg}✗{/white-fg}"
81
- : "{white-fg}✓{/white-fg}";
82
- logMessage("status", `${prefix} ${escapeBlessed(text)}`, data);
88
+ ? "{gray-fg}✗{/gray-fg}"
89
+ : "{gray-fg}✓{/gray-fg}";
90
+ resolveStatusLine(`${prefix} ${escapeBlessed(text)}`, data);
83
91
  }
84
92
  } else {
85
93
  enqueueBusStatus(item);
@@ -214,7 +222,7 @@ function createDaemonMessageRouter(options = {}) {
214
222
  );
215
223
  // Suppress lifecycle confirmations from chat history — status line plus structured payload is enough.
216
224
  if (!isLifecycleStatusOnly && !isGroupStartedConfirmation) {
217
- logMessage("reply", `{white-fg}←{/white-fg} ${escapeBlessed(replyText)}`);
225
+ logMessage("reply", `${speakerPrefix("ufoo", "white")}${escapeBlessed(replyText)}`);
218
226
  }
219
227
  }
220
228
 
@@ -352,15 +360,11 @@ function createDaemonMessageRouter(options = {}) {
352
360
  const publisher = report.agent_id || data.publisher || "ufoo-agent";
353
361
  const displayName = resolveAgentDisplayName(publisher);
354
362
  const detail = report.summary || report.message || data.message || report.task_id || "report";
355
- logMessage(
356
- "system",
357
- `{gray-fg}↥{/gray-fg} {cyan-fg}${escapeBlessed(displayName)}{/cyan-fg} {gray-fg}→ ufoo-agent{/gray-fg} ${escapeBlessed(detail)}`
358
- );
363
+ logMessage("bus", `${speakerPrefix(displayName)}${escapeBlessed(detail)}`);
359
364
  requestStatus();
360
365
  renderScreen();
361
366
  return true;
362
367
  }
363
- const prefix = data.event === "broadcast" ? "{gray-fg}⇢{/gray-fg}" : "{gray-fg}↔{/gray-fg}";
364
368
  const publisher = data.publisher && data.publisher !== "unknown"
365
369
  ? data.publisher
366
370
  : (data.event === "broadcast" ? "broadcast" : "bus");
@@ -399,7 +403,7 @@ function createDaemonMessageRouter(options = {}) {
399
403
  }
400
404
 
401
405
  const pendingBeforeMessage = getPendingState(publisher, displayName);
402
- const prefixLabel = `${prefix} {gray-fg}${escapeBlessed(displayName)}{/gray-fg}: `;
406
+ const prefixLabel = speakerPrefix(displayName);
403
407
  const continuationPrefix = " ".repeat(stripBlessedTags(prefixLabel).length);
404
408
 
405
409
  if (streamPayload) {
package/src/chat/index.js CHANGED
@@ -1905,6 +1905,7 @@ async function runChat(projectRoot, options = {}) {
1905
1905
  renderDashboard,
1906
1906
  renderScreen: () => screen.render(),
1907
1907
  logMessage,
1908
+ resolveStatusLine,
1908
1909
  escapeBlessed,
1909
1910
  });
1910
1911
 
@@ -1919,6 +1920,7 @@ async function runChat(projectRoot, options = {}) {
1919
1920
  parseCommand,
1920
1921
  escapeBlessed,
1921
1922
  logMessage,
1923
+ resolveStatusLine,
1922
1924
  renderScreen: () => screen.render(),
1923
1925
  getActiveAgents: () => activeAgents,
1924
1926
  getActiveAgentMetaMap: () => activeAgentMetaMap,
@@ -76,35 +76,30 @@ function getWrapWidth(input, fallbackWidth) {
76
76
 
77
77
  /**
78
78
  * Simulate blessed's wrapping for a single logical line (no newlines).
79
- * Returns { rows, lastCol }.
79
+ * Returns the zero-based visual row and column at the end of the line.
80
80
  *
81
- * Blessed preprocesses double-width chars by inserting a \x03 marker after
82
- * each one, then wraps at character count = width. This means a double-width
83
- * char at col (width - 1) overflows by 1 cell — only the \x03 marker wraps
84
- * to the next line. We replicate this behavior so cursor math matches what
85
- * blessed actually renders.
81
+ * A cursor at exactly width cells is still at the end of the current visual
82
+ * row; it should not become a phantom row until another character or an
83
+ * explicit newline is processed.
86
84
  */
87
- function wrapLine(line, width, strWidth) {
85
+ function measureLineEnd(line, width, strWidth) {
88
86
  const chars = Array.from(String(line || ""));
87
+ let row = 0;
89
88
  let col = 0;
90
- let rows = 1;
91
89
  for (const ch of chars) {
92
90
  const w = safeStrWidth(strWidth, ch);
93
91
  if (w === 0) continue;
94
- if (col >= width) {
95
- rows += 1;
96
- col = col - width;
92
+ if (col > 0 && col + w > width) {
93
+ row += 1;
94
+ col = 0;
97
95
  }
98
96
  col += w;
99
97
  }
100
- if (col > width) {
101
- rows += 1;
102
- col = col - width;
103
- } else if (col === width) {
104
- rows += 1;
105
- col = 0;
106
- }
107
- return { rows, lastCol: col };
98
+ return { row, col };
99
+ }
100
+
101
+ function countLineRows(line, width, strWidth) {
102
+ return measureLineEnd(line, width, strWidth).row + 1;
108
103
  }
109
104
 
110
105
  function countLines(text, width, strWidth) {
@@ -114,8 +109,7 @@ function countLines(text, width, strWidth) {
114
109
  const lines = expanded.split("\n");
115
110
  let total = 0;
116
111
  for (const line of lines) {
117
- const { rows, lastCol } = wrapLine(line, width, strWidth);
118
- total += (lastCol === 0 && rows > 1) ? rows - 1 : rows;
112
+ total += countLineRows(line, width, strWidth);
119
113
  }
120
114
  return total;
121
115
  }
@@ -136,28 +130,10 @@ function getCursorRowCol(text, pos, width, strWidth) {
136
130
  for (let i = 0; i < lines.length; i += 1) {
137
131
  const line = lines[i] || "";
138
132
  if (i < lines.length - 1) {
139
- const { rows, lastCol } = wrapLine(line, width, strWidth);
140
- row += (lastCol === 0 && rows > 1) ? rows - 1 : rows;
133
+ row += countLineRows(line, width, strWidth);
141
134
  } else {
142
- const chars = Array.from(line);
143
- let col = 0;
144
- for (const ch of chars) {
145
- const w = safeStrWidth(strWidth, ch);
146
- if (w === 0) continue;
147
- if (col >= width) {
148
- row += 1;
149
- col = col - width;
150
- }
151
- col += w;
152
- }
153
- if (col > width) {
154
- row += 1;
155
- col = col - width;
156
- } else if (col === width) {
157
- row += 1;
158
- col = 0;
159
- }
160
- return { row, col };
135
+ const measured = measureLineEnd(line, width, strWidth);
136
+ return { row: row + measured.row, col: measured.col };
161
137
  }
162
138
  }
163
139
  return { row, col: 0 };
@@ -187,27 +163,20 @@ function getCursorPosForRowCol(text, targetRow, targetCol, width, strWidth) {
187
163
  lineOffset += ch.length;
188
164
  continue;
189
165
  }
190
- if (col >= width) {
166
+ if (col > 0 && col + w > width) {
167
+ if (row === targetRow && targetCol >= col) {
168
+ return expandedToOriginal(original, expPos + lineOffset);
169
+ }
191
170
  row += 1;
192
- col = col - width;
171
+ col = 0;
193
172
  }
194
173
  if (row === targetRow && col + w > targetCol) {
195
174
  return expandedToOriginal(original, expPos + lineOffset);
196
175
  }
197
176
  col += w;
198
177
  lineOffset += ch.length;
199
- if (col > width) {
200
- if (row === targetRow) {
201
- return expandedToOriginal(original, expPos + lineOffset);
202
- }
203
- row += 1;
204
- col = col - width;
205
- } else if (col === width) {
206
- if (row === targetRow) {
207
- return expandedToOriginal(original, expPos + lineOffset);
208
- }
209
- row += 1;
210
- col = 0;
178
+ if (row === targetRow && col === targetCol) {
179
+ return expandedToOriginal(original, expPos + lineOffset);
211
180
  }
212
181
  }
213
182
 
@@ -30,6 +30,12 @@ function createInputSubmitHandler(options = {}) {
30
30
  throw new Error("createInputSubmitHandler requires a mutable state object");
31
31
  }
32
32
 
33
+ function userEcho(text, targetLabel = "") {
34
+ const body = escapeBlessed(text);
35
+ if (!targetLabel) return body;
36
+ return `{magenta-fg}@${escapeBlessed(targetLabel)}{/magenta-fg} ${body}`;
37
+ }
38
+
33
39
  async function tryActivateTargetAgent(agentId) {
34
40
  const adapter = getAgentAdapter(agentId);
35
41
  const capabilities = adapter && adapter.capabilities ? adapter.capabilities : null;
@@ -88,7 +94,7 @@ function createInputSubmitHandler(options = {}) {
88
94
  const label = getAgentLabel(state.targetAgent);
89
95
  logMessage(
90
96
  "user",
91
- `{cyan-fg}→{/cyan-fg} {magenta-fg}@${escapeBlessed(label)}{/magenta-fg} ${escapeBlessed(text)}`
97
+ userEcho(text, label)
92
98
  );
93
99
  renderScreen(); // Immediately render the user message
94
100
  markPendingDelivery(state.targetAgent);
@@ -114,10 +120,7 @@ function createInputSubmitHandler(options = {}) {
114
120
  return;
115
121
  }
116
122
  setTargetAgent(resolvedTarget);
117
- logMessage(
118
- "status",
119
- `{white-fg}⚙{/white-fg} Target selected: @${escapeBlessed(atTarget.target)}`
120
- );
123
+ queueStatusLine(`Target selected: @${escapeBlessed(atTarget.target)}`);
121
124
  focusInput();
122
125
  return;
123
126
  }
@@ -125,7 +128,7 @@ function createInputSubmitHandler(options = {}) {
125
128
  const message = atTarget.message.trim();
126
129
  logMessage(
127
130
  "user",
128
- `{cyan-fg}→{/cyan-fg} {magenta-fg}@${escapeBlessed(atTarget.target)}{/magenta-fg} ${escapeBlessed(message)}`
131
+ userEcho(message, atTarget.target)
129
132
  );
130
133
  renderScreen(); // Immediately render the user message
131
134
  markPendingDelivery(resolvedTarget);
@@ -142,7 +145,7 @@ function createInputSubmitHandler(options = {}) {
142
145
 
143
146
  if (text.startsWith("/")) {
144
147
  if (shouldEchoCommandInChat(text)) {
145
- logMessage("user", `{white-fg}→{/white-fg} ${escapeBlessed(text)}`);
148
+ logMessage("user", userEcho(text));
146
149
  renderScreen(); // Render slash command immediately
147
150
  }
148
151
  try {
@@ -188,7 +191,7 @@ function createInputSubmitHandler(options = {}) {
188
191
  allow_relevance_queue: true,
189
192
  },
190
193
  });
191
- logMessage("user", `{white-fg}→{/white-fg} ${escapeBlessed(text)}`);
194
+ logMessage("user", userEcho(text));
192
195
  renderScreen(); // Render plain text message immediately
193
196
  }
194
197
 
@@ -23,10 +23,14 @@ function createProjectCloseController(options = {}) {
23
23
  renderDashboard = () => {},
24
24
  renderScreen = () => {},
25
25
  logMessage = () => {},
26
+ resolveStatusLine = null,
26
27
  escapeBlessed = (value) => String(value || ""),
27
28
  } = options;
28
29
 
29
30
  let closingProject = false;
31
+ const statusMsg = typeof resolveStatusLine === "function"
32
+ ? resolveStatusLine
33
+ : (text) => logMessage("status", text);
30
34
 
31
35
  function pickFallbackProjectRoot(targetProjectRoot) {
32
36
  const rows = Array.isArray(getProjects()) ? getProjects() : [];
@@ -61,7 +65,7 @@ function createProjectCloseController(options = {}) {
61
65
 
62
66
  closingProject = true;
63
67
  try {
64
- logMessage("status", `{white-fg}⚙{/white-fg} Closing project ${escapedName} daemon and agents...`);
68
+ statusMsg(`{gray-fg}⚙{/gray-fg} Closing project ${escapedName} daemon and agents...`);
65
69
 
66
70
  let switchedTo = "";
67
71
  if (activeProjectRoot === projectRoot) {
@@ -89,9 +93,9 @@ function createProjectCloseController(options = {}) {
89
93
  renderScreen();
90
94
 
91
95
  if (wasRunning) {
92
- logMessage("status", `{white-fg}✓{/white-fg} Closed project ${escapedName} daemon and agents`);
96
+ statusMsg(`{gray-fg}✓{/gray-fg} Closed project ${escapedName} daemon and agents`);
93
97
  } else {
94
- logMessage("status", `{white-fg}✓{/white-fg} Project ${escapedName} daemon already stopped`);
98
+ statusMsg(`{gray-fg}✓{/gray-fg} Project ${escapedName} daemon already stopped`);
95
99
  }
96
100
 
97
101
  return {
@@ -75,7 +75,8 @@ function createSettingsController(options = {}) {
75
75
  setSelectedProviderIndex(Math.max(0, providerOptions.findIndex((opt) => opt.value === next)));
76
76
  saveConfig(projectRoot, { agentProvider: next });
77
77
  clearUfooAgentIdentity();
78
- logMessage("status", `{white-fg}⚙{/white-fg} ufoo-agent: ${providerLabel(next)}`);
78
+ const statusMsg = resolveStatusLine || ((text) => logMessage("status", text));
79
+ statusMsg(`{gray-fg}⚙{/gray-fg} ufoo-agent: ${providerLabel(next)}`);
79
80
  renderDashboard();
80
81
  renderScreen();
81
82
  void restartDaemon();
@@ -89,7 +90,8 @@ function createSettingsController(options = {}) {
89
90
  setSelectedResumeIndex(next ? 0 : 1);
90
91
  saveConfig(projectRoot, { autoResume: next });
91
92
  const label = next ? "Resume previous session" : "Start new session";
92
- logMessage("status", `{white-fg}⚙{/white-fg} Resume mode: ${label}`);
93
+ const statusMsg = resolveStatusLine || ((text) => logMessage("status", text));
94
+ statusMsg(`{gray-fg}⚙{/gray-fg} Resume mode: ${label}`);
93
95
  renderDashboard();
94
96
  renderScreen();
95
97
  return true;
@@ -0,0 +1,206 @@
1
+ "use strict";
2
+
3
+ function asTrimmedString(value) {
4
+ return typeof value === "string" ? value.trim() : "";
5
+ }
6
+
7
+ function normalizeNicknameSegment(value = "", fallback = "task") {
8
+ const normalized = asTrimmedString(value)
9
+ .toLowerCase()
10
+ .replace(/[^a-z0-9_-]+/g, "-")
11
+ .replace(/-+/g, "-")
12
+ .replace(/^-+|-+$/g, "");
13
+ return normalized || fallback;
14
+ }
15
+
16
+ function stripRoutingPromptMetadata(prompt = "") {
17
+ let text = String(prompt || "");
18
+ const markers = [
19
+ "\nRouting request metadata (JSON):",
20
+ "\nPrivate runtime reports for ufoo-agent (JSON):",
21
+ "\nController loop state (JSON):",
22
+ "\nController tool results so far (JSON):",
23
+ ];
24
+ for (const marker of markers) {
25
+ const index = text.indexOf(marker);
26
+ if (index >= 0) {
27
+ text = text.slice(0, index);
28
+ }
29
+ }
30
+ return text.trim();
31
+ }
32
+
33
+ function normalizeLaunchAgentForNickname(agent = "") {
34
+ const raw = asTrimmedString(agent).toLowerCase();
35
+ if (raw === "claude" || raw === "claude-code" || raw === "uclaude") return "claude";
36
+ if (raw === "codex" || raw === "ucodex") return "codex";
37
+ if (raw === "ufoo" || raw === "ucode" || raw === "ufoo-code") return "ucode";
38
+ return "";
39
+ }
40
+
41
+ function nicknameCapturePattern() {
42
+ return "(?:`([^`]{1,32})`|[\"'“”]([^\"'“”]{1,32})[\"'“”]?|([A-Za-z0-9_-]{1,32}))";
43
+ }
44
+
45
+ function pickNicknameCapture(match) {
46
+ if (!match) return "";
47
+ for (let i = 1; i < match.length; i += 1) {
48
+ const value = asTrimmedString(match[i]);
49
+ if (value) return normalizeNicknameSegment(value, "");
50
+ }
51
+ return "";
52
+ }
53
+
54
+ function extractRequestedLaunchNickname(prompt = "") {
55
+ const text = stripRoutingPromptMetadata(prompt);
56
+ if (!text) return "";
57
+ const value = nicknameCapturePattern();
58
+ const patterns = [
59
+ new RegExp(`(?:launch|start|create|spawn|open|new|启动|新建|创建|拉起)[\\s\\S]{0,80}(?:named|name|nickname|叫做|叫|名为|昵称|取名|为)\\s*${value}`, "i"),
60
+ new RegExp(`(?:named|name|nickname|叫做|叫|名为|昵称|取名)\\s*${value}[\\s\\S]{0,80}(?:agent|worker|codex|claude|ucode|ufoo|代理|智能体)`, "i"),
61
+ ];
62
+ for (const re of patterns) {
63
+ const nickname = pickNicknameCapture(text.match(re));
64
+ if (nickname) return nickname;
65
+ }
66
+ return "";
67
+ }
68
+
69
+ function collectKeywordTokens(text = "") {
70
+ const specs = [
71
+ [/处理|修复|解决|排查/g, "fix"],
72
+ [/路由|主路由/g, "route"],
73
+ [/启动|新启动|拉起/g, "launch"],
74
+ [/投递|派发|发送|分发/g, "dispatch"],
75
+ [/任务|工作/g, "task"],
76
+ [/测试|验证/g, "test"],
77
+ [/前端|界面/g, "frontend"],
78
+ [/后端|服务端/g, "backend"],
79
+ [/设计/g, "design"],
80
+ [/审查|评审/g, "review"],
81
+ [/调试/g, "debug"],
82
+ [/重构/g, "refactor"],
83
+ [/发布/g, "release"],
84
+ [/文档/g, "docs"],
85
+ [/性能/g, "perf"],
86
+ ];
87
+ const matches = [];
88
+ for (const [re, token] of specs) {
89
+ re.lastIndex = 0;
90
+ let match = re.exec(text);
91
+ while (match) {
92
+ matches.push({ index: match.index, token });
93
+ match = re.exec(text);
94
+ }
95
+ }
96
+ return matches
97
+ .sort((a, b) => a.index - b.index)
98
+ .map((item) => item.token);
99
+ }
100
+
101
+ const ENGLISH_STOPWORDS = new Set([
102
+ "the", "a", "an", "and", "or", "to", "for", "with", "from", "this", "that",
103
+ "please", "pls", "ufoo", "chat", "agent", "agents", "worker", "workers",
104
+ "nickname", "nick", "name", "named", "new", "current", "online", "source",
105
+ "metadata", "json", "request", "user", "feedback", "bug", "issue",
106
+ ]);
107
+
108
+ function collectEnglishTokens(text = "") {
109
+ const rawTokens = String(text || "").toLowerCase().match(/[a-z][a-z0-9_-]{1,24}/g) || [];
110
+ return rawTokens
111
+ .map((token) => normalizeNicknameSegment(token, ""))
112
+ .filter((token) => token && !ENGLISH_STOPWORDS.has(token));
113
+ }
114
+
115
+ function uniqueOrdered(values = []) {
116
+ const seen = new Set();
117
+ const result = [];
118
+ for (const value of values) {
119
+ const token = normalizeNicknameSegment(value, "");
120
+ if (!token || seen.has(token)) continue;
121
+ seen.add(token);
122
+ result.push(token);
123
+ }
124
+ return result;
125
+ }
126
+
127
+ function toExistingNicknameSet(existingNicknames = []) {
128
+ if (existingNicknames instanceof Set) return new Set(existingNicknames);
129
+ if (Array.isArray(existingNicknames)) return new Set(existingNicknames.map((item) => normalizeNicknameSegment(item, "")));
130
+ if (existingNicknames && typeof existingNicknames === "object") {
131
+ return new Set(Object.keys(existingNicknames).map((item) => normalizeNicknameSegment(item, "")));
132
+ }
133
+ return new Set();
134
+ }
135
+
136
+ function truncateNickname(value = "", maxLength = 32) {
137
+ const normalized = normalizeNicknameSegment(value, "task");
138
+ if (normalized.length <= maxLength) return normalized;
139
+ return normalized.slice(0, maxLength).replace(/-+$/g, "") || "task";
140
+ }
141
+
142
+ function makeUniqueNickname(base, existingNicknames = []) {
143
+ const existing = toExistingNicknameSet(existingNicknames);
144
+ const normalizedBase = truncateNickname(base);
145
+ if (!existing.has(normalizedBase)) return normalizedBase;
146
+ for (let i = 2; i < 100; i += 1) {
147
+ const suffix = `-${i}`;
148
+ const candidate = `${truncateNickname(normalizedBase, 32 - suffix.length)}${suffix}`;
149
+ if (!existing.has(candidate)) return candidate;
150
+ }
151
+ return `${truncateNickname(normalizedBase, 27)}-${Date.now().toString(36).slice(-4)}`;
152
+ }
153
+
154
+ function collectExistingNicknames(context = {}) {
155
+ const values = [];
156
+ if (context && typeof context === "object") {
157
+ if (context.nicknames && typeof context.nicknames === "object") {
158
+ values.push(...Object.keys(context.nicknames));
159
+ }
160
+ if (Array.isArray(context.agents)) {
161
+ for (const agent of context.agents) {
162
+ if (agent && agent.nickname) values.push(agent.nickname);
163
+ }
164
+ }
165
+ }
166
+ return values;
167
+ }
168
+
169
+ function buildLaunchNickname(prompt = "", agent = "", existingNicknames = []) {
170
+ const explicit = extractRequestedLaunchNickname(prompt);
171
+ if (explicit) return makeUniqueNickname(explicit, existingNicknames);
172
+
173
+ const corePrompt = stripRoutingPromptMetadata(prompt);
174
+ const agentPrefix = normalizeLaunchAgentForNickname(agent) || "agent";
175
+ const tokens = uniqueOrdered([
176
+ ...collectKeywordTokens(corePrompt),
177
+ ...collectEnglishTokens(corePrompt),
178
+ ]).filter((token) => token !== agentPrefix);
179
+ const stem = tokens.slice(0, 2).join("-") || "task";
180
+ return makeUniqueNickname(`${agentPrefix}-${stem}`, existingNicknames);
181
+ }
182
+
183
+ function assignMissingLaunchNicknames(payload = {}, options = {}) {
184
+ if (!payload || typeof payload !== "object" || !Array.isArray(payload.ops)) return payload;
185
+ const existing = new Set(collectExistingNicknames(options.context));
186
+ let changed = false;
187
+ const ops = payload.ops.map((op) => {
188
+ if (!op || op.action !== "launch" || op.nickname) return op;
189
+ const count = Number.parseInt(op.count || 1, 10);
190
+ if (Number.isFinite(count) && count > 1) return op;
191
+ const nickname = buildLaunchNickname(options.prompt || "", op.agent || "", existing);
192
+ existing.add(nickname);
193
+ changed = true;
194
+ return { ...op, nickname };
195
+ });
196
+ return changed ? { ...payload, ops } : payload;
197
+ }
198
+
199
+ module.exports = {
200
+ assignMissingLaunchNicknames,
201
+ buildLaunchNickname,
202
+ collectExistingNicknames,
203
+ extractRequestedLaunchNickname,
204
+ normalizeLaunchAgentForNickname,
205
+ stripRoutingPromptMetadata,
206
+ };
@@ -0,0 +1,222 @@
1
+ "use strict";
2
+
3
+ const {
4
+ normalizeLaunchAgentForNickname,
5
+ stripRoutingPromptMetadata,
6
+ } = require("./launchRouting");
7
+
8
+ const LAUNCH_TARGET_PREFIX = "__ufoo_launch_";
9
+
10
+ function normalizePayload(payload) {
11
+ if (!payload || typeof payload !== "object") {
12
+ return { reply: "", dispatch: [], ops: [] };
13
+ }
14
+ return {
15
+ ...payload,
16
+ reply: typeof payload.reply === "string" ? payload.reply : "",
17
+ dispatch: Array.isArray(payload.dispatch) ? payload.dispatch : [],
18
+ ops: Array.isArray(payload.ops) ? payload.ops : [],
19
+ };
20
+ }
21
+
22
+ function isLaunchOp(op) {
23
+ return Boolean(op && op.action === "launch");
24
+ }
25
+
26
+ function hasDispatchMessage(item) {
27
+ return Boolean(item && String(item.target || "").trim() && String(item.message || "").trim());
28
+ }
29
+
30
+ function launchPlaceholder(index) {
31
+ return `${LAUNCH_TARGET_PREFIX}${index}`;
32
+ }
33
+
34
+ function parseLaunchPlaceholder(target = "") {
35
+ const raw = String(target || "").trim();
36
+ if (!raw.startsWith(LAUNCH_TARGET_PREFIX)) return -1;
37
+ const parsed = Number.parseInt(raw.slice(LAUNCH_TARGET_PREFIX.length), 10);
38
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : -1;
39
+ }
40
+
41
+ function isLaunchDispatchTarget(target = "", op = {}) {
42
+ const raw = String(target || "").trim();
43
+ if (!raw || raw.includes(":")) return false;
44
+ const normalized = raw.toLowerCase();
45
+ const nickname = String(op.nickname || "").trim();
46
+ if (nickname && raw === nickname) return true;
47
+ const placeholders = new Set([
48
+ "new",
49
+ "new-agent",
50
+ "new-worker",
51
+ "worker",
52
+ "agent",
53
+ "launched",
54
+ "launched-agent",
55
+ "launched-worker",
56
+ ]);
57
+ if (placeholders.has(normalized)) return true;
58
+ const launchAgent = normalizeLaunchAgentForNickname(op.agent || "");
59
+ return Boolean(launchAgent && normalizeLaunchAgentForNickname(raw) === launchAgent);
60
+ }
61
+
62
+ function resolveLaunchResultTarget(op = {}, result = {}) {
63
+ if (result && result.agent_id) return String(result.agent_id);
64
+ if (result && Array.isArray(result.subscriber_ids) && result.subscriber_ids[0]) {
65
+ return String(result.subscriber_ids[0]);
66
+ }
67
+ if (result && result.subscriber_id) return String(result.subscriber_id);
68
+ if (result && result.nickname) return String(result.nickname);
69
+ if (op && op.nickname) return String(op.nickname);
70
+ return "";
71
+ }
72
+
73
+ function resolveDispatchLaunchIndex(dispatchItem = {}, launchOps = []) {
74
+ const target = String(dispatchItem.target || "").trim();
75
+ const placeholderIndex = parseLaunchPlaceholder(target);
76
+ if (placeholderIndex >= 0) return placeholderIndex;
77
+
78
+ for (let i = 0; i < launchOps.length; i += 1) {
79
+ if (isLaunchDispatchTarget(target, launchOps[i])) return i;
80
+ }
81
+ return -1;
82
+ }
83
+
84
+ function prepareLaunchDispatchPayload(payload, prompt = "") {
85
+ const normalized = normalizePayload(payload);
86
+ const launchOps = [];
87
+ const otherOps = [];
88
+ for (const op of normalized.ops) {
89
+ if (isLaunchOp(op)) launchOps.push(op);
90
+ else if (op) otherOps.push(op);
91
+ }
92
+
93
+ if (launchOps.length === 0) {
94
+ return {
95
+ payload: normalized,
96
+ launchOps,
97
+ otherOps,
98
+ dispatch: normalized.dispatch.filter(hasDispatchMessage),
99
+ };
100
+ }
101
+
102
+ let dispatch = normalized.dispatch.filter(hasDispatchMessage).map((item) => ({ ...item }));
103
+ const taskMessage = stripRoutingPromptMetadata(prompt);
104
+ if (dispatch.length === 0 && !taskMessage) {
105
+ return {
106
+ payload: { ...normalized, dispatch: [], ops: otherOps },
107
+ launchOps: [],
108
+ otherOps,
109
+ dispatch: [],
110
+ droppedLaunches: launchOps,
111
+ };
112
+ }
113
+
114
+ if (dispatch.length === 0 && taskMessage) {
115
+ dispatch = launchOps.map((op, index) => ({
116
+ target: op.nickname || launchPlaceholder(index),
117
+ message: taskMessage,
118
+ injection_mode: "immediate",
119
+ source: "ufoo-agent",
120
+ }));
121
+ } else if (launchOps.length === 1) {
122
+ const fallbackTarget = launchOps[0].nickname || launchPlaceholder(0);
123
+ dispatch = dispatch.map((item) => (
124
+ isLaunchDispatchTarget(item.target, launchOps[0])
125
+ ? { ...item, target: fallbackTarget }
126
+ : item
127
+ ));
128
+ }
129
+
130
+ return {
131
+ payload: { ...normalized, dispatch, ops: [...launchOps, ...otherOps] },
132
+ launchOps,
133
+ otherOps,
134
+ dispatch,
135
+ };
136
+ }
137
+
138
+ function bindDispatchToLaunchResults(dispatch = [], launchOps = [], launchResults = []) {
139
+ const resultByIndex = launchResults.filter((item) => item && item.action === "launch");
140
+ return dispatch.map((item) => {
141
+ const index = resolveDispatchLaunchIndex(item, launchOps);
142
+ if (index < 0) return item;
143
+ const target = resolveLaunchResultTarget(launchOps[index], resultByIndex[index] || {});
144
+ return target ? { ...item, target } : item;
145
+ });
146
+ }
147
+
148
+ async function finalizeRouterPayload({
149
+ projectRoot,
150
+ payload,
151
+ prompt = "",
152
+ processManager,
153
+ dispatchMessages,
154
+ handleOps,
155
+ markPending = () => {},
156
+ finalizeLocally = true,
157
+ }) {
158
+ const prepared = prepareLaunchDispatchPayload(payload, prompt);
159
+ if (finalizeLocally === false) {
160
+ return {
161
+ ok: true,
162
+ payload: prepared.payload,
163
+ opsResults: [],
164
+ };
165
+ }
166
+
167
+ if (prepared.launchOps.length === 0) {
168
+ for (const item of prepared.dispatch) {
169
+ if (item && item.target && item.target !== "broadcast") {
170
+ markPending(item.target);
171
+ }
172
+ }
173
+ if (typeof dispatchMessages === "function") {
174
+ await dispatchMessages(projectRoot, prepared.dispatch);
175
+ }
176
+ const opsResults = typeof handleOps === "function" && prepared.otherOps.length > 0
177
+ ? await handleOps(projectRoot, prepared.otherOps, processManager)
178
+ : [];
179
+ return {
180
+ ok: true,
181
+ payload: { ...prepared.payload, dispatch: prepared.dispatch, ops: prepared.otherOps },
182
+ opsResults,
183
+ };
184
+ }
185
+
186
+ const launchResults = typeof handleOps === "function"
187
+ ? await handleOps(projectRoot, prepared.launchOps, processManager)
188
+ : [];
189
+ const boundDispatch = bindDispatchToLaunchResults(prepared.dispatch, prepared.launchOps, launchResults);
190
+
191
+ for (const item of boundDispatch) {
192
+ if (item && item.target && item.target !== "broadcast") {
193
+ markPending(item.target);
194
+ }
195
+ }
196
+ if (typeof dispatchMessages === "function") {
197
+ await dispatchMessages(projectRoot, boundDispatch);
198
+ }
199
+
200
+ const otherResults = typeof handleOps === "function" && prepared.otherOps.length > 0
201
+ ? await handleOps(projectRoot, prepared.otherOps, processManager)
202
+ : [];
203
+
204
+ return {
205
+ ok: true,
206
+ payload: { ...prepared.payload, dispatch: boundDispatch },
207
+ opsResults: [...launchResults, ...otherResults],
208
+ };
209
+ }
210
+
211
+ module.exports = {
212
+ bindDispatchToLaunchResults,
213
+ finalizeRouterPayload,
214
+ normalizePayload,
215
+ prepareLaunchDispatchPayload,
216
+ __private: {
217
+ isLaunchDispatchTarget,
218
+ launchPlaceholder,
219
+ resolveLaunchResultTarget,
220
+ stripRoutingPromptMetadata,
221
+ },
222
+ };
@@ -1,3 +1,5 @@
1
+ const { finalizeRouterPayload } = require("../controller/routerFinalize");
2
+
1
3
  function normalizePayload(payload) {
2
4
  if (!payload || typeof payload !== "object") {
3
5
  return { reply: "", dispatch: [], ops: [] };
@@ -36,34 +38,23 @@ function stripAssistantCall(payload) {
36
38
  async function finalizePromptRun({
37
39
  projectRoot,
38
40
  payload,
41
+ prompt,
39
42
  processManager,
40
43
  dispatchMessages,
41
44
  handleOps,
42
45
  markPending,
43
46
  finalizeLocally = true,
44
47
  }) {
45
- if (finalizeLocally === false) {
46
- return {
47
- ok: true,
48
- payload,
49
- opsResults: [],
50
- };
51
- }
52
-
53
- for (const item of payload.dispatch || []) {
54
- if (item && item.target && item.target !== "broadcast") {
55
- markPending(item.target);
56
- }
57
- }
58
-
59
- await dispatchMessages(projectRoot, payload.dispatch || []);
60
- const opsResults = await handleOps(projectRoot, payload.ops || [], processManager);
61
-
62
- return {
63
- ok: true,
48
+ return finalizeRouterPayload({
49
+ projectRoot,
64
50
  payload,
65
- opsResults,
66
- };
51
+ prompt,
52
+ processManager,
53
+ dispatchMessages,
54
+ handleOps,
55
+ markPending,
56
+ finalizeLocally,
57
+ });
67
58
  }
68
59
 
69
60
  async function runPromptWithAssistant({
@@ -129,6 +120,7 @@ async function runPromptWithAssistant({
129
120
  return finalizePromptRun({
130
121
  projectRoot,
131
122
  payload: firstPayload,
123
+ prompt: prompt || "",
132
124
  processManager,
133
125
  dispatchMessages,
134
126
  handleOps,