dsh-explorer 0.0.0-stage → 0.1.0

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.
@@ -0,0 +1,209 @@
1
+ import { fetchFileContent } from './api.ts'
2
+ import {
3
+ insertMentionToComposer,
4
+ insertSnippetToComposer,
5
+ resolveActiveSessionId,
6
+ } from './conversation-draft.ts'
7
+
8
+ export interface DragExplorerItem {
9
+ path: string
10
+ relativePath: string
11
+ isDir: boolean
12
+ name: string
13
+ }
14
+
15
+ export const DSH_EXPLORER_MIME = 'application/vnd.dsh.explorer-items'
16
+
17
+ let currentDragItems: DragExplorerItem[] | null = null
18
+
19
+ export function setupDragStart(
20
+ e: React.DragEvent,
21
+ items: DragExplorerItem[],
22
+ ): void {
23
+ currentDragItems = items
24
+ const payload = JSON.stringify(items)
25
+ try {
26
+ e.dataTransfer.setData(DSH_EXPLORER_MIME, payload)
27
+ } catch {}
28
+ const plainText = items
29
+ .map(item => (item.isDir ? `@${item.relativePath}/` : `@${item.relativePath}`))
30
+ .join(' ')
31
+ try {
32
+ e.dataTransfer.setData('text/plain', plainText)
33
+ } catch {}
34
+ e.dataTransfer.effectAllowed = 'copy'
35
+ }
36
+
37
+ export function setupDragEnd(): void {
38
+ setTimeout(() => {
39
+ currentDragItems = null
40
+ }, 200)
41
+ }
42
+
43
+ let activeOverlay: HTMLDivElement | null = null
44
+
45
+ function getOrCreateOverlay(targetEl: HTMLElement): HTMLDivElement {
46
+ if (activeOverlay && document.body.contains(activeOverlay)) {
47
+ return activeOverlay
48
+ }
49
+ const div = document.createElement('div')
50
+ div.style.position = 'absolute'
51
+ div.style.inset = '0'
52
+ div.style.backgroundColor = 'rgba(33, 150, 243, 0.08)'
53
+ div.style.border = '2px dashed #007fd4'
54
+ div.style.borderRadius = '8px'
55
+ div.style.zIndex = '9999'
56
+ div.style.pointerEvents = 'none'
57
+ div.style.display = 'flex'
58
+ div.style.flexDirection = 'column'
59
+ div.style.alignItems = 'center'
60
+ div.style.justifyContent = 'center'
61
+ div.style.gap = '8px'
62
+ div.style.backdropFilter = 'blur(2px)'
63
+ div.style.transition = 'opacity 0.15s cubic-bezier(0.2, 0, 0, 1)'
64
+
65
+ div.innerHTML = `
66
+ <div style="font-weight: 600; font-size: 13px; color: #58a6ff; background: rgba(22, 27, 34, 0.85); border: 1px solid rgba(88, 166, 255, 0.3); padding: 8px 16px; border-radius: 6px; box-shadow: 0 4px 16px rgba(0,0,0,0.4); display: flex; align-items: center; gap: 8px;">
67
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
68
+ <path d="M8 2a.75.75 0 0 1 .75.75v8.69l3.22-3.22a.75.75 0 1 1 1.06 1.06l-4.5 4.5a.75.75 0 0 1-1.06 0l-4.5-4.5a.75.75 0 1 1 1.06-1.06l3.22 3.22V2.75A.75.75 0 0 1 8 2z"/>
69
+ </svg>
70
+ Drop at cursor to mention
71
+ </div>
72
+ <div style="font-size: 11px; opacity: 0.9; color: #8b949e; background: rgba(22, 27, 34, 0.7); padding: 3px 8px; border-radius: 4px; border: 1px solid rgba(255,255,255,0.06);">
73
+ Hold <b>Alt</b> or <b>Shift</b> to embed code snippet
74
+ </div>
75
+ `
76
+ activeOverlay = div
77
+ return div
78
+ }
79
+
80
+ export function attachGlobalConversationDropListener(ctx: any): () => void {
81
+ let dragDepth = 0
82
+
83
+ const hasExplorerData = (e: DragEvent): boolean => {
84
+ if (currentDragItems && currentDragItems.length > 0) return true
85
+ try {
86
+ const types = Array.from(e.dataTransfer?.types ?? [])
87
+ return types.includes(DSH_EXPLORER_MIME) || types.includes('text/plain')
88
+ } catch {
89
+ return false
90
+ }
91
+ }
92
+
93
+ const getConversationContainer = (): HTMLElement | null => {
94
+ return (
95
+ document.querySelector<HTMLElement>('[data-slot="conversation.session"]') ??
96
+ document.querySelector<HTMLElement>('[data-slot="conversation.shell"]') ??
97
+ document.querySelector<HTMLElement>('[data-slot*="conversation"]') ??
98
+ document.querySelector<HTMLElement>('.an3WzG_root') ??
99
+ document.querySelector<HTMLElement>('.conversation-main') ??
100
+ document.querySelector<HTMLElement>('main') ??
101
+ document.querySelector<HTMLElement>('#root > div') ??
102
+ null
103
+ )
104
+ }
105
+
106
+ const handleDragEnter = (e: DragEvent) => {
107
+ if (!hasExplorerData(e)) return
108
+ const container = getConversationContainer()
109
+ if (!container) return
110
+
111
+ dragDepth += 1
112
+ const overlay = getOrCreateOverlay(container)
113
+ if (!container.contains(overlay)) {
114
+ if (getComputedStyle(container).position === 'static') {
115
+ container.style.position = 'relative'
116
+ }
117
+ container.appendChild(overlay)
118
+ }
119
+ }
120
+
121
+ const handleDragOver = (e: DragEvent) => {
122
+ if (!hasExplorerData(e)) return
123
+ e.preventDefault()
124
+ if (e.dataTransfer) {
125
+ e.dataTransfer.dropEffect = 'copy'
126
+ }
127
+ }
128
+
129
+ const handleDragLeave = (e: DragEvent) => {
130
+ if (!hasExplorerData(e)) return
131
+ dragDepth -= 1
132
+ if (dragDepth <= 0) {
133
+ dragDepth = 0
134
+ activeOverlay?.remove()
135
+ }
136
+ }
137
+
138
+ const handleDrop = async (e: DragEvent) => {
139
+ if (!hasExplorerData(e)) return
140
+ e.preventDefault()
141
+ e.stopPropagation()
142
+
143
+ dragDepth = 0
144
+ activeOverlay?.remove()
145
+
146
+ let items = currentDragItems
147
+ if (!items || items.length === 0) {
148
+ try {
149
+ const raw = e.dataTransfer?.getData(DSH_EXPLORER_MIME)
150
+ if (raw) items = JSON.parse(raw) as DragExplorerItem[]
151
+ } catch {}
152
+ }
153
+
154
+ if (!items || items.length === 0) {
155
+ // Fallback: try reading plain text
156
+ const plain = e.dataTransfer?.getData('text/plain')
157
+ if (plain && plain.startsWith('@')) {
158
+ const relativePath = plain.replace(/^@"?/, '').replace(/"?$/, '')
159
+ items = [{
160
+ path: relativePath,
161
+ relativePath,
162
+ isDir: relativePath.endsWith('/'),
163
+ name: relativePath,
164
+ }]
165
+ }
166
+ }
167
+
168
+ if (!items || items.length === 0) return
169
+
170
+ try {
171
+ const activeSessionId = resolveActiveSessionId(ctx)
172
+ const isAlt = e.altKey || e.shiftKey
173
+ let currentPoint: { x: number; y: number } | undefined = { x: e.clientX, y: e.clientY }
174
+
175
+ for (const item of items) {
176
+ if (isAlt && !item.isDir) {
177
+ try {
178
+ const content = await fetchFileContent(item.path)
179
+ insertSnippetToComposer(ctx, activeSessionId, item.relativePath, content, currentPoint)
180
+ } catch {
181
+ insertMentionToComposer(ctx, activeSessionId, item.relativePath, item.isDir, currentPoint)
182
+ }
183
+ } else {
184
+ insertMentionToComposer(ctx, activeSessionId, item.relativePath, item.isDir, currentPoint)
185
+ }
186
+ currentPoint = undefined
187
+ if (items.length > 1) {
188
+ await new Promise((resolve) => setTimeout(resolve, 40))
189
+ }
190
+ }
191
+ } catch (err) {
192
+ console.warn('[dsh-explorer] drop failed:', err)
193
+ }
194
+ }
195
+
196
+ // Use capture phase so we intercept before child components swallow
197
+ document.addEventListener('dragenter', handleDragEnter, true)
198
+ document.addEventListener('dragover', handleDragOver, true)
199
+ document.addEventListener('dragleave', handleDragLeave, true)
200
+ document.addEventListener('drop', handleDrop, true)
201
+
202
+ return () => {
203
+ document.removeEventListener('dragenter', handleDragEnter, true)
204
+ document.removeEventListener('dragover', handleDragOver, true)
205
+ document.removeEventListener('dragleave', handleDragLeave, true)
206
+ document.removeEventListener('drop', handleDrop, true)
207
+ activeOverlay?.remove()
208
+ }
209
+ }