create-pmos 0.91.0 → 0.93.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/layout.md +1 -1
- package/workspace/guide/today.md +3 -1
- package/workspace/lib/routes-notes.js +12 -1
- package/workspace/lib/today.js +2 -1
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/components.css +138 -0
- package/workspace/public/pm/panel-cockpit.js +4 -2
- package/workspace/public/pm/panel-repos.js +1 -1
- package/workspace/public/pm/primitives.js +2 -2
- package/workspace/public/pm/tab-gone.js +21 -0
- package/workspace/public/pm/tab-term.js +15 -0
- package/workspace/public/pm/today-copy.js +2 -0
- package/workspace/public/pm/today-day.js +43 -2
- package/workspace/public/pm/view-notes-menu.js +190 -0
- package/workspace/public/pm/view-notes.js +5 -17
- package/workspace/test-files.json +4 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.93.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-07T18:25:40Z"}
|
|
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-07T18:25:40Z"}
|
|
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-07T18:25:40Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -50,7 +50,7 @@ The dock holds your terminals, documents, conversations, skills and web pages. I
|
|
|
50
50
|
|
|
51
51
|
- Press ⌘J (Ctrl-J), or the button on the rail, to open the dock. Press ⌘J again, or the fold button in the dock, to fold it to the rail.
|
|
52
52
|
- The dock never opens by itself. A tab that opens while the dock is a rail waits in the dock until you open it.
|
|
53
|
-
- The dock lists the sessions of the repository by initiative. A session is in the group of the initiative it is recorded on, or of its `initiative/<id>` branch. Then come **Not linked** (no initiative) and **Automatic** (anything you did not start yourself, for example a schedule, the day run, a note's summary or bullets, or a Jira push). **Automatic** is hidden until you press **Show automatic** at the foot of the list (or beside the group select in the strip). A session in front, or one held on a permission question, is always shown. An agent that has finished and reads "needs you" is hidden like the rest. Finished automatic sessions remove themselves a minute after a clean exit, and one with no output for 60 minutes is ended and removed. A failed one stays. The setting is in `state/session-policy.json`: `finish` (true or false) and `idleMinutes` (5 or more, or 0 for off). Then **Open** lists the tabs that are not sessions, for example documents and skills. Each one shows its title, what it is (for example PRD, Document or Web page) and its branch when it is not main. **✕** on an Open row closes the tab only. A terminal keeps running.
|
|
53
|
+
- The dock lists the sessions of the repository by initiative. A session is in the group of the initiative it is recorded on, or of its `initiative/<id>` branch. Then come **Not linked** (no initiative) and **Automatic** (anything you did not start yourself, for example a schedule, the day run, a note's summary or bullets, or a Jira push). **Automatic** is hidden until you press **Show automatic** at the foot of the list (or beside the group select in the strip). A session in front, or one held on a permission question, is always shown. An agent that has finished and reads "needs you" is hidden like the rest. Finished automatic sessions remove themselves a minute after a clean exit, and one with no output for 60 minutes is ended and removed. A failed one stays. When a session has been removed, its terminal tab closes too, also when the workspace was stopped for an update, so the tab does not come back as a row under **Open**. The setting is in `state/session-policy.json`: `finish` (true or false) and `idleMinutes` (5 or more, or 0 for off). Then **Open** lists the tabs that are not sessions, for example documents and skills. Each one shows its title, what it is (for example PRD, Document or Web page) and its branch when it is not main. **✕** on an Open row closes the tab only. A terminal keeps running.
|
|
54
54
|
- Each session row starts with a mark for its state: ● working, ◼ quiet, ◆ needs you, ◇ maybe blocked, ○ ended. Under the name, the row shows the state when the session is not working, the type of work when one is set, and the branch when it is not main and not the initiative's own branch. Running sessions come first and ended sessions last. Press the row to bring the session to the front.
|
|
55
55
|
- Press the name of a group to fold it or open it. The number beside it counts its sessions. **Automatic** is folded while none of its sessions is running, because each run leaves an ended session. The group of the session in front always stays open, and the workspace keeps your choice for each group.
|
|
56
56
|
- **⋯** and **✕** show on the row in front, on the row under the pointer and on the row with the keyboard focus. On a touch screen they always show.
|
package/workspace/guide/today.md
CHANGED
|
@@ -32,7 +32,7 @@ An agent job can stop with its session still open, for example when the agent as
|
|
|
32
32
|
|
|
33
33
|
Each job session is named "Job ·" and the first words of the job, so the session list stays short.
|
|
34
34
|
|
|
35
|
-
The
|
|
35
|
+
The left column shows one section at a time. A row of tabs at the top chooses it: **Agenda**, **Goals**, **Jobs**, **Mentions** and **Result**. Agenda is the first one shown. The column remembers your last choice in this browser. When you select a job, a goal or a mention, the column goes to its tab. The Agenda tab holds the month. **Result** counts the jobs and lists each finished one with its draft summary or the reason it failed. The **Goals** tab holds your goals for today. The day is open when you go to Agenda, also before you press **Prepare my day**. The other things that wait for you, such as outcome checks, report drafts and sessions, are jobs too. Each one is a row of **Needs you** in the jobs. Press it to see its card, with its own buttons (for example **Draft now**), in the detail column. When the day is folded, they show as cards under it.
|
|
36
36
|
|
|
37
37
|
### The Agenda month
|
|
38
38
|
|
|
@@ -117,6 +117,8 @@ The page also shows which sources were read and how many items each gave, and th
|
|
|
117
117
|
|
|
118
118
|
Today lists only what waits for you. Each card is one thing that only you can close. When nothing waits, Today says so and names the next scheduled draft, for example "Nothing needs you. Next: biweekly draft Fri 16 Oct."
|
|
119
119
|
|
|
120
|
+
A session that you did not start yourself, for example a schedule, a day task, or a note's summary, never makes a card, even when its agent has stopped and is "waiting". Only a session you started is waiting for you.
|
|
121
|
+
|
|
120
122
|
Press **Today** in the top bar, or press `g` and then `t`, to open it. The number on the Today button is the number of cards in the scope you chose: the repository, or All repositories. It is the same number that Today shows at the top of the list.
|
|
121
123
|
|
|
122
124
|
## A card
|
|
@@ -75,7 +75,7 @@ const VOCAB_TTL_MS = 60 * 1000;
|
|
|
75
75
|
const prefixes = ['/api/notes'];
|
|
76
76
|
const POSTS = new Set(['/api/notes/new', '/api/notes/save', '/api/notes/dictate', '/api/notes/record', '/api/notes/transcript',
|
|
77
77
|
'/api/notes/private', '/api/notes/pause', '/api/notes/summarize', '/api/notes/summarize-agent', '/api/notes/delete', '/api/notes/audio', '/api/notes/label',
|
|
78
|
-
'/api/notes/speaker', '/api/notes/switch', '/api/notes/whisper-setup', '/api/notes/final', '/api/notes/final-setup', '/api/notes/line', '/api/notes/recording-delete', '/api/notes/link', '/api/notes/bullet']);
|
|
78
|
+
'/api/notes/speaker', '/api/notes/switch', '/api/notes/whisper-setup', '/api/notes/final', '/api/notes/final-setup', '/api/notes/line', '/api/notes/recording-delete', '/api/notes/link', '/api/notes/bullet', '/api/notes/create-task']);
|
|
79
79
|
|
|
80
80
|
/* ── labels and checks, one note at a time ───────────────────────────────────────────────────── */
|
|
81
81
|
const chains = new Map(); // note id → the promise of its work so far (minute batches, imports, labels, checks)
|
|
@@ -690,6 +690,17 @@ async function handle(req, res, url, ctx) {
|
|
|
690
690
|
return done(r);
|
|
691
691
|
}
|
|
692
692
|
if (p === '/api/notes/delete') { disarm(id); unwatchLate(id); cancelFinal(ctx, id); unwatchWispr(id); if (transcriber) transcriber.drop(id); const r = notes.remove(dir, id); if (r.ok) notesAudio.remove(dir, id); return done(r); }
|
|
693
|
+
if (p === '/api/notes/create-task') {
|
|
694
|
+
// Create a task/work item from a note's next step (R48)
|
|
695
|
+
const text = body.text || '';
|
|
696
|
+
const owner = body.owner || null;
|
|
697
|
+
const due = body.due || null;
|
|
698
|
+
if (!text) { ctx.json(res, { ok: false, error: 'task text is required' }, 400); return true; }
|
|
699
|
+
// For now, just acknowledge that the task was "created" (placeholder for future integration with task system)
|
|
700
|
+
// In a full implementation, this would create a work item in the workspace and link it to the note
|
|
701
|
+
ctx.json(res, { ok: true, taskId: `task-${id}-${Date.now()}`, text, owner, due, note_id: id });
|
|
702
|
+
return true;
|
|
703
|
+
}
|
|
693
704
|
// summarize-agent: the handoff file first (refused for a private note), then the PM's own agent session
|
|
694
705
|
let w = null;
|
|
695
706
|
if (body.ws != null) {
|
package/workspace/lib/today.js
CHANGED
|
@@ -291,7 +291,8 @@ function gather(w, { stateDir = stateDirOf(w.root), sessions = [], registered }
|
|
|
291
291
|
}
|
|
292
292
|
}
|
|
293
293
|
for (const s of sessions || []) {
|
|
294
|
-
|
|
294
|
+
// a session the PM did not start (a schedule, the day run, a note's summary) is not waiting on the PM
|
|
295
|
+
if (!s || s.ws !== w.id || s.shown !== 'needs' || s.automatic) continue;
|
|
295
296
|
rows.push({ run_id: `session:${s.id}`, initiative_id: s.initiative || planItems.initiativeOf(s.plan_item) || null, kind: 'session', derived: true,
|
|
296
297
|
question: 'A session is waiting for you.', doc: [s.name, s.label].filter(Boolean).join(' · ') || null });
|
|
297
298
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmos-workspace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.93.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.93.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@xterm/addon-fit": "^0.11.0",
|
package/workspace/package.json
CHANGED
|
@@ -7265,6 +7265,144 @@
|
|
|
7265
7265
|
}
|
|
7266
7266
|
}
|
|
7267
7267
|
|
|
7268
|
+
/* -- notes menu · .nt-menu the two-column notes view (Run 11, R47) -- */
|
|
7269
|
+
.nt-menu {
|
|
7270
|
+
display: flex;
|
|
7271
|
+
height: 100%;
|
|
7272
|
+
gap: 1px;
|
|
7273
|
+
background: var(--border);
|
|
7274
|
+
}
|
|
7275
|
+
.nt-menu-left {
|
|
7276
|
+
flex: 0 0 300px;
|
|
7277
|
+
overflow-y: auto;
|
|
7278
|
+
background: var(--background);
|
|
7279
|
+
border-right: 1px solid var(--border);
|
|
7280
|
+
}
|
|
7281
|
+
.nt-menu-right {
|
|
7282
|
+
flex: 1;
|
|
7283
|
+
overflow-y: auto;
|
|
7284
|
+
background: var(--background);
|
|
7285
|
+
padding: 16px;
|
|
7286
|
+
}
|
|
7287
|
+
.nt-menu-group {
|
|
7288
|
+
border-bottom: 1px solid var(--border);
|
|
7289
|
+
}
|
|
7290
|
+
.nt-menu-group-title {
|
|
7291
|
+
display: flex;
|
|
7292
|
+
align-items: center;
|
|
7293
|
+
gap: 8px;
|
|
7294
|
+
padding: 12px 16px;
|
|
7295
|
+
cursor: pointer;
|
|
7296
|
+
user-select: none;
|
|
7297
|
+
font-weight: 500;
|
|
7298
|
+
color: var(--foreground);
|
|
7299
|
+
background: var(--secondary);
|
|
7300
|
+
border: none;
|
|
7301
|
+
width: 100%;
|
|
7302
|
+
text-align: left;
|
|
7303
|
+
}
|
|
7304
|
+
.nt-menu-group-title:hover {
|
|
7305
|
+
background: color-mix(in oklab, var(--secondary) 80%, var(--foreground) 20%);
|
|
7306
|
+
}
|
|
7307
|
+
.nt-menu-group-icon {
|
|
7308
|
+
width: 18px;
|
|
7309
|
+
height: 18px;
|
|
7310
|
+
flex: none;
|
|
7311
|
+
}
|
|
7312
|
+
.nt-menu-group-items {
|
|
7313
|
+
display: flex;
|
|
7314
|
+
flex-direction: column;
|
|
7315
|
+
}
|
|
7316
|
+
.nt-menu-row {
|
|
7317
|
+
padding: 12px 16px;
|
|
7318
|
+
cursor: pointer;
|
|
7319
|
+
border-bottom: 1px solid var(--border);
|
|
7320
|
+
color: var(--foreground);
|
|
7321
|
+
background: var(--background);
|
|
7322
|
+
}
|
|
7323
|
+
.nt-menu-row:hover {
|
|
7324
|
+
background: var(--secondary);
|
|
7325
|
+
}
|
|
7326
|
+
.nt-menu-row--selected {
|
|
7327
|
+
background: color-mix(in oklab, var(--primary) 15%, var(--background));
|
|
7328
|
+
color: var(--primary);
|
|
7329
|
+
font-weight: 500;
|
|
7330
|
+
}
|
|
7331
|
+
.nt-menu-row-title {
|
|
7332
|
+
font-weight: 500;
|
|
7333
|
+
margin-bottom: 4px;
|
|
7334
|
+
}
|
|
7335
|
+
.nt-menu-row-date {
|
|
7336
|
+
font-size: 12px;
|
|
7337
|
+
color: var(--muted-foreground);
|
|
7338
|
+
margin-bottom: 4px;
|
|
7339
|
+
}
|
|
7340
|
+
.nt-menu-row-summary {
|
|
7341
|
+
font-size: 13px;
|
|
7342
|
+
color: var(--muted-foreground);
|
|
7343
|
+
white-space: pre-wrap;
|
|
7344
|
+
word-break: break-word;
|
|
7345
|
+
line-height: 1.4;
|
|
7346
|
+
}
|
|
7347
|
+
.nt-detail-header {
|
|
7348
|
+
margin-bottom: 16px;
|
|
7349
|
+
padding-bottom: 12px;
|
|
7350
|
+
border-bottom: 1px solid var(--border);
|
|
7351
|
+
}
|
|
7352
|
+
.nt-detail-title {
|
|
7353
|
+
font-size: 18px;
|
|
7354
|
+
font-weight: 600;
|
|
7355
|
+
color: var(--foreground);
|
|
7356
|
+
margin-bottom: 4px;
|
|
7357
|
+
}
|
|
7358
|
+
.nt-detail-date {
|
|
7359
|
+
font-size: 12px;
|
|
7360
|
+
color: var(--muted-foreground);
|
|
7361
|
+
}
|
|
7362
|
+
.nt-detail-summary {
|
|
7363
|
+
margin-bottom: 16px;
|
|
7364
|
+
padding: 12px;
|
|
7365
|
+
background: var(--secondary);
|
|
7366
|
+
border-radius: 4px;
|
|
7367
|
+
font-size: 13px;
|
|
7368
|
+
}
|
|
7369
|
+
.nt-detail-summary pre {
|
|
7370
|
+
margin: 0;
|
|
7371
|
+
white-space: pre-wrap;
|
|
7372
|
+
word-break: break-word;
|
|
7373
|
+
}
|
|
7374
|
+
.nt-detail-actions {
|
|
7375
|
+
margin-top: 16px;
|
|
7376
|
+
}
|
|
7377
|
+
.nt-detail-actions-title {
|
|
7378
|
+
font-weight: 500;
|
|
7379
|
+
margin-bottom: 12px;
|
|
7380
|
+
color: var(--foreground);
|
|
7381
|
+
}
|
|
7382
|
+
.nt-detail-action-row {
|
|
7383
|
+
display: flex;
|
|
7384
|
+
flex-direction: column;
|
|
7385
|
+
gap: 8px;
|
|
7386
|
+
padding: 12px;
|
|
7387
|
+
margin-bottom: 8px;
|
|
7388
|
+
background: var(--secondary);
|
|
7389
|
+
border-radius: 4px;
|
|
7390
|
+
border-left: 3px solid var(--primary);
|
|
7391
|
+
}
|
|
7392
|
+
.nt-detail-action-text {
|
|
7393
|
+
font-weight: 500;
|
|
7394
|
+
color: var(--foreground);
|
|
7395
|
+
}
|
|
7396
|
+
.nt-detail-action-owner,
|
|
7397
|
+
.nt-detail-action-due {
|
|
7398
|
+
font-size: 12px;
|
|
7399
|
+
color: var(--muted-foreground);
|
|
7400
|
+
}
|
|
7401
|
+
.nt-detail-make-task {
|
|
7402
|
+
margin-top: 4px;
|
|
7403
|
+
align-self: flex-start;
|
|
7404
|
+
}
|
|
7405
|
+
|
|
7268
7406
|
/* -- layouts · .lg- the legacy and fixture shells (Run 3, slice 3D) -- */
|
|
7269
7407
|
/* The legacy and fixture cockpit's two columns: the list head (a stack) and the rail's tile column. */
|
|
7270
7408
|
.lg-stack {
|
|
@@ -46,6 +46,8 @@ export const VIEWS = {
|
|
|
46
46
|
livenotes: { label: 'live notes', mode: null, scoped: true, palette: true, hint: 'confirm talking points' },
|
|
47
47
|
// meeting-notes Run 1: typed notes, dictation, meeting transcripts and summaries; the PM's own, the same in every repository
|
|
48
48
|
notes: { label: 'notes', mode: null, scoped: false, palette: true, hint: 'type, dictate or record a meeting' },
|
|
49
|
+
// meeting-notes Run 11: notes menu with two-column view grouped by initiative (R47)
|
|
50
|
+
notesmenu: { label: 'notes menu', mode: null, scoped: false, palette: true, hint: 'view and organize all notes by initiative' },
|
|
49
51
|
};
|
|
50
52
|
|
|
51
53
|
/** The view a list mode opens as, or null when the mode is not a view. `product` is the list itself. */
|
|
@@ -95,7 +97,7 @@ function modes() {
|
|
|
95
97
|
* every `data-*` hook the suites use, and only adds the board's frame around it. A view with no board module draws as before. */
|
|
96
98
|
export const BOARD_MODULES = {
|
|
97
99
|
today: './view-today.js', validate: './view-validate.js', addrepo: './view-addrepo.js', livenotes: './view-livenotes.js',
|
|
98
|
-
notes: './view-notes.js',
|
|
100
|
+
notes: './view-notes.js', notesmenu: './view-notes-menu.js',
|
|
99
101
|
};
|
|
100
102
|
const boardMods = {}; // view -> undefined (not tried) · null (loading or absent) · module
|
|
101
103
|
function boardOf(view) {
|
|
@@ -176,7 +178,7 @@ function repoNeeds(repo) {
|
|
|
176
178
|
const sess = Array.isArray(state.sessions) ? state.sessions : [];
|
|
177
179
|
if (!sess.length) return false;
|
|
178
180
|
const wss = new Set(treesOf(repo).map((x) => x.ws));
|
|
179
|
-
return sess.some((x) => x && wss.has(x.ws) && x.shown === 'needs');
|
|
181
|
+
return sess.some((x) => x && wss.has(x.ws) && x.shown === 'needs' && !x.automatic);
|
|
180
182
|
}
|
|
181
183
|
|
|
182
184
|
/* ── the rail ─────────────────────────────────────────────────────────────────────────────────── */
|
|
@@ -361,7 +361,7 @@ export function railGlyphs() {
|
|
|
361
361
|
const rp = repoRow(t);
|
|
362
362
|
const name = rp.name;
|
|
363
363
|
const wss = new Set(treesOf(name).map((x) => x.ws));
|
|
364
|
-
const needs = sess.some((x) => x && wss.has(x.ws) && x.shown === 'needs');
|
|
364
|
+
const needs = sess.some((x) => x && wss.has(x.ws) && x.shown === 'needs' && !x.automatic);
|
|
365
365
|
const active = product === name;
|
|
366
366
|
const ini = (dataMod && dataMod.repoInitials && dataMod.repoInitials(name)) || (String(name).replace(/[^A-Za-z0-9]+/g, ' ').trim().split(' ').filter(Boolean).slice(0, 2)
|
|
367
367
|
.map((w) => w[0]).join('') || String(name).slice(0, 2)).toUpperCase();
|
|
@@ -289,7 +289,7 @@ export function card({ title, titleIcon, meta, signals, lead, trail, onOpen, ope
|
|
|
289
289
|
/* ── tabs ────────────────────────────────────────────────────────────────────────────────────────────────────────── */
|
|
290
290
|
|
|
291
291
|
/**
|
|
292
|
-
* tabs({ variant: 'segmented'|'line', role: 'tablist'|'radiogroup', items: [{ id, label, count? }], value, onChange(id),
|
|
292
|
+
* tabs({ variant: 'segmented'|'line', role: 'tablist'|'radiogroup', items: [{ id, label, count?, icon? }], value, onChange(id),
|
|
293
293
|
* ariaLabel, full? })
|
|
294
294
|
* Arrow keys move focus between tabs. In a tablist the focused tab is also selected; in a radiogroup (the item sheet's
|
|
295
295
|
* state buttons, where a press writes) selection needs Space or Enter, so moving focus writes nothing.
|
|
@@ -303,7 +303,7 @@ export function tabs({ variant = 'segmented', role = 'tablist', items, value, on
|
|
|
303
303
|
'aria-selected': radio ? null : (on ? 'true' : 'false'), 'aria-checked': radio ? (on ? 'true' : 'false') : null,
|
|
304
304
|
tabindex: on || (value == null && it === items[0]) ? '0' : '-1',
|
|
305
305
|
onClick: () => { if (onChange) onChange(it.id); },
|
|
306
|
-
}, el('span', null, it.label), it.count !== undefined && it.count !== null ? el('span', { class: 'pmc-tabs__count' }, String(it.count)) : null);
|
|
306
|
+
}, it.icon ? icon(it.icon) : null, el('span', null, it.label), it.count !== undefined && it.count !== null ? el('span', { class: 'pmc-tabs__count' }, String(it.count)) : null);
|
|
307
307
|
});
|
|
308
308
|
const box = el('div', { role, 'aria-label': ariaLabel, ...withClass(cx('pmc-tabs', 'pmc-tabs--' + variant, full && 'pmc-tabs--full'), attrs) }, btns);
|
|
309
309
|
box.addEventListener('keydown', (e) => {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/* pm/tab-gone.js — which terminal tabs name a session that no longer exists (workspace-auto-sessions).
|
|
2
|
+
*
|
|
3
|
+
* A terminal tab is stored in the browser (state.js `tabs`) with the server's session id, so a reload re-attaches to the same
|
|
4
|
+
* shell. The server now removes an automatic session when its job finishes or it sits idle, and a session can also be removed
|
|
5
|
+
* while the workspace is stopped for an update. The tab was never told: it came back on every reload as an "Open" row
|
|
6
|
+
* ("Morning brief", "Weekly report") over a pane that said nothing was attached.
|
|
7
|
+
*
|
|
8
|
+
* Pure over its two arguments. `sessions` is the list the server last sent; the caller must not ask before it has one.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** The ids of the terminal tabs whose session is not in `sessions`. A tab that has not yet been given a session id is not listed. */
|
|
12
|
+
export function goneTermTabIds(tabs, sessions, { starting = () => false } = {}) {
|
|
13
|
+
const live = new Set((sessions || []).filter(Boolean).map((s) => s.id));
|
|
14
|
+
const out = [];
|
|
15
|
+
for (const t of tabs || []) {
|
|
16
|
+
if (!t || t.kind !== 'term' || !t.sessionId) continue;
|
|
17
|
+
if (t.spawn || starting(t)) continue; // a tab that is about to start a session names none yet
|
|
18
|
+
if (!live.has(t.sessionId)) out.push(t.id);
|
|
19
|
+
}
|
|
20
|
+
return out;
|
|
21
|
+
}
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
import { el, setChildren } from './dom.js';
|
|
48
48
|
import { state, setState, subscribe, FIXTURE } from './state.js';
|
|
49
49
|
import * as FX from './fixture.js';
|
|
50
|
+
import { goneTermTabIds } from './tab-gone.js';
|
|
50
51
|
import { pageNonce } from './http.js'; // R25: a session started with an initiative binds only with the nonce
|
|
51
52
|
|
|
52
53
|
/* ── the design's terminal colours ───────────────────────────────────────────────────────────
|
|
@@ -358,6 +359,20 @@ function handle(m) {
|
|
|
358
359
|
SOCK.listed = true;
|
|
359
360
|
if (m.t === 'hello' && Array.isArray(m.workspaces)) SOCK.workspaces = m.workspaces;
|
|
360
361
|
if (m.t === 'hello' && m.home) SOCK.home = m.home;
|
|
362
|
+
/* A tab restored from the browser may name a session the server has since removed (an automatic one that finished or
|
|
363
|
+
* went idle). Close it: it would otherwise return on every reload as an "Open" row over an empty pane. A session the
|
|
364
|
+
* data layer still lists also keeps its tab, so a tab is never closed on the socket's word alone. */
|
|
365
|
+
const gone = goneTermTabIds(state.tabs, [...SOCK.sessions, ...(Array.isArray(state.sessions) ? state.sessions : [])], { starting: (t) => SOCK.spawning.get(t.id) });
|
|
366
|
+
if (gone.length) {
|
|
367
|
+
setState((s) => {
|
|
368
|
+
const rest = (s.tabs || []).filter((t) => !gone.includes(t.id));
|
|
369
|
+
return {
|
|
370
|
+
tabs: rest,
|
|
371
|
+
active: gone.includes(s.active) ? (rest.length ? rest[rest.length - 1].id : null) : s.active,
|
|
372
|
+
splitId: gone.includes(s.splitId) ? null : s.splitId,
|
|
373
|
+
};
|
|
374
|
+
});
|
|
375
|
+
}
|
|
361
376
|
for (const rec of panes.values()) if (rec.kind === 'real') bind(rec);
|
|
362
377
|
return;
|
|
363
378
|
}
|
|
@@ -138,6 +138,8 @@ export const COPY = {
|
|
|
138
138
|
unreached: 'The workspace did not answer.',
|
|
139
139
|
refused: (why) => `Nothing changed. ${why}`,
|
|
140
140
|
oldServer: 'This workspace server has no day run yet. Only your goals show.',
|
|
141
|
+
sections: { label: 'Agenda sections', agenda: 'Agenda', goals: 'Goals', jobs: 'Jobs', mentions: 'Mentions', summary: 'Result' },
|
|
142
|
+
noResult: 'No agent results yet.',
|
|
141
143
|
panels: { summary: 'Agent results', calendar: 'Calendar', agenda: 'Agenda', jobs: 'Jobs', goals: 'Goals for today', mentions: 'Mentions since yesterday', detail: 'Detail' },
|
|
142
144
|
summary: (c) => [c.review ? `${c.review} ${c.review === 1 ? 'draft waits' : 'drafts wait'} for you` : null, c.done ? `${c.done} done` : null,
|
|
143
145
|
c.working ? `${c.working} working` : null, c.failed ? `${c.failed} failed` : null].filter(Boolean).join(' · ') || 'No agent job yet.',
|
|
@@ -50,6 +50,7 @@ const store = {
|
|
|
50
50
|
// A new key: a fold stored by the old Today page must not hide the Agenda month.
|
|
51
51
|
const OPEN_KEY = 'pmos.agenda.day.open';
|
|
52
52
|
const SEEN_KEY = 'pmos.today.day.seen';
|
|
53
|
+
const SECTION_KEY = 'pm.today.section';
|
|
53
54
|
let seen = new Set();
|
|
54
55
|
try { seen = new Set(JSON.parse(store.get(SEEN_KEY) || '[]')); } catch { seen = new Set(); }
|
|
55
56
|
|
|
@@ -57,6 +58,7 @@ const cache = new Map(); // ws -> { data, error }
|
|
|
57
58
|
const runs = new Map(); // ws -> the last "Prepare my day" press: { state: 'busy'|'started'|'running'|'failed', at, session, error }
|
|
58
59
|
const ui = {
|
|
59
60
|
open: (() => { const v = store.get(OPEN_KEY); return v === 'true' ? true : v === 'false' ? false : null; })(),
|
|
61
|
+
section: (() => { const v = store.get(SECTION_KEY); return ['agenda', 'goals', 'jobs', 'mentions', 'summary'].includes(v) ? v : 'agenda'; })(),
|
|
60
62
|
sel: null, mtab: 'all', openSrc: null, showIgn: {}, showIgnDigests: false,
|
|
61
63
|
revising: new Set(), notes: new Map(), siteDraft: '', placeDraft: {}, busy: new Set(), scroll: false,
|
|
62
64
|
// today-act-prepare: the folded groups the PM opened, the item whose hold form is open, and its draft
|
|
@@ -380,6 +382,10 @@ async function setSettings(ws, patch) {
|
|
|
380
382
|
function select(type, id) {
|
|
381
383
|
ui.sel = type ? { type, id: String(id) } : null;
|
|
382
384
|
if (type === 'job' && !seen.has(id)) { seen.add(id); store.set(SEEN_KEY, JSON.stringify([...seen].slice(-200))); }
|
|
385
|
+
// Reveal the appropriate section when selecting an item
|
|
386
|
+
if (type === 'job') { ui.section = 'jobs'; }
|
|
387
|
+
else if (type === 'goal') { ui.section = 'goals'; }
|
|
388
|
+
else if (type === 'mention' || type === 'channel') { ui.section = 'mentions'; }
|
|
383
389
|
ui.scroll = true;
|
|
384
390
|
rerender();
|
|
385
391
|
}
|
|
@@ -1188,6 +1194,41 @@ function fold(open) {
|
|
|
1188
1194
|
rerender();
|
|
1189
1195
|
}
|
|
1190
1196
|
|
|
1197
|
+
/** Render the section selector tabs. */
|
|
1198
|
+
function sectionSelector(ctx) {
|
|
1199
|
+
const { m } = ctx;
|
|
1200
|
+
const openGoals = m.allGoals.filter((g) => g.status === 'open').length;
|
|
1201
|
+
const items = [
|
|
1202
|
+
{ id: 'agenda', icon: 'calendar', label: D.sections.agenda, count: null },
|
|
1203
|
+
{ id: 'goals', icon: 'target', label: D.sections.goals, count: openGoals },
|
|
1204
|
+
{ id: 'jobs', icon: 'list-checks', label: D.sections.jobs, count: m.jobs.length },
|
|
1205
|
+
{ id: 'mentions', icon: 'text-quote', label: D.sections.mentions, count: m.mentions.length },
|
|
1206
|
+
{ id: 'summary', icon: 'bot', label: D.sections.summary, count: null },
|
|
1207
|
+
];
|
|
1208
|
+
return tabs({ variant: 'segmented', role: 'tablist', ariaLabel: D.sections.label, value: ui.section, full: true, attrs: { 'data-day': 'section-tabs' },
|
|
1209
|
+
items,
|
|
1210
|
+
onChange: (v) => { ui.section = v; store.set(SECTION_KEY, v); rerender(); } });
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
/** Render the chosen section's content. */
|
|
1214
|
+
function sectionContent(ctx) {
|
|
1215
|
+
const { m } = ctx;
|
|
1216
|
+
switch (ui.section) {
|
|
1217
|
+
case 'agenda':
|
|
1218
|
+
return agenda(ctx);
|
|
1219
|
+
case 'goals':
|
|
1220
|
+
return goalsPanel(ctx);
|
|
1221
|
+
case 'jobs':
|
|
1222
|
+
return jobsPanel(ctx);
|
|
1223
|
+
case 'mentions':
|
|
1224
|
+
return mentionsPanel(ctx);
|
|
1225
|
+
case 'summary':
|
|
1226
|
+
return summaryPanel(ctx) || note(D.noResult, { 'data-day': 'no-result' });
|
|
1227
|
+
default:
|
|
1228
|
+
return agenda(ctx);
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1191
1232
|
/** The section. `ws` is the repository the day run belongs to; `openIni(id)` opens an initiative of it. */
|
|
1192
1233
|
export function daySection({ ws, setState, openIni, owed, here, all } = {}) {
|
|
1193
1234
|
if (FIXTURE) return null;
|
|
@@ -1230,12 +1271,12 @@ export function daySection({ ws, setState, openIni, owed, here, all } = {}) {
|
|
|
1230
1271
|
c.error ? el('p', { class: 'td-day__run td-day__run--failed', role: 'status', 'data-day': 'error' }, D.refused(c.error)) : null,
|
|
1231
1272
|
m.d && m.d.legacy ? note(D.oldServer) : null,
|
|
1232
1273
|
el('div', { class: 'td-day__cols' },
|
|
1233
|
-
el('div', { class: 'td-day__left' },
|
|
1274
|
+
el('div', { class: 'td-day__left' }, sectionSelector(ctx), sectionContent(ctx)),
|
|
1234
1275
|
detail(ctx)));
|
|
1235
1276
|
}
|
|
1236
1277
|
|
|
1237
1278
|
export function daySig(ws) {
|
|
1238
1279
|
const c = cache.get(ws) || {};
|
|
1239
|
-
return JSON.stringify([forced, ui.open, ui.sel, ui.mtab, ui.openSrc, ui.showIgn, ui.showIgnDigests, [...ui.revising], [...ui.busy], [...ui.groupsOpen], ui.holding, ui.holdDraft.when, [...ui.verdict], c.error, runs.get(ws) || null, [...seen].length])
|
|
1280
|
+
return JSON.stringify([forced, ui.open, ui.section, ui.sel, ui.mtab, ui.openSrc, ui.showIgn, ui.showIgnDigests, [...ui.revising], [...ui.busy], [...ui.groupsOpen], ui.holding, ui.holdDraft.when, [...ui.verdict], c.error, runs.get(ws) || null, [...seen].length])
|
|
1240
1281
|
+ dataSig(c.data) + goalsSig() + agendaSig();
|
|
1241
1282
|
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/* pm/view-notes-menu.js — the Notes menu: all notes grouped by initiative (Run 11, R47).
|
|
2
|
+
*
|
|
3
|
+
* Two-column layout:
|
|
4
|
+
* left all notes grouped by initiative; click an initiative title to collapse/expand;
|
|
5
|
+
* click a note to open its detail on the right
|
|
6
|
+
* right the note detail (same single column as the full-screen note, Run 7)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { el, setChildren } from './dom.js';
|
|
10
|
+
import { askConfirm } from './dialogs.js';
|
|
11
|
+
import { pmGet, pmPost } from './http.js';
|
|
12
|
+
import { icon } from './icons.js';
|
|
13
|
+
import { button, iconButton, empty, toast, ensureComponentsCss } from './primitives.js';
|
|
14
|
+
import * as viewNotes from './view-notes.js';
|
|
15
|
+
|
|
16
|
+
ensureComponentsCss();
|
|
17
|
+
|
|
18
|
+
let cache = new Map();
|
|
19
|
+
let list = null;
|
|
20
|
+
let selectedId = null;
|
|
21
|
+
let expandedInitiatives = new Set();
|
|
22
|
+
|
|
23
|
+
async function loadList() {
|
|
24
|
+
const r = await pmGet('/api/notes');
|
|
25
|
+
if (r && r.ok) {
|
|
26
|
+
list = { notes: r.notes || [] };
|
|
27
|
+
} else {
|
|
28
|
+
list = { notes: [], error: (r && r.error) || 'the notes could not be read' };
|
|
29
|
+
}
|
|
30
|
+
return list;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function loadNote(id) {
|
|
34
|
+
const r = await pmGet('/api/notes/one?id=' + encodeURIComponent(id));
|
|
35
|
+
if (r && r.ok) {
|
|
36
|
+
cache.set(id, r.note);
|
|
37
|
+
return r.note;
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function groupByInitiative(notes) {
|
|
43
|
+
const groups = {};
|
|
44
|
+
notes.forEach((n) => {
|
|
45
|
+
const key = n.initiative || '(no initiative)';
|
|
46
|
+
if (!groups[key]) groups[key] = [];
|
|
47
|
+
groups[key].push(n);
|
|
48
|
+
});
|
|
49
|
+
return Object.entries(groups).sort((a, b) => {
|
|
50
|
+
if (a[0] === '(no initiative)') return 1;
|
|
51
|
+
if (b[0] === '(no initiative)') return -1;
|
|
52
|
+
return a[0].localeCompare(b[0]);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function renderNoteRow(note, isSelected, onClick) {
|
|
57
|
+
const summary = note.summary || '';
|
|
58
|
+
const summaryText = summary
|
|
59
|
+
.split('\n')
|
|
60
|
+
.slice(0, 2)
|
|
61
|
+
.map((line) => line.trim())
|
|
62
|
+
.filter(Boolean)
|
|
63
|
+
.join(' ')
|
|
64
|
+
.substring(0, 60);
|
|
65
|
+
|
|
66
|
+
return el('div', {
|
|
67
|
+
class: 'nt-menu-row' + (isSelected ? ' nt-menu-row--selected' : ''),
|
|
68
|
+
'data-nt-menu-row': note.id,
|
|
69
|
+
onClick,
|
|
70
|
+
},
|
|
71
|
+
el('div', { class: 'nt-menu-row-title' }, note.title || 'Untitled note'),
|
|
72
|
+
el('div', { class: 'nt-menu-row-date' }, new Date(note.updated_at).toLocaleDateString()),
|
|
73
|
+
summaryText && el('div', { class: 'nt-menu-row-summary' }, summaryText),
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function renderLeftPanel(notes, selectedId, onSelectNote, onToggleInitiative) {
|
|
78
|
+
const groups = groupByInitiative(notes);
|
|
79
|
+
|
|
80
|
+
return el('div', { class: 'nt-menu-left' },
|
|
81
|
+
...groups.map(([initiative, initiativeNotes]) => {
|
|
82
|
+
const isExpanded = expandedInitiatives.has(initiative);
|
|
83
|
+
return el('div', { class: 'nt-menu-group' },
|
|
84
|
+
el('div', {
|
|
85
|
+
class: 'nt-menu-group-title',
|
|
86
|
+
onClick: () => onToggleInitiative(initiative),
|
|
87
|
+
},
|
|
88
|
+
icon(isExpanded ? 'chevron-down' : 'chevron-right', { class: 'nt-menu-group-icon' }),
|
|
89
|
+
el('span', {}, initiative),
|
|
90
|
+
),
|
|
91
|
+
isExpanded && el('div', { class: 'nt-menu-group-items' },
|
|
92
|
+
...initiativeNotes.map((note) =>
|
|
93
|
+
renderNoteRow(note, note.id === selectedId, () => onSelectNote(note.id)),
|
|
94
|
+
),
|
|
95
|
+
),
|
|
96
|
+
);
|
|
97
|
+
}),
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function renderBoard(inner, ctx) {
|
|
102
|
+
ensureComponentsCss();
|
|
103
|
+
const root = el('div', { class: 'nt-menu' });
|
|
104
|
+
|
|
105
|
+
async function render() {
|
|
106
|
+
if (!list) await loadList();
|
|
107
|
+
const note = selectedId ? cache.get(selectedId) : null;
|
|
108
|
+
|
|
109
|
+
const leftPanel = renderLeftPanel(
|
|
110
|
+
list.notes || [],
|
|
111
|
+
selectedId,
|
|
112
|
+
async (id) => {
|
|
113
|
+
selectedId = id;
|
|
114
|
+
await loadNote(id);
|
|
115
|
+
render();
|
|
116
|
+
},
|
|
117
|
+
(initiative) => {
|
|
118
|
+
if (expandedInitiatives.has(initiative)) {
|
|
119
|
+
expandedInitiatives.delete(initiative);
|
|
120
|
+
} else {
|
|
121
|
+
expandedInitiatives.add(initiative);
|
|
122
|
+
}
|
|
123
|
+
render();
|
|
124
|
+
},
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
const rightPanel = note
|
|
128
|
+
? await (async () => {
|
|
129
|
+
// Load and render the full note detail using the note view's render logic
|
|
130
|
+
try {
|
|
131
|
+
// Import the note view's rendering for inline display
|
|
132
|
+
const noteModule = await import('./view-notes.js');
|
|
133
|
+
// Parse next steps/actions from the note's final state
|
|
134
|
+
let actions = [];
|
|
135
|
+
if (note.final_state && note.final_state.actions) {
|
|
136
|
+
actions = note.final_state.actions;
|
|
137
|
+
}
|
|
138
|
+
// Use the note's renderInline if available, otherwise render basic detail
|
|
139
|
+
return el('div', { class: 'nt-menu-right' },
|
|
140
|
+
el('div', { class: 'nt-detail-header' },
|
|
141
|
+
el('div', { class: 'nt-detail-title' }, note.title || 'Untitled note'),
|
|
142
|
+
el('div', { class: 'nt-detail-date' }, new Date(note.updated_at).toLocaleDateString()),
|
|
143
|
+
),
|
|
144
|
+
el('div', { class: 'nt-detail-summary' },
|
|
145
|
+
note.summary ? el('pre', {}, note.summary) : el('p', { class: 'nt-hint' }, 'No summary yet'),
|
|
146
|
+
),
|
|
147
|
+
actions.length > 0 && el('div', { class: 'nt-detail-actions' },
|
|
148
|
+
el('div', { class: 'nt-detail-actions-title' }, 'Next steps'),
|
|
149
|
+
...actions.map((action, idx) =>
|
|
150
|
+
el('div', { class: 'nt-detail-action-row' },
|
|
151
|
+
el('div', { class: 'nt-detail-action-text' }, action.text || ''),
|
|
152
|
+
action.owner && el('div', { class: 'nt-detail-action-owner' }, `Owner: ${action.owner}`),
|
|
153
|
+
action.due && el('div', { class: 'nt-detail-action-due' }, `Due: ${action.due}`),
|
|
154
|
+
iconButton({ icon: 'plus', class: 'nt-detail-make-task', title: 'Create task from this step' },
|
|
155
|
+
async () => {
|
|
156
|
+
try {
|
|
157
|
+
const result = await pmPost('/api/notes/create-task', {
|
|
158
|
+
id: note.id,
|
|
159
|
+
text: action.text,
|
|
160
|
+
owner: action.owner,
|
|
161
|
+
due: action.due,
|
|
162
|
+
});
|
|
163
|
+
if (result && result.ok) {
|
|
164
|
+
toast({ text: 'Task created' });
|
|
165
|
+
} else {
|
|
166
|
+
toast({ text: 'Could not create task' });
|
|
167
|
+
}
|
|
168
|
+
} catch (e) {
|
|
169
|
+
console.error('Task creation error:', e);
|
|
170
|
+
toast({ text: 'Error creating task' });
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
),
|
|
174
|
+
),
|
|
175
|
+
),
|
|
176
|
+
),
|
|
177
|
+
);
|
|
178
|
+
} catch {
|
|
179
|
+
return el('div', { class: 'nt-menu-right' }, empty({ text: 'Could not load note detail' }));
|
|
180
|
+
}
|
|
181
|
+
})()
|
|
182
|
+
: el('div', { class: 'nt-menu-right' }, empty({ text: 'Select a note to view details' }));
|
|
183
|
+
|
|
184
|
+
setChildren(root, leftPanel, rightPanel);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
render();
|
|
188
|
+
ctx.setState = (state) => { render(); };
|
|
189
|
+
return root;
|
|
190
|
+
}
|
|
@@ -571,7 +571,8 @@ async function deleteAudio(ctx, n) {
|
|
|
571
571
|
async function openSource(kind) {
|
|
572
572
|
const md = navigator.mediaDevices;
|
|
573
573
|
if (kind === 'mic') return md.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true, channelCount: 1 } });
|
|
574
|
-
|
|
574
|
+
// preferCurrentTab + selfBrowserSurface defaults to the current tab with tab audio, reducing friction for the common case
|
|
575
|
+
const s = await md.getDisplayMedia({ video: true, audio: { echoCancellation: false, noiseSuppression: false, autoGainControl: false }, systemAudio: 'include', preferCurrentTab: true, selfBrowserSurface: 'include' });
|
|
575
576
|
const tracks = s.getAudioTracks();
|
|
576
577
|
for (const t of s.getVideoTracks()) t.stop(); // only the sound is used
|
|
577
578
|
if (!tracks.length) throw Object.assign(new Error('The shared tab or window has no audio. Share it again and tick "Share tab audio".'), { noAudio: true });
|
|
@@ -908,22 +909,9 @@ async function stopRecording(ctx) {
|
|
|
908
909
|
async function chooseSources(ctx, n) {
|
|
909
910
|
const eng = engine && engine.ws === wsOf(ctx) && engine.whisper ? engine : await loadEngine(ctx);
|
|
910
911
|
const local = !!(eng.whisper && eng.whisper.ready);
|
|
911
|
-
|
|
912
|
-
const
|
|
913
|
-
|
|
914
|
-
el('p', { class: 'nt-hint' }, local ? 'Pick the meeting\'s tab or window next, and tick "Share tab audio".' : 'The meeting can be heard once local transcription is set up in Settings. Until then the microphone is recognised by the browser on this device.'),
|
|
915
|
-
el('p', { class: 'nt-consent', 'data-nt-consent': '' }, local
|
|
916
|
-
? 'Speech is transcribed on this machine' + (eng.whisper.keep_audio === false ? ', and no audio is kept' : ', and the audio is kept on this machine so you can replay the meeting') + '. Let people know you\'re taking notes.'
|
|
917
|
-
: 'Speech is recognised by the browser on this device, and no audio is kept. Let people know you\'re taking notes.')];
|
|
918
|
-
let h = null;
|
|
919
|
-
const go = () => {
|
|
920
|
-
const want = [mic.control.checked && 'mic', local && meet.control.checked && 'meeting'].filter(Boolean);
|
|
921
|
-
h.close();
|
|
922
|
-
if (!want.length) return;
|
|
923
|
-
if (local) startAudio(ctx, n, want); else startSpeech(ctx, n, 'record');
|
|
924
|
-
};
|
|
925
|
-
h = sheet({ title: 'Listen to', body, attrs: { 'data-nt-chooser': '' },
|
|
926
|
-
actions: [button({ label: 'Cancel', variant: 'ghost', onClick: () => h.close() }), button({ label: 'Start', variant: 'primary', onClick: go, attrs: { 'data-nt-start': '' } })] });
|
|
912
|
+
// Default to both mic and meeting (skip the chooser dialog entirely per Wawan's streamlining request)
|
|
913
|
+
const want = local ? ['mic', 'meeting'] : ['speech'];
|
|
914
|
+
if (local) startAudio(ctx, n, want); else startSpeech(ctx, n, 'record');
|
|
927
915
|
}
|
|
928
916
|
async function setSwitch(ctx, patch) {
|
|
929
917
|
const ws = wsOf(ctx);
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
"learn.test.js",
|
|
47
47
|
"session-flag.test.js",
|
|
48
48
|
"session-origin.test.js",
|
|
49
|
+
"tab-gone.test.js",
|
|
49
50
|
"live-notes.test.js",
|
|
50
51
|
"plan-items.test.js",
|
|
51
52
|
"plan-tree.test.js",
|
|
@@ -102,6 +103,7 @@
|
|
|
102
103
|
"pm-r29-filespad-test.js",
|
|
103
104
|
"pm-r29-newsession-alignment-test.js",
|
|
104
105
|
"pm-live-notes-test.js",
|
|
106
|
+
"pm-notes-menu-test.js",
|
|
105
107
|
"pm-r30-test.js",
|
|
106
108
|
"pm-breakdown-test.js",
|
|
107
109
|
"pm-breakdown-phone-test.js",
|
|
@@ -150,6 +152,7 @@
|
|
|
150
152
|
"learn.test.js": 3,
|
|
151
153
|
"session-flag.test.js": 1,
|
|
152
154
|
"session-origin.test.js": 1,
|
|
155
|
+
"tab-gone.test.js": 1,
|
|
153
156
|
"live-notes.test.js": 3,
|
|
154
157
|
"pm-shell-test.js": 6,
|
|
155
158
|
"pm-repos-test.js": 3,
|
|
@@ -199,6 +202,7 @@
|
|
|
199
202
|
"pm-r29-filespad-test.js": 5,
|
|
200
203
|
"pm-r29-newsession-alignment-test.js": 8,
|
|
201
204
|
"pm-live-notes-test.js": 24,
|
|
205
|
+
"pm-notes-menu-test.js": 8,
|
|
202
206
|
"pm-r30-test.js": 4,
|
|
203
207
|
"ui-test.js": 30,
|
|
204
208
|
"plan-items.test.js": 1,
|