u-foo 3.0.10 → 3.0.11
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 +25 -9
- package/README.zh-CN.md +22 -9
- package/dist/tui/darwin-arm64/ufoo-tui +0 -0
- package/dist/tui/darwin-x64/ufoo-tui +0 -0
- package/dist/tui/linux-arm64/ufoo-tui +0 -0
- package/dist/tui/linux-x64/ufoo-tui +0 -0
- package/package.json +11 -3
- package/scripts/pack-tui.js +112 -0
- package/scripts/postinstall.js +11 -0
- package/src/agents/activity/activityReconcile.js +106 -0
- package/src/agents/activity/activityStatePublisher.js +31 -2
- package/src/agents/activity/index.js +1 -0
- package/src/agents/launch/launcher.js +19 -0
- package/src/agents/launch/ptyRunner.js +20 -1
- package/src/app/chat/ChatController.js +433 -0
- package/src/app/chat/agentDirectory.js +63 -0
- package/src/app/chat/agentEnter.js +70 -0
- package/src/app/chat/agentIdentity.js +50 -0
- package/src/app/chat/bootstrap.js +66 -0
- package/src/app/chat/commandExecutor.js +108 -0
- package/src/app/chat/commands.js +31 -0
- package/src/app/chat/dashboardView.js +6 -2
- package/src/app/chat/historyStore.js +181 -0
- package/src/app/chat/index.js +14 -2
- package/src/app/chat/inputSubmitHandler.js +21 -7
- package/src/app/chat/ipcBuilders.js +52 -0
- package/src/app/chat/multiWindow/paneManager.js +10 -1
- package/src/app/chat/multiWindow/renderer.js +1 -1
- package/src/app/chat/multiWindow/vtFrame.js +93 -0
- package/src/app/chat/streamState.js +182 -0
- package/src/app/cli/features/doctor.js +22 -0
- package/src/code/UcodeController.js +156 -0
- package/src/code/context/planGraphService.js +4 -0
- package/src/code/repl.js +4 -3
- package/src/code/runtime/taskLoop.js +46 -50
- package/src/code/tui.js +13 -2
- package/src/code/ucodeSlashDispatch.js +241 -0
- package/src/coordination/bus/activate.js +3 -0
- package/src/runtime/contracts/schemas/ufoo-ui-v1/envelope.json +28 -0
- package/src/runtime/contracts/uiProtocol.js +190 -0
- package/src/ui/{ink/chatLogModel.js → chatLogModel.js} +2 -2
- package/src/ui/dashboardBridge.js +81 -0
- package/src/ui/format/index.js +2 -2
- package/src/ui/index.js +8 -4
- package/src/ui/multiPaneBusMirror.js +137 -0
- package/src/ui/multiWindowHandoff.js +232 -0
- package/src/ui/ptyHandoff.js +23 -0
- package/src/ui/rustChatHost.js +1520 -0
- package/src/ui/rustMultiSession.js +497 -0
- package/src/ui/rustUcodeHost.js +999 -0
- package/src/ui/scrollbackReplay.js +82 -0
- package/src/ui/settingsBridge.js +49 -0
- package/src/ui/toolMergeBridge.js +66 -0
- package/src/ui/tuiLauncher.js +105 -0
- package/src/ui/ucodeStatusLine.js +74 -0
- package/src/ui/uiHostServer.js +339 -0
- package/src/ui/MIGRATION.md +0 -334
- package/src/ui/ink/ChatApp.js +0 -4163
- package/src/ui/ink/DashboardBar.js +0 -691
- package/src/ui/ink/InkDemo.js +0 -96
- package/src/ui/ink/MultilineInput.js +0 -662
- package/src/ui/ink/UcodeApp.js +0 -1675
- package/src/ui/ink/agentMirror.js +0 -730
- package/src/ui/ink/chatReducer.js +0 -473
- package/src/ui/runInk.js +0 -66
|
@@ -9,10 +9,19 @@ const {
|
|
|
9
9
|
loadGlobalUcodeConfig,
|
|
10
10
|
saveGlobalUcodeConfig,
|
|
11
11
|
normalizeControllerMode,
|
|
12
|
+
normalizeLaunchMode,
|
|
12
13
|
SETTINGS_MODEL_DEFAULTS,
|
|
13
14
|
defaultAgentModelForProvider,
|
|
14
15
|
defaultRouterModelForProvider,
|
|
15
16
|
} = require("../../config");
|
|
17
|
+
|
|
18
|
+
const LAUNCH_MODE_OPTIONS = Object.freeze(["auto", "host", "terminal", "tmux", "internal"]);
|
|
19
|
+
const AGENT_PROVIDER_OPTIONS = Object.freeze([
|
|
20
|
+
{ label: "codex", value: "codex-cli" },
|
|
21
|
+
{ label: "claude", value: "claude-cli" },
|
|
22
|
+
{ label: "agy", value: "agy-cli" },
|
|
23
|
+
{ label: "kimi", value: "kimi-cli" },
|
|
24
|
+
]);
|
|
16
25
|
const { resolveTransport } = require("../../code/nativeRunner");
|
|
17
26
|
const { resolveDisplayNickname } = require("../../runtime/daemon/nicknameScope");
|
|
18
27
|
const { parseIntervalMs, formatIntervalMs } = require("./cronScheduler");
|
|
@@ -167,6 +176,7 @@ function createCommandExecutor(options = {}) {
|
|
|
167
176
|
schedule = (fn, ms) => setTimeout(fn, ms),
|
|
168
177
|
clearLog = null,
|
|
169
178
|
fetchModelsImpl = null,
|
|
179
|
+
applyChatSettings = null,
|
|
170
180
|
} = options;
|
|
171
181
|
|
|
172
182
|
if (!projectRoot) {
|
|
@@ -998,6 +1008,96 @@ function createCommandExecutor(options = {}) {
|
|
|
998
1008
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
999
1009
|
}
|
|
1000
1010
|
|
|
1011
|
+
function clearUfooAgentIdentity() {
|
|
1012
|
+
try {
|
|
1013
|
+
const { getUfooPaths } = require("../../coordination/state/paths");
|
|
1014
|
+
const agentDir = getUfooPaths(projectRoot).agentDir;
|
|
1015
|
+
const fsModule = require("fs");
|
|
1016
|
+
fsModule.rmSync(path.join(agentDir, "ufoo-agent.json"), { force: true });
|
|
1017
|
+
fsModule.rmSync(path.join(agentDir, "ufoo-agent.history.jsonl"), { force: true });
|
|
1018
|
+
} catch {
|
|
1019
|
+
// Ignore cleanup failures.
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
async function notifyChatSettings(patch = {}) {
|
|
1024
|
+
if (typeof applyChatSettings !== "function") return;
|
|
1025
|
+
try {
|
|
1026
|
+
await applyChatSettings(patch);
|
|
1027
|
+
} catch {
|
|
1028
|
+
// Host refresh is best-effort; config is already persisted.
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
async function handleModeCommand(args = []) {
|
|
1033
|
+
const action = String(args[0] || "").trim().toLowerCase();
|
|
1034
|
+
const config = loadConfig(projectRoot) || {};
|
|
1035
|
+
const current = normalizeLaunchMode(config.launchMode || "auto");
|
|
1036
|
+
|
|
1037
|
+
if (!action || action === "show" || action === "status") {
|
|
1038
|
+
logMessage("system", "{cyan-fg}launch mode:{/cyan-fg}");
|
|
1039
|
+
logMessage("system", ` • current: ${current}`);
|
|
1040
|
+
logMessage("system", ` • options: ${LAUNCH_MODE_OPTIONS.join(", ")}`);
|
|
1041
|
+
logMessage("system", " • use: /mode <auto|host|terminal|tmux|internal>");
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
if (!LAUNCH_MODE_OPTIONS.includes(action)) {
|
|
1046
|
+
logMessage(
|
|
1047
|
+
"error",
|
|
1048
|
+
`{white-fg}✗{/white-fg} Unknown mode: ${action}. Use: ${LAUNCH_MODE_OPTIONS.join("|")}`
|
|
1049
|
+
);
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
const next = normalizeLaunchMode(action);
|
|
1054
|
+
if (next === current) {
|
|
1055
|
+
logMessage("system", `{cyan-fg}launch mode:{/cyan-fg} already ${next}`);
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
saveConfig(projectRoot, { launchMode: next });
|
|
1060
|
+
logMessage("system", `{white-fg}✓{/white-fg} Launch mode: ${next}`);
|
|
1061
|
+
await notifyChatSettings({ launchMode: next });
|
|
1062
|
+
await restartDaemon(projectRoot);
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
async function handleProviderCommand(args = []) {
|
|
1066
|
+
const action = String(args[0] || "").trim().toLowerCase();
|
|
1067
|
+
const config = loadConfig(projectRoot) || {};
|
|
1068
|
+
const current = normalizeSettingsProvider(config.agentProvider);
|
|
1069
|
+
const labels = AGENT_PROVIDER_OPTIONS.map((opt) => opt.label).join(", ");
|
|
1070
|
+
|
|
1071
|
+
if (!action || action === "show" || action === "status") {
|
|
1072
|
+
logMessage("system", "{cyan-fg}ufoo-agent provider:{/cyan-fg}");
|
|
1073
|
+
logMessage("system", ` • current: ${agentProviderKey(current)}`);
|
|
1074
|
+
logMessage("system", ` • options: ${labels}`);
|
|
1075
|
+
logMessage("system", " • use: /provider <codex|claude|agy|kimi>");
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
const next = normalizeSettingsProvider(action, "");
|
|
1080
|
+
const known = AGENT_PROVIDER_OPTIONS.some((opt) => opt.label === action || opt.value === next);
|
|
1081
|
+
if (!next || !known) {
|
|
1082
|
+
logMessage(
|
|
1083
|
+
"error",
|
|
1084
|
+
`{white-fg}✗{/white-fg} Unknown provider: ${action}. Use: ${AGENT_PROVIDER_OPTIONS.map((opt) => opt.label).join("|")}`
|
|
1085
|
+
);
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
if (next === current) {
|
|
1090
|
+
logMessage("system", `{cyan-fg}ufoo-agent:{/cyan-fg} already ${agentProviderKey(next)}`);
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
saveConfig(projectRoot, { agentProvider: next });
|
|
1095
|
+
clearUfooAgentIdentity();
|
|
1096
|
+
logMessage("system", `{white-fg}✓{/white-fg} ufoo-agent: ${agentProviderKey(next)}`);
|
|
1097
|
+
await notifyChatSettings({ agentProvider: next });
|
|
1098
|
+
await restartDaemon(projectRoot);
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1001
1101
|
async function handleCronCommand(args = []) {
|
|
1002
1102
|
const action = String(args[0] || "").trim().toLowerCase();
|
|
1003
1103
|
if (action === "list" || action === "ls") {
|
|
@@ -1861,6 +1961,12 @@ function createCommandExecutor(options = {}) {
|
|
|
1861
1961
|
case "cron":
|
|
1862
1962
|
await handleCronCommand(args);
|
|
1863
1963
|
return true;
|
|
1964
|
+
case "mode":
|
|
1965
|
+
await handleModeCommand(args);
|
|
1966
|
+
return true;
|
|
1967
|
+
case "provider":
|
|
1968
|
+
await handleProviderCommand(args);
|
|
1969
|
+
return true;
|
|
1864
1970
|
case "group":
|
|
1865
1971
|
await handleGroupCommand(args);
|
|
1866
1972
|
return true;
|
|
@@ -1893,6 +1999,8 @@ function createCommandExecutor(options = {}) {
|
|
|
1893
1999
|
handleRoleCommand,
|
|
1894
2000
|
handleSoloCommand,
|
|
1895
2001
|
handleCronCommand,
|
|
2002
|
+
handleModeCommand,
|
|
2003
|
+
handleProviderCommand,
|
|
1896
2004
|
handleGroupCommand,
|
|
1897
2005
|
handleSettingsCommand,
|
|
1898
2006
|
handleUcodeConfigCommand,
|
package/src/app/chat/commands.js
CHANGED
|
@@ -35,6 +35,27 @@ const COMMAND_TREE = {
|
|
|
35
35
|
stop: { desc: "Stop cron task by id or all" },
|
|
36
36
|
},
|
|
37
37
|
},
|
|
38
|
+
"/mode": {
|
|
39
|
+
desc: "Set agent launch mode",
|
|
40
|
+
children: {
|
|
41
|
+
show: { desc: "Show current launch mode", order: 1 },
|
|
42
|
+
auto: { desc: "Auto-select launch mode", order: 2 },
|
|
43
|
+
host: { desc: "Launch via host inject", order: 3 },
|
|
44
|
+
terminal: { desc: "Launch in external terminal", order: 4 },
|
|
45
|
+
tmux: { desc: "Launch in tmux", order: 5 },
|
|
46
|
+
internal: { desc: "Launch as internal agent", order: 6 },
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
"/provider": {
|
|
50
|
+
desc: "Set ufoo-agent provider",
|
|
51
|
+
children: {
|
|
52
|
+
show: { desc: "Show current agent provider", order: 1 },
|
|
53
|
+
codex: { desc: "Use Codex", order: 2 },
|
|
54
|
+
claude: { desc: "Use Claude", order: 3 },
|
|
55
|
+
agy: { desc: "Use Antigravity (agy)", order: 4 },
|
|
56
|
+
kimi: { desc: "Use Kimi Code", order: 5 },
|
|
57
|
+
},
|
|
58
|
+
},
|
|
38
59
|
"/clear": { desc: "Clear chat log on screen" },
|
|
39
60
|
"/group": {
|
|
40
61
|
desc: "Agent group orchestration",
|
|
@@ -315,6 +336,16 @@ function describeCommandForChat(text) {
|
|
|
315
336
|
return "Managing cron tasks";
|
|
316
337
|
}
|
|
317
338
|
|
|
339
|
+
if (command === "mode") {
|
|
340
|
+
if (!sub || sub === "show" || sub === "status") return "Showing launch mode";
|
|
341
|
+
return `Setting launch mode to ${sub}`;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (command === "provider") {
|
|
345
|
+
if (!sub || sub === "show" || sub === "status") return "Showing agent provider";
|
|
346
|
+
return `Setting agent provider to ${sub}`;
|
|
347
|
+
}
|
|
348
|
+
|
|
318
349
|
if (command === "mcp") {
|
|
319
350
|
if (!sub || sub === "status") return "Checking MCP bridge status";
|
|
320
351
|
if (sub === "tools") return "Listing MCP tools";
|
|
@@ -48,8 +48,11 @@ function buildSummaryLine(options = {}) {
|
|
|
48
48
|
: "none";
|
|
49
49
|
let line = `{gray-fg}Agents:{/gray-fg} {cyan-fg}${agents}{/cyan-fg}`
|
|
50
50
|
+ ` {gray-fg}Mode:{/gray-fg} {cyan-fg}${launchMode}{/cyan-fg}`
|
|
51
|
-
+ ` {gray-fg}Agent:{/gray-fg} {cyan-fg}${providerLabel(agentProvider)}{/cyan-fg}
|
|
52
|
-
|
|
51
|
+
+ ` {gray-fg}Agent:{/gray-fg} {cyan-fg}${providerLabel(agentProvider)}{/cyan-fg}`;
|
|
52
|
+
const cronCount = Array.isArray(cronTasks) ? cronTasks.length : 0;
|
|
53
|
+
if (cronCount > 0) {
|
|
54
|
+
line += ` {gray-fg}Cron:{/gray-fg} {cyan-fg}${cronCount}{/cyan-fg}`;
|
|
55
|
+
}
|
|
53
56
|
const loopPart = formatLoopSummary(loopSummary);
|
|
54
57
|
if (loopPart) {
|
|
55
58
|
line += ` {gray-fg}Loop:{/gray-fg} {cyan-fg}${loopPart}{/cyan-fg}`;
|
|
@@ -398,4 +401,5 @@ function computeDashboardContent(options = {}) {
|
|
|
398
401
|
module.exports = {
|
|
399
402
|
computeDashboardContent,
|
|
400
403
|
providerLabel,
|
|
404
|
+
formatLoopSummary,
|
|
401
405
|
};
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Chat / input history persistence (Phase 0B extraction from ChatApp).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const path = require("path");
|
|
8
|
+
const fs = require("fs");
|
|
9
|
+
const crypto = require("crypto");
|
|
10
|
+
const { stripBlessedTags } = require("../../ui/chatLogModel");
|
|
11
|
+
|
|
12
|
+
function projectRootToId(projectRoot) {
|
|
13
|
+
try {
|
|
14
|
+
const { buildProjectId } = require("../../runtime/projects");
|
|
15
|
+
return buildProjectId(projectRoot || process.cwd());
|
|
16
|
+
} catch {
|
|
17
|
+
return crypto.createHash("sha256").update(String(projectRoot || "")).digest("hex").slice(0, 16);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function inputHistoryFilePath(projectRoot, options = {}) {
|
|
22
|
+
const { getUfooPaths } = require("../../coordination/state/paths");
|
|
23
|
+
const { globalMode } = options || {};
|
|
24
|
+
if (globalMode) {
|
|
25
|
+
const os = require("os");
|
|
26
|
+
const globalChatRoot = path.join(os.homedir(), ".ufoo", "chat");
|
|
27
|
+
const globalDir = path.join(globalChatRoot, "global-input-history");
|
|
28
|
+
const projectId = projectRootToId(projectRoot);
|
|
29
|
+
return path.join(globalDir, `${projectId}.jsonl`);
|
|
30
|
+
}
|
|
31
|
+
return path.join(getUfooPaths(projectRoot || process.cwd()).ufooDir, "chat", "input-history.jsonl");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function chatHistoryFilePath(projectRoot, options = {}) {
|
|
35
|
+
const { getUfooPaths } = require("../../coordination/state/paths");
|
|
36
|
+
const { globalMode } = options || {};
|
|
37
|
+
if (globalMode) {
|
|
38
|
+
const os = require("os");
|
|
39
|
+
const globalChatRoot = path.join(os.homedir(), ".ufoo", "chat");
|
|
40
|
+
const globalDir = path.join(globalChatRoot, "global-history");
|
|
41
|
+
const projectId = projectRootToId(projectRoot);
|
|
42
|
+
return path.join(globalDir, `${projectId}.jsonl`);
|
|
43
|
+
}
|
|
44
|
+
return path.join(getUfooPaths(projectRoot || process.cwd()).ufooDir, "chat", "history.jsonl");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function normalizeHistoryLogLines(text = "") {
|
|
48
|
+
const clean = stripBlessedTags(text);
|
|
49
|
+
return clean.split(/\r?\n/);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function loadChatHistory(projectRoot, cap = 200, options = {}) {
|
|
53
|
+
const file = chatHistoryFilePath(projectRoot, options);
|
|
54
|
+
try {
|
|
55
|
+
if (!fs.existsSync(file)) return [];
|
|
56
|
+
const raw = fs.readFileSync(file, "utf8");
|
|
57
|
+
const lines = raw.split(/\r?\n/).filter(Boolean);
|
|
58
|
+
const out = [];
|
|
59
|
+
const pushLine = (line = "", sourceType = "") => {
|
|
60
|
+
const value = String(line || "");
|
|
61
|
+
if (!value.trim()) {
|
|
62
|
+
if (out.length > 0) {
|
|
63
|
+
const last = out[out.length - 1];
|
|
64
|
+
const lastText = typeof last === "object" ? last.text : last;
|
|
65
|
+
if (lastText !== "") out.push({ text: "", sourceType: sourceType || "system" });
|
|
66
|
+
}
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
out.push(sourceType ? { text: value, sourceType } : value);
|
|
70
|
+
};
|
|
71
|
+
for (const line of lines) {
|
|
72
|
+
try {
|
|
73
|
+
const entry = JSON.parse(line);
|
|
74
|
+
if (!entry) continue;
|
|
75
|
+
if (entry.type === "spacer") {
|
|
76
|
+
pushLine("", "system");
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const text = String(entry.text || "");
|
|
80
|
+
if (!text) continue;
|
|
81
|
+
const sourceType = String(entry.type || "");
|
|
82
|
+
const stripped = text.replace(/\{[^{}]+\}/g, "");
|
|
83
|
+
for (const renderedLine of normalizeHistoryLogLines(stripped)) {
|
|
84
|
+
pushLine(renderedLine, sourceType);
|
|
85
|
+
}
|
|
86
|
+
} catch {
|
|
87
|
+
// ignore malformed lines
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
while (out.length > 0) {
|
|
91
|
+
const first = out[0];
|
|
92
|
+
const firstText = typeof first === "object" ? first.text : first;
|
|
93
|
+
if (firstText !== "") break;
|
|
94
|
+
out.shift();
|
|
95
|
+
}
|
|
96
|
+
while (out.length > 0) {
|
|
97
|
+
const last = out[out.length - 1];
|
|
98
|
+
const lastText = typeof last === "object" ? last.text : last;
|
|
99
|
+
if (lastText !== "") break;
|
|
100
|
+
out.pop();
|
|
101
|
+
}
|
|
102
|
+
const capped = out.slice(-cap);
|
|
103
|
+
while (capped.length > 0) {
|
|
104
|
+
const first = capped[0];
|
|
105
|
+
const firstText = typeof first === "object" ? first.text : first;
|
|
106
|
+
if (firstText !== "") break;
|
|
107
|
+
capped.shift();
|
|
108
|
+
}
|
|
109
|
+
return capped;
|
|
110
|
+
} catch {
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function loadInputHistory(projectRoot, cap = 200, options = {}) {
|
|
116
|
+
const file = inputHistoryFilePath(projectRoot, options);
|
|
117
|
+
try {
|
|
118
|
+
if (!fs.existsSync(file)) return [];
|
|
119
|
+
const raw = fs.readFileSync(file, "utf8");
|
|
120
|
+
const lines = raw.split(/\r?\n/).filter(Boolean);
|
|
121
|
+
const out = [];
|
|
122
|
+
for (const line of lines) {
|
|
123
|
+
try {
|
|
124
|
+
const obj = JSON.parse(line);
|
|
125
|
+
const value = String((obj && obj.value) || "").trim();
|
|
126
|
+
if (value) out.push(value);
|
|
127
|
+
} catch {
|
|
128
|
+
// ignore malformed entries
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return out.slice(-cap);
|
|
132
|
+
} catch {
|
|
133
|
+
return [];
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function appendInputHistory(projectRoot, value, options = {}) {
|
|
138
|
+
const trimmed = String(value || "").trim();
|
|
139
|
+
if (!trimmed) return;
|
|
140
|
+
const file = inputHistoryFilePath(projectRoot, options);
|
|
141
|
+
try {
|
|
142
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
143
|
+
fs.appendFileSync(file, `${JSON.stringify({ value: trimmed, ts: Date.now() })}\n`);
|
|
144
|
+
} catch {
|
|
145
|
+
// best-effort
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function appendChatHistory(projectRoot, type, text, meta = {}, options = {}) {
|
|
150
|
+
const value = String(text || "");
|
|
151
|
+
if (!value && type !== "spacer") return;
|
|
152
|
+
const file = chatHistoryFilePath(projectRoot, options);
|
|
153
|
+
try {
|
|
154
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
155
|
+
fs.appendFileSync(file, `${JSON.stringify({
|
|
156
|
+
ts: new Date().toISOString(),
|
|
157
|
+
type,
|
|
158
|
+
text: value,
|
|
159
|
+
meta: meta && typeof meta === "object" ? meta : {},
|
|
160
|
+
})}\n`);
|
|
161
|
+
} catch {
|
|
162
|
+
// best-effort
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function chatHistoryOptionsForScope({ globalMode = false, globalScope = "controller" } = {}) {
|
|
167
|
+
return {
|
|
168
|
+
globalMode: Boolean(globalMode && globalScope !== "project"),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
module.exports = {
|
|
173
|
+
projectRootToId,
|
|
174
|
+
inputHistoryFilePath,
|
|
175
|
+
chatHistoryFilePath,
|
|
176
|
+
loadChatHistory,
|
|
177
|
+
loadInputHistory,
|
|
178
|
+
appendInputHistory,
|
|
179
|
+
appendChatHistory,
|
|
180
|
+
chatHistoryOptionsForScope,
|
|
181
|
+
};
|
package/src/app/chat/index.js
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
async function runChat(projectRoot, options = {}) {
|
|
2
|
-
const {
|
|
3
|
-
|
|
2
|
+
const { resolveTuiLaunchPlan } = require("../../ui/tuiLauncher");
|
|
3
|
+
const plan = resolveTuiLaunchPlan({
|
|
4
|
+
mode: options.tuiMode || process.env.UFOO_TUI,
|
|
5
|
+
});
|
|
6
|
+
|
|
7
|
+
if (plan.mode !== "rust") {
|
|
8
|
+
const err = new Error(`Rust TUI unavailable (${plan.reason})`);
|
|
9
|
+
err.code = "UFOO_TUI_UNAVAILABLE";
|
|
10
|
+
err.plan = plan;
|
|
11
|
+
throw err;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const { runChatRust } = require("../../ui/rustChatHost");
|
|
15
|
+
return runChatRust(projectRoot, { ...options, tuiMode: "rust" });
|
|
4
16
|
}
|
|
5
17
|
|
|
6
18
|
module.exports = { runChat };
|
|
@@ -20,6 +20,9 @@ function createInputSubmitHandler(options = {}) {
|
|
|
20
20
|
enterAgentView = () => {},
|
|
21
21
|
getAgentAdapter = () => null,
|
|
22
22
|
activateAgent = async () => {},
|
|
23
|
+
// When /multi is on: focus the in-window TUI pane instead of OS activate.
|
|
24
|
+
// Return true if handled.
|
|
25
|
+
focusMultiPane = null,
|
|
23
26
|
commitInputHistory = () => {},
|
|
24
27
|
focusInput = () => {},
|
|
25
28
|
renderScreen = () => {}, // Add renderScreen callback
|
|
@@ -39,6 +42,18 @@ function createInputSubmitHandler(options = {}) {
|
|
|
39
42
|
}
|
|
40
43
|
|
|
41
44
|
async function tryActivateTargetAgent(agentId) {
|
|
45
|
+
if (typeof focusMultiPane === "function") {
|
|
46
|
+
try {
|
|
47
|
+
const focused = await focusMultiPane(agentId);
|
|
48
|
+
if (focused) {
|
|
49
|
+
clearTargetAgent();
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
} catch {
|
|
53
|
+
// Fall through to OS activate / internal view.
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
42
57
|
const adapter = getAgentAdapter(agentId);
|
|
43
58
|
const capabilities = adapter && adapter.capabilities ? adapter.capabilities : null;
|
|
44
59
|
const supportsActivate = Boolean(capabilities && capabilities.supportsActivate);
|
|
@@ -47,13 +62,12 @@ function createInputSubmitHandler(options = {}) {
|
|
|
47
62
|
if (supportsActivate) {
|
|
48
63
|
clearTargetAgent();
|
|
49
64
|
try {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
65
|
+
// Always use the host-wired activator. Bare adapterRouter adapters
|
|
66
|
+
// expose activate() that no-ops unless activateTerminal/activateTmux
|
|
67
|
+
// were injected — which chat hosts do not do.
|
|
68
|
+
const pendingActivation = activateAgent(agentId);
|
|
69
|
+
if (pendingActivation && typeof pendingActivation.catch === "function") {
|
|
70
|
+
pendingActivation.catch(() => {});
|
|
57
71
|
}
|
|
58
72
|
} catch {
|
|
59
73
|
// Best-effort activation.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Chat → daemon IPC request builders (Phase 0B extraction).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { resolveActiveAgentId } = require("./agentIdentity");
|
|
8
|
+
|
|
9
|
+
function buildDirectBusSendRequest({
|
|
10
|
+
text,
|
|
11
|
+
targetAgentId = null,
|
|
12
|
+
activeAgents = [],
|
|
13
|
+
activeAgentMeta = new Map(),
|
|
14
|
+
} = {}) {
|
|
15
|
+
const trimmed = String(text || "").trim();
|
|
16
|
+
if (!trimmed) return null;
|
|
17
|
+
if (targetAgentId) {
|
|
18
|
+
return {
|
|
19
|
+
target: targetAgentId,
|
|
20
|
+
message: trimmed,
|
|
21
|
+
source: "chat-direct",
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const { parseAtTarget } = require("./commands");
|
|
26
|
+
const atTarget = parseAtTarget(trimmed);
|
|
27
|
+
if (!atTarget || !atTarget.message) return null;
|
|
28
|
+
const target = resolveActiveAgentId(atTarget.target, activeAgents, activeAgentMeta) || atTarget.target;
|
|
29
|
+
return {
|
|
30
|
+
target,
|
|
31
|
+
message: atTarget.message.trim(),
|
|
32
|
+
source: "chat-direct",
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function buildPromptIpcRequest(text) {
|
|
37
|
+
const { IPC_REQUEST_TYPES } = require("../../runtime/contracts/eventContract");
|
|
38
|
+
return {
|
|
39
|
+
type: IPC_REQUEST_TYPES.PROMPT,
|
|
40
|
+
text,
|
|
41
|
+
request_meta: {
|
|
42
|
+
source: "chat-dialog",
|
|
43
|
+
dispatch_default_injection_mode: "immediate",
|
|
44
|
+
allow_relevance_queue: true,
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = {
|
|
50
|
+
buildDirectBusSendRequest,
|
|
51
|
+
buildPromptIpcRequest,
|
|
52
|
+
};
|
|
@@ -139,7 +139,15 @@ function createPaneManager(options = {}) {
|
|
|
139
139
|
|
|
140
140
|
function sendInput(data) {
|
|
141
141
|
if (!focusedAgent) return;
|
|
142
|
-
|
|
142
|
+
sendInputToPane(panes.get(focusedAgent), data);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function sendInputToAgent(agentId, data) {
|
|
146
|
+
if (!agentId) return;
|
|
147
|
+
sendInputToPane(panes.get(agentId), data);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function sendInputToPane(pane, data) {
|
|
143
151
|
if (!pane) return;
|
|
144
152
|
if (pane.mode === "internal") {
|
|
145
153
|
handleInternalInput(pane, data);
|
|
@@ -284,6 +292,7 @@ function createPaneManager(options = {}) {
|
|
|
284
292
|
addAgent,
|
|
285
293
|
removeAgent,
|
|
286
294
|
sendInput,
|
|
295
|
+
sendInputToAgent,
|
|
287
296
|
sendResize,
|
|
288
297
|
cycleFocus,
|
|
289
298
|
getFocused,
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Convert a virtual-terminal screen into ANSI-styled text lines suitable for
|
|
5
|
+
* shipping over `multi.pane.frame`. Rust decodes these with `ansi-to-tui`.
|
|
6
|
+
*
|
|
7
|
+
* The colour/attribute-to-ANSI logic mirrors renderer.js so the rendered
|
|
8
|
+
* frame matches the Ink multi-window presentation.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const MAX_LINE_BYTES = 4096;
|
|
12
|
+
|
|
13
|
+
function attrToAnsi(attr) {
|
|
14
|
+
const parts = [];
|
|
15
|
+
if (attr.bold) parts.push("1");
|
|
16
|
+
if (attr.dim) parts.push("2");
|
|
17
|
+
if (attr.italic) parts.push("3");
|
|
18
|
+
if (attr.underline) parts.push("4");
|
|
19
|
+
if (attr.inverse) parts.push("7");
|
|
20
|
+
if (attr.fgRgb) {
|
|
21
|
+
parts.push(`38;2;${attr.fgRgb[0]};${attr.fgRgb[1]};${attr.fgRgb[2]}`);
|
|
22
|
+
} else if (attr.fg !== 7) {
|
|
23
|
+
if (attr.fg < 8) parts.push(String(30 + attr.fg));
|
|
24
|
+
else if (attr.fg < 16) parts.push(String(90 + attr.fg - 8));
|
|
25
|
+
else parts.push(`38;5;${attr.fg}`);
|
|
26
|
+
}
|
|
27
|
+
if (attr.bgRgb) {
|
|
28
|
+
parts.push(`48;2;${attr.bgRgb[0]};${attr.bgRgb[1]};${attr.bgRgb[2]}`);
|
|
29
|
+
} else if (attr.bg > 0) {
|
|
30
|
+
if (attr.bg < 8) parts.push(String(40 + attr.bg));
|
|
31
|
+
else if (attr.bg < 16) parts.push(String(100 + attr.bg - 8));
|
|
32
|
+
else parts.push(`48;5;${attr.bg}`);
|
|
33
|
+
}
|
|
34
|
+
return parts.length > 0 ? `\x1b[${parts.join(";")}m` : "";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function truncateLineBytes(line, maxBytes) {
|
|
38
|
+
if (!line) return "";
|
|
39
|
+
if (Buffer.byteLength(line, "utf8") <= maxBytes) return line;
|
|
40
|
+
const buf = Buffer.from(line, "utf8");
|
|
41
|
+
return buf.slice(0, maxBytes).toString("utf8");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function vtScreenToAnsiLines(vt, options = {}) {
|
|
45
|
+
if (!vt || typeof vt.getScreen !== "function") return [];
|
|
46
|
+
const { buffer, rows, cols, cursorRow, cursorCol } = vt.getScreen();
|
|
47
|
+
const maxRows = Number.isFinite(options.maxRows) && options.maxRows > 0
|
|
48
|
+
? Math.floor(options.maxRows)
|
|
49
|
+
: rows;
|
|
50
|
+
const maxCols = Number.isFinite(options.maxCols) && options.maxCols > 0
|
|
51
|
+
? Math.floor(options.maxCols)
|
|
52
|
+
: cols;
|
|
53
|
+
const cursorInverse = Boolean(options.cursorInverse);
|
|
54
|
+
const reset = "\x1b[0m";
|
|
55
|
+
const limit = Math.min(rows, maxRows);
|
|
56
|
+
const out = [];
|
|
57
|
+
for (let r = 0; r < limit; r++) {
|
|
58
|
+
const row = buffer[r] || [];
|
|
59
|
+
let line = "";
|
|
60
|
+
let lastAttr = "";
|
|
61
|
+
let col = 0;
|
|
62
|
+
let c = 0;
|
|
63
|
+
while (col < maxCols && c < cols) {
|
|
64
|
+
const cell = row[c];
|
|
65
|
+
if (!cell) {
|
|
66
|
+
line += " ";
|
|
67
|
+
col += 1;
|
|
68
|
+
c += 1;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (cell.wideContinuation) {
|
|
72
|
+
c += 1;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const isCursor = cursorInverse && r === cursorRow && c === cursorCol;
|
|
76
|
+
const attr = isCursor ? { ...cell.attr, inverse: !cell.attr.inverse } : cell.attr;
|
|
77
|
+
const ansi = attrToAnsi(attr);
|
|
78
|
+
if (ansi !== lastAttr) {
|
|
79
|
+
if (lastAttr) line += reset;
|
|
80
|
+
line += ansi;
|
|
81
|
+
lastAttr = ansi;
|
|
82
|
+
}
|
|
83
|
+
line += cell.char || " ";
|
|
84
|
+
col += 1;
|
|
85
|
+
c += 1;
|
|
86
|
+
}
|
|
87
|
+
if (lastAttr) line += reset;
|
|
88
|
+
out.push(truncateLineBytes(line, MAX_LINE_BYTES));
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = { vtScreenToAnsiLines, MAX_LINE_BYTES };
|