clay-server 4.8.0-beta.3 → 4.8.0-beta.5
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.
|
@@ -1,26 +1,43 @@
|
|
|
1
|
-
|
|
1
|
+
/* Linear workbench: mirrors #issues-panel / #terminal-container shell, header and controls. */
|
|
2
|
+
#linear-panel { position: relative; width: 50%; max-width: 720px; min-width: 360px; height: calc(100% - 16px); margin: 8px 8px 8px 10px; min-height: 0; align-self: center; display: flex; flex-direction: column; background: var(--bg); color: var(--text); border: 1px solid var(--border); border-radius: 12px; overflow: hidden; flex-shrink: 0; }
|
|
2
3
|
#linear-panel.hidden { display: none; }
|
|
3
|
-
|
|
4
|
-
.linear-
|
|
5
|
-
|
|
6
|
-
#linear-panel
|
|
7
|
-
#linear-panel button
|
|
8
|
-
#linear-panel button:
|
|
9
|
-
#linear-panel button:
|
|
10
|
-
|
|
11
|
-
.linear-
|
|
12
|
-
|
|
13
|
-
.linear-notice { margin: 0; padding: 0
|
|
14
|
-
.linear-notice:not(:empty) { padding
|
|
4
|
+
#linear-panel .linear-topbar { height: 48px; box-sizing: border-box; display: flex; align-items: center; gap: 12px; padding: 0 16px; border-bottom: 1px solid var(--border-subtle); flex: 0 0 auto; }
|
|
5
|
+
#linear-panel .linear-title { display: inline-flex; align-items: center; gap: 7px; font-weight: 700; flex: 1; }
|
|
6
|
+
|
|
7
|
+
#linear-panel .linear-window-actions { display: inline-flex; align-items: center; gap: 6px; margin-left: auto; }
|
|
8
|
+
#linear-panel button { font: inherit; color: var(--text-secondary); background: var(--bg-alt); border: 1px solid var(--border); border-radius: 6px; padding: 6px 10px; cursor: pointer; }
|
|
9
|
+
#linear-panel button:hover { background: var(--bg-hover, rgba(128,128,128,.12)); color: var(--text); }
|
|
10
|
+
#linear-panel button:disabled { opacity: .5; cursor: default; }
|
|
11
|
+
#linear-panel :focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
12
|
+
#linear-panel .linear-icon-btn { width: 30px; height: 30px; display: inline-flex; align-items: center; justify-content: center; padding: 0; border-radius: 7px; }
|
|
13
|
+
#linear-panel .linear-icon-btn .lucide { display: block; width: 15px; height: 15px; }
|
|
14
|
+
.linear-notice { margin: 0; padding: 0 16px; color: var(--text-muted); font-size: 12px; }
|
|
15
|
+
.linear-notice:not(:empty) { padding: 8px 16px; }
|
|
15
16
|
.linear-content { overflow: auto; min-height: 0; flex: 1; }
|
|
16
|
-
.linear-empty { padding:
|
|
17
|
-
.linear-
|
|
17
|
+
.linear-empty { max-width: 420px; margin: 0 auto; padding: 48px 28px; text-align: center; color: var(--text-muted); font-size: 13px; line-height: 1.6; }
|
|
18
|
+
.linear-empty strong { display: block; margin-bottom: 6px; color: var(--text); font-size: 14px; }
|
|
19
|
+
.linear-empty p { margin: 0; }
|
|
20
|
+
.linear-actions { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin: 0 0 12px; font-size: 12px; }
|
|
21
|
+
.linear-actions button { font-size: 12px; line-height: normal; }
|
|
18
22
|
#linear-panel a { color: var(--link); }
|
|
19
23
|
#linear-panel .issue-row { width: 100%; text-align: left; }
|
|
20
|
-
#linear-panel .issue-row small, #linear-panel .issue-row strong { display: block; }
|
|
21
24
|
#linear-panel .issue-discussion article { padding: 14px 0; border-top: 1px solid var(--border-subtle); }
|
|
22
|
-
#linear-panel .issue-
|
|
23
|
-
#linear-panel .issue-doc-body
|
|
25
|
+
#linear-panel .issue-discussion article > strong { display: block; margin-bottom: 4px; font-size: 13px; }
|
|
26
|
+
#linear-panel .issue-doc-body, #linear-panel .issue-doc-body .md-content { overflow-wrap: anywhere; }
|
|
27
|
+
#linear-panel .issue-doc-body .md-content { font-size: 14px; line-height: 1.7; }
|
|
28
|
+
#linear-panel .issue-discussion .md-content { font-size: 13px; line-height: 1.6; }
|
|
29
|
+
#linear-panel .issue-detail .issue-doc-title { font-size: clamp(20px, 2vw, 26px); line-height: 1.2; }
|
|
30
|
+
#linear-panel .md-content > :first-child { margin-top: 0; }
|
|
31
|
+
#linear-panel .md-content > :last-child { margin-bottom: 0; }
|
|
32
|
+
#linear-panel .md-content h1, #linear-panel .md-content h2, #linear-panel .md-content h3 { margin: 1.3em 0 .5em; font-family: var(--font-display); }
|
|
33
|
+
#linear-panel .md-content :is(ul, ol) { padding-left: 1.4em; }
|
|
34
|
+
#linear-panel .md-content li > :is(ul, ol) { margin-top: 4px; }
|
|
35
|
+
#linear-panel .md-content li.task-list-item { list-style: none; margin-left: -1.4em; }
|
|
36
|
+
#linear-panel .md-content pre { overflow-x: auto; max-width: 100%; }
|
|
37
|
+
#linear-panel .md-content table { display: block; max-width: 100%; overflow-x: auto; }
|
|
38
|
+
#linear-panel .md-content img { max-width: 100%; }
|
|
39
|
+
@media (max-width: 1023px) { #linear-panel { 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; } #linear-panel button { min-height: 36px; } #linear-panel .linear-icon-btn { width: 36px; } }
|
|
40
|
+
@media (max-width: 767px) { #linear-panel .issue-detail { padding: 20px 16px 40px; } }
|
|
24
41
|
.session-linear-link { max-width: 360px; min-width: 0; flex-shrink: 1; }
|
|
25
42
|
.session-linear-link > span { white-space: nowrap; }
|
|
26
43
|
.session-linear-link .linear-work-id, .session-linear-link .linear-work-state { flex-shrink: 0; }
|
|
@@ -32,4 +49,44 @@
|
|
|
32
49
|
#header-github-links:has(.session-linear-link) { max-width: 170px; }
|
|
33
50
|
#header-github-links .linear-work-title, #header-github-links .linear-work-state { display: none; }
|
|
34
51
|
}
|
|
35
|
-
|
|
52
|
+
#linear-panel .linear-session-pills { display: flex; flex-wrap: wrap; gap: 8px; }
|
|
53
|
+
#linear-panel .linear-session-pill { display: inline-flex; align-items: flex-start; gap: 7px; max-width: 100%; box-sizing: border-box; padding: 6px 12px; border-radius: 999px; text-align: left; font-size: 13px; line-height: 1.4; color: var(--text); background: var(--bg-alt); border: 1px solid var(--border); }
|
|
54
|
+
#linear-panel .linear-session-pill .lucide { width: 14px; height: 14px; flex: none; margin-top: 2px; color: var(--accent); }
|
|
55
|
+
#linear-panel .linear-session-pill > span { min-width: 0; overflow-wrap: anywhere; }
|
|
56
|
+
#linear-panel .linear-session-pill:hover { background: color-mix(in srgb, var(--accent) 10%, var(--bg-alt)); border-color: color-mix(in srgb, var(--accent) 40%, var(--border)); }
|
|
57
|
+
#linear-panel .linear-session-pill:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
58
|
+
.linear-work-brand { display: inline-flex; align-items: center; flex: none; color: inherit; }
|
|
59
|
+
.linear-work-brand svg { width: 13px; height: 13px; }
|
|
60
|
+
|
|
61
|
+
#linear-panel .linear-title .linear-work-brand svg { width: 16px; height: 16px; }
|
|
62
|
+
#linear-panel a.linear-icon-btn { color: var(--text-secondary); text-decoration: none; border: 1px solid var(--border); background: var(--bg-alt); box-sizing: border-box; }
|
|
63
|
+
#linear-panel a.linear-icon-btn:hover { background: var(--bg-hover, rgba(128,128,128,.12)); color: var(--text); }
|
|
64
|
+
#linear-panel [data-linear-external][hidden] { display: none; }
|
|
65
|
+
#linear-panel .linear-conversations { margin: 22px 0 26px; }
|
|
66
|
+
#linear-panel .linear-conversations h3 { font-size: 12px; font-weight: 600; color: var(--text-secondary); margin: 0 0 10px; }
|
|
67
|
+
#linear-panel .linear-action-chip { background: transparent; border-style: dashed; color: var(--text-secondary); }
|
|
68
|
+
#linear-panel .linear-sharing-hint { font-size: 11px; line-height: 1.5; color: var(--text-muted); margin: 10px 0 0; }
|
|
69
|
+
@media (max-width: 1023px) { #linear-panel a.linear-icon-btn { height: 36px; } }
|
|
70
|
+
|
|
71
|
+
/* Compact metadata sits with the title, without the document-style divider. */
|
|
72
|
+
#linear-panel .linear-issue-meta { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin: 0 0 14px; font-size: 12px; line-height: 1.4; }
|
|
73
|
+
#linear-panel .linear-issue-id { padding: 4px 7px; border-radius: 5px; background: var(--bg-alt); color: var(--text-secondary); font-family: var(--font-mono); font-weight: 500; letter-spacing: .015em; overflow-wrap: anywhere; }
|
|
74
|
+
#linear-panel .linear-issue-status { padding: 3px 8px; border: 1px solid var(--border); border-radius: 999px; color: var(--text-secondary); overflow-wrap: anywhere; }
|
|
75
|
+
|
|
76
|
+
#linear-panel .linear-title { min-width: 0; gap: 9px; height: 30px; font-size: 13px; line-height: 20px; font-weight: 600; }
|
|
77
|
+
#linear-panel .linear-window-actions { flex-shrink: 0; }
|
|
78
|
+
#linear-panel .linear-header-issue { display: inline-flex; align-items: center; gap: 5px; flex-shrink: 0; height: 30px; box-sizing: border-box; padding: 0 5px; border-radius: 5px; color: var(--text-secondary); text-decoration: none; font: inherit; }
|
|
79
|
+
#linear-panel .linear-header-issue .lucide { display: block; width: 12px; height: 12px; color: inherit; }
|
|
80
|
+
#linear-panel .linear-header-issue:hover { color: var(--text); background: var(--bg-alt); }
|
|
81
|
+
#linear-panel .linear-window-title { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13px; line-height: 20px; font-weight: 600; }
|
|
82
|
+
|
|
83
|
+
#linear-panel .issue-discussion .linear-comment { margin: 12px 0; padding: 16px; border: 1px solid var(--border-subtle); border-radius: 10px; background: var(--bg-alt); }
|
|
84
|
+
#linear-panel .linear-comment > header { display: flex; align-items: baseline; flex-wrap: wrap; gap: 8px; margin-bottom: 10px; font-size: 13px; }
|
|
85
|
+
#linear-panel .linear-comment > header strong { font-weight: 600; }
|
|
86
|
+
#linear-panel .linear-comment time { color: var(--text-muted); font-size: 11px; }
|
|
87
|
+
#linear-panel .linear-comment .issue-doc-body { margin: 0; }
|
|
88
|
+
@media (max-width: 767px) { #linear-panel .issue-discussion .linear-comment { padding: 12px; } }
|
|
89
|
+
|
|
90
|
+
#linear-panel .linear-title > .linear-work-brand { height: 20px; align-items: center; }
|
|
91
|
+
#linear-panel .linear-title > .linear-work-brand svg { display: block; }
|
|
92
|
+
#linear-panel [data-linear-window-id] { display: block; line-height: 20px; }
|
|
@@ -1357,23 +1357,6 @@ button.top-bar-pill.pill-accent:hover { background: color-mix(in srgb, var(--acc
|
|
|
1357
1357
|
filter: drop-shadow(0 12px 28px rgb(88 87 252 / 0.12));
|
|
1358
1358
|
}
|
|
1359
1359
|
|
|
1360
|
-
.connect-wordmark {
|
|
1361
|
-
width: clamp(155px, 18vw, 238px);
|
|
1362
|
-
aspect-ratio: 4611 / 1531;
|
|
1363
|
-
background: linear-gradient(
|
|
1364
|
-
105deg,
|
|
1365
|
-
var(--brand-indigo) 18%,
|
|
1366
|
-
var(--brand-indigo) 42%,
|
|
1367
|
-
var(--brand-green) 50%,
|
|
1368
|
-
var(--brand-indigo) 58%,
|
|
1369
|
-
var(--brand-indigo) 82%
|
|
1370
|
-
);
|
|
1371
|
-
background-size: 260% 100%;
|
|
1372
|
-
-webkit-mask: url("../clay-studio-wordmark.svg?v=400") center / contain no-repeat;
|
|
1373
|
-
mask: url("../clay-studio-wordmark.svg?v=400") center / contain no-repeat;
|
|
1374
|
-
animation: connect-wordmark-shimmer 6.4s ease-in-out infinite;
|
|
1375
|
-
}
|
|
1376
|
-
|
|
1377
1360
|
#connect-overlay-msg {
|
|
1378
1361
|
color: var(--text-dimmer);
|
|
1379
1362
|
font-family: var(--font-mono);
|
|
@@ -1387,22 +1370,12 @@ button.top-bar-pill.pill-accent:hover { background: color-mix(in srgb, var(--acc
|
|
|
1387
1370
|
50% { opacity: 1; transform: scale(1); }
|
|
1388
1371
|
}
|
|
1389
1372
|
|
|
1390
|
-
@keyframes connect-wordmark-shimmer {
|
|
1391
|
-
0%, 18% { background-position: 100% 50%; }
|
|
1392
|
-
58%, 100% { background-position: -100% 50%; }
|
|
1393
|
-
}
|
|
1394
|
-
|
|
1395
1373
|
@media (prefers-reduced-motion: reduce) {
|
|
1396
|
-
.connect-brand
|
|
1397
|
-
.connect-wordmark {
|
|
1374
|
+
.connect-brand {
|
|
1398
1375
|
animation: none;
|
|
1399
1376
|
}
|
|
1400
1377
|
|
|
1401
1378
|
.connect-brand {
|
|
1402
1379
|
opacity: 0.92;
|
|
1403
1380
|
}
|
|
1404
|
-
|
|
1405
|
-
.connect-wordmark {
|
|
1406
|
-
background-position: 50% 50%;
|
|
1407
|
-
}
|
|
1408
1381
|
}
|
package/lib/public/index.html
CHANGED
|
@@ -407,8 +407,7 @@
|
|
|
407
407
|
<div id="app">
|
|
408
408
|
<div id="connect-overlay">
|
|
409
409
|
<div class="connect-brand">
|
|
410
|
-
<img class="connect-symbol" src="clay-studio-symbol.png" alt="">
|
|
411
|
-
<div class="connect-wordmark" role="img" aria-label="Clay Studio"></div>
|
|
410
|
+
<img class="connect-symbol" src="clay-studio-symbol.png" alt="Clay Studio">
|
|
412
411
|
</div>
|
|
413
412
|
<p id="connect-overlay-msg">Connecting…</p>
|
|
414
413
|
</div>
|
|
@@ -3,18 +3,32 @@ import { getWs } from './ws-ref.js';
|
|
|
3
3
|
import { escapeHtml } from './utils.js';
|
|
4
4
|
import { renderMarkdown } from './markdown.js';
|
|
5
5
|
import { refreshIcons } from './icons.js';
|
|
6
|
+
import { linearIconMarkup } from './session-github.js';
|
|
6
7
|
import { claimRightWorkbench, registerRightWorkbench, releaseRightWorkbench } from './right-workbench.js';
|
|
7
|
-
import { openUserSettings } from './user-settings.js';
|
|
8
8
|
|
|
9
9
|
function esc(value) { return escapeHtml(String(value || '')).replace(/"/g, '"').replace(/'/g, '''); }
|
|
10
10
|
function panel() { return document.getElementById('linear-panel'); }
|
|
11
11
|
function content() { return panel().querySelector('.linear-content'); }
|
|
12
12
|
function notice(text) { panel().querySelector('[role="status"]').textContent = text; }
|
|
13
|
+
function updateExternalLink(issue) {
|
|
14
|
+
var link = panel() && panel().querySelector('[data-linear-external]');
|
|
15
|
+
if (!link) return;
|
|
16
|
+
var heading = panel().querySelector('[data-linear-window-title]');
|
|
17
|
+
var identifier = panel().querySelector('[data-linear-window-id]');
|
|
18
|
+
heading.textContent = issue ? issue.title || 'Linear issue' : 'Linear';
|
|
19
|
+
heading.title = heading.textContent;
|
|
20
|
+
identifier.textContent = issue ? issue.identifier || '' : '';
|
|
21
|
+
var url = issue && issue.url;
|
|
22
|
+
var valid = /^https:\/\/linear\.app\/[a-zA-Z0-9_-]+\/issue\/[A-Z][A-Z0-9_]*-[1-9][0-9]*$/.test(url || '');
|
|
23
|
+
link.hidden = !valid;
|
|
24
|
+
if (valid) link.setAttribute('href', url); else link.removeAttribute('href');
|
|
25
|
+
}
|
|
13
26
|
function request(action, args) {
|
|
14
27
|
var ws = getWs();
|
|
15
28
|
if (!ws || ws.readyState !== 1) { notice('Reconnect to use Linear.'); return; }
|
|
16
|
-
if (action === '
|
|
29
|
+
if (action === 'linear_read' && !args.cursor) {
|
|
17
30
|
store.set({ linearIssue: null, linearMatches: [] }); content().textContent = '';
|
|
31
|
+
updateExternalLink(null);
|
|
18
32
|
}
|
|
19
33
|
var id = crypto.randomUUID();
|
|
20
34
|
store.set({ linearRequest: { id: id, action: action, args: args, project: store.get('currentSlug'), socket: ws } });
|
|
@@ -22,6 +36,12 @@ function request(action, args) {
|
|
|
22
36
|
ws.send(JSON.stringify({ type: action, args: args, requestId: id }));
|
|
23
37
|
return id;
|
|
24
38
|
}
|
|
39
|
+
var SAFE_CLASS = /^(hljs[a-z0-9-]*|language-[a-z0-9+#-]+|task-list-item|contains-task-list)$/;
|
|
40
|
+
function keepSafeClasses(node) {
|
|
41
|
+
if (!node.getAttribute || !node.hasAttribute('class')) return;
|
|
42
|
+
var kept = node.getAttribute('class').split(/\s+/).filter(function (name) { return SAFE_CLASS.test(name); });
|
|
43
|
+
if (kept.length) node.setAttribute('class', kept.join(' ')); else node.removeAttribute('class');
|
|
44
|
+
}
|
|
25
45
|
function prose(text) {
|
|
26
46
|
var element = document.createElement('template');
|
|
27
47
|
element.innerHTML = renderMarkdown(text || '');
|
|
@@ -32,32 +52,53 @@ function prose(text) {
|
|
|
32
52
|
link.href = (store.get('linearIssue') || {}).url || 'https://linear.app'; link.target = '_blank'; link.rel = 'noopener noreferrer';
|
|
33
53
|
image.replaceWith(link);
|
|
34
54
|
});
|
|
35
|
-
|
|
55
|
+
// Task-list checkboxes are inputs (forbidden below); keep their state as text.
|
|
56
|
+
element.content.querySelectorAll('input[type="checkbox"]').forEach(function (box) { box.replaceWith(document.createTextNode(box.checked ? '\u2611 ' : '\u2610 ')); });
|
|
57
|
+
// Typography needs only code-highlight and task-list classes; ids and inline styles stay stripped.
|
|
58
|
+
DOMPurify.addHook('afterSanitizeAttributes', keepSafeClasses);
|
|
59
|
+
try {
|
|
60
|
+
return '<div class="md-content">' + DOMPurify.sanitize(element.innerHTML, { ALLOW_DATA_ATTR: false, FORBID_TAGS: ['form', 'button', 'input', 'select', 'textarea', 'iframe', 'video', 'audio', 'style'], FORBID_ATTR: ['id', 'style'] }) + '</div>';
|
|
61
|
+
} finally { DOMPurify.removeHook('afterSanitizeAttributes'); }
|
|
62
|
+
}
|
|
63
|
+
function chronologicalComments(comments) {
|
|
64
|
+
return (comments || []).slice().sort(function (a, b) {
|
|
65
|
+
var first = Date.parse(a.createdAt), second = Date.parse(b.createdAt);
|
|
66
|
+
if (!Number.isFinite(first) || !Number.isFinite(second)) return 0;
|
|
67
|
+
return first - second;
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
function commentMarkup(comment) {
|
|
71
|
+
var date = new Date(comment.createdAt);
|
|
72
|
+
var timestamp = Number.isFinite(date.getTime()) ? '<time datetime="' + esc(date.toISOString()) + '" title="' + esc(date.toLocaleString()) + '">' + esc(date.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })) + '</time>' : '';
|
|
73
|
+
return '<article class="linear-comment"><header><strong>' + esc(comment.author) + '</strong>' + timestamp + '</header><div class="issue-doc-body">' + prose(comment.body) + '</div></article>';
|
|
36
74
|
}
|
|
37
75
|
function renderIssue() {
|
|
38
76
|
var issue = store.get('linearIssue'), sessions = store.get('linearMatches') || [];
|
|
39
77
|
if (!issue) return;
|
|
78
|
+
updateExternalLink(issue);
|
|
40
79
|
var active = (store.get('githubWorkSessions') || []).find(function (item) { return item.id === store.get('activeSessionId'); });
|
|
41
80
|
var linked = !!(active && (active.linearLinks || []).some(function (item) { return item.url === issue.url; }));
|
|
42
81
|
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-
|
|
44
|
-
'<
|
|
45
|
-
|
|
46
|
-
'<
|
|
47
|
-
'<
|
|
48
|
-
|
|
82
|
+
content().innerHTML = '<article class="issue-detail"><div class="linear-issue-meta"><span class="linear-issue-id" aria-label="Issue identifier">' + esc(issue.identifier) + '</span>' + (issue.stateName ? '<span class="linear-issue-status" aria-label="Status">' + esc(issue.stateName) + '</span>' : '') + '</div><h2 class="issue-doc-title">' + esc(issue.title) + '</h2>' +
|
|
83
|
+
'<section class="linear-conversations"><h3>Linked conversations</h3><div class="linear-session-pills">' +
|
|
84
|
+
sessions.map(function (item) { return '<button type="button" class="linear-session-pill" data-linear-session="' + esc(item.sessionId) + '"><i data-lucide="message-square"></i><span>' + esc(item.title || 'Open conversation') + '</span></button>'; }).join('') +
|
|
85
|
+
'<button type="button" class="linear-session-pill linear-action-chip" data-linear-link' + (!store.get('activeSessionId') ? ' disabled' : '') + '><i data-lucide="' + (linked ? 'unlink' : 'plus') + '"></i><span>' + (linked ? 'Unlink this conversation' : 'Link this conversation') + '</span></button>' +
|
|
86
|
+
(!sessions.length ? '<button type="button" class="linear-session-pill linear-action-chip" data-linear-start><i data-lucide="plus"></i><span>Start work</span></button>' : '') +
|
|
87
|
+
'</div><p class="linear-sharing-hint">Linked issue titles and status are visible to conversation viewers.</p></section>' +
|
|
49
88
|
(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
89
|
'<div class="issue-doc-body">' + prose(issue.description) + '</div><section class="issue-discussion"><h3>Comments</h3>' +
|
|
51
|
-
(issue.comments.length ? issue.comments.map(
|
|
90
|
+
(issue.comments.length ? chronologicalComments(issue.comments).map(commentMarkup).join('') : '<p>No comments yet.</p>') +
|
|
52
91
|
(issue.pageInfo && issue.pageInfo.hasNextPage ? '<button type="button" data-linear-more>More comments</button>' : '') + '</section></article>';
|
|
92
|
+
refreshIcons(content());
|
|
53
93
|
}
|
|
54
|
-
function
|
|
94
|
+
function showEmpty() {
|
|
95
|
+
updateExternalLink(null);
|
|
55
96
|
store.set({ linearIssue: null, linearRequest: null, linearMatches: [] });
|
|
56
|
-
content().innerHTML = '<
|
|
97
|
+
content().innerHTML = '<div class="linear-empty"><strong>No issue selected</strong><p>Open a linked Linear issue from a conversation badge, or ask the Driver to find and link one. Manage your connection in Settings \u2192 Integrations.</p></div>';
|
|
57
98
|
notice('');
|
|
58
|
-
panel().querySelector('input').focus();
|
|
59
99
|
}
|
|
60
100
|
export function closeLinear() {
|
|
101
|
+
updateExternalLink(null);
|
|
61
102
|
if (panel()) panel().classList.add('hidden');
|
|
62
103
|
store.set({ linearOpen: false, linearRequest: null, linearIssue: null, linearMatches: [] });
|
|
63
104
|
releaseRightWorkbench('linear');
|
|
@@ -67,8 +108,9 @@ export function openLinear(url) {
|
|
|
67
108
|
if (!panel() || store.get('isMate')) return;
|
|
68
109
|
claimRightWorkbench('linear');
|
|
69
110
|
panel().classList.remove('hidden'); store.set({ linearOpen: true });
|
|
111
|
+
updateExternalLink(null);
|
|
70
112
|
if (url) { content().textContent = ''; request('linear_read', { url: url }); }
|
|
71
|
-
else
|
|
113
|
+
else showEmpty();
|
|
72
114
|
}
|
|
73
115
|
export function handleLinearResult(msg) {
|
|
74
116
|
var pending = store.get('linearRequest');
|
|
@@ -78,12 +120,7 @@ export function handleLinearResult(msg) {
|
|
|
78
120
|
if (msg.error) { notice(msg.error); return; }
|
|
79
121
|
notice('');
|
|
80
122
|
var result = msg.result || {};
|
|
81
|
-
if (msg.action === '
|
|
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') {
|
|
123
|
+
if (msg.action === 'linear_read') {
|
|
87
124
|
var issue = result.issue, old = store.get('linearIssue');
|
|
88
125
|
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
126
|
store.set({ linearIssue: issue, linearMatches: result.matchingSessions }); renderIssue();
|
|
@@ -94,16 +131,12 @@ export function initLinearPanel() {
|
|
|
94
131
|
var host = document.getElementById('main-panels');
|
|
95
132
|
if (!host || panel()) return;
|
|
96
133
|
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"><
|
|
134
|
+
el.innerHTML = '<header class="linear-topbar"><div class="linear-title">' + linearIconMarkup() + '<a class="linear-header-issue" data-linear-external hidden target="_blank" rel="noopener noreferrer" aria-label="Open issue in Linear" title="Open in Linear"><span data-linear-window-id></span><i data-lucide="external-link"></i></a><span class="linear-window-title" data-linear-window-title>Linear</span></div><div class="linear-window-actions"><button type="button" class="linear-icon-btn" data-linear-close aria-label="Close Linear" title="Close Linear"><i data-lucide="x"></i></button></div></header><p class="linear-notice" role="status"></p><div class="linear-content"></div>';
|
|
98
135
|
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
136
|
el.addEventListener('click', function (event) {
|
|
101
137
|
var button = event.target.closest('button'); if (!button) return;
|
|
102
138
|
var issue = store.get('linearIssue');
|
|
103
139
|
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
140
|
else if (issue && button.hasAttribute('data-linear-link')) {
|
|
108
141
|
var active = (store.get('githubWorkSessions') || []).find(function (item) { return item.id === store.get('activeSessionId'); });
|
|
109
142
|
var linked = active && (active.linearLinks || []).some(function (item) { return item.url === issue.url; });
|
|
@@ -115,6 +148,6 @@ export function initLinearPanel() {
|
|
|
115
148
|
document.addEventListener('keydown', function (event) { if (event.key === 'Escape' && store.get('linearOpen')) closeLinear(); });
|
|
116
149
|
store.subscribe(function (state, previous) {
|
|
117
150
|
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();
|
|
151
|
+
else if (state.linearOpen && (state.githubWorkSessions !== previous.githubWorkSessions || state.activeSessionId !== previous.activeSessionId) && state.linearIssue) renderIssue();
|
|
119
152
|
});
|
|
120
153
|
}
|
|
@@ -8,6 +8,9 @@ function escapeHtml(value) {
|
|
|
8
8
|
return escapeText(String(value)).replace(/"/g, '"').replace(/'/g, ''');
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
// Local Linear mark (no external asset): a disc cut by three diagonal bands.
|
|
12
|
+
var LINEAR_MARK = '<span class="linear-work-brand" aria-hidden="true"><svg viewBox="0 0 16 16" width="13" height="13" fill="currentColor" focusable="false"><path d="M1.2 9.7a6.9 6.9 0 0 0 5.1 5.1zM1 7.2l7.8 7.8a7 7 0 0 0 1.6-.4L1.4 5.6A7 7 0 0 0 1 7.2zm1-3 9.8 9.8a7 7 0 0 0 1.2-.9L2.9 3a7 7 0 0 0-.9 1.2zM4 2.2l9.8 9.8A7 7 0 0 0 8 1a7 7 0 0 0-4 1.2z"/></svg></span>';
|
|
13
|
+
export function linearIconMarkup() { return LINEAR_MARK; }
|
|
11
14
|
function safeLinks(session) {
|
|
12
15
|
return (session && Array.isArray(session.githubLinks) ? session.githubLinks : []).concat(session && Array.isArray(session.linearLinks) ? session.linearLinks : []).filter(function (link) {
|
|
13
16
|
if (link.provider === 'linear') return /^https:\/\/linear\.app\/[a-zA-Z0-9_-]+\/issue\/[A-Z][A-Z0-9_]*-[1-9][0-9]*$/.test(link.url);
|
|
@@ -23,13 +26,13 @@ function anchor(url, title, content, className) {
|
|
|
23
26
|
}
|
|
24
27
|
export function orderedGithubWork(session) {
|
|
25
28
|
var links = safeLinks(session).slice().reverse();
|
|
26
|
-
var latestIssue = links.find(function (link) { return link.kind === 'issue'; });
|
|
29
|
+
var latestIssue = links.find(function (link) { return link.kind === 'issue' || link.provider === 'linear'; });
|
|
27
30
|
if (latestIssue) links = [latestIssue].concat(links.filter(function (link) { return link !== latestIssue; }));
|
|
28
31
|
return links;
|
|
29
32
|
}
|
|
30
33
|
function compactGithubWork(links, sessionRow) {
|
|
31
34
|
if (!sessionRow && !links.some(function (link) { return link.provider === 'linear'; })) return links.slice(0, 1);
|
|
32
|
-
var latestIssue = links.find(function (link) { return link.kind === 'issue'; });
|
|
35
|
+
var latestIssue = links.find(function (link) { return link.kind === 'issue' || link.provider === 'linear'; });
|
|
33
36
|
var latestPr = links.find(function (link) { return link.kind === 'pr'; });
|
|
34
37
|
return [latestIssue, latestPr].filter(Boolean);
|
|
35
38
|
}
|
|
@@ -40,7 +43,7 @@ export function githubWorkMarkup(session, detailed, expanded, sessionRow) {
|
|
|
40
43
|
return '<span class="session-github-links' + (detailed ? ' github-work-details' : '') + (sessionRow ? ' session-row-github-links' : '') + '">' + displayed.map(function (link) {
|
|
41
44
|
if (link.provider === 'linear') {
|
|
42
45
|
var description = link.identifier + ' · ' + link.title + ' · ' + link.stateName + (link.stale ? ' · Last known status' : '');
|
|
43
|
-
return anchor(link.url, description,
|
|
46
|
+
return anchor(link.url, description, LINEAR_MARK + '<span class="linear-work-id">' + escapeHtml(link.identifier) + '</span><span class="linear-work-title">' + escapeHtml(link.title) + '</span><span class="linear-work-state">' + escapeHtml(link.stateName) + '</span>', 'session-github-link session-linear-link');
|
|
44
47
|
}
|
|
45
48
|
var state = ['open', 'closed', 'merged', 'draft'].indexOf(link.state) >= 0 ? link.state : 'unknown';
|
|
46
49
|
var icon = link.kind === 'pr' ? (state === 'merged' ? 'git-merge' : state === 'closed' ? 'git-pull-request-closed' : state === 'draft' ? 'git-pull-request-draft' : 'git-pull-request') : (state === 'closed' ? 'circle-check' : 'circle-dot');
|
package/package.json
CHANGED