clay-server 4.8.0-beta.2 → 4.8.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.
@@ -0,0 +1,295 @@
1
+ /* ==========================================================================
2
+ Settings UI primitives
3
+ One scoped visual language for User, Project and Server settings.
4
+ Loaded after the per-surface sheets so these rules win on equal specificity.
5
+ Uses existing theme tokens only; nothing here applies outside settings.
6
+ ========================================================================== */
7
+
8
+ :is(#user-settings, #project-settings, #server-settings) {
9
+ --sui-radius-control: 8px;
10
+ --sui-radius-card: 10px;
11
+ --sui-control-h: 36px;
12
+ --sui-focus: 0 0 0 3px var(--accent-12);
13
+ }
14
+
15
+ /* --- Headings --- */
16
+ :is(.us-section, .ps-section, .server-settings-section) h2 {
17
+ margin: 0 0 18px;
18
+ font-size: 20px;
19
+ font-weight: var(--font-weight-heading, 650);
20
+ letter-spacing: -0.01em;
21
+ line-height: 1.25;
22
+ color: var(--text);
23
+ }
24
+
25
+ :is(.us-section, .ps-section, .server-settings-section) h3 {
26
+ margin: 26px 0 8px;
27
+ font-size: 12px;
28
+ font-weight: var(--font-weight-heading, 650);
29
+ letter-spacing: 0.06em;
30
+ text-transform: uppercase;
31
+ color: var(--text-muted);
32
+ }
33
+
34
+ :is(.us-section, .ps-section, .server-settings-section) .settings-card h3 {
35
+ margin: 0 0 4px;
36
+ font-size: 14px;
37
+ letter-spacing: 0;
38
+ text-transform: none;
39
+ color: var(--text);
40
+ }
41
+
42
+ /* --- Navigation --- */
43
+ :is(#user-settings .us-nav-item, #project-settings .settings-nav-item, #server-settings .settings-nav-item) {
44
+ padding: 7px 10px;
45
+ border: 0;
46
+ border-radius: var(--sui-radius-control);
47
+ font-size: 13.5px;
48
+ font-weight: 500;
49
+ color: var(--text-muted);
50
+ }
51
+
52
+ :is(#user-settings .us-nav-item, #project-settings .settings-nav-item, #server-settings .settings-nav-item):hover {
53
+ background: rgba(var(--overlay-rgb), 0.05);
54
+ color: var(--text);
55
+ }
56
+
57
+ :is(#user-settings .us-nav-item, #project-settings .settings-nav-item, #server-settings .settings-nav-item).active {
58
+ background: rgba(var(--overlay-rgb), 0.08);
59
+ color: var(--text);
60
+ }
61
+
62
+ :is(#user-settings .us-nav-danger, #server-settings .settings-nav-danger, #project-settings .settings-nav-danger),
63
+ :is(#user-settings .us-nav-danger, #server-settings .settings-nav-danger, #project-settings .settings-nav-danger).active {
64
+ color: var(--error);
65
+ }
66
+
67
+ :is(#user-settings, #project-settings, #server-settings) :is(.us-nav-item, .settings-nav-item, .settings-nav-dropdown):focus-visible {
68
+ outline: 2px solid var(--accent);
69
+ outline-offset: -2px;
70
+ }
71
+
72
+ /* --- Cards and rows --- */
73
+ :is(#user-settings, #project-settings, #server-settings) .settings-card {
74
+ margin-bottom: 16px;
75
+ padding: 4px 0;
76
+ overflow: hidden;
77
+ border: 1px solid var(--border);
78
+ border-radius: var(--sui-radius-card);
79
+ background: var(--bg-alt);
80
+ box-shadow: none;
81
+ }
82
+
83
+ :is(#user-settings, #project-settings, #server-settings) .settings-card:has(.layout-switcher) { padding: 14px; }
84
+
85
+ :is(#user-settings, #project-settings, #server-settings) .settings-field { padding: 14px 16px; }
86
+
87
+ :is(#user-settings, #project-settings, #server-settings) .settings-field-row,
88
+ :is(#user-settings, #project-settings, #server-settings) .settings-toggle-row {
89
+ position: relative;
90
+ gap: 16px;
91
+ padding: 12px 16px;
92
+ }
93
+
94
+ :is(#user-settings, #project-settings, #server-settings) .settings-label {
95
+ display: block;
96
+ margin: 0 0 6px;
97
+ font-size: 13px;
98
+ font-weight: 500;
99
+ line-height: 1.4;
100
+ color: var(--text);
101
+ }
102
+
103
+ :is(#user-settings, #project-settings, #server-settings) :is(.settings-field-row, .settings-toggle-row) .settings-label { margin-bottom: 0; }
104
+
105
+ :is(#user-settings, #project-settings, #server-settings) .settings-hint,
106
+ :is(.ps-section-desc, .ps-save-status) {
107
+ margin: 4px 0 0;
108
+ font-size: 12.5px;
109
+ line-height: 1.5;
110
+ color: var(--text-muted);
111
+ }
112
+
113
+ :is(#user-settings, #project-settings, #server-settings) .settings-value { color: var(--text); }
114
+
115
+ /* --- Toggle: keep the input keyboard-reachable, show focus on the track --- */
116
+ :is(#user-settings, #project-settings, #server-settings) .settings-toggle-row input {
117
+ display: block;
118
+ position: absolute;
119
+ width: 1px;
120
+ height: 1px;
121
+ margin: 0;
122
+ opacity: 0;
123
+ pointer-events: none;
124
+ }
125
+
126
+ :is(#user-settings, #project-settings, #server-settings) .settings-toggle-row input:focus-visible + .toggle-track {
127
+ outline: 2px solid var(--accent);
128
+ outline-offset: 2px;
129
+ }
130
+
131
+ /* --- Text controls --- */
132
+ :is(#user-settings, #project-settings, #server-settings) :is(.us-text-input, .ps-text-input, .settings-select, .ps-select, .ps-textarea, .settings-network-origins, #settings-pin-input) {
133
+ box-sizing: border-box;
134
+ width: 100%;
135
+ min-height: var(--sui-control-h);
136
+ padding: 8px 12px;
137
+ border: 1px solid var(--border);
138
+ border-radius: var(--sui-radius-control);
139
+ background: var(--bg);
140
+ color: var(--text);
141
+ font-family: inherit;
142
+ font-size: 14px;
143
+ line-height: 1.4;
144
+ outline: none;
145
+ box-shadow: none;
146
+ transition: border-color 0.15s, box-shadow 0.15s;
147
+ }
148
+
149
+ :is(#user-settings, #project-settings, #server-settings) :is(.ps-textarea, .settings-network-origins) {
150
+ padding: 10px 12px;
151
+ line-height: 1.6;
152
+ }
153
+
154
+ :is(#user-settings, #project-settings, #server-settings) :is(.us-text-input, .ps-text-input, .settings-select, .ps-select, .ps-textarea, .settings-network-origins, #settings-pin-input):hover:not(:disabled) {
155
+ border-color: var(--text-dimmer);
156
+ }
157
+
158
+ :is(#user-settings, #project-settings, #server-settings) :is(.us-text-input, .ps-text-input, .settings-select, .ps-select, .ps-textarea, .settings-network-origins, #settings-pin-input):focus,
159
+ :is(#user-settings, #project-settings, #server-settings) :is(.us-text-input, .ps-text-input, .settings-select, .ps-select, .ps-textarea, .settings-network-origins, #settings-pin-input):focus-visible {
160
+ border-color: var(--accent);
161
+ box-shadow: var(--sui-focus);
162
+ }
163
+
164
+ :is(#user-settings, #project-settings, #server-settings) :is(.us-text-input, .ps-text-input, .ps-textarea, #settings-pin-input)::placeholder { color: var(--text-dimmer); }
165
+
166
+ :is(#user-settings, #project-settings, #server-settings) :is(input, textarea, select):disabled { opacity: 0.55; cursor: not-allowed; }
167
+
168
+ :is(#user-settings, #project-settings, #server-settings) :is(input, textarea)[aria-invalid="true"] {
169
+ border-color: var(--error);
170
+ box-shadow: 0 0 0 3px var(--error-12);
171
+ }
172
+
173
+ /* --- Buttons: primary / secondary / destructive --- */
174
+ :is(#user-settings, #project-settings, #server-settings) :is(.us-btn, .settings-btn-sm, .settings-btn-option) {
175
+ box-sizing: border-box;
176
+ min-height: 32px;
177
+ padding: 6px 14px;
178
+ border-radius: var(--sui-radius-control);
179
+ font-family: inherit;
180
+ font-size: 13px;
181
+ font-weight: 500;
182
+ line-height: 1.3;
183
+ cursor: pointer;
184
+ transition: background 0.15s, border-color 0.15s, color 0.15s, box-shadow 0.15s;
185
+ }
186
+
187
+ :is(#user-settings, #project-settings, #server-settings) .us-btn {
188
+ border: 1px solid var(--accent);
189
+ background: var(--accent);
190
+ color: #fff;
191
+ }
192
+
193
+ :is(#user-settings, #project-settings, #server-settings) .us-btn:hover:not(:disabled) {
194
+ opacity: 1;
195
+ filter: brightness(1.08);
196
+ }
197
+
198
+ :is(#user-settings, #project-settings, #server-settings) :is(.us-btn-secondary, .settings-btn-sm) {
199
+ border: 1px solid var(--border);
200
+ background: transparent;
201
+ color: var(--text-secondary);
202
+ }
203
+
204
+ :is(#user-settings, #project-settings, #server-settings) :is(.us-btn-secondary, .settings-btn-sm):hover:not(:disabled) {
205
+ filter: none;
206
+ border-color: var(--text-dimmer);
207
+ background: rgba(var(--overlay-rgb), 0.06);
208
+ color: var(--text);
209
+ }
210
+
211
+ :is(#user-settings, #project-settings, #server-settings) :is(.us-btn-danger, .settings-btn-danger) {
212
+ border-color: color-mix(in srgb, var(--error) 40%, var(--border));
213
+ background: transparent;
214
+ color: var(--error);
215
+ }
216
+
217
+ :is(#user-settings, #project-settings, #server-settings) :is(.us-btn-danger, .settings-btn-danger):hover:not(:disabled) {
218
+ background: var(--error-12);
219
+ border-color: var(--error);
220
+ color: var(--error);
221
+ }
222
+
223
+ :is(#user-settings, #project-settings, #server-settings) :is(.us-btn, .settings-btn-sm):focus-visible {
224
+ outline: 2px solid var(--accent);
225
+ outline-offset: 2px;
226
+ }
227
+
228
+ :is(#user-settings, #project-settings, #server-settings) :is(.us-btn, .settings-btn-sm):disabled {
229
+ opacity: 0.45;
230
+ cursor: not-allowed;
231
+ }
232
+
233
+ /* --- Form layout and status --- */
234
+ :is(#user-settings, #project-settings, #server-settings) .settings-form { display: flex; flex-direction: column; align-items: stretch; }
235
+ :is(#user-settings, #project-settings, #server-settings) .settings-form .settings-hint { margin-top: 8px; }
236
+
237
+ :is(#user-settings, #project-settings, #server-settings) .settings-form-actions {
238
+ display: flex;
239
+ flex-wrap: wrap;
240
+ align-items: center;
241
+ gap: 8px;
242
+ margin-top: 14px;
243
+ }
244
+
245
+ :is(#user-settings, #project-settings, #server-settings) .settings-status {
246
+ min-height: 18px;
247
+ margin: 12px 0 0;
248
+ padding-left: 14px;
249
+ position: relative;
250
+ font-size: 12.5px;
251
+ line-height: 1.5;
252
+ color: var(--text-muted);
253
+ }
254
+
255
+ :is(#user-settings, #project-settings, #server-settings) .settings-status::before {
256
+ content: "";
257
+ position: absolute;
258
+ left: 0;
259
+ top: 0.55em;
260
+ width: 7px;
261
+ height: 7px;
262
+ border-radius: 50%;
263
+ background: var(--text-dimmer);
264
+ }
265
+
266
+ :is(#user-settings, #project-settings, #server-settings) .settings-status:empty { display: none; }
267
+ :is(#user-settings, #project-settings, #server-settings) .settings-status[data-state="connected"] { color: var(--text); }
268
+ :is(#user-settings, #project-settings, #server-settings) .settings-status[data-state="connected"]::before { background: var(--success, #3fb950); }
269
+ :is(#user-settings, #project-settings, #server-settings) .settings-status[data-state="error"] { color: var(--error); }
270
+ :is(#user-settings, #project-settings, #server-settings) .settings-status[data-state="error"]::before { background: var(--error); }
271
+
272
+ @media (max-width: 580px) {
273
+ :is(#user-settings, #project-settings, #server-settings) .settings-form-actions > .us-btn { flex: 1 1 auto; }
274
+ :is(#user-settings, #project-settings, #server-settings) .settings-field { padding: 14px; }
275
+ }
276
+
277
+ /* Monospace controls keep their face after the shared control reset. */
278
+ :is(#user-settings, #project-settings, #server-settings) :is(.us-pin-input, .settings-network-origins, .ps-textarea-mono) {
279
+ font-family: var(--font-mono, "Roboto Mono", monospace);
280
+ font-size: 13px;
281
+ }
282
+
283
+ /* Description-to-form gap inside integration cards. */
284
+ :is(#user-settings, #project-settings, #server-settings) .settings-field > .settings-hint + .settings-form { margin-top: 16px; }
285
+
286
+ /* The shared server-settings mobile dropdown margin (16px sides) pushed the
287
+ 100%-wide User Settings dropdown past the modal edge. */
288
+ @media (max-width: 580px) {
289
+ #user-settings .settings-nav-dropdown {
290
+ box-sizing: border-box;
291
+ width: 100%;
292
+ max-width: 100%;
293
+ margin: 0;
294
+ }
295
+ }
@@ -1015,6 +1015,7 @@
1015
1015
  <option value="us-mates">Mates</option>
1016
1016
  <option value="us-experimental">Experimental</option>
1017
1017
  <option value="us-email">Email</option>
1018
+ <option value="us-integrations">Integrations</option>
1018
1019
  </select>
1019
1020
  <div class="us-modal-nav-items">
1020
1021
  <button class="us-nav-item active" data-section="us-account"><span>Account</span></button>
@@ -1023,6 +1024,7 @@
1023
1024
  <button class="us-nav-item" data-section="us-mates"><span>Mates</span></button>
1024
1025
  <button class="us-nav-item" data-section="us-experimental"><span>Experimental</span></button>
1025
1026
  <button class="us-nav-item" data-section="us-email"><span>Email</span></button>
1027
+ <button class="us-nav-item" data-section="us-integrations"><span>Integrations</span></button>
1026
1028
  <div class="us-nav-separator"></div>
1027
1029
  <button class="us-nav-item us-nav-danger" data-section="us-logout"><span>Log Out</span></button>
1028
1030
  </div>
@@ -1193,6 +1195,24 @@
1193
1195
  </div>
1194
1196
  </div>
1195
1197
 
1198
+ <div class="us-section" data-section="us-integrations">
1199
+ <h2>Integrations</h2>
1200
+ <div class="settings-card"><div class="settings-field">
1201
+ <h3>Linear</h3>
1202
+ <p class="settings-hint">Connect your personal Linear API key to find issues and link them to conversations. Clay reads issues and comments; it does not change Linear.</p>
1203
+ <form id="us-linear-form" class="settings-form">
1204
+ <label class="settings-label" for="us-linear-key">Personal API key</label>
1205
+ <input id="us-linear-key" class="us-text-input" type="password" autocomplete="off" maxlength="4096" required placeholder="Linear API key" aria-describedby="us-linear-help us-linear-status">
1206
+ <p id="us-linear-help" class="settings-hint">Create a read-only key in Linear Settings → Security &amp; access. Your key is encrypted and stored server-side for your account.</p>
1207
+ <div class="settings-form-actions">
1208
+ <button type="submit" class="us-btn">Connect</button>
1209
+ <button type="button" class="us-btn us-btn-secondary" id="us-linear-disconnect" disabled>Disconnect</button>
1210
+ </div>
1211
+ <p id="us-linear-status" class="settings-status" role="status" aria-live="polite"></p>
1212
+ </form>
1213
+ </div></div>
1214
+ </div>
1215
+
1196
1216
  <!-- Email section -->
1197
1217
  <div class="us-section" data-section="us-email">
1198
1218
  <h2>Email Accounts</h2>
@@ -1,3 +1,4 @@
1
+ import { handleLinearResult } from './linear-panel.js';
1
2
  import { handleSharedBrowserMessage } from './shared-browser.js';
2
3
  import { syncGithubSessions } from './session-github.js';
3
4
  import { handleIssuesMessageInContext as handleIssuesMessage } from './issues-context.js';
@@ -816,6 +817,9 @@ export function processMessage(msg) {
816
817
  handleRemoteSelection(msg);
817
818
  break;
818
819
 
820
+ case "linear_result":
821
+ handleLinearResult(msg);
822
+ break;
819
823
  case "pair_result_status":
820
824
  handlePairResultStatus(msg);
821
825
  break;
@@ -621,7 +621,7 @@ export function handleRemoveProjectResult(msg) {
621
621
  var targetSlug = chooseProjectAfterRemoval(cachedProjects, msg.slug);
622
622
 
623
623
  showToast(isWorktree ? "Worktree removed" : "Project removed", "success");
624
- closeProjectSettings();
624
+ closeProjectSettings({ cleanup: true });
625
625
 
626
626
  // Suppress disconnect overlay and reconnect by detaching the WS
627
627
  var ws = getWs();
@@ -1,3 +1,4 @@
1
+ import { openLinear, initLinearPanel } from './linear-panel.js';
1
2
  import { store } from './store.js';
2
3
  import { getWs } from './ws-ref.js';
3
4
  import { showToast } from './utils.js';
@@ -162,13 +163,15 @@ export function openIssues(ref) {
162
163
  else listing();
163
164
  }
164
165
  export function initIssues() {
166
+ initLinearPanel();
165
167
  var host = document.getElementById('main-panels');
166
168
  if (!host || panel()) return;
167
169
  registerRightWorkbench("issues", closeIssues);
168
170
  var el = document.createElement('section');
169
171
  el.id = 'issues-panel'; el.className = 'hidden'; el.setAttribute('aria-label', 'Project Issues');
170
- 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>';
172
+ 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><button type="button" class="issues-icon-btn" data-open-linear>Linear</button><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>';
171
173
  host.appendChild(el);
174
+ el.querySelector('[data-open-linear]').addEventListener('click', function () { openLinear(); });
172
175
  el.addEventListener('input', handleIssueGithubInput);
173
176
  var button = document.getElementById('issues-btn');
174
177
  if (button) button.addEventListener('click', function () { if (store.get('issuesOpen')) closeIssues(); else openIssues(); });
@@ -0,0 +1,120 @@
1
+ import { store } from './store.js';
2
+ import { getWs } from './ws-ref.js';
3
+ import { escapeHtml } from './utils.js';
4
+ import { renderMarkdown } from './markdown.js';
5
+ import { refreshIcons } from './icons.js';
6
+ import { claimRightWorkbench, registerRightWorkbench, releaseRightWorkbench } from './right-workbench.js';
7
+ import { openUserSettings } from './user-settings.js';
8
+
9
+ function esc(value) { return escapeHtml(String(value || '')).replace(/"/g, '&quot;').replace(/'/g, '&#39;'); }
10
+ function panel() { return document.getElementById('linear-panel'); }
11
+ function content() { return panel().querySelector('.linear-content'); }
12
+ function notice(text) { panel().querySelector('[role="status"]').textContent = text; }
13
+ function request(action, args) {
14
+ var ws = getWs();
15
+ if (!ws || ws.readyState !== 1) { notice('Reconnect to use Linear.'); return; }
16
+ if (action === 'linear_search' || (action === 'linear_read' && !args.cursor)) {
17
+ store.set({ linearIssue: null, linearMatches: [] }); content().textContent = '';
18
+ }
19
+ var id = crypto.randomUUID();
20
+ store.set({ linearRequest: { id: id, action: action, args: args, project: store.get('currentSlug'), socket: ws } });
21
+ notice('Loading…');
22
+ ws.send(JSON.stringify({ type: action, args: args, requestId: id }));
23
+ return id;
24
+ }
25
+ function prose(text) {
26
+ var element = document.createElement('template');
27
+ element.innerHTML = renderMarkdown(text || '');
28
+ // Private Linear attachments need authenticated delivery; keep them as links for now.
29
+ element.content.querySelectorAll('img').forEach(function (image) {
30
+ var link = document.createElement('a');
31
+ link.textContent = image.alt || 'View attachment in Linear';
32
+ link.href = (store.get('linearIssue') || {}).url || 'https://linear.app'; link.target = '_blank'; link.rel = 'noopener noreferrer';
33
+ image.replaceWith(link);
34
+ });
35
+ return DOMPurify.sanitize(element.innerHTML, { ALLOW_DATA_ATTR: false, FORBID_TAGS: ['form', 'button', 'input', 'select', 'textarea', 'iframe', 'video', 'audio'], FORBID_ATTR: ['id', 'class', 'style'] });
36
+ }
37
+ function renderIssue() {
38
+ var issue = store.get('linearIssue'), sessions = store.get('linearMatches') || [];
39
+ if (!issue) return;
40
+ var active = (store.get('githubWorkSessions') || []).find(function (item) { return item.id === store.get('activeSessionId'); });
41
+ var linked = !!(active && (active.linearLinks || []).some(function (item) { return item.url === issue.url; }));
42
+ var prs = active && linked ? (active.githubLinks || []).filter(function (item) { return item.kind === 'pr' && /^https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/[1-9][0-9]*$/.test(item.url); }) : [];
43
+ content().innerHTML = '<article class="issue-detail"><div class="issue-byline">' + esc(issue.identifier) + ' · ' + esc(issue.stateName) + '</div><h2 class="issue-doc-title">' + esc(issue.title) + '</h2>' +
44
+ '<div class="linear-actions"><button type="button" data-linear-link>' + (linked ? 'Unlink from this conversation' : 'Link to this conversation') + '</button>' +
45
+ (!sessions.length ? '<button type="button" data-linear-start>Start work</button>' : '') +
46
+ '<a target="_blank" rel="noopener noreferrer" href="' + esc(issue.url) + '">Open in Linear</a></div>' +
47
+ '<p class="settings-hint">Linking shares the issue title and status with this conversation’s viewers. Starting work adds the description and comments to a new conversation.</p>' +
48
+ (sessions.length ? '<section><h3>Linked conversations</h3>' + sessions.map(function (item) { return '<button type="button" class="issue-row" data-linear-session="' + item.sessionId + '">' + esc(item.title || 'Open conversation') + '</button>'; }).join('') + '</section>' : '') +
49
+ (prs.length ? '<section><h3>GitHub pull requests</h3>' + prs.map(function (pr) { return '<a target="_blank" rel="noopener noreferrer" href="' + esc(pr.url) + '">PR #' + esc(pr.number) + ' · ' + esc(pr.title) + '</a>'; }).join('<br>') + '</section>' : '') +
50
+ '<div class="issue-doc-body">' + prose(issue.description) + '</div><section class="issue-discussion"><h3>Comments</h3>' +
51
+ (issue.comments.length ? issue.comments.map(function (comment) { return '<article><strong>' + esc(comment.author) + '</strong><div class="issue-doc-body">' + prose(comment.body) + '</div></article>'; }).join('') : '<p>No comments yet.</p>') +
52
+ (issue.pageInfo && issue.pageInfo.hasNextPage ? '<button type="button" data-linear-more>More comments</button>' : '') + '</section></article>';
53
+ }
54
+ function showSearch() {
55
+ store.set({ linearIssue: null, linearRequest: null, linearMatches: [] });
56
+ content().innerHTML = '<p class="linear-empty">Search by title or identifier, or paste a Linear issue URL.</p>';
57
+ notice('');
58
+ panel().querySelector('input').focus();
59
+ }
60
+ export function closeLinear() {
61
+ if (panel()) panel().classList.add('hidden');
62
+ store.set({ linearOpen: false, linearRequest: null, linearIssue: null, linearMatches: [] });
63
+ releaseRightWorkbench('linear');
64
+ }
65
+ export function openLinear(url) {
66
+ initLinearPanel();
67
+ if (!panel() || store.get('isMate')) return;
68
+ claimRightWorkbench('linear');
69
+ panel().classList.remove('hidden'); store.set({ linearOpen: true });
70
+ if (url) { content().textContent = ''; request('linear_read', { url: url }); }
71
+ else showSearch();
72
+ }
73
+ export function handleLinearResult(msg) {
74
+ var pending = store.get('linearRequest');
75
+ if (!pending || !store.get('linearOpen') || pending.id !== msg.requestId || pending.project !== store.get('currentSlug') || msg.projectSlug !== pending.project || pending.socket !== getWs()) return;
76
+ store.set({ linearRequest: null });
77
+ panel().querySelectorAll('button').forEach(function (button) { button.disabled = false; });
78
+ if (msg.error) { notice(msg.error); return; }
79
+ notice('');
80
+ var result = msg.result || {};
81
+ if (msg.action === 'linear_search') {
82
+ store.set({ linearIssue: null });
83
+ content().innerHTML = (result.items || []).map(function (item) {
84
+ return '<button type="button" class="issue-row" data-linear-url="' + esc(item.url) + '"><small>' + esc(item.identifier) + ' · ' + esc(item.stateName) + '</small><strong>' + esc(item.title) + '</strong></button>';
85
+ }).join('') || '<p class="linear-empty">No matching issues.</p>';
86
+ } else if (msg.action === 'linear_read') {
87
+ var issue = result.issue, old = store.get('linearIssue');
88
+ if (pending.args.cursor && old && old.url === issue.url) issue.comments = old.comments.concat(issue.comments).filter(function (item, index, all) { return all.findIndex(function (other) { return item.id === other.id; }) === index; });
89
+ store.set({ linearIssue: issue, linearMatches: result.matchingSessions }); renderIssue();
90
+ } else if (msg.action === 'linear_link' || msg.action === 'linear_unlink') request('linear_read', { url: pending.args.url });
91
+ else if (msg.action === 'linear_start_work' || msg.action === 'linear_open_work') { request('linear_read', { url: pending.args.url }); }
92
+ }
93
+ export function initLinearPanel() {
94
+ var host = document.getElementById('main-panels');
95
+ if (!host || panel()) return;
96
+ var el = document.createElement('section'); el.id = 'linear-panel'; el.className = 'hidden'; el.setAttribute('aria-label', 'Linear issues');
97
+ el.innerHTML = '<header class="linear-topbar"><button type="button" data-linear-back aria-label="Back to Linear search"><i data-lucide="arrow-left"></i></button><strong>Linear</strong><button type="button" data-linear-settings>Connection</button><button type="button" data-linear-close aria-label="Close Linear"><i data-lucide="x"></i></button></header><form class="linear-search"><input type="search" aria-label="Search Linear issues" placeholder="Issue URL, identifier or title" maxlength="2048" required><button type="submit">Search</button></form><p class="linear-notice" role="status"></p><div class="linear-content"></div>';
98
+ host.appendChild(el); registerRightWorkbench('linear', closeLinear); refreshIcons(el);
99
+ el.querySelector('form').addEventListener('submit', function (event) { event.preventDefault(); request('linear_search', { query: el.querySelector('input').value }); });
100
+ el.addEventListener('click', function (event) {
101
+ var button = event.target.closest('button'); if (!button) return;
102
+ var issue = store.get('linearIssue');
103
+ if (button.hasAttribute('data-linear-close')) closeLinear();
104
+ else if (button.hasAttribute('data-linear-back')) showSearch();
105
+ else if (button.hasAttribute('data-linear-settings')) openUserSettings('us-integrations');
106
+ else if (button.dataset.linearUrl) { content().textContent = ''; request('linear_read', { url: button.dataset.linearUrl }); }
107
+ else if (issue && button.hasAttribute('data-linear-link')) {
108
+ var active = (store.get('githubWorkSessions') || []).find(function (item) { return item.id === store.get('activeSessionId'); });
109
+ var linked = active && (active.linearLinks || []).some(function (item) { return item.url === issue.url; });
110
+ button.disabled = true; request(linked ? 'linear_unlink' : 'linear_link', { url: issue.url, sessionId: store.get('activeSessionId') });
111
+ } else if (issue && button.dataset.linearSession) { button.disabled = true; request('linear_open_work', { url: issue.url, sessionId: Number(button.dataset.linearSession) }); }
112
+ else if (issue && button.hasAttribute('data-linear-start')) { button.disabled = true; request('linear_start_work', { url: issue.url }); }
113
+ else if (issue && button.hasAttribute('data-linear-more')) { button.disabled = true; request('linear_read', { url: issue.url, cursor: issue.pageInfo.endCursor }); }
114
+ });
115
+ document.addEventListener('keydown', function (event) { if (event.key === 'Escape' && store.get('linearOpen')) closeLinear(); });
116
+ store.subscribe(function (state, previous) {
117
+ if (state.currentSlug !== previous.currentSlug || state.myUserId !== previous.myUserId || state.dmMode !== previous.dmMode || (previous.connected && !state.connected)) closeLinear();
118
+ else if (state.linearOpen && state.githubWorkSessions !== previous.githubWorkSessions && state.linearIssue) renderIssue();
119
+ });
120
+ }
@@ -0,0 +1,61 @@
1
+ import { store } from './store.js';
2
+
3
+ export function clearLinearKeyInput() {
4
+ var input = document.getElementById('us-linear-key');
5
+ if (input) input.value = '';
6
+ }
7
+ function setLinearStatus(text, state) {
8
+ var status = document.getElementById('us-linear-status');
9
+ if (!status) return;
10
+ status.textContent = text;
11
+ if (state) status.dataset.state = state; else delete status.dataset.state;
12
+ }
13
+ export async function loadLinearConnection() {
14
+ var actor = store.get('myUserId');
15
+ var generation = (store.get('linearConnectionGeneration') || 0) + 1;
16
+ store.set({ linearConnectionGeneration: generation });
17
+ try {
18
+ var response = await fetch('/api/linear/connection');
19
+ if (!response.ok) throw new Error('Could not load Linear connection.');
20
+ var data = await response.json();
21
+ if (store.get('linearConnectionGeneration') !== generation || store.get('myUserId') !== actor) return;
22
+ store.set({ linearConnection: data });
23
+ setLinearStatus(data.connected ? 'Connected to ' + data.workspace.name : 'Not connected', data.connected ? 'connected' : '');
24
+ var disconnect = document.getElementById('us-linear-disconnect');
25
+ if (disconnect) disconnect.disabled = !data.connected;
26
+ } catch (error) { if (store.get('linearConnectionGeneration') === generation) setLinearStatus(error.message, 'error'); }
27
+ }
28
+ export function initLinearSettings() {
29
+ var form = document.getElementById('us-linear-form');
30
+ if (!form || store.get('linearSettingsBound')) return;
31
+ store.set({ linearSettingsBound: true });
32
+ async function save(disconnect) {
33
+ if (store.get('linearConnectionSaving')) return;
34
+ var actor = store.get('myUserId');
35
+ store.set({ linearConnectionSaving: true, linearConnectionGeneration: (store.get('linearConnectionGeneration') || 0) + 1 });
36
+ var input = document.getElementById('us-linear-key');
37
+ var buttons = form.querySelectorAll('button');
38
+ buttons.forEach(function (button) { button.disabled = true; });
39
+ setLinearStatus(disconnect ? 'Disconnecting…' : 'Connecting…', '');
40
+ try {
41
+ var response = await fetch('/api/linear/connection', { method: disconnect ? 'DELETE' : 'PUT',
42
+ headers: { 'Content-Type': 'application/json' }, body: disconnect ? undefined : JSON.stringify({ key: input.value }) });
43
+ clearLinearKeyInput();
44
+ if (store.get('myUserId') !== actor) return;
45
+ var data = await response.json();
46
+ if (!response.ok) throw new Error(data.error || 'Could not update Linear connection.');
47
+ store.set({ linearConnection: data, githubWorkRefresh: null });
48
+ setLinearStatus(data.connected ? 'Connected to ' + data.workspace.name : 'Disconnected', data.connected ? 'connected' : '');
49
+ } catch (error) { clearLinearKeyInput(); setLinearStatus(error.message, 'error'); }
50
+ finally {
51
+ store.set({ linearConnectionSaving: false });
52
+ buttons.forEach(function (button) { button.disabled = false; });
53
+ document.getElementById('us-linear-disconnect').disabled = !(store.get('linearConnection') || {}).connected;
54
+ }
55
+ }
56
+ store.subscribe(function (state, previous) {
57
+ if (state.myUserId !== previous.myUserId) { clearLinearKeyInput(); store.set({ linearConnection: null, linearConnectionGeneration: (state.linearConnectionGeneration || 0) + 1 }); }
58
+ });
59
+ form.addEventListener('submit', function (event) { event.preventDefault(); save(false); });
60
+ document.getElementById('us-linear-disconnect').addEventListener('click', function () { save(true); });
61
+ }
@@ -3,6 +3,8 @@ import { refreshIcons } from './icons.js';
3
3
  import { showToast } from './utils.js';
4
4
  import { parseEmojis } from './markdown.js';
5
5
  import { closeFileViewer } from './filebrowser.js';
6
+ import { holdNotesForSettings, releaseNotesForSettings } from './sticky-notes.js';
7
+ import { openNotesBrowser, closeNotesBrowser, isNotesBrowserOpen } from './sticky-notes-browser.js';
6
8
  import { renderModelList, renderModeList, renderEffortBar, renderThinkingBar, renderBetaCard } from './settings-defaults.js';
7
9
  import { initProjectSessionVisibilitySettings, populateProjectSessionVisibilitySettings } from './project-session-visibility-settings.js';
8
10
 
@@ -12,6 +14,7 @@ var navItems = null;
12
14
  var sections = null;
13
15
  var currentSlug = null;
14
16
  var currentProject = null; // { slug, name, icon }
17
+ var notesBrowserReopen = false;
15
18
 
16
19
  // Emoji categories (reuse from sidebar)
17
20
  var EMOJI_CATEGORIES = null;
@@ -186,15 +189,30 @@ export function openProjectSettings(slug, project) {
186
189
  // Close file viewer if open (prevent split-screen)
187
190
  closeFileViewer();
188
191
 
192
+ // Sticky notes must not sit over settings; remember what to restore.
193
+ if (panelEl.classList.contains("hidden")) {
194
+ notesBrowserReopen = isNotesBrowserOpen();
195
+ if (notesBrowserReopen) closeNotesBrowser();
196
+ holdNotesForSettings();
197
+ }
198
+
189
199
  // Show panel
190
200
  panelEl.classList.remove("hidden");
191
201
  refreshIcons();
192
202
  }
193
203
 
194
- export function closeProjectSettings() {
204
+ // `cleanup` is for teardown while leaving the project: the notes workbench is
205
+ // not reopened over whatever screen comes next.
206
+ export function closeProjectSettings(opts) {
195
207
  if (!panelEl) return;
208
+ var wasOpen = !panelEl.classList.contains("hidden");
196
209
  panelEl.classList.add("hidden");
197
210
  closePsEmojiPicker();
211
+ if (wasOpen) {
212
+ releaseNotesForSettings();
213
+ if (notesBrowserReopen && !(opts && opts.cleanup)) openNotesBrowser();
214
+ notesBrowserReopen = false;
215
+ }
198
216
  }
199
217
 
200
218
  export function isProjectSettingsOpen() {