create-pmos 0.145.0 → 0.147.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/c3-sessions-panel-test.js +126 -0
- package/workspace/guide/briefing-cards.md +2 -2
- package/workspace/guide/briefing.md +19 -1
- package/workspace/guide/console-calendar.md +18 -3
- package/workspace/guide/console3-sessions.md +35 -0
- package/workspace/guide/data-freshness.md +7 -0
- package/workspace/guide/index.md +1 -0
- package/workspace/lib/briefing-real.js +264 -0
- package/workspace/lib/calendar-layers.js +5 -2
- package/workspace/lib/freshness.js +4 -1
- package/workspace/lib/pmos-verbs.js +3 -0
- package/workspace/lib/routes-briefing.js +25 -5
- package/workspace/lib/routes-calendar-views.js +24 -2
- package/workspace/lib/routes-runs.js +12 -4
- package/workspace/lib/tracker-cli.js +68 -0
- package/workspace/lib/tracker-read.js +40 -0
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/briefing-model.js +12 -1
- package/workspace/public/pm/c3-brief-needs.js +16 -4
- package/workspace/public/pm/c3-brief-pulse.js +35 -7
- package/workspace/public/pm/c3-calendar.js +162 -25
- package/workspace/public/pm/c3-case.js +7 -2
- package/workspace/public/pm/c3-demo-cal4.js +3 -3
- package/workspace/public/pm/c3-dock.js +2 -1
- package/workspace/public/pm/c3-session-term.js +44 -0
- package/workspace/public/pm/c3-sheet.js +3 -2
- package/workspace/public/pm/c3-shell.js +2 -1
- package/workspace/public/pm/c3-terminal-data.js +1 -1
- package/workspace/public/pm/c3-terminal-icons.js +1 -1
- package/workspace/public/pm/console3.css +24 -0
- package/workspace/public/pm/run-card.js +4 -1
- package/workspace/public/pm/sessions-panel.js +40 -4
- package/workspace/skills/tracker-refresh.skill +2 -1
- package/workspace/test-files.json +6 -0
|
@@ -63,7 +63,7 @@ const setVar = (node, name, value) => { node.style.setProperty('--cal3-' + name,
|
|
|
63
63
|
/* ── the page's own state on the instance: which layers are on, whether free time shows, what is selected ─────────── */
|
|
64
64
|
|
|
65
65
|
function state(r) {
|
|
66
|
-
const st = r.page || (r.page = { on: new Set(LAYERS.map((l) => l[0]).filter((id) => !(r.ctx && r.ctx.overlay && mem.hide.includes(id)))), free: true, tidy: false, sel: null });
|
|
66
|
+
const st = r.page || (r.page = { on: new Set(LAYERS.map((l) => l[0]).filter((id) => !(r.ctx && r.ctx.overlay && mem.hide.includes(id)))), free: true, tidy: false, routine: false, sel: null });
|
|
67
67
|
if (r.sel) { st.sel = { type: 'event', id: r.sel }; r.sel = null; } // a meeting chosen on the Briefing (openDay)
|
|
68
68
|
if (r.pendingMode) { st.mode = r.pendingMode; r.pendingMode = null; } // a note opened before the page drew (the Record chip)
|
|
69
69
|
if (!st.mode) st.mode = 'cal';
|
|
@@ -146,7 +146,7 @@ export function drawCalendarPage(host, r, h) {
|
|
|
146
146
|
|
|
147
147
|
const body = el('div', { class: 'calbody' + (r.anim && !r.loading ? ' ' + r.anim : '') },
|
|
148
148
|
el('div', { class: 'cal3-wrap' },
|
|
149
|
-
el('div', { class: 'cal3-main' }, empty, grid),
|
|
149
|
+
el('div', { class: 'cal3-main' }, empty, empty ? null : highlights(ui, range), grid),
|
|
150
150
|
el('div', { class: 'cal3-side' }, detail(ui), st.tidy ? tidyList(ui) : null, st.free ? freeList(ui, range) : null, lanes(ui, range))));
|
|
151
151
|
return pageBar(chips(ui, range), body);
|
|
152
152
|
}
|
|
@@ -186,7 +186,8 @@ function chips(ui, range) {
|
|
|
186
186
|
}, el('i', { class: 'cal3-sw cal3-sw-' + id }))),
|
|
187
187
|
el('span', { class: 'cal3-sep', 'aria-hidden': 'true' }),
|
|
188
188
|
chip('free', 'Free time', freeN, st.free, () => { st.free = !st.free; }, c3ic('clock', 'sm')),
|
|
189
|
-
chip('tidy', 'Tidy-up', tidyCount(M, ui.d), st.tidy, () => { st.tidy = !st.tidy; }, c3ic('spark', 'sm'))
|
|
189
|
+
chip('tidy', 'Tidy-up', tidyCount(M, ui.d), st.tidy, () => { st.tidy = !st.tidy; }, c3ic('spark', 'sm')),
|
|
190
|
+
WIDE.includes(M.view) ? chip('routine', 'Routine', routineCount(ui, range), st.routine, () => { st.routine = !st.routine; }, c3ic('report', 'sm')) : null);
|
|
190
191
|
}
|
|
191
192
|
/* the number of suggestions in the range: the server's total (the list may be capped), or null when it sent none */
|
|
192
193
|
const tidyCount = (M, d) => (M.tidy ? Math.max(0, (typeof d.tidy_total === 'number' ? d.tidy_total : M.tidy.length) - (d.tidy.length - M.tidy.length)) : null); // less the ones the PM dismissed
|
|
@@ -332,7 +333,123 @@ function freeBlock(ui, date, b, Y) {
|
|
|
332
333
|
return n;
|
|
333
334
|
}
|
|
334
335
|
|
|
335
|
-
/* ──
|
|
336
|
+
/* ── significance (initiative calendar-highlights) ───────────────────────────────────────────────────────────────
|
|
337
|
+
* Month, Quarter and Year show what matters and fold the rest into a count. Routine: a report date that recurs more often
|
|
338
|
+
* than monthly (the server's `routine` flag, or a daily, weekly or bi-weekly cadence), a meeting that repeats in the range,
|
|
339
|
+
* a stand-up or a 1:1, and a meeting tied to nothing. A due item that is on track is counted, not named. Key at Day, Week and
|
|
340
|
+
* Month: milestones, releases, reports that go out, late or at-risk due items, and meetings tied to a promise or a decision.
|
|
341
|
+
* Quarter and Year are one step stricter: milestones, releases, reports that go out, and late items; meetings are counted.
|
|
342
|
+
* The Routine chip (Month, Quarter, Year) brings back every item as markers. */
|
|
343
|
+
const WIDE = ['month', 'quarter', 'year'];
|
|
344
|
+
const FREQUENT = /\b(daily|weekly|bi-?weekly|fortnightly|stand-?up)\b/i;
|
|
345
|
+
const normT = (t) => String(t || '').toLowerCase().replace(/[^\p{L}\p{N}]+/gu, ' ').trim();
|
|
346
|
+
const isDue = (m) => m.kind === 'due' || m.kind === 'own';
|
|
347
|
+
const routineMark = (m) => m.kind === 'report' && (m.routine === true || (m.routine !== false && (FREQUENT.test(String(m.cadence || '')) || FREQUENT.test(String(m.title || '')) || /\bdraft\b/i.test(String(m.title || '')))));
|
|
348
|
+
const quietDue = (m) => isDue(m) && m.tone !== 'bad' && m.tone !== 'warn';
|
|
349
|
+
const keyMark = (m) => !routineMark(m) && !quietDue(m);
|
|
350
|
+
const landMark = (m) => m.kind === 'milestone' || m.kind === 'release' || (m.kind === 'report' && !routineMark(m)) || (isDue(m) && m.tone === 'bad');
|
|
351
|
+
const shortDay = (h, date) => h.dayLabel(date).replace(/^\S+ /, '');
|
|
352
|
+
|
|
353
|
+
/* the meetings of the range, each marked key or routine; a title seen more than once in the range is a recurring meeting */
|
|
354
|
+
function meetingsKeyed(ui) {
|
|
355
|
+
const { M } = ui;
|
|
356
|
+
if (!M._keyed) {
|
|
357
|
+
const seen = new Map(); for (const m of M.meetings) { const k = normT(m.e.title); seen.set(k, (seen.get(k) || 0) + 1); }
|
|
358
|
+
const now = nowIso();
|
|
359
|
+
M._keyed = M.meetings.map((m) => {
|
|
360
|
+
const t = m.e.title || '';
|
|
361
|
+
const routine = m.e.routine === true || seen.get(normT(t)) > 1 || /stand-?up|\b1\s*[:/-]\s*1\b|one[- ]on[- ]one/i.test(t) || !evState(ui, m, now).tied;
|
|
362
|
+
return { ...m, key: m.e.key === true || !routine };
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
return M._keyed;
|
|
366
|
+
}
|
|
367
|
+
const meetingsIn = (ui, a, b) => (ui.st.on.has('meetings') ? meetingsKeyed(ui).filter((m) => m.date >= a && m.date <= b) : []);
|
|
368
|
+
|
|
369
|
+
/* the number of folded items in the range, for the Routine chip */
|
|
370
|
+
function routineCount(ui, range) {
|
|
371
|
+
const wide = ui.M.view !== 'month';
|
|
372
|
+
const marks = marksIn(ui.M, ui.st, range.start, range.end).filter((m) => (wide ? !landMark(m) : !keyMark(m))).length;
|
|
373
|
+
return marks + meetingsIn(ui, range.start, range.end).filter((m) => wide || !m.key).length;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/* a name inside a highlight: a link-styled button that selects the item, so its detail opens beside the grid */
|
|
377
|
+
function nameBtn(ui, sel, label, attrs) {
|
|
378
|
+
return button({ label, variant: 'ghost', attrs: { class: 'cal3-sl', ...attrs }, onClick: () => ui.select(sel, null) });
|
|
379
|
+
}
|
|
380
|
+
const markName = (ui, m, label) => nameBtn(ui, { type: 'marker', id: m.id }, label || m.title, { 'data-cal3-mk': m.id, 'data-cal3-layer': m.layer, 'data-cal3-kind': m.kind, title: kindOf(m)[0] + ': ' + m.title + ', ' + ui.h.dayLabel(m.date) });
|
|
381
|
+
const meetName = (ui, m) => nameBtn(ui, { type: 'event', id: m.e.id }, m.e.title || m.e.id, { 'data-cal3-hl-ev': m.e.id, title: (m.e.title || m.e.id) + ', ' + ui.h.dayLabel(m.date) });
|
|
382
|
+
|
|
383
|
+
/* join nodes as "a, b and c", with "N more" for what is past max */
|
|
384
|
+
function joinList(nodes, max) {
|
|
385
|
+
const a = nodes.slice(0, max); if (nodes.length > max) a.push(String(nodes.length - max) + ' more');
|
|
386
|
+
const out = [];
|
|
387
|
+
a.forEach((n, i) => { if (i) out.push(i === a.length - 1 ? ' and ' : ', '); out.push(n); });
|
|
388
|
+
return out;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/* the highlight of a span as a list of text and name nodes; [] when nothing in it is significant.
|
|
392
|
+
* mode 'cell' is the short form of a Month day; 'top' is the range summary and a Quarter or Year column. */
|
|
393
|
+
function highlightOf(ui, a, b, mode) {
|
|
394
|
+
const { h, M } = ui; const cell = mode === 'cell'; const wide = M.view === 'quarter' || M.view === 'year';
|
|
395
|
+
const marks = marksIn(M, ui.st, a, b); const meets = wide ? [] : meetingsIn(ui, a, b).filter((m) => m.key);
|
|
396
|
+
const on = (date) => (cell ? '' : ' on ' + shortDay(h, date));
|
|
397
|
+
const rel = marks.filter((m) => m.kind === 'release'); const ms = marks.filter((m) => m.kind === 'milestone' && (!m.tone || m.tone === 'ok'));
|
|
398
|
+
const rep = marks.filter((m) => m.kind === 'report' && !routineMark(m));
|
|
399
|
+
const lateMs = marks.filter((m) => m.kind === 'milestone' && (m.tone === 'bad' || m.tone === 'warn'));
|
|
400
|
+
const riskRel = rel.filter((m) => m.tone === 'bad' || m.tone === 'warn');
|
|
401
|
+
const lateDue = marks.filter((m) => isDue(m) && m.tone === 'bad'); const riskDue = wide ? [] : marks.filter((m) => isDue(m) && m.tone === 'warn');
|
|
402
|
+
const out = []; const sent = (...k) => { if (out.length) out.push(' '); out.push(...k); };
|
|
403
|
+
const dated = (list, max) => joinList(list.map((m) => el('span', null, markName(ui, m), on(m.date))), max);
|
|
404
|
+
if (rel.length) sent(...(cell ? [...joinList(rel.map((m) => markName(ui, m)), 2), rel.length === 1 ? ' ships.' : ' ship.'] : [plural(rel.length, 'release') + ': ', ...dated(rel, 3), '.']));
|
|
405
|
+
if (ms.length) sent(ms.length === 1 ? 'Milestone: ' : plural(ms.length, 'milestone') + ': ', ...dated(ms, 3), '.');
|
|
406
|
+
if (meets.length) {
|
|
407
|
+
if (cell || M.view === 'day') sent(...joinList(meets.map((m) => el('span', null, meetName(ui, m), h.timeOf(m.e.at) !== null ? ' at ' + h.hhmm(h.timeOf(m.e.at)) : '')), cell ? 2 : 4), '.');
|
|
408
|
+
else {
|
|
409
|
+
const big = meets.slice().sort((x, y) => hoursOf(y, h) - hoursOf(x, h) || String(x.e.at).localeCompare(String(y.e.at))).slice(0, 2).sort((x, y) => String(x.e.at).localeCompare(String(y.e.at)));
|
|
410
|
+
sent(plural(meets.length, 'key meeting') + (meets.length > big.length ? ', the biggest ' : ': '), ...joinList(big.map((m) => el('span', null, meetName(ui, m), ' on ' + shortDay(h, m.date))), 2), '.');
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
if (rep.length) sent(...(cell || rep.length <= 2 ? [...joinList(rep.map((m) => markName(ui, m, m.title.replace(/ goes out$/, ''))), 2), rep.length === 1 ? ' goes out.' : ' go out.'] : [rep.length + ' reports go out.']));
|
|
414
|
+
const watch = [];
|
|
415
|
+
for (const m of lateMs) watch.push(el('span', null, markName(ui, m, m.title.replace(/\s*\(late\)$/, '')), m.tone === 'bad' ? ' is late' : ' is at risk', cell ? '' : ' (' + shortDay(h, m.date) + ')'));
|
|
416
|
+
for (const m of riskRel) watch.push(el('span', null, markName(ui, m), m.tone === 'bad' ? ' is late' : ' is at risk'));
|
|
417
|
+
if (lateDue.length) watch.push(cell || lateDue.length <= 2 ? el('span', null, ...joinList(lateDue.map((m) => markName(ui, m)), 2), lateDue.length === 1 ? ' is late' : ' are late') : plural(lateDue.length, 'due item') + ' are late');
|
|
418
|
+
if (riskDue.length) watch.push(cell || riskDue.length <= 2 ? el('span', null, ...joinList(riskDue.map((m) => markName(ui, m)), 2), ' due soon') : plural(riskDue.length, 'due item') + ' due soon');
|
|
419
|
+
if (watch.length) sent(el('b', { class: 'cal3-watch' }, 'Watch:'), ' ', ...joinList(watch, 4), '.');
|
|
420
|
+
return out;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/* the range summary above the grid, at every zoom */
|
|
424
|
+
function highlights(ui, range) {
|
|
425
|
+
const { h, M } = ui;
|
|
426
|
+
const MF = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
|
|
427
|
+
const mo = Number(range.start.slice(5, 7));
|
|
428
|
+
const label = M.view === 'day' ? h.dayLabel(range.start) : M.view === 'week' ? 'Week of ' + h.dayLabel(range.start) : M.view === 'month' ? MF[mo - 1] : M.view === 'quarter' ? 'Q' + (Math.floor((mo - 1) / 3) + 1) : range.start.slice(0, 4);
|
|
429
|
+
const body = highlightOf(ui, range.start, range.end, 'top');
|
|
430
|
+
return el('div', { class: 'cal3-sum', 'data-cal3': 'highlights' },
|
|
431
|
+
el('span', { class: 'cal3-sum-h' }, label + ' highlights'),
|
|
432
|
+
el('p', null, ...(body.length ? body : ['Nothing significant. Only routine items fall in this range.'])));
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/* the quiet line of a span: what is counted and not named; pressing it opens the span */
|
|
436
|
+
function quietLine(ui, a, b, view, opts) {
|
|
437
|
+
const wide = opts && opts.wide;
|
|
438
|
+
const marks = marksIn(ui.M, ui.st, a, b); const meets = meetingsIn(ui, a, b);
|
|
439
|
+
const due = marks.filter((m) => isDue(m) && (wide ? m.tone !== 'bad' : quietDue(m)));
|
|
440
|
+
const rt = marks.filter(routineMark).length + (wide ? 0 : meets.filter((m) => !m.key).length);
|
|
441
|
+
const nMeet = wide ? meets.length : 0;
|
|
442
|
+
const parts = [nMeet ? plural(nMeet, 'meeting') : null, due.length ? due.length + ' due' : null, rt ? rt + ' routine' : null].filter(Boolean);
|
|
443
|
+
if (!parts.length) return null;
|
|
444
|
+
const mo = Number(a.slice(5, 7));
|
|
445
|
+
const where = view === 'day' ? ui.h.dayLabel(a) : view === 'week' ? 'the week of ' + ui.h.dayLabel(a) : view === 'quarter' ? 'Q' + (Math.floor((mo - 1) / 3) + 1) + ' ' + a.slice(0, 4) : ui.h.MN[mo - 1] + ' ' + a.slice(0, 4);
|
|
446
|
+
const names = [...new Set([...due.map((m) => m.title), ...marks.filter(routineMark).map((m) => m.title)])];
|
|
447
|
+
const tip = parts.join(', ') + (names.length ? ': ' + names.slice(0, 8).join(', ') + (names.length > 8 ? ' and more' : '') : '') + '. Open ' + where;
|
|
448
|
+
return button({ label: parts.join(' · '), variant: 'ghost', size: 'sm', attrs: { class: 'cal3-more cal3-rt', 'data-cal3-quiet': a, title: tip, 'aria-label': tip },
|
|
449
|
+
onClick: () => ui.h.go(ui.host, ui.r, { view, date: a, anim: 'zi' }) });
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/* ── Month: a cell per day. Its highlight in a sentence and a quiet line; with Routine on, the meeting count and every marker ── */
|
|
336
453
|
|
|
337
454
|
function monthGrid(ui) {
|
|
338
455
|
const { M, st, h, d } = ui;
|
|
@@ -342,38 +459,58 @@ function monthGrid(ui) {
|
|
|
342
459
|
for (let i = 0; i < off; i++) g.appendChild(el('div', { class: 'cal3-mc out', 'aria-hidden': 'true' }));
|
|
343
460
|
for (const c of days) {
|
|
344
461
|
const wd = h.dowOf(c.start); const n = st.on.has('meetings') ? M.meetingsOf(c).length : 0;
|
|
345
|
-
|
|
462
|
+
const hl = st.routine ? [] : highlightOf(ui, c.start, c.start, 'cell');
|
|
463
|
+
g.appendChild(el('div', { class: 'cal3-mc' + (wd === 0 || wd === 6 ? ' we' : '') + (c.start === d.today ? ' today' : '') + (hl.length ? ' key' : ''), 'data-cal3-date': c.start },
|
|
346
464
|
button({ label: String(Number(c.start.slice(8))), variant: 'ghost', size: 'sm', attrs: { class: 'cal3-dn', 'data-cal3-day': c.start, 'aria-label': 'Open ' + h.dayLabel(c.start) + (n ? ', ' + plural(n, 'meeting') : ''), title: 'Open ' + h.dayLabel(c.start) }, onClick: () => h.go(ui.host, ui.r, { view: 'day', date: c.start, anim: 'zi' }) }),
|
|
347
|
-
|
|
348
|
-
|
|
465
|
+
...(st.routine
|
|
466
|
+
? [n ? el('span', { class: 'cal3-cnt' }, plural(n, 'meeting')) : null, ...markRow(ui, c.start, c.start, 3, true, 'day')]
|
|
467
|
+
: [hl.length ? el('p', { class: 'cal3-hl', 'data-cal3-hl': c.start }, ...hl) : null, quietLine(ui, c.start, c.start, 'day')])));
|
|
349
468
|
}
|
|
350
|
-
return el('section', { class: 'card cal3-grid', 'data-cv': 'grid' }, g);
|
|
469
|
+
return el('section', { class: 'card cal3-grid', 'data-cv': 'grid' }, g, foldNote(ui));
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/* the line under a Month, Quarter or Year grid that says what is counted and not named */
|
|
473
|
+
function foldNote(ui) {
|
|
474
|
+
if (ui.st.routine) return null;
|
|
475
|
+
return el('p', { class: 'dim cal3-fold', 'data-cal3': 'fold-note' }, (ui.M.view === 'month'
|
|
476
|
+
? 'Report drafts, recurring meetings, stand-ups, 1:1s and due items on track are counted under each day, not listed.'
|
|
477
|
+
: 'Meetings, due items that are not late, and routine items are counted under each highlight, not listed. Open a month for its key meetings.')
|
|
478
|
+
+ ' Turn on Routine to show everything.');
|
|
351
479
|
}
|
|
352
480
|
|
|
353
|
-
/* ── Quarter: a
|
|
481
|
+
/* ── Quarter: a column per month; Year: a column per quarter. Each has its highlight and its quiet line ──────────── */
|
|
354
482
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
483
|
+
const monthEnd = (start) => { const y = Number(start.slice(0, 4)); const m = Number(start.slice(5, 7)); const last = new Date(y, m, 0).getDate(); return start.slice(0, 8) + String(last).padStart(2, '0'); };
|
|
484
|
+
const monthStart = (y, m) => y + '-' + String(m).padStart(2, '0') + '-01';
|
|
485
|
+
|
|
486
|
+
function spanColumn(ui, a, b, head, now, extra, view) {
|
|
487
|
+
const hl = ui.st.routine ? null : highlightOf(ui, a, b, 'top');
|
|
488
|
+
return el('div', { class: 'cal3-ym cal3-col' + (now ? ' now' : ''), 'data-cal3-span': a }, head,
|
|
489
|
+
ui.st.routine ? el('div', { class: 'cal3-qmk' }, ...markRow(ui, a, b, 12, false, view))
|
|
490
|
+
: el('p', { class: 'cal3-hl' + (hl.length ? '' : ' dim'), 'data-cal3-hl': a }, ...(hl.length ? hl : ['Nothing significant.'])),
|
|
491
|
+
extra || null, ui.st.routine ? null : quietLine(ui, a, b, view, { wide: true }));
|
|
361
492
|
}
|
|
362
493
|
|
|
363
494
|
function quarterRows(ui) {
|
|
364
|
-
const {
|
|
365
|
-
const
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
495
|
+
const { h, d } = ui; const range = d.range || { start: d.today, end: d.today };
|
|
496
|
+
const y = Number(range.start.slice(0, 4)); const m0 = Number(range.start.slice(5, 7));
|
|
497
|
+
const cols = [0, 1, 2].map((i) => {
|
|
498
|
+
const a = monthStart(y, m0 + i); const b = monthEnd(a);
|
|
499
|
+
const head = button({ label: h.MN[m0 + i - 1] + ' ' + y, variant: 'ghost', size: 'sm', attrs: { class: 'cal3-dbtn', 'data-cal3-month': a.slice(0, 7), title: 'Open ' + h.MN[m0 + i - 1] + ' ' + y }, onClick: () => h.go(ui.host, ui.r, { view: 'month', date: a, anim: 'zi' }) });
|
|
500
|
+
return spanColumn(ui, a, b, head, a <= d.today && d.today <= b, null, 'month');
|
|
501
|
+
});
|
|
502
|
+
return el('section', { class: 'card cal3-grid', 'data-cv': 'grid' }, el('div', { class: 'cal3-cols cal3-cols-3', 'data-cal3': 'quarter' }, ...cols), foldNote(ui));
|
|
369
503
|
}
|
|
370
504
|
|
|
371
505
|
function yearCards(ui) {
|
|
372
|
-
const {
|
|
373
|
-
const
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
506
|
+
const { h, d } = ui; const range = d.range || { start: d.today, end: d.today }; const y = Number(range.start.slice(0, 4));
|
|
507
|
+
const cols = [0, 1, 2, 3].map((q) => {
|
|
508
|
+
const a = monthStart(y, q * 3 + 1); const b = monthEnd(monthStart(y, q * 3 + 3));
|
|
509
|
+
const head = button({ label: 'Q' + (q + 1) + ' ' + y + ' · ' + h.MN[q * 3] + ' to ' + h.MN[q * 3 + 2], variant: 'ghost', size: 'sm', attrs: { class: 'cal3-dbtn', 'data-cal3-quarter': 'Q' + (q + 1), title: 'Open Q' + (q + 1) + ' ' + y }, onClick: () => h.go(ui.host, ui.r, { view: 'quarter', date: a, anim: 'zi' }) });
|
|
510
|
+
const months = el('div', { class: 'cal3-mlinks' }, ...[1, 2, 3].map((k) => { const s = monthStart(y, q * 3 + k); return button({ label: h.MN[q * 3 + k - 1], variant: 'ghost', size: 'sm', attrs: { class: 'cal3-more', 'data-cal3-month': s.slice(0, 7), title: 'Open ' + h.MN[q * 3 + k - 1] + ' ' + y }, onClick: () => h.go(ui.host, ui.r, { view: 'month', date: s, anim: 'zi' }) }); }));
|
|
511
|
+
return spanColumn(ui, a, b, head, a <= d.today && d.today <= b, months, 'quarter');
|
|
512
|
+
});
|
|
513
|
+
return el('section', { class: 'card cal3-grid', 'data-cv': 'grid' }, el('div', { class: 'cal3-cols cal3-cols-4', 'data-cal3': 'year' }, ...cols), foldNote(ui));
|
|
377
514
|
}
|
|
378
515
|
|
|
379
516
|
/* ── the detail column ────────────────────────────────────────────────────────────────────────────────────────────── */
|
|
@@ -171,13 +171,18 @@ export function renderCase(model, opts = {}) {
|
|
|
171
171
|
root.append(el('ul', { class: 'chgl', role: 'list' }, ...c.evidence.map((r) => el('li', { class: r.tone }, c3ic(r.icon, 'sm'), el('span', null, r.text), r.tag ? el('span', { class: 'tag' }, r.tag) : null))));
|
|
172
172
|
}
|
|
173
173
|
if (optsHost) { drawOptions(); drawSlide(); root.append(optsHost, slideHost); }
|
|
174
|
+
/* a go with an https href is a link to the record outside PMOS (a Jira ticket, the brief's source): it opens in a new tab, changes nothing,
|
|
175
|
+
* and takes the place of the in-page go button */
|
|
176
|
+
const href = c.go && typeof c.go.href === 'string' && /^https:\/\//.test(c.go.href) ? c.go.href : null;
|
|
177
|
+
const goLink = href ? el('a', { class: 'chip2 lk', href, target: '_blank', rel: 'noopener noreferrer', 'data-case-link': '' }, c3ic('out', 'sm'), c.go.label) : null;
|
|
178
|
+
if (goLink) c.go = null;
|
|
174
179
|
const goBtn = c.go ? el('button', { type: 'button', class: 'chip2 lk', onclick: () => { if (opts.onGo) opts.onGo(c.go); } }, c3ic('next', 'sm'), c.go.label) : null;
|
|
175
|
-
if (c.unsure || c.ifYouWait || c.why || goBtn) {
|
|
180
|
+
if (c.unsure || c.ifYouWait || c.why || goBtn || goLink) {
|
|
176
181
|
root.append(el('div', { class: 'cft' },
|
|
177
182
|
c.unsure ? el('span', null, c3ic('info', 'sm'), 'Unsure: ' + c.unsure) : null,
|
|
178
183
|
c.ifYouWait ? el('span', null, c3ic('alert', 'sm'), 'If you wait: ' + c.ifYouWait) : null,
|
|
179
184
|
c.why ? el('span', null, c3ic('eye', 'sm'), 'Why shown: ' + c.why) : null,
|
|
180
|
-
goBtn));
|
|
185
|
+
goLink || goBtn));
|
|
181
186
|
}
|
|
182
187
|
/* a handle for the caller: read or set the pick, and tear the slide down when the case leaves the page */
|
|
183
188
|
root.c3case = { get picked() { return picked; }, pick, model: c, dispose() { if (slideHost) disposeSlide(slideHost); if (disposeHold) { disposeHold(); disposeHold = null; } } };
|
|
@@ -39,12 +39,12 @@ const DATED = [
|
|
|
39
39
|
['2026-10-13', 'own', 'mine', 'Share returns insights with sales', null, item('item-demo017'), 'returns'],
|
|
40
40
|
['2026-11-16', 'own', 'mine', 'Write the Q1 discovery plan', null, item('item-demo020'), null],
|
|
41
41
|
];
|
|
42
|
-
/* the weekly status to leadership is a report date every Friday */
|
|
43
|
-
for (let d = '2026-09-04'; d <= '2026-12-25'; d = addDays(d, 7)) DATED.push([d, 'report', 'reports', 'Weekly status to leadership', null, { type: 'report', id: 'leadership-weekly' }, null]);
|
|
42
|
+
/* the weekly status to leadership is a report date every Friday: routine, so Month, Quarter and Year count it (calendar-highlights) */
|
|
43
|
+
for (let d = '2026-09-04'; d <= '2026-12-25'; d = addDays(d, 7)) DATED.push([d, 'report', 'reports', 'Weekly status to leadership', null, { type: 'report', id: 'leadership-weekly' }, null, { cadence: 'weekly', routine: true }]);
|
|
44
44
|
|
|
45
45
|
function layersIn(start, end) {
|
|
46
46
|
return DATED.filter((x) => x[0] >= start && x[0] <= end).sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0))
|
|
47
|
-
.map(([date, kind, layer, title, tone, ref, initiative], i) => ({ id: 'dated-' + date + '-' + i, date, kind, layer, title, tone, ref, ...(initiative ? { initiative } : {}) }));
|
|
47
|
+
.map(([date, kind, layer, title, tone, ref, initiative, extra], i) => ({ id: 'dated-' + date + '-' + i, date, kind, layer, title, tone, ref, ...(initiative ? { initiative } : {}), ...(extra || {}) }));
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
/* the suggested uses of a free block, from the demo's work items; a block past the first of a day gets none */
|
|
@@ -104,7 +104,8 @@ export function createDock(opts) {
|
|
|
104
104
|
el('div', { class: 'chips2' }, el('b', { style: 'font-size:var(--fs-3)' }, s.name), el('span', { class: 'chips2 dim mono', style: 'font-size:var(--fs-1)' }, c3ic(PI[s.agent] || 'term', 'sm'), (PN[s.agent] || s.agent) + ' · ' + ((t && t.branch) || 'no branch'))),
|
|
105
105
|
el('div', { class: 'dim mono', style: 'font-size:var(--fs-1)' }, facts),
|
|
106
106
|
s.shown === 'needs' ? el('div', { class: 'fold a' }, c3ic('alert', 'sm'), 'needs you') : null,
|
|
107
|
-
el('a', { class: 'chip2 lk', href: '/', 'data-c3pg': 'dock-open-session', title: TIPS.terminalOpen, 'aria-label': 'Open the session ' + s.name
|
|
107
|
+
el('a', { class: 'chip2 lk', href: '/', 'data-c3pg': 'dock-open-session', title: TIPS.terminalOpen, 'aria-label': 'Open the session ' + s.name,
|
|
108
|
+
onclick: (e) => { if (e.metaKey || e.ctrlKey || e.shiftKey || e.button > 0) return; e.preventDefault(); import('./c3-session-term.js').then((m) => m.openSessionTerm({ id: s.id, name: s.name })); } }, ic('term', 'sm'), 'Open the session'),
|
|
108
109
|
term,
|
|
109
110
|
el('div', { class: 'form' },
|
|
110
111
|
el('input', { type: 'text', readonly: true, 'data-off': '', placeholder: s.shown === 'needs' ? 'Your answer' : 'Message', 'aria-label': 'Message', title: MORE, 'data-c3pg': 'dock-msg' }),
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/* pm/c3-session-term.js — a session's real terminal inside console3, in a sheet, so "Open the session" no longer leaves /c3.html.
|
|
2
|
+
* openSessionTerm({ id, name })
|
|
3
|
+
* The terminal is tab-term.js's own (the one the main app draws): the same socket, attach, resize and input. Closing the sheet detaches the
|
|
4
|
+
* pane and leaves the session running, as closing a tab in the main app does. xterm and its addons are the main app's vendored globals,
|
|
5
|
+
* loaded here on the first open because /c3.html does not load them up front. */
|
|
6
|
+
import { el } from './dom.js';
|
|
7
|
+
import { openSheet } from './c3-sheet.js';
|
|
8
|
+
import { toast } from './c3-toast.js';
|
|
9
|
+
import { pmGet } from './http.js';
|
|
10
|
+
|
|
11
|
+
const VENDOR = ['/vendor/xterm.js', '/vendor/addon-fit.js', '/vendor/addon-web-links.js'];
|
|
12
|
+
let vendor = null;
|
|
13
|
+
|
|
14
|
+
function script(src) {
|
|
15
|
+
return new Promise((resolve) => {
|
|
16
|
+
if (document.querySelector('script[src="' + src + '"]')) { resolve(true); return; }
|
|
17
|
+
const s = document.createElement('script');
|
|
18
|
+
s.src = src; s.onload = () => resolve(true); s.onerror = () => resolve(false);
|
|
19
|
+
document.head.append(s);
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
function loadVendor() {
|
|
23
|
+
if (typeof window.Terminal === 'function') return Promise.resolve(true);
|
|
24
|
+
if (!vendor) {
|
|
25
|
+
if (!document.querySelector('link[href="/vendor/xterm.css"]')) document.head.append(el('link', { rel: 'stylesheet', href: '/vendor/xterm.css' }));
|
|
26
|
+
// the addons read window.Terminal when they run, so xterm goes first
|
|
27
|
+
vendor = script(VENDOR[0]).then((ok) => (ok ? Promise.all(VENDOR.slice(1).map(script)).then(() => true) : false));
|
|
28
|
+
}
|
|
29
|
+
return vendor;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function openSessionTerm({ id, name } = {}) {
|
|
33
|
+
if (!id) return null;
|
|
34
|
+
if (!name) { const r = await pmGet('/api/sessions').catch(() => null); const x = r && Array.isArray(r.sessions) ? r.sessions.find((y) => y.id === id) : null; name = x ? x.name : id; }
|
|
35
|
+
const [ok, term] = await Promise.all([loadVendor(), import('./tab-term.js').catch(() => null)]);
|
|
36
|
+
if (!ok || !term) { toast({ text: 'The terminal could not be loaded on this page.', tone: 'error' }); return null; }
|
|
37
|
+
const tab = { id: 'c3-session:' + id, kind: 'term', sessionId: id, spawn: false, sess: id };
|
|
38
|
+
const host = el('div', { class: 'c3t-term', 'data-c3-session-term': id });
|
|
39
|
+
// Esc and Tab belong to the shell while the terminal has focus (Esc interrupts an agent); the close button and a press outside still close
|
|
40
|
+
const sheet = openSheet({ title: name || id, icon: 'term', wide: true, body: host, onClose: () => term.disposeTerm(tab), passKeys: (e) => host.contains(e.target) });
|
|
41
|
+
term.mountTerm(host, tab);
|
|
42
|
+
requestAnimationFrame(() => { term.refitVisible(); term.focusTerm(tab); });
|
|
43
|
+
return sheet;
|
|
44
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* pm/c3-sheet.js — the console3 sheet, as a primitive (console3.html: bsHtml line 2502, the .sheet rules 143 to 145, 180, 496 to 502,
|
|
2
2
|
* the Esc handler 2536 and the phone drag 2538 to 2540).
|
|
3
3
|
*
|
|
4
|
-
* openSheet({ title, icon, body, wide, dw, scrim, onClose }) -> { close, root, sheet, body }
|
|
4
|
+
* openSheet({ title, icon, body, wide, dw, scrim, onClose, passKeys }) -> { close, root, sheet, body } passKeys(e): true leaves that key to the page
|
|
5
5
|
* closeSheet() closes the open sheet, if any
|
|
6
6
|
*
|
|
7
7
|
* On a desktop window the sheet is the right-hand panel (340px; `wide` 600px; `dw` the document sheet, up to 780px) over a
|
|
@@ -50,7 +50,7 @@ export function closeSheet() {
|
|
|
50
50
|
if (typeof c.onClose === 'function') { try { c.onClose(); } catch { /* the caller reports its own errors */ } }
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
export function openSheet({ title = '', icon = 'doc', body, wide = false, dw = false, scrim = true, onClose } = {}) {
|
|
53
|
+
export function openSheet({ title = '', icon = 'doc', body, wide = false, dw = false, scrim = true, onClose, passKeys } = {}) {
|
|
54
54
|
closeSheet();
|
|
55
55
|
ensureC3Css();
|
|
56
56
|
const opener = document.activeElement && document.activeElement !== document.body ? document.activeElement : null;
|
|
@@ -91,6 +91,7 @@ export function openSheet({ title = '', icon = 'doc', body, wide = false, dw = f
|
|
|
91
91
|
sheet.addEventListener('pointercancel', () => { if (drag) { drag = null; sheet.classList.remove('dragging'); sheet.style.transform = ''; } });
|
|
92
92
|
|
|
93
93
|
const onKey = (e) => {
|
|
94
|
+
if (typeof passKeys === 'function' && passKeys(e)) return; // a terminal in the sheet owns Esc and Tab while it has focus
|
|
94
95
|
if (e.key === 'Escape') { e.stopPropagation(); e.preventDefault(); close(); return; }
|
|
95
96
|
if (e.key !== 'Tab') return;
|
|
96
97
|
const items = [...sheet.querySelectorAll(FOCUSABLE)].filter((n) => n.offsetWidth > 0 || n.offsetHeight > 0 || n === document.activeElement);
|
|
@@ -424,7 +424,8 @@ function dock() {
|
|
|
424
424
|
onCount: (live, need) => { S.termBdg.textContent = String(need); S.termBdg.hidden = need === 0; import('./c3-terminal-icons.js').then((m) => { const t = m.TIPS.agentHeader(need); S.termBtn.title = t; S.termBtn.setAttribute('aria-label', t); }); }, // the badge counts runs that need you, never running ones
|
|
425
425
|
});
|
|
426
426
|
S.dockEl = S.dock.side; S.rail = S.dock.rail;
|
|
427
|
-
|
|
427
|
+
// a session opens in its own terminal sheet on this page (c3-session-term.js), not in the main app
|
|
428
|
+
window.addEventListener('pmos:open-session', (e) => { const id = e.detail && e.detail.id; if (!id) { location.assign('/'); return; } import('./c3-session-term.js').then((m) => m.openSessionTerm({ id, name: e.detail.name })); });
|
|
428
429
|
return [S.dock.side, S.dock.rail];
|
|
429
430
|
}
|
|
430
431
|
/* The mockup opens the dock on the Briefing and folds it to a rail on the other tabs (console3.html line 2017). */
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* needsYou(runs) is the one count for the needs-you badge: a run that asks, holds a permission, needs the terminal or did not start. */
|
|
8
8
|
import { pmGet, pmPost } from './http.js';
|
|
9
9
|
|
|
10
|
-
export const WHY_PROMPT = 'Sending a prompt to a session is not available on this page: no safe route sends one.
|
|
10
|
+
export const WHY_PROMPT = 'Sending a prompt to a session is not available on this page: no safe route sends one. To type to the session, press Open the session.';
|
|
11
11
|
|
|
12
12
|
const real = {
|
|
13
13
|
demo: false, canPrompt: false, why: WHY_PROMPT,
|
|
@@ -15,7 +15,7 @@ export const TIPS = {
|
|
|
15
15
|
agentHeader: (n) => (n ? 'Agent surfaces: ' + n + (n === 1 ? ' session needs you' : ' sessions need you') : 'Agent surfaces: sessions, results to accept, questions for you'),
|
|
16
16
|
agentDock: 'Agent surfaces: the full page of sessions that need you',
|
|
17
17
|
terminalRail: 'Sessions dock (Ctrl J): the terminals of your branches',
|
|
18
|
-
terminalOpen: 'Open the session: its real terminal,
|
|
18
|
+
terminalOpen: 'Open the session: its real terminal, on this page',
|
|
19
19
|
};
|
|
20
20
|
|
|
21
21
|
const NS = 'http://www.w3.org/2000/svg';
|
|
@@ -628,10 +628,32 @@
|
|
|
628
628
|
.c3 .cal3-counts{display:flex;gap:4px;flex-wrap:wrap}
|
|
629
629
|
.c3 .cal3-year{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:8px}
|
|
630
630
|
.c3 .cal3-ym{border:1px solid var(--border);border-radius:8px;padding:8px;display:grid;gap:6px;align-content:start;min-width:0}
|
|
631
|
+
/* calendar-highlights: the range summary above the grid, a highlight per Month day, per Quarter month and per Year quarter */
|
|
632
|
+
.c3 .cal3-sum{display:grid;gap:4px;margin:0 0 12px;padding:8px 12px;border-left:3px solid var(--acc);border-radius:8px;background:var(--muted)}
|
|
633
|
+
.c3 .cal3-sum-h{font:var(--fs-1) var(--mono);color:var(--muted-foreground);text-transform:uppercase;letter-spacing:.06em}
|
|
634
|
+
.c3 .cal3-sum p{margin:0;font-size:var(--fs-3);line-height:1.55;color:var(--foreground)}
|
|
635
|
+
.c3 .cal3-hl{margin:2px 2px 0;font-size:var(--fs-1);line-height:1.45;color:var(--foreground);overflow-wrap:anywhere}
|
|
636
|
+
.c3 .cal3-col .cal3-hl{font-size:var(--fs-2);line-height:1.55}
|
|
637
|
+
.c3 .cal3 button.cal3-sl{display:inline;height:auto;min-height:0;padding:0;border:0;background:none;font:inherit;font-weight:600;line-height:inherit;color:inherit;white-space:normal;text-align:left;cursor:pointer;text-decoration:underline;text-decoration-color:var(--border);text-underline-offset:3px}
|
|
638
|
+
@media (hover:hover){.c3 .cal3 button.cal3-sl:hover{text-decoration-color:currentColor}}
|
|
639
|
+
.c3 .cal3 button.cal3-sl:focus-visible{outline:2px solid var(--ring);outline-offset:2px}
|
|
640
|
+
.c3 .cal3-watch{color:var(--destructive)}
|
|
641
|
+
.c3 .cal3 button.cal3-more.cal3-rt{justify-content:flex-start;margin-top:auto;text-align:left}
|
|
642
|
+
.c3 .cal3-cols{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px}
|
|
643
|
+
.c3 .cal3-cols.cal3-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}
|
|
644
|
+
.c3 .cal3-col{padding:10px;gap:8px;min-height:160px}
|
|
645
|
+
.c3 .cal3-mlinks{display:flex;gap:4px;flex-wrap:wrap}
|
|
646
|
+
.c3 .cal3-fold{margin:8px 4px 0;font-size:var(--fs-2)}
|
|
647
|
+
/* titles wrap instead of being cut, in Month, Quarter and Year */
|
|
648
|
+
.c3 .cal3-mc button.cal3-mk,.c3 .cal3-qmk button.cal3-mk{white-space:normal;align-items:flex-start}
|
|
649
|
+
.c3 .cal3-mc button.cal3-mk>span,.c3 .cal3-qmk button.cal3-mk>span{align-items:flex-start;overflow:visible}
|
|
650
|
+
.c3 .cal3-mc .cal3-mk .cal3-mk-t,.c3 .cal3-qmk .cal3-mk .cal3-mk-t{overflow:visible;text-overflow:clip;overflow-wrap:anywhere}
|
|
631
651
|
@container (max-width:640px){
|
|
632
652
|
.c3 .cal3-mk.compact .cal3-mk-t{display:none}
|
|
633
653
|
.c3 .cal3-mc{min-height:64px;padding:2px}
|
|
634
654
|
.c3 .cal3-qrow{grid-template-columns:minmax(0,1fr)}
|
|
655
|
+
.c3 .cal3-cols,.c3 .cal3-cols.cal3-cols-4{grid-template-columns:minmax(0,1fr)}
|
|
656
|
+
.c3 .cal3-col{min-height:0}
|
|
635
657
|
}
|
|
636
658
|
/* the detail column */
|
|
637
659
|
.c3 .cal3-dt{display:flex;align-items:center;gap:8px}
|
|
@@ -1628,6 +1650,8 @@
|
|
|
1628
1650
|
.c3 .c3t-sw[aria-checked="true"] i{background:var(--acc)}.c3 .c3t-sw[aria-checked="true"] i::after{transform:translateX(16px)}
|
|
1629
1651
|
.c3 .c3t-sw:focus-visible{outline:2px solid var(--ring);outline-offset:2px}
|
|
1630
1652
|
.c3 .c3t-panel{display:grid;gap:var(--sp-2);border:1px solid var(--border);border-radius:10px;padding:var(--sp-3)}
|
|
1653
|
+
/* a session's terminal in a sheet (c3-session-term.js) */
|
|
1654
|
+
.c3 .c3t-term{height:calc(100vh - 150px);min-height:240px;display:flex;flex-direction:column}
|
|
1631
1655
|
.c3 .c3t-panel .feed{display:flex;flex-direction:column;gap:var(--sp-2);max-height:220px;overflow:auto}
|
|
1632
1656
|
.c3 .c3t-panel .m{padding:6px 12px;border-radius:12px;max-width:88%;overflow-wrap:anywhere}
|
|
1633
1657
|
.c3 .c3t-panel .m.me{align-self:flex-end;background:var(--acc);color:var(--on-accent)}.c3 .c3t-panel .m.bot{background:var(--muted)}
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* would send a prompt to the session (a suggestion, a revision, the free line, the mic) is drawn disabled with the reason: ctx.src.canPrompt.
|
|
13
13
|
* Per-run UI state (the open tray, the Revise chips, the flipped face) lives in `UI`, outside the render, so a poll never closes it. */
|
|
14
14
|
import { el } from './dom.js';
|
|
15
|
+
import { button } from './primitives.js';
|
|
15
16
|
import { ic } from './c3-terminal-icons.js';
|
|
16
17
|
import { ensureC3Css } from './pm-work-view.js';
|
|
17
18
|
import { attachSwipe } from './c3-swipe.js';
|
|
@@ -100,7 +101,9 @@ function pausedBox(ctx, card) {
|
|
|
100
101
|
el('div', { class: 'prog', 'aria-hidden': 'true' }, el('i', { class: 'd' }), el('i', { class: 'd' }), el('i'), el('i')), el('div', { class: 'c3t-tray' }, openBtn(ctx)));
|
|
101
102
|
}
|
|
102
103
|
export function openBtn(ctx) {
|
|
103
|
-
|
|
104
|
+
const b = button({ label: 'Open the session', onClick: () => ctx.openSession && ctx.openSession(ctx.id), attrs: { class: 'pill c3t-open', 'data-open-session': '', title: 'Open the session: its real terminal, on this page' } });
|
|
105
|
+
b.prepend(ic('term', 'sm'));
|
|
106
|
+
return b;
|
|
104
107
|
}
|
|
105
108
|
|
|
106
109
|
/* ── the next moves, the sub-chips, the tray, the voice line ── */
|
|
@@ -13,6 +13,8 @@ import { mountRunCard, disposeRunCard, openBtn, uiFor } from './run-card.js';
|
|
|
13
13
|
import { source, needsYou } from './c3-terminal-data.js';
|
|
14
14
|
import { attachExpand, makeXp } from './c3-expand.js';
|
|
15
15
|
import { toast } from './c3-toast.js';
|
|
16
|
+
import { pmPost } from './http.js';
|
|
17
|
+
import { button } from './primitives.js';
|
|
16
18
|
|
|
17
19
|
const live = new WeakMap();
|
|
18
20
|
const DOT = { working: 'w', queued: 'w', asking: 'n', permission: 'n', attention: 'n', not_started: 'n', failed: 'b', quiet: '', paused: 'n', done: '' };
|
|
@@ -20,6 +22,10 @@ const DOT = { working: 'w', queued: 'w', asking: 'n', permission: 'n', attention
|
|
|
20
22
|
const ago = (iso) => { const m = Math.max(0, Math.round((Date.now() - Date.parse(iso)) / 60000)); return Number.isFinite(m) ? (m < 1 ? 'just now' : m < 60 ? m + ' min' : m < 1440 ? Math.round(m / 60) + ' h' : Math.round(m / 1440) + ' d') : ''; };
|
|
21
23
|
const when = (iso) => { const d = new Date(iso); return Number.isFinite(+d) ? d.toLocaleDateString('en-GB', { weekday: 'short', day: 'numeric', month: 'short' }) : ''; };
|
|
22
24
|
const stateWord = (c) => (c.permissions && c.permissions.length ? 'waiting for your allow or deny' : ({ asking: 'asking you', permission: 'waiting for your allow or deny', working: 'working', queued: 'starting', quiet: 'quiet', failed: 'failed', done: 'result ready', attention: 'needs the terminal', not_started: 'not started', paused: 'paused', degraded: 'plain text only' })[c.state] || c.state);
|
|
25
|
+
/* A run with no reply lines (a plain terminal session) folds to 'queued', which read as "starting" for days. The server's own state of the
|
|
26
|
+
* session (GET /api/runs carries it as `label`) says what it is doing instead. */
|
|
27
|
+
const plain = (s, c) => (!c || (c.state === 'queued' && !(c.steps || []).length && !(c.permissions || []).length)) && !!s.label;
|
|
28
|
+
const liveWord = (s, c) => (plain(s, c) ? s.label : stateWord(c || { state: 'working' }));
|
|
23
29
|
function outcomeOf(card, meta) {
|
|
24
30
|
if (meta && meta.out) return meta.out;
|
|
25
31
|
if (!card) return 'ended';
|
|
@@ -64,10 +70,37 @@ function blankPanel(st, s, card) {
|
|
|
64
70
|
return el('div', { class: 'c3t-panel', 'data-blank-panel': s.id },
|
|
65
71
|
can ? el('div', { class: 'feed' }, chat.map((m) => el('div', { class: 'm ' + m[0] }, m[1]))) : null,
|
|
66
72
|
can ? el('div', { class: 'c3t-tray' }, ["Review this week's slips", 'Draft the monthly report'].map((t, i) => el('button', { type: 'button', class: 'pill' + (i ? '' : ' rec'), onclick: () => say(t) }, t))) : null,
|
|
67
|
-
el('div', { class: 'c3t-tray' }, input, openBtn({ id: s.id, openSession: st.openSession })),
|
|
73
|
+
el('div', { class: 'c3t-tray' }, input, openBtn({ id: s.id, openSession: st.openSession }), endBtn(st, s)),
|
|
74
|
+
st.confirm === s.id ? endConfirm(st, s) : null,
|
|
68
75
|
can ? null : dim(st.src.why));
|
|
69
76
|
}
|
|
70
77
|
|
|
78
|
+
/* End: the route the dock's End uses, POST /api/session/remove (it ends the process and removes the session). Asked first, as the dock asks. */
|
|
79
|
+
/* a primitives.js button with the console3 pill look (as c3-swipe.js's confirm): the icon goes before the label */
|
|
80
|
+
function pill(label, icon, onClick, attrs) {
|
|
81
|
+
const b = button({ label, onClick, attrs: { ...attrs, class: 'pill' + (attrs && attrs.class ? ' ' + attrs.class : '') } });
|
|
82
|
+
if (icon) b.prepend(ic(icon, 'sm'));
|
|
83
|
+
return b;
|
|
84
|
+
}
|
|
85
|
+
function endBtn(st, s) {
|
|
86
|
+
return pill('End the session', 'stop', () => { st.confirm = st.confirm === s.id ? null : s.id; st.redraw(); },
|
|
87
|
+
{ 'data-end-session': s.id, 'aria-expanded': String(st.confirm === s.id), title: 'End the session' });
|
|
88
|
+
}
|
|
89
|
+
function endConfirm(st, s) {
|
|
90
|
+
return el('div', { class: 'c3t-tray', role: 'group', 'aria-label': 'End this session?', 'data-end-confirm': s.id },
|
|
91
|
+
dim('End it? Any pending change stays a proposal.'),
|
|
92
|
+
pill('End', 'stop', () => endSession(st, s), { class: 'rec', 'data-end-yes': s.id }),
|
|
93
|
+
pill('Keep it', null, () => { st.confirm = null; st.redraw(); }, {}));
|
|
94
|
+
}
|
|
95
|
+
async function endSession(st, s) {
|
|
96
|
+
st.confirm = null;
|
|
97
|
+
if (st.src.demo) { toast('In the real workspace this ends the session.'); st.redraw(); return; }
|
|
98
|
+
const r = await pmPost('/api/session/remove', { id: s.id });
|
|
99
|
+
if (r && r.ok) toast({ text: r.removed === false ? 'Asked it to end. Press End again to remove it.' : 'Ended. Any pending change stays a proposal.', timeout: 4200 });
|
|
100
|
+
else toast({ text: 'Not ended: ' + ((r && r.error) || 'the workspace server did not answer'), tone: 'error' });
|
|
101
|
+
st.last = ''; st.redraw(); st.refresh();
|
|
102
|
+
}
|
|
103
|
+
|
|
71
104
|
function tlBody(st, s, card, meta) {
|
|
72
105
|
const tl = st.data.demo && st.data.demo.timeline;
|
|
73
106
|
const kids = [];
|
|
@@ -92,13 +125,14 @@ function draw(host, st) {
|
|
|
92
125
|
/* On a card: each run is its card */
|
|
93
126
|
const on = g.onCard || [];
|
|
94
127
|
root.append(section('spark', 'On a card', on.length, null, on.length
|
|
95
|
-
? el('div', { class: 'c3t-cards', 'data-c3t': 'on-card' }, on.map((s) => cardHost(st, s, cards[s.id] || { state: 'working', title: 'Working', nextMoves: [], claims: [], steps: [] })
|
|
128
|
+
? el('div', { class: 'c3t-cards', 'data-c3t': 'on-card' }, on.map((s) => el('div', null, cardHost(st, s, cards[s.id] || { state: 'working', title: 'Working', nextMoves: [], claims: [], steps: [] }),
|
|
129
|
+
el('div', { class: 'c3t-tray' }, endBtn(st, s)), st.confirm === s.id ? endConfirm(st, s) : null)))
|
|
96
130
|
: none('None running. A result appears on its card.')));
|
|
97
131
|
/* Started by me */
|
|
98
132
|
const mine = g.startedByMe || [];
|
|
99
133
|
const add = el('a', { class: 'chip2 lk c3t-new', href: '/', title: 'Start a session in the main app. A session starts over the terminal connection; this page has no route for it.' }, ic('plus', 'sm'), 'New');
|
|
100
134
|
root.append(section('term', 'Started by me', mine.length, add, mine.length
|
|
101
|
-
? el('ul', { class: 'list', 'data-c3t': 'mine' }, mine.map((s) => { const c = cards[s.id]; return row(st, s, c, { sub:
|
|
135
|
+
? el('ul', { class: 'list', 'data-c3t': 'mine' }, mine.map((s) => { const c = cards[s.id]; return row(st, s, c, { sub: liveWord(s, c) + ' · ' + ago(s.startedAt) + ' · main tree', body: [plain(s, c) ? null : cardHost(st, s, c || { state: 'working', title: 'Working', nextMoves: [], claims: [], steps: [] }), blankPanel(st, s, c)].filter(Boolean) }); }))
|
|
102
136
|
: none('None. Start one in the main app.')));
|
|
103
137
|
/* Past */
|
|
104
138
|
const past = g.past || [];
|
|
@@ -120,7 +154,9 @@ export function mountSessions(host, ctx = {}) {
|
|
|
120
154
|
disposeSessions(host);
|
|
121
155
|
if (ctx && ctx.FIXTURE) return;
|
|
122
156
|
ensureC3Css();
|
|
123
|
-
const st = { src: source(), data: null, showAuto: false, last: '', gone: false, timer: null, refresh: null, redraw: null, open: new Set(),
|
|
157
|
+
const st = { src: source(), data: null, showAuto: false, last: '', gone: false, timer: null, refresh: null, redraw: null, open: new Set(), confirm: null,
|
|
158
|
+
openSession: (id) => { const r = st.data && st.data.groups ? [].concat(...Object.values(st.data.groups).filter(Array.isArray)).find((x) => x.id === id) : null;
|
|
159
|
+
import('./c3-session-term.js').then((m) => m.openSessionTerm({ id, name: r ? r.name : id })); } };
|
|
124
160
|
if (st.src.demo) st.openSession = () => toast('In the real workspace this opens the CLI session.');
|
|
125
161
|
st.redraw = () => draw(host, st);
|
|
126
162
|
const busy = () => host.contains(document.activeElement) && ['INPUT', 'TEXTAREA'].includes(document.activeElement.tagName);
|
|
@@ -18,7 +18,7 @@ This skill runs before the bi-weekly report (`report-biweekly`). The report scri
|
|
|
18
18
|
Rules that always hold (D71):
|
|
19
19
|
1. LOCAL FILES ONLY. Read files in this repository. Never call a tracker, a connector, a URL or any network tool. Never run `curl`, `wget`, `git fetch` or `git pull`. A tracker that no local file describes is "not reached".
|
|
20
20
|
2. READ-ONLY on every source. Never send, reply, forward, post, comment, create, edit, transition, label, archive or delete anything in Jira, Confluence, mail, chat, calendar or any other source.
|
|
21
|
-
3. THE ONLY WRITE is `pmos item move
|
|
21
|
+
3. THE ONLY WRITE is `pmos item move`, plus `pmos tracker done` as the last step, which writes no ledger line. Never edit a repository file. Never run `pmos item add`, `set` or `withdraw`. Never write a ledger by hand. Never commit; the PM's Sync carries the ledger.
|
|
22
22
|
4. EVIDENCE FOR EVERY MOVE. Each move carries `--evidence file:<repository path>`: the local file that shows the tracker state. If you have no file to point at, do not move the item.
|
|
23
23
|
5. NOTHING INVENTED. Move an item only to the state a file says. If the files disagree, use the newest one and say so. If no file says, leave the item and list it under "Not found".
|
|
24
24
|
6. LIGHT ON CONTENT. Read titles, ids and statuses. Never copy a message body, an attachment, a token or a credential. Skip a file marked private, personal, confidential or HR, and count it.
|
|
@@ -43,6 +43,7 @@ Procedure:
|
|
|
43
43
|
6. If the mapped state is the item's state, leave the item. Write nothing. List it under "Left as they are" with the file and its date.
|
|
44
44
|
7. If the mapped state is different, run `pmos item move <item> <state> --evidence file:<path of the file>`. If the command is refused, read the reason and do not work around it. List the item under "Not found" with the reason.
|
|
45
45
|
8. Finish with the four sections of the output contract, as short lines: one line for each item, with its id, the old and new state and the file. Under "Sources reached and not reached", give each tracker reference as reached (the file and its date) or not reached. If you moved nothing, say so in one line: a refresh that moves nothing writes no line, so the bi-weekly report can still list the tracker as not refreshed. Do not commit.
|
|
46
|
+
9. Run `pmos tracker done --moved <n> --left <n> --not-found <n>` with the counts of the three sections. It writes no ledger line; it records in your runtime folder that this read finished, so the freshness line shows the time. Run it even when you moved nothing.
|
|
46
47
|
|
|
47
48
|
Output contract — produce EXACTLY these sections, the same shape every run:
|
|
48
49
|
- Moved
|