create-pmos 0.67.0 → 0.68.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/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/guide/command-palette.md +1 -1
- package/workspace/guide/layout.md +36 -9
- package/workspace/lib/sync.js +32 -4
- package/workspace/package-lock.json +1 -1
- package/workspace/package.json +1 -1
- package/workspace/public/pm/components.css +610 -3
- package/workspace/public/pm/doc-editor.js +473 -0
- package/workspace/public/pm/doc-rail.js +161 -0
- package/workspace/public/pm/doc-remote.js +180 -0
- package/workspace/public/pm/dock-sessions.js +57 -13
- package/workspace/public/pm/frame-copy.js +2 -0
- package/workspace/public/pm/icons.js +1 -1
- package/workspace/public/pm/panel-tabs.js +85 -3
- package/workspace/public/pm/primitives.js +8 -4
- package/workspace/public/pm/state.js +3 -0
- package/workspace/public/pm/tab-doc.js +175 -170
- package/workspace/public/vendor/lucide-sprite.js +15 -2
- package/workspace/replaced-tests.json +43 -0
- package/workspace/server.js +30 -0
- package/workspace/test-lists/workspace-tabs-declutter.json +13 -0
- package/workspace/third-party/README.md +1 -1
|
@@ -32,6 +32,12 @@
|
|
|
32
32
|
* 4-second session poll included — took a half-read document back to the top. `scrollPane` keeps one
|
|
33
33
|
* element per tab and mode; see the block comment above it.
|
|
34
34
|
*
|
|
35
|
+
* ── In live mode, a markdown file the PM can edit is edited in place (workspace-tabs-declutter) ─────────────
|
|
36
|
+
* The four modes are the prototype's, and the fixture keeps them. In live mode a markdown file the PM can write is
|
|
37
|
+
* drawn by doc-editor.js: read and edited in one surface, block by block, in the layout it is read in. `edit` stays as
|
|
38
|
+
* the whole file as markdown (the Markdown toggle) and `diff` as the Changes toggle; `raw` is the Source toggle of a
|
|
39
|
+
* file that cannot be edited. Beside the document, doc-rail.js holds its outline and its Connections.
|
|
40
|
+
*
|
|
35
41
|
* ── What `view` renders it through ──────────────────────────────────────────────────────────
|
|
36
42
|
* `markdown.js`, not `fixture.js`'s `mdParse`. The latter is a transcription of the design
|
|
37
43
|
* prototype's four-branch stand-in (`# `/`## `/`- `/paragraph) pinned byte-for-byte by
|
|
@@ -43,7 +49,10 @@ import { state, setState, FIXTURE } from './state.js';
|
|
|
43
49
|
import * as FX from './fixture.js';
|
|
44
50
|
import { mdRender } from './markdown.js';
|
|
45
51
|
import { openDoc } from './panel-initiatives.js';
|
|
46
|
-
import { button } from './primitives.js';
|
|
52
|
+
import { button, iconButton, menu } from './primitives.js';
|
|
53
|
+
import { editorNode, disposeEditor, headsOf, goToBlock, isEditing } from './doc-editor.js';
|
|
54
|
+
import { railNode, disposeRail, refreshGraph } from './doc-rail.js';
|
|
55
|
+
import { copyChip, refreshCopies } from './doc-remote.js';
|
|
47
56
|
|
|
48
57
|
/** `{{ expr }}` — one interpolated value in its own span, as the prototype's runtime emits it. */
|
|
49
58
|
const iv = (v) => el('span', null, v === null || v === undefined ? '' : String(v));
|
|
@@ -165,6 +174,8 @@ function save(tab, text) {
|
|
|
165
174
|
const final = out && typeof out.text === 'string' ? out.text : text;
|
|
166
175
|
disk.set(path, final);
|
|
167
176
|
claims.delete(wsOf(tab)); // the consistency record may move with the file; read it again
|
|
177
|
+
refreshCopies(wsOf(tab)); // …and so may the copy's state, and the file's ties
|
|
178
|
+
refreshGraph(wsOf(tab), tab.path);
|
|
168
179
|
commit(final);
|
|
169
180
|
},
|
|
170
181
|
(e) => { saveErr.set(path, (e && e.message) || 'could not reach the workspace server'); setState({}); },
|
|
@@ -388,35 +399,26 @@ function editBox(tab) {
|
|
|
388
399
|
/** Release what a closed doc tab held. Called by panel-tabs.js when the tab goes away. */
|
|
389
400
|
export function disposeDoc(id) {
|
|
390
401
|
boxes.delete(id);
|
|
402
|
+
docBars.delete(id);
|
|
391
403
|
normNote.delete(id);
|
|
392
404
|
frames.delete(id);
|
|
393
405
|
scrollers.delete(id);
|
|
394
406
|
running.delete(id); // the grant belonged to this open document, and it is closed
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
407
|
+
disposeEditor(id);
|
|
408
|
+
disposeRail(id);
|
|
409
|
+
places.delete(id);
|
|
410
|
+
railHidden.delete(id);
|
|
398
411
|
}
|
|
399
412
|
|
|
400
413
|
|
|
401
|
-
/* ── the
|
|
414
|
+
/* ── the findings beside a document (was the Alignment drawer) ─────────────────────────────────────────────────────
|
|
402
415
|
*
|
|
403
|
-
* The
|
|
404
|
-
*
|
|
405
|
-
*
|
|
406
|
-
*
|
|
407
|
-
*
|
|
408
|
-
* · the board's "Apply" (a proposed edit to the other file, with its diff) has no backing — PMOS
|
|
409
|
-
* drafts no edit to another file — so it is not drawn, and the drawer says so;
|
|
410
|
-
* · a file the last check did not read is said to be unread, not shown as clean.
|
|
411
|
-
* The drawer is closed by default, and closed it adds nothing to the pane: the pane is exactly
|
|
412
|
-
* [mode bar, body] as before. Open, it is a third child laid over the right of the pane, and the
|
|
413
|
-
* mode bar and body give it room with a margin. No node is re-parented, so a textarea keeps its
|
|
414
|
-
* caret, a scroller its offset and a sandboxed frame its document. */
|
|
415
|
-
const alignOpen = new Set(); // tab ids whose drawer is open
|
|
416
|
-
const alignSkip = new Map(); // tab id -> Set of rels the PM skipped for this tab
|
|
416
|
+
* The Alignment drawer (workspace-cockpit-nav Run 3) listed the documents with a concrete tie to this one, from the
|
|
417
|
+
* consistency record the Validate view reads (`/api/claims`). The Connections rail (doc-rail.js) now draws every stated
|
|
418
|
+
* tie, so what the record adds is its FINDINGS: the ones in this file, and the ones in another file that name this
|
|
419
|
+
* file as their source. A tie is still never drawn from wording, and a file the last check did not read is not shown as
|
|
420
|
+
* clean: it has no findings to show, and the rail says only what was read. */
|
|
417
421
|
const claims = new Map(); // ws -> { state: 'loading' | 'ok' | 'error', data?, error? }
|
|
418
|
-
const ALIGN_W = 'min(420px, 50%)';
|
|
419
|
-
const alignNodes = new Map(); // tab id -> the drawer element, kept so its scroll survives a repaint
|
|
420
422
|
|
|
421
423
|
function readClaims(ws) {
|
|
422
424
|
let c = claims.get(ws);
|
|
@@ -430,132 +432,24 @@ function readClaims(ws) {
|
|
|
430
432
|
return c;
|
|
431
433
|
}
|
|
432
434
|
|
|
433
|
-
/**
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
const
|
|
438
|
-
const
|
|
439
|
-
const
|
|
440
|
-
const
|
|
441
|
-
const STATUS_TONE = { contradicted: 'var(--destructive)', stale: 'var(--warning)', suspected: 'var(--muted-foreground)', undecided: 'var(--muted-foreground)' };
|
|
442
|
-
const STATUS_WORD = { contradicted: 'contradicted', stale: 'stale', suspected: 'suspected by Jev', undecided: 'worth a look' };
|
|
443
|
-
|
|
444
|
-
const statusBadge = (st) => el('span', {
|
|
445
|
-
style: A_BADGE + ';border-color:transparent;color:' + (STATUS_TONE[st] || 'var(--muted-foreground)')
|
|
446
|
-
+ ';background:color-mix(in oklab, ' + (STATUS_TONE[st] || 'var(--muted-foreground)') + ' 14%, transparent)',
|
|
447
|
-
}, iv(STATUS_WORD[st] || st));
|
|
448
|
-
|
|
449
|
-
/**
|
|
450
|
-
* The documents tied to `rel`, each with the ties that put it there. Three kinds, all read:
|
|
451
|
-
* · this file is compared with it (this file's `compared_with`)
|
|
452
|
-
* · it is compared with this file (its `compared_with` names this file)
|
|
453
|
-
* · a finding in it names this file (a finding whose `source` is this file)
|
|
454
|
-
*/
|
|
455
|
-
function tiesFor(rel, docs) {
|
|
456
|
-
const out = new Map();
|
|
457
|
-
const add = (r, tie, finding) => {
|
|
458
|
-
if (!r || r === rel) return;
|
|
459
|
-
let c = out.get(r);
|
|
460
|
-
if (!c) { c = { rel: r, ties: new Set(), findings: [] }; out.set(r, c); }
|
|
461
|
-
c.ties.add(tie);
|
|
462
|
-
if (finding) c.findings.push(finding);
|
|
463
|
-
};
|
|
464
|
-
const self = docs.find((x) => x.rel === rel) || null;
|
|
465
|
-
if (self) for (const r of self.compared_with || []) add(r, 'this file checks against it');
|
|
435
|
+
/** The findings that concern `rel`: its own, then each other file's finding whose source is `rel` (with that file). */
|
|
436
|
+
function findingsFor(ws, rel) {
|
|
437
|
+
const c = readClaims(ws);
|
|
438
|
+
if (c.state !== 'ok') return [];
|
|
439
|
+
const docs = Array.isArray(c.data && c.data.docs) ? c.data.docs : [];
|
|
440
|
+
const out = [];
|
|
441
|
+
const self = docs.find((x) => x.rel === rel);
|
|
442
|
+
if (self) for (const f of self.findings || []) out.push({ status: f.status, sentence: f.sentence });
|
|
466
443
|
for (const x of docs) {
|
|
467
444
|
if (x.rel === rel) continue;
|
|
468
|
-
|
|
469
|
-
for (const f of x.findings || []) if (f && f.source === rel) add(x.rel, 'a finding names this file', f);
|
|
470
|
-
}
|
|
471
|
-
return { self, list: [...out.values()] };
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
function alignDrawer(tab) {
|
|
475
|
-
const ws = wsOf(tab);
|
|
476
|
-
const rel = String(tab.path || '');
|
|
477
|
-
const close = () => { alignOpen.delete(tab.id); setState({}); };
|
|
478
|
-
const kids = [
|
|
479
|
-
el('div', { style: 'display:flex;align-items:center;justify-content:space-between;gap:8px' },
|
|
480
|
-
el('span', { style: 'font-weight:600;font-size:15px;font-family:var(--sans)', 'data-r29': 'align-title' }, 'Alignment'),
|
|
481
|
-
el('button', { onClick: close, 'data-r29': 'align-close', 'aria-label': 'close alignment', title: 'close the drawer',
|
|
482
|
-
style: 'background:transparent;border:0;color:var(--muted-foreground);font-size:15px;padding:2px 6px', hover: 'color:var(--foreground)' }, '⟩')),
|
|
483
|
-
el('div', { style: A_NOTE, 'data-r29': 'align-rule' }, 'Only documents with a concrete tie to this file are listed. No similarity guesses.'),
|
|
484
|
-
];
|
|
485
|
-
const c = readClaims(ws);
|
|
486
|
-
if (c.state === 'loading') {
|
|
487
|
-
kids.push(el('div', { style: A_NOTE, 'data-r29': 'align-status', 'data-state': 'loading' }, 'Reading the consistency record…'));
|
|
488
|
-
} else if (c.state === 'error') {
|
|
489
|
-
kids.push(el('div', { style: A_NOTE + ';color:var(--destructive)', 'data-r29': 'align-status', 'data-state': 'error' },
|
|
490
|
-
iv('The consistency record could not be read: ' + c.error + '. Nothing is listed.')));
|
|
491
|
-
} else {
|
|
492
|
-
const d = c.data || {};
|
|
493
|
-
const docs = Array.isArray(d.docs) ? d.docs : [];
|
|
494
|
-
const openValidate = () => import('./panel-cockpit.js').then((m) => m.openView('validate')).catch(() => {});
|
|
495
|
-
if (d.verdict === 'not-checked' || !docs.length) {
|
|
496
|
-
kids.push(el('div', { style: A_NOTE, 'data-r29': 'align-status', 'data-state': 'not-checked' },
|
|
497
|
-
'This repository has not been checked yet, so no tie is known. Open Validate and press “check now”.'));
|
|
498
|
-
kids.push(el('div', null, el('button', { style: A_BTN, hover: 'border-color:var(--ring)', onClick: openValidate, 'data-r29': 'align-validate' }, 'open Validate')));
|
|
499
|
-
} else {
|
|
500
|
-
const { self, list } = tiesFor(rel, docs);
|
|
501
|
-
const skipped = alignSkip.get(tab.id) || new Set();
|
|
502
|
-
const shown = list.filter((x) => !skipped.has(x.rel));
|
|
503
|
-
kids.push(el('div', { style: A_NOTE, 'data-r29': 'align-status', 'data-state': self ? 'read' : 'unread' },
|
|
504
|
-
iv(self
|
|
505
|
-
? 'This file: ' + (self.classified ? self.type : 'not classified') + ' · ' + self.verified + ' statement(s) verified'
|
|
506
|
-
+ (self.findings.length ? ' · ' + self.findings.length + ' finding(s)' : ' · no open findings')
|
|
507
|
-
: 'The last check did not read this file, so its own findings are unknown. Ties from other files are still listed.')));
|
|
508
|
-
if (self && self.findings.length) {
|
|
509
|
-
kids.push(el('div', { style: A_LBL }, 'findings in this file'));
|
|
510
|
-
kids.push(el('div', { style: 'display:flex;flex-direction:column;gap:6px', 'data-r29': 'align-self' },
|
|
511
|
-
self.findings.map((f) => el('div', { 'data-r29': 'align-finding', 'data-status': f.status, style: 'display:flex;flex-direction:column;gap:3px' },
|
|
512
|
-
el('span', null, statusBadge(f.status)),
|
|
513
|
-
el('span', { style: 'font-size:12px;line-height:1.5;color:var(--foreground)' }, iv(f.sentence))))));
|
|
514
|
-
}
|
|
515
|
-
if (state.savedDocs[docKey(tab)] != null && list.length) {
|
|
516
|
-
kids.push(el('div', { style: A_NOTE + ';color:var(--warning)', 'data-r29': 'align-saved' },
|
|
517
|
-
iv('Saved. ' + list.length + ' other document' + (list.length === 1 ? '' : 's') + ' may now disagree with this change.')));
|
|
518
|
-
}
|
|
519
|
-
kids.push(el('div', { style: A_LBL }, iv('tied documents · ' + list.length)));
|
|
520
|
-
if (!list.length) kids.push(el('div', { style: A_NOTE, 'data-r29': 'align-none' }, 'No other document has a concrete tie to this file.'));
|
|
521
|
-
for (const x of shown) {
|
|
522
|
-
kids.push(el('div', { style: A_CARD, 'data-r29': 'align-cand', 'data-rel': x.rel },
|
|
523
|
-
el('b', { style: 'font-family:var(--mono);font-size:12px;font-weight:600;overflow-wrap:anywhere' }, iv(x.rel)),
|
|
524
|
-
el('div', { style: 'display:flex;gap:6px;flex-wrap:wrap' }, [...x.ties].map((t) => el('span', { style: A_BADGE, 'data-r29': 'align-tie' }, iv(t)))),
|
|
525
|
-
x.findings.map((f) => el('div', { style: 'display:flex;flex-direction:column;gap:3px' },
|
|
526
|
-
el('span', null, statusBadge(f.status)),
|
|
527
|
-
el('span', { style: A_NOTE }, iv(f.sentence)))),
|
|
528
|
-
el('div', { style: 'display:flex;gap:6px' },
|
|
529
|
-
el('button', { style: A_BTN, hover: 'border-color:var(--ring)', 'data-r29': 'align-open', onClick: () => openDoc(tab.slug, x.rel, 'FILE', tab.ws ? { ws: tab.ws, branch: tab.branch || null, linked: true } : undefined) }, 'Open to edit'),
|
|
530
|
-
el('button', { style: A_BTN, hover: 'border-color:var(--ring)', 'data-r29': 'align-skip',
|
|
531
|
-
onClick: () => { const s2 = new Set(skipped); s2.add(x.rel); alignSkip.set(tab.id, s2); setState({}); } }, 'Skip'))));
|
|
532
|
-
}
|
|
533
|
-
if (skipped.size) {
|
|
534
|
-
kids.push(el('button', { style: A_BTN + ';align-self:flex-start', hover: 'border-color:var(--ring)', 'data-r29': 'align-unskip',
|
|
535
|
-
onClick: () => { alignSkip.delete(tab.id); setState({}); } }, iv('show ' + skipped.size + ' skipped')));
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
kids.push(el('div', { style: 'display:flex;gap:6px' },
|
|
539
|
-
el('button', { style: A_BTN, hover: 'border-color:var(--ring)', 'data-r29': 'align-reread', onClick: () => { claims.delete(ws); setState({}); } }, 're-read')));
|
|
445
|
+
for (const f of x.findings || []) if (f && f.source === rel) out.push({ status: f.status, sentence: f.sentence, rel: x.rel });
|
|
540
446
|
}
|
|
541
|
-
|
|
542
|
-
'Ties come from the last consistency check. PMOS drafts no edit to another file: open it to change it. Git keeps the history.'));
|
|
543
|
-
/* One node per tab, kept like the scrollers above, so a repaint does not take the drawer's own
|
|
544
|
-
* scroll back to the top. */
|
|
545
|
-
let node = alignNodes.get(tab.id);
|
|
546
|
-
if (!node) { node = makeDrawerNode(); alignNodes.set(tab.id, node); }
|
|
547
|
-
setChildren(node, kids);
|
|
548
|
-
return node;
|
|
447
|
+
return out;
|
|
549
448
|
}
|
|
550
449
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
style: 'position:absolute;top:0;right:0;bottom:0;width:' + ALIGN_W + ';box-sizing:border-box;overflow-y:auto;'
|
|
555
|
-
+ 'border-left:1px solid var(--border);background:var(--sidebar);color:var(--foreground);padding:16px;'
|
|
556
|
-
+ 'display:flex;flex-direction:column;gap:12px;font-family:var(--sans);font-size:13px;line-height:1.5',
|
|
557
|
-
});
|
|
558
|
-
}
|
|
450
|
+
/** Whether a tab can have the rail at all: a file in the checked-out tree, in real mode. */
|
|
451
|
+
const canRail = (tab) => !FIXTURE && !tab.guide && !tab.ref && !isReference(tab.path);
|
|
452
|
+
const railHidden = new Set(); // tab ids whose outline and connections the PM put away
|
|
559
453
|
|
|
560
454
|
/* ── mount ─────────────────────────────────────────────────────────────────────────────────── */
|
|
561
455
|
|
|
@@ -724,7 +618,10 @@ export function mountDoc(host, tab) {
|
|
|
724
618
|
const raw = docText(tab);
|
|
725
619
|
const text = state.docEdits[docKey(tab)] ?? raw;
|
|
726
620
|
const nonMd = !FIXTURE && isSourcePath(tab.path); // file-viewer-any-text: read-only, shown as source
|
|
727
|
-
const
|
|
621
|
+
const writable = canWrite(tab, nonMd);
|
|
622
|
+
let dm = ((tab.readonly || nonMd) && (state.docModes[tab.id] === 'edit' || state.docModes[tab.id] === 'diff')) ? 'view' : (state.docModes[tab.id] ?? 'view');
|
|
623
|
+
if (writable && dm === 'raw') dm = 'view'; // in place, the file you can write IS its source: no read-only copy of it
|
|
624
|
+
if (!FIXTURE && !writable && (dm === 'edit' || dm === 'diff')) dm = 'view'; // only a markdown file can be saved
|
|
728
625
|
const dirty = text !== raw;
|
|
729
626
|
const err = saveErr.get(docKey(tab));
|
|
730
627
|
|
|
@@ -780,14 +677,7 @@ export function mountDoc(host, tab) {
|
|
|
780
677
|
const canNormalize = !!(norm && typeof norm.normalize === 'function')
|
|
781
678
|
&& !isHtmlDoc(tab.path) && !isReference(tab.path) && !unread.has(docKey(tab)) && !nonMd;
|
|
782
679
|
const normBtn = canNormalize ? el('button', {
|
|
783
|
-
onClick: () =>
|
|
784
|
-
const r = norm.normalize(text);
|
|
785
|
-
normNote.set(tab.id, r.changed
|
|
786
|
-
? r.joins + ' line(s) joined' + (r.blanks ? ', ' + r.blanks + ' blank inserted' : '')
|
|
787
|
-
: 'already normalized — every paragraph is one line');
|
|
788
|
-
if (!r.changed) { setState({}); return; }
|
|
789
|
-
setState((s) => ({ docEdits: { ...s.docEdits, [docKey(tab)]: r.text } }));
|
|
790
|
-
},
|
|
680
|
+
onClick: () => normalizeBuffer(tab, norm),
|
|
791
681
|
style: 'margin-left:8px;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)',
|
|
792
682
|
hover: 'border-color:var(--ring)',
|
|
793
683
|
title: 'Join hard-wrapped paragraphs into one line each — what a Jira issue, a Google Doc or any'
|
|
@@ -812,21 +702,19 @@ export function mountDoc(host, tab) {
|
|
|
812
702
|
hover: 'color:var(--foreground)', 'data-r1': 'measure',
|
|
813
703
|
title: narrowOf(tab) ? 'use the full width of the tab' : 'limit lines to a 72-character reading measure',
|
|
814
704
|
}, iv(narrowOf(tab) ? '↔ full width' : '⇥ 72ch')),
|
|
815
|
-
canAlign(tab) ? el('button', {
|
|
816
|
-
onClick: () => {
|
|
817
|
-
if (alignOpen.has(tab.id)) alignOpen.delete(tab.id);
|
|
818
|
-
else { alignOpen.add(tab.id); claims.delete(wsOf(tab)); } // opening reads the record afresh
|
|
819
|
-
setState({});
|
|
820
|
-
},
|
|
821
|
-
style: 'margin-left:8px;font-family:var(--mono);font-size:10.5px;padding:4px 10px;min-height:26px;background:'
|
|
822
|
-
+ (alignOpen.has(tab.id) ? 'var(--secondary)' : 'transparent') + ';border:1px solid '
|
|
823
|
-
+ (alignOpen.has(tab.id) ? 'var(--brand)' : 'var(--border)') + ';color:var(--foreground)',
|
|
824
|
-
hover: 'border-color:var(--ring)', 'data-r29': 'align-toggle', 'data-open': alignOpen.has(tab.id) ? 'yes' : 'no',
|
|
825
|
-
title: 'Show the documents with a concrete tie to this one, from the last consistency check.',
|
|
826
|
-
}, 'alignment') : null,
|
|
827
705
|
el('span', { style: 'margin-left:auto;font-family:var(--mono);font-size:10px;color:' + dirtyColor }, iv(dirtyLabel)),
|
|
828
706
|
);
|
|
829
707
|
|
|
708
|
+
/* Live mode draws the bar from the design system (workspace-tabs-declutter): the four modes as one segmented control,
|
|
709
|
+
* the save state beside them, save and revert, and one ⋯ menu for what is used now and then (the reading width,
|
|
710
|
+
* normalize, alignment). Run scripts stays on the bar while a page can run: it grants a privilege, so its state is
|
|
711
|
+
* never one click away. The fixture keeps the prototype's bar above, which the parity suites pin. */
|
|
712
|
+
const bar = FIXTURE ? modeBar : docBar(tab, {
|
|
713
|
+
dm, readonly, writable, dirty, err, text, asPage, live, canNormalize, norm,
|
|
714
|
+
statusText: dirtyLabel,
|
|
715
|
+
tone: err ? 'error' : (dirty ? 'dirty' : 'clean'),
|
|
716
|
+
});
|
|
717
|
+
|
|
830
718
|
let body;
|
|
831
719
|
if (dm === 'raw') body = rawBody(tab, text);
|
|
832
720
|
else if (dm === 'diff') body = diffBody(tab, raw, text, dirty);
|
|
@@ -839,16 +727,133 @@ export function mountDoc(host, tab) {
|
|
|
839
727
|
if (body.defaultValue !== text) body.defaultValue = text;
|
|
840
728
|
if (body.value !== text) body.value = text;
|
|
841
729
|
} else if (!FIXTURE && (extOfPath(tab.path) === 'csv' || extOfPath(tab.path) === 'tsv')) body = gridBody(tab, text);
|
|
730
|
+
else if (writable) body = inPlace(tab, text, raw);
|
|
842
731
|
else body = asPage ? htmlFrame(tab, text, live) : (nonMd && !unread.has(docKey(tab)) ? sourceBody(tab, text) : viewBody(tab, text));
|
|
843
732
|
|
|
844
|
-
|
|
845
|
-
* margin is written on every paint so a closed drawer leaves no gap behind on a kept node. */
|
|
846
|
-
const drawerOn = canAlign(tab) && alignOpen.has(tab.id);
|
|
847
|
-
host.style.position = drawerOn ? 'relative' : '';
|
|
848
|
-
modeBar.style.marginRight = drawerOn ? ALIGN_W : '';
|
|
849
|
-
body.style.marginRight = drawerOn ? ALIGN_W : '';
|
|
850
|
-
setChildren(host, drawerOn ? [modeBar, body, alignDrawer(tab)] : [modeBar, body]);
|
|
733
|
+
setChildren(host, [bar, body]);
|
|
851
734
|
return host;
|
|
852
735
|
}
|
|
853
736
|
|
|
737
|
+
/* A markdown file the PM can write, in live mode: edited in place. A guide page ships with the app, a file from git is
|
|
738
|
+
* evidence, a reference names someone else's record, and a file that could not be read is a stub: none of those. */
|
|
739
|
+
function canWrite(tab, nonMd) {
|
|
740
|
+
if (FIXTURE || tab.readonly || tab.guide || tab.ref || nonMd || isReference(tab.path) || isHtmlDoc(tab.path)) return false;
|
|
741
|
+
const PK = typeof window !== 'undefined' ? window.PreviewKind : null;
|
|
742
|
+
if (!PK || typeof PK.previewKind !== 'function' || PK.previewKind(String(tab.path || '')) !== 'markdown') return false;
|
|
743
|
+
const k = docKey(tab);
|
|
744
|
+
return state.savedDocs[k] != null || (disk.has(k) && !unread.has(k));
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/* The in-place surface: the editor and, beside it, the rail. One scrolling node per tab, kept like the scrollers above,
|
|
748
|
+
* so neither a repaint nor a keystroke moves the page. */
|
|
749
|
+
const places = new Map(); // tab id -> { node, grid }
|
|
750
|
+
function inPlace(tab, text, raw) {
|
|
751
|
+
let pl = places.get(tab.id);
|
|
752
|
+
if (!pl) {
|
|
753
|
+
const grid = el('div', { class: 'fr-ed__grid' });
|
|
754
|
+
pl = { node: el('div', { class: 'fr-ed', 'data-ed': 'scroll' }, grid), grid };
|
|
755
|
+
places.set(tab.id, pl);
|
|
756
|
+
}
|
|
757
|
+
const ws = wsOf(tab);
|
|
758
|
+
const main = editorNode(tab, {
|
|
759
|
+
text, ctx: linkCtx(tab),
|
|
760
|
+
onChange: (t) => { saveErr.delete(docKey(tab)); setState((s) => { const de = { ...s.docEdits }; if (t === raw) delete de[docKey(tab)]; else de[docKey(tab)] = t; return { docEdits: de }; }); },
|
|
761
|
+
onSave: () => { const cur = state.docEdits[docKey(tab)]; if (cur != null && cur !== raw) save(tab, cur); },
|
|
762
|
+
});
|
|
763
|
+
const showRail = canRail(tab) && !railHidden.has(tab.id);
|
|
764
|
+
const rail = showRail ? railNode(tab, {
|
|
765
|
+
ws, rel: String(tab.path || ''), heads: headsOf(tab.id), findings: findingsFor(ws, String(tab.path || '')),
|
|
766
|
+
onGoto: (i) => goToBlock(tab.id, i),
|
|
767
|
+
onOpen: (rel) => openDoc(tab.slug, rel, 'FILE', tab.ws ? { ws: tab.ws, branch: tab.branch || null, linked: true } : undefined),
|
|
768
|
+
}) : null;
|
|
769
|
+
pl.grid.setAttribute('data-measure', narrowOf(tab) ? '72ch' : 'full');
|
|
770
|
+
pl.grid.toggleAttribute('data-rail', !!rail);
|
|
771
|
+
setChildren(pl.grid, [main, rail]);
|
|
772
|
+
if (isEditing(tab.id)) pl.node.setAttribute('data-editing', ''); else pl.node.removeAttribute('data-editing');
|
|
773
|
+
return pl.node;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/** normalize: the buffer's paragraphs joined into one line each. The buffer, never the file; a clean document says so. */
|
|
777
|
+
function normalizeBuffer(tab, norm) {
|
|
778
|
+
const raw = docText(tab);
|
|
779
|
+
const text = state.docEdits[docKey(tab)] ?? raw;
|
|
780
|
+
const r = norm.normalize(text);
|
|
781
|
+
normNote.set(tab.id, r.changed
|
|
782
|
+
? r.joins + ' line(s) joined' + (r.blanks ? ', ' + r.blanks + ' blank inserted' : '')
|
|
783
|
+
: 'already normalized — every paragraph is one line');
|
|
784
|
+
if (!r.changed) { setState({}); return; }
|
|
785
|
+
setState((s) => ({ docEdits: { ...s.docEdits, [docKey(tab)]: r.text } }));
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/* ── the document bar in live mode ──────────────────────────────────────────────────────────────────────────────────
|
|
789
|
+
* One bar per tab, kept so the ⋯ control keeps its focus across a repaint; its children are rebuilt. The bar says whether
|
|
790
|
+
* the file is saved and what its copy holds, and carries what changes the view: Changes while there is something to
|
|
791
|
+
* compare, the whole file as markdown (or, for a file that cannot be edited, its source), and one ⋯ menu for what is used
|
|
792
|
+
* now and then. Run scripts stays on the bar while a page can run: it grants a privilege, so its state is never one
|
|
793
|
+
* click away. Every action is the prototype's: the same state keys, the same buffer, the same save path. */
|
|
794
|
+
const docBars = new Map(); // tab id -> { node, more }
|
|
795
|
+
|
|
796
|
+
function docBar(tab, o) {
|
|
797
|
+
let kept = docBars.get(tab.id);
|
|
798
|
+
if (!kept) {
|
|
799
|
+
const more = iconButton({ icon: 'ellipsis', ariaLabel: 'Document options', size: 'sm', attrs: { 'data-doc': 'more', class: 'fr-doc-bar__more' },
|
|
800
|
+
onClick: (e) => menu({ anchor: e.currentTarget, ariaLabel: 'Document options', items: docMenu(tab) }) });
|
|
801
|
+
kept = { node: el('div', { class: 'fr-doc-bar', 'data-doc': 'bar' }), more };
|
|
802
|
+
docBars.set(tab.id, kept);
|
|
803
|
+
}
|
|
804
|
+
const setMode = (m) => setState((s) => ({ docModes: { ...s.docModes, [tab.id]: m } }));
|
|
805
|
+
const status = el('span', { class: 'fr-doc-bar__status', 'data-doc': 'status', 'data-tone': o.tone }, o.statusText);
|
|
806
|
+
const copy = canRail(tab) && !o.readonly ? copyChip(wsOf(tab), String(tab.path || '')) : null;
|
|
807
|
+
const inDiff = o.dm === 'diff';
|
|
808
|
+
const changes = o.writable && (o.dirty || inDiff) ? button({
|
|
809
|
+
label: 'changes', variant: inDiff ? 'outline' : 'ghost', size: 'sm', icon: 'git-compare',
|
|
810
|
+
attrs: { 'data-doc': 'changes', 'aria-pressed': inDiff ? 'true' : 'false', title: 'What saving would change on disk, line by line' },
|
|
811
|
+
onClick: () => setMode(inDiff ? 'view' : 'diff'),
|
|
812
|
+
}) : null;
|
|
813
|
+
const srcMode = o.writable ? 'edit' : 'raw';
|
|
814
|
+
const inSrc = o.dm === srcMode;
|
|
815
|
+
const source = button({
|
|
816
|
+
label: o.writable ? 'markdown' : 'source', variant: inSrc ? 'outline' : 'ghost', size: 'sm', icon: 'file-code',
|
|
817
|
+
attrs: { 'data-doc': 'source', 'aria-pressed': inSrc ? 'true' : 'false',
|
|
818
|
+
title: o.writable ? 'Edit the whole file as markdown' : 'Show the file as it is written' },
|
|
819
|
+
onClick: () => setMode(inSrc ? 'view' : srcMode),
|
|
820
|
+
});
|
|
821
|
+
const saveB = o.dirty ? button({ label: 'save', variant: 'primary', size: 'sm', attrs: { 'data-doc': 'save', title: 'Save (Ctrl or ⌘ S)' }, onClick: () => save(tab, o.text) }) : null;
|
|
822
|
+
const revertB = o.dirty ? button({ label: 'revert', variant: 'ghost', size: 'sm', attrs: { 'data-doc': 'revert' }, onClick: () => revert(tab) }) : null;
|
|
823
|
+
const run = o.asPage && o.dm === 'view' ? button({
|
|
824
|
+
label: o.live ? 'scripts running — stop' : 'run scripts', variant: o.live ? 'danger' : 'outline', size: 'sm',
|
|
825
|
+
onClick: () => { if (o.live) running.delete(tab.id); else running.add(tab.id); setState({}); },
|
|
826
|
+
attrs: { 'data-doc': 'run', 'aria-pressed': o.live ? 'true' : 'false',
|
|
827
|
+
title: o.live
|
|
828
|
+
? 'This document is running its own scripts, in a frame with no origin and no network. Stop discards it.'
|
|
829
|
+
: "Run this document's own scripts. It stays sandboxed: no origin, no network, nothing loaded from anywhere." },
|
|
830
|
+
}) : null;
|
|
831
|
+
const hasMenu = docMenu(tab).length > 0;
|
|
832
|
+
setChildren(kept.node, [status, copy, el('span', { class: 'fr-doc-bar__end' }, run, changes, revertB, saveB, source, hasMenu ? kept.more : null)]);
|
|
833
|
+
return kept.node;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/** The ⋯ menu's items, read afresh when it opens, so each one says what choosing it will do now. */
|
|
837
|
+
function docMenu(tab) {
|
|
838
|
+
const items = [];
|
|
839
|
+
const dm = state.docModes[tab.id] ?? 'view';
|
|
840
|
+
if (dm === 'view' || dm === 'raw') {
|
|
841
|
+
const narrow = narrowOf(tab);
|
|
842
|
+
items.push({ label: narrow ? 'Use the full width' : 'Limit lines to 72 characters', icon: 'arrow-left-right', attrs: { 'data-r1': 'measure' },
|
|
843
|
+
onSelect: () => setState((s) => ({ docNarrow: { ...(s.docNarrow || {}), [tab.id]: !narrowOf(tab) } })) });
|
|
844
|
+
}
|
|
845
|
+
const norm = typeof window !== 'undefined' ? window.MdNormalize : null;
|
|
846
|
+
const nonMd = isSourcePath(tab.path);
|
|
847
|
+
if (norm && typeof norm.normalize === 'function' && !tab.readonly && !isHtmlDoc(tab.path) && !isReference(tab.path) && !unread.has(docKey(tab)) && !nonMd) {
|
|
848
|
+
items.push({ label: 'Normalize line breaks', icon: 'refresh-cw', attrs: { 'data-doc': 'normalize', title: 'Join hard-wrapped paragraphs into one line each. Writes the buffer, not the file: read it in Changes, then save.' },
|
|
849
|
+
onSelect: () => normalizeBuffer(tab, norm) });
|
|
850
|
+
}
|
|
851
|
+
if (canRail(tab) && canWrite(tab, nonMd)) {
|
|
852
|
+
const hidden = railHidden.has(tab.id);
|
|
853
|
+
items.push({ label: hidden ? 'Show the outline and connections' : 'Hide the outline and connections', icon: 'share-2', attrs: { 'data-doc': 'rail' },
|
|
854
|
+
onSelect: () => { if (hidden) railHidden.delete(tab.id); else railHidden.add(tab.id); setState({}); } });
|
|
855
|
+
}
|
|
856
|
+
return items;
|
|
857
|
+
}
|
|
858
|
+
|
|
854
859
|
export default mountDoc;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* lucide-sprite.js — GENERATED by workspace/third-party/build-lucide-sprite.js from lucide-static 1.52.0.
|
|
2
|
-
* Do not edit by hand. The icons: grip-vertical, plus, check, square, square-check-big, x, undo-2, flag, bot, chevron-right, chevron-down, ellipsis, circle-alert, history, arrow-up, arrow-down, diamond, settings, search, refresh-cw, chevron-left, sun, moon, arrow-left, arrow-right, globe, columns-2, arrow-up-down, arrow-left-right, panel-top, panel-left, maximize-2, minimize-2, git-branch, clock.
|
|
2
|
+
* Do not edit by hand. The icons: grip-vertical, plus, check, square, square-check-big, x, undo-2, flag, bot, chevron-right, chevron-down, ellipsis, circle-alert, history, arrow-up, arrow-down, diamond, settings, search, refresh-cw, chevron-left, sun, moon, arrow-left, arrow-right, globe, columns-2, arrow-up-down, arrow-left-right, panel-top, panel-left, maximize-2, minimize-2, git-branch, clock, bold, italic, code, link, type, heading, list, list-checks, text-quote, file-code, git-compare, cloud, share-2.
|
|
3
3
|
*
|
|
4
4
|
* ISC License
|
|
5
5
|
*
|
|
@@ -81,5 +81,18 @@ export const SPRITE = '<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true
|
|
|
81
81
|
+ '<symbol id="lucide-minimize-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m14 10 7-7" /><path d="M20 10h-6V4" /><path d="m3 21 7-7" /><path d="M4 14h6v6" /></symbol>'
|
|
82
82
|
+ '<symbol id="lucide-git-branch" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 6a9 9 0 0 0-9 9V3" /><circle cx="18" cy="6" r="3" /><circle cx="6" cy="18" r="3" /></symbol>'
|
|
83
83
|
+ '<symbol id="lucide-clock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /><path d="M12 6v6l4 2" /></symbol>'
|
|
84
|
+
+ '<symbol id="lucide-bold" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 12h9a4 4 0 0 1 0 8H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h7a4 4 0 0 1 0 8" /></symbol>'
|
|
85
|
+
+ '<symbol id="lucide-italic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="19" x2="10" y1="4" y2="4" /><line x1="14" x2="5" y1="20" y2="20" /><line x1="15" x2="9" y1="4" y2="20" /></symbol>'
|
|
86
|
+
+ '<symbol id="lucide-code" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m16 18 6-6-6-6" /><path d="m8 6-6 6 6 6" /></symbol>'
|
|
87
|
+
+ '<symbol id="lucide-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" /><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" /></symbol>'
|
|
88
|
+
+ '<symbol id="lucide-type" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 4v16" /><path d="M4 7V5a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v2" /><path d="M9 20h6" /></symbol>'
|
|
89
|
+
+ '<symbol id="lucide-heading" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 12h12" /><path d="M6 20V4" /><path d="M18 20V4" /></symbol>'
|
|
90
|
+
+ '<symbol id="lucide-list" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 5h.01" /><path d="M3 12h.01" /><path d="M3 19h.01" /><path d="M8 5h13" /><path d="M8 12h13" /><path d="M8 19h13" /></symbol>'
|
|
91
|
+
+ '<symbol id="lucide-list-checks" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M13 5h8" /><path d="M13 12h8" /><path d="M13 19h8" /><path d="m3 17 2 2 4-4" /><path d="m3 7 2 2 4-4" /></symbol>'
|
|
92
|
+
+ '<symbol id="lucide-text-quote" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 5H3" /><path d="M21 12H8" /><path d="M21 19H8" /><path d="M3 12v7" /></symbol>'
|
|
93
|
+
+ '<symbol id="lucide-file-code" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" /><path d="M14 2v5a1 1 0 0 0 1 1h5" /><path d="M10 12.5 8 15l2 2.5" /><path d="m14 12.5 2 2.5-2 2.5" /></symbol>'
|
|
94
|
+
+ '<symbol id="lucide-git-compare" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="18" r="3" /><circle cx="6" cy="6" r="3" /><path d="M13 6h3a2 2 0 0 1 2 2v7" /><path d="M11 18H8a2 2 0 0 1-2-2V9" /></symbol>'
|
|
95
|
+
+ '<symbol id="lucide-cloud" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z" /></symbol>'
|
|
96
|
+
+ '<symbol id="lucide-share-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="3" /><circle cx="6" cy="12" r="3" /><circle cx="18" cy="19" r="3" /><line x1="8.59" x2="15.42" y1="13.51" y2="17.49" /><line x1="15.41" x2="8.59" y1="6.51" y2="10.49" /></symbol>'
|
|
84
97
|
+ '</svg>';
|
|
85
|
-
export const ICONS = ["grip-vertical","plus","check","square","square-check-big","x","undo-2","flag","bot","chevron-right","chevron-down","ellipsis","circle-alert","history","arrow-up","arrow-down","diamond","settings","search","refresh-cw","chevron-left","sun","moon","arrow-left","arrow-right","globe","columns-2","arrow-up-down","arrow-left-right","panel-top","panel-left","maximize-2","minimize-2","git-branch","clock"];
|
|
98
|
+
export const ICONS = ["grip-vertical","plus","check","square","square-check-big","x","undo-2","flag","bot","chevron-right","chevron-down","ellipsis","circle-alert","history","arrow-up","arrow-down","diamond","settings","search","refresh-cw","chevron-left","sun","moon","arrow-left","arrow-right","globe","columns-2","arrow-up-down","arrow-left-right","panel-top","panel-left","maximize-2","minimize-2","git-branch","clock","bold","italic","code","link","type","heading","list","list-checks","text-quote","file-code","git-compare","cloud","share-2"];
|
|
@@ -80,6 +80,49 @@
|
|
|
80
80
|
"no-network: the server process opens no HTTP client outside the one TypeSafe module (D79)",
|
|
81
81
|
"no-network: lib/typesafe.js is the ONLY file that opens an HTTP client"
|
|
82
82
|
]
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"id": "STD-R18",
|
|
86
|
+
"file": "workspace/pm-r18-test.js",
|
|
87
|
+
"run": "workspace-tabs-declutter",
|
|
88
|
+
"ref": "planning/initiatives/workspace-tabs-declutter.md (status, amended 2026-10-07 after the PM approved the clickable mockup)",
|
|
89
|
+
"reason": "A file that cannot be edited now has one Source toggle instead of the view and raw modes. The assertion keeps its name and still checks that a git file offers no edit, diff or save; it reads the new control names (source, markdown, changes).",
|
|
90
|
+
"names": [
|
|
91
|
+
"R5: a git file offers no edit, diff or save"
|
|
92
|
+
]
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"id": "STD-R21",
|
|
96
|
+
"file": "workspace/pm-r21-test.js",
|
|
97
|
+
"run": "workspace-tabs-declutter",
|
|
98
|
+
"ref": "planning/initiatives/workspace-tabs-declutter.md (status, amended 2026-10-07 after the PM approved the clickable mockup)",
|
|
99
|
+
"reason": "The four document modes became toggles: a markdown file is edited in place with a Markdown toggle, and a read-only file has a Source toggle. F7 now checks that the source is one toggle away; F10 checks that a markdown tab is edited in place and offers its markdown.",
|
|
100
|
+
"names": [
|
|
101
|
+
"F7: view and raw are still offered",
|
|
102
|
+
"F10: a markdown tab still offers edit"
|
|
103
|
+
]
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
"id": "STD-TABS",
|
|
107
|
+
"file": "workspace/pm-tabs-test.js",
|
|
108
|
+
"run": "workspace-tabs-declutter",
|
|
109
|
+
"ref": "planning/initiatives/workspace-tabs-declutter.md (status, amended 2026-10-07 after the PM approved the clickable mockup)",
|
|
110
|
+
"reason": "A markdown file has no raw mode any more: its source is the Markdown toggle, a textarea that scrolls itself. The scroll-position assertion now holds the Markdown source's position. pm-doc-inplace-test.js asserts the in-place editor.",
|
|
111
|
+
"names": [
|
|
112
|
+
"raw mode holds its position too — the fix is the pane's, not one mode's"
|
|
113
|
+
]
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"id": "STD-UI",
|
|
117
|
+
"file": "workspace/ui-test.js",
|
|
118
|
+
"run": "workspace-tabs-declutter",
|
|
119
|
+
"ref": "planning/initiatives/workspace-tabs-declutter.md (status, amended 2026-10-07 after the PM approved the clickable mockup)",
|
|
120
|
+
"reason": "The document round trip now goes through the in-place editor: DOC1 checks the tab is edited in place with a Markdown toggle (not four modes), DOC2 counts the Properties inputs (one per frontmatter key) instead of the old card's spans, and DOC4 reads the whole file from the Markdown toggle's textarea instead of raw. DOC5 and DOC6 keep their names and still type, diff and save the real file.",
|
|
121
|
+
"names": [
|
|
122
|
+
"DOC1: a document opens as a doc tab offering view · raw · edit · diff",
|
|
123
|
+
"DOC4: raw shows the file as written, frontmatter fence and all",
|
|
124
|
+
"DOC2: view renders the FILE ON DISK as markdown — a heading, its frontmatter card, its list"
|
|
125
|
+
]
|
|
83
126
|
}
|
|
84
127
|
],
|
|
85
128
|
"entries": [
|
package/workspace/server.js
CHANGED
|
@@ -119,6 +119,8 @@ const startPolicy = require('./lib/start-policy'); // ONE stated rule for wh
|
|
|
119
119
|
const treeShare = require('./lib/tree-share'); // who shares a working tree, and who is DECLARED to be driving it
|
|
120
120
|
const tmuxControl = require('./lib/tmux-control'); // tmux -CC protocol: the pane's raw stream, so xterm.js keeps its scrollback
|
|
121
121
|
const relatedDocs = require('./lib/related-docs'); // which documents a change puts out of step (R5/R6)
|
|
122
|
+
const Kg = require('./lib/kg'); // the stated ties between files (pmos kg)
|
|
123
|
+
const kgCli = require('./lib/kg-cli'); // …and its cached build for one repository
|
|
122
124
|
const applyChange = require('./lib/apply-change'); // anchored + whole-document saves, refuse-on-stale
|
|
123
125
|
const sessionState = require('./lib/session-state'); // the cadence×turn join — validated on BOTH sides
|
|
124
126
|
// Bundle 2 (planning/design/bundle-2-wiring-contract.md). Four already-built pure modules. The
|
|
@@ -138,6 +140,7 @@ const docCheckSessions = new Map();
|
|
|
138
140
|
// The session each Use … version press started (workspace id|path|kind → { session, use }), so the line says "Updating…"
|
|
139
141
|
// before the session has opened its sync, and a second press on the same copy refuses.
|
|
140
142
|
const docSyncSessions = new Map();
|
|
143
|
+
const docGraphs = new Map(); // root -> { at, graph }: the document tab's Connections, held 15 s
|
|
141
144
|
const jiraTickets = require('./lib/jira-tickets.js'); // jira-wbs — the ticket draft, the PM's selection, the push plan
|
|
142
145
|
const jiraStatus = require('./lib/jira-status.js'); // …and the status snapshot an agent's read writes
|
|
143
146
|
// The session each Push or Check now press started (workspace id|initiative → { session, at }), so a second press
|
|
@@ -8248,6 +8251,33 @@ async function handleRequest(req, res) {
|
|
|
8248
8251
|
}
|
|
8249
8252
|
return json(res, { ok: true, wrote: rel });
|
|
8250
8253
|
}
|
|
8254
|
+
if (p === '/api/doc/graph') {
|
|
8255
|
+
// The document tab's Connections (workspace-tabs-declutter): the files this file is tied to, read from the same
|
|
8256
|
+
// derived graph `pmos kg neighbors` prints. Only stated ties: a link, a frontmatter path, an initiative, an anchor
|
|
8257
|
+
// or a decision id. Nothing is guessed. The graph is built from git's tracked Markdown and cached by HEAD and the
|
|
8258
|
+
// dirty .md files, so a second read is a file read; a short in-memory hold keeps a repaint from asking git again.
|
|
8259
|
+
const w = getWs(url.searchParams.get('ws'));
|
|
8260
|
+
if (!w) return json(res, { ok: false, error: 'no workspace' }, 404);
|
|
8261
|
+
const rel = String(url.searchParams.get('path') || '');
|
|
8262
|
+
if (!rel || !insideRoot(w.root, path.resolve(w.root, rel))) return json(res, { ok: false, error: 'outside workspace' }, 404);
|
|
8263
|
+
let held = docGraphs.get(w.root);
|
|
8264
|
+
// A folder that is not a git repository, or one with no commit yet, tracks no file, so no tie is known.
|
|
8265
|
+
if (!held && spawnSync('git', ['rev-parse', '--verify', '-q', 'HEAD'], { cwd: w.root, stdio: 'ignore' }).status !== 0) {
|
|
8266
|
+
return json(res, { ok: true, tracked: false, ties: [] });
|
|
8267
|
+
}
|
|
8268
|
+
if (!held || Date.now() - held.at > 15000) {
|
|
8269
|
+
try { held = { at: Date.now(), graph: kgCli.graphFor(w.root).graph }; docGraphs.set(w.root, held); }
|
|
8270
|
+
catch (e) { return json(res, { ok: false, error: 'the graph could not be built: ' + ((e && e.message) || e).toString().split('\n')[0] }, 500); }
|
|
8271
|
+
}
|
|
8272
|
+
const g = held.graph;
|
|
8273
|
+
const me = Kg.nodeById(g, rel);
|
|
8274
|
+
if (!me) return json(res, { ok: true, tracked: false, ties: [] });
|
|
8275
|
+
const ties = (Kg.neighbors(g, rel, { depth: 1 }) || []).slice(0, 40).map((r) => {
|
|
8276
|
+
const n = Kg.nodeById(g, r.id) || {};
|
|
8277
|
+
return { path: r.id, title: n.title || r.id, type: n.type || null, via: r.via, basis: r.basis, direction: r.direction };
|
|
8278
|
+
});
|
|
8279
|
+
return json(res, { ok: true, tracked: true, title: me.title, ties });
|
|
8280
|
+
}
|
|
8251
8281
|
if (p === '/api/doc/changes') {
|
|
8252
8282
|
// The change log, read back (R8 — this is what an agent reads before working here).
|
|
8253
8283
|
const w = getWs(url.searchParams.get('ws'));
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "workspace-tabs-declutter: the document tab edited in place (block model, browser).",
|
|
3
|
+
"modules": [
|
|
4
|
+
"doc-editor.test.mjs"
|
|
5
|
+
],
|
|
6
|
+
"seconds": {
|
|
7
|
+
"doc-editor.test.mjs": 1,
|
|
8
|
+
"pm-doc-inplace-test.js": 25
|
|
9
|
+
},
|
|
10
|
+
"panels": [
|
|
11
|
+
"pm-doc-inplace-test.js"
|
|
12
|
+
]
|
|
13
|
+
}
|
|
@@ -12,7 +12,7 @@ The workspace runs with no network (D62). Two third-party browser files are comm
|
|
|
12
12
|
| Package | Version | Licence | Tarball | Tarball sha256 | Shipped file |
|
|
13
13
|
|---|---|---|---|---|---|
|
|
14
14
|
| sortablejs | 1.15.7 | MIT | https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.7.tgz | `66cbfc8fa18860783584fe0c346baf6cbf3399da72d23d21053425ba232abed4` | `public/vendor/Sortable.min.js`: an unmodified copy of `package/Sortable.min.js` (45 478 bytes, sha256 `bf4241bc73fef7f11c59a283a69fe8051cdd31c6d8ff5a2b9ba219e7831fcf76`; npm integrity `sha512-Kk8wLQPlS+yi1ZEf48a4+fzHa4yxjC30M/Sr2AnQu+f/MPwvvX9XjZ6OWejiz8crBsLwSq8GHqaxaET7u6ux0A==`) |
|
|
15
|
-
| lucide-static | 1.52.0 | ISC, plus the Feather MIT notice | https://registry.npmjs.org/lucide-static/-/lucide-static-1.52.0.tgz | `4d2aa079171e4eba4c704844b8b0c9edb6e4cfb2fb8bd86f7b14353994f04a87` | `public/vendor/lucide-sprite.js`: generated by `build-lucide-sprite.js` (
|
|
15
|
+
| lucide-static | 1.52.0 | ISC, plus the Feather MIT notice | https://registry.npmjs.org/lucide-static/-/lucide-static-1.52.0.tgz | `4d2aa079171e4eba4c704844b8b0c9edb6e4cfb2fb8bd86f7b14353994f04a87` | `public/vendor/lucide-sprite.js`: generated by `build-lucide-sprite.js` (17 352 bytes, sha256 `7f9e952399a3c1c91659a527bc1363a008b531ed6e296c2b3c819554ffb0a56f`). It holds 48 icons: grip-vertical, plus, check, square, square-check-big, x, undo-2, flag, bot, chevron-right, chevron-down, ellipsis, circle-alert, history, arrow-up, arrow-down, diamond, then the eight the frame's chrome draws (Run 3): settings, search, refresh-cw, chevron-left, sun, moon, arrow-left, arrow-right, then globe, columns-2, arrow-up-down and arrow-left-right for the dock strip's web, split and orientation chips (Run 3, C6), then panel-top, panel-left, maximize-2, minimize-2, git-branch and clock for the dock's icon controls and session rows (0.54.2), then bold, italic, code, link, type, heading, list, list-checks, text-quote, file-code, git-compare, cloud and share-2 for the document tab's in-place editor, its Confluence copy and its connections |
|
|
16
16
|
|
|
17
17
|
The licences are in this folder: [sortablejs-1.15.7.LICENSE](sortablejs-1.15.7.LICENSE) and [lucide-static-1.52.0.LICENSE](lucide-static-1.52.0.LICENSE). The sprite also carries the lucide licence text as its first comment.
|
|
18
18
|
|