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.
@@ -16,9 +16,9 @@
16
16
  import { getTerminalTheme } from './theme.js';
17
17
  import { getTerminalFontFamily, getTerminalFontSize, onTerminalFontChange } from './terminal-prefs.js';
18
18
 
19
- // TUI attention modal xterm follows Clay's active theme via
20
- // getTerminalTheme() and live-updates through setTuiAttentionTheme()
21
- // which theme.js calls from applyTheme.
19
+ // The TUI attention modal always uses Clay Studio Dark via
20
+ // getTerminalTheme(). Theme switches reapply that fixed palette through
21
+ // setTuiAttentionTheme(), which theme.js calls from applyTheme().
22
22
 
23
23
  var modalEl = null;
24
24
  var modalXterm = null;
@@ -266,9 +266,8 @@ export function tuiModalHandleTermResized() { return false; }
266
266
  export function tuiModalHandleTermExited() { return false; }
267
267
  export function tuiModalHandleTermClosed() { return false; }
268
268
 
269
- // Live theme update for the attention modal. Called by theme.js when
270
- // the user switches themes. Also retints the surrounding modal chrome
271
- // so the frame doesn't stay black when a light theme is active.
269
+ // Theme update hook for the attention modal. The supplied palette remains
270
+ // Clay Studio Dark even when the surrounding workspace switches themes.
272
271
  export function setTuiAttentionTheme(xtermTheme) {
273
272
  if (modalXterm) {
274
273
  try { modalXterm.options.theme = xtermTheme; } catch (e) {}
@@ -276,8 +275,6 @@ export function setTuiAttentionTheme(xtermTheme) {
276
275
  if (modalEl && xtermTheme && xtermTheme.background) {
277
276
  var bodyEl = modalEl.querySelector(".tui-modal-body");
278
277
  if (bodyEl) bodyEl.style.background = xtermTheme.background;
279
- var frameEl = modalEl.querySelector(".tui-modal");
280
- if (frameEl) frameEl.style.background = xtermTheme.background;
281
278
  }
282
279
  }
283
280
 
@@ -8,9 +8,10 @@
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
- @import url("css/filebrowser.css?v=20260828-workbench4");
14
+ @import url("css/filebrowser.css?v=20260829-terminal-canvas1");
14
15
  @import url("css/git-panel.css");
15
16
  @import url("css/diff.css");
16
17
  @import url("css/highlight.css");
@@ -35,5 +36,5 @@
35
36
  @import url("css/mention.css");
36
37
  @import url("css/debate.css");
37
38
  @import url("css/notifications-center.css");
38
- @import url("css/tui-attention.css");
39
+ @import url("css/tui-attention.css?v=20260829-terminal-canvas1");
39
40
  @import url("css/pwa-mobile.css?v=20260824f");
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",
3
+ "version": "3.7.0-beta.2",
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",