clay-server 5.2.0-beta.7 → 5.2.0-beta.9
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/pane-pending-interactions.js +20 -0
- package/lib/pane-session-activation.js +114 -0
- package/lib/pane-session-transport.js +26 -0
- package/lib/project-sessions.js +15 -5
- package/lib/project.js +11 -3
- package/lib/public/app.js +14 -0
- package/lib/public/css/contextual-clay.css +1 -1
- package/lib/public/css/input.css +6 -0
- package/lib/public/css/sticky-notes.css +37 -11
- package/lib/public/modules/app-connection.js +3 -0
- package/lib/public/modules/app-message-router.js +13 -0
- package/lib/public/modules/app-messages.js +25 -5
- package/lib/public/modules/app-misc.js +3 -1
- package/lib/public/modules/app-projects.js +5 -0
- package/lib/public/modules/clay-owner-navigation.js +0 -3
- package/lib/public/modules/clay-studio-render.js +3 -1
- package/lib/public/modules/command-palette.js +52 -14
- package/lib/public/modules/contextual-clay-workspace.js +14 -2
- package/lib/public/modules/contextual-clay.js +16 -3
- package/lib/public/modules/contextual-mate-context.js +6 -4
- package/lib/public/modules/elicitation-form.js +5 -0
- package/lib/public/modules/history-replay-batch.js +8 -0
- package/lib/public/modules/input.js +55 -11
- package/lib/public/modules/message-delivery.js +10 -2
- package/lib/public/modules/model-picker.js +2 -0
- package/lib/public/modules/pane-activation.js +214 -0
- package/lib/public/modules/pane-binding.js +82 -0
- package/lib/public/modules/pane-bridge.js +5 -2
- package/lib/public/modules/pane-browser-bridge.js +5 -3
- package/lib/public/modules/pane-file-bridge.js +4 -0
- package/lib/public/modules/pane-issue-bridge.js +4 -1
- package/lib/public/modules/rewind.js +5 -0
- package/lib/public/modules/session-actions.js +5 -2
- package/lib/public/modules/session-context-menu.js +5 -0
- package/lib/public/modules/session-rich-drafts.js +21 -0
- package/lib/public/modules/session-switch-defer.js +67 -0
- package/lib/public/modules/split-group-restore.js +68 -0
- package/lib/public/modules/split-pair-ui.js +1 -0
- package/lib/public/modules/split-pane-reconciler.js +15 -0
- package/lib/public/modules/split-pane-renderer.js +87 -6
- package/lib/public/modules/split-session-boundary.js +1 -1
- package/lib/public/modules/split-view.js +51 -52
- package/lib/public/modules/sticky-notes-checklist.js +51 -6
- package/lib/public/modules/stt-controller.js +4 -1
- package/lib/public/modules/stt-recording.js +1 -1
- package/lib/public/modules/stt.js +7 -2
- package/lib/public/modules/tools.js +18 -2
- package/lib/sessions.js +17 -10
- package/package.json +4 -3
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { store } from './store.js';
|
|
2
|
+
import { getWs } from './ws-ref.js';
|
|
3
|
+
import { getCachedSessions } from './sidebar-sessions.js';
|
|
4
|
+
import { isCurrentProjectSessionReady } from './split-session-boundary.js';
|
|
5
|
+
import { splitPanesMatchProject, reconcileRestoredSplit } from './project-activation.js';
|
|
6
|
+
import { splitGroupMemberIds } from './split-group-helpers.js';
|
|
7
|
+
import { splitPaneForSession } from './split-pane-reconciler.js';
|
|
8
|
+
import { clearProjectSplitState } from './split-session-boundary.js';
|
|
9
|
+
|
|
10
|
+
export function reconcileSplitIdentity(state, previous, host) {
|
|
11
|
+
var ownerChanged = (previous.authIdentityLoaded === true || state.splitIdentityHydrated) && state.myUserId !== previous.myUserId;
|
|
12
|
+
if (state.authIdentityLoaded && !state.splitIdentityHydrated) store.set({ splitIdentityHydrated: true });
|
|
13
|
+
if (state.currentSlug === previous.currentSlug && state.myUserId === previous.myUserId) return false;
|
|
14
|
+
Array.from(host.querySelectorAll('.split-pane-frame')).forEach(function (frame) {
|
|
15
|
+
if (frame.clayPane) frame.clayPane.suspend();
|
|
16
|
+
});
|
|
17
|
+
host.innerHTML = '';
|
|
18
|
+
if (ownerChanged) {
|
|
19
|
+
// This socket was authenticated as the previous owner. Fresh metadata on
|
|
20
|
+
// that connection cannot establish the replacement owner's pane boundary.
|
|
21
|
+
store.set({ splitIdentityBlockedSocket: getWs(), paneBindingSessions: [] });
|
|
22
|
+
clearProjectSplitState();
|
|
23
|
+
} else if (state.splitPanes) store.set({ splitPanes: null });
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// The initial owner response can arrive after history and split metadata.
|
|
28
|
+
// Wait for it so identity cleanup cannot discard panes we just restored.
|
|
29
|
+
export function restoreSplitGroup(openGroup) {
|
|
30
|
+
var state = store.snap();
|
|
31
|
+
if (state.authIdentityLoaded === false || state.isMultiUserMode && !state.myUserId) return;
|
|
32
|
+
if (!isCurrentProjectSessionReady(state, getWs())) return;
|
|
33
|
+
var split = state.splitPanes;
|
|
34
|
+
if (split && !splitPanesMatchProject(split, state.currentSlug)) {
|
|
35
|
+
store.set({ splitPanes: null });
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (state.paneMode) return;
|
|
39
|
+
if (split) {
|
|
40
|
+
var reconciliation = reconcileRestoredSplit(split, state.splitGroups);
|
|
41
|
+
if (reconciliation.action === "clear") {
|
|
42
|
+
store.set({ splitPanes: null });
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (reconciliation.action === "rebuild") {
|
|
46
|
+
var rebuilt = reconciliation.group;
|
|
47
|
+
if (rebuilt.members.some(function (id) { return !getCachedSessions().some(function (session) { return session.id === id; }); })) {
|
|
48
|
+
store.set({ splitPanes: null });
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
store.set({ splitPanes: {
|
|
52
|
+
groupId: rebuilt.id,
|
|
53
|
+
panes: splitGroupMemberIds(rebuilt).map(function (id) { return splitPaneForSession(id, state.currentSlug, getCachedSessions()); }),
|
|
54
|
+
} });
|
|
55
|
+
}
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
var activeId = store.get('activeSessionId');
|
|
59
|
+
if (!activeId) return;
|
|
60
|
+
var groups = store.get('splitGroups') || [];
|
|
61
|
+
for (var i = 0; i < groups.length; i++) {
|
|
62
|
+
var members = groups[i].members || [];
|
|
63
|
+
if (members.indexOf(activeId) !== -1) {
|
|
64
|
+
openGroup(groups[i]);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -236,6 +236,7 @@ function sendSetPair(groupId, driverId) {
|
|
|
236
236
|
|
|
237
237
|
export function syncPairChrome(host, split) {
|
|
238
238
|
if (!host || !split || !split.groupId) return;
|
|
239
|
+
if (Array.from(host.querySelectorAll('.split-pane-frame')).some(function (frame) { return !!frame.dataset.requestedSessionId; })) return;
|
|
239
240
|
var groups = store.get('splitGroups') || [];
|
|
240
241
|
var group = null;
|
|
241
242
|
for (var i = 0; i < groups.length; i++) {
|
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
// (and their iframe browsing contexts) are reused by stable project/session
|
|
3
3
|
// identity while metadata and visual order are refreshed.
|
|
4
4
|
|
|
5
|
+
export function splitPaneForSession(id, slug, sessions) {
|
|
6
|
+
var session = (sessions || []).find(function (item) { return item.id === id; });
|
|
7
|
+
return { slug: slug, sessionId: id, title: session && session.title || ('Session ' + id) };
|
|
8
|
+
}
|
|
9
|
+
|
|
5
10
|
export function splitPaneIdentity(pane) {
|
|
6
11
|
return String(pane && pane.slug || "") + "#" + String(pane && pane.sessionId || "");
|
|
7
12
|
}
|
|
@@ -16,9 +21,19 @@ export function reconcileSplitPanes(host, panes, createPane, updatePane) {
|
|
|
16
21
|
var pane = desired[pi];
|
|
17
22
|
var key = splitPaneIdentity(pane);
|
|
18
23
|
var paneEl = byKey.get(key);
|
|
24
|
+
if (!paneEl) {
|
|
25
|
+
for (var ci = 0; ci < existing.length; ci++) {
|
|
26
|
+
var candidate = existing[ci], frame = candidate.querySelector('.split-pane-frame');
|
|
27
|
+
if (!used.has(candidate) && frame && frame.dataset.projectSlug === pane.slug && frame.clayPane &&
|
|
28
|
+
!desired.some(function (item) { return splitPaneIdentity(item) === candidate.dataset.splitPaneKey; })) {
|
|
29
|
+
paneEl = candidate; break;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
19
33
|
if (!paneEl) paneEl = createPane(pane, pi);
|
|
20
34
|
paneEl.dataset.splitPaneKey = key;
|
|
21
35
|
updatePane(paneEl, pane, pi);
|
|
36
|
+
if (paneEl.style) paneEl.style.order = String(pi);
|
|
22
37
|
used.add(paneEl);
|
|
23
38
|
if (!paneEl.parentNode) host.appendChild(paneEl);
|
|
24
39
|
}
|
|
@@ -6,6 +6,8 @@ import { formatTokens } from './app-panels.js';
|
|
|
6
6
|
import { VENDOR_AVATARS, VENDOR_NAMES } from './app-rendering.js';
|
|
7
7
|
import { createPermissionControl, bindPermissionControl, renderPermissionControl } from './permission-control.js';
|
|
8
8
|
import { splitGroupRoles } from './split-group-helpers.js';
|
|
9
|
+
import { paneTarget, livePaneBinding, samePaneTarget } from './pane-binding.js';
|
|
10
|
+
import { syncPairChrome } from './split-pair-ui.js';
|
|
9
11
|
|
|
10
12
|
function sessionById(sessionId) {
|
|
11
13
|
var sessions = getCachedSessions() || [];
|
|
@@ -17,17 +19,31 @@ function paneUrl(pane) {
|
|
|
17
19
|
return "/p/" + encodeURIComponent(pane.slug) + "/?pane=1&session=" + encodeURIComponent(pane.sessionId);
|
|
18
20
|
}
|
|
19
21
|
|
|
22
|
+
function headerBindingStamp(frame) {
|
|
23
|
+
var live = frame.clayPane && frame.clayPane.identity();
|
|
24
|
+
return JSON.stringify([frame.dataset.sessionId, frame.dataset.sessionOriginId,
|
|
25
|
+
live && live.binding && live.binding.requestId, live && live.binding && live.binding.generation]);
|
|
26
|
+
}
|
|
27
|
+
|
|
20
28
|
function startPaneRename(header, titleEl, pane) {
|
|
21
29
|
if (header.querySelector(".split-pane-rename-input")) return;
|
|
22
30
|
var input = document.createElement("input");
|
|
23
31
|
input.type = "text"; input.className = "split-pane-rename-input"; input.value = pane.title;
|
|
24
32
|
titleEl.style.display = "none"; header.insertBefore(input, titleEl); input.focus(); input.select();
|
|
25
33
|
var done = false;
|
|
34
|
+
var section = header.parentNode, frame = section.querySelector('.split-pane-frame');
|
|
35
|
+
var origin = frame.dataset.sessionOriginId, binding = frame.clayPane && frame.clayPane.identity().binding, socket = getWs();
|
|
36
|
+
function current() {
|
|
37
|
+
var live = frame.clayPane && frame.clayPane.identity();
|
|
38
|
+
return header.isConnected && !header.inert && socket === getWs() && store.get('connected') &&
|
|
39
|
+
frame.dataset.sessionId === String(pane.sessionId) && frame.dataset.sessionOriginId === origin && !frame.dataset.requestedSessionId &&
|
|
40
|
+
(!frame.clayPane || livePaneBinding(frame) && live.binding === binding);
|
|
41
|
+
}
|
|
26
42
|
function finish(commit) {
|
|
27
43
|
if (done) return;
|
|
28
44
|
done = true;
|
|
29
45
|
var newTitle = input.value.trim(); input.remove(); titleEl.style.display = "";
|
|
30
|
-
if (!commit || !newTitle || newTitle === pane.title) return;
|
|
46
|
+
if (!commit || !current() || !newTitle || newTitle === pane.title) return;
|
|
31
47
|
pane.title = newTitle; titleEl.textContent = newTitle;
|
|
32
48
|
var ws = getWs();
|
|
33
49
|
if (ws && ws.readyState === 1) ws.send(JSON.stringify({ type: "rename_session", id: pane.sessionId, title: newTitle }));
|
|
@@ -37,6 +53,7 @@ function startPaneRename(header, titleEl, pane) {
|
|
|
37
53
|
if (event.key === "Escape") { event.preventDefault(); finish(false); }
|
|
38
54
|
});
|
|
39
55
|
input.addEventListener("blur", function () { finish(true); });
|
|
56
|
+
header.addEventListener('clay-pane-retarget', function cancel() { finish(false); header.removeEventListener('clay-pane-retarget', cancel); });
|
|
40
57
|
input.addEventListener("click", function (event) { event.stopPropagation(); });
|
|
41
58
|
}
|
|
42
59
|
|
|
@@ -73,33 +90,97 @@ export function createSplitPane(pane, closePane) {
|
|
|
73
90
|
var vendorIcon = document.createElement("img"); vendorIcon.className = "split-pane-vendor";
|
|
74
91
|
vendorIcon.src = VENDOR_AVATARS[vendor] || VENDOR_AVATARS.claude; vendorIcon.alt = ""; vendorIcon.title = VENDOR_NAMES[vendor] || vendor; header.appendChild(vendorIcon);
|
|
75
92
|
var title = document.createElement("span"); title.className = "split-pane-title"; title.textContent = pane.title; title.title = "Rename session";
|
|
76
|
-
title.addEventListener("click", function () { startPaneRename(header, title,
|
|
93
|
+
title.addEventListener("click", function () { var current = sessionById(Number(paneEl.dataset.sessionId)); if (current) startPaneRename(header, title, { sessionId: current.id, title: current.title }); }); header.appendChild(title);
|
|
77
94
|
var frame = document.createElement("iframe"); frame.className = "split-pane-frame"; frame.dataset.projectSlug = pane.slug;
|
|
78
95
|
frame.dataset.sessionId = String(pane.sessionId); frame.src = paneUrl(pane); frame.title = pane.title;
|
|
96
|
+
frame.dataset.sessionOriginId = session && session.sessionOriginId || '';
|
|
97
|
+
header.addEventListener('pointerdown', function () { header.dataset.pointerSessionId = frame.dataset.sessionId; header.dataset.pointerBinding = headerBindingStamp(frame); });
|
|
98
|
+
header.addEventListener('click', function (event) {
|
|
99
|
+
if (frame.clayPane && (!livePaneBinding(frame) || event.detail && header.dataset.pointerBinding !== headerBindingStamp(frame))) {
|
|
100
|
+
event.preventDefault(); event.stopImmediatePropagation();
|
|
101
|
+
}
|
|
102
|
+
}, true);
|
|
103
|
+
['keydown', 'keyup'].forEach(function (name) {
|
|
104
|
+
header.addEventListener(name, function (event) {
|
|
105
|
+
var stamp = headerBindingStamp(frame);
|
|
106
|
+
if (name === 'keydown' && !event.repeat) header.dataset.keyBinding = stamp;
|
|
107
|
+
if (header.inert || header.dataset.keyBinding !== stamp) { event.preventDefault(); event.stopImmediatePropagation(); }
|
|
108
|
+
}, true);
|
|
109
|
+
});
|
|
79
110
|
var ctxChip = document.createElement("button"); ctxChip.type = "button"; ctxChip.className = "split-pane-context"; ctxChip.title = "Context usage";
|
|
80
111
|
ctxChip.innerHTML = '<span class="split-pane-context-bar"><span class="split-pane-context-fill"></span></span><span class="split-pane-context-label"></span>';
|
|
81
|
-
ctxChip.addEventListener("click", function () { if (frame.contentWindow) frame.contentWindow.postMessage({ type: "clay-pane-toggle-context" }, window.location.origin); }); header.appendChild(ctxChip);
|
|
112
|
+
ctxChip.addEventListener("click", function () { if (frame.contentWindow && (!frame.clayPane || livePaneBinding(frame))) frame.contentWindow.postMessage({ type: "clay-pane-toggle-context", paneBinding: frame.clayPane && frame.clayPane.identity().binding }, window.location.origin); }); header.appendChild(ctxChip);
|
|
82
113
|
var fullAccess = createPermissionControl("split-pane-full-access hidden", "Driver session permission mode");
|
|
83
|
-
updatePaneFullAccessButton(fullAccess, session); bindPermissionControl(fullAccess, function () { return permissionState(sessionById(
|
|
114
|
+
updatePaneFullAccessButton(fullAccess, session); bindPermissionControl(fullAccess, function () { return permissionState(sessionById(Number(paneEl.dataset.sessionId))); }); header.appendChild(fullAccess); header.insertBefore(fullAccess, ctxChip);
|
|
84
115
|
var close = document.createElement("button"); close.type = "button"; close.className = "split-pane-close"; close.title = "Close pane"; close.setAttribute("aria-label", "Close pane"); close.innerHTML = iconHtml("x");
|
|
85
116
|
close.dataset.sessionId = String(pane.sessionId); close.addEventListener("click", function () { closePane(Number(this.dataset.sessionId)); }); header.appendChild(close);
|
|
117
|
+
frame.addEventListener('clay-pane-ready', function () {
|
|
118
|
+
var id = Number(frame.dataset.requestedSessionId || frame.dataset.sessionId), current = sessionById(id);
|
|
119
|
+
var live = frame.clayPane && frame.clayPane.identity(), target = paneTarget(store.snap(), getCachedSessions(), id);
|
|
120
|
+
if (!current || !live || !live.ready || !samePaneTarget(target, live.binding) || live.sessionId !== id) return;
|
|
121
|
+
frame.dataset.sessionId = String(id); delete frame.dataset.requestedSessionId;
|
|
122
|
+
frame.dataset.sessionOriginId = live.binding.sessionOriginId;
|
|
123
|
+
updateSplitPaneMetadata(paneEl, { slug: frame.dataset.projectSlug, sessionId: id, title: current.title });
|
|
124
|
+
syncPairChrome(paneEl.parentNode, store.get('splitPanes'));
|
|
125
|
+
});
|
|
126
|
+
frame.addEventListener('clay-pane-activation-failed', function () {
|
|
127
|
+
var id = Number(frame.dataset.requestedSessionId || frame.dataset.sessionId);
|
|
128
|
+
var fallbackTarget = paneTarget(store.snap(), getCachedSessions(), id);
|
|
129
|
+
if (!fallbackTarget) { header.inert = true; return; }
|
|
130
|
+
frame.dataset.sessionOriginId = fallbackTarget.sessionOriginId;
|
|
131
|
+
frame.src = paneUrl({ slug: frame.dataset.projectSlug, sessionId: id });
|
|
132
|
+
frame.dataset.sessionId = String(id); delete frame.dataset.requestedSessionId;
|
|
133
|
+
});
|
|
86
134
|
paneEl.appendChild(header); paneEl.appendChild(frame); return paneEl;
|
|
87
135
|
}
|
|
88
136
|
|
|
89
137
|
export function updateSplitPaneMetadata(paneEl, pane) {
|
|
90
138
|
var session = sessionById(pane.sessionId); var header = paneEl.querySelector(".split-pane-header");
|
|
91
139
|
var title = paneEl.querySelector(".split-pane-title"); var frame = paneEl.querySelector(".split-pane-frame"); var vendor = paneEl.querySelector(".split-pane-vendor");
|
|
140
|
+
var live = frame && frame.clayPane && frame.clayPane.identity();
|
|
141
|
+
var retarget = frame && (Number(frame.dataset.sessionId) !== pane.sessionId || frame.dataset.sessionOriginId !== (session && session.sessionOriginId || '') || frame.clayPane && (!live || !live.ready));
|
|
142
|
+
if (retarget && header) {
|
|
143
|
+
header.dispatchEvent(new Event('clay-pane-retarget'));
|
|
144
|
+
header.inert = true;
|
|
145
|
+
header.title = 'Loading conversation';
|
|
146
|
+
if (title) { title.style.display = ''; title.textContent = 'Loading conversation'; }
|
|
147
|
+
if (vendor) vendor.hidden = true;
|
|
148
|
+
var permission = paneEl.querySelector('.split-pane-full-access');
|
|
149
|
+
if (permission) permission.classList.add('hidden');
|
|
150
|
+
resetPaneContextChip(paneEl);
|
|
151
|
+
}
|
|
152
|
+
if (frame && frame.clayPane && retarget) {
|
|
153
|
+
var target = paneTarget(store.snap(), getCachedSessions(), pane.sessionId);
|
|
154
|
+
if (target && frame.clayPane.source === frame.contentWindow && frame.clayPane.document === frame.contentDocument && frame.clayPane.activate(pane.sessionId, false, target)) {
|
|
155
|
+
frame.dataset.requestedSessionId = String(pane.sessionId);
|
|
156
|
+
if (header) header.inert = true;
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
frame.src = paneUrl(pane);
|
|
160
|
+
frame.dataset.sessionId = String(pane.sessionId); frame.dataset.projectSlug = pane.slug;
|
|
161
|
+
frame.dataset.sessionOriginId = session && session.sessionOriginId || '';
|
|
162
|
+
}
|
|
163
|
+
if (header) header.inert = false;
|
|
92
164
|
paneEl.dataset.sessionId = String(pane.sessionId); paneEl.dataset.projectSlug = pane.slug;
|
|
93
165
|
if (header) header.title = pane.title; if (title && title.style.display !== "none") title.textContent = pane.title; if (frame) frame.title = pane.title;
|
|
94
|
-
if (vendor) { var vendorName = session && session.vendor || "claude"; vendor.src = VENDOR_AVATARS[vendorName] || VENDOR_AVATARS.claude; vendor.title = VENDOR_NAMES[vendorName] || vendorName; }
|
|
166
|
+
if (vendor) { vendor.hidden = false; var vendorName = session && session.vendor || "claude"; vendor.src = VENDOR_AVATARS[vendorName] || VENDOR_AVATARS.claude; vendor.title = VENDOR_NAMES[vendorName] || vendorName; }
|
|
167
|
+
updatePaneFullAccessButton(paneEl.querySelector('.split-pane-full-access'), session);
|
|
95
168
|
var close = paneEl.querySelector(".split-pane-close");
|
|
96
169
|
if (close) {
|
|
97
170
|
close.dataset.sessionId = String(pane.sessionId); close.disabled = false; close.title = "Close pane"; close.setAttribute("aria-label", close.title);
|
|
98
171
|
}
|
|
99
172
|
}
|
|
100
173
|
|
|
174
|
+
function resetPaneContextChip(paneEl) {
|
|
175
|
+
var chip = paneEl.querySelector('.split-pane-context'); if (!chip) return;
|
|
176
|
+
chip.classList.remove('has-data'); chip.title = 'Context usage';
|
|
177
|
+
var fill = chip.querySelector('.split-pane-context-fill'), label = chip.querySelector('.split-pane-context-label');
|
|
178
|
+
if (fill) { fill.style.width = '0%'; fill.className = 'split-pane-context-fill'; }
|
|
179
|
+
if (label) label.textContent = '';
|
|
180
|
+
}
|
|
181
|
+
|
|
101
182
|
export function updatePaneContextChip(paneEl, msg) {
|
|
102
|
-
if (!paneEl) return; var chip = paneEl.querySelector(".split-pane-context"); if (!chip) return; var pct = msg.pct || 0; if (pct <= 0) return;
|
|
183
|
+
if (!paneEl) return; var chip = paneEl.querySelector(".split-pane-context"); if (!chip) return; var pct = msg.pct || 0; if (pct <= 0) { resetPaneContextChip(paneEl); return; }
|
|
103
184
|
chip.classList.add("has-data"); var fill = chip.querySelector(".split-pane-context-fill"); var label = chip.querySelector(".split-pane-context-label");
|
|
104
185
|
fill.style.width = Math.min(100, pct).toFixed(1) + "%"; fill.className = "split-pane-context-fill" + (msg.cls || ""); label.textContent = pct.toFixed(0) + "%";
|
|
105
186
|
var tip = "Context " + pct.toFixed(0) + "% (" + formatTokens(msg.used || 0) + " / " + formatTokens(msg.win || 0) + " tokens)";
|
|
@@ -2,7 +2,7 @@ import { store } from './store.js';
|
|
|
2
2
|
import { isProjectSessionReady } from './project-activation.js';
|
|
3
3
|
|
|
4
4
|
export function isCurrentProjectSessionReady(state, socket) {
|
|
5
|
-
return isProjectSessionReady(state, socket);
|
|
5
|
+
return socket !== state.splitIdentityBlockedSocket && isProjectSessionReady(state, socket);
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
export function clearProjectSplitState() {
|
|
@@ -6,6 +6,9 @@ import { openWireframe } from './wireframe-workbench.js';
|
|
|
6
6
|
|
|
7
7
|
import { store } from './store.js';
|
|
8
8
|
import { getWs } from './ws-ref.js';
|
|
9
|
+
import { livePaneBinding } from './pane-binding.js';
|
|
10
|
+
import { showToast } from './utils.js';
|
|
11
|
+
import { checkComposerDeparture } from './session-rich-drafts.js';
|
|
9
12
|
import { getCachedSessions } from './sidebar-sessions.js';
|
|
10
13
|
import { refreshIcons } from './icons.js';
|
|
11
14
|
import { detachTuiView } from './session-tui-view.js';
|
|
@@ -14,11 +17,12 @@ import { syncPairChrome } from './split-pair-ui.js';
|
|
|
14
17
|
import { presentMarkdownEdit } from './filebrowser.js';
|
|
15
18
|
import { autoStartLoginIfNeeded } from './vendor-login.js';
|
|
16
19
|
import { isCurrentProjectSessionReady } from './split-session-boundary.js';
|
|
17
|
-
import { splitPanesMatchProject
|
|
20
|
+
import { splitPanesMatchProject } from './project-activation.js';
|
|
18
21
|
import { handlePaneIssueMessage } from './pane-issue-bridge.js';
|
|
19
22
|
import { handlePaneFileMessage, revealFilesPanel } from './pane-file-bridge.js';
|
|
20
23
|
import { openFile } from './filebrowser.js';
|
|
21
|
-
import {
|
|
24
|
+
import { restoreSplitGroup, reconcileSplitIdentity } from './split-group-restore.js';
|
|
25
|
+
import { reconcileSplitPanes, splitPaneForSession } from './split-pane-reconciler.js';
|
|
22
26
|
import { createSplitPane, updateSplitPaneMetadata, updatePaneContextChip, updatePaneFullAccessButton } from './split-pane-renderer.js';
|
|
23
27
|
|
|
24
28
|
var host = null;
|
|
@@ -54,12 +58,7 @@ function sessionById(sessionId) {
|
|
|
54
58
|
}
|
|
55
59
|
|
|
56
60
|
function paneForSession(sessionId) {
|
|
57
|
-
|
|
58
|
-
return {
|
|
59
|
-
slug: store.get('currentSlug'),
|
|
60
|
-
sessionId: sessionId,
|
|
61
|
-
title: (session && session.title) || ("Session " + sessionId),
|
|
62
|
-
};
|
|
61
|
+
return splitPaneForSession(sessionId, store.get('currentSlug'), getCachedSessions());
|
|
63
62
|
}
|
|
64
63
|
|
|
65
64
|
// Arc-style drop preview: hovering one half folds the live app into the
|
|
@@ -90,6 +89,7 @@ function showDropOverlay() {
|
|
|
90
89
|
}
|
|
91
90
|
|
|
92
91
|
function switchNativeSession(sessionId, expectedSlug) {
|
|
92
|
+
if (!canLeavePanes()) return;
|
|
93
93
|
store.set({ splitPanes: null });
|
|
94
94
|
var ws = getWs();
|
|
95
95
|
var state = store.snap();
|
|
@@ -128,16 +128,16 @@ export function syncPaneTitles() {
|
|
|
128
128
|
var split = store.get('splitPanes');
|
|
129
129
|
if (!split || !split.panes || !host) return;
|
|
130
130
|
var titleEls = host.querySelectorAll(".split-pane-title");
|
|
131
|
+
var paneEls = host.querySelectorAll('.split-pane');
|
|
131
132
|
for (var i = 0; i < split.panes.length && i < titleEls.length; i++) {
|
|
132
|
-
var session = sessionById(split.panes[i].sessionId);
|
|
133
|
+
var session = sessionById(paneEls[i] && Number(paneEls[i].dataset.sessionId) || split.panes[i].sessionId);
|
|
133
134
|
if (!session || !session.title) continue;
|
|
134
|
-
split.panes[i].title = session.title;
|
|
135
135
|
if (titleEls[i].parentNode) titleEls[i].parentNode.title = session.title;
|
|
136
136
|
if (titleEls[i].style.display !== "none") titleEls[i].textContent = session.title;
|
|
137
137
|
}
|
|
138
138
|
var accessBtns = host.querySelectorAll(".split-pane-full-access");
|
|
139
139
|
for (var ai = 0; ai < split.panes.length && ai < accessBtns.length; ai++) {
|
|
140
|
-
updatePaneFullAccessButton(accessBtns[ai], sessionById(split.panes[ai].sessionId));
|
|
140
|
+
updatePaneFullAccessButton(accessBtns[ai], sessionById(paneEls[ai] && Number(paneEls[ai].dataset.sessionId) || split.panes[ai].sessionId));
|
|
141
141
|
}
|
|
142
142
|
syncPairChrome(host, split);
|
|
143
143
|
}
|
|
@@ -146,6 +146,8 @@ function handlePaneMessage(event) {
|
|
|
146
146
|
if (event.origin !== window.location.origin) return;
|
|
147
147
|
var msg = event.data;
|
|
148
148
|
if (!msg || !host) return;
|
|
149
|
+
var reusable = Array.from(host.querySelectorAll('.split-pane-frame')).find(function (frame) { return frame.contentWindow === event.source && frame.clayPane; });
|
|
150
|
+
if (reusable && !livePaneBinding(reusable, msg)) return;
|
|
149
151
|
if (msg.type === "clay-pane-present-wireframe" || msg.type === "clay-pane-present-markdown") {
|
|
150
152
|
if (msg.message && !msg.message.controlPresentation && trustedStudioFrame(event, msg)) {
|
|
151
153
|
if (msg.type === "clay-pane-present-wireframe") openWireframe(msg.message);
|
|
@@ -177,7 +179,10 @@ function renderSplit(split) {
|
|
|
177
179
|
var panes = split && split.panes;
|
|
178
180
|
if (!panes || panes.length < 2 || panes.length > 3) {
|
|
179
181
|
placeStickyNotesOverlay(false);
|
|
180
|
-
|
|
182
|
+
var frames = host.querySelectorAll('.split-pane-frame');
|
|
183
|
+
var retain = frames.length > 0 && frames.length <= 3 && Array.from(frames).every(function (frame) { return frame.dataset.projectSlug === store.get('currentSlug') && frame.clayPane; });
|
|
184
|
+
if (retain) Array.from(frames).forEach(function (frame) { frame.clayPane.suspend(); });
|
|
185
|
+
else host.innerHTML = "";
|
|
181
186
|
host.classList.remove("visible");
|
|
182
187
|
nativeApp.classList.remove("split-native-hidden");
|
|
183
188
|
return;
|
|
@@ -217,6 +222,7 @@ function openSplit(side, draggedId) {
|
|
|
217
222
|
}
|
|
218
223
|
|
|
219
224
|
export function openGroup(group) {
|
|
225
|
+
if (!canLeavePanes()) return false;
|
|
220
226
|
if (!group || !Array.isArray(group.members) || group.members.length < 2 || group.members.length > 3) return false;
|
|
221
227
|
var memberIds = splitGroupMemberIds(group);
|
|
222
228
|
if (memberIds.length !== group.members.length) return false;
|
|
@@ -265,47 +271,9 @@ export function openWorkerWithDriver(worker) {
|
|
|
265
271
|
return true;
|
|
266
272
|
}
|
|
267
273
|
|
|
268
|
-
//
|
|
269
|
-
// group member, reopen that group. Safe to call often -- no-ops unless a
|
|
270
|
-
// group member is active natively with no split open.
|
|
274
|
+
// Keep the public entry shared by connection, sidebar and identity hydration.
|
|
271
275
|
export function maybeRestoreSplitGroup() {
|
|
272
|
-
|
|
273
|
-
if (!isCurrentProjectSessionReady(state, getWs())) return;
|
|
274
|
-
var split = state.splitPanes;
|
|
275
|
-
if (split && !splitPanesMatchProject(split, state.currentSlug)) {
|
|
276
|
-
store.set({ splitPanes: null });
|
|
277
|
-
return;
|
|
278
|
-
}
|
|
279
|
-
if (state.paneMode) return;
|
|
280
|
-
if (split) {
|
|
281
|
-
var reconciliation = reconcileRestoredSplit(split, state.splitGroups);
|
|
282
|
-
if (reconciliation.action === "clear") {
|
|
283
|
-
store.set({ splitPanes: null });
|
|
284
|
-
return;
|
|
285
|
-
}
|
|
286
|
-
if (reconciliation.action === "rebuild") {
|
|
287
|
-
var rebuilt = reconciliation.group;
|
|
288
|
-
if (rebuilt.members.some(function (id) { return !sessionById(id); })) {
|
|
289
|
-
store.set({ splitPanes: null });
|
|
290
|
-
return;
|
|
291
|
-
}
|
|
292
|
-
store.set({ splitPanes: {
|
|
293
|
-
groupId: rebuilt.id,
|
|
294
|
-
panes: splitGroupMemberIds(rebuilt).map(function (id) { return paneForSession(id); }),
|
|
295
|
-
} });
|
|
296
|
-
}
|
|
297
|
-
return;
|
|
298
|
-
}
|
|
299
|
-
var activeId = store.get('activeSessionId');
|
|
300
|
-
if (!activeId) return;
|
|
301
|
-
var groups = store.get('splitGroups') || [];
|
|
302
|
-
for (var i = 0; i < groups.length; i++) {
|
|
303
|
-
var members = groups[i].members || [];
|
|
304
|
-
if (members.indexOf(activeId) !== -1) {
|
|
305
|
-
openGroup(groups[i]);
|
|
306
|
-
return;
|
|
307
|
-
}
|
|
308
|
-
}
|
|
276
|
+
restoreSplitGroup(openGroup);
|
|
309
277
|
}
|
|
310
278
|
|
|
311
279
|
export function separateGroup(group) {
|
|
@@ -369,6 +337,24 @@ function handleSessionDragStart(event) {
|
|
|
369
337
|
if (draggedSessionId) showDropOverlay();
|
|
370
338
|
}
|
|
371
339
|
|
|
340
|
+
function openDriverGroup(driver) {
|
|
341
|
+
var state = store.snap();
|
|
342
|
+
if (!driver || driver.sessionRole !== "driver" || !isCurrentProjectSessionReady(state, getWs())) return false;
|
|
343
|
+
var groups = state.splitGroups || [];
|
|
344
|
+
for (var i = 0; i < groups.length; i++) {
|
|
345
|
+
var group = groups[i];
|
|
346
|
+
var roles = splitGroupRoles(group);
|
|
347
|
+
if (!roles || roles.driverId !== driver.id || typeof group.id !== "string" || !group.id) continue;
|
|
348
|
+
var available = roles.workerIds.every(function (id) {
|
|
349
|
+
var worker = sessionById(id);
|
|
350
|
+
return worker && worker.sessionRole === "worker" && worker.parentAvailable === true && worker.parentSessionId === driver.id;
|
|
351
|
+
});
|
|
352
|
+
// Keep mounted conversations when selecting their Driver again.
|
|
353
|
+
if (available) return openGroup(group);
|
|
354
|
+
}
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
357
|
+
|
|
372
358
|
function handleSidebarSessionClick(event) {
|
|
373
359
|
if (!store.get('splitPanes') || event.button !== 0) return;
|
|
374
360
|
// This document capture listener runs before a row's bubble handlers. Leave
|
|
@@ -381,11 +367,22 @@ function handleSidebarSessionClick(event) {
|
|
|
381
367
|
if (!sessionId) return;
|
|
382
368
|
event.preventDefault();
|
|
383
369
|
event.stopImmediatePropagation();
|
|
370
|
+
if (!canLeavePanes()) return;
|
|
384
371
|
var session = sessionById(sessionId);
|
|
385
372
|
if (session && session.sessionRole === "worker" && openWorkerWithDriver(session)) return;
|
|
373
|
+
if (openDriverGroup(session)) return;
|
|
386
374
|
switchNativeSession(sessionId);
|
|
387
375
|
}
|
|
388
376
|
|
|
377
|
+
function canLeavePanes() {
|
|
378
|
+
if (!host) return true;
|
|
379
|
+
try {
|
|
380
|
+
if (!store.get('splitPanes')) checkComposerDeparture();
|
|
381
|
+
Array.from(host.querySelectorAll('.split-pane-frame')).forEach(function (frame) { if (frame.clayPane) frame.clayPane.checkDraft(); });
|
|
382
|
+
return true;
|
|
383
|
+
} catch (error) { showToast(error.message, 'warn'); return false; }
|
|
384
|
+
}
|
|
385
|
+
|
|
389
386
|
export function initSplitView() {
|
|
390
387
|
if (store.get('paneMode')) return;
|
|
391
388
|
var mainPanels = document.getElementById("main-panels");
|
|
@@ -411,6 +408,8 @@ export function initSplitView() {
|
|
|
411
408
|
});
|
|
412
409
|
document.addEventListener("click", handleSidebarSessionClick, true);
|
|
413
410
|
store.subscribe(function (state, prev) {
|
|
411
|
+
if (state.authIdentityLoaded && !prev.authIdentityLoaded) queueMicrotask(maybeRestoreSplitGroup);
|
|
412
|
+
if (reconcileSplitIdentity(state, prev, host)) { renderSplit(null); return; }
|
|
414
413
|
if (state.splitPanes !== prev.splitPanes) renderSplit(state.splitPanes);
|
|
415
414
|
if (state.permissionModePendingByTarget !== prev.permissionModePendingByTarget || state.connected !== prev.connected ||
|
|
416
415
|
state.skipPermsEnabled !== prev.skipPermsEnabled) syncPaneTitles();
|
|
@@ -27,27 +27,55 @@ export function renderTaskControls(root, id) {
|
|
|
27
27
|
var ui = getNoteUiState(id);
|
|
28
28
|
var fields = ui.checklistInputs || {};
|
|
29
29
|
var busy = ui.pending.length > 0;
|
|
30
|
+
var active = document.activeElement;
|
|
31
|
+
var focusField = active && root.contains(active) ? active.dataset.taskField : undefined;
|
|
32
|
+
var selection = focusField !== undefined ? [active.selectionStart, active.selectionEnd] : null;
|
|
30
33
|
root.replaceChildren();
|
|
31
34
|
var form = document.createElement('form'); form.className = 'sn-task-form';
|
|
35
|
+
form.setAttribute('aria-label', 'Checklist');
|
|
36
|
+
form.setAttribute('aria-busy', busy ? 'true' : 'false');
|
|
37
|
+
var feedback = document.createElement('span'); feedback.className = 'sn-task-feedback';
|
|
38
|
+
feedback.setAttribute('role', 'status');
|
|
39
|
+
var saveButton = document.createElement('button');
|
|
40
|
+
function updateFeedback() {
|
|
41
|
+
var current = getNoteUiState(id);
|
|
42
|
+
var dirty = checklistDraftText(current) !== current.text;
|
|
43
|
+
feedback.textContent = busy ? 'Saving…' : current.error ? (dirty ? 'Draft kept. ' : 'Not saved. ') + current.error : dirty ? 'Unsaved changes' : 'All changes saved';
|
|
44
|
+
saveButton.disabled = busy || !dirty;
|
|
45
|
+
}
|
|
32
46
|
function input(label, value, key) {
|
|
33
|
-
var control = document.createElement('
|
|
47
|
+
var control = document.createElement('textarea'); control.rows = 1; control.value = fields[key] !== undefined ? fields[key] : value;
|
|
34
48
|
control.setAttribute('aria-label', label); control.placeholder = label; control.maxLength = 400;
|
|
35
|
-
control.
|
|
49
|
+
control.readOnly = busy;
|
|
50
|
+
control.dataset.taskField = key;
|
|
51
|
+
control.onkeydown = function (event) {
|
|
52
|
+
if (event.key === 'Enter' && !event.isComposing) { event.preventDefault(); form.requestSubmit(); }
|
|
53
|
+
};
|
|
36
54
|
control.oninput = function () {
|
|
37
55
|
var current = getNoteUiState(id);
|
|
56
|
+
// A task remains one persisted line; visual wrapping does not split tasks.
|
|
57
|
+
control.value = control.value.replace(/[\r\n]+/g, ' ');
|
|
38
58
|
var draft = Object.assign({}, current.checklistInputs || {}); draft[key] = control.value;
|
|
39
59
|
setNoteUiState(id, { checklistInputs: draft });
|
|
60
|
+
updateFeedback();
|
|
40
61
|
};
|
|
41
62
|
return control;
|
|
42
63
|
}
|
|
43
64
|
var title = input('Checklist title', (ui.text || '').split('\n')[0], 'title'); title.className = 'sn-task-title'; form.appendChild(title);
|
|
44
|
-
getChecklistItems(ui.text)
|
|
65
|
+
var items = getChecklistItems(ui.text);
|
|
66
|
+
var completed = 0;
|
|
67
|
+
var summary = document.createElement('div'); summary.className = 'sn-task-summary';
|
|
68
|
+
form.appendChild(summary);
|
|
69
|
+
items.forEach(function (item, position) {
|
|
45
70
|
var row = document.createElement('div'); row.className = 'sn-task-row';
|
|
46
71
|
var marker = item.text.match(/^\s*(?:[-*+]\s+)?\[([ xX])\]\s*(.*)$/);
|
|
47
72
|
var check = document.createElement('button'); check.type = 'button'; check.className = 'sn-check';
|
|
48
73
|
var checked = ui.checklistState[item.key];
|
|
49
74
|
if (checked === undefined) checked = marker && marker[1].toLowerCase() === 'x';
|
|
50
|
-
check.classList.toggle('checked', !!checked);
|
|
75
|
+
check.classList.toggle('checked', !!checked);
|
|
76
|
+
check.innerHTML = '<span class="sn-task-box" aria-hidden="true"><svg viewBox="0 0 16 16" fill="none"><path d="m3.5 8 3 3 6-6" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg></span>';
|
|
77
|
+
row.classList.toggle('is-complete', !!checked);
|
|
78
|
+
if (checked) completed++;
|
|
51
79
|
check.setAttribute('role', 'checkbox'); check.setAttribute('aria-checked', checked ? 'true' : 'false'); check.setAttribute('aria-label', 'Complete task ' + (position + 1));
|
|
52
80
|
check.dataset.checklistIndex = item.index; check.dataset.checklistKey = encodeURIComponent(item.key);
|
|
53
81
|
check.disabled = busy;
|
|
@@ -59,8 +87,15 @@ export function renderTaskControls(root, id) {
|
|
|
59
87
|
};
|
|
60
88
|
row.append(check, label, remove); form.appendChild(row);
|
|
61
89
|
});
|
|
62
|
-
|
|
63
|
-
var
|
|
90
|
+
summary.textContent = items.length ? completed + ' of ' + items.length + ' complete' : 'A little space for what’s next';
|
|
91
|
+
var addRow = document.createElement('div'); addRow.className = 'sn-task-add';
|
|
92
|
+
var plus = document.createElement('span'); plus.className = 'sn-task-plus'; plus.innerHTML = iconHtml('plus'); plus.setAttribute('aria-hidden', 'true');
|
|
93
|
+
var entry = input('New task', '', 'entry'); entry.className = 'sn-task-entry'; entry.placeholder = 'Add a task…';
|
|
94
|
+
addRow.append(plus, entry); form.appendChild(addRow);
|
|
95
|
+
var footer = document.createElement('div'); footer.className = 'sn-task-footer';
|
|
96
|
+
saveButton.type = 'submit'; saveButton.className = 'sn-task-save'; saveButton.textContent = 'Save tasks';
|
|
97
|
+
footer.append(feedback, saveButton); form.appendChild(footer);
|
|
98
|
+
updateFeedback();
|
|
64
99
|
function save(text) {
|
|
65
100
|
var current = getNoteUiState(id);
|
|
66
101
|
if (current.pending.length) return;
|
|
@@ -68,4 +103,14 @@ export function renderTaskControls(root, id) {
|
|
|
68
103
|
}
|
|
69
104
|
form.onsubmit = function (event) { event.preventDefault(); save(checklistDraftText(getNoteUiState(id))); };
|
|
70
105
|
root.appendChild(form); refreshIcons(root);
|
|
106
|
+
if (focusField !== undefined) {
|
|
107
|
+
var controls = form.querySelectorAll('[data-task-field]');
|
|
108
|
+
for (var i = 0; i < controls.length; i++) {
|
|
109
|
+
if (controls[i].dataset.taskField === focusField) {
|
|
110
|
+
controls[i].focus({ preventScroll: true });
|
|
111
|
+
controls[i].setSelectionRange(selection[0], selection[1]);
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
71
116
|
}
|
|
@@ -12,7 +12,8 @@ export function speechContext() {
|
|
|
12
12
|
var s = store.snap();
|
|
13
13
|
return JSON.stringify([s.currentSlug, s.activeSessionId, s.dmMode, s.dmKey,
|
|
14
14
|
s.dmTargetUser && s.dmTargetUser.id, s.homePrimarySurface, s.homeSubSurface,
|
|
15
|
-
s.homeChatMateId, s.homeChatSessionId
|
|
15
|
+
s.homeChatMateId, s.homeChatSessionId, s.sessionOriginId, s.myUserId, s.sessionActionEpoch || 0,
|
|
16
|
+
s.paneCommittedBinding && s.paneCommittedBinding.generation]);
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
export function setSpeechStatus(id, message, isError) {
|
|
@@ -76,6 +77,7 @@ export function claimSpeech(id, input, recognition) {
|
|
|
76
77
|
var active = { id: id, input: input, recognition: recognition, context: speechContext(),
|
|
77
78
|
before: input.value, lastValue: input.value, writing: false, started: false, timeout: null };
|
|
78
79
|
active.traceId = Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 7);
|
|
80
|
+
active.paneBinding = store.get('paneCommittedBinding') || null;
|
|
79
81
|
active.traceStarted = Date.now();
|
|
80
82
|
store.set({ speechActive: active, speechRetry: null });
|
|
81
83
|
return active;
|
|
@@ -113,6 +115,7 @@ export function startSTT(id, input) {
|
|
|
113
115
|
var active = { id: id, input: input, recognition: recognition, context: speechContext(),
|
|
114
116
|
before: input.value, lastValue: input.value, writing: false, started: false, timeout: null };
|
|
115
117
|
recognition.lang = store.get('speechLang') || 'en-US';
|
|
118
|
+
active.paneBinding = store.get('paneCommittedBinding') || null;
|
|
116
119
|
recognition.continuous = true;
|
|
117
120
|
recognition.interimResults = true;
|
|
118
121
|
recognition.onstart = function () {
|
|
@@ -26,7 +26,7 @@ async function upload(active, blob) {
|
|
|
26
26
|
model: active.model, language: active.language, blob: blob };
|
|
27
27
|
failSTT(error.name === 'AbortError' ? 'Transcription timed out. Please try again.' : error.message);
|
|
28
28
|
store.set({ speechRetry: retry });
|
|
29
|
-
setTimeout(function () { if (store.get('speechRetry') === retry) store.set({ speechRetry: null }); }, 300000);
|
|
29
|
+
setTimeout(function () { if (store.get('speechRetry') === retry && !store.get('deferredSessionSwitch')) store.set({ speechRetry: null }); }, 300000);
|
|
30
30
|
return false;
|
|
31
31
|
}
|
|
32
32
|
}
|
|
@@ -25,6 +25,7 @@ function bindComposer(id, inputId, wrapperId) {
|
|
|
25
25
|
if (!button || !input || !wrapper || button.dataset.speechBound) return;
|
|
26
26
|
var composer = document.getElementById(id === 'stt-btn' ? 'input-row' : 'home-mate-chat-composer') || wrapper;
|
|
27
27
|
var toolbar = document.getElementById(id === 'stt-btn' ? 'input-bottom' : 'home-mate-chat-composer') || composer;
|
|
28
|
+
button.addEventListener('click', refreshSpeechBrowser);
|
|
28
29
|
button.dataset.speechBound = 'true'; button.classList.add('stt-button');
|
|
29
30
|
var group = document.createElement('span'); group.className = 'stt-controls';
|
|
30
31
|
button.before(group); group.appendChild(button);
|
|
@@ -32,7 +33,7 @@ function bindComposer(id, inputId, wrapperId) {
|
|
|
32
33
|
picker.className = 'stt-picker-toggle'; picker.innerHTML = iconHtml('chevron-down');
|
|
33
34
|
picker.setAttribute('aria-label', 'Choose voice model'); picker.setAttribute('aria-haspopup', 'dialog'); picker.setAttribute('aria-expanded', 'false');
|
|
34
35
|
picker.title = 'Choose voice model'; group.appendChild(picker);
|
|
35
|
-
picker.addEventListener('click', function () { openSpeechPicker(picker); });
|
|
36
|
+
picker.addEventListener('click', function () { refreshSpeechBrowser(); openSpeechPicker(picker); });
|
|
36
37
|
var row = document.createElement('div'); row.className = 'stt-progress'; row.hidden = true;
|
|
37
38
|
var status = document.createElement('span'); status.id = id + '-status'; status.className = 'stt-status'; status.setAttribute('role', 'status');
|
|
38
39
|
var timer = document.createElement('span'); timer.className = 'stt-timer'; timer.setAttribute('aria-hidden', 'true');
|
|
@@ -70,7 +71,7 @@ function bindComposer(id, inputId, wrapperId) {
|
|
|
70
71
|
function render() {
|
|
71
72
|
var active = store.get('speechActive'); var own = !!active && active.id === id;
|
|
72
73
|
var model = voiceModel(); var provider = voiceProvider(model.provider);
|
|
73
|
-
var browser = model.provider === 'browser' ?
|
|
74
|
+
var browser = model.provider === 'browser' ? store.get('speechBrowserIdentity') : null;
|
|
74
75
|
var pending = own && active.model && active.phase !== 'recording';
|
|
75
76
|
var autoSend = own && !!active.autoSend;
|
|
76
77
|
composer.classList.toggle('stt-recording', own);
|
|
@@ -117,7 +118,11 @@ function bindComposer(id, inputId, wrapperId) {
|
|
|
117
118
|
observer.observe(input, { attributes: true, attributeFilter: ['disabled', 'readonly'] });
|
|
118
119
|
render();
|
|
119
120
|
}
|
|
121
|
+
// Browser identity may read layout; refresh on entry and interaction, not transcript updates.
|
|
122
|
+
function refreshSpeechBrowser() { store.set({ speechBrowserIdentity: browserSpeechInfo() }); }
|
|
120
123
|
export function initSTT() {
|
|
124
|
+
refreshSpeechBrowser();
|
|
125
|
+
window.addEventListener('focus', refreshSpeechBrowser);
|
|
121
126
|
initSpeechLifecycle(); initSpeechPicker();
|
|
122
127
|
bindComposer('stt-btn', 'input', 'input-wrapper');
|
|
123
128
|
bindComposer('home-stt-btn', 'home-mate-chat-input', 'home-mate-chat-composer-frame');
|