@kispace-io/core 0.7.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.
Files changed (272) hide show
  1. package/dist/api/base-classes.d.ts +7 -0
  2. package/dist/api/base-classes.d.ts.map +1 -0
  3. package/dist/api/constants.d.ts +2 -0
  4. package/dist/api/constants.d.ts.map +1 -0
  5. package/dist/api/index.d.ts +6 -0
  6. package/dist/api/index.d.ts.map +1 -0
  7. package/dist/api/index.js +80 -0
  8. package/dist/api/index.js.map +1 -0
  9. package/dist/api/services.d.ts +27 -0
  10. package/dist/api/services.d.ts.map +1 -0
  11. package/dist/api/types.d.ts +11 -0
  12. package/dist/api/types.d.ts.map +1 -0
  13. package/dist/commands/files.d.ts +2 -0
  14. package/dist/commands/files.d.ts.map +1 -0
  15. package/dist/commands/global.d.ts +1 -0
  16. package/dist/commands/global.d.ts.map +1 -0
  17. package/dist/commands/index.d.ts +1 -0
  18. package/dist/commands/index.d.ts.map +1 -0
  19. package/dist/commands/version-info.d.ts +2 -0
  20. package/dist/commands/version-info.d.ts.map +1 -0
  21. package/dist/components/index.d.ts +1 -0
  22. package/dist/components/index.d.ts.map +1 -0
  23. package/dist/components/k-app-selector.d.ts +17 -0
  24. package/dist/components/k-app-selector.d.ts.map +1 -0
  25. package/dist/components/k-app-switcher.d.ts +13 -0
  26. package/dist/components/k-app-switcher.d.ts.map +1 -0
  27. package/dist/components/k-command.d.ts +31 -0
  28. package/dist/components/k-command.d.ts.map +1 -0
  29. package/dist/components/k-extensions.d.ts +32 -0
  30. package/dist/components/k-extensions.d.ts.map +1 -0
  31. package/dist/components/k-fastviews.d.ts +34 -0
  32. package/dist/components/k-fastviews.d.ts.map +1 -0
  33. package/dist/components/k-filebrowser.d.ts +40 -0
  34. package/dist/components/k-filebrowser.d.ts.map +1 -0
  35. package/dist/components/k-language-selector.d.ts +12 -0
  36. package/dist/components/k-language-selector.d.ts.map +1 -0
  37. package/dist/components/k-log-terminal.d.ts +36 -0
  38. package/dist/components/k-log-terminal.d.ts.map +1 -0
  39. package/dist/components/k-part-name.d.ts +12 -0
  40. package/dist/components/k-part-name.d.ts.map +1 -0
  41. package/dist/components/k-tasks.d.ts +13 -0
  42. package/dist/components/k-tasks.d.ts.map +1 -0
  43. package/dist/components/k-workspace-name.d.ts +14 -0
  44. package/dist/components/k-workspace-name.d.ts.map +1 -0
  45. package/dist/contributions/default-ui-contributions.d.ts +2 -0
  46. package/dist/contributions/default-ui-contributions.d.ts.map +1 -0
  47. package/dist/contributions/index.d.ts +1 -0
  48. package/dist/contributions/index.d.ts.map +1 -0
  49. package/dist/contributions/marketplace-catalog-contributions.d.ts +2 -0
  50. package/dist/contributions/marketplace-catalog-contributions.d.ts.map +1 -0
  51. package/dist/core/app-host-config.d.ts +7 -0
  52. package/dist/core/app-host-config.d.ts.map +1 -0
  53. package/dist/core/apploader.d.ts +214 -0
  54. package/dist/core/apploader.d.ts.map +1 -0
  55. package/dist/core/appstate.d.ts +12 -0
  56. package/dist/core/appstate.d.ts.map +1 -0
  57. package/dist/core/commandregistry.d.ts +79 -0
  58. package/dist/core/commandregistry.d.ts.map +1 -0
  59. package/dist/core/config.d.ts +15 -0
  60. package/dist/core/config.d.ts.map +1 -0
  61. package/dist/core/constants.d.ts +21 -0
  62. package/dist/core/constants.d.ts.map +1 -0
  63. package/dist/core/contributionregistry.d.ts +49 -0
  64. package/dist/core/contributionregistry.d.ts.map +1 -0
  65. package/dist/core/di.d.ts +18 -0
  66. package/dist/core/di.d.ts.map +1 -0
  67. package/dist/core/dialogservice.d.ts +33 -0
  68. package/dist/core/dialogservice.d.ts.map +1 -0
  69. package/dist/core/editorregistry.d.ts +73 -0
  70. package/dist/core/editorregistry.d.ts.map +1 -0
  71. package/dist/core/esmsh-service.d.ts +40 -0
  72. package/dist/core/esmsh-service.d.ts.map +1 -0
  73. package/dist/core/events.d.ts +7 -0
  74. package/dist/core/events.d.ts.map +1 -0
  75. package/dist/core/events.js +63 -0
  76. package/dist/core/events.js.map +1 -0
  77. package/dist/core/extensionregistry.d.ts +98 -0
  78. package/dist/core/extensionregistry.d.ts.map +1 -0
  79. package/dist/core/filesys.d.ts +139 -0
  80. package/dist/core/filesys.d.ts.map +1 -0
  81. package/dist/core/i18n.d.ts +50 -0
  82. package/dist/core/i18n.d.ts.map +1 -0
  83. package/dist/core/index.d.ts +1 -0
  84. package/dist/core/index.d.ts.map +1 -0
  85. package/dist/core/k-utils.d.ts +2 -0
  86. package/dist/core/k-utils.d.ts.map +1 -0
  87. package/dist/core/keybindings.d.ts +67 -0
  88. package/dist/core/keybindings.d.ts.map +1 -0
  89. package/dist/core/logger.d.ts +44 -0
  90. package/dist/core/logger.d.ts.map +1 -0
  91. package/dist/core/marketplaceregistry.d.ts +25 -0
  92. package/dist/core/marketplaceregistry.d.ts.map +1 -0
  93. package/dist/core/packageinfoservice.d.ts +16 -0
  94. package/dist/core/packageinfoservice.d.ts.map +1 -0
  95. package/dist/core/persistenceservice.d.ts +6 -0
  96. package/dist/core/persistenceservice.d.ts.map +1 -0
  97. package/dist/core/settingsservice.d.ts +19 -0
  98. package/dist/core/settingsservice.d.ts.map +1 -0
  99. package/dist/core/signals.d.ts +3 -0
  100. package/dist/core/signals.d.ts.map +1 -0
  101. package/dist/core/taskservice.d.ts +20 -0
  102. package/dist/core/taskservice.d.ts.map +1 -0
  103. package/dist/core/toast.d.ts +4 -0
  104. package/dist/core/toast.d.ts.map +1 -0
  105. package/dist/core/tree-utils.d.ts +16 -0
  106. package/dist/core/tree-utils.d.ts.map +1 -0
  107. package/dist/dialogs/confirm-dialog.d.ts +14 -0
  108. package/dist/dialogs/confirm-dialog.d.ts.map +1 -0
  109. package/dist/dialogs/index.d.ts +5 -0
  110. package/dist/dialogs/index.d.ts.map +1 -0
  111. package/dist/dialogs/info-dialog.d.ts +13 -0
  112. package/dist/dialogs/info-dialog.d.ts.map +1 -0
  113. package/dist/dialogs/navigable-info-dialog.d.ts +33 -0
  114. package/dist/dialogs/navigable-info-dialog.d.ts.map +1 -0
  115. package/dist/dialogs/prompt-dialog.d.ts +21 -0
  116. package/dist/dialogs/prompt-dialog.d.ts.map +1 -0
  117. package/dist/externals/lit.d.ts +20 -0
  118. package/dist/externals/lit.d.ts.map +1 -0
  119. package/dist/externals/lit.js +15 -0
  120. package/dist/externals/lit.js.map +1 -0
  121. package/dist/externals/third-party.d.ts +7 -0
  122. package/dist/externals/third-party.d.ts.map +1 -0
  123. package/dist/externals/third-party.js +2 -0
  124. package/dist/externals/third-party.js.map +1 -0
  125. package/dist/externals/webawesome.d.ts +1 -0
  126. package/dist/externals/webawesome.d.ts.map +1 -0
  127. package/dist/externals/webawesome.js +52 -0
  128. package/dist/externals/webawesome.js.map +1 -0
  129. package/dist/i18n/extensions.json.d.ts +42 -0
  130. package/dist/i18n/fastviews.json.d.ts +13 -0
  131. package/dist/i18n/filebrowser.json.d.ts +35 -0
  132. package/dist/i18n/index.d.ts +2 -0
  133. package/dist/i18n/index.d.ts.map +1 -0
  134. package/dist/i18n/logterminal.json.d.ts +45 -0
  135. package/dist/i18n/partname.json.d.ts +15 -0
  136. package/dist/i18n/tasks.json.d.ts +15 -0
  137. package/dist/i18n/workspace.json.d.ts +15 -0
  138. package/dist/index.d.ts +2 -0
  139. package/dist/index.d.ts.map +1 -0
  140. package/dist/index.js +80 -0
  141. package/dist/index.js.map +1 -0
  142. package/dist/k-icon-BZC7dQV0.js +492 -0
  143. package/dist/k-icon-BZC7dQV0.js.map +1 -0
  144. package/dist/k-nocontent-Bh_yToGh.js +48 -0
  145. package/dist/k-nocontent-Bh_yToGh.js.map +1 -0
  146. package/dist/k-resizable-grid-Ch3iWZaL.js +3157 -0
  147. package/dist/k-resizable-grid-Ch3iWZaL.js.map +1 -0
  148. package/dist/k-standard-layout-CQ1VZoxa.js +5011 -0
  149. package/dist/k-standard-layout-CQ1VZoxa.js.map +1 -0
  150. package/dist/layouts/k-standard-layout.d.ts +16 -0
  151. package/dist/layouts/k-standard-layout.d.ts.map +1 -0
  152. package/dist/parts/index.d.ts +1 -0
  153. package/dist/parts/index.d.ts.map +1 -0
  154. package/dist/parts/index.js +53 -0
  155. package/dist/parts/index.js.map +1 -0
  156. package/dist/parts/k-app.d.ts +11 -0
  157. package/dist/parts/k-app.d.ts.map +1 -0
  158. package/dist/parts/k-container.d.ts +4 -0
  159. package/dist/parts/k-container.d.ts.map +1 -0
  160. package/dist/parts/k-contextmenu.d.ts +38 -0
  161. package/dist/parts/k-contextmenu.d.ts.map +1 -0
  162. package/dist/parts/k-dialog-content.d.ts +9 -0
  163. package/dist/parts/k-dialog-content.d.ts.map +1 -0
  164. package/dist/parts/k-element.d.ts +36 -0
  165. package/dist/parts/k-element.d.ts.map +1 -0
  166. package/dist/parts/k-part.d.ts +96 -0
  167. package/dist/parts/k-part.d.ts.map +1 -0
  168. package/dist/parts/k-resizable-grid.d.ts +31 -0
  169. package/dist/parts/k-resizable-grid.d.ts.map +1 -0
  170. package/dist/parts/k-tabs.d.ts +74 -0
  171. package/dist/parts/k-tabs.d.ts.map +1 -0
  172. package/dist/parts/k-toolbar.d.ts +21 -0
  173. package/dist/parts/k-toolbar.d.ts.map +1 -0
  174. package/dist/widgets/index.d.ts +1 -0
  175. package/dist/widgets/index.d.ts.map +1 -0
  176. package/dist/widgets/index.js +3 -0
  177. package/dist/widgets/index.js.map +1 -0
  178. package/dist/widgets/k-icon.d.ts +10 -0
  179. package/dist/widgets/k-icon.d.ts.map +1 -0
  180. package/dist/widgets/k-nocontent.d.ts +13 -0
  181. package/dist/widgets/k-nocontent.d.ts.map +1 -0
  182. package/dist/widgets/k-widget.d.ts +25 -0
  183. package/dist/widgets/k-widget.d.ts.map +1 -0
  184. package/package.json +81 -0
  185. package/src/api/base-classes.ts +10 -0
  186. package/src/api/constants.ts +3 -0
  187. package/src/api/index.ts +31 -0
  188. package/src/api/services.ts +52 -0
  189. package/src/api/types.ts +46 -0
  190. package/src/commands/files.ts +829 -0
  191. package/src/commands/global.ts +225 -0
  192. package/src/commands/index.ts +4 -0
  193. package/src/commands/version-info.ts +214 -0
  194. package/src/components/index.ts +10 -0
  195. package/src/components/k-app-selector.ts +233 -0
  196. package/src/components/k-app-switcher.ts +126 -0
  197. package/src/components/k-command.ts +236 -0
  198. package/src/components/k-extensions.ts +615 -0
  199. package/src/components/k-fastviews.ts +314 -0
  200. package/src/components/k-filebrowser.ts +442 -0
  201. package/src/components/k-language-selector.ts +166 -0
  202. package/src/components/k-log-terminal.ts +337 -0
  203. package/src/components/k-part-name.ts +54 -0
  204. package/src/components/k-tasks.ts +267 -0
  205. package/src/components/k-workspace-name.ts +56 -0
  206. package/src/contributions/default-ui-contributions.ts +51 -0
  207. package/src/contributions/index.ts +3 -0
  208. package/src/contributions/marketplace-catalog-contributions.ts +6 -0
  209. package/src/core/app-host-config.ts +23 -0
  210. package/src/core/apploader.ts +630 -0
  211. package/src/core/appstate.ts +15 -0
  212. package/src/core/commandregistry.ts +210 -0
  213. package/src/core/config.ts +29 -0
  214. package/src/core/constants.ts +27 -0
  215. package/src/core/contributionregistry.ts +77 -0
  216. package/src/core/di.ts +54 -0
  217. package/src/core/dialogservice.ts +266 -0
  218. package/src/core/editorregistry.ts +303 -0
  219. package/src/core/esmsh-service.ts +404 -0
  220. package/src/core/events.ts +68 -0
  221. package/src/core/extensionregistry.ts +399 -0
  222. package/src/core/filesys.ts +618 -0
  223. package/src/core/i18n.ts +221 -0
  224. package/src/core/index.ts +51 -0
  225. package/src/core/k-utils.ts +11 -0
  226. package/src/core/keybindings.ts +274 -0
  227. package/src/core/logger.ts +187 -0
  228. package/src/core/marketplaceregistry.ts +197 -0
  229. package/src/core/packageinfoservice.ts +56 -0
  230. package/src/core/persistenceservice.ts +15 -0
  231. package/src/core/settingsservice.ts +70 -0
  232. package/src/core/signals.ts +18 -0
  233. package/src/core/taskservice.ts +72 -0
  234. package/src/core/toast.ts +11 -0
  235. package/src/core/tree-utils.ts +24 -0
  236. package/src/dialogs/confirm-dialog.ts +72 -0
  237. package/src/dialogs/index.ts +4 -0
  238. package/src/dialogs/info-dialog.ts +67 -0
  239. package/src/dialogs/navigable-info-dialog.ts +256 -0
  240. package/src/dialogs/prompt-dialog.ts +123 -0
  241. package/src/externals/lit.ts +26 -0
  242. package/src/externals/third-party.ts +9 -0
  243. package/src/externals/webawesome.ts +54 -0
  244. package/src/i18n/extensions.json +39 -0
  245. package/src/i18n/fastviews.json +10 -0
  246. package/src/i18n/filebrowser.json +33 -0
  247. package/src/i18n/index.ts +25 -0
  248. package/src/i18n/logterminal.json +42 -0
  249. package/src/i18n/partname.json +12 -0
  250. package/src/i18n/tasks.json +12 -0
  251. package/src/i18n/workspace.json +12 -0
  252. package/src/icons/icons.txt +3 -0
  253. package/src/icons/js.svg +6 -0
  254. package/src/icons/jupyter.svg +18 -0
  255. package/src/icons/python.svg +15 -0
  256. package/src/index.ts +3 -0
  257. package/src/layouts/k-standard-layout.ts +174 -0
  258. package/src/parts/index.ts +6 -0
  259. package/src/parts/k-app.ts +29 -0
  260. package/src/parts/k-container.ts +4 -0
  261. package/src/parts/k-contextmenu.ts +245 -0
  262. package/src/parts/k-dialog-content.ts +31 -0
  263. package/src/parts/k-element.ts +100 -0
  264. package/src/parts/k-part.ts +158 -0
  265. package/src/parts/k-resizable-grid.ts +366 -0
  266. package/src/parts/k-tabs.ts +574 -0
  267. package/src/parts/k-toolbar.ts +158 -0
  268. package/src/vite-env.d.ts +2 -0
  269. package/src/widgets/index.ts +2 -0
  270. package/src/widgets/k-icon.ts +39 -0
  271. package/src/widgets/k-nocontent.ts +40 -0
  272. package/src/widgets/k-widget.ts +90 -0
@@ -0,0 +1,618 @@
1
+ import {persistenceService} from "./persistenceservice";
2
+ import {publish} from "./events";
3
+ import {rootContext} from "./di";
4
+
5
+
6
+ export const TOPIC_WORKSPACE_CHANGED = "events/filesys/workspaceChanged"
7
+ export const TOPIC_WORKSPACE_CONNECTED = "events/filesys/workspaceConnected"
8
+
9
+ export abstract class Resource {
10
+ public state: { [p: string]: any } = {};
11
+
12
+ public abstract getName(): string;
13
+
14
+ public abstract getParent(): Directory | undefined;
15
+
16
+ public abstract delete(name?: string, recursive?: boolean): Promise<void>;
17
+
18
+ public abstract copyTo(targetPath: string): Promise<void>;
19
+
20
+ public abstract rename(newName: string): Promise<void>;
21
+
22
+ public getWorkspacePath(): string {
23
+ const paths: string[] = []
24
+ let current: Resource | undefined = this
25
+ while (current) {
26
+ paths.push(current.getName())
27
+ current = current.getParent()
28
+ }
29
+ paths.reverse()
30
+ // the first path is the workspace itself, remove it as the path is always realtive to the workspace
31
+ paths.shift()
32
+ return paths.join("/");
33
+ }
34
+
35
+ public getWorkspace(): Directory {
36
+ let current: Resource | undefined = this
37
+ while (current) {
38
+ const parent: any = current.getParent()
39
+ if (parent) {
40
+ current = parent
41
+ } else {
42
+ break
43
+ }
44
+ }
45
+ return current as Directory
46
+ }
47
+ }
48
+
49
+ export const resourceComparator = (c1: Resource, c2: Resource) => {
50
+ if (c1 instanceof Directory && c2 instanceof File) {
51
+ return -1
52
+ }
53
+ if (c1 instanceof File && c2 instanceof Directory) {
54
+ return 1
55
+ }
56
+ return c1.getName().localeCompare(c2.getName())
57
+ }
58
+
59
+ export enum FileContentType {
60
+ TEXT, BINARY
61
+ }
62
+
63
+ export enum FileContentEncoding {
64
+ BASE64
65
+ }
66
+
67
+ export interface FileContentsOptions {
68
+ contentType?: FileContentType;
69
+ encoding?: FileContentEncoding;
70
+ uri?: boolean;
71
+ blob?: boolean;
72
+ }
73
+
74
+ export interface GetResourceOptions {
75
+ create?: boolean
76
+ }
77
+
78
+ export abstract class File extends Resource {
79
+ public abstract getContents(options?: FileContentsOptions): Promise<any>;
80
+
81
+ public abstract saveContents(contents: any, options?: FileContentsOptions): Promise<void>;
82
+
83
+ public abstract size(): Promise<number | null>;
84
+ }
85
+
86
+ export abstract class Directory extends Resource {
87
+ public abstract listChildren(forceRefresh: boolean): Promise<Resource[]>;
88
+
89
+ public abstract getResource(path: string, options?: GetResourceOptions): Promise<Resource | null>;
90
+
91
+ public abstract touch(): void;
92
+ }
93
+
94
+ export class StringFile extends File {
95
+ private contents: string;
96
+ private name: string;
97
+
98
+ constructor(contents: string, name: string) {
99
+ super();
100
+ this.contents = contents
101
+ this.name = name
102
+ }
103
+
104
+ async getContents(_options?: FileContentsOptions): Promise<any> {
105
+ return this.contents
106
+ }
107
+
108
+ async saveContents(contents: any, _options?: FileContentsOptions): Promise<void> {
109
+ this.contents = contents
110
+ }
111
+
112
+ async size(): Promise<number | null> {
113
+ return this.contents.length || null;
114
+ }
115
+
116
+ async copyTo(_targetPath: string): Promise<void> {
117
+ throw Error(`Not supported`);
118
+ }
119
+
120
+ delete(_name?: string, _recursive?: boolean): Promise<void> {
121
+ throw Error(`Not supported`);
122
+ }
123
+
124
+ async rename(_newName: string): Promise<void> {
125
+ throw Error(`Not supported`);
126
+ }
127
+
128
+ getName(): string {
129
+ return this.name;
130
+ }
131
+
132
+ getParent(): Directory | undefined {
133
+ return undefined;
134
+ }
135
+ }
136
+
137
+ export class FileSysFileHandleResource extends File {
138
+ private fileHandle: FileSystemFileHandle;
139
+ private parent: Directory;
140
+
141
+ constructor(fileHandle: FileSystemFileHandle, parent: Directory) {
142
+ super();
143
+ this.fileHandle = fileHandle;
144
+ this.parent = parent;
145
+ }
146
+
147
+ getName(): string {
148
+ return this.fileHandle.name;
149
+ }
150
+
151
+ getParent(): Directory {
152
+ return this.parent;
153
+ }
154
+
155
+ async delete() {
156
+ return this.getParent().delete(this.getName());
157
+ }
158
+
159
+ async getContents(options?: FileContentsOptions): Promise<any> {
160
+ const file = await this.fileHandle.getFile();
161
+
162
+ if (!options || options?.contentType == FileContentType.TEXT) {
163
+ return await file.text()
164
+ }
165
+
166
+ if (options?.encoding == FileContentEncoding.BASE64 || options?.uri) {
167
+ return URL.createObjectURL(file)
168
+ }
169
+
170
+ if (options?.blob) {
171
+ return file
172
+ }
173
+
174
+ return file.stream()
175
+ }
176
+
177
+ async size(): Promise<number | null> {
178
+ try {
179
+ const file = await this.fileHandle.getFile();
180
+ return file.size;
181
+ } catch {
182
+ return null;
183
+ }
184
+ }
185
+
186
+ async saveContents(contents: any, _options?: FileContentsOptions) {
187
+ const writable = await this.fileHandle.createWritable()
188
+
189
+ // Check if contents is a ReadableStream (for streaming large files)
190
+ if (contents && typeof contents.pipeTo === 'function') {
191
+ await contents.pipeTo(writable)
192
+ } else {
193
+ // Traditional approach for blobs, strings, etc.
194
+ const writer = writable.getWriter()
195
+ try {
196
+ await writer.write(contents)
197
+ } finally {
198
+ await writer.close()
199
+ }
200
+ }
201
+ }
202
+
203
+ async copyTo(targetPath: string): Promise<void> {
204
+ const contents = await this.getContents({blob: true})
205
+ const targetFile = await this.getWorkspace().getResource(targetPath, {create: true}) as File
206
+ await targetFile.saveContents(contents)
207
+ }
208
+
209
+ async rename(newName: string): Promise<void> {
210
+ const parent = this.getParent() as FileSysDirHandleResource;
211
+ if (!parent) {
212
+ throw new Error('Cannot rename root resource');
213
+ }
214
+
215
+ if (this.getName() === newName) {
216
+ return;
217
+ }
218
+
219
+ if (!('move' in this.fileHandle) || typeof (this.fileHandle as any).move !== 'function') {
220
+ throw new Error('File rename not supported in this browser. Please use a browser with File System Access API move() support.');
221
+ }
222
+
223
+ try {
224
+ await (this.fileHandle as any).move(newName);
225
+ } catch (error: any) {
226
+ if (error.name === 'NotAllowedError' ||
227
+ error.message?.includes('not allowed') ||
228
+ error.message?.includes('user agent')) {
229
+ throw new Error('File rename failed: The operation took too long and user activation expired. Please try again.');
230
+ }
231
+ throw error;
232
+ }
233
+
234
+ await parent.listChildren(true);
235
+ publish(TOPIC_WORKSPACE_CHANGED, this.getWorkspace());
236
+ }
237
+ }
238
+
239
+ export interface ResourceMap {
240
+ [key: string]: Resource;
241
+ }
242
+
243
+ export class FileSysDirHandleResource extends Directory {
244
+ private dirHandle: FileSystemDirectoryHandle;
245
+ private files?: ResourceMap;
246
+ private parent?: Directory;
247
+ private loadingPromise?: Promise<Resource[]>;
248
+
249
+ constructor(dirHandle: FileSystemDirectoryHandle, parent?: Directory) {
250
+ super();
251
+ this.dirHandle = dirHandle;
252
+ this.parent = parent;
253
+ }
254
+
255
+ getHandle() {
256
+ return this.dirHandle
257
+ }
258
+
259
+ getParent(): Directory | undefined {
260
+ return this.parent;
261
+ }
262
+
263
+ getName(): string {
264
+ return this.dirHandle.name;
265
+ }
266
+
267
+ async listChildren(forceRefresh: boolean = false): Promise<Resource[]> {
268
+ if (forceRefresh || !this.files) {
269
+ if (this.loadingPromise) {
270
+ return this.loadingPromise;
271
+ }
272
+
273
+ this.loadingPromise = (async () => {
274
+ try {
275
+ const files: ResourceMap = {};
276
+
277
+ try {
278
+ // @ts-ignore
279
+ for await (const entry of this.dirHandle.values()) {
280
+ const isFile = (<FileSystemHandle>entry).kind === "file"
281
+ const child = isFile ?
282
+ new FileSysFileHandleResource(entry, this)
283
+ : new FileSysDirHandleResource(entry, this);
284
+ files[child.getName()] = child;
285
+ }
286
+ } catch (error: any) {
287
+ if (error.name === 'NotFoundError') {
288
+ this.files = {};
289
+ return [];
290
+ }
291
+ throw error;
292
+ }
293
+
294
+ this.files = files;
295
+ return Object.values(this.files);
296
+ } finally {
297
+ this.loadingPromise = undefined;
298
+ }
299
+ })();
300
+
301
+ return this.loadingPromise;
302
+ }
303
+ return Object.values(this.files);
304
+ }
305
+
306
+ async getResource(path: string, options?: GetResourceOptions): Promise<Resource | null> {
307
+ if (!path) {
308
+ throw new Error("No path provided");
309
+ }
310
+ const segments = path.split("/")
311
+ let currentResource: Resource = this
312
+ let workspaceChanged = false;
313
+ try {
314
+ for (let i = 0; i < segments.length; i++) {
315
+ let segment = segments[i]
316
+ if (segment) {
317
+ segment = segment.trim()
318
+ }
319
+ if (!segment) {
320
+ break
321
+ }
322
+ if (currentResource instanceof FileSysDirHandleResource) {
323
+ await currentResource.listChildren()
324
+ if (!currentResource.files) {
325
+ return null
326
+ }
327
+ const next = currentResource.files[segment]
328
+ if (!next) {
329
+ if (options?.create) {
330
+ workspaceChanged = true;
331
+ if (i < segments.length - 1) {
332
+ try {
333
+ const newDirHandle = await currentResource.dirHandle.getDirectoryHandle(segment, {create: true})
334
+ const nextResource = new FileSysDirHandleResource(newDirHandle, currentResource)
335
+ currentResource.files[segment] = nextResource
336
+ currentResource = nextResource
337
+ if (currentResource instanceof FileSysDirHandleResource) {
338
+ await currentResource.listChildren()
339
+ }
340
+ continue
341
+ } catch (error: any) {
342
+ if (error.name === 'NotFoundError') {
343
+ throw new Error(`Directory not found or not accessible: ${segments.slice(0, i + 1).join('/')}`)
344
+ }
345
+ throw error
346
+ }
347
+ } else {
348
+ try {
349
+ const newFileHandle = await currentResource.dirHandle.getFileHandle(segment, {create: true})
350
+ const nextResource = new FileSysFileHandleResource(newFileHandle, currentResource)
351
+ currentResource.files[segment] = nextResource
352
+ return nextResource
353
+ } catch (error: any) {
354
+ if (error.name === 'NotFoundError') {
355
+ throw new Error(`File not found or not accessible: ${segments.join('/')}`)
356
+ }
357
+ throw error
358
+ }
359
+ }
360
+ } else {
361
+ return null
362
+ }
363
+ } else {
364
+ currentResource = next
365
+ }
366
+ }
367
+ }
368
+ } finally {
369
+ if (workspaceChanged) {
370
+ publish(TOPIC_WORKSPACE_CHANGED, this.getWorkspace());
371
+ }
372
+ }
373
+ return currentResource;
374
+ }
375
+
376
+ public touch() {
377
+ publish(TOPIC_WORKSPACE_CHANGED, this.getWorkspace());
378
+ }
379
+
380
+ async delete(name?: string, recursive: boolean = true) {
381
+ if (!name) {
382
+ const parent = this.getParent()
383
+ if (parent instanceof FileSysDirHandleResource) {
384
+ await parent.listChildren()
385
+ if (parent.files) {
386
+ delete parent.files[this.getName()]
387
+ }
388
+ }
389
+ this.files = undefined
390
+ this.loadingPromise = undefined
391
+ return parent?.delete(this.getName())
392
+ }
393
+ return this.dirHandle.removeEntry(name, {
394
+ recursive: recursive
395
+ }).then(async () => {
396
+ if (this.files) {
397
+ delete this.files[name]
398
+ }
399
+ publish(TOPIC_WORKSPACE_CHANGED, this.getWorkspace());
400
+ })
401
+ }
402
+
403
+ async copyTo(targetPath: string) {
404
+ for (const resource of (await this.listChildren())) {
405
+ const targetResourceName = [targetPath, resource.getName()].join("/")
406
+ await resource.copyTo(targetResourceName)
407
+ }
408
+ }
409
+
410
+ async rename(newName: string): Promise<void> {
411
+ const parent = this.getParent() as FileSysDirHandleResource;
412
+ if (!parent) {
413
+ throw new Error('Cannot rename workspace root');
414
+ }
415
+
416
+ if (this.getName() === newName) {
417
+ return;
418
+ }
419
+
420
+ if (!('move' in this.dirHandle) || typeof (this.dirHandle as any).move !== 'function') {
421
+ throw new Error('Directory rename not supported in this browser. Please use a browser with File System Access API move() support.');
422
+ }
423
+
424
+ try {
425
+ await (this.dirHandle as any).move(newName);
426
+ } catch (error: any) {
427
+ if (error.name === 'NotAllowedError' ||
428
+ error.message?.includes('not allowed') ||
429
+ error.message?.includes('user agent')) {
430
+ throw new Error('Directory rename failed: The operation took too long and user activation expired. Please try again.');
431
+ }
432
+ throw error;
433
+ }
434
+
435
+ await parent.listChildren(true);
436
+ publish(TOPIC_WORKSPACE_CHANGED, this.getWorkspace());
437
+ }
438
+ }
439
+
440
+ /**
441
+ * Interface for workspace contributions
442
+ *
443
+ * Allows extensions to register custom workspace implementations
444
+ */
445
+ export interface WorkspaceContribution {
446
+ /**
447
+ * Unique identifier for this workspace type (e.g., 'filesystem', 'webdav')
448
+ */
449
+ type: string;
450
+
451
+ /**
452
+ * Display name for this workspace type
453
+ */
454
+ name: string;
455
+
456
+ /**
457
+ * Check if this contribution can handle the given connection input
458
+ */
459
+ canHandle(input: any): boolean;
460
+
461
+ /**
462
+ * Create a Directory from the given connection input
463
+ */
464
+ connect(input: any): Promise<Directory>;
465
+
466
+ /**
467
+ * Restore a workspace from persisted data
468
+ */
469
+ restore?(data: any): Promise<Directory | undefined>;
470
+
471
+ /**
472
+ * Prepare data for persistence
473
+ */
474
+ persist?(workspace: Directory): Promise<any>;
475
+ }
476
+
477
+ interface PersistedWorkspaceData {
478
+ type: string;
479
+ data: any;
480
+ }
481
+
482
+ export class WorkspaceService {
483
+ private workspace?: Promise<Directory | undefined>;
484
+ private currentType?: string;
485
+ private contributions: Map<string, WorkspaceContribution> = new Map();
486
+
487
+ constructor() {
488
+ this.workspace = this.loadPersistedWorkspace();
489
+ this.workspace.then(workspace => {
490
+ if (workspace) {
491
+ publish(TOPIC_WORKSPACE_CONNECTED, workspace);
492
+ }
493
+ });
494
+ }
495
+
496
+ /**
497
+ * Register a workspace contribution
498
+ */
499
+ registerContribution(contribution: WorkspaceContribution): void {
500
+ this.contributions.set(contribution.type, contribution);
501
+ console.log(`Workspace contribution registered: ${contribution.name} (${contribution.type})`);
502
+ }
503
+
504
+ /**
505
+ * Get all registered workspace contributions
506
+ */
507
+ getContributions(): WorkspaceContribution[] {
508
+ return Array.from(this.contributions.values());
509
+ }
510
+
511
+ private async loadPersistedWorkspace(): Promise<Directory | undefined> {
512
+ const persistedData = await persistenceService.getObject("workspace_data") as PersistedWorkspaceData | null;
513
+
514
+ if (!persistedData) {
515
+ return undefined;
516
+ }
517
+
518
+ const contribution = this.contributions.get(persistedData.type);
519
+ if (!contribution) {
520
+ console.warn(`No contribution found for workspace type: ${persistedData.type}`);
521
+ return undefined;
522
+ }
523
+
524
+ try {
525
+ if (contribution.restore) {
526
+ const workspace = await contribution.restore(persistedData.data);
527
+ if (workspace) {
528
+ this.currentType = persistedData.type;
529
+ }
530
+ return workspace;
531
+ }
532
+ } catch (error) {
533
+ console.error(`Failed to restore workspace of type ${persistedData.type}:`, error);
534
+ }
535
+
536
+ return undefined;
537
+ }
538
+
539
+ async connectWorkspace(input: any): Promise<Directory> {
540
+ // Find a contribution that can handle this input
541
+ const contribution = Array.from(this.contributions.values()).find(c => c.canHandle(input));
542
+
543
+ if (!contribution) {
544
+ throw new Error('No workspace contribution can handle this input');
545
+ }
546
+
547
+ // Connect using the contribution
548
+ const workspace = await contribution.connect(input);
549
+
550
+ // Persist the workspace data
551
+ const persistData = contribution.persist ? await contribution.persist(workspace) : input;
552
+ const workspaceData: PersistedWorkspaceData = {
553
+ type: contribution.type,
554
+ data: persistData
555
+ };
556
+ await persistenceService.persistObject("workspace_data", workspaceData);
557
+
558
+ // Update current workspace
559
+ this.currentType = contribution.type;
560
+ this.workspace = Promise.resolve(workspace);
561
+ publish(TOPIC_WORKSPACE_CONNECTED, workspace);
562
+
563
+ return workspace;
564
+ }
565
+
566
+ public async getWorkspace(): Promise<Directory | undefined> {
567
+ if (!this.workspace) {
568
+ throw new Error('No workspace connected.');
569
+ }
570
+ return await this.workspace;
571
+ }
572
+
573
+ public isConnected(): boolean {
574
+ return !!this.workspace;
575
+ }
576
+
577
+ public getWorkspaceType(): string | undefined {
578
+ return this.currentType;
579
+ }
580
+
581
+ public async disconnectWorkspace(): Promise<void> {
582
+ this.workspace = undefined;
583
+ this.currentType = undefined;
584
+ await persistenceService.persistObject("workspace_data", null);
585
+ await persistenceService.persistObject("workspace", null); // Clean up legacy
586
+ }
587
+ }
588
+
589
+ export const workspaceService = new WorkspaceService();
590
+ rootContext.put("workspaceService", workspaceService);
591
+
592
+ // Register default filesystem contribution
593
+ workspaceService.registerContribution({
594
+ type: 'filesystem',
595
+ name: 'Local File System',
596
+
597
+ canHandle(input: any): boolean {
598
+ return input && 'kind' in input && input.kind === 'directory';
599
+ },
600
+
601
+ async connect(input: FileSystemDirectoryHandle): Promise<Directory> {
602
+ return new FileSysDirHandleResource(input);
603
+ },
604
+
605
+ async restore(data: any): Promise<Directory | undefined> {
606
+ if (data && 'kind' in data && data.kind === 'directory') {
607
+ return new FileSysDirHandleResource(data, undefined);
608
+ }
609
+ return undefined;
610
+ },
611
+
612
+ async persist(workspace: Directory): Promise<any> {
613
+ if (workspace instanceof FileSysDirHandleResource) {
614
+ return workspace.getHandle();
615
+ }
616
+ return null;
617
+ }
618
+ });