clay-server 3.6.0 → 3.7.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/project-http.js +51 -0
- package/lib/project-image.js +1 -1
- package/lib/project.js +3 -1
- package/lib/public/css/filebrowser.css +117 -7
- package/lib/public/css/generated-images.css +225 -0
- package/lib/public/css/input.css +2 -77
- package/lib/public/css/tui-attention.css +1 -2
- package/lib/public/index.html +3 -44
- package/lib/public/modules/app-messages.js +9 -33
- package/lib/public/modules/app-panels.js +0 -72
- package/lib/public/modules/filebrowser-context-menu.js +148 -0
- package/lib/public/modules/filebrowser.js +41 -4
- package/lib/public/modules/generated-images.js +132 -0
- package/lib/public/modules/input.js +2 -11
- package/lib/public/modules/session-tui-view.js +2 -3
- package/lib/public/modules/theme.js +4 -2
- package/lib/public/modules/tui-attention.js +5 -8
- package/lib/public/style.css +3 -2
- package/lib/sdk-bridge.js +2 -0
- package/lib/sdk-message-processor.js +57 -2
- package/lib/sessions.js +8 -6
- package/lib/yoke/adapters/codex.js +39 -0
- package/package.json +1 -1
|
@@ -46,6 +46,7 @@ import { showImageModal, sendExtensionCommand, handleMcpToolCallMessage } from '
|
|
|
46
46
|
import { handleMcpServersState } from './mcp-ui.js';
|
|
47
47
|
import { handleShellCommandResult } from './shell-command.js';
|
|
48
48
|
import { handleLoopRegistryUpdated, handleScheduleRunStarted, handleScheduleRunFinished, handleLoopScheduled, isSchedulerOpen, enterCraftingMode, exitCraftingMode, handleLoopRegistryFiles } from './scheduler.js';
|
|
49
|
+
import { renderGeneratedImage, renderImageGenerationProgress, clearImageGenerationProgress, clearAllImageGenerationProgress } from './generated-images.js';
|
|
49
50
|
|
|
50
51
|
// --- App module imports ---
|
|
51
52
|
import { scrollToBottom, addToMessages, addUserMessage, addSystemMessage, removeMatePreThinking, appendDelta, finalizeAssistantBlock, addConflictMessage, addContextOverflowMessage, showSuggestionChips, armStickyBottom, VENDOR_AVATARS, VENDOR_NAMES } from './app-rendering.js';
|
|
@@ -178,17 +179,6 @@ export function processMessage(msg) {
|
|
|
178
179
|
if (!store.get('sessionIsProcessing')) {
|
|
179
180
|
applyDeadSessionTodoCompaction();
|
|
180
181
|
}
|
|
181
|
-
// Show the locked vendor toggle only when history exists AND the
|
|
182
|
-
// vendor isn't already committed. With a committed vendor,
|
|
183
|
-
// session_switched has already hidden the toggle and shown the
|
|
184
|
-
// small #active-vendor-indicator; re-showing the locked toggle
|
|
185
|
-
// here would duplicate the avatar next to the indicator.
|
|
186
|
-
var _hTotal = store.get('historyTotal') || 0;
|
|
187
|
-
var _vtw2 = document.getElementById("vendor-toggle-wrap");
|
|
188
|
-
if (_vtw2 && _hTotal > 0 && !store.get('currentVendor')) {
|
|
189
|
-
_vtw2.classList.remove("hidden");
|
|
190
|
-
_vtw2.classList.add("locked");
|
|
191
|
-
}
|
|
192
182
|
// Restore cached rich context usage BEFORE updateContextPanel runs
|
|
193
183
|
if (msg.contextUsage) {
|
|
194
184
|
store.set({ richContextUsage: msg.contextUsage });
|
|
@@ -747,30 +737,9 @@ export function processMessage(msg) {
|
|
|
747
737
|
selectDefaultVendorForBlankSession();
|
|
748
738
|
}
|
|
749
739
|
if (!msg.model) requestVendorModels(store.get('currentVendor') || "claude", true);
|
|
750
|
-
//
|
|
751
|
-
// model chip.
|
|
752
|
-
// - Session has an explicit vendor: hide the toggle (it's
|
|
753
|
-
// committed for this conversation) and show a small avatar
|
|
754
|
-
// next to the config chip so the user still sees which vendor
|
|
755
|
-
// is in use.
|
|
756
|
-
// - History without recorded vendor: show locked toggle, no icon
|
|
757
|
-
// (we don't know what to render).
|
|
758
|
-
// - Brand-new no-vendor session: show toggle, no icon.
|
|
759
|
-
var _vtw = document.getElementById("vendor-toggle-wrap");
|
|
740
|
+
// Show the committed session vendor next to the model chip.
|
|
760
741
|
var _avi = document.getElementById("active-vendor-indicator");
|
|
761
742
|
var _avIcon = document.getElementById("active-vendor-icon");
|
|
762
|
-
if (_vtw) {
|
|
763
|
-
if (msg.vendor) {
|
|
764
|
-
_vtw.classList.add("hidden");
|
|
765
|
-
_vtw.classList.remove("locked");
|
|
766
|
-
} else if (msg.hasHistory) {
|
|
767
|
-
_vtw.classList.remove("hidden");
|
|
768
|
-
_vtw.classList.add("locked");
|
|
769
|
-
} else {
|
|
770
|
-
_vtw.classList.remove("locked");
|
|
771
|
-
_vtw.classList.remove("hidden");
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
743
|
if (_avi && _avIcon) {
|
|
775
744
|
if (msg.vendor && VENDOR_AVATARS[msg.vendor]) {
|
|
776
745
|
_avIcon.src = VENDOR_AVATARS[msg.vendor];
|
|
@@ -996,6 +965,7 @@ export function processMessage(msg) {
|
|
|
996
965
|
break;
|
|
997
966
|
|
|
998
967
|
case "tool_executing":
|
|
968
|
+
if (msg.name === "ImageGen") renderImageGenerationProgress(msg);
|
|
999
969
|
if ((msg.name === "propose_debate" || (msg.name && msg.name.indexOf("propose_debate") !== -1)) && msg.input) {
|
|
1000
970
|
var _dpTool = getTools()[msg.id];
|
|
1001
971
|
if (_dpTool) {
|
|
@@ -1047,6 +1017,7 @@ export function processMessage(msg) {
|
|
|
1047
1017
|
case "tool_result": {
|
|
1048
1018
|
var tr = getTools()[msg.id];
|
|
1049
1019
|
if (tr && tr.hidden) break; // skip hidden plan tools
|
|
1020
|
+
if (msg.is_error && tr && tr.name === "ImageGen") clearImageGenerationProgress(msg.id);
|
|
1050
1021
|
// Always call updateToolResult for Edit (to show diff from input), or when content exists
|
|
1051
1022
|
if (msg.content != null || msg.images || (tr && tr.name === "Edit" && tr.input && tr.input.old_string)) {
|
|
1052
1023
|
updateToolResult(msg.id, msg.content || "", msg.is_error || false, msg.images);
|
|
@@ -1061,6 +1032,10 @@ export function processMessage(msg) {
|
|
|
1061
1032
|
}
|
|
1062
1033
|
break;
|
|
1063
1034
|
|
|
1035
|
+
case "generated_image":
|
|
1036
|
+
renderGeneratedImage(msg);
|
|
1037
|
+
break;
|
|
1038
|
+
|
|
1064
1039
|
case "ask_user_answered":
|
|
1065
1040
|
markAskUserAnswered(msg.toolId, msg.answers);
|
|
1066
1041
|
stopUrgentBlink();
|
|
@@ -1160,6 +1135,7 @@ export function processMessage(msg) {
|
|
|
1160
1135
|
stopThinking();
|
|
1161
1136
|
markAllToolsDone();
|
|
1162
1137
|
closeToolGroup();
|
|
1138
|
+
clearAllImageGenerationProgress();
|
|
1163
1139
|
finalizeAssistantBlock();
|
|
1164
1140
|
addTurnMeta(msg.cost, msg.duration);
|
|
1165
1141
|
accumulateUsage(msg.cost, msg.usage);
|
|
@@ -5,7 +5,6 @@ import { refreshIcons } from "./icons.js";
|
|
|
5
5
|
import { escapeHtml, showToast } from "./utils.js";
|
|
6
6
|
import { store } from './store.js';
|
|
7
7
|
import { getWs } from './ws-ref.js';
|
|
8
|
-
import { VENDOR_NAMES, isExperimentalVendor } from './app-rendering.js';
|
|
9
8
|
import { reportPaneContext } from './pane-bridge.js';
|
|
10
9
|
import { setupModelPicker, renderModelPicker, requestVendorModels, prepareModelPickerOpen, modelDisplayName } from './model-picker.js';
|
|
11
10
|
|
|
@@ -368,79 +367,8 @@ export function initPanels() {
|
|
|
368
367
|
configWebsearchSection = $("config-websearch-section");
|
|
369
368
|
configWebsearchBar = $("config-websearch-bar");
|
|
370
369
|
|
|
371
|
-
// --- Vendor toggle ---
|
|
372
|
-
var vendorToggleWrap = $("vendor-toggle-wrap");
|
|
373
|
-
var vendorBtnClaude = $("vendor-btn-claude");
|
|
374
|
-
var vendorBtnCodex = $("vendor-btn-codex");
|
|
375
|
-
var vendorBtnAntigravity = $("vendor-btn-antigravity");
|
|
376
|
-
var vendorBtnOpenCode = $("vendor-btn-opencode");
|
|
377
|
-
var vendorBtnKimi = $("vendor-btn-kimi");
|
|
378
|
-
var vendorBtnGrok = $("vendor-btn-grok");
|
|
379
|
-
var vendorBtnCopilot = $("vendor-btn-copilot");
|
|
380
|
-
var vendorBtnQwen = $("vendor-btn-qwen");
|
|
381
|
-
var vendorBtnJunie = $("vendor-btn-junie");
|
|
382
|
-
var vendorBtnKiro = $("vendor-btn-kiro");
|
|
383
|
-
var vendorBtns = {
|
|
384
|
-
claude: vendorBtnClaude,
|
|
385
|
-
codex: vendorBtnCodex,
|
|
386
|
-
antigravity: vendorBtnAntigravity,
|
|
387
|
-
opencode: vendorBtnOpenCode,
|
|
388
|
-
kimi: vendorBtnKimi,
|
|
389
|
-
grok: vendorBtnGrok,
|
|
390
|
-
copilot: vendorBtnCopilot,
|
|
391
|
-
qwen: vendorBtnQwen,
|
|
392
|
-
junie: vendorBtnJunie,
|
|
393
|
-
kiro: vendorBtnKiro,
|
|
394
|
-
};
|
|
395
|
-
|
|
396
|
-
function updateVendorToggle() {
|
|
397
|
-
var installed = store.get('installedVendors') || [];
|
|
398
|
-
var current = store.get('currentVendor') || "claude";
|
|
399
|
-
|
|
400
|
-
var vendors = Object.keys(vendorBtns);
|
|
401
|
-
for (var i = 0; i < vendors.length; i++) {
|
|
402
|
-
var v = vendors[i];
|
|
403
|
-
var btn = vendorBtns[v];
|
|
404
|
-
if (!btn) continue;
|
|
405
|
-
var isInstalled = installed.indexOf(v) !== -1;
|
|
406
|
-
btn.classList.toggle("active", v === current);
|
|
407
|
-
btn.classList.toggle("disabled", !isInstalled);
|
|
408
|
-
btn.classList.toggle("experimental", isExperimentalVendor(v));
|
|
409
|
-
var title = isInstalled ? (VENDOR_NAMES[v] || v) : (VENDOR_NAMES[v] || v) + " is not installed";
|
|
410
|
-
if (isExperimentalVendor(v)) title += " · Experimental integration; not yet validated through direct use testing";
|
|
411
|
-
btn.title = title;
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
function onVendorClick(vendor) {
|
|
416
|
-
if (vendor === (store.get('currentVendor') || "claude")) return;
|
|
417
|
-
var installed = store.get('installedVendors') || [];
|
|
418
|
-
if (installed.indexOf(vendor) === -1) return;
|
|
419
|
-
store.set({ currentVendor: vendor, currentModel: "", currentModels: [], vendorCapabilities: {}, vendorSelectionLocked: true, sessionVendorBound: true });
|
|
420
|
-
var ws = getWs();
|
|
421
|
-
if (ws) ws.send(JSON.stringify({ type: "set_vendor", vendor: vendor }));
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
if (vendorBtnClaude) vendorBtnClaude.addEventListener("click", function() { onVendorClick("claude"); });
|
|
425
|
-
if (vendorBtnCodex) vendorBtnCodex.addEventListener("click", function() { onVendorClick("codex"); });
|
|
426
|
-
if (vendorBtnAntigravity) vendorBtnAntigravity.addEventListener("click", function() { onVendorClick("antigravity"); });
|
|
427
|
-
if (vendorBtnOpenCode) vendorBtnOpenCode.addEventListener("click", function() { onVendorClick("opencode"); });
|
|
428
|
-
if (vendorBtnKimi) vendorBtnKimi.addEventListener("click", function() { onVendorClick("kimi"); });
|
|
429
|
-
if (vendorBtnGrok) vendorBtnGrok.addEventListener("click", function() { onVendorClick("grok"); });
|
|
430
|
-
if (vendorBtnCopilot) vendorBtnCopilot.addEventListener("click", function() { onVendorClick("copilot"); });
|
|
431
|
-
if (vendorBtnQwen) vendorBtnQwen.addEventListener("click", function() { onVendorClick("qwen"); });
|
|
432
|
-
if (vendorBtnJunie) vendorBtnJunie.addEventListener("click", function() { onVendorClick("junie"); });
|
|
433
|
-
if (vendorBtnKiro) vendorBtnKiro.addEventListener("click", function() { onVendorClick("kiro"); });
|
|
434
|
-
|
|
435
370
|
// --- Reactive UI sync ---
|
|
436
371
|
store.subscribe(function (state, prev) {
|
|
437
|
-
// Vendor toggle state
|
|
438
|
-
if (state.availableVendors !== prev.availableVendors ||
|
|
439
|
-
state.installedVendors !== prev.installedVendors ||
|
|
440
|
-
state.currentVendor !== prev.currentVendor) {
|
|
441
|
-
updateVendorToggle();
|
|
442
|
-
}
|
|
443
|
-
|
|
444
372
|
// richContextUsage changed -> update popover + panel
|
|
445
373
|
if (state.richContextUsage !== prev.richContextUsage) {
|
|
446
374
|
if (state.richContextUsage) {
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// File-tree context menu and shared file download action.
|
|
2
|
+
|
|
3
|
+
import { iconHtml, refreshIcons } from './icons.js';
|
|
4
|
+
import { insertTextAtCursor } from './input.js';
|
|
5
|
+
import { copyToClipboard, showToast } from './utils.js';
|
|
6
|
+
|
|
7
|
+
var MAX_COPY_CONTENT_BYTES = 1024 * 1024;
|
|
8
|
+
|
|
9
|
+
function closeFileContextMenu() {
|
|
10
|
+
var menu = document.getElementById('file-tree-context-menu');
|
|
11
|
+
if (menu) menu.remove();
|
|
12
|
+
var activeRows = document.querySelectorAll('.file-tree-item.context-open');
|
|
13
|
+
for (var i = 0; i < activeRows.length; i++) activeRows[i].classList.remove('context-open');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function downloadProjectFile(filePath) {
|
|
17
|
+
if (!filePath) return;
|
|
18
|
+
var link = document.createElement('a');
|
|
19
|
+
link.href = 'api/file/download?path=' + encodeURIComponent(filePath);
|
|
20
|
+
link.download = filePath.split('/').pop() || 'download';
|
|
21
|
+
document.body.appendChild(link);
|
|
22
|
+
link.click();
|
|
23
|
+
link.remove();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function copyProjectFileContents(filePath) {
|
|
27
|
+
var url = 'api/file/download?path=' + encodeURIComponent(filePath);
|
|
28
|
+
fetch(url, { credentials: 'same-origin', cache: 'no-store' })
|
|
29
|
+
.then(function (response) {
|
|
30
|
+
if (!response.ok) throw new Error('Could not read file');
|
|
31
|
+
var contentLength = parseInt(response.headers.get('content-length') || '0', 10);
|
|
32
|
+
if (contentLength > MAX_COPY_CONTENT_BYTES) {
|
|
33
|
+
if (response.body && response.body.cancel) response.body.cancel();
|
|
34
|
+
throw new Error('File is too large to copy');
|
|
35
|
+
}
|
|
36
|
+
return response.arrayBuffer();
|
|
37
|
+
})
|
|
38
|
+
.then(function (buffer) {
|
|
39
|
+
if (buffer.byteLength > MAX_COPY_CONTENT_BYTES) throw new Error('File is too large to copy');
|
|
40
|
+
var bytes = new Uint8Array(buffer);
|
|
41
|
+
for (var i = 0; i < bytes.length; i++) {
|
|
42
|
+
if (bytes[i] === 0) throw new Error('Binary files cannot be copied as text');
|
|
43
|
+
}
|
|
44
|
+
var decoder = new TextDecoder('utf-8', { fatal: true });
|
|
45
|
+
var text;
|
|
46
|
+
try { text = decoder.decode(buffer); } catch (e) { throw new Error('Binary files cannot be copied as text'); }
|
|
47
|
+
return copyToClipboard(text);
|
|
48
|
+
})
|
|
49
|
+
.catch(function (error) {
|
|
50
|
+
var message = error && error.message ? error.message : 'Could not copy file contents';
|
|
51
|
+
showToast(message, 'error');
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function menuItem(icon, label, handler) {
|
|
56
|
+
var item = document.createElement('button');
|
|
57
|
+
item.type = 'button';
|
|
58
|
+
item.className = 'file-tree-context-item';
|
|
59
|
+
item.setAttribute('role', 'menuitem');
|
|
60
|
+
item.innerHTML = iconHtml(icon) + '<span>' + label + '</span>';
|
|
61
|
+
item.addEventListener('click', handler);
|
|
62
|
+
return item;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function positionMenu(menu, clientX, clientY) {
|
|
66
|
+
var edge = 8;
|
|
67
|
+
var rect = menu.getBoundingClientRect();
|
|
68
|
+
var left = Math.max(edge, Math.min(clientX, window.innerWidth - rect.width - edge));
|
|
69
|
+
var top = Math.max(edge, Math.min(clientY, window.innerHeight - rect.height - edge));
|
|
70
|
+
menu.style.left = left + 'px';
|
|
71
|
+
menu.style.top = top + 'px';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function showFileContextMenu(event, row) {
|
|
75
|
+
closeFileContextMenu();
|
|
76
|
+
row.classList.add('context-open');
|
|
77
|
+
|
|
78
|
+
var menu = document.createElement('div');
|
|
79
|
+
menu.id = 'file-tree-context-menu';
|
|
80
|
+
menu.className = 'file-tree-context-menu';
|
|
81
|
+
menu.setAttribute('role', 'menu');
|
|
82
|
+
menu.setAttribute('aria-label', 'File actions');
|
|
83
|
+
|
|
84
|
+
var mention = menuItem('at-sign', 'Mention in chat', function (clickEvent) {
|
|
85
|
+
clickEvent.stopPropagation();
|
|
86
|
+
var filePath = row.dataset.path;
|
|
87
|
+
closeFileContextMenu();
|
|
88
|
+
insertTextAtCursor(filePath + ' ');
|
|
89
|
+
});
|
|
90
|
+
menu.appendChild(mention);
|
|
91
|
+
|
|
92
|
+
var copyPath = menuItem('copy', 'Copy path', function (clickEvent) {
|
|
93
|
+
clickEvent.stopPropagation();
|
|
94
|
+
var filePath = row.dataset.path;
|
|
95
|
+
closeFileContextMenu();
|
|
96
|
+
copyToClipboard(filePath).catch(function () { showToast('Could not copy path', 'error'); });
|
|
97
|
+
});
|
|
98
|
+
menu.appendChild(copyPath);
|
|
99
|
+
|
|
100
|
+
var copyContents = menuItem('clipboard-copy', 'Copy contents', function (clickEvent) {
|
|
101
|
+
clickEvent.stopPropagation();
|
|
102
|
+
var filePath = row.dataset.path;
|
|
103
|
+
closeFileContextMenu();
|
|
104
|
+
copyProjectFileContents(filePath);
|
|
105
|
+
});
|
|
106
|
+
menu.appendChild(copyContents);
|
|
107
|
+
|
|
108
|
+
var separator = document.createElement('div');
|
|
109
|
+
separator.className = 'file-tree-context-separator';
|
|
110
|
+
separator.setAttribute('role', 'separator');
|
|
111
|
+
menu.appendChild(separator);
|
|
112
|
+
|
|
113
|
+
var download = menuItem('download', 'Download', function (clickEvent) {
|
|
114
|
+
clickEvent.stopPropagation();
|
|
115
|
+
var filePath = row.dataset.path;
|
|
116
|
+
closeFileContextMenu();
|
|
117
|
+
downloadProjectFile(filePath);
|
|
118
|
+
});
|
|
119
|
+
menu.appendChild(download);
|
|
120
|
+
document.body.appendChild(menu);
|
|
121
|
+
refreshIcons(menu);
|
|
122
|
+
positionMenu(menu, event.clientX, event.clientY);
|
|
123
|
+
try { mention.focus({ preventScroll: true }); } catch (e) { mention.focus(); }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function initFileBrowserContextMenu(treeEl) {
|
|
127
|
+
if (!treeEl || treeEl.dataset.contextMenuReady === 'true') return;
|
|
128
|
+
treeEl.dataset.contextMenuReady = 'true';
|
|
129
|
+
|
|
130
|
+
treeEl.addEventListener('contextmenu', function (event) {
|
|
131
|
+
var row = event.target && event.target.closest ? event.target.closest('.file-tree-item') : null;
|
|
132
|
+
if (!row || !treeEl.contains(row) || row.dataset.entryType !== 'file' || !row.dataset.path) return;
|
|
133
|
+
event.preventDefault();
|
|
134
|
+
event.stopPropagation();
|
|
135
|
+
showFileContextMenu(event, row);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
document.addEventListener('pointerdown', function (event) {
|
|
139
|
+
var menu = document.getElementById('file-tree-context-menu');
|
|
140
|
+
if (menu && !menu.contains(event.target)) closeFileContextMenu();
|
|
141
|
+
});
|
|
142
|
+
document.addEventListener('keydown', function (event) {
|
|
143
|
+
if (event.key === 'Escape') closeFileContextMenu();
|
|
144
|
+
});
|
|
145
|
+
window.addEventListener('resize', closeFileContextMenu);
|
|
146
|
+
window.addEventListener('blur', closeFileContextMenu);
|
|
147
|
+
treeEl.addEventListener('scroll', closeFileContextMenu, { passive: true });
|
|
148
|
+
}
|
|
@@ -10,6 +10,7 @@ import { animateMarkdownChange, beginMarkdownPresentation, cancelMarkdownFollow,
|
|
|
10
10
|
import { store } from './store.js';
|
|
11
11
|
import { enterMarkdownSlides, exitMarkdownSlides, handleMarkdownSlideKey, syncMarkdownSlidesButton, toggleMarkdownSlideLevelMenu } from './markdown-slides.js';
|
|
12
12
|
import { initFileViewerTabs, openFileViewerTab, previewFileViewerTab, updateFileViewerTab, closeFileViewerTab, focusedFileViewerTab, clearFileViewerTabs } from './filebrowser-tabs.js';
|
|
13
|
+
import { initFileBrowserContextMenu, downloadProjectFile } from './filebrowser-context-menu.js';
|
|
13
14
|
|
|
14
15
|
var ctx;
|
|
15
16
|
var showDropHint = function () {};
|
|
@@ -29,6 +30,7 @@ var gitDiffCache = {}; // hash -> diff text
|
|
|
29
30
|
var pendingGitDiff = null; // callback for pending git diff
|
|
30
31
|
var fileAtCache = {}; // hash -> file content
|
|
31
32
|
var pendingFileAt = null; // callback for pending file-at
|
|
33
|
+
var FILE_RICH_PREVIEW_MAX_BYTES = 1024 * 1024;
|
|
32
34
|
|
|
33
35
|
export function initFileBrowser(_ctx) {
|
|
34
36
|
ctx = _ctx;
|
|
@@ -55,6 +57,7 @@ export function initFileBrowser(_ctx) {
|
|
|
55
57
|
// collapses/expands folders and ascends/descends into them. The tree
|
|
56
58
|
// gets a tabindex so it can receive focus and keydown events.
|
|
57
59
|
if (ctx.fileTreeEl) {
|
|
60
|
+
initFileBrowserContextMenu(ctx.fileTreeEl);
|
|
58
61
|
ctx.fileTreeEl.setAttribute('tabindex', '0');
|
|
59
62
|
ctx.fileTreeEl.addEventListener('keydown', handleTreeKeyDown);
|
|
60
63
|
// When the user clicks any tree row, promote it to keyboard focus
|
|
@@ -180,6 +183,11 @@ export function initFileBrowser(_ctx) {
|
|
|
180
183
|
});
|
|
181
184
|
});
|
|
182
185
|
|
|
186
|
+
document.getElementById("file-viewer-download").addEventListener("click", function () {
|
|
187
|
+
if (!currentFilePath) return;
|
|
188
|
+
downloadProjectFile(currentFilePath);
|
|
189
|
+
});
|
|
190
|
+
|
|
183
191
|
// Markdown render toggle
|
|
184
192
|
document.getElementById("file-viewer-render").addEventListener("click", function () {
|
|
185
193
|
if (!currentContent || (!currentIsMarkdown && !currentIsSvg)) return;
|
|
@@ -754,6 +762,7 @@ function renderFilteredTree(container, tree, depth, query) {
|
|
|
754
762
|
|
|
755
763
|
var row = document.createElement("div");
|
|
756
764
|
row.className = "file-tree-item" + (isDir ? " expanded" : "");
|
|
765
|
+
row.dataset.entryType = isDir ? "dir" : "file";
|
|
757
766
|
row.style.paddingLeft = (8 + depth * 16) + "px";
|
|
758
767
|
if (entry) {
|
|
759
768
|
row.draggable = true;
|
|
@@ -972,6 +981,7 @@ function renderEntries(container, entries, depth) {
|
|
|
972
981
|
var entry = sorted[i];
|
|
973
982
|
var row = document.createElement("div");
|
|
974
983
|
row.className = "file-tree-item";
|
|
984
|
+
row.dataset.entryType = entry.type;
|
|
975
985
|
row.style.paddingLeft = (8 + depth * 16) + "px";
|
|
976
986
|
|
|
977
987
|
row.draggable = true;
|
|
@@ -1079,6 +1089,7 @@ function showFileContent(msg) {
|
|
|
1079
1089
|
var previousContent = currentContent;
|
|
1080
1090
|
var previousPath = currentFilePath;
|
|
1081
1091
|
var previousWasMarkdown = currentIsMarkdown;
|
|
1092
|
+
var lightweightPreview = false;
|
|
1082
1093
|
var refreshSlideIndex = pendingRefresh && store.get('markdownSlidesActive')
|
|
1083
1094
|
? store.get('markdownSlideIndex') || 0
|
|
1084
1095
|
: null;
|
|
@@ -1116,8 +1127,10 @@ function showFileContent(msg) {
|
|
|
1116
1127
|
} else {
|
|
1117
1128
|
currentContent = msg.content;
|
|
1118
1129
|
var ext = requestedExt;
|
|
1119
|
-
|
|
1120
|
-
|
|
1130
|
+
lightweightPreview = (msg.size || 0) > FILE_RICH_PREVIEW_MAX_BYTES;
|
|
1131
|
+
currentIsMarkdown = !lightweightPreview && (ext === "md" || ext === "mdx");
|
|
1132
|
+
currentIsSvg = !lightweightPreview && ext === "svg";
|
|
1133
|
+
if (lightweightPreview) isRendered = false;
|
|
1121
1134
|
if (pendingRenderedOpen && currentIsMarkdown) isRendered = true;
|
|
1122
1135
|
|
|
1123
1136
|
if (currentIsMarkdown || currentIsSvg) {
|
|
@@ -1129,7 +1142,9 @@ function showFileContent(msg) {
|
|
|
1129
1142
|
}
|
|
1130
1143
|
|
|
1131
1144
|
// Markdown starts as source; SVG starts as a safe image preview.
|
|
1132
|
-
if (
|
|
1145
|
+
if (lightweightPreview) {
|
|
1146
|
+
renderLargeTextFile(bodyEl, msg.content, msg.size);
|
|
1147
|
+
} else if (currentIsMarkdown) {
|
|
1133
1148
|
var transitionFrom = keepRenderState && prevRendered && previousWasMarkdown &&
|
|
1134
1149
|
isFollowingMarkdown(msg.path) && pathsReferToSameFile(previousPath, msg.path)
|
|
1135
1150
|
? (previousContent == null ? "" : previousContent)
|
|
@@ -1149,7 +1164,9 @@ function showFileContent(msg) {
|
|
|
1149
1164
|
refreshIcons();
|
|
1150
1165
|
|
|
1151
1166
|
// If opened with a diff request, show full-file split diff in wide mode
|
|
1152
|
-
if (pendingOpenMode &&
|
|
1167
|
+
if (pendingOpenMode && lightweightPreview) {
|
|
1168
|
+
pendingOpenMode = null;
|
|
1169
|
+
} else if (pendingOpenMode && pendingOpenMode.type === "diff" && currentContent != null) {
|
|
1153
1170
|
var diffOpts = pendingOpenMode;
|
|
1154
1171
|
pendingOpenMode = null;
|
|
1155
1172
|
historyVisible = false;
|
|
@@ -1444,6 +1461,26 @@ function renderCodeWithLineNumbers(bodyEl, content, ext) {
|
|
|
1444
1461
|
}
|
|
1445
1462
|
}
|
|
1446
1463
|
|
|
1464
|
+
function renderLargeTextFile(bodyEl, content, size) {
|
|
1465
|
+
var viewer = document.createElement("div");
|
|
1466
|
+
viewer.className = "file-viewer-large-text";
|
|
1467
|
+
|
|
1468
|
+
var notice = document.createElement("div");
|
|
1469
|
+
notice.className = "file-viewer-large-notice";
|
|
1470
|
+
notice.innerHTML = iconHtml("file-text") +
|
|
1471
|
+
"<span>Large file (" + formatSize(size || 0) + ") — showing plain text for performance</span>";
|
|
1472
|
+
|
|
1473
|
+
var codeWrap = document.createElement("pre");
|
|
1474
|
+
var codeEl = document.createElement("code");
|
|
1475
|
+
codeEl.textContent = content;
|
|
1476
|
+
codeWrap.appendChild(codeEl);
|
|
1477
|
+
|
|
1478
|
+
viewer.appendChild(notice);
|
|
1479
|
+
viewer.appendChild(codeWrap);
|
|
1480
|
+
bodyEl.innerHTML = "";
|
|
1481
|
+
bodyEl.appendChild(viewer);
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1447
1484
|
function formatSize(bytes) {
|
|
1448
1485
|
if (bytes < 1024) return bytes + " B";
|
|
1449
1486
|
if (bytes < 1048576) return (bytes / 1024).toFixed(1) + " KB";
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// Inline presentation for images produced by Codex ImageGen.
|
|
2
|
+
|
|
3
|
+
import { refreshIcons, iconHtml } from './icons.js';
|
|
4
|
+
import { showImageModal } from './app-misc.js';
|
|
5
|
+
import { addToMessages, scrollToBottom } from './app-rendering.js';
|
|
6
|
+
|
|
7
|
+
function fileNameFromImage(image) {
|
|
8
|
+
if (image.fileName) return image.fileName;
|
|
9
|
+
try {
|
|
10
|
+
var url = new URL(image.url, window.location.href);
|
|
11
|
+
return decodeURIComponent(url.pathname.split('/').pop()) || 'generated-image.png';
|
|
12
|
+
} catch (e) {
|
|
13
|
+
return 'generated-image.png';
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function actionButton(icon, label) {
|
|
18
|
+
var button = document.createElement('button');
|
|
19
|
+
button.type = 'button';
|
|
20
|
+
button.className = 'generated-image-action';
|
|
21
|
+
button.title = label;
|
|
22
|
+
button.setAttribute('aria-label', label);
|
|
23
|
+
button.innerHTML = iconHtml(icon) + '<span>' + label + '</span>';
|
|
24
|
+
return button;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function findProgressRow(toolId) {
|
|
28
|
+
var rows = document.querySelectorAll('.generated-image-row[data-image-tool-id]');
|
|
29
|
+
for (var i = 0; i < rows.length; i++) {
|
|
30
|
+
if (rows[i].dataset.imageToolId === String(toolId || '')) return rows[i];
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function renderImageGenerationProgress(msg) {
|
|
36
|
+
if (!msg.id || findProgressRow(msg.id)) return;
|
|
37
|
+
|
|
38
|
+
var row = document.createElement('div');
|
|
39
|
+
row.className = 'generated-image-row generated-image-row--pending';
|
|
40
|
+
row.dataset.imageToolId = msg.id;
|
|
41
|
+
|
|
42
|
+
var status = document.createElement('div');
|
|
43
|
+
status.className = 'generated-image-status';
|
|
44
|
+
status.setAttribute('role', 'status');
|
|
45
|
+
status.setAttribute('aria-live', 'polite');
|
|
46
|
+
status.innerHTML = iconHtml('sparkles') + '<span>Creating image</span><span class="generated-image-status-dots" aria-hidden="true"></span>';
|
|
47
|
+
row.appendChild(status);
|
|
48
|
+
|
|
49
|
+
var card = document.createElement('div');
|
|
50
|
+
card.className = 'generated-image-card generated-image-card--pending';
|
|
51
|
+
card.setAttribute('aria-hidden', 'true');
|
|
52
|
+
var field = document.createElement('div');
|
|
53
|
+
field.className = 'generated-image-particle-field';
|
|
54
|
+
card.appendChild(field);
|
|
55
|
+
row.appendChild(card);
|
|
56
|
+
|
|
57
|
+
addToMessages(row);
|
|
58
|
+
refreshIcons(row);
|
|
59
|
+
scrollToBottom();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function clearImageGenerationProgress(toolId) {
|
|
63
|
+
var row = findProgressRow(toolId);
|
|
64
|
+
if (row && row.classList.contains('generated-image-row--pending')) row.remove();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function clearAllImageGenerationProgress() {
|
|
68
|
+
var rows = document.querySelectorAll('.generated-image-row--pending');
|
|
69
|
+
for (var i = 0; i < rows.length; i++) rows[i].remove();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function renderGeneratedImage(msg) {
|
|
73
|
+
var image = msg.images && msg.images[0];
|
|
74
|
+
if (!image || !image.url) return;
|
|
75
|
+
|
|
76
|
+
var row = document.createElement('div');
|
|
77
|
+
row.className = 'generated-image-row';
|
|
78
|
+
row.dataset.imageToolId = msg.id || '';
|
|
79
|
+
var card = document.createElement('figure');
|
|
80
|
+
card.className = 'generated-image-card';
|
|
81
|
+
card.dataset.toolId = msg.id || '';
|
|
82
|
+
|
|
83
|
+
var imageWrap = document.createElement('div');
|
|
84
|
+
imageWrap.className = 'generated-image-preview';
|
|
85
|
+
var img = document.createElement('img');
|
|
86
|
+
img.src = image.url;
|
|
87
|
+
img.alt = msg.prompt ? 'Generated image: ' + msg.prompt : 'Generated image';
|
|
88
|
+
img.loading = 'lazy';
|
|
89
|
+
img.addEventListener('click', function () { showImageModal(image.url); });
|
|
90
|
+
imageWrap.appendChild(img);
|
|
91
|
+
card.appendChild(imageWrap);
|
|
92
|
+
|
|
93
|
+
var footer = document.createElement('figcaption');
|
|
94
|
+
footer.className = 'generated-image-footer';
|
|
95
|
+
var meta = document.createElement('div');
|
|
96
|
+
meta.className = 'generated-image-meta';
|
|
97
|
+
var label = document.createElement('span');
|
|
98
|
+
label.className = 'generated-image-label';
|
|
99
|
+
label.innerHTML = iconHtml('sparkles') + '<span>Generated image</span>';
|
|
100
|
+
meta.appendChild(label);
|
|
101
|
+
if (msg.prompt) {
|
|
102
|
+
var prompt = document.createElement('span');
|
|
103
|
+
prompt.className = 'generated-image-prompt';
|
|
104
|
+
prompt.textContent = msg.prompt;
|
|
105
|
+
prompt.title = msg.prompt;
|
|
106
|
+
meta.appendChild(prompt);
|
|
107
|
+
}
|
|
108
|
+
footer.appendChild(meta);
|
|
109
|
+
|
|
110
|
+
var actions = document.createElement('div');
|
|
111
|
+
actions.className = 'generated-image-actions';
|
|
112
|
+
var openButton = actionButton('maximize-2', 'Open');
|
|
113
|
+
openButton.addEventListener('click', function () { showImageModal(image.url); });
|
|
114
|
+
actions.appendChild(openButton);
|
|
115
|
+
var downloadLink = document.createElement('a');
|
|
116
|
+
downloadLink.className = 'generated-image-action';
|
|
117
|
+
downloadLink.href = image.url;
|
|
118
|
+
downloadLink.download = fileNameFromImage(image);
|
|
119
|
+
downloadLink.title = 'Download';
|
|
120
|
+
downloadLink.setAttribute('aria-label', 'Download');
|
|
121
|
+
downloadLink.innerHTML = iconHtml('download') + '<span>Download</span>';
|
|
122
|
+
actions.appendChild(downloadLink);
|
|
123
|
+
footer.appendChild(actions);
|
|
124
|
+
card.appendChild(footer);
|
|
125
|
+
row.appendChild(card);
|
|
126
|
+
|
|
127
|
+
var progressRow = findProgressRow(msg.id);
|
|
128
|
+
if (progressRow) progressRow.replaceWith(row);
|
|
129
|
+
else addToMessages(row);
|
|
130
|
+
refreshIcons(row);
|
|
131
|
+
scrollToBottom();
|
|
132
|
+
}
|
|
@@ -78,23 +78,15 @@ export var builtinCommands = [
|
|
|
78
78
|
|
|
79
79
|
function commitVendorForTurn() {
|
|
80
80
|
var committedVendor = store.get('currentVendor');
|
|
81
|
-
var vendorToggle = document.getElementById("vendor-toggle-wrap");
|
|
82
81
|
var activeIndicator = document.getElementById("active-vendor-indicator");
|
|
83
82
|
var activeIcon = document.getElementById("active-vendor-icon");
|
|
84
83
|
if (committedVendor) {
|
|
85
|
-
if (vendorToggle) {
|
|
86
|
-
vendorToggle.classList.add("hidden");
|
|
87
|
-
vendorToggle.classList.remove("locked");
|
|
88
|
-
}
|
|
89
84
|
if (activeIndicator && activeIcon) {
|
|
90
85
|
activeIcon.src = VENDOR_AVATARS[committedVendor] || VENDOR_AVATARS.claude;
|
|
91
86
|
activeIcon.alt = VENDOR_NAMES[committedVendor] || VENDOR_NAMES.claude;
|
|
92
87
|
activeIndicator.title = (VENDOR_NAMES[committedVendor] || VENDOR_NAMES.claude) + " session";
|
|
93
88
|
activeIndicator.classList.remove("hidden");
|
|
94
89
|
}
|
|
95
|
-
} else if (vendorToggle) {
|
|
96
|
-
vendorToggle.classList.remove("hidden");
|
|
97
|
-
vendorToggle.classList.add("locked");
|
|
98
90
|
}
|
|
99
91
|
store.set({ vendorSelectionLocked: false, sessionHasHistory: true });
|
|
100
92
|
}
|
|
@@ -339,8 +331,7 @@ export function sendMessage() {
|
|
|
339
331
|
|
|
340
332
|
// First message commits the vendor and bumps the session into
|
|
341
333
|
// has-history state. The server won't re-fire session_switched after a
|
|
342
|
-
// message, so
|
|
343
|
-
// hide the picker, show the small avatar next to the config chip.
|
|
334
|
+
// message, so show the small vendor avatar next to the config chip.
|
|
344
335
|
// Bumping sessionHasHistory drives in-session lock states (e.g. the
|
|
345
336
|
// Codex model picker that becomes informational once the thread is
|
|
346
337
|
// bound to a model).
|
|
@@ -437,7 +428,7 @@ function extractFilePaths(cd) {
|
|
|
437
428
|
}
|
|
438
429
|
|
|
439
430
|
// --- Insert text at cursor in textarea ---
|
|
440
|
-
function insertTextAtCursor(text) {
|
|
431
|
+
export function insertTextAtCursor(text) {
|
|
441
432
|
var el = ctx.inputEl;
|
|
442
433
|
el.focus();
|
|
443
434
|
var start = el.selectionStart;
|
|
@@ -23,9 +23,8 @@ import { attachTuiGrab, detachTuiGrab } from './tui-grab.js';
|
|
|
23
23
|
import { createKeyToolbar, TERMINAL_TOOLBAR_HTML } from './terminal-toolbar.js';
|
|
24
24
|
import { refreshIcons, iconHtml } from './icons.js';
|
|
25
25
|
|
|
26
|
-
// Claude TUI
|
|
27
|
-
//
|
|
28
|
-
// setTuiSessionTheme() below, which theme.js calls from applyTheme.
|
|
26
|
+
// Claude TUI sessions always use Clay Studio Dark via getTerminalTheme().
|
|
27
|
+
// Theme switches reapply that fixed palette through setTuiSessionTheme().
|
|
29
28
|
|
|
30
29
|
var hostEl = null; // container div mounted over #messages
|
|
31
30
|
var xtermContainerEl = null;
|
|
@@ -262,7 +262,7 @@ export function getComputedVar(varName) {
|
|
|
262
262
|
}
|
|
263
263
|
|
|
264
264
|
export function getTerminalTheme() {
|
|
265
|
-
return computeTerminalTheme(
|
|
265
|
+
return computeTerminalTheme(getTheme(DEFAULT_DARK_THEME_ID) || defaultDarkFallback);
|
|
266
266
|
}
|
|
267
267
|
|
|
268
268
|
export function getMermaidThemeVars() {
|
|
@@ -305,7 +305,9 @@ export function applyTheme(themeId, fromPicker) {
|
|
|
305
305
|
|
|
306
306
|
try { updateMascotSvgs(vars, isLight); } catch (e) {}
|
|
307
307
|
|
|
308
|
-
|
|
308
|
+
// Terminals deliberately keep Clay Studio Dark for predictable ANSI
|
|
309
|
+
// contrast, even while the surrounding workspace uses a light theme.
|
|
310
|
+
var termTheme = getTerminalTheme();
|
|
309
311
|
try { setTerminalTheme(termTheme); } catch (e) {}
|
|
310
312
|
try { setTuiSessionTheme(termTheme); } catch (e) {}
|
|
311
313
|
try { setTuiAttentionTheme(termTheme); } catch (e) {}
|