@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.
Files changed (29) hide show
  1. package/CHANGELOG.md +60 -4
  2. package/README.md +8 -5
  3. package/README.zh-CN.md +6 -4
  4. package/dist/runtime/legacy-thread-materializer.d.ts +13 -0
  5. package/dist/runtime/legacy-thread-materializer.js +72 -0
  6. package/dist/runtime/model-coordinator.d.ts +1 -0
  7. package/dist/runtime/model-coordinator.js +9 -2
  8. package/dist/runtime/session-composer-control.js +5 -1
  9. package/dist/services/session-storage.js +21 -0
  10. package/dist/services/workspace-registry.d.ts +9 -0
  11. package/dist/services/workspace-registry.js +13 -2
  12. package/dist/web/git-diff-document.d.ts +19 -0
  13. package/dist/web/git-diff-document.js +46 -2
  14. package/dist/web/git-history-service.js +3 -0
  15. package/dist/web/git-read-model-service.d.ts +9 -0
  16. package/dist/web/git-read-model-service.js +111 -5
  17. package/dist/web/git-version-service.d.ts +8 -0
  18. package/dist/web/git-version-service.js +50 -26
  19. package/dist/web/protocol.d.ts +5 -0
  20. package/dist/web/workbench-controller.js +1 -0
  21. package/dist/web-client/assets/{FilesPanel-BjEDjE7l.js → FilesPanel-CdoCa8u0.js} +1 -1
  22. package/dist/web-client/assets/{GitPanel-DvgAZDRA.js → GitPanel-CRr_iIDz.js} +1 -1
  23. package/dist/web-client/assets/{ReviewPanel-BMPaYKDY.js → ReviewPanel-C1Cu2IRV.js} +1 -1
  24. package/dist/web-client/assets/{TerminalPanel-Da1ZEhGF.js → TerminalPanel-BniCjatZ.js} +1 -1
  25. package/dist/web-client/assets/{index-DZOlUgt6.js → index-kQqA3li6.js} +11 -11
  26. package/dist/web-client/assets/{useAutoNotice-Ylj5bfSH.js → useAutoNotice-DPXFWIwK.js} +1 -1
  27. package/dist/web-client/index.html +1 -1
  28. package/npm-shrinkwrap.json +2 -2
  29. 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.24]
25
+ ## [0.3.26] — UNRELEASED
26
26
 
27
- > **Status: candidate.** Implementation on `codex/v0.3.24`, based on `main@6ed47fd`.
28
- > Plan: `docs/plan/v0.3.24-plan.md`. Registry and release status must be
29
- > verified from their own immutable receipts, not inferred from this changelog.
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.24 development focus — Web session responsiveness: Session-scoped request admission,
6
- > cached foreground switching, retained live views, and bounded Git probes during creation.
7
- > Built on the merged v0.3.23 source (`6ed47fd`, #266).
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.24 package when it is available on npm:
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.24
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.24 开发重点:Web 会话响应性。发送按会话等待、有效缓存即时切换、保留实时显示内容,
6
- > 新建使用有界 Git 探测。基于已合并的 v0.3.23 源码(`6ed47fd`,#266)。
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.24 在 npm 可用后,可安装固定版本:
103
+ v0.3.26 在 npm 可用后,可安装固定版本:
102
104
 
103
105
  ```bash
104
- npm install -g @orion-agents/orion-code@0.3.24
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
- if (!prev || target.resolvedContextWindow >= prev.resolvedContextWindow) {
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
- if (prev && target.resolvedContextWindow < prev.resolvedContextWindow) {
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
- compactRequired: profile.resolvedContextWindow < this.project(session, true).model.contextWindow,
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
- const recent = Date.parse(right.lastActivatedAt) - Date.parse(left.lastActivatedAt);
213
- return recent || left.label.localeCompare(right.label) || left.id.localeCompare(right.id);
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
- if (isMetaLine(raw)) {
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
  *