@orion-agents/orion-code 0.3.24 → 0.3.25
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 +35 -4
- package/README.md +8 -5
- package/README.zh-CN.md +6 -4
- package/dist/runtime/model-coordinator.d.ts +1 -0
- package/dist/runtime/model-coordinator.js +9 -2
- package/dist/runtime/session-composer-control.js +5 -1
- package/dist/services/workspace-registry.d.ts +9 -0
- package/dist/services/workspace-registry.js +13 -2
- package/dist/web/git-history-service.js +3 -0
- package/dist/web/git-read-model-service.d.ts +9 -0
- package/dist/web/git-read-model-service.js +35 -0
- package/dist/web/git-version-service.d.ts +8 -0
- package/dist/web/git-version-service.js +50 -26
- package/dist/web/protocol.d.ts +5 -0
- package/dist/web/workbench-controller.js +1 -0
- package/dist/web-client/assets/{FilesPanel-BjEDjE7l.js → FilesPanel-CdoCa8u0.js} +1 -1
- package/dist/web-client/assets/{GitPanel-DvgAZDRA.js → GitPanel-CRr_iIDz.js} +1 -1
- package/dist/web-client/assets/{ReviewPanel-BMPaYKDY.js → ReviewPanel-C1Cu2IRV.js} +1 -1
- package/dist/web-client/assets/{TerminalPanel-Da1ZEhGF.js → TerminalPanel-BniCjatZ.js} +1 -1
- package/dist/web-client/assets/{index-DZOlUgt6.js → index-kQqA3li6.js} +11 -11
- package/dist/web-client/assets/{useAutoNotice-Ylj5bfSH.js → useAutoNotice-DPXFWIwK.js} +1 -1
- package/dist/web-client/index.html +1 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -22,11 +22,42 @@ which is **not** a pass.
|
|
|
22
22
|
|
|
23
23
|
## [Unreleased]
|
|
24
24
|
|
|
25
|
-
## [0.3.
|
|
25
|
+
## [0.3.25] — UNRELEASED
|
|
26
26
|
|
|
27
|
-
> **Status: candidate.**
|
|
28
|
-
>
|
|
29
|
-
>
|
|
27
|
+
> **Status: candidate.** Open. Four open-issue fixes: the Git conflict view reads the
|
|
28
|
+
> edited working file from the filesystem instead of an unstored object (#269), file
|
|
29
|
+
> history walks the requested revision instead of always HEAD (#270), model switching
|
|
30
|
+
> gates the compact preflight on effective input budgets instead of raw context windows
|
|
31
|
+
> (#271), and the left project navigation keeps a stable registration order when
|
|
32
|
+
> sessions are activated (#272). Plan: `docs/plan/v0.3.25-plan.md`. Not merged, tagged
|
|
33
|
+
> or published.
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- #269: `GET /git/conflict` no longer fails with `git cat-file: could not get object
|
|
38
|
+
info` after editing a conflicted file before staging — the `worktree` version is read
|
|
39
|
+
from the filesystem (contained within the repository root) instead of an object
|
|
40
|
+
`hash-object` never wrote.
|
|
41
|
+
- #270: `GET /git/file-history` with a `rev` now walks that revision; previously the
|
|
42
|
+
validated revision never reached the `git log` command and every query walked HEAD.
|
|
43
|
+
- #271: switching to a model with the same or larger raw context window but a smaller
|
|
44
|
+
effective input budget (larger output reserve) now reports `compactRequired` and runs
|
|
45
|
+
the compact preflight instead of silently persisting a model the history cannot fit.
|
|
46
|
+
|
|
47
|
+
### Changed
|
|
48
|
+
|
|
49
|
+
- #272: the left project navigation keeps a stable registration order for unpinned
|
|
50
|
+
projects (`registeredAt`, persisted by the workspace registry and exposed additively
|
|
51
|
+
on `WebWorkspaceSummaryV1`); activating sessions no longer reshuffles the tree.
|
|
52
|
+
Pinned projects keep `pinnedOrder` first.
|
|
53
|
+
|
|
54
|
+
## [0.3.24] — 2026-10-13
|
|
55
|
+
|
|
56
|
+
> **Status: published.** Released as `@orion-agents/orion-code@0.3.24` from
|
|
57
|
+
> `refs/tags/v0.3.24`, and merged to `main` as `85fadb4` (#268). Session
|
|
58
|
+
> navigation & responsiveness for the Web Workbench. Plan:
|
|
59
|
+
> `docs/plan/v0.3.24-plan.md`. GitHub Release:
|
|
60
|
+
> https://github.com/orion-agents/orion-code/releases/tag/v0.3.24
|
|
30
61
|
|
|
31
62
|
### Fixed
|
|
32
63
|
|
package/README.md
CHANGED
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
Local-first, goal-driven coding agent for the terminal and browser.
|
|
4
4
|
|
|
5
|
-
> v0.3.
|
|
6
|
-
>
|
|
7
|
-
>
|
|
5
|
+
> v0.3.25 candidate — open. Four open-issue fixes: the Git conflict view reads the edited
|
|
6
|
+
> working file from the filesystem (#269), file history walks the requested revision
|
|
7
|
+
> (#270), model switching gates the compact preflight on effective input budgets (#271),
|
|
8
|
+
> and the left project navigation keeps a stable registration order (#272). The previous
|
|
9
|
+
> release 0.3.24 is on npm (registry `latest`), tagged `v0.3.24` (`85fadb4`, #268) and
|
|
10
|
+
> released on GitHub.
|
|
8
11
|
|
|
9
12
|
[中文说明](README.zh-CN.md) ·
|
|
10
13
|
[v0.3.24 plan](docs/plan/v0.3.24-plan.md) ·
|
|
@@ -167,10 +170,10 @@ user extension boundaries.
|
|
|
167
170
|
Node.js 22.12+, 24, and 26 are supported. Use Node 24 LTS for production or Node 26 Current for
|
|
168
171
|
current development environments. Node 20 is upstream EOL and is no longer a v0.3 runtime.
|
|
169
172
|
|
|
170
|
-
To install the pinned v0.3.
|
|
173
|
+
To install the pinned v0.3.25 package when it is available on npm:
|
|
171
174
|
|
|
172
175
|
```bash
|
|
173
|
-
npm install -g @orion-agents/orion-code@0.3.
|
|
176
|
+
npm install -g @orion-agents/orion-code@0.3.25
|
|
174
177
|
orion --version
|
|
175
178
|
orion doctor
|
|
176
179
|
```
|
package/README.zh-CN.md
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
面向终端与浏览器、本地优先的目标驱动 Coding Agent。
|
|
4
4
|
|
|
5
|
-
> v0.3.
|
|
6
|
-
>
|
|
5
|
+
> v0.3.25 候选版本:进行中。四个开放 issue 修复:Git 冲突视图从文件系统读取已编辑的
|
|
6
|
+
> 工作区文件(#269)、文件历史按请求的 revision 遍历(#270)、模型切换按有效输入预算
|
|
7
|
+
> 触发压缩预检(#271)、左侧项目导航保持稳定注册序(#272)。上一版 0.3.24 已发布 npm
|
|
8
|
+
> (registry `latest`)、打 tag `v0.3.24`(`85fadb4`,#268)并发布 GitHub Release。
|
|
7
9
|
|
|
8
10
|
[English](README.md) ·
|
|
9
11
|
[v0.3.24 方案](docs/plan/v0.3.24-plan.md) ·
|
|
@@ -98,10 +100,10 @@ MCP 仍是用户可见的扩展边界。
|
|
|
98
100
|
支持 Node.js 22.12+、24 和 26。生产环境建议使用 Node 24 LTS,当前开发环境也支持 Node 26
|
|
99
101
|
Current。Node 20 已结束上游维护,不再属于 v0.3 Runtime 合同。
|
|
100
102
|
|
|
101
|
-
v0.3.
|
|
103
|
+
v0.3.25 在 npm 可用后,可安装固定版本:
|
|
102
104
|
|
|
103
105
|
```bash
|
|
104
|
-
npm install -g @orion-agents/orion-code@0.3.
|
|
106
|
+
npm install -g @orion-agents/orion-code@0.3.25
|
|
105
107
|
orion --version
|
|
106
108
|
orion doctor
|
|
107
109
|
```
|
|
@@ -94,4 +94,5 @@ export declare class ModelCoordinator extends EventEmitter {
|
|
|
94
94
|
private validateTargetClient;
|
|
95
95
|
private commitResolvedTarget;
|
|
96
96
|
}
|
|
97
|
+
export declare function resolveProfileSafeInputBudget(profile: ResolvedModelProfile): number;
|
|
97
98
|
//# sourceMappingURL=model-coordinator.d.ts.map
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
exports.ModelCoordinator = void 0;
|
|
13
13
|
exports.validateModelSwitchCompactReceipt = validateModelSwitchCompactReceipt;
|
|
14
|
+
exports.resolveProfileSafeInputBudget = resolveProfileSafeInputBudget;
|
|
14
15
|
const events_1 = require("events");
|
|
15
16
|
function validateModelSwitchCompactReceipt(receipt, targetTokens, safeInputBudget = targetTokens) {
|
|
16
17
|
if (receipt.status === 'rejected')
|
|
@@ -114,7 +115,9 @@ class ModelCoordinator extends events_1.EventEmitter {
|
|
|
114
115
|
if (clientError)
|
|
115
116
|
return { success: false, error: clientError };
|
|
116
117
|
const prev = this.current;
|
|
117
|
-
|
|
118
|
+
// v0.3.25 #271 — same budget gate as commitSwitch: the effective input
|
|
119
|
+
// budgets decide, not raw context windows.
|
|
120
|
+
if (!prev || resolveProfileSafeInputBudget(target) >= resolveProfileSafeInputBudget(prev)) {
|
|
118
121
|
this.commitResolvedTarget(target, prev);
|
|
119
122
|
return {
|
|
120
123
|
success: true,
|
|
@@ -190,7 +193,11 @@ class ModelCoordinator extends events_1.EventEmitter {
|
|
|
190
193
|
let compacted = false;
|
|
191
194
|
let compactRequired = false;
|
|
192
195
|
let compactPreflight = 'not_required';
|
|
193
|
-
|
|
196
|
+
// v0.3.25 #271 — gate the preflight on the effective input budgets, not raw
|
|
197
|
+
// context windows: a target with the same (or larger) window but a larger
|
|
198
|
+
// output reserve can still have a smaller safe input budget, and the
|
|
199
|
+
// unchanged history would exceed it without any compact.
|
|
200
|
+
if (prev && resolveProfileSafeInputBudget(target) < resolveProfileSafeInputBudget(prev)) {
|
|
194
201
|
compactRequired = true;
|
|
195
202
|
const safeInputBudget = resolveProfileSafeInputBudget(target);
|
|
196
203
|
const targetTokens = Math.max(1, Math.floor(safeInputBudget * 0.65));
|
|
@@ -9,6 +9,7 @@ const effort_1 = require("../services/effort");
|
|
|
9
9
|
const global_config_1 = require("../services/global-config");
|
|
10
10
|
const session_storage_1 = require("../services/session-storage");
|
|
11
11
|
const token_estimate_1 = require("../utils/token-estimate");
|
|
12
|
+
const model_coordinator_1 = require("./model-coordinator");
|
|
12
13
|
const canonical_1 = require("./protocol/canonical");
|
|
13
14
|
class SessionComposerControlError extends Error {
|
|
14
15
|
constructor(code, message) {
|
|
@@ -96,7 +97,10 @@ class SessionComposerControlServiceV1 {
|
|
|
96
97
|
toModelId: profile.id,
|
|
97
98
|
requestedEffort,
|
|
98
99
|
compacted: false,
|
|
99
|
-
|
|
100
|
+
// v0.3.25 #271 — same gate as the live path: the effective input
|
|
101
|
+
// budgets decide, not raw context windows.
|
|
102
|
+
compactRequired: (0, model_coordinator_1.resolveProfileSafeInputBudget)(profile) <
|
|
103
|
+
(0, model_coordinator_1.resolveProfileSafeInputBudget)(this.resolveProfile(session.model)),
|
|
100
104
|
compactPreflight: 'deferred',
|
|
101
105
|
appliesFrom: 'next-logical-request',
|
|
102
106
|
sessionPreferencesDigest: (0, canonical_1.digestRuntimeValue)({
|
|
@@ -3,6 +3,15 @@ export interface WorkspaceRegistryEntryV1 {
|
|
|
3
3
|
readonly canonicalPath: string;
|
|
4
4
|
readonly label: string;
|
|
5
5
|
readonly lastActivatedAt: string;
|
|
6
|
+
/**
|
|
7
|
+
* v0.3.25 #272 — when the workspace was first registered. Navigation order is
|
|
8
|
+
* derived from this stable timestamp; `lastActivatedAt` no longer reorders
|
|
9
|
+
* the tree when sessions are activated. Documents persisted before this field
|
|
10
|
+
* existed are backfilled from `lastActivatedAt` on parse (and persisted with
|
|
11
|
+
* the backfilled value on the next write), which keeps the migration
|
|
12
|
+
* deterministic.
|
|
13
|
+
*/
|
|
14
|
+
readonly registeredAt: string;
|
|
6
15
|
readonly pinnedOrder?: number;
|
|
7
16
|
}
|
|
8
17
|
export interface WorkspaceRegistryOptions {
|
|
@@ -47,6 +47,7 @@ class WorkspaceRegistryV1 {
|
|
|
47
47
|
canonicalPath,
|
|
48
48
|
label: workspaceLabel(canonicalPath),
|
|
49
49
|
lastActivatedAt: options.activated ? now : (existing?.lastActivatedAt ?? now),
|
|
50
|
+
registeredAt: existing?.registeredAt ?? now,
|
|
50
51
|
...(existing?.pinnedOrder !== undefined ? { pinnedOrder: existing.pinnedOrder } : {}),
|
|
51
52
|
});
|
|
52
53
|
const entries = existing
|
|
@@ -74,6 +75,7 @@ class WorkspaceRegistryV1 {
|
|
|
74
75
|
canonicalPath: path,
|
|
75
76
|
label: workspaceLabel(path),
|
|
76
77
|
lastActivatedAt: path === active ? now : (existing?.lastActivatedAt ?? now),
|
|
78
|
+
registeredAt: existing?.registeredAt ?? now,
|
|
77
79
|
...(existing?.pinnedOrder !== undefined ? { pinnedOrder: existing.pinnedOrder } : {}),
|
|
78
80
|
}));
|
|
79
81
|
}
|
|
@@ -102,6 +104,7 @@ class WorkspaceRegistryV1 {
|
|
|
102
104
|
canonicalPath: target.canonicalPath,
|
|
103
105
|
label: target.label,
|
|
104
106
|
lastActivatedAt: target.lastActivatedAt,
|
|
107
|
+
registeredAt: target.registeredAt,
|
|
105
108
|
...(pinned ? { pinnedOrder: target.pinnedOrder ?? currentMax + 1 } : {}),
|
|
106
109
|
});
|
|
107
110
|
this.writeDocument(document.entries.map(entry => (entry.id === id ? updated : entry)));
|
|
@@ -189,6 +192,11 @@ function parseEntry(value) {
|
|
|
189
192
|
!row.label.trim() ||
|
|
190
193
|
typeof row.lastActivatedAt !== 'string' ||
|
|
191
194
|
!Number.isFinite(Date.parse(row.lastActivatedAt)) ||
|
|
195
|
+
// v0.3.25 #272 — older documents have no registeredAt; backfill from
|
|
196
|
+
// lastActivatedAt so the migration is deterministic and the write-back
|
|
197
|
+
// persists the stable value.
|
|
198
|
+
(row.registeredAt !== undefined &&
|
|
199
|
+
(typeof row.registeredAt !== 'string' || !Number.isFinite(Date.parse(row.registeredAt)))) ||
|
|
192
200
|
(row.pinnedOrder !== undefined &&
|
|
193
201
|
(!Number.isSafeInteger(row.pinnedOrder) || Number(row.pinnedOrder) < 1))) {
|
|
194
202
|
return invalidDocument();
|
|
@@ -198,6 +206,7 @@ function parseEntry(value) {
|
|
|
198
206
|
canonicalPath: row.canonicalPath,
|
|
199
207
|
label: row.label,
|
|
200
208
|
lastActivatedAt: row.lastActivatedAt,
|
|
209
|
+
registeredAt: typeof row.registeredAt === 'string' ? row.registeredAt : row.lastActivatedAt,
|
|
201
210
|
...(row.pinnedOrder !== undefined ? { pinnedOrder: Number(row.pinnedOrder) } : {}),
|
|
202
211
|
});
|
|
203
212
|
}
|
|
@@ -209,8 +218,10 @@ function compareEntries(left, right) {
|
|
|
209
218
|
const rightPinned = right.pinnedOrder ?? Number.MAX_SAFE_INTEGER;
|
|
210
219
|
if (leftPinned !== rightPinned)
|
|
211
220
|
return leftPinned - rightPinned;
|
|
212
|
-
|
|
213
|
-
|
|
221
|
+
// v0.3.25 #272 — stable registration order: activating a session must not
|
|
222
|
+
// reorder the navigation tree, so lastActivatedAt no longer participates.
|
|
223
|
+
const registration = Date.parse(left.registeredAt) - Date.parse(right.registeredAt);
|
|
224
|
+
return registration || left.label.localeCompare(right.label) || left.id.localeCompare(right.id);
|
|
214
225
|
}
|
|
215
226
|
function freezeEntries(entries) {
|
|
216
227
|
return Object.freeze([...entries].sort(compareEntries).map(entry => Object.freeze({ ...entry })));
|
|
@@ -572,11 +572,14 @@ function createGitHistoryReader(options) {
|
|
|
572
572
|
// `--follow` only accepts a single path, which is exactly this case. It explains its own
|
|
573
573
|
// limits for copies and complex merges (plan G6) — we surface `followed` rather than
|
|
574
574
|
// pretending tracking is complete.
|
|
575
|
+
// v0.3.25 #270 — the validated revision must reach the log command: it was
|
|
576
|
+
// previously used only for the existence check, so every query walked HEAD.
|
|
575
577
|
const raw = await runGit([
|
|
576
578
|
'log',
|
|
577
579
|
`--max-count=${limit + 1}`,
|
|
578
580
|
`--format=${LIST_FORMAT}${RECORD_SEPARATOR}`,
|
|
579
581
|
'--follow',
|
|
582
|
+
target,
|
|
580
583
|
'--',
|
|
581
584
|
path,
|
|
582
585
|
]);
|
|
@@ -207,6 +207,15 @@ export declare class GitReadModelServiceV1 {
|
|
|
207
207
|
}): Promise<GitDiffDocumentV2>;
|
|
208
208
|
private versionReaderInstance?;
|
|
209
209
|
private versionReader;
|
|
210
|
+
/**
|
|
211
|
+
* v0.3.25 #269 — reads one working-tree file for the conflict view, refusing
|
|
212
|
+
* to leave the repository: the requested relative path (already guarded as
|
|
213
|
+
* relative and non-sensitive by the version reader) is resolved against the
|
|
214
|
+
* repository root and both the file's real path and its parent's real path
|
|
215
|
+
* must stay inside the root's real path. Returns null instead of throwing so
|
|
216
|
+
* a missing or unreadable file reads as an absent worktree version.
|
|
217
|
+
*/
|
|
218
|
+
private readContainedWorktreeFile;
|
|
210
219
|
/**
|
|
211
220
|
* v0.3.17 S5 — base / ours / theirs / worktree for a conflicted file.
|
|
212
221
|
*
|
|
@@ -6,6 +6,7 @@ exports.assertRelativeGitPath = assertRelativeGitPath;
|
|
|
6
6
|
const child_process_1 = require("child_process");
|
|
7
7
|
const crypto_1 = require("crypto");
|
|
8
8
|
const fs_1 = require("fs");
|
|
9
|
+
const promises_1 = require("fs/promises");
|
|
9
10
|
const path_1 = require("path");
|
|
10
11
|
const redaction_1 = require("../services/redaction");
|
|
11
12
|
const errors_1 = require("./errors");
|
|
@@ -349,9 +350,43 @@ class GitReadModelServiceV1 {
|
|
|
349
350
|
this.versionReaderInstance ?? (this.versionReaderInstance = (0, git_version_service_1.createGitVersionReader)({
|
|
350
351
|
runGit: async (args) => this.runGit(args, await this.requireRepositoryRoot()),
|
|
351
352
|
runGitBytes: async (args) => this.runGitBytes(args, await this.requireRepositoryRoot()),
|
|
353
|
+
readWorktreeFile: async (relativePath) => this.readContainedWorktreeFile(relativePath),
|
|
352
354
|
}));
|
|
353
355
|
return this.versionReaderInstance;
|
|
354
356
|
}
|
|
357
|
+
/**
|
|
358
|
+
* v0.3.25 #269 — reads one working-tree file for the conflict view, refusing
|
|
359
|
+
* to leave the repository: the requested relative path (already guarded as
|
|
360
|
+
* relative and non-sensitive by the version reader) is resolved against the
|
|
361
|
+
* repository root and both the file's real path and its parent's real path
|
|
362
|
+
* must stay inside the root's real path. Returns null instead of throwing so
|
|
363
|
+
* a missing or unreadable file reads as an absent worktree version.
|
|
364
|
+
*/
|
|
365
|
+
async readContainedWorktreeFile(relativePath) {
|
|
366
|
+
let root;
|
|
367
|
+
try {
|
|
368
|
+
root = (0, fs_1.realpathSync)(await this.requireRepositoryRoot());
|
|
369
|
+
}
|
|
370
|
+
catch {
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
try {
|
|
374
|
+
const absolute = (0, path_1.resolve)(root, relativePath);
|
|
375
|
+
const fileReal = (0, fs_1.realpathSync)(absolute);
|
|
376
|
+
const parentReal = (0, fs_1.realpathSync)((0, path_1.resolve)(fileReal, '..'));
|
|
377
|
+
if (fileReal !== root && !fileReal.startsWith(root + path_1.sep))
|
|
378
|
+
return null;
|
|
379
|
+
if (parentReal !== root && !parentReal.startsWith(root + path_1.sep))
|
|
380
|
+
return null;
|
|
381
|
+
const stat = (0, fs_1.statSync)(fileReal);
|
|
382
|
+
if (!stat.isFile())
|
|
383
|
+
return null;
|
|
384
|
+
return await (0, promises_1.readFile)(fileReal);
|
|
385
|
+
}
|
|
386
|
+
catch {
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
355
390
|
/**
|
|
356
391
|
* v0.3.17 S5 — base / ours / theirs / worktree for a conflicted file.
|
|
357
392
|
*
|
|
@@ -66,6 +66,14 @@ export interface GitVersionReaderOptions {
|
|
|
66
66
|
readonly runGit: (args: readonly string[]) => Promise<string>;
|
|
67
67
|
/** Runs git and returns raw bytes, for content that may not be valid UTF-8. */
|
|
68
68
|
readonly runGitBytes: (args: readonly string[]) => Promise<Buffer>;
|
|
69
|
+
/**
|
|
70
|
+
* v0.3.25 #269 — reads one working-tree file relative to the repository root,
|
|
71
|
+
* returning null when it does not exist or cannot be read safely. The
|
|
72
|
+
* `worktree` conflict version must come from the filesystem: `hash-object`
|
|
73
|
+
* without `-w` computes an OID that is not in the object database, so any
|
|
74
|
+
* edited working file made `cat-file` fail the whole conflict response.
|
|
75
|
+
*/
|
|
76
|
+
readonly readWorktreeFile?: (relativePath: string) => Promise<Buffer | null>;
|
|
69
77
|
}
|
|
70
78
|
export declare function createGitVersionReader(options: GitVersionReaderOptions): GitVersionReader;
|
|
71
79
|
//# sourceMappingURL=git-version-service.d.ts.map
|
|
@@ -85,28 +85,25 @@ function parseLfsPointer(head) {
|
|
|
85
85
|
});
|
|
86
86
|
}
|
|
87
87
|
function createGitVersionReader(options) {
|
|
88
|
-
const { runGit, runGitBytes } = options;
|
|
88
|
+
const { runGit, runGitBytes, readWorktreeFile } = options;
|
|
89
|
+
/** Absent version — the label legitimately has no content for this shape. */
|
|
90
|
+
const absentVersion = (label) => Object.freeze({
|
|
91
|
+
label,
|
|
92
|
+
oid: null,
|
|
93
|
+
exists: false,
|
|
94
|
+
binary: false,
|
|
95
|
+
byteSize: 0,
|
|
96
|
+
truncated: false,
|
|
97
|
+
content: null,
|
|
98
|
+
lfs: null,
|
|
99
|
+
});
|
|
89
100
|
/**
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
* being pulled into memory just to be truncated.
|
|
101
|
+
* v0.3.25 #269 — decodes already-fetched bytes into a version, shared by the
|
|
102
|
+
* object-database reader and the working-tree reader so both apply the same
|
|
103
|
+
* size bound, binary probe and LFS-pointer detection.
|
|
94
104
|
*/
|
|
95
|
-
const
|
|
96
|
-
|
|
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;
|
|
105
|
+
const versionFromBytes = (label, oid, bytes) => {
|
|
106
|
+
const byteSize = bytes.length;
|
|
110
107
|
if (byteSize > MAX_VERSION_BYTES) {
|
|
111
108
|
return Object.freeze({
|
|
112
109
|
label,
|
|
@@ -119,7 +116,6 @@ function createGitVersionReader(options) {
|
|
|
119
116
|
lfs: null,
|
|
120
117
|
});
|
|
121
118
|
}
|
|
122
|
-
const bytes = await runGitBytes(['cat-file', 'blob', oid]);
|
|
123
119
|
const probe = bytes.subarray(0, LFS_PROBE_BYTES).toString('utf8');
|
|
124
120
|
const lfs = parseLfsPointer(probe);
|
|
125
121
|
if (lfs) {
|
|
@@ -149,6 +145,31 @@ function createGitVersionReader(options) {
|
|
|
149
145
|
dataUrl,
|
|
150
146
|
});
|
|
151
147
|
};
|
|
148
|
+
/**
|
|
149
|
+
* Reads one blob by OID, bounded, with binary and LFS detection.
|
|
150
|
+
*
|
|
151
|
+
* The size is read first so an enormous historical file is reported by its size instead of
|
|
152
|
+
* being pulled into memory just to be truncated.
|
|
153
|
+
*/
|
|
154
|
+
const readVersion = async (label, oid) => {
|
|
155
|
+
if (oid === null)
|
|
156
|
+
return absentVersion(label);
|
|
157
|
+
const sizeRaw = (await runGit(['cat-file', '-s', oid])).trim();
|
|
158
|
+
const byteSize = Number(sizeRaw) || 0;
|
|
159
|
+
if (byteSize > MAX_VERSION_BYTES) {
|
|
160
|
+
return Object.freeze({
|
|
161
|
+
label,
|
|
162
|
+
oid,
|
|
163
|
+
exists: true,
|
|
164
|
+
binary: false,
|
|
165
|
+
byteSize,
|
|
166
|
+
truncated: true,
|
|
167
|
+
content: null,
|
|
168
|
+
lfs: null,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
return versionFromBytes(label, oid, await runGitBytes(['cat-file', 'blob', oid]));
|
|
172
|
+
};
|
|
152
173
|
return {
|
|
153
174
|
async conflict(pathInput) {
|
|
154
175
|
const path = assertRelativePath(pathInput);
|
|
@@ -168,15 +189,18 @@ function createGitVersionReader(options) {
|
|
|
168
189
|
continue;
|
|
169
190
|
stageOids.set(Number(stage), oid);
|
|
170
191
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
192
|
+
// v0.3.25 #269 — the working file is read from the filesystem. The previous
|
|
193
|
+
// `hash-object` (without -w) → `cat-file` chain only worked while the edited
|
|
194
|
+
// content happened to match an object git already stored; any real edit made
|
|
195
|
+
// `cat-file` fail the entire conflict response.
|
|
196
|
+
const worktreeBytes = await readWorktreeFile?.(path);
|
|
175
197
|
const [base, ours, theirs, worktree] = await Promise.all([
|
|
176
198
|
readVersion('base', stageOids.get(1) ?? null),
|
|
177
199
|
readVersion('ours', stageOids.get(2) ?? null),
|
|
178
200
|
readVersion('theirs', stageOids.get(3) ?? null),
|
|
179
|
-
|
|
201
|
+
worktreeBytes
|
|
202
|
+
? Promise.resolve(versionFromBytes('worktree', null, worktreeBytes))
|
|
203
|
+
: Promise.resolve(absentVersion('worktree')),
|
|
180
204
|
]);
|
|
181
205
|
const hasBase = stageOids.has(1);
|
|
182
206
|
const hasOurs = stageOids.has(2);
|
package/dist/web/protocol.d.ts
CHANGED
|
@@ -361,6 +361,11 @@ export interface WebWorkspaceSummaryV1 {
|
|
|
361
361
|
readonly available: boolean;
|
|
362
362
|
readonly sessionCount: number;
|
|
363
363
|
readonly lastActivatedAt: string;
|
|
364
|
+
/**
|
|
365
|
+
* v0.3.25 #272 — when the workspace was first registered. The navigation's
|
|
366
|
+
* stable default order is derived from this; optional for older Hosts.
|
|
367
|
+
*/
|
|
368
|
+
readonly registeredAt?: string;
|
|
364
369
|
readonly pinnedOrder?: number;
|
|
365
370
|
}
|
|
366
371
|
export interface WebWorkspaceProjectSummaryV1 {
|
|
@@ -238,6 +238,7 @@ class WebWorkbenchController {
|
|
|
238
238
|
available,
|
|
239
239
|
sessionCount: counts.get(entry.canonicalPath) ?? 0,
|
|
240
240
|
lastActivatedAt: entry.lastActivatedAt,
|
|
241
|
+
registeredAt: entry.registeredAt,
|
|
241
242
|
...(entry.pinnedOrder !== undefined ? { pinnedOrder: entry.pinnedOrder } : {}),
|
|
242
243
|
});
|
|
243
244
|
}));
|
|
@@ -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-kQqA3li6.js";import{u as Ce,R as Ne}from"./useAutoNotice-DPXFWIwK.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};
|