@orion-agents/orion-code 0.3.24 → 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 +60 -4
- package/README.md +8 -5
- package/README.zh-CN.md +6 -4
- package/dist/runtime/legacy-thread-materializer.d.ts +13 -0
- package/dist/runtime/legacy-thread-materializer.js +72 -0
- 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/session-storage.js +21 -0
- package/dist/services/workspace-registry.d.ts +9 -0
- package/dist/services/workspace-registry.js +13 -2
- package/dist/web/git-diff-document.d.ts +19 -0
- package/dist/web/git-diff-document.js +46 -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 +111 -5
- 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,67 @@ which is **not** a pass.
|
|
|
22
22
|
|
|
23
23
|
## [Unreleased]
|
|
24
24
|
|
|
25
|
-
## [0.3.
|
|
25
|
+
## [0.3.26] — UNRELEASED
|
|
26
26
|
|
|
27
|
-
> **Status: candidate.**
|
|
28
|
-
>
|
|
29
|
-
>
|
|
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
|
|
59
|
+
|
|
60
|
+
### Fixed
|
|
61
|
+
|
|
62
|
+
- #269: `GET /git/conflict` no longer fails with `git cat-file: could not get object
|
|
63
|
+
info` after editing a conflicted file before staging — the `worktree` version is read
|
|
64
|
+
from the filesystem (contained within the repository root) instead of an object
|
|
65
|
+
`hash-object` never wrote.
|
|
66
|
+
- #270: `GET /git/file-history` with a `rev` now walks that revision; previously the
|
|
67
|
+
validated revision never reached the `git log` command and every query walked HEAD.
|
|
68
|
+
- #271: switching to a model with the same or larger raw context window but a smaller
|
|
69
|
+
effective input budget (larger output reserve) now reports `compactRequired` and runs
|
|
70
|
+
the compact preflight instead of silently persisting a model the history cannot fit.
|
|
71
|
+
|
|
72
|
+
### Changed
|
|
73
|
+
|
|
74
|
+
- #272: the left project navigation keeps a stable registration order for unpinned
|
|
75
|
+
projects (`registeredAt`, persisted by the workspace registry and exposed additively
|
|
76
|
+
on `WebWorkspaceSummaryV1`); activating sessions no longer reshuffles the tree.
|
|
77
|
+
Pinned projects keep `pinnedOrder` first.
|
|
78
|
+
|
|
79
|
+
## [0.3.24] — 2026-10-13
|
|
80
|
+
|
|
81
|
+
> **Status: published.** Released as `@orion-agents/orion-code@0.3.24` from
|
|
82
|
+
> `refs/tags/v0.3.24`, and merged to `main` as `85fadb4` (#268). Session
|
|
83
|
+
> navigation & responsiveness for the Web Workbench. Plan:
|
|
84
|
+
> `docs/plan/v0.3.24-plan.md`. GitHub Release:
|
|
85
|
+
> https://github.com/orion-agents/orion-code/releases/tag/v0.3.24
|
|
30
86
|
|
|
31
87
|
### Fixed
|
|
32
88
|
|
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.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.
|
|
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.26 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.26
|
|
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.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。
|
|
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.26 在 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.26
|
|
105
107
|
orion --version
|
|
106
108
|
orion doctor
|
|
107
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 = [
|
|
@@ -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)({
|
|
@@ -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
|
}
|
|
@@ -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 })));
|
|
@@ -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,
|
|
@@ -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
|
*
|