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.
- package/package.json +1 -1
- package/payload/KIT-VERSION +1 -1
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/state/discoveries.md +1 -1
- package/workspace/README.md +2 -0
- package/workspace/REMOTE.md +107 -65
- package/workspace/bin/pmos.js +4 -2
- package/workspace/first-paint-audit.js +6 -2
- package/workspace/fixtures/initiatives-api/list.json +13 -0
- package/workspace/fixtures/initiatives-api/roadmap.json +13 -0
- package/workspace/fixtures/report-api/draft.json +105 -0
- package/workspace/fixtures/reports-page/draft-biweekly-2026-10-16.md +12 -0
- package/workspace/guide/command-palette.md +1 -1
- package/workspace/guide/index.md +3 -1
- package/workspace/guide/initiatives-roadmap.md +1 -0
- package/workspace/guide/initiatives-screen.md +1 -0
- package/workspace/guide/launcher.md +51 -0
- package/workspace/guide/layout.md +7 -2
- package/workspace/guide/proactive-engine.md +21 -5
- package/workspace/guide/report-slides.md +28 -0
- package/workspace/guide/reports.md +2 -0
- package/workspace/lib/auto-sync.js +83 -0
- package/workspace/lib/board-fold.js +2 -2
- package/workspace/lib/breakdown-export.js +1 -6
- package/workspace/lib/brief.js +20 -2
- package/workspace/lib/goals.js +165 -0
- package/workspace/lib/initiative-rows.js +7 -3
- package/workspace/lib/launcher-cli.js +207 -0
- package/workspace/lib/plan-tree.js +15 -2
- package/workspace/lib/pmos-verbs.js +3 -0
- package/workspace/lib/proactive-cli.js +58 -2
- package/workspace/lib/proactive.js +2 -2
- package/workspace/lib/qr.js +293 -0
- package/workspace/lib/remote-access.js +184 -12
- package/workspace/lib/report-draft.js +34 -6
- package/workspace/lib/report-slides-data.js +141 -0
- package/workspace/lib/routes-initiatives.js +35 -2
- package/workspace/lib/routes-reports.js +3 -3
- package/workspace/lib/routes-timeline.js +1 -1
- package/workspace/lib/sync.js +61 -6
- package/workspace/package-lock.json +1 -1
- package/workspace/package.json +1 -1
- package/workspace/public/icons/apple-touch-icon.png +0 -0
- package/workspace/public/icons/icon-192.png +0 -0
- package/workspace/public/icons/icon-512.png +0 -0
- package/workspace/public/index.html +11 -0
- package/workspace/public/pm/app.js +55 -9
- package/workspace/public/pm/components.css +865 -1
- package/workspace/public/pm/dest-reports.js +8 -2
- package/workspace/public/pm/dest-today.js +8 -2
- package/workspace/public/pm/dock.js +73 -3
- package/workspace/public/pm/frame-copy.js +5 -0
- package/workspace/public/pm/initiative-list.js +18 -1
- package/workspace/public/pm/initiatives-copy.js +4 -0
- package/workspace/public/pm/panel-tabs.js +14 -12
- package/workspace/public/pm/print.css +11 -0
- package/workspace/public/pm/report-slides.js +438 -0
- package/workspace/public/pm/report-view.js +8 -2
- package/workspace/public/pm/reports-copy.js +3 -0
- package/workspace/public/pm/roadmap.js +23 -0
- package/workspace/public/pm/slides-copy.js +52 -0
- package/workspace/public/pm/today-copy.js +29 -0
- package/workspace/public/pm/today-goals.js +172 -0
- package/workspace/public/pm/tokens.css +13 -0
- package/workspace/public/pm/topbar.js +5 -2
- package/workspace/replaced-tests.json +36 -1
- package/workspace/server.js +165 -15
- package/workspace/skills/morning-brief.skill +15 -9
- package/workspace/skills/report-biweekly.skill +1 -0
- package/workspace/test-files.json +4 -0
- package/workspace/test-lists/run6.json +9 -0
- package/workspace/test-lists/workspace-launcher.json +5 -0
- package/workspace/test-lists/workspace-phone-app.json +5 -0
- package/workspace/test-lists/workspace-public-host.json +5 -0
package/workspace/lib/sync.js
CHANGED
|
@@ -18,6 +18,10 @@ const { execFile } = require('child_process');
|
|
|
18
18
|
const RECORD_PATHS = ['planning', 'pmos', '.pmos-profile', '.gitattributes'];
|
|
19
19
|
const NET_TIMEOUT_MS = 60000;
|
|
20
20
|
const LOCAL_TIMEOUT_MS = 10000;
|
|
21
|
+
// A reading compares HEAD with the remote-tracking ref the LAST fetch left, so it can say nothing about what another
|
|
22
|
+
// machine pushed since. Past this age (or with no fetch on record) the reading is `remote_stale`: the bar offers Sync
|
|
23
|
+
// rather than claim the repository is in sync on old evidence. Still no network: the age is the mtime of FETCH_HEAD.
|
|
24
|
+
const REMOTE_FRESH_MS = 10 * 60 * 1000;
|
|
21
25
|
|
|
22
26
|
function run(root, args, { net = false, raw = false } = {}) {
|
|
23
27
|
return new Promise((resolve) => {
|
|
@@ -28,6 +32,23 @@ function run(root, args, { net = false, raw = false } = {}) {
|
|
|
28
32
|
});
|
|
29
33
|
}
|
|
30
34
|
|
|
35
|
+
// When this clone last heard from its remote: the mtime of FETCH_HEAD, which every fetch rewrites. null = never.
|
|
36
|
+
async function remoteCheckedAt(root) {
|
|
37
|
+
const p = await run(root, ['rev-parse', '--git-path', 'FETCH_HEAD']);
|
|
38
|
+
if (!p.ok || !p.out) return null;
|
|
39
|
+
try { return fs.statSync(path.resolve(root, p.out)).mtime; } catch { return null; }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// A document repository (`.pmos-profile` says pm-artifacts, D68) holds nothing but the PM's documents, so every file
|
|
43
|
+
// in it is a record. A code repository's records are RECORD_PATHS only.
|
|
44
|
+
function profileOf(root) {
|
|
45
|
+
try {
|
|
46
|
+
const prof = fs.readFileSync(path.join(root, '.pmos-profile'), 'utf8').split('\n').map((l) => l.replace(/#.*$/, '').trim()).find(Boolean);
|
|
47
|
+
return prof === 'pm-artifacts' ? 'pm-artifacts' : 'product-code';
|
|
48
|
+
} catch { return 'product-code'; }
|
|
49
|
+
}
|
|
50
|
+
const recordTestFor = (root) => (profileOf(root) === 'pm-artifacts' ? () => true : isRecord);
|
|
51
|
+
|
|
31
52
|
const isRecord = (rel) => RECORD_PATHS.some((p) => rel === p || rel.startsWith(`${p}/`));
|
|
32
53
|
|
|
33
54
|
// PM repositories append ledgers from several machines; `merge=union` keeps both sides' lines instead
|
|
@@ -55,8 +76,9 @@ async function status(root) {
|
|
|
55
76
|
const up = await run(root, ['rev-parse', '--abbrev-ref', `${branch}@{upstream}`]);
|
|
56
77
|
const porcelain = await run(root, ['status', '--porcelain', '--untracked-files=all'], { raw: true });
|
|
57
78
|
const files = porcelain.out ? porcelain.out.split('\n').filter(Boolean).map((l) => l.slice(3).replace(/^"|"$/g, '').replace(/.* -> /, '')) : [];
|
|
58
|
-
const
|
|
59
|
-
const
|
|
79
|
+
const rec = recordTestFor(root);
|
|
80
|
+
const dirtyRecords = files.filter(rec);
|
|
81
|
+
const dirtyOther = files.filter((f) => !rec(f));
|
|
60
82
|
let ahead = null; let behind = null;
|
|
61
83
|
if (up.ok) {
|
|
62
84
|
const c = await run(root, ['rev-list', '--left-right', '--count', `HEAD...${up.out}`]);
|
|
@@ -64,11 +86,14 @@ async function status(root) {
|
|
|
64
86
|
}
|
|
65
87
|
const mid = ['MERGE_HEAD', 'rebase-merge', 'rebase-apply', 'CHERRY_PICK_HEAD']
|
|
66
88
|
.some((f) => fs.existsSync(path.join(root, '.git', f)));
|
|
89
|
+
const checked = up.ok ? await remoteCheckedAt(root) : null;
|
|
90
|
+
const remoteStale = up.ok && (!checked || Date.now() - checked.getTime() > REMOTE_FRESH_MS);
|
|
67
91
|
const state = mid ? 'mid-operation' : !up.ok ? 'no-upstream'
|
|
68
92
|
: ahead && behind ? 'diverged' : behind ? 'behind' : ahead ? 'ahead'
|
|
69
|
-
: dirtyRecords.length ? 'dirty' : 'in-sync';
|
|
93
|
+
: dirtyRecords.length ? 'dirty' : remoteStale ? 'unchecked' : 'in-sync';
|
|
70
94
|
return { ok: true, branch, upstream: up.ok ? up.out : null, ahead, behind,
|
|
71
95
|
dirty_records: dirtyRecords.length, dirty_other: dirtyOther.length, state,
|
|
96
|
+
remote_checked_at: checked ? checked.toISOString() : null, remote_stale: remoteStale,
|
|
72
97
|
network_used: false };
|
|
73
98
|
}
|
|
74
99
|
|
|
@@ -93,8 +118,9 @@ async function sync(root, { label } = {}) {
|
|
|
93
118
|
const addedAttr = ensureUnionAttribute(root);
|
|
94
119
|
const s1 = await status(root);
|
|
95
120
|
if (s1.dirty_records || addedAttr) {
|
|
96
|
-
|
|
97
|
-
const
|
|
121
|
+
// a document repository records every change; a code repository its record paths only
|
|
122
|
+
const present = profileOf(root) === 'pm-artifacts' ? ['-A'] : ['--', ...RECORD_PATHS.filter((p) => fs.existsSync(path.join(root, p)))];
|
|
123
|
+
const add = await run(root, ['add', ...present]);
|
|
98
124
|
if (!add.ok) return stop('commit', `could not stage records — ${add.err}`);
|
|
99
125
|
const staged = await run(root, ['diff', '--cached', '--name-only']);
|
|
100
126
|
if (staged.out) {
|
|
@@ -129,4 +155,33 @@ async function sync(root, { label } = {}) {
|
|
|
129
155
|
return { ok: true, steps, message: 'in sync', after: await status(root) };
|
|
130
156
|
}
|
|
131
157
|
|
|
132
|
-
|
|
158
|
+
// ── facts the idle auto-sync needs (D70 as amended 2026-10-06). All local: no network. ──────────
|
|
159
|
+
|
|
160
|
+
// The paths this machine's unpushed commits touch, or null when they cannot be read.
|
|
161
|
+
async function unpushedPaths(root, upstream) {
|
|
162
|
+
const d = await run(root, ['diff', '--name-only', `${upstream}...HEAD`]);
|
|
163
|
+
return d.ok ? d.out.split('\n').filter(Boolean) : null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// The newest mtime (ms) among the uncommitted record files, or 0 when none is dirty.
|
|
167
|
+
async function newestDirtyRecordMs(root) {
|
|
168
|
+
const porcelain = await run(root, ['status', '--porcelain', '--untracked-files=all'], { raw: true });
|
|
169
|
+
const files = porcelain.out ? porcelain.out.split('\n').filter(Boolean).map((l) => l.slice(3).replace(/^"|"$/g, '').replace(/.* -> /, '')) : [];
|
|
170
|
+
let newest = 0;
|
|
171
|
+
for (const f of files.filter(recordTestFor(root))) {
|
|
172
|
+
try { newest = Math.max(newest, fs.statSync(path.join(root, f)).mtimeMs); } catch { /* deleted: no time to read */ }
|
|
173
|
+
}
|
|
174
|
+
return newest;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// The remote's default branch as this clone last saw it (refs/remotes/<remote>/HEAD), else main or master if the
|
|
178
|
+
// remote has one. null when none of these is known.
|
|
179
|
+
async function defaultBranchOf(root, remote) {
|
|
180
|
+
const h = await run(root, ['symbolic-ref', '--short', `refs/remotes/${remote}/HEAD`]);
|
|
181
|
+
if (h.ok && h.out.startsWith(`${remote}/`)) return h.out.slice(remote.length + 1);
|
|
182
|
+
for (const b of ['main', 'master']) if ((await run(root, ['rev-parse', '--verify', '--quiet', `refs/remotes/${remote}/${b}`])).ok) return b;
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
module.exports = { status, sync, ensureUnionAttribute, RECORD_PATHS, isRecord, REMOTE_FRESH_MS,
|
|
187
|
+
unpushedPaths, newestDirtyRecordMs, defaultBranchOf, profileOf };
|
package/workspace/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -5,6 +5,17 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
|
6
6
|
<meta name="pmos-nonce" content="" />
|
|
7
7
|
<title>PMOS workspace</title>
|
|
8
|
+
<!-- workspace-phone-app: Add to Home Screen. The manifest is fetched WITH credentials because over the
|
|
9
|
+
tailnet every request needs the session cookie, and a manifest fetch sends none by default. The
|
|
10
|
+
apple-* tags are what iOS reads instead of the manifest. "black" keeps the page below the status
|
|
11
|
+
bar; the top bar does not pad for a translucent one. -->
|
|
12
|
+
<link rel="manifest" href="/manifest.webmanifest" crossorigin="use-credentials" />
|
|
13
|
+
<meta name="theme-color" content="#090b0c" />
|
|
14
|
+
<meta name="mobile-web-app-capable" content="yes" />
|
|
15
|
+
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
16
|
+
<meta name="apple-mobile-web-app-title" content="PMOS" />
|
|
17
|
+
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
|
|
18
|
+
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png" />
|
|
8
19
|
<!-- The terminal's face is preloaded, not left to be discovered. An @font-face is only fetched when
|
|
9
20
|
something first RENDERS in it, which for Plex Mono is about the same moment a tab constructs
|
|
10
21
|
the first xterm — and xterm measures its cell from whatever face is resolved at construction.
|
|
@@ -413,7 +413,7 @@ function readFrame() {
|
|
|
413
413
|
}
|
|
414
414
|
const fr = (() => {
|
|
415
415
|
const s = readFrame();
|
|
416
|
-
return { dest: FRAME_DESTS.includes(s.dest) ? s.dest : 'today', scope: s.scope === 'all' ? 'all' : 'repo', params: {}, dock: {}, syncing: false, reg: null, gAt: 0, needs: 0 };
|
|
416
|
+
return { dest: FRAME_DESTS.includes(s.dest) ? s.dest : 'today', scope: s.scope === 'all' ? 'all' : 'repo', params: {}, dock: {}, dockMax: false, dockW: null, syncing: false, reg: null, gAt: 0, needs: 0 };
|
|
417
417
|
})();
|
|
418
418
|
function writeFrame() {
|
|
419
419
|
try { localStorage.setItem(FRAME_KEY, JSON.stringify({ dest: fr.dest, scope: fr.scope })); } catch { /* private mode: the default stands */ }
|
|
@@ -421,7 +421,7 @@ function writeFrame() {
|
|
|
421
421
|
|
|
422
422
|
const activeRepo = () => (mods.repos && typeof mods.repos.activeProduct === 'function' ? mods.repos.activeProduct() : state.product || null);
|
|
423
423
|
/** The dock on the current destination: what the PM chose here with ⌘J or the rail, else open on Today only. */
|
|
424
|
-
const dockOpen = () => (Object.prototype.hasOwnProperty.call(fr.dock, fr.dest) ? fr.dock[fr.dest] : fr.dest === 'today');
|
|
424
|
+
const dockOpen = () => !!fr.dockMax || (Object.prototype.hasOwnProperty.call(fr.dock, fr.dest) ? fr.dock[fr.dest] : fr.dest === 'today');
|
|
425
425
|
|
|
426
426
|
function go(id, params) {
|
|
427
427
|
if (!FRAME_DESTS.includes(id)) return false;
|
|
@@ -433,10 +433,39 @@ function go(id, params) {
|
|
|
433
433
|
setState({ menuOpen: false, paletteOpen: false });
|
|
434
434
|
return true;
|
|
435
435
|
}
|
|
436
|
-
function setDock(open) { fr.dock[fr.dest] = !!open; setState({}); }
|
|
436
|
+
function setDock(open) { fr.dock[fr.dest] = !!open; if (!open) fr.dockMax = false; setState({}); }
|
|
437
437
|
const toggleDock = () => setDock(!dockOpen());
|
|
438
438
|
const foldDock = () => setDock(false);
|
|
439
439
|
const openDock = () => setDock(true);
|
|
440
|
+
/* Focus (0.53.1): the dock takes the whole window, on every destination, until the PM leaves it. It opens the dock. */
|
|
441
|
+
function toggleFocus() {
|
|
442
|
+
fr.dockMax = !fr.dockMax;
|
|
443
|
+
if (fr.dockMax) fr.dock[fr.dest] = true;
|
|
444
|
+
setState({});
|
|
445
|
+
}
|
|
446
|
+
/* The dock's width, chosen with the grip (0.53.1). Kept in this browser like the dock's other per-window choices;
|
|
447
|
+
* null is the default width. */
|
|
448
|
+
const DOCK_W_KEY = 'pmos.workspace.pm.dock-width';
|
|
449
|
+
function readDockW() {
|
|
450
|
+
if (!FRAME) return null;
|
|
451
|
+
let v = null;
|
|
452
|
+
try { v = Number(localStorage.getItem(DOCK_W_KEY)); } catch { v = null; }
|
|
453
|
+
return Number.isFinite(v) && v > 0 ? v : null;
|
|
454
|
+
}
|
|
455
|
+
const dockWidth = {
|
|
456
|
+
get: () => fr.dockW,
|
|
457
|
+
set: (px) => {
|
|
458
|
+
fr.dockW = Number.isFinite(px) && px > 0 ? Math.round(px) : null;
|
|
459
|
+
try { if (fr.dockW) localStorage.setItem(DOCK_W_KEY, String(fr.dockW)); else localStorage.removeItem(DOCK_W_KEY); } catch { /* private mode */ }
|
|
460
|
+
paintDockWidth();
|
|
461
|
+
},
|
|
462
|
+
};
|
|
463
|
+
function paintDockWidth() {
|
|
464
|
+
const v = fr.dockW ? fr.dockW + 'px' : '';
|
|
465
|
+
if (bodyGrid.style.getPropertyValue('--fr-dock-w') !== v) {
|
|
466
|
+
if (v) bodyGrid.style.setProperty('--fr-dock-w', v); else bodyGrid.style.removeProperty('--fr-dock-w');
|
|
467
|
+
}
|
|
468
|
+
}
|
|
440
469
|
function chooseRepo(name) {
|
|
441
470
|
fr.scope = 'repo';
|
|
442
471
|
writeFrame();
|
|
@@ -480,10 +509,25 @@ async function runSync() {
|
|
|
480
509
|
try { res = await mods.data.syncRepository(repo); } catch (e) { res = { ok: false, message: String((e && e.message) || e) }; }
|
|
481
510
|
fr.syncing = false;
|
|
482
511
|
syncSeen.delete(repo);
|
|
483
|
-
toast({ text: res && res.ok ? FC.sync.done : FC.sync.stopped((res && res.step) || FC.sync.label, (res && (res.message || res.error)) || '') });
|
|
512
|
+
toast({ text: res && res.ok ? FC.sync.done : FC.sync.stopped((res && (res.stopped_at || res.step)) || FC.sync.label, (res && (res.message || res.error)) || '') });
|
|
484
513
|
setState({});
|
|
485
514
|
}
|
|
486
515
|
|
|
516
|
+
/* Closing the tab with work that is not yet pushed asks first (D70 as amended 2026-10-06): idle auto-sync waits for a
|
|
517
|
+
* few quiet minutes, and a laptop closed inside that window would carry the work away unsynced. The reading is the
|
|
518
|
+
* last local one for each repository; a stale or unread reading never asks. */
|
|
519
|
+
function unpushedOf(st) {
|
|
520
|
+
// only a repository with an upstream: one with none has nowhere to push, and would ask on every close for ever
|
|
521
|
+
return !!st && st.ok !== false && !!st.upstream && ((st.ahead | 0) > 0 || (st.dirty_records | 0) > 0);
|
|
522
|
+
}
|
|
523
|
+
if (FRAME) {
|
|
524
|
+
window.addEventListener('beforeunload', (e) => {
|
|
525
|
+
if (![...syncSeen.values()].some((v) => unpushedOf(v.st))) return;
|
|
526
|
+
e.preventDefault();
|
|
527
|
+
e.returnValue = '';
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
|
|
487
531
|
let bar = null;
|
|
488
532
|
let dockRail = null;
|
|
489
533
|
let frDetail = null;
|
|
@@ -498,7 +542,8 @@ function setupFrame() {
|
|
|
498
542
|
ensureSprite();
|
|
499
543
|
bar = createTopbar({ go, openPalette, repoMenu, sync: runSync });
|
|
500
544
|
document.addEventListener('pointerdown', (e) => { fr.downAt = e.target; }, true);
|
|
501
|
-
dockRail = createDockRail({ open: openDock });
|
|
545
|
+
dockRail = createDockRail({ open: openDock, resize: dockWidth });
|
|
546
|
+
fr.dockW = readDockW();
|
|
502
547
|
frDetail = el('div', { class: 'fr-detail', 'data-fr': 'detail' },
|
|
503
548
|
el('div', { class: 'fr-detail__bar' }, iconButton({ icon: 'x', ariaLabel: FC.detail.close, onClick: closeDetail, attrs: { 'data-fr': 'detail-close' } })),
|
|
504
549
|
hostDetail);
|
|
@@ -546,7 +591,8 @@ function renderFrame() {
|
|
|
546
591
|
fr.unsynced = unsyncedOf(st); // the phone bar (mobile.js) shows Sync on the same reading
|
|
547
592
|
bar.update({ dest: fr.dest, repoLabel: fr.scope === 'all' ? FC.repo.all : repo, needs, unsynced: fr.unsynced, syncing: fr.syncing });
|
|
548
593
|
const open = dockOpen();
|
|
549
|
-
bodyGrid.setAttribute('data-fr-dock', phone ? 'phone' : open ? 'open' : 'rail');
|
|
594
|
+
bodyGrid.setAttribute('data-fr-dock', phone ? 'phone' : open ? (fr.dockMax ? 'max' : 'open') : 'rail');
|
|
595
|
+
paintDockWidth();
|
|
550
596
|
const detail = !!state.col2;
|
|
551
597
|
// Four children in every arrangement, hostTabs the fourth and last, so setChildren never moves it (a moved xterm loses
|
|
552
598
|
// its grid) and a reader of the body's fourth child still finds panel 4. A closed detail stays mounted, hidden.
|
|
@@ -584,7 +630,7 @@ const G_KEYS = { t: 'today', i: 'initiatives', b: 'board', r: 'reports' };
|
|
|
584
630
|
function frameKey(e) {
|
|
585
631
|
const mod = e.metaKey || e.ctrlKey;
|
|
586
632
|
if (mod && (e.key === 'k' || e.key === 'K')) { e.preventDefault(); setState({ paletteOpen: !state.paletteOpen, menuOpen: false }); return; }
|
|
587
|
-
if (mod && (e.key === 'j' || e.key === 'J')) { e.preventDefault(); if (!isMobile()) toggleDock(); return; }
|
|
633
|
+
if (mod && (e.key === 'j' || e.key === 'J')) { e.preventDefault(); if (!isMobile()) { if (e.shiftKey) toggleFocus(); else toggleDock(); } return; }
|
|
588
634
|
if (e.key === 'Escape') {
|
|
589
635
|
if (state.paletteOpen) { setState({ paletteOpen: false }); return; }
|
|
590
636
|
if (state.menuOpen) { setState({ menuOpen: false }); return; }
|
|
@@ -635,9 +681,9 @@ function frameKey(e) {
|
|
|
635
681
|
|
|
636
682
|
/* What the suites and the phone chrome reach the frame by. */
|
|
637
683
|
const frameApi = {
|
|
638
|
-
on: true, go, toggleDock, foldDock, openDock, chooseRepo, chooseAll, openPalette, repoMenu, closeDetail, sync: runSync,
|
|
684
|
+
on: true, go, toggleDock, foldDock, openDock, toggleFocus, dockWidth, chooseRepo, chooseAll, openPalette, repoMenu, closeDetail, sync: runSync,
|
|
639
685
|
get unsynced() { return !!fr.unsynced; },
|
|
640
|
-
get dest() { return fr.dest; }, get scope() { return fr.scope; }, get dockOpen() { return dockOpen(); },
|
|
686
|
+
get dest() { return fr.dest; }, get scope() { return fr.scope; }, get dockOpen() { return dockOpen(); }, get dockMax() { return !!fr.dockMax; },
|
|
641
687
|
get detailOpen() { return !!state.col2; }, get syncing() { return fr.syncing; }, get needs() { return fr.needs || 0; },
|
|
642
688
|
};
|
|
643
689
|
|