@orion-agents/orion-code 0.3.27 → 0.3.29
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 +115 -3
- package/README.md +9 -10
- package/README.zh-CN.md +7 -7
- package/dist/commands/workflow-command-handlers.d.ts +9 -1
- package/dist/commands/workflow-command-handlers.js +66 -0
- package/dist/commands/workflow-commands.js +5 -5
- package/dist/services/code-review/format.d.ts +12 -0
- package/dist/services/code-review/format.js +79 -0
- package/dist/services/code-review/runner.d.ts +76 -0
- package/dist/services/code-review/runner.js +368 -0
- package/dist/services/code-review/selector.d.ts +24 -0
- package/dist/services/code-review/selector.js +86 -0
- package/dist/services/code-review/snapshot.d.ts +7 -0
- package/dist/services/code-review/snapshot.js +11 -2
- package/dist/web/code-review-run-service.d.ts +68 -0
- package/dist/web/code-review-run-service.js +228 -0
- package/dist/web/server.js +37 -0
- package/dist/web/workbench-controller.d.ts +16 -0
- package/dist/web/workbench-controller.js +61 -0
- package/dist/web-client/assets/{FilesPanel-CdoCa8u0.js → FilesPanel-iBYj9ZRf.js} +1 -1
- package/dist/web-client/assets/{GitPanel-CRr_iIDz.js → GitPanel-D33R_bZr.js} +1 -1
- package/dist/web-client/assets/ReviewPanel-C_0upsDk.js +6 -0
- package/dist/web-client/assets/{TerminalPanel-BniCjatZ.js → TerminalPanel-C7A8dMNf.js} +1 -1
- package/dist/web-client/assets/{index-DCYKIeOj.css → index-BaOxrwGB.css} +1 -1
- package/dist/web-client/assets/{index-kQqA3li6.js → index-CqCgJ2ov.js} +13 -13
- package/dist/web-client/assets/{useAutoNotice-DPXFWIwK.js → useAutoNotice-C1qM-_Sg.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/ReviewPanel-C1Cu2IRV.js +0 -5
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WebCodeReviewRunServiceV1 = void 0;
|
|
4
|
+
exports.parseWebReviewSelectorV1 = parseWebReviewSelectorV1;
|
|
5
|
+
/**
|
|
6
|
+
* v0.3.29 — Host-side shared code-review run service
|
|
7
|
+
* (docs/plan/v0.3.29-plan.md S1). The Web Host starts the same bounded
|
|
8
|
+
* single-pass review runner the CLI uses (v0.3.28 `runCodeReview`) against the
|
|
9
|
+
* active Workspace, keeps a bounded in-memory receipt history, and re-verifies
|
|
10
|
+
* every receipt digest plus repository revision freshness when results are
|
|
11
|
+
* read. No second review engine: receipts, budgets, validation and honest
|
|
12
|
+
* failure states come from the frozen `src/services/code-review` contract.
|
|
13
|
+
*
|
|
14
|
+
* Delivery model: `start()` validates admission and returns immediately; the
|
|
15
|
+
* model completion runs in the background and clients poll `list()`/`get()`.
|
|
16
|
+
* At most one run is in flight per Host (a second start is a typed 409), and
|
|
17
|
+
* a run may be cancelled to release the slot.
|
|
18
|
+
*/
|
|
19
|
+
const crypto_1 = require("crypto");
|
|
20
|
+
const contract_1 = require("../services/code-review/contract");
|
|
21
|
+
const runner_1 = require("../services/code-review/runner");
|
|
22
|
+
const errors_1 = require("./errors");
|
|
23
|
+
class WebCodeReviewRunServiceV1 {
|
|
24
|
+
constructor(options) {
|
|
25
|
+
this.records = new Map();
|
|
26
|
+
this.inflight = new Map();
|
|
27
|
+
this.adapterProvider = options.adapterProvider;
|
|
28
|
+
this.cwdProvider = options.cwdProvider;
|
|
29
|
+
this.repositoryRevisionProvider = options.repositoryRevisionProvider;
|
|
30
|
+
this.runReview = options.runReview ?? runner_1.runCodeReview;
|
|
31
|
+
this.now = options.now ?? Date.now;
|
|
32
|
+
this.maxRuns = Math.max(1, options.maxRuns ?? 20);
|
|
33
|
+
}
|
|
34
|
+
/** Admission is synchronous and fast: validate, record, return. The model
|
|
35
|
+
* completion continues in the background. */
|
|
36
|
+
async start(selector) {
|
|
37
|
+
if (this.inflight.size > 0) {
|
|
38
|
+
throw new errors_1.WebWorkbenchError(409, 'A code review is already running in this Host. Wait for it to finish or cancel it.', 'review_run_in_progress');
|
|
39
|
+
}
|
|
40
|
+
const adapter = this.adapterProvider();
|
|
41
|
+
if (!adapter) {
|
|
42
|
+
throw new errors_1.WebWorkbenchError(422, 'The Host has no configured model. Configure a model first, then retry the review.', 'review_no_model');
|
|
43
|
+
}
|
|
44
|
+
const runId = (0, crypto_1.randomUUID)();
|
|
45
|
+
const repositoryRevisionAtAdmission = await this.repositoryRevisionProvider();
|
|
46
|
+
const startedAt = this.now();
|
|
47
|
+
let capturedFiles = [];
|
|
48
|
+
this.records.set(runId, {
|
|
49
|
+
runId,
|
|
50
|
+
selector,
|
|
51
|
+
status: 'running',
|
|
52
|
+
startedAt,
|
|
53
|
+
endedAt: null,
|
|
54
|
+
repositoryRevisionAtAdmission,
|
|
55
|
+
files: capturedFiles,
|
|
56
|
+
result: null,
|
|
57
|
+
failure: null,
|
|
58
|
+
stale: false,
|
|
59
|
+
});
|
|
60
|
+
const abort = new AbortController();
|
|
61
|
+
this.inflight.set(runId, abort);
|
|
62
|
+
const cwd = this.cwdProvider();
|
|
63
|
+
void this.execute({
|
|
64
|
+
runId,
|
|
65
|
+
selector,
|
|
66
|
+
cwd,
|
|
67
|
+
adapter,
|
|
68
|
+
abort,
|
|
69
|
+
onSnapshot: snapshot => {
|
|
70
|
+
capturedFiles = Object.freeze(snapshot.files.map(file => ({ fileKey: file.fileKey, path: file.path })));
|
|
71
|
+
const record = this.records.get(runId);
|
|
72
|
+
if (record)
|
|
73
|
+
this.records.set(runId, { ...record, files: capturedFiles });
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
return Object.freeze({ runId, status: 'running' });
|
|
77
|
+
}
|
|
78
|
+
/** Cancels a running review; finished or unknown runs are a no-op. */
|
|
79
|
+
cancel(runId) {
|
|
80
|
+
this.inflight.get(runId)?.abort();
|
|
81
|
+
}
|
|
82
|
+
async list() {
|
|
83
|
+
const currentRevision = await this.repositoryRevisionProvider();
|
|
84
|
+
return [...this.records.values()]
|
|
85
|
+
.sort((left, right) => right.startedAt - left.startedAt)
|
|
86
|
+
.map(record => this.present(record, currentRevision));
|
|
87
|
+
}
|
|
88
|
+
async get(runId) {
|
|
89
|
+
const record = this.records.get(runId);
|
|
90
|
+
if (!record) {
|
|
91
|
+
throw new errors_1.WebWorkbenchError(404, 'Unknown review run.', 'review_run_not_found');
|
|
92
|
+
}
|
|
93
|
+
const currentRevision = await this.repositoryRevisionProvider();
|
|
94
|
+
return this.present(record, currentRevision);
|
|
95
|
+
}
|
|
96
|
+
async execute(input) {
|
|
97
|
+
const { runId, selector, cwd, adapter, abort, onSnapshot } = input;
|
|
98
|
+
const options = {
|
|
99
|
+
cwd,
|
|
100
|
+
selector,
|
|
101
|
+
adapter,
|
|
102
|
+
identity: {
|
|
103
|
+
workspaceIdentity: cwd,
|
|
104
|
+
// Web control-plane runs are their own session identity; they are not
|
|
105
|
+
// attributed to any chat Session actor.
|
|
106
|
+
sessionId: `web-host:${runId}`,
|
|
107
|
+
threadId: `web-host:${runId}`,
|
|
108
|
+
turnId: runId,
|
|
109
|
+
},
|
|
110
|
+
abortSignal: abort.signal,
|
|
111
|
+
...(onSnapshot ? { onSnapshotCaptured: onSnapshot } : {}),
|
|
112
|
+
};
|
|
113
|
+
let failure = null;
|
|
114
|
+
let result = null;
|
|
115
|
+
try {
|
|
116
|
+
const outcome = await this.runReview(options);
|
|
117
|
+
if (outcome.ok) {
|
|
118
|
+
result = outcome.result;
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
failure = {
|
|
122
|
+
kind: outcome.failure.kind,
|
|
123
|
+
message: failureMessage(outcome.failure),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
failure = {
|
|
129
|
+
kind: 'model_error',
|
|
130
|
+
message: error instanceof Error ? error.message : String(error),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
this.inflight.delete(runId);
|
|
134
|
+
const existing = this.records.get(runId);
|
|
135
|
+
if (!existing)
|
|
136
|
+
return;
|
|
137
|
+
const finished = {
|
|
138
|
+
...existing,
|
|
139
|
+
status: result ? result.state : failure?.kind === 'cancelled' ? 'cancelled' : 'failed',
|
|
140
|
+
endedAt: this.now(),
|
|
141
|
+
result,
|
|
142
|
+
failure,
|
|
143
|
+
};
|
|
144
|
+
this.records.set(runId, finished);
|
|
145
|
+
this.evictFinished();
|
|
146
|
+
}
|
|
147
|
+
evictFinished() {
|
|
148
|
+
const finished = [...this.records.values()]
|
|
149
|
+
.filter(record => record.status !== 'running')
|
|
150
|
+
.sort((left, right) => (left.endedAt ?? left.startedAt) - (right.endedAt ?? right.startedAt));
|
|
151
|
+
let excess = this.records.size - this.maxRuns;
|
|
152
|
+
for (const record of finished) {
|
|
153
|
+
if (excess <= 0)
|
|
154
|
+
break;
|
|
155
|
+
this.records.delete(record.runId);
|
|
156
|
+
excess -= 1;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
present(record, currentRevision) {
|
|
160
|
+
const stale = record.result !== null && record.repositoryRevisionAtAdmission !== currentRevision;
|
|
161
|
+
if (record.result && !(0, contract_1.verifyReceiptDigest)(record.result)) {
|
|
162
|
+
// A receipt that no longer verifies must never be presented as findings;
|
|
163
|
+
// surface it as a failed run with an honest message instead.
|
|
164
|
+
return {
|
|
165
|
+
...record,
|
|
166
|
+
status: 'failed',
|
|
167
|
+
stale,
|
|
168
|
+
result: null,
|
|
169
|
+
failure: {
|
|
170
|
+
kind: 'receipt_digest_mismatch',
|
|
171
|
+
message: 'The stored review receipt failed digest re-verification.',
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
return { ...record, stale };
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
exports.WebCodeReviewRunServiceV1 = WebCodeReviewRunServiceV1;
|
|
179
|
+
function failureMessage(failure) {
|
|
180
|
+
switch (failure.kind) {
|
|
181
|
+
case 'cancelled':
|
|
182
|
+
return 'Review cancelled.';
|
|
183
|
+
case 'budget':
|
|
184
|
+
return `Review budget rejected: ${failure.field ?? 'unknown'}`;
|
|
185
|
+
case 'snapshot':
|
|
186
|
+
return `Review scope error (${failure.code ?? 'unknown'}): ${failure.message ?? ''}`;
|
|
187
|
+
case 'model_error':
|
|
188
|
+
return `Review model call failed: ${failure.message ?? ''}`;
|
|
189
|
+
case 'unparsable_output':
|
|
190
|
+
return `Review output could not be parsed: ${failure.message ?? ''}`;
|
|
191
|
+
default:
|
|
192
|
+
return failure.message ?? 'Review failed.';
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/** v0.3.29 — validates the JSON selector accepted by `POST /review/run`.
|
|
196
|
+
* Mirrors the CLI selector contract: worktree scope, base ref scope, optional
|
|
197
|
+
* path filters. Unknown shapes are typed 400s, never guesses. */
|
|
198
|
+
function parseWebReviewSelectorV1(value) {
|
|
199
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
200
|
+
throw new errors_1.WebWorkbenchError(400, 'Review selector must be an object.', 'review_selector_invalid');
|
|
201
|
+
}
|
|
202
|
+
const record = value;
|
|
203
|
+
const keys = Object.keys(record).sort();
|
|
204
|
+
const allowed = record.kind === 'base' ? ['kind', 'paths', 'ref'] : ['kind', 'paths'];
|
|
205
|
+
if (keys.some(key => !allowed.includes(key))) {
|
|
206
|
+
throw new errors_1.WebWorkbenchError(400, `Review selector accepts only ${allowed.join(', ')}.`, 'review_selector_invalid');
|
|
207
|
+
}
|
|
208
|
+
const paths = parseSelectorPaths(record.paths);
|
|
209
|
+
if (record.kind === 'base') {
|
|
210
|
+
if (typeof record.ref !== 'string' || record.ref.trim() === '') {
|
|
211
|
+
throw new errors_1.WebWorkbenchError(400, 'Review selector base ref must be a non-empty string.', 'review_selector_invalid');
|
|
212
|
+
}
|
|
213
|
+
return Object.freeze({ kind: 'base', ref: record.ref, ...(paths ? { paths } : {}) });
|
|
214
|
+
}
|
|
215
|
+
if (record.kind === 'worktree') {
|
|
216
|
+
return Object.freeze({ kind: 'worktree', ...(paths ? { paths } : {}) });
|
|
217
|
+
}
|
|
218
|
+
throw new errors_1.WebWorkbenchError(400, 'Review selector kind must be "worktree" or "base".', 'review_selector_invalid');
|
|
219
|
+
}
|
|
220
|
+
function parseSelectorPaths(value) {
|
|
221
|
+
if (value === undefined)
|
|
222
|
+
return undefined;
|
|
223
|
+
if (!Array.isArray(value) || value.some(path => typeof path !== 'string' || path.trim() === '')) {
|
|
224
|
+
throw new errors_1.WebWorkbenchError(400, 'Review selector paths must be an array of non-empty strings.', 'review_selector_invalid');
|
|
225
|
+
}
|
|
226
|
+
return Object.freeze([...value]);
|
|
227
|
+
}
|
|
228
|
+
//# sourceMappingURL=code-review-run-service.js.map
|
package/dist/web/server.js
CHANGED
|
@@ -9,6 +9,7 @@ const version_1 = require("../product/version");
|
|
|
9
9
|
const redaction_1 = require("../services/redaction");
|
|
10
10
|
const protocol_1 = require("./protocol");
|
|
11
11
|
const errors_1 = require("./errors");
|
|
12
|
+
const code_review_run_service_1 = require("./code-review-run-service");
|
|
12
13
|
const cli_options_1 = require("./cli-options");
|
|
13
14
|
const terminal_server_1 = require("./terminal-server");
|
|
14
15
|
const workbench_controller_1 = require("./workbench-controller");
|
|
@@ -885,6 +886,42 @@ async function handleRequest(context) {
|
|
|
885
886
|
sendJson(response, 200, await context.workbench.review(requireContextGuardQuery(url)));
|
|
886
887
|
return;
|
|
887
888
|
}
|
|
889
|
+
// v0.3.29 — shared code-review runs. Start is a fast mutation (admission
|
|
890
|
+
// only; the model completion runs in the background and clients poll the
|
|
891
|
+
// read-only list/get endpoints below).
|
|
892
|
+
if (method === 'POST' && path === '/review/run') {
|
|
893
|
+
assertMutation(request, context.nonce, context.origin);
|
|
894
|
+
const body = requireRecord(await readJson(request), 'Review run request');
|
|
895
|
+
assertOnlyKeys(body, ['requestId', 'selector', 'expectedContextRevision', 'workspaceId']);
|
|
896
|
+
const requestId = requireUuid(body.requestId, 'requestId');
|
|
897
|
+
const contextGuard = requireContextGuardRecord(body);
|
|
898
|
+
const selector = (0, code_review_run_service_1.parseWebReviewSelectorV1)(body.selector);
|
|
899
|
+
const result = await context.workbench.executeMutation(requestId, 'review.run', { selector, ...contextGuard }, () => context.workbench.reviewRunStart(selector, contextGuard));
|
|
900
|
+
sendJson(response, 200, result);
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
if (method === 'GET' && path === '/review/runs') {
|
|
904
|
+
sendJson(response, 200, await context.workbench.reviewRunList(requireContextGuardQuery(url)));
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
const reviewRunMatch = path.match(/^\/review\/runs\/([^/]+)$/);
|
|
908
|
+
if (method === 'GET' && reviewRunMatch) {
|
|
909
|
+
const runId = requireUuid(safeDecodePathSegment(reviewRunMatch[1]), 'runId');
|
|
910
|
+
sendJson(response, 200, await context.workbench.reviewRunGet(runId, requireContextGuardQuery(url)));
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
const reviewRunCancelMatch = path.match(/^\/review\/runs\/([^/]+)\/cancel$/);
|
|
914
|
+
if (method === 'POST' && reviewRunCancelMatch) {
|
|
915
|
+
assertMutation(request, context.nonce, context.origin);
|
|
916
|
+
const body = requireRecord(await readJson(request), 'Review cancel request');
|
|
917
|
+
assertOnlyKeys(body, ['requestId', 'expectedContextRevision', 'workspaceId']);
|
|
918
|
+
const requestId = requireUuid(body.requestId, 'requestId');
|
|
919
|
+
const contextGuard = requireContextGuardRecord(body);
|
|
920
|
+
const runId = requireUuid(safeDecodePathSegment(reviewRunCancelMatch[1]), 'runId');
|
|
921
|
+
const result = await context.workbench.executeMutation(requestId, 'review.run.cancel', { runId, ...contextGuard }, () => context.workbench.reviewRunCancel(runId, contextGuard));
|
|
922
|
+
sendJson(response, 200, result);
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
888
925
|
if (method === 'GET' && path === '/terminals') {
|
|
889
926
|
const contextGuard = requireContextGuardQuery(url);
|
|
890
927
|
sendJson(response, 200, collectionPage(url, 'terminals', context.workbench.listTerminals(contextGuard), terminal => terminal.id));
|
|
@@ -10,6 +10,7 @@ import { type WebComposerActionResultV1, type WebComposerActionV1, type WebCompo
|
|
|
10
10
|
import { type NativeDirectoryPickResult, type NativeDirectoryPicker } from './native-directory-picker';
|
|
11
11
|
import { WorkspaceDiscoveryTelemetryV1 } from './workspace-discovery';
|
|
12
12
|
import { WorkspaceRootsRegistryV1 } from '../services/workspace-roots-registry';
|
|
13
|
+
import type { ReviewSelectorV1 } from '../services/code-review/contract';
|
|
13
14
|
import { type WebSessionRuntimeSummaryV1 } from './session-runtime-registry';
|
|
14
15
|
import { TerminalManagerV1 } from './terminal-manager';
|
|
15
16
|
export { WebWorkbenchError } from './errors';
|
|
@@ -70,6 +71,8 @@ export declare class WebWorkbenchController {
|
|
|
70
71
|
private fileWriteService;
|
|
71
72
|
private gitService;
|
|
72
73
|
private reviewService;
|
|
74
|
+
/** v0.3.29 — shared code-review run history for the Review panel. */
|
|
75
|
+
private readonly codeReviewRuns;
|
|
73
76
|
private contextRevisionValue;
|
|
74
77
|
private composerRevisionValue;
|
|
75
78
|
private composerAuthorityDigest;
|
|
@@ -281,6 +284,19 @@ export declare class WebWorkbenchController {
|
|
|
281
284
|
gitLog(context: WebContextGuardV1, input: Parameters<GitReadModelServiceV1['log']>[0]): Promise<import("./git-read-model-service").WebGitLogPageV1>;
|
|
282
285
|
gitDiff(context: WebContextGuardV1, input: Parameters<GitReadModelServiceV1['diff']>[0]): Promise<import("./git-read-model-service").WebGitDiffPageV1>;
|
|
283
286
|
review(context: WebContextGuardV1): Promise<import("./review-service").WebReviewSnapshotV1>;
|
|
287
|
+
/** v0.3.29 — starts a shared code review run (fast admission, background run). */
|
|
288
|
+
reviewRunStart(selector: ReviewSelectorV1, context: WebContextGuardV1): Promise<{
|
|
289
|
+
readonly runId: string;
|
|
290
|
+
readonly status: 'running';
|
|
291
|
+
}>;
|
|
292
|
+
/** v0.3.29 — bounded list of review runs with freshness flags. */
|
|
293
|
+
reviewRunList(context: WebContextGuardV1): Promise<readonly import("./code-review-run-service").WebCodeReviewRunRecordV1[]>;
|
|
294
|
+
/** v0.3.29 — one review run record with digest re-verification. */
|
|
295
|
+
reviewRunGet(runId: string, context: WebContextGuardV1): Promise<import("./code-review-run-service").WebCodeReviewRunRecordV1>;
|
|
296
|
+
/** v0.3.29 — cancels a running review to release the single in-flight slot. */
|
|
297
|
+
reviewRunCancel(runId: string, context: WebContextGuardV1): {
|
|
298
|
+
readonly cancelled: boolean;
|
|
299
|
+
};
|
|
284
300
|
listTerminals(context: WebContextGuardV1): readonly import("./terminal-manager").WebTerminalMetadataV1[];
|
|
285
301
|
createSession(name?: string, context?: WebContextGuardV1): Promise<WebSessionSummaryV1>;
|
|
286
302
|
activateSession(sessionId: string, context?: WebContextGuardV1): Promise<WebCommandResultV1>;
|
|
@@ -39,6 +39,7 @@ const workspace_open_telemetry_1 = require("./workspace-open-telemetry");
|
|
|
39
39
|
const workspace_discovery_1 = require("./workspace-discovery");
|
|
40
40
|
const workspace_roots_registry_1 = require("../services/workspace-roots-registry");
|
|
41
41
|
const review_service_1 = require("./review-service");
|
|
42
|
+
const code_review_run_service_1 = require("./code-review-run-service");
|
|
42
43
|
const session_runtime_registry_1 = require("./session-runtime-registry");
|
|
43
44
|
const terminal_manager_1 = require("./terminal-manager");
|
|
44
45
|
var errors_2 = require("./errors");
|
|
@@ -73,6 +74,15 @@ class WebWorkbenchController {
|
|
|
73
74
|
this.toolDetails = new tool_detail_repository_1.FileToolDetailRepository();
|
|
74
75
|
this.workspaceMutationOwners = new Map();
|
|
75
76
|
this.pendingWorkspaceMutationStates = new Map();
|
|
77
|
+
/** v0.3.29 — shared code-review run history for the Review panel. */
|
|
78
|
+
this.codeReviewRuns = new code_review_run_service_1.WebCodeReviewRunServiceV1({
|
|
79
|
+
adapterProvider: () => createReviewModelAdapter(this.runtimeValue?.llm ?? null),
|
|
80
|
+
cwdProvider: () => this.workspaceValue,
|
|
81
|
+
repositoryRevisionProvider: async () => {
|
|
82
|
+
const status = await this.gitService.status({ pageSize: 1 });
|
|
83
|
+
return status.repositoryRevision;
|
|
84
|
+
},
|
|
85
|
+
});
|
|
76
86
|
this.contextRevisionValue = (0, crypto_1.randomUUID)();
|
|
77
87
|
this.composerRevisionValue = (0, crypto_1.randomUUID)();
|
|
78
88
|
this.suppressComposerEdges = 0;
|
|
@@ -1261,6 +1271,34 @@ class WebWorkbenchController {
|
|
|
1261
1271
|
this.assertContextGuard(context);
|
|
1262
1272
|
return result;
|
|
1263
1273
|
}
|
|
1274
|
+
/** v0.3.29 — starts a shared code review run (fast admission, background run). */
|
|
1275
|
+
async reviewRunStart(selector, context) {
|
|
1276
|
+
this.assertContextGuard(context);
|
|
1277
|
+
const started = await this.codeReviewRuns.start(selector);
|
|
1278
|
+
this.assertContextGuard(context);
|
|
1279
|
+
return started;
|
|
1280
|
+
}
|
|
1281
|
+
/** v0.3.29 — bounded list of review runs with freshness flags. */
|
|
1282
|
+
async reviewRunList(context) {
|
|
1283
|
+
this.assertContextGuard(context);
|
|
1284
|
+
const runs = await this.codeReviewRuns.list();
|
|
1285
|
+
this.assertContextGuard(context);
|
|
1286
|
+
return runs;
|
|
1287
|
+
}
|
|
1288
|
+
/** v0.3.29 — one review run record with digest re-verification. */
|
|
1289
|
+
async reviewRunGet(runId, context) {
|
|
1290
|
+
this.assertContextGuard(context);
|
|
1291
|
+
const run = await this.codeReviewRuns.get(runId);
|
|
1292
|
+
this.assertContextGuard(context);
|
|
1293
|
+
return run;
|
|
1294
|
+
}
|
|
1295
|
+
/** v0.3.29 — cancels a running review to release the single in-flight slot. */
|
|
1296
|
+
reviewRunCancel(runId, context) {
|
|
1297
|
+
this.assertContextGuard(context);
|
|
1298
|
+
this.codeReviewRuns.cancel(runId);
|
|
1299
|
+
this.assertContextGuard(context);
|
|
1300
|
+
return { cancelled: true };
|
|
1301
|
+
}
|
|
1264
1302
|
listTerminals(context) {
|
|
1265
1303
|
this.assertContextGuard(context);
|
|
1266
1304
|
const result = this.terminalManager.list(this.activeWorkspaceEntry().id);
|
|
@@ -3005,6 +3043,29 @@ function probeSessionGitBranch(workspace) {
|
|
|
3005
3043
|
}, (error, stdout) => resolveBranch(error ? undefined : stdout.trim() || undefined));
|
|
3006
3044
|
});
|
|
3007
3045
|
}
|
|
3046
|
+
/**
|
|
3047
|
+
* v0.3.29 — adapts the control-plane Runtime's LLM service to the shared code
|
|
3048
|
+
* review runner's bounded single-completion interface. Returns null when the
|
|
3049
|
+
* Host has no configured model so the run service can answer with the typed
|
|
3050
|
+
* `review_no_model` error instead of pretending to review.
|
|
3051
|
+
*/
|
|
3052
|
+
function createReviewModelAdapter(llm) {
|
|
3053
|
+
if (!llm)
|
|
3054
|
+
return null;
|
|
3055
|
+
return {
|
|
3056
|
+
async complete(messages) {
|
|
3057
|
+
const response = await llm.chat(messages.map(message => ({ role: message.role, content: message.content })));
|
|
3058
|
+
return {
|
|
3059
|
+
content: response.content,
|
|
3060
|
+
model: response.model,
|
|
3061
|
+
usage: {
|
|
3062
|
+
inputTokens: response.usage?.promptTokens ?? 0,
|
|
3063
|
+
outputTokens: response.usage?.completionTokens ?? 0,
|
|
3064
|
+
},
|
|
3065
|
+
};
|
|
3066
|
+
},
|
|
3067
|
+
};
|
|
3068
|
+
}
|
|
3008
3069
|
function mcpServerIds(config) {
|
|
3009
3070
|
const ids = new Set();
|
|
3010
3071
|
for (const envelope of [config.mcpServers, config.servers, config.orion?.mcp?.servers]) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{r as a,j as e,I as D,s as be,C as ye,W as ge}from"./index-
|
|
1
|
+
import{r as a,j as e,I as D,s as be,C as ye,W as ge}from"./index-CqCgJ2ov.js";import{u as Ce,R as Ne}from"./useAutoNotice-C1qM-_Sg.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};
|