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,497 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* In-process split-pane session for the Rust TUI host.
|
|
5
|
+
*
|
|
6
|
+
* Two kinds share the same draw/keys (Ctrl+W / Ctrl+Q) and multi.* wire:
|
|
7
|
+
* - "multi" — /multi; panes track all active agents
|
|
8
|
+
* - "side" — internal-only activate; single locked agent on the right
|
|
9
|
+
* (not a user-facing mode name; looks like multi with one agent)
|
|
10
|
+
*
|
|
11
|
+
* The Rust TUI owns TTY / layout / focus. Node owns paneManager (VT +
|
|
12
|
+
* inject.sock) and ships bounded, lossy `multi.pane.frame` events.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const crypto = require("crypto");
|
|
16
|
+
const { createPaneManager } = require("../app/chat/multiWindow/paneManager");
|
|
17
|
+
const { vtScreenToAnsiLines } = require("../app/chat/multiWindow/vtFrame");
|
|
18
|
+
|
|
19
|
+
const DEFAULT_COLS = 40;
|
|
20
|
+
const DEFAULT_ROWS = 12;
|
|
21
|
+
const FLUSH_MS = 60;
|
|
22
|
+
const MAX_FLUSH_MS = 140;
|
|
23
|
+
const MIN_EMIT_GAP_MS = 35;
|
|
24
|
+
const KIND_MULTI = "multi";
|
|
25
|
+
const KIND_SIDE = "side";
|
|
26
|
+
|
|
27
|
+
function generateSessionId(kind = KIND_MULTI) {
|
|
28
|
+
const rand = crypto.randomBytes(4).toString("hex");
|
|
29
|
+
const prefix = kind === KIND_SIDE ? "side" : "multi";
|
|
30
|
+
return `${prefix}-${Date.now().toString(36)}-${rand}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function normalizeAgentList(getActiveAgents) {
|
|
34
|
+
try {
|
|
35
|
+
const raw = typeof getActiveAgents === "function" ? getActiveAgents() : [];
|
|
36
|
+
if (!Array.isArray(raw)) return [];
|
|
37
|
+
const seen = new Set();
|
|
38
|
+
const out = [];
|
|
39
|
+
for (const item of raw) {
|
|
40
|
+
const id = String(item || "").trim();
|
|
41
|
+
if (!id || seen.has(id)) continue;
|
|
42
|
+
seen.add(id);
|
|
43
|
+
out.push(id);
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
} catch {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function createRustMultiSession(options = {}) {
|
|
52
|
+
const {
|
|
53
|
+
getActiveAgents = () => [],
|
|
54
|
+
getAgentMeta = () => ({}),
|
|
55
|
+
getInjectSockPath = () => "",
|
|
56
|
+
resolvePaneOptions = () => ({ mode: "socket" }),
|
|
57
|
+
onInternalSubmit = () => {},
|
|
58
|
+
publish = () => {},
|
|
59
|
+
publishLossy = null,
|
|
60
|
+
getLabel = (id) => id,
|
|
61
|
+
} = options;
|
|
62
|
+
|
|
63
|
+
let sessionId = null;
|
|
64
|
+
let active = false;
|
|
65
|
+
let kind = KIND_MULTI;
|
|
66
|
+
let lockedAgentIds = [];
|
|
67
|
+
let rev = 0;
|
|
68
|
+
let viewportRev = 0;
|
|
69
|
+
let focus = { target: "chat", agent_id: "" };
|
|
70
|
+
const paneSizes = new Map();
|
|
71
|
+
const paneMeta = new Map();
|
|
72
|
+
const dirty = new Set();
|
|
73
|
+
let flushTimer = null;
|
|
74
|
+
let paneManager = null;
|
|
75
|
+
/** @type {Map<string, number>} last successful frame emit per agent */
|
|
76
|
+
const lastEmitAt = new Map();
|
|
77
|
+
|
|
78
|
+
function paneAgentIds() {
|
|
79
|
+
if (kind === KIND_SIDE && lockedAgentIds.length > 0) {
|
|
80
|
+
return lockedAgentIds.slice();
|
|
81
|
+
}
|
|
82
|
+
return normalizeAgentList(getActiveAgents);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function currentPanesDesc() {
|
|
86
|
+
return paneAgentIds().map((id) => {
|
|
87
|
+
const opts = paneMeta.get(id) || (() => {
|
|
88
|
+
try { return resolvePaneOptions(id) || {}; } catch { return {}; }
|
|
89
|
+
})();
|
|
90
|
+
return {
|
|
91
|
+
agent_id: id,
|
|
92
|
+
label: String(getLabel(id) || id),
|
|
93
|
+
mode: opts.mode === "internal" ? "internal" : "socket",
|
|
94
|
+
};
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function ensurePaneMeta(id) {
|
|
99
|
+
if (paneMeta.has(id)) return paneMeta.get(id);
|
|
100
|
+
let opts = { mode: "socket" };
|
|
101
|
+
try {
|
|
102
|
+
opts = resolvePaneOptions(id) || opts;
|
|
103
|
+
} catch {
|
|
104
|
+
// ignore
|
|
105
|
+
}
|
|
106
|
+
if (!opts.mode) opts.mode = "socket";
|
|
107
|
+
paneMeta.set(id, opts);
|
|
108
|
+
return opts;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function scheduleFlush() {
|
|
112
|
+
if (!active) return;
|
|
113
|
+
if (flushTimer) return;
|
|
114
|
+
// Under load (many dirty panes), back off flush interval so we emit
|
|
115
|
+
// fewer full-screen snapshots while still latest-wins per pane.
|
|
116
|
+
const load = dirty.size;
|
|
117
|
+
const delay = load > 3
|
|
118
|
+
? Math.min(MAX_FLUSH_MS, FLUSH_MS + (load - 3) * 15)
|
|
119
|
+
: FLUSH_MS;
|
|
120
|
+
flushTimer = setTimeout(() => {
|
|
121
|
+
flushTimer = null;
|
|
122
|
+
flushDirty();
|
|
123
|
+
}, delay);
|
|
124
|
+
if (typeof flushTimer.unref === "function") flushTimer.unref();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function flushDirty() {
|
|
128
|
+
if (!active || !paneManager) return;
|
|
129
|
+
const ids = [...dirty];
|
|
130
|
+
dirty.clear();
|
|
131
|
+
const now = Date.now();
|
|
132
|
+
let deferred = false;
|
|
133
|
+
for (const id of ids) {
|
|
134
|
+
const last = lastEmitAt.get(id) || 0;
|
|
135
|
+
if (now - last < MIN_EMIT_GAP_MS) {
|
|
136
|
+
// Keep dirty; schedule another pass — drop intermediate by only
|
|
137
|
+
// re-emitting once the gap elapses (VT snapshot is always current).
|
|
138
|
+
dirty.add(id);
|
|
139
|
+
deferred = true;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
publishFrame(id);
|
|
143
|
+
lastEmitAt.set(id, now);
|
|
144
|
+
}
|
|
145
|
+
if (deferred) scheduleFlush();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function publishFrame(agentId) {
|
|
149
|
+
if (!active || !paneManager) return;
|
|
150
|
+
const pane = paneManager.getPane(agentId);
|
|
151
|
+
if (!pane) return;
|
|
152
|
+
const size = paneSizes.get(agentId) || { cols: DEFAULT_COLS, rows: DEFAULT_ROWS };
|
|
153
|
+
const lines = vtScreenToAnsiLines(pane.vt, {
|
|
154
|
+
maxCols: Math.max(1, size.cols),
|
|
155
|
+
maxRows: Math.max(1, size.rows),
|
|
156
|
+
});
|
|
157
|
+
const opts = paneMeta.get(agentId) || {};
|
|
158
|
+
const meta = (() => {
|
|
159
|
+
try { return getAgentMeta(agentId) || {}; } catch { return {}; }
|
|
160
|
+
})();
|
|
161
|
+
const payload = {
|
|
162
|
+
session_id: sessionId,
|
|
163
|
+
agent_id: agentId,
|
|
164
|
+
label: String(getLabel(agentId) || agentId),
|
|
165
|
+
mode: opts.mode === "internal" ? "internal" : "socket",
|
|
166
|
+
lines,
|
|
167
|
+
status: String(meta.activity_state || meta.state || "ready"),
|
|
168
|
+
viewport_rev: viewportRev,
|
|
169
|
+
};
|
|
170
|
+
if (opts.mode === "internal") {
|
|
171
|
+
payload.input = String(pane.internalInput || "");
|
|
172
|
+
payload.cursor = Number.isFinite(pane.internalCursor) ? pane.internalCursor : 0;
|
|
173
|
+
}
|
|
174
|
+
const emit = typeof publishLossy === "function" ? publishLossy : publish;
|
|
175
|
+
try { emit("multi.pane.frame", payload); } catch {}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function markDirty(agentId) {
|
|
179
|
+
if (!active) return;
|
|
180
|
+
dirty.add(agentId);
|
|
181
|
+
if (!paneManager || !paneManager.getPane(agentId)) return;
|
|
182
|
+
scheduleFlush();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function ensurePaneManager() {
|
|
186
|
+
if (paneManager) return paneManager;
|
|
187
|
+
paneManager = createPaneManager({
|
|
188
|
+
getInjectSockPath,
|
|
189
|
+
onPaneOutput: (agentId) => markDirty(agentId),
|
|
190
|
+
onInternalSubmit: (agentId, message) => {
|
|
191
|
+
try { onInternalSubmit(agentId, message); } catch {}
|
|
192
|
+
markDirty(agentId);
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
return paneManager;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function syncAgents() {
|
|
199
|
+
if (!paneManager) return;
|
|
200
|
+
const ids = paneAgentIds();
|
|
201
|
+
const existing = new Set(paneManager.getAgentIds());
|
|
202
|
+
let changed = false;
|
|
203
|
+
for (const id of ids) {
|
|
204
|
+
const opts = ensurePaneMeta(id);
|
|
205
|
+
const size = paneSizes.get(id) || { cols: DEFAULT_COLS, rows: DEFAULT_ROWS };
|
|
206
|
+
if (!existing.has(id)) {
|
|
207
|
+
paneManager.addAgent(id, size.cols, size.rows, opts);
|
|
208
|
+
markDirty(id);
|
|
209
|
+
changed = true;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
for (const id of existing) {
|
|
213
|
+
if (!ids.includes(id)) {
|
|
214
|
+
paneManager.removeAgent(id);
|
|
215
|
+
paneSizes.delete(id);
|
|
216
|
+
paneMeta.delete(id);
|
|
217
|
+
dirty.delete(id);
|
|
218
|
+
changed = true;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
// Only republish multi.set when membership actually changes. Spamming
|
|
222
|
+
// multi.set on every daemon status tick forced Rust to bump viewport_rev
|
|
223
|
+
// and drop in-flight pane frames (Ctrl+W felt stuck).
|
|
224
|
+
if (active && changed) {
|
|
225
|
+
rev += 1;
|
|
226
|
+
publishSet();
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function publishSet() {
|
|
231
|
+
const panes = currentPanesDesc();
|
|
232
|
+
// Focus fallback: if focused agent no longer exists, drop to chat.
|
|
233
|
+
if (focus.target === "agent") {
|
|
234
|
+
const stillActive = panes.some((p) => p.agent_id === focus.agent_id);
|
|
235
|
+
if (!stillActive) {
|
|
236
|
+
focus = { target: "chat", agent_id: "" };
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
publish("multi.set", {
|
|
240
|
+
session_id: sessionId,
|
|
241
|
+
active,
|
|
242
|
+
kind,
|
|
243
|
+
rev,
|
|
244
|
+
panes,
|
|
245
|
+
focus,
|
|
246
|
+
viewport_rev: viewportRev,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* @param {object} [options]
|
|
252
|
+
* @param {"multi"|"side"} [options.kind]
|
|
253
|
+
* @param {string[]} [options.agentIds] — required for kind "side" (exactly one)
|
|
254
|
+
* @param {{ target?: string, agent_id?: string }} [options.focus]
|
|
255
|
+
*/
|
|
256
|
+
function start(options = {}) {
|
|
257
|
+
const nextKind = options.kind === KIND_SIDE ? KIND_SIDE : KIND_MULTI;
|
|
258
|
+
if (active && kind === nextKind && nextKind === KIND_MULTI) {
|
|
259
|
+
return { ok: true, session_id: sessionId, kind };
|
|
260
|
+
}
|
|
261
|
+
if (active) {
|
|
262
|
+
stop();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
let ids;
|
|
266
|
+
if (nextKind === KIND_SIDE) {
|
|
267
|
+
const raw = Array.isArray(options.agentIds) ? options.agentIds : [];
|
|
268
|
+
const id = String(raw[0] || "").trim();
|
|
269
|
+
if (!id) {
|
|
270
|
+
return { ok: false, error: "side requires one agent_id" };
|
|
271
|
+
}
|
|
272
|
+
ids = [id];
|
|
273
|
+
lockedAgentIds = [id];
|
|
274
|
+
} else {
|
|
275
|
+
lockedAgentIds = [];
|
|
276
|
+
ids = normalizeAgentList(getActiveAgents);
|
|
277
|
+
if (ids.length === 0) {
|
|
278
|
+
return { ok: false, error: "No active agents for multi-window mode" };
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
kind = nextKind;
|
|
283
|
+
sessionId = generateSessionId(kind);
|
|
284
|
+
active = true;
|
|
285
|
+
rev = 1;
|
|
286
|
+
viewportRev = 0;
|
|
287
|
+
const focusOpt = options.focus && typeof options.focus === "object" ? options.focus : null;
|
|
288
|
+
if (focusOpt && focusOpt.target === "agent" && String(focusOpt.agent_id || "").trim()) {
|
|
289
|
+
focus = {
|
|
290
|
+
target: "agent",
|
|
291
|
+
agent_id: String(focusOpt.agent_id).trim(),
|
|
292
|
+
};
|
|
293
|
+
} else if (nextKind === KIND_SIDE) {
|
|
294
|
+
focus = { target: "agent", agent_id: ids[0] };
|
|
295
|
+
} else {
|
|
296
|
+
focus = { target: "chat", agent_id: "" };
|
|
297
|
+
}
|
|
298
|
+
paneSizes.clear();
|
|
299
|
+
paneMeta.clear();
|
|
300
|
+
dirty.clear();
|
|
301
|
+
lastEmitAt.clear();
|
|
302
|
+
ensurePaneManager();
|
|
303
|
+
syncAgents();
|
|
304
|
+
publishSet();
|
|
305
|
+
return { ok: true, session_id: sessionId, kind };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function stop() {
|
|
309
|
+
if (!active && !paneManager) return;
|
|
310
|
+
if (flushTimer) {
|
|
311
|
+
clearTimeout(flushTimer);
|
|
312
|
+
flushTimer = null;
|
|
313
|
+
}
|
|
314
|
+
dirty.clear();
|
|
315
|
+
if (paneManager) {
|
|
316
|
+
try { paneManager.disconnectAll(); } catch {}
|
|
317
|
+
paneManager = null;
|
|
318
|
+
}
|
|
319
|
+
paneSizes.clear();
|
|
320
|
+
paneMeta.clear();
|
|
321
|
+
lastEmitAt.clear();
|
|
322
|
+
active = false;
|
|
323
|
+
publish("multi.set", {
|
|
324
|
+
session_id: sessionId,
|
|
325
|
+
active: false,
|
|
326
|
+
kind,
|
|
327
|
+
rev: rev + 1,
|
|
328
|
+
panes: [],
|
|
329
|
+
focus: { target: "chat", agent_id: "" },
|
|
330
|
+
viewport_rev: viewportRev,
|
|
331
|
+
});
|
|
332
|
+
sessionId = null;
|
|
333
|
+
kind = KIND_MULTI;
|
|
334
|
+
lockedAgentIds = [];
|
|
335
|
+
rev = 0;
|
|
336
|
+
viewportRev = 0;
|
|
337
|
+
focus = { target: "chat", agent_id: "" };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function handleViewport(payload = {}) {
|
|
341
|
+
if (!active || !paneManager) return { ok: false, error: "multi not active" };
|
|
342
|
+
if (payload.session_id && payload.session_id !== sessionId) {
|
|
343
|
+
return { ok: false, error: "stale session_id" };
|
|
344
|
+
}
|
|
345
|
+
const incomingRev = Number(payload.viewport_rev);
|
|
346
|
+
if (Number.isFinite(incomingRev) && incomingRev > viewportRev) {
|
|
347
|
+
viewportRev = Math.floor(incomingRev);
|
|
348
|
+
} else {
|
|
349
|
+
viewportRev += 1;
|
|
350
|
+
}
|
|
351
|
+
const panes = Array.isArray(payload.panes) ? payload.panes : [];
|
|
352
|
+
for (const spec of panes) {
|
|
353
|
+
const id = String(spec && spec.agent_id || "").trim();
|
|
354
|
+
if (!id) continue;
|
|
355
|
+
const cols = Math.max(1, Math.floor(Number(spec.cols) || DEFAULT_COLS));
|
|
356
|
+
const rows = Math.max(1, Math.floor(Number(spec.rows) || DEFAULT_ROWS));
|
|
357
|
+
paneSizes.set(id, { cols, rows });
|
|
358
|
+
if (paneManager.getPane(id)) {
|
|
359
|
+
try { paneManager.sendResize(id, cols, rows); } catch {}
|
|
360
|
+
markDirty(id);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return { ok: true, viewport_rev: viewportRev };
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function handleRaw(payload = {}) {
|
|
367
|
+
if (!active || !paneManager) return { ok: false, error: "multi not active" };
|
|
368
|
+
if (payload.session_id && payload.session_id !== sessionId) {
|
|
369
|
+
return { ok: false, error: "stale session_id" };
|
|
370
|
+
}
|
|
371
|
+
const agentId = String(payload.agent_id || "").trim();
|
|
372
|
+
if (!agentId) return { ok: false, error: "missing agent_id" };
|
|
373
|
+
let data = "";
|
|
374
|
+
if (payload.data_encoding === "base64" && typeof payload.data === "string") {
|
|
375
|
+
try { data = Buffer.from(payload.data, "base64").toString("utf8"); } catch { data = ""; }
|
|
376
|
+
} else if (typeof payload.data === "string") {
|
|
377
|
+
data = payload.data;
|
|
378
|
+
}
|
|
379
|
+
if (!data) return { ok: false, error: "empty data" };
|
|
380
|
+
try { paneManager.sendInputToAgent(agentId, data); } catch {}
|
|
381
|
+
return { ok: true };
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function handleFocus(payload = {}) {
|
|
385
|
+
if (!active || !paneManager) return { ok: false, error: "multi not active" };
|
|
386
|
+
if (payload.session_id && payload.session_id !== sessionId) {
|
|
387
|
+
return { ok: false, error: "stale session_id" };
|
|
388
|
+
}
|
|
389
|
+
const target = payload.target === "agent" ? "agent" : "chat";
|
|
390
|
+
const agentId = String(payload.agent_id || "").trim();
|
|
391
|
+
if (target === "agent" && agentId && paneManager.getPane(agentId)) {
|
|
392
|
+
focus = { target, agent_id: agentId };
|
|
393
|
+
try { paneManager.setFocused(agentId); } catch {}
|
|
394
|
+
} else {
|
|
395
|
+
focus = { target: "chat", agent_id: "" };
|
|
396
|
+
}
|
|
397
|
+
return { ok: true, focus };
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/** Activate an agent pane inside the split (no fullscreen handoff). */
|
|
401
|
+
function focusAgent(agentId) {
|
|
402
|
+
if (!active || !paneManager) return { ok: false, error: "multi not active" };
|
|
403
|
+
const id = String(agentId || "").trim();
|
|
404
|
+
if (!id) return { ok: false, error: "missing agent_id" };
|
|
405
|
+
if (!paneManager.getPane(id)) {
|
|
406
|
+
try { syncAgents(); } catch {}
|
|
407
|
+
}
|
|
408
|
+
if (!paneManager.getPane(id)) {
|
|
409
|
+
return { ok: false, error: "agent pane not found" };
|
|
410
|
+
}
|
|
411
|
+
const result = handleFocus({
|
|
412
|
+
session_id: sessionId,
|
|
413
|
+
target: "agent",
|
|
414
|
+
agent_id: id,
|
|
415
|
+
});
|
|
416
|
+
if (result && result.ok) {
|
|
417
|
+
publishSet();
|
|
418
|
+
}
|
|
419
|
+
return result;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function handleExit() {
|
|
423
|
+
if (!active) return { ok: true };
|
|
424
|
+
stop();
|
|
425
|
+
return { ok: true };
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function isActive() { return active; }
|
|
429
|
+
function getSessionId() { return sessionId; }
|
|
430
|
+
function getKind() { return active ? kind : ""; }
|
|
431
|
+
function isMultiKind() { return active && kind === KIND_MULTI; }
|
|
432
|
+
function isSideKind() { return active && kind === KIND_SIDE; }
|
|
433
|
+
|
|
434
|
+
function getSnapshot() {
|
|
435
|
+
if (!active) return { active: false, kind: "" };
|
|
436
|
+
return {
|
|
437
|
+
active: true,
|
|
438
|
+
kind,
|
|
439
|
+
session_id: sessionId,
|
|
440
|
+
rev,
|
|
441
|
+
viewport_rev: viewportRev,
|
|
442
|
+
focus,
|
|
443
|
+
panes: currentPanesDesc(),
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function listInternalAgentIds() {
|
|
448
|
+
const out = [];
|
|
449
|
+
for (const id of paneAgentIds()) {
|
|
450
|
+
const opts = paneMeta.get(id) || ensurePaneMeta(id);
|
|
451
|
+
if (opts && opts.mode === "internal") out.push(id);
|
|
452
|
+
}
|
|
453
|
+
return out;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function writeToPane(agentId, data) {
|
|
457
|
+
if (!paneManager) return false;
|
|
458
|
+
try {
|
|
459
|
+
const ok = paneManager.writeToPane(agentId, data);
|
|
460
|
+
if (ok) markDirty(agentId);
|
|
461
|
+
return ok;
|
|
462
|
+
} catch {
|
|
463
|
+
return false;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return {
|
|
468
|
+
start,
|
|
469
|
+
stop,
|
|
470
|
+
isActive,
|
|
471
|
+
getSessionId,
|
|
472
|
+
getKind,
|
|
473
|
+
isMultiKind,
|
|
474
|
+
isSideKind,
|
|
475
|
+
getSnapshot,
|
|
476
|
+
handleViewport,
|
|
477
|
+
handleRaw,
|
|
478
|
+
handleFocus,
|
|
479
|
+
focusAgent,
|
|
480
|
+
handleExit,
|
|
481
|
+
syncAgents,
|
|
482
|
+
markDirty,
|
|
483
|
+
writeToPane,
|
|
484
|
+
listInternalAgentIds,
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
module.exports = {
|
|
489
|
+
createRustMultiSession,
|
|
490
|
+
DEFAULT_COLS,
|
|
491
|
+
DEFAULT_ROWS,
|
|
492
|
+
FLUSH_MS,
|
|
493
|
+
MAX_FLUSH_MS,
|
|
494
|
+
MIN_EMIT_GAP_MS,
|
|
495
|
+
KIND_MULTI,
|
|
496
|
+
KIND_SIDE,
|
|
497
|
+
};
|