@orion-agents/orion-code 0.3.12 → 0.3.14
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 +51 -4
- package/dist/web/file-read-service.d.ts +12 -0
- package/dist/web/file-read-service.js +24 -1
- package/dist/web/file-write-service.d.ts +16 -0
- package/dist/web/file-write-service.js +103 -0
- package/dist/web/protocol.d.ts +2 -2
- package/dist/web/review-service.d.ts +2 -33
- package/dist/web/review-service.js +4 -59
- package/dist/web/server.js +38 -9
- package/dist/web/workbench-controller.d.ts +11 -9
- package/dist/web/workbench-controller.js +13 -17
- package/dist/web-client/assets/{DiffViewer-Bs1C5uRM.js → DiffViewer-BrHqudpK.js} +1 -1
- package/dist/web-client/assets/FilesPanel-BcFY6L1a.js +1 -0
- package/dist/web-client/assets/{GitPanel-ykbaVNlo.js → GitPanel-B6E5IEXm.js} +1 -1
- package/dist/web-client/assets/{ResourceSplitLayout-B0Zs9aMC.js → ResourceSplitLayout-fPYE9JKJ.js} +1 -1
- package/dist/web-client/assets/ReviewPanel-5ecTVmhT.js +1 -0
- package/dist/web-client/assets/{TerminalPanel-Cz0ohyPm.js → TerminalPanel-IkCttxgw.js} +1 -1
- package/dist/web-client/assets/index-DgVNIqmE.js +20 -0
- package/dist/web-client/assets/index-R6E589Fc.css +1 -0
- package/dist/web-client/index.html +2 -2
- package/npm-shrinkwrap.json +1 -1
- package/package.json +1 -1
- package/dist/web-client/assets/FilesPanel-dW61EHHn.js +0 -2
- package/dist/web-client/assets/ReviewPanel-1oqfaqoh.js +0 -1
- package/dist/web-client/assets/index-B11PjVU2.css +0 -1
- package/dist/web-client/assets/index-DiNz6Qhw.js +0 -17
package/CHANGELOG.md
CHANGED
|
@@ -22,11 +22,58 @@ which is **not** a pass.
|
|
|
22
22
|
|
|
23
23
|
## [Unreleased]
|
|
24
24
|
|
|
25
|
-
## [0.3.
|
|
25
|
+
## [0.3.14] — CANDIDATE
|
|
26
26
|
|
|
27
|
-
> **Status: candidate.**
|
|
28
|
-
>
|
|
29
|
-
>
|
|
27
|
+
> **Status: candidate.** Editable Files panel (guarded `POST /files/write` with
|
|
28
|
+
> revision CAS) and removal of the legacy view-only controls. Not merged,
|
|
29
|
+
> tagged or published.
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
|
|
33
|
+
- File editing in the Files panel: content view gains a view/edit toggle; the
|
|
34
|
+
edit mode is a controlled monospace textarea with dirty tracking, explicit
|
|
35
|
+
save (guarded `POST /files/write`), cancel/Esc with an unsaved-changes
|
|
36
|
+
confirmation, and a 409 conflict path that offers an explicit reload.
|
|
37
|
+
- `POST /files/write`: workspace-fenced (registry + symlink escape checks),
|
|
38
|
+
sensitive-path block, 512 KiB text-only payload limit, on-disk stat
|
|
39
|
+
fingerprint CAS (`file_revision_conflict`), user-gesture header
|
|
40
|
+
(`file-mutation-v1`), idempotent `file.write` mutation ledger entry, and
|
|
41
|
+
one-shot snapshot invalidation so Git/Review decorations refresh after a
|
|
42
|
+
save.
|
|
43
|
+
|
|
44
|
+
### Fixed
|
|
45
|
+
|
|
46
|
+
- Resource refreshes keep the inspected file: `ReviewPanel` no longer drops the
|
|
47
|
+
open diff on a manual refresh, a `refreshEpoch` invalidation or a Git revision
|
|
48
|
+
conflict recovery; `FilesPanel` keeps the open file on refresh (still reset
|
|
49
|
+
when the workspace itself changes), so the editor no longer closes mid-edit.
|
|
50
|
+
- The review summary counters are localised (已暂存 / 未暂存 / 未跟踪 / 冲突)
|
|
51
|
+
instead of raw English labels.
|
|
52
|
+
- Binary detection tolerates an 8 KiB sample that ends inside one UTF-8 code
|
|
53
|
+
point, so CJK files are no longer rejected as binary on read or save.
|
|
54
|
+
|
|
55
|
+
### Removed
|
|
56
|
+
|
|
57
|
+
- The read-only file toolbar: 「跳转」(line jump), 「复制」 and 「自动换行」
|
|
58
|
+
controls, the line-number gutter spans and the wrap-only styles.
|
|
59
|
+
- The Review panel's 「验证证据」 module: the evidence list,
|
|
60
|
+
`WebReviewVerificationV1`, `ReviewServiceV1.verificationPage`, the
|
|
61
|
+
`GET /review/verification` route and the snapshot's `verification` /
|
|
62
|
+
`verificationTruncated` fields. The review overview is now composed only from
|
|
63
|
+
Git facts; durable tool receipts remain available to the runtime.
|
|
64
|
+
|
|
65
|
+
## [0.3.13] — CANDIDATE
|
|
66
|
+
|
|
67
|
+
> **Status: candidate.** Development baseline after v0.3.12 (version bump only;
|
|
68
|
+
> no functional changes yet). Not merged, tagged or published.
|
|
69
|
+
|
|
70
|
+
## [0.3.12] — NPM-PUBLISHED
|
|
71
|
+
|
|
72
|
+
> **Status: npm-published.** `@orion-agents/orion-code@0.3.12` is available
|
|
73
|
+
> through the npm registry (latest tag). Published 2026-09-07 via PR #252;
|
|
74
|
+
> git tag/release may lag. Wide Right Workspace + repository-truth + guarded
|
|
75
|
+
> Git mutations (v0.3.12-plan) plus resource split layout, conversation
|
|
76
|
+
> history navigator and the unified brand mark (v0.3.13-plan v3/v4/v5).
|
|
30
77
|
|
|
31
78
|
### Added
|
|
32
79
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type BigIntStats } from 'fs';
|
|
1
2
|
export type WebFileKindV1 = 'file' | 'directory' | 'symlink';
|
|
2
3
|
export interface WebFileNodeV1 {
|
|
3
4
|
readonly id: string;
|
|
@@ -79,6 +80,15 @@ export declare class FileReadServiceV1 {
|
|
|
79
80
|
readonly cursor?: string;
|
|
80
81
|
readonly limitBytes?: number;
|
|
81
82
|
}): WebFileContentPageV1;
|
|
83
|
+
/** v0.3.14 — write-path preflight for an existing in-root regular text file. */
|
|
84
|
+
writePreflight(fileId: string): {
|
|
85
|
+
readonly canonicalPath: string;
|
|
86
|
+
readonly relativePath: string;
|
|
87
|
+
readonly revision: string;
|
|
88
|
+
readonly sizeBytes: number;
|
|
89
|
+
};
|
|
90
|
+
/** v0.3.14 — post-write revision fingerprint for the response. */
|
|
91
|
+
revisionOf(canonicalPath: string): string;
|
|
82
92
|
private projectNode;
|
|
83
93
|
private resolveNode;
|
|
84
94
|
private resolveRelative;
|
|
@@ -86,4 +96,6 @@ export declare class FileReadServiceV1 {
|
|
|
86
96
|
private encodeCursor;
|
|
87
97
|
private decodeCursor;
|
|
88
98
|
}
|
|
99
|
+
export declare function fingerprintStat(stat: BigIntStats): string;
|
|
100
|
+
export declare function isBinary(buffer: Buffer): boolean;
|
|
89
101
|
//# sourceMappingURL=file-read-service.d.ts.map
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.FileReadServiceV1 = void 0;
|
|
4
|
+
exports.fingerprintStat = fingerprintStat;
|
|
5
|
+
exports.isBinary = isBinary;
|
|
4
6
|
const crypto_1 = require("crypto");
|
|
5
7
|
const fs_1 = require("fs");
|
|
6
8
|
const path_1 = require("path");
|
|
@@ -269,7 +271,7 @@ class FileReadServiceV1 {
|
|
|
269
271
|
if (sample.length > 0) {
|
|
270
272
|
this.performance.bytesRead += (0, fs_1.readSync)(descriptor, sample, 0, sample.length, 0);
|
|
271
273
|
}
|
|
272
|
-
const binary = isBinary(sample);
|
|
274
|
+
const binary = isBinary(validUtf8Prefix(sample));
|
|
273
275
|
if (binary) {
|
|
274
276
|
return Object.freeze({
|
|
275
277
|
fileId: input.fileId,
|
|
@@ -333,6 +335,27 @@ class FileReadServiceV1 {
|
|
|
333
335
|
(0, fs_1.closeSync)(descriptor);
|
|
334
336
|
}
|
|
335
337
|
}
|
|
338
|
+
/** v0.3.14 — write-path preflight for an existing in-root regular text file. */
|
|
339
|
+
writePreflight(fileId) {
|
|
340
|
+
this.performance.readOperations += 1;
|
|
341
|
+
const node = this.resolveNode(fileId);
|
|
342
|
+
if (!node.stat.isFile()) {
|
|
343
|
+
throw new errors_1.WebWorkbenchError(409, 'File node is not a regular file.', 'file_not_regular');
|
|
344
|
+
}
|
|
345
|
+
if (isSensitiveResolvedPath(this.root, node.relativePath, node.canonicalPath)) {
|
|
346
|
+
throw new errors_1.WebWorkbenchError(403, 'Sensitive files are not writable in the Web Workbench.', 'sensitive_file_blocked');
|
|
347
|
+
}
|
|
348
|
+
return Object.freeze({
|
|
349
|
+
canonicalPath: node.canonicalPath,
|
|
350
|
+
relativePath: node.relativePath,
|
|
351
|
+
revision: fingerprintStat(node.stat),
|
|
352
|
+
sizeBytes: Number(node.stat.size),
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
/** v0.3.14 — post-write revision fingerprint for the response. */
|
|
356
|
+
revisionOf(canonicalPath) {
|
|
357
|
+
return fingerprintStat((0, fs_1.statSync)(canonicalPath, { bigint: true }));
|
|
358
|
+
}
|
|
336
359
|
projectNode(relativePath) {
|
|
337
360
|
const lexicalPath = (0, path_1.resolve)(this.root, relativePath);
|
|
338
361
|
const lexicalStat = (0, fs_1.lstatSync)(lexicalPath, { bigint: true });
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { FileReadServiceV1 } from './file-read-service';
|
|
2
|
+
export interface WebFileWriteResultV1 {
|
|
3
|
+
readonly fileId: string;
|
|
4
|
+
readonly revision: string;
|
|
5
|
+
readonly sizeBytes: number;
|
|
6
|
+
}
|
|
7
|
+
export declare class FileWriteServiceV1 {
|
|
8
|
+
private readonly fileService;
|
|
9
|
+
constructor(fileService: FileReadServiceV1);
|
|
10
|
+
writeContent(input: {
|
|
11
|
+
readonly fileId: string;
|
|
12
|
+
readonly content: string;
|
|
13
|
+
readonly expectedRevision: string;
|
|
14
|
+
}): WebFileWriteResultV1;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=file-write-service.d.ts.map
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FileWriteServiceV1 = void 0;
|
|
4
|
+
const fs_1 = require("fs");
|
|
5
|
+
const errors_1 = require("./errors");
|
|
6
|
+
const file_read_service_1 = require("./file-read-service");
|
|
7
|
+
const MAX_WRITE_BYTES = 512 * 1024;
|
|
8
|
+
const WRITE_SAMPLE_BYTES = 8192;
|
|
9
|
+
/**
|
|
10
|
+
* v0.3.14 — bounded, CAS-guarded writes for an existing in-root text file.
|
|
11
|
+
*
|
|
12
|
+
* Safety stack mirrors the read path: the file id resolves through the read
|
|
13
|
+
* service's fenced registry (workspace escape, symlink escape and sensitive
|
|
14
|
+
* paths fail closed there), the payload must be valid UTF-8 text without NULs
|
|
15
|
+
* within 512 KiB, and the write commits only when the on-disk revision still
|
|
16
|
+
* matches the caller's `expectedRevision` (compare-and-swap against the same
|
|
17
|
+
* stat fingerprint the read path returns).
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* A bounded sample can end inside one UTF-8 code point (very likely for CJK
|
|
21
|
+
* text at an 8 KiB boundary); drop the trailing partial byte before judging the
|
|
22
|
+
* file as binary, mirroring the read path's `validUtf8Prefix`.
|
|
23
|
+
*/
|
|
24
|
+
function completeUtf8Tail(buffer) {
|
|
25
|
+
for (let trim = 0; trim <= Math.min(3, Math.max(0, buffer.length - 1)); trim += 1) {
|
|
26
|
+
const candidate = buffer.subarray(0, buffer.length - trim);
|
|
27
|
+
if (!(0, file_read_service_1.isBinary)(candidate))
|
|
28
|
+
return candidate;
|
|
29
|
+
}
|
|
30
|
+
return buffer;
|
|
31
|
+
}
|
|
32
|
+
class FileWriteServiceV1 {
|
|
33
|
+
constructor(fileService) {
|
|
34
|
+
this.fileService = fileService;
|
|
35
|
+
}
|
|
36
|
+
writeContent(input) {
|
|
37
|
+
const content = input.content;
|
|
38
|
+
if (typeof content !== 'string') {
|
|
39
|
+
throw new errors_1.WebWorkbenchError(400, 'File content must be a string.', 'file_content_invalid');
|
|
40
|
+
}
|
|
41
|
+
if (content.includes('\0')) {
|
|
42
|
+
throw new errors_1.WebWorkbenchError(415, 'Files containing NUL bytes are not editable in the Web Workbench.', 'file_binary');
|
|
43
|
+
}
|
|
44
|
+
const byteLength = Buffer.byteLength(content, 'utf8');
|
|
45
|
+
if (byteLength > MAX_WRITE_BYTES) {
|
|
46
|
+
throw new errors_1.WebWorkbenchError(413, 'Edited file content exceeds the 512 KiB Web editor limit.', 'file_too_large');
|
|
47
|
+
}
|
|
48
|
+
if (!/^[0-9a-f]{64}$/u.test(input.expectedRevision)) {
|
|
49
|
+
throw new errors_1.WebWorkbenchError(400, 'expectedRevision is invalid.', 'file_revision_invalid');
|
|
50
|
+
}
|
|
51
|
+
const preflight = this.fileService.writePreflight(input.fileId);
|
|
52
|
+
if (preflight.sizeBytes > MAX_WRITE_BYTES) {
|
|
53
|
+
throw new errors_1.WebWorkbenchError(413, 'Target file exceeds the 512 KiB Web editor limit.', 'file_too_large');
|
|
54
|
+
}
|
|
55
|
+
const payload = Buffer.from(content, 'utf8');
|
|
56
|
+
// Phase 1 (read descriptor): CAS + binary re-check. An O_WRONLY descriptor
|
|
57
|
+
// cannot readSync, so the preflight opens its own read handle first.
|
|
58
|
+
const readDescriptor = (0, fs_1.openSync)(preflight.canonicalPath, fs_1.constants.O_RDONLY);
|
|
59
|
+
let beforeSize;
|
|
60
|
+
try {
|
|
61
|
+
const before = (0, fs_1.fstatSync)(readDescriptor, { bigint: true });
|
|
62
|
+
beforeSize = before.size;
|
|
63
|
+
const sample = Buffer.alloc(Math.min(WRITE_SAMPLE_BYTES, Number(before.size)));
|
|
64
|
+
if (sample.length > 0) {
|
|
65
|
+
(0, fs_1.readSync)(readDescriptor, sample, 0, sample.length, 0);
|
|
66
|
+
if ((0, file_read_service_1.isBinary)(completeUtf8Tail(sample))) {
|
|
67
|
+
throw new errors_1.WebWorkbenchError(415, 'Target file is not UTF-8 text.', 'file_binary');
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if ((0, file_read_service_1.fingerprintStat)(before) !== input.expectedRevision) {
|
|
71
|
+
throw new errors_1.WebWorkbenchError(409, 'The file changed on disk before the save could run; reload it first.', 'file_revision_conflict');
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
(0, fs_1.closeSync)(readDescriptor);
|
|
76
|
+
}
|
|
77
|
+
// Phase 2 (write descriptor): CAS is re-checked by the kernel fingerprint
|
|
78
|
+
// window being as short as possible between the two handles.
|
|
79
|
+
const writeDescriptor = (0, fs_1.openSync)(preflight.canonicalPath, fs_1.constants.O_WRONLY);
|
|
80
|
+
try {
|
|
81
|
+
const before = (0, fs_1.fstatSync)(writeDescriptor, { bigint: true });
|
|
82
|
+
if ((0, file_read_service_1.fingerprintStat)(before) !== input.expectedRevision) {
|
|
83
|
+
throw new errors_1.WebWorkbenchError(409, 'The file changed on disk before the save could run; reload it first.', 'file_revision_conflict');
|
|
84
|
+
}
|
|
85
|
+
(0, fs_1.writeSync)(writeDescriptor, payload, 0, payload.length, 0);
|
|
86
|
+
// Shrink keeps the file exactly as long as the payload.
|
|
87
|
+
if (beforeSize > BigInt(payload.length)) {
|
|
88
|
+
(0, fs_1.truncateSync)(preflight.canonicalPath, payload.length);
|
|
89
|
+
}
|
|
90
|
+
const after = (0, fs_1.statSync)(preflight.canonicalPath, { bigint: true });
|
|
91
|
+
return Object.freeze({
|
|
92
|
+
fileId: input.fileId,
|
|
93
|
+
revision: (0, file_read_service_1.fingerprintStat)(after),
|
|
94
|
+
sizeBytes: Number(after.size),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
(0, fs_1.closeSync)(writeDescriptor);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
exports.FileWriteServiceV1 = FileWriteServiceV1;
|
|
103
|
+
//# sourceMappingURL=file-write-service.js.map
|
package/dist/web/protocol.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ import type { SessionMeta } from '../services/session-storage';
|
|
|
9
9
|
import type { ToolConfirmationPolicy } from '../services/global-config';
|
|
10
10
|
import type { WebFileContentPageV1, WebFileNodeV1, WebFileTreePageV1 } from './file-read-service';
|
|
11
11
|
import type { WebGitCommitV1, WebGitDiffPageV1, WebGitFileV1, WebGitLogPageV1, WebGitStatusV1 } from './git-read-model-service';
|
|
12
|
-
import type { WebReviewSnapshotV1
|
|
12
|
+
import type { WebReviewSnapshotV1 } from './review-service';
|
|
13
13
|
import type { WebSessionRuntimeSummaryV1 } from './session-runtime-registry';
|
|
14
14
|
import type { WebTerminalCreateResultV1, WebTerminalExitV1, WebTerminalGapV1, WebTerminalMetadataV1, WebTerminalOutputFrameV1, WebTerminalStateV1 } from './terminal-manager';
|
|
15
15
|
export declare const WEB_API_VERSION: 1;
|
|
@@ -376,7 +376,7 @@ export type WebWorkspaceResourceV1 = 'files' | 'git' | 'review';
|
|
|
376
376
|
export type WebWorkspaceInvalidationReasonV1 = 'context-change' | 'filesystem-change' | 'terminal-command' | 'tool-finished';
|
|
377
377
|
export type WebToolDetailSummaryV1 = ToolDetailSummary;
|
|
378
378
|
export type WebToolDetailPageV1 = ToolDetailPage;
|
|
379
|
-
export type { WebFileContentPageV1, WebFileNodeV1, WebFileTreePageV1, WebGitCommitV1, WebGitDiffPageV1, WebGitFileV1, WebGitLogPageV1, WebGitStatusV1, WebReviewSnapshotV1,
|
|
379
|
+
export type { WebFileContentPageV1, WebFileNodeV1, WebFileTreePageV1, WebGitCommitV1, WebGitDiffPageV1, WebGitFileV1, WebGitLogPageV1, WebGitStatusV1, WebReviewSnapshotV1, WebTerminalCreateResultV1, WebTerminalExitV1, WebTerminalGapV1, WebTerminalMetadataV1, WebTerminalOutputFrameV1, WebTerminalStateV1, };
|
|
380
380
|
export interface WebBootstrapV1 {
|
|
381
381
|
readonly apiVersion: 1;
|
|
382
382
|
readonly productVersion: string;
|
|
@@ -1,19 +1,4 @@
|
|
|
1
|
-
import type { VerifiedDurableToolReceiptRefV1 } from '../runtime/durable-tool-receipt-reader';
|
|
2
1
|
import type { GitReadModelServiceV1, WebGitDiffPageV1, WebGitFileV1 } from './git-read-model-service';
|
|
3
|
-
export interface WebReviewVerificationV1 {
|
|
4
|
-
readonly callId: string;
|
|
5
|
-
readonly sessionId: string;
|
|
6
|
-
readonly threadId: string;
|
|
7
|
-
readonly sequence: number;
|
|
8
|
-
readonly toolName: string;
|
|
9
|
-
readonly state: 'success' | 'error' | 'skipped';
|
|
10
|
-
readonly terminal: VerifiedDurableToolReceiptRefV1['terminal'];
|
|
11
|
-
readonly success: boolean;
|
|
12
|
-
readonly outputBytes: number;
|
|
13
|
-
readonly hasArtifact: boolean;
|
|
14
|
-
readonly executionPolicyDigest: string;
|
|
15
|
-
readonly receiptDigest: string;
|
|
16
|
-
}
|
|
17
2
|
export interface WebReviewSnapshotV1 {
|
|
18
3
|
readonly revision: string;
|
|
19
4
|
readonly repositoryRevision: string;
|
|
@@ -26,28 +11,12 @@ export interface WebReviewSnapshotV1 {
|
|
|
26
11
|
readonly untrackedCount: number;
|
|
27
12
|
readonly conflictCount: number;
|
|
28
13
|
readonly truncated: boolean;
|
|
29
|
-
readonly verification: readonly WebReviewVerificationV1[];
|
|
30
14
|
}
|
|
31
|
-
/** Review overview composed only from Git facts
|
|
15
|
+
/** Review overview composed only from Git facts. */
|
|
32
16
|
export declare class ReviewServiceV1 {
|
|
33
17
|
private readonly git;
|
|
34
|
-
|
|
35
|
-
constructor(git: GitReadModelServiceV1, listReceiptRefs: () => Promise<readonly VerifiedDurableToolReceiptRefV1[]> | readonly VerifiedDurableToolReceiptRefV1[]);
|
|
18
|
+
constructor(git: GitReadModelServiceV1);
|
|
36
19
|
snapshot(): Promise<WebReviewSnapshotV1>;
|
|
37
|
-
/**
|
|
38
|
-
* v0.3.12 S2 — progressive verification evidence. The Review summary loads
|
|
39
|
-
* first; receipts page in behind it, optionally filtered to one session.
|
|
40
|
-
*/
|
|
41
|
-
verificationPage(input: {
|
|
42
|
-
readonly sessionId?: string;
|
|
43
|
-
readonly cursor?: number;
|
|
44
|
-
readonly pageSize?: number;
|
|
45
|
-
}): Promise<{
|
|
46
|
-
readonly items: readonly WebReviewVerificationV1[];
|
|
47
|
-
readonly nextCursor: number | null;
|
|
48
|
-
readonly totalForSession: number;
|
|
49
|
-
}>;
|
|
50
|
-
private projectVerification;
|
|
51
20
|
diff(input: Parameters<GitReadModelServiceV1['diff']>[0]): Promise<WebGitDiffPageV1>;
|
|
52
21
|
}
|
|
53
22
|
//# sourceMappingURL=review-service.d.ts.map
|
|
@@ -2,21 +2,17 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ReviewServiceV1 = void 0;
|
|
4
4
|
const crypto_1 = require("crypto");
|
|
5
|
-
/** Review overview composed only from Git facts
|
|
5
|
+
/** Review overview composed only from Git facts. */
|
|
6
6
|
class ReviewServiceV1 {
|
|
7
|
-
constructor(git
|
|
7
|
+
constructor(git) {
|
|
8
8
|
this.git = git;
|
|
9
|
-
this.listReceiptRefs = listReceiptRefs;
|
|
10
9
|
}
|
|
11
10
|
async snapshot() {
|
|
12
11
|
// v0.3.9 #237/#228 — collect every Git status page. A repository with more
|
|
13
12
|
// than the single pageSize of changed files was silently truncated before;
|
|
14
13
|
// the review overview must aggregate all pages (bounded by a guard that
|
|
15
14
|
// keeps runaway repositories from looping forever).
|
|
16
|
-
const
|
|
17
|
-
this.listReceiptRefs(),
|
|
18
|
-
collectStatusPages((cursor) => this.git.status({ pageSize: 2000, ...(cursor ? { cursor } : {}) })),
|
|
19
|
-
]);
|
|
15
|
+
const statusPages = await collectStatusPages((cursor) => this.git.status({ pageSize: 2000, ...(cursor ? { cursor } : {}) }));
|
|
20
16
|
const status = statusPages.at(-1) ?? (await this.git.status({ pageSize: 2000 }));
|
|
21
17
|
const conflicted = [];
|
|
22
18
|
const staged = [];
|
|
@@ -29,12 +25,8 @@ class ReviewServiceV1 {
|
|
|
29
25
|
untracked.push(...page.untracked);
|
|
30
26
|
}
|
|
31
27
|
const changedFiles = uniqueFiles([...conflicted, ...staged, ...unstaged, ...untracked]);
|
|
32
|
-
const verification = this.projectVerification(receiptRefs.slice(0, 100));
|
|
33
28
|
const revision = (0, crypto_1.createHash)('sha256')
|
|
34
|
-
.update(JSON.stringify({
|
|
35
|
-
repositoryRevision: status.repositoryRevision,
|
|
36
|
-
verification,
|
|
37
|
-
}))
|
|
29
|
+
.update(JSON.stringify({ repositoryRevision: status.repositoryRevision }))
|
|
38
30
|
.digest('hex');
|
|
39
31
|
return Object.freeze({
|
|
40
32
|
revision,
|
|
@@ -48,66 +40,19 @@ class ReviewServiceV1 {
|
|
|
48
40
|
untrackedCount: untracked.length,
|
|
49
41
|
conflictCount: conflicted.length,
|
|
50
42
|
truncated: statusPages.at(-1)?.truncated ?? false,
|
|
51
|
-
verification: Object.freeze(verification),
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
/**
|
|
55
|
-
* v0.3.12 S2 — progressive verification evidence. The Review summary loads
|
|
56
|
-
* first; receipts page in behind it, optionally filtered to one session.
|
|
57
|
-
*/
|
|
58
|
-
async verificationPage(input) {
|
|
59
|
-
const refs = await this.listReceiptRefs();
|
|
60
|
-
const filtered = input.sessionId ? refs.filter(ref => ref.sessionId === input.sessionId) : refs;
|
|
61
|
-
const offset = Math.max(0, Math.min(filtered.length, Math.trunc(input.cursor ?? 0)));
|
|
62
|
-
const pageSize = Math.max(1, Math.min(100, Math.trunc(input.pageSize ?? 25)));
|
|
63
|
-
const page = filtered.slice(offset, offset + pageSize);
|
|
64
|
-
const nextCursor = offset + page.length < filtered.length ? offset + page.length : null;
|
|
65
|
-
return Object.freeze({
|
|
66
|
-
items: Object.freeze(this.projectVerification(page)),
|
|
67
|
-
nextCursor,
|
|
68
|
-
totalForSession: filtered.length,
|
|
69
43
|
});
|
|
70
44
|
}
|
|
71
|
-
projectVerification(refs) {
|
|
72
|
-
return refs.map(receipt => Object.freeze({
|
|
73
|
-
callId: receipt.callId,
|
|
74
|
-
sessionId: receipt.sessionId,
|
|
75
|
-
threadId: receipt.threadId,
|
|
76
|
-
sequence: receipt.sequence,
|
|
77
|
-
toolName: receipt.toolName,
|
|
78
|
-
state: reviewState(receipt),
|
|
79
|
-
terminal: receipt.terminal,
|
|
80
|
-
success: receipt.success,
|
|
81
|
-
outputBytes: receipt.outputBytes,
|
|
82
|
-
hasArtifact: receipt.hasArtifact,
|
|
83
|
-
executionPolicyDigest: receipt.executionPolicyDigest,
|
|
84
|
-
receiptDigest: receipt.receiptDigest,
|
|
85
|
-
}));
|
|
86
|
-
}
|
|
87
45
|
diff(input) {
|
|
88
46
|
return this.git.diff(input);
|
|
89
47
|
}
|
|
90
48
|
}
|
|
91
49
|
exports.ReviewServiceV1 = ReviewServiceV1;
|
|
92
|
-
function reviewState(receipt) {
|
|
93
|
-
if (receipt.success)
|
|
94
|
-
return 'success';
|
|
95
|
-
return receipt.terminal === 'interrupted' || receipt.terminal === 'indeterminate'
|
|
96
|
-
? 'skipped'
|
|
97
|
-
: 'error';
|
|
98
|
-
}
|
|
99
50
|
function uniqueFiles(files) {
|
|
100
51
|
const byId = new Map();
|
|
101
52
|
for (const file of files)
|
|
102
53
|
byId.set(file.fileId, file);
|
|
103
54
|
return [...byId.values()].sort((left, right) => left.path.localeCompare(right.path));
|
|
104
55
|
}
|
|
105
|
-
/**
|
|
106
|
-
* v0.3.9 #237/#228 — page through Git status until the snapshot is fully
|
|
107
|
-
* collected. The bounded guard (64 pages x 2_000 = 128k files) exists to stop
|
|
108
|
-
* runaway repositories from looping; a repository exceeding it stays marked
|
|
109
|
-
* truncated so the UI can still communicate the limit.
|
|
110
|
-
*/
|
|
111
56
|
async function collectStatusPages(fetchPage) {
|
|
112
57
|
const pages = [];
|
|
113
58
|
let page = await fetchPage(undefined);
|
package/dist/web/server.js
CHANGED
|
@@ -431,6 +431,32 @@ async function handleRequest(context) {
|
|
|
431
431
|
}));
|
|
432
432
|
return;
|
|
433
433
|
}
|
|
434
|
+
const fileWriteMatch = path === '/files/write';
|
|
435
|
+
if (method === 'POST' && fileWriteMatch) {
|
|
436
|
+
assertMutation(request, context.nonce, context.origin, 'file_mutation_forbidden');
|
|
437
|
+
assertFileUserGesture(request);
|
|
438
|
+
const body = requireRecord(await readJson(request), 'File write request');
|
|
439
|
+
assertOnlyKeys(body, [
|
|
440
|
+
'requestId',
|
|
441
|
+
'fileId',
|
|
442
|
+
'content',
|
|
443
|
+
'expectedRevision',
|
|
444
|
+
'workspaceId',
|
|
445
|
+
'expectedContextRevision',
|
|
446
|
+
]);
|
|
447
|
+
const requestId = requireUuid(body.requestId, 'requestId');
|
|
448
|
+
const contextGuard = requireContextGuardRecord(body);
|
|
449
|
+
const fileId = requireText(body.fileId, 'fileId', 256);
|
|
450
|
+
const content = requireText(body.content, 'content', 512 * 1024 + 8);
|
|
451
|
+
const expectedRevision = requireFileRevision(body.expectedRevision);
|
|
452
|
+
const result = await context.workbench.executeMutation(requestId, 'file.write', { fileId, expectedRevision, ...contextGuard }, () => context.workbench.writeFileContent(contextGuard, {
|
|
453
|
+
fileId,
|
|
454
|
+
content,
|
|
455
|
+
expectedRevision,
|
|
456
|
+
}));
|
|
457
|
+
sendJson(response, 200, result);
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
434
460
|
if (method === 'POST' && (path === '/git/stage' || path === '/git/unstage')) {
|
|
435
461
|
assertMutation(request, context.nonce, context.origin, 'git_mutation_forbidden');
|
|
436
462
|
assertGitUserGesture(request);
|
|
@@ -495,15 +521,6 @@ async function handleRequest(context) {
|
|
|
495
521
|
}));
|
|
496
522
|
return;
|
|
497
523
|
}
|
|
498
|
-
if (method === 'GET' && path === '/review/verification') {
|
|
499
|
-
const contextGuard = requireContextGuardQuery(url);
|
|
500
|
-
const sessionId = url.searchParams.get('sessionId') ?? undefined;
|
|
501
|
-
const cursorValue = url.searchParams.get('cursor');
|
|
502
|
-
const cursor = cursorValue === null ? undefined : Number(cursorValue);
|
|
503
|
-
const pageSize = boundedInteger(url.searchParams.get('pageSize'), 25, 1, 100);
|
|
504
|
-
sendJson(response, 200, await context.workbench.reviewVerificationPage(contextGuard, { sessionId, cursor, pageSize }));
|
|
505
|
-
return;
|
|
506
|
-
}
|
|
507
524
|
if (method === 'GET' && path === '/review') {
|
|
508
525
|
sendJson(response, 200, await context.workbench.review(requireContextGuardQuery(url)));
|
|
509
526
|
return;
|
|
@@ -646,6 +663,18 @@ function requireGitRevision(value) {
|
|
|
646
663
|
}
|
|
647
664
|
return text;
|
|
648
665
|
}
|
|
666
|
+
function assertFileUserGesture(request) {
|
|
667
|
+
if (request.headers[protocol_1.WEB_USER_GESTURE_HEADER] !== 'file-mutation-v1') {
|
|
668
|
+
throw new HttpProblem(403, 'File mutations require an explicit browser user gesture.', 'file_user_gesture_required');
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
function requireFileRevision(value) {
|
|
672
|
+
const text = requireText(value, 'expectedRevision', 64);
|
|
673
|
+
if (!/^[0-9a-f]{64}$/u.test(text)) {
|
|
674
|
+
throw new HttpProblem(400, 'expectedRevision must be a file content revision.');
|
|
675
|
+
}
|
|
676
|
+
return text;
|
|
677
|
+
}
|
|
649
678
|
function assertGitUserGesture(request) {
|
|
650
679
|
if (request.headers[protocol_1.WEB_USER_GESTURE_HEADER] !== 'git-mutation-v1') {
|
|
651
680
|
throw new HttpProblem(403, 'Git mutations require an explicit browser user gesture.', 'git_user_gesture_required');
|
|
@@ -43,6 +43,7 @@ export declare class WebWorkbenchController {
|
|
|
43
43
|
private readonly workspaceMutationOwners;
|
|
44
44
|
private readonly pendingWorkspaceMutationStates;
|
|
45
45
|
private fileService;
|
|
46
|
+
private fileWriteService;
|
|
46
47
|
private gitService;
|
|
47
48
|
private reviewService;
|
|
48
49
|
private contextRevisionValue;
|
|
@@ -100,6 +101,16 @@ export declare class WebWorkbenchController {
|
|
|
100
101
|
readToolDetail(callId: string, offsetBytes: number, limitBytes: number, context?: WebContextGuardV1): Promise<WebToolDetailPageV1>;
|
|
101
102
|
listFiles(context: WebContextGuardV1, input: Parameters<FileReadServiceV1['list']>[0]): import("./file-read-service").WebFileTreePageV1;
|
|
102
103
|
readFileContent(context: WebContextGuardV1, input: Parameters<FileReadServiceV1['readContent']>[0]): import("./file-read-service").WebFileContentPageV1;
|
|
104
|
+
/** v0.3.14 — guarded CAS write for an existing in-root text file. */
|
|
105
|
+
writeFileContent(context: WebContextGuardV1, input: {
|
|
106
|
+
readonly fileId: string;
|
|
107
|
+
readonly content: string;
|
|
108
|
+
readonly expectedRevision: string;
|
|
109
|
+
}): {
|
|
110
|
+
readonly fileId: string;
|
|
111
|
+
readonly revision: string;
|
|
112
|
+
readonly sizeBytes: number;
|
|
113
|
+
};
|
|
103
114
|
gitStatus(context: WebContextGuardV1, input: Parameters<GitReadModelServiceV1['status']>[0]): Promise<import("./git-read-model-service").WebGitStatusV1>;
|
|
104
115
|
searchFiles(context: WebContextGuardV1, input: {
|
|
105
116
|
readonly query: string;
|
|
@@ -137,15 +148,6 @@ export declare class WebWorkbenchController {
|
|
|
137
148
|
private resolveSafeGitPaths;
|
|
138
149
|
private assertGitRepositoryRevision;
|
|
139
150
|
private fileContextRevision;
|
|
140
|
-
reviewVerificationPage(context: WebContextGuardV1, input: {
|
|
141
|
-
readonly sessionId?: string;
|
|
142
|
-
readonly cursor?: number;
|
|
143
|
-
readonly pageSize?: number;
|
|
144
|
-
}): Promise<{
|
|
145
|
-
readonly items: readonly import("./review-service").WebReviewVerificationV1[];
|
|
146
|
-
readonly nextCursor: number | null;
|
|
147
|
-
readonly totalForSession: number;
|
|
148
|
-
}>;
|
|
149
151
|
gitLog(context: WebContextGuardV1, input: Parameters<GitReadModelServiceV1['log']>[0]): Promise<import("./git-read-model-service").WebGitLogPageV1>;
|
|
150
152
|
gitDiff(context: WebContextGuardV1, input: Parameters<GitReadModelServiceV1['diff']>[0]): Promise<import("./git-read-model-service").WebGitDiffPageV1>;
|
|
151
153
|
review(context: WebContextGuardV1): Promise<import("./review-service").WebReviewSnapshotV1>;
|
|
@@ -7,7 +7,6 @@ const crypto_1 = require("crypto");
|
|
|
7
7
|
const fs_1 = require("fs");
|
|
8
8
|
const path_1 = require("path");
|
|
9
9
|
const agent_runtime_controller_1 = require("../runtime/agent-runtime-controller");
|
|
10
|
-
const durable_tool_receipt_reader_1 = require("../runtime/durable-tool-receipt-reader");
|
|
11
10
|
const mcp_1 = require("../runtime/mcp");
|
|
12
11
|
const product_bootstrap_1 = require("../runtime/product-bootstrap");
|
|
13
12
|
const canonical_1 = require("../runtime/protocol/canonical");
|
|
@@ -27,6 +26,7 @@ const errors_1 = require("./errors");
|
|
|
27
26
|
const context_text_1 = require("./context-text");
|
|
28
27
|
const event_hub_1 = require("./event-hub");
|
|
29
28
|
const file_read_service_1 = require("./file-read-service");
|
|
29
|
+
const file_write_service_1 = require("./file-write-service");
|
|
30
30
|
const git_read_model_service_1 = require("./git-read-model-service");
|
|
31
31
|
const workspace_mutation_arbiter_1 = require("./workspace-mutation-arbiter");
|
|
32
32
|
const protocol_1 = require("./protocol");
|
|
@@ -840,6 +840,13 @@ class WebWorkbenchController {
|
|
|
840
840
|
this.assertContextGuard(context);
|
|
841
841
|
return result;
|
|
842
842
|
}
|
|
843
|
+
/** v0.3.14 — guarded CAS write for an existing in-root text file. */
|
|
844
|
+
writeFileContent(context, input) {
|
|
845
|
+
this.assertContextGuard(context);
|
|
846
|
+
const result = this.fileWriteService.writeContent(input);
|
|
847
|
+
this.assertContextGuard(context);
|
|
848
|
+
return result;
|
|
849
|
+
}
|
|
843
850
|
async gitStatus(context, input) {
|
|
844
851
|
this.assertContextGuard(context);
|
|
845
852
|
const result = await this.gitService.status(input);
|
|
@@ -901,10 +908,6 @@ class WebWorkbenchController {
|
|
|
901
908
|
const node = this.fileService;
|
|
902
909
|
return typeof node.rootRevision === 'function' ? node.rootRevision() : '';
|
|
903
910
|
}
|
|
904
|
-
async reviewVerificationPage(context, input) {
|
|
905
|
-
this.assertContextGuard(context);
|
|
906
|
-
return this.reviewService.verificationPage(input);
|
|
907
|
-
}
|
|
908
911
|
async gitLog(context, input) {
|
|
909
912
|
this.assertContextGuard(context);
|
|
910
913
|
const result = await this.gitService.log(input);
|
|
@@ -919,17 +922,9 @@ class WebWorkbenchController {
|
|
|
919
922
|
}
|
|
920
923
|
async review(context) {
|
|
921
924
|
this.assertContextGuard(context);
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
return result;
|
|
926
|
-
}
|
|
927
|
-
catch (error) {
|
|
928
|
-
if (error instanceof durable_tool_receipt_reader_1.DurableToolReceiptReaderError) {
|
|
929
|
-
throw new errors_1.WebWorkbenchError(500, 'Durable Review receipt facts failed integrity validation.', 'review_receipt_invalid');
|
|
930
|
-
}
|
|
931
|
-
throw error;
|
|
932
|
-
}
|
|
925
|
+
const result = await this.reviewService.snapshot();
|
|
926
|
+
this.assertContextGuard(context);
|
|
927
|
+
return result;
|
|
933
928
|
}
|
|
934
929
|
listTerminals(context) {
|
|
935
930
|
this.assertContextGuard(context);
|
|
@@ -1819,8 +1814,9 @@ class WebWorkbenchController {
|
|
|
1819
1814
|
this.workspaceValue = workspace;
|
|
1820
1815
|
this.runtimeValue = runtime;
|
|
1821
1816
|
this.fileService = new file_read_service_1.FileReadServiceV1(workspace);
|
|
1817
|
+
this.fileWriteService = new file_write_service_1.FileWriteServiceV1(this.fileService);
|
|
1822
1818
|
this.gitService = new git_read_model_service_1.GitReadModelServiceV1(workspace);
|
|
1823
|
-
this.reviewService = new review_service_1.ReviewServiceV1(this.gitService
|
|
1819
|
+
this.reviewService = new review_service_1.ReviewServiceV1(this.gitService);
|
|
1824
1820
|
const eventSink = {
|
|
1825
1821
|
emit: (event) => {
|
|
1826
1822
|
if (event.type === 'status_changed')
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{r as u,j as s,I as N}from"./index-
|
|
1
|
+
import{r as u,j as s,I as N}from"./index-DgVNIqmE.js";function v(t){const n=[];let i;for(const r of t)r.startsWith("@@")?(i&&n.push(i),i={title:r,lines:[r]}):i&&i.lines.push(r);return i&&n.push(i),n}function D(t,n){const i={schemaVersion:1,type:"review_context",repositoryRevision:t.repositoryRevision,path:t.path,hunk:n.title},r=n.lines.join(`
|
|
2
2
|
`).slice(0,12e3);return[`请审阅 ${t.path} 的这个 diff hunk,并指出正确性、安全性和测试风险:`,"","```review_context",JSON.stringify(i,null,2),"```","","```diff",r,"```"].join(`
|
|
3
3
|
`)}function H({page:t,loading:n=!1,onLoadMore:i,onSendToComposer:r}){const a=u.useMemo(()=>v(t.lines),[t.lines]),[m,h]=u.useState(0),[f,p]=u.useState(!1),[x,k]=u.useState(!1),[b,y]=u.useState(""),d=Math.min(m,Math.max(0,a.length-1)),c=a[d];u.useEffect(()=>{h(0),p(!1),y("")},[t.fileId,t.repositoryRevision]);const j=(e,l)=>{let o=l;if(e.key==="ArrowRight"||e.key==="ArrowDown")o=(l+1)%a.length;else if(e.key==="ArrowLeft"||e.key==="ArrowUp")o=(l-1+a.length)%a.length;else if(e.key==="Home")o=0;else if(e.key==="End")o=a.length-1;else return;e.preventDefault(),h(o);const w=e.currentTarget.parentElement;window.requestAnimationFrame(()=>w?.querySelector(`[data-hunk-index="${o}"]`)?.focus())};return t.binary?s.jsxs("div",{className:"resource-empty compact",children:[s.jsx(N,{name:"code"}),s.jsx("strong",{children:"二进制差异"}),s.jsx("p",{children:"只展示变更事实,不返回 binary patch。"})]}):s.jsxs("div",{className:"diff-viewer",children:[s.jsxs("header",{children:[s.jsx("strong",{title:t.path,children:t.path}),s.jsx("span",{children:t.truncated?"受限预览":`${t.lines.length} 行`})]}),a.length>1?s.jsx("div",{className:"diff-hunk-nav",role:"tablist","aria-label":"Diff hunks",children:a.map((e,l)=>s.jsx("button",{type:"button",role:"tab","data-hunk-index":l,"aria-selected":d===l,tabIndex:d===l?0:-1,onClick:()=>h(l),onKeyDown:o=>j(o,l),children:e.title||`Hunk ${l+1}`},`${e.title}-${l}`))}):null,s.jsxs("div",{className:"diff-actions","aria-label":"Diff 显示选项",children:[c?s.jsx("button",{type:"button",className:"text-button","aria-expanded":!f,onClick:()=>p(e=>!e),children:f?"展开 Hunk":"折叠 Hunk"}):null,s.jsx("button",{type:"button",className:"text-button","aria-pressed":x,onClick:()=>k(e=>!e),children:"显示空白字符"}),c?s.jsx("button",{type:"button",className:"text-button",onClick:()=>{$(c.lines.join(`
|
|
4
4
|
`),y)},children:"复制 Hunk"}):null]}),f?null:s.jsx("pre",{className:"diff-lines",tabIndex:0,"aria-label":`Diff ${t.path}`,children:(c?.lines??t.lines).map((e,l)=>s.jsxs("span",{className:e.startsWith("+")?"addition":e.startsWith("-")?"deletion":"",children:[S(e,x)||" ",`
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{r as l,j as e,I as R,s as fe,W as xe}from"./index-DgVNIqmE.js";import{R as me}from"./ResourceSplitLayout-fPYE9JKJ.js";function Ne({workspaceId:s,refreshEpoch:a,actions:o,navigatorWidthPx:r,onNavigatorWidthCommit:d}){const[u,h]=l.useState({}),[j,x]=l.useState(new Set(["workspace-root"])),[n,m]=l.useState(null),[b,M]=l.useState(""),[B,G]=l.useState(null),[U,z]=l.useState(!1),[X,K]=l.useState(""),[Y,N]=l.useState(""),[T,ie]=l.useState(""),[H,P]=l.useState({}),[J,_]=l.useState(""),[O,F]=l.useState("view"),[$,k]=l.useState(""),[D,V]=l.useState(!1),[le,S]=l.useState(!1),[ae,oe]=l.useState(""),f=l.useRef(0),w=l.useRef(0),Z=l.useRef(null),E=async(t,i=!1,g=f.current)=>{if(g!==f.current)return;const v=u[t],C=i?v?.nextCursor:void 0;if(!(i&&!C)){h(c=>({...c,[t]:{items:v?.items??[],nextCursor:v?.nextCursor??null,revision:v?.revision??"",loading:!0}}));try{const c=await o.listFiles(t,C??void 0);if(g!==f.current)return;h(p=>({...p,[t]:{items:i?je(p[t]?.items??[],c.items):c.items,nextCursor:c.nextCursor,revision:c.revision,loading:!1}}))}catch(c){if(g!==f.current)return;if(i&&W(c)){N("目录已变化,已重新载入第一页。"),E(t,!1,g);return}h(p=>({...p,[t]:{items:p[t]?.items??[],nextCursor:p[t]?.nextCursor??null,revision:p[t]?.revision??"",loading:!1,error:L(c)}}))}}},A=async(t=f.current)=>{try{const i=await o.gitStatus();if(t!==f.current)return;P(ve(i)),_(i.nextCursor?"Git 装饰仅显示首批 200 个变更;在 Git 面板继续分页查看。":"")}catch(i){if(t!==f.current)return;P({}),_(`Git 状态装饰不可用:${L(i)}`)}},q=l.useRef(s);l.useEffect(()=>{const t=f.current+1;f.current=t,w.current+=1;const i=q.current===s?Z.current:null;q.current=s,h({}),x(new Set(["workspace-root"])),m(i),M(""),G(null),z(!1),K(""),N(""),P({}),_(""),s&&(E("workspace-root",!1,t),A(t),i&&y(i,!1,!1,t))},[a,s]);const y=async(t,i=!1,g=!1,v=f.current,C=w.current+1)=>{if(v===f.current&&!(!t.readable||t.sensitive||Q(t))){w.current=C,m(t),K(""),i||(M(""),G(null),z(!1),F("view"),k(""),S(!1)),g||N("");try{const c=await o.readFileContent(t.id,i?B??void 0:void 0);if(v!==f.current||C!==w.current)return;z(c.binary),M(p=>i?`${p}${c.content??""}`:c.content??""),G(c.nextCursor),oe(c.revision),g&&N("文件已变化,已从第一页重新载入。")}catch(c){if(v!==f.current||C!==w.current)return;if(i&&W(c)){await y(t,!1,!0,v,C);return}K(L(c))}}},I=be({selected:n,binary:U,hasMorePages:B!==null}),ce=()=>{k(b),S(!1),F("edit")},ee=()=>{$!==b&&!window.confirm("放弃未保存的修改?")||(F("view"),k(""),S(!1))},ue=async()=>{if(!(!n||D)){V(!0),S(!1);try{await o.writeFileContent(n.id,$,ae),F("view"),k(""),await y(n,!1,!0,f.current,w.current+1),N("已保存文件。"),A()}catch(t){W(t)?S(!0):N(L(t))}finally{V(!1)}}},te=t=>{const i=new Set(j);i.has(t)?i.delete(t):(i.add(t),u[t]||E(t)),x(i)};l.useEffect(()=>{Z.current=n},[n]);const de=l.useMemo(()=>Object.values(u).flatMap(t=>t.items),[u]),se=T.trim()?de.filter(t=>t.name.toLocaleLowerCase().includes(T.toLocaleLowerCase())):null;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(R,{name:"search",size:14}),e.jsx("input",{type:"search",value:T,onChange:t=>ie(t.target.value),placeholder:"搜索已加载文件"})]}),e.jsx("button",{type:"button",className:"icon-button","aria-label":"刷新文件树",onClick:()=>{E("workspace-root"),A()},children:e.jsx(R,{name:"refresh",size:15})})]}),e.jsxs(me,{panelId:"files",navigatorWidthPx:r,onNavigatorWidthCommit:d,contentLabel:"文件预览",navigatorLabel:"工作区文件",handleLabel:"调整文件目录宽度",contentClassName:"file-preview",navigatorClassName:"file-tree",children:[e.jsxs(e.Fragment,{children:[J?e.jsx("p",{className:"resource-hint",role:"status",children:J}):null,se?e.jsxs(e.Fragment,{children:[e.jsx("p",{className:"resource-hint",children:"搜索仅覆盖已加载的目录和文件。"}),e.jsx("ul",{role:"list",className:"file-node-list search-results",children:se.map(t=>e.jsx(ne,{node:t,depth:0,expanded:!1,selected:n?.id===t.id,gitLabels:H[t.displayPath]??[],onToggle:te,onSelect:i=>{y(i)}},t.id))})]}):e.jsx(re,{parentId:"workspace-root",depth:0,directories:u,expanded:j,selectedId:n?.id,gitDecorations:H,onToggle:te,onSelect:t=>{y(t)},onLoadMore:t=>{E(t,!0)}})]}),e.jsxs(e.Fragment,{children:[Y?e.jsx("p",{className:"resource-notice",role:"status",children:Y}):null,n?e.jsxs(e.Fragment,{children:[e.jsxs("header",{children:[e.jsx("strong",{children:n.name}),e.jsx("span",{children:ye(n.sizeBytes??0)})]}),X?e.jsx("p",{className:"resource-error",role:"alert",children:X}):U?e.jsxs("div",{className:"resource-empty",children:[e.jsx(R,{name:"code"}),e.jsx("strong",{children:"二进制文件"}),e.jsx("p",{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:!I,title:I?void 0:"仅支持编辑 512 KB 内的 UTF-8 文本文件",onClick:ce,children:"编辑"}):e.jsxs(e.Fragment,{children:[e.jsx("button",{type:"button",className:"primary-button",disabled:D||$===b,onClick:()=>{ue()},children:D?"保存中…":"保存"}),e.jsx("button",{type:"button",className:"text-button",disabled:D,onClick:ee,children:"取消"})]})}),le?e.jsxs("p",{className:"resource-error",role:"alert",children:["文件已在别处变更,保存被拒绝。",e.jsx("button",{type:"button",className:"text-button",onClick:()=>{y(n,!1,!0)},children:"重新加载"})]}):null,O==="edit"?e.jsx("textarea",{className:"file-editor",value:$,spellCheck:!1,"aria-label":`编辑文件 ${n.name}`,onChange:t=>k(t.target.value),onKeyDown:t=>{t.key==="Escape"&&(t.preventDefault(),ee())}}):e.jsx("pre",{tabIndex:0,className:"file-code-view","aria-label":`文件内容 ${n.name}`,children:fe(b)})]}),B&&O==="view"?e.jsx("button",{type:"button",className:"secondary-button",onClick:()=>{y(n,!0)},children:"加载更多内容"}):null]}):e.jsxs("div",{className:"resource-empty",children:[e.jsx(R,{name:"workspace"}),e.jsx("strong",{children:"选择文件预览"}),e.jsx("p",{children:"敏感文件、工作区外链接和二进制正文不会返回浏览器。"})]})]})]})]})}function re({parentId:s,depth:a,directories:o,expanded:r,selectedId:d,gitDecorations:u,onToggle:h,onSelect:j,onLoadMore:x}){const n=o[s];return n?n.error?e.jsx("p",{className:"resource-error",role:"alert",children:n.error}):e.jsxs("ul",{role:"list",className:"file-node-list",children:[n.items.map(m=>{const b=r.has(m.id);return e.jsxs("li",{children:[e.jsx(ne,{node:m,depth:a,expanded:b,selected:d===m.id,gitLabels:u[m.displayPath]??[],onToggle:h,onSelect:j}),Q(m)&&b?e.jsx(re,{parentId:m.id,depth:a+1,directories:o,expanded:r,selectedId:d,gitDecorations:u,onToggle:h,onSelect:j,onLoadMore:x}):null]},m.id)}),n.loading?e.jsx("li",{className:"resource-loading",children:"正在读取…"}):null,n.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 ne({node:s,depth:a,expanded:o,selected:r,gitLabels:d,onToggle:u,onSelect:h}){const j=s.sensitive||!s.readable,x=Q(s);return e.jsxs("button",{type:"button",className:`file-node ${r?"selected":""} ${j?"blocked":""}`,style:{paddingInlineStart:`${8+a*15}px`},"aria-expanded":x?o:void 0,"aria-label":`${he(s)} ${s.name}${j?",不可读取":""}${d.length?`,Git ${d.join("、")}`:""}`,onClick:()=>x?u(s.id):h(s),disabled:j,children:[e.jsx(R,{name:x?"workspace":"code",size:14}),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 he(s){return s.kind==="directory"?"目录":s.kind==="symlink"?s.targetKind==="directory"?"符号链接目录":"符号链接":"文件"}function Q(s){return s.kind==="directory"||s.kind==="symlink"&&s.targetKind==="directory"}function je(s,a){const o=new Map(s.map(r=>[r.id,r]));for(const r of a)o.set(r.id,r);return[...o.values()]}function ve(s){if(!s.isRepository)return{};const a=new Map,o=(r,d)=>{const u=r.replace(/\\/gu,"/").replace(/^\.\//u,"");if(!u)return;const h=a.get(u)??new Set;h.add(d),a.set(u,h);const j=u.split("/");for(let x=1;x<j.length;x+=1){const n=j.slice(0,x).join("/"),m=a.get(n)??new Set;m.add("含变更"),a.set(n,m)}};return s.conflicted.forEach(r=>o(r.path,"冲突")),s.staged.forEach(r=>o(r.path,"已暂存")),s.unstaged.forEach(r=>o(r.path,"未暂存")),s.untracked.forEach(r=>o(r.path,"未跟踪")),Object.fromEntries([...a].map(([r,d])=>[r,Object.freeze([...d])]))}const pe=512*1024;function be(s){const{selected:a,binary:o,hasMorePages:r}=s;return!a||o||a.sensitive||!a.readable||r?!1:(a.sizeBytes??0)<=pe}function ye(s){return s<1024?`${s} B`:s<1048576?`${(s/1024).toFixed(1)} KB`:`${(s/1048576).toFixed(1)} MB`}function W(s){return s instanceof xe&&s.code==="file_revision_conflict"}function L(s){return s instanceof Error?s.message:"文件请求失败。"}export{pe as FILE_EDIT_MAX_BYTES,Ne as FilesPanel,be as canEditFileContent};
|