@myrtilleio/redline 0.1.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/client/boot.js ADDED
@@ -0,0 +1,146 @@
1
+ // ---------------------------------------------------------------- boot
2
+
3
+ function boot() {
4
+ document.documentElement.append(host);
5
+ // Photographed by the server: set the scene for one shot, no UI, no live updates.
6
+ if (SHOT) return runShot(SHOT);
7
+ if (FRAME) initFrame();
8
+ // A shared Check link (?redline=check): review only, for this browsing session.
9
+ if (new URLSearchParams(location.search).get('redline') === 'check') sessionPref('review', 'check');
10
+ state.reviewOnly = sessionPref('review') === 'check';
11
+ const resumed = sessionPref('mode');
12
+ if (!state.reviewOnly && (resumed === 'select' || resumed === 'pen')) state.mode = resumed;
13
+ renderBar();
14
+ // ?redline=off opens the page clean (for a link you share); the backslash key during the session is remembered.
15
+ const off = new URLSearchParams(location.search).get('redline') === 'off';
16
+ if (off || sessionPref('preview')) setPreviewMode(true, { remember: !off });
17
+ // The toolbar rises in once per browsing session, not on every live reload.
18
+ if (!sessionPref('seen') && !FRAME) {
19
+ bar.classList.add('intro');
20
+ sessionPref('seen', '1');
21
+ }
22
+ // Opened from a review link: go straight to the fixes. In the demo, say where you are first.
23
+ load().then(async () => {
24
+ if (state.reviewOnly && !FRAME && toCheck().length) return startCheck();
25
+ if (FRAME || state.reviewOnly) return;
26
+ if (await isDemo()) return showDemoIntro();
27
+ if (!prefs('coached')) showCoach();
28
+ });
29
+ watchRoutes();
30
+ store.subscribe({
31
+ annotations: load,
32
+ status(ok) {
33
+ if (state.connected !== ok) {
34
+ state.connected = ok;
35
+ renderBar();
36
+ if (ok) load();
37
+ }
38
+ },
39
+ reload() {
40
+ // The agent changed the site. Reload now, or right after the open note is saved.
41
+ if (state.composer && !state.composer.untouched()) {
42
+ state.pendingReload = true;
43
+ toast('Page updated — it will reload after you save this note');
44
+ } else location.reload();
45
+ },
46
+ });
47
+ // Keep the overlay last in the DOM, even if the page's framework re-renders <html>.
48
+ new MutationObserver(() => {
49
+ if (!host.isConnected) document.documentElement.append(host);
50
+ }).observe(document.documentElement, { childList: true });
51
+ }
52
+
53
+ async function isDemo() {
54
+ if (!SERVER) return false;
55
+ try {
56
+ return !!(await (await fetch(SERVER + '/__redline/api/info', { headers: keyHeader() })).json()).demo;
57
+ } catch {
58
+ return false;
59
+ }
60
+ }
61
+
62
+ // The demo starts in the middle of a story; tell it in three lines before anything
63
+ // asks for attention: what already happened, and the two things to do next.
64
+ function showDemoIntro() {
65
+ if (sessionPref('demoIntro')) return;
66
+ const n = toCheck().length;
67
+ if (!n) return;
68
+ toastEl.classList.remove('show'); // the intro already says the round is back
69
+ const close = () => {
70
+ sessionPref('demoIntro', '1');
71
+ intro.classList.add('out');
72
+ setTimeout(() => intro.remove(), 160);
73
+ };
74
+ const intro = h('div', { class: 'demo-intro', role: 'dialog', 'aria-label': 'About this demo' },
75
+ h('span', { class: 'demo-tag' }, 'Demo'),
76
+ h('h2', {}, `Your agent is back with ${n} fixes`),
77
+ h('p', {}, `In this demo, you already marked ${n} things on this page and sent them to your coding agent. It changed the code. Now you decide, one fix at a time: accept it, or ask for a redo.`),
78
+ h('div', { class: 'demo-actions' },
79
+ h('button', { class: 'btn', onclick: () => (close(), setMode('pen')) }, 'Draw my own first'),
80
+ h('button', { class: 'btn primary', onclick: () => (close(), startCheck()) }, `Review ${n} fixes`),
81
+ ),
82
+ );
83
+ root.append(intro);
84
+ intro.querySelector('.btn.primary').focus({ preventScroll: true });
85
+ }
86
+
87
+ // First visit: one friendly line on how to start, then never again. It goes
88
+ // away as soon as they pick a tool (see setMode) or press Got it.
89
+ let coachEl = null;
90
+ function closeCoach() {
91
+ if (!coachEl) return;
92
+ prefs('coached', true);
93
+ coachEl.remove();
94
+ coachEl = null;
95
+ }
96
+
97
+ function showCoach() {
98
+ const close = closeCoach;
99
+ const coach = (coachEl = h('div', { class: 'coach', role: 'note' },
100
+ // The whole loop in three steps; everything else can wait until it's needed.
101
+ h('span', { class: 'step' }, h('kbd', {}, 'P'), ' draw on it, or click to say why'),
102
+ h('span', { class: 'step' }, h('b', {}, 'Send'), ' to your agent'),
103
+ h('span', { class: 'step' }, h('b', {}, 'Review'), ' what comes back'),
104
+ h('button', { class: 'dismiss', onclick: close }, 'Got it'),
105
+ ));
106
+ root.append(coach);
107
+ }
108
+
109
+ // Single-page apps change the URL without reloading: follow them, so notes go to
110
+ // the right page and each route shows its own pins.
111
+ function watchRoutes() {
112
+ const check = () => {
113
+ const next = pagePath();
114
+ if (next === PAGE) return;
115
+ PAGE = next;
116
+ closeComposer();
117
+ showHover(null);
118
+ load();
119
+ };
120
+ for (const method of ['pushState', 'replaceState']) {
121
+ const original = history[method];
122
+ history[method] = function (...args) {
123
+ const result = original.apply(this, args);
124
+ queueMicrotask(check);
125
+ return result;
126
+ };
127
+ }
128
+ addEventListener('popstate', check);
129
+ addEventListener('hashchange', check);
130
+ setInterval(check, 1000); // routers that hold their own reference to pushState
131
+ }
132
+
133
+ window.__redline = {
134
+ version: 1,
135
+ server: SERVER,
136
+ get page() {
137
+ return PAGE;
138
+ },
139
+ setMode,
140
+ setVerdict,
141
+ annotations: () => state.annotations.slice(),
142
+ markdown: async () => formatMarkdown(await allAnnotations(), { cli: !!SERVER, includeDrafts: true }),
143
+ };
144
+
145
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot);
146
+ else boot();