clay-server 5.2.0-beta.6 → 5.2.0-beta.8

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.
Files changed (109) hide show
  1. package/lib/clay-control-presentation-binding.js +96 -0
  2. package/lib/clay-control-presentation.js +128 -0
  3. package/lib/clay-control-target-projection.js +26 -0
  4. package/lib/clay-mate-chat.js +1 -0
  5. package/lib/clay-mate-knowledge.js +1 -1
  6. package/lib/clay-product-control-tools.js +1 -1
  7. package/lib/clay-product-control.js +22 -12
  8. package/lib/clay-project-records.js +2 -1
  9. package/lib/clay-scheduling.js +1 -1
  10. package/lib/clay-screen-controls.js +13 -5
  11. package/lib/clay-workspace-controls.js +2 -1
  12. package/lib/contextual-screen-snapshot.js +38 -0
  13. package/lib/mate-context-service.js +138 -0
  14. package/lib/mate-context-tools.js +21 -0
  15. package/lib/project-connection.js +8 -5
  16. package/lib/project-control-git-view.js +12 -0
  17. package/lib/project-filesystem.js +5 -1
  18. package/lib/project-http.js +3 -0
  19. package/lib/project-notifications.js +5 -4
  20. package/lib/project-product-control.js +3 -1
  21. package/lib/project-scheduled-messages.js +4 -4
  22. package/lib/project-scheduled-task-control.js +1 -1
  23. package/lib/project-session-document.js +20 -5
  24. package/lib/project-shared-browser.js +22 -5
  25. package/lib/project-studio-presentation.js +58 -0
  26. package/lib/project-user-message.js +36 -0
  27. package/lib/project-workspace-query.js +11 -1
  28. package/lib/project.js +24 -7
  29. package/lib/public/app.js +24 -0
  30. package/lib/public/appearance-bootstrap.js +2 -0
  31. package/lib/public/css/contextual-clay.css +240 -0
  32. package/lib/public/modules/app-favicon.js +7 -7
  33. package/lib/public/modules/app-message-router.js +5 -1
  34. package/lib/public/modules/app-messages.js +14 -1
  35. package/lib/public/modules/app-notifications.js +10 -3
  36. package/lib/public/modules/app-projects.js +3 -0
  37. package/lib/public/modules/clay-control-activity.js +214 -0
  38. package/lib/public/modules/clay-control-copy.js +36 -0
  39. package/lib/public/modules/clay-control-detail-targets.js +44 -0
  40. package/lib/public/modules/clay-control-object-targets.js +52 -0
  41. package/lib/public/modules/clay-control-target-visibility.js +12 -0
  42. package/lib/public/modules/clay-control-targets.js +44 -0
  43. package/lib/public/modules/clay-navigation-render.js +52 -0
  44. package/lib/public/modules/clay-owner-navigation.js +11 -2
  45. package/lib/public/modules/clay-session-links.js +31 -0
  46. package/lib/public/modules/clay-studio-bridge.js +20 -0
  47. package/lib/public/modules/clay-studio-document.js +67 -0
  48. package/lib/public/modules/clay-studio-render.js +144 -0
  49. package/lib/public/modules/clay-tool-presentation.js +4 -0
  50. package/lib/public/modules/command-palette.js +26 -31
  51. package/lib/public/modules/contextual-ask-session.js +26 -0
  52. package/lib/public/modules/contextual-clay-geometry.js +155 -0
  53. package/lib/public/modules/contextual-clay-session.js +130 -0
  54. package/lib/public/modules/contextual-clay-workspace.js +157 -0
  55. package/lib/public/modules/contextual-clay.js +237 -0
  56. package/lib/public/modules/contextual-composer.js +91 -0
  57. package/lib/public/modules/contextual-mate-context.js +115 -0
  58. package/lib/public/modules/contextual-mate-hint.js +40 -0
  59. package/lib/public/modules/contextual-mate-identity.js +23 -0
  60. package/lib/public/modules/contextual-mate-sessions.js +134 -0
  61. package/lib/public/modules/contextual-search.js +137 -0
  62. package/lib/public/modules/contextual-session-citations.js +58 -0
  63. package/lib/public/modules/filebrowser.js +20 -1
  64. package/lib/public/modules/generation-stop.js +8 -5
  65. package/lib/public/modules/git-panel.js +3 -1
  66. package/lib/public/modules/icon-strip-tooltip.js +2 -1
  67. package/lib/public/modules/input.js +35 -1
  68. package/lib/public/modules/issues-render.js +3 -2
  69. package/lib/public/modules/knowledge-db-workbench.js +4 -4
  70. package/lib/public/modules/markdown.js +2 -34
  71. package/lib/public/modules/mate-knowledge-workbench.js +1 -1
  72. package/lib/public/modules/message-delivery.js +10 -0
  73. package/lib/public/modules/pane-bridge.js +2 -2
  74. package/lib/public/modules/pending-message-queue.js +1 -0
  75. package/lib/public/modules/product-review.js +26 -0
  76. package/lib/public/modules/project-chat-identity.js +13 -4
  77. package/lib/public/modules/project-logs-render.js +3 -0
  78. package/lib/public/modules/project-mate-navigation.js +58 -21
  79. package/lib/public/modules/scheduled-tasks.js +2 -0
  80. package/lib/public/modules/search-clay-chat.js +5 -454
  81. package/lib/public/modules/shared-browser.js +2 -0
  82. package/lib/public/modules/sidebar-mobile.js +4 -3
  83. package/lib/public/modules/sidebar-projects.js +1 -0
  84. package/lib/public/modules/sidebar-sessions.js +1 -0
  85. package/lib/public/modules/split-view.js +7 -16
  86. package/lib/public/modules/stt.js +7 -2
  87. package/lib/public/modules/terminal.js +9 -0
  88. package/lib/public/modules/tools.js +4 -1
  89. package/lib/public/modules/wireframe-workbench.js +5 -2
  90. package/lib/public/style.css +2 -0
  91. package/lib/sdk-bridge.js +4 -0
  92. package/lib/server-contextual-clay-settings.js +40 -0
  93. package/lib/server-contextual-mate-sessions.js +70 -0
  94. package/lib/server-home-chat.js +7 -1
  95. package/lib/server-home-clay-entry.js +23 -4
  96. package/lib/server-home-clay-session-links.js +12 -0
  97. package/lib/server-owner-ui.js +49 -8
  98. package/lib/server-settings.js +2 -0
  99. package/lib/server.js +17 -0
  100. package/lib/sessions.js +10 -3
  101. package/lib/terminal-manager.js +18 -11
  102. package/lib/users-contextual-clay-preferences.js +31 -0
  103. package/lib/users.js +3 -0
  104. package/lib/workspace-assignment-service.js +25 -0
  105. package/lib/workspace-query-mcp-server.js +1 -1
  106. package/lib/ws-request.js +1 -0
  107. package/lib/ws-schema.js +10 -0
  108. package/lib/yoke/claude-hook-installer.js +4 -0
  109. package/package.json +1 -1
@@ -0,0 +1,134 @@
1
+ // Saved conversations are independent of which compact renderers are mounted.
2
+ import { store } from './store.js';
3
+ import { getWs } from './ws-ref.js';
4
+ import { contextualMate, contextualMateStamp, contextualFrame } from './contextual-mate-identity.js';
5
+ import { selectContextualConversation } from './contextual-clay.js';
6
+ import { pauseContextualSearchForSelection } from './contextual-search.js';
7
+ import { iconHtml, refreshIcons } from './icons.js';
8
+ import { showToast } from './utils.js';
9
+
10
+ function selection() { var value = store.get('contextualClay') || {}; return JSON.stringify([value.mateId, value.sessionId, value.startingAsk]); }
11
+ function host() { return document.getElementById('contextual-clay'); }
12
+ function requestState(kind) { return store.get(kind === 'list' ? 'contextualConversationHistory' : 'contextualConversationRequest'); }
13
+ function save(kind, value) { var update = {}; update[kind === 'list' ? 'contextualConversationHistory' : 'contextualConversationRequest'] = value; store.set(update); }
14
+ function valid(request) { var mate = request && contextualMate(request.mateId); return request && request.ownerId === store.get('myUserId') && mate && contextualMateStamp(mate) === request.stamp; }
15
+ function guardDraft() {
16
+ var frame = contextualFrame();
17
+ try {
18
+ if (frame && frame.conversation) frame.conversation.checkDraft();
19
+ return true;
20
+ } catch (error) { showToast(error.message, 'warn'); return false; }
21
+ }
22
+ function send(kind, item, lookup) {
23
+ var mate = contextualMate((store.get('contextualClay') || {}).mateId), socket = getWs();
24
+ if (!mate) return;
25
+ var old = requestState(kind);
26
+ if (kind !== 'list' && old && old.status === 'loading') { showToast('Wait for the current conversation request to finish.', 'warn'); return; }
27
+ if (kind !== 'list' && !guardDraft()) return;
28
+ var request = lookup ? Object.assign({}, old) : { requestId: 'mate-conversations-' + crypto.randomUUID(), mateId: mate.id, stamp: contextualMateStamp(mate), ownerId: store.get('myUserId'), selection: selection(), kind: kind, item: item };
29
+ if (!valid(request)) return;
30
+ request.status = 'loading'; request.error = null; request.socket = socket;
31
+ save(kind, request);
32
+ if (!store.get('connected') || !socket || socket.readyState !== 1) { save(kind, Object.assign({}, request, { status: 'error', error: 'Reconnect to load or start a conversation.' })); return; }
33
+ var payload = { type: kind === 'list' ? 'home_mate_sessions_list' : kind === 'new' ? 'home_mate_new_session' : 'home_mate_session_open', presentation: 'contextual', mateId: request.mateId, mateStamp: request.stamp, requestId: request.requestId };
34
+ if (item) { payload.sessionId = item.id; payload.sessionOriginId = item.sessionOriginId; }
35
+ if (lookup && kind === 'new') payload.lookupOnly = true;
36
+ try { socket.send(JSON.stringify(payload)); }
37
+ catch (error) { save(kind, Object.assign({}, request, { status: 'error', error: 'The request is unconfirmed. Check again after reconnecting.' })); }
38
+ setTimeout(function () {
39
+ var current = requestState(kind);
40
+ if (current && current.requestId === request.requestId && current.status === 'loading') save(kind, Object.assign({}, current, { status: 'error', error: kind === 'new' ? 'Creation is unconfirmed. Check the same request or browse history.' : 'Conversation history did not respond. Try again.' }));
41
+ }, 15000);
42
+ }
43
+ function age(timestamp) {
44
+ if (!timestamp) return '';
45
+ var date = new Date(timestamp), today = new Date();
46
+ return date.toDateString() === today.toDateString() ? 'Today' : date.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
47
+ }
48
+ function render() {
49
+ var surface = host(); if (!surface) return;
50
+ var mateId = (store.get('contextualClay') || {}).mateId;
51
+ var request = requestState('action'), history = requestState('list');
52
+ var busy = request && request.mateId === mateId && request.status === 'loading';
53
+ surface.querySelector('[data-mate-new]').disabled = !!busy;
54
+ surface.querySelector('[data-mate-new]').setAttribute('aria-busy', String(!!busy));
55
+ var notice = surface.querySelector('[data-conversation-notice]');
56
+ notice.hidden = !request || request.mateId !== mateId || request.status === 'done';
57
+ if (!notice.hidden) {
58
+ notice.querySelector('span').textContent = request.error || (request.kind === 'new' ? 'Starting a new chat…' : 'Opening conversation…');
59
+ notice.querySelector('button').hidden = request.status !== 'error';
60
+ notice.querySelector('button').textContent = request.kind === 'new' ? 'Check request' : 'Try again';
61
+ }
62
+ var list = surface.querySelector('[data-mate-session-list]');
63
+ if (!surface.querySelector('[data-mate-history]').open) return;
64
+ var key = JSON.stringify([history && history.requestId, history && history.status, history && history.error, mateId, (store.get('contextualClay') || {}).sessionId, !!busy]);
65
+ if (list.dataset.renderKey === key) return;
66
+ list.dataset.renderKey = key;
67
+ list.replaceChildren();
68
+ if (!history || history.mateId !== mateId || history.status !== 'done') {
69
+ var status = document.createElement('p'); status.setAttribute('role', 'status');
70
+ status.textContent = history && history.mateId === mateId && history.error || 'Loading conversations…'; list.appendChild(status);
71
+ if (history && history.status === 'error') { var retry = document.createElement('button'); retry.type = 'button'; retry.textContent = 'Try again'; retry.onclick = function () { send('list'); }; list.appendChild(retry); }
72
+ return;
73
+ }
74
+ if (!history.sessions.length) { var empty = document.createElement('p'); empty.textContent = 'No saved conversations yet.'; list.appendChild(empty); }
75
+ history.sessions.forEach(function (item) {
76
+ var button = document.createElement('button'); button.type = 'button'; button.dataset.sessionId = String(item.localId);
77
+ button.setAttribute('aria-current', String(String((store.get('contextualClay') || {}).sessionId) === String(item.localId)));
78
+ var title = document.createElement('span'); title.textContent = item.title || 'New conversation';
79
+ var detail = document.createElement('small'); detail.textContent = item.isProcessing ? 'Working' : age(item.lastActivity || item.createdAt);
80
+ button.append(title, detail); button.title = title.textContent; button.disabled = !!busy;
81
+ button.onclick = function () { send('open', item); };
82
+ list.appendChild(button);
83
+ });
84
+ if (history.truncated) { var limit = document.createElement('p'); limit.textContent = 'Showing the 500 most recent conversations.'; list.appendChild(limit); }
85
+ }
86
+ export function attachContextualConversations(surface) {
87
+ var actions = surface.querySelector('.contextual-clay-actions');
88
+ var tools = document.createElement('div'); tools.className = 'contextual-conversation-tools';
89
+ tools.innerHTML = '<button type="button" data-mate-new aria-label="New chat" title="New chat">' + iconHtml('square-pen') + '<span>New chat</span></button><details data-mate-history><summary role="button" aria-label="Conversation history" aria-haspopup="dialog" aria-expanded="false" aria-controls="contextual-conversation-history" title="Conversation history">' + iconHtml('history') + '</summary><div id="contextual-conversation-history" role="dialog" aria-label="Conversation history"><span class="contextual-history-label">Conversations</span><div data-mate-session-list></div></div></details>';
90
+ actions.prepend(tools);
91
+ var notice = document.createElement('div'); notice.dataset.conversationNotice = ''; notice.hidden = true;
92
+ notice.innerHTML = '<span role="status"></span><button type="button">Check request</button>';
93
+ surface.insertBefore(notice, surface.querySelector('.contextual-clay-body'));
94
+ tools.querySelector('[data-mate-new]').onclick = function () { send('new'); };
95
+ tools.querySelector('details').addEventListener('toggle', function () { this.querySelector('summary').setAttribute('aria-expanded', String(this.open)); if (this.open) send('list'); });
96
+ surface.addEventListener('pointerdown', function (event) { var menu = tools.querySelector('details'); if (menu.open && !menu.contains(event.target)) menu.open = false; });
97
+
98
+ notice.querySelector('button').onclick = function () { var value = requestState('action'); if (value) send(value.kind, value.item, true); };
99
+ tools.addEventListener('keydown', function (event) {
100
+ if (event.key === 'Tab') {
101
+ var menu = tools.querySelector('details'), items = Array.from(tools.querySelectorAll('[data-mate-session-list] button:not(:disabled)'));
102
+ if (menu.open && (event.shiftKey && event.target === menu.querySelector('summary') || !event.shiftKey && event.target === items[items.length - 1])) queueMicrotask(function () { menu.open = false; });
103
+ return;
104
+ }
105
+ if (!/^Arrow(Down|Up)$/.test(event.key)) return;
106
+ var buttons = Array.from(tools.querySelectorAll('[data-mate-session-list] button:not(:disabled)'));
107
+ if (!buttons.length) return;
108
+ var index = buttons.indexOf(document.activeElement);
109
+ buttons[index < 0 ? (event.key === 'ArrowDown' ? 0 : buttons.length - 1) : (index + (event.key === 'ArrowDown' ? 1 : buttons.length - 1)) % buttons.length].focus(); event.preventDefault();
110
+ });
111
+ refreshIcons(); render();
112
+ }
113
+ export function handleContextualConversations(msg) {
114
+ if (msg.type !== 'contextual_mate_sessions_result') return false;
115
+ var kind = requestState('list') && requestState('list').requestId === msg.requestId ? 'list' : 'action';
116
+ var request = requestState(kind);
117
+ if (!request || request.status !== 'loading' || request.requestId !== msg.requestId || request.socket !== getWs() || !valid(request) || msg.mateId !== request.mateId || msg.mateStamp !== request.stamp) return true;
118
+ if (!msg.ok) { save(kind, Object.assign({}, request, { status: 'error', error: msg.error || 'Conversation unavailable.' })); return true; }
119
+ if (kind === 'list') { save(kind, Object.assign({}, request, { status: 'done', sessions: msg.sessions || [], truncated: msg.truncated })); return true; }
120
+ if (!Number.isSafeInteger(msg.sessionId) || msg.sessionId <= 0 || typeof msg.sessionOriginId !== 'string') return true;
121
+ if (request.selection !== selection() || (store.get('contextualClay') || {}).mode !== 'popover') { save(kind, Object.assign({}, request, { status: 'error', error: 'Your selection changed. The conversation remains in history.' })); return true; }
122
+ if (!guardDraft()) { save(kind, Object.assign({}, request, { status: 'error', error: 'Finish uploading or recording, then check this conversation again.' })); return true; }
123
+ pauseContextualSearchForSelection();
124
+ if (!selectContextualConversation(contextualMate(request.mateId), msg.sessionId, msg.sessionOriginId)) { save(kind, Object.assign({}, request, { status: 'error', error: 'This conversation could not be opened. Your draft is retained.' })); return true; }
125
+ surfaceClose(true); save(kind, Object.assign({}, request, { status: 'done' })); return true;
126
+ }
127
+ function surfaceClose(restoreFocus) { var surface = host(); if (surface) { var menu = surface.querySelector('[data-mate-history]'); var focused = menu.contains(document.activeElement); menu.open = false; if (focused || restoreFocus) menu.querySelector('summary').focus({ preventScroll: true }); } }
128
+ store.subscribe(function (value, previous) {
129
+ if (value.paneMode) return;
130
+ if (value.myUserId !== previous.myUserId) { store.set({ contextualConversationHistory: null, contextualConversationRequest: null }); return; }
131
+ if ((value.contextualClay || {}).mateId !== (previous.contextualClay || {}).mateId) surfaceClose();
132
+ if (previous.connected && !value.connected) ['list', 'action'].forEach(function (kind) { var request = requestState(kind); if (request && request.status === 'loading') save(kind, Object.assign({}, request, { status: 'error', error: 'Disconnected. Check again when connected; your conversation is retained.' })); });
133
+ render();
134
+ });
@@ -0,0 +1,137 @@
1
+ import { store } from './store.js';
2
+ import { getWs } from './ws-ref.js';
3
+ import { activateContextualMate } from './project-mate-navigation.js';
4
+ import { contextualMate, contextualMateStamp, contextualFrame } from './contextual-mate-identity.js';
5
+ import { showContextualAsk, enterComposer } from './contextual-clay.js';
6
+ import { showToast } from './utils.js';
7
+
8
+ function current() { return store.get('contextualSearchAsk'); }
9
+ function view() { return JSON.stringify([store.get('myUserId'), store.get('currentSlug'), store.get('activeSessionId'), store.get('homeShellVisible'), store.get('homeSubSurface'), store.get('dmMode'), store.get('rightWorkbenchOwner'), store.get('rightWorkbenchRevision')]); }
10
+ function patch(change) { store.set({ contextualSearchAsk: Object.assign({}, current(), change) }); }
11
+ function live(request) {
12
+ var mate = contextualMate(request.mateId);
13
+ return request.ownerId === store.get('myUserId') && mate && mate.builtinKey === 'clay' && contextualMateStamp(mate) === request.stamp;
14
+ }
15
+ function sendCreation() {
16
+ var request = current(), ws = getWs();
17
+ if (!request || !live(request)) { patch({ status: 'error', error: 'Clay is unavailable. Your question is retained.' }); return; }
18
+ if (!store.get('connected') || !ws || ws.readyState !== 1) { patch({ status: 'error', error: 'Reconnect, then retry. Your question is retained.' }); return; }
19
+ patch({ status: 'creating', socket: ws, view: view(), error: null });
20
+ try { ws.send(JSON.stringify({ type: 'home_clay_ask', presentation: 'contextual', requestId: request.requestId, text: request.text, sessionOriginId: request.sessionOriginId || null })); }
21
+ catch (error) { patch({ status: 'error', error: 'Creation is unconfirmed. Retry checks the same request.' }); }
22
+ setTimeout(function () { if (current() && current().requestId === request.requestId && current().status === 'creating') patch({ status: 'error', error: 'Conversation creation is unconfirmed. Retry checks the same request.' }); }, 15000);
23
+ }
24
+ export function pauseContextualSearchForSelection() {
25
+ if (current() && /^(creating|opening)$/.test(current().status)) stop();
26
+ }
27
+ function stop(execution) {
28
+ var request = current(), frame = request && request.sessionId && contextualFrame(request.mateId, request.sessionId);
29
+ if (frame && frame.conversation) { if (execution === true) frame.conversation.stopAsk(); else frame.conversation.pauseAsk(); }
30
+ patch({ status: 'stopped', error: request && request.clientMessageId ? 'Automatic delivery is paused. Inspect the conversation to confirm whether work stopped.' : 'Stopped before submission. Your question is retained; it will not be submitted automatically.' });
31
+ }
32
+ function resume() {
33
+ var request = current();
34
+ if (!request || !live(request)) return;
35
+ if (request.clientMessageId) { showToast('Inspect this conversation before retrying an uncertain message.', 'warn'); return; }
36
+ showContextualAsk(contextualMate(request.mateId), request.requestId, request.sessionId);
37
+ sendCreation();
38
+ }
39
+ function submit() {
40
+ var request = current();
41
+ if (!request || request.status !== 'opening') return;
42
+ if (!live(request) || request.view !== view()) { patch({ status: 'error', error: 'The view changed. Retry explicitly from the current view.' }); return; }
43
+ var frame = contextualFrame(request.mateId, request.sessionId);
44
+ if (!frame || frame.hidden || !frame.conversation || !frame.conversation.identity().ready) return;
45
+ var identity = frame.conversation.identity();
46
+ if (identity.ownerId !== request.ownerId || identity.sessionId !== request.sessionId || identity.sessionOriginId !== request.sessionOriginId || identity.mateId !== request.mateId) { patch({ status: 'error', error: 'The exact conversation identity changed. Nothing was submitted.' }); return; }
47
+ patch({ status: 'sending' });
48
+ var receipt = frame.conversation.submitAsk({ requestId: request.requestId, text: request.text, ownerId: request.ownerId, sessionId: request.sessionId, sessionOriginId: request.sessionOriginId });
49
+ if (!receipt || !receipt.clientMessageId) { patch({ status: 'error', error: 'Submission is unconfirmed. Inspect the retained question before retrying.' }); return; }
50
+ patch({ status: 'sent', clientMessageId: receipt.clientMessageId });
51
+ }
52
+ function render() {
53
+ var host = document.getElementById('contextual-clay');
54
+ if (!host) return;
55
+ var status = host.querySelector('[data-contextual-ask-status]');
56
+ if (!status) {
57
+ status = document.createElement('section'); status.dataset.contextualAskStatus = ''; status.setAttribute('role', 'region'); status.setAttribute('aria-label', 'Pending question');
58
+ status.innerHTML = '<div class="contextual-pending-question"><span>Pending question</span><blockquote data-ask-question></blockquote></div><div class="contextual-pending-progress"><p role="status" aria-live="polite" aria-atomic="true"></p><div><button type="button" data-ask-retry>Retry</button><button type="button" data-ask-stop>Stop</button><button type="button" data-ask-inspect>Open conversation</button></div></div>';
59
+ host.querySelector('.contextual-clay-body').appendChild(status);
60
+ status.querySelector('[data-ask-inspect]').addEventListener('click', function () { var request = current(); if (request && live(request)) activateContextualMate(request.mateId); });
61
+ status.querySelector('[data-ask-retry]').addEventListener('click', resume);
62
+ status.querySelector('[data-ask-stop]').addEventListener('click', function () { stop(true); });
63
+ host.addEventListener('contextual-clay-ready', submit);
64
+ host.addEventListener('contextual-clay-availability', function () {
65
+ var request = current(), frame = request && contextualFrame(request.mateId, request.sessionId);
66
+ if (request && request.status === 'opening' && frame && frame.conversation && frame.conversation.identity().available === false) patch({ status: 'error', error: 'This new conversation is unavailable. Your question is retained.' });
67
+ });
68
+ host.addEventListener('contextual-context-submitted', function (event) {
69
+ var request = current(), frame = request && contextualFrame(request.mateId, request.sessionId);
70
+ if (request && frame && frame.contentWindow === event.detail.source && request.clientMessageId && request.clientMessageId === event.detail.message.clientMessageId) patch({ status: 'acknowledged', acknowledged: true });
71
+ });
72
+ host.addEventListener('contextual-ask-stopped', function (event) { var request = current(), frame = request && contextualFrame(request.mateId, request.sessionId); if (frame && frame.contentWindow === event.detail.source) stop(); });
73
+ }
74
+ var focused = document.activeElement, ownedFocus = status.contains(focused);
75
+ var request = current(), selected = store.get('contextualClay') || {};
76
+ status.hidden = !request || selected.mateId !== request.mateId || request.acknowledged || request.status === 'acknowledged' || selected.sessionId && selected.sessionId !== request.sessionId;
77
+ if (request) {
78
+ status.querySelector('[data-ask-question]').textContent = request.text;
79
+ status.querySelector('p').textContent = request.error || (request.status === 'creating' ? 'Starting a new conversation. Your question is waiting.' : request.status === 'opening' ? 'Preparing this conversation. Nothing submitted yet.' : 'Waiting for confirmation. Delivery is not confirmed yet.');
80
+ status.querySelector('[data-ask-inspect]').hidden = !request.clientMessageId;
81
+ status.querySelector('[data-ask-retry]').hidden = request.status !== 'error' && request.status !== 'stopped' || !!request.clientMessageId;
82
+ status.querySelector('[data-ask-stop]').hidden = request.status === 'stopped';
83
+ }
84
+ host.dataset.askPending = String(!status.hidden);
85
+ var selectedFrame = contextualFrame();
86
+ host.querySelector('[data-clay-loading]').hidden = !status.hidden || !!(selectedFrame && selectedFrame.conversation && selectedFrame.conversation.identity().ready);
87
+ host.querySelectorAll('iframe').forEach(function (frame) { frame.inert = !status.hidden && frame.dataset.mateId === request.mateId && String(frame.dataset.sessionId) === String(request.sessionId); });
88
+ if (status.hidden) {
89
+ enterComposer();
90
+ if (ownedFocus && selectedFrame && selectedFrame.conversation && selectedFrame.conversation.identity().ready) selectedFrame.contentDocument.getElementById('input').focus({ preventScroll: true });
91
+ } else if (ownedFocus && focused.hidden) {
92
+ var nextControl = status.querySelector('[data-ask-retry]:not([hidden]), [data-ask-inspect]:not([hidden]), [data-ask-stop]:not([hidden])');
93
+ if (nextControl) nextControl.focus({ preventScroll: true });
94
+ }
95
+
96
+ }
97
+
98
+ export function startContextualSearch(text) {
99
+ text = typeof text === 'string' ? text.trim().slice(0, 12000) : '';
100
+ var mate = (store.get('cachedMatesList') || []).find(function (item) { return item.builtinKey === 'clay' && contextualMate(item.id); });
101
+ if (!text || !mate) { showToast('Clay is unavailable. Your question remains in Search.', 'warn'); return false; }
102
+ var request = current();
103
+ if (request && /^(creating|opening|sending|sent)$/.test(request.status)) { showToast('Clay is still starting the previous question. This question remains in Search.', 'warn'); return false; }
104
+ var requestId = 'contextual-search-' + crypto.randomUUID();
105
+ if (!showContextualAsk(mate, requestId, null)) return false;
106
+ store.set({ contextualSearchAsk: { requestId: requestId, text: text, mateId: mate.id, ownerId: store.get('myUserId'), stamp: contextualMateStamp(mate), status: 'creating', view: view() } });
107
+ render(); sendCreation(); return true;
108
+ }
109
+ export function retryContextualSearch() {
110
+ var request = current();
111
+ if (!request || !live(request) || request.clientMessageId || !/^(error|stopped)$/.test(request.status)) return false;
112
+ resume(); return true;
113
+ }
114
+ export function handleContextualSearchMessage(msg) {
115
+ var request = current();
116
+ if (!request || request.requestId !== msg.requestId) return false;
117
+ if (msg.type === 'home_mate_error') { patch({ status: 'error', error: msg.text || 'The conversation could not be started. Your question is retained.' }); return true; }
118
+ if (msg.type !== 'home_clay_contextual_session') return false;
119
+ if (request.status !== 'creating' || request.socket !== getWs() || !live(request) || request.mateId !== msg.mateId || !Number.isSafeInteger(msg.sessionId) || msg.sessionId <= 0 || typeof msg.sessionOriginId !== 'string' || request.sessionOriginId && request.sessionOriginId !== msg.sessionOriginId) return true;
120
+ if (request.view !== view()) { patch({ sessionId: msg.sessionId, sessionOriginId: msg.sessionOriginId, status: 'error', error: 'The view changed. Retry explicitly from the current view.' }); return true; }
121
+ patch({ sessionId: msg.sessionId, sessionOriginId: msg.sessionOriginId, status: 'opening' });
122
+ showContextualAsk(contextualMate(request.mateId), request.requestId, msg.sessionId);
123
+ var frame = contextualFrame(request.mateId, msg.sessionId);
124
+ if (frame) frame.dataset.contextualTitle = request.text.slice(0, 80);
125
+ setTimeout(function () { if (current() && current().requestId === request.requestId && current().status === 'opening') patch({ status: 'error', error: 'The conversation is not ready. Retry checks this same conversation.' }); }, 15000);
126
+ submit(); return true;
127
+ }
128
+ store.subscribe(function (value, previous) {
129
+ if (current() && current().ownerId !== value.myUserId) { store.set({ contextualSearchAsk: null }); return; }
130
+ if (current() && !live(current()) && /^(creating|opening)$/.test(current().status)) patch({ status: 'error', error: 'Clay is no longer available. Your question is retained.' });
131
+ if (current() && previous.connected && !value.connected && /^(creating|opening)$/.test(current().status)) patch({ status: 'error', error: 'Disconnected. Retry checks the same new conversation; your question is retained.' });
132
+ render();
133
+ });
134
+ document.addEventListener('click', function (event) {
135
+ var request = current();
136
+ if (request && store.get('currentSlug') === 'mate-' + request.mateId && store.get('activeSessionId') === request.sessionId && event.target.closest('#generation-stop, #autonomous-run-stop, #send-btn.stop')) stop();
137
+ }, true);
@@ -0,0 +1,58 @@
1
+ // Human citation clicks leave the pinned pane intact and use the parent viewer's
2
+ // existing authenticated reference resolver, never a project label as a target.
3
+ import { store } from './store.js';
4
+ import { getWs } from './ws-ref.js';
5
+ import { contextualFrame, contextualMate, contextualMateStamp } from './contextual-mate-identity.js';
6
+ import { parseClaySessionReference } from './clay-log-links.js';
7
+ import { requestClayRecordLink } from './command-palette.js';
8
+
9
+ function capture() {
10
+ var frame = contextualFrame(), state = store.get('contextualClay') || {}, mate = contextualMate(state.mateId);
11
+ var identity = frame && frame.conversation && frame.conversation.identity();
12
+ if (!mate || state.mode !== 'popover' || !frame || frame.hidden || frame.inert || !identity || !identity.ready || frame.conversation.source !== frame.contentWindow || identity.ownerId !== store.get('myUserId') || identity.mateId !== mate.id || frame.dataset.identityStamp !== contextualMateStamp(mate)) return null;
13
+ var surface = document.getElementById('contextual-clay');
14
+ return { mateId: mate.id, stamp: contextualMateStamp(mate), sessionId: identity.sessionId, originId: identity.sessionOriginId, ownerId: identity.ownerId, view: surface.dataset.screenReference, socket: getWs() };
15
+ }
16
+ export function validContextualCitation(context) {
17
+ var live = capture();
18
+ return !!(context && live && live.socket === context.socket && live.mateId === context.mateId && live.stamp === context.stamp && live.sessionId === context.sessionId && live.originId === context.originId && live.ownerId === context.ownerId && live.view === context.view);
19
+ }
20
+ export function contextualCitationLink(context, requestId) {
21
+ var frames = Array.from(document.querySelectorAll('#contextual-clay iframe'));
22
+ for (var frame of frames) {
23
+ var mate = contextualMate(frame.dataset.mateId), identity = frame.conversation && frame.conversation.identity();
24
+ if (!mate || !identity || identity.ownerId !== store.get('myUserId') || frame.dataset.identityStamp !== contextualMateStamp(mate)) continue;
25
+ if (context && (identity.mateId !== context.mateId || identity.sessionId !== context.sessionId || identity.sessionOriginId !== context.originId || context.ownerId !== identity.ownerId)) continue;
26
+ var link = Array.from(frame.contentDocument.querySelectorAll('.clayos-session-link')).find(function (item) { return item.dataset.sessionRequest === requestId; });
27
+ if (link) return link;
28
+ }
29
+ return null;
30
+ }
31
+ export function attachContextualSessionCitations(surface) {
32
+ surface.resolveSessionCitation = function (source, link) {
33
+ var context = capture(), frame = contextualFrame();
34
+ if (!context || !frame || frame.contentWindow !== source || !link || link.ownerDocument !== frame.contentDocument || !link.closest('.msg-assistant') || !parseClaySessionReference(link.dataset.sessionRef || '')) return false;
35
+ context.ref = link.dataset.sessionRef;
36
+ return requestClayRecordLink(link, context);
37
+ };
38
+ }
39
+ export function forwardContextualSessionCitation(link) {
40
+ if (!store.get('paneMode') || new URLSearchParams(location.search).get('contextual') !== '1') return false;
41
+ try {
42
+ var surface = window.parent.document.getElementById('contextual-clay');
43
+ if (surface && surface.resolveSessionCitation && surface.resolveSessionCitation(window, link)) return true;
44
+ } catch (error) { /* A changed authentication document cannot receive clicks. */ }
45
+ if (link.classList.contains('is-error')) return true;
46
+ link.classList.add('is-error'); link.title = 'This conversation or view changed. Reopen it before navigating.';
47
+ return true;
48
+ }
49
+ // Project transport replacement may discard an old reply before the palette
50
+ // sees it. Retire that pending UI receipt at the context change itself.
51
+ store.subscribe(function (value) {
52
+ var pending = value.sessionNavigationRequestContext;
53
+ if (value.paneMode || !pending || !pending.citation) return;
54
+ if (pending.projectSlug === value.currentSlug && pending.sessionId === value.activeSessionId && validContextualCitation(pending.citation)) return;
55
+ var link = contextualCitationLink(pending.citation, value.sessionNavigationRequestId);
56
+ if (link) { link.classList.remove('is-loading'); link.classList.add('is-error'); link.title = 'The view changed. Click again from the current conversation.'; delete link.dataset.sessionRequest; }
57
+ store.set({ sessionNavigationRequestId: null, sessionNavigationRequestContext: null });
58
+ });
@@ -1,3 +1,5 @@
1
+ import { presentedDocument, refreshPresentedDocument, closePresentedDocument } from './clay-studio-document.js';
2
+ import { registerStudioPaint } from './clay-studio-render.js';
1
3
  import { iconHtml, refreshIcons } from './icons.js';
2
4
  import { escapeHtml, copyToClipboard } from './utils.js';
3
5
  import { renderMarkdown, highlightCodeBlocks, renderMermaidBlocks, exportMarkdownAsPdf } from './markdown.js';
@@ -192,6 +194,9 @@ export function initFileBrowser(_ctx) {
192
194
 
193
195
  document.getElementById("file-viewer-download").addEventListener("click", function () {
194
196
  if (!currentFilePath) return;
197
+ if ((store.get("studioDocumentView") || {}).descriptor && store.get("studioDocumentView").descriptor.path === currentFilePath) {
198
+ var link = document.createElement('a'); link.href = URL.createObjectURL(new Blob([currentContent || ''], { type: 'text/markdown;charset=utf-8' })); link.download = currentFilePath.split('/').pop(); link.click(); setTimeout(function () { URL.revokeObjectURL(link.href); }, 1000); return;
199
+ }
195
200
  downloadProjectFile(currentFilePath);
196
201
  });
197
202
 
@@ -459,12 +464,14 @@ function handleTreeKeyDown(e) {
459
464
 
460
465
  // --- File watch helpers ---
461
466
  function sendWatch(filePath) {
467
+ if ((store.get("studioDocumentView") || {}).descriptor && store.get("studioDocumentView").descriptor.path === filePath) return;
462
468
  if (ctx.ws && ctx.connected) {
463
469
  ctx.ws.send(JSON.stringify({ type: "fs_watch", path: filePath }));
464
470
  }
465
471
  }
466
472
 
467
473
  function sendUnwatch() {
474
+ if (store.get("studioDocumentView")) { closePresentedDocument(); return; }
468
475
  if (ctx.ws && ctx.connected) {
469
476
  ctx.ws.send(JSON.stringify({ type: "fs_unwatch" }));
470
477
  }
@@ -532,6 +539,7 @@ export function resetFileBrowser() {
532
539
  treeData = {};
533
540
  currentContent = null;
534
541
  currentFilePath = null;
542
+ store.set({ clayControlFileView: null });
535
543
  isRendered = false;
536
544
  currentIsMarkdown = false;
537
545
  currentIsSvg = false;
@@ -568,6 +576,7 @@ export function openFile(filePath, opts) {
568
576
  if (!state.connected) return false;
569
577
  if (opts && opts.projectSlug && opts.projectSlug !== state.currentSlug) return false;
570
578
  if (opts && opts.sessionId && String(opts.sessionId) !== String(state.activeSessionId)) return false;
579
+ closePresentedDocument();
571
580
  var workbenchRevision = claimRightWorkbench("file-viewer");
572
581
  if (followedFileChanged(filePath) || (opts && opts.diff && isFollowingMarkdown(filePath))) {
573
582
  cancelMarkdownFollow();
@@ -635,6 +644,7 @@ export function presentMarkdownEdit(msg) {
635
644
  content: typeof msg.content === "string" ? msg.content : "",
636
645
  size: msg.size || 0,
637
646
  });
647
+ registerStudioPaint(msg, document.querySelector("#file-viewer .file-viewer-header"), null, document.getElementById("file-viewer-body"));
638
648
  }
639
649
 
640
650
  function followedFileChanged(filePath) {
@@ -723,6 +733,7 @@ function requestDirectory(dirPath) {
723
733
  }
724
734
 
725
735
  function requestFileContent(filePath) {
736
+ if (refreshPresentedDocument(filePath)) return;
726
737
  var requestId = "file-read-" + Date.now() + "-" + Math.random().toString(36).slice(2);
727
738
  var state = store.snap();
728
739
  store.set({ fileReadRequest: { requestId: requestId, path: filePath, projectSlug: state.currentSlug || null, sessionId: state.activeSessionId != null ? String(state.activeSessionId) : null, accountId: state.myUserId || null, workbenchRevision: store.get('rightWorkbenchRevision') || 0 } });
@@ -1148,6 +1159,7 @@ function renderEntries(container, entries, depth) {
1148
1159
  // --- File viewer ---
1149
1160
 
1150
1161
  function showFileContent(msg, expectedRevision) {
1162
+ var priorTarget = document.querySelector("#file-viewer .file-viewer-header"); if (priorTarget) delete priorTarget.dataset.controlSurface;
1151
1163
  if (!updateFileViewerTab(msg.path, { content: msg.content })) return;
1152
1164
  if (expectedRevision != null && !isRightWorkbenchCurrent("file-viewer", expectedRevision)) return;
1153
1165
  var pathEl = document.getElementById("file-viewer-path");
@@ -1170,6 +1182,8 @@ function showFileContent(msg, expectedRevision) {
1170
1182
  pendingRefresh = false;
1171
1183
  currentContent = null;
1172
1184
  currentFilePath = msg.path;
1185
+ var sourceView = store.get('studioDocumentView'), sourceDescriptor = sourceView && sourceView.descriptor.path === msg.path ? sourceView.descriptor : null;
1186
+ store.set({ clayControlFileView: { path: msg.path, revision: msg.controlRevision || null, projectSlug: sourceDescriptor ? sourceDescriptor.sourceSlug : store.get('currentSlug'), runtimeId: sourceDescriptor ? sourceDescriptor.runtimeId : store.get('controlRuntimeId') } });
1173
1187
  currentIsMarkdown = false;
1174
1188
  currentIsSvg = false;
1175
1189
  store.set({ fileViewerIsPlantUml: false, fileViewerIsHtml: false });
@@ -1279,6 +1293,8 @@ function showFileContent(msg, expectedRevision) {
1279
1293
  store.set({ pendingFileNavigation: null });
1280
1294
  }
1281
1295
 
1296
+ var sourceView = presentedDocument(msg.path);
1297
+ if (sourceView) Array.from(bodyEl.querySelectorAll('.clay-file-link')).forEach(function (link) { link.dataset.fileProjectSlug = sourceView.descriptor.sourceSlug; link.dataset.fileSessionId = String(sourceView.descriptor.sourceSessionId); });
1282
1298
  // Request edit history for this file (skip on auto-refresh)
1283
1299
  if (!keepRenderState) {
1284
1300
  historyVisible = false;
@@ -1361,7 +1377,9 @@ function pathsReferToSameFile(left, right) {
1361
1377
  return a === b || a.endsWith("/" + b) || b.endsWith("/" + a);
1362
1378
  }
1363
1379
 
1364
- export function handleFileChanged(msg) {
1380
+ export function handleFileChanged(msg, presentedSource) {
1381
+ var source = store.get('studioDocumentView');
1382
+ if (source && source.descriptor.path === msg.path && (!presentedSource || presentedDocument(msg.path) !== presentedSource)) return;
1365
1383
  if (!msg.path || !pathsReferToSameFile(msg.path, currentFilePath)) return;
1366
1384
  if (ctx.fileViewerEl.classList.contains("hidden")) return;
1367
1385
  if (historyVisible || inlineDiffActive) return;
@@ -1602,6 +1620,7 @@ function formatSize(bytes) {
1602
1620
  // --- File edit history ---
1603
1621
 
1604
1622
  function requestFileHistory(filePath) {
1623
+ if ((store.get("studioDocumentView") || {}).descriptor && store.get("studioDocumentView").descriptor.path === filePath) return;
1605
1624
  if (ctx.ws && ctx.connected) {
1606
1625
  ctx.ws.send(JSON.stringify({ type: "fs_file_history", path: filePath }));
1607
1626
  }
@@ -2,6 +2,13 @@ import { store } from './store.js';
2
2
  import { getWs } from './ws-ref.js';
3
3
  import { isDriverOperatedView } from './worker-pane-lock.js';
4
4
 
5
+ export function stopGeneration() {
6
+ var ws = getWs();
7
+ if (!store.get('processing') || !store.get('connected') || !ws || ws.readyState !== 1) return false;
8
+ ws.send(JSON.stringify({ type: 'stop' }));
9
+ return true;
10
+ }
11
+
5
12
  // Fallback Stop control for when Send cannot double as Stop: a draft sitting
6
13
  // in the composer (Send stays Send so the draft can still go out), or a
7
14
  // locked configured Worker pane (Send and the rest of the composer are
@@ -31,11 +38,7 @@ export function initGenerationStop() {
31
38
  button.title = store.get('connected') ? 'Stop generating' : 'Reconnect to stop generating';
32
39
  place();
33
40
  }
34
- button.addEventListener('click', function () {
35
- var ws = getWs();
36
- if (!store.get('processing') || !store.get('connected') || !ws || ws.readyState !== 1) return;
37
- ws.send(JSON.stringify({ type: 'stop' }));
38
- });
41
+ button.addEventListener('click', stopGeneration);
39
42
  store.subscribe(function (state, previous) {
40
43
  if (state.processing !== previous.processing || state.connected !== previous.connected
41
44
  || state.splitGroups !== previous.splitGroups || state.activeSessionId !== previous.activeSessionId
@@ -103,7 +103,8 @@ function renderFileRow(file, staged) {
103
103
  var actionTitle = staged ? "Unstage file — keep its changes" : "Stage file for commit";
104
104
  var actionIcon = staged ? "minus" : "plus";
105
105
  var actionsOpen = expandedFileActionsPath === file.path;
106
- return '<div class="git-file-row' + (actionsOpen ? ' active' : '') + '">' +
106
+ var control = currentStatus && currentStatus.controlView, view = control && control.files.find(function (item) { return item.path === file.path; });
107
+ return '<div data-control-git-path="' + escapeHtml(file.path) + '" data-control-revision="' + escapeHtml(view && view.revision || '') + '" data-control-runtime="' + escapeHtml(control && control.runtimeId || '') + '" class="git-file-row' + (actionsOpen ? ' active' : '') + '">' +
107
108
  '<span class="git-file-status' + statusClass + '">' + escapeHtml(fileStatusLabel(file, staged)) + '</span>' +
108
109
  '<div class="git-file-main" data-git-file-item="' + pathKey + '" tabindex="0" aria-label="File actions for ' + escapeHtml(parts.name) + '" aria-expanded="' + (actionsOpen ? 'true' : 'false') + '">' +
109
110
  '<div class="git-file-name">' + escapeHtml(parts.name) + '</div>' +
@@ -282,6 +283,7 @@ export function refreshGitStatus() {
282
283
  return requestJson(basePath + "api/git/status", { cache: "no-store" }).then(function (status) {
283
284
  if (statusBasePath !== basePath) return status;
284
285
  currentStatus = status;
286
+ store.set({ clayControlGitView: status.controlView ? Object.assign({ projectSlug: store.get('currentSlug') }, status.controlView) : null });
285
287
  if (expandedFileActionsPath && !status.files.some(function (file) { return file.path === expandedFileActionsPath; })) {
286
288
  expandedFileActionsPath = null;
287
289
  }
@@ -81,7 +81,8 @@ export function showIconTooltipHtml(el, html, options) {
81
81
  export function bindIconTooltip(el, resolveMetadata) {
82
82
  function show() {
83
83
  var metadata = tooltipMetadata(typeof resolveMetadata === "function" ? resolveMetadata() : resolveMetadata);
84
- showIconTooltip(el, metadata.text || "", metadata);
84
+ if (!metadata.text) { hideIconTooltip(); return; }
85
+ showIconTooltip(el, metadata.text, metadata);
85
86
  }
86
87
  el.addEventListener("mouseenter", show);
87
88
  el.addEventListener("focus", show);
@@ -1,3 +1,5 @@
1
+ import { contextualAccessReference } from './contextual-mate-context.js';
2
+ import { contextualScreenReference } from './contextual-clay-session.js';
1
3
  import { stopSTT } from './stt-controller.js';
2
4
  import { expectCreatedSession } from './session-create.js';
3
5
  import { hasRecordedSpeechFor, requestRecordedSend } from './stt-recording.js';
@@ -120,6 +122,7 @@ function showPreThinkingForTurn() {
120
122
 
121
123
  // --- Send ---
122
124
  export function sendMessage() {
125
+ if (store.get('contextualWriterSuspended')) return;
123
126
  if (ctx && requestRecordedSend(ctx.inputEl, sendMessage)) return;
124
127
  stopSTT();
125
128
  // Debate ended mode intercept: route to resume handler
@@ -349,7 +352,9 @@ export function sendMessage() {
349
352
  return;
350
353
  }
351
354
 
355
+ var screenReference = contextualScreenReference();
352
356
  var payload = { type: "message", text: text || "" };
357
+ if (screenReference) payload.contextualSnapshot = screenReference;
353
358
  if (images.length > 0) {
354
359
  payload.images = images;
355
360
  }
@@ -359,6 +364,8 @@ export function sendMessage() {
359
364
  // Include selected vendor for session binding (server uses on first message)
360
365
  var _selVendor = store.get("currentVendor") || null;
361
366
  if (_selVendor) payload.vendor = _selVendor;
367
+ var contextRef = contextualAccessReference();
368
+ if (contextRef) payload.mateContextRef = contextRef;
362
369
  if (!prepareAutonomousPayload(payload)) return;
363
370
  ctx.currentMsgTs = Date.now();
364
371
  sendAcknowledgedMessage(payload);
@@ -448,7 +455,9 @@ export function sendShellResultToAgent(msg) {
448
455
 
449
456
  export function autoResize() {
450
457
  ctx.inputEl.style.height = "auto";
451
- ctx.inputEl.style.height = Math.min(ctx.inputEl.scrollHeight, 120) + "px";
458
+ var border = ctx.inputEl.offsetHeight - ctx.inputEl.clientHeight;
459
+ ctx.inputEl.style.height = Math.min(ctx.inputEl.scrollHeight + border, 120) + "px";
460
+ ctx.inputEl.style.overflowY = ctx.inputEl.scrollHeight > ctx.inputEl.clientHeight ? "auto" : "hidden";
452
461
  // Defensive: sync send/stop button whenever input size changes
453
462
  if (ctx.processing && ctx.setSendBtnMode) {
454
463
  ctx.setSendBtnMode(hasSendableContent() ? "send" : "stop");
@@ -533,6 +542,30 @@ function removePendingImage(idx) {
533
542
  renderInputPreviews();
534
543
  }
535
544
 
545
+ // Move composer ownership between two authenticated views of the same session.
546
+ export function takeComposerDraft() {
547
+ if (uploadingCount) throw new Error('Wait for attachments to finish uploading before opening Clay.');
548
+ if (store.get('speechActive') || hasRecordedSpeechFor(ctx.inputEl)) throw new Error('Finish or discard the voice recording before changing Clay views.');
549
+ var input = ctx.inputEl;
550
+ var draft = { text: input.value, start: input.selectionStart, end: input.selectionEnd, direction: input.selectionDirection, images: pendingImages, pastes: pendingPastes, files: pendingFiles, delay: scheduleDelayMs };
551
+ input.value = '';
552
+ clearPendingImages();
553
+ clearScheduleDelay();
554
+ autoResize();
555
+ return draft;
556
+ }
557
+ export function restoreComposerDraft(draft) {
558
+ if (!draft) return;
559
+ pendingImages = draft.images || [];
560
+ pendingPastes = draft.pastes || [];
561
+ pendingFiles = draft.files || [];
562
+ ctx.inputEl.value = draft.text || '';
563
+ ctx.inputEl.setSelectionRange(draft.start || 0, draft.end || 0, draft.direction || 'none');
564
+ setScheduleDelayMs(draft.delay || 0);
565
+ renderInputPreviews();
566
+ autoResize();
567
+ }
568
+
536
569
  export function clearPendingImages() {
537
570
  pendingImages = [];
538
571
  pendingPastes = [];
@@ -840,6 +873,7 @@ function sendInputSync() {
840
873
  }
841
874
 
842
875
  export function handleInputSync(text) {
876
+ if (store.get('contextualWriterSuspended')) return;
843
877
  isRemoteInput = true;
844
878
  ctx.inputEl.value = text;
845
879
  autoResize();
@@ -1,3 +1,4 @@
1
+ import { store } from './store.js';
1
2
  import { githubWorkMarkup } from './session-github.js';
2
3
  import { issueGithubAction } from './issues-github.js';
3
4
  import { escapeHtml } from './utils.js';
@@ -18,7 +19,7 @@ export function renderIssueList(result) {
18
19
  var meta = [authorLine(entry), relativeTime(entry.updatedAt || entry.createdAt)];
19
20
  if (entry.revision > 1) meta.push('v' + entry.revision);
20
21
  if (entry.commentCount) meta.push(entry.commentCount + (entry.commentCount === 1 ? ' comment' : ' comments'));
21
- return '<button type="button" class="issue-row" data-open-issue="' + esc(entry.ref) + '">' +
22
+ return '<button type="button" class="issue-row" data-control-runtime="' + esc(store.get("controlRuntimeId")) + '" data-control-revision="' + esc(entry.revision) + '" data-open-issue="' + esc(entry.ref) + '">' +
22
23
  '<div class="issue-row-head"><span class="issue-type">' + esc(entry.type) + '</span>' + priorityChip(entry.priority) + '<strong>' + esc(entry.title) + '</strong></div>' +
23
24
  (entry.summary ? '<p>' + esc(entry.summary) + '</p>' : '') +
24
25
  '<div class="issue-row-meta">' + statusChip(entry.status) + '<span>' + esc(meta.filter(Boolean).join(' · ')) + '</span></div></button>';
@@ -35,7 +36,7 @@ export function renderIssueDetail(entry) {
35
36
  var closed = entry.status === 'resolved' || entry.status === 'closed';
36
37
  var time = entry.updatedAt || entry.createdAt;
37
38
  var byline = [authorLine(entry), time ? new Date(time).toLocaleString() : '', 'v' + entry.revision].filter(Boolean).join(' · ');
38
- return '<article class="issue-detail"><div class="issue-doc-chips"><span class="issue-type">' + esc(entry.type) + '</span>' + priorityChip(entry.priority) + statusChip(entry.status) + '</div>' +
39
+ return '<article class="issue-detail" data-control-ref="' + esc(entry.ref) + '" data-control-runtime="' + esc(store.get("controlRuntimeId")) + '" data-control-revision="' + esc(entry.revision) + '"><div class="issue-doc-chips"><span class="issue-type">' + esc(entry.type) + '</span>' + priorityChip(entry.priority) + statusChip(entry.status) + '</div>' +
39
40
  githubWorkMarkup(entry, true) +
40
41
  '<h2 class="issue-doc-title">' + esc(entry.title) + '</h2>' + prose(entry.summary, 'issue-doc-summary') + '<div class="issue-byline">' + esc(byline) + '</div>' +
41
42
  '<div class="issue-actions">' + action('data-issue-work', 'play', 'Start work', ' class="issue-primary"' + (closed ? ' disabled title="Reopen this issue to start work"' : '')) +