create-pmos 0.230.0 → 0.232.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-work-test.js +29 -5
- package/workspace/guide/my-work.md +8 -3
- package/workspace/lib/pm-items.js +5 -3
- package/workspace/lib/routes-pm-items.js +3 -2
- package/workspace/lib/win-hide.js +48 -0
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/c3-demo-data.js +1 -1
- package/workspace/public/pm/console3.css +4 -0
- package/workspace/public/pm/pm-work-view.js +93 -12
- package/workspace/server.js +2 -0
- package/workspace/test-lists/one-line-setup.json +5 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.232.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-14T08:45:03Z"}
|
|
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-14T08:45:03Z"}
|
|
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-14T08:45:03Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* dimensions 1 to 5 and 7). Loads /c3.html?demo=1 at 1440 wide and then at 390 wide. Demo mode answers every /api call in the page,
|
|
5
5
|
* so nothing reaches the server and the fixture (public/pm/c3-demo-data.js pmItems) is placed by the same rule the server uses.
|
|
6
6
|
*
|
|
7
|
-
* views
|
|
7
|
+
* views seven views in order (Inbox, Today, Upcoming, Waiting, Someday, Review, Done), each with its count; no Triage, Week, Later
|
|
8
8
|
* lens; the right column is Focus on every view, with no Flow today (console3-deck-flow-fixes); no Jobs card, Group by job, ring or plan deck (console3-work-parity)
|
|
9
9
|
* one home none of the decisions and approvals whose home is Briefing · Needs you is a Work row or Work text
|
|
10
10
|
* Focus Today opens with up to three suggestions, each with the reason; Accept pins, Not today hides, Edit title saves, Unpin
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
* to a later day; Done takes a one-line outcome
|
|
13
13
|
* Upcoming the next 7 days by day, then Later; a deferred item says "Not before …"
|
|
14
14
|
* Review one at a time with progress: Keep, Change (the edit fields), Drop, then "Review done"
|
|
15
|
+
* Done the last 30 days by day (Today, Yesterday, …) with the outcome and how it closed; the badge counts today; Reopen puts an
|
|
16
|
+
* item back in its view, with Undo, also an item that was done before the page opened (c3-work-agent amendment 18)
|
|
15
17
|
* capture the preview chip names the view the line lands in, by the server's rule, and never Triage
|
|
16
18
|
* phone at 390 wide every view draws with no horizontal scroll
|
|
17
19
|
* The page must raise no script error. Every wait polls the page; no fixed sleep stands in for a state.
|
|
@@ -40,8 +42,8 @@ const finish = async (code) => {
|
|
|
40
42
|
};
|
|
41
43
|
process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
|
|
42
44
|
|
|
43
|
-
const VIEWS = ['inbox', 'today', 'upcoming', 'waiting', 'someday', 'review'];
|
|
44
|
-
const LABELS = ['Inbox', 'Today', 'Upcoming', 'Waiting', 'Someday', 'Review'];
|
|
45
|
+
const VIEWS = ['inbox', 'today', 'upcoming', 'waiting', 'someday', 'review', 'done'];
|
|
46
|
+
const LABELS = ['Inbox', 'Today', 'Upcoming', 'Waiting', 'Someday', 'Review', 'Done'];
|
|
45
47
|
/* The decisions and approvals the demo used to list in Work (PRD R9): their home is Briefing · Needs you. */
|
|
46
48
|
const DECISIONS = ['Approve pricing-page copy before handoff', 'Confirm the growth assumption in the Q4 forecast', 'Say yes to Sam: ship the logging change',
|
|
47
49
|
'Self-serve onboarding: continue or park?', 'Pick interview slots with 3 customers', 'Assign owners to 2 unowned workshop actions',
|
|
@@ -91,9 +93,9 @@ const W = '[data-pm-work]';
|
|
|
91
93
|
const readBar = () => st((w) => [...document.querySelectorAll(w + ' [data-pm-lenses] [data-lens]')].filter((b) => b.offsetParent !== null).map((b) => [b.getAttribute('data-lens'), b.textContent.replace(/\d+$/, '').trim()]), W);
|
|
92
94
|
await until(async () => (await readBar()).length === VIEWS.length, 10000, 100);
|
|
93
95
|
const bar = await readBar();
|
|
94
|
-
ok('the bar has the
|
|
96
|
+
ok('the bar has the seven views in order: Inbox, Today, Upcoming, Waiting, Someday, Review, Done', JSON.stringify(bar.map((b) => b[0])) === JSON.stringify(VIEWS) && JSON.stringify(bar.map((b) => b[1])) === JSON.stringify(LABELS), JSON.stringify(bar));
|
|
95
97
|
const c0 = await counts();
|
|
96
|
-
ok('each view shows its count (3, 5, 6, 2, 2, 3)', JSON.stringify(VIEWS.map((v) => c0[v])) === '[3,5,6,2,2,3]', JSON.stringify(c0));
|
|
98
|
+
ok('each view shows its count (3, 5, 6, 2, 2, 3; Done counts today: 0)', JSON.stringify(VIEWS.map((v) => c0[v])) === '[3,5,6,2,2,3,0]', JSON.stringify(c0));
|
|
97
99
|
ok('the page opens on Today', await st((w) => (document.querySelector(w + ' [data-pm-lens-head]') || {}).getAttribute && document.querySelector(w + ' [data-pm-lens-head]').getAttribute('data-pm-lens-head') === 'today', W));
|
|
98
100
|
const text0 = await st((w) => document.querySelector(w).textContent, W);
|
|
99
101
|
ok('no Triage, Week or Later view', !(await st((w) => !!document.querySelector(w + ' [data-lens="triage"], ' + w + ' [data-lens="week"], ' + w + ' [data-lens="later"]'), W)));
|
|
@@ -174,6 +176,28 @@ const W = '[data-pm-work]';
|
|
|
174
176
|
if (sam) { await page.fill(W + ' ' + sam + ' [data-pm-outcome]', 'Booked for Tuesday 10:00'); await click(W + ' ' + sam + ' [data-pm-offline-done]'); }
|
|
175
177
|
ok('Done with a one-line outcome closes an offline item', !!sam && !!await until(async () => !(await titles()).includes('Set up a meeting with Sam about the logging rollout') && (await counts()).upcoming === 6, 5000));
|
|
176
178
|
|
|
179
|
+
/* Done (c3-work-agent amendment 18): what the PM finished, by day, with how it closed; Reopen puts it back */
|
|
180
|
+
ok('the Done badge counts the items done today', !!await until(async () => (await counts()).done === 1, 5000), JSON.stringify(await counts()));
|
|
181
|
+
await view('done');
|
|
182
|
+
const doneRows = () => st((w) => [...document.querySelectorAll(w + ' [data-pm-list="done"]')].map((s) => [s.getAttribute('data-pm-sect'), [...s.querySelectorAll('[data-pm-done-item]')].map((r) => ({
|
|
183
|
+
t: (r.querySelector('[data-pm-title]') || {}).textContent, out: (r.querySelector('[data-pm-done-outcome]') || {}).textContent || '', how: (r.querySelector('[data-pm-done-how]') || {}).textContent || '',
|
|
184
|
+
at: (r.querySelector('[data-pm-done-at]') || {}).textContent || '', reopen: !!r.querySelector('[data-pm-reopen]') }))]), W);
|
|
185
|
+
const d0 = await doneRows();
|
|
186
|
+
ok('Done groups the items by day: Today (Sam, done just now) and Yesterday (the PRD)', JSON.stringify(d0.map((g) => [g[0], g[1].map((r) => r.t)])) === JSON.stringify([['today', ['Set up a meeting with Sam about the logging rollout']], ['yday', ['Send revised PRD to Dara']]]), JSON.stringify(d0));
|
|
187
|
+
const samD = d0.length ? d0[0][1][0] : {};
|
|
188
|
+
ok('…a row shows the outcome, how it closed, the time and Reopen', String(samD.out).trim() === 'Booked for Tuesday 10:00' && samD.how === 'You marked it done' && /^Done \d\d:\d\d$/.test(samD.at) && samD.reopen, JSON.stringify(samD));
|
|
189
|
+
ok('…an older row shows the day it was done', d0.length > 1 && /^Done (Tue|Wed) 7 Oct$/.test(d0[1][1][0].at), JSON.stringify(d0[1]));
|
|
190
|
+
ok('…with the summary line and the header', await st((w) => /^2 done in the last 7 days, 2 in 30 days\. Older items stay in their job's sheet\.$/.test((document.querySelector(w + ' [data-pm-done-sum]') || {}).textContent.trim()) && /What you finished/.test(document.querySelector(w + ' [data-pm-lens-head]').textContent), W));
|
|
191
|
+
await click(W + ' [data-pm-done-item] [data-pm-reopen]');
|
|
192
|
+
ok('Reopen says where the item went back to', !!await until(() => st(() => /Reopened\. It is back in Upcoming\./.test(document.body.textContent)), 4000));
|
|
193
|
+
ok('…and it leaves Done and is back in Upcoming', !!await until(async () => { const c = await counts(); return c.done === 0 && c.upcoming === 7; }, 5000), JSON.stringify(await counts()));
|
|
194
|
+
await toastUndo();
|
|
195
|
+
ok('…Undo marks it done again', !!await until(async () => (await counts()).done === 1, 5000), JSON.stringify(await counts()));
|
|
196
|
+
await click(W + ' [data-pm-sect="yday"] [data-pm-reopen]');
|
|
197
|
+
ok('Reopen works on an item done before the page opened', !!await until(async () => !(await doneRows()).some((g) => g[1].some((r) => r.t === 'Send revised PRD to Dara')), 5000), JSON.stringify(await doneRows()));
|
|
198
|
+
await toastUndo();
|
|
199
|
+
ok('…and Undo puts it back in Done', !!await until(async () => (await doneRows()).some((g) => g[1].some((r) => r.t === 'Send revised PRD to Dara')), 5000));
|
|
200
|
+
|
|
177
201
|
/* the shared actions: Flag from the Inbox (R2) */
|
|
178
202
|
await view('inbox');
|
|
179
203
|
const tear = await open('Read the competitor pricing teardown');
|
|
@@ -69,7 +69,7 @@ Agenda shows one card, **N items need your brief**, when items wait in To brief.
|
|
|
69
69
|
|
|
70
70
|
None of these briefs an agent or takes an item. Do that in My work.
|
|
71
71
|
|
|
72
|
-
## The Work page: your work in
|
|
72
|
+
## The Work page: your work in seven views
|
|
73
73
|
|
|
74
74
|
The Work page holds all the work that is yours:
|
|
75
75
|
|
|
@@ -83,7 +83,7 @@ Every action works on these rows. The first action writes the item to your own l
|
|
|
83
83
|
|
|
84
84
|
The server decides where each item shows, so one item is never in two views.
|
|
85
85
|
|
|
86
|
-
The
|
|
86
|
+
The seven views follow the OmniFocus style. The **Work** tab badge counts Inbox plus Today.
|
|
87
87
|
|
|
88
88
|
| View | What it holds |
|
|
89
89
|
|---|---|
|
|
@@ -93,6 +93,7 @@ The six views follow the OmniFocus style. The **Work** tab badge counts Inbox pl
|
|
|
93
93
|
| Waiting | Items where you wait for someone. The item that has waited longest comes first. |
|
|
94
94
|
| Someday | Items you parked. They do not appear anywhere else. |
|
|
95
95
|
| Review | Items in Inbox, Someday or Waiting that nobody touched for 7 days. The most stale item comes first. |
|
|
96
|
+
| Done | Items you marked done in the last 30 days, newest first. Each one shows the outcome and how it closed. |
|
|
96
97
|
|
|
97
98
|
An item goes to the first view that fits these rules, in this order. Parked is Someday. Waiting on someone is Waiting. A defer date after today is Upcoming. Flagged is Today. Due today or earlier is Today. A later due date is Upcoming. An undated item whose defer date has arrived is Today. No date is Inbox.
|
|
98
99
|
|
|
@@ -166,6 +167,10 @@ An offline item can carry evidence that it is done. PMOS looks at your calendar
|
|
|
166
167
|
|
|
167
168
|
Review is a weekly pass. It shows one item at a time. Press **Keep** to leave the item as it is and clear it from the queue for 7 days. Press **Change** to edit it. Press **Drop** to remove it. Any change to an item also clears it from the queue.
|
|
168
169
|
|
|
170
|
+
### Done
|
|
171
|
+
|
|
172
|
+
Done lists the items you marked done in the last 30 days, in four cards: **Today**, **Yesterday**, **Earlier this week** and **Earlier, last 30 days**. Each row shows the outcome you wrote, when the item was done, and how it closed: you marked it done, you closed it from the agent draft, the agent posted it, the agent did the step you approved, or you confirmed it. Press **Reopen** to put the item back in its view. The badge on Done counts the items done today. Older items stay in their job's sheet.
|
|
173
|
+
|
|
169
174
|
### Link to a team task
|
|
170
175
|
|
|
171
176
|
A Work item can link to the plan item it advances. The link shows on the item with the title of the plan item. Closing the plan item is a separate step, in the initiative.
|
|
@@ -176,7 +181,7 @@ A decision or an approval only you can make is not a Work item. It shows in Brie
|
|
|
176
181
|
|
|
177
182
|
## Using the Work page
|
|
178
183
|
|
|
179
|
-
The Work page in the console has
|
|
184
|
+
The Work page in the console has seven views: **Inbox**, **Today**, **Upcoming**, **Waiting**, **Someday**, **Review** and **Done**. Each view button shows how many items it holds. The Done button shows how many items you finished today.
|
|
180
185
|
|
|
181
186
|
Each item is in one view only. The rule, in order: an item in Someday stays in Someday. An item that waits on a person is in Waiting. An item with a Not before date after today is in Upcoming. A flagged item is in Today. An item due today or earlier is in Today. An item due later is in Upcoming. An item with no date is in Inbox.
|
|
182
187
|
|
|
@@ -17,6 +17,8 @@ const fs = require('fs');
|
|
|
17
17
|
const path = require('path');
|
|
18
18
|
|
|
19
19
|
const UNDO_MS = 8000;
|
|
20
|
+
/* How an item was closed (Work › Done): the PM marked it, the PM closed it from the agent draft, or the PM confirmed offline work. */
|
|
21
|
+
const DONE_HOW = ['you', 'agent', 'confirmed'];
|
|
20
22
|
const LISTS = ['waiting', 'today', 'week', 'triage', 'later'];
|
|
21
23
|
const WORK_LISTS = ['inbox', 'today', 'upcoming', 'waiting', 'someday'];
|
|
22
24
|
const REVIEW_DAYS = 7;
|
|
@@ -136,9 +138,9 @@ function fold(lines) {
|
|
|
136
138
|
else if (ev.event === 'edited' && items.has(ev.id)) items.set(ev.id, { ...items.get(ev.id), ...ev.fields, touched_at: ev.at });
|
|
137
139
|
else if (ev.event === 'done' && items.has(ev.id)) {
|
|
138
140
|
const outcome = typeof ev.outcome === 'string' && ev.outcome.trim() ? { outcome: ev.outcome.trim() } : {};
|
|
139
|
-
items.set(ev.id, { ...items.get(ev.id), done: true, done_at: ev.at, touched_at: ev.at, ...outcome });
|
|
141
|
+
items.set(ev.id, { ...items.get(ev.id), done: true, done_at: ev.at, done_how: DONE_HOW.includes(ev.how) ? ev.how : 'you', touched_at: ev.at, ...outcome });
|
|
140
142
|
}
|
|
141
|
-
else if (ev.event === 'reopened' && items.has(ev.id)) items.set(ev.id, { ...items.get(ev.id), done: false, done_at: null, touched_at: ev.at });
|
|
143
|
+
else if (ev.event === 'reopened' && items.has(ev.id)) items.set(ev.id, { ...items.get(ev.id), done: false, done_at: null, done_how: null, touched_at: ev.at });
|
|
142
144
|
else if (ev.event === 'kept' && items.has(ev.id)) items.set(ev.id, { ...items.get(ev.id), touched_at: ev.at });
|
|
143
145
|
else if (ev.event === 'dropped' || ev.event === 'deleted') items.delete(ev.id);
|
|
144
146
|
}
|
|
@@ -177,4 +179,4 @@ function readLines(file) {
|
|
|
177
179
|
return fs.readFileSync(file, 'utf8').split('\n').filter(Boolean).map((l) => { try { return JSON.parse(l); } catch { return null; } });
|
|
178
180
|
}
|
|
179
181
|
|
|
180
|
-
module.exports = { UNDO_MS, LISTS, WORK_LISTS, REVIEW_DAYS, workPlacement, sortWorkList, reviewQueue, arrivesOn, parseLine, placement, fold, removalEvent, undoEvent, canUndo, appendEvent, readLines };
|
|
182
|
+
module.exports = { UNDO_MS, DONE_HOW, LISTS, WORK_LISTS, REVIEW_DAYS, workPlacement, sortWorkList, reviewQueue, arrivesOn, parseLine, placement, fold, removalEvent, undoEvent, canUndo, appendEvent, readLines };
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* defer, flagged, resolves, kind, person, evidence, outcome
|
|
14
14
|
* POST /api/pm-items/remove { id } → { ok, undo_ms } deleted (created here) or dropped (imported)
|
|
15
15
|
* POST /api/pm-items/undo { id } → { ok, item } | 409 restores the last removal inside the 8 s window
|
|
16
|
-
* POST /api/pm-items/done { id, outcome? } → { ok }
|
|
16
|
+
* POST /api/pm-items/done { id, outcome?, how? } → { ok } folds to item.done + done_at (+ outcome, one line; + done_how: you|agent|confirmed); /reopen undoes it
|
|
17
17
|
* POST /api/pm-items/park { id } → { ok } parked: true, placed in Later; /unpark clears it
|
|
18
18
|
* POST /api/pm-items/flag { id, flagged } → { ok } flagged: boolean; a flagged item is in Today
|
|
19
19
|
* POST /api/pm-items/defer { id, until|null } → { ok } defer: not before that date (null clears); clears parked
|
|
@@ -193,7 +193,8 @@ function flagItem(body, itemLines, events) {
|
|
|
193
193
|
function doneItem(b, l) {
|
|
194
194
|
if (b.outcome !== undefined && b.outcome !== null && typeof b.outcome !== 'string') return { status: 400, body: { ok: false, error: 'outcome must be a string' } };
|
|
195
195
|
const outcome = typeof b.outcome === 'string' && b.outcome.trim() ? { outcome: b.outcome.trim() } : {};
|
|
196
|
-
|
|
196
|
+
const how = pmItems.DONE_HOW.includes(b.how) ? { how: b.how } : {}; // how it closed, for Work › Done (c3-work-agent amendment 18)
|
|
197
|
+
return flagItem(b, l, [{ event: 'done', id: b.id, ...outcome, ...how }]);
|
|
197
198
|
}
|
|
198
199
|
const reopenItem = (b, l) => flagItem(b, l, [{ event: 'reopened', id: b.id }]);
|
|
199
200
|
const parkItem = (b, l) => flagItem(b, l, [{ event: 'edited', id: b.id, fields: { parked: true, list_override: null } }]);
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/* lib/win-hide.js — on Windows, no console window for the commands the server runs.
|
|
3
|
+
*
|
|
4
|
+
* The server runs in the background with no console of its own (bin/pmos.js starts it detached and
|
|
5
|
+
* hidden). On Windows, a process with no console that starts a console program (git, claude, npm, cmd)
|
|
6
|
+
* gets a NEW console window for it, unless the call passes `windowsHide: true`. The server runs such
|
|
7
|
+
* commands all the time, so without this the PM sees windows open again and again.
|
|
8
|
+
*
|
|
9
|
+
* This makes `windowsHide: true` the default for every child_process call in this process. It must be
|
|
10
|
+
* required before any other module takes a reference to child_process (server.js requires it first),
|
|
11
|
+
* because `const { spawn } = require('child_process')` keeps the function it saw at that moment.
|
|
12
|
+
* A call that passes `windowsHide` itself keeps its own value. Elsewhere this does nothing.
|
|
13
|
+
*/
|
|
14
|
+
const util = require('util');
|
|
15
|
+
|
|
16
|
+
function withHide(args) {
|
|
17
|
+
const a = [...args];
|
|
18
|
+
// The options object comes after the command and its optional argument list, before any callback.
|
|
19
|
+
const i = Array.isArray(a[1]) || (a[1] == null && a.length > 2) ? 2 : 1;
|
|
20
|
+
const o = a[i];
|
|
21
|
+
if (o && typeof o === 'object' && !Array.isArray(o)) {
|
|
22
|
+
if (o.windowsHide === undefined) a[i] = { ...o, windowsHide: true };
|
|
23
|
+
} else if (o == null && i < a.length) {
|
|
24
|
+
a[i] = { windowsHide: true };
|
|
25
|
+
} else {
|
|
26
|
+
a.splice(i, 0, { windowsHide: true });
|
|
27
|
+
}
|
|
28
|
+
return a;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function install(cp = require('child_process')) {
|
|
32
|
+
if (cp.__pmosWindowsHide) return cp;
|
|
33
|
+
for (const name of ['spawn', 'spawnSync', 'execFile', 'execFileSync', 'exec', 'execSync', 'fork']) {
|
|
34
|
+
const orig = cp[name];
|
|
35
|
+
if (typeof orig !== 'function') continue;
|
|
36
|
+
const wrapped = function (...args) { return orig.apply(this, withHide(args)); };
|
|
37
|
+
// util.promisify(execFile) resolves { stdout, stderr } through this hook; keep it, with the default.
|
|
38
|
+
const custom = orig[util.promisify.custom];
|
|
39
|
+
if (custom) wrapped[util.promisify.custom] = (...args) => custom(...withHide(args));
|
|
40
|
+
cp[name] = wrapped;
|
|
41
|
+
}
|
|
42
|
+
Object.defineProperty(cp, '__pmosWindowsHide', { value: true });
|
|
43
|
+
return cp;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (process.platform === 'win32') install();
|
|
47
|
+
|
|
48
|
+
module.exports = { withHide, install };
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmos-workspace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.232.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "pmos-workspace",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.232.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@xterm/addon-fit": "^0.11.0",
|
package/workspace/package.json
CHANGED
|
@@ -387,7 +387,7 @@ const W_KEYS = it('Payments sandbox keys', { waiting_on: 'Dara', due: '2026-10-0
|
|
|
387
387
|
const W_LABEL = it('Annual-plan label', { waiting_on: 'Lee', due: '2026-10-07', job: 'j1', priority: 'P3', touched_at: at('2026-09-29') });
|
|
388
388
|
const W_JIRA = it('Clean up the Checkout Jira labels', { parked: true, created_at: '2026-09-10T09:00:00.000Z', touched_at: at('2026-09-18') });
|
|
389
389
|
const W_NOMEET = it('Try a no-meeting Wednesday', { parked: true, touched_at: at('2026-10-02') });
|
|
390
|
-
/* done yesterday: counts in its job,
|
|
390
|
+
/* done yesterday: counts in its job, and Work › Done lists it under Yesterday */
|
|
391
391
|
const W_PRD = it('Send revised PRD to Dara', { due: '2026-10-07', job: 'j1', done: true, done_at: '2026-10-07T17:10:00.000Z' });
|
|
392
392
|
const ITEMS = [W_TIMELINE, W_QA, W_CALL, W_RISK, W_PAGER, W_SAM, W_SSO, W_NOTES, W_SALES, W_TAGS, W_Q1, W_TEAR, W_BOOK, W_IDEA, W_KEYS, W_LABEL, W_JIRA, W_NOMEET, W_PRD];
|
|
393
393
|
/* Focus: up to three jobs for today, each with the reason (rules first: an overdue promise, an offline item due, a flag) */
|
|
@@ -2039,6 +2039,10 @@
|
|
|
2039
2039
|
}
|
|
2040
2040
|
|
|
2041
2041
|
.c3 .wk-sm{font-size:var(--fs-2)}
|
|
2042
|
+
/* Work › Done (c3-work-agent amendment 18): the outcome line under the title, and the summary above the groups */
|
|
2043
|
+
.c3 .wk-done-out{display:flex;align-items:center;gap:4px;color:var(--foreground);font-size:var(--fs-2)}
|
|
2044
|
+
.c3 .wk-done-out .ic{color:var(--muted-foreground)}
|
|
2045
|
+
.c3 .wk-done-sum{margin-bottom:12px}
|
|
2042
2046
|
.c3 .wk-bar{appearance:none;-webkit-appearance:none;width:100%;height:8px;border:0;border-radius:6px;overflow:hidden;background:var(--muted)}
|
|
2043
2047
|
.c3 .wk-bar::-webkit-progress-bar{background:var(--muted)}
|
|
2044
2048
|
.c3 .wk-bar::-webkit-progress-value{background:var(--acc);border-radius:6px}
|
|
@@ -110,9 +110,11 @@ export function c3ib(label, icon, attrs, cls) {
|
|
|
110
110
|
|
|
111
111
|
const API = '/api/pm-items';
|
|
112
112
|
const JOBS = '/api/pm-jobs';
|
|
113
|
-
/* The
|
|
114
|
-
|
|
115
|
-
const
|
|
113
|
+
/* The seven views (initiative console3-work-page, PRD R1): five places an open item can be, the weekly Review pass, and Done:
|
|
114
|
+
* what the PM finished in the last 30 days (c3-work-agent amendment 18). */
|
|
115
|
+
const LENSES = [['inbox', 'Inbox'], ['today', 'Today'], ['upcoming', 'Upcoming'], ['waiting', 'Waiting'], ['someday', 'Someday'], ['review', 'Review'], ['done', 'Done']];
|
|
116
|
+
const WLI = { inbox: 'inbox', today: 'sun', upcoming: 'cal', waiting: 'wait', someday: 'moon', review: 'check', done: 'todo' };
|
|
117
|
+
const DONE_DAYS = 30;
|
|
116
118
|
const WHO = ['you', 'agent-drafts', 'agent-does'];
|
|
117
119
|
const OUTWARD = /\b(send|sends|sending|email|emails|message|messages|tell|post|reply to)\b/i; // mirrors lib/item-steps.js
|
|
118
120
|
const RESOLVES_TIP = 'Your work. The team task is separate: close it in its initiative.';
|
|
@@ -135,6 +137,7 @@ export const COPY = Object.freeze({
|
|
|
135
137
|
lens: {
|
|
136
138
|
inbox: 'Decide when or whether', today: 'Overdue, due today and flagged', upcoming: 'The next 7 days, then anything later',
|
|
137
139
|
waiting: 'Things you handed to someone', someday: 'Parked on purpose', review: 'A weekly pass over items without a date',
|
|
140
|
+
done: 'What you finished',
|
|
138
141
|
},
|
|
139
142
|
lensSub: {
|
|
140
143
|
inbox: 'Nothing here has a date. Tap Decide to give each item a day, park it or drop it.',
|
|
@@ -143,6 +146,7 @@ export const COPY = Object.freeze({
|
|
|
143
146
|
waiting: 'You expect these back. Nudge when one goes quiet.',
|
|
144
147
|
someday: 'Nothing here asks for attention until the weekly Review.',
|
|
145
148
|
review: 'Items in Inbox, Waiting or Someday that nobody touched for 7 days, most stale first. One at a time: keep it, change it, or drop it.',
|
|
149
|
+
done: 'The last 30 days, newest first, with the outcome you wrote. Reopen puts an item back where it was.',
|
|
146
150
|
},
|
|
147
151
|
sect: { inbox: 'Inbox', waiting: 'Waiting', someday: 'Someday', overdue: 'Overdue', dueToday: 'Due today', flagged: 'Flagged', next7: 'Next 7 days', later: 'Later' },
|
|
148
152
|
decisions: (n) => ` ${n} decision${n === 1 ? '' : 's'} wait${n === 1 ? 's' : ''} in Briefing, Needs you.`, openBriefing: 'Open Briefing',
|
|
@@ -153,7 +157,13 @@ export const COPY = Object.freeze({
|
|
|
153
157
|
emptyView: {
|
|
154
158
|
inbox: 'Inbox is empty. Capture anything that crosses your mind.', today: 'Nothing due, overdue or flagged. Pick from Upcoming when you are ready.',
|
|
155
159
|
upcoming: 'Nothing dated in the days ahead.', waiting: 'Nobody owes you anything right now.', someday: 'Nothing parked.',
|
|
160
|
+
done: 'Nothing done in the last 30 days. Items you mark done land here.',
|
|
156
161
|
},
|
|
162
|
+
/* Done (c3-work-agent amendment 18) */
|
|
163
|
+
doneSum: (w, m) => `${w} done in the last 7 days, ${m} in 30 days. Older items stay in their job's sheet.`,
|
|
164
|
+
doneSect: { today: 'Today', yday: 'Yesterday', week: 'Earlier this week', older: 'Earlier, last 30 days' }, doneLegend: 'Reopen puts it back',
|
|
165
|
+
doneAt: (t) => 'Done ' + t, reopen: 'Reopen', backTo: (v) => 'Back to ' + v, reopened: (v) => `Reopened. It is back in ${v}.`,
|
|
166
|
+
how: { you: 'You marked it done', agent: 'Closed from the agent draft', posted: 'Agent posted it', step: 'Agent did the step you approved', confirmed: 'You confirmed it' },
|
|
157
167
|
done: 'Done', doneToast: 'Marked done.', steps: 'Steps', noSteps: 'No steps yet.', addStep: 'Add a step',
|
|
158
168
|
who: { you: 'You', 'agent-drafts': 'Agent drafts', 'agent-does': 'Agent does' },
|
|
159
169
|
whoOutward: 'Sends to a person: the agent drafts, you slide to send.', options: 'Options', plan: 'Plan the steps',
|
|
@@ -194,8 +204,9 @@ function applyOv(base, ov) {
|
|
|
194
204
|
const jobOfItem = {};
|
|
195
205
|
const wasDone = new Set((d.items || []).filter((i) => i.done).map((i) => i.id));
|
|
196
206
|
const items = [...ov.added.slice().reverse(), ...(d.items || [])].filter((i) => !ov.gone.has(i.id)).map((i) => {
|
|
197
|
-
const
|
|
198
|
-
m
|
|
207
|
+
const e = ov.edits.get(i.id) || {};
|
|
208
|
+
const m = { ...i, ...e };
|
|
209
|
+
m.done = e.done !== undefined ? !!e.done : !!(i.done || ov.done.has(i.id)); // a reopen (done: false) also reopens a fixture's done item
|
|
199
210
|
m.steps = [...(i.steps || []), ...(ov.steps.get(i.id) || [])].map((s) => (ov.who.has(s.id) ? { ...s, who: ov.who.get(s.id) } : s));
|
|
200
211
|
jobOfItem[m.id] = m.job;
|
|
201
212
|
return m;
|
|
@@ -229,8 +240,8 @@ function applyLocal(inst, path, body, r) {
|
|
|
229
240
|
const now = new Date().toISOString();
|
|
230
241
|
const edit = (f) => ov.edits.set(id, { ...(ov.edits.get(id) || {}), ...(f || {}), touched_at: now }); // the server stamps touched_at on every event
|
|
231
242
|
switch (path.replace(API, '').replace(JOBS, '/job')) {
|
|
232
|
-
case '/done': ov.done.add(id); edit(body.outcome ? { outcome: body.outcome } : {}); break;
|
|
233
|
-
case '/reopen': ov.done.delete(id); edit(); break;
|
|
243
|
+
case '/done': ov.done.add(id); edit({ done: true, done_at: new Date(today + 'T' + new Date().toTimeString().slice(0, 8)).toISOString(), done_how: body.how || 'you', ...(body.outcome ? { outcome: body.outcome } : {}) }); break; // done_at: now, on the fixture's day
|
|
244
|
+
case '/reopen': ov.done.delete(id); edit({ done: false, done_at: null, done_how: null }); break;
|
|
234
245
|
case '/park': case '/someday': edit({ parked: true }); break;
|
|
235
246
|
case '/unpark': edit({ parked: false }); break;
|
|
236
247
|
case '/edit': edit(body.fields); break;
|
|
@@ -362,7 +373,8 @@ const undoBy = (inst, route, body) => async () => !!(await press(inst, API + rou
|
|
|
362
373
|
/* Does the change move the item out of the view on screen? Then its row leaves; otherwise it stays and redraws. */
|
|
363
374
|
const leaves = (inst, item, patch) => inst.lens === 'review' || workPlace({ ...item, ...patch }, inst.data.today) !== inst.lens;
|
|
364
375
|
|
|
365
|
-
|
|
376
|
+
/* `how` says how it closed, for the Done view: 'you' (the default), 'agent' (from the agent draft) or 'confirmed' (offline work the PM confirmed). */
|
|
377
|
+
const doneAct = (inst, item, row, text, outcome, how) => act(inst, row, { route: '/done', body: { id: item.id, ...(outcome ? { outcome } : {}), ...(how ? { how } : {}) }, text: text || COPY.doneToast, undo: undoBy(inst, '/reopen', { id: item.id }) });
|
|
366
378
|
const dropAct = (inst, item, row) => act(inst, row, { route: '/remove', body: { id: item.id }, text: COPY.removed, undo: undoBy(inst, '/undo', { id: item.id }) });
|
|
367
379
|
const todayAct = (inst, item, row) => act(inst, row, { route: '/edit', body: { id: item.id, fields: { due: inst.data.today, defer: null } }, text: 'In Today.',
|
|
368
380
|
undo: undoBy(inst, '/edit', { id: item.id, fields: { due: item.due || null, defer: item.defer || null } }), leave: leaves(inst, item, { due: inst.data.today, defer: null }) });
|
|
@@ -490,7 +502,10 @@ function views(inst) {
|
|
|
490
502
|
return { inbox: L.triage || [], today: L.today || [], upcoming: [...(L.week || []), ...(L.later || []).filter((i) => !i.parked)], waiting: L.waiting || [], someday: (L.later || []).filter((i) => i.parked) };
|
|
491
503
|
}
|
|
492
504
|
const shown = (inst, id) => ((id === 'review' ? (inst.data || {}).review : views(inst)[id]) || []).filter((i) => !inst.hidden.has(i.id) && !i.done);
|
|
493
|
-
const countOf = (inst, id) => {
|
|
505
|
+
const countOf = (inst, id) => {
|
|
506
|
+
if (id === 'done') return doneItems(inst).filter((i) => doneAgo(inst, i) <= 0).length; // the Done badge: items done today
|
|
507
|
+
const c = (inst.data || {}).counts; return c && typeof c[id] === 'number' ? c[id] : shown(inst, id).length;
|
|
508
|
+
};
|
|
494
509
|
const itemById = (inst, id) => ((inst.data && inst.data.items) || []).find((i) => i.id === id)
|
|
495
510
|
|| [...VIEWS.map((v) => views(inst)[v] || []).flat(), ...((inst.data && inst.data.review) || [])].find((i) => i.id === id);
|
|
496
511
|
/* The title of the team task a Work item advances (PRD R8): the item's own copy, then a map the answer may carry, else the id. */
|
|
@@ -589,7 +604,7 @@ function offlineBlock(inst, item) {
|
|
|
589
604
|
return el('div', { class: 'wkoff', 'data-pm-offline': item.id },
|
|
590
605
|
el('p', { class: 'wkoff-n' }, c3ic('user', 'sm'), COPY.offlineNote(item.person)),
|
|
591
606
|
item.evidence ? el('div', { class: 'wkev', 'data-pm-evidence': item.id }, c3ic('eye', 'sm'), el('span', { class: 'wk-min' }, el('b', null, COPY.looksDone + ' '), item.evidence),
|
|
592
|
-
pill(COPY.confirm, 'check', { class: 'pill pri', 'data-pm-confirm': item.id, onclick: () => doneAct(inst, item, rowOf(inst, item), 'Done. You confirmed it.') })) : null,
|
|
607
|
+
pill(COPY.confirm, 'check', { class: 'pill pri', 'data-pm-confirm': item.id, onclick: () => doneAct(inst, item, rowOf(inst, item), 'Done. You confirmed it.', undefined, 'confirmed') })) : null,
|
|
593
608
|
el('div', { class: 'wkq' }, el('span', { class: 'form wkoff-o' }, out), pill(COPY.done, 'check', { class: 'pill pri', 'data-pm-offline-done': item.id, onclick: doneNow })),
|
|
594
609
|
el('div', { class: 'wkq' }, el('span', { class: 'dim mono' }, COPY.notYet),
|
|
595
610
|
...[['later-today', COPY.laterToday], ['tomorrow', COPY.tomorrow], ['next-week', COPY.nextWeek]].map(([w, l]) => pill(l, null, { 'data-pm-notyet': w, onclick: () => notYetAct(inst, item, rowOf(inst, item), w) })),
|
|
@@ -807,10 +822,10 @@ function agentCase(inst, item, L) {
|
|
|
807
822
|
// a step in another system replaces the post: the approved send does the step (amendment 17)
|
|
808
823
|
step ? o('step', AG.doIt(step), [fx('lock', AG.stepFx1, 'warn'), fx('out', AG.stepFx2, 'warn')], () => agentPress(inst, '/api/my-work/decide', { item: item.id, action: 'approve' }, AG.doing), { outward: true }) : null,
|
|
809
824
|
mine || step ? null : o('post', AG.post(k), [fx('lock', 'Outward: exactly this text', 'warn'), fx('out', 'Goes to ' + w + ' as you', 'warn')], () => agentPress(inst, '/api/my-work/decide', { item: item.id, action: 'approve' }, AG.posting), { outward: true }),
|
|
810
|
-
mine ? o('done', AG.doneIt, [fx('check', AG.doneFx, 'ok')], (r) => doneAct(inst, item, r, AG.doneToast)) : null,
|
|
825
|
+
mine ? o('done', AG.doneIt, [fx('check', AG.doneFx, 'ok')], (r) => doneAct(inst, item, r, AG.doneToast, undefined, 'agent')) : null,
|
|
811
826
|
o('edit', AG.editDraft, [fx('edit', step ? 'The step then uses your version' : mine ? 'Your version replaces the draft here' : 'Post then sends your version', 'ok')], () => openDraft(item, inst, true)),
|
|
812
827
|
o('keep', AG.keepDraft, [fx('check', 'Nothing leaves PMOS', 'ok')], () => say({ text: mine ? AG.keptMine : AG.kept, timeout: 3000 })),
|
|
813
|
-
mine ? null : o('done', AG.doneIt, [fx('check', AG.doneFx, 'ok')], (r) => doneAct(inst, item, r, AG.doneToast)),
|
|
828
|
+
mine ? null : o('done', AG.doneIt, [fx('check', AG.doneFx, 'ok')], (r) => doneAct(inst, item, r, AG.doneToast, undefined, 'agent')),
|
|
814
829
|
].filter(Boolean);
|
|
815
830
|
extras.push(draftCard(inst, item, k, false), conversation(inst, item, false, w));
|
|
816
831
|
before = el('div', { class: 'wk-conv-h', 'data-pm-agent-decide': item.id }, el('span', { class: 'dim mono' }, c3ic('check', 'sm'), AG.orDecide));
|
|
@@ -1376,6 +1391,71 @@ function openNewJob(inst) {
|
|
|
1376
1391
|
later(inst, () => input.focus(), 30);
|
|
1377
1392
|
}
|
|
1378
1393
|
|
|
1394
|
+
/* ── Done (c3-work-agent amendment 18): what the PM finished in the last 30 days, by day, with how it closed and Reopen ── */
|
|
1395
|
+
|
|
1396
|
+
const localDay = (iso) => { const d = new Date(iso); return Number.isNaN(d.getTime()) ? '' : d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0'); };
|
|
1397
|
+
/* Days between the day an item was done and today: 0 = today. */
|
|
1398
|
+
const doneAgo = (inst, i) => daysBetween(localDay(i.done_at), inst.data.today);
|
|
1399
|
+
function doneItems(inst) {
|
|
1400
|
+
if (!inst.data) return [];
|
|
1401
|
+
return (inst.data.items || []).filter((i) => i.done && i.done_at && localDay(i.done_at) && doneAgo(inst, i) <= DONE_DAYS && !inst.hidden.has(i.id))
|
|
1402
|
+
.sort((a, b) => String(b.done_at).localeCompare(String(a.done_at)));
|
|
1403
|
+
}
|
|
1404
|
+
/* How it closed: what the agent did wins (it posted, or did the approved step), else what the done press said. */
|
|
1405
|
+
const DONE_HOW_IC = { you: 'check', agent: 'spark', posted: 'out', step: 'spark', confirmed: 'eye' };
|
|
1406
|
+
function howOf(item) {
|
|
1407
|
+
const a = item.agent;
|
|
1408
|
+
if (a && a.state === 'sent') return a.draft && a.draft.step ? 'step' : 'posted';
|
|
1409
|
+
return DONE_HOW_IC[item.done_how] ? item.done_how : 'you';
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
function doneRow(inst, item) {
|
|
1413
|
+
const today = inst.data.today;
|
|
1414
|
+
const how = howOf(item);
|
|
1415
|
+
const was = workPlace({ ...item, done: false }, today);
|
|
1416
|
+
const chips = [el('span', { class: 'st ok', 'data-pm-done-at': item.id }, c3ic('check', 'sm'), COPY.doneAt(doneAgo(inst, item) <= 0 ? hhmm(item.done_at) : dayLabel(localDay(item.done_at)))),
|
|
1417
|
+
el('span', { class: 'chip2', 'data-pm-done-how': how }, c3ic(DONE_HOW_IC[how], 'sm'), COPY.how[how])];
|
|
1418
|
+
if (item.origin === 'jira' || item.origin === 'wbs') {
|
|
1419
|
+
const label = (item.origin === 'jira' ? 'Jira ' + (item.key || '') : 'WBS' + (item.key ? ' ' + item.key : '')).trim();
|
|
1420
|
+
chips.push(item.url ? el('a', { class: 'tag', href: item.url, target: '_blank', rel: 'noopener' }, label) : el('span', { class: 'tag' }, label));
|
|
1421
|
+
} else if (item.origin === 'brief') chips.push(el('span', { class: 'tag' }, COPY.suggested));
|
|
1422
|
+
if (item.kind === 'offline' && item.person) chips.push(el('span', { class: 'chip2' }, c3ic('user', 'sm'), item.person));
|
|
1423
|
+
if (item.resolves) chips.push(el('span', { class: 'chip2', title: RESOLVES_TIP }, c3ic('init', 'sm'), resolvesTitle(inst, item)));
|
|
1424
|
+
const row = el('li', { class: 'nrow wrow wk-done-row', 'data-pm-done-item': item.id });
|
|
1425
|
+
const reopen = pill(COPY.reopen, 'undo', { 'data-pm-reopen': item.id, title: COPY.backTo(labelOf(was)),
|
|
1426
|
+
onclick: (e) => { e.stopPropagation(); act(inst, row, { route: '/reopen', body: { id: item.id }, text: COPY.reopened(labelOf(was)), undo: undoBy(inst, '/done', { id: item.id, ...(item.done_how ? { how: item.done_how } : {}) }) }); } });
|
|
1427
|
+
row.append(el('span', { class: 'ki' }, c3ic('check')),
|
|
1428
|
+
el('div', { class: 'nb' }, el('span', { class: 'tt', 'data-pm-title': item.id }, item.title),
|
|
1429
|
+
item.outcome ? el('div', { class: 'wk-done-out', 'data-pm-done-outcome': item.id }, c3ic('reply', 'sm'), item.outcome) : null,
|
|
1430
|
+
el('div', { class: 'chips' }, chips)),
|
|
1431
|
+
el('div', { class: 'acts' }, reopen));
|
|
1432
|
+
return row;
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
function doneMain(inst) {
|
|
1436
|
+
const all = doneItems(inst);
|
|
1437
|
+
if (!all.length) {
|
|
1438
|
+
const sec = sect(el('div', { class: 'sh2' }, el('h2', null, c3ic('todo', 'sm'), labelOf('done'), ' ', el('span', { class: 'bdg g' }, '0'))),
|
|
1439
|
+
el('ul', { class: 'list' }, el('li', { class: 'fold', 'data-pm-done-empty': '' }, c3ic('check', 'sm'), COPY.emptyView.done)));
|
|
1440
|
+
sec.setAttribute('data-pm-list', 'done');
|
|
1441
|
+
return [sec];
|
|
1442
|
+
}
|
|
1443
|
+
const g = { today: [], yday: [], week: [], older: [] };
|
|
1444
|
+
all.forEach((i) => { const n = doneAgo(inst, i); (n <= 0 ? g.today : n === 1 ? g.yday : n <= 7 ? g.week : g.older).push(i); });
|
|
1445
|
+
const week = all.filter((i) => doneAgo(inst, i) <= 7).length;
|
|
1446
|
+
const out = [el('div', { class: 'fold wk-done-sum', 'data-pm-done-sum': '' }, c3ic('check', 'sm'), COPY.doneSum(week, all.length))];
|
|
1447
|
+
let first = true;
|
|
1448
|
+
[['today', 'sun'], ['yday', 'clock'], ['week', 'cal'], ['older', 'moon']].filter(([k]) => g[k].length).forEach(([k, ic]) => {
|
|
1449
|
+
const sec = sect(el('div', { class: 'sh2' }, el('h2', null, c3ic(ic, 'sm'), COPY.doneSect[k], ' ', el('span', { class: 'bdg g' }, String(g[k].length))),
|
|
1450
|
+
first ? el('span', { class: 'dim wk-sm' }, COPY.doneLegend) : null),
|
|
1451
|
+
el('ul', { class: 'list', role: 'list' }, g[k].map((i) => doneRow(inst, i))));
|
|
1452
|
+
first = false;
|
|
1453
|
+
sec.setAttribute('data-pm-list', 'done'); sec.setAttribute('data-pm-sect', k);
|
|
1454
|
+
out.push(sec);
|
|
1455
|
+
});
|
|
1456
|
+
return out;
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1379
1459
|
/* ── the page ────────────────────────────────────────────────────────────────────────────────────────────────────── */
|
|
1380
1460
|
|
|
1381
1461
|
/* The mockup's workHead() calbar: the .seg view bar with a count on every view (line 3281). */
|
|
@@ -1436,6 +1516,7 @@ function draw(inst) {
|
|
|
1436
1516
|
if (lens === 'review') main = [reviewMain(inst), inst.draftsHost];
|
|
1437
1517
|
else if (lens === 'upcoming') main = upcomingMain(inst, list);
|
|
1438
1518
|
else if (lens === 'today') main = todayMain(inst, list);
|
|
1519
|
+
else if (lens === 'done') main = doneMain(inst);
|
|
1439
1520
|
else {
|
|
1440
1521
|
const sec = sect(el('div', { class: 'sh2' }, el('h2', null, c3ic(WLI[lens], 'sm'), COPY.sect[lens], ' ', el('span', { class: 'bdg g' }, String(list.length))), legendEl()),
|
|
1441
1522
|
el('ul', { class: 'list', role: 'list' }, list.length ? list.map((i) => itemRow(inst, i, lens)) : empty));
|
package/workspace/server.js
CHANGED
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
* under state/ (the reconcile seam). Request headers are never logged (D08).
|
|
21
21
|
*/
|
|
22
22
|
'use strict';
|
|
23
|
+
// Windows: hide the console window of every command the server runs. First, before anything holds child_process.
|
|
24
|
+
require('./lib/win-hide');
|
|
23
25
|
// The workspace reads git in the background (repo-watch, git-info, claims), and a plain `git status` takes the index lock to
|
|
24
26
|
// refresh the index. A press that writes, such as Sync's `git add`, then fails on `index.lock: File exists` when the two
|
|
25
27
|
// meet. With optional locks off, the server's reads never take that lock. Set only when the PM has not set it, and taken back
|