@eum-agent/file-explorer 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +181 -121
- package/dist/schema.js +70 -1
- package/package.json +5 -5
- package/ui/ui.js +393 -293
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AccessErrorSchema, FileBytesParamsSchema, FileEntrySchema, FileTargetSchema, ListParamsSchema, ListResultSchema, MkdirParamsSchema,
|
|
1
|
+
import { AccessErrorSchema, FileBytesParamsSchema, FileEntrySchema, FileTargetSchema, FilesChangedSchema, ListParamsSchema, ListResultSchema, MkdirParamsSchema, MoveFilesParamsSchema, MoveFilesResultSchema, OperationResultSchema, OperationStatusesParamsSchema, OperationStatusesResultSchema, PathSchema, RemoveFilesParamsSchema, RemoveFilesResultSchema, RenameParamsSchema, RootsParamsSchema, RootsResultSchema, SegmentSchema, StatsParamsSchema, StatsResultSchema } from "./schema.js";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
@@ -50,6 +50,143 @@ function accessError(error) {
|
|
|
50
50
|
return new FileAccessError(business in status ? business : "io-failed", status[business] ?? 500);
|
|
51
51
|
}
|
|
52
52
|
//#endregion
|
|
53
|
+
//#region src/batches.ts
|
|
54
|
+
const encoded = (value) => new TextEncoder().encode(JSON.stringify(value)).byteLength;
|
|
55
|
+
/** Runs independent members with owner cancellation and complete serialized-member limits. */
|
|
56
|
+
async function fileCollection(items, signal, work, maxItemBytes = Math.floor(8380416 / items.length)) {
|
|
57
|
+
const results = [];
|
|
58
|
+
for (const [index, target] of items.entries()) {
|
|
59
|
+
signal.throwIfAborted();
|
|
60
|
+
try {
|
|
61
|
+
const value = await work(target);
|
|
62
|
+
signal.throwIfAborted();
|
|
63
|
+
const member = {
|
|
64
|
+
index,
|
|
65
|
+
target,
|
|
66
|
+
ok: true,
|
|
67
|
+
value
|
|
68
|
+
};
|
|
69
|
+
if (encoded(member) > maxItemBytes) throw new FileAccessError("read-limit", 413);
|
|
70
|
+
results.push(member);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
signal.throwIfAborted();
|
|
73
|
+
const failure = accessError(error);
|
|
74
|
+
results.push({
|
|
75
|
+
index,
|
|
76
|
+
target,
|
|
77
|
+
ok: false,
|
|
78
|
+
error: {
|
|
79
|
+
code: failure.code,
|
|
80
|
+
message: failure.message
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return { results };
|
|
86
|
+
}
|
|
87
|
+
//#endregion
|
|
88
|
+
//#region src/listings.ts
|
|
89
|
+
/** Owner-local bounded immutable directory snapshots and opaque page cursors. */
|
|
90
|
+
/** Keeps at most eight sixty-second snapshots; every page still revalidates current scope. */
|
|
91
|
+
var Listings = class {
|
|
92
|
+
scope;
|
|
93
|
+
now;
|
|
94
|
+
snapshots = /* @__PURE__ */ new Map();
|
|
95
|
+
/** Shares the provider's scope and clock without retaining host descriptors. */
|
|
96
|
+
constructor(scope, now) {
|
|
97
|
+
this.scope = scope;
|
|
98
|
+
this.now = now;
|
|
99
|
+
}
|
|
100
|
+
/** Returns a 200-entry page of one immutable generation or refuses an expired/cross-target cursor. */
|
|
101
|
+
async list(params, signal) {
|
|
102
|
+
const selection = await this.scope.select(params.target, signal);
|
|
103
|
+
try {
|
|
104
|
+
const directory = selection.name ? await selection.parent.openDirectory([selection.name]) : selection.parent;
|
|
105
|
+
const current = this.scope.fingerprint(await directory.stat());
|
|
106
|
+
const now = this.now();
|
|
107
|
+
for (const [id, snapshot] of this.snapshots) if (now - snapshot.snapshotAt >= 6e4) this.snapshots.delete(id);
|
|
108
|
+
let snapshot;
|
|
109
|
+
let offset = 0;
|
|
110
|
+
if (params.cursor !== void 0) {
|
|
111
|
+
for (const candidate of this.snapshots.values()) for (const [position, cursor] of candidate.cursors) if (cursor === params.cursor) {
|
|
112
|
+
snapshot = candidate;
|
|
113
|
+
offset = position;
|
|
114
|
+
}
|
|
115
|
+
if (!snapshot || snapshot.target !== JSON.stringify(params.target)) throw new FileAccessError("stale-cursor", 409);
|
|
116
|
+
} else {
|
|
117
|
+
const observed = await directory.list(1e4);
|
|
118
|
+
const entries = [];
|
|
119
|
+
for (const item of observed) {
|
|
120
|
+
const path = [params.target.path, item.name].filter(Boolean).join("/");
|
|
121
|
+
if (item.observation.kind === "symlink") {
|
|
122
|
+
const linked = await this.scope.select({
|
|
123
|
+
...params.target,
|
|
124
|
+
path
|
|
125
|
+
}, signal);
|
|
126
|
+
try {
|
|
127
|
+
entries.push(this.scope.entry(linked));
|
|
128
|
+
} finally {
|
|
129
|
+
await linked.close();
|
|
130
|
+
}
|
|
131
|
+
} else entries.push(this.scope.entry({
|
|
132
|
+
...selection,
|
|
133
|
+
parent: directory,
|
|
134
|
+
name: item.name,
|
|
135
|
+
path: join(directory.path, item.name),
|
|
136
|
+
effectivePath: join(directory.path, item.name),
|
|
137
|
+
target: {
|
|
138
|
+
...params.target,
|
|
139
|
+
path
|
|
140
|
+
},
|
|
141
|
+
observation: item.observation
|
|
142
|
+
}));
|
|
143
|
+
}
|
|
144
|
+
if (this.scope.fingerprint(await directory.stat()) !== current) throw new FileAccessError("stale", 409);
|
|
145
|
+
const order = {
|
|
146
|
+
directory: 0,
|
|
147
|
+
file: 1,
|
|
148
|
+
symlink: 2,
|
|
149
|
+
other: 3
|
|
150
|
+
};
|
|
151
|
+
entries.sort((a, b) => order[a.kind] - order[b.kind] || (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
152
|
+
if (this.snapshots.size === 8) this.snapshots.delete(this.snapshots.keys().next().value);
|
|
153
|
+
snapshot = {
|
|
154
|
+
target: JSON.stringify(params.target),
|
|
155
|
+
entries,
|
|
156
|
+
listingId: randomUUID(),
|
|
157
|
+
snapshotAt: now,
|
|
158
|
+
fingerprint: current,
|
|
159
|
+
cursors: /* @__PURE__ */ new Map()
|
|
160
|
+
};
|
|
161
|
+
this.snapshots.set(snapshot.listingId, snapshot);
|
|
162
|
+
}
|
|
163
|
+
await selection.current();
|
|
164
|
+
const next = offset + 200;
|
|
165
|
+
let nextCursor;
|
|
166
|
+
if (next < snapshot.entries.length) {
|
|
167
|
+
nextCursor = snapshot.cursors.get(next) ?? randomUUID();
|
|
168
|
+
snapshot.cursors.set(next, nextCursor);
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
entries: snapshot.entries.slice(offset, next),
|
|
172
|
+
nextCursor,
|
|
173
|
+
fingerprint: snapshot.fingerprint,
|
|
174
|
+
listingId: snapshot.listingId,
|
|
175
|
+
snapshotAt: snapshot.snapshotAt,
|
|
176
|
+
stale: snapshot.fingerprint !== current
|
|
177
|
+
};
|
|
178
|
+
} catch (error) {
|
|
179
|
+
throw accessError(error);
|
|
180
|
+
} finally {
|
|
181
|
+
await selection.close();
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/** Releases in-memory generations on owner shutdown. */
|
|
185
|
+
close() {
|
|
186
|
+
this.snapshots.clear();
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
//#endregion
|
|
53
190
|
//#region src/path-semantics.ts
|
|
54
191
|
/** Host path syntax only; resolved object identity remains the platform's responsibility. */
|
|
55
192
|
/** Requires an independent absolute root, excluding Windows drive-relative and root-relative forms. */
|
|
@@ -240,7 +377,8 @@ var FileScope = class {
|
|
|
240
377
|
referenceUri(selection) {
|
|
241
378
|
const formatted = this.referenceFormatter?.formatCurrent?.({
|
|
242
379
|
gatewayId: selection.snapshot.gatewayId,
|
|
243
|
-
|
|
380
|
+
pathStyle: this.paths.sep === "\\" ? "windows" : "posix",
|
|
381
|
+
absolutePath: selection.path
|
|
244
382
|
});
|
|
245
383
|
if (!formatted) throw new FileAccessError("unavailable", 503);
|
|
246
384
|
return formatted.uri;
|
|
@@ -256,108 +394,6 @@ var FileScope = class {
|
|
|
256
394
|
}
|
|
257
395
|
};
|
|
258
396
|
//#endregion
|
|
259
|
-
//#region src/listings.ts
|
|
260
|
-
/** Owner-local bounded immutable directory snapshots and opaque page cursors. */
|
|
261
|
-
/** Keeps at most eight sixty-second snapshots; every page still revalidates current scope. */
|
|
262
|
-
var Listings = class {
|
|
263
|
-
scope;
|
|
264
|
-
now;
|
|
265
|
-
snapshots = /* @__PURE__ */ new Map();
|
|
266
|
-
/** Shares the provider's scope and clock without retaining host descriptors. */
|
|
267
|
-
constructor(scope, now) {
|
|
268
|
-
this.scope = scope;
|
|
269
|
-
this.now = now;
|
|
270
|
-
}
|
|
271
|
-
/** Returns a 200-entry page of one immutable generation or refuses an expired/cross-target cursor. */
|
|
272
|
-
async list(params, signal) {
|
|
273
|
-
const selection = await this.scope.select(params.target, signal);
|
|
274
|
-
try {
|
|
275
|
-
const directory = selection.name ? await selection.parent.openDirectory([selection.name]) : selection.parent;
|
|
276
|
-
const current = this.scope.fingerprint(await directory.stat());
|
|
277
|
-
const now = this.now();
|
|
278
|
-
for (const [id, snapshot] of this.snapshots) if (now - snapshot.snapshotAt >= 6e4) this.snapshots.delete(id);
|
|
279
|
-
let snapshot;
|
|
280
|
-
let offset = 0;
|
|
281
|
-
if (params.cursor !== void 0) {
|
|
282
|
-
for (const candidate of this.snapshots.values()) for (const [position, cursor] of candidate.cursors) if (cursor === params.cursor) {
|
|
283
|
-
snapshot = candidate;
|
|
284
|
-
offset = position;
|
|
285
|
-
}
|
|
286
|
-
if (!snapshot || snapshot.target !== JSON.stringify(params.target)) throw new FileAccessError("stale-cursor", 409);
|
|
287
|
-
} else {
|
|
288
|
-
const observed = await directory.list(1e4);
|
|
289
|
-
const entries = [];
|
|
290
|
-
for (const item of observed) {
|
|
291
|
-
const path = [params.target.path, item.name].filter(Boolean).join("/");
|
|
292
|
-
if (item.observation.kind === "symlink") {
|
|
293
|
-
const linked = await this.scope.select({
|
|
294
|
-
...params.target,
|
|
295
|
-
path
|
|
296
|
-
}, signal);
|
|
297
|
-
try {
|
|
298
|
-
entries.push(this.scope.entry(linked));
|
|
299
|
-
} finally {
|
|
300
|
-
await linked.close();
|
|
301
|
-
}
|
|
302
|
-
} else entries.push(this.scope.entry({
|
|
303
|
-
...selection,
|
|
304
|
-
parent: directory,
|
|
305
|
-
name: item.name,
|
|
306
|
-
path: join(directory.path, item.name),
|
|
307
|
-
effectivePath: join(directory.path, item.name),
|
|
308
|
-
target: {
|
|
309
|
-
...params.target,
|
|
310
|
-
path
|
|
311
|
-
},
|
|
312
|
-
observation: item.observation
|
|
313
|
-
}));
|
|
314
|
-
}
|
|
315
|
-
if (this.scope.fingerprint(await directory.stat()) !== current) throw new FileAccessError("stale", 409);
|
|
316
|
-
const order = {
|
|
317
|
-
directory: 0,
|
|
318
|
-
file: 1,
|
|
319
|
-
symlink: 2,
|
|
320
|
-
other: 3
|
|
321
|
-
};
|
|
322
|
-
entries.sort((a, b) => order[a.kind] - order[b.kind] || (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
323
|
-
if (this.snapshots.size === 8) this.snapshots.delete(this.snapshots.keys().next().value);
|
|
324
|
-
snapshot = {
|
|
325
|
-
target: JSON.stringify(params.target),
|
|
326
|
-
entries,
|
|
327
|
-
listingId: randomUUID(),
|
|
328
|
-
snapshotAt: now,
|
|
329
|
-
fingerprint: current,
|
|
330
|
-
cursors: /* @__PURE__ */ new Map()
|
|
331
|
-
};
|
|
332
|
-
this.snapshots.set(snapshot.listingId, snapshot);
|
|
333
|
-
}
|
|
334
|
-
await selection.current();
|
|
335
|
-
const next = offset + 200;
|
|
336
|
-
let nextCursor;
|
|
337
|
-
if (next < snapshot.entries.length) {
|
|
338
|
-
nextCursor = snapshot.cursors.get(next) ?? randomUUID();
|
|
339
|
-
snapshot.cursors.set(next, nextCursor);
|
|
340
|
-
}
|
|
341
|
-
return {
|
|
342
|
-
entries: snapshot.entries.slice(offset, next),
|
|
343
|
-
nextCursor,
|
|
344
|
-
fingerprint: snapshot.fingerprint,
|
|
345
|
-
listingId: snapshot.listingId,
|
|
346
|
-
snapshotAt: snapshot.snapshotAt,
|
|
347
|
-
stale: snapshot.fingerprint !== current
|
|
348
|
-
};
|
|
349
|
-
} catch (error) {
|
|
350
|
-
throw accessError(error);
|
|
351
|
-
} finally {
|
|
352
|
-
await selection.close();
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
/** Releases in-memory generations on owner shutdown. */
|
|
356
|
-
close() {
|
|
357
|
-
this.snapshots.clear();
|
|
358
|
-
}
|
|
359
|
-
};
|
|
360
|
-
//#endregion
|
|
361
397
|
//#region src/locks.ts
|
|
362
398
|
/** Atomic acquisition of ordered canonical path sets prevents opposing moves from deadlocking. */
|
|
363
399
|
/** Serializes overlapping selected paths, including recursive ancestors. */
|
|
@@ -1243,6 +1279,27 @@ function registerFileExplorer(ctx) {
|
|
|
1243
1279
|
signal: ctx.signal,
|
|
1244
1280
|
instanceId: identity.instanceId
|
|
1245
1281
|
});
|
|
1282
|
+
const changed = ctx.client.registerFrame({
|
|
1283
|
+
name: "changed",
|
|
1284
|
+
delivery: "broadcast",
|
|
1285
|
+
payload: FilesChangedSchema
|
|
1286
|
+
});
|
|
1287
|
+
const announced = /* @__PURE__ */ new WeakSet();
|
|
1288
|
+
async function mutate(items, request, work, workspace) {
|
|
1289
|
+
const workspaces = /* @__PURE__ */ new Set();
|
|
1290
|
+
try {
|
|
1291
|
+
return await fileCollection(items, AbortSignal.any([ctx.signal, request]), async (item) => {
|
|
1292
|
+
const value = await work(item, request);
|
|
1293
|
+
if (!announced.has(value) && (value.status === "succeeded" || value.error?.details?.destinationCommitted)) {
|
|
1294
|
+
announced.add(value);
|
|
1295
|
+
workspaces.add(workspace(item));
|
|
1296
|
+
}
|
|
1297
|
+
return value;
|
|
1298
|
+
});
|
|
1299
|
+
} finally {
|
|
1300
|
+
if (workspaces.size && !ctx.signal.aborted) changed.emit({ workspaces: [...workspaces] });
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1246
1303
|
const drain = () => provider.close();
|
|
1247
1304
|
const unbind = ctx.services.get("work-lifecycle").register(drain);
|
|
1248
1305
|
ctx.onDispose(async () => {
|
|
@@ -1262,10 +1319,10 @@ function registerFileExplorer(ctx) {
|
|
|
1262
1319
|
handle: (params, call) => provider.list(params, call.signal)
|
|
1263
1320
|
});
|
|
1264
1321
|
ctx.client.registerMethod({
|
|
1265
|
-
name: "
|
|
1266
|
-
params:
|
|
1267
|
-
result:
|
|
1268
|
-
handle: (params, call) => provider.stat(
|
|
1322
|
+
name: "stats",
|
|
1323
|
+
params: StatsParamsSchema,
|
|
1324
|
+
result: StatsResultSchema,
|
|
1325
|
+
handle: (params, call) => fileCollection(params.targets, AbortSignal.any([ctx.signal, call.signal]), (target) => provider.stat(target, call.signal), params.maxItemBytes)
|
|
1269
1326
|
});
|
|
1270
1327
|
ctx.client.registerMethod({
|
|
1271
1328
|
name: "mkdir",
|
|
@@ -1280,22 +1337,25 @@ function registerFileExplorer(ctx) {
|
|
|
1280
1337
|
handle: (params, call) => provider.rename(params, call.signal)
|
|
1281
1338
|
});
|
|
1282
1339
|
ctx.client.registerMethod({
|
|
1283
|
-
name: "
|
|
1284
|
-
params:
|
|
1285
|
-
result:
|
|
1286
|
-
handle: (params, call) =>
|
|
1340
|
+
name: "moveFiles",
|
|
1341
|
+
params: MoveFilesParamsSchema,
|
|
1342
|
+
result: MoveFilesResultSchema,
|
|
1343
|
+
handle: (params, call) => mutate(params.items, call.signal, provider.move, (item) => item.source.workspaceId)
|
|
1287
1344
|
});
|
|
1288
1345
|
ctx.client.registerMethod({
|
|
1289
|
-
name: "
|
|
1290
|
-
params:
|
|
1291
|
-
result:
|
|
1292
|
-
handle: (params, call) =>
|
|
1346
|
+
name: "removeFiles",
|
|
1347
|
+
params: RemoveFilesParamsSchema,
|
|
1348
|
+
result: RemoveFilesResultSchema,
|
|
1349
|
+
handle: (params, call) => mutate(params.items, call.signal, provider.remove, (item) => item.target.workspaceId)
|
|
1293
1350
|
});
|
|
1294
1351
|
ctx.client.registerMethod({
|
|
1295
|
-
name: "
|
|
1296
|
-
params:
|
|
1297
|
-
result:
|
|
1298
|
-
handle: (params) => provider.operationStatus(
|
|
1352
|
+
name: "operationStatuses",
|
|
1353
|
+
params: OperationStatusesParamsSchema,
|
|
1354
|
+
result: OperationStatusesResultSchema,
|
|
1355
|
+
handle: (params, call) => fileCollection(params.operationIds, AbortSignal.any([ctx.signal, call.signal]), (operationId) => provider.operationStatus({
|
|
1356
|
+
instanceId: params.instanceId,
|
|
1357
|
+
operationId
|
|
1358
|
+
}), params.maxItemBytes)
|
|
1299
1359
|
});
|
|
1300
1360
|
ctx.client.registerResource({
|
|
1301
1361
|
name: "fileBytes",
|
|
@@ -1314,7 +1374,7 @@ function registerFileExplorer(ctx) {
|
|
|
1314
1374
|
}
|
|
1315
1375
|
const plugin = {
|
|
1316
1376
|
name: "@eum-agent/file-explorer",
|
|
1317
|
-
dependencies: { "@eum-agent/file-references": "1.
|
|
1377
|
+
dependencies: { "@eum-agent/file-references": "1.1.0" },
|
|
1318
1378
|
requires: [
|
|
1319
1379
|
"client-files",
|
|
1320
1380
|
"workspace-roots",
|
package/dist/schema.js
CHANGED
|
@@ -142,5 +142,74 @@ const OperationStatusParamsSchema = z.object({
|
|
|
142
142
|
instanceId: GatewayIdSchema,
|
|
143
143
|
operationId: z.uuid()
|
|
144
144
|
}).strict();
|
|
145
|
+
const encoded = (value) => new TextEncoder().encode(JSON.stringify(value)).byteLength;
|
|
146
|
+
const budget = {
|
|
147
|
+
maxItemBytes: z.number().int().min(1).max(8388608).default(262144),
|
|
148
|
+
maxTotalBytes: z.number().int().min(1).max(8388608).default(8388608)
|
|
149
|
+
};
|
|
150
|
+
/** Rejects input and declared normalized response costs before any filesystem work. */
|
|
151
|
+
function admit(input, targets, memberBytes, totalBytes, ctx) {
|
|
152
|
+
if (encoded(input) * 2 > 8388608 || targets.length * memberBytes + 8192 > totalBytes || targets.some((target) => encoded(target) + 4096 > memberBytes)) ctx.addIssue({
|
|
153
|
+
code: "custom",
|
|
154
|
+
message: "File collection exceeds its byte budget"
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
/** Complete file observations with independent encoded response budgets. */
|
|
158
|
+
const StatsParamsSchema = z.object({
|
|
159
|
+
targets: z.array(FileTargetSchema).min(1).max(64),
|
|
160
|
+
...budget
|
|
161
|
+
}).strict().superRefine((p, ctx) => admit(p, p.targets, p.maxItemBytes, p.maxTotalBytes, ctx));
|
|
162
|
+
/** Retained operation queries never execute work, including unknown identities. */
|
|
163
|
+
const OperationStatusesParamsSchema = z.object({
|
|
164
|
+
instanceId: GatewayIdSchema,
|
|
165
|
+
operationIds: z.array(z.uuid()).min(1).max(64),
|
|
166
|
+
...budget
|
|
167
|
+
}).strict().superRefine((p, ctx) => admit(p, p.operationIds, p.maxItemBytes, p.maxTotalBytes, ctx));
|
|
168
|
+
/** Validates complete mutation members and refuses conflicting reuse before any member starts. */
|
|
169
|
+
function mutations(schema) {
|
|
170
|
+
return z.object({ items: z.array(schema).min(1).max(64) }).strict().superRefine((p, ctx) => {
|
|
171
|
+
if (encoded(p) * 2 > 8388608) ctx.addIssue({
|
|
172
|
+
code: "custom",
|
|
173
|
+
message: "File collection exceeds its byte budget"
|
|
174
|
+
});
|
|
175
|
+
const identities = /* @__PURE__ */ new Map();
|
|
176
|
+
for (const member of p.items) {
|
|
177
|
+
const input = JSON.stringify(member);
|
|
178
|
+
if (identities.has(member.operationId) && identities.get(member.operationId) !== input) ctx.addIssue({
|
|
179
|
+
code: "custom",
|
|
180
|
+
message: "Operation identity has conflicting members"
|
|
181
|
+
});
|
|
182
|
+
identities.set(member.operationId, input);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
/** Each move retains its own complete operation identity and physical outcome. */
|
|
187
|
+
const MoveFilesParamsSchema = mutations(MoveParamsSchema);
|
|
188
|
+
/** Each removal retains its own selected fingerprint and recursive decision. */
|
|
189
|
+
const RemoveFilesParamsSchema = mutations(RemoveParamsSchema);
|
|
190
|
+
/** One notification lists all workspaces physically changed by this collection. */
|
|
191
|
+
const FilesChangedSchema = z.object({ workspaces: z.array(IdentitySchema).min(1).max(64) }).strict();
|
|
192
|
+
/** Complete successful members and bounded safe refusals retain their original order. */
|
|
193
|
+
function collectionResult(target, value) {
|
|
194
|
+
return z.object({ results: z.array(z.discriminatedUnion("ok", [z.object({
|
|
195
|
+
index: z.number().int().min(0).max(63),
|
|
196
|
+
target,
|
|
197
|
+
ok: z.literal(true),
|
|
198
|
+
value
|
|
199
|
+
}).strict(), z.object({
|
|
200
|
+
index: z.number().int().min(0).max(63),
|
|
201
|
+
target,
|
|
202
|
+
ok: z.literal(false),
|
|
203
|
+
error: AccessErrorSchema
|
|
204
|
+
}).strict()])).min(1).max(64) }).strict();
|
|
205
|
+
}
|
|
206
|
+
/** Ordered explicit file observations. */
|
|
207
|
+
const StatsResultSchema = collectionResult(FileTargetSchema, StatResultSchema);
|
|
208
|
+
/** Ordered retained operation facts. */
|
|
209
|
+
const OperationStatusesResultSchema = collectionResult(z.uuid(), OperationResultSchema);
|
|
210
|
+
/** Ordered move results keep complete immutable operation members as their safe target. */
|
|
211
|
+
const MoveFilesResultSchema = collectionResult(MoveParamsSchema, OperationResultSchema);
|
|
212
|
+
/** Ordered removal results keep complete immutable operation members as their safe target. */
|
|
213
|
+
const RemoveFilesResultSchema = collectionResult(RemoveParamsSchema, OperationResultSchema);
|
|
145
214
|
//#endregion
|
|
146
|
-
export { AccessErrorSchema, DigestSchema, ErrorDetailsSchema, FileBytesParamsSchema, FileEntrySchema, FileSourceObservationSchema, FileTargetSchema, GatewayIdSchema, IdentitySchema, ListParamsSchema, ListResultSchema, MkdirParamsSchema, MoveParamsSchema, OperationResultSchema, OperationStatusParamsSchema, PathSchema, RemoveParamsSchema, RenameParamsSchema, RootsParamsSchema, RootsResultSchema, SegmentSchema, StatParamsSchema, StatResultSchema };
|
|
215
|
+
export { AccessErrorSchema, DigestSchema, ErrorDetailsSchema, FileBytesParamsSchema, FileEntrySchema, FileSourceObservationSchema, FileTargetSchema, FilesChangedSchema, GatewayIdSchema, IdentitySchema, ListParamsSchema, ListResultSchema, MkdirParamsSchema, MoveFilesParamsSchema, MoveFilesResultSchema, MoveParamsSchema, OperationResultSchema, OperationStatusParamsSchema, OperationStatusesParamsSchema, OperationStatusesResultSchema, PathSchema, RemoveFilesParamsSchema, RemoveFilesResultSchema, RemoveParamsSchema, RenameParamsSchema, RootsParamsSchema, RootsResultSchema, SegmentSchema, StatParamsSchema, StatResultSchema, StatsParamsSchema, StatsResultSchema, collectionResult };
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"en-US": "Workspace Files",
|
|
5
5
|
"zh-CN": "工作空间文件"
|
|
6
6
|
},
|
|
7
|
-
"version": "1.
|
|
7
|
+
"version": "1.1.0",
|
|
8
8
|
"type": "module",
|
|
9
9
|
"description": "Sessionless workspace file browsing, transfers and private references",
|
|
10
10
|
"license": "MIT",
|
|
@@ -24,14 +24,14 @@
|
|
|
24
24
|
"zod": "^4.4.3"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
|
-
"@eum-agent/plugin": "1.
|
|
27
|
+
"@eum-agent/plugin": "1.1.0",
|
|
28
28
|
"@eum-agent/protocol": "1.0.0",
|
|
29
29
|
"react": "^19.2.0",
|
|
30
30
|
"react-dom": "^19.2.0",
|
|
31
31
|
"@types/react": "^19.2.18",
|
|
32
32
|
"@types/react-dom": "^19.2.5",
|
|
33
33
|
"vite": "^8.2.2",
|
|
34
|
-
"@eum-agent/infra-local": "1.0.
|
|
34
|
+
"@eum-agent/infra-local": "1.0.1"
|
|
35
35
|
},
|
|
36
36
|
"yaoguang": {
|
|
37
37
|
"description": {
|
|
@@ -39,11 +39,11 @@
|
|
|
39
39
|
"zh-CN": "独立的工作区文件浏览、传输与私有引用"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@eum-agent/file-references": "1.
|
|
42
|
+
"@eum-agent/file-references": "1.1.0"
|
|
43
43
|
},
|
|
44
44
|
"kind": "extension",
|
|
45
45
|
"foundationDependencies": {
|
|
46
|
-
"@eum-agent/infra-local": "1.0.
|
|
46
|
+
"@eum-agent/infra-local": "1.0.1"
|
|
47
47
|
}
|
|
48
48
|
},
|
|
49
49
|
"scripts": {
|
package/ui/ui.js
CHANGED
|
@@ -2163,6 +2163,62 @@ var $ZodUnion = /*@__PURE__*/ $constructor("$ZodUnion", (inst, def) => {
|
|
|
2163
2163
|
});
|
|
2164
2164
|
};
|
|
2165
2165
|
});
|
|
2166
|
+
var $ZodDiscriminatedUnion = /*@__PURE__*/ $constructor("$ZodDiscriminatedUnion", (inst, def) => {
|
|
2167
|
+
def.inclusive = false;
|
|
2168
|
+
$ZodUnion.init(inst, def);
|
|
2169
|
+
const _super = inst._zod.parse;
|
|
2170
|
+
defineLazy(inst._zod, "propValues", () => {
|
|
2171
|
+
const propValues = {};
|
|
2172
|
+
for (const option of def.options) {
|
|
2173
|
+
const pv = option._zod.propValues;
|
|
2174
|
+
if (!pv || Object.keys(pv).length === 0) throw new Error(`Invalid discriminated union option at index "${def.options.indexOf(option)}"`);
|
|
2175
|
+
for (const [k, v] of Object.entries(pv)) {
|
|
2176
|
+
if (!propValues[k]) propValues[k] = /* @__PURE__ */ new Set();
|
|
2177
|
+
for (const val of v) propValues[k].add(val);
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
return propValues;
|
|
2181
|
+
});
|
|
2182
|
+
const disc = cached(() => {
|
|
2183
|
+
const opts = def.options;
|
|
2184
|
+
const map = /* @__PURE__ */ new Map();
|
|
2185
|
+
for (const o of opts) {
|
|
2186
|
+
const values = o._zod.propValues?.[def.discriminator];
|
|
2187
|
+
if (!values || values.size === 0) throw new Error(`Invalid discriminated union option at index "${def.options.indexOf(o)}"`);
|
|
2188
|
+
for (const v of values) {
|
|
2189
|
+
if (map.has(v)) throw new Error(`Duplicate discriminator value "${String(v)}"`);
|
|
2190
|
+
map.set(v, o);
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
return map;
|
|
2194
|
+
});
|
|
2195
|
+
inst._zod.parse = (payload, ctx) => {
|
|
2196
|
+
const input = payload.value;
|
|
2197
|
+
if (!isObject(input)) {
|
|
2198
|
+
payload.issues.push({
|
|
2199
|
+
code: "invalid_type",
|
|
2200
|
+
expected: "object",
|
|
2201
|
+
input,
|
|
2202
|
+
inst
|
|
2203
|
+
});
|
|
2204
|
+
return payload;
|
|
2205
|
+
}
|
|
2206
|
+
const opt = disc.value.get(input?.[def.discriminator]);
|
|
2207
|
+
if (opt) return opt._zod.run(payload, ctx);
|
|
2208
|
+
if (def.unionFallback || ctx.direction === "backward") return _super(payload, ctx);
|
|
2209
|
+
payload.issues.push({
|
|
2210
|
+
code: "invalid_union",
|
|
2211
|
+
errors: [],
|
|
2212
|
+
note: "No matching discriminator",
|
|
2213
|
+
discriminator: def.discriminator,
|
|
2214
|
+
options: Array.from(disc.value.keys()),
|
|
2215
|
+
input,
|
|
2216
|
+
path: [def.discriminator],
|
|
2217
|
+
inst
|
|
2218
|
+
});
|
|
2219
|
+
return payload;
|
|
2220
|
+
};
|
|
2221
|
+
});
|
|
2166
2222
|
var $ZodIntersection = /*@__PURE__*/ $constructor("$ZodIntersection", (inst, def) => {
|
|
2167
2223
|
$ZodType.init(inst, def);
|
|
2168
2224
|
inst._zod.parse = (payload, ctx) => {
|
|
@@ -4215,6 +4271,18 @@ function union(options, params) {
|
|
|
4215
4271
|
...normalizeParams(params)
|
|
4216
4272
|
});
|
|
4217
4273
|
}
|
|
4274
|
+
var ZodDiscriminatedUnion = /*@__PURE__*/ $constructor("ZodDiscriminatedUnion", (inst, def) => {
|
|
4275
|
+
ZodUnion.init(inst, def);
|
|
4276
|
+
$ZodDiscriminatedUnion.init(inst, def);
|
|
4277
|
+
});
|
|
4278
|
+
function discriminatedUnion(discriminator, options, params) {
|
|
4279
|
+
return new ZodDiscriminatedUnion({
|
|
4280
|
+
type: "union",
|
|
4281
|
+
options,
|
|
4282
|
+
discriminator,
|
|
4283
|
+
...normalizeParams(params)
|
|
4284
|
+
});
|
|
4285
|
+
}
|
|
4218
4286
|
var ZodIntersection = /*@__PURE__*/ $constructor("ZodIntersection", (inst, def) => {
|
|
4219
4287
|
$ZodIntersection.init(inst, def);
|
|
4220
4288
|
ZodType.init(inst, def);
|
|
@@ -4466,7 +4534,8 @@ var FileTargetSchema = object({
|
|
|
4466
4534
|
path: PathSchema
|
|
4467
4535
|
}).strict();
|
|
4468
4536
|
object({ workspaceId: IdentitySchema }).strict();
|
|
4469
|
-
|
|
4537
|
+
/** Stat always names one explicit current root-relative selection. */
|
|
4538
|
+
var StatParamsSchema = object({ target: FileTargetSchema }).strict();
|
|
4470
4539
|
/** Display metadata for one current filesystem object. */
|
|
4471
4540
|
var FileEntrySchema = object({
|
|
4472
4541
|
name: string(),
|
|
@@ -4518,8 +4587,7 @@ var FileSourceObservationSchema = object({
|
|
|
4518
4587
|
size: number().int().nonnegative().max(Number.MAX_SAFE_INTEGER),
|
|
4519
4588
|
modifiedAt: number().min(-864e13).max(864e13)
|
|
4520
4589
|
}).strict();
|
|
4521
|
-
|
|
4522
|
-
var FileBytesParamsSchema = object({
|
|
4590
|
+
object({
|
|
4523
4591
|
target: FileTargetSchema,
|
|
4524
4592
|
expectedFingerprint: DigestSchema.optional(),
|
|
4525
4593
|
operationId: uuid().optional(),
|
|
@@ -4564,7 +4632,8 @@ object({
|
|
|
4564
4632
|
replace: boolean().optional(),
|
|
4565
4633
|
targetFingerprint: DigestSchema.optional()
|
|
4566
4634
|
}).strict();
|
|
4567
|
-
|
|
4635
|
+
/** Same-workspace move to an explicit final target path. */
|
|
4636
|
+
var MoveParamsSchema = object({
|
|
4568
4637
|
source: FileTargetSchema,
|
|
4569
4638
|
destination: FileTargetSchema,
|
|
4570
4639
|
sourceFingerprint: DigestSchema,
|
|
@@ -4572,30 +4641,206 @@ object({
|
|
|
4572
4641
|
replace: boolean().optional(),
|
|
4573
4642
|
targetFingerprint: DigestSchema.optional()
|
|
4574
4643
|
}).strict();
|
|
4575
|
-
|
|
4644
|
+
/** Removal requires the selected revision and an explicit recursive choice. */
|
|
4645
|
+
var RemoveParamsSchema = object({
|
|
4576
4646
|
target: FileTargetSchema,
|
|
4577
4647
|
fingerprint: DigestSchema,
|
|
4578
4648
|
recursive: boolean(),
|
|
4579
4649
|
operationId: uuid()
|
|
4580
4650
|
}).strict();
|
|
4581
|
-
|
|
4651
|
+
/** Query retained facts without executing an operation. */
|
|
4652
|
+
var OperationStatusParamsSchema = object({
|
|
4582
4653
|
instanceId: GatewayIdSchema,
|
|
4583
4654
|
operationId: uuid()
|
|
4584
4655
|
}).strict();
|
|
4585
|
-
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
|
|
4594
|
-
|
|
4595
|
-
rootId,
|
|
4596
|
-
path
|
|
4656
|
+
var encoded = (value) => new TextEncoder().encode(JSON.stringify(value)).byteLength;
|
|
4657
|
+
var budget = {
|
|
4658
|
+
maxItemBytes: number().int().min(1).max(8388608).default(262144),
|
|
4659
|
+
maxTotalBytes: number().int().min(1).max(8388608).default(8388608)
|
|
4660
|
+
};
|
|
4661
|
+
/** Rejects input and declared normalized response costs before any filesystem work. */
|
|
4662
|
+
function admit(input, targets, memberBytes, totalBytes, ctx) {
|
|
4663
|
+
if (encoded(input) * 2 > 8388608 || targets.length * memberBytes + 8192 > totalBytes || targets.some((target) => encoded(target) + 4096 > memberBytes)) ctx.addIssue({
|
|
4664
|
+
code: "custom",
|
|
4665
|
+
message: "File collection exceeds its byte budget"
|
|
4597
4666
|
});
|
|
4598
4667
|
}
|
|
4668
|
+
object({
|
|
4669
|
+
targets: array(FileTargetSchema).min(1).max(64),
|
|
4670
|
+
...budget
|
|
4671
|
+
}).strict().superRefine((p, ctx) => admit(p, p.targets, p.maxItemBytes, p.maxTotalBytes, ctx));
|
|
4672
|
+
object({
|
|
4673
|
+
instanceId: GatewayIdSchema,
|
|
4674
|
+
operationIds: array(uuid()).min(1).max(64),
|
|
4675
|
+
...budget
|
|
4676
|
+
}).strict().superRefine((p, ctx) => admit(p, p.operationIds, p.maxItemBytes, p.maxTotalBytes, ctx));
|
|
4677
|
+
/** Validates complete mutation members and refuses conflicting reuse before any member starts. */
|
|
4678
|
+
function mutations(schema) {
|
|
4679
|
+
return object({ items: array(schema).min(1).max(64) }).strict().superRefine((p, ctx) => {
|
|
4680
|
+
if (encoded(p) * 2 > 8388608) ctx.addIssue({
|
|
4681
|
+
code: "custom",
|
|
4682
|
+
message: "File collection exceeds its byte budget"
|
|
4683
|
+
});
|
|
4684
|
+
const identities = /* @__PURE__ */ new Map();
|
|
4685
|
+
for (const member of p.items) {
|
|
4686
|
+
const input = JSON.stringify(member);
|
|
4687
|
+
if (identities.has(member.operationId) && identities.get(member.operationId) !== input) ctx.addIssue({
|
|
4688
|
+
code: "custom",
|
|
4689
|
+
message: "Operation identity has conflicting members"
|
|
4690
|
+
});
|
|
4691
|
+
identities.set(member.operationId, input);
|
|
4692
|
+
}
|
|
4693
|
+
});
|
|
4694
|
+
}
|
|
4695
|
+
mutations(MoveParamsSchema);
|
|
4696
|
+
mutations(RemoveParamsSchema);
|
|
4697
|
+
/** One notification lists all workspaces physically changed by this collection. */
|
|
4698
|
+
var FilesChangedSchema = object({ workspaces: array(IdentitySchema).min(1).max(64) }).strict();
|
|
4699
|
+
/** Complete successful members and bounded safe refusals retain their original order. */
|
|
4700
|
+
function collectionResult(target, value) {
|
|
4701
|
+
return object({ results: array(discriminatedUnion("ok", [object({
|
|
4702
|
+
index: number().int().min(0).max(63),
|
|
4703
|
+
target,
|
|
4704
|
+
ok: literal(true),
|
|
4705
|
+
value
|
|
4706
|
+
}).strict(), object({
|
|
4707
|
+
index: number().int().min(0).max(63),
|
|
4708
|
+
target,
|
|
4709
|
+
ok: literal(false),
|
|
4710
|
+
error: AccessErrorSchema
|
|
4711
|
+
}).strict()])).min(1).max(64) }).strict();
|
|
4712
|
+
}
|
|
4713
|
+
/** Ordered explicit file observations. */
|
|
4714
|
+
var StatsResultSchema = collectionResult(FileTargetSchema, StatResultSchema);
|
|
4715
|
+
/** Ordered retained operation facts. */
|
|
4716
|
+
var OperationStatusesResultSchema = collectionResult(uuid(), OperationResultSchema);
|
|
4717
|
+
/** Ordered move results keep complete immutable operation members as their safe target. */
|
|
4718
|
+
var MoveFilesResultSchema = collectionResult(MoveParamsSchema, OperationResultSchema);
|
|
4719
|
+
/** Ordered removal results keep complete immutable operation members as their safe target. */
|
|
4720
|
+
var RemoveFilesResultSchema = collectionResult(RemoveParamsSchema, OperationResultSchema);
|
|
4721
|
+
//#endregion
|
|
4722
|
+
//#region ui-src/collections.ts
|
|
4723
|
+
/** Owner-local coalescing turns concurrent file intents into actual collection RPCs. */
|
|
4724
|
+
var inputs = {
|
|
4725
|
+
stat: StatParamsSchema,
|
|
4726
|
+
move: MoveParamsSchema,
|
|
4727
|
+
remove: RemoveParamsSchema,
|
|
4728
|
+
operationStatus: OperationStatusParamsSchema
|
|
4729
|
+
};
|
|
4730
|
+
var definitions = {
|
|
4731
|
+
stat: {
|
|
4732
|
+
name: "stats",
|
|
4733
|
+
key: "targets",
|
|
4734
|
+
limit: 31,
|
|
4735
|
+
result: StatsResultSchema
|
|
4736
|
+
},
|
|
4737
|
+
move: {
|
|
4738
|
+
name: "moveFiles",
|
|
4739
|
+
key: "items",
|
|
4740
|
+
limit: 64,
|
|
4741
|
+
result: MoveFilesResultSchema
|
|
4742
|
+
},
|
|
4743
|
+
remove: {
|
|
4744
|
+
name: "removeFiles",
|
|
4745
|
+
key: "items",
|
|
4746
|
+
limit: 64,
|
|
4747
|
+
result: RemoveFilesResultSchema
|
|
4748
|
+
},
|
|
4749
|
+
operationStatus: {
|
|
4750
|
+
name: "operationStatuses",
|
|
4751
|
+
key: "operationIds",
|
|
4752
|
+
limit: 31,
|
|
4753
|
+
result: OperationStatusesResultSchema
|
|
4754
|
+
}
|
|
4755
|
+
};
|
|
4756
|
+
/** Groups only one owner and exact shared startup identity; each consumer retains cancellation. */
|
|
4757
|
+
var FileCollections = class {
|
|
4758
|
+
invoke;
|
|
4759
|
+
run;
|
|
4760
|
+
groups = /* @__PURE__ */ new Map();
|
|
4761
|
+
scheduled = false;
|
|
4762
|
+
/** Uses the owner's existing bounded transport admission, with no automatic mutation retries. */
|
|
4763
|
+
constructor(invoke, run) {
|
|
4764
|
+
this.invoke = invoke;
|
|
4765
|
+
this.run = run;
|
|
4766
|
+
}
|
|
4767
|
+
/** Queues one validated control intent and preserves its complete independent operation identity. */
|
|
4768
|
+
enqueue(method, params, signal) {
|
|
4769
|
+
signal.throwIfAborted();
|
|
4770
|
+
const input = inputs[method].parse(params);
|
|
4771
|
+
const instanceId = "instanceId" in input ? input.instanceId : void 0;
|
|
4772
|
+
const target = method === "stat" && "target" in input ? input.target : method === "operationStatus" && "operationId" in input ? input.operationId : input;
|
|
4773
|
+
const key = JSON.stringify([method, instanceId]);
|
|
4774
|
+
const group = this.groups.get(key) ?? {
|
|
4775
|
+
method,
|
|
4776
|
+
instanceId,
|
|
4777
|
+
items: []
|
|
4778
|
+
};
|
|
4779
|
+
this.groups.set(key, group);
|
|
4780
|
+
const promise = new Promise((resolve, reject) => {
|
|
4781
|
+
const item = {
|
|
4782
|
+
input,
|
|
4783
|
+
target,
|
|
4784
|
+
signal,
|
|
4785
|
+
resolve,
|
|
4786
|
+
reject,
|
|
4787
|
+
done: false,
|
|
4788
|
+
abort: () => {
|
|
4789
|
+
if (item.done) return;
|
|
4790
|
+
item.done = true;
|
|
4791
|
+
signal.removeEventListener("abort", item.abort);
|
|
4792
|
+
reject(signal.reason);
|
|
4793
|
+
}
|
|
4794
|
+
};
|
|
4795
|
+
group.items.push(item);
|
|
4796
|
+
signal.addEventListener("abort", item.abort, { once: true });
|
|
4797
|
+
});
|
|
4798
|
+
if (!this.scheduled) {
|
|
4799
|
+
this.scheduled = true;
|
|
4800
|
+
queueMicrotask(() => this.flush());
|
|
4801
|
+
}
|
|
4802
|
+
return promise;
|
|
4803
|
+
}
|
|
4804
|
+
flush() {
|
|
4805
|
+
this.scheduled = false;
|
|
4806
|
+
const groups = this.groups;
|
|
4807
|
+
this.groups = /* @__PURE__ */ new Map();
|
|
4808
|
+
for (const group of groups.values()) {
|
|
4809
|
+
const definition = definitions[group.method];
|
|
4810
|
+
const active = group.items.filter((item) => !item.done);
|
|
4811
|
+
for (let start = 0; start < active.length; start += definition.limit) this.dispatch(group, active.slice(start, start + definition.limit));
|
|
4812
|
+
}
|
|
4813
|
+
}
|
|
4814
|
+
finish(item, value, error) {
|
|
4815
|
+
if (item.done) return;
|
|
4816
|
+
item.done = true;
|
|
4817
|
+
item.signal.removeEventListener("abort", item.abort);
|
|
4818
|
+
if (error !== void 0) item.reject(error);
|
|
4819
|
+
else item.resolve(value);
|
|
4820
|
+
}
|
|
4821
|
+
async dispatch(group, queued) {
|
|
4822
|
+
try {
|
|
4823
|
+
await this.run(async () => {
|
|
4824
|
+
const items = queued.filter((item) => !item.done);
|
|
4825
|
+
if (!items.length) return;
|
|
4826
|
+
const definition = definitions[group.method];
|
|
4827
|
+
const params = {
|
|
4828
|
+
...group.instanceId === void 0 ? {} : { instanceId: group.instanceId },
|
|
4829
|
+
[definition.key]: items.map((item) => item.target)
|
|
4830
|
+
};
|
|
4831
|
+
const result = definition.result.parse(await this.invoke(definition.name, params));
|
|
4832
|
+
if (result.results.length !== items.length || result.results.some((member, index) => member.index !== index || JSON.stringify(member.target) !== JSON.stringify(items[index]?.target))) throw Object.assign(/* @__PURE__ */ new Error("File collection returned unexpected members"), { code: "io-failed" });
|
|
4833
|
+
for (const [index, item] of items.entries()) {
|
|
4834
|
+
const member = result.results[index];
|
|
4835
|
+
if (member.ok) this.finish(item, member.value);
|
|
4836
|
+
else this.finish(item, void 0, Object.assign(new Error(member.error.message), member.error));
|
|
4837
|
+
}
|
|
4838
|
+
});
|
|
4839
|
+
} catch (error) {
|
|
4840
|
+
for (const item of queued) this.finish(item, void 0, error);
|
|
4841
|
+
}
|
|
4842
|
+
}
|
|
4843
|
+
};
|
|
4599
4844
|
//#endregion
|
|
4600
4845
|
//#region ui-src/api.ts
|
|
4601
4846
|
var results = {
|
|
@@ -4619,10 +4864,20 @@ function errorCode(error) {
|
|
|
4619
4864
|
var FileApi = class {
|
|
4620
4865
|
active = 0;
|
|
4621
4866
|
waiting = [];
|
|
4867
|
+
collections;
|
|
4622
4868
|
ctx;
|
|
4623
4869
|
/** Receives public browser ports only. */
|
|
4624
4870
|
constructor(ctx) {
|
|
4625
4871
|
this.ctx = ctx;
|
|
4872
|
+
this.collections = new FileCollections((method, params) => ctx.invoke(method, params), async (work) => {
|
|
4873
|
+
await this.permit(ctx.signal);
|
|
4874
|
+
try {
|
|
4875
|
+
ctx.signal.throwIfAborted();
|
|
4876
|
+
await work();
|
|
4877
|
+
} finally {
|
|
4878
|
+
this.release();
|
|
4879
|
+
}
|
|
4880
|
+
});
|
|
4626
4881
|
}
|
|
4627
4882
|
/** Reads installation identity and the current authenticated resource surface. */
|
|
4628
4883
|
async bootstrap(signal) {
|
|
@@ -4676,6 +4931,11 @@ var FileApi = class {
|
|
|
4676
4931
|
/** Validates owner results once against the sole server/browser business schema. */
|
|
4677
4932
|
async invoke(method, params, signal) {
|
|
4678
4933
|
signal.throwIfAborted();
|
|
4934
|
+
if (method === "stat" || method === "move" || method === "remove" || method === "operationStatus") {
|
|
4935
|
+
const value = await this.collections.enqueue(method, params, signal);
|
|
4936
|
+
signal.throwIfAborted();
|
|
4937
|
+
return results[method].parse(value);
|
|
4938
|
+
}
|
|
4679
4939
|
await this.permit(signal);
|
|
4680
4940
|
try {
|
|
4681
4941
|
signal.throwIfAborted();
|
|
@@ -5033,7 +5293,7 @@ var FileActions = class {
|
|
|
5033
5293
|
this.context.signal
|
|
5034
5294
|
]);
|
|
5035
5295
|
const observed = await this.api.invoke("stat", { target }, signal);
|
|
5036
|
-
const value = kind === "path" ? observed.realPath :
|
|
5296
|
+
const value = kind === "path" ? observed.realPath : observed.entry.uri;
|
|
5037
5297
|
try {
|
|
5038
5298
|
await navigator.clipboard.writeText(value);
|
|
5039
5299
|
signal.throwIfAborted();
|
|
@@ -5182,7 +5442,7 @@ var FileTransfers = class {
|
|
|
5182
5442
|
id: this.ctx.uuid(),
|
|
5183
5443
|
batch,
|
|
5184
5444
|
name: file.name,
|
|
5185
|
-
reference:
|
|
5445
|
+
reference: [directory.path, file.name].filter(Boolean).join("/"),
|
|
5186
5446
|
size: file.size,
|
|
5187
5447
|
modifiedAt: file.lastModified,
|
|
5188
5448
|
direction: "upload",
|
|
@@ -5203,8 +5463,8 @@ var FileTransfers = class {
|
|
|
5203
5463
|
this.jobs.push(...jobs);
|
|
5204
5464
|
this.emit();
|
|
5205
5465
|
(async () => {
|
|
5206
|
-
|
|
5207
|
-
if (job.record.status !== "waiting")
|
|
5466
|
+
await Promise.all(jobs.map(async (job) => {
|
|
5467
|
+
if (job.record.status !== "waiting") return;
|
|
5208
5468
|
try {
|
|
5209
5469
|
job.observed = await this.observe(job);
|
|
5210
5470
|
if (job.observed) this.update(job, {
|
|
@@ -5217,7 +5477,7 @@ var FileTransfers = class {
|
|
|
5217
5477
|
error: errorCode(error)
|
|
5218
5478
|
});
|
|
5219
5479
|
}
|
|
5220
|
-
}
|
|
5480
|
+
}));
|
|
5221
5481
|
for (const job of jobs) job.prepared = true;
|
|
5222
5482
|
if (generation === this.generation) this.pump();
|
|
5223
5483
|
})();
|
|
@@ -5237,8 +5497,8 @@ var FileTransfers = class {
|
|
|
5237
5497
|
* Rejects genuine selection or admission failures without creating a transfer record.
|
|
5238
5498
|
*/
|
|
5239
5499
|
async download(session, descriptor, params, metadata) {
|
|
5240
|
-
const current =
|
|
5241
|
-
const reference = current.success ?
|
|
5500
|
+
const current = FileEntrySchema.safeParse(metadata.metadata);
|
|
5501
|
+
const reference = current.success ? current.data.uri : void 0;
|
|
5242
5502
|
if (!reference) throw Object.assign(/* @__PURE__ */ new Error("download identity missing"), { code: "invalid-params" });
|
|
5243
5503
|
this.capacity(1);
|
|
5244
5504
|
const controller = new AbortController();
|
|
@@ -5263,7 +5523,7 @@ var FileTransfers = class {
|
|
|
5263
5523
|
direction: "download",
|
|
5264
5524
|
status: "waiting",
|
|
5265
5525
|
size: metadata.bytes.contentLength,
|
|
5266
|
-
target:
|
|
5526
|
+
target: FileTargetSchema.safeParse(params && typeof params === "object" && "target" in params ? params.target : void 0).data
|
|
5267
5527
|
},
|
|
5268
5528
|
session,
|
|
5269
5529
|
controller,
|
|
@@ -5434,7 +5694,7 @@ var FileTransfers = class {
|
|
|
5434
5694
|
const path = [target.path.split("/").slice(0, -1).join("/"), valid].filter(Boolean).join("/");
|
|
5435
5695
|
this.update(job, {
|
|
5436
5696
|
name: valid,
|
|
5437
|
-
reference:
|
|
5697
|
+
reference: path,
|
|
5438
5698
|
target: {
|
|
5439
5699
|
...target,
|
|
5440
5700
|
path
|
|
@@ -5511,117 +5771,40 @@ var FileTransfers = class {
|
|
|
5511
5771
|
};
|
|
5512
5772
|
//#endregion
|
|
5513
5773
|
//#region ui-src/viewer.ts
|
|
5514
|
-
/**
|
|
5515
|
-
async function decodeImage(blob, signal) {
|
|
5516
|
-
signal.throwIfAborted();
|
|
5517
|
-
if (typeof createImageBitmap === "function") {
|
|
5518
|
-
const bitmap = await createImageBitmap(blob);
|
|
5519
|
-
try {
|
|
5520
|
-
signal.throwIfAborted();
|
|
5521
|
-
if (bitmap.width > 1e4 || bitmap.height > 1e4) throw Object.assign(/* @__PURE__ */ new Error("image side too large"), { code: "preview-too-large" });
|
|
5522
|
-
} finally {
|
|
5523
|
-
bitmap.close();
|
|
5524
|
-
}
|
|
5525
|
-
return;
|
|
5526
|
-
}
|
|
5527
|
-
const url = URL.createObjectURL(blob);
|
|
5528
|
-
try {
|
|
5529
|
-
await new Promise((resolve, reject) => {
|
|
5530
|
-
const image = new Image();
|
|
5531
|
-
const stop = () => {
|
|
5532
|
-
image.src = "";
|
|
5533
|
-
reject(signal.reason);
|
|
5534
|
-
};
|
|
5535
|
-
const clear = () => {
|
|
5536
|
-
signal.removeEventListener("abort", stop);
|
|
5537
|
-
image.onload = null;
|
|
5538
|
-
image.onerror = null;
|
|
5539
|
-
};
|
|
5540
|
-
signal.addEventListener("abort", stop, { once: true });
|
|
5541
|
-
image.onload = () => {
|
|
5542
|
-
clear();
|
|
5543
|
-
if (image.naturalWidth > 1e4 || image.naturalHeight > 1e4) reject(Object.assign(/* @__PURE__ */ new Error("image side too large"), { code: "preview-too-large" }));
|
|
5544
|
-
else resolve();
|
|
5545
|
-
};
|
|
5546
|
-
image.onerror = () => {
|
|
5547
|
-
clear();
|
|
5548
|
-
reject(Object.assign(/* @__PURE__ */ new Error("image decode failed"), { code: "unsupported-preview" }));
|
|
5549
|
-
};
|
|
5550
|
-
image.src = url;
|
|
5551
|
-
});
|
|
5552
|
-
signal.throwIfAborted();
|
|
5553
|
-
} finally {
|
|
5554
|
-
URL.revokeObjectURL(url);
|
|
5555
|
-
}
|
|
5556
|
-
}
|
|
5557
|
-
var imageTypes = {
|
|
5558
|
-
png: "image/png",
|
|
5559
|
-
jpg: "image/jpeg",
|
|
5560
|
-
jpeg: "image/jpeg",
|
|
5561
|
-
gif: "image/gif",
|
|
5562
|
-
webp: "image/webp",
|
|
5563
|
-
bmp: "image/bmp",
|
|
5564
|
-
avif: "image/avif"
|
|
5565
|
-
};
|
|
5566
|
-
function rasterBytes(bytes, mime) {
|
|
5567
|
-
const ascii = (start, end) => new TextDecoder("ascii").decode(bytes.subarray(start, end));
|
|
5568
|
-
if (!(mime === "image/png" ? [
|
|
5569
|
-
137,
|
|
5570
|
-
80,
|
|
5571
|
-
78,
|
|
5572
|
-
71,
|
|
5573
|
-
13,
|
|
5574
|
-
10,
|
|
5575
|
-
26,
|
|
5576
|
-
10
|
|
5577
|
-
].every((v, i) => bytes[i] === v) : mime === "image/jpeg" ? bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255 : mime === "image/gif" ? ["GIF87a", "GIF89a"].includes(ascii(0, 6)) : mime === "image/webp" ? ascii(0, 4) === "RIFF" && ascii(8, 12) === "WEBP" : mime === "image/bmp" ? ascii(0, 2) === "BM" : mime === "image/avif" ? ascii(4, 8) === "ftyp" && ["avif", "avis"].includes(ascii(8, 12)) : false)) throw Object.assign(/* @__PURE__ */ new Error("raster signature mismatch"), { code: "unsupported-preview" });
|
|
5578
|
-
}
|
|
5579
|
-
var languages = {
|
|
5580
|
-
svg: "xml",
|
|
5581
|
-
html: "html",
|
|
5582
|
-
htm: "html",
|
|
5583
|
-
xml: "xml",
|
|
5584
|
-
json: "json",
|
|
5585
|
-
js: "javascript",
|
|
5586
|
-
ts: "typescript",
|
|
5587
|
-
tsx: "typescript",
|
|
5588
|
-
jsx: "javascript",
|
|
5589
|
-
css: "css",
|
|
5590
|
-
py: "python",
|
|
5591
|
-
sh: "shell",
|
|
5592
|
-
md: "markdown",
|
|
5593
|
-
yaml: "yaml",
|
|
5594
|
-
yml: "yaml",
|
|
5595
|
-
c: "c",
|
|
5596
|
-
h: "c",
|
|
5597
|
-
rs: "rust",
|
|
5598
|
-
go: "go",
|
|
5599
|
-
sql: "sql"
|
|
5600
|
-
};
|
|
5601
|
-
var textExtensions = /* @__PURE__ */ new Set([
|
|
5602
|
-
"txt",
|
|
5603
|
-
"log",
|
|
5604
|
-
"csv",
|
|
5605
|
-
"ini",
|
|
5606
|
-
"conf",
|
|
5607
|
-
...Object.keys(languages)
|
|
5608
|
-
]);
|
|
5609
|
-
/** Owns bounded previews of the optional file-management page selection. */
|
|
5774
|
+
/** Adapts the shared readonly preview to the Files selection lifetime, without a second renderer. */
|
|
5610
5775
|
var FileViewer = class {
|
|
5611
|
-
ctx;
|
|
5612
|
-
decode;
|
|
5613
|
-
controller = new AbortController();
|
|
5614
5776
|
state;
|
|
5615
|
-
url;
|
|
5616
5777
|
listeners = /* @__PURE__ */ new Set();
|
|
5617
|
-
|
|
5618
|
-
|
|
5778
|
+
stop;
|
|
5779
|
+
controller = new AbortController();
|
|
5780
|
+
ctx;
|
|
5781
|
+
/** Reader withdrawal invalidates the sidebar and all its download affordances synchronously. */
|
|
5782
|
+
constructor(ctx) {
|
|
5619
5783
|
this.ctx = ctx;
|
|
5620
|
-
|
|
5784
|
+
const off = ctx.services.subscribe("file-reference-reader", () => {
|
|
5785
|
+
if (!ctx.services.get("file-reference-reader") && this.state) {
|
|
5786
|
+
const href = this.state.href, name = this.state.name;
|
|
5787
|
+
this.close();
|
|
5788
|
+
this.state = {
|
|
5789
|
+
href,
|
|
5790
|
+
name,
|
|
5791
|
+
content: {
|
|
5792
|
+
status: "unavailable",
|
|
5793
|
+
lifecycle: "closed",
|
|
5794
|
+
canDownload: false
|
|
5795
|
+
}
|
|
5796
|
+
};
|
|
5797
|
+
this.emit();
|
|
5798
|
+
}
|
|
5799
|
+
});
|
|
5800
|
+
ctx.signal.addEventListener("abort", () => {
|
|
5801
|
+
off();
|
|
5802
|
+
this.close();
|
|
5803
|
+
}, { once: true });
|
|
5621
5804
|
}
|
|
5622
|
-
/**
|
|
5805
|
+
/** Current immutable sidebar projection. */
|
|
5623
5806
|
get = () => this.state;
|
|
5624
|
-
/** Observes
|
|
5807
|
+
/** Observes shared content progress and selection changes. */
|
|
5625
5808
|
subscribe = (listener) => {
|
|
5626
5809
|
this.listeners.add(listener);
|
|
5627
5810
|
return () => {
|
|
@@ -5631,67 +5814,7 @@ var FileViewer = class {
|
|
|
5631
5814
|
emit() {
|
|
5632
5815
|
for (const listener of this.listeners) listener();
|
|
5633
5816
|
}
|
|
5634
|
-
|
|
5635
|
-
if (!this.state) return;
|
|
5636
|
-
this.state = {
|
|
5637
|
-
...this.state,
|
|
5638
|
-
...value
|
|
5639
|
-
};
|
|
5640
|
-
this.emit();
|
|
5641
|
-
}
|
|
5642
|
-
async source(session, href, signal, selected, expectedFingerprint) {
|
|
5643
|
-
const reader = this.ctx.services.get("file-reference-reader");
|
|
5644
|
-
if (!reader) throw Object.assign(/* @__PURE__ */ new Error("Reference reader unavailable"), { code: "unavailable" });
|
|
5645
|
-
const description = await reader.describe(href, signal);
|
|
5646
|
-
signal.throwIfAborted();
|
|
5647
|
-
const entry = description.source.kind === "workspace-file" ? FileEntrySchema.extend({ realPath: string().optional() }).parse(description.metadata.metadata) : void 0;
|
|
5648
|
-
const stat = entry ? {
|
|
5649
|
-
entry,
|
|
5650
|
-
realPath: entry.realPath ?? ""
|
|
5651
|
-
} : void 0;
|
|
5652
|
-
if (expectedFingerprint && stat?.entry.fingerprint !== expectedFingerprint) throw Object.assign(/* @__PURE__ */ new Error("Selected file changed"), { code: "stale" });
|
|
5653
|
-
const metadata = description.metadata;
|
|
5654
|
-
selected?.({
|
|
5655
|
-
stat,
|
|
5656
|
-
params: description.params,
|
|
5657
|
-
descriptor: description.resource,
|
|
5658
|
-
name: description.name
|
|
5659
|
-
});
|
|
5660
|
-
return {
|
|
5661
|
-
stat,
|
|
5662
|
-
params: description.params,
|
|
5663
|
-
descriptor: description.resource,
|
|
5664
|
-
metadata: entry?.kind === "directory" ? void 0 : metadata,
|
|
5665
|
-
name: description.name
|
|
5666
|
-
};
|
|
5667
|
-
}
|
|
5668
|
-
async collect(descriptor, params, metadata, limit, signal) {
|
|
5669
|
-
if (metadata.bytes.contentLength > limit) throw Object.assign(/* @__PURE__ */ new Error("preview input too large"), { code: "preview-too-large" });
|
|
5670
|
-
const read = await this.ctx.bytes.read(descriptor, params, { signal });
|
|
5671
|
-
try {
|
|
5672
|
-
signal.throwIfAborted();
|
|
5673
|
-
if (read.bytes.contentLength > limit || read.bytes.contentLength !== metadata.bytes.contentLength) throw Object.assign(/* @__PURE__ */ new Error("preview changed"), { code: "preview-too-large" });
|
|
5674
|
-
const chunks = [];
|
|
5675
|
-
let length = 0;
|
|
5676
|
-
for await (const chunk of read.body) {
|
|
5677
|
-
signal.throwIfAborted();
|
|
5678
|
-
length += chunk.byteLength;
|
|
5679
|
-
if (length > limit || length > read.bytes.contentLength) throw Object.assign(/* @__PURE__ */ new Error("preview grew"), { code: "preview-too-large" });
|
|
5680
|
-
chunks.push(chunk);
|
|
5681
|
-
}
|
|
5682
|
-
if (length !== read.bytes.contentLength) throw Object.assign(/* @__PURE__ */ new Error("preview length mismatch"), { code: "io-failed" });
|
|
5683
|
-
const result = new Uint8Array(length);
|
|
5684
|
-
let offset = 0;
|
|
5685
|
-
for (const chunk of chunks) {
|
|
5686
|
-
result.set(chunk, offset);
|
|
5687
|
-
offset += chunk.length;
|
|
5688
|
-
}
|
|
5689
|
-
return result;
|
|
5690
|
-
} finally {
|
|
5691
|
-
await read.close();
|
|
5692
|
-
}
|
|
5693
|
-
}
|
|
5694
|
-
/** Reads bounded content for one reference; an optional listing fingerprint refuses changed sources. */
|
|
5817
|
+
/** Creates its own preview, independently of an already open reader dialog. */
|
|
5695
5818
|
async open(session, href, expectedFingerprint) {
|
|
5696
5819
|
this.close();
|
|
5697
5820
|
this.controller = new AbortController();
|
|
@@ -5700,84 +5823,60 @@ var FileViewer = class {
|
|
|
5700
5823
|
session.signal,
|
|
5701
5824
|
this.controller.signal
|
|
5702
5825
|
]);
|
|
5703
|
-
|
|
5704
|
-
|
|
5705
|
-
|
|
5706
|
-
|
|
5707
|
-
|
|
5708
|
-
|
|
5826
|
+
const reader = this.ctx.services.get("file-reference-reader");
|
|
5827
|
+
if (!reader) {
|
|
5828
|
+
this.state = {
|
|
5829
|
+
href,
|
|
5830
|
+
name: href,
|
|
5831
|
+
content: {
|
|
5832
|
+
status: "unavailable",
|
|
5833
|
+
lifecycle: "closed",
|
|
5834
|
+
canDownload: false
|
|
5835
|
+
}
|
|
5836
|
+
};
|
|
5837
|
+
this.emit();
|
|
5838
|
+
return;
|
|
5839
|
+
}
|
|
5840
|
+
const handle = reader.createPreview({
|
|
5841
|
+
uri: href,
|
|
5842
|
+
expectedFingerprint
|
|
5843
|
+
});
|
|
5844
|
+
const update = () => {
|
|
5845
|
+
if (!signal.aborted) {
|
|
5846
|
+
this.state = {
|
|
5847
|
+
...this.state,
|
|
5848
|
+
href,
|
|
5849
|
+
name: handle.get().metadata?.name ?? href,
|
|
5850
|
+
content: handle.get(),
|
|
5851
|
+
handle,
|
|
5852
|
+
Content: reader.PreviewContent
|
|
5853
|
+
};
|
|
5854
|
+
this.emit();
|
|
5855
|
+
}
|
|
5709
5856
|
};
|
|
5710
|
-
|
|
5857
|
+
update();
|
|
5858
|
+
this.stop = handle.subscribe(update);
|
|
5859
|
+
signal.addEventListener("abort", () => {
|
|
5860
|
+
try {
|
|
5861
|
+
handle.close();
|
|
5862
|
+
} catch {}
|
|
5863
|
+
}, { once: true });
|
|
5711
5864
|
try {
|
|
5712
|
-
const
|
|
5713
|
-
signal.
|
|
5714
|
-
|
|
5715
|
-
|
|
5716
|
-
|
|
5717
|
-
|
|
5718
|
-
|
|
5719
|
-
const extension = source.name.split(".").pop()?.toLowerCase() ?? "";
|
|
5720
|
-
const mediaType = source.metadata.metadata?.mediaType ?? source.metadata.bytes.contentType;
|
|
5721
|
-
const mime = imageTypes[extension] ?? (Object.values(imageTypes).includes(mediaType) ? mediaType : void 0);
|
|
5722
|
-
if (!mime && !textExtensions.has(extension) && source.metadata.bytes.contentLength > 1048576) {
|
|
5723
|
-
this.patch({ content: { status: "unsupported" } });
|
|
5724
|
-
return;
|
|
5725
|
-
}
|
|
5726
|
-
const bytes = await this.collect(source.descriptor, source.params, source.metadata, mime ? 20971520 : 1048576, signal);
|
|
5727
|
-
signal.throwIfAborted();
|
|
5728
|
-
if (mime) {
|
|
5729
|
-
rasterBytes(bytes, mime);
|
|
5730
|
-
const blob = new Blob([bytes], { type: mime });
|
|
5731
|
-
await this.decode(blob, signal);
|
|
5732
|
-
signal.throwIfAborted();
|
|
5733
|
-
this.url = URL.createObjectURL(blob);
|
|
5734
|
-
this.patch({ content: {
|
|
5735
|
-
status: "ready",
|
|
5736
|
-
kind: "image",
|
|
5737
|
-
url: this.url,
|
|
5738
|
-
alt: source.name
|
|
5739
|
-
} });
|
|
5740
|
-
} else {
|
|
5741
|
-
let text;
|
|
5742
|
-
try {
|
|
5743
|
-
text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
5744
|
-
if ([...text].some((character) => character.charCodeAt(0) < 32 && ![
|
|
5745
|
-
"\n",
|
|
5746
|
-
"\r",
|
|
5747
|
-
" "
|
|
5748
|
-
].includes(character))) throw new Error("binary control");
|
|
5749
|
-
} catch {
|
|
5750
|
-
this.patch({ content: { status: "unsupported" } });
|
|
5751
|
-
return;
|
|
5752
|
-
}
|
|
5753
|
-
signal.throwIfAborted();
|
|
5754
|
-
this.patch({ content: {
|
|
5755
|
-
status: "ready",
|
|
5756
|
-
kind: languages[extension] ? "code" : "text",
|
|
5757
|
-
text,
|
|
5758
|
-
...languages[extension] ? { language: languages[extension] } : {}
|
|
5759
|
-
} });
|
|
5865
|
+
const description = await reader.describe(href, signal);
|
|
5866
|
+
if (!signal.aborted && this.state) {
|
|
5867
|
+
this.state = {
|
|
5868
|
+
...this.state,
|
|
5869
|
+
description
|
|
5870
|
+
};
|
|
5871
|
+
this.emit();
|
|
5760
5872
|
}
|
|
5761
|
-
} catch
|
|
5762
|
-
if (!signal.aborted) this.patch({ content: {
|
|
5763
|
-
status: "failed",
|
|
5764
|
-
message: this.ctx.i18n.t(`error.${errorCode(error)}`)
|
|
5765
|
-
} });
|
|
5766
|
-
}
|
|
5767
|
-
}
|
|
5768
|
-
/** Toggles owner image layout without changing the readonly input. */
|
|
5769
|
-
toggleOriginal() {
|
|
5770
|
-
if (this.state) this.patch({ original: !this.state.original });
|
|
5771
|
-
}
|
|
5772
|
-
/** Adjusts image zoom within one quarter to four times original size. */
|
|
5773
|
-
zoom(delta) {
|
|
5774
|
-
if (this.state) this.patch({ zoom: Math.max(.25, Math.min(4, this.state.zoom + delta)) });
|
|
5873
|
+
} catch {}
|
|
5775
5874
|
}
|
|
5776
|
-
/**
|
|
5875
|
+
/** Withdraws selection immediately while the reader retains any pending cleanup. */
|
|
5777
5876
|
close = () => {
|
|
5877
|
+
this.stop?.();
|
|
5878
|
+
this.stop = void 0;
|
|
5778
5879
|
this.controller.abort();
|
|
5779
|
-
if (this.url) URL.revokeObjectURL(this.url);
|
|
5780
|
-
this.url = void 0;
|
|
5781
5880
|
this.state = void 0;
|
|
5782
5881
|
this.emit();
|
|
5783
5882
|
};
|
|
@@ -5865,6 +5964,10 @@ var FileBrowser = class {
|
|
|
5865
5964
|
this.disposeCallbacks.push(ctx.onCore("config.workspaceChanged", () => {
|
|
5866
5965
|
if (this.state.workspaceId) this.syncSelection(true);
|
|
5867
5966
|
}));
|
|
5967
|
+
this.disposeCallbacks.push(ctx.on("changed", (event) => {
|
|
5968
|
+
const changed = FilesChangedSchema.safeParse(event.payload);
|
|
5969
|
+
if (changed.success && this.state.workspaceId && changed.data.workspaces.includes(this.state.workspaceId) && this.state.status === "online") this.refresh();
|
|
5970
|
+
}));
|
|
5868
5971
|
ctx.signal.addEventListener("abort", this.dispose, { once: true });
|
|
5869
5972
|
}
|
|
5870
5973
|
/** Returns a stable immutable snapshot until a visible state change. */
|
|
@@ -7042,6 +7145,7 @@ function FilePreview({ ctx, model, onClose, onDownload, onCopy, downloading = fa
|
|
|
7042
7145
|
const t = ctx.i18n.t;
|
|
7043
7146
|
const selected = browser.selected;
|
|
7044
7147
|
const content = state?.content;
|
|
7148
|
+
const Content = state?.Content;
|
|
7045
7149
|
return /* @__PURE__ */ jsxs("aside", {
|
|
7046
7150
|
className: "fx-preview-pane",
|
|
7047
7151
|
"aria-label": t("preview"),
|
|
@@ -7109,23 +7213,7 @@ function FilePreview({ ctx, model, onClose, onDownload, onCopy, downloading = fa
|
|
|
7109
7213
|
}) : browser.selectedError ? /* @__PURE__ */ jsx("p", {
|
|
7110
7214
|
role: "alert",
|
|
7111
7215
|
children: t(`error.${browser.selectedError}`)
|
|
7112
|
-
}) :
|
|
7113
|
-
className: "fx-preview-image",
|
|
7114
|
-
children: /* @__PURE__ */ jsx("img", {
|
|
7115
|
-
src: content.url,
|
|
7116
|
-
alt: state?.name ?? selected?.name ?? ""
|
|
7117
|
-
})
|
|
7118
|
-
}) : /* @__PURE__ */ jsx("pre", {
|
|
7119
|
-
className: "fx-code-preview",
|
|
7120
|
-
children: content.text.split("\n").map((line, index) => /* @__PURE__ */ jsxs("span", {
|
|
7121
|
-
className: "fx-code-line",
|
|
7122
|
-
children: [/* @__PURE__ */ jsx("span", {
|
|
7123
|
-
className: "fx-line-number",
|
|
7124
|
-
"aria-hidden": "true",
|
|
7125
|
-
children: index + 1
|
|
7126
|
-
}), /* @__PURE__ */ jsx("span", { children: line || "\xA0" })]
|
|
7127
|
-
}, `${index}:${line}`))
|
|
7128
|
-
}) : content?.status === "failed" ? /* @__PURE__ */ jsx("p", {
|
|
7216
|
+
}) : state?.handle && Content ? /* @__PURE__ */ jsx(Content, { handle: state.handle }) : content?.status === "failed" ? /* @__PURE__ */ jsx("p", {
|
|
7129
7217
|
role: "alert",
|
|
7130
7218
|
children: content.message
|
|
7131
7219
|
}) : content?.status === "loading" ? /* @__PURE__ */ jsx("p", {
|
|
@@ -7133,7 +7221,7 @@ function FilePreview({ ctx, model, onClose, onDownload, onCopy, downloading = fa
|
|
|
7133
7221
|
children: t("loading")
|
|
7134
7222
|
}) : /* @__PURE__ */ jsxs("div", {
|
|
7135
7223
|
className: "fx-preview-placeholder",
|
|
7136
|
-
children: [/* @__PURE__ */ jsx(Icon, { name: "file-text" }), /* @__PURE__ */ jsx("span", { children: t(selected ? "preview-unsupported" : "select-preview") })]
|
|
7224
|
+
children: [/* @__PURE__ */ jsx(Icon, { name: "file-text" }), /* @__PURE__ */ jsx("span", { children: t(content?.status === "unavailable" ? "unavailable" : selected ? "preview-unsupported" : "select-preview") })]
|
|
7137
7225
|
})
|
|
7138
7226
|
})
|
|
7139
7227
|
}),
|
|
@@ -7152,8 +7240,8 @@ function FilePreview({ ctx, model, onClose, onDownload, onCopy, downloading = fa
|
|
|
7152
7240
|
/* @__PURE__ */ jsx("dd", { children: fileSize(selected.size) }),
|
|
7153
7241
|
/* @__PURE__ */ jsx("dt", { children: t("modified") }),
|
|
7154
7242
|
/* @__PURE__ */ jsx("dd", { children: new Date(selected.modifiedAt).toLocaleString() }),
|
|
7155
|
-
state?.metadata && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("dt", { children: t("body-size") }), /* @__PURE__ */ jsx("dd", { children: fileSize(state.metadata.bytes.contentLength) })] }),
|
|
7156
|
-
state?.
|
|
7243
|
+
state?.description?.metadata && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("dt", { children: t("body-size") }), /* @__PURE__ */ jsx("dd", { children: fileSize(state.description.metadata.bytes.contentLength) })] }),
|
|
7244
|
+
state?.description?.source.kind === "gateway-file" && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("dt", { children: t("host-path") }), /* @__PURE__ */ jsx("dd", { children: state.description.source.absolutePath })] }),
|
|
7157
7245
|
/* @__PURE__ */ jsx("dt", { children: t("uri") }),
|
|
7158
7246
|
/* @__PURE__ */ jsx("dd", { children: selected.uri })
|
|
7159
7247
|
]
|
|
@@ -7171,7 +7259,7 @@ function FilePreview({ ctx, model, onClose, onDownload, onCopy, downloading = fa
|
|
|
7171
7259
|
children: [/* @__PURE__ */ jsxs(Button, {
|
|
7172
7260
|
size: "sm",
|
|
7173
7261
|
loading: downloading,
|
|
7174
|
-
disabled: downloading || !selected || selected.kind === "directory" || !browser.selectedMetadata,
|
|
7262
|
+
disabled: downloading || !selected || selected.kind === "directory" || !browser.selectedMetadata || !content?.canDownload,
|
|
7175
7263
|
onClick: onDownload,
|
|
7176
7264
|
children: [/* @__PURE__ */ jsx(Icon, { name: "download" }), t("download")]
|
|
7177
7265
|
}), /* @__PURE__ */ jsxs(Button, {
|
|
@@ -8430,19 +8518,31 @@ function registerFiles(ctx) {
|
|
|
8430
8518
|
ctx.styles.add(styles);
|
|
8431
8519
|
const model = new FileBrowser(ctx);
|
|
8432
8520
|
ctx.services.provide("file-reference-actions", { actions(description) {
|
|
8433
|
-
if (description.source.kind !== "
|
|
8521
|
+
if (description.source.kind !== "gateway-file") return [];
|
|
8434
8522
|
const source = description.source;
|
|
8435
|
-
const
|
|
8523
|
+
const state = model.get();
|
|
8524
|
+
if (!state.workspaceId) return [];
|
|
8525
|
+
const separator = source.pathStyle === "windows" ? /[\\/]/ : /\//;
|
|
8526
|
+
const parts = source.absolutePath.split(separator);
|
|
8527
|
+
if (parts.some((part) => part === "." || part === "..")) return [];
|
|
8528
|
+
const root = state.roots.find((item) => {
|
|
8529
|
+
const rootParts = item.path.split(separator).filter(Boolean);
|
|
8530
|
+
const pathParts = parts.filter(Boolean);
|
|
8531
|
+
return rootParts.every((part, index) => pathParts[index] === part);
|
|
8532
|
+
});
|
|
8533
|
+
if (!root) return [];
|
|
8534
|
+
const relative = parts.filter(Boolean).slice(root.path.split(separator).filter(Boolean).length);
|
|
8535
|
+
const target = {
|
|
8536
|
+
workspaceId: state.workspaceId,
|
|
8537
|
+
rootId: root.id,
|
|
8538
|
+
path: relative.slice(0, -1).join("/")
|
|
8539
|
+
};
|
|
8436
8540
|
return [{
|
|
8437
8541
|
id: "locate",
|
|
8438
8542
|
label: ctx.i18n.t("locate"),
|
|
8439
8543
|
run: () => {
|
|
8440
8544
|
if (ctx.signal.aborted || ctx.navigation.openPage("files").status !== "opened") return;
|
|
8441
|
-
model.locate(
|
|
8442
|
-
workspaceId: source.workspaceId,
|
|
8443
|
-
rootId: source.rootId,
|
|
8444
|
-
path: kind === "directory" ? source.path : source.path.split("/").slice(0, -1).join("/")
|
|
8445
|
-
});
|
|
8545
|
+
model.locate(target);
|
|
8446
8546
|
}
|
|
8447
8547
|
}];
|
|
8448
8548
|
} });
|