create-pmos 0.54.0 → 0.55.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/index.md +1 -0
- package/workspace/guide/layout.md +1 -0
- package/workspace/guide/proactive-engine.md +72 -9
- package/workspace/guide/today.md +25 -2
- package/workspace/lib/brief.js +244 -8
- package/workspace/lib/day-run.js +297 -0
- package/workspace/lib/goals.js +25 -3
- package/workspace/lib/proactive-cli.js +119 -6
- package/workspace/lib/proactive.js +53 -7
- package/workspace/package-lock.json +1 -1
- package/workspace/package.json +1 -1
- package/workspace/public/pm/app.js +13 -1
- package/workspace/public/pm/branches-sheet.js +56 -0
- package/workspace/public/pm/components.css +134 -23
- package/workspace/public/pm/dest-today.js +10 -8
- package/workspace/public/pm/frame-copy.js +8 -0
- package/workspace/public/pm/today-copy.js +182 -0
- package/workspace/public/pm/today-day.js +760 -0
- package/workspace/public/pm/today-goals.js +96 -161
- package/workspace/replaced-tests.json +10 -0
- package/workspace/server.js +190 -9
- package/workspace/skills/day-check.skill +56 -0
- package/workspace/skills/day-task.skill +47 -0
- package/workspace/skills/morning-brief.skill +45 -10
- package/workspace/test-files.json +2 -0
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
* newSignals(...) which candidates have not been appended to signals.jsonl inside the dedupe window
|
|
11
11
|
* dismissedIds(...) which candidates the PM dismissed within the last seven days
|
|
12
12
|
* validateFeedback(body) the four verdicts, the implicit events, an optional one-line note
|
|
13
|
-
* normalizeSettings(raw) off, per-trigger off, quiet hours, the stall threshold, pause learning
|
|
13
|
+
* normalizeSettings(raw) off, per-trigger off, quiet hours, the stall threshold, pause learning; and for Today's
|
|
14
|
+
* day run the brief sources, the level per channel/project/space (scopes), the daytime
|
|
15
|
+
* check (dayCheck) and the PM's website addresses
|
|
14
16
|
* inQuietHours(now, quiet) whether automatic scans are held
|
|
15
17
|
*
|
|
16
18
|
* A candidate is never a status. Nothing here changes an initiative, a lifecycle line or an acceptance.
|
|
@@ -29,10 +31,20 @@ const DISMISS_MS = 7 * DAY;
|
|
|
29
31
|
const BASE = { needs: 100, returned: 100, review: 80, schedule: 60, stalled: 40 };
|
|
30
32
|
const OWNER = { needs: 'pm', returned: 'pm', review: 'pm', schedule: 'pm', stalled: 'agent' };
|
|
31
33
|
|
|
32
|
-
const BRIEF_SOURCES = ['calendar', 'wispr', 'mail', 'slack', 'jira', 'confluence'];
|
|
33
|
-
// Mail is off by default: a brief reads titles and metadata of the other sources, and the PM turns mail on.
|
|
34
|
-
|
|
35
|
-
const
|
|
34
|
+
const BRIEF_SOURCES = ['calendar', 'wispr', 'mail', 'slack', 'jira', 'confluence', 'github', 'drive', 'figma', 'website'];
|
|
35
|
+
// Mail is off by default: a brief reads titles and metadata of the other sources, and the PM turns mail on. The four
|
|
36
|
+
// sources added for the day run (today-goals G19) are off too: the PM switches each on.
|
|
37
|
+
const BRIEF_DEFAULT = Object.freeze({ calendar: true, wispr: true, mail: false, slack: true, jira: true, confluence: true, github: false, drive: false, figma: false, website: false });
|
|
38
|
+
// G20: a level per Slack channel, Jira project and Confluence space. A place with no level is read as `search`.
|
|
39
|
+
const SCOPE_SERVICES = ['slack', 'jira', 'confluence'];
|
|
40
|
+
const SCOPE_LEVELS = ['watch', 'search', 'ignore'];
|
|
41
|
+
const MAX_SCOPE_NAMES = 50;
|
|
42
|
+
const MAX_WEBSITES = 10;
|
|
43
|
+
// G22: the daytime check — every 30 or 60 minutes, or off (0), between start and end of the working day.
|
|
44
|
+
const DAY_CHECK_EVERY = [0, 30, 60];
|
|
45
|
+
const DAY_CHECK_DEFAULT = Object.freeze({ every: 60, start: '09:00', end: '18:00' });
|
|
46
|
+
const DEFAULTS = Object.freeze({ name: '', briefSources: BRIEF_DEFAULT, briefScoring: false, backgroundPrepare: false, enabled: true, triggers: Object.freeze(Object.fromEntries(TRIGGERS.map((t) => [t, true]))), stalledDays: 5, quiet: null, pauseLearning: false,
|
|
47
|
+
scopes: Object.freeze({ slack: Object.freeze({}), jira: Object.freeze({}), confluence: Object.freeze({}) }), dayCheck: DAY_CHECK_DEFAULT, websites: Object.freeze([]) });
|
|
36
48
|
|
|
37
49
|
const idOf = (key) => crypto.createHash('sha1').update(String(key)).digest('hex').slice(0, 12);
|
|
38
50
|
const clip = (s, n) => String(s == null ? '' : s).replace(/\s+/g, ' ').trim().slice(0, n);
|
|
@@ -165,7 +177,8 @@ const HHMM = /^([01]\d|2[0-3]):([0-5]\d)$/;
|
|
|
165
177
|
/** @returns {{ok:true, settings:object}|{ok:false, error:string}} — `raw` is merged over the defaults; nothing is partly applied. */
|
|
166
178
|
function normalizeSettings(raw) {
|
|
167
179
|
const r = raw && typeof raw === 'object' ? raw : {};
|
|
168
|
-
const s = { name: DEFAULTS.name, briefSources: { ...BRIEF_DEFAULT }, briefScoring: DEFAULTS.briefScoring, backgroundPrepare: DEFAULTS.backgroundPrepare, enabled: DEFAULTS.enabled, triggers: { ...DEFAULTS.triggers }, stalledDays: DEFAULTS.stalledDays, quiet: DEFAULTS.quiet, pauseLearning: DEFAULTS.pauseLearning
|
|
180
|
+
const s = { name: DEFAULTS.name, briefSources: { ...BRIEF_DEFAULT }, briefScoring: DEFAULTS.briefScoring, backgroundPrepare: DEFAULTS.backgroundPrepare, enabled: DEFAULTS.enabled, triggers: { ...DEFAULTS.triggers }, stalledDays: DEFAULTS.stalledDays, quiet: DEFAULTS.quiet, pauseLearning: DEFAULTS.pauseLearning,
|
|
181
|
+
scopes: { slack: {}, jira: {}, confluence: {} }, dayCheck: { ...DAY_CHECK_DEFAULT }, websites: [] };
|
|
169
182
|
if (r.enabled !== undefined) { if (typeof r.enabled !== 'boolean') return { ok: false, error: 'enabled must be true or false' }; s.enabled = r.enabled; }
|
|
170
183
|
if (r.name !== undefined) { if (typeof r.name !== 'string' || r.name.length > 40) return { ok: false, error: 'name must be text of at most 40 characters' }; s.name = r.name.replace(/\s+/g, ' ').trim(); }
|
|
171
184
|
if (r.briefScoring !== undefined) { if (typeof r.briefScoring !== 'boolean') return { ok: false, error: 'briefScoring must be true or false' }; s.briefScoring = r.briefScoring; }
|
|
@@ -198,6 +211,39 @@ function normalizeSettings(raw) {
|
|
|
198
211
|
return { ok: false, error: 'quiet must be null, or {start, end} as HH:MM (24-hour, local), different from each other' };
|
|
199
212
|
} else s.quiet = { start: r.quiet.start, end: r.quiet.end };
|
|
200
213
|
}
|
|
214
|
+
if (r.scopes !== undefined) {
|
|
215
|
+
if (!r.scopes || typeof r.scopes !== 'object' || Array.isArray(r.scopes)) return { ok: false, error: `scopes must be an object of ${SCOPE_SERVICES.join(', ')} → {name: level}` };
|
|
216
|
+
for (const [svc, names] of Object.entries(r.scopes)) {
|
|
217
|
+
if (!SCOPE_SERVICES.includes(svc)) return { ok: false, error: `unknown scope service "${svc}" — one of ${SCOPE_SERVICES.join(', ')}` };
|
|
218
|
+
if (!names || typeof names !== 'object' || Array.isArray(names)) return { ok: false, error: `scopes.${svc} must be an object of name → ${SCOPE_LEVELS.join(' | ')}` };
|
|
219
|
+
const out = {};
|
|
220
|
+
for (const [name, level] of Object.entries(names)) {
|
|
221
|
+
// null clears a level: the place is read as `search` again. The route merges a patch over the stored levels.
|
|
222
|
+
if (level === null) continue;
|
|
223
|
+
if (!name.trim() || name.length > 120) return { ok: false, error: `a ${svc} name is 1 to 120 characters` };
|
|
224
|
+
if (!SCOPE_LEVELS.includes(level)) return { ok: false, error: `the level of ${svc} "${name}" must be one of ${SCOPE_LEVELS.join(', ')}` };
|
|
225
|
+
out[name] = level;
|
|
226
|
+
}
|
|
227
|
+
if (Object.keys(out).length > MAX_SCOPE_NAMES) return { ok: false, error: `scopes.${svc} has ${Object.keys(out).length} names — at most ${MAX_SCOPE_NAMES}` };
|
|
228
|
+
s.scopes[svc] = out;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (r.dayCheck !== undefined) {
|
|
232
|
+
const d = r.dayCheck;
|
|
233
|
+
if (!d || typeof d !== 'object' || Array.isArray(d)) return { ok: false, error: 'dayCheck must be {every, start, end}' };
|
|
234
|
+
const every = d.every === undefined ? DAY_CHECK_DEFAULT.every : d.every;
|
|
235
|
+
const start = d.start === undefined ? DAY_CHECK_DEFAULT.start : d.start;
|
|
236
|
+
const end = d.end === undefined ? DAY_CHECK_DEFAULT.end : d.end;
|
|
237
|
+
if (!DAY_CHECK_EVERY.includes(every)) return { ok: false, error: 'dayCheck.every must be 30 or 60 (minutes), or 0 for off' };
|
|
238
|
+
if (!HHMM.test(String(start)) || !HHMM.test(String(end)) || start >= end) return { ok: false, error: 'dayCheck start and end must be HH:MM (24-hour, local), with start before end' };
|
|
239
|
+
s.dayCheck = { every, start, end };
|
|
240
|
+
}
|
|
241
|
+
if (r.websites !== undefined) {
|
|
242
|
+
if (!Array.isArray(r.websites)) return { ok: false, error: 'websites must be a list of http(s) addresses' };
|
|
243
|
+
if (r.websites.length > MAX_WEBSITES) return { ok: false, error: `websites has ${r.websites.length} addresses — at most ${MAX_WEBSITES}` };
|
|
244
|
+
for (const u of r.websites) if (typeof u !== 'string' || u.length > 500 || !/^https?:\/\/\S+$/.test(u)) return { ok: false, error: 'a website must be an http(s) address of at most 500 characters' };
|
|
245
|
+
s.websites = [...new Set(r.websites)];
|
|
246
|
+
}
|
|
201
247
|
return { ok: true, settings: s };
|
|
202
248
|
}
|
|
203
249
|
|
|
@@ -210,4 +256,4 @@ function inQuietHours(nowMs, quiet) {
|
|
|
210
256
|
return a < b ? m >= a && m < b : m >= a || m < b;
|
|
211
257
|
}
|
|
212
258
|
|
|
213
|
-
module.exports = { BRIEF_SOURCES, TRIGGERS, VERDICTS, EVENTS, DEFAULTS, DEDUPE_MS, DISMISS_MS, BASE, detect, rank, newSignals, dismissedIds, validateFeedback, normalizeSettings, inQuietHours };
|
|
259
|
+
module.exports = { BRIEF_SOURCES, SCOPE_SERVICES, SCOPE_LEVELS, MAX_SCOPE_NAMES, MAX_WEBSITES, DAY_CHECK_EVERY, TRIGGERS, VERDICTS, EVENTS, DEFAULTS, DEDUPE_MS, DISMISS_MS, BASE, detect, rank, newSignals, dismissedIds, validateFeedback, normalizeSettings, inQuietHours };
|
package/workspace/package.json
CHANGED
|
@@ -42,6 +42,7 @@ import { createDockRail } from './dock.js';
|
|
|
42
42
|
import { SEL, ensureComponentsCss, iconButton, button, menu, toast } from './primitives.js';
|
|
43
43
|
import { ensureSprite } from './icons.js';
|
|
44
44
|
import { COPY as FC } from './frame-copy.js';
|
|
45
|
+
import { registerCommand } from './registry.js';
|
|
45
46
|
|
|
46
47
|
/* The LEGACY layout is the three-column shell of column-layout: repositories with the list under them, the detail, the tabs, and
|
|
47
48
|
* a list that a mode (skills, memory …) replaces. A phone still draws it until Run 4 of workspace-cockpit-nav, and `?layout=legacy`
|
|
@@ -482,9 +483,20 @@ function repoMenu(anchor) {
|
|
|
482
483
|
const items = tuples.map((t) => ({ label: t[0], icon: t[0] === cur ? 'check' : null, onSelect: () => chooseRepo(t[0]) }));
|
|
483
484
|
if (items.length) items.push({ sep: true });
|
|
484
485
|
items.push({ label: FC.repo.all, icon: fr.scope === 'all' ? 'check' : null, onSelect: chooseAll });
|
|
486
|
+
if (tuples.length) items.push({ sep: true }, { label: FC.repo.branches, onSelect: openBranchesSheet });
|
|
485
487
|
menu({ anchor, items, ariaLabel: FC.repo.menu });
|
|
486
488
|
}
|
|
487
489
|
|
|
490
|
+
/* Branches and sessions (0.54.1): the legacy panel 1's working trees, which the frame never drew. The current repository,
|
|
491
|
+
* or every repository when the scope is All. */
|
|
492
|
+
function openBranchesSheet() {
|
|
493
|
+
const tuples = mods.repos && typeof mods.repos.repoTuples === 'function' ? mods.repos.repoTuples() : [];
|
|
494
|
+
const cur = fr.scope === 'all' ? null : activeRepo();
|
|
495
|
+
const repos = cur ? [cur] : tuples.map((t) => t[0]);
|
|
496
|
+
import('./branches-sheet.js').then((m) => m.openBranches(repos)).catch((e) => console.error('[pm] could not open the branches:', e));
|
|
497
|
+
}
|
|
498
|
+
if (FRAME) registerCommand({ id: 'branches-open', label: FC.repo.branches, hint: FC.branches.hint, cli: 'git worktree list', run: () => openBranchesSheet() });
|
|
499
|
+
|
|
488
500
|
/* Sync: the bar shows it only while the current repository is not in sync. The reading is local git (GET
|
|
489
501
|
* /api/sync/status), re-read a minute after the last one and at once after a Sync. */
|
|
490
502
|
const syncSeen = new Map(); // repo -> { at, st, pending }
|
|
@@ -681,7 +693,7 @@ function frameKey(e) {
|
|
|
681
693
|
|
|
682
694
|
/* What the suites and the phone chrome reach the frame by. */
|
|
683
695
|
const frameApi = {
|
|
684
|
-
on: true, go, toggleDock, foldDock, openDock, toggleFocus, dockWidth, chooseRepo, chooseAll, openPalette, repoMenu, closeDetail, sync: runSync,
|
|
696
|
+
on: true, go, toggleDock, foldDock, openDock, toggleFocus, dockWidth, chooseRepo, chooseAll, openPalette, repoMenu, openBranchesSheet, closeDetail, sync: runSync,
|
|
685
697
|
get unsynced() { return !!fr.unsynced; },
|
|
686
698
|
get dest() { return fr.dest; }, get scope() { return fr.scope; }, get dockOpen() { return dockOpen(); }, get dockMax() { return !!fr.dockMax; },
|
|
687
699
|
get detailOpen() { return !!state.col2; }, get syncing() { return fr.syncing; }, get needs() { return fr.needs || 0; },
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/* pm/branches-sheet.js — the working trees of a repository and the sessions in each, in the frame (0.54.1).
|
|
2
|
+
*
|
|
3
|
+
* The legacy panel 1 listed every tree (main first) with its live sessions, `+ branch` and `+ session` (panel-trees.js).
|
|
4
|
+
* The frame never drew panel 1, so from 0.53.0 the branches and their sessions had no place. This sheet draws the same
|
|
5
|
+
* blocks, unchanged: one repository, or every repository when the scope is All. It repaints on every state change while
|
|
6
|
+
* it is open, so a session that starts or ends elsewhere (a terminal, the CLI) shows here within one poll.
|
|
7
|
+
*
|
|
8
|
+
* openBranches(repos) → the sheet. `repos` is the list of repository names to show, in order.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { el, setChildren } from './dom.js';
|
|
12
|
+
import { sheet } from './primitives.js';
|
|
13
|
+
import { state, subscribe } from './state.js';
|
|
14
|
+
import { treesOf } from './panel-repos.js';
|
|
15
|
+
import { treesBlock, branchForm, treeNoteFor, sessionsIn } from './panel-trees.js';
|
|
16
|
+
import { refreshTrees } from './data.js';
|
|
17
|
+
import { COPY } from './frame-copy.js';
|
|
18
|
+
|
|
19
|
+
const live = (s) => s && s.state !== 'exited' && s.shown !== 'exited';
|
|
20
|
+
|
|
21
|
+
function sub(repos) {
|
|
22
|
+
let trees = 0; let sessions = 0;
|
|
23
|
+
for (const r of repos) for (const t of treesOf(r)) { trees++; sessions += sessionsIn(t.ws).filter(live).length; }
|
|
24
|
+
return COPY.branches.sub(trees, sessions);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function body(repos) {
|
|
28
|
+
if (!repos.length) return [el('p', { class: 'fr-branches__empty' }, COPY.branches.none)];
|
|
29
|
+
return repos.map((r) => {
|
|
30
|
+
const trees = treesOf(r);
|
|
31
|
+
return el('section', { class: 'fr-branches__repo', 'data-fr-branches': r },
|
|
32
|
+
repos.length > 1 ? el('h3', { class: 'fr-branches__name' }, r) : null,
|
|
33
|
+
branchForm(r),
|
|
34
|
+
trees.length ? treesBlock(r, trees, true) : el('p', { class: 'fr-branches__empty' }, COPY.branches.noTrees),
|
|
35
|
+
treeNoteFor(r));
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function openBranches(repos) {
|
|
40
|
+
const list = (repos || []).filter(Boolean);
|
|
41
|
+
const s = sheet({ title: COPY.branches.title, sub: sub(list), body: body(list), attrs: { 'data-fr': 'branches' }, onClose: () => stop() });
|
|
42
|
+
// The trees can change behind the page (a branch made in a terminal): read them once more on open.
|
|
43
|
+
try { Promise.resolve(refreshTrees()).catch(() => {}); } catch { /* no data layer */ }
|
|
44
|
+
let last = null;
|
|
45
|
+
const stop = subscribe(() => {
|
|
46
|
+
if (!s.open) return;
|
|
47
|
+
// Repaint only when something the sheet shows changed, so an input the PM types in keeps its node between polls.
|
|
48
|
+
const key = JSON.stringify([state.branchForm && { ...state.branchForm, name: null, from: null }, state.treeNote, state.treeSel, state.treeConfirm,
|
|
49
|
+
list.map((r) => treesOf(r).map((t) => [t.ws, t.branch, sessionsIn(t.ws).map((x) => [x.id, x.state, x.shown, x.name, x.initiative])]))]);
|
|
50
|
+
if (key === last) return;
|
|
51
|
+
last = key;
|
|
52
|
+
s.setHead(COPY.branches.title, sub(list));
|
|
53
|
+
setChildren(s.body, body(list));
|
|
54
|
+
});
|
|
55
|
+
return s;
|
|
56
|
+
}
|
|
@@ -2148,45 +2148,156 @@
|
|
|
2148
2148
|
flex-direction: column;
|
|
2149
2149
|
gap: 8px;
|
|
2150
2150
|
}
|
|
2151
|
-
/* today-goals (
|
|
2152
|
-
.
|
|
2151
|
+
/* today-goals (G16 to G22): the day section above what is owed. Folded, it is one toggle; open, a one-line top bar, the
|
|
2152
|
+
lists on the left and the detail of the selected thing on the right. Below 860px of its own width the detail goes under
|
|
2153
|
+
the list. The controls are the primitives (button, row, tabs, field, badge, meter); this block lays them out and
|
|
2154
|
+
marks the selected row. Colour stays neutral, with warning and destructive for what needs the PM (R28-12). */
|
|
2155
|
+
.td-board:has(> .td-day[data-open="true"]) { max-width: 1240px; }
|
|
2156
|
+
.td-day {
|
|
2157
|
+
container: td-day / inline-size;
|
|
2153
2158
|
display: flex;
|
|
2154
2159
|
flex-direction: column;
|
|
2155
|
-
gap:
|
|
2160
|
+
gap: 12px;
|
|
2161
|
+
min-width: 0;
|
|
2162
|
+
}
|
|
2163
|
+
.td-day[data-open="false"] { border: 1px solid var(--border); background: var(--card); padding: 4px 8px; }
|
|
2164
|
+
.td-day__bar {
|
|
2165
|
+
display: flex;
|
|
2166
|
+
flex-wrap: wrap;
|
|
2167
|
+
align-items: center;
|
|
2168
|
+
gap: 8px 10px;
|
|
2156
2169
|
padding: 8px 12px;
|
|
2157
2170
|
border: 1px solid var(--border);
|
|
2158
2171
|
background: var(--card);
|
|
2159
2172
|
}
|
|
2160
|
-
.td-
|
|
2161
|
-
.td-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2173
|
+
.td-day__hello { flex: 0 1 220px; min-width: 0; overflow: hidden; text-overflow: ellipsis; font-size: 14px; color: var(--foreground); white-space: nowrap; }
|
|
2174
|
+
.td-day__dim { color: var(--muted-foreground); }
|
|
2175
|
+
.td-day__mono { font-family: var(--mono); font-variant-numeric: tabular-nums; font-size: 12px; }
|
|
2176
|
+
.td-day__steps { display: flex; flex-wrap: wrap; gap: 2px; list-style: none; margin: 0; padding: 0; }
|
|
2177
|
+
.td-day__ts { gap: 6px; padding: 0 6px; letter-spacing: 0.04em; color: var(--muted-foreground); }
|
|
2178
|
+
.td-day__ts[data-step-state="done"], .td-day__ts[data-step-state="now"] { color: var(--foreground); }
|
|
2179
|
+
.td-day__ts[data-step-state="now"] { background: var(--muted); }
|
|
2180
|
+
.td-day__ts[aria-current="true"] { border-color: var(--ring); }
|
|
2181
|
+
.td-day__tsdot { display: inline-block; flex: none; margin-right: 6px; vertical-align: middle; width: 8px; height: 8px; border-radius: 50%; border: 1.5px solid currentColor; }
|
|
2182
|
+
.td-day__ts[data-step-state="done"] .td-day__tsdot { background: currentColor; }
|
|
2183
|
+
.td-day__ts[data-step-state="now"] .td-day__tsdot { background: var(--foreground); animation: td-day-pulse 1.2s infinite; }
|
|
2184
|
+
@keyframes td-day-pulse { 50% { opacity: 0.3; } }
|
|
2185
|
+
@media (prefers-reduced-motion: reduce) { .td-day__ts[data-step-state="now"] .td-day__tsdot { animation: none; } }
|
|
2186
|
+
.td-day__status { flex: 1 1 0; min-width: 0; font-size: 12px; color: var(--muted-foreground); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
2187
|
+
.td-day__check { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: var(--muted-foreground); }
|
|
2188
|
+
.td-day__run { margin: 0; font-size: 12px; line-height: 16px; color: var(--foreground); }
|
|
2189
|
+
.td-day__run--failed { color: var(--destructive); }
|
|
2190
|
+
.td-day__note { margin: 0; font-size: 12px; line-height: 16px; color: var(--muted-foreground); }
|
|
2191
|
+
|
|
2192
|
+
.td-day__cols { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.25fr); gap: 12px; align-items: start; }
|
|
2193
|
+
.td-day__left { display: grid; gap: 12px; min-width: 0; }
|
|
2194
|
+
.td-day__panel, .td-day__detail {
|
|
2195
|
+
display: grid;
|
|
2196
|
+
gap: 8px;
|
|
2197
|
+
min-width: 0;
|
|
2198
|
+
padding: 10px 12px;
|
|
2199
|
+
border: 1px solid var(--border);
|
|
2200
|
+
background: var(--card);
|
|
2201
|
+
}
|
|
2202
|
+
.td-day__detail { position: sticky; top: 8px; max-height: calc(100vh - 16px); overflow: auto; align-content: start; }
|
|
2203
|
+
@container td-day (max-width: 859px) {
|
|
2204
|
+
.td-day__cols { grid-template-columns: minmax(0, 1fr); }
|
|
2205
|
+
.td-day__detail { position: static; max-height: none; }
|
|
2206
|
+
}
|
|
2207
|
+
.td-day__h2 {
|
|
2165
2208
|
display: flex;
|
|
2166
|
-
|
|
2167
|
-
gap:
|
|
2209
|
+
justify-content: space-between;
|
|
2210
|
+
gap: 8px;
|
|
2211
|
+
margin: 0;
|
|
2212
|
+
font-size: 12px;
|
|
2213
|
+
font-weight: 600;
|
|
2214
|
+
letter-spacing: 0.06em;
|
|
2215
|
+
text-transform: uppercase;
|
|
2216
|
+
color: var(--muted-foreground);
|
|
2217
|
+
}
|
|
2218
|
+
.td-day__h2 span { font-weight: 400; letter-spacing: 0; text-transform: none; }
|
|
2219
|
+
.td-day__rows { list-style: none; margin: 0; padding: 0; display: grid; gap: 2px; }
|
|
2220
|
+
.td-day__r { border: 1px solid transparent; }
|
|
2221
|
+
.td-day__r[data-cur="true"] { background: var(--accent); border-color: var(--ring); }
|
|
2222
|
+
.td-day__r[data-past="true"] { opacity: 0.62; }
|
|
2223
|
+
.td-day__r[data-goal-tie="true"] { border-left: 3px solid var(--foreground); }
|
|
2224
|
+
.td-day__lead { min-width: 44px; font: 12px var(--mono); color: var(--muted-foreground); font-variant-numeric: tabular-nums; }
|
|
2225
|
+
.td-day__chips { display: inline-flex; gap: 4px; }
|
|
2226
|
+
.td-day__nowrow { display: flex; align-items: center; gap: 8px; padding: 0 8px; font: 11px var(--mono); color: var(--destructive); }
|
|
2227
|
+
.td-day__nowrow::after { content: ""; flex: 1; border-top: 2px solid var(--destructive); }
|
|
2228
|
+
.td-day__gh { display: flex; align-items: center; gap: 6px; margin: 4px 8px 2px; font-size: 12px; font-weight: 600; color: var(--muted-foreground); }
|
|
2229
|
+
.td-day__group { display: grid; gap: 2px; }
|
|
2230
|
+
.td-day__newdot { display: inline-block; flex: none; width: 7px; height: 7px; border-radius: 50%; background: var(--warning); }
|
|
2231
|
+
.td-day__chip { min-width: 84px; padding: 3px 8px; font: 500 11px/1.2 var(--mono); text-align: center; background: var(--muted); color: var(--foreground); }
|
|
2232
|
+
|
|
2233
|
+
/* the detail */
|
|
2234
|
+
.td-day__dhead { display: grid; gap: 4px; }
|
|
2235
|
+
.td-day__kind { font: 11px var(--mono); letter-spacing: 0.06em; text-transform: uppercase; color: var(--muted-foreground); }
|
|
2236
|
+
.td-day__dhead h3 { margin: 0; font-size: 16px; line-height: 24px; color: var(--foreground); }
|
|
2237
|
+
.td-day__detail p { margin: 0; font-size: 14px; line-height: 20px; color: var(--foreground); }
|
|
2238
|
+
.td-day__detail p.td-day__note { font-size: 12px; line-height: 16px; color: var(--muted-foreground); }
|
|
2239
|
+
.td-day__acts { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
|
|
2240
|
+
.td-day__a {
|
|
2241
|
+
display: inline-flex;
|
|
2242
|
+
align-items: center;
|
|
2243
|
+
height: 28px;
|
|
2244
|
+
padding: 0 10px;
|
|
2245
|
+
border: 1px solid var(--border);
|
|
2246
|
+
color: var(--foreground);
|
|
2247
|
+
font-size: 12px;
|
|
2248
|
+
font-weight: 600;
|
|
2249
|
+
letter-spacing: 0.1em;
|
|
2250
|
+
text-transform: uppercase;
|
|
2251
|
+
text-decoration: none;
|
|
2252
|
+
white-space: nowrap;
|
|
2168
2253
|
}
|
|
2169
|
-
.td-
|
|
2170
|
-
.td-
|
|
2171
|
-
.td-
|
|
2172
|
-
.td-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
.td-
|
|
2176
|
-
.td-
|
|
2254
|
+
.td-day__a:focus-visible { outline: 2px solid color-mix(in oklab, var(--ring) 50%, var(--foreground)); outline-offset: 2px; }
|
|
2255
|
+
@media (pointer: coarse) { .td-day__a { min-height: 44px; } }
|
|
2256
|
+
.td-day__phase, .td-day__rel { display: grid; gap: 4px; }
|
|
2257
|
+
.td-day__phase h4, .td-day__rel h4, .td-day__notes h4, .td-day__draftbox h4, .td-day__more h4 {
|
|
2258
|
+
margin: 0; font-size: 11px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--muted-foreground);
|
|
2259
|
+
}
|
|
2260
|
+
.td-day__chk { list-style: none; margin: 0; padding: 0; display: grid; gap: 4px; font-size: 14px; color: var(--foreground); }
|
|
2261
|
+
.td-day__chk li { display: grid; grid-template-columns: 16px minmax(0, 1fr); gap: 6px; }
|
|
2262
|
+
.td-day__chk li::before { content: "○"; color: var(--muted-foreground); }
|
|
2263
|
+
.td-day__chk--ok li::before, .td-day__chk li.td-day__ok::before { content: "✓"; color: var(--foreground); }
|
|
2264
|
+
.td-day__chk li:has(.td-day__stepline--done)::before { content: "✓"; color: var(--foreground); }
|
|
2265
|
+
.td-day__chk li:has(.td-day__stepline--you)::before { content: "●"; color: var(--warning); }
|
|
2266
|
+
.td-day__who { margin-left: 6px; font-size: 11px; color: var(--muted-foreground); }
|
|
2267
|
+
.td-day__notes { display: grid; gap: 8px; padding: 10px; background: var(--muted); }
|
|
2268
|
+
.td-day__src { font: 11px var(--mono); color: var(--muted-foreground); }
|
|
2269
|
+
.td-day__ini { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; font-size: 14px; color: var(--foreground); }
|
|
2270
|
+
.td-day__job { display: grid; gap: 6px; padding: 10px 12px; border: 1px solid var(--border); background: var(--background); }
|
|
2271
|
+
.td-day__jobtop { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 8px; }
|
|
2272
|
+
.td-day__jobtitle { font-weight: 600; min-width: 0; color: var(--foreground); }
|
|
2273
|
+
.td-day__ctx { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 3px 10px; margin: 0; font-size: 14px; }
|
|
2274
|
+
.td-day__ctx dt { padding-top: 1px; font-size: 12px; color: var(--muted-foreground); }
|
|
2275
|
+
.td-day__ctx dd { margin: 0; color: var(--foreground); }
|
|
2276
|
+
.td-day__draftbox { display: grid; gap: 4px; }
|
|
2277
|
+
.td-day__draft { margin: 0; max-height: 220px; overflow: auto; padding: 8px 10px; white-space: pre-wrap; font: 12px/1.5 var(--mono); background: var(--muted); color: var(--foreground); }
|
|
2278
|
+
.td-day__revise { display: grid; gap: 6px; }
|
|
2279
|
+
.td-day__log { font-size: 12px; color: var(--muted-foreground); }
|
|
2280
|
+
.td-day__ch { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 8px; font-size: 14px; color: var(--foreground); }
|
|
2281
|
+
.td-day__slist { display: grid; }
|
|
2282
|
+
.td-day__sr { display: grid; grid-template-columns: 28px minmax(0, 1fr) auto auto; align-items: center; gap: 10px; padding: 7px 0; border-top: 1px solid var(--border); }
|
|
2283
|
+
.td-day__sr:first-child { border-top: 0; }
|
|
2284
|
+
.td-day__ic { display: grid; place-items: center; width: 28px; height: 28px; font: 600 11px var(--mono); background: var(--muted); color: var(--foreground); }
|
|
2285
|
+
.td-day__st { font-size: 12px; color: var(--muted-foreground); white-space: nowrap; }
|
|
2286
|
+
.td-day__more { grid-column: 2 / -1; display: grid; gap: 8px; min-width: 0; padding: 4px 0 6px; }
|
|
2287
|
+
.td-day__sites { list-style: none; margin: 0; padding: 0; display: grid; gap: 4px; }
|
|
2288
|
+
.td-day__sites li { display: flex; align-items: center; justify-content: space-between; gap: 8px; min-width: 0; }
|
|
2289
|
+
.td-day__sites li span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
2290
|
+
/* the goal detail keeps the steps the goals section showed (G6): Today cards and the agent steps that can be prepared */
|
|
2291
|
+
.td-goal__steps { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
|
|
2177
2292
|
.td-goal__step { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 8px; }
|
|
2178
2293
|
.td-goal__step-title { color: var(--foreground); }
|
|
2179
|
-
.td-goal__meta, .td-
|
|
2294
|
+
.td-goal__meta, .td-goal__prep-text {
|
|
2180
2295
|
color: var(--muted-foreground);
|
|
2181
2296
|
font-size: 12px;
|
|
2182
2297
|
line-height: 16px;
|
|
2183
2298
|
margin: 0;
|
|
2184
2299
|
}
|
|
2185
2300
|
.td-goal__prep { display: inline-flex; align-items: center; gap: 8px; }
|
|
2186
|
-
.td-goals__bar { display: flex; gap: 8px; }
|
|
2187
|
-
.td-goals__run { margin: 0; font-size: 12px; line-height: 16px; color: var(--foreground); }
|
|
2188
|
-
.td-goals__run--failed { color: var(--destructive); }
|
|
2189
|
-
.td-goals__sub { margin: 4px 0 0; font-size: 12px; font-weight: 600; color: var(--muted-foreground); }
|
|
2190
2301
|
.td-card {
|
|
2191
2302
|
display: flex;
|
|
2192
2303
|
flex-direction: column;
|
|
@@ -21,7 +21,7 @@ import { wsOf } from './data.js';
|
|
|
21
21
|
import { state, setState, FIXTURE } from './state.js';
|
|
22
22
|
import { COPY } from './today-copy.js';
|
|
23
23
|
import { todayCard, acceptCard, rankNodes, scopeCtx, ensureFeed, feedOf, feedErrorOf, bannerOf, onFeed } from './today-cards.js';
|
|
24
|
-
import {
|
|
24
|
+
import { daySection, daySig } from './today-day.js';
|
|
25
25
|
|
|
26
26
|
const SHOWN = 3; // cards drawn before "Show more": the first paint holds at most 17 controls (first-paint-today.test.js)
|
|
27
27
|
let expanded = false;
|
|
@@ -118,10 +118,12 @@ function build(ctx0) {
|
|
|
118
118
|
? button({ label: expanded ? COPY.fewer : COPY.more(nodes.length - SHOWN), variant: 'ghost', attrs: { 'data-today': 'more' }, onClick: () => { expanded = !expanded; setState({}); } })
|
|
119
119
|
: null;
|
|
120
120
|
nodes.forEach((node) => node.setAttribute('role', 'listitem'));
|
|
121
|
-
// today-goals (G6): the
|
|
122
|
-
|
|
123
|
-
const
|
|
124
|
-
|
|
121
|
+
// today-goals (G6, G16): the day section (goals, calendar, jobs, mentions, the day run) sits above what is owed; it is
|
|
122
|
+
// joined to the cards of the first repository in scope, and its initiatives open in that repository
|
|
123
|
+
const dayRepo = sc.repos.length ? sc.repos[0] : null;
|
|
124
|
+
const dayWs = dayRepo ? wsOf(dayRepo) : null;
|
|
125
|
+
const day = daySection({ ws: dayWs, setState: ctx0.setState || setState, openIni: dayRepo ? (id) => openInitiative(dayRepo, id) : null });
|
|
126
|
+
return { children: [head, day, health, list, more], sig: JSON.stringify([sc.all, sc.repo, n, ready, expanded, banner]) + daySig(dayWs) + nodes.map((x) => x.outerHTML).join('') };
|
|
125
127
|
}
|
|
126
128
|
|
|
127
129
|
/** The key of the control that has the focus inside the board, so it can be focused again after a rebuild. */
|
|
@@ -132,7 +134,7 @@ function focusKey(root) {
|
|
|
132
134
|
const attr = ['data-today-act', 'data-today-more', 'data-today-add-note', 'data-today-accept', 'data-today-return'].find((k) => a.hasAttribute(k));
|
|
133
135
|
if (card && attr) return { id: card.getAttribute('data-today-id'), attr };
|
|
134
136
|
if (a.closest('[data-today-note]')) return { id: card && card.getAttribute('data-today-id'), note: true };
|
|
135
|
-
if (a.
|
|
137
|
+
if (a.hasAttribute('data-day-input')) return { dayInput: a.getAttribute('data-day-input'), caret: a.selectionStart };
|
|
136
138
|
if (a.hasAttribute('data-today-scope')) return { scope: a.getAttribute('data-today-scope') };
|
|
137
139
|
if (a.hasAttribute('data-today') && a.getAttribute('data-today') === 'more') return { more: true };
|
|
138
140
|
return null;
|
|
@@ -143,10 +145,10 @@ function refocus(root, k) {
|
|
|
143
145
|
let t = null;
|
|
144
146
|
if (k.note && card) t = card.querySelector('[data-today-note] input, [data-today-note] textarea, input[data-today-note], textarea[data-today-note]');
|
|
145
147
|
else if (k.attr && card) t = card.querySelector('[' + k.attr + ']');
|
|
146
|
-
else if (k.
|
|
148
|
+
else if (k.dayInput) t = [...root.querySelectorAll('[data-day-input]')].find((x) => x.getAttribute('data-day-input') === k.dayInput) || null;
|
|
147
149
|
else if (k.scope) t = root.querySelector('[data-today-scope="' + k.scope + '"]');
|
|
148
150
|
else if (k.more) t = root.querySelector('[data-today="more"]');
|
|
149
|
-
if (t && !t.hidden) { try { t.focus({ preventScroll: true }); if (k.
|
|
151
|
+
if (t && !t.hidden) { try { t.focus({ preventScroll: true }); if (k.dayInput && typeof k.caret === 'number') t.setSelectionRange(k.caret, k.caret); } catch { /* not focusable */ } }
|
|
150
152
|
}
|
|
151
153
|
|
|
152
154
|
/** Draw Today into `host`. Safe to call on every render: nothing changes unless what Today shows changed. */
|
|
@@ -11,6 +11,14 @@ export const COPY = {
|
|
|
11
11
|
none: 'No repository',
|
|
12
12
|
menu: 'Choose a repository',
|
|
13
13
|
open: (name) => 'Repository: ' + name + '. Choose a repository.',
|
|
14
|
+
branches: 'Branches and sessions',
|
|
15
|
+
},
|
|
16
|
+
branches: {
|
|
17
|
+
title: 'Branches and sessions',
|
|
18
|
+
sub: (trees, live) => trees + (trees === 1 ? ' branch' : ' branches') + ' · ' + live + ' live ' + (live === 1 ? 'session' : 'sessions'),
|
|
19
|
+
none: 'Add a repository to see its branches.',
|
|
20
|
+
noTrees: 'This repository has no working tree the workspace can read.',
|
|
21
|
+
hint: 'Every branch and the sessions running in it',
|
|
14
22
|
},
|
|
15
23
|
nav: {
|
|
16
24
|
label: 'Destinations',
|