create-pmos 0.52.0 → 0.53.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 (229) 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/README.md +2 -0
  16. package/workspace/REMOTE.md +24 -63
  17. package/workspace/bin/pmos.js +19 -3
  18. package/workspace/cut-stub.js +40 -0
  19. package/workspace/first-paint-audit.js +145 -0
  20. package/workspace/fixtures/capture/requirements/capture-2026-10-04-1.md +10 -0
  21. package/workspace/fixtures/initiatives-api/list.json +176 -0
  22. package/workspace/fixtures/initiatives-api/post-403.json +5 -0
  23. package/workspace/fixtures/initiatives-api/post-ok.json +10 -0
  24. package/workspace/fixtures/initiatives-api/roadmap.json +208 -0
  25. package/workspace/fixtures/initiatives-api/sheet.json +112 -0
  26. package/workspace/fixtures/report-api/draft.json +177 -0
  27. package/workspace/fixtures/reports-page/draft-biweekly-2026-10-16.md +97 -0
  28. package/workspace/fixtures/today-api/provider-rows.json +204 -0
  29. package/workspace/fixtures/today-api/seam-probe-kinds.json +5 -0
  30. package/workspace/guide/board.md +81 -0
  31. package/workspace/guide/breakdown.md +154 -0
  32. package/workspace/guide/command-palette.md +75 -0
  33. package/workspace/guide/index.md +28 -17
  34. package/workspace/guide/initiatives-roadmap.md +130 -0
  35. package/workspace/guide/initiatives-screen.md +89 -0
  36. package/workspace/guide/initiatives-timeline.md +57 -0
  37. package/workspace/guide/integrated-work.md +94 -0
  38. package/workspace/guide/knowledge-graph.md +3 -68
  39. package/workspace/guide/launcher.md +51 -0
  40. package/workspace/guide/layout.md +102 -32
  41. package/workspace/guide/live-notes.md +3 -3
  42. package/workspace/guide/pm-only-acts.md +89 -0
  43. package/workspace/guide/pm-only-lines.md +61 -0
  44. package/workspace/guide/proactive-engine.md +21 -5
  45. package/workspace/guide/reports-page.md +80 -0
  46. package/workspace/guide/reports.md +73 -0
  47. package/workspace/guide/timeline.md +70 -0
  48. package/workspace/guide/today.md +85 -0
  49. package/workspace/guide/validate-with-typesafe.md +1 -1
  50. package/workspace/lib/actor.js +91 -0
  51. package/workspace/lib/board-cache.js +32 -2
  52. package/workspace/lib/board-cli.js +108 -0
  53. package/workspace/lib/board-fold.js +232 -0
  54. package/workspace/lib/breakdown-export.js +75 -0
  55. package/workspace/lib/brief.js +20 -2
  56. package/workspace/lib/capture.js +98 -0
  57. package/workspace/lib/engine/plan-items-stage.js +271 -0
  58. package/workspace/lib/engine/workflow-state.js +28 -2
  59. package/workspace/lib/factory-join.js +158 -0
  60. package/workspace/lib/goals.js +165 -0
  61. package/workspace/lib/grant-mint.js +113 -0
  62. package/workspace/lib/health-rules-factory.js +92 -0
  63. package/workspace/lib/health-rules-plan.js +164 -0
  64. package/workspace/lib/health.js +79 -0
  65. package/workspace/lib/initiative-cli.js +222 -0
  66. package/workspace/lib/initiative-display.js +183 -0
  67. package/workspace/lib/initiative-needs.js +127 -0
  68. package/workspace/lib/initiative-rows.js +210 -0
  69. package/workspace/lib/initiative-writer.js +141 -0
  70. package/workspace/lib/issues.js +206 -0
  71. package/workspace/lib/item-cli.js +195 -0
  72. package/workspace/lib/item-spans.js +137 -0
  73. package/workspace/lib/launcher-cli.js +207 -0
  74. package/workspace/lib/measure-cli.js +100 -0
  75. package/workspace/lib/measurements.js +189 -0
  76. package/workspace/lib/mobile-nav.js +57 -53
  77. package/workspace/lib/needs-providers.js +120 -0
  78. package/workspace/lib/placements.js +440 -0
  79. package/workspace/lib/plan-items.js +922 -0
  80. package/workspace/lib/plan-ledgers.js +85 -0
  81. package/workspace/lib/plan-rules.js +72 -0
  82. package/workspace/lib/plan-tree.js +380 -0
  83. package/workspace/lib/pm-guard.js +174 -0
  84. package/workspace/lib/pm-lines-needs.js +331 -0
  85. package/workspace/lib/pmos-verbs.js +69 -0
  86. package/workspace/lib/proactive-cli.js +58 -2
  87. package/workspace/lib/proactive.js +9 -6
  88. package/workspace/lib/qr.js +293 -0
  89. package/workspace/lib/remote-access.js +93 -1
  90. package/workspace/lib/report-cli.js +152 -0
  91. package/workspace/lib/report-draft.js +578 -0
  92. package/workspace/lib/report-needs.js +213 -0
  93. package/workspace/lib/route-modules.js +128 -0
  94. package/workspace/lib/routes-board.js +91 -0
  95. package/workspace/lib/routes-capture.js +52 -0
  96. package/workspace/lib/routes-initiatives.js +371 -0
  97. package/workspace/lib/routes-reports.js +163 -0
  98. package/workspace/lib/routes-timeline.js +197 -0
  99. package/workspace/lib/routes-today.js +32 -0
  100. package/workspace/lib/today-cli.js +99 -0
  101. package/workspace/lib/today.js +333 -0
  102. package/workspace/lib/triage.js +163 -0
  103. package/workspace/lib/verify-work-cli.js +81 -0
  104. package/workspace/lib/work-check-needs.js +99 -0
  105. package/workspace/package-lock.json +1 -1
  106. package/workspace/package.json +4 -5
  107. package/workspace/public/icons/apple-touch-icon.png +0 -0
  108. package/workspace/public/icons/icon-192.png +0 -0
  109. package/workspace/public/icons/icon-512.png +0 -0
  110. package/workspace/public/index.html +40 -19
  111. package/workspace/public/pm/app.js +330 -30
  112. package/workspace/public/pm/bd-board.js +113 -0
  113. package/workspace/public/pm/bd-drag.js +59 -0
  114. package/workspace/public/pm/bd-model.js +152 -0
  115. package/workspace/public/pm/bd-timeline.js +233 -0
  116. package/workspace/public/pm/bd-tree.js +244 -0
  117. package/workspace/public/pm/board-copy.js +57 -0
  118. package/workspace/public/pm/breakdown-api.js +58 -0
  119. package/workspace/public/pm/breakdown-copy.js +138 -0
  120. package/workspace/public/pm/components.css +3135 -0
  121. package/workspace/public/pm/cycle-review.js +125 -0
  122. package/workspace/public/pm/data.js +40 -187
  123. package/workspace/public/pm/dest-board.js +438 -0
  124. package/workspace/public/pm/dest-initiatives.js +134 -0
  125. package/workspace/public/pm/dest-reports.js +270 -0
  126. package/workspace/public/pm/dest-settings.js +217 -0
  127. package/workspace/public/pm/dest-today.js +182 -0
  128. package/workspace/public/pm/dialogs.js +88 -0
  129. package/workspace/public/pm/display-word.js +56 -0
  130. package/workspace/public/pm/dock.js +175 -0
  131. package/workspace/public/pm/fixture.js +8 -227
  132. package/workspace/public/pm/frame-copy.js +99 -0
  133. package/workspace/public/pm/handoff-box.js +82 -0
  134. package/workspace/public/pm/http.js +88 -0
  135. package/workspace/public/pm/icons.js +53 -0
  136. package/workspace/public/pm/initiative-list.js +218 -0
  137. package/workspace/public/pm/initiative-sheet.js +261 -0
  138. package/workspace/public/pm/initiatives-api.js +59 -0
  139. package/workspace/public/pm/initiatives-copy.js +215 -0
  140. package/workspace/public/pm/initiatives-timeline-copy.js +59 -0
  141. package/workspace/public/pm/initiatives-timeline.js +410 -0
  142. package/workspace/public/pm/initiatives-ui.js +123 -0
  143. package/workspace/public/pm/item-sheet.js +251 -0
  144. package/workspace/public/pm/keep-card.js +204 -0
  145. package/workspace/public/pm/lifecycle.js +7 -1
  146. package/workspace/public/pm/mobile.css +49 -20
  147. package/workspace/public/pm/mobile.js +157 -29
  148. package/workspace/public/pm/modes-live.js +7 -883
  149. package/workspace/public/pm/modes.js +32 -1024
  150. package/workspace/public/pm/new-initiative-sheet.js +74 -0
  151. package/workspace/public/pm/next-step-ui.js +3 -2
  152. package/workspace/public/pm/palette-copy.js +51 -0
  153. package/workspace/public/pm/palette.js +312 -0
  154. package/workspace/public/pm/panel-breakdown.js +702 -0
  155. package/workspace/public/pm/panel-cockpit.js +63 -295
  156. package/workspace/public/pm/panel-detail.js +25 -172
  157. package/workspace/public/pm/panel-initiatives.js +1 -19
  158. package/workspace/public/pm/panel-repos.js +31 -81
  159. package/workspace/public/pm/panel-tabs.js +200 -111
  160. package/workspace/public/pm/panel-trees.js +5 -2
  161. package/workspace/public/pm/portfolio-report.js +61 -0
  162. package/workspace/public/pm/primitives.js +656 -0
  163. package/workspace/public/pm/print.css +33 -0
  164. package/workspace/public/pm/provider-cards.js +136 -0
  165. package/workspace/public/pm/registry.js +194 -0
  166. package/workspace/public/pm/release-card.js +110 -0
  167. package/workspace/public/pm/report-view.js +309 -0
  168. package/workspace/public/pm/reports-copy.js +114 -0
  169. package/workspace/public/pm/roadmap.js +271 -0
  170. package/workspace/public/pm/sig-watch.js +41 -0
  171. package/workspace/public/pm/state.js +8 -4
  172. package/workspace/public/pm/status-banner.js +13 -5
  173. package/workspace/public/pm/tab-doc.js +2 -4
  174. package/workspace/public/pm/tab-term.js +12 -2
  175. package/workspace/public/pm/tab-web.js +3 -2
  176. package/workspace/public/pm/timeline-copy.js +48 -0
  177. package/workspace/public/pm/today-cards.js +244 -0
  178. package/workspace/public/pm/today-copy.js +126 -0
  179. package/workspace/public/pm/today-goals.js +172 -0
  180. package/workspace/public/pm/tokens.css +16 -18
  181. package/workspace/public/pm/topbar.js +102 -0
  182. package/workspace/public/pm/triage-card.js +60 -0
  183. package/workspace/public/pm/undo-hold.js +103 -0
  184. package/workspace/public/pm/view-addrepo.js +2 -1
  185. package/workspace/public/pm/view-livenotes.js +3 -1
  186. package/workspace/public/pm/view-today.js +7 -162
  187. package/workspace/public/vendor/Sortable.min.js +2 -0
  188. package/workspace/public/vendor/lucide-sprite.js +79 -0
  189. package/workspace/replaced-tests.json +2877 -0
  190. package/workspace/run-tests.js +56 -5
  191. package/workspace/server.js +446 -40
  192. package/workspace/sheet-answer.js +61 -0
  193. package/workspace/skills/measure.skill +41 -0
  194. package/workspace/skills/morning-brief.skill +15 -9
  195. package/workspace/skills/report-biweekly.skill +23 -25
  196. package/workspace/skills/tracker-refresh.skill +50 -0
  197. package/workspace/test-files.json +21 -3
  198. package/workspace/test-lists/base.json +6 -0
  199. package/workspace/test-lists/run2-a.json +6 -0
  200. package/workspace/test-lists/run2-b.json +13 -0
  201. package/workspace/test-lists/run2-c.json +19 -0
  202. package/workspace/test-lists/run2-d.json +22 -0
  203. package/workspace/test-lists/run3-0.json +5 -0
  204. package/workspace/test-lists/run3-a.json +5 -0
  205. package/workspace/test-lists/run3-b.json +6 -0
  206. package/workspace/test-lists/run3-c.json +15 -0
  207. package/workspace/test-lists/run3-d.json +6 -0
  208. package/workspace/test-lists/run4-a.json +8 -0
  209. package/workspace/test-lists/run4-b.json +6 -0
  210. package/workspace/test-lists/run4-c.json +13 -0
  211. package/workspace/test-lists/run4-d.json +9 -0
  212. package/workspace/test-lists/run5-a.json +6 -0
  213. package/workspace/test-lists/run5-b.json +6 -0
  214. package/workspace/test-lists/run5-c.json +15 -0
  215. package/workspace/test-lists/workspace-launcher.json +5 -0
  216. package/workspace/test-lists/workspace-phone-app.json +5 -0
  217. package/workspace/test-run.js +27 -1
  218. package/workspace/third-party/README.md +44 -0
  219. package/workspace/third-party/lucide-static-1.52.0.LICENSE +43 -0
  220. package/workspace/third-party/sortablejs-1.15.7.LICENSE +21 -0
  221. package/workspace/design-parity.js +0 -673
  222. package/workspace/public/pm/tab-task.js +0 -86
  223. package/workspace/public/pm/view-guide.js +0 -141
  224. package/workspace/public/pm/view-knowledge.js +0 -225
  225. package/workspace/public/pm/view-memory.js +0 -122
  226. package/workspace/public/pm/view-portfolio.js +0 -153
  227. package/workspace/public/pm/view-reports.js +0 -190
  228. package/workspace/public/pm/view-scheduled.js +0 -161
  229. package/workspace/public/pm/view-skills.js +0 -216
@@ -1,8 +1,11 @@
1
1
  /* pm/mobile.js — the phone surface of the four-panel workspace (≤760px).
2
2
  *
3
- * Owns three things and nothing else: the top bar (back · the screen's name · the ⌘K jump palette ·
4
- * theme), the five-destination bottom nav, and the terminal control-key accessory bar that writes
5
- * real PTY sequences. Its markup ships in public/index.html; its styling in pm/mobile.css.
3
+ * Owns three things and nothing else: the top bar (back · Repository · the screen's name · the ⌘K
4
+ * palette · Settings; the old shell shows theme in place of Repository and Settings), the five-tab
5
+ * bottom nav (Today · Initiatives · Board · Reports · Agent, pm-os-absorb Run 3, slice 3A), and the
6
+ * terminal control-key accessory bar that writes real PTY sequences. Its markup ships in
7
+ * public/index.html; its styling in pm/mobile.css. In the frame, a tab is the frame's place (Agent is
8
+ * the dock) and Settings is pushed over the current tab.
6
9
  *
7
10
  * ── ONE renderer, two shells ──────────────────────────────────────────────────────────────────
8
11
  * Nothing here draws a phone-shaped copy of a panel. The four panel modules render exactly what
@@ -33,6 +36,9 @@
33
36
  */
34
37
 
35
38
  import { state, setState, writeCount, FIXTURE } from './state.js';
39
+ import { COPY as FC } from './frame-copy.js';
40
+ import { ensureComponentsCss } from './primitives.js';
41
+ import { ensureSprite, icon } from './icons.js';
36
42
 
37
43
  /* ── the breakpoint ─────────────────────────────────────────────────────────────────────────── */
38
44
 
@@ -50,19 +56,17 @@ if (!NAV) {
50
56
  }
51
57
 
52
58
  /** kind → which of the four panels draws it. The reducer owns the map; this is only its fallback. */
53
- const PANEL = { more: 0, list: 1, detail: 2, terminal: 3, doc: 3 };
59
+ const PANEL = { dest: 0, more: 0, list: 1, detail: 2, terminal: 3, doc: 3, agent: 3 };
54
60
  const panelOf = (kind) =>
55
61
  (NAV && typeof NAV.panelOf === 'function') ? NAV.panelOf(kind)
56
- : (Object.prototype.hasOwnProperty.call(PANEL, kind) ? PANEL[kind] : 1);
62
+ : (Object.prototype.hasOwnProperty.call(PANEL, kind) ? PANEL[kind] : 0);
57
63
 
58
- /* A destination's root list is a MODE. `more` has none: its root is the structure panel itself,
59
- * which is where the repositories, the workflows, the skills, the knowledge and the connections
60
- * are reached from — the phone's "more" and the desktop's panel 1 are the same surface. */
61
- const DEST_MODE = { board: 'product', scheduled: 'scheduled', agents: 'agent', memory: 'memory' };
62
- /* Two modes postdate the reducer's map (`connect` and `focus` arrived with the four-panel design).
63
- * They are named here rather than added to `DEST_FOR_MODE`, because MOBNAV-P7 pins that map to the
64
- * retired app.js's MODES exactly and a mode invented for a different surface would read as drift. */
65
- const EXTRA_DEST = { connect: 'more', focus: 'board', portfolio: 'more', guide: 'more' };
64
+ /* The old shell (fixture mode, ?layout=legacy) on a phone: a tab's root is the list panel in the mode it shows. Agent
65
+ * has no mode: it is the tabs panel. In the frame a tab is the frame's place and no mode is set. */
66
+ const DEST_MODE = { today: 'product', initiatives: 'product', board: 'product', reports: 'product' };
67
+ /* Modes that postdate the reducer's map. They are named here rather than added to `DEST_FOR_MODE`, because MOBNAV-P2
68
+ * iterates that map and a mode invented for a different surface would read as drift. */
69
+ const EXTRA_DEST = {}; // no mode outside the nav's own table reaches a phone destination since the R29 cuts
66
70
 
67
71
  const destForMode = (m) => (NAV && NAV.destForMode(m)) || EXTRA_DEST[m] || null;
68
72
 
@@ -70,9 +74,11 @@ const destForMode = (m) => (NAV && NAV.destForMode(m)) || EXTRA_DEST[m] || null;
70
74
 
71
75
  const $ = (id) => document.getElementById(id);
72
76
 
73
- const DEST_STORE_KEY = 'pmos.workspace.pm.mdest';
77
+ const DEST_STORE_KEY = 'pmos.workspace.pm.mtab';
78
+ /* The retired five-tab nav's key: read once, through the reducer's migration, never written. */
79
+ const OLD_DEST_KEY = 'pmos.workspace.pm.mdest';
74
80
 
75
- let ctx = null; // { bodyGrid, render } — handed over by app.js at boot
81
+ let ctx = null; // { bodyGrid, render, frame } — handed over by app.js at boot (frame: pmApp.frame, or null)
76
82
  let nav = null; // the reducer's state
77
83
  let seen = null; // the app-state fingerprint the last screen was derived from
78
84
  let selfDriven = false; // the next state change is one this module caused — do not read it as navigation
@@ -86,6 +92,17 @@ function readDest() {
86
92
  if (FIXTURE) return null;
87
93
  try { return localStorage.getItem(DEST_STORE_KEY); } catch { return null; }
88
94
  }
95
+ /** The tab a phone opens on: the frame's place when it is a tab, else the stored tab, else the old key migrated. */
96
+ function firstDest() {
97
+ const f = ctx && ctx.frame;
98
+ if (f && NAV.ROOTS[f.dest]) return f.dest;
99
+ const d = readDest();
100
+ if (d && NAV.ROOTS[d]) return d;
101
+ if (FIXTURE) return NAV.migrateView(null);
102
+ let old = null;
103
+ try { old = localStorage.getItem(OLD_DEST_KEY); } catch { old = null; }
104
+ return NAV.migrateView(old);
105
+ }
89
106
  function writeDest(d) {
90
107
  if (FIXTURE) return;
91
108
  try { localStorage.setItem(DEST_STORE_KEY, d); } catch { /* quota / private mode */ }
@@ -199,6 +216,7 @@ function onCaptureKeyDown(e) {
199
216
 
200
217
  function fingerprint() {
201
218
  const d = state.detail || {};
219
+ const f = ctx && ctx.frame;
202
220
  return {
203
221
  mode: state.mode,
204
222
  sel: state.sel,
@@ -209,6 +227,9 @@ function fingerprint() {
209
227
  * screen even though the value does not move. The counter is state.js's write log; watching
210
228
  * the value alone is what left the terminal screen unreachable once you had navigated off it. */
211
229
  activeAsk: writeCount('active'),
230
+ // the frame: the place on screen and whether the detail is open (state.col2 is the frame's detail column)
231
+ fdest: f ? f.dest : null,
232
+ col2: !!state.col2,
212
233
  };
213
234
  }
214
235
 
@@ -219,6 +240,32 @@ function go(screen) {
219
240
  nav = NAV.push(base, screen);
220
241
  }
221
242
 
243
+ const frameOn = () => !!(ctx && ctx.frame);
244
+ const tabScreen = (tab, id) => ({ kind: (tab && (tab.kind === 'doc' || tab.kind === 'breakdown' || tab.kind === 'dest')) ? 'doc' : 'terminal', id, title: (tab && (tab.title || tab.name)) || String(id) });
245
+
246
+ /* The frame on a phone: the PM's own moves through the panels, the palette or Settings, read back as screens. A place
247
+ * opened from elsewhere (⌘K, Settings › Show its initiatives) is a tab switch; Settings is pushed on top; an open
248
+ * detail is pushed and a closed one popped; a session or document opened is pushed. */
249
+ function syncFromFrame(cur) {
250
+ const f = ctx.frame;
251
+ if (cur.active && (cur.active !== seen.active || cur.activeAsk !== seen.activeAsk)) {
252
+ go(tabScreen((state.tabs || []).find((t) => t && t.id === cur.active), cur.active));
253
+ } else if (cur.col2 && !seen.col2) {
254
+ const dk = (state.detail && state.detail.kind) || 'detail';
255
+ go({ kind: 'detail', dk, id: (state.detail && state.detail.id) || cur.sel });
256
+ } else if (!cur.col2 && seen.col2 && NAV.current(nav).kind === 'detail') {
257
+ nav = NAV.pop(nav);
258
+ } else if (cur.fdest !== seen.fdest) {
259
+ if (cur.fdest === 'settings') { if (NAV.current(nav).id !== 'settings') go({ kind: 'dest', id: 'settings' }); }
260
+ else if (NAV.ROOTS[cur.fdest]) { nav = NAV.switchDest(nav, cur.fdest); writeDest(nav.dest); }
261
+ }
262
+ if (f && NAV.current(nav).kind === 'dest' && NAV.current(nav).id !== f.dest) {
263
+ // the screen names a place the frame is not showing (a pop, say): put the frame on it
264
+ selfDriven = true;
265
+ f.go(NAV.current(nav).id);
266
+ }
267
+ }
268
+
222
269
  /**
223
270
  * Read the app's state for a navigation the PM made through a panel, and push the screen it means.
224
271
  * Exactly one transition is taken per render, most specific first: opening a session is a terminal
@@ -227,10 +274,10 @@ function go(screen) {
227
274
  function syncFromState() {
228
275
  const cur = fingerprint();
229
276
  if (!seen || selfDriven) { selfDriven = false; seen = cur; return; }
277
+ if (frameOn()) { syncFromFrame(cur); seen = cur; return; }
230
278
 
231
279
  if (cur.active && (cur.active !== seen.active || cur.activeAsk !== seen.activeAsk)) {
232
- const tab = (state.tabs || []).find((t) => t && t.id === cur.active);
233
- go({ kind: (tab && tab.kind === 'doc') ? 'doc' : 'terminal', id: cur.active, title: (tab && (tab.title || tab.name)) || String(cur.active) });
280
+ go(tabScreen((state.tabs || []).find((t) => t && t.id === cur.active), cur.active));
234
281
  } else if (cur.detail !== seen.detail || (cur.sel && cur.sel !== seen.sel)) {
235
282
  const dk = (state.detail && state.detail.kind) || '';
236
283
  if (dk && dk !== 'none') go({ kind: 'detail', dk, id: (state.detail && state.detail.id) || cur.sel });
@@ -249,6 +296,12 @@ function syncFromState() {
249
296
 
250
297
  /* ── applying it ────────────────────────────────────────────────────────────────────────────── */
251
298
 
299
+ /** The panel a screen shows. The frame's grid is the reducer's; the old shell draws a tab's root in its list panel. */
300
+ function panelFor(kind) {
301
+ if (!frameOn() && kind === 'dest') return 1;
302
+ return panelOf(kind);
303
+ }
304
+
252
305
  /**
253
306
  * Bring the chrome and the visible panel in line with the current screen. Called at the end of
254
307
  * every shell render, and on every breakpoint change.
@@ -268,15 +321,17 @@ export function applyMobile() {
268
321
  return;
269
322
  }
270
323
 
271
- if (!nav) nav = NAV.init(NAV.migrateView(readDest()));
324
+ if (!nav) nav = NAV.init(firstDest());
272
325
  syncFromState();
273
326
 
274
327
  const s = NAV.current(nav);
328
+ const f = ctx.frame;
275
329
 
276
330
  // The host is chosen FIRST, so a renderer that measures itself (the terminal's fit) sees the box
277
331
  // it is actually going to occupy rather than the one it is leaving.
278
- ctx.bodyGrid.dataset.mpanel = String(panelOf(s.kind));
279
- if (menu) menu.dataset.view = s.kind;
332
+ ctx.bodyGrid.dataset.mpanel = String(panelFor(s.kind));
333
+ const act = (state.tabs || []).find((t) => t && t.id === state.active);
334
+ if (menu) menu.dataset.view = s.kind === 'agent' ? (act && act.kind === 'term' ? 'terminal' : 'agent') : s.kind;
280
335
 
281
336
  const title = $('mobTitle');
282
337
  if (title) title.textContent = NAV.titleOf(s) || '';
@@ -285,15 +340,50 @@ export function applyMobile() {
285
340
  const back = $('mobBack');
286
341
  if (back) back.hidden = !deep; // absent at a destination root, never present-and-inert
287
342
  const brand = $('mobBrand');
288
- if (brand) brand.hidden = deep; // the lockup yields to the back control, as the design has it
343
+ if (brand) brand.hidden = deep || !!f; // the lockup yields to the back control, and to Repository in the frame
289
344
 
290
345
  const theme = $('mobTheme');
291
- if (theme) theme.textContent = state.theme === 'dark' ? '◐' : '◑';
346
+ if (theme) {
347
+ const want = state.theme === 'dark' ? 'moon' : 'sun';
348
+ if (theme.getAttribute('data-icon') !== want) { theme.setAttribute('data-icon', want); theme.replaceChildren(icon(want)); }
349
+ theme.hidden = !!f;
350
+ }
351
+ // the frame's own controls: Repository and Settings (the theme is in Settings)
352
+ const repo = $('mobRepo');
353
+ if (repo) {
354
+ repo.hidden = !f || deep;
355
+ if (f) {
356
+ const label = f.scope === 'all' ? FC.repo.all : (state.product || FC.repo.none);
357
+ if (repo.textContent !== label) repo.textContent = label;
358
+ repo.setAttribute('aria-label', FC.repo.open(label));
359
+ }
360
+ }
361
+ // Sync, as the desktop bar has it: only while the current repository is not in sync (never in All repositories)
362
+ const sync = $('mobSync');
363
+ if (sync) {
364
+ const want = !!f && !deep && f.scope !== 'all' && (f.unsynced || f.syncing);
365
+ sync.hidden = !want;
366
+ if (want) {
367
+ const label = f.syncing ? FC.sync.busy : FC.sync.label;
368
+ if (sync.textContent !== label) sync.textContent = label;
369
+ sync.disabled = !!f.syncing;
370
+ sync.title = FC.sync.title;
371
+ }
372
+ }
373
+ const set = $('mobSettings');
374
+ if (set) { set.hidden = !f; set.setAttribute('aria-current', f && f.dest === 'settings' ? 'page' : 'false'); }
375
+ const count = $('mobTodayCount');
376
+ if (count) {
377
+ const n = f ? Math.max(0, Number(f.needs) || 0) : 0;
378
+ count.hidden = n === 0;
379
+ count.textContent = String(n);
380
+ count.setAttribute('aria-label', FC.needs(n));
381
+ }
292
382
 
293
383
  const buttons = menu ? menu.querySelectorAll('#mobNav [data-dest]') : [];
294
384
  for (const b of buttons) b.setAttribute('aria-current', b.dataset.dest === nav.dest ? 'page' : 'false');
295
385
 
296
- if (s.kind !== 'terminal' && ctrlArmed) setCtrlArmed(false);
386
+ if (s.kind !== 'terminal' && s.kind !== 'agent' && ctrlArmed) setCtrlArmed(false);
297
387
  }
298
388
 
299
389
  /* ── the chrome's own controls ──────────────────────────────────────────────────────────────── */
@@ -302,16 +392,30 @@ export function applyMobile() {
302
392
  function setDest(dest) {
303
393
  nav = NAV.switchDest(nav, dest);
304
394
  writeDest(nav.dest);
305
- const mode = DEST_MODE[dest];
306
395
  selfDriven = true;
307
- setState(mode ? { mode, settingsOpen: false } : { settingsOpen: false });
396
+ const f = ctx && ctx.frame;
397
+ if (f) {
398
+ // a tab shows its place (Agent shows the dock over the current one); a detail left open does not follow
399
+ if (dest !== 'agent') f.go(dest);
400
+ setState({ col2: false });
401
+ return;
402
+ }
403
+ const mode = DEST_MODE[dest];
404
+ if (mode) setState({ mode });
308
405
  }
309
406
 
310
407
  /** Back: pop exactly one screen and put the app back in the state that screen describes. */
311
408
  function goBack() {
409
+ const was = NAV.current(nav);
312
410
  nav = NAV.pop(nav);
313
411
  const s = NAV.current(nav);
314
412
  selfDriven = true;
413
+ const f = ctx && ctx.frame;
414
+ if (f) {
415
+ if (s.kind === 'dest' && s.id !== f.dest) f.go(s.id);
416
+ setState(was.kind === 'detail' ? { col2: false } : {});
417
+ return;
418
+ }
315
419
  setState(s.kind === 'list' && s.mode ? { mode: s.mode } : {});
316
420
  }
317
421
 
@@ -325,10 +429,11 @@ function focusSearch() {
325
429
  /**
326
430
  * The jump palette. It is a jump control for EVERY destination, so it lives in the always-visible
327
431
  * top bar; the control bar below scrolls, and a control that scrolls out of view at 390px is the
328
- * hover-only trap in another costume. Search lives in panel 2, so the palette makes sure a list
329
- * screen is the one on screen before it hands focus over.
432
+ * hover-only trap in another costume. In the frame it opens ⌘K; in the old shell search lives in
433
+ * panel 2, so the palette makes sure a list screen is the one on screen before it hands focus over.
330
434
  */
331
435
  function openPalette() {
436
+ if (ctx && ctx.frame) { ctx.frame.openPalette(); return; }
332
437
  if (NAV.current(nav).kind !== 'list') go({ kind: 'list', mode: state.mode });
333
438
  selfDriven = true;
334
439
  setState({ col1: true }, focusSearch);
@@ -340,6 +445,25 @@ function toggleTheme() {
340
445
  setState((s) => ({ theme: s.theme === 'dark' ? 'light' : 'dark' }));
341
446
  }
342
447
 
448
+ /** The frame's Settings, pushed over the current tab so Back returns to it. */
449
+ function openSettings() {
450
+ const f = ctx && ctx.frame;
451
+ if (!f) return;
452
+ if (NAV.current(nav).id !== 'settings') go({ kind: 'dest', id: 'settings' });
453
+ selfDriven = true;
454
+ f.go('settings');
455
+ }
456
+
457
+ /* The frame's bar controls (Repository, Sync, Settings) are reached through one listener on the document: the bar is
458
+ * static markup. */
459
+ function onChromeClick(e) {
460
+ const t = e.target && e.target.closest ? e.target.closest('#mobRepo, #mobSync, #mobSettings') : null;
461
+ if (!t || !ctx || !ctx.frame) return;
462
+ if (t.id === 'mobSettings') openSettings();
463
+ else if (t.id === 'mobSync') ctx.frame.sync();
464
+ else ctx.frame.repoMenu(t);
465
+ }
466
+
343
467
  /* The terminal has to re-measure when the soft keyboard shows or hides and after a rotation, or
344
468
  * xterm keeps the row count it had and the bottom of the session is clipped. The fit itself belongs
345
469
  * to the terminal panel; this only tells it that the box changed. */
@@ -353,12 +477,15 @@ function refit() {
353
477
 
354
478
  /**
355
479
  * Wire the phone chrome up, once, at boot.
356
- * `context` is `{ bodyGrid, render }` — the body grid whose children are the four panel hosts, and
480
+ * `context` is `{ bodyGrid, render, frame }` — the body grid whose children are the hosts, the frame's surface
481
+ * (window.pmApp.frame; null in the old shell), and
357
482
  * the shell's render function, so a breakpoint change can repaint the panels the phone needs.
358
483
  */
359
484
  export function mountMobile(context) {
360
485
  ctx = context || null;
361
486
  if (!ctx || !ctx.bodyGrid || !NAV) return;
487
+ ensureComponentsCss(); // the phone chrome's icons are the lucide sprite's, in every shell
488
+ ensureSprite();
362
489
 
363
490
  for (const b of document.querySelectorAll('#ctrlBar [data-seq]')) {
364
491
  b.addEventListener('click', () => sendSeq(b.dataset.seq));
@@ -376,6 +503,7 @@ export function mountMobile(context) {
376
503
  if (pal) pal.addEventListener('click', openPalette);
377
504
  const theme = $('mobTheme');
378
505
  if (theme) theme.addEventListener('click', toggleTheme);
506
+ document.addEventListener('click', onChromeClick);
379
507
 
380
508
  if (window.visualViewport) {
381
509
  window.visualViewport.addEventListener('resize', refit);
@@ -387,7 +515,7 @@ export function mountMobile(context) {
387
515
  // phone shell exists at all AND repaint the panels — a shell left mounted above 760px is a second
388
516
  // navigation system on the desktop, and panels left unpainted below it are three empty screens.
389
517
  const onMQ = () => {
390
- if (isMobile()) nav = nav || NAV.init(NAV.migrateView(readDest()));
518
+ if (isMobile()) nav = nav || NAV.init(firstDest());
391
519
  if (ctx && typeof ctx.render === 'function') ctx.render();
392
520
  else applyMobile();
393
521
  refit();