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,279 @@
1
+ /* pm/tab-doc.js — the document tab body: view · raw · edit · diff, with save, revert and a dirty flag.
2
+ *
3
+ * A port of the prototype's doc pane (template lines 869–927, derivation 2020–2045). The four modes
4
+ * are the whole design: `view` renders the markdown, `raw` shows the file tinted by line kind,
5
+ * `edit` is the buffer, and `diff` is a real LCS line diff of the buffer against what is on disk.
6
+ * The diff is COMPUTED, never narrated — a "2 sections differ" that nothing read is the watermelon
7
+ * this repo has a word for, and the same rule that governs artifact sync governs this pane.
8
+ *
9
+ * ── Where the text comes from ───────────────────────────────────────────────────────────────
10
+ * 1. `savedDocs[path]` — saved in this window, so it is what the pane must show;
11
+ * 2. fixture mode → the prototype's seed documents (`fixture.js`, `rawFor`);
12
+ * 3. real mode → the file on disk, read once through the local server (`/api/doc`) and cached
13
+ * by path. Nothing is fetched from anywhere but this machine.
14
+ * A file that cannot be read says so, in the pane, with the reason — never a blank page.
15
+ *
16
+ * ── Why the textarea outlives the render ────────────────────────────────────────────────────
17
+ * Every keystroke is a state change, and a state change repaints the panel. A textarea rebuilt on
18
+ * each keystroke loses the caret, so the element is created once per tab and kept; its text is
19
+ * written back only when it actually differs from what the box already holds.
20
+ */
21
+
22
+ import { el, setChildren } from './dom.js';
23
+ import { state, setState, FIXTURE } from './state.js';
24
+ import * as FX from './fixture.js';
25
+
26
+ /** `{{ expr }}` — one interpolated value in its own span, as the prototype's runtime emits it. */
27
+ const iv = (v) => el('span', null, v === null || v === undefined ? '' : String(v));
28
+
29
+ /* ── reading ───────────────────────────────────────────────────────────────────────────────── */
30
+
31
+ const disk = new Map(); // path -> text read from this machine
32
+ const pending = new Set(); // paths whose read is in flight
33
+ const saveErr = new Map(); // path -> why the last save was refused
34
+
35
+ let dataMod = null;
36
+ import('./data.js').then((m) => { dataMod = m; }).catch(() => { dataMod = null; });
37
+
38
+ /** The workspace id the doc endpoints key on. The data layer names it; these are its aliases. */
39
+ const wsId = () => state.wsId || state.ws || state.product || '';
40
+
41
+ /* A REFERENCE is not a file. Panel 3's links section files `jira://ATL-501`, `confluence://PM/x`,
42
+ * `gdoc://x-brief` — URIs naming a record in someone else's system, deliberately kept out of the
43
+ * repo (AC5). Opening one used to be read as a path and sent to `/api/doc`, which answered 404 on
44
+ * every one of them: a request that cannot succeed, made on every click, logged as a page error and
45
+ * shown to the PM as "this file could not be read". PMOS holds no credential for any of those
46
+ * systems, so the honest pane says what the reference IS and that nothing was read from it.
47
+ * Anything with a scheme is a reference; a repo path never has one. */
48
+ const REFERENCE_URI = /^[a-z][a-z0-9+.-]*:\/\//i;
49
+ const isReference = (p) => REFERENCE_URI.test(String(p || ''));
50
+
51
+ /** The name of the system a reference scheme belongs to; the scheme itself when it is not one we file. */
52
+ const SYSTEM_OF = { jira: 'Jira', confluence: 'Confluence', gdoc: 'Google Docs', google: 'Google Docs' };
53
+
54
+ const referenceStub = (tab) => {
55
+ const scheme = String(tab.path).split('://')[0].toLowerCase();
56
+ const system = SYSTEM_OF[scheme] || scheme;
57
+ return '---\npath: ' + tab.path + '\nrole: ' + tab.role + '\ninitiative: ' + (tab.slug || '—')
58
+ + '\n---\n\n# ' + String(tab.path).split('://').pop()
59
+ + '\n\nThis is a REFERENCE, not a file in this repository: it names a record in ' + system + '.'
60
+ + '\n\nNothing was read and nothing was contacted — PMOS holds no credential for ' + system + '. '
61
+ + 'The reference is recorded so this initiative says where the work is; open it in ' + system
62
+ + ' to read it.\n';
63
+ };
64
+
65
+ const stub = (tab, body) =>
66
+ '---\npath: ' + tab.path + '\nrole: ' + tab.role + '\ninitiative: ' + (tab.slug || '—')
67
+ + '\n---\n\n# ' + String(tab.path).split('/').pop() + '\n\n' + body;
68
+
69
+ /** Read a document from the local server, once, then repaint. */
70
+ function requestDoc(tab) {
71
+ const path = tab.path;
72
+ if (pending.has(path)) return;
73
+ pending.add(path);
74
+ const done = (text) => { pending.delete(path); disk.set(path, text); setState({}); };
75
+ const load = (dataMod && typeof dataMod.readDoc === 'function')
76
+ ? dataMod.readDoc(path)
77
+ : fetch('/api/doc?ws=' + encodeURIComponent(wsId()) + '&path=' + encodeURIComponent(path))
78
+ .then((r) => (r.ok ? r.text() : Promise.reject(new Error('HTTP ' + r.status))));
79
+ Promise.resolve(load).then(
80
+ (text) => done(typeof text === 'string' ? text : String(text == null ? '' : text)),
81
+ (e) => done(stub(tab, 'This file could not be read: ' + (e && e.message ? e.message : e)
82
+ + '\n\nNothing was rendered from it. That is the failure being stated rather than an empty page shown.')),
83
+ );
84
+ }
85
+
86
+ /**
87
+ * The text on disk for this tab — the baseline every diff and the dirty flag are measured against.
88
+ * Exported because panel 4's `copy` button carries the same text.
89
+ */
90
+ export function docText(tab) {
91
+ if (!tab || !tab.path) return '';
92
+ const saved = state.savedDocs[tab.path];
93
+ if (saved != null) return saved;
94
+ if (FIXTURE) return FX.rawFor(tab, state.savedDocs);
95
+ if (isReference(tab.path)) return referenceStub(tab);
96
+ if (disk.has(tab.path)) return disk.get(tab.path);
97
+ requestDoc(tab);
98
+ return stub(tab, 'reading ' + tab.path + ' …');
99
+ }
100
+
101
+ /* ── writing ───────────────────────────────────────────────────────────────────────────────── */
102
+
103
+ /**
104
+ * Save (line 2039). In fixture mode the buffer simply becomes the saved text — there is no disk to
105
+ * write to and pretending otherwise would be a fiction. In real mode the write goes to the local
106
+ * server with the base it was opened from, so a file that changed underneath is REFUSED rather
107
+ * than silently overwritten; the refusal is shown where the PM is looking and the buffer is kept.
108
+ */
109
+ function save(tab, text) {
110
+ const path = tab.path;
111
+ const commit = () => setState((s) => {
112
+ const de = { ...s.docEdits }; delete de[path];
113
+ return { savedDocs: { ...s.savedDocs, [path]: text }, docEdits: de };
114
+ });
115
+ saveErr.delete(path);
116
+ if (FIXTURE) { commit(); return; }
117
+
118
+ const base = disk.has(path) ? disk.get(path) : (state.savedDocs[path] ?? '');
119
+ const write = (dataMod && typeof dataMod.saveDoc === 'function')
120
+ ? dataMod.saveDoc({ ws: wsId(), path, base, text })
121
+ : fetch('/api/doc/save', {
122
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
123
+ body: JSON.stringify({ ws: wsId(), path, base, text }),
124
+ }).then((r) => r.json());
125
+ Promise.resolve(write).then(
126
+ (out) => {
127
+ if (out && out.ok === false) { saveErr.set(path, out.error || 'refused'); setState({}); return; }
128
+ disk.set(path, text);
129
+ commit();
130
+ },
131
+ (e) => { saveErr.set(path, (e && e.message) || 'could not reach the workspace server'); setState({}); },
132
+ );
133
+ }
134
+
135
+ /** Revert (line 2043): drop the buffer, keep the file. */
136
+ function revert(tab) {
137
+ saveErr.delete(tab.path);
138
+ setState((s) => { const de = { ...s.docEdits }; delete de[tab.path]; return { docEdits: de }; });
139
+ }
140
+
141
+ /* ── the four bodies ───────────────────────────────────────────────────────────────────────── */
142
+
143
+ const FM_K = 'font-size:10.5px;color:var(--muted-foreground);padding-top:2px;font-family:var(--mono)';
144
+ const FM_V = 'overflow-wrap:anywhere;font-family:var(--mono);font-size:11.5px';
145
+
146
+ function viewBody(text) {
147
+ const doc = FX.mdParse(text);
148
+ const fm = [];
149
+ doc.fm.forEach((fp) => { fm.push(el('span', { style: FM_K }, iv(fp.k)), el('span', { style: FM_V }, iv(fp.v))); });
150
+ return el('div', {
151
+ style: 'flex:1;overflow-y:auto;background:var(--background);color:var(--foreground);padding:20px 24px 60px;font-size:14px;line-height:1.65',
152
+ },
153
+ el('div', { style: 'max-width:72ch' },
154
+ el('div', {
155
+ style: 'border:1px solid var(--border);background:var(--card);padding:10px 12px;margin-bottom:18px;display:grid;grid-template-columns:auto 1fr;gap:4px 14px;font-size:12.5px',
156
+ }, fm),
157
+ el('h1', {
158
+ style: 'font-size:21px;font-weight:600;line-height:1.3;margin:4px 0 12px;text-wrap:balance;font-family:var(--sans)',
159
+ }, iv(doc.title)),
160
+ doc.blocks.map((bk) => el('div', null,
161
+ bk.isH ? el('h2', {
162
+ style: 'font-size:16px;font-weight:600;line-height:1.35;margin:24px 0 8px;padding-bottom:5px;border-bottom:1px solid var(--border)',
163
+ }, iv(bk.h)) : null,
164
+ bk.isP ? el('p', { style: 'margin:0 0 12px' }, iv(bk.p)) : null,
165
+ bk.isList ? el('ul', { style: 'margin:0 0 12px;padding-left:22px' },
166
+ bk.items.map((li) => el('li', { style: 'margin:0 0 5px;padding-left:2px' }, iv(li)))) : null,
167
+ )),
168
+ ),
169
+ );
170
+ }
171
+
172
+ function rawBody(text) {
173
+ return el('div', {
174
+ style: 'flex:1;overflow-y:auto;background:var(--background);padding:14px 16px;font-family:var(--mono);font-size:12px;line-height:1.7',
175
+ }, text.split('\n').map((l) => {
176
+ const ln = FX.rawLine(l);
177
+ return el('div', {
178
+ style: 'color:' + ln.c + ';font-weight:' + ln.w + ';white-space:pre-wrap;overflow-wrap:anywhere;min-height:1.2em',
179
+ }, iv(ln.t));
180
+ }));
181
+ }
182
+
183
+ function diffBody(raw, text, dirty) {
184
+ const rows = dirty ? FX.diffLines(raw, text) : [FX.NO_DIFF_ROW];
185
+ return el('div', {
186
+ style: 'flex:1;overflow-y:auto;background:var(--background);padding:14px 0;font-family:var(--mono);font-size:12px;line-height:1.7',
187
+ }, rows.map((dr) => el('div', {
188
+ style: 'color:' + dr.c + ';background:' + dr.bg + ';padding:0 16px;white-space:pre-wrap;overflow-wrap:anywhere;min-height:1.2em',
189
+ }, iv(dr.t))));
190
+ }
191
+
192
+ /* ── per-tab state that is DOM, not data ───────────────────────────────────────────────────── */
193
+
194
+ const boxes = new Map(); // tab id -> the persistent textarea for that tab
195
+
196
+ function editBox(tab) {
197
+ let ta = boxes.get(tab.id);
198
+ if (ta) return ta;
199
+ ta = el('textarea', {
200
+ style: 'flex:1;background:var(--background);color:var(--foreground);padding:14px 16px;font-family:var(--mono);font-size:12px;line-height:1.7',
201
+ onInput: (e) => {
202
+ const v = e.target.value;
203
+ saveErr.delete(tab.path);
204
+ setState((s) => ({ docEdits: { ...s.docEdits, [tab.path]: v } }));
205
+ },
206
+ });
207
+ ta.spellcheck = false; // a property, not an attribute — the prototype renders no attribute
208
+ boxes.set(tab.id, ta);
209
+ return ta;
210
+ }
211
+
212
+ /** Release what a closed doc tab held. Called by panel-tabs.js when the tab goes away. */
213
+ export function disposeDoc(id) {
214
+ boxes.delete(id);
215
+ }
216
+
217
+ /* ── mount ─────────────────────────────────────────────────────────────────────────────────── */
218
+
219
+ /**
220
+ * Render the doc pane for `tab` into `host` — the pane element panel 4 keeps for this tab.
221
+ * Everything but the edit box is rebuilt on each paint; the box is kept so typing survives it.
222
+ */
223
+ export function mountDoc(host, tab) {
224
+ const raw = docText(tab);
225
+ const text = state.docEdits[tab.path] ?? raw;
226
+ const dm = state.docModes[tab.id] ?? 'view';
227
+ const dirty = text !== raw;
228
+ const err = saveErr.get(tab.path);
229
+
230
+ const modeBtns = ['view', 'raw', 'edit', 'diff'].map((mm) => el('button', {
231
+ onClick: () => setState((s) => ({ docModes: { ...s.docModes, [tab.id]: mm } })),
232
+ style: 'font-family:var(--mono);font-size:10.5px;padding:4px 10px;min-height:26px;background:'
233
+ + (dm === mm ? 'var(--secondary)' : 'transparent')
234
+ + ';color:' + (dm === mm ? 'var(--foreground)' : 'var(--muted-foreground)')
235
+ + ';border:1px solid ' + (dm === mm ? 'var(--input)' : 'transparent'),
236
+ hover: 'border-color:var(--ring)',
237
+ }, iv(mm)));
238
+
239
+ const saveBtn = dirty ? el('button', {
240
+ onClick: () => save(tab, text),
241
+ style: 'margin-left:8px;font-family:var(--mono);font-size:10.5px;padding:4px 10px;min-height:26px;background:var(--brand);color:oklch(0.987 0.002 197.1);font-weight:500',
242
+ }, 'save') : null;
243
+
244
+ const revertBtn = dirty ? el('button', {
245
+ onClick: () => revert(tab),
246
+ style: 'font-family:var(--mono);font-size:10.5px;padding:4px 10px;min-height:26px;background:var(--secondary);border:1px solid var(--border);color:var(--muted-foreground)',
247
+ hover: 'border-color:var(--ring)',
248
+ }, 'revert') : null;
249
+
250
+ // The one place a real-mode refusal is reported: the same line that carries the dirty flag, so
251
+ // a save that did not happen cannot read as one that did.
252
+ const dirtyLabel = err ? 'not saved — ' + err : (dirty ? '● modified — unsaved' : 'saved on disk');
253
+ const dirtyColor = err ? 'var(--destructive)' : (dirty ? 'var(--warning)' : 'var(--muted-foreground)');
254
+
255
+ const modeBar = el('div', {
256
+ style: 'display:flex;gap:2px;align-items:center;padding:6px 10px;border-bottom:1px solid var(--border);background:var(--background)',
257
+ },
258
+ modeBtns, saveBtn, revertBtn,
259
+ el('span', { style: 'margin-left:auto;font-family:var(--mono);font-size:10px;color:' + dirtyColor }, iv(dirtyLabel)),
260
+ );
261
+
262
+ let body;
263
+ if (dm === 'raw') body = rawBody(text);
264
+ else if (dm === 'diff') body = diffBody(raw, text, dirty);
265
+ else if (dm === 'edit') {
266
+ body = editBox(tab);
267
+ /* `defaultValue` IS the textarea's child text node, and the prototype's runtime keeps it in
268
+ * step with the buffer — so the rendered document is in the DOM, not only in a property. On a
269
+ * box the PM has typed into, writing it moves nothing; `value` is then written only when it
270
+ * genuinely differs, which is what keeps the caret still mid-sentence. */
271
+ if (body.defaultValue !== text) body.defaultValue = text;
272
+ if (body.value !== text) body.value = text;
273
+ } else body = viewBody(text);
274
+
275
+ setChildren(host, [modeBar, body]);
276
+ return host;
277
+ }
278
+
279
+ export default mountDoc;
@@ -0,0 +1,83 @@
1
+ /* pm/tab-task.js — the task tab body: one task, stated the way a ticket states one.
2
+ *
3
+ * A port of the prototype's task pane (template lines 940–979, derivation 2008–2018). The five
4
+ * fields are fixed and standardized — instruction · input · expected outcome · evaluation · output
5
+ * — because a task an agent is asked to run is only reviewable if what it was told, what it was
6
+ * given, what it owed and how it will be judged are all on one page.
7
+ *
8
+ * The output field is the honest one: it is a button to the artifact ONLY when an artifact exists.
9
+ * Where nothing has been produced it says so in as many words rather than showing an empty row that
10
+ * reads like a file nobody can find. Nothing here is invented per task — the panel renders the tab
11
+ * record panel 3 derived, and a field the engine has no evidence for arrives empty.
12
+ */
13
+
14
+ import { el, setChildren } from './dom.js';
15
+ import * as FX from './fixture.js';
16
+ import { openDoc } from './panel-initiatives.js';
17
+
18
+ /** `{{ expr }}` — one interpolated value in its own span, as the prototype's runtime emits it. */
19
+ const iv = (v) => el('span', null, v === null || v === undefined ? '' : String(v));
20
+
21
+ const LABEL = 'font-family:var(--mono);font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--muted-foreground);margin-bottom:5px';
22
+ /* The output label carries 6px, not 5px. It is not a typo in the design and it is not corrected. */
23
+ const LABEL_OUT = 'font-family:var(--mono);font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--muted-foreground);margin-bottom:6px';
24
+ const PROSE = 'font-size:14px;line-height:1.6;text-wrap:pretty';
25
+ const MONO = 'font-family:var(--mono);font-size:12.5px;color:var(--muted-foreground);overflow-wrap:anywhere';
26
+
27
+ /** One labelled field: a mono uppercase label over its value. */
28
+ const field = (label, value, labelStyle, valueStyle) =>
29
+ el('div', null,
30
+ el('div', { style: labelStyle || LABEL }, label),
31
+ el('div', { style: valueStyle || PROSE }, iv(value)),
32
+ );
33
+
34
+ /**
35
+ * Render the task pane for `tab` into `host` — the pane element panel 4 keeps for this tab.
36
+ * The pane has no editable state of its own, so it is rebuilt whole on each paint.
37
+ */
38
+ export function mountTask(host, tab) {
39
+ const hasOutput = !!tab.output && tab.output !== '—';
40
+ const statusColor = FX.TASK_STATUS_COLOR[tab.status] || FX.C.mut;
41
+
42
+ const output = hasOutput
43
+ ? el('button', {
44
+ onClick: () => openDoc(tab.slug, tab.output, 'FILE'),
45
+ style: 'display:flex;gap:8px;align-items:center;font-family:var(--mono);font-size:12px;padding:7px 11px;background:var(--card);border:1px solid var(--border);color:var(--info);min-height:32px;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap',
46
+ hover: 'border-color:var(--ring)',
47
+ },
48
+ el('span', { style: 'flex:none;font-size:9px;letter-spacing:.1em;color:var(--muted-foreground)' }, 'OPEN'),
49
+ iv(tab.output),
50
+ )
51
+ : el('div', { style: 'font-size:13px;color:var(--muted-foreground);font-style:italic' },
52
+ 'no artifact yet — produced when the agent runs this task');
53
+
54
+ const body = el('div', {
55
+ style: 'flex:1;overflow-y:auto;background:var(--background);color:var(--foreground);padding:22px 26px 60px;font-size:14px;line-height:1.65',
56
+ },
57
+ el('div', { style: 'max-width:76ch' },
58
+ el('div', { style: 'display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-bottom:5px' },
59
+ el('span', {
60
+ style: 'font-family:var(--mono);font-size:10px;letter-spacing:.12em;text-transform:uppercase;color:var(--muted-foreground)',
61
+ }, iv(tab.phase), ' · task'),
62
+ el('span', {
63
+ style: 'font-family:var(--mono);font-size:10px;padding:2px 8px;border:1px solid var(--border);color:' + statusColor,
64
+ }, iv(tab.status)),
65
+ ),
66
+ el('div', {
67
+ style: 'font-size:20px;font-weight:600;letter-spacing:-.01em;margin-bottom:20px;text-wrap:pretty',
68
+ }, iv(tab.title)),
69
+ el('div', { style: 'display:flex;flex-direction:column;gap:16px' },
70
+ field('instruction to the agent', tab.instruction),
71
+ field('input', tab.input, LABEL, MONO),
72
+ field('expected outcome', tab.expect),
73
+ field('evaluation', tab.evalNote),
74
+ el('div', null, el('div', { style: LABEL_OUT }, 'output'), output),
75
+ ),
76
+ ),
77
+ );
78
+
79
+ setChildren(host, [body]);
80
+ return host;
81
+ }
82
+
83
+ export default mountTask;