clay-server 5.3.0-beta.6 → 5.3.0-beta.7

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 (111) hide show
  1. package/lib/project-admin-overview.js +22 -11
  2. package/lib/project-background-tasks.js +76 -0
  3. package/lib/project-connection.js +1 -1
  4. package/lib/project-sessions.js +12 -5
  5. package/lib/project-split-views.js +12 -3
  6. package/lib/project-view-store.js +7 -5
  7. package/lib/project-worker-conversation.js +5 -0
  8. package/lib/public/app.js +7 -0
  9. package/lib/public/css/contextual-clay.css +2 -1
  10. package/lib/public/css/mate-conversation.css +1 -1
  11. package/lib/public/css/pending-message-queue.css +24 -19
  12. package/lib/public/css/project-admin-overview.css +42 -13
  13. package/lib/public/css/shared-browser.css +31 -8
  14. package/lib/public/css/split-view-layout.css +92 -7
  15. package/lib/public/css/tool-pane.css +4 -7
  16. package/lib/public/index.html +5 -5
  17. package/lib/public/modules/agent-message-bubbles.js +96 -0
  18. package/lib/public/modules/app-header.js +6 -0
  19. package/lib/public/modules/app-messages.js +26 -9
  20. package/lib/public/modules/background-task-previews.js +8 -0
  21. package/lib/public/modules/background-task-viewer.js +97 -0
  22. package/lib/public/modules/background-tasks-ui.js +34 -125
  23. package/lib/public/modules/chat-bubble-renderer.js +11 -5
  24. package/lib/public/modules/contextual-clay-geometry.js +7 -3
  25. package/lib/public/modules/pane-activation.js +5 -3
  26. package/lib/public/modules/pane-header-tooltips.js +32 -0
  27. package/lib/public/modules/pending-message-queue-drag.js +69 -0
  28. package/lib/public/modules/pending-message-queue.js +3 -33
  29. package/lib/public/modules/permission-control.js +6 -5
  30. package/lib/public/modules/project-admin-overview.js +125 -10
  31. package/lib/public/modules/project-grouping.js +9 -0
  32. package/lib/public/modules/server-settings-controls.js +33 -4
  33. package/lib/public/modules/session-folder-toolbar.js +1 -1
  34. package/lib/public/modules/session-tool-item.js +86 -0
  35. package/lib/public/modules/settings-select.js +2 -1
  36. package/lib/public/modules/shared-browser-tabs.js +37 -21
  37. package/lib/public/modules/sidebar-projects.js +1 -10
  38. package/lib/public/modules/split-session-empty-pane.js +47 -0
  39. package/lib/public/modules/split-view-create-menu.js +74 -65
  40. package/lib/public/modules/split-view-layout-scope.js +4 -0
  41. package/lib/public/modules/split-view-picker-actions.js +71 -0
  42. package/lib/public/modules/split-view-picker-render.js +81 -0
  43. package/lib/public/modules/split-view-presentation-renderer.js +8 -8
  44. package/lib/public/modules/split-view-worker-activity.js +2 -2
  45. package/lib/public/modules/split-view-worker-transcript.js +70 -33
  46. package/lib/public/modules/split-view-workers.js +6 -4
  47. package/lib/public/modules/terminal-viewport.js +14 -0
  48. package/lib/public/modules/terminal.js +3 -4
  49. package/lib/public/modules/theme-palette.js +177 -0
  50. package/lib/public/modules/theme.js +1 -174
  51. package/lib/public/modules/tool-pane-chrome.js +7 -1
  52. package/lib/public/modules/tools.js +5 -89
  53. package/lib/public/modules/tooltip.js +32 -6
  54. package/lib/public/modules/wireframe-workbench.js +2 -2
  55. package/lib/public/modules/worker-message-previews.js +39 -0
  56. package/lib/public/pane-assets/app-5W6KJXK5.js +1 -0
  57. package/lib/public/pane-assets/chunk-2IJUTAB6.js +2 -0
  58. package/lib/public/pane-assets/{chunk-BMHTBRMO.js → chunk-7OVNC5HT.js} +1 -1
  59. package/lib/public/pane-assets/chunk-BQN7B62H.js +3 -0
  60. package/lib/public/pane-assets/{chunk-2AIWCI6Y.js → chunk-EMLN2Y7L.js} +1 -1
  61. package/lib/public/pane-assets/{chunk-VKYBAVWT.js → chunk-ENPAHXLB.js} +1 -1
  62. package/lib/public/pane-assets/chunk-EPBGGZRT.js +1 -0
  63. package/lib/public/pane-assets/chunk-EYTE56IA.js +1 -0
  64. package/lib/public/pane-assets/{chunk-6NYCNWVF.js → chunk-FQTNR4ZS.js} +1 -1
  65. package/lib/public/pane-assets/chunk-L6LSA5WI.js +1 -0
  66. package/lib/public/pane-assets/chunk-M57UIGZV.js +1 -0
  67. package/lib/public/pane-assets/chunk-MGTFTEJP.js +1 -0
  68. package/lib/public/pane-assets/chunk-MMG6MYCN.js +1 -0
  69. package/lib/public/pane-assets/chunk-MZLMEXMW.js +1 -0
  70. package/lib/public/pane-assets/chunk-OG3E5SVA.js +166 -0
  71. package/lib/public/pane-assets/chunk-RGAFZ3T2.js +1 -0
  72. package/lib/public/pane-assets/chunk-RNDRURMF.js +1 -0
  73. package/lib/public/pane-assets/{chunk-PV4PCH2V.js → chunk-SJOT74VW.js} +1 -1
  74. package/lib/public/pane-assets/{chunk-LDIZLKB2.js → chunk-SJTGHIN5.js} +1 -1
  75. package/lib/public/pane-assets/chunk-SLMAFKI2.js +1 -0
  76. package/lib/public/pane-assets/chunk-U3I5KY3B.js +1 -0
  77. package/lib/public/pane-assets/{chunk-VNAV23CM.js → chunk-UPXU2IPC.js} +1 -1
  78. package/lib/public/pane-assets/{chunk-TMGSDFJI.js → chunk-VEBYD7TL.js} +1 -1
  79. package/lib/public/pane-assets/chunk-XBHYNXXT.js +1 -0
  80. package/lib/public/pane-assets/manifest.json +426 -414
  81. package/lib/public/pane-assets/style-BSKKK54Q.css +1 -0
  82. package/lib/sdk-message-processor.js +5 -2
  83. package/lib/server-storage.js +27 -3
  84. package/lib/server.js +4 -2
  85. package/lib/sessions.js +1 -1
  86. package/lib/storage-usage.js +29 -7
  87. package/lib/terminal-manager.js +2 -2
  88. package/lib/worker-message-previews.js +50 -0
  89. package/lib/ws-schema.js +1 -1
  90. package/lib/yoke/adapters/claude-worker.js +8 -0
  91. package/lib/yoke/adapters/claude.js +17 -2
  92. package/lib/yoke/claude-task-reader.js +94 -0
  93. package/lib/yoke/interface.js +1 -0
  94. package/package.json +1 -1
  95. package/lib/public/pane-assets/app-7UZ727NA.js +0 -1
  96. package/lib/public/pane-assets/chunk-6MIHIQ2W.js +0 -1
  97. package/lib/public/pane-assets/chunk-6UJ2ALOB.js +0 -1
  98. package/lib/public/pane-assets/chunk-6XK2OATZ.js +0 -1
  99. package/lib/public/pane-assets/chunk-BKIDR2ME.js +0 -1
  100. package/lib/public/pane-assets/chunk-GEFCFZEN.js +0 -1
  101. package/lib/public/pane-assets/chunk-GN2NQEVC.js +0 -2
  102. package/lib/public/pane-assets/chunk-IMLMBPT4.js +0 -166
  103. package/lib/public/pane-assets/chunk-K5S6AILM.js +0 -1
  104. package/lib/public/pane-assets/chunk-OWNNLZ36.js +0 -1
  105. package/lib/public/pane-assets/chunk-P2C3LOWL.js +0 -3
  106. package/lib/public/pane-assets/chunk-R4LMPTKU.js +0 -1
  107. package/lib/public/pane-assets/chunk-VWTHIQCE.js +0 -1
  108. package/lib/public/pane-assets/chunk-WM3K5AFH.js +0 -1
  109. package/lib/public/pane-assets/chunk-XOB3O327.js +0 -1
  110. package/lib/public/pane-assets/chunk-XOOUCFYT.js +0 -1
  111. package/lib/public/pane-assets/style-ZM7OOU3M.css +0 -1
@@ -1,21 +1,136 @@
1
- // This page never opens a project socket or loads project content modules.
1
+ // Restricted projects use public shell primitives without opening a project socket.
2
+ import { createStore, store } from './store.js';
3
+ import { groupProjects, getProjectAbbrev } from './project-grouping.js';
4
+ import { computeVars, validateTheme, defaultDarkFallback, defaultLightFallback } from './theme-palette.js';
5
+ import { initTooltips } from './tooltip.js';
6
+
7
+ var data = JSON.parse(document.getElementById('project-admin-data').textContent);
8
+ createStore({ adminOverview: data, adminUsage: null, adminUsageLoading: false, adminUsageTimer: null, adminUsageGeneration: 0, adminRailCollapsed: {}, adminThemes: null });
9
+
10
+ function applyAppearance() {
11
+ var seed = window.__clayAppearanceSeed;
12
+ var mode = seed && seed.configured && seed.mode || (matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark');
13
+ var id = seed && (seed[mode + 'Theme'] || seed.theme);
14
+ var themes = store.get('adminThemes') || {};
15
+ var theme = themes[id] && themes[id].variant === mode ? themes[id] : (mode === 'light' ? defaultLightFallback : defaultDarkFallback);
16
+ if (!validateTheme(theme)) return;
17
+ var vars = computeVars(theme);
18
+ Object.keys(vars).forEach(function (name) { document.documentElement.style.setProperty(name, vars[name]); });
19
+ document.documentElement.classList.toggle('light-theme', theme.variant === 'light');
20
+ document.documentElement.classList.toggle('dark-theme', theme.variant !== 'light');
21
+ }
22
+ applyAppearance();
23
+ fetch('/api/themes', { credentials: 'same-origin' }).then(function (response) { if (!response.ok) throw new Error('Themes unavailable'); return response.json(); }).then(function (themes) {
24
+ var valid = {};
25
+ Object.entries(Object.assign({}, themes.bundled, themes.custom)).forEach(function (entry) { if (validateTheme(entry[1])) valid[entry[0]] = entry[1]; });
26
+ store.set({ adminThemes: valid }); applyAppearance();
27
+ }).catch(function () {});
28
+ matchMedia('(prefers-color-scheme: light)').addEventListener('change', applyAppearance);
29
+
30
+ function projectLink(project, worktree) {
31
+ var link = document.createElement('a');
32
+ link.className = (worktree ? 'icon-strip-wt-item' : 'icon-strip-item') + (project.slug === data.project.slug ? ' active' : '');
33
+ link.href = '/p/' + encodeURIComponent(project.slug) + '/';
34
+ link.dataset.slug = project.slug;
35
+ link.dataset.tip = project.title + (project.contentAccess === false ? ' · Administrative overview' : '');
36
+ link.setAttribute('aria-label', link.dataset.tip);
37
+ if (project.slug === data.project.slug) link.setAttribute('aria-current', 'page');
38
+ var label = document.createElement('span');
39
+ label.className = worktree ? 'wt-branch-abbrev' : 'icon-strip-name';
40
+ if (project.icon) label.classList.add('project-emoji');
41
+ label.textContent = project.icon || getProjectAbbrev(project.title);
42
+ link.appendChild(label);
43
+ return link;
44
+ }
45
+ function renderNavigation() {
46
+ var container = document.getElementById('icon-strip-projects');
47
+ var grouped = groupProjects(data.projects);
48
+ grouped.parents.forEach(function (project) {
49
+ var children = grouped.wtByParent[project.slug] || [];
50
+ var link = projectLink(project, false);
51
+ if (!children.length) { container.appendChild(link); return; }
52
+ var group = document.createElement('div');
53
+ group.className = 'icon-strip-group'; group.dataset.parentSlug = project.slug;
54
+ var header = document.createElement('div'); header.className = 'project-admin-rail-header';
55
+ header.appendChild(link); link.classList.add('folder-header');
56
+ var toggle = document.createElement('button');
57
+ toggle.type = 'button'; toggle.className = 'icon-strip-group-chevron'; toggle.dataset.tip = 'Toggle worktrees';
58
+ toggle.setAttribute('aria-label', 'Toggle ' + project.title + ' worktrees'); toggle.setAttribute('aria-expanded', 'true');
59
+ var list = document.createElement('div'); list.className = 'icon-strip-group-items'; list.id = 'worktrees-' + project.slug;
60
+ toggle.setAttribute('aria-controls', list.id);
61
+ toggle.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M6 3v12a6 6 0 0 0 12 0V6M6 7h0"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="6" r="3"/></svg>';
62
+ toggle.addEventListener('click', function () {
63
+ var collapsed = group.classList.toggle('collapsed'); list.inert = collapsed;
64
+ toggle.setAttribute('aria-expanded', String(!collapsed));
65
+ var state = Object.assign({}, store.get('adminRailCollapsed')); state[project.slug] = collapsed; store.set({ adminRailCollapsed: state });
66
+ });
67
+ children.forEach(function (child) { list.appendChild(projectLink(child, true)); });
68
+ header.appendChild(toggle); group.append(header, list); container.appendChild(group);
69
+ });
70
+ var parent = data.projects.find(function (project) { return project.slug === data.project.parentSlug; });
71
+ var label = document.getElementById('project-admin-parent');
72
+ if (parent) { label.hidden = false; label.textContent = 'Worktree of ' + parent.title; }
73
+ }
74
+ renderNavigation();
75
+ initTooltips();
76
+
2
77
  var tabs = Array.from(document.querySelectorAll('[role="tab"]'));
3
78
  function select(tab) {
4
79
  tabs.forEach(function (item) {
5
80
  var active = item === tab;
6
- item.setAttribute("aria-selected", String(active));
7
- item.tabIndex = active ? 0 : -1;
8
- document.getElementById(item.getAttribute("aria-controls")).hidden = !active;
81
+ item.setAttribute('aria-selected', String(active)); item.tabIndex = active ? 0 : -1;
82
+ document.getElementById(item.getAttribute('aria-controls')).hidden = !active;
9
83
  });
10
84
  }
11
85
  tabs.forEach(function (tab, index) {
12
- tab.addEventListener("click", function () { select(tab); });
13
- tab.addEventListener("keydown", function (event) {
14
- if (["ArrowLeft", "ArrowRight", "Home", "End"].indexOf(event.key) === -1) return;
86
+ tab.addEventListener('click', function () { select(tab); });
87
+ tab.addEventListener('keydown', function (event) {
88
+ if (['ArrowLeft', 'ArrowRight', 'Home', 'End'].indexOf(event.key) === -1) return;
15
89
  event.preventDefault();
16
- var target = event.key === "Home" ? tabs[0] : event.key === "End" ? tabs[tabs.length - 1] : tabs[(index + (event.key === "ArrowRight" ? 1 : tabs.length - 1)) % tabs.length];
17
- select(target);
18
- target.focus();
90
+ var target = event.key === 'Home' ? tabs[0] : event.key === 'End' ? tabs[tabs.length - 1] : tabs[(index + (event.key === 'ArrowRight' ? 1 : tabs.length - 1)) % tabs.length];
91
+ select(target); target.focus();
19
92
  });
20
93
  });
21
94
  select(tabs[0]);
95
+
96
+ function size(bytes) {
97
+ if (!Number.isFinite(bytes) || bytes < 0) return 'Unavailable';
98
+ var units = ['B', 'KiB', 'MiB', 'GiB', 'TiB'], index = 0;
99
+ while (bytes >= 1024 && index < units.length - 1) { bytes /= 1024; index++; }
100
+ return bytes.toFixed(index ? 1 : 0) + ' ' + units[index];
101
+ }
102
+ function clearUsage() {
103
+ ['bytes', 'files', 'free', 'capacity'].forEach(function (field) { document.getElementById('usage-' + field).textContent = '—'; });
104
+ store.set({ adminUsage: null });
105
+ }
106
+ async function loadUsage(refresh) {
107
+ if (store.get('adminUsageLoading')) return;
108
+ clearTimeout(store.get('adminUsageTimer'));
109
+ var generation = store.get('adminUsageGeneration');
110
+ store.set({ adminUsageLoading: true });
111
+ var button = document.getElementById('project-admin-refresh'), status = document.getElementById('usage-status');
112
+ button.disabled = true; document.getElementById('project-admin-usage').setAttribute('aria-busy', 'true');
113
+ try {
114
+ var response = await fetch('/api/server/storage/project/' + encodeURIComponent(data.project.slug) + (refresh ? '?refresh=1' : ''), { credentials: 'same-origin' });
115
+ var result = await response.json();
116
+ if (generation !== store.get('adminUsageGeneration')) return;
117
+ if (!response.ok) { if ([401, 403, 404, 409].indexOf(response.status) >= 0) clearUsage(); throw new Error(result.error || 'Storage measurement failed.'); }
118
+ if (result.slug !== data.project.slug) throw new Error('Storage response belongs to another project.');
119
+ store.set({ adminUsage: result });
120
+ var usage = result.usage;
121
+ if (usage && usage.available) {
122
+ document.getElementById('usage-bytes').textContent = (result.partial || result.scanning ? 'At least ' : '') + size(usage.bytes);
123
+ document.getElementById('usage-files').textContent = (result.partial || result.scanning ? 'At least ' : '') + Number(usage.files).toLocaleString();
124
+ document.getElementById('usage-free').textContent = size(usage.availableBytes);
125
+ document.getElementById('usage-capacity').textContent = size(usage.capacityBytes);
126
+ } else clearUsage();
127
+ status.textContent = result.scanning ? 'Scan in progress. Totals are not final.' : !usage || !usage.available ? 'Storage is unavailable: the directory is missing, unreadable or shared by multiple registrations.' : (result.partial ? 'Partial scan; some paths could not be measured. ' : '') + 'Measured ' + new Date(result.checkedAt).toLocaleString() + '. Cached for up to 5 minutes.';
128
+ if (result.scanning && !document.hidden) store.set({ adminUsageTimer: setTimeout(function () { loadUsage(false); }, 1500) });
129
+ } catch (error) { if (generation === store.get('adminUsageGeneration')) status.textContent = error.message + (store.get('adminUsage') ? ' Previous measurement retained.' : ''); }
130
+ finally { if (generation === store.get('adminUsageGeneration')) { store.set({ adminUsageLoading: false }); button.disabled = false; document.getElementById('project-admin-usage').setAttribute('aria-busy', 'false'); } }
131
+ }
132
+ document.getElementById('project-admin-refresh').addEventListener('click', function () { loadUsage(true); });
133
+ document.addEventListener('visibilitychange', function () { if (document.hidden) clearTimeout(store.get('adminUsageTimer')); else if (store.get('adminUsage') && store.get('adminUsage').scanning) loadUsage(false); });
134
+ window.addEventListener('pagehide', function () { clearTimeout(store.get('adminUsageTimer')); store.set({ adminUsageGeneration: store.get('adminUsageGeneration') + 1 }); });
135
+ window.addEventListener('pageshow', function (event) { if (event.persisted) { clearUsage(); store.set({ adminUsageLoading: false }); loadUsage(false); } });
136
+ loadUsage(false);
@@ -20,3 +20,12 @@ export function groupProjects(projects) {
20
20
  }
21
21
  return { parents: parents, wtByParent: wtByParent };
22
22
  }
23
+
24
+ export function getProjectAbbrev(name) {
25
+ if (!name) return "?";
26
+ var words = name.replace(/[^a-zA-Z0-9\s]/g, "").trim().split(/\s+/);
27
+ if (words.length >= 2) {
28
+ return (words[0][0] + words[1][0]).toUpperCase();
29
+ }
30
+ return name.substring(0, 2).toUpperCase();
31
+ }
@@ -4,6 +4,23 @@ import { showToast } from './utils.js';
4
4
  import { confirmSettingsAction, clearSettingsDraft, setSettingsDraft } from './server-settings-state.js';
5
5
  import { enhanceSettingsSelects } from './settings-select.js';
6
6
 
7
+ var controlSettings = {
8
+ set_keep_awake: { config: 'keepAwake', field: 'value' },
9
+ set_image_retention: { config: 'imageRetentionDays', field: 'days' },
10
+ set_update_channel: { config: 'updateChannel', field: 'channel' }
11
+ };
12
+ function controlValue(input, type) {
13
+ if (type === 'set_image_retention') return Number(input.value);
14
+ if (type === 'set_update_channel') return input.checked ? 'beta' : 'stable';
15
+ return input.checked;
16
+ }
17
+ function savedControlValue(type) {
18
+ var value = (store.get('serverSettingsConfig') || {})[controlSettings[type].config];
19
+ if (type === 'set_image_retention') return value === undefined ? 7 : Number(value);
20
+ if (type === 'set_update_channel') return value === 'beta' ? 'beta' : 'stable';
21
+ return !!value;
22
+ }
23
+
7
24
  function status(id, text) {
8
25
  var el = document.getElementById(id);
9
26
  if (el) { el.textContent = text; el.classList.remove('hidden'); }
@@ -19,7 +36,7 @@ function sendSetting(type, values, buttonId, statusId) {
19
36
  if (input) { input.disabled = true; input.dispatchEvent(new Event('settings-sync')); }
20
37
  status(statusId, 'Saving…');
21
38
  var pending = Object.assign({}, store.get('serverSettingsPending') || {});
22
- pending[type] = { buttonId: buttonId, statusId: statusId };
39
+ pending[type] = { buttonId: buttonId, statusId: statusId, submitted: controlSettings[type] ? values[controlSettings[type].field] : null };
23
40
  store.set({ serverSettingsPending: pending });
24
41
  ws.send(JSON.stringify(Object.assign({ type: type }, values)));
25
42
  setTimeout(function () {
@@ -51,14 +68,25 @@ export function handleServerSettingResult(msg) {
51
68
  var input = request.buttonId === 'settings-pin-save-btn' ? document.getElementById('settings-pin-input') : request.buttonId.endsWith('-save') ? document.getElementById(request.buttonId.slice(0, -5)) : null;
52
69
  if (input) { input.disabled = false; input.dispatchEvent(new Event('settings-sync')); }
53
70
  status(request.statusId, msg.ok ? 'Saved. ' + (msg.timing || '') : (msg.error || 'Could not save. Retry your change.'));
54
- if (msg.ok) clearSettingsDraft(type);
71
+ if (msg.ok) {
72
+ var setting = controlSettings[type];
73
+ if (setting) {
74
+ var config = Object.assign({}, store.get('serverSettingsConfig') || {});
75
+ config[setting.config] = request.submitted;
76
+ store.set({ serverSettingsConfig: config });
77
+ var dirty = input && controlValue(input, type) !== savedControlValue(type);
78
+ setSettingsDraft(type, dirty);
79
+ if (button) button.hidden = !dirty;
80
+ if (dirty) status(request.statusId, 'Submitted value saved. Newer edits remain unsaved.');
81
+ } else clearSettingsDraft(type);
82
+ }
55
83
  }
56
84
 
57
85
  function finishSetting(msg, id, field) {
58
86
  handleServerSettingResult(msg);
59
87
  var el = document.getElementById(id);
60
88
  var key = 'set_keep_awake';
61
- if (!msg.type.endsWith('_result') && (store.get('serverSettingsDrafts') || {})[key]) return;
89
+ if ((store.get('serverSettingsDrafts') || {})[key]) return;
62
90
  if (el && msg.ok !== false && msg[field] !== undefined) el.checked = !!msg[field];
63
91
  }
64
92
 
@@ -108,7 +136,8 @@ export function initServerControls() {
108
136
  var save = document.createElement('button'); save.type = 'button'; save.className = 'settings-btn-sm'; save.textContent = 'Save changes';
109
137
  save.id = entry[0] + '-save'; save.hidden = true; receipt.insertAdjacentElement('beforebegin', save);
110
138
  input.onchange = function () {
111
- setSettingsDraft(entry[1], true); save.hidden = false; receipt.textContent = entry[3];
139
+ var dirty = controlValue(input, entry[1]) !== savedControlValue(entry[1]);
140
+ setSettingsDraft(entry[1], dirty); save.hidden = !dirty; receipt.textContent = dirty ? entry[3] : 'No unsaved changes';
112
141
  };
113
142
  save.onclick = function () {
114
143
  var values = {}; values[entry[2]] = entry[2] === 'days' ? Number(input.value) : entry[2] === 'channel' ? (input.checked ? 'beta' : 'stable') : input.checked;
@@ -165,7 +165,7 @@ export function renderFolderToolbar(surface, displayCounts) {
165
165
  view.setAttribute("aria-haspopup", "true");
166
166
  var viewState = store.get('sessionFolderViewMenu');
167
167
  view.setAttribute("aria-expanded", String(!!(viewState && viewState.surface === surface)));
168
- view.innerHTML = iconHtml("sliders-horizontal") + "<span>View</span>";
168
+ view.innerHTML = iconHtml("sliders-horizontal") + "<span>View options</span>";
169
169
  view.addEventListener("click", function () { toggleViewMenu(surface); });
170
170
 
171
171
  var magnifier = document.createElement("button");
@@ -0,0 +1,86 @@
1
+ import { clayToolGroupSummary } from './clay-tool-presentation.js';
2
+ import { iconHtml } from './icons.js';
3
+
4
+ // Shared stateless activity chrome for live sessions and read-only transcripts.
5
+ export function createSessionToolItem(id, name) {
6
+ var el = document.createElement("div");
7
+ el.className = "tool-item";
8
+ if (id != null) el.dataset.toolId = id;
9
+ el.innerHTML =
10
+ '<div class="tool-header">' +
11
+ '<span class="tool-chevron">' + iconHtml("chevron-right") + '</span>' +
12
+ '<span class="tool-bullet"></span>' +
13
+ '<span class="tool-name"></span>' +
14
+ '<span class="tool-desc"></span>' +
15
+ '<span class="tool-status-icon">' + iconHtml("loader", "icon-spin") + '</span>' +
16
+ '</div>' +
17
+ '<div class="tool-subtitle">' +
18
+ '<span class="tool-connector">&#9492;</span>' +
19
+ '<span class="tool-subtitle-text">Running...</span>' +
20
+ '</div>';
21
+
22
+ el.querySelector(".tool-name").textContent = name;
23
+
24
+ return el;
25
+ }
26
+
27
+ export function createSessionToolGroup(avatarUrl) {
28
+ var group = document.createElement('div'); group.className = 'tool-group';
29
+ var parent = group;
30
+ if (avatarUrl) {
31
+ var image = document.createElement('img'); image.className = 'dm-bubble-avatar dm-bubble-avatar-mate'; image.src = avatarUrl; image.alt = ''; group.appendChild(image);
32
+ parent = document.createElement('div'); parent.className = 'dm-bubble-content'; group.appendChild(parent);
33
+ }
34
+ parent.innerHTML = '<div class="tool-group-header" style="display:none"><span class="tool-group-chevron">' + iconHtml('chevron-right') + '</span><span class="tool-group-bullet"></span><span class="tool-group-label">Running...</span><span class="tool-group-status-icon">' + iconHtml('loader', 'icon-spin') + '</span></div><div class="tool-group-items"></div>';
35
+ return group;
36
+ }
37
+
38
+ export function sessionToolGroupSummary(group, tools) {
39
+ var claySummary = clayToolGroupSummary(group, tools);
40
+ if (claySummary) return claySummary;
41
+ var names = group.toolNames;
42
+ var count = names.length;
43
+ var allDone = group.doneCount >= count;
44
+
45
+ // Count by tool name
46
+ var counts = {};
47
+ for (var i = 0; i < names.length; i++) {
48
+ counts[names[i]] = (counts[names[i]] || 0) + 1;
49
+ }
50
+ var uniqueNames = Object.keys(counts);
51
+
52
+ if (uniqueNames.length === 1) {
53
+ var name = uniqueNames[0];
54
+ var n = counts[name];
55
+ if (allDone) {
56
+ switch (name) {
57
+ case "Read": return "Read " + n + " file" + (n > 1 ? "s" : "");
58
+ case "Edit": return "Edited " + n + " file" + (n > 1 ? "s" : "");
59
+ case "Write": return "Wrote " + n + " file" + (n > 1 ? "s" : "");
60
+ case "Bash": return "Ran " + n + " command" + (n > 1 ? "s" : "");
61
+ case "Grep": return "Searched " + n + " pattern" + (n > 1 ? "s" : "");
62
+ case "Glob": return "Found " + n + " pattern" + (n > 1 ? "s" : "");
63
+ case "Task": return "Ran " + n + " task" + (n > 1 ? "s" : "");
64
+ case "WebSearch": return "Searched " + n + " quer" + (n > 1 ? "ies" : "y");
65
+ case "WebFetch": return "Fetched " + n + " URL" + (n > 1 ? "s" : "");
66
+ default: return "Ran " + n + " tool" + (n > 1 ? "s" : "");
67
+ }
68
+ }
69
+ switch (name) {
70
+ case "Read": return "Reading " + n + " file" + (n > 1 ? "s" : "") + "...";
71
+ case "Edit": return "Editing " + n + " file" + (n > 1 ? "s" : "") + "...";
72
+ case "Write": return "Writing " + n + " file" + (n > 1 ? "s" : "") + "...";
73
+ case "Bash": return "Running " + n + " command" + (n > 1 ? "s" : "") + "...";
74
+ case "Grep": return "Searching " + n + " pattern" + (n > 1 ? "s" : "") + "...";
75
+ case "Glob": return "Finding " + n + " pattern" + (n > 1 ? "s" : "") + "...";
76
+ case "Task": return "Running " + n + " task" + (n > 1 ? "s" : "") + "...";
77
+ case "WebSearch": return "Searching " + n + " quer" + (n > 1 ? "ies" : "y") + "...";
78
+ case "WebFetch": return "Fetching " + n + " URL" + (n > 1 ? "s" : "") + "...";
79
+ default: return "Running " + n + " tool" + (n > 1 ? "s" : "") + "...";
80
+ }
81
+ }
82
+
83
+ // Mixed tools
84
+ if (allDone) return "Ran " + count + " tools";
85
+ return "Running " + count + " tools...";
86
+ }
@@ -55,8 +55,9 @@ export function enhanceSettingsSelects(root) {
55
55
  item.hidden = option.hidden;
56
56
  item.onclick = function (event) {
57
57
  event.preventDefault();
58
+ var changed = select.value !== option.value;
58
59
  select.value = option.value;
59
- select.dispatchEvent(new Event('change', { bubbles: true }));
60
+ if (changed) select.dispatchEvent(new Event('change', { bubbles: true }));
60
61
  sync();
61
62
  close(true);
62
63
  };
@@ -7,37 +7,53 @@ export function renderBrowserTabs(el, select, send) {
7
7
  var selected = store.get('sharedBrowser');
8
8
  var key = JSON.stringify([tabs.map(function (tab) { return [tab.id, tab.url, tab.control, tab.phase, tab.incognito, tab.personalCookiesAvailable]; }), selected && selected.id, store.get('connected')]);
9
9
  if (bar.dataset.key === key) return;
10
- bar.dataset.key = key;
10
+ var focused = bar.contains(document.activeElement) ? document.activeElement : null;
11
+ var focusedId = focused && focused.dataset.browserId, focusedAction = focused && focused.dataset.tabAction;
12
+ var previousSelection = bar.dataset.selected;
13
+ var previousStrip = bar.querySelector('.shared-browser-tab-list'), scrollLeft = previousStrip ? previousStrip.scrollLeft : 0;
14
+ bar.dataset.key = key; bar.dataset.selected = selected && selected.id || '';
11
15
  bar.replaceChildren();
16
+ var strip = document.createElement('div'); strip.className = 'shared-browser-tab-list'; strip.setAttribute('role', 'presentation');
17
+ bar.appendChild(strip);
12
18
  tabs.forEach(function (tab, index) {
13
- var item = document.createElement('div'); item.className = 'shared-browser-tab';
19
+ var item = document.createElement('div'); item.className = 'shared-browser-tab'; item.setAttribute('role', 'presentation');
20
+ var active = !!selected && selected.id === tab.id;
14
21
  var button = document.createElement('button'); button.type = 'button'; button.setAttribute('role', 'tab');
15
- button.setAttribute('aria-selected', String(!!selected && selected.id === tab.id));
16
- var title = 'Tab ' + (index + 1);
17
- try { if (tab.url) title = new URL(tab.url).hostname; } catch (e) {}
18
- var label = document.createElement('span'); label.textContent = title + (tab.personalCookiesAvailable && tab.incognito ? ' · Incognito' : '');
19
- var owner = document.createElement('small'); owner.textContent = tab.phase === 'ended' ? 'Ended' : tab.control === 'agent' ? 'Shared' : 'You only';
20
- button.append(label, owner); button.title = title + ' · ' + (tab.incognito === false ? 'Personal browser' : 'Incognito') + ' · ' + (tab.phase === 'ended' ? 'Ended' : tab.control === 'agent' ? 'You and Clay can control this tab' : 'Only you can control this tab. Clay is paused.');
22
+ button.dataset.browserId = tab.id; button.dataset.tabAction = 'select';
23
+ button.setAttribute('aria-selected', String(active)); button.tabIndex = active ? 0 : -1;
24
+ var title = 'New tab';
25
+ try { if (tab.url) title = new URL(tab.url).hostname || 'New tab'; } catch (e) {}
26
+ var mode = tab.phase === 'ended' ? 'Ended' : tab.phase === 'starting' ? 'Starting' : tab.control === 'agent' ? 'You and Clay can control this tab' : 'Only you can control this tab. Clay is paused.';
27
+ var description = title + ' · ' + (tab.incognito === false ? 'Personal browser' : 'Incognito') + ' · ' + mode;
28
+ var label = document.createElement('span'); label.className = 'shared-browser-tab-label'; label.textContent = title;
29
+ button.innerHTML = iconHtml(tab.personalCookiesAvailable && tab.incognito ? 'glasses' : 'globe');
30
+ button.appendChild(label); button.setAttribute('aria-label', description); button.setAttribute('data-tip', description);
31
+ item.dataset.phase = tab.phase; item.dataset.control = tab.control;
21
32
  button.onclick = function () { select(tab.id); };
22
33
  button.onkeydown = function (event) {
23
34
  var next = event.key === 'ArrowRight' ? (index + 1) % tabs.length : event.key === 'ArrowLeft' ? (index + tabs.length - 1) % tabs.length : event.key === 'Home' ? 0 : event.key === 'End' ? tabs.length - 1 : -1;
24
35
  if (next < 0) return;
25
36
  event.preventDefault(); select(tabs[next].id); bar.querySelectorAll('[role="tab"]')[next].focus();
26
37
  };
27
- var close = document.createElement('button'); close.type = 'button'; close.textContent = '×'; close.setAttribute('aria-label', 'Close browser tab ' + (index + 1)); close.disabled = !store.get('connected');
38
+ var close = document.createElement('button'); close.type = 'button'; close.className = 'shared-browser-tab-close'; close.innerHTML = iconHtml('x');
39
+ close.dataset.browserId = tab.id; close.dataset.tabAction = 'close';
40
+ close.setAttribute('aria-label', 'Close browser tab ' + (index + 1)); close.setAttribute('data-tip', 'Close tab'); close.disabled = !store.get('connected');
28
41
  close.onclick = function () { send('close', { browserId: tab.id }); };
29
- item.append(button, close); bar.appendChild(item);
42
+ item.append(button, close); strip.appendChild(item);
30
43
  });
31
- var add = document.createElement('button'); add.type = 'button'; add.textContent = '+'; add.setAttribute('aria-label', 'New browser tab'); add.disabled = !store.get('connected');
32
- add.title = selected && selected.personalCookiesAvailable ? 'New tab · Personal browser' : 'New incognito tab';
33
- add.onclick = function () { send('open', { newTab: true }); };
34
- bar.appendChild(add);
35
- if (selected && selected.personalCookiesAvailable) {
36
- var incognito = document.createElement('button'); incognito.type = 'button';
37
- incognito.innerHTML = iconHtml('glasses');
38
- incognito.setAttribute('aria-label', 'New incognito tab'); incognito.title = 'New incognito tab · Site data is not saved';
39
- incognito.disabled = !store.get('connected');
40
- incognito.onclick = function () { send('open', { newTab: true, incognito: true }); };
41
- bar.appendChild(incognito); refreshIcons();
44
+ function addButton(icon, label, description, fields, action) {
45
+ var button = document.createElement('button'); button.type = 'button'; button.className = 'shared-browser-new-tab';
46
+ button.innerHTML = iconHtml(icon); button.dataset.tabAction = action;
47
+ button.setAttribute('aria-label', label); button.setAttribute('data-tip', description); button.disabled = !store.get('connected');
48
+ button.onclick = function () { send('open', fields); }; bar.appendChild(button);
42
49
  }
50
+ addButton('plus', 'New browser tab', selected && selected.personalCookiesAvailable ? 'New tab · Personal browser' : 'New incognito tab', { newTab: true }, 'new');
51
+ if (selected && selected.personalCookiesAvailable) addButton('glasses', 'New incognito tab', 'New incognito tab · Site data is not saved', { newTab: true, incognito: true }, 'incognito');
52
+ refreshIcons();
53
+ strip.scrollLeft = scrollLeft;
54
+ var buttons = Array.from(bar.querySelectorAll('button'));
55
+ var replacement = focused && buttons.find(function (button) { return button.dataset.browserId === focusedId && button.dataset.tabAction === focusedAction; });
56
+ if (focused) (replacement || bar.querySelector('[aria-selected="true"]') || buttons[0]).focus({ preventScroll: true });
57
+ var current = bar.querySelector('[aria-selected="true"]');
58
+ if (current && previousSelection !== bar.dataset.selected) current.scrollIntoView({ block: 'nearest', inline: 'nearest' });
43
59
  }
@@ -15,7 +15,7 @@ import { openAddProjectModal } from './project-start-dialog.js';
15
15
  import { showHomeHub } from './app-home-hub.js';
16
16
  import { getPendingTuiAttention } from './app-notifications.js';
17
17
  import { isExternalWorktree, externalWorktreeTooltip, appendExternalWorktreeBadge } from './worktree-location.js';
18
- import { groupProjects } from './project-grouping.js';
18
+ import { groupProjects, getProjectAbbrev } from './project-grouping.js';
19
19
  import { showProjectAccessPopover } from './project-access.js';
20
20
  import { renderProjectMates, mateForWorkspace } from './project-mate-navigation.js';
21
21
  import { openProjectCreateChoice } from './project-mate-creation-workspace.js';
@@ -337,15 +337,6 @@ export function initSidebarProjects() {
337
337
  export function getCachedProjectList() { return cachedProjectList; }
338
338
  export function getCachedCurrentSlug() { return cachedCurrentSlug; }
339
339
 
340
- function getProjectAbbrev(name) {
341
- if (!name) return "?";
342
- var words = name.replace(/[^a-zA-Z0-9\s]/g, "").trim().split(/\s+/);
343
- if (words.length >= 2) {
344
- return (words[0][0] + words[1][0]).toUpperCase();
345
- }
346
- return name.substring(0, 2).toUpperCase();
347
- }
348
-
349
340
  export { getProjectAbbrev };
350
341
 
351
342
  // --- Project context menu ---
@@ -0,0 +1,47 @@
1
+ import { store } from './store.js';
2
+ import { currentLayoutScope } from './split-view-layout-scope.js';
3
+
4
+ export function clearEmptySplit(host) {
5
+ var pane = host && host.querySelector(':scope > .split-session-empty-pane');
6
+ var menu = document.getElementById('split-view-create-menu');
7
+ if (pane && menu && pane.contains(menu)) {
8
+ menu.hidden = true;
9
+ if (menu.contains(document.activeElement)) document.activeElement.blur();
10
+ if (pane.contains(menu)) document.body.appendChild(menu);
11
+ }
12
+ if (pane) pane.remove();
13
+ var divider = host && host.querySelector(':scope > .split-session-reserved-divider');
14
+ if (divider) divider.remove();
15
+ }
16
+
17
+ export function reserveEmptySplit(host, geometry, inset) {
18
+ var state = store.get('splitSessionPicker');
19
+ var receipt = state && state.receipt;
20
+ var target = receipt && geometry.panes[receipt.origin];
21
+ if (!state || !state.side || !target || !currentLayoutScope(receipt.scope)) { clearEmptySplit(host); return false; }
22
+ var side = state.side, vertical = side === 'top' || side === 'bottom', before = side === 'left' || side === 'top';
23
+ var size = vertical ? 'height' : 'width', axis = vertical ? 'y' : 'x';
24
+ var half = Math.max(0, (target[size] - 7) / 2);
25
+ var original = Object.assign({}, target), empty = Object.assign({}, target), gap = Object.assign({}, target);
26
+ original[size] = half; empty[size] = half; gap[size] = 7;
27
+ gap[axis] += half;
28
+ if (before) original[axis] += half + 7; else empty[axis] += half + 7;
29
+ geometry.panes[receipt.origin] = original;
30
+ var pane = host.querySelector(':scope > .split-session-empty-pane');
31
+ if (!pane) {
32
+ pane = document.createElement('section'); pane.className = 'split-session-empty-pane';
33
+ pane.innerHTML = '<div class="split-session-empty-body"></div>';
34
+ host.appendChild(pane);
35
+ }
36
+ pane.dataset.side = side; pane.setAttribute('aria-label', 'Empty ' + side + ' split');
37
+ var menu = document.getElementById('split-view-create-menu');
38
+ var body = pane.querySelector('.split-session-empty-body');
39
+ if (menu && menu.parentNode !== body) body.appendChild(menu);
40
+ var divider = host.querySelector(':scope > .split-session-reserved-divider');
41
+ if (!divider) { divider = document.createElement('div'); divider.className = 'split-session-reserved-divider'; divider.setAttribute('aria-hidden', 'true'); host.appendChild(divider); }
42
+ [[pane, empty], [divider, gap]].forEach(function (entry) {
43
+ entry[0].style.left = inset.left + entry[1].x + 'px'; entry[0].style.top = inset.top + entry[1].y + 'px';
44
+ entry[0].style.width = entry[1].width + 'px'; entry[0].style.height = entry[1].height + 'px';
45
+ });
46
+ return true;
47
+ }