clay-server 3.6.0-beta.2 → 3.7.0-beta.1

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.
@@ -34,7 +34,7 @@ function attachImage(ctx) {
34
34
  return hydrated;
35
35
  }
36
36
  if (!entry.imageRefs) return entry;
37
- if (entry.type !== "user_message" && entry.type !== "mention_user" && entry.type !== "user_mention") return entry;
37
+ if (entry.type !== "user_message" && entry.type !== "mention_user" && entry.type !== "user_mention" && entry.type !== "generated_image") return entry;
38
38
  var images = [];
39
39
  for (var ri = 0; ri < entry.imageRefs.length; ri++) {
40
40
  var ref = entry.imageRefs[ri];
package/lib/project.js CHANGED
@@ -882,6 +882,7 @@ function createProjectContext(opts) {
882
882
  scheduleMessage: function (session, text, resetsAt) {
883
883
  scheduleMessage(session, text, resetsAt);
884
884
  },
885
+ saveImageFile: saveImageFile,
885
886
  getAutoContinueSetting: function (session) {
886
887
  // Per-user setting in multi-user mode
887
888
  if (usersModule.isMultiUser() && session && session.ownerId) {
@@ -0,0 +1,225 @@
1
+ .generated-image-row {
2
+ width: 100%;
3
+ max-width: var(--content-width);
4
+ margin: 4px auto 16px;
5
+ padding: 0 20px;
6
+ }
7
+
8
+ .generated-image-card {
9
+ width: min(560px, 100%);
10
+ overflow: hidden;
11
+ border: 1px solid var(--border);
12
+ border-radius: 14px;
13
+ background: var(--bg-alt);
14
+ box-shadow: 0 8px 28px rgba(var(--shadow-rgb), 0.16);
15
+ }
16
+
17
+ .generated-image-status {
18
+ display: flex;
19
+ align-items: center;
20
+ gap: 7px;
21
+ width: min(560px, 100%);
22
+ min-height: 30px;
23
+ padding: 0 2px 7px;
24
+ color: var(--text-muted);
25
+ font-size: 12px;
26
+ font-weight: 500;
27
+ }
28
+
29
+ .generated-image-status .lucide {
30
+ width: 14px;
31
+ height: 14px;
32
+ color: var(--accent);
33
+ animation: generated-image-sparkle 2.4s ease-in-out infinite;
34
+ }
35
+
36
+ .generated-image-status-dots::after {
37
+ content: "";
38
+ animation: generated-image-ellipsis 1.4s steps(4, end) infinite;
39
+ }
40
+
41
+ .generated-image-card--pending {
42
+ position: relative;
43
+ width: min(560px, 100%);
44
+ aspect-ratio: 1 / 1;
45
+ border-color: var(--border-subtle);
46
+ background: var(--bg-alt);
47
+ box-shadow: none;
48
+ }
49
+
50
+ .generated-image-particle-field {
51
+ position: absolute;
52
+ inset: 0;
53
+ overflow: hidden;
54
+ }
55
+
56
+ .generated-image-particle-field::before {
57
+ position: absolute;
58
+ inset: 11%;
59
+ content: "";
60
+ background-image: radial-gradient(circle, var(--text-dimmer) 1.4px, transparent 1.7px);
61
+ background-position: center;
62
+ background-size: 25px 25px;
63
+ opacity: 0.52;
64
+ -webkit-mask-image: radial-gradient(circle, #000 0%, rgba(0, 0, 0, 0.88) 38%, transparent 72%);
65
+ mask-image: radial-gradient(circle, #000 0%, rgba(0, 0, 0, 0.88) 38%, transparent 72%);
66
+ animation: generated-image-field 3.2s ease-in-out infinite;
67
+ }
68
+
69
+ .generated-image-particle-field::after {
70
+ position: absolute;
71
+ top: 50%;
72
+ left: 50%;
73
+ width: 36%;
74
+ aspect-ratio: 1 / 1;
75
+ border: 1px solid var(--accent-15);
76
+ border-radius: 50%;
77
+ content: "";
78
+ background: radial-gradient(circle, var(--accent-12), transparent 68%);
79
+ transform: translate(-50%, -50%);
80
+ animation: generated-image-pulse 3.2s ease-in-out infinite;
81
+ }
82
+
83
+ @keyframes generated-image-field {
84
+ 0%, 100% { opacity: 0.28; transform: scale(0.92) rotate(-2deg); }
85
+ 50% { opacity: 0.68; transform: scale(1.04) rotate(2deg); }
86
+ }
87
+
88
+ @keyframes generated-image-pulse {
89
+ 0%, 100% { opacity: 0.2; transform: translate(-50%, -50%) scale(0.72); }
90
+ 50% { opacity: 0.72; transform: translate(-50%, -50%) scale(1.08); }
91
+ }
92
+
93
+ @keyframes generated-image-sparkle {
94
+ 0%, 100% { opacity: 0.5; transform: scale(0.86) rotate(-8deg); }
95
+ 50% { opacity: 1; transform: scale(1.08) rotate(8deg); }
96
+ }
97
+
98
+ @keyframes generated-image-ellipsis {
99
+ 0% { content: ""; }
100
+ 25% { content: "."; }
101
+ 50% { content: ".."; }
102
+ 75%, 100% { content: "..."; }
103
+ }
104
+
105
+ @media (prefers-reduced-motion: reduce) {
106
+ .generated-image-status .lucide,
107
+ .generated-image-status-dots::after,
108
+ .generated-image-particle-field::before,
109
+ .generated-image-particle-field::after {
110
+ animation: none;
111
+ }
112
+ }
113
+
114
+ .generated-image-preview {
115
+ display: grid;
116
+ place-items: center;
117
+ min-height: 180px;
118
+ max-height: 680px;
119
+ overflow: hidden;
120
+ background:
121
+ linear-gradient(45deg, rgba(var(--overlay-rgb), 0.025) 25%, transparent 25%),
122
+ linear-gradient(-45deg, rgba(var(--overlay-rgb), 0.025) 25%, transparent 25%),
123
+ linear-gradient(45deg, transparent 75%, rgba(var(--overlay-rgb), 0.025) 75%),
124
+ linear-gradient(-45deg, transparent 75%, rgba(var(--overlay-rgb), 0.025) 75%);
125
+ background-position: 0 0, 0 8px, 8px -8px, -8px 0;
126
+ background-size: 16px 16px;
127
+ }
128
+
129
+ .generated-image-preview img {
130
+ display: block;
131
+ width: 100%;
132
+ max-height: 680px;
133
+ object-fit: contain;
134
+ cursor: zoom-in;
135
+ }
136
+
137
+ .generated-image-footer {
138
+ display: flex;
139
+ align-items: center;
140
+ justify-content: space-between;
141
+ gap: 14px;
142
+ min-height: 52px;
143
+ padding: 9px 10px 9px 14px;
144
+ border-top: 1px solid var(--border-subtle);
145
+ }
146
+
147
+ .generated-image-meta {
148
+ display: flex;
149
+ flex: 1;
150
+ min-width: 0;
151
+ flex-direction: column;
152
+ gap: 2px;
153
+ }
154
+
155
+ .generated-image-label {
156
+ display: flex;
157
+ align-items: center;
158
+ gap: 6px;
159
+ color: var(--text-secondary);
160
+ font-size: 12px;
161
+ font-weight: 600;
162
+ }
163
+
164
+ .generated-image-label .lucide {
165
+ width: 13px;
166
+ height: 13px;
167
+ color: var(--accent);
168
+ }
169
+
170
+ .generated-image-prompt {
171
+ overflow: hidden;
172
+ color: var(--text-muted);
173
+ font-size: 11px;
174
+ line-height: 1.35;
175
+ text-overflow: ellipsis;
176
+ white-space: nowrap;
177
+ }
178
+
179
+ .generated-image-actions {
180
+ display: flex;
181
+ flex-shrink: 0;
182
+ gap: 4px;
183
+ }
184
+
185
+ .generated-image-action {
186
+ display: inline-flex;
187
+ align-items: center;
188
+ gap: 5px;
189
+ height: 32px;
190
+ padding: 0 9px;
191
+ border: 1px solid transparent;
192
+ border-radius: 8px;
193
+ background: transparent;
194
+ color: var(--text-muted);
195
+ font: inherit;
196
+ font-size: 11px;
197
+ text-decoration: none;
198
+ cursor: pointer;
199
+ }
200
+
201
+ .generated-image-action:hover {
202
+ border-color: var(--border);
203
+ background: rgba(var(--overlay-rgb), 0.04);
204
+ color: var(--text);
205
+ }
206
+
207
+ .generated-image-action .lucide {
208
+ width: 14px;
209
+ height: 14px;
210
+ }
211
+
212
+ @media (max-width: 560px) {
213
+ .generated-image-row { padding: 0 12px; }
214
+ .generated-image-card { border-radius: 12px; }
215
+
216
+ .generated-image-action span {
217
+ display: none;
218
+ }
219
+
220
+ .generated-image-action {
221
+ width: 32px;
222
+ justify-content: center;
223
+ padding: 0;
224
+ }
225
+ }
@@ -762,7 +762,7 @@
762
762
  color: var(--text-muted);
763
763
  }
764
764
 
765
- /* --- Bottom toolbar (attach left, model+send right) --- */
765
+ /* --- Bottom toolbar (attach left, session config + send right) --- */
766
766
  #input-bottom {
767
767
  display: flex;
768
768
  align-items: center;
@@ -777,21 +777,7 @@
777
777
  flex-shrink: 0;
778
778
  }
779
779
 
780
- /* --- Vendor toggle (split toggle) --- */
781
- #vendor-toggle-wrap {
782
- display: flex;
783
- align-items: center;
784
- border: 1px solid var(--border);
785
- border-radius: 8px;
786
- overflow: hidden;
787
- height: 30px;
788
- flex-shrink: 0;
789
- }
790
- #vendor-toggle-wrap.hidden { display: none; }
791
-
792
- /* Compact session-vendor icon shown next to the config-chip when the
793
- full vendor-toggle is hidden (because the session's vendor is already
794
- committed). Visual only - not clickable. */
780
+ /* Compact committed-session vendor icon next to the config chip. */
795
781
  #active-vendor-indicator {
796
782
  display: inline-flex;
797
783
  align-items: center;
@@ -806,73 +792,13 @@
806
792
  border-radius: 4px;
807
793
  object-fit: cover;
808
794
  }
809
- #vendor-toggle-wrap.locked {
810
- border: none;
811
- height: auto;
812
- pointer-events: none;
813
- }
814
- #vendor-toggle-wrap.locked .vendor-toggle-btn:not(.active) { display: none; }
815
- #vendor-toggle-wrap.locked .vendor-toggle-btn.active {
816
- background: transparent;
817
- padding: 0;
818
- gap: 0;
819
- opacity: 0.7;
820
- }
821
- #vendor-toggle-wrap.locked .vendor-toggle-btn.active .vendor-toggle-label { display: none; }
822
- #vendor-toggle-wrap.locked .vendor-toggle-btn:first-child { border-right: none; }
823
- .vendor-toggle-btn {
824
- display: flex;
825
- align-items: center;
826
- gap: 4px;
827
- padding: 0 10px;
828
- height: 100%;
829
- border: none;
830
- background: transparent;
831
- cursor: pointer;
832
- font-size: 12px;
833
- color: var(--text-secondary);
834
- transition: background 0.15s, color 0.15s;
835
- white-space: nowrap;
836
- }
837
- .vendor-toggle-btn:first-child { border-right: 1px solid var(--border); }
838
- .vendor-toggle-btn.active {
839
- background: var(--accent-12);
840
- color: var(--accent);
841
- font-weight: 600;
842
- }
843
- .vendor-toggle-btn.disabled {
844
- opacity: 0.35;
845
- cursor: default;
846
- }
847
- .vendor-toggle-btn.disabled:not(.active) { display: none; }
848
- .vendor-toggle-icon {
849
- width: 16px;
850
- height: 16px;
851
- border-radius: 4px;
852
- object-fit: cover;
853
- }
854
- .vendor-toggle-label {
855
- pointer-events: none;
856
- }
857
- .vendor-toggle-btn.experimental::after,
858
795
  .vendor-experimental-badge {
859
- content: "🧪";
860
796
  font-size: 10px;
861
797
  line-height: 1;
862
798
  font-weight: 400;
863
799
  filter: saturate(0.85);
864
- }
865
- .vendor-experimental-badge {
866
800
  margin-left: auto;
867
801
  }
868
- @media (min-width: 901px) {
869
- .vendor-toggle-btn:not(.active) .vendor-toggle-label { display: none; }
870
- .vendor-toggle-btn:not(.active) { padding: 0 8px; }
871
- }
872
- @media (max-width: 900px) {
873
- .vendor-toggle-label { display: none; }
874
- .vendor-toggle-btn { padding: 0 8px; }
875
- }
876
802
 
877
803
  /* --- Attach buttons & menu --- */
878
804
  #attach-wrap {
@@ -1290,7 +1216,6 @@
1290
1216
  Attach (files/images -> path injection) and voice input stay available. */
1291
1217
  body.tui-composer-active #schedule-btn,
1292
1218
  body.tui-composer-active #ask-mate-btn,
1293
- body.tui-composer-active #vendor-toggle-wrap,
1294
1219
  body.tui-composer-active #active-vendor-indicator,
1295
1220
  body.tui-composer-active #config-chip-wrap {
1296
1221
  display: none !important;
@@ -30,7 +30,7 @@
30
30
  (function(){try{var k="clay-theme-vars",v=localStorage.getItem(k),r=document.documentElement;if(v){var o=JSON.parse(v),p;for(p in o)r.style.setProperty(p,o[p]);var vt=localStorage.getItem(k.replace("-vars","-variant"));if(vt==="light"){r.classList.add("light-theme");r.classList.remove("dark-theme")}else{r.classList.add("dark-theme");r.classList.remove("light-theme")}var m=document.querySelector('meta[name="theme-color"]');if(m&&o["--bg"])m.setAttribute("content",o["--bg"])}else{var sl=window.matchMedia&&window.matchMedia("(prefers-color-scheme: light)").matches;if(sl){r.classList.add("light-theme");r.classList.remove("dark-theme")}}}catch(e){}})();
31
31
  </script>
32
32
  <script>if(window.navigator.standalone||window.matchMedia("(display-mode:standalone)").matches){document.documentElement.classList.add("pwa-standalone")}</script>
33
- <link rel="stylesheet" href="style.css?v=20260828-clay-theme1">
33
+ <link rel="stylesheet" href="style.css?v=20260829-imagegen3">
34
34
  <style>
35
35
  @media(max-width:768px){
36
36
  /* User messages: vertical stack, avatar on top, right-aligned */
@@ -524,48 +524,6 @@
524
524
  </div>
525
525
  </div>
526
526
  <div id="input-bottom-right">
527
- <div id="vendor-toggle-wrap">
528
- <button id="vendor-btn-claude" class="vendor-toggle-btn active" data-vendor="claude">
529
- <img src="/claude-code-avatar.png" class="vendor-toggle-icon" alt="Claude">
530
- <span class="vendor-toggle-label">Claude Code</span>
531
- </button>
532
- <button id="vendor-btn-codex" class="vendor-toggle-btn" data-vendor="codex">
533
- <img src="/codex-avatar.png" class="vendor-toggle-icon" alt="Codex">
534
- <span class="vendor-toggle-label">Codex</span>
535
- </button>
536
- <button id="vendor-btn-antigravity" class="vendor-toggle-btn" data-vendor="antigravity">
537
- <img src="/antigravity-avatar.png" class="vendor-toggle-icon" alt="Antigravity">
538
- <span class="vendor-toggle-label">Antigravity CLI</span>
539
- </button>
540
- <button id="vendor-btn-opencode" class="vendor-toggle-btn" data-vendor="opencode">
541
- <img src="/opencode-avatar.svg" class="vendor-toggle-icon" alt="OpenCode">
542
- <span class="vendor-toggle-label">OpenCode</span>
543
- </button>
544
- <button id="vendor-btn-kimi" class="vendor-toggle-btn" data-vendor="kimi">
545
- <img src="/kimi-avatar.svg" class="vendor-toggle-icon" alt="Kimi">
546
- <span class="vendor-toggle-label">Kimi Code</span>
547
- </button>
548
- <button id="vendor-btn-grok" class="vendor-toggle-btn" data-vendor="grok">
549
- <img src="/grok-avatar.svg" class="vendor-toggle-icon" alt="Grok">
550
- <span class="vendor-toggle-label">Grok Build</span>
551
- </button>
552
- <button id="vendor-btn-copilot" class="vendor-toggle-btn" data-vendor="copilot">
553
- <img src="/copilot-avatar.svg" class="vendor-toggle-icon" alt="GitHub Copilot">
554
- <span class="vendor-toggle-label">GitHub Copilot CLI</span>
555
- </button>
556
- <button id="vendor-btn-qwen" class="vendor-toggle-btn" data-vendor="qwen">
557
- <img src="/qwen-avatar.svg" class="vendor-toggle-icon" alt="Qwen">
558
- <span class="vendor-toggle-label">Qwen Code</span>
559
- </button>
560
- <button id="vendor-btn-junie" class="vendor-toggle-btn" data-vendor="junie">
561
- <img src="/junie-avatar.svg" class="vendor-toggle-icon" alt="Junie">
562
- <span class="vendor-toggle-label">Junie CLI</span>
563
- </button>
564
- <button id="vendor-btn-kiro" class="vendor-toggle-btn" data-vendor="kiro">
565
- <img src="/kiro-avatar.svg" class="vendor-toggle-icon" alt="Kiro">
566
- <span class="vendor-toggle-label">Kiro CLI</span>
567
- </button>
568
- </div>
569
527
  <div id="active-vendor-indicator" class="hidden" title="Session vendor">
570
528
  <img id="active-vendor-icon" alt="">
571
529
  </div>
@@ -2323,7 +2281,7 @@
2323
2281
  <script src="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0/lib/addon-fit.min.js"></script>
2324
2282
  <script src="https://cdn.jsdelivr.net/npm/@xterm/addon-web-links@0/lib/addon-web-links.min.js"></script>
2325
2283
  <script src="https://cdn.jsdelivr.net/npm/@xterm/addon-webgl@0/lib/addon-webgl.min.js"></script>
2326
- <script type="module" src="app.js?v=20260826a"></script>
2284
+ <script type="module" src="app.js?v=20260829-imagegen3"></script>
2327
2285
  <div id="pwa-install-modal" class="pwa-modal hidden">
2328
2286
  <div class="pwa-modal-backdrop"></div>
2329
2287
  <div class="pwa-modal-card">
@@ -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,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).
@@ -8,6 +8,7 @@
8
8
  @import url("css/overlays.css?v=20260827-brand3");
9
9
  @import url("css/menus.css");
10
10
  @import url("css/messages.css?v=20260828-delegated-muted1");
11
+ @import url("css/generated-images.css?v=20260829c");
11
12
  @import url("css/rewind.css");
12
13
  @import url("css/input.css?v=20260828-composer-depth1");
13
14
  @import url("css/filebrowser.css?v=20260828-workbench4");
package/lib/sdk-bridge.js CHANGED
@@ -392,6 +392,8 @@ function createSDKBridge(opts) {
392
392
  pushModule: pushModule,
393
393
  getNotificationsModule: getNotificationsModule,
394
394
  adapter: adapter,
395
+ saveImageFile: opts.saveImageFile,
396
+ getSessionLinuxUser: getSessionLinuxUser,
395
397
  onProcessingChanged: onProcessingChanged,
396
398
  onTurnDone: onTurnDone,
397
399
  onAutoTitle: function (session) { autoGenerateTitle(session); },
@@ -18,6 +18,8 @@ function attachMessageProcessor(ctx) {
18
18
  var opts = ctx.opts;
19
19
  var discoverSkillDirs = ctx.discoverSkillDirs;
20
20
  var mergeSkills = ctx.mergeSkills;
21
+ var saveImageFile = ctx.saveImageFile;
22
+ var getSessionLinuxUser = ctx.getSessionLinuxUser || function () { return null; };
21
23
 
22
24
  var AUTO_TITLE_TURN_THRESHOLD = 2;
23
25
 
@@ -29,8 +31,19 @@ function attachMessageProcessor(ctx) {
29
31
  return null;
30
32
  }
31
33
 
32
- function sendAndRecord(session, obj) {
33
- sm.sendAndRecord(session, obj);
34
+ function sendAndRecord(session, obj, clientObj) {
35
+ sm.sendAndRecord(session, obj, clientObj);
36
+ }
37
+
38
+ function parseGeneratedImage(parsed) {
39
+ var data = parsed.data || "";
40
+ var mediaType = parsed.mediaType || "image/png";
41
+ var dataUrlMatch = /^data:(image\/[a-z0-9.+-]+);base64,([\s\S]+)$/i.exec(data);
42
+ if (dataUrlMatch) {
43
+ mediaType = dataUrlMatch[1];
44
+ data = dataUrlMatch[2];
45
+ }
46
+ return { mediaType: mediaType, data: data.replace(/\s/g, "") };
34
47
  }
35
48
 
36
49
  function sendToSession(session, obj) {
@@ -276,6 +289,48 @@ function attachMessageProcessor(ctx) {
276
289
  is_error: !!parsed.isError,
277
290
  });
278
291
 
292
+ } else if (parsed.yokeType === "generated_image") {
293
+ var generated = parseGeneratedImage(parsed);
294
+ var fileName = null;
295
+ if (!parsed.isError && generated.data && typeof saveImageFile === "function") {
296
+ fileName = saveImageFile(generated.mediaType, generated.data, getSessionLinuxUser(session));
297
+ }
298
+ if (!fileName) {
299
+ sendAndRecord(session, {
300
+ type: "tool_result",
301
+ id: parsed.toolId,
302
+ content: parsed.isError ? "Image generation failed." : "The generated image could not be saved.",
303
+ is_error: true,
304
+ });
305
+ } else {
306
+ sendAndRecord(session, {
307
+ type: "tool_result",
308
+ id: parsed.toolId,
309
+ content: "Image generated",
310
+ is_error: false,
311
+ });
312
+ var imageRef = { mediaType: generated.mediaType, file: fileName };
313
+ var storedImage = {
314
+ type: "generated_image",
315
+ id: parsed.toolId,
316
+ prompt: parsed.prompt || "",
317
+ transparentBackground: parsed.transparentBackground,
318
+ imageRefs: [imageRef],
319
+ };
320
+ var liveImage = {
321
+ type: "generated_image",
322
+ id: parsed.toolId,
323
+ prompt: parsed.prompt || "",
324
+ transparentBackground: parsed.transparentBackground,
325
+ images: [{
326
+ mediaType: generated.mediaType,
327
+ url: "/p/" + slug + "/images/" + fileName,
328
+ fileName: fileName,
329
+ }],
330
+ };
331
+ sendAndRecord(session, storedImage, liveImage);
332
+ }
333
+
279
334
  } else if (parsed.yokeType === "plan_updated") {
280
335
  var todos = Array.isArray(parsed.plan) ? parsed.plan.map(function(step, idx) {
281
336
  var todo = {
package/lib/sessions.js CHANGED
@@ -874,9 +874,11 @@ function createSessionManager(opts) {
874
874
  }
875
875
  }
876
876
 
877
- function doSendAndRecord(session, obj) {
877
+ function doSendAndRecord(session, obj, clientObj) {
878
878
  // Stamp every recorded message so history replay preserves original times
879
879
  if (!obj._ts) obj._ts = Date.now();
880
+ var outgoingObj = clientObj || obj;
881
+ if (!outgoingObj._ts) outgoingObj._ts = obj._ts;
880
882
  session.history.push(obj);
881
883
  appendToSessionFile(session, obj);
882
884
  // Per-session out-of-band subscribers (used by home-chat to mirror
@@ -885,12 +887,12 @@ function createSessionManager(opts) {
885
887
  // clients; they are responsible for any transform + dispatch.
886
888
  if (session._subscribers && session._subscribers.size > 0) {
887
889
  for (var sub of session._subscribers) {
888
- try { sub(obj); } catch (e) { /* swallow — subscriber is optional */ }
890
+ try { sub(outgoingObj); } catch (e) { /* swallow — subscriber is optional */ }
889
891
  }
890
892
  }
891
893
  if (sendEach) {
892
894
  // Multi-user: send to clients whose active session matches this one
893
- var data = JSON.stringify(obj);
895
+ var data = JSON.stringify(outgoingObj);
894
896
  var ioData = null;
895
897
  sendEach(function (ws) {
896
898
  if (ws._clayActiveSession === session.localId) {
@@ -901,7 +903,7 @@ function createSessionManager(opts) {
901
903
  }
902
904
  // Track unread: increment on "done" for clients not viewing this session
903
905
  // Only count if session has no owner (my session) or owner matches this client
904
- if (obj.type === "done" && ws._clayActiveSession !== session.localId) {
906
+ if (outgoingObj.type === "done" && ws._clayActiveSession !== session.localId) {
905
907
  var _isMySession = !session.ownerId || (ws._clayUser && ws._clayUser.id === session.ownerId);
906
908
  if (_isMySession) {
907
909
  if (!ws._clayUnread) ws._clayUnread = {};
@@ -917,10 +919,10 @@ function createSessionManager(opts) {
917
919
  setTimeout(function () { session._ioThrottle = false; }, 80);
918
920
  }
919
921
  } else if (session.localId === activeSessionId) {
920
- send(obj);
922
+ send(outgoingObj);
921
923
  } else {
922
924
  // Track unread for single-user mode on "done"
923
- if (obj.type === "done") {
925
+ if (outgoingObj.type === "done") {
924
926
  singleUserUnread[session.localId] = (singleUserUnread[session.localId] || 0) + 1;
925
927
  send({ type: "session_unread", id: session.localId, count: singleUserUnread[session.localId] });
926
928
  }
@@ -31,6 +31,7 @@ var backgroundTasks = require("../codex-background-tasks");
31
31
  // agentMessage -> text response
32
32
  // reasoning -> thinking
33
33
  // commandExecution -> bash/shell
34
+ // imageGeneration -> ImageGen with a persisted inline image
34
35
  // fileChange -> file edits
35
36
  // mcpToolCall -> MCP tool
36
37
  // webSearch -> web search
@@ -417,6 +418,44 @@ function flattenEvent(notification, state) {
417
418
  return events;
418
419
  }
419
420
 
421
+ // Image generation. Codex emits the generated PNG as base64 in `result`;
422
+ // the message processor persists it before anything reaches the browser.
423
+ if (item.type === "imageGeneration" || item.type === "image_generation") {
424
+ if (!state.toolBlocks[item.id]) {
425
+ state.blockCounter++;
426
+ state.toolBlocks[item.id] = "blk_" + state.blockCounter;
427
+ var imageBlockId = state.toolBlocks[item.id];
428
+ events.push({
429
+ yokeType: "tool_start",
430
+ blockId: imageBlockId,
431
+ toolId: item.id,
432
+ toolName: "ImageGen",
433
+ });
434
+ events.push({
435
+ yokeType: "tool_executing",
436
+ blockId: imageBlockId,
437
+ toolId: item.id,
438
+ toolName: "ImageGen",
439
+ input: { prompt: item.revisedPrompt || "" },
440
+ });
441
+ }
442
+ if (evtPhase === "completed") {
443
+ events.push({
444
+ yokeType: "generated_image",
445
+ toolId: item.id,
446
+ blockId: state.toolBlocks[item.id],
447
+ data: item.result || "",
448
+ mediaType: item.mediaType || "image/png",
449
+ prompt: item.revisedPrompt || "",
450
+ savedPath: item.savedPath || null,
451
+ transparentBackground: item.transparentBackground == null ? null : !!item.transparentBackground,
452
+ isError: item.status === "failed",
453
+ status: item.status || "completed",
454
+ });
455
+ }
456
+ return events;
457
+ }
458
+
420
459
  // Command execution (bash/shell)
421
460
  if (item.type === "commandExecution" || item.type === "command_execution") {
422
461
  var commandText = item.command || state.commandInputs[item.id] || "";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "3.6.0-beta.2",
3
+ "version": "3.7.0-beta.1",
4
4
  "description": "Self-hosted team workspace for Claude Code and Codex. Multi-user, browser-based, with persistent AI mates.",
5
5
  "bin": {
6
6
  "clay-server": "./bin/cli.js",