@erdemtuna/doc-review 0.12.0 → 0.13.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.
- package/README.md +23 -25
- package/lib/SKILL.md +65 -202
- package/lib/agent-handoff.js +24 -0
- package/lib/agent-output.js +287 -0
- package/lib/anchor-text.js +31 -19
- package/lib/chrome-api.js +23 -4
- package/lib/chrome.html +0 -25
- package/lib/cli.js +282 -104
- package/lib/comment-target.js +4 -0
- package/lib/contracts/agent.js +122 -0
- package/lib/contracts/feedback.js +369 -1
- package/lib/contracts/frame.js +81 -0
- package/lib/contracts/history.js +189 -1
- package/lib/contracts/index.js +7 -2
- package/lib/contracts/page-boundary.js +187 -0
- package/lib/contracts/validation.js +200 -0
- package/lib/conversation-anchor-controller.js +84 -0
- package/lib/conversation-capture.js +81 -0
- package/lib/conversation-controller.js +798 -0
- package/lib/conversation-save.js +175 -0
- package/lib/conversation-server.js +184 -0
- package/lib/conversation-shell.js +1013 -0
- package/lib/conversation-store.js +809 -0
- package/lib/frame-controller.js +8 -8
- package/lib/frame-policy.js +1 -0
- package/lib/history-policy.js +4 -15
- package/lib/history-server.js +42 -326
- package/lib/html-transform.js +19 -4
- package/lib/icons.js +323 -0
- package/lib/new-message-target.js +27 -0
- package/lib/paths.js +2 -2
- package/lib/poll-transport.js +109 -26
- package/lib/positioning.js +51 -0
- package/lib/references/context-and-recovery.md +130 -0
- package/lib/references/response-contract.md +108 -0
- package/lib/references/source-edits.md +47 -0
- package/lib/revision-store.js +1 -1
- package/lib/save-controller.js +28 -14
- package/lib/sdk.js +357 -35
- package/lib/server.js +73 -595
- package/lib/setup.js +17 -48
- package/lib/state.js +21 -7
- package/lib/thread-anchor-controller.js +56 -0
- package/lib/toolbar-controller.js +3 -3
- package/lib/ui/THIRD_PARTY_NOTICES.md +127 -8
- package/lib/ui/chrome.css +1079 -2782
- package/lib/ui/chrome.js +81 -17
- package/package.json +2 -2
- package/lib/chrome-client.js +0 -1706
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Bounded reads and recovery
|
|
2
|
+
|
|
3
|
+
One agent interface limits final JSON stdout to 16 KiB UTF-8 including escaping
|
|
4
|
+
and newline. No compact/full or raw fallback exists. Durable evidence is not
|
|
5
|
+
truncated for delivery. Large values carry exact scoped references with
|
|
6
|
+
version/field identity, encoding, byte length and SHA-256. Previews are explicitly
|
|
7
|
+
incomplete. Original edit capture truncation is a separate fact.
|
|
8
|
+
|
|
9
|
+
Inventory/history/context/status blocker pages report `totalCount`,
|
|
10
|
+
`returnedCount`, `complete` and `nextCursor`. Continue the same command with
|
|
11
|
+
`--cursor <opaque-token>` until complete where full coverage is necessary.
|
|
12
|
+
Never assume the first page is the complete response obligation. The generated
|
|
13
|
+
template covers all items even when the inventory is paged.
|
|
14
|
+
|
|
15
|
+
## Deliberate historical context
|
|
16
|
+
|
|
17
|
+
For a follow-up, start with the closest relevant earlier exchange using the
|
|
18
|
+
message item's `contextCommand` (default generated `--limit 1`):
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
npx -y @erdemtuna/doc-review context --review <reviewId> --entry <entryKey> --submission <currentSubmissionId> --thread <threadId> --limit 1
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The producer reads immutable submissions, excludes current and later work and
|
|
25
|
+
saved-unsent replies before pagination, and provides historical lifecycle
|
|
26
|
+
evidence. Browser context intentionally still shows pending replies.
|
|
27
|
+
Do not load all threads' history speculatively.
|
|
28
|
+
|
|
29
|
+
For earlier overall notes, results, edits or lost chat context, use the
|
|
30
|
+
submission's `handoff.historyCommand`:
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
npx -y @erdemtuna/doc-review history --review <reviewId> --entry <entryKey> --before <currentSubmissionId> --limit 1
|
|
34
|
+
npx -y @erdemtuna/doc-review submission --review <reviewId> --entry <entryKey> --submission <priorSubmissionId>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
History retains note intent, result notes and handled/abandoned evidence.
|
|
38
|
+
Submission reads include exact paged messages/edits, inline responses, edit
|
|
39
|
+
outcomes and receipt. Ended reviews and terminal submissions remain readable.
|
|
40
|
+
Earlier request-change intent is historical evidence, never new permission.
|
|
41
|
+
|
|
42
|
+
## Exact content
|
|
43
|
+
|
|
44
|
+
Copy the reference's generated command. `field` is a scoped artifact path, not
|
|
45
|
+
a filesystem path. Never transplant a cursor into another scope/field/version.
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
npx -y @erdemtuna/doc-review content --review <reviewId> --entry <entryKey> --submission <submissionId> --field submission/edits/0/content/after_html
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
A content read gives exact `text`, encoding (`utf8` or serialized `json`),
|
|
52
|
+
UTF-8 byte `offset`, `returnedBytes`, complete-field byte length/hash,
|
|
53
|
+
`complete`, and `nextCursor`. Concatenate chunks in order until complete;
|
|
54
|
+
Unicode characters and line endings are preserved. Parse JSON only after
|
|
55
|
+
reconstruction. A continuation identity/content change is an error, not a splice.
|
|
56
|
+
|
|
57
|
+
For a complete field, append `--output-file <new-path>`. Omit `--field` to export
|
|
58
|
+
the whole submitted artifact (submission, result, receipt and canonical pages).
|
|
59
|
+
The receipt gives path/identity/encoding/bytes/hash. Writing is atomic/exclusive;
|
|
60
|
+
existing files and reviewed source are never overwritten. Read exported content
|
|
61
|
+
before using it. A file receipt alone does not prove inspection.
|
|
62
|
+
|
|
63
|
+
## Identity, retries and End
|
|
64
|
+
|
|
65
|
+
Keep reviewId, entryKey, submissionId, delivered version, response file and stable
|
|
66
|
+
requestId across compaction. `status` reads lifecycle evidence without starting a
|
|
67
|
+
server or mutating offline state. It deliberately provides only result previews/
|
|
68
|
+
references; explicit content/history reads return requested prose when it fits.
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
npx -y @erdemtuna/doc-review status --review <reviewId> --entry <entryKey>
|
|
72
|
+
npx -y @erdemtuna/doc-review receipt --review <reviewId> --entry <entryKey> --request-id <requestId>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Status reports queued/delivered work, pending counts and predecessor/overlapping
|
|
76
|
+
blockers; `source:"disk"` means validated offline evidence, not agent liveness.
|
|
77
|
+
Corrupt/unsupported state fails explicitly, without legacy fallback.
|
|
78
|
+
|
|
79
|
+
Semantic rejection (exit 1), uncertain acceptance (exit 2), and accepted receipt
|
|
80
|
+
are different. `not-found` from receipt is not proof of rejection. Retry an
|
|
81
|
+
uncertain response with exactly the same file/requestId; do not regenerate the
|
|
82
|
+
template or repeat source edits. Inspect source and receipts before considering
|
|
83
|
+
any further filesystem work. One cooperating handler is assumed; no worker
|
|
84
|
+
leases or filesystem exactly-once guarantees exist.
|
|
85
|
+
|
|
86
|
+
End freezes reviewer content, not accepted work. Complete non-abandoned work
|
|
87
|
+
after End/restart; keep polling that review until `ended`. Saved-unsent content
|
|
88
|
+
does not transfer to a new review. Abandonment releases work exclusion but does
|
|
89
|
+
not cancel the external handler or undo source writes. `SUBMISSION_ABANDONED`
|
|
90
|
+
means stop handling, never retry as a new completion.
|
|
91
|
+
|
|
92
|
+
## Reviewer interaction
|
|
93
|
+
|
|
94
|
+
The page starts in View. Feedback is nonmodal: it docks on roomy desktops and
|
|
95
|
+
floats on narrow PC windows, without dimming or disabling the document. Show in document
|
|
96
|
+
reveals the exact passage, hiding a floating panel only when it would obscure the
|
|
97
|
+
target. Focus and secondary thread actions are in the conversation menu.
|
|
98
|
+
Resolve/Reopen sits opposite Reply below an expanded conversation and remains in
|
|
99
|
+
Conversation actions when collapsed or a local draft is open. Resolve refuses unfinished drafts,
|
|
100
|
+
unsent messages and outstanding agent work. Resolve/Reopen do not show notifications.
|
|
101
|
+
Review-state transitions, especially receipt of an agent response, appear for five
|
|
102
|
+
seconds at the top left below the toolbar. Hover, notification focus or a hidden
|
|
103
|
+
tab pauses expiry; reading the document iframe does not. Focus includes a visible
|
|
104
|
+
Back to Feedback action; Open/Resolved are separate independent filters.
|
|
105
|
+
Sidebar headers end with Locate, More and Collapse; adjacent headers use Open in
|
|
106
|
+
Feedback (a right-pointing arrow), More and Close. Edit belongs beside each eligible
|
|
107
|
+
message's timestamp. The far-right delivery icon changes from Not sent to Sent,
|
|
108
|
+
then Received when the agent picks up the message or replies.
|
|
109
|
+
Resolved conversations automatically collapse and show a subdued check-circle
|
|
110
|
+
**Resolved** informational icon with a keyboard-visible explanation; they remain expandable
|
|
111
|
+
for reading. Reopen expands them again. Resolution from another tab never
|
|
112
|
+
hides a local draft.
|
|
113
|
+
|
|
114
|
+
Add comment / Add reply / Update comment queue a message;
|
|
115
|
+
**Send to agent (N)** dispatches a deliberate batch.
|
|
116
|
+
Send includes all saved pending messages and edits. **Your edits** contains exact
|
|
117
|
+
human edit evidence. **Note to agent** opens separately with its own unchecked
|
|
118
|
+
change permission. Unsaved comment drafts are not sent.
|
|
119
|
+
Send freezes the selected versions and note at activation; feedback arriving
|
|
120
|
+
during preparation stays pending. A changed selected version requires reviewing
|
|
121
|
+
the selection again.
|
|
122
|
+
|
|
123
|
+
Expanded conversations retain two recent sent exchanges and all unsent replies.
|
|
124
|
+
The preceding answer remains visible while composing a follow-up; **Show earlier
|
|
125
|
+
replies** loads older context. Closing Feedback or changing hosts preserves the
|
|
126
|
+
mounted editor and its local draft, but reloading a tab loses unsaved drafts.
|
|
127
|
+
Long result summaries expand with **Read more / Show less**. **Replies (N)**
|
|
128
|
+
lists source labels and comment excerpts. The reply reader retains the
|
|
129
|
+
Feedback/History tabs, **Previous/Next** and **Back to replies**, restoring the
|
|
130
|
+
originating list, scroll and keyboard focus without discarding drafts.
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Complete responses
|
|
2
|
+
|
|
3
|
+
Generate a new response file using the actual submission handoff:
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npx -y @erdemtuna/doc-review response-template --review <reviewId> --entry <entryKey> --submission <submissionId> --output-file response.json
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
This reads the authoritative immutable submission, not just the first inventory
|
|
10
|
+
page. It includes every message/edit and exact version, delivered expectedVersion,
|
|
11
|
+
and a stable requestId. The file is exclusive: an existing response is never
|
|
12
|
+
overwritten. Inspect/reuse it for recovery; do not generate a new ID for a retry.
|
|
13
|
+
All blank prose/outcomes deliberately fail validation until filled. The command
|
|
14
|
+
does not edit source or assert that anything was applied.
|
|
15
|
+
|
|
16
|
+
The JSON contract is strict (unknown fields are rejected):
|
|
17
|
+
|
|
18
|
+
```json
|
|
19
|
+
{
|
|
20
|
+
"operation": "respond",
|
|
21
|
+
"reviewId": "review-id",
|
|
22
|
+
"entryKey": "entry-key",
|
|
23
|
+
"submissionId": "submission-id",
|
|
24
|
+
"expectedVersion": 2,
|
|
25
|
+
"requestId": "stable-request-id",
|
|
26
|
+
"responses": [{
|
|
27
|
+
"threadId": "thread-id",
|
|
28
|
+
"messageId": "message-id",
|
|
29
|
+
"messageVersion": 1,
|
|
30
|
+
"body": "The explanation preserves the original meaning.",
|
|
31
|
+
"outcome": "answered"
|
|
32
|
+
}],
|
|
33
|
+
"editOutcomes": [{
|
|
34
|
+
"editId": "edit-id",
|
|
35
|
+
"editVersion": 1,
|
|
36
|
+
"outcome": "deferred",
|
|
37
|
+
"reason": "The source target is ambiguous; please identify the component."
|
|
38
|
+
}],
|
|
39
|
+
"overallOutcome": "answered",
|
|
40
|
+
"summary": "Answered the questions; one edit needs a clearer target.",
|
|
41
|
+
"resultNote": "Answered the discussion and overall note; deferred the ambiguous edit."
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Keep the generated IDs, versions and requestId. `responses` must cover every
|
|
46
|
+
submitted message exactly once, not just visible inventory items. Each response
|
|
47
|
+
has a nonempty body and scalar outcome: `answered`, `applied`,
|
|
48
|
+
`clarification-needed`, or `deferred`. Only that message's request-change permits
|
|
49
|
+
`applied`; another message or overall note cannot grant it permission.
|
|
50
|
+
|
|
51
|
+
`editOutcomes` covers each exact edit version once. Outcome is `applied`,
|
|
52
|
+
`already-saved`, or `deferred`, with nonempty `reason`. `already-saved` requires
|
|
53
|
+
server save evidence. Saved or capture-truncated edits cannot be newly `applied`.
|
|
54
|
+
Use truthful deferrals instead of fabricated success to satisfy coverage.
|
|
55
|
+
|
|
56
|
+
`overallOutcome` is a **scalar string**, not an object. Include it if and only if
|
|
57
|
+
the submission has an overall note. Its enum is `applied`, `answered`,
|
|
58
|
+
`clarification-needed`, `deferred`; `applied` requires that note's request-change.
|
|
59
|
+
The overall answer's prose belongs in `resultNote`. That independent, nonempty
|
|
60
|
+
result note is always required, even without an overall note.
|
|
61
|
+
|
|
62
|
+
`summary` is an authored orientation sentence or two, separate from the full
|
|
63
|
+
`resultNote`. Fill it in new templates. Older responses and records may omit it;
|
|
64
|
+
when present it must be nonempty. It grants no permission and replaces neither
|
|
65
|
+
message coverage nor edit outcomes. Its exact text participates in receipt replay:
|
|
66
|
+
do not revise it under an accepted requestId. Long summaries use the usual scoped
|
|
67
|
+
content references and export, including `--field result/summary`.
|
|
68
|
+
|
|
69
|
+
Write answer/outcome first. Routine replies usually need one or two sentences
|
|
70
|
+
and at most three useful bullets (roughly 40-90 words is guidance, not a limit).
|
|
71
|
+
Lead clarification with the exact question. Use Markdown for readable saved
|
|
72
|
+
prose, but avoid IDs/hashes, repetitive preservation boilerplate, or duplicating
|
|
73
|
+
all replies in the summary. Edit reasons explain the outcome, not the full evidence.
|
|
74
|
+
|
|
75
|
+
A note-only response has empty `responses` and `editOutcomes` arrays, for example:
|
|
76
|
+
|
|
77
|
+
```json
|
|
78
|
+
{
|
|
79
|
+
"operation": "respond",
|
|
80
|
+
"reviewId": "review-id",
|
|
81
|
+
"entryKey": "entry-key",
|
|
82
|
+
"submissionId": "submission-id",
|
|
83
|
+
"expectedVersion": 2,
|
|
84
|
+
"requestId": "stable-request-id",
|
|
85
|
+
"responses": [],
|
|
86
|
+
"editOutcomes": [],
|
|
87
|
+
"overallOutcome": "answered",
|
|
88
|
+
"summary": "Explained the tradeoff; no source change was requested.",
|
|
89
|
+
"resultNote": "The earlier result explained the tradeoff; no new source change was requested."
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Submit the filled file:
|
|
94
|
+
|
|
95
|
+
```sh
|
|
96
|
+
npx -y @erdemtuna/doc-review respond --review <reviewId> --entry <entryKey> --response-file response.json --timeout 600
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`{ok:true,receipt}` means the complete response persisted atomically. Comparison
|
|
100
|
+
capture does not gate acceptance. Reply-only work does not fabricate a version or
|
|
101
|
+
reload; saved human changes can display "What changed" without new agent edits.
|
|
102
|
+
Then poll the original review again.
|
|
103
|
+
|
|
104
|
+
Exit 1 is a semantic/explicit failure. Correct a rejected response deliberately;
|
|
105
|
+
do not turn a rejection into success. Exit 2 (`state:"unknown"`) means acceptance
|
|
106
|
+
is uncertain. The CLI already retries the same logical body within its deadline.
|
|
107
|
+
Retry the identical response file, not source work or a newly minted requestId.
|
|
108
|
+
See [context-and-recovery](context-and-recovery.md) before recovering uncertainty.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Exact source edits
|
|
2
|
+
|
|
3
|
+
Before source work, inspect every necessary inventory/content page and the actual
|
|
4
|
+
authoritative source. A downloaded/exported file is not evidence you inspected it.
|
|
5
|
+
`inline.value` is the complete retained object; a `reference` is only a delivery
|
|
6
|
+
pointer. Follow its command or export and read the artifact. A preview is never an
|
|
7
|
+
`after` replacement. Fields remain exact, including line endings and Unicode.
|
|
8
|
+
|
|
9
|
+
Every submitted message carries independent `discuss` or `request-change` intent.
|
|
10
|
+
Discuss is answered without editing for that message. Request-change permits,
|
|
11
|
+
but does not require, only its specific change. An overall note is independent;
|
|
12
|
+
neither it nor a historical request grants blanket permission.
|
|
13
|
+
|
|
14
|
+
Human direct edits are exact requested content separate from message intent.
|
|
15
|
+
Preserve non-truncated `after` wording verbatim. Translate HTML formatting into
|
|
16
|
+
the true source syntax, retain insertion points/assets, honor moves'
|
|
17
|
+
`moved_after`/`moved_before` boundaries and explicit deletions. Read all
|
|
18
|
+
representations needed to understand the edit, not merely a convenient preview.
|
|
19
|
+
|
|
20
|
+
The edit's `source` data contains `state:"pending"` or `state:"saved"` plus
|
|
21
|
+
server-owned evidence: review, page, edit ID/version, source hash and saved time.
|
|
22
|
+
Never reapply a saved edit. Use `already-saved` only with that exact evidence.
|
|
23
|
+
Report inconsistent source/evidence instead of guessing that a write succeeded.
|
|
24
|
+
|
|
25
|
+
Inventory page entries identify real files/URLs. For source-pending Markdown,
|
|
26
|
+
scripted HTML and localhost pages, find actual Markdown/MDX/TSX/template/component
|
|
27
|
+
source; do not replace source with rendered Markdown, fetched HTTP, or
|
|
28
|
+
script-generated DOM. Plain HTML alone supports the reviewer's direct save.
|
|
29
|
+
Disabling scripts is not permission to write source.
|
|
30
|
+
|
|
31
|
+
Use exact target anchors and authoritative source to identify the intended
|
|
32
|
+
location. Missing/ambiguous targets require safe identification or a
|
|
33
|
+
clarification/deferred outcome; never guess a destructive replacement.
|
|
34
|
+
Copy authoritative staged assets from their retained paths into the correct
|
|
35
|
+
project asset location and replace temporary preview references without losing
|
|
36
|
+
the original insertion point.
|
|
37
|
+
|
|
38
|
+
`captureTruncated` and `truncatedFields` mirror original `content.truncated` and
|
|
39
|
+
`content.truncated_fields`. These mean evidence was clipped during capture
|
|
40
|
+
(currently 200,000 Unicode code points per text/HTML/move field), not during
|
|
41
|
+
delivery. Delivery references do NOT clear those flags. Never apply incomplete
|
|
42
|
+
text/HTML as a complete replacement or invent missing text. Recover only from an
|
|
43
|
+
authoritative source or defer and request the complete edit.
|
|
44
|
+
|
|
45
|
+
Inspect current source and durable evidence after a restart or uncertain
|
|
46
|
+
response. Receipt replay protects durable responses, not filesystem operations;
|
|
47
|
+
never repeat source writes simply because an HTTP connection failed.
|
package/lib/revision-store.js
CHANGED
|
@@ -14,7 +14,7 @@ function captureTime(value, now) {
|
|
|
14
14
|
return time;
|
|
15
15
|
}
|
|
16
16
|
export class RevisionStore {
|
|
17
|
-
constructor({ root = path.join(stateDir(), "history"), write = atomicWrite, limits = REVISION_LIMITS } = {}) {
|
|
17
|
+
constructor({ root = path.join(stateDir(), "conversation-history"), write = atomicWrite, limits = REVISION_LIMITS } = {}) {
|
|
18
18
|
this.root = root;
|
|
19
19
|
this.write = write;
|
|
20
20
|
this.limits = normalizeRevisionLimits(limits);
|
package/lib/save-controller.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { decodePage, record } from "./chrome-api.js";
|
|
1
|
+
import { ApiError, decodePage, record, saveFailureMessage } from "./chrome-api.js";
|
|
2
2
|
import { createControllerStore } from "./controller-store.js";
|
|
3
|
-
export function createSaveController({ sessionId, current, policy, request, flush, send, sourceHash, pageChanged, conflict, failed, diagnostic, sending, clock = () => new Date().toLocaleTimeString([], { hour: "numeric", minute: "2-digit" }), setTimer = globalThis.setTimeout, clearTimer = globalThis.clearTimeout, }) {
|
|
3
|
+
export function createSaveController({ sessionId, current, policy, request, flush, send, sourceHash, pageChanged, conflict, failed, diagnostic, sending, conversation, clock = () => new Date().toLocaleTimeString([], { hour: "numeric", minute: "2-digit" }), setTimer = globalThis.setTimeout, clearTimer = globalThis.clearTimeout, }) {
|
|
4
4
|
const state = {
|
|
5
|
-
status: "idle", savedAt: "", baseHash: null, conflict: false, dirty: false, dynamic: false,
|
|
5
|
+
status: "idle", savedAt: "", baseHash: null, conflict: false, conflictMessage: "", dirty: false, dynamic: false,
|
|
6
6
|
};
|
|
7
7
|
const store = createControllerStore(() => state);
|
|
8
8
|
const pipelines = new Map();
|
|
@@ -67,14 +67,14 @@ export function createSaveController({ sessionId, current, policy, request, flus
|
|
|
67
67
|
if (disposed)
|
|
68
68
|
return false;
|
|
69
69
|
try {
|
|
70
|
-
const result = record(await request(`/api/page/${key}/edit`, {
|
|
70
|
+
const result = conversation ? await conversation.record(key, original) : record(await request(`/api/page/${key}/edit`, {
|
|
71
71
|
method: "POST", body: JSON.stringify(payload),
|
|
72
72
|
}));
|
|
73
73
|
if (ownedBacklog.get(id) === payload)
|
|
74
74
|
ownedBacklog.delete(id);
|
|
75
75
|
errors.delete(key);
|
|
76
|
-
if (matches(identity))
|
|
77
|
-
pageChanged(decodePage(result.page));
|
|
76
|
+
if (!conversation && matches(identity))
|
|
77
|
+
pageChanged(decodePage(record(result).page));
|
|
78
78
|
return true;
|
|
79
79
|
}
|
|
80
80
|
catch (error) {
|
|
@@ -100,6 +100,8 @@ export function createSaveController({ sessionId, current, policy, request, flus
|
|
|
100
100
|
if (disposed)
|
|
101
101
|
throw new Error("Review ended");
|
|
102
102
|
if (backlogs.get(key)?.size) {
|
|
103
|
+
if (conversation)
|
|
104
|
+
throw errors.get(key) || new Error("Reconcile the failed edit before continuing.");
|
|
103
105
|
const retry = [...(backlogs.get(key)?.values() || [])];
|
|
104
106
|
errors.delete(key);
|
|
105
107
|
for (const payload of retry)
|
|
@@ -122,7 +124,9 @@ export function createSaveController({ sessionId, current, policy, request, flus
|
|
|
122
124
|
return false;
|
|
123
125
|
}
|
|
124
126
|
try {
|
|
125
|
-
|
|
127
|
+
if (conversation)
|
|
128
|
+
await settleEdits(identity.key);
|
|
129
|
+
const result = conversation ? await conversation.save(identity.key, html, state.baseHash) : record(await request(`/api/page/${identity.key}/save`, {
|
|
126
130
|
method: "POST",
|
|
127
131
|
body: JSON.stringify({
|
|
128
132
|
html, baseHash: state.baseHash, renderId: identity.renderId,
|
|
@@ -149,19 +153,20 @@ export function createSaveController({ sessionId, current, policy, request, flus
|
|
|
149
153
|
state.baseHash = null;
|
|
150
154
|
state.status = "idle";
|
|
151
155
|
state.conflict = true;
|
|
156
|
+
state.conflictMessage = saveFailureMessage(error instanceof ApiError ? error.saveReason : undefined);
|
|
152
157
|
publish();
|
|
153
|
-
conflict();
|
|
158
|
+
conflict(state.conflictMessage);
|
|
154
159
|
diagnostic("save-conflict");
|
|
155
160
|
return false;
|
|
156
161
|
}
|
|
157
162
|
state.status = "failed";
|
|
158
163
|
publish();
|
|
159
|
-
if (attempts < 2) {
|
|
164
|
+
if (!conversation && attempts < 2) {
|
|
160
165
|
await delay(500);
|
|
161
166
|
return saveHtml(html, identity, attempts + 1);
|
|
162
167
|
}
|
|
163
168
|
if (!sending())
|
|
164
|
-
failed("Couldn't save. Retry before sending feedback; your edits remain on this page.");
|
|
169
|
+
failed(conversation && error instanceof Error ? error.message : "Couldn't save. Retry before sending feedback; your edits remain on this page.");
|
|
165
170
|
return false;
|
|
166
171
|
}
|
|
167
172
|
}
|
|
@@ -192,7 +197,7 @@ export function createSaveController({ sessionId, current, policy, request, flus
|
|
|
192
197
|
const identity = current();
|
|
193
198
|
await flushEdits(true);
|
|
194
199
|
await activeSave;
|
|
195
|
-
if (state.status === "failed" && !state.conflict && lastSave && matches(lastSave.identity)) {
|
|
200
|
+
if (!conversation && state.status === "failed" && !state.conflict && lastSave && matches(lastSave.identity)) {
|
|
196
201
|
await save(lastSave.html);
|
|
197
202
|
}
|
|
198
203
|
await settleEdits(identity.key);
|
|
@@ -200,7 +205,7 @@ export function createSaveController({ sessionId, current, policy, request, flus
|
|
|
200
205
|
if (!matches(identity))
|
|
201
206
|
throw new Error("The page changed while saving. Retry on the latest page.");
|
|
202
207
|
if (state.conflict)
|
|
203
|
-
throw new Error(
|
|
208
|
+
throw new Error(`Resolve the save conflict: ${state.conflictMessage || saveFailureMessage()}`);
|
|
204
209
|
if (state.status === "failed" || (policy() === "writable" && !state.dynamic && state.dirty)) {
|
|
205
210
|
throw new Error("Your page edits have not finished saving. They have not been sent.");
|
|
206
211
|
}
|
|
@@ -233,9 +238,18 @@ export function createSaveController({ sessionId, current, policy, request, flus
|
|
|
233
238
|
save, persistEdit, settleEdits, flush: flushEdits, barrier, captureStable,
|
|
234
239
|
applyClean, queued, cancelRetries,
|
|
235
240
|
async settled() { await activeSave; },
|
|
241
|
+
async discardLocal(key) {
|
|
242
|
+
if (!key)
|
|
243
|
+
return;
|
|
244
|
+
await pipelines.get(key);
|
|
245
|
+
await activeSave;
|
|
246
|
+
backlogs.delete(key);
|
|
247
|
+
errors.delete(key);
|
|
248
|
+
},
|
|
236
249
|
markEdit() { editSequence++; },
|
|
237
250
|
markSaving() { state.dirty = true; state.status = "saving"; publish(); },
|
|
238
|
-
markDynamic() { state.dynamic = true;
|
|
251
|
+
markDynamic() { state.dynamic = true; if (state.status === "saving" && !pending)
|
|
252
|
+
state.status = "idle"; publish(); },
|
|
239
253
|
observeClean() { clean = { identity: current(), sequence, editSequence }; applyClean(); },
|
|
240
254
|
baseline(hash) { state.baseHash = hash; publish(); },
|
|
241
255
|
hold() {
|
|
@@ -252,7 +266,7 @@ export function createSaveController({ sessionId, current, policy, request, flus
|
|
|
252
266
|
clean = null;
|
|
253
267
|
lastSave = null;
|
|
254
268
|
sequence++;
|
|
255
|
-
Object.assign(state, { status: "idle", savedAt: "", baseHash: null, conflict: false, dirty: false, dynamic: false });
|
|
269
|
+
Object.assign(state, { status: "idle", savedAt: "", baseHash: null, conflict: false, conflictMessage: "", dirty: false, dynamic: false });
|
|
256
270
|
publish();
|
|
257
271
|
},
|
|
258
272
|
async revert() {
|