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.
- package/LICENSE +22 -0
- package/README.md +106 -2
- package/cordis.patch.yml +5 -0
- package/icon.svg +7 -0
- package/lib/client.js +2814 -0
- package/lib/index.js +747 -0
- package/package.json +92 -4
- package/src/client/ExplorerContextMenu.tsx +159 -0
- package/src/client/ExplorerHeader.tsx +75 -0
- package/src/client/ExplorerOptionsModal.tsx +243 -0
- package/src/client/ExplorerRow.tsx +240 -0
- package/src/client/ExplorerSearch.tsx +42 -0
- package/src/client/ExplorerView.tsx +1139 -0
- package/src/client/api.ts +121 -0
- package/src/client/conversation-draft.ts +635 -0
- package/src/client/css-modules.d.ts +5 -0
- package/src/client/drag-drop.ts +209 -0
- package/src/client/explorer.module.css +628 -0
- package/src/client/index.tsx +160 -0
- package/src/client/options.ts +59 -0
- package/src/client/vscode-theme.tsx +250 -0
- package/src/config.ts +11 -0
- package/src/fs-operations.ts +116 -0
- package/src/fs-tree.ts +155 -0
- package/src/fs-watch.ts +179 -0
- package/src/git-status.ts +161 -0
- package/src/index.ts +165 -0
- package/src/path-security.ts +22 -0
- package/src/session-path.ts +35 -0
- package/src/wire.ts +63 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import React, { useState, useRef, useEffect } from 'react'
|
|
2
|
+
import css from './explorer.module.css'
|
|
3
|
+
import type { FsEntry } from './api.ts'
|
|
4
|
+
import type { SingleClickAction } from './options.ts'
|
|
5
|
+
import { renderFileIcon } from './vscode-theme.tsx'
|
|
6
|
+
import { setupDragStart, setupDragEnd, type DragExplorerItem } from './drag-drop.ts'
|
|
7
|
+
|
|
8
|
+
interface ExplorerRowProps {
|
|
9
|
+
entry: FsEntry
|
|
10
|
+
depth: number
|
|
11
|
+
isExpanded: boolean
|
|
12
|
+
isSelected: boolean
|
|
13
|
+
isFocused: boolean
|
|
14
|
+
gitStatus?: string
|
|
15
|
+
relativePath: string
|
|
16
|
+
compactChain?: FsEntry[]
|
|
17
|
+
indentStep?: number
|
|
18
|
+
iconSize?: number
|
|
19
|
+
enableMotion?: boolean
|
|
20
|
+
showGitBadges?: boolean
|
|
21
|
+
compactFolders?: boolean
|
|
22
|
+
singleClickAction?: SingleClickAction
|
|
23
|
+
onToggle: () => void
|
|
24
|
+
onSelect: (e: React.MouseEvent) => void
|
|
25
|
+
onDoubleClick?: () => void
|
|
26
|
+
onContextMenu: (e: React.MouseEvent) => void
|
|
27
|
+
onInsertReference: () => void
|
|
28
|
+
onCopyRelativePath: () => void
|
|
29
|
+
onRenameSubmit: (newName: string) => void
|
|
30
|
+
onDelete: () => void
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const ExplorerRow: React.FC<ExplorerRowProps> = ({
|
|
34
|
+
entry,
|
|
35
|
+
depth,
|
|
36
|
+
isExpanded,
|
|
37
|
+
isSelected,
|
|
38
|
+
isFocused,
|
|
39
|
+
gitStatus,
|
|
40
|
+
relativePath,
|
|
41
|
+
compactChain,
|
|
42
|
+
indentStep = 16,
|
|
43
|
+
iconSize = 16,
|
|
44
|
+
enableMotion = true,
|
|
45
|
+
showGitBadges = true,
|
|
46
|
+
compactFolders = true,
|
|
47
|
+
singleClickAction = 'select',
|
|
48
|
+
onToggle,
|
|
49
|
+
onSelect,
|
|
50
|
+
onDoubleClick,
|
|
51
|
+
onContextMenu,
|
|
52
|
+
onInsertReference,
|
|
53
|
+
onCopyRelativePath,
|
|
54
|
+
onRenameSubmit,
|
|
55
|
+
onDelete,
|
|
56
|
+
}) => {
|
|
57
|
+
const [isRenaming, setIsRenaming] = useState(false)
|
|
58
|
+
const [renameVal, setRenameVal] = useState(entry.name)
|
|
59
|
+
const inputRef = useRef<HTMLInputElement>(null)
|
|
60
|
+
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (isRenaming) {
|
|
63
|
+
inputRef.current?.focus()
|
|
64
|
+
inputRef.current?.select()
|
|
65
|
+
}
|
|
66
|
+
}, [isRenaming])
|
|
67
|
+
|
|
68
|
+
const handleDragStart = (e: React.DragEvent) => {
|
|
69
|
+
const item: DragExplorerItem = {
|
|
70
|
+
path: entry.path,
|
|
71
|
+
relativePath,
|
|
72
|
+
isDir: entry.isDir,
|
|
73
|
+
name: entry.name,
|
|
74
|
+
}
|
|
75
|
+
setupDragStart(e, [item])
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const handleRenameKeyDown = (e: React.KeyboardEvent) => {
|
|
79
|
+
if (e.key === 'Enter') {
|
|
80
|
+
const trimmed = renameVal.trim()
|
|
81
|
+
if (trimmed && trimmed !== entry.name) {
|
|
82
|
+
onRenameSubmit(trimmed)
|
|
83
|
+
}
|
|
84
|
+
setIsRenaming(false)
|
|
85
|
+
} else if (e.key === 'Escape') {
|
|
86
|
+
setRenameVal(entry.name)
|
|
87
|
+
setIsRenaming(false)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Display label: if compactFolders is on and compactChain has elements, join their names (e.g. "vscode/antigravity-quota-context")
|
|
92
|
+
const displayName = compactFolders && compactChain && compactChain.length > 1
|
|
93
|
+
? compactChain.map((c) => c.name).join('/')
|
|
94
|
+
: (compactFolders && entry.compactName
|
|
95
|
+
? entry.compactName
|
|
96
|
+
: entry.name)
|
|
97
|
+
|
|
98
|
+
// Git status styling
|
|
99
|
+
let gitClass = ''
|
|
100
|
+
let gitLetter = ''
|
|
101
|
+
if (gitStatus) {
|
|
102
|
+
if (gitStatus.includes('M')) {
|
|
103
|
+
gitClass = css.gitModified
|
|
104
|
+
gitLetter = 'M'
|
|
105
|
+
} else if (gitStatus.includes('?') || gitStatus.includes('A')) {
|
|
106
|
+
gitClass = css.gitUntracked
|
|
107
|
+
gitLetter = 'U'
|
|
108
|
+
} else if (gitStatus.includes('D')) {
|
|
109
|
+
gitClass = css.gitDeleted
|
|
110
|
+
gitLetter = 'D'
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const isIgnored = gitStatus?.includes('!')
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<div
|
|
118
|
+
className={`${css.row} ${isSelected ? css.rowSelected : ''} ${isFocused ? css.rowFocused : ''}`}
|
|
119
|
+
style={{ paddingLeft: `${depth * indentStep + 8}px` }}
|
|
120
|
+
onClick={(e) => {
|
|
121
|
+
onSelect(e)
|
|
122
|
+
if (entry.isDir) {
|
|
123
|
+
onToggle()
|
|
124
|
+
} else if (singleClickAction === 'mention') {
|
|
125
|
+
onInsertReference()
|
|
126
|
+
}
|
|
127
|
+
}}
|
|
128
|
+
onDoubleClick={(e) => {
|
|
129
|
+
if (!entry.isDir) {
|
|
130
|
+
e.stopPropagation()
|
|
131
|
+
onDoubleClick?.()
|
|
132
|
+
}
|
|
133
|
+
}}
|
|
134
|
+
onContextMenu={onContextMenu}
|
|
135
|
+
draggable={!isRenaming}
|
|
136
|
+
onDragStart={handleDragStart}
|
|
137
|
+
onDragEnd={setupDragEnd}
|
|
138
|
+
>
|
|
139
|
+
{/* Indentation guide lines */}
|
|
140
|
+
{Array.from({ length: depth }).map((_, i) => (
|
|
141
|
+
<span
|
|
142
|
+
key={i}
|
|
143
|
+
className={css.indentGuide}
|
|
144
|
+
style={{ left: `${i * indentStep + 15}px` }}
|
|
145
|
+
/>
|
|
146
|
+
))}
|
|
147
|
+
|
|
148
|
+
{/* Expand/Collapse Chevron */}
|
|
149
|
+
<span
|
|
150
|
+
className={`${css.chevron} ${isExpanded ? css.chevronExpanded : ''} ${!entry.isDir ? css.chevronHidden : ''}`}
|
|
151
|
+
style={!enableMotion ? { transition: 'none' } : undefined}
|
|
152
|
+
onClick={(e) => {
|
|
153
|
+
e.stopPropagation()
|
|
154
|
+
onToggle()
|
|
155
|
+
}}
|
|
156
|
+
>
|
|
157
|
+
<svg width="10" height="10" viewBox="0 0 16 16" fill="currentColor">
|
|
158
|
+
<path d="M6 3.5L10.5 8 6 12.5V3.5Z" />
|
|
159
|
+
</svg>
|
|
160
|
+
</span>
|
|
161
|
+
|
|
162
|
+
{/* Language / Folder Icon */}
|
|
163
|
+
<span className={css.iconWrap}>
|
|
164
|
+
{renderFileIcon(entry.name, entry.isDir, isExpanded, iconSize)}
|
|
165
|
+
</span>
|
|
166
|
+
|
|
167
|
+
{/* Label or Inline Rename */}
|
|
168
|
+
{isRenaming ? (
|
|
169
|
+
<input
|
|
170
|
+
ref={inputRef}
|
|
171
|
+
type="text"
|
|
172
|
+
className={css.inlineInput}
|
|
173
|
+
value={renameVal}
|
|
174
|
+
onChange={(e) => setRenameVal(e.target.value)}
|
|
175
|
+
onKeyDown={handleRenameKeyDown}
|
|
176
|
+
onBlur={() => {
|
|
177
|
+
const trimmed = renameVal.trim()
|
|
178
|
+
if (trimmed && trimmed !== entry.name) {
|
|
179
|
+
onRenameSubmit(trimmed)
|
|
180
|
+
}
|
|
181
|
+
setIsRenaming(false)
|
|
182
|
+
}}
|
|
183
|
+
onClick={(e) => e.stopPropagation()}
|
|
184
|
+
/>
|
|
185
|
+
) : (
|
|
186
|
+
<span
|
|
187
|
+
className={`${css.label} ${isIgnored ? css.labelDimmed : ''} ${showGitBadges ? gitClass : ''}`}
|
|
188
|
+
title={relativePath}
|
|
189
|
+
>
|
|
190
|
+
{displayName}
|
|
191
|
+
</span>
|
|
192
|
+
)}
|
|
193
|
+
|
|
194
|
+
{/* Git status badge */}
|
|
195
|
+
{showGitBadges && gitLetter && <span className={`${css.gitBadge} ${gitClass}`}>{gitLetter}</span>}
|
|
196
|
+
|
|
197
|
+
{/* Hover actions */}
|
|
198
|
+
{!isRenaming && (
|
|
199
|
+
<div className={css.hoverActions} onClick={(e) => e.stopPropagation()}>
|
|
200
|
+
<button
|
|
201
|
+
className={css.actionBtn}
|
|
202
|
+
title="Mention in Chat (@)"
|
|
203
|
+
onClick={(e) => {
|
|
204
|
+
e.stopPropagation()
|
|
205
|
+
onInsertReference()
|
|
206
|
+
}}
|
|
207
|
+
>
|
|
208
|
+
<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor">
|
|
209
|
+
<path d="M8 2a6 6 0 1 0 0 12A6 6 0 0 0 8 2zM7 5h2v2h2v2H9v2H7V9H5V7h2V5z"/>
|
|
210
|
+
</svg>
|
|
211
|
+
</button>
|
|
212
|
+
<button
|
|
213
|
+
className={css.actionBtn}
|
|
214
|
+
title="Copy Relative Path"
|
|
215
|
+
onClick={(e) => {
|
|
216
|
+
e.stopPropagation()
|
|
217
|
+
onCopyRelativePath()
|
|
218
|
+
}}
|
|
219
|
+
>
|
|
220
|
+
<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor">
|
|
221
|
+
<path d="M4 4h6v1H4v9h7v-4h1v5H3V4h1zm3-2h7v7h-1V3.7L7.7 9 7 8.3 12.3 3H7V2z"/>
|
|
222
|
+
</svg>
|
|
223
|
+
</button>
|
|
224
|
+
<button
|
|
225
|
+
className={css.actionBtn}
|
|
226
|
+
title="Rename (F2)"
|
|
227
|
+
onClick={(e) => {
|
|
228
|
+
e.stopPropagation()
|
|
229
|
+
setIsRenaming(true)
|
|
230
|
+
}}
|
|
231
|
+
>
|
|
232
|
+
<svg width="11" height="11" viewBox="0 0 16 16" fill="currentColor">
|
|
233
|
+
<path d="M12.854.146a.5.5 0 0 0-.707 0L10.5 1.793 14.207 5.5l1.647-1.646a.5.5 0 0 0 0-.708l-3-3zm.646 6.061L9.793 2.5 3.293 9H3.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.207l6.5-6.5zm-7.468 7.468A.5.5 0 0 1 6 13.5V13h-.5a.5.5 0 0 1-.5-.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.5-.5V10h-.5a.499.499 0 0 1-.175-.032l-.179.178a.5.5 0 0 0-.11.168l-2 5a.5.5 0 0 0 .65.65l5-2a.5.5 0 0 0 .168-.11l.178-.178z"/>
|
|
234
|
+
</svg>
|
|
235
|
+
</button>
|
|
236
|
+
</div>
|
|
237
|
+
)}
|
|
238
|
+
</div>
|
|
239
|
+
)
|
|
240
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import React, { useRef, useEffect } from 'react'
|
|
2
|
+
import css from './explorer.module.css'
|
|
3
|
+
|
|
4
|
+
interface ExplorerSearchProps {
|
|
5
|
+
value: string
|
|
6
|
+
onChange: (val: string) => void
|
|
7
|
+
onClose: () => void
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const ExplorerSearch: React.FC<ExplorerSearchProps> = ({ value, onChange, onClose }) => {
|
|
11
|
+
const inputRef = useRef<HTMLInputElement>(null)
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
inputRef.current?.focus()
|
|
15
|
+
}, [])
|
|
16
|
+
|
|
17
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
18
|
+
if (e.key === 'Escape') {
|
|
19
|
+
onClose()
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div className={css.searchBar}>
|
|
25
|
+
<input
|
|
26
|
+
ref={inputRef}
|
|
27
|
+
type="text"
|
|
28
|
+
className={css.searchInput}
|
|
29
|
+
placeholder="Filter by name (fuzzy)..."
|
|
30
|
+
value={value}
|
|
31
|
+
onChange={(e) => onChange(e.target.value)}
|
|
32
|
+
onKeyDown={handleKeyDown}
|
|
33
|
+
/>
|
|
34
|
+
{value && (
|
|
35
|
+
<button className={css.clearSearchBtn} title="Clear filter" onClick={() => onChange('')}>
|
|
36
|
+
✕
|
|
37
|
+
</button>
|
|
38
|
+
)}
|
|
39
|
+
</div>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|