@orion-agents/orion-code 0.3.25 → 0.3.26
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 +34 -9
- package/README.md +8 -8
- package/README.zh-CN.md +6 -6
- package/dist/runtime/legacy-thread-materializer.d.ts +13 -0
- package/dist/runtime/legacy-thread-materializer.js +72 -0
- package/dist/services/session-storage.js +21 -0
- package/dist/web/git-diff-document.d.ts +19 -0
- package/dist/web/git-diff-document.js +46 -2
- package/dist/web/git-read-model-service.js +76 -5
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -22,15 +22,40 @@ which is **not** a pass.
|
|
|
22
22
|
|
|
23
23
|
## [Unreleased]
|
|
24
24
|
|
|
25
|
-
## [0.3.
|
|
26
|
-
|
|
27
|
-
> **Status: candidate.** Open.
|
|
28
|
-
>
|
|
29
|
-
>
|
|
30
|
-
>
|
|
31
|
-
>
|
|
32
|
-
>
|
|
33
|
-
|
|
25
|
+
## [0.3.26] — UNRELEASED
|
|
26
|
+
|
|
27
|
+
> **Status: candidate.** Open. Structured-diff integrity and Session lifecycle: body
|
|
28
|
+
> rows that look like file headers stay body rows (#274), a hunk split across
|
|
29
|
+
> pagination keeps its rows, line numbers and identity (#275), permanent Session
|
|
30
|
+
> deletion removes the v2 Thread records (#276), and exiting a cutover Session no
|
|
31
|
+
> longer rewrites its summary from the frozen legacy JSONL (#277). Plan:
|
|
32
|
+
> `docs/plan/v0.3.26-plan.md`. Not merged, tagged or published.
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
|
|
36
|
+
- #274: a deletion row whose text starts with `-- ` (`--- old comment`) is no longer
|
|
37
|
+
mistaken for the old-file header; the hunk stays open and the remaining body is
|
|
38
|
+
no longer demoted to document metadata.
|
|
39
|
+
- #275: the structured diff continuation contract carries the split hunk's state
|
|
40
|
+
(header, index, running line cursors) in the cursor, so every page of a large
|
|
41
|
+
hunk keeps its rows with correct line numbers and the stable hunk identity, and
|
|
42
|
+
the last page reports the true completeness.
|
|
43
|
+
- #276: permanently deleting a cutover Session removes its v2 records — cutover
|
|
44
|
+
index entry, event log, head, projection, compact state and checkpoints,
|
|
45
|
+
session-index manifest, transcript pages and import receipt — under the same
|
|
46
|
+
file-lock and atomic-write discipline as the cutover itself.
|
|
47
|
+
- #277: `/exit` (and the shutdown path) no longer rewrites a cutover Session's
|
|
48
|
+
`messageCount`/`historySizeBytes` from the frozen legacy JSONL; the summary comes
|
|
49
|
+
from the authoritative v2 Thread.
|
|
50
|
+
|
|
51
|
+
## [0.3.25] — 2026-10-13
|
|
52
|
+
|
|
53
|
+
> **Status: published.** Released as `@orion-agents/orion-code@0.3.25` from
|
|
54
|
+
> `refs/tags/v0.3.25`, and merged to `main` as `c356323` (#273). Four open-issue fixes
|
|
55
|
+
> for the Git conflict view (#269), file-history revisions (#270), the model-switch
|
|
56
|
+
> compact preflight (#271) and the stable project navigation order (#272). Plan:
|
|
57
|
+
> `docs/plan/v0.3.25-plan.md`. GitHub Release:
|
|
58
|
+
> https://github.com/orion-agents/orion-code/releases/tag/v0.3.25
|
|
34
59
|
|
|
35
60
|
### Fixed
|
|
36
61
|
|
package/README.md
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
Local-first, goal-driven coding agent for the terminal and browser.
|
|
4
4
|
|
|
5
|
-
> v0.3.
|
|
6
|
-
>
|
|
7
|
-
> (#
|
|
8
|
-
>
|
|
9
|
-
> release 0.3.
|
|
10
|
-
> released on GitHub.
|
|
5
|
+
> v0.3.26 candidate — open. Structured-diff integrity and Session lifecycle: header-lookalike
|
|
6
|
+
> body rows stay body rows (#274), hunks split across pagination keep their rows and line
|
|
7
|
+
> numbers (#275), permanent Session deletion removes the v2 Thread records (#276), and
|
|
8
|
+
> exiting a cutover Session no longer regresses its summary from the legacy JSONL (#277).
|
|
9
|
+
> The previous release 0.3.25 is on npm (registry `latest`), tagged `v0.3.25` (`c356323`,
|
|
10
|
+
> #273) and released on GitHub.
|
|
11
11
|
|
|
12
12
|
[中文说明](README.zh-CN.md) ·
|
|
13
13
|
[v0.3.24 plan](docs/plan/v0.3.24-plan.md) ·
|
|
@@ -170,10 +170,10 @@ user extension boundaries.
|
|
|
170
170
|
Node.js 22.12+, 24, and 26 are supported. Use Node 24 LTS for production or Node 26 Current for
|
|
171
171
|
current development environments. Node 20 is upstream EOL and is no longer a v0.3 runtime.
|
|
172
172
|
|
|
173
|
-
To install the pinned v0.3.
|
|
173
|
+
To install the pinned v0.3.26 package when it is available on npm:
|
|
174
174
|
|
|
175
175
|
```bash
|
|
176
|
-
npm install -g @orion-agents/orion-code@0.3.
|
|
176
|
+
npm install -g @orion-agents/orion-code@0.3.26
|
|
177
177
|
orion --version
|
|
178
178
|
orion doctor
|
|
179
179
|
```
|
package/README.zh-CN.md
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
面向终端与浏览器、本地优先的目标驱动 Coding Agent。
|
|
4
4
|
|
|
5
|
-
> v0.3.
|
|
6
|
-
>
|
|
7
|
-
>
|
|
8
|
-
> (registry `latest`)、打 tag `v0.3.
|
|
5
|
+
> v0.3.26 候选版本:进行中。结构化 diff 完整性与会话生命周期:形似文件头的 body 行保持为
|
|
6
|
+
> body 行(#274)、跨页拆分的 hunk 保留行与行号(#275)、永久删除会话同时清除 v2 Thread
|
|
7
|
+
> 记录(#276)、退出已切换会话不再从旧 JSONL 回退 summary(#277)。上一版 0.3.25 已发布
|
|
8
|
+
> npm(registry `latest`)、打 tag `v0.3.25`(`c356323`,#273)并发布 GitHub Release。
|
|
9
9
|
|
|
10
10
|
[English](README.md) ·
|
|
11
11
|
[v0.3.24 方案](docs/plan/v0.3.24-plan.md) ·
|
|
@@ -100,10 +100,10 @@ MCP 仍是用户可见的扩展边界。
|
|
|
100
100
|
支持 Node.js 22.12+、24 和 26。生产环境建议使用 Node 24 LTS,当前开发环境也支持 Node 26
|
|
101
101
|
Current。Node 20 已结束上游维护,不再属于 v0.3 Runtime 合同。
|
|
102
102
|
|
|
103
|
-
v0.3.
|
|
103
|
+
v0.3.26 在 npm 可用后,可安装固定版本:
|
|
104
104
|
|
|
105
105
|
```bash
|
|
106
|
-
npm install -g @orion-agents/orion-code@0.3.
|
|
106
|
+
npm install -g @orion-agents/orion-code@0.3.26
|
|
107
107
|
orion --version
|
|
108
108
|
orion doctor
|
|
109
109
|
```
|
|
@@ -102,4 +102,17 @@ export declare function openSessionCheckpointStorageV1(projectPathInput: string,
|
|
|
102
102
|
*/
|
|
103
103
|
export declare function openSessionStorageV1(projectPathInput: string, sessionId: string): OpenedSessionStorageV1;
|
|
104
104
|
export declare function loadThreadCutoverIndexV1(projectPath: string): ThreadCutoverIndexV1;
|
|
105
|
+
/**
|
|
106
|
+
* v0.3.26 #276 — permanently removes one Session's durable v2 records: the
|
|
107
|
+
* cutover index entry plus every thread file the materializer and the event
|
|
108
|
+
* store publish (event log, projection, head, compact state and checkpoints,
|
|
109
|
+
* session-index manifest, transcript pages, import receipt). Returns the
|
|
110
|
+
* removed thread id, or null when the Session was never cut over.
|
|
111
|
+
*
|
|
112
|
+
* The index write takes the same file lock and atomic-write discipline as the
|
|
113
|
+
* cutover itself, with the generation bumped; an emptied index keeps its file
|
|
114
|
+
* with an empty session map so readers stay on the atomic authority instead of
|
|
115
|
+
* treating a missing file as "no v2 Sessions at all".
|
|
116
|
+
*/
|
|
117
|
+
export declare function deleteSessionThreadRecordsV1(projectPathInput: string, sessionId: string): string | null;
|
|
105
118
|
//# sourceMappingURL=legacy-thread-materializer.d.ts.map
|
|
@@ -7,6 +7,7 @@ exports.resolveSessionStorageV1 = resolveSessionStorageV1;
|
|
|
7
7
|
exports.openSessionCheckpointStorageV1 = openSessionCheckpointStorageV1;
|
|
8
8
|
exports.openSessionStorageV1 = openSessionStorageV1;
|
|
9
9
|
exports.loadThreadCutoverIndexV1 = loadThreadCutoverIndexV1;
|
|
10
|
+
exports.deleteSessionThreadRecordsV1 = deleteSessionThreadRecordsV1;
|
|
10
11
|
const fs_1 = require("fs");
|
|
11
12
|
const path_1 = require("path");
|
|
12
13
|
const atomic_write_1 = require("../services/atomic-write");
|
|
@@ -205,6 +206,77 @@ function openSessionStorageV1(projectPathInput, sessionId) {
|
|
|
205
206
|
function loadThreadCutoverIndexV1(projectPath) {
|
|
206
207
|
return readThreadCutoverIndex((0, paths_1.getProjectThreadsV2IndexPath)((0, path_1.resolve)(projectPath)));
|
|
207
208
|
}
|
|
209
|
+
/**
|
|
210
|
+
* v0.3.26 #276 — permanently removes one Session's durable v2 records: the
|
|
211
|
+
* cutover index entry plus every thread file the materializer and the event
|
|
212
|
+
* store publish (event log, projection, head, compact state and checkpoints,
|
|
213
|
+
* session-index manifest, transcript pages, import receipt). Returns the
|
|
214
|
+
* removed thread id, or null when the Session was never cut over.
|
|
215
|
+
*
|
|
216
|
+
* The index write takes the same file lock and atomic-write discipline as the
|
|
217
|
+
* cutover itself, with the generation bumped; an emptied index keeps its file
|
|
218
|
+
* with an empty session map so readers stay on the atomic authority instead of
|
|
219
|
+
* treating a missing file as "no v2 Sessions at all".
|
|
220
|
+
*/
|
|
221
|
+
function deleteSessionThreadRecordsV1(projectPathInput, sessionId) {
|
|
222
|
+
const projectPath = (0, path_1.resolve)(projectPathInput);
|
|
223
|
+
const indexPath = (0, paths_1.getProjectThreadsV2IndexPath)(projectPath);
|
|
224
|
+
// A project that never cut a Session over has no index and no threads-v2
|
|
225
|
+
// directory; taking the lock would create one on a pure legacy deletion.
|
|
226
|
+
if (!(0, fs_1.existsSync)(indexPath))
|
|
227
|
+
return null;
|
|
228
|
+
const threadsDir = (0, paths_1.getProjectThreadsV2Dir)(projectPath);
|
|
229
|
+
return (0, file_lock_1.withFileLockSync)(indexPath, () => {
|
|
230
|
+
const current = readThreadCutoverIndex(indexPath);
|
|
231
|
+
const entry = current.sessions[sessionId];
|
|
232
|
+
if (!entry)
|
|
233
|
+
return null;
|
|
234
|
+
const sessions = { ...current.sessions };
|
|
235
|
+
delete sessions[sessionId];
|
|
236
|
+
const content = { version: 1, generation: current.generation + 1, sessions };
|
|
237
|
+
const next = deepFreeze({ ...content, digest: (0, canonical_1.digestRuntimeValue)(content) });
|
|
238
|
+
(0, atomic_write_1.atomicWriteFileSync)(indexPath, `${(0, canonical_1.canonicalRuntimeJson)(next)}\n`, {
|
|
239
|
+
mode: 0o600,
|
|
240
|
+
fsync: true,
|
|
241
|
+
});
|
|
242
|
+
removeThreadFiles(threadsDir, projectPath, entry.threadId);
|
|
243
|
+
return entry.threadId;
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
/** v0.3.26 #276 — removes every durable file of one thread. Best effort per
|
|
247
|
+
* file: a missing artifact is already deleted, and an unlink failure must not
|
|
248
|
+
* resurrect the cutover entry that was just removed. */
|
|
249
|
+
function removeThreadFiles(threadsDir, projectPath, threadId) {
|
|
250
|
+
const files = [
|
|
251
|
+
(0, path_1.join)(threadsDir, `${threadId}.events.v1.jsonl`),
|
|
252
|
+
(0, path_1.join)(threadsDir, `${threadId}.projection.v1.json`),
|
|
253
|
+
(0, path_1.join)(threadsDir, `${threadId}.head.v1.json`),
|
|
254
|
+
(0, path_1.join)(threadsDir, `${threadId}.compact-state.v1.json`),
|
|
255
|
+
(0, path_1.join)(threadsDir, `${threadId}.session-index.v1.json`),
|
|
256
|
+
(0, paths_1.getProjectThreadV2ImportReceiptPath)(projectPath, threadId),
|
|
257
|
+
];
|
|
258
|
+
for (const file of files) {
|
|
259
|
+
try {
|
|
260
|
+
if ((0, fs_1.existsSync)(file))
|
|
261
|
+
(0, fs_1.unlinkSync)(file);
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
// Durable leftovers are reported by the integrity checks, never by
|
|
265
|
+
// throwing here: the cutover entry removal is already committed.
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
for (const dir of [
|
|
269
|
+
(0, path_1.join)(threadsDir, `${threadId}.compact-checkpoints.v1`),
|
|
270
|
+
(0, path_1.join)(threadsDir, `${threadId}.transcript-pages.v1`),
|
|
271
|
+
]) {
|
|
272
|
+
try {
|
|
273
|
+
(0, fs_1.rmSync)(dir, { recursive: true, force: true });
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
// See above.
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
208
280
|
function buildMaterializedEvents(snapshot) {
|
|
209
281
|
const receipt = snapshot.receipt;
|
|
210
282
|
const pending = [
|
|
@@ -1858,6 +1858,21 @@ function updateSessionSummary(sessionId, _messages) {
|
|
|
1858
1858
|
// Callers may hold a stale transcript snapshot. Re-read under the same lock
|
|
1859
1859
|
// used by append/delete so reconciliation can never roll metadata backwards.
|
|
1860
1860
|
withLockedSession(sessionId, session => {
|
|
1861
|
+
// v0.3.26 #277 — a cutover Session's summary comes from the authoritative
|
|
1862
|
+
// v2 Thread. Its legacy JSONL is frozen at cutover, so summarizing from it
|
|
1863
|
+
// (as /exit and the shutdown path do) would move messageCount and
|
|
1864
|
+
// historySizeBytes backwards while the Thread history is intact. Reopening
|
|
1865
|
+
// the Session repairs the read model from v2; this makes the exit path
|
|
1866
|
+
// agree instead of regressing it.
|
|
1867
|
+
const threadView = (0, thread_session_view_1.loadThreadSessionViewV1)(session.projectPath, session.id);
|
|
1868
|
+
if (threadView) {
|
|
1869
|
+
session.messageCount = threadView.messageCount;
|
|
1870
|
+
session.historySizeBytes = threadView.historySizeBytes;
|
|
1871
|
+
session.updatedAt = threadView.updatedAt;
|
|
1872
|
+
session.updatedAtIso = new Date(session.updatedAt).toISOString();
|
|
1873
|
+
writeSessionMetaUnlocked(session);
|
|
1874
|
+
return;
|
|
1875
|
+
}
|
|
1861
1876
|
const summary = summarizeSessionMessages(readSessionMessagesForSession(session));
|
|
1862
1877
|
session.toolsUsed = summary.toolsUsed;
|
|
1863
1878
|
session.filesModified = summary.filesModified;
|
|
@@ -2553,6 +2568,12 @@ function deleteSession(sessionId) {
|
|
|
2553
2568
|
updateSessionCatalog(catalog => {
|
|
2554
2569
|
delete catalog.sessions[sessionId];
|
|
2555
2570
|
});
|
|
2571
|
+
// v0.3.26 #276 — a cutover Session's v2 records are the authoritative
|
|
2572
|
+
// history; permanent deletion must remove them with the legacy files.
|
|
2573
|
+
// The Web deletion dialog promises exactly that.
|
|
2574
|
+
if ((0, legacy_thread_materializer_1.deleteSessionThreadRecordsV1)(session.projectPath, sessionId)) {
|
|
2575
|
+
deleted = true;
|
|
2576
|
+
}
|
|
2556
2577
|
return deleted;
|
|
2557
2578
|
}) ?? false);
|
|
2558
2579
|
}
|
|
@@ -41,6 +41,18 @@ export interface GitHunkV2 {
|
|
|
41
41
|
readonly complete: boolean;
|
|
42
42
|
}
|
|
43
43
|
export type GitDiffDocumentKindV2 = 'text' | 'binary' | 'symlink' | 'submodule' | 'metadata';
|
|
44
|
+
/**
|
|
45
|
+
* v0.3.26 #275 — parser state for a hunk that a page boundary split. Carried in
|
|
46
|
+
* the continuation cursor so the next page re-opens the hunk with its stable
|
|
47
|
+
* identity (`index:oldStart:newStart`) and correct running line cursors instead
|
|
48
|
+
* of demoting the continuation rows to document meta.
|
|
49
|
+
*/
|
|
50
|
+
export interface GitDiffHunkResumeV1 {
|
|
51
|
+
readonly header: string;
|
|
52
|
+
readonly index: number;
|
|
53
|
+
readonly oldCursor: number;
|
|
54
|
+
readonly newCursor: number;
|
|
55
|
+
}
|
|
44
56
|
export interface GitDiffCapabilitiesV2 {
|
|
45
57
|
readonly stageFile: boolean;
|
|
46
58
|
readonly unstageFile: boolean;
|
|
@@ -63,6 +75,11 @@ export interface GitDiffDocumentV2 {
|
|
|
63
75
|
readonly hunks: readonly GitHunkV2[];
|
|
64
76
|
/** Metadata lines that are not part of any hunk (file headers, mode changes, …). */
|
|
65
77
|
readonly meta: readonly string[];
|
|
78
|
+
/**
|
|
79
|
+
* v0.3.26 #275 — the hunk still open when this page ended, for the
|
|
80
|
+
* continuation cursor. Null when the page ends on a hunk boundary.
|
|
81
|
+
*/
|
|
82
|
+
readonly pendingHunk: GitDiffHunkResumeV1 | null;
|
|
66
83
|
readonly completeness: 'complete' | 'paged' | 'limited';
|
|
67
84
|
readonly additions: number;
|
|
68
85
|
readonly deletions: number;
|
|
@@ -79,5 +96,7 @@ export declare function parseGitDiffDocument(input: {
|
|
|
79
96
|
readonly hasMore: boolean;
|
|
80
97
|
/** Host-signed continuation cursor, or null. Never synthesised here. */
|
|
81
98
|
readonly nextCursor: string | null;
|
|
99
|
+
/** v0.3.26 #275 — hunk state from the previous page's `pendingHunk`. */
|
|
100
|
+
readonly resume?: GitDiffHunkResumeV1 | null;
|
|
82
101
|
}): GitDiffDocumentV2;
|
|
83
102
|
//# sourceMappingURL=git-diff-document.d.ts.map
|
|
@@ -75,6 +75,31 @@ function parseGitDiffDocument(input) {
|
|
|
75
75
|
let additions = 0;
|
|
76
76
|
let deletions = 0;
|
|
77
77
|
let current;
|
|
78
|
+
const openHunk = (header, index, oldCursor, newCursor) => {
|
|
79
|
+
const headerMatch = HUNK_HEADER.exec(header);
|
|
80
|
+
if (!headerMatch)
|
|
81
|
+
return;
|
|
82
|
+
const oldStart = Number(headerMatch[1]);
|
|
83
|
+
const newStart = Number(headerMatch[3]);
|
|
84
|
+
current = {
|
|
85
|
+
header,
|
|
86
|
+
oldStart,
|
|
87
|
+
oldLines: headerMatch[2] === undefined ? 1 : Number(headerMatch[2]),
|
|
88
|
+
newStart,
|
|
89
|
+
newLines: headerMatch[4] === undefined ? 1 : Number(headerMatch[4]),
|
|
90
|
+
contextLabel: (headerMatch[5] ?? '').trim(),
|
|
91
|
+
lines: [],
|
|
92
|
+
oldCursor,
|
|
93
|
+
newCursor,
|
|
94
|
+
index,
|
|
95
|
+
};
|
|
96
|
+
};
|
|
97
|
+
// v0.3.26 #275 — a continuation page starts mid-hunk: re-open it from the
|
|
98
|
+
// resume state before the first line is classified. A page that happens to
|
|
99
|
+
// start with its own `@@` header simply ignores the resume state.
|
|
100
|
+
if (input.resume && !HUNK_HEADER.test(input.lines[0] ?? '')) {
|
|
101
|
+
openHunk(input.resume.header, input.resume.index, input.resume.oldCursor, input.resume.newCursor);
|
|
102
|
+
}
|
|
78
103
|
const closeHunk = () => {
|
|
79
104
|
if (!current)
|
|
80
105
|
return;
|
|
@@ -134,14 +159,22 @@ function parseGitDiffDocument(input) {
|
|
|
134
159
|
}));
|
|
135
160
|
continue;
|
|
136
161
|
}
|
|
137
|
-
|
|
162
|
+
// v0.3.26 #274 — the body marker decides first. A deletion of a line that
|
|
163
|
+
// itself starts with `-- ` produces the body row `--- old comment`, which a
|
|
164
|
+
// header-first check mistook for the old-file header, closing the hunk and
|
|
165
|
+
// demoting the remaining body to document meta. Genuine document-level lines
|
|
166
|
+
// carry no body marker by construction: context rows start with a space,
|
|
167
|
+
// additions with `+`, deletions with `-`, and file boundaries are announced
|
|
168
|
+
// by a markerless `diff --git` line.
|
|
169
|
+
const marker = raw[0];
|
|
170
|
+
const isBodyRow = marker === '+' || marker === '-' || marker === ' ' || raw === '';
|
|
171
|
+
if (!isBodyRow && isMetaLine(raw)) {
|
|
138
172
|
// A file boundary inside one page (multi-command reads) closes the current hunk.
|
|
139
173
|
closeHunk();
|
|
140
174
|
if (raw)
|
|
141
175
|
meta.push(raw);
|
|
142
176
|
continue;
|
|
143
177
|
}
|
|
144
|
-
const marker = raw[0];
|
|
145
178
|
const text = raw.length > 0 ? raw.slice(1) : '';
|
|
146
179
|
let lineKind;
|
|
147
180
|
let oldLineNumber = null;
|
|
@@ -185,6 +218,16 @@ function parseGitDiffDocument(input) {
|
|
|
185
218
|
raw,
|
|
186
219
|
}));
|
|
187
220
|
}
|
|
221
|
+
// v0.3.26 #275 — a page that ends inside a hunk publishes the hunk state for
|
|
222
|
+
// the continuation cursor; a page ending on a boundary has nothing to resume.
|
|
223
|
+
const pendingHunk = input.hasMore && current
|
|
224
|
+
? Object.freeze({
|
|
225
|
+
header: current.header,
|
|
226
|
+
index: current.index,
|
|
227
|
+
oldCursor: current.oldCursor,
|
|
228
|
+
newCursor: current.newCursor,
|
|
229
|
+
})
|
|
230
|
+
: null;
|
|
188
231
|
closeHunk();
|
|
189
232
|
const frozenHunks = Object.freeze(hunks);
|
|
190
233
|
let completeness = 'complete';
|
|
@@ -203,6 +246,7 @@ function parseGitDiffDocument(input) {
|
|
|
203
246
|
binary,
|
|
204
247
|
hunks: frozenHunks,
|
|
205
248
|
meta: Object.freeze(meta),
|
|
249
|
+
pendingHunk,
|
|
206
250
|
completeness,
|
|
207
251
|
additions,
|
|
208
252
|
deletions,
|
|
@@ -236,7 +236,13 @@ class GitReadModelServiceV1 {
|
|
|
236
236
|
*/
|
|
237
237
|
async diffDocument(input) {
|
|
238
238
|
const page = await this.diff(input);
|
|
239
|
-
|
|
239
|
+
// v0.3.26 #275 — the incoming cursor may carry the hunk state this page
|
|
240
|
+
// continues; diff() already validated the cursor, so decoding it here only
|
|
241
|
+
// reads the resume state.
|
|
242
|
+
const resumeHunk = input.cursor
|
|
243
|
+
? this.decodeCursor(input.cursor, 'diff', page.repositoryRevision, page.fileId, diffScope(input.ignoreWhitespace === true, input.wordDiff === true)).resumeHunk
|
|
244
|
+
: undefined;
|
|
245
|
+
const document = (0, git_diff_document_1.parseGitDiffDocument)({
|
|
240
246
|
fileToken: page.fileId,
|
|
241
247
|
path: page.path,
|
|
242
248
|
source: page.source,
|
|
@@ -244,7 +250,18 @@ class GitReadModelServiceV1 {
|
|
|
244
250
|
lines: page.lines,
|
|
245
251
|
hasMore: page.nextCursor !== null,
|
|
246
252
|
nextCursor: page.nextCursor,
|
|
253
|
+
...(resumeHunk ? { resume: resumeHunk } : {}),
|
|
247
254
|
});
|
|
255
|
+
// A page that ends inside a hunk re-mints the cursor with the hunk state so
|
|
256
|
+
// the next page resumes it instead of demoting the rows to meta.
|
|
257
|
+
if (page.nextCursor && document.pendingHunk) {
|
|
258
|
+
const decoded = this.decodeCursor(page.nextCursor, 'diff', page.repositoryRevision, page.fileId, diffScope(input.ignoreWhitespace === true, input.wordDiff === true));
|
|
259
|
+
return Object.freeze({
|
|
260
|
+
...document,
|
|
261
|
+
nextCursor: this.encodeCursor({ ...decoded, resumeHunk: document.pendingHunk }),
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
return document;
|
|
248
265
|
}
|
|
249
266
|
historyReader() {
|
|
250
267
|
this.historyReaderInstance ?? (this.historyReaderInstance = (0, git_history_service_1.createGitHistoryReader)({
|
|
@@ -300,7 +317,8 @@ class GitReadModelServiceV1 {
|
|
|
300
317
|
.update(`compare-diff:${input.baseOid}:${input.headOid}:${path}:${snapshot.revision}`)
|
|
301
318
|
.digest('hex')
|
|
302
319
|
.slice(0, 24);
|
|
303
|
-
const
|
|
320
|
+
const cursorState = input.cursor ? decodeCommitDiffCursor(input.cursor, margin) : null;
|
|
321
|
+
const offset = cursorState?.offset ?? 0;
|
|
304
322
|
const page = await streamDiffPage({
|
|
305
323
|
cwd: root,
|
|
306
324
|
commands: Object.freeze([
|
|
@@ -339,9 +357,19 @@ class GitReadModelServiceV1 {
|
|
|
339
357
|
nextCursor: page.hasMore
|
|
340
358
|
? encodeCommitDiffCursor({ margin, offset: offset + page.returnedLines })
|
|
341
359
|
: null,
|
|
360
|
+
...(cursorState?.resumeHunk ? { resume: cursorState.resumeHunk } : {}),
|
|
342
361
|
});
|
|
362
|
+
// v0.3.26 #275 — re-mint the continuation cursor with the split hunk state.
|
|
363
|
+
const nextCursor = page.hasMore && document.pendingHunk
|
|
364
|
+
? encodeCommitDiffCursor({
|
|
365
|
+
margin,
|
|
366
|
+
offset: offset + page.returnedLines,
|
|
367
|
+
resumeHunk: document.pendingHunk,
|
|
368
|
+
})
|
|
369
|
+
: document.nextCursor;
|
|
343
370
|
return Object.freeze({
|
|
344
371
|
...document,
|
|
372
|
+
nextCursor,
|
|
345
373
|
baseOid: input.baseOid,
|
|
346
374
|
baseLabel: `比较 ${input.baseOid.slice(0, 10)} → ${input.headOid.slice(0, 10)}`,
|
|
347
375
|
});
|
|
@@ -450,7 +478,8 @@ class GitReadModelServiceV1 {
|
|
|
450
478
|
.update(`commit-diff:${detail.id}:${detail.baseOid}:${parentIndex}:${snapshot.revision}`)
|
|
451
479
|
.digest('hex')
|
|
452
480
|
.slice(0, 24);
|
|
453
|
-
const
|
|
481
|
+
const cursorState = input.cursor ? decodeCommitDiffCursor(input.cursor, margin) : null;
|
|
482
|
+
const offset = cursorState?.offset ?? 0;
|
|
454
483
|
const page = await streamDiffPage({
|
|
455
484
|
cwd: root,
|
|
456
485
|
commands: Object.freeze([
|
|
@@ -489,8 +518,22 @@ class GitReadModelServiceV1 {
|
|
|
489
518
|
nextCursor: page.hasMore
|
|
490
519
|
? encodeCommitDiffCursor({ margin, offset: offset + page.returnedLines })
|
|
491
520
|
: null,
|
|
521
|
+
...(cursorState?.resumeHunk ? { resume: cursorState.resumeHunk } : {}),
|
|
522
|
+
});
|
|
523
|
+
// v0.3.26 #275 — re-mint the continuation cursor with the split hunk state.
|
|
524
|
+
const nextCursor = page.hasMore && document.pendingHunk
|
|
525
|
+
? encodeCommitDiffCursor({
|
|
526
|
+
margin,
|
|
527
|
+
offset: offset + page.returnedLines,
|
|
528
|
+
resumeHunk: document.pendingHunk,
|
|
529
|
+
})
|
|
530
|
+
: document.nextCursor;
|
|
531
|
+
return Object.freeze({
|
|
532
|
+
...document,
|
|
533
|
+
nextCursor,
|
|
534
|
+
baseLabel: detail.baseLabel,
|
|
535
|
+
baseOid: detail.baseOid,
|
|
492
536
|
});
|
|
493
|
-
return Object.freeze({ ...document, baseLabel: detail.baseLabel, baseOid: detail.baseOid });
|
|
494
537
|
}
|
|
495
538
|
async capture() {
|
|
496
539
|
let root;
|
|
@@ -1211,6 +1254,9 @@ class GitReadModelServiceV1 {
|
|
|
1211
1254
|
parsed.offset < 0) {
|
|
1212
1255
|
return invalidCursor();
|
|
1213
1256
|
}
|
|
1257
|
+
if (parsed.resumeHunk !== undefined && !isDiffHunkResume(parsed.resumeHunk)) {
|
|
1258
|
+
return invalidCursor();
|
|
1259
|
+
}
|
|
1214
1260
|
if (parsed.revision !== revision) {
|
|
1215
1261
|
throw new errors_1.WebWorkbenchError(409, 'Git cursor revision is stale.', 'git_revision_conflict');
|
|
1216
1262
|
}
|
|
@@ -1224,6 +1270,22 @@ class GitReadModelServiceV1 {
|
|
|
1224
1270
|
}
|
|
1225
1271
|
}
|
|
1226
1272
|
exports.GitReadModelServiceV1 = GitReadModelServiceV1;
|
|
1273
|
+
/** v0.3.26 #275 — the hunk header shape, mirrored for cursor validation. */
|
|
1274
|
+
const HUNK_HEADER_PATTERN = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)$/u;
|
|
1275
|
+
/** v0.3.26 #275 — validates a cursor-carried hunk resume state. */
|
|
1276
|
+
function isDiffHunkResume(value) {
|
|
1277
|
+
if (!value || typeof value !== 'object')
|
|
1278
|
+
return false;
|
|
1279
|
+
const candidate = value;
|
|
1280
|
+
return (typeof candidate.header === 'string' &&
|
|
1281
|
+
HUNK_HEADER_PATTERN.test(candidate.header) &&
|
|
1282
|
+
Number.isSafeInteger(candidate.index) &&
|
|
1283
|
+
candidate.index >= 0 &&
|
|
1284
|
+
Number.isSafeInteger(candidate.oldCursor) &&
|
|
1285
|
+
candidate.oldCursor >= 1 &&
|
|
1286
|
+
Number.isSafeInteger(candidate.newCursor) &&
|
|
1287
|
+
candidate.newCursor >= 1);
|
|
1288
|
+
}
|
|
1227
1289
|
function parsePorcelainV1(output) {
|
|
1228
1290
|
const fields = output.split('\0');
|
|
1229
1291
|
const records = [];
|
|
@@ -1692,7 +1754,16 @@ function decodeCommitDiffCursor(cursor, margin) {
|
|
|
1692
1754
|
candidate.offset < 0) {
|
|
1693
1755
|
throw new errors_1.WebWorkbenchError(409, 'Diff changed; reload before paginating.', 'git_cursor_invalid');
|
|
1694
1756
|
}
|
|
1695
|
-
|
|
1757
|
+
if (candidate.resumeHunk !== undefined && !isDiffHunkResume(candidate.resumeHunk)) {
|
|
1758
|
+
throw new errors_1.WebWorkbenchError(400, 'Diff cursor is not valid.', 'git_cursor_invalid');
|
|
1759
|
+
}
|
|
1760
|
+
return {
|
|
1761
|
+
margin,
|
|
1762
|
+
offset: candidate.offset,
|
|
1763
|
+
...(candidate.resumeHunk !== undefined
|
|
1764
|
+
? { resumeHunk: candidate.resumeHunk }
|
|
1765
|
+
: {}),
|
|
1766
|
+
};
|
|
1696
1767
|
}
|
|
1697
1768
|
/**
|
|
1698
1769
|
* Kills a detached child and everything it spawned.
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orion-agents/orion-code",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.26",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@orion-agents/orion-code",
|
|
9
|
-
"version": "0.3.
|
|
9
|
+
"version": "0.3.26",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@clack/prompts": "^0.7.0",
|