@orion-agents/orion-code 0.3.12 → 0.3.15

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 (31) hide show
  1. package/CHANGELOG.md +118 -4
  2. package/README.md +6 -8
  3. package/README.zh-CN.md +4 -5
  4. package/dist/web/file-read-service.d.ts +12 -0
  5. package/dist/web/file-read-service.js +24 -1
  6. package/dist/web/file-write-service.d.ts +16 -0
  7. package/dist/web/file-write-service.js +103 -0
  8. package/dist/web/protocol.d.ts +2 -2
  9. package/dist/web/review-service.d.ts +2 -33
  10. package/dist/web/review-service.js +4 -59
  11. package/dist/web/server.js +38 -9
  12. package/dist/web/workbench-controller.d.ts +11 -9
  13. package/dist/web/workbench-controller.js +13 -17
  14. package/dist/web-client/assets/{DiffViewer-Bs1C5uRM.js → DiffViewer-C0A42Lma.js} +1 -1
  15. package/dist/web-client/assets/FilesPanel-DiXKnFMj.js +1 -0
  16. package/dist/web-client/assets/GitPanel--_Yrbldp.js +1 -0
  17. package/dist/web-client/assets/ReviewPanel-BilzL9Ma.js +1 -0
  18. package/dist/web-client/assets/{TerminalPanel-Cz0ohyPm.js → TerminalPanel-BpsLphLv.js} +1 -1
  19. package/dist/web-client/assets/index-C2M48tcK.js +20 -0
  20. package/dist/web-client/assets/index-D3sxrAh_.css +1 -0
  21. package/dist/web-client/assets/useAutoNotice-BeSf9Dfq.js +1 -0
  22. package/dist/web-client/favicon.svg +9 -0
  23. package/dist/web-client/index.html +3 -2
  24. package/npm-shrinkwrap.json +2 -2
  25. package/package.json +1 -1
  26. package/dist/web-client/assets/FilesPanel-dW61EHHn.js +0 -2
  27. package/dist/web-client/assets/GitPanel-ykbaVNlo.js +0 -1
  28. package/dist/web-client/assets/ResourceSplitLayout-B0Zs9aMC.js +0 -1
  29. package/dist/web-client/assets/ReviewPanel-1oqfaqoh.js +0 -1
  30. package/dist/web-client/assets/index-B11PjVU2.css +0 -1
  31. package/dist/web-client/assets/index-DiNz6Qhw.js +0 -17
package/CHANGELOG.md CHANGED
@@ -22,11 +22,125 @@ which is **not** a pass.
22
22
 
23
23
  ## [Unreleased]
24
24
 
25
- ## [0.3.12] — CANDIDATE
25
+ ## [0.3.15] — CANDIDATE
26
26
 
27
- > **Status: candidate.** Wide Right Workspace + repository-truth + guarded Git
28
- > mutations (plan docs/plan/v0.3.12-plan.md) plus the v0.3.13 follow-ups that
29
- > landed on this branch (plans v3/v4/v5). Not merged, tagged or published.
27
+ > **Status: candidate.** Web Workbench visual & interaction convergence
28
+ > (`docs/plan/v0.3.15-plan.md`): copy thinning, unified confirm modal, busy
29
+ > affordances and resource-panel visual tightening. No protocol, API or
30
+ > dependency changes. Not merged, tagged or published.
31
+
32
+ ### Added
33
+
34
+ - **Pixel brand identity:** the Workbench brand is now true pixel art. A
35
+ dot-matrix `orion-code` wordmark (`PixelWordmark`, 5×7 grid, run-merged hard
36
+ rects, integer `scale`) sits in the expanded rail header, and an SVG page
37
+ icon (`web/public/favicon.svg`) gives the browser tab a real favicon (there
38
+ was none). `OrionBrandMark` gained an optional `pixel` prop that renders
39
+ solid two-tone bars with a 1px base shadow; the default gradient glyph used
40
+ by the boot screen and the collapsed rail is unchanged.
41
+
42
+ ### Fixed
43
+
44
+ - Restored the keyboard-shortcut reference trigger (now a rail icon), which was
45
+ lost when the conversation header was removed.
46
+
47
+ - The eager archived-sessions loader no longer refetches in a loop after a
48
+ rejected request: `archived_sessions_failed` now claims the workspace
49
+ (`ownerWorkspaceId`), so the App effect's guard skips settled workspaces, and
50
+ the fire-and-forget call no longer surfaces the workspace-switch Context race
51
+ (`The active Context changed before the operation was admitted`) as hundreds
52
+ of unhandled promise rejections / 409 console errors.
53
+
54
+ ### Changed
55
+
56
+ - **Copy thinning (T1):** removed or compressed eight explanatory paragraphs in
57
+ Files / Git / Review / ProjectNavigator / Inspector; details moved to
58
+ `title` / `aria` attributes (search scope, binary metadata, sensitive-content
59
+ fence, Git read-model detail, hunk-to-composer hint, diagnostics redaction).
60
+ Empty states follow icon + one short line.
61
+ - **Transient notices (T1):** Files/Git/Review recovery and success notices now
62
+ fade out automatically after 4s; error notices persist until replaced
63
+ (`useAutoNotice`).
64
+ - **Unified confirm modal (T2/D3):** the two remaining `window.confirm` guards
65
+ — Files discard-unsaved-changes and Inspector clear-Goal — use a shared
66
+ `ConfirmDialog` (native `<dialog>`: Esc cancel, focus trap, cancel-focused
67
+ Enter, danger action styling).
68
+ - **Busy affordances (T2):** Files/Git/Review refresh buttons report
69
+ `aria-busy` with a rotating icon (box size unchanged, reduced-motion aware);
70
+ the Files save button holds a fixed minimum width so 保存/保存中… no longer
71
+ shifts the row, and reports `aria-busy` while saving.
72
+ - **Visual convergence (T3):** resource-panel icons unified on the 12/16/20
73
+ tiers (16px inside panels), search fields and change-list rows reuse the
74
+ existing radius tokens, and Git group/history headings step back to muted
75
+ 11px type. No new theme tokens.
76
+ - **Banner-free chrome:** the conversation header, the left-rail brand row and
77
+ the right-rail `PROJECT WORKSPACE` header are removed — conversation content
78
+ now starts at the top of the column. The theme-cycle and inspector-toggle
79
+ controls moved to the bottom of the right rail, and the left rail exposes a
80
+ collapse control in its toolbar. Project and session rows keep the plain
81
+ folder icon; the `orion-code` mark is used only in the rail header, the
82
+ collapsed rail and the favicon.
83
+
84
+ ### Tests
85
+
86
+ - New SSR contracts (`tests/web-ui-resource-copy-v0315.test.tsx`): thinned copy
87
+ never returns to first paint, busy buttons idle on first paint, confirm-modal
88
+ markup (focused cancel, danger action, closed shell), goal-clear wiring.
89
+ - E2E locators were unaffected: the only referenced copy (`二进制文件`,
90
+ `刷新文件树`, `选择文件预览`) is unchanged.
91
+
92
+ ## [0.3.14] — CANDIDATE
93
+
94
+ > **Status: candidate.** Editable Files panel (guarded `POST /files/write` with
95
+ > revision CAS) and removal of the legacy view-only controls. Not merged,
96
+ > tagged or published.
97
+
98
+ ### Added
99
+
100
+ - File editing in the Files panel: content view gains a view/edit toggle; the
101
+ edit mode is a controlled monospace textarea with dirty tracking, explicit
102
+ save (guarded `POST /files/write`), cancel/Esc with an unsaved-changes
103
+ confirmation, and a 409 conflict path that offers an explicit reload.
104
+ - `POST /files/write`: workspace-fenced (registry + symlink escape checks),
105
+ sensitive-path block, 512 KiB text-only payload limit, on-disk stat
106
+ fingerprint CAS (`file_revision_conflict`), user-gesture header
107
+ (`file-mutation-v1`), idempotent `file.write` mutation ledger entry, and
108
+ one-shot snapshot invalidation so Git/Review decorations refresh after a
109
+ save.
110
+
111
+ ### Fixed
112
+
113
+ - Resource refreshes keep the inspected file: `ReviewPanel` no longer drops the
114
+ open diff on a manual refresh, a `refreshEpoch` invalidation or a Git revision
115
+ conflict recovery; `FilesPanel` keeps the open file on refresh (still reset
116
+ when the workspace itself changes), so the editor no longer closes mid-edit.
117
+ - The review summary counters are localised (已暂存 / 未暂存 / 未跟踪 / 冲突)
118
+ instead of raw English labels.
119
+ - Binary detection tolerates an 8 KiB sample that ends inside one UTF-8 code
120
+ point, so CJK files are no longer rejected as binary on read or save.
121
+
122
+ ### Removed
123
+
124
+ - The read-only file toolbar: 「跳转」(line jump), 「复制」 and 「自动换行」
125
+ controls, the line-number gutter spans and the wrap-only styles.
126
+ - The Review panel's 「验证证据」 module: the evidence list,
127
+ `WebReviewVerificationV1`, `ReviewServiceV1.verificationPage`, the
128
+ `GET /review/verification` route and the snapshot's `verification` /
129
+ `verificationTruncated` fields. The review overview is now composed only from
130
+ Git facts; durable tool receipts remain available to the runtime.
131
+
132
+ ## [0.3.13] — CANDIDATE
133
+
134
+ > **Status: candidate.** Development baseline after v0.3.12 (version bump only;
135
+ > no functional changes yet). Not merged, tagged or published.
136
+
137
+ ## [0.3.12] — NPM-PUBLISHED
138
+
139
+ > **Status: npm-published.** `@orion-agents/orion-code@0.3.12` is available
140
+ > through the npm registry (latest tag). Published 2026-09-07 via PR #252;
141
+ > git tag/release may lag. Wide Right Workspace + repository-truth + guarded
142
+ > Git mutations (v0.3.12-plan) plus resource split layout, conversation
143
+ > history navigator and the unified brand mark (v0.3.13-plan v3/v4/v5).
30
144
 
31
145
  ### Added
32
146
 
package/README.md CHANGED
@@ -2,13 +2,11 @@
2
2
 
3
3
  Local-first, goal-driven coding agent for the terminal and browser.
4
4
 
5
- > v0.3.12 candidate — a Wide Right Workspace on top of the vertical rail: pure
6
- > dock/rail/drawer geometry (detail dockable to 80vw while the conversation
7
- > keeps a 320px minimum), per-workspace layout preferences, a shared repository
8
- > snapshot store, progressive Review verification, guarded Git stage/unstage/
9
- > commit, bounded workspace search and revision-bound file_range Context
10
- > references. Candidate source is not an npm publication, Git tag, or
11
- > merge-ready release.
5
+ > v0.3.15 candidate — Web Workbench visual & interaction convergence: thinned
6
+ > explanatory copy across the resource panels, one unified confirm modal
7
+ > replacing the last `window.confirm` guards, busy feedback for refresh/save
8
+ > actions, and Orion-style visual tightening of Files / Git / Review.
9
+ > Candidate source is not an npm publication, Git tag, or merge-ready release.
12
10
 
13
11
  [中文说明](README.zh-CN.md) ·
14
12
  [v0.3.7 plan](docs/plan/v0.3.7-left-rail-improvement-plan.md) ·
@@ -173,7 +171,7 @@ current development environments. Node 20 is upstream EOL and is no longer a v0.
173
171
  After the immutable `0.3.5` npm receipt exists:
174
172
 
175
173
  ```bash
176
- npm install -g @orion-agents/orion-code@0.3.12
174
+ npm install -g @orion-agents/orion-code@0.3.15
177
175
  orion --version
178
176
  orion doctor
179
177
  ```
package/README.zh-CN.md CHANGED
@@ -2,10 +2,9 @@
2
2
 
3
3
  面向终端与浏览器、本地优先的目标驱动 Coding Agent。
4
4
 
5
- > v0.3.12 候选版本:在竖状 rail 之上的**大尺寸右侧工作区**——纯 dock/rail/drawer 几何
6
- > (详情面可拉宽至 80vw,对话保留 320px 最小宽度)、按 workspace 的布局偏好、共享仓库快照
7
- > store、Review 验证证据渐进分页、受控 Git stage/unstage/commit、受限工作区搜索与带版本
8
- > 约束的 file_range Context 引用。Candidate 源码不代表已创建 npm 发布、Git tag 或达到可合并状态。
5
+ > v0.3.15 候选版本:Web Workbench 视觉与交互收敛——资源面板说明文案瘦身、统一确认模态
6
+ > 取代最后的 `window.confirm`、刷新/保存的进行中反馈,以及 Files / Git / Review 的
7
+ > Orion 风格视觉收敛。Candidate 源码不代表已创建 npm 发布、Git tag 或达到可合并状态。
9
8
 
10
9
  [English](README.md) ·
11
10
  [v0.3.7 方案](docs/plan/v0.3.7-left-rail-improvement-plan.md) ·
@@ -102,7 +101,7 @@ Current。Node 20 已结束上游维护,不再属于 v0.3 Runtime 合同。
102
101
  当 npm 已存在不可变的 `0.3.4` 发布凭据后:
103
102
 
104
103
  ```bash
105
- npm install -g @orion-agents/orion-code@0.3.12
104
+ npm install -g @orion-agents/orion-code@0.3.15
106
105
  orion --version
107
106
  orion doctor
108
107
  ```
@@ -1,3 +1,4 @@
1
+ import { type BigIntStats } from 'fs';
1
2
  export type WebFileKindV1 = 'file' | 'directory' | 'symlink';
2
3
  export interface WebFileNodeV1 {
3
4
  readonly id: string;
@@ -79,6 +80,15 @@ export declare class FileReadServiceV1 {
79
80
  readonly cursor?: string;
80
81
  readonly limitBytes?: number;
81
82
  }): WebFileContentPageV1;
83
+ /** v0.3.14 — write-path preflight for an existing in-root regular text file. */
84
+ writePreflight(fileId: string): {
85
+ readonly canonicalPath: string;
86
+ readonly relativePath: string;
87
+ readonly revision: string;
88
+ readonly sizeBytes: number;
89
+ };
90
+ /** v0.3.14 — post-write revision fingerprint for the response. */
91
+ revisionOf(canonicalPath: string): string;
82
92
  private projectNode;
83
93
  private resolveNode;
84
94
  private resolveRelative;
@@ -86,4 +96,6 @@ export declare class FileReadServiceV1 {
86
96
  private encodeCursor;
87
97
  private decodeCursor;
88
98
  }
99
+ export declare function fingerprintStat(stat: BigIntStats): string;
100
+ export declare function isBinary(buffer: Buffer): boolean;
89
101
  //# sourceMappingURL=file-read-service.d.ts.map
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.FileReadServiceV1 = void 0;
4
+ exports.fingerprintStat = fingerprintStat;
5
+ exports.isBinary = isBinary;
4
6
  const crypto_1 = require("crypto");
5
7
  const fs_1 = require("fs");
6
8
  const path_1 = require("path");
@@ -269,7 +271,7 @@ class FileReadServiceV1 {
269
271
  if (sample.length > 0) {
270
272
  this.performance.bytesRead += (0, fs_1.readSync)(descriptor, sample, 0, sample.length, 0);
271
273
  }
272
- const binary = isBinary(sample);
274
+ const binary = isBinary(validUtf8Prefix(sample));
273
275
  if (binary) {
274
276
  return Object.freeze({
275
277
  fileId: input.fileId,
@@ -333,6 +335,27 @@ class FileReadServiceV1 {
333
335
  (0, fs_1.closeSync)(descriptor);
334
336
  }
335
337
  }
338
+ /** v0.3.14 — write-path preflight for an existing in-root regular text file. */
339
+ writePreflight(fileId) {
340
+ this.performance.readOperations += 1;
341
+ const node = this.resolveNode(fileId);
342
+ if (!node.stat.isFile()) {
343
+ throw new errors_1.WebWorkbenchError(409, 'File node is not a regular file.', 'file_not_regular');
344
+ }
345
+ if (isSensitiveResolvedPath(this.root, node.relativePath, node.canonicalPath)) {
346
+ throw new errors_1.WebWorkbenchError(403, 'Sensitive files are not writable in the Web Workbench.', 'sensitive_file_blocked');
347
+ }
348
+ return Object.freeze({
349
+ canonicalPath: node.canonicalPath,
350
+ relativePath: node.relativePath,
351
+ revision: fingerprintStat(node.stat),
352
+ sizeBytes: Number(node.stat.size),
353
+ });
354
+ }
355
+ /** v0.3.14 — post-write revision fingerprint for the response. */
356
+ revisionOf(canonicalPath) {
357
+ return fingerprintStat((0, fs_1.statSync)(canonicalPath, { bigint: true }));
358
+ }
336
359
  projectNode(relativePath) {
337
360
  const lexicalPath = (0, path_1.resolve)(this.root, relativePath);
338
361
  const lexicalStat = (0, fs_1.lstatSync)(lexicalPath, { bigint: true });
@@ -0,0 +1,16 @@
1
+ import { FileReadServiceV1 } from './file-read-service';
2
+ export interface WebFileWriteResultV1 {
3
+ readonly fileId: string;
4
+ readonly revision: string;
5
+ readonly sizeBytes: number;
6
+ }
7
+ export declare class FileWriteServiceV1 {
8
+ private readonly fileService;
9
+ constructor(fileService: FileReadServiceV1);
10
+ writeContent(input: {
11
+ readonly fileId: string;
12
+ readonly content: string;
13
+ readonly expectedRevision: string;
14
+ }): WebFileWriteResultV1;
15
+ }
16
+ //# sourceMappingURL=file-write-service.d.ts.map
@@ -0,0 +1,103 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FileWriteServiceV1 = void 0;
4
+ const fs_1 = require("fs");
5
+ const errors_1 = require("./errors");
6
+ const file_read_service_1 = require("./file-read-service");
7
+ const MAX_WRITE_BYTES = 512 * 1024;
8
+ const WRITE_SAMPLE_BYTES = 8192;
9
+ /**
10
+ * v0.3.14 — bounded, CAS-guarded writes for an existing in-root text file.
11
+ *
12
+ * Safety stack mirrors the read path: the file id resolves through the read
13
+ * service's fenced registry (workspace escape, symlink escape and sensitive
14
+ * paths fail closed there), the payload must be valid UTF-8 text without NULs
15
+ * within 512 KiB, and the write commits only when the on-disk revision still
16
+ * matches the caller's `expectedRevision` (compare-and-swap against the same
17
+ * stat fingerprint the read path returns).
18
+ */
19
+ /**
20
+ * A bounded sample can end inside one UTF-8 code point (very likely for CJK
21
+ * text at an 8 KiB boundary); drop the trailing partial byte before judging the
22
+ * file as binary, mirroring the read path's `validUtf8Prefix`.
23
+ */
24
+ function completeUtf8Tail(buffer) {
25
+ for (let trim = 0; trim <= Math.min(3, Math.max(0, buffer.length - 1)); trim += 1) {
26
+ const candidate = buffer.subarray(0, buffer.length - trim);
27
+ if (!(0, file_read_service_1.isBinary)(candidate))
28
+ return candidate;
29
+ }
30
+ return buffer;
31
+ }
32
+ class FileWriteServiceV1 {
33
+ constructor(fileService) {
34
+ this.fileService = fileService;
35
+ }
36
+ writeContent(input) {
37
+ const content = input.content;
38
+ if (typeof content !== 'string') {
39
+ throw new errors_1.WebWorkbenchError(400, 'File content must be a string.', 'file_content_invalid');
40
+ }
41
+ if (content.includes('\0')) {
42
+ throw new errors_1.WebWorkbenchError(415, 'Files containing NUL bytes are not editable in the Web Workbench.', 'file_binary');
43
+ }
44
+ const byteLength = Buffer.byteLength(content, 'utf8');
45
+ if (byteLength > MAX_WRITE_BYTES) {
46
+ throw new errors_1.WebWorkbenchError(413, 'Edited file content exceeds the 512 KiB Web editor limit.', 'file_too_large');
47
+ }
48
+ if (!/^[0-9a-f]{64}$/u.test(input.expectedRevision)) {
49
+ throw new errors_1.WebWorkbenchError(400, 'expectedRevision is invalid.', 'file_revision_invalid');
50
+ }
51
+ const preflight = this.fileService.writePreflight(input.fileId);
52
+ if (preflight.sizeBytes > MAX_WRITE_BYTES) {
53
+ throw new errors_1.WebWorkbenchError(413, 'Target file exceeds the 512 KiB Web editor limit.', 'file_too_large');
54
+ }
55
+ const payload = Buffer.from(content, 'utf8');
56
+ // Phase 1 (read descriptor): CAS + binary re-check. An O_WRONLY descriptor
57
+ // cannot readSync, so the preflight opens its own read handle first.
58
+ const readDescriptor = (0, fs_1.openSync)(preflight.canonicalPath, fs_1.constants.O_RDONLY);
59
+ let beforeSize;
60
+ try {
61
+ const before = (0, fs_1.fstatSync)(readDescriptor, { bigint: true });
62
+ beforeSize = before.size;
63
+ const sample = Buffer.alloc(Math.min(WRITE_SAMPLE_BYTES, Number(before.size)));
64
+ if (sample.length > 0) {
65
+ (0, fs_1.readSync)(readDescriptor, sample, 0, sample.length, 0);
66
+ if ((0, file_read_service_1.isBinary)(completeUtf8Tail(sample))) {
67
+ throw new errors_1.WebWorkbenchError(415, 'Target file is not UTF-8 text.', 'file_binary');
68
+ }
69
+ }
70
+ if ((0, file_read_service_1.fingerprintStat)(before) !== input.expectedRevision) {
71
+ throw new errors_1.WebWorkbenchError(409, 'The file changed on disk before the save could run; reload it first.', 'file_revision_conflict');
72
+ }
73
+ }
74
+ finally {
75
+ (0, fs_1.closeSync)(readDescriptor);
76
+ }
77
+ // Phase 2 (write descriptor): CAS is re-checked by the kernel fingerprint
78
+ // window being as short as possible between the two handles.
79
+ const writeDescriptor = (0, fs_1.openSync)(preflight.canonicalPath, fs_1.constants.O_WRONLY);
80
+ try {
81
+ const before = (0, fs_1.fstatSync)(writeDescriptor, { bigint: true });
82
+ if ((0, file_read_service_1.fingerprintStat)(before) !== input.expectedRevision) {
83
+ throw new errors_1.WebWorkbenchError(409, 'The file changed on disk before the save could run; reload it first.', 'file_revision_conflict');
84
+ }
85
+ (0, fs_1.writeSync)(writeDescriptor, payload, 0, payload.length, 0);
86
+ // Shrink keeps the file exactly as long as the payload.
87
+ if (beforeSize > BigInt(payload.length)) {
88
+ (0, fs_1.truncateSync)(preflight.canonicalPath, payload.length);
89
+ }
90
+ const after = (0, fs_1.statSync)(preflight.canonicalPath, { bigint: true });
91
+ return Object.freeze({
92
+ fileId: input.fileId,
93
+ revision: (0, file_read_service_1.fingerprintStat)(after),
94
+ sizeBytes: Number(after.size),
95
+ });
96
+ }
97
+ finally {
98
+ (0, fs_1.closeSync)(writeDescriptor);
99
+ }
100
+ }
101
+ }
102
+ exports.FileWriteServiceV1 = FileWriteServiceV1;
103
+ //# sourceMappingURL=file-write-service.js.map
@@ -9,7 +9,7 @@ import type { SessionMeta } from '../services/session-storage';
9
9
  import type { ToolConfirmationPolicy } from '../services/global-config';
10
10
  import type { WebFileContentPageV1, WebFileNodeV1, WebFileTreePageV1 } from './file-read-service';
11
11
  import type { WebGitCommitV1, WebGitDiffPageV1, WebGitFileV1, WebGitLogPageV1, WebGitStatusV1 } from './git-read-model-service';
12
- import type { WebReviewSnapshotV1, WebReviewVerificationV1 } from './review-service';
12
+ import type { WebReviewSnapshotV1 } from './review-service';
13
13
  import type { WebSessionRuntimeSummaryV1 } from './session-runtime-registry';
14
14
  import type { WebTerminalCreateResultV1, WebTerminalExitV1, WebTerminalGapV1, WebTerminalMetadataV1, WebTerminalOutputFrameV1, WebTerminalStateV1 } from './terminal-manager';
15
15
  export declare const WEB_API_VERSION: 1;
@@ -376,7 +376,7 @@ export type WebWorkspaceResourceV1 = 'files' | 'git' | 'review';
376
376
  export type WebWorkspaceInvalidationReasonV1 = 'context-change' | 'filesystem-change' | 'terminal-command' | 'tool-finished';
377
377
  export type WebToolDetailSummaryV1 = ToolDetailSummary;
378
378
  export type WebToolDetailPageV1 = ToolDetailPage;
379
- export type { WebFileContentPageV1, WebFileNodeV1, WebFileTreePageV1, WebGitCommitV1, WebGitDiffPageV1, WebGitFileV1, WebGitLogPageV1, WebGitStatusV1, WebReviewSnapshotV1, WebReviewVerificationV1, WebTerminalCreateResultV1, WebTerminalExitV1, WebTerminalGapV1, WebTerminalMetadataV1, WebTerminalOutputFrameV1, WebTerminalStateV1, };
379
+ export type { WebFileContentPageV1, WebFileNodeV1, WebFileTreePageV1, WebGitCommitV1, WebGitDiffPageV1, WebGitFileV1, WebGitLogPageV1, WebGitStatusV1, WebReviewSnapshotV1, WebTerminalCreateResultV1, WebTerminalExitV1, WebTerminalGapV1, WebTerminalMetadataV1, WebTerminalOutputFrameV1, WebTerminalStateV1, };
380
380
  export interface WebBootstrapV1 {
381
381
  readonly apiVersion: 1;
382
382
  readonly productVersion: string;
@@ -1,19 +1,4 @@
1
- import type { VerifiedDurableToolReceiptRefV1 } from '../runtime/durable-tool-receipt-reader';
2
1
  import type { GitReadModelServiceV1, WebGitDiffPageV1, WebGitFileV1 } from './git-read-model-service';
3
- export interface WebReviewVerificationV1 {
4
- readonly callId: string;
5
- readonly sessionId: string;
6
- readonly threadId: string;
7
- readonly sequence: number;
8
- readonly toolName: string;
9
- readonly state: 'success' | 'error' | 'skipped';
10
- readonly terminal: VerifiedDurableToolReceiptRefV1['terminal'];
11
- readonly success: boolean;
12
- readonly outputBytes: number;
13
- readonly hasArtifact: boolean;
14
- readonly executionPolicyDigest: string;
15
- readonly receiptDigest: string;
16
- }
17
2
  export interface WebReviewSnapshotV1 {
18
3
  readonly revision: string;
19
4
  readonly repositoryRevision: string;
@@ -26,28 +11,12 @@ export interface WebReviewSnapshotV1 {
26
11
  readonly untrackedCount: number;
27
12
  readonly conflictCount: number;
28
13
  readonly truncated: boolean;
29
- readonly verification: readonly WebReviewVerificationV1[];
30
14
  }
31
- /** Review overview composed only from Git facts and doubly verified durable tool receipts. */
15
+ /** Review overview composed only from Git facts. */
32
16
  export declare class ReviewServiceV1 {
33
17
  private readonly git;
34
- private readonly listReceiptRefs;
35
- constructor(git: GitReadModelServiceV1, listReceiptRefs: () => Promise<readonly VerifiedDurableToolReceiptRefV1[]> | readonly VerifiedDurableToolReceiptRefV1[]);
18
+ constructor(git: GitReadModelServiceV1);
36
19
  snapshot(): Promise<WebReviewSnapshotV1>;
37
- /**
38
- * v0.3.12 S2 — progressive verification evidence. The Review summary loads
39
- * first; receipts page in behind it, optionally filtered to one session.
40
- */
41
- verificationPage(input: {
42
- readonly sessionId?: string;
43
- readonly cursor?: number;
44
- readonly pageSize?: number;
45
- }): Promise<{
46
- readonly items: readonly WebReviewVerificationV1[];
47
- readonly nextCursor: number | null;
48
- readonly totalForSession: number;
49
- }>;
50
- private projectVerification;
51
20
  diff(input: Parameters<GitReadModelServiceV1['diff']>[0]): Promise<WebGitDiffPageV1>;
52
21
  }
53
22
  //# sourceMappingURL=review-service.d.ts.map
@@ -2,21 +2,17 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ReviewServiceV1 = void 0;
4
4
  const crypto_1 = require("crypto");
5
- /** Review overview composed only from Git facts and doubly verified durable tool receipts. */
5
+ /** Review overview composed only from Git facts. */
6
6
  class ReviewServiceV1 {
7
- constructor(git, listReceiptRefs) {
7
+ constructor(git) {
8
8
  this.git = git;
9
- this.listReceiptRefs = listReceiptRefs;
10
9
  }
11
10
  async snapshot() {
12
11
  // v0.3.9 #237/#228 — collect every Git status page. A repository with more
13
12
  // than the single pageSize of changed files was silently truncated before;
14
13
  // the review overview must aggregate all pages (bounded by a guard that
15
14
  // keeps runaway repositories from looping forever).
16
- const [receiptRefs, statusPages] = await Promise.all([
17
- this.listReceiptRefs(),
18
- collectStatusPages((cursor) => this.git.status({ pageSize: 2000, ...(cursor ? { cursor } : {}) })),
19
- ]);
15
+ const statusPages = await collectStatusPages((cursor) => this.git.status({ pageSize: 2000, ...(cursor ? { cursor } : {}) }));
20
16
  const status = statusPages.at(-1) ?? (await this.git.status({ pageSize: 2000 }));
21
17
  const conflicted = [];
22
18
  const staged = [];
@@ -29,12 +25,8 @@ class ReviewServiceV1 {
29
25
  untracked.push(...page.untracked);
30
26
  }
31
27
  const changedFiles = uniqueFiles([...conflicted, ...staged, ...unstaged, ...untracked]);
32
- const verification = this.projectVerification(receiptRefs.slice(0, 100));
33
28
  const revision = (0, crypto_1.createHash)('sha256')
34
- .update(JSON.stringify({
35
- repositoryRevision: status.repositoryRevision,
36
- verification,
37
- }))
29
+ .update(JSON.stringify({ repositoryRevision: status.repositoryRevision }))
38
30
  .digest('hex');
39
31
  return Object.freeze({
40
32
  revision,
@@ -48,66 +40,19 @@ class ReviewServiceV1 {
48
40
  untrackedCount: untracked.length,
49
41
  conflictCount: conflicted.length,
50
42
  truncated: statusPages.at(-1)?.truncated ?? false,
51
- verification: Object.freeze(verification),
52
- });
53
- }
54
- /**
55
- * v0.3.12 S2 — progressive verification evidence. The Review summary loads
56
- * first; receipts page in behind it, optionally filtered to one session.
57
- */
58
- async verificationPage(input) {
59
- const refs = await this.listReceiptRefs();
60
- const filtered = input.sessionId ? refs.filter(ref => ref.sessionId === input.sessionId) : refs;
61
- const offset = Math.max(0, Math.min(filtered.length, Math.trunc(input.cursor ?? 0)));
62
- const pageSize = Math.max(1, Math.min(100, Math.trunc(input.pageSize ?? 25)));
63
- const page = filtered.slice(offset, offset + pageSize);
64
- const nextCursor = offset + page.length < filtered.length ? offset + page.length : null;
65
- return Object.freeze({
66
- items: Object.freeze(this.projectVerification(page)),
67
- nextCursor,
68
- totalForSession: filtered.length,
69
43
  });
70
44
  }
71
- projectVerification(refs) {
72
- return refs.map(receipt => Object.freeze({
73
- callId: receipt.callId,
74
- sessionId: receipt.sessionId,
75
- threadId: receipt.threadId,
76
- sequence: receipt.sequence,
77
- toolName: receipt.toolName,
78
- state: reviewState(receipt),
79
- terminal: receipt.terminal,
80
- success: receipt.success,
81
- outputBytes: receipt.outputBytes,
82
- hasArtifact: receipt.hasArtifact,
83
- executionPolicyDigest: receipt.executionPolicyDigest,
84
- receiptDigest: receipt.receiptDigest,
85
- }));
86
- }
87
45
  diff(input) {
88
46
  return this.git.diff(input);
89
47
  }
90
48
  }
91
49
  exports.ReviewServiceV1 = ReviewServiceV1;
92
- function reviewState(receipt) {
93
- if (receipt.success)
94
- return 'success';
95
- return receipt.terminal === 'interrupted' || receipt.terminal === 'indeterminate'
96
- ? 'skipped'
97
- : 'error';
98
- }
99
50
  function uniqueFiles(files) {
100
51
  const byId = new Map();
101
52
  for (const file of files)
102
53
  byId.set(file.fileId, file);
103
54
  return [...byId.values()].sort((left, right) => left.path.localeCompare(right.path));
104
55
  }
105
- /**
106
- * v0.3.9 #237/#228 — page through Git status until the snapshot is fully
107
- * collected. The bounded guard (64 pages x 2_000 = 128k files) exists to stop
108
- * runaway repositories from looping; a repository exceeding it stays marked
109
- * truncated so the UI can still communicate the limit.
110
- */
111
56
  async function collectStatusPages(fetchPage) {
112
57
  const pages = [];
113
58
  let page = await fetchPage(undefined);
@@ -431,6 +431,32 @@ async function handleRequest(context) {
431
431
  }));
432
432
  return;
433
433
  }
434
+ const fileWriteMatch = path === '/files/write';
435
+ if (method === 'POST' && fileWriteMatch) {
436
+ assertMutation(request, context.nonce, context.origin, 'file_mutation_forbidden');
437
+ assertFileUserGesture(request);
438
+ const body = requireRecord(await readJson(request), 'File write request');
439
+ assertOnlyKeys(body, [
440
+ 'requestId',
441
+ 'fileId',
442
+ 'content',
443
+ 'expectedRevision',
444
+ 'workspaceId',
445
+ 'expectedContextRevision',
446
+ ]);
447
+ const requestId = requireUuid(body.requestId, 'requestId');
448
+ const contextGuard = requireContextGuardRecord(body);
449
+ const fileId = requireText(body.fileId, 'fileId', 256);
450
+ const content = requireText(body.content, 'content', 512 * 1024 + 8);
451
+ const expectedRevision = requireFileRevision(body.expectedRevision);
452
+ const result = await context.workbench.executeMutation(requestId, 'file.write', { fileId, expectedRevision, ...contextGuard }, () => context.workbench.writeFileContent(contextGuard, {
453
+ fileId,
454
+ content,
455
+ expectedRevision,
456
+ }));
457
+ sendJson(response, 200, result);
458
+ return;
459
+ }
434
460
  if (method === 'POST' && (path === '/git/stage' || path === '/git/unstage')) {
435
461
  assertMutation(request, context.nonce, context.origin, 'git_mutation_forbidden');
436
462
  assertGitUserGesture(request);
@@ -495,15 +521,6 @@ async function handleRequest(context) {
495
521
  }));
496
522
  return;
497
523
  }
498
- if (method === 'GET' && path === '/review/verification') {
499
- const contextGuard = requireContextGuardQuery(url);
500
- const sessionId = url.searchParams.get('sessionId') ?? undefined;
501
- const cursorValue = url.searchParams.get('cursor');
502
- const cursor = cursorValue === null ? undefined : Number(cursorValue);
503
- const pageSize = boundedInteger(url.searchParams.get('pageSize'), 25, 1, 100);
504
- sendJson(response, 200, await context.workbench.reviewVerificationPage(contextGuard, { sessionId, cursor, pageSize }));
505
- return;
506
- }
507
524
  if (method === 'GET' && path === '/review') {
508
525
  sendJson(response, 200, await context.workbench.review(requireContextGuardQuery(url)));
509
526
  return;
@@ -646,6 +663,18 @@ function requireGitRevision(value) {
646
663
  }
647
664
  return text;
648
665
  }
666
+ function assertFileUserGesture(request) {
667
+ if (request.headers[protocol_1.WEB_USER_GESTURE_HEADER] !== 'file-mutation-v1') {
668
+ throw new HttpProblem(403, 'File mutations require an explicit browser user gesture.', 'file_user_gesture_required');
669
+ }
670
+ }
671
+ function requireFileRevision(value) {
672
+ const text = requireText(value, 'expectedRevision', 64);
673
+ if (!/^[0-9a-f]{64}$/u.test(text)) {
674
+ throw new HttpProblem(400, 'expectedRevision must be a file content revision.');
675
+ }
676
+ return text;
677
+ }
649
678
  function assertGitUserGesture(request) {
650
679
  if (request.headers[protocol_1.WEB_USER_GESTURE_HEADER] !== 'git-mutation-v1') {
651
680
  throw new HttpProblem(403, 'Git mutations require an explicit browser user gesture.', 'git_user_gesture_required');
@@ -43,6 +43,7 @@ export declare class WebWorkbenchController {
43
43
  private readonly workspaceMutationOwners;
44
44
  private readonly pendingWorkspaceMutationStates;
45
45
  private fileService;
46
+ private fileWriteService;
46
47
  private gitService;
47
48
  private reviewService;
48
49
  private contextRevisionValue;
@@ -100,6 +101,16 @@ export declare class WebWorkbenchController {
100
101
  readToolDetail(callId: string, offsetBytes: number, limitBytes: number, context?: WebContextGuardV1): Promise<WebToolDetailPageV1>;
101
102
  listFiles(context: WebContextGuardV1, input: Parameters<FileReadServiceV1['list']>[0]): import("./file-read-service").WebFileTreePageV1;
102
103
  readFileContent(context: WebContextGuardV1, input: Parameters<FileReadServiceV1['readContent']>[0]): import("./file-read-service").WebFileContentPageV1;
104
+ /** v0.3.14 — guarded CAS write for an existing in-root text file. */
105
+ writeFileContent(context: WebContextGuardV1, input: {
106
+ readonly fileId: string;
107
+ readonly content: string;
108
+ readonly expectedRevision: string;
109
+ }): {
110
+ readonly fileId: string;
111
+ readonly revision: string;
112
+ readonly sizeBytes: number;
113
+ };
103
114
  gitStatus(context: WebContextGuardV1, input: Parameters<GitReadModelServiceV1['status']>[0]): Promise<import("./git-read-model-service").WebGitStatusV1>;
104
115
  searchFiles(context: WebContextGuardV1, input: {
105
116
  readonly query: string;
@@ -137,15 +148,6 @@ export declare class WebWorkbenchController {
137
148
  private resolveSafeGitPaths;
138
149
  private assertGitRepositoryRevision;
139
150
  private fileContextRevision;
140
- reviewVerificationPage(context: WebContextGuardV1, input: {
141
- readonly sessionId?: string;
142
- readonly cursor?: number;
143
- readonly pageSize?: number;
144
- }): Promise<{
145
- readonly items: readonly import("./review-service").WebReviewVerificationV1[];
146
- readonly nextCursor: number | null;
147
- readonly totalForSession: number;
148
- }>;
149
151
  gitLog(context: WebContextGuardV1, input: Parameters<GitReadModelServiceV1['log']>[0]): Promise<import("./git-read-model-service").WebGitLogPageV1>;
150
152
  gitDiff(context: WebContextGuardV1, input: Parameters<GitReadModelServiceV1['diff']>[0]): Promise<import("./git-read-model-service").WebGitDiffPageV1>;
151
153
  review(context: WebContextGuardV1): Promise<import("./review-service").WebReviewSnapshotV1>;