clay-server 5.2.0-beta.8 → 5.2.0

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 (143) hide show
  1. package/README.md +0 -4
  2. package/lib/clay-product-control-tools.js +1 -0
  3. package/lib/clay-product-control.js +14 -11
  4. package/lib/clay-vendor-usage.js +44 -0
  5. package/lib/github-star-count.js +147 -0
  6. package/lib/notes-checklist.js +43 -1
  7. package/lib/notes.js +2 -1
  8. package/lib/pane-pending-interactions.js +20 -0
  9. package/lib/pane-session-activation.js +114 -0
  10. package/lib/pane-session-transport.js +26 -0
  11. package/lib/project-session-notes.js +33 -8
  12. package/lib/project-sessions.js +78 -29
  13. package/lib/project-start-protocol.js +29 -0
  14. package/lib/project-vendor-login.js +2 -0
  15. package/lib/project.js +16 -4
  16. package/lib/public/app.js +41 -18
  17. package/lib/public/css/contextual-clay.css +2 -2
  18. package/lib/public/css/filebrowser.css +1 -1
  19. package/lib/public/css/input.css +6 -0
  20. package/lib/public/css/mate-loading-scene.css +21 -0
  21. package/lib/public/css/overlays.css +64 -256
  22. package/lib/public/css/project-mate-creation.css +5 -6
  23. package/lib/public/css/project-start.css +108 -0
  24. package/lib/public/css/projects-hub.css +53 -14
  25. package/lib/public/css/settings-shell.css +103 -0
  26. package/lib/public/css/sidebar.css +7 -0
  27. package/lib/public/css/sticky-notes.css +37 -11
  28. package/lib/public/css/title-bar.css +83 -0
  29. package/lib/public/index.html +50 -63
  30. package/lib/public/modules/account-transport-settings.js +8 -2
  31. package/lib/public/modules/admin-settings-email.js +7 -2
  32. package/lib/public/modules/admin-settings-invitations.js +2 -2
  33. package/lib/public/modules/admin-settings-projects.js +2 -2
  34. package/lib/public/modules/admin-settings-ui.js +20 -4
  35. package/lib/public/modules/admin-settings-users.js +4 -4
  36. package/lib/public/modules/app-connection.js +3 -0
  37. package/lib/public/modules/app-home-hub.js +2 -1
  38. package/lib/public/modules/app-message-router.js +13 -0
  39. package/lib/public/modules/app-messages.js +35 -7
  40. package/lib/public/modules/app-misc.js +3 -1
  41. package/lib/public/modules/app-projects.js +6 -407
  42. package/lib/public/modules/app-rate-limit.js +12 -1
  43. package/lib/public/modules/clay-owner-navigation.js +0 -3
  44. package/lib/public/modules/clay-studio-render.js +3 -1
  45. package/lib/public/modules/clay-tool-presentation.js +1 -0
  46. package/lib/public/modules/command-palette.js +52 -14
  47. package/lib/public/modules/contextual-clay-workspace.js +14 -2
  48. package/lib/public/modules/contextual-clay.js +19 -4
  49. package/lib/public/modules/contextual-mate-context.js +6 -4
  50. package/lib/public/modules/elicitation-form.js +5 -0
  51. package/lib/public/modules/github-star.js +66 -0
  52. package/lib/public/modules/history-replay-batch.js +8 -0
  53. package/lib/public/modules/home-surface-boot.js +0 -1
  54. package/lib/public/modules/input.js +55 -11
  55. package/lib/public/modules/mate-loading-scene.js +17 -0
  56. package/lib/public/modules/message-delivery.js +10 -2
  57. package/lib/public/modules/model-picker.js +2 -0
  58. package/lib/public/modules/pane-activation.js +214 -0
  59. package/lib/public/modules/pane-appearance-bridge.js +59 -0
  60. package/lib/public/modules/pane-binding.js +82 -0
  61. package/lib/public/modules/pane-bridge.js +5 -2
  62. package/lib/public/modules/pane-browser-bridge.js +5 -3
  63. package/lib/public/modules/pane-file-bridge.js +4 -0
  64. package/lib/public/modules/pane-issue-bridge.js +4 -1
  65. package/lib/public/modules/profile.js +11 -3
  66. package/lib/public/modules/project-mate-creation-workspace.js +13 -4
  67. package/lib/public/modules/project-session-visibility-settings.js +19 -16
  68. package/lib/public/modules/project-settings-environment.js +12 -1
  69. package/lib/public/modules/project-settings.js +119 -129
  70. package/lib/public/modules/project-start-dialog.js +130 -0
  71. package/lib/public/modules/project-start-folder-state.js +78 -0
  72. package/lib/public/modules/project-start-folder-view.js +190 -0
  73. package/lib/public/modules/project-start-forms.js +66 -0
  74. package/lib/public/modules/project-start-path.js +60 -0
  75. package/lib/public/modules/project-start-state.js +54 -0
  76. package/lib/public/modules/project-start-submit.js +67 -0
  77. package/lib/public/modules/projects-hub-view.js +32 -44
  78. package/lib/public/modules/projects-hub.js +39 -28
  79. package/lib/public/modules/rewind.js +5 -0
  80. package/lib/public/modules/server-settings.js +36 -91
  81. package/lib/public/modules/session-actions.js +5 -2
  82. package/lib/public/modules/session-context-menu.js +5 -0
  83. package/lib/public/modules/session-rich-drafts.js +21 -0
  84. package/lib/public/modules/session-switch-defer.js +67 -0
  85. package/lib/public/modules/settings-project-target.js +56 -0
  86. package/lib/public/modules/settings-select.js +1 -1
  87. package/lib/public/modules/settings-shell-registry.js +88 -0
  88. package/lib/public/modules/settings-shell-view.js +104 -0
  89. package/lib/public/modules/settings-shell.js +342 -0
  90. package/lib/public/modules/sidebar-mobile.js +8 -30
  91. package/lib/public/modules/sidebar-projects.js +2 -1
  92. package/lib/public/modules/split-group-restore.js +68 -0
  93. package/lib/public/modules/split-pair-ui.js +1 -0
  94. package/lib/public/modules/split-pane-reconciler.js +15 -0
  95. package/lib/public/modules/split-pane-renderer.js +87 -6
  96. package/lib/public/modules/split-session-boundary.js +1 -1
  97. package/lib/public/modules/split-view.js +51 -52
  98. package/lib/public/modules/sticky-notes-checklist.js +51 -6
  99. package/lib/public/modules/stt-controller.js +4 -1
  100. package/lib/public/modules/stt-recording.js +1 -1
  101. package/lib/public/modules/theme.js +31 -3
  102. package/lib/public/modules/tools.js +18 -2
  103. package/lib/public/modules/user-settings-integrations.js +2 -1
  104. package/lib/public/modules/user-settings-layout.js +12 -34
  105. package/lib/public/modules/user-settings-profile.js +1 -1
  106. package/lib/public/modules/user-settings.js +18 -85
  107. package/lib/public/modules/vendor-usage-format.js +93 -0
  108. package/lib/public/modules/vendor-usage-list.js +45 -0
  109. package/lib/public/modules/vendor-usage-model.js +215 -0
  110. package/lib/public/modules/vendor-usage-overview.js +323 -0
  111. package/lib/public/style.css +10 -7
  112. package/lib/public/welcome-art/conversation-desk.png +0 -0
  113. package/lib/public/welcome-art/mate-clay.png +0 -0
  114. package/lib/public/welcome-art/project-clay.png +0 -0
  115. package/lib/sdk-bridge.js +6 -1
  116. package/lib/sdk-message-processor.js +9 -1
  117. package/lib/sdk-quota-snapshot.js +20 -2
  118. package/lib/server-github-stars.js +39 -0
  119. package/lib/server-global-ws.js +21 -17
  120. package/lib/server.js +11 -0
  121. package/lib/session-notes-mcp-server.js +26 -4
  122. package/lib/session-notes-writes.js +88 -0
  123. package/lib/session-visibility.js +20 -8
  124. package/lib/sessions.js +27 -10
  125. package/lib/vendor-usage-acp-readers.js +97 -0
  126. package/lib/vendor-usage-capabilities.js +88 -0
  127. package/lib/vendor-usage-http-readers.js +214 -0
  128. package/lib/vendor-usage-metrics.js +47 -0
  129. package/lib/vendor-usage-native-readers.js +106 -0
  130. package/lib/vendor-usage-overview.js +406 -0
  131. package/lib/vendor-usage-service.js +281 -0
  132. package/lib/ws-schema.js +4 -2
  133. package/lib/yoke/adapters/claude.js +12 -0
  134. package/lib/yoke/adapters/codex-query-contract.js +1 -0
  135. package/lib/yoke/adapters/codex.js +45 -12
  136. package/lib/yoke/claude-hook-installer.js +1 -0
  137. package/lib/yoke/codex-dynamic-tool-names.js +80 -0
  138. package/lib/yoke/codex-model-catalog.js +37 -3
  139. package/lib/yoke/muse-services.js +16 -3
  140. package/lib/yoke/providers/claude-model-descriptions.js +22 -2
  141. package/lib/yoke/vendor-registry.js +6 -2
  142. package/package.json +5 -3
  143. package/lib/public/modules/server-settings-navigation.js +0 -39
@@ -1,4 +1,3 @@
1
- import { escapeHtml } from './utils.js';
2
1
  import { iconHtml } from './icons.js';
3
2
 
4
3
  export function ordinaryProjects(projects) {
@@ -7,55 +6,44 @@ export function ordinaryProjects(projects) {
7
6
  });
8
7
  }
9
8
 
10
- function button(action, label, extra) {
11
- return '<button type="button" class="projects-button ' + (extra || '') + '" data-projects-action="' + action + '">' + label + '</button>';
9
+ // Zero ordinary projects (once the list is known): the welcome renders inside the normal app shell.
10
+ export function isShellWelcome(state) {
11
+ return !!state.projectListLoaded && ordinaryProjects(state.projectsHubList).length === 0;
12
12
  }
13
13
 
14
- function setupAction(mode, icon, title, text, disabled) {
15
- return '<button type="button" class="projects-setup-action" data-projects-mode="' + mode + '"' + (disabled ? ' disabled' : '') + '>' +
16
- '<span class="projects-action-icon">' + iconHtml(icon) + '</span><span><strong>' + title + '</strong><span>' + text + '</span></span>' + iconHtml('arrow-up-right') + '</button>';
14
+ function button(action, label, extra, disabled) {
15
+ return '<button type="button" class="projects-button ' + (extra || '') + '" data-projects-action="' + action + '"' + (disabled ? ' disabled' : '') + '>' + label + '</button>';
17
16
  }
18
17
 
18
+ // Mate creation starts with the built-in Clay Mate, so the CTA waits until it is in the loaded Mate list.
19
+ export function clayMateReady(state) {
20
+ var mates = Array.isArray(state.cachedMatesList) ? state.cachedMatesList : [];
21
+ for (var i = 0; i < mates.length; i++) if (mates[i] && mates[i].builtinKey === 'clay') return true;
22
+ return false;
23
+ }
24
+
25
+ function welcomeCard(label, title, text, action, artwork) {
26
+ return '<div class="projects-intro-card"><div class="projects-intro-art"><img src="/welcome-art/' + artwork + '.png" width="1536" height="1024" alt="" decoding="async"></div>' +
27
+ '<span class="projects-intro-label">' + label + '</span><h2>' + title + '</h2><p>' + text + '</p><div class="projects-intro-action">' + action + '</div></div>';
28
+ }
29
+
30
+ // The standalone project list no longer exists: the hub renders only the loading state
31
+ // and the zero-project welcome. Once ordinary projects are known the app opens a workspace.
19
32
  export function projectsHubContent(state) {
20
33
  var projects = ordinaryProjects(state.projectsHubList);
21
- var ready = state.projectListLoaded;
22
- var welcome = ready && (state.projectsShowGuide || (!projects.length && (state.onboardingStep || 'welcome') === 'welcome'));
34
+ if (!state.projectListLoaded || projects.length) {
35
+ return '<main class="projects-main" aria-labelledby="projects-title"><div class="projects-loading" role="status"><h1 id="projects-title" tabindex="-1">Your workspace</h1><p>Loading your workspace…</p></div></main>';
36
+ }
23
37
  var allowed = state.projectsAccessLoaded && (!state.permissions || state.permissions.createProject === true);
24
38
  var available = allowed && state.connected && !state.pendingHomeProjectSlug;
25
- var html = '<header class="projects-header"><a class="projects-brand" href="/projects"><img src="/clay-studio-symbol.png" alt="">Clay Studio</a>' +
26
- '<nav aria-label="Workspace navigation">' + button('settings', 'Settings', 'projects-button-quiet') + '</nav></header>';
27
- html += '<main class="projects-main" aria-labelledby="projects-title">';
28
- if (!ready) return html + '<div class="projects-loading" role="status"><h1 id="projects-title" tabindex="-1">Your projects</h1><p>Loading your workspace…</p></div></main>';
29
- if (welcome) {
30
- return html + '<section class="projects-welcome"><div class="projects-eyebrow">WELCOME TO CLAY</div>' +
31
- '<h1 id="projects-title" tabindex="-1">A place for<br>your next idea.</h1>' +
32
- '<p class="projects-lead">Bring a project. Describe what you want to build.<br>Your AI Driver helps you take it from there.</p>' +
33
- '<div class="projects-intro-grid"><div><span>01 / PROJECTS</span><h2>Work on something real.</h2><p>Open a folder, start fresh, or clone a repository. Your conversations and tools stay with the project.</p></div>' +
34
- '<div><span>02 / MATES</span><h2>Bring in the right perspective.</h2><p>Your Mates live beside projects and open their conversations in the same focused workspace.</p></div></div>' +
35
- '<div class="projects-welcome-actions">' + button('start', projects.length ? 'Explore your projects ' + iconHtml('arrow-right') : 'Set up your first project ' + iconHtml('arrow-right'), 'projects-button-primary') + '</div>' +
36
- '<p class="projects-footnote">You can revisit this guide from Projects at any time.</p></section></main>';
37
- }
38
- html += '<div class="projects-heading"><div><div class="projects-eyebrow">YOUR WORKSPACE</div><h1 id="projects-title" tabindex="-1">' + (projects.length ? 'Projects' : 'Start with a project.') + '</h1>' +
39
- '<p class="projects-lead">' + (projects.length ? 'Pick up where you left off, or begin something new.' : 'Choose where your work will live. You can add more projects later.') + '</p></div>' + button('guide', 'Getting started', 'projects-button-quiet') + '</div>';
40
- if (state.pendingHomeProjectSlug) html += '<p class="projects-status" role="status">Opening your project…</p>';
41
- if (projects.length) {
42
- html += '<div class="projects-list" aria-label="Your projects">';
43
- projects.forEach(function (project) {
44
- html += '<button type="button" class="projects-list-item" data-project-slug="' + escapeHtml(project.slug) + '"' + (!state.connected || state.pendingHomeProjectSlug ? ' disabled' : '') + '>' +
45
- '<span class="projects-action-icon">' + iconHtml(project.isWorktree ? 'git-branch' : 'folder') + '</span><span><strong>' + escapeHtml(project.title || project.project || project.slug) + '</strong>' +
46
- '<span>' + escapeHtml(project.branch || project.path || project.slug) + '</span></span>' + iconHtml('arrow-right') + '</button>';
47
- });
48
- html += '</div><h2 class="projects-section-title">Add a project</h2>';
49
- }
50
- if (!state.projectsAccessLoaded) html += '<p class="projects-status" role="status">Checking project access…</p>';
51
- else if (!allowed) html += '<p class="projects-status">Ask an administrator to create or share a project with you.</p>';
52
- else {
53
- html += '<div class="projects-setup-actions">' +
54
- setupAction('existing', 'folder-open', 'Open a folder', 'Use a directory on the machine running Clay.', !available) +
55
- setupAction('create', 'plus', 'Create a project', 'Start in a new, empty project directory.', !available) +
56
- setupAction('clone', 'git-branch', 'Clone a repository', 'Bring an existing Git repository into Clay.', !available) + '</div>';
57
- if (!projects.length) html += '<p class="projects-next-step"><span>UP NEXT</span> Choose your AI in the workspace, then tell your Driver what you want to build or fix.</p>';
58
- }
59
- if (!state.connected) html += '<p class="projects-status" role="status">Reconnecting… Project actions will be available when the connection returns.</p>';
60
- return html + '</main><footer class="projects-footer">Your projects, conversations, and tools. One workspace.</footer>';
39
+ var mateReady = !!state.connected && clayMateReady(state);
40
+ return '<main class="projects-main" aria-labelledby="projects-title"><section class="projects-welcome"><div class="projects-eyebrow">WELCOME TO</div>' +
41
+ '<h1 id="projects-title" tabindex="-1"><span class="projects-wordmark" role="img" aria-label="Clay Studio"></span></h1>' +
42
+ '<p class="projects-lead">Where the minds work together</p>' +
43
+ '<div class="projects-intro-grid">' +
44
+ welcomeCard('01 / PROJECTS', 'Work together on a project.', 'Bring a codebase or a folder of files, and work through it together with Clay Studio.',
45
+ button('create-project', 'Start a project ' + iconHtml('arrow-right'), 'projects-button-primary', !available), 'project-clay') +
46
+ welcomeCard('02 / MATES', 'Create your own specialist.', 'Create a Mate with the expertise you need, and work together on your ideas and tasks.',
47
+ button('mate', 'Create a Mate ' + iconHtml('arrow-right'), 'projects-button-secondary', !mateReady), 'mate-clay') +
48
+ '</div></section></main>';
61
49
  }
@@ -1,9 +1,10 @@
1
1
  import { store } from './store.js';
2
2
  import { getWs } from './ws-ref.js';
3
- import { openAddProjectModal, switchProject } from './app-projects.js';
4
- import { openUserSettings } from './user-settings.js';
5
- import { rememberHomePrimarySurface, updateHomeSurfacePreference } from './home-surface.js';
6
- import { projectsHubContent } from './projects-hub-view.js';
3
+ import { switchProject } from './app-projects.js';
4
+ import { openAddProjectModal } from './project-start-dialog.js';
5
+ import { startClayMateCreation } from './project-mate-creation-workspace.js';
6
+ import { updateHomeSurfacePreference } from './home-surface.js';
7
+ import { projectsHubContent, isShellWelcome, clayMateReady, ordinaryProjects } from './projects-hub-view.js';
7
8
  import { refreshIcons } from './icons.js';
8
9
 
9
10
  function root() { return document.getElementById('projects-hub'); }
@@ -11,17 +12,18 @@ function root() { return document.getElementById('projects-hub'); }
11
12
  function render() {
12
13
  var element = root();
13
14
  if (!element || !store.get('projectsHubVisible')) return;
15
+ document.body.classList.toggle('projects-shell', isShellWelcome(store.snap()));
14
16
  var html = projectsHubContent(store.snap());
15
17
  if (store.get('projectsHubMarkup') === html) return;
16
18
  store.set({ projectsHubMarkup: html });
17
19
  var active = element.contains(document.activeElement) ? document.activeElement : null;
18
- var action = active && (active.dataset.projectsAction || active.dataset.projectsMode || active.dataset.projectSlug);
20
+ var action = active && (active.dataset.projectsAction);
19
21
  element.innerHTML = html;
20
22
  refreshIcons();
21
23
  if (action) {
22
24
  var buttons = element.querySelectorAll('button, a');
23
25
  for (var i = 0; i < buttons.length; i++) {
24
- if ((buttons[i].dataset.projectsAction || buttons[i].dataset.projectsMode || buttons[i].dataset.projectSlug) === action && !buttons[i].disabled) {
26
+ if (buttons[i].dataset.projectsAction === action && !buttons[i].disabled) {
25
27
  buttons[i].focus({ preventScroll: true });
26
28
  break;
27
29
  }
@@ -35,23 +37,39 @@ function focusHeading() {
35
37
  if (title) title.focus({ preventScroll: true });
36
38
  }
37
39
 
40
+ // The authorised project to open when no standalone list exists: the server-saved
41
+ // last project, then the current one, then the first ordinary project. Only
42
+ // projects present in the server-provided list qualify.
43
+ function workspaceFallback() {
44
+ var known = ordinaryProjects(store.get('projectsHubList')).map(function (project) { return project.slug; });
45
+ var choices = [store.get('homeSurfaceProjectSlug'), store.get('currentSlug'), known[0]];
46
+ for (var i = 0; i < choices.length; i++) if (choices[i] && known.indexOf(choices[i]) !== -1) return choices[i];
47
+ return null;
48
+ }
49
+
50
+ // Shows the loading or zero-project welcome; with projects present it opens a workspace instead.
38
51
  export function showProjectsHub(fromHistory) {
52
+ if (store.get('projectListLoaded') && ordinaryProjects(store.get('projectsHubList')).length) {
53
+ var slug = workspaceFallback();
54
+ if (slug) {
55
+ hideProjectsHub();
56
+ if (fromHistory) history.replaceState(null, '', '/p/' + slug + '/');
57
+ switchProject(slug);
58
+ return;
59
+ }
60
+ }
39
61
  store.set({ projectsHubVisible: true, pendingHomeProjectSlug: null });
40
62
  document.body.classList.add('projects-active');
41
63
  root().classList.remove('hidden');
42
- rememberHomePrimarySurface('projects');
43
- if (!fromHistory && location.pathname !== '/projects') {
44
- var method = document.documentElement.classList.contains('pwa-standalone') ? 'replaceState' : 'pushState';
45
- history[method](null, '', '/projects');
46
- }
64
+ if (location.pathname === '/projects' || location.pathname === '/projects/') history.replaceState(null, '', '/');
47
65
  render();
48
66
  focusHeading();
49
67
  }
50
68
 
51
69
  export function hideProjectsHub() {
52
70
  if (!store.get('projectsHubVisible')) return;
53
- store.set({ projectsHubVisible: false, projectsShowGuide: false });
54
- document.body.classList.remove('projects-active');
71
+ store.set({ projectsHubVisible: false });
72
+ document.body.classList.remove('projects-active', 'projects-shell');
55
73
  if (root()) root().classList.add('hidden');
56
74
  }
57
75
 
@@ -61,25 +79,17 @@ export function completeProjectsOnboarding() {
61
79
  }
62
80
 
63
81
  function handleClick(event) {
64
- var button = event.target.closest('[data-projects-action], [data-projects-mode], [data-project-slug]');
82
+ var button = event.target.closest('[data-projects-action]');
65
83
  if (!button || button.disabled) return;
66
84
  event.preventDefault();
67
85
  var action = button.dataset.projectsAction;
68
- if (action === 'settings') { openUserSettings(); return; }
69
- if (action === 'guide') { store.set({ projectsShowGuide: true }); render(); focusHeading(); return; }
70
- if (action === 'start') {
71
- store.set({ projectsShowGuide: false });
72
- updateHomeSurfacePreference({ onboardingStep: 'project' });
73
- render(); focusHeading();
74
- return;
75
- }
76
86
  var ws = getWs();
77
87
  if (!ws || ws.readyState !== 1) return;
78
- if (button.dataset.projectSlug) { switchProject(button.dataset.projectSlug); return; }
79
- if (button.dataset.projectsMode) {
88
+ if (action === 'mate') { if (clayMateReady(store.snap())) startClayMateCreation(); return; }
89
+ if (action === 'create-project') {
80
90
  var permissions = store.get('permissions');
81
- if (!store.get('projectsAccessLoaded') || (permissions && permissions.createProject !== true)) return;
82
- openAddProjectModal(button.dataset.projectsMode);
91
+ if (!store.get('projectsAccessLoaded') || store.get('pendingHomeProjectSlug') || (permissions && permissions.createProject !== true)) return;
92
+ openAddProjectModal();
83
93
  }
84
94
  }
85
95
 
@@ -88,14 +98,15 @@ export function initProjectsHub() {
88
98
  store.subscribe(function (state, previous) {
89
99
  if (state.projectsHubRestoreRequested && !previous.projectsHubRestoreRequested) {
90
100
  showProjectsHub(true);
91
- if (location.pathname === '/') history.replaceState(null, '', '/projects');
92
101
  store.set({ projectsHubRestoreRequested: false });
93
102
  return;
94
103
  }
95
104
  if (!state.projectsHubVisible) return;
105
+ // A project already being opened keeps its own activation; otherwise known projects replace the hub.
106
+ if (state.projectListLoaded && !state.pendingHomeProjectSlug && ordinaryProjects(state.projectsHubList).length) { showProjectsHub(true); return; }
96
107
  if (state.projectsHubList !== previous.projectsHubList || state.projectListLoaded !== previous.projectListLoaded ||
97
108
  state.homeSurfaceLoaded !== previous.homeSurfaceLoaded || state.onboardingStep !== previous.onboardingStep ||
98
109
  state.projectsAccessLoaded !== previous.projectsAccessLoaded || state.permissions !== previous.permissions ||
99
- state.connected !== previous.connected || state.pendingHomeProjectSlug !== previous.pendingHomeProjectSlug) render();
110
+ state.connected !== previous.connected || state.cachedMatesList !== previous.cachedMatesList || state.pendingHomeProjectSlug !== previous.pendingHomeProjectSlug) render();
100
111
  });
101
112
  }
@@ -1,5 +1,7 @@
1
1
  import { escapeHtml } from './utils.js';
2
2
  import { iconHtml, refreshIcons } from './icons.js';
3
+ import { store } from './store.js';
4
+ import { captureSessionAction, sessionActionCurrent } from './pane-binding.js';
3
5
 
4
6
  var ctx;
5
7
  var rewindMode = false;
@@ -81,6 +83,7 @@ function initiateRewind(uuid) {
81
83
  return;
82
84
  }
83
85
  pendingRewindUuid = uuid;
86
+ store.set({ rewindAction: captureSessionAction() });
84
87
  rewindPreviewInFlight = true;
85
88
  if (ctx.ws && ctx.connected) {
86
89
  ctx.ws.send(JSON.stringify({ type: "rewind_preview", uuid: uuid }));
@@ -120,6 +123,7 @@ function updateSummaryForMode() {
120
123
  }
121
124
 
122
125
  export function showRewindModal(data) {
126
+ if (!sessionActionCurrent(store.get('rewindAction'))) return;
123
127
  rewindPreviewInFlight = false;
124
128
 
125
129
  // Ignore stale preview results that don't match current pending UUID
@@ -367,6 +371,7 @@ export function initRewind(_ctx) {
367
371
  });
368
372
 
369
373
  rewindConfirmBtn.addEventListener("click", function() {
374
+ if (!sessionActionCurrent(store.get('rewindAction'))) { hideRewindModal(); return; }
370
375
  if (rewindExecuting) return;
371
376
  if (pendingRewindUuid && ctx.ws && ctx.connected) {
372
377
  rewindExecuting = true;
@@ -1,46 +1,28 @@
1
1
  import { initStorageUsage, loadStorageUsage } from './server-storage-settings.js';
2
2
  import { initAccountAuthSettings, loadAccountAuthSettings } from './account-auth-settings.js';
3
- // Full-app server administration shell. Page state survives navigation and closing.
3
+ // Server pane of the unified settings shell. Page state survives navigation and closing.
4
4
  import { store } from './store.js';
5
5
  import { getWs } from './ws-ref.js';
6
6
  import { refreshIcons } from './icons.js';
7
7
  import { closeFileViewer } from './filebrowser.js';
8
- import { checkAdminAccess, loadAdminSection } from './admin.js';
9
- import { openUserSettings } from './user-settings.js';
8
+ import { loadAdminSection } from './admin.js';
9
+ import { registerSettingsScope, isSettingsShellOpen, activeSettingsScope } from './settings-shell.js';
10
10
  import { initNetworkSettings, loadNetworkSettings } from './server-network-settings.js';
11
11
  import { initServerControls, setServerPinAvailability } from './server-settings-controls.js';
12
12
  import { initServerEnvironment, loadSharedEnv } from './server-settings-environment.js';
13
- import { renderSettingsNavigation } from './server-settings-navigation.js';
14
- import { hasSettingsDrafts, confirmSettingsAction, trapSettingsFocus } from './server-settings-state.js';
13
+ import { hasSettingsDrafts } from './server-settings-state.js';
15
14
  import { enhanceSettingsSelects } from './settings-select.js';
16
15
  export { updateDaemonConfig, handleSetPinResult, handleKeepAwakeChanged, handleAutoContinueChanged, handleRestartResult, handleShutdownResult, handleServerSettingResult } from './server-settings-controls.js';
17
16
  export { handleSharedEnv, handleSharedEnvSaved } from './server-settings-environment.js';
18
17
 
19
- function isolateSettings(active) {
20
- if (active) {
21
- var entries = Array.from(document.body.children).filter(function (el) { return el !== root() && el.id !== 'confirm-modal' && el.tagName !== 'SCRIPT' && el.tagName !== 'STYLE'; }).map(function (el) { var entry = { el: el, inert: el.inert, hidden: el.getAttribute('aria-hidden') }; el.inert = true; el.setAttribute('aria-hidden', 'true'); return entry; });
22
- store.set({ serverSettingsInert: entries });
23
- } else {
24
- (store.get('serverSettingsInert') || []).forEach(function (entry) { entry.el.inert = entry.inert; if (entry.hidden === null) entry.el.removeAttribute('aria-hidden'); else entry.el.setAttribute('aria-hidden', entry.hidden); });
25
- store.set({ serverSettingsInert: [] });
26
- }
27
- }
28
18
  function root() { return document.getElementById('server-settings'); }
29
- export function isSettingsOpen() { return root() && !root().classList.contains('hidden'); }
19
+ export function isSettingsOpen() { return isSettingsShellOpen() && activeSettingsScope() === 'server'; }
30
20
  function send(type) { var ws = getWs(); if (ws && ws.readyState === 1) ws.send(JSON.stringify({ type: type })); }
31
21
 
32
- function selectPage(id, focus) {
22
+ function selectPage(id) {
33
23
  if (!store.get('serverSettingsAuthorized')) return;
34
- if (id === 'admin-invites') id = 'admin-users';
35
- var nav = document.getElementById('settings-page-nav');
36
- if (!nav.querySelector('[data-section="' + id + '"]')) id = 'overview';
37
24
  store.set({ serverSettingsPage: id });
38
25
  root().querySelectorAll('.server-settings-section').forEach(function (page) { page.classList.toggle('active', page.dataset.section === id); });
39
- nav.querySelectorAll('button').forEach(function (button) {
40
- button.classList.toggle('active', button.dataset.section === id);
41
- if (button.dataset.section === id) button.setAttribute('aria-current', 'page'); else button.removeAttribute('aria-current');
42
- });
43
- document.getElementById('settings-nav-dropdown').value = id;
44
26
  if (id === 'network') loadNetworkSettings();
45
27
  if (id === 'security') loadAccountAuthSettings();
46
28
  if (id === 'storage') loadStorageUsage();
@@ -49,85 +31,48 @@ function selectPage(id, focus) {
49
31
  var body = document.getElementById(id + '-body');
50
32
  if (id !== 'admin-smtp' || !(store.get('serverSettingsDrafts') || {}).smtp) loadAdminSection(id, body);
51
33
  }
52
- if (focus) {
53
- var heading = root().querySelector('.server-settings-section.active h2');
54
- heading.tabIndex = -1; heading.focus();
55
- }
34
+ enhanceSettingsSelects(root()); refreshIcons(root());
56
35
  }
57
36
 
58
- async function openSettings() {
59
- if (isSettingsOpen()) return;
37
+ // The shell has already verified administrator (or solo-owner) access.
38
+ function enterServerSettings(ctx) {
60
39
  closeFileViewer();
61
- isolateSettings(true);
62
- store.set({ serverSettingsReturnFocus: document.activeElement });
63
- root().classList.remove('hidden');
64
- document.getElementById('server-settings-btn').classList.add('active');
65
- document.getElementById('server-settings-close').focus();
66
40
  var status = document.getElementById('settings-access-status');
67
- status.textContent = 'Checking administrator access…';
68
- store.set({ serverSettingsAuthorized: false });
69
- var serverPinSection = document.getElementById('settings-server-pin');
70
- if (serverPinSection) serverPinSection.hidden = true;
41
+ status.textContent = '';
42
+ store.set({ serverSettingsAuthorized: true });
43
+ setServerPinAvailability(!ctx.access.multiUser);
71
44
  root().querySelectorAll('.server-settings-section').forEach(function (page) { page.classList.remove('active'); });
72
- try {
73
- var response = await fetch('/api/me', { credentials: 'same-origin' });
74
- if (!response.ok) throw new Error('Could not verify access. Close and reopen Settings to retry.');
75
- var data = await response.json();
76
- if (data.multiUser && (!data.user || data.user.role !== 'admin')) {
77
- status.textContent = 'Server settings require an administrator. Your personal preferences are available below.';
78
- document.getElementById('settings-page-nav').replaceChildren();
79
- document.getElementById('settings-nav-dropdown').replaceChildren();
80
- return;
81
- }
82
- await checkAdminAccess();
83
- if (!isSettingsOpen()) return;
84
- store.set({ serverSettingsAuthorized: true });
85
- setServerPinAvailability(!data.multiUser);
86
- status.textContent = '';
87
- renderSettingsNavigation(!!data.multiUser, selectPage);
88
- selectPage(store.get('serverSettingsPage') || 'overview', false);
89
- if (!hasSettingsDrafts()) send('get_daemon_config');
90
- setTimeout(function () {
91
- if (isSettingsOpen() && !store.get('serverSettingsConfigLoaded')) status.textContent = 'Server configuration has not loaded. Reconnect, then reopen Settings to retry.';
92
- }, 12000);
93
- send('process_stats');
94
- clearInterval(store.get('serverSettingsStatsTimer'));
95
- store.set({ serverSettingsStatsTimer: setInterval(function () { send('process_stats'); }, 5000) });
96
- document.getElementById('settings-project-slug').textContent = store.get('currentSlug') || '(none)';
97
- document.getElementById('settings-ws-path').textContent = store.get('wsPath') || '/ws';
98
- var pill = document.getElementById('skip-perms-pill');
99
- document.getElementById('settings-skip-perms').textContent = pill && !pill.classList.contains('hidden') ? 'Skip' : 'Permission checks';
100
- enhanceSettingsSelects(root()); refreshIcons(root());
101
- } catch (error) { status.textContent = error.message; }
45
+ if (!hasSettingsDrafts()) send('get_daemon_config');
46
+ setTimeout(function () {
47
+ if (isSettingsOpen() && !store.get('serverSettingsConfigLoaded')) status.textContent = 'Server configuration has not loaded. Reconnect, then reopen Settings to retry.';
48
+ }, 12000);
49
+ send('process_stats');
50
+ clearInterval(store.get('serverSettingsStatsTimer'));
51
+ store.set({ serverSettingsStatsTimer: setInterval(function () { send('process_stats'); }, 5000) });
52
+ document.getElementById('settings-project-slug').textContent = store.get('currentSlug') || '(none)';
53
+ document.getElementById('settings-ws-path').textContent = store.get('wsPath') || '/ws';
54
+ var pill = document.getElementById('skip-perms-pill');
55
+ document.getElementById('settings-skip-perms').textContent = pill && !pill.classList.contains('hidden') ? 'Skip' : 'Permission checks';
102
56
  }
103
57
 
104
- function closeSettings() {
105
- function close() {
106
- isolateSettings(false);
107
- root().classList.add('hidden');
108
- document.getElementById('server-settings-btn').classList.remove('active');
109
- clearInterval(store.get('serverSettingsStatsTimer'));
110
- var previous = store.get('serverSettingsReturnFocus');
111
- if (previous && previous.isConnected) previous.focus();
112
- }
113
- if (hasSettingsDrafts()) confirmSettingsAction('You have unsaved changes. Closing keeps these drafts in this tab, but reloading the page will lose them.', 'Close with drafts', close);
114
- else close();
58
+ function leaveServerSettings() {
59
+ clearInterval(store.get('serverSettingsStatsTimer'));
60
+ store.set({ serverSettingsAuthorized: false });
115
61
  }
116
62
 
117
63
  export function initServerSettings() {
118
64
  if (!root()) return;
119
65
  initNetworkSettings(); initServerControls(); initServerEnvironment(); initStorageUsage(); initAccountAuthSettings();
120
- document.getElementById('server-settings-btn').addEventListener('click', openSettings);
121
- document.getElementById('server-settings-close').onclick = closeSettings;
122
- document.getElementById('settings-personal-link').onclick = function () {
123
- function openPersonal() { isolateSettings(false); document.getElementById('server-settings-btn').classList.remove('active'); root().classList.add('hidden'); clearInterval(store.get('serverSettingsStatsTimer')); openUserSettings('us-notifications'); }
124
- if (hasSettingsDrafts()) confirmSettingsAction('Your server settings drafts will remain in this tab while you visit Personal settings.', 'Open Personal settings', openPersonal);
125
- else openPersonal();
126
- };
127
- document.addEventListener('keydown', function (event) {
128
- if (!isSettingsOpen() || document.querySelector('.admin-modal-overlay, #settings-confirm, .confirm-above-settings')) return;
129
- if (event.key === 'Escape') { event.preventDefault(); closeSettings(); }
130
- trapSettingsFocus(event, root());
66
+ registerSettingsScope('server', {
67
+ pane: root(),
68
+ enter: enterServerSettings,
69
+ activate: selectPage,
70
+ leave: leaveServerSettings,
71
+ leavePrompt: function (kind) {
72
+ if (!hasSettingsDrafts() || kind === 'page' || kind === 'project') return null;
73
+ if (kind === 'close') return { message: 'You have unsaved changes. Closing keeps these drafts in this tab, but reloading the page will lose them.', action: 'Close with drafts' };
74
+ return { message: 'Your server settings drafts will remain in this tab while you visit another settings area.', action: 'Leave Server settings' };
75
+ }
131
76
  });
132
77
  new MutationObserver(function () { enhanceSettingsSelects(root()); }).observe(root(), { childList: true, subtree: true });
133
78
  }
@@ -4,6 +4,7 @@ import { addSystemMessage, VENDOR_AVATARS, VENDOR_NAMES } from './app-rendering.
4
4
  import { openPairDialog } from './split-pair-ui.js';
5
5
  import { buildAgentVendorSelect, fillAgentModels, buildAgentEffortSelect, fillAgentEffort } from './agent-config-selects.js';
6
6
  import { showToast } from './utils.js';
7
+ import { sessionCallback, captureSessionAction, sessionActionCurrent } from './pane-binding.js';
7
8
 
8
9
  var addWorkerButton = null;
9
10
  var handoffButton = null;
@@ -33,6 +34,7 @@ function handoffField(labelText, control) {
33
34
  function requestHandoffDialog() {
34
35
  var ws = getWs();
35
36
  if (!ws || ws.readyState !== 1) return;
37
+ store.set({ handoffAction: captureSessionAction() });
36
38
  ws.send(JSON.stringify({ type: "handoff_session_options" }));
37
39
  }
38
40
 
@@ -47,6 +49,7 @@ function openWorkerDialog() {
47
49
  }
48
50
 
49
51
  function showHandoffDialog(options) {
52
+ if (!sessionActionCurrent(store.get('handoffAction'))) return;
50
53
  closeHandoffDialog();
51
54
  var installed = options.installedVendors || [];
52
55
  var vendorInfo = store.get('vendorInfo') || {};
@@ -147,7 +150,7 @@ function showHandoffDialog(options) {
147
150
  if (event.key === "Escape") closeHandoffDialog();
148
151
  };
149
152
  document.addEventListener("keydown", handoffEscapeHandler);
150
- submit.addEventListener("click", function () {
153
+ submit.addEventListener("click", sessionCallback(function () {
151
154
  var ws = getWs();
152
155
  if (!ws || ws.readyState !== 1) return;
153
156
  ws.send(JSON.stringify({
@@ -158,7 +161,7 @@ function showHandoffDialog(options) {
158
161
  }));
159
162
  submit.disabled = true;
160
163
  submit.textContent = "Continuing…";
161
- });
164
+ }));
162
165
  vendorSelect.focus();
163
166
  }
164
167
 
@@ -109,6 +109,8 @@ function currentRenameCanSend(state) {
109
109
  String(store.get('activeSessionId')) === String(state.sessionId) :
110
110
  state && state.row && String(state.row.dataset.sessionId) === String(state.sessionId);
111
111
  return !!(state && state.row && state.row.isConnected && sameSession &&
112
+ (state.source !== 'header' || state.origin === store.get('sessionOriginId')) &&
113
+ state.epoch === (store.get('sessionActionEpoch') || 0) && !store.get('panePendingBinding') && !store.get('deferredSessionSwitch') &&
112
114
  store.get('connected') && state.ws === ws && ws && ws.readyState === 1 &&
113
115
  state.slug === store.get('currentSlug') &&
114
116
  (!permissions || permissions.sessionRename !== false));
@@ -158,6 +160,8 @@ function beginSessionInlineRename(row, title, control, session, source, inputCla
158
160
  input: input,
159
161
  originalNodes: originalNodes,
160
162
  sessionId: session.id,
163
+ origin: session.sessionOriginId,
164
+ epoch: store.get('sessionActionEpoch') || 0,
161
165
  currentTitle: session.title || "New Session",
162
166
  control: control || null,
163
167
  controlDisplay: controlDisplay,
@@ -202,6 +206,7 @@ export function startHeaderSessionInlineRename(title, control, session) {
202
206
 
203
207
  store.subscribe(function (state, previous) {
204
208
  if (state.currentSlug !== previous.currentSlug || state.dmMode !== previous.dmMode ||
209
+ state.sessionActionEpoch !== previous.sessionActionEpoch || state.panePendingBinding !== previous.panePendingBinding ||
205
210
  (previous.connected && !state.connected)) {
206
211
  closeSessionContextMenu(false);
207
212
  cancelSessionInlineRename(false);
@@ -0,0 +1,21 @@
1
+ import { store } from './store.js';
2
+ import { takeComposerDraft, restoreComposerDraft } from './input.js';
3
+
4
+ function key() { return [store.get('myUserId') || '', store.get('currentSlug') || '', store.get('sessionOriginId') || ''].join(':'); }
5
+ export function checkComposerDeparture() {
6
+ var draft = takeComposerDraft();
7
+ restoreComposerDraft(draft);
8
+ }
9
+ export function saveSessionRichDraft() {
10
+ if (store.get('activeProjectMateId') || !store.get('sessionOriginId') || store.get('panePendingBinding')) return;
11
+ var drafts = Object.assign({}, store.get('sessionRichDrafts') || {});
12
+ var draft = takeComposerDraft();
13
+ drafts[key()] = draft;
14
+ restoreComposerDraft(draft);
15
+ store.set({ sessionRichDrafts: drafts });
16
+ }
17
+ export function restoreSessionRichDraft() {
18
+ if (store.get('activeProjectMateId') || store.get('panePendingBinding')) return;
19
+ var draft = (store.get('sessionRichDrafts') || {})[key()];
20
+ restoreComposerDraft(draft || { text: (store.get('sessionDrafts') || {})[store.get('activeSessionId')] || '', start: 0, end: 0 });
21
+ }
@@ -0,0 +1,67 @@
1
+ import { store } from './store.js';
2
+ import { getWs } from './ws-ref.js';
3
+ import { composerDepartureReason } from './input.js';
4
+ import { processMessage } from './app-message-router.js';
5
+
6
+ function notice(show, message) {
7
+ var element = document.getElementById('session-switch-wait');
8
+ if (!show) { if (element) element.remove(); return; }
9
+ if (element) { if (message) element.textContent = message; return; }
10
+ var area = document.getElementById('input-area');
11
+ if (!area) return;
12
+ element = document.createElement('div'); element.id = 'session-switch-wait';
13
+ element.className = 'session-switch-wait'; element.setAttribute('role', 'status');
14
+ element.textContent = 'Session change waiting. Finish or discard voice input, or wait for attachments. Your draft stays with this conversation.';
15
+ area.prepend(element);
16
+ }
17
+
18
+ // Keep the source composer and its async completions together. The destination
19
+ // replay waits in order; it cannot change identity midway through an upload.
20
+ export function deferSessionSwitch(message) {
21
+ var pending = store.get('deferredSessionSwitch');
22
+ if (pending && pending.draining) return false;
23
+ if (['account_auth_required', 'project_removed', 'project_access_revoked'].indexOf(message.type) >= 0) {
24
+ store.set({ deferredSessionSwitch: null }); notice(false); return false;
25
+ }
26
+ if (!pending && message.type === 'session_switched' && composerDepartureReason()) {
27
+ pending = { socket: getWs(), projectSlug: store.get('currentSlug'), ownerId: store.get('myUserId'),
28
+ sessionId: store.get('activeSessionId'), origin: store.get('sessionOriginId'), processing: store.get('processing'), messages: [] };
29
+ var speech = store.get('speechActive');
30
+ if (speech) speech.autoSend = false;
31
+ store.set({ deferredSessionSwitch: pending }); notice(true);
32
+ }
33
+ if (!pending) return false;
34
+ if (pending.projectSlug === store.get('currentSlug') && pending.ownerId === store.get('myUserId')) pending.socket = getWs();
35
+ if (store.get('connected') && pending.processing && !store.get('processing')) store.set({ processing: true });
36
+ pending.messages.push(message);
37
+ if (message.type === 'session_list' || message.type === 'session_switched') queueMicrotask(drain);
38
+ return true;
39
+ }
40
+
41
+ function drain() {
42
+ var pending = store.get('deferredSessionSwitch');
43
+ if (!pending || pending.draining || composerDepartureReason() || !store.get('connected')) return;
44
+ if (pending.socket !== getWs() || pending.projectSlug !== store.get('currentSlug') || pending.ownerId !== store.get('myUserId')) return;
45
+ var latestSwitch, latestList;
46
+ pending.messages.forEach(function (message) {
47
+ if (message.type === 'session_switched') latestSwitch = message;
48
+ if (message.type === 'session_list') latestList = message;
49
+ });
50
+ if (!latestSwitch || !latestList) return;
51
+ var target = (latestList.sessions || []).find(function (session) { return session.id === latestSwitch.id; });
52
+ if (!target || target.sessionOriginId !== latestSwitch.sessionOriginId) {
53
+ notice(true, 'The conversation changed while your draft was waiting. Your draft is preserved. Choose a conversation to continue.');
54
+ return;
55
+ }
56
+ // Restoring superseded snapshots must not trigger outbound session effects
57
+ // before the final authoritative target is installed.
58
+ pending.draining = true;
59
+ pending.finalSwitch = latestSwitch;
60
+ pending.messages.forEach(processMessage);
61
+ store.set({ deferredSessionSwitch: null }); notice(false);
62
+ }
63
+
64
+ store.subscribe(function (state, previous) {
65
+ if (state.deferredSessionSwitch && (state.composerUploadingCount !== previous.composerUploadingCount ||
66
+ state.speechActive !== previous.speechActive || state.speechRetry !== previous.speechRetry || state.connected !== previous.connected)) queueMicrotask(drain);
67
+ });