create-pmos 0.115.0 → 0.117.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/notes.md +2 -0
- package/workspace/guide/today.md +21 -12
- package/workspace/lib/audio-resample.js +76 -0
- package/workspace/lib/brief.js +2 -2
- package/workspace/lib/day-marks.js +29 -9
- package/workspace/lib/goals.js +68 -10
- package/workspace/lib/notes-audio.js +62 -20
- package/workspace/lib/notes-echo.js +32 -14
- package/workspace/lib/notes-hires.js +94 -0
- package/workspace/lib/notes-polish.js +2 -3
- package/workspace/lib/routes-notes.js +9 -1
- package/workspace/notes-test-signals.js +65 -0
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/audio-capture.js +20 -6
- package/workspace/public/pm/components.css +39 -11
- package/workspace/public/pm/today-agenda.js +27 -53
- package/workspace/public/pm/today-copy.js +34 -15
- package/workspace/public/pm/today-day.js +98 -76
- package/workspace/public/pm/today-goals.js +159 -15
- package/workspace/public/pm/today-jobs.js +84 -0
- package/workspace/public/pm/today-mentions.js +50 -0
- package/workspace/public/pm/view-notes.js +15 -4
- package/workspace/server.js +10 -8
- package/workspace/test-lists/meeting-notes.json +5 -3
- package/workspace/test-lists/today-goals-jobs-ui.json +5 -0
- package/workspace/test-lists/today-goals-jobs.json +5 -0
- package/workspace/test-lists/today-mentions-cards.json +5 -0
|
@@ -27,9 +27,11 @@
|
|
|
27
27
|
import { el } from './dom.js';
|
|
28
28
|
import { button, iconButton, badge, toast, row, tabs, field, menu, meter } from './primitives.js';
|
|
29
29
|
import { pmGet, pmPost } from './http.js';
|
|
30
|
-
import { FIXTURE } from './state.js';
|
|
30
|
+
import { FIXTURE, state } from './state.js';
|
|
31
31
|
import { COPY } from './today-copy.js';
|
|
32
|
-
import { goalRows, goalAdd, goalDetail, addGoal, goalsSig } from './today-goals.js';
|
|
32
|
+
import { goalRows, goalAdd, goalDetail, goalJobsBlock, jobsWithoutGoal, addGoal, goalsSig } from './today-goals.js';
|
|
33
|
+
import { statusPill, whoActs, mark as whoMark, tag, tagged, originOf, goalPicker } from './today-jobs.js';
|
|
34
|
+
import { hasCard, mentionCard } from './today-mentions.js';
|
|
33
35
|
import { peopleLinks } from './people-link.js';
|
|
34
36
|
import { agendaPanel, meetingDetail, memoryBlock, meetingFacts, keyItems, refreshAgenda, agendaSig, onAgenda } from './today-agenda.js';
|
|
35
37
|
import { notesBlock, onNotes } from './notes-links.js';
|
|
@@ -58,7 +60,7 @@ const cache = new Map(); // ws -> { data, error }
|
|
|
58
60
|
const runs = new Map(); // ws -> the last "Prepare my day" press: { state: 'busy'|'started'|'running'|'failed', at, session, error }
|
|
59
61
|
const ui = {
|
|
60
62
|
open: (() => { const v = store.get(OPEN_KEY); return v === 'true' ? true : v === 'false' ? false : null; })(),
|
|
61
|
-
section: (() => { const v = store.get(SECTION_KEY); return ['agenda', 'goals', 'jobs', 'mentions'
|
|
63
|
+
section: (() => { const v = store.get(SECTION_KEY); return ['agenda', 'goals', 'jobs', 'mentions'].includes(v) ? v : 'agenda'; })(),
|
|
62
64
|
sel: null, mtab: 'all', openSrc: null, showIgn: {}, showIgnDigests: false,
|
|
63
65
|
revising: new Set(), notes: new Map(), siteDraft: '', placeDraft: {}, busy: new Set(), scroll: false,
|
|
64
66
|
// today-act-prepare: the folded groups the PM opened, the item whose hold form is open, and its draft
|
|
@@ -129,7 +131,9 @@ function jobOf(item, task, worth) {
|
|
|
129
131
|
const status = task ? task.status : kind === 'you' ? 'you' : 'notStarted';
|
|
130
132
|
const group = worth ? 'worth' : ['you', 'review', 'failed'].includes(status) ? 'needs' : ['done', 'sent'].includes(status) ? 'done' : 'working';
|
|
131
133
|
return {
|
|
132
|
-
id: item ? item.id : task.item_id, item, task, kind, status, group, worth,
|
|
134
|
+
id: item ? item.id : task.item_id || task.id, item, task, kind, status, group, worth,
|
|
135
|
+
// the id a link mark is kept under: the brief item, else the run task (PRD L1)
|
|
136
|
+
linkId: item ? item.id : task.id,
|
|
133
137
|
title: (item && item.title) || (task && task.title) || '', source: (item && item.source) || (task && task.source) || '',
|
|
134
138
|
link: (item && isUrl(item.link) && item.link) || (task && isUrl(task.link) && task.link) || null,
|
|
135
139
|
goal: (item && item.goal) || (task && task.goal) || null, isNew: !!(item && item.new),
|
|
@@ -181,7 +185,9 @@ function model(d, view) {
|
|
|
181
185
|
const needsPrep = !ended && hadPrep;
|
|
182
186
|
const st = mark && mark.state;
|
|
183
187
|
const group = !needsPrep ? null : st === 'done' || st === 'dismissed' ? 'done' : st === 'snoozed' ? 'later' : 'needs';
|
|
184
|
-
|
|
188
|
+
// the goal a meeting counts toward: the brief's own, else the goal the PM made from it (event_id), PRD A4
|
|
189
|
+
const gid = (e.goal && goals.some((g) => g.id === e.goal) ? e.goal : null) || ((goals.find((g) => g.event_id && g.event_id === e.id) || {}).id) || null;
|
|
190
|
+
return { ...e, id: e.id || 'ev-' + i, gid, s, f, time: hhmm(e.start), ended, mark, before, hadPrep, needsPrep, group };
|
|
185
191
|
}).sort((a, b) => a.s - b.s);
|
|
186
192
|
const settings = (d && d.settings) || null;
|
|
187
193
|
const scopes = (settings && settings.scopes) || {};
|
|
@@ -194,6 +200,15 @@ function model(d, view) {
|
|
|
194
200
|
const repos = (d && Array.isArray(d.repos)) ? d.repos : [];
|
|
195
201
|
const repoName = (id) => { const r = repos.find((x) => x.id === id); return r ? r.name : id; };
|
|
196
202
|
for (const j of jobs) j.repo = itemRepos[j.id] || null;
|
|
203
|
+
// the goal of a job (initiative today-goals-jobs, PRD G4, L1): the plan puts a brief item under at most one open goal,
|
|
204
|
+
// after the PM's link mark; a job the plan does not place keeps the PM's mark, else the goal it came with
|
|
205
|
+
const stepGoal = new Map();
|
|
206
|
+
for (const g of goals) for (const st of g.steps || []) if (st.item_id) stepGoal.set(st.item_id, g.id);
|
|
207
|
+
for (const j of jobs) {
|
|
208
|
+
const lk = marks[j.linkId];
|
|
209
|
+
const linked = !!lk && lk.goal !== undefined;
|
|
210
|
+
j.goal = (j.item && stepGoal.get(j.item.id)) || (linked ? lk.goal || null : j.goal);
|
|
211
|
+
}
|
|
197
212
|
// the PM's marks (today-act-prepare, PRD M1 to M6): a job the PM owns leaves Needs you when it is done, snoozed, held
|
|
198
213
|
// or not the PM's; a snooze or hold whose time has passed comes back as open (the server reads it so)
|
|
199
214
|
for (const j of jobs) {
|
|
@@ -247,16 +262,6 @@ export function repoPicker(m, cur, onPick, attrs) {
|
|
|
247
262
|
sel.addEventListener('change', () => onPick(sel.value || null));
|
|
248
263
|
return sel;
|
|
249
264
|
}
|
|
250
|
-
function statusPill(j) {
|
|
251
|
-
const s = D.status2;
|
|
252
|
-
const map = {
|
|
253
|
-
notStarted: [s.notStarted, ''], waiting: [s.waiting, ''], working: [s.working, 'info'], sending: [s.sending, 'info'],
|
|
254
|
-
review: [j.kind === 'auto' ? s.reviewAuto : s.review, 'purple'], sent: [s.sent, 'ok'], done: [s.done, 'ok'],
|
|
255
|
-
failed: [s.failed, 'bad'],
|
|
256
|
-
};
|
|
257
|
-
const [t, tone] = map[j.status] || [j.status, ''];
|
|
258
|
-
return pill(t, tone, { 'data-day-status': j.status });
|
|
259
|
-
}
|
|
260
265
|
/** A link that opens outside the workspace, drawn as a small button. Only http and https addresses are drawn. */
|
|
261
266
|
function extLink(label, href, attrs) {
|
|
262
267
|
if (!isUrl(href)) return null;
|
|
@@ -412,24 +417,37 @@ function topBar(ctx) {
|
|
|
412
417
|
const started = m.run && m.run.started_at ? hhmm(m.run.started_at) : '';
|
|
413
418
|
const status = !m.run ? (busy ? D.starting : D.status.idle) : m.readFailed ? D.status.readFailed(m.readFailed.why) : m.stage === 'reading' ? D.status.reading : m.stage === 'working' ? D.status.working(working, m.needs)
|
|
414
419
|
: D.status.last(started, m.needs);
|
|
415
|
-
|
|
420
|
+
// S2: the status line (Last prepared), Check now, Settings and the way to the items with no repository are one menu
|
|
421
|
+
const label = busy ? D.starting : reading ? D.stillReading : m.run ? D.again : D.prepare;
|
|
422
|
+
const lines = [{ label: status, attrs: { 'data-day': 'status', 'aria-disabled': 'true' }, onSelect: () => {} }];
|
|
423
|
+
if (m.tasks.length) {
|
|
424
|
+
const c = { review: 0, done: 0, working: 0, failed: 0 };
|
|
425
|
+
for (const t of m.tasks) {
|
|
426
|
+
if (t.status === 'review') c.review++;
|
|
427
|
+
else if (t.status === 'done' || t.status === 'sent') c.done++;
|
|
428
|
+
else if (t.status === 'failed') c.failed++;
|
|
429
|
+
else c.working++;
|
|
430
|
+
}
|
|
431
|
+
lines.push({ label: D.summary(c), attrs: { 'data-day': 'summary', 'aria-disabled': 'true' }, onSelect: () => {} });
|
|
432
|
+
}
|
|
416
433
|
if (m.run && m.check) {
|
|
417
434
|
const every = m.settings && m.settings.dayCheck && typeof m.settings.dayCheck.every === 'number' ? m.settings.dayCheck.every : m.check.every;
|
|
418
435
|
const running = !!m.check.running || ui.busy.has('check');
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
iconButton({ icon: 'refresh-cw', ariaLabel: D.checkNow, size: 'sm', attrs: { 'data-day': 'check-now', disabled: running ? 'true' : null }, onClick: () => checkNow(ws) }));
|
|
436
|
+
lines.push({ label: every === 0 ? D.checkOff : running ? D.checking : D.checked(hhmm(m.check.last), hhmm(m.check.next)), attrs: { 'data-day': 'check', 'aria-disabled': 'true' }, onSelect: () => {} });
|
|
437
|
+
if (every !== 0 && !running) lines.push({ label: D.checkNow, icon: 'refresh-cw', attrs: { 'data-day': 'check-now' }, onSelect: () => checkNow(ws) });
|
|
422
438
|
}
|
|
423
|
-
const
|
|
424
|
-
|
|
439
|
+
const items = [...lines, { sep: true },
|
|
440
|
+
{ label: D.sources, icon: 'settings', attrs: { 'data-day': 'sources', 'data-day-sel': 'sources:all' }, onSelect: () => select('sources', 'all') }];
|
|
441
|
+
if (m.narrow) items.push({ label: D.menu.showAll, attrs: { 'data-day': 'show-all' }, onSelect: () => ctx.showAll() });
|
|
442
|
+
const more = iconButton({ icon: 'ellipsis', ariaLabel: D.menu.label, size: 'sm', attrs: { 'data-day': 'bar-menu', 'aria-haspopup': 'menu', 'aria-current': ctx.cur('sources', 'all') },
|
|
443
|
+
onClick: () => menu({ anchor: more, ariaLabel: D.menu.label, items }) });
|
|
425
444
|
return el('header', { class: 'td-day__bar' },
|
|
426
|
-
(() => { const when =
|
|
445
|
+
(() => { const when = dateLabel(m.now);
|
|
427
446
|
return el('div', { class: 'td-day__hello', title: greet(m.now) + ' ' + when }, el('b', null, greet(m.now)), ' ', el('span', { class: 'td-day__dim' }, when)); })(),
|
|
428
447
|
steps,
|
|
429
|
-
el('span', { class: 'td-
|
|
430
|
-
check,
|
|
431
|
-
iconButton({ icon: 'settings', ariaLabel: D.sources, size: 'sm', attrs: { 'data-day': 'sources', 'data-day-sel': 'sources:all', 'aria-current': ctx.cur('sources', 'all') }, onClick: () => select('sources', 'all') }),
|
|
448
|
+
el('span', { class: 'td-day__spacer', 'aria-hidden': 'true' }),
|
|
432
449
|
button({ label, variant: 'primary', size: 'sm', disabled: busy || reading, attrs: { 'data-day': 'prepare', 'data-goals': 'prepare-day', title: D.prepareHint, 'aria-busy': busy ? 'true' : 'false' }, onClick: () => prepareDay(ws) }),
|
|
450
|
+
more,
|
|
433
451
|
iconButton({ icon: 'chevron-down', ariaLabel: D.fold, size: 'sm', expanded: true, attrs: { 'data-day': 'toggle' }, onClick: () => fold(false) }));
|
|
434
452
|
}
|
|
435
453
|
|
|
@@ -477,7 +495,7 @@ function calendarRows(ctx) {
|
|
|
477
495
|
const nowRow = () => el('li', { class: 'td-day__nowrow', 'data-day': 'now', 'aria-hidden': 'true' }, D.now(hhmm(m.now)));
|
|
478
496
|
for (const e of m.events) {
|
|
479
497
|
if (!nowShown && e.s > m.now) { rows.push(nowRow()); nowShown = true; }
|
|
480
|
-
const n = e.
|
|
498
|
+
const n = e.gid ? m.goalNo.get(e.gid) : null;
|
|
481
499
|
rows.push(rowBtn(ctx, 'event', e.id, { lead: e.time, title: e.title, meta: e.purpose || e.with || '',
|
|
482
500
|
trail: el('span', { class: 'td-day__chips' }, n ? pill(D.pill.goal(n), 'goal') : null, eventChip(e)) },
|
|
483
501
|
{ 'data-past': e.ended ? 'true' : null, 'data-goal-tie': n ? 'true' : null }));
|
|
@@ -499,10 +517,14 @@ function jobRow(ctx, j) {
|
|
|
499
517
|
: [j.item && j.item.where, srcName(j.source)].filter(Boolean).join(' · ');
|
|
500
518
|
const mine = j.status === 'you';
|
|
501
519
|
const due = mine && j.item && j.item.due ? Date.parse(j.item.due) : NaN;
|
|
502
|
-
const
|
|
520
|
+
const held = mine && j.mark && j.mark.state === 'waiting' ? `${A.hold.replace('…', '')} ${j.mark.on}` : sub;
|
|
521
|
+
// J1: the row says who acts, in words and with a square (open for the PM, filled for the agent)
|
|
522
|
+
const who = whoActs(j);
|
|
523
|
+
const meta = [who.text, held].filter(Boolean).join(' · ');
|
|
503
524
|
return rowBtn(ctx, 'job', j.id, { lead: mine ? verbChip(verbOf(j.item)) : n ? D.pill.goal(n) : '·', title: j.title, meta,
|
|
504
525
|
trail: el('span', { class: 'td-day__chips' }, repoTag(ctx.m, j.repo), mine ? dueChip(ctx.m, j.mark, due, false) : statusPill(j)),
|
|
505
|
-
dot: j.isNew && !seen.has(j.id) ? el('span', { class: 'td-day__newdot', 'data-day': 'new', role: 'img', 'aria-label': D.newItem }) : null }
|
|
526
|
+
dot: el('span', { class: 'td-day__marks2' }, whoMark(who.who), j.isNew && !seen.has(j.id) ? el('span', { class: 'td-day__newdot', 'data-day': 'new', role: 'img', 'aria-label': D.newItem }) : null) },
|
|
527
|
+
{ 'data-who': who.who });
|
|
506
528
|
}
|
|
507
529
|
|
|
508
530
|
/* What the repositories owe the PM outside today's jobs (outcome checks, report drafts, sessions) are jobs too: each one is a
|
|
@@ -552,31 +574,6 @@ function jobsPanel(ctx) {
|
|
|
552
574
|
}));
|
|
553
575
|
}
|
|
554
576
|
|
|
555
|
-
/* The coordinator's view of the run: what every agent job came back with, in one place. The server runs the jobs as one
|
|
556
|
-
* session each and keeps their state; this reads that state, so it needs no session of its own. */
|
|
557
|
-
function summaryPanel(ctx) {
|
|
558
|
-
const { m } = ctx;
|
|
559
|
-
const ts = m.tasks;
|
|
560
|
-
if (!ts.length) return null;
|
|
561
|
-
const c = { review: 0, done: 0, working: 0, failed: 0 };
|
|
562
|
-
for (const t of ts) {
|
|
563
|
-
if (t.status === 'review') c.review++;
|
|
564
|
-
else if (t.status === 'done' || t.status === 'sent') c.done++;
|
|
565
|
-
else if (t.status === 'failed') c.failed++;
|
|
566
|
-
else c.working++;
|
|
567
|
-
}
|
|
568
|
-
const finished = ts.filter((t) => t.status !== 'waiting' && t.status !== 'working' && t.status !== 'sending');
|
|
569
|
-
const byTask = new Map(m.jobs.filter((j) => j.task).map((j) => [j.task.id, j]));
|
|
570
|
-
const rows = finished.map((t) => {
|
|
571
|
-
const j = byTask.get(t.id);
|
|
572
|
-
const what = t.status === 'failed' ? t.error : t.sent && t.sent.link ? t.sent.link : t.draft ? t.draft.summary : '';
|
|
573
|
-
return j ? rowBtn(ctx, 'job', j.id, { lead: '·', title: j.title, meta: what || '', trail: statusPill(j) }) : null;
|
|
574
|
-
}).filter(Boolean);
|
|
575
|
-
return panel('summary', null,
|
|
576
|
-
el('p', { class: 'td-day__note', 'data-day': 'summary' }, c.working ? D.summary(c) : D.summaryAll(ts.length) + ' ' + D.summary(c)),
|
|
577
|
-
rows.length ? el('ul', { class: 'td-day__rows', 'data-day-list': 'summary' }, ...rows) : null);
|
|
578
|
-
}
|
|
579
|
-
|
|
580
577
|
/* What waits for the PM outside today's jobs (the Today cards: outcome checks, reports, sessions). Today hands its list
|
|
581
578
|
* in, so the cards keep their own controls and the open day shows one layout. */
|
|
582
579
|
function goalsPanel(ctx) {
|
|
@@ -584,7 +581,7 @@ function goalsPanel(ctx) {
|
|
|
584
581
|
// a goal of another repository is not lost: the panel says how many the Show switch hides, with the way to them
|
|
585
582
|
const hidden = m.hiddenGoals ? el('p', { class: 'td-day__note', 'data-day': 'goals-hidden' }, D.repo.hiddenGoals(m.hiddenGoals) + ' ',
|
|
586
583
|
button({ label: D.repo.showAll, variant: 'ghost', size: 'sm', attrs: { 'data-day': 'show-all' }, onClick: () => ctx.showAll() })) : null;
|
|
587
|
-
return panel('goals', D.goalsCount(m.allGoals.filter((g) => g.status === 'open').length, m.maxGoals), goalRows(ctx), hidden, goalAdd(ctx));
|
|
584
|
+
return panel('goals', D.goalsCount(m.allGoals.filter((g) => g.status === 'open').length, m.maxGoals), goalRows(ctx), hidden, goalAdd(ctx), jobsWithoutGoal(ctx));
|
|
588
585
|
}
|
|
589
586
|
|
|
590
587
|
function mentionStatus(j) {
|
|
@@ -610,6 +607,8 @@ function mentionsPanel(ctx) {
|
|
|
610
607
|
const shown = m.mentions.filter((j) => ui.mtab === 'all' || j.source === ui.mtab);
|
|
611
608
|
const rows = shown.length ? el('ul', { class: 'td-day__rows', 'data-day-list': 'mentions' }, ...shown.map((j) => {
|
|
612
609
|
const it = j.item;
|
|
610
|
+
// Slack, Confluence and Figma draw as a card of their own (PRD M1); the press, the link and the status are the row's
|
|
611
|
+
if (hasCard(j.source)) return rowBtn(ctx, 'mention', j.id, { title: mentionCard(j, srcName(j.source)), trail: el('span', { class: 'td-day__chips' }, repoTag(ctx.m, j.repo), mentionStatus(j)) }, { 'data-mention-card': j.source });
|
|
613
612
|
return rowBtn(ctx, 'mention', j.id, { lead: el('span', { class: 'td-day__chip' }, srcName(j.source)),
|
|
614
613
|
title: it.about && it.ask ? `${it.about}: ${it.ask}` : it.ask || j.title, meta: D.fromWho(it.who, it.where) || it.why_now || '', trail: el('span', { class: 'td-day__chips' }, repoTag(ctx.m, j.repo), mentionStatus(j)) });
|
|
615
614
|
})) : note(D.noMentions);
|
|
@@ -752,7 +751,7 @@ function composer(ctx, j, verb) {
|
|
|
752
751
|
sendPress(ctx.ws, j, { text, verdict: review ? verdict : undefined }, to);
|
|
753
752
|
};
|
|
754
753
|
return el('div', { class: 'td-day__compose', 'data-day': 'compose' }, f, chips, toggle,
|
|
755
|
-
el('div', { class: 'td-day__acts' }, button({ label: review ? A.postReview(to) : A.sendTo(to), variant: 'primary', size: 'sm', iconEnd: 'arrow-right', disabled: busy, attrs: { 'data-day-act': 'send' }, onClick: go })),
|
|
754
|
+
el('div', { class: 'td-day__acts' }, button({ label: tagged(review ? A.postReview(to) : A.sendTo(to), 'you'), variant: 'primary', size: 'sm', iconEnd: 'arrow-right', disabled: busy, attrs: { 'data-day-act': 'send' }, onClick: go })),
|
|
756
755
|
el('span', { class: 'td-day__dim' }, A.sendNote));
|
|
757
756
|
}
|
|
758
757
|
/** The four presses of a job (Mark done, Snooze, Waiting on…, Not mine) or the three of a meeting (Mark ready, Remind
|
|
@@ -764,19 +763,19 @@ function marksBar(ctx, { id, title, kind, due, start, strong }) {
|
|
|
764
763
|
const st = (m.d && m.d.marks && m.d.marks[id] && m.d.marks[id].state) || null;
|
|
765
764
|
const times = snoozeTimes(m, { due, start });
|
|
766
765
|
const kids = [];
|
|
767
|
-
if (st !== 'done') kids.push(button({ label: prep ? A.ready : A.done, variant: strong ? 'primary' : 'outline', size: 'sm', icon: 'check', disabled: busy, attrs: { 'data-day-mark': 'done' },
|
|
766
|
+
if (st !== 'done') kids.push(button({ label: prep ? A.ready : tagged(A.done, 'you'), variant: strong ? 'primary' : 'outline', size: 'sm', icon: 'check', disabled: busy, attrs: { 'data-day-mark': 'done' },
|
|
768
767
|
onClick: () => markPress(ws, id, { action: 'done' }, (prep ? A.did.ready : A.did.done)(title)) }));
|
|
769
768
|
if (times.length) {
|
|
770
|
-
const b = button({ label: prep ? A.remind : A.snooze, size: 'sm', iconEnd: 'chevron-down', disabled: busy, attrs: { 'data-day-mark': 'snooze', 'aria-haspopup': 'menu' },
|
|
769
|
+
const b = button({ label: prep ? A.remind : tagged(A.snooze, 'you'), size: 'sm', iconEnd: 'chevron-down', disabled: busy, attrs: { 'data-day-mark': 'snooze', 'aria-haspopup': 'menu' },
|
|
771
770
|
onClick: () => menu({ anchor: b, ariaLabel: prep ? A.remind : A.snooze, items: times.map(([l, t]) => ({ label: `${l} · ${hhmm(t)}`, attrs: { 'data-day-snooze': hhmm(t) },
|
|
772
771
|
onSelect: () => markPress(ws, id, { action: 'snooze', until: new Date(t).toISOString() }, A.did.snooze(hhmm(t))) })) }) });
|
|
773
772
|
kids.push(b);
|
|
774
773
|
}
|
|
775
|
-
if (!prep) kids.push(button({ label: A.hold, size: 'sm', disabled: busy, attrs: { 'data-day-mark': 'hold', 'aria-expanded': ui.holding === id ? 'true' : 'false' },
|
|
774
|
+
if (!prep) kids.push(button({ label: tagged(A.hold, 'you'), size: 'sm', disabled: busy, attrs: { 'data-day-mark': 'hold', 'aria-expanded': ui.holding === id ? 'true' : 'false' },
|
|
776
775
|
onClick: () => { ui.holding = ui.holding === id ? null : id; ui.holdDraft = { who: '', when: 'h2' }; rerender(); } }));
|
|
777
776
|
const reasons = prep ? ['nothing', 'elsewhere'] : ['no-action', 'someone-else', 'done-elsewhere'];
|
|
778
777
|
if (st !== 'dismissed') {
|
|
779
|
-
const nb = button({ label: prep ? A.skip : A.notMine, size: 'sm', variant: 'ghost', iconEnd: 'chevron-down', disabled: busy, attrs: { 'data-day-mark': 'dismiss', 'aria-haspopup': 'menu' },
|
|
778
|
+
const nb = button({ label: prep ? A.skip : tagged(A.notMine, 'you'), size: 'sm', variant: 'ghost', iconEnd: 'chevron-down', disabled: busy, attrs: { 'data-day-mark': 'dismiss', 'aria-haspopup': 'menu' },
|
|
780
779
|
onClick: () => menu({ anchor: nb, ariaLabel: prep ? A.skip : A.notMine, items: reasons.map((w) => ({ label: A.why[w], attrs: { 'data-day-why': w },
|
|
781
780
|
onSelect: () => markPress(ws, id, { action: 'dismiss', why: w }, A.did.dismiss(A.why[w])) })) }) });
|
|
782
781
|
kids.push(el('span', { class: 'td-day__grow', 'aria-hidden': 'true' }), nb);
|
|
@@ -840,7 +839,7 @@ function actCard(ctx, j) {
|
|
|
840
839
|
canSend ? composer(ctx, j, verb) : null,
|
|
841
840
|
open ? el('div', { class: 'td-day__acts', 'data-day': 'cta' },
|
|
842
841
|
(canSend ? extLink : primaryLink)(A.open[j.source] || A.openLink, j.link, { 'data-day-link': 'conversation' }),
|
|
843
|
-
j.item && j.item.owner === 'pm' ? button({ label: A.draft, size: 'sm', disabled: ui.busy.has('draft:' + j.id), attrs: { 'data-day-act': 'draft' }, onClick: () => draftPress(ctx.ws, j) }) : null) : null,
|
|
842
|
+
j.item && j.item.owner === 'pm' ? button({ label: tagged(A.draft, 'agent'), size: 'sm', disabled: ui.busy.has('draft:' + j.id), attrs: { 'data-day-act': 'draft' }, onClick: () => draftPress(ctx.ws, j) }) : null) : null,
|
|
844
843
|
marksBar(ctx, { id: j.id, title: j.title, kind: 'item', due, strong: allDone }),
|
|
845
844
|
more ? el('details', { class: 'td-day__fold', 'data-day': 'more' }, el('summary', null, A.more), more, note(A.readOnly)) : null);
|
|
846
845
|
}
|
|
@@ -928,8 +927,8 @@ function stepItem(st) {
|
|
|
928
927
|
function eventDetail(ctx, e) {
|
|
929
928
|
const { m } = ctx;
|
|
930
929
|
const F = A.focus;
|
|
931
|
-
const
|
|
932
|
-
const
|
|
930
|
+
const g = e.gid ? m.allGoals.find((x) => x.id === e.gid) || null : null;
|
|
931
|
+
const n = g ? m.goalNo.get(g.id) : null;
|
|
933
932
|
const ini = e.initiative && e.initiative.id ? e.initiative : null;
|
|
934
933
|
const steps = e.steps || {};
|
|
935
934
|
const notes = e.notes || {};
|
|
@@ -947,7 +946,7 @@ function eventDetail(ctx, e) {
|
|
|
947
946
|
mailBlock || note(D.noMail)) : null;
|
|
948
947
|
const tie = g ? el('p', { class: 'td-day__goalline', 'data-day': 'tie' }, D.countsToward(n, g.text)) : null;
|
|
949
948
|
const addGoalItem = !g && m.allGoals.length < m.maxGoals
|
|
950
|
-
? [{ label: D.addAsGoal, attrs: { 'data-day-act': 'add-goal' }, onSelect: async () => { if (await addGoal(ctx, e.title, ini ? ini.id : null)) toast({ text: D.goalAdded }); } }] : [];
|
|
949
|
+
? [{ label: D.addAsGoal, attrs: { 'data-day-act': 'add-goal' }, onSelect: async () => { if (await addGoal(ctx, e.title, ini ? ini.id : null, { event_id: e.id })) toast({ text: D.goalAdded }); } }] : [];
|
|
951
950
|
// the one line under the title: the time, how many people join, and a star for a key meeting (F5)
|
|
952
951
|
const meta = [`${e.time}${e.end ? '–' + hhmm(e.end) : ''}`, facts && facts.people ? F.people(facts.people) : null, facts && facts.key ? F.key : null].filter(Boolean).join(' · ');
|
|
953
952
|
const duringAfter = [list(D.phases.during, (steps.during || []).map(stepItem)), list(D.phases.after, (steps.after || []).map(stepItem))];
|
|
@@ -969,6 +968,7 @@ function eventDetail(ctx, e) {
|
|
|
969
968
|
e.purpose ? el('p', { class: 'td-day__purpose', 'data-day': 'purpose' }, e.purpose) : null,
|
|
970
969
|
started ? notesBlock(e, { ws: ctx.ws, initiative: ini, mode: 'live', liveText: F.live }) : null,
|
|
971
970
|
e.hadPrep ? prepBlock(ctx, e, { fold: started }) : null,
|
|
971
|
+
g ? goalJobsBlock(ctx, g, n) : null,
|
|
972
972
|
after,
|
|
973
973
|
started ? null : notesBlock(e, { ws: ctx.ws, initiative: ini, mode: 'quiet' }),
|
|
974
974
|
el('div', { class: 'td-day__foot', 'data-day': 'foot' },
|
|
@@ -987,27 +987,27 @@ function jobCard(ctx, j) {
|
|
|
987
987
|
const revs = t && Array.isArray(t.revisions) ? t.revisions.length : 0;
|
|
988
988
|
const statusNote = { notStarted: D.notTask, waiting: D.waitingNote, working: D.workingNote, sending: D.sendingNote }[j.status]
|
|
989
989
|
|| (j.status === 'done' && j.kind === 'auto' ? D.autoNote : null);
|
|
990
|
-
|
|
990
|
+
// J5: a finished job shows its result (the run's summary and draft) inside the job; there is no separate Result section
|
|
991
|
+
const finished = !!t && (j.status === 'done' || j.status === 'sent');
|
|
992
|
+
const draft = t && t.draft ? el('div', { class: 'td-day__draftbox', 'data-day': finished ? 'result' : 'draft' }, el('h4', null, finished ? D.result : D.draft),
|
|
991
993
|
t.draft.summary ? el('p', null, t.draft.summary) : null,
|
|
992
994
|
t.draft.body ? el('pre', { class: 'td-day__draft' }, t.draft.body) : null) : null;
|
|
993
995
|
const acts = [];
|
|
994
996
|
const canRevise = t && (j.status === 'review' || j.status === 'done') && revs < MAX_REVISIONS;
|
|
995
|
-
if (t && j.status === 'review' && j.kind === 'approve') acts.push(button({ label: D.approve, variant: 'primary', size: 'sm', disabled: busy, attrs: { 'data-day-act': 'approve' }, onClick: () => decide(ws, t, 'approve') }));
|
|
996
|
-
if (t && j.kind === 'auto' && (j.status === 'review' || (j.status === 'done' && !t.decided))) acts.push(button({ label: D.approveAuto, size: 'sm', disabled: busy, attrs: { 'data-day-act': 'approve' }, onClick: () => decide(ws, t, 'approve') }));
|
|
997
|
-
if (canRevise && !ui.revising.has(t.id)) acts.push(button({ label: D.revise, size: 'sm', disabled: busy, attrs: { 'data-day-act': 'revise' }, onClick: () => { ui.revising.add(t.id); rerender(); } }));
|
|
998
|
-
if (t && j.status === 'failed') acts.push(button({ label: D.retry, variant: 'primary', size: 'sm', disabled: busy, attrs: { 'data-day-act': 'retry' }, onClick: () => decide(ws, t, 'retry') }));
|
|
997
|
+
if (t && j.status === 'review' && j.kind === 'approve') acts.push(button({ label: tagged(D.approve, 'you'), variant: 'primary', size: 'sm', disabled: busy, attrs: { 'data-day-act': 'approve' }, onClick: () => decide(ws, t, 'approve') }));
|
|
998
|
+
if (t && j.kind === 'auto' && (j.status === 'review' || (j.status === 'done' && !t.decided))) acts.push(button({ label: tagged(D.approveAuto, 'you'), size: 'sm', disabled: busy, attrs: { 'data-day-act': 'approve' }, onClick: () => decide(ws, t, 'approve') }));
|
|
999
|
+
if (canRevise && !ui.revising.has(t.id)) acts.push(button({ label: tagged(D.revise, 'agent'), size: 'sm', disabled: busy, attrs: { 'data-day-act': 'revise' }, onClick: () => { ui.revising.add(t.id); rerender(); } }));
|
|
1000
|
+
if (t && j.status === 'failed') acts.push(button({ label: tagged(D.retry, 'agent'), variant: 'primary', size: 'sm', disabled: busy, attrs: { 'data-day-act': 'retry' }, onClick: () => decide(ws, t, 'retry') }));
|
|
999
1001
|
acts.push(extLink(D.openConversation, j.link, { 'data-day-link': 'conversation' }));
|
|
1000
1002
|
if (t && t.draft) acts.push(extLink(D.openDraft, t.draft.link, { 'data-day-link': 'draft' }));
|
|
1001
1003
|
if (t && t.sent) acts.push(extLink(D.openSent, t.sent.link, { 'data-day-link': 'sent' }));
|
|
1002
|
-
const sid = t && (t.session_id || t.stalled_session);
|
|
1003
|
-
if (sid) acts.push(button({ label: D.openSession, size: 'sm', variant: 'ghost', attrs: { 'data-day-act': 'session' }, onClick: () => openSession({ id: sid, name: 'day-task' }) }));
|
|
1004
1004
|
let form = null;
|
|
1005
1005
|
if (t && ui.revising.has(t.id) && canRevise) {
|
|
1006
1006
|
const f = field({ label: D.noteLabel, kind: 'textarea', value: ui.notes.get(t.id) || '', placeholder: D.noteHint, hideLabel: true, attrs: { maxlength: '500', 'data-day-input': 'note-' + t.id } });
|
|
1007
1007
|
const ta = f.control;
|
|
1008
1008
|
ta.addEventListener('input', () => { ui.notes.set(t.id, ta.value); });
|
|
1009
1009
|
form = el('div', { class: 'td-day__revise', 'data-day': 'revise' }, f, el('div', { class: 'td-day__acts' },
|
|
1010
|
-
button({ label: D.sendNote, variant: 'primary', size: 'sm', disabled: busy, attrs: { 'data-day-act': 'send-note' }, onClick: () => {
|
|
1010
|
+
button({ label: tagged(D.sendNote, 'agent'), variant: 'primary', size: 'sm', disabled: busy, attrs: { 'data-day-act': 'send-note' }, onClick: () => {
|
|
1011
1011
|
const v = (ui.notes.get(t.id) || '').trim();
|
|
1012
1012
|
if (!v || v.length > 500) { toast({ text: D.noteNeeded, tone: 'destructive' }); return; }
|
|
1013
1013
|
decide(ws, t, 'revise', v);
|
|
@@ -1028,15 +1028,39 @@ function jobCard(ctx, j) {
|
|
|
1028
1028
|
form);
|
|
1029
1029
|
}
|
|
1030
1030
|
|
|
1031
|
+
/** The goal of a job is the PM's link mark (PRD L1). The press says what it did, with Undo, like the other marks. */
|
|
1032
|
+
function linkPress(ctx, linkId, goalId) {
|
|
1033
|
+
const g = goalId ? ctx.m.allGoals.find((x) => x.id === goalId) : null;
|
|
1034
|
+
return markPress(ctx.ws, linkId, { action: 'link', goal: goalId }, g ? D.linked(g.text) : D.unlinked);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
/** What a job says about itself (PRD J1, J3, J4): who acts, where it came from, its goal and, with a session, the way to it. */
|
|
1038
|
+
function jobMeta(ctx, j) {
|
|
1039
|
+
const { m } = ctx;
|
|
1040
|
+
const who = whoActs(j);
|
|
1041
|
+
const t = j.task;
|
|
1042
|
+
const sid = t && (t.session_id || t.stalled_session);
|
|
1043
|
+
const known = sid ? (state.sessions || []).find((x) => x && x.id === sid) : null;
|
|
1044
|
+
const name = (known && known.name) || 'day-task';
|
|
1045
|
+
const origin = originOf(j);
|
|
1046
|
+
const pick = goalPicker(m, j.goal, (g) => linkPress(ctx, j.linkId, g), { attrs: { 'data-day-job-goal': j.id } });
|
|
1047
|
+
return el('div', { class: 'td-day__jobmeta', 'data-day': 'jobmeta' },
|
|
1048
|
+
el('p', { class: 'td-day__who2', 'data-day': 'who', 'data-who': who.who }, whoMark(who.who), el('span', null, who.text)),
|
|
1049
|
+
dl([[D.origin.label, el('span', { 'data-day': 'origin', 'data-origin': origin }, D.origin[origin])], [D.goalCtl.label, pick]]),
|
|
1050
|
+
sid ? el('div', { class: 'td-day__acts' }, button({ label: tagged(D.openSessionOf(name), 'you'), size: 'sm', variant: 'ghost', attrs: { 'data-day-act': 'session' },
|
|
1051
|
+
onClick: () => openSession({ id: sid, name }) })) : null);
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1031
1054
|
function jobDetail(ctx, j) {
|
|
1032
1055
|
const { m } = ctx;
|
|
1033
1056
|
const n = j.goal ? m.goalNo.get(j.goal) : null;
|
|
1034
|
-
const meets = m.events.filter((e) => e.
|
|
1057
|
+
const meets = m.events.filter((e) => e.gid && e.gid === j.goal);
|
|
1035
1058
|
const mention = j.item && j.item.mention;
|
|
1036
1059
|
return [
|
|
1037
1060
|
dhead(n ? D.kind.jobGoal(n) : D.kind.job, null),
|
|
1038
1061
|
j.repo ? el('div', { class: 'td-day__acts' }, repoTag(m, j.repo)) : null,
|
|
1039
1062
|
jobCard(ctx, j),
|
|
1063
|
+
jobMeta(ctx, j),
|
|
1040
1064
|
meets.length || mention ? el('div', { class: 'td-day__rel' }, el('h4', null, D.related), el('ul', { class: 'td-day__rows' },
|
|
1041
1065
|
...meets.map((e) => relRow(ctx, 'event', e.id, e.title, e.time)),
|
|
1042
1066
|
mention ? relRow(ctx, 'mention', j.id, j.item.ask || j.title, srcName(j.source)) : null)) : null,
|
|
@@ -1048,6 +1072,7 @@ function mentionDetail(ctx, j) {
|
|
|
1048
1072
|
const reply = typeof it.reply_needed === 'boolean' ? (it.reply_needed ? D.yes : D.no) : null;
|
|
1049
1073
|
return [
|
|
1050
1074
|
dhead(D.kind.mention(srcName(j.source)), it.about || j.title),
|
|
1075
|
+
hasCard(j.source) ? el('div', { class: 'td-day__mcbox', 'data-day': 'mention-card' }, mentionCard(j, srcName(j.source))) : null,
|
|
1051
1076
|
dl([[D.where, D.fromWho(it.who, it.where)], [D.ask, it.ask || it.why_now || null], [D.reply, reply]]),
|
|
1052
1077
|
el('div', { class: 'td-day__acts' }, extLink(D.openIn(srcName(j.source)), j.link, { 'data-day-link': 'conversation' }), mentionStatus(j)),
|
|
1053
1078
|
el('div', { class: 'td-day__rel' }, el('h4', null, D.theJob), el('ul', { class: 'td-day__rows' }, relRow(ctx, 'job', j.id, j.title, '', j))),
|
|
@@ -1265,7 +1290,6 @@ function sectionSelector(ctx) {
|
|
|
1265
1290
|
{ id: 'goals', icon: 'target', label: D.sections.goals, count: openGoals },
|
|
1266
1291
|
{ id: 'jobs', icon: 'list-checks', label: D.sections.jobs, count: m.jobs.length },
|
|
1267
1292
|
{ id: 'mentions', icon: 'text-quote', label: D.sections.mentions, count: m.mentions.length },
|
|
1268
|
-
{ id: 'summary', icon: 'bot', label: D.sections.summary, count: null },
|
|
1269
1293
|
];
|
|
1270
1294
|
return tabs({ variant: 'segmented', role: 'tablist', ariaLabel: D.sections.label, value: ui.section, full: true, attrs: { 'data-day': 'section-tabs' },
|
|
1271
1295
|
items,
|
|
@@ -1284,8 +1308,6 @@ function sectionContent(ctx) {
|
|
|
1284
1308
|
return jobsPanel(ctx);
|
|
1285
1309
|
case 'mentions':
|
|
1286
1310
|
return mentionsPanel(ctx);
|
|
1287
|
-
case 'summary':
|
|
1288
|
-
return summaryPanel(ctx) || note(D.noResult, { 'data-day': 'no-result' });
|
|
1289
1311
|
default:
|
|
1290
1312
|
return agenda(ctx);
|
|
1291
1313
|
}
|
|
@@ -1309,7 +1331,7 @@ export function daySection({ ws, setState, openIni, owed, here, all } = {}) {
|
|
|
1309
1331
|
}
|
|
1310
1332
|
const sel = ui.sel;
|
|
1311
1333
|
const ctx = {
|
|
1312
|
-
ws, m, sel, openIni, reload: () => load(ws), rerender: () => rerender(), select, repoPicker,
|
|
1334
|
+
ws, m, sel, openIni, reload: () => load(ws), rerender: () => rerender(), select, repoPicker, mark: (id, op, said) => markPress(ws, id, op, said),
|
|
1313
1335
|
showAll: () => { if (setState) setState({ todayScope: 'all' }); },
|
|
1314
1336
|
cur: (type, id) => { const s = sel || defaultSel(m, ctx.owed); return s.type === type && s.id === String(id) ? 'true' : 'false'; },
|
|
1315
1337
|
owed: owedItems(owed),
|