u-foo 1.0.6 → 1.1.9
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 +44 -4
- 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 +11 -2
- 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 +154 -0
- package/src/chat/index.js +935 -2909
- 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 +132 -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 +1580 -0
- package/src/config.js +47 -1
- 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 +661 -488
- 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,839 @@ 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" },
|
|
1611
486
|
];
|
|
1612
487
|
let selectedProviderIndex = agentProvider === "claude-cli" ? 1 : 0;
|
|
488
|
+
const assistantOptions = [
|
|
489
|
+
{ label: "auto", value: "auto" },
|
|
490
|
+
{ label: "codex", value: "codex" },
|
|
491
|
+
{ label: "claude", value: "claude" },
|
|
492
|
+
{ label: "ufoo", value: "ufoo" },
|
|
493
|
+
];
|
|
494
|
+
let selectedAssistantIndex = Math.max(
|
|
495
|
+
0,
|
|
496
|
+
assistantOptions.findIndex((opt) => opt.value === assistantEngine)
|
|
497
|
+
);
|
|
1613
498
|
const resumeOptions = [
|
|
1614
|
-
{ label: "
|
|
1615
|
-
{ label: "
|
|
499
|
+
{ label: "Resume previous session", value: true },
|
|
500
|
+
{ label: "Start new session", value: false },
|
|
1616
501
|
];
|
|
1617
502
|
let selectedResumeIndex = autoResume ? 0 : 1;
|
|
1618
|
-
|
|
503
|
+
const DASH_HINTS = {
|
|
504
|
+
agents: "←/→ select · Enter · ↓ mode · ↑ back",
|
|
505
|
+
agentsEmpty: "↓ mode · ↑ back",
|
|
506
|
+
mode: "←/→ select · Enter · ↓ provider · ↑ back",
|
|
507
|
+
provider: "←/→ select · Enter · ↓ assistant · ↑ back",
|
|
508
|
+
assistant: "←/→ select · Enter · ↓ cron · ↑ back",
|
|
509
|
+
cron: "Ctrl+X close · ↑ back",
|
|
510
|
+
resume: "",
|
|
511
|
+
};
|
|
512
|
+
const AGENT_BAR_HINTS = {
|
|
513
|
+
normal: "↓ agents",
|
|
514
|
+
dashboard: "←/→ · Enter · ↑ · ^X",
|
|
515
|
+
};
|
|
1619
516
|
|
|
1620
|
-
function
|
|
1621
|
-
return
|
|
517
|
+
function getCurrentView() {
|
|
518
|
+
return agentViewController ? agentViewController.getCurrentView() : "main";
|
|
1622
519
|
}
|
|
1623
520
|
|
|
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;
|
|
521
|
+
function getViewingAgent() {
|
|
522
|
+
return agentViewController ? agentViewController.getViewingAgent() : "";
|
|
1640
523
|
}
|
|
1641
524
|
|
|
1642
|
-
function
|
|
1643
|
-
if (!
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
}
|
|
1648
|
-
client.write(`${JSON.stringify(req)}\n`);
|
|
525
|
+
function getAgentAdapter(agentId) {
|
|
526
|
+
if (!terminalAdapterRouter) return null;
|
|
527
|
+
const meta = activeAgentMetaMap ? activeAgentMetaMap.get(agentId) : null;
|
|
528
|
+
const agentLaunchMode = (meta && meta.launch_mode) || launchMode || "";
|
|
529
|
+
return terminalAdapterRouter.getAdapter({ launchMode: agentLaunchMode, agentId });
|
|
1649
530
|
}
|
|
1650
531
|
|
|
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();
|
|
532
|
+
function getViewingAgentAdapter() {
|
|
533
|
+
const viewingAgent = getViewingAgent();
|
|
534
|
+
if (!viewingAgent) return null;
|
|
535
|
+
return getAgentAdapter(viewingAgent);
|
|
1666
536
|
}
|
|
1667
537
|
|
|
1668
|
-
function
|
|
1669
|
-
|
|
1670
|
-
|
|
538
|
+
function canSendRaw(adapter) {
|
|
539
|
+
if (!adapter || !adapter.capabilities) return false;
|
|
540
|
+
return Boolean(
|
|
541
|
+
adapter.capabilities.supportsSocketProtocol
|
|
542
|
+
|| adapter.capabilities.supportsInternalQueueLoop
|
|
543
|
+
);
|
|
1671
544
|
}
|
|
1672
545
|
|
|
1673
|
-
function
|
|
1674
|
-
|
|
1675
|
-
screen.program.hideCursor();
|
|
546
|
+
function canResize(adapter) {
|
|
547
|
+
return Boolean(adapter && adapter.capabilities && adapter.capabilities.supportsSocketProtocol);
|
|
1676
548
|
}
|
|
1677
549
|
|
|
1678
|
-
function
|
|
1679
|
-
|
|
1680
|
-
|
|
550
|
+
function canSnapshot(adapter) {
|
|
551
|
+
if (!adapter || !adapter.capabilities) return false;
|
|
552
|
+
return Boolean(
|
|
553
|
+
adapter.capabilities.supportsSnapshot
|
|
554
|
+
|| adapter.capabilities.supportsSubscribeScreen
|
|
555
|
+
|| adapter.capabilities.supportsSubscribeFull
|
|
556
|
+
);
|
|
1681
557
|
}
|
|
1682
558
|
|
|
1683
|
-
function
|
|
1684
|
-
const
|
|
1685
|
-
if (
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
559
|
+
function sendRawWithCapabilities(data) {
|
|
560
|
+
const adapter = getViewingAgentAdapter();
|
|
561
|
+
if (!canSendRaw(adapter)) return;
|
|
562
|
+
try {
|
|
563
|
+
adapter.sendRaw(data);
|
|
564
|
+
} catch {
|
|
565
|
+
// ignore unsupported errors
|
|
1690
566
|
}
|
|
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
567
|
}
|
|
1704
568
|
|
|
1705
|
-
function
|
|
1706
|
-
const
|
|
1707
|
-
|
|
1708
|
-
const historyFile = path.join(agentDir, "ufoo-agent.history.jsonl");
|
|
569
|
+
function sendResizeWithCapabilities(cols, rows) {
|
|
570
|
+
const adapter = getViewingAgentAdapter();
|
|
571
|
+
if (!canResize(adapter)) return;
|
|
1709
572
|
try {
|
|
1710
|
-
|
|
573
|
+
adapter.resize(cols, rows);
|
|
1711
574
|
} catch {
|
|
1712
|
-
// ignore
|
|
575
|
+
// ignore unsupported errors
|
|
1713
576
|
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function requestSnapshotWithCapabilities() {
|
|
580
|
+
const adapter = getViewingAgentAdapter();
|
|
581
|
+
if (!canSnapshot(adapter)) return false;
|
|
1714
582
|
try {
|
|
1715
|
-
|
|
583
|
+
return adapter.snapshot();
|
|
1716
584
|
} catch {
|
|
1717
|
-
|
|
585
|
+
return false;
|
|
1718
586
|
}
|
|
1719
587
|
}
|
|
1720
588
|
|
|
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();
|
|
589
|
+
function isAgentViewUsesBus() {
|
|
590
|
+
return agentViewController ? agentViewController.isAgentViewUsesBus() : false;
|
|
1732
591
|
}
|
|
1733
592
|
|
|
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();
|
|
593
|
+
function getAgentInputSuppressUntil() {
|
|
594
|
+
return agentViewController ? agentViewController.getAgentInputSuppressUntil() : 0;
|
|
1744
595
|
}
|
|
1745
596
|
|
|
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;
|
|
597
|
+
function getAgentOutputSuppressed() {
|
|
598
|
+
return agentViewController ? agentViewController.getAgentOutputSuppressed() : false;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function setAgentOutputSuppressed(value) {
|
|
602
|
+
if (agentViewController) {
|
|
603
|
+
agentViewController.setAgentOutputSuppressed(value);
|
|
1770
604
|
}
|
|
1771
605
|
}
|
|
1772
606
|
|
|
1773
|
-
function
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
logBox.scrollTo(0);
|
|
607
|
+
function renderAgentDashboard() {
|
|
608
|
+
if (agentViewController) {
|
|
609
|
+
agentViewController.renderAgentDashboard();
|
|
1777
610
|
}
|
|
1778
|
-
screen.render();
|
|
1779
611
|
}
|
|
1780
612
|
|
|
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}`;
|
|
613
|
+
function setAgentBarVisible(visible) {
|
|
614
|
+
if (agentViewController) {
|
|
615
|
+
agentViewController.setAgentBarVisible(visible);
|
|
1857
616
|
}
|
|
1858
|
-
dashboard.setContent(content);
|
|
1859
617
|
}
|
|
1860
618
|
|
|
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
|
-
}
|
|
1878
|
-
}
|
|
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
|
-
}
|
|
619
|
+
function enterAgentView(agentId, options = {}) {
|
|
620
|
+
if (agentViewController) {
|
|
621
|
+
agentViewController.enterAgentView(agentId, options);
|
|
1919
622
|
}
|
|
1920
|
-
renderDashboard();
|
|
1921
|
-
screen.render();
|
|
1922
623
|
}
|
|
1923
624
|
|
|
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();
|
|
625
|
+
function exitAgentView() {
|
|
626
|
+
if (agentViewController) {
|
|
627
|
+
agentViewController.exitAgentView();
|
|
1937
628
|
}
|
|
1938
|
-
screen.grabKeys = true;
|
|
1939
|
-
renderDashboard();
|
|
1940
|
-
screen.program.hideCursor();
|
|
1941
|
-
screen.render();
|
|
1942
629
|
}
|
|
1943
630
|
|
|
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;
|
|
631
|
+
function sendRawToAgent(data) {
|
|
632
|
+
if (agentViewController) {
|
|
633
|
+
agentViewController.sendRawToAgent(data);
|
|
2218
634
|
}
|
|
2219
|
-
return false;
|
|
2220
635
|
}
|
|
2221
636
|
|
|
2222
|
-
function
|
|
2223
|
-
if (
|
|
2224
|
-
|
|
2225
|
-
updatePromptBox();
|
|
637
|
+
function sendResizeToAgent(cols, rows) {
|
|
638
|
+
if (agentViewController) {
|
|
639
|
+
agentViewController.sendResizeToAgent(cols, rows);
|
|
2226
640
|
}
|
|
2227
|
-
focusMode = "input";
|
|
2228
|
-
dashboardView = "agents";
|
|
2229
|
-
selectedAgentIndex = -1;
|
|
2230
|
-
screen.grabKeys = false;
|
|
2231
|
-
renderDashboard();
|
|
2232
|
-
focusInput();
|
|
2233
|
-
screen.render();
|
|
2234
|
-
}
|
|
2235
|
-
|
|
2236
|
-
function clearTargetAgent() {
|
|
2237
|
-
targetAgent = null;
|
|
2238
|
-
updatePromptBox();
|
|
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
641
|
}
|
|
2246
642
|
|
|
2247
|
-
function
|
|
2248
|
-
|
|
2249
|
-
|
|
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}`);
|
|
643
|
+
function requestAgentSnapshot() {
|
|
644
|
+
if (agentViewController) {
|
|
645
|
+
agentViewController.requestAgentSnapshot();
|
|
2279
646
|
}
|
|
2280
647
|
}
|
|
2281
648
|
|
|
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`;
|
|
649
|
+
function writeToAgentTerm(text) {
|
|
650
|
+
if (agentViewController) {
|
|
651
|
+
agentViewController.writeToAgentTerm(text);
|
|
2319
652
|
}
|
|
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
653
|
}
|
|
2327
654
|
|
|
2328
|
-
function
|
|
2329
|
-
if (
|
|
2330
|
-
|
|
2331
|
-
disconnectAgentOutput();
|
|
2332
|
-
disconnectAgentInput();
|
|
655
|
+
function placeAgentCursor(cursor) {
|
|
656
|
+
if (agentViewController) {
|
|
657
|
+
agentViewController.placeAgentCursor(cursor);
|
|
2333
658
|
}
|
|
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
659
|
}
|
|
2368
660
|
|
|
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();
|
|
661
|
+
function handleResizeInAgentView() {
|
|
662
|
+
if (!agentViewController) return false;
|
|
663
|
+
return agentViewController.handleResizeInAgentView();
|
|
2405
664
|
}
|
|
2406
665
|
|
|
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
|
-
});
|
|
666
|
+
function getAgentLabel(agentId) {
|
|
667
|
+
return agentDirectory.getAgentLabel(activeAgentLabelMap, agentId);
|
|
668
|
+
}
|
|
2435
669
|
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
writeToAgentTerm(msg.data);
|
|
2448
|
-
}
|
|
2449
|
-
}
|
|
2450
|
-
} catch {
|
|
2451
|
-
// ignore malformed messages
|
|
670
|
+
function resolveAgentId(label) {
|
|
671
|
+
return agentDirectory.resolveAgentId({
|
|
672
|
+
label,
|
|
673
|
+
activeAgents,
|
|
674
|
+
labelMap: activeAgentLabelMap,
|
|
675
|
+
lookupNickname: (nickname) => {
|
|
676
|
+
try {
|
|
677
|
+
const busPath = getUfooPaths(projectRoot).agentsFile;
|
|
678
|
+
const bus = JSON.parse(fs.readFileSync(busPath, "utf8"));
|
|
679
|
+
for (const [id, meta] of Object.entries(bus.agents || {})) {
|
|
680
|
+
if (meta && meta.nickname === nickname) return id;
|
|
2452
681
|
}
|
|
682
|
+
} catch {
|
|
683
|
+
// ignore lookup errors
|
|
2453
684
|
}
|
|
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
|
-
}
|
|
685
|
+
return null;
|
|
686
|
+
},
|
|
687
|
+
});
|
|
2509
688
|
}
|
|
2510
689
|
|
|
2511
|
-
function
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
690
|
+
function resolveAgentDisplayName(publisher) {
|
|
691
|
+
return agentDirectory.resolveAgentDisplayName({
|
|
692
|
+
publisher,
|
|
693
|
+
labelMap: activeAgentLabelMap,
|
|
694
|
+
lookupNicknameById: (id) => {
|
|
695
|
+
try {
|
|
696
|
+
const busPath = getUfooPaths(projectRoot).agentsFile;
|
|
697
|
+
const bus = JSON.parse(fs.readFileSync(busPath, "utf8"));
|
|
698
|
+
const meta = bus.agents && bus.agents[id];
|
|
699
|
+
if (meta && meta.nickname) return meta.nickname;
|
|
700
|
+
} catch {
|
|
701
|
+
// Keep original publisher ID
|
|
702
|
+
}
|
|
703
|
+
return null;
|
|
704
|
+
},
|
|
705
|
+
});
|
|
2518
706
|
}
|
|
2519
707
|
|
|
2520
|
-
function
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
}
|
|
708
|
+
function clampAgentWindowWithSelection(selectionIndex) {
|
|
709
|
+
agentListWindowStart = agentDirectory.clampAgentWindowWithSelection({
|
|
710
|
+
activeCount: activeAgents.length,
|
|
711
|
+
maxWindow: MAX_AGENT_WINDOW,
|
|
712
|
+
windowStart: agentListWindowStart,
|
|
713
|
+
selectionIndex,
|
|
714
|
+
});
|
|
2527
715
|
}
|
|
2528
716
|
|
|
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
|
-
}
|
|
717
|
+
function clampAgentWindow() {
|
|
718
|
+
clampAgentWindowWithSelection(selectedAgentIndex);
|
|
2546
719
|
}
|
|
2547
720
|
|
|
2548
|
-
function
|
|
2549
|
-
|
|
721
|
+
function send(req) {
|
|
722
|
+
if (!daemonCoordinator) return;
|
|
723
|
+
daemonCoordinator.send(req);
|
|
2550
724
|
}
|
|
2551
725
|
|
|
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
|
-
}
|
|
726
|
+
cronScheduler = createCronScheduler({
|
|
727
|
+
dispatch: ({ taskId, target, message }) => {
|
|
728
|
+
send({
|
|
729
|
+
type: IPC_REQUEST_TYPES.BUS_SEND,
|
|
730
|
+
target,
|
|
731
|
+
message,
|
|
732
|
+
});
|
|
733
|
+
queueStatusLine(`cron:${taskId} -> ${target}`);
|
|
734
|
+
},
|
|
735
|
+
onChange: () => {
|
|
736
|
+
renderDashboard();
|
|
737
|
+
screen.render();
|
|
738
|
+
},
|
|
2702
739
|
});
|
|
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
|
-
|
|
2725
|
-
// Capture console output safely
|
|
2726
|
-
const originalLog = console.log;
|
|
2727
|
-
const originalError = console.error;
|
|
2728
740
|
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
} finally {
|
|
2747
|
-
console.log = originalLog;
|
|
2748
|
-
console.error = originalError;
|
|
741
|
+
function updatePromptBox() {
|
|
742
|
+
if (targetAgent) {
|
|
743
|
+
const label = getAgentLabel(targetAgent);
|
|
744
|
+
promptBox.setContent(`>@${label}`);
|
|
745
|
+
promptBox.width = label.length + 3; // >@name + spacer
|
|
746
|
+
input.left = promptBox.width;
|
|
747
|
+
input.width = `100%-${promptBox.width}`;
|
|
748
|
+
} else {
|
|
749
|
+
promptBox.setContent(">");
|
|
750
|
+
promptBox.width = 2;
|
|
751
|
+
input.left = 2;
|
|
752
|
+
input.width = "100%-2";
|
|
753
|
+
}
|
|
754
|
+
if (!input.parent || !promptBox.parent) return;
|
|
755
|
+
resizeInput();
|
|
756
|
+
if (typeof input._updateCursor === "function") {
|
|
757
|
+
input._updateCursor();
|
|
2749
758
|
}
|
|
2750
759
|
}
|
|
2751
760
|
|
|
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}`);
|
|
761
|
+
function syncTargetFromSelection() {
|
|
762
|
+
if (focusMode !== "dashboard" || dashboardView !== "agents") return;
|
|
763
|
+
if (selectedAgentIndex >= 0 && selectedAgentIndex < activeAgents.length) {
|
|
764
|
+
const nextTarget = activeAgents[selectedAgentIndex];
|
|
765
|
+
if (nextTarget !== targetAgent) {
|
|
766
|
+
targetAgent = nextTarget;
|
|
767
|
+
updatePromptBox();
|
|
768
|
+
screen.render();
|
|
2763
769
|
}
|
|
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");
|
|
770
|
+
} else if (targetAgent) {
|
|
771
|
+
targetAgent = null;
|
|
772
|
+
updatePromptBox();
|
|
773
|
+
screen.render();
|
|
2771
774
|
}
|
|
2772
775
|
}
|
|
2773
776
|
|
|
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");
|
|
777
|
+
function restoreTargetFromSelection() {
|
|
778
|
+
if (selectedAgentIndex >= 0 && selectedAgentIndex < activeAgents.length) {
|
|
779
|
+
targetAgent = activeAgents[selectedAgentIndex];
|
|
780
|
+
updatePromptBox();
|
|
2810
781
|
}
|
|
2811
782
|
}
|
|
2812
783
|
|
|
2813
|
-
|
|
2814
|
-
|
|
784
|
+
function focusInput() {
|
|
785
|
+
input.focus();
|
|
786
|
+
input._updateCursor();
|
|
787
|
+
}
|
|
2815
788
|
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
789
|
+
function focusLog() {
|
|
790
|
+
logBox.focus();
|
|
791
|
+
screen.program.hideCursor();
|
|
792
|
+
}
|
|
2820
793
|
|
|
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
|
-
};
|
|
794
|
+
function scrollLog(offset) {
|
|
795
|
+
logBox.scroll(offset);
|
|
796
|
+
screen.render();
|
|
797
|
+
}
|
|
2832
798
|
|
|
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 });
|
|
799
|
+
let settingsController = null;
|
|
2838
800
|
|
|
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;
|
|
801
|
+
function setLaunchMode(mode) {
|
|
802
|
+
if (settingsController) {
|
|
803
|
+
settingsController.setLaunchMode(mode);
|
|
2850
804
|
}
|
|
2851
805
|
}
|
|
2852
806
|
|
|
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}`);
|
|
807
|
+
function requestCloseAgent(agentId) {
|
|
808
|
+
if (!agentId) {
|
|
809
|
+
logMessage("error", "{white-fg}✗{/white-fg} No agent selected");
|
|
810
|
+
return;
|
|
2916
811
|
}
|
|
812
|
+
const label = getAgentLabel(agentId);
|
|
813
|
+
logMessage("status", `{white-fg}⚙{/white-fg} Closing ${label}...`);
|
|
814
|
+
send({ type: IPC_REQUEST_TYPES.CLOSE_AGENT, agent_id: agentId });
|
|
2917
815
|
}
|
|
2918
816
|
|
|
2919
|
-
|
|
2920
|
-
|
|
817
|
+
function setAgentProvider(provider) {
|
|
818
|
+
if (settingsController) {
|
|
819
|
+
settingsController.setAgentProvider(provider);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
2921
822
|
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
823
|
+
function setAssistantEngine(value) {
|
|
824
|
+
if (settingsController) {
|
|
825
|
+
settingsController.setAssistantEngine(value);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
2925
828
|
|
|
2926
|
-
|
|
2927
|
-
|
|
829
|
+
function setAutoResume(value) {
|
|
830
|
+
if (settingsController) {
|
|
831
|
+
settingsController.setAutoResume(value);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
2928
834
|
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
835
|
+
async function restartDaemon() {
|
|
836
|
+
if (!daemonCoordinator) return;
|
|
837
|
+
return daemonCoordinator.restart();
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
settingsController = createSettingsController({
|
|
841
|
+
projectRoot,
|
|
842
|
+
saveConfig,
|
|
843
|
+
normalizeLaunchMode,
|
|
844
|
+
normalizeAgentProvider,
|
|
845
|
+
normalizeAssistantEngine,
|
|
846
|
+
fsModule: fs,
|
|
847
|
+
getUfooPaths,
|
|
848
|
+
logMessage,
|
|
849
|
+
renderDashboard,
|
|
850
|
+
renderScreen: () => screen.render(),
|
|
851
|
+
restartDaemon,
|
|
852
|
+
getLaunchMode: () => launchMode,
|
|
853
|
+
setLaunchModeState: (value) => {
|
|
854
|
+
launchMode = value;
|
|
855
|
+
},
|
|
856
|
+
setSelectedModeIndex: (value) => {
|
|
857
|
+
selectedModeIndex = value;
|
|
858
|
+
},
|
|
859
|
+
getAgentProvider: () => agentProvider,
|
|
860
|
+
setAgentProviderState: (value) => {
|
|
861
|
+
agentProvider = value;
|
|
862
|
+
},
|
|
863
|
+
setSelectedProviderIndex: (value) => {
|
|
864
|
+
selectedProviderIndex = value;
|
|
865
|
+
},
|
|
866
|
+
getAssistantEngine: () => assistantEngine,
|
|
867
|
+
setAssistantEngineState: (value) => {
|
|
868
|
+
assistantEngine = value;
|
|
869
|
+
},
|
|
870
|
+
setSelectedAssistantIndex: (value) => {
|
|
871
|
+
selectedAssistantIndex = value;
|
|
872
|
+
},
|
|
873
|
+
assistantOptions,
|
|
874
|
+
getAutoResume: () => autoResume,
|
|
875
|
+
setAutoResumeState: (value) => {
|
|
876
|
+
autoResume = value;
|
|
877
|
+
},
|
|
878
|
+
setSelectedResumeIndex: (value) => {
|
|
879
|
+
selectedResumeIndex = value;
|
|
880
|
+
},
|
|
881
|
+
});
|
|
2940
882
|
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
} finally {
|
|
2946
|
-
console.log = originalLog;
|
|
2947
|
-
console.error = originalError;
|
|
883
|
+
function clearLog() {
|
|
884
|
+
logBox.setContent("");
|
|
885
|
+
if (typeof logBox.scrollTo === "function") {
|
|
886
|
+
logBox.scrollTo(0);
|
|
2948
887
|
}
|
|
888
|
+
screen.render();
|
|
2949
889
|
}
|
|
2950
890
|
|
|
2951
|
-
|
|
2952
|
-
const
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
891
|
+
function renderDashboard() {
|
|
892
|
+
const computed = computeDashboardContent({
|
|
893
|
+
focusMode,
|
|
894
|
+
dashboardView,
|
|
895
|
+
activeAgents,
|
|
896
|
+
selectedAgentIndex,
|
|
897
|
+
agentListWindowStart,
|
|
898
|
+
maxAgentWindow: MAX_AGENT_WINDOW,
|
|
899
|
+
getAgentLabel,
|
|
900
|
+
launchMode,
|
|
901
|
+
agentProvider,
|
|
902
|
+
assistantEngine,
|
|
903
|
+
autoResume,
|
|
904
|
+
selectedModeIndex,
|
|
905
|
+
selectedProviderIndex,
|
|
906
|
+
selectedAssistantIndex,
|
|
907
|
+
selectedResumeIndex,
|
|
908
|
+
cronTasks: cronScheduler.listTasks(),
|
|
909
|
+
providerOptions,
|
|
910
|
+
assistantOptions,
|
|
911
|
+
resumeOptions,
|
|
912
|
+
pendingReports: reportPendingTotal,
|
|
913
|
+
dashHints: DASH_HINTS,
|
|
914
|
+
});
|
|
915
|
+
agentListWindowStart = computed.windowStart;
|
|
916
|
+
dashboard.setContent(computed.content);
|
|
917
|
+
}
|
|
2957
918
|
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
919
|
+
function updateDashboard(status) {
|
|
920
|
+
activeAgents = status.active || [];
|
|
921
|
+
reportPendingTotal = Number.isFinite(status?.reports?.pending_total)
|
|
922
|
+
? status.reports.pending_total
|
|
923
|
+
: 0;
|
|
924
|
+
const metaList = Array.isArray(status.active_meta) ? status.active_meta : [];
|
|
925
|
+
let fallbackMap = null;
|
|
926
|
+
if (metaList.length === 0 && activeAgents.length > 0) {
|
|
927
|
+
try {
|
|
928
|
+
const busPath = getUfooPaths(projectRoot).agentsFile;
|
|
929
|
+
const bus = JSON.parse(fs.readFileSync(busPath, "utf8"));
|
|
930
|
+
fallbackMap = new Map();
|
|
931
|
+
for (const [id, meta] of Object.entries(bus.agents || {})) {
|
|
932
|
+
if (meta && meta.nickname) fallbackMap.set(id, meta.nickname);
|
|
2971
933
|
}
|
|
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");
|
|
934
|
+
} catch {
|
|
935
|
+
fallbackMap = null;
|
|
2979
936
|
}
|
|
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
937
|
}
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
938
|
+
const maps = agentDirectory.buildAgentMaps(activeAgents, metaList, fallbackMap);
|
|
939
|
+
activeAgentLabelMap = maps.labelMap;
|
|
940
|
+
activeAgentMetaMap = maps.metaMap;
|
|
941
|
+
clampAgentWindow();
|
|
942
|
+
// If viewing agent went offline, exit view
|
|
943
|
+
const currentView = getCurrentView();
|
|
944
|
+
const viewingAgent = getViewingAgent();
|
|
945
|
+
if (currentView === "agent" && viewingAgent && !activeAgents.includes(viewingAgent)) {
|
|
946
|
+
writeToAgentTerm("\r\n\x1b[1;31m[Agent went offline]\x1b[0m\r\n");
|
|
947
|
+
exitAgentView();
|
|
2993
948
|
return;
|
|
2994
949
|
}
|
|
2995
950
|
|
|
2996
|
-
|
|
2997
|
-
if (
|
|
2998
|
-
|
|
951
|
+
// In agent view, only update the dashboard bar (blessed is frozen)
|
|
952
|
+
if (currentView === "agent") {
|
|
953
|
+
if (focusMode === "dashboard") {
|
|
954
|
+
const totalItems = 1 + activeAgents.length;
|
|
955
|
+
if (selectedAgentIndex < 0 || selectedAgentIndex >= totalItems) {
|
|
956
|
+
selectedAgentIndex = 0;
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
renderAgentDashboard();
|
|
2999
960
|
return;
|
|
3000
961
|
}
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
962
|
+
if (focusMode === "dashboard") {
|
|
963
|
+
if (dashboardView === "agents") {
|
|
964
|
+
if (activeAgents.length === 0) {
|
|
965
|
+
selectedAgentIndex = -1;
|
|
966
|
+
} else if (selectedAgentIndex < 0 || selectedAgentIndex >= activeAgents.length) {
|
|
967
|
+
selectedAgentIndex = 0;
|
|
968
|
+
}
|
|
969
|
+
clampAgentWindow();
|
|
3009
970
|
}
|
|
3010
971
|
}
|
|
972
|
+
syncTargetFromSelection();
|
|
973
|
+
renderDashboard();
|
|
974
|
+
screen.render();
|
|
975
|
+
}
|
|
3011
976
|
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
977
|
+
function enterDashboardMode() {
|
|
978
|
+
focusMode = "dashboard";
|
|
979
|
+
dashboardView = "agents";
|
|
980
|
+
selectedAgentIndex = activeAgents.length > 0 ? 0 : -1;
|
|
981
|
+
agentListWindowStart = 0;
|
|
982
|
+
clampAgentWindow();
|
|
983
|
+
selectedModeIndex = launchMode === "internal" ? 2 : (launchMode === "tmux" ? 1 : 0);
|
|
984
|
+
selectedProviderIndex = agentProvider === "claude-cli" ? 1 : 0;
|
|
985
|
+
selectedAssistantIndex = Math.max(
|
|
986
|
+
0,
|
|
987
|
+
assistantOptions.findIndex((opt) => opt.value === assistantEngine)
|
|
988
|
+
);
|
|
989
|
+
selectedResumeIndex = autoResume ? 0 : 1;
|
|
990
|
+
// Immediately set @target when first agent is selected
|
|
991
|
+
if (selectedAgentIndex >= 0 && selectedAgentIndex < activeAgents.length) {
|
|
992
|
+
targetAgent = activeAgents[selectedAgentIndex];
|
|
993
|
+
updatePromptBox();
|
|
3017
994
|
}
|
|
995
|
+
screen.grabKeys = true;
|
|
996
|
+
renderDashboard();
|
|
997
|
+
screen.program.hideCursor();
|
|
998
|
+
screen.render();
|
|
999
|
+
syncTargetFromSelection();
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
const dashboardState = {};
|
|
1003
|
+
Object.defineProperties(dashboardState, {
|
|
1004
|
+
currentView: { get: () => getCurrentView() },
|
|
1005
|
+
focusMode: { get: () => focusMode, set: (value) => { focusMode = value; } },
|
|
1006
|
+
dashboardView: { get: () => dashboardView, set: (value) => { dashboardView = value; } },
|
|
1007
|
+
selectedAgentIndex: { get: () => selectedAgentIndex, set: (value) => { selectedAgentIndex = value; } },
|
|
1008
|
+
activeAgents: { get: () => activeAgents },
|
|
1009
|
+
viewingAgent: { get: () => getViewingAgent() },
|
|
1010
|
+
activeAgentMetaMap: { get: () => activeAgentMetaMap },
|
|
1011
|
+
selectedModeIndex: { get: () => selectedModeIndex, set: (value) => { selectedModeIndex = value; } },
|
|
1012
|
+
selectedProviderIndex: { get: () => selectedProviderIndex, set: (value) => { selectedProviderIndex = value; } },
|
|
1013
|
+
selectedAssistantIndex: { get: () => selectedAssistantIndex, set: (value) => { selectedAssistantIndex = value; } },
|
|
1014
|
+
selectedResumeIndex: { get: () => selectedResumeIndex, set: (value) => { selectedResumeIndex = value; } },
|
|
1015
|
+
launchMode: { get: () => launchMode },
|
|
1016
|
+
agentProvider: { get: () => agentProvider },
|
|
1017
|
+
assistantEngine: { get: () => assistantEngine },
|
|
1018
|
+
autoResume: { get: () => autoResume },
|
|
1019
|
+
cronTasks: { get: () => cronScheduler.listTasks() },
|
|
1020
|
+
providerOptions: { get: () => providerOptions },
|
|
1021
|
+
assistantOptions: { get: () => assistantOptions },
|
|
1022
|
+
resumeOptions: { get: () => resumeOptions },
|
|
1023
|
+
agentOutputSuppressed: {
|
|
1024
|
+
get: () => getAgentOutputSuppressed(),
|
|
1025
|
+
set: (value) => { setAgentOutputSuppressed(value); },
|
|
1026
|
+
},
|
|
1027
|
+
});
|
|
3018
1028
|
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
1029
|
+
function activateAgent(agentId) {
|
|
1030
|
+
if (!agentId) return;
|
|
1031
|
+
const activator = new AgentActivator(projectRoot);
|
|
1032
|
+
activator.activate(agentId).catch(() => {});
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
terminalAdapterRouter = createTerminalAdapterRouter({
|
|
1036
|
+
activateAgent,
|
|
1037
|
+
sendRaw: (data) => agentSockets.sendRaw(data),
|
|
1038
|
+
sendResize: (cols, rows) => agentSockets.sendResize(cols, rows),
|
|
1039
|
+
requestSnapshot: (mode) => agentSockets.requestSnapshot(mode),
|
|
1040
|
+
});
|
|
1041
|
+
|
|
1042
|
+
const dashboardController = createDashboardKeyController({
|
|
1043
|
+
state: dashboardState,
|
|
1044
|
+
existsSync: fs.existsSync,
|
|
1045
|
+
getInjectSockPath,
|
|
1046
|
+
getAgentAdapter,
|
|
1047
|
+
activateAgent,
|
|
1048
|
+
requestCloseAgent,
|
|
1049
|
+
enterAgentView,
|
|
1050
|
+
exitAgentView,
|
|
1051
|
+
setAgentBarVisible,
|
|
1052
|
+
requestAgentSnapshot,
|
|
1053
|
+
clearTargetAgent,
|
|
1054
|
+
restoreTargetFromSelection,
|
|
1055
|
+
syncTargetFromSelection,
|
|
1056
|
+
exitDashboardMode,
|
|
1057
|
+
setLaunchMode,
|
|
1058
|
+
setAgentProvider,
|
|
1059
|
+
setAssistantEngine,
|
|
1060
|
+
setAutoResume,
|
|
1061
|
+
clampAgentWindow,
|
|
1062
|
+
clampAgentWindowWithSelection,
|
|
1063
|
+
renderDashboard,
|
|
1064
|
+
renderAgentDashboard,
|
|
1065
|
+
renderScreen: () => screen.render(),
|
|
1066
|
+
setScreenGrabKeys: (value) => {
|
|
1067
|
+
screen.grabKeys = Boolean(value);
|
|
1068
|
+
},
|
|
1069
|
+
});
|
|
1070
|
+
|
|
1071
|
+
function handleDashboardKey(key) {
|
|
1072
|
+
return dashboardController.handleDashboardKey(key);
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
function exitDashboardMode(selectAgent = false) {
|
|
1076
|
+
if (selectAgent && selectedAgentIndex >= 0 && selectedAgentIndex < activeAgents.length) {
|
|
1077
|
+
targetAgent = activeAgents[selectedAgentIndex];
|
|
1078
|
+
updatePromptBox();
|
|
3031
1079
|
}
|
|
1080
|
+
focusMode = "input";
|
|
1081
|
+
dashboardView = "agents";
|
|
1082
|
+
selectedAgentIndex = -1;
|
|
1083
|
+
screen.grabKeys = false;
|
|
1084
|
+
renderDashboard();
|
|
1085
|
+
focusInput();
|
|
1086
|
+
screen.render();
|
|
3032
1087
|
}
|
|
3033
1088
|
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
send({ type: "resume_agents", target });
|
|
3039
|
-
setTimeout(requestStatus, 1000);
|
|
1089
|
+
function clearTargetAgent() {
|
|
1090
|
+
targetAgent = null;
|
|
1091
|
+
updatePromptBox();
|
|
1092
|
+
screen.render();
|
|
3040
1093
|
}
|
|
3041
1094
|
|
|
3042
|
-
function
|
|
3043
|
-
|
|
1095
|
+
function getInjectSockPath(agentId) {
|
|
1096
|
+
const safeName = subscriberToSafeName(agentId);
|
|
1097
|
+
return path.join(getUfooPaths(projectRoot).busQueuesDir, safeName, "inject.sock");
|
|
1098
|
+
}
|
|
3044
1099
|
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
1100
|
+
agentViewController = createAgentViewController({
|
|
1101
|
+
screen,
|
|
1102
|
+
input,
|
|
1103
|
+
processStdout: process.stdout,
|
|
1104
|
+
computeAgentBar,
|
|
1105
|
+
agentBarHints: AGENT_BAR_HINTS,
|
|
1106
|
+
maxAgentWindow: MAX_AGENT_WINDOW,
|
|
1107
|
+
getFocusMode: () => focusMode,
|
|
1108
|
+
setFocusMode: (value) => {
|
|
1109
|
+
focusMode = value;
|
|
1110
|
+
},
|
|
1111
|
+
getSelectedAgentIndex: () => selectedAgentIndex,
|
|
1112
|
+
setSelectedAgentIndex: (value) => {
|
|
1113
|
+
selectedAgentIndex = value;
|
|
1114
|
+
},
|
|
1115
|
+
getActiveAgents: () => activeAgents,
|
|
1116
|
+
getAgentListWindowStart: () => agentListWindowStart,
|
|
1117
|
+
setAgentListWindowStart: (value) => {
|
|
1118
|
+
agentListWindowStart = value;
|
|
1119
|
+
},
|
|
1120
|
+
getAgentLabel,
|
|
1121
|
+
setDashboardView: (value) => {
|
|
1122
|
+
dashboardView = value;
|
|
1123
|
+
},
|
|
1124
|
+
setScreenGrabKeys: (value) => {
|
|
1125
|
+
screen.grabKeys = Boolean(value);
|
|
1126
|
+
},
|
|
1127
|
+
clearTargetAgent,
|
|
1128
|
+
renderDashboard,
|
|
1129
|
+
focusInput,
|
|
1130
|
+
resizeInput,
|
|
1131
|
+
renderScreen: () => screen.render(),
|
|
1132
|
+
getInjectSockPath,
|
|
1133
|
+
connectAgentOutput: (sockPath) => {
|
|
1134
|
+
agentSockets.connectOutput(sockPath);
|
|
1135
|
+
},
|
|
1136
|
+
disconnectAgentOutput: () => {
|
|
1137
|
+
agentSockets.disconnectOutput();
|
|
1138
|
+
},
|
|
1139
|
+
connectAgentInput: (sockPath) => {
|
|
1140
|
+
agentSockets.connectInput(sockPath);
|
|
1141
|
+
},
|
|
1142
|
+
disconnectAgentInput: () => {
|
|
1143
|
+
agentSockets.disconnectInput();
|
|
1144
|
+
},
|
|
1145
|
+
sendRaw: (data) => {
|
|
1146
|
+
sendRawWithCapabilities(data);
|
|
1147
|
+
},
|
|
1148
|
+
sendResize: (cols, rows) => {
|
|
1149
|
+
sendResizeWithCapabilities(cols, rows);
|
|
1150
|
+
},
|
|
1151
|
+
requestScreenSnapshot: () => {
|
|
1152
|
+
requestSnapshotWithCapabilities();
|
|
1153
|
+
},
|
|
1154
|
+
});
|
|
3048
1155
|
|
|
3049
|
-
|
|
3050
|
-
|
|
1156
|
+
function requestStatus() {
|
|
1157
|
+
if (!daemonCoordinator) return;
|
|
1158
|
+
daemonCoordinator.requestStatus();
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
const daemonMessageRouter = createDaemonMessageRouter({
|
|
1162
|
+
escapeBlessed,
|
|
1163
|
+
stripBlessedTags,
|
|
1164
|
+
logMessage,
|
|
1165
|
+
renderScreen: () => screen.render(),
|
|
1166
|
+
updateDashboard,
|
|
1167
|
+
requestStatus,
|
|
1168
|
+
resolveStatusLine,
|
|
1169
|
+
enqueueBusStatus,
|
|
1170
|
+
resolveBusStatus,
|
|
1171
|
+
getPending: () => pending,
|
|
1172
|
+
setPending: (value) => {
|
|
1173
|
+
pending = value;
|
|
1174
|
+
},
|
|
1175
|
+
resolveAgentDisplayName,
|
|
1176
|
+
getCurrentView: () => getCurrentView(),
|
|
1177
|
+
isAgentViewUsesBus: () => isAgentViewUsesBus(),
|
|
1178
|
+
getViewingAgent: () => getViewingAgent(),
|
|
1179
|
+
writeToAgentTerm,
|
|
1180
|
+
consumePendingDelivery,
|
|
1181
|
+
getPendingState,
|
|
1182
|
+
beginStream,
|
|
1183
|
+
appendStreamDelta,
|
|
1184
|
+
finalizeStream,
|
|
1185
|
+
hasStream: (publisher) => streamTracker.hasStream(publisher),
|
|
1186
|
+
});
|
|
3051
1187
|
|
|
3052
|
-
|
|
3053
|
-
|
|
1188
|
+
daemonCoordinator = createDaemonCoordinator({
|
|
1189
|
+
projectRoot,
|
|
1190
|
+
daemonTransport,
|
|
1191
|
+
handleMessage: (msg) => daemonMessageRouter.handleMessage(msg),
|
|
1192
|
+
queueStatusLine,
|
|
1193
|
+
resolveStatusLine,
|
|
1194
|
+
logMessage,
|
|
1195
|
+
stopDaemon,
|
|
1196
|
+
startDaemon,
|
|
1197
|
+
});
|
|
3054
1198
|
|
|
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;
|
|
1199
|
+
const connected = await daemonCoordinator.connect();
|
|
1200
|
+
if (!connected) {
|
|
1201
|
+
// Check if daemon failed to start
|
|
1202
|
+
if (!isRunning(projectRoot)) {
|
|
1203
|
+
const logFile = getUfooPaths(projectRoot).ufooDaemonLog;
|
|
1204
|
+
// eslint-disable-next-line no-console
|
|
1205
|
+
console.error("Failed to start ufoo daemon. Check logs at:", logFile);
|
|
1206
|
+
throw new Error("Daemon failed to start. Check the daemon log for details.");
|
|
3092
1207
|
}
|
|
1208
|
+
throw new Error("Failed to connect to ufoo daemon (timeout). The daemon may still be starting.");
|
|
3093
1209
|
}
|
|
3094
1210
|
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
1211
|
+
const commandExecutor = createCommandExecutor({
|
|
1212
|
+
projectRoot,
|
|
1213
|
+
parseCommand,
|
|
1214
|
+
escapeBlessed,
|
|
1215
|
+
logMessage,
|
|
1216
|
+
renderScreen: () => screen.render(),
|
|
1217
|
+
getActiveAgents: () => activeAgents,
|
|
1218
|
+
getActiveAgentMetaMap: () => activeAgentMetaMap,
|
|
1219
|
+
getAgentLabel,
|
|
1220
|
+
isDaemonRunning: isRunning,
|
|
1221
|
+
startDaemon,
|
|
1222
|
+
stopDaemon,
|
|
1223
|
+
restartDaemon,
|
|
1224
|
+
send,
|
|
1225
|
+
requestStatus,
|
|
1226
|
+
createCronTask: ({ intervalMs, targets, prompt }) =>
|
|
1227
|
+
cronScheduler.addTask({ intervalMs, targets, prompt }),
|
|
1228
|
+
listCronTasks: () => cronScheduler.listTasks(),
|
|
1229
|
+
stopCronTask: (id) => cronScheduler.stopTask(id),
|
|
1230
|
+
activateAgent: async (target) => {
|
|
1231
|
+
const activator = new AgentActivator(projectRoot);
|
|
1232
|
+
await activator.activate(target);
|
|
1233
|
+
},
|
|
1234
|
+
});
|
|
3117
1235
|
|
|
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
|
-
}
|
|
1236
|
+
async function executeCommand(text) {
|
|
1237
|
+
return commandExecutor.executeCommand(text);
|
|
1238
|
+
}
|
|
3152
1239
|
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
}
|
|
1240
|
+
const submitState = {};
|
|
1241
|
+
Object.defineProperties(submitState, {
|
|
1242
|
+
targetAgent: { get: () => targetAgent, set: (value) => { targetAgent = value; } },
|
|
1243
|
+
pending: { get: () => pending, set: (value) => { pending = value; } },
|
|
1244
|
+
activeAgentMetaMap: { get: () => activeAgentMetaMap },
|
|
1245
|
+
});
|
|
3157
1246
|
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
1247
|
+
const inputSubmitHandler = createInputSubmitHandler({
|
|
1248
|
+
state: submitState,
|
|
1249
|
+
parseAtTarget,
|
|
1250
|
+
resolveAgentId,
|
|
1251
|
+
executeCommand,
|
|
1252
|
+
queueStatusLine,
|
|
1253
|
+
send,
|
|
1254
|
+
logMessage,
|
|
1255
|
+
getAgentLabel,
|
|
1256
|
+
escapeBlessed,
|
|
1257
|
+
markPendingDelivery,
|
|
1258
|
+
clearTargetAgent,
|
|
1259
|
+
setTargetAgent: (agentId) => {
|
|
1260
|
+
targetAgent = agentId || null;
|
|
1261
|
+
updatePromptBox();
|
|
1262
|
+
screen.render();
|
|
1263
|
+
},
|
|
1264
|
+
enterAgentView,
|
|
1265
|
+
activateAgent: async (agentId) => {
|
|
1266
|
+
const activator = new AgentActivator(projectRoot);
|
|
1267
|
+
await activator.activate(agentId);
|
|
1268
|
+
},
|
|
1269
|
+
getInjectSockPath,
|
|
1270
|
+
existsSync: fs.existsSync,
|
|
1271
|
+
commitInputHistory: (text) => {
|
|
1272
|
+
if (inputHistoryController) inputHistoryController.commitSubmittedText(text);
|
|
1273
|
+
},
|
|
1274
|
+
focusInput: () => input.focus(),
|
|
1275
|
+
renderScreen: () => screen.render(), // Add renderScreen callback
|
|
1276
|
+
});
|
|
3169
1277
|
|
|
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();
|
|
1278
|
+
input.on("submit", async (value) => {
|
|
1279
|
+
input.clearValue();
|
|
1280
|
+
screen.render(); // Render cleared input
|
|
1281
|
+
await inputSubmitHandler.handleSubmit(value);
|
|
1282
|
+
// No need for second render - handleSubmit now calls renderScreen() internally
|
|
3190
1283
|
});
|
|
3191
1284
|
|
|
3192
1285
|
screen.key(["C-c"], exitHandler);
|
|
3193
1286
|
|
|
3194
1287
|
// Agent TTY view: enter dashboard mode
|
|
3195
1288
|
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;
|
|
1289
|
+
if (agentViewController) {
|
|
1290
|
+
agentViewController.enterAgentDashboardMode();
|
|
3223
1291
|
}
|
|
3224
1292
|
}
|
|
3225
1293
|
|
|
3226
1294
|
// Dashboard navigation - use screen.on to capture even when input is focused
|
|
3227
1295
|
screen.on("keypress", (ch, key) => {
|
|
3228
1296
|
// Agent TTY view: handle keystrokes
|
|
3229
|
-
if (
|
|
1297
|
+
if (getCurrentView() === "agent") {
|
|
3230
1298
|
if (focusMode === "dashboard") {
|
|
3231
1299
|
handleDashboardKey(key);
|
|
3232
1300
|
return;
|
|
3233
1301
|
}
|
|
3234
|
-
// Suppress input briefly after entering agent view
|
|
3235
|
-
|
|
3236
|
-
if (Date.now() < agentInputSuppressUntil) {
|
|
1302
|
+
// Suppress input briefly after entering agent view
|
|
1303
|
+
if (Date.now() < getAgentInputSuppressUntil()) {
|
|
3237
1304
|
return;
|
|
3238
1305
|
}
|
|
3239
1306
|
// Ctrl+C exits entire app
|
|
@@ -3258,7 +1325,7 @@ async function runChat(projectRoot) {
|
|
|
3258
1325
|
});
|
|
3259
1326
|
|
|
3260
1327
|
screen.key(["tab"], () => {
|
|
3261
|
-
if (
|
|
1328
|
+
if (getCurrentView() === "agent") return; // Tab goes to PTY via keypress handler
|
|
3262
1329
|
if (focusMode === "dashboard") {
|
|
3263
1330
|
exitDashboardMode(false);
|
|
3264
1331
|
} else {
|
|
@@ -3267,13 +1334,13 @@ async function runChat(projectRoot) {
|
|
|
3267
1334
|
});
|
|
3268
1335
|
|
|
3269
1336
|
screen.key(["C-k", "M-k"], () => {
|
|
3270
|
-
if (
|
|
1337
|
+
if (getCurrentView() === "agent") return;
|
|
3271
1338
|
clearLog();
|
|
3272
1339
|
});
|
|
3273
1340
|
|
|
3274
1341
|
|
|
3275
1342
|
screen.key(["i", "enter"], () => {
|
|
3276
|
-
if (
|
|
1343
|
+
if (getCurrentView() === "agent") return;
|
|
3277
1344
|
if (focusMode === "dashboard") return;
|
|
3278
1345
|
if (screen.focused === input) return;
|
|
3279
1346
|
focusInput();
|
|
@@ -3292,43 +1359,7 @@ async function runChat(projectRoot) {
|
|
|
3292
1359
|
}
|
|
3293
1360
|
if (screen.program) {
|
|
3294
1361
|
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
|
-
}
|
|
1362
|
+
pasteController.handleProgramData(data);
|
|
3332
1363
|
});
|
|
3333
1364
|
}
|
|
3334
1365
|
loadHistory();
|
|
@@ -3339,24 +1370,19 @@ async function runChat(projectRoot) {
|
|
|
3339
1370
|
|
|
3340
1371
|
// 定期刷新 dashboard 状态(兜底,daemon 会主动推送变化)
|
|
3341
1372
|
setInterval(() => {
|
|
3342
|
-
if (
|
|
1373
|
+
if (daemonCoordinator && daemonCoordinator.isConnected()) {
|
|
3343
1374
|
requestStatus();
|
|
3344
1375
|
}
|
|
3345
1376
|
}, 30000);
|
|
3346
|
-
|
|
3347
1377
|
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();
|
|
1378
|
+
if (handleResizeInAgentView()) {
|
|
3355
1379
|
return;
|
|
3356
1380
|
}
|
|
3357
1381
|
resizeInput();
|
|
3358
|
-
if (
|
|
1382
|
+
if (completionController.isActive()) completionController.hide();
|
|
3359
1383
|
input._updateCursor();
|
|
1384
|
+
// Force recalculate logBox width to match terminal
|
|
1385
|
+
logBox.width = screen.width;
|
|
3360
1386
|
screen.render();
|
|
3361
1387
|
});
|
|
3362
1388
|
screen.render();
|