clay-server 4.0.0-beta.13 → 4.0.0-beta.15
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/lib/capsule-display-floor.js +51 -0
- package/lib/capsule-frame-server.js +229 -0
- package/lib/capsule-pig-logic.js +268 -0
- package/lib/capsule-server-runtimes.js +41 -0
- package/lib/capsule-tictactoe-logic.js +260 -0
- package/lib/capsules/pig/display.js +190 -0
- package/lib/capsules/pig/manifest.json +9 -0
- package/lib/capsules/pig/ui.json +46 -0
- package/lib/capsules/tictactoe/manifest.json +9 -0
- package/lib/capsules/tictactoe/ui.json +203 -0
- package/lib/project-capsule-catalog.js +9 -1
- package/lib/project-connection.js +1 -1
- package/lib/project-log-feedback-delivery.js +76 -0
- package/lib/project-logs.js +4 -0
- package/lib/project-pair-lifecycle.js +7 -12
- package/lib/project-session-pair.js +29 -39
- package/lib/project-worker-proposal.js +203 -72
- package/lib/project.js +13 -0
- package/lib/public/css/capsule-ui.css +18 -0
- package/lib/public/css/home-session-actions.css +66 -0
- package/lib/public/css/home-sidebar.css +66 -0
- package/lib/public/css/mobile-nav.css +80 -0
- package/lib/public/css/sidebar.css +92 -0
- package/lib/public/css/worker-proposal.css +34 -1
- package/lib/public/modules/app-messages.js +14 -1
- package/lib/public/modules/home-conversations-sheet.js +102 -48
- package/lib/public/modules/home-session-actions.js +6 -0
- package/lib/public/modules/home-sidebar-chat-list.js +101 -41
- package/lib/public/modules/home-tool-frame.js +165 -0
- package/lib/public/modules/home-tools.js +129 -1
- package/lib/public/modules/session-hierarchy.js +54 -0
- package/lib/public/modules/sidebar-mobile.js +35 -6
- package/lib/public/modules/sidebar-session-hierarchy.js +206 -0
- package/lib/public/modules/sidebar-sessions.js +49 -7
- package/lib/public/modules/worker-proposal-state.js +16 -0
- package/lib/public/modules/worker-proposal.js +49 -12
- package/lib/sdk-bridge.js +16 -8
- package/lib/sdk-message-processor.js +34 -4
- package/lib/server-home-chat.js +12 -1
- package/lib/server-tools.js +97 -9
- package/lib/server.js +3 -0
- package/lib/session-driver-eligibility.js +20 -165
- package/lib/session-pair-factory.js +31 -28
- package/lib/session-pair-mcp-server.js +5 -9
- package/lib/session-pair-prompts.js +15 -15
- package/lib/session-provenance.js +119 -0
- package/lib/session-spawn-mcp-server.js +1 -1
- package/lib/sessions.js +40 -2
- package/lib/tools-registry.js +44 -10
- package/lib/ws-schema.js +8 -3
- package/lib/yoke/adapters/claude.js +12 -0
- package/package.json +1 -1
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// home-tool-frame.js - host side of the sandboxed rich Display frame.
|
|
2
|
+
//
|
|
3
|
+
// A rich element is additive Display: it renders in an iframe on a separate
|
|
4
|
+
// anonymous origin with sandbox="allow-scripts" and a frame CSP that allows
|
|
5
|
+
// no network at all. Its only channel is postMessage, and the only thing it
|
|
6
|
+
// can request is an act, which the host routes through the exact callback the
|
|
7
|
+
// floor buttons use. The floor never depends on the frame: if the frame fails
|
|
8
|
+
// to load or go ready, the host falls back to the floor automatically.
|
|
9
|
+
|
|
10
|
+
import { getWs } from './ws-ref.js';
|
|
11
|
+
|
|
12
|
+
var READY_TIMEOUT_MS = 6000;
|
|
13
|
+
var MAX_ACTION_LENGTH = 64;
|
|
14
|
+
|
|
15
|
+
var frames = Object.create(null);
|
|
16
|
+
|
|
17
|
+
// One listener for every frame. Messages are matched to a frame by the
|
|
18
|
+
// window they came from, never by origin text: a sandboxed frame without
|
|
19
|
+
// allow-same-origin reports the opaque origin "null", so identity comes from
|
|
20
|
+
// the contentWindow reference the host itself created.
|
|
21
|
+
var listenerAttached = false;
|
|
22
|
+
|
|
23
|
+
function ensureListener() {
|
|
24
|
+
if (listenerAttached) return;
|
|
25
|
+
listenerAttached = true;
|
|
26
|
+
window.addEventListener("message", function (event) {
|
|
27
|
+
var toolIds = Object.keys(frames);
|
|
28
|
+
var entry = null;
|
|
29
|
+
for (var i = 0; i < toolIds.length; i++) {
|
|
30
|
+
var candidate = frames[toolIds[i]];
|
|
31
|
+
if (candidate.iframe && candidate.iframe.contentWindow === event.source) {
|
|
32
|
+
entry = candidate;
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (!entry) return;
|
|
37
|
+
var data = event.data;
|
|
38
|
+
if (!data || data.clayCapsuleFrame !== 1 || typeof data.type !== "string") return;
|
|
39
|
+
if (data.type === "ready") {
|
|
40
|
+
markReady(entry);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (data.type === "act") {
|
|
44
|
+
// The frame asks; the host decides. Bounded validation here, then the
|
|
45
|
+
// same pipeline as a floor button click. Nothing else is accepted.
|
|
46
|
+
if (typeof data.actionId !== "string" || !data.actionId || data.actionId.length > MAX_ACTION_LENGTH) return;
|
|
47
|
+
if (!/^[A-Za-z][A-Za-z0-9_-]*$/.test(data.actionId)) return;
|
|
48
|
+
var args = data.args && typeof data.args === "object" && !Array.isArray(data.args) ? data.args : {};
|
|
49
|
+
entry.onAct(data.actionId, args);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function markReady(entry) {
|
|
55
|
+
if (entry.ready) return;
|
|
56
|
+
entry.ready = true;
|
|
57
|
+
if (entry.readyTimer) {
|
|
58
|
+
clearTimeout(entry.readyTimer);
|
|
59
|
+
entry.readyTimer = null;
|
|
60
|
+
}
|
|
61
|
+
for (var i = 0; i < entry.queue.length; i++) {
|
|
62
|
+
postToFrame(entry, entry.queue[i]);
|
|
63
|
+
}
|
|
64
|
+
entry.queue.length = 0;
|
|
65
|
+
if (entry.onReady) entry.onReady();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function postToFrame(entry, payload) {
|
|
69
|
+
if (!entry.iframe || !entry.iframe.contentWindow) return;
|
|
70
|
+
// targetOrigin must be "*": a sandboxed frame without allow-same-origin has
|
|
71
|
+
// an opaque origin no origin string can name. Delivery is still exact,
|
|
72
|
+
// because the message goes to the one contentWindow this host created, and
|
|
73
|
+
// the payload is only the projection already chosen for this element.
|
|
74
|
+
entry.iframe.contentWindow.postMessage(payload, "*");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function failFrame(toolId, message) {
|
|
78
|
+
var entry = frames[toolId];
|
|
79
|
+
if (!entry) return;
|
|
80
|
+
disposeFrame(toolId);
|
|
81
|
+
if (entry.onUnavailable) entry.onUnavailable(message);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Mounts the rich element for one Capsule. The frame URL is requested over
|
|
85
|
+
// the authenticated WebSocket and answered by handleToolFrameUrlState below.
|
|
86
|
+
export function mountRichDisplay(options) {
|
|
87
|
+
var toolId = options.toolId;
|
|
88
|
+
disposeFrame(toolId);
|
|
89
|
+
ensureListener();
|
|
90
|
+
frames[toolId] = {
|
|
91
|
+
toolId: toolId,
|
|
92
|
+
container: options.container,
|
|
93
|
+
onAct: options.onAct,
|
|
94
|
+
onReady: options.onReady || null,
|
|
95
|
+
onUnavailable: options.onUnavailable || null,
|
|
96
|
+
iframe: null,
|
|
97
|
+
ready: false,
|
|
98
|
+
queue: [],
|
|
99
|
+
readyTimer: null,
|
|
100
|
+
};
|
|
101
|
+
var ws = getWs();
|
|
102
|
+
if (!ws || ws.readyState !== 1) {
|
|
103
|
+
failFrame(toolId, "The connection to the server is not available.");
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
ws.send(JSON.stringify({ type: "tool_frame_url", toolId: toolId }));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function handleToolFrameUrlState(msg) {
|
|
110
|
+
if (!msg || !msg.toolId) return;
|
|
111
|
+
var entry = frames[msg.toolId];
|
|
112
|
+
if (!entry || entry.iframe) return;
|
|
113
|
+
if (msg.ok === false || !msg.frame || !msg.frame.port) {
|
|
114
|
+
failFrame(msg.toolId, msg.error || "The rich Display is unavailable.");
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
var scheme = msg.frame.secure ? "https" : "http";
|
|
118
|
+
var url = scheme + "://" + window.location.hostname + ":" + msg.frame.port + msg.frame.path;
|
|
119
|
+
var iframe = document.createElement("iframe");
|
|
120
|
+
iframe.className = "home-tool-frame";
|
|
121
|
+
// allow-scripts only. No allow-same-origin, so the frame's origin is opaque
|
|
122
|
+
// and nothing of the host origin (DOM, cookies, storage) is reachable.
|
|
123
|
+
iframe.setAttribute("sandbox", "allow-scripts");
|
|
124
|
+
iframe.setAttribute("title", "Capsule rich display");
|
|
125
|
+
iframe.src = url;
|
|
126
|
+
entry.iframe = iframe;
|
|
127
|
+
entry.container.appendChild(iframe);
|
|
128
|
+
entry.readyTimer = setTimeout(function () {
|
|
129
|
+
failFrame(msg.toolId, "The rich Display did not become ready.");
|
|
130
|
+
}, READY_TIMEOUT_MS);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function pushFrameState(toolId, state) {
|
|
134
|
+
var entry = frames[toolId];
|
|
135
|
+
if (!entry) return;
|
|
136
|
+
var payload = { clayCapsuleFrame: 1, type: "state", state: state || {} };
|
|
137
|
+
if (!entry.ready) {
|
|
138
|
+
entry.queue.push(payload);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
postToFrame(entry, payload);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function pushFrameEvent(toolId, event) {
|
|
145
|
+
var entry = frames[toolId];
|
|
146
|
+
if (!entry || !event) return;
|
|
147
|
+
var payload = { clayCapsuleFrame: 1, type: "event", event: event };
|
|
148
|
+
if (!entry.ready) {
|
|
149
|
+
entry.queue.push(payload);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
postToFrame(entry, payload);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function hasFrame(toolId) {
|
|
156
|
+
return !!frames[toolId];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function disposeFrame(toolId) {
|
|
160
|
+
var entry = frames[toolId];
|
|
161
|
+
if (!entry) return;
|
|
162
|
+
if (entry.readyTimer) clearTimeout(entry.readyTimer);
|
|
163
|
+
if (entry.iframe && entry.iframe.parentNode) entry.iframe.parentNode.removeChild(entry.iframe);
|
|
164
|
+
delete frames[toolId];
|
|
165
|
+
}
|
|
@@ -9,6 +9,7 @@ import { showToast } from './utils.js';
|
|
|
9
9
|
import { createToolLlmStatus, initialToolLlmAlias } from './tool-llm-status.js';
|
|
10
10
|
import { isToolModelAlias, shouldInjectToolLlmStatus } from './tool-ui-tree.js';
|
|
11
11
|
import { mountCapsuleHostControls, disposeCapsuleHostControls, handleToolSourceState as applyToolSourceState, handleToolMateAccessState as applyToolMateAccessState } from './home-capsule-source.js';
|
|
12
|
+
import { mountRichDisplay, pushFrameState, pushFrameEvent, disposeFrame } from './home-tool-frame.js';
|
|
12
13
|
|
|
13
14
|
var definitions = Object.create(null);
|
|
14
15
|
var runtimes = Object.create(null);
|
|
@@ -16,6 +17,8 @@ var registeredIds = Object.create(null);
|
|
|
16
17
|
var errorSurfaces = Object.create(null);
|
|
17
18
|
var displays = Object.create(null);
|
|
18
19
|
var llmStatuses = Object.create(null);
|
|
20
|
+
var serverStates = Object.create(null);
|
|
21
|
+
var serverEventSeqs = Object.create(null);
|
|
19
22
|
|
|
20
23
|
export function requestTools() {
|
|
21
24
|
var ws = getWs();
|
|
@@ -29,6 +32,7 @@ function stopRuntime(toolId) {
|
|
|
29
32
|
if (runtimes[toolId]) runtimes[toolId].stop();
|
|
30
33
|
if (llmStatuses[toolId]) llmStatuses[toolId].dispose();
|
|
31
34
|
if (displays[toolId]) disposeToolUi(displays[toolId]);
|
|
35
|
+
disposeFrame(toolId);
|
|
32
36
|
delete runtimes[toolId];
|
|
33
37
|
delete errorSurfaces[toolId];
|
|
34
38
|
delete displays[toolId];
|
|
@@ -52,6 +56,125 @@ function renderRuntimeState(toolId, state) {
|
|
|
52
56
|
}, display);
|
|
53
57
|
}
|
|
54
58
|
|
|
59
|
+
// Server-runtime Capsules hold their Logic on the server, so the human's click
|
|
60
|
+
// travels the same act pipeline a Mate's clay_tool_act call travels. The
|
|
61
|
+
// browser only renders the state Logic hands back.
|
|
62
|
+
function sendServerControl(toolId, request) {
|
|
63
|
+
var ws = getWs();
|
|
64
|
+
if (!ws || ws.readyState !== 1) {
|
|
65
|
+
showRuntimeError(toolId, "The connection to the server is not available.");
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
ws.send(JSON.stringify({
|
|
69
|
+
type: "tool_server_control",
|
|
70
|
+
toolId: toolId,
|
|
71
|
+
kind: request.kind,
|
|
72
|
+
actionId: request.actionId || null,
|
|
73
|
+
args: request.args || {},
|
|
74
|
+
}));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function renderServerState(toolId, state) {
|
|
78
|
+
var definition = definitions[toolId];
|
|
79
|
+
var display = displays[toolId];
|
|
80
|
+
if (!definition || !display) return;
|
|
81
|
+
renderToolUi(toolId, definition.uiTree, state || {}, function (actionName, args) {
|
|
82
|
+
sendServerControl(toolId, { kind: "act", actionId: actionName, args: args });
|
|
83
|
+
}, display);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Applies a server-runtime Capsule state if it is not older than what is
|
|
87
|
+
// already rendered. States carry the Logic's monotonic eventSeq, so a slow
|
|
88
|
+
// snapshot response can never roll the Display back behind a pushed event.
|
|
89
|
+
function applyServerState(toolId, state) {
|
|
90
|
+
state = state || {};
|
|
91
|
+
// A successful read or event clears any stale refusal either way.
|
|
92
|
+
var surface = errorSurfaces[toolId];
|
|
93
|
+
if (surface) surface.classList.add("hidden");
|
|
94
|
+
var seq = Number.isInteger(state.eventSeq) ? state.eventSeq : null;
|
|
95
|
+
var lastSeq = serverEventSeqs[toolId];
|
|
96
|
+
if (seq !== null && lastSeq !== undefined && seq <= lastSeq) return false;
|
|
97
|
+
if (seq !== null) serverEventSeqs[toolId] = seq;
|
|
98
|
+
serverStates[toolId] = state;
|
|
99
|
+
renderServerState(toolId, state);
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Brief host-side attribution when the other seat acts: the Display flashes
|
|
104
|
+
// and the dock icon pulses, so a watching human can tell the change was the
|
|
105
|
+
// Mate operating the same interface. The meaning itself stays in state; this
|
|
106
|
+
// is only the host pointing at it.
|
|
107
|
+
function flashRemoteAct(toolId) {
|
|
108
|
+
pulseDockTool(toolId);
|
|
109
|
+
var display = displays[toolId];
|
|
110
|
+
if (!display) return;
|
|
111
|
+
display.classList.remove("capsule-remote-act");
|
|
112
|
+
void display.offsetWidth;
|
|
113
|
+
display.classList.add("capsule-remote-act");
|
|
114
|
+
setTimeout(function () { display.classList.remove("capsule-remote-act"); }, 900);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function handleToolServerState(msg) {
|
|
118
|
+
if (!msg || !msg.toolId) return;
|
|
119
|
+
if (msg.ok === false) {
|
|
120
|
+
showRuntimeError(msg.toolId, msg.error || "The Capsule action failed.");
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (applyServerState(msg.toolId, msg.state)) pushFrameState(msg.toolId, msg.state || {});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// The push half of the shared-interface effect: Logic changed, no matter whose
|
|
127
|
+
// act changed it, so every open Display for this user re-renders. Events are
|
|
128
|
+
// applied one by one in seq order, so consecutive acts replay as steps rather
|
|
129
|
+
// than collapsing into a final state. The rich frame receives the full causal
|
|
130
|
+
// event so it can animate and attribute the transition.
|
|
131
|
+
export function handleToolServerEvent(msg) {
|
|
132
|
+
if (!msg || !msg.toolId || !msg.event || !msg.event.next) return;
|
|
133
|
+
if (!applyServerState(msg.toolId, msg.event.next)) return;
|
|
134
|
+
pushFrameEvent(msg.toolId, msg.event);
|
|
135
|
+
if (msg.event.actor && msg.event.actor !== "user") flashRemoteAct(msg.toolId);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Mounts the additive rich element above the floor. The floor is rendered
|
|
139
|
+
// first and stays a click away behind the toggle, and any frame failure
|
|
140
|
+
// (no URL, load error, never ready) falls back to the floor silently: the
|
|
141
|
+
// rich element may enrich the Capsule, never gate it.
|
|
142
|
+
function mountServerFrame(toolId, root, floorEl) {
|
|
143
|
+
var frameWrap = document.createElement("div");
|
|
144
|
+
frameWrap.className = "home-tool-frame-wrap hidden";
|
|
145
|
+
frameWrap.dataset.capsuleRuntimeSurface = "true";
|
|
146
|
+
var toggle = document.createElement("button");
|
|
147
|
+
toggle.type = "button";
|
|
148
|
+
toggle.className = "home-tool-frame-toggle hidden";
|
|
149
|
+
toggle.dataset.capsuleRuntimeSurface = "true";
|
|
150
|
+
var showingFrame = false;
|
|
151
|
+
function setView(rich) {
|
|
152
|
+
showingFrame = rich;
|
|
153
|
+
frameWrap.classList.toggle("hidden", !rich);
|
|
154
|
+
floorEl.classList.toggle("hidden", rich);
|
|
155
|
+
toggle.textContent = rich ? "Standard controls" : "Rich view";
|
|
156
|
+
}
|
|
157
|
+
toggle.addEventListener("click", function () { setView(!showingFrame); });
|
|
158
|
+
root.appendChild(frameWrap);
|
|
159
|
+
root.appendChild(toggle);
|
|
160
|
+
mountRichDisplay({
|
|
161
|
+
toolId: toolId,
|
|
162
|
+
container: frameWrap,
|
|
163
|
+
onAct: function (actionId, args) {
|
|
164
|
+
sendServerControl(toolId, { kind: "act", actionId: actionId, args: args });
|
|
165
|
+
},
|
|
166
|
+
onReady: function () {
|
|
167
|
+
toggle.classList.remove("hidden");
|
|
168
|
+
setView(true);
|
|
169
|
+
},
|
|
170
|
+
onUnavailable: function () {
|
|
171
|
+
toggle.classList.add("hidden");
|
|
172
|
+
setView(false);
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
pushFrameState(toolId, serverStates[toolId] || {});
|
|
176
|
+
}
|
|
177
|
+
|
|
55
178
|
function ensureRuntime(toolId) {
|
|
56
179
|
if (runtimes[toolId]) return runtimes[toolId];
|
|
57
180
|
var definition = definitions[toolId];
|
|
@@ -102,8 +225,11 @@ function mountTool(toolId, contentEl, chromeEl) {
|
|
|
102
225
|
var serverDisplay = document.createElement("div");
|
|
103
226
|
serverDisplay.className = "home-tool-display";
|
|
104
227
|
serverDisplay.dataset.capsuleRuntimeSurface = "true";
|
|
228
|
+
if (definition.hasRichDisplay) mountServerFrame(toolId, root, serverDisplay);
|
|
105
229
|
root.appendChild(serverDisplay);
|
|
106
|
-
|
|
230
|
+
displays[toolId] = serverDisplay;
|
|
231
|
+
renderServerState(toolId, serverStates[toolId] || {});
|
|
232
|
+
sendServerControl(toolId, { kind: "snapshot" });
|
|
107
233
|
return;
|
|
108
234
|
}
|
|
109
235
|
var runtime = ensureRuntime(toolId);
|
|
@@ -189,6 +315,8 @@ export function handleToolMateAccessState(msg) {
|
|
|
189
315
|
export function handleToolRemoved(msg) {
|
|
190
316
|
stopRuntime(msg.toolId);
|
|
191
317
|
delete definitions[msg.toolId];
|
|
318
|
+
delete serverStates[msg.toolId];
|
|
319
|
+
delete serverEventSeqs[msg.toolId];
|
|
192
320
|
delete registeredIds[msg.toolId];
|
|
193
321
|
unregisterDockTool(msg.toolId);
|
|
194
322
|
syncStore();
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Pure projection shared by desktop, mobile and Home session lists.
|
|
2
|
+
|
|
3
|
+
export function buildSessionHierarchy(sessions) {
|
|
4
|
+
var source = Array.isArray(sessions) ? sessions : [];
|
|
5
|
+
var byId = new Map();
|
|
6
|
+
var workersByParent = new Map();
|
|
7
|
+
var roots = [];
|
|
8
|
+
var orphans = [];
|
|
9
|
+
for (var i = 0; i < source.length; i++) byId.set(nodeId(source[i]), source[i]);
|
|
10
|
+
for (var j = 0; j < source.length; j++) {
|
|
11
|
+
var session = source[j];
|
|
12
|
+
if (session.sessionRole !== "worker") {
|
|
13
|
+
roots.push({ driver: session, workers: [] });
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
var parent = session.parentAvailable ? byId.get(parentNodeId(session)) : null;
|
|
17
|
+
if (!parent || parent.sessionRole === "worker") {
|
|
18
|
+
orphans.push(session);
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (!workersByParent.has(nodeId(parent))) workersByParent.set(nodeId(parent), []);
|
|
22
|
+
workersByParent.get(nodeId(parent)).push(session);
|
|
23
|
+
}
|
|
24
|
+
for (var k = 0; k < roots.length; k++) {
|
|
25
|
+
roots[k].workers = workersByParent.get(nodeId(roots[k].driver)) || [];
|
|
26
|
+
roots[k].workers.sort(compareWorkers);
|
|
27
|
+
roots[k].lastActivity = latestActivity(roots[k]);
|
|
28
|
+
}
|
|
29
|
+
orphans.sort(compareWorkers);
|
|
30
|
+
return { roots: roots, orphans: orphans };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function nodeId(session) {
|
|
34
|
+
return session && session.hierarchyId != null ? session.hierarchyId : session.id;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function parentNodeId(session) {
|
|
38
|
+
return session && session.parentHierarchyId != null ? session.parentHierarchyId : session.parentSessionId;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function compareWorkers(a, b) {
|
|
42
|
+
var ag = typeof a.workerGeneration === "number" ? a.workerGeneration : 0;
|
|
43
|
+
var bg = typeof b.workerGeneration === "number" ? b.workerGeneration : 0;
|
|
44
|
+
if (ag !== bg) return bg - ag;
|
|
45
|
+
return (b.lastActivity || 0) - (a.lastActivity || 0);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function latestActivity(root) {
|
|
49
|
+
var latest = root.driver.lastActivity || 0;
|
|
50
|
+
for (var i = 0; i < root.workers.length; i++) {
|
|
51
|
+
if ((root.workers[i].lastActivity || 0) > latest) latest = root.workers[i].lastActivity || 0;
|
|
52
|
+
}
|
|
53
|
+
return latest;
|
|
54
|
+
}
|
|
@@ -37,6 +37,11 @@ import { openHomeChat } from './home-mate-chat.js';
|
|
|
37
37
|
import { openTerminal } from './terminal.js';
|
|
38
38
|
import { requestKnowledgeList } from './mate-knowledge.js';
|
|
39
39
|
import { loadRootDirectory } from './filebrowser.js';
|
|
40
|
+
import {
|
|
41
|
+
prepareSidebarHierarchy,
|
|
42
|
+
renderMobileDriverHierarchy,
|
|
43
|
+
renderMobileOrphanHierarchy
|
|
44
|
+
} from './sidebar-session-hierarchy.js';
|
|
40
45
|
|
|
41
46
|
// --- Mobile state ---
|
|
42
47
|
var mobileChatSheetOpen = false;
|
|
@@ -374,9 +379,11 @@ function renderSheetSessions(listEl) {
|
|
|
374
379
|
}
|
|
375
380
|
|
|
376
381
|
// Helper: create a mobile session item element
|
|
377
|
-
function createMobileSessionItem(s) {
|
|
382
|
+
function createMobileSessionItem(s, options) {
|
|
383
|
+
var itemOptions = options || {};
|
|
378
384
|
var el = document.createElement("button");
|
|
379
385
|
el.className = "mobile-session-item" + (s.active ? " active" : "");
|
|
386
|
+
if (itemOptions.worker) el.classList.add("mobile-worker-item");
|
|
380
387
|
|
|
381
388
|
// Vendor badge (legacy sessions without a vendor are Claude)
|
|
382
389
|
var mVendor = s.vendor || "claude";
|
|
@@ -398,6 +405,13 @@ function createMobileSessionItem(s) {
|
|
|
398
405
|
titleSpan.appendChild(document.createTextNode(s.title || "New Session"));
|
|
399
406
|
el.appendChild(titleSpan);
|
|
400
407
|
|
|
408
|
+
if (itemOptions.worker) {
|
|
409
|
+
var generation = document.createElement("span");
|
|
410
|
+
generation.className = "mobile-worker-generation" + (itemOptions.current ? " current" : "");
|
|
411
|
+
generation.textContent = itemOptions.current ? "Current" : (s.workerGeneration ? "Gen " + s.workerGeneration : "Worker");
|
|
412
|
+
el.appendChild(generation);
|
|
413
|
+
}
|
|
414
|
+
|
|
401
415
|
// Unread badge (right side)
|
|
402
416
|
if (s.unread > 0 && !s.active) {
|
|
403
417
|
var badge = document.createElement("span");
|
|
@@ -720,10 +734,13 @@ function renderMobileSessionsInto(container) {
|
|
|
720
734
|
|
|
721
735
|
// Partition: loop sessions vs normal sessions (same logic as desktop renderSessionList)
|
|
722
736
|
var sessions = getCachedSessions();
|
|
737
|
+
var hierarchy = prepareSidebarHierarchy(sessions);
|
|
738
|
+
var hierarchyByDriver = hierarchy.byDriver;
|
|
723
739
|
var loopGroups = {};
|
|
724
740
|
var normalSessions = [];
|
|
725
741
|
for (var i = 0; i < sessions.length; i++) {
|
|
726
742
|
var s = sessions[i];
|
|
743
|
+
if (s.sessionRole === "worker") continue;
|
|
727
744
|
if (s.loop && s.loop.loopId && s.loop.role === "crafting" && s.loop.source !== "ralph" && s.loop.source !== "debate") {
|
|
728
745
|
continue;
|
|
729
746
|
} else if (s.loop && s.loop.loopId) {
|
|
@@ -740,7 +757,14 @@ function renderMobileSessionsInto(container) {
|
|
|
740
757
|
// Build virtual items
|
|
741
758
|
var items = [];
|
|
742
759
|
for (var j = 0; j < normalSessions.length; j++) {
|
|
743
|
-
|
|
760
|
+
var driverRoot = hierarchyByDriver.get(normalSessions[j].id);
|
|
761
|
+
if (driverRoot) items.push({ type: "driver-hierarchy", data: driverRoot.driver, root: driverRoot, lastActivity: driverRoot.lastActivity });
|
|
762
|
+
else items.push({ type: "session", data: normalSessions[j], lastActivity: normalSessions[j].lastActivity || 0 });
|
|
763
|
+
}
|
|
764
|
+
if (hierarchy.orphans.length) {
|
|
765
|
+
var orphanActivity = 0;
|
|
766
|
+
for (var oi = 0; oi < hierarchy.orphans.length; oi++) orphanActivity = Math.max(orphanActivity, hierarchy.orphans[oi].lastActivity || 0);
|
|
767
|
+
items.push({ type: "orphan-workers", workers: hierarchy.orphans, lastActivity: orphanActivity });
|
|
744
768
|
}
|
|
745
769
|
var groupKeys = Object.keys(loopGroups);
|
|
746
770
|
for (var k = 0; k < groupKeys.length; k++) {
|
|
@@ -757,8 +781,8 @@ function renderMobileSessionsInto(container) {
|
|
|
757
781
|
|
|
758
782
|
// Sort by lastActivity descending
|
|
759
783
|
items.sort(function (a, b) {
|
|
760
|
-
var aBookmarked = !!(a.type === "loop" ? false : a.data && a.data.bookmarked);
|
|
761
|
-
var bBookmarked = !!(b.type === "loop" ? false : b.data && b.data.bookmarked);
|
|
784
|
+
var aBookmarked = !!(a.type === "loop" || a.type === "orphan-workers" ? false : a.data && a.data.bookmarked);
|
|
785
|
+
var bBookmarked = !!(b.type === "loop" || b.type === "orphan-workers" ? false : b.data && b.data.bookmarked);
|
|
762
786
|
if (aBookmarked !== bBookmarked) return aBookmarked ? -1 : 1;
|
|
763
787
|
return (b.lastActivity || 0) - (a.lastActivity || 0);
|
|
764
788
|
});
|
|
@@ -767,7 +791,7 @@ function renderMobileSessionsInto(container) {
|
|
|
767
791
|
var regularItems = [];
|
|
768
792
|
for (var n = 0; n < items.length; n++) {
|
|
769
793
|
var item = items[n];
|
|
770
|
-
if (item.type === "session" && item.data && item.data.bookmarked) {
|
|
794
|
+
if ((item.type === "session" || item.type === "driver-hierarchy") && item.data && item.data.bookmarked) {
|
|
771
795
|
bookmarkedItems.push(item);
|
|
772
796
|
} else {
|
|
773
797
|
regularItems.push(item);
|
|
@@ -781,7 +805,8 @@ function renderMobileSessionsInto(container) {
|
|
|
781
805
|
container.appendChild(bookmarkedHeader);
|
|
782
806
|
|
|
783
807
|
for (var bi = 0; bi < bookmarkedItems.length; bi++) {
|
|
784
|
-
container.appendChild(
|
|
808
|
+
if (bookmarkedItems[bi].type === "driver-hierarchy") container.appendChild(renderMobileDriverHierarchy(bookmarkedItems[bi].root, createMobileSessionItem, refreshMobileChatSheet));
|
|
809
|
+
else container.appendChild(createMobileSessionItem(bookmarkedItems[bi].data));
|
|
785
810
|
}
|
|
786
811
|
}
|
|
787
812
|
|
|
@@ -800,6 +825,10 @@ function renderMobileSessionsInto(container) {
|
|
|
800
825
|
}
|
|
801
826
|
if (item.type === "loop") {
|
|
802
827
|
container.appendChild(createMobileLoopGroup(item.loopId, item.children, item.groupKey));
|
|
828
|
+
} else if (item.type === "driver-hierarchy") {
|
|
829
|
+
container.appendChild(renderMobileDriverHierarchy(item.root, createMobileSessionItem, refreshMobileChatSheet));
|
|
830
|
+
} else if (item.type === "orphan-workers") {
|
|
831
|
+
container.appendChild(renderMobileOrphanHierarchy(item.workers, createMobileSessionItem, refreshMobileChatSheet));
|
|
803
832
|
} else {
|
|
804
833
|
container.appendChild(createMobileSessionItem(item.data));
|
|
805
834
|
}
|