create-pmos 0.53.0 → 0.53.2

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 (75) hide show
  1. package/package.json +1 -1
  2. package/payload/KIT-VERSION +1 -1
  3. package/payload/planning/evals/eval-calibration.md +1 -1
  4. package/payload/planning/okrs/health-budgets.md +1 -1
  5. package/payload/state/discoveries.md +1 -1
  6. package/workspace/README.md +2 -0
  7. package/workspace/REMOTE.md +107 -65
  8. package/workspace/bin/pmos.js +4 -2
  9. package/workspace/first-paint-audit.js +6 -2
  10. package/workspace/fixtures/initiatives-api/list.json +13 -0
  11. package/workspace/fixtures/initiatives-api/roadmap.json +13 -0
  12. package/workspace/fixtures/report-api/draft.json +105 -0
  13. package/workspace/fixtures/reports-page/draft-biweekly-2026-10-16.md +12 -0
  14. package/workspace/guide/command-palette.md +1 -1
  15. package/workspace/guide/index.md +3 -1
  16. package/workspace/guide/initiatives-roadmap.md +1 -0
  17. package/workspace/guide/initiatives-screen.md +1 -0
  18. package/workspace/guide/launcher.md +51 -0
  19. package/workspace/guide/layout.md +7 -2
  20. package/workspace/guide/proactive-engine.md +21 -5
  21. package/workspace/guide/report-slides.md +28 -0
  22. package/workspace/guide/reports.md +2 -0
  23. package/workspace/lib/auto-sync.js +83 -0
  24. package/workspace/lib/board-fold.js +2 -2
  25. package/workspace/lib/breakdown-export.js +1 -6
  26. package/workspace/lib/brief.js +20 -2
  27. package/workspace/lib/goals.js +165 -0
  28. package/workspace/lib/initiative-rows.js +7 -3
  29. package/workspace/lib/launcher-cli.js +207 -0
  30. package/workspace/lib/plan-tree.js +15 -2
  31. package/workspace/lib/pmos-verbs.js +3 -0
  32. package/workspace/lib/proactive-cli.js +58 -2
  33. package/workspace/lib/proactive.js +2 -2
  34. package/workspace/lib/qr.js +293 -0
  35. package/workspace/lib/remote-access.js +184 -12
  36. package/workspace/lib/report-draft.js +34 -6
  37. package/workspace/lib/report-slides-data.js +141 -0
  38. package/workspace/lib/routes-initiatives.js +35 -2
  39. package/workspace/lib/routes-reports.js +3 -3
  40. package/workspace/lib/routes-timeline.js +1 -1
  41. package/workspace/lib/sync.js +61 -6
  42. package/workspace/package-lock.json +1 -1
  43. package/workspace/package.json +1 -1
  44. package/workspace/public/icons/apple-touch-icon.png +0 -0
  45. package/workspace/public/icons/icon-192.png +0 -0
  46. package/workspace/public/icons/icon-512.png +0 -0
  47. package/workspace/public/index.html +11 -0
  48. package/workspace/public/pm/app.js +55 -9
  49. package/workspace/public/pm/components.css +865 -1
  50. package/workspace/public/pm/dest-reports.js +8 -2
  51. package/workspace/public/pm/dest-today.js +8 -2
  52. package/workspace/public/pm/dock.js +73 -3
  53. package/workspace/public/pm/frame-copy.js +5 -0
  54. package/workspace/public/pm/initiative-list.js +18 -1
  55. package/workspace/public/pm/initiatives-copy.js +4 -0
  56. package/workspace/public/pm/panel-tabs.js +14 -12
  57. package/workspace/public/pm/print.css +11 -0
  58. package/workspace/public/pm/report-slides.js +438 -0
  59. package/workspace/public/pm/report-view.js +8 -2
  60. package/workspace/public/pm/reports-copy.js +3 -0
  61. package/workspace/public/pm/roadmap.js +23 -0
  62. package/workspace/public/pm/slides-copy.js +52 -0
  63. package/workspace/public/pm/today-copy.js +29 -0
  64. package/workspace/public/pm/today-goals.js +172 -0
  65. package/workspace/public/pm/tokens.css +13 -0
  66. package/workspace/public/pm/topbar.js +5 -2
  67. package/workspace/replaced-tests.json +36 -1
  68. package/workspace/server.js +165 -15
  69. package/workspace/skills/morning-brief.skill +15 -9
  70. package/workspace/skills/report-biweekly.skill +1 -0
  71. package/workspace/test-files.json +4 -0
  72. package/workspace/test-lists/run6.json +9 -0
  73. package/workspace/test-lists/workspace-launcher.json +5 -0
  74. package/workspace/test-lists/workspace-phone-app.json +5 -0
  75. package/workspace/test-lists/workspace-public-host.json +5 -0
@@ -23,7 +23,8 @@ import { pmGet, pmPost } from './http.js';
23
23
  import { watchSig } from './sig-watch.js';
24
24
  import { wsOf } from './data.js';
25
25
  import { COPY, cadenceName } from './reports-copy.js';
26
- import { renderReport, openIssueSheet, openNotesSheet, draftNow, printReport, copyMarkdown, clearPrint, ensurePrintCss } from './report-view.js';
26
+ import { openIssueSheet, openNotesSheet, draftNow, printReport, copyMarkdown, clearPrint, ensurePrintCss } from './report-view.js';
27
+ import { openSlides, slideCards } from './report-slides.js';
27
28
 
28
29
  const SHOWN = 4; // the reports listed before "Show older"
29
30
  const insts = new WeakMap(); // host -> instance
@@ -227,6 +228,10 @@ function viewerBlock(inst) {
227
228
  const bar = el('div', { class: 'rp-toolbar', role: 'toolbar', 'aria-label': COPY.reportName(r.cadence, r.slot) });
228
229
  bar.appendChild(el('h3', { class: 'rp-section__title rp-toolbar__title' }, COPY.reportName(r.cadence, r.slot)));
229
230
  const acts = el('div', { class: 'rp-toolbar__acts' });
231
+ // Present: the slides full screen (Run 6, R22.7); it reads the file and writes nothing
232
+ const present = button({ label: COPY.present, variant: 'outline', size: 'sm', icon: 'columns-2', attrs: { 'data-rp-act': 'present', 'data-rp-key': 'present' }, disabled: !doc,
233
+ onClick: () => doc && openSlides({ md: doc.markdown, label: COPY.reportName(r.cadence, r.slot), printable: !!r.issued, returnFocus: present }) });
234
+ acts.appendChild(present);
230
235
  if (r.issued) {
231
236
  acts.appendChild(button({ label: COPY.print, variant: 'outline', size: 'sm', attrs: { 'data-rp-act': 'print', 'data-rp-key': 'print' }, disabled: !doc, onClick: () => doc && printReport(doc.markdown) }));
232
237
  acts.appendChild(button({ label: COPY.copyMd, variant: 'outline', size: 'sm', attrs: { 'data-rp-act': 'copy', 'data-rp-key': 'copy' }, disabled: !doc, onClick: () => doc && copyMarkdown(doc.markdown) }));
@@ -246,7 +251,8 @@ function viewerBlock(inst) {
246
251
  bar.appendChild(acts);
247
252
  sec.appendChild(bar);
248
253
  if (r.issued) sec.appendChild(el('p', { class: 'rp-viewer__state' }, r.edited ? COPY.editedWhy : COPY.frozen));
249
- sec.appendChild(doc ? renderReport(doc.markdown) : el('p', { class: 'rp-section__empty' }, COPY.loading));
254
+ // the report is read as its slides (Run 6): the PM-OS deck, scaled to the column
255
+ sec.appendChild(doc ? slideCards(doc.markdown) : el('p', { class: 'rp-section__empty' }, COPY.loading));
250
256
  return sec;
251
257
  }
252
258
 
@@ -21,6 +21,7 @@ import { wsOf } from './data.js';
21
21
  import { state, setState, FIXTURE } from './state.js';
22
22
  import { COPY } from './today-copy.js';
23
23
  import { todayCard, acceptCard, rankNodes, scopeCtx, ensureFeed, feedOf, feedErrorOf, bannerOf, onFeed } from './today-cards.js';
24
+ import { goalsSection, goalsSig } from './today-goals.js';
24
25
 
25
26
  const SHOWN = 3; // cards drawn before "Show more": the first paint holds at most 17 controls (first-paint-today.test.js)
26
27
  let expanded = false;
@@ -117,7 +118,10 @@ function build(ctx0) {
117
118
  ? button({ label: expanded ? COPY.fewer : COPY.more(nodes.length - SHOWN), variant: 'ghost', attrs: { 'data-today': 'more' }, onClick: () => { expanded = !expanded; setState({}); } })
118
119
  : null;
119
120
  nodes.forEach((node) => node.setAttribute('role', 'listitem'));
120
- return { children: [head, health, list, more], sig: JSON.stringify([sc.all, sc.repo, n, ready, expanded, banner]) + nodes.map((x) => x.outerHTML).join('') };
121
+ // today-goals (G6): the PM's goals sit above what is owed; they are joined to the cards of the first repository in scope
122
+ const goalsWs = sc.repos.length ? wsOf(sc.repos[0]) : null;
123
+ const goals = goalsSection({ ws: goalsWs, setState: ctx0.setState || setState });
124
+ return { children: [head, goals, health, list, more], sig: JSON.stringify([sc.all, sc.repo, n, ready, expanded, banner]) + goalsSig(goalsWs) + nodes.map((x) => x.outerHTML).join('') };
121
125
  }
122
126
 
123
127
  /** The key of the control that has the focus inside the board, so it can be focused again after a rebuild. */
@@ -128,6 +132,7 @@ function focusKey(root) {
128
132
  const attr = ['data-today-act', 'data-today-more', 'data-today-add-note', 'data-today-accept', 'data-today-return'].find((k) => a.hasAttribute(k));
129
133
  if (card && attr) return { id: card.getAttribute('data-today-id'), attr };
130
134
  if (a.closest('[data-today-note]')) return { id: card && card.getAttribute('data-today-id'), note: true };
135
+ if (a.getAttribute('data-goals') === 'input') return { goalsInput: true, caret: a.selectionStart };
131
136
  if (a.hasAttribute('data-today-scope')) return { scope: a.getAttribute('data-today-scope') };
132
137
  if (a.hasAttribute('data-today') && a.getAttribute('data-today') === 'more') return { more: true };
133
138
  return null;
@@ -138,9 +143,10 @@ function refocus(root, k) {
138
143
  let t = null;
139
144
  if (k.note && card) t = card.querySelector('[data-today-note] input, [data-today-note] textarea, input[data-today-note], textarea[data-today-note]');
140
145
  else if (k.attr && card) t = card.querySelector('[' + k.attr + ']');
146
+ else if (k.goalsInput) t = root.querySelector('[data-goals="input"]');
141
147
  else if (k.scope) t = root.querySelector('[data-today-scope="' + k.scope + '"]');
142
148
  else if (k.more) t = root.querySelector('[data-today="more"]');
143
- if (t && !t.hidden) { try { t.focus({ preventScroll: true }); } catch { /* not focusable */ } }
149
+ if (t && !t.hidden) { try { t.focus({ preventScroll: true }); if (k.goalsInput && typeof k.caret === 'number') t.setSelectionRange(k.caret, k.caret); } catch { /* not focusable */ } }
144
150
  }
145
151
 
146
152
  /** Draw Today into `host`. Safe to call on every render: nothing changes unless what Today shows changed. */
@@ -6,8 +6,11 @@
6
6
  * the dock is a rail waits in the dock until the PM opens it.
7
7
  *
8
8
  * This module holds the parts of the dock that are new in the frame:
9
- * createDockRail({ open, label }) the 44px rail: one control that opens the dock
9
+ * createDockRail({ open, resize }) the 44px rail: one control that opens the dock. While the dock is open the rail
10
+ * is a thin grip on the dock's left edge that resizes it (0.53.1)
10
11
  * foldButton({ fold }) the control in the open dock's strip that folds it back to the rail
12
+ * focusButton({ toggle }) / paintFocus(b, on)
13
+ * Focus: the dock takes the whole window, and Exit focus gives the page back (0.53.1)
11
14
  * sessionMenuButton({ newSession, withOptions, openSkill, ws })
12
15
  * "+ Session": New session, Session with options, then the skills to start
13
16
  * from (R10: Skills start from the dock's +session menu; the Skills view goes)
@@ -47,9 +50,76 @@ export function setSessionDefaults(patch) {
47
50
 
48
51
  /* ── the rail and the fold control ────────────────────────────────────────────────────────────────────────────────── */
49
52
 
50
- export function createDockRail({ open } = {}) {
53
+ export function createDockRail({ open, resize } = {}) {
51
54
  const b = iconButton({ icon: 'bot', ariaLabel: COPY.dock.open, onClick: () => open && open(), attrs: { 'data-fr': 'dock-open', 'aria-keyshortcuts': 'Meta+J Control+J' } });
52
- return el('div', { class: 'fr-dock-rail', 'data-fr': 'dock-rail' }, b);
55
+ return el('div', { class: 'fr-dock-rail', 'data-fr': 'dock-rail' }, b, resize ? dockGrip(resize) : null);
56
+ }
57
+
58
+ /* ── the dock's width (0.53.1: the PM found the terminal too small) ───────────────────────────────────────────────────
59
+ * The grip sits in the rail's place while the dock is open, so the body keeps its four children and the dock stays the
60
+ * fourth. `resize` = { get() → px | null, set(px | null) }: null is the default width (the CSS clamp). The grip measures
61
+ * from the body's right edge, so the dock is as wide as the distance from the pointer to the window's edge. */
62
+ export const DOCK_MIN = 320;
63
+ export const DOCK_STEP = 32;
64
+ /** The widest the dock may be in a body `bodyW` wide: the page keeps 280px beside it. */
65
+ export const dockMax = (bodyW) => Math.max(DOCK_MIN, Math.round(bodyW) - 280);
66
+ export const clampDock = (px, bodyW) => Math.min(dockMax(bodyW), Math.max(DOCK_MIN, Math.round(px)));
67
+
68
+ function dockGrip(resize) {
69
+ const g = el('div', {
70
+ class: 'fr-dock-grip', 'data-fr': 'dock-grip', role: 'separator', 'aria-orientation': 'vertical',
71
+ 'aria-label': COPY.dock.resize, title: COPY.dock.resize, tabindex: '0',
72
+ });
73
+ grips.set(g, resize);
74
+ g.addEventListener('dblclick', () => resize.set(null));
75
+ g.addEventListener('keydown', (e) => {
76
+ if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') return;
77
+ e.preventDefault();
78
+ const b = g.closest('.fr-body');
79
+ const d = b && b.querySelector(':scope > .fr-dock');
80
+ const now = resize.get() || (d ? d.getBoundingClientRect().width : 0);
81
+ resize.set(clampDock(now + (e.key === 'ArrowLeft' ? DOCK_STEP : -DOCK_STEP), b ? b.getBoundingClientRect().width : window.innerWidth));
82
+ });
83
+ return g;
84
+ }
85
+
86
+ /* The drag is read on the document, once: a press on a grip starts it, and the moves and the release are the window's,
87
+ * so a drag that leaves the grip (it is 6px wide) keeps going. The grip itself is a separator with keys (above). */
88
+ const grips = new WeakMap(); // grip -> resize
89
+ let drag = null; // { g, resize, right }
90
+ document.addEventListener('pointerdown', (e) => {
91
+ const g = e.button === 0 && e.target && e.target.closest ? e.target.closest('[data-fr="dock-grip"]') : null;
92
+ const resize = g && grips.get(g);
93
+ const b = g && g.closest('.fr-body');
94
+ if (!resize || !b) return;
95
+ e.preventDefault();
96
+ drag = { g, resize, body: b, right: b.getBoundingClientRect().right };
97
+ g.setAttribute('data-dragging', '');
98
+ });
99
+ window.addEventListener('pointermove', (e) => {
100
+ if (drag) drag.resize.set(clampDock(drag.right - e.clientX, drag.body.getBoundingClientRect().width));
101
+ });
102
+ const endDrag = () => { if (drag) { drag.g.removeAttribute('data-dragging'); drag = null; } };
103
+ window.addEventListener('pointerup', endDrag);
104
+ window.addEventListener('pointercancel', endDrag);
105
+
106
+ const focusLabels = new WeakMap(); // the Focus control -> its label node
107
+ export function focusButton({ toggle } = {}) {
108
+ const label = el('span', null, COPY.dock.focus);
109
+ const b = button({ label, variant: 'ghost', size: 'sm',
110
+ attrs: { class: 'fr-dock-focus', 'data-fr': 'dock-focus', 'aria-pressed': 'false', title: COPY.dock.focusTitle, 'aria-keyshortcuts': 'Meta+Shift+J Control+Shift+J' },
111
+ onClick: () => toggle && toggle() });
112
+ focusLabels.set(b, label);
113
+ return b;
114
+ }
115
+ /** Show the Focus control's state: pressed while the dock has the whole window. */
116
+ export function paintFocus(b, on) {
117
+ if (!b) return;
118
+ const label = focusLabels.get(b);
119
+ const text = on ? COPY.dock.unfocus : COPY.dock.focus;
120
+ if (label && label.textContent !== text) label.textContent = text;
121
+ b.setAttribute('aria-pressed', on ? 'true' : 'false');
122
+ b.title = on ? COPY.dock.unfocusTitle : COPY.dock.focusTitle;
53
123
  }
54
124
 
55
125
  export function foldButton({ fold } = {}) {
@@ -36,6 +36,11 @@ export const COPY = {
36
36
  dock: {
37
37
  open: 'Open the agent dock (⌘J)',
38
38
  fold: 'Fold the agent dock (⌘J)',
39
+ focus: 'Focus',
40
+ unfocus: 'Exit focus',
41
+ focusTitle: 'Give the dock the whole window (⌘⇧J)',
42
+ unfocusTitle: 'Show the page beside the dock again (⌘⇧J)',
43
+ resize: 'Resize the agent dock. Drag, or use the left and right arrow keys. Double-click resets the width.',
39
44
  session: 'Session',
40
45
  sessionMenu: 'Start a session',
41
46
  newSession: 'New session',
@@ -23,6 +23,7 @@ import { watchSig } from './sig-watch.js';
23
23
  import { COPY, shortDate } from './initiatives-copy.js';
24
24
  import { getList, sigKey, place, placed, leaveNow, setLifecycleBulk } from './initiatives-api.js';
25
25
  import { healthSignal, say, confirmSheet, checkBySheet, onContextMenu } from './initiatives-ui.js';
26
+ import { openDoc } from './panel-initiatives.js';
26
27
 
27
28
  const ORDER = { now: 0, next: 1, later: 2 };
28
29
  const MAX_BULK = 20;
@@ -143,6 +144,20 @@ export function mountList(host, env) {
143
144
  el('h3', { class: 'in-group__head' }, head), g.anchors.map(anchorEl));
144
145
  }
145
146
 
147
+ // The units with documents and no record (routes-initiatives.js unrecordedOf), last: they have no anchor to group by.
148
+ function unrecordedEl(list) {
149
+ return el('section', { class: 'in-group', 'data-in-group': 'unrecorded' },
150
+ el('h3', { class: 'in-group__head' }, el('span', { class: 'in-group__name' }, COPY.noRecordGroup)),
151
+ el('div', { class: 'in-rows', role: 'list' }, list.map((u) => {
152
+ const doc = u.artifacts.find((a) => a.role === 'PRD') || u.artifacts[0];
153
+ return row({
154
+ title: u.title, meta: [COPY.noRecord],
155
+ onOpen: () => openDoc(null, doc.path, doc.role || 'DOC'), openLabel: COPY.aria.open(u.title),
156
+ attrs: { class: 'in-row', 'data-in-unrecorded-row': u.id }, openAttrs: { 'data-in-unrecorded-open': u.id },
157
+ });
158
+ })));
159
+ }
160
+
146
161
  function bulkBar() {
147
162
  const n = picked.size;
148
163
  const can = env.grants().includes('lifecycle-bulk');
@@ -167,12 +182,14 @@ export function mountList(host, env) {
167
182
  return;
168
183
  }
169
184
  const kids = [];
170
- if (!allRows().length) kids.push(empty({ text: COPY.emptyAll }));
185
+ const unrecorded = data.unrecorded || [];
186
+ if (!allRows().length && !unrecorded.length) kids.push(empty({ text: COPY.emptyAll }));
171
187
  else {
172
188
  kids.push(el('p', { class: 'in-note in-hint', 'data-in-hint': '' }, COPY.menuHintRow));
173
189
  kids.push(el('div', { class: 'in-toolbar' }, selecting ? bulkBar() : button({ label: COPY.select, variant: 'ghost', size: 'sm', onClick: () => { selecting = true; render(); }, attrs: { 'data-in-select-mode': '' } })));
174
190
  kids.push(...data.groups.map(groupEl));
175
191
  }
192
+ if (unrecorded.length) kids.push(unrecordedEl(unrecorded));
176
193
  root.replaceChildren(...kids);
177
194
  if (active) { const t = root.querySelector('[data-in-open="' + CSS.escape(active) + '"], [data-in-select="' + CSS.escape(active) + '"]'); if (t) t.focus({ preventScroll: true }); }
178
195
  }
@@ -41,6 +41,10 @@ export const COPY = {
41
41
  emptyColumn: { now: 'Nothing in Now.', next: 'Nothing in Next.', later: 'Nothing in Later.' },
42
42
  dormant: (n) => `${n} dormant`,
43
43
  hideDormant: (n) => `Hide ${n} dormant`,
44
+ unrecorded: (n) => `${n} with no record`,
45
+ hideUnrecorded: (n) => `Hide ${n} with no record`,
46
+ noRecord: 'no record yet',
47
+ noRecordGroup: 'No record yet',
44
48
  unconfirmed: 'unconfirmed',
45
49
  checkBy: (d) => `check-by ${d}`,
46
50
  waitingYou: 'waiting on you',
@@ -719,13 +719,11 @@ const bottomBar = el('div', { 'data-tabs': 'bottom', 'data-term-chrome': '', cla
719
719
  button({ label: copyLabel, variant: 'outline', size: 'sm', onClick: () => copyActive() }),
720
720
  );
721
721
 
722
- /* pm-os-absorb Run 3 (R11): the capture box is gone, so in real mode the bar has no scratchpad toggle and paintBottom
723
- * lays it out: the session's name and `copy`, or, on a phone, the terminal's compose line. Fixture mode keeps the
724
- * prototype's bar (the parity lock holds until slice 3D lifts it). */
725
- const [, bottomTitle, bottomCopy] = bottomBar.children;
726
-
727
- /* The drawer had its toggle in the bar and has none now: a stored "open" is dropped once, at load, so no capture box comes back. */
728
- if (!FIXTURE && state.scratchOpen) setState({ scratchOpen: false });
722
+ /* pm-os-absorb Run 3 (R11) took the scratchpad toggle out of the bar, and with it the only way to the pad on a desktop.
723
+ * 0.53.1 puts it back: paintBottom lays the bar out as the scratchpad toggle, the session's name and `copy`, or, on a
724
+ * phone, the terminal's compose line (the same per-session pad). Fixture mode keeps the prototype's bar. */
725
+ const [bottomToggle, bottomTitle, bottomCopy] = bottomBar.children;
726
+ bottomToggle.setAttribute('data-scratch', 'toggle');
729
727
 
730
728
  /* The compose line: one message field and Send on the SAME per-session pad as the drawer (padKey), so the text is kept
731
729
  * on this machine and never crosses sessions. On a desktop ⌘K "Ask <session>" types to a session (pm/palette.js). */
@@ -751,7 +749,7 @@ function sendCompose() {
751
749
  onScratchInput('');
752
750
  }
753
751
 
754
- /** The bar in real mode: no scratchpad toggle; a phone's terminal gets the compose line in its place. */
752
+ /** The bar in real mode: the scratchpad toggle, the title and copy; a phone's terminal gets the compose line instead. */
755
753
  function paintBottom(act) {
756
754
  const compose = isMobile() && !!act && act.kind === 'term';
757
755
  bottomTitle.textContent = act ? act.title : '';
@@ -761,7 +759,7 @@ function paintBottom(act) {
761
759
  if (composeInput.control.value !== text) composeInput.control.value = text;
762
760
  composeSend.disabled = !text.trim();
763
761
  }
764
- setChildren(bottomBar, compose ? [composeLine] : (act ? [bottomTitle, bottomCopy] : [bottomTitle]));
762
+ setChildren(bottomBar, compose ? [composeLine] : (act ? [bottomToggle, bottomTitle, bottomCopy] : [bottomToggle, bottomTitle]));
765
763
  }
766
764
 
767
765
  /* ── clipboard + scratchpad actions (lines 2098–2118) ───────────────────────────────────────── */
@@ -1101,9 +1099,12 @@ function frameDock() {
1101
1099
  openSkill: (sk) => openTab({ id: 'skill:' + sk.id, kind: 'skill', title: sk.label || sk.skill || sk.id, skillId: sk.id, repo: state.product || null, slug: null, role: 'SKILL' }),
1102
1100
  }),
1103
1101
  fold: dockParts ? dockParts.fold : dockMod.foldButton({ fold: () => window.pmApp.frame.foldDock() }),
1102
+ focus: dockParts ? dockParts.focus : dockMod.focusButton({ toggle: () => window.pmApp.frame.toggleFocus() }),
1104
1103
  };
1105
1104
  }
1106
- return { session: dockParts.session, fold: isMobile() ? null : dockParts.fold };
1105
+ if (isMobile()) return { session: dockParts.session, fold: null, focus: null };
1106
+ dockMod.paintFocus(dockParts.focus, !!window.pmApp.frame.dockMax);
1107
+ return { session: dockParts.session, fold: dockParts.fold, focus: dockParts.focus };
1107
1108
  }
1108
1109
 
1109
1110
  /**
@@ -1290,6 +1291,7 @@ export function renderTabs(host) {
1290
1291
  newSessionOptsBtn,
1291
1292
  canSplit ? splitBtn : null,
1292
1293
  orientBtn,
1294
+ fr ? fr.focus : null,
1293
1295
  fr ? fr.fold : null),
1294
1296
  ]);
1295
1297
  } else if (ck) {
@@ -1304,7 +1306,7 @@ export function renderTabs(host) {
1304
1306
  ].filter(Boolean);
1305
1307
  extras[0].style.marginLeft = 'auto';
1306
1308
  // The switch sits after `+ session`, so the strip still ends the mockup's way in the fixture, which does not draw it.
1307
- setChildren(tabBar, [...visTabs.map((t) => cockpitTabNode(t, act, splitTab)), ...extras, fr ? fr.session : newSessionBtn, FIXTURE ? null : orientBtn, fr ? fr.fold : null]);
1309
+ setChildren(tabBar, [...visTabs.map((t) => cockpitTabNode(t, act, splitTab)), ...extras, fr ? fr.session : newSessionBtn, FIXTURE ? null : orientBtn, fr ? fr.focus : null, fr ? fr.fold : null]);
1308
1310
  } else {
1309
1311
  tabBar.setAttribute('style', BAR_STYLE);
1310
1312
  newSessionBtn.setAttribute('style', CHIP_STYLE);
@@ -1363,7 +1365,7 @@ export function renderTabs(host) {
1363
1365
  newSessionBlock(),
1364
1366
  visTabs.length === 0 && !state.startPrompt && !state.newSess ? emptyState : null,
1365
1367
  panesRow,
1366
- state.scratchOpen ? scratchBlock : null,
1368
+ state.scratchOpen && (FIXTURE || !isMobile()) ? scratchBlock : null,
1367
1369
  bottomBar,
1368
1370
  ]);
1369
1371
  }
@@ -5,6 +5,11 @@
5
5
  @page {
6
6
  margin: 16mm;
7
7
  }
8
+ /* the slides (Run 6): each slide is one 1280 × 720 px page (13.333in × 7.5in), 16:9, with no margin */
9
+ @page slides {
10
+ size: 13.3333in 7.5in;
11
+ margin: 0;
12
+ }
8
13
  @media print {
9
14
  body > *:not(.rp-print) {
10
15
  display: none !important;
@@ -30,4 +35,10 @@
30
35
  .rp-print .rp-doc__note {
31
36
  break-inside: avoid;
32
37
  }
38
+ .rp-print--slides .rp-slide {
39
+ page: slides;
40
+ break-after: page;
41
+ print-color-adjust: exact;
42
+ -webkit-print-color-adjust: exact;
43
+ }
33
44
  }