clay-server 4.1.0-beta.18 → 4.1.0-beta.19

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.
package/lib/project.js CHANGED
@@ -257,11 +257,20 @@ function createProjectContext(opts) {
257
257
  var serverPort = opts.port || 2633;
258
258
  var serverTls = opts.tls || false;
259
259
  var serverAuthToken = opts.authToken || null;
260
+ var mcpBridgeToken = opts.mcpBridgeToken || crypto.randomBytes(32).toString("hex");
260
261
  var latestVersion = null;
261
262
  var sessionTitleMigrationScheduled = false;
262
263
 
263
264
  // --- YOKE adapters (multi-vendor, lazy init) ---
264
- var _yokeState = yoke.createAdapters({ cwd: cwd, slug: slug, osUsers: osUsers });
265
+ var _yokeState = yoke.createAdapters({
266
+ cwd: cwd,
267
+ slug: slug,
268
+ osUsers: osUsers,
269
+ clayPort: serverPort,
270
+ clayTls: serverTls,
271
+ clayAuthToken: serverAuthToken,
272
+ clayBridgeToken: mcpBridgeToken,
273
+ });
265
274
  var adapters = _yokeState.adapters;
266
275
  // A new project has no remembered vendor. Select the first installed
267
276
  // provider by the shared Claude -> Codex -> Kiro preference order.
@@ -1228,6 +1237,7 @@ function createProjectContext(opts) {
1228
1237
  clayPort: serverPort,
1229
1238
  clayTls: serverTls,
1230
1239
  clayAuthToken: serverAuthToken,
1240
+ clayBridgeToken: mcpBridgeToken,
1231
1241
  getWorkerPermissionRouter: function () {
1232
1242
  return isMate ? null : _sessionPair.workerPermission;
1233
1243
  },
@@ -2670,6 +2680,9 @@ function createProjectContext(opts) {
2670
2680
  handleDisconnection: handleDisconnection,
2671
2681
  handleHTTP: handleHTTP,
2672
2682
  getMcpBridgeHandler: getMcpBridgeHandler,
2683
+ isMcpBridgeAuthenticated: function (req) {
2684
+ return !!(req && req.headers && req.headers["x-clay-bridge-token"] && req.headers["x-clay-bridge-token"] === mcpBridgeToken);
2685
+ },
2673
2686
  getStatus: getStatus,
2674
2687
  getSessionManager: function () { return sm; },
2675
2688
  createDelegatedSession: _delegatedSession.createDelegatedSession,
package/lib/public/app.js CHANGED
@@ -400,6 +400,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
400
400
  issuesRequests: {},
401
401
  issuesViewRequest: null,
402
402
  issuesViewGeneration: 0,
403
+ issuesUpdateAvailable: null,
403
404
  issuesCache: {},
404
405
  issuesEpoch: 0,
405
406
  issuesSearchTimer: null,
@@ -513,6 +513,7 @@ body.mate-dm-active .search-clay-transcript .search-clay-message-user .bubble,
513
513
  @keyframes searchClaySpin { to { transform: rotate(360deg); } }
514
514
 
515
515
  .clayos-session-link,
516
+ .clayos-issue-link,
516
517
  .clayos-log-link {
517
518
  display: inline-flex;
518
519
  align-items: center;
@@ -531,6 +532,7 @@ body.mate-dm-active .search-clay-transcript .search-clay-message-user .bubble,
531
532
  cursor: pointer;
532
533
  }
533
534
  .clayos-session-link::before,
535
+ .clayos-issue-link::before,
534
536
  .clayos-log-link::before {
535
537
  content: "\2197";
536
538
  display: grid;
@@ -542,20 +544,28 @@ body.mate-dm-active .search-clay-transcript .search-clay-message-user .bubble,
542
544
  color: var(--accent);
543
545
  font-size: 12px;
544
546
  }
547
+ .clayos-issue-link::before,
545
548
  .clayos-log-link::before { content: "\2261"; }
546
549
  .clayos-session-link > span,
550
+ .clayos-issue-link > span,
547
551
  .clayos-log-link > span { font-weight: 620; }
548
552
  .clayos-session-link > small,
553
+ .clayos-issue-link > small,
549
554
  .clayos-log-link > small { color: var(--text-muted); font-size: 10px; }
550
555
  .clayos-session-link:hover,
556
+ .clayos-issue-link:hover,
551
557
  .clayos-log-link:hover { border-color: color-mix(in srgb, var(--accent) 52%, var(--border)); background: color-mix(in srgb, var(--accent) 11%, var(--bg-alt)); }
552
558
  .clayos-session-link:focus-visible,
559
+ .clayos-issue-link:focus-visible,
553
560
  .clayos-log-link:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
554
561
  .clayos-session-link.is-loading,
562
+ .clayos-issue-link.is-loading,
555
563
  .clayos-log-link.is-loading { opacity: 0.62; cursor: progress; }
556
564
  .clayos-session-link.is-loading::before,
565
+ .clayos-issue-link.is-loading::before,
557
566
  .clayos-log-link.is-loading::before { animation: searchClaySpin 1.2s linear infinite; }
558
567
  .clayos-session-link.is-error,
568
+ .clayos-issue-link.is-error,
559
569
  .clayos-log-link.is-error { border-color: color-mix(in srgb, var(--warning) 48%, var(--border)); }
560
570
 
561
571
  .search-clay-pending {
@@ -913,3 +923,13 @@ body.mate-dm-active .search-clay-transcript .search-clay-message-user .bubble,
913
923
  font-family: inherit;
914
924
  color: var(--text-muted);
915
925
  }
926
+
927
+ .clayos-issue-link { max-width: min(100%, 30em); box-sizing: border-box; }
928
+ .clayos-issue-link::before { content: "\25CE"; flex: 0 0 17px; }
929
+ .clayos-issue-link[data-status="in_progress"]::before { color: #bd923f; }
930
+ .clayos-issue-link[data-status="resolved"]::before { content: "\2713"; color: var(--brand-green); }
931
+ .clayos-issue-link[data-status="closed"]::before { content: "\00D7"; color: var(--text-dimmer); }
932
+ .clayos-issue-link > span { flex-shrink: 0; }
933
+ .clayos-issue-link > small { min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
934
+ .clayos-log-link { max-width: 100%; box-sizing: border-box; }
935
+ .clayos-log-link > small { min-width: 0; overflow-wrap: anywhere; line-height: 1.35; }
@@ -24,10 +24,10 @@
24
24
  #issues-panel .issues-icon-btn .lucide { width: 15px; }
25
25
  #issues-panel .issues-icon-btn.hidden { display: none; }
26
26
  #issues-panel .issues-icon-btn[aria-pressed="true"] { border-color: var(--accent); color: var(--accent); }
27
- #issues-panel button, #issues-panel input, #issues-panel textarea, #issues-panel select { font: inherit; color: inherit; background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: 8px 10px; }
27
+ #issues-panel :where(button:not(.clayos-issue-link):not(.clayos-log-link), input, textarea, select) { font: inherit; color: inherit; background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: 8px 10px; }
28
28
  #issues-panel button { cursor: pointer; }
29
- #issues-panel button:hover { background: var(--bg-hover, rgba(128,128,128,.12)); }
30
- #issues-panel button:disabled { opacity: .5; cursor: wait; }
29
+ #issues-panel button:not(.clayos-issue-link):not(.clayos-log-link):hover { background: var(--bg-hover, rgba(128,128,128,.12)); }
30
+ #issues-panel button:disabled { opacity: .5; cursor: default; }
31
31
  #issues-panel :focus-visible { outline: 2px solid var(--accent, #b98655); outline-offset: 2px; }
32
32
  .issues-filters { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; padding: 12px 14px; border-bottom: 1px solid var(--border-subtle); background: color-mix(in srgb, var(--bg-alt) 72%, transparent); }
33
33
  .issues-search { flex: 1; min-width: 120px; height: 36px; display: flex; align-items: center; gap: 8px; padding: 0 10px; border: 1px solid var(--border); border-radius: 6px; background: var(--bg); color: var(--text-dimmer); }
@@ -38,35 +38,44 @@
38
38
  .issues-notice { margin: 0; padding: 0 16px; font-size: 12px; color: var(--text-muted); }
39
39
  .issue-row { display: block; width: 100%; text-align: left; margin: 0; padding: 15px 18px 14px !important; border: 0 !important; border-bottom: 1px solid var(--border-subtle) !important; border-left: 2px solid transparent !important; border-radius: 0 !important; background: transparent !important; }
40
40
  .issue-row:hover { background: color-mix(in srgb, var(--accent) 5%, transparent) !important; border-left-color: var(--accent) !important; }
41
- .issue-row-head { display: flex; align-items: center; gap: 8px; margin-bottom: 7px; }
41
+ .issue-row-head { display: flex; align-items: baseline; flex-wrap: wrap; gap: 7px; }
42
42
  .issue-type { padding: 2px 7px; border-radius: 999px; background: rgba(var(--overlay-rgb), .08); color: var(--text-secondary); font-size: 10px; font-weight: 700; text-transform: uppercase; }
43
- .issue-row strong { display: block; font-size: 14px; }
43
+ .issue-row strong { flex: 1; min-width: 0; font-size: 14px; font-weight: 700; letter-spacing: -.01em; overflow-wrap: anywhere; }
44
44
  .issue-row p { margin: 7px 0 0; color: var(--text-secondary); font-size: 12.5px; line-height: 1.55; }
45
- .issue-row small, .issue-detail small { color: var(--text-muted); }
45
+
46
46
  .issue-row small { font-size: 10.5px; font-family: var(--font-mono); }
47
+ .issue-row-meta { display: flex; align-items: baseline; flex-wrap: wrap; gap: 8px; margin-top: 8px; color: var(--text-dimmer); font: 10.5px/1.5 var(--font-mono); }
48
+ .issue-row-meta .issue-status { font-family: var(--font-sans); }
47
49
  .issue-status { display: inline-flex; align-items: center; gap: 6px; font-size: 11px; letter-spacing: .03em; }
48
50
  .issue-status::before { content: ''; width: 7px; height: 7px; border: 1px solid currentColor; border-radius: 50%; }
49
- [data-status="in_progress"] { color: #bd923f; }
50
- [data-status="resolved"] { color: #479b77; }
51
- [data-status="closed"] { color: var(--text-muted); }
51
+ .issue-status[data-status="in_progress"] { color: #bd923f; }
52
+ .issue-status[data-status="resolved"] { color: #479b77; }
53
+ .issue-status[data-status="closed"] { color: var(--text-muted); }
52
54
  .issue-detail { max-width: 760px; margin: 0 auto; padding: 32px 28px 52px; }
53
- .issue-detail h2 { margin: 10px 0; font-family: var(--font-display); font-size: clamp(24px, 2.8vw, 34px); line-height: 1.12; }
54
- .issue-detail > p { font-size: 14px; line-height: 1.6; }
55
- .issue-detail > small { font-size: 11px; font-family: var(--font-mono); }
56
- .issue-detail .markdown-body { margin: 24px 0; }
57
- .issue-detail .markdown-body { font-size: 14px; line-height: 1.75; }
58
- .issue-detail section { margin-top: 24px; overflow-wrap: anywhere; }
59
- .issue-detail h3 { font-size: 12px; color: var(--text-muted); }
60
- #issues-panel .issue-discussion { border-top: 1px solid var(--border-subtle); }
61
- #issues-panel .issue-comment { padding: 12px 0; border-bottom: 1px solid var(--border-subtle); }
62
- #issues-panel .issue-comment-meta { color: var(--text-muted); font: 10.5px/1.4 var(--font-mono); }
63
- #issues-panel .issue-comment p { margin: 6px 0 0; font-size: 13px; line-height: 1.55; white-space: pre-wrap; overflow-wrap: anywhere; }
64
- #issues-panel .issue-comment small { display: block; margin-top: 7px; color: var(--text-dimmer); font-size: 11px; }
65
- #issues-panel .issue-discussion-empty { color: var(--text-muted); font-size: 13px; }
66
- #issues-panel #issue-comment-form { margin-top: 16px; }
67
- #issues-panel #issue-comment-form label { display: block; margin-bottom: 6px; color: var(--text-muted); font-size: 11px; }
68
- #issues-panel #issue-comment-input { width: 100%; box-sizing: border-box; min-height: 76px; resize: vertical; font-size: 13px; line-height: 1.55; }
69
- #issues-panel .issue-comment-status { min-height: 1.4em; margin: 6px 0; color: var(--text-muted); font-size: 11px; }
55
+ .issue-doc-chips { display: flex; align-items: center; flex-wrap: wrap; gap: 7px; margin-bottom: 10px; }
56
+ .issue-detail .issue-doc-title { margin: 0 0 10px; font-family: var(--font-display); font-size: clamp(24px, 2.8vw, 34px); letter-spacing: -.025em; line-height: 1.12; overflow-wrap: anywhere; }
57
+ .issue-doc-summary { color: var(--text-secondary); font-size: 14px; line-height: 1.6; }
58
+ .issue-detail .issue-doc-summary > p { margin: 0 0 13px; }
59
+ .issue-byline { margin: 0 0 18px; padding-bottom: 18px; border-bottom: 2px solid var(--border-subtle); color: var(--text-dimmer); font: 11px/1.6 var(--font-mono); overflow-wrap: anywhere; }
60
+ .issue-doc-body { margin: 24px 0 0; font-size: 14px; line-height: 1.75; overflow-wrap: anywhere; }
61
+ .issue-detail section { margin-top: 26px; overflow-wrap: anywhere; }
62
+ .issue-detail section h3 { margin: 0 0 12px; font-family: var(--font-display); font-size: 15px; color: var(--text); }
63
+ .issue-evidence { padding-top: 18px; border-top: 1px solid var(--border-subtle); font-size: 13px; line-height: 1.65; }
64
+ #issues-panel .issue-discussion { margin-top: 30px; padding-top: 18px; border-top: 1px solid var(--border-subtle); }
65
+ .issue-discussion h3 > span { margin-left: 4px; font: 11px var(--font-mono); color: var(--text-dimmer); }
66
+ #issues-panel .issue-comment { padding: 10px 12px; margin-bottom: 8px; border: 1px solid var(--border-subtle); border-radius: 9px; background: var(--bg-alt); }
67
+ .issue-comment-meta { margin-bottom: 5px; color: var(--text-dimmer); font: 10.5px/1.4 var(--font-mono); }
68
+ .issue-comment .markdown-body { font-size: 13px; line-height: 1.55; overflow-wrap: anywhere; }
69
+ .issue-comment .markdown-body > :first-child { margin-top: 0; }
70
+ .issue-comment .markdown-body > :last-child { margin-bottom: 0; }
71
+ .issue-comment-review { margin-top: 9px; padding: 8px 10px; border-left: 2px solid var(--accent); background: rgba(var(--overlay-rgb), .04); border-radius: 0 7px 7px 0; }
72
+ .issue-comment-review > span { display: block; margin-bottom: 5px; color: var(--text-dimmer); font: 10.5px var(--font-mono); }
73
+ .issue-discussion-empty { margin: 0; color: var(--text-dimmer); font-size: 12px; }
74
+ #issue-comment-form { display: grid; gap: 8px; margin-top: 18px; }
75
+ #issue-comment-form label { color: var(--text-secondary); font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .06em; }
76
+ #issues-panel #issue-comment-input { width: 100%; box-sizing: border-box; min-height: 80px; resize: vertical; font-size: 13px; line-height: 1.55; border-radius: 8px; padding: 10px; background: var(--bg-alt); }
77
+ #issue-comment-form .issue-comment-status { min-height: 16px; margin: 0; color: var(--text-dimmer); font-size: 11px; }
78
+ .issue-comment-actions { display: flex; justify-content: flex-end; }
70
79
  #issues-panel .issues-delete-dialog { position: absolute; inset: 0; z-index: 2; display: grid; place-items: center; padding: 24px; background: rgba(0, 0, 0, .38); }
71
80
  #issues-panel .issues-delete-dialog.hidden { display: none; }
72
81
  #issues-panel .issues-delete-card { width: min(360px, 100%); box-sizing: border-box; padding: 22px; border: 1px solid var(--border); border-radius: 10px; background: var(--bg); box-shadow: 0 14px 40px rgba(0, 0, 0, .24); }
@@ -91,8 +100,8 @@
91
100
  .issues-content > h2 { padding: 0 28px; }
92
101
  .issues-content > h2 { margin: 0 0 10px; font-family: var(--font-display); font-size: 14px; line-height: 1.2; }
93
102
  .issues-history-note { margin: 0; padding: 14px 18px; border-bottom: 1px solid var(--border-subtle); color: var(--text-dimmer); font-size: 11px; font-family: var(--font-mono); }
94
- .clayos-issue-link { display: inline-flex; max-width: 100%; border: 1px solid var(--border); border-radius: 5px; background: var(--bg); color: inherit; font: inherit; font-size: .85em; padding: 2px 7px; cursor: pointer; vertical-align: baseline; overflow-wrap: anywhere; }
95
- @media (max-width: 1023px) { #issues-panel, #issues-panel.issues-wide { position: fixed; inset: 0; z-index: 300; width: 100%; max-width: none; min-width: 0; height: 100%; margin: 0; border: 0; border-radius: 0; padding-top: var(--safe-top, 0px); padding-bottom: var(--safe-bottom, 0px); box-sizing: border-box; } #issues-panel button { min-height: 36px; } }
103
+
104
+ @media (max-width: 1023px) { #issues-panel, #issues-panel.issues-wide { position: fixed; inset: 0; z-index: 300; width: 100%; max-width: none; min-width: 0; height: 100%; margin: 0; border: 0; border-radius: 0; padding-top: var(--safe-top, 0px); padding-bottom: var(--safe-bottom, 0px); box-sizing: border-box; } #issues-panel button:not(.clayos-issue-link):not(.clayos-log-link) { min-height: 36px; } }
96
105
  @media (max-width: 1023px) { #issues-panel [data-issues-wide], #issues-panel [data-issues-full] { display: none; } }
97
106
  @media (max-width: 390px) {
98
107
  #issues-panel header { gap: 4px; padding: 8px; }
@@ -100,7 +109,7 @@
100
109
  #issues-panel [data-issues-wide], #issues-panel [data-issues-full] { display: none; }
101
110
  .issues-filters { display: grid; grid-template-columns: 1fr 1fr; padding: 8px; }
102
111
  .issues-filters .issues-search { grid-column: 1 / -1; width: 100%; box-sizing: border-box; }
103
- .issues-content { padding: 10px; }
112
+ .issues-content { padding: 0; }
104
113
  .issue-row { padding: 12px !important; }
105
114
  .issue-detail h2 { font-size: 20px; }
106
115
  }
@@ -113,3 +122,27 @@
113
122
  .issues-content > h2 { padding-right: 18px; padding-left: 18px; }
114
123
  .issue-detail { padding: 24px 18px 38px; }
115
124
  }
125
+
126
+ #issues-panel { font-family: var(--font-sans); font-size: 13px; }
127
+ #issues-panel .issues-title { font-size: 13px; }
128
+ .issues-notice:empty { display: none; }
129
+ .issues-notice:not(:empty) { padding: 10px 16px; border-bottom: 1px solid var(--border-subtle); line-height: 1.5; }
130
+ .issues-inline-update { display: inline-flex; align-items: center; gap: 7px; width: 100%; box-sizing: border-box; margin: 0 0 8px; padding: 7px 9px; border: 1px solid color-mix(in srgb, var(--accent) 35%, var(--border)); border-radius: 7px; background: color-mix(in srgb, var(--accent) 7%, transparent); color: var(--text-secondary); font-size: 11px; }
131
+ .issues-inline-update button { margin-left: auto; padding: 3px 8px !important; border-color: var(--accent) !important; color: var(--accent) !important; font-size: 11px !important; font-weight: 700; }
132
+ .issue-type { letter-spacing: .03em; flex-shrink: 0; }
133
+ #issues-panel .issue-actions { margin: 0 0 24px; gap: 6px; }
134
+ #issues-panel .issue-actions button, #issues-panel .issue-comment-actions button { display: inline-flex; align-items: center; justify-content: center; gap: 6px; padding: 7px 10px; border-radius: 7px; font-size: 11px; font-weight: 600; background: var(--bg-alt); color: var(--text-secondary); }
135
+ #issues-panel .issue-actions .lucide { width: 13px; height: 13px; }
136
+ #issues-panel button.issue-primary { border-color: var(--accent); background: var(--accent); color: #fff; }
137
+ #issues-panel button.issue-primary:hover { background: var(--accent-hover, var(--accent)); }
138
+ #issues-panel .issue-actions button.issue-danger { margin-left: auto; color: var(--danger); background: transparent; }
139
+ #issues-panel #issue-form .issue-actions { margin-top: 18px; }
140
+ #issues-panel.panel-fullscreen .issue-detail { padding: clamp(28px, 5vw, 64px); }
141
+ .issues-load-more { padding: 16px; text-align: center; }
142
+ #issues-panel .issues-filters select { height: 36px; max-width: 42%; }
143
+ @media (max-width: 480px) {
144
+ .issues-filters .issues-search { flex-basis: 100%; }
145
+ #issues-panel .issues-filters select { flex: 1; max-width: none; }
146
+ .issue-row-head strong { flex-basis: 100%; }
147
+ #issues-panel .issue-actions button.issue-danger { margin-left: 0; }
148
+ }
@@ -194,6 +194,16 @@ export function stopUrgentBlink() {
194
194
  }
195
195
 
196
196
  export function setActivity(text) {
197
+ var thinkingState = store.get('thinkingState');
198
+ if (text === "thinking" && thinkingState && thinkingState.active) {
199
+ // Live Thinking already owns the tail progress UI. Compaction and tool
200
+ // activity intentionally continue through so their distinct states show.
201
+ if (getActivityEl()) {
202
+ getActivityEl().remove();
203
+ setActivityEl(null);
204
+ }
205
+ return;
206
+ }
197
207
  if (text) {
198
208
  if (!getActivityEl()) {
199
209
  var _actEl = document.createElement("div");
@@ -1054,6 +1054,9 @@ export function processMessage(msg) {
1054
1054
  case "thinking_start":
1055
1055
  removeMatePreThinking();
1056
1056
  startThinking();
1057
+ // The live Thinking entry is the progress affordance while reasoning
1058
+ // is streaming; avoid a second bouncing indicator at the same tail.
1059
+ setActivity(null);
1057
1060
  break;
1058
1061
 
1059
1062
  case "thinking_delta":
@@ -145,6 +145,26 @@ export function addToMessages(element) {
145
145
  var messagesEl = getMessagesEl();
146
146
  if (prependAnchor) messagesEl.insertBefore(element, prependAnchor);
147
147
  else messagesEl.appendChild(element);
148
+ keepActiveThinkingAtTail();
149
+ }
150
+
151
+ // Thinking is a single accumulating record for the active turn. Other live
152
+ // transcript entries may arrive between reasoning segments, so keep only the
153
+ // currently active record after the tail activity indicator.
154
+ export function keepActiveThinkingAtTail() {
155
+ if (prependAnchor || store.get('replayingHistory')) return;
156
+ var thinkingState = store.get('thinkingState');
157
+ var thinkingEl = thinkingState && thinkingState.active && thinkingState.el;
158
+ var messagesEl = getMessagesEl();
159
+ if (!thinkingEl || !messagesEl || thinkingEl.parentNode !== messagesEl) return;
160
+ if (activityEl && activityEl.parentNode === messagesEl) {
161
+ if (activityEl.nextSibling !== thinkingEl || messagesEl.lastElementChild !== thinkingEl) {
162
+ messagesEl.appendChild(activityEl);
163
+ messagesEl.appendChild(thinkingEl);
164
+ }
165
+ } else if (messagesEl.lastElementChild !== thinkingEl) {
166
+ messagesEl.appendChild(thinkingEl);
167
+ }
148
168
  }
149
169
 
150
170
  export function scrollToBottom() {
@@ -1,5 +1,6 @@
1
1
  import { store } from './store.js';
2
2
  import { getWs } from './ws-ref.js';
3
+ import { forwardPaneIssueReference } from './pane-bridge.js';
3
4
  function accountKey(state) { return state.isMultiUserMode ? 'user:' + String(state.myUserId || '') : 'single'; }
4
5
  function requestContext(state) { return { accountKey: accountKey(state), projectSlug: state.currentSlug || null, epoch: state.issuesEpoch || 0 }; }
5
6
  function sameContext(request, state) {
@@ -42,12 +43,17 @@ function createIssueLink(ref, labelText) {
42
43
  button.appendChild(meta);
43
44
  }
44
45
  var cache = store.get('issuesCache') || {};
45
- if (cache[ref]) { button.textContent = cache[ref].status.replace(/_/g, ' ') + ' · ' + cache[ref].title; button.dataset.status = cache[ref].status; }
46
+ if (cache[ref]) updateIssueLink(button, cache[ref]);
46
47
  setTimeout(function () { resolve(ref); }, 0);
47
48
  return button;
48
49
  }
49
50
 
50
51
  export function enhanceClayIssueLinks(root) {
52
+ root.querySelectorAll('a').forEach(function (anchor) {
53
+ var ref = anchor.getAttribute('data-clay-record-ref') || anchor.getAttribute('href');
54
+ if (anchor.closest('pre, code, button') || !isExactClayIssueReference(ref)) return;
55
+ anchor.replaceWith(createIssueLink(ref.trim(), anchor.textContent.trim()));
56
+ });
51
57
  var codeNodes = root.querySelectorAll("code");
52
58
  for (var codeIndex = 0; codeIndex < codeNodes.length; codeIndex++) {
53
59
  var code = codeNodes[codeIndex];
@@ -80,6 +86,20 @@ export function enhanceClayIssueLinks(root) {
80
86
  }
81
87
  }
82
88
 
89
+ export function updateIssueLink(button, target) {
90
+ var status = target.status || 'open';
91
+ var title = target.title || 'Issue';
92
+ button.replaceChildren();
93
+ var label = document.createElement('span');
94
+ label.textContent = 'Issue';
95
+ var meta = document.createElement('small');
96
+ meta.textContent = title;
97
+ button.append(label, meta);
98
+ button.dataset.status = status;
99
+ button.title = title + ' · ' + status.replace(/_/g, ' ');
100
+ button.setAttribute('aria-label', 'Open Project Issue: ' + button.title);
101
+ }
102
+
83
103
  function resolve(ref) {
84
104
  var ws = getWs();
85
105
  var requests = store.get('issueLinkRequests') || {};
@@ -101,7 +121,7 @@ export function handleIssueReference(msg) {
101
121
  }
102
122
  document.querySelectorAll('[data-issue-ref]').forEach(function (button) {
103
123
  if (button.dataset.issueRef !== msg.ref) return;
104
- if (msg.target) { button.textContent = msg.target.status.replace(/_/g, ' ') + ' · ' + msg.target.title; button.dataset.status = msg.target.status; }
124
+ if (msg.target) updateIssueLink(button, msg.target);
105
125
  else button.title = msg.error || 'Issue unavailable';
106
126
  });
107
127
  if (store.get('issueLinkOpenRef') === msg.ref) {
@@ -111,6 +131,7 @@ export function handleIssueReference(msg) {
111
131
  return true;
112
132
  }
113
133
  function navigate(target) {
134
+ if (forwardPaneIssueReference(target.ref, target.projectSlug)) return;
114
135
  if (target.projectSlug !== store.get('currentSlug')) { window.location.assign('/p/' + encodeURIComponent(target.projectSlug) + '/?issue=' + encodeURIComponent(target.ref)); return; }
115
136
  import('./issues.js').then(function (module) { module.openIssues(target.ref); });
116
137
  }
@@ -1,4 +1,9 @@
1
- import { enhanceClayIssueLinks } from './clay-issue-links.js';
1
+ import { enhanceClayIssueLinks, isExactClayIssueReference } from './clay-issue-links.js';
2
+
3
+ export function renderClayRecordLink(href, text) {
4
+ if (!isExactClayLogReference(href) && !isExactClayIssueReference(href)) return null;
5
+ return '<a href="#" data-clay-record-ref="' + href.trim() + '">' + text + '</a>';
6
+ }
2
7
  export function parseClayLogReferences(text) {
3
8
  var pattern = /\[clayos\/(log:[A-Za-z0-9_-]{24})(?:\s+[\u2014-]\s+([^\]\n]{1,80}))?\]|(^|[^A-Za-z0-9_/:])(log:[A-Za-z0-9_-]{24})(?![A-Za-z0-9_-])/g;
4
9
  var results = [];
@@ -39,6 +44,11 @@ function createLogLink(ref, labelText) {
39
44
 
40
45
  export function enhanceClayLogLinks(root) {
41
46
  enhanceClayIssueLinks(root);
47
+ root.querySelectorAll('a').forEach(function (anchor) {
48
+ var ref = anchor.getAttribute('data-clay-record-ref') || anchor.getAttribute('href');
49
+ if (anchor.closest('pre, code, button') || !isExactClayLogReference(ref)) return;
50
+ anchor.replaceWith(createLogLink(ref.trim(), anchor.textContent.trim()));
51
+ });
42
52
  var codeNodes = root.querySelectorAll("code");
43
53
  for (var codeIndex = 0; codeIndex < codeNodes.length; codeIndex++) {
44
54
  var code = codeNodes[codeIndex];
@@ -1,24 +1,50 @@
1
1
  import { escapeHtml } from './utils.js';
2
2
  import { renderMarkdown } from './markdown.js';
3
+ import { authorLine, relativeTime } from './project-logs-render.js';
3
4
  export function statusLabel(status) { return { open: 'Open', in_progress: 'In progress', resolved: 'Resolved', closed: 'Closed' }[status] || 'Issue'; }
4
5
  function esc(value) { return escapeHtml(String(value || '')); }
5
6
  function options(values, selected) { return values.map(function (value) { return '<option value="' + value + '"' + (value === selected ? ' selected' : '') + '>' + esc(statusLabel(value) === 'Issue' ? value.replace(/_/g, ' ') : statusLabel(value)) + '</option>'; }).join(''); }
7
+ function label(value) { return value ? value.charAt(0).toUpperCase() + value.slice(1).replace(/_/g, ' ') : ''; }
8
+ function statusChip(status) { return '<span class="issue-status" data-status="' + esc(status) + '">' + statusLabel(status) + '</span>'; }
9
+ function priorityChip(priority) { return priority && priority !== 'normal' ? '<span class="project-log-priority" data-priority="' + esc(priority) + '">' + esc(label(priority)) + '</span>' : ''; }
10
+ function action(attribute, icon, text, extra) { return '<button type="button" ' + attribute + (extra || '') + '><i data-lucide="' + icon + '"></i>' + text + '</button>'; }
11
+ function prose(text, className) { return '<div class="markdown-body ' + (className || '') + '">' + renderMarkdown(text || '') + '</div>'; }
6
12
  export function renderIssueList(result) {
7
13
  var filtered = !!(result.query || result.status || result.type);
8
14
  if (!result.issues.length) return '<div class="issues-empty"><i data-lucide="circle-dot"></i><h2>' + (filtered ? 'No matching issues' : 'No issues yet') + '</h2><p>' + (filtered ? 'Try a different search or filter.' : 'Manage issues through Driver chat.') + '</p></div>';
9
15
  return result.issues.map(function (entry) {
10
- return '<button class="issue-row" data-open-issue="' + esc(entry.ref) + '"><div class="issue-row-head"><span class="issue-type">' + esc(entry.type) + '</span><span class="issue-status" data-status="' + entry.status + '">' + statusLabel(entry.status) + '</span></div><strong>' + esc(entry.title) + '</strong><p>' + esc(entry.summary) + '</p><small>' + esc(entry.priority) + ' · revision ' + entry.revision + '</small></button>';
11
- }).join('') + (result.nextCursor ? '<button data-issues-more="' + esc(result.nextCursor) + '">Load more</button>' : '');
16
+ var meta = [authorLine(entry), relativeTime(entry.updatedAt || entry.createdAt)];
17
+ if (entry.revision > 1) meta.push('v' + entry.revision);
18
+ if (entry.commentCount) meta.push(entry.commentCount + (entry.commentCount === 1 ? ' comment' : ' comments'));
19
+ return '<button type="button" class="issue-row" data-open-issue="' + esc(entry.ref) + '">' +
20
+ '<div class="issue-row-head"><span class="issue-type">' + esc(entry.type) + '</span>' + priorityChip(entry.priority) + '<strong>' + esc(entry.title) + '</strong></div>' +
21
+ (entry.summary ? '<p>' + esc(entry.summary) + '</p>' : '') +
22
+ '<div class="issue-row-meta">' + statusChip(entry.status) + '<span>' + esc(meta.filter(Boolean).join(' · ')) + '</span></div></button>';
23
+ }).join('') + (result.nextCursor ? '<div class="issues-load-more"><button data-issues-more="' + esc(result.nextCursor) + '">Load more</button></div>' : '');
24
+ }
25
+ function renderComment(comment) {
26
+ var actor = comment.author || {};
27
+ var review = comment.review;
28
+ return '<article class="issue-comment"><div class="issue-comment-meta">' + esc(actor.displayName || actor.userId || 'Project member') + ' · ' + esc(new Date(comment.at).toLocaleString()) + '</div>' +
29
+ prose(comment.body, 'issue-comment-body') + (review ? '<div class="issue-comment-review"><span>' + esc(label(review.action)) + '</span>' + prose(review.response) + '</div>' : '') + '</article>';
12
30
  }
13
31
  export function renderIssueDetail(entry) {
14
- var author = entry.updatedBy || entry.author || {};
15
- var comments = (entry.comments || []).map(function (comment) { var commentAuthor = comment.author || {}; var review = comment.review; return '<article class="issue-comment"><div class="issue-comment-meta">' + esc(commentAuthor.displayName || commentAuthor.userId || 'Project member') + ' · ' + esc(new Date(comment.at).toLocaleString()) + '</div><p>' + esc(comment.body) + '</p>' + (review ? '<small>' + esc(review.action) + ' · ' + esc(review.response) + '</small>' : '') + '</article>'; }).join('');
16
- return '<div class="issue-detail"><span class="issue-status" data-status="' + entry.status + '">' + statusLabel(entry.status) + '</span><h2>' + esc(entry.title) + '</h2><p>' + esc(entry.summary) + '</p><small>' + esc(entry.type) + ' · ' + esc(entry.priority) + ' · revision ' + entry.revision + ' · ' + esc(author.displayName || author.userId || 'Local user') + '</small><div class="markdown-body">' + renderMarkdown(entry.body || '') + '</div>' +
17
- (entry.verifiedCommit ? '<section><h3>Resolution</h3><p>' + esc(entry.resolutionSummary) + '</p><code>' + esc(entry.verifiedCommit.sha) + '</code><p>' + esc(entry.verifiedCommit.subject) + '</p></section>' : '') +
18
- (entry.closeReason ? '<section class="issue-evidence"><h3>Closure</h3><p>' + esc(entry.closeReason.replace(/_/g, ' ')) + '</p></section>' : '') +
19
- '<div class="issue-actions"><button data-issue-edit>Edit</button><button data-issue-work' + (entry.status === 'resolved' || entry.status === 'closed' ? ' disabled title="Reopen this issue to start work"' : '') + '>Start work</button><button data-issue-history>History</button><button data-issue-copy>Copy reference</button>' + (entry.canDelete ? '<button data-issue-delete>Delete issue</button>' : '') + '</div>' +
20
- ((entry.linkedWorkSessions || []).length ? '<section><h3>Linked work sessions</h3>' + entry.linkedWorkSessions.map(function (link) { return '<button data-issue-session="' + esc(link.sessionId) + '">Open Driver session</button>'; }).join('') + '</section>' : '') +
21
- ((entry.resolutionHistory || []).length ? '<section class="issue-evidence"><h3>Resolution history</h3>' + entry.resolutionHistory.map(function (item) { return '<p>' + esc(item.resolutionSummary) + ' <code>' + esc(item.verifiedCommit && item.verifiedCommit.sha) + '</code></p>'; }).join('') + '</section>' : '') + '<section class="issue-discussion"><h3>Discussion' + (comments ? ' (' + entry.comments.length + ')' : '') + '</h3>' + (comments || '<p class="issue-discussion-empty">No comments yet.</p>') + '<form id="issue-comment-form"><label for="issue-comment-input">Add a comment</label><textarea id="issue-comment-input" rows="3" required placeholder="Add context, a correction, or a question."></textarea><p class="issue-comment-status" role="status"></p><button type="submit">Comment</button></form></section></div>';
32
+ var comments = (entry.comments || []).map(renderComment).join('');
33
+ var closed = entry.status === 'resolved' || entry.status === 'closed';
34
+ var time = entry.updatedAt || entry.createdAt;
35
+ var byline = [authorLine(entry), time ? new Date(time).toLocaleString() : '', 'v' + entry.revision].filter(Boolean).join(' · ');
36
+ return '<article class="issue-detail"><div class="issue-doc-chips"><span class="issue-type">' + esc(entry.type) + '</span>' + priorityChip(entry.priority) + statusChip(entry.status) + '</div>' +
37
+ '<h2 class="issue-doc-title">' + esc(entry.title) + '</h2>' + prose(entry.summary, 'issue-doc-summary') + '<div class="issue-byline">' + esc(byline) + '</div>' +
38
+ '<div class="issue-actions">' + action('data-issue-work', 'play', 'Start work', ' class="issue-primary"' + (closed ? ' disabled title="Reopen this issue to start work"' : '')) +
39
+ action('data-issue-edit', 'pencil', 'Edit') + action('data-issue-history', 'history', 'History') + action('data-issue-copy', 'link', 'Copy reference') +
40
+ (entry.canDelete ? action('data-issue-delete', 'trash-2', 'Delete', ' class="issue-danger"') : '') + '</div>' +
41
+ prose(entry.body, 'issue-doc-body') +
42
+ (entry.verifiedCommit ? '<section class="issue-evidence"><h3>Resolution</h3>' + prose(entry.resolutionSummary) + '<code>' + esc(entry.verifiedCommit.sha) + '</code><p>' + esc(entry.verifiedCommit.subject) + '</p></section>' : '') +
43
+ (entry.closeReason ? '<section class="issue-evidence"><h3>Closure</h3><p>' + esc(label(entry.closeReason)) + '</p></section>' : '') +
44
+ ((entry.linkedWorkSessions || []).length ? '<section class="issue-evidence"><h3>Linked work sessions</h3>' + entry.linkedWorkSessions.map(function (link) { return action('data-issue-session="' + esc(link.sessionId) + '"', 'messages-square', 'Open Driver session'); }).join('') + '</section>' : '') +
45
+ ((entry.resolutionHistory || []).length ? '<section class="issue-evidence"><h3>Resolution history</h3>' + entry.resolutionHistory.map(function (item) { return prose(item.resolutionSummary) + '<code>' + esc(item.verifiedCommit && item.verifiedCommit.sha) + '</code>'; }).join('') + '</section>' : '') +
46
+ '<section class="issue-discussion"><h3>Discussion' + (comments ? ' <span>' + entry.comments.length + '</span>' : '') + '</h3>' + (comments || '<p class="issue-discussion-empty">No comments yet.</p>') +
47
+ '<form id="issue-comment-form"><label for="issue-comment-input">Add a comment</label><textarea id="issue-comment-input" rows="3" required placeholder="Add context, a correction, or a question."></textarea><p class="issue-comment-status" role="status"></p><div class="issue-comment-actions"><button type="submit" class="issue-primary">Comment</button></div></form></section></article>';
22
48
  }
23
49
  export function renderIssueForm(entry) {
24
50
  var item = entry || {};
@@ -3,16 +3,74 @@ import { getWs } from './ws-ref.js';
3
3
  import { showToast } from './utils.js';
4
4
  import { claimRightWorkbench, registerRightWorkbench, releaseRightWorkbench } from './right-workbench.js';
5
5
  import { refreshIcons } from './icons.js';
6
- import { renderIssueList, renderIssueDetail, renderIssueForm, renderIssueHistory, statusLabel } from './issues-render.js';
6
+ import { enhanceClayLogLinks } from './clay-log-links.js';
7
+ import { updateIssueLink } from './clay-issue-links.js';
8
+ import { renderIssueList, renderIssueDetail, renderIssueForm, renderIssueHistory } from './issues-render.js';
7
9
  function panel() { return document.getElementById('issues-panel'); }
8
10
  function content() { return panel().querySelector('.issues-content'); }
11
+ function clearUpdateNotice() {
12
+ store.set({ issuesUpdateAvailable: null });
13
+ var action = content() && content().querySelector('[data-issue-update-notice]');
14
+ if (action) action.remove();
15
+ }
16
+ function renderContextualUpdate() {
17
+ var update = store.get('issuesUpdateAvailable');
18
+ var issueContent = content();
19
+ var actions = issueContent && (issueContent.querySelector('.issue-detail .issue-actions') || issueContent.querySelector('#issue-form .issue-actions'));
20
+ if (!actions || !update) return;
21
+ var existing = actions.querySelector('[data-issue-update-notice]');
22
+ if (existing) existing.remove();
23
+ var notice = document.createElement('span');
24
+ notice.className = 'issues-inline-update';
25
+ notice.dataset.issueUpdateNotice = 'true';
26
+ notice.appendChild(document.createTextNode(store.get('issuesView') === 'edit' ? 'Updated remotely · Finish editing to refresh' : 'Updated remotely'));
27
+ var refresh = document.createElement('button');
28
+ refresh.type = 'button';
29
+ refresh.dataset.issueRefresh = update.ref;
30
+ refresh.textContent = 'Refresh';
31
+ refresh.disabled = store.get('issuesView') === 'edit';
32
+ if (refresh.disabled) refresh.title = 'Finish editing to refresh';
33
+ notice.appendChild(refresh);
34
+ actions.insertBefore(notice, actions.firstChild);
35
+ }
36
+ function captureDraft() {
37
+ var state = store.snap();
38
+ if (state.issuesView !== 'detail' && state.issuesView !== 'edit') return null;
39
+ var draft = { ref: entryRef(), view: state.issuesView, comment: null };
40
+ var comment = panel() && panel().querySelector('#issue-comment-input');
41
+ if (comment) draft.comment = comment.value;
42
+ return draft;
43
+ }
44
+ function restoreCommentDraft(draft) {
45
+ if (!draft || draft.comment === null) return;
46
+ var input = panel().querySelector('#issue-comment-input');
47
+ if (input) input.value = draft.comment;
48
+ }
49
+ function canRestoreCommentDraft(draft, ref) {
50
+ return !!draft && draft.ref === ref;
51
+ }
52
+ function hasNewerDraft(item) {
53
+ return (item.intent === 'detail' || item.intent === 'refresh') && item.viewGeneration !== (store.get('issuesViewGeneration') || 0);
54
+ }
55
+ function noteIssueUpdate(ref, revision) {
56
+ var current = store.get('issuesUpdateAvailable');
57
+ if (!current || current.ref !== ref || Number(revision || 0) >= Number(current.revision || 0)) {
58
+ store.set({ issuesUpdateAvailable: { ref: ref, revision: Number(revision || 0) } });
59
+ }
60
+ renderContextualUpdate();
61
+ }
62
+ function refreshSelectedIssue(ref) {
63
+ var selected = store.get('issuesSelected');
64
+ if (!selected || selected.ref !== ref || store.get('issuesView') === 'edit') return;
65
+ store.set({ issuesViewRequest: request('issue_read', { ref: ref }, 'refresh') });
66
+ }
9
67
  function bumpViewGeneration() { store.set({ issuesViewGeneration: (store.get('issuesViewGeneration') || 0) + 1 }); }
10
68
  function request(action, args, intent) {
11
69
  var ws = getWs();
12
70
  if (!ws || ws.readyState !== 1) { showToast('Reconnect to use Issues', 'warn'); return; }
13
71
  var id = 'issues-' + crypto.randomUUID();
14
72
  var pending = Object.assign({}, store.get('issuesRequests') || {});
15
- pending[id] = { action: action, intent: intent, slug: store.get('currentSlug'), sourceRef: entryRef(), view: store.get('issuesView'), viewGeneration: store.get('issuesViewGeneration') || 0 };
73
+ pending[id] = { action: action, intent: intent, slug: store.get('currentSlug'), sourceRef: entryRef(), view: store.get('issuesView'), viewGeneration: store.get('issuesViewGeneration') || 0, draft: captureDraft() };
16
74
  store.set({ issuesRequests: pending });
17
75
  ws.send(JSON.stringify({ type: action, args: args || {}, requestId: id }));
18
76
  return id;
@@ -39,7 +97,9 @@ function detail(entry) {
39
97
  panel().querySelector('.issues-filters').hidden = true;
40
98
  panel().querySelector('[data-issues-back]').classList.remove('hidden');
41
99
  content().innerHTML = renderIssueDetail(entry);
100
+ enhanceClayLogLinks(content());
42
101
  refreshIcons();
102
+ renderContextualUpdate();
43
103
  }
44
104
  function entryRef() {
45
105
  var entry = store.get('issuesSelected');
@@ -87,6 +147,7 @@ export function closeIssues() {
87
147
  export function openIssues(ref) {
88
148
  if (!panel() || store.get('isMate')) return;
89
149
  panel().querySelector('.issues-notice').textContent = '';
150
+ clearUpdateNotice();
90
151
  bumpViewGeneration();
91
152
  claimRightWorkbench("issues");
92
153
  panel().classList.remove('hidden');
@@ -102,7 +163,7 @@ export function initIssues() {
102
163
  registerRightWorkbench("issues", closeIssues);
103
164
  var el = document.createElement('section');
104
165
  el.id = 'issues-panel'; el.className = 'hidden'; el.setAttribute('aria-label', 'Project Issues');
105
- el.innerHTML = '<header class="issues-topbar"><button class="issues-icon-btn" data-issues-back aria-label="Back to issues" title="Back to issue list"><i data-lucide="arrow-left"></i></button><span class="issues-title"><i data-lucide="circle-dot"></i>Issues</span><span class="issues-subtitle">Manage issues through Driver chat.</span><div class="issues-window-actions"><button class="issues-icon-btn" data-issues-wide aria-label="Widen Issues" title="Widen Issues" aria-pressed="false"><i data-lucide="chevrons-left-right"></i></button><button class="issues-icon-btn" data-issues-full aria-label="Toggle Issues fullscreen" title="Toggle fullscreen" aria-pressed="false"><i data-lucide="maximize-2"></i></button><button class="issues-icon-btn" data-issues-close aria-label="Close Issues" title="Close Issues"><i data-lucide="x"></i></button></div></header><div class="issues-filters"><label class="issues-search"><i data-lucide="search"></i><input name="query" type="search" placeholder="Search issues" aria-label="Search issues"></label><select name="filter-status" aria-label="Filter status"><option value="">All statuses</option><option value="open">Open</option><option value="in_progress">In progress</option><option value="resolved">Resolved</option><option value="closed">Closed</option></select><select name="filter-type" aria-label="Filter type"><option value="">All types</option><option>bug</option><option>feature</option><option>plan</option></select></div><p class="issues-notice" role="status"></p><main class="issues-content" role="region" aria-live="polite"></main><div class="issues-delete-dialog hidden" role="dialog" aria-modal="true" aria-labelledby="issues-delete-heading"><div class="issues-delete-card"><h2 id="issues-delete-heading">Delete this issue?</h2><p>This removes the issue from the board but preserves its history.</p><p class="issues-delete-title"></p><p class="issues-delete-status" role="status"></p><div><button type="button" data-issue-delete-cancel>Cancel</button><button type="button" data-issue-delete-confirm>Delete issue</button></div></div></div>';
166
+ el.innerHTML = '<header class="issues-topbar"><button class="issues-icon-btn" data-issues-back aria-label="Back to issues" title="Back to issue list"><i data-lucide="arrow-left"></i></button><span class="issues-title"><i data-lucide="circle-dot"></i>Issues</span><span class="issues-subtitle">Manage issues through Driver chat.</span><div class="issues-window-actions"><button class="issues-icon-btn" data-issues-wide aria-label="Widen Issues" title="Widen Issues" aria-pressed="false"><i data-lucide="chevrons-left-right"></i></button><button class="issues-icon-btn" data-issues-full aria-label="Toggle Issues fullscreen" title="Toggle fullscreen" aria-pressed="false"><i data-lucide="maximize-2"></i></button><button class="issues-icon-btn" data-issues-close aria-label="Close Issues" title="Close Issues"><i data-lucide="x"></i></button></div></header><div class="issues-filters"><label class="issues-search"><i data-lucide="search"></i><input name="query" type="search" placeholder="Search issues" aria-label="Search issues"></label><select name="filter-status" aria-label="Filter status"><option value="">All statuses</option><option value="open">Open</option><option value="in_progress">In progress</option><option value="resolved">Resolved</option><option value="closed">Closed</option></select><select name="filter-type" aria-label="Filter type"><option value="">All types</option><option value="bug">Bug</option><option value="feature">Feature</option><option value="plan">Plan</option></select></div><p class="issues-notice" role="status"></p><main class="issues-content" role="region" aria-live="polite"></main><div class="issues-delete-dialog hidden" role="dialog" aria-modal="true" aria-labelledby="issues-delete-heading"><div class="issues-delete-card"><h2 id="issues-delete-heading">Delete this issue?</h2><p>This removes the issue from the board but preserves its history.</p><p class="issues-delete-title"></p><p class="issues-delete-status" role="status"></p><div><button type="button" data-issue-delete-cancel>Cancel</button><button type="button" data-issue-delete-confirm>Delete issue</button></div></div></div>';
106
167
  host.appendChild(el);
107
168
  var button = document.getElementById('issues-btn');
108
169
  if (button) button.addEventListener('click', function () { if (store.get('issuesOpen')) closeIssues(); else openIssues(); });
@@ -118,6 +179,7 @@ export function initIssues() {
118
179
  else if (target.hasAttribute('data-issue-delete-confirm')) confirmDelete();
119
180
  else if (target.hasAttribute('data-issue-edit')) showIssueEdit(entry);
120
181
  else if (target.hasAttribute('data-issue-cancel')) detail(entry);
182
+ else if (target.dataset.issueRefresh) refreshSelectedIssue(target.dataset.issueRefresh);
121
183
  else if (target.dataset.openIssue) openIssues(target.dataset.openIssue);
122
184
  else if (target.hasAttribute('data-issues-more')) listing(target.dataset.issuesMore);
123
185
  else if (target.hasAttribute('data-issue-history')) { bumpViewGeneration(); store.set({ issuesView: 'history', issuesViewRequest: request('issue_history', { ref: entry.ref }, 'history') }); }
@@ -173,6 +235,7 @@ function showIssueEdit(entry) {
173
235
  }
174
236
  formEl.addEventListener('change', syncFields);
175
237
  syncFields();
238
+ renderContextualUpdate();
176
239
  }
177
240
  export function handleIssuesMessage(msg) {
178
241
  if (msg.type === 'issue_commented') {
@@ -202,8 +265,11 @@ export function handleIssuesMessage(msg) {
202
265
  }
203
266
  if (msg.type === 'issue_updated') {
204
267
  var cache = Object.assign({}, store.get('issuesCache') || {}); cache[msg.ref] = Object.assign({}, cache[msg.ref] || {}, msg, { projectSlug: store.get('currentSlug') }); store.set({ issuesCache: cache });
205
- document.querySelectorAll('[data-issue-ref]').forEach(function (node) { if (node.dataset.issueRef === msg.ref) { node.textContent = statusLabel(msg.status) + ' · ' + msg.title; node.dataset.status = msg.status; } });
206
- if (store.get('issuesOpen')) panel().querySelector('.issues-notice').textContent = 'An issue changed. Back returns to the latest list; reopening an issue loads its latest revision.';
268
+ document.querySelectorAll('[data-issue-ref]').forEach(function (node) { if (node.dataset.issueRef === msg.ref) updateIssueLink(node, msg); });
269
+ var selected = store.get('issuesSelected');
270
+ var view = store.get('issuesView');
271
+ if (store.get('issuesOpen') && selected && selected.ref === msg.ref && (view === 'detail' || view === 'edit')) noteIssueUpdate(msg.ref, msg.revision);
272
+ else if (store.get('issuesOpen') && view === 'list') listing();
207
273
  return true;
208
274
  }
209
275
  if (msg.type !== 'issues_result') return false;
@@ -239,6 +305,16 @@ export function handleIssuesMessage(msg) {
239
305
  return true;
240
306
  }
241
307
  panel().querySelector('.issues-notice').textContent = '';
308
+ if (hasNewerDraft(item)) {
309
+ if (!msg.error && msg.result && msg.result.ref) {
310
+ var refreshedCache = Object.assign({}, store.get('issuesCache') || {});
311
+ refreshedCache[msg.result.ref] = Object.assign({}, msg.result, { canDelete: msg.canDelete === true, projectSlug: store.get('currentSlug') });
312
+ store.set({ issuesCache: refreshedCache });
313
+ if (Number(msg.result.revision || 0) > Number((store.get('issuesSelected') || {}).revision || 0)) noteIssueUpdate(msg.result.ref, msg.result.revision);
314
+ }
315
+ return true;
316
+ }
317
+ if (item.intent !== 'detail' && item.intent !== 'refresh') clearUpdateNotice();
242
318
  if (item.intent === 'work') { closeIssues(); return true; }
243
319
  if (item.intent === 'comment') {
244
320
  var commented = Object.assign({}, msg.result, { canDelete: msg.canDelete === true });
@@ -262,7 +338,15 @@ export function handleIssuesMessage(msg) {
262
338
  status: panel().querySelector('[name="filter-status"]').value, type: panel().querySelector('[name="filter-type"]').value });
263
339
  store.set({ issuesRows: result.issues }); content().innerHTML = renderIssueList(result); refreshIcons();
264
340
  } else if (item.intent === 'history') { content().innerHTML = renderIssueHistory(msg.result); refreshIcons(); }
265
- else if (item.intent === 'revision') { content().innerHTML = '<p class="issues-history-note">Historical revision · use Back to return to current issues</p>' + renderIssueDetail(msg.result); content().querySelectorAll('.issue-actions, [data-issue-session]').forEach(function (node) { node.remove(); }); refreshIcons(); }
266
- else detail(Object.assign({}, msg.result, { canDelete: msg.canDelete === true }));
341
+ else if (item.intent === 'revision') { content().innerHTML = '<p class="issues-history-note">Historical revision · use Back to return to current issues</p>' + renderIssueDetail(msg.result); content().querySelectorAll('.issue-actions, [data-issue-session], .issue-discussion form').forEach(function (node) { node.remove(); }); enhanceClayLogLinks(content()); refreshIcons(); }
342
+ else if (item.intent === 'save') {
343
+ detail(Object.assign({}, msg.result, { canDelete: msg.canDelete === true }));
344
+ } else {
345
+ var available = store.get('issuesUpdateAvailable');
346
+ if (available && available.ref === msg.result.ref && Number(msg.result.revision || 0) >= Number(available.revision || 0)) clearUpdateNotice();
347
+ var refreshed = Object.assign({}, msg.result, { canDelete: msg.canDelete === true });
348
+ detail(refreshed);
349
+ if (canRestoreCommentDraft(item.draft, msg.result.ref) && (item.intent === 'detail' || item.intent === 'refresh')) restoreCommentDraft(item.draft);
350
+ }
267
351
  return true;
268
352
  }
@@ -2,7 +2,7 @@ import { copyToClipboard, escapeHtml } from './utils.js';
2
2
  import { refreshIcons } from './icons.js';
3
3
  import { getMermaidThemeVars } from './theme.js';
4
4
  import { store } from './store.js';
5
- import { enhanceClayLogLinks } from './clay-log-links.js';
5
+ import { enhanceClayLogLinks, renderClayRecordLink } from './clay-log-links.js';
6
6
  import { renderLocalFileLink, enhanceClayFileLinks } from './clay-file-links.js';
7
7
 
8
8
  // Initialize markdown parser
@@ -12,6 +12,8 @@ marked.use({ gfm: true, breaks: false });
12
12
  marked.use({
13
13
  renderer: {
14
14
  link({ href, title, text }) {
15
+ var recordLink = renderClayRecordLink(href, text);
16
+ if (recordLink) return recordLink;
15
17
  var localLink = renderLocalFileLink(href, title, text);
16
18
  if (localLink) return localLink;
17
19
  var isExternal = href && (href.startsWith('http://') || href.startsWith('https://'));
@@ -36,6 +36,12 @@ export function forwardPaneMarkdownPresentation(message) {
36
36
  return true;
37
37
  }
38
38
 
39
+ export function forwardPaneIssueReference(ref, projectSlug) {
40
+ if (!store.get('paneMode') || window.parent === window) return false;
41
+ window.parent.postMessage({ type: "clay-pane-open-issue", ref: ref, projectSlug: projectSlug || store.get('currentSlug') }, window.location.origin);
42
+ return true;
43
+ }
44
+
39
45
  // Toggle without setContextView: panes use a transient override and must not
40
46
  // rewrite the user's server-persisted main-view preference.
41
47
  function togglePaneContextPanel() {
@@ -0,0 +1,27 @@
1
+ import { store } from './store.js';
2
+ import { openIssues } from './issues.js';
3
+
4
+ function isIssueReference(ref) {
5
+ return typeof ref === "string" && /^issue:[A-Za-z0-9_-]{24}$/.test(ref);
6
+ }
7
+
8
+ function isPaneSource(host, source) {
9
+ var frames = host.querySelectorAll(".split-pane-frame");
10
+ for (var i = 0; i < frames.length; i++) {
11
+ if (frames[i].contentWindow === source) return true;
12
+ }
13
+ return false;
14
+ }
15
+
16
+ export function handlePaneIssueMessage(event, host) {
17
+ if (!event || event.origin !== window.location.origin || !host) return false;
18
+ var msg = event.data;
19
+ if (!msg || msg.type !== "clay-pane-open-issue") return false;
20
+ if (!isIssueReference(msg.ref) || !isPaneSource(host, event.source)) return true;
21
+
22
+ var issueTarget = store.get('issuesCache') && store.get('issuesCache')[msg.ref];
23
+ var projectSlug = (issueTarget && issueTarget.projectSlug) || msg.projectSlug || store.get('currentSlug');
24
+ if (projectSlug === store.get('currentSlug')) openIssues(msg.ref);
25
+ else window.location.assign('/p/' + encodeURIComponent(projectSlug) + '/?issue=' + encodeURIComponent(msg.ref));
26
+ return true;
27
+ }
@@ -43,7 +43,7 @@ function actorLabel(actor) {
43
43
  // object (userId, sessionKey, vendor), but a session-authored entry must not
44
44
  // read as if the human owner wrote it: the Project Driver did. The owner is
45
45
  // only the account the session ran under.
46
- function authorLine(entry) {
46
+ export function authorLine(entry) {
47
47
  var actor = (entry && (entry.updatedBy || entry.createdBy)) || {};
48
48
  if (actor.type === "system") return actor.displayName || "Scheduled Tasks";
49
49
  if (actor.type === "session") {
@@ -63,7 +63,7 @@ function formatTime(value) {
63
63
  catch (e) { return ""; }
64
64
  }
65
65
 
66
- function relativeTime(value) {
66
+ export function relativeTime(value) {
67
67
  if (!value) return "";
68
68
  var delta = Date.now() - value;
69
69
  if (delta < 0) return formatTime(value);
@@ -14,6 +14,7 @@ import { presentMarkdownEdit } from './filebrowser.js';
14
14
  import { autoStartLoginIfNeeded } from './vendor-login.js';
15
15
  import { isCurrentProjectSessionReady } from './split-session-boundary.js';
16
16
  import { splitPanesMatchProject, hasCurrentSplitGroup, reconcileRestoredSplit } from './project-activation.js';
17
+ import { handlePaneIssueMessage } from './pane-issue-bridge.js';
17
18
 
18
19
  var host = null;
19
20
  var nativeApp = null;
@@ -291,6 +292,7 @@ function handlePaneMessage(event) {
291
292
  if (msg.message) autoStartLoginIfNeeded(msg.message);
292
293
  return;
293
294
  }
295
+ if (handlePaneIssueMessage(event, host)) return;
294
296
  if (msg.type !== "clay-pane-context") return;
295
297
  var frames = host.querySelectorAll(".split-pane-frame");
296
298
  for (var i = 0; i < frames.length; i++) {
@@ -1,7 +1,7 @@
1
1
  import { escapeHtml } from './utils.js';
2
2
  import { iconHtml, refreshIcons } from './icons.js';
3
3
  import { store } from './store.js';
4
- import { addToMessages, scrollToBottom } from './chat-render-runtime.js';
4
+ import { addToMessages, keepActiveThinkingAtTail, scrollToBottom } from './chat-render-runtime.js';
5
5
  import { prepareThinkingView, bindThinkingView, updateThinkingView, beginThinkingView, setThinkingViewLive, finishThinkingView } from './thinking-view.js';
6
6
 
7
7
  function getThinkingState() {
@@ -92,6 +92,7 @@ export function startThinkingSegment() {
92
92
  segmentText: "",
93
93
  startTime: Date.now(),
94
94
  });
95
+ if (!store.get('replayingHistory')) keepActiveThinkingAtTail();
95
96
  beginThinkingView(thinkingState.el, !store.get('replayingHistory') && !isMateThinking());
96
97
  showMateActivity(thinkingState.el, true, !!thinkingState.detailsText.trim());
97
98
  refreshIcons();
@@ -109,6 +110,7 @@ export function appendThinkingText(text) {
109
110
  detailsText: thinkingState.detailsText + separator + text,
110
111
  latestText: segmentText.trim() ? segmentText : thinkingState.latestText,
111
112
  });
113
+ if (!store.get('replayingHistory')) keepActiveThinkingAtTail();
112
114
  updateThinkingView(thinkingState.el, thinkingState.segmentText || thinkingState.latestText, thinkingState.detailsText);
113
115
  maybeScrollToBottom();
114
116
  }
@@ -156,6 +158,7 @@ export function restoreThinkingState(saved) {
156
158
  export function resumeThinkingAfterReplay(processing) {
157
159
  var thinkingState = getThinkingState();
158
160
  if (!thinkingState || !thinkingState.active || !processing) return;
161
+ keepActiveThinkingAtTail();
159
162
  setThinkingViewLive(thinkingState.el, !isMateThinking());
160
163
  showMateActivity(thinkingState.el, true, !!thinkingState.detailsText.trim());
161
164
  }
package/lib/sdk-bridge.js CHANGED
@@ -132,6 +132,7 @@ function createSDKBridge(opts) {
132
132
  var clayPort = opts.clayPort || 2633;
133
133
  var clayTls = opts.clayTls || false;
134
134
  var clayAuthToken = opts.clayAuthToken || null;
135
+ var clayBridgeToken = opts.clayBridgeToken || null;
135
136
  var getSessionToolDefs = opts.getSessionToolDefs || function () { return []; };
136
137
  var canUseSessionTools = opts.canUseSessionTools || function () { return true; };
137
138
  var queryToolSnapshots = new WeakMap();
@@ -155,6 +156,7 @@ function createSDKBridge(opts) {
155
156
  if (clayTls) bridgeArgs.push("--tls");
156
157
  var env = [];
157
158
  if (clayAuthToken) env.push({ name: "CLAY_AUTH_TOKEN", value: clayAuthToken });
159
+ if (clayBridgeToken) env.push({ name: "CLAY_BRIDGE_TOKEN", value: clayBridgeToken });
158
160
  return [{
159
161
  name: "clay-tools",
160
162
  command: process.execPath,
@@ -176,6 +178,7 @@ function createSDKBridge(opts) {
176
178
  if (clayTls) bridgeArgs.push("--tls");
177
179
  var bridgeEnv = {};
178
180
  if (clayAuthToken) bridgeEnv.CLAY_AUTH_TOKEN = clayAuthToken;
181
+ if (clayBridgeToken) bridgeEnv.CLAY_BRIDGE_TOKEN = clayBridgeToken;
179
182
  return { name: "clay-session-tools", command: process.execPath, args: bridgeArgs, env: bridgeEnv };
180
183
  }
181
184
 
@@ -1384,6 +1387,7 @@ function createSDKBridge(opts) {
1384
1387
  clayPort: clayPort,
1385
1388
  clayTls: clayTls,
1386
1389
  clayAuthToken: clayAuthToken,
1390
+ clayBridgeToken: clayBridgeToken,
1387
1391
  slug: slug,
1388
1392
  env: sessionRuntimeEnv,
1389
1393
  });
@@ -1398,6 +1402,7 @@ function createSDKBridge(opts) {
1398
1402
  clayPort: clayPort,
1399
1403
  clayTls: clayTls,
1400
1404
  clayAuthToken: clayAuthToken,
1405
+ clayBridgeToken: clayBridgeToken,
1401
1406
  slug: slug,
1402
1407
  env: sessionRuntimeEnv,
1403
1408
  });
@@ -1795,6 +1800,11 @@ function createSDKBridge(opts) {
1795
1800
  sendAndRecord(session, { type: "informational", level: "info", content: "Model changed from " + change.fromModel + " to " + change.toModel + " (" + change.source + ")." });
1796
1801
  },
1797
1802
  cwd: cwd,
1803
+ slug: slug,
1804
+ clayPort: clayPort,
1805
+ clayTls: clayTls,
1806
+ clayAuthToken: clayAuthToken,
1807
+ clayBridgeToken: clayBridgeToken,
1798
1808
  linuxUser: linuxUser || undefined,
1799
1809
  env: sessionRuntimeEnv,
1800
1810
  skillOptions: getSessionSkillOptions(session),
@@ -2254,6 +2264,7 @@ function createSDKBridge(opts) {
2254
2264
  clayTls: clayTls,
2255
2265
  clayAuthToken: clayAuthToken,
2256
2266
  slug: slug,
2267
+ clayBridgeToken: clayBridgeToken,
2257
2268
  });
2258
2269
 
2259
2270
  // Skill and slash-command catalogs are session-owned. The warmup has
package/lib/server.js CHANGED
@@ -945,9 +945,9 @@ function createServer(opts) {
945
945
  return;
946
946
  }
947
947
 
948
- // Auth check for project routes
949
- // Bypass auth for MCP bridge endpoint (localhost only).
950
- // The mcp-bridge-server.js runs as a local child process and cannot carry cookies.
948
+ // Auth check for project routes. The local MCP bridge gets a narrowly
949
+ // scoped project capability from its parent process; localhost alone is
950
+ // not sufficient because browser/local callers must not reach the bridge.
951
951
  var projectUrlForAuth = stripPrefix(req.url.split("?")[0], slug);
952
952
  // req.socket.remoteAddress may differ between HTTP/HTTPS (TLSSocket wraps net.Socket).
953
953
  // Also check req.connection.remoteAddress for compatibility.
@@ -958,10 +958,18 @@ function createServer(opts) {
958
958
  var isMcpBridgeLocal = projectUrlForAuth === "/api/mcp-bridge"
959
959
  && req.method === "POST"
960
960
  && isLocalhost;
961
+ var isMcpBridgeAuthenticated = isMcpBridgeLocal
962
+ && typeof ctx.isMcpBridgeAuthenticated === "function"
963
+ && ctx.isMcpBridgeAuthenticated(req);
961
964
  if (projectUrlForAuth === "/api/mcp-bridge") {
962
- console.log("[server] MCP bridge auth: method=" + req.method + " addr=" + remoteAddr + " bypass=" + isMcpBridgeLocal);
965
+ console.log("[server] MCP bridge auth: method=" + req.method + " addr=" + remoteAddr + " capability=" + isMcpBridgeAuthenticated);
963
966
  }
964
- if (!isMcpBridgeLocal && !isRequestAuthed(req)) {
967
+ if (projectUrlForAuth === "/api/mcp-bridge" && !isMcpBridgeAuthenticated) {
968
+ res.writeHead(403, { "Content-Type": "application/json" });
969
+ res.end('{"error":"MCP bridge capability required"}');
970
+ return;
971
+ }
972
+ if (!isMcpBridgeAuthenticated && !isRequestAuthed(req)) {
965
973
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
966
974
  res.end(auth.getAuthPage());
967
975
  return;
@@ -971,7 +979,7 @@ function createServer(opts) {
971
979
  res.setHeader("Set-Cookie", "clay_last_project=" + slug + "; Path=/; SameSite=Strict; Max-Age=31536000" + (tlsOptions ? "; Secure" : ""));
972
980
 
973
981
  // Multi-user: check project access for HTTP requests
974
- if (users.isMultiUser()) {
982
+ if (users.isMultiUser() && !isMcpBridgeAuthenticated) {
975
983
  var httpUser = getMultiUserFromReq(req);
976
984
  if (!httpUser || !canUserAccessSlug(httpUser.id, slug)) {
977
985
  res.writeHead(302, { "Location": "/" });
@@ -1325,6 +1333,7 @@ function createServer(opts) {
1325
1333
  projectOwnerId: projectOwnerId || null,
1326
1334
  projectKnowledgeId: extra.projectKnowledgeId || (worktreeMeta && worktreeMeta.projectKnowledgeId) || null,
1327
1335
  worktreeMeta: worktreeMeta || null,
1336
+ mcpBridgeToken: extra.mcpBridgeToken || null,
1328
1337
  isMate: extra.isMate || false,
1329
1338
  mateId: extra.mateId || (extra.isMate ? path.basename(cwd) : null),
1330
1339
  mateDisplayName: extra.mateDisplayName || "",
@@ -1808,6 +1808,7 @@ function createCodexAdapter(opts) {
1808
1808
  var clayPort = effectiveInitOpts.clayPort || process.env.CLAY_PORT || 2633;
1809
1809
  var clayTls = effectiveInitOpts.clayTls || false;
1810
1810
  var clayAuthToken = effectiveInitOpts.clayAuthToken || "";
1811
+ var clayBridgeToken = effectiveInitOpts.clayBridgeToken || "";
1811
1812
  var claySlug = effectiveInitOpts.slug || _slug || "";
1812
1813
  try {
1813
1814
  if (require("fs").existsSync(bridgePath)) {
@@ -1815,6 +1816,7 @@ function createCodexAdapter(opts) {
1815
1816
  if (clayTls) bridgeArgs.push("--tls");
1816
1817
  var bridgeEnv = {};
1817
1818
  if (clayAuthToken) bridgeEnv.CLAY_AUTH_TOKEN = clayAuthToken;
1819
+ if (clayBridgeToken) bridgeEnv.CLAY_BRIDGE_TOKEN = clayBridgeToken;
1818
1820
  mcpServerConfig["clay-tools"] = {
1819
1821
  command: process.execPath,
1820
1822
  args: bridgeArgs,
package/lib/yoke/index.js CHANGED
@@ -379,7 +379,11 @@ function createAdapters(opts) {
379
379
 
380
380
  if (installed.codex && supportsConfiguredIsolation("codex")) {
381
381
  try {
382
- adapters.codex = createAdapter({ vendor: "codex", cwd: opts.cwd, slug: opts.slug, osUsers: !!opts.osUsers });
382
+ adapters.codex = createAdapter({
383
+ vendor: "codex", cwd: opts.cwd, slug: opts.slug, osUsers: !!opts.osUsers,
384
+ clayPort: opts.clayPort, clayTls: opts.clayTls,
385
+ clayAuthToken: opts.clayAuthToken, clayBridgeToken: opts.clayBridgeToken,
386
+ });
383
387
  auth.codex = true;
384
388
  } catch (e) {
385
389
  console.error("[yoke] Failed to create adapter for codex:", e.message);
@@ -58,6 +58,7 @@ var CLAY_MCP_PATH = claySlug
58
58
 
59
59
  // --- Auth ---
60
60
  var clayAuthToken = process.env.CLAY_AUTH_TOKEN || "";
61
+ var clayBridgeToken = process.env.CLAY_BRIDGE_TOKEN || "";
61
62
 
62
63
  // --- Tool cache ---
63
64
  var _tools = []; // [{ server, name, description, inputSchema }]
@@ -79,6 +80,7 @@ function postJson(urlPath, body) {
79
80
  if (clayAuthToken) {
80
81
  headers["Cookie"] = "relay_auth=" + clayAuthToken;
81
82
  }
83
+ if (clayBridgeToken) headers["X-Clay-Bridge-Token"] = clayBridgeToken;
82
84
 
83
85
  var reqOpts = {
84
86
  hostname: parsed.hostname,
@@ -95,6 +97,15 @@ function postJson(urlPath, body) {
95
97
  res.on("data", function (chunk) { chunks.push(chunk); });
96
98
  res.on("end", function () {
97
99
  var respBody = Buffer.concat(chunks).toString("utf8");
100
+ if (res.statusCode < 200 || res.statusCode >= 300) {
101
+ var statusError = "Clay MCP bridge HTTP " + res.statusCode;
102
+ try {
103
+ var errorBody = JSON.parse(respBody);
104
+ if (errorBody && errorBody.error) statusError += ": " + errorBody.error;
105
+ } catch (e) {}
106
+ reject(new Error(statusError));
107
+ return;
108
+ }
98
109
  try {
99
110
  resolve(JSON.parse(respBody));
100
111
  } catch (e) {
@@ -122,8 +133,7 @@ function postJson(urlPath, body) {
122
133
  function fetchTools() {
123
134
  return postJson(CLAY_MCP_PATH, { action: "list_tools", sessionId: claySessionId, queryGeneration: clayQueryGeneration, sessionOnly: sessionOnly }).then(function (resp) {
124
135
  if (resp.error) {
125
- log("Failed to fetch tools: " + resp.error);
126
- return [];
136
+ throw new Error(resp.error);
127
137
  }
128
138
  _tools = resp.tools || [];
129
139
  _toolsFetched = true;
@@ -135,7 +145,8 @@ function fetchTools() {
135
145
  return _tools;
136
146
  }).catch(function (err) {
137
147
  log("Error fetching tools: " + err.message);
138
- return [];
148
+ _toolsFetched = false;
149
+ throw err;
139
150
  });
140
151
  }
141
152
 
@@ -194,7 +205,7 @@ function handleInitialize(id, params) {
194
205
  });
195
206
 
196
207
  // Pre-fetch tools after handshake
197
- fetchTools();
208
+ fetchTools().catch(function () {});
198
209
  }
199
210
 
200
211
  function handleToolsList(id, params) {
@@ -210,7 +221,9 @@ function handleToolsList(id, params) {
210
221
  }
211
222
 
212
223
  // Always re-fetch: remote MCP servers may connect after initial fetch
213
- fetchTools().then(respond);
224
+ fetchTools().then(respond).catch(function (err) {
225
+ sendError(id, -32001, "Clay tool discovery failed: " + err.message);
226
+ });
214
227
  }
215
228
 
216
229
  function handleToolsCall(id, params) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "4.1.0-beta.18",
3
+ "version": "4.1.0-beta.19",
4
4
  "description": "Self-hosted team workspace for Claude Code and Codex. Multi-user, browser-based, with persistent AI mates.",
5
5
  "bin": {
6
6
  "clay-server": "./bin/cli.js"