@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 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
- ![A landing page in Doc Review with highlighted text and a comment asking for more specific wording](https://raw.githubusercontent.com/erdemtuna/doc-review/main/assets/doc-review.png)
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)
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 **Comments**, add an overall note if needed, and
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
+ ![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)
55
+
56
+ *One batch, with the context attached: comments, your edits, and the overall direction.*
57
+
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)
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
- 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,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
+ }