clay-server 5.5.0-beta.1 → 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 (123) 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/sidebar-sessions.js +3 -0
  43. package/lib/public/modules/split-pane-force-stop.js +47 -0
  44. package/lib/public/modules/split-pane-header-state.js +11 -1
  45. package/lib/public/modules/split-pane-renderer.js +2 -2
  46. package/lib/public/modules/split-view-arrange-menu.js +38 -0
  47. package/lib/public/modules/split-view-create-menu.js +9 -5
  48. package/lib/public/modules/split-view-member-actions.js +42 -0
  49. package/lib/public/modules/split-view-picker-actions.js +2 -2
  50. package/lib/public/modules/split-view-presentation-renderer.js +10 -35
  51. package/lib/public/modules/split-view-presentations.js +9 -8
  52. package/lib/public/modules/split-view-sidebar-menu.js +2 -1
  53. package/lib/public/modules/split-view-workers.js +32 -17
  54. package/lib/public/modules/stt-settings.js +21 -4
  55. package/lib/public/modules/user-settings-layout.js +11 -8
  56. package/lib/public/modules/user-settings-provider-install.js +42 -0
  57. package/lib/public/modules/user-settings-providers.js +102 -0
  58. package/lib/public/modules/user-settings-voice.js +50 -41
  59. package/lib/public/modules/vendor-login-browser.js +1 -1
  60. package/lib/public/modules/vendor-login.js +72 -12
  61. package/lib/public/modules/view-workspace-state.js +1 -1
  62. package/lib/public/modules/worker-activity-indicator.js +49 -0
  63. package/lib/public/modules/worker-message-previews.js +47 -11
  64. package/lib/public/modules/worker-stop-controls.js +36 -0
  65. package/lib/public/modules/worktree-controls.js +86 -0
  66. package/lib/public/pane-assets/{app-4IZ6F6ND.js → app-ANJS2LSK.js} +1 -1
  67. package/lib/public/pane-assets/{chunk-L3FCTD7U.js → chunk-32YIYF7R.js} +1 -1
  68. package/lib/public/pane-assets/{chunk-RRYWV5JT.js → chunk-3EJ5IAHZ.js} +1 -1
  69. package/lib/public/pane-assets/{chunk-3E6LDNXF.js → chunk-3TIVBAKM.js} +1 -1
  70. package/lib/public/pane-assets/{chunk-I6X5J2V5.js → chunk-4QFNLHW5.js} +1 -1
  71. package/lib/public/pane-assets/{chunk-OB3KKICE.js → chunk-5XMZMIFG.js} +1 -1
  72. package/lib/public/pane-assets/chunk-6S2C7Q2N.js +1 -0
  73. package/lib/public/pane-assets/chunk-7B2GJI57.js +1 -0
  74. package/lib/public/pane-assets/{chunk-A7OB7HQS.js → chunk-BOFNCPTN.js} +1 -1
  75. package/lib/public/pane-assets/{chunk-ELL4UBGY.js → chunk-CXWAJEN5.js} +1 -1
  76. package/lib/public/pane-assets/{chunk-SEELA2D2.js → chunk-EWTXBZJO.js} +1 -1
  77. package/lib/public/pane-assets/{chunk-ADGKPVFD.js → chunk-G35ZOJGP.js} +1 -1
  78. package/lib/public/pane-assets/chunk-HYT24UBT.js +1 -0
  79. package/lib/public/pane-assets/chunk-ISQEPZMY.js +1 -0
  80. package/lib/public/pane-assets/{chunk-6ZF2FJT3.js → chunk-JM46WGDH.js} +1 -1
  81. package/lib/public/pane-assets/chunk-LBJG4MK2.js +174 -0
  82. package/lib/public/pane-assets/{chunk-YXJ7ISEK.js → chunk-P3AK4ORJ.js} +1 -1
  83. package/lib/public/pane-assets/chunk-QKPGE7DN.js +1 -0
  84. package/lib/public/pane-assets/chunk-TS5JUEVH.js +1 -0
  85. package/lib/public/pane-assets/{chunk-YWHBNEB2.js → chunk-UGH5MCNK.js} +1 -1
  86. package/lib/public/pane-assets/{chunk-YFUORWA6.js → chunk-V5DLPSLT.js} +1 -1
  87. package/lib/public/pane-assets/chunk-VFKDU64W.js +1 -0
  88. package/lib/public/pane-assets/{chunk-A4MLSYBJ.js → chunk-XSSTR6GC.js} +1 -1
  89. package/lib/public/pane-assets/{chunk-DJ5WXQKT.js → chunk-XVESKMU3.js} +1 -1
  90. package/lib/public/pane-assets/manifest.json +443 -433
  91. package/lib/public/pane-assets/style-VBVCWV44.css +1 -0
  92. package/lib/sdk-bridge.js +40 -6
  93. package/lib/sdk-force-stop.js +62 -0
  94. package/lib/sdk-message-processor.js +2 -1
  95. package/lib/server-worktree-controls.js +34 -0
  96. package/lib/server.js +11 -1
  97. package/lib/session-force-stop-support.js +15 -0
  98. package/lib/sessions.js +6 -1
  99. package/lib/terminal-manager.js +1 -1
  100. package/lib/terminal.js +13 -1
  101. package/lib/vendor-login-browser-flow.js +1 -1
  102. package/lib/worker-activity.js +26 -0
  103. package/lib/worker-message-previews.js +12 -7
  104. package/lib/worktree-branch.js +57 -0
  105. package/lib/worktree-identity.js +39 -0
  106. package/lib/worktree-preferences.js +25 -0
  107. package/lib/worktree-ref-transaction.js +36 -0
  108. package/lib/yoke/adapters/antigravity.js +3 -0
  109. package/lib/yoke/force-termination.js +26 -0
  110. package/lib/yoke/interface.js +14 -0
  111. package/lib/yoke/process-force-termination.js +24 -0
  112. package/lib/yoke/provider-install-recipes.js +20 -0
  113. package/lib/yoke/vendor-registry.js +20 -1
  114. package/package.json +1 -1
  115. package/lib/public/pane-assets/chunk-6KO36HMU.js +0 -1
  116. package/lib/public/pane-assets/chunk-FILRACQB.js +0 -168
  117. package/lib/public/pane-assets/chunk-H6MU72QE.js +0 -1
  118. package/lib/public/pane-assets/chunk-PJURCIJQ.js +0 -1
  119. package/lib/public/pane-assets/chunk-Q7RFSS5Q.js +0 -1
  120. package/lib/public/pane-assets/chunk-UTLQWAKG.js +0 -1
  121. package/lib/public/pane-assets/chunk-WE4NWXNK.js +0 -1
  122. package/lib/public/pane-assets/chunk-Z2MHS43B.js +0 -1
  123. package/lib/public/pane-assets/style-DZX6JAMF.css +0 -1
package/lib/sdk-bridge.js CHANGED
@@ -1075,7 +1075,7 @@ function createSDKBridge(opts) {
1075
1075
  console.log("[sdk-bridge] processQueryStream ended: isProcessing=" + session.isProcessing + " taskStopRequested=" + session.taskStopRequested);
1076
1076
  var stillOwnsRuntime = session.queryInstance === myQueryInstance && !responseRecovery.stopping(session);
1077
1077
  settleContinuationStartup(session, myQueryInstance, myQueryGeneration, false, "The successor query ended before initialization.");
1078
- if (session.isProcessing && session.taskStopRequested && stillOwnsRuntime) {
1078
+ if ((session.isProcessing || session._awaitingTurnResult) && session.taskStopRequested && stillOwnsRuntime) {
1079
1079
  session._lastTurnInterrupted = true;
1080
1080
  session.isProcessing = false;
1081
1081
  session._awaitingTurnResult = false;
@@ -1088,7 +1088,7 @@ function createSDKBridge(opts) {
1088
1088
  sendAndRecord(session, { type: "info", text: interruptMsg });
1089
1089
  sendAndRecord(session, { type: "done", code: 0 });
1090
1090
  sm.broadcastSessionList();
1091
- } else if (session.isProcessing && stillOwnsRuntime) {
1091
+ } else if ((session.isProcessing || session._awaitingTurnResult) && stillOwnsRuntime) {
1092
1092
  // Codex and Kiro can report a protocol error and then close their
1093
1093
  // iterator without a result event. Treat any result-less stream end as
1094
1094
  // a failed turn so the UI cannot remain on processing forever.
@@ -1109,7 +1109,7 @@ function createSDKBridge(opts) {
1109
1109
  }
1110
1110
  } catch (err) {
1111
1111
  settleContinuationStartup(session, myQueryInstance, myQueryGeneration, false, err.message || "The successor query failed before initialization.");
1112
- if (session.isProcessing && session.queryInstance === myQueryInstance && !responseRecovery.stopping(session)) {
1112
+ if ((session.isProcessing || session._awaitingTurnResult) && session.queryInstance === myQueryInstance && !responseRecovery.stopping(session)) {
1113
1113
  session.isProcessing = false;
1114
1114
  session._awaitingTurnResult = false;
1115
1115
  session._queuedTurnCount = 0;
@@ -1360,6 +1360,15 @@ function createSDKBridge(opts) {
1360
1360
  // Claude: conversation rollback is handled by rewindFiles + local history trim
1361
1361
  }
1362
1362
 
1363
+ function forceProvider(session) {
1364
+ var vendor = session.vendor || sm.defaultVendor || adapter && adapter.vendor;
1365
+ return adapters[vendor] || (adapter && adapter.vendor === vendor ? adapter : vendor);
1366
+ }
1367
+ sm.getForceStopSupport = function (session) {
1368
+ if (session._queryStarting) return { supported: false, reason: 'Force termination is unavailable while this runtime is starting' };
1369
+ return require('./yoke/force-termination').support(forceProvider(session), session.queryInstance);
1370
+ };
1371
+
1363
1372
  function getAdapterForSession(session) {
1364
1373
  var vendor = session.vendor || sm.defaultVendor || "claude";
1365
1374
  return adapters[vendor] || adapter;
@@ -1405,8 +1414,9 @@ function createSDKBridge(opts) {
1405
1414
  var payload = { text: text, images: images, linuxUser: linuxUser, beforePush: beforePush };
1406
1415
  var recoveryState = responseRecovery.begin(session, payload, retryState);
1407
1416
  if (recoveryState) recoveryState.handle = null;
1417
+ var startRevision = session._forceStopRevision || 0;
1408
1418
  var guardedPush = function() {
1409
- return !session.destroying && !session.taskStopRequested
1419
+ return (session._forceStopRevision || 0) === startRevision && !session.destroying && !session.taskStopRequested
1410
1420
  && (!recoveryState || responseRecovery.valid(session, recoveryState) && !recoveryState.stopping)
1411
1421
  && (typeof beforePush !== "function" || beforePush() === true);
1412
1422
  };
@@ -1417,6 +1427,7 @@ function createSDKBridge(opts) {
1417
1427
  if (recoveryState) recoveryState.starting = starting;
1418
1428
  try {
1419
1429
  var accepted = await starting;
1430
+ if ((session._forceStopRevision || 0) !== startRevision) return false;
1420
1431
  if (accepted === false && (!recoveryState || !recoveryState.stopping)) {
1421
1432
  responseRecovery.cancel(session);
1422
1433
  session.isProcessing = false;
@@ -1424,6 +1435,7 @@ function createSDKBridge(opts) {
1424
1435
  }
1425
1436
  return accepted;
1426
1437
  } catch (error) {
1438
+ if ((session._forceStopRevision || 0) !== startRevision) return false;
1427
1439
  // Includes synchronous input queue failures, before stream consumption.
1428
1440
  var failedHandle = session.queryInstance;
1429
1441
  if (failedHandle) {
@@ -1446,6 +1458,7 @@ function createSDKBridge(opts) {
1446
1458
  }
1447
1459
  return false;
1448
1460
  } finally {
1461
+ if ((session._forceStopRevision || 0) !== startRevision) return false;
1449
1462
  session._queryStarting = false;
1450
1463
  delete session._providerRecoveryWaiting;
1451
1464
  if (!session.queryInstance && session.pendingPush && session.pendingPush.length) session.pendingPush = [];
@@ -1580,11 +1593,18 @@ function createSDKBridge(opts) {
1580
1593
  }
1581
1594
  }
1582
1595
  }
1596
+ if (typeof beforePush === "function" && beforePush() !== true) return false;
1583
1597
  // If still not available after lazy check, send auth_required
1584
1598
  if (session.vendor && !adapters[session.vendor]) {
1585
1599
  var missingVendorInfo = yoke.getVendorInfo(session.vendor);
1586
1600
  var vendorName = (missingVendorInfo && missingVendorInfo.displayName)
1587
1601
  || session.vendor.charAt(0).toUpperCase() + session.vendor.slice(1);
1602
+ var installedForIdentity = linuxUser ? require('./yoke/discovery').installed({ user: linuxUser, env: sessionRuntimeEnv }) : yoke.checkInstalled();
1603
+ if (!installedForIdentity[session.vendor]) {
1604
+ sendAndRecord(session, { type: 'error', text: vendorName + ' is not installed for this runtime. Install the provider before retrying the task.' });
1605
+ sendAndRecord(session, { type: 'done', code: 1 });
1606
+ return false;
1607
+ }
1588
1608
  var authUser = session.ownerId ? usersModule.findUserById(session.ownerId) : null;
1589
1609
  var authLinuxUser = authUser && authUser.linuxUser ? authUser.linuxUser : null;
1590
1610
  var canAutoLogin = !usersModule.isMultiUser()
@@ -1650,6 +1670,7 @@ function createSDKBridge(opts) {
1650
1670
  if (typeof preparation === "function") preparation(linuxUser, session, cwd);
1651
1671
  }
1652
1672
 
1673
+ if (typeof beforePush === "function" && beforePush() !== true) return false;
1653
1674
  session.blocks = {};
1654
1675
  session.sentToolResults = {};
1655
1676
  taskLifecycle.clearAll(session);
@@ -1882,6 +1903,7 @@ function createSDKBridge(opts) {
1882
1903
  // the SDK ignores Options.title in favor of the persisted title.
1883
1904
  var initialTitle = null;
1884
1905
  var initialTitleProvisional = false;
1906
+ if (typeof beforePush === "function" && beforePush() !== true) return false;
1885
1907
  if (!session.cliSessionId && !session.titleManuallySet && !session.titleAutoGenerated) {
1886
1908
  var initialTitleChoice = sessionTitlePolicy.selectInitialTitle(session, text);
1887
1909
  initialTitle = initialTitleChoice.title;
@@ -2014,6 +2036,15 @@ function createSDKBridge(opts) {
2014
2036
  console.log("[sdk-bridge] calling adapter.createQuery... vendor=" + sessionAdapter.vendor);
2015
2037
  try {
2016
2038
  handle = await sessionAdapter.createQuery(queryOpts);
2039
+ if (Number(session._sdkQueryGeneration || 0) !== queryGeneration || !(recoveryState && recoveryState.stopping) && typeof beforePush === "function" && beforePush() !== true) {
2040
+ try { if (handle.abort) Promise.resolve(handle.abort()).catch(function () {}); } catch (e) {}
2041
+ try { if (handle.close) Promise.resolve(handle.close()).catch(function () {}); } catch (e) {}
2042
+ if (Number(session._sdkQueryGeneration || 0) === queryGeneration) {
2043
+ sendAndRecord(session, { type: "info", text: "Stopped before sending your message." });
2044
+ sendAndRecord(session, { type: "done", code: 0 });
2045
+ }
2046
+ return false;
2047
+ }
2017
2048
  // A guarded delivery must still own the exact startup after provider
2018
2049
  // creation. Never replace a newer handle or publish to a deleted target.
2019
2050
  if (beforePush && beforePush.exactProductDelivery) {
@@ -2046,7 +2077,7 @@ function createSDKBridge(opts) {
2046
2077
  console.error("[sdk-bridge] Failed to create query for session " + session.localId + ":", e.message || e);
2047
2078
  console.error("[sdk-bridge] cliSessionId:", session.cliSessionId, "resume:", !!session.cliSessionId);
2048
2079
  console.error("[sdk-bridge] Stack:", e.stack || "(no stack)");
2049
- if (beforePush && beforePush.exactProductDelivery && (sm.sessions.get(session.localId) !== session || session.queryInstance !== startupHandle || Number(session._sdkQueryGeneration || 0) !== queryGeneration)) return false;
2080
+ if (sm.sessions.get(session.localId) !== session || session.queryInstance !== startupHandle || Number(session._sdkQueryGeneration || 0) !== queryGeneration) return false;
2050
2081
  session.isProcessing = false;
2051
2082
  session._awaitingTurnResult = false;
2052
2083
  session._queuedTurnCount = 0;
@@ -2110,7 +2141,8 @@ function createSDKBridge(opts) {
2110
2141
  if (typeof beforePush === "function" && beforePush() !== true) {
2111
2142
  try { if (typeof handle.abort === "function") handle.abort(); } catch (e) {}
2112
2143
  try { handle.close(); } catch (e) {}
2113
- if (session.queryInstance === handle) session.queryInstance = null;
2144
+ if (session.queryInstance !== handle || Number(session._sdkQueryGeneration || 0) !== queryGeneration) return false;
2145
+ session.queryInstance = null;
2114
2146
  session.messageQueue = null;
2115
2147
  session.abortController = null;
2116
2148
  session.isProcessing = false;
@@ -2735,6 +2767,8 @@ function createSDKBridge(opts) {
2735
2767
  }
2736
2768
 
2737
2769
  return {
2770
+ getForceStopSupport: sm.getForceStopSupport,
2771
+ forceStopSession: require("./sdk-force-stop").createForceStop({ sm: sm, provider: forceProvider, record: sendAndRecord, changed: onProcessingChanged }),
2738
2772
  createMessageQueue: createMessageQueue,
2739
2773
  processSDKMessage: processSDKMessage,
2740
2774
  checkToolWhitelist: checkToolWhitelist,
@@ -0,0 +1,62 @@
1
+ // Retire one exact query and invoke only its declared YOKE force operation.
2
+ function createForceStop(ctx) {
3
+ return async function forceStop(session) {
4
+ if (ctx.sm.sessions.get(session.localId) !== session) throw new Error('Session was deleted or replaced.');
5
+ if (session._queryStarting) throw new Error('Force termination is unavailable while this runtime is starting');
6
+ var terminate = require("./yoke/force-termination").operation(ctx.provider(session), session.queryInstance);
7
+ var undelivered = session.pendingPush || [];
8
+ session._forceStopRevision = (session._forceStopRevision || 0) + 1;
9
+ session._sdkQueryGeneration = Number(session._sdkQueryGeneration || 0) + 1;
10
+ session._queryGeneration = (session._queryGeneration || 0) + 1;
11
+ session.taskStopRequested = true;
12
+ session._pendingMessageDrainPaused = true;
13
+ session.queryInstance = null;
14
+ session.abortController = null;
15
+ session.messageQueue = null;
16
+ session._queryStarting = false;
17
+ session._responseRecoveryStopPending = null;
18
+ session.isProcessing = false;
19
+ session._awaitingTurnResult = false;
20
+ session._queuedTurnCount = 0;
21
+ session.pendingPush = [];
22
+ delete session._foregroundAdmission;
23
+ delete session._providerRecoveryWaiting;
24
+ session._lastTurnInterrupted = true;
25
+ var probe = session._continuationStartupProbe;
26
+ if (probe && probe.timer) clearTimeout(probe.timer);
27
+ delete session._continuationStartupProbe;
28
+ if (probe && typeof probe.resolve === 'function') probe.resolve({ accepted: false, reason: 'Force stopped.' });
29
+ var permissions = session.pendingPermissions || {};
30
+ Object.keys(permissions).forEach(function (id) {
31
+ if (typeof permissions[id].resolve === 'function') permissions[id].resolve({ behavior: 'deny', message: 'Force stopped.' });
32
+ if (ctx.sm.permissionRequestIndex) delete ctx.sm.permissionRequestIndex[id];
33
+ });
34
+ var questions = session.pendingAskUser || {};
35
+ Object.keys(questions).forEach(function (id) {
36
+ var respond = questions[id] && questions[id].respond;
37
+ if (respond && typeof respond.cancel === 'function') respond.cancel('Force stopped.');
38
+ });
39
+ session.pendingPermissions = {};
40
+ session.pendingAskUser = {};
41
+ var dialogs = session.pendingUserDialogs || {};
42
+ Object.keys(dialogs).forEach(function (id) { if (typeof dialogs[id].resolve === 'function') dialogs[id].resolve({ behavior: 'cancelled' }); });
43
+ var elicitations = session.pendingElicitations || {};
44
+ Object.keys(elicitations).forEach(function (id) { if (typeof elicitations[id].resolve === 'function') elicitations[id].resolve({ action: 'cancel' }); });
45
+ session.pendingUserDialogs = {};
46
+ session.pendingElicitations = {};
47
+ var revision = session._forceStopRevision, generation = session._sdkQueryGeneration, owner = session.ownerId, vendor = session.vendor;
48
+ ctx.changed(); ctx.sm.broadcastSessionList();
49
+ var outcome;
50
+ try { outcome = await terminate(); }
51
+ catch (error) { outcome = { status: 'unconfirmed', externalEffects: 'unconfirmed' }; }
52
+ if (ctx.sm.sessions.get(session.localId) !== session || session._forceStopRevision !== revision || session._sdkQueryGeneration !== generation || session.ownerId !== owner || session.vendor !== vendor || session.queryInstance || session._queryStarting) return { status: 'failed', runtimeEffect: 'connection_retired', providerStop: outcome.status, externalEffects: 'unconfirmed', queuePaused: true, detail: 'Session was replaced during termination.' };
53
+ ctx.changed();
54
+ if (undelivered.length) ctx.record(session, { type: 'error', text: 'Buffered messages were not replayed after force stop. Review them before sending again.', recoveryAdmission: { state: 'not_sent', messages: undelivered.map(function (item) { return { text: item.text, images: item.images || null }; }) } });
55
+ ctx.record(session, { type: 'thinking_stop' });
56
+ ctx.record(session, { type: 'info', text: (outcome.status === 'terminated' ? 'The query runtime exited.' : 'Query runtime termination could not be confirmed.') + ' Queued messages are paused. Commands or external requests already sent may still be running; inspect their outcome before continuing.' });
57
+ ctx.record(session, { type: 'done', code: outcome.status === 'terminated' ? 0 : 1 });
58
+ ctx.sm.broadcastSessionList();
59
+ return { status: outcome.status === 'terminated' ? 'completed' : 'failed', runtimeEffect: 'connection_retired', providerStop: outcome.status, externalEffects: 'unconfirmed', queuePaused: true };
60
+ };
61
+ }
62
+ module.exports = { createForceStop: createForceStop };
@@ -81,8 +81,9 @@ function attachMessageProcessor(ctx) {
81
81
  // per session to avoid spamming when multiple auth errors arrive in a burst.
82
82
  function emitAuthRequired(session) {
83
83
  var now = Date.now();
84
- if (session._lastAuthEmit && (now - session._lastAuthEmit) < 5000) return;
84
+ if (session._lastAuthQuery === session._queryGeneration && session._lastAuthEmit && (now - session._lastAuthEmit) < 5000) return;
85
85
  session._lastAuthEmit = now;
86
+ session._lastAuthQuery = session._queryGeneration;
86
87
 
87
88
  var vendor = session.vendor || "claude";
88
89
 
@@ -0,0 +1,34 @@
1
+ // HTTP keeps the observed result reachable after the target project's sockets close.
2
+ var crypto = require('crypto');
3
+ function attachWorktreeHttp(ctx) {
4
+ function reply(res, code, data) { res.writeHead(code, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' }); res.end(JSON.stringify(data)); }
5
+ function handleRequest(req, res, url) {
6
+ if (url !== '/api/worktree-removal/preview' && url !== '/api/worktree-removal/execute') return false;
7
+ if (!ctx.isRequestAuthed(req)) { reply(res, 401, { ok: false, error: 'Authentication required.' }); return true; }
8
+ if (!ctx.service) { reply(res, 503, { ok: false, error: 'Worktree removal is unavailable in this server runtime.' }); return true; }
9
+ if (req.method !== 'POST' || req.headers['x-clay-worktree-control'] !== '1' ||
10
+ (req.headers['content-type'] || '').split(';')[0].trim().toLowerCase() !== 'application/json' ||
11
+ req.headers['sec-fetch-site'] && ['same-origin', 'none'].indexOf(req.headers['sec-fetch-site']) < 0) {
12
+ reply(res, 403, { ok: false, error: 'Use the worktree menu in Clay Studio.' }); return true;
13
+ }
14
+ var actor = ctx.getMultiUserFromReq(req), userId = actor && actor.id || null;
15
+ function validate() {
16
+ var current = ctx.getMultiUserFromReq(req);
17
+ if (!ctx.isRequestAuthed(req) || (current && current.id || null) !== userId) throw new Error('The authenticated owner session changed.');
18
+ }
19
+ var connection = crypto.createHash('sha256').update(req.headers.cookie || '').digest('hex');
20
+ var body = '', bytes = 0, oversized = false;
21
+ req.setEncoding('utf8');
22
+ req.on('data', function (chunk) { bytes += Buffer.byteLength(chunk); if (bytes > 4096) { if (!oversized) reply(res, 413, { ok: false, error: 'Request is too large.' }); oversized = true; } else body += chunk; });
23
+ req.on('end', function () {
24
+ if (oversized) return;
25
+ Promise.resolve().then(function () {
26
+ validate(); var data = JSON.parse(body);
27
+ return url.endsWith('/preview') ? ctx.service.preview(data.slug, data.action, userId, connection, data.deleteBranch) : ctx.service.execute(data.token, userId, connection, validate, data.deleteBranch);
28
+ }).then(function (result) { reply(res, 200, result); }, function (error) { reply(res, 409, Object.assign({ ok: false, error: error.message }, error.lifecycleResult || {})); });
29
+ });
30
+ return true;
31
+ }
32
+ return { handleRequest: handleRequest };
33
+ }
34
+ module.exports = { attachWorktreeHttp: attachWorktreeHttp };
package/lib/server.js CHANGED
@@ -636,6 +636,10 @@ function createServer(opts) {
636
636
  res.setHeader("Content-Security-Policy", securityHeaders["Content-Security-Policy"] + " frame-src " + frameSrcFor(req) + ";");
637
637
  }
638
638
 
639
+ var worktreeHttp = require("./server-worktree-controls").attachWorktreeHttp({
640
+ service: opts.worktreeControls, isRequestAuthed: isRequestAuthed, getMultiUserFromReq: getMultiUserFromReq,
641
+ });
642
+
639
643
  // --- HTTP handler ---
640
644
  var appHandler = function (req, res) {
641
645
  setSecurityHeaders(res, req);
@@ -899,6 +903,7 @@ function createServer(opts) {
899
903
  if (linear.handleRequest(req, res, fullUrl)) return;
900
904
  if (speech.handleRequest(req, res, fullUrl)) return;
901
905
  if (githubStars.handleRequest(req, res, fullUrl)) return;
906
+ if (worktreeHttp.handleRequest(req, res, fullUrl)) return;
902
907
  if (networkSettings.handleRequest(req, res, fullUrl)) return;
903
908
  if (storageSettings.handleRequest(req, res, fullUrl)) return;
904
909
  if (settings.handleRequest(req, res, fullUrl)) return;
@@ -1475,7 +1480,7 @@ function createServer(opts) {
1475
1480
  var mate = mates.getMate(mates.buildMateCtx(users.isMultiUser() ? projectOwnerId : null), extra.mateId || path.basename(cwd));
1476
1481
  return { isMate: true, visibility: "private", ownerId: projectOwnerId || null, allowedUsers: [], lifecycleExecutionPaused: !mate || !!mate.lifecycleDeletion };
1477
1482
  }
1478
- return onGetProjectAccess ? onGetProjectAccess(slug) : { visibility: "public", ownerId: projectOwnerId || null };
1483
+ return onGetProjectAccess ? onGetProjectAccess(slug, { slug: slug, path: cwd, changeSetId: worktreeMeta && worktreeMeta.changeSetId }) : { visibility: "public", ownerId: projectOwnerId || null };
1479
1484
  },
1480
1485
  shouldSuppressResponseNotification: function (session) {
1481
1486
  return shouldSuppressHomeResponse(projects, slug, session);
@@ -2118,6 +2123,11 @@ function createServer(opts) {
2118
2123
  destroyAll: destroyAll,
2119
2124
  durableScheduler: durableScheduler,
2120
2125
  setProjectLogContextState: setProjectLogContextState,
2126
+ captureWorktreeLogContext: function (slug) {
2127
+ var project = projects.get(slug), status = project && project.getStatus();
2128
+ if (!status || !status.isWorktree) throw new Error('Worktree context is unavailable.');
2129
+ return function (state) { return projectLogsService.setContextState(slug, state, status); };
2130
+ },
2121
2131
  };
2122
2132
  }
2123
2133
 
@@ -0,0 +1,15 @@
1
+ var provenance = require('./session-provenance');
2
+ var termination = require('./yoke/force-termination');
3
+ function targets(sm, session) {
4
+ return [session].concat(provenance.isWorker(session) ? [] : provenance.ownedWorkersByDriver(Array.from(sm.sessions.values()))[session.localId] || []);
5
+ }
6
+ function one(sm, session) {
7
+ if (session._queryStarting) return { supported: false, reason: 'Force termination is unavailable while this runtime is starting' };
8
+ return typeof sm.getForceStopSupport === 'function' ? sm.getForceStopSupport(session) : termination.support(session.vendor || sm.defaultVendor, session.queryInstance);
9
+ }
10
+ function describe(sm, session) {
11
+ var scope = targets(sm, session), unavailable = scope.find(function (target) { return !one(sm, target).supported; });
12
+ if (unavailable) return { supported: false, reason: one(sm, unavailable).reason + (unavailable === session ? '' : ' (owned Worker)') };
13
+ return { supported: true, scope: 'query-runtime', externalEffects: 'unconfirmed' };
14
+ }
15
+ module.exports = { targets: targets, one: one, describe: describe };
package/lib/sessions.js CHANGED
@@ -634,6 +634,7 @@ function createSessionManager(opts) {
634
634
  var node = hierarchy[session.localId];
635
635
  var ownedCount = ownedWorkerCountFor(session, node, owned, viewerId);
636
636
  if (ownedCount !== undefined) mapped.ownedWorkerCount = ownedCount;
637
+ mapped.forceTermination = ownedCount !== undefined && (!users.isMultiUser() || viewerId && session.ownerId === viewerId) ? require('./session-force-stop-support').describe(api || { sessions: sessions, defaultVendor: defaultVendor }, session) : { supported: false, reason: 'Force termination requires the session owner' };
637
638
  mapped.sessionRole = node.role;
638
639
  mapped.parentSessionId = node.parentSessionId;
639
640
  mapped.parentAvailable = node.parentAvailable;
@@ -1058,6 +1059,7 @@ function createSessionManager(opts) {
1058
1059
  }
1059
1060
 
1060
1061
  function doSendToSession(session, obj) {
1062
+ if (obj.type === 'auth_required' && opts.onAuthRequired && opts.onAuthRequired(session, obj)) return;
1061
1063
  // Send to active clients without recording to history/disk (ephemeral data)
1062
1064
  if (sendEach) {
1063
1065
  var data = JSON.stringify(obj);
@@ -1074,12 +1076,14 @@ function createSessionManager(opts) {
1074
1076
  }
1075
1077
 
1076
1078
  function doSendAndRecord(session, obj, clientObj) {
1079
+ if (obj.type === 'auth_required') { doSendToSession(session, clientObj || obj); return true; }
1077
1080
  // Stamp every recorded message so history replay preserves original times
1078
1081
  if (!obj._ts) obj._ts = Date.now();
1079
1082
  var outgoingObj = clientObj || obj;
1080
1083
  if (!outgoingObj._ts) outgoingObj._ts = obj._ts;
1081
1084
  session.history.push(obj);
1082
1085
  if (appendToSessionFile(session, obj) === false) return false;
1086
+ require('./worker-activity').observe(session, outgoingObj);
1083
1087
  // Per-session out-of-band subscribers mirror mate session events into
1084
1088
  // a parallel UI without joining the project's
1085
1089
  // ws clients set). Subscribers receive the same obj that goes to ws
@@ -1394,7 +1398,7 @@ function createSessionManager(opts) {
1394
1398
  }).catch(function() {});
1395
1399
  }
1396
1400
 
1397
- return {
1401
+ var api = {
1398
1402
  setSessionHydrator: function (fn) { onSessionHydrate = typeof fn === "function" ? fn : null; },
1399
1403
  get activeSessionId() { return activeSessionId; },
1400
1404
  get nextLocalId() { return nextLocalId; },
@@ -1567,6 +1571,7 @@ function createSessionManager(opts) {
1567
1571
  },
1568
1572
  permissionRequestIndex: permissionRequestIndex,
1569
1573
  };
1574
+ return api;
1570
1575
  }
1571
1576
 
1572
1577
  module.exports = { createSessionManager };
@@ -163,7 +163,7 @@ function createTerminalManager(opts) {
163
163
 
164
164
  function write(id, data) {
165
165
  var session = terminals.get(id);
166
- if (session && session.pty) {
166
+ if (session && session.pty && session.kind !== "provider-install") {
167
167
  session.lastActivityAt = Date.now(); // user input counts as activity
168
168
  session.pty.write(data);
169
169
  }
package/lib/terminal.js CHANGED
@@ -43,12 +43,24 @@ function createTerminal(cwd, cols, rows, osUserInfo, opts) {
43
43
  }
44
44
 
45
45
  var args = osUserInfo ? ["-l"] : [];
46
+ if (opts && opts.providerInstall) {
47
+ var recipe = require('./yoke/vendor-registry').getVendorInfo(opts.providerInstall);
48
+ recipe = recipe && recipe.installRecipe;
49
+ var reason = require('./yoke/provider-install-recipes').eligible(recipe,osUserInfo);
50
+ if (reason || typeof opts.revalidateInstall !== 'function') throw new Error(reason || 'Installation authority is missing.');
51
+ opts.revalidateInstall();
52
+ shell = '/bin/bash';
53
+ args = ['--noprofile','--norc','-o','pipefail','-c',recipe.command];
54
+ spawnOpts.cwd = osUserInfo.home;
55
+ // Bash must not source daemon/user startup files; installers receive no keys.
56
+ delete termEnv.NODE_OPTIONS;
57
+ }
46
58
  // Route through setpriv when group inheritance is enabled so the terminal
47
59
  // gets the user's supplementary groups (docker, etc.), not just primary gid.
48
60
  var ptySpawn = require("./os-users").wrapSpawnAsUser(shell, args, spawnOpts);
49
61
  var term = pty.spawn(ptySpawn.command, ptySpawn.args, ptySpawn.options);
50
62
 
51
- if (opts && opts.initialInput) {
63
+ if (opts && opts.initialInput && !opts.providerInstall) {
52
64
  try { term.write(opts.initialInput); } catch (e) {}
53
65
  }
54
66
 
@@ -3,7 +3,7 @@ var createLoginBrowser = require("./vendor-login-browser").createLoginBrowser;
3
3
  function attachBrowserLogin(ctx) {
4
4
  function allowed(ws, flow) {
5
5
  var userId = ws && ws._clayUser ? String(ws._clayUser.id) : null;
6
- return userId === flow.ownerId && ctx.hasTerminalPermission(ws);
6
+ return userId === flow.ownerId && ctx.hasTerminalPermission(ws) && (!ctx.validate || ctx.validate(ws, flow));
7
7
  }
8
8
  function prepare(ws, flow, identity) {
9
9
  if (ctx.osUsers && !identity.osUserInfo) throw new Error("OS user identity is unavailable");
@@ -0,0 +1,26 @@
1
+ // Live recorded output only. Reading history or projecting a snapshot never stamps activity.
2
+ var crypto = require('crypto');
3
+ var provenance = require('./session-provenance');
4
+ var tools = { Bash: 'Running a command', Read: 'Reading files', Edit: 'Editing files', Write: 'Writing files',
5
+ Grep: 'Searching files', Glob: 'Finding files', WebSearch: 'Searching the web', WebFetch: 'Reading a web page', Task: 'Delegating work' };
6
+ function summary(event) {
7
+ if (!event) return null;
8
+ if (event.type === 'delta' || event.type === 'assistant_message') return typeof event.text === 'string' && event.text.trim() ? 'Writing a reply' : null;
9
+ if (event.type === 'thinking_delta') return typeof event.text === 'string' && event.text.trim() ? 'Thinking' : null;
10
+ if (event.type === 'thinking_start') return 'Thinking';
11
+ if (event.type === 'tool_start' || event.type === 'tool_executing') return Object.prototype.hasOwnProperty.call(tools, event.name) ? tools[event.name] : 'Using a tool';
12
+ if (event.type === 'tool_result') return 'Tool result received';
13
+ return null;
14
+ }
15
+ function observe(session, event) {
16
+ if (!provenance.isWorker(session)) return;
17
+ var label = summary(event); if (!label) return;
18
+ var previous = session._workerActivity;
19
+ session._workerActivity = { epoch: previous ? previous.epoch : crypto.randomUUID(), sequence: previous ? previous.sequence + 1 : 1,
20
+ queryGeneration: Number(session._sdkQueryGeneration || 0), timestamp: Date.now(), summary: label };
21
+ }
22
+ function project(session) {
23
+ var activity = session._workerActivity;
24
+ return activity && activity.queryGeneration === Number(session._sdkQueryGeneration || 0) ? Object.assign({}, activity) : null;
25
+ }
26
+ module.exports = { observe: observe, project: project, summary: summary };
@@ -29,16 +29,21 @@ function attach(ctx) {
29
29
  function send(live) {
30
30
  var current = ctx.resolve(ws, target);
31
31
  if (subscriptions.get(ws) !== state || ws.readyState !== 1 || !current || current.driver !== pair.driver || current.worker !== pair.worker) { if (entry.release) entry.release(); return; }
32
- var preview = latest(pair.worker.history || []); if (!preview) return;
33
- preview.vendor = pair.worker.vendor || null;
34
- if (entry.last === JSON.stringify(preview)) return;
35
- entry.last = JSON.stringify(preview);
32
+ var preview = latest(pair.worker.history || []), activity = require('./worker-activity').project(pair.worker);
33
+ if (preview) preview.vendor = pair.worker.vendor || null;
34
+ var signature = JSON.stringify([preview, activity, pair.worker.isProcessing === true]);
35
+ if (entry.last === signature) return;
36
+ entry.last = signature;
36
37
  ctx.send(ws, { type: 'split_worker_message_preview', requestId: msg.requestId, parentSessionOriginId: target.parentSessionOriginId,
37
- workerSessionOriginId: target.workerSessionOriginId, generation: target.generation, live: live, preview: preview });
38
+ workerSessionOriginId: target.workerSessionOriginId, generation: target.generation, live: live, preview: preview, activity: activity, isProcessing: pair.worker.isProcessing === true, observedAt: Date.now(), vendor: pair.worker.vendor || null });
38
39
  }
39
40
  entry.release = ctx.sm.subscribeSession(pair.worker.localId, function (event) {
40
- if (!event || ['delta', 'assistant_message'].indexOf(event.type) < 0 || entry.timer) return;
41
- entry.timer = setTimeout(function () { entry.timer = null; send(true); }, 60);
41
+ if (!event) return;
42
+ var genuine = !!require('./worker-activity').summary(event);
43
+ if (!genuine && event.type !== 'done' && event.type !== 'error') return;
44
+ entry.pendingLive = entry.pendingLive || genuine;
45
+ if (entry.timer) return;
46
+ entry.timer = setTimeout(function () { entry.timer = null; var live = !!entry.pendingLive; entry.pendingLive = false; send(live); }, 60);
42
47
  });
43
48
  state.entries.push(entry); send(false);
44
49
  });
@@ -0,0 +1,57 @@
1
+ // Optional local-ref removal, bound to a reviewed tip and retained parent HEAD.
2
+ var identities = require('./worktree-identity');
3
+ var git = require('./git-cli').runGitSync;
4
+ var refTransaction = require('./worktree-ref-transaction');
5
+ function run(snapshot, args, os, options) { return git(snapshot.parent.path, args, options || null, os).trim(); }
6
+ function symbolic(snapshot, ref, os) {
7
+ try { return run(snapshot, ['symbolic-ref', '--quiet', ref], os); }
8
+ catch (error) { if (error.status === 1) return null; throw error; }
9
+ }
10
+ function inputFor(snapshot, branch, os, deleting) {
11
+ var input = 'start\noption no-deref\n';
12
+ // Git cannot verify HEAD's symref and its referent in one transaction.
13
+ if (symbolic(snapshot, 'HEAD', os) !== branch.parentRef) throw new Error('The retained parent HEAD/ref changed.');
14
+ input += 'verify ' + (branch.parentRef || 'HEAD') + ' ' + branch.parentHead + '\n';
15
+ input += 'option no-deref\n' + (deleting ? 'delete ' : 'verify ') + branch.ref + ' ' + branch.tip + '\nprepare\n';
16
+ // No dereferencing: a replacement symbolic ref must never delete its target.
17
+ return input;
18
+ }
19
+ function inspect(snapshot, os, removed, locked) {
20
+ if (!/^refs\/heads\//.test(snapshot.branch)) throw new Error('Detached HEAD has no local branch to delete.');
21
+ if (JSON.stringify(identities.identity(snapshot.parent.path, true)) !== JSON.stringify(snapshot.parent) ||
22
+ JSON.stringify(identities.identity(snapshot.common.path, true)) !== JSON.stringify(snapshot.common)) throw new Error('The retained repository identity changed.');
23
+ var ref = snapshot.branch;
24
+ run(snapshot, ['check-ref-format', ref], os);
25
+ if (symbolic(snapshot, ref, os)) throw new Error('The local branch was replaced by a symbolic ref.');
26
+ var tip = run(snapshot, ['show-ref', '--verify', '--hash', ref], os);
27
+ var parentRef = symbolic(snapshot, 'HEAD', os);
28
+ var parentHead = run(snapshot, ['rev-parse', '--verify', 'HEAD'], os);
29
+ if (parentRef === ref) throw new Error('The local branch is the current parent branch.');
30
+ if (parentRef && symbolic(snapshot, parentRef, os)) throw new Error('The retained parent branch is a symbolic ref.');
31
+ var blocks = run(snapshot, ['worktree', 'list', '--porcelain', '-z'], os).split('\0\0').filter(Boolean);
32
+ blocks.forEach(function (block) {
33
+ var fields = block.split('\0');
34
+ if (fields.indexOf('branch ' + ref) >= 0 && (removed || fields[0] !== 'worktree ' + snapshot.target.path)) throw new Error('The local branch is checked out by another worktree.');
35
+ });
36
+ try { run(snapshot, ['merge-base', '--is-ancestor', tip, parentHead], os); }
37
+ catch (error) { throw new Error('The local branch is not merged into the retained parent HEAD. Keep the branch or merge it first.'); }
38
+ var branch = { ref: ref, tip: tip, parentRef: parentRef, parentHead: parentHead };
39
+ // Probe the fixed transaction syntax before runtime or directory effects.
40
+ if (!locked) run(snapshot, ['update-ref', '--stdin'], os, { input: inputFor(snapshot, branch, os, false) + 'commit\n' });
41
+ return branch;
42
+ }
43
+ function exact(snapshot, branch, os, removed, locked) {
44
+ if (JSON.stringify(inspect(snapshot, os, removed, locked)) !== JSON.stringify(branch)) throw new Error('The reviewed local branch tip or retained parent HEAD/ref changed. Review again.');
45
+ }
46
+ async function remove(snapshot, branch, os, validate) {
47
+ exact(snapshot, branch, os, true);
48
+ await refTransaction.remove(snapshot.parent.path, inputFor(snapshot, branch, os, true), os, function () {
49
+ if (validate) validate();
50
+ // CAS alone permits same-tip symbolic replacements. Inspect under the prepared locks.
51
+ exact(snapshot, branch, os, true, true);
52
+ });
53
+ var refs = run(snapshot, ['for-each-ref', '--format=%(refname)', branch.ref], os).split('\n');
54
+ if (refs.indexOf(branch.ref) >= 0) throw new Error('Local branch absence was not confirmed. Inspect it before recovery.');
55
+ return true;
56
+ }
57
+ module.exports = { inspect: inspect, exact: exact, remove: remove };
@@ -0,0 +1,39 @@
1
+ // Exact Git and filesystem identities for reviewed linked-worktree removal.
2
+ var fs = require('fs');
3
+ var path = require('path');
4
+ var git = require('./git-cli').runGitSync;
5
+ function identity(value, directory) {
6
+ var resolved = path.resolve(value), real = fs.realpathSync(resolved), stat = fs.lstatSync(resolved);
7
+ if (resolved !== real || stat.isSymbolicLink() || (directory && !stat.isDirectory())) throw new Error('Worktree paths must be real directories without symlink aliases.');
8
+ return { path: real, dev: stat.dev, ino: stat.ino, uid: stat.uid };
9
+ }
10
+ function inspect(parentPath, targetPath, osUserInfo, clean) {
11
+ var parent = identity(parentPath, true), target = identity(targetPath, true);
12
+ function run(root, args) { return git(root, args, null, osUserInfo).trim(); }
13
+ if (parent.path === target.path) throw new Error('The main repository cannot be deleted as a worktree.');
14
+ var common = identity(path.resolve(parent.path, run(parent.path, ['rev-parse', '--git-common-dir'])), true);
15
+ var parentGit = identity(run(parent.path, ['rev-parse', '--absolute-git-dir']), true);
16
+ if (JSON.stringify(common) !== JSON.stringify(parentGit)) throw new Error('Choose the main repository as the parent.');
17
+ if (run(parent.path, ['rev-parse', '--show-toplevel']) !== parent.path || run(target.path, ['rev-parse', '--show-toplevel']) !== target.path) throw new Error('The registered path is not an exact repository root.');
18
+ var targetCommon = identity(path.resolve(target.path, run(target.path, ['rev-parse', '--git-common-dir'])), true);
19
+ var targetGit = identity(run(target.path, ['rev-parse', '--absolute-git-dir']), true);
20
+ if (JSON.stringify(common) !== JSON.stringify(targetCommon) || targetGit.path === common.path || path.dirname(targetGit.path) !== path.join(common.path, 'worktrees')) throw new Error('The target is not a linked worktree of this parent.');
21
+ var marker = identity(path.join(target.path, '.git'), false);
22
+ if (!fs.lstatSync(marker.path).isFile() || fs.readFileSync(marker.path, 'utf8').trim() !== 'gitdir: ' + targetGit.path) throw new Error('Worktree Git binding is invalid.');
23
+ if (fs.readFileSync(path.join(targetGit.path, 'gitdir'), 'utf8').trim() !== marker.path) throw new Error('Worktree Git backlink changed.');
24
+ var records = git(parent.path, ['worktree', 'list', '--porcelain', '-z'], null, osUserInfo).split('\0\0').filter(Boolean);
25
+ var matches = records.filter(function (block) { return block.split('\0')[0] === 'worktree ' + target.path; });
26
+ if (matches.length !== 1) throw new Error('Exact Git worktree membership is unavailable.');
27
+ var fields = matches[0].split('\0');
28
+ if (fields.some(function (field) { return /^prunable(?: |$)|^bare$/.test(field) || clean && /^locked(?: |$)/.test(field); })) throw new Error('The worktree is locked, prunable or unavailable.');
29
+ if (clean && git(target.path, ['status', '--porcelain=v1', '-z', '--untracked-files=all', '--ignored=matching'], null, osUserInfo)) throw new Error('Worktree contains changed, untracked or ignored files. Save or remove them before deleting.');
30
+ return { parent: parent, target: target, common: common, git: targetGit, marker: marker, membership: matches[0], branch: (fields.find(function (field) { return field.indexOf('branch ') === 0; }) || 'branch (detached)').slice(7) };
31
+ }
32
+ function remove(snapshot, osUserInfo) {
33
+ var current = inspect(snapshot.parent.path, snapshot.target.path, osUserInfo, true);
34
+ if (JSON.stringify(current) !== JSON.stringify(snapshot)) throw new Error('Worktree identity changed after review.');
35
+ git(snapshot.parent.path, ['worktree', 'remove', '--', snapshot.target.path], { timeout: 15000 }, osUserInfo);
36
+ if (fs.existsSync(snapshot.target.path) || fs.existsSync(snapshot.git.path)) throw new Error('Git returned without confirming removal. Inspect the worktree before another attempt.');
37
+ return true;
38
+ }
39
+ module.exports = { identity: identity, inspect: inspect, remove: remove };
@@ -0,0 +1,25 @@
1
+ // Worktree display preferences are parent-owned and bound to the Git change set.
2
+ function entry(config, parentSlug, slug) {
3
+ var parent = (config.projects || []).find(function (item) { return item.slug === parentSlug; });
4
+ return parent && parent.worktreePreferences && parent.worktreePreferences[slug];
5
+ }
6
+ function matches(record, targetPath, changeSetId) {
7
+ return !!record && record.path === targetPath && record.changeSetId === changeSetId;
8
+ }
9
+ function icon(config, parentSlug, slug, targetPath, changeSetId) {
10
+ var record = entry(config, parentSlug, slug);
11
+ return matches(record, targetPath, changeSetId) ? record.icon || null : null;
12
+ }
13
+ function blocked(config, parentSlug, slug, targetPath, changeSetId) {
14
+ var record = entry(config, parentSlug, slug);
15
+ return matches(record, targetPath, changeSetId) && (record.excluded || record.lifecycleState);
16
+ }
17
+ // A durable shutdown reservation fences parent mutations even if a display record is replaced.
18
+ function pendingRemoval(config, parentSlug) {
19
+ var parent = (config.projects || []).find(function (item) { return item.slug === parentSlug; });
20
+ return !!parent && Object.values(config.projectLifecycleOperations || {}).some(function (operation) {
21
+ return operation.phase === 'pending' && operation.action === 'remove' &&
22
+ operation.projectKnowledgeId === parent.projectKnowledgeId && operation.slug.indexOf(parentSlug + '--') === 0;
23
+ });
24
+ }
25
+ module.exports = { pendingRemoval: pendingRemoval, entry: entry, matches: matches, icon: icon, blocked: blocked };