@orion-agents/orion-code 0.3.21 → 0.3.24
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 +67 -6
- package/README.md +6 -6
- package/README.zh-CN.md +5 -5
- package/dist/harness/failure-attribution.d.ts +23 -0
- package/dist/harness/failure-attribution.js +103 -0
- package/dist/harness/verified-repair.d.ts +71 -0
- package/dist/harness/verified-repair.js +212 -0
- package/dist/product/paths.d.ts +20 -1
- package/dist/product/paths.js +293 -10
- package/dist/runtime/release-receipts.d.ts +3 -3
- package/dist/runtime/release-receipts.js +29 -0
- package/dist/runtime/tool-gateway.d.ts +6 -0
- package/dist/runtime/tool-gateway.js +37 -7
- package/dist/services/config-dir.d.ts +3 -2
- package/dist/services/config-dir.js +6 -29
- package/dist/services/repo-intelligence/index.d.ts +11 -0
- package/dist/services/repo-intelligence/index.js +20 -0
- package/dist/services/repo-intelligence/recall.d.ts +44 -0
- package/dist/services/repo-intelligence/recall.js +115 -0
- package/dist/services/repo-intelligence/symbol-index.d.ts +52 -0
- package/dist/services/repo-intelligence/symbol-index.js +258 -0
- package/dist/services/session-storage.d.ts +18 -2
- package/dist/services/session-storage.js +196 -60
- package/dist/web/protocol.d.ts +8 -0
- package/dist/web/workbench-controller.js +46 -2
- package/dist/web-client/assets/{FilesPanel-CKeXPofZ.js → FilesPanel-BjEDjE7l.js} +1 -1
- package/dist/web-client/assets/{GitPanel-DollPxv0.js → GitPanel-DvgAZDRA.js} +1 -1
- package/dist/web-client/assets/{ReviewPanel-CNADacIn.js → ReviewPanel-BMPaYKDY.js} +1 -1
- package/dist/web-client/assets/{TerminalPanel-Cyns4uJt.js → TerminalPanel-Da1ZEhGF.js} +1 -1
- package/dist/web-client/assets/index-DZOlUgt6.js +20 -0
- package/dist/web-client/assets/{useAutoNotice-BLADeFFj.js → useAutoNotice-Ylj5bfSH.js} +1 -1
- package/dist/web-client/index.html +1 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/dist/services/auto-fix/autoFixConfig.d.ts +0 -30
- package/dist/services/auto-fix/autoFixConfig.js +0 -78
- package/dist/services/auto-fix/autoFixHook.d.ts +0 -27
- package/dist/services/auto-fix/autoFixHook.js +0 -99
- package/dist/services/auto-fix/autoFixRunner.d.ts +0 -60
- package/dist/services/auto-fix/autoFixRunner.js +0 -207
- package/dist/services/auto-fix/index.d.ts +0 -7
- package/dist/services/auto-fix/index.js +0 -17
- package/dist/web-client/assets/index-BhGhORAX.js +0 -20
|
@@ -10,9 +10,11 @@ exports.SESSION_TAG_MAX_LENGTH = exports.SESSION_TAG_LIMIT = exports.SessionComp
|
|
|
10
10
|
exports.resolveProjectPath = resolveProjectPath;
|
|
11
11
|
exports.getProjectKey = getProjectKey;
|
|
12
12
|
exports.isSessionMetaFile = isSessionMetaFile;
|
|
13
|
+
exports.prepareSessionCatalogAsync = prepareSessionCatalogAsync;
|
|
13
14
|
exports.prepareSessionCompactSourceReceipt = prepareSessionCompactSourceReceipt;
|
|
14
15
|
exports.advanceSessionCompactSourceReceipt = advanceSessionCompactSourceReceipt;
|
|
15
16
|
exports.createSession = createSession;
|
|
17
|
+
exports.createSessionAsync = createSessionAsync;
|
|
16
18
|
exports.saveSessionMeta = saveSessionMeta;
|
|
17
19
|
exports.updateSessionEffort = updateSessionEffort;
|
|
18
20
|
exports.updateSessionComposerPreferences = updateSessionComposerPreferences;
|
|
@@ -219,7 +221,6 @@ function sanitizeTraceEvent(event) {
|
|
|
219
221
|
// ============================================================================
|
|
220
222
|
// Project helpers
|
|
221
223
|
// ============================================================================
|
|
222
|
-
const MAX_CACHE_SIZE = 256;
|
|
223
224
|
const SESSION_CATALOG_VERSION = 1;
|
|
224
225
|
let sessionCatalogCache = null;
|
|
225
226
|
function getSessionCatalogPath() {
|
|
@@ -228,63 +229,15 @@ function getSessionCatalogPath() {
|
|
|
228
229
|
function emptySessionCatalog() {
|
|
229
230
|
return { version: SESSION_CATALOG_VERSION, sessions: {} };
|
|
230
231
|
}
|
|
231
|
-
/**
|
|
232
|
-
* Evict the oldest entries when a Map exceeds MAX_CACHE_SIZE.
|
|
233
|
-
* Simple LRU approximation: delete the first (oldest) entries until
|
|
234
|
-
* the map is within bounds. Called after each insertion.
|
|
235
|
-
*/
|
|
236
|
-
function evictOldest(map, maxSize) {
|
|
237
|
-
while (map.size > maxSize) {
|
|
238
|
-
const firstKey = map.keys().next().value;
|
|
239
|
-
if (firstKey !== undefined)
|
|
240
|
-
map.delete(firstKey);
|
|
241
|
-
else
|
|
242
|
-
break;
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
const resolvedProjectPathCache = new Map();
|
|
246
232
|
/**
|
|
247
233
|
* Resolve a working directory to the project identity used for session storage.
|
|
248
234
|
* Git repositories share sessions from the repository root; non-git folders use
|
|
249
235
|
* their real absolute path.
|
|
250
236
|
*/
|
|
251
237
|
function resolveProjectPath(cwd = process.cwd()) {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
return cached;
|
|
256
|
-
}
|
|
257
|
-
let resolvedPath = absolute;
|
|
258
|
-
if ((0, fs_1.existsSync)(absolute)) {
|
|
259
|
-
try {
|
|
260
|
-
const root = (0, child_process_1.execFileSync)('git', ['-C', absolute, 'rev-parse', '--show-toplevel'], {
|
|
261
|
-
encoding: 'utf-8',
|
|
262
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
263
|
-
}).trim();
|
|
264
|
-
if (root) {
|
|
265
|
-
resolvedPath = (0, fs_1.realpathSync)(root);
|
|
266
|
-
resolvedProjectPathCache.set(absolute, resolvedPath);
|
|
267
|
-
evictOldest(resolvedProjectPathCache, MAX_CACHE_SIZE);
|
|
268
|
-
return resolvedPath;
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
catch (error) {
|
|
272
|
-
// Not a git worktree, or git is unavailable.
|
|
273
|
-
(0, debug_log_1.debugError)('session-storage.resolveGitRoot', error, absolute);
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
try {
|
|
277
|
-
resolvedPath = (0, fs_1.realpathSync)(absolute);
|
|
278
|
-
}
|
|
279
|
-
catch (error) {
|
|
280
|
-
// Broken symlink or concurrent deletion; the literal path still works
|
|
281
|
-
// as a stable project key.
|
|
282
|
-
(0, debug_log_1.debugError)('session-storage.realpath', error, absolute);
|
|
283
|
-
resolvedPath = absolute;
|
|
284
|
-
}
|
|
285
|
-
resolvedProjectPathCache.set(absolute, resolvedPath);
|
|
286
|
-
evictOldest(resolvedProjectPathCache, MAX_CACHE_SIZE);
|
|
287
|
-
return resolvedPath;
|
|
238
|
+
// Storage helpers and Session identity must share the same invalidation
|
|
239
|
+
// boundary; a second permanent cache could miss a newly initialized repo.
|
|
240
|
+
return (0, config_dir_1.resolveProjectStoragePath)(cwd);
|
|
288
241
|
}
|
|
289
242
|
function getProjectKey(projectPath) {
|
|
290
243
|
return (0, config_dir_1.encodeProjectPath)(resolveProjectPath(projectPath));
|
|
@@ -307,8 +260,8 @@ function getGitBranch(projectPath) {
|
|
|
307
260
|
return undefined;
|
|
308
261
|
}
|
|
309
262
|
}
|
|
310
|
-
function normalizeSessionMeta(session) {
|
|
311
|
-
const projectPath = resolveProjectPath(session.projectPath);
|
|
263
|
+
function normalizeSessionMeta(session, persistedStorage) {
|
|
264
|
+
const projectPath = persistedStorage?.projectPath ?? resolveProjectPath(session.projectPath);
|
|
312
265
|
const startTime = session.startTime ?? Date.now();
|
|
313
266
|
const updatedAt = session.updatedAt ?? session.endTime ?? startTime;
|
|
314
267
|
return {
|
|
@@ -321,15 +274,20 @@ function normalizeSessionMeta(session) {
|
|
|
321
274
|
updatedAt,
|
|
322
275
|
updatedAtIso: session.updatedAtIso ?? new Date(updatedAt).toISOString(),
|
|
323
276
|
messageCount: session.messageCount ?? 0,
|
|
324
|
-
historySizeBytes: session.historySizeBytes ??
|
|
277
|
+
historySizeBytes: session.historySizeBytes ??
|
|
278
|
+
(persistedStorage
|
|
279
|
+
? (safeFileSize(persistedStorage.historyPath) ?? 0)
|
|
280
|
+
: computeSessionHistorySizeBytes({ id: session.id, projectPath })),
|
|
325
281
|
tokenCount: session.tokenCount ?? 0,
|
|
326
282
|
cost: session.cost ?? 0,
|
|
327
|
-
|
|
283
|
+
// Normalization is a read-model operation. Missing optional metadata must
|
|
284
|
+
// not spawn Git on every read/save of a non-repository Session.
|
|
285
|
+
gitBranch: session.gitBranch,
|
|
328
286
|
};
|
|
329
287
|
}
|
|
330
|
-
function tryNormalizeSessionMeta(session, sourcePath) {
|
|
288
|
+
function tryNormalizeSessionMeta(session, sourcePath, persistedStorage) {
|
|
331
289
|
try {
|
|
332
|
-
return normalizeSessionMeta(session);
|
|
290
|
+
return normalizeSessionMeta(session, persistedStorage);
|
|
333
291
|
}
|
|
334
292
|
catch (error) {
|
|
335
293
|
(0, debug_log_1.debugError)('session-storage.normalizeSessionMeta', error, sourcePath);
|
|
@@ -479,6 +437,109 @@ function updateSessionCatalog(update) {
|
|
|
479
437
|
return catalog;
|
|
480
438
|
}, { waitMs: 10000 });
|
|
481
439
|
}
|
|
440
|
+
function catalogSourceStamp(path) {
|
|
441
|
+
try {
|
|
442
|
+
const stat = (0, fs_1.statSync)(path);
|
|
443
|
+
return [stat.dev, stat.ino, stat.mode, stat.size, stat.mtimeMs, stat.ctimeMs].join(':');
|
|
444
|
+
}
|
|
445
|
+
catch (error) {
|
|
446
|
+
if (error.code === 'ENOENT')
|
|
447
|
+
return 'missing';
|
|
448
|
+
throw error;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
async function scanSessionCatalogAsync() {
|
|
452
|
+
const catalog = emptySessionCatalog();
|
|
453
|
+
const stamps = new Map();
|
|
454
|
+
const projectsDir = (0, config_dir_1.getProjectsDir)();
|
|
455
|
+
stamps.set(projectsDir, catalogSourceStamp(projectsDir));
|
|
456
|
+
if (!(0, fs_1.existsSync)(projectsDir))
|
|
457
|
+
return { catalog, stamps };
|
|
458
|
+
let sinceYield = 0;
|
|
459
|
+
for (const projectKey of await fs_1.promises.readdir(projectsDir)) {
|
|
460
|
+
const sessionsDir = (0, path_1.join)(projectsDir, projectKey, 'sessions');
|
|
461
|
+
stamps.set(sessionsDir, catalogSourceStamp(sessionsDir));
|
|
462
|
+
if (!(0, fs_1.existsSync)(sessionsDir))
|
|
463
|
+
continue;
|
|
464
|
+
for (const file of (await fs_1.promises.readdir(sessionsDir)).filter(isSessionMetaFile)) {
|
|
465
|
+
const path = (0, path_1.join)(sessionsDir, file);
|
|
466
|
+
// Stamp before reading. A concurrent atomic replacement between these
|
|
467
|
+
// steps must invalidate the scan rather than bless stale bytes.
|
|
468
|
+
stamps.set(path, catalogSourceStamp(path));
|
|
469
|
+
let raw;
|
|
470
|
+
try {
|
|
471
|
+
raw = JSON.parse(await fs_1.promises.readFile(path, 'utf8'));
|
|
472
|
+
}
|
|
473
|
+
catch (error) {
|
|
474
|
+
(0, debug_log_1.debugError)('session-storage.scanCatalogAsync', error, path);
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
if (!raw || typeof raw.id !== 'string' || !raw.id || !raw.projectPath)
|
|
478
|
+
continue;
|
|
479
|
+
if (typeof raw.projectPath !== 'string')
|
|
480
|
+
continue;
|
|
481
|
+
// Historical metadata already owns its persisted root. Rediscovering
|
|
482
|
+
// Git here could migrate it, or let an unrelated broken repo deny Host
|
|
483
|
+
// startup. Rebuild the index without probing any historical repository.
|
|
484
|
+
const normalized = tryNormalizeSessionMeta(raw, path, {
|
|
485
|
+
projectPath: (0, path_1.resolve)(raw.projectPath),
|
|
486
|
+
historyPath: path.slice(0, -'.json'.length) + '.jsonl',
|
|
487
|
+
});
|
|
488
|
+
if (normalized) {
|
|
489
|
+
const previous = catalog.sessions[normalized.id];
|
|
490
|
+
if (!previous ||
|
|
491
|
+
(normalized.updatedAt ?? normalized.startTime) >
|
|
492
|
+
(previous.updatedAt ?? previous.startTime)) {
|
|
493
|
+
catalog.sessions[normalized.id] = normalized;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
if (++sinceYield >= 16) {
|
|
497
|
+
sinceYield = 0;
|
|
498
|
+
await new Promise(resolveYield => setImmediate(resolveYield));
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
return { catalog, stamps };
|
|
503
|
+
}
|
|
504
|
+
/** Warm/rebuild the global read model without synchronous lock retry or scan. */
|
|
505
|
+
async function prepareSessionCatalogAsync(waitMs = 10000) {
|
|
506
|
+
(0, config_dir_1.ensureConfigDir)();
|
|
507
|
+
const path = getSessionCatalogPath();
|
|
508
|
+
if (readCachedSessionCatalog(path))
|
|
509
|
+
return;
|
|
510
|
+
const deadline = Date.now() + waitMs;
|
|
511
|
+
let scanned = await scanSessionCatalogAsync();
|
|
512
|
+
const scanChanged = new Error('Session Catalog sources changed during scan.');
|
|
513
|
+
while (true) {
|
|
514
|
+
try {
|
|
515
|
+
(0, file_lock_1.withFileLockSync)(path, () => {
|
|
516
|
+
// A writer may have published a newer Catalog while the scan ran.
|
|
517
|
+
if (readCachedSessionCatalog(path))
|
|
518
|
+
return;
|
|
519
|
+
for (const [sourcePath, stamp] of scanned.stamps) {
|
|
520
|
+
if (catalogSourceStamp(sourcePath) !== stamp)
|
|
521
|
+
throw scanChanged;
|
|
522
|
+
}
|
|
523
|
+
writeSessionCatalog(path, scanned.catalog);
|
|
524
|
+
}, { waitMs: 0 });
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
catch (error) {
|
|
528
|
+
if (Date.now() >= deadline)
|
|
529
|
+
throw error;
|
|
530
|
+
if (error === scanChanged) {
|
|
531
|
+
scanned = await scanSessionCatalogAsync();
|
|
532
|
+
}
|
|
533
|
+
else if (error instanceof Error &&
|
|
534
|
+
error.message === `Timed out waiting for file lock ${path}.lock`) {
|
|
535
|
+
await new Promise(resolveWait => setTimeout(resolveWait, 10));
|
|
536
|
+
}
|
|
537
|
+
else {
|
|
538
|
+
throw error;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
482
543
|
function parseHarnessSidecarFile(path) {
|
|
483
544
|
try {
|
|
484
545
|
const content = (0, fs_1.readFileSync)(path, 'utf-8');
|
|
@@ -913,8 +974,9 @@ function computeSessionHistorySizeBytes(session) {
|
|
|
913
974
|
// ============================================================================
|
|
914
975
|
/**
|
|
915
976
|
* 创建新会话
|
|
977
|
+
* @param options Optional pre-probed Git metadata. Supplying this object skips synchronous Git.
|
|
916
978
|
*/
|
|
917
|
-
function createSession(projectPath, model) {
|
|
979
|
+
function createSession(projectPath, model, options) {
|
|
918
980
|
(0, config_dir_1.ensureConfigDir)();
|
|
919
981
|
const canonicalProjectPath = resolveProjectPath(projectPath);
|
|
920
982
|
const now = Date.now();
|
|
@@ -929,13 +991,87 @@ function createSession(projectPath, model) {
|
|
|
929
991
|
updatedAt: now,
|
|
930
992
|
updatedAtIso: new Date(now).toISOString(),
|
|
931
993
|
messageCount: 0,
|
|
932
|
-
|
|
994
|
+
// Interactive Hosts can supply an asynchronously probed branch. Passing
|
|
995
|
+
// options (including an unavailable branch) deliberately skips the legacy
|
|
996
|
+
// synchronous Git probe; CLI/TUI callers retain their existing behavior.
|
|
997
|
+
gitBranch: options ? options.gitBranch : getGitBranch(canonicalProjectPath),
|
|
933
998
|
tokenCount: 0,
|
|
934
999
|
cost: 0,
|
|
935
1000
|
};
|
|
936
1001
|
saveSessionMeta(session);
|
|
937
1002
|
return session;
|
|
938
1003
|
}
|
|
1004
|
+
/**
|
|
1005
|
+
* Web creation waits for cross-process metadata/catalog locks asynchronously.
|
|
1006
|
+
* Both locks are acquired before any Session bytes are written. The original
|
|
1007
|
+
* synchronous lock owner/recovery and atomic publication remain authoritative.
|
|
1008
|
+
*/
|
|
1009
|
+
async function createSessionAsync(projectPath, model, options = {}) {
|
|
1010
|
+
(0, config_dir_1.ensureConfigDir)();
|
|
1011
|
+
const deadline = Date.now() + (options.waitMs ?? 10000);
|
|
1012
|
+
options.beforeCommit?.();
|
|
1013
|
+
const storageIdentity = await (0, paths_1.resolveProjectStorageIdentityAsync)(projectPath);
|
|
1014
|
+
const canonicalProjectPath = storageIdentity.canonicalPath;
|
|
1015
|
+
await prepareSessionCatalogAsync(Math.max(0, deadline - Date.now()));
|
|
1016
|
+
const now = Date.now();
|
|
1017
|
+
const session = {
|
|
1018
|
+
id: (0, crypto_1.randomUUID)(),
|
|
1019
|
+
projectPath: canonicalProjectPath,
|
|
1020
|
+
projectKey: (0, config_dir_1.encodeProjectPath)(canonicalProjectPath),
|
|
1021
|
+
cwd: (0, path_1.resolve)(projectPath),
|
|
1022
|
+
model,
|
|
1023
|
+
startTime: now,
|
|
1024
|
+
createdAt: new Date(now).toISOString(),
|
|
1025
|
+
updatedAt: now,
|
|
1026
|
+
updatedAtIso: new Date(now).toISOString(),
|
|
1027
|
+
messageCount: 0,
|
|
1028
|
+
historySizeBytes: 0,
|
|
1029
|
+
gitBranch: options.gitBranch,
|
|
1030
|
+
tokenCount: 0,
|
|
1031
|
+
cost: 0,
|
|
1032
|
+
...(options.name?.trim() ? { name: options.name.trim() } : {}),
|
|
1033
|
+
};
|
|
1034
|
+
const sessionsDir = (0, paths_1.ensureResolvedProjectDir)(storageIdentity);
|
|
1035
|
+
const metaPath = (0, path_1.join)(sessionsDir, `${session.id}.json`);
|
|
1036
|
+
const catalogPath = getSessionCatalogPath();
|
|
1037
|
+
const lockTimeoutMessages = new Set([metaPath, catalogPath].map(path => `Timed out waiting for file lock ${path}.lock`));
|
|
1038
|
+
while (true) {
|
|
1039
|
+
options.beforeCommit?.();
|
|
1040
|
+
(0, paths_1.assertProjectStorageIdentity)(storageIdentity);
|
|
1041
|
+
try {
|
|
1042
|
+
return (0, file_lock_1.withFileLockSync)(metaPath, () => (0, file_lock_1.withFileLockSync)(catalogPath, () => {
|
|
1043
|
+
options.beforeCommit?.();
|
|
1044
|
+
(0, paths_1.assertProjectStorageIdentity)(storageIdentity);
|
|
1045
|
+
const catalog = readSessionCatalogFile(catalogPath);
|
|
1046
|
+
if (!catalog) {
|
|
1047
|
+
throw new Error('Session Catalog changed before creation. Retry.');
|
|
1048
|
+
}
|
|
1049
|
+
if ((0, fs_1.existsSync)(metaPath))
|
|
1050
|
+
throw new Error('Session identity already exists.');
|
|
1051
|
+
(0, atomic_write_1.atomicWriteFileSync)(metaPath, JSON.stringify(session, null, 2), { mode: 0o600 });
|
|
1052
|
+
try {
|
|
1053
|
+
catalog.sessions[session.id] = session;
|
|
1054
|
+
writeSessionCatalog(catalogPath, catalog);
|
|
1055
|
+
}
|
|
1056
|
+
catch (error) {
|
|
1057
|
+
// This identity was not exposed to a caller or Catalog yet.
|
|
1058
|
+
// Roll back its metadata while both locks are still ours.
|
|
1059
|
+
(0, fs_1.unlinkSync)(metaPath);
|
|
1060
|
+
throw error;
|
|
1061
|
+
}
|
|
1062
|
+
return session;
|
|
1063
|
+
}, { waitMs: 0 }), { waitMs: 0 });
|
|
1064
|
+
}
|
|
1065
|
+
catch (error) {
|
|
1066
|
+
if (!(error instanceof Error) ||
|
|
1067
|
+
!lockTimeoutMessages.has(error.message) ||
|
|
1068
|
+
Date.now() >= deadline) {
|
|
1069
|
+
throw error;
|
|
1070
|
+
}
|
|
1071
|
+
await new Promise(resolveWait => setTimeout(resolveWait, 10));
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
939
1075
|
/**
|
|
940
1076
|
* 保存会话元数据
|
|
941
1077
|
*/
|
package/dist/web/protocol.d.ts
CHANGED
|
@@ -471,6 +471,14 @@ export interface WebBootstrapV1 {
|
|
|
471
471
|
readonly workspace: string;
|
|
472
472
|
readonly configured: boolean;
|
|
473
473
|
readonly activeSessionId: string | null;
|
|
474
|
+
/**
|
|
475
|
+
* v0.3.24 — the Host's latest durable event cursor at bootstrap time. Clients
|
|
476
|
+
* use it as the replay anchor: durable envelopes at or below this cursor are
|
|
477
|
+
* already reflected by the baseline and must not be re-applied (a reloaded
|
|
478
|
+
* tab otherwise replays pre-baseline `workbench_state` broadcasts that would
|
|
479
|
+
* clear its restored foreground Session). Older clients ignore it.
|
|
480
|
+
*/
|
|
481
|
+
readonly eventCursor?: number;
|
|
474
482
|
readonly settings: WebSettingsSnapshotV1;
|
|
475
483
|
readonly capabilities: {
|
|
476
484
|
readonly goal: true;
|
|
@@ -5,7 +5,9 @@ exports.pageItems = pageItems;
|
|
|
5
5
|
exports.pageCollectionItems = pageCollectionItems;
|
|
6
6
|
exports.hasProjectSignal = hasProjectSignal;
|
|
7
7
|
const crypto_1 = require("crypto");
|
|
8
|
+
const child_process_1 = require("child_process");
|
|
8
9
|
const fs_1 = require("fs");
|
|
10
|
+
const paths_1 = require("../product/paths");
|
|
9
11
|
const promises_1 = require("fs/promises");
|
|
10
12
|
const path_1 = require("path");
|
|
11
13
|
const agent_runtime_controller_1 = require("../runtime/agent-runtime-controller");
|
|
@@ -156,6 +158,8 @@ class WebWorkbenchController {
|
|
|
156
158
|
}
|
|
157
159
|
static async create(options) {
|
|
158
160
|
const workbench = new WebWorkbenchController(options);
|
|
161
|
+
await (0, paths_1.resolveProjectStoragePathAsync)(workbench.workspaceValue);
|
|
162
|
+
await (0, session_storage_1.prepareSessionCatalogAsync)();
|
|
159
163
|
workbench.initializeWorkspaceRegistry();
|
|
160
164
|
await workbench.installRuntime(workbench.workspaceValue);
|
|
161
165
|
return workbench;
|
|
@@ -182,6 +186,7 @@ class WebWorkbenchController {
|
|
|
182
186
|
workspace: this.workspaceValue,
|
|
183
187
|
configured: this.runtimeValue.isConfigured,
|
|
184
188
|
activeSessionId: this.runtimeValue.getSession()?.id ?? null,
|
|
189
|
+
eventCursor: this.eventHub.snapshot().latest,
|
|
185
190
|
settings: this.settings(),
|
|
186
191
|
capabilities: {
|
|
187
192
|
goal: true,
|
|
@@ -1267,9 +1272,32 @@ class WebWorkbenchController {
|
|
|
1267
1272
|
// A new Session inherits the durable default; an override on another
|
|
1268
1273
|
// Session actor must never leak into it. Creation does not activate or
|
|
1269
1274
|
// close any actor; foreground selection belongs to the browser tab.
|
|
1270
|
-
const
|
|
1275
|
+
const workspace = this.workspaceValue;
|
|
1276
|
+
const creationContext = context ?? {
|
|
1277
|
+
workspaceId: this.activeWorkspaceEntry().id,
|
|
1278
|
+
expectedContextRevision: this.contextRevisionValue,
|
|
1279
|
+
};
|
|
1280
|
+
const gitBranch = await probeSessionGitBranch(workspace);
|
|
1281
|
+
// Git is optional metadata, but its asynchronous boundary must not allow a
|
|
1282
|
+
// stale browser to create a Session in a newly selected Workspace.
|
|
1283
|
+
this.assertContextGuard(creationContext);
|
|
1284
|
+
let named;
|
|
1285
|
+
try {
|
|
1286
|
+
await (0, paths_1.resolveProjectStoragePathAsync)(workspace);
|
|
1287
|
+
this.assertContextGuard(creationContext);
|
|
1288
|
+
named = await (0, session_storage_1.createSessionAsync)(workspace, this.settings().sections.defaults.model.effectiveValue, {
|
|
1289
|
+
gitBranch,
|
|
1290
|
+
name,
|
|
1291
|
+
beforeCommit: () => this.assertContextGuard(creationContext),
|
|
1292
|
+
});
|
|
1293
|
+
}
|
|
1294
|
+
catch (error) {
|
|
1295
|
+
if (error instanceof paths_1.ProjectStorageIdentityUnavailableError) {
|
|
1296
|
+
throw new errors_1.WebWorkbenchError(503, error.message, error.code);
|
|
1297
|
+
}
|
|
1298
|
+
throw error;
|
|
1299
|
+
}
|
|
1271
1300
|
(0, global_config_1.incrementSessionCount)();
|
|
1272
|
-
const named = name?.trim() ? ((0, session_storage_1.renameSession)(session.id, name.trim()) ?? session) : session;
|
|
1273
1301
|
this.sessionRegistry.summary(this.sessionActorKey(named));
|
|
1274
1302
|
return (0, protocol_1.projectSessionSummary)(named);
|
|
1275
1303
|
}
|
|
@@ -2154,6 +2182,9 @@ class WebWorkbenchController {
|
|
|
2154
2182
|
}
|
|
2155
2183
|
}
|
|
2156
2184
|
async installRuntime(workspace, publishState = true) {
|
|
2185
|
+
const storageRoot = await (0, paths_1.resolveProjectStoragePathAsync)(workspace);
|
|
2186
|
+
if (storageRoot !== workspace)
|
|
2187
|
+
await (0, paths_1.resolveProjectStoragePathAsync)(storageRoot);
|
|
2157
2188
|
// Every control-plane install reloads the Workspace catalog, provider
|
|
2158
2189
|
// wiring, Skills and MCP adapters, so this counter proxies "catalog
|
|
2159
2190
|
// reload" for the runtime-ownership diagnostics matrix.
|
|
@@ -2960,6 +2991,19 @@ function requireSession(sessionId) {
|
|
|
2960
2991
|
throw new errors_1.WebWorkbenchError(404, 'Session was not found.');
|
|
2961
2992
|
return session;
|
|
2962
2993
|
}
|
|
2994
|
+
/** Optional metadata must neither block the Host event loop nor delay creation indefinitely. */
|
|
2995
|
+
function probeSessionGitBranch(workspace) {
|
|
2996
|
+
return new Promise(resolveBranch => {
|
|
2997
|
+
(0, child_process_1.execFile)('git', ['-C', workspace, 'rev-parse', '--abbrev-ref', 'HEAD'], {
|
|
2998
|
+
encoding: 'utf8',
|
|
2999
|
+
timeout: 250,
|
|
3000
|
+
killSignal: 'SIGKILL',
|
|
3001
|
+
maxBuffer: 4096,
|
|
3002
|
+
env: process.env,
|
|
3003
|
+
windowsHide: true,
|
|
3004
|
+
}, (error, stdout) => resolveBranch(error ? undefined : stdout.trim() || undefined));
|
|
3005
|
+
});
|
|
3006
|
+
}
|
|
2963
3007
|
function mcpServerIds(config) {
|
|
2964
3008
|
const ids = new Set();
|
|
2965
3009
|
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-DZOlUgt6.js";import{u as Ce,R as Ne}from"./useAutoNotice-Ylj5bfSH.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};
|