@erdemtuna/doc-review 0.9.0 → 0.11.0

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 (103) hide show
  1. package/README.md +58 -222
  2. package/{src → lib}/SKILL.md +5 -2
  3. package/lib/anchor-text.js +155 -0
  4. package/lib/atomic-write.js +54 -0
  5. package/lib/changes-controller.js +184 -0
  6. package/lib/chrome-api.js +78 -0
  7. package/lib/chrome-client.js +1700 -0
  8. package/lib/chrome-session.js +73 -0
  9. package/lib/chrome.html +53 -0
  10. package/lib/cli.js +255 -0
  11. package/lib/click-target.js +28 -0
  12. package/lib/comment-anchor.js +23 -0
  13. package/lib/comment-target.js +164 -0
  14. package/lib/comments-controller.js +109 -0
  15. package/lib/contextual-controller.js +33 -0
  16. package/lib/contracts/feedback.js +1 -0
  17. package/lib/contracts/frame.js +7 -0
  18. package/lib/contracts/history.js +1 -0
  19. package/lib/contracts/index.js +2 -0
  20. package/lib/contracts/page.js +23 -0
  21. package/lib/controller-store.js +96 -0
  22. package/lib/document-execution.js +115 -0
  23. package/lib/edit-limits.js +24 -0
  24. package/lib/editing.js +107 -0
  25. package/lib/execution-client.js +19 -0
  26. package/lib/feedback-controller.js +113 -0
  27. package/lib/feedback-panel-controller.js +161 -0
  28. package/lib/frame-channel.js +42 -0
  29. package/lib/frame-controller.js +313 -0
  30. package/lib/frame-host.js +136 -0
  31. package/lib/frame-policy.js +50 -0
  32. package/lib/history-client.js +112 -0
  33. package/lib/history-coordinator.js +217 -0
  34. package/lib/history-policy.js +43 -0
  35. package/lib/history-server.js +464 -0
  36. package/lib/html-transform.js +52 -0
  37. package/lib/icons.js +325 -0
  38. package/{src → lib}/markdown.js +25 -26
  39. package/lib/paths.js +83 -0
  40. package/lib/poll-transport.js +220 -0
  41. package/lib/positioning.js +97 -0
  42. package/lib/recovery-controller.js +123 -0
  43. package/lib/review-controller.js +93 -0
  44. package/lib/review-mode.js +66 -0
  45. package/lib/revision-diff.js +482 -0
  46. package/lib/revision-schema.js +235 -0
  47. package/lib/revision-store.js +190 -0
  48. package/lib/save-controller.js +303 -0
  49. package/lib/sdk.js +2629 -0
  50. package/lib/semantic-snapshot.js +268 -0
  51. package/lib/serialize.js +32 -0
  52. package/lib/server-entry.js +26 -0
  53. package/lib/server-lock.js +109 -0
  54. package/lib/server.js +1501 -0
  55. package/lib/setup-guidance.js +119 -0
  56. package/{src → lib}/setup.js +44 -55
  57. package/lib/state.js +733 -0
  58. package/lib/toolbar-controller.js +32 -0
  59. package/lib/ui/THIRD_PARTY_NOTICES.md +1254 -0
  60. package/lib/ui/chrome.css +4881 -0
  61. package/lib/ui/chrome.js +52 -0
  62. package/lib/view-identity.js +112 -0
  63. package/package.json +50 -14
  64. package/src/anchor-text.js +0 -160
  65. package/src/atomic-write.js +0 -53
  66. package/src/chrome-client.js +0 -2747
  67. package/src/chrome-session.js +0 -85
  68. package/src/chrome.css +0 -652
  69. package/src/chrome.html +0 -196
  70. package/src/cli.js +0 -271
  71. package/src/click-target.js +0 -26
  72. package/src/comment-anchor.js +0 -22
  73. package/src/comment-target.js +0 -164
  74. package/src/comparison-view.js +0 -337
  75. package/src/document-execution.js +0 -103
  76. package/src/edit-limits.js +0 -23
  77. package/src/editing.js +0 -99
  78. package/src/execution-client.js +0 -63
  79. package/src/frame-channel.js +0 -44
  80. package/src/frame-policy.js +0 -54
  81. package/src/history-client.js +0 -104
  82. package/src/history-coordinator.js +0 -186
  83. package/src/history-policy.js +0 -43
  84. package/src/history-server.js +0 -463
  85. package/src/html-transform.js +0 -62
  86. package/src/icons.js +0 -251
  87. package/src/paths.js +0 -91
  88. package/src/poll-transport.js +0 -222
  89. package/src/positioning.js +0 -107
  90. package/src/review-mode.js +0 -59
  91. package/src/revision-diff.js +0 -440
  92. package/src/revision-schema.js +0 -219
  93. package/src/revision-store.js +0 -177
  94. package/src/sdk.js +0 -2607
  95. package/src/semantic-snapshot.js +0 -230
  96. package/src/serialize.js +0 -32
  97. package/src/server-entry.js +0 -26
  98. package/src/server-lock.js +0 -101
  99. package/src/server.js +0 -1466
  100. package/src/setup-guidance.js +0 -128
  101. package/src/state.js +0 -732
  102. package/src/view-identity.js +0 -99
  103. /package/{src → lib}/document-trust.js +0 -0
package/README.md CHANGED
@@ -1,260 +1,96 @@
1
1
  # Doc Review
2
2
 
3
- Review HTML, Markdown, and localhost pages, edit when you choose, leave contextual comments, and send all feedback to your AI agent at once.
3
+ **Review your coding agent's work in the browser, not in a wall of chat.**
4
4
 
5
- [Read the original Human Review launch post](https://creatoreconomy.so/p/use-my-human-review-skill-to-edit-html-markdown-visually)
5
+ Open an HTML file, a Markdown document, or a localhost page. Point to what needs
6
+ changing, edit the small things yourself, and send your feedback to the agent
7
+ in one batch.
6
8
 
7
- https://github.com/user-attachments/assets/7cab09c9-eaa0-4e8b-984d-2925e810b5c2
9
+ ![The Field Notes landing page in Review, with highlighted copy and an anchored comment asking for a concrete benefit](https://raw.githubusercontent.com/erdemtuna/doc-review/main/assets/doc-review.png)
8
10
 
9
- ## Problem
11
+ *Feedback stays beside the work. Your agent gets the comments, edits, and overall note together.*
10
12
 
11
- Giving AI feedback on files in chat is painful.
13
+ ## Get started
12
14
 
13
- Sometimes you want to change one sentence yourself. Instead, you end up typing:
14
-
15
- > In the third paragraph, change X to Y. Cut the third card because it repeats the first one. Also rewrite the CTA.
16
-
17
- Then the agent changes the file and you have to check whether it understood every instruction. This gets even harder when you’re reviewing a long plan, Markdown document, landing page, or multi-page website.
18
-
19
- ## How to install /doc-review
20
-
21
- The easiest way to install the skill is to paste this into ChatGPT, Claude Code, Codex, or your favorite coding agent:
22
-
23
- ```text
24
- Install the /doc-review skill globally from https://github.com/erdemtuna/doc-review
25
- ```
26
-
27
- You can also install it with `npx`:
15
+ Requires **Node.js 24.21.0 or newer** and a coding agent that can run shell commands.
16
+ Install the skill once:
28
17
 
29
18
  ```sh
30
19
  npx -y @erdemtuna/doc-review setup --global
31
20
  ```
32
21
 
33
- This fork uses the `doc-review` command, `/doc-review` skill, and
34
- `~/.doc-review` state directory. It does not install a `human-review` alias or
35
- migrate upstream state and skill files; remove those old files manually if you
36
- no longer need them.
37
-
38
- ## How to use /doc-review
39
-
40
- Review is opt-in: explicitly invoke `/doc-review` or ask to open an interactive
41
- browser review. Writing, updating, or asking for a general review of content does
42
- not automatically open the browser or start polling. An explicitly started review
43
- continues until you end it or switch tasks.
44
-
45
- After updating installed skill instructions, start a fresh agent session or reload
46
- skills (in Copilot CLI, `/skills reload`). Setup copies the executing package's
47
- skill template; rerunning setup from an older package can restore older behavior.
48
- Setup updates its own `AGENTS.md` guidance inside `<!-- BEGIN doc-review -->` and
49
- `<!-- END doc-review -->` markers. Exact, known older generated blocks are migrated.
50
- Custom guidance is preserved with a migration message rather than overwritten;
51
- update it yourself if it requests automatic review. Invalid or duplicate markers
52
- must be corrected before setup can proceed.
53
-
54
- ![Doc Review visual editor](assets/doc-review.png)
55
-
56
- Open an HTML or Markdown file:
22
+ Start a fresh agent session, then ask it to open a review:
57
23
 
58
24
  ```text
59
- /doc-review (your file)
25
+ /doc-review path/to/landing-page.html
60
26
  ```
61
27
 
62
- Review a page running on localhost:
28
+ The same command works with Markdown or a running local app:
63
29
 
64
30
  ```text
65
- /doc-review (localhost URL)
31
+ /doc-review path/to/plan.md
32
+ /doc-review http://localhost:3000
66
33
  ```
67
34
 
68
- Doc Review opens in **Review**, with **View** selected, so you can read, use page controls, and comment. Switch to **Edit** to change content directly. Plain HTML saves edits to the file; scripted HTML, Markdown, and localhost pages send edits to your agent. Commenting stays available in both modes.
69
-
70
- Select text, or hover or focus an element, then use the nearby comment icon. `Ctrl+Alt+M` (`Cmd+Option+M` on macOS) opens a comment for the current selection. Press Enter to submit or Shift+Enter for a new line. On desktop the composer stays beside its target, or pins to the effective top or bottom scrolling edge when that target leaves view. **Back to selection** reveals the target without changing the draft. The full-width sheet is used only at the narrow responsive breakpoint.
71
-
72
- **Comments** is the single toolbar entry point for the closed-by-default review drawer. Its feedback inventory scrolls independently while the overall note and **Send to agent** controls remain fixed. Submitting a comment creates its normal highlight and increments the count, but keeps the card closed until you activate its mark or choose **Jump to**. Focus returns to the exact element or selection you reviewed.
73
-
74
- Aligned cards show **Edit**, **Close**, and **More**; drawer cards show **Jump to**, **Edit**, and **More**. Delete lives only in **More** and requires an inline confirmation. Quote hints preserve both the beginning and ending of long selections. Editing has explicit **Save** and **Cancel** controls: Enter saves, Shift+Enter adds a line, Escape cancels, and moving focus never autosaves. A draft follows its comment between aligned and drawer cards and keeps its caret through target movement.
75
-
76
- ### Files and page interactions
77
-
78
- Self-contained HTML runs its inline scripts and event handlers automatically.
79
- There is no approval switch or renewed approval after an agent updates the file.
80
- Plain HTML, including ordinary JSON data blocks and escaped code examples, keeps
81
- direct autosave and Revert. Scripted HTML is **feedback-only**: your edits are sent
82
- to the agent to apply to the original source, rather than writing script-generated
83
- DOM into the file. The renderer re-evaluates that distinction when the source changes.
84
- Markdown and localhost also remain feedback-only.
85
-
86
- If a scripted page is broken, **More > Reload without scripts** provides a
87
- temporary recovery mode. **Use page interactions** restores automatic behavior.
88
- That preference belongs to this page in this review session, not to a saved
89
- approval record. Disabling scripts does not make a scripted file's preview
90
- edits writable. A changed preference takes effect when the frame is replaced;
91
- reload handling preserves comment drafts and reports genuine source conflicts.
92
-
93
- The supported interactive-file mode is for self-contained documents. Separate
94
- script files, application imports, workers, and embedded applications are not
95
- made compatible by this change. Use the existing localhost review route for
96
- application workflows. Dependency limitations appear in contextual details,
97
- not as a permission task or text inserted into the authored document.
98
-
99
- Files retain an opaque iframe origin. The parent owns API authentication and
100
- checks the frame identity for messages and writes. Authored code shares the
101
- document with the SDK, so frame correlation is not proof of human authorship.
102
- Relative assets stay scoped to the reviewed file. Localhost reviews retain their
103
- existing nonopaque artifact origin, popup, and download behavior; they are fetched
104
- and rewritten by the review server, not guaranteed to reproduce the application's
105
- original cookies, storage, or origin-sensitive requests.
106
-
107
- **Compatibility:** the old per-version `/trust` API is retired and returns 410.
108
- Existing decision files are left unused. The CLI does not reuse background
109
- servers with the previous protocol. Writable HTML save and revert requests must
110
- identify their current served frame and source hash so delayed edits cannot
111
- overwrite a newer file.
112
-
113
- Agent polling returns an immutable `batch_id`. After applying the batch, the
114
- agent acknowledges that exact receipt and keeps waiting:
115
-
116
- ```sh
117
- npx -y @erdemtuna/doc-review poll path/to/file.html --ack b_0123456789abcdef --timeout 600
118
- ```
119
-
120
- A stale or repeated batch ID is harmless: it never clears newer feedback. The
121
- complete acknowledgement command is included in each response's `next_step`.
122
-
123
- ### Review and Changes
124
-
125
- **Review** keeps the document interactive. **Changes** shows a selected review
126
- round, with round selection and comparison navigation instead of live-page controls.
127
- The default comparison is the content captured when feedback was sent against
128
- the result captured after the agent acknowledged that exact batch. Previous
129
- rounds retain their own fixed endpoints rather than changing on every reload.
130
-
131
- Content comparisons use a continuous aligned before/after reading surface with
132
- expandable unchanged context. Source comparisons use line-numbered source hunks.
133
- At narrow widths, a unified view retains each change's before/after attribution.
134
- Content comparisons cover text, structure, formatting, links, and image
135
- references. File-backed reviews also retain source for a source comparison.
136
- Deleted passages remain readable even when there is no current element to jump
137
- to, and uncertain matches do not pretend to identify an exact current target.
138
- These are changes observed during a round, not proof of agent authorship or that
139
- every comment was resolved.
140
-
141
- History works in the normal browser for HTML, Markdown, and localhost pages.
142
- It does not use screenshots, a browser extension, or automatic Git commits.
143
- Live history captures the reviewed page's available content, not every possible
144
- application state. Changes to image bytes at an unchanged URL, canvas pixels,
145
- external stylesheet appearance, and inaccessible embedded content are outside
146
- the comparison guarantee.
147
-
148
- Send waits for edit persistence and writable HTML saves, then makes a short
149
- best-effort baseline capture. Missing comparison data does not require a second
150
- decision or prevent feedback from being sent. Available snapshots are retained;
151
- inactive pages can have incomplete Content coverage. A last-visited live page is
152
- never described as a fresh Send-time snapshot. Actual save or delivery failures
153
- are reported without discarding your feedback.
154
-
155
- When an active tab has reliable authored tab/panel identifiers, Content capture
156
- records that visible view. A result on a different tab remains pending and asks
157
- you to return to the original tab. Returning retries capture without clicking
158
- through other tabs automatically. Unknown views and older snapshots are labelled
159
- as visible-content comparisons whose matching view is unverified. This does not
160
- capture all hidden panels or every application state. File Source comparison
161
- still covers the whole file independently.
162
-
163
- After acknowledgement, file-source results are captured independently of the
164
- browser. Content results are captured when the appropriate reviewed page is
165
- ready. These observations can have different timestamps; neither timestamp
166
- claims that the document was captured at the instant of acknowledgement.
167
- Background capture does not disable document interaction and retries transient
168
- failures a bounded number of times. An unchanged file does not need a new reload
169
- solely to capture an acknowledged result. Available Source remains readable
170
- while Content is pending. Contextual retry or return-to-view actions handle
171
- remaining failures; a continuously changing page may still have no stable Content
172
- snapshot. Permanent finalization is optional in capture details, not a prerequisite
173
- for reading the comparison. Failed or limited captures are not reported as zero
174
- changes, and handled feedback remains archived even when history is unavailable.
175
-
176
- Snapshots stay in the local Doc Review state directory. The latest five
177
- completed rounds are retained automatically, while active rounds, pending
178
- captures, and unsent feedback protect their referenced revisions. History starts
179
- with this capability; earlier documents cannot be reconstructed from old
180
- feedback receipts. Review snapshots can contain document content, so treat the
181
- state directory as private.
182
- Unreferenced snapshot files are collected at startup and during periodic
183
- maintenance, with a one-hour grace period protecting interrupted publications.
184
- Capture and comparison limits fail explicitly rather than publishing truncated
185
- content as a complete comparison.
35
+ Reviews start only when you request them. Your agent uses the installed skill
36
+ to open the page, wait for feedback, and apply the changes.
186
37
 
187
- Default capture limits are 8 MiB of file source and 4 MiB of normalized content,
188
- with at most 2,000 semantic blocks and 1,000,000 text characters. A single block
189
- is limited to 100,000 characters. Comparisons have independent size, token, and
190
- edit-work limits, including a 1,000,000-character budget and a 250 ms processing
191
- budget. A stored snapshot can therefore exceed comparison limits; the UI reports
192
- that limitation rather than claiming there were no changes.
38
+ ## From feedback to the next version
193
39
 
194
- ### Feedback reliability and limits
40
+ 1. **Open and explore.** Reviews start in **View**, so you can read and use page
41
+ controls without accidentally editing.
42
+ 2. **Point out what matters.** Select text or choose an element to leave a
43
+ comment. Switch to **Edit** for direct changes to wording, formatting, images,
44
+ or layout. Commenting works in either mode.
45
+ 3. **Send one batch.** Open **Feedback**, inspect Comments and Edits, add an overall note if needed, and
46
+ choose **Send to agent**. No need to describe where every sentence lives.
47
+ 4. **Check the result.** Use **Changes** to compare a review round's captured
48
+ before and after content, then continue reviewing. Comparisons show observed
49
+ changes, not a guarantee that every request was resolved.
195
50
 
196
- `poll` without `--timeout` waits for at most 12 hours. Explicit timeouts include
197
- server discovery, reconnection, and retry backoff, not just time spent connected.
198
- Recoverable connection drops are retried; invalid responses, authorization errors,
199
- and incompatible servers fail visibly. A timeout does not discard feedback: use
200
- `status` to check it, then start another poll if the review is still wanted.
51
+ You can review a plan, refine a landing page, or walk through a local app without
52
+ moving your feedback into a separate document.
201
53
 
202
- Each edit text or HTML field is limited to 200,000 Unicode code points. Oversized
203
- edits carry `truncated: true` and `truncated_fields` naming incomplete fields.
204
- The agent must not use partial text or HTML as a complete replacement or invent
205
- the rest. It must obtain the full edit from an authoritative source or ask you
206
- for it before acknowledging the batch.
54
+ ![The Feedback panel with separate Comments and Edits sections, an overall note, and End review beside Send to agent](https://raw.githubusercontent.com/erdemtuna/doc-review/main/assets/doc-review-feedback.png)
207
55
 
208
- External writes to an HTML or Markdown source refresh its rendered baseline without
209
- clearing unsent feedback. That preserves your edits; it does not automatically
210
- resolve conflicts with the changed source. HTML autosaves keep their existing
211
- behavior.
56
+ *One batch, with the context attached: comments, your edits, and the overall direction.*
212
57
 
213
- ### Upgrading from servers using protocol 14 or earlier
58
+ ![The completed Field Notes review round in Changes, comparing the revised description and call to action with their originals](https://raw.githubusercontent.com/erdemtuna/doc-review/main/assets/doc-review-changes.png)
214
59
 
215
- The updated CLI requires server protocol 15. An older server that is still
216
- running is not silently reused or forcibly replaced. End active reviews and
217
- shut down that specific old server (or let it exit when idle) before restarting
218
- Doc Review. Keep the `.doc-review` state directory: pending batches and their
219
- exact receipt IDs survive a controlled restart.
60
+ *Check the result beside the original. Move between changes or switch to Source for the saved file text.*
220
61
 
221
- After installing the updated package, rerun `doc-review setup --global` for
222
- personal skills and `doc-review setup` in projects with generated guidance.
223
- Reload skills or start a fresh agent session afterward. A newer CLI paired with
224
- older instructions is not a complete upgrade.
62
+ ## What happens to your edits?
225
63
 
226
- ## What this skill lets you do
64
+ | What you open | Where edits go |
65
+ | --- | --- |
66
+ | Plain HTML | Saved directly to the file, with Revert available |
67
+ | Scripted HTML or Markdown | Sent to the agent to apply to the original source |
68
+ | Localhost page | Sent to the agent to update the app's source |
227
69
 
228
- - **Edit text directly and tweak basic formatting** (e.g., bold, italic).
229
- - **Make bulleted and numbered lists** — type `- ` or `1. ` at the start of a line, or press ⌘⇧8 / ⌘⇧7. Tab and Shift+Tab indent and outdent.
230
- - **Add links** — select text and press ⌘K. ⌘K inside an existing link edits or removes it.
231
- - **Resize images** by dragging their corner, and **move images** by dragging them to a new spot.
232
- - **Rearrange the page** — hover any block and drag the handle on its left edge to move the whole block somewhere else.
233
- - **Paste images** from your clipboard — file reviews save them beside the document; localhost reviews stage them for the agent to place in the app source.
234
- - **Select a phrase and leave a comment** from the contextual icon, or press Ctrl+Alt+M / Cmd+Option+M.
235
- - **Comment on an image, chart, control, or section** from its hover or keyboard-focus affordance without taking over its normal click.
236
- - **Remove elements** without explaining the deletion in chat.
237
- - **Command-click links** to review multiple pages without losing your feedback.
238
- - **Send every edit and comment at once** instead of writing a long chat message.
70
+ Self contained HTML can run inline scripts. For an app with separate script
71
+ dependencies, use its localhost URL instead.
239
72
 
240
- Doc Review works well for editing AI-generated plans, updating landing pages, reviewing localhost apps, and removing extra copy from a UX.
73
+ Doc Review runs locally and needs no Doc Review account, hosted backend, or API
74
+ key. The page you review and the coding agent you use may still contact external
75
+ services.
241
76
 
242
- ## What’s inside
77
+ ## Learn more
243
78
 
244
- - [`cli.js`](src/cli.js) contains the `doc-review`, `poll`, `status`, and `setup` commands.
245
- - [`server.js`](src/server.js) runs the local review session.
246
- - [`sdk.js`](src/sdk.js) handles editing, comments, highlights, and feedback.
247
- - [`chrome-client.js`](src/chrome-client.js) contains the visual review interface.
248
- - [`markdown.js`](src/markdown.js) renders Markdown files for review.
249
- - [`SKILL.md`](src/SKILL.md) teaches Claude Code, Codex, and other agents how to use Doc Review.
79
+ [Usage guide](https://github.com/erdemtuna/doc-review/blob/main/docs/usage.md):
80
+ setup options, comments, comparisons, limitations, and upgrades.
250
81
 
251
- Everything runs on your computer. Doc Review doesn’t require an account, cloud service, database, or API key.
252
- Comment geometry remains local, transient presentation data and is never written to review state or sent to the agent. After the agent acknowledges the exact delivered `batch_id`, the comments carried by that batch leave the active feedback inventory; their round archive remains, and newer comments and corrections stay active.
82
+ [Development](https://github.com/erdemtuna/doc-review/blob/main/docs/development.md):
83
+ build, test, architecture, and package checks.
253
84
 
254
- ## Upstream project
85
+ [Prepared review example](docs/migration-review.md):
86
+ disposable HTML/Markdown sessions with saved comparison rounds and a shell review checklist.
255
87
 
256
- Doc Review is an independent fork of [Human Review](https://github.com/petergyang/human-review), originally created by Peter Yang. The fork preserves the upstream MIT license and continues from the upstream `v0.6.1` release.
88
+ [Releasing](https://github.com/erdemtuna/doc-review/blob/main/RELEASING.md):
89
+ the maintainers' release process.
257
90
 
258
- ## License
91
+ ## Credits and license
259
92
 
260
- MIT
93
+ Doc Review is an independent fork of Peter Yang's
94
+ [Human Review](https://github.com/petergyang/human-review), continuing from
95
+ upstream v0.6.1. Licensed under
96
+ [MIT](https://github.com/erdemtuna/doc-review/blob/main/LICENSE).
@@ -41,8 +41,11 @@ comment and Shift+Enter adds a new line. On desktop the composer stays attached
41
41
  to its target or pins to the effective top or bottom clipping edge; Back to
42
42
  selection reveals an offscreen target without changing the draft.
43
43
 
44
- Comments is the single toolbar entry point. The drawer inventory scrolls
45
- independently while the overall note and Send to agent controls remain fixed.
44
+ Feedback is the single toolbar entry point for Comments, Edits, and the overall
45
+ note. Comments and Edits collapse independently; their choices last until the
46
+ tab reloads. An active comment edit keeps Comments expanded until Save or Cancel.
47
+ The inventory scrolls independently while the overall note and bottom actions
48
+ remain reachable. End review is on the left and Send to agent on the right.
46
49
  Submitting creates the normal target mark and count but leaves the card closed
47
50
  until the user explicitly activates the mark or chooses Jump to. Focus returns
48
51
  to the reviewed element or selection. Aligned cards expose Edit, Close, and
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Quote-with-context anchoring, in the spirit of the W3C TextQuoteSelector.
3
+ *
4
+ * These functions are pure string maths so they can be unit-tested in Node and
5
+ * shipped verbatim to the browser, where the DOM half of anchoring lives.
6
+ */
7
+ export const CONTEXT_PAD = 32;
8
+ /** Capture `quote` plus surrounding context so it can be re-found after edits. */
9
+ export function buildContext(text, start, end, pad = CONTEXT_PAD) {
10
+ return {
11
+ prefix: text.slice(Math.max(0, start - pad), start),
12
+ quote: text.slice(start, end),
13
+ suffix: text.slice(end, Math.min(text.length, end + pad)),
14
+ };
15
+ }
16
+ function commonSuffixLength(a, b) {
17
+ let n = 0;
18
+ while (n < a.length && n < b.length && a[a.length - 1 - n] === b[b.length - 1 - n])
19
+ n += 1;
20
+ return n;
21
+ }
22
+ function commonPrefixLength(a, b) {
23
+ let n = 0;
24
+ while (n < a.length && n < b.length && a[n] === b[n])
25
+ n += 1;
26
+ return n;
27
+ }
28
+ function occurrences(text, quote) {
29
+ const found = [];
30
+ let at = text.indexOf(quote);
31
+ while (at !== -1) {
32
+ found.push(at);
33
+ at = text.indexOf(quote, at + 1);
34
+ }
35
+ return found;
36
+ }
37
+ function bestHit(text, hits, quote, prefix, suffix) {
38
+ let best = hits[0];
39
+ let bestScore = -1;
40
+ for (const at of hits) {
41
+ const before = text.slice(Math.max(0, at - prefix.length), at);
42
+ const after = text.slice(at + quote.length, at + quote.length + suffix.length);
43
+ const score = commonSuffixLength(prefix, before) + commonPrefixLength(suffix, after);
44
+ if (score > bestScore) {
45
+ bestScore = score;
46
+ best = at;
47
+ }
48
+ }
49
+ return { at: best, score: bestScore };
50
+ }
51
+ const collapse = (s) => String(s || "").replace(/\s+/g, " ");
52
+ /** Collapse whitespace runs, keeping a map from each kept char to its original index. */
53
+ function collapseWithMap(text) {
54
+ let flat = "";
55
+ const map = [];
56
+ let pendingWs = -1;
57
+ for (let i = 0; i < text.length; i += 1) {
58
+ if (/\s/.test(text[i])) {
59
+ if (pendingWs === -1)
60
+ pendingWs = i;
61
+ continue;
62
+ }
63
+ if (pendingWs !== -1 && flat) {
64
+ flat += " ";
65
+ map.push(pendingWs);
66
+ }
67
+ pendingWs = -1;
68
+ flat += text[i];
69
+ map.push(i);
70
+ }
71
+ return { flat, map };
72
+ }
73
+ /**
74
+ * Locate `ctx.quote` in `text`, using prefix/suffix to disambiguate repeats.
75
+ * Returns `{ start, end, exact }` or null when the quote is gone entirely.
76
+ */
77
+ export function findQuote(text, ctx) {
78
+ const quote = ctx && ctx.quote;
79
+ if (!quote)
80
+ return null;
81
+ const hits = occurrences(text, quote);
82
+ if (hits.length === 1) {
83
+ return { start: hits[0], end: hits[0] + quote.length, exact: true };
84
+ }
85
+ if (hits.length > 1) {
86
+ const { at, score } = bestHit(text, hits, quote, ctx.prefix || "", ctx.suffix || "");
87
+ return { start: at, end: at + quote.length, exact: score > 0 };
88
+ }
89
+ // Reformatting (a prettier run, an agent rewrite) reflows whitespace without
90
+ // changing any words. Match again on whitespace-collapsed text and map the
91
+ // hit back to real offsets, so those comments survive instead of orphaning.
92
+ const { flat, map } = collapseWithMap(text);
93
+ const flatQuote = collapse(quote).trim();
94
+ if (!flatQuote || !map.length)
95
+ return null;
96
+ const flatHits = occurrences(flat, flatQuote);
97
+ if (!flatHits.length)
98
+ return null;
99
+ const { at } = bestHit(flat, flatHits, flatQuote, collapse(ctx.prefix || ""), collapse(ctx.suffix || ""));
100
+ return { start: map[at], end: map[at + flatQuote.length - 1] + 1, exact: false };
101
+ }
102
+ /** Collapse runs of whitespace for display in a comment card. */
103
+ export function tidy(text, limit = 0) {
104
+ const flat = String(text == null ? "" : text)
105
+ .replace(/\s+/g, " ")
106
+ .trim();
107
+ if (!limit || flat.length <= limit)
108
+ return flat;
109
+ return `${flat.slice(0, limit - 1).trimEnd()}…`;
110
+ }
111
+ function graphemes(text) {
112
+ if (typeof Intl?.Segmenter === "function") {
113
+ const segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
114
+ return [...segmenter.segment(text)].map(({ segment }) => segment);
115
+ }
116
+ // Array.from is deterministic and, unlike string slicing, never splits a
117
+ // surrogate pair. Older engines may split a multi-code-point grapheme.
118
+ return Array.from(text);
119
+ }
120
+ function nearbyHeadBoundary(parts, ideal) {
121
+ for (let index = ideal; index >= Math.max(1, ideal - 8); index -= 1) {
122
+ if (/^\s$/u.test(parts[index - 1]))
123
+ return index - 1;
124
+ }
125
+ return ideal;
126
+ }
127
+ function nearbyTailBoundary(parts, ideal) {
128
+ for (let index = ideal; index < Math.min(parts.length - 1, ideal + 8); index += 1) {
129
+ if (/^\s$/u.test(parts[index]))
130
+ return index + 1;
131
+ }
132
+ return ideal;
133
+ }
134
+ /**
135
+ * Collapse whitespace and preserve both ends of a long quote. `limit` counts
136
+ * grapheme clusters, including the ellipsis.
137
+ */
138
+ export function tidyMiddle(text, limit = 0) {
139
+ const flat = String(text == null ? "" : text)
140
+ .replace(/\s+/g, " ")
141
+ .trim();
142
+ if (!limit)
143
+ return flat;
144
+ const parts = graphemes(flat);
145
+ if (parts.length <= limit)
146
+ return flat;
147
+ if (limit <= 1)
148
+ return "…";
149
+ const available = limit - 1;
150
+ const idealHead = Math.ceil(available * 0.6);
151
+ const idealTailStart = parts.length - (available - idealHead);
152
+ const headEnd = nearbyHeadBoundary(parts, idealHead);
153
+ const tailStart = nearbyTailBoundary(parts, idealTailStart);
154
+ return `${parts.slice(0, headEnd).join("").trimEnd()}…${parts.slice(tailStart).join("").trimStart()}`;
155
+ }
@@ -0,0 +1,54 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ const RETRY_DELAYS_MS = [10, 20, 40, 80, 160];
4
+ const TRANSIENT_RENAME_ERRORS = new Set(["EPERM", "EACCES", "EBUSY"]);
5
+ const waitState = new Int32Array(new SharedArrayBuffer(4));
6
+ // Keep Store transactions synchronous; rename backoff blocks for at most 310ms.
7
+ const sleepSync = (ms) => Atomics.wait(waitState, 0, 0, ms);
8
+ export function createAtomicWriter({ fileSystem = fs, sleep = sleepSync, report = console.error } = {}) {
9
+ return function atomicWrite(file, data) {
10
+ const tmp = `${file}.${process.pid}.${crypto.randomBytes(6).toString("hex")}.doc-review.tmp`;
11
+ let fd;
12
+ let created = false;
13
+ try {
14
+ fd = fileSystem.openSync(tmp, "wx");
15
+ created = true;
16
+ fileSystem.writeFileSync(fd, data);
17
+ fileSystem.closeSync(fd);
18
+ fd = undefined;
19
+ for (let attempt = 0;; attempt += 1) {
20
+ try {
21
+ fileSystem.renameSync(tmp, file);
22
+ return;
23
+ }
24
+ catch (err) {
25
+ if (!TRANSIENT_RENAME_ERRORS.has(err.code) || attempt === RETRY_DELAYS_MS.length)
26
+ throw err;
27
+ sleep(RETRY_DELAYS_MS[attempt]);
28
+ }
29
+ }
30
+ }
31
+ catch (err) {
32
+ if (fd !== undefined) {
33
+ try {
34
+ fileSystem.closeSync(fd);
35
+ }
36
+ catch (cleanupError) {
37
+ report(`Could not close atomic-write temporary file ${tmp}: ${cleanupError.message}`);
38
+ }
39
+ }
40
+ if (created) {
41
+ try {
42
+ fileSystem.unlinkSync(tmp);
43
+ }
44
+ catch (cleanupError) {
45
+ if (cleanupError.code !== "ENOENT") {
46
+ report(`Could not remove atomic-write temporary file ${tmp}: ${cleanupError.message}`);
47
+ }
48
+ }
49
+ }
50
+ throw err;
51
+ }
52
+ };
53
+ }
54
+ export const atomicWrite = createAtomicWriter();