clay-server 4.1.0 → 4.2.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.
@@ -41,6 +41,7 @@ function getProjectAccess(config, slug, osUsers) {
41
41
  visibility: visibility,
42
42
  allowedUsers: parentAllowedUsers,
43
43
  ownerId: project.ownerId || null,
44
+ sessionVisibilityDefault: project.sessionVisibilityDefault === "shared" ? "shared" : "private",
44
45
  isWorktree: false,
45
46
  };
46
47
  }
@@ -52,6 +53,7 @@ function getProjectAccess(config, slug, osUsers) {
52
53
  visibility: visibility,
53
54
  allowedUsers: uniqueUsers(parentAllowedUsers.concat(worktreeAllowedUsers)),
54
55
  ownerId: project.ownerId || null,
56
+ sessionVisibilityDefault: project.sessionVisibilityDefault === "shared" ? "shared" : "private",
55
57
  isWorktree: true,
56
58
  parentSlug: parentSlug,
57
59
  parentAllowedUsers: parentAllowedUsers,
@@ -61,6 +63,7 @@ function getProjectAccess(config, slug, osUsers) {
61
63
  visibility: visibility,
62
64
  allowedUsers: parentAllowedUsers,
63
65
  ownerId: project.ownerId || null,
66
+ sessionVisibilityDefault: project.sessionVisibilityDefault === "shared" ? "shared" : "private",
64
67
  },
65
68
  };
66
69
  }
@@ -84,6 +87,16 @@ function setAllowedUsers(config, slug, allowedUsers) {
84
87
  return { ok: true, isWorktree: true, allowedUsers: clean };
85
88
  }
86
89
 
90
+ function setSessionVisibilityDefault(config, slug, visibility) {
91
+ if (visibility !== "private" && visibility !== "shared") return { error: "Invalid session visibility default" };
92
+ if (parentSlugFor(slug)) return { error: "Worktrees inherit this setting from their parent project." };
93
+ var project = findProject(config, slug);
94
+ if (!project) return { error: "Project not found" };
95
+ if (visibility === "shared") project.sessionVisibilityDefault = "shared";
96
+ else delete project.sessionVisibilityDefault;
97
+ return { ok: true, visibility: visibility };
98
+ }
99
+
87
100
  function clearWorktreeGrant(config, slug) {
88
101
  var parentSlug = parentSlugFor(slug);
89
102
  var project = parentSlug ? findProject(config, parentSlug) : null;
@@ -113,6 +126,7 @@ module.exports = {
113
126
  parentSlugFor: parentSlugFor,
114
127
  getProjectAccess: getProjectAccess,
115
128
  setAllowedUsers: setAllowedUsers,
129
+ setSessionVisibilityDefault: setSessionVisibilityDefault,
116
130
  clearWorktreeGrant: clearWorktreeGrant,
117
131
  reconcileWorktreeGrants: reconcileWorktreeGrants,
118
132
  };
package/lib/daemon.js CHANGED
@@ -556,6 +556,13 @@ var relay = createServer({
556
556
  });
557
557
  return { ok: true };
558
558
  },
559
+ onSetProjectSessionVisibilityDefault: function (slug, visibility) {
560
+ var result = daemonProjectAccess.setSessionVisibilityDefault(config, slug, visibility);
561
+ if (result.error) return { ok: false, error: result.error };
562
+ saveConfig(config);
563
+ relay.broadcastAll({ type: "projects_updated", projects: relay.getProjects(), projectCount: config.projects.length });
564
+ return result;
565
+ },
559
566
  onProjectOwnerChanged: function (slug, ownerId) {
560
567
  console.log("[daemon] onProjectOwnerChanged:", slug, "→", ownerId);
561
568
  var oldOwnerId = null;
@@ -2,6 +2,7 @@
2
2
 
3
3
  var yoke = require("./yoke");
4
4
  var provenance = require("./session-provenance");
5
+ var sessionVisibility = require("./session-visibility");
5
6
 
6
7
  function attachIssueLaunch(ctx) {
7
8
  var requests = new Map();
@@ -79,8 +80,9 @@ function attachIssueLaunch(ctx) {
79
80
  var liveLinuxUser = typeof ctx.getLinuxUserForSession === "function" ? ctx.getLinuxUserForSession({ ownerId: actorId }) : null;
80
81
  if (osUsersEnabled() && liveLinuxUser !== expectedLinuxUser) throw new Error("The OS identity changed while issue work was being prepared.");
81
82
 
83
+ var projectAccess = typeof ctx.getProjectAccess === "function" ? ctx.getProjectAccess() : null;
82
84
  var session = ctx.sm.createSessionRaw({ vendor: runtime.vendor, model: runtime.model, effort: runtime.effort || null,
83
- mode: "gui", ownerId: actor && actor.id || null, sessionVisibility: "private" });
85
+ mode: "gui", ownerId: actor && actor.id || null, sessionVisibility: sessionVisibility.defaultForProject(projectAccess) });
84
86
  provenance.ensureOrigin(session);
85
87
  if (session.hidden === true || session.delegated === true || provenance.isWorker(session)) {
86
88
  ctx.sm.deleteSession(session.localId);
@@ -555,7 +555,7 @@ function attachSessions(ctx) {
555
555
  }
556
556
  sm.sweepBlankSessions();
557
557
  var sessionOpts = {
558
- sessionVisibility: sessionVisibility.normalize(msg.sessionVisibility),
558
+ sessionVisibility: "private",
559
559
  vendor: prepared.runtime.vendor,
560
560
  model: prepared.runtime.model,
561
561
  effort: prepared.runtime.effort || null,
@@ -619,7 +619,15 @@ function attachSessions(ctx) {
619
619
  sm.sweepBlankSessions();
620
620
  var sessionOpts = {};
621
621
  if (ws._clayUser && usersModule.isMultiUser()) sessionOpts.ownerId = ws._clayUser.id;
622
- sessionOpts.sessionVisibility = sessionVisibility.normalize(msg.sessionVisibility);
622
+ // Only ordinary project conversations may opt into this setting.
623
+ // Scheduled interviews and background/worker creation paths remain explicit.
624
+ var visibilityChoice = sessionVisibility.resolveNewSessionVisibility(getProjectAccess(), msg, isMate);
625
+ if (visibilityChoice.error) {
626
+ scheduledRequestError(ws, msg, visibilityChoice.error);
627
+ if (!msg.requestId) sendTo(ws, { type: "error", text: visibilityChoice.error });
628
+ return true;
629
+ }
630
+ sessionOpts.sessionVisibility = visibilityChoice.visibility;
623
631
  if (msg.vendor) sessionOpts.vendor = msg.vendor;
624
632
  // Mode resolution: vendors without a TUI session mode are always GUI.
625
633
  // TUI-capable sessions honor the explicit msg.mode if provided, otherwise
@@ -701,8 +709,7 @@ function attachSessions(ctx) {
701
709
  if (reusable) {
702
710
  if (sessionOpts.vendor && reusable.vendor !== sessionOpts.vendor) reusable.vendor = sessionOpts.vendor;
703
711
  if (!reusable.effort) reusable.effort = sessionOpts.effort;
704
- sm.setSessionVisibility(reusable.localId, sessionOpts.sessionVisibility);
705
- sessionVisibility.revokeViewers(ctx, reusable);
712
+ // Reusable blanks already exist; do not retroactively change them.
706
713
  sm.switchSession(reusable.localId, ws);
707
714
  newSess = reusable;
708
715
  } else {
@@ -735,6 +742,14 @@ function attachSessions(ctx) {
735
742
  return true;
736
743
  }
737
744
 
745
+ if (msg.type === "set_project_session_visibility_default") {
746
+ sessionVisibility.handleDefaultChange({
747
+ slug: slug, isMate: isMate, osUsers: osUsers, usersModule: usersModule, getProjectAccess: getProjectAccess,
748
+ opts: opts, sendTo: sendTo,
749
+ }, ws, msg);
750
+ return true;
751
+ }
752
+
738
753
  if (msg.type === "set_session_bookmark") {
739
754
  if (typeof msg.sessionId === "number") {
740
755
  var bookmarkTarget = sm.sessions.get(msg.sessionId);
@@ -1493,7 +1508,11 @@ function attachSessions(ctx) {
1493
1508
  } else {
1494
1509
  forkHistory = session.history.slice();
1495
1510
  }
1496
- var forked = sm.createSession({ vendor: session.vendor, ownerId: session.ownerId || null }, ws);
1511
+ var forked = sm.createSession({
1512
+ vendor: session.vendor,
1513
+ ownerId: session.ownerId || null,
1514
+ sessionVisibility: sessionVisibility.normalize(session.sessionVisibility),
1515
+ }, ws);
1497
1516
  forked.cliSessionId = result.sessionId;
1498
1517
  forked.title = forkTitle;
1499
1518
  forked.history = forkHistory;
@@ -1511,7 +1530,14 @@ function attachSessions(ctx) {
1511
1530
  // Read history from CLI session files
1512
1531
  var cliSess = require("./cli-sessions");
1513
1532
  return cliSess.readCliSessionHistory(resolveSessionHome(session), cwd, result.sessionId).then(function(history) {
1514
- var forked = sm.resumeSession(result.sessionId, { history: history, title: forkTitle }, ws);
1533
+ var forked = sm.resumeSession(result.sessionId, {
1534
+ history: history,
1535
+ title: forkTitle,
1536
+ vendor: session.vendor,
1537
+ ownerId: session.ownerId || null,
1538
+ sessionVisibility: sessionVisibility.normalize(session.sessionVisibility),
1539
+ sessionVisibilityExplicit: session.sessionVisibilityExplicit === true,
1540
+ }, ws);
1515
1541
  if (forked) {
1516
1542
  ws._clayActiveSession = forked.localId;
1517
1543
  sendTo(ws, { type: "fork_complete", sessionId: forked.localId });
package/lib/project.js CHANGED
@@ -794,6 +794,10 @@ function createProjectContext(opts) {
794
794
  var _issueWork = require("./project-issue-launch").attachIssueLaunch({
795
795
  sm: sm, resolveDefaultAi: opts.resolveDefaultAi, getSdk: function () { return sdk; },
796
796
  getLinuxUserForSession: getLinuxUserForSession, osUsers: osUsers,
797
+ getProjectAccess: function () {
798
+ if (isMate) return { visibility: "private", ownerId: projectOwnerId || null, allowedUsers: [] };
799
+ return opts.getProjectAccess ? opts.getProjectAccess() : { visibility: "public", ownerId: projectOwnerId || null };
800
+ },
797
801
  ensureProjectAccessForSession: ensureProjectAccessForSession,
798
802
  canStartWork: function (ws, actor) { return clients.has(ws) && ws.readyState === 1 && ws._clayUser === actor; },
799
803
  onProcessingChanged: onProcessingChanged, sendTo: sendTo,
package/lib/public/app.js CHANGED
@@ -432,6 +432,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
432
432
  defaultAiDraft: { vendor: "", model: "", effort: "" },
433
433
  defaultAiDraftDirty: false,
434
434
  defaultVendorState: { loading: false, saving: false, getRequestId: null, saveRequestId: null, preference: null, preferencePresent: false, installedVendors: [], accountId: null, projectSlug: null, serverEpoch: null, canonicalRevision: 0, error: "" },
435
+ projectSessionVisibilityState: { active: null, pending: null },
435
436
 
436
437
  // dm
437
438
  dmTargetUser: null,
@@ -512,9 +512,45 @@ body.mate-dm-active .search-clay-transcript .search-clay-message-user .bubble,
512
512
  .search-clay-activity-item small { color: var(--text-dimmer); font-size: 10px; line-height: 1.35; }
513
513
  @keyframes searchClaySpin { to { transform: rotate(360deg); } }
514
514
 
515
- .clayos-session-link,
516
- .clayos-issue-link,
517
- .clayos-log-link {
515
+ .clayos-record-link {
516
+ display: inline-flex;
517
+ align-items: center;
518
+ gap: 6px;
519
+ min-height: 28px;
520
+ margin: 2px 1px;
521
+ padding: 3px 8px 3px 6px;
522
+ border: 1px solid color-mix(in srgb, var(--record-color, var(--accent)) 32%, var(--border));
523
+ border-radius: 8px;
524
+ background: color-mix(in srgb, var(--record-color, var(--accent)) 7%, var(--bg-alt));
525
+ color: var(--text);
526
+ font: inherit;
527
+ font-size: 11px;
528
+ line-height: 1;
529
+ vertical-align: middle;
530
+ cursor: pointer;
531
+ }
532
+ .clayos-record-link::before {
533
+ content: "\2197";
534
+ display: grid;
535
+ place-items: center;
536
+ width: 17px;
537
+ height: 17px;
538
+ border-radius: 5px;
539
+ background: color-mix(in srgb, var(--record-color, var(--accent)) 14%, transparent);
540
+ color: var(--record-color, var(--accent));
541
+ font-size: 12px;
542
+ }
543
+ .clayos-record-link > span { font-weight: 620; }
544
+ .clayos-record-link > small { min-width: 0; max-width: 28em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; border-inline-start: 1px solid color-mix(in srgb, currentColor 22%, transparent); padding-inline-start: 6px; color: var(--text-muted); font-size: 11px; }
545
+ .clayos-record-link:hover { border-color: color-mix(in srgb, var(--record-color, var(--accent)) 58%, var(--border)); background: color-mix(in srgb, var(--record-color, var(--accent)) 11%, var(--bg-alt)); }
546
+ .clayos-record-link:focus-visible { outline: 2px solid var(--record-color, var(--accent)); outline-offset: 2px; }
547
+ .clayos-record-link.is-loading { opacity: 0.62; cursor: progress; }
548
+ .clayos-record-link.is-loading::before { animation: searchClaySpin 1.2s linear infinite; }
549
+ .clayos-record-link.is-error { border-color: color-mix(in srgb, var(--warning) 48%, var(--border)); }
550
+ .clayos-issue-link { --record-color: var(--link, var(--accent)); }
551
+ .clayos-log-link { --record-color: var(--accent); }
552
+
553
+ .clayos-session-link {
518
554
  display: inline-flex;
519
555
  align-items: center;
520
556
  gap: 6px;
@@ -531,9 +567,7 @@ body.mate-dm-active .search-clay-transcript .search-clay-message-user .bubble,
531
567
  vertical-align: middle;
532
568
  cursor: pointer;
533
569
  }
534
- .clayos-session-link::before,
535
- .clayos-issue-link::before,
536
- .clayos-log-link::before {
570
+ .clayos-session-link::before {
537
571
  content: "\2197";
538
572
  display: grid;
539
573
  place-items: center;
@@ -544,29 +578,13 @@ body.mate-dm-active .search-clay-transcript .search-clay-message-user .bubble,
544
578
  color: var(--accent);
545
579
  font-size: 12px;
546
580
  }
547
- .clayos-issue-link::before,
548
- .clayos-log-link::before { content: "\2261"; }
549
- .clayos-session-link > span,
550
- .clayos-issue-link > span,
551
- .clayos-log-link > span { font-weight: 620; }
552
- .clayos-session-link > small,
553
- .clayos-issue-link > small,
554
- .clayos-log-link > small { color: var(--text-muted); font-size: 10px; }
555
- .clayos-session-link:hover,
556
- .clayos-issue-link:hover,
557
- .clayos-log-link:hover { border-color: color-mix(in srgb, var(--accent) 52%, var(--border)); background: color-mix(in srgb, var(--accent) 11%, var(--bg-alt)); }
558
- .clayos-session-link:focus-visible,
559
- .clayos-issue-link:focus-visible,
560
- .clayos-log-link:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
561
- .clayos-session-link.is-loading,
562
- .clayos-issue-link.is-loading,
563
- .clayos-log-link.is-loading { opacity: 0.62; cursor: progress; }
564
- .clayos-session-link.is-loading::before,
565
- .clayos-issue-link.is-loading::before,
566
- .clayos-log-link.is-loading::before { animation: searchClaySpin 1.2s linear infinite; }
567
- .clayos-session-link.is-error,
568
- .clayos-issue-link.is-error,
569
- .clayos-log-link.is-error { border-color: color-mix(in srgb, var(--warning) 48%, var(--border)); }
581
+ .clayos-session-link > span { font-weight: 620; }
582
+ .clayos-session-link > small { color: var(--text-muted); font-size: 10px; }
583
+ .clayos-session-link:hover { border-color: color-mix(in srgb, var(--accent) 52%, var(--border)); background: color-mix(in srgb, var(--accent) 11%, var(--bg-alt)); }
584
+ .clayos-session-link:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
585
+ .clayos-session-link.is-loading { opacity: 0.62; cursor: progress; }
586
+ .clayos-session-link.is-loading::before { animation: searchClaySpin 1.2s linear infinite; }
587
+ .clayos-session-link.is-error { border-color: color-mix(in srgb, var(--warning) 48%, var(--border)); }
570
588
 
571
589
  .search-clay-pending {
572
590
  align-self: flex-start;
@@ -925,11 +943,11 @@ body.mate-dm-active .search-clay-transcript .search-clay-message-user .bubble,
925
943
  }
926
944
 
927
945
  .clayos-issue-link { max-width: min(100%, 30em); box-sizing: border-box; }
946
+ .clayos-issue-link::before,
947
+ .clayos-log-link::before { content: "\2261"; }
928
948
  .clayos-issue-link::before { content: "\25CE"; flex: 0 0 17px; }
929
- .clayos-issue-link[data-status="in_progress"]::before { color: #bd923f; }
930
- .clayos-issue-link[data-status="resolved"]::before { content: "\2713"; color: var(--brand-green); }
949
+ .clayos-issue-link[data-status="in_progress"]::before { color: var(--warning, var(--accent)); }
950
+ .clayos-issue-link[data-status="resolved"]::before { content: "\2713"; color: var(--success, var(--accent)); }
931
951
  .clayos-issue-link[data-status="closed"]::before { content: "\00D7"; color: var(--text-dimmer); }
932
952
  .clayos-issue-link > span { flex-shrink: 0; }
933
- .clayos-issue-link > small { min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
934
953
  .clayos-log-link { max-width: 100%; box-sizing: border-box; }
935
- .clayos-log-link > small { min-width: 0; overflow-wrap: anywhere; line-height: 1.35; }
@@ -46,11 +46,11 @@
46
46
  .issue-row small { font-size: 10.5px; font-family: var(--font-mono); }
47
47
  .issue-row-meta { display: flex; align-items: baseline; flex-wrap: wrap; gap: 8px; margin-top: 8px; color: var(--text-dimmer); font: 10.5px/1.5 var(--font-mono); }
48
48
  .issue-row-meta .issue-status { font-family: var(--font-sans); }
49
- .issue-status { display: inline-flex; align-items: center; gap: 6px; font-size: 11px; letter-spacing: .03em; }
49
+ .issue-status { display: inline-flex; align-items: center; gap: 6px; color: var(--text-secondary); font-size: 11px; letter-spacing: .03em; }
50
50
  .issue-status::before { content: ''; width: 7px; height: 7px; border: 1px solid currentColor; border-radius: 50%; }
51
- .issue-status[data-status="in_progress"] { color: #bd923f; }
52
- .issue-status[data-status="resolved"] { color: #479b77; }
53
- .issue-status[data-status="closed"] { color: var(--text-muted); }
51
+ .issue-status[data-status="in_progress"]::before { color: var(--warning, var(--accent)); }
52
+ .issue-status[data-status="resolved"]::before { color: var(--success, var(--accent)); }
53
+ .issue-status[data-status="closed"]::before { color: var(--text-muted); }
54
54
  .issue-detail { max-width: 760px; margin: 0 auto; padding: 32px 28px 52px; }
55
55
  .issue-doc-chips { display: flex; align-items: center; flex-wrap: wrap; gap: 7px; margin-bottom: 10px; }
56
56
  .issue-detail .issue-doc-title { margin: 0 0 10px; font-family: var(--font-display); font-size: clamp(24px, 2.8vw, 34px); letter-spacing: -.025em; line-height: 1.12; overflow-wrap: anywhere; }
@@ -830,6 +830,16 @@
830
830
  </div>
831
831
  </div>
832
832
  </div>
833
+ <div class="settings-field" id="ps-session-visibility-default-field" style="display:none">
834
+ <label class="settings-label" for="ps-session-visibility-default">New session visibility</label>
835
+ <div class="settings-hint">Applies to new project conversations and Issue Start work sessions. Existing sessions, hidden work, and delegated workflows keep their current visibility.</div>
836
+ <div class="settings-hint hidden" id="ps-session-visibility-default-inherited-hint">This worktree inherits the setting from its parent project. Change it there.</div>
837
+ <div class="settings-hint hidden" id="ps-session-visibility-default-non-current-hint">Open this project to change this setting.</div>
838
+ <select class="ps-select" id="ps-session-visibility-default">
839
+ <option value="private">Private</option>
840
+ <option value="shared">Shared with project members</option>
841
+ </select>
842
+ </div>
833
843
  </div>
834
844
  </div>
835
845
  <!-- Defaults -->
@@ -38,6 +38,7 @@ import { handleFsList, handleFsRead, handleFileChanged, handleDirChanged, handle
38
38
  import { beginMarkdownTurn, finishMarkdownTurn, markdownPathFromToolInput } from './markdown-live-edit.js';
39
39
  import { forwardPaneMarkdownPresentation } from './pane-bridge.js';
40
40
  import { isProjectSettingsOpen, handleInstructionsRead, handleInstructionsWrite, handleProjectEnv, handleProjectEnvSaved, handleProjectSharedEnv, handleProjectSharedEnvSaved, handleProjectOwnerChanged } from './project-settings.js';
41
+ import { handleProjectSessionVisibilityDefault, refreshProjectSessionVisibilitySettings } from './project-session-visibility-settings.js';
41
42
  import { updateSettingsStats, updateDaemonConfig, handleSetPinResult, handleKeepAwakeChanged, handleInheritGroupsChanged, handleAutoContinueChanged, handleRestartResult, handleShutdownResult, handleSharedEnv, handleSharedEnvSaved, handleGlobalClaudeMdRead, handleGlobalClaudeMdWrite } from './server-settings.js';
42
43
  import { handleTermList, handleTermCreated, sendTerminalCommand, handleTermOutput, handleTermResized, handleTermExited, handleTermClosed } from './terminal.js';
43
44
  import { attachTuiView, detachTuiView, setTuiSuspendedView, tuiHandleTermOutput, tuiHandleTermResized, tuiHandleTermExited, tuiHandleTermClosed } from './session-tui-view.js';
@@ -103,6 +104,10 @@ export function processMessage(msg) {
103
104
  if (handleScheduledTaskMessage(msg)) return;
104
105
  if (handleDefaultVendorMessage(msg)) return;
105
106
  if (handleContextViewMessage(msg)) return;
107
+ if (msg && msg.type === "set_project_session_visibility_default_result") {
108
+ handleProjectSessionVisibilityDefault(msg);
109
+ return;
110
+ }
106
111
  if (msg && msg.type === "schedule_message_result") {
107
112
  handleScheduleMessageResult(msg);
108
113
  return;
@@ -375,6 +380,7 @@ export function processMessage(msg) {
375
380
  if (spBanner) spBanner.classList.remove("hidden");
376
381
  }
377
382
  updateProjectList(msg);
383
+ refreshProjectSessionVisibilitySettings(msg.projects);
378
384
  break;
379
385
 
380
386
  case "update_available":
@@ -1735,6 +1741,7 @@ export function processMessage(msg) {
1735
1741
 
1736
1742
  case "projects_updated":
1737
1743
  updateProjectList(msg);
1744
+ refreshProjectSessionVisibilitySettings(msg.projects);
1738
1745
  renderUserStrip();
1739
1746
  break;
1740
1747
 
@@ -31,6 +31,7 @@ function createIssueLink(ref, labelText) {
31
31
  var button = document.createElement("button");
32
32
  button.type = "button";
33
33
  button.className = "clayos-issue-link";
34
+ button.classList.add("clayos-record-link");
34
35
  button.contentEditable = "false";
35
36
  button.dataset.issueRef = ref;
36
37
  button.setAttribute("aria-label", "Open Project Issue" + (labelText ? " " + labelText : ""));
@@ -28,6 +28,7 @@ function createLogLink(ref, labelText) {
28
28
  var button = document.createElement("button");
29
29
  button.type = "button";
30
30
  button.className = "clayos-log-link";
31
+ button.classList.add("clayos-record-link");
31
32
  button.contentEditable = "false";
32
33
  button.dataset.logRef = ref;
33
34
  button.setAttribute("aria-label", "Open Project Log" + (labelText ? " " + labelText : ""));
@@ -0,0 +1,138 @@
1
+ import { store } from './store.js';
2
+ import { getWs } from './ws-ref.js';
3
+ import { showToast } from './utils.js';
4
+
5
+ var requestSequence = 0;
6
+ var initialized = false;
7
+
8
+ function selectElement() {
9
+ return document.getElementById("ps-session-visibility-default");
10
+ }
11
+
12
+ function pendingState() {
13
+ return (store.get('projectSessionVisibilityState') || {}).pending || null;
14
+ }
15
+
16
+ function activeState() {
17
+ return (store.get('projectSessionVisibilityState') || {}).active || null;
18
+ }
19
+
20
+ function setVisibilityState(active, pending) {
21
+ store.set({ projectSessionVisibilityState: { active: active || null, pending: pending || null } });
22
+ }
23
+
24
+ function setSelectState(select, value, disabled) {
25
+ if (!select) return;
26
+ select.value = value;
27
+ select.disabled = !!disabled;
28
+ }
29
+
30
+ function isCurrentProject(active) {
31
+ return !!(active && active.slug && active.slug === store.get('currentSlug'));
32
+ }
33
+
34
+ function isReadOnly(active) {
35
+ return !active || !active.authorized || active.isWorktree || !isCurrentProject(active);
36
+ }
37
+
38
+ function renderActiveState() {
39
+ var active = activeState();
40
+ var pending = pendingState();
41
+ var select = selectElement();
42
+ var inheritedHint = document.getElementById("ps-session-visibility-default-inherited-hint");
43
+ var nonCurrentHint = document.getElementById("ps-session-visibility-default-non-current-hint");
44
+ if (inheritedHint) inheritedHint.classList.toggle("hidden", !(active && active.isWorktree));
45
+ if (nonCurrentHint) nonCurrentHint.classList.toggle("hidden", !(active && active.authorized && !active.isWorktree && !isCurrentProject(active)));
46
+ if (!select || !active) return;
47
+ setSelectState(select, active.effectiveValue, isReadOnly(active) || !!(pending && pending.slug === active.slug));
48
+ }
49
+
50
+ function isAuthorized(project) {
51
+ var users = store.get('cachedAllUsers') || [];
52
+ var userId = store.get('myUserId');
53
+ var currentUser = null;
54
+ for (var i = 0; i < users.length; i++) {
55
+ if (users[i].id === userId) { currentUser = users[i]; break; }
56
+ }
57
+ return !!(store.get('isMultiUserMode') && project &&
58
+ ((project.projectOwnerId && project.projectOwnerId === userId) || (currentUser && currentUser.role === "admin")) &&
59
+ (!store.get('permissions') || store.get('permissions').projectSettings !== false));
60
+ }
61
+
62
+ export function initProjectSessionVisibilitySettings() {
63
+ var select = selectElement();
64
+ if (!select || initialized) return;
65
+ initialized = true;
66
+ select.addEventListener("change", function () {
67
+ var ws = getWs();
68
+ var active = activeState();
69
+ var slug = active && active.slug || "";
70
+ var prior = active && active.effectiveValue || "private";
71
+ if (!slug || !ws || ws.readyState !== 1 || pendingState() || isReadOnly(active)) {
72
+ setSelectState(select, prior, isReadOnly(active) || !!pendingState());
73
+ return;
74
+ }
75
+ requestSequence += 1;
76
+ var requestId = "project-session-visibility-" + Date.now() + "-" + requestSequence;
77
+ setVisibilityState(active, { requestId: requestId, slug: slug, requested: select.value, previous: prior });
78
+ setSelectState(select, select.value, true);
79
+ ws.send(JSON.stringify({ type: "set_project_session_visibility_default", slug: slug, visibility: select.value, requestId: requestId }));
80
+ });
81
+
82
+ store.subscribe(function (state, previous) {
83
+ if (state.currentSlug !== previous.currentSlug) renderActiveState();
84
+ if (state.connected || previous.connected !== true) return;
85
+ var pending = pendingState();
86
+ if (!pending) return;
87
+ var current = selectElement();
88
+ var active = activeState();
89
+ if (current && active && active.slug === pending.slug) setSelectState(current, pending.previous, isReadOnly(active));
90
+ setVisibilityState(active, null);
91
+ renderActiveState();
92
+ });
93
+ }
94
+
95
+ export function populateProjectSessionVisibilitySettings(slug, project) {
96
+ var field = document.getElementById("ps-session-visibility-default-field");
97
+ var select = selectElement();
98
+ if (field) field.style.display = isAuthorized(project) ? "" : "none";
99
+ if (!select) return;
100
+ var value = project && project.sessionVisibilityDefault === "shared" ? "shared" : "private";
101
+ var active = { slug: slug || "", effectiveValue: value, isWorktree: !!(project && project.isWorktree), authorized: isAuthorized(project) };
102
+ var pending = pendingState();
103
+ setVisibilityState(active, pending);
104
+ renderActiveState();
105
+ }
106
+
107
+ export function refreshProjectSessionVisibilitySettings(projects) {
108
+ var select = selectElement();
109
+ if (!select) return;
110
+ var active = activeState();
111
+ var slug = active && active.slug || "";
112
+ var source = Array.isArray(projects) ? projects : [];
113
+ for (var i = 0; i < source.length; i++) {
114
+ if (source[i] && source[i].slug === slug) {
115
+ populateProjectSessionVisibilitySettings(slug, source[i]);
116
+ return;
117
+ }
118
+ }
119
+ }
120
+
121
+ export function handleProjectSessionVisibilityDefault(msg) {
122
+ var pending = pendingState();
123
+ if (!pending || msg.requestId !== pending.requestId || msg.slug !== pending.slug) return;
124
+ var active = activeState();
125
+ setVisibilityState(active, null);
126
+ var select = selectElement();
127
+ if (!select || !active || active.slug !== msg.slug) return;
128
+ if (!msg.ok) {
129
+ active = Object.assign({}, active, { effectiveValue: pending.previous });
130
+ setVisibilityState(active, null);
131
+ setSelectState(select, pending.previous, isReadOnly(active));
132
+ showToast(msg.error || "Failed to update new session visibility", "error");
133
+ return;
134
+ }
135
+ active = Object.assign({}, active, { effectiveValue: msg.visibility === "shared" ? "shared" : "private" });
136
+ setVisibilityState(active, null);
137
+ setSelectState(select, active.effectiveValue, isReadOnly(active));
138
+ }
@@ -4,6 +4,7 @@ import { showToast } from './utils.js';
4
4
  import { parseEmojis } from './markdown.js';
5
5
  import { closeFileViewer } from './filebrowser.js';
6
6
  import { renderModelList, renderModeList, renderEffortBar, renderThinkingBar, renderBetaCard } from './settings-defaults.js';
7
+ import { initProjectSessionVisibilitySettings, populateProjectSessionVisibilitySettings } from './project-session-visibility-settings.js';
7
8
 
8
9
  var ctx = null;
9
10
  var panelEl = null;
@@ -162,6 +163,8 @@ export function initProjectSettings(appCtx, emojiCategories) {
162
163
  hideTransferForm();
163
164
  });
164
165
  }
166
+
167
+ initProjectSessionVisibilitySettings();
165
168
  }
166
169
 
167
170
  // ===== Open / Close =====
@@ -246,6 +249,8 @@ function populateProfile() {
246
249
  // Icon
247
250
  updateIconPreview(currentProject ? currentProject.icon : null);
248
251
 
252
+ populateProjectSessionVisibilitySettings(currentSlug, currentProject);
253
+
249
254
  // Owner (only in multi-user mode)
250
255
  var ownerField = document.getElementById("ps-owner-field");
251
256
  if (ownerField) {
@@ -1,7 +1,7 @@
1
1
  // sidebar-projects.js - Project icon strip, context menus, emoji picker, drag-and-drop, worktree modal
2
2
  // Extracted from sidebar.js (PR-36)
3
3
 
4
- import { escapeHtml } from './utils.js';
4
+ import { escapeHtml, showToast } from './utils.js';
5
5
  import { iconHtml, refreshIcons } from './icons.js';
6
6
  import { openProjectSettings } from './project-settings.js';
7
7
  import { triggerShare } from './qrcode.js';
@@ -415,6 +415,11 @@ function showProjectCtxMenu(anchorEl, slug, name, icon, position) {
415
415
  closeProjectCtxMenu();
416
416
  if (closeUserCtxMenu) closeUserCtxMenu();
417
417
  closeEmojiPicker();
418
+ var projects = getCachedProjects();
419
+ var targetProject = null;
420
+ for (var projectIndex = 0; projectIndex < projects.length; projectIndex++) {
421
+ if (projects[projectIndex].slug === slug) { targetProject = projects[projectIndex]; break; }
422
+ }
418
423
 
419
424
  var menu = document.createElement("div");
420
425
  menu.className = "project-ctx-menu";
@@ -438,7 +443,13 @@ function showProjectCtxMenu(anchorEl, slug, name, icon, position) {
438
443
  settingsItem.addEventListener("click", function (e) {
439
444
  e.stopPropagation();
440
445
  closeProjectCtxMenu();
441
- openProjectSettings(slug, { slug: slug, name: name, icon: icon, projectOwnerId: store.get('currentProjectOwnerId'), ownerLocked: store.get('ownerLocked') });
446
+ if (!targetProject) {
447
+ showToast("Project details are unavailable. Refresh and try again.", "error");
448
+ return;
449
+ }
450
+ openProjectSettings(slug, Object.assign({}, targetProject, {
451
+ name: targetProject.title || targetProject.project || name,
452
+ }));
442
453
  });
443
454
  menu.appendChild(settingsItem);
444
455
  }
@@ -460,7 +471,7 @@ function showProjectCtxMenu(anchorEl, slug, name, icon, position) {
460
471
 
461
472
  // --- Manage Access ---
462
473
  if (store.get('isMultiUserMode')) {
463
- var isProjectOwner = store.get('myUserId') && store.get('currentProjectOwnerId') && store.get('myUserId') === store.get('currentProjectOwnerId');
474
+ var isProjectOwner = store.get('myUserId') && targetProject && targetProject.projectOwnerId && store.get('myUserId') === targetProject.projectOwnerId;
464
475
  var isAdmin = store.get('permissions') && store.get('permissions').projectSettings !== false;
465
476
  if (isProjectOwner || isAdmin) {
466
477
  var accessItem = document.createElement("button");
package/lib/server.js CHANGED
@@ -158,6 +158,7 @@ function createServer(opts) {
158
158
  var onReorderProjects = opts.onReorderProjects || null;
159
159
  var onSetProjectTitle = opts.onSetProjectTitle || null;
160
160
  var onSetProjectIcon = opts.onSetProjectIcon || null;
161
+ var onSetProjectSessionVisibilityDefault = opts.onSetProjectSessionVisibilityDefault || null;
161
162
  var onProjectOwnerChanged = opts.onProjectOwnerChanged || null;
162
163
  var onGetServerDefaultEffort = opts.onGetServerDefaultEffort || null;
163
164
  var onSetServerDefaultEffort = opts.onSetServerDefaultEffort || null;
@@ -1458,6 +1459,7 @@ function createServer(opts) {
1458
1459
  onReorderProjects: onReorderProjects,
1459
1460
  onSetProjectTitle: onSetProjectTitle,
1460
1461
  onSetProjectIcon: onSetProjectIcon,
1462
+ onSetProjectSessionVisibilityDefault: onSetProjectSessionVisibilityDefault,
1461
1463
  onProjectOwnerChanged: onProjectOwnerChanged,
1462
1464
  onGetServerDefaultEffort: onGetServerDefaultEffort,
1463
1465
  onSetServerDefaultEffort: onSetServerDefaultEffort,
@@ -1707,7 +1709,12 @@ function createServer(opts) {
1707
1709
  var visible = [];
1708
1710
  projects.forEach(function (ctx, slug) {
1709
1711
  if (users.isMultiUser() && (!userId || !canUserAccessSlug(userId, slug))) return;
1710
- visible.push(ctx.getStatus());
1712
+ var status = ctx.getStatus();
1713
+ if (!status.isMate && onGetProjectAccess) {
1714
+ var access = onGetProjectAccess(slug);
1715
+ status.sessionVisibilityDefault = access && access.sessionVisibilityDefault === "shared" ? "shared" : "private";
1716
+ }
1717
+ visible.push(status);
1711
1718
  });
1712
1719
  return visible;
1713
1720
  }
@@ -7,6 +7,66 @@ function restore(meta) {
7
7
  return meta.sessionVisibilityExplicit === true ? normalize(meta.sessionVisibility) : "private";
8
8
  }
9
9
 
10
+ function defaultForProject(access) {
11
+ return access && access.sessionVisibilityDefault === "shared" ? "shared" : "private";
12
+ }
13
+
14
+ function resolveNewSessionVisibility(access, msg, isMate) {
15
+ if (isMate === true) return { visibility: "private" };
16
+ if (!msg || !Object.prototype.hasOwnProperty.call(msg, "sessionVisibility")) {
17
+ return { visibility: defaultForProject(access) };
18
+ }
19
+ if (msg.sessionVisibility === "private" || msg.sessionVisibility === "shared") {
20
+ return { visibility: msg.sessionVisibility };
21
+ }
22
+ return { error: "Session visibility must be private or shared." };
23
+ }
24
+
25
+ function defaultChangeResult(ctx, ws, msg, result) {
26
+ var requestedSlug = typeof msg.slug === "string" && msg.slug.length > 0 && msg.slug.length <= 200 ? msg.slug : ctx.slug;
27
+ ctx.sendTo(ws, Object.assign({
28
+ type: "set_project_session_visibility_default_result",
29
+ slug: requestedSlug,
30
+ requestId: typeof msg.requestId === "string" ? msg.requestId.substring(0, 200) : null,
31
+ }, result));
32
+ }
33
+
34
+ function handleDefaultChange(ctx, ws, msg) {
35
+ if (!msg || (msg.visibility !== "shared" && msg.visibility !== "private")) {
36
+ defaultChangeResult(ctx, ws, msg || {}, { ok: false, error: "Session visibility default must be private or shared." });
37
+ return true;
38
+ }
39
+ if (msg.slug && msg.slug !== ctx.slug) {
40
+ defaultChangeResult(ctx, ws, msg, { ok: false, error: "Project settings access is not permitted" });
41
+ return true;
42
+ }
43
+ if (ctx.isMate === true || !ctx.usersModule || !ctx.usersModule.isMultiUser || !ctx.usersModule.isMultiUser()) {
44
+ defaultChangeResult(ctx, ws, msg, { ok: false, error: "Not supported" });
45
+ return true;
46
+ }
47
+ var user = ws && ws._clayUser && ctx.usersModule.findUserById(ws._clayUser.id);
48
+ var access = ctx.getProjectAccess ? ctx.getProjectAccess() : null;
49
+ if (access && access.isWorktree === true) {
50
+ defaultChangeResult(ctx, ws, msg, { ok: false, error: "Worktrees inherit this setting from their parent project." });
51
+ return true;
52
+ }
53
+ var permissions = user && ctx.usersModule.getEffectivePermissions
54
+ ? ctx.usersModule.getEffectivePermissions(user, ctx.osUsers) : {};
55
+ var isOwner = !!(user && access && access.ownerId && access.ownerId === user.id);
56
+ var isAdmin = !!(user && user.role === "admin");
57
+ if (!user || !permissions.projectSettings || (!isOwner && !isAdmin)) {
58
+ defaultChangeResult(ctx, ws, msg, { ok: false, error: "Project settings access is not permitted" });
59
+ return true;
60
+ }
61
+ if (typeof ctx.opts.onSetProjectSessionVisibilityDefault !== "function") {
62
+ defaultChangeResult(ctx, ws, msg, { ok: false, error: "Not supported" });
63
+ return true;
64
+ }
65
+ var result = ctx.opts.onSetProjectSessionVisibilityDefault(ctx.slug, msg.visibility);
66
+ defaultChangeResult(ctx, ws, msg, { ok: !!(result && result.ok), visibility: result && result.visibility, error: result && result.error });
67
+ return true;
68
+ }
69
+
10
70
  function handleChange(ctx, ws, msg) {
11
71
  if (typeof msg.sessionId !== "number" || (msg.visibility !== "shared" && msg.visibility !== "private")) return;
12
72
  var session = ctx.sm.sessions.get(msg.sessionId);
@@ -38,4 +98,5 @@ function revokeViewers(ctx, session) {
38
98
  }
39
99
  }
40
100
 
41
- module.exports = { normalize: normalize, restore: restore, handleChange: handleChange, revokeViewers: revokeViewers };
101
+ module.exports = { normalize: normalize, restore: restore, defaultForProject: defaultForProject, resolveNewSessionVisibility: resolveNewSessionVisibility,
102
+ handleChange: handleChange, handleDefaultChange: handleDefaultChange, revokeViewers: revokeViewers };
package/lib/sessions.js CHANGED
@@ -1100,6 +1100,8 @@ function createSessionManager(opts) {
1100
1100
  lastActivity: Date.now(),
1101
1101
  history: cliHistory,
1102
1102
  messageUUIDs: [],
1103
+ sessionVisibility: sessionVisibility.normalize(opts && opts.sessionVisibility),
1104
+ sessionVisibilityExplicit: !!(opts && opts.sessionVisibilityExplicit === true),
1103
1105
  bookmarked: false,
1104
1106
  favoriteOrder: null,
1105
1107
  };
package/lib/ws-schema.js CHANGED
@@ -27,6 +27,8 @@ var schema = {
27
27
  "reorder_session_bookmarks": { direction: "c2s", handler: "lib/project-sessions.js", description: "Reorder favorited sessions within the favorites area" },
28
28
  "bulk_delete_sessions": { direction: "c2s", handler: "lib/project-sessions.js", description: "Delete a group of sessions at once" },
29
29
  "set_session_visibility": { direction: "c2s", handler: "lib/project-sessions.js", description: "Show or hide a session in the sidebar" },
30
+ "set_project_session_visibility_default": { direction: "c2s", handler: "lib/project-sessions.js", description: "Set private/shared default for new ordinary project sessions" },
31
+ "set_project_session_visibility_default_result": { direction: "s2c", handler: "lib/public/modules/project-session-visibility-settings.js", description: "Correlated project session visibility default update result" },
30
32
  "search_sessions": { direction: "c2s", handler: "lib/project-sessions.js", description: "Search session titles" },
31
33
  "search_session_content": { direction: "c2s", handler: "lib/project-sessions.js", description: "Full-text search within a session" },
32
34
  "load_more_history": { direction: "c2s", handler: "lib/project-sessions.js", description: "Request older history entries for the current session" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "4.1.0",
3
+ "version": "4.2.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"