clay-server 4.4.0 → 4.5.0-beta.1

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.
Files changed (158) hide show
  1. package/LICENSES/README.md +2 -0
  2. package/lib/project-issues.js +11 -0
  3. package/lib/project-session-github.js +93 -0
  4. package/lib/project.js +32 -2
  5. package/lib/public/app.js +4 -0
  6. package/lib/public/css/base.css +3 -57
  7. package/lib/public/css/filebrowser.css +1 -1
  8. package/lib/public/css/fonts.css +14 -0
  9. package/lib/public/css/git-panel.css +2 -2
  10. package/lib/public/css/git-placard.css +1 -1
  11. package/lib/public/css/mobile-nav.css +13 -13
  12. package/lib/public/css/noto-sans-kr.css +126 -0
  13. package/lib/public/css/overlays.css +4 -4
  14. package/lib/public/css/profile.css +4 -4
  15. package/lib/public/css/server-settings.css +1 -1
  16. package/lib/public/css/session-github.css +112 -0
  17. package/lib/public/css/sidebar.css +2 -2
  18. package/lib/public/css/sticky-notes.css +3 -3
  19. package/lib/public/css/title-bar.css +26 -5
  20. package/lib/public/fonts/geist/Geist-Variable.woff2 +0 -0
  21. package/lib/public/fonts/geist/LICENSE.txt +92 -0
  22. package/lib/public/fonts/noto-sans-kr/LICENSE.txt +93 -0
  23. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-0-wght-normal.woff2 +0 -0
  24. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-1-wght-normal.woff2 +0 -0
  25. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-10-wght-normal.woff2 +0 -0
  26. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-100-wght-normal.woff2 +0 -0
  27. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-101-wght-normal.woff2 +0 -0
  28. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-102-wght-normal.woff2 +0 -0
  29. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-103-wght-normal.woff2 +0 -0
  30. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-104-wght-normal.woff2 +0 -0
  31. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-105-wght-normal.woff2 +0 -0
  32. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-106-wght-normal.woff2 +0 -0
  33. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-107-wght-normal.woff2 +0 -0
  34. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-108-wght-normal.woff2 +0 -0
  35. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-109-wght-normal.woff2 +0 -0
  36. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-11-wght-normal.woff2 +0 -0
  37. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-110-wght-normal.woff2 +0 -0
  38. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-111-wght-normal.woff2 +0 -0
  39. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-112-wght-normal.woff2 +0 -0
  40. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-113-wght-normal.woff2 +0 -0
  41. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-114-wght-normal.woff2 +0 -0
  42. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-115-wght-normal.woff2 +0 -0
  43. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-116-wght-normal.woff2 +0 -0
  44. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-117-wght-normal.woff2 +0 -0
  45. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-118-wght-normal.woff2 +0 -0
  46. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-119-wght-normal.woff2 +0 -0
  47. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-12-wght-normal.woff2 +0 -0
  48. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-13-wght-normal.woff2 +0 -0
  49. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-14-wght-normal.woff2 +0 -0
  50. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-15-wght-normal.woff2 +0 -0
  51. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-16-wght-normal.woff2 +0 -0
  52. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-17-wght-normal.woff2 +0 -0
  53. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-18-wght-normal.woff2 +0 -0
  54. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-19-wght-normal.woff2 +0 -0
  55. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-2-wght-normal.woff2 +0 -0
  56. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-20-wght-normal.woff2 +0 -0
  57. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-21-wght-normal.woff2 +0 -0
  58. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-22-wght-normal.woff2 +0 -0
  59. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-23-wght-normal.woff2 +0 -0
  60. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-24-wght-normal.woff2 +0 -0
  61. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-25-wght-normal.woff2 +0 -0
  62. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-26-wght-normal.woff2 +0 -0
  63. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-27-wght-normal.woff2 +0 -0
  64. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-28-wght-normal.woff2 +0 -0
  65. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-29-wght-normal.woff2 +0 -0
  66. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-3-wght-normal.woff2 +0 -0
  67. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-30-wght-normal.woff2 +0 -0
  68. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-31-wght-normal.woff2 +0 -0
  69. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-32-wght-normal.woff2 +0 -0
  70. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-33-wght-normal.woff2 +0 -0
  71. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-34-wght-normal.woff2 +0 -0
  72. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-35-wght-normal.woff2 +0 -0
  73. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-36-wght-normal.woff2 +0 -0
  74. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-37-wght-normal.woff2 +0 -0
  75. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-38-wght-normal.woff2 +0 -0
  76. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-39-wght-normal.woff2 +0 -0
  77. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-4-wght-normal.woff2 +0 -0
  78. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-40-wght-normal.woff2 +0 -0
  79. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-41-wght-normal.woff2 +0 -0
  80. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-42-wght-normal.woff2 +0 -0
  81. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-43-wght-normal.woff2 +0 -0
  82. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-44-wght-normal.woff2 +0 -0
  83. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-45-wght-normal.woff2 +0 -0
  84. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-46-wght-normal.woff2 +0 -0
  85. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-47-wght-normal.woff2 +0 -0
  86. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-48-wght-normal.woff2 +0 -0
  87. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-49-wght-normal.woff2 +0 -0
  88. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-5-wght-normal.woff2 +0 -0
  89. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-50-wght-normal.woff2 +0 -0
  90. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-51-wght-normal.woff2 +0 -0
  91. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-52-wght-normal.woff2 +0 -0
  92. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-53-wght-normal.woff2 +0 -0
  93. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-54-wght-normal.woff2 +0 -0
  94. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-55-wght-normal.woff2 +0 -0
  95. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-56-wght-normal.woff2 +0 -0
  96. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-57-wght-normal.woff2 +0 -0
  97. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-58-wght-normal.woff2 +0 -0
  98. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-59-wght-normal.woff2 +0 -0
  99. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-6-wght-normal.woff2 +0 -0
  100. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-60-wght-normal.woff2 +0 -0
  101. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-61-wght-normal.woff2 +0 -0
  102. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-62-wght-normal.woff2 +0 -0
  103. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-63-wght-normal.woff2 +0 -0
  104. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-64-wght-normal.woff2 +0 -0
  105. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-65-wght-normal.woff2 +0 -0
  106. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-66-wght-normal.woff2 +0 -0
  107. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-67-wght-normal.woff2 +0 -0
  108. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-68-wght-normal.woff2 +0 -0
  109. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-69-wght-normal.woff2 +0 -0
  110. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-7-wght-normal.woff2 +0 -0
  111. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-70-wght-normal.woff2 +0 -0
  112. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-71-wght-normal.woff2 +0 -0
  113. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-72-wght-normal.woff2 +0 -0
  114. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-73-wght-normal.woff2 +0 -0
  115. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-74-wght-normal.woff2 +0 -0
  116. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-75-wght-normal.woff2 +0 -0
  117. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-76-wght-normal.woff2 +0 -0
  118. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-77-wght-normal.woff2 +0 -0
  119. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-78-wght-normal.woff2 +0 -0
  120. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-79-wght-normal.woff2 +0 -0
  121. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-8-wght-normal.woff2 +0 -0
  122. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-80-wght-normal.woff2 +0 -0
  123. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-81-wght-normal.woff2 +0 -0
  124. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-82-wght-normal.woff2 +0 -0
  125. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-83-wght-normal.woff2 +0 -0
  126. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-84-wght-normal.woff2 +0 -0
  127. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-85-wght-normal.woff2 +0 -0
  128. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-86-wght-normal.woff2 +0 -0
  129. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-87-wght-normal.woff2 +0 -0
  130. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-88-wght-normal.woff2 +0 -0
  131. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-89-wght-normal.woff2 +0 -0
  132. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-9-wght-normal.woff2 +0 -0
  133. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-90-wght-normal.woff2 +0 -0
  134. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-91-wght-normal.woff2 +0 -0
  135. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-92-wght-normal.woff2 +0 -0
  136. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-93-wght-normal.woff2 +0 -0
  137. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-94-wght-normal.woff2 +0 -0
  138. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-95-wght-normal.woff2 +0 -0
  139. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-96-wght-normal.woff2 +0 -0
  140. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-97-wght-normal.woff2 +0 -0
  141. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-98-wght-normal.woff2 +0 -0
  142. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-99-wght-normal.woff2 +0 -0
  143. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-cyrillic-wght-normal.woff2 +0 -0
  144. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-latin-ext-wght-normal.woff2 +0 -0
  145. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-latin-wght-normal.woff2 +0 -0
  146. package/lib/public/fonts/noto-sans-kr/noto-sans-kr-vietnamese-wght-normal.woff2 +0 -0
  147. package/lib/public/modules/app-messages.js +2 -0
  148. package/lib/public/modules/git-panel.js +6 -2
  149. package/lib/public/modules/issues-render.js +2 -0
  150. package/lib/public/modules/markdown.js +3 -4
  151. package/lib/public/modules/session-github.js +153 -0
  152. package/lib/public/modules/sidebar-mobile.js +8 -1
  153. package/lib/public/modules/sidebar-sessions.js +2 -0
  154. package/lib/public/style.css +1 -0
  155. package/lib/sdk-bridge.js +11 -0
  156. package/lib/session-github.js +48 -0
  157. package/lib/sessions.js +3 -0
  158. package/package.json +1 -1
@@ -0,0 +1,153 @@
1
+ import { store } from './store.js';
2
+ import { getWs } from './ws-ref.js';
3
+ import { githubLinkTarget } from './github-links.js';
4
+ import { iconHtml, refreshIcons } from './icons.js';
5
+ import { escapeHtml as escapeText } from './utils.js';
6
+
7
+ function escapeHtml(value) {
8
+ return escapeText(String(value)).replace(/"/g, '"').replace(/'/g, ''');
9
+ }
10
+
11
+ function safeLinks(session) {
12
+ return (session && Array.isArray(session.githubLinks) ? session.githubLinks : []).filter(function (link) {
13
+ var target = githubLinkTarget(link.url);
14
+ return target && (target.kind === 'issue' || target.kind === 'pull');
15
+ });
16
+ }
17
+ function label(link) {
18
+ return (link.kind === 'pr' ? 'PR #' : 'Issue #') + link.number;
19
+ }
20
+ function anchor(url, title, content, className) {
21
+ return '<a class="' + className + '" href="' + escapeHtml(url) + '" target="_blank" rel="noopener noreferrer" title="' + escapeHtml(title) + '" aria-label="' + escapeHtml(title) + '">' + content + '</a>';
22
+ }
23
+ export function orderedGithubWork(session) {
24
+ var links = safeLinks(session).slice().reverse();
25
+ var latestIssue = links.find(function (link) { return link.kind === 'issue'; });
26
+ if (latestIssue) links = [latestIssue].concat(links.filter(function (link) { return link !== latestIssue; }));
27
+ return links;
28
+ }
29
+ export function githubWorkMarkup(session, detailed, expanded) {
30
+ var links = orderedGithubWork(session);
31
+ if (!links.length) return '';
32
+ return '<span class="session-github-links' + (detailed ? ' github-work-details' : '') + '">' + (detailed || expanded ? links : links.slice(0, 1)).map(function (link) {
33
+ var state = ['open', 'closed', 'merged', 'draft'].indexOf(link.state) >= 0 ? link.state : 'unknown';
34
+ 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');
35
+ var description = link.repository + ' · ' + label(link) + ' · ' + link.title + ' · ' + state + (link.checkedAt ? ' · Checked ' + new Date(link.checkedAt).toLocaleString() : '') + (link.stale ? ' (last known status; refresh unavailable)' : '');
36
+ var html = anchor(link.url, description, iconHtml('github', 'github-work-brand') + iconHtml(icon, 'github-work-status') + '<span><span class="github-work-kind">' + (link.kind === 'pr' ? 'PR ' : 'Issue ') + '</span>#' + escapeHtml(String(link.number)) + '</span>', 'session-github-link github-kind-' + (link.kind === 'pr' ? 'pr' : 'issue') + ' github-state-' + (link.stale ? 'unknown' : state));
37
+ if (expanded) html = '<span class="github-work-entry">' + html + '<span class="github-work-entry-title">' + escapeHtml(link.title || '') + '</span><span class="github-work-entry-meta">' + escapeHtml(link.repository + ' · ' + state + (link.stale ? ' · Last known status' : '')) + '</span></span>';
38
+ if (detailed && link.kind === 'pr') {
39
+ html += anchor(link.url + '/files', 'View PR changes on GitHub', 'Changes', 'github-work-action');
40
+ html += anchor(link.url + '#pullrequestreview', 'View PR reviews on GitHub', escapeHtml((link.review || 'Reviews').replace(/_/g, ' ').toLowerCase()), 'github-work-action');
41
+ (link.checks || []).forEach(function (check) {
42
+ if (/^https:\/\//i.test(check.url)) html += anchor(check.url, check.name + ': ' + check.state, escapeHtml(check.name + ' · ' + check.state.toLowerCase()), 'github-work-action');
43
+ });
44
+ }
45
+ return html;
46
+ }).join('') + (!detailed && !expanded && links.length > 1 ? '<button type="button" class="github-work-more" aria-label="Show all ' + links.length + ' linked GitHub items" aria-haspopup="dialog" aria-expanded="false" data-github-links="' + escapeHtml(JSON.stringify(safeLinks(session))) + '">+' + (links.length - 1) + '</button>' : '') + '</span>';
47
+ }
48
+ export function appendGithubWork(row, session, title) {
49
+ var html = githubWorkMarkup(session, false);
50
+ bindGithubWorkPopover();
51
+ if (!html) return;
52
+ var column = document.createElement('span');
53
+ column.className = 'session-work-column';
54
+ row.insertBefore(column, title);
55
+ column.appendChild(title);
56
+ var links = document.createElement('span');
57
+ links.innerHTML = html;
58
+ links.addEventListener('click', function (event) { if (event.target.closest('a')) event.stopPropagation(); });
59
+ links.addEventListener('contextmenu', function (event) { event.stopPropagation(); });
60
+ column.appendChild(links);
61
+ }
62
+ export function syncGithubSessions(sessions) {
63
+ bindGithubWorkPopover();
64
+ store.set({ githubWorkSessions: sessions || [], githubWorkProject: store.get('currentSlug') });
65
+ var active = (sessions || []).find(function (session) { return session.active; });
66
+ var title = document.getElementById('header-title');
67
+ var old = document.getElementById('header-github-links');
68
+ var html = title && !store.get('dmMode') && !store.get('splitPanes') && active ? githubWorkMarkup(active, false) : '';
69
+ if (!html) { if (old) old.remove(); }
70
+ else if (!old || old.dataset.markup !== html) {
71
+ var element = old || document.createElement('span');
72
+ element.id = 'header-github-links';
73
+ element.dataset.markup = html;
74
+ element.innerHTML = html;
75
+ if (!old) title.insertAdjacentElement('afterend', element);
76
+ }
77
+ refreshIcons();
78
+ refreshGithubWork();
79
+ }
80
+ export function currentGithubWorkMarkup() {
81
+ if (store.get('githubWorkProject') !== store.get('currentSlug')) return '';
82
+ var session = (store.get('githubWorkSessions') || []).find(function (item) { return item.active; });
83
+ return githubWorkMarkup(session, true);
84
+ }
85
+ export function refreshGithubWork() {
86
+ if (document.hidden || store.get('dmMode') || !store.get('connected')) return;
87
+ if (store.get('githubWorkProject') !== store.get('currentSlug')) return;
88
+ var session = (store.get('githubWorkSessions') || []).find(function (item) { return item.active; });
89
+ if (!safeLinks(session).length) return;
90
+ var key = store.get('currentSlug') + ':' + session.id;
91
+ var previous = store.get('githubWorkRefresh') || {};
92
+ if (previous.key === key && Date.now() - previous.at < 60000) return;
93
+ var ws = getWs();
94
+ if (!ws || ws.readyState !== 1) return;
95
+ store.set({ githubWorkRefresh: { key: key, at: Date.now() } });
96
+ ws.send(JSON.stringify({ type: 'session_github_refresh' }));
97
+ }
98
+
99
+ export function resetGithubWork() {
100
+ closeGithubWorkPopover();
101
+ store.set({ githubWorkSessions: [], githubWorkProject: null, githubWorkRefresh: null });
102
+ var header = document.getElementById('header-github-links');
103
+ if (header) header.remove();
104
+ }
105
+
106
+ function closeGithubWorkPopover() {
107
+ var popup = document.getElementById('github-work-popover');
108
+ if (popup) popup.hidePopover();
109
+ }
110
+ function bindGithubWorkPopover() {
111
+ if (store.get('githubWorkPopoverBound')) return;
112
+ store.set({ githubWorkPopoverBound: true });
113
+ document.addEventListener('click', function (event) {
114
+ var button = event.target.closest('.github-work-more');
115
+ if (!button) return;
116
+ event.preventDefault();
117
+ event.stopPropagation();
118
+ var existing = document.getElementById('github-work-popover');
119
+ var same = existing && button.getAttribute('aria-expanded') === 'true';
120
+ if (existing) { existing.hidePopover(); existing.remove(); }
121
+ if (same) return;
122
+ var links;
123
+ try { links = JSON.parse(button.dataset.githubLinks); } catch (error) { return; }
124
+ var popup = document.createElement('div');
125
+ popup.id = 'github-work-popover';
126
+ popup.className = 'github-work-popover';
127
+ popup.setAttribute('popover', 'auto');
128
+ popup.setAttribute('role', 'dialog');
129
+ popup.setAttribute('aria-label', 'Linked GitHub work');
130
+ popup.innerHTML = '<strong>Linked GitHub work</strong>' + githubWorkMarkup({ githubLinks: links }, false, true);
131
+ document.body.appendChild(popup);
132
+ button.setAttribute('aria-expanded', 'true');
133
+ popup.addEventListener('toggle', function (toggle) {
134
+ if (toggle.newState === 'closed') {
135
+ button.setAttribute('aria-expanded', 'false');
136
+ if (document.activeElement === document.body && button.isConnected) button.focus();
137
+ popup.remove();
138
+ }
139
+ });
140
+ popup.showPopover();
141
+ var rect = button.getBoundingClientRect();
142
+ var box = popup.getBoundingClientRect();
143
+ popup.style.left = Math.max(8, Math.min(rect.left, window.innerWidth - box.width - 8)) + 'px';
144
+ popup.style.top = Math.max(8, rect.bottom + box.height + 8 <= window.innerHeight ? rect.bottom + 6 : rect.top - box.height - 6) + 'px';
145
+ var first = popup.querySelector('a');
146
+ if (first) first.focus({ preventScroll: true });
147
+ refreshIcons();
148
+ }, true);
149
+ window.addEventListener('resize', closeGithubWorkPopover);
150
+ store.subscribe(function (state, previous) {
151
+ if (state.currentSlug !== previous.currentSlug || state.activeSessionId !== previous.activeSessionId || state.dmMode !== previous.dmMode) closeGithubWorkPopover();
152
+ });
153
+ }
@@ -1,3 +1,4 @@
1
+ import { appendGithubWork } from './session-github.js';
1
2
  // sidebar-mobile.js - Mobile sheet overlays, tab bar, and mobile-specific rendering
2
3
  // Extracted from sidebar.js (PR-38)
3
4
 
@@ -384,7 +385,7 @@ function renderSheetSessions(listEl) {
384
385
  // Helper: create a mobile session item element
385
386
  function createMobileSessionItem(s, options) {
386
387
  var itemOptions = options || {};
387
- var el = document.createElement("button");
388
+ var el = document.createElement("div");
388
389
  el.className = "mobile-session-item" + (s.active ? " active" : "");
389
390
  if (itemOptions.worker) el.classList.add("mobile-worker-item");
390
391
 
@@ -404,9 +405,15 @@ function createMobileSessionItem(s, options) {
404
405
  }
405
406
 
406
407
  var titleSpan = document.createElement("span");
408
+ titleSpan.tabIndex = 0;
409
+ titleSpan.setAttribute("role", "button");
410
+ titleSpan.addEventListener("keydown", function (event) {
411
+ if (event.key === "Enter" || event.key === " ") { event.preventDefault(); el.click(); }
412
+ });
407
413
  titleSpan.className = "mobile-session-title";
408
414
  titleSpan.appendChild(document.createTextNode(s.title || "New Session"));
409
415
  el.appendChild(titleSpan);
416
+ appendGithubWork(el, s, titleSpan);
410
417
 
411
418
  if (itemOptions.worker) {
412
419
  var generation = document.createElement("span");
@@ -1,3 +1,4 @@
1
+ import { appendGithubWork } from './session-github.js';
1
2
  // sidebar-sessions.js - Session list, search, presence, countdown, CLI picker
2
3
  // Extracted from sidebar.js (PR-35)
3
4
 
@@ -1367,6 +1368,7 @@ function renderSessionItem(s, options) {
1367
1368
  textHtml += highlightMatch(s.title || "New Session", searchQuery);
1368
1369
  textSpan.innerHTML = textHtml;
1369
1370
  el.appendChild(textSpan);
1371
+ appendGithubWork(el, s, textSpan);
1370
1372
 
1371
1373
  if (itemOptions.worker) {
1372
1374
  var workerLabel = document.createElement("span");
@@ -1,3 +1,4 @@
1
+ @import url("css/session-github.css");
1
2
  @import url("css/issues.css");
2
3
  @import url("css/base.css?v=20260828-type1");
3
4
  @import url("css/icon-strip.css?v=20260828-surfaces4");
package/lib/sdk-bridge.js CHANGED
@@ -567,6 +567,17 @@ function createSDKBridge(opts) {
567
567
  return { behavior: "allow", updatedInput: input };
568
568
  }
569
569
 
570
+ // Session search is read-only; its handler enforces project and session access.
571
+ if (toolName === "search_other_driver_sessions"
572
+ || toolName === "mcp__clay-handoff__search_other_driver_sessions") {
573
+ return { behavior: "allow", updatedInput: input };
574
+ }
575
+
576
+ // Local work associations verify GitHub data but never mutate GitHub.
577
+ if (["link_session_github", "get_session_github", "unlink_session_github"].some(function (name) {
578
+ return toolName === name || toolName === "mcp__clay-github__" + name;
579
+ })) return { behavior: "allow", updatedInput: input };
580
+
570
581
  // Auto-approve safe browser MCP tools.
571
582
  // Only watch/unwatch: user explicitly chose which tab to share.
572
583
  // Everything else (screenshot, read_page, list_tabs, etc.) can expose
@@ -0,0 +1,48 @@
1
+ // Verified GitHub references owned by a work session, never inferred from mentions.
2
+ var execFile = require("child_process").execFile;
3
+ var wrapSpawnAsUser = require("./os-users").wrapSpawnAsUser;
4
+
5
+ function parseReference(value) {
6
+ var match = typeof value === "string" && value.match(/^https:\/\/github\.com\/([a-zA-Z0-9][a-zA-Z0-9-]{0,38})\/([a-zA-Z0-9][a-zA-Z0-9._-]{0,99})\/(issues|pull)\/([1-9][0-9]*)(?:[?#].*)?$/);
7
+ if (!match || !Number.isSafeInteger(Number(match[4]))) throw new Error("Use a GitHub issue or pull request URL.");
8
+ return { repository: match[1] + "/" + match[2], kind: match[3] === "pull" ? "pr" : "issue", number: Number(match[4]), url: "https://github.com/" + match[1] + "/" + match[2] + "/" + match[3] + "/" + match[4] };
9
+ }
10
+ function runGh(cwd, args, identity) {
11
+ return new Promise(function (resolve, reject) {
12
+ var env = Object.assign({}, identity ? require("./build-user-env").buildUserEnv(identity) : process.env, { GH_PROMPT_DISABLED: "1", GH_HOST: "github.com" });
13
+ if (identity) {
14
+ // Never let the daemon's GitHub credentials cross an OS-user boundary.
15
+ delete env.GH_TOKEN; delete env.GITHUB_TOKEN; delete env.GH_ENTERPRISE_TOKEN; delete env.GITHUB_ENTERPRISE_TOKEN;
16
+ delete env.GH_CONFIG_DIR; delete env.XDG_CONFIG_HOME;
17
+ env.HOME = identity.home; env.USER = identity.user; env.LOGNAME = identity.user;
18
+ }
19
+ var options = { cwd: cwd, env: env, timeout: 15000, maxBuffer: 512 * 1024, encoding: "utf8" };
20
+ if (identity) { options.uid = identity.uid; options.gid = identity.gid; }
21
+ var wrapped = wrapSpawnAsUser("gh", args, options);
22
+ execFile(wrapped.command, wrapped.args, wrapped.options, function (error, stdout) {
23
+ if (error) return reject(new Error("GitHub is unavailable. Check gh authentication and repository access."));
24
+ try { resolve(JSON.parse(stdout)); } catch (e) { reject(new Error("GitHub returned an invalid response.")); }
25
+ });
26
+ });
27
+ }
28
+ async function readReference(cwd, url, identity, run) {
29
+ var ref = parseReference(url);
30
+ var fields = "title,state,url" + (ref.kind === "pr" ? ",isDraft,headRefName,reviewDecision,statusCheckRollup" : "");
31
+ var data = await (run || runGh)(cwd, [ref.kind === "pr" ? "pr" : "issue", "view", String(ref.number), "--repo", ref.repository, "--json", fields], identity);
32
+ var verified = parseReference(data.url);
33
+ if (verified.url.toLowerCase() !== ref.url.toLowerCase()) throw new Error("GitHub returned a different reference.");
34
+ ref.title = String(data.title || "").slice(0, 200);
35
+ ref.state = data.state === "MERGED" ? "merged" : data.state === "CLOSED" ? "closed" : data.state === "OPEN" ? (data.isDraft ? "draft" : "open") : "unknown";
36
+ ref.checkedAt = Date.now();
37
+ if (ref.kind === "pr") {
38
+ ref.branch = String(data.headRefName || "").slice(0, 200);
39
+ ref.review = ["APPROVED", "CHANGES_REQUESTED", "REVIEW_REQUIRED"].indexOf(data.reviewDecision) >= 0 ? data.reviewDecision : "";
40
+ ref.checks = (data.statusCheckRollup || []).slice(0, 30).map(function (check) {
41
+ var target = check.detailsUrl || check.targetUrl || "";
42
+ if (!/^https:\/\//i.test(target)) target = "";
43
+ return { name: String(check.name || check.context || "Check").slice(0, 100), state: String(check.conclusion || check.state || check.status || "PENDING").slice(0, 40), url: target };
44
+ });
45
+ }
46
+ return ref;
47
+ }
48
+ module.exports = { parseReference: parseReference, readReference: readReference };
package/lib/sessions.js CHANGED
@@ -176,6 +176,7 @@ function createSessionManager(opts) {
176
176
  titleManuallySet: session.titleManuallySet === true,
177
177
  createdAt: session.createdAt,
178
178
  };
179
+ if (session.githubLinks) metaObj.githubLinks = session.githubLinks;
179
180
  if (session.ownerId) metaObj.ownerId = session.ownerId;
180
181
  if (session.vendor) metaObj.vendor = session.vendor;
181
182
  if (session.codexDynamicToolCatalogVersion) metaObj.codexDynamicToolCatalogVersion = session.codexDynamicToolCatalogVersion;
@@ -343,6 +344,7 @@ function createSessionManager(opts) {
343
344
  title: m.title || "",
344
345
  titleProvisional: m.titleProvisional === true,
345
346
  titleAutoGenerated: m.titleAutoGenerated === true,
347
+ githubLinks: Array.isArray(m.githubLinks) ? m.githubLinks.slice(0, 8) : [],
346
348
  titleManuallySet: m.titleManuallySet === true || legacyTitleProtected,
347
349
  createdAt: m.createdAt || Date.now(),
348
350
  lastActivity: loaded[i].mtime || m.createdAt || Date.now(),
@@ -604,6 +606,7 @@ function createSessionManager(opts) {
604
606
  id: s.localId,
605
607
  cliSessionId: s.cliSessionId || null,
606
608
  title: s.title || "New Session",
609
+ githubLinks: s.githubLinks || [],
607
610
  active: isActive,
608
611
  isProcessing: s.isProcessing,
609
612
  // Sidebar badge: user work only. Ambient housekeeping is surfaced in the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "4.4.0",
3
+ "version": "4.5.0-beta.1",
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"