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
- #linear-panel { width: 45%; max-width: 720px; min-width: 340px; min-height: 0; height: calc(100% - 16px); margin: 8px; align-self: center; display: flex; flex-direction: column; flex-shrink: 0; border: 1px solid var(--border); border-radius: 12px; background: var(--bg); color: var(--text); overflow: hidden; }
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
- .linear-topbar, .linear-search, .linear-actions { display: flex; align-items: center; gap: 8px; }
4
- .linear-topbar { padding: 10px 12px; border-bottom: 1px solid var(--border); }
5
- .linear-topbar strong { flex: 1; }
6
- #linear-panel button { font: inherit; color: var(--text-secondary); background: var(--bg-alt); border: 1px solid var(--border); border-radius: 6px; padding: 6px 9px; cursor: pointer; }
7
- #linear-panel button:hover { background: var(--input-bg); color: var(--text); }
8
- #linear-panel button:disabled { opacity: .5; cursor: wait; }
9
- #linear-panel button:focus-visible, #linear-panel input:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
10
- .linear-topbar svg { width: 16px; height: 16px; }
11
- .linear-search { padding: 12px; }
12
- .linear-search input { width: 100%; min-width: 0; padding: 8px; background: var(--input-bg); color: var(--text); border: 1px solid var(--border); border-radius: 6px; font: inherit; }
13
- .linear-notice { margin: 0; padding: 0 12px; color: var(--text-muted); font-size: 12px; }
14
- .linear-notice:not(:empty) { padding-bottom: 10px; }
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: 20px; color: var(--text-muted); }
17
- .linear-actions { flex-wrap: wrap; font-size: 12px; }
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-doc-body { overflow-wrap: anywhere; }
23
- #linear-panel .issue-doc-body pre { overflow-x: auto; }
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
- @media (max-width: 767px) { #linear-panel { position: fixed; inset: 8px; width: auto; max-width: none; min-width: 0; height: auto; z-index: 300; margin: 0; } }
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
  }
@@ -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, '&quot;').replace(/'/g, '&#39;'); }
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 === 'linear_search' || (action === 'linear_read' && !args.cursor)) {
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
- return DOMPurify.sanitize(element.innerHTML, { ALLOW_DATA_ATTR: false, FORBID_TAGS: ['form', 'button', 'input', 'select', 'textarea', 'iframe', 'video', 'audio'], FORBID_ATTR: ['id', 'class', 'style'] });
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-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>' : '') +
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(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>') +
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 showSearch() {
94
+ function showEmpty() {
95
+ updateExternalLink(null);
55
96
  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>';
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 showSearch();
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 === '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') {
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"><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>';
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, '&quot;').replace(/'/g, '&#39;');
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, iconHtml('circle-dot', 'github-work-status') + '<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');
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "4.8.0-beta.3",
3
+ "version": "4.8.0-beta.5",
4
4
  "description": "Self-hosted team workspace for Claude Code and Codex. Multi-user, browser-based, with persistent AI mates.",
5
5
  "bin": {
6
6
  "clay-server": "./bin/cli.js"