u-foo 2.3.32 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +157 -213
- package/README.zh-CN.md +151 -197
- package/SKILLS/ufoo/SKILL.md +8 -8
- package/bin/uagy.js +69 -0
- package/bin/uclaude.js +2 -2
- package/bin/ucode.js +4 -4
- package/bin/ucodex.js +2 -2
- package/bin/ufoo.js +5 -23
- package/modules/AGENTS.template.md +1 -1
- package/modules/bus/SKILLS/ubus/SKILL.md +35 -10
- package/package.json +5 -5
- package/scripts/chat-app-smoke.js +1 -1
- package/scripts/global-chat-switch-benchmark.js +5 -5
- package/scripts/ink-demo.js +1 -1
- package/scripts/ink-smoke.js +1 -1
- package/scripts/ucode-app-smoke.js +1 -1
- package/src/{agent → agents/activity}/activityDetector.js +39 -2
- package/src/{agent → agents/activity}/activityStatePublisher.js +1 -1
- package/src/{agent → agents/activity}/activityStateWriter.js +2 -2
- package/src/{agent → agents/activity}/activityTracker.js +1 -1
- package/src/agents/activity/index.js +8 -0
- package/src/{agent → agents/controller}/controllerToolExecutor.js +4 -4
- package/src/agents/controller/index.js +8 -0
- package/src/{agent → agents/controller}/loopObservability.js +2 -2
- package/src/{agent → agents/controller}/loopRuntime.js +1 -1
- package/src/{agent → agents/controller}/ufooAgent.js +9 -9
- package/src/agents/index.js +10 -0
- package/src/agents/internal/index.js +3 -0
- package/src/{agent → agents/internal}/internalRunner.js +45 -22
- package/src/agents/launch/agyConversation.js +159 -0
- package/src/agents/launch/index.js +12 -0
- package/src/{agent → agents/launch}/launchEnvironment.js +2 -3
- package/src/{agent → agents/launch}/launcher.js +64 -21
- package/src/{agent → agents/launch}/notifier.js +23 -12
- package/src/{agent → agents/launch}/ptyRunner.js +44 -12
- package/src/{agent → agents/launch}/ptyWrapper.js +2 -2
- package/src/{agent → agents/launch}/publisherRouting.js +1 -1
- package/src/{agent → agents/launch}/readyDetector.js +23 -0
- package/src/{agent → agents/prompts}/defaultBootstrap.js +63 -4
- package/src/{group/bootstrap.js → agents/prompts/groupBootstrap.js} +41 -6
- package/src/agents/prompts/index.js +8 -0
- package/src/{code/prompts → agents/prompts/native}/index.js +1 -1
- package/src/{agent → agents/providers}/claudeThreadProvider.js +1 -1
- package/src/{agent → agents/providers}/codexThreadProvider.js +1 -1
- package/src/{agent → agents/providers}/directAuthStatus.js +184 -1
- package/src/agents/providers/index.js +13 -0
- package/src/{agent → agents/providers}/upstreamTransport.js +2 -2
- package/src/{chat → app/chat}/agentSockets.js +1 -1
- package/src/{chat → app/chat}/commandExecutor.js +50 -26
- package/src/{chat → app/chat}/commands.js +119 -5
- package/src/{chat → app/chat}/daemonConnection.js +1 -1
- package/src/{chat → app/chat}/daemonMessageRouter.js +45 -3
- package/src/{chat → app/chat}/dashboardView.js +2 -1
- package/src/app/chat/index.js +6 -0
- package/src/{chat → app/chat}/inputSubmitHandler.js +4 -13
- package/src/{chat → app/chat}/internalAgentLogHistory.js +1 -1
- package/src/app/chat/multiWindow/index.js +268 -0
- package/src/app/chat/multiWindow/paneLayout.js +84 -0
- package/src/app/chat/multiWindow/paneManager.js +299 -0
- package/src/app/chat/multiWindow/renderer.js +384 -0
- package/src/app/chat/multiWindow/virtualTerminal.js +327 -0
- package/src/{chat → app/chat}/transport.js +1 -1
- package/src/{cli → app/cli}/ctxCoreCommands.js +3 -3
- package/src/{doctor/index.js → app/cli/features/doctor.js} +1 -1
- package/src/{init/index.js → app/cli/features/init.js} +14 -32
- package/src/{cli → app/cli}/groupCoreCommands.js +2 -2
- package/src/app/cli/index.js +9 -0
- package/src/{cli → app/cli}/onlineCoreCommands.js +5 -5
- package/src/{cli.js → app/cli/run.js} +59 -57
- package/src/app/index.js +6 -0
- package/src/code/agent.js +10 -9
- package/src/code/index.js +2 -0
- package/src/code/launcher/index.js +9 -0
- package/src/{agent → code/launcher}/ucode.js +7 -8
- package/src/{agent → code/launcher}/ucodeBootstrap.js +3 -3
- package/src/{agent → code/launcher}/ucodeBuild.js +2 -2
- package/src/{agent → code/launcher}/ucodeDoctor.js +2 -2
- package/src/{agent → code/launcher}/ucodeRuntimeConfig.js +1 -2
- package/src/code/nativeRunner.js +4 -4
- package/src/code/tui.js +3 -1454
- package/src/config.js +15 -2
- package/src/{bus → coordination/bus}/activate.js +2 -2
- package/src/{bus → coordination/bus}/daemon.js +15 -5
- package/src/coordination/bus/envelope.js +173 -0
- package/src/{bus → coordination/bus}/index.js +7 -3
- package/src/{bus → coordination/bus}/inject.js +11 -3
- package/src/{bus → coordination/bus}/message.js +1 -1
- package/src/coordination/bus/messageMeta.js +130 -0
- package/src/coordination/bus/promptEnvelope.js +65 -0
- package/src/{bus → coordination/bus}/shake.js +1 -1
- package/src/{bus → coordination/bus}/store.js +3 -3
- package/src/{bus → coordination/bus}/subscriber.js +2 -2
- package/src/{bus → coordination/bus}/utils.js +2 -2
- package/src/{history → coordination/history}/inputTimeline.js +5 -5
- package/src/coordination/index.js +10 -0
- package/src/{memory → coordination/memory}/historySearch.js +1 -1
- package/src/{memory → coordination/memory}/index.js +3 -3
- package/src/{report → coordination/report}/store.js +2 -2
- package/src/{status → coordination/status}/index.js +3 -3
- package/src/online/bridge.js +2 -2
- package/src/{controller → orchestration/controller}/flags.js +1 -1
- package/src/{controller → orchestration/controller}/gateRouter.js +1 -1
- package/src/orchestration/controller/index.js +10 -0
- package/src/{controller → orchestration/controller}/shadowGuard.js +1 -1
- package/src/orchestration/groups/bootstrap.js +3 -0
- package/src/orchestration/groups/index.js +10 -0
- package/src/orchestration/groups/promptProfiles.js +3 -0
- package/src/{group → orchestration/groups}/templates.js +1 -1
- package/src/{group → orchestration/groups}/validateTemplate.js +1 -1
- package/src/orchestration/index.js +7 -0
- package/src/orchestration/solo/index.js +3 -0
- package/src/{daemon → runtime/daemon}/agentProcessManager.js +1 -1
- package/src/{daemon → runtime/daemon}/cronOps.js +3 -2
- package/src/{daemon → runtime/daemon}/groupOrchestrator.js +26 -9
- package/src/{daemon → runtime/daemon}/index.js +105 -53
- package/src/{daemon → runtime/daemon}/ipcServer.js +1 -1
- package/src/{daemon → runtime/daemon}/nicknameScope.js +6 -3
- package/src/{daemon → runtime/daemon}/ops.js +48 -61
- package/src/{daemon → runtime/daemon}/promptLoop.js +1 -1
- package/src/{daemon → runtime/daemon}/promptRequest.js +7 -7
- package/src/runtime/daemon/providerSessions.js +230 -0
- package/src/{daemon → runtime/daemon}/reporting.js +4 -4
- package/src/{daemon → runtime/daemon}/run.js +4 -4
- package/src/{daemon → runtime/daemon}/soloBootstrap.js +7 -7
- package/src/{daemon → runtime/daemon}/status.js +5 -5
- package/src/runtime/index.js +10 -0
- package/src/{projects → runtime/projects}/registry.js +1 -1
- package/src/{terminal → runtime/terminal}/adapterRouter.js +0 -10
- package/src/{terminal → runtime/terminal}/adapters/internalAdapter.js +0 -4
- package/src/tools/handlers/common.js +1 -1
- package/src/tools/handlers/listAgents.js +1 -1
- package/src/tools/handlers/memory.js +3 -3
- package/src/tools/handlers/readBusSummary.js +1 -1
- package/src/tools/handlers/readOpenDecisions.js +1 -1
- package/src/tools/handlers/readProjectRegistry.js +1 -1
- package/src/tools/handlers/readPromptHistory.js +2 -2
- package/src/tools/schemaFixtures.js +1 -1
- package/src/ui/MIGRATION.md +42 -88
- package/src/ui/format/index.js +5 -28
- package/src/ui/index.js +1 -1
- package/src/ui/{components → ink}/ChatApp.js +812 -88
- package/src/ui/ink/DashboardBar.js +685 -0
- package/src/ui/{components → ink}/MultilineInput.js +230 -5
- package/src/ui/{components → ink}/UcodeApp.js +16 -7
- package/src/ui/{components → ink}/agentMirror.js +24 -19
- package/src/ui/{components → ink}/chatReducer.js +29 -7
- package/src/bus/messageMeta.js +0 -52
- package/src/chat/agentViewController.js +0 -1072
- package/src/chat/chatLogController.js +0 -138
- package/src/chat/completionController.js +0 -533
- package/src/chat/dashboardKeyController.js +0 -533
- package/src/chat/index.js +0 -2222
- package/src/chat/inputHistoryController.js +0 -135
- package/src/chat/inputListenerController.js +0 -470
- package/src/chat/layout.js +0 -186
- package/src/chat/pasteController.js +0 -81
- package/src/chat/statusLineController.js +0 -223
- package/src/chat/streamTracker.js +0 -156
- package/src/code/config +0 -0
- package/src/daemon/providerSessions.js +0 -488
- package/src/terminal/adapters/internalPtyAdapter.js +0 -42
- package/src/ui/components/DashboardBar.js +0 -417
- /package/src/{code/prompts → agents/prompts/native}/actions.js +0 -0
- /package/src/{code/prompts → agents/prompts/native}/efficiency.js +0 -0
- /package/src/{code/prompts → agents/prompts/native}/environment.js +0 -0
- /package/src/{code/prompts → agents/prompts/native}/identity.js +0 -0
- /package/src/{code/prompts → agents/prompts/native}/safety.js +0 -0
- /package/src/{code/prompts → agents/prompts/native}/sections.js +0 -0
- /package/src/{code/prompts → agents/prompts/native}/system.js +0 -0
- /package/src/{code/prompts → agents/prompts/native}/tasks.js +0 -0
- /package/src/{code/prompts → agents/prompts/native}/toolDescriptions/bash.js +0 -0
- /package/src/{code/prompts → agents/prompts/native}/toolDescriptions/edit.js +0 -0
- /package/src/{code/prompts → agents/prompts/native}/toolDescriptions/read.js +0 -0
- /package/src/{code/prompts → agents/prompts/native}/toolDescriptions/write.js +0 -0
- /package/src/{code/prompts → agents/prompts/native}/ufoo.js +0 -0
- /package/src/{group → agents/prompts}/promptProfiles.js +0 -0
- /package/src/{agent → agents/providers}/claudeEventTranslator.js +0 -0
- /package/src/{agent → agents/providers}/claudeOauthTokenReader.js +0 -0
- /package/src/{agent → agents/providers}/claudeSessionFiles.js +0 -0
- /package/src/{agent → agents/providers}/codexEventTranslator.js +0 -0
- /package/src/{agent → agents/providers}/credentials/claude.js +0 -0
- /package/src/{agent → agents/providers}/credentials/codex.js +0 -0
- /package/src/{agent → agents/providers}/credentials/index.js +0 -0
- /package/src/{chat → app/chat}/agentBar.js +0 -0
- /package/src/{chat → app/chat}/agentDirectory.js +0 -0
- /package/src/{chat → app/chat}/cronScheduler.js +0 -0
- /package/src/{chat → app/chat}/daemonCoordinator.js +0 -0
- /package/src/{chat → app/chat}/daemonReconnect.js +0 -0
- /package/src/{chat → app/chat}/daemonTransport.js +0 -0
- /package/src/{chat → app/chat}/daemonTransportDefaults.js +0 -0
- /package/src/{chat → app/chat}/inputMath.js +0 -0
- /package/src/{chat → app/chat}/projectCloseController.js +0 -0
- /package/src/{chat → app/chat}/rawKeyMap.js +0 -0
- /package/src/{chat → app/chat}/settingsController.js +0 -0
- /package/src/{chat → app/chat}/shellCommand.js +0 -0
- /package/src/{chat → app/chat}/text.js +0 -0
- /package/src/{chat → app/chat}/transientAgentState.js +0 -0
- /package/src/{cli → app/cli}/busCoreCommands.js +0 -0
- /package/src/{skills/index.js → app/cli/features/skills.js} +0 -0
- /package/src/{bus → coordination/bus}/nickname.js +0 -0
- /package/src/{bus → coordination/bus}/queue.js +0 -0
- /package/src/{context → coordination/context}/decisions.js +0 -0
- /package/src/{context → coordination/context}/doctor.js +0 -0
- /package/src/{context → coordination/context}/index.js +0 -0
- /package/src/{context → coordination/context}/sync.js +0 -0
- /package/src/{ufoo → coordination/state}/agentRegistryDiagnostics.js +0 -0
- /package/src/{ufoo → coordination/state}/agentsStore.js +0 -0
- /package/src/{ufoo → coordination/state}/paths.js +0 -0
- /package/src/{controller → orchestration/controller}/launchRouting.js +0 -0
- /package/src/{controller → orchestration/controller}/routerFastPath.js +0 -0
- /package/src/{controller → orchestration/controller}/routerFinalize.js +0 -0
- /package/src/{group → orchestration/groups}/diagram.js +0 -0
- /package/src/{group → orchestration/groups}/templateValidation.js +0 -0
- /package/src/{solo → orchestration/solo}/commands.js +0 -0
- /package/src/{shared → runtime/contracts}/eventContract.js +0 -0
- /package/src/{shared → runtime/contracts}/ptySocketContract.js +0 -0
- /package/src/{providerapi → runtime/privacy}/redactor.js +0 -0
- /package/src/{providerapi → runtime/privacy}/shadowDiff.js +0 -0
- /package/src/{utils → runtime/process}/nodeExecutable.js +0 -0
- /package/src/{projects → runtime/projects}/identity.js +0 -0
- /package/src/{projects → runtime/projects}/index.js +0 -0
- /package/src/{projects → runtime/projects}/projectId.js +0 -0
- /package/src/{projects → runtime/projects}/runtimes.js +0 -0
- /package/src/{terminal → runtime/terminal}/adapterContract.js +0 -0
- /package/src/{terminal → runtime/terminal}/adapters/externalAdapter.js +0 -0
- /package/src/{terminal → runtime/terminal}/adapters/hostAdapter.js +0 -0
- /package/src/{terminal → runtime/terminal}/adapters/internalQueueAdapter.js +0 -0
- /package/src/{terminal → runtime/terminal}/adapters/terminalAdapter.js +0 -0
- /package/src/{terminal → runtime/terminal}/adapters/tmuxAdapter.js +0 -0
- /package/src/{terminal → runtime/terminal}/detect.js +0 -0
- /package/src/{terminal → runtime/terminal}/index.js +0 -0
- /package/src/{terminal → runtime/terminal}/iterm2.js +0 -0
- /package/src/{utils → ui/format}/banner.js +0 -0
- /package/src/{shared → ui/format}/markdownRenderer.js +0 -0
- /package/src/ui/{components → ink}/InkDemo.js +0 -0
package/src/chat/index.js
DELETED
|
@@ -1,2222 +0,0 @@
|
|
|
1
|
-
const path = require("path");
|
|
2
|
-
const os = require("os");
|
|
3
|
-
const crypto = require("crypto");
|
|
4
|
-
const blessed = require("blessed");
|
|
5
|
-
const { execSync } = require("child_process");
|
|
6
|
-
const fs = require("fs");
|
|
7
|
-
const {
|
|
8
|
-
loadConfig,
|
|
9
|
-
saveConfig,
|
|
10
|
-
normalizeLaunchMode,
|
|
11
|
-
normalizeAgentProvider,
|
|
12
|
-
} = require("../config");
|
|
13
|
-
const { socketPath, isRunning } = require("../daemon");
|
|
14
|
-
const UfooInit = require("../init");
|
|
15
|
-
const AgentActivator = require("../bus/activate");
|
|
16
|
-
const { subscriberToSafeName } = require("../bus/utils");
|
|
17
|
-
const { getUfooPaths } = require("../ufoo/paths");
|
|
18
|
-
const { resolveDisplayNickname } = require("../daemon/nicknameScope");
|
|
19
|
-
const { startDaemon, stopDaemon, connectWithRetry } = require("./transport");
|
|
20
|
-
const { escapeBlessed, stripBlessedTags, truncateText } = require("./text");
|
|
21
|
-
const { COMMAND_REGISTRY, parseCommand, parseAtTarget } = require("./commands");
|
|
22
|
-
const { buildPromptProfileCandidates } = require("../solo/commands");
|
|
23
|
-
const inputMath = require("./inputMath");
|
|
24
|
-
const { createStreamTracker } = require("./streamTracker");
|
|
25
|
-
const agentDirectory = require("./agentDirectory");
|
|
26
|
-
const { computeAgentBar } = require("./agentBar");
|
|
27
|
-
const { createAgentSockets } = require("./agentSockets");
|
|
28
|
-
const { createDashboardKeyController } = require("./dashboardKeyController");
|
|
29
|
-
const { computeDashboardContent } = require("./dashboardView");
|
|
30
|
-
const { createCommandExecutor } = require("./commandExecutor");
|
|
31
|
-
const { createInputSubmitHandler } = require("./inputSubmitHandler");
|
|
32
|
-
const { keyToRaw } = require("./rawKeyMap");
|
|
33
|
-
const { createCompletionController } = require("./completionController");
|
|
34
|
-
const { createStatusLineController } = require("./statusLineController");
|
|
35
|
-
const { createInputHistoryController } = require("./inputHistoryController");
|
|
36
|
-
const { createInputListenerController } = require("./inputListenerController");
|
|
37
|
-
const { createDaemonMessageRouter } = require("./daemonMessageRouter");
|
|
38
|
-
const { createChatLogController } = require("./chatLogController");
|
|
39
|
-
const { createPasteController } = require("./pasteController");
|
|
40
|
-
const { createAgentViewController } = require("./agentViewController");
|
|
41
|
-
const { loadInternalAgentLogHistory } = require("./internalAgentLogHistory");
|
|
42
|
-
const { createSettingsController } = require("./settingsController");
|
|
43
|
-
const { createProjectCloseController } = require("./projectCloseController");
|
|
44
|
-
const { createChatLayout } = require("./layout");
|
|
45
|
-
const { createDaemonCoordinator } = require("./daemonCoordinator");
|
|
46
|
-
const { IPC_REQUEST_TYPES } = require("../shared/eventContract");
|
|
47
|
-
const { createTerminalAdapterRouter } = require("../terminal/adapterRouter");
|
|
48
|
-
const { createDaemonTransport } = require("./daemonTransport");
|
|
49
|
-
const {
|
|
50
|
-
listProjectRuntimes,
|
|
51
|
-
resolveRuntimeDir,
|
|
52
|
-
canonicalProjectRoot,
|
|
53
|
-
buildProjectId,
|
|
54
|
-
sortProjectRuntimes,
|
|
55
|
-
parseTimestampMs,
|
|
56
|
-
filterVisibleProjectRuntimes,
|
|
57
|
-
isGlobalControllerProjectRoot,
|
|
58
|
-
resolveGlobalControllerProjectRoot,
|
|
59
|
-
} = require("../projects");
|
|
60
|
-
const { loadTemplateRegistry } = require("../group/templates");
|
|
61
|
-
const { loadPromptProfileRegistry } = require("../group/promptProfiles");
|
|
62
|
-
const {
|
|
63
|
-
DEFAULT_TRANSIENT_AGENT_STATE_TTL_MS,
|
|
64
|
-
setTransientAgentState: setTransientAgentStateValue,
|
|
65
|
-
getTransientAgentStateEntry,
|
|
66
|
-
getTransientAgentState,
|
|
67
|
-
pruneTransientAgentStates,
|
|
68
|
-
} = require("./transientAgentState");
|
|
69
|
-
|
|
70
|
-
const MODE_OPTIONS = ["auto", "host", "terminal", "tmux", "internal-pty", "internal"];
|
|
71
|
-
|
|
72
|
-
async function runChatBlessed(projectRoot, options = {}) {
|
|
73
|
-
const globalMode = options && options.globalMode === true;
|
|
74
|
-
const DASHBOARD_HEIGHT = globalMode ? 2 : 1;
|
|
75
|
-
let activeProjectRoot = projectRoot;
|
|
76
|
-
try {
|
|
77
|
-
activeProjectRoot = canonicalProjectRoot(projectRoot);
|
|
78
|
-
} catch {
|
|
79
|
-
activeProjectRoot = path.resolve(projectRoot || process.cwd());
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
let globalScope = globalMode ? "controller" : "project";
|
|
83
|
-
|
|
84
|
-
const runtimePaths = getUfooPaths(projectRoot);
|
|
85
|
-
const contextIndexFile = path.join(runtimePaths.ufooDir, "context", "decisions.jsonl");
|
|
86
|
-
const needsGlobalControllerBootstrap = globalMode && (
|
|
87
|
-
!fs.existsSync(runtimePaths.ufooDir)
|
|
88
|
-
|| !fs.existsSync(runtimePaths.busDir)
|
|
89
|
-
|| !fs.existsSync(runtimePaths.agentDir)
|
|
90
|
-
|| !fs.existsSync(contextIndexFile)
|
|
91
|
-
);
|
|
92
|
-
|
|
93
|
-
if (needsGlobalControllerBootstrap || !fs.existsSync(runtimePaths.ufooDir)) {
|
|
94
|
-
const repoRoot = path.join(__dirname, "..", "..");
|
|
95
|
-
const init = new UfooInit(repoRoot);
|
|
96
|
-
await init.init({
|
|
97
|
-
modules: "context,bus",
|
|
98
|
-
project: projectRoot,
|
|
99
|
-
controllerMode: globalMode,
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// Ensure subscriber ID exists for chat (persistent across restarts)
|
|
104
|
-
if (!process.env.UFOO_SUBSCRIBER_ID) {
|
|
105
|
-
const sessionFile = path.join(getUfooPaths(projectRoot).ufooDir, "chat", "session-id.txt");
|
|
106
|
-
const sessionDir = path.dirname(sessionFile);
|
|
107
|
-
fs.mkdirSync(sessionDir, { recursive: true });
|
|
108
|
-
|
|
109
|
-
let sessionId;
|
|
110
|
-
if (fs.existsSync(sessionFile)) {
|
|
111
|
-
sessionId = fs.readFileSync(sessionFile, "utf8").trim();
|
|
112
|
-
} else {
|
|
113
|
-
sessionId = crypto.randomBytes(4).toString("hex");
|
|
114
|
-
fs.writeFileSync(sessionFile, sessionId, "utf8");
|
|
115
|
-
}
|
|
116
|
-
// Chat 模式默认使用 claude-code 类型
|
|
117
|
-
process.env.UFOO_SUBSCRIBER_ID = `claude-code:${sessionId}`;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
if (!isRunning(projectRoot)) {
|
|
121
|
-
startDaemon(projectRoot);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const sock = socketPath(projectRoot);
|
|
125
|
-
let daemonCoordinator = null;
|
|
126
|
-
const daemonTransport = createDaemonTransport({
|
|
127
|
-
projectRoot,
|
|
128
|
-
sockPath: sock,
|
|
129
|
-
isRunning,
|
|
130
|
-
startDaemon,
|
|
131
|
-
connectWithRetry,
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
const config = loadConfig(projectRoot);
|
|
135
|
-
let launchMode = config.launchMode;
|
|
136
|
-
let agentProvider = config.agentProvider;
|
|
137
|
-
let autoResume = config.autoResume !== false;
|
|
138
|
-
let cronTasks = [];
|
|
139
|
-
let loopSummary = null;
|
|
140
|
-
|
|
141
|
-
// Dynamic input height settings.
|
|
142
|
-
// Layout: dashboard(N) + inputBottom(1) + content + inputTop(1) + status(1)
|
|
143
|
-
const MIN_INPUT_CONTENT_HEIGHT = 1;
|
|
144
|
-
const MAX_INPUT_CONTENT_HEIGHT = 6;
|
|
145
|
-
const MIN_INPUT_HEIGHT = MIN_INPUT_CONTENT_HEIGHT + DASHBOARD_HEIGHT + 2;
|
|
146
|
-
const MAX_INPUT_HEIGHT = MAX_INPUT_CONTENT_HEIGHT + DASHBOARD_HEIGHT + 2;
|
|
147
|
-
let currentInputHeight = MIN_INPUT_HEIGHT;
|
|
148
|
-
const pkg = require("../../package.json");
|
|
149
|
-
const {
|
|
150
|
-
screen,
|
|
151
|
-
logBox,
|
|
152
|
-
statusLine,
|
|
153
|
-
bannerText,
|
|
154
|
-
completionPanel,
|
|
155
|
-
dashboard,
|
|
156
|
-
inputBottomLine,
|
|
157
|
-
promptBox,
|
|
158
|
-
input,
|
|
159
|
-
inputTopLine,
|
|
160
|
-
} = createChatLayout({
|
|
161
|
-
blessed,
|
|
162
|
-
currentInputHeight,
|
|
163
|
-
dashboardHeight: DASHBOARD_HEIGHT,
|
|
164
|
-
version: pkg.version,
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
const globalChatRoot = path.join(os.homedir(), ".ufoo", "chat");
|
|
168
|
-
const globalDraftsFile = path.join(globalChatRoot, "global-drafts.json");
|
|
169
|
-
const GLOBAL_DRAFT_PERSIST_DEBOUNCE_MS = 150;
|
|
170
|
-
let globalDraftsLoaded = false;
|
|
171
|
-
let globalDraftPersistTimer = null;
|
|
172
|
-
const globalDraftMap = new Map();
|
|
173
|
-
|
|
174
|
-
function safeCanonicalProjectRoot(targetRoot) {
|
|
175
|
-
try {
|
|
176
|
-
return canonicalProjectRoot(targetRoot);
|
|
177
|
-
} catch {
|
|
178
|
-
return path.resolve(targetRoot || process.cwd());
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function resolveHistoryContext(targetProjectRoot) {
|
|
183
|
-
const canonicalRoot = safeCanonicalProjectRoot(targetProjectRoot);
|
|
184
|
-
if (!globalMode) {
|
|
185
|
-
const localHistoryDir = path.join(getUfooPaths(canonicalRoot).ufooDir, "chat");
|
|
186
|
-
return {
|
|
187
|
-
projectRoot: canonicalRoot,
|
|
188
|
-
historyDir: localHistoryDir,
|
|
189
|
-
historyFile: path.join(localHistoryDir, "history.jsonl"),
|
|
190
|
-
inputHistoryDir: localHistoryDir,
|
|
191
|
-
inputHistoryFile: path.join(localHistoryDir, "input-history.jsonl"),
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
let projectId = "";
|
|
195
|
-
try {
|
|
196
|
-
projectId = buildProjectId(canonicalRoot);
|
|
197
|
-
} catch {
|
|
198
|
-
projectId = crypto.createHash("sha256").update(canonicalRoot).digest("hex").slice(0, 16);
|
|
199
|
-
}
|
|
200
|
-
const globalHistoryDir = path.join(globalChatRoot, "global-history");
|
|
201
|
-
const globalInputHistoryDir = path.join(globalChatRoot, "global-input-history");
|
|
202
|
-
return {
|
|
203
|
-
projectRoot: canonicalRoot,
|
|
204
|
-
projectId,
|
|
205
|
-
historyDir: globalHistoryDir,
|
|
206
|
-
historyFile: path.join(globalHistoryDir, `${projectId}.jsonl`),
|
|
207
|
-
inputHistoryDir: globalInputHistoryDir,
|
|
208
|
-
inputHistoryFile: path.join(globalInputHistoryDir, `${projectId}.jsonl`),
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function loadGlobalDraftsOnce() {
|
|
213
|
-
if (!globalMode || globalDraftsLoaded) return;
|
|
214
|
-
globalDraftsLoaded = true;
|
|
215
|
-
try {
|
|
216
|
-
const raw = fs.readFileSync(globalDraftsFile, "utf8");
|
|
217
|
-
const parsed = JSON.parse(String(raw || "{}"));
|
|
218
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return;
|
|
219
|
-
Object.entries(parsed).forEach(([projectRootKey, draft]) => {
|
|
220
|
-
if (typeof draft !== "string") return;
|
|
221
|
-
const canonicalKey = safeCanonicalProjectRoot(projectRootKey);
|
|
222
|
-
if (!canonicalKey) return;
|
|
223
|
-
globalDraftMap.set(canonicalKey, draft);
|
|
224
|
-
});
|
|
225
|
-
} catch {
|
|
226
|
-
// Ignore missing/invalid drafts file.
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
function writeGlobalDraftsToDisk() {
|
|
231
|
-
if (!globalMode) return;
|
|
232
|
-
const out = {};
|
|
233
|
-
for (const [projectRootKey, draft] of globalDraftMap.entries()) {
|
|
234
|
-
if (!projectRootKey) continue;
|
|
235
|
-
if (typeof draft !== "string" || draft.length === 0) continue;
|
|
236
|
-
out[projectRootKey] = draft;
|
|
237
|
-
}
|
|
238
|
-
try {
|
|
239
|
-
fs.mkdirSync(path.dirname(globalDraftsFile), { recursive: true });
|
|
240
|
-
fs.writeFileSync(globalDraftsFile, `${JSON.stringify(out, null, 2)}\n`, "utf8");
|
|
241
|
-
} catch {
|
|
242
|
-
// Ignore draft persistence failures.
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
function persistGlobalDrafts(options = {}) {
|
|
247
|
-
if (!globalMode) return;
|
|
248
|
-
const immediate = Boolean(options.immediate);
|
|
249
|
-
if (immediate) {
|
|
250
|
-
if (globalDraftPersistTimer) {
|
|
251
|
-
clearTimeout(globalDraftPersistTimer);
|
|
252
|
-
globalDraftPersistTimer = null;
|
|
253
|
-
}
|
|
254
|
-
writeGlobalDraftsToDisk();
|
|
255
|
-
return;
|
|
256
|
-
}
|
|
257
|
-
if (globalDraftPersistTimer) {
|
|
258
|
-
clearTimeout(globalDraftPersistTimer);
|
|
259
|
-
}
|
|
260
|
-
globalDraftPersistTimer = setTimeout(() => {
|
|
261
|
-
globalDraftPersistTimer = null;
|
|
262
|
-
writeGlobalDraftsToDisk();
|
|
263
|
-
}, GLOBAL_DRAFT_PERSIST_DEBOUNCE_MS);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
function getProjectDraft(targetProjectRoot) {
|
|
267
|
-
if (!globalMode) return "";
|
|
268
|
-
loadGlobalDraftsOnce();
|
|
269
|
-
const canonicalRoot = safeCanonicalProjectRoot(targetProjectRoot);
|
|
270
|
-
return globalDraftMap.get(canonicalRoot) || "";
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
function setProjectDraft(targetProjectRoot, draft, options = {}) {
|
|
274
|
-
if (!globalMode) return;
|
|
275
|
-
loadGlobalDraftsOnce();
|
|
276
|
-
const canonicalRoot = safeCanonicalProjectRoot(targetProjectRoot);
|
|
277
|
-
const text = String(draft || "");
|
|
278
|
-
if (!text) {
|
|
279
|
-
globalDraftMap.delete(canonicalRoot);
|
|
280
|
-
} else {
|
|
281
|
-
globalDraftMap.set(canonicalRoot, text);
|
|
282
|
-
}
|
|
283
|
-
persistGlobalDrafts(options);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
let currentHistoryContext = resolveHistoryContext(activeProjectRoot);
|
|
287
|
-
|
|
288
|
-
let chatLogController = createChatLogController({
|
|
289
|
-
logBox,
|
|
290
|
-
fsModule: fs,
|
|
291
|
-
historyDir: currentHistoryContext.historyDir,
|
|
292
|
-
historyFile: currentHistoryContext.historyFile,
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
const streamTracker = createStreamTracker({
|
|
296
|
-
logBox,
|
|
297
|
-
writeSpacer: () => chatLogController.writeSpacer(false),
|
|
298
|
-
appendHistory: (...args) => chatLogController.appendHistory(...args),
|
|
299
|
-
escapeBlessed,
|
|
300
|
-
onStreamStart: () => chatLogController.markStreamStart(),
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
const beginStream = (...args) => streamTracker.beginStream(...args);
|
|
304
|
-
const appendStreamDelta = (...args) => streamTracker.appendStreamDelta(...args);
|
|
305
|
-
const finalizeStream = (...args) => streamTracker.finalizeStream(...args);
|
|
306
|
-
const markPendingDelivery = (...args) => streamTracker.markPendingDelivery(...args);
|
|
307
|
-
const getPendingState = (...args) => streamTracker.getPendingState(...args);
|
|
308
|
-
const consumePendingDelivery = (...args) => streamTracker.consumePendingDelivery(...args);
|
|
309
|
-
|
|
310
|
-
function logMessage(type, text, meta = {}) {
|
|
311
|
-
chatLogController.logMessage(type, text, meta);
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
function loadHistory(limit = 2000) {
|
|
315
|
-
chatLogController.loadHistory(limit);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
let inputHistoryController = null;
|
|
319
|
-
|
|
320
|
-
function loadInputHistory(limit = 2000) {
|
|
321
|
-
if (!inputHistoryController) return;
|
|
322
|
-
inputHistoryController.loadInputHistory(limit);
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
const statusLineController = createStatusLineController({
|
|
326
|
-
statusLine,
|
|
327
|
-
bannerText,
|
|
328
|
-
renderScreen: () => screen.render(),
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
const queueStatusLine = (...args) => statusLineController.queueStatusLine(...args);
|
|
332
|
-
const resolveStatusLine = (...args) => statusLineController.resolveStatusLine(...args);
|
|
333
|
-
const enqueueBusStatus = (...args) => statusLineController.enqueueBusStatus(...args);
|
|
334
|
-
const resolveBusStatus = (...args) => statusLineController.resolveBusStatus(...args);
|
|
335
|
-
|
|
336
|
-
let agentViewController = null;
|
|
337
|
-
let terminalAdapterRouter = null;
|
|
338
|
-
const agentSockets = createAgentSockets({
|
|
339
|
-
onTermWrite: (text) => writeToAgentTerm(text),
|
|
340
|
-
onPlaceCursor: (cursor) => placeAgentCursor(cursor),
|
|
341
|
-
isAgentView: () => getCurrentView() === "agent",
|
|
342
|
-
isBusMode: () => isAgentViewUsesBus(),
|
|
343
|
-
getViewingAgent: () => getViewingAgent(),
|
|
344
|
-
sendBusRaw: (target, data) => {
|
|
345
|
-
send({
|
|
346
|
-
type: IPC_REQUEST_TYPES.BUS_SEND,
|
|
347
|
-
target,
|
|
348
|
-
message: JSON.stringify({ raw: true, data }),
|
|
349
|
-
injection_mode: "immediate",
|
|
350
|
-
source: "chat-agent-view",
|
|
351
|
-
});
|
|
352
|
-
},
|
|
353
|
-
});
|
|
354
|
-
|
|
355
|
-
// Add cursor position tracking
|
|
356
|
-
let cursorPos = 0;
|
|
357
|
-
let preferredCol = null;
|
|
358
|
-
|
|
359
|
-
function getInnerWidth() {
|
|
360
|
-
let promptWidth = 2;
|
|
361
|
-
try {
|
|
362
|
-
if (promptBox && typeof promptBox.width === "number") promptWidth = promptBox.width;
|
|
363
|
-
} catch {
|
|
364
|
-
promptWidth = 2;
|
|
365
|
-
}
|
|
366
|
-
return inputMath.getInnerWidth({ input, screen, promptWidth });
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
function getWrapWidth() {
|
|
370
|
-
return inputMath.getWrapWidth(input, getInnerWidth());
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
function countLines(text, width) {
|
|
374
|
-
return inputMath.countLines(text, width, (value) => input.strWidth(value));
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
function getCursorRowCol(text, pos, width) {
|
|
378
|
-
return inputMath.getCursorRowCol(text, pos, width, (value) => input.strWidth(value));
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
function getCursorPosForRowCol(text, targetRow, targetCol, width) {
|
|
382
|
-
return inputMath.getCursorPosForRowCol(
|
|
383
|
-
text,
|
|
384
|
-
targetRow,
|
|
385
|
-
targetCol,
|
|
386
|
-
width,
|
|
387
|
-
(value) => input.strWidth(value),
|
|
388
|
-
);
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
function ensureInputCursorVisible() {
|
|
392
|
-
const innerWidth = getWrapWidth();
|
|
393
|
-
if (innerWidth <= 0) return;
|
|
394
|
-
const totalRows = countLines(input.value, innerWidth);
|
|
395
|
-
const visibleRows = Math.max(1, input.height || 1);
|
|
396
|
-
const { row } = getCursorRowCol(input.value, cursorPos, innerWidth);
|
|
397
|
-
let base = input.childBase || 0;
|
|
398
|
-
const maxBase = Math.max(0, totalRows - visibleRows);
|
|
399
|
-
const bottomMargin = visibleRows > 1 ? 1 : 0;
|
|
400
|
-
const upperLimit = base;
|
|
401
|
-
const lowerLimit = base + visibleRows - bottomMargin - 1;
|
|
402
|
-
|
|
403
|
-
if (row < upperLimit) {
|
|
404
|
-
base = row;
|
|
405
|
-
} else if (row > lowerLimit) {
|
|
406
|
-
base = row - (visibleRows - bottomMargin - 1);
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
if (base > maxBase) base = maxBase;
|
|
410
|
-
if (base < 0) base = 0;
|
|
411
|
-
if (base !== input.childBase) {
|
|
412
|
-
input.childBase = base;
|
|
413
|
-
if (typeof input.scrollTo === "function") {
|
|
414
|
-
input.scrollTo(base);
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
function resetPreferredCol() {
|
|
420
|
-
preferredCol = null;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
function getPreferredCol() {
|
|
424
|
-
return preferredCol;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
function setPreferredCol(value) {
|
|
428
|
-
preferredCol = value;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
function normalizePaste(text) {
|
|
432
|
-
return inputMath.normalizePaste(text);
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
function updateDraftFromInput() {
|
|
436
|
-
if (!inputHistoryController) return;
|
|
437
|
-
inputHistoryController.updateDraftFromInput();
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
function normalizeCommandPrefix() {
|
|
441
|
-
if (!input.value.startsWith("//")) return;
|
|
442
|
-
const match = input.value.match(/^\/{2,}/);
|
|
443
|
-
if (!match) return;
|
|
444
|
-
const extra = match[0].length - 1;
|
|
445
|
-
input.value = `/${input.value.slice(match[0].length)}`;
|
|
446
|
-
cursorPos = Math.max(0, cursorPos - extra);
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
function insertTextAtCursor(text) {
|
|
450
|
-
if (!text) return;
|
|
451
|
-
input.value = input.value.slice(0, cursorPos) + text + input.value.slice(cursorPos);
|
|
452
|
-
cursorPos += text.length;
|
|
453
|
-
normalizeCommandPrefix();
|
|
454
|
-
resetPreferredCol();
|
|
455
|
-
resizeInput();
|
|
456
|
-
ensureInputCursorVisible();
|
|
457
|
-
input._updateCursor();
|
|
458
|
-
screen.render();
|
|
459
|
-
updateDraftFromInput();
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
function setInputValue(value) {
|
|
463
|
-
input.value = value || "";
|
|
464
|
-
cursorPos = input.value.length;
|
|
465
|
-
resetPreferredCol();
|
|
466
|
-
resizeInput();
|
|
467
|
-
ensureInputCursorVisible();
|
|
468
|
-
input._updateCursor();
|
|
469
|
-
screen.render();
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
inputHistoryController = createInputHistoryController({
|
|
473
|
-
inputHistoryFile: currentHistoryContext.inputHistoryFile,
|
|
474
|
-
historyDir: currentHistoryContext.inputHistoryDir,
|
|
475
|
-
setInputValue,
|
|
476
|
-
getInputValue: () => input.value || "",
|
|
477
|
-
});
|
|
478
|
-
|
|
479
|
-
function captureCurrentProjectDraft() {
|
|
480
|
-
if (!inputHistoryController || typeof inputHistoryController.getDraftForPersistence !== "function") {
|
|
481
|
-
return input.value || "";
|
|
482
|
-
}
|
|
483
|
-
return inputHistoryController.getDraftForPersistence();
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
function seedGlobalHistoryFromProject(nextContext) {
|
|
487
|
-
if (!globalMode || !nextContext || !nextContext.projectRoot) return;
|
|
488
|
-
const projectUfooDir = getUfooPaths(nextContext.projectRoot).ufooDir;
|
|
489
|
-
const projectChatDir = path.join(projectUfooDir, "chat");
|
|
490
|
-
const projectHistoryFile = path.join(projectChatDir, "history.jsonl");
|
|
491
|
-
const projectInputHistoryFile = path.join(projectChatDir, "input-history.jsonl");
|
|
492
|
-
try {
|
|
493
|
-
if (!fs.existsSync(nextContext.historyFile) && fs.existsSync(projectHistoryFile)) {
|
|
494
|
-
fs.mkdirSync(path.dirname(nextContext.historyFile), { recursive: true });
|
|
495
|
-
fs.copyFileSync(projectHistoryFile, nextContext.historyFile);
|
|
496
|
-
}
|
|
497
|
-
} catch {
|
|
498
|
-
// best-effort seed only
|
|
499
|
-
}
|
|
500
|
-
try {
|
|
501
|
-
if (!fs.existsSync(nextContext.inputHistoryFile) && fs.existsSync(projectInputHistoryFile)) {
|
|
502
|
-
fs.mkdirSync(path.dirname(nextContext.inputHistoryFile), { recursive: true });
|
|
503
|
-
fs.copyFileSync(projectInputHistoryFile, nextContext.inputHistoryFile);
|
|
504
|
-
}
|
|
505
|
-
} catch {
|
|
506
|
-
// best-effort seed only
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
function applyProjectHistoryContext(nextProjectRoot) {
|
|
511
|
-
streamTracker.discardAll();
|
|
512
|
-
const nextContext = resolveHistoryContext(nextProjectRoot);
|
|
513
|
-
seedGlobalHistoryFromProject(nextContext);
|
|
514
|
-
currentHistoryContext = nextContext;
|
|
515
|
-
chatLogController.setHistoryTarget({
|
|
516
|
-
historyDir: nextContext.historyDir,
|
|
517
|
-
historyFile: nextContext.historyFile,
|
|
518
|
-
});
|
|
519
|
-
chatLogController.resetViewState();
|
|
520
|
-
|
|
521
|
-
inputHistoryController.setHistoryTarget({
|
|
522
|
-
inputHistoryFile: nextContext.inputHistoryFile,
|
|
523
|
-
historyDir: nextContext.inputHistoryDir,
|
|
524
|
-
});
|
|
525
|
-
inputHistoryController.loadInputHistory();
|
|
526
|
-
const nextDraft = getProjectDraft(nextContext.projectRoot);
|
|
527
|
-
inputHistoryController.restoreDraft(nextDraft);
|
|
528
|
-
|
|
529
|
-
clearLog();
|
|
530
|
-
loadHistory();
|
|
531
|
-
pending = null;
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
function historyUp() {
|
|
535
|
-
if (!inputHistoryController) return false;
|
|
536
|
-
return inputHistoryController.historyUp();
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
function historyDown() {
|
|
540
|
-
if (!inputHistoryController) return false;
|
|
541
|
-
return inputHistoryController.historyDown();
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
function exitHandler() {
|
|
545
|
-
if (globalMode) {
|
|
546
|
-
setProjectDraft(activeProjectRoot, captureCurrentProjectDraft(), { immediate: true });
|
|
547
|
-
}
|
|
548
|
-
if (daemonCoordinator) {
|
|
549
|
-
daemonCoordinator.markExit();
|
|
550
|
-
}
|
|
551
|
-
exitAgentView();
|
|
552
|
-
if (screen && screen.program && typeof screen.program.decrst === "function") {
|
|
553
|
-
screen.program.decrst(2004);
|
|
554
|
-
}
|
|
555
|
-
statusLineController.destroy();
|
|
556
|
-
if (daemonCoordinator) {
|
|
557
|
-
daemonCoordinator.close();
|
|
558
|
-
}
|
|
559
|
-
process.exit(0);
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
const completionController = createCompletionController({
|
|
563
|
-
input,
|
|
564
|
-
screen,
|
|
565
|
-
completionPanel,
|
|
566
|
-
promptBox,
|
|
567
|
-
commandRegistry: COMMAND_REGISTRY,
|
|
568
|
-
getGroupTemplateCandidates: () => {
|
|
569
|
-
const registry = loadTemplateRegistry(activeProjectRoot);
|
|
570
|
-
return registry.templates.map((item) => ({
|
|
571
|
-
alias: item.alias,
|
|
572
|
-
name: item.templateName || item.templateId || "",
|
|
573
|
-
desc: item.templateDescription || "",
|
|
574
|
-
source: item.source || "",
|
|
575
|
-
}));
|
|
576
|
-
},
|
|
577
|
-
getSoloProfileCandidates: () => {
|
|
578
|
-
const registry = loadPromptProfileRegistry(activeProjectRoot);
|
|
579
|
-
return buildPromptProfileCandidates(registry);
|
|
580
|
-
},
|
|
581
|
-
getMentionCandidates: () => activeAgents.map((id) => ({
|
|
582
|
-
id,
|
|
583
|
-
label: getAgentLabel(id),
|
|
584
|
-
})),
|
|
585
|
-
normalizeCommandPrefix,
|
|
586
|
-
truncateText,
|
|
587
|
-
getCurrentInputHeight: () => currentInputHeight,
|
|
588
|
-
getCursorPos: () => cursorPos,
|
|
589
|
-
setCursorPos: (value) => {
|
|
590
|
-
cursorPos = value;
|
|
591
|
-
},
|
|
592
|
-
resetPreferredCol,
|
|
593
|
-
updateDraftFromInput,
|
|
594
|
-
renderScreen: () => screen.render(),
|
|
595
|
-
});
|
|
596
|
-
|
|
597
|
-
const pasteController = createPasteController({
|
|
598
|
-
shouldHandle: () => screen.focused === input && focusMode === "input",
|
|
599
|
-
normalizePaste,
|
|
600
|
-
insertTextAtCursor,
|
|
601
|
-
});
|
|
602
|
-
|
|
603
|
-
const inputListenerController = createInputListenerController({
|
|
604
|
-
getCurrentView: () => getCurrentView(),
|
|
605
|
-
exitHandler,
|
|
606
|
-
getFocusMode: () => focusMode,
|
|
607
|
-
getDashboardView: () => dashboardView,
|
|
608
|
-
getSelectedAgentIndex: () => selectedAgentIndex,
|
|
609
|
-
getActiveAgents: () => activeAgents,
|
|
610
|
-
getTargetAgent: () => targetAgent,
|
|
611
|
-
getGlobalScope: () => globalScope,
|
|
612
|
-
clearTargetAgent,
|
|
613
|
-
exitProjectScope: () => {
|
|
614
|
-
setGlobalScope("controller").catch((err) => {
|
|
615
|
-
const message = err && err.message ? err.message : String(err || "scope switch failed");
|
|
616
|
-
logMessage("error", `{white-fg}✗{/white-fg} Scope switch failed: ${escapeBlessed(message)}`);
|
|
617
|
-
});
|
|
618
|
-
},
|
|
619
|
-
requestCloseAgent,
|
|
620
|
-
logMessage,
|
|
621
|
-
isSuppressKeypress: () => pasteController.isSuppressKeypress(),
|
|
622
|
-
normalizeCommandPrefix,
|
|
623
|
-
handleDashboardKey,
|
|
624
|
-
exitDashboardMode,
|
|
625
|
-
completionController,
|
|
626
|
-
getLogHeight: () => logBox.height,
|
|
627
|
-
scrollLog,
|
|
628
|
-
insertTextAtCursor,
|
|
629
|
-
normalizePaste,
|
|
630
|
-
resetPreferredCol,
|
|
631
|
-
getCursorPos: () => cursorPos,
|
|
632
|
-
setCursorPos: (value) => {
|
|
633
|
-
cursorPos = value;
|
|
634
|
-
},
|
|
635
|
-
ensureInputCursorVisible,
|
|
636
|
-
getWrapWidth,
|
|
637
|
-
getCursorRowCol,
|
|
638
|
-
countLines,
|
|
639
|
-
getCursorPosForRowCol,
|
|
640
|
-
getPreferredCol,
|
|
641
|
-
setPreferredCol,
|
|
642
|
-
historyUp,
|
|
643
|
-
historyDown,
|
|
644
|
-
enterDashboardMode,
|
|
645
|
-
resizeInput,
|
|
646
|
-
updateDraftFromInput,
|
|
647
|
-
});
|
|
648
|
-
|
|
649
|
-
// Resize input box based on content
|
|
650
|
-
function resizeInput() {
|
|
651
|
-
const innerWidth = getWrapWidth();
|
|
652
|
-
if (innerWidth <= 0) return;
|
|
653
|
-
|
|
654
|
-
const numLines = countLines(input.value, innerWidth);
|
|
655
|
-
const contentHeight = Math.min(MAX_INPUT_CONTENT_HEIGHT, Math.max(MIN_INPUT_CONTENT_HEIGHT, numLines));
|
|
656
|
-
const targetHeight = contentHeight + DASHBOARD_HEIGHT + 2;
|
|
657
|
-
|
|
658
|
-
if (targetHeight !== currentInputHeight) {
|
|
659
|
-
currentInputHeight = targetHeight;
|
|
660
|
-
input.height = contentHeight;
|
|
661
|
-
promptBox.height = contentHeight;
|
|
662
|
-
inputTopLine.bottom = currentInputHeight - 1; // Just above input area
|
|
663
|
-
}
|
|
664
|
-
statusLine.bottom = currentInputHeight;
|
|
665
|
-
// Reposition completion panel if active
|
|
666
|
-
if (completionController.isActive()) completionController.reflow();
|
|
667
|
-
// dashboard and inputBottomLine stay fixed at the bottom region.
|
|
668
|
-
logBox.height = Math.max(1, screen.height - currentInputHeight - 1);
|
|
669
|
-
ensureInputCursorVisible();
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
// Override the internal listener to support cursor movement
|
|
673
|
-
input._listener = function(ch, key) {
|
|
674
|
-
inputListenerController.handleKey(ch, key, this);
|
|
675
|
-
};
|
|
676
|
-
|
|
677
|
-
// Override cursor update to use our cursor position
|
|
678
|
-
input._updateCursor = function() {
|
|
679
|
-
if (this.screen.focused !== this) return;
|
|
680
|
-
|
|
681
|
-
let lpos;
|
|
682
|
-
try { lpos = this._getCoords(); } catch { return; }
|
|
683
|
-
if (!lpos) return;
|
|
684
|
-
|
|
685
|
-
const innerWidth = getWrapWidth();
|
|
686
|
-
if (innerWidth <= 0) return;
|
|
687
|
-
|
|
688
|
-
ensureInputCursorVisible();
|
|
689
|
-
const { row, col } = getCursorRowCol(this.value, cursorPos, innerWidth);
|
|
690
|
-
const scrollOffset = this.childBase || 0;
|
|
691
|
-
|
|
692
|
-
const displayRow = row - scrollOffset;
|
|
693
|
-
const safeCol = Math.min(Math.max(0, col), innerWidth - 1);
|
|
694
|
-
const cy = lpos.yi + displayRow;
|
|
695
|
-
const cx = lpos.xi + safeCol;
|
|
696
|
-
|
|
697
|
-
this.screen.program.cup(cy, cx);
|
|
698
|
-
this.screen.program.showCursor();
|
|
699
|
-
};
|
|
700
|
-
|
|
701
|
-
// Reset cursor and height on clear
|
|
702
|
-
const originalClearValue = input.clearValue.bind(input);
|
|
703
|
-
input.clearValue = function() {
|
|
704
|
-
cursorPos = 0;
|
|
705
|
-
resetPreferredCol();
|
|
706
|
-
currentInputHeight = MIN_INPUT_HEIGHT;
|
|
707
|
-
if (inputHistoryController) inputHistoryController.setIndexToEnd();
|
|
708
|
-
completionController.hide();
|
|
709
|
-
const contentHeight = MIN_INPUT_CONTENT_HEIGHT;
|
|
710
|
-
input.height = contentHeight;
|
|
711
|
-
promptBox.height = contentHeight;
|
|
712
|
-
inputTopLine.bottom = currentInputHeight - 1;
|
|
713
|
-
statusLine.bottom = currentInputHeight;
|
|
714
|
-
logBox.height = Math.max(1, screen.height - currentInputHeight - 1);
|
|
715
|
-
return originalClearValue();
|
|
716
|
-
};
|
|
717
|
-
|
|
718
|
-
let pending = null;
|
|
719
|
-
|
|
720
|
-
// Agent selection state
|
|
721
|
-
let activeAgents = [];
|
|
722
|
-
let activeAgentLabelMap = new Map();
|
|
723
|
-
let activeAgentMetaMap = new Map(); // Store full meta including launch_mode
|
|
724
|
-
const transientAgentStateMap = new Map();
|
|
725
|
-
let agentListWindowStart = 0;
|
|
726
|
-
const MAX_AGENT_WINDOW = 4;
|
|
727
|
-
let projectRuntimes = [];
|
|
728
|
-
let projectListWindowStart = 0;
|
|
729
|
-
const MAX_PROJECT_WINDOW = 5;
|
|
730
|
-
let selectedProjectIndex = -1;
|
|
731
|
-
let selectedAgentIndex = -1; // -1 = not in dashboard selection mode
|
|
732
|
-
let targetAgent = null; // Selected agent for direct messaging
|
|
733
|
-
let focusMode = "input"; // "input" or "dashboard"
|
|
734
|
-
let dashboardView = "agents"; // "projects" | "agents" | "mode" | "provider" | "cron"
|
|
735
|
-
let selectedModeIndex = Math.max(0, MODE_OPTIONS.indexOf(launchMode));
|
|
736
|
-
const providerOptions = [
|
|
737
|
-
{ label: "codex", value: "codex-cli" },
|
|
738
|
-
{ label: "claude", value: "claude-cli" },
|
|
739
|
-
];
|
|
740
|
-
let selectedProviderIndex = Math.max(0, providerOptions.findIndex((opt) => opt.value === agentProvider));
|
|
741
|
-
const resumeOptions = [
|
|
742
|
-
{ label: "Resume previous session", value: true },
|
|
743
|
-
{ label: "Start new session", value: false },
|
|
744
|
-
];
|
|
745
|
-
let selectedResumeIndex = autoResume ? 0 : 1;
|
|
746
|
-
let selectedCronIndex = -1;
|
|
747
|
-
const DASH_HINTS = {
|
|
748
|
-
agents: "←/→ select · Enter · ↓ mode · ↑ back",
|
|
749
|
-
agentsGlobal: "←/→ select · Enter · ↓ mode · ↑ projects",
|
|
750
|
-
agentsEmpty: "↓ mode · ↑ back",
|
|
751
|
-
mode: "←/→ select · Enter · ↓ provider · ↑ back",
|
|
752
|
-
provider: "←/→ select · Enter · ↓ cron · ↑ back",
|
|
753
|
-
cron: "←/→ switch · Ctrl+X stop · ↑ back",
|
|
754
|
-
resume: "",
|
|
755
|
-
projects: "Use /open <path> or /project switch <index|path>",
|
|
756
|
-
projectsFocus: "←/→ switch · Ctrl+X close · ↓ second row · Enter confirm · ↑ back",
|
|
757
|
-
projectsEmpty: "Run ufoo chat or ufoo daemon start in project directories",
|
|
758
|
-
};
|
|
759
|
-
const AGENT_BAR_HINTS = {
|
|
760
|
-
normal: "↓ agents",
|
|
761
|
-
dashboard: "←/→ · Enter · ↑ · ^X",
|
|
762
|
-
};
|
|
763
|
-
|
|
764
|
-
function getCurrentView() {
|
|
765
|
-
return agentViewController ? agentViewController.getCurrentView() : "main";
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
function getViewingAgent() {
|
|
769
|
-
return agentViewController ? agentViewController.getViewingAgent() : "";
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
function getAgentAdapter(agentId) {
|
|
773
|
-
if (!terminalAdapterRouter) return null;
|
|
774
|
-
const meta = activeAgentMetaMap ? activeAgentMetaMap.get(agentId) : null;
|
|
775
|
-
const agentLaunchMode = (meta && meta.launch_mode) || launchMode || "";
|
|
776
|
-
return terminalAdapterRouter.getAdapter({ launchMode: agentLaunchMode, agentId, meta });
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
function getViewingAgentAdapter() {
|
|
780
|
-
const viewingAgent = getViewingAgent();
|
|
781
|
-
if (!viewingAgent) return null;
|
|
782
|
-
return getAgentAdapter(viewingAgent);
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
function canSendRaw(adapter) {
|
|
786
|
-
if (!adapter || !adapter.capabilities) return false;
|
|
787
|
-
return Boolean(
|
|
788
|
-
adapter.capabilities.supportsSocketProtocol
|
|
789
|
-
|| adapter.capabilities.supportsInternalQueueLoop
|
|
790
|
-
);
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
function canResize(adapter) {
|
|
794
|
-
return Boolean(adapter && adapter.capabilities && adapter.capabilities.supportsSocketProtocol);
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
function canSnapshot(adapter) {
|
|
798
|
-
if (!adapter || !adapter.capabilities) return false;
|
|
799
|
-
return Boolean(
|
|
800
|
-
adapter.capabilities.supportsSnapshot
|
|
801
|
-
|| adapter.capabilities.supportsSubscribeScreen
|
|
802
|
-
|| adapter.capabilities.supportsSubscribeFull
|
|
803
|
-
);
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
function sendRawWithCapabilities(data) {
|
|
807
|
-
const adapter = getViewingAgentAdapter();
|
|
808
|
-
if (!canSendRaw(adapter)) return;
|
|
809
|
-
try {
|
|
810
|
-
adapter.sendRaw(data);
|
|
811
|
-
} catch {
|
|
812
|
-
// ignore unsupported errors
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
function sendResizeWithCapabilities(cols, rows) {
|
|
817
|
-
const adapter = getViewingAgentAdapter();
|
|
818
|
-
if (!canResize(adapter)) return;
|
|
819
|
-
try {
|
|
820
|
-
adapter.resize(cols, rows);
|
|
821
|
-
} catch {
|
|
822
|
-
// ignore unsupported errors
|
|
823
|
-
}
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
function requestSnapshotWithCapabilities() {
|
|
827
|
-
const adapter = getViewingAgentAdapter();
|
|
828
|
-
if (!canSnapshot(adapter)) return false;
|
|
829
|
-
try {
|
|
830
|
-
return adapter.snapshot();
|
|
831
|
-
} catch {
|
|
832
|
-
return false;
|
|
833
|
-
}
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
function isAgentViewUsesBus() {
|
|
837
|
-
return agentViewController ? agentViewController.isAgentViewUsesBus() : false;
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
function getAgentInputSuppressUntil() {
|
|
841
|
-
return agentViewController ? agentViewController.getAgentInputSuppressUntil() : 0;
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
function getAgentOutputSuppressed() {
|
|
845
|
-
return agentViewController ? agentViewController.getAgentOutputSuppressed() : false;
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
function setAgentOutputSuppressed(value) {
|
|
849
|
-
if (agentViewController) {
|
|
850
|
-
agentViewController.setAgentOutputSuppressed(value);
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
function renderAgentDashboard() {
|
|
855
|
-
if (agentViewController) {
|
|
856
|
-
agentViewController.renderAgentDashboard();
|
|
857
|
-
}
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
function setAgentBarVisible(visible) {
|
|
861
|
-
if (agentViewController) {
|
|
862
|
-
agentViewController.setAgentBarVisible(visible);
|
|
863
|
-
}
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
function enterAgentView(agentId, options = {}) {
|
|
867
|
-
if (agentViewController) {
|
|
868
|
-
agentViewController.enterAgentView(agentId, options);
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
function exitAgentView() {
|
|
873
|
-
if (agentViewController) {
|
|
874
|
-
agentViewController.exitAgentView();
|
|
875
|
-
}
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
function sendRawToAgent(data) {
|
|
879
|
-
if (agentViewController) {
|
|
880
|
-
agentViewController.sendRawToAgent(data);
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
function sendResizeToAgent(cols, rows) {
|
|
885
|
-
if (agentViewController) {
|
|
886
|
-
agentViewController.sendResizeToAgent(cols, rows);
|
|
887
|
-
}
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
function requestAgentSnapshot() {
|
|
891
|
-
if (agentViewController) {
|
|
892
|
-
agentViewController.requestAgentSnapshot();
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
function writeToAgentTerm(text) {
|
|
897
|
-
if (agentViewController) {
|
|
898
|
-
agentViewController.writeToAgentTerm(text);
|
|
899
|
-
}
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
function placeAgentCursor(cursor) {
|
|
903
|
-
if (agentViewController) {
|
|
904
|
-
agentViewController.placeAgentCursor(cursor);
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
function handleResizeInAgentView() {
|
|
909
|
-
if (!agentViewController) return false;
|
|
910
|
-
return agentViewController.handleResizeInAgentView();
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
function getAgentLabel(agentId) {
|
|
914
|
-
return agentDirectory.getAgentLabel(activeAgentLabelMap, agentId);
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
function resolveAgentId(label) {
|
|
918
|
-
return agentDirectory.resolveAgentId({
|
|
919
|
-
label,
|
|
920
|
-
activeAgents,
|
|
921
|
-
labelMap: activeAgentLabelMap,
|
|
922
|
-
lookupNickname: (nickname) => {
|
|
923
|
-
try {
|
|
924
|
-
const busPath = getUfooPaths(activeProjectRoot).agentsFile;
|
|
925
|
-
const bus = JSON.parse(fs.readFileSync(busPath, "utf8"));
|
|
926
|
-
for (const [id, meta] of Object.entries(bus.agents || {})) {
|
|
927
|
-
if (meta && (meta.nickname === nickname || meta.scoped_nickname === nickname)) return id;
|
|
928
|
-
}
|
|
929
|
-
} catch {
|
|
930
|
-
// ignore lookup errors
|
|
931
|
-
}
|
|
932
|
-
return null;
|
|
933
|
-
},
|
|
934
|
-
});
|
|
935
|
-
}
|
|
936
|
-
|
|
937
|
-
function resolveAgentDisplayName(publisher) {
|
|
938
|
-
return agentDirectory.resolveAgentDisplayName({
|
|
939
|
-
publisher,
|
|
940
|
-
labelMap: activeAgentLabelMap,
|
|
941
|
-
lookupNicknameById: (id) => {
|
|
942
|
-
try {
|
|
943
|
-
const busPath = getUfooPaths(activeProjectRoot).agentsFile;
|
|
944
|
-
const bus = JSON.parse(fs.readFileSync(busPath, "utf8"));
|
|
945
|
-
const meta = bus.agents && bus.agents[id];
|
|
946
|
-
if (meta) return resolveDisplayNickname(activeProjectRoot, meta);
|
|
947
|
-
} catch {
|
|
948
|
-
// Keep original publisher ID
|
|
949
|
-
}
|
|
950
|
-
return null;
|
|
951
|
-
},
|
|
952
|
-
});
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
function clampAgentWindowWithSelection(selectionIndex) {
|
|
956
|
-
agentListWindowStart = agentDirectory.clampAgentWindowWithSelection({
|
|
957
|
-
activeCount: activeAgents.length,
|
|
958
|
-
maxWindow: MAX_AGENT_WINDOW,
|
|
959
|
-
windowStart: agentListWindowStart,
|
|
960
|
-
selectionIndex,
|
|
961
|
-
});
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
function clampAgentWindow() {
|
|
965
|
-
clampAgentWindowWithSelection(selectedAgentIndex);
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
function resolveRuntimeProjectRoot(row = {}) {
|
|
969
|
-
const raw = row && row.project_root ? String(row.project_root) : "";
|
|
970
|
-
if (!raw) return "";
|
|
971
|
-
try {
|
|
972
|
-
return canonicalProjectRoot(raw);
|
|
973
|
-
} catch {
|
|
974
|
-
return path.resolve(raw);
|
|
975
|
-
}
|
|
976
|
-
}
|
|
977
|
-
|
|
978
|
-
function refreshProjectRuntimes() {
|
|
979
|
-
let rows = [];
|
|
980
|
-
try {
|
|
981
|
-
rows = listProjectRuntimes({ validate: true, cleanupTmp: true });
|
|
982
|
-
} catch {
|
|
983
|
-
rows = [];
|
|
984
|
-
}
|
|
985
|
-
rows = filterVisibleProjectRuntimes(rows);
|
|
986
|
-
if (globalMode) {
|
|
987
|
-
rows = rows.filter((row) => !isGlobalControllerProjectRoot(resolveRuntimeProjectRoot(row)));
|
|
988
|
-
}
|
|
989
|
-
const normalizedActive = String(activeProjectRoot || "");
|
|
990
|
-
if (
|
|
991
|
-
normalizedActive
|
|
992
|
-
&& !(globalMode && isGlobalControllerProjectRoot(normalizedActive))
|
|
993
|
-
&& !rows.some((row) => resolveRuntimeProjectRoot(row) === normalizedActive)
|
|
994
|
-
) {
|
|
995
|
-
rows.unshift({
|
|
996
|
-
project_root: normalizedActive,
|
|
997
|
-
project_name: path.basename(normalizedActive) || normalizedActive,
|
|
998
|
-
status: "untracked",
|
|
999
|
-
last_seen: null,
|
|
1000
|
-
});
|
|
1001
|
-
}
|
|
1002
|
-
projectRuntimes = sortProjectRuntimes({
|
|
1003
|
-
rows,
|
|
1004
|
-
activeProjectRoot: normalizedActive,
|
|
1005
|
-
resolveProjectRoot: resolveRuntimeProjectRoot,
|
|
1006
|
-
getInteractionMs: (row) => {
|
|
1007
|
-
const rowRoot = resolveRuntimeProjectRoot(row);
|
|
1008
|
-
if (!rowRoot) return 0;
|
|
1009
|
-
try {
|
|
1010
|
-
const historyContext = resolveHistoryContext(rowRoot);
|
|
1011
|
-
if (historyContext && historyContext.historyFile && fs.existsSync(historyContext.historyFile)) {
|
|
1012
|
-
const stat = fs.statSync(historyContext.historyFile);
|
|
1013
|
-
if (Number.isFinite(stat.mtimeMs) && stat.mtimeMs > 0) {
|
|
1014
|
-
return stat.mtimeMs;
|
|
1015
|
-
}
|
|
1016
|
-
}
|
|
1017
|
-
} catch {
|
|
1018
|
-
// fall through
|
|
1019
|
-
}
|
|
1020
|
-
return parseTimestampMs(row && row.last_seen);
|
|
1021
|
-
},
|
|
1022
|
-
});
|
|
1023
|
-
|
|
1024
|
-
if (projectRuntimes.length === 0) {
|
|
1025
|
-
selectedProjectIndex = -1;
|
|
1026
|
-
projectListWindowStart = 0;
|
|
1027
|
-
return;
|
|
1028
|
-
}
|
|
1029
|
-
const activeIndex = projectRuntimes.findIndex(
|
|
1030
|
-
(row) => resolveRuntimeProjectRoot(row) === normalizedActive
|
|
1031
|
-
);
|
|
1032
|
-
if (selectedProjectIndex < 0 || selectedProjectIndex >= projectRuntimes.length) {
|
|
1033
|
-
selectedProjectIndex = activeIndex >= 0 ? activeIndex : 0;
|
|
1034
|
-
}
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1037
|
-
function syncSelectedProjectToActive() {
|
|
1038
|
-
if (!Array.isArray(projectRuntimes) || projectRuntimes.length === 0) return;
|
|
1039
|
-
const activeIndex = projectRuntimes.findIndex(
|
|
1040
|
-
(row) => resolveRuntimeProjectRoot(row) === String(activeProjectRoot || "")
|
|
1041
|
-
);
|
|
1042
|
-
if (activeIndex >= 0) {
|
|
1043
|
-
selectedProjectIndex = activeIndex;
|
|
1044
|
-
}
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
|
-
function send(req) {
|
|
1048
|
-
if (!daemonCoordinator) return;
|
|
1049
|
-
daemonCoordinator.send(req);
|
|
1050
|
-
}
|
|
1051
|
-
|
|
1052
|
-
function updatePromptBox() {
|
|
1053
|
-
// Determine scope prefix (only in global mode)
|
|
1054
|
-
let prefix = "";
|
|
1055
|
-
if (globalMode && globalScope === "controller") {
|
|
1056
|
-
prefix = "g";
|
|
1057
|
-
} else if (globalMode && globalScope === "project") {
|
|
1058
|
-
prefix = truncateText(path.basename(activeProjectRoot), 10, "");
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
|
-
// Build content: [prefix]>[>@agent]
|
|
1062
|
-
const content = targetAgent
|
|
1063
|
-
? `${prefix}>@${getAgentLabel(targetAgent)}`
|
|
1064
|
-
: `${prefix}>`;
|
|
1065
|
-
|
|
1066
|
-
promptBox.setContent(content);
|
|
1067
|
-
if (!input.parent || !promptBox.parent) return;
|
|
1068
|
-
|
|
1069
|
-
promptBox.width = content.length + 1; // content + spacer
|
|
1070
|
-
input.left = promptBox.width;
|
|
1071
|
-
input.width = `100%-${promptBox.width}`;
|
|
1072
|
-
|
|
1073
|
-
resizeInput();
|
|
1074
|
-
if (typeof input._updateCursor === "function") {
|
|
1075
|
-
input._updateCursor();
|
|
1076
|
-
}
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1079
|
-
function syncTargetFromSelection() {
|
|
1080
|
-
if (focusMode !== "dashboard" || dashboardView !== "agents") return;
|
|
1081
|
-
if (selectedAgentIndex >= 0 && selectedAgentIndex < activeAgents.length) {
|
|
1082
|
-
const nextTarget = activeAgents[selectedAgentIndex];
|
|
1083
|
-
if (nextTarget !== targetAgent) {
|
|
1084
|
-
targetAgent = nextTarget;
|
|
1085
|
-
updatePromptBox();
|
|
1086
|
-
screen.render();
|
|
1087
|
-
}
|
|
1088
|
-
} else if (targetAgent) {
|
|
1089
|
-
targetAgent = null;
|
|
1090
|
-
updatePromptBox();
|
|
1091
|
-
screen.render();
|
|
1092
|
-
}
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
function restoreTargetFromSelection() {
|
|
1096
|
-
if (selectedAgentIndex >= 0 && selectedAgentIndex < activeAgents.length) {
|
|
1097
|
-
targetAgent = activeAgents[selectedAgentIndex];
|
|
1098
|
-
updatePromptBox();
|
|
1099
|
-
}
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
|
-
function focusInput() {
|
|
1103
|
-
input.focus();
|
|
1104
|
-
input._updateCursor();
|
|
1105
|
-
}
|
|
1106
|
-
|
|
1107
|
-
function focusLog() {
|
|
1108
|
-
logBox.focus();
|
|
1109
|
-
screen.program.hideCursor();
|
|
1110
|
-
}
|
|
1111
|
-
|
|
1112
|
-
function scrollLog(offset) {
|
|
1113
|
-
logBox.scroll(offset);
|
|
1114
|
-
screen.render();
|
|
1115
|
-
}
|
|
1116
|
-
|
|
1117
|
-
let settingsController = null;
|
|
1118
|
-
|
|
1119
|
-
function setLaunchMode(mode) {
|
|
1120
|
-
if (settingsController) {
|
|
1121
|
-
settingsController.setLaunchMode(mode);
|
|
1122
|
-
}
|
|
1123
|
-
}
|
|
1124
|
-
|
|
1125
|
-
function requestCloseAgent(agentId) {
|
|
1126
|
-
if (!agentId) {
|
|
1127
|
-
logMessage("error", "{white-fg}✗{/white-fg} No agent selected");
|
|
1128
|
-
return;
|
|
1129
|
-
}
|
|
1130
|
-
send({ type: IPC_REQUEST_TYPES.CLOSE_AGENT, agent_id: agentId });
|
|
1131
|
-
}
|
|
1132
|
-
|
|
1133
|
-
function setAgentProvider(provider) {
|
|
1134
|
-
if (settingsController) {
|
|
1135
|
-
settingsController.setAgentProvider(provider);
|
|
1136
|
-
}
|
|
1137
|
-
}
|
|
1138
|
-
|
|
1139
|
-
function setAutoResume(value) {
|
|
1140
|
-
if (settingsController) {
|
|
1141
|
-
settingsController.setAutoResume(value);
|
|
1142
|
-
}
|
|
1143
|
-
}
|
|
1144
|
-
|
|
1145
|
-
async function restartDaemon() {
|
|
1146
|
-
if (!daemonCoordinator) return;
|
|
1147
|
-
return daemonCoordinator.restart();
|
|
1148
|
-
}
|
|
1149
|
-
|
|
1150
|
-
settingsController = createSettingsController({
|
|
1151
|
-
projectRoot,
|
|
1152
|
-
saveConfig,
|
|
1153
|
-
normalizeLaunchMode,
|
|
1154
|
-
normalizeAgentProvider,
|
|
1155
|
-
fsModule: fs,
|
|
1156
|
-
getUfooPaths,
|
|
1157
|
-
logMessage,
|
|
1158
|
-
resolveStatusLine,
|
|
1159
|
-
renderDashboard,
|
|
1160
|
-
renderScreen: () => screen.render(),
|
|
1161
|
-
restartDaemon,
|
|
1162
|
-
getLaunchMode: () => launchMode,
|
|
1163
|
-
setLaunchModeState: (value) => {
|
|
1164
|
-
launchMode = value;
|
|
1165
|
-
},
|
|
1166
|
-
setSelectedModeIndex: (value) => {
|
|
1167
|
-
selectedModeIndex = value;
|
|
1168
|
-
},
|
|
1169
|
-
getAgentProvider: () => agentProvider,
|
|
1170
|
-
setAgentProviderState: (value) => {
|
|
1171
|
-
agentProvider = value;
|
|
1172
|
-
},
|
|
1173
|
-
setSelectedProviderIndex: (value) => {
|
|
1174
|
-
selectedProviderIndex = value;
|
|
1175
|
-
},
|
|
1176
|
-
providerOptions,
|
|
1177
|
-
modeOptions: MODE_OPTIONS,
|
|
1178
|
-
getAutoResume: () => autoResume,
|
|
1179
|
-
setAutoResumeState: (value) => {
|
|
1180
|
-
autoResume = value;
|
|
1181
|
-
},
|
|
1182
|
-
setSelectedResumeIndex: (value) => {
|
|
1183
|
-
selectedResumeIndex = value;
|
|
1184
|
-
},
|
|
1185
|
-
});
|
|
1186
|
-
|
|
1187
|
-
function clearLog() {
|
|
1188
|
-
logBox.setContent("");
|
|
1189
|
-
if (typeof logBox.scrollTo === "function") {
|
|
1190
|
-
logBox.scrollTo(0);
|
|
1191
|
-
}
|
|
1192
|
-
screen.render();
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1195
|
-
function renderDashboard() {
|
|
1196
|
-
const computed = computeDashboardContent({
|
|
1197
|
-
globalMode,
|
|
1198
|
-
globalScope,
|
|
1199
|
-
focusMode,
|
|
1200
|
-
dashboardView,
|
|
1201
|
-
activeAgents,
|
|
1202
|
-
projects: projectRuntimes,
|
|
1203
|
-
selectedProjectIndex,
|
|
1204
|
-
projectListWindowStart,
|
|
1205
|
-
maxProjectWindow: MAX_PROJECT_WINDOW,
|
|
1206
|
-
activeProjectRoot,
|
|
1207
|
-
selectedAgentIndex,
|
|
1208
|
-
agentListWindowStart,
|
|
1209
|
-
maxAgentWindow: MAX_AGENT_WINDOW,
|
|
1210
|
-
getAgentLabel,
|
|
1211
|
-
getAgentState: (agentId) => {
|
|
1212
|
-
let metaState = "";
|
|
1213
|
-
if (activeAgentMetaMap) {
|
|
1214
|
-
const meta = activeAgentMetaMap.get(agentId);
|
|
1215
|
-
metaState = meta && typeof meta.activity_state === "string"
|
|
1216
|
-
? String(meta.activity_state).trim()
|
|
1217
|
-
: "";
|
|
1218
|
-
}
|
|
1219
|
-
if (metaState) return metaState;
|
|
1220
|
-
return getTransientAgentState(transientAgentStateMap, agentId, {
|
|
1221
|
-
ttlMs: DEFAULT_TRANSIENT_AGENT_STATE_TTL_MS,
|
|
1222
|
-
});
|
|
1223
|
-
},
|
|
1224
|
-
launchMode,
|
|
1225
|
-
agentProvider,
|
|
1226
|
-
autoResume,
|
|
1227
|
-
selectedModeIndex,
|
|
1228
|
-
selectedProviderIndex,
|
|
1229
|
-
selectedResumeIndex,
|
|
1230
|
-
selectedCronIndex,
|
|
1231
|
-
cronTasks,
|
|
1232
|
-
loopSummary,
|
|
1233
|
-
providerOptions,
|
|
1234
|
-
resumeOptions,
|
|
1235
|
-
dashHints: DASH_HINTS,
|
|
1236
|
-
modeOptions: MODE_OPTIONS,
|
|
1237
|
-
});
|
|
1238
|
-
if (globalMode && (focusMode !== "dashboard" || dashboardView === "projects")) {
|
|
1239
|
-
projectListWindowStart = computed.windowStart;
|
|
1240
|
-
} else {
|
|
1241
|
-
agentListWindowStart = computed.windowStart;
|
|
1242
|
-
}
|
|
1243
|
-
let dashboardContent = computed.content;
|
|
1244
|
-
if (globalMode && !String(dashboardContent || "").includes("\n")) {
|
|
1245
|
-
dashboardContent = `${dashboardContent}\n `;
|
|
1246
|
-
}
|
|
1247
|
-
dashboard.setContent(dashboardContent);
|
|
1248
|
-
}
|
|
1249
|
-
|
|
1250
|
-
function readDiskMetaForActiveAgents(activeList = []) {
|
|
1251
|
-
const map = new Map();
|
|
1252
|
-
const ids = Array.isArray(activeList) ? activeList : [];
|
|
1253
|
-
if (ids.length === 0) return map;
|
|
1254
|
-
try {
|
|
1255
|
-
const busPath = getUfooPaths(activeProjectRoot).agentsFile;
|
|
1256
|
-
if (!fs.existsSync(busPath)) return map;
|
|
1257
|
-
const bus = JSON.parse(fs.readFileSync(busPath, "utf8"));
|
|
1258
|
-
const agents = bus && bus.agents && typeof bus.agents === "object" ? bus.agents : {};
|
|
1259
|
-
for (const id of ids) {
|
|
1260
|
-
const meta = agents[id];
|
|
1261
|
-
if (!meta || typeof meta !== "object") continue;
|
|
1262
|
-
map.set(id, meta);
|
|
1263
|
-
}
|
|
1264
|
-
} catch {
|
|
1265
|
-
// ignore disk fallback errors
|
|
1266
|
-
}
|
|
1267
|
-
return map;
|
|
1268
|
-
}
|
|
1269
|
-
|
|
1270
|
-
function updateDashboard(status) {
|
|
1271
|
-
activeAgents = status.active || [];
|
|
1272
|
-
pruneTransientAgentStates(transientAgentStateMap, activeAgents, {
|
|
1273
|
-
ttlMs: DEFAULT_TRANSIENT_AGENT_STATE_TTL_MS,
|
|
1274
|
-
});
|
|
1275
|
-
if (globalMode) {
|
|
1276
|
-
refreshProjectRuntimes();
|
|
1277
|
-
}
|
|
1278
|
-
cronTasks = Array.isArray(status?.cron?.tasks) ? status.cron.tasks : [];
|
|
1279
|
-
loopSummary = status && status.loop && typeof status.loop === "object" ? status.loop : null;
|
|
1280
|
-
const metaList = Array.isArray(status.active_meta) ? status.active_meta : [];
|
|
1281
|
-
let fallbackMap = null;
|
|
1282
|
-
if (metaList.length === 0 && activeAgents.length > 0) {
|
|
1283
|
-
try {
|
|
1284
|
-
const busPath = getUfooPaths(activeProjectRoot).agentsFile;
|
|
1285
|
-
const bus = JSON.parse(fs.readFileSync(busPath, "utf8"));
|
|
1286
|
-
fallbackMap = new Map();
|
|
1287
|
-
for (const [id, meta] of Object.entries(bus.agents || {})) {
|
|
1288
|
-
if (!meta) continue;
|
|
1289
|
-
const displayNickname = resolveDisplayNickname(activeProjectRoot, meta);
|
|
1290
|
-
if (displayNickname) fallbackMap.set(id, displayNickname);
|
|
1291
|
-
}
|
|
1292
|
-
} catch {
|
|
1293
|
-
fallbackMap = null;
|
|
1294
|
-
}
|
|
1295
|
-
}
|
|
1296
|
-
const maps = agentDirectory.buildAgentMaps(activeAgents, metaList, fallbackMap);
|
|
1297
|
-
activeAgentLabelMap = maps.labelMap;
|
|
1298
|
-
const diskMetaMap = readDiskMetaForActiveAgents(activeAgents);
|
|
1299
|
-
if (diskMetaMap.size > 0) {
|
|
1300
|
-
const mergedMetaMap = new Map(maps.metaMap);
|
|
1301
|
-
for (const id of activeAgents) {
|
|
1302
|
-
const currentMeta = mergedMetaMap.get(id);
|
|
1303
|
-
const diskMeta = diskMetaMap.get(id);
|
|
1304
|
-
if (!currentMeta && diskMeta) {
|
|
1305
|
-
mergedMetaMap.set(id, { id, ...diskMeta });
|
|
1306
|
-
continue;
|
|
1307
|
-
}
|
|
1308
|
-
if (!currentMeta || !diskMeta) continue;
|
|
1309
|
-
const currentState = typeof currentMeta.activity_state === "string"
|
|
1310
|
-
? String(currentMeta.activity_state).trim()
|
|
1311
|
-
: "";
|
|
1312
|
-
const diskState = typeof diskMeta.activity_state === "string"
|
|
1313
|
-
? String(diskMeta.activity_state).trim()
|
|
1314
|
-
: "";
|
|
1315
|
-
if (!currentState && diskState) {
|
|
1316
|
-
mergedMetaMap.set(id, {
|
|
1317
|
-
...currentMeta,
|
|
1318
|
-
activity_state: diskState,
|
|
1319
|
-
activity_since: currentMeta.activity_since || diskMeta.activity_since || "",
|
|
1320
|
-
activity_detail: currentMeta.activity_detail || diskMeta.activity_detail || "",
|
|
1321
|
-
});
|
|
1322
|
-
}
|
|
1323
|
-
}
|
|
1324
|
-
activeAgentMetaMap = mergedMetaMap;
|
|
1325
|
-
} else {
|
|
1326
|
-
activeAgentMetaMap = maps.metaMap;
|
|
1327
|
-
}
|
|
1328
|
-
clampAgentWindow();
|
|
1329
|
-
// If viewing agent went offline, exit view
|
|
1330
|
-
const currentView = getCurrentView();
|
|
1331
|
-
const viewingAgent = getViewingAgent();
|
|
1332
|
-
if (currentView === "agent" && viewingAgent && !activeAgents.includes(viewingAgent)) {
|
|
1333
|
-
writeToAgentTerm("\r\n\x1b[1;31m[Agent went offline]\x1b[0m\r\n");
|
|
1334
|
-
exitAgentView();
|
|
1335
|
-
return;
|
|
1336
|
-
}
|
|
1337
|
-
|
|
1338
|
-
// In agent view, only update the dashboard bar (blessed is frozen)
|
|
1339
|
-
if (currentView === "agent") {
|
|
1340
|
-
if (focusMode === "dashboard") {
|
|
1341
|
-
const totalItems = 1 + activeAgents.length;
|
|
1342
|
-
if (selectedAgentIndex < 0 || selectedAgentIndex >= totalItems) {
|
|
1343
|
-
selectedAgentIndex = 0;
|
|
1344
|
-
}
|
|
1345
|
-
}
|
|
1346
|
-
if (agentViewController && typeof agentViewController.refreshAgentView === "function") {
|
|
1347
|
-
agentViewController.refreshAgentView();
|
|
1348
|
-
} else {
|
|
1349
|
-
renderAgentDashboard();
|
|
1350
|
-
}
|
|
1351
|
-
return;
|
|
1352
|
-
}
|
|
1353
|
-
if (focusMode === "dashboard") {
|
|
1354
|
-
if (dashboardView === "agents") {
|
|
1355
|
-
if (activeAgents.length === 0) {
|
|
1356
|
-
selectedAgentIndex = -1;
|
|
1357
|
-
} else if (selectedAgentIndex < 0 || selectedAgentIndex >= activeAgents.length) {
|
|
1358
|
-
selectedAgentIndex = 0;
|
|
1359
|
-
}
|
|
1360
|
-
clampAgentWindow();
|
|
1361
|
-
} else if (dashboardView === "cron") {
|
|
1362
|
-
if (cronTasks.length === 0) {
|
|
1363
|
-
selectedCronIndex = -1;
|
|
1364
|
-
} else if (selectedCronIndex < 0 || selectedCronIndex >= cronTasks.length) {
|
|
1365
|
-
selectedCronIndex = Math.max(0, Math.min(selectedCronIndex, cronTasks.length - 1));
|
|
1366
|
-
}
|
|
1367
|
-
}
|
|
1368
|
-
}
|
|
1369
|
-
syncTargetFromSelection();
|
|
1370
|
-
renderDashboard();
|
|
1371
|
-
screen.render();
|
|
1372
|
-
}
|
|
1373
|
-
|
|
1374
|
-
function enterDashboardMode() {
|
|
1375
|
-
focusMode = "dashboard";
|
|
1376
|
-
dashboardView = globalMode ? "projects" : "agents";
|
|
1377
|
-
if (globalMode) {
|
|
1378
|
-
refreshProjectRuntimes();
|
|
1379
|
-
if (globalScope === "project") {
|
|
1380
|
-
syncSelectedProjectToActive();
|
|
1381
|
-
} else {
|
|
1382
|
-
// Controller scope: no active project in list, init to 0 for navigation
|
|
1383
|
-
if (projectRuntimes.length > 0 && (selectedProjectIndex < 0 || selectedProjectIndex >= projectRuntimes.length)) {
|
|
1384
|
-
selectedProjectIndex = 0;
|
|
1385
|
-
}
|
|
1386
|
-
}
|
|
1387
|
-
} else {
|
|
1388
|
-
selectedAgentIndex = activeAgents.length > 0 ? 0 : -1;
|
|
1389
|
-
agentListWindowStart = 0;
|
|
1390
|
-
clampAgentWindow();
|
|
1391
|
-
}
|
|
1392
|
-
selectedModeIndex = Math.max(0, MODE_OPTIONS.indexOf(launchMode));
|
|
1393
|
-
selectedProviderIndex = Math.max(0, providerOptions.findIndex((opt) => opt.value === agentProvider));
|
|
1394
|
-
selectedResumeIndex = autoResume ? 0 : 1;
|
|
1395
|
-
selectedCronIndex = cronTasks.length > 0 ? 0 : -1;
|
|
1396
|
-
// Immediately set @target when first agent is selected.
|
|
1397
|
-
if (!globalMode && selectedAgentIndex >= 0 && selectedAgentIndex < activeAgents.length) {
|
|
1398
|
-
targetAgent = activeAgents[selectedAgentIndex];
|
|
1399
|
-
updatePromptBox();
|
|
1400
|
-
}
|
|
1401
|
-
screen.grabKeys = true;
|
|
1402
|
-
renderDashboard();
|
|
1403
|
-
screen.program.hideCursor();
|
|
1404
|
-
screen.render();
|
|
1405
|
-
syncTargetFromSelection();
|
|
1406
|
-
}
|
|
1407
|
-
|
|
1408
|
-
const dashboardState = {};
|
|
1409
|
-
Object.defineProperties(dashboardState, {
|
|
1410
|
-
currentView: { get: () => getCurrentView() },
|
|
1411
|
-
focusMode: { get: () => focusMode, set: (value) => { focusMode = value; } },
|
|
1412
|
-
dashboardView: { get: () => dashboardView, set: (value) => { dashboardView = value; } },
|
|
1413
|
-
selectedProjectIndex: { get: () => selectedProjectIndex, set: (value) => { selectedProjectIndex = value; } },
|
|
1414
|
-
projects: { get: () => projectRuntimes },
|
|
1415
|
-
activeProjectRoot: { get: () => activeProjectRoot },
|
|
1416
|
-
selectedAgentIndex: { get: () => selectedAgentIndex, set: (value) => { selectedAgentIndex = value; } },
|
|
1417
|
-
activeAgents: { get: () => activeAgents },
|
|
1418
|
-
viewingAgent: { get: () => getViewingAgent() },
|
|
1419
|
-
activeAgentMetaMap: { get: () => activeAgentMetaMap },
|
|
1420
|
-
selectedModeIndex: { get: () => selectedModeIndex, set: (value) => { selectedModeIndex = value; } },
|
|
1421
|
-
selectedProviderIndex: { get: () => selectedProviderIndex, set: (value) => { selectedProviderIndex = value; } },
|
|
1422
|
-
selectedResumeIndex: { get: () => selectedResumeIndex, set: (value) => { selectedResumeIndex = value; } },
|
|
1423
|
-
selectedCronIndex: { get: () => selectedCronIndex, set: (value) => { selectedCronIndex = value; } },
|
|
1424
|
-
launchMode: { get: () => launchMode },
|
|
1425
|
-
agentProvider: { get: () => agentProvider },
|
|
1426
|
-
autoResume: { get: () => autoResume },
|
|
1427
|
-
cronTasks: { get: () => cronTasks },
|
|
1428
|
-
providerOptions: { get: () => providerOptions },
|
|
1429
|
-
resumeOptions: { get: () => resumeOptions },
|
|
1430
|
-
agentOutputSuppressed: {
|
|
1431
|
-
get: () => getAgentOutputSuppressed(),
|
|
1432
|
-
set: (value) => { setAgentOutputSuppressed(value); },
|
|
1433
|
-
},
|
|
1434
|
-
});
|
|
1435
|
-
|
|
1436
|
-
function activateAgent(agentId) {
|
|
1437
|
-
if (!agentId) return;
|
|
1438
|
-
const activator = new AgentActivator(activeProjectRoot);
|
|
1439
|
-
activator.activate(agentId).catch(() => {});
|
|
1440
|
-
}
|
|
1441
|
-
|
|
1442
|
-
terminalAdapterRouter = createTerminalAdapterRouter({
|
|
1443
|
-
activateAgent,
|
|
1444
|
-
sendRaw: (data) => agentSockets.sendRaw(data),
|
|
1445
|
-
sendResize: (cols, rows) => agentSockets.sendResize(cols, rows),
|
|
1446
|
-
requestSnapshot: (mode) => agentSockets.requestSnapshot(mode),
|
|
1447
|
-
});
|
|
1448
|
-
|
|
1449
|
-
const dashboardController = createDashboardKeyController({
|
|
1450
|
-
state: dashboardState,
|
|
1451
|
-
globalMode,
|
|
1452
|
-
existsSync: fs.existsSync,
|
|
1453
|
-
getInjectSockPath,
|
|
1454
|
-
getAgentAdapter,
|
|
1455
|
-
activateAgent,
|
|
1456
|
-
requestCloseAgent,
|
|
1457
|
-
enterAgentView,
|
|
1458
|
-
exitAgentView,
|
|
1459
|
-
setAgentBarVisible,
|
|
1460
|
-
requestAgentSnapshot,
|
|
1461
|
-
clearTargetAgent,
|
|
1462
|
-
restoreTargetFromSelection,
|
|
1463
|
-
syncTargetFromSelection,
|
|
1464
|
-
exitDashboardMode,
|
|
1465
|
-
setLaunchMode,
|
|
1466
|
-
setAgentProvider,
|
|
1467
|
-
setAutoResume,
|
|
1468
|
-
clampAgentWindow,
|
|
1469
|
-
clampAgentWindowWithSelection,
|
|
1470
|
-
requestProjectSwitch: requestProjectSwitchByIndex,
|
|
1471
|
-
requestCloseProject: requestCloseProjectByIndex,
|
|
1472
|
-
requestCron: (payload = {}) => {
|
|
1473
|
-
send({
|
|
1474
|
-
type: IPC_REQUEST_TYPES.CRON,
|
|
1475
|
-
...payload,
|
|
1476
|
-
});
|
|
1477
|
-
},
|
|
1478
|
-
setGlobalScope: (scope, targetProjectRoot) => {
|
|
1479
|
-
setGlobalScope(scope, targetProjectRoot).catch((err) => {
|
|
1480
|
-
const message = err && err.message ? err.message : String(err || "scope switch failed");
|
|
1481
|
-
logMessage("error", `{white-fg}✗{/white-fg} Scope switch failed: ${escapeBlessed(message)}`);
|
|
1482
|
-
});
|
|
1483
|
-
},
|
|
1484
|
-
getGlobalScope: () => globalScope,
|
|
1485
|
-
renderDashboard,
|
|
1486
|
-
renderAgentDashboard,
|
|
1487
|
-
renderScreen: () => screen.render(),
|
|
1488
|
-
setScreenGrabKeys: (value) => {
|
|
1489
|
-
screen.grabKeys = Boolean(value);
|
|
1490
|
-
},
|
|
1491
|
-
modeOptions: MODE_OPTIONS,
|
|
1492
|
-
});
|
|
1493
|
-
|
|
1494
|
-
function handleDashboardKey(key) {
|
|
1495
|
-
return dashboardController.handleDashboardKey(key);
|
|
1496
|
-
}
|
|
1497
|
-
|
|
1498
|
-
function exitDashboardMode(selectAgent = false) {
|
|
1499
|
-
if (selectAgent && selectedAgentIndex >= 0 && selectedAgentIndex < activeAgents.length) {
|
|
1500
|
-
targetAgent = activeAgents[selectedAgentIndex];
|
|
1501
|
-
updatePromptBox();
|
|
1502
|
-
}
|
|
1503
|
-
focusMode = "input";
|
|
1504
|
-
dashboardView = globalMode ? "projects" : "agents";
|
|
1505
|
-
selectedAgentIndex = -1;
|
|
1506
|
-
// Keep selectedProjectIndex across focus transitions so global rail preserves context.
|
|
1507
|
-
screen.grabKeys = false;
|
|
1508
|
-
renderDashboard();
|
|
1509
|
-
focusInput();
|
|
1510
|
-
screen.render();
|
|
1511
|
-
}
|
|
1512
|
-
|
|
1513
|
-
function clearTargetAgent() {
|
|
1514
|
-
targetAgent = null;
|
|
1515
|
-
updatePromptBox();
|
|
1516
|
-
screen.render();
|
|
1517
|
-
}
|
|
1518
|
-
|
|
1519
|
-
function getInjectSockPath(agentId) {
|
|
1520
|
-
const safeName = subscriberToSafeName(agentId);
|
|
1521
|
-
return path.join(getUfooPaths(activeProjectRoot).busQueuesDir, safeName, "inject.sock");
|
|
1522
|
-
}
|
|
1523
|
-
|
|
1524
|
-
agentViewController = createAgentViewController({
|
|
1525
|
-
screen,
|
|
1526
|
-
input,
|
|
1527
|
-
processStdout: process.stdout,
|
|
1528
|
-
computeAgentBar,
|
|
1529
|
-
agentBarHints: AGENT_BAR_HINTS,
|
|
1530
|
-
maxAgentWindow: MAX_AGENT_WINDOW,
|
|
1531
|
-
getFocusMode: () => focusMode,
|
|
1532
|
-
setFocusMode: (value) => {
|
|
1533
|
-
focusMode = value;
|
|
1534
|
-
},
|
|
1535
|
-
getSelectedAgentIndex: () => selectedAgentIndex,
|
|
1536
|
-
setSelectedAgentIndex: (value) => {
|
|
1537
|
-
selectedAgentIndex = value;
|
|
1538
|
-
},
|
|
1539
|
-
getActiveAgents: () => activeAgents,
|
|
1540
|
-
getAgentListWindowStart: () => agentListWindowStart,
|
|
1541
|
-
setAgentListWindowStart: (value) => {
|
|
1542
|
-
agentListWindowStart = value;
|
|
1543
|
-
},
|
|
1544
|
-
getAgentLabel,
|
|
1545
|
-
getAgentStates: () => {
|
|
1546
|
-
const states = {};
|
|
1547
|
-
for (const id of activeAgents) {
|
|
1548
|
-
let state = "";
|
|
1549
|
-
if (activeAgentMetaMap) {
|
|
1550
|
-
const meta = activeAgentMetaMap.get(id);
|
|
1551
|
-
if (meta && meta.activity_state) state = meta.activity_state;
|
|
1552
|
-
}
|
|
1553
|
-
if (!state) {
|
|
1554
|
-
state = getTransientAgentState(transientAgentStateMap, id, {
|
|
1555
|
-
ttlMs: DEFAULT_TRANSIENT_AGENT_STATE_TTL_MS,
|
|
1556
|
-
});
|
|
1557
|
-
}
|
|
1558
|
-
if (state) states[id] = state;
|
|
1559
|
-
}
|
|
1560
|
-
return states;
|
|
1561
|
-
},
|
|
1562
|
-
getAgentActivityMeta: (agentId) => {
|
|
1563
|
-
const id = String(agentId || "").trim();
|
|
1564
|
-
const meta = activeAgentMetaMap && activeAgentMetaMap.get(id)
|
|
1565
|
-
? { ...activeAgentMetaMap.get(id) }
|
|
1566
|
-
: {};
|
|
1567
|
-
const transient = getTransientAgentStateEntry(transientAgentStateMap, id, {
|
|
1568
|
-
ttlMs: DEFAULT_TRANSIENT_AGENT_STATE_TTL_MS,
|
|
1569
|
-
});
|
|
1570
|
-
if (transient) {
|
|
1571
|
-
const previousState = meta.activity_state;
|
|
1572
|
-
meta.activity_state = transient.state;
|
|
1573
|
-
if ((!meta.activity_since || previousState !== transient.state) && Number.isFinite(transient.updatedAt)) {
|
|
1574
|
-
meta.activity_since = new Date(transient.updatedAt).toISOString();
|
|
1575
|
-
}
|
|
1576
|
-
if (transient.detail) meta.activity_detail = transient.detail;
|
|
1577
|
-
}
|
|
1578
|
-
return meta;
|
|
1579
|
-
},
|
|
1580
|
-
getProjectRoot: () => activeProjectRoot,
|
|
1581
|
-
setDashboardView: (value) => {
|
|
1582
|
-
dashboardView = value;
|
|
1583
|
-
},
|
|
1584
|
-
setScreenGrabKeys: (value) => {
|
|
1585
|
-
screen.grabKeys = Boolean(value);
|
|
1586
|
-
},
|
|
1587
|
-
clearTargetAgent,
|
|
1588
|
-
renderDashboard,
|
|
1589
|
-
focusInput,
|
|
1590
|
-
resizeInput,
|
|
1591
|
-
renderScreen: () => screen.render(),
|
|
1592
|
-
getInjectSockPath,
|
|
1593
|
-
connectAgentOutput: (sockPath) => {
|
|
1594
|
-
agentSockets.connectOutput(sockPath);
|
|
1595
|
-
},
|
|
1596
|
-
disconnectAgentOutput: () => {
|
|
1597
|
-
agentSockets.disconnectOutput();
|
|
1598
|
-
},
|
|
1599
|
-
connectAgentInput: (sockPath) => {
|
|
1600
|
-
agentSockets.connectInput(sockPath);
|
|
1601
|
-
},
|
|
1602
|
-
disconnectAgentInput: () => {
|
|
1603
|
-
agentSockets.disconnectInput();
|
|
1604
|
-
},
|
|
1605
|
-
sendRaw: (data) => {
|
|
1606
|
-
sendRawWithCapabilities(data);
|
|
1607
|
-
},
|
|
1608
|
-
sendBusMessage: (target, message) => {
|
|
1609
|
-
if (!target || !message) return;
|
|
1610
|
-
send({
|
|
1611
|
-
type: IPC_REQUEST_TYPES.BUS_SEND,
|
|
1612
|
-
target,
|
|
1613
|
-
message,
|
|
1614
|
-
injection_mode: "immediate",
|
|
1615
|
-
source: "chat-internal-agent-view",
|
|
1616
|
-
});
|
|
1617
|
-
},
|
|
1618
|
-
sendBusWatch: (agentId, enabled) => {
|
|
1619
|
-
if (!agentId) return;
|
|
1620
|
-
send({
|
|
1621
|
-
type: IPC_REQUEST_TYPES.BUS_WATCH,
|
|
1622
|
-
agent_id: agentId,
|
|
1623
|
-
enabled: enabled !== false,
|
|
1624
|
-
});
|
|
1625
|
-
},
|
|
1626
|
-
sendResize: (cols, rows) => {
|
|
1627
|
-
sendResizeWithCapabilities(cols, rows);
|
|
1628
|
-
},
|
|
1629
|
-
requestScreenSnapshot: () => {
|
|
1630
|
-
requestSnapshotWithCapabilities();
|
|
1631
|
-
},
|
|
1632
|
-
getBusLogHistory: (agentId) => loadInternalAgentLogHistory(activeProjectRoot, agentId),
|
|
1633
|
-
});
|
|
1634
|
-
|
|
1635
|
-
function requestStatus() {
|
|
1636
|
-
if (!daemonCoordinator) return;
|
|
1637
|
-
daemonCoordinator.requestStatus();
|
|
1638
|
-
}
|
|
1639
|
-
|
|
1640
|
-
const daemonMessageRouter = createDaemonMessageRouter({
|
|
1641
|
-
escapeBlessed,
|
|
1642
|
-
stripBlessedTags,
|
|
1643
|
-
logMessage,
|
|
1644
|
-
renderScreen: () => screen.render(),
|
|
1645
|
-
updateDashboard,
|
|
1646
|
-
requestStatus,
|
|
1647
|
-
resolveStatusLine,
|
|
1648
|
-
enqueueBusStatus,
|
|
1649
|
-
resolveBusStatus,
|
|
1650
|
-
getPending: () => pending,
|
|
1651
|
-
setPending: (value) => {
|
|
1652
|
-
pending = value;
|
|
1653
|
-
},
|
|
1654
|
-
resolveAgentDisplayName,
|
|
1655
|
-
getCurrentView: () => getCurrentView(),
|
|
1656
|
-
isAgentViewUsesBus: () => isAgentViewUsesBus(),
|
|
1657
|
-
getViewingAgent: () => getViewingAgent(),
|
|
1658
|
-
isAgentEventForViewingAgent: (data, viewingAgent, publisher) => {
|
|
1659
|
-
if (!viewingAgent) return false;
|
|
1660
|
-
const label = getAgentLabel(viewingAgent);
|
|
1661
|
-
const candidates = [
|
|
1662
|
-
publisher,
|
|
1663
|
-
data && data.publisher,
|
|
1664
|
-
data && data.target,
|
|
1665
|
-
data && data.subscriber,
|
|
1666
|
-
].filter(Boolean);
|
|
1667
|
-
return candidates.some((value) => (
|
|
1668
|
-
value === viewingAgent ||
|
|
1669
|
-
value === label ||
|
|
1670
|
-
resolveAgentId(value) === viewingAgent
|
|
1671
|
-
));
|
|
1672
|
-
},
|
|
1673
|
-
writeToAgentTerm,
|
|
1674
|
-
consumePendingDelivery,
|
|
1675
|
-
getPendingState,
|
|
1676
|
-
beginStream,
|
|
1677
|
-
appendStreamDelta,
|
|
1678
|
-
finalizeStream,
|
|
1679
|
-
hasStream: (publisher) => streamTracker.hasStream(publisher),
|
|
1680
|
-
setTransientAgentState: (agentId, state, options) => {
|
|
1681
|
-
if (!agentId || !state) return;
|
|
1682
|
-
setTransientAgentStateValue(transientAgentStateMap, agentId, state, options);
|
|
1683
|
-
},
|
|
1684
|
-
clearTransientAgentState: (agentId) => {
|
|
1685
|
-
if (!agentId) return;
|
|
1686
|
-
transientAgentStateMap.delete(agentId);
|
|
1687
|
-
},
|
|
1688
|
-
refreshDashboard: () => {
|
|
1689
|
-
if (getCurrentView() === "agent") {
|
|
1690
|
-
if (agentViewController && typeof agentViewController.refreshAgentView === "function") {
|
|
1691
|
-
agentViewController.refreshAgentView();
|
|
1692
|
-
} else {
|
|
1693
|
-
renderAgentDashboard();
|
|
1694
|
-
}
|
|
1695
|
-
return;
|
|
1696
|
-
}
|
|
1697
|
-
renderDashboard();
|
|
1698
|
-
},
|
|
1699
|
-
});
|
|
1700
|
-
|
|
1701
|
-
daemonCoordinator = createDaemonCoordinator({
|
|
1702
|
-
projectRoot,
|
|
1703
|
-
daemonTransport,
|
|
1704
|
-
handleMessage: (msg) => daemonMessageRouter.handleMessage(msg),
|
|
1705
|
-
queueStatusLine,
|
|
1706
|
-
resolveStatusLine,
|
|
1707
|
-
logMessage,
|
|
1708
|
-
stopDaemon,
|
|
1709
|
-
startDaemon,
|
|
1710
|
-
});
|
|
1711
|
-
|
|
1712
|
-
const connected = await daemonCoordinator.connect();
|
|
1713
|
-
if (!connected) {
|
|
1714
|
-
// Check if daemon failed to start
|
|
1715
|
-
if (!isRunning(activeProjectRoot)) {
|
|
1716
|
-
const logFile = getUfooPaths(activeProjectRoot).ufooDaemonLog;
|
|
1717
|
-
// eslint-disable-next-line no-console
|
|
1718
|
-
console.error("Failed to start ufoo daemon. Check logs at:", logFile);
|
|
1719
|
-
throw new Error("Daemon failed to start. Check the daemon log for details.");
|
|
1720
|
-
}
|
|
1721
|
-
throw new Error("Failed to connect to ufoo daemon (timeout). The daemon may still be starting.");
|
|
1722
|
-
}
|
|
1723
|
-
|
|
1724
|
-
function resolveProjectSwitchTarget(rawTarget) {
|
|
1725
|
-
const target = String(rawTarget || "").trim();
|
|
1726
|
-
if (!target) {
|
|
1727
|
-
throw new Error("missing target");
|
|
1728
|
-
}
|
|
1729
|
-
if (/^\d+$/.test(target)) {
|
|
1730
|
-
const index = Number.parseInt(target, 10);
|
|
1731
|
-
if (!Number.isFinite(index) || index <= 0) {
|
|
1732
|
-
throw new Error("invalid project index");
|
|
1733
|
-
}
|
|
1734
|
-
const rows = listProjectRuntimes({ validate: true, cleanupTmp: true });
|
|
1735
|
-
const item = rows[index - 1];
|
|
1736
|
-
if (!item || !item.project_root) {
|
|
1737
|
-
throw new Error("project index out of range");
|
|
1738
|
-
}
|
|
1739
|
-
return {
|
|
1740
|
-
projectRoot: canonicalProjectRoot(item.project_root),
|
|
1741
|
-
source: `index ${index}`,
|
|
1742
|
-
};
|
|
1743
|
-
}
|
|
1744
|
-
return {
|
|
1745
|
-
projectRoot: canonicalProjectRoot(target),
|
|
1746
|
-
source: target,
|
|
1747
|
-
};
|
|
1748
|
-
}
|
|
1749
|
-
|
|
1750
|
-
async function switchProjectConnection(targetInput) {
|
|
1751
|
-
let targetInfo;
|
|
1752
|
-
try {
|
|
1753
|
-
targetInfo = resolveProjectSwitchTarget(targetInput);
|
|
1754
|
-
} catch (err) {
|
|
1755
|
-
return {
|
|
1756
|
-
ok: false,
|
|
1757
|
-
error: err && err.message ? err.message : "invalid project target",
|
|
1758
|
-
};
|
|
1759
|
-
}
|
|
1760
|
-
const nextProjectRoot = targetInfo.projectRoot;
|
|
1761
|
-
if (!nextProjectRoot) {
|
|
1762
|
-
return { ok: false, error: "invalid project target" };
|
|
1763
|
-
}
|
|
1764
|
-
if (nextProjectRoot === activeProjectRoot) {
|
|
1765
|
-
return { ok: true, project_root: activeProjectRoot, unchanged: true };
|
|
1766
|
-
}
|
|
1767
|
-
const outgoingDraftSnapshot = captureCurrentProjectDraft();
|
|
1768
|
-
|
|
1769
|
-
try {
|
|
1770
|
-
const nextPaths = getUfooPaths(nextProjectRoot);
|
|
1771
|
-
if (!fs.existsSync(nextPaths.ufooDir)) {
|
|
1772
|
-
const repoRoot = path.join(__dirname, "..", "..");
|
|
1773
|
-
const init = new UfooInit(repoRoot);
|
|
1774
|
-
await init.init({ modules: "context,bus", project: nextProjectRoot });
|
|
1775
|
-
}
|
|
1776
|
-
if (!isRunning(nextProjectRoot)) {
|
|
1777
|
-
startDaemon(nextProjectRoot);
|
|
1778
|
-
}
|
|
1779
|
-
const result = await daemonCoordinator.switchProject({
|
|
1780
|
-
projectRoot: nextProjectRoot,
|
|
1781
|
-
sockPath: socketPath(nextProjectRoot),
|
|
1782
|
-
});
|
|
1783
|
-
if (!result || result.ok !== true) {
|
|
1784
|
-
return {
|
|
1785
|
-
ok: false,
|
|
1786
|
-
error: (result && result.error) || "switch failed",
|
|
1787
|
-
};
|
|
1788
|
-
}
|
|
1789
|
-
const previousProjectRoot = activeProjectRoot;
|
|
1790
|
-
if (previousProjectRoot && previousProjectRoot !== nextProjectRoot) {
|
|
1791
|
-
setProjectDraft(previousProjectRoot, outgoingDraftSnapshot);
|
|
1792
|
-
}
|
|
1793
|
-
activeProjectRoot = nextProjectRoot;
|
|
1794
|
-
applyProjectHistoryContext(nextProjectRoot);
|
|
1795
|
-
if (globalMode) {
|
|
1796
|
-
refreshProjectRuntimes();
|
|
1797
|
-
syncSelectedProjectToActive();
|
|
1798
|
-
updatePromptBox();
|
|
1799
|
-
renderDashboard();
|
|
1800
|
-
screen.render();
|
|
1801
|
-
}
|
|
1802
|
-
return {
|
|
1803
|
-
ok: true,
|
|
1804
|
-
project_root: activeProjectRoot,
|
|
1805
|
-
};
|
|
1806
|
-
} catch (err) {
|
|
1807
|
-
return {
|
|
1808
|
-
ok: false,
|
|
1809
|
-
error: err && err.message ? err.message : "switch failed",
|
|
1810
|
-
};
|
|
1811
|
-
}
|
|
1812
|
-
}
|
|
1813
|
-
|
|
1814
|
-
async function setGlobalScope(scope, targetProjectRoot) {
|
|
1815
|
-
if (!globalMode) return;
|
|
1816
|
-
|
|
1817
|
-
if (scope === "controller") {
|
|
1818
|
-
if (globalScope === "controller") return;
|
|
1819
|
-
const controllerRoot = resolveGlobalControllerProjectRoot();
|
|
1820
|
-
if (activeProjectRoot !== controllerRoot) {
|
|
1821
|
-
const result = await requestProjectSwitchByTarget(controllerRoot);
|
|
1822
|
-
if (!result || !result.ok) {
|
|
1823
|
-
const reason = (result && result.error) || "switch to controller failed";
|
|
1824
|
-
logMessage("error", `{white-fg}✗{/white-fg} Scope switch failed: ${escapeBlessed(reason)}`);
|
|
1825
|
-
return;
|
|
1826
|
-
}
|
|
1827
|
-
}
|
|
1828
|
-
globalScope = "controller";
|
|
1829
|
-
targetAgent = null;
|
|
1830
|
-
updatePromptBox();
|
|
1831
|
-
if (projectRuntimes.length > 0 && (selectedProjectIndex < 0 || selectedProjectIndex >= projectRuntimes.length)) {
|
|
1832
|
-
selectedProjectIndex = 0;
|
|
1833
|
-
}
|
|
1834
|
-
renderDashboard();
|
|
1835
|
-
screen.render();
|
|
1836
|
-
} else if (scope === "project") {
|
|
1837
|
-
if (!targetProjectRoot) return;
|
|
1838
|
-
targetAgent = null;
|
|
1839
|
-
const result = await requestProjectSwitchByTarget(targetProjectRoot);
|
|
1840
|
-
if (!result || !result.ok) {
|
|
1841
|
-
const reason = (result && result.error) || "switch to project failed";
|
|
1842
|
-
logMessage("error", `{white-fg}✗{/white-fg} Scope switch failed: ${escapeBlessed(reason)}`);
|
|
1843
|
-
return;
|
|
1844
|
-
}
|
|
1845
|
-
globalScope = "project";
|
|
1846
|
-
updatePromptBox();
|
|
1847
|
-
renderDashboard();
|
|
1848
|
-
screen.render();
|
|
1849
|
-
}
|
|
1850
|
-
}
|
|
1851
|
-
|
|
1852
|
-
let projectSwitching = false;
|
|
1853
|
-
let pendingProjectSwitchRoot = null;
|
|
1854
|
-
let projectSwitchDebounceTimer = null;
|
|
1855
|
-
let projectSwitchFlushPromise = null;
|
|
1856
|
-
const PROJECT_SWITCH_DEBOUNCE_MS = 200;
|
|
1857
|
-
|
|
1858
|
-
function cancelProjectSwitchDebounce() {
|
|
1859
|
-
if (!projectSwitchDebounceTimer) return;
|
|
1860
|
-
clearTimeout(projectSwitchDebounceTimer);
|
|
1861
|
-
projectSwitchDebounceTimer = null;
|
|
1862
|
-
}
|
|
1863
|
-
|
|
1864
|
-
function scheduleProjectSwitchFlush(delayMs = PROJECT_SWITCH_DEBOUNCE_MS) {
|
|
1865
|
-
cancelProjectSwitchDebounce();
|
|
1866
|
-
projectSwitchDebounceTimer = setTimeout(() => {
|
|
1867
|
-
projectSwitchDebounceTimer = null;
|
|
1868
|
-
flushPendingProjectSwitch().catch((err) => {
|
|
1869
|
-
const message = err && err.message ? err.message : String(err || "switch failed");
|
|
1870
|
-
logMessage("error", `{white-fg}✗{/white-fg} Switch failed: ${escapeBlessed(message)}`);
|
|
1871
|
-
});
|
|
1872
|
-
}, Math.max(0, Number.isFinite(delayMs) ? delayMs : PROJECT_SWITCH_DEBOUNCE_MS));
|
|
1873
|
-
}
|
|
1874
|
-
|
|
1875
|
-
async function flushPendingProjectSwitch() {
|
|
1876
|
-
if (projectSwitchFlushPromise) {
|
|
1877
|
-
return projectSwitchFlushPromise;
|
|
1878
|
-
}
|
|
1879
|
-
projectSwitchFlushPromise = (async () => {
|
|
1880
|
-
projectSwitching = true;
|
|
1881
|
-
let lastResult = { ok: true, project_root: activeProjectRoot, unchanged: true };
|
|
1882
|
-
try {
|
|
1883
|
-
while (pendingProjectSwitchRoot) {
|
|
1884
|
-
const nextProjectRoot = pendingProjectSwitchRoot;
|
|
1885
|
-
pendingProjectSwitchRoot = null;
|
|
1886
|
-
if (!nextProjectRoot || nextProjectRoot === activeProjectRoot) continue;
|
|
1887
|
-
const result = await switchProjectConnection(nextProjectRoot);
|
|
1888
|
-
lastResult = result || { ok: false, error: "switch failed" };
|
|
1889
|
-
if (!result || result.ok !== true) {
|
|
1890
|
-
const reason = (result && result.error) || "switch failed";
|
|
1891
|
-
logMessage("error", `{white-fg}✗{/white-fg} Switch failed: ${escapeBlessed(reason)}`);
|
|
1892
|
-
}
|
|
1893
|
-
}
|
|
1894
|
-
return lastResult;
|
|
1895
|
-
} finally {
|
|
1896
|
-
projectSwitching = false;
|
|
1897
|
-
if (globalMode) {
|
|
1898
|
-
refreshProjectRuntimes();
|
|
1899
|
-
syncSelectedProjectToActive();
|
|
1900
|
-
renderDashboard();
|
|
1901
|
-
screen.render();
|
|
1902
|
-
}
|
|
1903
|
-
}
|
|
1904
|
-
})();
|
|
1905
|
-
try {
|
|
1906
|
-
return await projectSwitchFlushPromise;
|
|
1907
|
-
} finally {
|
|
1908
|
-
projectSwitchFlushPromise = null;
|
|
1909
|
-
if (pendingProjectSwitchRoot && !projectSwitchDebounceTimer) {
|
|
1910
|
-
scheduleProjectSwitchFlush(0);
|
|
1911
|
-
}
|
|
1912
|
-
}
|
|
1913
|
-
}
|
|
1914
|
-
|
|
1915
|
-
function requestProjectSwitchByIndex(index) {
|
|
1916
|
-
if (!globalMode) return;
|
|
1917
|
-
const numericIndex = Number(index);
|
|
1918
|
-
const nextIndex = Number.isFinite(numericIndex) ? Math.trunc(numericIndex) : Number.NaN;
|
|
1919
|
-
if (!Number.isFinite(nextIndex) || nextIndex < 0 || nextIndex >= projectRuntimes.length) {
|
|
1920
|
-
return;
|
|
1921
|
-
}
|
|
1922
|
-
selectedProjectIndex = nextIndex;
|
|
1923
|
-
const selected = projectRuntimes[nextIndex] || {};
|
|
1924
|
-
const nextProjectRoot = resolveRuntimeProjectRoot(selected);
|
|
1925
|
-
renderDashboard();
|
|
1926
|
-
screen.render();
|
|
1927
|
-
if (!nextProjectRoot) return;
|
|
1928
|
-
pendingProjectSwitchRoot = nextProjectRoot;
|
|
1929
|
-
scheduleProjectSwitchFlush();
|
|
1930
|
-
}
|
|
1931
|
-
|
|
1932
|
-
async function requestProjectSwitchByTarget(targetInput) {
|
|
1933
|
-
let targetInfo;
|
|
1934
|
-
try {
|
|
1935
|
-
targetInfo = resolveProjectSwitchTarget(targetInput);
|
|
1936
|
-
} catch (err) {
|
|
1937
|
-
return {
|
|
1938
|
-
ok: false,
|
|
1939
|
-
error: err && err.message ? err.message : "invalid project target",
|
|
1940
|
-
};
|
|
1941
|
-
}
|
|
1942
|
-
const nextProjectRoot = targetInfo && targetInfo.projectRoot ? targetInfo.projectRoot : "";
|
|
1943
|
-
if (!nextProjectRoot) {
|
|
1944
|
-
return { ok: false, error: "invalid project target" };
|
|
1945
|
-
}
|
|
1946
|
-
if (nextProjectRoot === activeProjectRoot) {
|
|
1947
|
-
return { ok: true, project_root: activeProjectRoot, unchanged: true };
|
|
1948
|
-
}
|
|
1949
|
-
|
|
1950
|
-
pendingProjectSwitchRoot = nextProjectRoot;
|
|
1951
|
-
cancelProjectSwitchDebounce();
|
|
1952
|
-
|
|
1953
|
-
let attempts = 0;
|
|
1954
|
-
while (attempts < 4) {
|
|
1955
|
-
attempts += 1;
|
|
1956
|
-
const result = await flushPendingProjectSwitch();
|
|
1957
|
-
if (activeProjectRoot === nextProjectRoot) {
|
|
1958
|
-
return { ok: true, project_root: activeProjectRoot };
|
|
1959
|
-
}
|
|
1960
|
-
if (!pendingProjectSwitchRoot) {
|
|
1961
|
-
if (result && result.ok !== true) return result;
|
|
1962
|
-
return { ok: false, error: "switch failed" };
|
|
1963
|
-
}
|
|
1964
|
-
if (pendingProjectSwitchRoot !== nextProjectRoot) {
|
|
1965
|
-
pendingProjectSwitchRoot = nextProjectRoot;
|
|
1966
|
-
}
|
|
1967
|
-
}
|
|
1968
|
-
return { ok: false, error: "switch did not complete" };
|
|
1969
|
-
}
|
|
1970
|
-
|
|
1971
|
-
const projectCloseController = createProjectCloseController({
|
|
1972
|
-
getProjects: () => projectRuntimes,
|
|
1973
|
-
getActiveProjectRoot: () => activeProjectRoot,
|
|
1974
|
-
resolveProjectRoot: resolveRuntimeProjectRoot,
|
|
1975
|
-
isRunning,
|
|
1976
|
-
stopDaemon,
|
|
1977
|
-
switchProject: (targetProjectRoot) => requestProjectSwitchByTarget(targetProjectRoot),
|
|
1978
|
-
refreshProjects: () => {
|
|
1979
|
-
if (!globalMode) return;
|
|
1980
|
-
refreshProjectRuntimes();
|
|
1981
|
-
syncSelectedProjectToActive();
|
|
1982
|
-
},
|
|
1983
|
-
renderDashboard,
|
|
1984
|
-
renderScreen: () => screen.render(),
|
|
1985
|
-
logMessage,
|
|
1986
|
-
resolveStatusLine,
|
|
1987
|
-
escapeBlessed,
|
|
1988
|
-
});
|
|
1989
|
-
|
|
1990
|
-
function requestCloseProjectByIndex(index) {
|
|
1991
|
-
if (!globalMode) return;
|
|
1992
|
-
void projectCloseController.requestCloseProject(index);
|
|
1993
|
-
}
|
|
1994
|
-
|
|
1995
|
-
const commandExecutor = createCommandExecutor({
|
|
1996
|
-
projectRoot,
|
|
1997
|
-
getActiveProjectRoot: () => activeProjectRoot,
|
|
1998
|
-
parseCommand,
|
|
1999
|
-
escapeBlessed,
|
|
2000
|
-
logMessage,
|
|
2001
|
-
resolveStatusLine,
|
|
2002
|
-
renderScreen: () => screen.render(),
|
|
2003
|
-
getActiveAgents: () => activeAgents,
|
|
2004
|
-
getActiveAgentMetaMap: () => activeAgentMetaMap,
|
|
2005
|
-
getAgentLabel,
|
|
2006
|
-
isDaemonRunning: isRunning,
|
|
2007
|
-
startDaemon,
|
|
2008
|
-
stopDaemon,
|
|
2009
|
-
restartDaemon,
|
|
2010
|
-
send,
|
|
2011
|
-
requestStatus,
|
|
2012
|
-
requestCron: (payload = {}) => {
|
|
2013
|
-
send({
|
|
2014
|
-
type: IPC_REQUEST_TYPES.CRON,
|
|
2015
|
-
...payload,
|
|
2016
|
-
});
|
|
2017
|
-
},
|
|
2018
|
-
activateAgent: async (target) => {
|
|
2019
|
-
const activator = new AgentActivator(activeProjectRoot);
|
|
2020
|
-
await activator.activate(target);
|
|
2021
|
-
},
|
|
2022
|
-
listProjects: () => listProjectRuntimes({ validate: true, cleanupTmp: true }),
|
|
2023
|
-
getCurrentProject: () => ({
|
|
2024
|
-
project_root: activeProjectRoot,
|
|
2025
|
-
project_name: globalMode && isGlobalControllerProjectRoot(activeProjectRoot)
|
|
2026
|
-
? "global-controller"
|
|
2027
|
-
: path.basename(activeProjectRoot),
|
|
2028
|
-
}),
|
|
2029
|
-
switchProject: async ({ target } = {}) => requestProjectSwitchByTarget(target),
|
|
2030
|
-
globalMode,
|
|
2031
|
-
});
|
|
2032
|
-
|
|
2033
|
-
async function executeCommand(text) {
|
|
2034
|
-
return commandExecutor.executeCommand(text);
|
|
2035
|
-
}
|
|
2036
|
-
|
|
2037
|
-
const submitState = {};
|
|
2038
|
-
Object.defineProperties(submitState, {
|
|
2039
|
-
targetAgent: { get: () => targetAgent, set: (value) => { targetAgent = value; } },
|
|
2040
|
-
pending: { get: () => pending, set: (value) => { pending = value; } },
|
|
2041
|
-
activeAgentMetaMap: { get: () => activeAgentMetaMap },
|
|
2042
|
-
});
|
|
2043
|
-
|
|
2044
|
-
const inputSubmitHandler = createInputSubmitHandler({
|
|
2045
|
-
state: submitState,
|
|
2046
|
-
parseAtTarget,
|
|
2047
|
-
resolveAgentId,
|
|
2048
|
-
executeCommand,
|
|
2049
|
-
queueStatusLine,
|
|
2050
|
-
send,
|
|
2051
|
-
logMessage,
|
|
2052
|
-
getAgentLabel,
|
|
2053
|
-
escapeBlessed,
|
|
2054
|
-
markPendingDelivery,
|
|
2055
|
-
clearTargetAgent,
|
|
2056
|
-
setTargetAgent: (agentId) => {
|
|
2057
|
-
targetAgent = agentId || null;
|
|
2058
|
-
updatePromptBox();
|
|
2059
|
-
screen.render();
|
|
2060
|
-
},
|
|
2061
|
-
enterAgentView,
|
|
2062
|
-
activateAgent: async (agentId) => {
|
|
2063
|
-
const activator = new AgentActivator(activeProjectRoot);
|
|
2064
|
-
await activator.activate(agentId);
|
|
2065
|
-
},
|
|
2066
|
-
getInjectSockPath,
|
|
2067
|
-
existsSync: fs.existsSync,
|
|
2068
|
-
commitInputHistory: (text) => {
|
|
2069
|
-
if (inputHistoryController) inputHistoryController.commitSubmittedText(text);
|
|
2070
|
-
},
|
|
2071
|
-
focusInput: () => input.focus(),
|
|
2072
|
-
renderScreen: () => screen.render(), // Add renderScreen callback
|
|
2073
|
-
});
|
|
2074
|
-
|
|
2075
|
-
input.on("submit", async (value) => {
|
|
2076
|
-
input.clearValue();
|
|
2077
|
-
screen.render(); // Render cleared input
|
|
2078
|
-
await inputSubmitHandler.handleSubmit(value);
|
|
2079
|
-
// No need for second render - handleSubmit now calls renderScreen() internally
|
|
2080
|
-
});
|
|
2081
|
-
|
|
2082
|
-
screen.key(["C-c"], exitHandler);
|
|
2083
|
-
|
|
2084
|
-
// Agent TTY view: enter dashboard mode
|
|
2085
|
-
function enterAgentDashboardMode() {
|
|
2086
|
-
if (agentViewController) {
|
|
2087
|
-
agentViewController.enterAgentDashboardMode();
|
|
2088
|
-
}
|
|
2089
|
-
}
|
|
2090
|
-
|
|
2091
|
-
// Dashboard navigation - use screen.on to capture even when input is focused
|
|
2092
|
-
screen.on("keypress", (ch, key) => {
|
|
2093
|
-
// Agent TTY view: handle keystrokes
|
|
2094
|
-
if (getCurrentView() === "agent") {
|
|
2095
|
-
if (focusMode === "dashboard") {
|
|
2096
|
-
handleDashboardKey(key);
|
|
2097
|
-
return;
|
|
2098
|
-
}
|
|
2099
|
-
// Suppress input briefly after entering agent view
|
|
2100
|
-
if (Date.now() < getAgentInputSuppressUntil()) {
|
|
2101
|
-
return;
|
|
2102
|
-
}
|
|
2103
|
-
// Ctrl+C exits entire app
|
|
2104
|
-
if (key && key.ctrl && key.name === "c") {
|
|
2105
|
-
return; // handled by screen.key(["C-c"])
|
|
2106
|
-
}
|
|
2107
|
-
if (agentViewController && agentViewController.handleBusAgentKey(ch, key)) {
|
|
2108
|
-
return;
|
|
2109
|
-
}
|
|
2110
|
-
// Down arrow: enter agents bar (same pattern as normal chat dashboard)
|
|
2111
|
-
if (key && key.name === "down") {
|
|
2112
|
-
enterAgentDashboardMode();
|
|
2113
|
-
return;
|
|
2114
|
-
}
|
|
2115
|
-
// All other keys (including Esc) go to agent PTY
|
|
2116
|
-
const raw = keyToRaw(ch, key);
|
|
2117
|
-
if (raw) {
|
|
2118
|
-
sendRawToAgent(raw);
|
|
2119
|
-
}
|
|
2120
|
-
return;
|
|
2121
|
-
}
|
|
2122
|
-
|
|
2123
|
-
// Normal mode: dashboard key handling
|
|
2124
|
-
handleDashboardKey(key);
|
|
2125
|
-
});
|
|
2126
|
-
|
|
2127
|
-
screen.key(["tab"], () => {
|
|
2128
|
-
if (getCurrentView() === "agent") return; // Tab goes to PTY via keypress handler
|
|
2129
|
-
if (focusMode === "dashboard") {
|
|
2130
|
-
exitDashboardMode(false);
|
|
2131
|
-
} else {
|
|
2132
|
-
enterDashboardMode();
|
|
2133
|
-
}
|
|
2134
|
-
});
|
|
2135
|
-
|
|
2136
|
-
screen.key(["C-k", "M-k"], () => {
|
|
2137
|
-
if (getCurrentView() === "agent") return;
|
|
2138
|
-
clearLog();
|
|
2139
|
-
});
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
screen.key(["i", "enter"], () => {
|
|
2143
|
-
if (getCurrentView() === "agent") return;
|
|
2144
|
-
if (focusMode === "dashboard") return;
|
|
2145
|
-
if (screen.focused === input) return;
|
|
2146
|
-
focusInput();
|
|
2147
|
-
});
|
|
2148
|
-
|
|
2149
|
-
focusInput();
|
|
2150
|
-
if (screen.program && typeof screen.program.decset === "function") {
|
|
2151
|
-
screen.program.decset(2004);
|
|
2152
|
-
}
|
|
2153
|
-
if (screen.program) {
|
|
2154
|
-
screen.program.on("data", (data) => {
|
|
2155
|
-
pasteController.handleProgramData(data);
|
|
2156
|
-
});
|
|
2157
|
-
}
|
|
2158
|
-
loadHistory();
|
|
2159
|
-
loadInputHistory();
|
|
2160
|
-
if (globalMode) {
|
|
2161
|
-
inputHistoryController.restoreDraft(getProjectDraft(activeProjectRoot));
|
|
2162
|
-
}
|
|
2163
|
-
if (globalMode) {
|
|
2164
|
-
refreshProjectRuntimes();
|
|
2165
|
-
}
|
|
2166
|
-
updatePromptBox();
|
|
2167
|
-
renderDashboard();
|
|
2168
|
-
resizeInput();
|
|
2169
|
-
requestStatus();
|
|
2170
|
-
|
|
2171
|
-
// 定期刷新 dashboard 状态(兜底,daemon 会主动推送变化)
|
|
2172
|
-
setInterval(() => {
|
|
2173
|
-
if (daemonCoordinator && daemonCoordinator.isConnected()) {
|
|
2174
|
-
requestStatus();
|
|
2175
|
-
}
|
|
2176
|
-
}, 5000);
|
|
2177
|
-
|
|
2178
|
-
// Global mode: watch runtime registry for new/removed projects
|
|
2179
|
-
if (globalMode) {
|
|
2180
|
-
const runtimeDir = resolveRuntimeDir();
|
|
2181
|
-
if (!fs.existsSync(runtimeDir)) {
|
|
2182
|
-
fs.mkdirSync(runtimeDir, { recursive: true });
|
|
2183
|
-
}
|
|
2184
|
-
let runtimeWatchDebounce = null;
|
|
2185
|
-
try {
|
|
2186
|
-
const watcher = fs.watch(runtimeDir, () => {
|
|
2187
|
-
if (runtimeWatchDebounce) return;
|
|
2188
|
-
runtimeWatchDebounce = setTimeout(() => {
|
|
2189
|
-
runtimeWatchDebounce = null;
|
|
2190
|
-
refreshProjectRuntimes();
|
|
2191
|
-
renderDashboard();
|
|
2192
|
-
screen.render();
|
|
2193
|
-
}, 300);
|
|
2194
|
-
});
|
|
2195
|
-
screen.on("destroy", () => watcher.close());
|
|
2196
|
-
} catch {
|
|
2197
|
-
// Fallback: ignore if fs.watch not supported
|
|
2198
|
-
}
|
|
2199
|
-
}
|
|
2200
|
-
screen.on("resize", () => {
|
|
2201
|
-
if (handleResizeInAgentView()) {
|
|
2202
|
-
return;
|
|
2203
|
-
}
|
|
2204
|
-
resizeInput();
|
|
2205
|
-
if (completionController.isActive()) completionController.hide();
|
|
2206
|
-
input._updateCursor();
|
|
2207
|
-
// Force recalculate logBox width to match terminal
|
|
2208
|
-
logBox.width = screen.width;
|
|
2209
|
-
screen.render();
|
|
2210
|
-
});
|
|
2211
|
-
screen.render();
|
|
2212
|
-
}
|
|
2213
|
-
|
|
2214
|
-
async function runChat(projectRoot, options = {}) {
|
|
2215
|
-
if (String(process.env.UFOO_TUI || "").trim().toLowerCase() === "blessed") {
|
|
2216
|
-
return runChatBlessed(projectRoot, options);
|
|
2217
|
-
}
|
|
2218
|
-
const { runChatInk } = require("../ui/components/ChatApp");
|
|
2219
|
-
return runChatInk(projectRoot, options);
|
|
2220
|
-
}
|
|
2221
|
-
|
|
2222
|
-
module.exports = { runChat };
|