clay-server 5.5.0-beta.2 → 5.5.0-beta.3

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 (114) hide show
  1. package/lib/clay-product-control-input.js +2 -2
  2. package/lib/clay-product-control-tools.js +1 -1
  3. package/lib/clay-product-control.js +4 -3
  4. package/lib/daemon-project-access.js +10 -2
  5. package/lib/daemon-projects.js +16 -4
  6. package/lib/daemon-worktree-controls.js +171 -0
  7. package/lib/daemon.js +14 -63
  8. package/lib/personal-provider-install.js +85 -0
  9. package/lib/personal-provider-settings.js +94 -0
  10. package/lib/project-logs-service.js +2 -2
  11. package/lib/project-pair-task-control.js +1 -1
  12. package/lib/project-pending-message-queue.js +6 -2
  13. package/lib/project-product-control.js +1 -0
  14. package/lib/project-session-actions.js +46 -3
  15. package/lib/project-session-pair.js +2 -0
  16. package/lib/project-sessions.js +11 -3
  17. package/lib/project-user-message.js +5 -1
  18. package/lib/project-vendor-login.js +95 -32
  19. package/lib/project-worker-auth.js +71 -0
  20. package/lib/project-worker-conversation.js +11 -2
  21. package/lib/project.js +9 -0
  22. package/lib/public/app.js +22 -0
  23. package/lib/public/css/icon-strip.css +5 -0
  24. package/lib/public/css/overlays.css +18 -0
  25. package/lib/public/css/settings-shell.css +14 -0
  26. package/lib/public/css/settings-ui.css +6 -0
  27. package/lib/public/css/split-view-layout.css +16 -11
  28. package/lib/public/modules/agent-message-bubbles.js +33 -4
  29. package/lib/public/modules/app-messages.js +7 -1
  30. package/lib/public/modules/app-projects.js +3 -4
  31. package/lib/public/modules/clay-control-copy.js +1 -0
  32. package/lib/public/modules/clay-tool-presentation.js +1 -1
  33. package/lib/public/modules/generation-stop.js +4 -4
  34. package/lib/public/modules/pane-activation.js +2 -2
  35. package/lib/public/modules/pane-bridge.js +4 -2
  36. package/lib/public/modules/personal-provider-context.js +11 -0
  37. package/lib/public/modules/project-settings.js +1 -1
  38. package/lib/public/modules/settings-section-nav.js +58 -0
  39. package/lib/public/modules/settings-shell-registry.js +4 -2
  40. package/lib/public/modules/settings-shell.js +17 -6
  41. package/lib/public/modules/sidebar-projects.js +23 -9
  42. package/lib/public/modules/split-pane-force-stop.js +47 -0
  43. package/lib/public/modules/split-pane-header-state.js +11 -1
  44. package/lib/public/modules/split-pane-renderer.js +2 -2
  45. package/lib/public/modules/split-view-workers.js +32 -17
  46. package/lib/public/modules/stt-settings.js +21 -4
  47. package/lib/public/modules/user-settings-layout.js +11 -8
  48. package/lib/public/modules/user-settings-provider-install.js +42 -0
  49. package/lib/public/modules/user-settings-providers.js +102 -0
  50. package/lib/public/modules/user-settings-voice.js +50 -41
  51. package/lib/public/modules/vendor-login-browser.js +1 -1
  52. package/lib/public/modules/vendor-login.js +72 -12
  53. package/lib/public/modules/worker-activity-indicator.js +49 -0
  54. package/lib/public/modules/worker-message-previews.js +47 -11
  55. package/lib/public/modules/worker-stop-controls.js +36 -0
  56. package/lib/public/modules/worktree-controls.js +86 -0
  57. package/lib/public/pane-assets/{app-SEMVZ4AY.js → app-ANJS2LSK.js} +1 -1
  58. package/lib/public/pane-assets/{chunk-4YE4RU27.js → chunk-32YIYF7R.js} +1 -1
  59. package/lib/public/pane-assets/{chunk-BNNPJJVG.js → chunk-3EJ5IAHZ.js} +1 -1
  60. package/lib/public/pane-assets/{chunk-C6P7PICY.js → chunk-3TIVBAKM.js} +1 -1
  61. package/lib/public/pane-assets/{chunk-XYZGVYWW.js → chunk-4QFNLHW5.js} +1 -1
  62. package/lib/public/pane-assets/{chunk-FK3YLGAP.js → chunk-5XMZMIFG.js} +1 -1
  63. package/lib/public/pane-assets/chunk-6S2C7Q2N.js +1 -0
  64. package/lib/public/pane-assets/chunk-7B2GJI57.js +1 -0
  65. package/lib/public/pane-assets/{chunk-QBCM7QEA.js → chunk-BOFNCPTN.js} +1 -1
  66. package/lib/public/pane-assets/{chunk-CQNPEH2M.js → chunk-CXWAJEN5.js} +1 -1
  67. package/lib/public/pane-assets/{chunk-5UBN6GR7.js → chunk-EWTXBZJO.js} +1 -1
  68. package/lib/public/pane-assets/{chunk-YR5GH3GI.js → chunk-G35ZOJGP.js} +1 -1
  69. package/lib/public/pane-assets/chunk-HYT24UBT.js +1 -0
  70. package/lib/public/pane-assets/chunk-ISQEPZMY.js +1 -0
  71. package/lib/public/pane-assets/{chunk-E4VM2IDB.js → chunk-JM46WGDH.js} +1 -1
  72. package/lib/public/pane-assets/chunk-LBJG4MK2.js +174 -0
  73. package/lib/public/pane-assets/{chunk-K4PAN77B.js → chunk-P3AK4ORJ.js} +1 -1
  74. package/lib/public/pane-assets/chunk-QKPGE7DN.js +1 -0
  75. package/lib/public/pane-assets/chunk-TS5JUEVH.js +1 -0
  76. package/lib/public/pane-assets/{chunk-QWQB2YRF.js → chunk-UGH5MCNK.js} +1 -1
  77. package/lib/public/pane-assets/{chunk-YVVT3CTV.js → chunk-V5DLPSLT.js} +1 -1
  78. package/lib/public/pane-assets/chunk-VFKDU64W.js +1 -0
  79. package/lib/public/pane-assets/{chunk-U7EHYRJZ.js → chunk-XSSTR6GC.js} +1 -1
  80. package/lib/public/pane-assets/{chunk-M5CWIIIT.js → chunk-XVESKMU3.js} +1 -1
  81. package/lib/public/pane-assets/manifest.json +394 -386
  82. package/lib/public/pane-assets/style-VBVCWV44.css +1 -0
  83. package/lib/sdk-bridge.js +40 -6
  84. package/lib/sdk-force-stop.js +62 -0
  85. package/lib/sdk-message-processor.js +2 -1
  86. package/lib/server-worktree-controls.js +34 -0
  87. package/lib/server.js +11 -1
  88. package/lib/session-force-stop-support.js +15 -0
  89. package/lib/sessions.js +6 -1
  90. package/lib/terminal-manager.js +1 -1
  91. package/lib/terminal.js +13 -1
  92. package/lib/vendor-login-browser-flow.js +1 -1
  93. package/lib/worker-activity.js +26 -0
  94. package/lib/worker-message-previews.js +12 -7
  95. package/lib/worktree-branch.js +57 -0
  96. package/lib/worktree-identity.js +39 -0
  97. package/lib/worktree-preferences.js +25 -0
  98. package/lib/worktree-ref-transaction.js +36 -0
  99. package/lib/yoke/adapters/antigravity.js +3 -0
  100. package/lib/yoke/force-termination.js +26 -0
  101. package/lib/yoke/interface.js +14 -0
  102. package/lib/yoke/process-force-termination.js +24 -0
  103. package/lib/yoke/provider-install-recipes.js +20 -0
  104. package/lib/yoke/vendor-registry.js +20 -1
  105. package/package.json +1 -1
  106. package/lib/public/pane-assets/chunk-6AGSOAOD.js +0 -1
  107. package/lib/public/pane-assets/chunk-AATAIAXL.js +0 -1
  108. package/lib/public/pane-assets/chunk-D3CE5FZL.js +0 -1
  109. package/lib/public/pane-assets/chunk-GKSLBGVE.js +0 -168
  110. package/lib/public/pane-assets/chunk-J5PTSKJ6.js +0 -1
  111. package/lib/public/pane-assets/chunk-QPBO6ID4.js +0 -1
  112. package/lib/public/pane-assets/chunk-RA6VUJTY.js +0 -1
  113. package/lib/public/pane-assets/chunk-XOZUM5ET.js +0 -1
  114. package/lib/public/pane-assets/style-DZX6JAMF.css +0 -1
@@ -1,3 +1,4 @@
1
+ import { activateSettingsSections, deactivateSettingsSections } from './settings-section-nav.js';
1
2
  // Unified full-screen settings: one shell for Personal, Project and Server
2
3
  // settings. Scope modules register a controller; the shell owns routing,
3
4
  // access, project target binding, focus, close restoration and draft guards.
@@ -134,6 +135,7 @@ function applyRoute(route, focus) {
134
135
  var resolved = resolveRoute(route, shell.access);
135
136
  var ctrl = controllers[resolved.scope];
136
137
  if (!ctrl) return;
138
+ deactivateSettingsSections();
137
139
  if (shell.scope && shell.scope !== resolved.scope && controllers[shell.scope].leave) controllers[shell.scope].leave();
138
140
  var entering = shell.scope !== resolved.scope;
139
141
  shell.scope = resolved.scope;
@@ -152,7 +154,8 @@ function applyRoute(route, focus) {
152
154
  markActivePage(page);
153
155
  store.set({ settingsLastRoute: { scope: resolved.scope, page: page, slug: shell.slug } });
154
156
  enhanceSettingsSelects(root); refreshIcons(root);
155
- if (focus) focusHeading(ctrl);
157
+ if (focus && !resolved.anchor) focusHeading(ctrl);
158
+ activateSettingsSections(page, resolved.anchor);
156
159
  }
157
160
 
158
161
  function onScopeSelect(scope) {
@@ -160,12 +163,18 @@ function onScopeSelect(scope) {
160
163
  guard('scope', function () { applyRoute({ scope: scope, page: shell.pages[scope], slug: scope === 'project' ? currentRoute().slug : null }, true); });
161
164
  }
162
165
  function onPageSelect(page) {
163
- if (page === shell.pages[shell.scope]) { focusHeading(controller()); return; }
166
+ var route = resolveRoute({scope:shell.scope,page:page}, shell.access);
167
+ if (route.page === shell.pages[shell.scope]) {
168
+ if (!route.anchor) focusHeading(controller());
169
+ activateSettingsSections(route.page, route.anchor); return;
170
+ }
164
171
  guard('page', function () {
165
172
  var ctrl = controller();
166
- shell.pages[shell.scope] = page; ctrl.activate(page, { access: shell.access, scope: shell.scope });
167
- markActivePage(page); store.set({ settingsLastRoute: { scope: shell.scope, page: page, slug: shell.slug } });
168
- focusHeading(ctrl);
173
+ deactivateSettingsSections();
174
+ shell.pages[shell.scope] = route.page; ctrl.activate(route.page, {access:shell.access,scope:shell.scope});
175
+ markActivePage(route.page); store.set({settingsLastRoute:{scope:shell.scope,page:route.page,slug:shell.slug}});
176
+ if (!route.anchor) focusHeading(ctrl);
177
+ activateSettingsSections(route.page, route.anchor);
169
178
  }, shell.pages[shell.scope]);
170
179
  }
171
180
  // A project switch retargets only the settings view, never the workspace.
@@ -181,7 +190,8 @@ export function navigateSettings(route) {
181
190
  var resolved = resolveRoute(route, shell.access);
182
191
  if (resolved.scope === shell.scope && resolved.page === shell.pages[shell.scope] && (resolved.scope !== 'project' || !route.slug || route.slug === shell.slug)) {
183
192
  setShellStatus(redirectNote(resolved), resolved.redirected ? 'error' : undefined);
184
- focusHeading(controller()); return;
193
+ if (!resolved.anchor) focusHeading(controller());
194
+ activateSettingsSections(resolved.page, resolved.anchor); return;
185
195
  }
186
196
  guard(resolved.scope === shell.scope ? (route.slug && route.slug !== shell.slug ? 'project' : 'page') : 'scope', function () { applyRoute(route, true); }, shell.pages[shell.scope]);
187
197
  }
@@ -251,6 +261,7 @@ export function closeSettings(opts) {
251
261
  function finish() {
252
262
  shell.token += 1; binding.invalidate();
253
263
  var ctrl = controller();
264
+ deactivateSettingsSections();
254
265
  if (ctrl && ctrl.leave) ctrl.leave();
255
266
  Object.keys(controllers).forEach(function (id) { if (controllers[id].close) controllers[id].close(); });
256
267
  root.classList.add('hidden');
@@ -1,3 +1,4 @@
1
+ import { reviewWorktreeRemoval } from './worktree-controls.js';
1
2
  // sidebar-projects.js - Project icon strip, context menus, emoji picker, drag-and-drop, worktree modal
2
3
  // Extracted from sidebar.js (PR-36)
3
4
 
@@ -433,8 +434,9 @@ export function showProjectCtxMenu(anchorEl, slug, name, icon, position, options
433
434
  }
434
435
 
435
436
  var isWorktree = slug.indexOf("--") !== -1;
436
- var canRemoveTarget = !isWorktree || options.canRemoveWorktree !== false;
437
- var canDeleteTarget = canRemoveTarget && (!store.get('permissions') || store.get('permissions').deleteProject !== false);
437
+ var ownsWorktree = !isWorktree || !store.get('isMultiUserMode') || !!(targetProject && targetProject.projectOwnerId === store.get('myUserId'));
438
+ if (!ownsWorktree) iconItem.disabled = true;
439
+ var canDeleteTarget = ownsWorktree && (!store.get('permissions') || store.get('permissions').deleteProject !== false);
438
440
  if (!isWorktree || canDeleteTarget) {
439
441
  var sep2 = document.createElement("div");
440
442
  sep2.className = "project-ctx-separator";
@@ -465,15 +467,26 @@ export function showProjectCtxMenu(anchorEl, slug, name, icon, position, options
465
467
  var deleteItem = document.createElement("button");
466
468
  deleteItem.className = "project-ctx-item project-ctx-delete";
467
469
  deleteItem.setAttribute("role", "menuitem");
468
- deleteItem.innerHTML = iconHtml("trash-2") + " <span>" + (isWorktree ? "Remove Worktree" : "Remove Project") + "</span>";
470
+ deleteItem.innerHTML = iconHtml("trash-2") + " <span>" + (isWorktree ? "Exclude worktree (keep files)" : "Remove Project") + "</span>";
469
471
  deleteItem.addEventListener("click", function (e) {
470
472
  e.stopPropagation();
471
473
  closeProjectCtxMenu();
472
- if (getWs() && store.get('connected')) {
474
+ if (isWorktree) reviewWorktreeRemoval(slug, "exclude", anchorEl);
475
+ else if (getWs() && store.get('connected')) {
473
476
  getWs().send(JSON.stringify({ type: "remove_project_check", slug: slug, name: name }));
474
477
  }
475
478
  });
476
479
  menu.appendChild(deleteItem);
480
+ if (isWorktree) {
481
+ var diskItem = document.createElement("button");
482
+ diskItem.className = "project-ctx-item project-ctx-delete";
483
+ diskItem.setAttribute("role", "menuitem");
484
+ diskItem.innerHTML = iconHtml("trash-2") + " <span>Delete worktree…</span>";
485
+ diskItem.addEventListener("click", function (event) {
486
+ event.stopPropagation(); closeProjectCtxMenu(); reviewWorktreeRemoval(slug, "delete", anchorEl);
487
+ });
488
+ menu.appendChild(diskItem);
489
+ }
477
490
  }
478
491
 
479
492
  document.body.appendChild(menu);
@@ -1103,7 +1116,7 @@ export function renderIconStrip(projects, currentSlug) {
1103
1116
  } else {
1104
1117
  var abbrev = document.createElement("span");
1105
1118
  abbrev.className = "wt-branch-abbrev";
1106
- abbrev.textContent = getProjectAbbrev(wt.name);
1119
+ abbrev.innerHTML = iconHtml("git-branch");
1107
1120
  wtEl.appendChild(abbrev);
1108
1121
  }
1109
1122
 
@@ -1124,13 +1137,13 @@ export function renderIconStrip(projects, currentSlug) {
1124
1137
  });
1125
1138
  })(wt.slug);
1126
1139
 
1127
- (function (slug, name, elem, external) {
1140
+ (function (slug, name, elem) {
1128
1141
  elem.addEventListener("contextmenu", function (e) {
1129
1142
  e.preventDefault();
1130
1143
  e.stopPropagation();
1131
- showProjectCtxMenu(elem, slug, name, null, null, { shareTargetSlug: true, canRemoveWorktree: !external });
1144
+ showProjectCtxMenu(elem, slug, name, null, null, { shareTargetSlug: true });
1132
1145
  });
1133
- })(wt.slug, wt.name, wtEl, isExternal);
1146
+ })(wt.slug, wt.name, wtEl);
1134
1147
 
1135
1148
  if (!isWtActive && (wt.pendingPermissions > 0 || getPendingTuiAttention(wt.slug) > 0)) {
1136
1149
  wtEl.classList.add("has-pending-perm");
@@ -1250,7 +1263,8 @@ function createMobileProjectItem(p, currentSlug, isWorktree) {
1250
1263
  abbrev.textContent = p.icon;
1251
1264
  parseEmojis(abbrev);
1252
1265
  } else {
1253
- abbrev.textContent = getProjectAbbrev(p.name);
1266
+ if (p.isWorktree) abbrev.innerHTML = iconHtml("git-branch");
1267
+ else abbrev.textContent = getProjectAbbrev(p.name);
1254
1268
  }
1255
1269
  el.appendChild(abbrev);
1256
1270
 
@@ -0,0 +1,47 @@
1
+ import { iconHtml } from './icons.js';
2
+ import { livePaneBinding, samePaneBinding } from './pane-binding.js';
3
+ import { syncPaneHeaderState } from './split-pane-header-state.js';
4
+ import { showConfirm, hideConfirm } from './app-misc.js';
5
+
6
+ export function createPaneForceStop(paneEl, frame) {
7
+ var button = document.createElement('button');
8
+ button.type = 'button'; button.className = 'split-pane-force-stop';
9
+ button.setAttribute('aria-label', 'Force stop session'); button.title = 'Force stop session and all its Workers';
10
+ button.innerHTML = iconHtml('power');
11
+ button.addEventListener('click', function (event) {
12
+ event.stopPropagation();
13
+ if (!syncPaneHeaderState(paneEl) || button.disabled || !frame.contentWindow || !frame.clayPane || !livePaneBinding(frame)) return;
14
+ var binding = frame.clayPane.identity().binding, sourceDocument = frame.contentDocument, settled = false;
15
+ var title = paneEl.querySelector('.split-pane-title');
16
+ var text = 'Force stop all work in ' + (title && title.textContent || 'this session') + ' and all of its Workers? Queued messages will be paused. The pane and conversation history will stay open. Commands or external requests already sent may still be running.';
17
+ function current() {
18
+ return paneEl.isConnected && !paneEl.hidden && !paneEl.inert && frame.contentDocument === sourceDocument &&
19
+ syncPaneHeaderState(paneEl) && !button.disabled && livePaneBinding(frame, { paneBinding: binding }) && samePaneBinding(binding, frame.clayPane.identity().binding);
20
+ }
21
+ var modal = document.getElementById('confirm-modal'), panel = modal && modal.querySelector('.confirm-dialog');
22
+ var cancel = document.getElementById('confirm-cancel'), ok = document.getElementById('confirm-ok');
23
+ function cleanup() {
24
+ settled = true;
25
+ window.removeEventListener('keydown', onKey, true);
26
+ if (panel) { panel.removeAttribute('role'); panel.removeAttribute('aria-modal'); panel.removeAttribute('aria-labelledby'); }
27
+ if (current()) button.focus();
28
+ }
29
+ function onKey(event) {
30
+ if (event.key === 'Escape') { event.preventDefault(); event.stopImmediatePropagation(); hideConfirm(); return; }
31
+ if (event.key !== 'Tab') return;
32
+ event.preventDefault(); event.stopImmediatePropagation();
33
+ var controls = [cancel, ok].filter(Boolean), index = controls.indexOf(document.activeElement);
34
+ controls[(index + (event.shiftKey ? controls.length - 1 : 1)) % controls.length].focus();
35
+ }
36
+ showConfirm(text, function () {
37
+ if (settled) return;
38
+ var valid = current(); cleanup();
39
+ if (!valid) return;
40
+ frame.contentWindow.postMessage({ type: 'clay-pane-force-stop', paneBinding: binding }, window.location.origin);
41
+ }, 'Force stop all work', true, cleanup);
42
+ if (panel) { panel.setAttribute('role', 'dialog'); panel.setAttribute('aria-modal', 'true'); panel.setAttribute('aria-labelledby', 'confirm-text'); }
43
+ window.addEventListener('keydown', onKey, true);
44
+ if (cancel) cancel.focus();
45
+ });
46
+ return button;
47
+ }
@@ -20,7 +20,7 @@ export function syncPaneHeaderState(paneEl) {
20
20
  header.setAttribute('aria-busy', ready ? 'false' : 'true');
21
21
  header.dataset.paneLoading = ready ? 'false' : 'true';
22
22
  header.inert = !ready;
23
- var actions = header.querySelectorAll('.split-pane-info, .split-pane-search, .split-pane-context, .split-pane-sidebar, .split-view-arrange-trigger');
23
+ var actions = header.querySelectorAll('.split-pane-info, .split-pane-force-stop, .split-pane-search, .split-pane-context, .split-pane-sidebar, .split-view-arrange-trigger');
24
24
  for (var i = 0; i < actions.length; i++) {
25
25
  var action = actions[i];
26
26
  if (!ready) {
@@ -31,6 +31,16 @@ export function syncPaneHeaderState(paneEl) {
31
31
  delete action.dataset.panePriorDisabled;
32
32
  }
33
33
  }
34
+ var force = header.querySelector('.split-pane-force-stop');
35
+ if (force) {
36
+ var session = getCachedSessions().find(function (item) { return item.id === Number(paneEl.dataset.sessionId); });
37
+ var support = session && session.forceTermination;
38
+ var reason = support && support.reason || 'Force termination is not yet supported for this vendor';
39
+ force.disabled = !ready || !support || support.supported !== true;
40
+ force.title = support && support.supported === true ? 'Force stop session and all its Workers' : reason;
41
+ force.setAttribute('aria-label', 'Force stop session' + (support && support.supported === true ? '' : ': ' + reason));
42
+ force.setAttribute('aria-disabled', force.disabled ? 'true' : 'false');
43
+ }
34
44
  var close = paneEl.querySelector('.split-pane-close');
35
45
  if (close) {
36
46
  var host = paneEl.parentNode;
@@ -1,3 +1,4 @@
1
+ import { createPaneForceStop } from './split-pane-force-stop.js';
1
2
  import { renderPaneRuntime } from './split-pane-runtime.js';
2
3
  import { store } from './store.js';
3
4
  import { getWs } from './ws-ref.js';
@@ -201,7 +202,6 @@ function createPaneSearchPopover(paneEl, frame, searchButton) {
201
202
  throw error;
202
203
  }
203
204
  }
204
-
205
205
  export function handlePaneSearchMessage(event, host) {
206
206
  var frames = host ? host.querySelectorAll(".split-pane-frame") : [];
207
207
  for (var i = 0; i < frames.length; i++) {
@@ -287,7 +287,6 @@ function isConfiguredWorker(groups, sessionId) {
287
287
  export function updatePaneFullAccessButton(button, session) {
288
288
  if (button) renderPermissionControl(button, permissionState(session));
289
289
  }
290
-
291
290
  export function createSplitPane(pane, closePane) {
292
291
  var paneEl = document.createElement("section"); paneEl.className = "split-pane";
293
292
  var header = document.createElement("header"); header.className = "split-pane-header"; header.title = pane.title;
@@ -345,6 +344,7 @@ export function createSplitPane(pane, closePane) {
345
344
  }); header.appendChild(sidebarButton);
346
345
  var search = document.createElement("button"); search.type = "button"; search.className = "split-pane-search"; search.setAttribute("aria-label", "Search this session"); search.title = "Search this session"; search.innerHTML = iconHtml("search");
347
346
  header.insertBefore(search, close);
347
+ header.insertBefore(createPaneForceStop(paneEl, frame), close);
348
348
  function refreshReadyHeader() {
349
349
  if (paneEl.classList.contains('split-pane-cached')) return;
350
350
  var id = Number(frame.dataset.requestedSessionId || frame.dataset.sessionId), current = sessionById(id);
@@ -1,3 +1,5 @@
1
+ import { appendWorkerStopControls, syncWorkerForceStop } from './worker-stop-controls.js';
2
+ import { createWorkerActivityIndicator, syncWorkerActivityIndicator, setWorkerActivityRuntime } from './worker-activity-indicator.js';
1
3
  import { syncWorkerMessagePreviews } from './worker-message-previews.js';
2
4
  import { appendWorkerRemoval, reconcileWorkerRemoval } from './split-view-worker-removal.js';
3
5
  import { workerCursorKey, workerCursorAdvances, shouldLoadWorkerPage, captureWorkerAnchor, restoreWorkerAnchor } from './split-view-worker-scroll.js';
@@ -52,14 +54,14 @@ function presentationSource(container, driver, connectionErrorBinding) {
52
54
  var inSingleton = !view && store.get('projectSplitViewSingletonOrigin') === driver.sessionOriginId;
53
55
  return inView || inSingleton ? { frame: frame, live: live, binding: binding, view: view, singleton: inSingleton } : null;
54
56
  }
55
- export function workerRemovalTarget(container, driverOrigin, workerOrigin, generation) {
57
+ export function workerRemovalTarget(container, driverOrigin, workerOrigin, generation, stopping) {
56
58
  var driver = exactDriver(driverOrigin), worker = exactWorker(driver, workerOrigin, generation);
57
59
  var source = presentationSource(container, driver);
58
60
  if (!worker || worker.hidden || !driver || driver.hidden || (driver.ownerId || null) !== (store.get('myUserId') || null) ||
59
61
  document.hidden || !source || !store.get('connected') || !container.isConnected || container.hidden || container.inert ||
60
- source.frame.hidden || source.frame.inert || store.get('permissions') && store.get('permissions').sessionDelete === false) return null;
62
+ source.frame.hidden || source.frame.inert || !stopping && store.get('permissions') && store.get('permissions').sessionDelete === false) return null;
61
63
  return { projectSlug: store.get('currentSlug'), ownerId: store.get('myUserId') || null, driverId: driver.id,
62
- driverOriginId: driverOrigin, workerId: worker.id, workerOriginId: workerOrigin, generation: generation,
64
+ driverOriginId: driverOrigin, workerId: worker.id, workerOriginId: workerOrigin, generation: generation, forceTermination: worker.forceTermination,
63
65
  bindingGeneration: source.binding.generation, bindingRequestId: source.binding.requestId,
64
66
  viewRevision: store.get('projectSplitViewRevision'), socketGeneration: store.get('projectSplitViewSocketGeneration') };
65
67
  }
@@ -204,6 +206,12 @@ function ensurePopover() {
204
206
  element.setAttribute("role", "dialog"); element.setAttribute("aria-label", "Read-only Worker conversation"); document.body.appendChild(element);
205
207
  var header = document.createElement("header"); header.className = "split-view-worker-popover-header";
206
208
  var title = document.createElement("strong"); title.className = "worker-popover-title"; header.appendChild(title);
209
+ appendWorkerStopControls(header, function () {
210
+ var state = store.get('splitViewWorkerConversation');
211
+ if (!state || state.status === 'error') return null;
212
+ var pane = document.querySelector('.split-pane[data-session-origin="' + CSS.escape(state.driverOriginId) + '"]');
213
+ return workerRemovalTarget(pane, state.driverOriginId, state.workerOriginId, state.generation, true);
214
+ });
207
215
  var closeButton = document.createElement("button"); closeButton.type = "button"; closeButton.setAttribute("aria-label", "Close Worker conversation"); closeButton.innerHTML = iconHtml("x");
208
216
  closeButton.addEventListener("click", function () { close(true); }); header.appendChild(closeButton); element.appendChild(header);
209
217
  var source = document.createElement("div"); source.className = "split-view-worker-popover-source"; element.appendChild(source);
@@ -227,6 +235,9 @@ function ensurePopover() {
227
235
  body.addEventListener('touchstart', scrollIntent, { passive: true });
228
236
  var footer = document.createElement("footer"); footer.className = "split-view-worker-popover-footer"; element.appendChild(footer);
229
237
  }
238
+ syncWorkerForceStop(element, worker);
239
+ var stopButtons = element.querySelectorAll('.worker-conversation-stop');
240
+ for (var i = 0; i < stopButtons.length; i++) stopButtons[i].disabled = stopButtons[i].dataset.workerForceStop === 'true' && stopButtons[i].disabled || !store.get('connected') || state.status === 'error';
230
241
  return element;
231
242
  }
232
243
  function renderContent() {
@@ -330,9 +341,9 @@ export function handleWorkerConversationChanged(message) {
330
341
  message.subscriptionId !== state.subscriptionId || message.parentSessionOriginId !== state.driverOriginId ||
331
342
  message.workerSessionOriginId !== state.workerOriginId || message.generation !== state.generation || !ensurePopover()) return false;
332
343
  document.querySelectorAll('.split-view-worker-chip').forEach(function (chip) {
333
- if (chip.dataset.workerOrigin === state.workerOriginId) {
334
- var dot = chip.querySelector('.split-view-worker-dot');
335
- if (dot) dot.dataset.state = message.isProcessing ? 'working' : 'ready';
344
+ if (chip.dataset.workerParent === state.driverOriginId && chip.dataset.workerOrigin === state.workerOriginId && Number(chip.dataset.workerGeneration) === state.generation) {
345
+ setWorkerActivityRuntime(chip.dataset.messagePreview, message.isProcessing === true);
346
+ syncWorkerActivityIndicator(chip, { isProcessing: message.isProcessing === true });
336
347
  }
337
348
  });
338
349
  refreshWorkerConversation();
@@ -341,11 +352,6 @@ export function handleWorkerConversationChanged(message) {
341
352
  export function handleWorkerSessionActivity(message) {
342
353
  var worker = sessions().find(function (item) { return item.id === message.id && item.sessionRole === 'worker'; });
343
354
  if (!worker || !store.get('connected')) return;
344
- document.querySelectorAll('.split-view-worker-chip').forEach(function (chip) {
345
- if (chip.dataset.workerOrigin !== worker.sessionOriginId) return;
346
- var dot = chip.querySelector('.split-view-worker-dot');
347
- if (dot) { dot.dataset.state = 'working'; dot.setAttribute('aria-label', 'Worker activity received'); }
348
- });
349
355
  var state = store.get('splitViewWorkerConversation');
350
356
  if (state && state.workerOriginId === worker.sessionOriginId && state.generation === worker.workerGeneration) refreshWorkerConversation();
351
357
  }
@@ -359,8 +365,12 @@ export function renderPaneWorkers(container, driver) {
359
365
  var source = presentationSource(container, driver);
360
366
  root.hidden = !source || data.items.length === 0;
361
367
  if (!source) { if (store.get('splitViewWorkerConversation') && store.get('splitViewWorkerConversation').driverOriginId === driver.sessionOriginId) ensurePopover(); return; }
362
- root.textContent = "";
363
- data.items.forEach(function (worker) {
368
+ var signature = JSON.stringify([store.get('myUserId'), store.get('currentSlug'), driver.sessionOriginId, driver.title, store.get('projectSplitViewSocketGeneration'), data.items.map(function (worker) {
369
+ return [worker.id, worker.sessionOriginId, worker.workerGeneration, worker.vendor, worker.model, worker.title, data.currentIds.has(worker.id)];
370
+ })]);
371
+ var rebuild = root.dataset.workerSignature !== signature;
372
+ if (rebuild) { root.textContent = ''; root.dataset.workerSignature = signature; }
373
+ if (rebuild) data.items.forEach(function (worker) {
364
374
  var pill = document.createElement("span"); pill.className = "split-view-worker-pill";
365
375
  var button = document.createElement("button"); button.type = "button"; button.className = "split-view-worker-chip";
366
376
  button.dataset.workerParent = driver.sessionOriginId; button.dataset.workerOrigin = worker.sessionOriginId; button.dataset.workerGeneration = String(worker.workerGeneration);
@@ -369,8 +379,7 @@ export function renderPaneWorkers(container, driver) {
369
379
  button.title = (VENDOR_NAMES[worker.vendor] || worker.vendor || "Unknown vendor") + " · " + modelLabel + " · Split Worker #" + worker.id +
370
380
  (current ? "" : " · Archived · Gen " + worker.workerGeneration) + " · " + (worker.title || "Worker");
371
381
  button.setAttribute("aria-label", button.title + " conversation");
372
- var dot = document.createElement("span"); dot.className = "split-view-worker-dot"; dot.dataset.state = !store.get('connected') ? 'disconnected' : worker.isProcessing ? 'working' : 'ready';
373
- dot.setAttribute('aria-label', !store.get('connected') ? 'Disconnected' : worker.isProcessing ? 'Worker is working' : 'Worker is idle');
382
+ var dot = createWorkerActivityIndicator();
374
383
  if (VENDOR_AVATARS[worker.vendor]) {
375
384
  var vendorIcon = document.createElement("img"); vendorIcon.className = "split-view-worker-vendor";
376
385
  vendorIcon.src = VENDOR_AVATARS[worker.vendor]; vendorIcon.alt = ""; vendorIcon.setAttribute("aria-hidden", "true"); button.appendChild(vendorIcon);
@@ -389,8 +398,15 @@ export function renderPaneWorkers(container, driver) {
389
398
  button.addEventListener("click", function () { if (presentationSource(container, exactDriver(driver.sessionOriginId))) open(button, driver, worker); }); pill.appendChild(button); root.appendChild(pill);
390
399
  appendWorkerRemoval(pill, button, container, driver, worker);
391
400
  });
401
+ root.querySelectorAll('.split-view-worker-chip').forEach(function (chip) {
402
+ var worker = data.items.find(function (item) { return item.sessionOriginId === chip.dataset.workerOrigin; });
403
+ syncWorkerActivityIndicator(chip, worker);
404
+ var conversation = store.get('splitViewWorkerConversation');
405
+ chip.setAttribute('aria-expanded', conversation && conversation.driverOriginId === driver.sessionOriginId && conversation.workerOriginId === chip.dataset.workerOrigin ? 'true' : 'false');
406
+ });
392
407
  introduceWorkerViewer(container, driver);
393
408
  syncWorkerMessagePreviews();
409
+ if (rebuild) refreshIcons();
394
410
  var conversation = store.get('splitViewWorkerConversation');
395
411
  if (conversation) ensurePopover();
396
412
  }
@@ -417,8 +433,7 @@ store.subscribe(function (state, previous) {
417
433
  if (state.paneBindingSessions !== previous.paneBindingSessions || state.connected !== previous.connected) {
418
434
  document.querySelectorAll('.split-view-worker-chip').forEach(function (chip) {
419
435
  var worker = sessions().find(function (item) { return item.sessionOriginId === chip.dataset.workerOrigin; });
420
- var dot = chip.querySelector('.split-view-worker-dot');
421
- if (dot) dot.dataset.state = !state.connected ? 'disconnected' : worker && worker.isProcessing ? 'working' : 'ready';
436
+ syncWorkerActivityIndicator(chip, worker);
422
437
  });
423
438
  if (state.connected && state.splitViewWorkerConversation) refreshWorkerConversation();
424
439
  }
@@ -1,3 +1,4 @@
1
+ import { getWs } from './ws-ref.js';
1
2
  import { store } from './store.js';
2
3
  import { stopSTT } from './stt-controller.js';
3
4
  export var speechLanguages = [
@@ -16,35 +17,51 @@ export function voiceProvider(id) {
16
17
  var settings = store.get('speechSettings');
17
18
  return settings && settings.providers.find(function (provider) { return provider.id === id; });
18
19
  }
20
+ function context() { return JSON.stringify([store.get('myUserId'), store.get('speechSettingsContextGeneration')]); }
21
+ function lostAccess() {
22
+ store.set({speechPopup:null,speechRetry:null,speechSettings:null,speechSettingsSaving:false,speechSettingsError:'Sign in again to manage audio transcription keys.',speechSettingsContextGeneration:(store.get('speechSettingsContextGeneration') || 0)+1,speechSettingsRequest:(store.get('speechSettingsRequest') || 0)+1});
23
+ }
19
24
  export async function loadSpeechSettings() {
20
25
  if (store.get('speechSettingsSaving')) return;
26
+ var actor = context(), socket = getWs();
21
27
  var request = (store.get('speechSettingsRequest') || 0) + 1;
22
28
  store.set({ speechSettingsRequest: request });
23
29
  try {
24
30
  var response = await fetch('/api/speech/settings');
31
+ if ((response.status === 401 || response.status === 403) && actor === context() && socket === getWs() && store.get('speechSettingsRequest') === request) { lostAccess(); return; }
25
32
  if (!response.ok) throw new Error('Voice settings unavailable. Please try again.');
26
33
  var settings = await response.json();
27
- if (store.get('speechSettingsRequest') !== request) return;
34
+ if (actor !== context() || socket !== getWs() || store.get('speechSettingsRequest') !== request) return;
28
35
  var previous = store.get('speechSettings');
29
36
  if ((previous && JSON.stringify(previous) !== JSON.stringify(settings)) ||
30
37
  (!previous && settings.selected !== 'browser')) stopSTT();
31
38
  if (!previous || JSON.stringify(previous) !== JSON.stringify(settings)) store.set({ speechSettings: settings, speechLang: settings.language || 'en-US', speechSettingsError: '' });
32
39
  else if (store.get('speechSettingsError')) store.set({ speechSettingsError: '' });
33
- } catch (error) { if (store.get('speechSettingsRequest') === request) store.set({ speechSettingsError: 'Voice settings unavailable. Please try again.' }); }
40
+ } catch (error) { if (actor === context() && socket === getWs() && store.get('speechSettingsRequest') === request) store.set({ speechSettingsError: 'Voice settings unavailable. Please try again.' }); }
34
41
  }
35
42
  export async function saveSpeechSettings(change) {
36
43
  if (store.get('speechSettingsSaving')) throw new Error('Voice settings are being saved. Please wait.');
44
+ var actor = context(), socket = getWs(), request = (store.get('speechSettingsRequest') || 0) + 1;
37
45
  stopSTT();
38
- store.set({ speechRetry: null, speechSettingsSaving: true, speechSettingsRequest: (store.get('speechSettingsRequest') || 0) + 1 });
46
+ store.set({ speechRetry: null, speechSettingsSaving: true, speechSettingsRequest: request });
39
47
  try {
40
48
  var response = await fetch('/api/speech/settings', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(change) });
41
49
  var result = await response.json();
50
+ if (actor !== context() || socket !== getWs() || store.get('speechSettingsRequest') !== request) throw new Error('Account or connection changed. Reopen API connections.');
51
+ if (response.status === 401 || response.status === 403) { lostAccess(); throw new Error('Sign in again to manage audio transcription keys.'); }
42
52
  if (!response.ok) throw new Error(result.error || 'Could not save voice settings.');
43
53
  store.set({ speechSettings: result, speechLang: result.language || 'en-US', speechSettingsError: '' });
44
- } finally { store.set({ speechSettingsSaving: false }); }
54
+ } finally { if (actor === context() && store.get('speechSettingsRequest') === request) store.set({ speechSettingsSaving: false }); }
45
55
  try { window.top.document.dispatchEvent(new window.top.Event('clay:speech-settings-saved')); } catch (error) { /* Standalone host. */ }
46
56
  }
47
57
  export async function saveSpeechLanguage(code) {
48
58
  await saveSpeechSettings({ language: code });
49
59
  window.dispatchEvent(new CustomEvent('clay:speech-language-saved', { detail: code }));
50
60
  }
61
+
62
+ store.subscribe(function (state, previous) {
63
+ if (state.myUserId !== previous.myUserId || state.projectSplitViewSocketGeneration !== previous.projectSplitViewSocketGeneration || state.currentSlug !== previous.currentSlug || state.connected === false && previous.connected !== false) {
64
+ stopSTT();
65
+ store.set({speechPopup:null,speechRetry:null,speechSettings:null,speechSettingsSaving:false,speechSettingsError:state.connected === false ? 'Reconnect before loading audio transcription providers.' : 'Account or connection changed. Reload providers to continue.',speechSettingsContextGeneration:(state.speechSettingsContextGeneration || 0)+1,speechSettingsRequest:(state.speechSettingsRequest || 0)+1});
66
+ }
67
+ });
@@ -1,3 +1,4 @@
1
+ import { initPersonalProviders, loadPersonalProviders, leavePersonalProviders } from './user-settings-providers.js';
1
2
  import { initPersonalIntegrations, loadPersonalIntegrations } from './user-settings-integrations.js';
2
3
  import { getWs } from './ws-ref.js';
3
4
  import { store } from './store.js';
@@ -20,13 +21,14 @@ export function buildUserSettingsLayout() {
20
21
  var push=document.getElementById('settings-notif-push').closest('label');
21
22
  heading(notifications,'This device','Browser permission and push delivery are configured separately on each browser.');notifications.appendChild(push);
22
23
  var accountNotifications=document.createElement('h3');accountNotifications.textContent='Account preferences';notifications.querySelector('.settings-card').before(accountNotifications);
23
- var chat = section('us-chat'); chat.querySelector('h2').textContent = 'Conversation';
24
- var voice = document.getElementById('us-lang-select').closest('.settings-card');
25
- voice.innerHTML = '<div id="us-voice-settings"></div>';
24
+ var chat = section('us-chat');
25
+ var providers = document.createElement('div'); providers.className = 'us-section'; providers.dataset.section = 'us-providers';
26
+ providers.innerHTML = '<h2>AI providers</h2><p class="settings-hint">Set up coding agents on the server and manage your personal audio transcription API keys.</p><div class="settings-section-heading"><h3 id="coding-agents" data-settings-section>Coding agents</h3><span id="us-provider-refresh"></span></div><div id="us-coding-agents"></div><h3 id="api-connections" data-settings-section>API connections</h3><div id="us-voice-settings"></div>';
27
+ root.querySelector('.us-modal-content').appendChild(providers);
26
28
  var ai = document.createElement('div'); ai.className = 'us-section'; ai.dataset.section = 'us-ai';
27
29
  ai.innerHTML = '<h2>AI preferences</h2><p class="settings-hint">Defaults for new Clay interviews and built-in AI assistance. Project defaults, individual Mates and existing conversation runtimes stay separate.</p><div id="us-default-ai"></div>';
28
30
  root.querySelector('.us-modal-content').appendChild(ai);
29
- heading(ai, 'Execution behavior');
31
+ heading(ai, 'Execution behavior'); ai.lastElementChild.id = 'execution-behavior';
30
32
  ['us-auto-continue'].forEach(function (id) {ai.appendChild(document.getElementById(id).closest('.settings-card'));});
31
33
  document.getElementById('us-auto-continue').closest('label').querySelector('.settings-hint').textContent = 'When a rate-limited conversation becomes eligible to retry, Clay can send a continuation automatically. This may resume work and consume usage without another message from you.';
32
34
  heading(ai, 'Command permissions', 'These are personal Claude CLI allow rules, separate from model selection and Clay session approval modes. Managed defaults are shown for reference. Server access restrictions still apply. On an OS-user server these install in your OS home; otherwise they install in the shared server process home and can affect other Claude CLI launches there.');
@@ -36,7 +38,8 @@ export function buildUserSettingsLayout() {
36
38
  root.querySelector('.us-modal-content').appendChild(agent);
37
39
  section('us-mates').remove();
38
40
  var experiments = document.createElement('details'); experiments.className = 'us-experiments'; experiments.innerHTML = '<summary>Experimental features</summary><p class="settings-hint">Personal opt-ins for features that may change.</p>';
39
- experiments.appendChild(document.getElementById('us-capsules-enabled').closest('.settings-card'));chat.appendChild(experiments);section('us-experimental').remove();
41
+ experiments.appendChild(document.getElementById('us-capsules-enabled').closest('.settings-card'));ai.appendChild(experiments);experiments.id = 'experimental-features';section('us-experimental').remove();chat.remove();
42
+ initPersonalProviders();
40
43
  section('us-email').remove();
41
44
  initPersonalIntegrations();
42
45
  var account = section('us-account');heading(account, 'Sign out');account.appendChild(document.getElementById('us-logout-btn').closest('.settings-card'));section('us-logout').remove();
@@ -47,12 +50,12 @@ export function buildUserSettingsLayout() {
47
50
  }
48
51
  export function loadUserSettingsExtras(page) {
49
52
  if (page === 'us-integrations') loadPersonalIntegrations();
50
- if (page === 'us-chat') loadSpeechSettings();
53
+ if (page === 'us-providers') { loadSpeechSettings(); loadPersonalProviders(); } else leavePersonalProviders();
51
54
  if (page === 'us-agent') mountAgentBehavior(document.getElementById('us-agent-behavior'));
52
55
  if (page === 'us-ai') { mountDefaultAi(document.getElementById('us-default-ai')); requestDefaultAi(); var ws=getWs();if(ws && ws.readyState===1)ws.send(JSON.stringify({type:'get_claude_allow_list'})); }
53
56
  }
54
57
  export function leaveAgentBehaviorPage() {unmountAgentBehavior();resetAgentBehavior();}
55
- export function finishUserSettingsExtras() {leaveAgentBehaviorPage();closeSpeechPicker();unmountDefaultAi(document.getElementById('us-default-ai'));}
58
+ export function finishUserSettingsExtras() {leavePersonalProviders();leaveAgentBehaviorPage();closeSpeechPicker();unmountDefaultAi(document.getElementById('us-default-ai'));}
56
59
  // Returns the draft prompt for leaving one personal page (or every page when
57
60
  // pageId is omitted); the shell shows it with the shared confirmation.
58
61
  export function personalLeavePrompt(pageId) {
@@ -64,7 +67,7 @@ export function personalLeavePrompt(pageId) {
64
67
  var key = document.getElementById('us-linear-key');
65
68
  var keyDraft = key && key.value && store.get('personalLinearEditing') && !(key.form && key.form.hidden);
66
69
  var dirty = leaving('us-integrations') && (store.get('personalEmailDirty') || store.get('personalLinearDirty') || keyDraft) ||
67
- leaving('us-chat') && store.get('userVoiceDirty') ||
70
+ leaving('us-providers') && store.get('userVoiceDirty') ||
68
71
  leaving('us-notifications') && store.get('notificationDraft') ||
69
72
  leaving('us-appearance') && (store.get('appearanceDraft') || store.get('userTerminalDirty')) ||
70
73
  leaving('us-account') && (store.get('userProfileDirty') || pinDirty) ||
@@ -0,0 +1,42 @@
1
+ import { store } from './store.js';
2
+ import { getWs } from './ws-ref.js';
3
+ import { personalProviderContext } from './personal-provider-context.js';
4
+ function send(type,provider,reviewId,render) {
5
+ var ws = getWs();
6
+ if (!ws || ws.readyState !== 1 || !store.get('personalProvidersOpen') || store.get('personalProviderContext') !== personalProviderContext()) return false;
7
+ var requestId = type === 'stop' || type === 'cancel' ? store.get('personalProviderInstallRequest') : crypto.randomUUID();
8
+ store.set({personalProviderInstallRequest:requestId});
9
+ ws.send(JSON.stringify({requestId:requestId,type:'personal_provider_install_'+type,vendor:provider.id,personalRequestId:store.get('personalProviderRevision'),reviewId:reviewId}));
10
+ if (type === 'review' || type === 'start') setTimeout(function () { if (store.get('personalProviderInstallRequest') === requestId && store.get('personalProviderInstallAwaiting') === requestId) { store.set({personalProviderInstallBusy:false,personalProviderInstall:{vendor:provider.id,state:'unknown',message:'Installation response timed out. Reopen settings and refresh status; never assume an installation completed.'}}); if (render) render(); } },10000);
11
+ store.set({personalProviderInstallAwaiting:requestId}); return true;
12
+ }
13
+ export function resetProviderInstall() { store.set({personalProviderInstall:null,personalProviderInstallBusy:false,personalProviderInstallRequest:null,personalProviderInstallAwaiting:null}); }
14
+ export function handleProviderInstall(message,render) {
15
+ if (message.type !== 'personal_provider_install') return false;
16
+ if (!store.get('personalProvidersOpen') || !store.get('personalProviderRevision') || message.requestId !== store.get('personalProviderInstallRequest') || message.personalRequestId !== store.get('personalProviderRevision') || store.get('personalProviderContext') !== personalProviderContext()) return true;
17
+ store.set({personalProviderInstallAwaiting:null,personalProviderInstall:message,personalProviderInstallBusy:['review','started','stopping','unknown'].indexOf(message.state)>=0}); render();
18
+ var editor=document.getElementById('provider-setup-'+message.vendor); var status=editor && editor.querySelector('[data-install-status]'); if (status) status.focus({preventScroll:true}); return true;
19
+ }
20
+ export function renderProviderInstall(editor,provider,render) {
21
+ if (!provider.canInstall) return;
22
+ var item = store.get('personalProviderInstall');
23
+ var actions = document.createElement('div'); actions.className = 'settings-form-actions';
24
+ if (!item || item.vendor !== provider.id || ['review','started','stopping','unknown'].indexOf(item.state)<0) {
25
+ var review = document.createElement('button'); review.type='button'; review.className='settings-btn-sm'; review.textContent='Review installation'; review.disabled=!!store.get('personalProviderInstallBusy') || !!store.get('personalProviderSigningIn');
26
+ review.onclick=function () { if (send('review',provider,null,render)) { store.set({personalProviderInstallBusy:true}); review.disabled=true; } }; actions.appendChild(review);
27
+ }
28
+ if (item && item.vendor === provider.id) {
29
+ var status=document.createElement('p'); status.className='settings-status'; status.role='status'; status.tabIndex=-1; status.dataset.installStatus=''; status.textContent=item.message; editor.appendChild(status);
30
+ if (item.state==='review') {
31
+ var details=document.createElement('p'); details.className='settings-hint'; details.textContent='Runtime account: '+item.runtime.user+' (UID '+item.runtime.uid+'), HOME '+item.runtime.home+'. '+item.effects; editor.appendChild(details);
32
+ var command=document.createElement('pre'); command.className='settings-hint'; command.style.whiteSpace='pre-wrap'; command.style.overflowWrap='anywhere'; command.textContent=item.command; editor.appendChild(command);
33
+ var source=document.createElement('a'); source.className='settings-link'; source.href=item.source; source.target='_blank'; source.rel='noopener noreferrer'; source.textContent='Official setup instructions'; editor.appendChild(source);
34
+ var confirm=document.createElement('button'); confirm.type='button'; confirm.className='settings-btn-sm'; confirm.textContent='Install for this account'; confirm.onclick=function () { if (send('start',provider,item.reviewId,render)) { confirm.disabled=true; } }; actions.appendChild(confirm);
35
+ var cancel=document.createElement('button'); cancel.type='button'; cancel.className='settings-btn-sm'; cancel.textContent='Cancel installation'; cancel.onclick=function () { send('cancel',provider,item.reviewId); resetProviderInstall(); render(); }; actions.appendChild(cancel);
36
+ }
37
+ if (item.state==='started') {
38
+ var stop=document.createElement('button'); stop.type='button'; stop.className='settings-btn-sm'; stop.textContent='Stop installation'; stop.onclick=function () { send('stop',provider); stop.disabled=true; }; actions.appendChild(stop);
39
+ }
40
+ }
41
+ editor.appendChild(actions);
42
+ }