dsh-workspace-explorer 1.1.0 → 1.1.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.
@@ -1,25 +1,35 @@
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, sessionFilePath, underPath, windowsPath } from '../ui/navigation.js';
5
5
  import { renameFavorites } from '../ui/favorites.js';
6
+ import { installSidebarMotion } from './sidebar-motion.js';
7
+ import { 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]);
12
13
 
13
14
  function apply(ctx) {
14
15
  if (location.protocol !== 'dsh-app:' || location.hostname !== 'app') return;
15
16
  if (!navigator.platform.startsWith('Win')) return;
16
- ctx.inject(['sidebarRightTabs', 'sidebarRight', 'remote', 'remote.session'], (scope) => {
17
+ ctx.inject(['sidebarRightTabs', 'sidebarRight', 'remote', 'remote.session', 'remote.workspaceFiles'], (scope) => {
17
18
  const sidebar = scope.sidebarRight;
18
- const views = new Map();
19
+ const bindings = new Map();
19
20
  const folderTabs = new Map();
20
21
  const listeners = new Set();
21
22
  let viewList = [];
22
23
  let activeView;
24
+ let presentedSession;
25
+ let workspace;
26
+ let expanded;
27
+ let fileFocused = false;
28
+ let projecting = false;
29
+ let detached = false;
30
+ let captureQueued = false;
31
+ let handoffFrame;
32
+ let handoffAnimation;
23
33
 
24
34
  function subscribe(listener) {
25
35
  listeners.add(listener);
@@ -30,9 +40,171 @@ function apply(ctx) {
30
40
  for (const listener of listeners) listener();
31
41
  }
32
42
 
33
- function updateViews() {
34
- viewList = [...new Set(views.values())];
35
- notify();
43
+ function filesIn(sessionId) {
44
+ return sidebar.openTabs.getSnapshot().filter(tab => tab.sessionId === sessionId && tab.kind === 'files');
45
+ }
46
+
47
+ function createView(initialPath) {
48
+ const host = document.createElement('div');
49
+ host.className = 'dwe-host';
50
+ const view = { host, address: instancePrefix + crypto.randomUUID(), path: initialPath ?? null, initialPath };
51
+ view.getTarget = () => view.target;
52
+ view.onPath = path => {
53
+ if (view.path === path) return;
54
+ view.path = path;
55
+ notify();
56
+ };
57
+ view.bindFileActions = actions => {
58
+ view.fileActions = actions;
59
+ if (view.pendingPreview) {
60
+ const preview = view.pendingPreview;
61
+ view.pendingPreview = undefined;
62
+ actions.reveal(...preview);
63
+ }
64
+ return () => { view.fileActions = undefined; };
65
+ };
66
+ return view;
67
+ }
68
+
69
+ function boundView(sessionId, tabId) {
70
+ return sessionId === presentedSession ? bindings.get(tabId) : undefined;
71
+ }
72
+
73
+ function rememberPresentation() {
74
+ if (projecting || detached || !presentedSession || sidebar.mounted.getSnapshot() !== presentedSession) return;
75
+ const tab = sidebar.active();
76
+ const view = tab && bindings.get(tab.id);
77
+ if (view) activeView = view;
78
+ fileFocused = Boolean(view);
79
+ expanded = sidebar.isExpanded();
80
+ }
81
+
82
+ function rememberOrder(sessionId) {
83
+ const selector = '[data-sidebar-right-session="' + CSS.escape(sessionId) + '"] [data-dockkit-tab]';
84
+ const ordered = new Set();
85
+ for (const chip of document.querySelectorAll(selector)) {
86
+ const view = bindings.get(chip.dataset.dockkitTab);
87
+ if (view) ordered.add(view);
88
+ }
89
+ const next = [...ordered, ...viewList.filter(view => !ordered.has(view))];
90
+ if (next.some((view, index) => view !== viewList[index])) {
91
+ viewList = next;
92
+ notify();
93
+ }
94
+ }
95
+
96
+ function capturePresentation(sessionId) {
97
+ if (captureQueued) return;
98
+ captureQueued = true;
99
+ queueMicrotask(() => {
100
+ captureQueued = false;
101
+ if (presentedSession !== sessionId || sidebar.mounted.getSnapshot() !== sessionId) return;
102
+ rememberPresentation();
103
+ rememberOrder(sessionId);
104
+ });
105
+ }
106
+
107
+ function syncTabs() {
108
+ if (projecting || detached || !presentedSession || sidebar.mounted.getSnapshot() !== presentedSession) return;
109
+ const tabs = filesIn(presentedSession);
110
+ const live = new Set(tabs.map(tab => tab.tabId));
111
+ const added = [];
112
+ const removed = new Set();
113
+ for (const [tabId, view] of bindings) {
114
+ if (live.has(tabId)) continue;
115
+ bindings.delete(tabId);
116
+ removed.add(view);
117
+ }
118
+ for (const tab of tabs) {
119
+ if (bindings.has(tab.tabId)) continue;
120
+ const view = createView(folderTabs.get(tab.contentId) ?? workspace);
121
+ bindings.set(tab.tabId, view);
122
+ added.push(view);
123
+ }
124
+ if (removed.size || added.length) {
125
+ viewList = [...viewList.filter(view => !removed.has(view)), ...added];
126
+ if (removed.has(activeView)) activeView = undefined;
127
+ notify();
128
+ }
129
+ rememberPresentation();
130
+ }
131
+
132
+ function closeDepartingTabs() {
133
+ const arriving = sidebar.mounted.getSnapshot();
134
+ if (!arriving || !presentedSession) return;
135
+ if (arriving === presentedSession) {
136
+ if (detached) queueMicrotask(() => {
137
+ if (detached && sidebar.mounted.getSnapshot() === arriving) present(arriving, workspace);
138
+ });
139
+ return;
140
+ }
141
+ if (detached) return;
142
+ rememberOrder(presentedSession);
143
+ detached = true;
144
+ if (expanded && viewList.length) beginHandoff();
145
+ // The old native store is still adopted before React commits the session change.
146
+ for (const tab of filesIn(presentedSession)) sidebar.closeIn?.(presentedSession, tab.tabId);
147
+ }
148
+
149
+ function beginHandoff() {
150
+ const frame = document.querySelector('[data-rightbar-col]')?.parentElement;
151
+ if (!frame) return;
152
+ if (handoffFrame !== frame) {
153
+ finishHandoff();
154
+ handoffFrame = frame;
155
+ frame.style.setProperty('--dwe-handoff-columns', getComputedStyle(frame).gridTemplateColumns);
156
+ frame.dataset.dweSidebarHandoff = '';
157
+ }
158
+ cancelAnimationFrame(handoffAnimation);
159
+ handoffAnimation = requestAnimationFrame(() => {
160
+ handoffAnimation = requestAnimationFrame(finishHandoff);
161
+ });
162
+ }
163
+
164
+ function finishHandoff() {
165
+ cancelAnimationFrame(handoffAnimation);
166
+ if (!handoffFrame) return;
167
+ delete handoffFrame.dataset.dweSidebarHandoff;
168
+ handoffFrame.style.removeProperty('--dwe-handoff-columns');
169
+ handoffFrame = undefined;
170
+ }
171
+
172
+ function placeView(view, options) {
173
+ sidebar.openResource(view.address, { kind: 'files', ...options });
174
+ const tab = sidebar.active();
175
+ if (tab?.kind === 'files' && tab.contentId === view.address) bindings.set(tab.id, view);
176
+ }
177
+
178
+ function present(sessionId, cwd) {
179
+ workspace = windowsPath(cwd);
180
+ if (sessionId !== presentedSession || detached) {
181
+ const first = !presentedSession;
182
+ presentedSession = sessionId;
183
+ detached = false;
184
+ bindings.clear();
185
+ if (first) {
186
+ syncTabs();
187
+ } else {
188
+ const nativeActive = sidebar.active();
189
+ const preserveExpansion = expanded;
190
+ projecting = true;
191
+ try {
192
+ for (const tab of filesIn(sessionId)) sidebar.close(tab.tabId);
193
+ const replaceTab = fileFocused && sidebar.active()?.kind === 'guide' ? sidebar.active()?.id : undefined;
194
+ viewList.forEach((view, index) => placeView(view, index === 0 ? { replaceTab } : undefined));
195
+ if (viewList.length) {
196
+ const restoreTab = !fileFocused && nativeActive?.kind !== 'files' ? nativeActive?.id
197
+ : [...bindings].find(([, view]) => view === activeView)?.[0];
198
+ if (restoreTab) sidebar.focus(restoreTab);
199
+ if (preserveExpansion !== undefined && sidebar.isExpanded() !== preserveExpansion) sidebar.toggleExpanded();
200
+ }
201
+ } finally {
202
+ projecting = false;
203
+ }
204
+ notify();
205
+ rememberPresentation();
206
+ }
207
+ }
36
208
  }
37
209
 
38
210
  const openLocal = async (path, action) => {
@@ -40,17 +212,101 @@ function apply(ctx) {
40
212
  if (!result.ok) throw result.error;
41
213
  };
42
214
 
215
+ function openDocument(address, options) {
216
+ sidebar.openResource(address, { ...options, kind: 'text' });
217
+ }
218
+
43
219
  function openFolderTab(path, paneId) {
44
220
  const address = instancePrefix + crypto.randomUUID();
45
221
  folderTabs.set(address, path);
46
222
  try {
47
223
  sidebar.openResource(address, { kind: 'files', paneId });
48
- } catch (error) {
224
+ syncTabs();
225
+ const tab = sidebar.active();
226
+ return tab?.contentId === address ? bindings.get(tab.id) : undefined;
227
+ } finally {
49
228
  folderTabs.delete(address);
50
- throw error;
51
229
  }
52
230
  }
53
231
 
232
+ async function statFile(sessionId, file, signal) {
233
+ try {
234
+ const result = await scope.remote.workspaceFiles.stat(sessionId, file, signal);
235
+ const absolute = windowsPath(result.ok && result.value.absolutePath);
236
+ if (absolute) return { absolute, name: absolute.split('\\').at(-1), size: result.value.bytes };
237
+ } catch { /* The built-in preview can report a failed session file lookup. */ }
238
+ }
239
+
240
+ function revealFile(view, file, line) {
241
+ const preview = [file.absolute, file.name, file.size, line];
242
+ if (view.fileActions) view.fileActions.reveal(...preview);
243
+ else view.pendingPreview = preview;
244
+ }
245
+
246
+ function fileRequest(address, options) {
247
+ const sessionId = sidebar.mounted.getSnapshot();
248
+ const file = workspace && sessionId === presentedSession && sessionFilePath(address, sessionId);
249
+ if (file) return { address, sessionId, file, params: options?.params };
250
+ }
251
+
252
+ function currentExplorer() {
253
+ return bindings.get(sidebar.active()?.id) ?? activeView ?? viewList[0];
254
+ }
255
+
256
+ async function openConversationFile(request, signal, { view, newTab, paneId }) {
257
+ const { address, sessionId, file, params } = request;
258
+ const metadata = await statFile(sessionId, file, signal);
259
+ if (signal.aborted || sidebar.mounted.getSnapshot() !== sessionId) return;
260
+ if (!metadata) {
261
+ openDocument(address, { params, paneId });
262
+ return;
263
+ }
264
+ const existing = newTab ? undefined : viewList.includes(view) ? view : currentExplorer();
265
+ const tabId = existing && [...bindings].find(([, item]) => item === existing)?.[0];
266
+ if (tabId) {
267
+ sidebar.focus(tabId);
268
+ if (!sidebar.isExpanded() && !existing.target?.isFloating) sidebar.toggleExpanded();
269
+ }
270
+ const target = tabId ? existing : openFolderTab(parentOf(metadata.absolute), paneId);
271
+ if (target) revealFile(target, metadata, params?.line);
272
+ }
273
+
274
+ function installFileOpener() {
275
+ const openResource = sidebar.openResource;
276
+ let fileClick;
277
+ let dragSource;
278
+ function open(address, options) {
279
+ const request = (!options || Object.keys(options).every(key => key === 'params')) && fileRequest(address, options);
280
+ if (!request) return openResource.call(this, address, options);
281
+ if (dragSource) {
282
+ dragSource.file = request;
283
+ return;
284
+ }
285
+ fileClick?.abort();
286
+ fileClick = new AbortController();
287
+ void openConversationFile(request, fileClick.signal, { view: currentExplorer() });
288
+ }
289
+ function readDragSource(button) {
290
+ // Let Harness resolve the same path and line as a real click, without opening a tab yet.
291
+ const capture = {};
292
+ dragSource = capture;
293
+ try { button.click(); }
294
+ finally { dragSource = undefined; }
295
+ return capture.file;
296
+ }
297
+ sidebar.openResource = open;
298
+ const stopDrop = installConversationDrop(sidebar.mounted, readDragSource, (request, paneId, signal) => {
299
+ void openConversationFile(request, signal, { newTab: true, paneId });
300
+ });
301
+ const stop = sidebar.mounted.subscribe(() => fileClick?.abort());
302
+ return () => {
303
+ stopDrop();
304
+ stop();
305
+ fileClick?.abort();
306
+ if (sidebar.openResource === open) sidebar.openResource = openResource;
307
+ };
308
+ }
309
+
54
310
  function refreshDirectories(paths, moved = []) {
55
311
  const changed = new Set(paths.map(path => path.toLowerCase()));
56
312
  for (const view of viewList) {
@@ -67,101 +323,81 @@ function apply(ctx) {
67
323
  }
68
324
 
69
325
  function Anchor({ sessionId, useTabInfo }) {
70
- const { tab } = useTabInfo();
326
+ const { tab, panel } = useTabInfo();
71
327
  const anchor = react.useRef(null);
72
- const key = occurrence(sessionId, tab.id);
328
+ const view = react.useSyncExternalStore(subscribe, () => boundView(sessionId, tab.id));
329
+ const navigation = tab.navigation;
73
330
  react.useLayoutEffect(() => {
74
- if (!tab.visible) return;
331
+ if (!tab.visible || !view) return;
75
332
  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 };
333
+ const target = { sessionId, paneId: panel.id, isFloating: panel.isFloating };
97
334
  view.target = target;
98
335
  element.appendChild(view.host);
99
336
  notify();
337
+ capturePresentation(sessionId);
100
338
  const dispose = tab.actions.bindCommands?.({ refresh: () => view.fileActions?.refresh() });
101
339
  return () => {
102
340
  dispose?.();
103
341
  if (view.target !== target) return;
104
342
  view.target = undefined;
105
343
  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
344
  notify();
345
+ capturePresentation(sessionId);
111
346
  };
112
- }, [key, sessionId, tab.visible, tab.actions]);
113
- react.useLayoutEffect(() => {
114
- if (tab.visible && sidebar.active()?.id === tab.id) activeView = views.get(key);
115
- });
347
+ }, [view, sessionId, tab.visible, tab.actions, panel.id, panel.isFloating]);
348
+ react.useEffect(() => {
349
+ if (!tab.visible || !view || view.handledNavigation === navigation) return;
350
+ const { address, params } = navigation;
351
+ const file = sessionFilePath(address, sessionId);
352
+ if (!file) return;
353
+
354
+ const controller = new AbortController();
355
+ const { signal } = controller;
356
+ const isCurrent = () => !signal.aborted && !tab.signal?.aborted
357
+ && sidebar.mounted.getSnapshot() === sessionId && boundView(sessionId, tab.id) === view;
358
+ async function reveal() {
359
+ const metadata = await statFile(sessionId, file, signal);
360
+ if (!isCurrent()) return;
361
+ view.handledNavigation = navigation;
362
+ if (metadata) {
363
+ view.revealedFile = true;
364
+ revealFile(view, metadata, params?.line);
365
+ } else {
366
+ openDocument(address, { paneId: panel.id, params, replaceTab: view.revealedFile ? undefined : tab.id });
367
+ }
368
+ }
369
+ void reveal();
370
+ return () => controller.abort();
371
+ }, [view, sessionId, tab.id, tab.visible, navigation, panel.id]);
116
372
  return h('div', { className: 'dwe-host', ref: anchor });
117
373
  }
118
374
 
119
375
  function Title({ sessionId, useTabInfo }) {
120
376
  const { tab } = useTabInfo();
121
- const path = react.useSyncExternalStore(subscribe, () => views.get(occurrence(sessionId, tab.id))?.path ?? null);
377
+ const path = react.useSyncExternalStore(subscribe, () => boundView(sessionId, tab.id)?.path ?? null);
378
+ react.useLayoutEffect(() => {
379
+ if (tab.visible) capturePresentation(sessionId);
380
+ });
122
381
  return h(ExplorerTitle, { path });
123
382
  }
124
383
 
125
- function View({ view, useSessions }) {
384
+ function View({ view, cwd }) {
126
385
  const target = react.useSyncExternalStore(subscribe, view.getTarget);
127
386
  const sessionId = target?.sessionId;
128
- const cwd = useSessions(state => sessionId ? state.byId[sessionId]?.cwd : undefined);
129
387
  return reactDom.createPortal(h(Explorer, {
130
388
  sessionId, cwd, initialPath: view.initialPath, getTarget: view.getTarget, bindFileActions: view.bindFileActions,
131
- onPath: view.onPath, openLocal, openFolderTab, refreshDirectories, updateRenamedPath,
389
+ onPath: view.onPath, openLocal, openDocument, openFolderTab, refreshDirectories, updateRenamedPath,
132
390
  }), view.host);
133
391
  }
134
392
 
135
393
  function Keeper({ useSessions }) {
136
394
  const sessionId = react.useSyncExternalStore(sidebar.mounted.subscribe, sidebar.mounted.getSnapshot);
137
395
  const currentViews = react.useSyncExternalStore(subscribe, () => viewList);
138
- const previous = react.useRef();
396
+ const cwd = useSessions(state => sessionId ? state.byId[sessionId]?.cwd : undefined);
139
397
  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 }));
398
+ if (sessionId && sidebar.mounted.getSnapshot() === sessionId) present(sessionId, cwd);
399
+ }, [sessionId, cwd]);
400
+ return currentViews.map(view => h(View, { key: view.address, view, cwd }));
165
401
  }
166
402
 
167
403
  scope.effect(() => {
@@ -179,26 +415,17 @@ function apply(ctx) {
179
415
  }
180
416
  }
181
417
  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
- });
418
+ const disposeFileOpener = installFileOpener();
419
+ const disposeMotion = installSidebarMotion(sidebar);
420
+ const disposeTabs = sidebar.openTabs.subscribe(syncTabs);
421
+ const disposeSwitch = sidebar.mounted.subscribe(closeDepartingTabs);
200
422
  return () => {
201
- dispose();
423
+ disposeFileOpener();
424
+ disposeMotion();
425
+ disposeTabs();
426
+ disposeSwitch();
427
+ detached = false;
428
+ finishHandoff();
202
429
  document.removeEventListener('wheel', wheel, true);
203
430
  style.remove();
204
431
  };
@@ -212,7 +439,7 @@ function apply(ctx) {
212
439
  scope.slots.register(key ? { name, key } : { name, id }, component)));
213
440
  }
214
441
  scope.effect(() => scope.sidebarRightTabs.register({
215
- id, kind: 'files', priority: 'extension', title: () => '文件浏览', keepMounted: true, multiple: true,
442
+ id, kind: 'files', priority: 'extension', title: () => '文件浏览', multiple: true,
216
443
  guide: [{
217
444
  id: 'workspace', commandId: 'workspace.files', order: 10, title: () => '文件浏览',
218
445
  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
+ }
package/src/host/files.js CHANGED
@@ -281,7 +281,7 @@ function createDiskAccess() {
281
281
  const target = await resolve(absolute, signal, workspace);
282
282
  const info = await stat(target);
283
283
  if (info.isDirectory()) return { path: absolute, nativePath: target, type: 'directory' };
284
- if (info.isFile()) return { path: absolute, nativePath: target, type: 'file', name: path.basename(absolute) };
284
+ if (info.isFile()) return { path: absolute, nativePath: target, type: 'file', name: path.basename(absolute), size: info.size };
285
285
  throw new BrowseError('该路径不指向普通文件或文件夹');
286
286
  }
287
287