u-foo 1.0.3 → 1.0.6
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/README.md +67 -8
- package/README.zh-CN.md +9 -7
- package/SKILLS/ufoo/SKILL.md +117 -0
- package/SKILLS/uinit/SKILL.md +73 -0
- package/SKILLS/ustatus/SKILL.md +36 -0
- package/bin/uclaude.js +13 -0
- package/bin/ucodex.js +13 -0
- package/bin/ufoo +9 -31
- package/bin/ufoo.js +13 -0
- package/modules/AGENTS.template.md +15 -7
- package/modules/bus/README.md +28 -23
- package/modules/bus/SKILLS/ubus/SKILL.md +18 -8
- package/modules/context/README.md +18 -40
- package/modules/context/SKILLS/uctx/SKILL.md +61 -1
- package/package.json +16 -4
- package/scripts/.archived/bash-to-js-migration/README.md +46 -0
- package/scripts/.archived/bash-to-js-migration/banner.sh +89 -0
- package/scripts/{bus-inject.sh → .archived/bash-to-js-migration/bus-inject.sh} +35 -3
- package/scripts/{bus.sh → .archived/bash-to-js-migration/bus.sh} +3 -1
- package/scripts/banner.sh +2 -89
- package/scripts/postinstall.js +59 -0
- package/src/agent/cliRunner.js +33 -5
- package/src/agent/internalRunner.js +78 -51
- package/src/agent/launcher.js +702 -0
- package/src/agent/notifier.js +200 -0
- package/src/agent/ptyRunner.js +377 -0
- package/src/agent/ptyWrapper.js +354 -0
- package/src/agent/readyDetector.js +159 -0
- package/src/agent/ufooAgent.js +37 -42
- package/src/bus/API_DESIGN.md +204 -0
- package/src/bus/activate.js +156 -0
- package/src/bus/daemon.js +308 -0
- package/src/bus/index.js +785 -0
- package/src/bus/inject.js +285 -0
- package/src/bus/message.js +302 -0
- package/src/bus/nickname.js +86 -0
- package/src/bus/queue.js +131 -0
- package/src/bus/shake.js +26 -0
- package/src/bus/subscriber.js +296 -0
- package/src/bus/utils.js +357 -0
- package/src/chat/index.js +1842 -249
- package/src/cli.js +658 -95
- package/src/config.js +9 -2
- package/src/context/decisions.js +314 -0
- package/src/context/doctor.js +183 -0
- package/src/context/index.js +38 -0
- package/src/daemon/index.js +749 -94
- package/src/daemon/ops.js +395 -51
- package/src/daemon/providerSessions.js +291 -0
- package/src/daemon/run.js +34 -1
- package/src/daemon/status.js +24 -7
- package/src/doctor/index.js +50 -0
- package/src/init/index.js +264 -0
- package/src/skills/index.js +159 -0
- package/src/status/index.js +252 -0
- package/src/terminal/detect.js +64 -0
- package/src/terminal/index.js +8 -0
- package/src/terminal/iterm2.js +126 -0
- package/src/ufoo/agentsStore.js +41 -0
- package/src/ufoo/paths.js +46 -0
- package/src/utils/banner.js +73 -0
- package/bin/uclaude +0 -65
- package/bin/ucodex +0 -65
- package/modules/bus/scripts/bus-alert.sh +0 -185
- package/modules/bus/scripts/bus-listen.sh +0 -117
- package/modules/context/ASSUMPTIONS.md +0 -7
- package/modules/context/CONSTRAINTS.md +0 -7
- package/modules/context/CONTEXT-STRUCTURE.md +0 -49
- package/modules/context/DECISION-PROTOCOL.md +0 -62
- package/modules/context/HANDOFF.md +0 -33
- package/modules/context/RULES.md +0 -15
- package/modules/context/SKILLS/README.md +0 -14
- package/modules/context/SYSTEM.md +0 -18
- package/modules/context/TEMPLATES/assumptions.md +0 -4
- package/modules/context/TEMPLATES/constraints.md +0 -4
- package/modules/context/TEMPLATES/decision.md +0 -16
- package/modules/context/TEMPLATES/project-context-readme.md +0 -6
- package/modules/context/TEMPLATES/system.md +0 -3
- package/modules/context/TEMPLATES/terminology.md +0 -4
- package/modules/context/TERMINOLOGY.md +0 -10
- /package/scripts/{bus-alert.sh → .archived/bash-to-js-migration/bus-alert.sh} +0 -0
- /package/scripts/{bus-autotrigger.sh → .archived/bash-to-js-migration/bus-autotrigger.sh} +0 -0
- /package/scripts/{bus-daemon.sh → .archived/bash-to-js-migration/bus-daemon.sh} +0 -0
- /package/scripts/{bus-listen.sh → .archived/bash-to-js-migration/bus-listen.sh} +0 -0
- /package/scripts/{context-decisions.sh → .archived/bash-to-js-migration/context-decisions.sh} +0 -0
- /package/scripts/{context-doctor.sh → .archived/bash-to-js-migration/context-doctor.sh} +0 -0
- /package/scripts/{context-lint.sh → .archived/bash-to-js-migration/context-lint.sh} +0 -0
- /package/scripts/{doctor.sh → .archived/bash-to-js-migration/doctor.sh} +0 -0
- /package/scripts/{init.sh → .archived/bash-to-js-migration/init.sh} +0 -0
- /package/scripts/{skills.sh → .archived/bash-to-js-migration/skills.sh} +0 -0
- /package/scripts/{status.sh → .archived/bash-to-js-migration/status.sh} +0 -0
package/src/chat/index.js
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
const net = require("net");
|
|
2
2
|
const path = require("path");
|
|
3
3
|
const blessed = require("blessed");
|
|
4
|
-
const { spawn, spawnSync } = require("child_process");
|
|
4
|
+
const { spawn, spawnSync, execSync } = require("child_process");
|
|
5
5
|
const fs = require("fs");
|
|
6
6
|
const { loadConfig, saveConfig, normalizeLaunchMode, normalizeAgentProvider } = require("../config");
|
|
7
7
|
const { socketPath, isRunning } = require("../daemon");
|
|
8
|
+
const UfooInit = require("../init");
|
|
9
|
+
const EventBus = require("../bus");
|
|
10
|
+
const AgentActivator = require("../bus/activate");
|
|
11
|
+
const { getUfooPaths } = require("../ufoo/paths");
|
|
12
|
+
const { subscriberToSafeName } = require("../bus/utils");
|
|
8
13
|
|
|
9
14
|
function connectSocket(sockPath) {
|
|
10
15
|
return new Promise((resolve, reject) => {
|
|
@@ -19,12 +24,16 @@ function resolveProjectFile(projectRoot, relativePath, fallbackRelativePath) {
|
|
|
19
24
|
return path.join(__dirname, "..", "..", fallbackRelativePath);
|
|
20
25
|
}
|
|
21
26
|
|
|
22
|
-
function startDaemon(projectRoot) {
|
|
27
|
+
function startDaemon(projectRoot, options = {}) {
|
|
23
28
|
const daemonBin = resolveProjectFile(projectRoot, path.join("bin", "ufoo.js"), path.join("bin", "ufoo.js"));
|
|
29
|
+
const env = options.forceResume
|
|
30
|
+
? { ...process.env, UFOO_FORCE_RESUME: "1" }
|
|
31
|
+
: process.env;
|
|
24
32
|
const child = spawn(process.execPath, [daemonBin, "daemon", "--start"], {
|
|
25
33
|
detached: true,
|
|
26
34
|
stdio: "ignore",
|
|
27
35
|
cwd: projectRoot,
|
|
36
|
+
env,
|
|
28
37
|
});
|
|
29
38
|
child.unref();
|
|
30
39
|
}
|
|
@@ -52,12 +61,30 @@ async function connectWithRetry(sockPath, retries, delayMs) {
|
|
|
52
61
|
}
|
|
53
62
|
|
|
54
63
|
async function runChat(projectRoot) {
|
|
55
|
-
if (!fs.existsSync(
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
64
|
+
if (!fs.existsSync(getUfooPaths(projectRoot).ufooDir)) {
|
|
65
|
+
const repoRoot = path.join(__dirname, "..", "..");
|
|
66
|
+
const init = new UfooInit(repoRoot);
|
|
67
|
+
await init.init({ modules: "context,bus", project: projectRoot });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Ensure subscriber ID exists for chat (persistent across restarts)
|
|
71
|
+
if (!process.env.UFOO_SUBSCRIBER_ID) {
|
|
72
|
+
const crypto = require("crypto");
|
|
73
|
+
const sessionFile = path.join(getUfooPaths(projectRoot).ufooDir, "chat", "session-id.txt");
|
|
74
|
+
const sessionDir = path.dirname(sessionFile);
|
|
75
|
+
fs.mkdirSync(sessionDir, { recursive: true });
|
|
76
|
+
|
|
77
|
+
let sessionId;
|
|
78
|
+
if (fs.existsSync(sessionFile)) {
|
|
79
|
+
sessionId = fs.readFileSync(sessionFile, "utf8").trim();
|
|
80
|
+
} else {
|
|
81
|
+
sessionId = crypto.randomBytes(4).toString("hex");
|
|
82
|
+
fs.writeFileSync(sessionFile, sessionId, "utf8");
|
|
83
|
+
}
|
|
84
|
+
// Chat 模式默认使用 claude-code 类型
|
|
85
|
+
process.env.UFOO_SUBSCRIBER_ID = `claude-code:${sessionId}`;
|
|
60
86
|
}
|
|
87
|
+
|
|
61
88
|
if (!isRunning(projectRoot)) {
|
|
62
89
|
startDaemon(projectRoot);
|
|
63
90
|
}
|
|
@@ -65,6 +92,11 @@ async function runChat(projectRoot) {
|
|
|
65
92
|
const daemonBin = resolveProjectFile(projectRoot, path.join("bin", "ufoo.js"), path.join("bin", "ufoo.js"));
|
|
66
93
|
const sock = socketPath(projectRoot);
|
|
67
94
|
let client = null;
|
|
95
|
+
let reconnectPromise = null;
|
|
96
|
+
let exitRequested = false;
|
|
97
|
+
let connectionLostNotified = false;
|
|
98
|
+
const pendingRequests = [];
|
|
99
|
+
const MAX_PENDING_REQUESTS = 50;
|
|
68
100
|
|
|
69
101
|
const connectClient = async () => {
|
|
70
102
|
let newClient = await connectWithRetry(sock, 25, 200);
|
|
@@ -72,17 +104,61 @@ async function runChat(projectRoot) {
|
|
|
72
104
|
// Retry once with a fresh daemon start and longer wait.
|
|
73
105
|
if (!isRunning(projectRoot)) {
|
|
74
106
|
startDaemon(projectRoot);
|
|
107
|
+
// Wait for daemon to write PID file and create socket
|
|
108
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
75
109
|
}
|
|
76
110
|
newClient = await connectWithRetry(sock, 50, 200);
|
|
77
111
|
}
|
|
78
112
|
return newClient;
|
|
79
113
|
};
|
|
80
114
|
|
|
115
|
+
function enqueueRequest(req) {
|
|
116
|
+
if (!req || req.type === "status") return;
|
|
117
|
+
pendingRequests.push(req);
|
|
118
|
+
if (pendingRequests.length > MAX_PENDING_REQUESTS) {
|
|
119
|
+
pendingRequests.shift();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function flushPendingRequests() {
|
|
124
|
+
if (!client || client.destroyed) return;
|
|
125
|
+
while (pendingRequests.length > 0) {
|
|
126
|
+
const req = pendingRequests.shift();
|
|
127
|
+
client.write(`${JSON.stringify(req)}\n`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function ensureConnected() {
|
|
132
|
+
if (client && !client.destroyed) return true;
|
|
133
|
+
if (exitRequested) return false;
|
|
134
|
+
if (reconnectPromise) return reconnectPromise;
|
|
135
|
+
queueStatusLine("Reconnecting to daemon");
|
|
136
|
+
logMessage("status", "{magenta-fg}⚙{/magenta-fg} Reconnecting to daemon...");
|
|
137
|
+
reconnectPromise = (async () => {
|
|
138
|
+
const newClient = await connectClient();
|
|
139
|
+
if (!newClient) {
|
|
140
|
+
resolveStatusLine("{red-fg}✗{/red-fg} Daemon offline");
|
|
141
|
+
logMessage("error", "{red-fg}✗{/red-fg} Failed to reconnect to daemon");
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
attachClient(newClient);
|
|
145
|
+
connectionLostNotified = false;
|
|
146
|
+
resolveStatusLine("{green-fg}✓{/green-fg} Daemon reconnected");
|
|
147
|
+
requestStatus();
|
|
148
|
+
return true;
|
|
149
|
+
})();
|
|
150
|
+
try {
|
|
151
|
+
return await reconnectPromise;
|
|
152
|
+
} finally {
|
|
153
|
+
reconnectPromise = null;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
81
157
|
client = await connectClient();
|
|
82
158
|
if (!client) {
|
|
83
159
|
// Check if daemon failed to start
|
|
84
160
|
if (!isRunning(projectRoot)) {
|
|
85
|
-
const logFile =
|
|
161
|
+
const logFile = getUfooPaths(projectRoot).ufooDaemonLog;
|
|
86
162
|
// eslint-disable-next-line no-console
|
|
87
163
|
console.error("Failed to start ufoo daemon. Check logs at:", logFile);
|
|
88
164
|
throw new Error("Daemon failed to start. Check the daemon log for details.");
|
|
@@ -94,17 +170,28 @@ async function runChat(projectRoot) {
|
|
|
94
170
|
smartCSR: true,
|
|
95
171
|
title: "ufoo chat",
|
|
96
172
|
fullUnicode: true,
|
|
97
|
-
//
|
|
98
|
-
// Hold Option/Alt to use native selection in most terminals
|
|
173
|
+
// Toggle mouse at runtime to balance copy vs scroll
|
|
99
174
|
sendFocus: true,
|
|
100
175
|
mouse: false,
|
|
101
176
|
// Allow Ctrl+C to exit even when input grabs keys
|
|
102
177
|
ignoreLocked: ["C-c"],
|
|
103
178
|
});
|
|
179
|
+
// Prefer normal buffer for reliable terminal selection/copy
|
|
180
|
+
if (screen.program && typeof screen.program.normalBuffer === "function") {
|
|
181
|
+
screen.program.normalBuffer();
|
|
182
|
+
if (screen.program.put && typeof screen.program.put.keypad_local === "function") {
|
|
183
|
+
screen.program.put.keypad_local();
|
|
184
|
+
}
|
|
185
|
+
if (typeof screen.program.clear === "function") {
|
|
186
|
+
screen.program.clear();
|
|
187
|
+
screen.program.cup(0, 0);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
104
190
|
|
|
105
191
|
const config = loadConfig(projectRoot);
|
|
106
192
|
let launchMode = config.launchMode;
|
|
107
193
|
let agentProvider = config.agentProvider;
|
|
194
|
+
let autoResume = config.autoResume !== false;
|
|
108
195
|
|
|
109
196
|
// Dynamic input height settings
|
|
110
197
|
// Layout: topLine(1) + content + bottomLine(1) + dashboard(1)
|
|
@@ -123,11 +210,11 @@ async function runChat(projectRoot) {
|
|
|
123
210
|
scrollable: true,
|
|
124
211
|
alwaysScroll: true,
|
|
125
212
|
scrollback: 10000,
|
|
126
|
-
scrollbar:
|
|
213
|
+
scrollbar: null,
|
|
127
214
|
keys: true,
|
|
128
215
|
vi: true,
|
|
129
|
-
//
|
|
130
|
-
mouse:
|
|
216
|
+
// Mouse handled globally (toggleable) to keep copy working
|
|
217
|
+
mouse: false,
|
|
131
218
|
});
|
|
132
219
|
|
|
133
220
|
// Status line just above input
|
|
@@ -145,7 +232,7 @@ async function runChat(projectRoot) {
|
|
|
145
232
|
const bannerText = `{bold}UFOO{/bold} · Multi-Agent Manager{|}v${pkg.version}`;
|
|
146
233
|
statusLine.setContent(bannerText);
|
|
147
234
|
|
|
148
|
-
const historyDir = path.join(projectRoot
|
|
235
|
+
const historyDir = path.join(getUfooPaths(projectRoot).ufooDir, "chat");
|
|
149
236
|
const historyFile = path.join(historyDir, "history.jsonl");
|
|
150
237
|
const inputHistoryFile = path.join(historyDir, "input-history.jsonl");
|
|
151
238
|
|
|
@@ -159,13 +246,19 @@ async function runChat(projectRoot) {
|
|
|
159
246
|
let lastLogType = null;
|
|
160
247
|
let hasLoggedAny = false;
|
|
161
248
|
|
|
162
|
-
function shouldSpace(type) {
|
|
163
|
-
|
|
249
|
+
function shouldSpace(type, text) {
|
|
250
|
+
if (SPACED_TYPES.has(type)) return true;
|
|
251
|
+
if (text && /daemon/i.test(text)) return true;
|
|
252
|
+
return false;
|
|
164
253
|
}
|
|
165
254
|
|
|
166
255
|
function writeSpacer(writeHistory) {
|
|
167
256
|
if (lastLogWasSpacer || !hasLoggedAny) return;
|
|
168
|
-
|
|
257
|
+
try {
|
|
258
|
+
logBox.log(" ");
|
|
259
|
+
} catch {
|
|
260
|
+
// ignore rendering errors
|
|
261
|
+
}
|
|
169
262
|
if (writeHistory) {
|
|
170
263
|
appendHistory({
|
|
171
264
|
ts: new Date().toISOString(),
|
|
@@ -180,15 +273,16 @@ async function runChat(projectRoot) {
|
|
|
180
273
|
}
|
|
181
274
|
|
|
182
275
|
function recordLog(type, text, meta = {}, writeHistory = true) {
|
|
183
|
-
|
|
276
|
+
const lineText = text == null ? "" : String(text);
|
|
277
|
+
if (type !== "spacer" && shouldSpace(type, text)) {
|
|
184
278
|
writeSpacer(writeHistory);
|
|
185
279
|
}
|
|
186
|
-
|
|
280
|
+
appendToLogBox(lineText);
|
|
187
281
|
if (writeHistory) {
|
|
188
282
|
appendHistory({
|
|
189
283
|
ts: new Date().toISOString(),
|
|
190
284
|
type,
|
|
191
|
-
text,
|
|
285
|
+
text: lineText,
|
|
192
286
|
meta,
|
|
193
287
|
});
|
|
194
288
|
}
|
|
@@ -201,6 +295,39 @@ async function runChat(projectRoot) {
|
|
|
201
295
|
recordLog(type, text, meta, true);
|
|
202
296
|
}
|
|
203
297
|
|
|
298
|
+
// Prevent blessed tag parsing crashes from untrusted text.
|
|
299
|
+
// blessed parses `{...}` as style tags; certain inputs like `{foo,bar}` can
|
|
300
|
+
// trigger a blessed bug (Program._attr on unknown comma/semicolon parts).
|
|
301
|
+
//
|
|
302
|
+
// Workaround: blessed@0.1.81 has a bug where tags containing comma/semicolon
|
|
303
|
+
// (e.g. `{foo,bar}`) can crash when the log widget reparses cached lines.
|
|
304
|
+
// We proactively neutralize any such tag-like sequences so they don't match
|
|
305
|
+
// blessed's tag regex on subsequent reparses.
|
|
306
|
+
function neutralizeBlessedCommaTags(text) {
|
|
307
|
+
if (text == null) return "";
|
|
308
|
+
const raw = String(text);
|
|
309
|
+
if (!raw.includes("{")) return raw;
|
|
310
|
+
return raw.replace(/\{\/?[\w\-,;!#]*[;,][\w\-,;!#]*\}/g, (m) => {
|
|
311
|
+
// Insert a space after separators so `{foo,bar}` becomes `{foo, bar}`.
|
|
312
|
+
// This stops blessed from treating it as a tag on future reparses.
|
|
313
|
+
const inner = m.slice(1, -1).replace(/[,;]/g, (ch) => `${ch} `);
|
|
314
|
+
return `{${inner}}`;
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function escapeBlessed(text) {
|
|
319
|
+
if (text == null) return "{escape}{/escape}";
|
|
320
|
+
const raw = neutralizeBlessedCommaTags(text);
|
|
321
|
+
// Avoid allowing payload to terminate escape mode.
|
|
322
|
+
const safe = raw.replace(/\{\/escape\}/g, "{open}/escape{close}");
|
|
323
|
+
return `{escape}${safe}{/escape}`;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function appendToLogBox(text) {
|
|
327
|
+
// Avoid a blessed render-time crash for `{foo,bar}`-like tag sequences.
|
|
328
|
+
logBox.log(neutralizeBlessedCommaTags(text));
|
|
329
|
+
}
|
|
330
|
+
|
|
204
331
|
function loadHistory(limit = 2000) {
|
|
205
332
|
try {
|
|
206
333
|
const lines = fs.readFileSync(historyFile, "utf8").trim().split(/\r?\n/).filter(Boolean);
|
|
@@ -214,7 +341,7 @@ async function runChat(projectRoot) {
|
|
|
214
341
|
}
|
|
215
342
|
if (!item.text) continue;
|
|
216
343
|
if (hasSpacer) {
|
|
217
|
-
|
|
344
|
+
appendToLogBox(item.text);
|
|
218
345
|
lastLogWasSpacer = false;
|
|
219
346
|
lastLogType = item.type || null;
|
|
220
347
|
hasLoggedAny = true;
|
|
@@ -255,16 +382,69 @@ async function runChat(projectRoot) {
|
|
|
255
382
|
const pendingStatusLines = [];
|
|
256
383
|
const busStatusQueue = [];
|
|
257
384
|
let primaryStatusText = bannerText;
|
|
385
|
+
let primaryStatusPending = false;
|
|
386
|
+
const shimmerStart = Date.now();
|
|
387
|
+
let statusAnimationTimer = null;
|
|
388
|
+
const STATUS_ANIM_FRAME_MS = 50;
|
|
389
|
+
const SHIMMER_PADDING = 10;
|
|
390
|
+
const SHIMMER_BAND_HALF_WIDTH = 5;
|
|
391
|
+
const SHIMMER_SWEEP_MS = 2000;
|
|
392
|
+
const SPINNER_PERIOD_MS = 600;
|
|
258
393
|
|
|
259
394
|
function formatProcessingText(text) {
|
|
260
395
|
if (!text) return text;
|
|
261
396
|
if (text.includes("{")) return text;
|
|
262
397
|
if (!/processing/i.test(text)) return text;
|
|
263
|
-
return
|
|
398
|
+
return text;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function shimmerText(text, nowMs) {
|
|
402
|
+
if (!text) return "";
|
|
403
|
+
if (text.includes("{")) return text;
|
|
404
|
+
const chars = Array.from(text);
|
|
405
|
+
const period = chars.length + SHIMMER_PADDING * 2;
|
|
406
|
+
const pos =
|
|
407
|
+
Math.floor(((nowMs - shimmerStart) % SHIMMER_SWEEP_MS) / SHIMMER_SWEEP_MS * period);
|
|
408
|
+
let out = "";
|
|
409
|
+
for (let i = 0; i < chars.length; i += 1) {
|
|
410
|
+
const iPos = i + SHIMMER_PADDING;
|
|
411
|
+
const dist = Math.abs(iPos - pos);
|
|
412
|
+
let intensity = 0;
|
|
413
|
+
if (dist <= SHIMMER_BAND_HALF_WIDTH) {
|
|
414
|
+
const x = Math.PI * (dist / SHIMMER_BAND_HALF_WIDTH);
|
|
415
|
+
intensity = 0.5 * (1 + Math.cos(x));
|
|
416
|
+
}
|
|
417
|
+
const ch = chars[i];
|
|
418
|
+
if (intensity < 0.2) {
|
|
419
|
+
out += `{gray-fg}${ch}{/gray-fg}`;
|
|
420
|
+
} else if (intensity < 0.6) {
|
|
421
|
+
out += ch;
|
|
422
|
+
} else {
|
|
423
|
+
out += `{bold}{white-fg}${ch}{/white-fg}{/bold}`;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return out;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function spinnerFrame(nowMs) {
|
|
430
|
+
const on = Math.floor((nowMs - shimmerStart) / SPINNER_PERIOD_MS) % 2 === 0;
|
|
431
|
+
return on
|
|
432
|
+
? "{white-fg}•{/white-fg}"
|
|
433
|
+
: "{gray-fg}◦{/gray-fg}";
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function renderPendingStatus(text, nowMs) {
|
|
437
|
+
const spinner = spinnerFrame(nowMs);
|
|
438
|
+
const shimmer = shimmerText(text, nowMs);
|
|
439
|
+
if (!shimmer) return spinner;
|
|
440
|
+
return `${spinner} ${shimmer}`;
|
|
264
441
|
}
|
|
265
442
|
|
|
266
|
-
function renderStatusLine() {
|
|
443
|
+
function renderStatusLine(nowMs = Date.now()) {
|
|
267
444
|
let content = primaryStatusText || "";
|
|
445
|
+
if (primaryStatusPending) {
|
|
446
|
+
content = renderPendingStatus(primaryStatusText, nowMs);
|
|
447
|
+
}
|
|
268
448
|
if (busStatusQueue.length > 0) {
|
|
269
449
|
const extra = busStatusQueue.length > 1
|
|
270
450
|
? ` {gray-fg}(+${busStatusQueue.length - 1}){/gray-fg}`
|
|
@@ -277,16 +457,31 @@ async function runChat(projectRoot) {
|
|
|
277
457
|
statusLine.setContent(content);
|
|
278
458
|
}
|
|
279
459
|
|
|
280
|
-
function
|
|
460
|
+
function updateStatusAnimation() {
|
|
461
|
+
if (primaryStatusPending && !statusAnimationTimer) {
|
|
462
|
+
statusAnimationTimer = setInterval(() => {
|
|
463
|
+
if (!primaryStatusPending) return;
|
|
464
|
+
renderStatusLine(Date.now());
|
|
465
|
+
screen.render();
|
|
466
|
+
}, STATUS_ANIM_FRAME_MS);
|
|
467
|
+
} else if (!primaryStatusPending && statusAnimationTimer) {
|
|
468
|
+
clearInterval(statusAnimationTimer);
|
|
469
|
+
statusAnimationTimer = null;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function setPrimaryStatus(text, options = {}) {
|
|
281
474
|
primaryStatusText = text || "";
|
|
475
|
+
primaryStatusPending = Boolean(options.pending);
|
|
476
|
+
updateStatusAnimation();
|
|
282
477
|
renderStatusLine();
|
|
283
478
|
}
|
|
284
479
|
|
|
285
480
|
function queueStatusLine(text) {
|
|
286
|
-
|
|
287
|
-
pendingStatusLines.push(
|
|
481
|
+
let raw = text || "";
|
|
482
|
+
pendingStatusLines.push(raw);
|
|
288
483
|
if (pendingStatusLines.length === 1) {
|
|
289
|
-
setPrimaryStatus(
|
|
484
|
+
setPrimaryStatus(raw, { pending: true });
|
|
290
485
|
screen.render();
|
|
291
486
|
}
|
|
292
487
|
}
|
|
@@ -296,17 +491,18 @@ async function runChat(projectRoot) {
|
|
|
296
491
|
pendingStatusLines.shift();
|
|
297
492
|
}
|
|
298
493
|
if (pendingStatusLines.length > 0) {
|
|
299
|
-
setPrimaryStatus(pendingStatusLines[0]);
|
|
494
|
+
setPrimaryStatus(pendingStatusLines[0], { pending: true });
|
|
300
495
|
} else {
|
|
301
|
-
setPrimaryStatus(text || "");
|
|
496
|
+
setPrimaryStatus(text || "", { pending: false });
|
|
302
497
|
}
|
|
303
498
|
screen.render();
|
|
304
499
|
}
|
|
305
500
|
|
|
306
501
|
function enqueueBusStatus(item) {
|
|
307
502
|
if (!item || !item.text) return;
|
|
308
|
-
const
|
|
309
|
-
const
|
|
503
|
+
const rawText = item.text == null ? "" : String(item.text);
|
|
504
|
+
const key = item.key || rawText;
|
|
505
|
+
const formatted = escapeBlessed(formatProcessingText(rawText));
|
|
310
506
|
const existing = busStatusQueue.find((entry) => entry.key === key);
|
|
311
507
|
if (existing) {
|
|
312
508
|
existing.text = formatted;
|
|
@@ -318,7 +514,8 @@ async function runChat(projectRoot) {
|
|
|
318
514
|
|
|
319
515
|
function resolveBusStatus(item) {
|
|
320
516
|
if (!item) return;
|
|
321
|
-
const
|
|
517
|
+
const rawText = item.text == null ? "" : String(item.text);
|
|
518
|
+
const key = item.key || rawText;
|
|
322
519
|
let index = -1;
|
|
323
520
|
if (key) {
|
|
324
521
|
index = busStatusQueue.findIndex((entry) => entry.key === key);
|
|
@@ -339,6 +536,7 @@ async function runChat(projectRoot) {
|
|
|
339
536
|
width: "100%",
|
|
340
537
|
height: 0,
|
|
341
538
|
hidden: true,
|
|
539
|
+
wrap: false,
|
|
342
540
|
border: {
|
|
343
541
|
type: "line",
|
|
344
542
|
top: true,
|
|
@@ -371,6 +569,15 @@ async function runChat(projectRoot) {
|
|
|
371
569
|
tags: true,
|
|
372
570
|
});
|
|
373
571
|
|
|
572
|
+
// Agent TTY view state
|
|
573
|
+
let currentView = "main"; // "main" | "agent"
|
|
574
|
+
let viewingAgent = null; // subscriber ID of agent being viewed
|
|
575
|
+
let agentOutputClient = null; // net.Socket connected to inject.sock
|
|
576
|
+
let agentOutputBuffer = ""; // partial line buffer for output parsing
|
|
577
|
+
let agentInputClient = null; // net.Socket for sending raw input
|
|
578
|
+
let _detachedChildren = null; // Screen children saved during agent view
|
|
579
|
+
let agentInputSuppressUntil = 0; // Suppress input forwarding until this timestamp
|
|
580
|
+
|
|
374
581
|
// Bottom border line for input area (above dashboard)
|
|
375
582
|
const inputBottomLine = blessed.line({
|
|
376
583
|
parent: screen,
|
|
@@ -418,6 +625,8 @@ async function runChat(projectRoot) {
|
|
|
418
625
|
// Add cursor position tracking
|
|
419
626
|
let cursorPos = 0;
|
|
420
627
|
let preferredCol = null;
|
|
628
|
+
const unicode = blessed.unicode;
|
|
629
|
+
const wideRegex = new RegExp(unicode.chars.all.source);
|
|
421
630
|
|
|
422
631
|
// Get inner width
|
|
423
632
|
function getInnerWidth() {
|
|
@@ -438,13 +647,86 @@ async function runChat(projectRoot) {
|
|
|
438
647
|
return 1;
|
|
439
648
|
}
|
|
440
649
|
|
|
441
|
-
|
|
650
|
+
function getWrapWidth() {
|
|
651
|
+
if (input._clines && typeof input._clines.width === "number") {
|
|
652
|
+
return Math.max(1, input._clines.width);
|
|
653
|
+
}
|
|
654
|
+
return getInnerWidth();
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function isWideChar(ch) {
|
|
658
|
+
return wideRegex.test(ch);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function transformChar(ch) {
|
|
662
|
+
if (ch === "\n") return "\n";
|
|
663
|
+
if (ch === "\r") return "";
|
|
664
|
+
if (ch === "\t") return screen.tabc;
|
|
665
|
+
|
|
666
|
+
const code = ch.codePointAt(0);
|
|
667
|
+
if (
|
|
668
|
+
code <= 0x08
|
|
669
|
+
|| code === 0x0b
|
|
670
|
+
|| code === 0x0c
|
|
671
|
+
|| (code >= 0x0e && code <= 0x1a)
|
|
672
|
+
|| (code >= 0x1c && code <= 0x1f)
|
|
673
|
+
|| code === 0x7f
|
|
674
|
+
) {
|
|
675
|
+
return "";
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
if (ch === "\x1b") return "";
|
|
679
|
+
|
|
680
|
+
const isWide = isWideChar(ch);
|
|
681
|
+
|
|
682
|
+
if (screen.fullUnicode) {
|
|
683
|
+
if (screen.program && screen.program.isiTerm2 && unicode.isCombining(ch, 0)) {
|
|
684
|
+
return "";
|
|
685
|
+
}
|
|
686
|
+
if (isWide) return `${ch}\x03`;
|
|
687
|
+
return ch;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
if (unicode.isCombining(ch, 0)) return "";
|
|
691
|
+
if (unicode.isSurrogate(ch, 0)) return "?";
|
|
692
|
+
if (isWide) return "??";
|
|
693
|
+
return ch;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function transformText(text) {
|
|
697
|
+
if (!text) return "";
|
|
698
|
+
const out = [];
|
|
699
|
+
for (const ch of text) {
|
|
700
|
+
out.push(transformChar(ch));
|
|
701
|
+
}
|
|
702
|
+
return out.join("");
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function visualLength(text) {
|
|
706
|
+
return transformText(text).length;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
function originalIndexForVisual(line, visualIndex) {
|
|
710
|
+
if (visualIndex <= 0) return 0;
|
|
711
|
+
let visual = 0;
|
|
712
|
+
let offset = 0;
|
|
713
|
+
for (const ch of line) {
|
|
714
|
+
const rep = transformChar(ch);
|
|
715
|
+
const repLen = rep.length;
|
|
716
|
+
if (visual + repLen > visualIndex) return offset;
|
|
717
|
+
visual += repLen;
|
|
718
|
+
offset += ch.length;
|
|
719
|
+
}
|
|
720
|
+
return line.length;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// Count lines considering both wrapping and newlines (matches blessed wrap)
|
|
442
724
|
function countLines(text, width) {
|
|
443
725
|
if (width <= 0) return 1;
|
|
444
|
-
const lines = text.split("\n");
|
|
726
|
+
const lines = (text || "").split("\n");
|
|
445
727
|
let total = 0;
|
|
446
728
|
for (const line of lines) {
|
|
447
|
-
const lineWidth =
|
|
729
|
+
const lineWidth = visualLength(line);
|
|
448
730
|
total += Math.max(1, Math.ceil(lineWidth / width));
|
|
449
731
|
}
|
|
450
732
|
return total;
|
|
@@ -452,45 +734,33 @@ async function runChat(projectRoot) {
|
|
|
452
734
|
|
|
453
735
|
function getCursorRowCol(text, pos, width) {
|
|
454
736
|
if (width <= 0) return { row: 0, col: 0 };
|
|
455
|
-
const before = text.slice(0, pos);
|
|
456
|
-
const
|
|
737
|
+
const before = (text || "").slice(0, pos);
|
|
738
|
+
const transformed = transformText(before);
|
|
739
|
+
const lines = transformed.split("\n");
|
|
457
740
|
let row = 0;
|
|
458
741
|
for (let i = 0; i < lines.length - 1; i++) {
|
|
459
|
-
const lineWidth =
|
|
742
|
+
const lineWidth = lines[i].length;
|
|
460
743
|
row += Math.max(1, Math.ceil(lineWidth / width));
|
|
461
744
|
}
|
|
462
745
|
const lastLine = lines[lines.length - 1] || "";
|
|
463
|
-
const lastWidth =
|
|
746
|
+
const lastWidth = lastLine.length;
|
|
464
747
|
row += Math.floor(lastWidth / width);
|
|
465
748
|
const col = lastWidth % width;
|
|
466
749
|
return { row, col };
|
|
467
750
|
}
|
|
468
751
|
|
|
469
|
-
function getLinePosForCol(line, targetCol) {
|
|
470
|
-
if (targetCol <= 0) return 0;
|
|
471
|
-
let col = 0;
|
|
472
|
-
let offset = 0;
|
|
473
|
-
for (const ch of Array.from(line)) {
|
|
474
|
-
const w = input.strWidth(ch);
|
|
475
|
-
if (col + w > targetCol) return offset;
|
|
476
|
-
col += w;
|
|
477
|
-
offset += ch.length;
|
|
478
|
-
}
|
|
479
|
-
return offset;
|
|
480
|
-
}
|
|
481
|
-
|
|
482
752
|
function getCursorPosForRowCol(text, targetRow, targetCol, width) {
|
|
483
753
|
if (width <= 0) return 0;
|
|
484
|
-
const lines = text.split("\n");
|
|
754
|
+
const lines = (text || "").split("\n");
|
|
485
755
|
let row = 0;
|
|
486
756
|
let pos = 0;
|
|
487
757
|
for (const line of lines) {
|
|
488
|
-
const lineWidth =
|
|
758
|
+
const lineWidth = visualLength(line);
|
|
489
759
|
const wrappedRows = Math.max(1, Math.ceil(lineWidth / width));
|
|
490
760
|
if (targetRow < row + wrappedRows) {
|
|
491
761
|
const rowInLine = targetRow - row;
|
|
492
762
|
const visualCol = rowInLine * width + Math.max(0, targetCol);
|
|
493
|
-
return pos +
|
|
763
|
+
return pos + originalIndexForVisual(line, Math.min(visualCol, lineWidth));
|
|
494
764
|
}
|
|
495
765
|
pos += line.length + 1;
|
|
496
766
|
row += wrappedRows;
|
|
@@ -498,6 +768,34 @@ async function runChat(projectRoot) {
|
|
|
498
768
|
return text.length;
|
|
499
769
|
}
|
|
500
770
|
|
|
771
|
+
function ensureInputCursorVisible() {
|
|
772
|
+
const innerWidth = getWrapWidth();
|
|
773
|
+
if (innerWidth <= 0) return;
|
|
774
|
+
const totalRows = countLines(input.value, innerWidth);
|
|
775
|
+
const visibleRows = Math.max(1, input.height || 1);
|
|
776
|
+
const { row } = getCursorRowCol(input.value, cursorPos, innerWidth);
|
|
777
|
+
let base = input.childBase || 0;
|
|
778
|
+
const maxBase = Math.max(0, totalRows - visibleRows);
|
|
779
|
+
const bottomMargin = visibleRows > 1 ? 1 : 0;
|
|
780
|
+
const upperLimit = base;
|
|
781
|
+
const lowerLimit = base + visibleRows - bottomMargin - 1;
|
|
782
|
+
|
|
783
|
+
if (row < upperLimit) {
|
|
784
|
+
base = row;
|
|
785
|
+
} else if (row > lowerLimit) {
|
|
786
|
+
base = row - (visibleRows - bottomMargin - 1);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
if (base > maxBase) base = maxBase;
|
|
790
|
+
if (base < 0) base = 0;
|
|
791
|
+
if (base !== input.childBase) {
|
|
792
|
+
input.childBase = base;
|
|
793
|
+
if (typeof input.scrollTo === "function") {
|
|
794
|
+
input.scrollTo(base);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
501
799
|
function resetPreferredCol() {
|
|
502
800
|
preferredCol = null;
|
|
503
801
|
}
|
|
@@ -547,6 +845,7 @@ async function runChat(projectRoot) {
|
|
|
547
845
|
normalizeCommandPrefix();
|
|
548
846
|
resetPreferredCol();
|
|
549
847
|
resizeInput();
|
|
848
|
+
ensureInputCursorVisible();
|
|
550
849
|
input._updateCursor();
|
|
551
850
|
screen.render();
|
|
552
851
|
updateDraftFromInput();
|
|
@@ -557,6 +856,7 @@ async function runChat(projectRoot) {
|
|
|
557
856
|
cursorPos = input.value.length;
|
|
558
857
|
resetPreferredCol();
|
|
559
858
|
resizeInput();
|
|
859
|
+
ensureInputCursorVisible();
|
|
560
860
|
input._updateCursor();
|
|
561
861
|
screen.render();
|
|
562
862
|
}
|
|
@@ -590,9 +890,17 @@ async function runChat(projectRoot) {
|
|
|
590
890
|
}
|
|
591
891
|
|
|
592
892
|
function exitHandler() {
|
|
893
|
+
exitRequested = true;
|
|
894
|
+
// Clean up agent view connections
|
|
895
|
+
disconnectAgentOutput();
|
|
896
|
+
disconnectAgentInput();
|
|
593
897
|
if (screen && screen.program && typeof screen.program.decrst === "function") {
|
|
594
898
|
screen.program.decrst(2004);
|
|
595
899
|
}
|
|
900
|
+
if (statusAnimationTimer) {
|
|
901
|
+
clearInterval(statusAnimationTimer);
|
|
902
|
+
statusAnimationTimer = null;
|
|
903
|
+
}
|
|
596
904
|
if (client) {
|
|
597
905
|
client.end();
|
|
598
906
|
}
|
|
@@ -630,9 +938,12 @@ async function runChat(projectRoot) {
|
|
|
630
938
|
const parts = filterText.split(/\s+/);
|
|
631
939
|
let commands = [];
|
|
632
940
|
|
|
633
|
-
|
|
941
|
+
const mainCmd = parts[0];
|
|
942
|
+
const isLaunch = mainCmd && mainCmd.toLowerCase() === "/launch";
|
|
943
|
+
const wantsSubcommands = (parts.length > 1 || (endsWithSpace && parts.length === 1));
|
|
944
|
+
|
|
945
|
+
if ((wantsSubcommands || isLaunch) && mainCmd && mainCmd.startsWith("/")) {
|
|
634
946
|
// Subcommand mode: "/bus rename"
|
|
635
|
-
const mainCmd = parts[0];
|
|
636
947
|
const subFilter = parts[1] || "";
|
|
637
948
|
|
|
638
949
|
// Find the main command
|
|
@@ -640,31 +951,41 @@ async function runChat(projectRoot) {
|
|
|
640
951
|
item.cmd.toLowerCase() === mainCmd.toLowerCase()
|
|
641
952
|
);
|
|
642
953
|
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
954
|
+
const fallbackLaunchSubs = [
|
|
955
|
+
{ cmd: "claude", desc: "Launch Claude agent" },
|
|
956
|
+
{ cmd: "codex", desc: "Launch Codex agent" },
|
|
957
|
+
];
|
|
958
|
+
|
|
959
|
+
if ((mainCmdObj && mainCmdObj.subcommands) || isLaunch) {
|
|
960
|
+
const baseSubs = mainCmdObj && mainCmdObj.subcommands ? mainCmdObj.subcommands : [];
|
|
961
|
+
let subs = baseSubs;
|
|
962
|
+
if (isLaunch) {
|
|
963
|
+
const merged = new Map();
|
|
964
|
+
for (const sub of [...baseSubs, ...fallbackLaunchSubs]) {
|
|
965
|
+
if (!sub || !sub.cmd) continue;
|
|
966
|
+
merged.set(sub.cmd, sub);
|
|
967
|
+
}
|
|
968
|
+
subs = Array.from(merged.values());
|
|
969
|
+
}
|
|
970
|
+
if (isLaunch) {
|
|
971
|
+
// Always show both launch targets for clarity
|
|
972
|
+
commands = subs
|
|
973
|
+
.map(sub => ({ ...sub, isSubcommand: true, parentCmd: mainCmd }))
|
|
974
|
+
.sort((a, b) => a.cmd.localeCompare(b.cmd));
|
|
975
|
+
} else {
|
|
976
|
+
// Filter subcommands
|
|
977
|
+
commands = subs
|
|
978
|
+
.filter(sub => sub.cmd.toLowerCase().startsWith(subFilter.toLowerCase()))
|
|
979
|
+
.map(sub => ({ ...sub, isSubcommand: true, parentCmd: mainCmd }))
|
|
980
|
+
.sort((a, b) => a.cmd.localeCompare(b.cmd));
|
|
981
|
+
}
|
|
648
982
|
}
|
|
649
983
|
} else {
|
|
650
984
|
// Main command mode: "/bus"
|
|
651
|
-
const
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
let fuzzyMatches = [];
|
|
656
|
-
if (filterText.startsWith("/") && parts.length === 1) {
|
|
657
|
-
const needle = filterText.slice(1).toLowerCase();
|
|
658
|
-
if (needle) {
|
|
659
|
-
fuzzyMatches = COMMAND_REGISTRY.filter(item =>
|
|
660
|
-
item.cmd.toLowerCase().includes(needle)
|
|
661
|
-
);
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
const merged = new Map();
|
|
665
|
-
for (const item of prefixMatches) merged.set(item.cmd, item);
|
|
666
|
-
for (const item of fuzzyMatches) merged.set(item.cmd, item);
|
|
667
|
-
commands = Array.from(merged.values());
|
|
985
|
+
const filterLower = filterText.toLowerCase();
|
|
986
|
+
commands = COMMAND_REGISTRY
|
|
987
|
+
.filter(item => item.cmd.toLowerCase().startsWith(filterLower))
|
|
988
|
+
.sort((a, b) => a.cmd.localeCompare(b.cmd, "en", { sensitivity: "base" }));
|
|
668
989
|
}
|
|
669
990
|
|
|
670
991
|
if (commands.length === 0) {
|
|
@@ -677,9 +998,12 @@ async function runChat(projectRoot) {
|
|
|
677
998
|
completionIndex = 0;
|
|
678
999
|
completionScrollOffset = 0;
|
|
679
1000
|
|
|
680
|
-
// Calculate panel height (
|
|
681
|
-
|
|
682
|
-
|
|
1001
|
+
// Calculate panel height (visible items + 2 for blessed border overhead)
|
|
1002
|
+
// blessed reserves 2 rows for border (iheight) even when only border.top is set
|
|
1003
|
+
const availableHeight = screen.height - currentInputHeight - 1;
|
|
1004
|
+
completionVisibleCount = Math.min(7, completionCommands.length);
|
|
1005
|
+
completionVisibleCount = Math.min(completionVisibleCount, Math.max(1, availableHeight - 2));
|
|
1006
|
+
completionPanel.height = completionVisibleCount + 2;
|
|
683
1007
|
completionPanel.bottom = currentInputHeight - 1;
|
|
684
1008
|
completionPanel.hidden = false;
|
|
685
1009
|
|
|
@@ -691,6 +1015,7 @@ async function runChat(projectRoot) {
|
|
|
691
1015
|
completionCommands = [];
|
|
692
1016
|
completionIndex = 0;
|
|
693
1017
|
completionScrollOffset = 0;
|
|
1018
|
+
completionVisibleCount = 0;
|
|
694
1019
|
completionPanel.hidden = true;
|
|
695
1020
|
screen.render();
|
|
696
1021
|
}
|
|
@@ -698,7 +1023,11 @@ async function runChat(projectRoot) {
|
|
|
698
1023
|
function renderCompletionPanel() {
|
|
699
1024
|
if (!completionActive || completionCommands.length === 0) return;
|
|
700
1025
|
|
|
701
|
-
|
|
1026
|
+
// blessed reserves 2 rows for border (iheight=2) even with only border.top
|
|
1027
|
+
const panelVisible = Math.max(1, (completionPanel.height || 2) - 2);
|
|
1028
|
+
const maxVisible = completionVisibleCount
|
|
1029
|
+
? Math.max(1, Math.min(completionVisibleCount, panelVisible))
|
|
1030
|
+
: panelVisible;
|
|
702
1031
|
|
|
703
1032
|
// Adjust scroll offset to keep selected item visible
|
|
704
1033
|
if (completionIndex < completionScrollOffset) {
|
|
@@ -712,21 +1041,37 @@ async function runChat(projectRoot) {
|
|
|
712
1041
|
const visibleEnd = Math.min(completionScrollOffset + maxVisible, completionCommands.length);
|
|
713
1042
|
const visibleCommands = completionCommands.slice(visibleStart, visibleEnd);
|
|
714
1043
|
|
|
1044
|
+
const panelWidth = typeof completionPanel.width === "number"
|
|
1045
|
+
? completionPanel.width
|
|
1046
|
+
: screen.width;
|
|
715
1047
|
const lines = visibleCommands.map((item, i) => {
|
|
716
1048
|
const actualIndex = visibleStart + i;
|
|
1049
|
+
const cmdText = item.cmd;
|
|
1050
|
+
const descText = item.desc || "";
|
|
717
1051
|
const cmdPart = actualIndex === completionIndex
|
|
718
|
-
? `{inverse}${
|
|
719
|
-
: `{cyan-fg}${
|
|
720
|
-
const descPart = `{gray-fg}${item.desc}{/gray-fg}`;
|
|
721
|
-
// Use promptBox width (2) to align with input position
|
|
1052
|
+
? `{inverse}${cmdText}{/inverse}`
|
|
1053
|
+
: `{cyan-fg}${cmdText}{/cyan-fg}`;
|
|
722
1054
|
const indent = " ".repeat(promptBox.width || 2);
|
|
723
|
-
|
|
1055
|
+
const maxDescWidth = Math.max(0, panelWidth - indent.length - cmdText.length - 2);
|
|
1056
|
+
const trimmedDesc = truncateText(descText, maxDescWidth);
|
|
1057
|
+
const descPart = trimmedDesc ? `{gray-fg}${trimmedDesc}{/gray-fg}` : "";
|
|
1058
|
+
// Use promptBox width (2) to align with input position
|
|
1059
|
+
return descPart
|
|
1060
|
+
? `${indent}${cmdPart} ${descPart}`
|
|
1061
|
+
: `${indent}${cmdPart}`;
|
|
724
1062
|
});
|
|
725
1063
|
|
|
726
1064
|
completionPanel.setContent(lines.join("\n"));
|
|
727
1065
|
screen.render();
|
|
728
1066
|
}
|
|
729
1067
|
|
|
1068
|
+
function completionPageSize() {
|
|
1069
|
+
const panelVisible = Math.max(1, (completionPanel.height || 2) - 2);
|
|
1070
|
+
return completionVisibleCount
|
|
1071
|
+
? Math.max(1, Math.min(completionVisibleCount, panelVisible))
|
|
1072
|
+
: panelVisible;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
730
1075
|
function completionUp() {
|
|
731
1076
|
if (completionCommands.length === 0) return;
|
|
732
1077
|
completionIndex = completionIndex <= 0
|
|
@@ -743,6 +1088,55 @@ async function runChat(projectRoot) {
|
|
|
743
1088
|
renderCompletionPanel();
|
|
744
1089
|
}
|
|
745
1090
|
|
|
1091
|
+
function completionPageUp() {
|
|
1092
|
+
if (completionCommands.length === 0) return;
|
|
1093
|
+
const step = completionPageSize();
|
|
1094
|
+
completionIndex = Math.max(0, completionIndex - step);
|
|
1095
|
+
renderCompletionPanel();
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
function completionPageDown() {
|
|
1099
|
+
if (completionCommands.length === 0) return;
|
|
1100
|
+
const step = completionPageSize();
|
|
1101
|
+
completionIndex = Math.min(completionCommands.length - 1, completionIndex + step);
|
|
1102
|
+
renderCompletionPanel();
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
function completionPreview(selected) {
|
|
1106
|
+
const current = input.value || "";
|
|
1107
|
+
const trimmed = current.trim();
|
|
1108
|
+
const endsWithSpace = /\s$/.test(current);
|
|
1109
|
+
if (selected.isSubcommand) {
|
|
1110
|
+
const parts = trimmed.split(/\s+/);
|
|
1111
|
+
const base = parts[0] || "";
|
|
1112
|
+
const completedCore = base ? `${base} ${selected.cmd}` : selected.cmd;
|
|
1113
|
+
const isComplete = trimmed === completedCore || trimmed.startsWith(`${completedCore} `);
|
|
1114
|
+
return { text: `${completedCore} `, isComplete };
|
|
1115
|
+
}
|
|
1116
|
+
const completedCore = selected.cmd;
|
|
1117
|
+
const hasChildren = selected.subcommands && selected.subcommands.length > 0;
|
|
1118
|
+
const isComplete =
|
|
1119
|
+
(trimmed === completedCore && (!hasChildren || endsWithSpace)) ||
|
|
1120
|
+
trimmed.startsWith(`${completedCore} `);
|
|
1121
|
+
return { text: `${completedCore} `, isComplete };
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
function applyCompletionPreview(preview) {
|
|
1125
|
+
input.value = preview.text;
|
|
1126
|
+
cursorPos = input.value.length;
|
|
1127
|
+
resetPreferredCol();
|
|
1128
|
+
input._updateCursor();
|
|
1129
|
+
updateDraftFromInput();
|
|
1130
|
+
screen.render();
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
function truncateText(text, maxWidth) {
|
|
1134
|
+
if (maxWidth <= 0) return "";
|
|
1135
|
+
if (text.length <= maxWidth) return text;
|
|
1136
|
+
if (maxWidth <= 3) return text.slice(0, maxWidth);
|
|
1137
|
+
return `${text.slice(0, maxWidth - 3)}...`;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
746
1140
|
function confirmCompletion() {
|
|
747
1141
|
if (!completionActive || completionCommands.length === 0) return;
|
|
748
1142
|
|
|
@@ -790,9 +1184,43 @@ async function runChat(projectRoot) {
|
|
|
790
1184
|
confirmCompletion();
|
|
791
1185
|
return true;
|
|
792
1186
|
}
|
|
1187
|
+
if (key.name === "pageup") {
|
|
1188
|
+
completionPageUp();
|
|
1189
|
+
return true;
|
|
1190
|
+
}
|
|
1191
|
+
if (key.name === "pagedown") {
|
|
1192
|
+
completionPageDown();
|
|
1193
|
+
return true;
|
|
1194
|
+
}
|
|
793
1195
|
if (key.name === "enter" || key.name === "return") {
|
|
794
|
-
|
|
1196
|
+
if (completionEnterSuppressed) {
|
|
1197
|
+
return true;
|
|
1198
|
+
}
|
|
1199
|
+
const selected = completionCommands[completionIndex];
|
|
1200
|
+
if (selected) {
|
|
1201
|
+
const preview = completionPreview(selected);
|
|
1202
|
+
if (!preview.isComplete) {
|
|
1203
|
+
applyCompletionPreview(preview);
|
|
1204
|
+
if (!selected.isSubcommand && selected.subcommands && selected.subcommands.length > 0) {
|
|
1205
|
+
showCompletion(input.value);
|
|
1206
|
+
} else {
|
|
1207
|
+
hideCompletion();
|
|
1208
|
+
}
|
|
1209
|
+
completionEnterSuppressed = true;
|
|
1210
|
+
if (completionEnterReset) clearImmediate(completionEnterReset);
|
|
1211
|
+
completionEnterReset = setImmediate(() => {
|
|
1212
|
+
completionEnterSuppressed = false;
|
|
1213
|
+
});
|
|
1214
|
+
return true;
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
// Already complete; allow normal submit
|
|
795
1218
|
hideCompletion();
|
|
1219
|
+
completionEnterSuppressed = true;
|
|
1220
|
+
if (completionEnterReset) clearImmediate(completionEnterReset);
|
|
1221
|
+
completionEnterReset = setImmediate(() => {
|
|
1222
|
+
completionEnterSuppressed = false;
|
|
1223
|
+
});
|
|
796
1224
|
return false;
|
|
797
1225
|
}
|
|
798
1226
|
if (key.name === "escape") {
|
|
@@ -816,7 +1244,7 @@ async function runChat(projectRoot) {
|
|
|
816
1244
|
|
|
817
1245
|
// Resize input box based on content
|
|
818
1246
|
function resizeInput() {
|
|
819
|
-
const innerWidth =
|
|
1247
|
+
const innerWidth = getWrapWidth();
|
|
820
1248
|
if (innerWidth <= 0) return;
|
|
821
1249
|
|
|
822
1250
|
const numLines = countLines(input.value, innerWidth);
|
|
@@ -833,13 +1261,21 @@ async function runChat(projectRoot) {
|
|
|
833
1261
|
// Reposition completion panel if active
|
|
834
1262
|
if (completionActive) {
|
|
835
1263
|
completionPanel.bottom = currentInputHeight - 1;
|
|
1264
|
+
// Re-clamp visible count for new available space
|
|
1265
|
+
const availableHeight = screen.height - currentInputHeight - 1;
|
|
1266
|
+
const maxVisible = Math.min(7, completionCommands.length);
|
|
1267
|
+
completionVisibleCount = Math.min(maxVisible, Math.max(1, availableHeight - 2));
|
|
1268
|
+
completionPanel.height = completionVisibleCount + 2;
|
|
1269
|
+
renderCompletionPanel();
|
|
836
1270
|
}
|
|
837
1271
|
// dashboard and inputBottomLine stay fixed at bottom 0 and 1
|
|
838
1272
|
logBox.height = Math.max(1, screen.height - currentInputHeight - 1);
|
|
1273
|
+
ensureInputCursorVisible();
|
|
839
1274
|
}
|
|
840
1275
|
|
|
841
1276
|
// Override the internal listener to support cursor movement
|
|
842
1277
|
input._listener = function(ch, key) {
|
|
1278
|
+
if (currentView === "agent") return; // Agent view handles keys at screen level
|
|
843
1279
|
if (key && key.ctrl && key.name === "c") {
|
|
844
1280
|
exitHandler();
|
|
845
1281
|
return;
|
|
@@ -848,20 +1284,27 @@ async function runChat(projectRoot) {
|
|
|
848
1284
|
return;
|
|
849
1285
|
}
|
|
850
1286
|
normalizeCommandPrefix();
|
|
851
|
-
if (key && (key.name === "pageup" || key.name === "pagedown")) {
|
|
852
|
-
const delta = Math.max(1, Math.floor(logBox.height / 2));
|
|
853
|
-
scrollLog(key.name === "pageup" ? -delta : delta);
|
|
854
|
-
return;
|
|
855
|
-
}
|
|
856
1287
|
if (focusMode === "dashboard") {
|
|
857
1288
|
if (handleDashboardKey(key)) return;
|
|
858
|
-
|
|
1289
|
+
// On agents view, printable char auto-exits dashboard keeping @target
|
|
1290
|
+
if (dashboardView === "agents" && ch && ch.length === 1 && !key.ctrl && !key.meta
|
|
1291
|
+
&& !/^[\x00-\x1f\x7f]$/.test(ch)) {
|
|
1292
|
+
exitDashboardMode(true);
|
|
1293
|
+
// Fall through to normal input handling so the char is inserted
|
|
1294
|
+
} else {
|
|
1295
|
+
return;
|
|
1296
|
+
}
|
|
859
1297
|
}
|
|
860
1298
|
|
|
861
1299
|
// Command completion mode
|
|
862
1300
|
if (completionActive) {
|
|
863
1301
|
if (handleCompletionKey(ch, key)) return;
|
|
864
1302
|
}
|
|
1303
|
+
if (key && (key.name === "pageup" || key.name === "pagedown")) {
|
|
1304
|
+
const delta = Math.max(1, Math.floor(logBox.height / 2));
|
|
1305
|
+
scrollLog(key.name === "pageup" ? -delta : delta);
|
|
1306
|
+
return;
|
|
1307
|
+
}
|
|
865
1308
|
|
|
866
1309
|
// Treat multi-char input (paste) as insertion, including newlines.
|
|
867
1310
|
if (ch && ch.length > 1 && (!key || !key.name || key.name.length !== 1)) {
|
|
@@ -888,6 +1331,7 @@ async function runChat(projectRoot) {
|
|
|
888
1331
|
if (key.name === "left") {
|
|
889
1332
|
if (cursorPos > 0) cursorPos--;
|
|
890
1333
|
resetPreferredCol();
|
|
1334
|
+
ensureInputCursorVisible();
|
|
891
1335
|
this._updateCursor();
|
|
892
1336
|
this.screen.render();
|
|
893
1337
|
return;
|
|
@@ -896,6 +1340,7 @@ async function runChat(projectRoot) {
|
|
|
896
1340
|
if (key.name === "right") {
|
|
897
1341
|
if (cursorPos < this.value.length) cursorPos++;
|
|
898
1342
|
resetPreferredCol();
|
|
1343
|
+
ensureInputCursorVisible();
|
|
899
1344
|
this._updateCursor();
|
|
900
1345
|
this.screen.render();
|
|
901
1346
|
return;
|
|
@@ -904,6 +1349,7 @@ async function runChat(projectRoot) {
|
|
|
904
1349
|
if (key.name === "home") {
|
|
905
1350
|
cursorPos = 0;
|
|
906
1351
|
resetPreferredCol();
|
|
1352
|
+
ensureInputCursorVisible();
|
|
907
1353
|
this._updateCursor();
|
|
908
1354
|
this.screen.render();
|
|
909
1355
|
return;
|
|
@@ -912,6 +1358,7 @@ async function runChat(projectRoot) {
|
|
|
912
1358
|
if (key.name === "end") {
|
|
913
1359
|
cursorPos = this.value.length;
|
|
914
1360
|
resetPreferredCol();
|
|
1361
|
+
ensureInputCursorVisible();
|
|
915
1362
|
this._updateCursor();
|
|
916
1363
|
this.screen.render();
|
|
917
1364
|
return;
|
|
@@ -936,7 +1383,7 @@ async function runChat(projectRoot) {
|
|
|
936
1383
|
}
|
|
937
1384
|
}
|
|
938
1385
|
if (key.name === "up" || key.name === "down") {
|
|
939
|
-
const innerWidth =
|
|
1386
|
+
const innerWidth = getWrapWidth();
|
|
940
1387
|
if (innerWidth > 0) {
|
|
941
1388
|
const { row, col } = getCursorRowCol(this.value, cursorPos, innerWidth);
|
|
942
1389
|
if (preferredCol === null) preferredCol = col;
|
|
@@ -953,6 +1400,7 @@ async function runChat(projectRoot) {
|
|
|
953
1400
|
: Math.min(totalRows - 1, row + 1);
|
|
954
1401
|
cursorPos = getCursorPosForRowCol(this.value, targetRow, preferredCol, innerWidth);
|
|
955
1402
|
}
|
|
1403
|
+
ensureInputCursorVisible();
|
|
956
1404
|
this._updateCursor();
|
|
957
1405
|
this.screen.render();
|
|
958
1406
|
return;
|
|
@@ -969,6 +1417,7 @@ async function runChat(projectRoot) {
|
|
|
969
1417
|
cursorPos--;
|
|
970
1418
|
resetPreferredCol();
|
|
971
1419
|
resizeInput();
|
|
1420
|
+
ensureInputCursorVisible();
|
|
972
1421
|
this._updateCursor();
|
|
973
1422
|
updateDraftFromInput();
|
|
974
1423
|
|
|
@@ -989,6 +1438,7 @@ async function runChat(projectRoot) {
|
|
|
989
1438
|
this.value = this.value.slice(0, cursorPos) + this.value.slice(cursorPos + 1);
|
|
990
1439
|
resetPreferredCol();
|
|
991
1440
|
resizeInput();
|
|
1441
|
+
ensureInputCursorVisible();
|
|
992
1442
|
this._updateCursor();
|
|
993
1443
|
this.screen.render();
|
|
994
1444
|
updateDraftFromInput();
|
|
@@ -1025,27 +1475,16 @@ async function runChat(projectRoot) {
|
|
|
1025
1475
|
input._updateCursor = function() {
|
|
1026
1476
|
if (this.screen.focused !== this) return;
|
|
1027
1477
|
|
|
1028
|
-
|
|
1478
|
+
let lpos;
|
|
1479
|
+
try { lpos = this._getCoords(); } catch { return; }
|
|
1029
1480
|
if (!lpos) return;
|
|
1030
1481
|
|
|
1031
|
-
const innerWidth =
|
|
1482
|
+
const innerWidth = getWrapWidth();
|
|
1032
1483
|
if (innerWidth <= 0) return;
|
|
1033
1484
|
|
|
1485
|
+
ensureInputCursorVisible();
|
|
1034
1486
|
const { row, col } = getCursorRowCol(this.value, cursorPos, innerWidth);
|
|
1035
|
-
const
|
|
1036
|
-
|
|
1037
|
-
let scrollOffset = this.childBase || 0;
|
|
1038
|
-
if (row < scrollOffset) {
|
|
1039
|
-
scrollOffset = row;
|
|
1040
|
-
} else if (row >= scrollOffset + innerHeight) {
|
|
1041
|
-
scrollOffset = row - innerHeight + 1;
|
|
1042
|
-
}
|
|
1043
|
-
if (scrollOffset !== this.childBase) {
|
|
1044
|
-
this.childBase = scrollOffset;
|
|
1045
|
-
if (typeof this.scrollTo === "function") {
|
|
1046
|
-
this.scrollTo(scrollOffset);
|
|
1047
|
-
}
|
|
1048
|
-
}
|
|
1487
|
+
const scrollOffset = this.childBase || 0;
|
|
1049
1488
|
|
|
1050
1489
|
const displayRow = row - scrollOffset;
|
|
1051
1490
|
const safeCol = Math.min(Math.max(0, col), innerWidth - 1);
|
|
@@ -1081,54 +1520,101 @@ async function runChat(projectRoot) {
|
|
|
1081
1520
|
let completionCommands = [];
|
|
1082
1521
|
let completionIndex = 0;
|
|
1083
1522
|
let completionScrollOffset = 0;
|
|
1523
|
+
let completionVisibleCount = 0;
|
|
1524
|
+
let completionEnterSuppressed = false;
|
|
1525
|
+
let completionEnterReset = null;
|
|
1084
1526
|
|
|
1085
|
-
const
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1527
|
+
const COMMAND_TREE = {
|
|
1528
|
+
"/bus": {
|
|
1529
|
+
desc: "Event bus operations",
|
|
1530
|
+
children: {
|
|
1531
|
+
activate: { desc: "Activate agent terminal" },
|
|
1532
|
+
list: { desc: "List all agents" },
|
|
1533
|
+
rename: { desc: "Rename agent nickname" },
|
|
1534
|
+
send: { desc: "Send message to agent" },
|
|
1535
|
+
status: { desc: "Bus status" },
|
|
1536
|
+
},
|
|
1537
|
+
},
|
|
1538
|
+
"/ctx": {
|
|
1539
|
+
desc: "Context management",
|
|
1540
|
+
children: {
|
|
1541
|
+
decisions: { desc: "List all decisions" },
|
|
1542
|
+
doctor: { desc: "Check context integrity" },
|
|
1543
|
+
status: { desc: "Show context status (default)" },
|
|
1544
|
+
},
|
|
1545
|
+
},
|
|
1546
|
+
"/daemon": {
|
|
1090
1547
|
desc: "Daemon management",
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1548
|
+
children: {
|
|
1549
|
+
restart: { desc: "Restart daemon" },
|
|
1550
|
+
start: { desc: "Start daemon" },
|
|
1551
|
+
status: { desc: "Daemon status" },
|
|
1552
|
+
stop: { desc: "Stop daemon" },
|
|
1553
|
+
},
|
|
1097
1554
|
},
|
|
1098
|
-
|
|
1099
|
-
{
|
|
1100
|
-
|
|
1101
|
-
desc: "
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
{ cmd: "status", desc: "Bus status" },
|
|
1107
|
-
]
|
|
1555
|
+
"/doctor": { desc: "Health check diagnostics" },
|
|
1556
|
+
"/init": { desc: "Initialize modules" },
|
|
1557
|
+
"/launch": {
|
|
1558
|
+
desc: "Launch new agent",
|
|
1559
|
+
children: {
|
|
1560
|
+
claude: { desc: "Launch Claude agent" },
|
|
1561
|
+
codex: { desc: "Launch Codex agent" },
|
|
1562
|
+
},
|
|
1108
1563
|
},
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1564
|
+
"/resume": { desc: "Resume agents (optional nickname)" },
|
|
1565
|
+
"/skills": {
|
|
1566
|
+
desc: "Skills management",
|
|
1567
|
+
children: {
|
|
1568
|
+
install: { desc: "Install skills (use: all or name)" },
|
|
1569
|
+
list: { desc: "List available skills" },
|
|
1570
|
+
},
|
|
1571
|
+
},
|
|
1572
|
+
"/status": { desc: "Status display" },
|
|
1573
|
+
};
|
|
1574
|
+
|
|
1575
|
+
function buildCommandRegistry(tree) {
|
|
1576
|
+
return Object.keys(tree)
|
|
1577
|
+
.sort((a, b) => a.localeCompare(b, "en", { sensitivity: "base" }))
|
|
1578
|
+
.map((cmd) => {
|
|
1579
|
+
const node = tree[cmd] || {};
|
|
1580
|
+
const entry = { cmd, desc: node.desc || "" };
|
|
1581
|
+
if (node.children) {
|
|
1582
|
+
entry.subcommands = Object.keys(node.children)
|
|
1583
|
+
.sort((a, b) => a.localeCompare(b, "en", { sensitivity: "base" }))
|
|
1584
|
+
.map((sub) => ({
|
|
1585
|
+
cmd: sub,
|
|
1586
|
+
desc: (node.children[sub] && node.children[sub].desc) || "",
|
|
1587
|
+
}));
|
|
1588
|
+
}
|
|
1589
|
+
return entry;
|
|
1590
|
+
});
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
const COMMAND_REGISTRY = buildCommandRegistry(COMMAND_TREE);
|
|
1116
1594
|
|
|
1117
1595
|
// Agent selection state
|
|
1118
1596
|
let activeAgents = [];
|
|
1119
1597
|
let activeAgentLabelMap = new Map();
|
|
1598
|
+
let activeAgentMetaMap = new Map(); // Store full meta including launch_mode
|
|
1120
1599
|
let agentListWindowStart = 0;
|
|
1121
1600
|
const MAX_AGENT_WINDOW = 5;
|
|
1122
1601
|
let selectedAgentIndex = -1; // -1 = not in dashboard selection mode
|
|
1123
1602
|
let targetAgent = null; // Selected agent for direct messaging
|
|
1124
1603
|
let focusMode = "input"; // "input" or "dashboard"
|
|
1125
|
-
let dashboardView = "agents"; // "agents"
|
|
1126
|
-
|
|
1604
|
+
let dashboardView = "agents"; // "agents" | "mode" | "provider" | "resume"
|
|
1605
|
+
const launchModes = ["auto", "terminal", "tmux", "internal"];
|
|
1606
|
+
function modeToIndex(m) { const i = launchModes.indexOf(m); return i >= 0 ? i : 0; }
|
|
1607
|
+
let selectedModeIndex = modeToIndex(launchMode);
|
|
1127
1608
|
const providerOptions = [
|
|
1128
1609
|
{ label: "codex", value: "codex-cli" },
|
|
1129
1610
|
{ label: "claude", value: "claude-cli" },
|
|
1130
1611
|
];
|
|
1131
1612
|
let selectedProviderIndex = agentProvider === "claude-cli" ? 1 : 0;
|
|
1613
|
+
const resumeOptions = [
|
|
1614
|
+
{ label: "Auto", value: true },
|
|
1615
|
+
{ label: "Off", value: false },
|
|
1616
|
+
];
|
|
1617
|
+
let selectedResumeIndex = autoResume ? 0 : 1;
|
|
1132
1618
|
let restartInProgress = false;
|
|
1133
1619
|
|
|
1134
1620
|
function getAgentLabel(agentId) {
|
|
@@ -1154,7 +1640,11 @@ async function runChat(projectRoot) {
|
|
|
1154
1640
|
}
|
|
1155
1641
|
|
|
1156
1642
|
function send(req) {
|
|
1157
|
-
if (!client || client.destroyed)
|
|
1643
|
+
if (!client || client.destroyed) {
|
|
1644
|
+
enqueueRequest(req);
|
|
1645
|
+
void ensureConnected();
|
|
1646
|
+
return;
|
|
1647
|
+
}
|
|
1158
1648
|
client.write(`${JSON.stringify(req)}\n`);
|
|
1159
1649
|
}
|
|
1160
1650
|
|
|
@@ -1193,30 +1683,66 @@ async function runChat(projectRoot) {
|
|
|
1193
1683
|
function setLaunchMode(mode) {
|
|
1194
1684
|
const next = normalizeLaunchMode(mode);
|
|
1195
1685
|
if (next === launchMode) return;
|
|
1686
|
+
// Check tmux availability before switching
|
|
1687
|
+
if (next === "tmux" && !process.env.TMUX) {
|
|
1688
|
+
logMessage("error", "{red-fg}✗{/red-fg} tmux mode requires running inside a tmux session");
|
|
1689
|
+
return;
|
|
1690
|
+
}
|
|
1196
1691
|
launchMode = next;
|
|
1197
|
-
selectedModeIndex = launchMode
|
|
1692
|
+
selectedModeIndex = modeToIndex(launchMode);
|
|
1198
1693
|
saveConfig(projectRoot, { launchMode });
|
|
1199
1694
|
logMessage("status", `{magenta-fg}⚙{/magenta-fg} Launch mode: ${launchMode}`);
|
|
1200
1695
|
renderDashboard();
|
|
1201
1696
|
screen.render();
|
|
1697
|
+
void restartDaemon();
|
|
1202
1698
|
}
|
|
1203
1699
|
|
|
1700
|
+
|
|
1204
1701
|
function providerLabel(value) {
|
|
1205
1702
|
return value === "claude-cli" ? "claude" : "codex";
|
|
1206
1703
|
}
|
|
1207
1704
|
|
|
1705
|
+
function clearUfooAgentIdentity() {
|
|
1706
|
+
const agentDir = getUfooPaths(projectRoot).agentDir;
|
|
1707
|
+
const stateFile = path.join(agentDir, "ufoo-agent.json");
|
|
1708
|
+
const historyFile = path.join(agentDir, "ufoo-agent.history.jsonl");
|
|
1709
|
+
try {
|
|
1710
|
+
fs.rmSync(stateFile, { force: true });
|
|
1711
|
+
} catch {
|
|
1712
|
+
// ignore
|
|
1713
|
+
}
|
|
1714
|
+
try {
|
|
1715
|
+
fs.rmSync(historyFile, { force: true });
|
|
1716
|
+
} catch {
|
|
1717
|
+
// ignore
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1208
1721
|
function setAgentProvider(provider) {
|
|
1209
1722
|
const next = normalizeAgentProvider(provider);
|
|
1210
1723
|
if (next === agentProvider) return;
|
|
1211
1724
|
agentProvider = next;
|
|
1212
1725
|
selectedProviderIndex = agentProvider === "claude-cli" ? 1 : 0;
|
|
1213
1726
|
saveConfig(projectRoot, { agentProvider });
|
|
1727
|
+
clearUfooAgentIdentity();
|
|
1214
1728
|
logMessage("status", `{magenta-fg}⚙{/magenta-fg} ufoo-agent: ${providerLabel(agentProvider)}`);
|
|
1215
1729
|
renderDashboard();
|
|
1216
1730
|
screen.render();
|
|
1217
1731
|
void restartDaemon();
|
|
1218
1732
|
}
|
|
1219
1733
|
|
|
1734
|
+
function setAutoResume(value) {
|
|
1735
|
+
const next = value !== false;
|
|
1736
|
+
if (next === autoResume) return;
|
|
1737
|
+
autoResume = next;
|
|
1738
|
+
selectedResumeIndex = autoResume ? 0 : 1;
|
|
1739
|
+
saveConfig(projectRoot, { autoResume });
|
|
1740
|
+
const label = autoResume ? "Auto" : "Off";
|
|
1741
|
+
logMessage("status", `{magenta-fg}⚙{/magenta-fg} Resume: ${label}`);
|
|
1742
|
+
renderDashboard();
|
|
1743
|
+
screen.render();
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1220
1746
|
async function restartDaemon() {
|
|
1221
1747
|
if (restartInProgress) return;
|
|
1222
1748
|
restartInProgress = true;
|
|
@@ -1231,7 +1757,7 @@ async function runChat(projectRoot) {
|
|
|
1231
1757
|
}
|
|
1232
1758
|
}
|
|
1233
1759
|
stopDaemon(projectRoot);
|
|
1234
|
-
startDaemon(projectRoot);
|
|
1760
|
+
startDaemon(projectRoot, { forceResume: true });
|
|
1235
1761
|
const newClient = await connectClient();
|
|
1236
1762
|
if (newClient) {
|
|
1237
1763
|
attachClient(newClient);
|
|
@@ -1256,11 +1782,13 @@ async function runChat(projectRoot) {
|
|
|
1256
1782
|
let content = " ";
|
|
1257
1783
|
if (focusMode === "dashboard") {
|
|
1258
1784
|
if (dashboardView === "mode") {
|
|
1259
|
-
const
|
|
1260
|
-
const modeParts = modes.map((mode, i) => {
|
|
1785
|
+
const modeParts = launchModes.map((mode, i) => {
|
|
1261
1786
|
if (i === selectedModeIndex) {
|
|
1262
1787
|
return `{inverse}${mode}{/inverse}`;
|
|
1263
1788
|
}
|
|
1789
|
+
if (mode === launchMode) {
|
|
1790
|
+
return `{bold}{cyan-fg}${mode}{/cyan-fg}{/bold}`;
|
|
1791
|
+
}
|
|
1264
1792
|
return `{cyan-fg}${mode}{/cyan-fg}`;
|
|
1265
1793
|
});
|
|
1266
1794
|
content += `{gray-fg}Mode:{/gray-fg} ${modeParts.join(" ")}`;
|
|
@@ -1270,9 +1798,24 @@ async function runChat(projectRoot) {
|
|
|
1270
1798
|
if (i === selectedProviderIndex) {
|
|
1271
1799
|
return `{inverse}${opt.label}{/inverse}`;
|
|
1272
1800
|
}
|
|
1801
|
+
if (opt.value === agentProvider) {
|
|
1802
|
+
return `{bold}{cyan-fg}${opt.label}{/cyan-fg}{/bold}`;
|
|
1803
|
+
}
|
|
1273
1804
|
return `{cyan-fg}${opt.label}{/cyan-fg}`;
|
|
1274
1805
|
});
|
|
1275
1806
|
content += `{gray-fg}Agent:{/gray-fg} ${providerParts.join(" ")}`;
|
|
1807
|
+
content += " {gray-fg}│ ←/→ select, Enter confirm, ↓ resume, ↑ back{/gray-fg}";
|
|
1808
|
+
} else if (dashboardView === "resume") {
|
|
1809
|
+
const resumeParts = resumeOptions.map((opt, i) => {
|
|
1810
|
+
if (i === selectedResumeIndex) {
|
|
1811
|
+
return `{inverse}${opt.label}{/inverse}`;
|
|
1812
|
+
}
|
|
1813
|
+
if (opt.value === autoResume) {
|
|
1814
|
+
return `{bold}{cyan-fg}${opt.label}{/cyan-fg}{/bold}`;
|
|
1815
|
+
}
|
|
1816
|
+
return `{cyan-fg}${opt.label}{/cyan-fg}`;
|
|
1817
|
+
});
|
|
1818
|
+
content += `{gray-fg}Resume:{/gray-fg} ${resumeParts.join(" ")}`;
|
|
1276
1819
|
content += " {gray-fg}│ ←/→ select, Enter confirm, ↑ back{/gray-fg}";
|
|
1277
1820
|
} else {
|
|
1278
1821
|
if (activeAgents.length > 0) {
|
|
@@ -1293,7 +1836,7 @@ async function runChat(projectRoot) {
|
|
|
1293
1836
|
const rightMore = end < activeAgents.length ? " {gray-fg}»{/gray-fg}" : "";
|
|
1294
1837
|
content += `{gray-fg}Agents:{/gray-fg} ${agentParts.join(" ")}`;
|
|
1295
1838
|
content = `${content.replace("{gray-fg}Agents:{/gray-fg} ", `{gray-fg}Agents:{/gray-fg} ${leftMore}`)}${rightMore}`;
|
|
1296
|
-
content += " {gray-fg}│ ←/→ select, Enter confirm, ↓ mode, ↑ back{/gray-fg}";
|
|
1839
|
+
content += " {gray-fg}│ ←/→ select, Enter confirm, ^X close, ↓ mode, ↑ back{/gray-fg}";
|
|
1297
1840
|
} else {
|
|
1298
1841
|
content += "{gray-fg}Agents:{/gray-fg} {cyan-fg}none{/cyan-fg}";
|
|
1299
1842
|
content += " {gray-fg}│ ↓ mode, ↑ back{/gray-fg}";
|
|
@@ -1302,11 +1845,15 @@ async function runChat(projectRoot) {
|
|
|
1302
1845
|
} else {
|
|
1303
1846
|
// Normal dashboard display (input mode)
|
|
1304
1847
|
const agents = activeAgents.length > 0
|
|
1305
|
-
? activeAgents.slice(0, 3).map((id) =>
|
|
1848
|
+
? activeAgents.slice(0, 3).map((id) => {
|
|
1849
|
+
const label = getAgentLabel(id);
|
|
1850
|
+
return label;
|
|
1851
|
+
}).join(", ") + (activeAgents.length > 3 ? ` +${activeAgents.length - 3}` : "")
|
|
1306
1852
|
: "none";
|
|
1307
1853
|
content += `{gray-fg}Agents:{/gray-fg} {cyan-fg}${agents}{/cyan-fg}`;
|
|
1308
1854
|
content += ` {gray-fg}Mode:{/gray-fg} {cyan-fg}${launchMode}{/cyan-fg}`;
|
|
1309
1855
|
content += ` {gray-fg}Agent:{/gray-fg} {cyan-fg}${providerLabel(agentProvider)}{/cyan-fg}`;
|
|
1856
|
+
content += ` {gray-fg}Resume:{/gray-fg} {cyan-fg}${autoResume ? "auto" : "off"}{/cyan-fg}`;
|
|
1310
1857
|
}
|
|
1311
1858
|
dashboard.setContent(content);
|
|
1312
1859
|
}
|
|
@@ -1315,13 +1862,14 @@ async function runChat(projectRoot) {
|
|
|
1315
1862
|
activeAgents = status.active || [];
|
|
1316
1863
|
const metaList = Array.isArray(status.active_meta) ? status.active_meta : [];
|
|
1317
1864
|
activeAgentLabelMap = new Map();
|
|
1865
|
+
activeAgentMetaMap = new Map();
|
|
1318
1866
|
let fallbackMap = null;
|
|
1319
1867
|
if (metaList.length === 0 && activeAgents.length > 0) {
|
|
1320
1868
|
try {
|
|
1321
|
-
const busPath =
|
|
1869
|
+
const busPath = getUfooPaths(projectRoot).agentsFile;
|
|
1322
1870
|
const bus = JSON.parse(fs.readFileSync(busPath, "utf8"));
|
|
1323
1871
|
fallbackMap = new Map();
|
|
1324
|
-
for (const [id, meta] of Object.entries(bus.
|
|
1872
|
+
for (const [id, meta] of Object.entries(bus.agents || {})) {
|
|
1325
1873
|
if (meta && meta.nickname) fallbackMap.set(id, meta.nickname);
|
|
1326
1874
|
}
|
|
1327
1875
|
} catch {
|
|
@@ -1334,8 +1882,31 @@ async function runChat(projectRoot) {
|
|
|
1334
1882
|
? meta.nickname
|
|
1335
1883
|
: (fallbackMap && fallbackMap.get(id)) || id;
|
|
1336
1884
|
activeAgentLabelMap.set(id, label);
|
|
1885
|
+
if (meta) {
|
|
1886
|
+
activeAgentMetaMap.set(id, meta);
|
|
1887
|
+
}
|
|
1337
1888
|
}
|
|
1338
1889
|
clampAgentWindow();
|
|
1890
|
+
|
|
1891
|
+
// Check if viewed agent went offline
|
|
1892
|
+
if (currentView === "agent" && viewingAgent && !activeAgents.includes(viewingAgent)) {
|
|
1893
|
+
writeToAgentTerm("\r\n\x1b[1;31m[Agent went offline]\x1b[0m\r\n");
|
|
1894
|
+
exitAgentView();
|
|
1895
|
+
return;
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
// In agent view, only update the dashboard bar (via ANSI, blessed is frozen)
|
|
1899
|
+
if (currentView === "agent") {
|
|
1900
|
+
if (focusMode === "dashboard") {
|
|
1901
|
+
const totalItems = 1 + activeAgents.length;
|
|
1902
|
+
if (selectedAgentIndex < 0 || selectedAgentIndex >= totalItems) {
|
|
1903
|
+
selectedAgentIndex = 0;
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
renderAgentDashboard();
|
|
1907
|
+
return;
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1339
1910
|
if (focusMode === "dashboard") {
|
|
1340
1911
|
if (dashboardView === "agents") {
|
|
1341
1912
|
if (activeAgents.length === 0) {
|
|
@@ -1356,8 +1927,14 @@ async function runChat(projectRoot) {
|
|
|
1356
1927
|
selectedAgentIndex = activeAgents.length > 0 ? 0 : -1;
|
|
1357
1928
|
agentListWindowStart = 0;
|
|
1358
1929
|
clampAgentWindow();
|
|
1359
|
-
selectedModeIndex = launchMode
|
|
1930
|
+
selectedModeIndex = modeToIndex(launchMode);
|
|
1360
1931
|
selectedProviderIndex = agentProvider === "claude-cli" ? 1 : 0;
|
|
1932
|
+
selectedResumeIndex = autoResume ? 0 : 1;
|
|
1933
|
+
// Immediately set @target when first agent is selected
|
|
1934
|
+
if (selectedAgentIndex >= 0 && selectedAgentIndex < activeAgents.length) {
|
|
1935
|
+
targetAgent = activeAgents[selectedAgentIndex];
|
|
1936
|
+
updatePromptBox();
|
|
1937
|
+
}
|
|
1361
1938
|
screen.grabKeys = true;
|
|
1362
1939
|
renderDashboard();
|
|
1363
1940
|
screen.program.hideCursor();
|
|
@@ -1366,15 +1943,88 @@ async function runChat(projectRoot) {
|
|
|
1366
1943
|
|
|
1367
1944
|
function handleDashboardKey(key) {
|
|
1368
1945
|
if (!key || focusMode !== "dashboard") return false;
|
|
1946
|
+
|
|
1947
|
+
// Agent TTY view dashboard navigation
|
|
1948
|
+
// Items: [ufoo(0), agent1(1), agent2(2), ...]
|
|
1949
|
+
if (currentView === "agent") {
|
|
1950
|
+
const totalItems = 1 + activeAgents.length; // ufoo + agents
|
|
1951
|
+
if (key.name === "left") {
|
|
1952
|
+
if (selectedAgentIndex > 0) {
|
|
1953
|
+
selectedAgentIndex--;
|
|
1954
|
+
}
|
|
1955
|
+
renderAgentDashboard();
|
|
1956
|
+
return true;
|
|
1957
|
+
}
|
|
1958
|
+
if (key.name === "right") {
|
|
1959
|
+
if (selectedAgentIndex < totalItems - 1) {
|
|
1960
|
+
selectedAgentIndex++;
|
|
1961
|
+
}
|
|
1962
|
+
renderAgentDashboard();
|
|
1963
|
+
return true;
|
|
1964
|
+
}
|
|
1965
|
+
if (key.name === "enter" || key.name === "return") {
|
|
1966
|
+
if (selectedAgentIndex === 0) {
|
|
1967
|
+
// "ufoo" selected -> exit agent view back to main chat
|
|
1968
|
+
exitAgentView();
|
|
1969
|
+
} else {
|
|
1970
|
+
// Another agent selected -> switch based on launch mode
|
|
1971
|
+
const agentId = activeAgents[selectedAgentIndex - 1];
|
|
1972
|
+
if (agentId && agentId !== viewingAgent) {
|
|
1973
|
+
const meta = activeAgentMetaMap.get(agentId);
|
|
1974
|
+
const agentLaunchMode = meta?.launch_mode || "";
|
|
1975
|
+
|
|
1976
|
+
if (agentLaunchMode === "tmux" || agentLaunchMode === "terminal") {
|
|
1977
|
+
// Exit PTY view, then activate agent's terminal/pane
|
|
1978
|
+
exitAgentView();
|
|
1979
|
+
try {
|
|
1980
|
+
const activator = new AgentActivator(projectRoot);
|
|
1981
|
+
activator.activate(agentId).catch(() => {});
|
|
1982
|
+
} catch { /* ignore */ }
|
|
1983
|
+
} else {
|
|
1984
|
+
// Internal mode: switch PTY view
|
|
1985
|
+
focusMode = "input";
|
|
1986
|
+
enterAgentView(agentId);
|
|
1987
|
+
}
|
|
1988
|
+
} else {
|
|
1989
|
+
// Same agent, just exit dashboard
|
|
1990
|
+
focusMode = "input";
|
|
1991
|
+
renderAgentDashboard();
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
return true;
|
|
1995
|
+
}
|
|
1996
|
+
if (key.name === "up") {
|
|
1997
|
+
// Up exits dashboard back to agent PTY view
|
|
1998
|
+
focusMode = "input";
|
|
1999
|
+
renderAgentDashboard();
|
|
2000
|
+
return true;
|
|
2001
|
+
}
|
|
2002
|
+
if (key.name === "x" && key.ctrl) {
|
|
2003
|
+
// Ctrl+x: close selected agent (not ufoo)
|
|
2004
|
+
if (selectedAgentIndex > 0 && selectedAgentIndex <= activeAgents.length) {
|
|
2005
|
+
const agentId = activeAgents[selectedAgentIndex - 1];
|
|
2006
|
+
const label = getAgentLabel(agentId);
|
|
2007
|
+
// If closing the currently viewed agent, exit view first
|
|
2008
|
+
if (agentId === viewingAgent) {
|
|
2009
|
+
exitAgentView();
|
|
2010
|
+
}
|
|
2011
|
+
closeAgentViaDaemon(agentId, label);
|
|
2012
|
+
}
|
|
2013
|
+
return true;
|
|
2014
|
+
}
|
|
2015
|
+
return true;
|
|
2016
|
+
}
|
|
2017
|
+
|
|
1369
2018
|
if (dashboardView === "mode") {
|
|
2019
|
+
const maxMode = launchModes.length - 1;
|
|
1370
2020
|
if (key.name === "left") {
|
|
1371
|
-
selectedModeIndex = selectedModeIndex <= 0 ?
|
|
2021
|
+
selectedModeIndex = selectedModeIndex <= 0 ? maxMode : selectedModeIndex - 1;
|
|
1372
2022
|
renderDashboard();
|
|
1373
2023
|
screen.render();
|
|
1374
2024
|
return true;
|
|
1375
2025
|
}
|
|
1376
2026
|
if (key.name === "right") {
|
|
1377
|
-
selectedModeIndex = selectedModeIndex >=
|
|
2027
|
+
selectedModeIndex = selectedModeIndex >= maxMode ? 0 : selectedModeIndex + 1;
|
|
1378
2028
|
renderDashboard();
|
|
1379
2029
|
screen.render();
|
|
1380
2030
|
return true;
|
|
@@ -1388,13 +2038,17 @@ async function runChat(projectRoot) {
|
|
|
1388
2038
|
}
|
|
1389
2039
|
if (key.name === "up") {
|
|
1390
2040
|
dashboardView = "agents";
|
|
2041
|
+
// Restore @target when returning to agents page
|
|
2042
|
+
if (selectedAgentIndex >= 0 && selectedAgentIndex < activeAgents.length) {
|
|
2043
|
+
targetAgent = activeAgents[selectedAgentIndex];
|
|
2044
|
+
updatePromptBox();
|
|
2045
|
+
}
|
|
1391
2046
|
renderDashboard();
|
|
1392
2047
|
screen.render();
|
|
1393
2048
|
return true;
|
|
1394
2049
|
}
|
|
1395
2050
|
if (key.name === "enter" || key.name === "return") {
|
|
1396
|
-
|
|
1397
|
-
setLaunchMode(modes[selectedModeIndex]);
|
|
2051
|
+
setLaunchMode(launchModes[selectedModeIndex]);
|
|
1398
2052
|
exitDashboardMode(false);
|
|
1399
2053
|
return true;
|
|
1400
2054
|
}
|
|
@@ -1417,6 +2071,13 @@ async function runChat(projectRoot) {
|
|
|
1417
2071
|
screen.render();
|
|
1418
2072
|
return true;
|
|
1419
2073
|
}
|
|
2074
|
+
if (key.name === "down") {
|
|
2075
|
+
dashboardView = "resume";
|
|
2076
|
+
selectedResumeIndex = autoResume ? 0 : 1;
|
|
2077
|
+
renderDashboard();
|
|
2078
|
+
screen.render();
|
|
2079
|
+
return true;
|
|
2080
|
+
}
|
|
1420
2081
|
if (key.name === "up") {
|
|
1421
2082
|
dashboardView = "mode";
|
|
1422
2083
|
renderDashboard();
|
|
@@ -1435,38 +2096,124 @@ async function runChat(projectRoot) {
|
|
|
1435
2096
|
}
|
|
1436
2097
|
return true;
|
|
1437
2098
|
}
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
selectedAgentIndex--;
|
|
1442
|
-
clampAgentWindow();
|
|
2099
|
+
if (dashboardView === "resume") {
|
|
2100
|
+
if (key.name === "left") {
|
|
2101
|
+
selectedResumeIndex = selectedResumeIndex <= 0 ? resumeOptions.length - 1 : selectedResumeIndex - 1;
|
|
1443
2102
|
renderDashboard();
|
|
1444
2103
|
screen.render();
|
|
2104
|
+
return true;
|
|
1445
2105
|
}
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
2106
|
+
if (key.name === "right") {
|
|
2107
|
+
selectedResumeIndex = selectedResumeIndex >= resumeOptions.length - 1 ? 0 : selectedResumeIndex + 1;
|
|
2108
|
+
renderDashboard();
|
|
2109
|
+
screen.render();
|
|
2110
|
+
return true;
|
|
2111
|
+
}
|
|
2112
|
+
if (key.name === "up") {
|
|
2113
|
+
dashboardView = "provider";
|
|
2114
|
+
renderDashboard();
|
|
2115
|
+
screen.render();
|
|
2116
|
+
return true;
|
|
2117
|
+
}
|
|
2118
|
+
if (key.name === "enter" || key.name === "return") {
|
|
2119
|
+
const selected = resumeOptions[selectedResumeIndex];
|
|
2120
|
+
if (selected) {
|
|
2121
|
+
setAutoResume(selected.value);
|
|
2122
|
+
const label = selected.value ? "Auto" : "Off";
|
|
2123
|
+
logMessage("status", `{magenta-fg}⚙{/magenta-fg} Resume: ${label}`);
|
|
2124
|
+
}
|
|
2125
|
+
exitDashboardMode(false);
|
|
2126
|
+
return true;
|
|
2127
|
+
}
|
|
2128
|
+
if (key.name === "escape") {
|
|
2129
|
+
exitDashboardMode(false);
|
|
2130
|
+
return true;
|
|
2131
|
+
}
|
|
2132
|
+
return true;
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
if (key.name === "left") {
|
|
2136
|
+
if (activeAgents.length > 0 && selectedAgentIndex > 0) {
|
|
2137
|
+
selectedAgentIndex--;
|
|
2138
|
+
clampAgentWindow();
|
|
2139
|
+
// Update @target in real-time as user navigates
|
|
2140
|
+
targetAgent = activeAgents[selectedAgentIndex];
|
|
2141
|
+
updatePromptBox();
|
|
2142
|
+
renderDashboard();
|
|
2143
|
+
screen.render();
|
|
2144
|
+
}
|
|
2145
|
+
return true;
|
|
2146
|
+
}
|
|
2147
|
+
if (key.name === "right") {
|
|
1449
2148
|
if (activeAgents.length > 0 && selectedAgentIndex < activeAgents.length - 1) {
|
|
1450
2149
|
selectedAgentIndex++;
|
|
1451
2150
|
clampAgentWindow();
|
|
2151
|
+
// Update @target in real-time as user navigates
|
|
2152
|
+
targetAgent = activeAgents[selectedAgentIndex];
|
|
2153
|
+
updatePromptBox();
|
|
1452
2154
|
renderDashboard();
|
|
1453
2155
|
screen.render();
|
|
1454
2156
|
}
|
|
1455
2157
|
return true;
|
|
1456
2158
|
}
|
|
1457
2159
|
if (key.name === "down") {
|
|
2160
|
+
// Leaving agents page: clear temporary @target
|
|
2161
|
+
clearTargetAgent();
|
|
1458
2162
|
dashboardView = "mode";
|
|
1459
|
-
selectedModeIndex = launchMode
|
|
2163
|
+
selectedModeIndex = modeToIndex(launchMode);
|
|
1460
2164
|
renderDashboard();
|
|
1461
2165
|
screen.render();
|
|
1462
2166
|
return true;
|
|
1463
2167
|
}
|
|
1464
2168
|
if (key.name === "up" || key.name === "escape") {
|
|
2169
|
+
// Cancel: clear @target, back to normal chat
|
|
2170
|
+
clearTargetAgent();
|
|
1465
2171
|
exitDashboardMode(false);
|
|
1466
2172
|
return true;
|
|
1467
2173
|
}
|
|
2174
|
+
if (key.name === "x" && key.ctrl) {
|
|
2175
|
+
// Ctrl+x: close selected agent
|
|
2176
|
+
if (selectedAgentIndex >= 0 && selectedAgentIndex < activeAgents.length) {
|
|
2177
|
+
const agentId = activeAgents[selectedAgentIndex];
|
|
2178
|
+
const label = getAgentLabel(agentId);
|
|
2179
|
+
closeAgentViaDaemon(agentId, label);
|
|
2180
|
+
clearTargetAgent();
|
|
2181
|
+
exitDashboardMode(false);
|
|
2182
|
+
}
|
|
2183
|
+
return true;
|
|
2184
|
+
}
|
|
1468
2185
|
if (key.name === "enter" || key.name === "return") {
|
|
1469
|
-
|
|
2186
|
+
// Enter: action depends on agent's launch mode
|
|
2187
|
+
if (selectedAgentIndex >= 0 && selectedAgentIndex < activeAgents.length) {
|
|
2188
|
+
const agentId = activeAgents[selectedAgentIndex];
|
|
2189
|
+
const meta = activeAgentMetaMap.get(agentId);
|
|
2190
|
+
const agentLaunchMode = meta?.launch_mode || "";
|
|
2191
|
+
|
|
2192
|
+
if (agentLaunchMode === "tmux" || agentLaunchMode === "terminal") {
|
|
2193
|
+
// Tmux: select pane; Terminal: activate tab/window by tty
|
|
2194
|
+
clearTargetAgent();
|
|
2195
|
+
exitDashboardMode(false);
|
|
2196
|
+
try {
|
|
2197
|
+
const activator = new AgentActivator(projectRoot);
|
|
2198
|
+
activator.activate(agentId).catch(() => {});
|
|
2199
|
+
} catch { /* ignore */ }
|
|
2200
|
+
return true;
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2203
|
+
// Internal / internal-pty mode: enter PTY view if inject.sock exists
|
|
2204
|
+
const sockPath = getInjectSockPath(agentId);
|
|
2205
|
+
if (fs.existsSync(sockPath)) {
|
|
2206
|
+
clearTargetAgent();
|
|
2207
|
+
focusMode = "input";
|
|
2208
|
+
dashboardView = "agents";
|
|
2209
|
+
selectedAgentIndex = -1;
|
|
2210
|
+
screen.grabKeys = false;
|
|
2211
|
+
enterAgentView(agentId);
|
|
2212
|
+
return true;
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
// Fallback: just exit dashboard, keep @target for messaging
|
|
2216
|
+
exitDashboardMode(false);
|
|
1470
2217
|
return true;
|
|
1471
2218
|
}
|
|
1472
2219
|
return false;
|
|
@@ -1492,6 +2239,312 @@ async function runChat(projectRoot) {
|
|
|
1492
2239
|
screen.render();
|
|
1493
2240
|
}
|
|
1494
2241
|
|
|
2242
|
+
function getInjectSockPath(agentId) {
|
|
2243
|
+
const safeName = subscriberToSafeName(agentId);
|
|
2244
|
+
return path.join(getUfooPaths(projectRoot).busQueuesDir, safeName, "inject.sock");
|
|
2245
|
+
}
|
|
2246
|
+
|
|
2247
|
+
function closeAgentViaDaemon(agentId, label) {
|
|
2248
|
+
logMessage("system", `{yellow-fg}⚙{/yellow-fg} Closing ${label}...`);
|
|
2249
|
+
const sockFile = socketPath(projectRoot);
|
|
2250
|
+
try {
|
|
2251
|
+
const conn = net.createConnection(sockFile, () => {
|
|
2252
|
+
conn.write(JSON.stringify({ type: "close_agent", agentId }) + "\n");
|
|
2253
|
+
});
|
|
2254
|
+
let buffer = "";
|
|
2255
|
+
conn.on("data", (data) => {
|
|
2256
|
+
buffer += data.toString("utf8");
|
|
2257
|
+
const lines = buffer.split("\n");
|
|
2258
|
+
buffer = lines.pop() || "";
|
|
2259
|
+
for (const line of lines) {
|
|
2260
|
+
if (!line.trim()) continue;
|
|
2261
|
+
try {
|
|
2262
|
+
const res = JSON.parse(line);
|
|
2263
|
+
if (res.type === "close_agent_ok") {
|
|
2264
|
+
if (res.ok) {
|
|
2265
|
+
logMessage("system", `{green-fg}✓{/green-fg} Closed ${label}`);
|
|
2266
|
+
} else {
|
|
2267
|
+
logMessage("system", `{red-fg}✗{/red-fg} Agent ${label} not found or already stopped`);
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
} catch { /* ignore */ }
|
|
2271
|
+
}
|
|
2272
|
+
});
|
|
2273
|
+
conn.on("error", () => {
|
|
2274
|
+
logMessage("error", `{red-fg}✗{/red-fg} Failed to connect to daemon`);
|
|
2275
|
+
});
|
|
2276
|
+
setTimeout(() => { try { conn.destroy(); } catch {} }, 3000);
|
|
2277
|
+
} catch {
|
|
2278
|
+
logMessage("error", `{red-fg}✗{/red-fg} Failed to close ${label}`);
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
// Freeze blessed rendering during agent PTY view (direct stdout mode)
|
|
2283
|
+
const _originalRender = screen.render.bind(screen);
|
|
2284
|
+
let renderFrozen = false;
|
|
2285
|
+
screen.render = function() {
|
|
2286
|
+
if (renderFrozen) return;
|
|
2287
|
+
return _originalRender();
|
|
2288
|
+
};
|
|
2289
|
+
|
|
2290
|
+
// Render agent view dashboard bar via ANSI — matches blessed dashboard style
|
|
2291
|
+
function renderAgentDashboard() {
|
|
2292
|
+
const rows = process.stdout.rows || 24;
|
|
2293
|
+
const cols = process.stdout.columns || 80;
|
|
2294
|
+
let bar = " ";
|
|
2295
|
+
|
|
2296
|
+
if (focusMode === "dashboard") {
|
|
2297
|
+
// Dashboard mode: \x1b[90;7m = gray+inverse, matches blessed {inverse} on gray fg widget
|
|
2298
|
+
const ufooItem = selectedAgentIndex === 0
|
|
2299
|
+
? "\x1b[90;7mufoo\x1b[0m"
|
|
2300
|
+
: "\x1b[36mufoo\x1b[0m";
|
|
2301
|
+
const agentParts = activeAgents.map((agent, i) => {
|
|
2302
|
+
const label = getAgentLabel(agent);
|
|
2303
|
+
const idx = i + 1; // +1 for ufoo at index 0
|
|
2304
|
+
if (idx === selectedAgentIndex) return `\x1b[90;7m${label}\x1b[0m`;
|
|
2305
|
+
if (agent === viewingAgent) return `\x1b[1;36m${label}\x1b[0m`;
|
|
2306
|
+
return `\x1b[36m${label}\x1b[0m`;
|
|
2307
|
+
});
|
|
2308
|
+
bar += `${ufooItem} ${agentParts.join(" ")}`;
|
|
2309
|
+
bar += ` \x1b[90m│ ←/→ select, Enter switch, ^X close, ↑ back\x1b[0m`;
|
|
2310
|
+
} else {
|
|
2311
|
+
// Normal PTY mode: bold current viewing agent
|
|
2312
|
+
const agentParts = activeAgents.map((agent) => {
|
|
2313
|
+
const label = getAgentLabel(agent);
|
|
2314
|
+
if (agent === viewingAgent) return `\x1b[1;36m${label}\x1b[0m`;
|
|
2315
|
+
return `\x1b[36m${label}\x1b[0m`;
|
|
2316
|
+
});
|
|
2317
|
+
bar += `\x1b[36mufoo\x1b[0m ${agentParts.join(" ")}`;
|
|
2318
|
+
bar += ` \x1b[90m│ ↓: agents\x1b[0m`;
|
|
2319
|
+
}
|
|
2320
|
+
|
|
2321
|
+
// Pad to full width
|
|
2322
|
+
const plainLen = bar.replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
2323
|
+
const pad = Math.max(0, cols - plainLen);
|
|
2324
|
+
// Save cursor → move to last row → write bar → restore cursor
|
|
2325
|
+
process.stdout.write(`\x1b7\x1b[${rows};1H${bar}${" ".repeat(pad)}\x1b8`);
|
|
2326
|
+
}
|
|
2327
|
+
|
|
2328
|
+
function enterAgentView(agentId) {
|
|
2329
|
+
if (currentView === "agent" && viewingAgent === agentId) return;
|
|
2330
|
+
if (currentView === "agent") {
|
|
2331
|
+
disconnectAgentOutput();
|
|
2332
|
+
disconnectAgentInput();
|
|
2333
|
+
}
|
|
2334
|
+
|
|
2335
|
+
currentView = "agent";
|
|
2336
|
+
viewingAgent = agentId;
|
|
2337
|
+
focusMode = "input";
|
|
2338
|
+
|
|
2339
|
+
// Detach all blessed widgets from screen — nothing left to render
|
|
2340
|
+
_detachedChildren = [...screen.children];
|
|
2341
|
+
for (const child of _detachedChildren) screen.remove(child);
|
|
2342
|
+
|
|
2343
|
+
// Freeze blessed — we take over the terminal with direct stdout
|
|
2344
|
+
renderFrozen = true;
|
|
2345
|
+
|
|
2346
|
+
const rows = process.stdout.rows || 24;
|
|
2347
|
+
const cols = process.stdout.columns || 80;
|
|
2348
|
+
process.stdout.write("\x1b[2J\x1b[H"); // Clear + home
|
|
2349
|
+
process.stdout.write(`\x1b[1;${rows - 1}r`); // Scroll region
|
|
2350
|
+
process.stdout.write("\x1b[H"); // Cursor to top
|
|
2351
|
+
process.stdout.write("\x1b[?25h"); // Show cursor
|
|
2352
|
+
|
|
2353
|
+
// Render dashboard bar
|
|
2354
|
+
renderAgentDashboard();
|
|
2355
|
+
|
|
2356
|
+
// Suppress input forwarding briefly — prevents the Enter that triggered
|
|
2357
|
+
// view switch and any terminal query responses (CPR etc) from leaking
|
|
2358
|
+
agentInputSuppressUntil = Date.now() + 300;
|
|
2359
|
+
|
|
2360
|
+
// Connect to agent's inject.sock for output streaming and input
|
|
2361
|
+
const sockPath = getInjectSockPath(agentId);
|
|
2362
|
+
connectAgentOutput(sockPath);
|
|
2363
|
+
connectAgentInput(sockPath);
|
|
2364
|
+
|
|
2365
|
+
// Resize agent PTY to match our viewport (rows-1 for status bar)
|
|
2366
|
+
setTimeout(() => sendResizeToAgent(cols, rows - 1), 100);
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2369
|
+
function exitAgentView() {
|
|
2370
|
+
if (currentView !== "agent") return;
|
|
2371
|
+
|
|
2372
|
+
// Restore agent PTY to full terminal size before disconnecting
|
|
2373
|
+
const rows = process.stdout.rows || 24;
|
|
2374
|
+
const cols = process.stdout.columns || 80;
|
|
2375
|
+
sendResizeToAgent(cols, rows);
|
|
2376
|
+
|
|
2377
|
+
disconnectAgentOutput();
|
|
2378
|
+
disconnectAgentInput();
|
|
2379
|
+
|
|
2380
|
+
currentView = "main";
|
|
2381
|
+
viewingAgent = null;
|
|
2382
|
+
|
|
2383
|
+
// Reset scroll region to full screen
|
|
2384
|
+
process.stdout.write(`\x1b[1;${rows}r`);
|
|
2385
|
+
process.stdout.write("\x1b[2J\x1b[H");
|
|
2386
|
+
|
|
2387
|
+
// Re-attach all blessed widgets to screen
|
|
2388
|
+
if (_detachedChildren) {
|
|
2389
|
+
for (const child of _detachedChildren) screen.append(child);
|
|
2390
|
+
_detachedChildren = null;
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2393
|
+
// Unfreeze blessed and force full redraw
|
|
2394
|
+
renderFrozen = false;
|
|
2395
|
+
focusMode = "input";
|
|
2396
|
+
dashboardView = "agents";
|
|
2397
|
+
selectedAgentIndex = -1;
|
|
2398
|
+
screen.grabKeys = false;
|
|
2399
|
+
clearTargetAgent();
|
|
2400
|
+
renderDashboard();
|
|
2401
|
+
focusInput();
|
|
2402
|
+
resizeInput();
|
|
2403
|
+
screen.alloc();
|
|
2404
|
+
screen.render();
|
|
2405
|
+
}
|
|
2406
|
+
|
|
2407
|
+
function connectAgentOutput(sockPath) {
|
|
2408
|
+
if (agentOutputClient) {
|
|
2409
|
+
disconnectAgentOutput();
|
|
2410
|
+
}
|
|
2411
|
+
agentOutputBuffer = "";
|
|
2412
|
+
|
|
2413
|
+
if (!fs.existsSync(sockPath)) {
|
|
2414
|
+
writeToAgentTerm("\x1b[1;31m[Error]\x1b[0m inject.sock not found\r\n");
|
|
2415
|
+
writeToAgentTerm("\x1b[33m[Hint]\x1b[0m Agent may not be running in terminal mode\r\n");
|
|
2416
|
+
writeToAgentTerm("Press Esc to return\r\n");
|
|
2417
|
+
return;
|
|
2418
|
+
}
|
|
2419
|
+
|
|
2420
|
+
try {
|
|
2421
|
+
agentOutputClient = net.createConnection(sockPath, () => {
|
|
2422
|
+
agentOutputClient.write(JSON.stringify({ type: "subscribe" }) + "\n");
|
|
2423
|
+
});
|
|
2424
|
+
|
|
2425
|
+
// Connection timeout
|
|
2426
|
+
const connectTimeout = setTimeout(() => {
|
|
2427
|
+
if (agentOutputClient && !agentOutputClient.connecting) return;
|
|
2428
|
+
writeToAgentTerm("\x1b[1;31m[Timeout]\x1b[0m Could not connect\r\nPress Esc to return\r\n");
|
|
2429
|
+
disconnectAgentOutput();
|
|
2430
|
+
}, 5000);
|
|
2431
|
+
|
|
2432
|
+
agentOutputClient.on("connect", () => {
|
|
2433
|
+
clearTimeout(connectTimeout);
|
|
2434
|
+
});
|
|
2435
|
+
|
|
2436
|
+
agentOutputClient.on("data", (data) => {
|
|
2437
|
+
agentOutputBuffer += data.toString("utf8");
|
|
2438
|
+
const lines = agentOutputBuffer.split("\n");
|
|
2439
|
+
agentOutputBuffer = lines.pop() || "";
|
|
2440
|
+
|
|
2441
|
+
for (const line of lines) {
|
|
2442
|
+
if (!line.trim()) continue;
|
|
2443
|
+
try {
|
|
2444
|
+
const msg = JSON.parse(line);
|
|
2445
|
+
if (msg.type === "output" || msg.type === "replay") {
|
|
2446
|
+
if (msg.data) {
|
|
2447
|
+
writeToAgentTerm(msg.data);
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
} catch {
|
|
2451
|
+
// ignore malformed messages
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2454
|
+
});
|
|
2455
|
+
|
|
2456
|
+
agentOutputClient.on("error", (err) => {
|
|
2457
|
+
if (currentView === "agent") {
|
|
2458
|
+
writeToAgentTerm(`\r\n\x1b[1;31m[Connection error]\x1b[0m ${err.message}\r\nPress Esc to return\r\n`);
|
|
2459
|
+
}
|
|
2460
|
+
});
|
|
2461
|
+
|
|
2462
|
+
agentOutputClient.on("close", () => {
|
|
2463
|
+
agentOutputClient = null;
|
|
2464
|
+
if (currentView === "agent") {
|
|
2465
|
+
writeToAgentTerm("\r\n\x1b[1;33m[Agent disconnected]\x1b[0m\r\nPress Esc to return\r\n");
|
|
2466
|
+
}
|
|
2467
|
+
});
|
|
2468
|
+
} catch (err) {
|
|
2469
|
+
writeToAgentTerm(`\x1b[1;31m[Error]\x1b[0m ${err.message}\r\nPress Esc to return\r\n`);
|
|
2470
|
+
}
|
|
2471
|
+
}
|
|
2472
|
+
|
|
2473
|
+
function disconnectAgentOutput() {
|
|
2474
|
+
if (agentOutputClient) {
|
|
2475
|
+
try {
|
|
2476
|
+
agentOutputClient.removeAllListeners();
|
|
2477
|
+
agentOutputClient.destroy();
|
|
2478
|
+
} catch { /* ignore */ }
|
|
2479
|
+
agentOutputClient = null;
|
|
2480
|
+
}
|
|
2481
|
+
agentOutputBuffer = "";
|
|
2482
|
+
}
|
|
2483
|
+
|
|
2484
|
+
function connectAgentInput(sockPath) {
|
|
2485
|
+
if (agentInputClient) {
|
|
2486
|
+
disconnectAgentInput();
|
|
2487
|
+
}
|
|
2488
|
+
try {
|
|
2489
|
+
agentInputClient = net.createConnection(sockPath);
|
|
2490
|
+
agentInputClient.on("error", () => {
|
|
2491
|
+
agentInputClient = null;
|
|
2492
|
+
});
|
|
2493
|
+
agentInputClient.on("close", () => {
|
|
2494
|
+
agentInputClient = null;
|
|
2495
|
+
});
|
|
2496
|
+
} catch {
|
|
2497
|
+
agentInputClient = null;
|
|
2498
|
+
}
|
|
2499
|
+
}
|
|
2500
|
+
|
|
2501
|
+
function disconnectAgentInput() {
|
|
2502
|
+
if (agentInputClient) {
|
|
2503
|
+
try {
|
|
2504
|
+
agentInputClient.removeAllListeners();
|
|
2505
|
+
agentInputClient.destroy();
|
|
2506
|
+
} catch { /* ignore */ }
|
|
2507
|
+
agentInputClient = null;
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2510
|
+
|
|
2511
|
+
function sendRawToAgent(data) {
|
|
2512
|
+
if (!agentInputClient || agentInputClient.destroyed) return;
|
|
2513
|
+
try {
|
|
2514
|
+
agentInputClient.write(JSON.stringify({ type: "raw", data }) + "\n");
|
|
2515
|
+
} catch {
|
|
2516
|
+
// ignore write errors
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2519
|
+
|
|
2520
|
+
function sendResizeToAgent(cols, rows) {
|
|
2521
|
+
if (!agentInputClient || agentInputClient.destroyed) return;
|
|
2522
|
+
try {
|
|
2523
|
+
agentInputClient.write(JSON.stringify({ type: "resize", cols, rows }) + "\n");
|
|
2524
|
+
} catch {
|
|
2525
|
+
// ignore write errors
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2528
|
+
|
|
2529
|
+
function writeToAgentTerm(text) {
|
|
2530
|
+
if (!text) return;
|
|
2531
|
+
if (currentView === "agent") {
|
|
2532
|
+
// Strip sequences that cause the real terminal to respond, feeding
|
|
2533
|
+
// garbage back into the agent's input:
|
|
2534
|
+
// - OSC queries: \x1b]10;?\x07 etc (color queries)
|
|
2535
|
+
// - CSI DSR: \x1b[6n / \x1b[?6n (cursor position query → CPR response)
|
|
2536
|
+
// - CSI DSR: \x1b[5n (device status query)
|
|
2537
|
+
// - CSI DA: \x1b[c / \x1b[>c / \x1b[=c (device attributes query)
|
|
2538
|
+
const cleaned = text
|
|
2539
|
+
.replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, "")
|
|
2540
|
+
.replace(/\x1b\[(?:[?>=]?[0-9]*c|[?]?6n|5n)/g, "");
|
|
2541
|
+
if (cleaned) process.stdout.write(cleaned);
|
|
2542
|
+
// Always re-render dashboard bar — PTY output may overwrite it
|
|
2543
|
+
// via absolute cursor positioning before the resize takes effect
|
|
2544
|
+
renderAgentDashboard();
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
|
|
1495
2548
|
function requestStatus() {
|
|
1496
2549
|
send({ type: "status" });
|
|
1497
2550
|
}
|
|
@@ -1512,6 +2565,7 @@ async function runChat(projectRoot) {
|
|
|
1512
2565
|
if (!newClient) return;
|
|
1513
2566
|
detachClient();
|
|
1514
2567
|
client = newClient;
|
|
2568
|
+
connectionLostNotified = false;
|
|
1515
2569
|
let buffer = "";
|
|
1516
2570
|
client.on("data", (data) => {
|
|
1517
2571
|
buffer += data.toString("utf8");
|
|
@@ -1520,84 +2574,102 @@ async function runChat(projectRoot) {
|
|
|
1520
2574
|
for (const line of lines.filter((l) => l.trim())) {
|
|
1521
2575
|
try {
|
|
1522
2576
|
const msg = JSON.parse(line);
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
const
|
|
1527
|
-
const item = { key: data.key, text };
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
2577
|
+
if (msg.type === "status") {
|
|
2578
|
+
const data = msg.data || {};
|
|
2579
|
+
if (typeof data.phase === "string") {
|
|
2580
|
+
const rawText = data.text == null ? "" : String(data.text);
|
|
2581
|
+
const item = { key: data.key, text: rawText };
|
|
2582
|
+
if (data.phase === "start") {
|
|
2583
|
+
enqueueBusStatus(item);
|
|
2584
|
+
} else if (data.phase === "done" || data.phase === "error") {
|
|
2585
|
+
resolveBusStatus(item);
|
|
2586
|
+
if (rawText) {
|
|
2587
|
+
const prefix = data.phase === "error"
|
|
2588
|
+
? "{red-fg}✗{/red-fg}"
|
|
2589
|
+
: "{green-fg}✓{/green-fg}";
|
|
2590
|
+
logMessage("status", `${prefix} ${escapeBlessed(rawText)}`, data);
|
|
2591
|
+
}
|
|
2592
|
+
} else {
|
|
2593
|
+
enqueueBusStatus(item);
|
|
2594
|
+
}
|
|
2595
|
+
screen.render();
|
|
2596
|
+
} else {
|
|
2597
|
+
// 收到 dashboard 状态更新
|
|
2598
|
+
if (process.env.UFOO_DEBUG) {
|
|
2599
|
+
logMessage("debug", `[status] active: ${(data.active || []).length}`);
|
|
1540
2600
|
}
|
|
1541
|
-
screen.render();
|
|
1542
|
-
} else {
|
|
1543
2601
|
updateDashboard(data);
|
|
1544
2602
|
}
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
2603
|
+
} else if (msg.type === "response") {
|
|
2604
|
+
const payload = msg.data || {};
|
|
2605
|
+
if (payload.reply) {
|
|
2606
|
+
resolveStatusLine(`{green-fg}←{/green-fg} ${escapeBlessed(payload.reply)}`);
|
|
2607
|
+
logMessage("reply", `{green-fg}←{/green-fg} ${escapeBlessed(payload.reply)}`);
|
|
2608
|
+
}
|
|
2609
|
+
if (payload.dispatch && payload.dispatch.length > 0) {
|
|
2610
|
+
const targets = payload.dispatch.map((d) => d.target || d).join(", ");
|
|
2611
|
+
logMessage("dispatch", `{blue-fg}→{/blue-fg} Dispatched to: ${escapeBlessed(targets)}`);
|
|
2612
|
+
}
|
|
2613
|
+
if (payload.disambiguate && Array.isArray(payload.disambiguate.candidates) && payload.disambiguate.candidates.length > 0) {
|
|
2614
|
+
pending = { disambiguate: payload.disambiguate, original: pending?.original };
|
|
2615
|
+
const prompt = payload.disambiguate.prompt || "Choose target:";
|
|
2616
|
+
resolveStatusLine(`{yellow-fg}?{/yellow-fg} ${escapeBlessed(prompt)}`);
|
|
2617
|
+
logMessage("disambiguate", `{yellow-fg}?{/yellow-fg} ${escapeBlessed(prompt)}`);
|
|
2618
|
+
payload.disambiguate.candidates.forEach((c, i) => {
|
|
2619
|
+
const agentId = c.agent_id || "";
|
|
2620
|
+
const reason = c.reason || "";
|
|
2621
|
+
logMessage(
|
|
2622
|
+
"disambiguate",
|
|
2623
|
+
` {cyan-fg}${i + 1}){/cyan-fg} ${escapeBlessed(agentId)} {gray-fg}— ${escapeBlessed(reason)}{/gray-fg}`
|
|
2624
|
+
);
|
|
2625
|
+
});
|
|
2626
|
+
} else {
|
|
2627
|
+
pending = null;
|
|
2628
|
+
}
|
|
1564
2629
|
if (!payload.reply && !payload.disambiguate) {
|
|
1565
2630
|
resolveStatusLine("{gray-fg}✓{/gray-fg} Done");
|
|
1566
2631
|
}
|
|
1567
|
-
|
|
1568
|
-
logMessage("ops", `{magenta-fg}⚡{/magenta-fg} ${JSON.stringify(msg.opsResults)}`);
|
|
1569
|
-
}
|
|
2632
|
+
// opsResults are noisy JSON; keep them out of the log UI
|
|
1570
2633
|
screen.render();
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
2634
|
+
} else if (msg.type === "bus") {
|
|
2635
|
+
const data = msg.data || {};
|
|
2636
|
+
const prefix = data.event === "broadcast" ? "{magenta-fg}⇢{/magenta-fg}" : "{blue-fg}↔{/blue-fg}";
|
|
2637
|
+
let publisher = data.publisher && data.publisher !== "unknown"
|
|
2638
|
+
? data.publisher
|
|
2639
|
+
: (data.event === "broadcast" ? "broadcast" : "bus");
|
|
1577
2640
|
|
|
1578
2641
|
// Try to parse message as JSON (from internal agents)
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
2642
|
+
let displayMessage = data.message == null ? "" : String(data.message);
|
|
2643
|
+
let isStream = false;
|
|
2644
|
+
try {
|
|
2645
|
+
const parsed = JSON.parse(data.message);
|
|
2646
|
+
if (parsed && typeof parsed === "object" && parsed.reply) {
|
|
2647
|
+
displayMessage = parsed.reply == null ? "" : String(parsed.reply);
|
|
2648
|
+
} else if (parsed && typeof parsed === "object" && parsed.stream) {
|
|
2649
|
+
displayMessage = typeof parsed.delta === "string" ? parsed.delta : "";
|
|
2650
|
+
isStream = true;
|
|
2651
|
+
}
|
|
2652
|
+
} catch {
|
|
2653
|
+
// Not JSON, use as-is
|
|
2654
|
+
}
|
|
2655
|
+
|
|
2656
|
+
// Convert literal \n to actual newlines for better display
|
|
2657
|
+
if (typeof displayMessage === "string") {
|
|
2658
|
+
displayMessage = displayMessage.replace(/\\n/g, "\n");
|
|
1587
2659
|
}
|
|
1588
2660
|
|
|
1589
2661
|
// Extract nickname if publisher is in subscriber:id format
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
// Try to get nickname from activeAgentLabelMap or
|
|
2662
|
+
let displayName = publisher;
|
|
2663
|
+
if (publisher.includes(":")) {
|
|
2664
|
+
// Try to get nickname from activeAgentLabelMap or all-agents.json
|
|
1593
2665
|
if (activeAgentLabelMap && activeAgentLabelMap.has(publisher)) {
|
|
1594
2666
|
displayName = activeAgentLabelMap.get(publisher);
|
|
1595
2667
|
} else {
|
|
1596
|
-
// Fallback: read directly from
|
|
2668
|
+
// Fallback: read directly from all-agents.json
|
|
1597
2669
|
try {
|
|
1598
|
-
const busPath =
|
|
2670
|
+
const busPath = getUfooPaths(projectRoot).agentsFile;
|
|
1599
2671
|
const bus = JSON.parse(fs.readFileSync(busPath, "utf8"));
|
|
1600
|
-
const meta = bus.
|
|
2672
|
+
const meta = bus.agents && bus.agents[publisher];
|
|
1601
2673
|
if (meta && meta.nickname) {
|
|
1602
2674
|
displayName = meta.nickname;
|
|
1603
2675
|
}
|
|
@@ -1605,36 +2677,436 @@ async function runChat(projectRoot) {
|
|
|
1605
2677
|
// Keep original publisher ID
|
|
1606
2678
|
}
|
|
1607
2679
|
}
|
|
1608
|
-
|
|
2680
|
+
}
|
|
1609
2681
|
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
2682
|
+
const line = `${prefix} {gray-fg}${escapeBlessed(displayName)}{/gray-fg}: ${escapeBlessed(displayMessage)}`;
|
|
2683
|
+
if (isStream) {
|
|
2684
|
+
recordLog("bus_stream", line, data, true);
|
|
2685
|
+
} else {
|
|
2686
|
+
logMessage("bus", line, data);
|
|
2687
|
+
}
|
|
2688
|
+
if (data.event === "agent_renamed" || data.event === "message") {
|
|
2689
|
+
// 收到消息时刷新 status,更新在线 agent 列表
|
|
1613
2690
|
requestStatus();
|
|
1614
2691
|
}
|
|
1615
2692
|
screen.render();
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
2693
|
+
} else if (msg.type === "error") {
|
|
2694
|
+
resolveStatusLine(`{red-fg}✗{/red-fg} Error: ${escapeBlessed(msg.error)}`);
|
|
2695
|
+
logMessage("error", `{red-fg}✗{/red-fg} Error: ${escapeBlessed(msg.error)}`);
|
|
2696
|
+
screen.render();
|
|
2697
|
+
}
|
|
1621
2698
|
} catch {
|
|
1622
2699
|
// ignore
|
|
1623
2700
|
}
|
|
1624
2701
|
}
|
|
1625
2702
|
});
|
|
1626
|
-
|
|
1627
|
-
client
|
|
1628
|
-
|
|
2703
|
+
const handleDisconnect = () => {
|
|
2704
|
+
if (client === newClient) {
|
|
2705
|
+
client = null;
|
|
2706
|
+
}
|
|
2707
|
+
if (exitRequested) return;
|
|
2708
|
+
if (!connectionLostNotified) {
|
|
2709
|
+
connectionLostNotified = true;
|
|
2710
|
+
logMessage("status", "{red-fg}✗{/red-fg} Daemon disconnected");
|
|
2711
|
+
}
|
|
2712
|
+
void ensureConnected();
|
|
2713
|
+
};
|
|
2714
|
+
client.on("close", handleDisconnect);
|
|
2715
|
+
client.on("error", handleDisconnect);
|
|
2716
|
+
flushPendingRequests();
|
|
1629
2717
|
};
|
|
1630
2718
|
|
|
1631
2719
|
attachClient(client);
|
|
1632
2720
|
|
|
1633
|
-
|
|
2721
|
+
// Command handlers
|
|
2722
|
+
async function handleDoctorCommand() {
|
|
2723
|
+
logMessage("system", "{yellow-fg}⚙{/yellow-fg} Running health check...");
|
|
2724
|
+
|
|
2725
|
+
// Capture console output safely
|
|
2726
|
+
const originalLog = console.log;
|
|
2727
|
+
const originalError = console.error;
|
|
2728
|
+
|
|
2729
|
+
console.log = (...args) => logMessage("system", args.join(" "));
|
|
2730
|
+
console.error = (...args) => logMessage("error", args.join(" "));
|
|
2731
|
+
|
|
2732
|
+
try {
|
|
2733
|
+
const UfooDoctor = require("../doctor");
|
|
2734
|
+
const doctor = new UfooDoctor(projectRoot);
|
|
2735
|
+
const result = doctor.run();
|
|
2736
|
+
|
|
2737
|
+
if (result) {
|
|
2738
|
+
logMessage("system", "{green-fg}✓{/green-fg} System healthy");
|
|
2739
|
+
} else {
|
|
2740
|
+
logMessage("error", "{red-fg}✗{/red-fg} Health check failed");
|
|
2741
|
+
}
|
|
2742
|
+
screen.render();
|
|
2743
|
+
} catch (err) {
|
|
2744
|
+
logMessage("error", `{red-fg}✗{/red-fg} Doctor check failed: ${err.message}`);
|
|
2745
|
+
screen.render();
|
|
2746
|
+
} finally {
|
|
2747
|
+
console.log = originalLog;
|
|
2748
|
+
console.error = originalError;
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
|
|
2752
|
+
async function handleStatusCommand() {
|
|
2753
|
+
// Display current status directly instead of requesting
|
|
2754
|
+
if (activeAgents.length === 0) {
|
|
2755
|
+
logMessage("system", "{cyan-fg}Status:{/cyan-fg} No active agents");
|
|
2756
|
+
} else {
|
|
2757
|
+
logMessage("system", `{cyan-fg}Status:{/cyan-fg} ${activeAgents.length} active agent(s)`);
|
|
2758
|
+
for (const id of activeAgents) {
|
|
2759
|
+
const label = getAgentLabel(id);
|
|
2760
|
+
const meta = activeAgentMetaMap.get(id);
|
|
2761
|
+
const mode = meta?.launch_mode || "unknown";
|
|
2762
|
+
logMessage("system", ` • {cyan-fg}${label}{/cyan-fg} {gray-fg}[${mode}]{/gray-fg}`);
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2765
|
+
|
|
2766
|
+
// Also show daemon status
|
|
2767
|
+
if (isRunning(projectRoot)) {
|
|
2768
|
+
logMessage("system", "{green-fg}✓{/green-fg} Daemon is running");
|
|
2769
|
+
} else {
|
|
2770
|
+
logMessage("system", "{red-fg}✗{/red-fg} Daemon is not running");
|
|
2771
|
+
}
|
|
2772
|
+
}
|
|
2773
|
+
|
|
2774
|
+
async function handleDaemonCommand(args) {
|
|
2775
|
+
const subcommand = args[0];
|
|
2776
|
+
|
|
2777
|
+
if (subcommand === "start") {
|
|
2778
|
+
if (isRunning(projectRoot)) {
|
|
2779
|
+
logMessage("system", "{yellow-fg}⚠{/yellow-fg} Daemon already running");
|
|
2780
|
+
} else {
|
|
2781
|
+
logMessage("system", "{yellow-fg}⚙{/yellow-fg} Starting daemon...");
|
|
2782
|
+
startDaemon(projectRoot);
|
|
2783
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
2784
|
+
if (isRunning(projectRoot)) {
|
|
2785
|
+
logMessage("system", "{green-fg}✓{/green-fg} Daemon started");
|
|
2786
|
+
} else {
|
|
2787
|
+
logMessage("error", "{red-fg}✗{/red-fg} Failed to start daemon");
|
|
2788
|
+
}
|
|
2789
|
+
}
|
|
2790
|
+
} else if (subcommand === "stop") {
|
|
2791
|
+
logMessage("system", "{yellow-fg}⚙{/yellow-fg} Stopping daemon...");
|
|
2792
|
+
stopDaemon(projectRoot);
|
|
2793
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
2794
|
+
if (!isRunning(projectRoot)) {
|
|
2795
|
+
logMessage("system", "{green-fg}✓{/green-fg} Daemon stopped");
|
|
2796
|
+
} else {
|
|
2797
|
+
logMessage("error", "{red-fg}✗{/red-fg} Failed to stop daemon");
|
|
2798
|
+
}
|
|
2799
|
+
} else if (subcommand === "restart") {
|
|
2800
|
+
logMessage("system", "{yellow-fg}⚙{/yellow-fg} Restarting daemon...");
|
|
2801
|
+
await restartDaemon();
|
|
2802
|
+
} else if (subcommand === "status") {
|
|
2803
|
+
if (isRunning(projectRoot)) {
|
|
2804
|
+
logMessage("system", "{green-fg}✓{/green-fg} Daemon is running");
|
|
2805
|
+
} else {
|
|
2806
|
+
logMessage("system", "{red-fg}✗{/red-fg} Daemon is not running");
|
|
2807
|
+
}
|
|
2808
|
+
} else {
|
|
2809
|
+
logMessage("error", "{red-fg}✗{/red-fg} Unknown daemon command. Use: start, stop, restart, status");
|
|
2810
|
+
}
|
|
2811
|
+
}
|
|
2812
|
+
|
|
2813
|
+
async function handleInitCommand(args) {
|
|
2814
|
+
logMessage("system", "{yellow-fg}⚙{/yellow-fg} Initializing ufoo modules...");
|
|
2815
|
+
|
|
2816
|
+
// Capture console output safely
|
|
2817
|
+
const originalLog = console.log;
|
|
2818
|
+
const originalError = console.error;
|
|
2819
|
+
const logs = [];
|
|
2820
|
+
|
|
2821
|
+
console.log = (...args) => {
|
|
2822
|
+
const msg = args.join(" ");
|
|
2823
|
+
logs.push(msg);
|
|
2824
|
+
// Also output to logMessage immediately to avoid UI blocking
|
|
2825
|
+
logMessage("system", msg);
|
|
2826
|
+
};
|
|
2827
|
+
console.error = (...args) => {
|
|
2828
|
+
const msg = args.join(" ");
|
|
2829
|
+
logs.push(`ERROR: ${msg}`);
|
|
2830
|
+
logMessage("error", msg);
|
|
2831
|
+
};
|
|
2832
|
+
|
|
2833
|
+
try {
|
|
2834
|
+
const repoRoot = path.join(__dirname, "..", "..");
|
|
2835
|
+
const init = new UfooInit(repoRoot);
|
|
2836
|
+
const modules = args.length > 0 ? args.join(",") : "context,bus";
|
|
2837
|
+
await init.init({ modules, project: projectRoot });
|
|
2838
|
+
|
|
2839
|
+
logMessage("system", "{green-fg}✓{/green-fg} Initialization complete");
|
|
2840
|
+
screen.render();
|
|
2841
|
+
} catch (err) {
|
|
2842
|
+
logMessage("error", `{red-fg}✗{/red-fg} Init failed: ${err.message}`);
|
|
2843
|
+
if (err.stack) {
|
|
2844
|
+
logMessage("error", err.stack);
|
|
2845
|
+
}
|
|
2846
|
+
screen.render();
|
|
2847
|
+
} finally {
|
|
2848
|
+
console.log = originalLog;
|
|
2849
|
+
console.error = originalError;
|
|
2850
|
+
}
|
|
2851
|
+
}
|
|
2852
|
+
|
|
2853
|
+
async function handleBusCommand(args) {
|
|
2854
|
+
const subcommand = args[0];
|
|
2855
|
+
|
|
2856
|
+
try {
|
|
2857
|
+
if (subcommand === "send") {
|
|
2858
|
+
if (args.length < 3) {
|
|
2859
|
+
logMessage("error", "{red-fg}✗{/red-fg} Usage: /bus send <target> <message>");
|
|
2860
|
+
return;
|
|
2861
|
+
}
|
|
2862
|
+
const target = args[1];
|
|
2863
|
+
const message = args.slice(2).join(" ");
|
|
2864
|
+
// Send via daemon to ensure proper publisher ID
|
|
2865
|
+
send({ type: "bus_send", target, message });
|
|
2866
|
+
logMessage("system", `{green-fg}✓{/green-fg} Message sent to ${target}`);
|
|
2867
|
+
return;
|
|
2868
|
+
}
|
|
2869
|
+
|
|
2870
|
+
const bus = new EventBus(projectRoot);
|
|
2871
|
+
|
|
2872
|
+
if (subcommand === "rename") {
|
|
2873
|
+
if (args.length < 3) {
|
|
2874
|
+
logMessage("error", "{red-fg}✗{/red-fg} Usage: /bus rename <agent> <nickname>");
|
|
2875
|
+
return;
|
|
2876
|
+
}
|
|
2877
|
+
const agentId = args[1];
|
|
2878
|
+
const nickname = args[2];
|
|
2879
|
+
await bus.rename(agentId, nickname);
|
|
2880
|
+
logMessage("system", `{green-fg}✓{/green-fg} Renamed ${agentId} to ${nickname}`);
|
|
2881
|
+
requestStatus();
|
|
2882
|
+
} else if (subcommand === "list") {
|
|
2883
|
+
bus.ensureBus();
|
|
2884
|
+
bus.loadBusData();
|
|
2885
|
+
const subscribers = Object.entries(bus.busData.agents || {});
|
|
2886
|
+
if (subscribers.length === 0) {
|
|
2887
|
+
logMessage("system", "{gray-fg}No active agents{/gray-fg}");
|
|
2888
|
+
} else {
|
|
2889
|
+
logMessage("system", "{cyan-fg}Active agents:{/cyan-fg}");
|
|
2890
|
+
for (const [id, meta] of subscribers) {
|
|
2891
|
+
const nickname = meta.nickname ? ` (${meta.nickname})` : "";
|
|
2892
|
+
const status = meta.status || "unknown";
|
|
2893
|
+
logMessage("system", ` • ${id}${nickname} {gray-fg}[${status}]{/gray-fg}`);
|
|
2894
|
+
}
|
|
2895
|
+
}
|
|
2896
|
+
} else if (subcommand === "status") {
|
|
2897
|
+
bus.ensureBus();
|
|
2898
|
+
bus.loadBusData();
|
|
2899
|
+
const count = Object.keys(bus.busData.agents || {}).length;
|
|
2900
|
+
logMessage("system", `{cyan-fg}Bus status:{/cyan-fg} ${count} agent(s) registered`);
|
|
2901
|
+
} else if (subcommand === "activate") {
|
|
2902
|
+
if (args.length < 2) {
|
|
2903
|
+
logMessage("error", "{red-fg}✗{/red-fg} Usage: /bus activate <agent>");
|
|
2904
|
+
return;
|
|
2905
|
+
}
|
|
2906
|
+
const target = args[1];
|
|
2907
|
+
const AgentActivator = require("../bus/activate");
|
|
2908
|
+
const activator = new AgentActivator(projectRoot);
|
|
2909
|
+
await activator.activate(target);
|
|
2910
|
+
logMessage("system", `{green-fg}✓{/green-fg} Activated ${target}`);
|
|
2911
|
+
} else {
|
|
2912
|
+
logMessage("error", "{red-fg}✗{/red-fg} Unknown bus command. Use: send, rename, list, status, activate");
|
|
2913
|
+
}
|
|
2914
|
+
} catch (err) {
|
|
2915
|
+
logMessage("error", `{red-fg}✗{/red-fg} Bus command failed: ${err.message}`);
|
|
2916
|
+
}
|
|
2917
|
+
}
|
|
2918
|
+
|
|
2919
|
+
async function handleCtxCommand(args) {
|
|
2920
|
+
logMessage("system", "{yellow-fg}⚙{/yellow-fg} Running context check...");
|
|
2921
|
+
|
|
2922
|
+
// Capture console output safely
|
|
2923
|
+
const originalLog = console.log;
|
|
2924
|
+
const originalError = console.error;
|
|
2925
|
+
|
|
2926
|
+
console.log = (...args) => logMessage("system", args.join(" "));
|
|
2927
|
+
console.error = (...args) => logMessage("error", args.join(" "));
|
|
2928
|
+
|
|
2929
|
+
try {
|
|
2930
|
+
const UfooContext = require("../context");
|
|
2931
|
+
const ctx = new UfooContext(projectRoot);
|
|
2932
|
+
|
|
2933
|
+
if (args.length === 0 || args[0] === "doctor") {
|
|
2934
|
+
await ctx.doctor();
|
|
2935
|
+
} else if (args[0] === "decisions") {
|
|
2936
|
+
await ctx.listDecisions();
|
|
2937
|
+
} else {
|
|
2938
|
+
await ctx.status();
|
|
2939
|
+
}
|
|
2940
|
+
|
|
2941
|
+
screen.render();
|
|
2942
|
+
} catch (err) {
|
|
2943
|
+
logMessage("error", `{red-fg}✗{/red-fg} Context check failed: ${err.message}`);
|
|
2944
|
+
screen.render();
|
|
2945
|
+
} finally {
|
|
2946
|
+
console.log = originalLog;
|
|
2947
|
+
console.error = originalError;
|
|
2948
|
+
}
|
|
2949
|
+
}
|
|
2950
|
+
|
|
2951
|
+
async function handleSkillsCommand(args) {
|
|
2952
|
+
const subcommand = args[0];
|
|
2953
|
+
|
|
2954
|
+
// Capture console output safely
|
|
2955
|
+
const originalLog = console.log;
|
|
2956
|
+
console.log = (...args) => logMessage("system", args.join(" "));
|
|
2957
|
+
|
|
2958
|
+
try {
|
|
2959
|
+
const UfooSkills = require("../skills");
|
|
2960
|
+
const skills = new UfooSkills(projectRoot);
|
|
2961
|
+
|
|
2962
|
+
if (subcommand === "list") {
|
|
2963
|
+
const skillList = skills.list();
|
|
2964
|
+
if (skillList.length === 0) {
|
|
2965
|
+
logMessage("system", "{gray-fg}No skills found{/gray-fg}");
|
|
2966
|
+
} else {
|
|
2967
|
+
logMessage("system", `{cyan-fg}Available skills:{/cyan-fg} ${skillList.length}`);
|
|
2968
|
+
for (const skill of skillList) {
|
|
2969
|
+
logMessage("system", ` • ${skill}`);
|
|
2970
|
+
}
|
|
2971
|
+
}
|
|
2972
|
+
} else if (subcommand === "install") {
|
|
2973
|
+
const target = args[1] || "all";
|
|
2974
|
+
logMessage("system", `{yellow-fg}⚙{/yellow-fg} Installing skills: ${target}...`);
|
|
2975
|
+
await skills.install(target);
|
|
2976
|
+
logMessage("system", "{green-fg}✓{/green-fg} Skills installed");
|
|
2977
|
+
} else {
|
|
2978
|
+
logMessage("error", "{red-fg}✗{/red-fg} Unknown skills command. Use: list, install");
|
|
2979
|
+
}
|
|
2980
|
+
|
|
2981
|
+
screen.render();
|
|
2982
|
+
} catch (err) {
|
|
2983
|
+
logMessage("error", `{red-fg}✗{/red-fg} Skills command failed: ${err.message}`);
|
|
2984
|
+
screen.render();
|
|
2985
|
+
} finally {
|
|
2986
|
+
console.log = originalLog;
|
|
2987
|
+
}
|
|
2988
|
+
}
|
|
2989
|
+
|
|
2990
|
+
async function handleLaunchCommand(args) {
|
|
2991
|
+
if (args.length === 0) {
|
|
2992
|
+
logMessage("error", "{red-fg}✗{/red-fg} Usage: /launch <claude|codex> [nickname=<name>] [count=<n>]");
|
|
2993
|
+
return;
|
|
2994
|
+
}
|
|
2995
|
+
|
|
2996
|
+
const agentType = args[0];
|
|
2997
|
+
if (agentType !== "claude" && agentType !== "codex") {
|
|
2998
|
+
logMessage("error", "{red-fg}✗{/red-fg} Unknown agent type. Use: claude or codex");
|
|
2999
|
+
return;
|
|
3000
|
+
}
|
|
3001
|
+
|
|
3002
|
+
// Parse options
|
|
3003
|
+
const options = {};
|
|
3004
|
+
for (let i = 1; i < args.length; i++) {
|
|
3005
|
+
const arg = args[i];
|
|
3006
|
+
if (arg.includes("=")) {
|
|
3007
|
+
const [key, value] = arg.split("=", 2);
|
|
3008
|
+
options[key] = value;
|
|
3009
|
+
}
|
|
3010
|
+
}
|
|
3011
|
+
|
|
3012
|
+
const nickname = options.nickname || "";
|
|
3013
|
+
const count = parseInt(options.count || "1", 10);
|
|
3014
|
+
if (nickname && count > 1) {
|
|
3015
|
+
logMessage("error", "{red-fg}✗{/red-fg} nickname requires count=1");
|
|
3016
|
+
return;
|
|
3017
|
+
}
|
|
3018
|
+
|
|
3019
|
+
try {
|
|
3020
|
+
const label = nickname ? ` (${nickname})` : "";
|
|
3021
|
+
logMessage("system", `{yellow-fg}⚙{/yellow-fg} Launching ${agentType}${label}...`);
|
|
3022
|
+
send({
|
|
3023
|
+
type: "launch_agent",
|
|
3024
|
+
agent: agentType,
|
|
3025
|
+
count: Number.isFinite(count) ? count : 1,
|
|
3026
|
+
nickname,
|
|
3027
|
+
});
|
|
3028
|
+
setTimeout(requestStatus, 1000);
|
|
3029
|
+
} catch (err) {
|
|
3030
|
+
logMessage("error", `{red-fg}✗{/red-fg} Launch failed: ${err.message}`);
|
|
3031
|
+
}
|
|
3032
|
+
}
|
|
3033
|
+
|
|
3034
|
+
async function handleResumeCommand(args) {
|
|
3035
|
+
const target = args[0] || "";
|
|
3036
|
+
const label = target ? ` (${target})` : "";
|
|
3037
|
+
logMessage("system", `{yellow-fg}⚙{/yellow-fg} Resuming agents${label}...`);
|
|
3038
|
+
send({ type: "resume_agents", target });
|
|
3039
|
+
setTimeout(requestStatus, 1000);
|
|
3040
|
+
}
|
|
3041
|
+
|
|
3042
|
+
function parseCommand(text) {
|
|
3043
|
+
if (!text.startsWith("/")) return null;
|
|
3044
|
+
|
|
3045
|
+
// Split by whitespace, respecting quotes
|
|
3046
|
+
const parts = text.match(/(?:[^\s"]+|"[^"]*")+/g) || [];
|
|
3047
|
+
if (parts.length === 0) return null;
|
|
3048
|
+
|
|
3049
|
+
const command = parts[0].slice(1); // Remove leading /
|
|
3050
|
+
const args = parts.slice(1).map(arg => arg.replace(/^"|"$/g, "")); // Remove quotes
|
|
3051
|
+
|
|
3052
|
+
return { command, args };
|
|
3053
|
+
}
|
|
3054
|
+
|
|
3055
|
+
async function executeCommand(text) {
|
|
3056
|
+
const parsed = parseCommand(text);
|
|
3057
|
+
if (!parsed) return false;
|
|
3058
|
+
|
|
3059
|
+
const { command, args } = parsed;
|
|
3060
|
+
|
|
3061
|
+
switch (command) {
|
|
3062
|
+
case "doctor":
|
|
3063
|
+
await handleDoctorCommand();
|
|
3064
|
+
return true;
|
|
3065
|
+
case "status":
|
|
3066
|
+
await handleStatusCommand();
|
|
3067
|
+
return true;
|
|
3068
|
+
case "daemon":
|
|
3069
|
+
await handleDaemonCommand(args);
|
|
3070
|
+
return true;
|
|
3071
|
+
case "init":
|
|
3072
|
+
await handleInitCommand(args);
|
|
3073
|
+
return true;
|
|
3074
|
+
case "bus":
|
|
3075
|
+
await handleBusCommand(args);
|
|
3076
|
+
return true;
|
|
3077
|
+
case "ctx":
|
|
3078
|
+
await handleCtxCommand(args);
|
|
3079
|
+
return true;
|
|
3080
|
+
case "skills":
|
|
3081
|
+
await handleSkillsCommand(args);
|
|
3082
|
+
return true;
|
|
3083
|
+
case "launch":
|
|
3084
|
+
await handleLaunchCommand(args);
|
|
3085
|
+
return true;
|
|
3086
|
+
case "resume":
|
|
3087
|
+
await handleResumeCommand(args);
|
|
3088
|
+
return true;
|
|
3089
|
+
default:
|
|
3090
|
+
logMessage("error", `{red-fg}✗{/red-fg} Unknown command: /${command}`);
|
|
3091
|
+
return true;
|
|
3092
|
+
}
|
|
3093
|
+
}
|
|
3094
|
+
|
|
3095
|
+
input.on("submit", async (value) => {
|
|
1634
3096
|
const text = value.trim();
|
|
1635
3097
|
input.clearValue();
|
|
1636
3098
|
screen.render();
|
|
1637
3099
|
if (!text) {
|
|
3100
|
+
// Empty Enter with @target → enter TTY view
|
|
3101
|
+
if (targetAgent) {
|
|
3102
|
+
const agentId = targetAgent;
|
|
3103
|
+
const sockPath = getInjectSockPath(agentId);
|
|
3104
|
+
if (fs.existsSync(sockPath)) {
|
|
3105
|
+
clearTargetAgent();
|
|
3106
|
+
enterAgentView(agentId);
|
|
3107
|
+
return;
|
|
3108
|
+
}
|
|
3109
|
+
}
|
|
1638
3110
|
input.focus();
|
|
1639
3111
|
return;
|
|
1640
3112
|
}
|
|
@@ -1643,18 +3115,58 @@ async function runChat(projectRoot) {
|
|
|
1643
3115
|
historyIndex = inputHistory.length;
|
|
1644
3116
|
historyDraft = "";
|
|
1645
3117
|
|
|
1646
|
-
// If target agent is selected,
|
|
3118
|
+
// If target agent is selected, inject directly into agent's PTY
|
|
1647
3119
|
if (targetAgent) {
|
|
1648
3120
|
const label = getAgentLabel(targetAgent);
|
|
1649
|
-
logMessage("user", `{
|
|
1650
|
-
|
|
1651
|
-
const
|
|
1652
|
-
|
|
3121
|
+
logMessage("user", `{magenta-fg}${escapeBlessed(label)}{/magenta-fg}: ${escapeBlessed(text)}`);
|
|
3122
|
+
|
|
3123
|
+
const meta = activeAgentMetaMap.get(targetAgent);
|
|
3124
|
+
const agentMode = meta?.launch_mode || "";
|
|
3125
|
+
|
|
3126
|
+
if (agentMode === "tmux" && meta?.tmux_pane) {
|
|
3127
|
+
// Tmux mode: use tmux send-keys
|
|
3128
|
+
// Send text first, then Enter after a delay (Claude Code needs time to process)
|
|
3129
|
+
const pane = meta.tmux_pane;
|
|
3130
|
+
const textProc = spawn("tmux", ["send-keys", "-t", pane, text]);
|
|
3131
|
+
textProc.on("close", () => {
|
|
3132
|
+
setTimeout(() => {
|
|
3133
|
+
spawn("tmux", ["send-keys", "-t", pane, "Enter"]);
|
|
3134
|
+
}, 150);
|
|
3135
|
+
});
|
|
3136
|
+
} else {
|
|
3137
|
+
// Terminal / internal mode: inject via inject.sock
|
|
3138
|
+
const sockPath = getInjectSockPath(targetAgent);
|
|
3139
|
+
try {
|
|
3140
|
+
const conn = net.createConnection(sockPath, () => {
|
|
3141
|
+
conn.write(JSON.stringify({ type: "raw", data: text }) + "\n");
|
|
3142
|
+
setTimeout(() => {
|
|
3143
|
+
conn.write(JSON.stringify({ type: "raw", data: "\r" }) + "\n");
|
|
3144
|
+
setTimeout(() => conn.destroy(), 500);
|
|
3145
|
+
}, 100);
|
|
3146
|
+
});
|
|
3147
|
+
conn.on("error", () => {});
|
|
3148
|
+
} catch {
|
|
3149
|
+
// ignore connection errors
|
|
3150
|
+
}
|
|
3151
|
+
}
|
|
3152
|
+
|
|
1653
3153
|
clearTargetAgent();
|
|
1654
3154
|
input.focus();
|
|
1655
3155
|
return;
|
|
1656
3156
|
}
|
|
1657
3157
|
|
|
3158
|
+
// Check if it's a command
|
|
3159
|
+
if (text.startsWith("/")) {
|
|
3160
|
+
logMessage("user", `{cyan-fg}→{/cyan-fg} ${escapeBlessed(text)}`);
|
|
3161
|
+
try {
|
|
3162
|
+
await executeCommand(text);
|
|
3163
|
+
} catch (err) {
|
|
3164
|
+
logMessage("error", `{red-fg}✗{/red-fg} Command error: ${escapeBlessed(err.message)}`);
|
|
3165
|
+
}
|
|
3166
|
+
input.focus();
|
|
3167
|
+
return;
|
|
3168
|
+
}
|
|
3169
|
+
|
|
1658
3170
|
if (pending && pending.disambiguate) {
|
|
1659
3171
|
const idx = parseInt(text, 10);
|
|
1660
3172
|
const choice = pending.disambiguate.candidates[idx - 1];
|
|
@@ -1666,25 +3178,87 @@ async function runChat(projectRoot) {
|
|
|
1666
3178
|
});
|
|
1667
3179
|
pending = null;
|
|
1668
3180
|
} else {
|
|
1669
|
-
logMessage("error", "Invalid selection.");
|
|
3181
|
+
logMessage("error", escapeBlessed("Invalid selection."));
|
|
1670
3182
|
}
|
|
1671
3183
|
} else {
|
|
1672
3184
|
pending = { original: text };
|
|
1673
3185
|
queueStatusLine("ufoo-agent processing");
|
|
1674
3186
|
send({ type: "prompt", text });
|
|
1675
|
-
logMessage("user", `{cyan-fg}→{/cyan-fg} ${text}`);
|
|
3187
|
+
logMessage("user", `{cyan-fg}→{/cyan-fg} ${escapeBlessed(text)}`);
|
|
1676
3188
|
}
|
|
1677
3189
|
input.focus();
|
|
1678
3190
|
});
|
|
1679
3191
|
|
|
1680
3192
|
screen.key(["C-c"], exitHandler);
|
|
1681
3193
|
|
|
3194
|
+
// Agent TTY view: enter dashboard mode
|
|
3195
|
+
function enterAgentDashboardMode() {
|
|
3196
|
+
focusMode = "dashboard";
|
|
3197
|
+
dashboardView = "agents";
|
|
3198
|
+
// Find the current viewing agent's index in the [ufoo, ...agents] list
|
|
3199
|
+
selectedAgentIndex = 0; // Default to ufoo for quick exit
|
|
3200
|
+
renderAgentDashboard();
|
|
3201
|
+
}
|
|
3202
|
+
|
|
3203
|
+
// Map key names to ANSI escape sequences for raw PTY passthrough
|
|
3204
|
+
function keyToRaw(ch, key) {
|
|
3205
|
+
if (ch && ch.length === 1) return ch;
|
|
3206
|
+
if (!key) return null;
|
|
3207
|
+
switch (key.name) {
|
|
3208
|
+
case "return": case "enter": return "\r";
|
|
3209
|
+
case "backspace": return "\x7f";
|
|
3210
|
+
case "tab": return "\t";
|
|
3211
|
+
case "escape": return "\x1b";
|
|
3212
|
+
case "up": return "\x1b[A";
|
|
3213
|
+
case "down": return "\x1b[B";
|
|
3214
|
+
case "right": return "\x1b[C";
|
|
3215
|
+
case "left": return "\x1b[D";
|
|
3216
|
+
case "home": return "\x1b[H";
|
|
3217
|
+
case "end": return "\x1b[F";
|
|
3218
|
+
case "pageup": return "\x1b[5~";
|
|
3219
|
+
case "pagedown": return "\x1b[6~";
|
|
3220
|
+
case "delete": return "\x1b[3~";
|
|
3221
|
+
case "insert": return "\x1b[2~";
|
|
3222
|
+
default: return ch || null;
|
|
3223
|
+
}
|
|
3224
|
+
}
|
|
3225
|
+
|
|
1682
3226
|
// Dashboard navigation - use screen.on to capture even when input is focused
|
|
1683
3227
|
screen.on("keypress", (ch, key) => {
|
|
3228
|
+
// Agent TTY view: handle keystrokes
|
|
3229
|
+
if (currentView === "agent") {
|
|
3230
|
+
if (focusMode === "dashboard") {
|
|
3231
|
+
handleDashboardKey(key);
|
|
3232
|
+
return;
|
|
3233
|
+
}
|
|
3234
|
+
// Suppress input briefly after entering agent view (prevents Enter
|
|
3235
|
+
// leak from dashboard selection and terminal query responses like CPR)
|
|
3236
|
+
if (Date.now() < agentInputSuppressUntil) {
|
|
3237
|
+
return;
|
|
3238
|
+
}
|
|
3239
|
+
// Ctrl+C exits entire app
|
|
3240
|
+
if (key && key.ctrl && key.name === "c") {
|
|
3241
|
+
return; // handled by screen.key(["C-c"])
|
|
3242
|
+
}
|
|
3243
|
+
// Down arrow: enter agents bar (same pattern as normal chat dashboard)
|
|
3244
|
+
if (key && key.name === "down") {
|
|
3245
|
+
enterAgentDashboardMode();
|
|
3246
|
+
return;
|
|
3247
|
+
}
|
|
3248
|
+
// All other keys (including Esc) go to agent PTY
|
|
3249
|
+
const raw = keyToRaw(ch, key);
|
|
3250
|
+
if (raw) {
|
|
3251
|
+
sendRawToAgent(raw);
|
|
3252
|
+
}
|
|
3253
|
+
return;
|
|
3254
|
+
}
|
|
3255
|
+
|
|
3256
|
+
// Normal mode: dashboard key handling
|
|
1684
3257
|
handleDashboardKey(key);
|
|
1685
3258
|
});
|
|
1686
3259
|
|
|
1687
3260
|
screen.key(["tab"], () => {
|
|
3261
|
+
if (currentView === "agent") return; // Tab goes to PTY via keypress handler
|
|
1688
3262
|
if (focusMode === "dashboard") {
|
|
1689
3263
|
exitDashboardMode(false);
|
|
1690
3264
|
} else {
|
|
@@ -1693,10 +3267,13 @@ async function runChat(projectRoot) {
|
|
|
1693
3267
|
});
|
|
1694
3268
|
|
|
1695
3269
|
screen.key(["C-k", "M-k"], () => {
|
|
3270
|
+
if (currentView === "agent") return;
|
|
1696
3271
|
clearLog();
|
|
1697
3272
|
});
|
|
1698
3273
|
|
|
3274
|
+
|
|
1699
3275
|
screen.key(["i", "enter"], () => {
|
|
3276
|
+
if (currentView === "agent") return;
|
|
1700
3277
|
if (focusMode === "dashboard") return;
|
|
1701
3278
|
if (screen.focused === input) return;
|
|
1702
3279
|
focusInput();
|
|
@@ -1759,8 +3336,24 @@ async function runChat(projectRoot) {
|
|
|
1759
3336
|
renderDashboard();
|
|
1760
3337
|
resizeInput();
|
|
1761
3338
|
requestStatus();
|
|
1762
|
-
|
|
3339
|
+
|
|
3340
|
+
// 定期刷新 dashboard 状态(兜底,daemon 会主动推送变化)
|
|
3341
|
+
setInterval(() => {
|
|
3342
|
+
if (client && !client.destroyed) {
|
|
3343
|
+
requestStatus();
|
|
3344
|
+
}
|
|
3345
|
+
}, 30000);
|
|
3346
|
+
|
|
1763
3347
|
screen.on("resize", () => {
|
|
3348
|
+
if (currentView === "agent") {
|
|
3349
|
+
// Update scroll region and agent PTY size for new terminal dimensions
|
|
3350
|
+
const rows = process.stdout.rows || 24;
|
|
3351
|
+
const cols = process.stdout.columns || 80;
|
|
3352
|
+
process.stdout.write(`\x1b[1;${rows - 1}r`);
|
|
3353
|
+
sendResizeToAgent(cols, rows - 1);
|
|
3354
|
+
renderAgentDashboard();
|
|
3355
|
+
return;
|
|
3356
|
+
}
|
|
1764
3357
|
resizeInput();
|
|
1765
3358
|
if (completionActive) hideCompletion();
|
|
1766
3359
|
input._updateCursor();
|