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.
Files changed (37) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +106 -2
  3. package/cordis.patch.yml +5 -0
  4. package/icon.svg +7 -0
  5. package/lib/client.js +859 -0
  6. package/lib/index.js +854 -0
  7. package/package.json +91 -4
  8. package/src/client/ExplorerView.tsx +981 -0
  9. package/src/client/components/ExplorerContextMenu.tsx +157 -0
  10. package/src/client/components/ExplorerDeleteModal.tsx +124 -0
  11. package/src/client/components/ExplorerEmptyContextMenu.tsx +125 -0
  12. package/src/client/components/ExplorerHeader.tsx +81 -0
  13. package/src/client/components/ExplorerOptionsModal.tsx +230 -0
  14. package/src/client/components/ExplorerRow.tsx +258 -0
  15. package/src/client/components/ExplorerSearch.tsx +55 -0
  16. package/src/client/components/TreeNode.tsx +384 -0
  17. package/src/client/css-modules.d.ts +5 -0
  18. package/src/client/explorer.module.css +833 -0
  19. package/src/client/hooks/useKeyboardNav.ts +89 -0
  20. package/src/client/hooks/useLiveSync.ts +81 -0
  21. package/src/client/index.tsx +238 -0
  22. package/src/client/utils/api.ts +251 -0
  23. package/src/client/utils/conversation-draft.ts +644 -0
  24. package/src/client/utils/drag-drop.ts +209 -0
  25. package/src/client/utils/open-tab.ts +69 -0
  26. package/src/client/utils/options.ts +59 -0
  27. package/src/client/utils/vscode-theme.tsx +250 -0
  28. package/src/config.ts +11 -0
  29. package/src/fs-operations.ts +116 -0
  30. package/src/fs-search.ts +116 -0
  31. package/src/fs-tree.ts +155 -0
  32. package/src/fs-watch.ts +179 -0
  33. package/src/git-status.ts +167 -0
  34. package/src/index.ts +177 -0
  35. package/src/path-security.ts +22 -0
  36. package/src/session-path.ts +35 -0
  37. 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
+ })