create-pmos 0.151.0 → 0.153.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.
@@ -1,24 +1,38 @@
1
- /* pm/c3-doc-sheet.js — the DOCUMENT SHEET of /c3.html Initiatives > Documents (console3.html: docPanel line 4090, dpHead 4089, the diff 4075 and 4076, the sheet 4104,
2
- * the handlers dmode, dask, dsave 4136 to 4143, relHtml 4180 to 4190). The right-hand sheet from c3-sheet.js, up to 780px, a bottom sheet on a phone.
1
+ /* pm/c3-doc-sheet.js — the DOCUMENT SHEET of /c3.html (Initiatives › Documents, an initiative's Docs, Reports). The right-hand sheet from
2
+ * c3-sheet.js, up to 780px, a bottom sheet on a phone. Initiative c3-doc-sheet-rich: the approved mockup (planning/prd/c3-doc-sheet-rich-prd.md).
3
3
  *
4
- * openDocSheet({ ws, doc, mode, prompt, relations(doc, openOther), onChanged }) doc = { path, name, ty, v, st, ini }
4
+ * openDocSheet({ ws, doc, mode, prompt, relations(doc, openOther), onChanged, generated, secExtra }) doc = { path, name, ty, v, st, ini, iniName }
5
5
  *
6
- * Six modes (the segment under the header): Read (one `.dsec` per heading with who changed it last), Edit by hand (sections become editable; a WHY is required;
7
- * POST /api/doc-sheet/edit), Ask the agent (the word diff of what the agent proposes and Accept as a slide bound to the proposal's hash: POST /api/doc-sheet/accept),
8
- * History (GET /api/doc-sheet/revisions and /at: the revisions and a compare of any one with the one before), Health (POST /api/doc-health/check: the six checks, N of M),
9
- * Relations (the document graph around this document, from the library's edges).
10
- * Ask the agent (page-starts-agent): in real mode the button starts a background agent run from the page (POST /api/doc-sheet/ask). The run may only read; its
11
- * answer is recorded as a proposal (GET /api/doc-sheet/ask?id= is polled until it is), and the proposal is accepted only by the PM's slide, bound to its hash.
12
- * Copy the request stays as the way to give it to a session the PM already has. The demo (?demo=1) answers the propose and accept routes from fixtures.
13
- * Nothing is invented: an edit writes the file through the route, and every save keeps its why in the revisions.
14
- * Pure parts (Node-testable): splitDoc, joinDoc, changedSections, wordDiffTokens, byName, dayName. */
6
+ * Six modes (the segment under the header; the keys are kept for the callers):
7
+ * read Read the markdown rendered (pm/c3-md.js): the front matter as properties, each section with who changed it last (from the
8
+ * revisions), "Show who wrote what", the Markdown source with a copy button, a Contents menu, the stale banner, and the
9
+ * open proposals pinned to the sections they change, with "Show change" for the diff in place.
10
+ * edit Edit a rich-text editor of the whole document (toolbar, Rich | Markdown over the same draft), the front matter as fields; a
11
+ * changed section is marked; saving needs a reason (POST /api/doc-sheet/edit). A section nobody touched is written back
12
+ * exactly as it was, so opening and saving without a change writes nothing.
13
+ * ask Proposals Ask the agent (POST /api/doc-sheet/ask: a background run that may only read; its answer becomes a proposal), the open
14
+ * proposals (GET /api/doc-sheet/proposals) with a tick per section, a rendered diff and the slide that accepts the ticked
15
+ * sections (POST /api/doc-sheet/accept with `sections`), Reject with a reason (POST /api/doc-sheet/reject), and the closed ones.
16
+ * hist Edit log every revision and event (GET /api/doc-sheet/log): who wrote the text, how it came in (this sheet, a proposal, outside
17
+ * PMOS with its commit), the reason, the sections, the words added and removed, Show the change, and a reason for an outside
18
+ * change that has none (POST /api/doc-sheet/why).
19
+ * health Health POST /api/doc-health/check: the checks, N of M; a failing Fresh check lists the sources that changed, and "Mark as still
20
+ * current" records a reason (POST /api/doc-sheet/mark-current).
21
+ * links Relations the document graph around this document, from the caller.
22
+ * Nothing is invented: a part with no data says so. The demo (?demo=1) answers the routes from fixtures.
23
+ *
24
+ * Two options for a report, which is a document with no stored file yet (report-families-view.js):
25
+ * generated = { text, why, health } Read shows the generated text; Edit and Proposals say why they need a stored document; Health shows `health`.
26
+ * secExtra(name) a node under each section in Read: the report's claims and where they come from.
27
+ * Pure parts (Node-testable): splitDoc, joinDoc, changedSections, wordDiffTokens, byName, dayName, parseFront, setFront, composeSections. */
15
28
  import { el } from './dom.js';
16
29
  import { pmGet, pmPost } from './http.js';
17
- import { c3ic, c3ib } from './pm-work-view.js';
30
+ import { c3ic } from './pm-work-view.js';
18
31
  import { openSheet } from './c3-sheet.js';
19
- import { renderCase } from './c3-case.js';
20
32
  import { say } from './c3-init-extras.js';
21
33
  import { c3btn } from './page-agent.js';
34
+ import { mountSlide } from './slide-confirm.js';
35
+ import { parseMd, toMd, canonMd, diffMd, toDom, fromDom, wordCounts, safeHref } from './c3-md.js';
22
36
 
23
37
  /* ── pure: sections, round trip, word diff ── */
24
38
 
@@ -49,12 +63,12 @@ export function joinDoc(secs, edits) {
49
63
  /** The sections of `after` that differ from `before`, by name: [{ name, before, after, kind: 'changed'|'added'|'removed' }]. */
50
64
  export function changedSections(before, after) {
51
65
  const A = splitDoc(before); const B = splitDoc(after); const out = [];
52
- B.forEach((s) => { const p = A.find((q) => q.name === s.name); if (!p) out.push({ name: s.name, before: '', after: s.text, kind: 'added' }); else if (p.text !== s.text) out.push({ name: s.name, before: p.text, after: s.text, kind: 'changed' }); });
66
+ B.forEach((s) => { const p = A.find((q) => q.name === s.name); if (!p) out.push({ name: s.name, before: '', after: s.text, kind: 'added' }); else if (p.text !== s.text || p.prefix !== s.prefix) out.push({ name: s.name, before: p.text, after: s.text, kind: 'changed' }); });
53
67
  A.forEach((q) => { if (!B.some((s) => s.name === q.name)) out.push({ name: q.name, before: q.text, after: '', kind: 'removed' }); });
54
68
  return out;
55
69
  }
56
70
 
57
- /** The word diff (the mockup's wdiff, line 4065): [{ op: 'same'|'add'|'del', text }], whitespace kept in the tokens. */
71
+ /** The word diff: [{ op: 'same'|'add'|'del', text }], whitespace kept in the tokens. */
58
72
  export function wordDiffTokens(a, b) {
59
73
  const x = String(a || '').split(/(\s+)/).filter((s) => s !== ''); const y = String(b || '').split(/(\s+)/).filter((s) => s !== '');
60
74
  const n = x.length; const m = y.length;
@@ -70,7 +84,7 @@ export function wordDiffTokens(a, b) {
70
84
  }
71
85
 
72
86
  /** Who a ledger `by` is, as the page says it. */
73
- export const byName = (by) => { const b = String(by || ''); return /^(agent|claude)/i.test(b) ? 'Agent' : /^(pm|human:wawan|you)$/i.test(b) || b === '' ? 'You' : b.replace(/^human:/, ''); };
87
+ export const byName = (by) => { const b = String(by || ''); return /^(agent|claude)/i.test(b) ? 'Agent' : /^(pm|human:wawan|human:pm|you)$/i.test(b) || b === '' ? 'You' : b.replace(/^human:/, ''); };
74
88
  const WD = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; const MON = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
75
89
  /** '2026-10-05T09:00:00Z' to 'Mon 5 Oct'. Anything that is not a date comes back as it is. */
76
90
  export function dayName(v) {
@@ -78,60 +92,115 @@ export function dayName(v) {
78
92
  const d = new Date(Date.UTC(+m[1], +m[2] - 1, +m[3]));
79
93
  return WD[d.getUTCDay()] + ' ' + (+m[3]) + ' ' + MON[+m[2] - 1];
80
94
  }
81
- const nextV = (v) => { const m = /^v(\d+)$/.exec(v || ''); return m ? 'v' + (+m[1] + 1) : 'v1'; };
95
+ const timeOf = (v) => { const m = /T(\d{2}):(\d{2})/.exec(String(v || '')); return m ? m[1] + ':' + m[2] : ''; };
96
+ const whenOf = (v) => dayName(v) + (timeOf(v) ? ', ' + timeOf(v) : '');
97
+ const nextV = (v) => { const m = /^v(\d+)$/.exec(v || ''); return m ? 'v' + (+m[1] + 1) : ''; };
98
+
99
+ /** The front matter of a text: { block, rest, fields: [{ key, value, line, editable, quote }] } or null. `block` is the `---` … `---` lines
100
+ * without the newline after them; `rest` is everything after. A field is editable when it is one line with a plain value. */
101
+ export function parseFront(text) {
102
+ const t = String(text == null ? '' : text);
103
+ const m = /^---\n([\s\S]*?)\n---(?=\n|$)/.exec(t);
104
+ if (!m) return null;
105
+ const fields = []; const lines = m[1].split('\n');
106
+ lines.forEach((l, line) => {
107
+ const f = /^([A-Za-z_][\w.-]*):[ \t]*(.*)$/.exec(l);
108
+ if (f) {
109
+ const raw = f[2]; const q = /^"(.*)"\s*$/.exec(raw) || /^'(.*)'\s*$/.exec(raw);
110
+ const multi = raw === '' || /^[|>]/.test(raw);
111
+ fields.push({ key: f[1], value: q ? (raw[0] === '"' ? q[1].replace(/\\"/g, '"').replace(/\\\\/g, '\\') : q[1].replace(/''/g, "'")) : raw, line, editable: !multi && !/^[[{]/.test(raw), quote: q ? raw[0] : '' });
112
+ } else if (fields.length && /^\s+(-\s+)?\S/.test(l)) {
113
+ const last = fields[fields.length - 1];
114
+ last.value = (last.value ? last.value + ', ' : '') + l.trim().replace(/^-\s+/, '');
115
+ last.editable = false;
116
+ }
117
+ });
118
+ return { block: m[0], rest: t.slice(m[0].length), fields };
119
+ }
120
+ /** The front matter block with `values` ({ key: value }) written into its editable fields; every other line stays as it was. */
121
+ export function setFront(block, values) {
122
+ const p = parseFront(block); if (!p) return block;
123
+ const inner = block.replace(/^---\n/, '').replace(/\n---$/, '').split('\n');
124
+ for (const f of p.fields) {
125
+ if (!f.editable || !Object.prototype.hasOwnProperty.call(values, f.key) || values[f.key] === f.value) continue;
126
+ const v = String(values[f.key]);
127
+ const needs = /^[\s"'#&*!|>%@`[{-]|:\s|\s#|\s$/.test(v) || v === '';
128
+ const out = f.quote === "'" ? "'" + v.replace(/'/g, "''") + "'" : f.quote === '"' || needs ? JSON.stringify(v) : v;
129
+ inner[f.line] = f.key + ': ' + out;
130
+ }
131
+ return '---\n' + inner.join('\n') + '\n---';
132
+ }
133
+
134
+ /** The new text of a document from the editor's sections. `base` is splitDoc of the text the editor was drawn from; each group is
135
+ * { sid, level, name, md } where sid is the index of the base section it was drawn from (or null for a new one) and md is the markdown
136
+ * the editor holds now. `intro` is the markdown before the first heading, `front` the front matter block (or null).
137
+ * A section whose heading and canonical text did not change is written back exactly as it was. */
138
+ export function composeSections(base, groups, intro, front) {
139
+ const levelOf = (s) => (/^(#+)/.exec(s.prefix || '') || ['', ''])[1].length;
140
+ const out = [];
141
+ const b0 = base.find((s) => s.prefix === '');
142
+ if (b0 || intro || front) {
143
+ const fm = b0 ? parseFront(b0.text) : null;
144
+ const restOrig = fm ? fm.rest : b0 ? b0.text : '';
145
+ const same = canonMd(restOrig) === canonMd(intro || '');
146
+ const body = same ? restOrig : (intro ? (front ? '\n\n' : '') + intro : '');
147
+ const text = (front || '') + body;
148
+ if (text || (b0 && (b0.lead || b0.tail))) out.push({ prefix: '', lead: b0 ? b0.lead : '', text, tail: b0 ? b0.tail : '\n\n' });
149
+ }
150
+ const used = new Set();
151
+ for (const g of groups) {
152
+ const B = g.sid != null && !used.has(g.sid) ? base[g.sid] : null;
153
+ if (B) used.add(g.sid);
154
+ const sameHead = B && B.name === g.name && levelOf(B) === g.level;
155
+ const sameText = B && canonMd(B.text) === canonMd(g.md);
156
+ out.push({ bi: B ? g.sid : null, prefix: sameHead ? B.prefix : '#'.repeat(g.level || 2) + ' ' + g.name + '\n', lead: B ? B.lead : (g.md ? '\n' : ''), text: sameText ? B.text : g.md, tail: B ? B.tail : '\n\n' });
157
+ }
158
+ if (out.length && out[0].prefix === '' && b0) out[0].bi = b0.i;
159
+ // where two parts were not next to each other before, the first ends in a blank line; a new last part ends in one newline
160
+ out.forEach((p, k) => {
161
+ const s = p.prefix + p.lead + p.text + p.tail;
162
+ if (k < out.length - 1) {
163
+ if (p.bi != null && out[k + 1].bi === p.bi + 1) return;
164
+ if (!/\n[ \t]*\n$/.test(s)) p.tail = p.tail.replace(/[ \t]*$/, '') + (/\n$/.test(s) ? '\n' : '\n\n');
165
+ } else if (p.bi !== base.length - 1 || p.bi == null) p.tail = p.tail.replace(/\s*$/, '') + '\n';
166
+ });
167
+ return out.map((p) => p.prefix + p.lead + p.text + p.tail).join('');
168
+ }
82
169
 
83
170
  /* ── the sheet ── */
84
171
 
85
172
  const NAMES = { fresh: 'Fresh', decisions: 'Matches decisions', data: 'Matches the data', complete: 'Complete', traced: 'Traced to a goal and tickets', readers: 'Readers have this version' };
86
- const hcol = (h) => (!h || h.total === 0 ? 'var(--muted-foreground)' : h.passed === h.total ? 'var(--success)' : h.total - h.passed === 1 ? 'var(--warning)' : 'var(--destructive)');
87
- const STATE = { current: ['Current', 'var(--success)', 'check'], stale: ['Stale', 'var(--warning)', 'alert'], draft: ['Draft', 'var(--acc)', 'clock'], missing: ['Missing', 'var(--destructive)', 'plus'] };
88
- const TABS = [['read', 'eye', 'Read'], ['edit', 'notes', 'Edit by hand'], ['ask', 'spark', 'Ask the agent'], ['hist', 'clock', 'History'], ['health', 'shield', 'Health'], ['links', 'init', 'Relations']];
173
+ const STATE = { current: ['Current', 'ds-good', 'check'], stale: ['Stale', 'ds-warn', 'alert'], draft: ['Draft', 'ds-pe', 'clock'], missing: ['Missing', 'ds-bad', 'plus'] };
174
+ const TABS = [['read', 'eye', 'Read'], ['edit', 'edit', 'Edit'], ['ask', 'spark', 'Proposals'], ['hist', 'clock', 'Edit log'], ['health', 'shield', 'Health'], ['links', 'init', 'Relations']];
175
+ const MODE = { proposals: 'ask', props: 'ask', log: 'hist', history: 'hist' };
89
176
  const isDemo = () => !!window.__c3demo;
90
- const q = (o) => Object.entries(o).map(([k, v]) => k + '=' + encodeURIComponent(v)).join('&');
177
+ const q = (o) => Object.entries(o).filter(([, v]) => v != null && v !== '').map(([k, v]) => k + '=' + encodeURIComponent(v)).join('&');
178
+ const plural = (n, w) => n + ' ' + w + (n === 1 ? '' : 's');
179
+ const esc = (s) => String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
180
+ const hcls = (h) => (!h || h.total === 0 ? '' : h.passed === h.total ? 'ds-good' : h.total - h.passed === 1 ? 'ds-warn' : 'ds-bad');
181
+ const isAgentRev = (r) => !!r && (r.text_by ? r.text_by === 'agent' : byName(r.by) === 'Agent');
182
+ const md = (text, extra) => el('div', { class: 'ds-md' + (extra ? ' ' + extra : '') }, toDom(parseMd(text), document));
183
+ const btn = (cls, attrs, kids, onClick) => c3btn(cls, attrs, kids, onClick);
184
+ const headLevel = (s) => (/^(#+)/.exec(s.prefix || '') || ['', ''])[1].length;
91
185
 
92
- /** The mockup's agentDraft (line 4077): a deterministic rewrite of the sections for the demo's request box. Real mode has no agent behind the page. */
93
- function demoDraft(secs, prompt) {
94
- const p = prompt.toLowerCase(); const out = secs.map((s) => ({ name: s.name, text: s.text })); let did = false;
95
- if (/flow|scope|cut|three|3/.test(p)) out.forEach((s) => { if (s.name === 'Scope') { s.text = 'Three flows ship together: one-page address entry, wallet pay and guest checkout. Saved-card removal moves to the next release.'; did = true; } if (s.name === 'Risks' && /audit/i.test(s.text)) s.text += ' The cut frees the 21 Oct audit slot.'; });
96
- if (/growth|forecast|6%|number|assumption/.test(p)) out.forEach((s) => { if (/4%/.test(s.text) && s.name === 'Goal') { s.text = s.text.replace('Growth assumption 4%', 'Growth assumption 6% (Q4 forecast, row 14), pending your confirmation'); did = true; } });
97
- if (/summar|tl;dr|short/.test(p)) { out.unshift({ name: 'Summary', text: 'One page: raise conversion to 4.0% by fixing the address step, removing saved cards and shipping wallet pay and guest checkout.' }); did = true; }
98
- if (/leadership|note|change/.test(p)) { out.push({ name: 'Change note', text: 'The next version changes the scope after the 8 Oct decision. Everything else is unchanged.' }); did = true; }
99
- if (!did) out.push({ name: 'Agent note', text: 'Drafted from your request: ' + prompt.slice(0, 160) });
100
- return out;
101
- }
102
- /** The draft's sections back into the document's text: a changed section is replaced in place, a new Summary goes first, any other new section goes last. */
103
- const sectionsToText = (before, drafted) => {
104
- const A = splitDoc(before); const names = new Set(A.map((s) => s.name)); const edits = {};
105
- A.forEach((s) => { const d = drafted.find((x) => x.name === s.name); if (d && d.text !== s.text) edits[s.i] = d.text; });
106
- let text = joinDoc(A, edits);
107
- for (const d of drafted) {
108
- if (names.has(d.name)) continue;
109
- if (d.name === 'Summary') text = '## Summary\n' + d.text + '\n\n' + text;
110
- else text = text.replace(/\s*$/, '\n\n') + '## ' + d.name + '\n' + d.text + '\n';
111
- }
112
- return text;
113
- };
114
-
115
- /* Two additive options for a report, which is a document in the mockup (console3.html r2doc):
116
- * generated = { text, why, health } the report has no stored document yet. The sheet shows the generated sections read-only; Edit and Ask say why they
117
- * need a stored document (why), History says there is none, Health shows `health` ({ passed, total, checks }) as given.
118
- * secExtra(name) a node under each section in Read: the report's claims and where they come from. */
119
186
  export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, onChanged, generated, secExtra }) {
120
- const S = { draft: {}, mode, prompt, text: null, err: '', revs: null, texts: {}, cmp: null, health: null, hAsk: false, prop: null, busy: false, changed: false, why: '', metaFor: null, asking: null, closed: false };
187
+ const S = { mode: MODE[mode] || mode, prompt, text: null, err: '', revs: null, texts: {}, health: null, hAsk: false, busy: false, changed: false,
188
+ meta: null, metaFor: null, props: null, propsErr: '', log: null, logErr: '', asking: null, closed: false, who: false, src: false, preview: {},
189
+ pick: {}, rejFor: null, rejWhy: '', whyFor: null, whyText: '', staleWhy: null, logF: 'all', cmp: null, cmpNode: null, flashRev: null, ed: null, leave: null };
121
190
  const demo = isDemo();
122
191
  const base = { ws, path: doc.path };
123
192
  let ui = null;
124
193
 
125
- /* data */
194
+ /* ── data ── */
126
195
  const loadText = async () => {
127
196
  if (generated) { S.text = String(generated.text || ''); S.err = ''; return; }
128
197
  try { const r = await fetch('/api/doc?' + q({ ws, path: doc.path })); if (!r.ok) throw new Error((await r.text()) || 'could not read the document'); S.text = await r.text(); S.err = ''; }
129
198
  catch (e) { S.text = null; S.err = (e && e.message) || 'could not read the document'; }
130
199
  };
131
200
  const loadRevs = async () => {
132
- if (generated) { S.revs = []; S.revsErr = generated.why; return; }
201
+ if (generated) { S.revs = []; return; }
133
202
  const j = await pmGet('/api/doc-sheet/revisions?' + q(base)).catch(() => null);
134
- S.revs = j && j.ok ? j.revisions : []; S.revsErr = j && j.ok ? '' : (j && j.error) || 'The history could not be read.';
203
+ S.revs = j && j.ok ? j.revisions : [];
135
204
  };
136
205
  const textAt = async (rev) => {
137
206
  if (S.texts[rev] != null) return S.texts[rev];
@@ -139,12 +208,23 @@ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, o
139
208
  S.texts[rev] = j && j.ok ? j.text : null; return S.texts[rev];
140
209
  };
141
210
  const loadHealth = async () => {
142
- S.hAsk = true; draw();
211
+ S.hAsk = true;
143
212
  if (generated) { S.health = generated.health || { passed: 0, total: 0, checks: [], err: generated.why }; drawBg(); return; }
144
- const r = await pmPost('/api/doc-health/check', base);
213
+ const r = await pmPost('/api/doc-health/check', base).catch(() => null);
145
214
  S.health = r && r.ok && r.health ? r.health : { passed: 0, total: 0, checks: [], err: (r && r.error) || 'The checks could not run.' }; drawBg();
146
215
  };
147
- /* who changed each section last: needs the text of every revision (a few, fetched once) */
216
+ const loadProps = async () => {
217
+ if (generated) { S.props = []; return; }
218
+ const j = await pmGet('/api/doc-sheet/proposals?' + q(base)).catch(() => null);
219
+ S.props = j && j.ok ? j.proposals || [] : []; S.propsErr = j && j.ok ? '' : (j && j.error) || 'The proposals could not be read.';
220
+ };
221
+ const loadLog = async () => {
222
+ if (generated) { S.log = { revisions: [], events: [] }; return; }
223
+ const j = await pmGet('/api/doc-sheet/log?' + q(base)).catch(() => null);
224
+ if (j && j.ok) { S.log = { revisions: j.revisions || [], events: j.events || [] }; S.logErr = ''; }
225
+ else { S.log = { revisions: S.revs || [], events: [] }; S.logErr = j && j.error ? j.error : ''; }
226
+ };
227
+ /* who changed each section last: the text of the last revisions (a few, fetched once) */
148
228
  const loadMeta = async () => {
149
229
  if (!S.revs || !S.revs.length || S.metaFor === S.revs.length) return;
150
230
  S.metaFor = S.revs.length;
@@ -154,190 +234,512 @@ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, o
154
234
  rs.forEach((r, i) => { if (texts[i] == null) return; const cur = splitDoc(texts[i]); const prev = i ? splitDoc(texts[i - 1] || '') : []; cur.forEach((s) => { const p = prev.find((x) => x.name === s.name); if (!p || p.text !== s.text) meta[s.name] = r; }); });
155
235
  S.meta = meta; drawBg();
156
236
  };
237
+ const reloadAll = async () => {
238
+ S.revs = null; S.meta = null; S.metaFor = null; S.texts = {}; S.health = null; S.hAsk = false; S.log = null;
239
+ await loadText(); draw();
240
+ await Promise.all([loadRevs(), loadProps(), loadHealth()]); loadMeta(); if (S.mode === 'hist') { await loadLog(); } draw();
241
+ };
157
242
 
158
- /* ── drawing ── */
159
- const head = () => {
160
- const st = STATE[doc.st] || STATE.current; const h = S.health;
161
- return el('div', { class: 'chips2', style: 'flex-wrap:wrap', 'data-doc-meta': '' },
162
- el('span', { class: 'chip2', style: 'color:' + st[1] }, c3ic(st[2], 'sm'), st[0]), el('span', { class: 'chip2' }, doc.ty || 'Document'), doc.v ? el('span', { class: 'chip2 mono' }, doc.v) : null,
163
- h && h.total ? el('span', { class: 'chip2', style: 'color:' + hcol(h) }, c3ic('shield', 'sm'), h.passed + ' of ' + h.total + ' checks') : null,
164
- el('span', { class: 'chip2' }, doc.iniName || doc.ini || 'Portfolio'));
243
+ /* ── drawing helpers ── */
244
+ const openProps = () => (S.props || []).filter((p) => p.state === 'proposed');
245
+ const failing = () => (S.health && !S.health.err ? S.health.checks.filter((c) => c.pass === false) : []);
246
+ const fresh = () => (S.health && !S.health.err ? S.health.checks.find((c) => c.id === 'fresh' && c.pass === false) : null);
247
+ const lastRev = () => (S.revs && S.revs.length ? S.revs[S.revs.length - 1] : null);
248
+ const whoOf = (r) => {
249
+ if (!r) return '';
250
+ if (isAgentRev(r)) return 'Agent' + (r.via === 'proposal' || r.by === 'pm' ? ' · accepted by you' : '');
251
+ if (r.git && r.git.author) return r.git.author;
252
+ if (r.via === 'first' || r.via === 'outside' || r.by === 'baseline') return r.via === 'first' ? 'Before the sheet' : 'Outside PMOS';
253
+ return byName(r.by);
254
+ };
255
+ const byChip = (r) => {
256
+ if (!r) return null;
257
+ const ag = isAgentRev(r); const label = whoOf(r);
258
+ return btn('ds-by', { 'data-doc-by': String(r.rev), title: 'Open revision ' + r.rev + ' in the edit log' },
259
+ [el('span', { class: 'ds-dot' + (ag ? ' a' : '') }, ag ? c3ic('spark') : (label[0] || '?').toUpperCase()), label + ' · ' + (r.v || 'rev ' + r.rev) + ' · ' + dayName(r.at)],
260
+ () => { S.flashRev = r.rev; S.logF = 'all'; go('hist'); });
165
261
  };
166
- const failing = () => (S.health ? S.health.checks.filter((c) => c.pass === false) : []);
167
- const seg = () => el('div', { class: 'seg', role: 'group', 'aria-label': 'Document mode' }, TABS.map(([k, ic, l]) => {
168
- const bad = k === 'health' ? failing().length : 0;
169
- return el('button', { type: 'button', 'aria-pressed': String(S.mode === k), 'aria-label': l, title: l, 'data-dmode': k, onclick: () => go(k) }, c3ic(ic, 'sm'), S.mode === k ? l : null, bad ? [' ', el('span', { class: 'bdg' }, String(bad))] : null);
170
- }));
171
- const go = (m) => { S.mode = m; if (m !== 'hist') S.cmp = null; if (m === 'health' && !S.hAsk) loadHealth(); if (m === 'hist' && S.revs === null) loadRevs().then(draw); if (m === 'read' || m === 'edit') loadMeta(); draw(); };
262
+ const goEl = (sel) => { const n = ui && ui.body.querySelector(sel); if (n) { n.scrollIntoView({ block: 'start' }); n.classList.remove('flash'); void n.offsetWidth; n.classList.add('flash'); } };
263
+
264
+ function head() {
265
+ const st = STATE[fresh() ? 'stale' : doc.st] || STATE.current; const h = S.health; const last = lastRev();
266
+ return el('div', { class: 'chips2 ds-meta', 'data-doc-meta': '' },
267
+ el('span', { class: 'chip2 ' + st[1] }, c3ic(st[2], 'sm'), st[0]), el('span', { class: 'chip2' }, doc.ty || 'Document'), doc.v ? el('span', { class: 'chip2 mono' }, doc.v) : null,
268
+ h && h.total ? btn('chip2 lk ' + hcls(h), { 'data-doc-hchip': '' }, [c3ic('shield', 'sm'), h.passed + ' of ' + h.total + ' checks'], () => go('health')) : null,
269
+ el('span', { class: 'chip2' }, c3ic('init', 'sm'), doc.iniName || doc.ini || 'Portfolio'),
270
+ last && !generated ? btn('chip2 lk', { 'data-doc-last': '' }, [c3ic(isAgentRev(last) ? 'spark' : 'user', 'sm'), 'Last change: ', el('b', null, whoOf(last) + ','), ' ' + dayName(last.at)], () => go('hist')) : null);
271
+ }
272
+ function seg() {
273
+ return el('div', { class: 'seg', role: 'group', 'aria-label': 'Document mode' }, TABS.map(([k, ic, l]) => {
274
+ const n = k === 'health' ? failing().length : k === 'ask' ? openProps().length : 0;
275
+ return btn('', { 'aria-pressed': String(S.mode === k), 'aria-label': l, title: l, 'data-dmode': k },
276
+ [c3ic(ic, 'sm'), el('span', { class: 'lbl' }, l), n ? el('span', { class: 'bdg ' + (k === 'ask' ? 'p' : 'w') }, String(n)) : null], () => go(k));
277
+ }));
278
+ }
279
+ function go(m) {
280
+ if (S.mode === 'edit' && m !== 'edit' && S.ed && dirty().length && S.leave !== m) { S.leave = m; draw(); return; }
281
+ if (m !== 'edit') S.ed = null;
282
+ S.leave = null; S.mode = m;
283
+ if (m === 'edit' && !S.ed) startEdit();
284
+ if (m === 'health' && !S.hAsk) loadHealth();
285
+ if (m === 'hist') { S.cmp = null; loadLog().then(() => { draw(); if (S.flashRev != null) { goEl('[data-log-rev="' + S.flashRev + '"]'); S.flashRev = null; } }); }
286
+ if (m === 'ask' && S.props === null) loadProps().then(draw);
287
+ if (m === 'read') loadMeta();
288
+ draw();
289
+ const sb = ui && ui.body.closest('.sb'); if (sb) sb.scrollTop = 0;
290
+ }
291
+
292
+ /* the stale banner: the Fresh check's sources, the sections that cite them, the open proposal */
293
+ function staleBanner() {
294
+ const f = fresh(); if (!f || generated) return null;
295
+ const secs = S.text ? splitDoc(S.text) : [];
296
+ const srcs = Array.isArray(f.sources) ? f.sources : [];
297
+ const cites = (p) => { const b = String(p).split('/').pop(); return secs.filter((s) => s.name !== 'Front matter' && (s.text.includes(p) || (b && s.text.includes(b)))).map((s) => s.name); };
298
+ const op = openProps();
299
+ if (S.staleWhy !== null) {
300
+ const inp = el('input', { type: 'text', id: 'dstalewhy', 'data-doc-stalewhy': '', placeholder: 'For example: the launch date is not agreed yet', 'aria-label': 'Why it is still current', value: S.staleWhy, onInput: (e) => { S.staleWhy = e.target.value; } });
301
+ return el('div', { class: 'ds-stale', role: 'note', 'data-doc-stale': '' }, el('div', { class: 't' }, c3ic('alert', 'sm'), 'Mark as still current'),
302
+ el('p', null, 'Say why the document is still right. The reason goes in the edit log, and the Fresh check counts it as a change.'),
303
+ el('div', { class: 'ds-whyadd' }, inp, btn('ds-btn pri', { 'data-doc-markok': '' }, 'Mark current', () => markCurrent()), btn('ds-btn', {}, 'Cancel', () => { S.staleWhy = null; draw(); })));
304
+ }
305
+ return el('div', { class: 'ds-stale', role: 'note', 'data-doc-stale': '' }, el('div', { class: 't' }, c3ic('alert', 'sm'), 'Stale'),
306
+ el('p', null, f.reason + '.' + (op.length ? ' ' + plural(op.length, 'open proposal') + ' may bring it up to date.' : ' Edit the sections they touch, or ask the agent for an update.')),
307
+ srcs.length ? el('ul', null, srcs.map((s) => { const c = cites(s.path); return el('li', { 'data-doc-stalesrc': s.path }, s.path + (s.changed ? ', changed ' + dayName(s.changed) : '') + (c.length ? ' (cited in ' + c.join(', ') + ')' : '')); })) : null,
308
+ el('div', { class: 'ds-acts' }, op.length ? btn('ds-btn ag', {}, [c3ic('spark', 'sm'), 'Review the proposals'], () => go('ask'))
309
+ : btn('ds-btn ag', {}, [c3ic('spark', 'sm'), 'Ask the agent'], () => { S.prompt = 'Bring this document up to date with ' + (srcs.map((s) => s.path).join(', ') || 'its sources'); go('ask'); }),
310
+ btn('ds-btn', { 'data-doc-mark': '' }, 'Mark as still current', () => { S.staleWhy = ''; draw(); const i = ui.body.querySelector('#dstalewhy'); if (i) i.focus(); })));
311
+ }
312
+ async function markCurrent() {
313
+ const w = String(S.staleWhy || '').trim();
314
+ if (!w) { say('Say why it is still current'); const i = ui.body.querySelector('#dstalewhy'); if (i) i.focus(); return; }
315
+ const r = await pmPost('/api/doc-sheet/mark-current', { ...base, why: w }).catch(() => null);
316
+ if (!r || !r.ok) { say((r && r.error) || 'It was not marked.'); return; }
317
+ S.staleWhy = null; S.changed = true; if (doc.st === 'stale') doc.st = 'current'; S.health = null; S.hAsk = false; S.log = null;
318
+ await loadHealth(); draw(); say('Marked as current. The reason is in the edit log.');
319
+ }
172
320
 
173
- const flags = () => { const f = {}; failing().forEach((c) => { if (c.section && c.flag) f[c.section] = c.flag; }); return f; };
174
- const readBody = (editing, quiet) => {
175
- if (editing && generated) return el('div', { class: 'dpn' }, el('div', { class: 'fold w', role: 'note', 'data-doc-nostore': 'edit' }, c3ic('lock', 'sm'), generated.why), readBody(false, true));
321
+ /* ── Read ── */
322
+ function propsBlock(secs, editing) {
323
+ const s0 = secs.find((s) => s.prefix === ''); const fm = s0 && parseFront(s0.text);
324
+ if (!fm || !fm.fields.length) return null;
325
+ if (editing) {
326
+ return el('dl', { class: 'ds-props', 'data-doc-props': 'edit' }, fm.fields.map((f) => [el('dt', null, el('label', { for: 'dsp-' + f.key }, f.key)),
327
+ el('dd', null, f.editable ? el('input', { id: 'dsp-' + f.key, type: 'text', value: S.ed.front[f.key] != null ? S.ed.front[f.key] : f.value, 'data-doc-prop': f.key, onInput: (e) => { S.ed.front[f.key] = e.target.value; mark(); } }) : f.value)]));
328
+ }
329
+ const r = S.meta && S.meta['Front matter'];
330
+ return el('dl', { class: 'ds-props', 'data-doc-props': '' }, fm.fields.map((f) => [el('dt', null, f.key), el('dd', null, f.value)]),
331
+ el('div', { class: 'sub' }, c3ic('doc', 'sm'), 'Front matter', r ? [' · ', byChip(r)] : null));
332
+ }
333
+ function readBody() {
176
334
  if (S.text == null) return S.err ? el('div', { class: 'fold w', role: 'alert' }, c3ic('alert', 'sm'), S.err) : el('div', { class: 'fold', role: 'status' }, c3ic('clock', 'sm'), 'Reading the document.');
177
- const secs = splitDoc(S.text); const fl = flags(); const meta = S.meta || {};
178
- if (!secs.some((s) => s.text.trim() || s.name !== 'Overview')) return el('div', { class: 'fold' }, c3ic('moon', 'sm'), 'The document is empty.');
335
+ const secs = splitDoc(S.text);
336
+ if (!S.text.trim()) return el('div', { class: 'fold' }, c3ic('moon', 'sm'), 'The document is empty.');
337
+ const named = secs.filter((s) => s.prefix);
338
+ const toc = el('select', { class: 'ds-sel', 'aria-label': 'Contents', 'data-doc-toc': '', onChange: (e) => { const v = e.target.value; e.target.value = ''; if (v !== '') goEl('[data-dsec-i="' + v + '"]'); } },
339
+ el('option', { value: '' }, 'Contents'), named.map((s) => el('option', { value: String(s.i) }, (headLevel(s) > 2 ? ' ' : '') + s.name)));
340
+ const tg = (id, label, on, set) => el('label', { class: 'ds-tg' }, el('input', { type: 'checkbox', checked: on, 'data-doc-toggle': id, onChange: (e) => { set(e.target.checked); draw(); } }), label);
341
+ const bar = el('div', { class: 'ds-rbar' }, toc, el('span', { class: 'ds-acts' },
342
+ generated ? null : tg('who', 'Show who wrote what', S.who, (v) => { S.who = v; }), tg('src', 'Markdown source', S.src, (v) => { S.src = v; })));
343
+ const legend = S.who ? el('div', { class: 'ds-legend' }, el('span', null, el('i', { class: 'h' }), 'Written by a person'), el('span', null, el('i', { class: 'a' }), 'Written by the agent, accepted by a person')) : null;
344
+ if (S.src) {
345
+ const pre = el('pre', { 'data-doc-source': '' }, el('code', null, S.text));
346
+ const copy = async () => { try { await navigator.clipboard.writeText(S.text); say('Copied the markdown.'); } catch { const s = getSelection(); const rg = document.createRange(); rg.selectNodeContents(pre); s.removeAllRanges(); s.addRange(rg); say('Copying is blocked here. The text is selected.'); } };
347
+ return el('div', { class: 'ds' }, bar, el('div', { class: 'ds-acts' }, btn('ds-btn', { 'data-doc-copysrc': '' }, [c3ic('copy', 'sm'), 'Copy the markdown'], copy), el('span', { class: 'ds-mono' }, doc.path)), el('div', { class: 'ds-md' }, pre));
348
+ }
349
+ const meta = S.meta || {};
350
+ const pins = (name) => openProps().filter((p) => p.sections.some((c) => c.name === name && c.kind !== 'added'));
351
+ const flags = {}; failing().forEach((c) => { if (c.section && c.flag) flags[c.section] = c.flag; });
179
352
  const cells = secs.map((s) => {
180
- const mt = meta[s.name];
181
- const dt = el('div', { class: 'dt', 'data-sec': s.i, ...(editing ? { contenteditable: 'plaintext-only', role: 'textbox', 'aria-label': s.name, 'aria-multiline': 'true', style: 'white-space:pre-wrap' } : { style: 'white-space:pre-wrap' }) }, editing && S.draft[s.i] != null ? S.draft[s.i] : s.text);
182
- if (editing && dt.contentEditable !== 'plaintext-only') dt.setAttribute('contenteditable', 'true');
183
- if (editing) dt.addEventListener('input', () => { S.draft[s.i] = dt.contentEditable === 'plaintext-only' ? dt.textContent : dt.innerText; });
184
- return el('div', { class: 'dsec' + (fl[s.name] ? ' warn' : ''), 'data-dsec': s.name }, el('h4', null, s.name), dt,
185
- el('div', { class: 'mt' }, mt ? el('span', { style: 'color:' + (byName(mt.by) === 'Agent' ? 'var(--purple)' : 'var(--acc)') }, c3ic(byName(mt.by) === 'Agent' ? 'spark' : 'user', 'sm'), 'Last changed by ' + byName(mt.by) + ', ' + dayName(mt.at)) : null,
186
- fl[s.name] ? el('span', { style: 'color:var(--warning)' }, c3ic('alert', 'sm'), fl[s.name]) : null), !editing && secExtra ? secExtra(s.name) : null);
353
+ const fm = s.prefix === '' ? parseFront(s.text) : null;
354
+ const body = fm ? fm.rest.trim() : s.text;
355
+ if (!s.prefix && !body) return null;
356
+ const r = meta[s.name]; const P = pins(s.name);
357
+ const shown = P.find((p) => S.preview[p.id]);
358
+ const ch = shown && shown.sections.find((c) => c.name === s.name);
359
+ const content = ch ? md(diffMd(ch.stale ? s.text : ch.before, ch.after), 'ds-body') : body ? md(body, 'ds-body') : headLevel(s) === 1 ? null : el('p', { class: 'ds-note' }, 'This section is empty.');
360
+ const lvl = Math.min(3, Math.max(2, headLevel(s)));
361
+ return el('div', { class: 'ds-sec ' + (r ? (isAgentRev(r) ? 'a' : 'h') : '') + (ch ? ' prev' : ''), 'data-dsec': s.name, 'data-dsec-i': String(s.i) },
362
+ s.prefix ? el('div', { class: 'ds-sec-h' }, headLevel(s) === 1 ? el('div', { class: 'ds-md' }, el('h1', null, s.name)) : el('h' + lvl, null, s.name), generated ? null : byChip(r)) : null,
363
+ P.map((p) => { const c = p.sections.find((x) => x.name === s.name); return el('div', { class: 'ds-pin', 'data-doc-pin': p.id }, el('span', null, c3ic('spark', 'sm'), (c.stale ? 'A proposal was drafted against older text here' : 'The agent proposes a change here') + ': “' + short(p.prompt) + '”'),
364
+ el('span', { class: 'ds-acts' }, btn('ds-btn', { 'data-doc-preview': p.id }, S.preview[p.id] ? 'Show current' : 'Show change', () => { S.preview[p.id] = !S.preview[p.id]; draw(); }),
365
+ btn('ds-btn ag', {}, 'Review', () => { S.openProp = p.id; go('ask'); }))); }),
366
+ flags[s.name] ? el('div', { class: 'fold w', role: 'note' }, c3ic('alert', 'sm'), flags[s.name]) : null,
367
+ content, secExtra ? secExtra(s.name) : null);
187
368
  });
188
- if (!editing) return el('div', { class: 'dpn' }, generated && !quiet ? el('div', { class: 'fold', role: 'note', 'data-doc-generated': '' }, c3ic('info', 'sm'), generated.why) : null, cells);
189
- const why = el('input', { type: 'text', id: 'dwhy', placeholder: 'Why are you changing it? (kept in the history)', 'aria-label': 'Why', 'data-doc-why': '', value: S.why, onInput: (e) => { S.why = e.target.value; } });
190
- return el('div', { class: 'dpn' }, cells, why,
191
- S.err ? el('div', { class: 'fold w', role: 'alert' }, c3ic('alert', 'sm'), S.err) : null,
192
- el('div', { class: 'chips2' }, c3ib('Save as ' + nextV(doc.v), 'check', { 'data-doc-save': '', onClick: () => save(secs) }, ''), c3ib('Cancel', 'x', { onClick: () => { S.err = ''; S.draft = {}; go('read'); } }, '')));
193
- };
369
+ const adds = openProps().flatMap((p) => p.sections.filter((c) => c.kind === 'added' && S.preview[p.id]).map((c) => el('div', { class: 'ds-sec a prev', 'data-dsec-new': c.name },
370
+ el('div', { class: 'ds-pin' }, el('span', null, c3ic('spark', 'sm'), 'The proposal adds a section: ' + c.name)), el('div', { class: 'ds-sec-h' }, el('h2', null, c.name)), md(diffMd(null, c.after), 'ds-body'))));
371
+ return el('div', { class: 'ds' + (S.who ? ' who' : '') }, bar, legend, generated ? el('div', { class: 'fold', role: 'note', 'data-doc-generated': '' }, c3ic('info', 'sm'), generated.why) : null,
372
+ staleBanner(), propsBlock(secs, false), cells, adds);
373
+ }
374
+ const short = (t) => { const s = String(t || '').replace(/\s+/g, ' ').trim(); return s.length > 90 ? s.slice(0, 88) + '…' : s; };
194
375
 
195
- async function save(secs) {
196
- const edits = {};
197
- ui.body.querySelectorAll('[data-sec]').forEach((n) => { const i = Number(n.dataset.sec); const v = n.contentEditable === 'plaintext-only' ? n.textContent : n.innerText; if (v !== secs[i].text) edits[i] = v; });
198
- if (!Object.keys(edits).length) { say('Nothing changed'); S.err = ''; S.draft = {}; go('read'); return; }
199
- if (!S.why.trim()) { S.err = 'Say why the document changes: the reason is kept in the history.'; draw(); ui.body.querySelector('#dwhy') && ui.body.querySelector('#dwhy').focus(); return; }
200
- const before = S.text; const after = joinDoc(secs, edits);
201
- const r = await pmPost('/api/doc-sheet/edit', { ...base, text: after, why: S.why.trim() });
202
- if (!r || !r.ok) { S.err = (r && r.error) || 'The document was not saved.'; draw(); return; }
203
- S.err = ''; S.why = ''; S.draft = {}; S.changed = true; S.revs = null; S.meta = null; S.metaFor = null; S.texts = {}; S.health = null; S.hAsk = false;
204
- await loadText(); go('read');
205
- say('Saved as ' + nextV(doc.v) + (r.rev ? ' (revision ' + r.rev + ')' : ''), async () => {
206
- const u = await pmPost('/api/doc-sheet/edit', { ...base, text: before, why: 'Undone: restored the text from before the last edit.' });
207
- if (u && u.ok) { S.revs = null; S.meta = null; S.metaFor = null; S.texts = {}; S.health = null; S.hAsk = false; await loadText(); draw(); } else say((u && u.error) || 'The edit could not be undone.');
376
+ /* ── Edit ── */
377
+ function startEdit() {
378
+ if (generated || S.text == null) { S.ed = null; return; }
379
+ S.ed = { text: S.text, base: splitDoc(S.text), front: {}, kind: 'rich', why: '', whyBad: false, node: null, ta: null, range: null, link: false };
380
+ }
381
+ function edNode() {
382
+ const E = S.ed; const kids = [];
383
+ E.base.forEach((s, i) => {
384
+ if (s.prefix === '') { const fm = parseFront(s.text); const rest = fm ? fm.rest.trim() : s.text; if (rest) kids.push(...toDom(parseMd(rest), document, { editable: true })); return; }
385
+ const h = el('h' + Math.min(6, Math.max(1, headLevel(s))), { 'data-sid': String(i) }, s.name);
386
+ kids.push(h, ...toDom(parseMd(s.text), document, { editable: true }));
387
+ });
388
+ const n = el('div', { class: 'ds-md ds-ed', id: 'ded', contenteditable: 'true', role: 'textbox', 'aria-multiline': 'true', 'aria-label': 'Document', spellcheck: 'true', 'data-doc-editor': '',
389
+ onInput: () => { clearTimeout(E.t); E.t = setTimeout(mark, 200); }, onChange: () => mark(),
390
+ onKeyDown: (e) => { if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') { e.preventDefault(); linkOpen(); } if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') { e.preventDefault(); save(); } } }, kids);
391
+ n.querySelectorAll('input[type=checkbox]').forEach((c) => { c.disabled = false; });
392
+ return n;
393
+ }
394
+ /** The draft as text, from whichever editor is open. */
395
+ function draftText() {
396
+ const E = S.ed; if (!E) return S.text;
397
+ const s0 = E.base.find((s) => s.prefix === ''); const fm = s0 && parseFront(s0.text);
398
+ const front = fm ? setFront(fm.block, E.front) : null;
399
+ if (E.kind === 'md') {
400
+ let t = E.ta ? E.ta.value : E.text;
401
+ if (fm && front !== fm.block && t.startsWith(fm.block)) t = front + t.slice(fm.block.length);
402
+ return t;
403
+ }
404
+ if (!E.node) return E.text;
405
+ const groups = []; let intro = []; let cur = null;
406
+ for (const n of Array.from(E.node.childNodes)) {
407
+ if (n.nodeType === 1 && /^H[1-6]$/.test(n.tagName)) { cur = { sid: n.hasAttribute('data-sid') ? Number(n.getAttribute('data-sid')) : null, level: Number(n.tagName[1]), name: n.textContent.replace(/\s+/g, ' ').trim() || 'Untitled section', nodes: [] }; groups.push(cur); continue; }
408
+ (cur ? cur.nodes : intro).push(n);
409
+ }
410
+ const mdOf = (nodes) => { const d = document.createElement('div'); nodes.forEach((x) => d.appendChild(x.cloneNode(true))); return toMd(fromDom(d)); };
411
+ return composeSections(E.base, groups.map((g) => ({ sid: g.sid, level: g.level, name: g.name, md: mdOf(g.nodes) })), mdOf(intro), front);
412
+ }
413
+ const dirty = () => (S.ed ? changedSections(S.text, draftText()) : []);
414
+ const clashOf = (name) => openProps().find((p) => p.sections.some((c) => c.name === name));
415
+ function mark() {
416
+ const E = S.ed; if (!E) return;
417
+ const ch = dirty(); const names = new Set(ch.map((c) => c.name));
418
+ if (E.node) E.node.querySelectorAll(':scope > h1,:scope > h2,:scope > h3,:scope > h4,:scope > h5,:scope > h6').forEach((h) => {
419
+ const nm = h.textContent.replace(/\s+/g, ' ').trim(); const on = !h.hasAttribute('data-sid') || names.has(nm) || names.has(E.base[Number(h.getAttribute('data-sid'))]?.name);
420
+ h.classList.toggle('chg', on); h.classList.toggle('clash', !!(on && clashOf(nm)));
421
+ });
422
+ const s = ui.body.querySelector('[data-doc-savesum]'); if (s) s.replaceChildren(...saveSum(ch).filter(Boolean));
423
+ }
424
+ function saveSum(ch) {
425
+ if (!ch.length) return [c3ic('info', 'sm'), 'No changes yet. Health checks the document again after you save.'];
426
+ const cl = [...new Set(ch.map((c) => clashOf(c.name)).filter(Boolean))];
427
+ return [c3ic('edit', 'sm'), plural(ch.length, 'section') + ' changed: ' + ch.map((c) => c.name).join(', ') + '.',
428
+ cl.length ? el('span', { class: 'ds-warn' }, ' ' + plural(cl.length, 'open proposal') + ' also change' + (cl.length === 1 ? 's' : '') + ' them; accepting it after your save needs a new draft.') : null];
429
+ }
430
+ const keepRange = () => { const E = S.ed; const s = getSelection(); if (E && E.node && s.rangeCount && E.node.contains(s.anchorNode)) E.range = s.getRangeAt(0).cloneRange(); };
431
+ const restore = () => { const E = S.ed; if (E && E.range) { const s = getSelection(); s.removeAllRanges(); s.addRange(E.range); } };
432
+ function cmd(c, arg) {
433
+ const E = S.ed; if (!E || E.kind !== 'rich' || !E.node) { say('Switch to Rich to use the toolbar.'); return; }
434
+ E.node.focus(); restore();
435
+ const txt = getSelection().toString();
436
+ if (c === 'code') document.execCommand('insertHTML', false, '<code>' + esc(txt || 'code') + '</code>&#8203;');
437
+ else if (c === 'task') document.execCommand('insertHTML', false, '<ul class="tasks"><li class="task"><input type="checkbox" aria-label="Done"><span>' + esc(txt || 'New item') + '</span></li></ul>');
438
+ else if (c === 'table') document.execCommand('insertHTML', false, '<div class="tw"><table><thead><tr><th>Column</th><th>Column</th></tr></thead><tbody><tr><td>&nbsp;</td><td>&nbsp;</td></tr></tbody></table></div><p><br></p>');
439
+ else if (c === 'formatBlock') document.execCommand('formatBlock', false, arg);
440
+ else document.execCommand(c, false, arg);
441
+ keepRange(); mark();
442
+ }
443
+ function linkOpen() { keepRange(); S.ed.link = true; drawEdit(); const i = ui.body.querySelector('#dlink'); if (i) i.focus(); }
444
+ function linkDone(ok, url) {
445
+ const E = S.ed; E.link = false; const u = safeHref(String(url || '').trim());
446
+ drawEdit();
447
+ if (!ok || !u) { if (ok && url && !u) say('That link cannot be used. Use an http or https address, or a path.'); return; }
448
+ E.node.focus(); restore();
449
+ if (getSelection().toString()) document.execCommand('createLink', false, u); else document.execCommand('insertHTML', false, '<a href="' + esc(u) + '">' + esc(u) + '</a>');
450
+ mark();
451
+ }
452
+ function toolbar() {
453
+ const E = S.ed;
454
+ const tb = (c, ic, l, arg) => btn('', { 'aria-label': l, title: l, 'data-doc-cmd': c, onMouseDown: (e) => e.preventDefault() }, c3ic(ic, 'sm'), () => (c === 'link' ? linkOpen() : cmd(c, arg)));
455
+ const style = el('select', { 'aria-label': 'Paragraph style', 'data-doc-style': '', onChange: (e) => { cmd('formatBlock', e.target.value); e.target.value = ''; } },
456
+ el('option', { value: '' }, 'Style'), el('option', { value: 'p' }, 'Text'), el('option', { value: 'h2' }, 'Section heading'), el('option', { value: 'h3' }, 'Subheading'), el('option', { value: 'blockquote' }, 'Callout'));
457
+ const kind = el('div', { class: 'seg', role: 'group', 'aria-label': 'Editor' },
458
+ btn('', { 'aria-pressed': String(E.kind === 'rich'), 'data-doc-edkind': 'rich' }, [c3ic('eye', 'sm'), 'Rich'], () => switchKind('rich')),
459
+ btn('', { 'aria-pressed': String(E.kind === 'md'), 'data-doc-edkind': 'md' }, [c3ic('md', 'sm'), 'Markdown'], () => switchKind('md')));
460
+ const bar = el('div', { class: 'ds-tb', role: 'toolbar', 'aria-label': 'Formatting' }, style, el('span', { class: 'sep' }),
461
+ tb('bold', 'bold', 'Bold (Ctrl+B)'), tb('italic', 'italic', 'Italic (Ctrl+I)'), tb('code', 'code', 'Inline code'), tb('link', 'link', 'Link (Ctrl+K)'), el('span', { class: 'sep' }),
462
+ tb('insertUnorderedList', 'ul', 'Bulleted list'), tb('insertOrderedList', 'ol', 'Numbered list'), tb('task', 'task', 'Checklist'), tb('table', 'table', 'Table'), tb('insertHorizontalRule', 'hr', 'Divider'), el('span', { class: 'sep' }),
463
+ tb('undo', 'undo', 'Undo (Ctrl+Z)'), tb('redo', 'redo', 'Redo'), el('span', { class: 'sp' }), kind);
464
+ if (!E.link) return bar;
465
+ const inp = el('input', { id: 'dlink', type: 'url', placeholder: 'Paste a link, for example a Confluence page', 'aria-label': 'Link address', onKeyDown: (e) => { if (e.key === 'Enter') { e.preventDefault(); linkDone(true, e.target.value); } if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); linkDone(false); } } });
466
+ return [bar, el('div', { class: 'ds-pop', 'data-doc-linkpop': '' }, c3ic('link', 'sm'), inp, btn('ds-btn pri', {}, 'Add link', () => linkDone(true, inp.value)), btn('ds-btn', {}, 'Cancel', () => linkDone(false)))];
467
+ }
468
+ function switchKind(k) {
469
+ const E = S.ed; if (E.kind === k) return;
470
+ E.text = draftText(); E.kind = k;
471
+ if (k === 'rich') { E.base = splitDoc(E.text); E.node = null; }
472
+ drawEdit();
473
+ }
474
+ function editBody() {
475
+ if (generated) return el('div', { class: 'fold w', role: 'note', 'data-doc-nostore': 'edit' }, c3ic('lock', 'sm'), generated.why);
476
+ if (S.text == null) return readBody();
477
+ if (!S.ed) startEdit();
478
+ const E = S.ed;
479
+ const secs = splitDoc(S.text);
480
+ let area;
481
+ if (E.kind === 'rich') { if (!E.node) E.node = edNode(); area = E.node; E.ta = null; }
482
+ else { E.ta = E.ta || el('textarea', { class: 'ds-src', id: 'dsrc', 'aria-label': 'Markdown source', spellcheck: 'false', 'data-doc-mdsrc': '', onInput: () => { clearTimeout(E.t); E.t = setTimeout(mark, 250); } }, E.text); area = E.ta; E.node = null; }
483
+ const why = el('input', { type: 'text', id: 'dwhy', class: E.whyBad ? 'bad' : '', placeholder: 'Why are you changing it? Kept in the edit log.', 'aria-label': 'Why', 'data-doc-why': '', value: E.why,
484
+ onInput: (e) => { E.why = e.target.value; if (E.whyBad && E.why.trim()) { E.whyBad = false; e.target.classList.remove('bad'); const a = ui.body.querySelector('[data-doc-whybad]'); if (a) a.remove(); } } });
485
+ const save = el('div', { class: 'ds-save' }, el('div', { class: 'sum', 'data-doc-savesum': '' }, saveSum([])), why,
486
+ E.whyBad ? el('div', { class: 'fold w', role: 'alert', 'data-doc-whybad': '' }, c3ic('alert', 'sm'), 'Say why the document changes. The reason is kept in the edit log.') : null,
487
+ S.err ? el('div', { class: 'fold w', role: 'alert' }, c3ic('alert', 'sm'), S.err) : null,
488
+ el('div', { class: 'row' }, el('span', { class: 'ds-mono' }, c3ic('user', 'sm'), ' Saved by you' + (nextV(doc.v) ? ' as ' + nextV(doc.v) : '')),
489
+ el('span', { class: 'ds-acts' }, btn('ds-btn', { 'data-doc-cancel': '' }, 'Cancel', () => { S.ed = null; S.err = ''; go('read'); }),
490
+ btn('ds-btn pri', { 'data-doc-save': '' }, [c3ic('check', 'sm'), 'Save' + (nextV(doc.v) ? ' as ' + nextV(doc.v) : '')], () => saveDoc()))));
491
+ return el('div', { class: 'ds', 'data-doc-edit': '' }, staleBanner(), propsBlock(secs, true), area, save);
492
+ }
493
+ function drawEdit() { draw(); if (S.ed) setTimeout(mark, 0); }
494
+ async function saveDoc() {
495
+ const E = S.ed; if (!E || S.busy) return;
496
+ const after = draftText(); const ch = changedSections(S.text, after);
497
+ if (after === S.text || !ch.length) { say('Nothing changed.'); S.ed = null; S.err = ''; go('read'); return; }
498
+ if (!E.why.trim()) { E.whyBad = true; E.text = after; if (E.kind === 'rich') { E.base = splitDoc(after); E.node = null; } else if (E.ta) E.ta.value = after; drawEdit(); const w = ui.body.querySelector('#dwhy'); if (w) w.focus(); return; }
499
+ const before = S.text; S.busy = true;
500
+ const r = await pmPost('/api/doc-sheet/edit', { ...base, text: after, why: E.why.trim() }).catch(() => null);
501
+ S.busy = false;
502
+ if (!r || !r.ok) { S.err = (r && r.error) || 'The document was not saved.'; E.text = after; if (E.kind === 'rich') { E.base = splitDoc(after); E.node = null; } drawEdit(); return; }
503
+ const v = nextV(doc.v); if (v) doc.v = v; if (doc.st === 'stale') doc.st = 'current';
504
+ S.err = ''; S.ed = null; S.changed = true; S.mode = 'read';
505
+ await reloadAll();
506
+ say('Saved' + (v ? ' as ' + v : '') + '. ' + plural(ch.length, 'section') + ' changed; the reason is in the edit log.', async () => {
507
+ const u = await pmPost('/api/doc-sheet/edit', { ...base, text: before, why: 'Undone: restored the text from before the last edit.' }).catch(() => null);
508
+ if (u && u.ok) { await reloadAll(); } else say((u && u.error) || 'The edit could not be undone.');
208
509
  });
209
510
  }
210
511
 
211
- /* Ask the agent */
212
- const diffNode = (list, all) => {
213
- const rows = list.filter((d) => all || d.kind !== 'same');
214
- return el('div', { class: 'dpn dfi', 'data-doc-diff': '' }, rows.length ? rows.map((d) => el('div', { class: 'dsec warn' }, el('h4', null, d.name), el('p', null, wordDiffTokens(d.before, d.after).map((t) => (t.op === 'add' ? el('ins', null, t.text) : t.op === 'del' ? el('del', null, t.text) : t.text))))) : el('div', { class: 'dim' }, 'No change.'));
215
- };
216
- const askBody = () => {
217
- if (generated) return el('div', { class: 'dpn' }, el('div', { class: 'fold w', role: 'note', 'data-doc-nostore': 'ask' }, c3ic('lock', 'sm'), generated.why));
218
- const ta = el('textarea', { id: 'dpr', placeholder: 'Tell the agent what to change, for example: cut the scope to 3 flows', 'aria-label': 'Request to the agent', 'data-doc-prompt': '', onInput: (e) => { S.prompt = e.target.value; } }, S.prompt);
512
+ /* ── Proposals ── */
513
+ function askBox() {
514
+ const A = S.asking;
515
+ const ta = el('textarea', { id: 'dpr', placeholder: 'Tell the agent what to change, for example: add a one-paragraph summary at the top', 'aria-label': 'Request to the agent', 'data-doc-prompt': '', onInput: (e) => { S.prompt = e.target.value; } }, S.prompt);
219
516
  const sug = [...new Set(failing().map((c) => c.fix).filter(Boolean))];
220
517
  const copy = async () => { const t = 'Change ' + doc.path + ': ' + (S.prompt || '').trim(); try { await navigator.clipboard.writeText(t); say('The request is copied. Give it to your agent session.'); } catch { say('Copying is blocked here. Select the request and copy it.'); } };
221
- const kids = [ta, el('div', { class: 'chips2' },
222
- demo ? c3ib('Ask the agent', 'spark', { 'data-doc-ask': '', onClick: () => askDemo() }, '')
223
- : S.asking && S.asking.state === 'working' ? c3btn('pill', { disabled: true, 'aria-disabled': 'true', 'data-doc-ask': '' }, [c3ic('clock', 'sm'), 'The agent is drafting'])
224
- : c3btn('pill', { 'data-doc-ask': '' }, [c3ic('spark', 'sm'), 'Ask the agent'], () => askReal()),
225
- !demo ? el('button', { type: 'button', class: 'chip2 lk', 'data-doc-copy': '', onclick: copy }, c3ic('doc', 'sm'), 'Copy the request') : null,
226
- ...sug.map((t, i) => el('button', { type: 'button', class: 'chip2 lk', 'data-doc-chip': String(i), onclick: () => { S.prompt = t; draw(); } }, c3ic('spark', 'sm'), t)))];
227
- const A = S.asking;
228
- if (!demo && A && A.state === 'working') kids.push(el('div', { class: 'fold', role: 'status', 'data-doc-asking': '' }, c3ic('clock', 'sm'), 'The agent is reading the document and drafting the change. It runs in the background and can only read; you can close this sheet and come back. Its run is listed in Sessions.'));
229
- else if (!demo && A && A.state === 'failed') kids.push(el('div', { class: 'fold w', role: 'alert', 'data-doc-ask-err': '' }, c3ic('alert', 'sm'), A.error || 'The agent did not answer.'));
230
- if (S.prop) kids.push(propCase());
231
- else if (!demo && !A) kids.push(el('div', { class: 'fold', role: 'note', 'data-doc-ask-why': '' }, c3ic('info', 'sm'), 'The agent drafts the change in the background and shows you the difference. It can only read: nothing is saved until you slide to accept it.'));
232
- else if (demo) kids.push(el('div', { class: 'dim', style: 'font-size:var(--fs-2)' }, 'The agent drafts the change as a new version and shows you the difference. Nothing is saved or shared until you accept it.'));
233
- return el('div', { class: 'dpn' }, kids);
234
- };
518
+ const working = A && A.state === 'working';
519
+ return el('div', { class: 'ds-ask' }, el('label', { class: 'ds-h', for: 'dpr' }, c3ic('spark', 'sm'), 'Ask the agent'), ta,
520
+ el('div', { class: 'ds-acts' },
521
+ btn('ds-btn ag', { 'data-doc-ask': '', disabled: working, 'aria-disabled': working ? 'true' : null }, [c3ic(working ? 'clock' : 'spark', 'sm'), working ? 'The agent is drafting' : 'Ask the agent'], () => (demo ? askDemo() : askReal())),
522
+ demo ? null : btn('ds-btn', { 'data-doc-copy': '' }, [c3ic('copy', 'sm'), 'Copy the request'], copy),
523
+ sug.map((t, i) => btn('chip2 lk', { 'data-doc-chip': String(i) }, [c3ic('spark', 'sm'), t], () => { S.prompt = t; draw(); }))),
524
+ working ? el('div', { class: 'fold', role: 'status', 'data-doc-asking': '' }, c3ic('clock', 'sm'), 'The agent is reading the document and drafting the change. It runs in the background and can only read; you can close this sheet and come back. Its run is listed in Sessions.')
525
+ : A && A.state === 'failed' ? el('div', { class: 'fold w', role: 'alert', 'data-doc-ask-err': '' }, c3ic('alert', 'sm'), A.error || 'The agent did not answer.')
526
+ : el('p', { class: 'ds-note', 'data-doc-ask-why': '' }, 'The agent drafts a proposal and can only read. Nothing in the document changes until you accept it, and your request is kept in the edit log.'));
527
+ }
235
528
  async function askDemo() {
236
529
  const pr = (S.prompt || '').trim(); if (!pr) { say('Tell the agent what to change'); return; }
237
- const before = S.text || ''; const drafted = demoDraft(splitDoc(before), pr); const after = sectionsToText(before, drafted);
238
- const r = await pmPost('/api/doc-sheet/propose', { ...base, prompt: pr, after });
530
+ const before = S.text || ''; const after = before.replace(/\s*$/, '\n\n') + '## Agent note\n\nDrafted from your request: ' + pr.slice(0, 160) + '\n';
531
+ const r = await pmPost('/api/doc-sheet/propose', { ...base, prompt: pr, after }).catch(() => null);
239
532
  if (!r || !r.ok) { say((r && r.error) || 'The agent did not answer.'); return; }
240
- S.prop = { id: r.id, hash: r.hash, prompt: pr, before, after, nextV: nextV(doc.v) }; draw();
533
+ S.prompt = ''; S.openProp = r.id; await loadProps(); draw();
241
534
  }
242
- /* real mode: start the run from the page, then read its state until the proposal is there */
535
+ /* real mode: start the run from the page (page-starts-agent), then read its state until the proposal is there */
243
536
  async function askReal() {
244
- const pr = (S.prompt || '').trim(); if (!pr) { say('Tell the agent what to change'); return; }
245
- S.prop = null; S.asking = { state: 'working' }; draw();
246
- const r = await pmPost('/api/doc-sheet/ask', { ...base, prompt: pr });
537
+ const pr = (S.prompt || '').trim(); if (!pr) { say('Tell the agent what to change'); const t = ui.body.querySelector('#dpr'); if (t) t.focus(); return; }
538
+ S.asking = { state: 'working' }; draw();
539
+ const r = await pmPost('/api/doc-sheet/ask', { ...base, prompt: pr }).catch(() => null);
247
540
  if (!r || !r.ok) { S.asking = { state: 'failed', error: (r && r.error) || 'The agent could not start.' }; draw(); return; }
248
- S.asking = { id: r.id, state: 'working' }; draw();
249
- pollAsk(r.id, pr);
541
+ S.asking = { id: r.id, state: 'working' }; S.prompt = ''; S.log = null; draw();
542
+ pollAsk(r.id);
250
543
  }
251
- async function pollAsk(id, pr) {
544
+ async function pollAsk(id) {
252
545
  while (!S.closed && S.asking && S.asking.id === id) {
253
546
  await new Promise((ok) => setTimeout(ok, 3000));
254
547
  if (S.closed || !S.asking || S.asking.id !== id) return;
255
- const r = await pmGet('/api/doc-sheet/ask?id=' + encodeURIComponent(id) + (ws ? '&ws=' + encodeURIComponent(ws) : '')).catch(() => null);
548
+ const r = await pmGet('/api/doc-sheet/ask?' + q({ id, ws })).catch(() => null);
256
549
  if (!r || !r.ok || r.state === 'working') continue;
257
- if (r.state === 'proposed' && r.proposal) { S.asking = null; S.prop = { id: r.proposal.id, hash: r.proposal.hash, prompt: r.proposal.prompt || pr, before: r.proposal.before, after: r.proposal.after, nextV: nextV(doc.v) }; }
550
+ if (r.state === 'proposed' && r.proposal) { S.asking = null; S.openProp = r.proposal.id; await loadProps(); }
258
551
  else S.asking = { id, state: 'failed', error: r.error };
259
- if (S.mode === 'ask') draw();
260
- say(S.prop ? 'The agent’s draft is ready. Read it, then slide to accept.' : 'The agent did not draft the change.');
552
+ S.log = null; drawBg();
553
+ say(r.state === 'proposed' ? 'The agent’s draft is ready in Proposals. Read it, then slide to accept.' : 'The agent did not draft the change.');
261
554
  return;
262
555
  }
263
556
  }
264
- function propCase() {
265
- const P = S.prop; const D = changedSections(P.before, P.after);
266
- const c = renderCase({ id: 'docedit:' + doc.path, sit: 'You asked: “' + P.prompt + '”. The draft changes ' + D.length + ' section' + (D.length === 1 ? '' : 's') + ' and saves as ' + P.nextV + '. Nothing is shared until you send it.', conf: 76,
267
- chg: D.map((x) => ['notes', 'Section: ' + x.name, x.kind, 'warn']), cta: 'Slide to accept ' + P.nextV,
268
- opts: [{ id: 'ac', l: 'Accept as ' + P.nextV, rec: 1, fx: [['check', 'Health is re-checked', 'ok'], ['lock', 'Bound to proposal ' + P.hash.slice(0, 8), '']] }, { id: 'rf', l: 'Change the request', fx: [] }, { id: 'ds', l: 'Discard', fx: [] }] },
269
- { onConfirm: async ({ option }) => {
270
- if (option.id === 'ds') { S.prop = null; say('Discarded.'); draw(); return; }
271
- if (option.id === 'rf') { S.prop = null; draw(); const t = ui.body.querySelector('#dpr'); if (t) t.focus(); return; }
272
- const r = await pmPost('/api/doc-sheet/accept', { ...base, id: P.id, hash: P.hash });
273
- if (!r || !r.ok) { say('Not accepted: ' + ((r && (r.error || r.reason)) || 'the proposal changed.')); return; }
274
- S.prop = null; S.changed = true; S.revs = null; S.meta = null; S.metaFor = null; S.texts = {}; S.health = null; S.hAsk = false; doc.v = P.nextV; if (doc.st !== 'current') doc.st = 'current';
275
- await loadText(); go('read'); say('Saved as ' + P.nextV + '. History records your request.');
276
- } });
277
- const viz = diffNode(D.map((x) => ({ ...x })));
278
- c.insertBefore(viz, c.querySelector('.chgl') || c.querySelector('.opts'));
279
- return c;
557
+ function propCard(p) {
558
+ const pick = S.pick[p.id] || (S.pick[p.id] = {});
559
+ p.sections.forEach((c) => { if (pick[c.name] == null || c.stale) pick[c.name] = !c.stale; });
560
+ const n = p.sections.filter((c) => pick[c.name]).length;
561
+ const anyStale = p.sections.some((c) => c.stale);
562
+ const hd = el('div', { class: 'hd' }, el('span', { class: 'ds-av ' + (p.by === 'pm' ? 'h' : 'a') }, p.by === 'pm' ? 'Y' : c3ic('spark', 'sm')),
563
+ el('span', null, el('div', { class: 'ti' }, short(p.prompt) || 'A proposal'), el('div', { class: 'sub' }, el('span', null, p.by === 'pm' ? 'Drafted by you' : 'Drafted by the agent'), el('span', null, whenOf(p.at)), p.hash ? el('span', null, 'hash ' + String(p.hash).slice(0, 8)) : null)),
564
+ el('span', { class: 'tag ds-ag' }, plural(p.sections.length, 'section')));
565
+ const why = el('p', { class: 'ds-why' }, c3ic('user', 'sm'), el('span', null, 'You asked: ', el('q', null, p.prompt)));
566
+ const chs = p.sections.map((c) => {
567
+ const [a, d] = wordCounts(c.stale ? c.current || '' : c.before, c.after);
568
+ const box = el('input', { type: 'checkbox', checked: !!pick[c.name], disabled: c.stale, 'data-doc-pick': p.id + ':' + c.name, onChange: (e) => { pick[c.name] = e.target.checked; draw(); } });
569
+ return el('div', { class: 'ds-chg' + (pick[c.name] ? '' : ' off'), 'data-doc-chg': c.name },
570
+ el('header', null, el('label', null, box, (c.kind === 'added' ? 'New section: ' : c.kind === 'removed' ? 'Removes: ' : '') + c.name),
571
+ c.stale ? el('span', { class: 'ds-warn', 'data-doc-chgstale': '' }, c3ic('alert', 'sm'), ' Changed since this draft. Ask again to re-draft it.') : el('span', { class: 'dim' }, '+' + a + ' −' + d + ' words')),
572
+ md(c.kind === 'added' ? diffMd(null, c.after) : diffMd(c.stale ? c.current || '' : c.before, c.after)));
573
+ });
574
+ const host = el('div', { class: 'ds-slide', 'data-doc-slide': p.id });
575
+ if (n) mountSlide(host, { label: 'Slide to accept ' + n + ' of ' + p.sections.length, hash: p.hash, onConfirm: (h) => acceptProp(p, h) });
576
+ else host.append(el('div', { class: 'slt-why', role: 'note' }, c3ic('lock', 'sm'), anyStale && p.sections.every((c) => c.stale) ? 'Every section changed since this draft. Ask the agent again.' : 'Tick at least one section to accept.'));
577
+ const rej = S.rejFor === p.id
578
+ ? el('div', { class: 'ds-whyadd' }, el('input', { type: 'text', id: 'drejwhy', 'data-doc-rejwhy': '', placeholder: 'Why are you rejecting it? Kept in the edit log.', 'aria-label': 'Why reject', value: S.rejWhy, onInput: (e) => { S.rejWhy = e.target.value; } }),
579
+ btn('ds-btn pri', { 'data-doc-rejok': p.id }, 'Reject', () => rejectProp(p)), btn('ds-btn', {}, 'Cancel', () => { S.rejFor = null; draw(); }))
580
+ : el('div', { class: 'ds-acts' }, btn('ds-btn', { 'data-doc-showin': p.id }, [c3ic('eye', 'sm'), 'Show it in the document'], () => { S.preview[p.id] = true; go('read'); const c = p.sections[0]; if (c) goEl(c.kind === 'added' ? '[data-dsec-new="' + c.name + '"]' : '[data-dsec="' + c.name + '"]'); }),
581
+ demo ? null : btn('ds-btn', { 'data-doc-refine': p.id }, [c3ic('reply', 'sm'), 'Change the request'], () => { S.prompt = p.prompt + ', and '; draw(); const t = ui.body.querySelector('#dpr'); if (t) { t.focus(); t.setSelectionRange(t.value.length, t.value.length); } }),
582
+ btn('ds-btn', { 'data-doc-rej': p.id }, [c3ic('x', 'sm'), 'Reject'], () => { S.rejFor = p.id; S.rejWhy = ''; draw(); const i = ui.body.querySelector('#drejwhy'); if (i) i.focus(); }));
583
+ return el('div', { class: 'ds-prop', id: 'dprop-' + p.id, 'data-doc-prop-card': p.id }, hd, el('div', { class: 'pb' }, why, chs, host,
584
+ el('p', { class: 'ds-note' }, 'Logged as text by the agent, accepted by you. Health checks the document again after.'), rej));
585
+ }
586
+ async function acceptProp(p, hash) {
587
+ const names = p.sections.filter((c) => (S.pick[p.id] || {})[c.name] && !c.stale).map((c) => c.name);
588
+ const r = await pmPost('/api/doc-sheet/accept', { ...base, id: p.id, hash, sections: names }).catch(() => null);
589
+ if (!r || !r.ok) { say('Not accepted: ' + ((r && (r.error || r.reason)) || 'the proposal changed.')); await loadProps(); draw(); return; }
590
+ const v = nextV(doc.v); if (v) doc.v = v; if (doc.st === 'stale') doc.st = 'current';
591
+ S.changed = true; S.preview[p.id] = false; delete S.pick[p.id];
592
+ await reloadAll();
593
+ say('Accepted ' + plural(names.length, 'section') + (v ? ' as ' + v : '') + '. The edit log names the agent and you.');
594
+ }
595
+ async function rejectProp(p) {
596
+ const w = String(S.rejWhy || '').trim();
597
+ if (!w) { say('Say why you are rejecting it'); const i = ui.body.querySelector('#drejwhy'); if (i) i.focus(); return; }
598
+ const r = await pmPost('/api/doc-sheet/reject', { ...base, id: p.id, why: w }).catch(() => null);
599
+ if (!r || !r.ok) { say((r && r.error) || 'It was not rejected.'); return; }
600
+ S.rejFor = null; S.rejWhy = ''; S.log = null; await loadProps(); draw(); say('Rejected. The reason is in the edit log.');
601
+ }
602
+ function propsBody() {
603
+ if (generated) return el('div', { class: 'fold w', role: 'note', 'data-doc-nostore': 'ask' }, c3ic('lock', 'sm'), generated.why);
604
+ const op = openProps(); const closed = (S.props || []).filter((p) => p.state !== 'proposed');
605
+ const list = S.props === null ? el('div', { class: 'fold', role: 'status' }, c3ic('clock', 'sm'), 'Reading the proposals.')
606
+ : S.propsErr ? el('div', { class: 'fold w', role: 'alert' }, c3ic('alert', 'sm'), S.propsErr)
607
+ : op.length ? op.map(propCard) : el('div', { class: 'fold', 'data-doc-noprops': '' }, c3ic('check', 'sm'), 'No open proposals.');
608
+ return el('div', { class: 'ds', 'data-doc-proposals': '' }, staleBanner(), askBox(), el('h3', { class: 'ds-h' }, 'Open proposals · ' + op.length), list,
609
+ closed.length ? [el('h3', { class: 'ds-h' }, 'Closed'), el('ul', { class: 'ds-closed hx' }, closed.map((p) => el('li', { 'data-doc-closed': p.id },
610
+ el('span', { class: 'chip2 ' + (p.state === 'accepted' ? 'ds-good' : 'dim') }, c3ic(p.state === 'accepted' ? 'check' : 'x', 'sm'), p.state === 'accepted' ? 'Accepted' : 'Rejected'),
611
+ el('span', null, el('b', null, short(p.prompt)), el('small', null, p.state === 'accepted' ? (p.taken && p.of && p.taken.length < p.of ? p.taken.length + ' of ' + p.of + ' sections: ' : 'Sections: ') + (p.taken || []).join(', ') : 'Why: ' + (p.why || 'no reason'))),
612
+ el('span', { class: 'w' }, dayName(p.closed_at || p.at)))))] : null);
280
613
  }
281
614
 
282
- /* History */
283
- const histBody = () => {
284
- if (S.revs === null) return el('div', { class: 'fold', role: 'status' }, c3ic('clock', 'sm'), 'Reading the history.');
285
- if (!S.revs.length) return el('div', { class: 'fold' }, c3ic('info', 'sm'), S.revsErr || 'No revision is recorded yet. The first save through this sheet starts the history.');
286
- const rs = S.revs.slice().reverse();
287
- const cmp = S.cmp != null ? S.revs.find((r) => r.rev === S.cmp) : null;
288
- return el('div', { class: 'dpn' }, el('ul', { class: 'hx' }, rs.map((r) => {
289
- const prev = S.revs.find((x) => x.rev === r.rev - 1);
290
- return el('li', null, el('span', { class: 'chip2', style: 'color:' + (byName(r.by) === 'Agent' ? 'var(--purple)' : 'var(--acc)') }, c3ic(byName(r.by) === 'Agent' ? 'spark' : 'user', 'sm'), byName(r.by)),
291
- el('span', null, el('b', null, r.v || 'Revision ' + r.rev), ' ' + (r.why || ''), el('small', null, prev ? el('button', { type: 'button', class: 'chip2 lk', 'data-doc-cmp': String(r.rev), onclick: () => cmpWith(r.rev) }, c3ic('eye', 'sm'), 'Compare with ' + (prev.v || 'revision ' + prev.rev)) : 'First version')),
292
- el('span', { class: 'w' }, dayName(r.at)));
293
- })), cmp ? el('div', { class: 'dpn' }, el('h4', { style: 'margin:0;font-size:var(--fs-3)' }, ((S.revs.find((x) => x.rev === cmp.rev - 1) || {}).v || 'Revision ' + (cmp.rev - 1)) + ' to ' + (cmp.v || 'revision ' + cmp.rev)), S.cmpNode || el('div', { class: 'fold' }, c3ic('clock', 'sm'), 'Comparing.')) : null);
294
- };
615
+ /* ── Edit log ── */
616
+ function logEntries() {
617
+ const L = S.log || { revisions: [], events: [] };
618
+ const out = [];
619
+ for (const r of L.revisions) {
620
+ const ag = isAgentRev(r); const outside = r.via === 'outside' || r.via === 'first' || (r.by === 'baseline' && !r.via);
621
+ const title = r.via === 'proposal' ? 'Proposal accepted' : r.via === 'outside' || (r.by === 'baseline' && !r.via) ? 'Changed outside the sheet' : r.via === 'first' ? 'First seen by the sheet' : 'Edited in this sheet';
622
+ out.push({ kind: 'rev', at: r.at, r, ag, outside, people: !ag, title: (r.v ? r.v + ' · ' : 'Revision ' + r.rev + ' · ') + title });
623
+ }
624
+ for (const e of L.events) {
625
+ const t = { proposed: 'The agent drafted a proposal', revised: 'The agent revised a proposal', rejected: 'Proposal rejected', asked: 'You asked the agent', 'ask-failed': 'The agent did not draft the change', marked: 'Marked as still current' }[e.kind] || e.kind;
626
+ out.push({ kind: e.kind, at: e.at, e, ag: e.by === 'agent' || e.kind === 'proposed' || e.kind === 'revised' || e.kind === 'ask-failed', outside: false, people: e.by === 'pm' || e.kind === 'rejected' || e.kind === 'asked' || e.kind === 'marked', title: t });
627
+ }
628
+ return out.sort((a, b) => String(b.at).localeCompare(String(a.at)));
629
+ }
630
+ function logRow(x) {
631
+ const r = x.r; const e = x.e;
632
+ const av = el('span', { class: 'ds-av ' + (x.kind === 'ask-failed' ? 'w' : x.ag ? 'a' : 'h') }, x.ag ? c3ic(x.kind === 'ask-failed' ? 'alert' : 'spark', 'sm') : (r ? (whoOf(r)[0] || '?') : 'Y').toUpperCase());
633
+ const who = [];
634
+ if (r) {
635
+ if (r.via === 'proposal') who.push(el('span', { class: 'ds-ag' }, c3ic('spark', 'sm'), ' Text by the agent'), el('span', { class: 'ds-pe' }, c3ic('user', 'sm'), ' Accepted by you'), el('span', null, 'Proposal ' + String(r.proposal || '').replace(/^prop-/, '') + (r.sections && r.of ? ', ' + r.sections.length + ' of ' + r.of + ' sections' : '')));
636
+ else if (x.outside) who.push(el('span', { class: x.ag ? 'ds-ag' : 'ds-pe' }, c3ic(x.ag ? 'spark' : 'user', 'sm'), ' ' + (r.git && r.git.author ? r.git.author : 'Unknown author')), el('span', { class: 'ds-warn' }, c3ic('git', 'sm'), ' ' + (r.git && r.git.commit ? 'commit ' + r.git.commit : 'not committed') + (r.via === 'first' ? ', before the edit log' : ', outside PMOS')));
637
+ else who.push(el('span', { class: 'ds-pe' }, c3ic('user', 'sm'), ' ' + byName(r.by)), el('span', null, 'This sheet'));
638
+ } else if (x.ag) who.push(el('span', { class: 'ds-ag' }, c3ic('spark', 'sm'), ' The agent' + (e.session ? ' · session ' + e.session : '')));
639
+ else who.push(el('span', { class: 'ds-pe' }, c3ic('user', 'sm'), ' You'));
640
+ let why = null;
641
+ if (r) {
642
+ const noReason = x.outside && !r.why_known && !r.why_added;
643
+ if (r.why_added) why = el('p', { class: 'why' }, r.why_added, el('span', { class: 'dim' }, ' (reason added ' + dayName(r.why_added_at) + ')'));
644
+ else if (noReason && S.whyFor === r.rev) why = el('div', { class: 'ds-whyadd' }, el('input', { type: 'text', id: 'daddwhy', 'data-doc-addwhy': '', placeholder: 'Why did this change?', 'aria-label': 'Reason', value: S.whyText, onInput: (ev) => { S.whyText = ev.target.value; } }),
645
+ btn('ds-btn pri', { 'data-doc-whyok': String(r.rev) }, 'Add the reason', () => addWhy(r)), btn('ds-btn', {}, 'Cancel', () => { S.whyFor = null; draw(); }));
646
+ else if (noReason) why = el('p', { class: 'why none' }, c3ic('alert', 'sm'), ' No reason recorded: this change was made outside PMOS. ', btn('chip2 lk ds-pe', { 'data-doc-whyadd': String(r.rev) }, 'Add a reason', () => { S.whyFor = r.rev; S.whyText = ''; draw(); const i = ui.body.querySelector('#daddwhy'); if (i) i.focus(); }));
647
+ else if (r.why) why = el('p', { class: 'why' }, r.why);
648
+ } else {
649
+ const t = e.kind === 'asked' ? '“' + e.prompt + '”' : e.kind === 'proposed' || e.kind === 'revised' ? 'For: “' + (e.prompt || '') + '”' : e.why || '';
650
+ if (t) why = el('p', { class: 'why' }, t);
651
+ }
652
+ const tags = [];
653
+ if (r) {
654
+ const prev = S.texts[r.rev - 1]; const cur = S.texts[r.rev];
655
+ const secs = r.sections || (prev != null && cur != null ? changedSections(prev, cur).map((c) => c.name) : null);
656
+ if (secs) secs.forEach((s) => tags.push(el('span', { class: 'tag' }, s)));
657
+ if (prev != null && cur != null) { const [a, d] = wordCounts(prev, cur); tags.push(el('span', { class: 'tag add' }, '+' + a), el('span', { class: 'tag del' }, '−' + d)); }
658
+ }
659
+ const acts = [];
660
+ if (r && r.rev > 1) acts.push(btn('chip2 lk', { 'data-doc-cmp': String(r.rev) }, [c3ic('eye', 'sm'), S.cmp === r.rev ? 'Hide the change' : 'Show the change'], () => cmpWith(r.rev)));
661
+ if (e && (e.kind === 'proposed' || e.kind === 'revised') && openProps().some((p) => p.id === e.id)) acts.push(btn('chip2 lk ds-ag', {}, [c3ic('spark', 'sm'), 'Review it'], () => { S.openProp = e.id; go('ask'); }));
662
+ const cmp = r && S.cmp === r.rev ? el('div', { class: 'ds-cmp', 'data-doc-cmpbox': '' }, S.cmpNode || el('div', { class: 'fold' }, c3ic('clock', 'sm'), 'Comparing.')) : null;
663
+ return el('li', { 'data-log-kind': x.kind, ...(r ? { 'data-log-rev': String(r.rev) } : {}) }, av, el('div', { class: 'ds-le' },
664
+ el('div', { class: 'l1' }, el('b', null, x.title), el('span', { class: 'when' }, whenOf(x.at))), el('div', { class: 'who' }, who), why,
665
+ tags.length ? el('div', { class: 'tags' }, tags) : null, acts.length ? el('div', { class: 'ds-acts' }, acts) : null, cmp));
666
+ }
295
667
  async function cmpWith(rev) {
296
668
  if (S.cmp === rev) { S.cmp = null; draw(); return; }
297
669
  S.cmp = rev; S.cmpNode = null; draw();
298
670
  const [a, b] = await Promise.all([textAt(rev - 1), textAt(rev)]);
299
671
  if (S.cmp !== rev) return;
300
- S.cmpNode = a == null || b == null ? el('div', { class: 'fold w' }, c3ic('alert', 'sm'), 'That revision could not be read.') : diffNode(changedSections(a, b));
672
+ const D = a == null || b == null ? null : changedSections(a, b);
673
+ S.cmpNode = !D ? el('div', { class: 'fold w' }, c3ic('alert', 'sm'), 'That revision could not be read.')
674
+ : D.length ? D.map((d) => [el('h4', null, d.name), md(diffMd(d.kind === 'added' ? null : d.before, d.after))]) : el('div', { class: 'dim' }, 'No change in the text.');
301
675
  draw();
302
676
  }
677
+ async function addWhy(r) {
678
+ const w = String(S.whyText || '').trim(); if (!w) { const i = ui.body.querySelector('#daddwhy'); if (i) i.focus(); return; }
679
+ const j = await pmPost('/api/doc-sheet/why', { ...base, rev: r.rev, why: w }).catch(() => null);
680
+ if (!j || !j.ok) { say((j && j.error) || 'The reason was not added.'); return; }
681
+ S.whyFor = null; await loadLog(); draw(); say('Reason added. The revision itself is unchanged.');
682
+ }
683
+ function logBody() {
684
+ if (generated) return el('div', { class: 'fold', 'data-doc-nostore': 'hist' }, c3ic('info', 'sm'), generated.why);
685
+ if (!S.log) return el('div', { class: 'fold', role: 'status' }, c3ic('clock', 'sm'), 'Reading the edit log.');
686
+ const F = [['all', 'All'], ['people', 'People'], ['agent', 'Agent'], ['outside', 'Outside PMOS']];
687
+ const all = logEntries();
688
+ const rows = all.filter((x) => S.logF === 'all' || (S.logF === 'people' && x.people) || (S.logF === 'agent' && x.ag) || (S.logF === 'outside' && x.outside));
689
+ return el('div', { class: 'ds', 'data-doc-log': '' }, el('div', { class: 'ds-rbar' }, el('div', { class: 'seg', role: 'group', 'aria-label': 'Filter' }, F.map(([k, l]) => btn('', { 'aria-pressed': String(S.logF === k), 'data-doc-logf': k }, l, () => { S.logF = k; draw(); }))),
690
+ el('span', { class: 'ds-mono' }, plural(all.length, 'entry').replace('entrys', 'entries') + ' · append-only')),
691
+ S.logErr ? el('div', { class: 'fold w', role: 'alert' }, c3ic('alert', 'sm'), S.logErr) : null,
692
+ rows.length ? el('ul', { class: 'ds-log' }, rows.map(logRow)) : el('div', { class: 'fold' }, c3ic('info', 'sm'), all.length ? 'Nothing in this filter.' : 'Nothing is recorded yet. The first save through this sheet starts the log.'));
693
+ }
303
694
 
304
- /* Health */
305
- const healthBody = () => {
695
+ /* ── Health ── */
696
+ function healthBody() {
306
697
  const h = S.health;
307
698
  if (!h) return el('div', { class: 'fold', role: 'status' }, c3ic('clock', 'sm'), 'Checking this document.');
308
699
  if (h.err) return el('div', { class: 'fold w', role: 'alert' }, c3ic('alert', 'sm'), 'The checks could not run: ' + h.err + '. Nothing is shown.');
309
700
  const unanswered = h.checks.filter((c) => c.pass === null).length;
310
- return el('div', { class: 'dpn' }, el('div', { class: 'chips2', 'data-doc-health': '' }, el('span', { class: 'ki', style: 'color:' + hcol(h) }, c3ic('shield')), el('b', null, h.passed + ' of ' + h.total + ' checks pass')),
311
- unanswered ? el('p', { class: 'dim', style: 'font-size:var(--fs-2);margin:0' }, unanswered + ' check' + (unanswered === 1 ? '' : 's') + ' not counted: not enough input.') : null,
701
+ return el('div', { class: 'ds' }, el('div', { class: 'chips2', 'data-doc-health': '' }, el('span', { class: 'ki ' + hcls(h) }, c3ic('shield')), el('b', null, h.passed + ' of ' + h.total + ' checks pass')),
702
+ unanswered ? el('p', { class: 'ds-note' }, plural(unanswered, 'check') + ' not counted: not enough input.') : null,
312
703
  el('ul', { class: 'chgl' }, h.checks.map((c) => el('li', { class: c.pass === null ? '' : c.pass ? 'ok' : 'bad', 'data-check': c.id }, c3ic(c.pass === null ? 'info' : c.pass ? 'check' : 'alert', 'sm'),
313
- el('span', null, (c.name || NAMES[c.id] || c.id) + ': ' + c.reason + ' ', c.pass === false && !generated ? el('button', { type: 'button', class: 'chip2 lk', 'data-doc-fix': c.id, onclick: () => { S.prompt = c.fix || 'Fix: ' + (NAMES[c.id] || c.id) + ' (' + c.reason + ')'; go('ask'); } }, c3ic('spark', 'sm'), 'Fix with the agent') : null),
704
+ el('span', null, (c.name || NAMES[c.id] || c.id) + ': ' + c.reason + ' ',
705
+ c.id === 'fresh' && c.pass === false && Array.isArray(c.sources) && c.sources.length ? el('small', { class: 'ds-mono' }, ' ' + c.sources.map((s) => s.path).join(', ') + ' ') : null,
706
+ c.id === 'fresh' && c.pass === false && !generated ? btn('chip2 lk', { 'data-doc-hmark': '' }, 'Mark as still current', () => { S.staleWhy = ''; go('read'); const i = ui.body.querySelector('#dstalewhy'); if (i) i.focus(); }) : null,
707
+ c.pass === false && !generated ? btn('chip2 lk ds-ag', { 'data-doc-fix': c.id }, [c3ic('spark', 'sm'), 'Fix with the agent'], () => { S.prompt = c.fix || 'Fix: ' + (NAMES[c.id] || c.id) + ' (' + c.reason + ')'; go('ask'); }) : null),
314
708
  el('span', { class: 'tag' }, c.pass === null ? 'no input' : c.pass ? 'pass' : 'fail')))),
315
- el('div', { class: 'dim', style: 'font-size:var(--fs-2)' }, 'Health answers one question: can I trust this document without rereading it? Checks are recomputed from the text every time it changes.'));
316
- };
709
+ el('p', { class: 'ds-note' }, 'Health answers one question: can I trust this document without rereading it? The checks run again every time the text changes.'));
710
+ }
317
711
 
318
712
  const linksBody = () => (relations ? relations(doc, (d) => { if (S.open) S.open(d); }) : el('div', { class: 'fold' }, c3ic('info', 'sm'), 'No relation is recorded for this document.'));
319
713
 
320
714
  /* a load that finished in the background must not redraw under a hand that is typing */
321
- const drawBg = () => { const a = document.activeElement; if (S.mode === 'edit' && a && ui && ui.body.contains(a) && a.closest('[data-sec],#dwhy')) return; draw(); };
715
+ const drawBg = () => { const a = document.activeElement; if (ui && a && ui.body.contains(a) && a.closest('input,textarea,[contenteditable=true]')) { refreshTop(); return; } draw(); };
716
+ const top = () => el('div', { class: 'ds-top' }, head(), seg(), S.mode === 'edit' && S.ed ? toolbar() : null,
717
+ S.leave ? el('div', { class: 'fold w', role: 'alert', 'data-doc-leave': '' }, c3ic('alert', 'sm'), 'You have unsaved changes in ' + plural(dirty().length, 'section') + '. ',
718
+ btn('ds-btn', {}, 'Keep editing', () => { S.leave = null; drawEdit(); }), btn('ds-btn', { 'data-doc-discard': '' }, 'Discard and leave', () => { const m = S.leave; S.ed = null; S.leave = null; go(m); })) : null);
719
+ function refreshTop() { const t = ui && ui.body.querySelector('.ds-top'); if (t) t.replaceWith(top()); }
322
720
  function draw() {
323
721
  if (!ui) return;
324
- const body = S.mode === 'read' ? readBody(false) : S.mode === 'edit' ? readBody(true) : S.mode === 'ask' ? askBody() : S.mode === 'hist' ? histBody() : S.mode === 'health' ? healthBody() : linksBody();
325
- const keep = ui.body.querySelector('textarea,input');
326
- const had = keep && document.activeElement === keep ? { id: keep.id, s: keep.selectionStart } : null;
327
- ui.body.replaceChildren(el('div', { class: 'dpn' }, head(), seg(), body));
722
+ if (S.mode === 'edit' && S.ed && S.ed.kind === 'rich' && S.ed.node) keepRange();
723
+ const body = S.mode === 'read' ? readBody() : S.mode === 'edit' ? editBody() : S.mode === 'ask' ? propsBody() : S.mode === 'hist' ? logBody() : S.mode === 'health' ? healthBody() : linksBody();
724
+ const keep = ui.body.querySelector('textarea:focus,input[type=text]:focus');
725
+ const had = keep ? { id: keep.id, s: keep.selectionStart } : null;
726
+ ui.body.replaceChildren(el('div', { class: 'dpn ds' }, top(), body));
328
727
  if (had && had.id) { const n = ui.body.querySelector('#' + had.id); if (n) { n.focus(); try { n.setSelectionRange(had.s, had.s); } catch { /* not a text field */ } } }
728
+ if (S.mode === 'ask' && S.openProp) { const id = S.openProp; S.openProp = null; goEl('[data-doc-prop-card="' + id + '"]'); }
329
729
  }
330
730
 
331
- ui = openSheet({ title: doc.name, icon: 'doc', dw: true, body: el('div', { class: 'dpn' }), onClose: () => { S.closed = true; if (S.changed && onChanged) onChanged(); } });
731
+ document.addEventListener('selectionchange', keepRange);
732
+ ui = openSheet({ title: doc.name, icon: 'doc', dw: true, body: el('div', { class: 'dpn' }), onClose: () => { S.closed = true; document.removeEventListener('selectionchange', keepRange); if (S.changed && onChanged) onChanged(); } });
332
733
  S.open = (d) => { ui.close(); if (S.openOther) S.openOther(d); };
333
734
  ui.setOpener = (fn) => { S.openOther = fn; };
334
735
  draw();
335
736
  (async () => {
336
- await loadText(); drawBg();
337
- if (S.mode === 'health' && !S.hAsk) loadHealth();
338
- if (S.mode === 'hist') loadRevs().then(draw);
339
- loadHealth();
340
- await loadRevs(); loadMeta();
737
+ await loadText();
738
+ if (S.mode === 'edit') startEdit();
739
+ drawBg();
740
+ await Promise.all([loadRevs(), loadProps(), loadHealth()]);
741
+ if (S.mode === 'hist') await loadLog();
742
+ drawBg(); loadMeta();
341
743
  })();
342
744
  return ui;
343
745
  }