clay-server 5.0.1 → 5.1.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.
- package/lib/project-file-path.js +2 -3
- package/lib/project-request-access.js +13 -1
- package/lib/public/css/filebrowser.css +10 -0
- package/lib/public/css/session-folders.css +37 -8
- package/lib/public/css/session-github.css +88 -7
- package/lib/public/modules/filebrowser.js +28 -8
- package/lib/public/modules/html-preview.js +19 -0
- package/lib/public/modules/session-date-group-header.js +3 -5
- package/lib/public/modules/session-folder-toolbar.js +6 -6
- package/lib/public/modules/session-github.js +7 -1
- package/lib/public/modules/session-presence-ui.js +1 -1
- package/lib/public/modules/sidebar-mobile.js +11 -6
- package/lib/public/modules/sidebar-session-counts.js +65 -0
- package/lib/public/modules/sidebar-session-folders-ui.js +7 -12
- package/lib/public/modules/sidebar-session-hierarchy.js +28 -17
- package/lib/public/modules/sidebar-sessions.js +14 -7
- package/lib/public/modules/split-group-helpers.js +18 -0
- package/lib/public/modules/split-view.js +25 -1
- package/package.json +1 -1
package/lib/project-file-path.js
CHANGED
|
@@ -12,9 +12,8 @@ function inside(base, target) {
|
|
|
12
12
|
return relative !== ".." && !relative.startsWith(".." + path.sep) && !path.isAbsolute(relative);
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
// Scope is server-derived for each request.
|
|
16
|
-
//
|
|
17
|
-
// Unmapped multi-user deployments retain the project and symlink boundary.
|
|
15
|
+
// Scope is server-derived for each request. File-viewer operations follow the
|
|
16
|
+
// executing OS identity within the account-derived project boundary, if any.
|
|
18
17
|
function resolveFilePath(cwd, requested, scope) {
|
|
19
18
|
if (typeof requested !== "string" || !requested || requested.indexOf("\0") !== -1) {
|
|
20
19
|
throw failure("INVALID_PATH", "Invalid file path");
|
|
@@ -60,10 +60,22 @@ function attachRequestAccess(ctx) {
|
|
|
60
60
|
return info;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
function isSoleAccount(ws) {
|
|
64
|
+
if (!users || typeof users.getAllUsers !== "function") return false;
|
|
65
|
+
try {
|
|
66
|
+
var accounts = users.getAllUsers();
|
|
67
|
+
var user = userFor(ws);
|
|
68
|
+
return !!(user && user.id && Array.isArray(accounts) && accounts.length === 1 &&
|
|
69
|
+
accounts[0] && accounts[0].id === user.id);
|
|
70
|
+
} catch (e) { return false; }
|
|
71
|
+
}
|
|
72
|
+
|
|
63
73
|
function fileScope(ws) {
|
|
64
74
|
if (!canUseFiles(ws)) throw fileFailure("FILE_FORBIDDEN", "File browser access is not permitted");
|
|
65
75
|
var identity = osIdentity(ws);
|
|
66
|
-
|
|
76
|
+
// Recheck the account roster so adding another user restores the boundary
|
|
77
|
+
// for shared installations without OS isolation, including active watches.
|
|
78
|
+
return { identity: identity, projectBound: !identity && isMultiUser() && !isSoleAccount(ws) };
|
|
67
79
|
}
|
|
68
80
|
|
|
69
81
|
return { permitMessage: permitMessage, canAccessProject: canAccessProject, hasPermission: hasPermission,
|
|
@@ -1696,6 +1696,16 @@
|
|
|
1696
1696
|
padding: 0;
|
|
1697
1697
|
}
|
|
1698
1698
|
|
|
1699
|
+
.file-viewer-html-preview {
|
|
1700
|
+
display: block;
|
|
1701
|
+
width: 100%;
|
|
1702
|
+
height: 100%;
|
|
1703
|
+
min-height: 320px;
|
|
1704
|
+
border: 0;
|
|
1705
|
+
background: #fff;
|
|
1706
|
+
color-scheme: light;
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1699
1709
|
.file-viewer-body pre {
|
|
1700
1710
|
margin: 0;
|
|
1701
1711
|
padding: 16px 20px;
|
|
@@ -30,6 +30,22 @@
|
|
|
30
30
|
background: rgba(var(--overlay-rgb), 0.05); color: var(--text-dimmer); font-size: 9.5px; font-weight: 600;
|
|
31
31
|
text-align: center; font-variant-numeric: tabular-nums;
|
|
32
32
|
}
|
|
33
|
+
.session-count-summary { position: relative; }
|
|
34
|
+
.session-count-summary::after {
|
|
35
|
+
content: attr(data-count-detail); position: absolute; right: -4px; bottom: calc(100% + 6px); z-index: 20;
|
|
36
|
+
width: max-content; max-width: min(220px, calc(100vw - 24px)); padding: 5px 7px; border: 1px solid var(--border);
|
|
37
|
+
border-radius: 7px; background: var(--bg-alt, var(--sidebar-bg)); color: var(--text); box-shadow: 0 4px 14px rgba(var(--shadow-rgb), 0.3);
|
|
38
|
+
font-size: 10.5px; font-weight: 500; line-height: 1.25; letter-spacing: 0; white-space: nowrap;
|
|
39
|
+
opacity: 0; visibility: hidden; transform: translateY(2px); pointer-events: none;
|
|
40
|
+
transition: opacity 0.12s ease, transform 0.12s ease, visibility 0.12s;
|
|
41
|
+
}
|
|
42
|
+
.session-folder-toggle:hover .session-count-summary::after,
|
|
43
|
+
.session-folder-toggle:focus-visible .session-count-summary::after,
|
|
44
|
+
.session-date-group-count:hover::after, .session-date-group-count:focus-visible::after,
|
|
45
|
+
.session-search-count:hover::after, .session-search-count:focus-visible::after {
|
|
46
|
+
opacity: 1; visibility: visible; transform: translateY(0);
|
|
47
|
+
}
|
|
48
|
+
.session-date-group-count:focus-visible, .session-search-count:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
33
49
|
|
|
34
50
|
.session-folder-body[hidden] { display: none; }
|
|
35
51
|
.session-folder-body { padding-left: 4px; border-left: 1px solid var(--border-subtle, var(--border)); margin-left: 7px; }
|
|
@@ -84,7 +100,7 @@
|
|
|
84
100
|
.session-search-count {
|
|
85
101
|
position: absolute; right: 30px; top: 50%; transform: translateY(-50%); min-width: 16px; padding: 0 5px; height: 15px; line-height: 15px;
|
|
86
102
|
border-radius: 999px; background: rgba(var(--overlay-rgb), 0.08); color: var(--text-dimmer); font-size: 9.5px; font-weight: 600; text-align: center;
|
|
87
|
-
|
|
103
|
+
cursor: help;
|
|
88
104
|
}
|
|
89
105
|
.session-search-count:empty { display: none; }
|
|
90
106
|
.session-search-clear {
|
|
@@ -162,8 +178,9 @@
|
|
|
162
178
|
.session-folder-tool svg { width: 12px; height: 12px; display: block; }
|
|
163
179
|
.session-folder-tool:hover { color: var(--text); background: rgba(var(--overlay-rgb), 0.08); }
|
|
164
180
|
|
|
165
|
-
/* The Favorites star: unfilled and revealed on hover or focus, filled
|
|
166
|
-
|
|
181
|
+
/* The Favorites star: unfilled and revealed on hover or focus, filled when
|
|
182
|
+
selected. Desktop rows reveal it in the shared trailing slot; mobile keeps
|
|
183
|
+
the selected star visible as a persistent touch affordance. */
|
|
167
184
|
.session-folder-star-btn {
|
|
168
185
|
position: absolute; right: 52px; top: 50%; transform: translateY(-50%); width: 18px; height: 18px; z-index: 1;
|
|
169
186
|
display: flex; align-items: center; justify-content: center; border: 0; border-radius: 999px; background: transparent;
|
|
@@ -172,13 +189,19 @@
|
|
|
172
189
|
.session-folder-star-btn svg, .mobile-session-star svg { width: 12px; height: 12px; display: block; }
|
|
173
190
|
.session-item:hover .session-folder-star-btn, .session-item:focus-within .session-folder-star-btn { opacity: 0.82; pointer-events: auto; }
|
|
174
191
|
.session-folder-star-btn:hover { opacity: 1; color: var(--text); background: rgba(var(--overlay-rgb), 0.08); }
|
|
175
|
-
.session-folder-star-btn.is-favorite, .mobile-session-star.is-favorite { color: var(--accent);
|
|
192
|
+
.session-folder-star-btn.is-favorite, .mobile-session-star.is-favorite { color: var(--accent); }
|
|
193
|
+
.mobile-session-star.is-favorite { opacity: 1; pointer-events: auto; }
|
|
176
194
|
.session-folder-star-btn.is-favorite svg, .mobile-session-star.is-favorite svg { fill: currentColor; }
|
|
177
195
|
.session-folder-star-btn:focus-visible, .mobile-session-star:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
|
|
178
196
|
|
|
179
|
-
/*
|
|
180
|
-
|
|
181
|
-
|
|
197
|
+
/* Age and actions share one intrinsic trailing slot. CSS grid takes the wider
|
|
198
|
+
of the two without adding their widths, so hover/focus swaps content without
|
|
199
|
+
moving the title, linked work, presence, or row controls. */
|
|
200
|
+
.session-item .session-row-trailing {
|
|
201
|
+
flex: 0 0 auto; display: grid; align-items: center; justify-items: end; min-width: 0;
|
|
202
|
+
}
|
|
203
|
+
.session-item .session-row-trailing > .session-item-age,
|
|
204
|
+
.session-item .session-row-trailing > .session-row-actions { grid-area: 1 / 1; }
|
|
182
205
|
.session-item .session-row-actions {
|
|
183
206
|
flex: 0 0 auto; display: flex; align-items: center; justify-content: flex-end; gap: 4px;
|
|
184
207
|
}
|
|
@@ -186,6 +209,9 @@
|
|
|
186
209
|
.session-item .session-row-actions > .session-close-btn {
|
|
187
210
|
position: static; flex: 0 0 18px; transform: none;
|
|
188
211
|
}
|
|
212
|
+
.session-item .session-row-trailing .session-folder-star-btn.is-favorite { opacity: 0; pointer-events: none; }
|
|
213
|
+
.session-item:hover .session-row-trailing .session-folder-star-btn.is-favorite,
|
|
214
|
+
.session-item:focus-within .session-row-trailing .session-folder-star-btn.is-favorite { opacity: 0.82; pointer-events: auto; }
|
|
189
215
|
|
|
190
216
|
/* Mobile: touch-sized targets, always-visible actions, no drag affordances. */
|
|
191
217
|
.session-folder.is-mobile .session-folder-header { min-height: 40px; margin-top: 8px; }
|
|
@@ -237,7 +263,10 @@
|
|
|
237
263
|
.session-folder-view-note { font-size: 12px; line-height: 1.45; color: var(--text-muted); }
|
|
238
264
|
|
|
239
265
|
@media (prefers-reduced-motion: reduce) {
|
|
240
|
-
.session-folder, .session-folder-chevron, .session-folder-toggle, .session-folder-tool { transition: none; }
|
|
266
|
+
.session-folder, .session-folder-chevron, .session-folder-toggle, .session-folder-tool, .session-count-summary::after { transition: none; }
|
|
267
|
+
.session-row-trailing .session-item-age,
|
|
268
|
+
.session-row-trailing .session-folder-star-btn,
|
|
269
|
+
.session-row-trailing .session-close-btn { transition: none; }
|
|
241
270
|
.session-folder-body.folder-collapse-animating { will-change: auto; }
|
|
242
271
|
}
|
|
243
272
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
.session-work-column {
|
|
3
3
|
display: flex;
|
|
4
4
|
flex-direction: column;
|
|
5
|
-
gap:
|
|
5
|
+
gap: 2px;
|
|
6
6
|
flex: 1 1 auto;
|
|
7
7
|
min-width: 0;
|
|
8
8
|
overflow: visible;
|
|
@@ -28,10 +28,71 @@
|
|
|
28
28
|
line-height: 18px;
|
|
29
29
|
}
|
|
30
30
|
.session-work-column .session-row-github-links {
|
|
31
|
-
flex-wrap:
|
|
32
|
-
gap: 3px
|
|
33
|
-
overflow:
|
|
31
|
+
flex-wrap: nowrap;
|
|
32
|
+
gap: 3px;
|
|
33
|
+
overflow: hidden;
|
|
34
|
+
line-height: 16px;
|
|
35
|
+
}
|
|
36
|
+
.session-row-github-links .session-github-link {
|
|
37
|
+
min-width: 0;
|
|
38
|
+
height: 18px;
|
|
39
|
+
box-sizing: border-box;
|
|
40
|
+
gap: 3px;
|
|
41
|
+
padding: 0 4px;
|
|
42
|
+
font-size: 10.5px;
|
|
43
|
+
}
|
|
44
|
+
.session-row-github-links .session-linear-link {
|
|
45
|
+
flex: 1 1 0;
|
|
46
|
+
min-width: 0;
|
|
47
|
+
max-width: 100%;
|
|
48
|
+
overflow: hidden;
|
|
49
|
+
}
|
|
50
|
+
.session-row-github-links:has(.github-kind-pr, .github-kind-issue) .session-linear-link { max-width: calc(100% - 76px); }
|
|
51
|
+
.linear-work-state-compact { display: none; }
|
|
52
|
+
.session-row-github-links .linear-work-id {
|
|
53
|
+
flex: 0 1 auto;
|
|
54
|
+
min-width: 0;
|
|
55
|
+
max-width: 52%;
|
|
56
|
+
overflow: hidden;
|
|
57
|
+
text-overflow: ellipsis;
|
|
58
|
+
white-space: nowrap;
|
|
59
|
+
}
|
|
60
|
+
.session-row-github-links .linear-work-state {
|
|
61
|
+
display: block;
|
|
62
|
+
flex: 1 4 0;
|
|
63
|
+
min-width: 0;
|
|
64
|
+
max-width: 52px;
|
|
65
|
+
overflow: hidden;
|
|
66
|
+
text-overflow: ellipsis;
|
|
67
|
+
white-space: nowrap;
|
|
34
68
|
}
|
|
69
|
+
.session-row-github-links .linear-work-state-full { display: none; }
|
|
70
|
+
.session-row-github-links .linear-work-state-compact {
|
|
71
|
+
display: block;
|
|
72
|
+
overflow: hidden;
|
|
73
|
+
text-overflow: ellipsis;
|
|
74
|
+
white-space: nowrap;
|
|
75
|
+
}
|
|
76
|
+
.session-row-github-links .linear-work-title {
|
|
77
|
+
flex: 1 5 0;
|
|
78
|
+
min-width: 0;
|
|
79
|
+
max-width: 34px;
|
|
80
|
+
overflow: hidden;
|
|
81
|
+
text-overflow: ellipsis;
|
|
82
|
+
white-space: nowrap;
|
|
83
|
+
}
|
|
84
|
+
.session-row-github-links .github-kind-pr,
|
|
85
|
+
.session-row-github-links .github-kind-issue { flex: 0 0 auto; }
|
|
86
|
+
.session-row-github-links .github-work-brand { display: none; }
|
|
87
|
+
.session-row-github-links .github-work-brand svg,
|
|
88
|
+
.session-row-github-links .github-work-status svg { width: 11px; height: 11px; }
|
|
89
|
+
.session-row-github-links .github-work-more { height: 18px; line-height: 16px; padding: 0 3px; font-size: 10.5px; }
|
|
90
|
+
.session-row-github-links .linear-work-brand svg { width: 11px; height: 11px; }
|
|
91
|
+
.session-row-github-links .linear-work-brand { flex: none; }
|
|
92
|
+
.session-row-github-links .session-linear-link,
|
|
93
|
+
.session-row-github-links .session-github-link,
|
|
94
|
+
.session-row-github-links .github-work-more { touch-action: manipulation; }
|
|
95
|
+
.session-row-github-links { min-height: 18px; }
|
|
35
96
|
.session-github-link {
|
|
36
97
|
display: inline-flex;
|
|
37
98
|
align-items: center;
|
|
@@ -60,10 +121,25 @@
|
|
|
60
121
|
}
|
|
61
122
|
|
|
62
123
|
.session-item:has(.session-work-column) {
|
|
124
|
+
position: relative;
|
|
125
|
+
container-type: inline-size;
|
|
63
126
|
height: auto;
|
|
64
|
-
min-height:
|
|
127
|
+
min-height: 46px;
|
|
65
128
|
contain: layout;
|
|
66
129
|
}
|
|
130
|
+
@container (max-width: 280px) {
|
|
131
|
+
.session-row-github-links .linear-work-title { display: none; }
|
|
132
|
+
}
|
|
133
|
+
.session-item:has(.session-work-column) > .session-vendor-mark,
|
|
134
|
+
.session-item:has(.session-work-column) > .session-work-column,
|
|
135
|
+
.session-item:has(.session-work-column) > .session-presence,
|
|
136
|
+
.session-item:has(.session-work-column) > .session-row-trailing { align-self: flex-start; margin-top: 5px; }
|
|
137
|
+
.session-item:has(.session-work-column) .session-row-github-links {
|
|
138
|
+
position: absolute;
|
|
139
|
+
left: 30px;
|
|
140
|
+
right: 44px;
|
|
141
|
+
bottom: 3px;
|
|
142
|
+
}
|
|
67
143
|
|
|
68
144
|
.github-kind-pr.github-state-closed .github-work-status svg { color: #f85149; }
|
|
69
145
|
@media (max-width: 600px) {
|
|
@@ -117,6 +193,11 @@
|
|
|
117
193
|
.github-work-entry-meta { color: var(--text-muted, #888); font-size: 11px; overflow-wrap: anywhere; }
|
|
118
194
|
@media (max-width: 600px) {
|
|
119
195
|
#header-github-links { max-width: 120px; }
|
|
120
|
-
.mobile-session-item:has(.session-work-column) { align-items: flex-start; }
|
|
121
|
-
.mobile-session-item .
|
|
196
|
+
.mobile-session-item:has(.session-work-column) { position: relative; container-type: inline-size; align-items: flex-start; min-height: 70px; padding-block: 8px; }
|
|
197
|
+
.mobile-session-item:has(.session-work-column) .session-work-column { position: static; }
|
|
198
|
+
.mobile-session-item:has(.session-work-column) .session-row-github-links { position: absolute; left: 44px; right: 54px; bottom: 8px; }
|
|
199
|
+
.mobile-session-item .session-row-github-links .session-github-link,
|
|
200
|
+
.mobile-session-item .session-row-github-links .github-work-more { min-height: 28px; height: 28px; }
|
|
201
|
+
.mobile-session-item .session-row-github-links .github-work-more { padding-inline: 2px; }
|
|
202
|
+
.mobile-session-item .session-row-github-links { min-height: 28px; }
|
|
122
203
|
}
|
|
@@ -13,6 +13,7 @@ import { initFileViewerTabs, openFileViewerTab, previewFileViewerTab, updateFile
|
|
|
13
13
|
import { initFileBrowserContextMenu, downloadProjectFile } from './filebrowser-context-menu.js';
|
|
14
14
|
import { registerClayFileLinkOpener } from './clay-file-links.js';
|
|
15
15
|
import { isPlantUmlFile, renderPlantUmlPreview, cancelPlantUmlPreview } from './plantuml-preview.js';
|
|
16
|
+
import { isHtmlFile, renderHtmlPreview } from './html-preview.js';
|
|
16
17
|
|
|
17
18
|
var ctx;
|
|
18
19
|
var showDropHint = function () {};
|
|
@@ -43,7 +44,7 @@ export function initFileBrowser(_ctx) {
|
|
|
43
44
|
onFocus: function(path) {
|
|
44
45
|
claimRightWorkbench("file-viewer");
|
|
45
46
|
ctx.fileViewerEl.classList.remove("hidden");
|
|
46
|
-
if (path !== currentFilePath) requestFileContent(path);
|
|
47
|
+
if (path !== currentFilePath || (store.get("fileViewerIsHtml") && isRendered && !document.querySelector(".file-viewer-html-preview"))) requestFileContent(path);
|
|
47
48
|
},
|
|
48
49
|
onEmpty: function() { teardownFileViewer(); },
|
|
49
50
|
});
|
|
@@ -194,10 +195,14 @@ export function initFileBrowser(_ctx) {
|
|
|
194
195
|
downloadProjectFile(currentFilePath);
|
|
195
196
|
});
|
|
196
197
|
|
|
197
|
-
//
|
|
198
|
+
// File preview/source toggle
|
|
198
199
|
document.getElementById("file-viewer-render").addEventListener("click", function () {
|
|
199
|
-
if (currentContent == null || (!currentIsMarkdown && !currentIsSvg && !store.get("fileViewerIsPlantUml"))) return;
|
|
200
|
+
if (currentContent == null || (!currentIsMarkdown && !currentIsSvg && !store.get("fileViewerIsPlantUml") && !store.get("fileViewerIsHtml"))) return;
|
|
200
201
|
isRendered = !isRendered;
|
|
202
|
+
if (store.get("fileViewerIsHtml")) {
|
|
203
|
+
renderHtmlBody();
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
201
206
|
if (store.get("fileViewerIsPlantUml")) {
|
|
202
207
|
renderPlantUmlBody();
|
|
203
208
|
return;
|
|
@@ -491,6 +496,8 @@ export function closeFileViewer() {
|
|
|
491
496
|
|
|
492
497
|
function teardownFileViewer() {
|
|
493
498
|
cancelPlantUmlPreview();
|
|
499
|
+
var htmlFrame = document.querySelector(".file-viewer-html-preview");
|
|
500
|
+
if (htmlFrame) htmlFrame.remove();
|
|
494
501
|
if (currentFilePath && isFollowingMarkdown(currentFilePath)) cancelMarkdownFollow();
|
|
495
502
|
sendUnwatch();
|
|
496
503
|
inlineDiffActive = false;
|
|
@@ -528,7 +535,7 @@ export function resetFileBrowser() {
|
|
|
528
535
|
isRendered = false;
|
|
529
536
|
currentIsMarkdown = false;
|
|
530
537
|
currentIsSvg = false;
|
|
531
|
-
store.set({ fileViewerIsPlantUml: false });
|
|
538
|
+
store.set({ fileViewerIsPlantUml: false, fileViewerIsHtml: false });
|
|
532
539
|
cancelPlantUmlPreview();
|
|
533
540
|
historyVisible = false;
|
|
534
541
|
currentHistoryEntries = [];
|
|
@@ -1165,7 +1172,7 @@ function showFileContent(msg, expectedRevision) {
|
|
|
1165
1172
|
currentFilePath = msg.path;
|
|
1166
1173
|
currentIsMarkdown = false;
|
|
1167
1174
|
currentIsSvg = false;
|
|
1168
|
-
store.set({ fileViewerIsPlantUml: false });
|
|
1175
|
+
store.set({ fileViewerIsPlantUml: false, fileViewerIsHtml: false });
|
|
1169
1176
|
cancelPlantUmlPreview();
|
|
1170
1177
|
if (!keepRenderState) isRendered = false;
|
|
1171
1178
|
var requestedExt = msg.path.split(".").pop().toLowerCase();
|
|
@@ -1193,19 +1200,20 @@ function showFileContent(msg, expectedRevision) {
|
|
|
1193
1200
|
lightweightPreview = (msg.size || 0) > FILE_RICH_PREVIEW_MAX_BYTES;
|
|
1194
1201
|
currentIsMarkdown = !lightweightPreview && (ext === "md" || ext === "mdx");
|
|
1195
1202
|
currentIsSvg = !lightweightPreview && ext === "svg";
|
|
1203
|
+
store.set({ fileViewerIsHtml: !lightweightPreview && isHtmlFile(msg.path) });
|
|
1196
1204
|
store.set({ fileViewerIsPlantUml: !lightweightPreview && isPlantUmlFile(msg.path) });
|
|
1197
1205
|
if (lightweightPreview) isRendered = false;
|
|
1198
1206
|
if (pendingRenderedOpen && currentIsMarkdown) isRendered = true;
|
|
1199
1207
|
|
|
1200
|
-
if (currentIsMarkdown || currentIsSvg || store.get("fileViewerIsPlantUml")) {
|
|
1208
|
+
if (currentIsMarkdown || currentIsSvg || store.get("fileViewerIsPlantUml") || store.get("fileViewerIsHtml")) {
|
|
1201
1209
|
renderBtn.classList.remove("hidden");
|
|
1202
1210
|
renderBtn.title = currentIsSvg ? "Show SVG source" : "Render markdown";
|
|
1203
|
-
copyBtn.title = store.get("fileViewerIsPlantUml") ? "Copy PlantUML source" : currentIsSvg ? "Copy SVG source" : "Copy Markdown source";
|
|
1211
|
+
copyBtn.title = store.get("fileViewerIsHtml") ? "Copy HTML source" : store.get("fileViewerIsPlantUml") ? "Copy PlantUML source" : currentIsSvg ? "Copy SVG source" : "Copy Markdown source";
|
|
1204
1212
|
} else {
|
|
1205
1213
|
copyBtn.title = "Copy contents";
|
|
1206
1214
|
}
|
|
1207
1215
|
|
|
1208
|
-
// Markdown starts as source; SVG
|
|
1216
|
+
// Markdown starts as source; HTML, SVG and diagrams start as previews.
|
|
1209
1217
|
if (lightweightPreview) {
|
|
1210
1218
|
renderLargeTextFile(bodyEl, msg.content, msg.size);
|
|
1211
1219
|
} else if (currentIsMarkdown) {
|
|
@@ -1214,6 +1222,9 @@ function showFileContent(msg, expectedRevision) {
|
|
|
1214
1222
|
? (previousContent == null ? "" : previousContent)
|
|
1215
1223
|
: null;
|
|
1216
1224
|
renderBody(transitionFrom, refreshSlideIndex);
|
|
1225
|
+
} else if (store.get("fileViewerIsHtml")) {
|
|
1226
|
+
if (!keepRenderState) isRendered = !store.get("pendingFileNavigation");
|
|
1227
|
+
renderHtmlBody();
|
|
1217
1228
|
} else if (store.get("fileViewerIsPlantUml")) {
|
|
1218
1229
|
if (!keepRenderState) isRendered = !store.get("pendingFileNavigation");
|
|
1219
1230
|
renderPlantUmlBody();
|
|
@@ -1283,6 +1294,15 @@ function showFileContent(msg, expectedRevision) {
|
|
|
1283
1294
|
}
|
|
1284
1295
|
}
|
|
1285
1296
|
|
|
1297
|
+
function renderHtmlBody() {
|
|
1298
|
+
var bodyEl = document.getElementById("file-viewer-body");
|
|
1299
|
+
var renderBtn = document.getElementById("file-viewer-render");
|
|
1300
|
+
renderBtn.classList.toggle("active", isRendered);
|
|
1301
|
+
renderBtn.title = isRendered ? "Show HTML source" : "Show HTML preview";
|
|
1302
|
+
if (isRendered) renderHtmlPreview(bodyEl, currentContent, currentFilePath);
|
|
1303
|
+
else renderCodeWithLineNumbers(bodyEl, currentContent, "html");
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1286
1306
|
function renderPlantUmlBody() {
|
|
1287
1307
|
var bodyEl = document.getElementById("file-viewer-body");
|
|
1288
1308
|
var renderBtn = document.getElementById("file-viewer-render");
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function isHtmlFile(path) {
|
|
2
|
+
return /\.html?$/i.test(String(path || ""));
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function renderHtmlPreview(body, source, path) {
|
|
6
|
+
var frame = document.createElement("iframe");
|
|
7
|
+
frame.className = "file-viewer-html-preview";
|
|
8
|
+
frame.title = "HTML preview of " + path;
|
|
9
|
+
// Scripts can drive the preview, but cannot access Clay's document or cookies.
|
|
10
|
+
frame.setAttribute("sandbox", "allow-scripts");
|
|
11
|
+
frame.setAttribute("referrerpolicy", "no-referrer");
|
|
12
|
+
var doc = new DOMParser().parseFromString(source, "text/html");
|
|
13
|
+
var policy = doc.createElement("meta");
|
|
14
|
+
policy.httpEquiv = "Content-Security-Policy";
|
|
15
|
+
policy.content = "default-src 'none'; script-src 'unsafe-inline' https: http:; style-src 'unsafe-inline' https: http:; img-src data: blob: https: http:; font-src data: https: http:; media-src data: blob: https: http:; connect-src 'none'; frame-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'";
|
|
16
|
+
doc.head.prepend(policy);
|
|
17
|
+
frame.srcdoc = "<!doctype html>\n" + doc.documentElement.outerHTML;
|
|
18
|
+
body.replaceChildren(frame);
|
|
19
|
+
}
|
|
@@ -3,6 +3,7 @@ import { getWs } from './ws-ref.js';
|
|
|
3
3
|
import { showConfirm } from './app-misc.js';
|
|
4
4
|
import { describeGroupDelete } from './session-delete-scope.js';
|
|
5
5
|
import { showToast } from './utils.js';
|
|
6
|
+
import { renderSidebarCount } from './sidebar-session-counts.js';
|
|
6
7
|
|
|
7
8
|
function confirmClear(label, ids, sessions) {
|
|
8
9
|
if (!ids.length) return;
|
|
@@ -22,7 +23,7 @@ function confirmClear(label, ids, sessions) {
|
|
|
22
23
|
});
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
export function renderDateGroupHeader(dateLabel, folderLabel, ids, sessions, surface, searching) {
|
|
26
|
+
export function renderDateGroupHeader(dateLabel, folderLabel, ids, sessions, surface, searching, displayCounts) {
|
|
26
27
|
var header = document.createElement("div");
|
|
27
28
|
header.className = "session-group-header session-date-group-header" + (surface === "mobile" ? " is-mobile" : "");
|
|
28
29
|
header.setAttribute("role", "heading");
|
|
@@ -33,10 +34,7 @@ export function renderDateGroupHeader(dateLabel, folderLabel, ids, sessions, sur
|
|
|
33
34
|
label.textContent = dateLabel;
|
|
34
35
|
header.appendChild(label);
|
|
35
36
|
|
|
36
|
-
var count =
|
|
37
|
-
count.className = "session-date-group-count";
|
|
38
|
-
count.textContent = String(ids.length);
|
|
39
|
-
count.setAttribute("aria-label", ids.length + (ids.length === 1 ? " session" : " sessions"));
|
|
37
|
+
var count = renderSidebarCount("session-date-group-count", displayCounts || { roots: ids.length, workers: 0 }, true);
|
|
40
38
|
header.appendChild(count);
|
|
41
39
|
|
|
42
40
|
if ((!store.get('permissions') || store.get('permissions').sessionDelete !== false) && ids.length) {
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
openListSearch, closeListSearch, clearListSearchText, setListSearchQuery, rememberListSearchCaret,
|
|
14
14
|
getListSearchQuery, getListSearchMatchIds, listSearchNeedsRender
|
|
15
15
|
} from './session-list-search.js';
|
|
16
|
+
import { renderSidebarCount } from './sidebar-session-counts.js';
|
|
16
17
|
|
|
17
18
|
// Visible one wins: the desktop list and the mobile sheet can both be in the DOM.
|
|
18
19
|
function visible(selector) {
|
|
@@ -44,7 +45,7 @@ export function folderToolbarNeedsRender(state, previous) {
|
|
|
44
45
|
return folderDraftNeedsRender(state, previous) || listSearchNeedsRender(state, previous);
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
function renderSearchBar(bar, surface) {
|
|
48
|
+
function renderSearchBar(bar, surface, displayCounts) {
|
|
48
49
|
var state = store.get('sessionSearch');
|
|
49
50
|
var query = getListSearchQuery();
|
|
50
51
|
var matches = getListSearchMatchIds();
|
|
@@ -92,9 +93,8 @@ function renderSearchBar(bar, surface) {
|
|
|
92
93
|
}, 0);
|
|
93
94
|
});
|
|
94
95
|
|
|
95
|
-
var count =
|
|
96
|
-
count.
|
|
97
|
-
if (query && matches !== null) count.textContent = String(matches.size);
|
|
96
|
+
var count = renderSidebarCount("session-search-count", displayCounts || { roots: matches ? matches.size : 0, workers: 0 }, true);
|
|
97
|
+
if (!query || matches === null) count.textContent = "";
|
|
98
98
|
var clear = document.createElement("button");
|
|
99
99
|
clear.type = "button";
|
|
100
100
|
clear.className = "session-search-clear";
|
|
@@ -130,7 +130,7 @@ function renderSearchBar(bar, surface) {
|
|
|
130
130
|
});
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
export function renderFolderToolbar(surface) {
|
|
133
|
+
export function renderFolderToolbar(surface, displayCounts) {
|
|
134
134
|
var create = store.get('sessionFolderCreate');
|
|
135
135
|
var search = store.get('sessionSearch');
|
|
136
136
|
var wrap = document.createElement("div");
|
|
@@ -140,7 +140,7 @@ export function renderFolderToolbar(surface) {
|
|
|
140
140
|
wrap.appendChild(bar);
|
|
141
141
|
|
|
142
142
|
if (search && search.open) {
|
|
143
|
-
renderSearchBar(bar, surface);
|
|
143
|
+
renderSearchBar(bar, surface, displayCounts);
|
|
144
144
|
return wrap;
|
|
145
145
|
}
|
|
146
146
|
|
|
@@ -21,6 +21,12 @@ function safeLinks(session) {
|
|
|
21
21
|
function label(link) {
|
|
22
22
|
return (link.kind === 'pr' ? 'PR #' : 'Issue #') + link.number;
|
|
23
23
|
}
|
|
24
|
+
function compactLinearState(state) {
|
|
25
|
+
var value = String(state || '');
|
|
26
|
+
if (/^in progress$/i.test(value)) return 'Active';
|
|
27
|
+
if (/^cancell?ed$/i.test(value)) return 'Canceled';
|
|
28
|
+
return value.length > 9 ? value.slice(0, 8) + '\u2026' : value;
|
|
29
|
+
}
|
|
24
30
|
function anchor(url, title, content, className) {
|
|
25
31
|
return '<a class="' + className + '" href="' + escapeHtml(url) + '" target="_blank" rel="noopener noreferrer" title="' + escapeHtml(title) + '" aria-label="' + escapeHtml(title) + '">' + content + '</a>';
|
|
26
32
|
}
|
|
@@ -43,7 +49,7 @@ export function githubWorkMarkup(session, detailed, expanded, sessionRow) {
|
|
|
43
49
|
return '<span class="session-github-links' + (detailed ? ' github-work-details' : '') + (sessionRow ? ' session-row-github-links' : '') + '">' + displayed.map(function (link) {
|
|
44
50
|
if (link.provider === 'linear') {
|
|
45
51
|
var description = link.identifier + ' · ' + link.title + ' · ' + link.stateName + (link.stale ? ' · Last known status' : '');
|
|
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');
|
|
52
|
+
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"><span class="linear-work-state-full">' + escapeHtml(link.stateName) + '</span><span class="linear-work-state-compact" aria-hidden="true">' + escapeHtml(compactLinearState(link.stateName)) + '</span></span>', 'session-github-link session-linear-link');
|
|
47
53
|
}
|
|
48
54
|
var state = ['open', 'closed', 'merged', 'draft'].indexOf(link.state) >= 0 ? link.state : 'unknown';
|
|
49
55
|
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');
|
|
@@ -38,7 +38,7 @@ export function renderSessionPresence(row, sessionId) {
|
|
|
38
38
|
container.appendChild(more);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
var anchor = row.querySelector(".session-item-age, .mobile-worker-generation, .mobile-session-unread, .session-row-actions, .mobile-session-star");
|
|
41
|
+
var anchor = row.querySelector(".session-row-trailing, .session-item-age, .mobile-worker-generation, .mobile-session-unread, .session-row-actions, .mobile-session-star");
|
|
42
42
|
row.insertBefore(container, anchor || null);
|
|
43
43
|
return container;
|
|
44
44
|
}
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
import { store } from './store.js';
|
|
35
35
|
import { getWs } from './ws-ref.js';
|
|
36
36
|
import { dismissOverlayPanels, closeSidebar } from './sidebar.js';
|
|
37
|
+
import { openWorkerWithDriver } from './split-view.js';
|
|
37
38
|
import { switchProject, getCachedProjects } from './app-projects.js';
|
|
38
39
|
import { showHomeHub } from './app-home-hub.js';
|
|
39
40
|
import { openHomeChat } from './home-mate-chat.js';
|
|
@@ -45,6 +46,7 @@ import {
|
|
|
45
46
|
renderMobileDriverHierarchy,
|
|
46
47
|
renderMobileOrphanHierarchy
|
|
47
48
|
} from './sidebar-session-hierarchy.js';
|
|
49
|
+
import { summarizeSidebarItems } from './sidebar-session-counts.js';
|
|
48
50
|
import { cancelSessionInlineRename, closeSessionContextMenu } from './session-context-menu.js';
|
|
49
51
|
|
|
50
52
|
// --- Mobile state ---
|
|
@@ -443,15 +445,17 @@ function createMobileSessionItem(s, options) {
|
|
|
443
445
|
openSessionContextMenu(el, s);
|
|
444
446
|
});
|
|
445
447
|
|
|
446
|
-
(function (
|
|
448
|
+
(function (session, worker) {
|
|
447
449
|
el.addEventListener("click", function () {
|
|
448
450
|
if (getWs() && store.get('connected')) {
|
|
449
|
-
|
|
451
|
+
if (!worker || !openWorkerWithDriver(session)) {
|
|
452
|
+
getWs().send(JSON.stringify({ type: "switch_session", id: session.id }));
|
|
453
|
+
}
|
|
450
454
|
}
|
|
451
455
|
if (dismissOverlayPanels) dismissOverlayPanels();
|
|
452
456
|
closeMobileSheet();
|
|
453
457
|
});
|
|
454
|
-
})(s.
|
|
458
|
+
})(s, !!itemOptions.worker);
|
|
455
459
|
|
|
456
460
|
return el;
|
|
457
461
|
}
|
|
@@ -745,15 +749,16 @@ export function renderMobileSessionsInto(container) {
|
|
|
745
749
|
var folderCtx = {
|
|
746
750
|
surface: "mobile",
|
|
747
751
|
searching: matchIds !== null,
|
|
752
|
+
matchIds: matchIds,
|
|
748
753
|
sessions: getCachedSessions(),
|
|
749
754
|
renderItem: function (it) {
|
|
750
755
|
if (it.type === "loop") return createMobileLoopGroup(it.loopId, it.children, it.groupKey);
|
|
751
|
-
if (it.type === "driver-hierarchy") return renderMobileDriverHierarchy(it.root, createMobileSessionItem, refreshMobileChatSheet);
|
|
752
|
-
if (it.type === "orphan-workers") return renderMobileOrphanHierarchy(it.workers, createMobileSessionItem, refreshMobileChatSheet);
|
|
756
|
+
if (it.type === "driver-hierarchy") return renderMobileDriverHierarchy(it.root, createMobileSessionItem, refreshMobileChatSheet, matchIds);
|
|
757
|
+
if (it.type === "orphan-workers") return renderMobileOrphanHierarchy(it.workers, createMobileSessionItem, refreshMobileChatSheet, matchIds);
|
|
753
758
|
return createMobileSessionItem(it.data);
|
|
754
759
|
},
|
|
755
760
|
};
|
|
756
|
-
container.appendChild(renderFolderToolbar("mobile"));
|
|
761
|
+
container.appendChild(renderFolderToolbar("mobile", summarizeSidebarItems(filterItemsBySearch(items, matchIds), matchIds)));
|
|
757
762
|
container.appendChild(renderFavoritesSection(layout, folderCtx));
|
|
758
763
|
renderOrdinarySections(layout, folderCtx, container);
|
|
759
764
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Display-only sidebar counts. Destructive Clear actions keep their separate
|
|
2
|
+
// authoritative session-id scope and cascade semantics.
|
|
3
|
+
|
|
4
|
+
function sessionId(session) {
|
|
5
|
+
return session && typeof session.id === "number" ? session.id : null;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function addSession(session, roots, workers) {
|
|
9
|
+
var id = sessionId(session);
|
|
10
|
+
if (id === null) return;
|
|
11
|
+
if (session.sessionRole === "worker") workers.add(id);
|
|
12
|
+
else roots.add(id);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function visibleWorker(worker, driver, matchIds) {
|
|
16
|
+
return matchIds === null || matchIds === undefined || matchIds.has(worker.id) || matchIds.has(driver.id);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function addItem(item, roots, workers, matchIds) {
|
|
20
|
+
if (!item) return;
|
|
21
|
+
if (item.type === "session") {
|
|
22
|
+
addSession(item.data, roots, workers);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (item.type === "driver-hierarchy") {
|
|
26
|
+
addSession(item.root && item.root.driver, roots, workers);
|
|
27
|
+
var children = item.root && item.root.workers || [];
|
|
28
|
+
for (var i = 0; i < children.length; i++) if (visibleWorker(children[i], item.root.driver, matchIds)) addSession(children[i], roots, workers);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
var sessions = item.type === "orphan-workers" ? item.workers : item.type === "split-group" ? item.members : item.type === "loop" ? item.children : [];
|
|
32
|
+
for (var j = 0; j < (sessions || []).length; j++) {
|
|
33
|
+
if (item.type === "orphan-workers" && matchIds !== null && matchIds !== undefined && !matchIds.has(sessions[j].id)) continue;
|
|
34
|
+
addSession(sessions[j], roots, workers);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function summarizeSidebarItems(items, matchIds) {
|
|
39
|
+
var roots = new Set();
|
|
40
|
+
var workers = new Set();
|
|
41
|
+
for (var i = 0; i < (items || []).length; i++) addItem(items[i], roots, workers, matchIds);
|
|
42
|
+
return { roots: roots.size, workers: workers.size };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function summarizeSidebarUnits(units, matchIds) {
|
|
46
|
+
return summarizeSidebarItems((units || []).map(function (unit) { return unit.item; }), matchIds);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function sidebarCountLabel(counts, short) {
|
|
50
|
+
var roots = counts && counts.roots || 0;
|
|
51
|
+
var workers = counts && counts.workers || 0;
|
|
52
|
+
if (short) return roots + (roots === 1 ? " Driver" : " Drivers") + " · " + workers + (workers === 1 ? " Split Worker" : " Split Workers");
|
|
53
|
+
return roots + (roots === 1 ? " Driver session" : " Driver sessions") + " and " + workers + (workers === 1 ? " Split Worker session" : " Split Worker sessions");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function renderSidebarCount(className, counts, focusable) {
|
|
57
|
+
var count = document.createElement("span");
|
|
58
|
+
count.className = className + " session-count-summary";
|
|
59
|
+
count.textContent = String(counts.roots);
|
|
60
|
+
count.dataset.countDetail = sidebarCountLabel(counts, true);
|
|
61
|
+
count.setAttribute("aria-label", sidebarCountLabel(counts, false));
|
|
62
|
+
count.title = sidebarCountLabel(counts, false);
|
|
63
|
+
if (focusable) count.tabIndex = 0;
|
|
64
|
+
return count;
|
|
65
|
+
}
|
|
@@ -22,6 +22,7 @@ import { renderNewSessionButton, renderCreateRow } from './session-create-form.j
|
|
|
22
22
|
import { restoreFolderCollapse, toggleFolderCollapse } from './session-folder-collapse.js';
|
|
23
23
|
import { groupUnitsByDate, groupRootSessionIds } from './session-date-groups.js';
|
|
24
24
|
import { renderDateGroupHeader } from './session-date-group-header.js';
|
|
25
|
+
import { summarizeSidebarUnits, sidebarCountLabel, renderSidebarCount } from './sidebar-session-counts.js';
|
|
25
26
|
|
|
26
27
|
// Mutable UI state (open menu, drag in flight, the layout each surface last
|
|
27
28
|
// rendered) lives in the store; this module only owns the functions.
|
|
@@ -269,15 +270,6 @@ function wireUnitDrop(wrapper, containerId, key) {
|
|
|
269
270
|
|
|
270
271
|
// --- Rendering ---
|
|
271
272
|
|
|
272
|
-
function countUnits(units) {
|
|
273
|
-
var total = 0;
|
|
274
|
-
for (var i = 0; i < units.length; i++) {
|
|
275
|
-
var item = units[i].item;
|
|
276
|
-
total += item.type === "driver-hierarchy" ? 1 + item.root.workers.length : 1;
|
|
277
|
-
}
|
|
278
|
-
return total;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
273
|
function renderUnit(unit, containerId, ctx) {
|
|
282
274
|
var wrapper = document.createElement("div");
|
|
283
275
|
wrapper.className = "session-folder-unit";
|
|
@@ -317,14 +309,17 @@ function renderSection(section, ctx, kind) {
|
|
|
317
309
|
toggle.className = "session-folder-toggle";
|
|
318
310
|
toggle.setAttribute("aria-expanded", String(!section.collapsed));
|
|
319
311
|
toggle.setAttribute("aria-controls", bodyId);
|
|
312
|
+
var counts = summarizeSidebarUnits(section.units, ctx.matchIds);
|
|
313
|
+
toggle.setAttribute("aria-label", (section.collapsed ? "Expand " : "Collapse ") + section.label + ". " + sidebarCountLabel(counts, false));
|
|
320
314
|
var icon = isFavorites ? "star" : (kind === "unfiled" ? "inbox" : "folder");
|
|
321
315
|
toggle.innerHTML = '<span class="session-folder-chevron">' + iconHtml("chevron-right") + "</span>" +
|
|
322
316
|
'<span class="session-folder-icon">' + iconHtml(icon) + "</span>" +
|
|
323
|
-
'<span class="session-folder-label">' + escapeHtml(section.label) + "</span>"
|
|
324
|
-
|
|
317
|
+
'<span class="session-folder-label">' + escapeHtml(section.label) + "</span>";
|
|
318
|
+
toggle.appendChild(renderSidebarCount("session-folder-count", counts, false));
|
|
325
319
|
toggle.addEventListener("click", function () {
|
|
326
320
|
if (consumeFolderClickSuppress()) return;
|
|
327
321
|
toggleFolderCollapse(root, body, toggle, surface, section.id, function (collapsed) {
|
|
322
|
+
toggle.setAttribute("aria-label", (collapsed ? "Expand " : "Collapse ") + section.label + ". " + sidebarCountLabel(counts, false));
|
|
328
323
|
sendFolderOp({ op: "set_collapsed", containerKey: section.id, collapsed: collapsed });
|
|
329
324
|
});
|
|
330
325
|
});
|
|
@@ -362,7 +357,7 @@ function renderSection(section, ctx, kind) {
|
|
|
362
357
|
if (dateGroups) {
|
|
363
358
|
for (var g = 0; g < dateGroups.length; g++) {
|
|
364
359
|
var ids = groupRootSessionIds(dateGroups[g].units);
|
|
365
|
-
body.appendChild(renderDateGroupHeader(dateGroups[g].label, section.label, ids, ctx.sessions || [], surface, !!ctx.searching));
|
|
360
|
+
body.appendChild(renderDateGroupHeader(dateGroups[g].label, section.label, ids, ctx.sessions || [], surface, !!ctx.searching, summarizeSidebarUnits(dateGroups[g].units, ctx.matchIds)));
|
|
366
361
|
for (var gu = 0; gu < dateGroups[g].units.length; gu++) body.appendChild(renderUnit(dateGroups[g].units[gu], section.id, ctx));
|
|
367
362
|
}
|
|
368
363
|
} else {
|
|
@@ -32,7 +32,15 @@ export function currentWorkerIds() {
|
|
|
32
32
|
var result = new Set();
|
|
33
33
|
var groups = store.get('splitGroups') || [];
|
|
34
34
|
for (var i = 0; i < groups.length; i++) {
|
|
35
|
-
|
|
35
|
+
var pair = groups[i].pair;
|
|
36
|
+
if (!pair) continue;
|
|
37
|
+
if (Array.isArray(pair.workerIds)) {
|
|
38
|
+
for (var j = 0; j < pair.workerIds.length; j++) {
|
|
39
|
+
if (Number.isInteger(pair.workerIds[j])) result.add(pair.workerIds[j]);
|
|
40
|
+
}
|
|
41
|
+
} else if (Number.isInteger(pair.workerId)) {
|
|
42
|
+
result.add(pair.workerId);
|
|
43
|
+
}
|
|
36
44
|
}
|
|
37
45
|
return result;
|
|
38
46
|
}
|
|
@@ -55,22 +63,23 @@ export function hierarchyItemMatches(item, matchIds) {
|
|
|
55
63
|
return true;
|
|
56
64
|
}
|
|
57
65
|
|
|
58
|
-
function
|
|
66
|
+
function expansionKey(key) {
|
|
67
|
+
return String(store.get('currentSlug') || '') + ":" + key;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function expanded(surface, key) {
|
|
59
71
|
var state = expansionBySurface[surface];
|
|
60
|
-
|
|
61
|
-
|
|
72
|
+
var scopedKey = expansionKey(key);
|
|
73
|
+
if (state.has(scopedKey)) return state.get(scopedKey);
|
|
74
|
+
return defaultHierarchyExpanded();
|
|
62
75
|
}
|
|
63
76
|
|
|
64
|
-
export function defaultHierarchyExpanded(
|
|
65
|
-
var currentIds = current || new Set();
|
|
66
|
-
for (var i = 0; i < workers.length; i++) {
|
|
67
|
-
if (workers[i].active || currentIds.has(workers[i].id) || (matchIds !== null && matchIds.has(workers[i].id))) return true;
|
|
68
|
-
}
|
|
77
|
+
export function defaultHierarchyExpanded() {
|
|
69
78
|
return false;
|
|
70
79
|
}
|
|
71
80
|
|
|
72
81
|
function toggle(surface, key, wasExpanded, rerender) {
|
|
73
|
-
expansionBySurface[surface].set(key, !wasExpanded);
|
|
82
|
+
expansionBySurface[surface].set(expansionKey(key), !wasExpanded);
|
|
74
83
|
rerender();
|
|
75
84
|
}
|
|
76
85
|
|
|
@@ -82,7 +91,7 @@ export function renderDesktopDriverHierarchy(root, renderSession, rerender, matc
|
|
|
82
91
|
var wrapper = document.createElement("div");
|
|
83
92
|
wrapper.className = "session-driver-hierarchy";
|
|
84
93
|
var key = "driver:" + root.driver.id;
|
|
85
|
-
var isExpanded = expanded("desktop", key
|
|
94
|
+
var isExpanded = expanded("desktop", key);
|
|
86
95
|
var childrenId = "session-workers-" + root.driver.id;
|
|
87
96
|
var header = document.createElement("div");
|
|
88
97
|
header.className = "session-driver-header";
|
|
@@ -105,7 +114,7 @@ export function renderDesktopDriverHierarchy(root, renderSession, rerender, matc
|
|
|
105
114
|
event.preventDefault();
|
|
106
115
|
event.stopPropagation();
|
|
107
116
|
var nextExpanded = !isExpanded;
|
|
108
|
-
expansionBySurface.desktop.set(key, nextExpanded);
|
|
117
|
+
expansionBySurface.desktop.set(expansionKey(key), nextExpanded);
|
|
109
118
|
control.setAttribute("aria-expanded", String(nextExpanded));
|
|
110
119
|
control.setAttribute("aria-label", (nextExpanded ? "Collapse" : "Expand") + " Workers for " + (root.driver.title || "New Session"));
|
|
111
120
|
children.hidden = !nextExpanded;
|
|
@@ -127,7 +136,7 @@ export function renderDesktopDriverHierarchy(root, renderSession, rerender, matc
|
|
|
127
136
|
export function renderDesktopOrphanHierarchy(workers, renderSession, rerender, matchIds) {
|
|
128
137
|
var wrapper = document.createElement("div");
|
|
129
138
|
wrapper.className = "session-driver-hierarchy session-orphan-hierarchy";
|
|
130
|
-
var isExpanded = expanded("desktop", "orphan"
|
|
139
|
+
var isExpanded = expanded("desktop", "orphan");
|
|
131
140
|
var control = document.createElement("button");
|
|
132
141
|
control.type = "button";
|
|
133
142
|
control.className = "session-orphan-toggle";
|
|
@@ -152,11 +161,11 @@ export function renderDesktopOrphanHierarchy(workers, renderSession, rerender, m
|
|
|
152
161
|
return wrapper;
|
|
153
162
|
}
|
|
154
163
|
|
|
155
|
-
export function renderMobileDriverHierarchy(root, renderSession, rerender) {
|
|
164
|
+
export function renderMobileDriverHierarchy(root, renderSession, rerender, matchIds) {
|
|
156
165
|
var wrapper = document.createElement("div");
|
|
157
166
|
wrapper.className = "mobile-driver-hierarchy";
|
|
158
167
|
var key = "driver:" + root.driver.id;
|
|
159
|
-
var isExpanded = expanded("mobile", key
|
|
168
|
+
var isExpanded = expanded("mobile", key);
|
|
160
169
|
var childrenId = "mobile-session-workers-" + root.driver.id;
|
|
161
170
|
var header = document.createElement("div");
|
|
162
171
|
header.className = "mobile-driver-header";
|
|
@@ -180,16 +189,17 @@ export function renderMobileDriverHierarchy(root, renderSession, rerender) {
|
|
|
180
189
|
children.hidden = !isExpanded;
|
|
181
190
|
var current = currentWorkerIds();
|
|
182
191
|
for (var i = 0; i < root.workers.length; i++) {
|
|
192
|
+
if (!visibleWorker(root.workers[i], root.driver, matchIds === undefined ? null : matchIds)) continue;
|
|
183
193
|
children.appendChild(renderSession(root.workers[i], { worker: true, current: current.has(root.workers[i].id) }));
|
|
184
194
|
}
|
|
185
195
|
wrapper.appendChild(children);
|
|
186
196
|
return wrapper;
|
|
187
197
|
}
|
|
188
198
|
|
|
189
|
-
export function renderMobileOrphanHierarchy(workers, renderSession, rerender) {
|
|
199
|
+
export function renderMobileOrphanHierarchy(workers, renderSession, rerender, matchIds) {
|
|
190
200
|
var wrapper = document.createElement("div");
|
|
191
201
|
wrapper.className = "mobile-driver-hierarchy mobile-orphan-hierarchy";
|
|
192
|
-
var isExpanded = expanded("mobile", "orphan"
|
|
202
|
+
var isExpanded = expanded("mobile", "orphan");
|
|
193
203
|
var control = document.createElement("button");
|
|
194
204
|
control.type = "button";
|
|
195
205
|
control.className = "mobile-orphan-toggle";
|
|
@@ -207,6 +217,7 @@ export function renderMobileOrphanHierarchy(workers, renderSession, rerender) {
|
|
|
207
217
|
children.hidden = !isExpanded;
|
|
208
218
|
var current = currentWorkerIds();
|
|
209
219
|
for (var i = 0; i < workers.length; i++) {
|
|
220
|
+
if (matchIds !== null && matchIds !== undefined && !matchIds.has(workers[i].id)) continue;
|
|
210
221
|
children.appendChild(renderSession(workers[i], { worker: true, current: current.has(workers[i].id) }));
|
|
211
222
|
}
|
|
212
223
|
wrapper.appendChild(children);
|
|
@@ -13,7 +13,7 @@ import { showConfirm } from './app-misc.js';
|
|
|
13
13
|
import { getUpcomingSchedules } from './scheduler.js';
|
|
14
14
|
import { refreshMobileChatSheet } from './sidebar-mobile.js';
|
|
15
15
|
import { VENDOR_AVATARS, VENDOR_NAMES } from './app-rendering.js';
|
|
16
|
-
import { openGroup, separateGroup } from './split-view.js';
|
|
16
|
+
import { openGroup, openWorkerWithDriver, separateGroup } from './split-view.js';
|
|
17
17
|
import { groupedSessionIds } from './split-group-helpers.js';
|
|
18
18
|
import { resolvePreferredVendor } from './default-vendor.js';
|
|
19
19
|
import {
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
renderDesktopDriverHierarchy,
|
|
32
32
|
renderDesktopOrphanHierarchy
|
|
33
33
|
} from './sidebar-session-hierarchy.js';
|
|
34
|
+
import { summarizeSidebarItems } from './sidebar-session-counts.js';
|
|
34
35
|
import {
|
|
35
36
|
cancelSessionInlineRename,
|
|
36
37
|
closeSessionContextMenu,
|
|
@@ -841,12 +842,14 @@ function renderSessionItem(s, options) {
|
|
|
841
842
|
el.appendChild(workerLabel);
|
|
842
843
|
}
|
|
843
844
|
|
|
845
|
+
var trailing = document.createElement("span");
|
|
846
|
+
trailing.className = "session-row-trailing";
|
|
844
847
|
var ageText = relativeTime(s.lastActivity || 0);
|
|
845
848
|
if (ageText) {
|
|
846
849
|
var age = document.createElement("span");
|
|
847
850
|
age.className = "session-item-age";
|
|
848
851
|
age.textContent = ageText;
|
|
849
|
-
|
|
852
|
+
trailing.appendChild(age);
|
|
850
853
|
}
|
|
851
854
|
|
|
852
855
|
// Right-click / long-press: context menu
|
|
@@ -874,13 +877,16 @@ function renderSessionItem(s, options) {
|
|
|
874
877
|
if (isFavoriteSession(s)) el.classList.add("is-favorite");
|
|
875
878
|
}
|
|
876
879
|
appendSessionCloseButton(actions, s);
|
|
877
|
-
if (actions.childNodes.length)
|
|
880
|
+
if (actions.childNodes.length) trailing.appendChild(actions);
|
|
881
|
+
if (trailing.childNodes.length) el.appendChild(trailing);
|
|
878
882
|
|
|
879
|
-
el.addEventListener("click", (function (
|
|
883
|
+
el.addEventListener("click", (function (session, worker) {
|
|
880
884
|
return function () {
|
|
881
885
|
if (getWs() && store.get('connected')) {
|
|
882
886
|
var pendingQuery = getListSearchQuery() || "";
|
|
883
|
-
|
|
887
|
+
if (!worker || !openWorkerWithDriver(session)) {
|
|
888
|
+
getWs().send(JSON.stringify({ type: "switch_session", id: session.id }));
|
|
889
|
+
}
|
|
884
890
|
dismissOverlayPanels();
|
|
885
891
|
closeSidebar();
|
|
886
892
|
if (pendingQuery) {
|
|
@@ -888,7 +894,7 @@ function renderSessionItem(s, options) {
|
|
|
888
894
|
}
|
|
889
895
|
}
|
|
890
896
|
};
|
|
891
|
-
})(s.
|
|
897
|
+
})(s, !!itemOptions.worker));
|
|
892
898
|
|
|
893
899
|
// Presence avatars (multi-user)
|
|
894
900
|
renderSessionPresence(el, String(s.id));
|
|
@@ -1071,6 +1077,7 @@ export function renderSessionList(sessions) {
|
|
|
1071
1077
|
var folderCtx = {
|
|
1072
1078
|
surface: "desktop",
|
|
1073
1079
|
searching: matchIds !== null,
|
|
1080
|
+
matchIds: matchIds,
|
|
1074
1081
|
sessions: cachedSessions,
|
|
1075
1082
|
renderItem: function (it) {
|
|
1076
1083
|
if (it.type === "loop") return renderLoopGroup(it.loopId, it.children, it.groupKey) || document.createElement("div");
|
|
@@ -1085,7 +1092,7 @@ export function renderSessionList(sessions) {
|
|
|
1085
1092
|
// custom folders, then Unfiled. Each header has its own New session button.
|
|
1086
1093
|
var topActions = document.createElement("div");
|
|
1087
1094
|
topActions.className = "session-list-sticky-top";
|
|
1088
|
-
topActions.appendChild(renderFolderToolbar("desktop"));
|
|
1095
|
+
topActions.appendChild(renderFolderToolbar("desktop", summarizeSidebarItems(visibleItems, matchIds)));
|
|
1089
1096
|
getSessionListEl().appendChild(topActions);
|
|
1090
1097
|
|
|
1091
1098
|
var regularContainer = document.createElement("div");
|
|
@@ -72,6 +72,24 @@ export function isConfiguredWorker(groups, sessionId) {
|
|
|
72
72
|
return false;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
export function resolveWorkerSplitTarget(worker, sessions, groups) {
|
|
76
|
+
if (!worker || worker.sessionRole !== "worker" || !worker.parentAvailable || !Number.isInteger(worker.parentSessionId)) return null;
|
|
77
|
+
var list = sessions || [];
|
|
78
|
+
var driver = null;
|
|
79
|
+
for (var i = 0; i < list.length; i++) {
|
|
80
|
+
if (list[i].id === worker.parentSessionId && list[i].sessionRole !== "worker") driver = list[i];
|
|
81
|
+
}
|
|
82
|
+
if (!driver) return null;
|
|
83
|
+
var groupList = groups || [];
|
|
84
|
+
for (var j = 0; j < groupList.length; j++) {
|
|
85
|
+
var roles = splitGroupRoles(groupList[j]);
|
|
86
|
+
if (roles && roles.driverId === driver.id && roles.workerIds.indexOf(worker.id) !== -1) {
|
|
87
|
+
return { driver: driver, worker: worker, group: groupList[j] };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return { driver: driver, worker: worker, group: null };
|
|
91
|
+
}
|
|
92
|
+
|
|
75
93
|
export function splitWorkerCloseRequest(group, sessions, projectSlug, workerId, requestId) {
|
|
76
94
|
var roles = splitGroupRoles(group);
|
|
77
95
|
if (!roles || roles.version !== 2 || roles.workerIds.indexOf(workerId) === -1) return null;
|
|
@@ -6,7 +6,7 @@ import { getWs } from './ws-ref.js';
|
|
|
6
6
|
import { getCachedSessions } from './sidebar-sessions.js';
|
|
7
7
|
import { refreshIcons } from './icons.js';
|
|
8
8
|
import { detachTuiView } from './session-tui-view.js';
|
|
9
|
-
import { groupedSessionIds, findSplitGroup, isConfiguredWorker, splitGroupRoles, splitGroupMemberIds, splitGroupActiveAnchor, splitWorkerCloseRequest } from './split-group-helpers.js';
|
|
9
|
+
import { groupedSessionIds, findSplitGroup, isConfiguredWorker, resolveWorkerSplitTarget, splitGroupRoles, splitGroupMemberIds, splitGroupActiveAnchor, splitWorkerCloseRequest } from './split-group-helpers.js';
|
|
10
10
|
import { syncPairChrome } from './split-pair-ui.js';
|
|
11
11
|
import { presentMarkdownEdit } from './filebrowser.js';
|
|
12
12
|
import { autoStartLoginIfNeeded } from './vendor-login.js';
|
|
@@ -250,6 +250,28 @@ export function openGroup(group) {
|
|
|
250
250
|
return true;
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
+
export function openWorkerWithDriver(worker) {
|
|
254
|
+
var target = resolveWorkerSplitTarget(worker, getCachedSessions(), store.get('splitGroups'));
|
|
255
|
+
if (!target) return false;
|
|
256
|
+
if (target.group) return openGroup(target.group);
|
|
257
|
+
detachTuiView();
|
|
258
|
+
store.set({
|
|
259
|
+
splitPanes: {
|
|
260
|
+
groupId: null,
|
|
261
|
+
panes: [paneForSession(target.driver.id), paneForSession(target.worker.id)],
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
var activeId = store.get('activeSessionId');
|
|
265
|
+
if (activeId !== target.driver.id) {
|
|
266
|
+
var ws = getWs();
|
|
267
|
+
if (isCurrentProjectSessionReady(store.snap(), ws)) {
|
|
268
|
+
ws.send(JSON.stringify({ type: "switch_session", id: target.driver.id }));
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
dismissSplitOverlays();
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
|
|
253
275
|
// Reload survival: when the restored active session turns out to be a split
|
|
254
276
|
// group member, reopen that group. Safe to call often -- no-ops unless a
|
|
255
277
|
// group member is active natively with no split open.
|
|
@@ -366,6 +388,8 @@ function handleSidebarSessionClick(event) {
|
|
|
366
388
|
if (!sessionId) return;
|
|
367
389
|
event.preventDefault();
|
|
368
390
|
event.stopImmediatePropagation();
|
|
391
|
+
var session = sessionById(sessionId);
|
|
392
|
+
if (session && session.sessionRole === "worker" && openWorkerWithDriver(session)) return;
|
|
369
393
|
switchNativeSession(sessionId);
|
|
370
394
|
}
|
|
371
395
|
|
package/package.json
CHANGED