u-foo 3.0.9 → 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 +12 -4
- 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 +38 -1
- 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 -4152
- 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
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Mirror inbound bus events into multi/side internal panes.
|
|
5
|
+
* Rust host uses this so pane VT stays live while the main transcript
|
|
6
|
+
* router still runs.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
function parseInternalBusPayload(raw = "") {
|
|
10
|
+
let displayMessage = String(raw || "");
|
|
11
|
+
let streamPayload = null;
|
|
12
|
+
try {
|
|
13
|
+
const parsed = JSON.parse(raw);
|
|
14
|
+
if (parsed && typeof parsed === "object" && parsed.reply) {
|
|
15
|
+
displayMessage = parsed.reply;
|
|
16
|
+
} else if (parsed && typeof parsed === "object" && parsed.stream) {
|
|
17
|
+
streamPayload = parsed;
|
|
18
|
+
}
|
|
19
|
+
} catch {
|
|
20
|
+
// Plain text.
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
displayMessage: String(displayMessage || "")
|
|
24
|
+
.replace(/\\r\\n/g, "\n")
|
|
25
|
+
.replace(/\\n/g, "\n")
|
|
26
|
+
.replace(/\\r/g, "\n"),
|
|
27
|
+
streamPayload,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function internalStatusLabel(value = "") {
|
|
32
|
+
const state = String(value || "").trim().toLowerCase();
|
|
33
|
+
if (state === "waiting" || state === "waiting_input") return "waiting";
|
|
34
|
+
if (state === "blocked" || state === "error") return "blocked";
|
|
35
|
+
if (state === "busy" || state === "processing" || state === "working") return "working";
|
|
36
|
+
if (state === "idle" || state === "ready") return "ready";
|
|
37
|
+
return state || "ready";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function buildAgentAliases(agentId, getMeta = () => ({})) {
|
|
41
|
+
const meta = (() => {
|
|
42
|
+
try { return getMeta(agentId) || {}; } catch { return {}; }
|
|
43
|
+
})();
|
|
44
|
+
return new Set([
|
|
45
|
+
agentId,
|
|
46
|
+
meta.nickname,
|
|
47
|
+
meta.scoped_nickname,
|
|
48
|
+
meta.display_nickname,
|
|
49
|
+
meta.fullId,
|
|
50
|
+
meta.id,
|
|
51
|
+
].filter(Boolean).map(String));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @param {object} data — bus event payload (msg.data)
|
|
56
|
+
* @param {object} options
|
|
57
|
+
* @param {Iterable<string>|Set<string>} options.agentIds — internal pane agents to mirror
|
|
58
|
+
* @param {(agentId: string) => object} [options.getMeta]
|
|
59
|
+
* @param {(agentId: string, text: string) => boolean|void} options.writeToPane
|
|
60
|
+
* @returns {boolean} true if at least one pane consumed the event
|
|
61
|
+
*/
|
|
62
|
+
function writeMultiPaneBusEvent(data = {}, options = {}) {
|
|
63
|
+
const {
|
|
64
|
+
agentIds = [],
|
|
65
|
+
getMeta = () => ({}),
|
|
66
|
+
writeToPane = () => false,
|
|
67
|
+
} = options;
|
|
68
|
+
|
|
69
|
+
const watched = agentIds instanceof Set
|
|
70
|
+
? agentIds
|
|
71
|
+
: new Set([...agentIds].map((id) => String(id || "").trim()).filter(Boolean));
|
|
72
|
+
if (!watched.size || typeof writeToPane !== "function") return false;
|
|
73
|
+
|
|
74
|
+
let handled = false;
|
|
75
|
+
for (const agentId of watched) {
|
|
76
|
+
const aliases = buildAgentAliases(agentId, getMeta);
|
|
77
|
+
const publisher = String(data.publisher || (data.event === "broadcast" ? "broadcast" : "bus"));
|
|
78
|
+
const target = String(data.target || data.subscriber || "");
|
|
79
|
+
const fromAgent = aliases.has(publisher);
|
|
80
|
+
const toAgent = aliases.has(target) || aliases.has(String(data.subscriber || ""));
|
|
81
|
+
if (!fromAgent && !toAgent) continue;
|
|
82
|
+
|
|
83
|
+
if (data.silent) {
|
|
84
|
+
handled = true;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
// Echo of chat→agent sends already shown as local "> " input.
|
|
88
|
+
if ((data.source === "chat-internal-agent-view" || data.source === "rust-multi-window"
|
|
89
|
+
|| data.source === "chat-direct") && toAgent && !fromAgent) {
|
|
90
|
+
handled = true;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (data.event === "activity_state_changed") {
|
|
94
|
+
const state = internalStatusLabel(data.state || data.activity_state || "");
|
|
95
|
+
const detail = String(data.detail || (data.data && data.data.detail) || data.message || "").trim();
|
|
96
|
+
try {
|
|
97
|
+
writeToPane(agentId, `\r\n[${state}${detail ? ` · ${detail}` : ""}]\r\n`);
|
|
98
|
+
} catch { /* ignore */ }
|
|
99
|
+
handled = true;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const { displayMessage, streamPayload } = parseInternalBusPayload(data.message || "");
|
|
104
|
+
if (streamPayload) {
|
|
105
|
+
if (!fromAgent) {
|
|
106
|
+
handled = true;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const delta = typeof streamPayload.delta === "string"
|
|
110
|
+
? streamPayload.delta.replace(/\\r\\n/g, "\n").replace(/\\n/g, "\n").replace(/\\r/g, "\n")
|
|
111
|
+
: "";
|
|
112
|
+
try {
|
|
113
|
+
if (delta) writeToPane(agentId, delta);
|
|
114
|
+
if (streamPayload.done) writeToPane(agentId, "\r\n");
|
|
115
|
+
} catch { /* ignore */ }
|
|
116
|
+
handled = true;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (!displayMessage) {
|
|
120
|
+
handled = true;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const prefix = fromAgent ? "* " : "> ";
|
|
124
|
+
try {
|
|
125
|
+
writeToPane(agentId, `${prefix}${displayMessage.replace(/\n/g, "\r\n ")}\r\n`);
|
|
126
|
+
} catch { /* ignore */ }
|
|
127
|
+
handled = true;
|
|
128
|
+
}
|
|
129
|
+
return handled;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
module.exports = {
|
|
133
|
+
parseInternalBusPayload,
|
|
134
|
+
internalStatusLabel,
|
|
135
|
+
buildAgentAliases,
|
|
136
|
+
writeMultiPaneBusEvent,
|
|
137
|
+
};
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Multi-window handoff after Rust TUI suspend (exit 75, reason=multi).
|
|
5
|
+
* Reuses createMultiWindowController (same VT/pane code as Ink).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { createMultiWindowController } = require("../app/chat/multiWindow");
|
|
9
|
+
const { resolveAgentEnterRequest, resolveInjectSockPathForAgent } = require("../app/chat/agentEnter");
|
|
10
|
+
const { restoreStdinAfterHandoff } = require("./ptyHandoff");
|
|
11
|
+
|
|
12
|
+
function agentIdsFromMeta(activeAgentMeta) {
|
|
13
|
+
if (activeAgentMeta instanceof Map) {
|
|
14
|
+
return Array.from(activeAgentMeta.keys()).map(String).filter(Boolean);
|
|
15
|
+
}
|
|
16
|
+
if (activeAgentMeta && typeof activeAgentMeta === "object") {
|
|
17
|
+
return Object.keys(activeAgentMeta);
|
|
18
|
+
}
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function labelForAgent(activeAgentMeta, agentId) {
|
|
23
|
+
const meta = activeAgentMeta instanceof Map
|
|
24
|
+
? activeAgentMeta.get(agentId)
|
|
25
|
+
: (activeAgentMeta && activeAgentMeta[agentId]);
|
|
26
|
+
if (!meta || typeof meta !== "object") return agentId;
|
|
27
|
+
return String(meta.display_nickname || meta.nickname || meta.scoped_nickname || agentId);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @returns {Promise<{ ok: boolean, error?: string }>}
|
|
32
|
+
*/
|
|
33
|
+
async function runMultiWindowHandoff({
|
|
34
|
+
projectRoot = process.cwd(),
|
|
35
|
+
activeAgentMeta = new Map(),
|
|
36
|
+
settings = {},
|
|
37
|
+
getChatLogLines = () => [],
|
|
38
|
+
getStatusText = () => "",
|
|
39
|
+
getDashboardLines = () => [],
|
|
40
|
+
onInternalSubmit = null,
|
|
41
|
+
stdin = process.stdin,
|
|
42
|
+
stdout = process.stdout,
|
|
43
|
+
} = {}) {
|
|
44
|
+
const agents = agentIdsFromMeta(activeAgentMeta);
|
|
45
|
+
if (agents.length === 0) {
|
|
46
|
+
return { ok: false, error: "No active agents for multi-window mode" };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let terminalFocused = false;
|
|
50
|
+
let done = false;
|
|
51
|
+
let resolveDone = null;
|
|
52
|
+
const finished = new Promise((resolve) => {
|
|
53
|
+
resolveDone = resolve;
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const originalWrite = stdout.write.bind(stdout);
|
|
57
|
+
const controller = createMultiWindowController({
|
|
58
|
+
processStdout: {
|
|
59
|
+
write: originalWrite,
|
|
60
|
+
rows: stdout.rows,
|
|
61
|
+
columns: stdout.columns,
|
|
62
|
+
},
|
|
63
|
+
getRows: () => stdout.rows || 24,
|
|
64
|
+
getCols: () => stdout.columns || 80,
|
|
65
|
+
getInjectSockPath: (agentId) => resolveInjectSockPathForAgent(projectRoot, agentId),
|
|
66
|
+
getActiveAgents: () => agentIdsFromMeta(activeAgentMeta),
|
|
67
|
+
getAgentPaneOptions: (agentId) => {
|
|
68
|
+
const enterRequest = resolveAgentEnterRequest({
|
|
69
|
+
agentId,
|
|
70
|
+
projectRoot,
|
|
71
|
+
activeAgentMeta,
|
|
72
|
+
settings,
|
|
73
|
+
});
|
|
74
|
+
if (!enterRequest || !enterRequest.useBus) return { mode: "socket" };
|
|
75
|
+
let initialLines = [];
|
|
76
|
+
try {
|
|
77
|
+
const { loadInternalAgentLogHistory } = require("../app/chat/internalAgentLogHistory");
|
|
78
|
+
initialLines = loadInternalAgentLogHistory(projectRoot, agentId, {
|
|
79
|
+
maxEvents: 200,
|
|
80
|
+
maxLines: 200,
|
|
81
|
+
});
|
|
82
|
+
} catch {
|
|
83
|
+
initialLines = [];
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
mode: "internal",
|
|
87
|
+
initialLines: [
|
|
88
|
+
`ufoo internal agent · ${labelForAgent(activeAgentMeta, agentId)}`,
|
|
89
|
+
`agent: ${agentId}`,
|
|
90
|
+
"",
|
|
91
|
+
...initialLines,
|
|
92
|
+
],
|
|
93
|
+
};
|
|
94
|
+
},
|
|
95
|
+
getChatLogLines,
|
|
96
|
+
getStatusText,
|
|
97
|
+
getPromptPrefix: () => "› ",
|
|
98
|
+
getCurrentDraft: () => "",
|
|
99
|
+
getCursorPos: () => 0,
|
|
100
|
+
getCompletions: () => ({ items: [], index: -1, windowStart: 0, pageSize: 8 }),
|
|
101
|
+
getAgentLabel: (id) => labelForAgent(activeAgentMeta, id),
|
|
102
|
+
getInternalPaneInfo: (id) => {
|
|
103
|
+
const meta = activeAgentMeta instanceof Map
|
|
104
|
+
? activeAgentMeta.get(id)
|
|
105
|
+
: (activeAgentMeta && activeAgentMeta[id]);
|
|
106
|
+
return {
|
|
107
|
+
status: String((meta && meta.activity_state) || ""),
|
|
108
|
+
detail: String((meta && meta.activity_detail) || ""),
|
|
109
|
+
input: "",
|
|
110
|
+
cursor: 0,
|
|
111
|
+
};
|
|
112
|
+
},
|
|
113
|
+
getDashboardLines,
|
|
114
|
+
getTerminalFocused: () => terminalFocused,
|
|
115
|
+
freezeScreen: (frozen) => {
|
|
116
|
+
if (frozen) stdout.write = () => true;
|
|
117
|
+
else stdout.write = originalWrite;
|
|
118
|
+
},
|
|
119
|
+
restoreTerminal: () => {
|
|
120
|
+
const rows = stdout.rows || 24;
|
|
121
|
+
originalWrite(`\x1b[1;${rows}r`);
|
|
122
|
+
originalWrite("\x1b[2J\x1b[H");
|
|
123
|
+
},
|
|
124
|
+
onInternalSubmit: (agentId, message) => {
|
|
125
|
+
if (typeof onInternalSubmit === "function") {
|
|
126
|
+
onInternalSubmit(agentId, message);
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
onExit: () => {
|
|
130
|
+
done = true;
|
|
131
|
+
if (typeof resolveDone === "function") resolveDone();
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
if (!controller.enter()) {
|
|
136
|
+
return { ok: false, error: "No active agents for multi-window mode" };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const wasRaw = typeof stdin.isRaw === "boolean" ? stdin.isRaw : false;
|
|
140
|
+
try {
|
|
141
|
+
if (typeof stdin.setRawMode === "function" && stdin.isTTY) {
|
|
142
|
+
stdin.setRawMode(true);
|
|
143
|
+
}
|
|
144
|
+
if (typeof stdin.resume === "function") stdin.resume();
|
|
145
|
+
} catch {
|
|
146
|
+
// ignore
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const onResize = () => {
|
|
150
|
+
try {
|
|
151
|
+
controller.handleResize();
|
|
152
|
+
} catch {
|
|
153
|
+
// ignore
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
if (stdout && typeof stdout.on === "function") {
|
|
157
|
+
stdout.on("resize", onResize);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const onData = (chunk) => {
|
|
161
|
+
if (done) return;
|
|
162
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk || ""), "utf8");
|
|
163
|
+
if (buf.length === 0) return;
|
|
164
|
+
|
|
165
|
+
// Ctrl+C → exit multi
|
|
166
|
+
if (buf.includes(0x03)) {
|
|
167
|
+
controller.exit();
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
// Ctrl+Q → exit multi
|
|
171
|
+
if (buf.includes(0x11)) {
|
|
172
|
+
controller.handleKey({ name: "q", ctrl: true, sequence: "" });
|
|
173
|
+
terminalFocused = false;
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
// Ctrl+W → cycle focus (chat chrome ↔ agent panes)
|
|
177
|
+
if (buf.includes(0x17)) {
|
|
178
|
+
const ids = controller.getAgentIds();
|
|
179
|
+
if (ids.length === 0) return;
|
|
180
|
+
if (!terminalFocused) {
|
|
181
|
+
controller.focusAgent(ids[0]);
|
|
182
|
+
terminalFocused = true;
|
|
183
|
+
} else {
|
|
184
|
+
const current = controller.getFocused();
|
|
185
|
+
const idx = current ? ids.indexOf(current) : -1;
|
|
186
|
+
if (idx >= 0 && idx < ids.length - 1) {
|
|
187
|
+
controller.focusAgent(ids[idx + 1]);
|
|
188
|
+
} else {
|
|
189
|
+
terminalFocused = false;
|
|
190
|
+
controller.focusAgent(ids[0]);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
controller.renderAll();
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (terminalFocused) {
|
|
198
|
+
controller.sendInput(buf.toString("utf8"));
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
stdin.on("data", onData);
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
await finished;
|
|
206
|
+
return { ok: true };
|
|
207
|
+
} finally {
|
|
208
|
+
stdin.removeListener("data", onData);
|
|
209
|
+
if (stdout && typeof stdout.off === "function") {
|
|
210
|
+
stdout.off("resize", onResize);
|
|
211
|
+
} else if (stdout && typeof stdout.removeListener === "function") {
|
|
212
|
+
stdout.removeListener("resize", onResize);
|
|
213
|
+
}
|
|
214
|
+
try {
|
|
215
|
+
if (controller.isActive()) controller.exit();
|
|
216
|
+
} catch {
|
|
217
|
+
// ignore
|
|
218
|
+
}
|
|
219
|
+
try {
|
|
220
|
+
if (typeof stdin.setRawMode === "function" && stdin.isTTY) {
|
|
221
|
+
stdin.setRawMode(wasRaw);
|
|
222
|
+
}
|
|
223
|
+
} catch {
|
|
224
|
+
// ignore
|
|
225
|
+
}
|
|
226
|
+
restoreStdinAfterHandoff(stdin);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
module.exports = {
|
|
231
|
+
runMultiWindowHandoff,
|
|
232
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Stdin restore helpers after any fullscreen handoff (e.g. legacy multi).
|
|
5
|
+
* Agent PTY mirror handoff has been removed — activate / side / multi only.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
function restoreStdinAfterHandoff(stdin = process.stdin) {
|
|
9
|
+
try {
|
|
10
|
+
if (stdin && typeof stdin.setRawMode === "function" && stdin.isTTY) {
|
|
11
|
+
stdin.setRawMode(false);
|
|
12
|
+
}
|
|
13
|
+
if (stdin && typeof stdin.resume === "function") {
|
|
14
|
+
stdin.resume();
|
|
15
|
+
}
|
|
16
|
+
} catch {
|
|
17
|
+
// ignore — next TUI will reconfigure
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
module.exports = {
|
|
22
|
+
restoreStdinAfterHandoff,
|
|
23
|
+
};
|