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.
@@ -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
- // Vendor toggle visibility + active-vendor indicator next to the
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
- currentIsMarkdown = (ext === "md" || ext === "mdx");
1120
- currentIsSvg = ext === "svg";
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 (currentIsMarkdown) {
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 && pendingOpenMode.type === "diff" && currentContent != null) {
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 mirror the vendor-toggle/active-indicator swap locally:
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 session terminal colors follow Clay's active theme via
27
- // getTerminalTheme(). Live theme switches are wired through
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(getCurrentTheme());
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
- var termTheme = computeTerminalTheme(theme);
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) {}