u-foo 2.3.31 → 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 +9 -5
- package/scripts/chat-app-smoke.js +30 -0
- package/scripts/global-chat-switch-benchmark.js +5 -5
- package/scripts/ink-demo.js +23 -0
- package/scripts/ink-smoke.js +30 -0
- package/scripts/ucode-app-smoke.js +36 -0
- 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 +56 -28
- 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 +54 -4
- package/src/{chat → app/chat}/daemonTransport.js +2 -1
- package/src/{chat → app/chat}/dashboardView.js +2 -21
- package/src/app/chat/index.js +6 -0
- package/src/{chat → app/chat}/inputSubmitHandler.js +38 -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}/projectCloseController.js +1 -1
- package/src/app/chat/shellCommand.js +42 -0
- package/src/{chat → app/chat}/transport.js +16 -3
- 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} +62 -59
- 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/taskDecomposer.js +5 -4
- package/src/code/tui.js +39 -1997
- 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/{ufoo → coordination/state}/agentRegistryDiagnostics.js +43 -0
- 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 +273 -79
- package/src/{daemon → runtime/daemon}/ipcServer.js +24 -2
- 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 +13 -8
- package/src/runtime/daemon/providerSessions.js +230 -0
- package/src/{daemon → runtime/daemon}/reporting.js +4 -4
- package/src/{daemon → runtime/daemon}/run.js +12 -5
- 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/runtime/process/nodeExecutable.js +26 -0
- package/src/{projects → runtime/projects}/registry.js +1 -1
- package/src/{projects → runtime/projects}/runtimes.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 +336 -0
- package/src/ui/format/index.js +974 -0
- package/src/ui/index.js +9 -0
- package/src/ui/ink/ChatApp.js +3674 -0
- package/src/ui/ink/DashboardBar.js +685 -0
- package/src/ui/ink/InkDemo.js +96 -0
- package/src/ui/ink/MultilineInput.js +612 -0
- package/src/ui/ink/UcodeApp.js +822 -0
- package/src/ui/ink/agentMirror.js +730 -0
- package/src/ui/ink/chatReducer.js +359 -0
- package/src/ui/runInk.js +57 -0
- 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 -573
- package/src/chat/index.js +0 -2214
- 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/{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}/daemonTransportDefaults.js +0 -0
- /package/src/{chat → app/chat}/inputMath.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}/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}/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/{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/{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
|
@@ -0,0 +1,3674 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Ink-based chat TUI rendered via React + ink.
|
|
5
|
+
*
|
|
6
|
+
* Activation: this is the only chat TUI.
|
|
7
|
+
*
|
|
8
|
+
* Coverage today: layout shell + dashboard bar (5 modes: projects, agents,
|
|
9
|
+
* mode, provider, cron) + multiline editor + status line +
|
|
10
|
+
* Tab/Esc focus + agent selection + Up/Down history, daemon routing,
|
|
11
|
+
* command execution, completion and internal-agent views.
|
|
12
|
+
*
|
|
13
|
+
* Chat state is kept in chatReducer.js so the entire transition table can
|
|
14
|
+
* be exercised by jest without mounting ink.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const path = require("path");
|
|
18
|
+
const fs = require("fs");
|
|
19
|
+
const crypto = require("crypto");
|
|
20
|
+
|
|
21
|
+
const { runInk } = require("../runInk");
|
|
22
|
+
const fmt = require("../format");
|
|
23
|
+
const { createMultilineInput } = require("./MultilineInput");
|
|
24
|
+
const { createDashboardBar } = require("./DashboardBar");
|
|
25
|
+
const { reducer, createInitialState } = require("./chatReducer");
|
|
26
|
+
|
|
27
|
+
function bootstrapEnvironment(projectRoot, options = {}) {
|
|
28
|
+
// Ensure ufoo dirs exist and that we have a stable subscriber ID.
|
|
29
|
+
// We deliberately keep the
|
|
30
|
+
// non-UI side-effects in their own helper so unit tests can assert on
|
|
31
|
+
// them without importing ink.
|
|
32
|
+
const { canonicalProjectRoot } = require("../../runtime/projects");
|
|
33
|
+
const { getUfooPaths } = require("../../coordination/state/paths");
|
|
34
|
+
const UfooInit = require("../../app/cli/features/init");
|
|
35
|
+
const { isRunning } = require("../../runtime/daemon");
|
|
36
|
+
const { startDaemon } = require("../../app/chat/transport");
|
|
37
|
+
|
|
38
|
+
const globalMode = options && options.globalMode === true;
|
|
39
|
+
let activeProjectRoot = projectRoot;
|
|
40
|
+
try {
|
|
41
|
+
activeProjectRoot = canonicalProjectRoot(projectRoot);
|
|
42
|
+
} catch {
|
|
43
|
+
activeProjectRoot = path.resolve(projectRoot || process.cwd());
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const runtimePaths = getUfooPaths(projectRoot);
|
|
47
|
+
const contextIndexFile = path.join(runtimePaths.ufooDir, "context", "decisions.jsonl");
|
|
48
|
+
const needsBootstrap = globalMode && (
|
|
49
|
+
!fs.existsSync(runtimePaths.ufooDir)
|
|
50
|
+
|| !fs.existsSync(runtimePaths.busDir)
|
|
51
|
+
|| !fs.existsSync(runtimePaths.agentDir)
|
|
52
|
+
|| !fs.existsSync(contextIndexFile)
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
activeProjectRoot,
|
|
57
|
+
globalMode,
|
|
58
|
+
runtimePaths,
|
|
59
|
+
needsBootstrap,
|
|
60
|
+
UfooInit,
|
|
61
|
+
isRunning,
|
|
62
|
+
startDaemon,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function ensureSubscriberId(projectRoot) {
|
|
67
|
+
if (process.env.UFOO_SUBSCRIBER_ID) return;
|
|
68
|
+
const { getUfooPaths } = require("../../coordination/state/paths");
|
|
69
|
+
const sessionFile = path.join(getUfooPaths(projectRoot).ufooDir, "chat", "session-id.txt");
|
|
70
|
+
const sessionDir = path.dirname(sessionFile);
|
|
71
|
+
fs.mkdirSync(sessionDir, { recursive: true });
|
|
72
|
+
let sessionId;
|
|
73
|
+
if (fs.existsSync(sessionFile)) {
|
|
74
|
+
sessionId = fs.readFileSync(sessionFile, "utf8").trim();
|
|
75
|
+
} else {
|
|
76
|
+
sessionId = crypto.randomBytes(4).toString("hex");
|
|
77
|
+
fs.writeFileSync(sessionFile, sessionId, "utf8");
|
|
78
|
+
}
|
|
79
|
+
process.env.UFOO_SUBSCRIBER_ID = `claude-code:${sessionId}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function inputHistoryFilePath(projectRoot, options = {}) {
|
|
83
|
+
const { getUfooPaths } = require("../../coordination/state/paths");
|
|
84
|
+
const { globalMode } = options || {};
|
|
85
|
+
if (globalMode) {
|
|
86
|
+
const os = require("os");
|
|
87
|
+
const globalChatRoot = path.join(os.homedir(), ".ufoo", "chat");
|
|
88
|
+
const globalDir = path.join(globalChatRoot, "global-input-history");
|
|
89
|
+
const projectId = projectRootToId(projectRoot);
|
|
90
|
+
return path.join(globalDir, `${projectId}.jsonl`);
|
|
91
|
+
}
|
|
92
|
+
return path.join(getUfooPaths(projectRoot || process.cwd()).ufooDir, "chat", "input-history.jsonl");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function chatHistoryFilePath(projectRoot, options = {}) {
|
|
96
|
+
const { getUfooPaths } = require("../../coordination/state/paths");
|
|
97
|
+
const { globalMode } = options || {};
|
|
98
|
+
if (globalMode) {
|
|
99
|
+
const os = require("os");
|
|
100
|
+
const globalChatRoot = path.join(os.homedir(), ".ufoo", "chat");
|
|
101
|
+
const globalDir = path.join(globalChatRoot, "global-history");
|
|
102
|
+
const projectId = projectRootToId(projectRoot);
|
|
103
|
+
return path.join(globalDir, `${projectId}.jsonl`);
|
|
104
|
+
}
|
|
105
|
+
return path.join(getUfooPaths(projectRoot || process.cwd()).ufooDir, "chat", "history.jsonl");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function projectRootToId(projectRoot) {
|
|
109
|
+
try {
|
|
110
|
+
const { buildProjectId } = require("../../runtime/projects");
|
|
111
|
+
return buildProjectId(projectRoot || process.cwd());
|
|
112
|
+
} catch {
|
|
113
|
+
return crypto.createHash("sha256").update(String(projectRoot || "")).digest("hex").slice(0, 16);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function resolveInjectSockPathForAgent(projectRoot, agentId) {
|
|
118
|
+
const { getUfooPaths } = require("../../coordination/state/paths");
|
|
119
|
+
const { subscriberToSafeName } = require("../../coordination/bus/utils");
|
|
120
|
+
const safeName = subscriberToSafeName(agentId);
|
|
121
|
+
return path.join(getUfooPaths(projectRoot || process.cwd()).busQueuesDir, safeName, "inject.sock");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function createInkMultiWindowToggle({
|
|
125
|
+
getController = () => null,
|
|
126
|
+
setActive = () => {},
|
|
127
|
+
logMessage = () => {},
|
|
128
|
+
} = {}) {
|
|
129
|
+
return () => {
|
|
130
|
+
const controller = typeof getController === "function" ? getController() : null;
|
|
131
|
+
if (!controller || typeof controller.enter !== "function" || typeof controller.exit !== "function") {
|
|
132
|
+
logMessage("error", "✗ Multi-window mode is not available");
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (typeof controller.isActive === "function" && controller.isActive()) {
|
|
137
|
+
controller.exit();
|
|
138
|
+
setActive(false);
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
setActive(true);
|
|
143
|
+
if (!controller.enter()) {
|
|
144
|
+
setActive(false);
|
|
145
|
+
logMessage("info", "No active agents for multi-window mode");
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
return true;
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function loadChatHistory(projectRoot, cap = 200, options = {}) {
|
|
153
|
+
const file = chatHistoryFilePath(projectRoot, options);
|
|
154
|
+
try {
|
|
155
|
+
if (!fs.existsSync(file)) return [];
|
|
156
|
+
const raw = fs.readFileSync(file, "utf8");
|
|
157
|
+
const lines = raw.split(/\r?\n/).filter(Boolean);
|
|
158
|
+
const out = [];
|
|
159
|
+
for (const line of lines) {
|
|
160
|
+
try {
|
|
161
|
+
const entry = JSON.parse(line);
|
|
162
|
+
if (!entry) continue;
|
|
163
|
+
if (entry.type === "spacer") {
|
|
164
|
+
out.push("");
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
const text = String(entry.text || "");
|
|
168
|
+
if (!text) continue;
|
|
169
|
+
// Strip blessed-tag markup that the legacy log writer used; ink
|
|
170
|
+
// can't render those tags and we don't want them shown literally.
|
|
171
|
+
const stripped = text.replace(/\{[^{}]+\}/g, "");
|
|
172
|
+
out.push(stripped);
|
|
173
|
+
} catch {
|
|
174
|
+
// ignore malformed lines
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return out.slice(-cap);
|
|
178
|
+
} catch {
|
|
179
|
+
return [];
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function loadInputHistory(projectRoot, cap = 200, options = {}) {
|
|
184
|
+
const file = inputHistoryFilePath(projectRoot, options);
|
|
185
|
+
try {
|
|
186
|
+
if (!fs.existsSync(file)) return [];
|
|
187
|
+
const raw = fs.readFileSync(file, "utf8");
|
|
188
|
+
const lines = raw.split(/\r?\n/).filter(Boolean);
|
|
189
|
+
const out = [];
|
|
190
|
+
for (const line of lines) {
|
|
191
|
+
try {
|
|
192
|
+
const obj = JSON.parse(line);
|
|
193
|
+
const value = String((obj && obj.value) || "").trim();
|
|
194
|
+
if (value) out.push(value);
|
|
195
|
+
} catch {
|
|
196
|
+
// ignore malformed entries
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return out.slice(-cap);
|
|
200
|
+
} catch {
|
|
201
|
+
return [];
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function appendInputHistory(projectRoot, value, options = {}) {
|
|
206
|
+
const trimmed = String(value || "").trim();
|
|
207
|
+
if (!trimmed) return;
|
|
208
|
+
const file = inputHistoryFilePath(projectRoot, options);
|
|
209
|
+
try {
|
|
210
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
211
|
+
fs.appendFileSync(file, `${JSON.stringify({ value: trimmed, ts: Date.now() })}\n`);
|
|
212
|
+
} catch {
|
|
213
|
+
// best-effort persistence; failure is not user-visible
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function appendChatHistory(projectRoot, type, text, meta = {}, options = {}) {
|
|
218
|
+
const value = String(text || "");
|
|
219
|
+
if (!value && type !== "spacer") return;
|
|
220
|
+
const file = chatHistoryFilePath(projectRoot, options);
|
|
221
|
+
try {
|
|
222
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
223
|
+
fs.appendFileSync(file, `${JSON.stringify({
|
|
224
|
+
ts: new Date().toISOString(),
|
|
225
|
+
type,
|
|
226
|
+
text: value,
|
|
227
|
+
meta: meta && typeof meta === "object" ? meta : {},
|
|
228
|
+
})}\n`);
|
|
229
|
+
} catch {
|
|
230
|
+
// best-effort persistence; failure is not user-visible
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function chatHistoryOptionsForScope({ globalMode = false, globalScope = "controller" } = {}) {
|
|
235
|
+
return {
|
|
236
|
+
globalMode: Boolean(globalMode && globalScope !== "project"),
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function getAgentLabelFor(meta, agentId) {
|
|
241
|
+
// Prefer the project-stripped display nickname so the dashboard never shows
|
|
242
|
+
// the scoped form ("neptune-builder"); fall back to the raw nickname (which
|
|
243
|
+
// may itself be unscoped depending on write path) and finally to a short
|
|
244
|
+
// form of the subscriber id.
|
|
245
|
+
if (meta && meta.display_nickname) return meta.display_nickname;
|
|
246
|
+
if (meta && meta.nickname) return meta.nickname;
|
|
247
|
+
if (!agentId) return "";
|
|
248
|
+
const colon = agentId.indexOf(":");
|
|
249
|
+
if (colon < 0) return agentId;
|
|
250
|
+
const head = agentId.slice(0, colon);
|
|
251
|
+
const tail = agentId.slice(colon + 1).slice(0, 6);
|
|
252
|
+
return tail ? `${head}:${tail}` : head;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function buildActiveAgentLabelMap(activeAgents = [], activeAgentMeta = new Map()) {
|
|
256
|
+
const out = new Map();
|
|
257
|
+
const metaMap = activeAgentMeta instanceof Map ? activeAgentMeta : new Map();
|
|
258
|
+
for (const id of Array.isArray(activeAgents) ? activeAgents : []) {
|
|
259
|
+
out.set(id, getAgentLabelFor(metaMap.get(id), id));
|
|
260
|
+
}
|
|
261
|
+
return out;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function resolveActiveAgentId(label, activeAgents = [], activeAgentMeta = new Map()) {
|
|
265
|
+
const { resolveAgentId } = require("../../app/chat/agentDirectory");
|
|
266
|
+
const metaMap = activeAgentMeta instanceof Map ? activeAgentMeta : new Map();
|
|
267
|
+
return resolveAgentId({
|
|
268
|
+
label,
|
|
269
|
+
activeAgents: Array.isArray(activeAgents) ? activeAgents : [],
|
|
270
|
+
labelMap: buildActiveAgentLabelMap(activeAgents, metaMap),
|
|
271
|
+
lookupNickname: (nickname) => {
|
|
272
|
+
for (const [id, meta] of metaMap.entries()) {
|
|
273
|
+
if (!meta) continue;
|
|
274
|
+
if (meta.nickname === nickname || meta.scoped_nickname === nickname || meta.display_nickname === nickname) {
|
|
275
|
+
return id;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return null;
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function buildDirectBusSendRequest({
|
|
284
|
+
text,
|
|
285
|
+
targetAgentId = null,
|
|
286
|
+
activeAgents = [],
|
|
287
|
+
activeAgentMeta = new Map(),
|
|
288
|
+
} = {}) {
|
|
289
|
+
const trimmed = String(text || "").trim();
|
|
290
|
+
if (!trimmed) return null;
|
|
291
|
+
if (targetAgentId) {
|
|
292
|
+
return {
|
|
293
|
+
target: targetAgentId,
|
|
294
|
+
message: trimmed,
|
|
295
|
+
source: "chat-direct",
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const { parseAtTarget } = require("../../app/chat/commands");
|
|
300
|
+
const atTarget = parseAtTarget(trimmed);
|
|
301
|
+
if (!atTarget || !atTarget.message) return null;
|
|
302
|
+
const target = resolveActiveAgentId(atTarget.target, activeAgents, activeAgentMeta) || atTarget.target;
|
|
303
|
+
return {
|
|
304
|
+
target,
|
|
305
|
+
message: atTarget.message.trim(),
|
|
306
|
+
source: "chat-direct",
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function resolveAgentEnterRequest({
|
|
311
|
+
agentId,
|
|
312
|
+
projectRoot = "",
|
|
313
|
+
activeAgentMeta = new Map(),
|
|
314
|
+
settings = {},
|
|
315
|
+
} = {}) {
|
|
316
|
+
const id = String(agentId || "").trim();
|
|
317
|
+
if (!id) return null;
|
|
318
|
+
|
|
319
|
+
const metaMap = activeAgentMeta instanceof Map ? activeAgentMeta : new Map();
|
|
320
|
+
const meta = metaMap.get(id) || {};
|
|
321
|
+
const configuredLaunchMode = settings && settings.launchMode && settings.launchMode !== "auto"
|
|
322
|
+
? settings.launchMode
|
|
323
|
+
: "";
|
|
324
|
+
const launchMode = String(meta.launch_mode || meta.launchMode || configuredLaunchMode || "").trim();
|
|
325
|
+
const { createTerminalAdapterRouter } = require("../../runtime/terminal/adapterRouter");
|
|
326
|
+
const adapter = createTerminalAdapterRouter().getAdapter({ launchMode, agentId: id, meta });
|
|
327
|
+
const caps = adapter && adapter.capabilities ? adapter.capabilities : {};
|
|
328
|
+
|
|
329
|
+
return {
|
|
330
|
+
agentId: id,
|
|
331
|
+
projectRoot: String(projectRoot || ""),
|
|
332
|
+
launchMode,
|
|
333
|
+
useBus: Boolean(caps.supportsInternalQueueLoop && !caps.supportsSocketProtocol),
|
|
334
|
+
supportsSocket: Boolean(caps.supportsSocketProtocol),
|
|
335
|
+
supportsInternalQueue: Boolean(caps.supportsInternalQueueLoop),
|
|
336
|
+
supportsActivate: Boolean(caps.supportsActivate),
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function resolveDashboardAgentEnterAction(enterRequest = {}) {
|
|
341
|
+
if (!enterRequest || typeof enterRequest !== "object") return "none";
|
|
342
|
+
if (enterRequest.useBus) return "internal";
|
|
343
|
+
if (enterRequest.supportsActivate) return "activate";
|
|
344
|
+
return "agent-view";
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function buildEmptyProjectsDownActions(state = {}, displayAgents = []) {
|
|
348
|
+
if (!state.emptyProjectsDownArmed) {
|
|
349
|
+
return [{ type: "projects/armEmptyDown" }];
|
|
350
|
+
}
|
|
351
|
+
const actions = [{ type: "view/set", view: "agents" }];
|
|
352
|
+
if (displayAgents.length > 0 && state.selectedAgentIndex < 0) {
|
|
353
|
+
actions.push({ type: "agents/select", index: 0 });
|
|
354
|
+
}
|
|
355
|
+
return actions;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function buildPromptIpcRequest(text) {
|
|
359
|
+
const { IPC_REQUEST_TYPES } = require("../../runtime/contracts/eventContract");
|
|
360
|
+
return {
|
|
361
|
+
type: IPC_REQUEST_TYPES.PROMPT,
|
|
362
|
+
text,
|
|
363
|
+
request_meta: {
|
|
364
|
+
source: "chat-dialog",
|
|
365
|
+
dispatch_default_injection_mode: "immediate",
|
|
366
|
+
allow_relevance_queue: true,
|
|
367
|
+
},
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function stripBlessedTags(text = "") {
|
|
372
|
+
return String(text || "")
|
|
373
|
+
.replace(/\{\/?[^{}\n]+\}/g, "")
|
|
374
|
+
.replace(/\r/g, "");
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function normalizeInkLogLines(text = "") {
|
|
378
|
+
const clean = stripBlessedTags(text);
|
|
379
|
+
return clean.split(/\r?\n/);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function stripMarkdownDecorators(text = "") {
|
|
383
|
+
return String(text || "")
|
|
384
|
+
.replace(/\*\*([^*]+)\*\*/g, "$1")
|
|
385
|
+
.replace(/`([^`]+)`/g, "$1");
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function classifyChatLogLine(text = "") {
|
|
389
|
+
const raw = stripBlessedTags(text).replace(/\r/g, "");
|
|
390
|
+
const clean = stripMarkdownDecorators(raw);
|
|
391
|
+
const trimmed = clean.trim();
|
|
392
|
+
if (!trimmed) return { kind: "spacer", marker: " ", speaker: "", body: " " };
|
|
393
|
+
if (/^[█▀▄ ]+$/.test(trimmed) || /^ufoo chat/i.test(trimmed)) {
|
|
394
|
+
return { kind: "banner", marker: " ", speaker: "", body: clean };
|
|
395
|
+
}
|
|
396
|
+
if (/^───.*───$/.test(trimmed)) {
|
|
397
|
+
return { kind: "divider", marker: "─", speaker: "", body: clean };
|
|
398
|
+
}
|
|
399
|
+
if (/^(error:|✗|failed\b)/i.test(trimmed)) {
|
|
400
|
+
return { kind: "error", marker: "!", speaker: "error", body: clean.replace(/^(error:\s*)/i, "") };
|
|
401
|
+
}
|
|
402
|
+
if (/^(✓|✔|done\b|closed\b)/i.test(trimmed)) {
|
|
403
|
+
return { kind: "success", marker: "✓", speaker: "", body: clean.replace(/^[✓✔]\s*/, "") };
|
|
404
|
+
}
|
|
405
|
+
const dotMatch = clean.match(/^([^·:\n]{1,42})\s+·\s+(.*)$/);
|
|
406
|
+
if (dotMatch) {
|
|
407
|
+
const speaker = dotMatch[1].trim();
|
|
408
|
+
const lower = speaker.toLowerCase();
|
|
409
|
+
const kind = lower === "ufoo" ? "assistant" : "agent";
|
|
410
|
+
return { kind, marker: kind === "assistant" ? "◆" : "●", speaker, body: dotMatch[2] || " " };
|
|
411
|
+
}
|
|
412
|
+
const colonMatch = clean.match(/^([A-Za-z0-9_.:@/-]{1,42}):\s+(.*)$/);
|
|
413
|
+
if (colonMatch) {
|
|
414
|
+
return { kind: "agent", marker: "●", speaker: colonMatch[1], body: colonMatch[2] || " " };
|
|
415
|
+
}
|
|
416
|
+
if (/^(CHAT|UCODE)\s+·/i.test(trimmed)) {
|
|
417
|
+
return { kind: "meta", marker: "·", speaker: "", body: clean };
|
|
418
|
+
}
|
|
419
|
+
return { kind: "plain", marker: "│", speaker: "", body: clean };
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function createInkStreamState({
|
|
423
|
+
dispatch,
|
|
424
|
+
appendHistory,
|
|
425
|
+
displayNameForPublisher = (value) => value,
|
|
426
|
+
} = {}) {
|
|
427
|
+
const streams = new Map();
|
|
428
|
+
const pendingDeliveries = new Map();
|
|
429
|
+
|
|
430
|
+
function deliveryKey(agentId, agentLabel) {
|
|
431
|
+
return String(agentId || agentLabel || "").trim();
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function markPendingDelivery(agentId, agentLabel) {
|
|
435
|
+
const key = deliveryKey(agentId, agentLabel);
|
|
436
|
+
if (!key) return;
|
|
437
|
+
const existing = pendingDeliveries.get(key) || { count: 0, keys: new Set() };
|
|
438
|
+
existing.count += 1;
|
|
439
|
+
for (const candidate of [agentId, agentLabel]) {
|
|
440
|
+
const value = String(candidate || "").trim();
|
|
441
|
+
if (value) {
|
|
442
|
+
pendingDeliveries.set(value, existing);
|
|
443
|
+
existing.keys.add(value);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function getPendingState(publisher, displayName) {
|
|
449
|
+
for (const candidate of [publisher, displayName]) {
|
|
450
|
+
const key = String(candidate || "").trim();
|
|
451
|
+
if (key && pendingDeliveries.has(key)) {
|
|
452
|
+
return { key, state: pendingDeliveries.get(key) };
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
return null;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function consumePendingDelivery(publisher, displayName) {
|
|
459
|
+
const hit = getPendingState(publisher, displayName);
|
|
460
|
+
if (!hit) return false;
|
|
461
|
+
hit.state.count -= 1;
|
|
462
|
+
if (hit.state.count <= 0) {
|
|
463
|
+
for (const key of hit.state.keys || []) pendingDeliveries.delete(key);
|
|
464
|
+
}
|
|
465
|
+
return true;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function beginStream(publisher, prefix, continuationPrefix, meta) {
|
|
469
|
+
const key = String(publisher || "bus");
|
|
470
|
+
let state = streams.get(key);
|
|
471
|
+
if (state) return state;
|
|
472
|
+
const displayName = stripBlessedTags(prefix || displayNameForPublisher(key) || key)
|
|
473
|
+
.replace(/\s*·\s*$/, "")
|
|
474
|
+
.trim() || displayNameForPublisher(key) || key;
|
|
475
|
+
state = {
|
|
476
|
+
publisher: key,
|
|
477
|
+
displayName,
|
|
478
|
+
prefix,
|
|
479
|
+
continuationPrefix,
|
|
480
|
+
full: "",
|
|
481
|
+
meta: meta || {},
|
|
482
|
+
};
|
|
483
|
+
streams.set(key, state);
|
|
484
|
+
dispatch({ type: "stream/begin", publisher: displayName });
|
|
485
|
+
return state;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function appendStreamDelta(state, delta) {
|
|
489
|
+
if (!state || !delta) return;
|
|
490
|
+
state.full += String(delta || "");
|
|
491
|
+
dispatch({ type: "stream/delta", publisher: state.displayName || state.publisher, delta: String(delta || "") });
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function finalizeStream(publisher, meta, reason = "") {
|
|
495
|
+
const key = String(publisher || "bus");
|
|
496
|
+
const state = streams.get(key);
|
|
497
|
+
if (!state) return;
|
|
498
|
+
dispatch({ type: "stream/end" });
|
|
499
|
+
if (typeof appendHistory === "function") {
|
|
500
|
+
const text = state.displayName
|
|
501
|
+
? `${state.displayName}: ${state.full}`
|
|
502
|
+
: state.full;
|
|
503
|
+
appendHistory("bus", text, { ...(meta || state.meta || {}), stream_done: true, stream_reason: reason });
|
|
504
|
+
}
|
|
505
|
+
streams.delete(key);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function hasStream(publisher) {
|
|
509
|
+
return streams.has(String(publisher || "bus"));
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
return {
|
|
513
|
+
markPendingDelivery,
|
|
514
|
+
getPendingState,
|
|
515
|
+
consumePendingDelivery,
|
|
516
|
+
beginStream,
|
|
517
|
+
appendStreamDelta,
|
|
518
|
+
finalizeStream,
|
|
519
|
+
hasStream,
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function formatShellCommandResultLines(result = {}) {
|
|
524
|
+
const lines = [];
|
|
525
|
+
const stdout = String(result.stdout || "").trimEnd();
|
|
526
|
+
const stderr = String(result.stderr || "").trimEnd();
|
|
527
|
+
if (stdout) lines.push(...stdout.split(/\r?\n/).map((line) => ({ type: "system", text: line })));
|
|
528
|
+
if (stderr) lines.push(...stderr.split(/\r?\n/).map((line) => ({ type: result.ok ? "system" : "error", text: line })));
|
|
529
|
+
if (!stdout && !stderr) lines.push({ type: "system", text: "(no output)" });
|
|
530
|
+
if (!result.ok) {
|
|
531
|
+
const suffix = result.signal ? ` signal ${result.signal}` : ` exit ${result.code != null ? result.code : 1}`;
|
|
532
|
+
lines.push({ type: "error", text: `Command failed:${suffix}` });
|
|
533
|
+
}
|
|
534
|
+
return lines;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function fitPlainLine(text = "", width = 80) {
|
|
538
|
+
const limit = Math.max(1, Math.floor(Number(width) || 80));
|
|
539
|
+
const raw = String(text || "").replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "");
|
|
540
|
+
let out = "";
|
|
541
|
+
let cells = 0;
|
|
542
|
+
for (const char of Array.from(raw)) {
|
|
543
|
+
const charWidth = fmt.charDisplayWidth(char);
|
|
544
|
+
if (cells + charWidth > limit) break;
|
|
545
|
+
out += char;
|
|
546
|
+
cells += charWidth;
|
|
547
|
+
}
|
|
548
|
+
if (out.length < raw.length && limit > 1) {
|
|
549
|
+
while (fmt.displayCellWidth(out) > limit - 1) {
|
|
550
|
+
out = Array.from(out).slice(0, -1).join("");
|
|
551
|
+
}
|
|
552
|
+
out = `${out}…`;
|
|
553
|
+
}
|
|
554
|
+
return out || " ";
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function stripInternalLogMarkup(text = "") {
|
|
558
|
+
return String(text || "")
|
|
559
|
+
.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "")
|
|
560
|
+
.replace(/\{\/?[^{}\n]+\}/g, "");
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function wrapInternalPlainLine(text = "", width = 80) {
|
|
564
|
+
const limit = Math.max(1, Math.floor(Number(width) || 80));
|
|
565
|
+
const clean = stripInternalLogMarkup(text).replace(/\r/g, "");
|
|
566
|
+
if (!clean) return [""];
|
|
567
|
+
const rows = [];
|
|
568
|
+
let row = "";
|
|
569
|
+
let cells = 0;
|
|
570
|
+
for (const char of Array.from(clean)) {
|
|
571
|
+
const charWidth = fmt.charDisplayWidth(char);
|
|
572
|
+
if (cells > 0 && cells + charWidth > limit) {
|
|
573
|
+
rows.push(row);
|
|
574
|
+
row = "";
|
|
575
|
+
cells = 0;
|
|
576
|
+
}
|
|
577
|
+
row += char;
|
|
578
|
+
cells += charWidth;
|
|
579
|
+
}
|
|
580
|
+
rows.push(row);
|
|
581
|
+
return rows;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function classifyInternalLogLine(line = "") {
|
|
585
|
+
const raw = stripInternalLogMarkup(line).replace(/\r/g, "");
|
|
586
|
+
if (!raw) return { kind: "spacer", text: "", markdown: false, bold: false };
|
|
587
|
+
if (raw.startsWith("> ")) return { kind: "user", text: raw.slice(2), markdown: false, bold: false };
|
|
588
|
+
if (raw.startsWith("* ")) return { kind: "agent", text: raw.slice(2), markdown: true, bold: false };
|
|
589
|
+
if (/^error:/i.test(raw) || /^\[error\]/i.test(raw)) {
|
|
590
|
+
return { kind: "error", text: raw, markdown: true, bold: false };
|
|
591
|
+
}
|
|
592
|
+
if (/^ufoo internal agent\b/i.test(raw)) {
|
|
593
|
+
return { kind: "system", text: raw, markdown: false, bold: true };
|
|
594
|
+
}
|
|
595
|
+
if (/^(agent|directory):/i.test(raw)) {
|
|
596
|
+
return { kind: "meta", text: raw, markdown: false, bold: false };
|
|
597
|
+
}
|
|
598
|
+
return { kind: "agent", text: raw, markdown: true, bold: false };
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function internalLogPrefixes(kind) {
|
|
602
|
+
if (kind === "user") return { first: "› ", rest: " " };
|
|
603
|
+
if (kind === "system") return { first: "· ", rest: " " };
|
|
604
|
+
if (kind === "meta") return { first: " ", rest: " " };
|
|
605
|
+
return { first: "", rest: "" };
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function buildInternalLogRows(lines = [], width = 80, maxRows = 20) {
|
|
609
|
+
const limit = Math.max(1, Math.floor(Number(width) || 80));
|
|
610
|
+
const rows = [];
|
|
611
|
+
const markdownState = {};
|
|
612
|
+
const source = Array.isArray(lines) ? lines : [];
|
|
613
|
+
for (const line of source) {
|
|
614
|
+
const classified = classifyInternalLogLine(line);
|
|
615
|
+
if (classified.kind === "spacer") {
|
|
616
|
+
rows.push({ kind: "spacer", text: " ", bold: false });
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
let rendered = [classified.text];
|
|
621
|
+
if (classified.markdown) {
|
|
622
|
+
try {
|
|
623
|
+
rendered = fmt.renderLogLinesWithMarkdown(classified.text, markdownState, (value) => String(value || ""))
|
|
624
|
+
.map(stripInternalLogMarkup);
|
|
625
|
+
} catch {
|
|
626
|
+
rendered = [classified.text];
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
const prefixes = internalLogPrefixes(classified.kind);
|
|
631
|
+
for (const renderedLine of rendered) {
|
|
632
|
+
const chunks = wrapInternalPlainLine(
|
|
633
|
+
renderedLine,
|
|
634
|
+
Math.max(1, limit - fmt.displayCellWidth(prefixes.first)),
|
|
635
|
+
);
|
|
636
|
+
chunks.forEach((chunk, idx) => {
|
|
637
|
+
const prefix = idx === 0 ? prefixes.first : prefixes.rest;
|
|
638
|
+
rows.push({
|
|
639
|
+
kind: classified.kind,
|
|
640
|
+
text: fitPlainLine(`${prefix}${chunk}`, limit),
|
|
641
|
+
bold: classified.bold,
|
|
642
|
+
});
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
return rows.slice(-Math.max(1, Math.floor(Number(maxRows) || 20)));
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function internalInputBoundaries(text = "") {
|
|
650
|
+
const source = String(text || "");
|
|
651
|
+
if (!source) return [0];
|
|
652
|
+
try {
|
|
653
|
+
if (typeof Intl !== "undefined" && typeof Intl.Segmenter === "function") {
|
|
654
|
+
const segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
655
|
+
const boundaries = [0];
|
|
656
|
+
for (const part of segmenter.segment(source)) {
|
|
657
|
+
boundaries.push(part.index + part.segment.length);
|
|
658
|
+
}
|
|
659
|
+
return Array.from(new Set(boundaries)).sort((a, b) => a - b);
|
|
660
|
+
}
|
|
661
|
+
} catch {
|
|
662
|
+
// Fall through.
|
|
663
|
+
}
|
|
664
|
+
const boundaries = [0];
|
|
665
|
+
let offset = 0;
|
|
666
|
+
for (const char of Array.from(source)) {
|
|
667
|
+
offset += char.length;
|
|
668
|
+
boundaries.push(offset);
|
|
669
|
+
}
|
|
670
|
+
return boundaries;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function previousInternalBoundary(text = "", cursor = 0) {
|
|
674
|
+
const target = Math.max(0, Math.min(String(text || "").length, cursor));
|
|
675
|
+
let previous = 0;
|
|
676
|
+
for (const boundary of internalInputBoundaries(text)) {
|
|
677
|
+
if (boundary < target) previous = boundary;
|
|
678
|
+
else break;
|
|
679
|
+
}
|
|
680
|
+
return previous;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function nextInternalBoundary(text = "", cursor = 0) {
|
|
684
|
+
const source = String(text || "");
|
|
685
|
+
const target = Math.max(0, Math.min(source.length, cursor));
|
|
686
|
+
for (const boundary of internalInputBoundaries(source)) {
|
|
687
|
+
if (boundary > target) return boundary;
|
|
688
|
+
}
|
|
689
|
+
return source.length;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function resolveInternalKeyName(input = "", key = {}) {
|
|
693
|
+
const raw = String(input || "");
|
|
694
|
+
if (raw === "\x7f" || raw === "\b" || raw === "\x08") return "backspace";
|
|
695
|
+
if (raw === "\x1b[3~" || raw === "\u001b[3~") return "delete";
|
|
696
|
+
if (key && key.backspace) return "backspace";
|
|
697
|
+
if (key && key.delete) return "backspace";
|
|
698
|
+
if (key && key.name === "backspace") return "backspace";
|
|
699
|
+
if (key && key.name === "delete") return "backspace";
|
|
700
|
+
if (key && key.name) return String(key.name);
|
|
701
|
+
if (key && key.escape) return "escape";
|
|
702
|
+
if (key && key.return) return "return";
|
|
703
|
+
if (key && key.leftArrow) return "left";
|
|
704
|
+
if (key && key.rightArrow) return "right";
|
|
705
|
+
if (key && key.upArrow) return "up";
|
|
706
|
+
if (key && key.downArrow) return "down";
|
|
707
|
+
if (key && key.ctrl && raw.length === 1) return raw.toLowerCase();
|
|
708
|
+
return "";
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function isInternalViewingAgent(agentId, meta, view = {}, viewingAgentId = "") {
|
|
712
|
+
const id = String(agentId || "").trim();
|
|
713
|
+
if (!id) return false;
|
|
714
|
+
const candidates = new Set([
|
|
715
|
+
viewingAgentId,
|
|
716
|
+
view && view.agentId,
|
|
717
|
+
view && view.label,
|
|
718
|
+
...((view && Array.isArray(view.aliases)) ? view.aliases : []),
|
|
719
|
+
].filter(Boolean).map((value) => String(value).trim()).filter(Boolean));
|
|
720
|
+
if (candidates.has(id)) return true;
|
|
721
|
+
const metaIds = [
|
|
722
|
+
meta && meta.fullId,
|
|
723
|
+
meta && meta.agent_id,
|
|
724
|
+
meta && meta.subscriber_id,
|
|
725
|
+
meta && meta.nickname,
|
|
726
|
+
meta && meta.scoped_nickname,
|
|
727
|
+
meta && meta.display_nickname,
|
|
728
|
+
meta && meta.type && meta.id ? `${meta.type}:${meta.id}` : "",
|
|
729
|
+
getAgentLabelFor(meta, id),
|
|
730
|
+
].filter(Boolean).map((value) => String(value).trim()).filter(Boolean);
|
|
731
|
+
return metaIds.some((value) => candidates.has(value));
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function compactDisplayProjectRoot(projectRoot = "") {
|
|
735
|
+
const os = require("os");
|
|
736
|
+
const raw = String(projectRoot || process.cwd() || "").trim();
|
|
737
|
+
const home = os.homedir();
|
|
738
|
+
if (home && (raw === home || raw.startsWith(`${home}/`))) return `~${raw.slice(home.length)}`;
|
|
739
|
+
return raw || ".";
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function buildInternalAgentStartupLines({ agentId = "", label = "", projectRoot = "", width = 80 } = {}) {
|
|
743
|
+
return [
|
|
744
|
+
fitPlainLine(`ufoo internal agent · ${label || agentId}`, width),
|
|
745
|
+
fitPlainLine(`agent: ${agentId}`, width),
|
|
746
|
+
fitPlainLine(`directory: ${compactDisplayProjectRoot(projectRoot)}`, width),
|
|
747
|
+
"",
|
|
748
|
+
];
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function createInternalAgentViewState({
|
|
752
|
+
agentId,
|
|
753
|
+
label,
|
|
754
|
+
aliases = [],
|
|
755
|
+
projectRoot,
|
|
756
|
+
width = 80,
|
|
757
|
+
} = {}) {
|
|
758
|
+
let history = [];
|
|
759
|
+
try {
|
|
760
|
+
const { loadInternalAgentLogHistory } = require("../../app/chat/internalAgentLogHistory");
|
|
761
|
+
history = loadInternalAgentLogHistory(projectRoot || process.cwd(), agentId, {
|
|
762
|
+
maxEvents: 400,
|
|
763
|
+
maxLines: 1000,
|
|
764
|
+
});
|
|
765
|
+
} catch {
|
|
766
|
+
history = [];
|
|
767
|
+
}
|
|
768
|
+
const safeAliases = [agentId, label].concat(aliases || []).filter(Boolean).map(String);
|
|
769
|
+
return {
|
|
770
|
+
agentId: String(agentId || ""),
|
|
771
|
+
label: String(label || agentId || ""),
|
|
772
|
+
aliases: Array.from(new Set(safeAliases)),
|
|
773
|
+
projectRoot: String(projectRoot || ""),
|
|
774
|
+
lines: buildInternalAgentStartupLines({ agentId, label, projectRoot, width })
|
|
775
|
+
.concat(history.length > 0 ? history : [""]),
|
|
776
|
+
input: "",
|
|
777
|
+
cursor: 0,
|
|
778
|
+
status: "ready",
|
|
779
|
+
detail: "",
|
|
780
|
+
statusStartedAt: 0,
|
|
781
|
+
barIndex: 0,
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function appendInternalAgentText(view, text = "", options = {}) {
|
|
786
|
+
const current = view && typeof view === "object" ? view : {};
|
|
787
|
+
const lines = Array.isArray(current.lines) ? current.lines.slice() : [];
|
|
788
|
+
if (lines.length === 0) lines.push("");
|
|
789
|
+
const prefix = options.prefix || "";
|
|
790
|
+
const clean = String(text || "").replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "")
|
|
791
|
+
.replace(/\r\n/g, "\n")
|
|
792
|
+
.replace(/\r/g, "\n");
|
|
793
|
+
if (prefix && lines[lines.length - 1] !== "") lines.push("");
|
|
794
|
+
if (prefix && lines[lines.length - 1] === "") lines[lines.length - 1] = prefix;
|
|
795
|
+
for (const char of clean) {
|
|
796
|
+
if (char === "\n") {
|
|
797
|
+
lines.push("");
|
|
798
|
+
} else {
|
|
799
|
+
lines[lines.length - 1] += char;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
return {
|
|
803
|
+
...current,
|
|
804
|
+
lines: lines.slice(-1000),
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
function parseInternalBusPayload(raw = "") {
|
|
809
|
+
let displayMessage = String(raw || "");
|
|
810
|
+
let streamPayload = null;
|
|
811
|
+
try {
|
|
812
|
+
const parsed = JSON.parse(raw);
|
|
813
|
+
if (parsed && typeof parsed === "object" && parsed.reply) {
|
|
814
|
+
displayMessage = parsed.reply;
|
|
815
|
+
} else if (parsed && typeof parsed === "object" && parsed.stream) {
|
|
816
|
+
streamPayload = parsed;
|
|
817
|
+
}
|
|
818
|
+
} catch {
|
|
819
|
+
// Plain text.
|
|
820
|
+
}
|
|
821
|
+
return {
|
|
822
|
+
displayMessage: String(displayMessage || "").replace(/\\r\\n/g, "\n").replace(/\\n/g, "\n").replace(/\\r/g, "\n"),
|
|
823
|
+
streamPayload,
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
function internalStatusLabel(value = "") {
|
|
828
|
+
const state = String(value || "").trim().toLowerCase();
|
|
829
|
+
if (state === "waiting" || state === "waiting_input") return "waiting";
|
|
830
|
+
if (state === "blocked" || state === "error") return "blocked";
|
|
831
|
+
if (state === "busy" || state === "processing" || state === "working") return "working";
|
|
832
|
+
if (state === "idle" || state === "ready") return "ready";
|
|
833
|
+
return state || "ready";
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
function updateInternalViewStatus(view = {}, status = "", detail = "", now = Date.now()) {
|
|
837
|
+
const current = view && typeof view === "object" ? view : {};
|
|
838
|
+
const nextStatus = internalStatusLabel(status || current.status || "");
|
|
839
|
+
const nextDetail = String(detail || "").trim();
|
|
840
|
+
const timed = nextStatus === "working" || nextStatus === "waiting" || nextStatus === "blocked";
|
|
841
|
+
const previousStartedAt = Number.isFinite(current.statusStartedAt) ? current.statusStartedAt : 0;
|
|
842
|
+
const statusStartedAt = timed
|
|
843
|
+
? (current.status === nextStatus && previousStartedAt ? previousStartedAt : now)
|
|
844
|
+
: 0;
|
|
845
|
+
return {
|
|
846
|
+
...current,
|
|
847
|
+
status: nextStatus,
|
|
848
|
+
detail: nextDetail,
|
|
849
|
+
statusStartedAt,
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
function applyInternalAgentTermWrite(view = {}, activeAgentId = "", text = "", meta = {}) {
|
|
854
|
+
const current = view && typeof view === "object" ? view : {};
|
|
855
|
+
if (!current.agentId || current.agentId !== activeAgentId) return current;
|
|
856
|
+
const streamPayload = meta && meta.streamPayload && typeof meta.streamPayload === "object"
|
|
857
|
+
? meta.streamPayload
|
|
858
|
+
: {};
|
|
859
|
+
const done = Boolean((meta && meta.done) || streamPayload.done);
|
|
860
|
+
const rawText = String(text || "");
|
|
861
|
+
const next = rawText
|
|
862
|
+
? appendInternalAgentText(current, rawText, { prefix: "* " })
|
|
863
|
+
: current;
|
|
864
|
+
if (done) return updateInternalViewStatus(next, "ready", "");
|
|
865
|
+
return updateInternalViewStatus(next, "working", "");
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
function appendInternalErrorToView(view = {}, activeAgentId = "", message = "") {
|
|
869
|
+
const current = view && typeof view === "object" ? view : {};
|
|
870
|
+
if (!current.agentId || current.agentId !== activeAgentId) return current;
|
|
871
|
+
const detail = String(message || "unknown error");
|
|
872
|
+
const lines = Array.isArray(current.lines) ? current.lines : [];
|
|
873
|
+
const separator = lines.length > 0 && lines[lines.length - 1] ? "\n" : "";
|
|
874
|
+
return appendInternalAgentText(
|
|
875
|
+
updateInternalViewStatus(current, "blocked", detail),
|
|
876
|
+
`${separator}Error: ${detail}\n`,
|
|
877
|
+
);
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
function computeInternalStatusText(view = {}, spinnerTick = 0, now = Date.now()) {
|
|
881
|
+
const current = view && typeof view === "object" ? view : {};
|
|
882
|
+
const status = internalStatusLabel(current.status || "");
|
|
883
|
+
const label = String(current.label || current.agentId || "agent").trim();
|
|
884
|
+
const detail = String(current.detail || "").trim();
|
|
885
|
+
if (status === "ready") {
|
|
886
|
+
return `ufoo · ${label} · Ready · Enter send · Esc back`;
|
|
887
|
+
}
|
|
888
|
+
const type = status === "waiting" ? "waiting" : "thinking";
|
|
889
|
+
const indicators = fmt.STATUS_INDICATORS[type] || fmt.STATUS_INDICATORS.thinking;
|
|
890
|
+
const indicator = status === "blocked"
|
|
891
|
+
? "!"
|
|
892
|
+
: indicators[Math.max(0, Math.floor(Number(spinnerTick) || 0)) % indicators.length];
|
|
893
|
+
const message = status === "waiting"
|
|
894
|
+
? "Waiting for input"
|
|
895
|
+
: (status === "blocked" ? "Blocked" : "Working");
|
|
896
|
+
const startedAt = Number.isFinite(current.statusStartedAt) ? current.statusStartedAt : 0;
|
|
897
|
+
const timer = startedAt ? ` (${fmt.formatPendingElapsed(now - startedAt)})` : "";
|
|
898
|
+
return `${indicator} ${label} · ${message}${detail ? ` · ${detail}` : ""}${timer} · Esc back`;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
const CHAT_BANNER_LINES = [
|
|
902
|
+
"█ █ █▀▀ █▀█ █▀▄ █▀▀ █ █ ▄▀█ ▀█▀",
|
|
903
|
+
"█ █ █ █ █ █ █ █ █▀█ █▀█ █ ",
|
|
904
|
+
"▀▀▀ ▀▀▀ ▀▀▀ ▀▀ ▀▀▀ ▀ ▀ ▀ ▀ ▀ ",
|
|
905
|
+
];
|
|
906
|
+
|
|
907
|
+
function buildChatBannerLines(props, version) {
|
|
908
|
+
const os = require("os");
|
|
909
|
+
const home = os.homedir();
|
|
910
|
+
const root = props.activeProjectRoot || process.cwd();
|
|
911
|
+
const shortRoot = root.startsWith(home) ? root.replace(home, "~") : root;
|
|
912
|
+
const modeLabel = props.globalMode
|
|
913
|
+
? `global (${props.globalScope || "controller"})`
|
|
914
|
+
: "project";
|
|
915
|
+
const padding = " ".repeat(
|
|
916
|
+
CHAT_BANNER_LINES.reduce((max, line) => Math.max(max, line.length), 0)
|
|
917
|
+
);
|
|
918
|
+
const info = [
|
|
919
|
+
`Version: ${version}`,
|
|
920
|
+
`Mode: ${modeLabel}`,
|
|
921
|
+
`Dictionary: ${shortRoot}`,
|
|
922
|
+
];
|
|
923
|
+
const rows = Math.max(CHAT_BANNER_LINES.length, info.length);
|
|
924
|
+
const out = [];
|
|
925
|
+
for (let i = 0; i < rows; i += 1) {
|
|
926
|
+
const left = CHAT_BANNER_LINES[i] || padding;
|
|
927
|
+
const right = info[i] || "";
|
|
928
|
+
out.push(` ${left} ${right}`);
|
|
929
|
+
}
|
|
930
|
+
return out;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
function resolveProjectRowRoot(row = {}) {
|
|
934
|
+
const raw = String((row && (row.root || row.project_root)) || "").trim();
|
|
935
|
+
if (!raw) return "";
|
|
936
|
+
try {
|
|
937
|
+
const { canonicalProjectRoot } = require("../../runtime/projects");
|
|
938
|
+
return canonicalProjectRoot(raw);
|
|
939
|
+
} catch {
|
|
940
|
+
return path.resolve(raw);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
function loadGlobalProjectRows(activeProjectRoot = "") {
|
|
945
|
+
const {
|
|
946
|
+
listProjectRuntimes,
|
|
947
|
+
filterVisibleProjectRuntimes,
|
|
948
|
+
isGlobalControllerProjectRoot,
|
|
949
|
+
markProjectStopped,
|
|
950
|
+
} = require("../../runtime/projects");
|
|
951
|
+
let rows = listProjectRuntimes({ validate: true, cleanupTmp: true }) || [];
|
|
952
|
+
for (const row of rows) {
|
|
953
|
+
const status = String((row && row.status) || "").trim().toLowerCase();
|
|
954
|
+
const root = resolveProjectRowRoot(row);
|
|
955
|
+
if (status === "stale" && root && !isGlobalControllerProjectRoot(root)) {
|
|
956
|
+
try { markProjectStopped(root); } catch { /* ignore stale cleanup failures */ }
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
rows = filterVisibleProjectRuntimes(rows);
|
|
960
|
+
rows = rows.filter((row) => !isGlobalControllerProjectRoot(resolveProjectRowRoot(row)));
|
|
961
|
+
return rows.map((row) => ({
|
|
962
|
+
id: row.project_id || row.project_root || "",
|
|
963
|
+
label: row.project_name || (row.project_root ? path.basename(row.project_root) : ""),
|
|
964
|
+
root: row.project_root || "",
|
|
965
|
+
status: row.status || "",
|
|
966
|
+
active: resolveProjectRowRoot(row) === String(activeProjectRoot || ""),
|
|
967
|
+
}));
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
function readProjectAgentSnapshot(projectRoot = "") {
|
|
971
|
+
if (!projectRoot) return { agents: [], metaMap: new Map() };
|
|
972
|
+
try {
|
|
973
|
+
const { buildStatus } = require("../../runtime/daemon/status");
|
|
974
|
+
const { buildAgentMaps } = require("../../app/chat/agentDirectory");
|
|
975
|
+
const status = buildStatus(projectRoot);
|
|
976
|
+
const activeIds = Array.isArray(status.active) ? status.active : [];
|
|
977
|
+
const metaList = Array.isArray(status.active_meta) ? status.active_meta : [];
|
|
978
|
+
const { labelMap, metaMap } = buildAgentMaps(activeIds, metaList);
|
|
979
|
+
const merged = new Map();
|
|
980
|
+
for (const id of activeIds) {
|
|
981
|
+
const meta = metaMap.get(id) || {};
|
|
982
|
+
const colon = id.indexOf(":");
|
|
983
|
+
const fallbackType = colon > 0 ? id.slice(0, colon) : id;
|
|
984
|
+
const fallbackId = colon > 0 ? id.slice(colon + 1) : "";
|
|
985
|
+
merged.set(id, {
|
|
986
|
+
...meta,
|
|
987
|
+
fullId: id,
|
|
988
|
+
type: meta.type || fallbackType,
|
|
989
|
+
id: meta.id || fallbackId,
|
|
990
|
+
nickname: labelMap.get(id) || id,
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
return { agents: activeIds, metaMap: merged };
|
|
994
|
+
} catch {
|
|
995
|
+
return { agents: [], metaMap: new Map() };
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
function isCJK(ch) {
|
|
1000
|
+
if (!ch) return false;
|
|
1001
|
+
const code = ch.codePointAt(0);
|
|
1002
|
+
return (code >= 0x2e80 && code <= 0x9fff) ||
|
|
1003
|
+
(code >= 0xac00 && code <= 0xd7af) ||
|
|
1004
|
+
(code >= 0xf900 && code <= 0xfaff) ||
|
|
1005
|
+
(code >= 0xfe30 && code <= 0xfe4f) ||
|
|
1006
|
+
(code >= 0x20000 && code <= 0x2fa1f);
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
function inferStatusType(text = "", requestedType = "") {
|
|
1010
|
+
const type = String(requestedType || "").trim().toLowerCase();
|
|
1011
|
+
if (type === "done" || type === "success" || type === "error" || type === "idle") return type;
|
|
1012
|
+
const clean = stripBlessedTags(String(text || "")).trim();
|
|
1013
|
+
if (/^[✓✔]/.test(clean) || /\bdone\b/i.test(clean) || /\bprocessed\b/i.test(clean)) return "done";
|
|
1014
|
+
if (/^[✗!]/.test(clean) || /\berror\b/i.test(clean) || /\bfailed\b/i.test(clean)) return "error";
|
|
1015
|
+
return type || "typing";
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
function isAnimatedStatusType(type = "") {
|
|
1019
|
+
const value = String(type || "").trim().toLowerCase();
|
|
1020
|
+
return value !== "done" && value !== "success" && value !== "error" && value !== "idle" && value !== "none";
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
function inkKeyToRaw(input, key) {
|
|
1024
|
+
if (key.ctrl && input) {
|
|
1025
|
+
const code = input.charCodeAt(0) - 96;
|
|
1026
|
+
if (code >= 1 && code <= 26) return String.fromCharCode(code);
|
|
1027
|
+
return "";
|
|
1028
|
+
}
|
|
1029
|
+
if (key.return) return "\r";
|
|
1030
|
+
if (key.escape) return "\x1b";
|
|
1031
|
+
if (key.backspace || key.delete) return "\x7f";
|
|
1032
|
+
if (key.tab) return "\t";
|
|
1033
|
+
if (key.upArrow) return "\x1b[A";
|
|
1034
|
+
if (key.downArrow) return "\x1b[B";
|
|
1035
|
+
if (key.rightArrow) return "\x1b[C";
|
|
1036
|
+
if (key.leftArrow) return "\x1b[D";
|
|
1037
|
+
if (input && !key.meta) return input;
|
|
1038
|
+
if (key.meta && input) return `\x1b${input}`;
|
|
1039
|
+
return "";
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
function createChatApp({ React, ink, props, interactive = true }) {
|
|
1043
|
+
const { useReducer, useEffect, useState, useCallback, useRef } = React;
|
|
1044
|
+
const { Box, Text, Static, useInput, useApp, useStdout } = ink;
|
|
1045
|
+
const h = React.createElement;
|
|
1046
|
+
const MultilineInput = createMultilineInput({ React, ink });
|
|
1047
|
+
const DashboardBar = createDashboardBar({ React, ink });
|
|
1048
|
+
|
|
1049
|
+
// Build the initial log: chat history if there is any, otherwise an
|
|
1050
|
+
// ASCII banner with project / mode / version info. We resolve history
|
|
1051
|
+
// synchronously here so the very first paint already shows it instead
|
|
1052
|
+
// of rendering an empty banner and then flashing in the lines.
|
|
1053
|
+
const versionLabel = String(fmt.UCODE_VERSION || "");
|
|
1054
|
+
const banner = buildChatBannerLines(props, versionLabel);
|
|
1055
|
+
const persistedHistory = loadChatHistory(props.projectRoot, 200, { globalMode: props.globalMode });
|
|
1056
|
+
const initialLogText = persistedHistory.length > 0
|
|
1057
|
+
? banner.concat(["", "─── history ───"]).concat(persistedHistory).concat([""])
|
|
1058
|
+
: banner.concat([""]);
|
|
1059
|
+
|
|
1060
|
+
return function ChatApp() {
|
|
1061
|
+
const [state, dispatch] = useReducer(
|
|
1062
|
+
reducer,
|
|
1063
|
+
undefined,
|
|
1064
|
+
() => createInitialState({
|
|
1065
|
+
banner: initialLogText,
|
|
1066
|
+
globalMode: props.globalMode,
|
|
1067
|
+
globalScope: props.globalScope || "controller",
|
|
1068
|
+
settings: props.initialSettings || {},
|
|
1069
|
+
})
|
|
1070
|
+
);
|
|
1071
|
+
const [size, setSize] = useState({ cols: 0, rows: 0 });
|
|
1072
|
+
const [spinnerTick, setSpinnerTick] = useState(0);
|
|
1073
|
+
const [currentProjectRoot, setCurrentProjectRoot] = useState(props.activeProjectRoot || props.projectRoot || "");
|
|
1074
|
+
const [internalAgentView, setInternalAgentView] = useState(() => createInternalAgentViewState());
|
|
1075
|
+
const [multiWindowActive, setMultiWindowActive] = useState(false);
|
|
1076
|
+
const [mwCursor, setMwCursor] = useState(0);
|
|
1077
|
+
const [mwTerminalFocused, setMwTerminalFocused] = useState(false);
|
|
1078
|
+
const mwTerminalFocusedRef = useRef(false);
|
|
1079
|
+
const mwLastInputRef = useRef({ char: "", time: 0 });
|
|
1080
|
+
const stateRef = useRef(state);
|
|
1081
|
+
const sizeRef = useRef(size);
|
|
1082
|
+
const currentProjectRootRef = useRef(currentProjectRoot);
|
|
1083
|
+
const internalAgentViewRef = useRef(internalAgentView);
|
|
1084
|
+
const multiWindowControllerRef = useRef(null);
|
|
1085
|
+
const multiWindowChromeRef = useRef({ statusText: "", promptPrefix: "› ", draft: "", dashboardLines: [] });
|
|
1086
|
+
const multiWindowWatchedInternalAgentsRef = useRef(new Set());
|
|
1087
|
+
const pendingRef = useRef(null);
|
|
1088
|
+
const streamStateRef = useRef(null);
|
|
1089
|
+
const historyScopeRef = useRef(null);
|
|
1090
|
+
const switchToProjectRootRef = useRef(null);
|
|
1091
|
+
const activeChatHistoryRoot = currentProjectRoot || props.projectRoot;
|
|
1092
|
+
const activeChatHistoryOptions = chatHistoryOptionsForScope({
|
|
1093
|
+
globalMode: props.globalMode,
|
|
1094
|
+
globalScope: state.globalScope,
|
|
1095
|
+
});
|
|
1096
|
+
const { exit } = useApp();
|
|
1097
|
+
const { stdout } = useStdout();
|
|
1098
|
+
|
|
1099
|
+
useEffect(() => {
|
|
1100
|
+
stateRef.current = state;
|
|
1101
|
+
}, [state]);
|
|
1102
|
+
|
|
1103
|
+
useEffect(() => {
|
|
1104
|
+
sizeRef.current = size;
|
|
1105
|
+
}, [size]);
|
|
1106
|
+
|
|
1107
|
+
useEffect(() => {
|
|
1108
|
+
currentProjectRootRef.current = currentProjectRoot;
|
|
1109
|
+
}, [currentProjectRoot]);
|
|
1110
|
+
|
|
1111
|
+
historyScopeRef.current = {
|
|
1112
|
+
root: activeChatHistoryRoot,
|
|
1113
|
+
options: activeChatHistoryOptions,
|
|
1114
|
+
};
|
|
1115
|
+
|
|
1116
|
+
const appendScopedHistory = useCallback((kind, text, meta = {}) => {
|
|
1117
|
+
appendChatHistory(activeChatHistoryRoot, kind, text, meta, activeChatHistoryOptions);
|
|
1118
|
+
}, [activeChatHistoryRoot, activeChatHistoryOptions.globalMode]);
|
|
1119
|
+
|
|
1120
|
+
const setStatusText = useCallback((text, options = {}) => {
|
|
1121
|
+
const clean = stripBlessedTags(text).trim();
|
|
1122
|
+
if (!clean) {
|
|
1123
|
+
dispatch({ type: "status/idle" });
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1126
|
+
dispatch({
|
|
1127
|
+
type: "status/set",
|
|
1128
|
+
payload: {
|
|
1129
|
+
message: clean,
|
|
1130
|
+
type: inferStatusType(clean, options.type || "typing"),
|
|
1131
|
+
showTimer: options.showTimer === true,
|
|
1132
|
+
startedAt: options.startedAt || Date.now(),
|
|
1133
|
+
},
|
|
1134
|
+
});
|
|
1135
|
+
}, []);
|
|
1136
|
+
|
|
1137
|
+
const logInkMessage = useCallback((kind, text, meta = {}) => {
|
|
1138
|
+
const type = String(kind || "system");
|
|
1139
|
+
if (type === "status") {
|
|
1140
|
+
setStatusText(text);
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
1143
|
+
const lines = normalizeInkLogLines(text);
|
|
1144
|
+
if (lines.length === 0) return;
|
|
1145
|
+
dispatch({ type: "log/appendMany", lines });
|
|
1146
|
+
appendScopedHistory(type, stripBlessedTags(text), meta);
|
|
1147
|
+
}, [appendScopedHistory, setStatusText]);
|
|
1148
|
+
|
|
1149
|
+
if (!streamStateRef.current) {
|
|
1150
|
+
streamStateRef.current = createInkStreamState({
|
|
1151
|
+
dispatch,
|
|
1152
|
+
appendHistory: (kind, text, meta = {}) => {
|
|
1153
|
+
const scope = historyScopeRef.current || {};
|
|
1154
|
+
appendChatHistory(scope.root || props.projectRoot, kind, text, meta, scope.options || {});
|
|
1155
|
+
},
|
|
1156
|
+
displayNameForPublisher: (publisher) => {
|
|
1157
|
+
const current = stateRef.current || {};
|
|
1158
|
+
const meta = current.activeAgentMeta instanceof Map ? current.activeAgentMeta.get(publisher) : null;
|
|
1159
|
+
return getAgentLabelFor(meta, publisher);
|
|
1160
|
+
},
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
const getMultiWindowController = useCallback(() => {
|
|
1165
|
+
if (multiWindowControllerRef.current) return multiWindowControllerRef.current;
|
|
1166
|
+
const processStdout = stdout || (typeof process !== "undefined" ? process.stdout : null);
|
|
1167
|
+
if (!processStdout || typeof processStdout.write !== "function") return null;
|
|
1168
|
+
|
|
1169
|
+
const originalWrite = processStdout.write.bind(processStdout);
|
|
1170
|
+
const { createMultiWindowController } = require("../../app/chat/multiWindow");
|
|
1171
|
+
multiWindowControllerRef.current = createMultiWindowController({
|
|
1172
|
+
processStdout: { write: originalWrite, rows: processStdout.rows, columns: processStdout.columns },
|
|
1173
|
+
getRows: () => {
|
|
1174
|
+
const currentSize = sizeRef.current || {};
|
|
1175
|
+
return currentSize.rows || processStdout.rows || 24;
|
|
1176
|
+
},
|
|
1177
|
+
getCols: () => {
|
|
1178
|
+
const currentSize = sizeRef.current || {};
|
|
1179
|
+
return currentSize.cols || processStdout.columns || 80;
|
|
1180
|
+
},
|
|
1181
|
+
getInjectSockPath: (agentId) =>
|
|
1182
|
+
resolveInjectSockPathForAgent(currentProjectRootRef.current || props.projectRoot, agentId),
|
|
1183
|
+
getActiveAgents: () => {
|
|
1184
|
+
const current = stateRef.current || {};
|
|
1185
|
+
return Array.isArray(current.agents) ? current.agents : [];
|
|
1186
|
+
},
|
|
1187
|
+
getAgentPaneOptions: (agentId) => {
|
|
1188
|
+
const current = stateRef.current || {};
|
|
1189
|
+
const enterRequest = resolveAgentEnterRequest({
|
|
1190
|
+
agentId,
|
|
1191
|
+
projectRoot: currentProjectRootRef.current || props.projectRoot,
|
|
1192
|
+
activeAgentMeta: current.activeAgentMeta,
|
|
1193
|
+
settings: current.settings,
|
|
1194
|
+
});
|
|
1195
|
+
if (!enterRequest || !enterRequest.useBus) return { mode: "socket" };
|
|
1196
|
+
const metaMap = current.activeAgentMeta instanceof Map ? current.activeAgentMeta : new Map();
|
|
1197
|
+
const agentMeta = metaMap.get(agentId) || {};
|
|
1198
|
+
let initialLines = [];
|
|
1199
|
+
try {
|
|
1200
|
+
const { loadInternalAgentLogHistory } = require("../../app/chat/internalAgentLogHistory");
|
|
1201
|
+
initialLines = loadInternalAgentLogHistory(currentProjectRootRef.current || props.projectRoot, agentId, {
|
|
1202
|
+
maxEvents: 200,
|
|
1203
|
+
maxLines: 200,
|
|
1204
|
+
});
|
|
1205
|
+
} catch { initialLines = []; }
|
|
1206
|
+
return {
|
|
1207
|
+
mode: "internal",
|
|
1208
|
+
initialLines: [
|
|
1209
|
+
`ufoo internal agent · ${getAgentLabelFor(agentMeta, agentId)}`,
|
|
1210
|
+
`agent: ${agentId}`,
|
|
1211
|
+
"",
|
|
1212
|
+
...initialLines,
|
|
1213
|
+
],
|
|
1214
|
+
};
|
|
1215
|
+
},
|
|
1216
|
+
getChatLogLines: () => {
|
|
1217
|
+
const current = stateRef.current || {};
|
|
1218
|
+
return Array.isArray(current.logLines)
|
|
1219
|
+
? current.logLines.map((item) => String((item && item.text) || ""))
|
|
1220
|
+
: [];
|
|
1221
|
+
},
|
|
1222
|
+
getStatusText: () => {
|
|
1223
|
+
const chrome = multiWindowChromeRef.current;
|
|
1224
|
+
return chrome ? chrome.statusText : "";
|
|
1225
|
+
},
|
|
1226
|
+
getPromptPrefix: () => {
|
|
1227
|
+
const chrome = multiWindowChromeRef.current;
|
|
1228
|
+
return chrome ? chrome.promptPrefix : "› ";
|
|
1229
|
+
},
|
|
1230
|
+
getCurrentDraft: () => {
|
|
1231
|
+
const chrome = multiWindowChromeRef.current;
|
|
1232
|
+
return chrome ? chrome.draft : "";
|
|
1233
|
+
},
|
|
1234
|
+
getCursorPos: () => {
|
|
1235
|
+
const chrome = multiWindowChromeRef.current;
|
|
1236
|
+
return chrome ? chrome.cursor : 0;
|
|
1237
|
+
},
|
|
1238
|
+
getCompletions: () => {
|
|
1239
|
+
const chrome = multiWindowChromeRef.current;
|
|
1240
|
+
if (!chrome || !chrome.completions || chrome.completions.length === 0) {
|
|
1241
|
+
return { items: [], index: -1, windowStart: 0, pageSize: 8 };
|
|
1242
|
+
}
|
|
1243
|
+
return {
|
|
1244
|
+
items: chrome.completions,
|
|
1245
|
+
index: chrome.completionIndex,
|
|
1246
|
+
windowStart: chrome.completionWindowStart,
|
|
1247
|
+
pageSize: chrome.completionPageSize || 8,
|
|
1248
|
+
};
|
|
1249
|
+
},
|
|
1250
|
+
getAgentLabel: (id) => {
|
|
1251
|
+
const current = stateRef.current || {};
|
|
1252
|
+
const metaMap = current.activeAgentMeta || new Map();
|
|
1253
|
+
return getAgentLabelFor(metaMap.get(id), id);
|
|
1254
|
+
},
|
|
1255
|
+
getInternalPaneInfo: (id) => {
|
|
1256
|
+
const current = stateRef.current || {};
|
|
1257
|
+
const metaMap = current.activeAgentMeta instanceof Map ? current.activeAgentMeta : new Map();
|
|
1258
|
+
const meta = metaMap.get(id) || {};
|
|
1259
|
+
const status = internalStatusLabel(meta.activity_state || meta.state || "");
|
|
1260
|
+
const detail = String(meta.activity_detail || meta.detail || meta.status_text || "").trim();
|
|
1261
|
+
return {
|
|
1262
|
+
status,
|
|
1263
|
+
detail,
|
|
1264
|
+
input: "",
|
|
1265
|
+
cursor: 0,
|
|
1266
|
+
};
|
|
1267
|
+
},
|
|
1268
|
+
getDashboardLines: () => {
|
|
1269
|
+
const chrome = multiWindowChromeRef.current;
|
|
1270
|
+
return chrome ? chrome.dashboardLines : [];
|
|
1271
|
+
},
|
|
1272
|
+
getTerminalFocused: () => mwTerminalFocusedRef.current,
|
|
1273
|
+
freezeScreen: (frozen) => {
|
|
1274
|
+
if (frozen) {
|
|
1275
|
+
processStdout.write = () => true;
|
|
1276
|
+
} else {
|
|
1277
|
+
processStdout.write = originalWrite;
|
|
1278
|
+
}
|
|
1279
|
+
},
|
|
1280
|
+
restoreTerminal: () => {
|
|
1281
|
+
const rows = processStdout.rows || 24;
|
|
1282
|
+
originalWrite(`\x1b[1;${rows}r`);
|
|
1283
|
+
originalWrite("\x1b[2J\x1b[H");
|
|
1284
|
+
},
|
|
1285
|
+
onInternalSubmit: (agentId, message) => {
|
|
1286
|
+
sendInternalAgentMessage(agentId, message);
|
|
1287
|
+
},
|
|
1288
|
+
onExit: () => {
|
|
1289
|
+
setMultiWindowActive(false);
|
|
1290
|
+
},
|
|
1291
|
+
});
|
|
1292
|
+
return multiWindowControllerRef.current;
|
|
1293
|
+
}, [props.projectRoot, stdout]);
|
|
1294
|
+
|
|
1295
|
+
const toggleMultiWindow = useCallback(() => createInkMultiWindowToggle({
|
|
1296
|
+
getController: getMultiWindowController,
|
|
1297
|
+
setActive: setMultiWindowActive,
|
|
1298
|
+
logMessage: logInkMessage,
|
|
1299
|
+
})(), [getMultiWindowController, logInkMessage]);
|
|
1300
|
+
|
|
1301
|
+
useEffect(() => () => {
|
|
1302
|
+
const controller = multiWindowControllerRef.current;
|
|
1303
|
+
if (controller && typeof controller.exit === "function") {
|
|
1304
|
+
try { controller.exit(); } catch { /* ignore */ }
|
|
1305
|
+
}
|
|
1306
|
+
multiWindowControllerRef.current = null;
|
|
1307
|
+
}, []);
|
|
1308
|
+
|
|
1309
|
+
useEffect(() => {
|
|
1310
|
+
internalAgentViewRef.current = internalAgentView;
|
|
1311
|
+
}, [internalAgentView]);
|
|
1312
|
+
|
|
1313
|
+
useEffect(() => {
|
|
1314
|
+
if (!stdout) return undefined;
|
|
1315
|
+
const update = () =>
|
|
1316
|
+
setSize({ cols: stdout.columns || 0, rows: stdout.rows || 0 });
|
|
1317
|
+
update();
|
|
1318
|
+
stdout.on("resize", update);
|
|
1319
|
+
return () => stdout.off("resize", update);
|
|
1320
|
+
}, [stdout]);
|
|
1321
|
+
|
|
1322
|
+
// Load persisted input history once on mount.
|
|
1323
|
+
useEffect(() => {
|
|
1324
|
+
try {
|
|
1325
|
+
const history = loadInputHistory(props.projectRoot, 200, { globalMode: props.globalMode });
|
|
1326
|
+
if (history.length > 0) dispatch({ type: "history/load", list: history });
|
|
1327
|
+
} catch { /* ignore */ }
|
|
1328
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1329
|
+
}, []);
|
|
1330
|
+
|
|
1331
|
+
const sendInternalAgentWatch = (agentId, enabled) => {
|
|
1332
|
+
if (!agentId || !props.daemonConnection || typeof props.daemonConnection.send !== "function") return;
|
|
1333
|
+
try {
|
|
1334
|
+
const { IPC_REQUEST_TYPES } = require("../../runtime/contracts/eventContract");
|
|
1335
|
+
props.daemonConnection.send({
|
|
1336
|
+
type: IPC_REQUEST_TYPES.BUS_WATCH,
|
|
1337
|
+
agent_id: agentId,
|
|
1338
|
+
enabled: enabled !== false,
|
|
1339
|
+
});
|
|
1340
|
+
} catch { /* ignore */ }
|
|
1341
|
+
};
|
|
1342
|
+
|
|
1343
|
+
const reconcileMultiWindowInternalWatches = useCallback(() => {
|
|
1344
|
+
const current = stateRef.current || {};
|
|
1345
|
+
const agents = Array.isArray(current.agents) ? current.agents : [];
|
|
1346
|
+
const next = new Set();
|
|
1347
|
+
if (multiWindowActive) {
|
|
1348
|
+
for (const agentId of agents) {
|
|
1349
|
+
const enterRequest = resolveAgentEnterRequest({
|
|
1350
|
+
agentId,
|
|
1351
|
+
projectRoot: currentProjectRootRef.current || props.projectRoot,
|
|
1352
|
+
activeAgentMeta: current.activeAgentMeta,
|
|
1353
|
+
settings: current.settings,
|
|
1354
|
+
});
|
|
1355
|
+
if (enterRequest && enterRequest.useBus) next.add(agentId);
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
const previous = multiWindowWatchedInternalAgentsRef.current;
|
|
1359
|
+
for (const agentId of next) {
|
|
1360
|
+
if (!previous.has(agentId)) sendInternalAgentWatch(agentId, true);
|
|
1361
|
+
}
|
|
1362
|
+
for (const agentId of previous) {
|
|
1363
|
+
if (!next.has(agentId)) sendInternalAgentWatch(agentId, false);
|
|
1364
|
+
}
|
|
1365
|
+
multiWindowWatchedInternalAgentsRef.current = next;
|
|
1366
|
+
}, [multiWindowActive, props.projectRoot, props.daemonConnection]);
|
|
1367
|
+
|
|
1368
|
+
useEffect(() => {
|
|
1369
|
+
if (!multiWindowActive) return;
|
|
1370
|
+
const controller = multiWindowControllerRef.current;
|
|
1371
|
+
if (!controller) return;
|
|
1372
|
+
reconcileMultiWindowInternalWatches();
|
|
1373
|
+
if (typeof controller.syncAgents === "function") controller.syncAgents();
|
|
1374
|
+
if (typeof controller.renderAll === "function") controller.renderAll();
|
|
1375
|
+
}, [multiWindowActive, state.agents, state.logLines, state.draft, state.status, size.cols, size.rows, mwCursor, state.focusMode, state.dashboardView, state.selectedAgentIndex, state.selectedProjectIndex, state.selectedModeIndex, state.selectedProviderIndex, state.selectedCronIndex, mwTerminalFocused, reconcileMultiWindowInternalWatches]);
|
|
1376
|
+
|
|
1377
|
+
useEffect(() => {
|
|
1378
|
+
if (multiWindowActive) return;
|
|
1379
|
+
reconcileMultiWindowInternalWatches();
|
|
1380
|
+
}, [multiWindowActive, reconcileMultiWindowInternalWatches]);
|
|
1381
|
+
|
|
1382
|
+
const sendInternalAgentMessage = (agentId, message) => {
|
|
1383
|
+
if (!agentId || !message || !props.daemonConnection || typeof props.daemonConnection.send !== "function") return;
|
|
1384
|
+
try {
|
|
1385
|
+
const { IPC_REQUEST_TYPES } = require("../../runtime/contracts/eventContract");
|
|
1386
|
+
props.daemonConnection.send({
|
|
1387
|
+
type: IPC_REQUEST_TYPES.BUS_SEND,
|
|
1388
|
+
target: agentId,
|
|
1389
|
+
message,
|
|
1390
|
+
injection_mode: "immediate",
|
|
1391
|
+
source: "chat-internal-agent-view",
|
|
1392
|
+
});
|
|
1393
|
+
} catch (err) {
|
|
1394
|
+
setInternalAgentView((prev) => appendInternalAgentText(
|
|
1395
|
+
updateInternalViewStatus(prev, "blocked", err && err.message ? err.message : String(err || "")),
|
|
1396
|
+
`Error: ${err && err.message ? err.message : err}\n`,
|
|
1397
|
+
));
|
|
1398
|
+
}
|
|
1399
|
+
};
|
|
1400
|
+
|
|
1401
|
+
const isInternalAlias = (view, value) => {
|
|
1402
|
+
if (!view || !view.agentId) return false;
|
|
1403
|
+
const text = String(value || "");
|
|
1404
|
+
if (!text) return false;
|
|
1405
|
+
const aliases = new Set((view.aliases || []).concat([view.agentId, view.label]).filter(Boolean).map(String));
|
|
1406
|
+
return aliases.has(text);
|
|
1407
|
+
};
|
|
1408
|
+
|
|
1409
|
+
const buildInternalAgentAliases = (agentId) => {
|
|
1410
|
+
const current = stateRef.current || {};
|
|
1411
|
+
const metaMap = current.activeAgentMeta instanceof Map ? current.activeAgentMeta : new Map();
|
|
1412
|
+
const meta = metaMap.get(agentId) || {};
|
|
1413
|
+
return new Set([
|
|
1414
|
+
agentId,
|
|
1415
|
+
meta.nickname,
|
|
1416
|
+
meta.scoped_nickname,
|
|
1417
|
+
meta.display_nickname,
|
|
1418
|
+
meta.fullId,
|
|
1419
|
+
].filter(Boolean).map(String));
|
|
1420
|
+
};
|
|
1421
|
+
|
|
1422
|
+
const writeMultiWindowInternalEvent = useCallback((data = {}) => {
|
|
1423
|
+
const controller = multiWindowControllerRef.current;
|
|
1424
|
+
if (!multiWindowActive || !controller || typeof controller.writeToPane !== "function") return false;
|
|
1425
|
+
const watched = multiWindowWatchedInternalAgentsRef.current;
|
|
1426
|
+
if (!watched || watched.size === 0) return false;
|
|
1427
|
+
|
|
1428
|
+
let handled = false;
|
|
1429
|
+
for (const agentId of watched) {
|
|
1430
|
+
const aliases = buildInternalAgentAliases(agentId);
|
|
1431
|
+
const publisher = String(data.publisher || (data.event === "broadcast" ? "broadcast" : "bus"));
|
|
1432
|
+
const target = String(data.target || data.subscriber || "");
|
|
1433
|
+
const fromAgent = aliases.has(publisher);
|
|
1434
|
+
const toAgent = aliases.has(target) || aliases.has(String(data.subscriber || ""));
|
|
1435
|
+
if (!fromAgent && !toAgent) continue;
|
|
1436
|
+
if (data.silent) {
|
|
1437
|
+
handled = true;
|
|
1438
|
+
continue;
|
|
1439
|
+
}
|
|
1440
|
+
if (data.source === "chat-internal-agent-view" && toAgent && !fromAgent) {
|
|
1441
|
+
handled = true;
|
|
1442
|
+
continue;
|
|
1443
|
+
}
|
|
1444
|
+
if (data.event === "activity_state_changed") {
|
|
1445
|
+
const state = internalStatusLabel(data.state || data.activity_state || "");
|
|
1446
|
+
const detail = String(data.detail || (data.data && data.data.detail) || data.message || "").trim();
|
|
1447
|
+
controller.writeToPane(agentId, `\r\n[${state}${detail ? ` · ${detail}` : ""}]\r\n`);
|
|
1448
|
+
handled = true;
|
|
1449
|
+
continue;
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
const { displayMessage, streamPayload } = parseInternalBusPayload(data.message || "");
|
|
1453
|
+
if (streamPayload) {
|
|
1454
|
+
if (!fromAgent) {
|
|
1455
|
+
handled = true;
|
|
1456
|
+
continue;
|
|
1457
|
+
}
|
|
1458
|
+
const delta = typeof streamPayload.delta === "string"
|
|
1459
|
+
? streamPayload.delta.replace(/\\r\\n/g, "\n").replace(/\\n/g, "\n").replace(/\\r/g, "\n")
|
|
1460
|
+
: "";
|
|
1461
|
+
if (delta) controller.writeToPane(agentId, delta);
|
|
1462
|
+
if (streamPayload.done) controller.writeToPane(agentId, "\r\n");
|
|
1463
|
+
handled = true;
|
|
1464
|
+
continue;
|
|
1465
|
+
}
|
|
1466
|
+
if (!displayMessage) {
|
|
1467
|
+
handled = true;
|
|
1468
|
+
continue;
|
|
1469
|
+
}
|
|
1470
|
+
const prefix = fromAgent ? "* " : "> ";
|
|
1471
|
+
controller.writeToPane(agentId, `${prefix}${displayMessage.replace(/\n/g, `\r\n `)}\r\n`);
|
|
1472
|
+
handled = true;
|
|
1473
|
+
}
|
|
1474
|
+
return handled;
|
|
1475
|
+
}, [multiWindowActive]);
|
|
1476
|
+
|
|
1477
|
+
const handleInternalStatus = (data = {}) => {
|
|
1478
|
+
const view = internalAgentViewRef.current;
|
|
1479
|
+
if (!view || !view.agentId) return;
|
|
1480
|
+
const metaList = Array.isArray(data.active_meta) ? data.active_meta : [];
|
|
1481
|
+
for (const meta of metaList) {
|
|
1482
|
+
const metaId = meta && (meta.fullId || meta.subscriber_id || meta.id) ? String(meta.fullId || meta.subscriber_id || meta.id) : "";
|
|
1483
|
+
const typedId = meta && meta.type && meta.id ? `${meta.type}:${meta.id}` : "";
|
|
1484
|
+
if (!isInternalAlias(view, metaId) && !isInternalAlias(view, typedId)) continue;
|
|
1485
|
+
const status = internalStatusLabel(meta.activity_state || meta.state || "");
|
|
1486
|
+
const detail = String(meta.activity_detail || meta.detail || meta.status_text || "").trim();
|
|
1487
|
+
setInternalAgentView((prev) => (
|
|
1488
|
+
prev.agentId === view.agentId ? updateInternalViewStatus(prev, status, detail) : prev
|
|
1489
|
+
));
|
|
1490
|
+
return;
|
|
1491
|
+
}
|
|
1492
|
+
};
|
|
1493
|
+
|
|
1494
|
+
const handleInternalBusMessage = (data = {}) => {
|
|
1495
|
+
const view = internalAgentViewRef.current;
|
|
1496
|
+
if (!view || !view.agentId) return false;
|
|
1497
|
+
if (data.event === "activity_state_changed") {
|
|
1498
|
+
const actor = String(data.subscriber || data.publisher || "").trim();
|
|
1499
|
+
if (!isInternalAlias(view, actor)) return false;
|
|
1500
|
+
setInternalAgentView((prev) => (
|
|
1501
|
+
prev.agentId === view.agentId
|
|
1502
|
+
? {
|
|
1503
|
+
...updateInternalViewStatus(
|
|
1504
|
+
prev,
|
|
1505
|
+
data.state || data.activity_state || "",
|
|
1506
|
+
data.detail || (data.data && data.data.detail) || data.message || "",
|
|
1507
|
+
),
|
|
1508
|
+
}
|
|
1509
|
+
: prev
|
|
1510
|
+
));
|
|
1511
|
+
return true;
|
|
1512
|
+
}
|
|
1513
|
+
const publisher = String(data.publisher || (data.event === "broadcast" ? "broadcast" : "bus"));
|
|
1514
|
+
const target = String(data.target || data.subscriber || "");
|
|
1515
|
+
const fromAgent = isInternalAlias(view, publisher);
|
|
1516
|
+
const toAgent = isInternalAlias(view, target);
|
|
1517
|
+
if (!fromAgent && !toAgent) return false;
|
|
1518
|
+
if (data.silent) return true;
|
|
1519
|
+
if (data.source === "chat-internal-agent-view" && toAgent && !fromAgent) return true;
|
|
1520
|
+
|
|
1521
|
+
const { displayMessage, streamPayload } = parseInternalBusPayload(data.message || "");
|
|
1522
|
+
if (streamPayload) {
|
|
1523
|
+
if (!fromAgent) return true;
|
|
1524
|
+
const delta = typeof streamPayload.delta === "string"
|
|
1525
|
+
? streamPayload.delta.replace(/\\r\\n/g, "\n").replace(/\\n/g, "\n").replace(/\\r/g, "\n")
|
|
1526
|
+
: "";
|
|
1527
|
+
if (delta) {
|
|
1528
|
+
setInternalAgentView((prev) => (
|
|
1529
|
+
prev.agentId === view.agentId
|
|
1530
|
+
? updateInternalViewStatus(
|
|
1531
|
+
appendInternalAgentText(prev, delta, { prefix: "* " }),
|
|
1532
|
+
streamPayload.done ? "ready" : "working",
|
|
1533
|
+
streamPayload.reason || prev.detail || "",
|
|
1534
|
+
)
|
|
1535
|
+
: prev
|
|
1536
|
+
));
|
|
1537
|
+
} else if (streamPayload.done) {
|
|
1538
|
+
setInternalAgentView((prev) => (
|
|
1539
|
+
prev.agentId === view.agentId ? updateInternalViewStatus(prev, "ready", "") : prev
|
|
1540
|
+
));
|
|
1541
|
+
}
|
|
1542
|
+
return true;
|
|
1543
|
+
}
|
|
1544
|
+
if (!displayMessage) return true;
|
|
1545
|
+
setInternalAgentView((prev) => {
|
|
1546
|
+
if (prev.agentId !== view.agentId) return prev;
|
|
1547
|
+
const next = fromAgent
|
|
1548
|
+
? appendInternalAgentText(prev, `${displayMessage}\n`, { prefix: "* " })
|
|
1549
|
+
: appendInternalAgentText(prev, `${displayMessage}\n`, { prefix: "> " });
|
|
1550
|
+
return fromAgent ? updateInternalViewStatus(next, "ready", "") : next;
|
|
1551
|
+
});
|
|
1552
|
+
return true;
|
|
1553
|
+
};
|
|
1554
|
+
|
|
1555
|
+
const handleInternalErrorMessage = (message = "") => {
|
|
1556
|
+
const view = internalAgentViewRef.current;
|
|
1557
|
+
if (!view || !view.agentId) return false;
|
|
1558
|
+
setInternalAgentView((prev) => (
|
|
1559
|
+
appendInternalErrorToView(prev, view.agentId, message)
|
|
1560
|
+
));
|
|
1561
|
+
return true;
|
|
1562
|
+
};
|
|
1563
|
+
|
|
1564
|
+
const handleInternalSendOk = () => {
|
|
1565
|
+
const view = internalAgentViewRef.current;
|
|
1566
|
+
if (!view || !view.agentId) return false;
|
|
1567
|
+
setInternalAgentView((prev) => (
|
|
1568
|
+
prev.agentId === view.agentId ? updateInternalViewStatus(prev, "ready", "") : prev
|
|
1569
|
+
));
|
|
1570
|
+
return true;
|
|
1571
|
+
};
|
|
1572
|
+
|
|
1573
|
+
const requestDaemonStatus = useCallback(() => {
|
|
1574
|
+
try {
|
|
1575
|
+
const { IPC_REQUEST_TYPES } = require("../../runtime/contracts/eventContract");
|
|
1576
|
+
const conn = props.daemonConnection;
|
|
1577
|
+
if (conn && typeof conn.send === "function") conn.send({ type: IPC_REQUEST_TYPES.STATUS });
|
|
1578
|
+
} catch { /* ignore */ }
|
|
1579
|
+
}, [props.daemonConnection]);
|
|
1580
|
+
|
|
1581
|
+
const updateDashboardFromStatus = useCallback((data = {}) => {
|
|
1582
|
+
const activeIds = Array.isArray(data.active) ? data.active : [];
|
|
1583
|
+
const metaList = Array.isArray(data.active_meta) ? data.active_meta : [];
|
|
1584
|
+
const { buildAgentMaps } = require("../../app/chat/agentDirectory");
|
|
1585
|
+
const { labelMap, metaMap } = buildAgentMaps(activeIds, metaList);
|
|
1586
|
+
const agentsForDispatch = activeIds.map((id) => {
|
|
1587
|
+
const meta = metaMap.get(id) || {};
|
|
1588
|
+
const colon = id.indexOf(":");
|
|
1589
|
+
const fallbackType = colon > 0 ? id.slice(0, colon) : id;
|
|
1590
|
+
const fallbackId = colon > 0 ? id.slice(colon + 1) : "";
|
|
1591
|
+
return {
|
|
1592
|
+
...meta,
|
|
1593
|
+
fullId: id,
|
|
1594
|
+
type: meta.type || fallbackType,
|
|
1595
|
+
id: meta.id || fallbackId,
|
|
1596
|
+
nickname: labelMap.get(id) || id,
|
|
1597
|
+
};
|
|
1598
|
+
});
|
|
1599
|
+
dispatch({ type: "agents/set", list: agentsForDispatch });
|
|
1600
|
+
if (data.cron && Array.isArray(data.cron.tasks)) {
|
|
1601
|
+
dispatch({ type: "cron/set", list: data.cron.tasks });
|
|
1602
|
+
}
|
|
1603
|
+
dispatch({ type: "loop/set", summary: data.loop || null });
|
|
1604
|
+
handleInternalStatus(data);
|
|
1605
|
+
}, []);
|
|
1606
|
+
|
|
1607
|
+
// Wire daemon: register a message handler that turns IPC responses
|
|
1608
|
+
// through the same daemonMessageRouter blessed uses, then adapts the
|
|
1609
|
+
// blessed callbacks to Ink state updates.
|
|
1610
|
+
useEffect(() => {
|
|
1611
|
+
if (!interactive) return undefined;
|
|
1612
|
+
const conn = props.daemonConnection;
|
|
1613
|
+
const setHandler = props.setDaemonMessageHandler;
|
|
1614
|
+
if (!conn || typeof conn.connect !== "function" || typeof setHandler !== "function") {
|
|
1615
|
+
return undefined;
|
|
1616
|
+
}
|
|
1617
|
+
const { IPC_RESPONSE_TYPES } = require("../../runtime/contracts/eventContract");
|
|
1618
|
+
const { createDaemonMessageRouter } = require("../../app/chat/daemonMessageRouter");
|
|
1619
|
+
const streamState = streamStateRef.current;
|
|
1620
|
+
const router = createDaemonMessageRouter({
|
|
1621
|
+
escapeBlessed: (value) => String(value == null ? "" : value),
|
|
1622
|
+
stripBlessedTags,
|
|
1623
|
+
logMessage: logInkMessage,
|
|
1624
|
+
renderScreen: () => {},
|
|
1625
|
+
updateDashboard: updateDashboardFromStatus,
|
|
1626
|
+
requestStatus: requestDaemonStatus,
|
|
1627
|
+
resolveStatusLine: (text, data = {}) => {
|
|
1628
|
+
setStatusText(text, {
|
|
1629
|
+
type: data && data.phase === "error" ? "error" : "typing",
|
|
1630
|
+
showTimer: false,
|
|
1631
|
+
});
|
|
1632
|
+
},
|
|
1633
|
+
enqueueBusStatus: (item = {}) => setStatusText(item.text || "Processing bus message", { type: "typing" }),
|
|
1634
|
+
resolveBusStatus: (item = {}) => setStatusText(item.text || "Bus message processed", { type: "done" }),
|
|
1635
|
+
getPending: () => pendingRef.current,
|
|
1636
|
+
setPending: (value) => { pendingRef.current = value || null; },
|
|
1637
|
+
resolveAgentDisplayName: (value) => {
|
|
1638
|
+
const current = stateRef.current || {};
|
|
1639
|
+
const meta = current.activeAgentMeta instanceof Map ? current.activeAgentMeta.get(value) : null;
|
|
1640
|
+
return getAgentLabelFor(meta, value);
|
|
1641
|
+
},
|
|
1642
|
+
getCurrentView: () => {
|
|
1643
|
+
const current = stateRef.current || {};
|
|
1644
|
+
return current.viewingAgentId ? "agent" : "main";
|
|
1645
|
+
},
|
|
1646
|
+
isAgentViewUsesBus: () => Boolean(internalAgentViewRef.current && internalAgentViewRef.current.agentId),
|
|
1647
|
+
getViewingAgent: () => {
|
|
1648
|
+
const current = stateRef.current || {};
|
|
1649
|
+
return current.viewingAgentId || (internalAgentViewRef.current && internalAgentViewRef.current.agentId) || "";
|
|
1650
|
+
},
|
|
1651
|
+
isAgentEventForViewingAgent: (data, viewingAgent, publisher) => {
|
|
1652
|
+
const view = internalAgentViewRef.current || {};
|
|
1653
|
+
if (!view.agentId && !viewingAgent) return false;
|
|
1654
|
+
const candidates = [
|
|
1655
|
+
viewingAgent,
|
|
1656
|
+
publisher,
|
|
1657
|
+
data && data.publisher,
|
|
1658
|
+
data && data.target,
|
|
1659
|
+
data && data.subscriber,
|
|
1660
|
+
];
|
|
1661
|
+
return candidates.some((candidate) => isInternalAlias(view, candidate));
|
|
1662
|
+
},
|
|
1663
|
+
writeToAgentTerm: (text, meta = {}) => {
|
|
1664
|
+
const view = internalAgentViewRef.current;
|
|
1665
|
+
if (!view || !view.agentId) return;
|
|
1666
|
+
setInternalAgentView((prev) => (
|
|
1667
|
+
applyInternalAgentTermWrite(prev, view.agentId, text, meta)
|
|
1668
|
+
));
|
|
1669
|
+
},
|
|
1670
|
+
consumePendingDelivery: (...args) => streamState.consumePendingDelivery(...args),
|
|
1671
|
+
getPendingState: (...args) => streamState.getPendingState(...args),
|
|
1672
|
+
beginStream: (...args) => streamState.beginStream(...args),
|
|
1673
|
+
appendStreamDelta: (...args) => streamState.appendStreamDelta(...args),
|
|
1674
|
+
finalizeStream: (...args) => streamState.finalizeStream(...args),
|
|
1675
|
+
hasStream: (...args) => streamState.hasStream(...args),
|
|
1676
|
+
setTransientAgentState: (agentId, value, options = {}) => {
|
|
1677
|
+
if (!agentId || !value) return;
|
|
1678
|
+
dispatch({
|
|
1679
|
+
type: "agents/patchMeta",
|
|
1680
|
+
agentId,
|
|
1681
|
+
patch: {
|
|
1682
|
+
activity_state: value,
|
|
1683
|
+
activity_detail: options.detail || "",
|
|
1684
|
+
},
|
|
1685
|
+
});
|
|
1686
|
+
},
|
|
1687
|
+
clearTransientAgentState: (agentId) => {
|
|
1688
|
+
if (!agentId) return;
|
|
1689
|
+
dispatch({
|
|
1690
|
+
type: "agents/patchMeta",
|
|
1691
|
+
agentId,
|
|
1692
|
+
patch: {
|
|
1693
|
+
activity_state: "",
|
|
1694
|
+
activity_detail: "",
|
|
1695
|
+
},
|
|
1696
|
+
});
|
|
1697
|
+
},
|
|
1698
|
+
refreshDashboard: () => {},
|
|
1699
|
+
});
|
|
1700
|
+
setHandler((msg) => {
|
|
1701
|
+
if (!msg || typeof msg !== "object") return;
|
|
1702
|
+
if (msg.type === IPC_RESPONSE_TYPES.ERROR && handleInternalErrorMessage(msg.error || "unknown error")) {
|
|
1703
|
+
return;
|
|
1704
|
+
}
|
|
1705
|
+
if (msg.type === IPC_RESPONSE_TYPES.BUS_SEND_OK) {
|
|
1706
|
+
if (handleInternalSendOk()) return;
|
|
1707
|
+
const text = `✓ Message delivered`;
|
|
1708
|
+
logInkMessage("system", text);
|
|
1709
|
+
dispatch({ type: "status/idle" });
|
|
1710
|
+
requestDaemonStatus();
|
|
1711
|
+
return;
|
|
1712
|
+
}
|
|
1713
|
+
if (msg.type === IPC_RESPONSE_TYPES.BUS) {
|
|
1714
|
+
writeMultiWindowInternalEvent(msg.data || {});
|
|
1715
|
+
}
|
|
1716
|
+
router.handleMessage(msg);
|
|
1717
|
+
});
|
|
1718
|
+
conn.connect();
|
|
1719
|
+
return () => {
|
|
1720
|
+
try { if (typeof conn.close === "function") conn.close(); } catch { /* ignore */ }
|
|
1721
|
+
};
|
|
1722
|
+
}, [interactive, logInkMessage, requestDaemonStatus, setStatusText, updateDashboardFromStatus, writeMultiWindowInternalEvent]);
|
|
1723
|
+
|
|
1724
|
+
// commandExecutor wiring. The blessed implementation reuses this
|
|
1725
|
+
// module to dispatch every slash command (~30 callbacks). We adapt
|
|
1726
|
+
// the callback surface to ink: log/status/render writes go through
|
|
1727
|
+
// dispatch, daemon ops go through props.daemonConnection, and
|
|
1728
|
+
// blessed-tag markup the executor sprinkles into log lines is
|
|
1729
|
+
// stripped before rendering.
|
|
1730
|
+
const commandExecutorRef = useRef(null);
|
|
1731
|
+
useEffect(() => {
|
|
1732
|
+
if (!interactive) return undefined;
|
|
1733
|
+
const { createCommandExecutor } = require("../../app/chat/commandExecutor");
|
|
1734
|
+
const { parseCommand: parseCmd } = require("../../app/chat/commands");
|
|
1735
|
+
const { startDaemon: transportStartDaemon, stopDaemon: transportStopDaemon } = require("../../app/chat/transport");
|
|
1736
|
+
const AgentActivator = require("../../coordination/bus/activate");
|
|
1737
|
+
const conn = props.daemonConnection;
|
|
1738
|
+
|
|
1739
|
+
try {
|
|
1740
|
+
commandExecutorRef.current = createCommandExecutor({
|
|
1741
|
+
projectRoot: props.projectRoot,
|
|
1742
|
+
getActiveProjectRoot: () => currentProjectRootRef.current || props.projectRoot,
|
|
1743
|
+
parseCommand: parseCmd,
|
|
1744
|
+
escapeBlessed: (v) => String(v == null ? "" : v),
|
|
1745
|
+
logMessage: logInkMessage,
|
|
1746
|
+
resolveStatusLine: (text) => setStatusText(text),
|
|
1747
|
+
renderScreen: () => {},
|
|
1748
|
+
clearLog: () => {
|
|
1749
|
+
// Clear the persisted chat history file so reopening the chat
|
|
1750
|
+
// doesn't reload old messages.
|
|
1751
|
+
try {
|
|
1752
|
+
const root = currentProjectRootRef.current || props.projectRoot;
|
|
1753
|
+
const historyOptions = chatHistoryOptionsForScope({
|
|
1754
|
+
globalMode: props.globalMode,
|
|
1755
|
+
globalScope: (stateRef.current && stateRef.current.globalScope) || "controller",
|
|
1756
|
+
});
|
|
1757
|
+
const file = chatHistoryFilePath(root, historyOptions);
|
|
1758
|
+
if (file && fs.existsSync(file)) fs.writeFileSync(file, "");
|
|
1759
|
+
} catch { /* ignore */ }
|
|
1760
|
+
// ink redraws by erasing only as many lines as the last frame
|
|
1761
|
+
// emitted. After log/clear the next frame is shorter, so the
|
|
1762
|
+
// older log lines remain in the terminal scrollback. Wipe the
|
|
1763
|
+
// visible screen + scrollback first, then dispatch — ink will
|
|
1764
|
+
// repaint the (now small) frame onto a clean buffer.
|
|
1765
|
+
try {
|
|
1766
|
+
const out = (typeof process !== "undefined" && process.stdout) || null;
|
|
1767
|
+
if (out && out.isTTY && typeof out.write === "function") {
|
|
1768
|
+
out.write("\x1b[2J\x1b[3J\x1b[H");
|
|
1769
|
+
}
|
|
1770
|
+
} catch { /* ignore */ }
|
|
1771
|
+
dispatch({ type: "log/clear" });
|
|
1772
|
+
},
|
|
1773
|
+
getActiveAgents: () => (stateRef.current && stateRef.current.agents) || [],
|
|
1774
|
+
getActiveAgentMetaMap: () => (stateRef.current && stateRef.current.activeAgentMeta) || new Map(),
|
|
1775
|
+
getAgentLabel: (id) => {
|
|
1776
|
+
const metaMap = (stateRef.current && stateRef.current.activeAgentMeta) || new Map();
|
|
1777
|
+
return getAgentLabelFor(metaMap.get(id), id);
|
|
1778
|
+
},
|
|
1779
|
+
isDaemonRunning: (root) => props.env && props.env.isRunning ? props.env.isRunning(root || props.projectRoot) : true,
|
|
1780
|
+
startDaemon: (root, options = {}) => {
|
|
1781
|
+
const targetRoot = root || props.projectRoot;
|
|
1782
|
+
if (props.env && typeof props.env.startDaemon === "function") return props.env.startDaemon(targetRoot, options);
|
|
1783
|
+
return transportStartDaemon(targetRoot, options);
|
|
1784
|
+
},
|
|
1785
|
+
stopDaemon: (root, options = {}) => transportStopDaemon(root || props.projectRoot, options),
|
|
1786
|
+
restartDaemon: async (root) => {
|
|
1787
|
+
const targetRoot = root || currentProjectRootRef.current || props.projectRoot;
|
|
1788
|
+
if (
|
|
1789
|
+
targetRoot === (currentProjectRootRef.current || props.projectRoot) &&
|
|
1790
|
+
props.daemonCoordinator &&
|
|
1791
|
+
typeof props.daemonCoordinator.restart === "function"
|
|
1792
|
+
) {
|
|
1793
|
+
await props.daemonCoordinator.restart();
|
|
1794
|
+
return;
|
|
1795
|
+
}
|
|
1796
|
+
try { if (conn && typeof conn.close === "function") conn.close(); } catch { /* ignore */ }
|
|
1797
|
+
transportStopDaemon(targetRoot, { source: "ink-command:/daemon restart" });
|
|
1798
|
+
transportStartDaemon(targetRoot);
|
|
1799
|
+
if (targetRoot === (currentProjectRootRef.current || props.projectRoot) && conn && typeof conn.connect === "function") {
|
|
1800
|
+
await conn.connect();
|
|
1801
|
+
}
|
|
1802
|
+
},
|
|
1803
|
+
send: (req) => { try { if (conn && typeof conn.send === "function") conn.send(req); } catch { /* ignore */ } },
|
|
1804
|
+
requestStatus: requestDaemonStatus,
|
|
1805
|
+
requestCron: (payload = {}) => {
|
|
1806
|
+
try {
|
|
1807
|
+
const { IPC_REQUEST_TYPES } = require("../../runtime/contracts/eventContract");
|
|
1808
|
+
if (conn && typeof conn.send === "function") {
|
|
1809
|
+
conn.send({ type: IPC_REQUEST_TYPES.CRON, ...payload });
|
|
1810
|
+
}
|
|
1811
|
+
} catch { /* ignore */ }
|
|
1812
|
+
},
|
|
1813
|
+
activateAgent: async (target) => {
|
|
1814
|
+
const activator = new AgentActivator(currentProjectRootRef.current || props.projectRoot);
|
|
1815
|
+
await activator.activate(target);
|
|
1816
|
+
},
|
|
1817
|
+
globalMode: Boolean(props.globalMode),
|
|
1818
|
+
listProjects: () => (stateRef.current && stateRef.current.projects) || [],
|
|
1819
|
+
getCurrentProject: () => ({ project_root: currentProjectRootRef.current || props.projectRoot }),
|
|
1820
|
+
switchProject: async (target) => {
|
|
1821
|
+
const rawTarget = String((target && (target.projectRoot || target.project_root || target.target)) || target || "").trim();
|
|
1822
|
+
let targetRoot = rawTarget;
|
|
1823
|
+
if (/^\d+$/.test(rawTarget)) {
|
|
1824
|
+
const idx = Number.parseInt(rawTarget, 10) - 1;
|
|
1825
|
+
const projects = (stateRef.current && stateRef.current.projects) || [];
|
|
1826
|
+
targetRoot = resolveProjectRowRoot(projects[idx]);
|
|
1827
|
+
}
|
|
1828
|
+
const switchProject = switchToProjectRootRef.current;
|
|
1829
|
+
if (typeof switchProject !== "function") {
|
|
1830
|
+
return { ok: false, error: "project switching unavailable" };
|
|
1831
|
+
}
|
|
1832
|
+
return switchProject(targetRoot, { focusInput: true });
|
|
1833
|
+
},
|
|
1834
|
+
toggleMultiWindow,
|
|
1835
|
+
});
|
|
1836
|
+
} catch (err) {
|
|
1837
|
+
dispatch({ type: "log/append", text: `Error: command executor unavailable (${err && err.message ? err.message : err})` });
|
|
1838
|
+
}
|
|
1839
|
+
return undefined;
|
|
1840
|
+
}, [interactive, logInkMessage, requestDaemonStatus, setStatusText, toggleMultiWindow]);
|
|
1841
|
+
|
|
1842
|
+
// Periodic STATUS poll to keep the agents footer fresh, mirroring
|
|
1843
|
+
// blessed's requestStatus on a timer.
|
|
1844
|
+
useEffect(() => {
|
|
1845
|
+
if (!interactive) return undefined;
|
|
1846
|
+
const conn = props.daemonConnection;
|
|
1847
|
+
if (!conn || typeof conn.send !== "function") return undefined;
|
|
1848
|
+
const { IPC_REQUEST_TYPES } = require("../../runtime/contracts/eventContract");
|
|
1849
|
+
const tick = () => {
|
|
1850
|
+
try { conn.send({ type: IPC_REQUEST_TYPES.STATUS }); } catch { /* ignore */ }
|
|
1851
|
+
};
|
|
1852
|
+
tick();
|
|
1853
|
+
const timer = setInterval(tick, 3000);
|
|
1854
|
+
return () => clearInterval(timer);
|
|
1855
|
+
}, [interactive]);
|
|
1856
|
+
|
|
1857
|
+
// Refresh the project rail in global mode. blessed pulls this off the
|
|
1858
|
+
// local registry; we do the same so the dashboard's first row tracks
|
|
1859
|
+
// every running project without needing a daemon round-trip.
|
|
1860
|
+
const refreshGlobalProjects = useCallback((activeRoot = currentProjectRoot) => {
|
|
1861
|
+
if (!props.globalMode) return [];
|
|
1862
|
+
const list = loadGlobalProjectRows(activeRoot);
|
|
1863
|
+
dispatch({
|
|
1864
|
+
type: "projects/set",
|
|
1865
|
+
list,
|
|
1866
|
+
activeProjectRoot: activeRoot,
|
|
1867
|
+
});
|
|
1868
|
+
return list;
|
|
1869
|
+
}, [props.globalMode, currentProjectRoot]);
|
|
1870
|
+
|
|
1871
|
+
useEffect(() => {
|
|
1872
|
+
if (!interactive || !props.globalMode) return undefined;
|
|
1873
|
+
const refresh = () => {
|
|
1874
|
+
try { refreshGlobalProjects(currentProjectRoot); } catch { /* ignore */ }
|
|
1875
|
+
};
|
|
1876
|
+
refresh();
|
|
1877
|
+
const timer = setInterval(refresh, 4000);
|
|
1878
|
+
return () => clearInterval(timer);
|
|
1879
|
+
}, [interactive, props.globalMode, currentProjectRoot, refreshGlobalProjects]);
|
|
1880
|
+
|
|
1881
|
+
useEffect(() => {
|
|
1882
|
+
const internalStatus = state.viewingAgentId ? internalStatusLabel(internalAgentView.status) : "ready";
|
|
1883
|
+
const internalActive = internalStatus !== "ready";
|
|
1884
|
+
const statusAnimated = state.status.message && isAnimatedStatusType(state.status.type);
|
|
1885
|
+
if ((!statusAnimated) && !internalActive) return undefined;
|
|
1886
|
+
const timer = setInterval(() => setSpinnerTick((t) => t + 1), 100);
|
|
1887
|
+
return () => clearInterval(timer);
|
|
1888
|
+
}, [state.status.message, state.status.type, state.viewingAgentId, internalAgentView.status]);
|
|
1889
|
+
|
|
1890
|
+
const selectedProject = state.selectedProjectIndex >= 0 ? state.projects[state.selectedProjectIndex] : null;
|
|
1891
|
+
const selectedProjectRoot = state.selectedProjectRoot || resolveProjectRowRoot(selectedProject);
|
|
1892
|
+
const currentProject = state.projects.find((row) => resolveProjectRowRoot(row) === currentProjectRoot) || null;
|
|
1893
|
+
const currentProjectLabel = currentProject
|
|
1894
|
+
? String(currentProject.label || currentProject.project_name || path.basename(currentProjectRoot) || currentProjectRoot)
|
|
1895
|
+
: "";
|
|
1896
|
+
const inCommittedProjectScope = Boolean(props.globalMode && state.globalScope === "project" && currentProjectRoot);
|
|
1897
|
+
const displayAgents = state.agents;
|
|
1898
|
+
const displayAgentMeta = state.activeAgentMeta;
|
|
1899
|
+
const targetAgentId = state.agentSelectionMode && state.selectedAgentIndex >= 0
|
|
1900
|
+
? displayAgents[state.selectedAgentIndex]
|
|
1901
|
+
: null;
|
|
1902
|
+
const targetAgentMeta = targetAgentId ? displayAgentMeta.get(targetAgentId) : null;
|
|
1903
|
+
const targetAgentLabel = targetAgentId ? getAgentLabelFor(targetAgentMeta, targetAgentId) : "";
|
|
1904
|
+
const restartDaemonBestEffort = useCallback(() => {
|
|
1905
|
+
const coordinator = props.daemonCoordinator;
|
|
1906
|
+
if (coordinator && typeof coordinator.restart === "function") {
|
|
1907
|
+
Promise.resolve(coordinator.restart()).catch((err) => {
|
|
1908
|
+
dispatch({ type: "log/append", text: `Error: ${err && err.message ? err.message : err}` });
|
|
1909
|
+
});
|
|
1910
|
+
return;
|
|
1911
|
+
}
|
|
1912
|
+
const conn = props.daemonConnection;
|
|
1913
|
+
try { if (conn && typeof conn.close === "function") conn.close(); } catch { /* ignore */ }
|
|
1914
|
+
try { if (conn && typeof conn.connect === "function") conn.connect(); } catch { /* ignore */ }
|
|
1915
|
+
}, []);
|
|
1916
|
+
|
|
1917
|
+
const persistSetting = useCallback((patch, statusText, restart = false) => {
|
|
1918
|
+
try {
|
|
1919
|
+
const { saveConfig } = require("../../config");
|
|
1920
|
+
saveConfig(props.projectRoot, patch);
|
|
1921
|
+
} catch (err) {
|
|
1922
|
+
dispatch({ type: "log/append", text: `Error: ${err && err.message ? err.message : err}` });
|
|
1923
|
+
}
|
|
1924
|
+
if (statusText) {
|
|
1925
|
+
dispatch({
|
|
1926
|
+
type: "status/set",
|
|
1927
|
+
payload: { message: statusText, type: "typing", showTimer: false, startedAt: Date.now() },
|
|
1928
|
+
});
|
|
1929
|
+
}
|
|
1930
|
+
if (restart) restartDaemonBestEffort();
|
|
1931
|
+
}, [restartDaemonBestEffort]);
|
|
1932
|
+
|
|
1933
|
+
const clearUfooAgentIdentity = useCallback(() => {
|
|
1934
|
+
try {
|
|
1935
|
+
const { getUfooPaths } = require("../../coordination/state/paths");
|
|
1936
|
+
const agentDir = getUfooPaths(props.projectRoot).agentDir;
|
|
1937
|
+
fs.rmSync(path.join(agentDir, "ufoo-agent.json"), { force: true });
|
|
1938
|
+
fs.rmSync(path.join(agentDir, "ufoo-agent.history.jsonl"), { force: true });
|
|
1939
|
+
} catch { /* ignore */ }
|
|
1940
|
+
}, []);
|
|
1941
|
+
|
|
1942
|
+
const applySelectedMode = useCallback(() => {
|
|
1943
|
+
const { normalizeLaunchMode } = require("../../config");
|
|
1944
|
+
const mode = normalizeLaunchMode(state.modeOptions[state.selectedModeIndex]);
|
|
1945
|
+
dispatch({ type: "settings/applyMode" });
|
|
1946
|
+
persistSetting({ launchMode: mode }, `Launch mode: ${mode}`, true);
|
|
1947
|
+
dispatch({ type: "focus/set", mode: "input" });
|
|
1948
|
+
}, [state.modeOptions, state.selectedModeIndex, persistSetting]);
|
|
1949
|
+
|
|
1950
|
+
const applySelectedProvider = useCallback(() => {
|
|
1951
|
+
const { normalizeAgentProvider } = require("../../config");
|
|
1952
|
+
const selected = state.providerOptions[state.selectedProviderIndex];
|
|
1953
|
+
const provider = normalizeAgentProvider(selected && selected.value);
|
|
1954
|
+
dispatch({ type: "settings/applyProvider" });
|
|
1955
|
+
clearUfooAgentIdentity();
|
|
1956
|
+
persistSetting({ agentProvider: provider }, `ufoo-agent: ${provider === "claude-cli" ? "claude" : "codex"}`, true);
|
|
1957
|
+
dispatch({ type: "focus/set", mode: "input" });
|
|
1958
|
+
}, [state.providerOptions, state.selectedProviderIndex, clearUfooAgentIdentity, persistSetting]);
|
|
1959
|
+
|
|
1960
|
+
const sendCronStop = useCallback((taskId) => {
|
|
1961
|
+
if (!taskId || !props.daemonConnection || typeof props.daemonConnection.send !== "function") return;
|
|
1962
|
+
try {
|
|
1963
|
+
const { IPC_REQUEST_TYPES } = require("../../runtime/contracts/eventContract");
|
|
1964
|
+
props.daemonConnection.send({ type: IPC_REQUEST_TYPES.CRON, operation: "stop", id: taskId });
|
|
1965
|
+
} catch (err) {
|
|
1966
|
+
dispatch({ type: "log/append", text: `Error: ${err && err.message ? err.message : err}` });
|
|
1967
|
+
}
|
|
1968
|
+
}, []);
|
|
1969
|
+
|
|
1970
|
+
const switchToProjectRoot = useCallback(async (targetRoot, options = {}) => {
|
|
1971
|
+
const root = String(targetRoot || "").trim();
|
|
1972
|
+
if (!root) return { ok: false, error: "project root unavailable" };
|
|
1973
|
+
if (props.globalMode && props.env && typeof props.env.isRunning === "function" && !props.env.isRunning(root)) {
|
|
1974
|
+
try {
|
|
1975
|
+
const { markProjectStopped } = require("../../runtime/projects");
|
|
1976
|
+
markProjectStopped(root);
|
|
1977
|
+
} catch { /* ignore */ }
|
|
1978
|
+
refreshGlobalProjects(currentProjectRoot);
|
|
1979
|
+
dispatch({ type: "projects/clearSelection" });
|
|
1980
|
+
dispatch({ type: "focus/set", mode: "input" });
|
|
1981
|
+
const label = path.basename(root) || root;
|
|
1982
|
+
const result = { ok: false, error: `project is not running: ${label}`, stopped: true };
|
|
1983
|
+
dispatch({ type: "log/append", text: `Project ${label} is not running; removed stale dashboard entry` });
|
|
1984
|
+
return result;
|
|
1985
|
+
}
|
|
1986
|
+
const focusInput = options.focusInput === true;
|
|
1987
|
+
const selected = state.projects.find((row) => resolveProjectRowRoot(row) === root) || {};
|
|
1988
|
+
dispatch({ type: "log/clear" });
|
|
1989
|
+
const banner = buildChatBannerLines({
|
|
1990
|
+
...props,
|
|
1991
|
+
activeProjectRoot: root,
|
|
1992
|
+
globalScope: "project",
|
|
1993
|
+
}, fmt.UCODE_VERSION || "");
|
|
1994
|
+
dispatch({ type: "log/appendMany", lines: banner });
|
|
1995
|
+
const persisted = loadChatHistory(root, 200, { globalMode: false });
|
|
1996
|
+
if (persisted.length > 0) {
|
|
1997
|
+
dispatch({ type: "log/append", text: "" });
|
|
1998
|
+
dispatch({ type: "log/append", text: "─── history ───" });
|
|
1999
|
+
dispatch({ type: "log/appendMany", lines: persisted });
|
|
2000
|
+
}
|
|
2001
|
+
if (props.daemonCoordinator && typeof props.daemonCoordinator.switchProject === "function") {
|
|
2002
|
+
const { socketPath } = require("../../runtime/daemon");
|
|
2003
|
+
const res = await Promise.resolve(props.daemonCoordinator.switchProject({
|
|
2004
|
+
projectRoot: root,
|
|
2005
|
+
sockPath: socketPath(root),
|
|
2006
|
+
autoStart: false,
|
|
2007
|
+
}));
|
|
2008
|
+
if (!res || res.ok !== true) {
|
|
2009
|
+
dispatch({ type: "log/append", text: `Error: ${(res && res.error) || "switch failed"}` });
|
|
2010
|
+
return res || { ok: false, error: "switch failed" };
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
setCurrentProjectRoot(root);
|
|
2014
|
+
dispatch({ type: "scope/set", scope: "project" });
|
|
2015
|
+
dispatch({
|
|
2016
|
+
type: "projects/select",
|
|
2017
|
+
index: state.projects.indexOf(selected),
|
|
2018
|
+
projectRoot: root,
|
|
2019
|
+
});
|
|
2020
|
+
refreshGlobalProjects(root);
|
|
2021
|
+
if (focusInput) dispatch({ type: "focus/set", mode: "input" });
|
|
2022
|
+
try {
|
|
2023
|
+
const { IPC_REQUEST_TYPES } = require("../../runtime/contracts/eventContract");
|
|
2024
|
+
if (props.daemonConnection && typeof props.daemonConnection.send === "function") {
|
|
2025
|
+
props.daemonConnection.send({ type: IPC_REQUEST_TYPES.STATUS });
|
|
2026
|
+
}
|
|
2027
|
+
} catch { /* ignore */ }
|
|
2028
|
+
return { ok: true, project_root: root };
|
|
2029
|
+
}, [
|
|
2030
|
+
props,
|
|
2031
|
+
props.daemonCoordinator,
|
|
2032
|
+
props.daemonConnection,
|
|
2033
|
+
props.env,
|
|
2034
|
+
state.projects,
|
|
2035
|
+
refreshGlobalProjects,
|
|
2036
|
+
currentProjectRoot,
|
|
2037
|
+
]);
|
|
2038
|
+
|
|
2039
|
+
useEffect(() => {
|
|
2040
|
+
switchToProjectRootRef.current = switchToProjectRoot;
|
|
2041
|
+
}, [switchToProjectRoot]);
|
|
2042
|
+
|
|
2043
|
+
const switchToControllerRoot = useCallback(async () => {
|
|
2044
|
+
const root = props.activeProjectRoot || props.projectRoot || "";
|
|
2045
|
+
if (!root) return { ok: false, error: "controller root unavailable" };
|
|
2046
|
+
if (props.daemonCoordinator && typeof props.daemonCoordinator.switchProject === "function") {
|
|
2047
|
+
const { socketPath } = require("../../runtime/daemon");
|
|
2048
|
+
const res = await Promise.resolve(props.daemonCoordinator.switchProject({
|
|
2049
|
+
projectRoot: root,
|
|
2050
|
+
sockPath: socketPath(root),
|
|
2051
|
+
}));
|
|
2052
|
+
if (!res || res.ok !== true) {
|
|
2053
|
+
dispatch({ type: "log/append", text: `Error: ${(res && res.error) || "switch to global failed"}` });
|
|
2054
|
+
return res || { ok: false, error: "switch to global failed" };
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
dispatch({ type: "projects/clearSelection" });
|
|
2059
|
+
dispatch({ type: "scope/set", scope: "controller" });
|
|
2060
|
+
setCurrentProjectRoot(root);
|
|
2061
|
+
refreshGlobalProjects(root);
|
|
2062
|
+
|
|
2063
|
+
dispatch({ type: "log/clear" });
|
|
2064
|
+
const banner = buildChatBannerLines({
|
|
2065
|
+
...props,
|
|
2066
|
+
activeProjectRoot: root,
|
|
2067
|
+
globalScope: "controller",
|
|
2068
|
+
}, fmt.UCODE_VERSION || "");
|
|
2069
|
+
dispatch({ type: "log/appendMany", lines: banner });
|
|
2070
|
+
const persisted = loadChatHistory(root, 200, { globalMode: true });
|
|
2071
|
+
if (persisted.length > 0) {
|
|
2072
|
+
dispatch({ type: "log/append", text: "" });
|
|
2073
|
+
dispatch({ type: "log/append", text: "─── history ───" });
|
|
2074
|
+
dispatch({ type: "log/appendMany", lines: persisted });
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
const snapshot = readProjectAgentSnapshot(root);
|
|
2078
|
+
dispatch({ type: "agents/set", list: snapshot.agents.map((id) => snapshot.metaMap.get(id) || { fullId: id }) });
|
|
2079
|
+
try {
|
|
2080
|
+
const { IPC_REQUEST_TYPES } = require("../../runtime/contracts/eventContract");
|
|
2081
|
+
if (props.daemonConnection && typeof props.daemonConnection.send === "function") {
|
|
2082
|
+
props.daemonConnection.send({ type: IPC_REQUEST_TYPES.STATUS });
|
|
2083
|
+
}
|
|
2084
|
+
} catch { /* ignore */ }
|
|
2085
|
+
return { ok: true, project_root: root };
|
|
2086
|
+
}, [
|
|
2087
|
+
props,
|
|
2088
|
+
props.daemonCoordinator,
|
|
2089
|
+
props.daemonConnection,
|
|
2090
|
+
refreshGlobalProjects,
|
|
2091
|
+
]);
|
|
2092
|
+
|
|
2093
|
+
const closeSelectedProject = useCallback(async () => {
|
|
2094
|
+
if (!props.globalMode || !Array.isArray(state.projects) || state.projects.length === 0) return;
|
|
2095
|
+
const selectedIndex = state.selectedProjectIndex >= 0 ? state.selectedProjectIndex : 0;
|
|
2096
|
+
const proj = state.projects[selectedIndex];
|
|
2097
|
+
const targetRoot = resolveProjectRowRoot(proj);
|
|
2098
|
+
const label = (proj && (proj.label || proj.project_name)) || targetRoot;
|
|
2099
|
+
if (!targetRoot) {
|
|
2100
|
+
dispatch({ type: "log/append", text: "Error: project root unavailable" });
|
|
2101
|
+
return;
|
|
2102
|
+
}
|
|
2103
|
+
|
|
2104
|
+
dispatch({ type: "log/append", text: `Closing project ${label} daemon and agents...` });
|
|
2105
|
+
let activeRoot = currentProjectRoot;
|
|
2106
|
+
try {
|
|
2107
|
+
if (targetRoot === currentProjectRoot) {
|
|
2108
|
+
const fallback = state.projects
|
|
2109
|
+
.map(resolveProjectRowRoot)
|
|
2110
|
+
.find((root) => root && root !== targetRoot);
|
|
2111
|
+
if (!fallback) {
|
|
2112
|
+
dispatch({ type: "log/append", text: "Error: Cannot close current project; switch to another project first" });
|
|
2113
|
+
return;
|
|
2114
|
+
}
|
|
2115
|
+
if (!props.daemonCoordinator || typeof props.daemonCoordinator.switchProject !== "function") {
|
|
2116
|
+
dispatch({ type: "log/append", text: "Error: project switching unavailable" });
|
|
2117
|
+
return;
|
|
2118
|
+
}
|
|
2119
|
+
const { socketPath } = require("../../runtime/daemon");
|
|
2120
|
+
const switched = await Promise.resolve(props.daemonCoordinator.switchProject({
|
|
2121
|
+
projectRoot: fallback,
|
|
2122
|
+
sockPath: socketPath(fallback),
|
|
2123
|
+
autoStart: false,
|
|
2124
|
+
}));
|
|
2125
|
+
if (!switched || switched.ok !== true) {
|
|
2126
|
+
dispatch({ type: "log/append", text: `Error: Failed to switch project before close: ${(switched && switched.error) || "switch failed"}` });
|
|
2127
|
+
return;
|
|
2128
|
+
}
|
|
2129
|
+
activeRoot = fallback;
|
|
2130
|
+
setCurrentProjectRoot(fallback);
|
|
2131
|
+
dispatch({ type: "scope/set", scope: "project" });
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
const { stopDaemon } = require("../../app/chat/transport");
|
|
2135
|
+
const { isRunning } = require("../../runtime/daemon");
|
|
2136
|
+
stopDaemon(targetRoot, { source: `ink-project-close:${targetRoot}` });
|
|
2137
|
+
refreshGlobalProjects(activeRoot);
|
|
2138
|
+
if (isRunning(targetRoot)) {
|
|
2139
|
+
dispatch({ type: "log/append", text: `Error: Project ${label} daemon is still running after stop` });
|
|
2140
|
+
return;
|
|
2141
|
+
}
|
|
2142
|
+
dispatch({ type: "log/append", text: `Closed project ${label} daemon and agents` });
|
|
2143
|
+
} catch (err) {
|
|
2144
|
+
dispatch({ type: "log/append", text: `Error: ${err && err.message ? err.message : err}` });
|
|
2145
|
+
}
|
|
2146
|
+
}, [
|
|
2147
|
+
props.globalMode,
|
|
2148
|
+
props.daemonCoordinator,
|
|
2149
|
+
state.projects,
|
|
2150
|
+
state.selectedProjectIndex,
|
|
2151
|
+
currentProjectRoot,
|
|
2152
|
+
refreshGlobalProjects,
|
|
2153
|
+
]);
|
|
2154
|
+
|
|
2155
|
+
const submit = useCallback(async (submitted) => {
|
|
2156
|
+
const value = String(submitted == null ? state.draft : submitted);
|
|
2157
|
+
const trimmed = value.trim();
|
|
2158
|
+
if (props.globalMode && state.globalScope === "project" && selectedProjectRoot && selectedProjectRoot !== currentProjectRoot) {
|
|
2159
|
+
const switched = await switchToProjectRoot(selectedProjectRoot, { focusInput: true });
|
|
2160
|
+
if (!switched || switched.ok !== true) return;
|
|
2161
|
+
}
|
|
2162
|
+
dispatch({ type: "draft/clear" });
|
|
2163
|
+
const { createInputSubmitHandler } = require("../../app/chat/inputSubmitHandler");
|
|
2164
|
+
const { parseAtTarget } = require("../../app/chat/commands");
|
|
2165
|
+
const { resolveAgentId } = require("../../app/chat/agentDirectory");
|
|
2166
|
+
const { subscriberToSafeName } = require("../../coordination/bus/utils");
|
|
2167
|
+
const { getUfooPaths } = require("../../coordination/state/paths");
|
|
2168
|
+
const { createTerminalAdapterRouter } = require("../../runtime/terminal/adapterRouter");
|
|
2169
|
+
const submitState = {};
|
|
2170
|
+
Object.defineProperties(submitState, {
|
|
2171
|
+
targetAgent: {
|
|
2172
|
+
get: () => targetAgentId || null,
|
|
2173
|
+
set: (next) => {
|
|
2174
|
+
const id = String(next || "");
|
|
2175
|
+
if (!id) {
|
|
2176
|
+
dispatch({ type: "agents/clearTarget" });
|
|
2177
|
+
return;
|
|
2178
|
+
}
|
|
2179
|
+
const idx = displayAgents.indexOf(id);
|
|
2180
|
+
if (idx >= 0) dispatch({ type: "agents/select", index: idx });
|
|
2181
|
+
},
|
|
2182
|
+
},
|
|
2183
|
+
pending: {
|
|
2184
|
+
get: () => pendingRef.current,
|
|
2185
|
+
set: (next) => { pendingRef.current = next || null; },
|
|
2186
|
+
},
|
|
2187
|
+
activeAgentMetaMap: {
|
|
2188
|
+
get: () => displayAgentMeta,
|
|
2189
|
+
},
|
|
2190
|
+
});
|
|
2191
|
+
const send = (req) => {
|
|
2192
|
+
if (!props.daemonConnection || typeof props.daemonConnection.send !== "function") {
|
|
2193
|
+
throw new Error("daemon connection unavailable");
|
|
2194
|
+
}
|
|
2195
|
+
props.daemonConnection.send(req);
|
|
2196
|
+
};
|
|
2197
|
+
const handler = createInputSubmitHandler({
|
|
2198
|
+
state: submitState,
|
|
2199
|
+
parseAtTarget,
|
|
2200
|
+
resolveAgentId: (label) => resolveAgentId({
|
|
2201
|
+
label,
|
|
2202
|
+
activeAgents: displayAgents,
|
|
2203
|
+
labelMap: buildActiveAgentLabelMap(displayAgents, displayAgentMeta),
|
|
2204
|
+
lookupNickname: (nickname) => {
|
|
2205
|
+
for (const [id, meta] of displayAgentMeta.entries()) {
|
|
2206
|
+
if (!meta) continue;
|
|
2207
|
+
if (meta.nickname === nickname || meta.scoped_nickname === nickname || meta.display_nickname === nickname) return id;
|
|
2208
|
+
}
|
|
2209
|
+
return null;
|
|
2210
|
+
},
|
|
2211
|
+
}),
|
|
2212
|
+
executeCommand: async (text) => {
|
|
2213
|
+
const exec = commandExecutorRef.current;
|
|
2214
|
+
if (!exec || typeof exec.executeCommand !== "function") {
|
|
2215
|
+
throw new Error("command executor not ready yet");
|
|
2216
|
+
}
|
|
2217
|
+
return exec.executeCommand(text);
|
|
2218
|
+
},
|
|
2219
|
+
queueStatusLine: (text) => setStatusText(text, { type: "typing", showTimer: true }),
|
|
2220
|
+
send,
|
|
2221
|
+
logMessage: logInkMessage,
|
|
2222
|
+
getAgentLabel: (id) => getAgentLabelFor(displayAgentMeta.get(id), id),
|
|
2223
|
+
escapeBlessed: (next) => String(next == null ? "" : next),
|
|
2224
|
+
markPendingDelivery: (agentId) => {
|
|
2225
|
+
const meta = displayAgentMeta.get(agentId);
|
|
2226
|
+
streamStateRef.current.markPendingDelivery(agentId, getAgentLabelFor(meta, agentId));
|
|
2227
|
+
},
|
|
2228
|
+
clearTargetAgent: () => dispatch({ type: "agents/clearTarget" }),
|
|
2229
|
+
setTargetAgent: (agentId) => {
|
|
2230
|
+
const idx = displayAgents.indexOf(agentId);
|
|
2231
|
+
if (idx >= 0) dispatch({ type: "agents/select", index: idx });
|
|
2232
|
+
},
|
|
2233
|
+
enterAgentView: (agentId, options = {}) => {
|
|
2234
|
+
const payload = buildAgentEnterPayload(agentId);
|
|
2235
|
+
if (payload && options.useBus) payload.useBus = true;
|
|
2236
|
+
if (payload && payload.useBus) {
|
|
2237
|
+
enterInternalAgentView(payload);
|
|
2238
|
+
return;
|
|
2239
|
+
}
|
|
2240
|
+
if (payload && typeof props.requestEnterAgentView === "function") {
|
|
2241
|
+
props.requestEnterAgentView(agentId, payload);
|
|
2242
|
+
exit();
|
|
2243
|
+
}
|
|
2244
|
+
},
|
|
2245
|
+
getAgentAdapter: (agentId) => {
|
|
2246
|
+
const meta = displayAgentMeta.get(agentId) || {};
|
|
2247
|
+
const launchMode = String(meta.launch_mode || meta.launchMode || state.settings.launchMode || "").trim();
|
|
2248
|
+
return createTerminalAdapterRouter().getAdapter({ launchMode, agentId, meta });
|
|
2249
|
+
},
|
|
2250
|
+
activateAgent: async (agentId) => {
|
|
2251
|
+
const AgentActivator = require("../../coordination/bus/activate");
|
|
2252
|
+
const activator = new AgentActivator(currentProjectRoot || props.projectRoot);
|
|
2253
|
+
await activator.activate(agentId);
|
|
2254
|
+
},
|
|
2255
|
+
getInjectSockPath: (agentId) => {
|
|
2256
|
+
const safeName = subscriberToSafeName(agentId);
|
|
2257
|
+
return path.join(getUfooPaths(currentProjectRoot || props.projectRoot).busQueuesDir, safeName, "inject.sock");
|
|
2258
|
+
},
|
|
2259
|
+
existsSync: fs.existsSync,
|
|
2260
|
+
commitInputHistory: (text) => {
|
|
2261
|
+
dispatch({ type: "history/push", value: text });
|
|
2262
|
+
try { appendInputHistory(props.projectRoot, text, { globalMode: props.globalMode }); } catch { /* ignore */ }
|
|
2263
|
+
},
|
|
2264
|
+
focusInput: () => dispatch({ type: "focus/set", mode: "input" }),
|
|
2265
|
+
renderScreen: () => {},
|
|
2266
|
+
getShellCwd: () => activeChatHistoryRoot,
|
|
2267
|
+
runShellCommand: async (shellCommand, options = {}) => {
|
|
2268
|
+
const { runShellCommand } = require("../../app/chat/shellCommand");
|
|
2269
|
+
return runShellCommand(shellCommand, options);
|
|
2270
|
+
},
|
|
2271
|
+
});
|
|
2272
|
+
try {
|
|
2273
|
+
await handler.handleSubmit(value);
|
|
2274
|
+
} catch (err) {
|
|
2275
|
+
dispatch({ type: "log/append", text: `Error: ${err && err.message ? err.message : "send failed"}` });
|
|
2276
|
+
dispatch({ type: "status/idle" });
|
|
2277
|
+
}
|
|
2278
|
+
}, [
|
|
2279
|
+
state.draft,
|
|
2280
|
+
targetAgentId,
|
|
2281
|
+
props.globalMode,
|
|
2282
|
+
props.projectRoot,
|
|
2283
|
+
props.daemonConnection,
|
|
2284
|
+
props.requestEnterAgentView,
|
|
2285
|
+
selectedProjectRoot,
|
|
2286
|
+
currentProjectRoot,
|
|
2287
|
+
state.globalScope,
|
|
2288
|
+
state.settings.launchMode,
|
|
2289
|
+
switchToProjectRoot,
|
|
2290
|
+
displayAgents,
|
|
2291
|
+
displayAgentMeta,
|
|
2292
|
+
activeChatHistoryRoot,
|
|
2293
|
+
logInkMessage,
|
|
2294
|
+
setStatusText,
|
|
2295
|
+
exit,
|
|
2296
|
+
]);
|
|
2297
|
+
|
|
2298
|
+
const onArrowUpAtTop = useCallback(() => {
|
|
2299
|
+
if (state.inputHistory.length > 0) {
|
|
2300
|
+
const next = Math.max(0, state.historyIndex - 1);
|
|
2301
|
+
if (next !== state.historyIndex || state.draft !== state.inputHistory[next]) {
|
|
2302
|
+
dispatch({ type: "history/setIndex", index: next });
|
|
2303
|
+
dispatch({ type: "draft/set", value: state.inputHistory[next] || "" });
|
|
2304
|
+
setCompletionSuppressedDraft(state.inputHistory[next] || "");
|
|
2305
|
+
setDraftVersion((v) => v + 1);
|
|
2306
|
+
return;
|
|
2307
|
+
}
|
|
2308
|
+
}
|
|
2309
|
+
if (state.agentSelectionMode) dispatch({ type: "agents/clearTarget" });
|
|
2310
|
+
}, [state.inputHistory, state.historyIndex, state.draft, state.agentSelectionMode]);
|
|
2311
|
+
|
|
2312
|
+
const onArrowDownAtBottom = useCallback((currentValue) => {
|
|
2313
|
+
if (state.inputHistory.length > 0) {
|
|
2314
|
+
const transition = fmt.resolveHistoryDownTransition({
|
|
2315
|
+
inputHistory: state.inputHistory,
|
|
2316
|
+
historyIndex: state.historyIndex,
|
|
2317
|
+
currentValue,
|
|
2318
|
+
});
|
|
2319
|
+
if (transition.moved) {
|
|
2320
|
+
dispatch({ type: "history/setIndex", index: transition.nextHistoryIndex });
|
|
2321
|
+
dispatch({ type: "draft/set", value: transition.nextValue });
|
|
2322
|
+
setCompletionSuppressedDraft(transition.nextValue);
|
|
2323
|
+
setDraftVersion((v) => v + 1);
|
|
2324
|
+
return;
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
// Hand focus to the dashboard. Three-tier flow:
|
|
2328
|
+
// global mode → projects → agents → mode/provider/cron
|
|
2329
|
+
// project mode → agents → mode/provider/cron
|
|
2330
|
+
if (props.globalMode) {
|
|
2331
|
+
dispatch({ type: "focus/set", mode: "dashboard" });
|
|
2332
|
+
if (state.projects.length > 0 && state.selectedProjectIndex < 0) {
|
|
2333
|
+
dispatch({ type: "view/set", view: "projects" });
|
|
2334
|
+
dispatch({ type: "projects/select", index: 0, projectRoot: resolveProjectRowRoot(state.projects[0]) });
|
|
2335
|
+
dispatch({ type: "projects/window", windowStart: 0 });
|
|
2336
|
+
} else {
|
|
2337
|
+
dispatch({ type: "view/set", view: "agents" });
|
|
2338
|
+
if (displayAgents.length > 0 && state.selectedAgentIndex < 0) {
|
|
2339
|
+
dispatch({ type: "agents/select", index: 0 });
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
return;
|
|
2343
|
+
}
|
|
2344
|
+
dispatch({ type: "focus/set", mode: "dashboard" });
|
|
2345
|
+
dispatch({ type: "view/set", view: "agents" });
|
|
2346
|
+
if (displayAgents.length > 0 && state.selectedAgentIndex < 0) {
|
|
2347
|
+
dispatch({ type: "agents/select", index: 0 });
|
|
2348
|
+
}
|
|
2349
|
+
}, [state.inputHistory, state.historyIndex, state.projects.length, state.selectedProjectIndex, displayAgents.length, state.selectedAgentIndex, props.globalMode]);
|
|
2350
|
+
|
|
2351
|
+
const onArrowSideAtEmpty = useCallback((direction) => {
|
|
2352
|
+
if (!state.agentSelectionMode || displayAgents.length === 0) return;
|
|
2353
|
+
const cur = state.selectedAgentIndex < 0 ? 0 : state.selectedAgentIndex;
|
|
2354
|
+
const next = direction === "left"
|
|
2355
|
+
? Math.max(0, cur - 1)
|
|
2356
|
+
: Math.min(displayAgents.length - 1, cur + 1);
|
|
2357
|
+
dispatch({ type: "agents/select", index: next });
|
|
2358
|
+
}, [state.agentSelectionMode, state.selectedAgentIndex, displayAgents.length]);
|
|
2359
|
+
|
|
2360
|
+
// Inline completions: shown above the input whenever the draft starts
|
|
2361
|
+
// with "/" or "@". Tab/Enter accept the highlighted entry, ↑↓ move the
|
|
2362
|
+
// selection. The list reuses the pure buildCompletions helper from
|
|
2363
|
+
// src/ui/format so jest can pin the source list without rendering ink.
|
|
2364
|
+
const { COMMAND_REGISTRY, COMMAND_TREE } = require("../../app/chat/commands");
|
|
2365
|
+
const agentLabels = displayAgents.map((id) =>
|
|
2366
|
+
getAgentLabelFor(displayAgentMeta.get(id), id)
|
|
2367
|
+
);
|
|
2368
|
+
|
|
2369
|
+
// Lazy-load the dynamic completion sources once so /group run and
|
|
2370
|
+
// /solo run get the same alias/profile suggestions blessed shows.
|
|
2371
|
+
const dynamicSourcesRef = useRef(null);
|
|
2372
|
+
if (!dynamicSourcesRef.current) {
|
|
2373
|
+
const sources = { groupTemplates: [], soloProfiles: [] };
|
|
2374
|
+
try {
|
|
2375
|
+
const { loadTemplateRegistry } = require("../../orchestration/groups/templates");
|
|
2376
|
+
const reg = typeof loadTemplateRegistry === "function" ? loadTemplateRegistry(props.projectRoot) : null;
|
|
2377
|
+
if (reg && Array.isArray(reg.templates)) {
|
|
2378
|
+
sources.groupTemplates = reg.templates.map((item) => ({
|
|
2379
|
+
alias: item.alias,
|
|
2380
|
+
cmd: item.alias,
|
|
2381
|
+
desc: item.templateDescription || "",
|
|
2382
|
+
source: item.source || "",
|
|
2383
|
+
}));
|
|
2384
|
+
}
|
|
2385
|
+
} catch { /* ignore */ }
|
|
2386
|
+
try {
|
|
2387
|
+
const { loadPromptProfileRegistry } = require("../../orchestration/groups/promptProfiles");
|
|
2388
|
+
const { buildPromptProfileCandidates } = require("../../orchestration/solo/commands");
|
|
2389
|
+
const reg = typeof loadPromptProfileRegistry === "function" ? loadPromptProfileRegistry(props.projectRoot) : null;
|
|
2390
|
+
if (reg && typeof buildPromptProfileCandidates === "function") {
|
|
2391
|
+
sources.soloProfiles = buildPromptProfileCandidates(reg) || [];
|
|
2392
|
+
}
|
|
2393
|
+
} catch { /* ignore */ }
|
|
2394
|
+
dynamicSourcesRef.current = sources;
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2397
|
+
const completions = fmt.buildCompletions({
|
|
2398
|
+
text: state.draft,
|
|
2399
|
+
agents: displayAgents,
|
|
2400
|
+
agentLabels,
|
|
2401
|
+
commands: COMMAND_REGISTRY,
|
|
2402
|
+
commandTree: COMMAND_TREE,
|
|
2403
|
+
groupTemplates: dynamicSourcesRef.current.groupTemplates,
|
|
2404
|
+
soloProfiles: dynamicSourcesRef.current.soloProfiles,
|
|
2405
|
+
limit: 20,
|
|
2406
|
+
});
|
|
2407
|
+
const [completionIndex, setCompletionIndex] = useState(0);
|
|
2408
|
+
// First visible row inside the popup. We show 8 rows at a time
|
|
2409
|
+
// (POPUP_PAGE_SIZE) and slide the window when the cursor crosses
|
|
2410
|
+
// the bottom or top, mimicking how a terminal list typically scrolls.
|
|
2411
|
+
const POPUP_PAGE_SIZE = 8;
|
|
2412
|
+
const [completionWindowStart, setCompletionWindowStart] = useState(0);
|
|
2413
|
+
// Bumped whenever the completion popup writes a new value into the
|
|
2414
|
+
// draft — MultilineInput watches this counter so it can park its
|
|
2415
|
+
// cursor at the end of the freshly accepted suggestion instead of
|
|
2416
|
+
// staying wherever the user last typed.
|
|
2417
|
+
const [draftVersion, setDraftVersion] = useState(0);
|
|
2418
|
+
// History recall should not immediately turn a recalled command such as
|
|
2419
|
+
// "/history" into an active completion popup; otherwise ↑/↓ get captured
|
|
2420
|
+
// by completion navigation and the user cannot keep walking history.
|
|
2421
|
+
const [completionSuppressedDraft, setCompletionSuppressedDraft] = useState(null);
|
|
2422
|
+
// Reset the selection cursor whenever the suggestion list shape changes.
|
|
2423
|
+
useEffect(() => {
|
|
2424
|
+
if (completions.length === 0) {
|
|
2425
|
+
if (completionIndex !== 0) setCompletionIndex(0);
|
|
2426
|
+
if (completionWindowStart !== 0) setCompletionWindowStart(0);
|
|
2427
|
+
} else if (completionIndex >= completions.length) {
|
|
2428
|
+
setCompletionIndex(completions.length - 1);
|
|
2429
|
+
setCompletionWindowStart(Math.max(0, completions.length - POPUP_PAGE_SIZE));
|
|
2430
|
+
}
|
|
2431
|
+
}, [completions.length, completionIndex, completionWindowStart]);
|
|
2432
|
+
useEffect(() => {
|
|
2433
|
+
if (multiWindowActive) setMwCursor(String(state.draft || "").length);
|
|
2434
|
+
}, [draftVersion]);
|
|
2435
|
+
const completionsOpen = completions.length > 0 && state.draft !== completionSuppressedDraft;
|
|
2436
|
+
const acceptCompletion = useCallback(() => {
|
|
2437
|
+
if (!completionsOpen) return false;
|
|
2438
|
+
const item = completions[Math.max(0, Math.min(completions.length - 1, completionIndex))];
|
|
2439
|
+
if (item) {
|
|
2440
|
+
dispatch({ type: "draft/set", value: item.replace });
|
|
2441
|
+
setCompletionSuppressedDraft(item.hasChildren ? null : item.replace);
|
|
2442
|
+
setDraftVersion((v) => v + 1);
|
|
2443
|
+
}
|
|
2444
|
+
setCompletionIndex(0);
|
|
2445
|
+
return true;
|
|
2446
|
+
}, [completionsOpen, completions, completionIndex]);
|
|
2447
|
+
|
|
2448
|
+
const buildAgentEnterPayload = (agentId) => {
|
|
2449
|
+
const agentMeta = displayAgentMeta.get(agentId);
|
|
2450
|
+
const enterRequest = resolveAgentEnterRequest({
|
|
2451
|
+
agentId,
|
|
2452
|
+
projectRoot: currentProjectRoot || props.projectRoot,
|
|
2453
|
+
activeAgentMeta: displayAgentMeta,
|
|
2454
|
+
settings: state.settings,
|
|
2455
|
+
});
|
|
2456
|
+
return {
|
|
2457
|
+
...enterRequest,
|
|
2458
|
+
agentLabel: getAgentLabelFor(agentMeta, agentId),
|
|
2459
|
+
agentAliases: [
|
|
2460
|
+
agentId,
|
|
2461
|
+
agentMeta && agentMeta.nickname,
|
|
2462
|
+
agentMeta && agentMeta.scoped_nickname,
|
|
2463
|
+
agentMeta && agentMeta.display_nickname,
|
|
2464
|
+
].filter(Boolean).map(String),
|
|
2465
|
+
};
|
|
2466
|
+
};
|
|
2467
|
+
|
|
2468
|
+
const activateExternalAgent = (agentId) => {
|
|
2469
|
+
const id = String(agentId || "").trim();
|
|
2470
|
+
if (!id) return;
|
|
2471
|
+
try {
|
|
2472
|
+
const AgentActivator = require("../../coordination/bus/activate");
|
|
2473
|
+
const activator = new AgentActivator(currentProjectRoot || props.projectRoot);
|
|
2474
|
+
void activator.activate(id);
|
|
2475
|
+
} catch (err) {
|
|
2476
|
+
logInkMessage("error", `✗ Failed to activate ${id}: ${err && err.message ? err.message : "unknown error"}`);
|
|
2477
|
+
}
|
|
2478
|
+
};
|
|
2479
|
+
|
|
2480
|
+
const enterInternalAgentView = (enterRequest = {}) => {
|
|
2481
|
+
const agentId = String(enterRequest.agentId || "").trim();
|
|
2482
|
+
if (!agentId) return;
|
|
2483
|
+
const previous = internalAgentViewRef.current;
|
|
2484
|
+
if (previous && previous.agentId && previous.agentId !== agentId) {
|
|
2485
|
+
sendInternalAgentWatch(previous.agentId, false);
|
|
2486
|
+
}
|
|
2487
|
+
const next = createInternalAgentViewState({
|
|
2488
|
+
agentId,
|
|
2489
|
+
label: enterRequest.agentLabel || agentId,
|
|
2490
|
+
aliases: enterRequest.agentAliases || [],
|
|
2491
|
+
projectRoot: enterRequest.projectRoot || currentProjectRoot || props.projectRoot,
|
|
2492
|
+
width: size.cols || 80,
|
|
2493
|
+
});
|
|
2494
|
+
setInternalAgentView(next);
|
|
2495
|
+
internalAgentViewRef.current = next;
|
|
2496
|
+
dispatch({ type: "agentView/enter", agentId });
|
|
2497
|
+
dispatch({ type: "focus/set", mode: "input" });
|
|
2498
|
+
dispatch({ type: "agents/clearTarget" });
|
|
2499
|
+
sendInternalAgentWatch(agentId, true);
|
|
2500
|
+
try {
|
|
2501
|
+
const { IPC_REQUEST_TYPES } = require("../../runtime/contracts/eventContract");
|
|
2502
|
+
if (props.daemonConnection && typeof props.daemonConnection.send === "function") {
|
|
2503
|
+
props.daemonConnection.send({ type: IPC_REQUEST_TYPES.STATUS });
|
|
2504
|
+
}
|
|
2505
|
+
} catch { /* ignore */ }
|
|
2506
|
+
};
|
|
2507
|
+
|
|
2508
|
+
const exitInternalAgentView = () => {
|
|
2509
|
+
const view = internalAgentViewRef.current;
|
|
2510
|
+
if (view && view.agentId) sendInternalAgentWatch(view.agentId, false);
|
|
2511
|
+
const empty = createInternalAgentViewState();
|
|
2512
|
+
setInternalAgentView(empty);
|
|
2513
|
+
internalAgentViewRef.current = empty;
|
|
2514
|
+
dispatch({ type: "agentView/exit" });
|
|
2515
|
+
dispatch({ type: "view/set", view: "agents" });
|
|
2516
|
+
dispatch({ type: "focus/set", mode: "input" });
|
|
2517
|
+
};
|
|
2518
|
+
|
|
2519
|
+
const submitInternalAgentInput = () => {
|
|
2520
|
+
const view = internalAgentViewRef.current;
|
|
2521
|
+
const text = String((view && view.input) || "").trim();
|
|
2522
|
+
if (!view || !view.agentId || !text) return;
|
|
2523
|
+
setInternalAgentView((prev) => ({
|
|
2524
|
+
...updateInternalViewStatus(
|
|
2525
|
+
appendInternalAgentText(prev, `${text}\n`, { prefix: "> " }),
|
|
2526
|
+
"working",
|
|
2527
|
+
"",
|
|
2528
|
+
),
|
|
2529
|
+
input: "",
|
|
2530
|
+
cursor: 0,
|
|
2531
|
+
}));
|
|
2532
|
+
sendInternalAgentMessage(view.agentId, text);
|
|
2533
|
+
};
|
|
2534
|
+
|
|
2535
|
+
const handleInternalAgentDashboardKey = (input, key = {}) => {
|
|
2536
|
+
const keyName = resolveInternalKeyName(input, key);
|
|
2537
|
+
const totalItems = 1 + displayAgents.length;
|
|
2538
|
+
const currentIndex = Math.max(
|
|
2539
|
+
0,
|
|
2540
|
+
Math.min(totalItems - 1, Number(internalAgentViewRef.current.barIndex) || 0),
|
|
2541
|
+
);
|
|
2542
|
+
if (keyName === "left") {
|
|
2543
|
+
setInternalAgentView((prev) => ({
|
|
2544
|
+
...prev,
|
|
2545
|
+
barIndex: Math.max(0, (Number(prev.barIndex) || 0) - 1),
|
|
2546
|
+
}));
|
|
2547
|
+
return true;
|
|
2548
|
+
}
|
|
2549
|
+
if (keyName === "right") {
|
|
2550
|
+
setInternalAgentView((prev) => ({
|
|
2551
|
+
...prev,
|
|
2552
|
+
barIndex: Math.min(totalItems - 1, (Number(prev.barIndex) || 0) + 1),
|
|
2553
|
+
}));
|
|
2554
|
+
return true;
|
|
2555
|
+
}
|
|
2556
|
+
if (keyName === "up") {
|
|
2557
|
+
dispatch({ type: "focus/set", mode: "input" });
|
|
2558
|
+
return true;
|
|
2559
|
+
}
|
|
2560
|
+
if (keyName === "return" || keyName === "enter") {
|
|
2561
|
+
if (currentIndex === 0) {
|
|
2562
|
+
exitInternalAgentView();
|
|
2563
|
+
return true;
|
|
2564
|
+
}
|
|
2565
|
+
const agentId = displayAgents[currentIndex - 1];
|
|
2566
|
+
if (!agentId) return true;
|
|
2567
|
+
if (agentId === state.viewingAgentId) {
|
|
2568
|
+
dispatch({ type: "focus/set", mode: "input" });
|
|
2569
|
+
return true;
|
|
2570
|
+
}
|
|
2571
|
+
const payload = buildAgentEnterPayload(agentId);
|
|
2572
|
+
const action = resolveDashboardAgentEnterAction(payload);
|
|
2573
|
+
if (action === "internal") {
|
|
2574
|
+
enterInternalAgentView(payload);
|
|
2575
|
+
return true;
|
|
2576
|
+
}
|
|
2577
|
+
if (action === "activate") {
|
|
2578
|
+
if (state.viewingAgentId) sendInternalAgentWatch(state.viewingAgentId, false);
|
|
2579
|
+
dispatch({ type: "agentView/exit" });
|
|
2580
|
+
dispatch({ type: "view/set", view: "agents" });
|
|
2581
|
+
dispatch({ type: "focus/set", mode: "input" });
|
|
2582
|
+
activateExternalAgent(agentId);
|
|
2583
|
+
return true;
|
|
2584
|
+
}
|
|
2585
|
+
if (payload && typeof props.requestEnterAgentView === "function") {
|
|
2586
|
+
if (state.viewingAgentId) sendInternalAgentWatch(state.viewingAgentId, false);
|
|
2587
|
+
props.requestEnterAgentView(agentId, payload);
|
|
2588
|
+
exit();
|
|
2589
|
+
}
|
|
2590
|
+
return true;
|
|
2591
|
+
}
|
|
2592
|
+
if (key && key.ctrl && input === "x") {
|
|
2593
|
+
if (currentIndex <= 0) return true;
|
|
2594
|
+
const agentId = displayAgents[currentIndex - 1];
|
|
2595
|
+
if (!agentId) return true;
|
|
2596
|
+
try {
|
|
2597
|
+
const { IPC_REQUEST_TYPES } = require("../../runtime/contracts/eventContract");
|
|
2598
|
+
if (props.daemonConnection && typeof props.daemonConnection.send === "function") {
|
|
2599
|
+
props.daemonConnection.send({ type: IPC_REQUEST_TYPES.CLOSE_AGENT, agent_id: agentId });
|
|
2600
|
+
}
|
|
2601
|
+
} catch { /* ignore */ }
|
|
2602
|
+
if (agentId === state.viewingAgentId) {
|
|
2603
|
+
exitInternalAgentView();
|
|
2604
|
+
} else {
|
|
2605
|
+
setInternalAgentView((prev) => ({
|
|
2606
|
+
...prev,
|
|
2607
|
+
barIndex: Math.min(Number(prev.barIndex) || 0, Math.max(0, displayAgents.length - 1)),
|
|
2608
|
+
}));
|
|
2609
|
+
}
|
|
2610
|
+
return true;
|
|
2611
|
+
}
|
|
2612
|
+
return true;
|
|
2613
|
+
};
|
|
2614
|
+
|
|
2615
|
+
const handleInternalAgentViewKey = (input, key = {}) => {
|
|
2616
|
+
if (!state.viewingAgentId) return false;
|
|
2617
|
+
const keyName = resolveInternalKeyName(input, key);
|
|
2618
|
+
|
|
2619
|
+
if (state.focusMode === "dashboard") {
|
|
2620
|
+
return handleInternalAgentDashboardKey(input, key);
|
|
2621
|
+
}
|
|
2622
|
+
|
|
2623
|
+
if (keyName === "escape") {
|
|
2624
|
+
exitInternalAgentView();
|
|
2625
|
+
return true;
|
|
2626
|
+
}
|
|
2627
|
+
if (keyName === "down") {
|
|
2628
|
+
setInternalAgentView((prev) => ({ ...prev, barIndex: 0 }));
|
|
2629
|
+
dispatch({ type: "focus/set", mode: "dashboard" });
|
|
2630
|
+
return true;
|
|
2631
|
+
}
|
|
2632
|
+
if (keyName === "return" || keyName === "enter") {
|
|
2633
|
+
submitInternalAgentInput();
|
|
2634
|
+
return true;
|
|
2635
|
+
}
|
|
2636
|
+
if (key && key.ctrl && keyName === "u") {
|
|
2637
|
+
setInternalAgentView((prev) => ({ ...prev, input: "", cursor: 0 }));
|
|
2638
|
+
return true;
|
|
2639
|
+
}
|
|
2640
|
+
if (key && key.ctrl && keyName === "a") {
|
|
2641
|
+
setInternalAgentView((prev) => ({ ...prev, cursor: 0 }));
|
|
2642
|
+
return true;
|
|
2643
|
+
}
|
|
2644
|
+
if (key && key.ctrl && keyName === "e") {
|
|
2645
|
+
setInternalAgentView((prev) => ({ ...prev, cursor: String(prev.input || "").length }));
|
|
2646
|
+
return true;
|
|
2647
|
+
}
|
|
2648
|
+
if (keyName === "left") {
|
|
2649
|
+
setInternalAgentView((prev) => ({
|
|
2650
|
+
...prev,
|
|
2651
|
+
cursor: previousInternalBoundary(prev.input, prev.cursor),
|
|
2652
|
+
}));
|
|
2653
|
+
return true;
|
|
2654
|
+
}
|
|
2655
|
+
if (keyName === "right") {
|
|
2656
|
+
setInternalAgentView((prev) => ({
|
|
2657
|
+
...prev,
|
|
2658
|
+
cursor: nextInternalBoundary(prev.input, prev.cursor),
|
|
2659
|
+
}));
|
|
2660
|
+
return true;
|
|
2661
|
+
}
|
|
2662
|
+
if (keyName === "backspace") {
|
|
2663
|
+
setInternalAgentView((prev) => {
|
|
2664
|
+
const cursor = Number.isFinite(prev.cursor) ? prev.cursor : String(prev.input || "").length;
|
|
2665
|
+
if (cursor <= 0) return prev;
|
|
2666
|
+
const previous = previousInternalBoundary(prev.input, cursor);
|
|
2667
|
+
return {
|
|
2668
|
+
...prev,
|
|
2669
|
+
input: String(prev.input || "").slice(0, previous) + String(prev.input || "").slice(cursor),
|
|
2670
|
+
cursor: previous,
|
|
2671
|
+
};
|
|
2672
|
+
});
|
|
2673
|
+
return true;
|
|
2674
|
+
}
|
|
2675
|
+
if (keyName === "delete") {
|
|
2676
|
+
setInternalAgentView((prev) => {
|
|
2677
|
+
const text = String(prev.input || "");
|
|
2678
|
+
const cursor = Number.isFinite(prev.cursor) ? prev.cursor : text.length;
|
|
2679
|
+
if (cursor >= text.length) return prev;
|
|
2680
|
+
const next = nextInternalBoundary(text, cursor);
|
|
2681
|
+
return {
|
|
2682
|
+
...prev,
|
|
2683
|
+
input: text.slice(0, cursor) + text.slice(next),
|
|
2684
|
+
cursor,
|
|
2685
|
+
};
|
|
2686
|
+
});
|
|
2687
|
+
return true;
|
|
2688
|
+
}
|
|
2689
|
+
if (input
|
|
2690
|
+
&& !(key && key.ctrl)
|
|
2691
|
+
&& !(key && key.meta)
|
|
2692
|
+
&& !/^[\x00-\x08\x0b-\x0c\x0e-\x1f\x7f]+$/.test(input)) {
|
|
2693
|
+
const clean = String(input).replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
2694
|
+
setInternalAgentView((prev) => {
|
|
2695
|
+
const text = String(prev.input || "");
|
|
2696
|
+
const cursor = Number.isFinite(prev.cursor) ? prev.cursor : text.length;
|
|
2697
|
+
return {
|
|
2698
|
+
...prev,
|
|
2699
|
+
input: text.slice(0, cursor) + clean + text.slice(cursor),
|
|
2700
|
+
cursor: cursor + clean.length,
|
|
2701
|
+
};
|
|
2702
|
+
});
|
|
2703
|
+
return true;
|
|
2704
|
+
}
|
|
2705
|
+
return true;
|
|
2706
|
+
};
|
|
2707
|
+
|
|
2708
|
+
useInput((input, key) => {
|
|
2709
|
+
if (multiWindowActive) {
|
|
2710
|
+
const controller = multiWindowControllerRef.current;
|
|
2711
|
+
const termFocused = mwTerminalFocusedRef.current;
|
|
2712
|
+
if (key.ctrl && input === "q") {
|
|
2713
|
+
if (controller && typeof controller.handleKey === "function") {
|
|
2714
|
+
controller.handleKey({ name: "q", ctrl: true, sequence: "" });
|
|
2715
|
+
}
|
|
2716
|
+
mwTerminalFocusedRef.current = false;
|
|
2717
|
+
setMwTerminalFocused(false);
|
|
2718
|
+
return;
|
|
2719
|
+
}
|
|
2720
|
+
if (key.ctrl && input === "w") {
|
|
2721
|
+
const agents = controller ? controller.getAgentIds() : [];
|
|
2722
|
+
if (agents.length === 0) return;
|
|
2723
|
+
if (!termFocused) {
|
|
2724
|
+
if (controller) controller.focusAgent(agents[0]);
|
|
2725
|
+
mwTerminalFocusedRef.current = true;
|
|
2726
|
+
setMwTerminalFocused(true);
|
|
2727
|
+
} else {
|
|
2728
|
+
const current = controller ? controller.getFocused() : null;
|
|
2729
|
+
const idx = current ? agents.indexOf(current) : -1;
|
|
2730
|
+
if (idx >= 0 && idx < agents.length - 1) {
|
|
2731
|
+
controller.focusAgent(agents[idx + 1]);
|
|
2732
|
+
} else {
|
|
2733
|
+
mwTerminalFocusedRef.current = false;
|
|
2734
|
+
setMwTerminalFocused(false);
|
|
2735
|
+
if (controller) controller.focusAgent(agents[0]);
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
2738
|
+
if (controller) controller.renderAll();
|
|
2739
|
+
return;
|
|
2740
|
+
}
|
|
2741
|
+
if (termFocused && controller && typeof controller.sendInput === "function") {
|
|
2742
|
+
const now = Date.now();
|
|
2743
|
+
const last = mwLastInputRef.current;
|
|
2744
|
+
if (input === " " && !key.ctrl && !key.meta && isCJK(last.char) && now - last.time < 150) {
|
|
2745
|
+
return;
|
|
2746
|
+
}
|
|
2747
|
+
const raw = inkKeyToRaw(input, key);
|
|
2748
|
+
if (raw) {
|
|
2749
|
+
const cleaned = raw.length > 1 && /[⺀-鿿가-豈-︰-﹏]/.test(raw)
|
|
2750
|
+
? raw.replace(/ +$/, "")
|
|
2751
|
+
: raw;
|
|
2752
|
+
if (cleaned) {
|
|
2753
|
+
controller.sendInput(cleaned);
|
|
2754
|
+
const lastChar = cleaned[cleaned.length - 1];
|
|
2755
|
+
mwLastInputRef.current = { char: lastChar, time: now };
|
|
2756
|
+
}
|
|
2757
|
+
}
|
|
2758
|
+
return;
|
|
2759
|
+
}
|
|
2760
|
+
}
|
|
2761
|
+
if (key.ctrl && input === "c") { exit(); return; }
|
|
2762
|
+
if (key.ctrl && input === "o") { dispatch({ type: "merge/expand" }); return; }
|
|
2763
|
+
if (state.viewingAgentId) {
|
|
2764
|
+
handleInternalAgentViewKey(input, key);
|
|
2765
|
+
return;
|
|
2766
|
+
}
|
|
2767
|
+
|
|
2768
|
+
// Completion popup steals arrow/Enter/Esc/Tab while it's open. The
|
|
2769
|
+
// user types to filter, picks with the cursor and accepts with Tab
|
|
2770
|
+
// or Enter; Esc dismisses by clearing the trigger character.
|
|
2771
|
+
if (completionsOpen) {
|
|
2772
|
+
if (key.upArrow) {
|
|
2773
|
+
setCompletionIndex((i) => {
|
|
2774
|
+
const next = (i - 1 + completions.length) % completions.length;
|
|
2775
|
+
setCompletionWindowStart((ws) => {
|
|
2776
|
+
if (next < ws) return next;
|
|
2777
|
+
if (next === completions.length - 1) {
|
|
2778
|
+
// wrapped to the bottom — snap window to the tail.
|
|
2779
|
+
return Math.max(0, completions.length - POPUP_PAGE_SIZE);
|
|
2780
|
+
}
|
|
2781
|
+
return ws;
|
|
2782
|
+
});
|
|
2783
|
+
return next;
|
|
2784
|
+
});
|
|
2785
|
+
return;
|
|
2786
|
+
}
|
|
2787
|
+
if (key.downArrow) {
|
|
2788
|
+
setCompletionIndex((i) => {
|
|
2789
|
+
const next = (i + 1) % completions.length;
|
|
2790
|
+
setCompletionWindowStart((ws) => {
|
|
2791
|
+
if (next === 0) return 0; // wrapped to the head
|
|
2792
|
+
if (next >= ws + POPUP_PAGE_SIZE) return next - POPUP_PAGE_SIZE + 1;
|
|
2793
|
+
return ws;
|
|
2794
|
+
});
|
|
2795
|
+
return next;
|
|
2796
|
+
});
|
|
2797
|
+
return;
|
|
2798
|
+
}
|
|
2799
|
+
if (key.return || key.tab) { acceptCompletion(); return; }
|
|
2800
|
+
if (key.escape) {
|
|
2801
|
+
setCompletionSuppressedDraft(null);
|
|
2802
|
+
dispatch({ type: "draft/clear" });
|
|
2803
|
+
return;
|
|
2804
|
+
}
|
|
2805
|
+
}
|
|
2806
|
+
|
|
2807
|
+
if (key.tab) {
|
|
2808
|
+
if (state.focusMode === "dashboard") {
|
|
2809
|
+
dispatch({ type: "focus/set", mode: "input" });
|
|
2810
|
+
return;
|
|
2811
|
+
}
|
|
2812
|
+
dispatch({ type: "focus/set", mode: "dashboard" });
|
|
2813
|
+
dispatch({ type: "view/set", view: props.globalMode ? "projects" : "agents" });
|
|
2814
|
+
if (props.globalMode && state.projects.length > 0 && state.selectedProjectIndex < 0) {
|
|
2815
|
+
dispatch({ type: "view/set", view: "projects" });
|
|
2816
|
+
dispatch({ type: "projects/select", index: 0, projectRoot: resolveProjectRowRoot(state.projects[0]) });
|
|
2817
|
+
} else if (!props.globalMode && state.agents.length > 0 && state.selectedAgentIndex < 0) {
|
|
2818
|
+
dispatch({ type: "agents/select", index: 0 });
|
|
2819
|
+
} else if (props.globalMode && state.projects.length === 0) {
|
|
2820
|
+
dispatch({ type: "view/set", view: "agents" });
|
|
2821
|
+
if (displayAgents.length > 0 && state.selectedAgentIndex < 0) {
|
|
2822
|
+
dispatch({ type: "agents/select", index: 0 });
|
|
2823
|
+
}
|
|
2824
|
+
}
|
|
2825
|
+
return;
|
|
2826
|
+
}
|
|
2827
|
+
// Dashboard focus + agents view + agent selected + Enter: hand off
|
|
2828
|
+
// to the agent view. Queue-only internal agents stay inside Ink,
|
|
2829
|
+
// matching blessed's useBus view; PTY/socket agents still hand off
|
|
2830
|
+
// to the raw mirror via the runChatInk loop.
|
|
2831
|
+
if (key.return && state.focusMode === "dashboard"
|
|
2832
|
+
&& state.dashboardView === "agents"
|
|
2833
|
+
&& state.agentSelectionMode
|
|
2834
|
+
&& state.selectedAgentIndex >= 0) {
|
|
2835
|
+
const agentId = displayAgents[state.selectedAgentIndex];
|
|
2836
|
+
if (agentId && multiWindowActive) {
|
|
2837
|
+
const controller = multiWindowControllerRef.current;
|
|
2838
|
+
if (controller && typeof controller.focusAgent === "function") {
|
|
2839
|
+
controller.focusAgent(agentId);
|
|
2840
|
+
}
|
|
2841
|
+
setMwTerminalFocused(true);
|
|
2842
|
+
mwTerminalFocusedRef.current = true;
|
|
2843
|
+
dispatch({ type: "focus/set", mode: "input" });
|
|
2844
|
+
return;
|
|
2845
|
+
}
|
|
2846
|
+
if (agentId) {
|
|
2847
|
+
const enterPayload = buildAgentEnterPayload(agentId);
|
|
2848
|
+
const action = resolveDashboardAgentEnterAction(enterPayload);
|
|
2849
|
+
if (action === "internal") {
|
|
2850
|
+
enterInternalAgentView(enterPayload);
|
|
2851
|
+
return;
|
|
2852
|
+
}
|
|
2853
|
+
if (action === "activate") {
|
|
2854
|
+
dispatch({ type: "agents/clearTarget" });
|
|
2855
|
+
dispatch({ type: "focus/set", mode: "input" });
|
|
2856
|
+
activateExternalAgent(agentId);
|
|
2857
|
+
return;
|
|
2858
|
+
}
|
|
2859
|
+
if (typeof props.requestEnterAgentView === "function") {
|
|
2860
|
+
props.requestEnterAgentView(agentId, enterPayload);
|
|
2861
|
+
exit();
|
|
2862
|
+
}
|
|
2863
|
+
}
|
|
2864
|
+
return;
|
|
2865
|
+
}
|
|
2866
|
+
// Dashboard focus + projects view: ←/→ moves the highlighted
|
|
2867
|
+
// project, Enter switches the daemon connection to that project,
|
|
2868
|
+
// Ctrl+X stops it.
|
|
2869
|
+
if (state.focusMode === "dashboard" && state.dashboardView === "projects" && state.projects.length === 0) {
|
|
2870
|
+
if (key.downArrow) {
|
|
2871
|
+
for (const action of buildEmptyProjectsDownActions(state, displayAgents)) dispatch(action);
|
|
2872
|
+
return;
|
|
2873
|
+
}
|
|
2874
|
+
if (key.upArrow || key.return || key.escape) {
|
|
2875
|
+
dispatch({ type: "focus/set", mode: "input" });
|
|
2876
|
+
}
|
|
2877
|
+
return;
|
|
2878
|
+
}
|
|
2879
|
+
if (state.focusMode === "dashboard" && state.dashboardView === "projects" && state.projects.length > 0) {
|
|
2880
|
+
if (key.leftArrow || key.rightArrow) {
|
|
2881
|
+
const cur = Number.isFinite(state.selectedProjectIndex) && state.selectedProjectIndex >= 0
|
|
2882
|
+
? state.selectedProjectIndex : 0;
|
|
2883
|
+
const next = key.leftArrow
|
|
2884
|
+
? Math.max(0, cur - 1)
|
|
2885
|
+
: Math.min(state.projects.length - 1, cur + 1);
|
|
2886
|
+
if (next === cur) return;
|
|
2887
|
+
dispatch({ type: "projects/select", index: next, projectRoot: resolveProjectRowRoot(state.projects[next]) });
|
|
2888
|
+
// Slide the visible window to keep the cursor on screen. We mirror
|
|
2889
|
+
// clampAgentWindowWithSelection's logic with maxProjectWindow=5.
|
|
2890
|
+
const max = Math.max(1, Math.min(5, state.projects.length));
|
|
2891
|
+
let nextStart = state.projectListWindowStart || 0;
|
|
2892
|
+
if (next < nextStart) nextStart = next;
|
|
2893
|
+
else if (next >= nextStart + max) nextStart = next - max + 1;
|
|
2894
|
+
if (nextStart !== state.projectListWindowStart) {
|
|
2895
|
+
dispatch({ type: "projects/window", windowStart: nextStart });
|
|
2896
|
+
}
|
|
2897
|
+
|
|
2898
|
+
const proj = state.projects[next];
|
|
2899
|
+
const target = resolveProjectRowRoot(proj);
|
|
2900
|
+
if (target && state.globalScope === "project") {
|
|
2901
|
+
void switchToProjectRoot(target);
|
|
2902
|
+
}
|
|
2903
|
+
return;
|
|
2904
|
+
}
|
|
2905
|
+
if (key.return) {
|
|
2906
|
+
const cur = state.selectedProjectIndex >= 0 ? state.selectedProjectIndex : 0;
|
|
2907
|
+
const proj = state.projects[cur];
|
|
2908
|
+
const target = resolveProjectRowRoot(proj);
|
|
2909
|
+
void switchToProjectRoot(target, { focusInput: true });
|
|
2910
|
+
return;
|
|
2911
|
+
}
|
|
2912
|
+
if (input
|
|
2913
|
+
&& !(key && key.ctrl)
|
|
2914
|
+
&& !(key && key.meta)
|
|
2915
|
+
&& !/^[\x00-\x1f\x7f]+$/.test(input)
|
|
2916
|
+
&& !input.includes("\n")
|
|
2917
|
+
&& !input.includes("\r")) {
|
|
2918
|
+
const cur = state.selectedProjectIndex >= 0 ? state.selectedProjectIndex : 0;
|
|
2919
|
+
const target = resolveProjectRowRoot(state.projects[cur]);
|
|
2920
|
+
void switchToProjectRoot(target, { focusInput: true });
|
|
2921
|
+
dispatch({ type: "draft/set", value: `${state.draft || ""}${input}` });
|
|
2922
|
+
setDraftVersion((v) => v + 1);
|
|
2923
|
+
return;
|
|
2924
|
+
}
|
|
2925
|
+
if (key.ctrl && input === "x") {
|
|
2926
|
+
void closeSelectedProject();
|
|
2927
|
+
return;
|
|
2928
|
+
}
|
|
2929
|
+
if (key.upArrow) {
|
|
2930
|
+
// Up out of projects → toggle back to input.
|
|
2931
|
+
dispatch({ type: "projects/clearSelection" });
|
|
2932
|
+
dispatch({ type: "focus/set", mode: "input" });
|
|
2933
|
+
return;
|
|
2934
|
+
}
|
|
2935
|
+
if (key.escape) {
|
|
2936
|
+
dispatch({ type: "projects/clearSelection" });
|
|
2937
|
+
if (state.globalScope === "project") {
|
|
2938
|
+
void switchToControllerRoot();
|
|
2939
|
+
return;
|
|
2940
|
+
}
|
|
2941
|
+
dispatch({ type: "focus/set", mode: "input" });
|
|
2942
|
+
return;
|
|
2943
|
+
}
|
|
2944
|
+
if (key.downArrow) {
|
|
2945
|
+
// Down from projects → agents row stays in dashboard focus.
|
|
2946
|
+
dispatch({ type: "view/set", view: "agents" });
|
|
2947
|
+
if (displayAgents.length > 0 && state.selectedAgentIndex < 0) {
|
|
2948
|
+
dispatch({ type: "agents/select", index: 0 });
|
|
2949
|
+
}
|
|
2950
|
+
return;
|
|
2951
|
+
}
|
|
2952
|
+
}
|
|
2953
|
+
|
|
2954
|
+
if (state.focusMode === "dashboard"
|
|
2955
|
+
&& state.dashboardView === "agents"
|
|
2956
|
+
&& input
|
|
2957
|
+
&& !(key && key.ctrl)
|
|
2958
|
+
&& !(key && key.meta)
|
|
2959
|
+
&& !/^[\x00-\x1f\x7f]+$/.test(input)
|
|
2960
|
+
&& !input.includes("\n")
|
|
2961
|
+
&& !input.includes("\r")) {
|
|
2962
|
+
if (displayAgents.length > 0 && state.selectedAgentIndex < 0) {
|
|
2963
|
+
dispatch({ type: "agents/select", index: 0 });
|
|
2964
|
+
}
|
|
2965
|
+
dispatch({ type: "focus/set", mode: "input" });
|
|
2966
|
+
dispatch({ type: "draft/set", value: `${state.draft || ""}${input}` });
|
|
2967
|
+
setDraftVersion((v) => v + 1);
|
|
2968
|
+
return;
|
|
2969
|
+
}
|
|
2970
|
+
|
|
2971
|
+
// Dashboard focus on agents/mode/provider/cron — ↑↓ flip between
|
|
2972
|
+
// sibling views, ←/→ pick within the active view, Esc returns to
|
|
2973
|
+
// the input.
|
|
2974
|
+
if (state.focusMode === "dashboard"
|
|
2975
|
+
&& (state.dashboardView === "agents"
|
|
2976
|
+
|| state.dashboardView === "mode"
|
|
2977
|
+
|| state.dashboardView === "provider"
|
|
2978
|
+
|| state.dashboardView === "cron")) {
|
|
2979
|
+
if (key.escape) {
|
|
2980
|
+
if (state.dashboardView === "agents") dispatch({ type: "agents/clearTarget" });
|
|
2981
|
+
dispatch({ type: "focus/set", mode: "input" });
|
|
2982
|
+
return;
|
|
2983
|
+
}
|
|
2984
|
+
if (state.dashboardView === "agents") {
|
|
2985
|
+
if (key.leftArrow || key.rightArrow) {
|
|
2986
|
+
if (displayAgents.length > 0) {
|
|
2987
|
+
const cur = state.selectedAgentIndex < 0 ? 0 : state.selectedAgentIndex;
|
|
2988
|
+
const next = key.leftArrow
|
|
2989
|
+
? Math.max(0, cur - 1)
|
|
2990
|
+
: Math.min(displayAgents.length - 1, cur + 1);
|
|
2991
|
+
dispatch({ type: "agents/select", index: next });
|
|
2992
|
+
}
|
|
2993
|
+
return;
|
|
2994
|
+
}
|
|
2995
|
+
if (key.ctrl && input === "x") {
|
|
2996
|
+
if (state.selectedAgentIndex >= 0 && state.selectedAgentIndex < displayAgents.length) {
|
|
2997
|
+
const agentId = displayAgents[state.selectedAgentIndex];
|
|
2998
|
+
try {
|
|
2999
|
+
const { IPC_REQUEST_TYPES } = require("../../runtime/contracts/eventContract");
|
|
3000
|
+
if (props.daemonConnection && typeof props.daemonConnection.send === "function") {
|
|
3001
|
+
props.daemonConnection.send({ type: IPC_REQUEST_TYPES.CLOSE_AGENT, agent_id: agentId });
|
|
3002
|
+
}
|
|
3003
|
+
} catch (err) {
|
|
3004
|
+
dispatch({ type: "log/append", text: `Error: ${err && err.message ? err.message : err}` });
|
|
3005
|
+
}
|
|
3006
|
+
dispatch({ type: "agents/clearTarget" });
|
|
3007
|
+
dispatch({ type: "focus/set", mode: "input" });
|
|
3008
|
+
}
|
|
3009
|
+
return;
|
|
3010
|
+
}
|
|
3011
|
+
if (key.return) {
|
|
3012
|
+
dispatch({ type: "focus/set", mode: "input" });
|
|
3013
|
+
return;
|
|
3014
|
+
}
|
|
3015
|
+
if (key.downArrow) {
|
|
3016
|
+
dispatch({ type: "view/set", view: "mode" });
|
|
3017
|
+
const launchModeIndex = state.modeOptions.indexOf(state.settings.launchMode);
|
|
3018
|
+
dispatch({ type: "modeIndex/set", index: launchModeIndex >= 0 ? launchModeIndex : 0 });
|
|
3019
|
+
return;
|
|
3020
|
+
}
|
|
3021
|
+
if (key.upArrow) {
|
|
3022
|
+
// Top of the agents tier: in global mode go back to projects,
|
|
3023
|
+
// otherwise leave dashboard focus altogether.
|
|
3024
|
+
dispatch({ type: "agents/clearTarget" });
|
|
3025
|
+
if (props.globalMode) dispatch({ type: "view/set", view: "projects" });
|
|
3026
|
+
else dispatch({ type: "focus/set", mode: "input" });
|
|
3027
|
+
return;
|
|
3028
|
+
}
|
|
3029
|
+
}
|
|
3030
|
+
if (state.dashboardView === "mode") {
|
|
3031
|
+
if (key.leftArrow || key.rightArrow) {
|
|
3032
|
+
const len = state.modeOptions.length;
|
|
3033
|
+
if (len > 0) {
|
|
3034
|
+
const cur = state.selectedModeIndex;
|
|
3035
|
+
const next = key.leftArrow
|
|
3036
|
+
? Math.max(0, cur - 1)
|
|
3037
|
+
: Math.min(len - 1, cur + 1);
|
|
3038
|
+
if (next !== cur) dispatch({ type: "modeIndex/set", index: next });
|
|
3039
|
+
}
|
|
3040
|
+
return;
|
|
3041
|
+
}
|
|
3042
|
+
if (key.downArrow) {
|
|
3043
|
+
dispatch({ type: "view/set", view: "provider" });
|
|
3044
|
+
const providerIndex = state.providerOptions.findIndex((opt) => opt.value === state.settings.agentProvider);
|
|
3045
|
+
dispatch({ type: "providerIndex/set", index: providerIndex >= 0 ? providerIndex : 0 });
|
|
3046
|
+
return;
|
|
3047
|
+
}
|
|
3048
|
+
if (key.upArrow) { dispatch({ type: "view/set", view: "agents" }); return; }
|
|
3049
|
+
if (key.return) { applySelectedMode(); return; }
|
|
3050
|
+
}
|
|
3051
|
+
if (state.dashboardView === "provider") {
|
|
3052
|
+
if (key.leftArrow || key.rightArrow) {
|
|
3053
|
+
const len = state.providerOptions.length;
|
|
3054
|
+
if (len > 0) {
|
|
3055
|
+
const cur = state.selectedProviderIndex;
|
|
3056
|
+
const next = key.leftArrow
|
|
3057
|
+
? Math.max(0, cur - 1)
|
|
3058
|
+
: Math.min(len - 1, cur + 1);
|
|
3059
|
+
if (next !== cur) dispatch({ type: "providerIndex/set", index: next });
|
|
3060
|
+
}
|
|
3061
|
+
return;
|
|
3062
|
+
}
|
|
3063
|
+
if (key.downArrow) {
|
|
3064
|
+
dispatch({ type: "view/set", view: "cron" });
|
|
3065
|
+
dispatch({ type: "cronIndex/set", index: state.cronTasks.length > 0 ? 0 : -1 });
|
|
3066
|
+
return;
|
|
3067
|
+
}
|
|
3068
|
+
if (key.upArrow) { dispatch({ type: "view/set", view: "mode" }); return; }
|
|
3069
|
+
if (key.return) { applySelectedProvider(); return; }
|
|
3070
|
+
}
|
|
3071
|
+
if (state.dashboardView === "cron") {
|
|
3072
|
+
if (key.leftArrow || key.rightArrow) {
|
|
3073
|
+
const len = state.cronTasks.length;
|
|
3074
|
+
if (len > 0) {
|
|
3075
|
+
const cur = state.selectedCronIndex < 0 ? 0 : state.selectedCronIndex;
|
|
3076
|
+
const next = key.leftArrow ? Math.max(0, cur - 1) : Math.min(len - 1, cur + 1);
|
|
3077
|
+
if (next !== cur) dispatch({ type: "cronIndex/set", index: next });
|
|
3078
|
+
}
|
|
3079
|
+
return;
|
|
3080
|
+
}
|
|
3081
|
+
if (key.downArrow) {
|
|
3082
|
+
// Cron is the last tier — don't wrap back to agents.
|
|
3083
|
+
return;
|
|
3084
|
+
}
|
|
3085
|
+
if (key.upArrow) { dispatch({ type: "view/set", view: "provider" }); return; }
|
|
3086
|
+
if (key.ctrl && input === "x") {
|
|
3087
|
+
const maxIndex = state.cronTasks.length - 1;
|
|
3088
|
+
if (maxIndex >= 0 && state.selectedCronIndex >= 0 && state.selectedCronIndex <= maxIndex) {
|
|
3089
|
+
const task = state.cronTasks[state.selectedCronIndex];
|
|
3090
|
+
const id = task && task.id ? String(task.id).trim() : "";
|
|
3091
|
+
if (id) {
|
|
3092
|
+
sendCronStop(id);
|
|
3093
|
+
return;
|
|
3094
|
+
}
|
|
3095
|
+
}
|
|
3096
|
+
dispatch({ type: "focus/set", mode: "input" });
|
|
3097
|
+
return;
|
|
3098
|
+
}
|
|
3099
|
+
if (key.return) { dispatch({ type: "focus/set", mode: "input" }); return; }
|
|
3100
|
+
}
|
|
3101
|
+
}
|
|
3102
|
+
|
|
3103
|
+
// Multi-window typing handler: replicates MultilineInput's key handling
|
|
3104
|
+
// so both modes share the same input behavior.
|
|
3105
|
+
if (multiWindowActive && state.focusMode !== "dashboard") {
|
|
3106
|
+
const intercepted = completionsOpen && (key.upArrow || key.downArrow || key.leftArrow || key.rightArrow || key.return);
|
|
3107
|
+
if (intercepted) return;
|
|
3108
|
+
if (key.return) {
|
|
3109
|
+
if (key.meta) {
|
|
3110
|
+
const before = (state.draft || "").slice(0, mwCursor);
|
|
3111
|
+
const after = (state.draft || "").slice(mwCursor);
|
|
3112
|
+
dispatch({ type: "draft/set", value: `${before}\n${after}` });
|
|
3113
|
+
setMwCursor(mwCursor + 1);
|
|
3114
|
+
return;
|
|
3115
|
+
}
|
|
3116
|
+
const value = String(state.draft || "").trim();
|
|
3117
|
+
if (value) { submit(value); setMwCursor(0); }
|
|
3118
|
+
return;
|
|
3119
|
+
}
|
|
3120
|
+
if (key.escape) {
|
|
3121
|
+
if (state.agentSelectionMode) { dispatch({ type: "agents/clearTarget" }); return; }
|
|
3122
|
+
if (state.draft) { dispatch({ type: "draft/clear" }); setMwCursor(0); }
|
|
3123
|
+
else if (state.status && state.status.message) { dispatch({ type: "status/idle" }); }
|
|
3124
|
+
return;
|
|
3125
|
+
}
|
|
3126
|
+
if (key.ctrl) {
|
|
3127
|
+
if (input === "a") { setMwCursor(fmt.moveCursorToVisualLineBoundary({ cursorPos: mwCursor, inputValue: state.draft || "", width: inputWidth, boundary: "start" })); return; }
|
|
3128
|
+
if (input === "e") { setMwCursor(fmt.moveCursorToVisualLineBoundary({ cursorPos: mwCursor, inputValue: state.draft || "", width: inputWidth, boundary: "end" })); return; }
|
|
3129
|
+
if (input === "b") { setMwCursor(fmt.moveCursorHorizontally(mwCursor, state.draft || "", "left")); return; }
|
|
3130
|
+
if (input === "f") { setMwCursor(fmt.moveCursorHorizontally(mwCursor, state.draft || "", "right")); return; }
|
|
3131
|
+
if (input === "d") { const d = state.draft || ""; if (mwCursor < d.length) { dispatch({ type: "draft/set", value: d.slice(0, mwCursor) + d.slice(mwCursor + 1) }); } return; }
|
|
3132
|
+
if (input === "h") { const d = state.draft || ""; if (mwCursor > 0) { dispatch({ type: "draft/set", value: d.slice(0, mwCursor - 1) + d.slice(mwCursor) }); setMwCursor(mwCursor - 1); } return; }
|
|
3133
|
+
if (input === "k") { dispatch({ type: "draft/set", value: (state.draft || "").slice(0, mwCursor) }); return; }
|
|
3134
|
+
if (input === "u") { dispatch({ type: "draft/set", value: (state.draft || "").slice(mwCursor) }); setMwCursor(0); return; }
|
|
3135
|
+
if (input === "w") { const r = fmt.deleteWordBeforeCursor(state.draft || "", mwCursor); dispatch({ type: "draft/set", value: r.value }); setMwCursor(r.cursorPos); return; }
|
|
3136
|
+
return;
|
|
3137
|
+
}
|
|
3138
|
+
if (key.meta) {
|
|
3139
|
+
if (input === "b") { setMwCursor(fmt.moveCursorByWord(state.draft || "", mwCursor, "backward")); return; }
|
|
3140
|
+
if (input === "f") { setMwCursor(fmt.moveCursorByWord(state.draft || "", mwCursor, "forward")); return; }
|
|
3141
|
+
if (input === "d") { const end = fmt.moveCursorByWord(state.draft || "", mwCursor, "forward"); const d = state.draft || ""; dispatch({ type: "draft/set", value: d.slice(0, mwCursor) + d.slice(end) }); return; }
|
|
3142
|
+
}
|
|
3143
|
+
if (key.backspace || key.delete) {
|
|
3144
|
+
const d = state.draft || "";
|
|
3145
|
+
if (key.meta || key.ctrl) { const r = fmt.deleteWordBeforeCursor(d, mwCursor); dispatch({ type: "draft/set", value: r.value }); setMwCursor(r.cursorPos); }
|
|
3146
|
+
else if (mwCursor > 0) { dispatch({ type: "draft/set", value: d.slice(0, mwCursor - 1) + d.slice(mwCursor) }); setMwCursor(mwCursor - 1); }
|
|
3147
|
+
return;
|
|
3148
|
+
}
|
|
3149
|
+
if (key.leftArrow) {
|
|
3150
|
+
if (!state.draft && typeof onArrowSideAtEmpty === "function") { onArrowSideAtEmpty("left"); return; }
|
|
3151
|
+
setMwCursor(fmt.moveCursorHorizontally(mwCursor, state.draft || "", "left"));
|
|
3152
|
+
return;
|
|
3153
|
+
}
|
|
3154
|
+
if (key.rightArrow) {
|
|
3155
|
+
if (!state.draft && typeof onArrowSideAtEmpty === "function") { onArrowSideAtEmpty("right"); return; }
|
|
3156
|
+
setMwCursor(fmt.moveCursorHorizontally(mwCursor, state.draft || "", "right"));
|
|
3157
|
+
return;
|
|
3158
|
+
}
|
|
3159
|
+
if (key.upArrow) { onArrowUpAtTop(); return; }
|
|
3160
|
+
if (key.downArrow) { onArrowDownAtBottom(state.draft); return; }
|
|
3161
|
+
if (input && !key.ctrl && !key.meta) {
|
|
3162
|
+
const filtered = input.replace(/[\x00-\x08\x0b-\x0c\x0e-\x1f\x7f]/g, "");
|
|
3163
|
+
if (filtered) {
|
|
3164
|
+
const d = state.draft || "";
|
|
3165
|
+
dispatch({ type: "draft/set", value: d.slice(0, mwCursor) + filtered + d.slice(mwCursor) });
|
|
3166
|
+
setMwCursor(mwCursor + filtered.length);
|
|
3167
|
+
}
|
|
3168
|
+
return;
|
|
3169
|
+
}
|
|
3170
|
+
return;
|
|
3171
|
+
}
|
|
3172
|
+
}, { isActive: interactive });
|
|
3173
|
+
|
|
3174
|
+
const statusText = computeStatusText(state.status, spinnerTick);
|
|
3175
|
+
const inputWidth = Math.max(20, (size.cols || 80) - 4);
|
|
3176
|
+
const promptPrefix = (() => {
|
|
3177
|
+
const projectPrefix = inCommittedProjectScope && currentProjectLabel ? `${currentProjectLabel} ` : "";
|
|
3178
|
+
const visibleTargetAgentLabel = state.focusMode === "dashboard" && state.dashboardView !== "agents"
|
|
3179
|
+
? ""
|
|
3180
|
+
: targetAgentLabel;
|
|
3181
|
+
if (visibleTargetAgentLabel) return `${projectPrefix}›@${visibleTargetAgentLabel} `;
|
|
3182
|
+
return `${projectPrefix}› `;
|
|
3183
|
+
})();
|
|
3184
|
+
|
|
3185
|
+
if (multiWindowActive) {
|
|
3186
|
+
const { renderDashboardLines } = require("./DashboardBar");
|
|
3187
|
+
const clampedCursor = fmt.clampCursorPos(mwCursor, state.draft || "");
|
|
3188
|
+
multiWindowChromeRef.current = {
|
|
3189
|
+
statusText,
|
|
3190
|
+
promptPrefix,
|
|
3191
|
+
draft: state.draft || "",
|
|
3192
|
+
cursor: clampedCursor,
|
|
3193
|
+
completions: completionsOpen ? completions : [],
|
|
3194
|
+
completionIndex: completionsOpen ? completionIndex : -1,
|
|
3195
|
+
completionWindowStart: completionsOpen ? completionWindowStart : 0,
|
|
3196
|
+
completionPageSize: POPUP_PAGE_SIZE,
|
|
3197
|
+
dashboardLines: renderDashboardLines({
|
|
3198
|
+
dashboardView: state.dashboardView,
|
|
3199
|
+
focusMode: state.focusMode,
|
|
3200
|
+
globalMode: state.globalMode,
|
|
3201
|
+
globalScope: state.globalScope,
|
|
3202
|
+
activeAgents: displayAgents,
|
|
3203
|
+
activeAgentMeta: displayAgentMeta,
|
|
3204
|
+
activeAgentId: targetAgentId || "",
|
|
3205
|
+
selectedAgentIndex: state.selectedAgentIndex,
|
|
3206
|
+
agentListWindowStart: state.agentListWindowStart,
|
|
3207
|
+
projectListWindowStart: state.projectListWindowStart,
|
|
3208
|
+
maxProjectWindow: 5,
|
|
3209
|
+
maxWidth: Math.max(20, size.cols || 80),
|
|
3210
|
+
getAgentLabel: (id) => getAgentLabelFor(displayAgentMeta.get(id), id),
|
|
3211
|
+
getAgentState: (id) => {
|
|
3212
|
+
const meta = displayAgentMeta.get(id);
|
|
3213
|
+
return meta && typeof meta.activity_state === "string" ? meta.activity_state : "";
|
|
3214
|
+
},
|
|
3215
|
+
launchMode: state.settings.launchMode,
|
|
3216
|
+
agentProvider: state.settings.agentProvider,
|
|
3217
|
+
modeOptions: state.modeOptions,
|
|
3218
|
+
selectedModeIndex: state.selectedModeIndex,
|
|
3219
|
+
providerOptions: state.providerOptions,
|
|
3220
|
+
selectedProviderIndex: state.selectedProviderIndex,
|
|
3221
|
+
cronTasks: state.cronTasks,
|
|
3222
|
+
selectedCronIndex: state.selectedCronIndex,
|
|
3223
|
+
projects: state.projects,
|
|
3224
|
+
selectedProjectIndex: state.selectedProjectIndex,
|
|
3225
|
+
activeProjectRoot: currentProjectRoot,
|
|
3226
|
+
dashHints: buildDashHints(state, targetAgentLabel),
|
|
3227
|
+
}),
|
|
3228
|
+
};
|
|
3229
|
+
}
|
|
3230
|
+
|
|
3231
|
+
useEffect(() => {
|
|
3232
|
+
if (!multiWindowActive) return;
|
|
3233
|
+
const controller = multiWindowControllerRef.current;
|
|
3234
|
+
if (controller && typeof controller.renderAll === "function") {
|
|
3235
|
+
controller.renderAll();
|
|
3236
|
+
}
|
|
3237
|
+
}, [multiWindowActive, completionsOpen, completions.length, completionIndex, completionWindowStart]);
|
|
3238
|
+
|
|
3239
|
+
if (multiWindowActive) {
|
|
3240
|
+
return null;
|
|
3241
|
+
}
|
|
3242
|
+
|
|
3243
|
+
const renderChatLogLine = (item) => {
|
|
3244
|
+
const row = classifyChatLogLine((item && item.text) || "");
|
|
3245
|
+
const key = item && item.id ? item.id : `log-${row.body}`;
|
|
3246
|
+
if (row.kind === "spacer") {
|
|
3247
|
+
return h(Text, { key, color: "gray" }, " ");
|
|
3248
|
+
}
|
|
3249
|
+
const palette = {
|
|
3250
|
+
assistant: { marker: "cyan", speaker: "white", body: undefined, bold: true },
|
|
3251
|
+
agent: { marker: "cyan", speaker: "cyan", body: undefined, bold: false },
|
|
3252
|
+
error: { marker: "red", speaker: "red", body: "red", bold: true },
|
|
3253
|
+
success: { marker: "green", speaker: "green", body: "green", bold: false },
|
|
3254
|
+
divider: { marker: "gray", speaker: "gray", body: "gray", bold: false },
|
|
3255
|
+
banner: { marker: "cyan", speaker: "cyan", body: "cyan", bold: true },
|
|
3256
|
+
meta: { marker: "gray", speaker: "gray", body: "gray", bold: false },
|
|
3257
|
+
plain: { marker: "gray", speaker: "gray", body: undefined, bold: false },
|
|
3258
|
+
};
|
|
3259
|
+
const colors = palette[row.kind] || palette.plain;
|
|
3260
|
+
if (row.kind === "divider") {
|
|
3261
|
+
return h(Box, { key, marginBottom: 1 },
|
|
3262
|
+
h(Text, { color: colors.body, wrap: "truncate" }, row.body),
|
|
3263
|
+
);
|
|
3264
|
+
}
|
|
3265
|
+
if (row.kind === "banner") {
|
|
3266
|
+
return h(Box, { key, marginBottom: 1 },
|
|
3267
|
+
h(Text, { color: colors.body, bold: true, wrap: "truncate" }, row.body),
|
|
3268
|
+
);
|
|
3269
|
+
}
|
|
3270
|
+
return h(Box, { key, width: "100%", marginBottom: 1 },
|
|
3271
|
+
h(Box, { width: 2 },
|
|
3272
|
+
h(Text, { color: colors.marker, bold: row.kind === "error" }, row.marker || " "),
|
|
3273
|
+
),
|
|
3274
|
+
row.speaker
|
|
3275
|
+
? h(Text, { color: colors.speaker, bold: colors.bold }, row.speaker)
|
|
3276
|
+
: null,
|
|
3277
|
+
row.speaker
|
|
3278
|
+
? h(Text, { color: "gray" }, " · ")
|
|
3279
|
+
: null,
|
|
3280
|
+
h(Text, { color: colors.body, wrap: "wrap" }, row.body || " "),
|
|
3281
|
+
);
|
|
3282
|
+
};
|
|
3283
|
+
|
|
3284
|
+
if (state.viewingAgentId) {
|
|
3285
|
+
const maxWidth = Math.max(20, size.cols || 80);
|
|
3286
|
+
const logRows = Math.max(1, (size.rows || 24) - 5);
|
|
3287
|
+
const visibleRows = buildInternalLogRows(internalAgentView.lines || [], maxWidth, logRows);
|
|
3288
|
+
const status = internalStatusLabel(internalAgentView.status);
|
|
3289
|
+
const internalStatusText = computeInternalStatusText(internalAgentView, spinnerTick);
|
|
3290
|
+
const internalStatusColor = status === "blocked" ? "red" : (status === "ready" ? "gray" : "cyan");
|
|
3291
|
+
const inputText = String(internalAgentView.input || "");
|
|
3292
|
+
const cursor = Math.max(0, Math.min(inputText.length, Number(internalAgentView.cursor) || 0));
|
|
3293
|
+
const beforeCursor = inputText.slice(0, cursor);
|
|
3294
|
+
const cursorChar = inputText.slice(cursor, nextInternalBoundary(inputText, cursor)) || " ";
|
|
3295
|
+
const afterCursor = inputText.slice(cursor + (cursorChar === " " ? 0 : cursorChar.length));
|
|
3296
|
+
const barFocused = state.focusMode === "dashboard";
|
|
3297
|
+
const barIndex = Math.max(
|
|
3298
|
+
0,
|
|
3299
|
+
Math.min(displayAgents.length, Number(internalAgentView.barIndex) || 0),
|
|
3300
|
+
);
|
|
3301
|
+
const barHint = barFocused ? "│ ←/→ · Enter · ↑ · ^X" : "│ ↓ agents";
|
|
3302
|
+
const barItem = (text, index, options = {}) => {
|
|
3303
|
+
const keyboardSelected = barFocused && barIndex === index;
|
|
3304
|
+
return h(Text, {
|
|
3305
|
+
key: `agent-bar-${index}-${text}`,
|
|
3306
|
+
color: keyboardSelected || options.current === true ? undefined : "cyan",
|
|
3307
|
+
inverse: keyboardSelected,
|
|
3308
|
+
bold: options.current === true,
|
|
3309
|
+
wrap: "truncate",
|
|
3310
|
+
}, text);
|
|
3311
|
+
};
|
|
3312
|
+
const agentBarChildren = displayAgents.length === 0
|
|
3313
|
+
? [h(Text, { key: "agent-bar-none", color: "cyan", wrap: "truncate" }, "none")]
|
|
3314
|
+
: displayAgents.flatMap((id, idx) => {
|
|
3315
|
+
const meta = displayAgentMeta.get(id);
|
|
3316
|
+
return [
|
|
3317
|
+
idx > 0 ? h(Text, { key: `agent-bar-space-${id}`, color: "gray", wrap: "truncate" }, " ") : null,
|
|
3318
|
+
barItem(getAgentLabelFor(meta, id), idx + 1, {
|
|
3319
|
+
current: isInternalViewingAgent(id, meta, internalAgentView, state.viewingAgentId),
|
|
3320
|
+
}),
|
|
3321
|
+
];
|
|
3322
|
+
}).filter(Boolean);
|
|
3323
|
+
return h(Box, { flexDirection: "column", width: "100%" },
|
|
3324
|
+
h(Box, { flexDirection: "column", width: "100%" },
|
|
3325
|
+
...visibleRows.map((row, idx) => {
|
|
3326
|
+
const kind = row && row.kind ? row.kind : "agent";
|
|
3327
|
+
const color = kind === "user"
|
|
3328
|
+
? "cyan"
|
|
3329
|
+
: (kind === "system" || kind === "meta" || kind === "spacer" ? "gray" : (kind === "error" ? "red" : undefined));
|
|
3330
|
+
return h(Text, {
|
|
3331
|
+
key: `agent-log-${idx}`,
|
|
3332
|
+
color,
|
|
3333
|
+
bold: Boolean(row && row.bold),
|
|
3334
|
+
wrap: "truncate",
|
|
3335
|
+
}, (row && row.text) || " ");
|
|
3336
|
+
}),
|
|
3337
|
+
),
|
|
3338
|
+
h(Text, { color: internalStatusColor, wrap: "truncate" },
|
|
3339
|
+
fitPlainLine(internalStatusText, maxWidth)),
|
|
3340
|
+
h(Text, { color: "gray", wrap: "truncate" }, "─".repeat(maxWidth)),
|
|
3341
|
+
h(Box, { width: "100%" },
|
|
3342
|
+
h(Text, { color: "magenta" }, "› "),
|
|
3343
|
+
beforeCursor ? h(Text, { wrap: "truncate" }, beforeCursor) : null,
|
|
3344
|
+
h(Text, { inverse: true }, cursorChar),
|
|
3345
|
+
afterCursor ? h(Text, { wrap: "truncate" }, afterCursor) : null,
|
|
3346
|
+
),
|
|
3347
|
+
h(Text, { color: "gray", wrap: "truncate" }, "─".repeat(maxWidth)),
|
|
3348
|
+
h(Box, { width: "100%" },
|
|
3349
|
+
h(Text, { color: "gray", wrap: "truncate" }, " "),
|
|
3350
|
+
barItem("ufoo", 0),
|
|
3351
|
+
h(Text, { color: "gray", wrap: "truncate" }, " "),
|
|
3352
|
+
...agentBarChildren,
|
|
3353
|
+
h(Text, { color: "gray", wrap: "truncate" }, ` ${barHint}`),
|
|
3354
|
+
),
|
|
3355
|
+
);
|
|
3356
|
+
}
|
|
3357
|
+
|
|
3358
|
+
return h(Box, { flexDirection: "column", width: "100%" },
|
|
3359
|
+
h(Box, { flexDirection: "column", width: "100%" },
|
|
3360
|
+
...state.logLines.map(renderChatLogLine),
|
|
3361
|
+
),
|
|
3362
|
+
state.activeMerge ? h(Box, null,
|
|
3363
|
+
h(Text, { color: state.activeMerge.entries.some((e) => e.isError) ? "red" : "cyan" },
|
|
3364
|
+
fmt.buildToolMergeRowText(state.activeMerge.entries)),
|
|
3365
|
+
) : null,
|
|
3366
|
+
state.activeStream ? h(Box, { flexDirection: "column" },
|
|
3367
|
+
...(() => {
|
|
3368
|
+
const lines = String(state.activeStream.text || "").split(/\r?\n/);
|
|
3369
|
+
const prefix = state.activeStream.publisher
|
|
3370
|
+
? `${state.activeStream.publisher}: `
|
|
3371
|
+
: "";
|
|
3372
|
+
return lines.map((line, idx) => renderChatLogLine({
|
|
3373
|
+
id: `s-${idx}`,
|
|
3374
|
+
text: idx === 0 ? `${prefix}${line}` : ` ${line}`,
|
|
3375
|
+
}));
|
|
3376
|
+
})(),
|
|
3377
|
+
) : null,
|
|
3378
|
+
h(Box, { marginTop: 1, width: "100%" },
|
|
3379
|
+
h(Text, { color: "gray" }, statusText),
|
|
3380
|
+
h(Box, { flexGrow: 1 }),
|
|
3381
|
+
h(Text, { color: "gray" }, `v${fmt.UCODE_VERSION}`),
|
|
3382
|
+
),
|
|
3383
|
+
completionsOpen ? (() => {
|
|
3384
|
+
const start = Math.min(completionWindowStart, Math.max(0, completions.length - POPUP_PAGE_SIZE));
|
|
3385
|
+
const end = Math.min(completions.length, start + POPUP_PAGE_SIZE);
|
|
3386
|
+
const visible = completions.slice(start, end);
|
|
3387
|
+
return h(Box, { flexDirection: "column" },
|
|
3388
|
+
h(Text, { color: "gray" }, "─".repeat(Math.max(8, size.cols || 80))),
|
|
3389
|
+
...visible.map((s, idxInWindow) => {
|
|
3390
|
+
const idx = start + idxInWindow;
|
|
3391
|
+
return h(Box, { key: `cmp-${idx}` },
|
|
3392
|
+
h(Text, { color: idx === completionIndex ? "cyan" : "gray", inverse: idx === completionIndex }, s.label),
|
|
3393
|
+
s.description ? h(Text, { color: "gray" }, ` ${s.description}`) : null,
|
|
3394
|
+
);
|
|
3395
|
+
}),
|
|
3396
|
+
);
|
|
3397
|
+
})() : null,
|
|
3398
|
+
h(Box, { width: "100%" },
|
|
3399
|
+
h(MultilineInput, {
|
|
3400
|
+
value: state.draft,
|
|
3401
|
+
valueVersion: draftVersion,
|
|
3402
|
+
onChange: (next) => {
|
|
3403
|
+
if (completionSuppressedDraft !== null && next !== completionSuppressedDraft) {
|
|
3404
|
+
setCompletionSuppressedDraft(null);
|
|
3405
|
+
}
|
|
3406
|
+
dispatch({ type: "draft/set", value: next });
|
|
3407
|
+
},
|
|
3408
|
+
onSubmit: (value) => {
|
|
3409
|
+
setCompletionSuppressedDraft(null);
|
|
3410
|
+
submit(value);
|
|
3411
|
+
},
|
|
3412
|
+
onCancel: () => {
|
|
3413
|
+
setCompletionSuppressedDraft(null);
|
|
3414
|
+
if (props.globalMode && state.globalScope === "project") {
|
|
3415
|
+
void switchToControllerRoot();
|
|
3416
|
+
return;
|
|
3417
|
+
}
|
|
3418
|
+
// Esc clears the current target if one is locked, otherwise
|
|
3419
|
+
// dismisses the in-flight task status. There's no per-request
|
|
3420
|
+
// AbortController on daemonConnection (the IPC layer is fire-
|
|
3421
|
+
// and-forget), so we clear the spinner so the user knows the
|
|
3422
|
+
// UI is responsive again.
|
|
3423
|
+
if (state.agentSelectionMode) {
|
|
3424
|
+
dispatch({ type: "agents/clearTarget" });
|
|
3425
|
+
return;
|
|
3426
|
+
}
|
|
3427
|
+
if (state.status && state.status.message) {
|
|
3428
|
+
dispatch({ type: "status/idle" });
|
|
3429
|
+
}
|
|
3430
|
+
},
|
|
3431
|
+
onArrowUpAtTop,
|
|
3432
|
+
onArrowDownAtBottom,
|
|
3433
|
+
onArrowLeftAtEmpty: () => onArrowSideAtEmpty("left"),
|
|
3434
|
+
onArrowRightAtEmpty: () => onArrowSideAtEmpty("right"),
|
|
3435
|
+
width: inputWidth,
|
|
3436
|
+
interactive: interactive && state.focusMode !== "dashboard",
|
|
3437
|
+
interceptArrowsAndEnter: completionsOpen,
|
|
3438
|
+
placeholder: "",
|
|
3439
|
+
promptPrefix,
|
|
3440
|
+
// Dashboard renders 2 rows in global mode (always shows the
|
|
3441
|
+
// projects rail) or when an agents/mode/provider/cron view is
|
|
3442
|
+
// focused; otherwise it's a single summary row. Telling
|
|
3443
|
+
// MultilineInput how many UI rows live below it lets the IME
|
|
3444
|
+
// composition popup follow the on-screen caret instead of
|
|
3445
|
+
// appearing at the bottom-right of the terminal.
|
|
3446
|
+
linesBelowInput: props.globalMode
|
|
3447
|
+
? 2
|
|
3448
|
+
: (state.focusMode === "dashboard" ? 2 : 1),
|
|
3449
|
+
}),
|
|
3450
|
+
),
|
|
3451
|
+
h(DashboardBar, {
|
|
3452
|
+
dashboardView: state.dashboardView,
|
|
3453
|
+
focusMode: state.focusMode,
|
|
3454
|
+
globalMode: state.globalMode,
|
|
3455
|
+
globalScope: state.globalScope,
|
|
3456
|
+
activeAgents: displayAgents,
|
|
3457
|
+
activeAgentMeta: displayAgentMeta,
|
|
3458
|
+
activeAgentId: targetAgentId || "",
|
|
3459
|
+
selectedAgentIndex: state.selectedAgentIndex,
|
|
3460
|
+
agentListWindowStart: state.agentListWindowStart,
|
|
3461
|
+
projectListWindowStart: state.projectListWindowStart,
|
|
3462
|
+
maxProjectWindow: 5,
|
|
3463
|
+
maxWidth: Math.max(20, size.cols || 80),
|
|
3464
|
+
getAgentLabel: (id) => getAgentLabelFor(displayAgentMeta.get(id), id),
|
|
3465
|
+
getAgentState: (id) => {
|
|
3466
|
+
const meta = displayAgentMeta.get(id);
|
|
3467
|
+
return meta && typeof meta.activity_state === "string" ? meta.activity_state : "";
|
|
3468
|
+
},
|
|
3469
|
+
launchMode: state.settings.launchMode,
|
|
3470
|
+
agentProvider: state.settings.agentProvider,
|
|
3471
|
+
modeOptions: state.modeOptions,
|
|
3472
|
+
selectedModeIndex: state.selectedModeIndex,
|
|
3473
|
+
providerOptions: state.providerOptions,
|
|
3474
|
+
selectedProviderIndex: state.selectedProviderIndex,
|
|
3475
|
+
cronTasks: state.cronTasks,
|
|
3476
|
+
selectedCronIndex: state.selectedCronIndex,
|
|
3477
|
+
projects: state.projects,
|
|
3478
|
+
selectedProjectIndex: state.selectedProjectIndex,
|
|
3479
|
+
activeProjectRoot: currentProjectRoot,
|
|
3480
|
+
dashHints: buildDashHints(state, targetAgentLabel),
|
|
3481
|
+
}),
|
|
3482
|
+
);
|
|
3483
|
+
};
|
|
3484
|
+
}
|
|
3485
|
+
|
|
3486
|
+
function buildDashHints(state, targetAgentLabel) {
|
|
3487
|
+
void targetAgentLabel; // navigation hint removed by request
|
|
3488
|
+
return {
|
|
3489
|
+
agents: "←/→ select · Enter · ↓ mode · ↑ back",
|
|
3490
|
+
agentsGlobal: "←/→ select · Enter · ↓ mode · ↑ projects",
|
|
3491
|
+
agentsEmpty: "↓ mode · ↑ back",
|
|
3492
|
+
mode: "←/→ select · Enter · ↓ provider · ↑ back",
|
|
3493
|
+
provider: "←/→ select · Enter · ↓ cron · ↑ back",
|
|
3494
|
+
cron: "←/→ switch · Ctrl+X stop · ↑ back",
|
|
3495
|
+
projects: "Use /open <path> or /project switch <index|path>",
|
|
3496
|
+
projectsFocus: "←/→ switch · Ctrl+X close · ↓ second row · Enter confirm · ↑ back",
|
|
3497
|
+
projectsEmpty: "Run ufoo chat or ufoo daemon start in project directories",
|
|
3498
|
+
};
|
|
3499
|
+
}
|
|
3500
|
+
|
|
3501
|
+
function computeStatusText(status, spinnerTick) {
|
|
3502
|
+
const message = String((status && status.message) || "");
|
|
3503
|
+
if (!message) return "CHAT · Ready";
|
|
3504
|
+
const type = String((status && status.type) || "thinking");
|
|
3505
|
+
if (type === "done" || type === "success") {
|
|
3506
|
+
const clean = stripBlessedTags(message).trim();
|
|
3507
|
+
return /^[✓✔]/.test(clean) ? clean : `✓ ${clean}`;
|
|
3508
|
+
}
|
|
3509
|
+
if (type === "error") {
|
|
3510
|
+
const clean = stripBlessedTags(message).trim();
|
|
3511
|
+
return /^[✗!]/.test(clean) ? clean : `✗ ${clean}`;
|
|
3512
|
+
}
|
|
3513
|
+
if (!isAnimatedStatusType(type)) return stripBlessedTags(message).trim() || "CHAT · Ready";
|
|
3514
|
+
const indicators = fmt.STATUS_INDICATORS[type] || fmt.STATUS_INDICATORS.thinking;
|
|
3515
|
+
const indicator = indicators[Math.max(0, Math.floor(Number(spinnerTick) || 0)) % indicators.length];
|
|
3516
|
+
const startedAt = Number.isFinite(status && status.startedAt) ? status.startedAt : 0;
|
|
3517
|
+
const timerText = status && status.showTimer && startedAt
|
|
3518
|
+
? ` (${fmt.formatPendingElapsed(Date.now() - startedAt)}, esc cancel)`
|
|
3519
|
+
: "";
|
|
3520
|
+
return `${indicator} ${message}${timerText}`;
|
|
3521
|
+
}
|
|
3522
|
+
|
|
3523
|
+
async function runChatInk(projectRoot, options = {}) {
|
|
3524
|
+
const env = bootstrapEnvironment(projectRoot, options);
|
|
3525
|
+
|
|
3526
|
+
if (env.needsBootstrap || !fs.existsSync(env.runtimePaths.ufooDir)) {
|
|
3527
|
+
const repoRoot = path.join(__dirname, "..", "..", "..");
|
|
3528
|
+
const init = new env.UfooInit(repoRoot);
|
|
3529
|
+
await init.init({
|
|
3530
|
+
modules: "context,bus",
|
|
3531
|
+
project: projectRoot,
|
|
3532
|
+
controllerMode: env.globalMode,
|
|
3533
|
+
});
|
|
3534
|
+
}
|
|
3535
|
+
|
|
3536
|
+
await ensureSubscriberId(projectRoot);
|
|
3537
|
+
|
|
3538
|
+
if (!env.isRunning(projectRoot)) {
|
|
3539
|
+
env.startDaemon(projectRoot);
|
|
3540
|
+
}
|
|
3541
|
+
|
|
3542
|
+
const { socketPath } = require("../../runtime/daemon");
|
|
3543
|
+
const { connectWithRetry } = require("../../app/chat/transport");
|
|
3544
|
+
const { createDaemonTransport } = require("../../app/chat/daemonTransport");
|
|
3545
|
+
const { createDaemonConnection } = require("../../app/chat/daemonConnection");
|
|
3546
|
+
const { createDaemonCoordinator } = require("../../app/chat/daemonCoordinator");
|
|
3547
|
+
const { startDaemon, stopDaemon } = require("../../app/chat/transport");
|
|
3548
|
+
const { loadConfig } = require("../../config");
|
|
3549
|
+
const { startAgentMirror, startInternalAgentMirror } = require("./agentMirror");
|
|
3550
|
+
const sock = socketPath(projectRoot);
|
|
3551
|
+
const daemonTransport = createDaemonTransport({
|
|
3552
|
+
projectRoot,
|
|
3553
|
+
sockPath: sock,
|
|
3554
|
+
isRunning: env.isRunning,
|
|
3555
|
+
startDaemon: env.startDaemon,
|
|
3556
|
+
connectWithRetry,
|
|
3557
|
+
});
|
|
3558
|
+
|
|
3559
|
+
// The connection's `handleMessage` callback is filled in by ChatApp once
|
|
3560
|
+
// it mounts and has its dispatcher ready. We expose a setter so the
|
|
3561
|
+
// component can wire it without ChatApp needing to construct daemon
|
|
3562
|
+
// internals itself.
|
|
3563
|
+
let routedMessageHandler = () => {};
|
|
3564
|
+
const daemonConnection = createDaemonConnection({
|
|
3565
|
+
connectClient: daemonTransport.connectClient.bind(daemonTransport),
|
|
3566
|
+
handleMessage: (msg) => routedMessageHandler(msg),
|
|
3567
|
+
queueStatusLine: () => {},
|
|
3568
|
+
resolveStatusLine: () => {},
|
|
3569
|
+
logMessage: () => {},
|
|
3570
|
+
});
|
|
3571
|
+
const daemonCoordinator = createDaemonCoordinator({
|
|
3572
|
+
projectRoot,
|
|
3573
|
+
daemonTransport,
|
|
3574
|
+
daemonConnection,
|
|
3575
|
+
stopDaemon,
|
|
3576
|
+
startDaemon,
|
|
3577
|
+
logMessage: () => {},
|
|
3578
|
+
queueStatusLine: () => {},
|
|
3579
|
+
resolveStatusLine: () => {},
|
|
3580
|
+
});
|
|
3581
|
+
|
|
3582
|
+
// We loop the ink mount so an "enter agent" request can unmount ink,
|
|
3583
|
+
// hand stdout/stdin to the raw PTY mirror, then bring ink back on exit.
|
|
3584
|
+
let pendingEnter = null;
|
|
3585
|
+
const baseProps = {
|
|
3586
|
+
activeProjectRoot: env.activeProjectRoot,
|
|
3587
|
+
projectRoot,
|
|
3588
|
+
globalMode: env.globalMode,
|
|
3589
|
+
globalScope: env.globalMode ? "controller" : "project",
|
|
3590
|
+
daemonConnection,
|
|
3591
|
+
daemonTransport,
|
|
3592
|
+
daemonCoordinator,
|
|
3593
|
+
env,
|
|
3594
|
+
initialSettings: loadConfig(projectRoot),
|
|
3595
|
+
setDaemonMessageHandler: (fn) => { routedMessageHandler = typeof fn === "function" ? fn : () => {}; },
|
|
3596
|
+
requestEnterAgentView: (agentId, enterOptions = {}) => {
|
|
3597
|
+
pendingEnter = {
|
|
3598
|
+
agentId,
|
|
3599
|
+
options: enterOptions && typeof enterOptions === "object" ? enterOptions : {},
|
|
3600
|
+
};
|
|
3601
|
+
},
|
|
3602
|
+
};
|
|
3603
|
+
|
|
3604
|
+
// eslint-disable-next-line no-constant-condition
|
|
3605
|
+
while (true) {
|
|
3606
|
+
pendingEnter = null;
|
|
3607
|
+
const handle = await runInk(
|
|
3608
|
+
(React, ink) => {
|
|
3609
|
+
const ChatApp = createChatApp({ React, ink, props: baseProps });
|
|
3610
|
+
return React.createElement(ChatApp);
|
|
3611
|
+
},
|
|
3612
|
+
{ stdin: process.stdin, stdout: process.stdout, exitOnCtrlC: true }
|
|
3613
|
+
);
|
|
3614
|
+
|
|
3615
|
+
// Wait until either the user exits the app or ChatApp asks to enter
|
|
3616
|
+
// an agent view. The component triggers the latter by setting
|
|
3617
|
+
// pendingEnter and then calling handle.unmount() via its onExit.
|
|
3618
|
+
await handle.waitUntilExit();
|
|
3619
|
+
if (!pendingEnter) return;
|
|
3620
|
+
|
|
3621
|
+
// Hand stdout/stdin to the mirror. When it exits, loop and re-mount.
|
|
3622
|
+
const enterRequest = pendingEnter;
|
|
3623
|
+
pendingEnter = null;
|
|
3624
|
+
const enteredAgentId = enterRequest && enterRequest.agentId;
|
|
3625
|
+
const enterOptions = enterRequest && enterRequest.options ? enterRequest.options : {};
|
|
3626
|
+
const enteredProjectRoot = enterOptions.projectRoot || projectRoot;
|
|
3627
|
+
await new Promise((resolve) => {
|
|
3628
|
+
if (enterOptions.useBus) {
|
|
3629
|
+
startInternalAgentMirror({
|
|
3630
|
+
agentId: enteredAgentId,
|
|
3631
|
+
agentLabel: enterOptions.agentLabel,
|
|
3632
|
+
agentAliases: enterOptions.agentAliases,
|
|
3633
|
+
projectRoot: enteredProjectRoot,
|
|
3634
|
+
daemonConnection,
|
|
3635
|
+
setDaemonMessageHandler: (fn) => {
|
|
3636
|
+
routedMessageHandler = typeof fn === "function" ? fn : () => {};
|
|
3637
|
+
},
|
|
3638
|
+
onExit: resolve,
|
|
3639
|
+
});
|
|
3640
|
+
return;
|
|
3641
|
+
}
|
|
3642
|
+
startAgentMirror({
|
|
3643
|
+
agentId: enteredAgentId,
|
|
3644
|
+
projectRoot: enteredProjectRoot,
|
|
3645
|
+
onExit: resolve,
|
|
3646
|
+
});
|
|
3647
|
+
});
|
|
3648
|
+
}
|
|
3649
|
+
}
|
|
3650
|
+
|
|
3651
|
+
module.exports = {
|
|
3652
|
+
runChatInk,
|
|
3653
|
+
createChatApp,
|
|
3654
|
+
bootstrapEnvironment,
|
|
3655
|
+
buildDirectBusSendRequest,
|
|
3656
|
+
buildPromptIpcRequest,
|
|
3657
|
+
chatHistoryOptionsForScope,
|
|
3658
|
+
classifyChatLogLine,
|
|
3659
|
+
createInkMultiWindowToggle,
|
|
3660
|
+
resolveActiveAgentId,
|
|
3661
|
+
resolveInjectSockPathForAgent,
|
|
3662
|
+
resolveAgentEnterRequest,
|
|
3663
|
+
resolveDashboardAgentEnterAction,
|
|
3664
|
+
buildEmptyProjectsDownActions,
|
|
3665
|
+
buildInternalLogRows,
|
|
3666
|
+
computeStatusText,
|
|
3667
|
+
computeInternalStatusText,
|
|
3668
|
+
inferStatusType,
|
|
3669
|
+
isAnimatedStatusType,
|
|
3670
|
+
resolveInternalKeyName,
|
|
3671
|
+
isInternalViewingAgent,
|
|
3672
|
+
applyInternalAgentTermWrite,
|
|
3673
|
+
appendInternalErrorToView,
|
|
3674
|
+
};
|