@erdemtuna/doc-review 0.10.0 → 0.11.1
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 +13 -2
- package/lib/SKILL.md +5 -2
- package/lib/changes-controller.js +184 -0
- package/lib/chrome-client.js +361 -916
- package/lib/chrome-session.js +11 -14
- package/lib/chrome.html +19 -162
- package/lib/comments-controller.js +109 -0
- package/lib/contextual-controller.js +33 -0
- package/lib/controller-store.js +96 -0
- package/lib/execution-client.js +1 -51
- package/lib/feedback-controller.js +21 -14
- package/lib/feedback-panel-controller.js +161 -0
- package/lib/frame-controller.js +15 -8
- package/lib/history-coordinator.js +7 -0
- package/lib/icons.js +76 -0
- package/lib/recovery-controller.js +123 -0
- package/lib/save-controller.js +13 -10
- package/lib/server.js +4 -4
- package/lib/toolbar-controller.js +32 -0
- package/lib/ui/THIRD_PARTY_NOTICES.md +1254 -0
- package/lib/ui/chrome.css +4885 -0
- package/lib/ui/chrome.js +52 -0
- package/package.json +31 -5
- package/lib/chrome.css +0 -652
- package/lib/comparison-view.js +0 -380
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Open an HTML file, a Markdown document, or a localhost page. Point to what needs
|
|
|
6
6
|
changing, edit the small things yourself, and send your feedback to the agent
|
|
7
7
|
in one batch.
|
|
8
8
|
|
|
9
|
-

|
|
10
10
|
|
|
11
11
|
*Feedback stays beside the work. Your agent gets the comments, edits, and overall note together.*
|
|
12
12
|
|
|
@@ -42,7 +42,7 @@ to open the page, wait for feedback, and apply the changes.
|
|
|
42
42
|
2. **Point out what matters.** Select text or choose an element to leave a
|
|
43
43
|
comment. Switch to **Edit** for direct changes to wording, formatting, images,
|
|
44
44
|
or layout. Commenting works in either mode.
|
|
45
|
-
3. **Send one batch.** Open **
|
|
45
|
+
3. **Send one batch.** Open **Feedback**, inspect Comments and Edits, add an overall note if needed, and
|
|
46
46
|
choose **Send to agent**. No need to describe where every sentence lives.
|
|
47
47
|
4. **Check the result.** Use **Changes** to compare a review round's captured
|
|
48
48
|
before and after content, then continue reviewing. Comparisons show observed
|
|
@@ -51,6 +51,14 @@ to open the page, wait for feedback, and apply the changes.
|
|
|
51
51
|
You can review a plan, refine a landing page, or walk through a local app without
|
|
52
52
|
moving your feedback into a separate document.
|
|
53
53
|
|
|
54
|
+

|
|
55
|
+
|
|
56
|
+
*One batch, with the context attached: comments, your edits, and the overall direction.*
|
|
57
|
+
|
|
58
|
+

|
|
59
|
+
|
|
60
|
+
*Check the result beside the original. Move between changes or switch to Source for the saved file text.*
|
|
61
|
+
|
|
54
62
|
## What happens to your edits?
|
|
55
63
|
|
|
56
64
|
| What you open | Where edits go |
|
|
@@ -74,6 +82,9 @@ setup options, comments, comparisons, limitations, and upgrades.
|
|
|
74
82
|
[Development](https://github.com/erdemtuna/doc-review/blob/main/docs/development.md):
|
|
75
83
|
build, test, architecture, and package checks.
|
|
76
84
|
|
|
85
|
+
[Prepared review example](docs/migration-review.md):
|
|
86
|
+
disposable HTML/Markdown sessions with saved comparison rounds and a shell review checklist.
|
|
87
|
+
|
|
77
88
|
[Releasing](https://github.com/erdemtuna/doc-review/blob/main/RELEASING.md):
|
|
78
89
|
the maintainers' release process.
|
|
79
90
|
|
package/lib/SKILL.md
CHANGED
|
@@ -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
|
-
|
|
45
|
-
|
|
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,184 @@
|
|
|
1
|
+
import { createControllerStore } from "./controller-store.js";
|
|
2
|
+
import { historyPresentation } from "./history-coordinator.js";
|
|
3
|
+
import { changeKind, comparisonFreshness, excerpt } from "./history-client.js";
|
|
4
|
+
import { tidyMiddle } from "./anchor-text.js";
|
|
5
|
+
const roundId = (round) => round?.id || round?.roundId || "";
|
|
6
|
+
const timestamp = (value) => value == null ? NaN : new Date(value).getTime();
|
|
7
|
+
const formatTime = (value) => {
|
|
8
|
+
const time = timestamp(value);
|
|
9
|
+
return Number.isFinite(time) ? new Date(time).toLocaleString() : "Not available";
|
|
10
|
+
};
|
|
11
|
+
/** Pure normalization: reading or rendering never edits the authoritative history. */
|
|
12
|
+
export function changesSelection(history) {
|
|
13
|
+
const round = history.round && roundId(history.round) === history.selectedId ? history.round : null;
|
|
14
|
+
const targets = round?.targets || [];
|
|
15
|
+
const target = targets.find((item) => item.key === history.targetKey) || targets[0] || null;
|
|
16
|
+
const comparison = target?.comparison || {};
|
|
17
|
+
const modes = ["content", "source"].filter((mode) => comparison[mode]?.available === true);
|
|
18
|
+
const mode = modes.find((item) => item === history.preferredMode) || modes[0] || "content";
|
|
19
|
+
const value = comparison[mode] || {};
|
|
20
|
+
const items = value.changes || value.items || (mode === "content" ? comparison.changes || comparison.items : null) || [];
|
|
21
|
+
const index = Number.isSafeInteger(history.index) ? Math.max(0, Math.min(history.index, items.length - 1)) : 0;
|
|
22
|
+
return { round, target, targets, comparison, modes, mode, value, items, index, key: `${roundId(round)}:${target?.key || ""}` };
|
|
23
|
+
}
|
|
24
|
+
export function createChangesController(options) {
|
|
25
|
+
let disposed = false;
|
|
26
|
+
let diagnosticsOpen = false;
|
|
27
|
+
let limitationsOpen = false;
|
|
28
|
+
let pending = null;
|
|
29
|
+
let actionError = null;
|
|
30
|
+
let scrollSequence = 0;
|
|
31
|
+
let scrollRequest = null;
|
|
32
|
+
const getDetail = () => changesSelection(options.read().history);
|
|
33
|
+
let detailValue = getDetail().comparison[getDetail().mode] || null;
|
|
34
|
+
let detailVersion = 0;
|
|
35
|
+
function read() {
|
|
36
|
+
const context = options.read();
|
|
37
|
+
const history = context.history;
|
|
38
|
+
const selection = changesSelection(history);
|
|
39
|
+
const { round, target, comparison, modes, mode, value, items, index, key } = selection;
|
|
40
|
+
const failure = history.failures.get(key);
|
|
41
|
+
const presentation = historyPresentation({ ...history, round, target, comparison, failure });
|
|
42
|
+
const captureBusy = history.captureBusy || pending === "capture";
|
|
43
|
+
const finalizing = history.finalizing || pending === "finish";
|
|
44
|
+
const disabled = context.ended || !context.comparing;
|
|
45
|
+
const captureAllowed = !!target && !target.resultRevisionId && target.capture?.status !== "unavailable" &&
|
|
46
|
+
round?.feedbackStatus === "acknowledged";
|
|
47
|
+
const finishAllowed = !!target && !target.resultRevisionId && round?.feedbackStatus === "acknowledged" &&
|
|
48
|
+
["pending", "failed"].includes(target.capture?.status || target.captureStatus || "pending");
|
|
49
|
+
const delay = timestamp(value.afterCapturedAt) - timestamp(round?.acknowledgedAt);
|
|
50
|
+
const view = mode === "content" ? value.viewComparison : null;
|
|
51
|
+
return {
|
|
52
|
+
disabled, loading: history.loading, key, mode, index, modes, detailVersion, scrollRequest, hasComparison: modes.length > 0,
|
|
53
|
+
rounds: history.rounds.map((item, position) => ({
|
|
54
|
+
value: roundId(item),
|
|
55
|
+
shortLabel: `Round ${item.ordinal || item.number || history.rounds.length - position}`,
|
|
56
|
+
label: `Round ${item.ordinal || item.number || history.rounds.length - position} · ${item.captureStatus === "ready" ? "completed" : item.captureStatus || item.status || item.feedbackStatus || "pending"}`,
|
|
57
|
+
})),
|
|
58
|
+
selectedId: history.selectedId || "",
|
|
59
|
+
targets: selection.targets.map((item) => ({ value: item.key, label: item.filename || item.label || item.key })),
|
|
60
|
+
targetKey: target?.key || "",
|
|
61
|
+
status: presentation.state,
|
|
62
|
+
message: history.loading ? round ? "Refreshing comparison…" : "Loading comparison…" : presentation.message,
|
|
63
|
+
error: actionError?.key === key ? actionError.message : history.error?.message || failure || target?.capture?.error || "",
|
|
64
|
+
canRetry: !!history.error && !history.loading,
|
|
65
|
+
counts: modes.length ? value.counts || null : null,
|
|
66
|
+
changes: items.map((item, position) => ({
|
|
67
|
+
value: String(position),
|
|
68
|
+
label: `${position + 1}. ${changeKind(item)} · ${tidyMiddle(item.label || excerpt(item.after) || excerpt(item.before) || "Structure", 65)}`,
|
|
69
|
+
})),
|
|
70
|
+
position: `${items.length ? index + 1 : 0} of ${items.length}`,
|
|
71
|
+
previousDisabled: disabled || history.loading || index === 0,
|
|
72
|
+
nextDisabled: disabled || history.loading || index >= items.length - 1,
|
|
73
|
+
captureAllowed, captureBusy,
|
|
74
|
+
captureDisabled: disabled || history.loading || context.sending || captureBusy || finalizing ||
|
|
75
|
+
target?.key !== context.current.key || !context.current.ready || context.current.pendingReload,
|
|
76
|
+
finishAllowed, finalizing,
|
|
77
|
+
finishDisabled: disabled || history.loading || context.sending || finalizing || captureBusy,
|
|
78
|
+
diagnosticsOpen, limitationsOpen,
|
|
79
|
+
unavailable: ["content", "source"].flatMap((item) => comparison[item]?.available === false
|
|
80
|
+
? [`${item === "content" ? "Content" : "Source"} comparison unavailable: ${comparison[item]?.reason || "This representation could not be compared."}`] : []),
|
|
81
|
+
timing: round ? [
|
|
82
|
+
{ label: "Before captured", value: formatTime(value.beforeCapturedAt) },
|
|
83
|
+
{ label: "Agent acknowledged", value: formatTime(round.acknowledgedAt) },
|
|
84
|
+
{ label: "After captured", value: formatTime(value.afterCapturedAt) },
|
|
85
|
+
] : [],
|
|
86
|
+
delay: !Number.isFinite(delay) ? "" : delay >= 0
|
|
87
|
+
? `Result captured ${Math.round(delay / 1000)} seconds after acknowledgment—not at the acknowledgment instant. A delayed capture can include later changes.`
|
|
88
|
+
: "This result snapshot predates acknowledgment. It is not proof of the page state at acknowledgment.",
|
|
89
|
+
freshness: target?.resultRevisionId || value.afterCapturedAt
|
|
90
|
+
? comparisonFreshness(value, target?.key, context.current) : "",
|
|
91
|
+
view: view?.message ? view : null,
|
|
92
|
+
limitations: [...new Set([...(comparison.content?.limitations || []), ...(comparison.source?.limitations || [])])]
|
|
93
|
+
.map((item) => String(item).replaceAll("-", " ").replaceAll("_", " ")),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
const store = createControllerStore(read);
|
|
97
|
+
const publish = () => {
|
|
98
|
+
if (disposed)
|
|
99
|
+
return;
|
|
100
|
+
const selection = getDetail();
|
|
101
|
+
const next = selection.comparison[selection.mode] || null;
|
|
102
|
+
if (detailValue !== next) {
|
|
103
|
+
detailValue = next;
|
|
104
|
+
detailVersion++;
|
|
105
|
+
}
|
|
106
|
+
store.publish();
|
|
107
|
+
};
|
|
108
|
+
const available = () => !disposed && !options.read().ended && options.read().comparing;
|
|
109
|
+
async function run(action, key) {
|
|
110
|
+
const current = read();
|
|
111
|
+
if (!available() || key !== current.key || pending ||
|
|
112
|
+
(action === "capture" ? !current.captureAllowed || current.captureDisabled : !current.finishAllowed || current.finishDisabled))
|
|
113
|
+
return;
|
|
114
|
+
pending = action;
|
|
115
|
+
actionError = null;
|
|
116
|
+
publish();
|
|
117
|
+
try {
|
|
118
|
+
await (action === "capture" ? options.capture() : options.finish());
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
if (!disposed) {
|
|
122
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
123
|
+
actionError = { key, message };
|
|
124
|
+
options.failed(message);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
finally {
|
|
128
|
+
pending = null;
|
|
129
|
+
publish();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
getSnapshot: store.getSnapshot, subscribe: store.subscribe, publish, getDetail,
|
|
134
|
+
commands: {
|
|
135
|
+
selectRound(id) {
|
|
136
|
+
const history = options.read().history;
|
|
137
|
+
if (available() && history.selectedId !== id && history.rounds.some((round) => roundId(round) === id)) {
|
|
138
|
+
actionError = null;
|
|
139
|
+
scrollRequest = null;
|
|
140
|
+
return options.selectRound(id);
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
selectTarget(key) {
|
|
144
|
+
const selection = getDetail();
|
|
145
|
+
if (available() && !options.read().history.loading && key !== selection.target?.key && selection.targets.some((target) => target.key === key)) {
|
|
146
|
+
actionError = null;
|
|
147
|
+
scrollRequest = null;
|
|
148
|
+
return options.selectTarget(key);
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
selectMode(mode) {
|
|
152
|
+
const selection = getDetail();
|
|
153
|
+
if (available() && !options.read().history.loading && (mode === "content" || mode === "source") &&
|
|
154
|
+
mode !== selection.mode && selection.modes.includes(mode)) {
|
|
155
|
+
scrollRequest = null;
|
|
156
|
+
options.selectMode(mode);
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
jump(index, key) {
|
|
160
|
+
const selection = getDetail();
|
|
161
|
+
if (!available() || options.read().history.loading || key !== selection.key ||
|
|
162
|
+
!Number.isSafeInteger(index) || index < 0 || index >= selection.items.length)
|
|
163
|
+
return;
|
|
164
|
+
scrollRequest = { sequence: ++scrollSequence, key, mode: selection.mode, index };
|
|
165
|
+
options.selectIndex(index);
|
|
166
|
+
publish();
|
|
167
|
+
},
|
|
168
|
+
capture: (key) => run("capture", key),
|
|
169
|
+
finish: (key) => run("finish", key),
|
|
170
|
+
retry() { if (available() && !options.read().history.loading)
|
|
171
|
+
return options.refresh(); },
|
|
172
|
+
disclose(name, open) {
|
|
173
|
+
if (!available())
|
|
174
|
+
return;
|
|
175
|
+
if (name === "diagnostics")
|
|
176
|
+
diagnosticsOpen = open;
|
|
177
|
+
else
|
|
178
|
+
limitationsOpen = open;
|
|
179
|
+
publish();
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
dispose() { disposed = true; store.dispose(); },
|
|
183
|
+
};
|
|
184
|
+
}
|