create-pmos 0.20.0 → 0.21.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
CHANGED
|
@@ -333,6 +333,38 @@ Refreshes the skills, concepts, and scripts only. Your records — `state/`, `pl
|
|
|
333
333
|
`okf/product/` — are yours, and an update never touches them. `pmos/KIT-VERSION` always says
|
|
334
334
|
exactly what you're running.
|
|
335
335
|
|
|
336
|
+
## What's new in 0.21.0
|
|
337
|
+
|
|
338
|
+
**The workspace terminal's lines are no longer double-spaced.** Run
|
|
339
|
+
`npx create-pmos@latest workspace`; the version bump means an existing install auto-upgrades.
|
|
340
|
+
|
|
341
|
+
- **A terminal row is 20px instead of 26px.** Agent output in the terminal panel was rendering with
|
|
342
|
+
13.5px of empty space around a 12.5px character — and because agent CLIs print blank lines
|
|
343
|
+
between blocks, and those were 26px too, the whole transcript read as nearly double-spaced. It
|
|
344
|
+
now matches the spacing the workspace was designed with.
|
|
345
|
+
- **Your agent gets more terminal.** This was never only cosmetic: the workspace divides the
|
|
346
|
+
panel's height by the row height to decide how many lines to tell the shell it has. The same
|
|
347
|
+
panel that reported 21 lines now reports 28, so full-screen agent interfaces have a third more
|
|
348
|
+
room to draw into.
|
|
349
|
+
- *(If you are curious: the number `1.65` appeared twice in one file and meant two different things.
|
|
350
|
+
As a CSS rule it multiplies the font size — 12.5 × 1.65 = 20.6px, which is the design. Handed to
|
|
351
|
+
the terminal engine, the same number multiplies the font's own already-leaded box, which is 16px
|
|
352
|
+
— giving 26px. A comment in the file asserted the two agreed, which is how it survived.)*
|
|
353
|
+
|
|
354
|
+
## What's new in 0.20.1
|
|
355
|
+
|
|
356
|
+
**An HTML document opens rendered, not as its own source.** Opening an `.html` file in the
|
|
357
|
+
workspace's document viewer showed you the markup instead of the page.
|
|
358
|
+
|
|
359
|
+
## What's new in 0.20.0
|
|
360
|
+
|
|
361
|
+
**External links name a real record instead of inventing one.** The Jira/Confluence/Docs link
|
|
362
|
+
controls used to fabricate every identifier they recorded. A `+` button now opens a form and
|
|
363
|
+
records nothing until you supply the reference or a session resolves it — and `resolved` cannot be
|
|
364
|
+
asserted, only evidenced: a row claiming it without naming the profile that resolved it is refused.
|
|
365
|
+
PMOS still holds no credential and makes no outbound request; the lookup runs in an agent session
|
|
366
|
+
carrying your own MCP.
|
|
367
|
+
|
|
336
368
|
## What's new in 0.19.0
|
|
337
369
|
|
|
338
370
|
**A long document scrolls its own panel, and starting a session asks what is already running.** Run
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"description": "Scaffold the PMOS portable kit — run the PMOS product-management workflow (initiative → lane-fit spec → contract → build → three gates, with work-type lanes for features, research, maintenance, incidents, and content) entirely inside your own repo, with all state as git-versioned files. `create-pmos workspace` also installs + launches the local Terminal Workspace GUI. No database, no network, no telemetry.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
package/payload/KIT-VERSION
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmos-workspace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "pmos-workspace",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.21.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@xterm/addon-fit": "^0.11.0",
|
package/workspace/package.json
CHANGED
|
@@ -40,6 +40,12 @@
|
|
|
40
40
|
rather than only in a browser. pm/mobile.js reads it from the global; the module graph does not
|
|
41
41
|
import it, because a second copy of the reducer is exactly the drift it was extracted to stop. -->
|
|
42
42
|
<script src="/lib/mobile-nav.js"></script>
|
|
43
|
+
<!-- ONE classifier, both sides (workspace-reader-preview). What the reader may do with a file —
|
|
44
|
+
markdown, escaped text, an HTML page, an image — is decided by this module, which server.js
|
|
45
|
+
requires for its read endpoints and the doc tab reads here as window.PreviewKind. A second
|
|
46
|
+
opinion about what an .html file is would mean the endpoint and the pane disagreeing about the
|
|
47
|
+
same document. Classic script (a UMD), like the three above. -->
|
|
48
|
+
<script src="/lib/preview-kind.js"></script>
|
|
43
49
|
<link rel="preload" href="/vendor/ibm-plex-mono-500.woff2" as="font" type="font/woff2" crossorigin />
|
|
44
50
|
</head>
|
|
45
51
|
<body>
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
/* pm/tab-doc.js — the document tab body: view · raw · edit · diff, with save, revert and a dirty flag.
|
|
2
2
|
*
|
|
3
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
|
|
4
|
+
* are the whole design: `view` renders the document, `raw` shows the file tinted by line kind,
|
|
5
5
|
* `edit` is the buffer, and `diff` is a real LCS line diff of the buffer against what is on disk.
|
|
6
|
+
*
|
|
7
|
+
* `view` renders the markdown — EXCEPT for an HTML file, which is a page, not prose. Handing one to
|
|
8
|
+
* the markdown parser prints its own markup back at the PM: the document read as its source code.
|
|
9
|
+
* So an `.html` document renders, in `view`, as the page it is; `raw` is the one click that still
|
|
10
|
+
* shows the code. See `htmlFrame` for why rendering it is not a new privilege.
|
|
6
11
|
* The diff is COMPUTED, never narrated — a "2 sections differ" that nothing read is the watermelon
|
|
7
12
|
* this repo has a word for, and the same rule that governs artifact sync governs this pane.
|
|
8
13
|
*
|
|
@@ -31,6 +36,7 @@ const iv = (v) => el('span', null, v === null || v === undefined ? '' : String(v
|
|
|
31
36
|
const disk = new Map(); // path -> text read from this machine
|
|
32
37
|
const pending = new Set(); // paths whose read is in flight
|
|
33
38
|
const saveErr = new Map(); // path -> why the last save was refused
|
|
39
|
+
const unread = new Set(); // paths whose read FAILED — what `disk` holds for them is a stub
|
|
34
40
|
|
|
35
41
|
let dataMod = null;
|
|
36
42
|
import('./data.js').then((m) => { dataMod = m; }).catch(() => { dataMod = null; });
|
|
@@ -77,9 +83,12 @@ function requestDoc(tab) {
|
|
|
77
83
|
: fetch('/api/doc?ws=' + encodeURIComponent(wsId()) + '&path=' + encodeURIComponent(path))
|
|
78
84
|
.then((r) => (r.ok ? r.text() : Promise.reject(new Error('HTTP ' + r.status))));
|
|
79
85
|
Promise.resolve(load).then(
|
|
80
|
-
(text) => done(typeof text === 'string' ? text : String(text == null ? '' : text)),
|
|
81
|
-
(e) =>
|
|
82
|
-
|
|
86
|
+
(text) => { unread.delete(path); done(typeof text === 'string' ? text : String(text == null ? '' : text)); },
|
|
87
|
+
(e) => {
|
|
88
|
+
unread.add(path);
|
|
89
|
+
done(stub(tab, 'This file could not be read: ' + (e && e.message ? e.message : e)
|
|
90
|
+
+ '\n\nNothing was rendered from it. That is the failure being stated rather than an empty page shown.'));
|
|
91
|
+
},
|
|
83
92
|
);
|
|
84
93
|
}
|
|
85
94
|
|
|
@@ -180,6 +189,71 @@ function rawBody(text) {
|
|
|
180
189
|
}));
|
|
181
190
|
}
|
|
182
191
|
|
|
192
|
+
/* ── the HTML page body ────────────────────────────────────────────────────────────────────────
|
|
193
|
+
*
|
|
194
|
+
* `previewKind` is the single classifier both sides of this app already trust (server.js requires
|
|
195
|
+
* it; index.html loads it as a classic script), so "what counts as HTML" is decided in exactly one
|
|
196
|
+
* place and cannot drift from the read endpoints. If it somehow did not load, `isHtmlDoc` answers
|
|
197
|
+
* false and the pane behaves exactly as it did before — a missing classifier degrades to the old
|
|
198
|
+
* view, never to an unclassified render.
|
|
199
|
+
*/
|
|
200
|
+
const isHtmlDoc = (p) => {
|
|
201
|
+
const PK = typeof window !== 'undefined' ? window.PreviewKind : null;
|
|
202
|
+
if (!PK || typeof PK.previewKind !== 'function') return false;
|
|
203
|
+
return PK.previewKind(String(p || '')) === 'html';
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
/* A STUB IS NOT THE DOCUMENT, AND MUST NOT BE RENDERED AS ONE. When a read is in flight or has
|
|
207
|
+
* failed, `docText` hands back this pane's own prose — "reading …", or the failure with its reason.
|
|
208
|
+
* Rendering that inside a frame would bury the one sentence the PM needs behind an iframe boundary,
|
|
209
|
+
* and an .html file that could not be read would look like an empty page: the exact "never a blank
|
|
210
|
+
* page" rule this pane already keeps for every other kind. So a page is drawn only from text that
|
|
211
|
+
* genuinely came from disk (or from an edit saved in this window). A reference URI never renders —
|
|
212
|
+
* nothing was read from it to render. */
|
|
213
|
+
function rendersAsPage(tab) {
|
|
214
|
+
const p = tab.path;
|
|
215
|
+
if (!isHtmlDoc(p) || isReference(p)) return false;
|
|
216
|
+
if (state.savedDocs[p] != null) return true;
|
|
217
|
+
if (FIXTURE) return true;
|
|
218
|
+
return disk.has(p) && !unread.has(p);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/* Rendering an HTML document adds no privilege over showing it. The frame is fully sandboxed —
|
|
222
|
+
* `sandbox=""` grants nothing, so scripts and same-origin access are both denied — and the injected
|
|
223
|
+
* CSP is `default-src 'none'`, which blocks every network fetch the page could attempt. A poisoned
|
|
224
|
+
* explainer therefore can neither execute nor beacon out, which is the same no-remote-fetch-on-view
|
|
225
|
+
* invariant the markdown image rule holds (R4). Only inline styles, data: images and data: fonts
|
|
226
|
+
* resolve — enough for a page to look like itself, not enough to reach anything. */
|
|
227
|
+
const HTML_CSP = '<meta http-equiv="Content-Security-Policy" content="'
|
|
228
|
+
+ "default-src 'none'; img-src data:; style-src 'unsafe-inline'; font-src data:" + '">';
|
|
229
|
+
|
|
230
|
+
const frames = new Map(); // tab id -> { text, node } — the frame currently showing that text
|
|
231
|
+
|
|
232
|
+
/* THE FRAME IS NEVER RE-POINTED, IT IS REPLACED. Writing a new `srcdoc` onto a frame that is
|
|
233
|
+
* already in the document does navigate it — but the replacement document does NOT get the meta
|
|
234
|
+
* CSP applied to its subresource loads, so the remote <link>s in the new page are fetched. That
|
|
235
|
+
* is not a theory: the panel-4 suite opens `web/index.html`, whose remote font stylesheet went out
|
|
236
|
+
* exactly this way, and the containment assertion caught it. Setting `srcdoc` BEFORE the frame is
|
|
237
|
+
* inserted is enforced correctly, so a text change builds a new frame and the old one is dropped.
|
|
238
|
+
*
|
|
239
|
+
* The cache is therefore keyed on the TEXT, not just the tab: a repaint that changed nothing hands
|
|
240
|
+
* back the identical node — which is what `setChildren` needs to leave a live frame alone — while a
|
|
241
|
+
* changed document gets a frame built from scratch. The cost is that saving an open HTML file
|
|
242
|
+
* returns it to the top; the alternative is a page that renders contained only until it is edited. */
|
|
243
|
+
function htmlFrame(tab, text) {
|
|
244
|
+
const cur = frames.get(tab.id);
|
|
245
|
+
if (cur && cur.text === text) return cur.node;
|
|
246
|
+
const node = el('iframe', {
|
|
247
|
+
style: 'flex:1;min-height:0;width:100%;border:0;background:var(--background)',
|
|
248
|
+
sandbox: '', // no allow-scripts, no allow-same-origin
|
|
249
|
+
referrerpolicy: 'no-referrer',
|
|
250
|
+
title: String(tab.path || 'document'),
|
|
251
|
+
srcdoc: HTML_CSP + text, // set HERE, on a node not yet in the document — see above
|
|
252
|
+
});
|
|
253
|
+
frames.set(tab.id, { text, node });
|
|
254
|
+
return node;
|
|
255
|
+
}
|
|
256
|
+
|
|
183
257
|
function diffBody(raw, text, dirty) {
|
|
184
258
|
const rows = dirty ? FX.diffLines(raw, text) : [FX.NO_DIFF_ROW];
|
|
185
259
|
return el('div', {
|
|
@@ -212,6 +286,7 @@ function editBox(tab) {
|
|
|
212
286
|
/** Release what a closed doc tab held. Called by panel-tabs.js when the tab goes away. */
|
|
213
287
|
export function disposeDoc(id) {
|
|
214
288
|
boxes.delete(id);
|
|
289
|
+
frames.delete(id);
|
|
215
290
|
}
|
|
216
291
|
|
|
217
292
|
/* ── mount ─────────────────────────────────────────────────────────────────────────────────── */
|
|
@@ -270,7 +345,7 @@ export function mountDoc(host, tab) {
|
|
|
270
345
|
* genuinely differs, which is what keeps the caret still mid-sentence. */
|
|
271
346
|
if (body.defaultValue !== text) body.defaultValue = text;
|
|
272
347
|
if (body.value !== text) body.value = text;
|
|
273
|
-
} else body = viewBody(text);
|
|
348
|
+
} else body = rendersAsPage(tab) ? htmlFrame(tab, text) : viewBody(text);
|
|
274
349
|
|
|
275
350
|
setChildren(host, [modeBar, body]);
|
|
276
351
|
return host;
|
|
@@ -37,7 +37,9 @@
|
|
|
37
37
|
* so the first real metrics arrive after the woff2 lands).
|
|
38
38
|
*
|
|
39
39
|
* The xterm palette is the DESIGN's: `--term-bg` for the background — dark in both themes, D41 —
|
|
40
|
-
* and IBM Plex Mono at the design's 12.5px
|
|
40
|
+
* and IBM Plex Mono at the design's 12.5px. Its LINE HEIGHT is not the design's 1.65: that number
|
|
41
|
+
* is a CSS line-height and xterm's option of the same name is not one, so it is converted rather
|
|
42
|
+
* than copied — see TERM_LINE_HEIGHT. xterm's colour parser does not understand
|
|
41
43
|
* `oklch()`, so the token is resolved to a concrete sRGB value at runtime (canvas round-trip, with
|
|
42
44
|
* the pre-computed hex as the fallback) rather than handed to xterm as written.
|
|
43
45
|
*/
|
|
@@ -446,6 +448,31 @@ if (typeof window !== 'undefined') {
|
|
|
446
448
|
|
|
447
449
|
/* ── fixture pane ──────────────────────────────────────────────────────────────────────────── */
|
|
448
450
|
|
|
451
|
+
/* ── one terminal row ────────────────────────────────────────────────────────────────────────
|
|
452
|
+
* TWO pane kinds, TWO different meanings for the same number, and that is the whole of the defect
|
|
453
|
+
* this constant exists to keep fixed.
|
|
454
|
+
*
|
|
455
|
+
* FAUX SHELL (fixture) — SCROLL_STYLE below. `line-height:1.65` is CSS: a unitless multiplier on
|
|
456
|
+
* the FONT SIZE. One line box is 12.5 × 1.65 = 20.625px. That is the PM's prototype, ported
|
|
457
|
+
* character for character (planning/design/four-panel/prototype-original.html:929), and it is
|
|
458
|
+
* also the value design-parity.js compares. It was never wrong; leave it alone.
|
|
459
|
+
*
|
|
460
|
+
* LIVE TERMINAL (real) — the xterm options. `lineHeight` there is xterm's OWN option, and its
|
|
461
|
+
* renderer computes `cell.height = floor(char.height × lineHeight)` where `char.height` is the
|
|
462
|
+
* font's MEASURED glyph box — 16px for IBM Plex Mono at 12.5px, already carrying the leading the
|
|
463
|
+
* typeface ships with. So xterm's number adds leading ON TOP OF leading where CSS's replaces it.
|
|
464
|
+
*
|
|
465
|
+
* Passing the design's CSS 1.65 straight into xterm therefore rendered floor(1.65 × 16) = 26px
|
|
466
|
+
* rows against a 20.625px reference — 26% looser than the design, 13.5px of empty space around a
|
|
467
|
+
* 12.5px glyph — and the header comment at the top of this file recorded the belief that the two
|
|
468
|
+
* agreed. Measured in Chromium against the served woff2: 1.0 → 16px, 1.2 → 19px, 1.3 → 20px,
|
|
469
|
+
* 1.65 → 26px. 1.3 is the ratio that reproduces the design's line box through xterm's formula.
|
|
470
|
+
*
|
|
471
|
+
* It is not paint-only: addon-fit divides the pane height by the cell height to pick `rows`, and
|
|
472
|
+
* syncSize() sends that to the PTY. At 26px a 565px pane told the shell it had 21 rows; at 20px it
|
|
473
|
+
* tells it 28. The row height IS how much terminal the agent CLI gets to draw into.
|
|
474
|
+
*/
|
|
475
|
+
const TERM_LINE_HEIGHT = 1.3;
|
|
449
476
|
const SCROLL_STYLE = 'flex:1;min-height:0;overflow-y:auto;padding:14px 16px;font-family:var(--mono);font-size:12.5px;line-height:1.65;color:#c4ccce';
|
|
450
477
|
const LINE_STYLE = (c) => 'color:' + c + ';white-space:pre-wrap;overflow-wrap:anywhere';
|
|
451
478
|
const PROMPT_ROW_STYLE = 'display:flex;gap:8px;align-items:baseline';
|
|
@@ -542,7 +569,10 @@ function buildReal(tab) {
|
|
|
542
569
|
const term = new Terminal({
|
|
543
570
|
fontFamily: '"IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace',
|
|
544
571
|
fontSize: 12.5,
|
|
545
|
-
|
|
572
|
+
// NOT the design's 1.65 — and that is the whole point. See TERM_LINE_HEIGHT above: xterm's
|
|
573
|
+
// `lineHeight` multiplies the font's MEASURED box, where the design's CSS number multiplies the
|
|
574
|
+
// font size. Handing the CSS number to xterm is what made a row 26px instead of 20.6px.
|
|
575
|
+
lineHeight: TERM_LINE_HEIGHT,
|
|
546
576
|
theme: xtheme(),
|
|
547
577
|
cursorBlink: true,
|
|
548
578
|
scrollback: 5000,
|