@orion-agents/orion-code 0.3.19 → 0.3.21
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/CHANGELOG.md +65 -3
- package/README.md +5 -7
- package/README.zh-CN.md +4 -5
- package/dist/runtime/release-receipts.d.ts +19 -2
- package/dist/runtime/release-receipts.js +58 -6
- package/dist/services/workspace-roots-registry.d.ts +46 -0
- package/dist/services/workspace-roots-registry.js +199 -0
- package/dist/web/protocol.d.ts +36 -0
- package/dist/web/server.js +45 -0
- package/dist/web/workbench-controller.d.ts +22 -1
- package/dist/web/workbench-controller.js +107 -0
- package/dist/web/workspace-discovery.d.ts +92 -0
- package/dist/web/workspace-discovery.js +324 -0
- package/dist/web-client/assets/{FilesPanel-DGWgLv6R.js → FilesPanel-CKeXPofZ.js} +1 -1
- package/dist/web-client/assets/{GitPanel-DIuJNXoB.js → GitPanel-DollPxv0.js} +1 -1
- package/dist/web-client/assets/{ReviewPanel-YWZXJdm5.js → ReviewPanel-CNADacIn.js} +1 -1
- package/dist/web-client/assets/{TerminalPanel-DPSMepio.js → TerminalPanel-Cyns4uJt.js} +1 -1
- package/dist/web-client/assets/index-BhGhORAX.js +20 -0
- package/dist/web-client/assets/{index--hnGXXjj.css → index-DCYKIeOj.css} +1 -1
- package/dist/web-client/assets/{useAutoNotice-h7c0Iz1E.js → useAutoNotice-BLADeFFj.js} +1 -1
- package/dist/web-client/index.html +2 -2
- package/npm-shrinkwrap.json +27 -27
- package/package.json +1 -1
- package/dist/web-client/assets/index-DYeu_rs7.js +0 -20
|
@@ -6,8 +6,10 @@ import { WorkspaceRegistryV1 } from '../services/workspace-registry';
|
|
|
6
6
|
import { WebEventHub } from './event-hub';
|
|
7
7
|
import { FileReadServiceV1 } from './file-read-service';
|
|
8
8
|
import { GitReadModelServiceV1 } from './git-read-model-service';
|
|
9
|
-
import { type WebComposerActionResultV1, type WebComposerActionV1, type WebComposerControlStateV1, type WebBootstrapV1, type WebCommandResultV1, type WebContextActivateRequestV1, type WebContextGuardV1, type WebMcpServerSummaryV1, type WebPageV1, type WebSessionSnapshotV1, type WebSessionSummaryV1, type WebSettingsMutationResultV1, type WebSettingsSnapshotV1, type WebSettingsUpdateRequestV1, type WebSkillSummaryV1, type WebToolDetailPageV1, type WebToolDetailSummaryV1, type WebWorkspaceSummaryV1, type WebWorkspaceProjectSummaryV1, type WebWorkspaceCandidateSourceV1, type WebWorkspaceCandidateV1 } from './protocol';
|
|
9
|
+
import { type WebComposerActionResultV1, type WebComposerActionV1, type WebComposerControlStateV1, type WebBootstrapV1, type WebCommandResultV1, type WebContextActivateRequestV1, type WebContextGuardV1, type WebMcpServerSummaryV1, type WebPageV1, type WebSessionSnapshotV1, type WebSessionSummaryV1, type WebSettingsMutationResultV1, type WebSettingsSnapshotV1, type WebSettingsUpdateRequestV1, type WebSkillSummaryV1, type WebToolDetailPageV1, type WebToolDetailSummaryV1, type WebWorkspaceSummaryV1, type WebWorkspaceProjectSummaryV1, type WebWorkspaceCandidateSourceV1, type WebWorkspaceCandidateV1, type WebWorkspaceRootViewV1, type WebWorkspaceRootDiscoverOutcomeV1 } from './protocol';
|
|
10
10
|
import { type NativeDirectoryPickResult, type NativeDirectoryPicker } from './native-directory-picker';
|
|
11
|
+
import { WorkspaceDiscoveryTelemetryV1 } from './workspace-discovery';
|
|
12
|
+
import { WorkspaceRootsRegistryV1 } from '../services/workspace-roots-registry';
|
|
11
13
|
import { type WebSessionRuntimeSummaryV1 } from './session-runtime-registry';
|
|
12
14
|
import { TerminalManagerV1 } from './terminal-manager';
|
|
13
15
|
export { WebWorkbenchError } from './errors';
|
|
@@ -17,6 +19,10 @@ export interface WebWorkbenchControllerOptions {
|
|
|
17
19
|
readonly createRuntime?: (cwd: string) => Promise<OrionCodeUiRuntime>;
|
|
18
20
|
readonly createSessionRuntime?: (cwd: string) => Promise<OrionCodeUiRuntime>;
|
|
19
21
|
readonly workspaceRegistry?: WorkspaceRegistryV1;
|
|
22
|
+
/** v0.3.21 — user-authorized scan boundaries; injectable for tests. */
|
|
23
|
+
readonly workspaceRootsRegistry?: WorkspaceRootsRegistryV1;
|
|
24
|
+
/** v0.3.21 — injectable discovery telemetry for tests. */
|
|
25
|
+
readonly discoveryTelemetry?: WorkspaceDiscoveryTelemetryV1;
|
|
20
26
|
/** v0.3.16 — Host native directory picker; injectable for tests. */
|
|
21
27
|
readonly nativeDirectoryPicker?: NativeDirectoryPicker;
|
|
22
28
|
}
|
|
@@ -44,6 +50,14 @@ export declare class WebWorkbenchController {
|
|
|
44
50
|
private pickerInFlight;
|
|
45
51
|
/** v0.3.17 — bounded, path-free open-path traces exposed through diagnostics(). */
|
|
46
52
|
private readonly openTelemetry;
|
|
53
|
+
private readonly workspaceRootsRegistry;
|
|
54
|
+
private readonly discoveryTelemetry;
|
|
55
|
+
private readonly discoveryService;
|
|
56
|
+
/**
|
|
57
|
+
* v0.3.21 — global discovery single-flight: at most one scan runs per Host;
|
|
58
|
+
* a new discover request aborts the previous one (scan_cancelled) and wins.
|
|
59
|
+
*/
|
|
60
|
+
private readonly discoveryInflight;
|
|
47
61
|
readonly terminalManager: TerminalManagerV1;
|
|
48
62
|
private readonly mutationResults;
|
|
49
63
|
private readonly sessionViews;
|
|
@@ -111,6 +125,13 @@ export declare class WebWorkbenchController {
|
|
|
111
125
|
* file contents, or touches the session catalog beyond a warm-cache peek.
|
|
112
126
|
*/
|
|
113
127
|
inspectWorkspaceFast(path: string, source?: WebWorkspaceCandidateSourceV1, requestId?: string): Promise<WebWorkspaceCandidateV1>;
|
|
128
|
+
listWorkspaceRoots(): readonly WebWorkspaceRootViewV1[];
|
|
129
|
+
addWorkspaceRoot(path: string, requestId?: string): Promise<WebWorkspaceRootViewV1>;
|
|
130
|
+
removeWorkspaceRoot(rootId: string): boolean;
|
|
131
|
+
discoverWorkspaceRoot(rootId: string, requestId?: string, options?: {
|
|
132
|
+
readonly refresh?: boolean;
|
|
133
|
+
}): Promise<WebWorkspaceRootDiscoverOutcomeV1>;
|
|
134
|
+
private toDiscoverOutcome;
|
|
114
135
|
listWorkspaceSessions(workspaceId: string, context?: WebContextGuardV1): readonly WebSessionSummaryV1[];
|
|
115
136
|
/** Archived sessions of a Workspace (v0.3.7), newest archive first. */
|
|
116
137
|
listArchivedWorkspaceSessions(workspaceId: string, context?: WebContextGuardV1): readonly WebSessionSummaryV1[];
|
|
@@ -34,6 +34,8 @@ const workspace_mutation_arbiter_1 = require("./workspace-mutation-arbiter");
|
|
|
34
34
|
const protocol_1 = require("./protocol");
|
|
35
35
|
const native_directory_picker_1 = require("./native-directory-picker");
|
|
36
36
|
const workspace_open_telemetry_1 = require("./workspace-open-telemetry");
|
|
37
|
+
const workspace_discovery_1 = require("./workspace-discovery");
|
|
38
|
+
const workspace_roots_registry_1 = require("../services/workspace-roots-registry");
|
|
37
39
|
const review_service_1 = require("./review-service");
|
|
38
40
|
const session_runtime_registry_1 = require("./session-runtime-registry");
|
|
39
41
|
const terminal_manager_1 = require("./terminal-manager");
|
|
@@ -58,6 +60,11 @@ class WebWorkbenchController {
|
|
|
58
60
|
this.pickerInFlight = false;
|
|
59
61
|
/** v0.3.17 — bounded, path-free open-path traces exposed through diagnostics(). */
|
|
60
62
|
this.openTelemetry = new workspace_open_telemetry_1.WorkspaceOpenTelemetryV1();
|
|
63
|
+
/**
|
|
64
|
+
* v0.3.21 — global discovery single-flight: at most one scan runs per Host;
|
|
65
|
+
* a new discover request aborts the previous one (scan_cancelled) and wins.
|
|
66
|
+
*/
|
|
67
|
+
this.discoveryInflight = new Map();
|
|
61
68
|
this.mutationResults = new Map();
|
|
62
69
|
this.sessionViews = new Map();
|
|
63
70
|
this.sessionViewBytes = 0;
|
|
@@ -93,6 +100,11 @@ class WebWorkbenchController {
|
|
|
93
100
|
onStateChanged: state => this.emitWorkspaceMutationState(state),
|
|
94
101
|
});
|
|
95
102
|
this.workspaceRegistry = options.workspaceRegistry ?? new workspace_registry_1.WorkspaceRegistryV1();
|
|
103
|
+
this.workspaceRootsRegistry = options.workspaceRootsRegistry ?? new workspace_roots_registry_1.WorkspaceRootsRegistryV1();
|
|
104
|
+
this.discoveryTelemetry = options.discoveryTelemetry ?? new workspace_discovery_1.WorkspaceDiscoveryTelemetryV1();
|
|
105
|
+
this.discoveryService = new workspace_discovery_1.WorkspaceDiscoveryServiceV1({
|
|
106
|
+
telemetry: this.discoveryTelemetry,
|
|
107
|
+
});
|
|
96
108
|
this.directoryPicker = options.nativeDirectoryPicker ?? (0, native_directory_picker_1.createNativeDirectoryPicker)();
|
|
97
109
|
this.terminalManager = new terminal_manager_1.TerminalManagerV1({
|
|
98
110
|
resolveWorkspace: workspaceId => this.workspaceRegistry.find(workspaceId)?.canonicalPath,
|
|
@@ -317,6 +329,98 @@ class WebWorkbenchController {
|
|
|
317
329
|
}
|
|
318
330
|
return candidate;
|
|
319
331
|
}
|
|
332
|
+
// ---------------------------------------------------------------------
|
|
333
|
+
// v0.3.21 — user-authorized project roots and bounded discovery.
|
|
334
|
+
//
|
|
335
|
+
// Roots are scan boundaries, not workspaces: nothing here registers a
|
|
336
|
+
// workspace, activates a Context, installs a Runtime, or reads file
|
|
337
|
+
// contents. A discovered candidate is a hint; the client must still run the
|
|
338
|
+
// existing POST /workspaces/inspect → confirm → activate flow.
|
|
339
|
+
// ---------------------------------------------------------------------
|
|
340
|
+
listWorkspaceRoots() {
|
|
341
|
+
return this.workspaceRootsRegistry.list().map(entry => {
|
|
342
|
+
const cached = this.discoveryService.cached(entry.id);
|
|
343
|
+
return Object.freeze({
|
|
344
|
+
...entry,
|
|
345
|
+
discovery: cached
|
|
346
|
+
? Object.freeze({
|
|
347
|
+
status: cached.status,
|
|
348
|
+
candidateCount: cached.candidates.length,
|
|
349
|
+
durationMs: cached.durationMs,
|
|
350
|
+
...(cached.errorCode ? { errorCode: cached.errorCode } : {}),
|
|
351
|
+
cached: true,
|
|
352
|
+
})
|
|
353
|
+
: null,
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
async addWorkspaceRoot(path, requestId) {
|
|
358
|
+
if (this.closed) {
|
|
359
|
+
throw new errors_1.WebWorkbenchError(503, 'The local Web Host is shutting down.', 'host_closed');
|
|
360
|
+
}
|
|
361
|
+
const entry = this.workspaceRootsRegistry.add(path);
|
|
362
|
+
void requestId;
|
|
363
|
+
const view = this.listWorkspaceRoots().find(root => root.id === entry.id);
|
|
364
|
+
if (!view)
|
|
365
|
+
throw new errors_1.WebWorkbenchError(500, 'Workspace root vanished after add.', 'scan_failed');
|
|
366
|
+
return view;
|
|
367
|
+
}
|
|
368
|
+
removeWorkspaceRoot(rootId) {
|
|
369
|
+
// Cancelling first: a removed root must never receive a late scan result.
|
|
370
|
+
const inflight = this.discoveryInflight.get(rootId);
|
|
371
|
+
if (inflight) {
|
|
372
|
+
inflight.controller.abort();
|
|
373
|
+
this.discoveryInflight.delete(rootId);
|
|
374
|
+
}
|
|
375
|
+
this.discoveryService.invalidate(rootId);
|
|
376
|
+
return this.workspaceRootsRegistry.remove(rootId);
|
|
377
|
+
}
|
|
378
|
+
async discoverWorkspaceRoot(rootId, requestId, options = {}) {
|
|
379
|
+
if (this.closed) {
|
|
380
|
+
throw new errors_1.WebWorkbenchError(503, 'The local Web Host is shutting down.', 'host_closed');
|
|
381
|
+
}
|
|
382
|
+
const entry = this.workspaceRootsRegistry.find(rootId);
|
|
383
|
+
if (!entry) {
|
|
384
|
+
throw new errors_1.WebWorkbenchError(404, 'Workspace root is not registered.', 'root_missing');
|
|
385
|
+
}
|
|
386
|
+
// Global single-flight: the newest discover request wins; the previous
|
|
387
|
+
// scan is aborted and reports scan_cancelled.
|
|
388
|
+
for (const [id, inflight] of this.discoveryInflight) {
|
|
389
|
+
inflight.controller.abort();
|
|
390
|
+
this.discoveryInflight.delete(id);
|
|
391
|
+
}
|
|
392
|
+
if (!options.refresh) {
|
|
393
|
+
const cached = this.discoveryService.cached(rootId);
|
|
394
|
+
if (cached)
|
|
395
|
+
return this.toDiscoverOutcome(cached, entry.lastScannedAt, requestId);
|
|
396
|
+
}
|
|
397
|
+
const controller = new AbortController();
|
|
398
|
+
const promise = this.discoveryService.discover({ root: entry, signal: controller.signal });
|
|
399
|
+
this.discoveryInflight.set(rootId, { controller, promise });
|
|
400
|
+
let outcome;
|
|
401
|
+
try {
|
|
402
|
+
outcome = await promise;
|
|
403
|
+
}
|
|
404
|
+
finally {
|
|
405
|
+
const current = this.discoveryInflight.get(rootId);
|
|
406
|
+
if (current && current.promise === promise)
|
|
407
|
+
this.discoveryInflight.delete(rootId);
|
|
408
|
+
}
|
|
409
|
+
const scanned = this.workspaceRootsRegistry.markScanned(rootId);
|
|
410
|
+
return this.toDiscoverOutcome(outcome, scanned?.lastScannedAt, requestId);
|
|
411
|
+
}
|
|
412
|
+
toDiscoverOutcome(outcome, lastScannedAt, requestId) {
|
|
413
|
+
return Object.freeze({
|
|
414
|
+
...(requestId ? { requestId } : {}),
|
|
415
|
+
rootId: outcome.rootId,
|
|
416
|
+
status: outcome.status,
|
|
417
|
+
candidates: outcome.candidates,
|
|
418
|
+
...(outcome.errorCode ? { errorCode: outcome.errorCode } : {}),
|
|
419
|
+
durationMs: outcome.durationMs,
|
|
420
|
+
scannedEntries: outcome.scannedEntries,
|
|
421
|
+
...(lastScannedAt ? { lastScannedAt } : {}),
|
|
422
|
+
});
|
|
423
|
+
}
|
|
320
424
|
listWorkspaceSessions(workspaceId, context) {
|
|
321
425
|
if (context)
|
|
322
426
|
this.assertContextGuard(context);
|
|
@@ -1766,6 +1870,9 @@ class WebWorkbenchController {
|
|
|
1766
1870
|
workspaceKernel: this.runtimeValue.workspaceRuntimeKernel?.diagnostics() ?? null,
|
|
1767
1871
|
// v0.3.17 — bounded, path-free open-path traces (picker / inspect / activate).
|
|
1768
1872
|
workspaceOpen: this.openTelemetry.snapshot(),
|
|
1873
|
+
// v0.3.21 — bounded, path-free discovery traces (scan duration, counts,
|
|
1874
|
+
// error codes). Never a root path, a label, a marker name, or content.
|
|
1875
|
+
workspaceDiscovery: this.discoveryTelemetry.snapshot(),
|
|
1769
1876
|
session: {
|
|
1770
1877
|
...this.sessionActivity,
|
|
1771
1878
|
actors: this.sessionRegistry.stats(),
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { WorkspaceRootEntryV1 } from '../services/workspace-roots-registry';
|
|
2
|
+
/** Scan bounds. Centralized so tests can inject exact values. */
|
|
3
|
+
export interface WorkspaceDiscoveryLimitsV1 {
|
|
4
|
+
/** How deep below the root the scan walks (the root itself is depth 0). */
|
|
5
|
+
readonly maxDepth: number;
|
|
6
|
+
/** How many candidates may be reported. */
|
|
7
|
+
readonly maxCandidates: number;
|
|
8
|
+
/** How many directory entries may be inspected at all. */
|
|
9
|
+
readonly maxEntries: number;
|
|
10
|
+
/** Wall-clock budget for one scan. */
|
|
11
|
+
readonly budgetMs: number;
|
|
12
|
+
/** How long a cached scan result may be reused. */
|
|
13
|
+
readonly cacheTtlMs: number;
|
|
14
|
+
}
|
|
15
|
+
export declare const WORKSPACE_DISCOVERY_LIMITS_V1: WorkspaceDiscoveryLimitsV1;
|
|
16
|
+
export type WorkspaceDiscoveryHint = 'git' | 'manifest';
|
|
17
|
+
export interface WorkspaceDiscoveryCandidateV1 {
|
|
18
|
+
readonly rootId: string;
|
|
19
|
+
/** Root-relative POSIX-style path ('' for the root itself). */
|
|
20
|
+
readonly relativePath: string;
|
|
21
|
+
readonly label: string;
|
|
22
|
+
readonly hint: WorkspaceDiscoveryHint;
|
|
23
|
+
/** 'partial' when the scan stopped early (budget exhausted). */
|
|
24
|
+
readonly status: 'ready' | 'partial';
|
|
25
|
+
}
|
|
26
|
+
export type WorkspaceDiscoveryErrorCode = 'root_missing' | 'root_unreadable' | 'budget_exceeded' | 'scan_cancelled' | 'scan_failed';
|
|
27
|
+
export interface WorkspaceDiscoveryOutcomeV1 {
|
|
28
|
+
readonly rootId: string;
|
|
29
|
+
readonly candidates: readonly WorkspaceDiscoveryCandidateV1[];
|
|
30
|
+
readonly status: 'ready' | 'partial';
|
|
31
|
+
readonly errorCode?: WorkspaceDiscoveryErrorCode;
|
|
32
|
+
readonly durationMs: number;
|
|
33
|
+
readonly scannedEntries: number;
|
|
34
|
+
}
|
|
35
|
+
export interface WorkspaceDiscoveryOptions {
|
|
36
|
+
readonly root: WorkspaceRootEntryV1;
|
|
37
|
+
readonly signal?: AbortSignal;
|
|
38
|
+
readonly now?: () => number;
|
|
39
|
+
readonly limits?: Partial<WorkspaceDiscoveryLimitsV1>;
|
|
40
|
+
}
|
|
41
|
+
export type WorkspaceDiscoveryOutcomeCode = 'success' | 'partial' | 'cancelled' | 'failed';
|
|
42
|
+
export interface WorkspaceDiscoveryTraceV1 {
|
|
43
|
+
readonly operation: 'discover';
|
|
44
|
+
readonly outcome: WorkspaceDiscoveryOutcomeCode;
|
|
45
|
+
readonly durationMs: number;
|
|
46
|
+
readonly candidateCount: number;
|
|
47
|
+
readonly scannedEntries: number;
|
|
48
|
+
readonly partial: boolean;
|
|
49
|
+
readonly errorCode?: WorkspaceDiscoveryErrorCode;
|
|
50
|
+
}
|
|
51
|
+
export interface WorkspaceDiscoveryTelemetrySnapshotV1 {
|
|
52
|
+
readonly retained: number;
|
|
53
|
+
readonly limit: number;
|
|
54
|
+
readonly recent: readonly WorkspaceDiscoveryTraceV1[];
|
|
55
|
+
}
|
|
56
|
+
export declare class WorkspaceDiscoveryTelemetryV1 {
|
|
57
|
+
readonly options: {
|
|
58
|
+
limit?: number;
|
|
59
|
+
};
|
|
60
|
+
private readonly limit;
|
|
61
|
+
private readonly entries;
|
|
62
|
+
constructor(options?: {
|
|
63
|
+
limit?: number;
|
|
64
|
+
});
|
|
65
|
+
record(trace: WorkspaceDiscoveryTraceV1): void;
|
|
66
|
+
snapshot(): WorkspaceDiscoveryTelemetrySnapshotV1;
|
|
67
|
+
}
|
|
68
|
+
export interface WorkspaceDiscoveryServiceOptions {
|
|
69
|
+
readonly telemetry?: WorkspaceDiscoveryTelemetryV1;
|
|
70
|
+
readonly now?: () => number;
|
|
71
|
+
readonly cacheTtlMs?: number;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Owns discovery runs: one bounded scan per call, a per-root result cache with
|
|
75
|
+
* a short TTL, and sanitized telemetry. The service holds no Context or
|
|
76
|
+
* Runtime authority; callers must still inspect a candidate before activating
|
|
77
|
+
* it.
|
|
78
|
+
*/
|
|
79
|
+
export declare class WorkspaceDiscoveryServiceV1 {
|
|
80
|
+
private readonly telemetry?;
|
|
81
|
+
private readonly now;
|
|
82
|
+
private readonly cacheTtlMs;
|
|
83
|
+
private readonly cache;
|
|
84
|
+
constructor(options?: WorkspaceDiscoveryServiceOptions);
|
|
85
|
+
/** A cached outcome younger than the TTL, or undefined. Never a truth. */
|
|
86
|
+
cached(rootId: string): WorkspaceDiscoveryOutcomeV1 | undefined;
|
|
87
|
+
/** Drop the cached result for one root or for every root. */
|
|
88
|
+
invalidate(rootId?: string): void;
|
|
89
|
+
discover(options: WorkspaceDiscoveryOptions): Promise<WorkspaceDiscoveryOutcomeV1>;
|
|
90
|
+
private scan;
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=workspace-discovery.d.ts.map
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WorkspaceDiscoveryServiceV1 = exports.WorkspaceDiscoveryTelemetryV1 = exports.WORKSPACE_DISCOVERY_LIMITS_V1 = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* v0.3.21 — bounded, cancellable project discovery inside a user-authorized
|
|
6
|
+
* root directory.
|
|
7
|
+
*
|
|
8
|
+
* Discovery is a read-only hint, never a truth: it enumerates directory entries
|
|
9
|
+
* and checks for the existence of a small, fixed set of project markers, and it
|
|
10
|
+
* never reads file contents, follows symlinks, leaves the root, registers a
|
|
11
|
+
* workspace, or activates a Context. Every scan is bounded by depth, directory
|
|
12
|
+
* entries, candidates and a wall-clock budget; when a budget is exhausted the
|
|
13
|
+
* scan returns the candidates found so far with `status: 'partial'` instead of
|
|
14
|
+
* pretending the result is complete.
|
|
15
|
+
*
|
|
16
|
+
* Telemetry is sanitized by construction: traces carry operation, outcome,
|
|
17
|
+
* duration, counts and error codes only — never a path, a label, a marker name
|
|
18
|
+
* or file content.
|
|
19
|
+
*/
|
|
20
|
+
const promises_1 = require("fs/promises");
|
|
21
|
+
const path_1 = require("path");
|
|
22
|
+
exports.WORKSPACE_DISCOVERY_LIMITS_V1 = Object.freeze({
|
|
23
|
+
maxDepth: 2,
|
|
24
|
+
maxCandidates: 200,
|
|
25
|
+
maxEntries: 500,
|
|
26
|
+
budgetMs: 750,
|
|
27
|
+
cacheTtlMs: 60000,
|
|
28
|
+
});
|
|
29
|
+
class WorkspaceDiscoveryTelemetryV1 {
|
|
30
|
+
constructor(options = {}) {
|
|
31
|
+
this.options = options;
|
|
32
|
+
this.entries = [];
|
|
33
|
+
this.limit = Math.max(1, options.limit ?? 50);
|
|
34
|
+
}
|
|
35
|
+
record(trace) {
|
|
36
|
+
this.entries.push(trace);
|
|
37
|
+
if (this.entries.length > this.limit) {
|
|
38
|
+
this.entries.splice(0, this.entries.length - this.limit);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
snapshot() {
|
|
42
|
+
return Object.freeze({
|
|
43
|
+
retained: this.entries.length,
|
|
44
|
+
limit: this.limit,
|
|
45
|
+
recent: Object.freeze([...this.entries]),
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
exports.WorkspaceDiscoveryTelemetryV1 = WorkspaceDiscoveryTelemetryV1;
|
|
50
|
+
const IGNORED_DIRECTORY_NAMES = new Set([
|
|
51
|
+
'node_modules',
|
|
52
|
+
'dist',
|
|
53
|
+
'build',
|
|
54
|
+
'out',
|
|
55
|
+
'.git',
|
|
56
|
+
'cache',
|
|
57
|
+
'Cache',
|
|
58
|
+
'.cache',
|
|
59
|
+
'.venv',
|
|
60
|
+
'venv',
|
|
61
|
+
'target',
|
|
62
|
+
'vendor',
|
|
63
|
+
'__pycache__',
|
|
64
|
+
]);
|
|
65
|
+
const PROJECT_MARKERS = new Map([
|
|
66
|
+
['.git', 'git'],
|
|
67
|
+
['package.json', 'manifest'],
|
|
68
|
+
['pyproject.toml', 'manifest'],
|
|
69
|
+
['go.mod', 'manifest'],
|
|
70
|
+
['Cargo.toml', 'manifest'],
|
|
71
|
+
['pom.xml', 'manifest'],
|
|
72
|
+
]);
|
|
73
|
+
/**
|
|
74
|
+
* Owns discovery runs: one bounded scan per call, a per-root result cache with
|
|
75
|
+
* a short TTL, and sanitized telemetry. The service holds no Context or
|
|
76
|
+
* Runtime authority; callers must still inspect a candidate before activating
|
|
77
|
+
* it.
|
|
78
|
+
*/
|
|
79
|
+
class WorkspaceDiscoveryServiceV1 {
|
|
80
|
+
constructor(options = {}) {
|
|
81
|
+
this.cache = new Map();
|
|
82
|
+
this.telemetry = options.telemetry;
|
|
83
|
+
this.now = options.now ?? (() => performance.now());
|
|
84
|
+
this.cacheTtlMs = options.cacheTtlMs ?? exports.WORKSPACE_DISCOVERY_LIMITS_V1.cacheTtlMs;
|
|
85
|
+
}
|
|
86
|
+
/** A cached outcome younger than the TTL, or undefined. Never a truth. */
|
|
87
|
+
cached(rootId) {
|
|
88
|
+
const entry = this.cache.get(rootId);
|
|
89
|
+
if (!entry)
|
|
90
|
+
return undefined;
|
|
91
|
+
if (this.now() - entry.recordedAt > this.cacheTtlMs) {
|
|
92
|
+
this.cache.delete(rootId);
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
return entry.outcome;
|
|
96
|
+
}
|
|
97
|
+
/** Drop the cached result for one root or for every root. */
|
|
98
|
+
invalidate(rootId) {
|
|
99
|
+
if (rootId === undefined)
|
|
100
|
+
this.cache.clear();
|
|
101
|
+
else
|
|
102
|
+
this.cache.delete(rootId);
|
|
103
|
+
}
|
|
104
|
+
async discover(options) {
|
|
105
|
+
const startedAt = this.now();
|
|
106
|
+
const { root, signal } = options;
|
|
107
|
+
const limits = { ...exports.WORKSPACE_DISCOVERY_LIMITS_V1, ...options.limits };
|
|
108
|
+
try {
|
|
109
|
+
const outcome = await this.scan(root, signal, limits, startedAt);
|
|
110
|
+
this.cache.set(root.id, { outcome, recordedAt: this.now() });
|
|
111
|
+
this.telemetry?.record({
|
|
112
|
+
operation: 'discover',
|
|
113
|
+
outcome: outcome.status === 'ready' ? 'success' : 'partial',
|
|
114
|
+
durationMs: outcome.durationMs,
|
|
115
|
+
candidateCount: outcome.candidates.length,
|
|
116
|
+
scannedEntries: outcome.scannedEntries,
|
|
117
|
+
partial: outcome.status === 'partial',
|
|
118
|
+
...(outcome.errorCode ? { errorCode: outcome.errorCode } : {}),
|
|
119
|
+
});
|
|
120
|
+
return outcome;
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
const cancelled = signal?.aborted === true ||
|
|
124
|
+
(error instanceof Error && error.name === 'AbortError') ||
|
|
125
|
+
error instanceof DiscoveryCancelledError;
|
|
126
|
+
const errorCode = cancelled
|
|
127
|
+
? 'scan_cancelled'
|
|
128
|
+
: error instanceof DiscoveryError
|
|
129
|
+
? error.code
|
|
130
|
+
: 'scan_failed';
|
|
131
|
+
const durationMs = Math.max(0, Math.round(this.now() - startedAt));
|
|
132
|
+
this.telemetry?.record({
|
|
133
|
+
operation: 'discover',
|
|
134
|
+
outcome: cancelled ? 'cancelled' : 'failed',
|
|
135
|
+
durationMs,
|
|
136
|
+
candidateCount: 0,
|
|
137
|
+
scannedEntries: 0,
|
|
138
|
+
partial: false,
|
|
139
|
+
errorCode,
|
|
140
|
+
});
|
|
141
|
+
if (cancelled) {
|
|
142
|
+
return {
|
|
143
|
+
rootId: root.id,
|
|
144
|
+
candidates: [],
|
|
145
|
+
status: 'partial',
|
|
146
|
+
errorCode: 'scan_cancelled',
|
|
147
|
+
durationMs,
|
|
148
|
+
scannedEntries: 0,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
// Actionable failures (missing/unreadable root, mid-scan errors) are
|
|
152
|
+
// outcomes, not exceptions: the client shows a status with an action,
|
|
153
|
+
// and the telemetry ring keeps a sanitized trace.
|
|
154
|
+
if (error instanceof DiscoveryError) {
|
|
155
|
+
return {
|
|
156
|
+
rootId: root.id,
|
|
157
|
+
candidates: [],
|
|
158
|
+
status: 'partial',
|
|
159
|
+
errorCode: error.code,
|
|
160
|
+
durationMs,
|
|
161
|
+
scannedEntries: 0,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
throw error;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
async scan(root, signal, limits, startedAt) {
|
|
168
|
+
const candidates = [];
|
|
169
|
+
let scannedEntries = 0;
|
|
170
|
+
let partial = false;
|
|
171
|
+
let errorCode;
|
|
172
|
+
const throwIfCancelled = () => {
|
|
173
|
+
if (signal?.aborted)
|
|
174
|
+
throw new DiscoveryCancelledError();
|
|
175
|
+
};
|
|
176
|
+
const remainingBudget = () => limits.budgetMs - Math.round(this.now() - startedAt);
|
|
177
|
+
const walk = async (relativePath, depth) => {
|
|
178
|
+
throwIfCancelled();
|
|
179
|
+
if (remainingBudget() <= 0) {
|
|
180
|
+
partial = true;
|
|
181
|
+
errorCode = 'budget_exceeded';
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (candidates.length >= limits.maxCandidates || scannedEntries >= limits.maxEntries) {
|
|
185
|
+
partial = true;
|
|
186
|
+
errorCode = 'budget_exceeded';
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const absolutePath = (0, path_1.join)(root.canonicalPath, relativePath);
|
|
190
|
+
let entries = [];
|
|
191
|
+
try {
|
|
192
|
+
entries = await (0, promises_1.readdir)(absolutePath, { withFileTypes: true });
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
const code = error?.code;
|
|
196
|
+
if (depth === 0) {
|
|
197
|
+
throw new DiscoveryError(code === 'ENOENT'
|
|
198
|
+
? 'The root directory no longer exists.'
|
|
199
|
+
: 'The root directory is not readable.', code === 'ENOENT' ? 'root_missing' : 'root_unreadable');
|
|
200
|
+
}
|
|
201
|
+
// A nested unreadable directory is not fatal: skip it and keep going.
|
|
202
|
+
if (code !== 'EACCES' && code !== 'EPERM') {
|
|
203
|
+
partial = true;
|
|
204
|
+
errorCode = 'scan_failed';
|
|
205
|
+
}
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
scannedEntries += 1;
|
|
209
|
+
// walk() is only called for directories that were already checked to
|
|
210
|
+
// carry no marker (the root excepted — it is always walked so the user's
|
|
211
|
+
// authorized boundary is searched even when it is itself a project).
|
|
212
|
+
const subdirectories = [];
|
|
213
|
+
for (const entry of entries) {
|
|
214
|
+
if (entry.isDirectory() &&
|
|
215
|
+
!entry.name.startsWith('.') &&
|
|
216
|
+
!IGNORED_DIRECTORY_NAMES.has(entry.name)) {
|
|
217
|
+
subdirectories.push(entry.name);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
for (const name of subdirectories) {
|
|
221
|
+
throwIfCancelled();
|
|
222
|
+
if (candidates.length >= limits.maxCandidates || scannedEntries >= limits.maxEntries) {
|
|
223
|
+
partial = true;
|
|
224
|
+
errorCode = 'budget_exceeded';
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
const childRelative = relativePath ? `${relativePath}/${name}` : name;
|
|
228
|
+
const childAbsolute = (0, path_1.join)(root.canonicalPath, childRelative);
|
|
229
|
+
let childMarker;
|
|
230
|
+
try {
|
|
231
|
+
// stat (not lstat): a symlinked marker would resolve, but directory
|
|
232
|
+
// symlinks themselves are skipped below without following.
|
|
233
|
+
const childStat = await (0, promises_1.stat)(childAbsolute);
|
|
234
|
+
if (!childStat.isDirectory())
|
|
235
|
+
continue;
|
|
236
|
+
const childEntries = await (0, promises_1.readdir)(childAbsolute, { withFileTypes: true });
|
|
237
|
+
scannedEntries += 1;
|
|
238
|
+
for (const childEntry of childEntries) {
|
|
239
|
+
if (PROJECT_MARKERS.has(childEntry.name)) {
|
|
240
|
+
childMarker = PROJECT_MARKERS.get(childEntry.name);
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
catch (error) {
|
|
246
|
+
const code = error?.code;
|
|
247
|
+
if (code === 'EACCES' || code === 'EPERM' || code === 'ENOENT')
|
|
248
|
+
continue;
|
|
249
|
+
partial = true;
|
|
250
|
+
errorCode = 'scan_failed';
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
if (childMarker) {
|
|
254
|
+
candidates.push(Object.freeze({
|
|
255
|
+
rootId: root.id,
|
|
256
|
+
relativePath: childRelative,
|
|
257
|
+
label: name,
|
|
258
|
+
hint: childMarker,
|
|
259
|
+
status: 'ready',
|
|
260
|
+
}));
|
|
261
|
+
}
|
|
262
|
+
else if (depth + 1 < limits.maxDepth) {
|
|
263
|
+
await walk(childRelative, depth + 1);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
// The root itself is a candidate when it carries a marker (depth 0).
|
|
268
|
+
let rootHint;
|
|
269
|
+
try {
|
|
270
|
+
const rootEntries = await (0, promises_1.readdir)(root.canonicalPath, { withFileTypes: true });
|
|
271
|
+
scannedEntries += 1;
|
|
272
|
+
for (const entry of rootEntries) {
|
|
273
|
+
if (PROJECT_MARKERS.has(entry.name)) {
|
|
274
|
+
rootHint = PROJECT_MARKERS.get(entry.name);
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
catch (error) {
|
|
280
|
+
const code = error?.code;
|
|
281
|
+
throw new DiscoveryError(code === 'ENOENT'
|
|
282
|
+
? 'The root directory no longer exists.'
|
|
283
|
+
: 'The root directory is not readable.', code === 'ENOENT' ? 'root_missing' : 'root_unreadable');
|
|
284
|
+
}
|
|
285
|
+
if (rootHint) {
|
|
286
|
+
candidates.push(Object.freeze({
|
|
287
|
+
rootId: root.id,
|
|
288
|
+
relativePath: '',
|
|
289
|
+
label: root.label,
|
|
290
|
+
hint: rootHint,
|
|
291
|
+
status: 'ready',
|
|
292
|
+
}));
|
|
293
|
+
}
|
|
294
|
+
await walk('', 0);
|
|
295
|
+
throwIfCancelled();
|
|
296
|
+
if (remainingBudget() <= 0) {
|
|
297
|
+
partial = true;
|
|
298
|
+
errorCode = 'budget_exceeded';
|
|
299
|
+
}
|
|
300
|
+
return Object.freeze({
|
|
301
|
+
rootId: root.id,
|
|
302
|
+
candidates: Object.freeze(candidates),
|
|
303
|
+
status: partial ? 'partial' : 'ready',
|
|
304
|
+
...(partial && errorCode ? { errorCode } : {}),
|
|
305
|
+
durationMs: Math.max(0, Math.round(this.now() - startedAt)),
|
|
306
|
+
scannedEntries,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
exports.WorkspaceDiscoveryServiceV1 = WorkspaceDiscoveryServiceV1;
|
|
311
|
+
class DiscoveryError extends Error {
|
|
312
|
+
constructor(message, code) {
|
|
313
|
+
super(message);
|
|
314
|
+
this.code = code;
|
|
315
|
+
this.name = 'DiscoveryError';
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
class DiscoveryCancelledError extends Error {
|
|
319
|
+
constructor() {
|
|
320
|
+
super('Workspace discovery was cancelled.');
|
|
321
|
+
this.name = 'DiscoveryCancelledError';
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
//# sourceMappingURL=workspace-discovery.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{r as a,j as e,I as D,s as be,C as ye,W as ge}from"./index-
|
|
1
|
+
import{r as a,j as e,I as D,s as be,C as ye,W as ge}from"./index-BhGhORAX.js";import{u as Ce,R as Ne}from"./useAutoNotice-BLADeFFj.js";function Le({workspaceId:s,refreshEpoch:o,actions:l,navigatorWidthPx:r,onNavigatorWidthCommit:d,revealRequest:c}){const[f,m]=a.useState({}),[x,j]=a.useState(new Set(["workspace-root"])),[i,N]=a.useState(null),[F,B]=a.useState(""),[M,G]=a.useState(null),[Y,P]=a.useState(!1),[H,K]=a.useState(""),{notice:J,showNotice:$,clearNotice:V}=Ce(),[T,ue]=a.useState(""),[Z,_]=a.useState({}),[I,A]=a.useState(""),[O,R]=a.useState("view"),[L,w]=a.useState(""),[k,q]=a.useState(!1),[de,S]=a.useState(!1),[fe,me]=a.useState(""),[he,W]=a.useState(!1),h=a.useRef(0),C=a.useRef(0),ee=a.useRef(null),E=async(t,n=!1,y=h.current)=>{if(y!==h.current)return;const p=f[t],g=n?p?.nextCursor:void 0;if(!(n&&!g)){m(u=>({...u,[t]:{items:p?.items??[],nextCursor:p?.nextCursor??null,revision:p?.revision??"",loading:!0}}));try{const u=await l.listFiles(t,g??void 0);if(y!==h.current)return;m(v=>({...v,[t]:{items:n?ke(v[t]?.items??[],u.items):u.items,nextCursor:u.nextCursor,revision:u.revision,loading:!1}}))}catch(u){if(y!==h.current)return;if(n&&U(u)){$("目录已变化,已重新载入第一页。"),E(t,!1,y);return}m(v=>({...v,[t]:{items:v[t]?.items??[],nextCursor:v[t]?.nextCursor??null,revision:v[t]?.revision??"",loading:!1,error:z(u)}}))}}},Q=async(t=h.current)=>{try{const n=await l.gitStatus();if(t!==h.current)return;_(Se(n)),A(n.nextCursor?"Git 装饰仅显示首批 200 个变更;在 Git 面板继续分页查看。":"")}catch(n){if(t!==h.current)return;_({}),A(`Git 状态装饰不可用:${z(n)}`)}},te=a.useRef(s);a.useEffect(()=>{const t=h.current+1;h.current=t,C.current+=1;const n=te.current===s?ee.current:null;te.current=s,m({}),j(new Set(["workspace-root"])),N(n),B(""),G(null),P(!1),K(""),V(),_({}),A(""),s&&(E("workspace-root",!1,t),Q(t),n&&b(n,!1,!1,t))},[o,s]);const se=a.useRef(null),b=async(t,n=!1,y=!1,p=h.current,g=C.current+1)=>{if(p===h.current&&!(!t.readable||t.sensitive||X(t))){C.current=g,N(t),K(""),n||(B(""),G(null),P(!1),R("view"),w(""),S(!1)),y||V();try{const u=await l.readFileContent(t.id,n?M??void 0:void 0);if(p!==h.current||g!==C.current)return;P(u.binary),B(v=>n?`${v}${u.content??""}`:u.content??""),G(u.nextCursor),me(u.revision),y&&$("文件已变化,已从第一页重新载入。")}catch(u){if(p!==h.current||g!==C.current)return;if(n&&U(u)){await b(t,!1,!0,p,g);return}K(z(u))}}},re=De({selected:i,binary:Y,hasMorePages:M!==null}),xe=()=>{w(F),S(!1),R("edit")},ie=()=>{if(L!==F){W(!0);return}ne()},ne=()=>{R("view"),w(""),S(!1),W(!1)},je=async()=>{if(!(!i||k)){q(!0),S(!1);try{await l.writeFileContent(i.id,L,fe),R("view"),w(""),await b(i,!1,!0,h.current,C.current+1),$("已保存文件。","success"),Q()}catch(t){U(t)?S(!0):$(z(t),"error")}finally{q(!1)}}},ae=t=>{const n=new Set(x);n.has(t)?n.delete(t):(n.add(t),f[t]||E(t)),j(n)};a.useEffect(()=>{c&&se.current!==c.id&&(se.current=c.id,b({id:c.fileId,name:c.displayPath.split("/").pop()??c.displayPath,displayPath:c.displayPath,kind:"file",modifiedAt:new Date().toISOString(),sensitive:!1,readable:!0}))},[c?.id]),a.useEffect(()=>{ee.current=i},[i]);const pe=a.useMemo(()=>Object.values(f).flatMap(t=>t.items),[f]),oe=T.trim()?pe.filter(t=>t.name.toLocaleLowerCase().includes(T.toLocaleLowerCase())):null,ve=!!f["workspace-root"]?.loading;return e.jsxs("div",{className:"work-resource-panel files-panel",children:[e.jsxs("div",{className:"resource-toolbar",children:[e.jsxs("label",{className:"resource-search",children:[e.jsx("span",{className:"sr-only",children:"搜索已加载文件"}),e.jsx(D,{name:"search",size:16}),e.jsx("input",{type:"search",value:T,onChange:t=>ue(t.target.value),placeholder:"搜索已加载文件",title:"搜索仅覆盖已加载的目录和文件"})]}),e.jsx("button",{type:"button",className:"icon-button","aria-label":"刷新文件树","aria-busy":ve,onClick:()=>{E("workspace-root"),Q()},children:e.jsx(D,{name:"refresh",size:16})})]}),e.jsxs(Ne,{panelId:"files",navigatorWidthPx:r,onNavigatorWidthCommit:d,contentLabel:"文件预览",navigatorLabel:"工作区文件",handleLabel:"调整文件目录宽度",contentClassName:"file-preview",navigatorClassName:"file-tree",children:[e.jsxs(e.Fragment,{children:[I?e.jsx("p",{className:"resource-hint",role:"status",children:I}):null,oe?e.jsx(e.Fragment,{children:e.jsx("ul",{role:"list",className:"file-node-list search-results",children:oe.map(t=>e.jsx(ce,{node:t,depth:0,expanded:!1,selected:i?.id===t.id,gitLabels:Z[t.displayPath]??[],onToggle:ae,onSelect:n=>{b(n)}},t.id))})}):e.jsx(le,{parentId:"workspace-root",depth:0,directories:f,expanded:x,selectedId:i?.id,gitDecorations:Z,onToggle:ae,onSelect:t=>{b(t)},onLoadMore:t=>{E(t,!0)}})]}),e.jsxs(e.Fragment,{children:[J?e.jsx("p",{className:"resource-notice",role:"status",children:J.text}):null,i?e.jsxs(e.Fragment,{children:[e.jsxs("header",{children:[e.jsx("strong",{children:i.name}),e.jsx("span",{children:Fe(i.sizeBytes??0)})]}),H?e.jsx("p",{className:"resource-error",role:"alert",children:H}):Y?e.jsxs("div",{className:"resource-empty",children:[e.jsx(D,{name:"code",size:16}),e.jsx("strong",{title:"出于安全和性能考虑,只显示元数据。",children:"二进制文件"})]}):e.jsxs(e.Fragment,{children:[e.jsx("div",{className:"file-preview-actions",children:O==="view"?e.jsx("button",{type:"button",className:"text-button",disabled:!re,title:re?void 0:"仅支持编辑 512 KB 内的 UTF-8 文本文件",onClick:xe,children:"编辑"}):e.jsxs(e.Fragment,{children:[e.jsx("button",{type:"button",className:"primary-button",disabled:k||L===F,"aria-busy":k,onClick:()=>{je()},children:k?"保存中…":"保存"}),e.jsx("button",{type:"button",className:"text-button",disabled:k,onClick:ie,children:"取消"})]})}),de?e.jsxs("p",{className:"resource-error",role:"alert",children:["文件已在别处变更,保存被拒绝。",e.jsx("button",{type:"button",className:"text-button",onClick:()=>{b(i,!1,!0)},children:"重新加载"})]}):null,O==="edit"?e.jsx("textarea",{className:"file-editor",value:L,spellCheck:!1,"aria-label":`编辑文件 ${i.name}`,onChange:t=>w(t.target.value),onKeyDown:t=>{t.key==="Escape"&&(t.preventDefault(),ie())}}):e.jsx("pre",{tabIndex:0,className:"file-code-view","aria-label":`文件内容 ${i.name}`,children:be(F)})]}),M&&O==="view"?e.jsx("button",{type:"button",className:"secondary-button",onClick:()=>{b(i,!0)},children:"加载更多内容"}):null]}):e.jsxs("div",{className:"resource-empty",children:[e.jsx(D,{name:"workspace",size:16}),e.jsx("strong",{children:"选择文件预览"}),e.jsx("p",{title:"敏感文件、工作区外链接和二进制正文不会返回浏览器。",children:"受限内容不会返回浏览器。"})]})]})]}),e.jsx(ye,{open:he,onClose:()=>W(!1),title:"放弃未保存的修改?",body:"当前草稿尚未保存,放弃后将回到文件的最新内容。",confirmLabel:"放弃修改",danger:!0,onConfirm:ne})]})}function le({parentId:s,depth:o,directories:l,expanded:r,selectedId:d,gitDecorations:c,onToggle:f,onSelect:m,onLoadMore:x}){const j=l[s];return j?j.error?e.jsx("p",{className:"resource-error",role:"alert",children:j.error}):e.jsxs("ul",{role:"list",className:"file-node-list",children:[j.items.map(i=>{const N=r.has(i.id);return e.jsxs("li",{children:[e.jsx(ce,{node:i,depth:o,expanded:N,selected:d===i.id,gitLabels:c[i.displayPath]??[],onToggle:f,onSelect:m}),X(i)&&N?e.jsx(le,{parentId:i.id,depth:o+1,directories:l,expanded:r,selectedId:d,gitDecorations:c,onToggle:f,onSelect:m,onLoadMore:x}):null]},i.id)}),j.loading?e.jsx("li",{className:"resource-loading",children:"正在读取…"}):null,j.nextCursor?e.jsx("li",{children:e.jsx("button",{type:"button",className:"text-button",onClick:()=>x(s),children:"加载更多"})}):null]}):e.jsx("p",{className:"resource-loading",children:"正在读取…"})}function ce({node:s,depth:o,expanded:l,selected:r,gitLabels:d,onToggle:c,onSelect:f}){const m=s.sensitive||!s.readable,x=X(s);return e.jsxs("button",{type:"button",className:`file-node ${r?"selected":""} ${m?"blocked":""}`,style:{paddingInlineStart:`${8+o*15}px`},"aria-expanded":x?l:void 0,"aria-label":`${we(s)} ${s.name}${m?",不可读取":""}${d.length?`,Git ${d.join("、")}`:""}`,onClick:()=>x?c(s.id):f(s),disabled:m,children:[e.jsx(D,{name:x?"workspace":"code",size:16}),e.jsx("span",{children:s.name}),s.kind==="symlink"?e.jsx("small",{children:"链接"}):null,s.sensitive?e.jsx("small",{children:"敏感"}):null,d.length?e.jsx("small",{className:"file-git-status",title:`Git ${d.join("、")}`,children:d.join(" · ")}):null]})}function we(s){return s.kind==="directory"?"目录":s.kind==="symlink"?s.targetKind==="directory"?"符号链接目录":"符号链接":"文件"}function X(s){return s.kind==="directory"||s.kind==="symlink"&&s.targetKind==="directory"}function ke(s,o){const l=new Map(s.map(r=>[r.id,r]));for(const r of o)l.set(r.id,r);return[...l.values()]}function Se(s){if(!s.hasWorktree)return{};const o=new Map,l=(r,d)=>{const c=r.replace(/\\/gu,"/").replace(/^\.\//u,"");if(!c)return;const f=o.get(c)??new Set;f.add(d),o.set(c,f);const m=c.split("/");for(let x=1;x<m.length;x+=1){const j=m.slice(0,x).join("/"),i=o.get(j)??new Set;i.add("含变更"),o.set(j,i)}};return s.conflicted.forEach(r=>l(r.path,"冲突")),s.staged.forEach(r=>l(r.path,"已暂存")),s.unstaged.forEach(r=>l(r.path,"未暂存")),s.untracked.forEach(r=>l(r.path,"未跟踪")),Object.fromEntries([...o].map(([r,d])=>[r,Object.freeze([...d])]))}const Ee=512*1024;function De(s){const{selected:o,binary:l,hasMorePages:r}=s;return!o||l||o.sensitive||!o.readable||r?!1:(o.sizeBytes??0)<=Ee}function Fe(s){return s<1024?`${s} B`:s<1048576?`${(s/1024).toFixed(1)} KB`:`${(s/1048576).toFixed(1)} MB`}function U(s){return s instanceof ge&&s.code==="file_revision_conflict"}function z(s){return s instanceof Error?s.message:"文件请求失败。"}export{Ee as FILE_EDIT_MAX_BYTES,Le as FilesPanel,De as canEditFileContent};
|