create-pmos 0.14.0 → 0.16.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 (39) hide show
  1. package/README.md +57 -0
  2. package/package.json +1 -1
  3. package/payload/KIT-VERSION +2 -2
  4. package/workspace/README.md +44 -12
  5. package/workspace/design-parity.js +358 -0
  6. package/workspace/lib/console-state.js +48 -1
  7. package/workspace/lib/engine/workflow-state.js +13 -3
  8. package/workspace/lib/find-browser.js +60 -0
  9. package/workspace/lib/focus.js +967 -0
  10. package/workspace/lib/four-panel.js +345 -0
  11. package/workspace/lib/git-info.js +28 -1
  12. package/workspace/lib/handoff.js +280 -0
  13. package/workspace/lib/layout.js +68 -46
  14. package/workspace/lib/mobile-nav.js +157 -0
  15. package/workspace/lib/profiles.js +285 -0
  16. package/workspace/lib/repo-scan.js +469 -0
  17. package/workspace/package-lock.json +29 -2
  18. package/workspace/package.json +14 -4
  19. package/workspace/public/index.html +79 -279
  20. package/workspace/public/pm/app.js +242 -0
  21. package/workspace/public/pm/data.js +877 -0
  22. package/workspace/public/pm/dom.js +116 -0
  23. package/workspace/public/pm/fixture.js +979 -0
  24. package/workspace/public/pm/mobile.css +200 -0
  25. package/workspace/public/pm/mobile.js +403 -0
  26. package/workspace/public/pm/modes.js +1076 -0
  27. package/workspace/public/pm/panel-detail.js +648 -0
  28. package/workspace/public/pm/panel-initiatives.js +426 -0
  29. package/workspace/public/pm/panel-repos.js +460 -0
  30. package/workspace/public/pm/panel-tabs.js +487 -0
  31. package/workspace/public/pm/state.js +164 -0
  32. package/workspace/public/pm/tab-doc.js +279 -0
  33. package/workspace/public/pm/tab-task.js +83 -0
  34. package/workspace/public/pm/tab-term.js +560 -0
  35. package/workspace/public/pm/tokens.css +73 -0
  36. package/workspace/public/vendor/ibm-plex-mono-500.woff2 +0 -0
  37. package/workspace/server.js +1072 -31
  38. package/workspace/public/app.js +0 -3450
  39. package/workspace/public/styles.css +0 -1444
@@ -1,298 +1,98 @@
1
1
  <!doctype html>
2
- <html lang="en" data-theme="dark">
2
+ <html lang="en">
3
3
  <head>
4
4
  <meta charset="utf-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
6
- <title>PMOS Terminal Workspace</title>
6
+ <title>PMOS workspace</title>
7
7
  <!-- The terminal's face is preloaded, not left to be discovered. An @font-face is only fetched when
8
- something first RENDERS in it, which for Plex Mono is about the same moment app.js constructs
8
+ something first RENDERS in it, which for Plex Mono is about the same moment a tab constructs
9
9
  the first xterm — and xterm measures its cell from whatever face is resolved at construction.
10
10
  Starting the fetch here, during head parse, means the real metrics are in place before the
11
- first terminal exists. app.js also re-fits on document.fonts.ready as the backstop. -->
11
+ first terminal exists. -->
12
12
  <link rel="preload" href="/vendor/ibm-plex-mono-400.woff2" as="font" type="font/woff2" crossorigin />
13
13
  <link rel="preload" href="/vendor/ibm-plex-mono-700.woff2" as="font" type="font/woff2" crossorigin />
14
14
  <link rel="stylesheet" href="/vendor/xterm.css" />
15
- <link rel="stylesheet" href="/styles.css" />
15
+ <link rel="stylesheet" href="/pm/tokens.css" />
16
+ <!-- The phone surface's stylesheet. It is a separate file from tokens.css on purpose: tokens.css is
17
+ a verbatim copy of the design prototype's token block and is diffed mechanically against it, so
18
+ a media query added there would read as a defect. Everything below the ≤760px breakpoint lives
19
+ here instead, and above the breakpoint this file declares nothing but `display:none`. -->
20
+ <link rel="stylesheet" href="/pm/mobile.css" />
21
+ <!-- xterm and its addons come straight from node_modules through the local server, as globals, so
22
+ the terminal tab can attach a real PTY. No CDN anywhere: this app renders, and works, offline. -->
23
+ <script src="/vendor/xterm.js"></script>
24
+ <script src="/vendor/addon-fit.js"></script>
25
+ <script src="/vendor/addon-web-links.js"></script>
26
+ <!-- ONE table, TWO consumers (W7). The words and glyphs a session is described with — working,
27
+ quiet, needs you, blocked — live in exactly one file, which the SERVER requires and the browser
28
+ loads here. A second copy would be free to drift, and the sidebar and the server would end up
29
+ holding different opinions about one session. Loaded before the module graph so a panel can
30
+ bind from window.PMOSSessionState rather than re-typing a word. -->
31
+ <script src="/lib/session-state.js"></script>
32
+ <!-- The phone's navigation reducer. DOM-free, loaded as a classic script (it is a UMD that sets
33
+ window.PMOSMobileNav) and required by test.js, so the whole screen domain is walkable in node
34
+ rather than only in a browser. pm/mobile.js reads it from the global; the module graph does not
35
+ import it, because a second copy of the reducer is exactly the drift it was extracted to stop. -->
36
+ <script src="/lib/mobile-nav.js"></script>
37
+ <link rel="preload" href="/vendor/ibm-plex-mono-500.woff2" as="font" type="font/woff2" crossorigin />
16
38
  </head>
17
39
  <body>
18
- <div class="app">
19
-
20
- <header class="bar">
21
- <span class="mark">P</span><b>PMOS</b><span class="sub">workspace</span>
22
- <span class="conn" id="conn">connecting…</span>
23
- <span class="branch" id="branchChip" hidden title="current branch"></span>
24
- <div class="right">
25
- <button class="ibtn" id="scratchBtn" title="Scratchpad — stage text before the terminal (⌘J)" aria-pressed="false">scratch</button>
26
- <!-- The layout control: names the arrangement you are in, and opens the list. Adjusting a
27
- layout is kept against THAT layout automatically — there is no save, no dirty state and
28
- no discard (R4). `restore` on a stock entry is the escape hatch that replaces them. -->
29
- <button class="ibtn lay-chip" id="layBtn" aria-expanded="false" aria-controls="layMenu"
30
- title="Layout — pick one, then adjust it freely (⌃1–⌃4)"><i aria-hidden="true">▤</i><span id="layName">review</span></button>
31
- <button class="ibtn" id="themeBtn" title="Toggle theme">theme</button>
32
- </div>
33
-
34
- <div class="lay-menu" id="layMenu" hidden role="dialog" aria-label="Layouts">
35
- <div class="lay-cap">layouts</div>
36
- <div id="layList"></div>
37
- <!-- R12 — every position the pointer drag can reach is also a button here. Without this,
38
- putting the sidebar on the right or docking the reader at the bottom was pointer-only:
39
- no stock layout carries either, and the grip's Enter/Space merely opened this menu,
40
- which had nothing in it that could move a pane. -->
41
- <div class="lay-pos" id="layPos"></div>
42
- <form class="lay-new miniform" id="layNewForm" hidden>
43
- <label class="lbl" for="layNewName">name this layout</label>
44
- <input id="layNewName" type="text" placeholder="e.g. two agents + prd" autocomplete="off" spellcheck="false" maxlength="40" />
45
- <div class="row"><button type="submit" class="go">create</button>
46
- <button type="button" class="quiet" id="layNewCancel">cancel</button></div>
47
- </form>
48
- <button class="lay-add" id="layNewBtn">+ new layout from this arrangement</button>
49
- <p class="lay-foot">Layouts belong to <b id="layScope">this repo</b>. Drag a pane by its ⠿ grip,
50
- or an edge to resize — or use the buttons above. Whatever you change is kept.</p>
51
- </div>
40
+ <!-- ── the phone surface (≤760px) ────────────────────────────────────────────────────────────────
41
+ Everything else on this page is built by pm/app.js; these two blocks are markup because they
42
+ are the phone's CHROME, and chrome that exists in the document is chrome a screen reader can
43
+ reach in source order — the top bar above the content, the destination nav below it.
44
+
45
+ `hidden` is the SHIPPED state of both. pm/mobile.js removes it only at ≤760px, so above the
46
+ breakpoint this is absent from the accessibility tree rather than merely invisible: a desktop
47
+ PM must not be able to tab into a second navigation system they cannot see. -->
48
+
49
+ <!-- The top bar: back · the screen's name · the ⌘K jump palette · theme. It sits ABOVE .body in
50
+ DOM order because it is the row above the content on screen, and putting it beside the bottom
51
+ nav would read the app backwards. -->
52
+ <div class="mob" id="mobShell" hidden>
53
+ <header class="mob-bar" id="mobBar">
54
+ <button class="mob-back" id="mobBack" type="button" hidden aria-label="Back">‹ back</button>
55
+ <span class="mob-brand" id="mobBrand" aria-hidden="true">◎</span>
56
+ <span class="mob-title" id="mobTitle"></span>
57
+ <!-- The jump palette lives HERE, not in the control bar below. Two reasons, and the second is
58
+ the binding one: it is not a terminal control key (every other button in the bar writes a
59
+ PTY sequence), and that bar is `overflow-x:auto` with seven 44px targets, so at 390px its
60
+ last child sits past the right edge — reachable only by a horizontal scroll nobody would
61
+ guess at. A control the PM cannot see is the hover-only trap in another costume. The
62
+ palette is a jump control for every destination, so it belongs in the bar that is always
63
+ on screen. -->
64
+ <button class="mob-pal" id="mobPalette" type="button" aria-label="Open the jump palette (⌘K)">⌘K</button>
65
+ <button class="mob-theme" id="mobTheme" type="button" aria-label="Toggle theme">◐</button>
52
66
  </header>
67
+ </div>
53
68
 
54
- <!-- The arrangement is COMPUTED from the current layout (lib/layout.js), not hard-coded here:
55
- the sidebar, the terminal and the reader are grid areas whose columns/rows/areas are written
56
- by applyLayout(). Each pane carries a ⠿ grip (move) and, where it has an outer edge, a
57
- resize handle. DOM order is fixed; only the grid decides where a pane lands. -->
58
- <div class="body" id="bodyGrid">
59
-
60
- <!-- ONE tree (R1): repositories → their working trees, each labelled by the initiative on its
61
- branch → that tree's sessions. It replaces the old `workspaces` + `running agents` pair,
62
- which split a parent from its children across two panes and made the PM correlate a live
63
- count against a separate list. The fold key stays `ws` so an arrangement saved before this
64
- change still resolves; `sess` is retired and ignored (R13). -->
65
- <nav class="side" id="side" aria-label="Repositories, initiatives and running agents">
66
- <div class="edge" id="sideEdge" data-edge="side" role="separator" aria-label="Resize the sidebar" tabindex="0"></div>
67
- <section class="pane" id="paneWs">
68
- <div class="cap">
69
- <span class="grip" id="sideGrip" data-pane="side" role="button" tabindex="0"
70
- aria-label="Move the sidebar" title="Move this column">⠿</span>
71
- <!-- R8: the pane states its structure. It was called `work` — the least informative word
72
- available for a tree of repositories, their working trees, and their sessions. -->
73
- <button class="fold" id="wsFold" data-fold="ws" aria-expanded="true" title="Hide the repositories, their working trees and their sessions"><span class="chev" aria-hidden="true">▾</span>repos &amp; sessions</button>
74
- <span class="ct" id="needCount"></span>
75
- <button class="mini" id="addWsBtn" aria-expanded="false" title="Add a repository">+</button></div>
76
- <div class="pane-body">
77
- <button class="newsess" id="newBtn" aria-expanded="false"><i>+</i> new session — run an agent here</button>
78
- <form class="miniform" id="newForm" hidden>
79
- <!-- R5: "here" gets a visible referent. The composer has always acted on the selected
80
- tree; with every repository on screen at once, nothing said which that was. -->
81
- <p class="where" id="newWhere"></p>
82
- <label class="lbl" for="newName">name this session</label>
83
- <input id="newName" type="text" placeholder="e.g. jira-sync build" aria-label="Session name" autocomplete="off" spellcheck="false" />
84
- <div class="seg" role="radiogroup" aria-label="Agent to run" id="newAgents">
85
- <button type="button" data-agent="shell" role="radio" aria-checked="true">shell</button>
86
- <button type="button" data-agent="claude" role="radio" aria-checked="false">claude</button>
87
- <button type="button" data-agent="codex" role="radio" aria-checked="false">codex</button>
88
- </div>
89
- <div class="brwrap" id="newBranchWrap" hidden>
90
- <label class="lbl" for="newBranch">branch <span class="opt">— optional</span></label>
91
- <select id="newBranch" aria-label="Branch to run this session on"></select>
92
- <input id="newBranchName" type="text" hidden placeholder="new-branch-name" aria-label="New branch name" autocomplete="off" spellcheck="false" />
93
- <label class="chk" id="newIsolateWrap" hidden>
94
- <input type="checkbox" id="newIsolate" checked />
95
- <span>give it its own folder — leave running sessions alone</span>
96
- </label>
97
- <p class="hint" id="newBranchHint"></p>
98
- </div>
99
- <p class="hint" id="newNoGit" hidden>no git in this folder — nothing to branch, the session just starts here</p>
100
- <div class="row">
101
- <button type="submit" class="go">start session</button>
102
- <button type="button" class="quiet" id="newCancel">cancel</button>
103
- </div>
104
- </form>
105
- <div id="tree"></div>
106
- <div id="workspaces" hidden></div>
107
- <form class="miniform" id="addWsForm" hidden>
108
- <button type="button" class="pick" id="pickFolderBtn">📁 Choose folder…</button>
109
- <label class="lbl" for="addWsPath">or type a path</label>
110
- <input id="addWsPath" type="text" placeholder="/path/to/another/repo" aria-label="Path to a repository" autocomplete="off" spellcheck="false" />
111
- <div class="row">
112
- <button type="submit" class="go">add</button>
113
- <button type="button" class="quiet" id="addWsCancel">cancel</button>
114
- </div>
115
- <p class="hint">or just run <code>pmos</code> inside that repo</p>
116
- </form>
117
- <!-- Worktrees the workspace made are folders the PM has to live with, so the app that
118
- creates them is the app that clears them. Hidden entirely until there is one. -->
119
- <details class="wtbox" id="wtBox" hidden>
120
- <summary id="wtSummary">worktrees</summary>
121
- <div id="wtList"></div>
122
- </details>
123
- </div>
124
- </section>
125
-
126
- </nav>
127
-
128
- <main class="term-col" aria-label="Sessions and terminal">
129
- <!-- Terminal GROUPS (R6). One group is the shipped behaviour; `split` shows a second beside
130
- it, each with its own tab strip and its own visible session. The #needs strip is NOT
131
- duplicated — it belongs to the column, not to a group. -->
132
- <div class="groups" id="groups" data-split="off">
133
- <section class="group" id="group0" data-g="0">
134
- <div class="stabs" role="tablist" aria-label="Open sessions" id="stabs"></div>
135
- <div class="termhost" id="termhost">
136
- <!-- INSIDE termhost (position:relative): the overlay covers only the terminal area,
137
- never the sidebar/tabs/needs — those must stay usable while a session is exited. -->
138
- <div class="exited-overlay" id="exited">
139
- <div class="card">
140
- <p id="exitedMsg">This session has exited.</p>
141
- <div class="row">
142
- <button id="respawnBtn">↻ respawn</button>
143
- <button id="removeBtn" class="quiet">✕ remove session</button>
144
- </div>
145
- </div>
146
- </div>
147
- <div class="empty-overlay" id="noSess">
148
- <div class="card">
149
- <p>No sessions in <b id="noSessWs">this workspace</b> yet.</p>
150
- <p class="reason">+ new session runs an agent here — it starts in this repo's root.</p>
151
- </div>
152
- </div>
153
- </div>
154
- </section>
155
- <section class="group" id="group1" data-g="1" hidden>
156
- <div class="stabs" role="tablist" aria-label="Open sessions, second group" id="stabs1"></div>
157
- <div class="termhost" id="termhost1">
158
- <!-- a split with nothing to put in it offers the next step, never an empty black box -->
159
- <div class="empty-overlay" id="noSess1">
160
- <div class="card">
161
- <p>Nothing else to show here yet.</p>
162
- <p class="reason">Start a second session and it appears in this group.</p>
163
- <div class="row"><button id="newHere1">+ new session</button>
164
- <button id="unsplitBtn" class="quiet">✕ close this group</button></div>
165
- </div>
166
- </div>
167
- </div>
168
- </section>
169
- </div>
170
- <div class="needs" id="needs" role="status" style="display:none"></div>
171
-
172
- <!-- Scratchpad: a collapsible drawer at the BOTTOM of the terminal column. Folded it is a thin
173
- bar; open it takes a bounded slice below the terminal — it never overlays the tabs, the
174
- terminal, or the document rail. Persists PER TAB: each session keeps its own staging
175
- text, and the label below says which tab you are staging for. -->
176
- <section class="scratch" id="scratch" data-open="false" aria-label="Scratchpad">
177
- <div class="scratch-head">
178
- <button type="button" class="scratch-toggle" id="scratchToggle" aria-expanded="false" aria-controls="scratchText">
179
- <span class="chev" aria-hidden="true">▴</span> scratchpad
180
- </button>
181
- <span class="scratch-who" id="scratchWho"></span>
182
- <span class="scratch-status" id="scratchStatus" role="status"></span>
183
- <span class="scratch-actions">
184
- <button type="button" class="mini" id="scratchCopy" title="Copy to clipboard">copy</button>
185
- <button type="button" class="mini" id="scratchSend" title="Paste into the active terminal (does not press enter)">→ terminal</button>
186
- </span>
187
- </div>
188
- <textarea id="scratchText" class="scratch-text" placeholder="Stage commands or notes here, then copy or send them to the terminal…" spellcheck="false" autocomplete="off" aria-label="Scratchpad text"></textarea>
189
- </section>
190
- </main>
191
-
192
- <aside class="rail" id="rail" aria-label="Library and document">
193
- <div class="railgrip edge" id="railGrip" data-edge="read" role="separator" aria-label="Resize the reader" tabindex="0"></div>
194
- <section class="pane lib-pane" id="paneLib">
195
- <div class="cap">
196
- <span class="grip" id="readGrip" data-pane="reader" role="button" tabindex="0"
197
- aria-label="Move the reader" title="Move this pane">⠿</span>
198
- <button class="fold" id="libFold" aria-expanded="true" title="Hide the library"><span class="chev" aria-hidden="true">▾</span>library</button>
199
- <!-- R7 — the four standing lenses are gone. What the rail shows follows the SELECTION:
200
- a repository group gives repo detail (the folder map, its schedules, its history), a
201
- tree row gives that initiative's dossier. The views below are the sub-views of
202
- whichever of those two is current, rendered by renderLensTabs(). -->
203
- <span class="lens" role="tablist" aria-label="Detail view" id="lensTabs"></span>
204
- <span class="reading" id="libWho" title="the repository these documents come from"></span>
205
- <button class="mini librefresh" id="libRefresh" title="Rescan the repository for new files & folders" aria-label="Refresh the library">↻</button>
206
- </div>
207
- <div class="pane-body">
208
- <input class="libfind" id="libFind" type="search" placeholder="filter the library…" aria-label="Filter the library" autocomplete="off" />
209
- <div id="library"></div>
210
- </div>
211
- </section>
212
-
213
- <div class="railsplit" id="railSplit" role="separator" aria-label="Resize the library and document panes" tabindex="0" hidden></div>
214
-
215
- <section class="pane doc-pane" id="paneDoc" hidden>
216
- <div class="cap">
217
- <button class="fold" id="docFold" aria-expanded="true" title="Hide the document"><span class="chev" aria-hidden="true">▾</span>document</button>
218
- <span class="crumb" id="crumb"></span>
219
- <button class="mini docrev" id="docEdit" hidden title="Edit this document">edit</button>
220
- <button class="mini docrev" id="docSave" hidden title="Save (⌘S)">save</button>
221
- <button class="mini docrev" id="docCancel" hidden title="Discard changes">cancel</button>
222
- <button class="mini docrev" id="docHistory" hidden title="Changes you have made to this document">history</button>
223
- <button class="x" id="closeDoc" aria-label="Close document">✕</button>
224
- </div>
225
- <div class="pane-body doc-body" id="docBody" tabindex="0"></div>
226
- </section>
227
- </aside>
228
-
229
- <!-- Drop targets, shown only while a ⠿ grip is being dragged. They are an ACCELERATOR: every
230
- position they offer is also reachable from the layout menu, so the drag is never the only
231
- path to an arrangement (R12). -->
232
- <div class="drop" id="drop" aria-hidden="true">
233
- <div class="dz" data-z="left">left</div>
234
- <div class="dz" data-z="right">right</div>
235
- <div class="dz" data-z="bottom">bottom</div>
236
- <div class="dz" data-z="off">hide</div>
237
- </div>
238
- </div>
239
-
240
- <!-- ⌘K jump palette (client-side; no backend) -->
241
- <div class="palette" id="palette" role="dialog" aria-modal="true" aria-label="Jump" style="display:none">
242
- <div class="palette-box">
243
- <input id="palInput" type="text" placeholder="Summon a view (initiatives, roadmap, okr, report) or jump to a session, initiative, document…" aria-label="Summon a view or jump to a workspace, session, initiative or document" autocomplete="off" />
244
- <div class="palette-list" id="palList" role="listbox"></div>
245
- </div>
246
- </div>
247
-
248
- <!-- in-app folder browser — the stand-in when the machine has no native chooser -->
249
- <div class="palette" id="fsPicker" role="dialog" aria-modal="true" aria-label="Choose a repository" style="display:none">
250
- <div class="palette-box">
251
- <div class="fs-head">
252
- <button class="fs-up" id="fsUp" aria-label="Parent folder">↑</button>
253
- <span class="fs-crumb" id="fsCrumb"></span>
254
- <button class="fs-use" id="fsUse">use this folder</button>
255
- <button class="x" id="fsClose" aria-label="Cancel">✕</button>
256
- </div>
257
- <div class="palette-list" id="fsList" role="listbox"></div>
258
- </div>
259
- </div>
260
-
261
- <!-- Mobile only (≤760px): a terminal control-key accessory bar (phones lack these keys) over a
262
- pane switcher. Hidden on desktop via CSS; the switcher swaps the single visible column. -->
263
- <div class="mobmenu" id="mobMenu" data-view="terminal">
264
- <div class="ctrlbar" id="ctrlBar" role="toolbar" aria-label="Terminal control keys">
265
- <button type="button" data-seq="esc" aria-label="Escape"><span class="lbl">esc</span></button>
266
- <button type="button" data-seq="tab" aria-label="Tab"><span class="lbl">tab</span></button>
267
- <button type="button" id="ctrlKey" aria-pressed="false" aria-label="Control modifier (one-shot)"><span class="lbl">ctrl</span></button>
268
- <button type="button" data-seq="left" aria-label="Left arrow">←</button>
269
- <button type="button" data-seq="up" aria-label="Up arrow">↑</button>
270
- <button type="button" data-seq="down" aria-label="Down arrow">↓</button>
271
- <button type="button" data-seq="right" aria-label="Right arrow">→</button>
272
- <button type="button" id="mobPalette" aria-label="Open the jump palette (⌘K)">⌘K</button>
273
- </div>
274
- <nav class="mobtabs" role="tablist" aria-label="Switch pane">
275
- <button type="button" role="tab" data-mview="sessions" aria-selected="false">sessions</button>
276
- <button type="button" role="tab" data-mview="terminal" aria-selected="true">terminal</button>
277
- <button type="button" role="tab" data-mview="library" aria-selected="false">library</button>
278
- </nav>
69
+ <!-- The bottom block: a terminal control-key accessory bar (terminal screen only — phones have no
70
+ Esc/Tab/Ctrl/arrows) over the five-destination nav. The nav is a <nav> of buttons carrying
71
+ aria-current, NOT a tablist: the retired markup declared role="tablist" over role="tab"
72
+ children with no tabpanel and no aria-controls anywhere, which is a promise the DOM did not
73
+ keep. Selecting a destination resets it to its root; depth is pushed on top of it and #mobBack
74
+ pops one screen. The glyphs are the mobile prototype's own (◧ ◔ ◐ ◒ ☰). -->
75
+ <div class="mobmenu" id="mobMenu" hidden>
76
+ <div class="ctrlbar" id="ctrlBar" role="toolbar" aria-label="Terminal control keys">
77
+ <button type="button" data-seq="esc" aria-label="Escape"><span class="lbl">esc</span></button>
78
+ <button type="button" data-seq="tab" aria-label="Tab"><span class="lbl">tab</span></button>
79
+ <button type="button" id="ctrlKey" aria-pressed="false" aria-label="Control modifier (one-shot)"><span class="lbl">ctrl</span></button>
80
+ <button type="button" data-seq="left" aria-label="Left arrow">←</button>
81
+ <button type="button" data-seq="up" aria-label="Up arrow">↑</button>
82
+ <button type="button" data-seq="down" aria-label="Down arrow">↓</button>
83
+ <button type="button" data-seq="right" aria-label="Right arrow">→</button>
279
84
  </div>
280
-
281
- <footer class="strip">
282
- <span id="wsFoot">local · persistent server</span>
283
- <span class="right">sessions survive detach · ⌘K jump · F2 renames · Esc closes doc</span>
284
- </footer>
85
+ <nav class="mobnav" id="mobNav" aria-label="Sections">
86
+ <button type="button" class="mobnav-b" data-dest="board" aria-current="page"><span class="g" aria-hidden="true">◧</span><span class="l">board</span></button>
87
+ <button type="button" class="mobnav-b" data-dest="scheduled" aria-current="false"><span class="g" aria-hidden="true">◔</span><span class="l">scheduled</span></button>
88
+ <button type="button" class="mobnav-b" data-dest="agents" aria-current="false"><span class="g" aria-hidden="true">◐</span><span class="l">agents</span></button>
89
+ <button type="button" class="mobnav-b" data-dest="memory" aria-current="false"><span class="g" aria-hidden="true">◒</span><span class="l">memory</span></button>
90
+ <button type="button" class="mobnav-b" data-dest="more" aria-current="false"><span class="g" aria-hidden="true">☰</span><span class="l">more</span></button>
91
+ </nav>
285
92
  </div>
286
93
 
287
- <script src="/vendor/xterm.js"></script>
288
- <script src="/vendor/addon-fit.js"></script>
289
- <script src="/vendor/addon-web-links.js"></script>
290
- <script src="/lib/preview-kind.js"></script>
291
- <script src="/lib/layout.js"></script>
292
- <script src="/lib/session-state.js"></script>
293
- <!-- The editor computes its diff with the SAME module the server saves through, so what you see
294
- while editing and what the server writes can never disagree. -->
295
- <script src="/lib/apply-change.js"></script>
296
- <script src="/app.js"></script>
94
+ <!-- The rest of the surface is built by pm/app.js: the root element carries data-pm-theme and is
95
+ appended here at boot. Nothing is server-rendered, so there is no markup here to keep in sync. -->
96
+ <script type="module" src="/pm/app.js"></script>
297
97
  </body>
298
98
  </html>