@orion-agents/orion-code 0.3.15 → 0.3.17

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 (44) hide show
  1. package/CHANGELOG.md +219 -0
  2. package/dist/runtime/release-receipts.d.ts +15 -3
  3. package/dist/runtime/release-receipts.js +27 -1
  4. package/dist/services/session-storage.d.ts +9 -0
  5. package/dist/services/session-storage.js +27 -1
  6. package/dist/web/git-compare-service.d.ts +45 -0
  7. package/dist/web/git-compare-service.js +160 -0
  8. package/dist/web/git-diff-document.d.ts +83 -0
  9. package/dist/web/git-diff-document.js +214 -0
  10. package/dist/web/git-file-source.d.ts +25 -0
  11. package/dist/web/git-file-source.js +21 -0
  12. package/dist/web/git-history-service.d.ts +151 -0
  13. package/dist/web/git-history-service.js +610 -0
  14. package/dist/web/git-index-transaction.d.ts +45 -0
  15. package/dist/web/git-index-transaction.js +174 -0
  16. package/dist/web/git-patch-builder.d.ts +30 -0
  17. package/dist/web/git-patch-builder.js +134 -0
  18. package/dist/web/git-read-model-service.d.ts +338 -18
  19. package/dist/web/git-read-model-service.js +1029 -110
  20. package/dist/web/git-version-service.d.ts +71 -0
  21. package/dist/web/git-version-service.js +255 -0
  22. package/dist/web/native-directory-picker.d.ts +63 -0
  23. package/dist/web/native-directory-picker.js +169 -0
  24. package/dist/web/protocol.d.ts +51 -2
  25. package/dist/web/server.js +384 -8
  26. package/dist/web/workbench-controller.d.ts +126 -10
  27. package/dist/web/workbench-controller.js +383 -15
  28. package/dist/web/workspace-open-telemetry.d.ts +55 -0
  29. package/dist/web/workspace-open-telemetry.js +73 -0
  30. package/dist/web-client/assets/FilesPanel-BARQCmne.js +1 -0
  31. package/dist/web-client/assets/GitPanel-DjB9eSiJ.js +4 -0
  32. package/dist/web-client/assets/ReviewPanel-YhEGlp-G.js +5 -0
  33. package/dist/web-client/assets/{TerminalPanel-BpsLphLv.js → TerminalPanel-DnWh3Vp2.js} +1 -1
  34. package/dist/web-client/assets/{index-D3sxrAh_.css → index--hnGXXjj.css} +1 -1
  35. package/dist/web-client/assets/index-Drja0_BT.js +20 -0
  36. package/dist/web-client/assets/{useAutoNotice-BeSf9Dfq.js → useAutoNotice-DfQi4G1X.js} +1 -1
  37. package/dist/web-client/index.html +2 -2
  38. package/npm-shrinkwrap.json +2 -2
  39. package/package.json +1 -1
  40. package/dist/web-client/assets/DiffViewer-C0A42Lma.js +0 -5
  41. package/dist/web-client/assets/FilesPanel-DiXKnFMj.js +0 -1
  42. package/dist/web-client/assets/GitPanel--_Yrbldp.js +0 -1
  43. package/dist/web-client/assets/ReviewPanel-BilzL9Ma.js +0 -1
  44. package/dist/web-client/assets/index-C2M48tcK.js +0 -20
package/CHANGELOG.md CHANGED
@@ -22,6 +22,225 @@ which is **not** a pass.
22
22
 
23
23
  ## [Unreleased]
24
24
 
25
+ ## [0.3.17]
26
+
27
+ > **Status: candidate.** Opening a local project is made fast and honest
28
+ > (`docs/plan/v0.3.16-plan-v1.md`): the directory preview no longer waits on the
29
+ > session catalog, the Finder browse action no longer occupies the global
30
+ > Context mutation admission, and every entry point shares one
31
+ > inspect → confirm → activate contract. Covers S2, S3 and S5; S1 (per-stage
32
+ > telemetry) and S4 (staged activate baseline) are not implemented yet. Not
33
+ > merged, tagged or published.
34
+ >
35
+ > **Update:** S1 (per-stage telemetry) and the S4 headline (the foreground
36
+ > snapshot no longer gates the first render) also landed on this branch, plus a
37
+ > bounded inspect probe. The measured Host-side receipt is in
38
+ > `docs/plan/evidence/v0.3.17-open-baseline.md`.
39
+ >
40
+ > **Update 2:** the Git right-hand work panel (`docs/plan/v0.3.17-plan.md`,
41
+ > S0–S6) is implemented on this branch as well. Evidence per stage is under
42
+ > `docs/plan/evidence/v0.3.17-git/`, and the scenario matrix is
43
+ > `g317-status.md`. All functional gaps are closed; the remaining items are
44
+ > assertion and evidence backlog, listed under **Known gaps** below.
45
+
46
+ ### Added
47
+
48
+ - **Per-stage open-path telemetry:** `WorkspaceOpenTelemetryV1` keeps a bounded
49
+ ring of `{ requestId, operation, outcome, stage ms, errorCode }` for the
50
+ picker, the inspect probe and the Context transition, and exposes it
51
+ read-only through Diagnostics as `workspaceOpen`. Traces never carry a path,
52
+ a label or file content.
53
+
54
+ ### Fixed
55
+
56
+ - **The preview no longer blocks on the session catalog:** `inspect` used to
57
+ call `countSessionsByProject()`, which can rebuild the catalog and hold its
58
+ file lock for up to 10s before the confirmation card appeared. It now reads
59
+ the warm cache only (`peekCachedSessionCounts()`) and reports
60
+ `sessionCountStatus: 'deferred'` when the catalog is cold; the card stays
61
+ usable and shows 会话数读取中 instead of stalling.
62
+
63
+ ### Changed
64
+
65
+ - **The native picker is browse admission, not a mutation:**
66
+ `POST /workspaces/pick-directory` no longer goes through `executeMutation()`,
67
+ so an open Finder dialog does not raise `activeMutationCount` or enter the
68
+ idempotency ledger and can no longer make a project switch look like a busy
69
+ Runtime. Single-flight (`picker_busy`) and the nonce/origin/content-type
70
+ guards are unchanged.
71
+ - **One path contract:** the advanced manual entry and the pinned/recent rows
72
+ now go through the same inspect → confirm → activate state machine as the
73
+ Finder entry. Nothing activates without an explicit confirmation, and
74
+ availability plus the Git/Folder kind are always re-checked.
75
+ - **The picker timeout is wired:** `timeoutMs` reaches the default `execFile`
76
+ runner, and a timeout reports its own `picker_timeout` reason code instead of
77
+ being folded into `picker_unavailable`.
78
+ - **The preview probe is bounded:** `inspectDirectory` now uses asynchronous fs
79
+ raced against a 250ms budget, so a hung network mount or a broken symlink
80
+ cannot freeze the Node event loop; on timeout the directory is reported
81
+ `unreadable` with an `inspect_budget_exceeded` trace code.
82
+ - **The foreground snapshot no longer gates the first render:** `loadBaseline`
83
+ dispatches the identity baseline (bootstrap, workspaces, sessions, settings)
84
+ before restoring the session, so the project shell and session list appear
85
+ while the transcript is still loading. The reducer already marks the active
86
+ session `loading`, so the Composer stays disabled until the snapshot settles.
87
+ - **Activation no longer re-requests the workspace page:** the page returned by
88
+ `POST /workspaces/activate` is reused by the baseline when its `activeId`
89
+ matches the freshly bootstrapped Context, so opening a project issues one
90
+ fewer round-trip. A mismatch falls back to the normal request.
91
+ - **The confirmation card reports discrete stages:** while opening, the card
92
+ shows `准备项目 → 加载本地 Runtime → 恢复会话(如有)` with only the stage the
93
+ Host actually reports marked `aria-current="step"`, instead of one vague
94
+ "正在打开…" label covering the whole wait.
95
+
96
+ ### Tests
97
+
98
+ - `tests/web-workbench-workspace-picker.test.ts` — the preview never calls the
99
+ catalog rebuild entry point; the count is `ready` when the cache is warm.
100
+ - `tests/session-catalog-peek.test.ts` — the peek answers `null` on a cold
101
+ module instead of loading or locking.
102
+ - `tests/native-directory-picker.test.ts` — a timeout keeps its own reason code.
103
+ - `tests/web-ui-workspace-picker.test.tsx` — the card stays actionable while the
104
+ count is deferred, and the stage list only appears after confirming.
105
+ - `tests/workspace-open-telemetry.test.ts` — stage offsets, duplicate marks,
106
+ bounded retention, and that a serialized trace contains no path.
107
+ - `tests/e2e/web-v0317-workspace-open.spec.ts` — W316P-01/02/04/08/09 driven by a
108
+ scripted picker: no browse side effects, inspect-then-confirm, cancellation,
109
+ the shared advanced-path contract, and the no-path-leak regression.
110
+ - `tests/e2e/*` — the four manual-path specs confirm on the card before
111
+ activating.
112
+
113
+ ### Test seams
114
+
115
+ - **`ORION_CODE_WEB_PICKER_FIXTURE`** points the Host at a JSON script file so
116
+ end-to-end runs never open a real Finder dialog (`{"path": …}` /
117
+ `{"paths": […]}` / `{"outcome": "cancelled" | "unavailable"}`). The script is
118
+ re-read on every invocation, and an absent or malformed file behaves like a
119
+ cancelled dialog. It is browse-only: the returned path still has to be
120
+ confirmed and passes through the same guarded activation flow.
121
+
122
+ ### Added — Git right-hand work panel (S0–S6)
123
+
124
+ - **Change navigation:** staged / unstaged / untracked / conflict groups with a
125
+ host-side filter and search, counts that cover the whole matching set, and a
126
+ pagination cursor bound to the group and query it was issued for.
127
+ - **Diff reading:** unified and side-by-side, old/new line numbers, continuous
128
+ hunks, fold state, line anchors, word-level diff (`--word-diff=plain`) and
129
+ ignore-all-space — the last two are bound into the pagination cursor, so a
130
+ page can never be continued under a different view.
131
+ - **Writes through an index transaction:** every index change holds
132
+ `index.lock` (and only ever removes a lock it created), writes to a private
133
+ sibling index, verifies it, and publishes with a single atomic rename.
134
+ Hunk-level and line-level staging rebuild the patch on the Host from the
135
+ reviewed document — the browser only ever sends ids.
136
+ - **Commit form and result recovery:** summary plus multi-line body, the
137
+ identity / signing / hook configuration that will actually run, a preview of
138
+ the real index (independent of list filters), the draft preserved on failure,
139
+ and idempotent replay by `requestId`.
140
+ - **History:** search across the whole reachable history, commit details,
141
+ root commits compared with the empty tree, merge parents selectable and
142
+ labelled, file history with `--follow`, blame, historical blobs for files
143
+ that no longer exist locally, and a commit graph drawn by git itself.
144
+ - **Branch and version comparison:** two-endpoint snapshot and diverged-from
145
+ (merge-base) modes; the resolved commit ids are part of the result so an open
146
+ comparison can be frozen, and a multi-merge-base pair is refused rather than
147
+ silently picking one.
148
+ - **Conflict views:** base / ours / theirs read from index stages 1/2/3 plus
149
+ the working tree, with a stage that legitimately does not exist reported as
150
+ absent instead of being elided.
151
+ - **Submodule and LFS:** recorded gitlink SHAs are shown without traversing
152
+ submodules, and LFS pointers are reported as state without fetching objects.
153
+ - **Image versions:** a bounded, magic-byte-identified image becomes a data URL
154
+ so both versions of a picture can be shown without a download.
155
+ - **Cross-panel links:** "open in Files" translates a Git token into a Files
156
+ token on the Host, and the composer reference carries the file, comparison
157
+ source, revision and line range into the draft without sending it.
158
+ - **Responsive and keyboard:** three tiers keyed off the panel container (not
159
+ the window), with a narrow layout that never writes the downgrade back into
160
+ the stored preference; arrow keys, Enter, F7 / Shift+F7 change navigation,
161
+ Escape unwinding one layer at a time, and Space toggling selection only —
162
+ never a Git write.
163
+
164
+ ### Fixed — Git panel
165
+
166
+ - **Staging could silently do nothing:** the private index copy received a
167
+ fresh mtime, which defeated Git’s racy-clean rule — a file edited within the
168
+ same timestamp granularity and with an unchanged size was not re-hashed, so
169
+ `git add` succeeded while changing nothing. The copy now preserves the
170
+ original timestamps, and staging verifies each path with `hash-object
171
+ --path` against the index entry.
172
+ - **`localhost` was rejected site-wide:** the Host and Origin checks compared
173
+ header strings exactly, so `http://localhost:<port>` returned 421 (and, with
174
+ only the Host check fixed, every mutation would have returned 403). Both now
175
+ accept any loopback spelling of the same endpoint while still refusing
176
+ attacker-controlled names and a mismatched port.
177
+ - **`git apply -` and `git commit -F -` blocked forever:** the async
178
+ `execFile` `input` option is silently ignored, so stdin was never closed.
179
+ Writes now use `spawn` and close stdin explicitly, and a timeout kills the
180
+ whole process group so a slow hook cannot be orphaned.
181
+ - **Refs came back empty:** `for-each-ref` does not support the `%x1f` escapes
182
+ that `git log --format` does; it printed them literally. Refs are now parsed
183
+ from a space-separated format.
184
+ - **A detached HEAD could be reported as a branch:** the check was "no ref
185
+ points at HEAD", which is wrong when detaching onto a branch tip. It now
186
+ asks git whether HEAD is a symbolic ref.
187
+
188
+ ### Known gaps
189
+
190
+ - Assertion and evidence backlog for the Git panel: an end-to-end spec
191
+ (`tests/e2e/web-v0317-git-workspace.spec.ts`), automated assertions for the
192
+ Files cross-panel link, and scenario variants not yet covered (staging a
193
+ deletion or rename, CRLF line selection, signing failure, bare repository
194
+ and linked worktree cases, agent-led file changes).
195
+ - Narrow-viewport and dark-theme screenshots, plus performance samples for the
196
+ reads added in S5 and S6.
197
+ - `npm run typecheck:e2e` fails on this branch with 12 errors in the v0.3.12
198
+ and v0.3.13 specs; the same 12 errors reproduce on the v0.3.16 baseline, so
199
+ they are pre-existing and unrelated to the Git panel.
200
+
201
+ ## [0.3.16] — CANDIDATE
202
+
203
+ > **Status: candidate.** LOCAL WORKSPACE opens a new project without typing a
204
+ > path (`docs/plan/v0.3.16-plan.md`): a Host-side native folder picker, and a
205
+ > read-only inspect step separated from activation. Covers slices S1 and S2;
206
+ > controlled workspace-roots discovery (S3) is not implemented yet. No protocol
207
+ > removals or dependency changes. Not merged, tagged or published.
208
+
209
+ ### Added
210
+
211
+ - **Native folder picker (Host):** `POST /workspaces/pick-directory` runs the OS
212
+ directory chooser on the Host. The AppleScript is a fixed constant and the
213
+ prompt / default location travel as `argv` through `execFile`, so a chosen
214
+ path is never interpolated into a shell or script string. Cancelling is a
215
+ normal outcome, platforms without an adapter report `picker_unavailable`
216
+ rather than faking a selection, and only one picker may be open at a time.
217
+ - **Read-only workspace inspect:** `POST /workspaces/inspect` resolves the
218
+ canonical path, availability (`available` / `missing` / `not_directory` /
219
+ `unreadable`) and Git-vs-folder kind, plus existing workspace id, session
220
+ count and pin order. It never registers a workspace, activates a Context or
221
+ reads file contents.
222
+ - **Project picker dialog:** the LOCAL WORKSPACE dialog now leads with
223
+ `从 Finder 选择文件夹…`, groups pinned and recent projects, and shows a
224
+ confirmation card (canonical path, kind, availability, session count). The
225
+ manual absolute-path entry moved into a collapsed 高级 disclosure.
226
+
227
+ ### Changed
228
+
229
+ - Browsing, searching, inspecting and cancelling never call `activateContext`,
230
+ pause the event stream or stop a running Session; only confirming 打开项目
231
+ enters the existing activation flow.
232
+
233
+ ### Tests
234
+
235
+ - `tests/native-directory-picker.test.ts` — outcome mapping, argv isolation,
236
+ trailing-separator handling and non-macOS unavailability.
237
+ - `tests/web-workbench-workspace-picker.test.ts` — inspect is strictly
238
+ read-only and the picker is single-in-flight.
239
+ - `tests/web-ui-workspace-picker.test.tsx` — state-machine invariants (no
240
+ activation without a confirmed candidate) and the confirmation card.
241
+ - `tests/e2e/*` — the four specs that filled the manual path now open the
242
+ advanced disclosure first.
243
+
25
244
  ## [0.3.15] — CANDIDATE
26
245
 
27
246
  > **Status: candidate.** Web Workbench visual & interaction convergence
@@ -9,12 +9,24 @@ export declare const WEB_E2E_WEB32_SCENARIOS_V1: readonly ["WEB32-P0-01", "WEB32
9
9
  export declare const WEB_E2E_WEB33_THEME_SCENARIOS_V1: readonly ["WEB33-P0-01", "WEB33-P0-02", "WEB33-P0-03", "WEB33-P0-04", "WEB33-P0-05", "WEB33-P0-06", "WEB33-P0-07", "WEB33-P0-08", "WEB33-P0-09", "WEB33-P0-10", "WEB33-P0-11", "WEB33-P0-12"];
10
10
  export declare const WEB_E2E_WEB33_SESSION_SCENARIOS_V1: readonly ["WEB33-P0-16", "WEB33-P0-17", "WEB33-P0-18", "WEB33-P0-19", "WEB33-P0-20", "WEB33-P0-21", "WEB33-P0-22", "WEB33-P0-23", "WEB33-P0-24"];
11
11
  export declare const WEB_E2E_WEB33_SCENARIOS_V1: readonly ("WEB33-P0-01" | "WEB33-P0-02" | "WEB33-P0-03" | "WEB33-P0-04" | "WEB33-P0-05" | "WEB33-P0-06" | "WEB33-P0-07" | "WEB33-P0-08" | "WEB33-P0-09" | "WEB33-P0-10" | "WEB33-P0-11" | "WEB33-P0-12" | "WEB33-P0-16" | "WEB33-P0-17" | "WEB33-P0-18" | "WEB33-P0-19" | "WEB33-P0-20" | "WEB33-P0-21" | "WEB33-P0-22" | "WEB33-P0-23" | "WEB33-P0-24")[];
12
- export declare const WEB_E2E_FULL_SCENARIOS_V1: readonly ("E2E-P0-01" | "E2E-P0-02" | "E2E-P0-03" | "E2E-P0-04" | "E2E-P0-05" | "E2E-P0-06" | "E2E-P0-07" | "E2E-P0-08" | "SET-P0-01" | "SET-P0-02" | "SET-P0-03" | "SET-P0-04" | "SET-P0-05" | "SET-P0-06" | "SET-P0-07" | "SET-P0-08" | "SET-P0-09" | "SET-P0-10" | "SET-P0-11" | "SET-P0-12" | "SET-P0-13" | "SET-P0-14" | "WEB31-P0-01" | "WEB31-P0-02" | "WEB31-P0-03" | "WEB31-P0-04" | "WEB31-P0-05" | "WEB31-P0-06" | "WEB31-P0-07" | "WEB31-P0-08" | "WEB31-P0-09" | "WEB31-P0-10" | "WEB31-P0-11" | "WEB31-P0-12" | "WEB32-P0-01" | "WEB32-P0-02" | "WEB32-P0-03" | "WEB32-P0-04" | "WEB32-P0-05" | "WEB32-P0-06" | "WEB32-P0-07" | "WEB32-P0-08" | "WEB32-P0-09" | "WEB32-P0-10" | "WEB32-P0-11" | "WEB32-P0-12" | "WEB33-P0-01" | "WEB33-P0-02" | "WEB33-P0-03" | "WEB33-P0-04" | "WEB33-P0-05" | "WEB33-P0-06" | "WEB33-P0-07" | "WEB33-P0-08" | "WEB33-P0-09" | "WEB33-P0-10" | "WEB33-P0-11" | "WEB33-P0-12" | "WEB33-P0-16" | "WEB33-P0-17" | "WEB33-P0-18" | "WEB33-P0-19" | "WEB33-P0-20" | "WEB33-P0-21" | "WEB33-P0-22" | "WEB33-P0-23" | "WEB33-P0-24")[];
13
- export declare const WEB_E2E_SETTINGS_SCENARIOS_V1: readonly ("E2E-P0-01" | "E2E-P0-02" | "E2E-P0-03" | "E2E-P0-04" | "E2E-P0-05" | "E2E-P0-06" | "E2E-P0-07" | "E2E-P0-08" | "SET-P0-01" | "SET-P0-02" | "SET-P0-03" | "SET-P0-04" | "SET-P0-05" | "SET-P0-06" | "SET-P0-07" | "SET-P0-08" | "SET-P0-09" | "SET-P0-10" | "SET-P0-11" | "SET-P0-12" | "SET-P0-13" | "SET-P0-14" | "WEB31-P0-01" | "WEB31-P0-02" | "WEB31-P0-03" | "WEB31-P0-04" | "WEB31-P0-05" | "WEB31-P0-06" | "WEB31-P0-07" | "WEB31-P0-08" | "WEB31-P0-09" | "WEB31-P0-10" | "WEB31-P0-11" | "WEB31-P0-12" | "WEB32-P0-01" | "WEB32-P0-02" | "WEB32-P0-03" | "WEB32-P0-04" | "WEB32-P0-05" | "WEB32-P0-06" | "WEB32-P0-07" | "WEB32-P0-08" | "WEB32-P0-09" | "WEB32-P0-10" | "WEB32-P0-11" | "WEB32-P0-12" | "WEB33-P0-01" | "WEB33-P0-02" | "WEB33-P0-03" | "WEB33-P0-04" | "WEB33-P0-05" | "WEB33-P0-06" | "WEB33-P0-07" | "WEB33-P0-08" | "WEB33-P0-09" | "WEB33-P0-10" | "WEB33-P0-11" | "WEB33-P0-12" | "WEB33-P0-16" | "WEB33-P0-17" | "WEB33-P0-18" | "WEB33-P0-19" | "WEB33-P0-20" | "WEB33-P0-21" | "WEB33-P0-22" | "WEB33-P0-23" | "WEB33-P0-24")[];
12
+ /**
13
+ * v0.3.17 — LOCAL WORKSPACE open path. The picker is driven by a script file
14
+ * (`ORION_CODE_WEB_PICKER_FIXTURE`) so CI never opens a real Finder dialog.
15
+ */
16
+ export declare const WEB_E2E_WEB38_SCENARIOS_V1: readonly ["WEB38-P0-01", "WEB38-P0-02", "WEB38-P0-03", "WEB38-P0-04", "WEB38-P0-05"];
17
+ /**
18
+ * v0.3.17 — Git right-hand work panel (`docs/plan/v0.3.17-plan.md`).
19
+ *
20
+ * One id per user-visible capability the plan calls P0/P1 and that a browser can
21
+ * actually verify; the per-stage unit suites are registered separately.
22
+ */
23
+ export declare const WEB_E2E_WEB317_SCENARIOS_V1: readonly string[];
24
+ export declare const WEB_E2E_FULL_SCENARIOS_V1: readonly string[];
25
+ export declare const WEB_E2E_SETTINGS_SCENARIOS_V1: readonly string[];
14
26
  export declare const WEB_E2E_WEB31_CRITICAL_SCENARIOS_V1: readonly ["WEB31-P0-01", "WEB31-P0-02", "WEB31-P0-08", "WEB31-P0-09", "WEB31-P0-10", "WEB31-P0-12"];
15
27
  export declare const WEB_E2E_WEB32_CRITICAL_SCENARIOS_V1: readonly ["WEB32-P0-01", "WEB32-P0-04", "WEB32-P0-05", "WEB32-P0-06", "WEB32-P0-09", "WEB32-P0-11", "WEB32-P0-12"];
16
28
  export declare const WEB_E2E_WEB33_CRITICAL_SCENARIOS_V1: readonly ["WEB33-P0-01", "WEB33-P0-03", "WEB33-P0-08", "WEB33-P0-10", "WEB33-P0-12", "WEB33-P0-16", "WEB33-P0-17", "WEB33-P0-18", "WEB33-P0-20", "WEB33-P0-21", "WEB33-P0-23", "WEB33-P0-24"];
17
- export declare const WEB_E2E_CRITICAL_SCENARIOS_V1: readonly ("E2E-P0-01" | "E2E-P0-02" | "E2E-P0-03" | "E2E-P0-04" | "E2E-P0-05" | "E2E-P0-06" | "E2E-P0-07" | "E2E-P0-08" | "SET-P0-01" | "SET-P0-02" | "SET-P0-03" | "SET-P0-04" | "SET-P0-05" | "SET-P0-06" | "SET-P0-07" | "SET-P0-08" | "SET-P0-09" | "SET-P0-10" | "SET-P0-11" | "SET-P0-12" | "SET-P0-13" | "SET-P0-14" | "WEB31-P0-01" | "WEB31-P0-02" | "WEB31-P0-03" | "WEB31-P0-04" | "WEB31-P0-05" | "WEB31-P0-06" | "WEB31-P0-07" | "WEB31-P0-08" | "WEB31-P0-09" | "WEB31-P0-10" | "WEB31-P0-11" | "WEB31-P0-12" | "WEB32-P0-01" | "WEB32-P0-02" | "WEB32-P0-03" | "WEB32-P0-04" | "WEB32-P0-05" | "WEB32-P0-06" | "WEB32-P0-07" | "WEB32-P0-08" | "WEB32-P0-09" | "WEB32-P0-10" | "WEB32-P0-11" | "WEB32-P0-12" | "WEB33-P0-01" | "WEB33-P0-02" | "WEB33-P0-03" | "WEB33-P0-04" | "WEB33-P0-05" | "WEB33-P0-06" | "WEB33-P0-07" | "WEB33-P0-08" | "WEB33-P0-09" | "WEB33-P0-10" | "WEB33-P0-11" | "WEB33-P0-12" | "WEB33-P0-16" | "WEB33-P0-17" | "WEB33-P0-18" | "WEB33-P0-19" | "WEB33-P0-20" | "WEB33-P0-21" | "WEB33-P0-22" | "WEB33-P0-23" | "WEB33-P0-24")[];
29
+ export declare const WEB_E2E_CRITICAL_SCENARIOS_V1: readonly string[];
18
30
  export type ReleaseGateDecisionV1 = 'GO' | 'NO_GO';
19
31
  export interface TarballArtifactReceiptV1 {
20
32
  readonly version: typeof RELEASE_RECEIPT_VERSION_V1;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.WEB_E2E_CRITICAL_SCENARIOS_V1 = exports.WEB_E2E_WEB33_CRITICAL_SCENARIOS_V1 = exports.WEB_E2E_WEB32_CRITICAL_SCENARIOS_V1 = exports.WEB_E2E_WEB31_CRITICAL_SCENARIOS_V1 = exports.WEB_E2E_SETTINGS_SCENARIOS_V1 = exports.WEB_E2E_FULL_SCENARIOS_V1 = exports.WEB_E2E_WEB33_SCENARIOS_V1 = exports.WEB_E2E_WEB33_SESSION_SCENARIOS_V1 = exports.WEB_E2E_WEB33_THEME_SCENARIOS_V1 = exports.WEB_E2E_WEB32_SCENARIOS_V1 = exports.WEB_E2E_WEB31_SCENARIOS_V1 = exports.WEB_E2E_LEGACY_SCENARIOS_V1 = exports.SUPPORTED_RELEASE_VERSION_LINE_V1 = exports.SUPPORTED_RELEASE_NODE_FLOORS_V1 = exports.SUPPORTED_RELEASE_NODE_MAJORS_V1 = exports.RELEASE_RECEIPT_VERSION_V1 = void 0;
3
+ exports.WEB_E2E_CRITICAL_SCENARIOS_V1 = exports.WEB_E2E_WEB33_CRITICAL_SCENARIOS_V1 = exports.WEB_E2E_WEB32_CRITICAL_SCENARIOS_V1 = exports.WEB_E2E_WEB31_CRITICAL_SCENARIOS_V1 = exports.WEB_E2E_SETTINGS_SCENARIOS_V1 = exports.WEB_E2E_FULL_SCENARIOS_V1 = exports.WEB_E2E_WEB317_SCENARIOS_V1 = exports.WEB_E2E_WEB38_SCENARIOS_V1 = exports.WEB_E2E_WEB33_SCENARIOS_V1 = exports.WEB_E2E_WEB33_SESSION_SCENARIOS_V1 = exports.WEB_E2E_WEB33_THEME_SCENARIOS_V1 = exports.WEB_E2E_WEB32_SCENARIOS_V1 = exports.WEB_E2E_WEB31_SCENARIOS_V1 = exports.WEB_E2E_LEGACY_SCENARIOS_V1 = exports.SUPPORTED_RELEASE_VERSION_LINE_V1 = exports.SUPPORTED_RELEASE_NODE_FLOORS_V1 = exports.SUPPORTED_RELEASE_NODE_MAJORS_V1 = exports.RELEASE_RECEIPT_VERSION_V1 = void 0;
4
4
  exports.createTarballArtifactReceiptV1 = createTarballArtifactReceiptV1;
5
5
  exports.verifyTarballArtifactReceiptV1 = verifyTarballArtifactReceiptV1;
6
6
  exports.createRuntimeMatrixReceiptV1 = createRuntimeMatrixReceiptV1;
@@ -101,11 +101,37 @@ exports.WEB_E2E_WEB33_SCENARIOS_V1 = Object.freeze([
101
101
  ...exports.WEB_E2E_WEB33_THEME_SCENARIOS_V1,
102
102
  ...exports.WEB_E2E_WEB33_SESSION_SCENARIOS_V1,
103
103
  ]);
104
+ /**
105
+ * v0.3.17 — LOCAL WORKSPACE open path. The picker is driven by a script file
106
+ * (`ORION_CODE_WEB_PICKER_FIXTURE`) so CI never opens a real Finder dialog.
107
+ */
108
+ exports.WEB_E2E_WEB38_SCENARIOS_V1 = Object.freeze([
109
+ 'WEB38-P0-01',
110
+ 'WEB38-P0-02',
111
+ 'WEB38-P0-03',
112
+ 'WEB38-P0-04',
113
+ 'WEB38-P0-05',
114
+ ]);
115
+ /**
116
+ * v0.3.17 — Git right-hand work panel (`docs/plan/v0.3.17-plan.md`).
117
+ *
118
+ * One id per user-visible capability the plan calls P0/P1 and that a browser can
119
+ * actually verify; the per-stage unit suites are registered separately.
120
+ */
121
+ exports.WEB_E2E_WEB317_SCENARIOS_V1 = Object.freeze([
122
+ 'WEB39-P0-01',
123
+ 'WEB39-P0-02',
124
+ 'WEB39-P0-03',
125
+ 'WEB39-P0-04',
126
+ 'WEB39-P0-05',
127
+ ]);
104
128
  exports.WEB_E2E_FULL_SCENARIOS_V1 = Object.freeze([
105
129
  ...exports.WEB_E2E_LEGACY_SCENARIOS_V1,
106
130
  ...exports.WEB_E2E_WEB31_SCENARIOS_V1,
107
131
  ...exports.WEB_E2E_WEB32_SCENARIOS_V1,
108
132
  ...exports.WEB_E2E_WEB33_SCENARIOS_V1,
133
+ ...exports.WEB_E2E_WEB38_SCENARIOS_V1,
134
+ ...exports.WEB_E2E_WEB317_SCENARIOS_V1,
109
135
  ]);
110
136
  exports.WEB_E2E_SETTINGS_SCENARIOS_V1 = Object.freeze(exports.WEB_E2E_FULL_SCENARIOS_V1.filter(id => id.startsWith('SET-P0-')));
111
137
  exports.WEB_E2E_WEB31_CRITICAL_SCENARIOS_V1 = Object.freeze([
@@ -620,6 +620,15 @@ export declare function listSessions(limit?: number): SessionMeta[];
620
620
  * Sessions are excluded so the picker count matches the default listing.
621
621
  */
622
622
  export declare function countSessionsByProject(): ReadonlyMap<string, number>;
623
+ /**
624
+ * v0.3.17 — Counts **only when the catalog is already warm**.
625
+ *
626
+ * Previewing a directory must never make the user wait for a catalog rebuild or
627
+ * a file-lock acquisition (the lock ceiling is 10s). This validates the
628
+ * in-memory cache with a cheap `stat` — no scan, no lock, no write — and
629
+ * returns `null` otherwise so callers can defer the count instead of blocking.
630
+ */
631
+ export declare function peekCachedSessionCounts(): ReadonlyMap<string, number> | null;
623
632
  /**
624
633
  * List sessions for a single canonical project (default listings exclude
625
634
  * archived sessions).
@@ -53,6 +53,7 @@ exports.loadSessionHistoryWithDiagnostics = loadSessionHistoryWithDiagnostics;
53
53
  exports.loadSessionRestoreBundle = loadSessionRestoreBundle;
54
54
  exports.listSessions = listSessions;
55
55
  exports.countSessionsByProject = countSessionsByProject;
56
+ exports.peekCachedSessionCounts = peekCachedSessionCounts;
56
57
  exports.listProjectSessions = listProjectSessions;
57
58
  exports.listArchivedProjectSessions = listArchivedProjectSessions;
58
59
  exports.findSession = findSession;
@@ -2233,8 +2234,33 @@ function listSessions(limit) {
2233
2234
  * Sessions are excluded so the picker count matches the default listing.
2234
2235
  */
2235
2236
  function countSessionsByProject() {
2237
+ return countSessionsByProjectFromCatalog(loadOrRebuildSessionCatalog());
2238
+ }
2239
+ /**
2240
+ * v0.3.17 — Counts **only when the catalog is already warm**.
2241
+ *
2242
+ * Previewing a directory must never make the user wait for a catalog rebuild or
2243
+ * a file-lock acquisition (the lock ceiling is 10s). This validates the
2244
+ * in-memory cache with a cheap `stat` — no scan, no lock, no write — and
2245
+ * returns `null` otherwise so callers can defer the count instead of blocking.
2246
+ */
2247
+ function peekCachedSessionCounts() {
2248
+ const cache = sessionCatalogCache;
2249
+ if (!cache)
2250
+ return null;
2251
+ try {
2252
+ const stat = (0, fs_1.statSync)(cache.path);
2253
+ if (stat.mtimeMs !== cache.mtimeMs || stat.size !== cache.size)
2254
+ return null;
2255
+ }
2256
+ catch {
2257
+ return null;
2258
+ }
2259
+ return countSessionsByProjectFromCatalog(cache.catalog);
2260
+ }
2261
+ function countSessionsByProjectFromCatalog(catalog) {
2236
2262
  const counts = new Map();
2237
- for (const session of Object.values(loadOrRebuildSessionCatalog().sessions)) {
2263
+ for (const session of Object.values(catalog.sessions)) {
2238
2264
  if (session.archivedAt)
2239
2265
  continue;
2240
2266
  counts.set(session.projectPath, (counts.get(session.projectPath) ?? 0) + 1);
@@ -0,0 +1,45 @@
1
+ export type GitCompareModeV1 = 'snapshot' | 'merge-base';
2
+ export interface GitCompareFileV1 {
3
+ readonly path: string;
4
+ readonly renamedFrom: string | null;
5
+ readonly additions: number;
6
+ readonly deletions: number;
7
+ readonly binary: boolean;
8
+ readonly sensitive: boolean;
9
+ }
10
+ export interface GitCompareResultV1 {
11
+ readonly mode: GitCompareModeV1;
12
+ readonly requestedBaseRef: string;
13
+ readonly requestedHeadRef: string;
14
+ /** The commits the comparison actually used; the UI freezes the result against these. */
15
+ readonly baseOid: string;
16
+ readonly headOid: string;
17
+ readonly baseLabel: string;
18
+ readonly headLabel: string;
19
+ /** Set only in merge-base mode: the divergence point that was used. */
20
+ readonly mergeBaseOid: string | null;
21
+ readonly files: readonly GitCompareFileV1[];
22
+ readonly additions: number;
23
+ readonly deletions: number;
24
+ readonly truncated: boolean;
25
+ }
26
+ export interface GitCompareReaderOptions {
27
+ readonly runGit: (args: readonly string[]) => Promise<string>;
28
+ }
29
+ export declare function createGitCompareReader(options: GitCompareReaderOptions): {
30
+ /** Validates a pair of refs and returns their commits, without computing anything yet. */
31
+ resolvePair(baseRef: string, headRef: string): Promise<Readonly<{
32
+ baseRef: string;
33
+ headRef: string;
34
+ baseOid: string;
35
+ headOid: string;
36
+ baseLabel: string;
37
+ headLabel: string;
38
+ }>>;
39
+ compare(input: {
40
+ readonly baseRef: string;
41
+ readonly headRef: string;
42
+ readonly mode: GitCompareModeV1;
43
+ }): Promise<GitCompareResultV1>;
44
+ };
45
+ //# sourceMappingURL=git-compare-service.d.ts.map
@@ -0,0 +1,160 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createGitCompareReader = createGitCompareReader;
4
+ /**
5
+ * v0.3.17 S5 — branch and version comparison (plan G5).
6
+ *
7
+ * Two modes, and the difference between them is the whole point:
8
+ * - `snapshot`: the tree difference between A and B;
9
+ * - `merge-base`: what B introduced since the branches diverged.
10
+ *
11
+ * Semantics this module is responsible for:
12
+ * - refs are resolved to **commits** before anything else, and the resolved OIDs are part of
13
+ * the result, so the UI can detect that a ref moved and freeze the opened result instead
14
+ * of silently comparing against a different tree (plan G5);
15
+ * - `merge-base --all` with more than one answer is refused — picking one candidate would
16
+ * present an arbitrary choice as if it were the unique divergence point, so the caller is
17
+ * told to use the two-endpoint mode instead (plan G5);
18
+ * - no common ancestor and a shallow clone with missing objects produce distinct, explicit
19
+ * errors rather than an empty file list that looks like a legitimate answer;
20
+ * - a ref name is validated before it ever reaches argv: revision expressions, ranges and
21
+ * pathspec magic are refused.
22
+ */
23
+ const redaction_1 = require("../services/redaction");
24
+ const errors_1 = require("./errors");
25
+ const REF_NAME_PATTERN = /^[0-9A-Za-z](?:[0-9A-Za-z._/-]{0,250}[0-9A-Za-z])?$/u;
26
+ const COMPARE_FILE_LIMIT = 2000;
27
+ /** Resolves a ref-ish name to a commit, refusing anything that is not a plain ref name. */
28
+ function assertRefName(value, name) {
29
+ if (typeof value !== 'string' || !REF_NAME_PATTERN.test(value) || value.includes('..')) {
30
+ throw new errors_1.WebWorkbenchError(400, `${name} must be a branch, tag or commit hash.`, 'git_ref_invalid');
31
+ }
32
+ return value;
33
+ }
34
+ async function resolveCommit(runGit, ref) {
35
+ const resolved = (await runGit(['rev-parse', '--verify', '--quiet', `${ref}^{commit}`]).catch(() => ''))
36
+ .toString()
37
+ .trim();
38
+ if (!/^[0-9a-f]{7,64}$/iu.test(resolved)) {
39
+ throw new errors_1.WebWorkbenchError(404, `The ref ${ref} could not be resolved to a commit.`, 'git_ref_not_found');
40
+ }
41
+ return resolved;
42
+ }
43
+ function createGitCompareReader(options) {
44
+ const { runGit } = options;
45
+ const numstat = async (base, head) => {
46
+ const raw = await runGit([
47
+ 'diff',
48
+ '--numstat',
49
+ '-z',
50
+ '--no-ext-diff',
51
+ '--no-textconv',
52
+ base,
53
+ head,
54
+ ]);
55
+ const files = [];
56
+ const fields = raw.split('\0');
57
+ let index = 0;
58
+ while (index < fields.length) {
59
+ const record = fields[index];
60
+ index += 1;
61
+ if (!record)
62
+ continue;
63
+ const [added, deleted, inlinePath] = record.split('\t');
64
+ let path = inlinePath ?? '';
65
+ let renamedFrom = null;
66
+ if (path === '' && index + 1 < fields.length) {
67
+ renamedFrom = fields[index];
68
+ path = fields[index + 1];
69
+ index += 2;
70
+ }
71
+ if (!path)
72
+ continue;
73
+ const binary = added === '-' || deleted === '-';
74
+ files.push(Object.freeze({
75
+ path,
76
+ renamedFrom: renamedFrom || null,
77
+ additions: binary ? 0 : Number(added) || 0,
78
+ deletions: binary ? 0 : Number(deleted) || 0,
79
+ binary,
80
+ sensitive: (0, redaction_1.isSensitiveFilePath)(path),
81
+ }));
82
+ if (files.length >= COMPARE_FILE_LIMIT)
83
+ break;
84
+ }
85
+ return Object.freeze({
86
+ files,
87
+ additions: files.reduce((sum, file) => sum + file.additions, 0),
88
+ deletions: files.reduce((sum, file) => sum + file.deletions, 0),
89
+ truncated: files.length >= COMPARE_FILE_LIMIT,
90
+ });
91
+ };
92
+ return {
93
+ /** Validates a pair of refs and returns their commits, without computing anything yet. */
94
+ async resolvePair(baseRef, headRef) {
95
+ const base = assertRefName(baseRef, 'base');
96
+ const head = assertRefName(headRef, 'head');
97
+ const [baseOid, headOid] = await Promise.all([
98
+ resolveCommit(runGit, base),
99
+ resolveCommit(runGit, head),
100
+ ]);
101
+ return Object.freeze({
102
+ baseRef: base,
103
+ headRef: head,
104
+ baseOid,
105
+ headOid,
106
+ baseLabel: base,
107
+ headLabel: head,
108
+ });
109
+ },
110
+ async compare(input) {
111
+ const pair = await this.resolvePair(input.baseRef, input.headRef);
112
+ const mode = input.mode === 'merge-base' ? 'merge-base' : 'snapshot';
113
+ let baseOid = pair.baseOid;
114
+ let mergeBaseOid = null;
115
+ if (mode === 'merge-base') {
116
+ // `--all` is the honest way to ask: a single answer means one divergence point, and
117
+ // more than one means the question has no unique answer.
118
+ const raw = await runGit(['merge-base', '--all', pair.baseOid, pair.headOid]).catch(error => {
119
+ const message = error instanceof Error ? error.message : '';
120
+ if (/missing|bad object/iu.test(message)) {
121
+ throw new errors_1.WebWorkbenchError(409, 'The repository does not contain the objects needed for this comparison (a shallow clone?).', 'git_shallow_history');
122
+ }
123
+ // git exits non-zero with no candidates when the histories are unrelated, and with
124
+ // a diagnostic when objects are missing — those are different failures.
125
+ throw new errors_1.WebWorkbenchError(409, 'The two refs have no common ancestor; use the two-endpoint comparison instead.', 'git_merge_base_missing');
126
+ });
127
+ const candidates = raw
128
+ .split('\n')
129
+ .map(line => line.trim())
130
+ .filter(line => /^[0-9a-f]{7,64}$/iu.test(line));
131
+ if (candidates.length === 0) {
132
+ throw new errors_1.WebWorkbenchError(409, 'The two refs have no common ancestor; use the two-endpoint comparison instead.', 'git_merge_base_missing');
133
+ }
134
+ if (candidates.length > 1) {
135
+ throw new errors_1.WebWorkbenchError(409, `These refs have ${candidates.length} merge bases, so there is no unique divergence point. Use the two-endpoint comparison instead.`, 'git_merge_base_multiple');
136
+ }
137
+ mergeBaseOid = candidates[0];
138
+ baseOid = mergeBaseOid;
139
+ }
140
+ const stats = await numstat(baseOid, pair.headOid);
141
+ return Object.freeze({
142
+ mode,
143
+ requestedBaseRef: pair.baseRef,
144
+ requestedHeadRef: pair.headRef,
145
+ baseOid,
146
+ headOid: pair.headOid,
147
+ baseLabel: mode === 'merge-base' && mergeBaseOid !== null
148
+ ? `共同祖先 ${mergeBaseOid.slice(0, 10)}`
149
+ : pair.baseLabel,
150
+ headLabel: pair.headLabel,
151
+ mergeBaseOid,
152
+ files: stats.files,
153
+ additions: stats.additions,
154
+ deletions: stats.deletions,
155
+ truncated: stats.truncated,
156
+ });
157
+ },
158
+ };
159
+ }
160
+ //# sourceMappingURL=git-compare-service.js.map
@@ -0,0 +1,83 @@
1
+ /**
2
+ * v0.3.17 S2 — structured Git diff document.
3
+ *
4
+ * The v1 diff page exposed raw unified-diff text lines and a `## Staged` style section
5
+ * banner. That shape made line numbers, side-by-side rendering, hunk folding and the
6
+ * "jump to next change" navigation impossible without re-parsing text in the browser.
7
+ *
8
+ * This module parses the raw lines the Host already produces into a hunk/line model with
9
+ * real old/new line numbers, a stable line id, per-hunk completeness, and the write
10
+ * capabilities that apply to the selected comparison source.
11
+ *
12
+ * Deliberate limits for S2:
13
+ * - No hunk or line level writes. `capabilities.stageHunk` / `stageLines` are always
14
+ * false and carry a reason; staging granularity is S3 (plan §10).
15
+ * - Word level highlighting and ignore-whitespace views are S5. This model preserves the
16
+ * original text so those can be computed later without a protocol change.
17
+ */
18
+ import type { WebGitFileSourceV1 } from './git-file-source';
19
+ export type GitDiffLineKindV2 = 'context' | 'addition' | 'deletion' | 'meta';
20
+ export interface GitDiffLineV2 {
21
+ /** Stable inside one document: `<hunkId>#<index>`. */
22
+ readonly lineId: string;
23
+ readonly kind: GitDiffLineKindV2;
24
+ readonly oldLineNumber: number | null;
25
+ readonly newLineNumber: number | null;
26
+ /** Content without the leading `+`/`-`/space marker. */
27
+ readonly text: string;
28
+ /** The original line including its marker, so copy never loses fidelity. */
29
+ readonly raw: string;
30
+ }
31
+ export interface GitHunkV2 {
32
+ readonly hunkId: string;
33
+ readonly header: string;
34
+ readonly oldStart: number;
35
+ readonly oldLines: number;
36
+ readonly newStart: number;
37
+ readonly newLines: number;
38
+ readonly contextLabel: string;
39
+ readonly lines: readonly GitDiffLineV2[];
40
+ /** False when pagination cut this hunk; partial hunks must not be written (plan G3). */
41
+ readonly complete: boolean;
42
+ }
43
+ export type GitDiffDocumentKindV2 = 'text' | 'binary' | 'symlink' | 'submodule' | 'metadata';
44
+ export interface GitDiffCapabilitiesV2 {
45
+ readonly stageFile: boolean;
46
+ readonly unstageFile: boolean;
47
+ readonly stageHunk: boolean;
48
+ readonly unstageHunk: boolean;
49
+ readonly stageLines: boolean;
50
+ readonly reason?: string;
51
+ }
52
+ export interface GitDiffDocumentV2 {
53
+ readonly schemaVersion: 2;
54
+ readonly fileToken: string;
55
+ readonly path: string;
56
+ readonly source: WebGitFileSourceV1;
57
+ /** Set only for a historical comparison: the base tree and how it was chosen. */
58
+ readonly baseOid?: string;
59
+ readonly baseLabel?: string;
60
+ readonly repositoryRevision: string;
61
+ readonly kind: GitDiffDocumentKindV2;
62
+ readonly binary: boolean;
63
+ readonly hunks: readonly GitHunkV2[];
64
+ /** Metadata lines that are not part of any hunk (file headers, mode changes, …). */
65
+ readonly meta: readonly string[];
66
+ readonly completeness: 'complete' | 'paged' | 'limited';
67
+ readonly additions: number;
68
+ readonly deletions: number;
69
+ readonly lineCount: number;
70
+ readonly nextCursor: string | null;
71
+ readonly capabilities: GitDiffCapabilitiesV2;
72
+ }
73
+ export declare function parseGitDiffDocument(input: {
74
+ readonly fileToken: string;
75
+ readonly path: string;
76
+ readonly source: WebGitFileSourceV1;
77
+ readonly repositoryRevision: string;
78
+ readonly lines: readonly string[];
79
+ readonly hasMore: boolean;
80
+ /** Host-signed continuation cursor, or null. Never synthesised here. */
81
+ readonly nextCursor: string | null;
82
+ }): GitDiffDocumentV2;
83
+ //# sourceMappingURL=git-diff-document.d.ts.map