create-pmos 0.72.1 → 0.74.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/payload/KIT-VERSION +1 -1
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/state/discoveries.md +1 -1
- package/workspace/guide/notes.md +1 -1
- package/workspace/guide/today.md +23 -2
- package/workspace/lib/brief.js +88 -2
- package/workspace/lib/day-marks.js +163 -0
- package/workspace/lib/day-run.js +38 -4
- package/workspace/package-lock.json +1 -1
- package/workspace/package.json +1 -1
- package/workspace/public/pm/app.js +2 -0
- package/workspace/public/pm/components.css +99 -8
- package/workspace/public/pm/note-dock.js +137 -0
- package/workspace/public/pm/notes-links.js +16 -37
- package/workspace/public/pm/today-copy.js +82 -4
- package/workspace/public/pm/today-day.js +349 -19
- package/workspace/public/pm/view-notes.js +39 -9
- package/workspace/server.js +81 -3
- package/workspace/skills/day-task.skill +1 -0
- package/workspace/skills/morning-brief.skill +13 -2
- package/workspace/test-lists/today-act-prepare.json +5 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.74.0",
|
|
4
4
|
"description": "Scaffold the PMOS portable kit — run the PMOS product-management workflow (initiative → lane-fit spec → contract → build → three gates, with work-type lanes for features, research, maintenance, incidents, and content) entirely inside your own repo, with all state as git-versioned files. Includes `workflow-sweep`, which audits the state every board derives from and separates real defects from work you simply have not done. `create-pmos workspace` also installs + launches the local Terminal Workspace GUI. No database, no network, no telemetry.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
package/payload/KIT-VERSION
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Eval calibration log"
|
|
4
4
|
description: "Append-only record of every PM override of an evaluator verdict."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-07T07:25:37Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Health budgets"
|
|
4
4
|
description: "Closed registry of health-budget anchors (D59), edited only by the PM."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-07T07:25:37Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Discoveries and friction log"
|
|
4
4
|
description: "Append-only record of what runs discovered and where the process rubbed."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-07T07:25:37Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
package/workspace/guide/notes.md
CHANGED
|
@@ -95,7 +95,7 @@ Under the title, a note shows two lines: **Meeting** and **Initiative**.
|
|
|
95
95
|
|
|
96
96
|
When a note has no initiative, the summary suggests one: the open initiative that shares 3 or more words with what was said, when no other initiative shares as many. A suggestion never replaces an initiative you chose.
|
|
97
97
|
|
|
98
|
-
To take notes for a calendar event, open the event's detail on the Agenda and press **Take notes**. The note is linked to the event, and to the event's initiative when it has one. The note
|
|
98
|
+
To take notes for a calendar event, open the event's detail on the Agenda and press **Take notes**. PMOS starts to listen at once, to your microphone and to the meeting when local transcription is set up. Pick the meeting's tab or window when the browser asks. Let people know that you are taking notes. The note is linked to the event, and to the event's initiative when it has one. The note stays small at the bottom right of the workspace, with a red dot, its title and its time, on every page. Press it to open the note as a pop-up: type your notes, read the transcript, and press **Stop** when the meeting ends. Press **Minimize** or Esc to make it small again, and **Open in Notes** to see it on the full screen. One note is taken at a time. If you press **Take notes** on another event while a note listens, PMOS asks first: **Keep it**, or **Stop it and start this one**. The note stays at the bottom right after a reload. When it no longer listens, press the x beside it to put it away. An event that already has a note shows its summary and **Open notes**, which opens the note in the pop-up. See [Today](today.md) and [the initiative sheet](initiatives-screen.md).
|
|
99
99
|
|
|
100
100
|
## Dictate into your notes
|
|
101
101
|
|
package/workspace/guide/today.md
CHANGED
|
@@ -50,7 +50,7 @@ Each day of the month answers three questions:
|
|
|
50
50
|
|
|
51
51
|
The switch above the month shows one answer at a time: **Overview** (all three), **Full** (the time in meetings), **Key** (the meeting that matters), **Prep** (how ready you are) or **Free** (the free blocks of 90 minutes or more).
|
|
52
52
|
|
|
53
|
-
A meeting's detail ends with **Meeting notes**. Press **Take notes** to start a note linked to the meeting, and to its initiative when the brief names one.
|
|
53
|
+
A meeting's detail ends with **Meeting notes**. Press **Take notes** to start a note linked to the meeting, and to its initiative when the brief names one. PMOS starts to listen at once and keeps the note small at the bottom right. Press it to open the note as a pop-up. When the meeting has a note, the detail shows its summary and **Open notes**, which opens it in the pop-up, and the meeting's row shows **notes**. See [Notes](notes.md).
|
|
54
54
|
|
|
55
55
|
The memory of a meeting says what kind of meeting it is (one-to-one, onboarding or group), how many times it was held, how many are to come, and why it matters.
|
|
56
56
|
|
|
@@ -82,12 +82,33 @@ Each job has one of three kinds.
|
|
|
82
82
|
|
|
83
83
|
| Kind | Who does it | What you do |
|
|
84
84
|
|---|---|---|
|
|
85
|
-
| you | Only you. | Do it.
|
|
85
|
+
| you | Only you. | Do it, then mark it. See [Jobs you do and meetings you prepare for](#jobs-you-do-and-meetings-you-prepare-for). |
|
|
86
86
|
| approve | An agent prepares one draft. | Read the draft. Press **Approve** to let the one outward step happen, or **Revise** with a note. |
|
|
87
87
|
| auto | An agent does it, and nothing leaves the computer. | Read the result. Press **Revise** if it is wrong. |
|
|
88
88
|
|
|
89
89
|
A draft that waits for you shows its summary, its text and the link of the place it is for. **Approve** sends only that text, to that place. **Revise** takes a note of at most 500 characters and asks for a new draft. You can revise a job 3 times. A fourth revise is refused, so do the job yourself or drop it. A failed job shows its reason and **Retry**.
|
|
90
90
|
|
|
91
|
+
### Jobs you do and meetings you prepare for
|
|
92
|
+
|
|
93
|
+
A job that only you can do starts with a verb, such as **Reply**, **Review** or **Sign off**, and one sentence that says what to do. Under the sentence are who asked, why it is for today, and the steps. Check each step when you finish it. The first button opens the place where you do the job, for example **Reply in Slack**. **Have the agent draft it** gives the job to an agent. The draft then waits for your approval, as for any approve job.
|
|
94
|
+
|
|
95
|
+
A job card holds what you need to do the job without opening another file. **The conversation** shows the last messages, up to 4. A step that reads a source shows what the source says, in a few lines, under the step. **What the sources say** shows the other sources. **Open the source** opens the source itself when you want more.
|
|
96
|
+
|
|
97
|
+
When the job is a reply or a review, you can finish it in the card. Write your answer in the box, or press a suggestion to start from it. For a review, choose **Approve** or **Ask for changes**. Then press **Send to …**. An agent posts exactly your text, as you, to the place the button names. Nothing else is sent, and nothing is sent before you press. The job then shows **Sending**, and **Sent** when it is posted. If the send fails, the job shows the reason and **Retry**, which sends the same text again.
|
|
98
|
+
|
|
99
|
+
A meeting that needs prep is a row of **Needs you**, with the verb **Prepare**. Its detail shows the prep, how many steps are ready, the free time before the meeting, and the steps before it. A step that reads a source shows what the source says under it. A step that asks for your view, such as **Write your recommendation**, has a box for your note. The note is saved when you leave the box. It stays with the meeting on this computer and is not sent. A step that the brief already marked done stays checked.
|
|
100
|
+
|
|
101
|
+
When you finish or put off a job or a meeting, mark it:
|
|
102
|
+
|
|
103
|
+
| Press | What it does |
|
|
104
|
+
|---|---|
|
|
105
|
+
| **Mark done** (a meeting: **Mark ready**) | Moves it to **Done today**. |
|
|
106
|
+
| **Snooze** (a meeting: **Remind me**) | Moves it to **Snoozed** until a time later today. At that time it comes back to **Needs you**. |
|
|
107
|
+
| **Waiting on…** | Puts a job on hold. Write who or what you wait on, and when to ask again today. It shows in **Waiting on others** until that time. |
|
|
108
|
+
| **Not mine** (a meeting: **Skip prep**) | Takes it off **Needs you**, with a reason. |
|
|
109
|
+
|
|
110
|
+
Each press shows **Undo**. **Bring it back now** or **Reopen** puts a marked item back in **Needs you**. A mark lasts for the day. A mark does not send anything and does not change the source. Only **Send to …** sends, and only your text. **Snoozed**, **Waiting on others** and **Done today** are folded. Press a group to open it.
|
|
111
|
+
|
|
91
112
|
The page also shows which sources were read and how many items each gave, and the next daytime check. **Check now** starts a check at once.
|
|
92
113
|
|
|
93
114
|
## What waits for you
|
package/workspace/lib/brief.js
CHANGED
|
@@ -41,6 +41,8 @@ const EFFORTS = ['minutes', 'an-hour', 'half-day', 'day'];
|
|
|
41
41
|
const KINDS = ['you', 'approve', 'auto'];
|
|
42
42
|
const DIGEST_SOURCES = ['slack', 'jira', 'confluence'];
|
|
43
43
|
const STEP_WHEN = ['before', 'during', 'after'];
|
|
44
|
+
// The verb of an item the PM acts on (initiative today-act-prepare, PRD A2): the chip Your day shows first.
|
|
45
|
+
const VERBS = ['reply', 'review', 'decide', 'sign-off', 'read', 'do'];
|
|
44
46
|
const WORTH_ASKING = 0.5;
|
|
45
47
|
const MAX_ITEMS = 30;
|
|
46
48
|
const MAX_EVENTS = 20;
|
|
@@ -95,6 +97,10 @@ function eventExtras(e, at, sources) {
|
|
|
95
97
|
const err = optText(e[k], n, `${at}: ${k}`); if (err) return { error: err };
|
|
96
98
|
if (typeof e[k] === 'string' && e[k].trim()) f[k] = clip(e[k], n);
|
|
97
99
|
}
|
|
100
|
+
// The sources the PM reads to prepare, each in a few lines, so no other file needs opening (PRD X2).
|
|
101
|
+
const refs = refList(e.refs, at);
|
|
102
|
+
if (refs.error) return refs;
|
|
103
|
+
if (refs.refs.length) f.refs = refs.refs;
|
|
98
104
|
if (e.steps !== undefined && e.steps !== null) {
|
|
99
105
|
if (typeof e.steps !== 'object' || Array.isArray(e.steps)) return { error: `${at}: steps must be {before, during, after}` };
|
|
100
106
|
for (const k of Object.keys(e.steps)) if (!STEP_WHEN.includes(k)) return { error: `${at}: steps has "${k}" — only ${STEP_WHEN.join(', ')}` };
|
|
@@ -112,7 +118,10 @@ function eventExtras(e, at, sources) {
|
|
|
112
118
|
if (tooLong(st.t, 140)) return { error: `${sa}: t is over 140 characters` };
|
|
113
119
|
if (!OWNERS.includes(st.who)) return { error: `${sa}: who must be one of ${OWNERS.join(', ')}` };
|
|
114
120
|
if (st.done !== undefined && typeof st.done !== 'boolean') return { error: `${sa}: done must be true or false` };
|
|
115
|
-
|
|
121
|
+
if (st.ref !== undefined && st.ref !== null && !(Number.isInteger(st.ref) && st.ref >= 0 && st.ref < refs.refs.length)) return { error: `${sa}: ref must name one of the refs` };
|
|
122
|
+
if (st.note !== undefined && typeof st.note !== 'boolean') return { error: `${sa}: note must be true or false` };
|
|
123
|
+
if (st.note && (k !== 'before' || st.who !== 'pm')) return { error: `${sa}: note is for a step the PM does before the meeting` };
|
|
124
|
+
steps[k].push({ t: clip(st.t, 140), who: st.who, done: st.done === true, ...(Number.isInteger(st.ref) ? { ref: st.ref } : {}), ...(st.note ? { note: true } : {}) });
|
|
116
125
|
}
|
|
117
126
|
}
|
|
118
127
|
f.steps = steps;
|
|
@@ -177,9 +186,86 @@ function itemExtras(x, at) {
|
|
|
177
186
|
if (!KINDS.includes(kind)) return { error: `${at}: kind must be one of ${KINDS.join(', ')}` };
|
|
178
187
|
if (x.owner === 'pm' && kind !== 'you') return { error: `${at}: an item the PM owns has kind "you" — "${kind}" is for an item the agent owns` };
|
|
179
188
|
f.kind = kind;
|
|
189
|
+
// What the PM does, said once (PRD A2): a verb, one sentence, a due time and at most 4 steps. All optional.
|
|
190
|
+
if (x.verb !== undefined && x.verb !== null) {
|
|
191
|
+
if (!VERBS.includes(x.verb)) return { error: `${at}: verb must be one of ${VERBS.join(', ')}` };
|
|
192
|
+
f.verb = x.verb;
|
|
193
|
+
}
|
|
194
|
+
const terr = optText(x.todo, 200, `${at}: todo`);
|
|
195
|
+
if (terr) return { error: terr };
|
|
196
|
+
if (typeof x.todo === 'string' && x.todo.trim()) f.todo = clip(x.todo, 200);
|
|
197
|
+
// The context the PM reads in the card, so no other file needs opening (PRD X1, X2), and where the PM's own answer goes
|
|
198
|
+
// (PRD F1). All optional.
|
|
199
|
+
const ctx = cardContext(x, at);
|
|
200
|
+
if (ctx.error) return { error: ctx.error };
|
|
201
|
+
Object.assign(f, ctx.fields);
|
|
202
|
+
if (x.due !== undefined && x.due !== null) {
|
|
203
|
+
if (typeof x.due !== 'string' || Number.isNaN(Date.parse(x.due))) return { error: `${at}: due must be a date and time` };
|
|
204
|
+
f.due = x.due;
|
|
205
|
+
}
|
|
206
|
+
const st = stepList(x.steps, 4, f.refs ? f.refs.length : 0, at);
|
|
207
|
+
if (st.error) return { error: st.error };
|
|
208
|
+
if (st.steps.length) f.steps = st.steps;
|
|
180
209
|
return { fields: f };
|
|
181
210
|
}
|
|
182
211
|
|
|
212
|
+
/* The messages of the conversation (at most 4) and the sources it points to, each read in a few lines (at most 3). */
|
|
213
|
+
function cardContext(x, at) {
|
|
214
|
+
const f = {};
|
|
215
|
+
if (x.thread !== undefined && x.thread !== null) {
|
|
216
|
+
if (!Array.isArray(x.thread) || x.thread.length > 4) return { error: `${at}: thread must be a list of at most 4 messages` };
|
|
217
|
+
const thread = [];
|
|
218
|
+
for (const [i, m] of x.thread.entries()) {
|
|
219
|
+
const w = `${at}: thread message ${i + 1}`;
|
|
220
|
+
if (!m || typeof m !== 'object' || typeof m.text !== 'string' || !m.text.trim()) return { error: `${w} needs text` };
|
|
221
|
+
const e = optText(m.text, 280, `${w} text`) || optText(m.who, 80, `${w} who`) || optText(m.at, 20, `${w} at`);
|
|
222
|
+
if (e) return { error: e };
|
|
223
|
+
thread.push({ who: m.who ? clip(m.who, 80) : null, at: m.at ? clip(m.at, 20) : null, text: clip(m.text, 280) });
|
|
224
|
+
}
|
|
225
|
+
if (thread.length) f.thread = thread;
|
|
226
|
+
}
|
|
227
|
+
const refs = refList(x.refs, at);
|
|
228
|
+
if (refs.error) return refs;
|
|
229
|
+
if (refs.refs.length) f.refs = refs.refs;
|
|
230
|
+
if (x.send !== undefined && x.send !== null) {
|
|
231
|
+
const sd = x.send;
|
|
232
|
+
if (typeof sd !== 'object' || Array.isArray(sd)) return { error: `${at}: send must be {to, suggest}` };
|
|
233
|
+
const e = optText(sd.to, 120, `${at}: send.to`) || optLines(sd.suggest, 3, 280, `${at}: send.suggest`);
|
|
234
|
+
if (e) return { error: e };
|
|
235
|
+
if (sd.review !== undefined && typeof sd.review !== 'boolean') return { error: `${at}: send.review must be true or false` };
|
|
236
|
+
f.send = { to: sd.to ? clip(sd.to, 120) : null, suggest: lines(sd.suggest, 280), ...(sd.review ? { review: true } : {}) };
|
|
237
|
+
}
|
|
238
|
+
return { fields: f };
|
|
239
|
+
}
|
|
240
|
+
function refList(v, at) {
|
|
241
|
+
if (v === undefined || v === null) return { refs: [] };
|
|
242
|
+
if (!Array.isArray(v) || v.length > 3) return { error: `${at}: refs must be a list of at most 3 sources` };
|
|
243
|
+
const refs = [];
|
|
244
|
+
for (const [i, r] of v.entries()) {
|
|
245
|
+
const w = `${at}: ref ${i + 1}`;
|
|
246
|
+
if (!r || typeof r !== 'object' || typeof r.label !== 'string' || !r.label.trim()) return { error: `${w} needs a label` };
|
|
247
|
+
const e = optText(r.label, 120, `${w} label`) || optText(r.source, 20, `${w} source`) || optLink(r.link, `${w} link`) || optLines(r.lines, 5, 200, `${w} lines`);
|
|
248
|
+
if (e) return { error: e };
|
|
249
|
+
if (!Array.isArray(r.lines) || !r.lines.length) return { error: `${w} needs lines: what the source says, in your words` };
|
|
250
|
+
refs.push({ label: clip(r.label, 120), source: r.source ? clip(r.source, 20) : null, link: r.link || null, lines: lines(r.lines, 200) });
|
|
251
|
+
}
|
|
252
|
+
return { refs };
|
|
253
|
+
}
|
|
254
|
+
/* Steps of an item: text, or {t, ref} where ref is the index of the source the step reads. */
|
|
255
|
+
function stepList(v, max, nRefs, at) {
|
|
256
|
+
if (v === undefined || v === null) return { steps: [] };
|
|
257
|
+
if (!Array.isArray(v) || v.length > max) return { error: `${at}: steps must be a list of at most ${max}` };
|
|
258
|
+
const steps = [];
|
|
259
|
+
for (const [i, s] of v.entries()) {
|
|
260
|
+
const o = typeof s === 'string' ? { t: s } : s;
|
|
261
|
+
if (!o || typeof o !== 'object' || typeof o.t !== 'string' || !o.t.trim()) return { error: `${at}: step ${i + 1} must be text` };
|
|
262
|
+
if (tooLong(o.t, 140)) return { error: `${at}: step ${i + 1} is over 140 characters` };
|
|
263
|
+
if (o.ref !== undefined && o.ref !== null && !(Number.isInteger(o.ref) && o.ref >= 0 && o.ref < nRefs)) return { error: `${at}: step ${i + 1}: ref must name one of the refs` };
|
|
264
|
+
steps.push(Number.isInteger(o.ref) ? { t: clip(o.t, 140), ref: o.ref } : { t: clip(o.t, 140) });
|
|
265
|
+
}
|
|
266
|
+
return { steps };
|
|
267
|
+
}
|
|
268
|
+
|
|
183
269
|
/* The fixed order of items: higher confidence first, then PM-owned before agent-owned, then by title. */
|
|
184
270
|
function sortItems(items) {
|
|
185
271
|
return items.sort((a, b) => b.confidence - a.confidence || (a.owner === b.owner ? 0 : a.owner === 'pm' ? -1 : 1) || a.title.localeCompare(b.title));
|
|
@@ -419,5 +505,5 @@ function rereadBrief(stored, incoming) {
|
|
|
419
505
|
digests, places, resolved: [...gone].slice(-MAX_RESOLVED) } };
|
|
420
506
|
}
|
|
421
507
|
|
|
422
|
-
module.exports = { SOURCES, ITEM_SOURCES, SOURCE_STATES, OWNERS, EFFORTS, KINDS, DIGEST_SOURCES, WORTH_ASKING, MAX_ITEMS, MAX_EVENTS, MAX_DIGESTS, MAX_PLACES,
|
|
508
|
+
module.exports = { VERBS, SOURCES, ITEM_SOURCES, SOURCE_STATES, OWNERS, EFFORTS, KINDS, DIGEST_SOURCES, WORTH_ASKING, MAX_ITEMS, MAX_EVENTS, MAX_DIGESTS, MAX_PLACES,
|
|
423
509
|
isUrl, validateBrief, mergeBrief, rereadBrief, greeting, localDate };
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/* day-marks.js — the PM's marks on today's items and meetings (initiative `today-act-prepare`, PRD M1 to M6).
|
|
3
|
+
*
|
|
4
|
+
* A brief item the PM owns, or a meeting with prep, used to stay in Needs you all day: nothing the PM did changed it.
|
|
5
|
+
* A mark is what the PM did with it: done (a meeting: ready), snoozed until a time, waiting on someone until a time, or
|
|
6
|
+
* dismissed with a reason (not mine, or skip prep). A mark also holds the steps the PM checked, by their text.
|
|
7
|
+
*
|
|
8
|
+
* PURE: no I/O and no clock of its own. The server reads goals/<date>.marks.json, passes it with `now` and the ids of
|
|
9
|
+
* today's brief, and writes what `edit` returns. A refused edit returns the reason and the caller writes nothing.
|
|
10
|
+
*
|
|
11
|
+
* emptyMarks(date) → { date, marks: {} }
|
|
12
|
+
* readMarks(raw, date) → a checked record, or an empty one when `raw` is not one for `date`
|
|
13
|
+
* edit(rec, op, { now, known }) → { ok:true, rec, mark, prev } | { ok:false, error }
|
|
14
|
+
* op: { action: 'done' | 'reopen', id }
|
|
15
|
+
* | { action: 'snooze', id, until } until: a time later today
|
|
16
|
+
* | { action: 'hold', id, on, until } on: who or what (1 to 80 characters), until: a time later today
|
|
17
|
+
* | { action: 'dismiss', id, why } why: no-action | someone-else | done-elsewhere (an item)
|
|
18
|
+
* nothing | elsewhere (a meeting)
|
|
19
|
+
* | { action: 'step', id, step, done } step: the step's text, done: true | false
|
|
20
|
+
* | { action: 'note', id, text } the PM's own note, written in the card (at most 1000
|
|
21
|
+
* characters; empty clears it). It stays on this machine.
|
|
22
|
+
* | { action: 'restore', id, mark } mark: a `prev` an earlier edit returned, or null (Undo)
|
|
23
|
+
* known: Map of today's ids → 'item' | 'event'. An id not in it is refused.
|
|
24
|
+
* effective(rec, now) → { id: mark } with a snooze or hold whose time has passed read as open
|
|
25
|
+
*
|
|
26
|
+
* A mark lasts for its day: brief item ids are per day (lib/brief.js), so a snooze or a hold ends at midnight.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const STATES = ['done', 'snoozed', 'waiting', 'dismissed'];
|
|
30
|
+
const ACTIONS = ['done', 'reopen', 'snooze', 'hold', 'dismiss', 'step', 'note', 'restore'];
|
|
31
|
+
const KINDS = ['item', 'event'];
|
|
32
|
+
const REASONS = { item: ['no-action', 'someone-else', 'done-elsewhere'], event: ['nothing', 'elsewhere'] };
|
|
33
|
+
const ID_RE = /^[0-9a-f]{12}$/;
|
|
34
|
+
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
35
|
+
const MAX_IDS = 400;
|
|
36
|
+
const MAX_STEPS = 12;
|
|
37
|
+
const MAX_STEP = 140;
|
|
38
|
+
const MAX_ON = 80;
|
|
39
|
+
const MAX_NOTE = 1000;
|
|
40
|
+
|
|
41
|
+
const clip = (s, n) => String(s == null ? '' : s).replace(/\s+/g, ' ').trim().slice(0, n);
|
|
42
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
43
|
+
const localDate = (ms) => { const d = new Date(ms); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; };
|
|
44
|
+
const iso = (ms) => new Date(ms).toISOString();
|
|
45
|
+
|
|
46
|
+
const emptyMarks = (date) => ({ date, marks: {} });
|
|
47
|
+
|
|
48
|
+
/** One mark read from disk or from an Undo, checked. Returns the clean mark, or null when it is not one. */
|
|
49
|
+
function cleanMark(m, date) {
|
|
50
|
+
if (!m || typeof m !== 'object' || Array.isArray(m) || !KINDS.includes(m.kind)) return null;
|
|
51
|
+
const state = m.state === null || m.state === undefined ? null : m.state;
|
|
52
|
+
if (state !== null && !STATES.includes(state)) return null;
|
|
53
|
+
const out = { kind: m.kind, state, at: typeof m.at === 'string' ? m.at : null };
|
|
54
|
+
if (state === 'snoozed' || state === 'waiting') {
|
|
55
|
+
const t = Date.parse(m.until);
|
|
56
|
+
if (!Number.isFinite(t) || localDate(t) !== date) return null;
|
|
57
|
+
out.until = iso(t);
|
|
58
|
+
}
|
|
59
|
+
if (state === 'waiting') { const on = clip(m.on, MAX_ON); if (!on) return null; out.on = on; }
|
|
60
|
+
if (state === 'dismissed') { if (!REASONS[m.kind].includes(m.why)) return null; out.why = m.why; }
|
|
61
|
+
const steps = Array.isArray(m.steps) ? [...new Set(m.steps.map((s) => clip(s, MAX_STEP)).filter(Boolean))].slice(0, MAX_STEPS) : [];
|
|
62
|
+
if (steps.length) out.steps = steps;
|
|
63
|
+
if (typeof m.note === 'string' && m.note.trim()) out.note = m.note.trim().slice(0, MAX_NOTE);
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** A marks record read from disk, checked. Anything that is not one for `date` reads as an empty record. */
|
|
68
|
+
function readMarks(raw, date) {
|
|
69
|
+
if (!raw || typeof raw !== 'object' || raw.date !== date || !raw.marks || typeof raw.marks !== 'object') return emptyMarks(date);
|
|
70
|
+
const marks = {};
|
|
71
|
+
for (const [id, m] of Object.entries(raw.marks).slice(0, MAX_IDS)) {
|
|
72
|
+
if (!ID_RE.test(id)) continue;
|
|
73
|
+
const c = cleanMark(m, date);
|
|
74
|
+
if (c) marks[id] = c;
|
|
75
|
+
}
|
|
76
|
+
return { date, marks };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** A time later today, or the reason it is not one. */
|
|
80
|
+
function laterToday(v, now, date) {
|
|
81
|
+
const t = typeof v === 'number' ? v : Date.parse(v);
|
|
82
|
+
if (!Number.isFinite(t)) return { error: 'until must be a date and time' };
|
|
83
|
+
if (t <= now) return { error: 'until must be later than now' };
|
|
84
|
+
if (localDate(t) !== date) return { error: 'until must be today: a mark ends with the day' };
|
|
85
|
+
return { at: t };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function edit(rec, op, { now = Date.now(), known } = {}) {
|
|
89
|
+
if (!rec || !DATE_RE.test(String(rec.date)) || !rec.marks || typeof rec.marks !== 'object') return { ok: false, error: 'no marks record to edit' };
|
|
90
|
+
if (!op || typeof op !== 'object' || !ACTIONS.includes(op.action)) return { ok: false, error: `action must be one of ${ACTIONS.join(', ')}` };
|
|
91
|
+
const id = String(op.id || '');
|
|
92
|
+
if (!ID_RE.test(id)) return { ok: false, error: 'id must be the id of an item or a meeting of today\'s brief' };
|
|
93
|
+
const kind = known && typeof known.get === 'function' ? known.get(id) : null;
|
|
94
|
+
if (!KINDS.includes(kind)) return { ok: false, error: 'no item or meeting with that id in today\'s brief' };
|
|
95
|
+
if (localDate(now) !== rec.date) return { ok: false, error: `the marks are for ${rec.date}, and today is ${localDate(now)}` };
|
|
96
|
+
const marks = { ...rec.marks };
|
|
97
|
+
const prev = marks[id] ? JSON.parse(JSON.stringify(marks[id])) : null;
|
|
98
|
+
const cur = prev ? { ...prev } : { kind, state: null, at: null };
|
|
99
|
+
cur.kind = kind;
|
|
100
|
+
const at = iso(now);
|
|
101
|
+
const setState = (state, extra) => {
|
|
102
|
+
delete cur.until; delete cur.on; delete cur.why;
|
|
103
|
+
Object.assign(cur, { state, at }, extra || {});
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
if (op.action === 'restore') {
|
|
107
|
+
if (op.mark === null || op.mark === undefined) { delete marks[id]; return { ok: true, rec: { date: rec.date, marks }, mark: null, prev }; }
|
|
108
|
+
const c = cleanMark({ ...op.mark, kind }, rec.date);
|
|
109
|
+
if (!c) return { ok: false, error: 'mark is not a mark of this item' };
|
|
110
|
+
marks[id] = c;
|
|
111
|
+
return { ok: true, rec: { date: rec.date, marks }, mark: c, prev };
|
|
112
|
+
}
|
|
113
|
+
if (op.action === 'step') {
|
|
114
|
+
const step = clip(op.step, MAX_STEP);
|
|
115
|
+
if (!step) return { ok: false, error: 'step must be the text of a step' };
|
|
116
|
+
if (typeof op.done !== 'boolean') return { ok: false, error: 'done must be true or false' };
|
|
117
|
+
const steps = new Set(cur.steps || []);
|
|
118
|
+
if (op.done) steps.add(step); else steps.delete(step);
|
|
119
|
+
if (steps.size > MAX_STEPS) return { ok: false, error: `at most ${MAX_STEPS} steps are kept for one item` };
|
|
120
|
+
if (steps.size) cur.steps = [...steps]; else delete cur.steps;
|
|
121
|
+
} else if (op.action === 'note') {
|
|
122
|
+
if (typeof op.text !== 'string') return { ok: false, error: 'text must be the note' };
|
|
123
|
+
if (op.text.trim().length > MAX_NOTE) return { ok: false, error: `the note is over ${MAX_NOTE} characters` };
|
|
124
|
+
if (op.text.trim()) cur.note = op.text.trim(); else delete cur.note;
|
|
125
|
+
} else if (op.action === 'done') {
|
|
126
|
+
if (cur.state === 'done') return { ok: false, error: kind === 'event' ? 'that meeting is already marked ready' : 'that item is already done' };
|
|
127
|
+
setState('done');
|
|
128
|
+
} else if (op.action === 'reopen') {
|
|
129
|
+
if (cur.state === null) return { ok: false, error: 'that item is already open' };
|
|
130
|
+
setState(null);
|
|
131
|
+
} else if (op.action === 'snooze') {
|
|
132
|
+
const t = laterToday(op.until, now, rec.date);
|
|
133
|
+
if (t.error) return { ok: false, error: t.error };
|
|
134
|
+
setState('snoozed', { until: iso(t.at) });
|
|
135
|
+
} else if (op.action === 'hold') {
|
|
136
|
+
if (kind !== 'item') return { ok: false, error: 'a meeting is not put on hold: snooze it, or skip prep' };
|
|
137
|
+
const on = clip(op.on, MAX_ON);
|
|
138
|
+
if (!on) return { ok: false, error: `on must say who or what you wait on, in 1 to ${MAX_ON} characters` };
|
|
139
|
+
if (typeof op.on === 'string' && op.on.trim().length > MAX_ON) return { ok: false, error: `on is over ${MAX_ON} characters` };
|
|
140
|
+
const t = laterToday(op.until, now, rec.date);
|
|
141
|
+
if (t.error) return { ok: false, error: t.error };
|
|
142
|
+
setState('waiting', { on, until: iso(t.at) });
|
|
143
|
+
} else if (op.action === 'dismiss') {
|
|
144
|
+
if (!REASONS[kind].includes(op.why)) return { ok: false, error: `why must be one of ${REASONS[kind].join(', ')}` };
|
|
145
|
+
setState('dismissed', { why: op.why });
|
|
146
|
+
}
|
|
147
|
+
if (!marks[id] && Object.keys(marks).length >= MAX_IDS) return { ok: false, error: `at most ${MAX_IDS} marks a day` };
|
|
148
|
+
const clean = cleanMark(cur, rec.date);
|
|
149
|
+
if (clean.state === null && !clean.steps && !clean.note) delete marks[id]; else marks[id] = clean;
|
|
150
|
+
return { ok: true, rec: { date: rec.date, marks }, mark: marks[id] || null, prev };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** The marks as the page shows them now: a snooze or a hold whose time has passed is open again, with `back: true`. */
|
|
154
|
+
function effective(rec, now = Date.now()) {
|
|
155
|
+
const out = {};
|
|
156
|
+
for (const [id, m] of Object.entries((rec && rec.marks) || {})) {
|
|
157
|
+
const passed = (m.state === 'snoozed' || m.state === 'waiting') && Date.parse(m.until) <= now;
|
|
158
|
+
out[id] = passed ? { ...m, state: null, back: true, was: m.state } : { ...m };
|
|
159
|
+
}
|
|
160
|
+
return out;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
module.exports = { STATES, ACTIONS, REASONS, MAX_ON, MAX_STEPS, MAX_NOTE, emptyMarks, readMarks, edit, effective };
|
package/workspace/lib/day-run.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* newRun(date, now) · readRun(raw, date)
|
|
16
16
|
* addTasks(run, brief) agent priorities (kind approve|auto) → waiting tasks
|
|
17
17
|
* addBriefed(run, spec) one task the PM briefed in My work (initiative my-work, PRD R5)
|
|
18
|
+
* addSend(run, spec, now) the PM's own text, written in the card, to post as written (today-act-prepare)
|
|
18
19
|
* toStart(run, limit) the tasks to start now, each with its mode (do | revise | send)
|
|
19
20
|
* started · deliver · decide · sent · fail · sessionEnded · sessionStalled
|
|
20
21
|
* stage(run, brief) reading | working | review | done
|
|
@@ -113,7 +114,8 @@ function addTasks(run, brief) {
|
|
|
113
114
|
}
|
|
114
115
|
|
|
115
116
|
/**
|
|
116
|
-
* One task the PM briefed in My work
|
|
117
|
+
* One task the PM briefed in My work, or a job of Your day the PM gave to the agent (`item_id`, initiative
|
|
118
|
+
* today-act-prepare, PRD C2): kind approve, with the PM's instruction and the item's context card on it, so the
|
|
117
119
|
* day-task session reads the instruction as the ask (PRD R5, R6). `spec` is lib/my-work.js taskSpec. A run opened only
|
|
118
120
|
* for this (no Prepare my day yet) is marked `opened_by: 'my-work'`, so it does not read as a morning read that failed.
|
|
119
121
|
*/
|
|
@@ -124,12 +126,43 @@ function addBriefed(run, spec) {
|
|
|
124
126
|
if (!text(s.instruction)) return { ok: false, error: 'a briefed task needs the PM\'s instruction' };
|
|
125
127
|
if (findTask(run, s.id)) return { ok: false, error: `task ${s.id} is already in today's run` };
|
|
126
128
|
const next = clone(run);
|
|
127
|
-
next.tasks.push({ id: s.id, item_id: null, title: text(s.title).slice(0, 250) || s.id, kind: 'approve', source: s.source || 'my-work', link: isUrl(s.link) ? s.link : null,
|
|
129
|
+
next.tasks.push({ id: s.id, item_id: /^[0-9a-f]{12}$/.test(String(s.item_id || '')) ? s.item_id : null, title: text(s.title).slice(0, 250) || s.id, kind: 'approve', source: s.source || 'my-work', link: isUrl(s.link) ? s.link : null,
|
|
128
130
|
goal: null, my_work: s.my_work || null, brief_at: s.brief_at || null, instruction: text(s.instruction), context: s.context && typeof s.context === 'object' ? clone(s.context) : null,
|
|
129
131
|
status: 'waiting', session_id: null, started_at: null, attempts: 0, revisions: [], draft: null, decided: null, error: null });
|
|
130
132
|
return { ok: true, run: next, added: s.id };
|
|
131
133
|
}
|
|
132
134
|
|
|
135
|
+
/**
|
|
136
|
+
* The PM's own text, written in a Your day card, to post as written (initiative today-act-prepare, PRD F1 to F3). The press
|
|
137
|
+
* IS the approval: the task goes straight to `sending` with the PM's text as its draft (`by: 'pm'`), and one send session
|
|
138
|
+
* does that one outward step. A task already in the run for the item (the agent's draft) takes the PM's text instead.
|
|
139
|
+
* `spec`: { id, item_id, title, source, link, to, text, verdict? }.
|
|
140
|
+
*/
|
|
141
|
+
function addSend(run, spec, now) {
|
|
142
|
+
if (!run) return { ok: false, error: 'no day run today' };
|
|
143
|
+
const s = spec && typeof spec === 'object' ? spec : {};
|
|
144
|
+
if (!TASK_RE.test(String(s.id || ''))) return { ok: false, error: 'a task needs an id like t-0123456789ab' };
|
|
145
|
+
const body = typeof s.text === 'string' ? s.text.trim() : '';
|
|
146
|
+
if (!body) return { ok: false, error: 'write the text to send' };
|
|
147
|
+
if (body.length > 4000) return { ok: false, error: 'the text is over 4000 characters' };
|
|
148
|
+
const to = text(s.to).slice(0, 120) || 'the place it came from';
|
|
149
|
+
const what = s.verdict === 'approve' ? 'review, approving' : s.verdict === 'changes' ? 'review, asking for changes' : 'reply';
|
|
150
|
+
const at = iso(now);
|
|
151
|
+
const draft = { summary: `The PM's own ${what}: post it as written to ${to}`.slice(0, 240), body, link: isUrl(s.link) ? s.link : null, at, by: 'pm' };
|
|
152
|
+
const next = clone(run);
|
|
153
|
+
const t = findTask(next, s.id);
|
|
154
|
+
if (t) {
|
|
155
|
+
if (['working', 'sending', 'sent'].includes(t.status)) return { ok: false, error: `task ${s.id} is ${t.status} — wait for it, or open its session` };
|
|
156
|
+
keepSession(t);
|
|
157
|
+
Object.assign(t, { kind: 'approve', status: 'sending', draft, decided: { action: 'approve', at, by: 'pm' }, error: null, started_at: null });
|
|
158
|
+
return { ok: true, run: next, task: s.id };
|
|
159
|
+
}
|
|
160
|
+
next.tasks.push({ id: s.id, item_id: /^[0-9a-f]{12}$/.test(String(s.item_id || '')) ? s.item_id : null, title: text(s.title).slice(0, 250) || s.id, kind: 'approve',
|
|
161
|
+
source: s.source || 'today', link: isUrl(s.link) ? s.link : null, goal: null, instruction: `Post the PM's own text exactly as written to ${to}. Change nothing.`,
|
|
162
|
+
status: 'sending', session_id: null, started_at: null, attempts: 0, revisions: [], draft, decided: { action: 'approve', at, by: 'pm' }, error: null });
|
|
163
|
+
return { ok: true, run: next, task: s.id };
|
|
164
|
+
}
|
|
165
|
+
|
|
133
166
|
/**
|
|
134
167
|
* The tasks to start now, oldest first, so that the sessions running (working, and sending with a session) stay at
|
|
135
168
|
* most `limit`. An approved task waiting for its send session comes first: the PM is waiting on it.
|
|
@@ -208,7 +241,8 @@ function decide(run, taskId, op, now) {
|
|
|
208
241
|
}
|
|
209
242
|
if (t.status !== 'failed') return `task ${taskId} is ${t.status} — only a failed task can be run again`;
|
|
210
243
|
const stalled = !!t.stalled_session;
|
|
211
|
-
|
|
244
|
+
// the PM's own text is sent again as it is: a retry never asks an agent to draft over it
|
|
245
|
+
t.status = t.draft && t.draft.by === 'pm' ? 'sending' : 'waiting'; keepSession(t); t.error = null; t.decided = { action: 'retry', at };
|
|
212
246
|
if (stalled) { t.prev_session = null; delete t.stalled_session; delete t.stalled_status; }
|
|
213
247
|
return null;
|
|
214
248
|
});
|
|
@@ -350,5 +384,5 @@ function checkDone(run, { found = 0, resolved = 0 } = {}, now) {
|
|
|
350
384
|
}
|
|
351
385
|
|
|
352
386
|
module.exports = { MAX_PARALLEL, MAX_REVISIONS, MAX_CHECK_LOG, MAX_INTERVAL_MIN, STATUSES, KINDS, ACTIONS, TASK_RE,
|
|
353
|
-
newRun, readRun, findTask, modeOf, addTasks, addBriefed, toStart, started, deliver, decide, sent, fail, sessionEnded, sessionStalled, stage, intervalMin,
|
|
387
|
+
newRun, readRun, findTask, modeOf, addTasks, addBriefed, addSend, toStart, started, deliver, decide, sent, fail, sessionEnded, sessionStalled, stage, intervalMin,
|
|
354
388
|
checkDue, checkStarted, checkDone };
|
package/workspace/package.json
CHANGED
|
@@ -776,6 +776,8 @@ async function boot() {
|
|
|
776
776
|
if (FRAME) { reg.setFrameHome((id, p) => go(id, p)); fr.reg = reg; } // the frame: pmApp.open(id) is the place on screen
|
|
777
777
|
}
|
|
778
778
|
catch (e) { console.error('[pm] the registry failed to load:', e); }
|
|
779
|
+
// meeting-notes Run 6: the note being taken stays at the bottom right on every destination
|
|
780
|
+
import('./note-dock.js').then((m) => m.mountDock()).catch((e) => console.error('[pm] the note dock failed to load:', e));
|
|
779
781
|
render();
|
|
780
782
|
}
|
|
781
783
|
}
|
|
@@ -3346,6 +3346,40 @@
|
|
|
3346
3346
|
.td-day__draftbox { display: grid; gap: 4px; }
|
|
3347
3347
|
.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); }
|
|
3348
3348
|
.td-day__revise { display: grid; gap: 6px; }
|
|
3349
|
+
/* act and prepare (initiative today-act-prepare): a verb first, one sentence, the steps, one button that fits, the marks */
|
|
3350
|
+
.td-day__verb { display: inline-block; min-width: 58px; padding: 2px 5px; border: 1px solid currentColor; font: 600 10px/1.3 var(--mono); letter-spacing: 0.05em; text-transform: uppercase; text-align: center; color: var(--foreground); white-space: nowrap; }
|
|
3351
|
+
.td-day__verb[data-verb="decide"], .td-day__verb[data-verb="prepare"] { color: var(--warning); }
|
|
3352
|
+
.td-day__acth { margin: 0; font-size: 16px; font-weight: 600; line-height: 1.3; text-wrap: balance; }
|
|
3353
|
+
.td-day__todo { margin: 0; padding: 7px 10px; border-left: 3px solid var(--foreground); background: var(--muted); font-size: 14px; line-height: 1.45; color: var(--foreground); }
|
|
3354
|
+
.td-day__steps2 { list-style: none; margin: 0; padding: 0; display: grid; gap: 4px; }
|
|
3355
|
+
.td-day__step2 { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 5px 8px; border: 1px solid var(--border); background: var(--background); }
|
|
3356
|
+
.td-day__step2[data-done="true"] .td-day__steprow { text-decoration: line-through; color: var(--muted-foreground); }
|
|
3357
|
+
.td-day__marks { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; padding-top: 8px; border-top: 1px solid var(--border); }
|
|
3358
|
+
.td-day__grow { flex: 1 1 0; }
|
|
3359
|
+
.td-day__mark { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px; padding: 6px 10px; border: 1px solid var(--border); background: var(--muted); font-size: 14px; }
|
|
3360
|
+
.td-day__mark[data-day-mark-state="done"] { border-color: var(--foreground); }
|
|
3361
|
+
.td-day__ready { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 12px; font-size: 12px; }
|
|
3362
|
+
.td-day__prep { display: grid; gap: 8px; padding: 10px 12px; border: 1px solid var(--border); background: var(--background); }
|
|
3363
|
+
.td-day__fold { border-top: 1px solid var(--border); padding-top: 6px; }
|
|
3364
|
+
.td-day__fold > summary { cursor: pointer; font-size: 12px; color: var(--muted-foreground); }
|
|
3365
|
+
.td-day__fold[open] > summary { margin-bottom: 6px; }
|
|
3366
|
+
.td-day__fold > summary:focus-visible, .td-day__fold[open] > summary:focus-visible, .td-day__ref > summary:focus-visible { outline: 2px solid color-mix(in oklab, var(--ring) 50%, var(--foreground)); outline-offset: 2px; }
|
|
3367
|
+
@media (pointer: coarse) { .td-day__fold > summary, .td-day__fold[open] > summary, .td-day__ref > summary { min-height: 44px; } }
|
|
3368
|
+
.td-day__fold-head { justify-self: start; }
|
|
3369
|
+
.td-day__steprow { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
|
3370
|
+
.td-day__step2:has(.td-day__ref), .td-day__step2:has(.td-day__notebox) { flex-direction: column; align-items: stretch; }
|
|
3371
|
+
.td-day__ref { display: grid; gap: 4px; padding: 6px 8px; background: var(--muted); font-size: 14px; }
|
|
3372
|
+
.td-day__ref > summary { display: flex; justify-content: space-between; gap: 8px; cursor: pointer; font-weight: 600; color: var(--foreground); }
|
|
3373
|
+
.td-day__reflines { margin: 0; padding-left: 18px; display: grid; gap: 2px; color: var(--foreground); line-height: 1.45; }
|
|
3374
|
+
.td-day__thread { display: grid; gap: 6px; }
|
|
3375
|
+
.td-day__msg { display: grid; gap: 2px; padding: 6px 10px; border-left: 2px solid var(--border); }
|
|
3376
|
+
.td-day__msg p { margin: 0; font-size: 14px; line-height: 1.45; color: var(--foreground); }
|
|
3377
|
+
.td-day__msg--last { border-left-color: var(--foreground); }
|
|
3378
|
+
.td-day__compose { display: grid; gap: 6px; padding: 8px 10px; border: 1px solid var(--border); background: var(--background); }
|
|
3379
|
+
.td-day__chipsrow { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; }
|
|
3380
|
+
.td-day__notebox { display: grid; gap: 4px; }
|
|
3381
|
+
.td-day__a--primary { background: var(--primary); color: var(--primary-foreground); border-color: var(--primary); }
|
|
3382
|
+
@media (hover: hover) { .td-day__a--primary:hover { color: var(--primary-foreground); opacity: 0.9; } }
|
|
3349
3383
|
.td-day__log { font-size: 12px; color: var(--muted-foreground); }
|
|
3350
3384
|
.td-day__ch { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 8px; font-size: 14px; color: var(--foreground); }
|
|
3351
3385
|
.td-day__slist { display: grid; }
|
|
@@ -6212,13 +6246,7 @@
|
|
|
6212
6246
|
color: var(--muted-foreground);
|
|
6213
6247
|
}
|
|
6214
6248
|
|
|
6215
|
-
/* meeting-notes
|
|
6216
|
-
.nt-inhost__head {
|
|
6217
|
-
display: flex;
|
|
6218
|
-
align-items: center;
|
|
6219
|
-
justify-content: space-between;
|
|
6220
|
-
gap: 8px;
|
|
6221
|
-
}
|
|
6249
|
+
/* meeting-notes Runs 5 and 6: the note in one column, as the pop-up draws it */
|
|
6222
6250
|
.nt-wrap--inline .nt-body2 {
|
|
6223
6251
|
grid-template-columns: 1fr;
|
|
6224
6252
|
}
|
|
@@ -6232,7 +6260,70 @@
|
|
|
6232
6260
|
border-top: 1px solid var(--border);
|
|
6233
6261
|
}
|
|
6234
6262
|
.nt-wrap--inline .nt-lines {
|
|
6235
|
-
max-height:
|
|
6263
|
+
max-height: 240px;
|
|
6264
|
+
}
|
|
6265
|
+
|
|
6266
|
+
/* meeting-notes Run 6: the note being taken, minimized at the bottom right, and its pop-up */
|
|
6267
|
+
.nt-dock {
|
|
6268
|
+
position: fixed;
|
|
6269
|
+
right: 16px;
|
|
6270
|
+
bottom: 40px;
|
|
6271
|
+
z-index: 1;
|
|
6272
|
+
display: flex;
|
|
6273
|
+
flex-direction: column;
|
|
6274
|
+
align-items: flex-end;
|
|
6275
|
+
}
|
|
6276
|
+
.nt-dock:empty {
|
|
6277
|
+
display: none;
|
|
6278
|
+
}
|
|
6279
|
+
.nt-pillrow {
|
|
6280
|
+
display: flex;
|
|
6281
|
+
align-items: center;
|
|
6282
|
+
gap: 4px;
|
|
6283
|
+
}
|
|
6284
|
+
.nt-pill {
|
|
6285
|
+
max-width: 320px;
|
|
6286
|
+
background: var(--card);
|
|
6287
|
+
box-shadow: 0 4px 16px color-mix(in oklab, var(--term-bg) 18%, transparent);
|
|
6288
|
+
}
|
|
6289
|
+
.nt-pill__txt {
|
|
6290
|
+
display: flex;
|
|
6291
|
+
align-items: center;
|
|
6292
|
+
gap: 8px;
|
|
6293
|
+
min-width: 0;
|
|
6294
|
+
}
|
|
6295
|
+
.nt-pill__title {
|
|
6296
|
+
min-width: 0;
|
|
6297
|
+
overflow: hidden;
|
|
6298
|
+
text-overflow: ellipsis;
|
|
6299
|
+
white-space: nowrap;
|
|
6300
|
+
}
|
|
6301
|
+
.nt-pill__clock {
|
|
6302
|
+
font-size: 12px;
|
|
6303
|
+
font-variant-numeric: tabular-nums;
|
|
6304
|
+
color: var(--muted-foreground);
|
|
6305
|
+
}
|
|
6306
|
+
.nt-pop {
|
|
6307
|
+
width: min(440px, calc(100vw - 32px));
|
|
6308
|
+
max-height: calc(100vh - 120px);
|
|
6309
|
+
display: flex;
|
|
6310
|
+
flex-direction: column;
|
|
6311
|
+
border: 1px solid var(--border);
|
|
6312
|
+
background: var(--card);
|
|
6313
|
+
box-shadow: 0 12px 40px color-mix(in oklab, var(--term-bg) 24%, transparent);
|
|
6314
|
+
}
|
|
6315
|
+
.nt-pop__head {
|
|
6316
|
+
display: flex;
|
|
6317
|
+
align-items: center;
|
|
6318
|
+
justify-content: space-between;
|
|
6319
|
+
gap: 8px;
|
|
6320
|
+
padding: 8px 12px;
|
|
6321
|
+
border-bottom: 1px solid var(--border);
|
|
6322
|
+
}
|
|
6323
|
+
.nt-pop__body {
|
|
6324
|
+
min-height: 0;
|
|
6325
|
+
overflow-y: auto;
|
|
6326
|
+
padding: 12px;
|
|
6236
6327
|
}
|
|
6237
6328
|
|
|
6238
6329
|
/* -- layouts · .lg- the legacy and fixture shells (Run 3, slice 3D) -- */
|