dsh-workspace-explorer 1.1.0 → 1.1.2

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,25 +1,36 @@
1
1
  import { Explorer, ExplorerTitle } from '../ui/explorer.js';
2
2
  import { explorerCss } from '../ui/styles.js';
3
3
  import { scrollHorizontally } from '../ui/horizontal-wheel.js';
4
- import { underPath } from '../ui/navigation.js';
4
+ import { parentOf, resourceAddress, sessionFilePath, underPath, windowsPath } from '../ui/navigation.js';
5
5
  import { renameFavorites } from '../ui/favorites.js';
6
+ import { installSidebarMotion } from './sidebar-motion.js';
7
+ import { changedFileButton, installConversationDrop } from './conversation-drop.js';
6
8
 
7
9
  const id = 'dsh-workspace-explorer';
8
10
  const inject = ['slots'];
9
11
  const h = react.createElement;
10
12
  const instancePrefix = 'dsh-resource://workspace-explorer/tab/';
11
- const occurrence = (sessionId, tabId) => JSON.stringify([sessionId, tabId]);
13
+ const changesReviewPrefix = 'dsh-resource://changes-review/session/';
12
14
 
13
15
  function apply(ctx) {
14
16
  if (location.protocol !== 'dsh-app:' || location.hostname !== 'app') return;
15
17
  if (!navigator.platform.startsWith('Win')) return;
16
- ctx.inject(['sidebarRightTabs', 'sidebarRight', 'remote', 'remote.session'], (scope) => {
18
+ ctx.inject(['sidebarRightTabs', 'sidebarRight', 'remote', 'remote.session', 'remote.workspaceFiles'], (scope) => {
17
19
  const sidebar = scope.sidebarRight;
18
- const views = new Map();
20
+ const bindings = new Map();
19
21
  const folderTabs = new Map();
20
22
  const listeners = new Set();
21
23
  let viewList = [];
22
24
  let activeView;
25
+ let presentedSession;
26
+ let workspace;
27
+ let expanded;
28
+ let fileFocused = false;
29
+ let projecting = false;
30
+ let detached = false;
31
+ let captureQueued = false;
32
+ let handoffFrame;
33
+ let handoffAnimation;
23
34
 
24
35
  function subscribe(listener) {
25
36
  listeners.add(listener);
@@ -30,9 +41,171 @@ function apply(ctx) {
30
41
  for (const listener of listeners) listener();
31
42
  }
32
43
 
33
- function updateViews() {
34
- viewList = [...new Set(views.values())];
35
- notify();
44
+ function filesIn(sessionId) {
45
+ return sidebar.openTabs.getSnapshot().filter(tab => tab.sessionId === sessionId && tab.kind === 'files');
46
+ }
47
+
48
+ function createView(initialPath) {
49
+ const host = document.createElement('div');
50
+ host.className = 'dwe-host';
51
+ const view = { host, address: instancePrefix + crypto.randomUUID(), path: initialPath ?? null, initialPath };
52
+ view.getTarget = () => view.target;
53
+ view.onPath = path => {
54
+ if (view.path === path) return;
55
+ view.path = path;
56
+ notify();
57
+ };
58
+ view.bindFileActions = actions => {
59
+ view.fileActions = actions;
60
+ if (view.pendingPreview) {
61
+ const preview = view.pendingPreview;
62
+ view.pendingPreview = undefined;
63
+ actions.reveal(...preview);
64
+ }
65
+ return () => { view.fileActions = undefined; };
66
+ };
67
+ return view;
68
+ }
69
+
70
+ function boundView(sessionId, tabId) {
71
+ return sessionId === presentedSession ? bindings.get(tabId) : undefined;
72
+ }
73
+
74
+ function rememberPresentation() {
75
+ if (projecting || detached || !presentedSession || sidebar.mounted.getSnapshot() !== presentedSession) return;
76
+ const tab = sidebar.active();
77
+ const view = tab && bindings.get(tab.id);
78
+ if (view) activeView = view;
79
+ fileFocused = Boolean(view);
80
+ expanded = sidebar.isExpanded();
81
+ }
82
+
83
+ function rememberOrder(sessionId) {
84
+ const selector = '[data-sidebar-right-session="' + CSS.escape(sessionId) + '"] [data-dockkit-tab]';
85
+ const ordered = new Set();
86
+ for (const chip of document.querySelectorAll(selector)) {
87
+ const view = bindings.get(chip.dataset.dockkitTab);
88
+ if (view) ordered.add(view);
89
+ }
90
+ const next = [...ordered, ...viewList.filter(view => !ordered.has(view))];
91
+ if (next.some((view, index) => view !== viewList[index])) {
92
+ viewList = next;
93
+ notify();
94
+ }
95
+ }
96
+
97
+ function capturePresentation(sessionId) {
98
+ if (captureQueued) return;
99
+ captureQueued = true;
100
+ queueMicrotask(() => {
101
+ captureQueued = false;
102
+ if (presentedSession !== sessionId || sidebar.mounted.getSnapshot() !== sessionId) return;
103
+ rememberPresentation();
104
+ rememberOrder(sessionId);
105
+ });
106
+ }
107
+
108
+ function syncTabs() {
109
+ if (projecting || detached || !presentedSession || sidebar.mounted.getSnapshot() !== presentedSession) return;
110
+ const tabs = filesIn(presentedSession);
111
+ const live = new Set(tabs.map(tab => tab.tabId));
112
+ const added = [];
113
+ const removed = new Set();
114
+ for (const [tabId, view] of bindings) {
115
+ if (live.has(tabId)) continue;
116
+ bindings.delete(tabId);
117
+ removed.add(view);
118
+ }
119
+ for (const tab of tabs) {
120
+ if (bindings.has(tab.tabId)) continue;
121
+ const view = createView(folderTabs.get(tab.contentId) ?? workspace);
122
+ bindings.set(tab.tabId, view);
123
+ added.push(view);
124
+ }
125
+ if (removed.size || added.length) {
126
+ viewList = [...viewList.filter(view => !removed.has(view)), ...added];
127
+ if (removed.has(activeView)) activeView = undefined;
128
+ notify();
129
+ }
130
+ rememberPresentation();
131
+ }
132
+
133
+ function closeDepartingTabs() {
134
+ const arriving = sidebar.mounted.getSnapshot();
135
+ if (!arriving || !presentedSession) return;
136
+ if (arriving === presentedSession) {
137
+ if (detached) queueMicrotask(() => {
138
+ if (detached && sidebar.mounted.getSnapshot() === arriving) present(arriving, workspace);
139
+ });
140
+ return;
141
+ }
142
+ if (detached) return;
143
+ rememberOrder(presentedSession);
144
+ detached = true;
145
+ if (expanded && viewList.length) beginHandoff();
146
+ // The old native store is still adopted before React commits the session change.
147
+ for (const tab of filesIn(presentedSession)) sidebar.closeIn?.(presentedSession, tab.tabId);
148
+ }
149
+
150
+ function beginHandoff() {
151
+ const frame = document.querySelector('[data-rightbar-col]')?.parentElement;
152
+ if (!frame) return;
153
+ if (handoffFrame !== frame) {
154
+ finishHandoff();
155
+ handoffFrame = frame;
156
+ frame.style.setProperty('--dwe-handoff-columns', getComputedStyle(frame).gridTemplateColumns);
157
+ frame.dataset.dweSidebarHandoff = '';
158
+ }
159
+ cancelAnimationFrame(handoffAnimation);
160
+ handoffAnimation = requestAnimationFrame(() => {
161
+ handoffAnimation = requestAnimationFrame(finishHandoff);
162
+ });
163
+ }
164
+
165
+ function finishHandoff() {
166
+ cancelAnimationFrame(handoffAnimation);
167
+ if (!handoffFrame) return;
168
+ delete handoffFrame.dataset.dweSidebarHandoff;
169
+ handoffFrame.style.removeProperty('--dwe-handoff-columns');
170
+ handoffFrame = undefined;
171
+ }
172
+
173
+ function placeView(view, options) {
174
+ sidebar.openResource(view.address, { kind: 'files', ...options });
175
+ const tab = sidebar.active();
176
+ if (tab?.kind === 'files' && tab.contentId === view.address) bindings.set(tab.id, view);
177
+ }
178
+
179
+ function present(sessionId, cwd) {
180
+ workspace = windowsPath(cwd);
181
+ if (sessionId !== presentedSession || detached) {
182
+ const first = !presentedSession;
183
+ presentedSession = sessionId;
184
+ detached = false;
185
+ bindings.clear();
186
+ if (first) {
187
+ syncTabs();
188
+ } else {
189
+ const nativeActive = sidebar.active();
190
+ const preserveExpansion = expanded;
191
+ projecting = true;
192
+ try {
193
+ for (const tab of filesIn(sessionId)) sidebar.close(tab.tabId);
194
+ const replaceTab = fileFocused && sidebar.active()?.kind === 'guide' ? sidebar.active()?.id : undefined;
195
+ viewList.forEach((view, index) => placeView(view, index === 0 ? { replaceTab } : undefined));
196
+ if (viewList.length) {
197
+ const restoreTab = !fileFocused && nativeActive?.kind !== 'files' ? nativeActive?.id
198
+ : [...bindings].find(([, view]) => view === activeView)?.[0];
199
+ if (restoreTab) sidebar.focus(restoreTab);
200
+ if (preserveExpansion !== undefined && sidebar.isExpanded() !== preserveExpansion) sidebar.toggleExpanded();
201
+ }
202
+ } finally {
203
+ projecting = false;
204
+ }
205
+ notify();
206
+ rememberPresentation();
207
+ }
208
+ }
36
209
  }
37
210
 
38
211
  const openLocal = async (path, action) => {
@@ -40,17 +213,143 @@ function apply(ctx) {
40
213
  if (!result.ok) throw result.error;
41
214
  };
42
215
 
216
+ function openDocument(address, options) {
217
+ sidebar.openResource(address, { ...options, kind: 'text' });
218
+ }
219
+
43
220
  function openFolderTab(path, paneId) {
44
221
  const address = instancePrefix + crypto.randomUUID();
45
222
  folderTabs.set(address, path);
46
223
  try {
47
224
  sidebar.openResource(address, { kind: 'files', paneId });
48
- } catch (error) {
225
+ syncTabs();
226
+ const tab = sidebar.active();
227
+ return tab?.contentId === address ? bindings.get(tab.id) : undefined;
228
+ } finally {
49
229
  folderTabs.delete(address);
50
- throw error;
51
230
  }
52
231
  }
53
232
 
233
+ async function statFile(sessionId, file, signal) {
234
+ try {
235
+ const result = await scope.remote.workspaceFiles.stat(sessionId, file, signal);
236
+ const absolute = windowsPath(result.ok && result.value.absolutePath);
237
+ if (absolute) return { absolute, name: absolute.split('\\').at(-1), size: result.value.bytes };
238
+ } catch { /* The built-in preview can report a failed session file lookup. */ }
239
+ }
240
+
241
+ function revealFile(view, file, line) {
242
+ const preview = [file.absolute, file.name, file.size, line];
243
+ if (view.fileActions) view.fileActions.reveal(...preview);
244
+ else view.pendingPreview = preview;
245
+ }
246
+
247
+ function fileRequest(address, options) {
248
+ const sessionId = sidebar.mounted.getSnapshot();
249
+ const file = workspace && sessionId === presentedSession && sessionFilePath(address, sessionId);
250
+ if (file) return { address, sessionId, file, params: options?.params };
251
+ }
252
+
253
+ function currentExplorer() {
254
+ return bindings.get(sidebar.active()?.id) ?? activeView ?? viewList[0];
255
+ }
256
+
257
+ async function openConversationFile(request, signal, { view, onUnavailable }) {
258
+ const { address, sessionId, file, params } = request;
259
+ const metadata = await statFile(sessionId, file, signal);
260
+ if (signal.aborted || sidebar.mounted.getSnapshot() !== sessionId) return;
261
+ if (!metadata) {
262
+ if (onUnavailable) onUnavailable();
263
+ else openDocument(address, { params });
264
+ return;
265
+ }
266
+ const existing = viewList.includes(view) ? view : currentExplorer();
267
+ const tabId = existing && [...bindings].find(([, item]) => item === existing)?.[0];
268
+ if (tabId) {
269
+ sidebar.focus(tabId);
270
+ if (!sidebar.isExpanded() && !existing.target?.isFloating) sidebar.toggleExpanded();
271
+ }
272
+ const target = tabId ? existing : openFolderTab(parentOf(metadata.absolute));
273
+ if (target) revealFile(target, metadata, params?.line);
274
+ }
275
+
276
+ function installFileOpener() {
277
+ const openResource = sidebar.openResource;
278
+ let fileClick;
279
+ let dragSource;
280
+ let openingReview = false;
281
+ function openFile(request, onUnavailable) {
282
+ fileClick?.abort();
283
+ fileClick = new AbortController();
284
+ void openConversationFile(request, fileClick.signal, { view: currentExplorer(), onUnavailable });
285
+ }
286
+ function open(address, options) {
287
+ const changed = dragSource?.changed;
288
+ if (changed && address.startsWith(changesReviewPrefix)) {
289
+ if (address.startsWith(changesReviewPrefix + encodeURIComponent(changed.sessionId) + '/')) {
290
+ dragSource.file = { ...changed, review: { address, params: options?.params } };
291
+ }
292
+ return;
293
+ }
294
+ const request = (!options || Object.keys(options).every(key => key === 'params')) && fileRequest(address, options);
295
+ if (!request) return openResource.call(this, address, options);
296
+ if (dragSource) {
297
+ dragSource.file = request;
298
+ return;
299
+ }
300
+ openFile(request);
301
+ }
302
+ function changedFileRequest(button) {
303
+ const card = button.closest('[data-changed-files]');
304
+ const description = document.getElementById(button.getAttribute('aria-describedby'));
305
+ const sessionId = card?.closest('[data-conversation-content]')?.dataset.conversationSession;
306
+ if (sessionId !== sidebar.mounted.getSnapshot() || !card?.contains(description)) return;
307
+ const absolute = windowsPath(description.textContent);
308
+ if (absolute) return fileRequest(resourceAddress(absolute, sessionId));
309
+ }
310
+ function clickChangedFile(event) {
311
+ // Shift-click retains the native change review for an individual file.
312
+ if (openingReview || dragSource?.changed || event.shiftKey || !(event.target instanceof Element)) return;
313
+ const button = event.target.closest(changedFileButton);
314
+ const request = button && changedFileRequest(button);
315
+ if (!request) return;
316
+ event.preventDefault();
317
+ event.stopPropagation();
318
+ openFile(request, () => {
319
+ if (!button.isConnected) return;
320
+ openingReview = true;
321
+ try { button.click(); }
322
+ finally { openingReview = false; }
323
+ });
324
+ }
325
+ function readDragSource(button) {
326
+ const isChanged = button.matches(changedFileButton);
327
+ const changed = isChanged ? changedFileRequest(button) : undefined;
328
+ if (isChanged && !changed) return;
329
+ // Capture Harness's file or review address without opening the tab until drop.
330
+ const capture = { changed };
331
+ dragSource = capture;
332
+ try { button.click(); }
333
+ finally { dragSource = undefined; }
334
+ return capture.file ?? changed;
335
+ }
336
+ sidebar.openResource = open;
337
+ const stopDrop = installConversationDrop(sidebar.mounted, readDragSource, ({ address, sessionId, params, review }, paneId) => {
338
+ if (sidebar.mounted.getSnapshot() !== sessionId) return;
339
+ if (review) sidebar.openResource(review.address, { paneId, params: review.params });
340
+ else openDocument(address, { paneId, params });
341
+ });
342
+ document.addEventListener('click', clickChangedFile, true);
343
+ const stop = sidebar.mounted.subscribe(() => fileClick?.abort());
344
+ return () => {
345
+ document.removeEventListener('click', clickChangedFile, true);
346
+ stopDrop();
347
+ stop();
348
+ fileClick?.abort();
349
+ if (sidebar.openResource === open) sidebar.openResource = openResource;
350
+ };
351
+ }
352
+
54
353
  function refreshDirectories(paths, moved = []) {
55
354
  const changed = new Set(paths.map(path => path.toLowerCase()));
56
355
  for (const view of viewList) {
@@ -67,101 +366,81 @@ function apply(ctx) {
67
366
  }
68
367
 
69
368
  function Anchor({ sessionId, useTabInfo }) {
70
- const { tab } = useTabInfo();
369
+ const { tab, panel } = useTabInfo();
71
370
  const anchor = react.useRef(null);
72
- const key = occurrence(sessionId, tab.id);
371
+ const view = react.useSyncExternalStore(subscribe, () => boundView(sessionId, tab.id));
372
+ const navigation = tab.navigation;
73
373
  react.useLayoutEffect(() => {
74
- if (!tab.visible) return;
374
+ if (!tab.visible || !view) return;
75
375
  const element = anchor.current;
76
- let view = views.get(key);
77
- if (!view) {
78
- const host = document.createElement('div');
79
- host.className = 'dwe-host';
80
- const initialPath = folderTabs.get(tab.contentId);
81
- folderTabs.delete(tab.contentId);
82
- view = { host, address: initialPath ? tab.contentId : instancePrefix + crypto.randomUUID(), path: null, initialPath };
83
- view.getTarget = () => view.target;
84
- view.onPath = path => {
85
- if (view.path === path) return;
86
- view.path = path;
87
- notify();
88
- };
89
- view.bindFileActions = actions => {
90
- view.fileActions = actions;
91
- return () => { view.fileActions = undefined; };
92
- };
93
- views.set(key, view);
94
- updateViews();
95
- }
96
- const target = { sessionId, actions: tab.actions };
376
+ const target = { sessionId, paneId: panel.id, isFloating: panel.isFloating };
97
377
  view.target = target;
98
378
  element.appendChild(view.host);
99
379
  notify();
380
+ capturePresentation(sessionId);
100
381
  const dispose = tab.actions.bindCommands?.({ refresh: () => view.fileActions?.refresh() });
101
382
  return () => {
102
383
  dispose?.();
103
384
  if (view.target !== target) return;
104
385
  view.target = undefined;
105
386
  if (view.host.parentNode === element) view.host.remove();
106
- if (activeView === view && sidebar.mounted.getSnapshot() === sessionId
107
- && (!sidebar.isExpanded() || sidebar.active()?.kind !== 'files')) {
108
- activeView = undefined;
109
- }
110
387
  notify();
388
+ capturePresentation(sessionId);
111
389
  };
112
- }, [key, sessionId, tab.visible, tab.actions]);
113
- react.useLayoutEffect(() => {
114
- if (tab.visible && sidebar.active()?.id === tab.id) activeView = views.get(key);
115
- });
390
+ }, [view, sessionId, tab.visible, tab.actions, panel.id, panel.isFloating]);
391
+ react.useEffect(() => {
392
+ if (!tab.visible || !view || view.handledNavigation === navigation) return;
393
+ const { address, params } = navigation;
394
+ const file = sessionFilePath(address, sessionId);
395
+ if (!file) return;
396
+
397
+ const controller = new AbortController();
398
+ const { signal } = controller;
399
+ const isCurrent = () => !signal.aborted && !tab.signal?.aborted
400
+ && sidebar.mounted.getSnapshot() === sessionId && boundView(sessionId, tab.id) === view;
401
+ async function reveal() {
402
+ const metadata = await statFile(sessionId, file, signal);
403
+ if (!isCurrent()) return;
404
+ view.handledNavigation = navigation;
405
+ if (metadata) {
406
+ view.revealedFile = true;
407
+ revealFile(view, metadata, params?.line);
408
+ } else {
409
+ openDocument(address, { paneId: panel.id, params, replaceTab: view.revealedFile ? undefined : tab.id });
410
+ }
411
+ }
412
+ void reveal();
413
+ return () => controller.abort();
414
+ }, [view, sessionId, tab.id, tab.visible, navigation, panel.id]);
116
415
  return h('div', { className: 'dwe-host', ref: anchor });
117
416
  }
118
417
 
119
418
  function Title({ sessionId, useTabInfo }) {
120
419
  const { tab } = useTabInfo();
121
- const path = react.useSyncExternalStore(subscribe, () => views.get(occurrence(sessionId, tab.id))?.path ?? null);
420
+ const path = react.useSyncExternalStore(subscribe, () => boundView(sessionId, tab.id)?.path ?? null);
421
+ react.useLayoutEffect(() => {
422
+ if (tab.visible) capturePresentation(sessionId);
423
+ });
122
424
  return h(ExplorerTitle, { path });
123
425
  }
124
426
 
125
- function View({ view, useSessions }) {
427
+ function View({ view, cwd }) {
126
428
  const target = react.useSyncExternalStore(subscribe, view.getTarget);
127
429
  const sessionId = target?.sessionId;
128
- const cwd = useSessions(state => sessionId ? state.byId[sessionId]?.cwd : undefined);
129
430
  return reactDom.createPortal(h(Explorer, {
130
431
  sessionId, cwd, initialPath: view.initialPath, getTarget: view.getTarget, bindFileActions: view.bindFileActions,
131
- onPath: view.onPath, openLocal, openFolderTab, refreshDirectories, updateRenamedPath,
432
+ onPath: view.onPath, openLocal, openDocument, openFolderTab, refreshDirectories, updateRenamedPath,
132
433
  }), view.host);
133
434
  }
134
435
 
135
436
  function Keeper({ useSessions }) {
136
437
  const sessionId = react.useSyncExternalStore(sidebar.mounted.subscribe, sidebar.mounted.getSnapshot);
137
438
  const currentViews = react.useSyncExternalStore(subscribe, () => viewList);
138
- const previous = react.useRef();
439
+ const cwd = useSessions(state => sessionId ? state.byId[sessionId]?.cwd : undefined);
139
440
  react.useLayoutEffect(() => {
140
- if (!sessionId || previous.current === sessionId) return;
141
- const view = previous.current === undefined ? undefined : activeView;
142
- previous.current = sessionId;
143
- if (!view) return;
144
- // Wait for the new conversation's dock store to be adopted.
145
- queueMicrotask(() => {
146
- if (sidebar.mounted.getSnapshot() !== sessionId) return;
147
- const existing = sidebar.openTabs.getSnapshot().find(tab =>
148
- tab.sessionId === sessionId && views.get(occurrence(sessionId, tab.tabId)) === view);
149
- if (existing) {
150
- if (!sidebar.isExpanded()) sidebar.toggleExpanded();
151
- sidebar.focus(existing.tabId);
152
- return;
153
- }
154
- sidebar.openResource(view.address, { kind: 'files' });
155
- const tab = sidebar.active();
156
- if (tab?.kind !== 'files') return;
157
- const key = occurrence(sessionId, tab.id);
158
- if (!views.has(key)) {
159
- views.set(key, view);
160
- updateViews();
161
- }
162
- });
163
- }, [sessionId]);
164
- return currentViews.map(view => h(View, { key: view.address, view, useSessions }));
441
+ if (sessionId && sidebar.mounted.getSnapshot() === sessionId) present(sessionId, cwd);
442
+ }, [sessionId, cwd]);
443
+ return currentViews.map(view => h(View, { key: view.address, view, cwd }));
165
444
  }
166
445
 
167
446
  scope.effect(() => {
@@ -179,26 +458,17 @@ function apply(ctx) {
179
458
  }
180
459
  }
181
460
  document.addEventListener('wheel', wheel, { passive: false, capture: true });
182
- const dispose = sidebar.openTabs.subscribe(() => {
183
- const inventory = sidebar.openTabs.getSnapshot();
184
- const live = new Set(inventory.map(tab => occurrence(tab.sessionId, tab.tabId)));
185
- const addresses = new Set(inventory.map(tab => tab.contentId));
186
- for (const address of folderTabs.keys()) {
187
- if (!addresses.has(address)) folderTabs.delete(address);
188
- }
189
- let removed = false;
190
- for (const key of views.keys()) {
191
- if (live.has(key)) continue;
192
- views.delete(key);
193
- removed = true;
194
- }
195
- if (removed) {
196
- updateViews();
197
- if (!viewList.includes(activeView)) activeView = undefined;
198
- }
199
- });
461
+ const disposeFileOpener = installFileOpener();
462
+ const disposeMotion = installSidebarMotion(sidebar);
463
+ const disposeTabs = sidebar.openTabs.subscribe(syncTabs);
464
+ const disposeSwitch = sidebar.mounted.subscribe(closeDepartingTabs);
200
465
  return () => {
201
- dispose();
466
+ disposeFileOpener();
467
+ disposeMotion();
468
+ disposeTabs();
469
+ disposeSwitch();
470
+ detached = false;
471
+ finishHandoff();
202
472
  document.removeEventListener('wheel', wheel, true);
203
473
  style.remove();
204
474
  };
@@ -212,7 +482,7 @@ function apply(ctx) {
212
482
  scope.slots.register(key ? { name, key } : { name, id }, component)));
213
483
  }
214
484
  scope.effect(() => scope.sidebarRightTabs.register({
215
- id, kind: 'files', priority: 'extension', title: () => '文件浏览', keepMounted: true, multiple: true,
485
+ id, kind: 'files', priority: 'extension', title: () => '文件浏览', multiple: true,
216
486
  guide: [{
217
487
  id: 'workspace', commandId: 'workspace.files', order: 10, title: () => '文件浏览',
218
488
  description: () => '浏览本机磁盘,在下方独立预览文件',
@@ -0,0 +1,69 @@
1
+ const dockItems = ':is([data-dockkit-host="dock"], [data-dockkit-empty], [data-dockkit-divider])';
2
+ const toggleButtons = 'button:is([data-sidebar-right-expand], [data-sidebar-right-toggle])';
3
+
4
+ export function installSidebarMotion(sidebar) {
5
+ let allowed;
6
+ let collectFrame;
7
+ const instantFrames = new Map();
8
+
9
+ function stopMotion() {
10
+ cancelAnimationFrame(collectFrame);
11
+ if (!allowed) return;
12
+ for (const item of allowed.items) delete item.dataset.dweSidebarToggleMotion;
13
+ allowed = undefined;
14
+ }
15
+
16
+ function allowButtonMotion(event) {
17
+ const button = event.target instanceof Element && event.target.closest(toggleButtons);
18
+ if (!button || button.disabled) return;
19
+ const sessionId = sidebar.mounted.getSnapshot();
20
+ const panel = sessionId && document.querySelector(
21
+ '[data-sidebar-right-panel][data-sidebar-right-session="' + CSS.escape(sessionId) + '"]',
22
+ );
23
+ const frame = panel?.closest('[data-rightbar-col]')?.parentElement;
24
+ if (!frame) return;
25
+
26
+ // A tab selected during the slide must not inherit permission to animate.
27
+ stopMotion();
28
+ const items = [...panel.querySelectorAll(dockItems + ':not([hidden])')];
29
+ const motion = { frame, items };
30
+ allowed = motion;
31
+ for (const item of items) item.dataset.dweSidebarToggleMotion = '';
32
+ collectFrame = requestAnimationFrame(() => {
33
+ const transitions = [...frame.getAnimations(), ...items.flatMap(item => item.getAnimations())]
34
+ .filter(animation => ['transform', 'grid-template-columns'].includes(animation.transitionProperty));
35
+ Promise.allSettled(transitions.map(animation => animation.finished)).then(() => {
36
+ if (allowed === motion) stopMotion();
37
+ });
38
+ });
39
+ }
40
+
41
+ // The shell shares one grid transition between the left and right columns.
42
+ // Snap right-track changes outside the toggle controls so the left controls still animate.
43
+ const track = new MutationObserver(records => {
44
+ for (const frame of new Set(records.map(record => record.target))) {
45
+ if (allowed?.frame === frame || !frame.querySelector(':scope > [data-rightbar-col]')) continue;
46
+ cancelAnimationFrame(instantFrames.get(frame));
47
+ frame.dataset.dweSidebarTrackInstant = '';
48
+ instantFrames.set(frame, requestAnimationFrame(() => {
49
+ instantFrames.set(frame, requestAnimationFrame(() => {
50
+ delete frame.dataset.dweSidebarTrackInstant;
51
+ instantFrames.delete(frame);
52
+ }));
53
+ }));
54
+ }
55
+ });
56
+ track.observe(document.documentElement, { subtree: true, attributeFilter: ['data-rightbar-collapsed'] });
57
+ document.addEventListener('click', allowButtonMotion, true);
58
+ const disposeSwitch = sidebar.mounted.subscribe(stopMotion);
59
+ return () => {
60
+ stopMotion();
61
+ disposeSwitch();
62
+ track.disconnect();
63
+ for (const [frame, request] of instantFrames) {
64
+ cancelAnimationFrame(request);
65
+ delete frame.dataset.dweSidebarTrackInstant;
66
+ }
67
+ document.removeEventListener('click', allowButtonMotion, true);
68
+ };
69
+ }
@@ -1,2 +1,2 @@
1
- export const officeExtensions = new Set(['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx']);
2
- export const pdfAssetsPath = '/api/dsh.workspaceExplorer/pdf';
1
+ export const officeExtensions = new Set(['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx']);
2
+ export const pdfAssetsPath = '/api/dsh.workspaceExplorer/pdf';