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,157 @@
1
+ import React, { useEffect, useRef } from 'react'
2
+ import css from '../explorer.module.css'
3
+ import type { FsEntry } from '../utils/api.ts'
4
+
5
+ interface ContextMenuProps {
6
+ x: number
7
+ y: number
8
+ entry: FsEntry
9
+ relativePath: string
10
+ onClose: () => void
11
+ onOpenFile?: () => void
12
+ onInsertReference: () => void
13
+ onInsertSnippet: () => void
14
+ onCopyPath: () => void
15
+ onCopyRelativePath: () => void
16
+ onNewFile: () => void
17
+ onNewFolder: () => void
18
+ onRename: () => void
19
+ onDelete: () => void
20
+ }
21
+
22
+ export const ExplorerContextMenu: React.FC<ContextMenuProps> = ({
23
+ x,
24
+ y,
25
+ entry,
26
+ relativePath,
27
+ onClose,
28
+ onOpenFile,
29
+ onInsertReference,
30
+ onInsertSnippet,
31
+ onCopyPath,
32
+ onCopyRelativePath,
33
+ onNewFile,
34
+ onNewFolder,
35
+ onRename,
36
+ onDelete,
37
+ }) => {
38
+ const menuRef = useRef<HTMLDivElement>(null)
39
+
40
+ useEffect(() => {
41
+ const handleOutsideClick = (e: MouseEvent) => {
42
+ if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
43
+ onClose()
44
+ }
45
+ }
46
+ window.addEventListener('mousedown', handleOutsideClick)
47
+ return () => window.removeEventListener('mousedown', handleOutsideClick)
48
+ }, [onClose])
49
+
50
+ const top = Math.min(y, window.innerHeight - 260)
51
+ const left = Math.min(x, window.innerWidth - 200)
52
+
53
+ return (
54
+ <div ref={menuRef} className={css.contextMenu} style={{ top, left }}>
55
+ {!entry.isDir && onOpenFile && (
56
+ <>
57
+ <div
58
+ className={css.menuItem}
59
+ onClick={() => {
60
+ onOpenFile()
61
+ onClose()
62
+ }}
63
+ >
64
+ <span>Open in New Tab</span>
65
+ <span className={css.menuShortcut}>Double-Click</span>
66
+ </div>
67
+ <div className={css.menuDivider} />
68
+ </>
69
+ )}
70
+ <div
71
+ className={css.menuItem}
72
+ onClick={() => {
73
+ onInsertReference()
74
+ onClose()
75
+ }}
76
+ >
77
+ <span>Insert Mention to Chat</span>
78
+ <span className={css.menuShortcut}>@</span>
79
+ </div>
80
+ {!entry.isDir && (
81
+ <div
82
+ className={css.menuItem}
83
+ onClick={() => {
84
+ onInsertSnippet()
85
+ onClose()
86
+ }}
87
+ >
88
+ <span>Insert Code to Chat</span>
89
+ <span className={css.menuShortcut}>Alt+Drop</span>
90
+ </div>
91
+ )}
92
+ <div className={css.menuDivider} />
93
+ {entry.isDir && (
94
+ <>
95
+ <div
96
+ className={css.menuItem}
97
+ onClick={() => {
98
+ onNewFile()
99
+ onClose()
100
+ }}
101
+ >
102
+ <span>New File...</span>
103
+ </div>
104
+ <div
105
+ className={css.menuItem}
106
+ onClick={() => {
107
+ onNewFolder()
108
+ onClose()
109
+ }}
110
+ >
111
+ <span>New Folder...</span>
112
+ </div>
113
+ <div className={css.menuDivider} />
114
+ </>
115
+ )}
116
+ <div
117
+ className={css.menuItem}
118
+ onClick={() => {
119
+ onCopyRelativePath()
120
+ onClose()
121
+ }}
122
+ >
123
+ <span>Copy Relative Path</span>
124
+ </div>
125
+ <div
126
+ className={css.menuItem}
127
+ onClick={() => {
128
+ onCopyPath()
129
+ onClose()
130
+ }}
131
+ >
132
+ <span>Copy Path</span>
133
+ </div>
134
+ <div className={css.menuDivider} />
135
+ <div
136
+ className={css.menuItem}
137
+ onClick={() => {
138
+ onRename()
139
+ onClose()
140
+ }}
141
+ >
142
+ <span>Rename...</span>
143
+ <span className={css.menuShortcut}>F2</span>
144
+ </div>
145
+ <div
146
+ className={css.menuItem}
147
+ onClick={() => {
148
+ onDelete()
149
+ onClose()
150
+ }}
151
+ >
152
+ <span style={{ color: '#f44336' }}>Delete</span>
153
+ <span className={css.menuShortcut}>Del</span>
154
+ </div>
155
+ </div>
156
+ )
157
+ }
@@ -0,0 +1,124 @@
1
+ import React, { useEffect, useRef, useState } from 'react'
2
+ import css from '../explorer.module.css'
3
+ import type { FsEntry } from '../utils/api.ts'
4
+ import { renderFileIcon } from '../utils/vscode-theme.tsx'
5
+
6
+ interface ExplorerDeleteModalProps {
7
+ entry: FsEntry
8
+ relativePath: string
9
+ onConfirm: () => Promise<void>
10
+ onClose: () => void
11
+ }
12
+
13
+ export const ExplorerDeleteModal: React.FC<ExplorerDeleteModalProps> = ({
14
+ entry,
15
+ relativePath,
16
+ onConfirm,
17
+ onClose,
18
+ }) => {
19
+ const [loading, setLoading] = useState(false)
20
+ const [error, setError] = useState<string | null>(null)
21
+ const cancelBtnRef = useRef<HTMLButtonElement>(null)
22
+ const modalRef = useRef<HTMLDivElement>(null)
23
+
24
+ useEffect(() => {
25
+ cancelBtnRef.current?.focus()
26
+
27
+ const handleKeyDown = (e: KeyboardEvent) => {
28
+ if (e.key === 'Escape') {
29
+ e.preventDefault()
30
+ onClose()
31
+ }
32
+ }
33
+
34
+ const handleMouseDown = (e: MouseEvent) => {
35
+ if (modalRef.current && !modalRef.current.contains(e.target as Node)) {
36
+ onClose()
37
+ }
38
+ }
39
+
40
+ document.addEventListener('keydown', handleKeyDown)
41
+ document.addEventListener('mousedown', handleMouseDown)
42
+ return () => {
43
+ document.removeEventListener('keydown', handleKeyDown)
44
+ document.removeEventListener('mousedown', handleMouseDown)
45
+ }
46
+ }, [onClose])
47
+
48
+ const handleDelete = async () => {
49
+ if (loading) return
50
+ setLoading(true)
51
+ setError(null)
52
+ try {
53
+ await onConfirm()
54
+ onClose()
55
+ } catch (err: any) {
56
+ setError(err?.message || 'Failed to delete')
57
+ setLoading(false)
58
+ }
59
+ }
60
+
61
+ return (
62
+ <div className={css.modalOverlay}>
63
+ <div ref={modalRef} className={css.modalCard} role="dialog" aria-modal="true">
64
+ <div className={css.modalHeader}>
65
+ <div className={css.modalTitleWrap}>
66
+ <svg width="15" height="15" viewBox="0 0 16 16" fill="currentColor">
67
+ <path d="M6.5 1h3a.5.5 0 0 1 .5.5v1H14a.5.5 0 0 1 0 1h-.6l-.8 11.2A2 2 0 0 1 10.6 16H5.4a2 2 0 0 1-2-1.8L2.6 3.5H2a.5.5 0 0 1 0-1h4v-1a.5.5 0 0 1 .5-.5zM7 2v.5h2V2H7zm-2.4 12.4a1 1 0 0 0 1 .6h5.2a1 1 0 0 0 1-.9L12.4 3.5H3.6l.8 10.9h.2zm2.9-9.9a.5.5 0 0 1 .5.5v7a.5.5 0 0 1-1 0v-7a.5.5 0 0 1 .5-.5zm3 0a.5.5 0 0 1 .5.5v7a.5.5 0 0 1-1 0v-7a.5.5 0 0 1 .5-.5z" />
68
+ </svg>
69
+ <span>{entry.isDir ? 'Delete Folder' : 'Delete File'}</span>
70
+ </div>
71
+ <button className={css.optionsIconBtn} title="Close" onClick={onClose}>
72
+ <svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor">
73
+ <path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z" />
74
+ </svg>
75
+ </button>
76
+ </div>
77
+
78
+ <div className={css.modalBody}>
79
+ <div>
80
+ Are you sure you want to permanently delete this {entry.isDir ? 'directory' : 'file'}?
81
+ </div>
82
+
83
+ <div className={css.modalItemPreview}>
84
+ <span className={css.iconWrap}>
85
+ {renderFileIcon(entry.name, entry.isDir, false, 16)}
86
+ </span>
87
+ <div className={css.modalItemInfo}>
88
+ <div className={css.modalItemName}>{entry.name}</div>
89
+ <div className={css.modalItemPath}>{relativePath}</div>
90
+ </div>
91
+ </div>
92
+
93
+ {entry.isDir && (
94
+ <div className={css.modalWarning}>
95
+ Warning: All nested files and subdirectories inside this folder will be permanently removed.
96
+ </div>
97
+ )}
98
+
99
+ {error && <div className={css.modalErrorBanner}>{error}</div>}
100
+ </div>
101
+
102
+ <div className={css.modalFooter}>
103
+ <button
104
+ ref={cancelBtnRef}
105
+ type="button"
106
+ className={css.modalBtnCancel}
107
+ onClick={onClose}
108
+ disabled={loading}
109
+ >
110
+ Cancel
111
+ </button>
112
+ <button
113
+ type="button"
114
+ className={css.modalBtnDanger}
115
+ onClick={handleDelete}
116
+ disabled={loading}
117
+ >
118
+ {loading ? 'Deleting...' : 'Delete'}
119
+ </button>
120
+ </div>
121
+ </div>
122
+ </div>
123
+ )
124
+ }
@@ -0,0 +1,125 @@
1
+ import React, { useEffect, useRef } from 'react'
2
+ import css from '../explorer.module.css'
3
+
4
+ interface ExplorerEmptyContextMenuProps {
5
+ x: number
6
+ y: number
7
+ onClose: () => void
8
+ onNewFile: () => void
9
+ onNewFolder: () => void
10
+ onToggleSearch: () => void
11
+ onExpandAll: () => void
12
+ onCollapseAll: () => void
13
+ onRefresh: () => void
14
+ onOpenOptions: () => void
15
+ }
16
+
17
+ export const ExplorerEmptyContextMenu: React.FC<ExplorerEmptyContextMenuProps> = ({
18
+ x,
19
+ y,
20
+ onClose,
21
+ onNewFile,
22
+ onNewFolder,
23
+ onToggleSearch,
24
+ onExpandAll,
25
+ onCollapseAll,
26
+ onRefresh,
27
+ onOpenOptions,
28
+ }) => {
29
+ const menuRef = useRef<HTMLDivElement>(null)
30
+
31
+ useEffect(() => {
32
+ const handleOutsideClick = (e: MouseEvent) => {
33
+ if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
34
+ onClose()
35
+ }
36
+ }
37
+ const handleKeyDown = (e: KeyboardEvent) => {
38
+ if (e.key === 'Escape') {
39
+ onClose()
40
+ }
41
+ }
42
+ window.addEventListener('mousedown', handleOutsideClick)
43
+ window.addEventListener('keydown', handleKeyDown)
44
+ return () => {
45
+ window.removeEventListener('mousedown', handleOutsideClick)
46
+ window.removeEventListener('keydown', handleKeyDown)
47
+ }
48
+ }, [onClose])
49
+
50
+ const top = Math.min(y, window.innerHeight - 250)
51
+ const left = Math.min(x, window.innerWidth - 200)
52
+
53
+ return (
54
+ <div ref={menuRef} className={css.contextMenu} style={{ top, left }}>
55
+ <div
56
+ className={css.menuItem}
57
+ onClick={() => {
58
+ onNewFile()
59
+ onClose()
60
+ }}
61
+ >
62
+ <span>New File...</span>
63
+ </div>
64
+ <div
65
+ className={css.menuItem}
66
+ onClick={() => {
67
+ onNewFolder()
68
+ onClose()
69
+ }}
70
+ >
71
+ <span>New Folder...</span>
72
+ </div>
73
+ <div className={css.menuDivider} />
74
+ <div
75
+ className={css.menuItem}
76
+ onClick={() => {
77
+ onToggleSearch()
78
+ onClose()
79
+ }}
80
+ >
81
+ <span>Search Files...</span>
82
+ <span className={css.menuShortcut}>Ctrl+F</span>
83
+ </div>
84
+ <div className={css.menuDivider} />
85
+ <div
86
+ className={css.menuItem}
87
+ onClick={() => {
88
+ onExpandAll()
89
+ onClose()
90
+ }}
91
+ >
92
+ <span>Expand All</span>
93
+ </div>
94
+ <div
95
+ className={css.menuItem}
96
+ onClick={() => {
97
+ onCollapseAll()
98
+ onClose()
99
+ }}
100
+ >
101
+ <span>Collapse All</span>
102
+ </div>
103
+ <div className={css.menuDivider} />
104
+ <div
105
+ className={css.menuItem}
106
+ onClick={() => {
107
+ onRefresh()
108
+ onClose()
109
+ }}
110
+ >
111
+ <span>Refresh</span>
112
+ </div>
113
+ <div className={css.menuDivider} />
114
+ <div
115
+ className={css.menuItem}
116
+ onClick={() => {
117
+ onOpenOptions()
118
+ onClose()
119
+ }}
120
+ >
121
+ <span>Display Preferences...</span>
122
+ </div>
123
+ </div>
124
+ )
125
+ }
@@ -0,0 +1,81 @@
1
+ import React from 'react'
2
+ import css from '../explorer.module.css'
3
+
4
+ interface ExplorerHeaderProps {
5
+ title: string
6
+ searchOpen: boolean
7
+ optionsOpen: boolean
8
+ onToggleSearch: () => void
9
+ onToggleOptions: () => void
10
+ onNewFile: () => void
11
+ onNewFolder: () => void
12
+ onRefresh: () => void
13
+ onExpandAll: () => void
14
+ onCollapseAll: () => void
15
+ }
16
+
17
+ export const ExplorerHeader: React.FC<ExplorerHeaderProps> = ({
18
+ title,
19
+ searchOpen,
20
+ optionsOpen,
21
+ onToggleSearch,
22
+ onToggleOptions,
23
+ onNewFile,
24
+ onNewFolder,
25
+ onRefresh,
26
+ onExpandAll,
27
+ onCollapseAll,
28
+ }) => {
29
+ return (
30
+ <div className={css.header}>
31
+ <span className={css.headerTitle} title={title}>
32
+ {title}
33
+ </span>
34
+ <div className={css.headerActions}>
35
+ <button
36
+ className={`${css.iconBtn} ${searchOpen ? css.iconBtnActive : ''}`}
37
+ title="Search files and folders"
38
+ onClick={onToggleSearch}
39
+ >
40
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
41
+ <path d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0z"/>
42
+ </svg>
43
+ </button>
44
+ <button className={css.iconBtn} title="New File" onClick={onNewFile}>
45
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
46
+ <path d="M9 1H4a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V6L9 1zm0 1.5L12.5 6H9V2.5zM8 8.5V11h1V8.5h2.5v-1H9V5H8v2.5H5.5v1H8z"/>
47
+ </svg>
48
+ </button>
49
+ <button className={css.iconBtn} title="New Folder" onClick={onNewFolder}>
50
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
51
+ <path d="M7 2l2 2h5a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h5zm2.5 6.5H8V6H7v2.5H4.5v1H7V12h1V9.5h2.5v-1z"/>
52
+ </svg>
53
+ </button>
54
+ <button className={css.iconBtn} title="Refresh" onClick={onRefresh}>
55
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
56
+ <path d="M13.65 2.35A7.958 7.958 0 0 0 8 0a8 8 0 1 0 8 8h-2a6 6 0 1 1-1.76-4.24l-2.24 2.24H16V0l-2.35 2.35z"/>
57
+ </svg>
58
+ </button>
59
+ <button className={css.iconBtn} title="Expand All" onClick={onExpandAll}>
60
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
61
+ <path d="M4.646 5.854a.5.5 0 0 1 0-.708l3-3a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1-.708.708L8 3.207 5.354 5.854a.5.5 0 0 1-.708 0zm0 4.292a.5.5 0 0 0 0 .708l3 3a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8 12.793l-2.646-2.647a.5.5 0 0 0-.708 0z"/>
62
+ </svg>
63
+ </button>
64
+ <button className={css.iconBtn} title="Collapse All" onClick={onCollapseAll}>
65
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
66
+ <path d="M4.646 2.854a.5.5 0 0 0 0 .708l3 3a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8 5.293 5.354 2.854a.5.5 0 0 0-.708 0zm0 10.292a.5.5 0 0 1 0-.708l3-3a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1-.708.708L8 10.707l-2.646 2.647a.5.5 0 0 1-.708 0z"/>
67
+ </svg>
68
+ </button>
69
+ <button
70
+ className={`${css.iconBtn} ${optionsOpen ? css.iconBtnActive : ''}`}
71
+ title="Explorer display preferences"
72
+ onClick={onToggleOptions}
73
+ >
74
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
75
+ <path d="M9.405 1.05c-.413-1.4-2.397-1.4-2.81 0l-.1.34a1.464 1.464 0 0 1-2.105.872l-.31-.17c-1.283-.698-2.686.705-1.987 1.987l.169.311c.446.82.023 1.841-.872 2.105l-.34.1c-1.4.413-1.4 2.397 0 2.81l.34.1a1.464 1.464 0 0 1 .872 2.105l-.17.31c-.698 1.283.705 2.686 1.987 1.987l.311-.169a1.464 1.464 0 0 1 2.105.872l.1.34c.413 1.4 2.397 1.4 2.81 0l.1-.34a1.464 1.464 0 0 1 2.105-.872l.31.17c1.283.698 2.686-.705 1.987-1.987l-.169-.311a1.464 1.464 0 0 1 .872-2.105l.34-.1c1.4-.413 1.4-2.397 0-2.81l-.34-.1a1.464 1.464 0 0 1-.872-2.105l.17-.31c.698-1.283-.705-2.686-1.987-1.987l-.311.169a1.464 1.464 0 0 1-2.105-.872l-.1-.34zM8 10.93a2.929 2.929 0 1 1 0-5.86 2.929 2.929 0 0 1 0 5.858z"/>
76
+ </svg>
77
+ </button>
78
+ </div>
79
+ </div>
80
+ )
81
+ }