clay-server 3.4.0-beta.21 → 3.4.0-beta.23

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.
@@ -960,7 +960,7 @@
960
960
  width: 50%;
961
961
  max-width: 720px;
962
962
  min-width: 360px;
963
- border-left: 1px solid var(--border);
963
+ border-right: 1px solid var(--border);
964
964
  background: var(--bg);
965
965
  display: flex;
966
966
  flex-direction: column;
@@ -1011,6 +1011,11 @@
1011
1011
  min-height: 44px;
1012
1012
  }
1013
1013
 
1014
+ .file-viewer-tabs { display: flex; min-width: 0; flex: 1; overflow-x: auto; gap: 2px; }
1015
+ .file-viewer-tab { display: inline-flex; align-items: center; gap: 6px; max-width: 170px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; border: 0; border-radius: 5px; padding: 4px 7px; background: transparent; color: var(--text-secondary); font: inherit; font-size: 12px; cursor: pointer; }
1016
+ .file-viewer-tab.active { background: var(--sidebar-hover); color: var(--text); }
1017
+ .file-viewer-tab-close { font-size: 16px; line-height: 12px; color: var(--text-muted); }
1018
+
1014
1019
  .file-viewer-path {
1015
1020
  flex: 1;
1016
1021
  font-family: "Roboto Mono", monospace;
@@ -23,6 +23,8 @@ body.pane-mode #sticky-notes-container {
23
23
  display: none !important;
24
24
  }
25
25
 
26
+ body.pane-mode #file-viewer { display: none !important; }
27
+
26
28
  body.pane-mode .notif-banner-container {
27
29
  display: none !important;
28
30
  }
@@ -680,6 +680,7 @@
680
680
 
681
681
  <div id="file-viewer" class="hidden">
682
682
  <div class="file-viewer-header">
683
+ <div class="file-viewer-tabs" id="file-viewer-tabs" role="tablist"></div>
683
684
  <span class="file-viewer-path" id="file-viewer-path"></span>
684
685
  <span class="file-viewer-live-status hidden" id="file-viewer-live-status" aria-live="polite">
685
686
  <span class="file-viewer-live-dot"></span>
@@ -28,6 +28,7 @@ import { startThinking, appendThinking, stopThinking, resetThinkingGroup, create
28
28
  import { showDoneNotification, playDoneSound, isNotifAlertEnabled, isNotifSoundEnabled } from './notifications.js';
29
29
  import { handleFsList, handleFsRead, handleFileChanged, handleDirChanged, handleFileHistory, handleGitDiff, handleFileAt, refreshIfOpen, getPendingNavigate, handleFsSearch, presentMarkdownEdit } from './filebrowser.js';
30
30
  import { beginMarkdownTurn, finishMarkdownTurn, markdownPathFromToolInput } from './markdown-live-edit.js';
31
+ import { forwardPaneMarkdownPresentation } from './pane-bridge.js';
31
32
  import { isProjectSettingsOpen, handleInstructionsRead, handleInstructionsWrite, handleProjectEnv, handleProjectEnvSaved, handleProjectSharedEnv, handleProjectSharedEnvSaved, handleProjectOwnerChanged } from './project-settings.js';
32
33
  import { updateSettingsModels, updateSettingsStats, updateDaemonConfig, handleSetPinResult, handleKeepAwakeChanged, handleInheritGroupsChanged, handleAutoContinueChanged, handleRestartResult, handleShutdownResult, handleSharedEnv, handleSharedEnvSaved, handleGlobalClaudeMdRead, handleGlobalClaudeMdWrite } from './server-settings.js';
33
34
  import { handleTermList, handleTermCreated, sendTerminalCommand, handleTermOutput, handleTermResized, handleTermExited, handleTermClosed } from './terminal.js';
@@ -1400,7 +1401,10 @@ export function processMessage(msg) {
1400
1401
  break;
1401
1402
 
1402
1403
  case "markdown_edit_present":
1403
- if (!store.get('replayingHistory') && !store.get('dmMode')) presentMarkdownEdit(msg);
1404
+ if (!store.get('replayingHistory') && !store.get('dmMode')) {
1405
+ if (store.get('paneMode')) forwardPaneMarkdownPresentation(msg);
1406
+ else if (!store.get('splitPanes')) presentMarkdownEdit(msg);
1407
+ }
1404
1408
  break;
1405
1409
 
1406
1410
  case "fs_dir_changed":
@@ -0,0 +1,65 @@
1
+ var tabs = new Map();
2
+ var focusedPath = null;
3
+ var onFocus = null;
4
+ var onEmpty = null;
5
+
6
+ function label(path) {
7
+ var parts = String(path || "").split("/");
8
+ return parts[parts.length - 1] || path;
9
+ }
10
+
11
+ function render() {
12
+ var root = document.getElementById("file-viewer-tabs");
13
+ if (!root) return;
14
+ root.innerHTML = "";
15
+ tabs.forEach(function(value, path) {
16
+ var tab = document.createElement("button");
17
+ tab.type = "button";
18
+ tab.className = "file-viewer-tab" + (path === focusedPath ? " active" : "");
19
+ tab.textContent = label(path);
20
+ tab.title = path;
21
+ tab.addEventListener("click", function() { focusFileViewerTab(path); });
22
+ var close = document.createElement("span");
23
+ close.className = "file-viewer-tab-close";
24
+ close.textContent = "×";
25
+ close.addEventListener("click", function(event) { event.stopPropagation(); closeFileViewerTab(path); });
26
+ tab.appendChild(close);
27
+ root.appendChild(tab);
28
+ });
29
+ }
30
+
31
+ export function initFileViewerTabs(options) {
32
+ onFocus = options && options.onFocus;
33
+ onEmpty = options && options.onEmpty;
34
+ }
35
+
36
+ export function openFileViewerTab(path, data) {
37
+ if (!path) return false;
38
+ var exists = tabs.has(path);
39
+ tabs.set(path, Object.assign(tabs.get(path) || {}, data || {}));
40
+ focusedPath = path;
41
+ render();
42
+ return !exists;
43
+ }
44
+
45
+ export function focusFileViewerTab(path) {
46
+ if (!tabs.has(path)) return false;
47
+ focusedPath = path;
48
+ render();
49
+ if (onFocus) onFocus(path, tabs.get(path));
50
+ return true;
51
+ }
52
+
53
+ export function closeFileViewerTab(path) {
54
+ if (!tabs.has(path)) return;
55
+ var paths = Array.from(tabs.keys());
56
+ var index = paths.indexOf(path);
57
+ tabs.delete(path);
58
+ if (focusedPath === path) focusedPath = paths[index + 1] || paths[index - 1] || null;
59
+ render();
60
+ if (focusedPath && onFocus) onFocus(focusedPath, tabs.get(focusedPath));
61
+ if (!focusedPath && onEmpty) onEmpty();
62
+ }
63
+
64
+ export function focusedFileViewerTab() { return focusedPath; }
65
+ export function clearFileViewerTabs() { tabs.clear(); focusedPath = null; render(); }
@@ -9,6 +9,7 @@ import { copyMarkdownFormatting } from './rich-clipboard.js';
9
9
  import { animateMarkdownChange, beginMarkdownPresentation, cancelMarkdownFollow, isFollowingMarkdown } from './markdown-live-edit.js';
10
10
  import { store } from './store.js';
11
11
  import { enterMarkdownSlides, exitMarkdownSlides, handleMarkdownSlideKey, syncMarkdownSlidesButton, toggleMarkdownSlideLevelMenu } from './markdown-slides.js';
12
+ import { initFileViewerTabs, openFileViewerTab, closeFileViewerTab, focusedFileViewerTab, clearFileViewerTabs } from './filebrowser-tabs.js';
12
13
 
13
14
  var ctx;
14
15
  var showDropHint = function () {};
@@ -30,6 +31,14 @@ var pendingFileAt = null; // callback for pending file-at
30
31
 
31
32
  export function initFileBrowser(_ctx) {
32
33
  ctx = _ctx;
34
+ var mainPanels = document.getElementById("main-panels");
35
+ if (mainPanels && ctx.fileViewerEl) mainPanels.insertBefore(ctx.fileViewerEl, mainPanels.firstChild);
36
+ initFileViewerTabs({
37
+ onFocus: function(path) {
38
+ if (path !== currentFilePath) requestFileContent(path);
39
+ },
40
+ onEmpty: function() { teardownFileViewer(); },
41
+ });
33
42
 
34
43
  // Load material file icons in background
35
44
  initFileIcons().then(function () {
@@ -133,7 +142,7 @@ export function initFileBrowser(_ctx) {
133
142
 
134
143
  // Close button
135
144
  document.getElementById("file-viewer-close").addEventListener("click", function () {
136
- closeFileViewer();
145
+ closeFileViewerTab(focusedFileViewerTab());
137
146
  });
138
147
 
139
148
  // Full-viewport presentation toggle
@@ -448,6 +457,14 @@ function ensureRenderedMarkdown() {
448
457
  }
449
458
 
450
459
  export function closeFileViewer() {
460
+ if (focusedFileViewerTab()) {
461
+ closeFileViewerTab(focusedFileViewerTab());
462
+ return;
463
+ }
464
+ teardownFileViewer();
465
+ }
466
+
467
+ function teardownFileViewer() {
451
468
  if (currentFilePath && isFollowingMarkdown(currentFilePath)) cancelMarkdownFollow();
452
469
  sendUnwatch();
453
470
  inlineDiffActive = false;
@@ -475,8 +492,8 @@ export function setFileViewerFullscreen(enabled) {
475
492
  }
476
493
 
477
494
  export function resetFileBrowser() {
478
- // Close viewer
479
- closeFileViewer();
495
+ clearFileViewerTabs();
496
+ teardownFileViewer();
480
497
  // Clear all cached state
481
498
  treeData = {};
482
499
  currentContent = null;
@@ -513,6 +530,7 @@ export function openFile(filePath, opts) {
513
530
  cancelMarkdownFollow();
514
531
  }
515
532
  pendingRenderedOpen = !!(opts && opts.rendered);
533
+ openFileViewerTab(filePath);
516
534
  if (opts && opts.diff) {
517
535
  pendingOpenMode = { type: "diff", oldStr: opts.diff.oldStr, newStr: opts.diff.newStr };
518
536
  } else {
@@ -540,6 +558,7 @@ export function openWorkingTreeDiff(diff) {
540
558
 
541
559
  export function presentMarkdownEdit(msg) {
542
560
  if (!msg || !beginMarkdownPresentation(msg.path)) return;
561
+ openFileViewerTab(msg.path, { content: msg.content });
543
562
  pendingOpenMode = null;
544
563
  pendingRenderedOpen = true;
545
564
  pendingRefresh = false;
@@ -1031,6 +1050,7 @@ function renderEntries(container, entries, depth) {
1031
1050
  // --- File viewer ---
1032
1051
 
1033
1052
  function showFileContent(msg) {
1053
+ openFileViewerTab(msg.path, { content: msg.content });
1034
1054
  var pathEl = document.getElementById("file-viewer-path");
1035
1055
  var bodyEl = document.getElementById("file-viewer-body");
1036
1056
  var renderBtn = document.getElementById("file-viewer-render");
@@ -23,6 +23,12 @@ export function reportPaneContext(data) {
23
23
  }, window.location.origin);
24
24
  }
25
25
 
26
+ export function forwardPaneMarkdownPresentation(message) {
27
+ if (!store.get('paneMode') || window.parent === window) return false;
28
+ window.parent.postMessage({ type: "clay-pane-present-markdown", message: message }, window.location.origin);
29
+ return true;
30
+ }
31
+
26
32
  // Toggle without setContextView: panes share localStorage with the main app,
27
33
  // so persisting here would silently rewrite the user's main-view preference.
28
34
  function togglePaneContextPanel() {
@@ -10,6 +10,7 @@ import { VENDOR_AVATARS, VENDOR_NAMES } from './app-rendering.js';
10
10
  import { groupedSessionIds, findSplitGroup } from './split-group-helpers.js';
11
11
  import { showConfirm } from './app-misc.js';
12
12
  import { syncPairChrome } from './split-pair-ui.js';
13
+ import { presentMarkdownEdit } from './filebrowser.js';
13
14
 
14
15
  var host = null;
15
16
  var nativeApp = null;
@@ -269,7 +270,13 @@ function updatePaneContextChip(paneEl, msg) {
269
270
  function handlePaneMessage(event) {
270
271
  if (event.origin !== window.location.origin) return;
271
272
  var msg = event.data;
272
- if (!msg || msg.type !== "clay-pane-context" || !host) return;
273
+ if (!msg || !host) return;
274
+ if (msg.type === "clay-pane-present-markdown") {
275
+ var present = msg.message;
276
+ if (present) presentMarkdownEdit(present);
277
+ return;
278
+ }
279
+ if (msg.type !== "clay-pane-context") return;
273
280
  var frames = host.querySelectorAll(".split-pane-frame");
274
281
  for (var i = 0; i < frames.length; i++) {
275
282
  if (frames[i].contentWindow === event.source) {
@@ -8,6 +8,7 @@ var fs = require("fs");
8
8
  var { CodexAppServer } = require("../codex-app-server");
9
9
  var skillDiscovery = require("../skill-discovery");
10
10
  var { resolveOsUserInfo } = require("../../os-users");
11
+ var backgroundTasks = require("../codex-background-tasks");
11
12
 
12
13
  // --- Event flattening ---
13
14
  // Converts app-server JSON-RPC notifications into flat objects with a yokeType field.
@@ -672,6 +673,7 @@ function createEventState(model) {
672
673
  toolBlocks: {},
673
674
  commandInputs: {},
674
675
  planTexts: {},
676
+ backgroundTasks: backgroundTasks.createState(),
675
677
  };
676
678
  }
677
679
 
@@ -698,6 +700,7 @@ function createCodexQueryHandle(appServer, queryOpts) {
698
700
  var eventWaiting = null;
699
701
  var iteratorDone = false;
700
702
  var finishedNotified = false;
703
+ var removeBackgroundTaskReset = null;
701
704
 
702
705
  function notifyFinished() {
703
706
  if (finishedNotified) return;
@@ -722,6 +725,12 @@ function createCodexQueryHandle(appServer, queryOpts) {
722
725
  }
723
726
  }
724
727
 
728
+ if (typeof queryOpts.registerBackgroundTaskReset === "function") {
729
+ removeBackgroundTaskReset = queryOpts.registerBackgroundTaskReset(function() {
730
+ backgroundTasks.emitReset(state.backgroundTasks, pushEvent);
731
+ });
732
+ }
733
+
725
734
  function endIterator() {
726
735
  iteratorDone = true;
727
736
  if (eventWaiting) {
@@ -729,6 +738,10 @@ function createCodexQueryHandle(appServer, queryOpts) {
729
738
  eventWaiting = null;
730
739
  resolve({ value: undefined, done: true });
731
740
  }
741
+ if (removeBackgroundTaskReset) {
742
+ removeBackgroundTaskReset();
743
+ removeBackgroundTaskReset = null;
744
+ }
732
745
  notifyFinished();
733
746
  }
734
747
 
@@ -934,6 +947,10 @@ function createCodexQueryHandle(appServer, queryOpts) {
934
947
  pushEvent(yokeEvents[i]);
935
948
  }
936
949
 
950
+ if (method === "turn/completed" || method === "turn/failed") {
951
+ backgroundTasks.poll(appServer, state.threadId, state.backgroundTasks, pushEvent).catch(function() {});
952
+ }
953
+
937
954
  // Resolve turn promise when turn ends
938
955
  if (method === "turn/completed" || method === "turn/failed") {
939
956
  if (turnResolve) {
@@ -1246,6 +1263,20 @@ function createCodexAdapter(opts) {
1246
1263
  ];
1247
1264
  var _cachedModels = CODEX_MODELS.slice();
1248
1265
  var _appServer = null;
1266
+ var _backgroundTaskResetListeners = [];
1267
+
1268
+ function registerBackgroundTaskReset(listener) {
1269
+ _backgroundTaskResetListeners.push(listener);
1270
+ return function() {
1271
+ var index = _backgroundTaskResetListeners.indexOf(listener);
1272
+ if (index !== -1) _backgroundTaskResetListeners.splice(index, 1);
1273
+ };
1274
+ }
1275
+
1276
+ function emitBackgroundTaskReset() {
1277
+ var listeners = _backgroundTaskResetListeners.slice();
1278
+ for (var i = 0; i < listeners.length; i++) listeners[i]();
1279
+ }
1249
1280
  var _initPromise = null;
1250
1281
  var _shutdownPromise = null;
1251
1282
  var _refCount = 0;
@@ -1541,6 +1572,7 @@ function createCodexAdapter(opts) {
1541
1572
  // Spawn and initialize app-server
1542
1573
  _appServer = _createAppServer(serverOpts);
1543
1574
  await _appServer.start();
1575
+ emitBackgroundTaskReset();
1544
1576
 
1545
1577
  await _appServer.send("initialize", {
1546
1578
  clientInfo: { name: "clay", title: "Clay", version: "1.0.0" },
@@ -1676,6 +1708,7 @@ function createCodexAdapter(opts) {
1676
1708
  canUseTool: queryOpts.canUseTool || null,
1677
1709
  onElicitation: queryOpts.onElicitation || null,
1678
1710
  resumeSessionId: queryOpts.resumeSessionId || null,
1711
+ registerBackgroundTaskReset: registerBackgroundTaskReset,
1679
1712
  };
1680
1713
 
1681
1714
  // Reasoning effort
@@ -0,0 +1,84 @@
1
+ function terminalList(result) {
2
+ if (Array.isArray(result)) return result;
3
+ if (!result || typeof result !== "object") return [];
4
+ if (Array.isArray(result.terminals)) return result.terminals;
5
+ if (Array.isArray(result.backgroundTerminals)) return result.backgroundTerminals;
6
+ if (Array.isArray(result.items)) return result.items;
7
+ return [];
8
+ }
9
+
10
+ function isLiveTerminal(terminal) {
11
+ var status = String((terminal && (terminal.status || terminal.state)) || "").toLowerCase();
12
+ return status !== "exited" && status !== "terminated" && status !== "completed" && status !== "failed";
13
+ }
14
+
15
+ function mapTerminals(result) {
16
+ var terminals = terminalList(result);
17
+ var tasks = [];
18
+ for (var i = 0; i < terminals.length; i++) {
19
+ var terminal = terminals[i] || {};
20
+ var taskId = terminal.id || terminal.terminalId || terminal.taskId || "";
21
+ if (!taskId || !isLiveTerminal(terminal)) continue;
22
+ tasks.push({
23
+ task_id: String(taskId),
24
+ task_type: "shell",
25
+ description: terminal.commandLine || terminal.command || terminal.name || "",
26
+ });
27
+ }
28
+ return tasks;
29
+ }
30
+
31
+ function taskIds(tasks) {
32
+ var ids = {};
33
+ for (var i = 0; i < tasks.length; i++) ids[tasks[i].task_id] = true;
34
+ return ids;
35
+ }
36
+
37
+ function sameMembership(first, second) {
38
+ var firstKeys = Object.keys(first || {});
39
+ var secondKeys = Object.keys(second || {});
40
+ if (firstKeys.length !== secondKeys.length) return false;
41
+ for (var i = 0; i < firstKeys.length; i++) {
42
+ if (!second[firstKeys[i]]) return false;
43
+ }
44
+ return true;
45
+ }
46
+
47
+ function createState() {
48
+ return { taskIds: null };
49
+ }
50
+
51
+ function emitIfChanged(state, tasks, pushEvent) {
52
+ var ids = taskIds(tasks);
53
+ if (state.taskIds === null && tasks.length === 0) {
54
+ state.taskIds = ids;
55
+ return false;
56
+ }
57
+ if (state.taskIds && sameMembership(state.taskIds, ids)) return false;
58
+ state.taskIds = ids;
59
+ pushEvent({ yokeType: "background_tasks_changed", tasks: tasks });
60
+ return true;
61
+ }
62
+
63
+ function poll(appServer, threadId, state, pushEvent) {
64
+ if (!appServer || !threadId || appServer._clayBackgroundTasksPollingDisabled) return Promise.resolve(false);
65
+ return appServer.send("thread/backgroundTerminals/list", { threadId: threadId }).then(function(result) {
66
+ return emitIfChanged(state, mapTerminals(result), pushEvent);
67
+ }).catch(function() {
68
+ appServer._clayBackgroundTasksPollingDisabled = true;
69
+ return false;
70
+ });
71
+ }
72
+
73
+ function emitReset(state, pushEvent) {
74
+ state.taskIds = {};
75
+ pushEvent({ yokeType: "background_tasks_changed", tasks: [] });
76
+ }
77
+
78
+ module.exports = {
79
+ createState: createState,
80
+ mapTerminals: mapTerminals,
81
+ emitIfChanged: emitIfChanged,
82
+ poll: poll,
83
+ emitReset: emitReset,
84
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "3.4.0-beta.21",
3
+ "version": "3.4.0-beta.23",
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",