clay-server 5.5.0-beta.4 → 5.5.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 (104) hide show
  1. package/lib/github-connection-store.js +39 -0
  2. package/lib/github-runtime.js +43 -0
  3. package/lib/project-github-connection.js +164 -0
  4. package/lib/project.js +8 -0
  5. package/lib/public/appearance-bootstrap.js +6 -0
  6. package/lib/public/css/chrome-material.css +133 -0
  7. package/lib/public/css/menus.css +1 -17
  8. package/lib/public/css/navigation-island.css +261 -0
  9. package/lib/public/css/notifications-center.css +1 -17
  10. package/lib/public/css/settings-ui.css +8 -2
  11. package/lib/public/css/shared-browser.css +1 -18
  12. package/lib/public/css/split-view-layout.css +9 -5
  13. package/lib/public/css/sticky-notes.css +1 -18
  14. package/lib/public/css/title-bar.css +1 -1
  15. package/lib/public/css/workspace-background-gallery.css +43 -0
  16. package/lib/public/css/you.css +13 -10
  17. package/lib/public/index.html +53 -46
  18. package/lib/public/modules/app-messages.js +5 -0
  19. package/lib/public/modules/appearance-preferences.js +81 -0
  20. package/lib/public/modules/context-sources.js +2 -282
  21. package/lib/public/modules/github-mark.js +2 -0
  22. package/lib/public/modules/pane-appearance-bridge.js +9 -1
  23. package/lib/public/modules/pane-header-tooltips.js +36 -9
  24. package/lib/public/modules/personal-email-editor.js +305 -0
  25. package/lib/public/modules/personal-integration-editor.js +70 -0
  26. package/lib/public/modules/personal-settings-dialog.js +15 -1
  27. package/lib/public/modules/session-list-material.js +25 -0
  28. package/lib/public/modules/sidebar-collapse-focus.js +22 -0
  29. package/lib/public/modules/sidebar-sessions.js +2 -0
  30. package/lib/public/modules/sidebar.js +9 -10
  31. package/lib/public/modules/split-pane-renderer.js +5 -5
  32. package/lib/public/modules/split-view-shell-controls.js +9 -5
  33. package/lib/public/modules/split-view.js +2 -2
  34. package/lib/public/modules/theme.js +10 -28
  35. package/lib/public/modules/tool-pane-chrome.js +4 -1
  36. package/lib/public/modules/tool-pane-material.js +23 -0
  37. package/lib/public/modules/tooltip.js +7 -3
  38. package/lib/public/modules/tui-attention.js +9 -4
  39. package/lib/public/modules/user-settings-account.js +10 -1
  40. package/lib/public/modules/user-settings-github.js +176 -0
  41. package/lib/public/modules/user-settings-integrations.js +23 -12
  42. package/lib/public/modules/user-settings-layout.js +5 -3
  43. package/lib/public/modules/user-settings-themes.js +6 -2
  44. package/lib/public/modules/user-settings.js +2 -1
  45. package/lib/public/modules/workspace-background-gallery.js +92 -0
  46. package/lib/public/modules/workspace-custom-colors.js +132 -0
  47. package/lib/public/modules/you-activity.js +4 -2
  48. package/lib/public/pane-assets/app-C27ONMHZ.js +1 -0
  49. package/lib/public/pane-assets/chunk-4REWYEXI.js +1 -0
  50. package/lib/public/pane-assets/chunk-6EDR4PMU.js +1 -0
  51. package/lib/public/pane-assets/{chunk-X45WI2U7.js → chunk-76JE6SO6.js} +1 -1
  52. package/lib/public/pane-assets/chunk-AFATDM32.js +1 -0
  53. package/lib/public/pane-assets/{chunk-UVTS6GUM.js → chunk-DTNAWMGQ.js} +1 -1
  54. package/lib/public/pane-assets/chunk-EGCR2MH4.js +1 -0
  55. package/lib/public/pane-assets/chunk-ET4NUBYW.js +1 -0
  56. package/lib/public/pane-assets/chunk-EYJZ3R7Y.js +1 -0
  57. package/lib/public/pane-assets/{chunk-FB5B2WYF.js → chunk-I5Q4CZDJ.js} +1 -1
  58. package/lib/public/pane-assets/{chunk-53KRXJDL.js → chunk-KJN6KGEN.js} +1 -1
  59. package/lib/public/pane-assets/chunk-KZ3CYS5U.js +1 -0
  60. package/lib/public/pane-assets/chunk-MR4FHQFA.js +1 -0
  61. package/lib/public/pane-assets/chunk-NRC5RDIL.js +1 -0
  62. package/lib/public/pane-assets/chunk-NRP4NXVZ.js +1 -0
  63. package/lib/public/pane-assets/chunk-OERDYIHU.js +1 -0
  64. package/lib/public/pane-assets/{chunk-TK446XFF.js → chunk-OGIHAXLD.js} +1 -1
  65. package/lib/public/pane-assets/chunk-OQBKX36J.js +2 -0
  66. package/lib/public/pane-assets/chunk-Q2HJ5CWT.js +1 -0
  67. package/lib/public/pane-assets/{chunk-4WYURTPH.js → chunk-QOL6PHMZ.js} +1 -1
  68. package/lib/public/pane-assets/{chunk-AMZ7P526.js → chunk-TVKTFEST.js} +1 -1
  69. package/lib/public/pane-assets/{chunk-TU3AHPSN.js → chunk-UMXZTYOY.js} +1 -1
  70. package/lib/public/pane-assets/chunk-W5MFUKXJ.js +1 -0
  71. package/lib/public/pane-assets/chunk-W7XXER5Z.js +174 -0
  72. package/lib/public/pane-assets/chunk-Y77DR2RU.js +1 -0
  73. package/lib/public/pane-assets/{chunk-SGMEO3P7.js → chunk-YBYNU4VI.js} +1 -1
  74. package/lib/public/pane-assets/{chunk-JIV7O5GN.js → chunk-YHBTK56T.js} +1 -1
  75. package/lib/public/pane-assets/manifest.json +451 -437
  76. package/lib/public/pane-assets/style-F4MQRRX2.css +1 -0
  77. package/lib/public/style.css +4 -0
  78. package/lib/public/workspace-art/workshop-dark.webp +0 -0
  79. package/lib/public/workspace-art/workshop-light.webp +0 -0
  80. package/lib/public/workspace-background-catalog.js +40 -0
  81. package/lib/server-appearance-settings.js +16 -5
  82. package/lib/server.js +6 -0
  83. package/lib/session-github.js +1 -7
  84. package/lib/terminal-manager.js +5 -1
  85. package/lib/terminal.js +10 -1
  86. package/package.json +1 -1
  87. package/lib/public/pane-assets/app-PV3SEOTX.js +0 -1
  88. package/lib/public/pane-assets/chunk-2G6DB3ES.js +0 -1
  89. package/lib/public/pane-assets/chunk-6QFLYOVY.js +0 -1
  90. package/lib/public/pane-assets/chunk-BK2NRZFM.js +0 -1
  91. package/lib/public/pane-assets/chunk-D3WR2TYZ.js +0 -2
  92. package/lib/public/pane-assets/chunk-E7D5A27I.js +0 -1
  93. package/lib/public/pane-assets/chunk-FDBSE7BE.js +0 -174
  94. package/lib/public/pane-assets/chunk-FHB76KH2.js +0 -1
  95. package/lib/public/pane-assets/chunk-FNGO6432.js +0 -1
  96. package/lib/public/pane-assets/chunk-HF7AK56Q.js +0 -1
  97. package/lib/public/pane-assets/chunk-ILVRGTDF.js +0 -1
  98. package/lib/public/pane-assets/chunk-L3SFMSLP.js +0 -1
  99. package/lib/public/pane-assets/chunk-PAY3A4JS.js +0 -1
  100. package/lib/public/pane-assets/chunk-QVHC324H.js +0 -1
  101. package/lib/public/pane-assets/chunk-SFANEWZA.js +0 -1
  102. package/lib/public/pane-assets/chunk-US5VELGB.js +0 -1
  103. package/lib/public/pane-assets/chunk-VZ2OKDAO.js +0 -1
  104. package/lib/public/pane-assets/style-563D4UN6.css +0 -1
@@ -225,7 +225,6 @@ export function handlePaneSearchMessage(event, host) {
225
225
  }
226
226
  return false;
227
227
  }
228
-
229
228
  function startPaneRename(header, titleEl, pane) {
230
229
  if (header.querySelector(".split-pane-rename-input")) return;
231
230
  var section = header.parentNode, frame = section.querySelector('.split-pane-frame');
@@ -234,7 +233,7 @@ function startPaneRename(header, titleEl, pane) {
234
233
  initial.binding.sessionOriginId !== frame.dataset.sessionOriginId) return;
235
234
  var input = document.createElement("input");
236
235
  input.type = "text"; input.className = "split-pane-rename-input"; input.value = pane.title;
237
- titleEl.style.display = "none"; header.insertBefore(input, titleEl); input.focus(); input.select();
236
+ titleEl.style.display = "none"; titleEl.parentNode.insertBefore(input, titleEl); input.focus(); input.select();
238
237
  var done = false;
239
238
  var origin = frame.dataset.sessionOriginId, binding = initial.binding, socket = getWs();
240
239
  function current() {
@@ -293,8 +292,9 @@ export function createSplitPane(pane, closePane) {
293
292
  var session = sessionById(pane.sessionId); var vendor = session && session.vendor || "unknown";
294
293
  var vendorIcon = document.createElement("img"); vendorIcon.className = "split-pane-vendor"; vendorIcon.hidden = !VENDOR_AVATARS[vendor];
295
294
  if (!vendorIcon.hidden) vendorIcon.src = VENDOR_AVATARS[vendor]; vendorIcon.alt = ""; vendorIcon.title = VENDOR_NAMES[vendor] || vendor; header.appendChild(vendorIcon);
295
+ var identity = document.createElement("span"); identity.className = "split-pane-identity"; header.appendChild(identity);
296
296
  var title = document.createElement("span"); title.className = "split-pane-title"; title.textContent = pane.title; title.title = "Rename session";
297
- title.addEventListener("click", function () { if (title.getAttribute('aria-disabled') === 'true' || !syncPaneHeaderState(paneEl)) return; var current = sessionById(Number(paneEl.dataset.sessionId)); if (current) startPaneRename(header, title, { sessionId: current.id, title: current.title }); }); header.appendChild(title);
297
+ title.addEventListener("click", function () { if (title.getAttribute('aria-disabled') === 'true' || !syncPaneHeaderState(paneEl)) return; var current = sessionById(Number(paneEl.dataset.sessionId)); if (current) startPaneRename(header, title, { sessionId: current.id, title: current.title }); }); identity.appendChild(title);
298
298
  var dragSpace = document.createElement("span"); dragSpace.className = "split-pane-drag-space"; dragSpace.title = "Drag to rearrange panes"; dragSpace.setAttribute("aria-hidden", "true"); header.appendChild(dragSpace);
299
299
  var frame = document.createElement("iframe"); frame.className = "split-pane-frame"; frame.dataset.projectSlug = pane.slug;
300
300
  frame.dataset.sessionId = String(pane.sessionId); frame.src = paneUrl(pane); frame.title = pane.title;
@@ -323,7 +323,7 @@ export function createSplitPane(pane, closePane) {
323
323
  }
324
324
  close.dataset.sessionId = String(pane.sessionId); close.addEventListener("click", function () { if (syncPaneHeaderState(paneEl)) closePane(Number(this.dataset.sessionId)); }); header.appendChild(close);
325
325
  var info = document.createElement("button"); info.type = "button"; info.className = "split-pane-info"; info.setAttribute("aria-label", "Session information"); info.title = "Session information"; info.innerHTML = iconHtml("info");
326
- info.addEventListener("click", function (event) { event.stopPropagation(); if (syncPaneHeaderState(paneEl)) togglePaneInfo(info, frame); }); header.appendChild(info);
326
+ info.addEventListener("click", function (event) { event.stopPropagation(); if (syncPaneHeaderState(paneEl)) togglePaneInfo(info, frame); }); identity.insertBefore(info, title.nextSibling);
327
327
  var sidebarButton = document.createElement("button"); sidebarButton.type = "button"; sidebarButton.className = "split-pane-sidebar";
328
328
  var sidebarExpanded = !document.getElementById("layout").classList.contains("sidebar-collapsed");
329
329
  sidebarButton.setAttribute("aria-label", sidebarExpanded ? "Collapse sidebar" : "Open sidebar");
@@ -378,7 +378,7 @@ export function createSplitPane(pane, closePane) {
378
378
  var runtime = document.createElement("div"); runtime.className = "split-view-runtime";
379
379
  vendorIcon.classList.add("split-view-runtime-icon"); runtime.appendChild(vendorIcon);
380
380
  var runtimeModel = document.createElement("span"); runtimeModel.className = "split-view-model"; runtime.appendChild(runtimeModel);
381
- header.insertBefore(runtime, title); header.appendChild(close); paneEl.appendChild(frame);
381
+ header.insertBefore(runtime, identity); header.appendChild(close); paneEl.appendChild(frame);
382
382
  syncPaneHeaderState(paneEl);
383
383
  return paneEl;
384
384
  }
@@ -1,21 +1,23 @@
1
1
  import { toggleSidebarCollapse } from './sidebar.js';
2
2
 
3
- export function syncSidebarReopenButton(button, active) {
3
+ export function syncSidebarReopenButton(button) {
4
4
  if (!button) return;
5
5
  var layout = document.getElementById("layout");
6
6
  var desktop = !window.matchMedia || window.matchMedia("(min-width: 769px)").matches;
7
- button.hidden = !layout || !desktop || !active || !layout.classList.contains("sidebar-collapsed");
7
+ button.hidden = !layout || !desktop || !layout.classList.contains("sidebar-collapsed");
8
8
  }
9
9
 
10
10
  export function createSidebarReopenButton() {
11
- var target = document.querySelector("#top-bar .top-bar-left-pills");
11
+ var target = document.getElementById("icon-strip");
12
12
  if (!target) return null;
13
+ var existing = document.getElementById("sidebar-shell-expand-btn");
14
+ if (existing) return existing;
13
15
  var button = document.createElement("button");
14
16
  button.id = "sidebar-shell-expand-btn";
15
17
  button.type = "button";
16
18
  button.title = "Open sidebar";
17
19
  button.setAttribute("aria-label", "Open sidebar");
18
- button.setAttribute("aria-controls", "sidebar-column");
20
+ button.setAttribute("aria-controls", "sidebar-column mate-sidebar-column");
19
21
  button.setAttribute("aria-expanded", "false");
20
22
  button.innerHTML = '<i data-lucide="panel-left-open"></i>';
21
23
  button.hidden = true;
@@ -23,6 +25,8 @@ export function createSidebarReopenButton() {
23
25
  if (button.hidden) return;
24
26
  toggleSidebarCollapse();
25
27
  });
26
- target.prepend(button);
28
+ var hint = document.getElementById("icon-strip-hint-project");
29
+ hint.parentNode.insertBefore(button, hint);
30
+ syncSidebarReopenButton(button);
27
31
  return button;
28
32
  }
@@ -200,7 +200,7 @@ function renderSplit(split) {
200
200
  else clearRetainedPanes(host);
201
201
  var panes = split && split.panes;
202
202
  if (!panes || panes.length < 2) {
203
- if (shellSidebarExpand) shellSidebarExpand.hidden = true;
203
+ syncSidebarReopenButton(shellSidebarExpand);
204
204
  placeStickyNotesOverlay(false);
205
205
  var frames = host.querySelectorAll('.split-pane-frame');
206
206
  var retain = frames.length > 0 && frames.length <= 3 && Array.from(frames).every(function (frame) { return frame.dataset.projectSlug === store.get('currentSlug') && frame.clayPane; });
@@ -212,7 +212,7 @@ function renderSplit(split) {
212
212
  nativeApp.classList.remove("split-native-hidden");
213
213
  return;
214
214
  }
215
- if (shellSidebarExpand) shellSidebarExpand.hidden = !document.getElementById("layout").classList.contains("sidebar-collapsed");
215
+ syncSidebarReopenButton(shellSidebarExpand);
216
216
  placeStickyNotesOverlay(true);
217
217
  nativeApp.classList.add("split-native-hidden");
218
218
  host.classList.add("visible");
@@ -1,4 +1,5 @@
1
1
  import { store } from './store.js';
2
+ import { appearanceStatus, queueAppearanceSave, initWorkspaceBackground, effectiveAppearance, workspaceBackgrounds } from './appearance-preferences.js';
2
3
  import { refreshIcons } from './icons.js';
3
4
  import { setTerminalTheme } from './terminal.js';
4
5
  import { setTuiSessionTheme } from './session-tui-view.js';
@@ -120,10 +121,10 @@ export function onThemeChange(fn) {
120
121
  // The appearance this frame currently shows, for the parent shell to hand to
121
122
  // its pane iframes. Never persisted from here.
122
123
  export function getAppearanceSnapshot() {
123
- var pref = store.get('appearanceDraft') || store.get('appearancePreference') || {};
124
+ var pref = effectiveAppearance();
124
125
  return { themeId: currentThemeId, userId: store.get('myUserId') || null,
125
- preference: { configured: !!pref.configured || !!pref.mode || !!pref.theme, theme: pref.theme || null, mode: pref.mode || null,
126
- lightTheme: pref.lightTheme || null, darkTheme: pref.darkTheme || null } };
126
+ preference: { configured: !!pref.configured || !!pref.mode || !!pref.theme || !!pref.background, theme: pref.theme || null, mode: pref.mode || null,
127
+ lightTheme: pref.lightTheme || null, darkTheme: pref.darkTheme || null, background: pref.background === 'workshop' ? 'workshop' : 'plain', backgroundColor: workspaceBackgrounds.getColor(pref.backgroundColor, pref.customBackgroundColors).id, customBackgroundColors: pref.customBackgroundColors || [] } };
127
128
  }
128
129
 
129
130
  // A pane iframe mirrors the parent's live appearance. The server remains the
@@ -464,6 +465,7 @@ function rebuildPicker() {
464
465
 
465
466
  // --- Init ---
466
467
  export function initTheme() {
468
+ initWorkspaceBackground();
467
469
  var seed = window.__clayAppearanceSeed;
468
470
  if (seed && seed.configured) store.set({ appearancePreference: seed });
469
471
  var initialMode = seed && seed.configured && (seed.mode === 'light' || seed.mode === 'dark')
@@ -507,11 +509,6 @@ export function initTheme() {
507
509
 
508
510
  }
509
511
 
510
- function appearanceStatus(text, error) {
511
- store.set({ appearanceSaveStatus: text, appearanceSaveError: !!error });
512
- var status = document.getElementById('us-appearance-status');
513
- if (status) { status.textContent = text; status.dataset.state = error ? 'error' : ''; }
514
- }
515
512
  export function getAppearanceChoices() {
516
513
  return { mode: (store.get('appearanceDraft') || store.get('appearancePreference') || {}).mode || 'system', lightTheme: themeIdForMode('light'), darkTheme: themeIdForMode('dark') };
517
514
  }
@@ -520,12 +517,14 @@ export function retryAppearanceSave() {
520
517
  if (draft) saveAppearance(draft.theme, draft.mode, draft);
521
518
  }
522
519
  export function selectAppearanceMode(mode) {
520
+ if ((store.get('appearanceColorEditor') || {}).saving) return;
523
521
  var effective = mode === 'system' ? getSystemMode() : mode;
524
522
  var id = themeIdForMode(effective);
525
523
  saveAppearance(id, mode === 'system' ? null : mode);
526
524
  applyTheme(id);
527
525
  }
528
526
  export function selectAppearanceTheme(mode, id) {
527
+ if ((store.get('appearanceColorEditor') || {}).saving) return;
529
528
  if (!getTheme(id) || getTheme(id).variant !== mode) return;
530
529
  var effective = getEffectiveMode();
531
530
  var current = store.get('appearanceDraft') || store.get('appearancePreference') || {};
@@ -536,34 +535,17 @@ export function selectAppearanceTheme(mode, id) {
536
535
  function saveAppearance(theme, mode, overrides) {
537
536
  var values = Object.assign({}, store.get('appearanceDraft') || store.get('appearancePreference') || {}, {theme:theme, mode:mode}, overrides || {});
538
537
  delete values.configured;
538
+ ['lightTheme', 'darkTheme'].forEach(function (key) { if (!values[key]) delete values[key]; });
539
539
  var selected = getTheme(theme);
540
540
  if (selected) values[selected.variant + 'Theme'] = theme;
541
- store.set({appearanceDraft:values});
542
- var queue = store.get('appearanceSaveQueue') || Promise.resolve();
543
- var next = queue.catch(function () {}).then(function () {return persistAppearance(values);});
544
- store.set({appearanceSaveQueue:next});
545
- return next;
546
- }
547
- async function persistAppearance(values) {
548
- var request = (store.get('appearanceSaveRequest') || 0) + 1;
549
- store.set({ appearanceSaveRequest: request });
550
- appearanceStatus('Saving appearance…');
551
- try {
552
- var response = await fetch('/api/user/appearance', {method:'PUT',headers:{'Content-Type':'application/json','X-Clay-Settings':'1'},body:JSON.stringify(values)});
553
- if (!response.ok) throw new Error('Could not save appearance. Your preview remains. Retry to save it.');
554
- if (store.get('appearanceSaveRequest') !== request) return;
555
- var saved = await response.json();
556
- store.set({ appearancePreference: saved, appearanceDraft: store.get('appearanceDraft') === values ? null : store.get('appearanceDraft') });
557
- [STORAGE_KEY,SKIN_KEY,MODE_KEY,STORAGE_KEY+'-vars',STORAGE_KEY+'-variant'].forEach(function (key) {try {localStorage.removeItem(key);}catch(error){}});
558
- appearanceStatus('Saved. Applied to this view; other devices use it when they reload.');
559
- } catch(error) {if(store.get('appearanceSaveRequest') === request) appearanceStatus(error.message,true);}
541
+ return queueAppearanceSave(values);
560
542
  }
561
543
  async function loadAppearance() {
562
544
  try {
563
545
  var response = await fetch('/api/user/appearance');
564
546
  if (!response.ok) throw new Error('Appearance preferences unavailable.');
565
547
  var data = await response.json();
566
- if(store.get('appearanceSaveRequest'))return;
548
+ if(store.get('appearanceDraft') || store.get('appearanceSaveRequest'))return;
567
549
  // A pane that already mirrors its parent's live view is newer than this GET, which may predate a pending save.
568
550
  if(externalThemeId)return;
569
551
  if (data.configured) {store.set({appearancePreference:data});applyTheme(themeIdForMode(data.mode || getSystemMode()));}
@@ -1,3 +1,4 @@
1
+ import { bindToolPaneMaterial } from './tool-pane-material.js';
1
2
  import { ensureWorkbenchWindowControls, syncWorkbenchWindowControls } from './workbench-window-controls.js';
2
3
  import { WORKBENCH_PANELS } from './workbench-panels.js';
3
4
  import { recordToolExpansion } from './view-presentation-state.js';
@@ -16,7 +17,7 @@ function titleFor(panel, name) {
16
17
  try { return browser && browser.url ? new URL(browser.url).hostname : 'Browser'; } catch (error) { return 'Browser'; }
17
18
  }
18
19
  export function renderToolPaneChrome(panel, name) {
19
- if (panel.dataset.fileInstance) return;
20
+ if (panel.dataset.fileInstance) { bindToolPaneMaterial(panel, panel.querySelector(':scope > .tool-pane-header')); return; }
20
21
  if (WORKBENCH_PANELS[name]) { renderWorkbenchChrome(panel, name); return; }
21
22
  var definition = TOOL_PANES[name];
22
23
  if (!definition) return;
@@ -50,6 +51,7 @@ export function renderToolPaneChrome(panel, name) {
50
51
  if (title.textContent !== label) { title.textContent = label; title.title = label; }
51
52
  }
52
53
  update();
54
+ bindToolPaneMaterial(panel, header);
53
55
  }
54
56
 
55
57
  store.subscribe(function (state, previous) {
@@ -61,6 +63,7 @@ store.subscribe(function (state, previous) {
61
63
  function renderWorkbenchChrome(panel, name) {
62
64
  panel.classList.add('workbench-pane-shell');
63
65
  var header = panel.querySelector(':scope > header');
66
+ bindToolPaneMaterial(panel, header);
64
67
  if (!header || header.dataset.toolPane === name) return;
65
68
  header.dataset.toolPane = name;
66
69
  header.classList.add('tool-pane-header', 'split-pane-header');
@@ -0,0 +1,23 @@
1
+ // Keep the content backing below the real header without moving content nodes
2
+ // or giving filters to containers that own viewport-fixed controls.
3
+ export function bindToolPaneMaterial(panel, header) {
4
+ if (!header || panel.classList.contains('tool-pane-material')) return;
5
+ function measure() {
6
+ if (!panel.isConnected || !header.getClientRects().length) return;
7
+ // Layout coordinates stay stable during the existing pane scale animation.
8
+ var bottom = header.offsetTop + header.offsetHeight;
9
+ var value = Math.max(0, bottom) + 'px';
10
+ if (panel.style.getPropertyValue('--tool-pane-body-top') !== value) panel.style.setProperty('--tool-pane-body-top', value);
11
+ }
12
+ measure();
13
+ panel.classList.add('tool-pane-material');
14
+ var observer = typeof ResizeObserver === 'function' ? new ResizeObserver(measure) : null;
15
+ if (observer) { observer.observe(header); observer.observe(panel); }
16
+ else window.addEventListener('resize', measure);
17
+ panel.addEventListener('clay-tool-pane-dispose', function () {
18
+ if (observer) observer.disconnect();
19
+ else window.removeEventListener('resize', measure);
20
+ panel.classList.remove('tool-pane-material');
21
+ panel.style.removeProperty('--tool-pane-body-top');
22
+ }, { once: true });
23
+ }
@@ -18,8 +18,8 @@ function initTooltips() {
18
18
  document.body.appendChild(tooltipEl);
19
19
 
20
20
  // Convert existing title attributes to data-tip in target areas
21
- convertTitles();
22
21
  initPaneHeaderTooltips();
22
+ convertTitles();
23
23
 
24
24
  // Delegate hover events on document for [data-tip]
25
25
  document.addEventListener("mouseover", function (e) {
@@ -96,7 +96,7 @@ function cancelShow() {
96
96
  function showTooltipAt(target) {
97
97
  if (!tooltipEl || !target.isConnected || !target.getClientRects().length) return;
98
98
  // A message preview already describes this Worker/task tab.
99
- if (target.getAttribute('aria-describedby') === 'agent-message-bubble' && document.getElementById('agent-message-bubble')) return;
99
+ if ((target.getAttribute('aria-describedby') || '').split(/\s+/).indexOf('agent-message-bubble') >= 0 && document.getElementById('agent-message-bubble')) return;
100
100
  // Suppress tooltip when an associated popover/picker is open
101
101
  var suppressSel = target.getAttribute("data-tip-suppress-when-open");
102
102
  if (suppressSel) {
@@ -171,7 +171,11 @@ function hideTooltip() {
171
171
  currentTarget = null;
172
172
  }
173
173
 
174
- function pruneTooltip() { if (currentTarget && (!currentTarget.isConnected || !currentTarget.getClientRects().length)) { cancelShow(); hideTooltip(); } }
174
+ function pruneTooltip() {
175
+ if (!currentTarget) return;
176
+ if (!currentTarget.isConnected || !currentTarget.getClientRects().length) { cancelShow(); hideTooltip(); }
177
+ else if (!currentTarget.hasAttribute('data-tip-html') && tooltipEl.textContent !== currentTarget.getAttribute('data-tip')) showTooltipAt(currentTarget);
178
+ }
175
179
 
176
180
  function dismissTooltip() { cancelShow(); hideTooltip(); }
177
181
 
@@ -1,3 +1,4 @@
1
+ import { store } from './store.js';
1
2
  // tui-attention.js
2
3
  //
3
4
  // Modal that mounts a transient xterm attached to a Claude TUI session's
@@ -64,7 +65,7 @@ function teardownModalXterm() {
64
65
 
65
66
  function modalSend(msg) {
66
67
  if (!modalWs || modalWs.readyState !== 1) return;
67
- try { modalWs.send(JSON.stringify(msg)); } catch (e) {}
68
+ try { if (store.get('tuiModalGithubAttachKey')) msg.githubAttachKey = store.get('tuiModalGithubAttachKey'); modalWs.send(JSON.stringify(msg)); } catch (e) {}
68
69
  }
69
70
 
70
71
  function fitModalXterm() {
@@ -143,6 +144,7 @@ export function openTuiModal(terminalId, sourceSlug, info) {
143
144
  modalTerminalId = terminalId;
144
145
  modalSourceSlug = sourceSlug;
145
146
  var infoObj = info || {};
147
+ store.set({tuiModalGithubAttachKey:infoObj.githubAttachKey || null,tuiModalProcessExited:false});
146
148
  modalCloseHook = typeof infoObj.onClose === "function" ? infoObj.onClose : null;
147
149
  setModalBreadcrumb({
148
150
  projectIcon: infoObj.projectIcon || null,
@@ -206,7 +208,8 @@ export function openTuiModal(terminalId, sourceSlug, info) {
206
208
  } else if (msg.type === "term_resized" && modalXterm && msg.cols > 0 && msg.rows > 0) {
207
209
  try { modalXterm.resize(msg.cols, msg.rows); } catch (err) {}
208
210
  } else if (msg.type === "term_exited" && modalXterm) {
209
- try { modalXterm.write("\r\n\x1b[90m[claude exited]\x1b[0m\r\n"); } catch (err) {}
211
+ store.set({tuiModalProcessExited:true});
212
+ try { modalXterm.write("\r\n\x1b[90m[process exited]\x1b[0m\r\n"); } catch (err) {}
210
213
  } else if (msg.type === "term_closed") {
211
214
  closeTuiModal();
212
215
  }
@@ -237,16 +240,18 @@ export function openTuiModal(terminalId, sourceSlug, info) {
237
240
 
238
241
  export function closeTuiModal() {
239
242
  if (!modalEl) return;
243
+ var processExited = store.get('tuiModalProcessExited');
240
244
  var closeHook = modalCloseHook;
241
245
  modalCloseHook = null;
242
246
  if (modalTerminalId != null && modalWs && modalWs.readyState === 1) {
243
- try { modalWs.send(JSON.stringify({ type: "term_detach", id: modalTerminalId })); } catch (e) {}
247
+ try { modalWs.send(JSON.stringify({ type: "term_detach", id: modalTerminalId, githubAttachKey: store.get('tuiModalGithubAttachKey') })); } catch (e) {}
244
248
  }
245
249
  if (modalWs) {
246
250
  try { modalWs.close(); } catch (e) {}
247
251
  modalWs = null;
248
252
  }
249
253
  modalTerminalId = null;
254
+ store.set({tuiModalGithubAttachKey:null,tuiModalProcessExited:false});
250
255
  modalSourceSlug = null;
251
256
  teardownModalXterm();
252
257
  modalEl.classList.add("hidden");
@@ -255,7 +260,7 @@ export function closeTuiModal() {
255
260
  modalKeyHandler = null;
256
261
  }
257
262
  if (closeHook) {
258
- try { closeHook(); } catch (e) {}
263
+ try { closeHook({ exited: processExited }); } catch (e) {}
259
264
  }
260
265
  }
261
266
 
@@ -1,3 +1,4 @@
1
+ import { selectIntegrationEditor } from './personal-integration-editor.js';
1
2
  import { refreshIcons } from './icons.js';
2
3
  import { resetAccountSessions } from './user-settings-sessions.js';
3
4
  import { confirmSettingsAction as showConfirmDialog } from './server-settings-state.js';
@@ -283,6 +284,7 @@ export function renderEmailSettings() {
283
284
  var acc = accounts[i];
284
285
  var row = document.createElement('div');
285
286
  row.className = 'us-integration-email-row';
287
+ row.dataset.emailAccountId = acc.id;
286
288
  row.innerHTML =
287
289
  '<div class="us-voice-row-controls"><div class="us-integration-brand">' + providerIcon(acc.provider) + '</div>' +
288
290
  '<div class="us-voice-provider-copy"><strong>' + escHtml(acc.email) + '</strong><small>Email · ' + escHtml(({gmail:'Gmail',outlook:'Outlook Mail',yahoo:'Yahoo Mail',custom:'Custom email'})[acc.provider] || 'Custom email') + (acc.label ? ' · ' + escHtml(acc.label) : '') + '</small></div>' +
@@ -292,7 +294,10 @@ export function renderEmailSettings() {
292
294
  manageBtn.setAttribute('aria-label', 'Manage ' + acc.email);
293
295
  manageBtn.addEventListener('click', function () {
294
296
  var panel = this.closest('.us-integration-email-row').querySelector('.us-email-manage');
295
- panel.hidden = !panel.hidden; this.setAttribute('aria-expanded', String(!panel.hidden));
297
+ var row = this.closest('.us-integration-email-row');
298
+ selectIntegrationEditor('email-manage:' + row.dataset.emailAccountId, '[data-email-account-id="' + CSS.escape(row.dataset.emailAccountId) + '"] .us-email-manage', function () {
299
+ panel.hidden = false; row.querySelector('[aria-expanded]').setAttribute('aria-expanded', 'true'); panel.querySelector('button').focus();
300
+ });
296
301
  });
297
302
  var removeBtn = row.querySelector('[data-account-id]');
298
303
  removeBtn.addEventListener('click', function () {
@@ -305,6 +310,10 @@ export function renderEmailSettings() {
305
310
  });
306
311
 
307
312
  listEl.appendChild(row);
313
+ var active = store.get('personalIntegrationEditor');
314
+ if (active && active.key === 'email-manage:' + acc.id) {
315
+ row.querySelector('.us-email-manage').hidden = false; manageBtn.setAttribute('aria-expanded','true');
316
+ }
308
317
  }
309
318
  refreshIcons();
310
319
  }
@@ -0,0 +1,176 @@
1
+ import { selectIntegrationEditor, clearIntegrationEditor } from './personal-integration-editor.js';
2
+ import { showToast } from './utils.js';
3
+ import { store } from './store.js';
4
+ import { getWs } from './ws-ref.js';
5
+ import { githubMark } from './github-mark.js';
6
+ import { confirmSettingsAction } from './server-settings-state.js';
7
+ import { openTuiModal, closeTuiModal, getTuiModalTerminalId } from './tui-attention.js';
8
+ function context() { return JSON.stringify([store.get('myUserId'),store.get('currentSlug'),store.get('projectSplitViewSocketGeneration')]); }
9
+ function send(action) {
10
+ var ws = getWs();
11
+ if (!ws || ws.readyState !== 1 || !store.get('githubSettingsContextId')) return false;
12
+ var requestId = crypto.randomUUID(), data = store.get('githubSettingsData') || {};
13
+ ws.send(JSON.stringify({type:'personal_github_'+action,requestId:requestId,contextId:store.get('githubSettingsContextId'),reviewId:data.reviewId}));
14
+ if (action !== 'release' && action !== 'stop') {
15
+ store.set({githubSettingsRequest:requestId,githubSettingsPending:true,githubSettingsAction:action});
16
+ if (action === 'get') store.set({githubSettingsData:Object.assign({},data,{state:'checking',observed:null,reviewId:null})});
17
+ setTimeout(function () {
18
+ if (store.get('githubSettingsRequest') !== requestId || !store.get('githubSettingsPending')) return;
19
+ store.set({githubSettingsPending:false,githubSettingsData:Object.assign({},store.get('githubSettingsData'),{state:'unknown',reviewId:null,message:'The response is unknown. Stop any active login, then refresh. No action is retried automatically.'})}); render();
20
+ },20000);
21
+ }
22
+ return true;
23
+ }
24
+ function text(tag,value,cls) { var el=document.createElement(tag); el.textContent=value; if(cls) el.className=cls; return el; }
25
+ function button(parent,label,action,disabled) {
26
+ var el=text('button',label,'settings-btn-sm');el.type='button';el.disabled=!!disabled;el.onclick=action;parent.appendChild(el);return el;
27
+ }
28
+ function open() { selectIntegrationEditor('github', '#us-github-editor', function () { loadGithubIntegration(); if (!store.get('githubSettingsPending')) send('get'); store.set({githubSettingsEditing:true});render();document.getElementById('us-github-editor').focus(); }); }
29
+ function closeTerminal() {
30
+ var id=store.get('githubSettingsTerminal');store.set({githubSettingsTerminal:null});
31
+ if(id && getTuiModalTerminalId() === id) closeTuiModal();
32
+ }
33
+ export function closeGithubEditor() { store.set({githubSettingsEditing:false});clearIntegrationEditor('github');render(); }
34
+ function close() { closeGithubEditor();var el=document.querySelector('#us-github-connections button') || document.getElementById('us-github-add');if(el)el.focus(); }
35
+ function statusLabel(data) {
36
+ var saved=data.saved && data.saved.connection, observed=data.observed;
37
+ if (!saved) return '';
38
+ if (!observed || data.state === 'checking') return 'Saved · Verification pending';
39
+ if (observed.status !== 'authenticated') return 'Saved · '+(observed.status === 'unauthenticated' ? 'Sign-in required' : 'Verification unknown');
40
+ var sameRuntime=['user','uid','gid','home'].every(function(key){return saved.runtime[key]===data.runtime[key];});
41
+ if(saved.account.id !== observed.account.id || saved.account.login !== observed.account.login || !sameRuntime)return 'Saved · Account or runtime changed';
42
+ return 'Verified '+new Date(observed.checkedAt).toLocaleString();
43
+ }
44
+ function render() {
45
+ var editor = document.getElementById('us-github-editor');
46
+ var focused = editor && editor.contains(document.activeElement);
47
+ renderEditor();
48
+ if (focused && editor && !editor.hidden && !editor.contains(document.activeElement)) editor.focus();
49
+ }
50
+ function renderEditor() {
51
+ var root=document.getElementById('us-github-connections'), add=document.getElementById('us-github-add'), editor=document.getElementById('us-github-editor');
52
+ if(!root || !add || !editor)return;
53
+ var data=store.get('githubSettingsData') || {}, saved=data.saved && data.saved.connection, observed=data.observed, pending=store.get('githubSettingsPending');
54
+ add.hidden=!!saved;
55
+ if(saved){
56
+ var row=root.firstElementChild;
57
+ if(!row){
58
+ row=text('div','','us-voice-row');row.dataset.connectionId='github';var controls=text('div','','us-voice-row-controls');
59
+ var brand=text('span','','us-integration-brand');brand.innerHTML=githubMark();var copy=text('div','','us-voice-provider-copy');
60
+ copy.append(text('strong',''),text('small',''));controls.append(brand,copy,text('span','','us-integration-state'));
61
+ var manage=button(controls,'Manage',open);manage.setAttribute('aria-controls','us-github-editor');
62
+ row.appendChild(controls);root.appendChild(row);
63
+ }
64
+ row.querySelector('strong').textContent=saved.account.login;
65
+ row.querySelector('small').textContent='GitHub · '+saved.account.host+' · ID '+saved.account.id;
66
+ row.querySelector('.us-integration-state').textContent=statusLabel(data);
67
+ row.querySelector('button').setAttribute('aria-expanded',String(!!store.get('githubSettingsEditing')));
68
+ if(store.get('githubSettingsEditing') && editor.parentElement!==row)row.appendChild(editor);
69
+ } else {
70
+ var setup=document.getElementById('us-integration-setup');if(editor.parentElement!==setup)setup.appendChild(editor);
71
+ root.replaceChildren();
72
+ }
73
+ editor.hidden=!store.get('githubSettingsEditing'); editor.replaceChildren();
74
+ if(editor.hidden)return;
75
+ editor.appendChild(text('h3',saved?'Manage GitHub':'Connect GitHub'));
76
+ if(data.runtime){
77
+ editor.appendChild(text('p',data.runtime.shared?'This server OS account shares GitHub CLI credentials across Clay Studio accounts and other processes.':'GitHub CLI runs as your mapped OS account.','settings-hint'));
78
+ var details=text('details'), summary=text('summary','Runtime account');details.append(summary,text('p','OS account '+data.runtime.user+' · UID '+data.runtime.uid+' / GID '+data.runtime.gid+' · HOME '+data.runtime.home,'settings-hint'));editor.appendChild(details);
79
+ }
80
+ if(observed){
81
+ var labels={missing:'GitHub CLI is not installed for this runtime. Ask the server operator to install gh, then refresh.',unknown:'Authentication could not be verified. Check the network and server setup, then refresh.',unauthenticated:'GitHub CLI is not authenticated for github.com.'};
82
+ editor.appendChild(text('p',observed.account?'Current CLI account: '+observed.account.login+' · ID '+observed.account.id+' · '+observed.account.host:labels[observed.status] || 'Verification pending.','settings-hint'));
83
+ if(observed.overridden)editor.appendChild(text('p','GH_TOKEN or GITHUB_TOKEN controls the effective CLI account. Browser sign-in cannot change this override. Ask the server operator to update the environment.','settings-hint'));
84
+ }
85
+ if(saved)editor.appendChild(text('p','Saved account: '+saved.account.login+' · ID '+saved.account.id+' · '+saved.account.host+'. Connected '+new Date(saved.connectedAt).toLocaleString()+'.','settings-hint'));
86
+ var help=text('a','Official GitHub CLI setup and authentication','settings-link');help.href='https://cli.github.com/manual/gh_auth_login';help.target='_blank';help.rel='noopener noreferrer';editor.appendChild(help);
87
+ if(data.state === 'started' || data.state === 'stopping' || store.get('githubSettingsTerminal')){
88
+ editor.appendChild(text('p',data.message || 'Waiting for GitHub approval…','settings-hint'));
89
+ button(editor,'Stop login',function(){send('stop');},data.state==='stopping');return;
90
+ }
91
+ var actions=text('div','','settings-form-actions');editor.appendChild(actions);
92
+ button(actions,'Refresh',function(){send('get');},pending);
93
+ if(observed && observed.account && data.reviewId){
94
+ button(actions,(saved?'Confirm account: ':'Connect ')+observed.account.login,function(){send('connect');},pending);
95
+ }
96
+ if(data.canLogin && observed && ['authenticated','unauthenticated'].indexOf(observed.status)>=0 && data.reviewId){
97
+ editor.appendChild(text('p','A new login may replace the GitHub CLI account for this runtime and change its Git authentication settings. GitHub CLI owns the credentials. After approval, verify and explicitly confirm the resulting account.','settings-hint'));
98
+ button(actions,saved?'Reconnect with browser':'Sign in with browser',function(){
99
+ confirmSettingsAction('Start gh auth login for github.com in OS account '+data.runtime.user+' (HOME '+data.runtime.home+')? This may replace credentials used by other applications. Approve only at https://github.com/login/device.','Start GitHub login',function(){send('login');});
100
+ },pending);
101
+ } else if(data.runtime && data.runtime.shared && !data.canLogin && !(observed && observed.overridden))editor.appendChild(text('p','Only a server administrator can start a new login in the shared OS account. You can connect its verified existing account.','settings-hint'));
102
+ if(saved && data.reviewId)button(actions,'Disconnect',function(){confirmSettingsAction('Remove only this Clay Studio association? GitHub CLI credentials remain available to other applications and existing CLI-backed GitHub tools.','Disconnect',function(){send('disconnect');});},pending);
103
+ button(actions,'Cancel',close,pending);
104
+ }
105
+ export function initGithubIntegration(root){
106
+ var list=text('div');list.id='us-github-connections';root.querySelector('#us-linear-connections').after(list);
107
+ var editor=text('section','','settings-form');editor.id='us-github-editor';editor.hidden=true;editor.tabIndex=-1;root.querySelector('#us-integration-setup').appendChild(editor);
108
+ var add=text('button','','us-integration-tile');add.type='button';add.id='us-github-add';add.innerHTML=githubMark()+'<span>GitHub</span>';add.onclick=open;root.querySelector('.us-integration-tiles').appendChild(add);
109
+ var status=text('p','','settings-status');status.id='us-github-status';status.setAttribute('role','status');status.setAttribute('aria-live','polite');list.after(status);
110
+ store.subscribe(function(state,previous){
111
+ if(state.myUserId!==previous.myUserId || state.currentSlug!==previous.currentSlug || state.projectSplitViewSocketGeneration!==previous.projectSplitViewSocketGeneration || state.connected===false && previous.connected!==false){leaveGithubIntegration();}
112
+ });
113
+ }
114
+ export function loadGithubIntegration(){
115
+ if(store.get('githubSettingsContextId'))return;
116
+ store.set({githubSettingsContextId:crypto.randomUUID(),githubSettingsContext:context(),githubSettingsData:null});send('get');
117
+ }
118
+ export function leaveGithubIntegration(){
119
+ if(!store.get('githubSettingsContextId'))return;
120
+ if(store.get('githubSettingsTerminal') || store.get('githubSettingsPending') && store.get('githubSettingsAction')==='login') {
121
+ store.set({githubSettingsClosing:{contextId:store.get('githubSettingsContextId'),requestId:store.get('githubSettingsRequest'),owner:store.get('myUserId')}});
122
+ showToast(store.get('connected')===false?'GitHub login closure is unconfirmed after disconnection. Refresh before another attempt.':'Stopping the GitHub login terminal. Waiting for exit acknowledgement.','info');
123
+ }
124
+ send('release');closeTerminal();
125
+ store.set({githubSettingsContextId:null,githubSettingsRequest:null,githubSettingsPending:false,githubSettingsEditing:false,githubSettingsData:null});
126
+ var root=document.querySelector('.us-section[data-section=us-integrations]');if(root)root.dataset.githubLoaded='false';
127
+ clearIntegrationEditor('github');render();
128
+ }
129
+ export function handleGithubIntegration(message){
130
+ var closing=store.get('githubSettingsClosing');
131
+ if(closing && closing.contextId===message.contextId && closing.requestId===message.requestId && closing.owner===store.get('myUserId')) {
132
+ if(message.state==='stopped' || message.state==='unknown') {
133
+ showToast(message.message,message.state==='unknown'?'error':'info');
134
+ if(message.state==='stopped')store.set({githubSettingsClosing:null});
135
+ }
136
+ }
137
+ if(!store.get('githubSettingsContextId') || store.get('githubSettingsContext')!==context() || message.contextId!==store.get('githubSettingsContextId') || message.requestId!==store.get('githubSettingsRequest'))return;
138
+ var previous=store.get('githubSettingsData') || {};
139
+ var data=Object.assign({},previous,message);
140
+ if(message.state==='error' || message.state==='unknown')data.observed=null;
141
+ store.set({githubSettingsData:data,githubSettingsPending:['checking','verifying','stopping'].indexOf(message.state)>=0});
142
+ var status=document.getElementById('us-github-status');if(status){status.textContent=message.message || (message.state==='checking'?'Checking GitHub…':'');status.dataset.state=['error','unknown'].indexOf(message.state)>=0?'error':'';}
143
+ if(message.state==='started'){
144
+ store.set({githubSettingsTerminal:message.terminalId});
145
+ openTuiModal(message.terminalId,message.slug,{sessionTitle:'GitHub sign in',compact:true,githubAttachKey:message.attachKey,onClose:function(result){if(!result.exited && store.get('githubSettingsTerminal')===message.terminalId)send('stop');}});
146
+ } else if(['ready','saved','stopped','unknown'].indexOf(message.state)>=0)closeTerminal();
147
+ var root=document.querySelector('.us-section[data-section=us-integrations]');if(root && message.saved)root.dataset.githubLoaded='true';
148
+ if(message.state==='saved'){store.set({githubSettingsEditing:false});}
149
+ render();
150
+ if(message.state==='saved')close();
151
+ }
152
+
153
+ // Retain the editor until the exact login reports terminal exit; unknown never navigates.
154
+ export function stopGithubEditorLogin() {
155
+ var data = store.get('githubSettingsData') || {};
156
+ var login = store.get('githubSettingsTerminal') || ['started','stopping'].indexOf(data.state) >= 0 ||
157
+ store.get('githubSettingsAction') === 'login' && (store.get('githubSettingsPending') || data.state === 'unknown');
158
+ if (!login) return Promise.resolve(true);
159
+ return new Promise(function (resolve) {
160
+ var contextId = store.get('githubSettingsContextId'), requestId = store.get('githubSettingsRequest');
161
+ var timer;
162
+ function finish(ok) { clearTimeout(timer); unsubscribe(); resolve(ok); }
163
+ var unsubscribe = store.subscribe(function (state) {
164
+ if (state.githubSettingsContextId !== contextId || state.githubSettingsRequest !== requestId) { finish(false); return; }
165
+ var current = state.githubSettingsData || {};
166
+ if (current.state === 'stopped') finish(true);
167
+ else if (current.state === 'unknown' || state.connected === false) finish(false);
168
+ });
169
+ timer = setTimeout(function () {
170
+ var message = 'GitHub login closure is unconfirmed. Keep this editor open and ask the server operator to inspect it. No login is retried automatically.';
171
+ store.set({githubSettingsPending:false,githubSettingsData:Object.assign({},store.get('githubSettingsData'),{state:'unknown',message:message,reviewId:null})});
172
+ showToast(message,'error'); render(); finish(false);
173
+ },20000);
174
+ if (!send('stop')) finish(false);
175
+ });
176
+ }