@erdemtuna/doc-review 0.10.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.
@@ -0,0 +1,161 @@
1
+ import { createControllerStore } from "./controller-store.js";
2
+ export const createNoteDraft = () => ({ text: "", selectionStart: 0, selectionEnd: 0, composing: false });
3
+ function savePresentation(state) {
4
+ if (state.save.conflict)
5
+ return "Source changed — reload latest before saving. Your page edits remain in this tab.";
6
+ if (state.kind === "url")
7
+ return "Localhost page — your direct edits go to the agent for source updates";
8
+ if (state.markdown)
9
+ return "Markdown source — edits go to the agent as feedback";
10
+ if (state.save.dynamic)
11
+ return "Live page — edits go to the agent, the file is left alone";
12
+ if (state.save.status === "saving")
13
+ return `Saving to ${state.filename}…`;
14
+ if (state.save.status === "failed")
15
+ return "Couldn't save — retry before sending";
16
+ return `Saved to ${state.filename}${state.save.savedAt ? ` · ${state.save.savedAt}` : ""}`;
17
+ }
18
+ export function createFeedbackPanelController(options) {
19
+ let disposed = false;
20
+ let expanded = false;
21
+ let editsOpen = true;
22
+ let dialog = null;
23
+ let copyStatus = "idle";
24
+ let copyError = "";
25
+ const fingerprint = (action, state) => JSON.stringify(action === "revert"
26
+ ? state.edits : [state.total, state.drafts, state.note.text]);
27
+ const valid = (owned, full = true) => {
28
+ const state = options.read();
29
+ return !disposed && !state.ended && state.available && !state.delivery.sending &&
30
+ state.identity === owned.identity && (!full ||
31
+ (state.source === owned.source && fingerprint(owned.action, state) === owned.fingerprint));
32
+ };
33
+ const store = createControllerStore(() => {
34
+ const state = options.read();
35
+ const busy = state.delivery.sending;
36
+ const sent = state.delivery.sent || state.agent === "working" || state.agent === "stranded";
37
+ const count = state.total;
38
+ const hasNote = !!state.note.text.trim();
39
+ return {
40
+ note: state.note, ended: state.ended, disabled: state.ended || !state.available,
41
+ edits: expanded ? state.edits : state.edits.slice(0, 5), editCount: state.edits.length, expanded, editsOpen,
42
+ saveText: savePresentation(state), saveError: state.save.conflict || state.save.status === "failed",
43
+ saveStatus: state.save.status, drafts: state.drafts, busy, delivery: state.delivery,
44
+ sendDisabled: state.ended || !state.available || (!count && !hasNote) || busy || sent || !!dialog || state.note.composing,
45
+ sendText: busy ? "Sending feedback…" : state.agent === "working" ? "Feedback delivered"
46
+ : state.agent === "stranded" ? "Sent — agent is not listening"
47
+ : state.delivery.sent ? "Sent — waiting for agent" : count ? `Send ${count} to agent`
48
+ : hasNote ? "Send note to agent" : "Nothing to send yet",
49
+ agent: state.agent, prompt: state.prompt, copyStatus, copyError,
50
+ dialog: dialog ? { ...dialog, stale: !valid(dialog) } : null,
51
+ };
52
+ });
53
+ const publish = () => { if (!disposed)
54
+ store.publish(); };
55
+ const available = () => !disposed && !options.read().ended && options.read().available;
56
+ async function send() {
57
+ const state = options.read();
58
+ if (!available() || dialog || state.note.composing || (!state.total && !state.note.text.trim()) ||
59
+ state.delivery.sending || state.delivery.sent || state.agent === "working" || state.agent === "stranded")
60
+ return;
61
+ await options.send();
62
+ }
63
+ return {
64
+ getSnapshot: store.getSnapshot, subscribe: store.subscribe, publish,
65
+ commands: {
66
+ setEditsOpen(open) {
67
+ if (!available() || dialog)
68
+ return;
69
+ editsOpen = open;
70
+ publish();
71
+ },
72
+ updateNote(value) {
73
+ if (!available() || dialog)
74
+ return;
75
+ Object.assign(options.read().note, value);
76
+ publish();
77
+ },
78
+ expand() { if (available()) {
79
+ expanded = !expanded;
80
+ publish();
81
+ } },
82
+ send,
83
+ open(action) {
84
+ const state = options.read();
85
+ if (!available() || dialog || state.delivery.sending || (action === "revert" && !state.edits.length))
86
+ return;
87
+ if (action === "revert")
88
+ editsOpen = true;
89
+ const drafts = state.drafts + Number(!!state.note.text.trim());
90
+ dialog = {
91
+ action, identity: state.identity, source: state.source, fingerprint: fingerprint(action, state),
92
+ pending: false, error: "",
93
+ description: action === "revert" ? `Discard all ${state.edits.length} of your edits? Comments and the overall note are kept.`
94
+ : `${state.total ? `${state.total} unsent ${state.total === 1 ? "item will" : "items will"} be kept for next time. ` : ""}${drafts ? `${drafts} open ${drafts === 1 ? "draft (including any overall note) is" : "drafts (including any overall note) are"} only in this tab and will not be sent. ` : ""}The waiting agent will be told to stop polling.`,
95
+ };
96
+ publish();
97
+ },
98
+ cancel() { if (dialog && !dialog.pending) {
99
+ dialog = null;
100
+ publish();
101
+ } },
102
+ async confirm() {
103
+ const owned = dialog;
104
+ if (!owned || owned.pending)
105
+ return;
106
+ if (!valid(owned)) {
107
+ owned.error = "The page or feedback changed. Cancel and review the current state before trying again.";
108
+ publish();
109
+ return;
110
+ }
111
+ owned.pending = true;
112
+ owned.error = "";
113
+ publish();
114
+ try {
115
+ // Flush the SDK debounce window before either destructive request.
116
+ await options.flush(owned.action);
117
+ if (!valid(owned, false))
118
+ throw new Error("The page changed while preparing this action. Cancel and try again on the current page.");
119
+ if (owned.action === "revert")
120
+ await options.revert();
121
+ else
122
+ await options.end();
123
+ if (!disposed && dialog === owned)
124
+ dialog = null;
125
+ }
126
+ catch (error) {
127
+ if (!disposed && dialog === owned) {
128
+ owned.error = error instanceof Error ? error.message : String(error);
129
+ options.failed(owned.error);
130
+ }
131
+ }
132
+ finally {
133
+ owned.pending = false;
134
+ publish();
135
+ }
136
+ },
137
+ async copy() {
138
+ const prompt = options.read().prompt;
139
+ if (!available() || !prompt || copyStatus === "copying")
140
+ return;
141
+ copyStatus = "copying";
142
+ copyError = "";
143
+ publish();
144
+ try {
145
+ await options.copy(prompt);
146
+ if (!disposed)
147
+ copyStatus = "copied";
148
+ }
149
+ catch {
150
+ if (!disposed) {
151
+ copyStatus = "failed";
152
+ copyError = "Couldn't copy — select the prompt and copy it manually";
153
+ options.failed(copyError);
154
+ }
155
+ }
156
+ publish();
157
+ },
158
+ },
159
+ dispose() { disposed = true; dialog = null; store.dispose(); },
160
+ };
161
+ }
@@ -153,6 +153,13 @@ export function createHistoryController({ request, changed = () => { }, refreshe
153
153
  state.targetKey = targetKey;
154
154
  if (selectedId)
155
155
  targets.set(selectedId, targetKey);
156
+ const comparison = round?.targets?.find((target) => target.key === targetKey)?.comparison || {};
157
+ const modes = ["content", "source"].filter((mode) => comparison[mode]?.available === true);
158
+ state.mode = modes.includes(state.preferredMode) ? state.preferredMode : modes[0] || "content";
159
+ const value = comparison[state.mode];
160
+ const items = value?.changes || value?.items ||
161
+ (state.mode === "content" ? comparison.changes || comparison.items : null) || [];
162
+ state.index = Math.max(0, Math.min(state.index, items.length - 1));
156
163
  state.loading = false;
157
164
  changed();
158
165
  refreshed();
package/lib/icons.js CHANGED
@@ -30,6 +30,74 @@ export const ICON_NODES = Object.freeze({
30
30
  }
31
31
  ]
32
32
  ],
33
+ "plus": [
34
+ [
35
+ "path",
36
+ {
37
+ "d": "M5 12h14"
38
+ }
39
+ ],
40
+ [
41
+ "path",
42
+ {
43
+ "d": "M12 5v14"
44
+ }
45
+ ]
46
+ ],
47
+ "minus": [
48
+ [
49
+ "path",
50
+ {
51
+ "d": "M5 12h14"
52
+ }
53
+ ]
54
+ ],
55
+ "locate": [
56
+ [
57
+ "line",
58
+ {
59
+ "x1": "2",
60
+ "x2": "5",
61
+ "y1": "12",
62
+ "y2": "12"
63
+ }
64
+ ],
65
+ [
66
+ "line",
67
+ {
68
+ "x1": "19",
69
+ "x2": "22",
70
+ "y1": "12",
71
+ "y2": "12"
72
+ }
73
+ ],
74
+ [
75
+ "line",
76
+ {
77
+ "x1": "12",
78
+ "x2": "12",
79
+ "y1": "2",
80
+ "y2": "5"
81
+ }
82
+ ],
83
+ [
84
+ "line",
85
+ {
86
+ "x1": "12",
87
+ "x2": "12",
88
+ "y1": "19",
89
+ "y2": "22"
90
+ }
91
+ ],
92
+ [
93
+ "circle",
94
+ {
95
+ "cx": "12",
96
+ "cy": "12",
97
+ "r": "7"
98
+ }
99
+ ]
100
+ ],
33
101
  "chevronDown": [
34
102
  [
35
103
  "path",
@@ -38,6 +106,14 @@ export const ICON_NODES = Object.freeze({
38
106
  }
39
107
  ]
40
108
  ],
109
+ "chevronRight": [
110
+ [
111
+ "path",
112
+ {
113
+ "d": "m9 18 6-6-6-6"
114
+ }
115
+ ]
116
+ ],
41
117
  "messageSquarePlus": [
42
118
  [
43
119
  "path",
@@ -0,0 +1,123 @@
1
+ import { createControllerStore } from "./controller-store.js";
2
+ import { decodePage, record } from "./chrome-api.js";
3
+ import { executionPresentation } from "./execution-client.js";
4
+ export function createRecoveryController(options) {
5
+ let disposed = false;
6
+ let busy = false;
7
+ let reloadBusy = false;
8
+ let menuOpen = false;
9
+ let restoreMenuFocus = true;
10
+ let failure = null;
11
+ const lifetime = new AbortController();
12
+ const matches = (a, b) => a.key === b.key && a.generation === b.generation && a.renderId === b.renderId;
13
+ function project() {
14
+ const context = options.read();
15
+ const view = executionPresentation(context.page, context.rendered, {
16
+ loading: context.loading, pendingReload: context.pendingReload, error: context.frameError,
17
+ });
18
+ const visible = !context.ended && !context.comparing;
19
+ const menuVisible = visible && (view.eligible || !!view.detail);
20
+ const error = failure && matches(failure.identity, context.identity) ? failure.message : null;
21
+ return {
22
+ menuVisible, menuOpen: menuVisible && menuOpen, restoreMenuFocus,
23
+ busy, canRecover: !disposed && !context.ended && !context.comparing && !busy && view.eligible && !!context.identity.key,
24
+ preference: view.preference, eligible: view.eligible, detail: view.detail,
25
+ reloadVisible: visible && context.reload.visible,
26
+ reloadMessage: context.reload.message, reloadError: context.reload.error,
27
+ canReload: !disposed && visible && context.reload.visible && !!context.identity.key && (!context.loading || !!context.frameError) && !reloadBusy,
28
+ reloadBusy,
29
+ status: visible && !context.reload.visible ? (context.frameError || error || view.status || (busy ? "Updating page recovery settings…" : "")) : "",
30
+ statusError: !!context.frameError || !!error,
31
+ };
32
+ }
33
+ const store = createControllerStore(project);
34
+ function setMenuOpen(open, { restoreFocus = true } = {}) {
35
+ if (disposed || (open && !project().menuVisible))
36
+ return;
37
+ if (menuOpen === open) {
38
+ // The iframe's interaction message can arrive after native dismissal.
39
+ if (!open && !restoreFocus) {
40
+ restoreMenuFocus = false;
41
+ store.publish();
42
+ }
43
+ return;
44
+ }
45
+ menuOpen = open;
46
+ restoreMenuFocus = restoreFocus;
47
+ store.publish();
48
+ options.menuChanged(open);
49
+ }
50
+ function publish() {
51
+ if (!project().menuVisible)
52
+ setMenuOpen(false, { restoreFocus: false });
53
+ return store.publish();
54
+ }
55
+ async function recover(preference) {
56
+ if (!project().canRecover)
57
+ return;
58
+ const identity = { ...options.read().identity };
59
+ busy = true;
60
+ failure = null;
61
+ setMenuOpen(false);
62
+ publish();
63
+ try {
64
+ const response = record(await options.request(`/api/session/${options.sessionId}/execution`, {
65
+ method: "POST", body: JSON.stringify({ key: identity.key, preference }), signal: lifetime.signal,
66
+ }));
67
+ const page = decodePage(response.page);
68
+ if (!disposed && matches(identity, options.read().identity)) {
69
+ if (page.key !== identity.key)
70
+ throw new Error("The recovery response belongs to a different page.");
71
+ options.changed(page);
72
+ }
73
+ // The server event remains the only owner of draft-safe frame replacement.
74
+ }
75
+ catch (error) {
76
+ if (!disposed && matches(identity, options.read().identity)) {
77
+ const message = error instanceof Error ? error.message : String(error);
78
+ failure = { identity, message: `Recovery could not be updated: ${message}. Retry from More.` };
79
+ options.failed(message);
80
+ }
81
+ }
82
+ finally {
83
+ busy = false;
84
+ publish();
85
+ }
86
+ }
87
+ async function reload() {
88
+ if (!project().canReload)
89
+ return;
90
+ reloadBusy = true;
91
+ publish();
92
+ try {
93
+ await options.reload();
94
+ }
95
+ catch (error) {
96
+ if (!disposed)
97
+ options.failed(error instanceof Error ? error.message : String(error));
98
+ }
99
+ finally {
100
+ reloadBusy = false;
101
+ publish();
102
+ }
103
+ }
104
+ return {
105
+ getSnapshot: store.getSnapshot, subscribe: store.subscribe, publish,
106
+ commands: {
107
+ setMenuOpen, recover, reload,
108
+ keepCurrent() {
109
+ if (disposed || !project().reloadVisible || reloadBusy)
110
+ return;
111
+ options.keepCurrent();
112
+ publish();
113
+ },
114
+ },
115
+ dispose() {
116
+ if (disposed)
117
+ return;
118
+ disposed = true;
119
+ lifetime.abort();
120
+ store.dispose();
121
+ },
122
+ };
123
+ }
@@ -1,9 +1,10 @@
1
1
  import { decodePage, record } from "./chrome-api.js";
2
+ import { createControllerStore } from "./controller-store.js";
2
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
4
  const state = {
4
5
  status: "idle", savedAt: "", baseHash: null, conflict: false, dirty: false, dynamic: false,
5
6
  };
6
- const listeners = new Set();
7
+ const store = createControllerStore(() => state);
7
8
  const pipelines = new Map();
8
9
  const backlogs = new Map();
9
10
  const errors = new Map();
@@ -19,8 +20,7 @@ export function createSaveController({ sessionId, current, policy, request, flus
19
20
  identity.key === current().key && identity.renderId === current().renderId &&
20
21
  identity.generation === current().generation;
21
22
  const publish = () => { if (!disposed)
22
- for (const listener of listeners)
23
- listener(); };
23
+ store.publish(); };
24
24
  const delay = (milliseconds) => new Promise((resolve) => {
25
25
  const timer = setTimer(() => { delays.delete(timer); resolve(); }, milliseconds);
26
26
  delays.set(timer, resolve);
@@ -171,12 +171,14 @@ export function createSaveController({ sessionId, current, policy, request, flus
171
171
  lastSave = { html, identity };
172
172
  pending++;
173
173
  state.dirty = true;
174
+ publish();
174
175
  activeSave = activeSave.then(async () => {
175
176
  if (!matches(identity) || state.conflict)
176
177
  return false;
177
178
  const saved = await saveHtml(html, identity);
178
179
  if (saved && sequence === started)
179
180
  state.dirty = false;
181
+ publish();
180
182
  return saved;
181
183
  }).finally(() => { pending--; applyClean(); });
182
184
  return activeSave;
@@ -204,6 +206,7 @@ export function createSaveController({ sessionId, current, policy, request, flus
204
206
  }
205
207
  if (policy() !== "writable" || state.dynamic)
206
208
  state.dirty = false;
209
+ publish();
207
210
  }
208
211
  async function captureStable(capture, signal) {
209
212
  const identity = current();
@@ -225,10 +228,8 @@ export function createSaveController({ sessionId, current, policy, request, flus
225
228
  }
226
229
  return {
227
230
  get state() { return state; },
228
- subscribe(listener) {
229
- listeners.add(listener);
230
- return () => { listeners.delete(listener); };
231
- },
231
+ getSnapshot: store.getSnapshot,
232
+ subscribe: store.subscribe,
232
233
  save, persistEdit, settleEdits, flush: flushEdits, barrier, captureStable,
233
234
  applyClean, queued, cancelRetries,
234
235
  async settled() { await activeSave; },
@@ -236,21 +237,23 @@ export function createSaveController({ sessionId, current, policy, request, flus
236
237
  markSaving() { state.dirty = true; state.status = "saving"; publish(); },
237
238
  markDynamic() { state.dynamic = true; publish(); },
238
239
  observeClean() { clean = { identity: current(), sequence, editSequence }; applyClean(); },
239
- baseline(hash) { state.baseHash = hash; },
240
+ baseline(hash) { state.baseHash = hash; publish(); },
240
241
  hold() {
241
242
  state.baseHash = null;
242
243
  cancelRetries();
243
244
  if (state.dirty)
244
245
  state.conflict = true;
245
246
  send({ type: "eh:abortSave" });
247
+ publish();
246
248
  },
247
- markConflict() { state.conflict = true; },
249
+ markConflict() { state.conflict = true; publish(); },
248
250
  reset() {
249
251
  cancelRetries();
250
252
  clean = null;
251
253
  lastSave = null;
252
254
  sequence++;
253
255
  Object.assign(state, { status: "idle", savedAt: "", baseHash: null, conflict: false, dirty: false, dynamic: false });
256
+ publish();
254
257
  },
255
258
  async revert() {
256
259
  const identity = current();
@@ -294,7 +297,7 @@ export function createSaveController({ sessionId, current, policy, request, flus
294
297
  return;
295
298
  disposed = true;
296
299
  cancelRetries();
297
- listeners.clear();
300
+ store.dispose();
298
301
  },
299
302
  };
300
303
  }
package/lib/server.js CHANGED
@@ -646,9 +646,9 @@ export function createServer({ store: suppliedStore, storeOptions, owner = null,
646
646
  return undefined;
647
647
  // --- static chrome assets
648
648
  if (route === "/chrome.css")
649
- return serveFile(res, path.join(here, "chrome.css"));
649
+ return serveFile(res, path.join(here, "ui", "chrome.css"));
650
650
  if (route === "/chrome.js")
651
- return serveFile(res, path.join(here, "chrome-client.js"));
651
+ return serveFile(res, path.join(here, "ui", "chrome.js"));
652
652
  if (route === "/chrome-session.js")
653
653
  return serveFile(res, path.join(here, "chrome-session.js"));
654
654
  if (route === "/chrome-api.js")
@@ -663,6 +663,8 @@ export function createServer({ store: suppliedStore, storeOptions, owner = null,
663
663
  return serveFile(res, path.join(here, "save-controller.js"));
664
664
  if (route === "/feedback-controller.js")
665
665
  return serveFile(res, path.join(here, "feedback-controller.js"));
666
+ if (route === "/controller-store.js")
667
+ return serveFile(res, path.join(here, "controller-store.js"));
666
668
  if (route === "/review-controller.js")
667
669
  return serveFile(res, path.join(here, "review-controller.js"));
668
670
  if (route === "/icons.js")
@@ -695,8 +697,6 @@ export function createServer({ store: suppliedStore, storeOptions, owner = null,
695
697
  return serveFile(res, path.join(here, "history-client.js"));
696
698
  if (route === "/view-identity.js")
697
699
  return serveFile(res, path.join(here, "view-identity.js"), opaqueModuleCors(req));
698
- if (route === "/comparison-view.js")
699
- return serveFile(res, path.join(here, "comparison-view.js"));
700
700
  if (route === "/history-coordinator.js")
701
701
  return serveFile(res, path.join(here, "history-coordinator.js"));
702
702
  if (route === "/execution-client.js")
@@ -0,0 +1,32 @@
1
+ import { createControllerStore } from "./controller-store.js";
2
+ export function createToolbarController(read, commands) {
3
+ const store = createControllerStore(read);
4
+ return {
5
+ ...store,
6
+ commands: {
7
+ setComparing(comparing) {
8
+ if (!read().ended)
9
+ commands.setComparing(comparing);
10
+ },
11
+ setMode(mode) {
12
+ const state = read();
13
+ if (!state.ended && !state.modeDisabled)
14
+ commands.setMode(mode);
15
+ },
16
+ setModeMenu(open) {
17
+ const state = read();
18
+ if (!state.ended && (!open || !state.modeDisabled))
19
+ commands.setModeMenu(open);
20
+ },
21
+ openComments() {
22
+ const state = read();
23
+ if (!state.ended && !state.comparing)
24
+ commands.openComments();
25
+ },
26
+ toggleTheme() {
27
+ if (!read().ended)
28
+ commands.toggleTheme();
29
+ },
30
+ },
31
+ };
32
+ }