u-foo 1.0.3 → 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 +110 -11
- package/README.zh-CN.md +9 -7
- package/SKILLS/ufoo/SKILL.md +132 -0
- package/SKILLS/uinit/SKILL.md +78 -0
- package/SKILLS/ustatus/SKILL.md +36 -0
- package/bin/uclaude.js +13 -0
- package/bin/ucode-core.js +15 -0
- package/bin/ucode.js +125 -0
- package/bin/ucodex.js +13 -0
- package/bin/ufoo +9 -31
- package/bin/ufoo-assistant-agent.js +5 -0
- package/bin/ufoo-engine.js +25 -0
- package/bin/ufoo.js +17 -0
- package/modules/AGENTS.template.md +29 -11
- package/modules/bus/README.md +33 -25
- package/modules/bus/SKILLS/ubus/SKILL.md +19 -8
- package/modules/context/README.md +18 -40
- package/modules/context/SKILLS/uctx/SKILL.md +63 -1
- package/modules/online/SKILLS/ufoo-online/SKILL.md +144 -0
- package/package.json +25 -4
- package/scripts/import-pi-mono.js +124 -0
- package/scripts/postinstall.js +30 -0
- package/scripts/sync-claude-skills.sh +21 -0
- package/src/agent/cliRunner.js +554 -33
- package/src/agent/internalRunner.js +150 -56
- package/src/agent/launcher.js +754 -0
- package/src/agent/normalizeOutput.js +1 -1
- package/src/agent/notifier.js +340 -0
- package/src/agent/ptyRunner.js +847 -0
- package/src/agent/ptyWrapper.js +379 -0
- package/src/agent/readyDetector.js +175 -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 +46 -42
- 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 +172 -0
- package/src/bus/daemon.js +436 -0
- package/src/bus/index.js +842 -0
- package/src/bus/inject.js +315 -0
- package/src/bus/message.js +430 -0
- package/src/bus/nickname.js +88 -0
- package/src/bus/queue.js +136 -0
- package/src/bus/shake.js +26 -0
- package/src/bus/store.js +189 -0
- package/src/bus/subscriber.js +312 -0
- package/src/bus/utils.js +363 -0
- 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 +1011 -1392
- 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 +1162 -96
- 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 +56 -3
- package/src/context/decisions.js +324 -0
- package/src/context/doctor.js +183 -0
- package/src/context/index.js +55 -0
- 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 +998 -170
- package/src/daemon/ipcServer.js +99 -0
- package/src/daemon/ops.js +630 -48
- package/src/daemon/promptLoop.js +319 -0
- package/src/daemon/promptRequest.js +101 -0
- package/src/daemon/providerSessions.js +306 -0
- package/src/daemon/reporting.js +90 -0
- package/src/daemon/run.js +31 -1
- package/src/daemon/status.js +48 -8
- package/src/doctor/index.js +50 -0
- package/src/init/index.js +318 -0
- 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/skills/index.js +159 -0
- package/src/status/index.js +285 -0
- 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/terminal/detect.js +64 -0
- package/src/terminal/index.js +8 -0
- package/src/terminal/iterm2.js +126 -0
- package/src/ufoo/agentsStore.js +107 -0
- package/src/ufoo/paths.js +46 -0
- package/src/utils/banner.js +76 -0
- package/bin/uclaude +0 -65
- package/bin/ucodex +0 -65
- package/modules/bus/scripts/bus-alert.sh +0 -185
- package/modules/bus/scripts/bus-listen.sh +0 -117
- package/modules/context/ASSUMPTIONS.md +0 -7
- package/modules/context/CONSTRAINTS.md +0 -7
- package/modules/context/CONTEXT-STRUCTURE.md +0 -49
- package/modules/context/DECISION-PROTOCOL.md +0 -62
- package/modules/context/HANDOFF.md +0 -33
- package/modules/context/RULES.md +0 -15
- package/modules/context/SKILLS/README.md +0 -14
- package/modules/context/SYSTEM.md +0 -18
- package/modules/context/TEMPLATES/assumptions.md +0 -4
- package/modules/context/TEMPLATES/constraints.md +0 -4
- package/modules/context/TEMPLATES/decision.md +0 -16
- package/modules/context/TEMPLATES/project-context-readme.md +0 -6
- package/modules/context/TEMPLATES/system.md +0 -3
- package/modules/context/TEMPLATES/terminology.md +0 -4
- package/modules/context/TERMINOLOGY.md +0 -10
- package/scripts/banner.sh +0 -89
- package/scripts/bus-alert.sh +0 -6
- package/scripts/bus-autotrigger.sh +0 -6
- package/scripts/bus-daemon.sh +0 -231
- package/scripts/bus-inject.sh +0 -144
- package/scripts/bus-listen.sh +0 -6
- package/scripts/bus.sh +0 -984
- package/scripts/context-decisions.sh +0 -167
- package/scripts/context-doctor.sh +0 -72
- package/scripts/context-lint.sh +0 -110
- package/scripts/doctor.sh +0 -22
- package/scripts/init.sh +0 -247
- package/scripts/skills.sh +0 -113
- package/scripts/status.sh +0 -125
package/src/chat/index.js
CHANGED
|
@@ -1,534 +1,269 @@
|
|
|
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");
|
|
13
|
+
const UfooInit = require("../init");
|
|
14
|
+
const AgentActivator = require("../bus/activate");
|
|
15
|
+
const { subscriberToSafeName } = require("../bus/utils");
|
|
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");
|
|
8
45
|
|
|
9
|
-
function
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function resolveProjectFile(projectRoot, relativePath, fallbackRelativePath) {
|
|
17
|
-
const local = path.join(projectRoot, relativePath);
|
|
18
|
-
if (fs.existsSync(local)) return local;
|
|
19
|
-
return path.join(__dirname, "..", "..", fallbackRelativePath);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function startDaemon(projectRoot) {
|
|
23
|
-
const daemonBin = resolveProjectFile(projectRoot, path.join("bin", "ufoo.js"), path.join("bin", "ufoo.js"));
|
|
24
|
-
const child = spawn(process.execPath, [daemonBin, "daemon", "--start"], {
|
|
25
|
-
detached: true,
|
|
26
|
-
stdio: "ignore",
|
|
27
|
-
cwd: projectRoot,
|
|
28
|
-
});
|
|
29
|
-
child.unref();
|
|
30
|
-
}
|
|
46
|
+
async function runChat(projectRoot) {
|
|
47
|
+
if (!fs.existsSync(getUfooPaths(projectRoot).ufooDir)) {
|
|
48
|
+
const repoRoot = path.join(__dirname, "..", "..");
|
|
49
|
+
const init = new UfooInit(repoRoot);
|
|
50
|
+
await init.init({ modules: "context,bus", project: projectRoot });
|
|
51
|
+
}
|
|
31
52
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
53
|
+
// Ensure subscriber ID exists for chat (persistent across restarts)
|
|
54
|
+
if (!process.env.UFOO_SUBSCRIBER_ID) {
|
|
55
|
+
const crypto = require("crypto");
|
|
56
|
+
const sessionFile = path.join(getUfooPaths(projectRoot).ufooDir, "chat", "session-id.txt");
|
|
57
|
+
const sessionDir = path.dirname(sessionFile);
|
|
58
|
+
fs.mkdirSync(sessionDir, { recursive: true });
|
|
39
59
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
} catch {
|
|
47
|
-
// eslint-disable-next-line no-await-in-loop
|
|
48
|
-
await new Promise((r) => setTimeout(r, delayMs));
|
|
60
|
+
let sessionId;
|
|
61
|
+
if (fs.existsSync(sessionFile)) {
|
|
62
|
+
sessionId = fs.readFileSync(sessionFile, "utf8").trim();
|
|
63
|
+
} else {
|
|
64
|
+
sessionId = crypto.randomBytes(4).toString("hex");
|
|
65
|
+
fs.writeFileSync(sessionFile, sessionId, "utf8");
|
|
49
66
|
}
|
|
67
|
+
// Chat 模式默认使用 claude-code 类型
|
|
68
|
+
process.env.UFOO_SUBSCRIBER_ID = `claude-code:${sessionId}`;
|
|
50
69
|
}
|
|
51
|
-
return null;
|
|
52
|
-
}
|
|
53
70
|
|
|
54
|
-
async function runChat(projectRoot) {
|
|
55
|
-
if (!fs.existsSync(path.join(projectRoot, ".ufoo"))) {
|
|
56
|
-
const initScript = resolveProjectFile(projectRoot, path.join("scripts", "init.sh"), path.join("scripts", "init.sh"));
|
|
57
|
-
spawnSync("bash", [initScript, "--modules", "context,bus", "--project", projectRoot], {
|
|
58
|
-
stdio: "inherit",
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
71
|
if (!isRunning(projectRoot)) {
|
|
62
72
|
startDaemon(projectRoot);
|
|
63
73
|
}
|
|
64
74
|
|
|
65
|
-
const daemonBin = resolveProjectFile(projectRoot, path.join("bin", "ufoo.js"), path.join("bin", "ufoo.js"));
|
|
66
75
|
const sock = socketPath(projectRoot);
|
|
67
|
-
let
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
startDaemon(projectRoot);
|
|
75
|
-
}
|
|
76
|
-
newClient = await connectWithRetry(sock, 50, 200);
|
|
77
|
-
}
|
|
78
|
-
return newClient;
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
client = await connectClient();
|
|
82
|
-
if (!client) {
|
|
83
|
-
// Check if daemon failed to start
|
|
84
|
-
if (!isRunning(projectRoot)) {
|
|
85
|
-
const logFile = path.join(projectRoot, ".ufoo", "run", "ufoo-daemon.log");
|
|
86
|
-
// eslint-disable-next-line no-console
|
|
87
|
-
console.error("Failed to start ufoo daemon. Check logs at:", logFile);
|
|
88
|
-
throw new Error("Daemon failed to start. Check the daemon log for details.");
|
|
89
|
-
}
|
|
90
|
-
throw new Error("Failed to connect to ufoo daemon (timeout). The daemon may still be starting.");
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const screen = blessed.screen({
|
|
94
|
-
smartCSR: true,
|
|
95
|
-
title: "ufoo chat",
|
|
96
|
-
fullUnicode: true,
|
|
97
|
-
// Allow terminal native copy by not fully grabbing mouse
|
|
98
|
-
// Hold Option/Alt to use native selection in most terminals
|
|
99
|
-
sendFocus: true,
|
|
100
|
-
mouse: false,
|
|
101
|
-
// Allow Ctrl+C to exit even when input grabs keys
|
|
102
|
-
ignoreLocked: ["C-c"],
|
|
76
|
+
let daemonCoordinator = null;
|
|
77
|
+
const daemonTransport = createDaemonTransport({
|
|
78
|
+
projectRoot,
|
|
79
|
+
sockPath: sock,
|
|
80
|
+
isRunning,
|
|
81
|
+
startDaemon,
|
|
82
|
+
connectWithRetry,
|
|
103
83
|
});
|
|
104
84
|
|
|
105
85
|
const config = loadConfig(projectRoot);
|
|
106
86
|
let launchMode = config.launchMode;
|
|
107
87
|
let agentProvider = config.agentProvider;
|
|
88
|
+
let assistantEngine = normalizeAssistantEngine(config.assistantEngine);
|
|
89
|
+
let autoResume = config.autoResume !== false;
|
|
90
|
+
let cronScheduler = {
|
|
91
|
+
addTask: () => null,
|
|
92
|
+
listTasks: () => [],
|
|
93
|
+
stopTask: () => false,
|
|
94
|
+
stopAll: () => 0,
|
|
95
|
+
};
|
|
108
96
|
|
|
109
97
|
// Dynamic input height settings
|
|
110
98
|
// Layout: topLine(1) + content + bottomLine(1) + dashboard(1)
|
|
111
99
|
const MIN_INPUT_HEIGHT = 4; // 1 content + 3
|
|
112
100
|
const MAX_INPUT_HEIGHT = 9; // 6 content + 3
|
|
113
101
|
let currentInputHeight = MIN_INPUT_HEIGHT;
|
|
114
|
-
|
|
115
|
-
// Log area (no border for cleaner look)
|
|
116
|
-
const logBox = blessed.log({
|
|
117
|
-
parent: screen,
|
|
118
|
-
top: 0,
|
|
119
|
-
left: 0,
|
|
120
|
-
width: "100%",
|
|
121
|
-
height: "100%-5", // Will be adjusted dynamically
|
|
122
|
-
tags: true,
|
|
123
|
-
scrollable: true,
|
|
124
|
-
alwaysScroll: true,
|
|
125
|
-
scrollback: 10000,
|
|
126
|
-
scrollbar: { ch: "│", style: { fg: "cyan" } },
|
|
127
|
-
keys: true,
|
|
128
|
-
vi: true,
|
|
129
|
-
// Enable mouse wheel scrolling in log area (use Option/Alt for native selection)
|
|
130
|
-
mouse: true,
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
// Status line just above input
|
|
134
|
-
const statusLine = blessed.box({
|
|
135
|
-
parent: screen,
|
|
136
|
-
bottom: currentInputHeight,
|
|
137
|
-
left: 0,
|
|
138
|
-
width: "100%",
|
|
139
|
-
height: 1,
|
|
140
|
-
style: { fg: "gray" },
|
|
141
|
-
tags: true,
|
|
142
|
-
content: "",
|
|
143
|
-
});
|
|
144
102
|
const pkg = require("../../package.json");
|
|
145
|
-
const
|
|
146
|
-
|
|
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
|
+
});
|
|
147
119
|
|
|
148
|
-
const historyDir = path.join(projectRoot
|
|
120
|
+
const historyDir = path.join(getUfooPaths(projectRoot).ufooDir, "chat");
|
|
149
121
|
const historyFile = path.join(historyDir, "history.jsonl");
|
|
150
122
|
const inputHistoryFile = path.join(historyDir, "input-history.jsonl");
|
|
151
123
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
fs
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
let lastLogWasSpacer = false;
|
|
159
|
-
let lastLogType = null;
|
|
160
|
-
let hasLoggedAny = false;
|
|
161
|
-
|
|
162
|
-
function shouldSpace(type) {
|
|
163
|
-
return SPACED_TYPES.has(type);
|
|
164
|
-
}
|
|
124
|
+
const chatLogController = createChatLogController({
|
|
125
|
+
logBox,
|
|
126
|
+
fsModule: fs,
|
|
127
|
+
historyDir,
|
|
128
|
+
historyFile,
|
|
129
|
+
});
|
|
165
130
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
text: "",
|
|
174
|
-
meta: {},
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
lastLogWasSpacer = true;
|
|
178
|
-
lastLogType = "spacer";
|
|
179
|
-
hasLoggedAny = true;
|
|
180
|
-
}
|
|
131
|
+
const streamTracker = createStreamTracker({
|
|
132
|
+
logBox,
|
|
133
|
+
writeSpacer: () => chatLogController.writeSpacer(false),
|
|
134
|
+
appendHistory: (...args) => chatLogController.appendHistory(...args),
|
|
135
|
+
escapeBlessed,
|
|
136
|
+
onStreamStart: () => chatLogController.markStreamStart(),
|
|
137
|
+
});
|
|
181
138
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
appendHistory({
|
|
189
|
-
ts: new Date().toISOString(),
|
|
190
|
-
type,
|
|
191
|
-
text,
|
|
192
|
-
meta,
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
lastLogWasSpacer = false;
|
|
196
|
-
lastLogType = type;
|
|
197
|
-
hasLoggedAny = true;
|
|
198
|
-
}
|
|
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);
|
|
199
145
|
|
|
200
146
|
function logMessage(type, text, meta = {}) {
|
|
201
|
-
|
|
147
|
+
chatLogController.logMessage(type, text, meta);
|
|
202
148
|
}
|
|
203
149
|
|
|
204
150
|
function loadHistory(limit = 2000) {
|
|
205
|
-
|
|
206
|
-
const lines = fs.readFileSync(historyFile, "utf8").trim().split(/\r?\n/).filter(Boolean);
|
|
207
|
-
const items = lines.slice(-limit).map((line) => JSON.parse(line));
|
|
208
|
-
const hasSpacer = items.some((item) => item && item.type === "spacer");
|
|
209
|
-
for (const item of items) {
|
|
210
|
-
if (!item) continue;
|
|
211
|
-
if (item.type === "spacer") {
|
|
212
|
-
writeSpacer(false);
|
|
213
|
-
continue;
|
|
214
|
-
}
|
|
215
|
-
if (!item.text) continue;
|
|
216
|
-
if (hasSpacer) {
|
|
217
|
-
logBox.log(item.text);
|
|
218
|
-
lastLogWasSpacer = false;
|
|
219
|
-
lastLogType = item.type || null;
|
|
220
|
-
hasLoggedAny = true;
|
|
221
|
-
} else {
|
|
222
|
-
recordLog(item.type || "unknown", item.text, item.meta || {}, false);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
} catch {
|
|
226
|
-
// ignore missing/invalid history
|
|
227
|
-
}
|
|
151
|
+
chatLogController.loadHistory(limit);
|
|
228
152
|
}
|
|
229
153
|
|
|
230
|
-
|
|
231
|
-
let historyIndex = 0;
|
|
232
|
-
let historyDraft = "";
|
|
233
|
-
|
|
234
|
-
function appendInputHistory(text) {
|
|
235
|
-
if (!text) return;
|
|
236
|
-
fs.mkdirSync(historyDir, { recursive: true });
|
|
237
|
-
fs.appendFileSync(inputHistoryFile, `${JSON.stringify({ text })}\n`);
|
|
238
|
-
}
|
|
154
|
+
let inputHistoryController = null;
|
|
239
155
|
|
|
240
156
|
function loadInputHistory(limit = 2000) {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
const items = lines.slice(-limit).map((line) => JSON.parse(line));
|
|
244
|
-
for (const item of items) {
|
|
245
|
-
if (item && typeof item.text === "string" && item.text.trim() !== "") {
|
|
246
|
-
inputHistory.push(item.text);
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
} catch {
|
|
250
|
-
// ignore missing/invalid history
|
|
251
|
-
}
|
|
252
|
-
historyIndex = inputHistory.length;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
const pendingStatusLines = [];
|
|
256
|
-
const busStatusQueue = [];
|
|
257
|
-
let primaryStatusText = bannerText;
|
|
258
|
-
|
|
259
|
-
function formatProcessingText(text) {
|
|
260
|
-
if (!text) return text;
|
|
261
|
-
if (text.includes("{")) return text;
|
|
262
|
-
if (!/processing/i.test(text)) return text;
|
|
263
|
-
return `{yellow-fg}⏳{/yellow-fg} ${text}`;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
function renderStatusLine() {
|
|
267
|
-
let content = primaryStatusText || "";
|
|
268
|
-
if (busStatusQueue.length > 0) {
|
|
269
|
-
const extra = busStatusQueue.length > 1
|
|
270
|
-
? ` {gray-fg}(+${busStatusQueue.length - 1}){/gray-fg}`
|
|
271
|
-
: "";
|
|
272
|
-
const busText = `${busStatusQueue[0].text}${extra}`;
|
|
273
|
-
content = content
|
|
274
|
-
? `${content} {gray-fg}·{/gray-fg} ${busText}`
|
|
275
|
-
: busText;
|
|
276
|
-
}
|
|
277
|
-
statusLine.setContent(content);
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
function setPrimaryStatus(text) {
|
|
281
|
-
primaryStatusText = text || "";
|
|
282
|
-
renderStatusLine();
|
|
157
|
+
if (!inputHistoryController) return;
|
|
158
|
+
inputHistoryController.loadInputHistory(limit);
|
|
283
159
|
}
|
|
284
160
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
setPrimaryStatus(formatted);
|
|
290
|
-
screen.render();
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
function resolveStatusLine(text) {
|
|
295
|
-
if (pendingStatusLines.length > 0) {
|
|
296
|
-
pendingStatusLines.shift();
|
|
297
|
-
}
|
|
298
|
-
if (pendingStatusLines.length > 0) {
|
|
299
|
-
setPrimaryStatus(pendingStatusLines[0]);
|
|
300
|
-
} else {
|
|
301
|
-
setPrimaryStatus(text || "");
|
|
302
|
-
}
|
|
303
|
-
screen.render();
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
function enqueueBusStatus(item) {
|
|
307
|
-
if (!item || !item.text) return;
|
|
308
|
-
const key = item.key || item.text;
|
|
309
|
-
const formatted = formatProcessingText(item.text);
|
|
310
|
-
const existing = busStatusQueue.find((entry) => entry.key === key);
|
|
311
|
-
if (existing) {
|
|
312
|
-
existing.text = formatted;
|
|
313
|
-
} else {
|
|
314
|
-
busStatusQueue.push({ key, text: formatted });
|
|
315
|
-
}
|
|
316
|
-
renderStatusLine();
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
function resolveBusStatus(item) {
|
|
320
|
-
if (!item) return;
|
|
321
|
-
const key = item.key || item.text;
|
|
322
|
-
let index = -1;
|
|
323
|
-
if (key) {
|
|
324
|
-
index = busStatusQueue.findIndex((entry) => entry.key === key);
|
|
325
|
-
}
|
|
326
|
-
if (index === -1 && item.text) {
|
|
327
|
-
index = busStatusQueue.findIndex((entry) => entry.text === item.text);
|
|
328
|
-
}
|
|
329
|
-
if (index === -1) return;
|
|
330
|
-
busStatusQueue.splice(index, 1);
|
|
331
|
-
renderStatusLine();
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
// Command completion panel
|
|
335
|
-
const completionPanel = blessed.box({
|
|
336
|
-
parent: screen,
|
|
337
|
-
bottom: currentInputHeight - 1,
|
|
338
|
-
left: 0,
|
|
339
|
-
width: "100%",
|
|
340
|
-
height: 0,
|
|
341
|
-
hidden: true,
|
|
342
|
-
border: {
|
|
343
|
-
type: "line",
|
|
344
|
-
top: true,
|
|
345
|
-
left: false,
|
|
346
|
-
right: false,
|
|
347
|
-
bottom: false
|
|
348
|
-
},
|
|
349
|
-
style: {
|
|
350
|
-
border: { fg: "yellow" },
|
|
351
|
-
fg: "white"
|
|
352
|
-
// No bg - uses terminal default background
|
|
353
|
-
},
|
|
354
|
-
padding: {
|
|
355
|
-
left: 0,
|
|
356
|
-
right: 0,
|
|
357
|
-
top: 0,
|
|
358
|
-
bottom: 0
|
|
359
|
-
},
|
|
360
|
-
tags: true,
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
// Dashboard at very bottom
|
|
364
|
-
const dashboard = blessed.box({
|
|
365
|
-
parent: screen,
|
|
366
|
-
bottom: 0,
|
|
367
|
-
left: 0,
|
|
368
|
-
width: "100%",
|
|
369
|
-
height: 1,
|
|
370
|
-
style: { fg: "gray" },
|
|
371
|
-
tags: true,
|
|
372
|
-
});
|
|
373
|
-
|
|
374
|
-
// Bottom border line for input area (above dashboard)
|
|
375
|
-
const inputBottomLine = blessed.line({
|
|
376
|
-
parent: screen,
|
|
377
|
-
bottom: 1,
|
|
378
|
-
left: 0,
|
|
379
|
-
width: "100%",
|
|
380
|
-
orientation: "horizontal",
|
|
381
|
-
style: { fg: "cyan" },
|
|
161
|
+
const statusLineController = createStatusLineController({
|
|
162
|
+
statusLine,
|
|
163
|
+
bannerText,
|
|
164
|
+
renderScreen: () => screen.render(),
|
|
382
165
|
});
|
|
383
166
|
|
|
384
|
-
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
});
|
|
405
|
-
// Avoid textarea's extra wrap margin (causes a phantom empty column)
|
|
406
|
-
input.type = "box";
|
|
407
|
-
|
|
408
|
-
// Top border line for input area (just above input)
|
|
409
|
-
const inputTopLine = blessed.line({
|
|
410
|
-
parent: screen,
|
|
411
|
-
bottom: currentInputHeight - 1, // 4-1=3: above input(2) + inputHeight(1)
|
|
412
|
-
left: 0,
|
|
413
|
-
width: "100%",
|
|
414
|
-
orientation: "horizontal",
|
|
415
|
-
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
|
+
},
|
|
416
187
|
});
|
|
417
188
|
|
|
418
189
|
// Add cursor position tracking
|
|
419
190
|
let cursorPos = 0;
|
|
420
191
|
let preferredCol = null;
|
|
421
192
|
|
|
422
|
-
// Get inner width
|
|
423
193
|
function getInnerWidth() {
|
|
424
|
-
const lpos = input.lpos || input._getCoords();
|
|
425
|
-
if (lpos && Number.isFinite(lpos.xl) && Number.isFinite(lpos.xi)) {
|
|
426
|
-
return Math.max(1, lpos.xl - lpos.xi + 1);
|
|
427
|
-
}
|
|
428
|
-
if (typeof input.width === "number") return Math.max(1, input.width);
|
|
429
|
-
if (typeof input.width === "string") {
|
|
430
|
-
const match = input.width.match(/^100%-([0-9]+)$/);
|
|
431
|
-
if (match && typeof screen.width === "number") {
|
|
432
|
-
return Math.max(1, screen.width - parseInt(match[1], 10));
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
194
|
const promptWidth = typeof promptBox.width === "number" ? promptBox.width : 2;
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
195
|
+
return inputMath.getInnerWidth({ input, screen, promptWidth });
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function getWrapWidth() {
|
|
199
|
+
return inputMath.getWrapWidth(input, getInnerWidth());
|
|
439
200
|
}
|
|
440
201
|
|
|
441
|
-
// Count lines considering both wrapping and newlines
|
|
442
202
|
function countLines(text, width) {
|
|
443
|
-
|
|
444
|
-
const lines = text.split("\n");
|
|
445
|
-
let total = 0;
|
|
446
|
-
for (const line of lines) {
|
|
447
|
-
const lineWidth = input.strWidth(line);
|
|
448
|
-
total += Math.max(1, Math.ceil(lineWidth / width));
|
|
449
|
-
}
|
|
450
|
-
return total;
|
|
203
|
+
return inputMath.countLines(text, width, (value) => input.strWidth(value));
|
|
451
204
|
}
|
|
452
205
|
|
|
453
206
|
function getCursorRowCol(text, pos, width) {
|
|
454
|
-
|
|
455
|
-
const before = text.slice(0, pos);
|
|
456
|
-
const lines = before.split("\n");
|
|
457
|
-
let row = 0;
|
|
458
|
-
for (let i = 0; i < lines.length - 1; i++) {
|
|
459
|
-
const lineWidth = input.strWidth(lines[i]);
|
|
460
|
-
row += Math.max(1, Math.ceil(lineWidth / width));
|
|
461
|
-
}
|
|
462
|
-
const lastLine = lines[lines.length - 1] || "";
|
|
463
|
-
const lastWidth = input.strWidth(lastLine);
|
|
464
|
-
row += Math.floor(lastWidth / width);
|
|
465
|
-
const col = lastWidth % width;
|
|
466
|
-
return { row, col };
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
function getLinePosForCol(line, targetCol) {
|
|
470
|
-
if (targetCol <= 0) return 0;
|
|
471
|
-
let col = 0;
|
|
472
|
-
let offset = 0;
|
|
473
|
-
for (const ch of Array.from(line)) {
|
|
474
|
-
const w = input.strWidth(ch);
|
|
475
|
-
if (col + w > targetCol) return offset;
|
|
476
|
-
col += w;
|
|
477
|
-
offset += ch.length;
|
|
478
|
-
}
|
|
479
|
-
return offset;
|
|
207
|
+
return inputMath.getCursorRowCol(text, pos, width, (value) => input.strWidth(value));
|
|
480
208
|
}
|
|
481
209
|
|
|
482
210
|
function getCursorPosForRowCol(text, targetRow, targetCol, width) {
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
211
|
+
return inputMath.getCursorPosForRowCol(
|
|
212
|
+
text,
|
|
213
|
+
targetRow,
|
|
214
|
+
targetCol,
|
|
215
|
+
width,
|
|
216
|
+
(value) => input.strWidth(value),
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function ensureInputCursorVisible() {
|
|
221
|
+
const innerWidth = getInnerWidth();
|
|
222
|
+
if (innerWidth <= 0) return;
|
|
223
|
+
const totalRows = countLines(input.value, innerWidth);
|
|
224
|
+
const visibleRows = Math.max(1, input.height || 1);
|
|
225
|
+
const { row } = getCursorRowCol(input.value, cursorPos, innerWidth);
|
|
226
|
+
let base = input.childBase || 0;
|
|
227
|
+
const maxBase = Math.max(0, totalRows - visibleRows);
|
|
228
|
+
const bottomMargin = visibleRows > 1 ? 1 : 0;
|
|
229
|
+
const upperLimit = base;
|
|
230
|
+
const lowerLimit = base + visibleRows - bottomMargin - 1;
|
|
231
|
+
|
|
232
|
+
if (row < upperLimit) {
|
|
233
|
+
base = row;
|
|
234
|
+
} else if (row > lowerLimit) {
|
|
235
|
+
base = row - (visibleRows - bottomMargin - 1);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (base > maxBase) base = maxBase;
|
|
239
|
+
if (base < 0) base = 0;
|
|
240
|
+
if (base !== input.childBase) {
|
|
241
|
+
input.childBase = base;
|
|
242
|
+
if (typeof input.scrollTo === "function") {
|
|
243
|
+
input.scrollTo(base);
|
|
494
244
|
}
|
|
495
|
-
pos += line.length + 1;
|
|
496
|
-
row += wrappedRows;
|
|
497
245
|
}
|
|
498
|
-
return text.length;
|
|
499
246
|
}
|
|
500
247
|
|
|
501
248
|
function resetPreferredCol() {
|
|
502
249
|
preferredCol = null;
|
|
503
250
|
}
|
|
504
251
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
let pasteBuffer = "";
|
|
509
|
-
let pasteRemainder = "";
|
|
510
|
-
let suppressKeypress = false;
|
|
511
|
-
let suppressReset = null;
|
|
252
|
+
function getPreferredCol() {
|
|
253
|
+
return preferredCol;
|
|
254
|
+
}
|
|
512
255
|
|
|
513
|
-
function
|
|
514
|
-
|
|
515
|
-
if (suppressReset) clearImmediate(suppressReset);
|
|
516
|
-
suppressReset = setImmediate(() => {
|
|
517
|
-
if (!pasteActive) suppressKeypress = false;
|
|
518
|
-
});
|
|
256
|
+
function setPreferredCol(value) {
|
|
257
|
+
preferredCol = value;
|
|
519
258
|
}
|
|
520
259
|
|
|
521
260
|
function normalizePaste(text) {
|
|
522
|
-
|
|
523
|
-
let normalized = text.replace(/\x1b\[200~|\x1b\[201~/g, "");
|
|
524
|
-
normalized = normalized.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
525
|
-
return normalized;
|
|
261
|
+
return inputMath.normalizePaste(text);
|
|
526
262
|
}
|
|
527
263
|
|
|
528
264
|
function updateDraftFromInput() {
|
|
529
|
-
if (
|
|
530
|
-
|
|
531
|
-
}
|
|
265
|
+
if (!inputHistoryController) return;
|
|
266
|
+
inputHistoryController.updateDraftFromInput();
|
|
532
267
|
}
|
|
533
268
|
|
|
534
269
|
function normalizeCommandPrefix() {
|
|
@@ -547,6 +282,7 @@ async function runChat(projectRoot) {
|
|
|
547
282
|
normalizeCommandPrefix();
|
|
548
283
|
resetPreferredCol();
|
|
549
284
|
resizeInput();
|
|
285
|
+
ensureInputCursorVisible();
|
|
550
286
|
input._updateCursor();
|
|
551
287
|
screen.render();
|
|
552
288
|
updateDraftFromInput();
|
|
@@ -557,266 +293,113 @@ async function runChat(projectRoot) {
|
|
|
557
293
|
cursorPos = input.value.length;
|
|
558
294
|
resetPreferredCol();
|
|
559
295
|
resizeInput();
|
|
296
|
+
ensureInputCursorVisible();
|
|
560
297
|
input._updateCursor();
|
|
561
298
|
screen.render();
|
|
562
299
|
}
|
|
563
300
|
|
|
301
|
+
inputHistoryController = createInputHistoryController({
|
|
302
|
+
inputHistoryFile,
|
|
303
|
+
historyDir,
|
|
304
|
+
setInputValue,
|
|
305
|
+
getInputValue: () => input.value || "",
|
|
306
|
+
});
|
|
307
|
+
|
|
564
308
|
function historyUp() {
|
|
565
|
-
if (
|
|
566
|
-
|
|
567
|
-
historyDraft = input.value;
|
|
568
|
-
}
|
|
569
|
-
if (historyIndex > 0) {
|
|
570
|
-
historyIndex -= 1;
|
|
571
|
-
setInputValue(inputHistory[historyIndex]);
|
|
572
|
-
return true;
|
|
573
|
-
}
|
|
574
|
-
return true;
|
|
309
|
+
if (!inputHistoryController) return false;
|
|
310
|
+
return inputHistoryController.historyUp();
|
|
575
311
|
}
|
|
576
312
|
|
|
577
313
|
function historyDown() {
|
|
578
|
-
if (
|
|
579
|
-
|
|
580
|
-
historyIndex += 1;
|
|
581
|
-
setInputValue(inputHistory[historyIndex]);
|
|
582
|
-
return true;
|
|
583
|
-
}
|
|
584
|
-
if (historyIndex === inputHistory.length - 1) {
|
|
585
|
-
historyIndex = inputHistory.length;
|
|
586
|
-
setInputValue(historyDraft || "");
|
|
587
|
-
return true;
|
|
588
|
-
}
|
|
589
|
-
return false;
|
|
314
|
+
if (!inputHistoryController) return false;
|
|
315
|
+
return inputHistoryController.historyDown();
|
|
590
316
|
}
|
|
591
317
|
|
|
592
318
|
function exitHandler() {
|
|
319
|
+
if (daemonCoordinator) {
|
|
320
|
+
daemonCoordinator.markExit();
|
|
321
|
+
}
|
|
322
|
+
cronScheduler.stopAll();
|
|
323
|
+
exitAgentView();
|
|
593
324
|
if (screen && screen.program && typeof screen.program.decrst === "function") {
|
|
594
325
|
screen.program.decrst(2004);
|
|
595
326
|
}
|
|
596
|
-
|
|
597
|
-
|
|
327
|
+
statusLineController.destroy();
|
|
328
|
+
if (daemonCoordinator) {
|
|
329
|
+
daemonCoordinator.close();
|
|
598
330
|
}
|
|
599
331
|
process.exit(0);
|
|
600
332
|
}
|
|
601
333
|
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
if (!trimmed) {
|
|
624
|
-
hideCompletion();
|
|
625
|
-
return;
|
|
626
|
-
}
|
|
627
|
-
filterText = trimmed;
|
|
628
|
-
|
|
629
|
-
// Check if we're in subcommand mode
|
|
630
|
-
const parts = filterText.split(/\s+/);
|
|
631
|
-
let commands = [];
|
|
632
|
-
|
|
633
|
-
if ((parts.length > 1 || (endsWithSpace && parts.length === 1)) && parts[0].startsWith("/")) {
|
|
634
|
-
// Subcommand mode: "/bus rename"
|
|
635
|
-
const mainCmd = parts[0];
|
|
636
|
-
const subFilter = parts[1] || "";
|
|
637
|
-
|
|
638
|
-
// Find the main command
|
|
639
|
-
const mainCmdObj = COMMAND_REGISTRY.find(item =>
|
|
640
|
-
item.cmd.toLowerCase() === mainCmd.toLowerCase()
|
|
641
|
-
);
|
|
642
|
-
|
|
643
|
-
if (mainCmdObj && mainCmdObj.subcommands) {
|
|
644
|
-
// Filter subcommands
|
|
645
|
-
commands = mainCmdObj.subcommands
|
|
646
|
-
.filter(sub => sub.cmd.toLowerCase().startsWith(subFilter.toLowerCase()))
|
|
647
|
-
.map(sub => ({ ...sub, isSubcommand: true, parentCmd: mainCmd }));
|
|
648
|
-
}
|
|
649
|
-
} else {
|
|
650
|
-
// Main command mode: "/bus"
|
|
651
|
-
const prefixMatches = COMMAND_REGISTRY.filter(item =>
|
|
652
|
-
item.cmd.toLowerCase().startsWith(filterText.toLowerCase())
|
|
653
|
-
);
|
|
654
|
-
// Also allow fuzzy matches on the command body (e.g. "/b" -> /bus + /ubus)
|
|
655
|
-
let fuzzyMatches = [];
|
|
656
|
-
if (filterText.startsWith("/") && parts.length === 1) {
|
|
657
|
-
const needle = filterText.slice(1).toLowerCase();
|
|
658
|
-
if (needle) {
|
|
659
|
-
fuzzyMatches = COMMAND_REGISTRY.filter(item =>
|
|
660
|
-
item.cmd.toLowerCase().includes(needle)
|
|
661
|
-
);
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
const merged = new Map();
|
|
665
|
-
for (const item of prefixMatches) merged.set(item.cmd, item);
|
|
666
|
-
for (const item of fuzzyMatches) merged.set(item.cmd, item);
|
|
667
|
-
commands = Array.from(merged.values());
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
if (commands.length === 0) {
|
|
671
|
-
hideCompletion();
|
|
672
|
-
return;
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
completionCommands = commands;
|
|
676
|
-
completionActive = true;
|
|
677
|
-
completionIndex = 0;
|
|
678
|
-
completionScrollOffset = 0;
|
|
679
|
-
|
|
680
|
-
// Calculate panel height (max 8 visible + 1 for top border)
|
|
681
|
-
const visibleItems = Math.min(8, completionCommands.length);
|
|
682
|
-
completionPanel.height = visibleItems + 1;
|
|
683
|
-
completionPanel.bottom = currentInputHeight - 1;
|
|
684
|
-
completionPanel.hidden = false;
|
|
685
|
-
|
|
686
|
-
renderCompletionPanel();
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
function hideCompletion() {
|
|
690
|
-
completionActive = false;
|
|
691
|
-
completionCommands = [];
|
|
692
|
-
completionIndex = 0;
|
|
693
|
-
completionScrollOffset = 0;
|
|
694
|
-
completionPanel.hidden = true;
|
|
695
|
-
screen.render();
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
function renderCompletionPanel() {
|
|
699
|
-
if (!completionActive || completionCommands.length === 0) return;
|
|
700
|
-
|
|
701
|
-
const maxVisible = 8;
|
|
702
|
-
|
|
703
|
-
// Adjust scroll offset to keep selected item visible
|
|
704
|
-
if (completionIndex < completionScrollOffset) {
|
|
705
|
-
completionScrollOffset = completionIndex;
|
|
706
|
-
} else if (completionIndex >= completionScrollOffset + maxVisible) {
|
|
707
|
-
completionScrollOffset = completionIndex - maxVisible + 1;
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
// Calculate visible slice
|
|
711
|
-
const visibleStart = completionScrollOffset;
|
|
712
|
-
const visibleEnd = Math.min(completionScrollOffset + maxVisible, completionCommands.length);
|
|
713
|
-
const visibleCommands = completionCommands.slice(visibleStart, visibleEnd);
|
|
714
|
-
|
|
715
|
-
const lines = visibleCommands.map((item, i) => {
|
|
716
|
-
const actualIndex = visibleStart + i;
|
|
717
|
-
const cmdPart = actualIndex === completionIndex
|
|
718
|
-
? `{inverse}${item.cmd}{/inverse}`
|
|
719
|
-
: `{cyan-fg}${item.cmd}{/cyan-fg}`;
|
|
720
|
-
const descPart = `{gray-fg}${item.desc}{/gray-fg}`;
|
|
721
|
-
// Use promptBox width (2) to align with input position
|
|
722
|
-
const indent = " ".repeat(promptBox.width || 2);
|
|
723
|
-
return `${indent}${cmdPart} ${descPart}`;
|
|
724
|
-
});
|
|
725
|
-
|
|
726
|
-
completionPanel.setContent(lines.join("\n"));
|
|
727
|
-
screen.render();
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
function completionUp() {
|
|
731
|
-
if (completionCommands.length === 0) return;
|
|
732
|
-
completionIndex = completionIndex <= 0
|
|
733
|
-
? completionCommands.length - 1
|
|
734
|
-
: completionIndex - 1;
|
|
735
|
-
renderCompletionPanel();
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
function completionDown() {
|
|
739
|
-
if (completionCommands.length === 0) return;
|
|
740
|
-
completionIndex = completionIndex >= completionCommands.length - 1
|
|
741
|
-
? 0
|
|
742
|
-
: completionIndex + 1;
|
|
743
|
-
renderCompletionPanel();
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
function confirmCompletion() {
|
|
747
|
-
if (!completionActive || completionCommands.length === 0) return;
|
|
748
|
-
|
|
749
|
-
const selected = completionCommands[completionIndex];
|
|
750
|
-
|
|
751
|
-
if (selected.isSubcommand) {
|
|
752
|
-
// Subcommand: replace the last word with selected subcommand
|
|
753
|
-
const parts = input.value.split(/\s+/);
|
|
754
|
-
parts[parts.length - 1] = selected.cmd;
|
|
755
|
-
input.value = parts.join(" ") + " ";
|
|
756
|
-
} else {
|
|
757
|
-
// Main command
|
|
758
|
-
input.value = selected.cmd + " ";
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
cursorPos = input.value.length;
|
|
762
|
-
resetPreferredCol();
|
|
763
|
-
input._updateCursor();
|
|
764
|
-
updateDraftFromInput();
|
|
765
|
-
|
|
766
|
-
// If selected command has subcommands, trigger subcommand completion immediately
|
|
767
|
-
if (!selected.isSubcommand && selected.subcommands && selected.subcommands.length > 0) {
|
|
768
|
-
// Don't hide - directly show subcommand completion
|
|
769
|
-
showCompletion(input.value);
|
|
770
|
-
} else {
|
|
771
|
-
// No subcommands - hide completion
|
|
772
|
-
hideCompletion();
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
screen.render();
|
|
776
|
-
}
|
|
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
|
+
});
|
|
777
355
|
|
|
778
|
-
|
|
779
|
-
|
|
356
|
+
const pasteController = createPasteController({
|
|
357
|
+
shouldHandle: () => screen.focused === input && focusMode === "input",
|
|
358
|
+
normalizePaste,
|
|
359
|
+
insertTextAtCursor,
|
|
360
|
+
});
|
|
780
361
|
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
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
|
+
});
|
|
816
399
|
|
|
817
400
|
// Resize input box based on content
|
|
818
401
|
function resizeInput() {
|
|
819
|
-
const innerWidth =
|
|
402
|
+
const innerWidth = getWrapWidth();
|
|
820
403
|
if (innerWidth <= 0) return;
|
|
821
404
|
|
|
822
405
|
const numLines = countLines(input.value, innerWidth);
|
|
@@ -831,221 +414,31 @@ async function runChat(projectRoot) {
|
|
|
831
414
|
}
|
|
832
415
|
statusLine.bottom = currentInputHeight;
|
|
833
416
|
// Reposition completion panel if active
|
|
834
|
-
if (
|
|
835
|
-
completionPanel.bottom = currentInputHeight - 1;
|
|
836
|
-
}
|
|
417
|
+
if (completionController.isActive()) completionController.reflow();
|
|
837
418
|
// dashboard and inputBottomLine stay fixed at bottom 0 and 1
|
|
838
419
|
logBox.height = Math.max(1, screen.height - currentInputHeight - 1);
|
|
420
|
+
ensureInputCursorVisible();
|
|
839
421
|
}
|
|
840
422
|
|
|
841
423
|
// Override the internal listener to support cursor movement
|
|
842
424
|
input._listener = function(ch, key) {
|
|
843
|
-
|
|
844
|
-
exitHandler();
|
|
845
|
-
return;
|
|
846
|
-
}
|
|
847
|
-
if (suppressKeypress) {
|
|
848
|
-
return;
|
|
849
|
-
}
|
|
850
|
-
normalizeCommandPrefix();
|
|
851
|
-
if (key && (key.name === "pageup" || key.name === "pagedown")) {
|
|
852
|
-
const delta = Math.max(1, Math.floor(logBox.height / 2));
|
|
853
|
-
scrollLog(key.name === "pageup" ? -delta : delta);
|
|
854
|
-
return;
|
|
855
|
-
}
|
|
856
|
-
if (focusMode === "dashboard") {
|
|
857
|
-
if (handleDashboardKey(key)) return;
|
|
858
|
-
return;
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
// Command completion mode
|
|
862
|
-
if (completionActive) {
|
|
863
|
-
if (handleCompletionKey(ch, key)) return;
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
// Treat multi-char input (paste) as insertion, including newlines.
|
|
867
|
-
if (ch && ch.length > 1 && (!key || !key.name || key.name.length !== 1)) {
|
|
868
|
-
insertTextAtCursor(normalizePaste(ch));
|
|
869
|
-
return;
|
|
870
|
-
}
|
|
871
|
-
if (ch && (ch.includes("\n") || ch.includes("\r")) && (!key || (key.name !== "return" && key.name !== "enter"))) {
|
|
872
|
-
insertTextAtCursor(normalizePaste(ch));
|
|
873
|
-
return;
|
|
874
|
-
}
|
|
875
|
-
// Plain enter submits, shift+enter inserts newline
|
|
876
|
-
if (key.name === "return" || key.name === "enter") {
|
|
877
|
-
if (key.shift) {
|
|
878
|
-
// Insert newline at cursor
|
|
879
|
-
insertTextAtCursor("\n");
|
|
880
|
-
} else {
|
|
881
|
-
// Submit
|
|
882
|
-
resetPreferredCol();
|
|
883
|
-
this._done(null, this.value);
|
|
884
|
-
}
|
|
885
|
-
return;
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
if (key.name === "left") {
|
|
889
|
-
if (cursorPos > 0) cursorPos--;
|
|
890
|
-
resetPreferredCol();
|
|
891
|
-
this._updateCursor();
|
|
892
|
-
this.screen.render();
|
|
893
|
-
return;
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
if (key.name === "right") {
|
|
897
|
-
if (cursorPos < this.value.length) cursorPos++;
|
|
898
|
-
resetPreferredCol();
|
|
899
|
-
this._updateCursor();
|
|
900
|
-
this.screen.render();
|
|
901
|
-
return;
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
if (key.name === "home") {
|
|
905
|
-
cursorPos = 0;
|
|
906
|
-
resetPreferredCol();
|
|
907
|
-
this._updateCursor();
|
|
908
|
-
this.screen.render();
|
|
909
|
-
return;
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
if (key.name === "end") {
|
|
913
|
-
cursorPos = this.value.length;
|
|
914
|
-
resetPreferredCol();
|
|
915
|
-
this._updateCursor();
|
|
916
|
-
this.screen.render();
|
|
917
|
-
return;
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
if (key.name === "up") {
|
|
921
|
-
// Special case: "/" + Up → jump to last command in completion
|
|
922
|
-
if (completionActive && input.value === "/" && cursorPos === 1) {
|
|
923
|
-
completionIndex = completionCommands.length - 1;
|
|
924
|
-
renderCompletionPanel();
|
|
925
|
-
return;
|
|
926
|
-
}
|
|
927
|
-
if (historyUp()) {
|
|
928
|
-
hideCompletion();
|
|
929
|
-
return;
|
|
930
|
-
}
|
|
931
|
-
}
|
|
932
|
-
if (key.name === "down") {
|
|
933
|
-
if (historyDown()) {
|
|
934
|
-
hideCompletion();
|
|
935
|
-
return;
|
|
936
|
-
}
|
|
937
|
-
}
|
|
938
|
-
if (key.name === "up" || key.name === "down") {
|
|
939
|
-
const innerWidth = getInnerWidth();
|
|
940
|
-
if (innerWidth > 0) {
|
|
941
|
-
const { row, col } = getCursorRowCol(this.value, cursorPos, innerWidth);
|
|
942
|
-
if (preferredCol === null) preferredCol = col;
|
|
943
|
-
const totalRows = countLines(this.value, innerWidth);
|
|
944
|
-
|
|
945
|
-
// Down at last row -> enter dashboard mode
|
|
946
|
-
if (key.name === "down" && row >= totalRows - 1) {
|
|
947
|
-
enterDashboardMode();
|
|
948
|
-
return;
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
const targetRow = key.name === "up"
|
|
952
|
-
? Math.max(0, row - 1)
|
|
953
|
-
: Math.min(totalRows - 1, row + 1);
|
|
954
|
-
cursorPos = getCursorPosForRowCol(this.value, targetRow, preferredCol, innerWidth);
|
|
955
|
-
}
|
|
956
|
-
this._updateCursor();
|
|
957
|
-
this.screen.render();
|
|
958
|
-
return;
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
if (key.name === "escape") {
|
|
962
|
-
this._done(null, null);
|
|
963
|
-
return;
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
if (key.name === "backspace") {
|
|
967
|
-
if (cursorPos > 0) {
|
|
968
|
-
this.value = this.value.slice(0, cursorPos - 1) + this.value.slice(cursorPos);
|
|
969
|
-
cursorPos--;
|
|
970
|
-
resetPreferredCol();
|
|
971
|
-
resizeInput();
|
|
972
|
-
this._updateCursor();
|
|
973
|
-
updateDraftFromInput();
|
|
974
|
-
|
|
975
|
-
// Update or hide completion after backspace
|
|
976
|
-
if (this.value.startsWith("/")) {
|
|
977
|
-
showCompletion(this.value);
|
|
978
|
-
} else {
|
|
979
|
-
hideCompletion();
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
this.screen.render();
|
|
983
|
-
}
|
|
984
|
-
return;
|
|
985
|
-
}
|
|
986
|
-
|
|
987
|
-
if (key.name === "delete") {
|
|
988
|
-
if (cursorPos < this.value.length) {
|
|
989
|
-
this.value = this.value.slice(0, cursorPos) + this.value.slice(cursorPos + 1);
|
|
990
|
-
resetPreferredCol();
|
|
991
|
-
resizeInput();
|
|
992
|
-
this._updateCursor();
|
|
993
|
-
this.screen.render();
|
|
994
|
-
updateDraftFromInput();
|
|
995
|
-
}
|
|
996
|
-
return;
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
// Insert character at cursor position
|
|
1000
|
-
const insertChar = (ch && ch.length === 1)
|
|
1001
|
-
? ch
|
|
1002
|
-
: (key && key.name && key.name.length === 1 ? key.name : null);
|
|
1003
|
-
if (insertChar && !/^[\x00-\x08\x0b-\x0c\x0e-\x1f\x7f]$/.test(insertChar)) {
|
|
1004
|
-
this.value = this.value.slice(0, cursorPos) + insertChar + this.value.slice(cursorPos);
|
|
1005
|
-
cursorPos++;
|
|
1006
|
-
normalizeCommandPrefix();
|
|
1007
|
-
resetPreferredCol();
|
|
1008
|
-
resizeInput();
|
|
1009
|
-
this._updateCursor();
|
|
1010
|
-
updateDraftFromInput();
|
|
1011
|
-
|
|
1012
|
-
// Update completion filter if typing after "/"
|
|
1013
|
-
if (this.value.startsWith("/")) {
|
|
1014
|
-
showCompletion(this.value);
|
|
1015
|
-
} else if (completionActive) {
|
|
1016
|
-
hideCompletion();
|
|
1017
|
-
}
|
|
1018
|
-
|
|
1019
|
-
this.screen.render();
|
|
1020
|
-
return;
|
|
1021
|
-
}
|
|
425
|
+
inputListenerController.handleKey(ch, key, this);
|
|
1022
426
|
};
|
|
1023
427
|
|
|
1024
428
|
// Override cursor update to use our cursor position
|
|
1025
429
|
input._updateCursor = function() {
|
|
1026
430
|
if (this.screen.focused !== this) return;
|
|
1027
431
|
|
|
1028
|
-
|
|
432
|
+
let lpos;
|
|
433
|
+
try { lpos = this._getCoords(); } catch { return; }
|
|
1029
434
|
if (!lpos) return;
|
|
1030
435
|
|
|
1031
|
-
const innerWidth =
|
|
436
|
+
const innerWidth = getWrapWidth();
|
|
1032
437
|
if (innerWidth <= 0) return;
|
|
1033
438
|
|
|
439
|
+
ensureInputCursorVisible();
|
|
1034
440
|
const { row, col } = getCursorRowCol(this.value, cursorPos, innerWidth);
|
|
1035
|
-
const
|
|
1036
|
-
|
|
1037
|
-
let scrollOffset = this.childBase || 0;
|
|
1038
|
-
if (row < scrollOffset) {
|
|
1039
|
-
scrollOffset = row;
|
|
1040
|
-
} else if (row >= scrollOffset + innerHeight) {
|
|
1041
|
-
scrollOffset = row - innerHeight + 1;
|
|
1042
|
-
}
|
|
1043
|
-
if (scrollOffset !== this.childBase) {
|
|
1044
|
-
this.childBase = scrollOffset;
|
|
1045
|
-
if (typeof this.scrollTo === "function") {
|
|
1046
|
-
this.scrollTo(scrollOffset);
|
|
1047
|
-
}
|
|
1048
|
-
}
|
|
441
|
+
const scrollOffset = this.childBase || 0;
|
|
1049
442
|
|
|
1050
443
|
const displayRow = row - scrollOffset;
|
|
1051
444
|
const safeCol = Math.min(Math.max(0, col), innerWidth - 1);
|
|
@@ -1062,9 +455,8 @@ async function runChat(projectRoot) {
|
|
|
1062
455
|
cursorPos = 0;
|
|
1063
456
|
resetPreferredCol();
|
|
1064
457
|
currentInputHeight = MIN_INPUT_HEIGHT;
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
hideCompletion();
|
|
458
|
+
if (inputHistoryController) inputHistoryController.setIndexToEnd();
|
|
459
|
+
completionController.hide();
|
|
1068
460
|
const contentHeight = 1; // MIN content height
|
|
1069
461
|
input.height = contentHeight;
|
|
1070
462
|
promptBox.height = contentHeight;
|
|
@@ -1074,95 +466,283 @@ async function runChat(projectRoot) {
|
|
|
1074
466
|
return originalClearValue();
|
|
1075
467
|
};
|
|
1076
468
|
|
|
1077
|
-
let pending = null;
|
|
469
|
+
let pending = null;
|
|
470
|
+
|
|
471
|
+
// Agent selection state
|
|
472
|
+
let activeAgents = [];
|
|
473
|
+
let activeAgentLabelMap = new Map();
|
|
474
|
+
let activeAgentMetaMap = new Map(); // Store full meta including launch_mode
|
|
475
|
+
let agentListWindowStart = 0;
|
|
476
|
+
const MAX_AGENT_WINDOW = 4;
|
|
477
|
+
let selectedAgentIndex = -1; // -1 = not in dashboard selection mode
|
|
478
|
+
let targetAgent = null; // Selected agent for direct messaging
|
|
479
|
+
let focusMode = "input"; // "input" or "dashboard"
|
|
480
|
+
let dashboardView = "agents"; // "agents" | "mode" | "provider" | "assistant" | "cron"
|
|
481
|
+
let reportPendingTotal = 0;
|
|
482
|
+
let selectedModeIndex = launchMode === "internal" ? 2 : (launchMode === "tmux" ? 1 : 0);
|
|
483
|
+
const providerOptions = [
|
|
484
|
+
{ label: "codex", value: "codex-cli" },
|
|
485
|
+
{ label: "claude", value: "claude-cli" },
|
|
486
|
+
];
|
|
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
|
+
);
|
|
498
|
+
const resumeOptions = [
|
|
499
|
+
{ label: "Resume previous session", value: true },
|
|
500
|
+
{ label: "Start new session", value: false },
|
|
501
|
+
];
|
|
502
|
+
let selectedResumeIndex = autoResume ? 0 : 1;
|
|
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
|
+
};
|
|
516
|
+
|
|
517
|
+
function getCurrentView() {
|
|
518
|
+
return agentViewController ? agentViewController.getCurrentView() : "main";
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function getViewingAgent() {
|
|
522
|
+
return agentViewController ? agentViewController.getViewingAgent() : "";
|
|
523
|
+
}
|
|
524
|
+
|
|
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 });
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function getViewingAgentAdapter() {
|
|
533
|
+
const viewingAgent = getViewingAgent();
|
|
534
|
+
if (!viewingAgent) return null;
|
|
535
|
+
return getAgentAdapter(viewingAgent);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function canSendRaw(adapter) {
|
|
539
|
+
if (!adapter || !adapter.capabilities) return false;
|
|
540
|
+
return Boolean(
|
|
541
|
+
adapter.capabilities.supportsSocketProtocol
|
|
542
|
+
|| adapter.capabilities.supportsInternalQueueLoop
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function canResize(adapter) {
|
|
547
|
+
return Boolean(adapter && adapter.capabilities && adapter.capabilities.supportsSocketProtocol);
|
|
548
|
+
}
|
|
549
|
+
|
|
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
|
+
);
|
|
557
|
+
}
|
|
558
|
+
|
|
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
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function sendResizeWithCapabilities(cols, rows) {
|
|
570
|
+
const adapter = getViewingAgentAdapter();
|
|
571
|
+
if (!canResize(adapter)) return;
|
|
572
|
+
try {
|
|
573
|
+
adapter.resize(cols, rows);
|
|
574
|
+
} catch {
|
|
575
|
+
// ignore unsupported errors
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function requestSnapshotWithCapabilities() {
|
|
580
|
+
const adapter = getViewingAgentAdapter();
|
|
581
|
+
if (!canSnapshot(adapter)) return false;
|
|
582
|
+
try {
|
|
583
|
+
return adapter.snapshot();
|
|
584
|
+
} catch {
|
|
585
|
+
return false;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function isAgentViewUsesBus() {
|
|
590
|
+
return agentViewController ? agentViewController.isAgentViewUsesBus() : false;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function getAgentInputSuppressUntil() {
|
|
594
|
+
return agentViewController ? agentViewController.getAgentInputSuppressUntil() : 0;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function getAgentOutputSuppressed() {
|
|
598
|
+
return agentViewController ? agentViewController.getAgentOutputSuppressed() : false;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function setAgentOutputSuppressed(value) {
|
|
602
|
+
if (agentViewController) {
|
|
603
|
+
agentViewController.setAgentOutputSuppressed(value);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function renderAgentDashboard() {
|
|
608
|
+
if (agentViewController) {
|
|
609
|
+
agentViewController.renderAgentDashboard();
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function setAgentBarVisible(visible) {
|
|
614
|
+
if (agentViewController) {
|
|
615
|
+
agentViewController.setAgentBarVisible(visible);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function enterAgentView(agentId, options = {}) {
|
|
620
|
+
if (agentViewController) {
|
|
621
|
+
agentViewController.enterAgentView(agentId, options);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function exitAgentView() {
|
|
626
|
+
if (agentViewController) {
|
|
627
|
+
agentViewController.exitAgentView();
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function sendRawToAgent(data) {
|
|
632
|
+
if (agentViewController) {
|
|
633
|
+
agentViewController.sendRawToAgent(data);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function sendResizeToAgent(cols, rows) {
|
|
638
|
+
if (agentViewController) {
|
|
639
|
+
agentViewController.sendResizeToAgent(cols, rows);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function requestAgentSnapshot() {
|
|
644
|
+
if (agentViewController) {
|
|
645
|
+
agentViewController.requestAgentSnapshot();
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function writeToAgentTerm(text) {
|
|
650
|
+
if (agentViewController) {
|
|
651
|
+
agentViewController.writeToAgentTerm(text);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
1078
654
|
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
const COMMAND_REGISTRY = [
|
|
1086
|
-
{ cmd: "/doctor", desc: "Health check diagnostics" },
|
|
1087
|
-
{ cmd: "/status", desc: "Status display" },
|
|
1088
|
-
{
|
|
1089
|
-
cmd: "/daemon",
|
|
1090
|
-
desc: "Daemon management",
|
|
1091
|
-
subcommands: [
|
|
1092
|
-
{ cmd: "start", desc: "Start daemon" },
|
|
1093
|
-
{ cmd: "stop", desc: "Stop daemon" },
|
|
1094
|
-
{ cmd: "restart", desc: "Restart daemon" },
|
|
1095
|
-
{ cmd: "status", desc: "Daemon status" },
|
|
1096
|
-
]
|
|
1097
|
-
},
|
|
1098
|
-
{ cmd: "/init", desc: "Initialize modules" },
|
|
1099
|
-
{
|
|
1100
|
-
cmd: "/bus",
|
|
1101
|
-
desc: "Event bus operations",
|
|
1102
|
-
subcommands: [
|
|
1103
|
-
{ cmd: "send", desc: "Send message to agent" },
|
|
1104
|
-
{ cmd: "rename", desc: "Rename agent nickname" },
|
|
1105
|
-
{ cmd: "list", desc: "List all agents" },
|
|
1106
|
-
{ cmd: "status", desc: "Bus status" },
|
|
1107
|
-
]
|
|
1108
|
-
},
|
|
1109
|
-
{ cmd: "/ctx", desc: "Context management" },
|
|
1110
|
-
{ cmd: "/skills", desc: "Skills management" },
|
|
1111
|
-
{ cmd: "/ubus", desc: "Check bus messages" },
|
|
1112
|
-
{ cmd: "/uctx", desc: "Context status" },
|
|
1113
|
-
{ cmd: "/uinit", desc: "Initialize/repair" },
|
|
1114
|
-
{ cmd: "/ustatus", desc: "Unified status" },
|
|
1115
|
-
];
|
|
655
|
+
function placeAgentCursor(cursor) {
|
|
656
|
+
if (agentViewController) {
|
|
657
|
+
agentViewController.placeAgentCursor(cursor);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
1116
660
|
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
const MAX_AGENT_WINDOW = 5;
|
|
1122
|
-
let selectedAgentIndex = -1; // -1 = not in dashboard selection mode
|
|
1123
|
-
let targetAgent = null; // Selected agent for direct messaging
|
|
1124
|
-
let focusMode = "input"; // "input" or "dashboard"
|
|
1125
|
-
let dashboardView = "agents"; // "agents" or "mode"
|
|
1126
|
-
let selectedModeIndex = launchMode === "internal" ? 1 : 0;
|
|
1127
|
-
const providerOptions = [
|
|
1128
|
-
{ label: "codex", value: "codex-cli" },
|
|
1129
|
-
{ label: "claude", value: "claude-cli" },
|
|
1130
|
-
];
|
|
1131
|
-
let selectedProviderIndex = agentProvider === "claude-cli" ? 1 : 0;
|
|
1132
|
-
let restartInProgress = false;
|
|
661
|
+
function handleResizeInAgentView() {
|
|
662
|
+
if (!agentViewController) return false;
|
|
663
|
+
return agentViewController.handleResizeInAgentView();
|
|
664
|
+
}
|
|
1133
665
|
|
|
1134
666
|
function getAgentLabel(agentId) {
|
|
1135
|
-
return
|
|
667
|
+
return agentDirectory.getAgentLabel(activeAgentLabelMap, agentId);
|
|
668
|
+
}
|
|
669
|
+
|
|
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;
|
|
681
|
+
}
|
|
682
|
+
} catch {
|
|
683
|
+
// ignore lookup errors
|
|
684
|
+
}
|
|
685
|
+
return null;
|
|
686
|
+
},
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
|
|
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
|
+
});
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function clampAgentWindowWithSelection(selectionIndex) {
|
|
709
|
+
agentListWindowStart = agentDirectory.clampAgentWindowWithSelection({
|
|
710
|
+
activeCount: activeAgents.length,
|
|
711
|
+
maxWindow: MAX_AGENT_WINDOW,
|
|
712
|
+
windowStart: agentListWindowStart,
|
|
713
|
+
selectionIndex,
|
|
714
|
+
});
|
|
1136
715
|
}
|
|
1137
716
|
|
|
1138
717
|
function clampAgentWindow() {
|
|
1139
|
-
|
|
1140
|
-
agentListWindowStart = 0;
|
|
1141
|
-
return;
|
|
1142
|
-
}
|
|
1143
|
-
const maxItems = Math.max(1, Math.min(MAX_AGENT_WINDOW, activeAgents.length));
|
|
1144
|
-
if (selectedAgentIndex >= 0) {
|
|
1145
|
-
if (selectedAgentIndex < agentListWindowStart) {
|
|
1146
|
-
agentListWindowStart = selectedAgentIndex;
|
|
1147
|
-
} else if (selectedAgentIndex >= agentListWindowStart + maxItems) {
|
|
1148
|
-
agentListWindowStart = selectedAgentIndex - maxItems + 1;
|
|
1149
|
-
}
|
|
1150
|
-
}
|
|
1151
|
-
const maxStart = Math.max(0, activeAgents.length - maxItems);
|
|
1152
|
-
if (agentListWindowStart > maxStart) agentListWindowStart = maxStart;
|
|
1153
|
-
if (agentListWindowStart < 0) agentListWindowStart = 0;
|
|
718
|
+
clampAgentWindowWithSelection(selectedAgentIndex);
|
|
1154
719
|
}
|
|
1155
720
|
|
|
1156
721
|
function send(req) {
|
|
1157
|
-
if (!
|
|
1158
|
-
|
|
722
|
+
if (!daemonCoordinator) return;
|
|
723
|
+
daemonCoordinator.send(req);
|
|
1159
724
|
}
|
|
1160
725
|
|
|
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
|
+
},
|
|
739
|
+
});
|
|
740
|
+
|
|
1161
741
|
function updatePromptBox() {
|
|
1162
742
|
if (targetAgent) {
|
|
1163
743
|
const label = getAgentLabel(targetAgent);
|
|
1164
|
-
promptBox.setContent(
|
|
1165
|
-
promptBox.width = label.length + 3; //
|
|
744
|
+
promptBox.setContent(`>@${label}`);
|
|
745
|
+
promptBox.width = label.length + 3; // >@name + spacer
|
|
1166
746
|
input.left = promptBox.width;
|
|
1167
747
|
input.width = `100%-${promptBox.width}`;
|
|
1168
748
|
} else {
|
|
@@ -1171,8 +751,34 @@ async function runChat(projectRoot) {
|
|
|
1171
751
|
input.left = 2;
|
|
1172
752
|
input.width = "100%-2";
|
|
1173
753
|
}
|
|
754
|
+
if (!input.parent || !promptBox.parent) return;
|
|
1174
755
|
resizeInput();
|
|
1175
|
-
input._updateCursor
|
|
756
|
+
if (typeof input._updateCursor === "function") {
|
|
757
|
+
input._updateCursor();
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
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();
|
|
769
|
+
}
|
|
770
|
+
} else if (targetAgent) {
|
|
771
|
+
targetAgent = null;
|
|
772
|
+
updatePromptBox();
|
|
773
|
+
screen.render();
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
function restoreTargetFromSelection() {
|
|
778
|
+
if (selectedAgentIndex >= 0 && selectedAgentIndex < activeAgents.length) {
|
|
779
|
+
targetAgent = activeAgents[selectedAgentIndex];
|
|
780
|
+
updatePromptBox();
|
|
781
|
+
}
|
|
1176
782
|
}
|
|
1177
783
|
|
|
1178
784
|
function focusInput() {
|
|
@@ -1190,60 +796,90 @@ async function runChat(projectRoot) {
|
|
|
1190
796
|
screen.render();
|
|
1191
797
|
}
|
|
1192
798
|
|
|
799
|
+
let settingsController = null;
|
|
800
|
+
|
|
1193
801
|
function setLaunchMode(mode) {
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
selectedModeIndex = launchMode === "internal" ? 1 : 0;
|
|
1198
|
-
saveConfig(projectRoot, { launchMode });
|
|
1199
|
-
logMessage("status", `{magenta-fg}⚙{/magenta-fg} Launch mode: ${launchMode}`);
|
|
1200
|
-
renderDashboard();
|
|
1201
|
-
screen.render();
|
|
802
|
+
if (settingsController) {
|
|
803
|
+
settingsController.setLaunchMode(mode);
|
|
804
|
+
}
|
|
1202
805
|
}
|
|
1203
806
|
|
|
1204
|
-
function
|
|
1205
|
-
|
|
807
|
+
function requestCloseAgent(agentId) {
|
|
808
|
+
if (!agentId) {
|
|
809
|
+
logMessage("error", "{white-fg}✗{/white-fg} No agent selected");
|
|
810
|
+
return;
|
|
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 });
|
|
1206
815
|
}
|
|
1207
816
|
|
|
1208
817
|
function setAgentProvider(provider) {
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
selectedProviderIndex = agentProvider === "claude-cli" ? 1 : 0;
|
|
1213
|
-
saveConfig(projectRoot, { agentProvider });
|
|
1214
|
-
logMessage("status", `{magenta-fg}⚙{/magenta-fg} ufoo-agent: ${providerLabel(agentProvider)}`);
|
|
1215
|
-
renderDashboard();
|
|
1216
|
-
screen.render();
|
|
1217
|
-
void restartDaemon();
|
|
818
|
+
if (settingsController) {
|
|
819
|
+
settingsController.setAgentProvider(provider);
|
|
820
|
+
}
|
|
1218
821
|
}
|
|
1219
822
|
|
|
1220
|
-
|
|
1221
|
-
if (
|
|
1222
|
-
|
|
1223
|
-
logMessage("status", "{magenta-fg}⚙{/magenta-fg} Restarting daemon...");
|
|
1224
|
-
try {
|
|
1225
|
-
if (client) {
|
|
1226
|
-
client.removeAllListeners();
|
|
1227
|
-
try {
|
|
1228
|
-
client.end();
|
|
1229
|
-
} catch {
|
|
1230
|
-
// ignore
|
|
1231
|
-
}
|
|
1232
|
-
}
|
|
1233
|
-
stopDaemon(projectRoot);
|
|
1234
|
-
startDaemon(projectRoot);
|
|
1235
|
-
const newClient = await connectClient();
|
|
1236
|
-
if (newClient) {
|
|
1237
|
-
attachClient(newClient);
|
|
1238
|
-
logMessage("status", "{green-fg}✓{/green-fg} Daemon reconnected");
|
|
1239
|
-
} else {
|
|
1240
|
-
logMessage("error", "{red-fg}✗{/red-fg} Failed to reconnect to daemon");
|
|
1241
|
-
}
|
|
1242
|
-
} finally {
|
|
1243
|
-
restartInProgress = false;
|
|
823
|
+
function setAssistantEngine(value) {
|
|
824
|
+
if (settingsController) {
|
|
825
|
+
settingsController.setAssistantEngine(value);
|
|
1244
826
|
}
|
|
1245
827
|
}
|
|
1246
828
|
|
|
829
|
+
function setAutoResume(value) {
|
|
830
|
+
if (settingsController) {
|
|
831
|
+
settingsController.setAutoResume(value);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
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
|
+
});
|
|
882
|
+
|
|
1247
883
|
function clearLog() {
|
|
1248
884
|
logBox.setContent("");
|
|
1249
885
|
if (typeof logBox.scrollTo === "function") {
|
|
@@ -1253,89 +889,76 @@ async function runChat(projectRoot) {
|
|
|
1253
889
|
}
|
|
1254
890
|
|
|
1255
891
|
function renderDashboard() {
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
const start = agentListWindowStart;
|
|
1282
|
-
const end = start + maxItems;
|
|
1283
|
-
const visibleAgents = activeAgents.slice(start, end);
|
|
1284
|
-
const agentParts = visibleAgents.map((agent, i) => {
|
|
1285
|
-
const absoluteIndex = start + i;
|
|
1286
|
-
const label = getAgentLabel(agent);
|
|
1287
|
-
if (absoluteIndex === selectedAgentIndex) {
|
|
1288
|
-
return `{inverse}${label}{/inverse}`;
|
|
1289
|
-
}
|
|
1290
|
-
return `{cyan-fg}${label}{/cyan-fg}`;
|
|
1291
|
-
});
|
|
1292
|
-
const leftMore = start > 0 ? "{gray-fg}«{/gray-fg} " : "";
|
|
1293
|
-
const rightMore = end < activeAgents.length ? " {gray-fg}»{/gray-fg}" : "";
|
|
1294
|
-
content += `{gray-fg}Agents:{/gray-fg} ${agentParts.join(" ")}`;
|
|
1295
|
-
content = `${content.replace("{gray-fg}Agents:{/gray-fg} ", `{gray-fg}Agents:{/gray-fg} ${leftMore}`)}${rightMore}`;
|
|
1296
|
-
content += " {gray-fg}│ ←/→ select, Enter confirm, ↓ mode, ↑ back{/gray-fg}";
|
|
1297
|
-
} else {
|
|
1298
|
-
content += "{gray-fg}Agents:{/gray-fg} {cyan-fg}none{/cyan-fg}";
|
|
1299
|
-
content += " {gray-fg}│ ↓ mode, ↑ back{/gray-fg}";
|
|
1300
|
-
}
|
|
1301
|
-
}
|
|
1302
|
-
} else {
|
|
1303
|
-
// Normal dashboard display (input mode)
|
|
1304
|
-
const agents = activeAgents.length > 0
|
|
1305
|
-
? activeAgents.slice(0, 3).map((id) => getAgentLabel(id)).join(", ") + (activeAgents.length > 3 ? ` +${activeAgents.length - 3}` : "")
|
|
1306
|
-
: "none";
|
|
1307
|
-
content += `{gray-fg}Agents:{/gray-fg} {cyan-fg}${agents}{/cyan-fg}`;
|
|
1308
|
-
content += ` {gray-fg}Mode:{/gray-fg} {cyan-fg}${launchMode}{/cyan-fg}`;
|
|
1309
|
-
content += ` {gray-fg}Agent:{/gray-fg} {cyan-fg}${providerLabel(agentProvider)}{/cyan-fg}`;
|
|
1310
|
-
}
|
|
1311
|
-
dashboard.setContent(content);
|
|
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);
|
|
1312
917
|
}
|
|
1313
918
|
|
|
1314
919
|
function updateDashboard(status) {
|
|
1315
920
|
activeAgents = status.active || [];
|
|
921
|
+
reportPendingTotal = Number.isFinite(status?.reports?.pending_total)
|
|
922
|
+
? status.reports.pending_total
|
|
923
|
+
: 0;
|
|
1316
924
|
const metaList = Array.isArray(status.active_meta) ? status.active_meta : [];
|
|
1317
|
-
activeAgentLabelMap = new Map();
|
|
1318
925
|
let fallbackMap = null;
|
|
1319
926
|
if (metaList.length === 0 && activeAgents.length > 0) {
|
|
1320
927
|
try {
|
|
1321
|
-
const busPath =
|
|
928
|
+
const busPath = getUfooPaths(projectRoot).agentsFile;
|
|
1322
929
|
const bus = JSON.parse(fs.readFileSync(busPath, "utf8"));
|
|
1323
930
|
fallbackMap = new Map();
|
|
1324
|
-
for (const [id, meta] of Object.entries(bus.
|
|
931
|
+
for (const [id, meta] of Object.entries(bus.agents || {})) {
|
|
1325
932
|
if (meta && meta.nickname) fallbackMap.set(id, meta.nickname);
|
|
1326
933
|
}
|
|
1327
934
|
} catch {
|
|
1328
935
|
fallbackMap = null;
|
|
1329
936
|
}
|
|
1330
937
|
}
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
? meta.nickname
|
|
1335
|
-
: (fallbackMap && fallbackMap.get(id)) || id;
|
|
1336
|
-
activeAgentLabelMap.set(id, label);
|
|
1337
|
-
}
|
|
938
|
+
const maps = agentDirectory.buildAgentMaps(activeAgents, metaList, fallbackMap);
|
|
939
|
+
activeAgentLabelMap = maps.labelMap;
|
|
940
|
+
activeAgentMetaMap = maps.metaMap;
|
|
1338
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();
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
|
|
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();
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
1339
962
|
if (focusMode === "dashboard") {
|
|
1340
963
|
if (dashboardView === "agents") {
|
|
1341
964
|
if (activeAgents.length === 0) {
|
|
@@ -1346,6 +969,7 @@ async function runChat(projectRoot) {
|
|
|
1346
969
|
clampAgentWindow();
|
|
1347
970
|
}
|
|
1348
971
|
}
|
|
972
|
+
syncTargetFromSelection();
|
|
1349
973
|
renderDashboard();
|
|
1350
974
|
screen.render();
|
|
1351
975
|
}
|
|
@@ -1356,120 +980,96 @@ async function runChat(projectRoot) {
|
|
|
1356
980
|
selectedAgentIndex = activeAgents.length > 0 ? 0 : -1;
|
|
1357
981
|
agentListWindowStart = 0;
|
|
1358
982
|
clampAgentWindow();
|
|
1359
|
-
selectedModeIndex = launchMode === "internal" ? 1 : 0;
|
|
983
|
+
selectedModeIndex = launchMode === "internal" ? 2 : (launchMode === "tmux" ? 1 : 0);
|
|
1360
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();
|
|
994
|
+
}
|
|
1361
995
|
screen.grabKeys = true;
|
|
1362
996
|
renderDashboard();
|
|
1363
997
|
screen.program.hideCursor();
|
|
1364
998
|
screen.render();
|
|
999
|
+
syncTargetFromSelection();
|
|
1365
1000
|
}
|
|
1366
1001
|
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
return true;
|
|
1394
|
-
}
|
|
1395
|
-
if (key.name === "enter" || key.name === "return") {
|
|
1396
|
-
const modes = ["terminal", "internal"];
|
|
1397
|
-
setLaunchMode(modes[selectedModeIndex]);
|
|
1398
|
-
exitDashboardMode(false);
|
|
1399
|
-
return true;
|
|
1400
|
-
}
|
|
1401
|
-
if (key.name === "escape") {
|
|
1402
|
-
exitDashboardMode(false);
|
|
1403
|
-
return true;
|
|
1404
|
-
}
|
|
1405
|
-
return true;
|
|
1406
|
-
}
|
|
1407
|
-
if (dashboardView === "provider") {
|
|
1408
|
-
if (key.name === "left") {
|
|
1409
|
-
selectedProviderIndex = selectedProviderIndex <= 0 ? providerOptions.length - 1 : selectedProviderIndex - 1;
|
|
1410
|
-
renderDashboard();
|
|
1411
|
-
screen.render();
|
|
1412
|
-
return true;
|
|
1413
|
-
}
|
|
1414
|
-
if (key.name === "right") {
|
|
1415
|
-
selectedProviderIndex = selectedProviderIndex >= providerOptions.length - 1 ? 0 : selectedProviderIndex + 1;
|
|
1416
|
-
renderDashboard();
|
|
1417
|
-
screen.render();
|
|
1418
|
-
return true;
|
|
1419
|
-
}
|
|
1420
|
-
if (key.name === "up") {
|
|
1421
|
-
dashboardView = "mode";
|
|
1422
|
-
renderDashboard();
|
|
1423
|
-
screen.render();
|
|
1424
|
-
return true;
|
|
1425
|
-
}
|
|
1426
|
-
if (key.name === "enter" || key.name === "return") {
|
|
1427
|
-
const selected = providerOptions[selectedProviderIndex];
|
|
1428
|
-
if (selected) setAgentProvider(selected.value);
|
|
1429
|
-
exitDashboardMode(false);
|
|
1430
|
-
return true;
|
|
1431
|
-
}
|
|
1432
|
-
if (key.name === "escape") {
|
|
1433
|
-
exitDashboardMode(false);
|
|
1434
|
-
return true;
|
|
1435
|
-
}
|
|
1436
|
-
return true;
|
|
1437
|
-
}
|
|
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
|
+
});
|
|
1438
1028
|
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
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);
|
|
1473
1073
|
}
|
|
1474
1074
|
|
|
1475
1075
|
function exitDashboardMode(selectAgent = false) {
|
|
@@ -1492,199 +1092,240 @@ async function runChat(projectRoot) {
|
|
|
1492
1092
|
screen.render();
|
|
1493
1093
|
}
|
|
1494
1094
|
|
|
1495
|
-
function
|
|
1496
|
-
|
|
1095
|
+
function getInjectSockPath(agentId) {
|
|
1096
|
+
const safeName = subscriberToSafeName(agentId);
|
|
1097
|
+
return path.join(getUfooPaths(projectRoot).busQueuesDir, safeName, "inject.sock");
|
|
1497
1098
|
}
|
|
1498
1099
|
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
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
|
+
});
|
|
1510
1155
|
|
|
1511
|
-
|
|
1512
|
-
if (!
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
let buffer = "";
|
|
1516
|
-
client.on("data", (data) => {
|
|
1517
|
-
buffer += data.toString("utf8");
|
|
1518
|
-
const lines = buffer.split(/\r?\n/);
|
|
1519
|
-
buffer = lines.pop() || "";
|
|
1520
|
-
for (const line of lines.filter((l) => l.trim())) {
|
|
1521
|
-
try {
|
|
1522
|
-
const msg = JSON.parse(line);
|
|
1523
|
-
if (msg.type === "status") {
|
|
1524
|
-
const data = msg.data || {};
|
|
1525
|
-
if (typeof data.phase === "string") {
|
|
1526
|
-
const text = data.text || "";
|
|
1527
|
-
const item = { key: data.key, text };
|
|
1528
|
-
if (data.phase === "start") {
|
|
1529
|
-
enqueueBusStatus(item);
|
|
1530
|
-
} else if (data.phase === "done" || data.phase === "error") {
|
|
1531
|
-
resolveBusStatus(item);
|
|
1532
|
-
if (text) {
|
|
1533
|
-
const prefix = data.phase === "error"
|
|
1534
|
-
? "{red-fg}✗{/red-fg}"
|
|
1535
|
-
: "{green-fg}✓{/green-fg}";
|
|
1536
|
-
logMessage("status", `${prefix} ${text}`, data);
|
|
1537
|
-
}
|
|
1538
|
-
} else {
|
|
1539
|
-
enqueueBusStatus(item);
|
|
1540
|
-
}
|
|
1541
|
-
screen.render();
|
|
1542
|
-
} else {
|
|
1543
|
-
updateDashboard(data);
|
|
1544
|
-
}
|
|
1545
|
-
} else if (msg.type === "response") {
|
|
1546
|
-
const payload = msg.data || {};
|
|
1547
|
-
if (payload.reply) {
|
|
1548
|
-
resolveStatusLine(`{green-fg}←{/green-fg} ${payload.reply}`);
|
|
1549
|
-
logMessage("reply", `{green-fg}←{/green-fg} ${payload.reply}`);
|
|
1550
|
-
}
|
|
1551
|
-
if (payload.dispatch && payload.dispatch.length > 0) {
|
|
1552
|
-
logMessage("dispatch", `{blue-fg}→{/blue-fg} Dispatched to: ${payload.dispatch.map(d => d.target || d).join(", ")}`);
|
|
1553
|
-
}
|
|
1554
|
-
if (payload.disambiguate && Array.isArray(payload.disambiguate.candidates) && payload.disambiguate.candidates.length > 0) {
|
|
1555
|
-
pending = { disambiguate: payload.disambiguate, original: pending?.original };
|
|
1556
|
-
resolveStatusLine(`{yellow-fg}?{/yellow-fg} ${payload.disambiguate.prompt || "Choose target:"}`);
|
|
1557
|
-
logMessage("disambiguate", `{yellow-fg}?{/yellow-fg} ${payload.disambiguate.prompt || "Choose target:"}`);
|
|
1558
|
-
payload.disambiguate.candidates.forEach((c, i) => {
|
|
1559
|
-
logMessage("disambiguate", ` {cyan-fg}${i + 1}){/cyan-fg} ${c.agent_id} {gray-fg}— ${c.reason || ""}{/gray-fg}`);
|
|
1560
|
-
});
|
|
1561
|
-
} else {
|
|
1562
|
-
pending = null;
|
|
1563
|
-
}
|
|
1564
|
-
if (!payload.reply && !payload.disambiguate) {
|
|
1565
|
-
resolveStatusLine("{gray-fg}✓{/gray-fg} Done");
|
|
1566
|
-
}
|
|
1567
|
-
if (msg.opsResults && msg.opsResults.length > 0) {
|
|
1568
|
-
logMessage("ops", `{magenta-fg}⚡{/magenta-fg} ${JSON.stringify(msg.opsResults)}`);
|
|
1569
|
-
}
|
|
1570
|
-
screen.render();
|
|
1571
|
-
} else if (msg.type === "bus") {
|
|
1572
|
-
const data = msg.data || {};
|
|
1573
|
-
const prefix = data.event === "broadcast" ? "{magenta-fg}⇢{/magenta-fg}" : "{blue-fg}↔{/blue-fg}";
|
|
1574
|
-
let publisher = data.publisher && data.publisher !== "unknown"
|
|
1575
|
-
? data.publisher
|
|
1576
|
-
: (data.event === "broadcast" ? "broadcast" : "bus");
|
|
1577
|
-
|
|
1578
|
-
// Try to parse message as JSON (from internal agents)
|
|
1579
|
-
let displayMessage = data.message || "";
|
|
1580
|
-
try {
|
|
1581
|
-
const parsed = JSON.parse(data.message);
|
|
1582
|
-
if (parsed && typeof parsed === "object" && parsed.reply) {
|
|
1583
|
-
displayMessage = parsed.reply;
|
|
1584
|
-
}
|
|
1585
|
-
} catch {
|
|
1586
|
-
// Not JSON, use as-is
|
|
1587
|
-
}
|
|
1156
|
+
function requestStatus() {
|
|
1157
|
+
if (!daemonCoordinator) return;
|
|
1158
|
+
daemonCoordinator.requestStatus();
|
|
1159
|
+
}
|
|
1588
1160
|
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
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
|
+
});
|
|
1609
1187
|
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1188
|
+
daemonCoordinator = createDaemonCoordinator({
|
|
1189
|
+
projectRoot,
|
|
1190
|
+
daemonTransport,
|
|
1191
|
+
handleMessage: (msg) => daemonMessageRouter.handleMessage(msg),
|
|
1192
|
+
queueStatusLine,
|
|
1193
|
+
resolveStatusLine,
|
|
1194
|
+
logMessage,
|
|
1195
|
+
stopDaemon,
|
|
1196
|
+
startDaemon,
|
|
1197
|
+
});
|
|
1198
|
+
|
|
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.");
|
|
1624
1207
|
}
|
|
1208
|
+
throw new Error("Failed to connect to ufoo daemon (timeout). The daemon may still be starting.");
|
|
1209
|
+
}
|
|
1210
|
+
|
|
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
|
+
},
|
|
1625
1234
|
});
|
|
1626
|
-
client.on("close", () => {
|
|
1627
|
-
client = null;
|
|
1628
|
-
});
|
|
1629
|
-
};
|
|
1630
1235
|
|
|
1631
|
-
|
|
1236
|
+
async function executeCommand(text) {
|
|
1237
|
+
return commandExecutor.executeCommand(text);
|
|
1238
|
+
}
|
|
1632
1239
|
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
return;
|
|
1640
|
-
}
|
|
1641
|
-
inputHistory.push(text);
|
|
1642
|
-
appendInputHistory(text);
|
|
1643
|
-
historyIndex = inputHistory.length;
|
|
1644
|
-
historyDraft = "";
|
|
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
|
+
});
|
|
1645
1246
|
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
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
|
+
});
|
|
1657
1277
|
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
send({
|
|
1664
|
-
type: "prompt",
|
|
1665
|
-
text: `Use agent ${choice.agent_id} to handle: ${pending.original || "the request"}`,
|
|
1666
|
-
});
|
|
1667
|
-
pending = null;
|
|
1668
|
-
} else {
|
|
1669
|
-
logMessage("error", "Invalid selection.");
|
|
1670
|
-
}
|
|
1671
|
-
} else {
|
|
1672
|
-
pending = { original: text };
|
|
1673
|
-
queueStatusLine("ufoo-agent processing");
|
|
1674
|
-
send({ type: "prompt", text });
|
|
1675
|
-
logMessage("user", `{cyan-fg}→{/cyan-fg} ${text}`);
|
|
1676
|
-
}
|
|
1677
|
-
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
|
|
1678
1283
|
});
|
|
1679
1284
|
|
|
1680
1285
|
screen.key(["C-c"], exitHandler);
|
|
1681
1286
|
|
|
1287
|
+
// Agent TTY view: enter dashboard mode
|
|
1288
|
+
function enterAgentDashboardMode() {
|
|
1289
|
+
if (agentViewController) {
|
|
1290
|
+
agentViewController.enterAgentDashboardMode();
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1682
1294
|
// Dashboard navigation - use screen.on to capture even when input is focused
|
|
1683
1295
|
screen.on("keypress", (ch, key) => {
|
|
1296
|
+
// Agent TTY view: handle keystrokes
|
|
1297
|
+
if (getCurrentView() === "agent") {
|
|
1298
|
+
if (focusMode === "dashboard") {
|
|
1299
|
+
handleDashboardKey(key);
|
|
1300
|
+
return;
|
|
1301
|
+
}
|
|
1302
|
+
// Suppress input briefly after entering agent view
|
|
1303
|
+
if (Date.now() < getAgentInputSuppressUntil()) {
|
|
1304
|
+
return;
|
|
1305
|
+
}
|
|
1306
|
+
// Ctrl+C exits entire app
|
|
1307
|
+
if (key && key.ctrl && key.name === "c") {
|
|
1308
|
+
return; // handled by screen.key(["C-c"])
|
|
1309
|
+
}
|
|
1310
|
+
// Down arrow: enter agents bar (same pattern as normal chat dashboard)
|
|
1311
|
+
if (key && key.name === "down") {
|
|
1312
|
+
enterAgentDashboardMode();
|
|
1313
|
+
return;
|
|
1314
|
+
}
|
|
1315
|
+
// All other keys (including Esc) go to agent PTY
|
|
1316
|
+
const raw = keyToRaw(ch, key);
|
|
1317
|
+
if (raw) {
|
|
1318
|
+
sendRawToAgent(raw);
|
|
1319
|
+
}
|
|
1320
|
+
return;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
// Normal mode: dashboard key handling
|
|
1684
1324
|
handleDashboardKey(key);
|
|
1685
1325
|
});
|
|
1686
1326
|
|
|
1687
1327
|
screen.key(["tab"], () => {
|
|
1328
|
+
if (getCurrentView() === "agent") return; // Tab goes to PTY via keypress handler
|
|
1688
1329
|
if (focusMode === "dashboard") {
|
|
1689
1330
|
exitDashboardMode(false);
|
|
1690
1331
|
} else {
|
|
@@ -1693,10 +1334,13 @@ async function runChat(projectRoot) {
|
|
|
1693
1334
|
});
|
|
1694
1335
|
|
|
1695
1336
|
screen.key(["C-k", "M-k"], () => {
|
|
1337
|
+
if (getCurrentView() === "agent") return;
|
|
1696
1338
|
clearLog();
|
|
1697
1339
|
});
|
|
1698
1340
|
|
|
1341
|
+
|
|
1699
1342
|
screen.key(["i", "enter"], () => {
|
|
1343
|
+
if (getCurrentView() === "agent") return;
|
|
1700
1344
|
if (focusMode === "dashboard") return;
|
|
1701
1345
|
if (screen.focused === input) return;
|
|
1702
1346
|
focusInput();
|
|
@@ -1715,43 +1359,7 @@ async function runChat(projectRoot) {
|
|
|
1715
1359
|
}
|
|
1716
1360
|
if (screen.program) {
|
|
1717
1361
|
screen.program.on("data", (data) => {
|
|
1718
|
-
|
|
1719
|
-
const chunk = data.toString("utf8");
|
|
1720
|
-
if (!pasteActive && !chunk.includes(PASTE_START) && !pasteRemainder.includes(PASTE_START)) {
|
|
1721
|
-
const keep = PASTE_START.length - 1;
|
|
1722
|
-
pasteRemainder = (pasteRemainder + chunk).slice(-keep);
|
|
1723
|
-
return;
|
|
1724
|
-
}
|
|
1725
|
-
let buffer = pasteRemainder + chunk;
|
|
1726
|
-
pasteRemainder = "";
|
|
1727
|
-
while (buffer.length > 0) {
|
|
1728
|
-
if (!pasteActive) {
|
|
1729
|
-
const start = buffer.indexOf(PASTE_START);
|
|
1730
|
-
if (start === -1) {
|
|
1731
|
-
const keep = PASTE_START.length - 1;
|
|
1732
|
-
pasteRemainder = buffer.slice(-keep);
|
|
1733
|
-
return;
|
|
1734
|
-
}
|
|
1735
|
-
buffer = buffer.slice(start + PASTE_START.length);
|
|
1736
|
-
pasteActive = true;
|
|
1737
|
-
pasteBuffer = "";
|
|
1738
|
-
scheduleSuppressReset();
|
|
1739
|
-
continue;
|
|
1740
|
-
}
|
|
1741
|
-
const end = buffer.indexOf(PASTE_END);
|
|
1742
|
-
if (end === -1) {
|
|
1743
|
-
pasteBuffer += buffer;
|
|
1744
|
-
scheduleSuppressReset();
|
|
1745
|
-
return;
|
|
1746
|
-
}
|
|
1747
|
-
pasteBuffer += buffer.slice(0, end);
|
|
1748
|
-
buffer = buffer.slice(end + PASTE_END.length);
|
|
1749
|
-
pasteActive = false;
|
|
1750
|
-
scheduleSuppressReset();
|
|
1751
|
-
const normalized = normalizePaste(pasteBuffer);
|
|
1752
|
-
pasteBuffer = "";
|
|
1753
|
-
if (normalized) insertTextAtCursor(normalized);
|
|
1754
|
-
}
|
|
1362
|
+
pasteController.handleProgramData(data);
|
|
1755
1363
|
});
|
|
1756
1364
|
}
|
|
1757
1365
|
loadHistory();
|
|
@@ -1759,11 +1367,22 @@ async function runChat(projectRoot) {
|
|
|
1759
1367
|
renderDashboard();
|
|
1760
1368
|
resizeInput();
|
|
1761
1369
|
requestStatus();
|
|
1762
|
-
|
|
1370
|
+
|
|
1371
|
+
// 定期刷新 dashboard 状态(兜底,daemon 会主动推送变化)
|
|
1372
|
+
setInterval(() => {
|
|
1373
|
+
if (daemonCoordinator && daemonCoordinator.isConnected()) {
|
|
1374
|
+
requestStatus();
|
|
1375
|
+
}
|
|
1376
|
+
}, 30000);
|
|
1763
1377
|
screen.on("resize", () => {
|
|
1378
|
+
if (handleResizeInAgentView()) {
|
|
1379
|
+
return;
|
|
1380
|
+
}
|
|
1764
1381
|
resizeInput();
|
|
1765
|
-
if (
|
|
1382
|
+
if (completionController.isActive()) completionController.hide();
|
|
1766
1383
|
input._updateCursor();
|
|
1384
|
+
// Force recalculate logBox width to match terminal
|
|
1385
|
+
logBox.width = screen.width;
|
|
1767
1386
|
screen.render();
|
|
1768
1387
|
});
|
|
1769
1388
|
screen.render();
|