runlist 0.0.0 → 0.94.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1114 -6
  3. package/assets/app/app.css +283 -0
  4. package/assets/app/app.mjs +381 -0
  5. package/assets/app/block-editor.mjs +182 -0
  6. package/assets/app/block-model.mjs +52 -0
  7. package/assets/app/document-create.mjs +86 -0
  8. package/assets/app/document-lifecycle.mjs +36 -0
  9. package/assets/app/document-stages.mjs +22 -0
  10. package/assets/app/document-views.mjs +33 -0
  11. package/assets/app/document-yardstick.mjs +258 -0
  12. package/assets/app/editor-navigation.mjs +51 -0
  13. package/assets/app/filing-navigation.mjs +138 -0
  14. package/assets/app/git-changes.mjs +80 -0
  15. package/assets/app/git-commit.mjs +46 -0
  16. package/assets/app/index.html +70 -0
  17. package/assets/app/library-navigation.mjs +93 -0
  18. package/assets/app/outline.mjs +18 -0
  19. package/assets/app/quick-navigation.mjs +52 -0
  20. package/assets/app/record-navigation.mjs +78 -0
  21. package/assets/app/recovery-center.mjs +53 -0
  22. package/assets/app/semantic-search.mjs +14 -0
  23. package/assets/app/settings.mjs +15 -0
  24. package/assets/app/shared.mjs +104 -0
  25. package/assets/app/stage-source.mjs +75 -0
  26. package/assets/app/template-editor.mjs +47 -0
  27. package/assets/app/theme.mjs +25 -0
  28. package/assets/app/transport.mjs +43 -0
  29. package/assets/contracts/source-editing-v1.md +232 -0
  30. package/assets/opencode/plugin.js +162 -0
  31. package/assets/schemas/runlist-record-v1.schema.json +912 -0
  32. package/bin/dotmd.mjs +5 -0
  33. package/bin/runlist.mjs +2714 -0
  34. package/package.json +53 -6
  35. package/plugins/runlist-codex/.codex-plugin/plugin.json +18 -0
  36. package/plugins/runlist-codex/bin/runlist-hook +86 -0
  37. package/plugins/runlist-codex/hooks/hooks.json +16 -0
  38. package/plugins/runlist-codex/skills/runlist/SKILL.md +25 -0
  39. package/runlist.config.example.mjs +370 -0
  40. package/scripts/postinstall.mjs +56 -0
  41. package/src/agent-context.mjs +201 -0
  42. package/src/ai.mjs +30 -0
  43. package/src/app-create.mjs +169 -0
  44. package/src/app-filing.mjs +139 -0
  45. package/src/app-git-commits.mjs +48 -0
  46. package/src/app-git.mjs +180 -0
  47. package/src/app-library.mjs +152 -0
  48. package/src/app-lifecycle-worker.mjs +12 -0
  49. package/src/app-lifecycle.mjs +79 -0
  50. package/src/app-records.mjs +134 -0
  51. package/src/app-search.mjs +80 -0
  52. package/src/app-semantic-search.mjs +105 -0
  53. package/src/app-service.mjs +208 -0
  54. package/src/app-yardstick.mjs +269 -0
  55. package/src/app.mjs +102 -0
  56. package/src/atomic-mutation.mjs +1736 -0
  57. package/src/baton.mjs +435 -0
  58. package/src/body-link.mjs +48 -0
  59. package/src/bulk-tag.mjs +174 -0
  60. package/src/check-collapse.mjs +75 -0
  61. package/src/claude-commands.mjs +75 -0
  62. package/src/cli-editor.mjs +8 -0
  63. package/src/code-refs.mjs +545 -0
  64. package/src/codex-integration.mjs +81 -0
  65. package/src/color.mjs +14 -0
  66. package/src/commands.mjs +374 -0
  67. package/src/completions.mjs +93 -0
  68. package/src/config-edit.mjs +606 -0
  69. package/src/config.mjs +705 -0
  70. package/src/decision-action.mjs +58 -0
  71. package/src/decision.mjs +220 -0
  72. package/src/decisions.mjs +787 -0
  73. package/src/deps.mjs +317 -0
  74. package/src/desktop-install.mjs +158 -0
  75. package/src/desktop-runtime.mjs +4 -0
  76. package/src/desktop.mjs +25 -0
  77. package/src/diff.mjs +101 -0
  78. package/src/doctor.mjs +591 -0
  79. package/src/durable-rename.mjs +55 -0
  80. package/src/errors-read.mjs +44 -0
  81. package/src/export.mjs +496 -0
  82. package/src/extractors.mjs +101 -0
  83. package/src/filing.mjs +255 -0
  84. package/src/fix-membership.mjs +134 -0
  85. package/src/fix-refs.mjs +174 -0
  86. package/src/flags.mjs +318 -0
  87. package/src/frontmatter-fix.mjs +195 -0
  88. package/src/frontmatter.mjs +252 -0
  89. package/src/git-commit-filters.mjs +32 -0
  90. package/src/git-commit-protocol.mjs +198 -0
  91. package/src/git-commit-signing.mjs +75 -0
  92. package/src/git-commit-store.mjs +26 -0
  93. package/src/git-commit-support.mjs +16 -0
  94. package/src/git-commit-worker.mjs +32 -0
  95. package/src/git.mjs +909 -0
  96. package/src/glossary-check.mjs +35 -0
  97. package/src/glossary.mjs +243 -0
  98. package/src/graph.mjs +298 -0
  99. package/src/guard.mjs +488 -0
  100. package/src/health.mjs +212 -0
  101. package/src/hints.mjs +129 -0
  102. package/src/host-integration.mjs +339 -0
  103. package/src/hub-membership.mjs +284 -0
  104. package/src/hub.mjs +292 -0
  105. package/src/hud.mjs +436 -0
  106. package/src/index-file.mjs +163 -0
  107. package/src/index.mjs +478 -0
  108. package/src/init.mjs +512 -0
  109. package/src/install.mjs +222 -0
  110. package/src/journal-read.mjs +116 -0
  111. package/src/journal.mjs +472 -0
  112. package/src/lifecycle.mjs +1444 -0
  113. package/src/lint.mjs +386 -0
  114. package/src/managed-path.mjs +192 -0
  115. package/src/markdown-code-spans.mjs +58 -0
  116. package/src/migrate-prompts.mjs +171 -0
  117. package/src/migrate-template.mjs +170 -0
  118. package/src/migrate.mjs +90 -0
  119. package/src/misuse-read.mjs +61 -0
  120. package/src/model-request.mjs +33 -0
  121. package/src/model.mjs +523 -0
  122. package/src/modules.mjs +355 -0
  123. package/src/naming.mjs +55 -0
  124. package/src/native-action.mjs +59 -0
  125. package/src/native-record.mjs +244 -0
  126. package/src/new.mjs +1321 -0
  127. package/src/output-identity.mjs +111 -0
  128. package/src/parent-plan.mjs +58 -0
  129. package/src/parse-cache.mjs +99 -0
  130. package/src/path-containment.mjs +15 -0
  131. package/src/pickup-card.mjs +293 -0
  132. package/src/pickup.mjs +671 -0
  133. package/src/prompt.mjs +25 -0
  134. package/src/prompts.mjs +585 -0
  135. package/src/query.mjs +977 -0
  136. package/src/record-action.mjs +11 -0
  137. package/src/record-create.mjs +82 -0
  138. package/src/reference-planner.mjs +388 -0
  139. package/src/reference-values.mjs +11 -0
  140. package/src/rename.mjs +107 -0
  141. package/src/render.mjs +678 -0
  142. package/src/roadmap.mjs +184 -0
  143. package/src/runlist.mjs +944 -0
  144. package/src/section.mjs +225 -0
  145. package/src/semantic-search-client.mjs +58 -0
  146. package/src/ship.mjs +166 -0
  147. package/src/show.mjs +101 -0
  148. package/src/skill-drift.mjs +98 -0
  149. package/src/source-editor.mjs +491 -0
  150. package/src/stages.mjs +73 -0
  151. package/src/state-migration.mjs +123 -0
  152. package/src/stats.mjs +200 -0
  153. package/src/status-metadata.mjs +87 -0
  154. package/src/statuses.mjs +721 -0
  155. package/src/summary.mjs +74 -0
  156. package/src/surfaces.mjs +28 -0
  157. package/src/sync-status.mjs +316 -0
  158. package/src/template-store.mjs +62 -0
  159. package/src/update.mjs +338 -0
  160. package/src/use.mjs +58 -0
  161. package/src/util.mjs +337 -0
  162. package/src/validate.mjs +794 -0
  163. package/src/watch.mjs +56 -0
  164. package/src/xref.mjs +406 -0
  165. package/src/yardstick-command.mjs +178 -0
  166. package/src/yardstick.mjs +112 -0
@@ -0,0 +1,138 @@
1
+ /** Read-only filing report. Construction never requests or scans documents. */
2
+ export function filingNavigation({ state, $, api, open, showLibrary, checkout, notice }) {
3
+ const root = $('filing-home');
4
+ let scanId = null, sequence = 0, detailSequence = 0, offset = 0, selected = null, timer, owner;
5
+ const element = (tag, text, className) => { const node = document.createElement(tag); if (text !== undefined) node.textContent = text; if (className) node.className = className; return node; };
6
+ const button = (label, action) => { const node = element('button', label, 'quiet small'); node.type = 'button'; node.onclick = run(action); return node; };
7
+ const run = fn => async () => { try { await fn(); } catch (error) { notice(error.message, true); } };
8
+ const heading = element('h1', 'Filing'); heading.id = 'filing-title'; heading.tabIndex = -1;
9
+ const description = element('p', 'Where live plans are homed in status-bearing hub table rows. Links in prose are references. This report changes no files.', 'muted');
10
+ const feedback = element('p'); feedback.id = 'filing-feedback'; feedback.setAttribute('role', 'status'); feedback.setAttribute('aria-live', 'polite');
11
+ const totals = element('div', undefined, 'filing-totals'); totals.id = 'filing-totals';
12
+ const controls = element('div', undefined, 'filing-controls');
13
+ const searchLabel = element('label', 'Find a plan');
14
+ const search = element('input'); search.type = 'search'; search.id = 'filing-search'; search.placeholder = 'Title or path…'; searchLabel.append(search);
15
+ const groupLabel = element('label', 'Show');
16
+ const group = element('select'); group.id = 'filing-group';
17
+ for (const [value, label] of [['all', 'All plans'], ['unfiled', 'Unfiled'], ['multiple', 'Multiple homes'], ['no-category', 'No category'], ['parent', 'Through parent'], ['direct', 'Direct home']]) {
18
+ const option = element('option', label); option.value = value; group.append(option);
19
+ }
20
+ groupLabel.append(group);
21
+ const refresh = button('Refresh', () => load({ refresh: true })); refresh.id = 'filing-refresh';
22
+ controls.append(searchLabel, groupLabel, refresh);
23
+ const range = element('p', '', 'muted'); range.id = 'filing-range';
24
+ const layout = element('div', undefined, 'filing-layout');
25
+ const list = element('div', undefined, 'filing-list'); list.id = 'filing-list';
26
+ const detail = element('section', undefined, 'filing-detail'); detail.id = 'filing-detail'; detail.setAttribute('aria-label', 'Filing evidence');
27
+ layout.append(list, detail);
28
+ const pager = element('div', undefined, 'filing-pagination');
29
+ const previous = button('Previous', () => { offset = Math.max(0, offset - 50); return load(); }); previous.id = 'filing-previous';
30
+ const next = button('Next', () => { offset += 50; return load(); }); next.id = 'filing-next';
31
+ pager.append(previous, next);
32
+ root.replaceChildren(heading, description, controls, feedback, totals, range, layout, pager);
33
+ const visible = () => state.mode === 'filing' && !root.hidden;
34
+ const call = (op, params = {}) => api(`filing?${new URLSearchParams({ op, ...(scanId ? { scanId } : {}), ...params })}`);
35
+ const label = plan => plan.filing === 'parent' ? `Through parent · ${plan.filedThrough}` : plan.filing === 'direct' ? 'Direct home' : plan.filing === 'unfiled' ? 'Unfiled' : plan.filing === 'observed' ? 'Observed home evidence · incomplete scan' : 'Home unknown · incomplete scan';
36
+ function deactivate() { sequence++; detailSequence++; clearTimeout(timer); root.hidden = true; $('library-filing').setAttribute('aria-pressed', 'false'); }
37
+ function invalidate() {
38
+ sequence++; detailSequence++; scanId = null;
39
+ if (visible()) { feedback.textContent = 'Saved files changed. Refresh to review current filing evidence.'; list.setAttribute('aria-busy', 'false'); refresh.disabled = false; search.disabled = false; group.disabled = false; previous.disabled = true; next.disabled = true; }
40
+ }
41
+ function renderSummary(result) {
42
+ if (!result.enabled) feedback.textContent = result.reason;
43
+ else if (result.state === 'scanning') feedback.textContent = `Reading filing evidence… ${result.progress.done} of ${result.progress.total} documents checked.`;
44
+ else if (!result.complete) feedback.textContent = result.reason ?? 'Scan incomplete. Observed evidence is available; unfiled and unique-home findings are unavailable. Refresh to retry.';
45
+ else feedback.textContent = `${result.totals.plans} live plans checked. Finding groups may overlap.`;
46
+ totals.replaceChildren();
47
+ if (result.totals) for (const [key, title] of [['unfiled', 'Unfiled'], ['multiplyFiled', 'Multiple homes'], ['noCategory', 'No category'], ['filedThroughParent', 'Through parent']]) totals.append(element('span', `${title}: ${result.totals[key]}`));
48
+ if (result.failureCount) {
49
+ const failures = element('details'), summary = element('summary', `${result.failureCount} unavailable sources or folders`); failures.append(summary);
50
+ for (const failure of result.failures ?? []) failures.append(element('p', `${failure.path ?? 'Inventory'}: ${failure.message}`));
51
+ if (result.failureCount > (result.failures?.length ?? 0)) failures.append(element('p', 'Additional failures omitted. Narrow the source problem and refresh.'));
52
+ totals.append(failures);
53
+ }
54
+ for (const option of group.options) option.disabled = option.value !== 'all' && !result.complete;
55
+ if (!result.complete) group.value = 'all';
56
+ }
57
+ function renderPage(result) {
58
+ renderSummary(result); offset = result.offset ?? 0;
59
+ range.textContent = result.total ? `${offset + 1}–${offset + result.plans.length} of ${result.total} plans` : '0 plans';
60
+ previous.disabled = !offset; next.disabled = !result.hasMore;
61
+ list.replaceChildren(...(result.plans ?? []).map(plan => {
62
+ const row = button('', () => inspect(plan.path)); row.className = 'filing-plan quiet'; row.setAttribute('aria-pressed', String(selected === plan.path)); row.dataset.path = plan.path;
63
+ row.append(element('strong', plan.title), element('span', plan.path, 'muted'), element('span', label(plan)));
64
+ if (plan.multiplyFiled) row.append(element('small', 'Multiple homes'));
65
+ if (plan.noCategory) row.append(element('small', 'No category'));
66
+ return row;
67
+ }));
68
+ if (!result.plans?.length) list.append(element('p', result.complete ? 'No plans match this filter.' : result.enabled ? 'No plan evidence is available in this scan.' : 'Enable filing in the repository configuration to use this report.', 'library-no-results'));
69
+ }
70
+ async function inspect(path, homeOffset = 0) {
71
+ if (!scanId || !visible()) return;
72
+ const own = ++detailSequence; selected = path;
73
+ for (const row of list.querySelectorAll('[data-path]')) row.setAttribute('aria-pressed', String(row.dataset.path === path));
74
+ detail.setAttribute('aria-busy', 'true');
75
+ try {
76
+ const result = await call('detail', { path, homeOffset: String(homeOffset), homeLimit: '20' });
77
+ if (own !== detailSequence || !visible()) return;
78
+ const nodes = [element('h2', result.title), element('p', result.path, 'muted'), element('p', label(result)), button('Open plan', () => open(result.path))];
79
+ if (result.filedThrough && result.filedThrough !== result.path) nodes.push(button('Open parent home', () => open(result.filedThrough)));
80
+ if (!result.homes.length) nodes.push(element('p', result.complete ? 'No status-bearing hub subject row homes this plan.' : 'No home observed. Missing sources prevent an unfiled finding.'));
81
+ for (const home of result.homes) {
82
+ const row = element('article', undefined, 'filing-home-row');
83
+ row.append(element('h3', `${home.hub}:${home.line}`), element('p', `Categories: ${home.categories.join(', ') || (result.complete ? 'No category' : 'No category observed · incomplete scan')}`), element('p', `Subject: ${home.subject} · Printed status: ${home.printedStatus}`));
84
+ if (home.categoriesShortened) row.append(element('p', 'Category display shortened. Open the hub for its full categories.', 'muted'));
85
+ if (home.current === 'unchanged') row.append(button('Open hub row', () => open(home.hub, { line: home.line, expectedRevision: home.revision })));
86
+ else { row.append(element('p', home.current === 'changed' ? 'Hub changed after this scan. Refresh before using this row location.' : 'Hub source is currently unavailable.', 'inline-note')); row.append(button('Open current hub', () => open(home.hub))); }
87
+ nodes.push(row);
88
+ }
89
+ if (result.totalHomes) nodes.push(element('p', `${result.homeOffset + 1}–${result.homeOffset + result.homes.length} of ${result.totalHomes} home rows`, 'muted'));
90
+ if (result.homeOffset) nodes.push(button('Previous home rows', () => inspect(path, Math.max(0, result.homeOffset - 20))));
91
+ if (result.hasMoreHomes) nodes.push(button('More home rows', () => inspect(path, result.homeOffset + 20)));
92
+ detail.replaceChildren(...nodes);
93
+ } catch (error) { if (own === detailSequence && visible()) detail.replaceChildren(element('p', `${error.message} Refresh to review current evidence.`, 'inline-note')); throw error; }
94
+ finally { if (own === detailSequence) detail.setAttribute('aria-busy', 'false'); }
95
+ }
96
+ async function load({ refresh: force = false } = {}) {
97
+ if (!visible()) return;
98
+ if (!force && !scanId) { feedback.textContent = 'Refresh to review current filing evidence.'; return; }
99
+ const own = ++sequence; list.setAttribute('aria-busy', 'true'); refresh.disabled = true;
100
+ if (force) { search.disabled = true; group.disabled = true; previous.disabled = true; next.disabled = true; }
101
+ try {
102
+ if (force) {
103
+ selected = null; detailSequence++; offset = 0; detail.replaceChildren(element('p', 'Select a plan to inspect its exact home evidence.', 'muted'));
104
+ let result = await call('start', { refresh: '1' });
105
+ if (own !== sequence || !visible()) return;
106
+ scanId = result.scanId ?? null; renderSummary(result);
107
+ while (result.state === 'scanning') {
108
+ await new Promise(resolve => setTimeout(resolve, 0));
109
+ if (own !== sequence || !visible()) return;
110
+ result = await call('advance');
111
+ if (own !== sequence || !visible()) return;
112
+ renderSummary(result);
113
+ }
114
+ if (!result.enabled) { renderPage({ ...result, plans: [], total: 0 }); return; }
115
+ }
116
+ const result = await call('page', { q: search.value, group: group.value, offset: String(offset), limit: '50' });
117
+ if (own !== sequence || !visible()) return;
118
+ renderPage(result);
119
+ } catch (error) {
120
+ if (own === sequence && visible()) { feedback.textContent = `${error.message} Try Refresh.`; if (error.code === 'filing-scan-stale') scanId = null; }
121
+ throw error;
122
+ } finally { if (own === sequence) { list.setAttribute('aria-busy', 'false'); refresh.disabled = false; search.disabled = false; group.disabled = false; } }
123
+ }
124
+ async function show() {
125
+ await showLibrary();
126
+ if (owner !== checkout()) { owner = checkout(); scanId = null; offset = 0; search.value = ''; group.value = 'all'; }
127
+ state.mode = 'filing'; root.hidden = false; $('empty').hidden = true; $('document').hidden = true;
128
+ for (const id of ['records-home', 'changes-home']) if ($(id)) $(id).hidden = true;
129
+ $('workspace').classList.add('library-start'); $('doc-title').textContent = 'Filing';
130
+ for (const name of ['all', 'hubs', 'plans', 'documents', 'flags', 'decisions', 'changes']) $('library-' + name)?.setAttribute('aria-pressed', 'false');
131
+ $('library-filing').setAttribute('aria-pressed', 'true');
132
+ await load({ refresh: true }); heading.focus();
133
+ }
134
+ $('library-filing').onclick = run(show);
135
+ search.oninput = () => { if (search.disabled) return; clearTimeout(timer); offset = 0; timer = setTimeout(() => run(load)(), 200); };
136
+ group.onchange = run(() => { offset = 0; return load(); });
137
+ return { show, load, deactivate, invalidate, visible };
138
+ }
@@ -0,0 +1,80 @@
1
+ import {escapeHtml as esc,diffHtml} from './shared.mjs';
2
+
3
+ export function gitChanges({$,api,state,checkout,showLibrary,open,returnToDraft=()=>open(state.doc.path),notice}) {
4
+ const selected=new Map();let data=null,offset=0,sequence=0,diffSequence=0,reviewed=null,timer,owner=null;
5
+ const root=$('changes-home');
6
+ const run=fn=>async()=>{try{await fn();}catch(error){notice(error.message,true);}};
7
+ const displayPath=value=>String(value).replace(/[\x00-\x1f\x7f]/g,c=>`\\u${c.charCodeAt(0).toString(16).padStart(4,'0')}`);
8
+ function selection() {$('changes-selected').textContent=`${selected.size} selected`;$('changes-clear').disabled=!selected.size;$('changes-commit').disabled=!selected.size||data?.commitSupport?.available===false;$('changes-commit').title=data?.commitSupport?.reason??'Review selected files before creating a local commit';}
9
+ function deactivate() {sequence++;diffSequence++;clearTimeout(timer);root.hidden=true;$('library-changes').setAttribute('aria-pressed','false');}
10
+ function invalidate() {data=null;$('library-changes-count').textContent='•';$('library-changes').title='Saved files changed. Open Changes to refresh.';}
11
+ function activate() {
12
+ state.mode='changes';root.hidden=false;$('empty').hidden=true;$('document').hidden=true;$('records-home').hidden=true;
13
+ $('workspace').classList.add('library-start');$('doc-title').textContent='Changes';
14
+ for(const name of ['all','hubs','plans','documents','flags','decisions'])$('library-'+name).setAttribute('aria-pressed','false');
15
+ $('library-changes').setAttribute('aria-pressed','true');
16
+ const url=new URL(location.href);for(const key of ['path','ref','from','record'])url.searchParams.delete(key);url.searchParams.set('view','changes');history.replaceState(null,'',url);
17
+ $('changes-draft').hidden=!(state.doc&&(state.dirty||state.pending));
18
+ }
19
+ function rows() {
20
+ const scroll=$('changes-list').scrollTop;
21
+ const focused=document.activeElement,activePath=focused?.getAttribute('data-git-path'),activeTag=focused?.tagName;
22
+ $('changes-list').replaceChildren(...(data?.changes??[]).map(row=>{
23
+ const item=document.createElement('div');item.className='changes-row';
24
+ const check=document.createElement('input');check.type='checkbox';check.checked=selected.has(row.path);check.disabled=!row.eligible;check.setAttribute('aria-label',`Select ${displayPath(row.path)}`);check.title=row.blockedReason??'Select the whole saved file';
25
+ check.setAttribute('data-git-path',row.path);
26
+ check.onchange=()=>{if(check.checked){if(selected.size>=100){check.checked=false;notice('Review up to 100 selected files at a time.',true);return;}selected.set(row.path,row);}else selected.delete(row.path);selection();};
27
+ const button=document.createElement('button');button.className='changes-file quiet';button.setAttribute('aria-pressed',String(reviewed===row.path));
28
+ button.setAttribute('data-git-path',row.path);
29
+ button.innerHTML=`<strong>${esc(row.title??displayPath(row.path))}</strong><span class="changes-path">${esc(displayPath(row.path))}</span>${row.original?`<small>From ${esc(displayPath(row.original))}</small>`:''}<span class="changes-meta">${esc(row.kind)}${row.staged?' · Staged':''}${row.partial&&!row.filterReviewRequired?' · Partial staging':''}${row.filterReviewRequired?' · Clean-filter review required':''}</span>${row.blockedReason?`<small class="changes-blocked">${esc(row.blockedReason)}</small>`:''}`;
30
+ button.onclick=run(()=>review(row.path));item.append(check,button);return item;
31
+ }));
32
+ if(data?.available&&!data.changes.length){const empty=document.createElement('p');empty.className='library-no-results';empty.textContent=$('changes-search').value?'No saved documents match this search.':'No saved document changes. Unsaved drafts stay in the editor.';$('changes-list').append(empty);}
33
+ $('changes-list').scrollTop=scroll;selection();
34
+ if(activePath)[...$('changes-list').querySelectorAll('[data-git-path]')].find(node=>node.tagName===activeTag&&node.getAttribute('data-git-path')===activePath)?.focus();
35
+ }
36
+ async function load({refresh=false}={}) {
37
+ const own=++sequence;$('changes-list').setAttribute('aria-busy','true');$('changes-refresh').disabled=true;$('changes-feedback').textContent='Inspecting saved changes…';
38
+ const old=data,params=new URLSearchParams({q:$('changes-search').value,offset:String(offset),archived:$('changes-archived').checked?'1':'0',refresh:refresh?'1':'0'});
39
+ try{
40
+ const result=await api(`git/status?${params}`);if(own!==sequence||state.mode!=='changes')return;
41
+ data=result;offset=result.offset??0;
42
+ if(!result.available){selected.clear();reviewed=null;diffSequence++;$('changes-review').innerHTML='<h2>Git review unavailable</h2><p class="muted">Your document library and editor remain available.</p>';}
43
+ for(const row of result.changes){if(!row.eligible)selected.delete(row.path);else if(selected.has(row.path))selected.set(row.path,row);}
44
+ if(old&&old.offset===result.offset&&old.query===params.get('q')&&old.archived===params.get('archived'))for(const row of old.changes)if(!result.changes.some(next=>next.path===row.path))selected.delete(row.path);
45
+ data.query=params.get('q');data.archived=params.get('archived');
46
+ $('changes-checkout').textContent=result.available?`${result.branch??'No branch'} · ${result.head?.slice(0,12)??'No commits yet'}`:'';
47
+ $('changes-feedback').textContent=[result.reason??`${result.changedDocuments} changed documents · ${result.localOnly} local-only documents`,result.commitSupport?.reason].filter(Boolean).join(' ');
48
+ $('changes-range').textContent=result.total?`${offset+1}–${offset+result.changes.length} of ${result.total}`:'0 documents';
49
+ $('changes-previous').disabled=!offset||!result.available;$('changes-next').disabled=!result.hasMore||!result.available;
50
+ $('library-changes-count').textContent=result.available?String(result.changedDocuments):'—';$('library-changes').title='Review saved document changes';
51
+ $('changes-staging').textContent=result.unrelatedStaged?`${result.unrelatedStaged} staged files outside this document scope stay staged.`:'Selection and review leave Git staging unchanged.';
52
+ rows();if(reviewed)await review(reviewed);
53
+ }catch(error){if(own!==sequence||state.mode!=='changes')return;if(own===sequence&&state.mode==='changes')$('changes-feedback').textContent=`${error.message} Selection is kept. Try Refresh.`;throw error;}
54
+ finally{if(own===sequence){$('changes-list').setAttribute('aria-busy','false');$('changes-refresh').disabled=false;}}
55
+ }
56
+ async function review(file) {
57
+ const own=++diffSequence;reviewed=file;rows();$('changes-review').setAttribute('aria-busy','true');
58
+ $('changes-review').innerHTML=`<h2>${esc(displayPath(file))}</h2><p class="muted">Loading saved file diff…</p>`;
59
+ try{
60
+ const result=await api(`git/diff?${new URLSearchParams({path:file,archived:$('changes-archived').checked?'1':'0'})}`);if(own!==diffSequence||state.mode!=='changes')return;
61
+ let diff;try{diff=diffHtml(result.before,result.after);}catch{diff='<p>This diff is too large to display. Review the file with Git.</p>';}
62
+ const row=result.change;
63
+ $('changes-review').innerHTML=`<div class="changes-review-heading"><div><h2>${esc(displayPath(row.path))}</h2><p>${esc(result.basis)} · ${esc(result.branch??'Detached HEAD')}${row.original?`<br>Renamed from ${esc(displayPath(row.original))}`:''}</p></div>${row.kind!=='Deleted'?'<button id="changes-open-file" class="quiet small">Open document</button>':''}</div>${row.blockedReason?`<p class="inline-note">${esc(row.blockedReason)}</p>`:''}<div class="diff">${diff}</div>`;
64
+ if($('changes-open-file'))$('changes-open-file').onclick=run(()=>open(row.path));
65
+ }catch(error){if(own!==diffSequence||state.mode!=='changes')return;if(own===diffSequence&&state.mode==='changes')$('changes-review').innerHTML=`<h2>${esc(displayPath(file))}</h2><p class="inline-note">${esc(error.message)}</p>`;throw error;}
66
+ finally{if(own===diffSequence)$('changes-review').setAttribute('aria-busy','false');}
67
+ }
68
+ async function show() {
69
+ await showLibrary();
70
+ if(owner!==checkout()){owner=checkout();selected.clear();reviewed=null;offset=0;data=null;$('changes-search').value='';$('changes-archived').checked=false;}
71
+ activate();await load({refresh:true});$('changes-title').focus();
72
+ }
73
+ $('library-changes').onclick=run(show);$('changes-refresh').onclick=run(()=>load({refresh:true}));
74
+ $('changes-clear').onclick=()=>{selected.clear();rows();};
75
+ $('changes-search').oninput=()=>{clearTimeout(timer);timer=setTimeout(()=>{offset=0;run(load)();},200);};
76
+ $('changes-archived').onchange=run(()=>{offset=0;return load();});
77
+ $('changes-previous').onclick=run(()=>{offset=Math.max(0,offset-100);return load();});$('changes-next').onclick=run(()=>{offset+=100;return load();});
78
+ $('changes-return-draft').onclick=run(returnToDraft);
79
+ return {show,deactivate,invalidate,selectedPaths:()=>[...selected.keys()]};
80
+ }
@@ -0,0 +1,46 @@
1
+ import {escapeHtml as esc,diffHtml} from './shared.mjs';
2
+ // Explicit review/commit/inspection only. Polling never executes or recovers.
3
+ export function gitCommit({$,api,checkout,selection,changed,notice}){
4
+ const messages=new Map(),dialog=$('git-commit-dialog');let operation=null,owner=null,busy=false,timer,sequence=0,visited=new Set();
5
+ const pathLabel=name=>String(name).replace(/[\x00-\x1f\x7f]/g,c=>`\\u${c.charCodeAt(0).toString(16).padStart(4,'0')}`);
6
+ function stop(){clearTimeout(timer);sequence++;}
7
+ function close(){if(owner)messages.set(owner,$('git-commit-message').value);stop();dialog.close();}
8
+ function buttons(){
9
+ $('git-commit-confirm').disabled=busy||!operation?.canCommit||visited.size!==operation.review.length||Date.now()>operation.expiresAt;
10
+ $('git-commit-confirm').hidden=operation?.state!=='reviewed';$('git-commit-inspect').hidden=!operation||operation.running||operation.state==='reviewed'||operation.state==='failed';
11
+ $('git-commit-recover').hidden=!operation?.canRecover;$('git-commit-cancel').hidden=!operation?.running;$('git-commit-settlement').hidden=!operation?.canSettle;$('git-commit-settle').disabled=busy||!$('git-commit-note').value.trim();
12
+ for(const id of ['git-commit-inspect','git-commit-recover','git-commit-cancel','git-commit-preview'])$(id).disabled=busy;
13
+ $('git-commit-preview').disabled=busy||!$('git-commit-message').value.trim()||!selection().length;
14
+ }
15
+ function fileReview(index){
16
+ const row=operation?.review[index];if(!row)return;
17
+ let diff;try{diff=diffHtml(row.before,row.after);}catch{diff=`<div class="git-source-pair"><section><h3>Before</h3><pre>${esc(row.before)}</pre></section><section><h3>After</h3><pre>${esc(row.after)}</pre></section></div>`;}
18
+ $('git-commit-diff').innerHTML=`<h3>${esc(pathLabel(row.path))}</h3>${row.original?`<p>Renamed from ${esc(pathLabel(row.original))}</p>`:''}<div class="diff">${diff}</div>`;
19
+ visited.add(index);$('git-commit-reviewed').textContent=`${visited.size} of ${operation.review.length} files reviewed`;
20
+ for(const node of $('git-commit-files').querySelectorAll('button'))node.setAttribute('aria-pressed',String(Number(node.dataset.index)===index));buttons();
21
+ }
22
+ function render(value){
23
+ const previous=operation?.operationId;operation=value;if(previous!==value.operationId)visited.clear();
24
+ $('git-commit-form').hidden=true;$('git-commit-result').hidden=false;
25
+ $('git-commit-summary').textContent=`${value.branch} · ${value.paths.length} paths · parent ${value.head.slice(0,12)}${value.execution?` · Hooks: ${value.execution.hooks.join(', ')||'none'} · Signing: ${value.execution.signing} · Clean filters: ${(value.execution.filters??[]).join(', ')||'none'}`:''}`;
26
+ $('git-commit-reviewed-message').textContent=value.message;
27
+ const labels={reviewing:'Building the exact Git snapshot…',reviewed:'Review the saved Git content below before committing.',starting:'Starting local commit…',preparing:'Preparing selected files…',executing:'Creating the local commit…','ref-prepared':'Publishing the reviewed commit…','ref-committed':'Commit published; checking the index…',committed:'Local commit complete.','not-committed':'No commit was recorded. Inspect before reviewing a new commit.','committed-index-recovery':'Commit exists. Git staging still needs recovery.','committed-external-ref':'Commit was recorded; the branch has since moved.','uncertain-ref-publication':'Publication is uncertain. This operation will not be repeated.','uncertain-external-ref':'The branch changed. This operation will not be repeated.',failed:'Review unavailable.'};
28
+ $('git-commit-feedback').textContent=[labels[value.state]??value.state,value.commitId?`Commit ${value.commitId}`:'',value.failure?.message??'',value.recovery?`Recovery: ${value.recovery.replaceAll('-',' ')}`:''].filter(Boolean).join(' ');
29
+ $('git-commit-files').replaceChildren(...value.review.map((row,index)=>{const button=document.createElement('button');button.type='button';button.className='quiet small';button.textContent=pathLabel(row.path);button.dataset.index=String(index);button.onclick=()=>fileReview(index);return button;}));
30
+ if(value.state==='reviewed'){if(!visited.size)fileReview(0);}else{$('git-commit-diff').replaceChildren();$('git-commit-reviewed').textContent='';}
31
+ buttons();if(value.state==='committed')changed(value);
32
+ }
33
+ async function inspect(){const own=sequence,current=owner,id=operation.operationId;const value=await api('git/operation?'+new URLSearchParams({id}));if(own!==sequence||current!==checkout()||!dialog.open)return;if(!value)throw new Error('The retained Git operation is unavailable.');render(value);if(value.running)timer=setTimeout(()=>inspect().catch(error=>{$('git-commit-feedback').textContent=error.message;}),500);}
34
+ async function action(route){if(busy||!operation||owner!==checkout())return;busy=true;buttons();const own=sequence;try{const result=await api(route,{operationId:operation.operationId,...(route==='git/operation/settle'?{expectedRef:operation.observedRef,expectedIndexRevision:operation.observedIndex,note:$('git-commit-note').value}:{})});if(own!==sequence||owner!==checkout()||!dialog.open)return;render(result);if(result.running)timer=setTimeout(()=>inspect().catch(error=>{$('git-commit-feedback').textContent=error.message;}),500);}catch(error){if(own===sequence)$('git-commit-feedback').textContent=`${error.message} The receipt was kept. Inspect the outcome before continuing.`;}finally{busy=false;buttons();}}
35
+ async function show({operationId}={}){
36
+ stop();const current=checkout();if(owner!==current){if(owner)messages.set(owner,$('git-commit-message').value);$('git-commit-message').value=messages.get(current)??'';}owner=current;const own=sequence;busy=false;visited.clear();operation=null;$('git-commit-note').value='';dialog.showModal();$('git-commit-diff').replaceChildren();$('git-commit-files').replaceChildren();$('git-commit-feedback').textContent='';
37
+ if(operationId){busy=true;$('git-commit-form').hidden=true;$('git-commit-result').hidden=true;$('git-commit-feedback').textContent='Reading retained Git operation…';buttons();try{const value=await api('git/operation?'+new URLSearchParams({id:operationId}));if(own!==sequence||current!==checkout()||!dialog.open)return;if(!value)throw new Error('This Git operation is unavailable.');render(value);if(value.running)timer=setTimeout(()=>inspect().catch(error=>notice(error.message,true)),500);}finally{if(own===sequence){busy=false;buttons();}}}
38
+ else{$('git-commit-form').hidden=false;$('git-commit-result').hidden=true;$('git-commit-paths').textContent=selection().map(pathLabel).join('\n');buttons();$('git-commit-message').focus();}
39
+ }
40
+ $('git-commit-form').onsubmit=async event=>{event.preventDefault();if(busy||owner!==checkout()||!selection().length)return;busy=true;buttons();const own=sequence;try{const value=await api('git/commit/preview',{operationId:crypto.randomUUID(),paths:selection(),message:$('git-commit-message').value});if(own!==sequence||owner!==checkout()||!dialog.open)return;render(value);if(value.running)timer=setTimeout(()=>inspect().catch(error=>notice(error.message,true)),500);}catch(error){if(own===sequence)$('git-commit-feedback').textContent=error.message;}finally{busy=false;buttons();}};
41
+ $('git-commit-note').oninput=buttons;$('git-commit-settle').onclick=()=>{if(!$('git-commit-settle').disabled)return action('git/operation/settle');};
42
+ $('git-commit-message').oninput=buttons;$('git-commit-close').onclick=close;dialog.oncancel=()=>stop();
43
+ $('git-commit-confirm').onclick=()=>{if(!$('git-commit-confirm').disabled)return action('git/commit/start');};
44
+ $('git-commit-inspect').onclick=()=>action('git/operation/inspect');$('git-commit-recover').onclick=()=>action('git/operation/recover');$('git-commit-cancel').onclick=()=>action('git/operation/cancel');
45
+ return {show,close};
46
+ }
@@ -0,0 +1,70 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="color-scheme" content="light dark"><script src="/theme.mjs"></script><title>Runlist — Document library</title><link rel="stylesheet" href="/app.css"></head>
4
+ <body>
5
+ <header class="app-header"><button id="library-home" class="brand" aria-label="Library home"><span class="brand-mark">/</span>runlist</button><div class="checkout" id="checkout">Document library</div><div class="identity" id="identity"></div><label class="theme-control"><span aria-hidden="true">◐</span><span class="sr-only">Appearance</span><select id="theme-mode" aria-label="Appearance"><option value="system">System</option><option value="light">Light</option><option value="dark">Dark</option></select></label><button id="quick-open" class="quiet" hidden aria-keyshortcuts="Meta+K Control+K">Search <kbd>⌘ / Ctrl K</kbd></button><button id="new-document" class="primary" hidden>New…</button><button id="open-recovery" class="quiet" hidden>Recovery</button><button id="desktop-open" class="quiet" hidden>Open folder…</button><button id="disconnect" class="quiet" hidden>Disconnect</button></header>
6
+ <div id="notice" role="status" aria-live="polite">Connecting to the local checkout…</div>
7
+ <div id="desktop-location" class="desktop-location" hidden></div>
8
+ <section id="connect-panel" class="connect-panel" hidden><h1>Open your checkout</h1><p>Use the access link printed by <code>runlist app</code>, or paste it here.</p><form id="connect-form"><label for="access">Local access link</label><input id="access" type="password" autocomplete="off" required><button class="primary">Connect</button></form></section>
9
+ <section id="desktop-welcome" class="connect-panel desktop-welcome" hidden><span class="welcome-mark" aria-hidden="true">/</span><h1>Your work, in local files.</h1><p>Open a checkout to browse its hubs, plans, documents, decisions and flags.</p><button id="desktop-choose" class="primary">Open folder…</button><p class="muted">Local files · This computer only<br>Save to Markdown. Share through Git when you choose.</p><div id="desktop-recents" aria-label="Recent checkouts"></div></section>
10
+ <div id="workspace" class="workspace library-start" hidden>
11
+ <aside class="sidebar" id="sidebar">
12
+ <div class="sidebar-label">LIBRARY <span id="plan-count"></span></div>
13
+ <input id="library-kind" type="hidden" value="all">
14
+ <nav class="library-kinds" aria-label="Document category">
15
+ <button id="library-all" class="quiet" aria-pressed="true"><span>All documents</span><span id="library-all-count"></span></button>
16
+ <button id="library-hubs" class="quiet" aria-pressed="false"><span>Hubs</span><span id="library-hubs-count"></span></button>
17
+ <button id="library-plans" class="quiet" aria-pressed="false"><span>Plans</span><span id="library-plans-count"></span></button>
18
+ <button id="library-documents" class="quiet" aria-pressed="false"><span>Docs</span><span id="library-documents-count"></span></button>
19
+ </nav>
20
+ <nav class="library-kinds record-kinds" aria-label="Records"><button id="library-flags" class="quiet" aria-pressed="false"><span>Flags</span><span id="library-flags-count"></span></button><button id="library-decisions" class="quiet" aria-pressed="false"><span>Decisions</span><span id="library-decisions-count"></span></button></nav>
21
+ <nav class="library-kinds record-kinds" aria-label="Checkout"><button id="library-changes" class="quiet" aria-pressed="false"><span>Changes</span><span id="library-changes-count"></span></button><button id="library-filing" class="quiet" aria-pressed="false"><span>Filing</span><span id="library-filing-count"></span></button></nav>
22
+ <div class="library-local"><section id="library-saved-section" hidden><details open><summary>PINNED</summary><nav id="library-saved" aria-label="Pinned documents"></nav></details></section><section id="library-recent-section" hidden><details open><summary>RECENT</summary><nav id="library-recent" aria-label="Recent documents"></nav></details></section></div>
23
+ <div class="sidebar-bottom"><button id="open-templates" class="template-launch quiet">Templates <span>View &amp; customize</span></button><button id="open-settings" class="template-launch quiet">Settings <span>Preferences &amp; checkout</span></button><div class="sidebar-footer">Local Markdown<br><span>Save here. Review local commits in Changes.</span></div></div>
24
+ </aside>
25
+ <main id="main" aria-label="Checkout workspace"><div class="document-toolbar"><button id="toggle-plans" class="quiet" aria-expanded="false" aria-controls="sidebar">☰ Navigation</button><button id="back-library" class="quiet">← Library</button><div class="document-name"><div id="doc-title">Choose a document</div><span id="doc-path"></span></div><button id="pin-document" class="quiet document-pin" aria-label="Pin document" hidden>☆</button><span id="doc-status" class="status-badge"></span><button id="lifecycle-open" class="quiet small">Change status</button><div class="document-actions"><button id="undo" class="quiet" disabled>Undo save</button><button id="edit" class="primary" disabled>Review changes</button></div></div>
26
+ <section id="empty" class="library-home" aria-label="Browse documents">
27
+ <div class="library-heading"><div><h1 id="library-title">All documents</h1><p id="library-description">Hubs, plans, and reference material across this checkout.</p></div><button id="library-refresh" class="quiet small" aria-label="Refresh library">↻ Refresh</button></div>
28
+ <div class="library-controls"><label class="sr-only" for="filter">Find a document</label><input id="filter" class="search" placeholder="Search title, path, module…" type="search"><label class="sr-only" for="library-sort">Sort documents</label><select id="library-sort"><option value="updated">Recently updated</option><option value="relevance" hidden>Relevance · top matches</option><option value="title">Title</option><option value="path">File path</option></select><label class="content-search-choice"><input id="library-search-content" type="checkbox"> Search contents</label><label class="content-search-choice"><input id="library-search-semantic" type="checkbox"> Semantic · experimental</label><button id="library-semantic-search" class="quiet small" hidden>Search</button><button id="library-semantic-cancel" class="quiet small" hidden>Cancel</button><button id="save-library-view" class="quiet small">Save view…</button></div>
29
+ <div class="saved-views"><label for="library-view">Saved views</label><select id="library-view"><option value="">Current filters</option></select><button id="delete-library-view" class="quiet small" hidden>Delete view</button></div>
30
+ <details class="library-filters"><summary id="library-filter-label">Filters</summary><div class="filter-fields"><label>Status<select id="library-status"><option value="">All statuses</option></select></label><label id="library-stage-field" hidden>Stage<select id="library-stage"><option value="">All stages</option></select></label><label id="library-group-field" hidden>Group<select id="library-group"><option value="">No grouping</option><option value="stage">Stage</option></select></label><label>Document type<select id="library-type"><option value="">All types</option></select></label><label>Location<select id="library-folder"><option value="">All locations</option></select></label><label class="archive-choice"><input id="library-archived" type="checkbox"> Include archived</label><button id="library-clear" class="quiet small">Clear filters</button></div></details>
31
+ <div class="library-pagination"><span id="library-range" role="status">Loading documents…</span><button id="library-previous" class="quiet small" aria-label="Previous document page" disabled>← Previous</button><button id="library-next" class="quiet small" aria-label="Next document page" disabled>Next →</button></div>
32
+ <div class="library-columns" aria-hidden="true"><span>Document</span><span>Type</span><span>Status</span><span>Updated</span></div>
33
+ <nav id="plans" aria-label="Document library"></nav><p id="library-scan-note" class="muted"></p>
34
+ </section>
35
+ <section id="records-home" class="library-home records-home" hidden aria-label="Browse records"></section>
36
+ <section id="filing-home" class="library-home filing-home" hidden aria-label="Plan filing"></section>
37
+ <section id="changes-home" class="library-home changes-home" hidden aria-labelledby="changes-title">
38
+ <div class="library-heading"><div><h1 id="changes-title" tabindex="-1">Changes</h1><p>Review saved documents against local Git history.</p></div><button id="changes-refresh" class="quiet small">↻ Refresh</button></div>
39
+ <p id="changes-checkout" class="muted"></p><p id="changes-feedback" role="status" aria-live="polite"></p>
40
+ <div id="changes-draft" class="inline-note" hidden><span>Draft—not saved yet. Review saved Markdown here, or return to the document to save your draft.</span><button id="changes-return-draft" class="quiet small">Return to draft</button></div>
41
+ <div class="changes-controls"><label class="sr-only" for="changes-search">Find a changed document</label><input id="changes-search" class="search" type="search" placeholder="Find a changed document…"><label class="archive-choice"><input id="changes-archived" type="checkbox"> Include archived</label></div>
42
+ <div class="library-pagination"><span id="changes-range"></span><button id="changes-previous" class="quiet small" disabled>← Previous</button><button id="changes-next" class="quiet small" disabled>Next →</button></div>
43
+ <div class="changes-layout"><section class="changes-files" aria-label="Saved file selection"><div class="changes-selection"><span id="changes-selected">0 selected</span><button id="changes-clear" class="quiet small" disabled>Clear selection</button><button id="changes-commit" class="primary small" disabled>Review local commit</button></div><div id="changes-list" aria-busy="false"></div></section><section id="changes-review" class="changes-review" aria-label="Saved file diff"><h2>Select a file to review</h2><p class="muted">Select whole saved files. Selection leaves Git staging unchanged.</p></section></div>
44
+ <div class="changes-footer"><p id="changes-staging" class="muted"></p><p class="muted">Commit selected saved files locally. Sharing remains in your Git workflow.</p></div>
45
+ </section>
46
+ <section id="document" hidden aria-labelledby="doc-title">
47
+ <section id="yardstick-panel" class="yardstick-panel" hidden aria-label="Product goal"></section>
48
+ <div class="view-tabs"><div id="document-tabs" class="document-view-tabs" role="tablist" aria-label="Document view"><button role="tab" id="tab-read" aria-controls="reading" tabindex="-1" aria-selected="false">Preview</button><button role="tab" id="tab-edit" aria-controls="editing" tabindex="0" aria-selected="true">Edit</button><button role="tab" id="tab-review" aria-controls="reviewing" tabindex="-1" aria-selected="false">Review changes <span id="change-count"></span></button><button role="tab" id="tab-source" aria-controls="source-view" tabindex="-1" aria-selected="false">Source</button></div><button id="toggle-outline" class="quiet small">Sections</button><span id="draft-status" class="draft-status"></span></div>
49
+ <div id="claim" class="inline-note" hidden></div>
50
+ <div id="pending-note" class="inline-note recovery" hidden><span>An interrupted operation is kept. Retry its reviewed request or inspect the outcome before editing further.</span><button id="inspect-pending" class="small">Inspect operation</button></div>
51
+ <div id="recovery" class="inline-note recovery" hidden><span id="recovery-label"></span><button id="recover" class="small">Recover draft</button><button id="discard-recovery" class="quiet small">Discard draft</button></div>
52
+ <div id="conflict" class="conflict" hidden><h2>The file changed while you were editing</h2><p>Your draft is kept. Compare the changes below, edit your draft to include what you want to keep, then review it against the current file.</p><div class="conflict-columns"><section><h3>Changes in the file</h3><div id="current-diff" class="diff"></div><details><summary>Current source</summary><pre id="current-source"></pre></details></section><section><h3>Your draft changes</h3><div id="draft-diff" class="diff"></div><details><summary>Original base source</summary><pre id="base-source"></pre></details></section></div><button id="review-merge" class="primary">Review merged draft</button><button id="use-current" class="quiet">Keep draft and read current file</button></div>
53
+ <div id="operation-repair" class="inline-note" hidden><p id="repair-label"></p><details><summary>Retained operation evidence</summary><pre id="repair-evidence"></pre></details><button id="settle-operation" class="small">Leave current file and settle operation</button></div>
54
+ <article id="reading" role="tabpanel" aria-labelledby="tab-read" tabindex="0" class="markdown"></article>
55
+ <section id="editing" role="tabpanel" aria-labelledby="tab-edit" tabindex="0" class="editing" hidden><div id="document-stage-fields" class="document-stage-fields" hidden><label for="document-stage">Stage</label><select id="document-stage"><option value="">Unset · no stage</option></select><p id="document-stage-meaning" class="muted"></p></div><div class="inline-editor-hint"><span>Click anywhere to write. Type <kbd>/</kbd> for blocks.</span><button id="toggle-markdown" class="quiet small" aria-expanded="false">Edit Markdown</button></div><div id="block-editor" class="markdown"></div><div id="markdown-fallback" hidden><div class="editor-caption"><span>Markdown body</span><span>Metadata and lifecycle history are preserved.</span></div><label class="sr-only" for="body-editor">Markdown body</label><textarea id="body-editor" spellcheck="false"></textarea></div><div class="editor-footer"><button id="review" class="primary">Review changes</button><button id="cancel" class="quiet">Preview</button><button id="discard" class="quiet">Discard changes</button></div></section>
56
+ <section id="reviewing" role="tabpanel" aria-labelledby="tab-review" tabindex="0" class="reviewing" hidden><div class="review-heading"><h2>Source changes</h2><p id="review-summary"></p></div><div id="diff" class="diff"></div><div class="save-bar"><div><strong>Save to this checkout</strong><p>Writes this Markdown file. Git commit and sharing are separate.</p></div><button id="back-edit" class="quiet">Back to edit</button><button id="document-changes" class="quiet">Saved Git changes</button><button id="save" class="primary" disabled>Save changes</button></div></section>
57
+ <section id="source-view" role="tabpanel" aria-labelledby="tab-source" tabindex="0" class="source-view" hidden><div class="editor-caption"><span>Canonical source · read only</span><button id="copy-source" class="quiet small">Copy source</button></div><pre id="full-source"></pre></section>
58
+ </section></main>
59
+ <aside id="evidence" class="evidence"><section class="section-navigation" aria-label="Document sections"><div class="sidebar-label">ON THIS PAGE <span id="section-count"></span><button id="close-outline" class="quiet small" aria-label="Close sections">×</button></div><label class="sr-only" for="section-filter">Find a section</label><input id="section-filter" class="search" type="search" placeholder="Jump to a section…"><nav id="outline" aria-label="Section outline"></nav><button id="add-section" class="quiet small">+ Add section</button><div class="sidebar-label bookmark-heading">BOOKMARKS</div><nav id="bookmarks" aria-label="Bookmarked sections"></nav><p id="bookmark-empty" class="muted">Star a heading to keep it here.<br>Bookmarks stay in this browser.</p><p class="navigation-keys">⌘ / Ctrl + Shift + J to jump<br>Alt + Page Up / Down for sections</p></section><details class="file-evidence" open><summary>FILE &amp; EVIDENCE</summary><section id="document-connections" class="document-connections" hidden></section><div id="file-info" class="file-info"><p class="muted">Select a document to inspect its file and related flags.</p></div><div id="flags"></div></details></aside>
60
+ </div>
61
+ <dialog aria-labelledby="template-dialog-title" id="template-dialog" class="template-dialog"><div class="template-header"><div><h2 id="template-dialog-title">Templates</h2><p>Scaffolds for new documents in this checkout.</p></div><button id="close-templates" class="quiet" aria-label="Close templates">×</button></div><div class="template-layout"><nav id="template-list" aria-label="Templates"></nav><section class="template-detail"><div class="template-heading"><h3 id="template-name"></h3><span id="template-origin" class="muted"></span></div><p id="template-description"></p><p id="template-config-note" class="inline-note" hidden>This repo defines a JavaScript template. Its source is shown below. Saving Markdown creates an override for this type.</p><details id="template-config" hidden><summary>Repository JavaScript source</summary><pre id="template-config-source"></pre></details><div class="template-tabs"><button id="template-edit-tab" class="quiet" aria-pressed="true">Edit Markdown</button><button id="template-preview-tab" class="quiet" aria-pressed="false">Preview example</button></div><label class="sr-only" for="template-source">Template Markdown</label><textarea id="template-source" spellcheck="false"></textarea><article id="template-preview" class="markdown" hidden></article><p id="template-tokens" class="muted">Placeholders: <code>{{title}}</code>, <code>{{status}}</code>, <code>{{date}}</code>, <code>{{body}}</code>, <code>{{version}}</code>. Keep the metadata fields and body placeholder.</p><button id="template-reload" class="quiet small" hidden>Reload saved templates, keep draft</button><div id="template-feedback" role="status" aria-live="polite"></div><details id="template-review" hidden><summary>Changes to the saved template</summary><div id="template-diff" class="diff"></div></details><div class="template-actions"><button id="template-review-button" class="primary">Review template changes</button><button id="template-save" class="primary" hidden>Save template</button><button id="template-reset" class="quiet">Use original template</button><button id="template-discard" class="quiet">Discard draft</button></div><p class="muted">Save writes <code>runlist.templates.json</code> for future <code>runlist new</code> commands. Existing documents keep their content.</p></section></div></dialog>
62
+ <dialog aria-labelledby="settings-dialog-title" id="settings-dialog" class="settings-dialog"><div class="template-header"><div><h2 id="settings-dialog-title">Settings</h2><p id="settings-checkout"></p></div><button id="settings-close" class="quiet" aria-label="Close settings">×</button></div><div id="settings-content"></div></dialog>
63
+ <dialog aria-labelledby="lifecycle-dialog-title" id="lifecycle-dialog" class="lifecycle-dialog"><div class="template-header"><div><h2 id="lifecycle-dialog-title">Change document status</h2><p id="lifecycle-path"></p></div><button id="lifecycle-close" class="quiet" aria-label="Close status change">×</button></div><div id="lifecycle-content"></div></dialog>
64
+ <dialog id="create-dialog" class="create-dialog" aria-labelledby="create-title"><div class="template-header"><div><h2 id="create-title">New document</h2><p>Create a Markdown file in this checkout.</p></div><button id="create-close" class="quiet" aria-label="Close new document">×</button></div><form id="create-form"><div class="create-fields"><label>Template<select id="create-template"></select></label><label>Initial status<select id="create-status"></select></label><label class="field-wide"><span id="create-name-label">Title</span><input id="create-name" required maxlength="200" placeholder="What is this document about?"></label><label>Folder<input id="create-folder" required list="create-folders"></label><datalist id="create-folders"></datalist><label>Filename<input id="create-filename" required pattern="[a-z0-9][a-z0-9_-]{0,119}\.md" placeholder="document-name.md"></label><label class="field-wide">Initial content <span class="muted">optional</span><textarea id="create-body" rows="5" maxlength="262144" placeholder="Start writing, or leave the template scaffold to fill in later."></textarea></label><fieldset id="create-flag-fields" class="field-wide" hidden><legend>Finding severity</legend><label>Severity<select id="create-severity"><option value="warn">Warning</option><option value="problem">Problem</option><option value="info">Information</option></select></label><p class="muted">Starts open and unreviewed. Triage and resolution are separate actions.</p></fieldset><fieldset id="create-decision-fields" class="field-wide" hidden><legend>Alternatives</legend><p class="muted">Describe at least two approaches. Add consequences where they help the decision.</p><div id="create-options"></div><button id="create-option-add" type="button" class="quiet small">+ Add alternative</button><p class="muted">Starts open. Creating this question does not select an answer.</p></fieldset></div><p id="create-origin" class="muted"></p><button id="create-preview-button" class="primary">Preview document</button></form><section id="create-review" hidden><div class="review-heading"><h3 id="create-path"></h3><p>Confirm the destination and content before creating the file.</p></div><article id="create-preview" class="markdown"></article><details><summary>Markdown source</summary><pre id="create-source"></pre></details><section id="create-setup" hidden><h3>Repository record settings</h3><p>This first record also creates <code>runlist.records.json</code> to keep record identities and their folder consistent across checkouts. Review both files before creating.</p><pre id="create-setup-source"></pre></section><div class="dialog-actions"><button id="create-back" class="quiet">Back to form</button><button id="create-inspect" class="quiet">Inspect outcome</button><button id="create-confirm" class="primary">Create document</button></div></section><p id="create-feedback" role="status" aria-live="polite"></p><ul id="create-diagnostics"></ul><button id="create-discard" class="quiet small">Discard this new document draft</button><p class="muted">Creates local Markdown. Git commit and sharing remain separate.</p></dialog>
65
+ <dialog id="quick-dialog" class="quick-dialog" aria-labelledby="quick-title"><div class="template-header"><h2 id="quick-title">Go to…</h2><button id="quick-close" class="quiet" aria-label="Close search">×</button></div><label class="sr-only" for="quick-query">Find documents, sections or actions</label><input id="quick-query" class="search" type="search" autocomplete="off" placeholder="Find documents, sections or actions…" role="combobox" aria-autocomplete="list" aria-expanded="true" aria-controls="quick-results"><div class="quick-controls"><label>Search mode <select id="quick-mode"><option value="text">Text</option><option value="semantic">Semantic · experimental</option></select></label><button id="quick-search" class="primary" hidden>Search</button><button id="quick-cancel" class="quiet" hidden>Cancel</button><button id="quick-tool-settings" class="quiet" hidden>Tool settings</button></div><label class="archive-choice"><input id="quick-archived" type="checkbox"> Include archived</label><p id="quick-status" role="status" aria-live="polite"></p><div id="quick-results" role="listbox" aria-label="Search results"></div><p id="quick-hint" class="muted">↑ ↓ to choose · Enter to open · Esc to close. Searches local document contents on demand.</p></dialog>
66
+ <dialog id="recovery-dialog" class="recovery-dialog" aria-labelledby="recovery-title"><div class="template-header"><div><h2 id="recovery-title">Recovery</h2><p>Drafts and unfinished operations in this checkout.</p></div><button id="recovery-close" class="quiet" aria-label="Close recovery">×</button></div><div class="recovery-controls"><label class="sr-only" for="recovery-filter">Filter recovery items</label><input id="recovery-filter" class="search" type="search" placeholder="Filter by document or state…"><button id="recovery-refresh" class="quiet">Refresh</button></div><p class="inline-note">Draft preserved locally means the Markdown file has not been saved. Interrupted operations require inspection; opening this view never retries a write.</p><p id="recovery-status" role="status" aria-live="polite"></p><div id="recovery-items"></div><button id="recovery-more" class="quiet" hidden>Load more</button></dialog>
67
+ <dialog id="filter-dialog" class="filter-dialog" aria-labelledby="filter-title"><div class="template-header"><h2 id="filter-title">Save library view</h2><button id="filter-close" class="quiet" aria-label="Close saved view">×</button></div><form id="filter-form"><label>View name<input id="filter-name" required maxlength="60" placeholder="For example: Active release plans"></label><p class="muted">Remembers the current search, category, filters, sort, grouping and archived choice on this computer.</p><button class="primary">Save view</button><p id="filter-feedback" role="status"></p></form></dialog>
68
+ <dialog id="git-commit-dialog" class="git-commit-dialog" aria-labelledby="git-commit-title"><div class="template-header"><h2 id="git-commit-title">Local commit</h2><button id="git-commit-close" class="quiet" aria-label="Close local commit">×</button></div><p class="inline-note">Commits whole saved files. Unsaved drafts stay in the editor. Nothing is pushed.</p><form id="git-commit-form"><h3>Selected saved documents</h3><pre id="git-commit-paths"></pre><label>Commit message<textarea id="git-commit-message" required maxlength="8192" rows="3" placeholder="Describe the document changes"></textarea></label><p class="muted">Review applies configured clean filters. Filtered candidates may have no Git changes.</p><button id="git-commit-preview" class="primary" disabled>Review exact Git content</button></form><section id="git-commit-result" hidden><p id="git-commit-summary" class="muted"></p><h3>Reviewed commit message</h3><pre id="git-commit-reviewed-message"></pre><nav id="git-commit-files" aria-label="Selected commit files"></nav><p id="git-commit-reviewed" class="muted"></p><section id="git-commit-diff" aria-label="Reviewed Git content"></section><div class="dialog-actions"><button id="git-commit-inspect" class="quiet" hidden>Inspect outcome</button><button id="git-commit-recover" class="quiet" hidden>Recover owned staging</button><button id="git-commit-cancel" class="quiet" hidden>Stop job and inspect</button><button id="git-commit-confirm" class="primary" hidden disabled>Commit locally</button></div></section><section id="git-commit-settlement" hidden><p>Acknowledge only after inspecting the branch and staging with Git. This keeps the evidence and permits a fresh review.</p><label>What you inspected<textarea id="git-commit-note" maxlength="4000" rows="2"></textarea></label><button id="git-commit-settle" class="quiet" disabled>Acknowledge without retrying</button></section><p id="git-commit-feedback" role="status" aria-live="polite"></p><p class="muted">Closing keeps the operation receipt in Recovery. An interrupted commit is inspected before any new attempt.</p></dialog>
69
+ <script type="module" src="/app.mjs"></script>
70
+ </body></html>
@@ -0,0 +1,93 @@
1
+ import {semanticSearch} from './semantic-search.mjs';
2
+ export function libraryNavigation({state,$,api,open,showLibrary,checkout,notice}) {
3
+ let result,offset=0,sequence=0,timer,recent=[],favorites=[],savedViews=[],semantic=null,textSort='updated';
4
+ const viewKey=()=>`runlist:views:${checkout()}`;
5
+ const filterIds=['library-kind','filter','library-status','library-type','library-folder','library-sort','library-stage','library-group'];
6
+ function viewOptions(){const selected=$('library-view').value;$('library-view').replaceChildren(new Option('Current filters',''),new Option('Active plans','active-plans'),new Option('Hubs','hubs'),...savedViews.map(v=>new Option(v.name,v.id)));$('library-view').value=selected;$('delete-library-view').hidden=!savedViews.some(v=>v.id===selected);}
7
+ function snapshot(){return {...Object.fromEntries(filterIds.map(id=>[id,$(id).value])),archived:$('library-archived').checked,content:$('library-search-content').checked,semantic:$('library-search-semantic').checked};}
8
+ async function applyView(id){
9
+ await showLibrary();const filters=id==='active-plans'?{'library-kind':'plans','library-status':'active'}:id==='hubs'?{'library-kind':'hubs'}:savedViews.find(v=>v.id===id)?.filters;
10
+ if(!filters)return;
11
+ // Saved stage options must survive applying a view before fresh facets arrive.
12
+ if(filters['library-stage']&&![...$('library-stage').options].some(o=>o.value===filters['library-stage']))$('library-stage').add(new Option(filters['library-stage'].replace(/^word:/,''),filters['library-stage']));
13
+ for(const key of filterIds)$(key).value=filters[key]??(key==='library-kind'?'all':key==='library-sort'?'updated':'');
14
+ $('library-archived').checked=filters.archived===true;$('library-search-content').checked=filters.content===true;$('library-search-semantic').checked=filters.semantic===true;semanticControls();
15
+ $('library-view').value=id;viewOptions();await load({reset:true});
16
+ }
17
+ const storageKey=()=>`runlist:library:${checkout()}`;
18
+ function keep(){try{localStorage.setItem(storageKey(),JSON.stringify({recent,favorites}));}catch{notice('Recent documents and pins could not be saved in local editor storage.',true);}}
19
+ function button(doc,compact=false){
20
+ const button=document.createElement('button'),active=!['home','records'].includes(state.mode)&&state.doc?.path===doc.path;
21
+ button.className=`plan${active?' active':''}`;button.title=doc.path;
22
+ if(active)button.setAttribute('aria-current','page');
23
+ const name=document.createElement('span');name.className='plan-name';name.textContent=doc.title;
24
+ const location=document.createElement('span');location.className='plan-location';location.textContent=doc.path;
25
+ if(doc.excerpt){const excerpt=document.createElement('small');excerpt.className='search-excerpt';excerpt.textContent=doc.excerpt;name.append(excerpt);}
26
+ if(compact){button.append(name,location);}
27
+ else {
28
+ const label=document.createElement('span');label.className='plan-label';label.append(name,location);
29
+ const type=document.createElement('span');type.className='plan-type';type.textContent=doc.kind==='hub'?'Hub':doc.type??'Document';
30
+ const status=document.createElement('span');status.className='plan-meta';
31
+ const dot=document.createElement('span');dot.className=`dot ${/^[a-z-]+$/.test(doc.status)?doc.status:''}`;
32
+ status.append(dot,document.createTextNode(doc.status??'unknown'));
33
+ const updated=document.createElement('span');updated.className='plan-updated';const date=new Date(/^\d{4}-\d{2}-\d{2}$/.test(doc.updated)?`${doc.updated}T12:00:00`:doc.updated??NaN);
34
+ updated.textContent=Number.isNaN(date.getTime())?'—':date.toLocaleDateString(undefined,{month:'short',day:'numeric',year:'numeric'});
35
+ if(doc.kind==='plan'){const stage=document.createElement('small');stage.className='plan-stage';stage.textContent=`Stage · ${doc.stageInvalid?'Invalid metadata':doc.stage??'Unset · no stage'}`;label.append(stage);}
36
+ button.append(label,type,status,updated);
37
+ }
38
+ button.onclick=async()=>{try{await open(doc.path);}catch(error){notice(error.message,true);}};return button;
39
+ }
40
+ function render(){
41
+ if(!result)return;
42
+ const entries=[];let previous;for(const doc of state.plans){const key=doc.stageInvalid?'@invalid':doc.stage===null?'@unset':`word:${doc.stage}`;if(result.group==='stage'&&key!==previous){const section=document.createElement('div');section.className='library-stage-heading';const heading=document.createElement('h2'),definition=result.stageDefinitions?.find(s=>s.word===doc.stage);heading.textContent=doc.stageInvalid?'Invalid metadata':doc.stage??'Unset · no stage';section.append(heading);if(definition?.meaning){const meaning=document.createElement('p');meaning.textContent=definition.meaning;section.append(meaning);}entries.push(section);previous=key;}entries.push(button(doc));}
43
+ $('plans').replaceChildren(...entries);if(!state.plans.length){const empty=document.createElement('p');empty.className='library-no-results';empty.textContent='No documents match these filters. Try another search or clear the filters.';$('plans').append(empty);}
44
+ $('plan-count').textContent=result.counts.all.toLocaleString();$('library-range').textContent=result.semantic?`${result.total} top semantic matches · Relevance order`:result.pendingSemantic?'Press Search to find semantic matches':result.total?`${result.offset+1}–${Math.min(result.total,result.offset+result.documents.length)} of ${result.total.toLocaleString()}`:'0 results';
45
+ $('library-previous').disabled=result.offset===0;$('library-next').disabled=!result.hasMore;
46
+ for(const kind of ['all','hubs','plans','documents']){$('library-'+kind+'-count').textContent=result.counts[kind].toLocaleString();$('library-'+kind).setAttribute('aria-pressed',String(state.mode==='home'&&($('library-kind').value||'all')===kind));}
47
+ const titles={all:'All documents',hubs:'Hubs',plans:'Plans',documents:'Docs'},descriptions={all:'Hubs, plans, and reference material across this checkout.',hubs:'Programs, roadmaps, and runlists that connect the work.',plans:'Work across this checkout, from planned to complete.',documents:'Guides, research, and other reference material.'};
48
+ const kind=$('library-kind').value||'all';$('library-title').textContent=titles[kind];$('library-description').textContent=descriptions[kind];
49
+ if(state.mode==='home')$('doc-title').textContent=titles[kind];
50
+ const filters=['library-status','library-type','library-folder',...(kind==='plans'?['library-stage']:[])].filter(id=>$(id).value).length+Number($('library-archived').checked);
51
+ $('library-filter-label').textContent=filters?`Filters · ${filters}`:'Filters';
52
+ $('library-saved').replaceChildren(...favorites.map(doc=>{const row=document.createElement('div');row.className='saved-document';const remove=document.createElement('button');remove.className='quiet small';remove.textContent='×';remove.setAttribute('aria-label',`Unpin ${doc.title}`);remove.onclick=()=>{favorites=favorites.filter(f=>f.path!==doc.path);keep();render();};row.append(button(doc,true),remove);return row;}));$('library-recent').replaceChildren(...recent.filter(r=>!favorites.some(f=>f.path===r.path)).slice(0,6).map(doc=>button(doc,true)));$('library-saved-section').hidden=!favorites.length;$('library-recent-section').hidden=!recent.length;
53
+ $('pin-document').hidden=!state.doc;const pinned=!!state.doc&&favorites.some(r=>r.path===state.doc.path);$('pin-document').textContent=pinned?'★':'☆';$('pin-document').setAttribute('aria-label',pinned?'Unpin document':'Pin document');$('pin-document').setAttribute('aria-pressed',String(pinned));
54
+ $('library-scan-note').textContent=[result.semantic?`Experimental semantic search. ${result.message}${result.coverage?.partial?' Index coverage is incomplete.':''}${result.indexUpdating?' Index is updating.':''}${result.stale?' Changed sources open at the document.':''}`:'',result.partial?`Content search is partial: scanned ${result.scanned} of ${result.inventoryTotal} documents. Narrow the folder or filters to search the rest.`:'',result.unavailable?`${result.unavailable} files unavailable to content search.`:'',result.stats.unavailable||result.stats.discoveryErrors?`${result.stats.unavailable} unavailable files · ${result.stats.discoveryErrors} unreadable folders. Refresh to retry.`:''].filter(Boolean).join(' ');
55
+ }
56
+ function options(id,values,label){const select=$(id),value=select.value;select.replaceChildren(new Option(label,''),...values.map(v=>new Option(`${v.label??v.value}${v.unknown?' · unknown':''} (${v.count})`,v.value)));if(value&&!values.some(v=>v.value===value))select.add(new Option(`${value} (0)`,value));select.value=value;}
57
+ async function load({refresh=false,reset=false,semanticRun=false}={}){
58
+ if(reset)offset=0;semantic?.abort();semantic=null;const own=++sequence;stageControls();semanticControls();
59
+ const params=new URLSearchParams({q:$('filter').value,kind:$('library-kind').value,status:$('library-status').value,type:$('library-type').value,folder:$('library-folder').value,sort:$('library-sort').value,stage:$('library-kind').value==='plans'?$('library-stage').value:'',group:$('library-kind').value==='plans'?$('library-group').value:'',archived:$('library-archived').checked?'1':'0',content:$('library-search-content').checked?'1':'0',offset:String(offset),limit:'50'});if(refresh)params.set('refresh','1');const semanticMode=$('library-search-semantic').checked,query=$('filter').value.trim();if(semanticMode){params.delete('q');params.set('content','0');params.set('offset','0');}
60
+ $('plans').setAttribute('aria-busy','true');$('library-refresh').disabled=true;$('library-range').textContent='Loading documents…';
61
+ try{let response=await api(`library?${params}`);if(own!==sequence)return;
62
+ if(semanticMode&&query){
63
+ if(semanticRun){const controller=new AbortController();semantic=controller;$('library-semantic-cancel').hidden=false;const found=await semanticSearch(api,{query,kind:$('library-kind').value,status:$('library-status').value,type:$('library-type').value,folder:$('library-folder').value,archived:$('library-archived').checked,stage:$('library-kind').value==='plans'?$('library-stage').value:''},controller.signal);if(own!==sequence||controller.signal.aborted)return;response={...response,...found,group:null,documents:found.documents??[],total:found.documents?.length??0,offset:0,hasMore:false,semantic:true};}
64
+ else response={...response,documents:[],total:0,offset:0,hasMore:false,pendingSemantic:true};
65
+ }
66
+ result=response;state.plans=result.documents;offset=result.offset;options('library-stage',result.facets.stages??[],'All stages');options('library-status',result.facets.statuses,'All statuses');options('library-type',result.facets.types,'All types');options('library-folder',result.facets.folders,'All locations');render();$('plans').scrollTop=0;return result;}
67
+ catch(error){if(own===sequence){$('library-range').textContent='Library unavailable · try Refresh';notice(error.message,true);}throw error;}
68
+ finally{if(own===sequence){$('plans').setAttribute('aria-busy','false');$('library-refresh').disabled=false;semantic=null;$('library-semantic-cancel').hidden=true;}}
69
+ }
70
+ function stageControls(){const plans=$('library-kind').value==='plans';$('library-stage-field').hidden=!plans;$('library-group-field').hidden=!plans;}
71
+ function semanticControls(){const enabled=$('library-search-semantic').checked;$('library-semantic-search').hidden=!enabled;$('library-sort').disabled=enabled;$('library-group').disabled=enabled;if(enabled){if($('library-sort').value!=='relevance')textSort=$('library-sort').value;$('library-sort').value='relevance';}else if($('library-sort').value==='relevance')$('library-sort').value=textSort;}
72
+ $('library-semantic-search').onclick=()=>run({reset:true,semanticRun:true});
73
+ $('library-semantic-cancel').onclick=()=>{changed();run({reset:true});};
74
+ const run=options=>load(options).catch(()=>{});
75
+ function changed(){semantic?.abort();semantic=null;sequence++;$('library-semantic-cancel').hidden=true;$('plans').setAttribute('aria-busy','false');$('library-refresh').disabled=false; $('library-view').value='';viewOptions(); }
76
+ $('filter').oninput=()=>{changed();clearTimeout(timer);if($('library-search-semantic').checked){if(result){result={...result,documents:[],total:0,offset:0,hasMore:false,semantic:false,pendingSemantic:true};state.plans=[];render();}}else timer=setTimeout(()=>run({reset:true}),250);};
77
+ for(const id of ['library-status','library-type','library-folder','library-sort','library-stage','library-group','library-archived','library-search-content','library-search-semantic'])$(id).onchange=()=>{if(id==='library-search-semantic'&&$('library-search-semantic').checked)$('library-search-content').checked=false;if(id==='library-search-content'&&$('library-search-content').checked)$('library-search-semantic').checked=false;changed();semanticControls();run({reset:true});};
78
+ for(const kind of ['all','hubs','plans','documents'])$('library-'+kind).onclick=async()=>{try{await showLibrary();$('library-kind').value=kind;changed();await load({reset:true});}catch(error){notice(error.message,true);}};
79
+ $('library-previous').onclick=()=>{offset=Math.max(0,offset-50);run({});};$('library-next').onclick=()=>{offset+=50;run({});};$('library-refresh').onclick=()=>run({refresh:true});
80
+ $('library-clear').onclick=()=>{$('filter').value='';for(const id of ['library-status','library-type','library-folder','library-stage','library-group'])$(id).value='';$('library-archived').checked=false;$('library-search-content').checked=false;$('library-search-semantic').checked=false;semanticControls();changed();run({reset:true});};
81
+ $('library-view').onchange=()=>applyView($('library-view').value).catch(error=>notice(error.message,true));
82
+ $('save-library-view').onclick=()=>{$('filter-feedback').textContent='';$('filter-name').value='';$('filter-dialog').showModal();$('filter-name').focus();};
83
+ $('filter-close').onclick=()=>$('filter-dialog').close();
84
+ $('filter-form').onsubmit=event=>{event.preventDefault();try{const name=$('filter-name').value.trim();if(!name||name.length>60)throw new Error('Give this view a name, up to 60 characters.');if(savedViews.length>=30)throw new Error('Remove an old saved view before adding another.');const value={id:crypto.randomUUID(),name,filters:snapshot()},next=[...savedViews,value];localStorage.setItem(viewKey(),JSON.stringify(next));savedViews=next;viewOptions();$('library-view').value=value.id;viewOptions();$('filter-dialog').close();notice('Library view saved on this computer.');}catch(error){$('filter-feedback').textContent=error.message;}};
85
+ $('delete-library-view').onclick=()=>{try{const next=savedViews.filter(v=>v.id!==$('library-view').value);localStorage.setItem(viewKey(),JSON.stringify(next));savedViews=next;changed();notice('Saved view deleted. Current filters are kept.');}catch(error){notice(error.message,true);}};
86
+ $('pin-document').onclick=()=>{const doc=state.doc,index=favorites.findIndex(r=>r.path===doc.path);if(index>=0)favorites.splice(index,1);else favorites.unshift(summary(doc));favorites=favorites.slice(0,30);keep();render();};
87
+ function summary(doc){return {path:doc.path,title:doc.title,status:doc.status,type:doc.type,kind:doc.kind};}
88
+ function opened(){const doc=summary(state.doc);recent=[doc,...recent.filter(r=>r.path!==doc.path)].slice(0,12);favorites=favorites.map(r=>r.path===doc.path?doc:r);keep();render();}
89
+ function restore(){try{const saved=JSON.parse(localStorage.getItem(storageKey())??'{}'),valid=r=>r&&typeof r.path==='string'&&typeof r.title==='string';recent=Array.isArray(saved.recent)?saved.recent.filter(valid).slice(0,12):[];favorites=Array.isArray(saved.favorites)?saved.favorites.filter(valid).slice(0,30):[];const views=JSON.parse(localStorage.getItem(viewKey())??'[]');savedViews=Array.isArray(views)?views.filter(v=>v&&typeof v.id==='string'&&typeof v.name==='string'&&v.filters).slice(0,30):[];}catch{}viewOptions();render();}
90
+ async function preferences(prefs,reload=true){$('library-sort').value=prefs.sort;$('library-archived').checked=prefs.archived;if(reload&&state.mode==='home')await load({reset:true});}
91
+ function relocated(oldPath,newPath){if(oldPath===newPath)return;favorites=favorites.map(doc=>doc.path===oldPath?summary({...doc,...state.doc,path:newPath}):doc);recent=recent.filter(doc=>doc.path!==oldPath);keep();render();const oldKey=`runlist:bookmarks:${checkout()}:${encodeURIComponent(oldPath)}`,newKey=`runlist:bookmarks:${checkout()}:${encodeURIComponent(newPath)}`;try{const bookmarks=localStorage.getItem(oldKey);if(bookmarks&&!localStorage.getItem(newKey))localStorage.setItem(newKey,bookmarks);}catch{notice('Moved document bookmarks could not be copied in local editor storage.',true);}}
92
+ return {load,render,opened,restore,preferences,relocated,applyView,savedViews:()=>savedViews,quickDocuments:()=>[...favorites,...recent.filter(doc=>!favorites.some(f=>f.path===doc.path))].slice(0,12)};
93
+ }
@@ -0,0 +1,18 @@
1
+ import { parseBlocks } from './block-model.mjs';
2
+
3
+ // Source offsets refer to the normalized body, not frontmatter. Fenced code,
4
+ // comments and raw fallback blocks cannot accidentally create outline entries.
5
+ export function documentOutline(body, blocks = parseBlocks(body)) {
6
+ const counts = new Map(), parents = []; let offset = 0, line = 1;
7
+ return blocks.flatMap(block => {
8
+ const start = offset, startLine = line; offset += block.raw.length; line += (block.raw.match(/\n/g) ?? []).length;
9
+ let level = /^h([1-6])$/.exec(block.kind)?.[1], title = block.text;
10
+ if (block.kind === 'history') { level=2; title='Version History'; }
11
+ if (!level) return [];
12
+ level=Number(level); parents.length=level-1;
13
+ const signature = JSON.stringify([parents.filter(Boolean),level,title]);
14
+ const occurrence = counts.get(signature) ?? 0; counts.set(signature,occurrence+1);
15
+ const key = JSON.stringify([signature,occurrence]); parents[level-1]=title;
16
+ return [{key,id:block.id,title,level,offset:start,line:startLine,locked:!!block.locked}];
17
+ });
18
+ }