@lijian-ui/dsh-file-manager 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/LICENSE +38 -38
  2. package/README.i18n.yaml +6 -6
  3. package/README.md +66 -66
  4. package/README.zh.md +66 -66
  5. package/cordis.patch.yml +13 -13
  6. package/lib/client.js +27354 -355
  7. package/lib/client.js.map +1 -1
  8. package/lib/index.js +474 -42
  9. package/lib/tsconfig.client.tsbuildinfo +1 -1
  10. package/lib/tsconfig.host.tsbuildinfo +1 -1
  11. package/lib/types/client/file-source.d.ts +10 -0
  12. package/lib/types/client/file-source.d.ts.map +1 -1
  13. package/lib/types/client/fileType.d.ts +19 -4
  14. package/lib/types/client/fileType.d.ts.map +1 -1
  15. package/lib/types/client/index.d.ts +0 -1
  16. package/lib/types/client/index.d.ts.map +1 -1
  17. package/lib/types/client/locales.d.ts +3 -0
  18. package/lib/types/client/locales.d.ts.map +1 -1
  19. package/lib/types/client/mount.d.ts +1 -1
  20. package/lib/types/client/mount.d.ts.map +1 -1
  21. package/lib/types/client/picker/FilePickerModal.d.ts +8 -6
  22. package/lib/types/client/picker/FilePickerModal.d.ts.map +1 -1
  23. package/lib/types/client/preview/PreviewPanel.d.ts +2 -1
  24. package/lib/types/client/preview/PreviewPanel.d.ts.map +1 -1
  25. package/lib/types/client/preview/PreviewToolbar.d.ts +3 -1
  26. package/lib/types/client/preview/PreviewToolbar.d.ts.map +1 -1
  27. package/lib/types/client/preview/content.d.ts +3 -1
  28. package/lib/types/client/preview/content.d.ts.map +1 -1
  29. package/lib/types/client/preview/office.d.ts +23 -0
  30. package/lib/types/client/preview/office.d.ts.map +1 -0
  31. package/lib/types/client/reference.d.ts +23 -10
  32. package/lib/types/client/reference.d.ts.map +1 -1
  33. package/lib/types/client/store.d.ts.map +1 -1
  34. package/lib/types/host/fs-service.d.ts.map +1 -1
  35. package/lib/types/host/office-preview.d.ts +13 -0
  36. package/lib/types/host/office-preview.d.ts.map +1 -0
  37. package/lib/types/host/routes.d.ts.map +1 -1
  38. package/lib/types/index.d.ts.map +1 -1
  39. package/package.json +19 -4
  40. package/src/client/DockItem.tsx +1 -1
  41. package/src/client/FileManagerSettingsCard.tsx +117 -117
  42. package/src/client/PluginSettingsCard.tsx +337 -337
  43. package/src/client/chat/file-ref.ts +144 -144
  44. package/src/client/chat/mermaid-chat.tsx +102 -102
  45. package/src/client/chat/placeholder-hint.tsx +46 -46
  46. package/src/client/components/ExplorerPanel.tsx +534 -534
  47. package/src/client/components/FileIcon.tsx +46 -46
  48. package/src/client/components/ScmPanel.tsx +480 -480
  49. package/src/client/components/a11y.ts +20 -20
  50. package/src/client/components/icons.tsx +274 -274
  51. package/src/client/components/overlay.tsx +225 -225
  52. package/src/client/drag/DragFileInlay.tsx +89 -89
  53. package/src/client/drag/file-drag.ts +67 -67
  54. package/src/client/drag.ts +154 -154
  55. package/src/client/file-source.ts +34 -0
  56. package/src/client/fileType.ts +32 -5
  57. package/src/client/hooks/useResizableSplit.ts +91 -91
  58. package/src/client/hooks/useStore.ts +15 -15
  59. package/src/client/index.ts +45 -17
  60. package/src/client/layout.ts +523 -523
  61. package/src/client/locales.ts +6 -0
  62. package/src/client/maximize.ts +34 -34
  63. package/src/client/mount.tsx +2 -1
  64. package/src/client/persist.ts +194 -194
  65. package/src/client/picker/FilePickerModal.tsx +526 -507
  66. package/src/client/picker/file-picker.ts +86 -86
  67. package/src/client/preview/PreviewPanel.tsx +8 -1
  68. package/src/client/preview/PreviewTabs.tsx +159 -159
  69. package/src/client/preview/PreviewToolbar.tsx +3 -2
  70. package/src/client/preview/content.tsx +7 -2
  71. package/src/client/preview/markdown.ts +351 -351
  72. package/src/client/preview/mermaid.ts +274 -274
  73. package/src/client/preview/office.tsx +321 -0
  74. package/src/client/reference.ts +62 -8
  75. package/src/client/settings-card.module.css +316 -316
  76. package/src/client/settings-form.ts +451 -451
  77. package/src/client/store.ts +17 -12
  78. package/src/client/styles/drag.module.css +30 -30
  79. package/src/client/styles/explorer.module.css +371 -371
  80. package/src/client/styles/picker.module.css +344 -344
  81. package/src/client/styles/preview.module.css +18 -11
  82. package/src/client/styles/scm.module.css +380 -380
  83. package/src/client/styles/tokens.module.css +376 -367
  84. package/src/core/types.ts +162 -162
  85. package/src/host/fs-service.ts +7 -1
  86. package/src/host/gate.ts +73 -73
  87. package/src/host/git-runner.ts +116 -116
  88. package/src/host/git-service.ts +395 -395
  89. package/src/host/loopback.ts +63 -63
  90. package/src/host/office-preview.ts +399 -0
  91. package/src/host/poll-guard.ts +109 -109
  92. package/src/host/routes.ts +72 -1
  93. package/src/index.ts +3 -1
  94. package/src/mount-once.ts +48 -48
  95. package/lib/types/client/chat/AttachedFilesDock.d.ts +0 -51
  96. package/lib/types/client/chat/AttachedFilesDock.d.ts.map +0 -1
  97. package/lib/types/client/floating.d.ts +0 -27
  98. package/lib/types/client/floating.d.ts.map +0 -1
  99. package/lib/types/client/mention.d.ts +0 -44
  100. package/lib/types/client/mention.d.ts.map +0 -1
  101. package/src/client/styles/chip.module.css +0 -38
@@ -1,67 +1,67 @@
1
- /**
2
- * Pure drag-to-composer helpers shared by the explorer rows (the drag
3
- * source) and the composer dock inlay (the drop target): the custom MIME
4
- * type, the drag-state detector, and the draft-splicing rule. Deliberately
5
- * framework-free so the splicing math is unit-testable in isolation.
6
- *
7
- * The composer host only accepts OS image drops (its document-level drop
8
- * handler checks `dataTransfer.types` for `Files` and routes through the
9
- * image pipeline), so a workspace file needs its own MIME. A plain relative
10
- * path is inserted into the draft — the agent reads the file through its
11
- * existing tools without any prefix grammar.
12
- * @module dsh-filemgr/client/drag/file-drag
13
- */
14
-
15
- /** Custom MIME carrying a workspace-relative file path. */
16
- export const FILE_DRAG_MIME = 'application/x-dsh-file'
17
-
18
- /**
19
- * Whether a drag event carries our file payload.
20
- * @param types - the live `dataTransfer.types` list (read-only during drag).
21
- * @returns true when our MIME is present.
22
- */
23
- export function hasFileDrag(types: readonly string[] | undefined): boolean {
24
- return types !== undefined && types.includes(FILE_DRAG_MIME)
25
- }
26
-
27
- /**
28
- * Whether a drop payload is a plausible workspace-relative path. The custom
29
- * MIME only proves the drag carries *some* string — a foreign page can set it
30
- * too, so the payload itself is validated before it reaches the draft:
31
- * relative POSIX shape only; no absolute paths, no '..' segments, no
32
- * backslashes, no control characters, and a sane length.
33
- * @param path - the raw payload from dataTransfer.
34
- * @returns true when the payload is safe to splice into the draft.
35
- */
36
- export function isValidFileDragPayload(path: string): boolean {
37
- if (path === '' || path.length > 512) return false
38
- if (path.startsWith('/') || path.includes('\\')) return false
39
- // eslint-disable-next-line no-control-regex
40
- if (/[\x00-\x1f\x7f]/.test(path)) return false
41
- if (path.split('/').some(segment => segment === '..')) return false
42
- return true
43
- }
44
-
45
- /**
46
- * Splice a workspace-relative path into a composer draft at the caret.
47
- *
48
- * Separator rule: one space is added before the path unless the caret sits
49
- * at the start of the draft or right after whitespace; one space is added
50
- * after the path unless the caret sits at the end of the draft or right
51
- * before whitespace. Empty path or an out-of-range caret are no-ops.
52
- *
53
- * @param draft - the current draft text.
54
- * @param path - the relative path to insert.
55
- * @param caret - insertion offset (default: the end of the draft).
56
- * @returns the next draft; the caller owns writing it through the input
57
- * facade.
58
- */
59
- export function insertPathIntoDraft(draft: string, path: string, caret?: number): string {
60
- if (path === '') return draft
61
- const at = caret === undefined ? draft.length : Math.min(Math.max(caret, 0), draft.length)
62
- const before = draft.slice(0, at)
63
- const after = draft.slice(at)
64
- const needBefore = before !== '' && !/\s$/.test(before)
65
- const needAfter = after !== '' && !/^\s/.test(after)
66
- return before + (needBefore ? ' ' : '') + path + (needAfter ? ' ' : '') + after
67
- }
1
+ /**
2
+ * Pure drag-to-composer helpers shared by the explorer rows (the drag
3
+ * source) and the composer dock inlay (the drop target): the custom MIME
4
+ * type, the drag-state detector, and the draft-splicing rule. Deliberately
5
+ * framework-free so the splicing math is unit-testable in isolation.
6
+ *
7
+ * The composer host only accepts OS image drops (its document-level drop
8
+ * handler checks `dataTransfer.types` for `Files` and routes through the
9
+ * image pipeline), so a workspace file needs its own MIME. A plain relative
10
+ * path is inserted into the draft — the agent reads the file through its
11
+ * existing tools without any prefix grammar.
12
+ * @module dsh-filemgr/client/drag/file-drag
13
+ */
14
+
15
+ /** Custom MIME carrying a workspace-relative file path. */
16
+ export const FILE_DRAG_MIME = 'application/x-dsh-file'
17
+
18
+ /**
19
+ * Whether a drag event carries our file payload.
20
+ * @param types - the live `dataTransfer.types` list (read-only during drag).
21
+ * @returns true when our MIME is present.
22
+ */
23
+ export function hasFileDrag(types: readonly string[] | undefined): boolean {
24
+ return types !== undefined && types.includes(FILE_DRAG_MIME)
25
+ }
26
+
27
+ /**
28
+ * Whether a drop payload is a plausible workspace-relative path. The custom
29
+ * MIME only proves the drag carries *some* string — a foreign page can set it
30
+ * too, so the payload itself is validated before it reaches the draft:
31
+ * relative POSIX shape only; no absolute paths, no '..' segments, no
32
+ * backslashes, no control characters, and a sane length.
33
+ * @param path - the raw payload from dataTransfer.
34
+ * @returns true when the payload is safe to splice into the draft.
35
+ */
36
+ export function isValidFileDragPayload(path: string): boolean {
37
+ if (path === '' || path.length > 512) return false
38
+ if (path.startsWith('/') || path.includes('\\')) return false
39
+ // eslint-disable-next-line no-control-regex
40
+ if (/[\x00-\x1f\x7f]/.test(path)) return false
41
+ if (path.split('/').some(segment => segment === '..')) return false
42
+ return true
43
+ }
44
+
45
+ /**
46
+ * Splice a workspace-relative path into a composer draft at the caret.
47
+ *
48
+ * Separator rule: one space is added before the path unless the caret sits
49
+ * at the start of the draft or right after whitespace; one space is added
50
+ * after the path unless the caret sits at the end of the draft or right
51
+ * before whitespace. Empty path or an out-of-range caret are no-ops.
52
+ *
53
+ * @param draft - the current draft text.
54
+ * @param path - the relative path to insert.
55
+ * @param caret - insertion offset (default: the end of the draft).
56
+ * @returns the next draft; the caller owns writing it through the input
57
+ * facade.
58
+ */
59
+ export function insertPathIntoDraft(draft: string, path: string, caret?: number): string {
60
+ if (path === '') return draft
61
+ const at = caret === undefined ? draft.length : Math.min(Math.max(caret, 0), draft.length)
62
+ const before = draft.slice(0, at)
63
+ const after = draft.slice(at)
64
+ const needBefore = before !== '' && !/\s$/.test(before)
65
+ const needAfter = after !== '' && !/^\s/.test(after)
66
+ return before + (needBefore ? ' ' : '') + path + (needAfter ? ' ' : '') + after
67
+ }
@@ -1,154 +1,154 @@
1
- /**
2
- * Framework-free drag machinery shared by the React hook (useResizableSplit)
3
- * and the DOM layout controller: one pointer-down handler that owns capture,
4
- * the rAF-flush loop, the e.buttons === 0 fallback, every end path, and the
5
- * drag styles. Pure DOM — no React, so both callers run the exact same
6
- * smoothness core (FileManager's useResizableSplit architecture, re-implemented).
7
- * @module dsh-filemgr/client/drag
8
- */
9
-
10
- export interface DragStartOptions {
11
- /** Drag left = grow (handle on the left edge of its container). */
12
- reverse: boolean
13
- /** Width at drag start (read at pointer-down time). */
14
- getStartWidth: () => number
15
- /** Clamped width from the raw delta (px mode adds deltaX directly). */
16
- compute: (startWidth: number, deltaX: number) => number
17
- /** Per-frame flush (rAF-merged; called at most once per frame). */
18
- onFrame: (width: number) => void
19
- /** Final commit (also fired on every end path). */
20
- onEnd: (width: number) => void
21
- }
22
-
23
- /** Whether a pointer event is the primary (left) button or touch. */
24
- function isPrimaryPointer(event: PointerEvent): boolean {
25
- return event.pointerType === 'touch' || event.button === 0
26
- }
27
-
28
- /**
29
- * Handle one pointer-down: wire capture + window listeners, run the rAF
30
- * loop, and end on any of the five termination paths. Call from a
31
- * onPointerDown handler (React or plain DOM).
32
- * @param event - the raw pointerdown event.
33
- * @param el - the handle element (capture target + reverse marker source).
34
- * @param opts - drag behavior.
35
- * @returns a disposer (idempotent; also called internally on end).
36
- */
37
- export function handlePointerDragStart(event: PointerEvent, el: HTMLElement, opts: DragStartOptions): () => void {
38
- if (!isPrimaryPointer(event)) return () => {}
39
- event.preventDefault()
40
-
41
- const startX = event.clientX
42
- const startWidth = opts.getStartWidth()
43
- const pointerId = event.pointerId
44
- const reverse = opts.reverse
45
-
46
- let rafId: number | null = null
47
- let pendingWidth: number | null = null
48
- let latestWidth = startWidth
49
- let isDragging = true
50
- let cleanup: (() => void) | null = null
51
-
52
- const flushPending = (): void => {
53
- if (pendingWidth === null) return
54
- latestWidth = pendingWidth
55
- opts.onFrame(pendingWidth)
56
- }
57
-
58
- const addWindowListener = <K extends keyof WindowEventMap>(
59
- key: K,
60
- handler: (e: WindowEventMap[K]) => void,
61
- ): (() => void) => {
62
- window.addEventListener(key, handler)
63
- return () => window.removeEventListener(key, handler)
64
- }
65
-
66
- const computeWidth = (clientX: number): number => {
67
- const deltaX = reverse ? startX - clientX : clientX - startX
68
- return opts.compute(startWidth, deltaX)
69
- }
70
-
71
- const finishDrag = (e?: PointerEvent | MouseEvent | FocusEvent): void => {
72
- if (!isDragging) return
73
- isDragging = false
74
- if (rafId !== null) {
75
- cancelAnimationFrame(rafId)
76
- rafId = null
77
- }
78
- flushPending()
79
- let finalWidth = latestWidth
80
- if (e !== undefined && 'clientX' in e && typeof e.clientX === 'number') {
81
- finalWidth = computeWidth(e.clientX)
82
- }
83
- opts.onEnd(finalWidth)
84
- cleanup?.()
85
- }
86
-
87
- const handlePointerMove = (e: PointerEvent): void => {
88
- if (!isDragging) return
89
- if (e.buttons === 0) {
90
- // Lost pointerup (capture steal / release outside): end now.
91
- finishDrag(e)
92
- return
93
- }
94
- pendingWidth = computeWidth(e.clientX)
95
- if (rafId === null) {
96
- rafId = requestAnimationFrame(() => {
97
- rafId = null
98
- flushPending()
99
- })
100
- }
101
- }
102
- const handlePointerUp = (e: PointerEvent): void => finishDrag(e)
103
- const handlePointerCancel = (e: PointerEvent): void => finishDrag(e)
104
- const handleMouseUp = (e: MouseEvent): void => finishDrag(e)
105
- const handleLostCapture = (): void => finishDrag()
106
-
107
- // Drag styles: no text selection, col-resize cursor, disable transitions.
108
- const previousUserSelect = document.body.style.userSelect
109
- const previousCursor = document.body.style.cursor
110
- document.body.style.userSelect = 'none'
111
- document.body.style.cursor = 'col-resize'
112
- const frame = el.closest('[data-dsh-frame]')
113
- frame?.setAttribute('data-filemgr-instant', '')
114
-
115
- const restore = (): void => {
116
- document.body.style.userSelect = previousUserSelect
117
- document.body.style.cursor = previousCursor
118
- frame?.removeAttribute('data-filemgr-instant')
119
- }
120
-
121
- if (el.setPointerCapture) {
122
- try {
123
- el.setPointerCapture(pointerId)
124
- el.addEventListener('lostpointercapture', handleLostCapture)
125
- } catch {
126
- // capture failed; the window listeners cover the drag
127
- }
128
- }
129
-
130
- const releaseCapture = (): void => {
131
- try {
132
- if (el.releasePointerCapture && el.hasPointerCapture?.(pointerId)) {
133
- el.releasePointerCapture(pointerId)
134
- }
135
- } catch {
136
- // already released
137
- }
138
- el.removeEventListener('lostpointercapture', handleLostCapture)
139
- }
140
-
141
- const listeners = [
142
- addWindowListener('pointermove', handlePointerMove),
143
- addWindowListener('pointerup', handlePointerUp),
144
- addWindowListener('pointercancel', handlePointerCancel),
145
- addWindowListener('mouseup', handleMouseUp),
146
- addWindowListener('blur', () => finishDrag()),
147
- ]
148
- cleanup = () => {
149
- restore()
150
- releaseCapture()
151
- for (const dispose of listeners) dispose()
152
- }
153
- return cleanup
154
- }
1
+ /**
2
+ * Framework-free drag machinery shared by the React hook (useResizableSplit)
3
+ * and the DOM layout controller: one pointer-down handler that owns capture,
4
+ * the rAF-flush loop, the e.buttons === 0 fallback, every end path, and the
5
+ * drag styles. Pure DOM — no React, so both callers run the exact same
6
+ * smoothness core (FileManager's useResizableSplit architecture, re-implemented).
7
+ * @module dsh-filemgr/client/drag
8
+ */
9
+
10
+ export interface DragStartOptions {
11
+ /** Drag left = grow (handle on the left edge of its container). */
12
+ reverse: boolean
13
+ /** Width at drag start (read at pointer-down time). */
14
+ getStartWidth: () => number
15
+ /** Clamped width from the raw delta (px mode adds deltaX directly). */
16
+ compute: (startWidth: number, deltaX: number) => number
17
+ /** Per-frame flush (rAF-merged; called at most once per frame). */
18
+ onFrame: (width: number) => void
19
+ /** Final commit (also fired on every end path). */
20
+ onEnd: (width: number) => void
21
+ }
22
+
23
+ /** Whether a pointer event is the primary (left) button or touch. */
24
+ function isPrimaryPointer(event: PointerEvent): boolean {
25
+ return event.pointerType === 'touch' || event.button === 0
26
+ }
27
+
28
+ /**
29
+ * Handle one pointer-down: wire capture + window listeners, run the rAF
30
+ * loop, and end on any of the five termination paths. Call from a
31
+ * onPointerDown handler (React or plain DOM).
32
+ * @param event - the raw pointerdown event.
33
+ * @param el - the handle element (capture target + reverse marker source).
34
+ * @param opts - drag behavior.
35
+ * @returns a disposer (idempotent; also called internally on end).
36
+ */
37
+ export function handlePointerDragStart(event: PointerEvent, el: HTMLElement, opts: DragStartOptions): () => void {
38
+ if (!isPrimaryPointer(event)) return () => {}
39
+ event.preventDefault()
40
+
41
+ const startX = event.clientX
42
+ const startWidth = opts.getStartWidth()
43
+ const pointerId = event.pointerId
44
+ const reverse = opts.reverse
45
+
46
+ let rafId: number | null = null
47
+ let pendingWidth: number | null = null
48
+ let latestWidth = startWidth
49
+ let isDragging = true
50
+ let cleanup: (() => void) | null = null
51
+
52
+ const flushPending = (): void => {
53
+ if (pendingWidth === null) return
54
+ latestWidth = pendingWidth
55
+ opts.onFrame(pendingWidth)
56
+ }
57
+
58
+ const addWindowListener = <K extends keyof WindowEventMap>(
59
+ key: K,
60
+ handler: (e: WindowEventMap[K]) => void,
61
+ ): (() => void) => {
62
+ window.addEventListener(key, handler)
63
+ return () => window.removeEventListener(key, handler)
64
+ }
65
+
66
+ const computeWidth = (clientX: number): number => {
67
+ const deltaX = reverse ? startX - clientX : clientX - startX
68
+ return opts.compute(startWidth, deltaX)
69
+ }
70
+
71
+ const finishDrag = (e?: PointerEvent | MouseEvent | FocusEvent): void => {
72
+ if (!isDragging) return
73
+ isDragging = false
74
+ if (rafId !== null) {
75
+ cancelAnimationFrame(rafId)
76
+ rafId = null
77
+ }
78
+ flushPending()
79
+ let finalWidth = latestWidth
80
+ if (e !== undefined && 'clientX' in e && typeof e.clientX === 'number') {
81
+ finalWidth = computeWidth(e.clientX)
82
+ }
83
+ opts.onEnd(finalWidth)
84
+ cleanup?.()
85
+ }
86
+
87
+ const handlePointerMove = (e: PointerEvent): void => {
88
+ if (!isDragging) return
89
+ if (e.buttons === 0) {
90
+ // Lost pointerup (capture steal / release outside): end now.
91
+ finishDrag(e)
92
+ return
93
+ }
94
+ pendingWidth = computeWidth(e.clientX)
95
+ if (rafId === null) {
96
+ rafId = requestAnimationFrame(() => {
97
+ rafId = null
98
+ flushPending()
99
+ })
100
+ }
101
+ }
102
+ const handlePointerUp = (e: PointerEvent): void => finishDrag(e)
103
+ const handlePointerCancel = (e: PointerEvent): void => finishDrag(e)
104
+ const handleMouseUp = (e: MouseEvent): void => finishDrag(e)
105
+ const handleLostCapture = (): void => finishDrag()
106
+
107
+ // Drag styles: no text selection, col-resize cursor, disable transitions.
108
+ const previousUserSelect = document.body.style.userSelect
109
+ const previousCursor = document.body.style.cursor
110
+ document.body.style.userSelect = 'none'
111
+ document.body.style.cursor = 'col-resize'
112
+ const frame = el.closest('[data-dsh-frame]')
113
+ frame?.setAttribute('data-filemgr-instant', '')
114
+
115
+ const restore = (): void => {
116
+ document.body.style.userSelect = previousUserSelect
117
+ document.body.style.cursor = previousCursor
118
+ frame?.removeAttribute('data-filemgr-instant')
119
+ }
120
+
121
+ if (el.setPointerCapture) {
122
+ try {
123
+ el.setPointerCapture(pointerId)
124
+ el.addEventListener('lostpointercapture', handleLostCapture)
125
+ } catch {
126
+ // capture failed; the window listeners cover the drag
127
+ }
128
+ }
129
+
130
+ const releaseCapture = (): void => {
131
+ try {
132
+ if (el.releasePointerCapture && el.hasPointerCapture?.(pointerId)) {
133
+ el.releasePointerCapture(pointerId)
134
+ }
135
+ } catch {
136
+ // already released
137
+ }
138
+ el.removeEventListener('lostpointercapture', handleLostCapture)
139
+ }
140
+
141
+ const listeners = [
142
+ addWindowListener('pointermove', handlePointerMove),
143
+ addWindowListener('pointerup', handlePointerUp),
144
+ addWindowListener('pointercancel', handlePointerCancel),
145
+ addWindowListener('mouseup', handleMouseUp),
146
+ addWindowListener('blur', () => finishDrag()),
147
+ ]
148
+ cleanup = () => {
149
+ restore()
150
+ releaseCapture()
151
+ for (const dispose of listeners) dispose()
152
+ }
153
+ return cleanup
154
+ }
@@ -56,3 +56,37 @@ export function registerFileReferenceSource(ctx: ClientContext): () => void {
56
56
  return () => {}
57
57
  }
58
58
  }
59
+
60
+ /** The roster name Excel-selection chips address (see reference.ts). */
61
+ export const SHEET_SOURCE_NAME = 'sheet'
62
+
63
+ /**
64
+ * Register the inert `sheet` reference source (codec only). Mirrors the
65
+ * `file` source: it isn't a menu participant, its only job is to let the
66
+ * shell's submit pipeline serialize the `相对路径!Sheet名!Range` chip text.
67
+ * Inert — no pick, no menu candidates; Excel references are minted by
68
+ * reference.ts's appendSheetChip, purely for chip appearance.
69
+ */
70
+ export function registerSheetReferenceSource(ctx: ClientContext): () => void {
71
+ const inputTriggers = (ctx.get as (name: string) => unknown)('inputTriggers') as
72
+ | { registerSource(src: InputTriggerSource): () => void }
73
+ | undefined
74
+ if (inputTriggers === undefined) return () => {}
75
+ const source: InputTriggerSource = {
76
+ trigger: '@',
77
+ name: SHEET_SOURCE_NAME,
78
+ candidates: async () => [],
79
+ onPick: () => undefined,
80
+ codec: {
81
+ clipboardText: (ref) => ref,
82
+ serialize: (ref, _signal) => Promise.resolve(ref),
83
+ },
84
+ }
85
+ try {
86
+ return inputTriggers.registerSource(source)
87
+ } catch (error) {
88
+ // Duplicate registration (HMR remount) is not a failure.
89
+ console.warn('[dsh-filemgr] sheet reference source already registered:', error)
90
+ return () => {}
91
+ }
92
+ }
@@ -86,6 +86,16 @@ export function isEditableType(type: PreviewContentType): boolean {
86
86
  return type === 'markdown' || type === 'html' || type === 'code' || type === 'csv' || type === 'text'
87
87
  }
88
88
 
89
+ /** Whether a file path names an editable .xlsx (the only editable excel kind). */
90
+ export function isExcelEditablePath(path: string): boolean {
91
+ return path.toLowerCase().endsWith('.xlsx')
92
+ }
93
+
94
+ /** Whether a file path names an editable .pptx (the only editable ppt kind). */
95
+ export function isPptEditablePath(path: string): boolean {
96
+ return path.toLowerCase().endsWith('.pptx')
97
+ }
98
+
89
99
  /** Whether the type reads its content as text (vs image data URL). */
90
100
  export function isTextType(type: PreviewContentType): boolean {
91
101
  return type !== 'image' && type !== 'pdf' && type !== 'word' && type !== 'excel'
@@ -117,13 +127,30 @@ export function parentRel(path: string): string {
117
127
  }
118
128
 
119
129
  /**
120
- * The streaming URL a pdf tab renders: the host raw route serves the bytes
121
- * with mime application/pdf, so the preview iframe loads them directly — no
122
- * base64 round-trip and no read-size cap. The nonce defeats browser caching
123
- * when the tab is refreshed after the file changed on disk.
130
+ * The streaming URL a pdf/office tab renders: the host raw route serves the
131
+ * bytes with the derived mime, so the preview iframe / viewer loads them
132
+ * directly — no base64 round-trip and no read-size cap. The nonce defeats
133
+ * browser caching when the tab is refreshed after the file changed on disk.
134
+ */
135
+ export function rawPreviewUrl(root: string, path: string, nonce: number): string {
136
+ return `/filemgr/raw?root=${encodeURIComponent(root)}&path=${encodeURIComponent(path)}&v=${nonce}`
137
+ }
138
+
139
+ /**
140
+ * Whether a content type streams its bytes through the raw route (never the
141
+ * /read text path): pdf and the office kinds. The tab's `content` is the
142
+ * raw-route URL; the viewer fetches it into bytes itself.
143
+ */
144
+ export function isStreamedType(type: PreviewContentType): boolean {
145
+ return type === 'pdf' || type === 'word' || type === 'excel' || type === 'ppt'
146
+ }
147
+
148
+ /**
149
+ * The streaming URL a pdf tab renders (mime application/pdf; the preview
150
+ * iframe loads it directly). Kept as the pdf-specific alias of rawPreviewUrl.
124
151
  *
125
152
  * Contributed by EricWang1358 (#239).
126
153
  */
127
154
  export function pdfPreviewUrl(root: string, path: string, nonce: number): string {
128
- return `/filemgr/raw?root=${encodeURIComponent(root)}&path=${encodeURIComponent(path)}&v=${nonce}`
155
+ return rawPreviewUrl(root, path, nonce)
129
156
  }