create-pmos 0.13.0 → 0.15.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.
- package/README.md +39 -0
- package/package.json +1 -1
- package/payload/KIT-VERSION +2 -2
- package/workspace/focus.test.js +650 -0
- package/workspace/handoff.test.js +443 -0
- package/workspace/lib/console-state.js +48 -1
- package/workspace/lib/drive-guard.js +98 -0
- package/workspace/lib/drive-plan.js +97 -0
- package/workspace/lib/engine/workflow-state.js +13 -3
- package/workspace/lib/focus.js +967 -0
- package/workspace/lib/handoff.js +280 -0
- package/workspace/lib/layout.js +68 -46
- package/workspace/lib/mobile-nav.js +127 -0
- package/workspace/lib/profiles.js +285 -0
- package/workspace/lib/repo-scan.js +469 -0
- package/workspace/package.json +4 -2
- package/workspace/profiles.test.js +362 -0
- package/workspace/public/app.js +2318 -208
- package/workspace/public/index.html +158 -28
- package/workspace/public/styles.css +865 -51
- package/workspace/repo-scan.test.js +327 -0
- package/workspace/server.js +914 -23
|
@@ -51,12 +51,37 @@
|
|
|
51
51
|
</div>
|
|
52
52
|
</header>
|
|
53
53
|
|
|
54
|
+
<!-- workspace-mobile-surface — the phone's top bar: back, the screen's name, theme. It sits HERE,
|
|
55
|
+
above .body, because it is the row above the content on screen and the row above it for a
|
|
56
|
+
screen reader too; putting it beside the bottom nav in DOM order would read the app backwards.
|
|
57
|
+
`hidden` is the shipped state and app.js removes it at ≤760px only, so above the breakpoint
|
|
58
|
+
this is absent from the accessibility tree rather than merely invisible (R12). -->
|
|
59
|
+
<div class="mob" id="mobShell" hidden>
|
|
60
|
+
<header class="mob-bar" id="mobBar">
|
|
61
|
+
<button class="mob-back" id="mobBack" hidden aria-label="Back">‹</button>
|
|
62
|
+
<span class="mob-title" id="mobTitle"></span>
|
|
63
|
+
<!-- The jump palette lives HERE, not in the control bar below. Two reasons, and the second is
|
|
64
|
+
the binding one: it is not a terminal control key (the bar's other buttons all write a
|
|
65
|
+
PTY sequence), and the bar is `overflow-x:auto` with eight 44px buttons, so at 390px the
|
|
66
|
+
last one sat past the right edge — reachable only by a horizontal scroll nobody would
|
|
67
|
+
guess at. A control the PM cannot see is the hover-only trap in another costume. -->
|
|
68
|
+
<button class="mob-pal" id="mobPalette" aria-label="Open the jump palette (⌘K)">⌘K</button>
|
|
69
|
+
<button class="mob-theme" id="mobTheme" aria-label="Toggle theme">theme</button>
|
|
70
|
+
</header>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
54
73
|
<!-- The arrangement is COMPUTED from the current layout (lib/layout.js), not hard-coded here:
|
|
55
74
|
the sidebar, the terminal and the reader are grid areas whose columns/rows/areas are written
|
|
56
75
|
by applyLayout(). Each pane carries a ⠿ grip (move) and, where it has an outer edge, a
|
|
57
76
|
resize handle. DOM order is fixed; only the grid decides where a pane lands. -->
|
|
58
77
|
<div class="body" id="bodyGrid">
|
|
59
78
|
|
|
79
|
+
<!-- Collapsed-column rails. Shown INSTEAD of their column, in the same grid area, so the
|
|
80
|
+
track narrows to 36px rather than the column disappearing. Each keeps a real accessible
|
|
81
|
+
name; the rotated text is decoration over it, never the only label. -->
|
|
82
|
+
<button class="colrail" id="col1Rail" hidden aria-label="Show the initiatives column"><span id="col1RailLabel">initiatives ⟩</span></button>
|
|
83
|
+
<button class="colrail" id="col2Rail" hidden aria-label="Show the detail panel"><span id="col2RailLabel">initiative ⟩</span></button>
|
|
84
|
+
|
|
60
85
|
<!-- ONE tree (R1): repositories → their working trees, each labelled by the initiative on its
|
|
61
86
|
branch → that tree's sessions. It replaces the old `workspaces` + `running agents` pair,
|
|
62
87
|
which split a parent from its children across two panes and made the PM correlate a live
|
|
@@ -64,6 +89,7 @@
|
|
|
64
89
|
change still resolves; `sess` is retired and ignored (R13). -->
|
|
65
90
|
<nav class="side" id="side" aria-label="Repositories, initiatives and running agents">
|
|
66
91
|
<div class="edge" id="sideEdge" data-edge="side" role="separator" aria-label="Resize the sidebar" tabindex="0"></div>
|
|
92
|
+
|
|
67
93
|
<section class="pane" id="paneWs">
|
|
68
94
|
<div class="cap">
|
|
69
95
|
<span class="grip" id="sideGrip" data-pane="side" role="button" tabindex="0"
|
|
@@ -74,6 +100,45 @@
|
|
|
74
100
|
<span class="ct" id="needCount"></span>
|
|
75
101
|
<button class="mini" id="addWsBtn" aria-expanded="false" title="Add a repository">+</button></div>
|
|
76
102
|
<div class="pane-body">
|
|
103
|
+
<!-- workspace-focus R1 — a FULL-WIDTH row above the mode menu, not a ninth cell in
|
|
104
|
+
#spMenu: the eighth cell there is the grid-squaring "hide" control and a ninth breaks
|
|
105
|
+
the two-row grid. Focus joins every registered repository, so it sits ABOVE the
|
|
106
|
+
product switcher, which scopes only what is below it. Visible in every mode.
|
|
107
|
+
The badge prints badge.text, never a count — a digit would be a claim about the whole
|
|
108
|
+
world, and the world includes reads that failed. -->
|
|
109
|
+
<button class="focusbtn" id="focusBtn" aria-expanded="false" aria-controls="focusPanel"
|
|
110
|
+
title="What only you can unblock, across every repository">
|
|
111
|
+
<span class="g" aria-hidden="true">◎</span><span class="l">focus</span>
|
|
112
|
+
<span class="ct" id="focusBadge"></span>
|
|
113
|
+
</button>
|
|
114
|
+
<!-- workspace-product-switcher — one button per PRODUCT (a distinct repo_key), not per
|
|
115
|
+
working tree: a product with four trees was four rows of the same name, and the tree
|
|
116
|
+
is a detail of where you stand, not of what you are working on. Buttons WRAP so a
|
|
117
|
+
full product name always shows; a horizontally scrolling strip is not acceptable —
|
|
118
|
+
it hides products off-screen with nothing on screen saying they exist. Product-scoped,
|
|
119
|
+
so app.js hides it outside `product` mode. -->
|
|
120
|
+
<div class="psw" id="prodSwitch" role="group" aria-label="Products" hidden>
|
|
121
|
+
<div class="psw-list" id="pswList"></div>
|
|
122
|
+
<!-- opens the SHIPPED #addWsForm — the same node #addWsBtn opens. Two controls onto one
|
|
123
|
+
form is fine; two forms that both add a repository is not. -->
|
|
124
|
+
<button class="psw-add" id="pswAdd" aria-expanded="false" aria-controls="addWsForm"
|
|
125
|
+
aria-label="Add a repository" title="Add a repository">+</button>
|
|
126
|
+
</div>
|
|
127
|
+
<!-- workspace-pm-spine — the destinations sit ON TOP OF the initiative list they scope,
|
|
128
|
+
not in a separate nav column. Everything here is PRODUCT-SPECIFIC — knowledge and
|
|
129
|
+
scheduled tasks belong to the repository in view, not to the app — so the menu
|
|
130
|
+
lives inside the product's own column, where its scope is unambiguous. -->
|
|
131
|
+
<div class="spmenu" id="spMenu" role="toolbar" aria-label="Views"></div>
|
|
132
|
+
<!-- One search box over everything the column can hold, not a per-mode filter. It
|
|
133
|
+
narrows the current mode's list AND surfaces matches from elsewhere (artifacts,
|
|
134
|
+
sessions) underneath, so a name you half-remember is findable without first
|
|
135
|
+
knowing which mode it lives in. ⌘K focuses it. -->
|
|
136
|
+
<div class="spfind-wrap">
|
|
137
|
+
<div class="spfind">
|
|
138
|
+
<span class="g" aria-hidden="true">⌕</span>
|
|
139
|
+
<input id="spFind" type="search" placeholder="search everything…" aria-label="Search initiatives, artifacts and sessions" autocomplete="off" />
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
77
142
|
<button class="newsess" id="newBtn" aria-expanded="false"><i>+</i> new session — run an agent here</button>
|
|
78
143
|
<form class="miniform" id="newForm" hidden>
|
|
79
144
|
<!-- R5: "here" gets a visible referent. The composer has always acted on the selected
|
|
@@ -86,6 +151,14 @@
|
|
|
86
151
|
<button type="button" data-agent="claude" role="radio" aria-checked="false">claude</button>
|
|
87
152
|
<button type="button" data-agent="codex" role="radio" aria-checked="false">codex</button>
|
|
88
153
|
</div>
|
|
154
|
+
<!-- workspace-profiles — the chips are written from the descriptors actually read off
|
|
155
|
+
disk. NOTHING is hard-coded here: a picker that renders two chips on a machine with
|
|
156
|
+
no descriptors makes a choice that applies nothing. An empty group renders whatever
|
|
157
|
+
readProfiles said — absent, or a successful read of nothing. -->
|
|
158
|
+
<div class="prof" id="newProfile" role="radiogroup" aria-label="Profile to start this session under"></div>
|
|
159
|
+
<p class="prof-note" id="newProfileNote">Chosen at start and never changed afterwards — a process's
|
|
160
|
+
environment is fixed when it is created. A session started on one profile does not get the MCP
|
|
161
|
+
servers another profile declares.</p>
|
|
89
162
|
<div class="brwrap" id="newBranchWrap" hidden>
|
|
90
163
|
<label class="lbl" for="newBranch">branch <span class="opt">— optional</span></label>
|
|
91
164
|
<select id="newBranch" aria-label="Branch to run this session on"></select>
|
|
@@ -103,11 +176,41 @@
|
|
|
103
176
|
</div>
|
|
104
177
|
</form>
|
|
105
178
|
<div id="tree"></div>
|
|
106
|
-
|
|
179
|
+
<!-- workspace-focus — a SIBLING of #tree, not a replacement for it: focus is a second
|
|
180
|
+
reading of the same repositories, so leaving the tree in place is what lets the PM
|
|
181
|
+
check the queue against the structure it was derived from. app.js writes six sections
|
|
182
|
+
(#fcIntro #fcObjectives #fcQueue #fcHandoffs #fcDelegated #fcUpkeep) into #focusBody,
|
|
183
|
+
always all six — a section that is absent says nothing, and "nothing waiting on you"
|
|
184
|
+
is a fact worth printing. -->
|
|
185
|
+
<section class="fc" id="focusPanel" hidden aria-label="Focus — what only you can unblock">
|
|
186
|
+
<div class="fc-body" id="focusBody"></div>
|
|
187
|
+
</section>
|
|
188
|
+
<!-- workspace-repo-ingest — the two sources are stated in the affordance, before the
|
|
189
|
+
click, because they do different things: a folder is REGISTERED (read where it lies)
|
|
190
|
+
and a GitHub repo is INGESTED (cloned, scanned, proposed). Both consequence lines are
|
|
191
|
+
always present, one under each option, so the difference is never something you learn
|
|
192
|
+
afterwards from what happened. -->
|
|
107
193
|
<form class="miniform" id="addWsForm" hidden>
|
|
108
|
-
<
|
|
109
|
-
|
|
110
|
-
|
|
194
|
+
<div class="ing-src seg" role="radiogroup" aria-label="Where this repository comes from" id="addWsSource">
|
|
195
|
+
<button type="button" data-src="local" role="radio" aria-checked="true">local folder</button>
|
|
196
|
+
<button type="button" data-src="github" role="radio" aria-checked="false">GitHub</button>
|
|
197
|
+
</div>
|
|
198
|
+
|
|
199
|
+
<div class="ing-opt" id="addWsLocal">
|
|
200
|
+
<button type="button" class="pick" id="pickFolderBtn">📁 Choose folder…</button>
|
|
201
|
+
<label class="lbl" for="addWsPath">or type a path</label>
|
|
202
|
+
<input id="addWsPath" type="text" placeholder="/path/to/another/repo" aria-label="Path to a repository" autocomplete="off" spellcheck="false" />
|
|
203
|
+
<p class="ing-note" id="addWsLocalNote">Adding a folder registers it. Nothing is imported from a
|
|
204
|
+
bare folder — its own records are read and shown, but no PMOS state is proposed.</p>
|
|
205
|
+
</div>
|
|
206
|
+
|
|
207
|
+
<div class="ing-opt" id="addWsGithub" hidden>
|
|
208
|
+
<label class="lbl" for="addWsRepo">repository</label>
|
|
209
|
+
<input id="addWsRepo" type="text" placeholder="owner/name or https://github.com/owner/name" aria-label="Repository to ingest — owner/name or a GitHub URL" autocomplete="off" spellcheck="false" />
|
|
210
|
+
<p class="ing-note" id="addWsGithubNote">Four stages run: clone · scan (read-only) · review ·
|
|
211
|
+
accept. The scan proposes; nothing is written into the repository until you confirm one item.</p>
|
|
212
|
+
</div>
|
|
213
|
+
|
|
111
214
|
<div class="row">
|
|
112
215
|
<button type="submit" class="go">add</button>
|
|
113
216
|
<button type="button" class="quiet" id="addWsCancel">cancel</button>
|
|
@@ -132,6 +235,13 @@
|
|
|
132
235
|
<div class="groups" id="groups" data-split="off">
|
|
133
236
|
<section class="group" id="group0" data-g="0">
|
|
134
237
|
<div class="stabs" role="tablist" aria-label="Open sessions" id="stabs"></div>
|
|
238
|
+
<!-- workspace-profiles R6 — which profile this pane was started under, as WORKSPACE
|
|
239
|
+
chrome between the tab strip and the terminal, never written into the pane. Writing
|
|
240
|
+
into a tmux pane sends input to the agent; anything appended to the ring buffer is
|
|
241
|
+
erased on the next reattach; and a line the workspace injects into scrollback is
|
|
242
|
+
indistinguishable from one the agent printed, which defeats a line whose whole
|
|
243
|
+
purpose is provenance. The sentence is computed server-side and printed verbatim. -->
|
|
244
|
+
<div class="profline" id="profLine0" role="status"></div>
|
|
135
245
|
<div class="termhost" id="termhost">
|
|
136
246
|
<!-- INSIDE termhost (position:relative): the overlay covers only the terminal area,
|
|
137
247
|
never the sidebar/tabs/needs — those must stay usable while a session is exited. -->
|
|
@@ -154,6 +264,8 @@
|
|
|
154
264
|
</section>
|
|
155
265
|
<section class="group" id="group1" data-g="1" hidden>
|
|
156
266
|
<div class="stabs" role="tablist" aria-label="Open sessions, second group" id="stabs1"></div>
|
|
267
|
+
<!-- the second group carries its own line: two panes side by side may be two profiles -->
|
|
268
|
+
<div class="profline" id="profLine1" role="status"></div>
|
|
157
269
|
<div class="termhost" id="termhost1">
|
|
158
270
|
<!-- a split with nothing to put in it offers the next step, never an empty black box -->
|
|
159
271
|
<div class="empty-overlay" id="noSess1">
|
|
@@ -173,6 +285,25 @@
|
|
|
173
285
|
bar; open it takes a bounded slice below the terminal — it never overlays the tabs, the
|
|
174
286
|
terminal, or the document rail. Persists PER TAB: each session keeps its own staging
|
|
175
287
|
text, and the label below says which tab you are staging for. -->
|
|
288
|
+
|
|
289
|
+
<!-- workspace-viewer-column — the viewer. Documents, images and summoned surfaces are tabs in
|
|
290
|
+
THIS column now, beside the terminals, rather than a pane inside the rail. It is one pane
|
|
291
|
+
spanning the group row rather than one per group: a session belongs to a group because two
|
|
292
|
+
agents genuinely run side by side, and a document has no such property — giving it a group
|
|
293
|
+
would invent an affinity the object does not have. When a viewer tab is active it covers
|
|
294
|
+
the groups; un-selecting it returns both groups exactly as they were. -->
|
|
295
|
+
<section class="viewpane" id="viewPane" hidden aria-label="Document viewer">
|
|
296
|
+
<div class="cap">
|
|
297
|
+
<span class="crumb" id="crumb"></span>
|
|
298
|
+
<button class="mini docrev" id="docEdit" hidden title="Edit this document">edit</button>
|
|
299
|
+
<button class="mini docrev" id="docSave" hidden title="Save (⌘S)">save</button>
|
|
300
|
+
<button class="mini docrev" id="docCancel" hidden title="Discard changes">cancel</button>
|
|
301
|
+
<button class="mini docrev" id="docHistory" hidden title="Changes you have made to this document">history</button>
|
|
302
|
+
<button class="x" id="closeDoc" aria-label="Close document">✕</button>
|
|
303
|
+
</div>
|
|
304
|
+
<div class="pane-body doc-body" id="docBody" tabindex="0"></div>
|
|
305
|
+
</section>
|
|
306
|
+
|
|
176
307
|
<section class="scratch" id="scratch" data-open="false" aria-label="Scratchpad">
|
|
177
308
|
<div class="scratch-head">
|
|
178
309
|
<button type="button" class="scratch-toggle" id="scratchToggle" aria-expanded="false" aria-controls="scratchText">
|
|
@@ -195,7 +326,7 @@
|
|
|
195
326
|
<div class="cap">
|
|
196
327
|
<span class="grip" id="readGrip" data-pane="reader" role="button" tabindex="0"
|
|
197
328
|
aria-label="Move the reader" title="Move this pane">⠿</span>
|
|
198
|
-
<button class="fold" id="libFold" aria-expanded="true" title="Hide the
|
|
329
|
+
<button class="fold" id="libFold" aria-expanded="true" title="Hide the initiative panel"><span class="chev" aria-hidden="true">▾</span>initiative</button>
|
|
199
330
|
<!-- R7 — the four standing lenses are gone. What the rail shows follows the SELECTION:
|
|
200
331
|
a repository group gives repo detail (the folder map, its schedules, its history), a
|
|
201
332
|
tree row gives that initiative's dossier. The views below are the sub-views of
|
|
@@ -203,6 +334,7 @@
|
|
|
203
334
|
<span class="lens" role="tablist" aria-label="Detail view" id="lensTabs"></span>
|
|
204
335
|
<span class="reading" id="libWho" title="the repository these documents come from"></span>
|
|
205
336
|
<button class="mini librefresh" id="libRefresh" title="Rescan the repository for new files & folders" aria-label="Refresh the library">↻</button>
|
|
337
|
+
<button class="mini" id="col2Hide" title="Collapse this column" aria-label="Collapse the detail panel">⟨</button>
|
|
206
338
|
</div>
|
|
207
339
|
<div class="pane-body">
|
|
208
340
|
<input class="libfind" id="libFind" type="search" placeholder="filter the library…" aria-label="Filter the library" autocomplete="off" />
|
|
@@ -210,20 +342,6 @@
|
|
|
210
342
|
</div>
|
|
211
343
|
</section>
|
|
212
344
|
|
|
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
345
|
</aside>
|
|
228
346
|
|
|
229
347
|
<!-- Drop targets, shown only while a ⠿ grip is being dragged. They are an ACCELERATOR: every
|
|
@@ -232,7 +350,6 @@
|
|
|
232
350
|
<div class="drop" id="drop" aria-hidden="true">
|
|
233
351
|
<div class="dz" data-z="left">left</div>
|
|
234
352
|
<div class="dz" data-z="right">right</div>
|
|
235
|
-
<div class="dz" data-z="bottom">bottom</div>
|
|
236
353
|
<div class="dz" data-z="off">hide</div>
|
|
237
354
|
</div>
|
|
238
355
|
</div>
|
|
@@ -258,9 +375,13 @@
|
|
|
258
375
|
</div>
|
|
259
376
|
</div>
|
|
260
377
|
|
|
261
|
-
<!-- Mobile only (≤760px): a terminal control-key accessory bar (phones lack these keys) over
|
|
262
|
-
|
|
263
|
-
|
|
378
|
+
<!-- Mobile only (≤760px): a terminal control-key accessory bar (phones lack these keys) over the
|
|
379
|
+
destination nav. Hidden on desktop via CSS. The accessory bar keeps its ids and its behaviour
|
|
380
|
+
— it is a deliberate divergence from the prototype, whose terminal screen is a text input over
|
|
381
|
+
sample lines; this one is driving a real PTY, and the control keys are the thing the current
|
|
382
|
+
phone mode does well. No `data-view` is hard-coded: the screen at load is a destination root,
|
|
383
|
+
not the terminal, and app.js sets the attribute from the screen it actually applied. -->
|
|
384
|
+
<div class="mobmenu" id="mobMenu">
|
|
264
385
|
<div class="ctrlbar" id="ctrlBar" role="toolbar" aria-label="Terminal control keys">
|
|
265
386
|
<button type="button" data-seq="esc" aria-label="Escape"><span class="lbl">esc</span></button>
|
|
266
387
|
<button type="button" data-seq="tab" aria-label="Tab"><span class="lbl">tab</span></button>
|
|
@@ -269,12 +390,18 @@
|
|
|
269
390
|
<button type="button" data-seq="up" aria-label="Up arrow">↑</button>
|
|
270
391
|
<button type="button" data-seq="down" aria-label="Down arrow">↓</button>
|
|
271
392
|
<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
393
|
</div>
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
394
|
+
<!-- Five destinations, replacing the three-column switcher. It is a <nav> of buttons carrying
|
|
395
|
+
aria-current, NOT the tablist it used to be: that markup declared role="tablist" over
|
|
396
|
+
role="tab" children with no tabpanel and no aria-controls anywhere — a promise the DOM did
|
|
397
|
+
not keep. Selection here resets a destination to its root; depth is pushed on top and the
|
|
398
|
+
back control in #mobBar pops it. -->
|
|
399
|
+
<nav class="mobnav" id="mobNav" aria-label="Sections">
|
|
400
|
+
<button type="button" class="mobnav-b" data-dest="board" aria-current="page">board</button>
|
|
401
|
+
<button type="button" class="mobnav-b" data-dest="scheduled">scheduled</button>
|
|
402
|
+
<button type="button" class="mobnav-b" data-dest="agents">agents</button>
|
|
403
|
+
<button type="button" class="mobnav-b" data-dest="memory">memory</button>
|
|
404
|
+
<button type="button" class="mobnav-b" data-dest="more">more</button>
|
|
278
405
|
</nav>
|
|
279
406
|
</div>
|
|
280
407
|
|
|
@@ -289,6 +416,9 @@
|
|
|
289
416
|
<script src="/vendor/addon-web-links.js"></script>
|
|
290
417
|
<script src="/lib/preview-kind.js"></script>
|
|
291
418
|
<script src="/lib/layout.js"></script>
|
|
419
|
+
<!-- the phone's navigation reducer: DOM-free, and required by test.js so the whole screen
|
|
420
|
+
domain is walkable in node rather than only in a browser -->
|
|
421
|
+
<script src="/lib/mobile-nav.js"></script>
|
|
292
422
|
<script src="/lib/session-state.js"></script>
|
|
293
423
|
<!-- The editor computes its diff with the SAME module the server saves through, so what you see
|
|
294
424
|
while editing and what the server writes can never disagree. -->
|