create-pmos 0.146.0 → 0.148.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/console-calendar.md +31 -7
- package/workspace/guide/console3-sessions.md +35 -0
- package/workspace/guide/index.md +1 -0
- package/workspace/guide/notes.md +1 -1
- package/workspace/lib/calendar-events.js +11 -1
- package/workspace/lib/calendar-layers.js +95 -5
- package/workspace/lib/routes-calendar-views.js +9 -4
- package/workspace/lib/routes-runs.js +12 -4
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/c3-calendar.js +225 -32
- package/workspace/public/pm/c3-demo-cal4.js +18 -4
- package/workspace/public/pm/c3-demo-data.js +2 -2
- package/workspace/public/pm/c3-dock.js +2 -1
- package/workspace/public/pm/c3-notes.js +47 -9
- 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 +31 -0
- package/workspace/public/pm/note-dock.js +5 -8
- package/workspace/public/pm/run-card.js +4 -1
- package/workspace/public/pm/sessions-panel.js +40 -4
- package/workspace/test-files.json +2 -0
- package/workspace/test-lists/c3-meeting-card.json +5 -0
|
@@ -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';
|
|
@@ -529,6 +529,9 @@
|
|
|
529
529
|
.c3 .cal3-ov{margin:0}
|
|
530
530
|
.c3 .cal3-tdr{text-align:right}
|
|
531
531
|
.c3 .cal3-ncard{display:grid;gap:var(--sp-2)}
|
|
532
|
+
.c3 .cal3-nlinks{display:grid;gap:var(--sp-1);margin-top:var(--sp-1)}
|
|
533
|
+
.c3 .cal3-replay{display:grid;gap:var(--sp-1)}
|
|
534
|
+
.c3 .cal3-replay audio{width:100%;max-width:100%}
|
|
532
535
|
/* a primitives.js button drawn as a console3 control: the primitive's own height, padding, nowrap and hover come off */
|
|
533
536
|
.c3 .cal3-b{height:auto;min-height:0;line-height:inherit;font-weight:inherit;justify-content:flex-start;text-align:left}
|
|
534
537
|
.c3 .cal3-b.chip2{padding:0;border:0;background:transparent;white-space:nowrap}
|
|
@@ -628,10 +631,32 @@
|
|
|
628
631
|
.c3 .cal3-counts{display:flex;gap:4px;flex-wrap:wrap}
|
|
629
632
|
.c3 .cal3-year{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:8px}
|
|
630
633
|
.c3 .cal3-ym{border:1px solid var(--border);border-radius:8px;padding:8px;display:grid;gap:6px;align-content:start;min-width:0}
|
|
634
|
+
/* calendar-highlights: the range summary above the grid, a highlight per Month day, per Quarter month and per Year quarter */
|
|
635
|
+
.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)}
|
|
636
|
+
.c3 .cal3-sum-h{font:var(--fs-1) var(--mono);color:var(--muted-foreground);text-transform:uppercase;letter-spacing:.06em}
|
|
637
|
+
.c3 .cal3-sum p{margin:0;font-size:var(--fs-3);line-height:1.55;color:var(--foreground)}
|
|
638
|
+
.c3 .cal3-hl{margin:2px 2px 0;font-size:var(--fs-1);line-height:1.45;color:var(--foreground);overflow-wrap:anywhere}
|
|
639
|
+
.c3 .cal3-col .cal3-hl{font-size:var(--fs-2);line-height:1.55}
|
|
640
|
+
.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}
|
|
641
|
+
@media (hover:hover){.c3 .cal3 button.cal3-sl:hover{text-decoration-color:currentColor}}
|
|
642
|
+
.c3 .cal3 button.cal3-sl:focus-visible{outline:2px solid var(--ring);outline-offset:2px}
|
|
643
|
+
.c3 .cal3-watch{color:var(--destructive)}
|
|
644
|
+
.c3 .cal3 button.cal3-more.cal3-rt{justify-content:flex-start;margin-top:auto;text-align:left}
|
|
645
|
+
.c3 .cal3-cols{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px}
|
|
646
|
+
.c3 .cal3-cols.cal3-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}
|
|
647
|
+
.c3 .cal3-col{padding:10px;gap:8px;min-height:160px}
|
|
648
|
+
.c3 .cal3-mlinks{display:flex;gap:4px;flex-wrap:wrap}
|
|
649
|
+
.c3 .cal3-fold{margin:8px 4px 0;font-size:var(--fs-2)}
|
|
650
|
+
/* titles wrap instead of being cut, in Month, Quarter and Year */
|
|
651
|
+
.c3 .cal3-mc button.cal3-mk,.c3 .cal3-qmk button.cal3-mk{white-space:normal;align-items:flex-start}
|
|
652
|
+
.c3 .cal3-mc button.cal3-mk>span,.c3 .cal3-qmk button.cal3-mk>span{align-items:flex-start;overflow:visible}
|
|
653
|
+
.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
654
|
@container (max-width:640px){
|
|
632
655
|
.c3 .cal3-mk.compact .cal3-mk-t{display:none}
|
|
633
656
|
.c3 .cal3-mc{min-height:64px;padding:2px}
|
|
634
657
|
.c3 .cal3-qrow{grid-template-columns:minmax(0,1fr)}
|
|
658
|
+
.c3 .cal3-cols,.c3 .cal3-cols.cal3-cols-4{grid-template-columns:minmax(0,1fr)}
|
|
659
|
+
.c3 .cal3-col{min-height:0}
|
|
635
660
|
}
|
|
636
661
|
/* the detail column */
|
|
637
662
|
.c3 .cal3-dt{display:flex;align-items:center;gap:8px}
|
|
@@ -1628,6 +1653,8 @@
|
|
|
1628
1653
|
.c3 .c3t-sw[aria-checked="true"] i{background:var(--acc)}.c3 .c3t-sw[aria-checked="true"] i::after{transform:translateX(16px)}
|
|
1629
1654
|
.c3 .c3t-sw:focus-visible{outline:2px solid var(--ring);outline-offset:2px}
|
|
1630
1655
|
.c3 .c3t-panel{display:grid;gap:var(--sp-2);border:1px solid var(--border);border-radius:10px;padding:var(--sp-3)}
|
|
1656
|
+
/* a session's terminal in a sheet (c3-session-term.js) */
|
|
1657
|
+
.c3 .c3t-term{height:calc(100vh - 150px);min-height:240px;display:flex;flex-direction:column}
|
|
1631
1658
|
.c3 .c3t-panel .feed{display:flex;flex-direction:column;gap:var(--sp-2);max-height:220px;overflow:auto}
|
|
1632
1659
|
.c3 .c3t-panel .m{padding:6px 12px;border-radius:12px;max-width:88%;overflow-wrap:anywhere}
|
|
1633
1660
|
.c3 .c3t-panel .m.me{align-self:flex-end;background:var(--acc);color:var(--on-accent)}.c3 .c3t-panel .m.bot{background:var(--muted)}
|
|
@@ -1876,3 +1903,7 @@
|
|
|
1876
1903
|
.c3 .kp.k-foreground{--c:var(--foreground)}
|
|
1877
1904
|
.c3 .kp.k-muted-foreground{--c:var(--muted-foreground)}
|
|
1878
1905
|
.c3 .psess{display:inline-flex;gap:6px;align-items:center}
|
|
1906
|
+
/* the card before a meeting (initiative c3-meeting-card) */
|
|
1907
|
+
.c3 .cal3-ctx .cal3-p{margin:0;font-size:var(--fs-2);line-height:1.5;color:var(--foreground)}
|
|
1908
|
+
.c3 .cal3-ctx.cal3-gist{padding:10px 12px;border:1px solid var(--border);border-radius:8px;background:var(--muted)}
|
|
1909
|
+
.c3 .cal3-ctx h4{display:flex;align-items:center;gap:6px}
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
*
|
|
9
9
|
* PAUSE keeps the meeting open and resumable for an hour (the server ends it by itself after that and writes the final
|
|
10
10
|
* notes). STOP ends it for good, asks first, and writes the final notes now. A stopped meeting cannot be resumed. A note
|
|
11
|
-
* that is stopped
|
|
12
|
-
*
|
|
11
|
+
* that is stopped before anything was heard, with nothing typed, is deleted instead (a recording started by mistake must not
|
|
12
|
+
* take the place of the meeting before it). A short note with speech in it is kept. Resume applies to the most recent meeting only.
|
|
13
13
|
*
|
|
14
14
|
* What is held is read, not remembered: the listening note comes from pm/view-notes.js, and a paused meeting from the note
|
|
15
15
|
* rows (GET /api/notes: state, resume_until, sessions), read again every REREAD_MS and after each action.
|
|
@@ -28,7 +28,6 @@ import { state, setState, subscribe, FIXTURE } from './state.js';
|
|
|
28
28
|
import { meetingOf, refreshNotes, noteById, notesRows, onNotes } from './notes-links.js';
|
|
29
29
|
|
|
30
30
|
const REREAD_MS = 30000;
|
|
31
|
-
const MIN = 60000;
|
|
32
31
|
let V = null; // pm/view-notes.js, read the first time it is needed
|
|
33
32
|
let mounted = false;
|
|
34
33
|
const viewCtx = { state, setState, FIXTURE };
|
|
@@ -163,14 +162,12 @@ export async function resumeNow() {
|
|
|
163
162
|
return ok;
|
|
164
163
|
}
|
|
165
164
|
|
|
166
|
-
/** Is this note one that was started by mistake?
|
|
167
|
-
export function discardable(n
|
|
165
|
+
/** Is this note one that was started by mistake? Nothing heard, nothing typed, no highlight, one stretch. A short note with speech is kept. Pure. */
|
|
166
|
+
export function discardable(n) {
|
|
168
167
|
if (!n || String(n.body || '').trim() || (n.highlights || []).length) return false;
|
|
169
168
|
const s = n.sessions || [];
|
|
170
169
|
if (s.length !== 1) return false;
|
|
171
|
-
|
|
172
|
-
const heard = (n.transcript || []).some((x) => !x.dropped);
|
|
173
|
-
return !heard || to - from < MIN;
|
|
170
|
+
return !(n.transcript || []).some((x) => !x.dropped);
|
|
174
171
|
}
|
|
175
172
|
|
|
176
173
|
/** The "Stop for good?" pop-up. Resolves 'stop', 'pause' (only while recording) or null. */
|
|
@@ -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);
|
|
@@ -136,6 +136,7 @@
|
|
|
136
136
|
"pm-portfolio-inline-test.js",
|
|
137
137
|
"pm-brief-page-test.js",
|
|
138
138
|
"c3-settings-test.js",
|
|
139
|
+
"c3-sessions-panel-test.js",
|
|
139
140
|
"c3-updates-test.js"
|
|
140
141
|
],
|
|
141
142
|
"ui": [
|
|
@@ -269,6 +270,7 @@
|
|
|
269
270
|
"pm-portfolio-inline-test.js": 15,
|
|
270
271
|
"settings-store.test.js": 1,
|
|
271
272
|
"c3-settings-test.js": 25,
|
|
273
|
+
"c3-sessions-panel-test.js": 20,
|
|
272
274
|
"c3-updates-test.js": 20
|
|
273
275
|
},
|
|
274
276
|
"_comment_seconds": "Measured wall time of each suite run alone (4-CPU sandbox, 2026-10-05). Only the ORDER uses it: the slowest suites start first, and --shard deals suites to the least-loaded shard so the shards finish together. A wrong or missing number never skips a suite; a missing one counts as 10."
|