create-pmos 0.108.0 → 0.110.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/layout.md +2 -2
- package/workspace/guide/notes.md +1 -1
- package/workspace/lib/routes-notes.js +28 -2
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/dest-settings.js +9 -2
- package/workspace/public/pm/dock-sessions.js +9 -2
- package/workspace/public/pm/dock.js +11 -0
- package/workspace/public/pm/frame-copy.js +1 -0
- package/workspace/public/pm/panel-newsess.js +5 -1
- package/workspace/public/pm/view-notes.js +52 -15
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.110.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. Includes `workflow-sweep`, which audits the state every board derives from and separates real defects from work you simply have not done. `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
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Eval calibration log"
|
|
4
4
|
description: "Append-only record of every PM override of an evaluator verdict."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-08T04:20:34Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Health budgets"
|
|
4
4
|
description: "Closed registry of health-budget anchors (D59), edited only by the PM."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-08T04:20:34Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Discoveries and friction log"
|
|
4
4
|
description: "Append-only record of what runs discovered and where the process rubbed."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-08T04:20:34Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Guide
|
|
3
3
|
title: "Guide: layout"
|
|
4
4
|
description: "The frame: one top bar, the place on screen, the detail and the agent dock; the phone tabs and lists; the keys; and the older layout."
|
|
5
|
-
timestamp: 2026-10-
|
|
5
|
+
timestamp: 2026-10-08
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Guide: layout
|
|
@@ -31,7 +31,7 @@ Settings holds settings only, in eight sections. On a wide screen they are cards
|
|
|
31
31
|
|
|
32
32
|
- **Repositories**: every registered repository. Press a repository to make it the current one. Press its ⋯ for **Rename** or **Remove from PMOS**. Each asks in a sheet, and a remove asks you to confirm. **Add repository** shows the scan of a folder before you add it.
|
|
33
33
|
- **Profiles**: the Claude profiles a session can start with. These are the descriptors in `~/.claude/profiles`, and also the profiles you already run: a shell command that sets `CLAUDE_CONFIG_DIR` (for example `alias claude-work='CLAUDE_CONFIG_DIR=~/.claude-work claude'`), a launcher script named `claude-*`, or a config folder such as `~/.claude-work`. Each profile is a row with a **Detected** badge when PMOS found it rather than wrote it, and a **Default** badge when it is the default. Its ⋯ menu has **Use as default**. A profile found this way says where it was found, and a session under it sets only `CLAUDE_CONFIG_DIR`. The list reads again every few seconds while Settings is open, and **Look again** in the Claude profiles menu reads it at once. When nothing is found, the note names the start-up files and folders PMOS looked at. Press **New profile** to make one. This writes `~/.claude/profiles/<id>.json`. Account secrets are never shown in the browser.
|
|
34
|
-
- **New sessions**: the default CLI, the default model, the default Claude profile and the default launch flags for a new session. The flags come from `claude --help`, for example `--chrome`. A flag that needs a value lists the values it accepts.
|
|
34
|
+
- **New sessions**: the default CLI, the default model, the default Claude profile and the default launch flags for a new session. The flags come from `claude --help`, for example `--chrome`. A flag that needs a value lists the values it accepts. The default profile is one setting for the whole workspace. PMOS keeps it on the server, so every browser, port and device starts a Claude session with the same profile. Choose it here or with "make … the default" in the New session form. Both change the same setting.
|
|
35
35
|
- **Validate consent**: the consent of the current repository. Use its switch to turn it on or off. **Open Validate** opens the Validate page as a tab.
|
|
36
36
|
- **People**: whether the morning brief keeps attendee names from the calendar for the current repository. It is off until you turn it on. Turning it off also hides the names recorded before.
|
|
37
37
|
- **Online**: puts this workspace at your own address, such as `pmos.pm-os.app`, through a Cloudflare tunnel. Enter the public address, the tunnel name, your Cloudflare team domain, the Access audience (AUD) tag and your email, then turn on the **Online** switch. The workspace runs the tunnel and remembers the switch when it restarts. On your phone, open the address and sign in with Cloudflare Access (Google or an email code). The workspace does not ask for its token there. Only the computer that runs the workspace can change this section. A phone sees whether it is on. When a sign-in at the address is refused, this section names the check that failed, such as the email or the AUD tag. [REMOTE.md](../REMOTE.md) has the one-time Cloudflare steps.
|
package/workspace/guide/notes.md
CHANGED
|
@@ -163,7 +163,7 @@ The task appears in your My Work view and on the initiative's board when the not
|
|
|
163
163
|
Under the title, a note shows two lines: **Meeting** and **Initiative**.
|
|
164
164
|
|
|
165
165
|
- **Meeting** is the calendar event the note was taken in, or **Not on the calendar** for a hallway chat or a call. Press **Change** to select an event of the note's day or the day before, or **Not on the calendar**.
|
|
166
|
-
- **Initiative** is the initiative the meeting was about. It says how it was set: **from the calendar event**, **suggested from what was said**, or **chosen by you**. Press **Change** to
|
|
166
|
+
- **Initiative** is the initiative the meeting was about. It says how it was set: **from the calendar event**, **suggested from what was said**, or **chosen by you**. Press **Change** to open the list. The list shows the suggestion first, then the initiatives that are active now, then **None**. Initiatives planned for Next and Later are in the menu under them, each marked with its horizon. When more than one repository is open, a **Repository** menu at the top of the list changes which repository's initiatives you see. If the repository is still being read, the list says so and fills in by itself. Press the initiative's name to open it.
|
|
167
167
|
|
|
168
168
|
When a note has no initiative, the summary suggests one: the open initiative that shares 3 or more words with what was said, when no other initiative shares as many. A suggestion never replaces an initiative you chose.
|
|
169
169
|
|
|
@@ -269,6 +269,32 @@ function glossaryOf(root) {
|
|
|
269
269
|
glossaryCache.set(root, { at: Date.now(), terms: out });
|
|
270
270
|
return out;
|
|
271
271
|
}
|
|
272
|
+
/** The open initiatives of one repository, the active ones first. The Initiatives view reads the repository's documents
|
|
273
|
+
* (lib/initiative-rows.js) and places each one Now, Next or Later; a repository kept as documents has no planning/initiatives
|
|
274
|
+
* records, so those alone left the picker empty. Placed rows come first in horizon order; records the view does not place follow
|
|
275
|
+
* as Now. `pending` is true while the first read of the documents is still running. */
|
|
276
|
+
function openInitiativesOf(w) {
|
|
277
|
+
const legacy = notesLinks.initiativesOf(w.root);
|
|
278
|
+
let placed = []; let pending = false;
|
|
279
|
+
try {
|
|
280
|
+
const rowsLib = require('./initiative-rows.js');
|
|
281
|
+
const ri = require('./routes-initiatives.js');
|
|
282
|
+
const got = rowsLib.rowsFor(w.root);
|
|
283
|
+
if (got.pending) pending = true;
|
|
284
|
+
else if (got.ok) {
|
|
285
|
+
const rm = ri.roadmapOf(w, ri.frameOf(w, got.rows, { now: Date.now() }));
|
|
286
|
+
const byId = new Map(got.rows.map((r) => [r.id, r]));
|
|
287
|
+
for (const h of ['now', 'next', 'later']) {
|
|
288
|
+
for (const r of rm.horizons[h]) { const rec = byId.get(r.id); if (!(rec && rec.stopped)) placed.push({ id: r.id, title: r.title, anchor: r.anchor, intent: '', horizon: h }); }
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
} catch { placed = []; }
|
|
292
|
+
const seen = new Set(placed.map((i) => i.id));
|
|
293
|
+
const rest = legacy.filter((i) => !seen.has(i.id)).map((i) => ({ ...i, horizon: 'now' }));
|
|
294
|
+
const order = { now: 0, next: 1, later: 2 };
|
|
295
|
+
const open = [...placed, ...rest].sort((a, b) => order[a.horizon] - order[b.horizon]);
|
|
296
|
+
return { open, pending };
|
|
297
|
+
}
|
|
272
298
|
/** The repository's terms for a note: its initiative's title, the glossary, and the product terms in the open initiatives' titles. */
|
|
273
299
|
function repoTermsFor(ctx, note) {
|
|
274
300
|
const w = note && note.ws && typeof ctx.getWs === 'function' ? ctx.getWs(note.ws) : null;
|
|
@@ -500,12 +526,12 @@ async function handle(req, res, url, ctx) {
|
|
|
500
526
|
const wsId = url.searchParams.get('ws');
|
|
501
527
|
const w = wsId ? ctx.getWs(wsId) : null;
|
|
502
528
|
if (!w || w.id !== wsId) { ctx.json(res, { ok: false, error: 'This repository is not registered.' }, 404); return true; }
|
|
503
|
-
const open =
|
|
529
|
+
const { open, pending } = openInitiativesOf(w);
|
|
504
530
|
const nid = url.searchParams.get('id');
|
|
505
531
|
const r = nid ? notes.read(dir, nid) : null;
|
|
506
532
|
if (r && !r.ok) { send(ctx, res, r); return true; }
|
|
507
533
|
const s = r ? notesLinks.suggest(r.note, open) : { pick: null, ranked: [] };
|
|
508
|
-
ctx.json(res, { ok: true, open: open.map((i) => ({ id: i.id, title: i.title })), pick: s.pick });
|
|
534
|
+
ctx.json(res, { ok: true, open: open.map((i) => ({ id: i.id, title: i.title, horizon: i.horizon })), pick: s.pick, pending });
|
|
509
535
|
return true;
|
|
510
536
|
}
|
|
511
537
|
if (p === '/api/notes/engine') {
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmos-workspace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.110.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.110.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@xterm/addon-fit": "^0.11.0",
|
package/workspace/package.json
CHANGED
|
@@ -28,7 +28,7 @@ import { pmGet, pmPost } from './http.js';
|
|
|
28
28
|
import { button, iconButton, field, badge, row, tabs, menu, empty } from './primitives.js';
|
|
29
29
|
import { repoTuples, activeProduct, renameRepo, removeRepo } from './panel-repos.js';
|
|
30
30
|
import { repoRow } from './fixture.js';
|
|
31
|
-
import { sessionDefaults, setSessionDefaults } from './dock.js';
|
|
31
|
+
import { sessionDefaults, setSessionDefaults, adoptProfileDefault } from './dock.js';
|
|
32
32
|
import { COPY } from './frame-copy.js';
|
|
33
33
|
import { askConfirm } from './dialogs.js';
|
|
34
34
|
import { icon } from './icons.js';
|
|
@@ -219,8 +219,13 @@ function profilesSection() {
|
|
|
219
219
|
/** The default Claude profile, from a row or the select: kept for the dock, and remembered by the server for the New session form. */
|
|
220
220
|
function chooseDefaultProfile(id) {
|
|
221
221
|
setSessionDefaults({ profile: id || null });
|
|
222
|
-
pmPost('/api/profiles/prefer', { agent: 'claude', profile: id || null });
|
|
223
222
|
setState({});
|
|
223
|
+
// the server's answer is the default: a refusal reads the profiles again, so the select goes back to what is stored
|
|
224
|
+
pmPost('/api/profiles/prefer', { agent: 'claude', profile: id || null }).then((j) => {
|
|
225
|
+
if (j && j.ok && j.prefs) { adoptProfileDefault({ prefs: j.prefs }); if (sess.profiles) sess.profiles = { ...sess.profiles, prefs: j.prefs }; }
|
|
226
|
+
else { sess.msg = (j && j.error) || P.profileDefaultFailed; readProfilesAgain(); }
|
|
227
|
+
setState({});
|
|
228
|
+
});
|
|
224
229
|
}
|
|
225
230
|
|
|
226
231
|
/* ── the Claude profile and launch flags a new session starts with (workspace-launch-flags) ───────────────────────── */
|
|
@@ -244,6 +249,8 @@ function readProfilesAgain() {
|
|
|
244
249
|
pmGet('/api/profiles').then((j) => {
|
|
245
250
|
sess.reading = false; sess.readAt = Date.now();
|
|
246
251
|
sess.profiles = j || { ok: false, error: 'no answer', profiles: [] };
|
|
252
|
+
// the server's default wins over this browser's copy, so every browser and port shows and starts with the same one
|
|
253
|
+
adoptProfileDefault(sess.profiles);
|
|
247
254
|
// the detail column and the New session form read the same answer
|
|
248
255
|
state.profiles = sess.profiles;
|
|
249
256
|
setState({});
|
|
@@ -24,7 +24,7 @@ import { state, setState } from './state.js';
|
|
|
24
24
|
import { boardOf } from './board-read.js';
|
|
25
25
|
import { button, iconButton, row, field, sheet, toast } from './primitives.js';
|
|
26
26
|
import { COPY } from './frame-copy.js';
|
|
27
|
-
import { sessionDefaults, loadSkills } from './dock.js';
|
|
27
|
+
import { sessionDefaults, adoptProfileDefault, loadSkills } from './dock.js';
|
|
28
28
|
|
|
29
29
|
let dataMod = null; let termMod = null; let iniMod = null; let tabsMod = null;
|
|
30
30
|
import('./data.js').then((m) => { dataMod = m; }).catch(() => { dataMod = null; });
|
|
@@ -298,10 +298,17 @@ export function openNewSessionSheet(repo, preset) {
|
|
|
298
298
|
const typed = nameF.control.value.trim();
|
|
299
299
|
const base = (sk ? (sk.label || sk.skill || sk.id) : (d.cli === 'claude' ? 'Claude' : 'Shell')) + (m.initiative ? ' · ' + m.initiative : '');
|
|
300
300
|
const name = typed || uniqueName(base);
|
|
301
|
+
// The default profile is the server's. This browser's copy can be empty or old (another browser, port or host name),
|
|
302
|
+
// so a Claude start reads it first; if the read fails, the copy is used.
|
|
303
|
+
const claudeStart = !!sk || d.cli === 'claude';
|
|
304
|
+
m.busy = true; // one start per press, while the read is out too
|
|
305
|
+
const profile = claudeStart && dataMod && typeof dataMod.loadProfiles === 'function'
|
|
306
|
+
? adoptProfileDefault(await dataMod.loadProfiles().catch(() => null)) : d.profile;
|
|
307
|
+
m.busy = false;
|
|
301
308
|
const common = {
|
|
302
309
|
name, agent: sk ? 'claude' : d.cli, initiative: m.initiative || null,
|
|
303
310
|
// the Settings defaults for a Claude session (workspace-launch-flags); a shell takes neither
|
|
304
|
-
profile:
|
|
311
|
+
profile: claudeStart ? profile : null,
|
|
305
312
|
flags: !sk && d.cli === 'claude' && d.flags.length ? d.flags.map((x) => ({ flag: x.flag, ...(x.value ? { value: x.value } : {}) })) : undefined,
|
|
306
313
|
skill: sk ? sk.id : null, workType: sk ? typeOfSkill(sk) : 'general',
|
|
307
314
|
tier: !sk && d.cli === 'claude' && d.tier !== 'auto' ? d.tier : null,
|
|
@@ -47,6 +47,17 @@ export function sessionDefaults() {
|
|
|
47
47
|
return { cli: CLIS.includes(r.cli) ? r.cli : 'shell', tier: TIERS.includes(r.tier) ? r.tier : 'auto', profile: cleanProfile(r.profile), flags: cleanFlags(r.flags) };
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
/* The default Claude profile is the SERVER's (prefs.json, `prefs.claude`), not this browser's. The copy above is a cache: it
|
|
51
|
+
* lives per browser origin, so another browser, a phone, or the workspace on another port or host name starts with an
|
|
52
|
+
* empty or old copy. A dock start then sent no profile, or an old one, and the session opened under a different Claude
|
|
53
|
+
* profile than the one set as the default. Every profiles read carries `prefs`; this makes the cache follow it. */
|
|
54
|
+
export function adoptProfileDefault(read) {
|
|
55
|
+
if (!read || read.ok === false || !read.prefs || typeof read.prefs !== 'object') return sessionDefaults().profile;
|
|
56
|
+
const want = cleanProfile(read.prefs.claude);
|
|
57
|
+
if (sessionDefaults().profile !== want) setSessionDefaults({ profile: want });
|
|
58
|
+
return want;
|
|
59
|
+
}
|
|
60
|
+
|
|
50
61
|
export function setSessionDefaults(patch) {
|
|
51
62
|
const next = { ...sessionDefaults(), ...(patch || {}) };
|
|
52
63
|
const clean = { cli: CLIS.includes(next.cli) ? next.cli : 'shell', tier: TIERS.includes(next.tier) ? next.tier : 'auto', profile: cleanProfile(next.profile), flags: cleanFlags(next.flags) };
|
|
@@ -175,6 +175,7 @@ export const COPY = {
|
|
|
175
175
|
modelDeep: 'Deep',
|
|
176
176
|
sessionsNote: 'The dock starts new sessions with these defaults. A model, a profile and launch flags apply only to a Claude session. The New session form can change them for one session.',
|
|
177
177
|
profileDefault: 'Default Claude profile',
|
|
178
|
+
profileDefaultFailed: 'The default profile could not be saved.',
|
|
178
179
|
noProfile: 'No profile',
|
|
179
180
|
profileMissing: (id) => id + ' (not found now)',
|
|
180
181
|
flagsDefault: 'Default launch flags',
|
|
@@ -34,7 +34,7 @@ import { state, setState, FIXTURE } from './state.js';
|
|
|
34
34
|
import * as FX from './fixture.js';
|
|
35
35
|
import { openTerm as openTermTab } from './panel-initiatives.js';
|
|
36
36
|
import { startHere } from './panel-trees.js';
|
|
37
|
-
import { sessionDefaults } from './dock.js';
|
|
37
|
+
import { sessionDefaults, setSessionDefaults, adoptProfileDefault } from './dock.js';
|
|
38
38
|
import { button } from './primitives.js';
|
|
39
39
|
import { flagPicker, flagsLine, flagsMissingValue, flagsWire } from './flag-picker.js';
|
|
40
40
|
|
|
@@ -184,6 +184,8 @@ function refreshReads() {
|
|
|
184
184
|
}
|
|
185
185
|
if (typeof dataMod.loadProfiles === 'function') {
|
|
186
186
|
dataMod.loadProfiles().then((r) => {
|
|
187
|
+
// the server's Claude default wins over this browser's copy (dock.js adoptProfileDefault)
|
|
188
|
+
adoptProfileDefault(r);
|
|
187
189
|
setState({ profiles: r || null });
|
|
188
190
|
// Preselect from the server's remembered preference — never from a hardcoded name. A
|
|
189
191
|
// preference naming a descriptor that no longer reads preselects NOTHING rather than
|
|
@@ -414,6 +416,8 @@ async function setDefault() {
|
|
|
414
416
|
if (!dataMod || typeof dataMod.preferProfile !== 'function') return;
|
|
415
417
|
const r = await dataMod.preferProfile(state.newSessAgent || 'shell', state.preflightProfile === undefined ? null : state.preflightProfile);
|
|
416
418
|
if (!r || !r.ok) { setState({ newSessError: (r && r.error) || 'the default could not be saved' }); return; }
|
|
419
|
+
// the Claude default is one default: Settings and a dock start read it too, so they follow this choice
|
|
420
|
+
if ((state.newSessAgent || 'shell') === 'claude') setSessionDefaults({ profile: state.preflightProfile || null });
|
|
417
421
|
setState((s) => ({ newSessError: null, profiles: s.profiles ? { ...s.profiles, prefs: r.prefs || {} } : s.profiles }));
|
|
418
422
|
}
|
|
419
423
|
|
|
@@ -1067,32 +1067,69 @@ async function meetingSheet(ctx, n) {
|
|
|
1067
1067
|
}), evs.length ? null : el('p', { class: 'nt-hint' }, 'No calendar events on these days.'),
|
|
1068
1068
|
opt('adhoc', 'Not on the calendar', 'a hallway chat, a call', pick === 'adhoc'));
|
|
1069
1069
|
}
|
|
1070
|
+
const HORIZON_WORDS = { now: 'Now', next: 'Next', later: 'Later' };
|
|
1071
|
+
/** The repositories the notes can be about, one row per repository (its main tree), from the workspace server. */
|
|
1072
|
+
async function reposForNotes() {
|
|
1073
|
+
const r = await pmGet('/api/workspaces');
|
|
1074
|
+
const seen = new Map();
|
|
1075
|
+
for (const w of (r && r.workspaces) || []) {
|
|
1076
|
+
if (!w || !w.id) continue;
|
|
1077
|
+
const key = w.repo_key || w.root;
|
|
1078
|
+
if (!seen.has(key) || w.root === key) seen.set(key, { ws: w.id, name: w.label || w.repo_name || w.name || w.id });
|
|
1079
|
+
}
|
|
1080
|
+
return [...seen.values()];
|
|
1081
|
+
}
|
|
1070
1082
|
async function initiativeSheet(ctx, n) {
|
|
1071
|
-
|
|
1083
|
+
let ws = n.ws || wsOf(ctx);
|
|
1084
|
+
let repo = (n.initiative && n.initiative.repo) || (ctx.state && ctx.state.product) || ctx.repo || null;
|
|
1072
1085
|
const box = el('div', { class: 'nt-picks' }, el('p', { class: 'nt-hint' }, 'reading the initiatives …'));
|
|
1086
|
+
const where = el('div', { 'data-nt-ini-where': '' });
|
|
1073
1087
|
let pick = n.initiative ? n.initiative.id : '';
|
|
1074
1088
|
let open = [];
|
|
1075
|
-
|
|
1089
|
+
let radios = [];
|
|
1090
|
+
let sel = null;
|
|
1091
|
+
let loadId = 0;
|
|
1092
|
+
const h = sheet({ title: 'Which initiative is this note about?', sub: 'The note shows on the initiative\'s page.', body: [where, box], attrs: { 'data-nt-ini-sheet': '' },
|
|
1076
1093
|
actions: [button({ label: 'Cancel', variant: 'ghost', onClick: () => h.close() }), button({ label: 'Save', variant: 'primary', attrs: { 'data-nt-ini-save': '' }, onClick: () => {
|
|
1077
1094
|
h.close();
|
|
1078
1095
|
if (pick === (n.initiative ? n.initiative.id : '') && (!n.initiative || n.initiative.by === 'pm')) return;
|
|
1079
1096
|
if (!pick) return link(ctx, n, { initiative: null }, () => 'Not linked to an initiative.');
|
|
1080
1097
|
const i = open.find((x) => x.id === pick) || { id: pick, title: pick };
|
|
1081
|
-
return link(ctx, n, { initiative: { id: i.id, ws, repo
|
|
1098
|
+
return link(ctx, n, { initiative: { id: i.id, ws, repo, title: i.title } }, () => 'Now on ' + i.title + '. The note shows on its page.');
|
|
1082
1099
|
} })] });
|
|
1083
|
-
|
|
1084
|
-
const r = await pmGet('/api/notes/initiatives?ws=' + encodeURIComponent(ws) + '&id=' + encodeURIComponent(n.id));
|
|
1085
|
-
if (!h.open) return;
|
|
1086
|
-
if (!r || !r.ok) { box.replaceChildren(el('p', { class: 'nt-hint', role: 'alert' }, (r && r.error) || 'The initiatives could not be read.')); return; }
|
|
1087
|
-
open = r.open || [];
|
|
1088
|
-
const radios = [];
|
|
1089
|
-
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 : ''; };
|
|
1100
|
+
const choose = (v) => { pick = v; for (const b of radios) b.setAttribute('aria-checked', b.dataset.ntIniPick === v ? 'true' : 'false'); if (sel && sel.control.value !== v) sel.control.value = open.some((x) => x.id === v && x.horizon !== 'now') ? v : ''; };
|
|
1090
1101
|
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; };
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1102
|
+
async function load(tries = 0) {
|
|
1103
|
+
const mine = ++loadId;
|
|
1104
|
+
open = []; radios = []; sel = null;
|
|
1105
|
+
if (!ws) { box.replaceChildren(el('p', { class: 'nt-hint' }, 'Choose a repository: its initiatives are the ones a note can belong to.')); return; }
|
|
1106
|
+
box.replaceChildren(el('p', { class: 'nt-hint' }, 'reading the initiatives …'));
|
|
1107
|
+
const r = await pmGet('/api/notes/initiatives?ws=' + encodeURIComponent(ws) + '&id=' + encodeURIComponent(n.id));
|
|
1108
|
+
if (!h.open || mine !== loadId) return;
|
|
1109
|
+
if (!r || !r.ok) { box.replaceChildren(el('p', { class: 'nt-hint', role: 'alert' }, (r && r.error) || 'The initiatives could not be read.')); return; }
|
|
1110
|
+
open = r.open || [];
|
|
1111
|
+
if (r.pending && !open.length && tries < 8) { setTimeout(() => load(tries + 1), 1500); return; }
|
|
1112
|
+
// the active initiatives (Now) are listed as choices; Next and Later are in the list below them
|
|
1113
|
+
const active = open.filter((x) => x.horizon === 'now' && !(r.pick && r.pick.id === x.id));
|
|
1114
|
+
const later = open.filter((x) => x.horizon !== 'now');
|
|
1115
|
+
sel = field({ label: later.length ? 'Or choose a later initiative' : 'Or choose any open initiative', kind: 'select', value: later.some((x) => x.id === pick) ? pick : '',
|
|
1116
|
+
options: [{ value: '', label: 'Choose …' }, ...later.map((x) => ({ value: x.id, label: (HORIZON_WORDS[x.horizon] || 'Now') + ' · ' + x.title }))], attrs: { 'data-nt-ini-all': '' } });
|
|
1117
|
+
sel.control.addEventListener('change', () => choose(sel.control.value));
|
|
1118
|
+
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')] : [];
|
|
1119
|
+
const current = n.initiative && (!r.pick || r.pick.id !== n.initiative.id) && !active.some((x) => x.id === n.initiative.id) ? [opt(n.initiative.id, n.initiative.title || n.initiative.id, BY_WORDS[n.initiative.by] || '')] : [];
|
|
1120
|
+
const now = active.length ? [el('h3', { class: 'nt-sec__h' }, 'Active now'), ...active.map((x) => opt(x.id, x.title, 'active'))] : [];
|
|
1121
|
+
box.replaceChildren(el('div', { role: 'radiogroup', 'aria-label': 'Initiative' }, ...sugg, ...current, ...now, opt('', 'None', 'not about an initiative')),
|
|
1122
|
+
later.length ? sel : (open.length ? null : el('p', { class: 'nt-hint' }, 'This repository has no open initiatives. Choose another repository above, or open the Initiatives tab to add one.')));
|
|
1123
|
+
}
|
|
1124
|
+
const repos = await reposForNotes();
|
|
1125
|
+
if (!h.open) return;
|
|
1126
|
+
if (repos.length > 1) {
|
|
1127
|
+
const rs = field({ label: 'Repository', kind: 'select', value: ws || '', options: [...(ws && !repos.some((x) => x.ws === ws) ? [{ value: ws, label: repo || ws }] : []), ...repos.map((x) => ({ value: x.ws, label: x.name }))], attrs: { 'data-nt-ini-repo': '' } });
|
|
1128
|
+
rs.control.addEventListener('change', () => { ws = rs.control.value; const f = repos.find((x) => x.ws === ws); repo = f ? f.name : repo; pick = ''; load(); });
|
|
1129
|
+
where.replaceChildren(rs);
|
|
1130
|
+
}
|
|
1131
|
+
if (!ws && repos.length) { ws = repos[0].ws; repo = repos[0].name; if (where.firstChild) where.firstChild.control && (where.firstChild.control.value = ws); }
|
|
1132
|
+
await load();
|
|
1096
1133
|
}
|
|
1097
1134
|
async function openInitiativeOf(ctx, ini) {
|
|
1098
1135
|
const reg = await import('./registry.js');
|