create-pmos 0.67.1 → 0.69.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/initiatives-screen.md +1 -1
- package/workspace/guide/layout.md +35 -8
- package/workspace/guide/notes.md +12 -1
- package/workspace/guide/today.md +3 -1
- package/workspace/lib/notes-links.js +56 -0
- package/workspace/lib/notes.js +87 -5
- package/workspace/lib/routes-notes.js +47 -6
- package/workspace/package-lock.json +1 -1
- package/workspace/package.json +1 -1
- package/workspace/public/pm/components.css +697 -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/initiative-sheet.js +10 -2
- package/workspace/public/pm/initiatives-copy.js +1 -0
- package/workspace/public/pm/notes-links.js +110 -0
- 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/pm/today-agenda.js +6 -1
- package/workspace/public/pm/today-day.js +6 -0
- package/workspace/public/pm/view-notes.js +113 -6
- 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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* pm/view-notes.js — the Notes view tab: one meeting on one screen (meeting-notes, Runs 1 to
|
|
1
|
+
/* pm/view-notes.js — the Notes view tab: one meeting on one screen (meeting-notes, Runs 1 to 4).
|
|
2
2
|
*
|
|
3
3
|
* The screen follows the simplified mockup the PM reviewed (prototype/meeting-notes):
|
|
4
4
|
* header the title and the date, and one action: Listen; while listening a red dot, the time, the sources and Stop;
|
|
@@ -24,10 +24,15 @@
|
|
|
24
24
|
* the transcript line, its item and the note line written at that moment are highlighted, and a time in the transcript
|
|
25
25
|
* jumps there. Your notes are shown read-only while the meeting plays, so their lines can be highlighted.
|
|
26
26
|
*
|
|
27
|
+
* MEETING AND INITIATIVE (Run 4). Under the title, the note says which meeting it is (a calendar event, or not on the
|
|
28
|
+
* calendar) and which initiative it belongs to, and how that was set: from the calendar event, suggested from what was said,
|
|
29
|
+
* or chosen by the PM. Change opens a sheet for each. A note taken from an event's detail (pm/notes-links.js) opens here.
|
|
30
|
+
*
|
|
27
31
|
* Reads and writes (lib/routes-notes.js). Notes live in the per-user runtime directory, never in a repository:
|
|
28
32
|
* GET /api/notes · /api/notes/one?id=… · /api/notes/engine?ws=… · /api/notes/recording?id=… · /api/people?ws=…
|
|
33
|
+
* · /api/notes/initiatives?ws=…&id=… · /api/proactive/agenda?month=…
|
|
29
34
|
* POST /api/notes/new · save · dictate · record · transcript · audio · label · speaker · switch · whisper-setup · private
|
|
30
|
-
* · summarize · summarize-agent · delete · line · recording-delete
|
|
35
|
+
* · summarize · summarize-agent · delete · line · recording-delete · link
|
|
31
36
|
*/
|
|
32
37
|
import { el } from './dom.js';
|
|
33
38
|
import { askConfirm, askText } from './dialogs.js';
|
|
@@ -58,6 +63,7 @@ let pollTimer = null; let agentTimer = null;
|
|
|
58
63
|
let busy = '';
|
|
59
64
|
let player = null; // the replay: { id, bytes, el, playing }, kept across redraws so playback goes on
|
|
60
65
|
let people = null; // { ws, names } — the repository's people, offered in the line editor
|
|
66
|
+
let pendingOpen = null; // a note another page asked to open (openNote), selected at the next draw
|
|
61
67
|
const LABELS = [['decision', 'Decision'], ['action', 'Action'], ['question', 'Question'], ['fact', 'Fact'], ['topic', 'Topic'], ['statement', 'Statement'], ['small talk', 'Small talk']];
|
|
62
68
|
|
|
63
69
|
const nowClock = (iso) => { try { return new Date(iso).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); } catch { return ''; } };
|
|
@@ -199,7 +205,7 @@ async function loadList(ctx) {
|
|
|
199
205
|
if (!currentId && list.notes.length) currentId = list.notes[0].id;
|
|
200
206
|
ctx.setState({});
|
|
201
207
|
}
|
|
202
|
-
const sig = (n) => JSON.stringify([n.transcript, n.summary && n.summary.at, n.agent && n.agent.markdown, n.private, n.speakers, n.title, (n.versions || []).length, n.__recording && n.__recording.bytes]);
|
|
208
|
+
const sig = (n) => JSON.stringify([n.transcript, n.summary && n.summary.at, n.agent && n.agent.markdown, n.private, n.speakers, n.title, (n.versions || []).length, n.__recording && n.__recording.bytes, n.meeting, n.initiative]);
|
|
203
209
|
async function loadNote(ctx, id) {
|
|
204
210
|
const r = await pmGet('/api/notes/one?id=' + encodeURIComponent(id));
|
|
205
211
|
if (r && r.ok) {
|
|
@@ -312,9 +318,10 @@ async function setPrivate(ctx, n, priv) {
|
|
|
312
318
|
async function summarize(ctx, n, quiet = false) {
|
|
313
319
|
await flushSave(ctx);
|
|
314
320
|
busy = 'summary'; ctx.setState({});
|
|
315
|
-
const
|
|
321
|
+
const ws = n.ws || wsOf(ctx);
|
|
322
|
+
const r = await pmPost('/api/notes/summarize', { id: n.id, ...(ws ? { ws } : {}) });
|
|
316
323
|
busy = '';
|
|
317
|
-
if (r && r.ok) { n.summary = r.summary; banner = null; } else if (!quiet) banner = { tone: 'bad', text: (r && r.error) || 'no summary was made' };
|
|
324
|
+
if (r && r.ok) { n.summary = r.summary; if (r.initiative !== undefined) { n.initiative = r.initiative; const c = cache.get(n.id); if (c) c.initiative = r.initiative; } banner = null; } else if (!quiet) banner = { tone: 'bad', text: (r && r.error) || 'no summary was made' };
|
|
318
325
|
ctx.setState({});
|
|
319
326
|
}
|
|
320
327
|
async function summarizeAgent(ctx, n) {
|
|
@@ -762,6 +769,105 @@ function editorFor(ctx, n) {
|
|
|
762
769
|
title.addEventListener('blur', () => flushSave(ctx));
|
|
763
770
|
return editor;
|
|
764
771
|
}
|
|
772
|
+
/* ── the meeting and the initiative of a note (Run 4) ───────────────────────────────────────────── */
|
|
773
|
+
const BY_WORDS = { event: 'from the calendar event', local: 'suggested from what was said', pm: 'chosen by you' };
|
|
774
|
+
const ymOfIso = (iso) => { const d = new Date(iso); return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0'); };
|
|
775
|
+
const dateOfIso = (iso) => { const d = new Date(iso); return ymOfIso(iso) + '-' + String(d.getDate()).padStart(2, '0'); };
|
|
776
|
+
/** Open the Notes tab on this note at its next draw (pm/notes-links.js openNote). */
|
|
777
|
+
export function openNote(id) { pendingOpen = id; }
|
|
778
|
+
async function link(ctx, n, patch, said) {
|
|
779
|
+
const r = await pmPost('/api/notes/link', { id: n.id, ...patch });
|
|
780
|
+
if (r && r.ok) {
|
|
781
|
+
for (const x of new Set([n, cache.get(n.id)])) if (x) { x.meeting = r.note.meeting; x.initiative = r.note.initiative; x.title = r.note.title; }
|
|
782
|
+
if (editor && editor.id === n.id && !editor.title.value.trim()) editor.title.value = r.note.title || '';
|
|
783
|
+
touchRow(n); banner = { tone: 'ok', text: said(r.note) };
|
|
784
|
+
import('./notes-links.js').then((m) => m.refreshNotes()).catch(() => {});
|
|
785
|
+
} else banner = { tone: 'bad', text: (r && r.error) || 'the link was not changed' };
|
|
786
|
+
ctx.setState({});
|
|
787
|
+
}
|
|
788
|
+
/** The events of the note's day and the day before, from the Agenda's read. */
|
|
789
|
+
async function eventsNear(n) {
|
|
790
|
+
const at = n.created_at || new Date().toISOString();
|
|
791
|
+
const day = dateOfIso(at); const before = dateOfIso(new Date(Date.parse(at) - 86400000).toISOString());
|
|
792
|
+
const months = [...new Set([ymOfIso(at), before.slice(0, 7)])];
|
|
793
|
+
const reads = await Promise.all(months.map((m) => pmGet('/api/proactive/agenda?month=' + m)));
|
|
794
|
+
const out = []; const seen = new Set();
|
|
795
|
+
for (const r of reads) {
|
|
796
|
+
if (!r || !r.days) continue;
|
|
797
|
+
for (const d of [before, day]) for (const e of ((r.days[d] && r.days[d].events) || [])) { const k = e.start + '|' + e.title; if (!seen.has(k)) { seen.add(k); out.push({ ...e, date: d }); } }
|
|
798
|
+
}
|
|
799
|
+
return out.sort((a, b) => String(a.start).localeCompare(String(b.start)));
|
|
800
|
+
}
|
|
801
|
+
async function meetingSheet(ctx, n) {
|
|
802
|
+
const box = el('div', { class: 'nt-picks', role: 'radiogroup', 'aria-label': 'Meeting' }, el('p', { class: 'nt-hint' }, 'reading your calendar …'));
|
|
803
|
+
let pick = n.meeting && n.meeting.kind === 'adhoc' ? 'adhoc' : null;
|
|
804
|
+
let evs = [];
|
|
805
|
+
const opt = (key, title, sub, on) => button({ label: el('span', { class: 'nt-pick__txt' }, el('b', null, title), el('span', { class: 'nt-hint' }, sub)), variant: 'ghost', attrs: { class: 'nt-pick', role: 'radio', 'aria-checked': on ? 'true' : 'false', 'data-nt-meeting-pick': key },
|
|
806
|
+
onClick: (ev) => { pick = key; for (const b of box.querySelectorAll('[data-nt-meeting-pick]')) b.setAttribute('aria-checked', b === ev.currentTarget ? 'true' : 'false'); } });
|
|
807
|
+
const h = sheet({ title: 'Which meeting is this?', sub: 'The day of the note and the day before, from your calendar.', body: [box], attrs: { 'data-nt-meeting-sheet': '' },
|
|
808
|
+
actions: [button({ label: 'Cancel', variant: 'ghost', onClick: () => h.close() }), button({ label: 'Link', variant: 'primary', attrs: { 'data-nt-meeting-save': '' }, onClick: async () => {
|
|
809
|
+
if (!pick) { h.close(); return; }
|
|
810
|
+
h.close();
|
|
811
|
+
if (pick === 'adhoc') return link(ctx, n, { meeting: { kind: 'adhoc' } }, () => 'Marked as a meeting that is not on the calendar.');
|
|
812
|
+
const e = evs[Number(pick)];
|
|
813
|
+
const { meetingOf } = await import('./notes-links.js');
|
|
814
|
+
return link(ctx, n, { meeting: meetingOf(e) }, () => 'Linked to ' + e.title + '. The note shows on the calendar.');
|
|
815
|
+
} })] });
|
|
816
|
+
evs = await eventsNear(n);
|
|
817
|
+
if (!h.open) return;
|
|
818
|
+
const { notesFor } = await import('./notes-links.js');
|
|
819
|
+
box.replaceChildren(...evs.map((e, i) => {
|
|
820
|
+
const on = !!(n.meeting && n.meeting.kind === 'event' && (n.meeting.event_id === e.id || (Date.parse(n.meeting.start) === Date.parse(e.start) && n.meeting.title === e.title)));
|
|
821
|
+
if (on) pick = String(i);
|
|
822
|
+
const other = notesFor(e).some((x) => x.id !== n.id);
|
|
823
|
+
return opt(String(i), e.title, dayOf(e.start) + ', ' + e.time + (other ? ' · has a note' : ''), on);
|
|
824
|
+
}), evs.length ? null : el('p', { class: 'nt-hint' }, 'No calendar events on these days.'),
|
|
825
|
+
opt('adhoc', 'Not on the calendar', 'a hallway chat, a call', pick === 'adhoc'));
|
|
826
|
+
}
|
|
827
|
+
async function initiativeSheet(ctx, n) {
|
|
828
|
+
const ws = n.ws || wsOf(ctx);
|
|
829
|
+
const box = el('div', { class: 'nt-picks' }, el('p', { class: 'nt-hint' }, 'reading the initiatives …'));
|
|
830
|
+
let pick = n.initiative ? n.initiative.id : '';
|
|
831
|
+
let open = [];
|
|
832
|
+
const h = sheet({ title: 'Which initiative is this note about?', sub: 'The note shows on the initiative\'s page.', body: [box], attrs: { 'data-nt-ini-sheet': '' },
|
|
833
|
+
actions: [button({ label: 'Cancel', variant: 'ghost', onClick: () => h.close() }), button({ label: 'Save', variant: 'primary', attrs: { 'data-nt-ini-save': '' }, onClick: () => {
|
|
834
|
+
h.close();
|
|
835
|
+
if (pick === (n.initiative ? n.initiative.id : '') && (!n.initiative || n.initiative.by === 'pm')) return;
|
|
836
|
+
if (!pick) return link(ctx, n, { initiative: null }, () => 'Not linked to an initiative.');
|
|
837
|
+
const i = open.find((x) => x.id === pick) || { id: pick, title: pick };
|
|
838
|
+
return link(ctx, n, { initiative: { id: i.id, ws, repo: (ctx.state && ctx.state.product) || ctx.repo || null, title: i.title } }, () => 'Now on ' + i.title + '. The note shows on its page.');
|
|
839
|
+
} })] });
|
|
840
|
+
if (!ws) { box.replaceChildren(el('p', { class: 'nt-hint' }, 'Open a repository first: its initiatives are the ones a note can belong to.')); return; }
|
|
841
|
+
const r = await pmGet('/api/notes/initiatives?ws=' + encodeURIComponent(ws) + '&id=' + encodeURIComponent(n.id));
|
|
842
|
+
if (!h.open) return;
|
|
843
|
+
if (!r || !r.ok) { box.replaceChildren(el('p', { class: 'nt-hint', role: 'alert' }, (r && r.error) || 'The initiatives could not be read.')); return; }
|
|
844
|
+
open = r.open || [];
|
|
845
|
+
const radios = [];
|
|
846
|
+
const choose = (v) => { pick = v; for (const b of radios) b.setAttribute('aria-checked', b.dataset.ntIniPick === v ? 'true' : 'false'); if (sel.control.value !== v) sel.control.value = open.some((x) => x.id === v) ? v : ''; };
|
|
847
|
+
const opt = (v, title, sub) => { const b = button({ label: el('span', { class: 'nt-pick__txt' }, el('b', null, title), el('span', { class: 'nt-hint' }, sub)), variant: 'ghost', attrs: { class: 'nt-pick', role: 'radio', 'aria-checked': pick === v ? 'true' : 'false', 'data-nt-ini-pick': v }, onClick: () => choose(v) }); radios.push(b); return b; };
|
|
848
|
+
const sel = field({ label: 'Or choose any open initiative', kind: 'select', value: open.some((x) => x.id === pick) ? pick : '', options: [{ value: '', label: 'Choose …' }, ...open.map((x) => ({ value: x.id, label: x.title }))], attrs: { 'data-nt-ini-all': '' } });
|
|
849
|
+
sel.control.addEventListener('change', () => choose(sel.control.value));
|
|
850
|
+
const sugg = r.pick ? [el('h3', { class: 'nt-sec__h' }, 'Suggested from what was said'), opt(r.pick.id, r.pick.title, r.pick.shared + ' words in common')] : [];
|
|
851
|
+
const current = n.initiative && (!r.pick || r.pick.id !== n.initiative.id) ? [opt(n.initiative.id, n.initiative.title || n.initiative.id, BY_WORDS[n.initiative.by] || '')] : [];
|
|
852
|
+
box.replaceChildren(el('div', { role: 'radiogroup', 'aria-label': 'Initiative' }, ...sugg, ...current, opt('', 'None', 'not about an initiative')), open.length ? sel : el('p', { class: 'nt-hint' }, 'This repository has no open initiatives.'));
|
|
853
|
+
}
|
|
854
|
+
async function openInitiativeOf(ctx, ini) {
|
|
855
|
+
const reg = await import('./registry.js');
|
|
856
|
+
reg.openInitiative(ini.repo || (ctx.state && ctx.state.product) || ctx.repo || null, ini.id);
|
|
857
|
+
}
|
|
858
|
+
function linksLine(ctx, n) {
|
|
859
|
+
const m = n.meeting; const ini = n.initiative;
|
|
860
|
+
const meet = m && m.kind === 'event'
|
|
861
|
+
? [el('b', null, m.title), el('span', { class: 'nt-hint' }, dayOf(m.start) + ', ' + nowClock(m.start) + (m.end ? '–' + nowClock(m.end) : ''))]
|
|
862
|
+
: m && m.kind === 'adhoc' ? [el('b', null, 'Not on the calendar'), el('span', { class: 'nt-hint' }, dayOf(n.created_at) + ', ' + nowClock(n.created_at))]
|
|
863
|
+
: [el('b', null, 'None')];
|
|
864
|
+
return el('div', { class: 'nt-links', 'data-nt-part': 'links' },
|
|
865
|
+
el('div', { class: 'nt-link', 'data-nt-link': 'meeting' }, el('span', { class: 'nt-link__k' }, 'Meeting'), ...meet,
|
|
866
|
+
button({ label: 'Change', variant: 'ghost', size: 'sm', onClick: () => meetingSheet(ctx, n), attrs: { 'data-nt-meeting': '' } })),
|
|
867
|
+
el('div', { class: 'nt-link', 'data-nt-link': 'initiative', 'data-nt-ini-by': ini ? ini.by : '' }, el('span', { class: 'nt-link__k' }, 'Initiative'),
|
|
868
|
+
ini ? [button({ label: ini.title || ini.id, variant: 'ghost', size: 'sm', onClick: () => openInitiativeOf(ctx, ini), attrs: { 'data-nt-ini-open': ini.id } }), el('span', { class: 'nt-hint' }, BY_WORDS[ini.by] || '')] : el('b', null, 'None'),
|
|
869
|
+
button({ label: 'Change', variant: 'ghost', size: 'sm', onClick: () => initiativeSheet(ctx, n), attrs: { 'data-nt-ini': '' } })));
|
|
870
|
+
}
|
|
765
871
|
function listening(n) {
|
|
766
872
|
if (audio && audio.id === n.id) return { kind: 'local', state: audio.state, sources: audio.sources, started: audio.started };
|
|
767
873
|
if (speech && speech.id === n.id && speech.mode === 'record' && (speech.state === 'on' || speech.state === 'checking')) return { kind: 'browser', state: speech.state, sources: ['mic'], started: speech.started };
|
|
@@ -790,7 +896,7 @@ function header(ctx, n, ed) {
|
|
|
790
896
|
}
|
|
791
897
|
acts.push(button({ label: 'All notes', variant: 'ghost', onClick: () => { showList = !showList; ctx.setState({}); if (showList) loadList(ctx); }, attrs: { 'data-nt-all': '', 'aria-expanded': showList ? 'true' : 'false' } }));
|
|
792
898
|
acts.push(iconButton({ icon: 'settings', ariaLabel: 'Settings', onClick: () => openSettings(ctx, n), attrs: { 'data-nt-settings': '' } }));
|
|
793
|
-
return el('header', { class: 'nt-header', 'data-nt-part': 'header' }, left, el('div', { class: 'nt-acts' }, acts));
|
|
899
|
+
return el('header', { class: 'nt-header', 'data-nt-part': 'header' }, el('div', { class: 'nt-headwrap' }, left, linksLine(ctx, n)), el('div', { class: 'nt-acts' }, acts));
|
|
794
900
|
}
|
|
795
901
|
function listPane(ctx) {
|
|
796
902
|
const rows = (list && list.notes) || [];
|
|
@@ -900,6 +1006,7 @@ export function renderBoard(inner, ctx) {
|
|
|
900
1006
|
ensureComponentsCss();
|
|
901
1007
|
if (FIXTURE) { inner.appendChild(el('p', { class: 'nt-hint' }, 'The fixture keeps no notes.')); return; }
|
|
902
1008
|
if (!list) loadList(ctx);
|
|
1009
|
+
if (pendingOpen) { const id = pendingOpen; pendingOpen = null; if (id !== currentId) { selectNote(ctx, id); loadList(ctx); } }
|
|
903
1010
|
if (!engine || engine.ws !== wsOf(ctx)) { if (!engine || !engine.loading || engine.ws !== wsOf(ctx)) loadEngine(ctx); }
|
|
904
1011
|
|
|
905
1012
|
const focus = typeof document !== 'undefined' && editor && (document.activeElement === editor.body ? 'body' : document.activeElement === editor.title ? 'title' : null);
|
|
@@ -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
|
|