clay-server 5.2.0-beta.4 → 5.2.0-beta.5

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.
Files changed (171) hide show
  1. package/lib/account-auth-socket.js +1 -0
  2. package/lib/builtin-mates.js +17 -14
  3. package/lib/canonical-clay-record-authority.js +15 -0
  4. package/lib/canonical-clay-scheduling-authority.js +19 -0
  5. package/lib/claude-hook-installer.js +28 -0
  6. package/lib/clay-mate-chat-receipts.js +26 -0
  7. package/lib/clay-mate-chat.js +87 -0
  8. package/lib/clay-mate-knowledge-input.js +26 -0
  9. package/lib/clay-mate-knowledge.js +95 -0
  10. package/lib/clay-mate-lifecycle.js +95 -0
  11. package/lib/clay-product-control-input.js +33 -0
  12. package/lib/clay-product-control-tools.js +51 -0
  13. package/lib/clay-product-control.js +237 -0
  14. package/lib/clay-project-lifecycle.js +154 -0
  15. package/lib/clay-project-record-input.js +44 -0
  16. package/lib/clay-project-records.js +83 -0
  17. package/lib/clay-scheduling-input.js +21 -0
  18. package/lib/clay-scheduling-receipts.js +26 -0
  19. package/lib/clay-scheduling.js +119 -0
  20. package/lib/clay-screen-controls.js +96 -0
  21. package/lib/clay-workspace-controls.js +83 -0
  22. package/lib/clay-workspace-input.js +17 -0
  23. package/lib/clay-workspace-receipts.js +26 -0
  24. package/lib/daemon-project-access.js +11 -0
  25. package/lib/daemon-project-lifecycle-callbacks.js +15 -0
  26. package/lib/daemon-project-lifecycle.js +262 -0
  27. package/lib/daemon-projects.js +1 -1
  28. package/lib/daemon.js +73 -221
  29. package/lib/issues-service.js +29 -15
  30. package/lib/knowledge-db-import.js +5 -4
  31. package/lib/knowledge-db-service.js +9 -6
  32. package/lib/knowledge-document-index.js +2 -1
  33. package/lib/knowledge-product-operations.js +46 -0
  34. package/lib/mate-creation-mcp-server.js +2 -0
  35. package/lib/mate-knowledge-canonical-access.js +10 -0
  36. package/lib/mate-knowledge-service.js +7 -4
  37. package/lib/mate-lifecycle-service.js +94 -0
  38. package/lib/mate-message-delivery.js +43 -0
  39. package/lib/mate-ready-creation.js +9 -1
  40. package/lib/mate-schedule-input.js +29 -0
  41. package/lib/mate-schedule-lifecycle.js +151 -0
  42. package/lib/mate-scheduling.js +103 -0
  43. package/lib/mates.js +4 -3
  44. package/lib/notes-checklist.js +50 -0
  45. package/lib/notes.js +49 -3
  46. package/lib/os-users.js +20 -5
  47. package/lib/project-admin-overview.js +51 -0
  48. package/lib/project-ask-user.js +4 -2
  49. package/lib/project-delegated-follow-up.js +2 -1
  50. package/lib/project-directory-claim.js +65 -0
  51. package/lib/project-file-operations.js +61 -0
  52. package/lib/project-filesystem.js +13 -87
  53. package/lib/project-full-access.js +23 -2
  54. package/lib/project-knowledge.js +32 -18
  55. package/lib/project-lifecycle-operations.js +98 -0
  56. package/lib/project-lifecycle-process-witness.js +32 -0
  57. package/lib/project-lifecycle-runtime.js +50 -0
  58. package/lib/project-linux-access.js +22 -0
  59. package/lib/project-logs-canonical-clay.js +27 -0
  60. package/lib/project-logs-clay-query.js +16 -0
  61. package/lib/project-logs-schema.js +4 -2
  62. package/lib/project-logs-service.js +8 -7
  63. package/lib/project-logs.js +1 -0
  64. package/lib/project-loop.js +1 -0
  65. package/lib/project-mate-chat-control.js +55 -0
  66. package/lib/project-mate-creation-proposal.js +18 -3
  67. package/lib/project-models.js +14 -31
  68. package/lib/project-notifications.js +74 -16
  69. package/lib/project-ownership.js +77 -0
  70. package/lib/project-pair-result-delivery.js +14 -3
  71. package/lib/project-pending-message-queue.js +1 -0
  72. package/lib/project-product-control.js +87 -0
  73. package/lib/project-product-session-queue.js +46 -0
  74. package/lib/project-push-access.js +28 -0
  75. package/lib/project-scheduled-message-queue.js +30 -0
  76. package/lib/project-scheduled-messages.js +63 -64
  77. package/lib/project-scheduled-task-control.js +83 -0
  78. package/lib/project-scheduled-tasks.js +13 -2
  79. package/lib/project-scheduling-access.js +10 -0
  80. package/lib/project-session-actions.js +65 -0
  81. package/lib/project-session-delete.js +8 -1
  82. package/lib/project-session-github.js +47 -12
  83. package/lib/project-session-history-actions.js +165 -0
  84. package/lib/project-session-runtime-actions.js +63 -0
  85. package/lib/project-session-vendor.js +54 -0
  86. package/lib/project-sessions.js +64 -241
  87. package/lib/project-user-message.js +30 -1
  88. package/lib/project-workspace-operations.js +126 -0
  89. package/lib/project-workspace-query.js +9 -2
  90. package/lib/project.js +95 -3
  91. package/lib/public/css/clay-tool-activity.css +51 -0
  92. package/lib/public/css/home-mate-creation.css +5 -0
  93. package/lib/public/css/overlays.css +4 -2
  94. package/lib/public/css/project-admin-overview.css +13 -0
  95. package/lib/public/css/session-folders.css +11 -0
  96. package/lib/public/css/session-github.css +3 -0
  97. package/lib/public/css/sticky-notes.css +61 -1
  98. package/lib/public/index.html +1 -1
  99. package/lib/public/modules/admin-settings-projects.js +6 -2
  100. package/lib/public/modules/app-connection.js +8 -1
  101. package/lib/public/modules/app-message-router.js +1 -1
  102. package/lib/public/modules/app-messages.js +18 -3
  103. package/lib/public/modules/app-projects.js +5 -0
  104. package/lib/public/modules/clay-owner-navigation.js +27 -0
  105. package/lib/public/modules/clay-tool-activity.js +140 -0
  106. package/lib/public/modules/clay-tool-presentation.js +338 -0
  107. package/lib/public/modules/command-palette.js +1 -1
  108. package/lib/public/modules/default-ai.js +11 -3
  109. package/lib/public/modules/home-debate-planning.js +5 -5
  110. package/lib/public/modules/home-mate-chat.js +8 -6
  111. package/lib/public/modules/home-mate-creation.js +73 -1
  112. package/lib/public/modules/personal-settings-dialog.js +7 -1
  113. package/lib/public/modules/project-access-recovery.js +17 -0
  114. package/lib/public/modules/project-admin-overview.js +21 -0
  115. package/lib/public/modules/session-folder-layout.js +1 -1
  116. package/lib/public/modules/session-folder-view-menu.js +9 -0
  117. package/lib/public/modules/session-github.js +65 -8
  118. package/lib/public/modules/sidebar-session-folders-ui.js +1 -1
  119. package/lib/public/modules/sticky-note-markdown.js +72 -6
  120. package/lib/public/modules/sticky-notes-actions.js +30 -0
  121. package/lib/public/modules/sticky-notes-browser.js +9 -2
  122. package/lib/public/modules/sticky-notes-card.js +23 -7
  123. package/lib/public/modules/sticky-notes-checklist.js +71 -0
  124. package/lib/public/modules/sticky-notes-editor.js +108 -44
  125. package/lib/public/modules/sticky-notes-shared.js +50 -2
  126. package/lib/public/modules/sticky-notes-state.js +66 -0
  127. package/lib/public/modules/sticky-notes.js +34 -51
  128. package/lib/public/modules/thinking-lifecycle.js +33 -3
  129. package/lib/public/modules/thinking-view.js +23 -1
  130. package/lib/public/modules/tools.js +34 -6
  131. package/lib/public/modules/user-settings-appearance.js +10 -3
  132. package/lib/public/modules/user-settings-editors.js +1 -1
  133. package/lib/public/modules/user-settings-layout.js +11 -2
  134. package/lib/public/modules/user-settings-preferences.js +6 -2
  135. package/lib/public/modules/user-settings-profile.js +12 -2
  136. package/lib/public/modules/user-settings.js +3 -2
  137. package/lib/public/style.css +2 -0
  138. package/lib/scheduled-task-records.js +3 -1
  139. package/lib/scheduler.js +2 -0
  140. package/lib/sdk-bridge.js +220 -21
  141. package/lib/sdk-message-processor.js +2 -0
  142. package/lib/sdk-response-recovery.js +155 -0
  143. package/lib/server-admin.js +63 -16
  144. package/lib/server-dm.js +1 -1
  145. package/lib/server-global-ws.js +10 -20
  146. package/lib/server-home-chat-events.js +7 -3
  147. package/lib/server-home-chat.js +4 -16
  148. package/lib/server-home-mate-creation.js +1 -0
  149. package/lib/server-home-mcp-consent.js +3 -3
  150. package/lib/server-mates.js +25 -67
  151. package/lib/server-owner-ui.js +77 -0
  152. package/lib/server-path-access.js +38 -0
  153. package/lib/server-project-removal.js +22 -0
  154. package/lib/server.js +99 -29
  155. package/lib/session-config-transition.js +45 -0
  156. package/lib/session-folders.js +7 -2
  157. package/lib/session-github.js +9 -2
  158. package/lib/session-visibility.js +5 -2
  159. package/lib/sessions.js +10 -0
  160. package/lib/terminal-manager.js +52 -0
  161. package/lib/users-permissions.js +2 -8
  162. package/lib/users.js +4 -0
  163. package/lib/workspace-query-access.js +4 -1
  164. package/lib/ws-schema.js +8 -2
  165. package/lib/yoke/adapters/claude.js +47 -8
  166. package/lib/yoke/adapters/codex.js +52 -111
  167. package/lib/yoke/claude-worker-startup.js +28 -0
  168. package/lib/yoke/codex-idle-recovery.js +81 -1
  169. package/lib/yoke/codex-recovery-admission.js +164 -0
  170. package/lib/yoke/session-tool-transport.js +3 -0
  171. package/package.json +1 -1
@@ -0,0 +1,126 @@
1
+ var fs = require('fs'), path = require('path'), crypto = require('crypto');
2
+ var git = require('./git-cli');
3
+ function digest(value) { return crypto.createHash('sha256').update(typeof value === 'string' || Buffer.isBuffer(value) ? value : JSON.stringify(value)).digest('hex'); }
4
+ function inode(file) { var s = fs.statSync(file); return [s.dev, s.ino, s.mode, s.uid, s.gid]; }
5
+ function attachWorkspaceOperations(ctx) {
6
+ var busy = false;
7
+ function authorize(principal, domain) {
8
+ var record = ctx.getAccess(), user = principal.userId && ctx.users.findUserById(principal.userId);
9
+ if (ctx.isMate || !record || record.lifecycleExecutionPaused || ctx.users.isMultiUser() && (!user || record.ownerId !== user.id || !ctx.users.canAccessProject(user.id, record))) throw new Error('Live selected-project ownership or access is unavailable.');
10
+ if (!ctx.users.isMultiUser() && record.ownerId && record.ownerId !== principal.singleUserOwnerId) throw new Error('Selected-project ownership changed.');
11
+ if (user && ctx.users.getEffectivePermissions(user, ctx.osUsers)[domain === 'terminal' ? 'terminal' : 'fileBrowser'] !== true) throw new Error('The account does not have permission for this product operation.');
12
+ var identity = null;
13
+ if (ctx.osUsers) {
14
+ if (!user || !user.linuxUser) throw new Error('A live mapped OS identity is required.');
15
+ identity = require('./os-users').resolveOsUserInfo(user.linuxUser);
16
+ if (!identity || !Number.isInteger(identity.uid) || identity.uid < 0 || !Number.isInteger(identity.gid) || identity.gid < 0 || identity.user !== user.linuxUser) throw new Error('The mapped OS identity is unavailable.');
17
+ }
18
+ return { identity: identity, projectBound: true, ownerId: user ? user.id : null };
19
+ }
20
+ function rootIdentity(principal, domain) {
21
+ var scope = authorize(principal, domain), root = fs.realpathSync(ctx.cwd);
22
+ return digest([root, inode(root), scope.ownerId, scope.identity && [scope.identity.user, scope.identity.uid, scope.identity.gid, scope.identity.home]]);
23
+ }
24
+ function bounded(requested, missing) {
25
+ if (typeof requested !== 'string' || !requested || path.isAbsolute(requested) || /[\u0000-\u001f\u007f]/.test(requested) || requested.split(/[\\/]/).some(function (part) { return part === '..' || ['.git', '.clay', '.claude', '.codex', '.envrc'].indexOf(part) >= 0; })) throw new Error('Use an in-project relative path outside managed settings and Git metadata.');
26
+ var root = fs.realpathSync(ctx.cwd), resolved = root;
27
+ var parts = requested.split('/').filter(function (part) { return part && part !== '.'; });
28
+ for (var i = 0; i < parts.length; i++) {
29
+ resolved = path.join(resolved, parts[i]);
30
+ var stat;
31
+ try { stat = fs.lstatSync(resolved); } catch (error) { if (missing && error.code === 'ENOENT' && i === parts.length - 1) return resolved; throw error; }
32
+ if (stat.isSymbolicLink() || stat.isFile() && stat.nlink !== 1) throw new Error('Symlinks and multiply linked files are unavailable in canonical project controls.');
33
+ }
34
+ if (path.relative(root, resolved).split(path.sep).indexOf('..') >= 0) throw new Error('The canonical project boundary changed.');
35
+ return resolved;
36
+ }
37
+ function repository(scope) {
38
+ var marker = fs.lstatSync(path.join(ctx.cwd, '.git'));
39
+ if (!marker.isDirectory() || marker.isSymbolicLink()) throw new Error('Canonical Git controls require a direct local .git directory; linked metadata and submodules are unavailable.');
40
+ var status = git.getStatus(ctx.cwd, scope.identity);
41
+ if (!status.isRepository || fs.realpathSync(status.root) !== fs.realpathSync(ctx.cwd)) throw new Error('Select the exact registered repository root, rather than a repository subdirectory.');
42
+ if (fs.realpathSync(status.gitDir) !== fs.realpathSync(path.join(ctx.cwd, '.git'))) throw new Error('The Git metadata directory escaped the selected project.');
43
+ return status;
44
+ }
45
+ function gitProjection(status) { return { isRepository: true, branch: status.branch, oid: status.oid, upstream: status.upstream, ahead: status.ahead, behind: status.behind, detached: status.detached, files: status.files.slice(0, 200), truncated: status.files.length > 200, dirty: status.dirty }; }
46
+ function terminal(scope, id) {
47
+ var item = ctx.tm.inspect(id);
48
+ var identity = scope.identity ? JSON.stringify([scope.identity.user, scope.identity.uid, scope.identity.gid, scope.identity.home]) : null;
49
+ if (!item || item.kind !== 'shell' || item.ownerId !== scope.ownerId || item.osIdentity !== identity) throw new Error('Select an exact same-owner shell terminal. Coding-agent, login and foreign terminals are excluded.');
50
+ return item;
51
+ }
52
+ function read(principal, op) {
53
+ var scope = authorize(principal, op.domain), before = rootIdentity(principal, op.domain), result;
54
+ if (op.domain === 'files') {
55
+ bounded(op.path || '.');
56
+ if (op.action === 'list') { var entries = ctx.files().list(scope, op.path || '.'); result = { entries: entries.slice(0, 200), truncated: entries.length > 200 }; }
57
+ else { result = ctx.files().read(scope, op.path); if (result.content && result.content.length > 65536) result = { error: 'Canonical text reads are limited to 64 KB.', size: result.size }; }
58
+ } else if (op.domain === 'git') {
59
+ var status = repository(scope);
60
+ if (op.action === 'status') result = gitProjection(status);
61
+ else { var working = bounded(op.path, true); if (fs.existsSync(working)) ctx.files().read(scope, op.path); result = git.getFileDiff(ctx.cwd, op.path, scope.identity); if ((result.oldContent.length + result.newContent.length) > 65536) throw new Error('Canonical diff reads are limited to 64 KB.'); }
62
+ } else if (op.action === 'list') result = { terminals: ctx.tm.list().filter(function (item) { try { terminal(scope, item.id); return true; } catch (error) { return false; } }).map(function (item) { return ctx.tm.inspect(item.id); }) };
63
+ else { var item = terminal(scope, op.id); result = Object.assign({}, item, ctx.tm.getScrollback(item.id)); }
64
+ if (rootIdentity(principal, op.domain) !== before) throw new Error('Project or OS identity changed during the read.');
65
+ return result;
66
+ }
67
+ function snapshot(principal, op) {
68
+ var scope = authorize(principal, op.domain);
69
+ if (op.domain === 'files') { bounded(op.path); return ctx.files().snapshot(scope, op.path); }
70
+ if (op.domain === 'git') {
71
+ var status = repository(scope);
72
+ if (!op.paths.length || !op.paths.every(function (name) { return status.files.some(function (item) { return item.path === name; }); })) throw new Error('Select exact currently changed Git paths.');
73
+ var entries = op.paths.map(function (name) {
74
+ var working = bounded(name, true);
75
+ if (fs.existsSync(working) && !fs.lstatSync(working).isFile()) throw new Error('Canonical staging requires individual regular file paths; recursive directory staging is unavailable.');
76
+ if (fs.existsSync(working)) ctx.files().read(scope, name);
77
+ var item = status.files.find(function (file) { return file.path === name; });
78
+ if (item.originalPath) bounded(item.originalPath, true);
79
+ var diff = git.getFileDiff(ctx.cwd, name, scope.identity);
80
+ if (diff.binary) throw new Error('Canonical staging of binary files is unavailable.');
81
+ return digest(diff);
82
+ });
83
+ var index = path.join(status.gitDir, 'index');
84
+ return { status: gitProjection(status), gitDirectory: [fs.realpathSync(status.gitDir), inode(status.gitDir)], index: fs.existsSync(index) ? digest(fs.readFileSync(index)) : null, files: entries,
85
+ configuration: digest(git.runGitSync(ctx.cwd, ['config', '--null', '--list', '--show-origin'], null, scope.identity)),
86
+ attributes: digest(git.runGitSync(ctx.cwd, ['check-attr', '-z', '--all', '--'].concat(op.paths), null, scope.identity)) };
87
+ }
88
+ return op.action === 'create' ? ctx.tm.list().map(function (item) { return ctx.tm.inspect(item.id); }) : terminal(scope, op.id);
89
+ }
90
+ async function apply(principal, op, expected, revalidate) {
91
+ if (busy) throw new Error('Another project file, Git or terminal change is applying. Inspect again after it settles.');
92
+ busy = true;
93
+ var attempted = false, result;
94
+ try {
95
+ var scope = authorize(principal, op.domain);
96
+ revalidate();
97
+ if (digest(snapshot(principal, op)) !== digest(expected)) throw new Error('The exact product revision changed.');
98
+ if (op.domain === 'files') return ctx.files().write(scope, op.path, op.content, { expected: expected, revalidate: function () { revalidate(); bounded(op.path); } });
99
+ attempted = true;
100
+ if (op.domain === 'git') {
101
+ await git.runAction(ctx.cwd, { action: op.action, paths: op.paths }, scope.identity);
102
+ result = { status: 'completed', runtimeEffect: 'index_updated', persisted: true };
103
+ } else if (op.action === 'create') {
104
+ var created = ctx.tm.create(op.cols, op.rows, scope.identity, null, { kind: 'shell', ownerId: scope.ownerId });
105
+ if (!created) throw new Error('The terminal transport is unavailable or its limit was reached.');
106
+ result = { status: 'completed', runtimeEffect: 'shell_created', terminalId: created.id, terminalIdentity: created.identity, commandSent: false, persisted: false };
107
+ } else if (op.action === 'rename') {
108
+ ctx.tm.rename(op.id, op.title); result = { status: 'completed', runtimeEffect: 'renamed', terminalId: op.id, terminalIdentity: expected.identity, persisted: false };
109
+ } else if (op.action === 'resize') {
110
+ ctx.tm.resizeExact(op.id, op.cols, op.rows, expected.identity, revalidate);
111
+ result = { status: 'completed', runtimeEffect: 'resized', terminalId: op.id, terminalIdentity: expected.identity, persisted: false };
112
+ } else {
113
+ await ctx.tm.closeAndWait(op.id, { identity: expected.identity, revalidate: function () { revalidate(); var live = terminal(authorize(principal, 'terminal'), op.id); if (live.identity !== expected.identity) throw new Error('Terminal identity changed during closure.'); } });
114
+ result = { status: 'completed', runtimeEffect: 'closed', terminalId: op.id, terminalIdentity: expected.identity, exitAcknowledged: true, persisted: false };
115
+ }
116
+ revalidate();
117
+ if (op.domain === 'terminal') ctx.publishTerminals();
118
+ return result;
119
+ } catch (error) {
120
+ error.productControlResult = Object.assign({ persisted: attempted ? 'unknown' : false, runtimeEffect: attempted ? 'unknown' : 'none', retrySafe: false }, result, error.productControlResult || {}, { status: 'failed', error: error.message });
121
+ throw error;
122
+ } finally { busy = false; }
123
+ }
124
+ return { rootIdentity: rootIdentity, read: read, snapshot: snapshot, apply: apply };
125
+ }
126
+ module.exports = { attachWorkspaceOperations: attachWorkspaceOperations };
@@ -1,4 +1,5 @@
1
1
  var workspaceMcp = require("./workspace-query-mcp-server");
2
+ var productTools = require("./clay-product-control-tools");
2
3
  var clayHistoryMcp = require("./clay-history-mcp-server");
3
4
 
4
5
  function attachProjectWorkspaceQuery(ctx) {
@@ -25,7 +26,12 @@ function attachProjectWorkspaceQuery(ctx) {
25
26
  function getToolDefs(session) {
26
27
  var bound = session ? binding(session) : null;
27
28
  if (session && !bound) return [];
28
- return workspaceMcp.getToolDefs(bound, !!(bound && bound.isClay));
29
+ var tools = workspaceMcp.getToolDefs(bound, !!(bound && bound.isClay));
30
+ if (bound && bound.isClay && ctx.controlService) {
31
+ var controls = ctx.controlService.bind({ projectSlug: projectSlug, projectOwnerId: getProjectOwnerId(), isMate: true, mateId: mateId, session: session });
32
+ if (controls) tools = tools.concat(productTools.getToolDefs(controls));
33
+ }
34
+ return tools;
29
35
  }
30
36
 
31
37
  function createMcpServer(adapter, session) {
@@ -34,7 +40,8 @@ function attachProjectWorkspaceQuery(ctx) {
34
40
  if (session && !bound) return null;
35
41
  var projectBound = binding(null);
36
42
  if (!projectBound) return null;
37
- return workspaceMcp.createMcpServer(adapter, bound, projectBound.isClay);
43
+ if (!adapter || typeof adapter.createToolServer !== "function") return null;
44
+ return adapter.createToolServer({ name: "clay-workspace", version: "2.0.0", tools: session ? getToolDefs(session) : workspaceMcp.getToolDefs(null, projectBound.isClay) });
38
45
  }
39
46
 
40
47
  function createHistoryMcpServer(adapter, session) {
package/lib/project.js CHANGED
@@ -46,6 +46,8 @@ var { attachShellCommand } = require("./project-shell-command");
46
46
  var { attachVendorLogin } = require("./project-vendor-login");
47
47
  var { attachConnection } = require("./project-connection");
48
48
  var { attachProjectScheduledMessages } = require("./project-scheduled-messages");
49
+ var { attachMateScheduling } = require("./mate-scheduling");
50
+ var { attachMateScheduleLifecycle } = require("./mate-schedule-lifecycle");
49
51
  var { attachMcp } = require("./project-mcp");
50
52
  var { createSessionSkillCatalog } = require("./yoke/session-skill-catalog");
51
53
  var { attachEmail: attachEmailModule } = require("./project-email");
@@ -229,6 +231,8 @@ function createProjectContext(opts) {
229
231
  var getScheduleCount = opts.getScheduleCount || function () { return 0; };
230
232
  var notifyProcessingChanged = opts.onProcessingChanged || function () {};
231
233
  var _scheduledMessages = null;
234
+ var _mateScheduling = null;
235
+ var _mateLifecycle = null;
232
236
  var _scheduledTasks = null;
233
237
  var _scheduledTaskExecution = null;
234
238
  function onProcessingChanged() {
@@ -819,6 +823,7 @@ function createProjectContext(opts) {
819
823
  });
820
824
  var _workspaceQuery = attachProjectWorkspaceQuery({
821
825
  service: opts.workspaceQueryService || null,
826
+ controlService: opts.clayProductControl || null,
822
827
  sm: sm,
823
828
  projectSlug: slug,
824
829
  getProjectOwnerId: function () { return projectOwnerId; },
@@ -1047,6 +1052,7 @@ function createProjectContext(opts) {
1047
1052
  persistSession: function (session) { return sm.saveSessionFile(session); },
1048
1053
  getVendorModelCatalog: function (ws, vendor) { return _models.getVendorCatalog(ws, vendor); },
1049
1054
  createReadyMate: opts.createReadyMate,
1055
+ isLive: function (session) { return sm.sessions.get(session.localId) === session; },
1050
1056
  });
1051
1057
  var _askUser = attachAskUser({
1052
1058
  record: function (session, event) { sm.sendAndRecord(session, event); },
@@ -1405,6 +1411,7 @@ function createProjectContext(opts) {
1405
1411
 
1406
1412
  // --- SDK bridge ---
1407
1413
  sdk = createSDKBridge({
1414
+ isLifecycleExecutionPaused: function () { return !!(opts.getProjectAccess && opts.getProjectAccess().lifecycleExecutionPaused); },
1408
1415
  cwd: cwd,
1409
1416
  slug: slug,
1410
1417
  sessionManager: sm,
@@ -1480,6 +1487,7 @@ function createProjectContext(opts) {
1480
1487
  _sessionGithub.getSystemPrompt(session),
1481
1488
  _sessionLinear.getSystemPrompt(session),
1482
1489
  _scheduledTasks ? _scheduledTasks.getSystemPrompt(session) : "",
1490
+ _mateScheduling ? _mateScheduling.getSystemPrompt(session) : "",
1483
1491
  _mateKnowledge.getSystemPrompt(session),
1484
1492
  _userKnowledge.getSystemPrompt(session),
1485
1493
  _autonomousRun ? _autonomousRun.getSystemPrompt(session) : "",
@@ -1509,6 +1517,7 @@ function createProjectContext(opts) {
1509
1517
  .concat(opts.userKnowledgeService ? _userKnowledge.getDynamicToolDefs(session) : [])
1510
1518
  .concat(_loopInterview ? _loopInterview.getToolDefs(session) : [])
1511
1519
  .concat(_scheduledTasks ? _scheduledTasks.getToolDefs(session) : [])
1520
+ .concat(_mateScheduling ? _mateScheduling.getToolDefs(session) : [])
1512
1521
  .concat(_scheduledTaskExecution ? _scheduledTaskExecution.getToolDefs(session) : [])
1513
1522
  .concat(_autonomousRun ? _autonomousRun.getToolDefs(session) : []);
1514
1523
  },
@@ -1564,6 +1573,8 @@ function createProjectContext(opts) {
1564
1573
 
1565
1574
  _scheduledMessages = attachProjectScheduledMessages({
1566
1575
  scheduler: opts.durableScheduler,
1576
+ mateLifecycle: function () { return _mateLifecycle; },
1577
+ deferRehydrate: isMate,
1567
1578
  slug: slug,
1568
1579
  isMate: isMate,
1569
1580
  osUsers: osUsers,
@@ -1586,6 +1597,32 @@ function createProjectContext(opts) {
1586
1597
  consumePendingMessage: function (session) { return _userMessage && _userMessage.consumePendingMessage(session); },
1587
1598
  });
1588
1599
 
1600
+ var _schedulingAccess = require('./project-scheduling-access').attachSchedulingAccess({ osUsers: osUsers, getLinuxUser: getLinuxUserForSession, resolveLinuxUser: getOsUserInfoForLinuxUser });
1601
+ _mateLifecycle = attachMateScheduleLifecycle({
1602
+ sm: sm, scheduler: opts.durableScheduler, slug: slug,
1603
+ owner: function (session) { return _mateScheduling.owner(session); },
1604
+ authorize: function (session, control) { return control ? _mateScheduling.authorizeControl(session, control) : _mateScheduling.authorize(session); },
1605
+ authorizeRuntime: function (session) { return _mateScheduling.authorizeLifecycle(session); },
1606
+ schedule: function () { return _scheduledMessages.schedule.apply(null, arguments); },
1607
+ queueItem: function (session, job) { return _scheduledMessages.queueItem(session, job); },
1608
+ cancelJob: function (session, job) {
1609
+ return _scheduledMessages.cancel(session, { id: job.ownerId }, null, "cancelled_by_mate", { jobId: job.id, revision: job.payload.revision });
1610
+ },
1611
+ });
1612
+ _mateScheduling = attachMateScheduling({
1613
+ isMate: isMate, isHostAgent: isHostAgent, projectOwnerId: projectOwnerId, slug: slug, mateId: opts.mateId,
1614
+ sm: sm, users: usersModule, lifecycle: _mateLifecycle,
1615
+ authorizeAccess: function (session, ownerId) { _schedulingAccess.ownerIdentity(session); return _scheduledMessages.authorize(session, ownerId); },
1616
+ userInput: function (session) { return _askUser.createHandler(session); },
1617
+ });
1618
+ if (isMate) {
1619
+ sm.sessions.forEach(function (session) {
1620
+ try { _mateLifecycle.reconcile(session); }
1621
+ catch (error) { console.error("[mate-schedules] Recovery failed:", error.message); }
1622
+ });
1623
+ _scheduledMessages.rehydrate();
1624
+ }
1625
+
1589
1626
  // --- Loop engine (delegated to project-loop.js) ---
1590
1627
  // --- Notification center (global singleton from server.js) ---
1591
1628
  var _notifications = opts.notificationsModule || null;
@@ -1667,6 +1704,7 @@ function createProjectContext(opts) {
1667
1704
  cwd: cwd,
1668
1705
  projectSlug: slug,
1669
1706
  projectTitle: title || project,
1707
+ validateOwnerIdentity: _schedulingAccess.ownerIdentity,
1670
1708
  isMate: isMate,
1671
1709
  sm: sm,
1672
1710
  sdk: sdk,
@@ -1765,8 +1803,8 @@ function createProjectContext(opts) {
1765
1803
  }
1766
1804
 
1767
1805
  // --- Schedule / cancel a message (used by WS handler and auto-continue) ---
1768
- function scheduleMessage(session, text, resetsAt, actorUser, targetWs, attachments, requestId, transportAccountId) {
1769
- return _scheduledMessages ? _scheduledMessages.schedule(session, text, resetsAt, actorUser, targetWs, attachments, requestId, transportAccountId) : null;
1806
+ function scheduleMessage(session, text, resetsAt, actorUser, targetWs, attachments, requestId, transportAccountId, options) {
1807
+ return _scheduledMessages ? _scheduledMessages.schedule(session, text, resetsAt, actorUser, targetWs, attachments, requestId, transportAccountId, options) : null;
1770
1808
  }
1771
1809
 
1772
1810
  function cancelScheduledMessage(session, actorUser, targetWs, correlation) {
@@ -1800,7 +1838,7 @@ function createProjectContext(opts) {
1800
1838
  }
1801
1839
 
1802
1840
  // --- DM messages (delegated to server-level handler) ---
1803
- if (msg.type === "home_debate_question_response" || msg.type === "home_debate_control" || msg.type === "home_mate_creation_question_response" || msg.type === "default_ai_get" || msg.type === "default_ai_catalog_get" || msg.type === "default_ai_set" || msg.type === "default_vendor_get" || msg.type === "default_vendor_set" || msg.type === "context_view_get" || msg.type === "context_view_set" || msg.type === "cursor_sharing_get" || msg.type === "cursor_sharing_set") {
1841
+ if (msg.type === "home_mate_question_response" || msg.type === "clay_ui_ready" || msg.type === "clay_navigation_claim" || msg.type === "clay_navigation_ack" || msg.type === "home_debate_question_response" || msg.type === "home_debate_control" || msg.type === "home_mate_creation_question_response" || msg.type === "default_ai_get" || msg.type === "default_ai_catalog_get" || msg.type === "default_ai_set" || msg.type === "default_vendor_get" || msg.type === "default_vendor_set" || msg.type === "context_view_get" || msg.type === "context_view_set" || msg.type === "cursor_sharing_get" || msg.type === "cursor_sharing_set") {
1804
1842
  if (typeof opts.onDmMessage === "function") opts.onDmMessage(ws, msg, slug);
1805
1843
  return;
1806
1844
  }
@@ -2096,6 +2134,8 @@ function createProjectContext(opts) {
2096
2134
  });
2097
2135
  sm.setSessionHydrator(_sessionSkillCatalog.hydrate);
2098
2136
  var _sessions = attachSessions({
2137
+ adapters: adapters,
2138
+ isPendingQueuePaused: function (session) { return _pendingMessageQueue.isPaused(session); },
2099
2139
  cwd: cwd,
2100
2140
  slug: slug,
2101
2141
  isMate: isMate,
@@ -2169,6 +2209,26 @@ function createProjectContext(opts) {
2169
2209
  _notifications: _notifications,
2170
2210
  _projectLogs: _projectLogs,
2171
2211
  });
2212
+ var _productControl = require("./project-product-control").attachProjectProductControl({
2213
+ sm: sm, users: usersModule, clients: clients, slug: slug, isMate: isMate, osUsers: osUsers,
2214
+ getProjectAccess: opts.getProjectAccess || function () { return { visibility: "public", ownerId: projectOwnerId || null }; },
2215
+ userInput: function (session) { return _askUser.createHandler(session); },
2216
+ actions: _sessions.sessionActions, history: _sessions.sessionHistoryActions, selectModel: function (session, model, options) { return _models.selectSessionModel(session, model, null, options); },
2217
+ isDriverOperatedSession: function (session) { return _sessionPair.workerPermission.isDriverOperated(session); },
2218
+ deletePlan: _sessions.deleteProductControlPlan,
2219
+ queue: _pendingMessageQueue,
2220
+ requestQueueDrain: function (session) { if (_userMessage) _userMessage.consumePendingMessage(session); },
2221
+ vendor: _sessions.sessionVendorActions,
2222
+ runtime: _sessions.sessionRuntimeActions,
2223
+ });
2224
+ var _mateChatControl = require('./project-mate-chat-control').attachMateChatControl({
2225
+ sm: sm, sdk: sdk, isMate: isMate, osUsers: osUsers, getLinuxUser: getLinuxUserForSession,
2226
+ users: usersModule, getAccess: opts.getProjectAccess || function () { return { isMate: isMate, ownerId: projectOwnerId || null, visibility: 'private' }; },
2227
+ actions: _sessions.sessionActions, vendor: _sessions.sessionVendorActions,
2228
+ selectModel: function (session, model, options) { return _models.selectSessionModel(session, model, null, options); },
2229
+ });
2230
+
2231
+
2172
2232
 
2173
2233
  var _models = attachModels({
2174
2234
  cwd: cwd,
@@ -2215,6 +2275,7 @@ function createProjectContext(opts) {
2215
2275
  });
2216
2276
  var _delegatedFollowUp = attachDelegatedFollowUp({
2217
2277
  sm: sm,
2278
+ isPendingQueuePaused: function (session) { return _pendingMessageQueue.isPaused(session); },
2218
2279
  getSdk: function () { return sdk; },
2219
2280
  getLinuxUserForSession: getLinuxUserForSession,
2220
2281
  onProcessingChanged: onProcessingChanged,
@@ -2229,6 +2290,12 @@ function createProjectContext(opts) {
2229
2290
 
2230
2291
  // --- User message handler (delegated to project-user-message.js) ---
2231
2292
  var _userMessage = attachUserMessage({
2293
+ isLifecycleExecutionPaused: function () { return !!(opts.getProjectAccess && opts.getProjectAccess().lifecycleExecutionPaused); },
2294
+ resumeLifecycleProject: function (ws) {
2295
+ if (!opts.getProjectAccess || !opts.getProjectAccess().lifecycleExecutionPaused) return true;
2296
+ if (!clients.has(ws) || !ws._clayUser || !opts.resumeLifecycleProject) throw new Error('The current owner must explicitly resume recovered project execution.');
2297
+ return opts.resumeLifecycleProject(ws._clayUser.id);
2298
+ },
2232
2299
  // Exact live pair state decides whether a session is Driver-operated; the
2233
2300
  // handler never infers the role from the message.
2234
2301
  isDriverOperatedSession: function (session) {
@@ -2351,6 +2418,13 @@ function createProjectContext(opts) {
2351
2418
  FS_MAX_SIZE: FS_VIEWER_MAX_SIZE,
2352
2419
  });
2353
2420
 
2421
+ var _workspaceOperations = require('./project-workspace-operations').attachWorkspaceOperations({
2422
+ cwd: cwd, isMate: isMate, osUsers: osUsers, users: usersModule, tm: tm,
2423
+ getAccess: opts.getProjectAccess || function () { return { ownerId: projectOwnerId || null, visibility: 'private' }; },
2424
+ files: function () { return _filesystem.productOperations; },
2425
+ publishTerminals: function () { send({ type: 'term_list', terminals: tm.list() }); },
2426
+ });
2427
+
2354
2428
  // --- MCP bridge handler for Codex and session-bound Kiro tools ---
2355
2429
  // Provides list_tools and call_tool operations over HTTP for mcp-bridge-server.js.
2356
2430
  // The normal Codex bridge excludes local MCP servers it manages natively;
@@ -2780,6 +2854,7 @@ function createProjectContext(opts) {
2780
2854
  return false;
2781
2855
  }));
2782
2856
  } catch (err) {
2857
+ shutdowns.push(Promise.resolve(false));
2783
2858
  console.error("[project] " + vendor + " shutdown threw for " + slug + ":", err && err.message ? err.message : err);
2784
2859
  }
2785
2860
  });
@@ -2923,8 +2998,24 @@ function createProjectContext(opts) {
2923
2998
  },
2924
2999
  getYouLinuxUser: getLinuxUserForSession,
2925
3000
  getStatus: getStatus,
3001
+ bindMateSchedule: function (session, intent, id) {
3002
+ return _mateLifecycle.create(session, require("./mate-schedule-input").normalize(intent), id);
3003
+ },
3004
+ cancelMateSchedule: function (session, record) { return _mateLifecycle.cancel(session, record); },
2926
3005
  getSessionManager: function () { return sm; },
2927
3006
  createDelegatedSession: _delegatedSession.createDelegatedSession,
3007
+ refreshEnvironmentRuntime: function () { return sdk.refreshEnvironmentRuntime(); },
3008
+ getWorkspaceProductOperations: function () { return isMate ? null : _workspaceOperations; },
3009
+ getMateChatControl: function () { return isMate ? _mateChatControl : null; },
3010
+ getMateScheduleControl: function () { return isMate ? _mateLifecycle : null; },
3011
+ getScheduledTaskControl: function () { return _scheduledTasks.productControl; },
3012
+ notifyIssueUpdate: _issues.notify,
3013
+ notifyLogUpdate: _projectLogs.broadcastUpdate,
3014
+ requestProductControlReview: _productControl.requestProductControlReview,
3015
+ applyProductSessionAction: _productControl.applyProductSessionAction,
3016
+ inspectProductSessionQueue: _productControl.inspectProductSessionQueue,
3017
+ inspectProductSessionRuntime: _sessions.sessionRuntimeActions.inspect,
3018
+ inspectMateLifecycleRuntime: _sessions.sessionRuntimeActions.inspectLifecycle,
2928
3019
  inspectDelegatedFollowUp: _delegatedFollowUp.inspectDelegatedFollowUp,
2929
3020
  dispatchDelegatedFollowUp: _delegatedFollowUp.dispatchDelegatedFollowUp,
2930
3021
  deliverCapsuleTurn: _capsuleTurn.deliverCapsuleTurn,
@@ -2941,6 +3032,7 @@ function createProjectContext(opts) {
2941
3032
  getMemoryState: _memory.readMemoryState,
2942
3033
  listKnowledgeFiles: _knowledge.listKnowledgeFiles,
2943
3034
  getKnowledgeDbService: function () { return _knowledge.databases; },
3035
+ getKnowledgeProductOperations: function () { return _knowledge.productOperations; },
2944
3036
  refreshCapsuleCatalog: _capsuleCatalog.refreshSessions,
2945
3037
  getNotificationsModule: function () { return _notifications; },
2946
3038
  refreshVendorAuth: _vendorLogin.refreshVendorAuth,
@@ -0,0 +1,51 @@
1
+ /* Keep internal activity in the established compact tool disclosure rows. */
2
+ .clay-tool-activity .tool-name {
3
+ flex-shrink: 1;
4
+ min-width: 0;
5
+ overflow: hidden;
6
+ text-overflow: ellipsis;
7
+ white-space: nowrap;
8
+ }
9
+ .clay-tool-activity .tool-desc {
10
+ font-family: var(--font-ui);
11
+ flex: 0 1 auto;
12
+ }
13
+ .clay-tool-activity .tool-status-icon { margin-left: auto; }
14
+ .clay-tool-activity .tool-subtitle { display: none; }
15
+ .tool-header[role="button"]:focus-visible,
16
+ .tool-group-header[role="button"]:focus-visible {
17
+ outline: 2px solid var(--accent);
18
+ outline-offset: 2px;
19
+ }
20
+ .clay-tool-activity[data-activity-phase="running"] .tool-bullet {
21
+ animation: pulse 1.5s ease-in-out infinite;
22
+ }
23
+ .clay-tool-activity[data-activity-phase="pending"] .tool-bullet,
24
+ .tool-group[data-activity-phase="pending"] .tool-group-bullet {
25
+ background: var(--text-muted);
26
+ animation: none;
27
+ }
28
+ .clay-tool-activity[data-activity-phase="error"] .tool-name,
29
+ .clay-tool-activity[data-activity-phase="cancelled"] .tool-name,
30
+ .tool-group[data-activity-phase="error"] .tool-group-label { color: var(--error); }
31
+ .clay-tool-technical-name,
32
+ .clay-tool-evidence-label {
33
+ padding: 8px 14px 0;
34
+ color: var(--text-muted);
35
+ font-size: 12px;
36
+ overflow-wrap: anywhere;
37
+ }
38
+ .clay-tool-technical-name { font-family: "Roboto Mono", monospace; }
39
+ .clay-tool-evidence-label { font-weight: 600; }
40
+ @media (prefers-reduced-motion: reduce) {
41
+ .clay-tool-activity .tool-bullet { animation: none; }
42
+ }
43
+
44
+ @media (max-width: 600px) {
45
+ .clay-tool-activity .tool-name {
46
+ white-space: normal;
47
+ overflow-wrap: anywhere;
48
+ line-height: 1.4;
49
+ }
50
+ .clay-tool-activity .tool-desc { max-width: 35%; }
51
+ }
@@ -32,3 +32,8 @@
32
32
  .home-mate-proposal-facts dd + dt { margin-top: 7px; }
33
33
  .home-mate-proposal-actions button { flex: 1; }
34
34
  }
35
+
36
+ .home-mate-proposal-schedule-details { margin-top: 2px; color: var(--text-dimmer); }
37
+ .home-mate-proposal-schedule-details summary { width: fit-content; padding-block: 3px; cursor: pointer; font-weight: 400; }
38
+ .home-mate-proposal-schedule-details summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
39
+ .home-mate-proposal-schedule-details[open] > div { padding-top: 3px; color: var(--text-muted); }
@@ -1360,8 +1360,10 @@ button.top-bar-pill.pill-accent:hover { background: color-mix(in srgb, var(--acc
1360
1360
  .connect-wordmark {
1361
1361
  display: block;
1362
1362
  width: clamp(142px, 15vw, 184px);
1363
- height: auto;
1364
- filter: brightness(0);
1363
+ aspect-ratio: 4742 / 1156;
1364
+ background: var(--text-muted);
1365
+ -webkit-mask: url("../clay-studio-wordmark-bold.svg") center / contain no-repeat;
1366
+ mask: url("../clay-studio-wordmark-bold.svg") center / contain no-repeat;
1365
1367
  }
1366
1368
 
1367
1369
  #connect-overlay-msg {
@@ -0,0 +1,13 @@
1
+ .project-admin-overview { overflow: auto; padding: 8px; }
2
+ .project-admin-canvas { width: 100%; min-height: 100%; padding: 24px; border: 1px solid var(--border); border-radius: 8px; background: var(--bg); }
3
+ .project-admin-canvas h1 { font-size: 20px; margin: 0 0 16px; }
4
+ .project-admin-canvas h2 { font-size: 16px; }
5
+ .project-admin-canvas [role="tablist"] { display: flex; gap: 4px; border-bottom: 1px solid var(--border); padding-bottom: 8px; }
6
+ .project-admin-canvas [role="tab"] { height: 32px; padding: 0 12px; border: 1px solid var(--border); border-radius: 8px; color: inherit; background: transparent; cursor: pointer; }
7
+ .project-admin-canvas [aria-selected="true"] { background: #5857fc; color: white; }
8
+ .project-admin-canvas :focus-visible { outline: 2px solid #5857fc; outline-offset: 2px; }
9
+ .project-admin-canvas dl { display: grid; grid-template-columns: 110px 1fr; gap: 12px; margin: 24px 0; font-size: 13px; }
10
+ .project-admin-canvas dd { margin: 0; }
11
+ .project-admin-canvas p { max-width: 640px; line-height: 1.6; font-size: 14px; }
12
+ .project-admin-secondary { opacity: .65; }
13
+ @media (max-width: 600px) { .project-admin-canvas { padding: 16px; } }
@@ -158,6 +158,16 @@
158
158
  .session-folder-viewmenu .session-folder-radio-dot { width: 12px; height: 12px; }
159
159
  .session-folder-viewmenu .session-folder-radio-legend { margin: 2px 0 1px 6px; font-size: 10px; text-transform: uppercase; letter-spacing: 0.4px; }
160
160
  .session-folder-viewmenu .session-folder-view-note { padding: 2px 6px 4px; font-size: 11px; line-height: 1.35; }
161
+ .session-folder-view-toggle {
162
+ display: flex; align-items: center; justify-content: space-between; gap: 10px; width: 100%; min-height: 30px; padding: 0 6px;
163
+ border: 0; border-radius: 6px; background: transparent; color: var(--text); font: inherit; font-size: 12.5px; text-align: left; cursor: pointer;
164
+ }
165
+ .session-folder-view-toggle:hover { background: rgba(var(--overlay-rgb), 0.08); }
166
+ .session-folder-view-toggle-track { position: relative; flex: none; width: 28px; height: 16px; border-radius: 999px; background: var(--text-dimmer); transition: background 0.15s ease; }
167
+ .session-folder-view-toggle-thumb { position: absolute; top: 2px; left: 2px; width: 12px; height: 12px; border-radius: 50%; background: var(--sidebar-bg); transition: transform 0.15s ease; }
168
+ .session-folder-view-toggle[aria-checked="true"] .session-folder-view-toggle-track { background: var(--accent); }
169
+ .session-folder-view-toggle[aria-checked="true"] .session-folder-view-toggle-thumb { transform: translateX(12px); }
170
+ .session-folder-view-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
161
171
  .session-folder-viewmenu .session-folder-radio-group.segmented {
162
172
  display: flex; gap: 2px; padding: 2px; margin-top: 2px; border-radius: 8px; background: rgba(var(--overlay-rgb), 0.05);
163
173
  }
@@ -167,6 +177,7 @@
167
177
  .session-folder-viewmenu.is-mobile { padding: 6px; }
168
178
  .session-folder-viewmenu.is-mobile .session-folder-radio { min-height: 40px; font-size: 14px; }
169
179
  .session-folder-viewmenu.is-mobile .session-folder-radio-dot { width: 14px; height: 14px; }
180
+ .session-folder-viewmenu.is-mobile .session-folder-view-toggle { min-height: 40px; font-size: 14px; }
170
181
  @media (pointer: coarse) {
171
182
  .session-folder-viewmenu .session-folder-radio { min-height: 40px; font-size: 14px; }
172
183
  }
@@ -168,6 +168,9 @@
168
168
  .session-item:has(.session-work-column) > .session-work-links-slot { grid-column: 1 / 3; grid-row: 2; }
169
169
 
170
170
  .github-kind-pr.github-state-closed .github-work-status svg { color: #f85149; }
171
+ .github-kind-pr.github-state-open .github-review-changes svg { color: var(--error); }
172
+ .github-kind-pr.github-state-open .github-review-requested svg { color: var(--warning); }
173
+ .github-kind-pr.github-state-open .github-review-approved svg { color: var(--success); }
171
174
  @media (max-width: 600px) {
172
175
  #header-github-links { flex: 0 0 auto; }
173
176
  #header-github-links .session-github-link { display: none; }
@@ -514,6 +514,20 @@
514
514
  background: color-mix(in srgb, var(--note-ink) 12%, transparent) !important;
515
515
  border-radius: 4px;
516
516
  }
517
+ .sticky-note-checklist-btn.active {
518
+ opacity: 1 !important;
519
+ background: color-mix(in srgb, var(--note-ink) 12%, transparent) !important;
520
+ border-radius: 4px;
521
+ }
522
+ .sticky-note-checklist-btn .lucide { width: 13px; height: 13px; }
523
+ .sticky-note-save-status {
524
+ color: var(--danger, #b42318);
525
+ font-size: 10px;
526
+ max-width: 90px;
527
+ overflow: hidden;
528
+ text-overflow: ellipsis;
529
+ white-space: nowrap;
530
+ }
517
531
 
518
532
  .sn-fmt-bold { font-weight: 800; }
519
533
  .sn-fmt-italic { font-style: italic; }
@@ -692,6 +706,7 @@
692
706
  overflow-y: auto;
693
707
  display: grid;
694
708
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
709
+ grid-auto-rows: max-content;
695
710
  gap: 10px;
696
711
  padding: 12px 12px 28px;
697
712
  align-content: start;
@@ -715,8 +730,23 @@
715
730
  .notes-browser-card:hover { box-shadow: 0 2px 10px rgba(var(--shadow-rgb), 0.12); }
716
731
  .notes-browser-card:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
717
732
  /* A closed note is calm, not crossed out: still fully readable. */
718
- .notes-browser-card.closed { opacity: 0.72; cursor: default; }
733
+ .notes-browser-card.closed {
734
+ min-height: 150px;
735
+ align-self: start;
736
+ opacity: 0.88;
737
+ cursor: default;
738
+ }
719
739
  .notes-browser-card.closed:hover { box-shadow: none; }
740
+ .notes-browser-card.closed .notes-browser-card-header {
741
+ display: flex;
742
+ flex-wrap: wrap;
743
+ align-items: start;
744
+ }
745
+ .notes-browser-card.closed .notes-browser-card-title {
746
+ flex: 0 0 100%;
747
+ display: block;
748
+ overflow-wrap: anywhere;
749
+ }
720
750
 
721
751
  .notes-browser-card-title { color: var(--note-ink); }
722
752
  .notes-browser-card-body { color: var(--note-ink); }
@@ -791,6 +821,23 @@
791
821
  -webkit-line-clamp: 5;
792
822
  -webkit-box-orient: vertical;
793
823
  }
824
+ .notes-browser-card.closed .notes-browser-card-body {
825
+ flex: 0 0 auto;
826
+ display: block;
827
+ min-width: 0;
828
+ overflow: visible;
829
+ overflow-wrap: anywhere;
830
+ word-break: break-word;
831
+ }
832
+ .notes-browser-card.closed .notes-browser-card-body a,
833
+ .notes-browser-card.closed .notes-browser-card-body code,
834
+ .notes-browser-card.closed .notes-browser-card-body pre {
835
+ overflow-wrap: anywhere;
836
+ word-break: break-word;
837
+ }
838
+ .notes-browser-card.closed .notes-browser-card-body pre {
839
+ white-space: pre-wrap;
840
+ }
794
841
  .notes-browser-card-body code {
795
842
  padding: 1px 3px;
796
843
  border-radius: 3px;
@@ -879,3 +926,16 @@
879
926
  .sticky-note.sticky-note-flight-away,
880
927
  .sticky-note:not(.sticky-note-flight-away) { transition-duration: 0ms; }
881
928
  }
929
+
930
+ /* Checklist inputs retain the card's compact reading rhythm. */
931
+ .sn-task-form { display: grid; gap: 6px; }
932
+ .sn-task-row { display: flex; align-items: center; gap: 6px; }
933
+ .sn-task-form input { min-width: 0; width: 100%; box-sizing: border-box; border: 1px solid transparent; border-radius: 4px; background: transparent; color: inherit; font: inherit; padding: 3px 4px; }
934
+ .sn-task-form input:hover, .sn-task-form input:focus { border-color: color-mix(in srgb, var(--note-ink) 25%, transparent); }
935
+ .sn-task-title { font-weight: 600 !important; }
936
+ .sn-task-form button { color: inherit; background: transparent; border: 0; font: inherit; cursor: pointer; }
937
+ .sn-task-form .sn-check { flex-shrink: 0; }
938
+ .sn-task-form .sn-task-save { justify-self: start; padding: 4px 8px; border: 1px solid color-mix(in srgb, var(--note-ink) 25%, transparent); border-radius: 4px; }
939
+ .sn-task-form button:focus-visible, .sn-task-form input:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
940
+ .sticky-note-header .sticky-note-md-btn[hidden] { display: none !important; }
941
+ .sn-task-remove .lucide { width: 13px; height: 13px; }
@@ -332,7 +332,7 @@ body { background: transparent; color: inherit; }
332
332
  <div id="connect-overlay">
333
333
  <div class="connect-brand">
334
334
  <img class="connect-symbol" src="clay-studio-symbol.png" alt="">
335
- <img class="connect-wordmark" src="clay-studio-wordmark-bold.svg" alt="Clay Studio" width="4742" height="1156">
335
+ <span class="connect-wordmark" role="img" aria-label="Clay Studio"></span>
336
336
  </div>
337
337
  <p id="connect-overlay-msg">Connecting…</p>
338
338
  </div>
@@ -16,7 +16,7 @@ export function loadProjectsTab(body) {
16
16
 
17
17
  var accessPromises = projectList.map(function (p) {
18
18
  return apiGet("/api/admin/projects/" + p.slug + "/access").then(function (access) {
19
- return { slug: p.slug, title: p.title || p.project || p.slug, projectOwnerId: p.projectOwnerId || null, visibility: access.visibility || "public", allowedUsers: access.allowedUsers || [] };
19
+ return { slug: p.slug, title: p.title || p.project || p.slug, projectOwnerId: p.projectOwnerId || null, visibility: access.visibility || "public", allowedUsers: access.allowedUsers || [], readOnly: access.contentAccess === false || p.projectOwnerId !== store.get("myUserId") };
20
20
  });
21
21
  });
22
22
 
@@ -39,9 +39,13 @@ function renderProjectsTab(body, projectAccessList, allUsers) {
39
39
  html += '<div class="admin-project-slug">' + escapeHtml(p.slug) + '</div>';
40
40
  html += '</div>';
41
41
  html += '<div class="admin-project-controls">';
42
+ if (p.readOnly) {
43
+ html += '<span class="admin-project-access-readonly">Administrative metadata only</span></div></div>';
44
+ continue;
45
+ }
42
46
  // Owner select
43
47
  html += '<select aria-label="Project owner" class="admin-owner-select" data-slug="' + escapeHtml(p.slug) + '">';
44
- html += '<option value=""' + (!p.projectOwnerId ? ' selected' : '') + '>No owner</option>';
48
+ if (!p.projectOwnerId) html += '<option value="" selected disabled>Ownership needs recovery</option>';
45
49
  for (var u = 0; u < allUsers.length; u++) {
46
50
  var user = allUsers[u];
47
51
  var sel = p.projectOwnerId === user.id ? ' selected' : '';