clay-server 5.2.1-beta.2 → 5.3.0-beta.2
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/clay-control-presentation-binding.js +5 -3
- package/lib/clay-product-control-tools.js +3 -3
- package/lib/clay-product-control.js +4 -2
- package/lib/clay-screen-controls.js +18 -3
- package/lib/clay-split-view-awareness.js +35 -0
- package/lib/clay-split-view-controls.js +122 -0
- package/lib/clay-split-view-input.js +20 -0
- package/lib/clay-split-view-render-proof.js +22 -0
- package/lib/pane-asset-manifest.js +63 -0
- package/lib/project-connection.js +19 -10
- package/lib/project-session-actions.js +1 -0
- package/lib/project-session-delete.js +2 -2
- package/lib/project-session-folders.js +1 -0
- package/lib/project-sessions.js +6 -1
- package/lib/project-split-view-document.js +33 -0
- package/lib/project-split-view-names.js +67 -0
- package/lib/project-split-view-organization.js +23 -0
- package/lib/project-split-views.js +479 -0
- package/lib/project-worker-conversation.js +222 -0
- package/lib/project-worker-creation-proposal.js +3 -4
- package/lib/project-worker-proposal.js +1 -1
- package/lib/project-worker-removal.js +101 -0
- package/lib/project.js +28 -1
- package/lib/public/app.js +32 -13
- package/lib/public/css/base.css +9 -0
- package/lib/public/css/filebrowser.css +0 -1
- package/lib/public/css/pane-header-controls.css +100 -0
- package/lib/public/css/pane.css +24 -41
- package/lib/public/css/popup-chat.css +123 -0
- package/lib/public/css/right-workbench.css +50 -18
- package/lib/public/css/session-folders.css +1 -1
- package/lib/public/css/split-view-layout.css +463 -0
- package/lib/public/css/split-view-worker-activity.css +97 -0
- package/lib/public/css/sticky-notes.css +13 -0
- package/lib/public/index.html +3 -3
- package/lib/public/modules/app-connection.js +4 -1
- package/lib/public/modules/app-favicon.js +1 -1
- package/lib/public/modules/app-messages.js +31 -9
- package/lib/public/modules/chat-render-runtime.js +54 -0
- package/lib/public/modules/clay-control-copy.js +3 -0
- package/lib/public/modules/clay-navigation-render.js +1 -1
- package/lib/public/modules/clay-owner-navigation.js +15 -5
- package/lib/public/modules/clay-split-view-screen.js +74 -0
- package/lib/public/modules/clay-tool-activity.js +5 -0
- package/lib/public/modules/command-palette.js +30 -28
- package/lib/public/modules/contextual-clay-session.js +26 -2
- package/lib/public/modules/contextual-clay.js +5 -2
- package/lib/public/modules/contextual-mate-sessions.js +2 -0
- package/lib/public/modules/filebrowser.js +6 -2
- package/lib/public/modules/generation-stop.js +7 -1
- package/lib/public/modules/knowledge-workbench-entry.js +43 -0
- package/lib/public/modules/markdown-view-mode.js +34 -0
- package/lib/public/modules/markdown.js +11 -0
- package/lib/public/modules/mate-knowledge-workbench.js +2 -7
- package/lib/public/modules/mate-sidebar.js +1 -1
- package/lib/public/modules/palette-clay-conversation.js +87 -0
- package/lib/public/modules/palette-render-helpers.js +25 -0
- package/lib/public/modules/pane-activation.js +46 -7
- package/lib/public/modules/pane-binding.js +12 -2
- package/lib/public/modules/pane-bridge.js +12 -3
- package/lib/public/modules/pane-deferred-bootstrap.js +32 -0
- package/lib/public/modules/pane-feature-assets.js +35 -0
- package/lib/public/modules/pane-file-bridge.js +8 -3
- package/lib/public/modules/pane-search-receipt.js +12 -0
- package/lib/public/modules/permission-control.js +6 -6
- package/lib/public/modules/popup-chat-rendering.js +244 -0
- package/lib/public/modules/project-activation.js +2 -2
- package/lib/public/modules/qrcode.js +12 -1
- package/lib/public/modules/right-workbench.js +68 -0
- package/lib/public/modules/search-hints.js +79 -0
- package/lib/public/modules/session-context-menu.js +35 -2
- package/lib/public/modules/session-folder-collapse.js +2 -1
- package/lib/public/modules/session-folder-context.js +24 -13
- package/lib/public/modules/session-folder-delete.js +18 -1
- package/lib/public/modules/session-folder-dnd.js +4 -1
- package/lib/public/modules/session-folder-layout.js +7 -6
- package/lib/public/modules/session-search-pane-state.js +30 -0
- package/lib/public/modules/session-search.js +53 -13
- package/lib/public/modules/sidebar-mobile.js +3 -3
- package/lib/public/modules/sidebar-projects.js +1 -0
- package/lib/public/modules/sidebar-session-folders-ui.js +3 -3
- package/lib/public/modules/sidebar-sessions.js +57 -24
- package/lib/public/modules/sidebar.js +20 -3
- package/lib/public/modules/split-group-helpers.js +1 -1
- package/lib/public/modules/split-group-restore.js +8 -11
- package/lib/public/modules/split-pair-ui.js +18 -0
- package/lib/public/modules/split-pane-header-state.js +45 -0
- package/lib/public/modules/split-pane-reconciler.js +23 -4
- package/lib/public/modules/split-pane-renderer.js +330 -23
- package/lib/public/modules/split-pane-retention.js +157 -0
- package/lib/public/modules/split-session-boundary.js +21 -0
- package/lib/public/modules/split-view-create-action.js +15 -0
- package/lib/public/modules/split-view-create-menu.js +83 -0
- package/lib/public/modules/split-view-create-request.js +23 -0
- package/lib/public/modules/split-view-layout-client.js +198 -0
- package/lib/public/modules/split-view-layout-icons.js +37 -0
- package/lib/public/modules/split-view-layout-scope.js +38 -0
- package/lib/public/modules/split-view-organization.js +73 -0
- package/lib/public/modules/split-view-presentation-close.js +35 -0
- package/lib/public/modules/split-view-presentation-drag.js +278 -0
- package/lib/public/modules/split-view-presentation-drop.js +57 -0
- package/lib/public/modules/split-view-presentation-lookup.js +44 -0
- package/lib/public/modules/split-view-presentation-renderer.js +494 -0
- package/lib/public/modules/split-view-presentations.js +498 -0
- package/lib/public/modules/split-view-shell-controls.js +28 -0
- package/lib/public/modules/split-view-sidebar-menu.js +42 -0
- package/lib/public/modules/split-view-sidebar-renderer.js +30 -0
- package/lib/public/modules/split-view-singleton.js +21 -0
- package/lib/public/modules/split-view-worker-activity.js +94 -0
- package/lib/public/modules/split-view-worker-controls.js +30 -0
- package/lib/public/modules/split-view-worker-removal.js +137 -0
- package/lib/public/modules/split-view-worker-scroll.js +57 -0
- package/lib/public/modules/split-view-worker-transcript.js +46 -0
- package/lib/public/modules/split-view-workers.js +418 -0
- package/lib/public/modules/split-view.js +61 -22
- package/lib/public/modules/sticky-notes.js +9 -2
- package/lib/public/modules/tools.js +25 -5
- package/lib/public/modules/workbench-layout-preference.js +76 -0
- package/lib/public/modules/worker-created-presentation.js +37 -0
- package/lib/public/pane-assets/app-2DRBYY3L.js +1 -0
- package/lib/public/pane-assets/chunk-4BM7UVNG.js +2 -0
- package/lib/public/pane-assets/chunk-6SM2WQJX.js +1 -0
- package/lib/public/pane-assets/chunk-7H36SUZA.js +2 -0
- package/lib/public/pane-assets/chunk-C4RVVYNT.js +1 -0
- package/lib/public/pane-assets/chunk-DGOFGEUK.js +1 -0
- package/lib/public/pane-assets/chunk-I57URYSL.js +1 -0
- package/lib/public/pane-assets/chunk-O6MQL6MR.js +167 -0
- package/lib/public/pane-assets/chunk-PO2JOHHW.js +1 -0
- package/lib/public/pane-assets/chunk-UMGHINOT.js +1 -0
- package/lib/public/pane-assets/manifest.json +553 -0
- package/lib/public/pane-assets/style-63EZCREE.css +1 -0
- package/lib/public/style.css +5 -1
- package/lib/server-owner-ui.js +6 -2
- package/lib/server-owner-view-observation.js +34 -0
- package/lib/server-palette.js +1 -0
- package/lib/server-pane-bootstrap.js +24 -0
- package/lib/server-settings.js +2 -0
- package/lib/server-static-assets.js +52 -0
- package/lib/server-workbench-layout-settings.js +56 -0
- package/lib/server.js +6 -45
- package/lib/session-activity-state.js +28 -0
- package/lib/session-list-delivery.js +17 -0
- package/lib/session-pair-turn-control.js +1 -6
- package/lib/session-search.js +3 -0
- package/lib/session-split-group-anchors.js +2 -2
- package/lib/session-split-group-roles.js +5 -8
- package/lib/session-split-group-v2-store.js +0 -3
- package/lib/sessions.js +16 -6
- package/lib/split-view-layout.js +132 -0
- package/lib/users-workbench-layout-preferences.js +33 -0
- package/lib/users.js +3 -0
- package/lib/workspace-query-service.js +1 -5
- package/lib/ws-schema.js +16 -0
- package/package.json +6 -3
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { clayToolDefinition } from './clay-tool-presentation.js';
|
|
2
|
+
|
|
3
|
+
// These are recorded events, not live tool controllers or authority receipts.
|
|
4
|
+
export function workerActivityNames(messages) {
|
|
5
|
+
var names = Object.create(null), ambiguous = Object.create(null);
|
|
6
|
+
messages.forEach(function (message) {
|
|
7
|
+
if (!message.toolId || (message.recordType !== 'tool_start' && message.recordType !== 'tool_use')) return;
|
|
8
|
+
if (names[message.toolId] && names[message.toolId].index !== message.recordIndex) ambiguous[message.toolId] = true;
|
|
9
|
+
names[message.toolId] = { index: message.recordIndex, name: message.toolName };
|
|
10
|
+
});
|
|
11
|
+
Object.keys(ambiguous).forEach(function (id) { delete names[id]; });
|
|
12
|
+
return names;
|
|
13
|
+
}
|
|
14
|
+
export function workerActivityLabel(message, names) {
|
|
15
|
+
var match = message.toolId && names[message.toolId];
|
|
16
|
+
var name = message.toolName || match && match.name;
|
|
17
|
+
var definition = clayToolDefinition(name);
|
|
18
|
+
var generic = { Read: 'Read file', read_file: 'Read file', Edit: 'Edit file', Write: 'Write file',
|
|
19
|
+
Bash: 'Run command', exec_command: 'Run command', Grep: 'Search files', Glob: 'Find files',
|
|
20
|
+
WebSearch: 'Search the web', WebFetch: 'Read web page' };
|
|
21
|
+
return definition ? definition.noun : (Object.prototype.hasOwnProperty.call(generic, name) ? generic[name] : null) || (typeof name === 'string' ? name.slice(0, 120) : 'Tool activity');
|
|
22
|
+
}
|
|
23
|
+
function workerActivityActionIcon(message, names) {
|
|
24
|
+
var match = message.toolId && names[message.toolId];
|
|
25
|
+
var name = message.toolName || match && match.name;
|
|
26
|
+
var definition = clayToolDefinition(name);
|
|
27
|
+
if (definition && typeof definition.icon === 'string' && definition.icon) return definition.icon;
|
|
28
|
+
var generic = { Read: 'file-text', read_file: 'file-text', Edit: 'pencil', Write: 'file-plus-2',
|
|
29
|
+
Bash: 'square-terminal', exec_command: 'square-terminal', Grep: 'search', Glob: 'search', WebSearch: 'globe', WebFetch: 'globe' };
|
|
30
|
+
return Object.prototype.hasOwnProperty.call(generic, name) ? generic[name] : null;
|
|
31
|
+
}
|
|
32
|
+
export function appendWorkerActivity(body, messages, names) {
|
|
33
|
+
var group = document.createElement('section'); group.className = 'worker-activity-group';
|
|
34
|
+
var key = String(messages[messages.length - 1].recordIndex);
|
|
35
|
+
group.dataset.activityKey = key;
|
|
36
|
+
var toggle = document.createElement('button'); toggle.type = 'button'; toggle.className = 'worker-activity-summary';
|
|
37
|
+
toggle.dataset.disclosureKey = 'activity-' + key;
|
|
38
|
+
var activityIcon = document.createElement('i'); activityIcon.setAttribute('data-lucide', 'activity'); activityIcon.setAttribute('aria-hidden', 'true');
|
|
39
|
+
var heading = document.createElement('span'); heading.className = 'worker-activity-heading'; heading.textContent = 'Activity';
|
|
40
|
+
var count = document.createElement('span'); count.className = 'worker-activity-count'; count.textContent = messages.length + (messages.length === 1 ? ' recorded step' : ' recorded steps');
|
|
41
|
+
var chevron = document.createElement('i'); chevron.className = 'worker-activity-chevron';
|
|
42
|
+
chevron.setAttribute('data-lucide', 'chevron-down'); chevron.setAttribute('aria-hidden', 'true');
|
|
43
|
+
toggle.append(activityIcon, heading, count, chevron);
|
|
44
|
+
toggle.setAttribute('aria-expanded', 'false'); toggle.disabled = messages.length <= 2;
|
|
45
|
+
var list = document.createElement('div'); list.className = 'worker-activity-list';
|
|
46
|
+
var items = [];
|
|
47
|
+
messages.forEach(function (message) {
|
|
48
|
+
var item = document.createElement('article'); item.className = 'worker-activity-item worker-tool-message';
|
|
49
|
+
item.dataset.recordIndex = String(message.recordIndex);
|
|
50
|
+
var phase = message.isError ? 'failed' : message.recordType === 'tool_result' ? 'returned' : 'recorded';
|
|
51
|
+
item.dataset.phase = phase;
|
|
52
|
+
var row = document.createElement('div'); row.className = 'worker-activity-row';
|
|
53
|
+
var actionIcon = workerActivityActionIcon(message, names);
|
|
54
|
+
var statusIcon = phase === 'failed' ? 'circle-alert' : phase === 'returned' ? 'corner-down-left' : 'circle-dot';
|
|
55
|
+
var glyph = document.createElement('i'); glyph.className = actionIcon ? 'worker-activity-glyph' : 'worker-activity-glyph worker-activity-status'; glyph.setAttribute('aria-hidden', 'true');
|
|
56
|
+
glyph.setAttribute('data-lucide', actionIcon || statusIcon);
|
|
57
|
+
var label = document.createElement('span'); label.className = 'worker-activity-label'; label.textContent = workerActivityLabel(message, names);
|
|
58
|
+
var detail = document.createElement('span'); detail.className = 'worker-activity-detail';
|
|
59
|
+
detail.textContent = phase === 'failed' ? 'Failed' : phase === 'returned' ? 'Result returned' : 'Call recorded';
|
|
60
|
+
row.append(glyph, label, detail);
|
|
61
|
+
if (actionIcon) {
|
|
62
|
+
var status = document.createElement('i'); status.className = 'worker-activity-status';
|
|
63
|
+
status.setAttribute('data-lucide', statusIcon); status.setAttribute('aria-hidden', 'true'); row.appendChild(status);
|
|
64
|
+
}
|
|
65
|
+
var evidence = document.createElement('details'); evidence.className = 'worker-activity-evidence'; evidence.dataset.disclosureKey = 'record-' + message.recordIndex;
|
|
66
|
+
var summary = document.createElement('summary'); summary.textContent = 'Technical details';
|
|
67
|
+
var output = document.createElement('pre'); output.textContent = message.text;
|
|
68
|
+
evidence.append(summary, output); item.append(row, evidence); items.push(item); list.appendChild(item);
|
|
69
|
+
});
|
|
70
|
+
function expand(open) {
|
|
71
|
+
toggle.setAttribute('aria-expanded', String(open));
|
|
72
|
+
items.forEach(function (item, index) { item.hidden = !open && index < items.length - 2; item.inert = item.hidden; });
|
|
73
|
+
}
|
|
74
|
+
toggle.addEventListener('click', function () { expand(toggle.getAttribute('aria-expanded') !== 'true'); });
|
|
75
|
+
expand(false); group.append(toggle, list); body.appendChild(group);
|
|
76
|
+
}
|
|
77
|
+
export function captureWorkerDisclosures(body) {
|
|
78
|
+
return {
|
|
79
|
+
open: Array.from(body.querySelectorAll('details[open]')).map(function (node) { return node.dataset.disclosureKey; }),
|
|
80
|
+
groups: Array.from(body.querySelectorAll('.worker-activity-summary[aria-expanded="true"]')).map(function (node) { return node.dataset.disclosureKey; }),
|
|
81
|
+
focus: document.activeElement && body.contains(document.activeElement) && (document.activeElement.dataset.disclosureKey || document.activeElement.parentElement && document.activeElement.parentElement.dataset.disclosureKey)
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
export function restoreWorkerDisclosures(body, saved) {
|
|
85
|
+
Array.from(body.querySelectorAll('[data-disclosure-key]')).forEach(function (node) {
|
|
86
|
+
var key = node.dataset.disclosureKey;
|
|
87
|
+
if (saved.groups.indexOf(key) >= 0 && node.getAttribute('aria-expanded') === 'false') node.click();
|
|
88
|
+
if (saved.open.indexOf(key) >= 0) node.open = true;
|
|
89
|
+
if (saved.focus === key) {
|
|
90
|
+
var target = node.tagName === 'DETAILS' ? node.querySelector('summary') : node;
|
|
91
|
+
if (target) target.focus({ preventScroll: true });
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { store } from './store.js';
|
|
2
|
+
import { livePaneBinding } from './pane-binding.js';
|
|
3
|
+
import { getCachedSessions } from './sidebar-sessions.js';
|
|
4
|
+
import { showToast } from './utils.js';
|
|
5
|
+
import { splitGroupRoles } from './split-group-helpers.js';
|
|
6
|
+
|
|
7
|
+
export function initSplitViewWorkerControls(openGroup) {
|
|
8
|
+
document.addEventListener('clay-split-view-open-worker-controls', function (event) {
|
|
9
|
+
var detail = event.detail || {}, state = store.snap(), sessions = getCachedSessions() || [];
|
|
10
|
+
var driver = sessions.find(function (session) { return session.sessionOriginId === detail.driverOriginId && session.id === detail.driverId && session.sessionRole === 'driver'; });
|
|
11
|
+
var view = (state.projectSplitViews || []).find(function (item) { return item.id === state.projectSplitViewActiveId; });
|
|
12
|
+
var singleton = !view && state.projectSplitViewSingletonOrigin === detail.driverOriginId;
|
|
13
|
+
var frame = document.querySelector('.split-pane[data-session-origin="' + CSS.escape(String(detail.driverOriginId || '')) + '"] .split-pane-frame');
|
|
14
|
+
var live = frame && frame.clayPane && frame.clayPane.identity();
|
|
15
|
+
var group = (state.splitGroups || []).find(function (item) { return String(item.id) === String(detail.groupId); });
|
|
16
|
+
var roles = splitGroupRoles(group);
|
|
17
|
+
if (!driver || !(singleton || view && view.sessions.some(function (session) { return session.id === driver.id && session.sessionOriginId === driver.sessionOriginId; })) ||
|
|
18
|
+
detail.projectSlug !== state.currentSlug || state.splitGroupsProjectSlug !== state.currentSlug ||
|
|
19
|
+
detail.ownerId !== (state.myUserId || null) || (driver.ownerId || null) !== (state.myUserId || null) || !frame || frame.dataset.projectSlug !== state.currentSlug ||
|
|
20
|
+
!live || !live.ready || !livePaneBinding(frame) || live.sessionId !== driver.id || live.binding.sessionOriginId !== driver.sessionOriginId ||
|
|
21
|
+
!roles || roles.version !== 2 || roles.driverId !== driver.id || !Array.isArray(detail.workerOrigins) ||
|
|
22
|
+
detail.workerOrigins.length !== group.members.length) return;
|
|
23
|
+
for (var i = 0; i < roles.workerIds.length; i++) {
|
|
24
|
+
var worker = sessions.find(function (session) { return session.id === roles.workerIds[i]; });
|
|
25
|
+
if (!worker || worker.sessionRole !== 'worker' || worker.parentSessionId !== driver.id || worker.parentAvailable !== true ||
|
|
26
|
+
(worker.ownerId || null) !== (driver.ownerId || null) || detail.workerOrigins.indexOf(worker.sessionOriginId) === -1) return;
|
|
27
|
+
}
|
|
28
|
+
if (!openGroup(group)) showToast('Worker controls could not be opened. The Split view remains open.', 'warn');
|
|
29
|
+
});
|
|
30
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { store } from './store.js';
|
|
2
|
+
import { getWs } from './ws-ref.js';
|
|
3
|
+
import { getCachedSessions } from './sidebar-sessions.js';
|
|
4
|
+
import { iconHtml, refreshIcons } from './icons.js';
|
|
5
|
+
import { showConfirm, hideConfirm } from './app-misc.js';
|
|
6
|
+
import { showToast } from './utils.js';
|
|
7
|
+
import { spawnDustParticles } from './sidebar.js';
|
|
8
|
+
import { workerRemovalTarget } from './split-view-workers.js';
|
|
9
|
+
|
|
10
|
+
// Runtime resources belong to this module; the store contains only removal data.
|
|
11
|
+
var removalResources = null;
|
|
12
|
+
|
|
13
|
+
function state() { return store.get('splitViewWorkerRemoval'); }
|
|
14
|
+
function same(left, right) { return !!left && !!right && JSON.stringify(left) === JSON.stringify(right); }
|
|
15
|
+
function live(record) {
|
|
16
|
+
return record && removalResources && getWs() === removalResources.socket && same(record.target, workerRemovalTarget(removalResources.container, record.target.driverOriginId, record.target.workerOriginId, record.target.generation));
|
|
17
|
+
}
|
|
18
|
+
function paint(button, phase) {
|
|
19
|
+
button.classList.toggle('armed', phase === 'armed');
|
|
20
|
+
button.classList.toggle('pending', phase === 'pending' || phase === 'confirming');
|
|
21
|
+
button.disabled = phase === 'pending' || phase === 'confirming';
|
|
22
|
+
button.setAttribute('aria-label', phase === 'armed' ? 'Confirm Worker removal' : phase === 'pending' ? 'Removing Worker' : 'Remove Worker session');
|
|
23
|
+
button.setAttribute('aria-pressed', String(phase === 'armed'));
|
|
24
|
+
button.innerHTML = iconHtml(phase === 'armed' ? 'check' : 'x'); refreshIcons();
|
|
25
|
+
}
|
|
26
|
+
function send(record, action, token, resources) {
|
|
27
|
+
resources = resources || removalResources;
|
|
28
|
+
if (!resources || !resources.socket || resources.socket.readyState !== 1) return false;
|
|
29
|
+
resources.socket.send(JSON.stringify(Object.assign({}, record.target, { type: 'split_worker_remove', requestId: record.requestId, action: action, token: token || null })));
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
function clear(cancel, focus) {
|
|
33
|
+
var record = state(); if (!record) return;
|
|
34
|
+
var resources = removalResources;
|
|
35
|
+
removalResources = null;
|
|
36
|
+
store.set({ splitViewWorkerRemoval: null });
|
|
37
|
+
if (resources && resources.timer) clearTimeout(resources.timer);
|
|
38
|
+
if (cancel && record.requestId) send(record, 'cancel', null, resources);
|
|
39
|
+
if (record.phase === 'confirming') hideConfirm();
|
|
40
|
+
if (resources && resources.button.isConnected) { paint(resources.button, 'idle'); if (focus) resources.button.focus(); }
|
|
41
|
+
}
|
|
42
|
+
function request(record, token) {
|
|
43
|
+
if (!live(record)) { clear(true, false); return; }
|
|
44
|
+
var next = Object.assign({}, record, { phase: 'pending' });
|
|
45
|
+
if (removalResources.timer) clearTimeout(removalResources.timer);
|
|
46
|
+
removalResources.timer = setTimeout(function () {
|
|
47
|
+
if (!state() || state().requestId !== next.requestId || state().phase !== 'pending') return;
|
|
48
|
+
clear(true, true); showToast('Worker removal outcome is unknown. Check the session before trying again.', 'error');
|
|
49
|
+
}, 10000);
|
|
50
|
+
store.set({ splitViewWorkerRemoval: next }); paint(removalResources.button, 'pending');
|
|
51
|
+
try { if (!send(next, token ? 'confirm' : 'request', token)) throw new Error('Connection lost.'); }
|
|
52
|
+
catch (error) { clear(true, true); showToast('Worker removal could not be sent.', 'error'); }
|
|
53
|
+
}
|
|
54
|
+
export function reconcileWorkerRemoval(container) {
|
|
55
|
+
var record = state(); if (!record || !removalResources || removalResources.container !== container) return;
|
|
56
|
+
var frame = container.querySelector('.split-pane-frame');
|
|
57
|
+
var stillListed = (getCachedSessions() || []).some(function (session) { return session.sessionOriginId === record.target.workerOriginId; });
|
|
58
|
+
if (!live(record) && (record.phase !== 'pending' || stillListed) || container.hidden || container.inert || !container.isConnected || frame && (frame.hidden || frame.inert)) clear(true, false);
|
|
59
|
+
}
|
|
60
|
+
export function appendWorkerRemoval(pill, button, container, driver, worker) {
|
|
61
|
+
var remove = document.createElement('button'); remove.type = 'button'; remove.className = 'split-view-worker-remove';
|
|
62
|
+
remove.dataset.workerOrigin = worker.sessionOriginId; remove.dataset.workerGeneration = String(worker.workerGeneration);
|
|
63
|
+
paint(remove, 'idle'); pill.appendChild(remove);
|
|
64
|
+
var target = workerRemovalTarget(container, driver.sessionOriginId, worker.sessionOriginId, worker.workerGeneration);
|
|
65
|
+
remove.disabled = !target;
|
|
66
|
+
var existing = state();
|
|
67
|
+
if (existing && same(existing.target, target)) {
|
|
68
|
+
removalResources.button = remove; removalResources.container = container;
|
|
69
|
+
paint(remove, existing.phase);
|
|
70
|
+
}
|
|
71
|
+
remove.addEventListener('click', function (event) {
|
|
72
|
+
event.preventDefault(); event.stopPropagation();
|
|
73
|
+
var current = workerRemovalTarget(container, driver.sessionOriginId, worker.sessionOriginId, worker.workerGeneration);
|
|
74
|
+
if (!remove.isConnected || remove.disabled || !same(current, target)) { clear(true, false); return; }
|
|
75
|
+
var previous = state();
|
|
76
|
+
if (previous && same(previous.target, target) && previous.phase === 'armed') {
|
|
77
|
+
var rect = pill.getBoundingClientRect();
|
|
78
|
+
var record = Object.assign({}, previous, { requestId: 'remove_' + crypto.randomUUID().replace(/-/g, ''),
|
|
79
|
+
center: { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 } });
|
|
80
|
+
request(record); return;
|
|
81
|
+
}
|
|
82
|
+
clear(true, false);
|
|
83
|
+
removalResources = { socket: getWs(), container: container, button: remove, timer: null };
|
|
84
|
+
store.set({ splitViewWorkerRemoval: { phase: 'armed', target: target } });
|
|
85
|
+
paint(remove, 'armed');
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
export function handleWorkerRemovalResult(msg) {
|
|
89
|
+
var record = state();
|
|
90
|
+
if (!record || !removalResources || record.requestId !== msg.requestId || record.target.workerOriginId !== msg.workerOriginId ||
|
|
91
|
+
record.target.generation !== msg.generation || record.target.projectSlug !== msg.projectSlug || getWs() !== removalResources.socket) return;
|
|
92
|
+
if (msg.status === 'removed') {
|
|
93
|
+
var container = removalResources.container, center = record.center;
|
|
94
|
+
var visible = store.get('connected') && store.get('currentSlug') === record.target.projectSlug &&
|
|
95
|
+
(store.get('myUserId') || null) === record.target.ownerId && container.isConnected && !container.hidden && !container.inert;
|
|
96
|
+
clear(false, false);
|
|
97
|
+
if (visible) {
|
|
98
|
+
spawnDustParticles(center.x, center.y, { containerClass: 'worker-removal-particles' });
|
|
99
|
+
var focus = container.querySelector('.split-view-worker-chip') || container.querySelector('.split-pane-info');
|
|
100
|
+
if (focus) focus.focus();
|
|
101
|
+
}
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (!live(record)) { clear(true, false); return; }
|
|
105
|
+
if (msg.status === 'confirmation_required' && record.phase === 'pending' && msg.token) {
|
|
106
|
+
clearTimeout(removalResources.timer); removalResources.timer = null;
|
|
107
|
+
var next = Object.assign({}, record, { phase: 'confirming' });
|
|
108
|
+
store.set({ splitViewWorkerRemoval: next }); paint(removalResources.button, 'confirming');
|
|
109
|
+
showConfirm('Stop and remove Worker? This deletes Split Worker #' + record.target.workerId + ' and its conversation.', function () {
|
|
110
|
+
if (state() && state().requestId === next.requestId) request(state(), msg.token);
|
|
111
|
+
}, 'Stop and remove Worker', true, function () { if (state() && state().phase === 'confirming') clear(true, true); });
|
|
112
|
+
var ok = document.getElementById('confirm-ok'); if (ok) ok.focus();
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
clear(false, true); showToast(msg.error || 'Worker removal was not completed.', 'error');
|
|
116
|
+
}
|
|
117
|
+
document.addEventListener('keydown', function (event) {
|
|
118
|
+
if (event.key === 'Escape' && state()) { event.preventDefault(); event.stopImmediatePropagation(); clear(true, true); }
|
|
119
|
+
}, true);
|
|
120
|
+
document.addEventListener('click', function (event) {
|
|
121
|
+
var record = state(); if (!record || record.phase !== 'armed') return;
|
|
122
|
+
if (removalResources && (event.target === removalResources.button || removalResources.button.contains(event.target))) return;
|
|
123
|
+
clear(true, false);
|
|
124
|
+
}, true);
|
|
125
|
+
store.subscribe(function (next, previous) {
|
|
126
|
+
var record = state(); if (!record) return;
|
|
127
|
+
if (next.connected !== previous.connected || next.currentSlug !== previous.currentSlug || next.myUserId !== previous.myUserId ||
|
|
128
|
+
next.projectSplitViewRevision !== previous.projectSplitViewRevision || next.projectSplitViewActiveId !== previous.projectSplitViewActiveId ||
|
|
129
|
+
next.projectSplitViewSocketGeneration !== previous.projectSplitViewSocketGeneration ||
|
|
130
|
+
next.projectSplitViewFocusedOrigin !== previous.projectSplitViewFocusedOrigin) { clear(true, false); return; }
|
|
131
|
+
if (next.paneBindingSessions !== previous.paneBindingSessions && !live(record)) {
|
|
132
|
+
// Deletion broadcasts may precede their receipt. Retain only that pending
|
|
133
|
+
// receipt; absence from the list alone never claims removal or animates it.
|
|
134
|
+
var stillListed = (getCachedSessions() || []).some(function (session) { return session.sessionOriginId === record.target.workerOriginId; });
|
|
135
|
+
if (record.phase !== 'pending' || stillListed) clear(true, false);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Read-only transcript paging and viewport anchors. Request state lives in store.
|
|
2
|
+
export function workerCursorKey(cursor) {
|
|
3
|
+
if (cursor == null) return 'latest';
|
|
4
|
+
if (Number.isInteger(cursor) && cursor >= 0) return String(cursor);
|
|
5
|
+
return cursor && Number.isInteger(cursor.index) && cursor.index >= 0 && Number.isInteger(cursor.offset) && cursor.offset >= 0 ? cursor.index + ':' + cursor.offset : null;
|
|
6
|
+
}
|
|
7
|
+
export function workerCursorAdvances(before, next) {
|
|
8
|
+
if (!next || workerCursorKey(next) === null) return false;
|
|
9
|
+
if (before == null) return true;
|
|
10
|
+
if (Number.isInteger(before)) return next.index < before;
|
|
11
|
+
return next.index < before.index || next.index === before.index && next.offset < before.offset;
|
|
12
|
+
}
|
|
13
|
+
export function shouldLoadWorkerPage(state, body, intentional) {
|
|
14
|
+
if (!state || !body || state.inFlight || !state.hasMore || state.nextBefore == null || !workerCursorKey(state.nextBefore)) return false;
|
|
15
|
+
if (state.status === 'error' && !intentional) return false;
|
|
16
|
+
if (state.status !== 'ready' && state.status !== 'error') return false;
|
|
17
|
+
return intentional ? body.scrollTop <= 96 : body.clientHeight > 0 && body.scrollHeight <= body.clientHeight + 1;
|
|
18
|
+
}
|
|
19
|
+
export function captureWorkerAnchor(body) {
|
|
20
|
+
var top = body.getBoundingClientRect().top;
|
|
21
|
+
var article = Array.from(body.querySelectorAll('article[data-record-index]')).find(function (item) { return !item.hidden && item.getBoundingClientRect().bottom > top; });
|
|
22
|
+
if (!article) return { scrollTop: body.scrollTop, height: body.scrollHeight };
|
|
23
|
+
var anchor = { index: article.dataset.recordIndex, bottom: article.getBoundingClientRect().bottom - top,
|
|
24
|
+
scrollTop: body.scrollTop, height: body.scrollHeight };
|
|
25
|
+
var content = article.querySelector('.message-body') || article.querySelector('pre');
|
|
26
|
+
var rect = body.getBoundingClientRect();
|
|
27
|
+
var range = document.caretRangeFromPoint && document.caretRangeFromPoint(rect.left + Math.min(45, rect.width / 2), top + 16);
|
|
28
|
+
if (range && content && content.contains(range.startContainer) && range.startContainer.nodeType === 3) {
|
|
29
|
+
var prefix = document.createRange(); prefix.selectNodeContents(content); prefix.setEnd(range.startContainer, range.startOffset);
|
|
30
|
+
var offset = prefix.toString().length, text = content.textContent;
|
|
31
|
+
var start = Math.max(0, offset - 24), end = Math.min(text.length, offset + 48);
|
|
32
|
+
anchor.text = text.slice(start, end); anchor.offset = offset - start;
|
|
33
|
+
var character = range.cloneRange(); character.setEnd(range.startContainer, Math.min(range.startOffset + 1, range.startContainer.length));
|
|
34
|
+
anchor.y = character.getBoundingClientRect().top - top;
|
|
35
|
+
}
|
|
36
|
+
return anchor;
|
|
37
|
+
}
|
|
38
|
+
export function restoreWorkerAnchor(body, anchor) {
|
|
39
|
+
if (!anchor) return;
|
|
40
|
+
var article = Array.from(body.querySelectorAll('article[data-record-index]')).find(function (item) { return item.dataset.recordIndex === anchor.index; });
|
|
41
|
+
if (!article) { body.scrollTop = anchor.scrollTop + body.scrollHeight - anchor.height; return; }
|
|
42
|
+
var content = article.querySelector('.message-body') || article.querySelector('pre');
|
|
43
|
+
var at = content && anchor.text ? content.textContent.lastIndexOf(anchor.text) : -1;
|
|
44
|
+
if (at >= 0 && document.createTreeWalker) {
|
|
45
|
+
var walker = document.createTreeWalker(content, 4), node, offset = at + anchor.offset;
|
|
46
|
+
while ((node = walker.nextNode())) {
|
|
47
|
+
if (offset < node.length) {
|
|
48
|
+
var range = document.createRange(); range.setStart(node, offset); range.setEnd(node, Math.min(offset + 1, node.length));
|
|
49
|
+
body.scrollTop += range.getBoundingClientRect().top - body.getBoundingClientRect().top - anchor.y;
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
offset -= node.length;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// The last record key remains stable when earlier streaming fragments merge.
|
|
56
|
+
body.scrollTop += article.getBoundingClientRect().bottom - body.getBoundingClientRect().top - anchor.bottom;
|
|
57
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { appendWorkerActivity, workerActivityNames } from './split-view-worker-activity.js';
|
|
2
|
+
import { renderMarkdown } from './markdown.js';
|
|
3
|
+
|
|
4
|
+
// Reassemble bounded record fragments and contiguous streaming deltas before
|
|
5
|
+
// parsing Markdown, including fences and lists that cross a page boundary.
|
|
6
|
+
export function workerTranscriptRows(messages) {
|
|
7
|
+
var rows = [];
|
|
8
|
+
(messages || []).forEach(function (message) {
|
|
9
|
+
if (!message || typeof message.text !== 'string' || ['user', 'assistant', 'tool'].indexOf(message.role) < 0) return;
|
|
10
|
+
var last = rows[rows.length - 1];
|
|
11
|
+
var fragment = last && Number.isInteger(message.recordIndex) && last.recordIndex === message.recordIndex && last.end === message.start;
|
|
12
|
+
var delta = last && message.recordType === 'delta' && last.recordType === 'delta' && message.start === 0 && message.recordIndex === last.recordIndex + 1;
|
|
13
|
+
if (last && last.role === message.role && (fragment || delta)) {
|
|
14
|
+
last.text += message.text; last.end = message.end; last.recordIndex = message.recordIndex;
|
|
15
|
+
} else rows.push(Object.assign({}, message));
|
|
16
|
+
});
|
|
17
|
+
return rows;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function appendWorkerMessage(body, message) {
|
|
21
|
+
if (message.role === 'tool') { appendWorkerActivity(body, [message], workerActivityNames([message])); return; }
|
|
22
|
+
var article = document.createElement('article');
|
|
23
|
+
article.className = 'message worker-readonly-message ' + (message.role === 'user' ? 'msg-user' : 'msg-assistant');
|
|
24
|
+
if (Number.isInteger(message.recordIndex)) article.dataset.recordIndex = String(message.recordIndex);
|
|
25
|
+
var author = document.createElement('strong'); author.className = 'author';
|
|
26
|
+
author.textContent = message.role === 'user' ? 'You' : 'Worker';
|
|
27
|
+
var content = document.createElement('div'); content.className = 'message-body md-content';
|
|
28
|
+
// Recorded HTML must not create controls, scoped overlays or active embeds.
|
|
29
|
+
// Keep the shared Markdown parser and its sanitization, with a narrower
|
|
30
|
+
// read-only prose vocabulary for this secondary history surface.
|
|
31
|
+
content.innerHTML = DOMPurify.sanitize(renderMarkdown(message.text), {
|
|
32
|
+
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'del', 's', 'blockquote', 'ul', 'ol', 'li', 'pre', 'code', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 'a', 'wbr'],
|
|
33
|
+
ALLOWED_ATTR: ['href', 'title', 'target', 'rel', 'start'], ALLOW_DATA_ATTR: false
|
|
34
|
+
});
|
|
35
|
+
article.append(author, content); body.appendChild(article);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function appendWorkerTranscript(body, messages) {
|
|
39
|
+
var rows = workerTranscriptRows(messages), names = workerActivityNames(rows), pending = [];
|
|
40
|
+
function flush() { if (pending.length) appendWorkerActivity(body, pending, names); pending = []; }
|
|
41
|
+
rows.forEach(function (message) {
|
|
42
|
+
if (message.role === 'tool') pending.push(message);
|
|
43
|
+
else { flush(); appendWorkerMessage(body, message); }
|
|
44
|
+
});
|
|
45
|
+
flush();
|
|
46
|
+
}
|