@orion-agents/orion-code 0.3.20 → 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.
@@ -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-D5ze0fOv.js";import{u as Ce,R as Ne}from"./useAutoNotice-Bsr8yfc3.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};
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};