u-foo 1.8.5 → 1.8.8

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 (36) hide show
  1. package/package.json +1 -1
  2. package/src/agent/activityDetector.js +33 -0
  3. package/src/agent/claudeSessionFiles.js +127 -0
  4. package/src/agent/launcher.js +13 -2
  5. package/src/bus/subscriber.js +16 -3
  6. package/src/chat/commandExecutor.js +0 -1
  7. package/src/chat/commands.js +10 -2
  8. package/src/chat/daemonCoordinator.js +1 -0
  9. package/src/chat/daemonMessageRouter.js +27 -16
  10. package/src/chat/daemonReconnect.js +6 -3
  11. package/src/chat/index.js +1 -0
  12. package/src/chat/inputMath.js +175 -38
  13. package/src/chat/inputSubmitHandler.js +10 -5
  14. package/src/chat/settingsController.js +3 -1
  15. package/src/chat/text.js +6 -0
  16. package/src/code/agent.js +27 -6
  17. package/src/code/nativeRunner.js +8 -4
  18. package/src/code/prompts/actions.js +21 -0
  19. package/src/code/prompts/efficiency.js +18 -0
  20. package/src/code/prompts/environment.js +50 -0
  21. package/src/code/prompts/identity.js +20 -0
  22. package/src/code/prompts/index.js +103 -0
  23. package/src/code/prompts/safety.js +11 -0
  24. package/src/code/prompts/sections.js +60 -0
  25. package/src/code/prompts/system.js +16 -0
  26. package/src/code/prompts/tasks.js +17 -0
  27. package/src/code/prompts/toolDescriptions/bash.js +21 -0
  28. package/src/code/prompts/toolDescriptions/edit.js +16 -0
  29. package/src/code/prompts/toolDescriptions/read.js +17 -0
  30. package/src/code/prompts/toolDescriptions/write.js +16 -0
  31. package/src/code/prompts/ufoo.js +21 -0
  32. package/src/daemon/groupOrchestrator.js +97 -7
  33. package/src/daemon/index.js +53 -14
  34. package/src/daemon/nicknameScope.js +80 -0
  35. package/src/daemon/ops.js +19 -6
  36. package/src/daemon/soloBootstrap.js +15 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "u-foo",
3
- "version": "1.8.5",
3
+ "version": "1.8.8",
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",
@@ -10,6 +10,8 @@
10
10
  * BLOCKED ----+
11
11
  */
12
12
 
13
+ const { isTranscriptActive } = require("./claudeSessionFiles");
14
+
13
15
  const ACTIVITY_STATES = {
14
16
  starting: "starting",
15
17
  ready: "ready",
@@ -24,6 +26,7 @@ const DEFAULT_TAIL_LINES = 10;
24
26
  const DEFAULT_BLOCKED_TIMEOUT_MS = 300000;
25
27
  const DEFAULT_INTERNAL_QUIET_MS = 3500;
26
28
  const DEFAULT_EXTERNAL_QUIET_MS = 5000;
29
+ const DEFAULT_TRANSCRIPT_THRESHOLD_MS = 5000;
27
30
  const ANSI_PATTERN = /\x1b\[[0-?]*[ -/]*[@-~]/g;
28
31
  const OSC_PATTERN = /\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g;
29
32
 
@@ -34,6 +37,10 @@ const INPUT_PATTERNS = {
34
37
  /\bAllow\b.*\bDeny\b/, // Claude Code permission dialog: "Allow | Deny"
35
38
  /\ballow mcp\b/i, // MCP tool approval prompt
36
39
  /Enter to select.*\u2191\/\u2193 to navigate/, // Ink TUI interactive prompt navigation bar (permissions, AskUserQuestion, Plan approval)
40
+ /\u276f\s+.*\n.*\u276f\s+/, // AskUserQuestion multi-option selector (❯ markers)
41
+ /Do you want to proceed/i, // Plan mode approval prompt
42
+ /\bApprove\b.*\bReject\b/, // Plan approval dialog
43
+ /\bYes\b.*\bNo\b.*\bAlways allow\b/, // Permission with "Always allow" option
37
44
  ],
38
45
  codex: [
39
46
  /\[Y\/n\]/, // Bracket-style prompt
@@ -79,6 +86,8 @@ class ActivityDetector {
79
86
  * @param {boolean} [options.startOnOutput=false] - allow STARTING -> WORKING on first output
80
87
  * @param {number} [options.quietWindowMs] - output quiet window before WAITING_INPUT/IDLE classification
81
88
  * @param {number} [options.blockedTimeoutMs=300000] - 5 min WAITING_INPUT → BLOCKED
89
+ * @param {number} [options.pid] - agent PID for transcript mtime checking (claude-code only)
90
+ * @param {number} [options.transcriptThresholdMs=5000] - transcript mtime threshold for busy detection
82
91
  */
83
92
  constructor(agentType, options = {}) {
84
93
  this.agentType = agentType;
@@ -87,6 +96,8 @@ class ActivityDetector {
87
96
  this.tailLines = toPositiveInt(options.tailLines, DEFAULT_TAIL_LINES);
88
97
  this.startOnOutput = options.startOnOutput === true;
89
98
  this.blockedTimeoutMs = toPositiveInt(options.blockedTimeoutMs, DEFAULT_BLOCKED_TIMEOUT_MS);
99
+ this.pid = toPositiveInt(options.pid, 0);
100
+ this.transcriptThresholdMs = toPositiveInt(options.transcriptThresholdMs, DEFAULT_TRANSCRIPT_THRESHOLD_MS);
90
101
  const optionQuietMs = toPositiveInt(options.quietWindowMs, 0);
91
102
  const envQuietMs = toPositiveInt(process.env.UFOO_ACTIVITY_QUIET_MS, 0);
92
103
  this.quietWindowMs = optionQuietMs || envQuietMs || getDefaultQuietWindowMs(this.mode);
@@ -126,6 +137,13 @@ class ActivityDetector {
126
137
  }
127
138
  }
128
139
 
140
+ /**
141
+ * Set the agent PID (for transcript mtime checking after spawn).
142
+ */
143
+ setPid(pid) {
144
+ this.pid = toPositiveInt(pid, 0);
145
+ }
146
+
129
147
  /**
130
148
  * STARTING → READY
131
149
  */
@@ -224,6 +242,21 @@ class ActivityDetector {
224
242
 
225
243
  _classifyAfterQuietWindow() {
226
244
  if (this.state !== ACTIVITY_STATES.working) return;
245
+
246
+ // Transcript mtime check: if Claude Code's transcript was recently written,
247
+ // the agent is still working (e.g. waiting for API response). Reschedule
248
+ // classification instead of prematurely going idle.
249
+ if (this.pid && this.agentType === "claude-code") {
250
+ try {
251
+ if (isTranscriptActive(this.pid, this.transcriptThresholdMs)) {
252
+ this._scheduleQuietClassification();
253
+ return;
254
+ }
255
+ } catch {
256
+ // ignore — fall through to normal classification
257
+ }
258
+ }
259
+
227
260
  const tailBuffer = this._tailWindow();
228
261
 
229
262
  // Check agent-specific patterns only after output has stabilized.
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Claude Code session file utilities.
5
+ *
6
+ * Reads Claude Code's PID files (~/.claude/sessions/{pid}.json) and
7
+ * transcript files (~/.claude/projects/{encoded-cwd}/{sessionId}.jsonl)
8
+ * to provide external status signals.
9
+ */
10
+
11
+ const fs = require("fs");
12
+ const path = require("path");
13
+ const os = require("os");
14
+
15
+ const CLAUDE_CONFIG_DIR = path.join(os.homedir(), ".claude");
16
+ const SESSIONS_DIR = path.join(CLAUDE_CONFIG_DIR, "sessions");
17
+ const PROJECTS_DIR = path.join(CLAUDE_CONFIG_DIR, "projects");
18
+
19
+ /**
20
+ * Encode a CWD path to Claude Code's project directory name.
21
+ * e.g. "/Users/icy/Code/ufoo" → "-Users-icy-Code-ufoo"
22
+ */
23
+ function encodeProjectPath(cwd) {
24
+ return String(cwd || "").replace(/\//g, "-");
25
+ }
26
+
27
+ /**
28
+ * Read a Claude Code PID file.
29
+ * Returns { pid, sessionId, cwd, startedAt, kind, entrypoint } or null.
30
+ */
31
+ function readClaudePidFile(pid) {
32
+ try {
33
+ const filePath = path.join(SESSIONS_DIR, `${pid}.json`);
34
+ const raw = fs.readFileSync(filePath, "utf8");
35
+ const data = JSON.parse(raw);
36
+ if (!data || typeof data !== "object") return null;
37
+ return data;
38
+ } catch {
39
+ return null;
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Find the transcript JSONL file for a Claude Code session.
45
+ * Returns the file path or "" if not found.
46
+ */
47
+ function findTranscriptFile(cwd, sessionId) {
48
+ if (!cwd || !sessionId) return "";
49
+ const projectDir = path.join(PROJECTS_DIR, encodeProjectPath(cwd));
50
+ const transcriptPath = path.join(projectDir, `${sessionId}.jsonl`);
51
+ try {
52
+ if (fs.existsSync(transcriptPath)) return transcriptPath;
53
+ } catch {
54
+ // ignore
55
+ }
56
+ return "";
57
+ }
58
+
59
+ /**
60
+ * Get the mtime (in ms) of a Claude Code transcript file.
61
+ * Returns 0 if the file doesn't exist or can't be read.
62
+ */
63
+ function getTranscriptMtimeMs(transcriptPath) {
64
+ if (!transcriptPath) return 0;
65
+ try {
66
+ const stat = fs.statSync(transcriptPath);
67
+ return stat.mtimeMs || 0;
68
+ } catch {
69
+ return 0;
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Check if a Claude Code instance (by PID) is actively writing to its transcript.
75
+ * Returns true if the transcript was modified within `thresholdMs` (default 5000ms).
76
+ *
77
+ * This is useful as a supplementary busy signal — when PTY output goes quiet
78
+ * (e.g. during API wait), the transcript file is still being written to.
79
+ */
80
+ function isTranscriptActive(pid, thresholdMs = 5000) {
81
+ const pidData = readClaudePidFile(pid);
82
+ if (!pidData || !pidData.sessionId || !pidData.cwd) return false;
83
+ const transcriptPath = findTranscriptFile(pidData.cwd, pidData.sessionId);
84
+ if (!transcriptPath) return false;
85
+ const mtime = getTranscriptMtimeMs(transcriptPath);
86
+ if (!mtime) return false;
87
+ return (Date.now() - mtime) < thresholdMs;
88
+ }
89
+
90
+ /**
91
+ * Find all live Claude Code sessions.
92
+ * Returns array of { pid, sessionId, cwd, startedAt, kind, transcriptPath }.
93
+ */
94
+ function listClaudeSessions() {
95
+ const results = [];
96
+ try {
97
+ const files = fs.readdirSync(SESSIONS_DIR);
98
+ for (const file of files) {
99
+ if (!/^\d+\.json$/.test(file)) continue;
100
+ const pid = parseInt(file.slice(0, -5), 10);
101
+ // Check if process is alive
102
+ try {
103
+ process.kill(pid, 0);
104
+ } catch {
105
+ continue; // dead process
106
+ }
107
+ const data = readClaudePidFile(pid);
108
+ if (!data) continue;
109
+ const transcriptPath = findTranscriptFile(data.cwd, data.sessionId);
110
+ results.push({ ...data, pid, transcriptPath });
111
+ }
112
+ } catch {
113
+ // ignore
114
+ }
115
+ return results;
116
+ }
117
+
118
+ module.exports = {
119
+ encodeProjectPath,
120
+ readClaudePidFile,
121
+ findTranscriptFile,
122
+ getTranscriptMtimeMs,
123
+ isTranscriptActive,
124
+ listClaudeSessions,
125
+ SESSIONS_DIR,
126
+ PROJECTS_DIR,
127
+ };
@@ -491,7 +491,10 @@ class AgentLauncher {
491
491
  const child = spawn(this.command, args, {
492
492
  cwd: this.cwd,
493
493
  stdio: "inherit",
494
- env: process.env,
494
+ env: {
495
+ ...process.env,
496
+ ...(this.agentType === "claude-code" ? { CLAUDE_CODE_EMIT_SESSION_STATE_EVENTS: "1" } : {}),
497
+ },
495
498
  });
496
499
 
497
500
  child.on("error", (err) => {
@@ -588,7 +591,11 @@ class AgentLauncher {
588
591
  try {
589
592
  const wrapper = new PtyWrapper(this.command, args, {
590
593
  cwd: this.cwd,
591
- env: process.env,
594
+ env: {
595
+ ...process.env,
596
+ // Enable Claude Code SDK session state events for precise idle/busy detection
597
+ ...(this.agentType === "claude-code" ? { CLAUDE_CODE_EMIT_SESSION_STATE_EVENTS: "1" } : {}),
598
+ },
592
599
  // 未来扩展:ioAdapter: new TerminalIOAdapter()
593
600
  });
594
601
 
@@ -716,6 +723,10 @@ class AgentLauncher {
716
723
 
717
724
  // 启动PTY
718
725
  wrapper.spawn();
726
+ // Pass PID to ActivityDetector for transcript mtime checking
727
+ if (wrapper.pty && wrapper.pty.pid) {
728
+ launcherActivityDetector.setPid(wrapper.pty.pid);
729
+ }
719
730
  wrapper.attachStreams(process.stdin, process.stdout, process.stderr);
720
731
 
721
732
 
@@ -99,9 +99,10 @@ class SubscriberManager {
99
99
  }
100
100
 
101
101
  async cleanupDuplicateTty(currentSubscriber, ttyPath) {
102
- if (!ttyPath) return;
103
- if (!this.busData.agents) return;
102
+ if (!ttyPath) return null;
103
+ if (!this.busData.agents) return null;
104
104
 
105
+ let inheritedNickname = null;
105
106
  const entries = Object.entries(this.busData.agents);
106
107
  for (const [id, meta] of entries) {
107
108
  if (id === currentSubscriber) continue;
@@ -111,6 +112,10 @@ class SubscriberManager {
111
112
  : (await this.queueManager.readTty(id));
112
113
  if (!metaTty) continue;
113
114
  if (metaTty === ttyPath) {
115
+ // Inherit user-set nickname from the displaced entry
116
+ if (meta.nickname && !inheritedNickname) {
117
+ inheritedNickname = meta.nickname;
118
+ }
114
119
  // Remove stale subscriber using same tty
115
120
  delete this.busData.agents[id];
116
121
  try {
@@ -125,6 +130,7 @@ class SubscriberManager {
125
130
  }
126
131
  }
127
132
  }
133
+ return inheritedNickname;
128
134
  }
129
135
 
130
136
  /**
@@ -186,7 +192,12 @@ class SubscriberManager {
186
192
  const ttyInfo = finalTty ? getTtyProcessInfo(finalTty) : null;
187
193
 
188
194
  // 清理同一 tty 的旧订阅者(避免重复启动污染)
189
- await this.cleanupDuplicateTty(subscriber, finalTty);
195
+ // Inherit nickname from displaced entry when this is a new subscriber
196
+ // with no explicit nickname (e.g. session restart on same TTY)
197
+ const inheritedNickname = await this.cleanupDuplicateTty(subscriber, finalTty);
198
+ if (inheritedNickname && !nickname && !existingMeta) {
199
+ finalNickname = inheritedNickname;
200
+ }
190
201
 
191
202
  // 更新订阅者信息(保留已有字段,如 provider_session_*)
192
203
  const preserved = existingMeta && typeof existingMeta === "object"
@@ -288,6 +299,7 @@ class SubscriberManager {
288
299
  }
289
300
 
290
301
  this.busData.agents[subscriber].status = "inactive";
302
+ this.busData.agents[subscriber].activity_state = "";
291
303
  this.busData.agents[subscriber].last_seen = getTimestamp();
292
304
 
293
305
  return true;
@@ -350,6 +362,7 @@ class SubscriberManager {
350
362
  for (const [id, meta] of Object.entries(this.busData.agents)) {
351
363
  if (meta.status === "active" && !isMetaActive(meta)) {
352
364
  meta.status = "inactive";
365
+ meta.activity_state = "";
353
366
  meta.last_seen = getTimestamp();
354
367
  }
355
368
  }
@@ -189,7 +189,6 @@ function createCommandExecutor(options = {}) {
189
189
  }
190
190
 
191
191
  if (subcommand === "restart") {
192
- logMessage("system", "{white-fg}⚙{/white-fg} Restarting daemon...");
193
192
  await restartDaemon();
194
193
  return;
195
194
  }
@@ -38,10 +38,10 @@ const COMMAND_TREE = {
38
38
  "/group": {
39
39
  desc: "Agent group orchestration",
40
40
  children: {
41
- diagram: { desc: "Render group diagram (ascii|mermaid)" },
42
41
  run: { desc: "Launch a group template" },
43
- status: { desc: "Show group runtime status" },
44
42
  stop: { desc: "Stop a running group" },
43
+ status: { desc: "Show group runtime status" },
44
+ diagram: { desc: "Render group diagram (ascii|mermaid)" },
45
45
  template: { desc: "Template ops (list/show/validate/new)" },
46
46
  templates: { desc: "List available templates" },
47
47
  },
@@ -136,6 +136,13 @@ function parseCommand(text) {
136
136
  return { command, args };
137
137
  }
138
138
 
139
+ function shouldEchoCommandInChat(text) {
140
+ const parsed = parseCommand(String(text || "").trim());
141
+ if (!parsed) return true;
142
+ if (parsed.command === "group" && parsed.args[0] === "run") return false;
143
+ return true;
144
+ }
145
+
139
146
  function parseAtTarget(text) {
140
147
  if (!text.startsWith("@")) return null;
141
148
  const trimmed = text.slice(1).trim();
@@ -155,5 +162,6 @@ module.exports = {
155
162
  sortCommands,
156
163
  buildCommandRegistry,
157
164
  parseCommand,
165
+ shouldEchoCommandInChat,
158
166
  parseAtTarget,
159
167
  };
@@ -39,6 +39,7 @@ function createDaemonCoordinator(options = {}) {
39
39
  startDaemon,
40
40
  daemonConnection: connection,
41
41
  logMessage,
42
+ resolveStatusLine,
42
43
  });
43
44
  let switchProjectChain = Promise.resolve();
44
45
 
@@ -1,5 +1,6 @@
1
1
  const { IPC_RESPONSE_TYPES, BUS_STATUS_PHASES } = require("../shared/eventContract");
2
2
  const { renderMarkdownLines } = require("../shared/markdownRenderer");
3
+ const { decodeEscapedNewlines } = require("./text");
3
4
 
4
5
  function createDaemonMessageRouter(options = {}) {
5
6
  const {
@@ -51,7 +52,7 @@ function createDaemonMessageRouter(options = {}) {
51
52
  }
52
53
 
53
54
  if (typeof displayMessage === "string") {
54
- displayMessage = displayMessage.replace(/\\n/g, "\n");
55
+ displayMessage = decodeEscapedNewlines(displayMessage);
55
56
  }
56
57
 
57
58
  return { displayMessage, streamPayload };
@@ -198,8 +199,23 @@ function createDaemonMessageRouter(options = {}) {
198
199
  function handleResponseMessage(msg) {
199
200
  const payload = msg.data || {};
200
201
  if (payload.reply) {
201
- resolveStatusLine(`{gray-fg}←{/gray-fg} ${escapeBlessed(payload.reply)}`);
202
- logMessage("reply", `{white-fg}←{/white-fg} ${escapeBlessed(payload.reply)}`);
202
+ const replyText = decodeEscapedNewlines(payload.reply);
203
+ resolveStatusLine(`{gray-fg}←{/gray-fg} ${escapeBlessed(replyText)}`);
204
+ const ops = Array.isArray(payload.ops) ? payload.ops : [];
205
+ const isLifecycleStatusOnly = ops.length > 0
206
+ && ops.every((op) => op && (op.action === "close" || op.action === "launch"));
207
+ const group = payload.group && typeof payload.group === "object" ? payload.group : null;
208
+ const isGroupStartedConfirmation = Boolean(
209
+ group &&
210
+ group.group_id &&
211
+ Array.isArray(group.members) &&
212
+ !group.dry_run &&
213
+ /^Group started\b/i.test(replyText)
214
+ );
215
+ // Suppress lifecycle confirmations from chat history — status line plus structured payload is enough.
216
+ if (!isLifecycleStatusOnly && !isGroupStartedConfirmation) {
217
+ logMessage("reply", `{white-fg}←{/white-fg} ${escapeBlessed(replyText)}`);
218
+ }
203
219
  }
204
220
 
205
221
  if (payload.recoverable && typeof payload.recoverable === "object") {
@@ -277,7 +293,7 @@ function createDaemonMessageRouter(options = {}) {
277
293
 
278
294
  if (payload.dispatch && payload.dispatch.length > 0) {
279
295
  const targets = payload.dispatch.map((d) => d.target || d).join(", ");
280
- logMessage("dispatch", `{white-fg}→{/white-fg} Dispatched to: ${escapeBlessed(targets)}`);
296
+ resolveStatusLine(`{gray-fg}→{/gray-fg} Dispatched to: ${escapeBlessed(targets)}`);
281
297
  }
282
298
 
283
299
  if (
@@ -350,6 +366,10 @@ function createDaemonMessageRouter(options = {}) {
350
366
 
351
367
  const { displayMessage, streamPayload } = normalizeDisplayMessage(data.message || "");
352
368
 
369
+ // Skip silent events (e.g. delivery confirmations from notifier) and empty messages
370
+ if (data.silent && !streamPayload) return true;
371
+ if (!displayMessage && !streamPayload) return true;
372
+
353
373
  const isAgentViewTarget =
354
374
  getCurrentView() === "agent" &&
355
375
  isAgentViewUsesBus() &&
@@ -361,7 +381,7 @@ function createDaemonMessageRouter(options = {}) {
361
381
  if (isAgentViewTarget) {
362
382
  if (streamPayload) {
363
383
  const delta = typeof streamPayload.delta === "string"
364
- ? streamPayload.delta.replace(/\\n/g, "\n")
384
+ ? decodeEscapedNewlines(streamPayload.delta)
365
385
  : "";
366
386
  if (delta) writeToAgentTerm(delta);
367
387
  } else if (displayMessage) {
@@ -371,15 +391,7 @@ function createDaemonMessageRouter(options = {}) {
371
391
  }
372
392
 
373
393
  if (data.event === "delivery" && consumePendingDelivery(publisher, displayName)) {
374
- const ok = (data.status || "").toLowerCase() !== "error";
375
- const detail = typeof data.message === "string" && data.message
376
- ? data.message
377
- : (ok ? `Delivered to @${displayName}` : `Delivery failed to @${displayName}`);
378
- if (ok) {
379
- logMessage("status", `{white-fg}✓{/white-fg} ${escapeBlessed(detail)}`);
380
- } else {
381
- logMessage("error", `{white-fg}✗{/white-fg} ${escapeBlessed(detail)}`);
382
- }
394
+ // Delivery confirmations are already shown in the status bar suppress from chat.
383
395
  requestStatus();
384
396
  renderScreen();
385
397
  return true;
@@ -391,7 +403,7 @@ function createDaemonMessageRouter(options = {}) {
391
403
 
392
404
  if (streamPayload) {
393
405
  const delta = typeof streamPayload.delta === "string"
394
- ? streamPayload.delta.replace(/\\n/g, "\n")
406
+ ? decodeEscapedNewlines(streamPayload.delta)
395
407
  : "";
396
408
  const state = beginStream(publisher, prefixLabel, continuationPrefix, data);
397
409
  if (delta) appendStreamDelta(state, delta);
@@ -426,7 +438,6 @@ function createDaemonMessageRouter(options = {}) {
426
438
 
427
439
  function handleErrorMessage(msg) {
428
440
  resolveStatusLine(`{gray-fg}✗{/gray-fg} Error: ${msg.error}`);
429
- logMessage("error", `{white-fg}✗{/white-fg} Error: ${msg.error}`);
430
441
  renderScreen();
431
442
  return false;
432
443
  }
@@ -9,14 +9,17 @@ function restartDaemonFlow(options = {}) {
9
9
  startDaemon,
10
10
  daemonConnection,
11
11
  logMessage,
12
+ resolveStatusLine = null,
12
13
  } = options;
13
14
 
15
+ const statusMsg = resolveStatusLine || ((text) => logMessage("status", text));
16
+
14
17
  let restartInProgress = false;
15
18
 
16
19
  return async function restartDaemon() {
17
20
  if (restartInProgress) return;
18
21
  restartInProgress = true;
19
- logMessage("status", "{white-fg}⚙{/white-fg} Restarting daemon...");
22
+ statusMsg("{gray-fg}⚙{/gray-fg} Restarting daemon...");
20
23
  try {
21
24
  const connection = resolveDaemonConnection(daemonConnection);
22
25
  if (connection) {
@@ -26,9 +29,9 @@ function restartDaemonFlow(options = {}) {
26
29
  startDaemon(projectRoot);
27
30
  const connected = connection ? await connection.connect() : false;
28
31
  if (connected) {
29
- logMessage("status", "{white-fg}✓{/white-fg} Daemon reconnected");
32
+ statusMsg("{gray-fg}✓{/gray-fg} Daemon reconnected");
30
33
  } else {
31
- logMessage("error", "{white-fg}✗{/white-fg} Failed to reconnect to daemon");
34
+ statusMsg("{gray-fg}✗{/gray-fg} Failed to reconnect to daemon");
32
35
  }
33
36
  } finally {
34
37
  restartInProgress = false;
package/src/chat/index.js CHANGED
@@ -1136,6 +1136,7 @@ async function runChat(projectRoot, options = {}) {
1136
1136
  fsModule: fs,
1137
1137
  getUfooPaths,
1138
1138
  logMessage,
1139
+ resolveStatusLine,
1139
1140
  renderDashboard,
1140
1141
  renderScreen: () => screen.render(),
1141
1142
  restartDaemon,