u-foo 1.0.6 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +247 -23
- package/SKILLS/ufoo/SKILL.md +17 -2
- package/SKILLS/uinit/SKILL.md +8 -3
- package/bin/ucode-core.js +15 -0
- package/bin/ucode.js +125 -0
- package/bin/ufoo-assistant-agent.js +5 -0
- package/bin/ufoo-engine.js +25 -0
- package/bin/ufoo.js +4 -0
- package/modules/AGENTS.template.md +14 -4
- package/modules/bus/README.md +8 -5
- package/modules/bus/SKILLS/ubus/SKILL.md +5 -4
- package/modules/context/SKILLS/uctx/SKILL.md +3 -1
- package/modules/online/SKILLS/ufoo-online/SKILL.md +144 -0
- package/package.json +12 -3
- package/scripts/import-pi-mono.js +124 -0
- package/scripts/postinstall.js +20 -49
- package/scripts/sync-claude-skills.sh +21 -0
- package/src/agent/cliRunner.js +524 -31
- package/src/agent/internalRunner.js +76 -9
- package/src/agent/launcher.js +97 -45
- package/src/agent/normalizeOutput.js +1 -1
- package/src/agent/notifier.js +144 -4
- package/src/agent/ptyRunner.js +480 -10
- package/src/agent/ptyWrapper.js +28 -3
- package/src/agent/readyDetector.js +16 -0
- package/src/agent/ucode.js +443 -0
- package/src/agent/ucodeBootstrap.js +113 -0
- package/src/agent/ucodeBuild.js +67 -0
- package/src/agent/ucodeDoctor.js +184 -0
- package/src/agent/ucodeRuntimeConfig.js +129 -0
- package/src/agent/ufooAgent.js +168 -28
- package/src/assistant/agent.js +260 -0
- package/src/assistant/bridge.js +172 -0
- package/src/assistant/engine.js +252 -0
- package/src/assistant/stdio.js +58 -0
- package/src/assistant/ufooEngineCli.js +306 -0
- package/src/bus/activate.js +27 -11
- package/src/bus/daemon.js +133 -5
- package/src/bus/index.js +137 -80
- package/src/bus/inject.js +47 -17
- package/src/bus/message.js +145 -17
- package/src/bus/nickname.js +3 -1
- package/src/bus/queue.js +6 -1
- package/src/bus/store.js +189 -0
- package/src/bus/subscriber.js +20 -4
- package/src/bus/utils.js +9 -3
- package/src/chat/agentBar.js +117 -0
- package/src/chat/agentDirectory.js +88 -0
- package/src/chat/agentSockets.js +225 -0
- package/src/chat/agentViewController.js +298 -0
- package/src/chat/chatLogController.js +115 -0
- package/src/chat/commandExecutor.js +700 -0
- package/src/chat/commands.js +132 -0
- package/src/chat/completionController.js +414 -0
- package/src/chat/cronScheduler.js +160 -0
- package/src/chat/daemonConnection.js +166 -0
- package/src/chat/daemonCoordinator.js +64 -0
- package/src/chat/daemonMessageRouter.js +257 -0
- package/src/chat/daemonReconnect.js +41 -0
- package/src/chat/daemonTransport.js +36 -0
- package/src/chat/daemonTransportDefaults.js +10 -0
- package/src/chat/dashboardKeyController.js +480 -0
- package/src/chat/dashboardView.js +157 -0
- package/src/chat/index.js +938 -2910
- package/src/chat/inputHistoryController.js +105 -0
- package/src/chat/inputListenerController.js +304 -0
- package/src/chat/inputMath.js +104 -0
- package/src/chat/inputSubmitHandler.js +171 -0
- package/src/chat/layout.js +165 -0
- package/src/chat/pasteController.js +81 -0
- package/src/chat/rawKeyMap.js +42 -0
- package/src/chat/settingsController.js +133 -0
- package/src/chat/statusLineController.js +177 -0
- package/src/chat/streamTracker.js +138 -0
- package/src/chat/text.js +70 -0
- package/src/chat/transport.js +61 -0
- package/src/cli/busCoreCommands.js +59 -0
- package/src/cli/ctxCoreCommands.js +199 -0
- package/src/cli/onlineCoreCommands.js +379 -0
- package/src/cli.js +741 -238
- package/src/code/README.md +29 -0
- package/src/code/UCODE_PROMPT.md +32 -0
- package/src/code/agent.js +1651 -0
- package/src/code/cli.js +158 -0
- package/src/code/config +0 -0
- package/src/code/dispatch.js +42 -0
- package/src/code/index.js +70 -0
- package/src/code/nativeRunner.js +1213 -0
- package/src/code/runtime.js +154 -0
- package/src/code/sessionStore.js +162 -0
- package/src/code/taskDecomposer.js +269 -0
- package/src/code/tools/bash.js +53 -0
- package/src/code/tools/common.js +42 -0
- package/src/code/tools/edit.js +70 -0
- package/src/code/tools/read.js +44 -0
- package/src/code/tools/write.js +35 -0
- package/src/code/tui.js +1587 -0
- package/src/config.js +50 -2
- package/src/context/decisions.js +12 -2
- package/src/context/index.js +18 -1
- package/src/context/sync.js +127 -0
- package/src/daemon/agentProcessManager.js +74 -0
- package/src/daemon/cronOps.js +241 -0
- package/src/daemon/index.js +662 -489
- package/src/daemon/ipcServer.js +99 -0
- package/src/daemon/ops.js +417 -179
- package/src/daemon/promptLoop.js +319 -0
- package/src/daemon/promptRequest.js +101 -0
- package/src/daemon/providerSessions.js +32 -17
- package/src/daemon/reporting.js +90 -0
- package/src/daemon/run.js +2 -5
- package/src/daemon/status.js +24 -1
- package/src/init/index.js +68 -14
- package/src/online/bridge.js +663 -0
- package/src/online/client.js +245 -0
- package/src/online/runner.js +253 -0
- package/src/online/server.js +992 -0
- package/src/online/tokens.js +103 -0
- package/src/report/store.js +331 -0
- package/src/shared/eventContract.js +35 -0
- package/src/shared/ptySocketContract.js +21 -0
- package/src/status/index.js +50 -17
- package/src/terminal/adapterContract.js +87 -0
- package/src/terminal/adapterRouter.js +84 -0
- package/src/terminal/adapters/externalAdapter.js +14 -0
- package/src/terminal/adapters/internalAdapter.js +13 -0
- package/src/terminal/adapters/internalPtyAdapter.js +42 -0
- package/src/terminal/adapters/internalQueueAdapter.js +37 -0
- package/src/terminal/adapters/terminalAdapter.js +31 -0
- package/src/terminal/adapters/tmuxAdapter.js +30 -0
- package/src/ufoo/agentsStore.js +69 -3
- package/src/utils/banner.js +5 -2
- package/scripts/.archived/bash-to-js-migration/README.md +0 -46
- package/scripts/.archived/bash-to-js-migration/banner.sh +0 -89
- package/scripts/.archived/bash-to-js-migration/bus-alert.sh +0 -6
- package/scripts/.archived/bash-to-js-migration/bus-autotrigger.sh +0 -6
- package/scripts/.archived/bash-to-js-migration/bus-daemon.sh +0 -231
- package/scripts/.archived/bash-to-js-migration/bus-inject.sh +0 -176
- package/scripts/.archived/bash-to-js-migration/bus-listen.sh +0 -6
- package/scripts/.archived/bash-to-js-migration/bus.sh +0 -986
- package/scripts/.archived/bash-to-js-migration/context-decisions.sh +0 -167
- package/scripts/.archived/bash-to-js-migration/context-doctor.sh +0 -72
- package/scripts/.archived/bash-to-js-migration/context-lint.sh +0 -110
- package/scripts/.archived/bash-to-js-migration/doctor.sh +0 -22
- package/scripts/.archived/bash-to-js-migration/init.sh +0 -247
- package/scripts/.archived/bash-to-js-migration/skills.sh +0 -113
- package/scripts/.archived/bash-to-js-migration/status.sh +0 -125
- package/scripts/banner.sh +0 -2
- package/src/bus/API_DESIGN.md +0 -204
package/src/chat/index.js
CHANGED
|
@@ -1,64 +1,47 @@
|
|
|
1
|
-
const net = require("net");
|
|
2
1
|
const path = require("path");
|
|
3
2
|
const blessed = require("blessed");
|
|
4
|
-
const {
|
|
3
|
+
const { execSync } = require("child_process");
|
|
5
4
|
const fs = require("fs");
|
|
6
|
-
const {
|
|
5
|
+
const {
|
|
6
|
+
loadConfig,
|
|
7
|
+
saveConfig,
|
|
8
|
+
normalizeLaunchMode,
|
|
9
|
+
normalizeAgentProvider,
|
|
10
|
+
normalizeAssistantEngine,
|
|
11
|
+
} = require("../config");
|
|
7
12
|
const { socketPath, isRunning } = require("../daemon");
|
|
8
13
|
const UfooInit = require("../init");
|
|
9
|
-
const EventBus = require("../bus");
|
|
10
14
|
const AgentActivator = require("../bus/activate");
|
|
11
|
-
const { getUfooPaths } = require("../ufoo/paths");
|
|
12
15
|
const { subscriberToSafeName } = require("../bus/utils");
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const daemonBin = resolveProjectFile(projectRoot, path.join("bin", "ufoo.js"), path.join("bin", "ufoo.js"));
|
|
43
|
-
spawnSync(process.execPath, [daemonBin, "daemon", "--stop"], {
|
|
44
|
-
stdio: "ignore",
|
|
45
|
-
cwd: projectRoot,
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
async function connectWithRetry(sockPath, retries, delayMs) {
|
|
50
|
-
for (let i = 0; i < retries; i += 1) {
|
|
51
|
-
try {
|
|
52
|
-
// eslint-disable-next-line no-await-in-loop
|
|
53
|
-
const client = await connectSocket(sockPath);
|
|
54
|
-
return client;
|
|
55
|
-
} catch {
|
|
56
|
-
// eslint-disable-next-line no-await-in-loop
|
|
57
|
-
await new Promise((r) => setTimeout(r, delayMs));
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
return null;
|
|
61
|
-
}
|
|
16
|
+
const { getUfooPaths } = require("../ufoo/paths");
|
|
17
|
+
const { startDaemon, stopDaemon, connectWithRetry } = require("./transport");
|
|
18
|
+
const { escapeBlessed, stripBlessedTags, truncateText } = require("./text");
|
|
19
|
+
const { COMMAND_REGISTRY, parseCommand, parseAtTarget } = require("./commands");
|
|
20
|
+
const inputMath = require("./inputMath");
|
|
21
|
+
const { createStreamTracker } = require("./streamTracker");
|
|
22
|
+
const agentDirectory = require("./agentDirectory");
|
|
23
|
+
const { computeAgentBar } = require("./agentBar");
|
|
24
|
+
const { createAgentSockets } = require("./agentSockets");
|
|
25
|
+
const { createDashboardKeyController } = require("./dashboardKeyController");
|
|
26
|
+
const { computeDashboardContent } = require("./dashboardView");
|
|
27
|
+
const { createCommandExecutor } = require("./commandExecutor");
|
|
28
|
+
const { createInputSubmitHandler } = require("./inputSubmitHandler");
|
|
29
|
+
const { keyToRaw } = require("./rawKeyMap");
|
|
30
|
+
const { createCompletionController } = require("./completionController");
|
|
31
|
+
const { createStatusLineController } = require("./statusLineController");
|
|
32
|
+
const { createInputHistoryController } = require("./inputHistoryController");
|
|
33
|
+
const { createInputListenerController } = require("./inputListenerController");
|
|
34
|
+
const { createDaemonMessageRouter } = require("./daemonMessageRouter");
|
|
35
|
+
const { createChatLogController } = require("./chatLogController");
|
|
36
|
+
const { createPasteController } = require("./pasteController");
|
|
37
|
+
const { createCronScheduler } = require("./cronScheduler");
|
|
38
|
+
const { createAgentViewController } = require("./agentViewController");
|
|
39
|
+
const { createSettingsController } = require("./settingsController");
|
|
40
|
+
const { createChatLayout } = require("./layout");
|
|
41
|
+
const { createDaemonCoordinator } = require("./daemonCoordinator");
|
|
42
|
+
const { IPC_REQUEST_TYPES } = require("../shared/eventContract");
|
|
43
|
+
const { createTerminalAdapterRouter } = require("../terminal/adapterRouter");
|
|
44
|
+
const { createDaemonTransport } = require("./daemonTransport");
|
|
62
45
|
|
|
63
46
|
async function runChat(projectRoot) {
|
|
64
47
|
if (!fs.existsSync(getUfooPaths(projectRoot).ufooDir)) {
|
|
@@ -89,687 +72,153 @@ async function runChat(projectRoot) {
|
|
|
89
72
|
startDaemon(projectRoot);
|
|
90
73
|
}
|
|
91
74
|
|
|
92
|
-
const daemonBin = resolveProjectFile(projectRoot, path.join("bin", "ufoo.js"), path.join("bin", "ufoo.js"));
|
|
93
75
|
const sock = socketPath(projectRoot);
|
|
94
|
-
let
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const connectClient = async () => {
|
|
102
|
-
let newClient = await connectWithRetry(sock, 25, 200);
|
|
103
|
-
if (!newClient) {
|
|
104
|
-
// Retry once with a fresh daemon start and longer wait.
|
|
105
|
-
if (!isRunning(projectRoot)) {
|
|
106
|
-
startDaemon(projectRoot);
|
|
107
|
-
// Wait for daemon to write PID file and create socket
|
|
108
|
-
await new Promise(r => setTimeout(r, 1000));
|
|
109
|
-
}
|
|
110
|
-
newClient = await connectWithRetry(sock, 50, 200);
|
|
111
|
-
}
|
|
112
|
-
return newClient;
|
|
113
|
-
};
|
|
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
|
-
|
|
157
|
-
client = await connectClient();
|
|
158
|
-
if (!client) {
|
|
159
|
-
// Check if daemon failed to start
|
|
160
|
-
if (!isRunning(projectRoot)) {
|
|
161
|
-
const logFile = getUfooPaths(projectRoot).ufooDaemonLog;
|
|
162
|
-
// eslint-disable-next-line no-console
|
|
163
|
-
console.error("Failed to start ufoo daemon. Check logs at:", logFile);
|
|
164
|
-
throw new Error("Daemon failed to start. Check the daemon log for details.");
|
|
165
|
-
}
|
|
166
|
-
throw new Error("Failed to connect to ufoo daemon (timeout). The daemon may still be starting.");
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const screen = blessed.screen({
|
|
170
|
-
smartCSR: true,
|
|
171
|
-
title: "ufoo chat",
|
|
172
|
-
fullUnicode: true,
|
|
173
|
-
// Toggle mouse at runtime to balance copy vs scroll
|
|
174
|
-
sendFocus: true,
|
|
175
|
-
mouse: false,
|
|
176
|
-
// Allow Ctrl+C to exit even when input grabs keys
|
|
177
|
-
ignoreLocked: ["C-c"],
|
|
76
|
+
let daemonCoordinator = null;
|
|
77
|
+
const daemonTransport = createDaemonTransport({
|
|
78
|
+
projectRoot,
|
|
79
|
+
sockPath: sock,
|
|
80
|
+
isRunning,
|
|
81
|
+
startDaemon,
|
|
82
|
+
connectWithRetry,
|
|
178
83
|
});
|
|
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
|
-
}
|
|
190
84
|
|
|
191
85
|
const config = loadConfig(projectRoot);
|
|
192
86
|
let launchMode = config.launchMode;
|
|
193
87
|
let agentProvider = config.agentProvider;
|
|
88
|
+
let assistantEngine = normalizeAssistantEngine(config.assistantEngine);
|
|
194
89
|
let autoResume = config.autoResume !== false;
|
|
90
|
+
let cronScheduler = {
|
|
91
|
+
addTask: () => null,
|
|
92
|
+
listTasks: () => [],
|
|
93
|
+
stopTask: () => false,
|
|
94
|
+
stopAll: () => 0,
|
|
95
|
+
};
|
|
195
96
|
|
|
196
97
|
// Dynamic input height settings
|
|
197
98
|
// Layout: topLine(1) + content + bottomLine(1) + dashboard(1)
|
|
198
99
|
const MIN_INPUT_HEIGHT = 4; // 1 content + 3
|
|
199
100
|
const MAX_INPUT_HEIGHT = 9; // 6 content + 3
|
|
200
101
|
let currentInputHeight = MIN_INPUT_HEIGHT;
|
|
201
|
-
|
|
202
|
-
// Log area (no border for cleaner look)
|
|
203
|
-
const logBox = blessed.log({
|
|
204
|
-
parent: screen,
|
|
205
|
-
top: 0,
|
|
206
|
-
left: 0,
|
|
207
|
-
width: "100%",
|
|
208
|
-
height: "100%-5", // Will be adjusted dynamically
|
|
209
|
-
tags: true,
|
|
210
|
-
scrollable: true,
|
|
211
|
-
alwaysScroll: true,
|
|
212
|
-
scrollback: 10000,
|
|
213
|
-
scrollbar: null,
|
|
214
|
-
keys: true,
|
|
215
|
-
vi: true,
|
|
216
|
-
// Mouse handled globally (toggleable) to keep copy working
|
|
217
|
-
mouse: false,
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
// Status line just above input
|
|
221
|
-
const statusLine = blessed.box({
|
|
222
|
-
parent: screen,
|
|
223
|
-
bottom: currentInputHeight,
|
|
224
|
-
left: 0,
|
|
225
|
-
width: "100%",
|
|
226
|
-
height: 1,
|
|
227
|
-
style: { fg: "gray" },
|
|
228
|
-
tags: true,
|
|
229
|
-
content: "",
|
|
230
|
-
});
|
|
231
102
|
const pkg = require("../../package.json");
|
|
232
|
-
const
|
|
233
|
-
|
|
103
|
+
const {
|
|
104
|
+
screen,
|
|
105
|
+
logBox,
|
|
106
|
+
statusLine,
|
|
107
|
+
bannerText,
|
|
108
|
+
completionPanel,
|
|
109
|
+
dashboard,
|
|
110
|
+
inputBottomLine,
|
|
111
|
+
promptBox,
|
|
112
|
+
input,
|
|
113
|
+
inputTopLine,
|
|
114
|
+
} = createChatLayout({
|
|
115
|
+
blessed,
|
|
116
|
+
currentInputHeight,
|
|
117
|
+
version: pkg.version,
|
|
118
|
+
});
|
|
234
119
|
|
|
235
120
|
const historyDir = path.join(getUfooPaths(projectRoot).ufooDir, "chat");
|
|
236
121
|
const historyFile = path.join(historyDir, "history.jsonl");
|
|
237
122
|
const inputHistoryFile = path.join(historyDir, "input-history.jsonl");
|
|
238
123
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
fs
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
let lastLogWasSpacer = false;
|
|
246
|
-
let lastLogType = null;
|
|
247
|
-
let hasLoggedAny = false;
|
|
248
|
-
|
|
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;
|
|
253
|
-
}
|
|
124
|
+
const chatLogController = createChatLogController({
|
|
125
|
+
logBox,
|
|
126
|
+
fsModule: fs,
|
|
127
|
+
historyDir,
|
|
128
|
+
historyFile,
|
|
129
|
+
});
|
|
254
130
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
if (writeHistory) {
|
|
263
|
-
appendHistory({
|
|
264
|
-
ts: new Date().toISOString(),
|
|
265
|
-
type: "spacer",
|
|
266
|
-
text: "",
|
|
267
|
-
meta: {},
|
|
268
|
-
});
|
|
269
|
-
}
|
|
270
|
-
lastLogWasSpacer = true;
|
|
271
|
-
lastLogType = "spacer";
|
|
272
|
-
hasLoggedAny = true;
|
|
273
|
-
}
|
|
131
|
+
const streamTracker = createStreamTracker({
|
|
132
|
+
logBox,
|
|
133
|
+
writeSpacer: () => chatLogController.writeSpacer(false),
|
|
134
|
+
appendHistory: (...args) => chatLogController.appendHistory(...args),
|
|
135
|
+
escapeBlessed,
|
|
136
|
+
onStreamStart: () => chatLogController.markStreamStart(),
|
|
137
|
+
});
|
|
274
138
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
if (writeHistory) {
|
|
282
|
-
appendHistory({
|
|
283
|
-
ts: new Date().toISOString(),
|
|
284
|
-
type,
|
|
285
|
-
text: lineText,
|
|
286
|
-
meta,
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
|
-
lastLogWasSpacer = false;
|
|
290
|
-
lastLogType = type;
|
|
291
|
-
hasLoggedAny = true;
|
|
292
|
-
}
|
|
139
|
+
const beginStream = (...args) => streamTracker.beginStream(...args);
|
|
140
|
+
const appendStreamDelta = (...args) => streamTracker.appendStreamDelta(...args);
|
|
141
|
+
const finalizeStream = (...args) => streamTracker.finalizeStream(...args);
|
|
142
|
+
const markPendingDelivery = (...args) => streamTracker.markPendingDelivery(...args);
|
|
143
|
+
const getPendingState = (...args) => streamTracker.getPendingState(...args);
|
|
144
|
+
const consumePendingDelivery = (...args) => streamTracker.consumePendingDelivery(...args);
|
|
293
145
|
|
|
294
146
|
function logMessage(type, text, meta = {}) {
|
|
295
|
-
|
|
296
|
-
}
|
|
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));
|
|
147
|
+
chatLogController.logMessage(type, text, meta);
|
|
329
148
|
}
|
|
330
149
|
|
|
331
150
|
function loadHistory(limit = 2000) {
|
|
332
|
-
|
|
333
|
-
const lines = fs.readFileSync(historyFile, "utf8").trim().split(/\r?\n/).filter(Boolean);
|
|
334
|
-
const items = lines.slice(-limit).map((line) => JSON.parse(line));
|
|
335
|
-
const hasSpacer = items.some((item) => item && item.type === "spacer");
|
|
336
|
-
for (const item of items) {
|
|
337
|
-
if (!item) continue;
|
|
338
|
-
if (item.type === "spacer") {
|
|
339
|
-
writeSpacer(false);
|
|
340
|
-
continue;
|
|
341
|
-
}
|
|
342
|
-
if (!item.text) continue;
|
|
343
|
-
if (hasSpacer) {
|
|
344
|
-
appendToLogBox(item.text);
|
|
345
|
-
lastLogWasSpacer = false;
|
|
346
|
-
lastLogType = item.type || null;
|
|
347
|
-
hasLoggedAny = true;
|
|
348
|
-
} else {
|
|
349
|
-
recordLog(item.type || "unknown", item.text, item.meta || {}, false);
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
} catch {
|
|
353
|
-
// ignore missing/invalid history
|
|
354
|
-
}
|
|
151
|
+
chatLogController.loadHistory(limit);
|
|
355
152
|
}
|
|
356
153
|
|
|
357
|
-
|
|
358
|
-
let historyIndex = 0;
|
|
359
|
-
let historyDraft = "";
|
|
360
|
-
|
|
361
|
-
function appendInputHistory(text) {
|
|
362
|
-
if (!text) return;
|
|
363
|
-
fs.mkdirSync(historyDir, { recursive: true });
|
|
364
|
-
fs.appendFileSync(inputHistoryFile, `${JSON.stringify({ text })}\n`);
|
|
365
|
-
}
|
|
154
|
+
let inputHistoryController = null;
|
|
366
155
|
|
|
367
156
|
function loadInputHistory(limit = 2000) {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
const items = lines.slice(-limit).map((line) => JSON.parse(line));
|
|
371
|
-
for (const item of items) {
|
|
372
|
-
if (item && typeof item.text === "string" && item.text.trim() !== "") {
|
|
373
|
-
inputHistory.push(item.text);
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
} catch {
|
|
377
|
-
// ignore missing/invalid history
|
|
378
|
-
}
|
|
379
|
-
historyIndex = inputHistory.length;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
const pendingStatusLines = [];
|
|
383
|
-
const busStatusQueue = [];
|
|
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;
|
|
393
|
-
|
|
394
|
-
function formatProcessingText(text) {
|
|
395
|
-
if (!text) return text;
|
|
396
|
-
if (text.includes("{")) return text;
|
|
397
|
-
if (!/processing/i.test(text)) return text;
|
|
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}`;
|
|
157
|
+
if (!inputHistoryController) return;
|
|
158
|
+
inputHistoryController.loadInputHistory(limit);
|
|
441
159
|
}
|
|
442
160
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
}
|
|
448
|
-
if (busStatusQueue.length > 0) {
|
|
449
|
-
const extra = busStatusQueue.length > 1
|
|
450
|
-
? ` {gray-fg}(+${busStatusQueue.length - 1}){/gray-fg}`
|
|
451
|
-
: "";
|
|
452
|
-
const busText = `${busStatusQueue[0].text}${extra}`;
|
|
453
|
-
content = content
|
|
454
|
-
? `${content} {gray-fg}·{/gray-fg} ${busText}`
|
|
455
|
-
: busText;
|
|
456
|
-
}
|
|
457
|
-
statusLine.setContent(content);
|
|
458
|
-
}
|
|
459
|
-
|
|
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 = {}) {
|
|
474
|
-
primaryStatusText = text || "";
|
|
475
|
-
primaryStatusPending = Boolean(options.pending);
|
|
476
|
-
updateStatusAnimation();
|
|
477
|
-
renderStatusLine();
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
function queueStatusLine(text) {
|
|
481
|
-
let raw = text || "";
|
|
482
|
-
pendingStatusLines.push(raw);
|
|
483
|
-
if (pendingStatusLines.length === 1) {
|
|
484
|
-
setPrimaryStatus(raw, { pending: true });
|
|
485
|
-
screen.render();
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
function resolveStatusLine(text) {
|
|
490
|
-
if (pendingStatusLines.length > 0) {
|
|
491
|
-
pendingStatusLines.shift();
|
|
492
|
-
}
|
|
493
|
-
if (pendingStatusLines.length > 0) {
|
|
494
|
-
setPrimaryStatus(pendingStatusLines[0], { pending: true });
|
|
495
|
-
} else {
|
|
496
|
-
setPrimaryStatus(text || "", { pending: false });
|
|
497
|
-
}
|
|
498
|
-
screen.render();
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
function enqueueBusStatus(item) {
|
|
502
|
-
if (!item || !item.text) return;
|
|
503
|
-
const rawText = item.text == null ? "" : String(item.text);
|
|
504
|
-
const key = item.key || rawText;
|
|
505
|
-
const formatted = escapeBlessed(formatProcessingText(rawText));
|
|
506
|
-
const existing = busStatusQueue.find((entry) => entry.key === key);
|
|
507
|
-
if (existing) {
|
|
508
|
-
existing.text = formatted;
|
|
509
|
-
} else {
|
|
510
|
-
busStatusQueue.push({ key, text: formatted });
|
|
511
|
-
}
|
|
512
|
-
renderStatusLine();
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
function resolveBusStatus(item) {
|
|
516
|
-
if (!item) return;
|
|
517
|
-
const rawText = item.text == null ? "" : String(item.text);
|
|
518
|
-
const key = item.key || rawText;
|
|
519
|
-
let index = -1;
|
|
520
|
-
if (key) {
|
|
521
|
-
index = busStatusQueue.findIndex((entry) => entry.key === key);
|
|
522
|
-
}
|
|
523
|
-
if (index === -1 && item.text) {
|
|
524
|
-
index = busStatusQueue.findIndex((entry) => entry.text === item.text);
|
|
525
|
-
}
|
|
526
|
-
if (index === -1) return;
|
|
527
|
-
busStatusQueue.splice(index, 1);
|
|
528
|
-
renderStatusLine();
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
// Command completion panel
|
|
532
|
-
const completionPanel = blessed.box({
|
|
533
|
-
parent: screen,
|
|
534
|
-
bottom: currentInputHeight - 1,
|
|
535
|
-
left: 0,
|
|
536
|
-
width: "100%",
|
|
537
|
-
height: 0,
|
|
538
|
-
hidden: true,
|
|
539
|
-
wrap: false,
|
|
540
|
-
border: {
|
|
541
|
-
type: "line",
|
|
542
|
-
top: true,
|
|
543
|
-
left: false,
|
|
544
|
-
right: false,
|
|
545
|
-
bottom: false
|
|
546
|
-
},
|
|
547
|
-
style: {
|
|
548
|
-
border: { fg: "yellow" },
|
|
549
|
-
fg: "white"
|
|
550
|
-
// No bg - uses terminal default background
|
|
551
|
-
},
|
|
552
|
-
padding: {
|
|
553
|
-
left: 0,
|
|
554
|
-
right: 0,
|
|
555
|
-
top: 0,
|
|
556
|
-
bottom: 0
|
|
557
|
-
},
|
|
558
|
-
tags: true,
|
|
559
|
-
});
|
|
560
|
-
|
|
561
|
-
// Dashboard at very bottom
|
|
562
|
-
const dashboard = blessed.box({
|
|
563
|
-
parent: screen,
|
|
564
|
-
bottom: 0,
|
|
565
|
-
left: 0,
|
|
566
|
-
width: "100%",
|
|
567
|
-
height: 1,
|
|
568
|
-
style: { fg: "gray" },
|
|
569
|
-
tags: true,
|
|
570
|
-
});
|
|
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
|
-
|
|
581
|
-
// Bottom border line for input area (above dashboard)
|
|
582
|
-
const inputBottomLine = blessed.line({
|
|
583
|
-
parent: screen,
|
|
584
|
-
bottom: 1,
|
|
585
|
-
left: 0,
|
|
586
|
-
width: "100%",
|
|
587
|
-
orientation: "horizontal",
|
|
588
|
-
style: { fg: "cyan" },
|
|
589
|
-
});
|
|
590
|
-
|
|
591
|
-
// Prompt indicator
|
|
592
|
-
const promptBox = blessed.box({
|
|
593
|
-
parent: screen,
|
|
594
|
-
bottom: 2,
|
|
595
|
-
left: 0,
|
|
596
|
-
width: 2,
|
|
597
|
-
height: currentInputHeight - 3,
|
|
598
|
-
content: ">",
|
|
599
|
-
style: { fg: "cyan" },
|
|
161
|
+
const statusLineController = createStatusLineController({
|
|
162
|
+
statusLine,
|
|
163
|
+
bannerText,
|
|
164
|
+
renderScreen: () => screen.render(),
|
|
600
165
|
});
|
|
601
166
|
|
|
602
|
-
|
|
603
|
-
const
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
style: { fg: "cyan" },
|
|
167
|
+
const queueStatusLine = (...args) => statusLineController.queueStatusLine(...args);
|
|
168
|
+
const resolveStatusLine = (...args) => statusLineController.resolveStatusLine(...args);
|
|
169
|
+
const enqueueBusStatus = (...args) => statusLineController.enqueueBusStatus(...args);
|
|
170
|
+
const resolveBusStatus = (...args) => statusLineController.resolveBusStatus(...args);
|
|
171
|
+
|
|
172
|
+
let agentViewController = null;
|
|
173
|
+
let terminalAdapterRouter = null;
|
|
174
|
+
const agentSockets = createAgentSockets({
|
|
175
|
+
onTermWrite: (text) => writeToAgentTerm(text),
|
|
176
|
+
onPlaceCursor: (cursor) => placeAgentCursor(cursor),
|
|
177
|
+
isAgentView: () => getCurrentView() === "agent",
|
|
178
|
+
isBusMode: () => isAgentViewUsesBus(),
|
|
179
|
+
getViewingAgent: () => getViewingAgent(),
|
|
180
|
+
sendBusRaw: (target, data) => {
|
|
181
|
+
send({
|
|
182
|
+
type: IPC_REQUEST_TYPES.BUS_SEND,
|
|
183
|
+
target,
|
|
184
|
+
message: JSON.stringify({ raw: true, data }),
|
|
185
|
+
});
|
|
186
|
+
},
|
|
623
187
|
});
|
|
624
188
|
|
|
625
189
|
// Add cursor position tracking
|
|
626
190
|
let cursorPos = 0;
|
|
627
191
|
let preferredCol = null;
|
|
628
|
-
const unicode = blessed.unicode;
|
|
629
|
-
const wideRegex = new RegExp(unicode.chars.all.source);
|
|
630
192
|
|
|
631
|
-
// Get inner width
|
|
632
193
|
function getInnerWidth() {
|
|
633
|
-
const lpos = input.lpos || input._getCoords();
|
|
634
|
-
if (lpos && Number.isFinite(lpos.xl) && Number.isFinite(lpos.xi)) {
|
|
635
|
-
return Math.max(1, lpos.xl - lpos.xi + 1);
|
|
636
|
-
}
|
|
637
|
-
if (typeof input.width === "number") return Math.max(1, input.width);
|
|
638
|
-
if (typeof input.width === "string") {
|
|
639
|
-
const match = input.width.match(/^100%-([0-9]+)$/);
|
|
640
|
-
if (match && typeof screen.width === "number") {
|
|
641
|
-
return Math.max(1, screen.width - parseInt(match[1], 10));
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
194
|
const promptWidth = typeof promptBox.width === "number" ? promptBox.width : 2;
|
|
645
|
-
|
|
646
|
-
if (typeof screen.cols === "number") return Math.max(1, screen.cols - promptWidth);
|
|
647
|
-
return 1;
|
|
195
|
+
return inputMath.getInnerWidth({ input, screen, promptWidth });
|
|
648
196
|
}
|
|
649
197
|
|
|
650
198
|
function getWrapWidth() {
|
|
651
|
-
|
|
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;
|
|
199
|
+
return inputMath.getWrapWidth(input, getInnerWidth());
|
|
707
200
|
}
|
|
708
201
|
|
|
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)
|
|
724
202
|
function countLines(text, width) {
|
|
725
|
-
|
|
726
|
-
const lines = (text || "").split("\n");
|
|
727
|
-
let total = 0;
|
|
728
|
-
for (const line of lines) {
|
|
729
|
-
const lineWidth = visualLength(line);
|
|
730
|
-
total += Math.max(1, Math.ceil(lineWidth / width));
|
|
731
|
-
}
|
|
732
|
-
return total;
|
|
203
|
+
return inputMath.countLines(text, width, (value) => input.strWidth(value));
|
|
733
204
|
}
|
|
734
205
|
|
|
735
206
|
function getCursorRowCol(text, pos, width) {
|
|
736
|
-
|
|
737
|
-
const before = (text || "").slice(0, pos);
|
|
738
|
-
const transformed = transformText(before);
|
|
739
|
-
const lines = transformed.split("\n");
|
|
740
|
-
let row = 0;
|
|
741
|
-
for (let i = 0; i < lines.length - 1; i++) {
|
|
742
|
-
const lineWidth = lines[i].length;
|
|
743
|
-
row += Math.max(1, Math.ceil(lineWidth / width));
|
|
744
|
-
}
|
|
745
|
-
const lastLine = lines[lines.length - 1] || "";
|
|
746
|
-
const lastWidth = lastLine.length;
|
|
747
|
-
row += Math.floor(lastWidth / width);
|
|
748
|
-
const col = lastWidth % width;
|
|
749
|
-
return { row, col };
|
|
207
|
+
return inputMath.getCursorRowCol(text, pos, width, (value) => input.strWidth(value));
|
|
750
208
|
}
|
|
751
209
|
|
|
752
210
|
function getCursorPosForRowCol(text, targetRow, targetCol, width) {
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
if (targetRow < row + wrappedRows) {
|
|
761
|
-
const rowInLine = targetRow - row;
|
|
762
|
-
const visualCol = rowInLine * width + Math.max(0, targetCol);
|
|
763
|
-
return pos + originalIndexForVisual(line, Math.min(visualCol, lineWidth));
|
|
764
|
-
}
|
|
765
|
-
pos += line.length + 1;
|
|
766
|
-
row += wrappedRows;
|
|
767
|
-
}
|
|
768
|
-
return text.length;
|
|
211
|
+
return inputMath.getCursorPosForRowCol(
|
|
212
|
+
text,
|
|
213
|
+
targetRow,
|
|
214
|
+
targetCol,
|
|
215
|
+
width,
|
|
216
|
+
(value) => input.strWidth(value),
|
|
217
|
+
);
|
|
769
218
|
}
|
|
770
219
|
|
|
771
220
|
function ensureInputCursorVisible() {
|
|
772
|
-
const innerWidth =
|
|
221
|
+
const innerWidth = getInnerWidth();
|
|
773
222
|
if (innerWidth <= 0) return;
|
|
774
223
|
const totalRows = countLines(input.value, innerWidth);
|
|
775
224
|
const visibleRows = Math.max(1, input.height || 1);
|
|
@@ -800,33 +249,21 @@ async function runChat(projectRoot) {
|
|
|
800
249
|
preferredCol = null;
|
|
801
250
|
}
|
|
802
251
|
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
let pasteBuffer = "";
|
|
807
|
-
let pasteRemainder = "";
|
|
808
|
-
let suppressKeypress = false;
|
|
809
|
-
let suppressReset = null;
|
|
252
|
+
function getPreferredCol() {
|
|
253
|
+
return preferredCol;
|
|
254
|
+
}
|
|
810
255
|
|
|
811
|
-
function
|
|
812
|
-
|
|
813
|
-
if (suppressReset) clearImmediate(suppressReset);
|
|
814
|
-
suppressReset = setImmediate(() => {
|
|
815
|
-
if (!pasteActive) suppressKeypress = false;
|
|
816
|
-
});
|
|
256
|
+
function setPreferredCol(value) {
|
|
257
|
+
preferredCol = value;
|
|
817
258
|
}
|
|
818
259
|
|
|
819
260
|
function normalizePaste(text) {
|
|
820
|
-
|
|
821
|
-
let normalized = text.replace(/\x1b\[200~|\x1b\[201~/g, "");
|
|
822
|
-
normalized = normalized.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
823
|
-
return normalized;
|
|
261
|
+
return inputMath.normalizePaste(text);
|
|
824
262
|
}
|
|
825
263
|
|
|
826
264
|
function updateDraftFromInput() {
|
|
827
|
-
if (
|
|
828
|
-
|
|
829
|
-
}
|
|
265
|
+
if (!inputHistoryController) return;
|
|
266
|
+
inputHistoryController.updateDraftFromInput();
|
|
830
267
|
}
|
|
831
268
|
|
|
832
269
|
function normalizeCommandPrefix() {
|
|
@@ -861,639 +298,156 @@ async function runChat(projectRoot) {
|
|
|
861
298
|
screen.render();
|
|
862
299
|
}
|
|
863
300
|
|
|
301
|
+
inputHistoryController = createInputHistoryController({
|
|
302
|
+
inputHistoryFile,
|
|
303
|
+
historyDir,
|
|
304
|
+
setInputValue,
|
|
305
|
+
getInputValue: () => input.value || "",
|
|
306
|
+
});
|
|
307
|
+
|
|
864
308
|
function historyUp() {
|
|
865
|
-
if (
|
|
866
|
-
|
|
867
|
-
historyDraft = input.value;
|
|
868
|
-
}
|
|
869
|
-
if (historyIndex > 0) {
|
|
870
|
-
historyIndex -= 1;
|
|
871
|
-
setInputValue(inputHistory[historyIndex]);
|
|
872
|
-
return true;
|
|
873
|
-
}
|
|
874
|
-
return true;
|
|
309
|
+
if (!inputHistoryController) return false;
|
|
310
|
+
return inputHistoryController.historyUp();
|
|
875
311
|
}
|
|
876
312
|
|
|
877
313
|
function historyDown() {
|
|
878
|
-
if (
|
|
879
|
-
|
|
880
|
-
historyIndex += 1;
|
|
881
|
-
setInputValue(inputHistory[historyIndex]);
|
|
882
|
-
return true;
|
|
883
|
-
}
|
|
884
|
-
if (historyIndex === inputHistory.length - 1) {
|
|
885
|
-
historyIndex = inputHistory.length;
|
|
886
|
-
setInputValue(historyDraft || "");
|
|
887
|
-
return true;
|
|
888
|
-
}
|
|
889
|
-
return false;
|
|
314
|
+
if (!inputHistoryController) return false;
|
|
315
|
+
return inputHistoryController.historyDown();
|
|
890
316
|
}
|
|
891
317
|
|
|
892
318
|
function exitHandler() {
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
319
|
+
if (daemonCoordinator) {
|
|
320
|
+
daemonCoordinator.markExit();
|
|
321
|
+
}
|
|
322
|
+
cronScheduler.stopAll();
|
|
323
|
+
exitAgentView();
|
|
897
324
|
if (screen && screen.program && typeof screen.program.decrst === "function") {
|
|
898
325
|
screen.program.decrst(2004);
|
|
899
326
|
}
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
}
|
|
904
|
-
if (client) {
|
|
905
|
-
client.end();
|
|
327
|
+
statusLineController.destroy();
|
|
328
|
+
if (daemonCoordinator) {
|
|
329
|
+
daemonCoordinator.close();
|
|
906
330
|
}
|
|
907
331
|
process.exit(0);
|
|
908
332
|
}
|
|
909
333
|
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
if (!trimmed) {
|
|
932
|
-
hideCompletion();
|
|
933
|
-
return;
|
|
934
|
-
}
|
|
935
|
-
filterText = trimmed;
|
|
936
|
-
|
|
937
|
-
// Check if we're in subcommand mode
|
|
938
|
-
const parts = filterText.split(/\s+/);
|
|
939
|
-
let commands = [];
|
|
940
|
-
|
|
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("/")) {
|
|
946
|
-
// Subcommand mode: "/bus rename"
|
|
947
|
-
const subFilter = parts[1] || "";
|
|
948
|
-
|
|
949
|
-
// Find the main command
|
|
950
|
-
const mainCmdObj = COMMAND_REGISTRY.find(item =>
|
|
951
|
-
item.cmd.toLowerCase() === mainCmd.toLowerCase()
|
|
952
|
-
);
|
|
953
|
-
|
|
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
|
-
}
|
|
982
|
-
}
|
|
983
|
-
} else {
|
|
984
|
-
// Main command mode: "/bus"
|
|
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" }));
|
|
989
|
-
}
|
|
334
|
+
const completionController = createCompletionController({
|
|
335
|
+
input,
|
|
336
|
+
screen,
|
|
337
|
+
completionPanel,
|
|
338
|
+
promptBox,
|
|
339
|
+
commandRegistry: COMMAND_REGISTRY,
|
|
340
|
+
getMentionCandidates: () => activeAgents.map((id) => ({
|
|
341
|
+
id,
|
|
342
|
+
label: getAgentLabel(id),
|
|
343
|
+
})),
|
|
344
|
+
normalizeCommandPrefix,
|
|
345
|
+
truncateText,
|
|
346
|
+
getCurrentInputHeight: () => currentInputHeight,
|
|
347
|
+
getCursorPos: () => cursorPos,
|
|
348
|
+
setCursorPos: (value) => {
|
|
349
|
+
cursorPos = value;
|
|
350
|
+
},
|
|
351
|
+
resetPreferredCol,
|
|
352
|
+
updateDraftFromInput,
|
|
353
|
+
renderScreen: () => screen.render(),
|
|
354
|
+
});
|
|
990
355
|
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
356
|
+
const pasteController = createPasteController({
|
|
357
|
+
shouldHandle: () => screen.focused === input && focusMode === "input",
|
|
358
|
+
normalizePaste,
|
|
359
|
+
insertTextAtCursor,
|
|
360
|
+
});
|
|
995
361
|
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
362
|
+
const inputListenerController = createInputListenerController({
|
|
363
|
+
getCurrentView: () => getCurrentView(),
|
|
364
|
+
exitHandler,
|
|
365
|
+
getFocusMode: () => focusMode,
|
|
366
|
+
getDashboardView: () => dashboardView,
|
|
367
|
+
getSelectedAgentIndex: () => selectedAgentIndex,
|
|
368
|
+
getActiveAgents: () => activeAgents,
|
|
369
|
+
getTargetAgent: () => targetAgent,
|
|
370
|
+
requestCloseAgent,
|
|
371
|
+
logMessage,
|
|
372
|
+
isSuppressKeypress: () => pasteController.isSuppressKeypress(),
|
|
373
|
+
normalizeCommandPrefix,
|
|
374
|
+
handleDashboardKey,
|
|
375
|
+
exitDashboardMode,
|
|
376
|
+
completionController,
|
|
377
|
+
getLogHeight: () => logBox.height,
|
|
378
|
+
scrollLog,
|
|
379
|
+
insertTextAtCursor,
|
|
380
|
+
normalizePaste,
|
|
381
|
+
resetPreferredCol,
|
|
382
|
+
getCursorPos: () => cursorPos,
|
|
383
|
+
setCursorPos: (value) => {
|
|
384
|
+
cursorPos = value;
|
|
385
|
+
},
|
|
386
|
+
ensureInputCursorVisible,
|
|
387
|
+
getWrapWidth,
|
|
388
|
+
getCursorRowCol,
|
|
389
|
+
countLines,
|
|
390
|
+
getCursorPosForRowCol,
|
|
391
|
+
getPreferredCol,
|
|
392
|
+
setPreferredCol,
|
|
393
|
+
historyUp,
|
|
394
|
+
historyDown,
|
|
395
|
+
enterDashboardMode,
|
|
396
|
+
resizeInput,
|
|
397
|
+
updateDraftFromInput,
|
|
398
|
+
});
|
|
1022
399
|
|
|
1023
|
-
|
|
1024
|
-
|
|
400
|
+
// Resize input box based on content
|
|
401
|
+
function resizeInput() {
|
|
402
|
+
const innerWidth = getWrapWidth();
|
|
403
|
+
if (innerWidth <= 0) return;
|
|
1025
404
|
|
|
1026
|
-
|
|
1027
|
-
const
|
|
1028
|
-
const
|
|
1029
|
-
? Math.max(1, Math.min(completionVisibleCount, panelVisible))
|
|
1030
|
-
: panelVisible;
|
|
405
|
+
const numLines = countLines(input.value, innerWidth);
|
|
406
|
+
const contentHeight = Math.min(MAX_INPUT_HEIGHT - 3, Math.max(1, numLines));
|
|
407
|
+
const targetHeight = contentHeight + 3; // +1 topLine +1 bottomLine +1 dashboard
|
|
1031
408
|
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
409
|
+
if (targetHeight !== currentInputHeight) {
|
|
410
|
+
currentInputHeight = targetHeight;
|
|
411
|
+
input.height = contentHeight;
|
|
412
|
+
promptBox.height = contentHeight;
|
|
413
|
+
inputTopLine.bottom = currentInputHeight - 1; // Just above input area
|
|
1037
414
|
}
|
|
1038
|
-
|
|
1039
|
-
//
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
const panelWidth = typeof completionPanel.width === "number"
|
|
1045
|
-
? completionPanel.width
|
|
1046
|
-
: screen.width;
|
|
1047
|
-
const lines = visibleCommands.map((item, i) => {
|
|
1048
|
-
const actualIndex = visibleStart + i;
|
|
1049
|
-
const cmdText = item.cmd;
|
|
1050
|
-
const descText = item.desc || "";
|
|
1051
|
-
const cmdPart = actualIndex === completionIndex
|
|
1052
|
-
? `{inverse}${cmdText}{/inverse}`
|
|
1053
|
-
: `{cyan-fg}${cmdText}{/cyan-fg}`;
|
|
1054
|
-
const indent = " ".repeat(promptBox.width || 2);
|
|
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}`;
|
|
1062
|
-
});
|
|
1063
|
-
|
|
1064
|
-
completionPanel.setContent(lines.join("\n"));
|
|
1065
|
-
screen.render();
|
|
1066
|
-
}
|
|
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
|
-
|
|
1075
|
-
function completionUp() {
|
|
1076
|
-
if (completionCommands.length === 0) return;
|
|
1077
|
-
completionIndex = completionIndex <= 0
|
|
1078
|
-
? completionCommands.length - 1
|
|
1079
|
-
: completionIndex - 1;
|
|
1080
|
-
renderCompletionPanel();
|
|
415
|
+
statusLine.bottom = currentInputHeight;
|
|
416
|
+
// Reposition completion panel if active
|
|
417
|
+
if (completionController.isActive()) completionController.reflow();
|
|
418
|
+
// dashboard and inputBottomLine stay fixed at bottom 0 and 1
|
|
419
|
+
logBox.height = Math.max(1, screen.height - currentInputHeight - 1);
|
|
420
|
+
ensureInputCursorVisible();
|
|
1081
421
|
}
|
|
1082
422
|
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
: completionIndex + 1;
|
|
1088
|
-
renderCompletionPanel();
|
|
1089
|
-
}
|
|
423
|
+
// Override the internal listener to support cursor movement
|
|
424
|
+
input._listener = function(ch, key) {
|
|
425
|
+
inputListenerController.handleKey(ch, key, this);
|
|
426
|
+
};
|
|
1090
427
|
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
completionIndex = Math.max(0, completionIndex - step);
|
|
1095
|
-
renderCompletionPanel();
|
|
1096
|
-
}
|
|
428
|
+
// Override cursor update to use our cursor position
|
|
429
|
+
input._updateCursor = function() {
|
|
430
|
+
if (this.screen.focused !== this) return;
|
|
1097
431
|
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
completionIndex = Math.min(completionCommands.length - 1, completionIndex + step);
|
|
1102
|
-
renderCompletionPanel();
|
|
1103
|
-
}
|
|
432
|
+
let lpos;
|
|
433
|
+
try { lpos = this._getCoords(); } catch { return; }
|
|
434
|
+
if (!lpos) return;
|
|
1104
435
|
|
|
1105
|
-
|
|
1106
|
-
|
|
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
|
-
}
|
|
436
|
+
const innerWidth = getWrapWidth();
|
|
437
|
+
if (innerWidth <= 0) return;
|
|
1123
438
|
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
resetPreferredCol();
|
|
1128
|
-
input._updateCursor();
|
|
1129
|
-
updateDraftFromInput();
|
|
1130
|
-
screen.render();
|
|
1131
|
-
}
|
|
439
|
+
ensureInputCursorVisible();
|
|
440
|
+
const { row, col } = getCursorRowCol(this.value, cursorPos, innerWidth);
|
|
441
|
+
const scrollOffset = this.childBase || 0;
|
|
1132
442
|
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
return `${text.slice(0, maxWidth - 3)}...`;
|
|
1138
|
-
}
|
|
443
|
+
const displayRow = row - scrollOffset;
|
|
444
|
+
const safeCol = Math.min(Math.max(0, col), innerWidth - 1);
|
|
445
|
+
const cy = lpos.yi + displayRow;
|
|
446
|
+
const cx = lpos.xi + safeCol;
|
|
1139
447
|
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
const selected = completionCommands[completionIndex];
|
|
1144
|
-
|
|
1145
|
-
if (selected.isSubcommand) {
|
|
1146
|
-
// Subcommand: replace the last word with selected subcommand
|
|
1147
|
-
const parts = input.value.split(/\s+/);
|
|
1148
|
-
parts[parts.length - 1] = selected.cmd;
|
|
1149
|
-
input.value = parts.join(" ") + " ";
|
|
1150
|
-
} else {
|
|
1151
|
-
// Main command
|
|
1152
|
-
input.value = selected.cmd + " ";
|
|
1153
|
-
}
|
|
1154
|
-
|
|
1155
|
-
cursorPos = input.value.length;
|
|
1156
|
-
resetPreferredCol();
|
|
1157
|
-
input._updateCursor();
|
|
1158
|
-
updateDraftFromInput();
|
|
1159
|
-
|
|
1160
|
-
// If selected command has subcommands, trigger subcommand completion immediately
|
|
1161
|
-
if (!selected.isSubcommand && selected.subcommands && selected.subcommands.length > 0) {
|
|
1162
|
-
// Don't hide - directly show subcommand completion
|
|
1163
|
-
showCompletion(input.value);
|
|
1164
|
-
} else {
|
|
1165
|
-
// No subcommands - hide completion
|
|
1166
|
-
hideCompletion();
|
|
1167
|
-
}
|
|
1168
|
-
|
|
1169
|
-
screen.render();
|
|
1170
|
-
}
|
|
1171
|
-
|
|
1172
|
-
function handleCompletionKey(ch, key) {
|
|
1173
|
-
if (!completionActive) return false;
|
|
1174
|
-
|
|
1175
|
-
if (key.name === "up") {
|
|
1176
|
-
completionUp();
|
|
1177
|
-
return true;
|
|
1178
|
-
}
|
|
1179
|
-
if (key.name === "down") {
|
|
1180
|
-
completionDown();
|
|
1181
|
-
return true;
|
|
1182
|
-
}
|
|
1183
|
-
if (key.name === "tab") {
|
|
1184
|
-
confirmCompletion();
|
|
1185
|
-
return true;
|
|
1186
|
-
}
|
|
1187
|
-
if (key.name === "pageup") {
|
|
1188
|
-
completionPageUp();
|
|
1189
|
-
return true;
|
|
1190
|
-
}
|
|
1191
|
-
if (key.name === "pagedown") {
|
|
1192
|
-
completionPageDown();
|
|
1193
|
-
return true;
|
|
1194
|
-
}
|
|
1195
|
-
if (key.name === "enter" || key.name === "return") {
|
|
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
|
|
1218
|
-
hideCompletion();
|
|
1219
|
-
completionEnterSuppressed = true;
|
|
1220
|
-
if (completionEnterReset) clearImmediate(completionEnterReset);
|
|
1221
|
-
completionEnterReset = setImmediate(() => {
|
|
1222
|
-
completionEnterSuppressed = false;
|
|
1223
|
-
});
|
|
1224
|
-
return false;
|
|
1225
|
-
}
|
|
1226
|
-
if (key.name === "escape") {
|
|
1227
|
-
hideCompletion();
|
|
1228
|
-
return true;
|
|
1229
|
-
}
|
|
1230
|
-
if (ch === " ") {
|
|
1231
|
-
// Check if current input is a command that might have subcommands
|
|
1232
|
-
const currentInput = input.value.trim();
|
|
1233
|
-
if (currentInput.startsWith("/") && !currentInput.includes(" ")) {
|
|
1234
|
-
// Let space be inserted, will trigger subcommand completion
|
|
1235
|
-
return false;
|
|
1236
|
-
}
|
|
1237
|
-
hideCompletion();
|
|
1238
|
-
return false;
|
|
1239
|
-
}
|
|
1240
|
-
// Regular character and backspace - don't intercept, let it be handled normally
|
|
1241
|
-
// Completion will be updated in the main input handler
|
|
1242
|
-
return false;
|
|
1243
|
-
}
|
|
1244
|
-
|
|
1245
|
-
// Resize input box based on content
|
|
1246
|
-
function resizeInput() {
|
|
1247
|
-
const innerWidth = getWrapWidth();
|
|
1248
|
-
if (innerWidth <= 0) return;
|
|
1249
|
-
|
|
1250
|
-
const numLines = countLines(input.value, innerWidth);
|
|
1251
|
-
const contentHeight = Math.min(MAX_INPUT_HEIGHT - 3, Math.max(1, numLines));
|
|
1252
|
-
const targetHeight = contentHeight + 3; // +1 topLine +1 bottomLine +1 dashboard
|
|
1253
|
-
|
|
1254
|
-
if (targetHeight !== currentInputHeight) {
|
|
1255
|
-
currentInputHeight = targetHeight;
|
|
1256
|
-
input.height = contentHeight;
|
|
1257
|
-
promptBox.height = contentHeight;
|
|
1258
|
-
inputTopLine.bottom = currentInputHeight - 1; // Just above input area
|
|
1259
|
-
}
|
|
1260
|
-
statusLine.bottom = currentInputHeight;
|
|
1261
|
-
// Reposition completion panel if active
|
|
1262
|
-
if (completionActive) {
|
|
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();
|
|
1270
|
-
}
|
|
1271
|
-
// dashboard and inputBottomLine stay fixed at bottom 0 and 1
|
|
1272
|
-
logBox.height = Math.max(1, screen.height - currentInputHeight - 1);
|
|
1273
|
-
ensureInputCursorVisible();
|
|
1274
|
-
}
|
|
1275
|
-
|
|
1276
|
-
// Override the internal listener to support cursor movement
|
|
1277
|
-
input._listener = function(ch, key) {
|
|
1278
|
-
if (currentView === "agent") return; // Agent view handles keys at screen level
|
|
1279
|
-
if (key && key.ctrl && key.name === "c") {
|
|
1280
|
-
exitHandler();
|
|
1281
|
-
return;
|
|
1282
|
-
}
|
|
1283
|
-
if (suppressKeypress) {
|
|
1284
|
-
return;
|
|
1285
|
-
}
|
|
1286
|
-
normalizeCommandPrefix();
|
|
1287
|
-
if (focusMode === "dashboard") {
|
|
1288
|
-
if (handleDashboardKey(key)) return;
|
|
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
|
-
}
|
|
1297
|
-
}
|
|
1298
|
-
|
|
1299
|
-
// Command completion mode
|
|
1300
|
-
if (completionActive) {
|
|
1301
|
-
if (handleCompletionKey(ch, key)) return;
|
|
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
|
-
}
|
|
1308
|
-
|
|
1309
|
-
// Treat multi-char input (paste) as insertion, including newlines.
|
|
1310
|
-
if (ch && ch.length > 1 && (!key || !key.name || key.name.length !== 1)) {
|
|
1311
|
-
insertTextAtCursor(normalizePaste(ch));
|
|
1312
|
-
return;
|
|
1313
|
-
}
|
|
1314
|
-
if (ch && (ch.includes("\n") || ch.includes("\r")) && (!key || (key.name !== "return" && key.name !== "enter"))) {
|
|
1315
|
-
insertTextAtCursor(normalizePaste(ch));
|
|
1316
|
-
return;
|
|
1317
|
-
}
|
|
1318
|
-
// Plain enter submits, shift+enter inserts newline
|
|
1319
|
-
if (key.name === "return" || key.name === "enter") {
|
|
1320
|
-
if (key.shift) {
|
|
1321
|
-
// Insert newline at cursor
|
|
1322
|
-
insertTextAtCursor("\n");
|
|
1323
|
-
} else {
|
|
1324
|
-
// Submit
|
|
1325
|
-
resetPreferredCol();
|
|
1326
|
-
this._done(null, this.value);
|
|
1327
|
-
}
|
|
1328
|
-
return;
|
|
1329
|
-
}
|
|
1330
|
-
|
|
1331
|
-
if (key.name === "left") {
|
|
1332
|
-
if (cursorPos > 0) cursorPos--;
|
|
1333
|
-
resetPreferredCol();
|
|
1334
|
-
ensureInputCursorVisible();
|
|
1335
|
-
this._updateCursor();
|
|
1336
|
-
this.screen.render();
|
|
1337
|
-
return;
|
|
1338
|
-
}
|
|
1339
|
-
|
|
1340
|
-
if (key.name === "right") {
|
|
1341
|
-
if (cursorPos < this.value.length) cursorPos++;
|
|
1342
|
-
resetPreferredCol();
|
|
1343
|
-
ensureInputCursorVisible();
|
|
1344
|
-
this._updateCursor();
|
|
1345
|
-
this.screen.render();
|
|
1346
|
-
return;
|
|
1347
|
-
}
|
|
1348
|
-
|
|
1349
|
-
if (key.name === "home") {
|
|
1350
|
-
cursorPos = 0;
|
|
1351
|
-
resetPreferredCol();
|
|
1352
|
-
ensureInputCursorVisible();
|
|
1353
|
-
this._updateCursor();
|
|
1354
|
-
this.screen.render();
|
|
1355
|
-
return;
|
|
1356
|
-
}
|
|
1357
|
-
|
|
1358
|
-
if (key.name === "end") {
|
|
1359
|
-
cursorPos = this.value.length;
|
|
1360
|
-
resetPreferredCol();
|
|
1361
|
-
ensureInputCursorVisible();
|
|
1362
|
-
this._updateCursor();
|
|
1363
|
-
this.screen.render();
|
|
1364
|
-
return;
|
|
1365
|
-
}
|
|
1366
|
-
|
|
1367
|
-
if (key.name === "up") {
|
|
1368
|
-
// Special case: "/" + Up → jump to last command in completion
|
|
1369
|
-
if (completionActive && input.value === "/" && cursorPos === 1) {
|
|
1370
|
-
completionIndex = completionCommands.length - 1;
|
|
1371
|
-
renderCompletionPanel();
|
|
1372
|
-
return;
|
|
1373
|
-
}
|
|
1374
|
-
if (historyUp()) {
|
|
1375
|
-
hideCompletion();
|
|
1376
|
-
return;
|
|
1377
|
-
}
|
|
1378
|
-
}
|
|
1379
|
-
if (key.name === "down") {
|
|
1380
|
-
if (historyDown()) {
|
|
1381
|
-
hideCompletion();
|
|
1382
|
-
return;
|
|
1383
|
-
}
|
|
1384
|
-
}
|
|
1385
|
-
if (key.name === "up" || key.name === "down") {
|
|
1386
|
-
const innerWidth = getWrapWidth();
|
|
1387
|
-
if (innerWidth > 0) {
|
|
1388
|
-
const { row, col } = getCursorRowCol(this.value, cursorPos, innerWidth);
|
|
1389
|
-
if (preferredCol === null) preferredCol = col;
|
|
1390
|
-
const totalRows = countLines(this.value, innerWidth);
|
|
1391
|
-
|
|
1392
|
-
// Down at last row -> enter dashboard mode
|
|
1393
|
-
if (key.name === "down" && row >= totalRows - 1) {
|
|
1394
|
-
enterDashboardMode();
|
|
1395
|
-
return;
|
|
1396
|
-
}
|
|
1397
|
-
|
|
1398
|
-
const targetRow = key.name === "up"
|
|
1399
|
-
? Math.max(0, row - 1)
|
|
1400
|
-
: Math.min(totalRows - 1, row + 1);
|
|
1401
|
-
cursorPos = getCursorPosForRowCol(this.value, targetRow, preferredCol, innerWidth);
|
|
1402
|
-
}
|
|
1403
|
-
ensureInputCursorVisible();
|
|
1404
|
-
this._updateCursor();
|
|
1405
|
-
this.screen.render();
|
|
1406
|
-
return;
|
|
1407
|
-
}
|
|
1408
|
-
|
|
1409
|
-
if (key.name === "escape") {
|
|
1410
|
-
this._done(null, null);
|
|
1411
|
-
return;
|
|
1412
|
-
}
|
|
1413
|
-
|
|
1414
|
-
if (key.name === "backspace") {
|
|
1415
|
-
if (cursorPos > 0) {
|
|
1416
|
-
this.value = this.value.slice(0, cursorPos - 1) + this.value.slice(cursorPos);
|
|
1417
|
-
cursorPos--;
|
|
1418
|
-
resetPreferredCol();
|
|
1419
|
-
resizeInput();
|
|
1420
|
-
ensureInputCursorVisible();
|
|
1421
|
-
this._updateCursor();
|
|
1422
|
-
updateDraftFromInput();
|
|
1423
|
-
|
|
1424
|
-
// Update or hide completion after backspace
|
|
1425
|
-
if (this.value.startsWith("/")) {
|
|
1426
|
-
showCompletion(this.value);
|
|
1427
|
-
} else {
|
|
1428
|
-
hideCompletion();
|
|
1429
|
-
}
|
|
1430
|
-
|
|
1431
|
-
this.screen.render();
|
|
1432
|
-
}
|
|
1433
|
-
return;
|
|
1434
|
-
}
|
|
1435
|
-
|
|
1436
|
-
if (key.name === "delete") {
|
|
1437
|
-
if (cursorPos < this.value.length) {
|
|
1438
|
-
this.value = this.value.slice(0, cursorPos) + this.value.slice(cursorPos + 1);
|
|
1439
|
-
resetPreferredCol();
|
|
1440
|
-
resizeInput();
|
|
1441
|
-
ensureInputCursorVisible();
|
|
1442
|
-
this._updateCursor();
|
|
1443
|
-
this.screen.render();
|
|
1444
|
-
updateDraftFromInput();
|
|
1445
|
-
}
|
|
1446
|
-
return;
|
|
1447
|
-
}
|
|
1448
|
-
|
|
1449
|
-
// Insert character at cursor position
|
|
1450
|
-
const insertChar = (ch && ch.length === 1)
|
|
1451
|
-
? ch
|
|
1452
|
-
: (key && key.name && key.name.length === 1 ? key.name : null);
|
|
1453
|
-
if (insertChar && !/^[\x00-\x08\x0b-\x0c\x0e-\x1f\x7f]$/.test(insertChar)) {
|
|
1454
|
-
this.value = this.value.slice(0, cursorPos) + insertChar + this.value.slice(cursorPos);
|
|
1455
|
-
cursorPos++;
|
|
1456
|
-
normalizeCommandPrefix();
|
|
1457
|
-
resetPreferredCol();
|
|
1458
|
-
resizeInput();
|
|
1459
|
-
this._updateCursor();
|
|
1460
|
-
updateDraftFromInput();
|
|
1461
|
-
|
|
1462
|
-
// Update completion filter if typing after "/"
|
|
1463
|
-
if (this.value.startsWith("/")) {
|
|
1464
|
-
showCompletion(this.value);
|
|
1465
|
-
} else if (completionActive) {
|
|
1466
|
-
hideCompletion();
|
|
1467
|
-
}
|
|
1468
|
-
|
|
1469
|
-
this.screen.render();
|
|
1470
|
-
return;
|
|
1471
|
-
}
|
|
1472
|
-
};
|
|
1473
|
-
|
|
1474
|
-
// Override cursor update to use our cursor position
|
|
1475
|
-
input._updateCursor = function() {
|
|
1476
|
-
if (this.screen.focused !== this) return;
|
|
1477
|
-
|
|
1478
|
-
let lpos;
|
|
1479
|
-
try { lpos = this._getCoords(); } catch { return; }
|
|
1480
|
-
if (!lpos) return;
|
|
1481
|
-
|
|
1482
|
-
const innerWidth = getWrapWidth();
|
|
1483
|
-
if (innerWidth <= 0) return;
|
|
1484
|
-
|
|
1485
|
-
ensureInputCursorVisible();
|
|
1486
|
-
const { row, col } = getCursorRowCol(this.value, cursorPos, innerWidth);
|
|
1487
|
-
const scrollOffset = this.childBase || 0;
|
|
1488
|
-
|
|
1489
|
-
const displayRow = row - scrollOffset;
|
|
1490
|
-
const safeCol = Math.min(Math.max(0, col), innerWidth - 1);
|
|
1491
|
-
const cy = lpos.yi + displayRow;
|
|
1492
|
-
const cx = lpos.xi + safeCol;
|
|
1493
|
-
|
|
1494
|
-
this.screen.program.cup(cy, cx);
|
|
1495
|
-
this.screen.program.showCursor();
|
|
1496
|
-
};
|
|
448
|
+
this.screen.program.cup(cy, cx);
|
|
449
|
+
this.screen.program.showCursor();
|
|
450
|
+
};
|
|
1497
451
|
|
|
1498
452
|
// Reset cursor and height on clear
|
|
1499
453
|
const originalClearValue = input.clearValue.bind(input);
|
|
@@ -1501,9 +455,8 @@ async function runChat(projectRoot) {
|
|
|
1501
455
|
cursorPos = 0;
|
|
1502
456
|
resetPreferredCol();
|
|
1503
457
|
currentInputHeight = MIN_INPUT_HEIGHT;
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
hideCompletion();
|
|
458
|
+
if (inputHistoryController) inputHistoryController.setIndexToEnd();
|
|
459
|
+
completionController.hide();
|
|
1507
460
|
const contentHeight = 1; // MIN content height
|
|
1508
461
|
input.height = contentHeight;
|
|
1509
462
|
promptBox.height = contentHeight;
|
|
@@ -1515,1725 +468,841 @@ async function runChat(projectRoot) {
|
|
|
1515
468
|
|
|
1516
469
|
let pending = null;
|
|
1517
470
|
|
|
1518
|
-
// Command completion state
|
|
1519
|
-
let completionActive = false;
|
|
1520
|
-
let completionCommands = [];
|
|
1521
|
-
let completionIndex = 0;
|
|
1522
|
-
let completionScrollOffset = 0;
|
|
1523
|
-
let completionVisibleCount = 0;
|
|
1524
|
-
let completionEnterSuppressed = false;
|
|
1525
|
-
let completionEnterReset = null;
|
|
1526
|
-
|
|
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": {
|
|
1547
|
-
desc: "Daemon management",
|
|
1548
|
-
children: {
|
|
1549
|
-
restart: { desc: "Restart daemon" },
|
|
1550
|
-
start: { desc: "Start daemon" },
|
|
1551
|
-
status: { desc: "Daemon status" },
|
|
1552
|
-
stop: { desc: "Stop daemon" },
|
|
1553
|
-
},
|
|
1554
|
-
},
|
|
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
|
-
},
|
|
1563
|
-
},
|
|
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);
|
|
1594
|
-
|
|
1595
471
|
// Agent selection state
|
|
1596
472
|
let activeAgents = [];
|
|
1597
473
|
let activeAgentLabelMap = new Map();
|
|
1598
474
|
let activeAgentMetaMap = new Map(); // Store full meta including launch_mode
|
|
1599
475
|
let agentListWindowStart = 0;
|
|
1600
|
-
const MAX_AGENT_WINDOW =
|
|
476
|
+
const MAX_AGENT_WINDOW = 4;
|
|
1601
477
|
let selectedAgentIndex = -1; // -1 = not in dashboard selection mode
|
|
1602
478
|
let targetAgent = null; // Selected agent for direct messaging
|
|
1603
479
|
let focusMode = "input"; // "input" or "dashboard"
|
|
1604
|
-
let dashboardView = "agents"; // "agents" | "mode" | "provider" | "
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
let selectedModeIndex = modeToIndex(launchMode);
|
|
480
|
+
let dashboardView = "agents"; // "agents" | "mode" | "provider" | "assistant" | "cron"
|
|
481
|
+
let reportPendingTotal = 0;
|
|
482
|
+
let selectedModeIndex = launchMode === "internal" ? 2 : (launchMode === "tmux" ? 1 : 0);
|
|
1608
483
|
const providerOptions = [
|
|
1609
484
|
{ label: "codex", value: "codex-cli" },
|
|
1610
485
|
{ label: "claude", value: "claude-cli" },
|
|
486
|
+
{ label: "ucode", value: "ucode" },
|
|
487
|
+
];
|
|
488
|
+
let selectedProviderIndex = Math.max(0, providerOptions.findIndex((opt) => opt.value === agentProvider));
|
|
489
|
+
const assistantOptions = [
|
|
490
|
+
{ label: "auto", value: "auto" },
|
|
491
|
+
{ label: "codex", value: "codex" },
|
|
492
|
+
{ label: "claude", value: "claude" },
|
|
493
|
+
{ label: "ucode", value: "ufoo" },
|
|
1611
494
|
];
|
|
1612
|
-
let
|
|
495
|
+
let selectedAssistantIndex = Math.max(
|
|
496
|
+
0,
|
|
497
|
+
assistantOptions.findIndex((opt) => opt.value === assistantEngine)
|
|
498
|
+
);
|
|
1613
499
|
const resumeOptions = [
|
|
1614
|
-
{ label: "
|
|
1615
|
-
{ label: "
|
|
500
|
+
{ label: "Resume previous session", value: true },
|
|
501
|
+
{ label: "Start new session", value: false },
|
|
1616
502
|
];
|
|
1617
503
|
let selectedResumeIndex = autoResume ? 0 : 1;
|
|
1618
|
-
|
|
504
|
+
const DASH_HINTS = {
|
|
505
|
+
agents: "←/→ select · Enter · ↓ mode · ↑ back",
|
|
506
|
+
agentsEmpty: "↓ mode · ↑ back",
|
|
507
|
+
mode: "←/→ select · Enter · ↓ provider · ↑ back",
|
|
508
|
+
provider: "←/→ select · Enter · ↓ assistant · ↑ back",
|
|
509
|
+
assistant: "←/→ select · Enter · ↓ cron · ↑ back",
|
|
510
|
+
cron: "Ctrl+X close · ↑ back",
|
|
511
|
+
resume: "",
|
|
512
|
+
};
|
|
513
|
+
const AGENT_BAR_HINTS = {
|
|
514
|
+
normal: "↓ agents",
|
|
515
|
+
dashboard: "←/→ · Enter · ↑ · ^X",
|
|
516
|
+
};
|
|
1619
517
|
|
|
1620
|
-
function
|
|
1621
|
-
return
|
|
518
|
+
function getCurrentView() {
|
|
519
|
+
return agentViewController ? agentViewController.getCurrentView() : "main";
|
|
1622
520
|
}
|
|
1623
521
|
|
|
1624
|
-
function
|
|
1625
|
-
|
|
1626
|
-
agentListWindowStart = 0;
|
|
1627
|
-
return;
|
|
1628
|
-
}
|
|
1629
|
-
const maxItems = Math.max(1, Math.min(MAX_AGENT_WINDOW, activeAgents.length));
|
|
1630
|
-
if (selectedAgentIndex >= 0) {
|
|
1631
|
-
if (selectedAgentIndex < agentListWindowStart) {
|
|
1632
|
-
agentListWindowStart = selectedAgentIndex;
|
|
1633
|
-
} else if (selectedAgentIndex >= agentListWindowStart + maxItems) {
|
|
1634
|
-
agentListWindowStart = selectedAgentIndex - maxItems + 1;
|
|
1635
|
-
}
|
|
1636
|
-
}
|
|
1637
|
-
const maxStart = Math.max(0, activeAgents.length - maxItems);
|
|
1638
|
-
if (agentListWindowStart > maxStart) agentListWindowStart = maxStart;
|
|
1639
|
-
if (agentListWindowStart < 0) agentListWindowStart = 0;
|
|
522
|
+
function getViewingAgent() {
|
|
523
|
+
return agentViewController ? agentViewController.getViewingAgent() : "";
|
|
1640
524
|
}
|
|
1641
525
|
|
|
1642
|
-
function
|
|
1643
|
-
if (!
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
}
|
|
1648
|
-
client.write(`${JSON.stringify(req)}\n`);
|
|
526
|
+
function getAgentAdapter(agentId) {
|
|
527
|
+
if (!terminalAdapterRouter) return null;
|
|
528
|
+
const meta = activeAgentMetaMap ? activeAgentMetaMap.get(agentId) : null;
|
|
529
|
+
const agentLaunchMode = (meta && meta.launch_mode) || launchMode || "";
|
|
530
|
+
return terminalAdapterRouter.getAdapter({ launchMode: agentLaunchMode, agentId });
|
|
1649
531
|
}
|
|
1650
532
|
|
|
1651
|
-
function
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
promptBox.width = label.length + 3; // @name>
|
|
1656
|
-
input.left = promptBox.width;
|
|
1657
|
-
input.width = `100%-${promptBox.width}`;
|
|
1658
|
-
} else {
|
|
1659
|
-
promptBox.setContent(">");
|
|
1660
|
-
promptBox.width = 2;
|
|
1661
|
-
input.left = 2;
|
|
1662
|
-
input.width = "100%-2";
|
|
1663
|
-
}
|
|
1664
|
-
resizeInput();
|
|
1665
|
-
input._updateCursor();
|
|
533
|
+
function getViewingAgentAdapter() {
|
|
534
|
+
const viewingAgent = getViewingAgent();
|
|
535
|
+
if (!viewingAgent) return null;
|
|
536
|
+
return getAgentAdapter(viewingAgent);
|
|
1666
537
|
}
|
|
1667
538
|
|
|
1668
|
-
function
|
|
1669
|
-
|
|
1670
|
-
|
|
539
|
+
function canSendRaw(adapter) {
|
|
540
|
+
if (!adapter || !adapter.capabilities) return false;
|
|
541
|
+
return Boolean(
|
|
542
|
+
adapter.capabilities.supportsSocketProtocol
|
|
543
|
+
|| adapter.capabilities.supportsInternalQueueLoop
|
|
544
|
+
);
|
|
1671
545
|
}
|
|
1672
546
|
|
|
1673
|
-
function
|
|
1674
|
-
|
|
1675
|
-
screen.program.hideCursor();
|
|
547
|
+
function canResize(adapter) {
|
|
548
|
+
return Boolean(adapter && adapter.capabilities && adapter.capabilities.supportsSocketProtocol);
|
|
1676
549
|
}
|
|
1677
550
|
|
|
1678
|
-
function
|
|
1679
|
-
|
|
1680
|
-
|
|
551
|
+
function canSnapshot(adapter) {
|
|
552
|
+
if (!adapter || !adapter.capabilities) return false;
|
|
553
|
+
return Boolean(
|
|
554
|
+
adapter.capabilities.supportsSnapshot
|
|
555
|
+
|| adapter.capabilities.supportsSubscribeScreen
|
|
556
|
+
|| adapter.capabilities.supportsSubscribeFull
|
|
557
|
+
);
|
|
1681
558
|
}
|
|
1682
559
|
|
|
1683
|
-
function
|
|
1684
|
-
const
|
|
1685
|
-
if (
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
560
|
+
function sendRawWithCapabilities(data) {
|
|
561
|
+
const adapter = getViewingAgentAdapter();
|
|
562
|
+
if (!canSendRaw(adapter)) return;
|
|
563
|
+
try {
|
|
564
|
+
adapter.sendRaw(data);
|
|
565
|
+
} catch {
|
|
566
|
+
// ignore unsupported errors
|
|
1690
567
|
}
|
|
1691
|
-
launchMode = next;
|
|
1692
|
-
selectedModeIndex = modeToIndex(launchMode);
|
|
1693
|
-
saveConfig(projectRoot, { launchMode });
|
|
1694
|
-
logMessage("status", `{magenta-fg}⚙{/magenta-fg} Launch mode: ${launchMode}`);
|
|
1695
|
-
renderDashboard();
|
|
1696
|
-
screen.render();
|
|
1697
|
-
void restartDaemon();
|
|
1698
|
-
}
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
function providerLabel(value) {
|
|
1702
|
-
return value === "claude-cli" ? "claude" : "codex";
|
|
1703
568
|
}
|
|
1704
569
|
|
|
1705
|
-
function
|
|
1706
|
-
const
|
|
1707
|
-
|
|
1708
|
-
const historyFile = path.join(agentDir, "ufoo-agent.history.jsonl");
|
|
570
|
+
function sendResizeWithCapabilities(cols, rows) {
|
|
571
|
+
const adapter = getViewingAgentAdapter();
|
|
572
|
+
if (!canResize(adapter)) return;
|
|
1709
573
|
try {
|
|
1710
|
-
|
|
574
|
+
adapter.resize(cols, rows);
|
|
1711
575
|
} catch {
|
|
1712
|
-
// ignore
|
|
576
|
+
// ignore unsupported errors
|
|
1713
577
|
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function requestSnapshotWithCapabilities() {
|
|
581
|
+
const adapter = getViewingAgentAdapter();
|
|
582
|
+
if (!canSnapshot(adapter)) return false;
|
|
1714
583
|
try {
|
|
1715
|
-
|
|
584
|
+
return adapter.snapshot();
|
|
1716
585
|
} catch {
|
|
1717
|
-
|
|
586
|
+
return false;
|
|
1718
587
|
}
|
|
1719
588
|
}
|
|
1720
589
|
|
|
1721
|
-
function
|
|
1722
|
-
|
|
1723
|
-
if (next === agentProvider) return;
|
|
1724
|
-
agentProvider = next;
|
|
1725
|
-
selectedProviderIndex = agentProvider === "claude-cli" ? 1 : 0;
|
|
1726
|
-
saveConfig(projectRoot, { agentProvider });
|
|
1727
|
-
clearUfooAgentIdentity();
|
|
1728
|
-
logMessage("status", `{magenta-fg}⚙{/magenta-fg} ufoo-agent: ${providerLabel(agentProvider)}`);
|
|
1729
|
-
renderDashboard();
|
|
1730
|
-
screen.render();
|
|
1731
|
-
void restartDaemon();
|
|
590
|
+
function isAgentViewUsesBus() {
|
|
591
|
+
return agentViewController ? agentViewController.isAgentViewUsesBus() : false;
|
|
1732
592
|
}
|
|
1733
593
|
|
|
1734
|
-
function
|
|
1735
|
-
|
|
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();
|
|
594
|
+
function getAgentInputSuppressUntil() {
|
|
595
|
+
return agentViewController ? agentViewController.getAgentInputSuppressUntil() : 0;
|
|
1744
596
|
}
|
|
1745
597
|
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
try {
|
|
1754
|
-
client.end();
|
|
1755
|
-
} catch {
|
|
1756
|
-
// ignore
|
|
1757
|
-
}
|
|
1758
|
-
}
|
|
1759
|
-
stopDaemon(projectRoot);
|
|
1760
|
-
startDaemon(projectRoot, { forceResume: true });
|
|
1761
|
-
const newClient = await connectClient();
|
|
1762
|
-
if (newClient) {
|
|
1763
|
-
attachClient(newClient);
|
|
1764
|
-
logMessage("status", "{green-fg}✓{/green-fg} Daemon reconnected");
|
|
1765
|
-
} else {
|
|
1766
|
-
logMessage("error", "{red-fg}✗{/red-fg} Failed to reconnect to daemon");
|
|
1767
|
-
}
|
|
1768
|
-
} finally {
|
|
1769
|
-
restartInProgress = false;
|
|
598
|
+
function getAgentOutputSuppressed() {
|
|
599
|
+
return agentViewController ? agentViewController.getAgentOutputSuppressed() : false;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function setAgentOutputSuppressed(value) {
|
|
603
|
+
if (agentViewController) {
|
|
604
|
+
agentViewController.setAgentOutputSuppressed(value);
|
|
1770
605
|
}
|
|
1771
606
|
}
|
|
1772
607
|
|
|
1773
|
-
function
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
logBox.scrollTo(0);
|
|
608
|
+
function renderAgentDashboard() {
|
|
609
|
+
if (agentViewController) {
|
|
610
|
+
agentViewController.renderAgentDashboard();
|
|
1777
611
|
}
|
|
1778
|
-
screen.render();
|
|
1779
612
|
}
|
|
1780
613
|
|
|
1781
|
-
function
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
if (dashboardView === "mode") {
|
|
1785
|
-
const modeParts = launchModes.map((mode, i) => {
|
|
1786
|
-
if (i === selectedModeIndex) {
|
|
1787
|
-
return `{inverse}${mode}{/inverse}`;
|
|
1788
|
-
}
|
|
1789
|
-
if (mode === launchMode) {
|
|
1790
|
-
return `{bold}{cyan-fg}${mode}{/cyan-fg}{/bold}`;
|
|
1791
|
-
}
|
|
1792
|
-
return `{cyan-fg}${mode}{/cyan-fg}`;
|
|
1793
|
-
});
|
|
1794
|
-
content += `{gray-fg}Mode:{/gray-fg} ${modeParts.join(" ")}`;
|
|
1795
|
-
content += " {gray-fg}│ ←/→ select, Enter confirm, ↓ agent, ↑ back{/gray-fg}";
|
|
1796
|
-
} else if (dashboardView === "provider") {
|
|
1797
|
-
const providerParts = providerOptions.map((opt, i) => {
|
|
1798
|
-
if (i === selectedProviderIndex) {
|
|
1799
|
-
return `{inverse}${opt.label}{/inverse}`;
|
|
1800
|
-
}
|
|
1801
|
-
if (opt.value === agentProvider) {
|
|
1802
|
-
return `{bold}{cyan-fg}${opt.label}{/cyan-fg}{/bold}`;
|
|
1803
|
-
}
|
|
1804
|
-
return `{cyan-fg}${opt.label}{/cyan-fg}`;
|
|
1805
|
-
});
|
|
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(" ")}`;
|
|
1819
|
-
content += " {gray-fg}│ ←/→ select, Enter confirm, ↑ back{/gray-fg}";
|
|
1820
|
-
} else {
|
|
1821
|
-
if (activeAgents.length > 0) {
|
|
1822
|
-
clampAgentWindow();
|
|
1823
|
-
const maxItems = Math.max(1, Math.min(MAX_AGENT_WINDOW, activeAgents.length));
|
|
1824
|
-
const start = agentListWindowStart;
|
|
1825
|
-
const end = start + maxItems;
|
|
1826
|
-
const visibleAgents = activeAgents.slice(start, end);
|
|
1827
|
-
const agentParts = visibleAgents.map((agent, i) => {
|
|
1828
|
-
const absoluteIndex = start + i;
|
|
1829
|
-
const label = getAgentLabel(agent);
|
|
1830
|
-
if (absoluteIndex === selectedAgentIndex) {
|
|
1831
|
-
return `{inverse}${label}{/inverse}`;
|
|
1832
|
-
}
|
|
1833
|
-
return `{cyan-fg}${label}{/cyan-fg}`;
|
|
1834
|
-
});
|
|
1835
|
-
const leftMore = start > 0 ? "{gray-fg}«{/gray-fg} " : "";
|
|
1836
|
-
const rightMore = end < activeAgents.length ? " {gray-fg}»{/gray-fg}" : "";
|
|
1837
|
-
content += `{gray-fg}Agents:{/gray-fg} ${agentParts.join(" ")}`;
|
|
1838
|
-
content = `${content.replace("{gray-fg}Agents:{/gray-fg} ", `{gray-fg}Agents:{/gray-fg} ${leftMore}`)}${rightMore}`;
|
|
1839
|
-
content += " {gray-fg}│ ←/→ select, Enter confirm, ^X close, ↓ mode, ↑ back{/gray-fg}";
|
|
1840
|
-
} else {
|
|
1841
|
-
content += "{gray-fg}Agents:{/gray-fg} {cyan-fg}none{/cyan-fg}";
|
|
1842
|
-
content += " {gray-fg}│ ↓ mode, ↑ back{/gray-fg}";
|
|
1843
|
-
}
|
|
1844
|
-
}
|
|
1845
|
-
} else {
|
|
1846
|
-
// Normal dashboard display (input mode)
|
|
1847
|
-
const agents = activeAgents.length > 0
|
|
1848
|
-
? activeAgents.slice(0, 3).map((id) => {
|
|
1849
|
-
const label = getAgentLabel(id);
|
|
1850
|
-
return label;
|
|
1851
|
-
}).join(", ") + (activeAgents.length > 3 ? ` +${activeAgents.length - 3}` : "")
|
|
1852
|
-
: "none";
|
|
1853
|
-
content += `{gray-fg}Agents:{/gray-fg} {cyan-fg}${agents}{/cyan-fg}`;
|
|
1854
|
-
content += ` {gray-fg}Mode:{/gray-fg} {cyan-fg}${launchMode}{/cyan-fg}`;
|
|
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}`;
|
|
614
|
+
function setAgentBarVisible(visible) {
|
|
615
|
+
if (agentViewController) {
|
|
616
|
+
agentViewController.setAgentBarVisible(visible);
|
|
1857
617
|
}
|
|
1858
|
-
dashboard.setContent(content);
|
|
1859
618
|
}
|
|
1860
619
|
|
|
1861
|
-
function
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
activeAgentLabelMap = new Map();
|
|
1865
|
-
activeAgentMetaMap = new Map();
|
|
1866
|
-
let fallbackMap = null;
|
|
1867
|
-
if (metaList.length === 0 && activeAgents.length > 0) {
|
|
1868
|
-
try {
|
|
1869
|
-
const busPath = getUfooPaths(projectRoot).agentsFile;
|
|
1870
|
-
const bus = JSON.parse(fs.readFileSync(busPath, "utf8"));
|
|
1871
|
-
fallbackMap = new Map();
|
|
1872
|
-
for (const [id, meta] of Object.entries(bus.agents || {})) {
|
|
1873
|
-
if (meta && meta.nickname) fallbackMap.set(id, meta.nickname);
|
|
1874
|
-
}
|
|
1875
|
-
} catch {
|
|
1876
|
-
fallbackMap = null;
|
|
1877
|
-
}
|
|
620
|
+
function enterAgentView(agentId, options = {}) {
|
|
621
|
+
if (agentViewController) {
|
|
622
|
+
agentViewController.enterAgentView(agentId, options);
|
|
1878
623
|
}
|
|
1879
|
-
for (const id of activeAgents) {
|
|
1880
|
-
const meta = metaList.find((item) => item && item.id === id);
|
|
1881
|
-
const label = meta && meta.nickname
|
|
1882
|
-
? meta.nickname
|
|
1883
|
-
: (fallbackMap && fallbackMap.get(id)) || id;
|
|
1884
|
-
activeAgentLabelMap.set(id, label);
|
|
1885
|
-
if (meta) {
|
|
1886
|
-
activeAgentMetaMap.set(id, meta);
|
|
1887
|
-
}
|
|
1888
|
-
}
|
|
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
|
-
|
|
1910
|
-
if (focusMode === "dashboard") {
|
|
1911
|
-
if (dashboardView === "agents") {
|
|
1912
|
-
if (activeAgents.length === 0) {
|
|
1913
|
-
selectedAgentIndex = -1;
|
|
1914
|
-
} else if (selectedAgentIndex < 0 || selectedAgentIndex >= activeAgents.length) {
|
|
1915
|
-
selectedAgentIndex = 0;
|
|
1916
|
-
}
|
|
1917
|
-
clampAgentWindow();
|
|
1918
|
-
}
|
|
1919
|
-
}
|
|
1920
|
-
renderDashboard();
|
|
1921
|
-
screen.render();
|
|
1922
624
|
}
|
|
1923
625
|
|
|
1924
|
-
function
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
selectedAgentIndex = activeAgents.length > 0 ? 0 : -1;
|
|
1928
|
-
agentListWindowStart = 0;
|
|
1929
|
-
clampAgentWindow();
|
|
1930
|
-
selectedModeIndex = modeToIndex(launchMode);
|
|
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();
|
|
626
|
+
function exitAgentView() {
|
|
627
|
+
if (agentViewController) {
|
|
628
|
+
agentViewController.exitAgentView();
|
|
1937
629
|
}
|
|
1938
|
-
screen.grabKeys = true;
|
|
1939
|
-
renderDashboard();
|
|
1940
|
-
screen.program.hideCursor();
|
|
1941
|
-
screen.render();
|
|
1942
630
|
}
|
|
1943
631
|
|
|
1944
|
-
function
|
|
1945
|
-
if (
|
|
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
|
-
|
|
2018
|
-
if (dashboardView === "mode") {
|
|
2019
|
-
const maxMode = launchModes.length - 1;
|
|
2020
|
-
if (key.name === "left") {
|
|
2021
|
-
selectedModeIndex = selectedModeIndex <= 0 ? maxMode : selectedModeIndex - 1;
|
|
2022
|
-
renderDashboard();
|
|
2023
|
-
screen.render();
|
|
2024
|
-
return true;
|
|
2025
|
-
}
|
|
2026
|
-
if (key.name === "right") {
|
|
2027
|
-
selectedModeIndex = selectedModeIndex >= maxMode ? 0 : selectedModeIndex + 1;
|
|
2028
|
-
renderDashboard();
|
|
2029
|
-
screen.render();
|
|
2030
|
-
return true;
|
|
2031
|
-
}
|
|
2032
|
-
if (key.name === "down") {
|
|
2033
|
-
dashboardView = "provider";
|
|
2034
|
-
selectedProviderIndex = agentProvider === "claude-cli" ? 1 : 0;
|
|
2035
|
-
renderDashboard();
|
|
2036
|
-
screen.render();
|
|
2037
|
-
return true;
|
|
2038
|
-
}
|
|
2039
|
-
if (key.name === "up") {
|
|
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
|
-
}
|
|
2046
|
-
renderDashboard();
|
|
2047
|
-
screen.render();
|
|
2048
|
-
return true;
|
|
2049
|
-
}
|
|
2050
|
-
if (key.name === "enter" || key.name === "return") {
|
|
2051
|
-
setLaunchMode(launchModes[selectedModeIndex]);
|
|
2052
|
-
exitDashboardMode(false);
|
|
2053
|
-
return true;
|
|
2054
|
-
}
|
|
2055
|
-
if (key.name === "escape") {
|
|
2056
|
-
exitDashboardMode(false);
|
|
2057
|
-
return true;
|
|
2058
|
-
}
|
|
2059
|
-
return true;
|
|
2060
|
-
}
|
|
2061
|
-
if (dashboardView === "provider") {
|
|
2062
|
-
if (key.name === "left") {
|
|
2063
|
-
selectedProviderIndex = selectedProviderIndex <= 0 ? providerOptions.length - 1 : selectedProviderIndex - 1;
|
|
2064
|
-
renderDashboard();
|
|
2065
|
-
screen.render();
|
|
2066
|
-
return true;
|
|
2067
|
-
}
|
|
2068
|
-
if (key.name === "right") {
|
|
2069
|
-
selectedProviderIndex = selectedProviderIndex >= providerOptions.length - 1 ? 0 : selectedProviderIndex + 1;
|
|
2070
|
-
renderDashboard();
|
|
2071
|
-
screen.render();
|
|
2072
|
-
return true;
|
|
2073
|
-
}
|
|
2074
|
-
if (key.name === "down") {
|
|
2075
|
-
dashboardView = "resume";
|
|
2076
|
-
selectedResumeIndex = autoResume ? 0 : 1;
|
|
2077
|
-
renderDashboard();
|
|
2078
|
-
screen.render();
|
|
2079
|
-
return true;
|
|
2080
|
-
}
|
|
2081
|
-
if (key.name === "up") {
|
|
2082
|
-
dashboardView = "mode";
|
|
2083
|
-
renderDashboard();
|
|
2084
|
-
screen.render();
|
|
2085
|
-
return true;
|
|
2086
|
-
}
|
|
2087
|
-
if (key.name === "enter" || key.name === "return") {
|
|
2088
|
-
const selected = providerOptions[selectedProviderIndex];
|
|
2089
|
-
if (selected) setAgentProvider(selected.value);
|
|
2090
|
-
exitDashboardMode(false);
|
|
2091
|
-
return true;
|
|
2092
|
-
}
|
|
2093
|
-
if (key.name === "escape") {
|
|
2094
|
-
exitDashboardMode(false);
|
|
2095
|
-
return true;
|
|
2096
|
-
}
|
|
2097
|
-
return true;
|
|
2098
|
-
}
|
|
2099
|
-
if (dashboardView === "resume") {
|
|
2100
|
-
if (key.name === "left") {
|
|
2101
|
-
selectedResumeIndex = selectedResumeIndex <= 0 ? resumeOptions.length - 1 : selectedResumeIndex - 1;
|
|
2102
|
-
renderDashboard();
|
|
2103
|
-
screen.render();
|
|
2104
|
-
return true;
|
|
2105
|
-
}
|
|
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") {
|
|
2148
|
-
if (activeAgents.length > 0 && selectedAgentIndex < activeAgents.length - 1) {
|
|
2149
|
-
selectedAgentIndex++;
|
|
2150
|
-
clampAgentWindow();
|
|
2151
|
-
// Update @target in real-time as user navigates
|
|
2152
|
-
targetAgent = activeAgents[selectedAgentIndex];
|
|
2153
|
-
updatePromptBox();
|
|
2154
|
-
renderDashboard();
|
|
2155
|
-
screen.render();
|
|
2156
|
-
}
|
|
2157
|
-
return true;
|
|
2158
|
-
}
|
|
2159
|
-
if (key.name === "down") {
|
|
2160
|
-
// Leaving agents page: clear temporary @target
|
|
2161
|
-
clearTargetAgent();
|
|
2162
|
-
dashboardView = "mode";
|
|
2163
|
-
selectedModeIndex = modeToIndex(launchMode);
|
|
2164
|
-
renderDashboard();
|
|
2165
|
-
screen.render();
|
|
2166
|
-
return true;
|
|
2167
|
-
}
|
|
2168
|
-
if (key.name === "up" || key.name === "escape") {
|
|
2169
|
-
// Cancel: clear @target, back to normal chat
|
|
2170
|
-
clearTargetAgent();
|
|
2171
|
-
exitDashboardMode(false);
|
|
2172
|
-
return true;
|
|
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
|
-
}
|
|
2185
|
-
if (key.name === "enter" || key.name === "return") {
|
|
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);
|
|
2217
|
-
return true;
|
|
632
|
+
function sendRawToAgent(data) {
|
|
633
|
+
if (agentViewController) {
|
|
634
|
+
agentViewController.sendRawToAgent(data);
|
|
2218
635
|
}
|
|
2219
|
-
return false;
|
|
2220
636
|
}
|
|
2221
637
|
|
|
2222
|
-
function
|
|
2223
|
-
if (
|
|
2224
|
-
|
|
2225
|
-
updatePromptBox();
|
|
638
|
+
function sendResizeToAgent(cols, rows) {
|
|
639
|
+
if (agentViewController) {
|
|
640
|
+
agentViewController.sendResizeToAgent(cols, rows);
|
|
2226
641
|
}
|
|
2227
|
-
focusMode = "input";
|
|
2228
|
-
dashboardView = "agents";
|
|
2229
|
-
selectedAgentIndex = -1;
|
|
2230
|
-
screen.grabKeys = false;
|
|
2231
|
-
renderDashboard();
|
|
2232
|
-
focusInput();
|
|
2233
|
-
screen.render();
|
|
2234
642
|
}
|
|
2235
643
|
|
|
2236
|
-
function
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
screen.render();
|
|
2240
|
-
}
|
|
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}`);
|
|
644
|
+
function requestAgentSnapshot() {
|
|
645
|
+
if (agentViewController) {
|
|
646
|
+
agentViewController.requestAgentSnapshot();
|
|
2279
647
|
}
|
|
2280
648
|
}
|
|
2281
649
|
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
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`;
|
|
650
|
+
function writeToAgentTerm(text) {
|
|
651
|
+
if (agentViewController) {
|
|
652
|
+
agentViewController.writeToAgentTerm(text);
|
|
2319
653
|
}
|
|
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
654
|
}
|
|
2327
655
|
|
|
2328
|
-
function
|
|
2329
|
-
if (
|
|
2330
|
-
|
|
2331
|
-
disconnectAgentOutput();
|
|
2332
|
-
disconnectAgentInput();
|
|
656
|
+
function placeAgentCursor(cursor) {
|
|
657
|
+
if (agentViewController) {
|
|
658
|
+
agentViewController.placeAgentCursor(cursor);
|
|
2333
659
|
}
|
|
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
660
|
}
|
|
2368
661
|
|
|
2369
|
-
function
|
|
2370
|
-
if (
|
|
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();
|
|
662
|
+
function handleResizeInAgentView() {
|
|
663
|
+
if (!agentViewController) return false;
|
|
664
|
+
return agentViewController.handleResizeInAgentView();
|
|
2405
665
|
}
|
|
2406
666
|
|
|
2407
|
-
function
|
|
2408
|
-
|
|
2409
|
-
|
|
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
|
-
});
|
|
667
|
+
function getAgentLabel(agentId) {
|
|
668
|
+
return agentDirectory.getAgentLabel(activeAgentLabelMap, agentId);
|
|
669
|
+
}
|
|
2435
670
|
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
writeToAgentTerm(msg.data);
|
|
2448
|
-
}
|
|
2449
|
-
}
|
|
2450
|
-
} catch {
|
|
2451
|
-
// ignore malformed messages
|
|
671
|
+
function resolveAgentId(label) {
|
|
672
|
+
return agentDirectory.resolveAgentId({
|
|
673
|
+
label,
|
|
674
|
+
activeAgents,
|
|
675
|
+
labelMap: activeAgentLabelMap,
|
|
676
|
+
lookupNickname: (nickname) => {
|
|
677
|
+
try {
|
|
678
|
+
const busPath = getUfooPaths(projectRoot).agentsFile;
|
|
679
|
+
const bus = JSON.parse(fs.readFileSync(busPath, "utf8"));
|
|
680
|
+
for (const [id, meta] of Object.entries(bus.agents || {})) {
|
|
681
|
+
if (meta && meta.nickname === nickname) return id;
|
|
2452
682
|
}
|
|
683
|
+
} catch {
|
|
684
|
+
// ignore lookup errors
|
|
2453
685
|
}
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
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
|
-
}
|
|
686
|
+
return null;
|
|
687
|
+
},
|
|
688
|
+
});
|
|
2509
689
|
}
|
|
2510
690
|
|
|
2511
|
-
function
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
691
|
+
function resolveAgentDisplayName(publisher) {
|
|
692
|
+
return agentDirectory.resolveAgentDisplayName({
|
|
693
|
+
publisher,
|
|
694
|
+
labelMap: activeAgentLabelMap,
|
|
695
|
+
lookupNicknameById: (id) => {
|
|
696
|
+
try {
|
|
697
|
+
const busPath = getUfooPaths(projectRoot).agentsFile;
|
|
698
|
+
const bus = JSON.parse(fs.readFileSync(busPath, "utf8"));
|
|
699
|
+
const meta = bus.agents && bus.agents[id];
|
|
700
|
+
if (meta && meta.nickname) return meta.nickname;
|
|
701
|
+
} catch {
|
|
702
|
+
// Keep original publisher ID
|
|
703
|
+
}
|
|
704
|
+
return null;
|
|
705
|
+
},
|
|
706
|
+
});
|
|
2518
707
|
}
|
|
2519
708
|
|
|
2520
|
-
function
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
}
|
|
709
|
+
function clampAgentWindowWithSelection(selectionIndex) {
|
|
710
|
+
agentListWindowStart = agentDirectory.clampAgentWindowWithSelection({
|
|
711
|
+
activeCount: activeAgents.length,
|
|
712
|
+
maxWindow: MAX_AGENT_WINDOW,
|
|
713
|
+
windowStart: agentListWindowStart,
|
|
714
|
+
selectionIndex,
|
|
715
|
+
});
|
|
2527
716
|
}
|
|
2528
717
|
|
|
2529
|
-
function
|
|
2530
|
-
|
|
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
|
-
}
|
|
718
|
+
function clampAgentWindow() {
|
|
719
|
+
clampAgentWindowWithSelection(selectedAgentIndex);
|
|
2546
720
|
}
|
|
2547
721
|
|
|
2548
|
-
function
|
|
2549
|
-
|
|
722
|
+
function send(req) {
|
|
723
|
+
if (!daemonCoordinator) return;
|
|
724
|
+
daemonCoordinator.send(req);
|
|
2550
725
|
}
|
|
2551
726
|
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
if (!newClient) return;
|
|
2566
|
-
detachClient();
|
|
2567
|
-
client = newClient;
|
|
2568
|
-
connectionLostNotified = false;
|
|
2569
|
-
let buffer = "";
|
|
2570
|
-
client.on("data", (data) => {
|
|
2571
|
-
buffer += data.toString("utf8");
|
|
2572
|
-
const lines = buffer.split(/\r?\n/);
|
|
2573
|
-
buffer = lines.pop() || "";
|
|
2574
|
-
for (const line of lines.filter((l) => l.trim())) {
|
|
2575
|
-
try {
|
|
2576
|
-
const msg = JSON.parse(line);
|
|
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}`);
|
|
2600
|
-
}
|
|
2601
|
-
updateDashboard(data);
|
|
2602
|
-
}
|
|
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
|
-
}
|
|
2629
|
-
if (!payload.reply && !payload.disambiguate) {
|
|
2630
|
-
resolveStatusLine("{gray-fg}✓{/gray-fg} Done");
|
|
2631
|
-
}
|
|
2632
|
-
// opsResults are noisy JSON; keep them out of the log UI
|
|
2633
|
-
screen.render();
|
|
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");
|
|
2640
|
-
|
|
2641
|
-
// Try to parse message as JSON (from internal agents)
|
|
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");
|
|
2659
|
-
}
|
|
2660
|
-
|
|
2661
|
-
// Extract nickname if publisher is in subscriber:id format
|
|
2662
|
-
let displayName = publisher;
|
|
2663
|
-
if (publisher.includes(":")) {
|
|
2664
|
-
// Try to get nickname from activeAgentLabelMap or all-agents.json
|
|
2665
|
-
if (activeAgentLabelMap && activeAgentLabelMap.has(publisher)) {
|
|
2666
|
-
displayName = activeAgentLabelMap.get(publisher);
|
|
2667
|
-
} else {
|
|
2668
|
-
// Fallback: read directly from all-agents.json
|
|
2669
|
-
try {
|
|
2670
|
-
const busPath = getUfooPaths(projectRoot).agentsFile;
|
|
2671
|
-
const bus = JSON.parse(fs.readFileSync(busPath, "utf8"));
|
|
2672
|
-
const meta = bus.agents && bus.agents[publisher];
|
|
2673
|
-
if (meta && meta.nickname) {
|
|
2674
|
-
displayName = meta.nickname;
|
|
2675
|
-
}
|
|
2676
|
-
} catch {
|
|
2677
|
-
// Keep original publisher ID
|
|
2678
|
-
}
|
|
2679
|
-
}
|
|
2680
|
-
}
|
|
2681
|
-
|
|
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 列表
|
|
2690
|
-
requestStatus();
|
|
2691
|
-
}
|
|
2692
|
-
screen.render();
|
|
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
|
-
}
|
|
2698
|
-
} catch {
|
|
2699
|
-
// ignore
|
|
2700
|
-
}
|
|
2701
|
-
}
|
|
727
|
+
cronScheduler = createCronScheduler({
|
|
728
|
+
dispatch: ({ taskId, target, message }) => {
|
|
729
|
+
send({
|
|
730
|
+
type: IPC_REQUEST_TYPES.BUS_SEND,
|
|
731
|
+
target,
|
|
732
|
+
message,
|
|
733
|
+
});
|
|
734
|
+
queueStatusLine(`cron:${taskId} -> ${target}`);
|
|
735
|
+
},
|
|
736
|
+
onChange: () => {
|
|
737
|
+
renderDashboard();
|
|
738
|
+
screen.render();
|
|
739
|
+
},
|
|
2702
740
|
});
|
|
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();
|
|
2717
|
-
};
|
|
2718
|
-
|
|
2719
|
-
attachClient(client);
|
|
2720
|
-
|
|
2721
|
-
// Command handlers
|
|
2722
|
-
async function handleDoctorCommand() {
|
|
2723
|
-
logMessage("system", "{yellow-fg}⚙{/yellow-fg} Running health check...");
|
|
2724
741
|
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
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;
|
|
742
|
+
function updatePromptBox() {
|
|
743
|
+
if (targetAgent) {
|
|
744
|
+
const label = getAgentLabel(targetAgent);
|
|
745
|
+
promptBox.setContent(`>@${label}`);
|
|
746
|
+
promptBox.width = label.length + 3; // >@name + spacer
|
|
747
|
+
input.left = promptBox.width;
|
|
748
|
+
input.width = `100%-${promptBox.width}`;
|
|
749
|
+
} else {
|
|
750
|
+
promptBox.setContent(">");
|
|
751
|
+
promptBox.width = 2;
|
|
752
|
+
input.left = 2;
|
|
753
|
+
input.width = "100%-2";
|
|
754
|
+
}
|
|
755
|
+
if (!input.parent || !promptBox.parent) return;
|
|
756
|
+
resizeInput();
|
|
757
|
+
if (typeof input._updateCursor === "function") {
|
|
758
|
+
input._updateCursor();
|
|
2749
759
|
}
|
|
2750
760
|
}
|
|
2751
761
|
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
if (activeAgents.length
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
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}`);
|
|
762
|
+
function syncTargetFromSelection() {
|
|
763
|
+
if (focusMode !== "dashboard" || dashboardView !== "agents") return;
|
|
764
|
+
if (selectedAgentIndex >= 0 && selectedAgentIndex < activeAgents.length) {
|
|
765
|
+
const nextTarget = activeAgents[selectedAgentIndex];
|
|
766
|
+
if (nextTarget !== targetAgent) {
|
|
767
|
+
targetAgent = nextTarget;
|
|
768
|
+
updatePromptBox();
|
|
769
|
+
screen.render();
|
|
2763
770
|
}
|
|
2764
|
-
}
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
logMessage("system", "{green-fg}✓{/green-fg} Daemon is running");
|
|
2769
|
-
} else {
|
|
2770
|
-
logMessage("system", "{red-fg}✗{/red-fg} Daemon is not running");
|
|
771
|
+
} else if (targetAgent) {
|
|
772
|
+
targetAgent = null;
|
|
773
|
+
updatePromptBox();
|
|
774
|
+
screen.render();
|
|
2771
775
|
}
|
|
2772
776
|
}
|
|
2773
777
|
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
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");
|
|
778
|
+
function restoreTargetFromSelection() {
|
|
779
|
+
if (selectedAgentIndex >= 0 && selectedAgentIndex < activeAgents.length) {
|
|
780
|
+
targetAgent = activeAgents[selectedAgentIndex];
|
|
781
|
+
updatePromptBox();
|
|
2810
782
|
}
|
|
2811
783
|
}
|
|
2812
784
|
|
|
2813
|
-
|
|
2814
|
-
|
|
785
|
+
function focusInput() {
|
|
786
|
+
input.focus();
|
|
787
|
+
input._updateCursor();
|
|
788
|
+
}
|
|
2815
789
|
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
790
|
+
function focusLog() {
|
|
791
|
+
logBox.focus();
|
|
792
|
+
screen.program.hideCursor();
|
|
793
|
+
}
|
|
2820
794
|
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
logMessage("system", msg);
|
|
2826
|
-
};
|
|
2827
|
-
console.error = (...args) => {
|
|
2828
|
-
const msg = args.join(" ");
|
|
2829
|
-
logs.push(`ERROR: ${msg}`);
|
|
2830
|
-
logMessage("error", msg);
|
|
2831
|
-
};
|
|
795
|
+
function scrollLog(offset) {
|
|
796
|
+
logBox.scroll(offset);
|
|
797
|
+
screen.render();
|
|
798
|
+
}
|
|
2832
799
|
|
|
2833
|
-
|
|
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 });
|
|
800
|
+
let settingsController = null;
|
|
2838
801
|
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
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;
|
|
802
|
+
function setLaunchMode(mode) {
|
|
803
|
+
if (settingsController) {
|
|
804
|
+
settingsController.setLaunchMode(mode);
|
|
2850
805
|
}
|
|
2851
806
|
}
|
|
2852
807
|
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
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}`);
|
|
808
|
+
function requestCloseAgent(agentId) {
|
|
809
|
+
if (!agentId) {
|
|
810
|
+
logMessage("error", "{white-fg}✗{/white-fg} No agent selected");
|
|
811
|
+
return;
|
|
2916
812
|
}
|
|
813
|
+
const label = getAgentLabel(agentId);
|
|
814
|
+
logMessage("status", `{white-fg}⚙{/white-fg} Closing ${label}...`);
|
|
815
|
+
send({ type: IPC_REQUEST_TYPES.CLOSE_AGENT, agent_id: agentId });
|
|
2917
816
|
}
|
|
2918
817
|
|
|
2919
|
-
|
|
2920
|
-
|
|
818
|
+
function setAgentProvider(provider) {
|
|
819
|
+
if (settingsController) {
|
|
820
|
+
settingsController.setAgentProvider(provider);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
2921
823
|
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
824
|
+
function setAssistantEngine(value) {
|
|
825
|
+
if (settingsController) {
|
|
826
|
+
settingsController.setAssistantEngine(value);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
2925
829
|
|
|
2926
|
-
|
|
2927
|
-
|
|
830
|
+
function setAutoResume(value) {
|
|
831
|
+
if (settingsController) {
|
|
832
|
+
settingsController.setAutoResume(value);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
2928
835
|
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
836
|
+
async function restartDaemon() {
|
|
837
|
+
if (!daemonCoordinator) return;
|
|
838
|
+
return daemonCoordinator.restart();
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
settingsController = createSettingsController({
|
|
842
|
+
projectRoot,
|
|
843
|
+
saveConfig,
|
|
844
|
+
normalizeLaunchMode,
|
|
845
|
+
normalizeAgentProvider,
|
|
846
|
+
normalizeAssistantEngine,
|
|
847
|
+
fsModule: fs,
|
|
848
|
+
getUfooPaths,
|
|
849
|
+
logMessage,
|
|
850
|
+
renderDashboard,
|
|
851
|
+
renderScreen: () => screen.render(),
|
|
852
|
+
restartDaemon,
|
|
853
|
+
getLaunchMode: () => launchMode,
|
|
854
|
+
setLaunchModeState: (value) => {
|
|
855
|
+
launchMode = value;
|
|
856
|
+
},
|
|
857
|
+
setSelectedModeIndex: (value) => {
|
|
858
|
+
selectedModeIndex = value;
|
|
859
|
+
},
|
|
860
|
+
getAgentProvider: () => agentProvider,
|
|
861
|
+
setAgentProviderState: (value) => {
|
|
862
|
+
agentProvider = value;
|
|
863
|
+
},
|
|
864
|
+
setSelectedProviderIndex: (value) => {
|
|
865
|
+
selectedProviderIndex = value;
|
|
866
|
+
},
|
|
867
|
+
getAssistantEngine: () => assistantEngine,
|
|
868
|
+
setAssistantEngineState: (value) => {
|
|
869
|
+
assistantEngine = value;
|
|
870
|
+
},
|
|
871
|
+
setSelectedAssistantIndex: (value) => {
|
|
872
|
+
selectedAssistantIndex = value;
|
|
873
|
+
},
|
|
874
|
+
assistantOptions,
|
|
875
|
+
providerOptions,
|
|
876
|
+
getAutoResume: () => autoResume,
|
|
877
|
+
setAutoResumeState: (value) => {
|
|
878
|
+
autoResume = value;
|
|
879
|
+
},
|
|
880
|
+
setSelectedResumeIndex: (value) => {
|
|
881
|
+
selectedResumeIndex = value;
|
|
882
|
+
},
|
|
883
|
+
});
|
|
2940
884
|
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
} finally {
|
|
2946
|
-
console.log = originalLog;
|
|
2947
|
-
console.error = originalError;
|
|
885
|
+
function clearLog() {
|
|
886
|
+
logBox.setContent("");
|
|
887
|
+
if (typeof logBox.scrollTo === "function") {
|
|
888
|
+
logBox.scrollTo(0);
|
|
2948
889
|
}
|
|
890
|
+
screen.render();
|
|
2949
891
|
}
|
|
2950
892
|
|
|
2951
|
-
|
|
2952
|
-
const
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
893
|
+
function renderDashboard() {
|
|
894
|
+
const computed = computeDashboardContent({
|
|
895
|
+
focusMode,
|
|
896
|
+
dashboardView,
|
|
897
|
+
activeAgents,
|
|
898
|
+
selectedAgentIndex,
|
|
899
|
+
agentListWindowStart,
|
|
900
|
+
maxAgentWindow: MAX_AGENT_WINDOW,
|
|
901
|
+
getAgentLabel,
|
|
902
|
+
launchMode,
|
|
903
|
+
agentProvider,
|
|
904
|
+
assistantEngine,
|
|
905
|
+
autoResume,
|
|
906
|
+
selectedModeIndex,
|
|
907
|
+
selectedProviderIndex,
|
|
908
|
+
selectedAssistantIndex,
|
|
909
|
+
selectedResumeIndex,
|
|
910
|
+
cronTasks: cronScheduler.listTasks(),
|
|
911
|
+
providerOptions,
|
|
912
|
+
assistantOptions,
|
|
913
|
+
resumeOptions,
|
|
914
|
+
pendingReports: reportPendingTotal,
|
|
915
|
+
dashHints: DASH_HINTS,
|
|
916
|
+
});
|
|
917
|
+
agentListWindowStart = computed.windowStart;
|
|
918
|
+
dashboard.setContent(computed.content);
|
|
919
|
+
}
|
|
2957
920
|
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
921
|
+
function updateDashboard(status) {
|
|
922
|
+
activeAgents = status.active || [];
|
|
923
|
+
reportPendingTotal = Number.isFinite(status?.reports?.pending_total)
|
|
924
|
+
? status.reports.pending_total
|
|
925
|
+
: 0;
|
|
926
|
+
const metaList = Array.isArray(status.active_meta) ? status.active_meta : [];
|
|
927
|
+
let fallbackMap = null;
|
|
928
|
+
if (metaList.length === 0 && activeAgents.length > 0) {
|
|
929
|
+
try {
|
|
930
|
+
const busPath = getUfooPaths(projectRoot).agentsFile;
|
|
931
|
+
const bus = JSON.parse(fs.readFileSync(busPath, "utf8"));
|
|
932
|
+
fallbackMap = new Map();
|
|
933
|
+
for (const [id, meta] of Object.entries(bus.agents || {})) {
|
|
934
|
+
if (meta && meta.nickname) fallbackMap.set(id, meta.nickname);
|
|
2971
935
|
}
|
|
2972
|
-
}
|
|
2973
|
-
|
|
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");
|
|
936
|
+
} catch {
|
|
937
|
+
fallbackMap = null;
|
|
2979
938
|
}
|
|
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
939
|
}
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
940
|
+
const maps = agentDirectory.buildAgentMaps(activeAgents, metaList, fallbackMap);
|
|
941
|
+
activeAgentLabelMap = maps.labelMap;
|
|
942
|
+
activeAgentMetaMap = maps.metaMap;
|
|
943
|
+
clampAgentWindow();
|
|
944
|
+
// If viewing agent went offline, exit view
|
|
945
|
+
const currentView = getCurrentView();
|
|
946
|
+
const viewingAgent = getViewingAgent();
|
|
947
|
+
if (currentView === "agent" && viewingAgent && !activeAgents.includes(viewingAgent)) {
|
|
948
|
+
writeToAgentTerm("\r\n\x1b[1;31m[Agent went offline]\x1b[0m\r\n");
|
|
949
|
+
exitAgentView();
|
|
2993
950
|
return;
|
|
2994
951
|
}
|
|
2995
952
|
|
|
2996
|
-
|
|
2997
|
-
if (
|
|
2998
|
-
|
|
953
|
+
// In agent view, only update the dashboard bar (blessed is frozen)
|
|
954
|
+
if (currentView === "agent") {
|
|
955
|
+
if (focusMode === "dashboard") {
|
|
956
|
+
const totalItems = 1 + activeAgents.length;
|
|
957
|
+
if (selectedAgentIndex < 0 || selectedAgentIndex >= totalItems) {
|
|
958
|
+
selectedAgentIndex = 0;
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
renderAgentDashboard();
|
|
2999
962
|
return;
|
|
3000
963
|
}
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
964
|
+
if (focusMode === "dashboard") {
|
|
965
|
+
if (dashboardView === "agents") {
|
|
966
|
+
if (activeAgents.length === 0) {
|
|
967
|
+
selectedAgentIndex = -1;
|
|
968
|
+
} else if (selectedAgentIndex < 0 || selectedAgentIndex >= activeAgents.length) {
|
|
969
|
+
selectedAgentIndex = 0;
|
|
970
|
+
}
|
|
971
|
+
clampAgentWindow();
|
|
3009
972
|
}
|
|
3010
973
|
}
|
|
974
|
+
syncTargetFromSelection();
|
|
975
|
+
renderDashboard();
|
|
976
|
+
screen.render();
|
|
977
|
+
}
|
|
3011
978
|
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
979
|
+
function enterDashboardMode() {
|
|
980
|
+
focusMode = "dashboard";
|
|
981
|
+
dashboardView = "agents";
|
|
982
|
+
selectedAgentIndex = activeAgents.length > 0 ? 0 : -1;
|
|
983
|
+
agentListWindowStart = 0;
|
|
984
|
+
clampAgentWindow();
|
|
985
|
+
selectedModeIndex = launchMode === "internal" ? 2 : (launchMode === "tmux" ? 1 : 0);
|
|
986
|
+
selectedProviderIndex = Math.max(0, providerOptions.findIndex((opt) => opt.value === agentProvider));
|
|
987
|
+
selectedAssistantIndex = Math.max(
|
|
988
|
+
0,
|
|
989
|
+
assistantOptions.findIndex((opt) => opt.value === assistantEngine)
|
|
990
|
+
);
|
|
991
|
+
selectedResumeIndex = autoResume ? 0 : 1;
|
|
992
|
+
// Immediately set @target when first agent is selected
|
|
993
|
+
if (selectedAgentIndex >= 0 && selectedAgentIndex < activeAgents.length) {
|
|
994
|
+
targetAgent = activeAgents[selectedAgentIndex];
|
|
995
|
+
updatePromptBox();
|
|
3017
996
|
}
|
|
997
|
+
screen.grabKeys = true;
|
|
998
|
+
renderDashboard();
|
|
999
|
+
screen.program.hideCursor();
|
|
1000
|
+
screen.render();
|
|
1001
|
+
syncTargetFromSelection();
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
const dashboardState = {};
|
|
1005
|
+
Object.defineProperties(dashboardState, {
|
|
1006
|
+
currentView: { get: () => getCurrentView() },
|
|
1007
|
+
focusMode: { get: () => focusMode, set: (value) => { focusMode = value; } },
|
|
1008
|
+
dashboardView: { get: () => dashboardView, set: (value) => { dashboardView = value; } },
|
|
1009
|
+
selectedAgentIndex: { get: () => selectedAgentIndex, set: (value) => { selectedAgentIndex = value; } },
|
|
1010
|
+
activeAgents: { get: () => activeAgents },
|
|
1011
|
+
viewingAgent: { get: () => getViewingAgent() },
|
|
1012
|
+
activeAgentMetaMap: { get: () => activeAgentMetaMap },
|
|
1013
|
+
selectedModeIndex: { get: () => selectedModeIndex, set: (value) => { selectedModeIndex = value; } },
|
|
1014
|
+
selectedProviderIndex: { get: () => selectedProviderIndex, set: (value) => { selectedProviderIndex = value; } },
|
|
1015
|
+
selectedAssistantIndex: { get: () => selectedAssistantIndex, set: (value) => { selectedAssistantIndex = value; } },
|
|
1016
|
+
selectedResumeIndex: { get: () => selectedResumeIndex, set: (value) => { selectedResumeIndex = value; } },
|
|
1017
|
+
launchMode: { get: () => launchMode },
|
|
1018
|
+
agentProvider: { get: () => agentProvider },
|
|
1019
|
+
assistantEngine: { get: () => assistantEngine },
|
|
1020
|
+
autoResume: { get: () => autoResume },
|
|
1021
|
+
cronTasks: { get: () => cronScheduler.listTasks() },
|
|
1022
|
+
providerOptions: { get: () => providerOptions },
|
|
1023
|
+
assistantOptions: { get: () => assistantOptions },
|
|
1024
|
+
resumeOptions: { get: () => resumeOptions },
|
|
1025
|
+
agentOutputSuppressed: {
|
|
1026
|
+
get: () => getAgentOutputSuppressed(),
|
|
1027
|
+
set: (value) => { setAgentOutputSuppressed(value); },
|
|
1028
|
+
},
|
|
1029
|
+
});
|
|
3018
1030
|
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
1031
|
+
function activateAgent(agentId) {
|
|
1032
|
+
if (!agentId) return;
|
|
1033
|
+
const activator = new AgentActivator(projectRoot);
|
|
1034
|
+
activator.activate(agentId).catch(() => {});
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
terminalAdapterRouter = createTerminalAdapterRouter({
|
|
1038
|
+
activateAgent,
|
|
1039
|
+
sendRaw: (data) => agentSockets.sendRaw(data),
|
|
1040
|
+
sendResize: (cols, rows) => agentSockets.sendResize(cols, rows),
|
|
1041
|
+
requestSnapshot: (mode) => agentSockets.requestSnapshot(mode),
|
|
1042
|
+
});
|
|
1043
|
+
|
|
1044
|
+
const dashboardController = createDashboardKeyController({
|
|
1045
|
+
state: dashboardState,
|
|
1046
|
+
existsSync: fs.existsSync,
|
|
1047
|
+
getInjectSockPath,
|
|
1048
|
+
getAgentAdapter,
|
|
1049
|
+
activateAgent,
|
|
1050
|
+
requestCloseAgent,
|
|
1051
|
+
enterAgentView,
|
|
1052
|
+
exitAgentView,
|
|
1053
|
+
setAgentBarVisible,
|
|
1054
|
+
requestAgentSnapshot,
|
|
1055
|
+
clearTargetAgent,
|
|
1056
|
+
restoreTargetFromSelection,
|
|
1057
|
+
syncTargetFromSelection,
|
|
1058
|
+
exitDashboardMode,
|
|
1059
|
+
setLaunchMode,
|
|
1060
|
+
setAgentProvider,
|
|
1061
|
+
setAssistantEngine,
|
|
1062
|
+
setAutoResume,
|
|
1063
|
+
clampAgentWindow,
|
|
1064
|
+
clampAgentWindowWithSelection,
|
|
1065
|
+
renderDashboard,
|
|
1066
|
+
renderAgentDashboard,
|
|
1067
|
+
renderScreen: () => screen.render(),
|
|
1068
|
+
setScreenGrabKeys: (value) => {
|
|
1069
|
+
screen.grabKeys = Boolean(value);
|
|
1070
|
+
},
|
|
1071
|
+
});
|
|
1072
|
+
|
|
1073
|
+
function handleDashboardKey(key) {
|
|
1074
|
+
return dashboardController.handleDashboardKey(key);
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
function exitDashboardMode(selectAgent = false) {
|
|
1078
|
+
if (selectAgent && selectedAgentIndex >= 0 && selectedAgentIndex < activeAgents.length) {
|
|
1079
|
+
targetAgent = activeAgents[selectedAgentIndex];
|
|
1080
|
+
updatePromptBox();
|
|
3031
1081
|
}
|
|
1082
|
+
focusMode = "input";
|
|
1083
|
+
dashboardView = "agents";
|
|
1084
|
+
selectedAgentIndex = -1;
|
|
1085
|
+
screen.grabKeys = false;
|
|
1086
|
+
renderDashboard();
|
|
1087
|
+
focusInput();
|
|
1088
|
+
screen.render();
|
|
3032
1089
|
}
|
|
3033
1090
|
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
send({ type: "resume_agents", target });
|
|
3039
|
-
setTimeout(requestStatus, 1000);
|
|
1091
|
+
function clearTargetAgent() {
|
|
1092
|
+
targetAgent = null;
|
|
1093
|
+
updatePromptBox();
|
|
1094
|
+
screen.render();
|
|
3040
1095
|
}
|
|
3041
1096
|
|
|
3042
|
-
function
|
|
3043
|
-
|
|
1097
|
+
function getInjectSockPath(agentId) {
|
|
1098
|
+
const safeName = subscriberToSafeName(agentId);
|
|
1099
|
+
return path.join(getUfooPaths(projectRoot).busQueuesDir, safeName, "inject.sock");
|
|
1100
|
+
}
|
|
3044
1101
|
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
1102
|
+
agentViewController = createAgentViewController({
|
|
1103
|
+
screen,
|
|
1104
|
+
input,
|
|
1105
|
+
processStdout: process.stdout,
|
|
1106
|
+
computeAgentBar,
|
|
1107
|
+
agentBarHints: AGENT_BAR_HINTS,
|
|
1108
|
+
maxAgentWindow: MAX_AGENT_WINDOW,
|
|
1109
|
+
getFocusMode: () => focusMode,
|
|
1110
|
+
setFocusMode: (value) => {
|
|
1111
|
+
focusMode = value;
|
|
1112
|
+
},
|
|
1113
|
+
getSelectedAgentIndex: () => selectedAgentIndex,
|
|
1114
|
+
setSelectedAgentIndex: (value) => {
|
|
1115
|
+
selectedAgentIndex = value;
|
|
1116
|
+
},
|
|
1117
|
+
getActiveAgents: () => activeAgents,
|
|
1118
|
+
getAgentListWindowStart: () => agentListWindowStart,
|
|
1119
|
+
setAgentListWindowStart: (value) => {
|
|
1120
|
+
agentListWindowStart = value;
|
|
1121
|
+
},
|
|
1122
|
+
getAgentLabel,
|
|
1123
|
+
setDashboardView: (value) => {
|
|
1124
|
+
dashboardView = value;
|
|
1125
|
+
},
|
|
1126
|
+
setScreenGrabKeys: (value) => {
|
|
1127
|
+
screen.grabKeys = Boolean(value);
|
|
1128
|
+
},
|
|
1129
|
+
clearTargetAgent,
|
|
1130
|
+
renderDashboard,
|
|
1131
|
+
focusInput,
|
|
1132
|
+
resizeInput,
|
|
1133
|
+
renderScreen: () => screen.render(),
|
|
1134
|
+
getInjectSockPath,
|
|
1135
|
+
connectAgentOutput: (sockPath) => {
|
|
1136
|
+
agentSockets.connectOutput(sockPath);
|
|
1137
|
+
},
|
|
1138
|
+
disconnectAgentOutput: () => {
|
|
1139
|
+
agentSockets.disconnectOutput();
|
|
1140
|
+
},
|
|
1141
|
+
connectAgentInput: (sockPath) => {
|
|
1142
|
+
agentSockets.connectInput(sockPath);
|
|
1143
|
+
},
|
|
1144
|
+
disconnectAgentInput: () => {
|
|
1145
|
+
agentSockets.disconnectInput();
|
|
1146
|
+
},
|
|
1147
|
+
sendRaw: (data) => {
|
|
1148
|
+
sendRawWithCapabilities(data);
|
|
1149
|
+
},
|
|
1150
|
+
sendResize: (cols, rows) => {
|
|
1151
|
+
sendResizeWithCapabilities(cols, rows);
|
|
1152
|
+
},
|
|
1153
|
+
requestScreenSnapshot: () => {
|
|
1154
|
+
requestSnapshotWithCapabilities();
|
|
1155
|
+
},
|
|
1156
|
+
});
|
|
3048
1157
|
|
|
3049
|
-
|
|
3050
|
-
|
|
1158
|
+
function requestStatus() {
|
|
1159
|
+
if (!daemonCoordinator) return;
|
|
1160
|
+
daemonCoordinator.requestStatus();
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
const daemonMessageRouter = createDaemonMessageRouter({
|
|
1164
|
+
escapeBlessed,
|
|
1165
|
+
stripBlessedTags,
|
|
1166
|
+
logMessage,
|
|
1167
|
+
renderScreen: () => screen.render(),
|
|
1168
|
+
updateDashboard,
|
|
1169
|
+
requestStatus,
|
|
1170
|
+
resolveStatusLine,
|
|
1171
|
+
enqueueBusStatus,
|
|
1172
|
+
resolveBusStatus,
|
|
1173
|
+
getPending: () => pending,
|
|
1174
|
+
setPending: (value) => {
|
|
1175
|
+
pending = value;
|
|
1176
|
+
},
|
|
1177
|
+
resolveAgentDisplayName,
|
|
1178
|
+
getCurrentView: () => getCurrentView(),
|
|
1179
|
+
isAgentViewUsesBus: () => isAgentViewUsesBus(),
|
|
1180
|
+
getViewingAgent: () => getViewingAgent(),
|
|
1181
|
+
writeToAgentTerm,
|
|
1182
|
+
consumePendingDelivery,
|
|
1183
|
+
getPendingState,
|
|
1184
|
+
beginStream,
|
|
1185
|
+
appendStreamDelta,
|
|
1186
|
+
finalizeStream,
|
|
1187
|
+
hasStream: (publisher) => streamTracker.hasStream(publisher),
|
|
1188
|
+
});
|
|
3051
1189
|
|
|
3052
|
-
|
|
3053
|
-
|
|
1190
|
+
daemonCoordinator = createDaemonCoordinator({
|
|
1191
|
+
projectRoot,
|
|
1192
|
+
daemonTransport,
|
|
1193
|
+
handleMessage: (msg) => daemonMessageRouter.handleMessage(msg),
|
|
1194
|
+
queueStatusLine,
|
|
1195
|
+
resolveStatusLine,
|
|
1196
|
+
logMessage,
|
|
1197
|
+
stopDaemon,
|
|
1198
|
+
startDaemon,
|
|
1199
|
+
});
|
|
3054
1200
|
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
if
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
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;
|
|
1201
|
+
const connected = await daemonCoordinator.connect();
|
|
1202
|
+
if (!connected) {
|
|
1203
|
+
// Check if daemon failed to start
|
|
1204
|
+
if (!isRunning(projectRoot)) {
|
|
1205
|
+
const logFile = getUfooPaths(projectRoot).ufooDaemonLog;
|
|
1206
|
+
// eslint-disable-next-line no-console
|
|
1207
|
+
console.error("Failed to start ufoo daemon. Check logs at:", logFile);
|
|
1208
|
+
throw new Error("Daemon failed to start. Check the daemon log for details.");
|
|
3092
1209
|
}
|
|
1210
|
+
throw new Error("Failed to connect to ufoo daemon (timeout). The daemon may still be starting.");
|
|
3093
1211
|
}
|
|
3094
1212
|
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
1213
|
+
const commandExecutor = createCommandExecutor({
|
|
1214
|
+
projectRoot,
|
|
1215
|
+
parseCommand,
|
|
1216
|
+
escapeBlessed,
|
|
1217
|
+
logMessage,
|
|
1218
|
+
renderScreen: () => screen.render(),
|
|
1219
|
+
getActiveAgents: () => activeAgents,
|
|
1220
|
+
getActiveAgentMetaMap: () => activeAgentMetaMap,
|
|
1221
|
+
getAgentLabel,
|
|
1222
|
+
isDaemonRunning: isRunning,
|
|
1223
|
+
startDaemon,
|
|
1224
|
+
stopDaemon,
|
|
1225
|
+
restartDaemon,
|
|
1226
|
+
send,
|
|
1227
|
+
requestStatus,
|
|
1228
|
+
createCronTask: ({ intervalMs, targets, prompt }) =>
|
|
1229
|
+
cronScheduler.addTask({ intervalMs, targets, prompt }),
|
|
1230
|
+
listCronTasks: () => cronScheduler.listTasks(),
|
|
1231
|
+
stopCronTask: (id) => cronScheduler.stopTask(id),
|
|
1232
|
+
activateAgent: async (target) => {
|
|
1233
|
+
const activator = new AgentActivator(projectRoot);
|
|
1234
|
+
await activator.activate(target);
|
|
1235
|
+
},
|
|
1236
|
+
});
|
|
3117
1237
|
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
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
|
-
}
|
|
1238
|
+
async function executeCommand(text) {
|
|
1239
|
+
return commandExecutor.executeCommand(text);
|
|
1240
|
+
}
|
|
3152
1241
|
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
}
|
|
1242
|
+
const submitState = {};
|
|
1243
|
+
Object.defineProperties(submitState, {
|
|
1244
|
+
targetAgent: { get: () => targetAgent, set: (value) => { targetAgent = value; } },
|
|
1245
|
+
pending: { get: () => pending, set: (value) => { pending = value; } },
|
|
1246
|
+
activeAgentMetaMap: { get: () => activeAgentMetaMap },
|
|
1247
|
+
});
|
|
3157
1248
|
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
1249
|
+
const inputSubmitHandler = createInputSubmitHandler({
|
|
1250
|
+
state: submitState,
|
|
1251
|
+
parseAtTarget,
|
|
1252
|
+
resolveAgentId,
|
|
1253
|
+
executeCommand,
|
|
1254
|
+
queueStatusLine,
|
|
1255
|
+
send,
|
|
1256
|
+
logMessage,
|
|
1257
|
+
getAgentLabel,
|
|
1258
|
+
escapeBlessed,
|
|
1259
|
+
markPendingDelivery,
|
|
1260
|
+
clearTargetAgent,
|
|
1261
|
+
setTargetAgent: (agentId) => {
|
|
1262
|
+
targetAgent = agentId || null;
|
|
1263
|
+
updatePromptBox();
|
|
1264
|
+
screen.render();
|
|
1265
|
+
},
|
|
1266
|
+
enterAgentView,
|
|
1267
|
+
activateAgent: async (agentId) => {
|
|
1268
|
+
const activator = new AgentActivator(projectRoot);
|
|
1269
|
+
await activator.activate(agentId);
|
|
1270
|
+
},
|
|
1271
|
+
getInjectSockPath,
|
|
1272
|
+
existsSync: fs.existsSync,
|
|
1273
|
+
commitInputHistory: (text) => {
|
|
1274
|
+
if (inputHistoryController) inputHistoryController.commitSubmittedText(text);
|
|
1275
|
+
},
|
|
1276
|
+
focusInput: () => input.focus(),
|
|
1277
|
+
renderScreen: () => screen.render(), // Add renderScreen callback
|
|
1278
|
+
});
|
|
3169
1279
|
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
send({
|
|
3176
|
-
type: "prompt",
|
|
3177
|
-
text: `Use agent ${choice.agent_id} to handle: ${pending.original || "the request"}`,
|
|
3178
|
-
});
|
|
3179
|
-
pending = null;
|
|
3180
|
-
} else {
|
|
3181
|
-
logMessage("error", escapeBlessed("Invalid selection."));
|
|
3182
|
-
}
|
|
3183
|
-
} else {
|
|
3184
|
-
pending = { original: text };
|
|
3185
|
-
queueStatusLine("ufoo-agent processing");
|
|
3186
|
-
send({ type: "prompt", text });
|
|
3187
|
-
logMessage("user", `{cyan-fg}→{/cyan-fg} ${escapeBlessed(text)}`);
|
|
3188
|
-
}
|
|
3189
|
-
input.focus();
|
|
1280
|
+
input.on("submit", async (value) => {
|
|
1281
|
+
input.clearValue();
|
|
1282
|
+
screen.render(); // Render cleared input
|
|
1283
|
+
await inputSubmitHandler.handleSubmit(value);
|
|
1284
|
+
// No need for second render - handleSubmit now calls renderScreen() internally
|
|
3190
1285
|
});
|
|
3191
1286
|
|
|
3192
1287
|
screen.key(["C-c"], exitHandler);
|
|
3193
1288
|
|
|
3194
1289
|
// Agent TTY view: enter dashboard mode
|
|
3195
1290
|
function enterAgentDashboardMode() {
|
|
3196
|
-
|
|
3197
|
-
|
|
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;
|
|
1291
|
+
if (agentViewController) {
|
|
1292
|
+
agentViewController.enterAgentDashboardMode();
|
|
3223
1293
|
}
|
|
3224
1294
|
}
|
|
3225
1295
|
|
|
3226
1296
|
// Dashboard navigation - use screen.on to capture even when input is focused
|
|
3227
1297
|
screen.on("keypress", (ch, key) => {
|
|
3228
1298
|
// Agent TTY view: handle keystrokes
|
|
3229
|
-
if (
|
|
1299
|
+
if (getCurrentView() === "agent") {
|
|
3230
1300
|
if (focusMode === "dashboard") {
|
|
3231
1301
|
handleDashboardKey(key);
|
|
3232
1302
|
return;
|
|
3233
1303
|
}
|
|
3234
|
-
// Suppress input briefly after entering agent view
|
|
3235
|
-
|
|
3236
|
-
if (Date.now() < agentInputSuppressUntil) {
|
|
1304
|
+
// Suppress input briefly after entering agent view
|
|
1305
|
+
if (Date.now() < getAgentInputSuppressUntil()) {
|
|
3237
1306
|
return;
|
|
3238
1307
|
}
|
|
3239
1308
|
// Ctrl+C exits entire app
|
|
@@ -3258,7 +1327,7 @@ async function runChat(projectRoot) {
|
|
|
3258
1327
|
});
|
|
3259
1328
|
|
|
3260
1329
|
screen.key(["tab"], () => {
|
|
3261
|
-
if (
|
|
1330
|
+
if (getCurrentView() === "agent") return; // Tab goes to PTY via keypress handler
|
|
3262
1331
|
if (focusMode === "dashboard") {
|
|
3263
1332
|
exitDashboardMode(false);
|
|
3264
1333
|
} else {
|
|
@@ -3267,13 +1336,13 @@ async function runChat(projectRoot) {
|
|
|
3267
1336
|
});
|
|
3268
1337
|
|
|
3269
1338
|
screen.key(["C-k", "M-k"], () => {
|
|
3270
|
-
if (
|
|
1339
|
+
if (getCurrentView() === "agent") return;
|
|
3271
1340
|
clearLog();
|
|
3272
1341
|
});
|
|
3273
1342
|
|
|
3274
1343
|
|
|
3275
1344
|
screen.key(["i", "enter"], () => {
|
|
3276
|
-
if (
|
|
1345
|
+
if (getCurrentView() === "agent") return;
|
|
3277
1346
|
if (focusMode === "dashboard") return;
|
|
3278
1347
|
if (screen.focused === input) return;
|
|
3279
1348
|
focusInput();
|
|
@@ -3292,43 +1361,7 @@ async function runChat(projectRoot) {
|
|
|
3292
1361
|
}
|
|
3293
1362
|
if (screen.program) {
|
|
3294
1363
|
screen.program.on("data", (data) => {
|
|
3295
|
-
|
|
3296
|
-
const chunk = data.toString("utf8");
|
|
3297
|
-
if (!pasteActive && !chunk.includes(PASTE_START) && !pasteRemainder.includes(PASTE_START)) {
|
|
3298
|
-
const keep = PASTE_START.length - 1;
|
|
3299
|
-
pasteRemainder = (pasteRemainder + chunk).slice(-keep);
|
|
3300
|
-
return;
|
|
3301
|
-
}
|
|
3302
|
-
let buffer = pasteRemainder + chunk;
|
|
3303
|
-
pasteRemainder = "";
|
|
3304
|
-
while (buffer.length > 0) {
|
|
3305
|
-
if (!pasteActive) {
|
|
3306
|
-
const start = buffer.indexOf(PASTE_START);
|
|
3307
|
-
if (start === -1) {
|
|
3308
|
-
const keep = PASTE_START.length - 1;
|
|
3309
|
-
pasteRemainder = buffer.slice(-keep);
|
|
3310
|
-
return;
|
|
3311
|
-
}
|
|
3312
|
-
buffer = buffer.slice(start + PASTE_START.length);
|
|
3313
|
-
pasteActive = true;
|
|
3314
|
-
pasteBuffer = "";
|
|
3315
|
-
scheduleSuppressReset();
|
|
3316
|
-
continue;
|
|
3317
|
-
}
|
|
3318
|
-
const end = buffer.indexOf(PASTE_END);
|
|
3319
|
-
if (end === -1) {
|
|
3320
|
-
pasteBuffer += buffer;
|
|
3321
|
-
scheduleSuppressReset();
|
|
3322
|
-
return;
|
|
3323
|
-
}
|
|
3324
|
-
pasteBuffer += buffer.slice(0, end);
|
|
3325
|
-
buffer = buffer.slice(end + PASTE_END.length);
|
|
3326
|
-
pasteActive = false;
|
|
3327
|
-
scheduleSuppressReset();
|
|
3328
|
-
const normalized = normalizePaste(pasteBuffer);
|
|
3329
|
-
pasteBuffer = "";
|
|
3330
|
-
if (normalized) insertTextAtCursor(normalized);
|
|
3331
|
-
}
|
|
1364
|
+
pasteController.handleProgramData(data);
|
|
3332
1365
|
});
|
|
3333
1366
|
}
|
|
3334
1367
|
loadHistory();
|
|
@@ -3339,24 +1372,19 @@ async function runChat(projectRoot) {
|
|
|
3339
1372
|
|
|
3340
1373
|
// 定期刷新 dashboard 状态(兜底,daemon 会主动推送变化)
|
|
3341
1374
|
setInterval(() => {
|
|
3342
|
-
if (
|
|
1375
|
+
if (daemonCoordinator && daemonCoordinator.isConnected()) {
|
|
3343
1376
|
requestStatus();
|
|
3344
1377
|
}
|
|
3345
1378
|
}, 30000);
|
|
3346
|
-
|
|
3347
1379
|
screen.on("resize", () => {
|
|
3348
|
-
if (
|
|
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();
|
|
1380
|
+
if (handleResizeInAgentView()) {
|
|
3355
1381
|
return;
|
|
3356
1382
|
}
|
|
3357
1383
|
resizeInput();
|
|
3358
|
-
if (
|
|
1384
|
+
if (completionController.isActive()) completionController.hide();
|
|
3359
1385
|
input._updateCursor();
|
|
1386
|
+
// Force recalculate logBox width to match terminal
|
|
1387
|
+
logBox.width = screen.width;
|
|
3360
1388
|
screen.render();
|
|
3361
1389
|
});
|
|
3362
1390
|
screen.render();
|