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.
- package/lib/linear-api.js +62 -0
- package/lib/linear-store.js +52 -0
- package/lib/project-linear-launch.js +70 -0
- package/lib/project-session-linear.js +197 -0
- package/lib/project.js +40 -2
- package/lib/public/css/linear.css +35 -0
- package/lib/public/css/right-workbench.css +3 -3
- package/lib/public/css/settings-ui.css +295 -0
- package/lib/public/index.html +20 -0
- package/lib/public/modules/app-messages.js +4 -0
- package/lib/public/modules/app-projects.js +1 -1
- package/lib/public/modules/issues.js +4 -1
- package/lib/public/modules/linear-panel.js +120 -0
- package/lib/public/modules/linear-settings.js +61 -0
- package/lib/public/modules/project-settings.js +19 -1
- package/lib/public/modules/session-github.js +15 -5
- package/lib/public/modules/sticky-notes.js +23 -6
- package/lib/public/modules/user-settings.js +4 -0
- package/lib/public/style.css +2 -0
- package/lib/sdk-bridge.js +9 -1
- package/lib/server-linear.js +50 -0
- package/lib/server.js +5 -0
- package/lib/sessions.js +5 -0
- package/package.json +1 -1
|
@@ -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
|
+
}
|
package/lib/public/index.html
CHANGED
|
@@ -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 & 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, '"').replace(/'/g, '''); }
|
|
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
|
-
|
|
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() {
|