@orion-agents/orion-code 0.3.15 → 0.3.18
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 +254 -0
- package/dist/runtime/release-receipts.d.ts +15 -3
- package/dist/runtime/release-receipts.js +27 -1
- package/dist/services/session-storage.d.ts +9 -0
- package/dist/services/session-storage.js +27 -1
- package/dist/web/git-compare-service.d.ts +45 -0
- package/dist/web/git-compare-service.js +160 -0
- package/dist/web/git-diff-document.d.ts +83 -0
- package/dist/web/git-diff-document.js +214 -0
- package/dist/web/git-file-source.d.ts +25 -0
- package/dist/web/git-file-source.js +21 -0
- package/dist/web/git-history-service.d.ts +151 -0
- package/dist/web/git-history-service.js +610 -0
- package/dist/web/git-index-transaction.d.ts +45 -0
- package/dist/web/git-index-transaction.js +174 -0
- package/dist/web/git-patch-builder.d.ts +30 -0
- package/dist/web/git-patch-builder.js +138 -0
- package/dist/web/git-read-model-service.d.ts +338 -18
- package/dist/web/git-read-model-service.js +1039 -110
- package/dist/web/git-version-service.d.ts +71 -0
- package/dist/web/git-version-service.js +255 -0
- package/dist/web/native-directory-picker.d.ts +63 -0
- package/dist/web/native-directory-picker.js +169 -0
- package/dist/web/protocol.d.ts +51 -2
- package/dist/web/server.js +384 -8
- package/dist/web/workbench-controller.d.ts +126 -10
- package/dist/web/workbench-controller.js +383 -15
- package/dist/web/workspace-open-telemetry.d.ts +55 -0
- package/dist/web/workspace-open-telemetry.js +73 -0
- package/dist/web-client/assets/FilesPanel-BARQCmne.js +1 -0
- package/dist/web-client/assets/GitPanel-DjB9eSiJ.js +4 -0
- package/dist/web-client/assets/ReviewPanel-YhEGlp-G.js +5 -0
- package/dist/web-client/assets/{TerminalPanel-BpsLphLv.js → TerminalPanel-DnWh3Vp2.js} +1 -1
- package/dist/web-client/assets/{index-D3sxrAh_.css → index--hnGXXjj.css} +1 -1
- package/dist/web-client/assets/index-Drja0_BT.js +20 -0
- package/dist/web-client/assets/{useAutoNotice-BeSf9Dfq.js → useAutoNotice-DfQi4G1X.js} +1 -1
- package/dist/web-client/index.html +2 -2
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/dist/web-client/assets/DiffViewer-C0A42Lma.js +0 -5
- package/dist/web-client/assets/FilesPanel-DiXKnFMj.js +0 -1
- package/dist/web-client/assets/GitPanel--_Yrbldp.js +0 -1
- package/dist/web-client/assets/ReviewPanel-BilzL9Ma.js +0 -1
- package/dist/web-client/assets/index-C2M48tcK.js +0 -20
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export type GitVersionLabelV1 = 'base' | 'ours' | 'theirs' | 'worktree' | 'revision';
|
|
2
|
+
export interface GitFileVersionV1 {
|
|
3
|
+
readonly label: GitVersionLabelV1;
|
|
4
|
+
/** The blob OID, or null when this version does not exist for this conflict shape. */
|
|
5
|
+
readonly oid: string | null;
|
|
6
|
+
readonly exists: boolean;
|
|
7
|
+
readonly binary: boolean;
|
|
8
|
+
readonly byteSize: number;
|
|
9
|
+
readonly truncated: boolean;
|
|
10
|
+
/** Text content, or null when absent, binary or over the limit. */
|
|
11
|
+
readonly content: string | null;
|
|
12
|
+
/**
|
|
13
|
+
* Set when the content is an LFS pointer: the pointer is shown as state, and the object is
|
|
14
|
+
* never fetched.
|
|
15
|
+
*/
|
|
16
|
+
readonly lfs: {
|
|
17
|
+
readonly oid: string;
|
|
18
|
+
readonly size: number | null;
|
|
19
|
+
} | null;
|
|
20
|
+
/**
|
|
21
|
+
* v0.3.17 S6 — a renderable data URL when this version is a bounded, known image.
|
|
22
|
+
* Built from bytes already read locally; the plan forbids fetching anything.
|
|
23
|
+
*/
|
|
24
|
+
readonly dataUrl?: string | null;
|
|
25
|
+
}
|
|
26
|
+
export type GitConflictShapeV1 = 'both-modified' | 'both-added' | 'added-by-us' | 'added-by-them' | 'deleted-by-us' | 'deleted-by-them' | 'both-deleted' | 'other';
|
|
27
|
+
export interface GitConflictVersionsV1 {
|
|
28
|
+
readonly path: string;
|
|
29
|
+
readonly shape: GitConflictShapeV1;
|
|
30
|
+
readonly base: GitFileVersionV1;
|
|
31
|
+
readonly ours: GitFileVersionV1;
|
|
32
|
+
readonly theirs: GitFileVersionV1;
|
|
33
|
+
readonly worktree: GitFileVersionV1;
|
|
34
|
+
}
|
|
35
|
+
export interface GitBlobResultV1 {
|
|
36
|
+
readonly path: string;
|
|
37
|
+
readonly rev: string;
|
|
38
|
+
readonly oid: string;
|
|
39
|
+
readonly binary: boolean;
|
|
40
|
+
readonly byteSize: number;
|
|
41
|
+
readonly truncated: boolean;
|
|
42
|
+
readonly content: readonly string[];
|
|
43
|
+
readonly lfs: {
|
|
44
|
+
readonly oid: string;
|
|
45
|
+
readonly size: number | null;
|
|
46
|
+
} | null;
|
|
47
|
+
/** Present only when the blob is a bounded, known image. */
|
|
48
|
+
readonly dataUrl: string | null;
|
|
49
|
+
}
|
|
50
|
+
export interface GitSubmoduleEntryV1 {
|
|
51
|
+
readonly path: string;
|
|
52
|
+
readonly gitlinkOid: string;
|
|
53
|
+
/** Never inspected: plan G6 forbids traversing submodules to compute this. */
|
|
54
|
+
readonly dirtySummary: null;
|
|
55
|
+
readonly dirtyNote: string;
|
|
56
|
+
}
|
|
57
|
+
export interface GitVersionReader {
|
|
58
|
+
conflict(path: string): Promise<GitConflictVersionsV1>;
|
|
59
|
+
blob(input: {
|
|
60
|
+
readonly path: string;
|
|
61
|
+
readonly rev: string;
|
|
62
|
+
}): Promise<GitBlobResultV1>;
|
|
63
|
+
submodules(): Promise<readonly GitSubmoduleEntryV1[]>;
|
|
64
|
+
}
|
|
65
|
+
export interface GitVersionReaderOptions {
|
|
66
|
+
readonly runGit: (args: readonly string[]) => Promise<string>;
|
|
67
|
+
/** Runs git and returns raw bytes, for content that may not be valid UTF-8. */
|
|
68
|
+
readonly runGitBytes: (args: readonly string[]) => Promise<Buffer>;
|
|
69
|
+
}
|
|
70
|
+
export declare function createGitVersionReader(options: GitVersionReaderOptions): GitVersionReader;
|
|
71
|
+
//# sourceMappingURL=git-version-service.d.ts.map
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createGitVersionReader = createGitVersionReader;
|
|
4
|
+
/**
|
|
5
|
+
* v0.3.17 S5 — historical file versions: conflict stages, arbitrary-revision blobs, LFS
|
|
6
|
+
* pointer state and gitlink (submodule) entries.
|
|
7
|
+
*
|
|
8
|
+
* Plan G5/G6 constraints this module is responsible for:
|
|
9
|
+
* - a conflicted file must NOT be rendered as an ordinary staged diff: base / ours / theirs
|
|
10
|
+
* come from index stages 1/2/3, and a version that legitimately does not exist (add/add,
|
|
11
|
+
* modify/delete) is reported as absent rather than silently omitted;
|
|
12
|
+
* - a file that no longer exists in the working tree can still be read at a revision
|
|
13
|
+
* ("view that version");
|
|
14
|
+
* - LFS only reads local objects or reports pointer state — nothing is fetched;
|
|
15
|
+
* - submodules are never traversed or switched: only the recorded gitlink SHAs are read, and
|
|
16
|
+
* the dirty summary is explicitly reported as not inspected.
|
|
17
|
+
*/
|
|
18
|
+
const redaction_1 = require("../services/redaction");
|
|
19
|
+
const errors_1 = require("./errors");
|
|
20
|
+
/** Upper bound on the bytes returned for one file version. */
|
|
21
|
+
const MAX_VERSION_BYTES = 256 * 1024;
|
|
22
|
+
/** Bytes inspected when deciding whether a file is an LFS pointer. */
|
|
23
|
+
const LFS_PROBE_BYTES = 512;
|
|
24
|
+
function assertRelativePath(value, name = 'path') {
|
|
25
|
+
if (typeof value !== 'string' || !value || value.length > 1024) {
|
|
26
|
+
throw new errors_1.WebWorkbenchError(400, `A ${name} is required.`, 'git_path_invalid');
|
|
27
|
+
}
|
|
28
|
+
if (value.startsWith('/') || value.startsWith(':') || value.includes('\0')) {
|
|
29
|
+
throw new errors_1.WebWorkbenchError(400, 'Unsafe Git path.', 'git_path_unsafe');
|
|
30
|
+
}
|
|
31
|
+
if (value.split('/').some(segment => segment === '..' || segment === '.' || segment === '')) {
|
|
32
|
+
throw new errors_1.WebWorkbenchError(400, 'Unsafe Git path.', 'git_path_unsafe');
|
|
33
|
+
}
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
function assertRev(value) {
|
|
37
|
+
// A revision expression here would be an injection surface; a plain ref or hash is enough.
|
|
38
|
+
if (typeof value !== 'string' || !/^[0-9A-Za-z][0-9A-Za-z._/-]{0,250}$/u.test(value)) {
|
|
39
|
+
throw new errors_1.WebWorkbenchError(400, 'rev must be a ref name or commit hash.', 'git_ref_invalid');
|
|
40
|
+
}
|
|
41
|
+
if (value.includes('..')) {
|
|
42
|
+
throw new errors_1.WebWorkbenchError(400, 'rev must be a ref name or commit hash.', 'git_ref_invalid');
|
|
43
|
+
}
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Recognises the image formats the browser can render from a data URL, by magic bytes —
|
|
48
|
+
* never by extension, which is user-controlled and proves nothing.
|
|
49
|
+
*/
|
|
50
|
+
const IMAGE_SIGNATURES = Object.freeze([
|
|
51
|
+
{ offset: 0, bytes: [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a], mime: 'image/png' },
|
|
52
|
+
{ offset: 0, bytes: [0xff, 0xd8, 0xff], mime: 'image/jpeg' },
|
|
53
|
+
{ offset: 0, bytes: [0x47, 0x49, 0x46, 0x38], mime: 'image/gif' },
|
|
54
|
+
{ offset: 0, bytes: [0x52, 0x49, 0x46, 0x46], mime: 'image/webp' },
|
|
55
|
+
]);
|
|
56
|
+
/** Largest image that may be inlined as a data URL (bounded, and nothing is fetched). */
|
|
57
|
+
const MAX_IMAGE_BYTES = 2 * 1024 * 1024;
|
|
58
|
+
/**
|
|
59
|
+
* Returns a renderable data URL when the bytes are a known image, so the UI can show the
|
|
60
|
+
* two versions of a picture side by side. The bytes were already read locally — the plan
|
|
61
|
+
* forbids fetching anything.
|
|
62
|
+
*/
|
|
63
|
+
function imageDataUrl(bytes) {
|
|
64
|
+
for (const signature of IMAGE_SIGNATURES) {
|
|
65
|
+
if (bytes.length < signature.offset + signature.bytes.length)
|
|
66
|
+
continue;
|
|
67
|
+
const matches = signature.bytes.every((byte, index) => bytes[signature.offset + index] === byte);
|
|
68
|
+
if (matches) {
|
|
69
|
+
return `data:${signature.mime};base64,${bytes.toString('base64')}`;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
/** Detects an LFS pointer and extracts its state. Never fetches anything. */
|
|
75
|
+
function parseLfsPointer(head) {
|
|
76
|
+
if (!head.startsWith('version https://git-lfs.github.com/spec/'))
|
|
77
|
+
return null;
|
|
78
|
+
const oidMatch = /^oid sha256:([0-9a-f]{64})$/mu.exec(head);
|
|
79
|
+
if (!oidMatch)
|
|
80
|
+
return null;
|
|
81
|
+
const sizeMatch = /^size (\d+)$/mu.exec(head);
|
|
82
|
+
return Object.freeze({
|
|
83
|
+
oid: oidMatch[1],
|
|
84
|
+
size: sizeMatch ? Number(sizeMatch[1]) : null,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
function createGitVersionReader(options) {
|
|
88
|
+
const { runGit, runGitBytes } = options;
|
|
89
|
+
/**
|
|
90
|
+
* Reads one blob by OID, bounded, with binary and LFS detection.
|
|
91
|
+
*
|
|
92
|
+
* The size is read first so an enormous historical file is reported by its size instead of
|
|
93
|
+
* being pulled into memory just to be truncated.
|
|
94
|
+
*/
|
|
95
|
+
const readVersion = async (label, oid) => {
|
|
96
|
+
if (oid === null) {
|
|
97
|
+
return Object.freeze({
|
|
98
|
+
label,
|
|
99
|
+
oid: null,
|
|
100
|
+
exists: false,
|
|
101
|
+
binary: false,
|
|
102
|
+
byteSize: 0,
|
|
103
|
+
truncated: false,
|
|
104
|
+
content: null,
|
|
105
|
+
lfs: null,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
const sizeRaw = (await runGit(['cat-file', '-s', oid])).trim();
|
|
109
|
+
const byteSize = Number(sizeRaw) || 0;
|
|
110
|
+
if (byteSize > MAX_VERSION_BYTES) {
|
|
111
|
+
return Object.freeze({
|
|
112
|
+
label,
|
|
113
|
+
oid,
|
|
114
|
+
exists: true,
|
|
115
|
+
binary: false,
|
|
116
|
+
byteSize,
|
|
117
|
+
truncated: true,
|
|
118
|
+
content: null,
|
|
119
|
+
lfs: null,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
const bytes = await runGitBytes(['cat-file', 'blob', oid]);
|
|
123
|
+
const probe = bytes.subarray(0, LFS_PROBE_BYTES).toString('utf8');
|
|
124
|
+
const lfs = parseLfsPointer(probe);
|
|
125
|
+
if (lfs) {
|
|
126
|
+
return Object.freeze({
|
|
127
|
+
label,
|
|
128
|
+
oid,
|
|
129
|
+
exists: true,
|
|
130
|
+
binary: false,
|
|
131
|
+
byteSize,
|
|
132
|
+
truncated: false,
|
|
133
|
+
content: probe.trimEnd(),
|
|
134
|
+
lfs,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
const binary = bytes.includes(0);
|
|
138
|
+
// Only a bounded known image becomes a data URL; anything else stays an opaque count.
|
|
139
|
+
const dataUrl = binary && byteSize <= MAX_IMAGE_BYTES ? imageDataUrl(bytes) : null;
|
|
140
|
+
return Object.freeze({
|
|
141
|
+
label,
|
|
142
|
+
oid,
|
|
143
|
+
exists: true,
|
|
144
|
+
binary,
|
|
145
|
+
byteSize,
|
|
146
|
+
truncated: false,
|
|
147
|
+
content: binary ? null : bytes.toString('utf8'),
|
|
148
|
+
lfs: null,
|
|
149
|
+
dataUrl,
|
|
150
|
+
});
|
|
151
|
+
};
|
|
152
|
+
return {
|
|
153
|
+
async conflict(pathInput) {
|
|
154
|
+
const path = assertRelativePath(pathInput);
|
|
155
|
+
if ((0, redaction_1.isSensitiveFilePath)(path)) {
|
|
156
|
+
throw new errors_1.WebWorkbenchError(403, 'Sensitive file content is not available in the Web Workbench.', 'sensitive_file_blocked');
|
|
157
|
+
}
|
|
158
|
+
// `ls-files -u` lists one entry per unmerged stage, which is the authoritative record
|
|
159
|
+
// of what the conflict actually contains. Stages absent here do not exist.
|
|
160
|
+
const raw = await runGit(['ls-files', '-u', '-z', '--', path]);
|
|
161
|
+
const stageOids = new Map();
|
|
162
|
+
for (const record of raw.split('\0')) {
|
|
163
|
+
if (!record)
|
|
164
|
+
continue;
|
|
165
|
+
const [meta] = record.split('\t');
|
|
166
|
+
const [mode, oid, stage] = meta.split(' ');
|
|
167
|
+
if (!mode || !oid || !stage)
|
|
168
|
+
continue;
|
|
169
|
+
stageOids.set(Number(stage), oid);
|
|
170
|
+
}
|
|
171
|
+
const worktreeOid = (await runGit(['hash-object', '--path=' + path, '--', path]).catch(() => ''))
|
|
172
|
+
.toString()
|
|
173
|
+
.trim();
|
|
174
|
+
const worktreeExists = /^[0-9a-f]{7,64}$/iu.test(worktreeOid);
|
|
175
|
+
const [base, ours, theirs, worktree] = await Promise.all([
|
|
176
|
+
readVersion('base', stageOids.get(1) ?? null),
|
|
177
|
+
readVersion('ours', stageOids.get(2) ?? null),
|
|
178
|
+
readVersion('theirs', stageOids.get(3) ?? null),
|
|
179
|
+
worktreeExists ? readVersion('worktree', worktreeOid) : readVersion('worktree', null),
|
|
180
|
+
]);
|
|
181
|
+
const hasBase = stageOids.has(1);
|
|
182
|
+
const hasOurs = stageOids.has(2);
|
|
183
|
+
const hasTheirs = stageOids.has(3);
|
|
184
|
+
let shape = 'other';
|
|
185
|
+
if (hasBase && hasOurs && hasTheirs)
|
|
186
|
+
shape = 'both-modified';
|
|
187
|
+
else if (!hasBase && hasOurs && hasTheirs)
|
|
188
|
+
shape = 'both-added';
|
|
189
|
+
// The delete shapes must be tested BEFORE the add shapes: a modify/delete conflict also
|
|
190
|
+
// has exactly one side present, so a loose `hasOurs && !hasTheirs` would swallow it and
|
|
191
|
+
// mislabel a deletion as an addition. A missing side only means 'added' when there is no
|
|
192
|
+
// base to delete from.
|
|
193
|
+
else if (hasBase && !hasOurs && hasTheirs)
|
|
194
|
+
shape = 'deleted-by-us';
|
|
195
|
+
else if (hasBase && hasOurs && !hasTheirs)
|
|
196
|
+
shape = 'deleted-by-them';
|
|
197
|
+
else if (hasOurs && !hasTheirs)
|
|
198
|
+
shape = 'added-by-us';
|
|
199
|
+
else if (!hasOurs && hasTheirs)
|
|
200
|
+
shape = 'added-by-them';
|
|
201
|
+
else if (hasBase && !hasOurs && !hasTheirs)
|
|
202
|
+
shape = 'both-deleted';
|
|
203
|
+
return Object.freeze({ path, shape, base, ours, theirs, worktree });
|
|
204
|
+
},
|
|
205
|
+
async blob(input) {
|
|
206
|
+
const path = assertRelativePath(input.path);
|
|
207
|
+
const rev = assertRev(input.rev);
|
|
208
|
+
if ((0, redaction_1.isSensitiveFilePath)(path)) {
|
|
209
|
+
throw new errors_1.WebWorkbenchError(403, 'Sensitive file content is not available in the Web Workbench.', 'sensitive_file_blocked');
|
|
210
|
+
}
|
|
211
|
+
// `rev:path` is a tree lookup, not a working-tree read: a file deleted since that
|
|
212
|
+
// revision is still readable, which is the whole point of "view that version".
|
|
213
|
+
const oid = (await runGit(['rev-parse', '--verify', '--quiet', `${rev}:${path}`]).catch(() => ''))
|
|
214
|
+
.toString()
|
|
215
|
+
.trim();
|
|
216
|
+
if (!/^[0-9a-f]{7,64}$/iu.test(oid)) {
|
|
217
|
+
throw new errors_1.WebWorkbenchError(404, 'That file does not exist at the requested revision.', 'git_blob_not_found');
|
|
218
|
+
}
|
|
219
|
+
const version = await readVersion('revision', oid);
|
|
220
|
+
return Object.freeze({
|
|
221
|
+
path,
|
|
222
|
+
rev,
|
|
223
|
+
oid,
|
|
224
|
+
binary: version.binary,
|
|
225
|
+
byteSize: version.byteSize,
|
|
226
|
+
truncated: version.truncated,
|
|
227
|
+
content: Object.freeze((version.content ?? '').split('\n')),
|
|
228
|
+
lfs: version.lfs,
|
|
229
|
+
dataUrl: version.dataUrl ?? null,
|
|
230
|
+
});
|
|
231
|
+
},
|
|
232
|
+
async submodules() {
|
|
233
|
+
// Only the recorded gitlink SHAs. `git submodule status` is deliberately NOT run: it
|
|
234
|
+
// traverses every submodule, which plan G6 forbids.
|
|
235
|
+
const raw = await runGit(['ls-files', '--stage', '-z']);
|
|
236
|
+
const entries = [];
|
|
237
|
+
for (const record of raw.split('\0')) {
|
|
238
|
+
if (!record)
|
|
239
|
+
continue;
|
|
240
|
+
const [meta, filePath] = record.split('\t');
|
|
241
|
+
const [mode, oid] = (meta ?? '').split(' ');
|
|
242
|
+
if (mode !== '160000' || !oid || !filePath)
|
|
243
|
+
continue;
|
|
244
|
+
entries.push(Object.freeze({
|
|
245
|
+
path: filePath,
|
|
246
|
+
gitlinkOid: oid,
|
|
247
|
+
dirtySummary: null,
|
|
248
|
+
dirtyNote: '未检测:不遍历子模块(计划 G6)。',
|
|
249
|
+
}));
|
|
250
|
+
}
|
|
251
|
+
return Object.freeze(entries);
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
//# sourceMappingURL=git-version-service.js.map
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export type NativeDirectoryPickResult = {
|
|
2
|
+
readonly kind: 'selected';
|
|
3
|
+
readonly path: string;
|
|
4
|
+
} | {
|
|
5
|
+
readonly kind: 'cancelled';
|
|
6
|
+
} | {
|
|
7
|
+
readonly kind: 'unavailable';
|
|
8
|
+
readonly reason: string;
|
|
9
|
+
};
|
|
10
|
+
export interface NativeDirectoryPickerInput {
|
|
11
|
+
readonly title?: string;
|
|
12
|
+
readonly initialPath?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface NativeDirectoryPicker {
|
|
15
|
+
pickDirectory(input: NativeDirectoryPickerInput): Promise<NativeDirectoryPickResult>;
|
|
16
|
+
}
|
|
17
|
+
export interface DirectoryPickerCommand {
|
|
18
|
+
readonly file: string;
|
|
19
|
+
readonly args: readonly string[];
|
|
20
|
+
}
|
|
21
|
+
/** Injectable process runner so the picker can be unit-tested without a GUI. */
|
|
22
|
+
export type DirectoryPickerRunner = (command: DirectoryPickerCommand) => Promise<{
|
|
23
|
+
readonly stdout: string;
|
|
24
|
+
}>;
|
|
25
|
+
export interface NativeDirectoryPickerOptions {
|
|
26
|
+
readonly platform?: NodeJS.Platform;
|
|
27
|
+
readonly runner?: DirectoryPickerRunner;
|
|
28
|
+
/** Reject absurd paths from a misbehaving picker. */
|
|
29
|
+
readonly maxPathLength?: number;
|
|
30
|
+
readonly timeoutMs?: number;
|
|
31
|
+
}
|
|
32
|
+
/** Why a picker invocation could not produce a directory. */
|
|
33
|
+
export type DirectoryPickerFailureReason = 'picker_unavailable' | 'picker_timeout';
|
|
34
|
+
export declare function parseNativePickerStdout(stdout: string, maxPathLength: number): NativeDirectoryPickResult;
|
|
35
|
+
export declare function createNativeDirectoryPicker(options?: NativeDirectoryPickerOptions): NativeDirectoryPicker;
|
|
36
|
+
/**
|
|
37
|
+
* v0.3.17 — scripted picker for end-to-end tests.
|
|
38
|
+
*
|
|
39
|
+
* CI must never pop a real Finder dialog, so E2E points the Host at a JSON
|
|
40
|
+
* script file instead. The script is re-read on every invocation, which lets a
|
|
41
|
+
* test change the next answer between clicks:
|
|
42
|
+
*
|
|
43
|
+
* ```json
|
|
44
|
+
* { "path": "/abs/dir" } // selected
|
|
45
|
+
* { "paths": ["/a", "/b"] } // sequential, last one repeats
|
|
46
|
+
* { "outcome": "cancelled" } // user cancelled
|
|
47
|
+
* { "outcome": "unavailable", "reason": "..." } // no picker on this platform
|
|
48
|
+
* ```
|
|
49
|
+
*
|
|
50
|
+
* This is a browse-only seam: the returned path still has to be confirmed on
|
|
51
|
+
* the card and passes through the same guarded activation flow, so it can never
|
|
52
|
+
* cause an unattended workspace switch.
|
|
53
|
+
*/
|
|
54
|
+
export interface FixturePickerScript {
|
|
55
|
+
readonly path?: string;
|
|
56
|
+
readonly paths?: readonly string[];
|
|
57
|
+
readonly outcome?: 'cancelled' | 'unavailable';
|
|
58
|
+
readonly reason?: string;
|
|
59
|
+
}
|
|
60
|
+
export declare function createFixtureDirectoryPicker(readScript: () => FixturePickerScript): NativeDirectoryPicker;
|
|
61
|
+
/** Reads the script from `ORION_CODE_WEB_PICKER_FIXTURE`; absent means real. */
|
|
62
|
+
export declare function fixturePickerFromEnvironment(env?: NodeJS.ProcessEnv): NativeDirectoryPicker | undefined;
|
|
63
|
+
//# sourceMappingURL=native-directory-picker.d.ts.map
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseNativePickerStdout = parseNativePickerStdout;
|
|
4
|
+
exports.createNativeDirectoryPicker = createNativeDirectoryPicker;
|
|
5
|
+
exports.createFixtureDirectoryPicker = createFixtureDirectoryPicker;
|
|
6
|
+
exports.fixturePickerFromEnvironment = fixturePickerFromEnvironment;
|
|
7
|
+
/**
|
|
8
|
+
* v0.3.16 — Host-side native directory picker.
|
|
9
|
+
*
|
|
10
|
+
* The browser cannot hand the local Node Host a real filesystem path (no such
|
|
11
|
+
* API exists, by design), so opening a *new* local project has to start from
|
|
12
|
+
* the Host. This adapter shells out to the OS picker and returns the chosen
|
|
13
|
+
* directory — nothing else. It never writes to the workspace registry, never
|
|
14
|
+
* activates a Context and never reads file contents.
|
|
15
|
+
*
|
|
16
|
+
* Safety contract:
|
|
17
|
+
* - The AppleScript is a fixed constant. The prompt and the optional default
|
|
18
|
+
* location travel as `argv`, so a path can never be interpolated into a
|
|
19
|
+
* script (or a shell) string.
|
|
20
|
+
* - We use `execFile`, not `exec`/`spawn(shell: true)`, so no shell parses the
|
|
21
|
+
* arguments.
|
|
22
|
+
* - Cancellation is a normal outcome, not an error. Unexpected output is
|
|
23
|
+
* reported as `unavailable` with a fixed reason code — never echoed back,
|
|
24
|
+
* because picker output may contain a path the user has not confirmed.
|
|
25
|
+
*/
|
|
26
|
+
const node_child_process_1 = require("node:child_process");
|
|
27
|
+
const node_fs_1 = require("node:fs");
|
|
28
|
+
/** Fixed markers so we can tell outcomes apart without pattern-matching paths. */
|
|
29
|
+
const MARKER_SELECTED = '__ORION_PICK_SELECTED__';
|
|
30
|
+
const MARKER_CANCELLED = '__ORION_PICK_CANCELLED__';
|
|
31
|
+
const MARKER_ERROR = '__ORION_PICK_ERROR__';
|
|
32
|
+
const DEFAULT_TITLE = '选择要打开的本地项目文件夹';
|
|
33
|
+
const DEFAULT_MAX_PATH_LENGTH = 4096;
|
|
34
|
+
const DEFAULT_TIMEOUT_MS = 5 * 60 * 1000;
|
|
35
|
+
/**
|
|
36
|
+
* `choose folder` blocks until the user acts. Cancelling raises AppleScript
|
|
37
|
+
* error -128, which we translate into the CANCELLED marker so the process
|
|
38
|
+
* exits 0 and the caller can treat it as a normal outcome.
|
|
39
|
+
*/
|
|
40
|
+
const MACOS_PICK_SCRIPT = [
|
|
41
|
+
'on run argv',
|
|
42
|
+
' set promptText to item 1 of argv',
|
|
43
|
+
' set defaultPath to item 2 of argv',
|
|
44
|
+
' try',
|
|
45
|
+
' if defaultPath is "" then',
|
|
46
|
+
' set chosen to choose folder with prompt promptText',
|
|
47
|
+
' else',
|
|
48
|
+
' set chosen to choose folder with prompt promptText default location (POSIX file defaultPath)',
|
|
49
|
+
' end if',
|
|
50
|
+
' on error errMsg number errNum',
|
|
51
|
+
' if errNum is -128 then',
|
|
52
|
+
` return "${MARKER_CANCELLED}"`,
|
|
53
|
+
' end if',
|
|
54
|
+
` return "${MARKER_ERROR}"`,
|
|
55
|
+
' end try',
|
|
56
|
+
` return "${MARKER_SELECTED}" & (POSIX path of chosen)`,
|
|
57
|
+
'end run',
|
|
58
|
+
].join('\n');
|
|
59
|
+
/**
|
|
60
|
+
* The default runner kills the child process when `timeoutMs` elapses, which is
|
|
61
|
+
* a distinct outcome from "this platform has no picker". It is surfaced as a
|
|
62
|
+
* fixed reason code so callers never have to parse an error message.
|
|
63
|
+
*/
|
|
64
|
+
const createDefaultRunner = (timeoutMs) => command => new Promise((resolve, reject) => {
|
|
65
|
+
(0, node_child_process_1.execFile)(command.file, [...command.args], { timeout: timeoutMs, maxBuffer: 64 * 1024, windowsHide: true }, (error, stdout) => {
|
|
66
|
+
if (error) {
|
|
67
|
+
const failure = new Error('native directory picker failed');
|
|
68
|
+
failure.reason = error.killed
|
|
69
|
+
? 'picker_timeout'
|
|
70
|
+
: 'picker_unavailable';
|
|
71
|
+
reject(failure);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
resolve({ stdout: typeof stdout === 'string' ? stdout : String(stdout ?? '') });
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
/**
|
|
78
|
+
* `POSIX path of` always appends a trailing slash; the registry compares
|
|
79
|
+
* canonical paths exactly, so normalize here rather than downstream.
|
|
80
|
+
*/
|
|
81
|
+
function stripTrailingSeparator(path) {
|
|
82
|
+
if (path.length <= 1)
|
|
83
|
+
return path;
|
|
84
|
+
return path.replace(/\/+$/u, '') || '/';
|
|
85
|
+
}
|
|
86
|
+
function parseNativePickerStdout(stdout, maxPathLength) {
|
|
87
|
+
const text = stdout.trim();
|
|
88
|
+
if (text === MARKER_CANCELLED)
|
|
89
|
+
return { kind: 'cancelled' };
|
|
90
|
+
if (text.startsWith(MARKER_SELECTED)) {
|
|
91
|
+
const path = stripTrailingSeparator(text.slice(MARKER_SELECTED.length).trim());
|
|
92
|
+
if (!path || path.length > maxPathLength) {
|
|
93
|
+
return { kind: 'unavailable', reason: 'picker_returned_invalid_path' };
|
|
94
|
+
}
|
|
95
|
+
return { kind: 'selected', path };
|
|
96
|
+
}
|
|
97
|
+
if (text.startsWith(MARKER_ERROR)) {
|
|
98
|
+
return { kind: 'unavailable', reason: 'picker_failed' };
|
|
99
|
+
}
|
|
100
|
+
return { kind: 'unavailable', reason: 'picker_unexpected_output' };
|
|
101
|
+
}
|
|
102
|
+
function createNativeDirectoryPicker(options = {}) {
|
|
103
|
+
const platform = options.platform ?? process.platform;
|
|
104
|
+
const runner = options.runner ?? createDefaultRunner(options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
105
|
+
const maxPathLength = options.maxPathLength ?? DEFAULT_MAX_PATH_LENGTH;
|
|
106
|
+
return {
|
|
107
|
+
async pickDirectory(input) {
|
|
108
|
+
// Only macOS has an implemented adapter. Other platforms answer
|
|
109
|
+
// explicitly instead of pretending a directory was chosen, so the UI can
|
|
110
|
+
// fall back to the advanced manual path entry.
|
|
111
|
+
if (platform !== 'darwin') {
|
|
112
|
+
return { kind: 'unavailable', reason: 'picker_unavailable' };
|
|
113
|
+
}
|
|
114
|
+
const title = input.title?.trim() || DEFAULT_TITLE;
|
|
115
|
+
const initialPath = input.initialPath?.trim() ?? '';
|
|
116
|
+
let stdout;
|
|
117
|
+
try {
|
|
118
|
+
({ stdout } = await runner({
|
|
119
|
+
file: 'osascript',
|
|
120
|
+
args: ['-e', MACOS_PICK_SCRIPT, title, initialPath],
|
|
121
|
+
}));
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
const reason = error?.reason;
|
|
125
|
+
return {
|
|
126
|
+
kind: 'unavailable',
|
|
127
|
+
reason: reason === 'picker_timeout' ? 'picker_timeout' : 'picker_unavailable',
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
return parseNativePickerStdout(stdout, maxPathLength);
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function createFixtureDirectoryPicker(readScript) {
|
|
135
|
+
let cursor = 0;
|
|
136
|
+
return {
|
|
137
|
+
async pickDirectory() {
|
|
138
|
+
const script = readScript();
|
|
139
|
+
const paths = script.paths ?? (script.path ? [script.path] : []);
|
|
140
|
+
if (paths.length > 0) {
|
|
141
|
+
const next = paths[Math.min(cursor, paths.length - 1)];
|
|
142
|
+
cursor += 1;
|
|
143
|
+
return next ? { kind: 'selected', path: next } : { kind: 'cancelled' };
|
|
144
|
+
}
|
|
145
|
+
if (script.outcome === 'unavailable') {
|
|
146
|
+
return { kind: 'unavailable', reason: script.reason ?? 'picker_unavailable' };
|
|
147
|
+
}
|
|
148
|
+
return { kind: 'cancelled' };
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
/** Reads the script from `ORION_CODE_WEB_PICKER_FIXTURE`; absent means real. */
|
|
153
|
+
function fixturePickerFromEnvironment(env = process.env) {
|
|
154
|
+
const scriptPath = env.ORION_CODE_WEB_PICKER_FIXTURE?.trim();
|
|
155
|
+
if (!scriptPath)
|
|
156
|
+
return undefined;
|
|
157
|
+
return createFixtureDirectoryPicker(() => {
|
|
158
|
+
try {
|
|
159
|
+
const parsed = JSON.parse((0, node_fs_1.readFileSync)(scriptPath, 'utf8'));
|
|
160
|
+
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
// A missing or malformed script behaves like a cancelled dialog rather
|
|
164
|
+
// than failing the request.
|
|
165
|
+
return {};
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
//# sourceMappingURL=native-directory-picker.js.map
|
package/dist/web/protocol.d.ts
CHANGED
|
@@ -8,7 +8,8 @@ import type { ToolDetailPage, ToolDetailSummary } from '../runtime/tool-detail-r
|
|
|
8
8
|
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
|
-
import type { WebGitCommitV1, WebGitDiffPageV1, WebGitFileV1, WebGitLogPageV1, WebGitStatusV1 } from './git-read-model-service';
|
|
11
|
+
import type { GitCommitIdentityV1, GitCommitOutcomeV1, GitCommitPreviewV1, GitIndexEntryV1, GitMutationResultV1, GitWorktreeSourceV1, WebGitCommitV1, WebGitDiffPageV1, WebGitFileSourceV1, WebGitFileV1, WebGitLogPageV1, WebGitStatusCountsV1, WebGitStatusV1 } from './git-read-model-service';
|
|
12
|
+
import type { GitDiffCapabilitiesV2, GitDiffDocumentV2, GitDiffDocumentKindV2, GitDiffLineKindV2, GitDiffLineV2, GitHunkV2 } from './git-diff-document';
|
|
12
13
|
import type { WebReviewSnapshotV1 } from './review-service';
|
|
13
14
|
import type { WebSessionRuntimeSummaryV1 } from './session-runtime-registry';
|
|
14
15
|
import type { WebTerminalCreateResultV1, WebTerminalExitV1, WebTerminalGapV1, WebTerminalMetadataV1, WebTerminalOutputFrameV1, WebTerminalStateV1 } from './terminal-manager';
|
|
@@ -373,10 +374,58 @@ export interface WebWorkspaceProjectSummaryV1 {
|
|
|
373
374
|
readonly conflictCount: number;
|
|
374
375
|
}
|
|
375
376
|
export type WebWorkspaceResourceV1 = 'files' | 'git' | 'review';
|
|
377
|
+
/** v0.3.16 — LOCAL WORKSPACE picker contracts (read-only until confirm). */
|
|
378
|
+
export type WebDirectoryPickOutcomeV1 = 'selected' | 'cancelled' | 'unavailable';
|
|
379
|
+
export interface WebDirectoryPickResultV1 {
|
|
380
|
+
readonly outcome: WebDirectoryPickOutcomeV1;
|
|
381
|
+
/** Present only when `outcome === 'selected'`. */
|
|
382
|
+
readonly path?: string;
|
|
383
|
+
/** Operator-readable reason code; present only when `outcome === 'unavailable'`. */
|
|
384
|
+
readonly reason?: string;
|
|
385
|
+
}
|
|
386
|
+
export interface WebDirectoryPickRequestV1 {
|
|
387
|
+
readonly requestId: string;
|
|
388
|
+
readonly title?: string;
|
|
389
|
+
readonly initialPath?: string;
|
|
390
|
+
}
|
|
391
|
+
export type WebWorkspaceAvailabilityV1 = 'available' | 'missing' | 'not_directory' | 'unreadable';
|
|
392
|
+
export type WebWorkspaceKindV1 = 'git' | 'folder';
|
|
393
|
+
export type WebWorkspaceCandidateSourceV1 = 'picker' | 'recent' | 'pinned' | 'discovered' | 'manual';
|
|
394
|
+
/**
|
|
395
|
+
* v0.3.17 — `deferred` means the picker deliberately did not wait for the
|
|
396
|
+
* session catalog to rebuild; the count may arrive later or not at all. It must
|
|
397
|
+
* never be rendered as a spinner that blocks the confirmation card.
|
|
398
|
+
*/
|
|
399
|
+
export type WebWorkspaceSessionCountStatusV1 = 'ready' | 'deferred';
|
|
400
|
+
/**
|
|
401
|
+
* A read-only description of a directory the user *may* open. Inspecting a
|
|
402
|
+
* path never registers it, never activates a Context and never reads file
|
|
403
|
+
* contents — only `stat` plus the existence of a few project markers.
|
|
404
|
+
*/
|
|
405
|
+
export interface WebWorkspaceCandidateV1 {
|
|
406
|
+
readonly canonicalPath: string;
|
|
407
|
+
readonly label: string;
|
|
408
|
+
readonly availability: WebWorkspaceAvailabilityV1;
|
|
409
|
+
readonly kind: WebWorkspaceKindV1;
|
|
410
|
+
readonly source: WebWorkspaceCandidateSourceV1;
|
|
411
|
+
readonly existingWorkspaceId?: string;
|
|
412
|
+
readonly sessionCount?: number;
|
|
413
|
+
readonly sessionCountStatus?: WebWorkspaceSessionCountStatusV1;
|
|
414
|
+
readonly pinnedOrder?: number;
|
|
415
|
+
readonly isActive?: boolean;
|
|
416
|
+
}
|
|
417
|
+
export interface WebWorkspaceInspectRequestV1 {
|
|
418
|
+
readonly requestId: string;
|
|
419
|
+
readonly path: string;
|
|
420
|
+
readonly source?: WebWorkspaceCandidateSourceV1;
|
|
421
|
+
}
|
|
376
422
|
export type WebWorkspaceInvalidationReasonV1 = 'context-change' | 'filesystem-change' | 'terminal-command' | 'tool-finished';
|
|
377
423
|
export type WebToolDetailSummaryV1 = ToolDetailSummary;
|
|
378
424
|
export type WebToolDetailPageV1 = ToolDetailPage;
|
|
379
|
-
export type {
|
|
425
|
+
export type { GitBlameLineV1, GitGraphPageV1, GitGraphRowV1, GitBlameResultV1, GitRefKindV1, GitRefV1, GitRefsV1, GitCommitSummaryV1, GitCommitDetailV1, GitCommitFileV1, GitCommitFilesV1, GitHistoryQueryV1, GitHistoryPageV1, GitFileHistoryPageV1, } from './git-history-service';
|
|
426
|
+
export type { GitCompareFileV1, GitCompareModeV1, GitCompareResultV1, } from './git-compare-service';
|
|
427
|
+
export type { GitBlobResultV1, GitConflictShapeV1, GitConflictVersionsV1, GitFileVersionV1, GitSubmoduleEntryV1, GitVersionLabelV1, } from './git-version-service';
|
|
428
|
+
export type { WebFileContentPageV1, WebFileNodeV1, WebFileTreePageV1, GitCommitIdentityV1, GitCommitOutcomeV1, GitCommitPreviewV1, GitIndexEntryV1, GitMutationResultV1, GitWorktreeSourceV1, GitDiffCapabilitiesV2, GitDiffDocumentV2, GitDiffDocumentKindV2, GitDiffLineKindV2, GitDiffLineV2, GitHunkV2, WebGitCommitV1, WebGitDiffPageV1, WebGitFileSourceV1, WebGitFileV1, WebGitLogPageV1, WebGitStatusCountsV1, WebGitStatusV1, WebReviewSnapshotV1, WebTerminalCreateResultV1, WebTerminalExitV1, WebTerminalGapV1, WebTerminalMetadataV1, WebTerminalOutputFrameV1, WebTerminalStateV1, };
|
|
380
429
|
export interface WebBootstrapV1 {
|
|
381
430
|
readonly apiVersion: 1;
|
|
382
431
|
readonly productVersion: string;
|