create-pmos 0.52.0 → 0.53.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.
Files changed (212) hide show
  1. package/package.json +1 -1
  2. package/payload/KIT-VERSION +2 -2
  3. package/payload/mods/pmos-cockpit/README.md +13 -0
  4. package/payload/mods/pmos-cockpit/hooks/lib/commands.mjs +2 -1
  5. package/payload/mods/pmos-cockpit/hooks/lib/grants.mjs +41 -11
  6. package/payload/mods/pmos-cockpit/hooks/lib/guard.mjs +228 -8
  7. package/payload/mods/pmos-cockpit/hooks/register.tsx +11 -3
  8. package/payload/planning/evals/eval-calibration.md +1 -1
  9. package/payload/planning/okrs/health-budgets.md +1 -1
  10. package/payload/scripts/plan-items-stage.js +271 -0
  11. package/payload/scripts/pmos-log.sh +18 -7
  12. package/payload/scripts/workflow-state.js +28 -2
  13. package/payload/state/discoveries.md +1 -1
  14. package/payload/state/log.md +1 -1
  15. package/workspace/bin/pmos.js +15 -1
  16. package/workspace/cut-stub.js +40 -0
  17. package/workspace/first-paint-audit.js +145 -0
  18. package/workspace/fixtures/capture/requirements/capture-2026-10-04-1.md +10 -0
  19. package/workspace/fixtures/initiatives-api/list.json +176 -0
  20. package/workspace/fixtures/initiatives-api/post-403.json +5 -0
  21. package/workspace/fixtures/initiatives-api/post-ok.json +10 -0
  22. package/workspace/fixtures/initiatives-api/roadmap.json +208 -0
  23. package/workspace/fixtures/initiatives-api/sheet.json +112 -0
  24. package/workspace/fixtures/report-api/draft.json +177 -0
  25. package/workspace/fixtures/reports-page/draft-biweekly-2026-10-16.md +97 -0
  26. package/workspace/fixtures/today-api/provider-rows.json +204 -0
  27. package/workspace/fixtures/today-api/seam-probe-kinds.json +5 -0
  28. package/workspace/guide/board.md +81 -0
  29. package/workspace/guide/breakdown.md +154 -0
  30. package/workspace/guide/command-palette.md +75 -0
  31. package/workspace/guide/index.md +27 -17
  32. package/workspace/guide/initiatives-roadmap.md +130 -0
  33. package/workspace/guide/initiatives-screen.md +89 -0
  34. package/workspace/guide/initiatives-timeline.md +57 -0
  35. package/workspace/guide/integrated-work.md +94 -0
  36. package/workspace/guide/knowledge-graph.md +3 -68
  37. package/workspace/guide/layout.md +98 -32
  38. package/workspace/guide/live-notes.md +3 -3
  39. package/workspace/guide/pm-only-acts.md +89 -0
  40. package/workspace/guide/pm-only-lines.md +61 -0
  41. package/workspace/guide/reports-page.md +80 -0
  42. package/workspace/guide/reports.md +73 -0
  43. package/workspace/guide/timeline.md +70 -0
  44. package/workspace/guide/today.md +85 -0
  45. package/workspace/guide/validate-with-typesafe.md +1 -1
  46. package/workspace/lib/actor.js +91 -0
  47. package/workspace/lib/board-cache.js +32 -2
  48. package/workspace/lib/board-cli.js +108 -0
  49. package/workspace/lib/board-fold.js +232 -0
  50. package/workspace/lib/breakdown-export.js +80 -0
  51. package/workspace/lib/capture.js +98 -0
  52. package/workspace/lib/engine/plan-items-stage.js +271 -0
  53. package/workspace/lib/engine/workflow-state.js +28 -2
  54. package/workspace/lib/factory-join.js +158 -0
  55. package/workspace/lib/grant-mint.js +113 -0
  56. package/workspace/lib/health-rules-factory.js +92 -0
  57. package/workspace/lib/health-rules-plan.js +164 -0
  58. package/workspace/lib/health.js +79 -0
  59. package/workspace/lib/initiative-cli.js +222 -0
  60. package/workspace/lib/initiative-display.js +183 -0
  61. package/workspace/lib/initiative-needs.js +127 -0
  62. package/workspace/lib/initiative-rows.js +206 -0
  63. package/workspace/lib/initiative-writer.js +141 -0
  64. package/workspace/lib/issues.js +206 -0
  65. package/workspace/lib/item-cli.js +195 -0
  66. package/workspace/lib/item-spans.js +137 -0
  67. package/workspace/lib/measure-cli.js +100 -0
  68. package/workspace/lib/measurements.js +189 -0
  69. package/workspace/lib/mobile-nav.js +57 -53
  70. package/workspace/lib/needs-providers.js +120 -0
  71. package/workspace/lib/placements.js +440 -0
  72. package/workspace/lib/plan-items.js +922 -0
  73. package/workspace/lib/plan-ledgers.js +85 -0
  74. package/workspace/lib/plan-rules.js +72 -0
  75. package/workspace/lib/plan-tree.js +367 -0
  76. package/workspace/lib/pm-guard.js +174 -0
  77. package/workspace/lib/pm-lines-needs.js +331 -0
  78. package/workspace/lib/pmos-verbs.js +66 -0
  79. package/workspace/lib/proactive.js +7 -4
  80. package/workspace/lib/report-cli.js +152 -0
  81. package/workspace/lib/report-draft.js +579 -0
  82. package/workspace/lib/report-needs.js +213 -0
  83. package/workspace/lib/route-modules.js +128 -0
  84. package/workspace/lib/routes-board.js +91 -0
  85. package/workspace/lib/routes-capture.js +52 -0
  86. package/workspace/lib/routes-initiatives.js +371 -0
  87. package/workspace/lib/routes-reports.js +163 -0
  88. package/workspace/lib/routes-timeline.js +197 -0
  89. package/workspace/lib/routes-today.js +32 -0
  90. package/workspace/lib/today-cli.js +99 -0
  91. package/workspace/lib/today.js +333 -0
  92. package/workspace/lib/triage.js +163 -0
  93. package/workspace/lib/verify-work-cli.js +81 -0
  94. package/workspace/lib/work-check-needs.js +99 -0
  95. package/workspace/package-lock.json +1 -1
  96. package/workspace/package.json +4 -5
  97. package/workspace/public/index.html +29 -19
  98. package/workspace/public/pm/app.js +299 -30
  99. package/workspace/public/pm/bd-board.js +113 -0
  100. package/workspace/public/pm/bd-drag.js +59 -0
  101. package/workspace/public/pm/bd-model.js +152 -0
  102. package/workspace/public/pm/bd-timeline.js +233 -0
  103. package/workspace/public/pm/bd-tree.js +244 -0
  104. package/workspace/public/pm/board-copy.js +57 -0
  105. package/workspace/public/pm/breakdown-api.js +58 -0
  106. package/workspace/public/pm/breakdown-copy.js +138 -0
  107. package/workspace/public/pm/components.css +3054 -0
  108. package/workspace/public/pm/cycle-review.js +125 -0
  109. package/workspace/public/pm/data.js +40 -187
  110. package/workspace/public/pm/dest-board.js +438 -0
  111. package/workspace/public/pm/dest-initiatives.js +134 -0
  112. package/workspace/public/pm/dest-reports.js +270 -0
  113. package/workspace/public/pm/dest-settings.js +217 -0
  114. package/workspace/public/pm/dest-today.js +176 -0
  115. package/workspace/public/pm/dialogs.js +88 -0
  116. package/workspace/public/pm/display-word.js +56 -0
  117. package/workspace/public/pm/dock.js +105 -0
  118. package/workspace/public/pm/fixture.js +8 -227
  119. package/workspace/public/pm/frame-copy.js +94 -0
  120. package/workspace/public/pm/handoff-box.js +82 -0
  121. package/workspace/public/pm/http.js +88 -0
  122. package/workspace/public/pm/icons.js +53 -0
  123. package/workspace/public/pm/initiative-list.js +218 -0
  124. package/workspace/public/pm/initiative-sheet.js +261 -0
  125. package/workspace/public/pm/initiatives-api.js +59 -0
  126. package/workspace/public/pm/initiatives-copy.js +215 -0
  127. package/workspace/public/pm/initiatives-timeline-copy.js +59 -0
  128. package/workspace/public/pm/initiatives-timeline.js +410 -0
  129. package/workspace/public/pm/initiatives-ui.js +123 -0
  130. package/workspace/public/pm/item-sheet.js +251 -0
  131. package/workspace/public/pm/keep-card.js +204 -0
  132. package/workspace/public/pm/lifecycle.js +7 -1
  133. package/workspace/public/pm/mobile.css +49 -20
  134. package/workspace/public/pm/mobile.js +157 -29
  135. package/workspace/public/pm/modes-live.js +7 -883
  136. package/workspace/public/pm/modes.js +32 -1024
  137. package/workspace/public/pm/new-initiative-sheet.js +74 -0
  138. package/workspace/public/pm/next-step-ui.js +3 -2
  139. package/workspace/public/pm/palette-copy.js +51 -0
  140. package/workspace/public/pm/palette.js +312 -0
  141. package/workspace/public/pm/panel-breakdown.js +702 -0
  142. package/workspace/public/pm/panel-cockpit.js +63 -295
  143. package/workspace/public/pm/panel-detail.js +25 -172
  144. package/workspace/public/pm/panel-initiatives.js +1 -19
  145. package/workspace/public/pm/panel-repos.js +31 -81
  146. package/workspace/public/pm/panel-tabs.js +197 -110
  147. package/workspace/public/pm/panel-trees.js +5 -2
  148. package/workspace/public/pm/portfolio-report.js +61 -0
  149. package/workspace/public/pm/primitives.js +656 -0
  150. package/workspace/public/pm/print.css +33 -0
  151. package/workspace/public/pm/provider-cards.js +136 -0
  152. package/workspace/public/pm/registry.js +194 -0
  153. package/workspace/public/pm/release-card.js +110 -0
  154. package/workspace/public/pm/report-view.js +309 -0
  155. package/workspace/public/pm/reports-copy.js +114 -0
  156. package/workspace/public/pm/roadmap.js +271 -0
  157. package/workspace/public/pm/sig-watch.js +41 -0
  158. package/workspace/public/pm/state.js +8 -4
  159. package/workspace/public/pm/status-banner.js +13 -5
  160. package/workspace/public/pm/tab-doc.js +2 -4
  161. package/workspace/public/pm/tab-term.js +12 -2
  162. package/workspace/public/pm/tab-web.js +3 -2
  163. package/workspace/public/pm/timeline-copy.js +48 -0
  164. package/workspace/public/pm/today-cards.js +244 -0
  165. package/workspace/public/pm/today-copy.js +97 -0
  166. package/workspace/public/pm/tokens.css +16 -18
  167. package/workspace/public/pm/topbar.js +102 -0
  168. package/workspace/public/pm/triage-card.js +60 -0
  169. package/workspace/public/pm/undo-hold.js +103 -0
  170. package/workspace/public/pm/view-addrepo.js +2 -1
  171. package/workspace/public/pm/view-livenotes.js +3 -1
  172. package/workspace/public/pm/view-today.js +7 -162
  173. package/workspace/public/vendor/Sortable.min.js +2 -0
  174. package/workspace/public/vendor/lucide-sprite.js +79 -0
  175. package/workspace/replaced-tests.json +2864 -0
  176. package/workspace/run-tests.js +56 -5
  177. package/workspace/server.js +312 -36
  178. package/workspace/sheet-answer.js +61 -0
  179. package/workspace/skills/measure.skill +41 -0
  180. package/workspace/skills/report-biweekly.skill +23 -25
  181. package/workspace/skills/tracker-refresh.skill +50 -0
  182. package/workspace/test-files.json +19 -3
  183. package/workspace/test-lists/base.json +6 -0
  184. package/workspace/test-lists/run2-a.json +6 -0
  185. package/workspace/test-lists/run2-b.json +13 -0
  186. package/workspace/test-lists/run2-c.json +19 -0
  187. package/workspace/test-lists/run2-d.json +22 -0
  188. package/workspace/test-lists/run3-0.json +5 -0
  189. package/workspace/test-lists/run3-a.json +5 -0
  190. package/workspace/test-lists/run3-b.json +6 -0
  191. package/workspace/test-lists/run3-c.json +15 -0
  192. package/workspace/test-lists/run3-d.json +6 -0
  193. package/workspace/test-lists/run4-a.json +8 -0
  194. package/workspace/test-lists/run4-b.json +6 -0
  195. package/workspace/test-lists/run4-c.json +13 -0
  196. package/workspace/test-lists/run4-d.json +9 -0
  197. package/workspace/test-lists/run5-a.json +6 -0
  198. package/workspace/test-lists/run5-b.json +6 -0
  199. package/workspace/test-lists/run5-c.json +15 -0
  200. package/workspace/test-run.js +27 -1
  201. package/workspace/third-party/README.md +44 -0
  202. package/workspace/third-party/lucide-static-1.52.0.LICENSE +43 -0
  203. package/workspace/third-party/sortablejs-1.15.7.LICENSE +21 -0
  204. package/workspace/design-parity.js +0 -673
  205. package/workspace/public/pm/tab-task.js +0 -86
  206. package/workspace/public/pm/view-guide.js +0 -141
  207. package/workspace/public/pm/view-knowledge.js +0 -225
  208. package/workspace/public/pm/view-memory.js +0 -122
  209. package/workspace/public/pm/view-portfolio.js +0 -153
  210. package/workspace/public/pm/view-reports.js +0 -190
  211. package/workspace/public/pm/view-scheduled.js +0 -161
  212. package/workspace/public/pm/view-skills.js +0 -216
@@ -1,86 +0,0 @@
1
- /* pm/tab-task.js — the task tab body: one task, stated the way a ticket states one.
2
- *
3
- * A port of the prototype's task pane (template lines 940–979, derivation 2008–2018). The five
4
- * fields are fixed and standardized — instruction · input · expected outcome · evaluation · output
5
- * — because a task an agent is asked to run is only reviewable if what it was told, what it was
6
- * given, what it owed and how it will be judged are all on one page.
7
- *
8
- * The output field is the honest one: it is a button to the artifact ONLY when an artifact exists.
9
- * Where nothing has been produced it says so in as many words rather than showing an empty row that
10
- * reads like a file nobody can find. Nothing here is invented per task — the panel renders the tab
11
- * record panel 3 derived, and a field the engine has no evidence for arrives empty.
12
- */
13
-
14
- import { el, setChildren } from './dom.js';
15
- import * as FX from './fixture.js';
16
- import { openDoc } from './panel-initiatives.js';
17
-
18
- /** `{{ expr }}` — one interpolated value in its own span, as the prototype's runtime emits it. */
19
- const iv = (v) => el('span', null, v === null || v === undefined ? '' : String(v));
20
-
21
- const LABEL = 'font-family:var(--mono);font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--muted-foreground);margin-bottom:5px';
22
- /* The output label carries 6px, not 5px. It is not a typo in the design and it is not corrected. */
23
- const LABEL_OUT = 'font-family:var(--mono);font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--muted-foreground);margin-bottom:6px';
24
- const PROSE = 'font-size:14px;line-height:1.6;text-wrap:pretty';
25
- const MONO = 'font-family:var(--mono);font-size:12.5px;color:var(--muted-foreground);overflow-wrap:anywhere';
26
-
27
- /** One labelled field: a mono uppercase label over its value. */
28
- const field = (label, value, labelStyle, valueStyle) =>
29
- el('div', null,
30
- el('div', { style: labelStyle || LABEL }, label),
31
- el('div', { style: valueStyle || PROSE }, iv(value)),
32
- );
33
-
34
- /**
35
- * Render the task pane for `tab` into `host` — the pane element panel 4 keeps for this tab.
36
- * The pane has no editable state of its own, so it is rebuilt whole on each paint.
37
- */
38
- export function mountTask(host, tab) {
39
- const hasOutput = !!tab.output && tab.output !== '—';
40
- const statusColor = FX.TASK_STATUS_COLOR[tab.status] || FX.C.mut;
41
-
42
- const output = hasOutput
43
- ? el('button', {
44
- onClick: () => openDoc(tab.slug, tab.output, 'FILE'),
45
- style: 'display:flex;gap:8px;align-items:center;font-family:var(--mono);font-size:12px;padding:7px 11px;background:var(--card);border:1px solid var(--border);color:var(--info);min-height:32px;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap',
46
- hover: 'border-color:var(--ring)',
47
- },
48
- el('span', { style: 'flex:none;font-size:9px;letter-spacing:.1em;color:var(--muted-foreground)' }, 'OPEN'),
49
- iv(tab.output),
50
- )
51
- : el('div', { style: 'font-size:13px;color:var(--muted-foreground);font-style:italic' },
52
- // Only a step that is not done is "not produced yet". A done step with no file says what decided it.
53
- tab.status === 'done'
54
- ? 'this step read no file of its own — the rule and evidence above are its result'
55
- : 'no artifact yet — produced when the agent runs this task');
56
-
57
- const body = el('div', {
58
- style: 'flex:1;min-height:0;overflow-y:auto;background:var(--background);color:var(--foreground);padding:22px 26px 60px;font-size:14px;line-height:1.65',
59
- },
60
- el('div', { style: 'max-width:76ch' },
61
- el('div', { style: 'display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-bottom:5px' },
62
- el('span', {
63
- style: 'font-family:var(--mono);font-size:10px;letter-spacing:.12em;text-transform:uppercase;color:var(--muted-foreground)',
64
- }, iv(tab.phase), ' · task'),
65
- el('span', {
66
- style: 'font-family:var(--mono);font-size:10px;padding:2px 8px;border:1px solid var(--border);color:' + statusColor,
67
- }, iv(tab.status)),
68
- ),
69
- el('div', {
70
- style: 'font-size:20px;font-weight:600;letter-spacing:-.01em;margin-bottom:20px;text-wrap:pretty',
71
- }, iv(tab.title)),
72
- el('div', { style: 'display:flex;flex-direction:column;gap:16px' },
73
- field('instruction to the agent', tab.instruction),
74
- field('input', tab.input, LABEL, MONO),
75
- field('expected outcome', tab.expect),
76
- field('evaluation', tab.evalNote),
77
- el('div', null, el('div', { style: LABEL_OUT }, 'output'), output),
78
- ),
79
- ),
80
- );
81
-
82
- setChildren(host, [body]);
83
- return host;
84
- }
85
-
86
- export default mountTask;
@@ -1,141 +0,0 @@
1
- /* pm/view-guide.js — the User guide view, drawn from planning/design/cockpit-mockup/ViewGuide.dc.html.
2
- *
3
- * PRD workspace-cockpit-nav, Run 3 (R4). The board is two panes. On the left, a 280px column with a search field and the
4
- * list of pages. On the right, the page that is chosen, with its title and its text.
5
- *
6
- * The list is the view's existing list (modes-live.js `guideList`, from GET /api/guide), placed in the left column and
7
- * restyled. No node is removed, so every `data-r18="guide-page"` row keeps its `data-page` and its press: a row still opens
8
- * the page as a read-only doc tab. This module adds a search that hides the rows that do not match, and remembers the page
9
- * opened last so the right pane can show it. The right pane reads the page from GET /api/guide?page=, the same route the doc
10
- * tab uses, and draws it with the doc tab's markdown renderer. A link to another guide page shows that page here.
11
- *
12
- * The board's group labels (Start, Working, Control) and its page titles are not in the guide's data, so the column shows
13
- * the pages in the order the server gives them, under one label. In the fixture the guide is not read, and the board says so.
14
- */
15
- import { el } from './dom.js';
16
- import { mdRender } from './markdown.js';
17
-
18
- const TTL = 15000;
19
- const pages = new Map(); // page -> { state: 'loading'|'ok'|'error', text, error, at }
20
- let chosen = null; // the page opened last from this board
21
- let query = ''; // the search text; kept so a redraw keeps the filter
22
-
23
- const LBL = 'font:500 10px var(--mono);letter-spacing:.1em;text-transform:uppercase;color:var(--muted-foreground)';
24
- const ITEM = 'display:flex;align-items:center;width:100%;box-sizing:border-box;min-height:36px;padding:6px 12px;border:0;border-radius:8px;font:500 13px var(--sans);line-height:1.35;text-align:left;color:var(--foreground);background:transparent;cursor:pointer';
25
- const PILL = 'font:500 11px var(--sans);border:1px solid var(--border);border-radius:999px;padding:0 8px;line-height:18px;background:transparent;color:var(--foreground);cursor:pointer';
26
- const NOTE = 'color:var(--muted-foreground);font-size:13px;line-height:1.5';
27
-
28
- /** The board's own read of one guide page, read again after 15 seconds. */
29
- function readPage(ctx, page) {
30
- const hit = pages.get(page);
31
- if (hit && (hit.state === 'loading' || Date.now() - hit.at < TTL)) return hit;
32
- const next = { state: hit ? hit.state : 'loading', text: hit ? hit.text : null, error: null, at: hit ? hit.at : 0 };
33
- if (!hit || hit.state !== 'ok') next.state = 'loading';
34
- pages.set(page, next);
35
- fetch('/api/guide?page=' + encodeURIComponent(page))
36
- .then((r) => (r.ok ? r.text() : Promise.reject(new Error('no such guide page'))))
37
- .then((text) => { pages.set(page, { state: 'ok', text, error: null, at: Date.now() }); ctx.setState({}); },
38
- (e) => { pages.set(page, { state: 'error', text: null, error: (e && e.message) || String(e), at: Date.now() }); ctx.setState({}); });
39
- return next;
40
- }
41
-
42
- /** Hide the rows that do not match the search. Hiding is a style; the rows stay in the list. */
43
- function filter(list) {
44
- const q = query.trim().toLowerCase();
45
- let shown = 0;
46
- for (const row of list.querySelectorAll('[data-r18="guide-page"]')) {
47
- const hit = !q || (row.textContent || '').toLowerCase().includes(q) || (row.getAttribute('data-page') || '').includes(q);
48
- row.style.display = hit ? '' : 'none';
49
- if (hit) shown++;
50
- }
51
- return shown;
52
- }
53
-
54
- function restyle(list, current) {
55
- const wrap = list.querySelector('[data-r18="guide-list"]');
56
- if (wrap) {
57
- wrap.setAttribute('style', 'display:flex;flex-direction:column;gap:2px');
58
- // the list's opening line has no hook; it reads as the column's small print
59
- for (const k of wrap.children) if (!k.hasAttribute('data-r18')) k.setAttribute('style', NOTE + ';font-size:12px;padding:2px 12px 8px');
60
- }
61
- for (const row of list.querySelectorAll('[data-r18="guide-page"]')) {
62
- const on = row.getAttribute('data-page') === current;
63
- row.setAttribute('style', ITEM + (on ? ';background:var(--accent)' : ''));
64
- if (on) row.setAttribute('data-board-chosen', ''); else row.removeAttribute('data-board-chosen');
65
- }
66
- for (const n of list.querySelectorAll('[data-r18="guide-empty"]')) n.setAttribute('style', NOTE + ';padding:4px 12px');
67
- }
68
-
69
- /** The right pane: the chosen page, read from the server and drawn with the doc tab's renderer. */
70
- function pagePane(ctx, list, page, title) {
71
- if (!page) return el('div', { 'data-board-part': 'page', style: NOTE }, 'Choose a page on the left.');
72
- const r = readPage(ctx, page);
73
- const row = list.querySelector('[data-r18="guide-page"][data-page="' + page.replace(/"/g, '') + '"]');
74
- const actions = el('div', { 'data-board-part': 'actions', style: 'display:flex;gap:8px;flex-wrap:wrap;margin-top:6px' },
75
- row ? el('button', { style: PILL, hover: 'background:var(--accent)', 'data-board-action': 'open-tab', onClick: () => row.click() }, 'Open in a tab') : null,
76
- el('button', { style: PILL, hover: 'background:var(--accent)', 'data-board-action': 'open-today', onClick: () => ctx.openView('today') }, 'Open Today'));
77
- if (r.state === 'loading' && !r.text) {
78
- return el('div', { 'data-board-part': 'page', 'data-page': page, style: 'display:flex;flex-direction:column;gap:12px' },
79
- el('div', { style: LBL }, 'Guide'), el('div', { style: NOTE }, 'Reading the page …'));
80
- }
81
- if (r.state === 'error') {
82
- return el('div', { 'data-board-part': 'page', 'data-page': page, style: 'display:flex;flex-direction:column;gap:12px' },
83
- el('div', { style: LBL }, 'Guide'), el('div', { style: NOTE + ';color:var(--destructive)' }, 'The page could not be read: ' + r.error));
84
- }
85
- const known = new Set([...list.querySelectorAll('[data-r18="guide-page"]')].map((n) => n.getAttribute('data-page')));
86
- const doc = mdRender(r.text, {
87
- dir: [],
88
- // a link to another guide page shows that page here; any other link is left as text by the renderer's rules
89
- onDoc: (p) => { const name = String(p).replace(/\.md$/, '').split('/').pop(); if (known.has(name)) { chosen = name; ctx.setState({}); } },
90
- });
91
- return el('div', { 'data-board-part': 'page', 'data-page': page, style: 'display:flex;flex-direction:column;gap:12px;min-width:0' },
92
- el('div', { style: LBL }, 'Guide'),
93
- el('div', { 'data-board-title': '', style: 'font:600 26px var(--sans);line-height:1.3' }, doc.title || title || page),
94
- actions,
95
- el('div', { 'data-board-part': 'page-body', style: 'max-width:640px;font-size:14px;line-height:1.6;color:var(--foreground)' }, doc.blocks.map((b) => el('div', null, b))));
96
- }
97
-
98
- export function renderBoard(inner, ctx) {
99
- const { FIXTURE } = ctx;
100
-
101
- const list = el('div', { 'data-board-section': 'pages', style: 'display:flex;flex-direction:column;gap:2px' });
102
- ctx.renderMode(list);
103
- // remember the page opened last; the row's own press still opens it as a tab
104
- list.addEventListener('click', (e) => {
105
- const row = e.target && e.target.closest ? e.target.closest('[data-r18="guide-page"]') : null;
106
- if (row) chosen = row.getAttribute('data-page');
107
- }, true);
108
-
109
- const rows = [...list.querySelectorAll('[data-r18="guide-page"]')];
110
- const names = rows.map((r) => r.getAttribute('data-page'));
111
- if (!chosen || !names.includes(chosen)) chosen = names.includes('index') ? 'index' : (names[0] || null);
112
- restyle(list, chosen);
113
- const shown = filter(list);
114
-
115
- const search = el('input', {
116
- type: 'search', placeholder: 'Search the guide…', value: query, 'data-board-part': 'search', 'aria-label': 'Search the guide',
117
- style: 'width:100%;box-sizing:border-box;height:32px;min-height:32px;padding:0 10px;border:1px solid var(--input);border-radius:8px;background:transparent;color:var(--foreground);font:13px var(--sans)',
118
- onInput: (e) => {
119
- query = e.target.value;
120
- const n = filter(list);
121
- const none = side.querySelector('[data-board-part="no-match"]');
122
- if (none) none.style.display = n || !rows.length ? 'none' : '';
123
- },
124
- });
125
- const noMatch = el('div', { 'data-board-part': 'no-match', style: NOTE + ';padding:4px 12px' + (shown || !rows.length ? ';display:none' : '') }, 'No page matches the search.');
126
-
127
- const side = el('div', { 'data-board-part': 'sidebar',
128
- style: 'width:280px;flex:none;box-sizing:border-box;border-right:1px solid var(--border);background:var(--sidebar);padding:14px;display:flex;flex-direction:column;gap:6px' },
129
- search,
130
- el('div', { style: LBL + ';padding:8px 12px 2px' }, 'Pages'),
131
- FIXTURE ? el('div', { style: NOTE + ';padding:4px 12px' }, 'The guide is not read in the fixture.') : list,
132
- noMatch);
133
-
134
- const chosenRow = rows.find((r) => r.getAttribute('data-page') === chosen);
135
- const main = el('div', { 'data-board-part': 'main', style: 'flex:1 1 0;min-width:0;padding:26px 40px;display:flex;flex-direction:column;gap:12px' },
136
- FIXTURE ? el('div', { style: NOTE }, 'The guide is not read in the fixture.') : pagePane(ctx, list, chosen, chosenRow ? (chosenRow.textContent || '').trim() : null));
137
-
138
- inner.appendChild(el('div', { 'data-board': 'guide',
139
- style: 'display:flex;min-height:560px;border:1px solid var(--border);border-radius:10px;overflow:hidden;background:var(--card);color:var(--foreground);font-size:14px;line-height:1.6' },
140
- side, main));
141
- }
@@ -1,225 +0,0 @@
1
- /* pm/view-knowledge.js — the Knowledge view tab, drawn from planning/design/cockpit-mockup/ViewKnowledge.dc.html.
2
- *
3
- * PRD workspace-cockpit-nav, Run 3 (R4). The board is two panes. On the left, a 330px column with a two-way switch, a search
4
- * field and the list. On the right, the detail of the chosen item.
5
- *
6
- * The list is the view's existing list (modes-live.js `knowledgeLive`, or the fixture's decisions), placed unchanged, so every
7
- * `data-r4="know"`, `data-r11`, `data-cat` and `data-cat-filter` hook and every live behaviour stays as it was: a row still opens
8
- * its document as a tab. This module only adds the frame, a search that hides rows that do not match, and a `data-path` on each
9
- * row so the right pane can show the document that was opened last.
10
- *
11
- * The facts in the right pane come from GET /api/library, the same read the list uses: a document's name, its category and
12
- * the basis that decided it, and its file. The board's "Cited by", "Supersedes" and status badge come from the knowledge graph,
13
- * which this app does not read in the browser. The pane says so, and names `pmos kg` as the place to read them. The board's
14
- * "Graph" switch shows the same plain note. Nothing on this board is invented.
15
- */
16
- import { el } from './dom.js';
17
-
18
- const TTL = 15000;
19
- let lib = { ws: null, state: 'idle', data: null, error: null, at: 0 }; // this board's read of /api/library
20
- let seg = 'docs'; // the left switch: 'docs' (the list) or 'graph' (not read here)
21
- let query = ''; // the search text; kept so a redraw keeps the filter
22
- const chosen = {}; // ws -> the path of the document opened last from this board
23
-
24
- const LBL = 'font:500 10px var(--mono);letter-spacing:.1em;text-transform:uppercase;color:var(--muted-foreground)';
25
- const BADGE = 'font:500 11px var(--sans);border:1px solid var(--border);border-radius:999px;padding:0 8px;line-height:18px;white-space:nowrap;display:inline-block;flex:none';
26
- const TONE = {
27
- s: ';border-color:transparent;background:color-mix(in oklab, var(--success) 16%, transparent);color:var(--success)',
28
- w: ';border-color:transparent;background:color-mix(in oklab, var(--warning) 16%, transparent);color:var(--warning)',
29
- };
30
- const BTN = 'font:500 12.5px var(--sans);background:transparent;color:var(--foreground);border:1px solid var(--input);border-radius:8px;padding:0 12px;height:30px;display:inline-grid;place-items:center;cursor:pointer;flex:none';
31
- const MUTED = 'color:var(--muted-foreground)';
32
- const badge = (text, tone, attrs) => el('span', { ...(attrs || {}), style: BADGE + (tone ? TONE[tone] : '') }, String(text));
33
- const rule = () => el('div', { style: 'height:1px;background:var(--border);flex:none' });
34
-
35
- /** The board's own read of /api/library for one repository, read again after 15 seconds, like the list's. */
36
- function library(ctx, ws) {
37
- if (ctx.FIXTURE) return null;
38
- const same = lib.ws === ws;
39
- if (same && (lib.state === 'loading' || Date.now() - lib.at < TTL)) return lib;
40
- const prev = same ? lib : null;
41
- lib = { ws, state: 'loading', data: prev ? prev.data : null, error: null, at: prev ? prev.at : 0 };
42
- fetch('/api/library?ws=' + encodeURIComponent(ws)).then((r) => r.json()).then(
43
- (data) => { if (lib.ws !== ws) return; lib = { ws, state: data && data.ok === false ? 'error' : 'ok', data, error: data && data.error, at: Date.now() }; ctx.setState({}); },
44
- (e) => { if (lib.ws !== ws) return; lib = { ws, state: 'error', data: null, error: (e && e.message) || String(e), at: Date.now() }; ctx.setState({}); },
45
- );
46
- return lib;
47
- }
48
-
49
- /** The same walk as the list's (modes-live.js `flatten`), so the n-th row and the n-th document are the same document. */
50
- function flatten(node, out, depth, tree) {
51
- for (const d of node.docs || []) out.push({ path: d.path, name: d.name, tree, category: d.category || null, basis: d.category_basis || null, why: d.category_why || null });
52
- if (depth < 3) for (const sub of node.dirs || []) { out.push({ dir: true }); flatten(sub, out, depth + 1, tree); }
53
- return out;
54
- }
55
- const docsOf = (trees) => {
56
- const out = [];
57
- for (const t of trees || []) for (const it of flatten(t, [], 0, t.path || t.name || 'knowledge')) if (!it.dir) out.push(it);
58
- return out;
59
- };
60
-
61
- /** Give each row of the list the path of its document. The list has a category filter of its own; its pressed button says
62
- * which category is shown, so the rows are matched against the same subset. When the counts differ (the two reads are a
63
- * moment apart) nothing is marked, and the next redraw tries again. */
64
- function markRows(host, data) {
65
- const know = [...host.querySelectorAll('[data-r4="know"]')];
66
- const pressed = host.querySelector('[data-cat-filter][aria-pressed="true"]');
67
- const cat = pressed ? pressed.getAttribute('data-cat-filter') : '';
68
- const docs = docsOf(data.knowledge).filter((d) => !cat || d.category === cat);
69
- if (docs.length === know.length) know.forEach((b, i) => b.setAttribute('data-path', docs[i].path));
70
- const noteRows = [...host.querySelectorAll('[data-r11="note"]')];
71
- const notes = docsOf(data.notes);
72
- if (notes.length === noteRows.length) noteRows.forEach((b, i) => b.setAttribute('data-path', notes[i].path));
73
- }
74
-
75
- /** Hide the rows that do not match the search. Folder lines are hidden while a search is on, as they no longer group anything. */
76
- function applySearch(host, q) {
77
- const needle = q.trim().toLowerCase();
78
- let shown = 0;
79
- for (const b of host.querySelectorAll('[data-r4="know"], [data-r11="note"], button')) {
80
- if (b.hasAttribute('data-cat-filter')) continue;
81
- const hay = ((b.getAttribute('data-path') || '') + ' ' + (b.textContent || '')).toLowerCase();
82
- const hit = !needle || hay.includes(needle);
83
- b.style.display = hit ? '' : 'none';
84
- if (hit) shown++;
85
- }
86
- for (const d of host.querySelectorAll('div')) {
87
- if (d.attributes.length !== 1 || d.children.length !== 1) continue; // a folder line: one style, one text span
88
- if (/\/$/.test((d.textContent || '').trim())) d.style.display = needle ? 'none' : '';
89
- }
90
- return shown;
91
- }
92
-
93
- function segButton(key, label, ctx) {
94
- const on = seg === key;
95
- return el('button', {
96
- 'data-kn-seg': key, 'aria-pressed': on ? 'true' : 'false',
97
- onClick: () => { seg = key; ctx.setState({}); },
98
- style: 'flex:1 1 0;text-align:center;border:0;border-radius:6px;padding:3px 0;font:12.5px var(--sans);cursor:pointer;'
99
- + (on ? 'background:var(--background);font-weight:500;color:var(--foreground)' : 'background:transparent;color:var(--muted-foreground)'),
100
- }, label);
101
- }
102
-
103
- /** The right pane when the Graph switch is on: the graph is not read in this view. */
104
- function graphPane() {
105
- return [
106
- el('div', { 'data-board': 'detail-head', style: 'display:flex;align-items:center;gap:10px' }, el('span', { style: LBL }, 'Graph')),
107
- el('div', { 'data-board': 'title', style: 'font:600 22px var(--sans)' }, 'The knowledge graph'),
108
- el('div', { 'data-kn': 'not-read', style: MUTED + ';max-width:640px' },
109
- 'This view does not read the knowledge graph. To see how decisions cite and supersede each other, run ',
110
- el('code', { style: 'font:12px var(--mono);color:var(--foreground)' }, 'pmos kg'), ' in a terminal.'),
111
- ];
112
- }
113
-
114
- /** The right pane for the document opened last: its name, category and file, and what is not read here. */
115
- function docPane(doc, reopen) {
116
- const cat = doc.category;
117
- const tone = cat === 'undetermined' ? 'w' : cat ? 's' : null;
118
- return [
119
- el('div', { 'data-board': 'detail-head', style: 'display:flex;align-items:center;gap:10px;flex-wrap:wrap' },
120
- el('span', { style: 'font:500 12px var(--mono);' + MUTED }, doc.tree),
121
- cat ? badge(cat, tone, { 'data-kn': 'category' }) : null),
122
- el('div', { 'data-board': 'title', style: 'font:600 22px var(--sans);overflow-wrap:anywhere' }, doc.name || doc.path),
123
- el('div', { style: MUTED + ';max-width:640px' }, cat
124
- ? 'Category ' + cat + (doc.basis ? ', decided by ' + doc.basis : ', with no evidence') + (doc.why ? ' (' + doc.why + ')' : '') + '. The category is advice. Nothing is written.'
125
- : 'This document has no category.'),
126
- el('div', null, el('button', { 'data-kn': 'open', style: BTN, hover: 'background:var(--accent)', onClick: reopen }, 'Open in a tab')),
127
- rule(),
128
- el('div', { style: LBL }, 'Cited by and supersedes'),
129
- el('div', { 'data-kn': 'not-read', style: MUTED }, 'Not read in this view. Run ',
130
- el('code', { style: 'font:12px var(--mono);color:var(--foreground)' }, 'pmos kg'), ' in a terminal to see them.'),
131
- el('div', { style: LBL + ';margin-top:8px' }, 'File'),
132
- el('div', { 'data-kn': 'file', style: 'font:12px var(--mono);color:var(--info);overflow-wrap:anywhere' }, doc.path),
133
- ];
134
- }
135
-
136
- /** The right pane before anything is chosen: what the list holds, counted from the read. */
137
- function summaryPane(ctx, r) {
138
- const out = [
139
- el('div', { 'data-board': 'detail-head', style: 'display:flex;align-items:center;gap:10px' }, el('span', { style: LBL }, 'Knowledge')),
140
- el('div', { 'data-board': 'title', style: 'font:600 22px var(--sans)' }, ctx.FIXTURE ? 'Decisions' : 'This repository\'s knowledge'),
141
- el('div', { style: MUTED + ';max-width:640px' }, ctx.FIXTURE
142
- ? 'Choose a decision on the left. Its detail opens in the detail column.'
143
- : 'Choose a document on the left to open it in a tab. Its facts then show here.'),
144
- ];
145
- if (ctx.FIXTURE) return out;
146
- out.push(rule());
147
- if (!r || r.state === 'loading' && !r.data) { out.push(el('div', { 'data-kn': 'reading', style: MUTED }, 'Reading the knowledge …')); return out; }
148
- if (r.state === 'error') { out.push(el('div', { 'data-kn': 'error', style: 'color:var(--destructive)' }, 'The knowledge could not be read: ' + (r.error || 'error'))); return out; }
149
- const sets = [['knowledge', (r.data && r.data.knowledge) || []], ['notes', (r.data && r.data.notes) || []]];
150
- out.push(el('div', { style: LBL }, 'In this repository'));
151
- const rows = [];
152
- for (const [kind, trees] of sets) {
153
- for (const t of trees) {
154
- const n = docsOf([t]).length;
155
- rows.push(el('div', { 'data-kn-tree': kind, style: 'display:flex;gap:10px;align-items:center' },
156
- badge(kind === 'notes' ? 'notes' : 'bundle'),
157
- el('span', { style: 'font:12px var(--mono);flex:1 1 auto;min-width:0;overflow-wrap:anywhere' }, t.path || t.name || kind),
158
- el('span', { style: MUTED + ';font-size:12px' }, n + (n === 1 ? ' document' : ' documents'))));
159
- }
160
- }
161
- if (!rows.length) rows.push(el('div', { 'data-kn': 'empty', style: MUTED }, 'This repository has no knowledge bundle and no notes.'));
162
- out.push(el('div', { style: 'display:flex;flex-direction:column;gap:6px' }, rows));
163
- return out;
164
- }
165
-
166
- export function renderBoard(inner, ctx) {
167
- const { state } = ctx;
168
- const ws = state.wsId || '';
169
- const r = library(ctx, ws);
170
-
171
- // The board fills the tab from edge to edge, as the mockup does: the pane's own padding is dropped for this view only.
172
- const pane = inner.parentElement;
173
- if (pane) { pane.style.padding = '0'; pane.style.overflow = 'hidden'; pane.style.display = 'flex'; pane.style.flexDirection = 'column'; }
174
- inner.style.cssText = 'flex:1 1 auto;min-height:0;display:flex;flex-direction:row;gap:0;max-width:none;height:100%';
175
- inner.setAttribute('data-board', 'knowledge');
176
-
177
- // the list, unchanged, inside the left column
178
- const host = el('div', { 'data-board-section': 'list', style: 'display:flex;flex-direction:column;gap:2px;margin:0 -14px' });
179
- ctx.renderMode(host);
180
- if (!host.firstChild) host.appendChild(el('div', { style: MUTED + ';padding:8px 14px;font-size:12px' }, ctx.ready ? 'This list has nothing to show.' : 'Reading the list …'));
181
- if (r && r.data) markRows(host, r.data);
182
- const docPath = chosen[ws] || '';
183
- if (docPath) for (const b of host.querySelectorAll('[data-path]')) {
184
- if (b.getAttribute('data-path') !== docPath) continue;
185
- b.setAttribute('data-kn-on', '1');
186
- b.style.background = 'var(--accent)';
187
- }
188
- // A press on a row is recorded before the row's own handler opens the document, so the right pane can show it.
189
- host.addEventListener('click', (e) => {
190
- const b = e.target && e.target.closest ? e.target.closest('[data-path]') : null;
191
- if (b && host.contains(b)) chosen[ws] = b.getAttribute('data-path');
192
- }, true);
193
-
194
- const noMatch = el('div', { 'data-kn': 'no-match', style: MUTED + ';padding:8px 0;font-size:12px;display:none' }, 'No document matches the search.');
195
- const filter = (q) => { noMatch.style.display = applySearch(host, q) || !q.trim() ? 'none' : ''; };
196
- filter(query);
197
-
198
- const wasFocused = typeof document !== 'undefined' && document.activeElement && document.activeElement.getAttribute('data-kn-search') === '1';
199
- const caret = wasFocused ? document.activeElement.selectionStart : null;
200
- const search = el('input', {
201
- type: 'search', 'data-kn-search': '1', 'data-board': 'search', 'aria-label': ctx.FIXTURE ? 'Search decisions' : 'Search documents',
202
- placeholder: ctx.FIXTURE ? 'Search decisions…' : 'Search documents…', value: query,
203
- onInput: (e) => { query = e.target.value; filter(query); },
204
- style: 'box-sizing:border-box;width:100%;flex:none;border:1px solid var(--input);border-radius:8px;height:32px;padding:0 10px;background:transparent;color:var(--foreground);font:13px var(--sans)',
205
- });
206
- if (wasFocused) requestAnimationFrame(() => { search.focus(); if (caret !== null) try { search.setSelectionRange(caret, caret); } catch { /* not a text field */ } });
207
-
208
- const left = el('div', { 'data-board-section': 'sidebar',
209
- style: 'width:330px;flex:none;box-sizing:border-box;border-right:1px solid var(--border);padding:14px;display:flex;flex-direction:column;gap:10px;background:var(--sidebar);overflow-y:auto;min-height:0' },
210
- el('div', { 'data-board': 'toggle', style: 'display:flex;gap:2px;background:var(--accent);border-radius:8px;padding:3px;flex:none' },
211
- segButton('docs', ctx.FIXTURE ? 'Decisions' : 'Documents', ctx), segButton('graph', 'Graph', ctx)),
212
- search, host, noMatch);
213
-
214
- let doc = null;
215
- if (docPath && r && r.data) doc = docsOf(r.data.knowledge).concat(docsOf(r.data.notes)).find((d) => d.path === docPath) || null;
216
- const reopen = () => {
217
- const b = host.querySelector('[data-kn-on="1"]');
218
- if (b) b.click();
219
- };
220
- const right = el('div', { 'data-board-section': 'detail',
221
- style: 'flex:1 1 0;min-width:0;box-sizing:border-box;padding:22px 28px;display:flex;flex-direction:column;gap:12px;overflow-y:auto;min-height:0' },
222
- seg === 'graph' ? graphPane() : doc ? docPane(doc, reopen) : summaryPane(ctx, r));
223
-
224
- inner.append(left, right);
225
- }
@@ -1,122 +0,0 @@
1
- /* pm/view-memory.js — the Memory view, drawn from planning/design/cockpit-mockup/ViewMemory.dc.html.
2
- *
3
- * PRD workspace-cockpit-nav, Run 3 (R4). The board is a header with the actions on the right, a row of badges, the notes as
4
- * cards and a closing note. The notes and every action are the view's existing list (modes-live.js `memoryLive`): this module
5
- * draws it into a host, moves its action bar into the header and restyles its rows as cards. No node is removed, so every
6
- * `data-r9` hook and every live behaviour stays as it was. Only the frame is new.
7
- *
8
- * The badges are read off the list once it is drawn, so they come from the same GET /api/memory as the notes and never
9
- * disagree with them. The board's "used by N sessions" and the note kinds (preference, standard, lesson, fact) have no backing
10
- * in this app, so they are not drawn. In the fixture the prototype's list is drawn under the header, unchanged, and the badges
11
- * are not read.
12
- */
13
- import { el } from './dom.js';
14
-
15
- const BTN = 'font:500 12.5px var(--sans);background:transparent;color:var(--foreground);border:1px solid var(--input);border-radius:8px;padding:0 12px;height:30px;min-height:30px;display:inline-grid;place-items:center;cursor:pointer;flex:none';
16
- const BTNP = 'font:500 12.5px var(--sans);background:var(--foreground);color:var(--background);border:0;border-radius:8px;padding:0 12px;height:30px;min-height:30px;display:inline-grid;place-items:center;cursor:pointer;flex:none';
17
- const SMALL = 'font:500 11.5px var(--sans);background:transparent;color:var(--muted-foreground);border:1px solid var(--border);border-radius:6px;padding:0 9px;height:24px;min-height:24px;display:inline-grid;place-items:center;cursor:pointer';
18
- const BADGE = 'font:500 11px var(--sans);border:1px solid var(--border);border-radius:999px;padding:0 8px;line-height:18px;white-space:nowrap;display:inline-block';
19
- const TONE = {
20
- s: ';border-color:transparent;background:color-mix(in oklab, var(--success) 16%, transparent);color:var(--success)',
21
- w: ';border-color:transparent;background:color-mix(in oklab, var(--warning) 16%, transparent);color:var(--warning)',
22
- d: ';border-color:transparent;background:color-mix(in oklab, var(--destructive) 18%, transparent);color:var(--destructive)',
23
- };
24
- const ENTRY = 'display:flex;flex-direction:column;gap:6px;width:100%;box-sizing:border-box;text-align:left;border:1px solid var(--border);border-radius:10px;padding:12px 14px;background:var(--background);min-height:0';
25
- const LBL = 'font:500 10px var(--mono);letter-spacing:.1em;text-transform:uppercase;color:var(--muted-foreground);padding:8px 0 0';
26
- const NOTE = 'color:var(--muted-foreground);font-size:13px;line-height:1.5';
27
-
28
- const badge = (text, tone, key) => el('span', { style: BADGE + (tone ? TONE[tone] : ''), 'data-board-badge': key }, String(text));
29
-
30
- /** The badges row. It is read off the list this module just drew, so the badges and the notes always come from the same read
31
- * of GET /api/memory and agree after every action. Before that read succeeds the list states its own status, so no badge is
32
- * drawn. Nothing is shown that the read does not hold. */
33
- function badges(list) {
34
- if (!list.querySelector('[data-r9="usage-memory"]')) return null;
35
- const wire = list.querySelector('[data-r9="wire"]');
36
- const appr = list.querySelector('[data-r9="approval"]');
37
- const wired = !!wire && /wired \u2713/.test(wire.textContent || '');
38
- const n = list.querySelectorAll('[data-r9="entry"]').length;
39
- const pending = list.querySelectorAll('[data-r9="pending"]').length;
40
- const approval = !!appr && /approval on/.test(appr.textContent || '');
41
- return el('div', { 'data-board-part': 'badges', style: 'display:flex;gap:6px;flex-wrap:wrap' },
42
- wired ? badge('wired into CLAUDE.md', 's', 'wired') : badge('not wired into CLAUDE.md', 'w', 'wired'),
43
- badge(n + (n === 1 ? ' note' : ' notes'), null, 'count'),
44
- badge('memory.jsonl \u00b7 append-only', null, 'ledger'),
45
- pending ? badge(pending + ' waiting for you', 'w', 'pending') : null,
46
- badge('agent notes ' + (approval ? 'wait for your approval' : 'apply at once'), null, 'approval'));
47
- }
48
-
49
- /** Restyle the existing list in place: its bar goes into the header, its rows become cards. Nothing is removed. */
50
- function restyle(list, actions) {
51
- const bar = list.querySelector('[data-r9="memory-bar"]');
52
- // the list's wrapper, when it drew one (a failed or pending read returns its parts unwrapped)
53
- const first = list.firstElementChild;
54
- const wrap = first && bar && first !== bar && first.contains(bar) ? first : null;
55
- if (bar) {
56
- bar.setAttribute('style', 'display:flex;gap:8px;flex-wrap:wrap;align-items:center;justify-content:flex-end');
57
- for (const b of bar.querySelectorAll('button')) {
58
- const k = b.getAttribute('data-r9');
59
- b.setAttribute('style', k === 'add' ? BTNP : BTN);
60
- }
61
- // the board's order: the secondary actions first, the primary action last
62
- const add = bar.querySelector('[data-r9="add"]');
63
- if (add) bar.appendChild(add);
64
- actions.appendChild(bar);
65
- }
66
- if (wrap) {
67
- wrap.setAttribute('style', 'display:flex;flex-direction:column;gap:10px');
68
- // the list's closing line has no hook; give it the board's note style
69
- for (const k of wrap.children) if (!k.hasAttribute('data-r9')) k.setAttribute('style', NOTE);
70
- }
71
- for (const row of list.querySelectorAll('[data-r9="entry"],[data-r9="elsewhere"],[data-r9="pending"]')) {
72
- row.setAttribute('style', ENTRY);
73
- row.setAttribute('data-board-card', row.getAttribute('data-r9'));
74
- const [text, meta, btns] = row.children;
75
- if (text) text.setAttribute('style', 'display:block;font-size:13px;line-height:1.5;color:var(--foreground)');
76
- if (meta) meta.setAttribute('style', 'display:block;font:11px var(--mono);color:var(--muted-foreground)');
77
- if (btns) {
78
- btns.setAttribute('style', 'display:flex;gap:6px;flex-wrap:wrap');
79
- for (const b of btns.querySelectorAll('button')) b.setAttribute('style', SMALL);
80
- }
81
- }
82
- for (const lab of list.querySelectorAll('[data-r9^="usage-"],[data-r9$="-label"]')) {
83
- const tone = lab.getAttribute('data-r9') === 'blocked-label' ? ';color:var(--destructive)' : '';
84
- lab.setAttribute('style', LBL + tone);
85
- }
86
- for (const note of list.querySelectorAll('[data-r9="empty"],[data-r9="blocked"]')) note.setAttribute('style', NOTE);
87
- const memNote = list.querySelector('[data-r9="note"]');
88
- if (memNote) memNote.setAttribute('style', NOTE + ';color:var(--destructive)');
89
- }
90
-
91
- export function renderBoard(inner, ctx) {
92
- const { FIXTURE, state } = ctx;
93
- const repo = state.product || null;
94
-
95
- const actions = el('div', { 'data-board-part': 'toolbar', style: 'display:flex;gap:8px;flex-wrap:wrap;align-items:center' });
96
- const header = el('div', { 'data-board-part': 'header', style: 'display:flex;justify-content:space-between;align-items:flex-end;gap:12px;flex-wrap:wrap' },
97
- el('div', null,
98
- el('div', { 'data-board-title': '', style: 'font:600 22px var(--sans);line-height:1.45' }, 'Memory'),
99
- el('div', { 'data-board-subtitle': '', style: 'color:var(--muted-foreground)' },
100
- 'Curated notes agents read before they work in ' + (repo || 'this repository'))),
101
- actions);
102
-
103
- const list = el('div', { 'data-board-section': 'notes', style: 'display:flex;flex-direction:column;gap:10px;max-width:900px' });
104
- ctx.renderMode(list);
105
-
106
- let part = null; let foot = null;
107
- if (FIXTURE) part = el('div', { 'data-board-part': 'badges', style: NOTE }, 'The repository memory is not read in the fixture.');
108
- else {
109
- // read the badges and the approval setting before the bar moves into the header
110
- part = badges(list);
111
- const appr = list.querySelector('[data-r9="approval"]');
112
- if (part) {
113
- foot = el('div', { 'data-board-part': 'note', style: NOTE }, appr && /approval on/.test(appr.textContent || '')
114
- ? 'You add notes here, and agents propose them. A note an agent proposes waits here until you approve it.'
115
- : 'You add notes here, and agents add them too. Turn approval on to keep an agent\u2019s note pending until you approve it.');
116
- }
117
- restyle(list, actions);
118
- }
119
-
120
- inner.appendChild(el('div', { 'data-board': 'memory', style: 'display:flex;flex-direction:column;gap:14px;padding:4px 6px;font-size:13px;line-height:1.5;color:var(--foreground)' },
121
- header, part, list, foot));
122
- }