dsh-explorer 0.0.0-stage → 0.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.
- package/LICENSE +22 -0
- package/README.md +106 -2
- package/cordis.patch.yml +5 -0
- package/icon.svg +7 -0
- package/lib/client.js +859 -0
- package/lib/index.js +854 -0
- package/package.json +91 -4
- package/src/client/ExplorerView.tsx +981 -0
- package/src/client/components/ExplorerContextMenu.tsx +157 -0
- package/src/client/components/ExplorerDeleteModal.tsx +124 -0
- package/src/client/components/ExplorerEmptyContextMenu.tsx +125 -0
- package/src/client/components/ExplorerHeader.tsx +81 -0
- package/src/client/components/ExplorerOptionsModal.tsx +230 -0
- package/src/client/components/ExplorerRow.tsx +258 -0
- package/src/client/components/ExplorerSearch.tsx +55 -0
- package/src/client/components/TreeNode.tsx +384 -0
- package/src/client/css-modules.d.ts +5 -0
- package/src/client/explorer.module.css +833 -0
- package/src/client/hooks/useKeyboardNav.ts +89 -0
- package/src/client/hooks/useLiveSync.ts +81 -0
- package/src/client/index.tsx +238 -0
- package/src/client/utils/api.ts +251 -0
- package/src/client/utils/conversation-draft.ts +644 -0
- package/src/client/utils/drag-drop.ts +209 -0
- package/src/client/utils/open-tab.ts +69 -0
- package/src/client/utils/options.ts +59 -0
- package/src/client/utils/vscode-theme.tsx +250 -0
- package/src/config.ts +11 -0
- package/src/fs-operations.ts +116 -0
- package/src/fs-search.ts +116 -0
- package/src/fs-tree.ts +155 -0
- package/src/fs-watch.ts +179 -0
- package/src/git-status.ts +167 -0
- package/src/index.ts +177 -0
- package/src/path-security.ts +22 -0
- package/src/session-path.ts +35 -0
- package/src/wire.ts +63 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { baseName } from './conversation-draft.ts'
|
|
2
|
+
|
|
3
|
+
export function safeService<T = any>(ctx: any, name: string): T | undefined {
|
|
4
|
+
if (!ctx) return undefined
|
|
5
|
+
try {
|
|
6
|
+
if (typeof ctx.get === 'function') {
|
|
7
|
+
const val = ctx.get(name)
|
|
8
|
+
if (val !== undefined) return val
|
|
9
|
+
}
|
|
10
|
+
return ctx[name]
|
|
11
|
+
} catch {
|
|
12
|
+
return undefined
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function openFileInTab(ctx: any, sessionId: string, filePath: string, relPath: string): void {
|
|
17
|
+
const normPath = filePath.replace(/\\/g, '/')
|
|
18
|
+
const fileName = baseName(filePath)
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const betterSidebar = safeService(ctx, 'betterSidebar')
|
|
22
|
+
if (betterSidebar && typeof betterSidebar.openTab === 'function') {
|
|
23
|
+
betterSidebar.openTab({
|
|
24
|
+
type: 'editor',
|
|
25
|
+
title: fileName,
|
|
26
|
+
path: filePath,
|
|
27
|
+
id: `editor:${filePath}`,
|
|
28
|
+
})
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
} catch {}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const sidebarRight = safeService(ctx, 'sidebarRight')
|
|
35
|
+
if (sidebarRight && typeof sidebarRight.openResource === 'function') {
|
|
36
|
+
const normRel = relPath.replace(/\\/g, '/')
|
|
37
|
+
const encodePath = (p: string) =>
|
|
38
|
+
p
|
|
39
|
+
.split('/')
|
|
40
|
+
.map((seg) => encodeURIComponent(seg).replace(/%3A/gi, ':'))
|
|
41
|
+
.join('/')
|
|
42
|
+
const address = sessionId
|
|
43
|
+
? `dsh-resource://file/session/${encodeURIComponent(sessionId)}/${encodePath(normRel)}`
|
|
44
|
+
: `dsh-resource://file/absolute/${encodePath(normPath.replace(/^\/+/, ''))}`
|
|
45
|
+
sidebarRight.openResource(address)
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
} catch {}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const tabs = safeService(ctx, 'tabs')
|
|
52
|
+
if (tabs && typeof tabs.open === 'function') {
|
|
53
|
+
tabs.open({ path: filePath, title: fileName })
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
} catch {}
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const remote = ctx?.remote?.session
|
|
60
|
+
if (remote && typeof remote.openWorkspacePath === 'function') {
|
|
61
|
+
remote.openWorkspacePath({ path: filePath })
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
} catch {}
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
window.open(`/dsh-explorer/api/read-file?path=${encodeURIComponent(filePath)}`, '_blank')
|
|
68
|
+
} catch {}
|
|
69
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export type ExplorerDensity = 'compact' | 'normal' | 'relaxed'
|
|
2
|
+
export type SingleClickAction = 'select' | 'mention'
|
|
3
|
+
|
|
4
|
+
export interface ExplorerOptions {
|
|
5
|
+
scale: number
|
|
6
|
+
density: ExplorerDensity
|
|
7
|
+
enableMotion: boolean
|
|
8
|
+
showHiddenFiles: boolean
|
|
9
|
+
showGitBadges: boolean
|
|
10
|
+
compactFolders: boolean
|
|
11
|
+
singleClickAction: SingleClickAction
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const DEFAULT_OPTIONS: ExplorerOptions = {
|
|
15
|
+
scale: 1.0,
|
|
16
|
+
density: 'normal',
|
|
17
|
+
enableMotion: true,
|
|
18
|
+
showHiddenFiles: true,
|
|
19
|
+
showGitBadges: true,
|
|
20
|
+
compactFolders: false,
|
|
21
|
+
singleClickAction: 'select',
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const STORAGE_KEY = 'dsh_explorer_options_v1'
|
|
25
|
+
|
|
26
|
+
export function loadExplorerOptions(): ExplorerOptions {
|
|
27
|
+
if (typeof window === 'undefined' || !window.localStorage) {
|
|
28
|
+
return { ...DEFAULT_OPTIONS }
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
const raw = window.localStorage.getItem(STORAGE_KEY)
|
|
32
|
+
if (!raw) return { ...DEFAULT_OPTIONS }
|
|
33
|
+
const parsed = JSON.parse(raw)
|
|
34
|
+
return {
|
|
35
|
+
scale:
|
|
36
|
+
typeof parsed.scale === 'number' && Number.isFinite(parsed.scale)
|
|
37
|
+
? Math.max(0.75, Math.min(1.35, Math.round(parsed.scale * 100) / 100))
|
|
38
|
+
: DEFAULT_OPTIONS.scale,
|
|
39
|
+
density: parsed.density === 'compact' || parsed.density === 'relaxed' ? parsed.density : 'normal',
|
|
40
|
+
enableMotion: typeof parsed.enableMotion === 'boolean' ? parsed.enableMotion : DEFAULT_OPTIONS.enableMotion,
|
|
41
|
+
showHiddenFiles:
|
|
42
|
+
typeof parsed.showHiddenFiles === 'boolean' ? parsed.showHiddenFiles : DEFAULT_OPTIONS.showHiddenFiles,
|
|
43
|
+
showGitBadges:
|
|
44
|
+
typeof parsed.showGitBadges === 'boolean' ? parsed.showGitBadges : DEFAULT_OPTIONS.showGitBadges,
|
|
45
|
+
compactFolders:
|
|
46
|
+
typeof parsed.compactFolders === 'boolean' ? parsed.compactFolders : DEFAULT_OPTIONS.compactFolders,
|
|
47
|
+
singleClickAction: parsed.singleClickAction === 'mention' ? 'mention' : 'select',
|
|
48
|
+
}
|
|
49
|
+
} catch {
|
|
50
|
+
return { ...DEFAULT_OPTIONS }
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function saveExplorerOptions(options: ExplorerOptions): void {
|
|
55
|
+
if (typeof window === 'undefined' || !window.localStorage) return
|
|
56
|
+
try {
|
|
57
|
+
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(options))
|
|
58
|
+
} catch {}
|
|
59
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
export interface IconStyle {
|
|
4
|
+
color: string
|
|
5
|
+
label?: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function getFileExtension(filename: string): string {
|
|
9
|
+
const at = filename.lastIndexOf('.')
|
|
10
|
+
return at <= 0 ? '' : filename.slice(at + 1).toLowerCase()
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function getLanguageColor(filename: string, isDir: boolean): string {
|
|
14
|
+
if (isDir) return '#dcb67a'
|
|
15
|
+
const ext = getFileExtension(filename)
|
|
16
|
+
const lowerName = filename.toLowerCase()
|
|
17
|
+
|
|
18
|
+
if (lowerName === 'dockerfile' || lowerName.startsWith('dockerfile.')) return '#2496ed'
|
|
19
|
+
if (lowerName === '.gitignore' || lowerName === '.gitattributes') return '#f14e32'
|
|
20
|
+
if (lowerName === 'package.json') return '#cb3837'
|
|
21
|
+
if (lowerName.startsWith('.env')) return '#ecd53f'
|
|
22
|
+
|
|
23
|
+
switch (ext) {
|
|
24
|
+
case 'ts':
|
|
25
|
+
case 'mts':
|
|
26
|
+
case 'cts':
|
|
27
|
+
return '#3178c6'
|
|
28
|
+
case 'tsx':
|
|
29
|
+
return '#3178c6'
|
|
30
|
+
case 'js':
|
|
31
|
+
case 'mjs':
|
|
32
|
+
case 'cjs':
|
|
33
|
+
return '#f7df1e'
|
|
34
|
+
case 'jsx':
|
|
35
|
+
return '#00d8ff'
|
|
36
|
+
case 'py':
|
|
37
|
+
case 'pyw':
|
|
38
|
+
return '#3572a5'
|
|
39
|
+
case 'rs':
|
|
40
|
+
return '#dea584'
|
|
41
|
+
case 'go':
|
|
42
|
+
return '#00add8'
|
|
43
|
+
case 'c':
|
|
44
|
+
case 'h':
|
|
45
|
+
return '#a8b9cc'
|
|
46
|
+
case 'cpp':
|
|
47
|
+
case 'cc':
|
|
48
|
+
case 'cxx':
|
|
49
|
+
case 'hpp':
|
|
50
|
+
return '#00599c'
|
|
51
|
+
case 'cs':
|
|
52
|
+
return '#178600'
|
|
53
|
+
case 'java':
|
|
54
|
+
case 'jar':
|
|
55
|
+
return '#b07219'
|
|
56
|
+
case 'kt':
|
|
57
|
+
case 'kts':
|
|
58
|
+
return '#a97bff'
|
|
59
|
+
case 'swift':
|
|
60
|
+
return '#f05138'
|
|
61
|
+
case 'php':
|
|
62
|
+
return '#777bb4'
|
|
63
|
+
case 'rb':
|
|
64
|
+
return '#cc342d'
|
|
65
|
+
case 'html':
|
|
66
|
+
case 'htm':
|
|
67
|
+
return '#e34c26'
|
|
68
|
+
case 'css':
|
|
69
|
+
return '#42a5f5'
|
|
70
|
+
case 'scss':
|
|
71
|
+
case 'sass':
|
|
72
|
+
return '#cd6799'
|
|
73
|
+
case 'less':
|
|
74
|
+
return '#1d365d'
|
|
75
|
+
case 'json':
|
|
76
|
+
case 'jsonc':
|
|
77
|
+
case 'json5':
|
|
78
|
+
return '#cbcb41'
|
|
79
|
+
case 'yaml':
|
|
80
|
+
case 'yml':
|
|
81
|
+
return '#cb171e'
|
|
82
|
+
case 'toml':
|
|
83
|
+
return '#9c4221'
|
|
84
|
+
case 'md':
|
|
85
|
+
case 'markdown':
|
|
86
|
+
case 'mdx':
|
|
87
|
+
return '#519aba'
|
|
88
|
+
case 'sql':
|
|
89
|
+
return '#e38c00'
|
|
90
|
+
case 'sh':
|
|
91
|
+
case 'bash':
|
|
92
|
+
case 'zsh':
|
|
93
|
+
return '#89e051'
|
|
94
|
+
case 'ps1':
|
|
95
|
+
case 'psm1':
|
|
96
|
+
return '#012456'
|
|
97
|
+
case 'svg':
|
|
98
|
+
return '#ff9800'
|
|
99
|
+
case 'png':
|
|
100
|
+
case 'jpg':
|
|
101
|
+
case 'jpeg':
|
|
102
|
+
case 'gif':
|
|
103
|
+
case 'webp':
|
|
104
|
+
case 'ico':
|
|
105
|
+
return '#ab47bc'
|
|
106
|
+
case 'zip':
|
|
107
|
+
case 'tar':
|
|
108
|
+
case 'gz':
|
|
109
|
+
case '7z':
|
|
110
|
+
case 'rar':
|
|
111
|
+
return '#ec407a'
|
|
112
|
+
default:
|
|
113
|
+
return '#90a4ae'
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function renderFileIcon(filename: string, isDir: boolean, isOpen = false, size = 16): React.ReactNode {
|
|
118
|
+
if (isDir) {
|
|
119
|
+
const folderColor = isOpen ? '#e5c07b' : '#dcb67a'
|
|
120
|
+
if (isOpen) {
|
|
121
|
+
return (
|
|
122
|
+
<svg width={size} height={size} viewBox="0 0 16 16" fill="none" style={{ flexShrink: 0 }}>
|
|
123
|
+
<path d="M1.5 2.5C1.5 1.94772 1.94772 1.5 2.5 1.5H6.29289C6.5581 1.5 6.81249 1.60536 7 1.79289L8.20711 3H13.5C14.0523 3 14.5 3.44772 14.5 4V5H2.5L1.5 2.5Z" fill={folderColor} opacity="0.8"/>
|
|
124
|
+
<path d="M1.5 5.5H14.5L13.5 13.5C13.5 14.0523 13.0523 14.5 12.5 14.5H3.5C2.94772 14.5 2.5 14.0523 2.5 13.5L1.5 5.5Z" fill={folderColor}/>
|
|
125
|
+
<path d="M1.5 5.5L2.8 14H13.2L14.5 5.5H1.5Z" fill="#ffe082" fillOpacity="0.25"/>
|
|
126
|
+
</svg>
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
return (
|
|
130
|
+
<svg width={size} height={size} viewBox="0 0 16 16" fill="none" style={{ flexShrink: 0 }}>
|
|
131
|
+
<path d="M1.5 2.5C1.5 1.94772 1.94772 1.5 2.5 1.5H6.29289C6.5581 1.5 6.81249 1.60536 7 1.79289L8.20711 3H13.5C14.0523 3 14.5 3.44772 14.5 4V13.5C14.5 14.0523 14.0523 14.5 13.5 14.5H2.5C1.94772 14.5 1.5 14.0523 1.5 13.5V2.5Z" fill={folderColor}/>
|
|
132
|
+
</svg>
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const ext = getFileExtension(filename)
|
|
137
|
+
const lowerName = filename.toLowerCase()
|
|
138
|
+
const color = getLanguageColor(filename, false)
|
|
139
|
+
|
|
140
|
+
// Specific high-frequency icons
|
|
141
|
+
if (ext === 'ts') {
|
|
142
|
+
return (
|
|
143
|
+
<svg width={size} height={size} viewBox="0 0 16 16" fill="none" style={{ flexShrink: 0 }}>
|
|
144
|
+
<rect width="16" height="16" rx="2.5" fill="#3178c6"/>
|
|
145
|
+
<text x="3" y="12" fill="#ffffff" fontSize="9.5" fontWeight="bold" fontFamily="monospace">TS</text>
|
|
146
|
+
</svg>
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
if (ext === 'tsx') {
|
|
150
|
+
return (
|
|
151
|
+
<svg width={size} height={size} viewBox="0 0 16 16" fill="none" style={{ flexShrink: 0 }}>
|
|
152
|
+
<rect width="16" height="16" rx="2.5" fill="#3178c6"/>
|
|
153
|
+
<circle cx="8" cy="8" r="2" fill="#00d8ff"/>
|
|
154
|
+
<ellipse cx="8" cy="8" rx="5" ry="2.2" stroke="#00d8ff" strokeWidth="0.9" fill="none"/>
|
|
155
|
+
<ellipse cx="8" cy="8" rx="5" ry="2.2" stroke="#00d8ff" strokeWidth="0.9" fill="none" transform="rotate(60 8 8)"/>
|
|
156
|
+
<ellipse cx="8" cy="8" rx="5" ry="2.2" stroke="#00d8ff" strokeWidth="0.9" fill="none" transform="rotate(120 8 8)"/>
|
|
157
|
+
</svg>
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
if (ext === 'js' || ext === 'mjs' || ext === 'cjs') {
|
|
161
|
+
return (
|
|
162
|
+
<svg width={size} height={size} viewBox="0 0 16 16" fill="none" style={{ flexShrink: 0 }}>
|
|
163
|
+
<rect width="16" height="16" rx="2.5" fill="#f7df1e"/>
|
|
164
|
+
<text x="3.5" y="12" fill="#000000" fontSize="9.5" fontWeight="bold" fontFamily="monospace">JS</text>
|
|
165
|
+
</svg>
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
if (ext === 'jsx') {
|
|
169
|
+
return (
|
|
170
|
+
<svg width={size} height={size} viewBox="0 0 16 16" fill="none" style={{ flexShrink: 0 }}>
|
|
171
|
+
<rect width="16" height="16" rx="2.5" fill="#20232a"/>
|
|
172
|
+
<circle cx="8" cy="8" r="2" fill="#00d8ff"/>
|
|
173
|
+
<ellipse cx="8" cy="8" rx="5" ry="2.2" stroke="#00d8ff" strokeWidth="0.9" fill="none"/>
|
|
174
|
+
<ellipse cx="8" cy="8" rx="5" ry="2.2" stroke="#00d8ff" strokeWidth="0.9" fill="none" transform="rotate(60 8 8)"/>
|
|
175
|
+
<ellipse cx="8" cy="8" rx="5" ry="2.2" stroke="#00d8ff" strokeWidth="0.9" fill="none" transform="rotate(120 8 8)"/>
|
|
176
|
+
</svg>
|
|
177
|
+
)
|
|
178
|
+
}
|
|
179
|
+
if (ext === 'py') {
|
|
180
|
+
return (
|
|
181
|
+
<svg width={size} height={size} viewBox="0 0 16 16" fill="none" style={{ flexShrink: 0 }}>
|
|
182
|
+
<path d="M7.9 1.5C5.2 1.5 5.3 2.7 5.3 2.7V4H8V4.4H3.8C2.5 4.4 1.5 5.5 1.5 7.2C1.5 8.9 2.3 9.4 3.3 9.4H4.3V8.1C4.3 6.6 5.5 5.5 7 5.5H9.6C10.5 5.5 11.2 4.8 11.2 3.9V2.7C11.2 1.8 10.3 1.5 7.9 1.5ZM6.5 2.6C6.9 2.6 7.2 2.9 7.2 3.3C7.2 3.7 6.9 4 6.5 4C6.1 4 5.8 3.7 5.8 3.3C5.8 2.9 6.1 2.6 6.5 2.6Z" fill="#3572a5"/>
|
|
183
|
+
<path d="M8.1 14.5C10.8 14.5 10.7 13.3 10.7 13.3V12H8V11.6H12.2C13.5 11.6 14.5 10.5 14.5 8.8C14.5 7.1 13.7 6.6 12.7 6.6H11.7V7.9C11.7 9.4 10.5 10.5 9 10.5H6.4C5.5 10.5 4.8 11.2 4.8 12.1V13.3C4.8 14.2 5.7 14.5 8.1 14.5ZM9.5 13.4C9.1 13.4 8.8 13.1 8.8 12.7C8.8 12.3 9.1 12 9.5 12C9.9 12 10.2 12.3 10.2 12.7C10.2 13.1 9.9 13.4 9.5 13.4Z" fill="#ffd43b"/>
|
|
184
|
+
</svg>
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
if (ext === 'rs') {
|
|
188
|
+
return (
|
|
189
|
+
<svg width={size} height={size} viewBox="0 0 16 16" fill="none" style={{ flexShrink: 0 }}>
|
|
190
|
+
<circle cx="8" cy="8" r="6.5" stroke="#dea584" strokeWidth="1.2" fill="#1e1e1e"/>
|
|
191
|
+
<path d="M5.5 5H9C9.8 5 10.5 5.5 10.5 6.3C10.5 7 10 7.6 9.3 7.8L10.8 11H9.3L8 8.2H7V11H5.5V5ZM7 7.1H8.8C9.2 7.1 9.5 6.8 9.5 6.4C9.5 6 9.2 5.8 8.8 5.8H7V7.1Z" fill="#dea584"/>
|
|
192
|
+
</svg>
|
|
193
|
+
)
|
|
194
|
+
}
|
|
195
|
+
if (ext === 'go') {
|
|
196
|
+
return (
|
|
197
|
+
<svg width={size} height={size} viewBox="0 0 16 16" fill="none" style={{ flexShrink: 0 }}>
|
|
198
|
+
<rect width="16" height="16" rx="2.5" fill="#00add8"/>
|
|
199
|
+
<text x="2" y="11.5" fill="#ffffff" fontSize="8" fontWeight="bold" fontFamily="sans-serif">GO</text>
|
|
200
|
+
</svg>
|
|
201
|
+
)
|
|
202
|
+
}
|
|
203
|
+
if (ext === 'json') {
|
|
204
|
+
return (
|
|
205
|
+
<svg width={size} height={size} viewBox="0 0 16 16" fill="none" style={{ flexShrink: 0 }}>
|
|
206
|
+
<path d="M2.5 2C2.5 1.44772 2.94772 1 3.5 1H10L14 5V14C14 14.5523 13.5523 15 13 15H3.5C2.94772 15 2.5 14.5523 2.5 14V2Z" fill="#2d3748"/>
|
|
207
|
+
<text x="4" y="11" fill="#cbcb41" fontSize="9" fontWeight="bold" fontFamily="monospace">{'{}'}</text>
|
|
208
|
+
</svg>
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
if (ext === 'md' || ext === 'markdown') {
|
|
212
|
+
return (
|
|
213
|
+
<svg width={size} height={size} viewBox="0 0 16 16" fill="none" style={{ flexShrink: 0 }}>
|
|
214
|
+
<rect x="1" y="3" width="14" height="10" rx="1.5" stroke="#519aba" strokeWidth="1.2" fill="#1e1e1e"/>
|
|
215
|
+
<path d="M3.5 10V6L5.5 8.5L7.5 6V10" stroke="#519aba" strokeWidth="1.1" strokeLinecap="round" strokeLinejoin="round"/>
|
|
216
|
+
<path d="M11 6V10M9.5 8.5L11 10L12.5 8.5" stroke="#519aba" strokeWidth="1.1" strokeLinecap="round" strokeLinejoin="round"/>
|
|
217
|
+
</svg>
|
|
218
|
+
)
|
|
219
|
+
}
|
|
220
|
+
if (lowerName === '.gitignore' || lowerName === '.gitattributes') {
|
|
221
|
+
return (
|
|
222
|
+
<svg width={size} height={size} viewBox="0 0 16 16" fill="none" style={{ flexShrink: 0 }}>
|
|
223
|
+
<path d="M14.6 7.4L8.6 1.4C8.2 1 7.6 1 7.2 1.4L1.4 7.2C1 7.6 1 8.2 1.4 8.6L7.4 14.6C7.8 15 8.4 15 8.8 14.6L14.6 8.8C15 8.4 15 7.8 14.6 7.4ZM8.5 10.8V8.3C7.4 8.4 6.8 9 6.8 9.5C6.8 9.7 6.6 9.8 6.4 9.8C6.2 9.8 6 9.6 6 9.4C6 8.3 7 7.3 8.5 7.1V5.5C8.1 5.4 7.8 5 7.8 4.5C7.8 3.9 8.2 3.5 8.8 3.5C9.4 3.5 9.8 3.9 9.8 4.5C9.8 5 9.4 5.4 9.1 5.5V10.8C9.4 10.9 9.8 11.3 9.8 11.8C9.8 12.4 9.4 12.8 8.8 12.8C8.2 12.8 7.8 12.4 7.8 11.8C7.8 11.3 8.1 10.9 8.5 10.8Z" fill="#f14e32"/>
|
|
224
|
+
</svg>
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
if (lowerName === 'dockerfile' || lowerName.startsWith('dockerfile.')) {
|
|
228
|
+
return (
|
|
229
|
+
<svg width={size} height={size} viewBox="0 0 16 16" fill="none" style={{ flexShrink: 0 }}>
|
|
230
|
+
<path d="M14.5 7.5C14 7.5 13.5 7.8 13.2 8.2C12.4 7.7 11.2 7.7 10.5 8.2V5H8.5V7H9.5V8.5H5.5V6H3.5V8.5H2V7H1V8.5C0.4 8.8 0 9.4 0 10C0 12.2 2.5 14 6.5 14C11.5 14 14.8 11.2 15 8.5C15 8 14.8 7.5 14.5 7.5Z" fill="#2496ed"/>
|
|
231
|
+
</svg>
|
|
232
|
+
)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Generic document sheet with badge
|
|
236
|
+
const shortExt = ext.slice(0, 3).toUpperCase()
|
|
237
|
+
return (
|
|
238
|
+
<svg width={size} height={size} viewBox="0 0 16 16" fill="none" style={{ flexShrink: 0 }}>
|
|
239
|
+
<path d="M3 2C3 1.44772 3.44772 1 4 1H10L14 5V14C14 14.5523 13.5523 15 13 15H4C3.44772 15 3 14.5523 3 14V2Z" fill="#2b313c" stroke="#3e4451" strokeWidth="0.8"/>
|
|
240
|
+
<path d="M10 1V5H14" fill="#3e4451" opacity="0.6"/>
|
|
241
|
+
{shortExt ? (
|
|
242
|
+
<text x="4" y="11" fill={color} fontSize="6" fontWeight="bold" fontFamily="monospace">{shortExt}</text>
|
|
243
|
+
) : (
|
|
244
|
+
<circle cx="8" cy="9" r="1.8" fill={color}/>
|
|
245
|
+
)}
|
|
246
|
+
</svg>
|
|
247
|
+
)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export const renderVSCodeIcon = renderFileIcon
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import z from '@deepseek-ai/schemastery'
|
|
2
|
+
|
|
3
|
+
export interface ExplorerConfig {
|
|
4
|
+
maxListingEntries?: number
|
|
5
|
+
defaultCompactFolders?: boolean
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const Config = z.object({
|
|
9
|
+
maxListingEntries: z.natural().default(2000),
|
|
10
|
+
defaultCompactFolders: z.boolean().default(true),
|
|
11
|
+
})
|