create-pmos 0.15.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 (36) hide show
  1. package/README.md +38 -0
  2. package/package.json +1 -1
  3. package/payload/KIT-VERSION +1 -1
  4. package/workspace/README.md +44 -12
  5. package/workspace/design-parity.js +358 -0
  6. package/workspace/lib/find-browser.js +60 -0
  7. package/workspace/lib/four-panel.js +345 -0
  8. package/workspace/lib/git-info.js +28 -1
  9. package/workspace/lib/mobile-nav.js +32 -2
  10. package/workspace/package-lock.json +29 -2
  11. package/workspace/package.json +13 -5
  12. package/workspace/public/index.html +79 -409
  13. package/workspace/public/pm/app.js +242 -0
  14. package/workspace/public/pm/data.js +877 -0
  15. package/workspace/public/pm/dom.js +116 -0
  16. package/workspace/public/pm/fixture.js +979 -0
  17. package/workspace/public/pm/mobile.css +200 -0
  18. package/workspace/public/pm/mobile.js +403 -0
  19. package/workspace/public/pm/modes.js +1076 -0
  20. package/workspace/public/pm/panel-detail.js +648 -0
  21. package/workspace/public/pm/panel-initiatives.js +426 -0
  22. package/workspace/public/pm/panel-repos.js +460 -0
  23. package/workspace/public/pm/panel-tabs.js +487 -0
  24. package/workspace/public/pm/state.js +164 -0
  25. package/workspace/public/pm/tab-doc.js +279 -0
  26. package/workspace/public/pm/tab-task.js +83 -0
  27. package/workspace/public/pm/tab-term.js +560 -0
  28. package/workspace/public/pm/tokens.css +73 -0
  29. package/workspace/public/vendor/ibm-plex-mono-500.woff2 +0 -0
  30. package/workspace/server.js +158 -8
  31. package/workspace/focus.test.js +0 -650
  32. package/workspace/handoff.test.js +0 -443
  33. package/workspace/profiles.test.js +0 -362
  34. package/workspace/public/app.js +0 -5560
  35. package/workspace/public/styles.css +0 -2258
  36. package/workspace/repo-scan.test.js +0 -327
@@ -0,0 +1,242 @@
1
+ /* pm/app.js — the shell of the four-panel workspace, and its render loop.
2
+ *
3
+ * Owns: the root grid, the 44px top bar, the 26px status bar, the body grid's four tracks, the
4
+ * three collapsed rails, and the keyboard (⌘K / Esc). Everything inside a panel belongs to that
5
+ * panel's module.
6
+ *
7
+ * ── The host contract, for the panel modules ────────────────────────────────────────────────
8
+ * app.js creates ONE persistent host element per panel and hands the SAME element to that panel's
9
+ * renderer on every render:
10
+ *
11
+ * renderRepos(host) renderInitiatives(host) renderDetail(host) renderTabs(host)
12
+ *
13
+ * The host IS the prototype's open-panel container — app.js has already set its verbatim `style`
14
+ * and it is the direct grid child. A renderer therefore clears the host and appends the panel's
15
+ * CONTENTS; it must not wrap them in another div, because an extra layer between the grid and the
16
+ * panel body changes the geometry the comparator measures. The host is persistent on purpose:
17
+ * panel 4 holds a live xterm, and re-parenting a terminal on every unrelated state change is the
18
+ * quiet PTY regression the PRD warns about.
19
+ *
20
+ * A panel module that does not exist yet is not an error — its host renders empty and the rest of
21
+ * the shell still comes up.
22
+ */
23
+
24
+ import { el, clear, setChildren } from './dom.js';
25
+ import { state, setState, subscribe, flush, FIXTURE } from './state.js';
26
+ /* The phone surface (≤760px). It owns its own chrome and decides which single panel is visible;
27
+ * the shell's only part in it is to keep all four hosts mounted below the breakpoint and to hand
28
+ * the module a repaint. See pm/mobile.js. */
29
+ import { isMobile, mountMobile, applyMobile } from './mobile.js';
30
+
31
+ /* ── derived labels (the prototype's two title maps, lines 1409 / 1699) ─────────────────────── */
32
+
33
+ const MODE_TITLE = {
34
+ product: 'initiatives', scheduled: 'scheduled tasks', workflow: 'workflows', skills: 'skills',
35
+ knowledge: 'knowledge', agent: 'agents', memory: 'memory', connect: 'connections', focus: 'focus',
36
+ };
37
+ const DETAIL_TITLE = {
38
+ init: 'initiative', flow: 'workflow', sched: 'scheduled task', skill: 'skill', know: 'decision',
39
+ agent: 'agent', mem: 'memory', prof: 'claude profile', intg: 'connection',
40
+ addprod: 'add product', new: 'file new',
41
+ };
42
+
43
+ const modeTitle = () => MODE_TITLE[state.mode] || 'initiatives';
44
+ const detailTitle = () => DETAIL_TITLE[(state.detail && state.detail.kind) || ''] || 'detail';
45
+
46
+ const gridCols = () =>
47
+ (state.col0 ? '210px' : '36px') + ' ' +
48
+ (state.col1 ? '280px' : '36px') + ' ' +
49
+ (state.col2 ? 'minmax(420px,560px)' : '36px') + ' minmax(0,1fr)';
50
+
51
+ /* ── running-session count for the status bar ───────────────────────────────────────────────
52
+ * The count is over every session of every initiative in every repository, seeded plus started
53
+ * (`extraSess`), whose state is `working` or `needs`, skipping records that failed to read. That
54
+ * rule lives with the data — an agent's `sessions` list and an initiative's `sessions` list have
55
+ * the same shape, so anything that counts by shape rather than by record counts the wrong things.
56
+ * Order of preference:
57
+ * 1. `state.runningCount`, if the data layer has published one;
58
+ * 2. `data.js`'s `runningCount(state)`;
59
+ * 3. `fixture.js`'s `countRunning(state)` — fixture mode's own derivation;
60
+ * 4. otherwise only the sessions started in this window.
61
+ */
62
+ function runningCount() {
63
+ if (typeof state.runningCount === 'number') return state.runningCount;
64
+ if (mods.data && typeof mods.data.runningCount === 'function') {
65
+ // Only a NUMBER counts as an answer. A source that declines returns null, and the chain
66
+ // continues — taking `null` as an answer is how the data layer's fixture-mode decline used to
67
+ // render as "null sessions running", and taking a 0 it did not mean is how it rendered "0".
68
+ try {
69
+ const n = mods.data.runningCount(state);
70
+ if (typeof n === 'number') return n;
71
+ } catch { /* fall through */ }
72
+ }
73
+ if (FIXTURE && mods.fixture && typeof mods.fixture.countRunning === 'function') {
74
+ try { return mods.fixture.countRunning(state); } catch { /* fall through */ }
75
+ }
76
+ let n = 0;
77
+ for (const k in state.extraSess) {
78
+ const list = state.extraSess[k];
79
+ if (Array.isArray(list)) for (const s of list) if (s && (s.st === 'working' || s.st === 'needs')) n++;
80
+ }
81
+ return n;
82
+ }
83
+
84
+ /* ── the shell, built once ─────────────────────────────────────────────────────────────────── */
85
+
86
+ const root = el('div', {
87
+ 'data-pm-theme': state.theme,
88
+ style: 'display:grid;grid-template-rows:44px 1fr 26px;height:100vh;background:var(--background);color:var(--foreground);font-family:var(--sans);font-size:13px;line-height:1.5',
89
+ });
90
+
91
+ const themeBtn = el('button', {
92
+ onClick: () => setState((s) => ({ theme: s.theme === 'dark' ? 'light' : 'dark' })),
93
+ style: 'margin-left:auto;font-size:12px;color:oklch(0.85 0.01 214);padding:5px 12px;background:oklch(1 0 0 / 6%);border:1px solid oklch(1 0 0 / 12%);min-height:30px',
94
+ hover: 'border-color:oklch(0.56 0.021 213.5)',
95
+ }, 'theme · dark');
96
+
97
+ const topBar = el('div', {
98
+ style: 'display:flex;align-items:center;gap:12px;padding:0 12px;background:oklch(0.1 0.004 228.8);color:oklch(0.987 0.002 197.1);border-bottom:1px solid oklch(1 0 0 / 10%)',
99
+ },
100
+ el('span', { style: 'display:grid;place-items:center;width:24px;height:24px;background:oklch(0.488 0.243 264.376);font-size:14px;line-height:1' }, '◎'),
101
+ el('span', { style: 'font-weight:600;letter-spacing:.02em' }, 'PMOS'),
102
+ el('span', { style: 'color:oklch(0.723 0.014 214.4);font-size:12px' }, 'PM Operating System · workspace'),
103
+ el('span', { style: 'display:flex;gap:6px;align-items:baseline;font-family:var(--mono);font-size:11px;color:oklch(0.76 0.17 152)' },
104
+ el('span', { style: 'font-size:9px' }, '●'), 'LIVE · local'),
105
+ themeBtn,
106
+ );
107
+
108
+ const statusLeft = el('span', null, 'local · persistent server · 0 sessions running');
109
+ const statusBar = el('div', {
110
+ style: 'display:flex;align-items:center;padding:0 12px;background:oklch(0.1 0.004 228.8);color:oklch(0.6 0.02 213);font-size:11px;border-top:1px solid oklch(1 0 0 / 10%)',
111
+ },
112
+ statusLeft,
113
+ el('span', { style: 'margin-left:auto' }, 'sessions survive detach · ⌘K search · Esc closes tab'),
114
+ );
115
+
116
+ const bodyGrid = el('div', { style: 'display:grid;grid-template-columns:' + gridCols() + ';min-height:0' });
117
+
118
+ /** The collapsed 36px strip: a full-height button with a rotated vertical-rl label. */
119
+ function makeRail(flag) {
120
+ const label = el('span', { style: 'writing-mode:vertical-rl;font-size:11px;letter-spacing:.1em;color:var(--muted-foreground);font-family:var(--mono)' });
121
+ const btn = el('button', {
122
+ onClick: () => setState((s) => ({ [flag]: !s[flag] })),
123
+ style: 'background:var(--sidebar);border-right:1px solid var(--border);display:flex;justify-content:center;padding-top:14px',
124
+ hover: 'background:var(--accent)',
125
+ }, label);
126
+ return { btn, label };
127
+ }
128
+
129
+ const rail0 = makeRail('col0');
130
+ const rail1 = makeRail('col1');
131
+ const rail2 = makeRail('col2');
132
+ rail0.label.textContent = 'repositories ⟩';
133
+
134
+ const hostRepos = el('div', { style: 'background:var(--sidebar);border-right:1px solid var(--border);display:flex;flex-direction:column;min-width:0' });
135
+ const hostInits = el('div', { style: 'background:var(--sidebar);border-right:1px solid var(--border);overflow-y:auto;min-width:0' });
136
+ const hostDetail = el('div', { style: 'background:var(--sidebar);border-right:1px solid var(--border);overflow-y:auto;min-width:0;display:flex;flex-direction:column' });
137
+ const hostTabs = el('div', { style: 'background:var(--term-bg);display:flex;flex-direction:column;min-width:0' });
138
+
139
+ setChildren(root, [topBar, bodyGrid, statusBar]);
140
+
141
+ /* ── panel modules, loaded at boot and tolerated when absent ────────────────────────────────── */
142
+
143
+ const mods = { repos: null, inits: null, detail: null, tabs: null, data: null, fixture: null };
144
+
145
+ async function loadModules() {
146
+ const want = [
147
+ ['repos', './panel-repos.js'], ['inits', './panel-initiatives.js'],
148
+ ['detail', './panel-detail.js'], ['tabs', './panel-tabs.js'],
149
+ ['data', './data.js'], ['fixture', './fixture.js'],
150
+ ];
151
+ await Promise.all(want.map(async ([key, path]) => {
152
+ try { mods[key] = await import(path); } catch { mods[key] = null; }
153
+ }));
154
+ }
155
+
156
+ /** Call a panel renderer, if it exists, without letting one panel take down the shell. */
157
+ function paint(mod, name, host) {
158
+ const fn = mod && mod[name];
159
+ if (typeof fn !== 'function') { clear(host); return; }
160
+ try { fn(host); } catch (e) { console.error('[pm] ' + name + ' failed:', e); }
161
+ }
162
+
163
+ /* ── render ────────────────────────────────────────────────────────────────────────────────── */
164
+
165
+ function render() {
166
+ root.setAttribute('data-pm-theme', state.theme);
167
+ themeBtn.textContent = state.theme === 'dark' ? 'theme · dark' : 'theme · light';
168
+ statusLeft.textContent = 'local · persistent server · ' + runningCount() + ' sessions running';
169
+
170
+ // Panel 1's rail is a constant; panels 2 and 3 name whatever is behind them, so a PM who folded
171
+ // a panel away can still see what they folded.
172
+ rail1.label.textContent = modeTitle() + ' ⟩';
173
+ rail2.label.textContent = detailTitle() + ' ⟩';
174
+
175
+ bodyGrid.setAttribute('style', 'display:grid;grid-template-columns:' + gridCols() + ';min-height:0');
176
+
177
+ // On a phone there are no rails to collapse into and no second column to fold away: the four
178
+ // hosts all stay mounted, one is DISPLAYED (pm/mobile.css picks it from `data-mpanel`), and the
179
+ // PM's desktop arrangement — col0/col1/col2 — is left exactly as they set it. Mounting all four
180
+ // is also what keeps a live terminal alive: a host dropped from this list would be removed from
181
+ // the document, and panel 4's xterm does not survive that.
182
+ const phone = isMobile();
183
+ setChildren(bodyGrid, phone ? [hostRepos, hostInits, hostDetail, hostTabs] : [
184
+ state.col0 ? hostRepos : rail0.btn,
185
+ state.col1 ? hostInits : rail1.btn,
186
+ state.col2 ? hostDetail : rail2.btn,
187
+ hostTabs,
188
+ ]);
189
+
190
+ if (phone || state.col0) paint(mods.repos, 'renderRepos', hostRepos);
191
+ if (phone || state.col1) paint(mods.inits, 'renderInitiatives', hostInits);
192
+ if (phone || state.col2) paint(mods.detail, 'renderDetail', hostDetail);
193
+ paint(mods.tabs, 'renderTabs', hostTabs);
194
+
195
+ applyMobile();
196
+ }
197
+
198
+ /* ── keyboard ──────────────────────────────────────────────────────────────────────────────── */
199
+
200
+ function focusSearch() {
201
+ const input = document.querySelector('[data-pm-search]')
202
+ || document.querySelector('input[placeholder="search everything…"]');
203
+ if (input) input.focus();
204
+ }
205
+
206
+ function onKeyDown(e) {
207
+ if ((e.metaKey || e.ctrlKey) && (e.key === 'k' || e.key === 'K')) {
208
+ e.preventDefault();
209
+ setState({ col1: true }, focusSearch);
210
+ return;
211
+ }
212
+ if (e.key === 'Escape' && !/INPUT|TEXTAREA/.test(e.target && e.target.tagName)) {
213
+ // Verbatim from the prototype: Esc closes the ACTIVE tab, the last remaining tab takes over,
214
+ // and a split pointing at the closed tab is dropped rather than left dangling.
215
+ setState((s) => {
216
+ if (!s.active) return null;
217
+ const rest = s.tabs.filter((t) => t.id !== s.active);
218
+ return {
219
+ tabs: rest,
220
+ active: rest.length ? rest[rest.length - 1].id : null,
221
+ splitId: s.splitId === s.active ? null : s.splitId,
222
+ };
223
+ });
224
+ }
225
+ }
226
+
227
+ /* ── boot ──────────────────────────────────────────────────────────────────────────────────── */
228
+
229
+ async function boot() {
230
+ document.body.appendChild(root);
231
+ subscribe(render);
232
+ window.addEventListener('keydown', onKeyDown);
233
+ mountMobile({ bodyGrid, render }); // wires the phone chrome; inert above 760px
234
+ render(); // the shell is on screen before any panel module has resolved
235
+ await loadModules();
236
+ render();
237
+ }
238
+
239
+ /* A tiny surface for the browser suite and for a panel module that needs to force a paint. */
240
+ window.pmApp = { root, render, state, setState, flush, fixture: FIXTURE, mods, hosts: { hostRepos, hostInits, hostDetail, hostTabs } };
241
+
242
+ boot();