@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.
- 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/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 +4881 -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
|
@@ -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
|
+
}
|
package/lib/save-controller.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
229
|
-
|
|
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
|
-
|
|
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
|
|
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
|
+
}
|