create-pmos 0.133.0 → 0.135.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/bin/notes-replay.js +235 -0
- package/workspace/c3-briefing-test.js +130 -0
- package/workspace/c3-foundation-test.js +159 -0
- package/workspace/c3-ib-items-test.js +105 -0
- package/workspace/c3-ib-more-test.js +120 -0
- package/workspace/c3-ib-triage-backlog-test.js +119 -0
- package/workspace/c3-ini-harness.js +66 -0
- package/workspace/c3-initiatives-test.js +140 -0
- package/workspace/c3-one-home-test.js +14 -1
- package/workspace/c3-reports-test.js +57 -18
- package/workspace/c3-settings-test.js +139 -0
- package/workspace/c3-work-test.js +42 -10
- package/workspace/fixtures/one-home-baseline.json +1 -9
- package/workspace/guide/briefing-cards.md +15 -3
- package/workspace/guide/briefing.md +12 -8
- package/workspace/guide/calendar-popup.md +2 -2
- package/workspace/guide/console-calendar.md +39 -10
- package/workspace/guide/index.md +1 -0
- package/workspace/guide/initiative-boards.md +65 -0
- package/workspace/guide/jira.md +3 -1
- package/workspace/guide/layout.md +2 -0
- package/workspace/guide/my-work.md +5 -3
- package/workspace/guide/notes.md +35 -2
- package/workspace/guide/reports-dashboards.md +29 -5
- package/workspace/guide/settings.md +68 -0
- package/workspace/guide/welcome.md +10 -10
- package/workspace/lib/calendar-cli.js +3 -2
- package/workspace/lib/calendar-layers.js +21 -10
- package/workspace/lib/calendar-tidy.js +230 -0
- package/workspace/lib/engine/boards.js +542 -0
- package/workspace/lib/engine/breakdown-ops.js +180 -0
- package/workspace/lib/engine/calendar-import.js +84 -0
- package/workspace/lib/engine/calendar-model.js +185 -0
- package/workspace/lib/engine/card-state.js +92 -0
- package/workspace/lib/engine/derived/derive.js +92 -0
- package/workspace/lib/engine/derived/plan.js +3 -0
- package/workspace/lib/engine/derived/trust.js +3 -0
- package/workspace/lib/engine/doc-health.js +142 -0
- package/workspace/lib/engine/doc-sheet.js +75 -0
- package/workspace/lib/engine/intake.js +78 -0
- package/workspace/lib/engine/outbox.js +86 -0
- package/workspace/lib/engine/plan-forecast.js +342 -0
- package/workspace/lib/engine/promises.js +29 -0
- package/workspace/lib/engine/records/index.js +114 -0
- package/workspace/lib/engine/releases.js +96 -0
- package/workspace/lib/engine/replies.js +99 -0
- package/workspace/lib/engine/reply-contract/reply-contract.js +372 -0
- package/workspace/lib/engine/report-families.js +175 -0
- package/workspace/lib/engine/roadmap-ops.js +150 -0
- package/workspace/lib/engine/sessions-list.js +27 -0
- package/workspace/lib/engine/signal-rules.js +95 -0
- package/workspace/lib/engine/signals/cards.js +57 -0
- package/workspace/lib/engine/signals/detectors.js +370 -0
- package/workspace/lib/engine/signals/facts-cli.js +8 -0
- package/workspace/lib/engine/signals/facts.js +197 -0
- package/workspace/lib/engine/signals/index.js +33 -0
- package/workspace/lib/engine/signals/kinds.js +110 -0
- package/workspace/lib/engine/signals/layout-paths.js +44 -0
- package/workspace/lib/engine/signals/options.js +24 -0
- package/workspace/lib/engine/signals/score.js +72 -0
- package/workspace/lib/engine/signals/select.js +47 -0
- package/workspace/lib/engine/step-planner.js +65 -0
- package/workspace/lib/engine/trust/claim-rules.json +93 -0
- package/workspace/lib/engine/trust/import-calendar.js +21 -0
- package/workspace/lib/engine/trust/import-guard.js +36 -0
- package/workspace/lib/engine/trust/ledger.js +53 -0
- package/workspace/lib/engine/trust/outbox.js +45 -0
- package/workspace/lib/engine/trust/plan.js +5 -0
- package/workspace/lib/engine/trust/trust.js +21 -0
- package/workspace/lib/engine/truth-table.js +57 -0
- package/workspace/lib/engine/workspace-lib.js +24 -0
- package/workspace/lib/factory-join.js +1 -1
- package/workspace/lib/facts-async.js +4 -4
- package/workspace/lib/headless.js +95 -0
- package/workspace/lib/import-overlay.js +2 -1
- package/workspace/lib/jira-status.js +30 -3
- package/workspace/lib/notes-audio.js +28 -9
- package/workspace/lib/notes-capture.js +136 -0
- package/workspace/lib/notes-final.js +38 -13
- package/workspace/lib/notes-live-guard.js +117 -0
- package/workspace/lib/notes-replay.js +125 -0
- package/workspace/lib/notes-screen.js +180 -0
- package/workspace/lib/notes-whisper.js +139 -16
- package/workspace/lib/notes.js +4 -2
- package/workspace/lib/outbox-store.js +10 -6
- package/workspace/lib/pmos-verbs.js +3 -1
- package/workspace/lib/proactive-cli.js +7 -0
- package/workspace/lib/proactive.js +15 -2
- package/workspace/lib/repo-script.js +36 -0
- package/workspace/lib/report-dashboards.js +5 -2
- package/workspace/lib/report-frame.js +122 -0
- package/workspace/lib/report-schedules.js +113 -0
- package/workspace/lib/route-modules.js +9 -2
- package/workspace/lib/routes-boards.js +734 -0
- package/workspace/lib/routes-brief-page.js +13 -4
- package/workspace/lib/routes-briefing.js +22 -11
- package/workspace/lib/routes-c3-init.js +4 -3
- package/workspace/lib/routes-c3-life.js +2 -1
- package/workspace/lib/routes-c3-reports.js +96 -13
- package/workspace/lib/routes-c3-strategy.js +4 -3
- package/workspace/lib/routes-c3-timeline.js +3 -2
- package/workspace/lib/routes-calendar-import.js +3 -2
- package/workspace/lib/routes-calendar-views.js +9 -4
- package/workspace/lib/routes-doc-health.js +2 -1
- package/workspace/lib/routes-doc-sheet.js +2 -1
- package/workspace/lib/routes-forecast.js +2 -1
- package/workspace/lib/routes-hooks.js +3 -2
- package/workspace/lib/routes-initiatives.js +2 -1
- package/workspace/lib/routes-intake.js +2 -1
- package/workspace/lib/routes-notes.js +113 -15
- package/workspace/lib/routes-outbox.js +23 -3
- package/workspace/lib/routes-pm-items.js +19 -6
- package/workspace/lib/routes-records.js +3 -2
- package/workspace/lib/routes-releases.js +2 -1
- package/workspace/lib/routes-report-facts.js +4 -3
- package/workspace/lib/routes-report-families.js +2 -1
- package/workspace/lib/routes-roadmap-ops.js +5 -4
- package/workspace/lib/routes-runs.js +6 -5
- package/workspace/lib/routes-settings.js +51 -0
- package/workspace/lib/routes-signals.js +3 -2
- package/workspace/lib/routes-stage-tools.js +220 -0
- package/workspace/lib/run-answers.js +2 -1
- package/workspace/lib/settings-store.js +226 -0
- package/workspace/lib/stage-tools.js +208 -0
- package/workspace/lib/vendor/scripts/signals/layout-paths.js +2 -1
- package/workspace/lib/vendor/scripts/workspace-lib.js +24 -0
- package/workspace/lib/work-evidence.js +73 -0
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/briefing-deck.js +63 -17
- package/workspace/public/pm/briefing-model.js +36 -7
- package/workspace/public/pm/c3-ambient.js +100 -0
- package/workspace/public/pm/c3-brief-needs.js +26 -6
- package/workspace/public/pm/c3-brief-pulse.js +12 -4
- package/workspace/public/pm/c3-brief-today.js +56 -21
- package/workspace/public/pm/c3-brief.js +2 -1
- package/workspace/public/pm/c3-calendar.js +161 -42
- package/workspace/public/pm/c3-calpop.js +5 -5
- package/workspace/public/pm/c3-demo-boards.js +649 -0
- package/workspace/public/pm/c3-demo-brief-page.js +1 -1
- package/workspace/public/pm/c3-demo-cal4.js +3 -2
- package/workspace/public/pm/c3-demo-cal5.js +52 -0
- package/workspace/public/pm/c3-demo-data.js +17 -27
- package/workspace/public/pm/c3-demo-rep3.js +46 -5
- package/workspace/public/pm/c3-demo.js +2 -1
- package/workspace/public/pm/c3-dock.js +1 -1
- package/workspace/public/pm/c3-ib-docs.js +80 -0
- package/workspace/public/pm/c3-ib-initiatives.js +134 -0
- package/workspace/public/pm/c3-ib-items.js +201 -0
- package/workspace/public/pm/c3-ib-releases.js +51 -0
- package/workspace/public/pm/c3-ib-sprints.js +55 -0
- package/workspace/public/pm/c3-ib-triage.js +178 -0
- package/workspace/public/pm/c3-ii-tabs.js +240 -0
- package/workspace/public/pm/c3-ini-records.js +197 -0
- package/workspace/public/pm/c3-ini-timeline.js +278 -0
- package/workspace/public/pm/c3-ini-ui.js +229 -0
- package/workspace/public/pm/c3-ini-work.js +139 -0
- package/workspace/public/pm/c3-initiatives.css +218 -0
- package/workspace/public/pm/c3-initiatives.js +741 -0
- package/workspace/public/pm/c3-plan.js +2 -2
- package/workspace/public/pm/c3-reports.css +47 -47
- package/workspace/public/pm/c3-reports.js +223 -185
- package/workspace/public/pm/c3-settings.css +74 -0
- package/workspace/public/pm/c3-settings.js +703 -0
- package/workspace/public/pm/c3-shell.js +102 -36
- package/workspace/public/pm/c3-terminal-icons.js +2 -2
- package/workspace/public/pm/c3-terminal.js +2 -2
- package/workspace/public/pm/calendar-views.js +26 -13
- package/workspace/public/pm/components.css +319 -213
- package/workspace/public/pm/console3.css +118 -23
- package/workspace/public/pm/dest-notes.js +26 -4
- package/workspace/public/pm/notes-screen-capture.js +165 -0
- package/workspace/public/pm/notes-screen-view.js +98 -0
- package/workspace/public/pm/panel-trees.js +5 -2
- package/workspace/public/pm/pm-work-view.js +233 -146
- package/workspace/public/pm/shell-bell.js +3 -2
- package/workspace/public/pm/shell-recording.js +148 -17
- package/workspace/public/pm/tokens.css +2 -2
- package/workspace/public/pm/view-notes.js +53 -11
- package/workspace/public/pm/welcome.js +197 -89
- package/workspace/public/welcome.html +6 -4
- package/workspace/server.js +105 -6
- package/workspace/skills/day-check.skill +1 -1
- package/workspace/skills/jira-sync.skill +6 -4
- package/workspace/skills/morning-brief.skill +1 -1
- package/workspace/test-files.json +14 -3
- package/workspace/test-lists/c3-ib-items.json +9 -0
- package/workspace/test-lists/c3-ib-more.json +9 -0
- package/workspace/test-lists/c3-ib-triage-backlog.json +9 -0
- package/workspace/test-lists/c3-initiative-boards.json +6 -0
- package/workspace/test-lists/calendar-tidy-up.json +6 -0
- package/workspace/test-lists/console3-briefing.json +9 -0
- package/workspace/test-lists/console3-foundation.json +9 -0
- package/workspace/test-lists/console3-reports.json +3 -3
- package/workspace/test-lists/meeting-notes.json +12 -4
- package/workspace/test-lists/workspace-headless-jobs.json +5 -0
- package/workspace/test-lists/workspace-package-script-closure.json +6 -0
- package/workspace/lib/vendor/workspace/lib/actor.js +0 -91
- package/workspace/lib/vendor/workspace/lib/board-cache.js +0 -156
- package/workspace/lib/vendor/workspace/lib/console-state.js +0 -295
- package/workspace/lib/vendor/workspace/lib/factory-join.js +0 -158
- package/workspace/lib/vendor/workspace/lib/git-info.js +0 -306
- package/workspace/lib/vendor/workspace/lib/plan-items.js +0 -922
- package/workspace/lib/vendor/workspace/lib/plan-tree.js +0 -419
- package/workspace/lib/vendor/workspace/lib/review-gate.js +0 -39
- package/workspace/lib/vendor/workspace/lib/sink.js +0 -321
- package/workspace/lib/vendor/workspace/lib/state-root.js +0 -587
|
@@ -0,0 +1,703 @@
|
|
|
1
|
+
/* pm/c3-settings.js — the console3 Settings page (initiative console3-settings). Its markup is the approved console3 mockup's
|
|
2
|
+
* Settings page: a section list card and, per section, .card.sect groups of li.nrow setting rows. Every save shows a toast,
|
|
3
|
+
* with Undo when the old value can be put back. A feature PMOS does not have yet shows as a row with its control off and
|
|
4
|
+
* the reason (PM decision 9).
|
|
5
|
+
*
|
|
6
|
+
* mountSettings(host, ctx) ctx: { ws, workspaces, theme, themePref, setTheme(t), goTab(id), section?, root }
|
|
7
|
+
* SECTIONS · LABELS · rowsMatching(q) · gearIcon() pure parts, for the tests
|
|
8
|
+
*
|
|
9
|
+
* ONE HOME PER SETTING. A setting that already has a store is read and written through its own route, never copied here:
|
|
10
|
+
* workspaces GET /api/workspaces · POST /api/workspaces, /api/workspaces/rename, /api/workspaces/forget
|
|
11
|
+
* recording GET /api/notes/engine · POST /api/notes/switch (language, model, accuracy, keep_audio, clean_audio, wispr, jev, check)
|
|
12
|
+
* attendee names GET /api/people · POST /api/people/decide
|
|
13
|
+
* validate consent GET and POST /api/validate/repo Online GET and POST /api/online
|
|
14
|
+
* profiles GET /api/profiles · POST /api/profiles/prefer new sessions: dock.js sessionDefaults (this browser)
|
|
15
|
+
* TypeSafe key GET /api/typesafe (present or not; the key is never read)
|
|
16
|
+
* about you GET and POST /api/proactive/settings {name, about:{role, work, duties}} (the Welcome page writes the same)
|
|
17
|
+
* running sessions GET /api/sessions calendar import POST /api/sources/refresh {source:'calendar'}
|
|
18
|
+
* versions GET /api/kit/status · POST /api/kit/check, /api/kit/update
|
|
19
|
+
* Every other setting is in the settings store: GET /api/settings and POST /api/settings (lib/settings-store.js).
|
|
20
|
+
* A board's own workflow is edited on the board; here are only the templates new boards start from.
|
|
21
|
+
* Nothing here asks for or shows a key or a password. */
|
|
22
|
+
import { el } from './dom.js';
|
|
23
|
+
import { button, SEL } from './primitives.js';
|
|
24
|
+
import { pmGet, pmPost } from './http.js';
|
|
25
|
+
import { c3ic } from './pm-work-view.js';
|
|
26
|
+
import { sessionDefaults, setSessionDefaults } from './dock.js';
|
|
27
|
+
import { toast } from './c3-toast.js';
|
|
28
|
+
import { dayPartOf } from './c3-daypart.js';
|
|
29
|
+
|
|
30
|
+
const cx = (...c) => c.filter(Boolean).join(' ');
|
|
31
|
+
|
|
32
|
+
export const SECTIONS = Object.freeze([
|
|
33
|
+
['about', 'About you', 'user', 'Tell PMOS who you are, so your Briefing knows what matters to you. Every field is optional. The Welcome page asks for the same four.'],
|
|
34
|
+
['workspaces', 'Workspaces', 'repo', 'A workspace is one repository PMOS works in. Settings marked Workspace apply to the one picked at the top of this page.'],
|
|
35
|
+
['boards', 'Boards and workflows', 'cols', 'The templates and defaults new boards start from. A board\'s own workflow is edited on the board, in Initiatives.'],
|
|
36
|
+
['connectors', 'Connectors', 'org', 'PMOS keeps no passwords or keys for these. Your Claude session reaches each tool through the connectors of its profile.'],
|
|
37
|
+
['recording', 'Recording', 'mic', 'How meetings are transcribed. This is the one place to check when a transcript looks wrong.'],
|
|
38
|
+
['time', 'Day and time', 'sun', 'The Briefing follows your clock. These settings say what your day looks like.'],
|
|
39
|
+
['sharing', 'Sharing', 'mail', 'Who dashboards go to, and how. Each dashboard\'s schedule is set on the dashboard, in Reports.'],
|
|
40
|
+
['access', 'Access and devices', 'phone', 'Open PMOS from your phone or another computer.'],
|
|
41
|
+
['sessions', 'Claude sessions', 'term', 'The Claude Code sessions PMOS starts, and what they start with.'],
|
|
42
|
+
['privacy', 'Privacy and data', 'shield', 'Nothing leaves this computer unless a line below says so.'],
|
|
43
|
+
['appearance', 'Appearance', 'eye', 'How PMOS looks and where it opens.'],
|
|
44
|
+
['updates', 'Updates', 'cloud', 'The PMOS app, and the kit inside each workspace.'],
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
/* Labels for the settings store's keys: [section group, label, help, { value: option label }]. */
|
|
48
|
+
export const LABELS = Object.freeze({
|
|
49
|
+
'ws.start': ['Start', 'Open on start', 'Which workspace PMOS opens when it starts.', { last: 'Last used', ask: 'Ask each time' }],
|
|
50
|
+
'ws.briefing': ['Start', 'Briefing covers', 'With several workspaces, the Briefing can show one or all of them. Each item still opens in its own workspace.', { one: 'This workspace', all: 'All workspaces' }],
|
|
51
|
+
'board.template': ['Defaults', 'Template for new boards', 'A new board copies this template. You can change its workflow on the board afterwards.', { product: 'Product', project: 'Project', simple: 'Simple' }],
|
|
52
|
+
'board.rules': ['Defaults', 'Order rules on boards', 'You move cards between stages. When a card skips a step, such as Build before a spec exists, the card shows a warning (decision D3).', { warn: 'Warn on the card', off: 'Off' }],
|
|
53
|
+
'conn.cal.window': ['Google Calendar', 'Import window', 'How far back and ahead your session imports events.', { '7-14': 'Past 7 days, next 14', '14-30': 'Past 14 days, next 30', '30-60': 'Past 30 days, next 60' }],
|
|
54
|
+
'conn.cal.refresh': ['Google Calendar', 'Refresh', 'When your session imports events again.', { open: 'When I open Calendar', hour: 'Every hour', manual: 'Only when I import' }],
|
|
55
|
+
'conn.cal.private': ['Google Calendar', 'Private events', 'Events marked private in Google Calendar.', { busy: 'Show as busy', hide: 'Hide', title: 'Show titles' }],
|
|
56
|
+
'conn.jira.refresh': ['Jira', 'Refresh ticket status', 'Jira owns ticket status. PMOS reads it back on this schedule.', { open: 'When I open a board', 15: 'Every 15 min', manual: 'Only when I sync' }],
|
|
57
|
+
'conn.wispr.wait': ['Wispr Flow', 'Wait before fetching', 'Wispr Flow needs a few minutes to finish its notes after a meeting.', { 2: '2 minutes', 5: '5 minutes', 10: '10 minutes' }],
|
|
58
|
+
'conn.sender': ['Sending', 'Sender for shared dashboards', 'Your session sends through this connector. Until you pick one, a scheduled share is a ready draft.', { none: 'None', slack: 'Slack', gmail: 'Gmail drafts' }],
|
|
59
|
+
'rec.sources': ['Live transcript', 'Listen to', 'The meeting is the sound from your call app. The mic is you.', { both: 'Mic and meeting', mic: 'Mic only', meeting: 'Meeting only' }],
|
|
60
|
+
'rec.guard': ['Live transcript', 'Re-run lines that come back in another language', 'A line that comes back mostly in another script is transcribed again in the meeting language. The original is kept (decision D8).'],
|
|
61
|
+
'rec.echo': ['Live transcript', 'Remove the meeting echo from the mic', 'When your speakers leak into the mic, the same words come in twice. Turn off to compare by ear.'],
|
|
62
|
+
'rec.settle': ['Live transcript', 'Correct lines a few seconds later', 'At each pause, the last 30 seconds are transcribed again with the stronger model and corrected in place.'],
|
|
63
|
+
'rec.vocab': ['Live transcript', 'Words to listen for', 'Names and terms that are often misheard, separated by commas.'],
|
|
64
|
+
'rec.final': ['After Stop', 'Transcribe the whole meeting again after Stop', 'Your notes are written from this final transcript.'],
|
|
65
|
+
'rec.retain': ['Audio', 'Delete kept audio after', 'The transcript and the notes stay.', { never: 'Never', 90: '90 days', 30: '30 days', 7: '7 days' }],
|
|
66
|
+
'time.zone': ['Where and when', 'Time zone', 'Empty means the time zone of this computer.'],
|
|
67
|
+
'time.weekStart': ['Where and when', 'Week starts on', '', { mon: 'Monday', sun: 'Sunday' }],
|
|
68
|
+
'time.workStart': ['Your day', 'Working hours start', 'Free time, prep at risk and the end of the day use your working hours.'],
|
|
69
|
+
'time.workEnd': ['Your day', 'Working hours end', ''],
|
|
70
|
+
'time.prep': ['Your day', 'Free time needed before a meeting with prep', 'With less than this, the meeting shows as prep at risk.', { 15: '15 min', 30: '30 min', 45: '45 min', 60: '1 hour' }],
|
|
71
|
+
'time.buffer': ['Your day', 'Buffer between meetings', 'Back-to-back meetings with less than this get a tidy-up suggestion on the Calendar.', { 0: 'None', 5: '5 min', 10: '10 min', 15: '15 min' }],
|
|
72
|
+
'time.tint': ['Ambience', 'Tint the top bar with the time of day', ''],
|
|
73
|
+
'time.strip': ['Ambience', 'Show the daylight strip', 'The sun or the moon on its arc, with sunrise or sunset.'],
|
|
74
|
+
'share.mode': ['Sending', 'Before a scheduled share goes out', 'Until a sender is chosen in Connectors, every scheduled share is a ready draft and a notification.', { draft: 'Make a draft for me to check', send: 'Send on schedule' }],
|
|
75
|
+
'sess.max': ['Housekeeping', 'Keep at most this many running', 'When a new session is needed, the longest idle background session closes first. A session you opened yourself is never closed for you.', { 2: '2', 3: '3', 4: '4', 6: '6', 8: '8' }],
|
|
76
|
+
'sess.idle': ['Housekeeping', 'Close a background session idle for', '', { 15: '15 min', 30: '30 min', 60: '1 hour', never: 'Never' }],
|
|
77
|
+
'sess.share': ['Housekeeping', 'Background jobs', 'Card refresh, imports and final notes can share one session per workspace. It starts faster and costs less.', { ws: 'One session per workspace', job: 'A new session per job' }],
|
|
78
|
+
'look.start': ['Look', 'Start page', 'The page /c3.html opens on.', { brief: 'Briefing', cal: 'Calendar', work: 'Work', init: 'Initiatives', rep: 'Reports' }],
|
|
79
|
+
'upd.check': ['When', 'Check for updates', 'A check asks the npm registry for the latest version. It is the only request PMOS makes by itself.', { press: 'When I press Check', daily: 'Once a day' }],
|
|
80
|
+
'upd.apply': ['When', 'Install updates', 'An app update restarts PMOS. When idle waits until no recording or session runs.', { ask: 'Ask me', idle: 'When PMOS is idle' }],
|
|
81
|
+
'upd.kits': ['When', 'Workspace kits', 'Each workspace carries its own copy of the kit.', { all: 'Update all together', each: 'One at a time' }],
|
|
82
|
+
'upd.channel': ['When', 'Release channel', '', { stable: 'Stable', early: 'Early' }],
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
/* The settings that live in another store, with the words the page shows. The search reads these too. */
|
|
86
|
+
const EXT = Object.freeze({
|
|
87
|
+
'notes.model': ['recording', 'Live transcript', 'Whisper model', 'The bigger model makes fewer mistakes on names and accents.'],
|
|
88
|
+
'notes.accuracy': ['recording', 'Live transcript', 'Decoding', 'More accurate is slower on every line.'],
|
|
89
|
+
'notes.language': ['recording', 'Live transcript', 'Language spoken in meetings', 'Every line is written in this language. Detect guesses for each line, and can turn accented English into another language.'],
|
|
90
|
+
'notes.keep': ['recording', 'Audio', 'Keep the audio for replay', 'Kept on this computer, outside every repository. Never sent anywhere.'],
|
|
91
|
+
'notes.clean': ['recording', 'Audio', 'Clean up the replay sound', 'After Stop, the replay gets less rumble and hiss. The transcript does not change.'],
|
|
92
|
+
'notes.jev': ['recording', 'Jev', 'Label lines with Jev', 'Each line goes to TypeSafe for its label. Audio, names and private notes never do.'],
|
|
93
|
+
'notes.check': ['recording', 'Jev', 'Check notes against this workspace with Jev', 'Each captured line and one matching passage go to TypeSafe.'],
|
|
94
|
+
'notes.wispr': ['connectors', 'Wispr Flow', 'Fetch Wispr Flow notes after a meeting', 'Your session finds the same meeting in Wispr Flow and adds its notes to yours. Never for a private note.'],
|
|
95
|
+
'people.names': ['connectors', 'Google Calendar', 'Attendee names', 'Use the names of the people in your meetings for prep and People. The names stay on this computer.'],
|
|
96
|
+
'validate.consent': ['sessions', 'Workspace', 'Let Claude validate this workspace', 'Runs the checks against this workspace and records the result.'],
|
|
97
|
+
'sess.cli': ['sessions', 'New sessions', 'Start with', 'What a new session opens.'],
|
|
98
|
+
'sess.tier': ['sessions', 'New sessions', 'Model', 'Auto picks for each job: fast for a card refresh, deep for a PRD or final notes.'],
|
|
99
|
+
'sess.profile': ['sessions', 'New sessions', 'Default profile', 'The account and connectors a new Claude session starts with. Connectors use it too.'],
|
|
100
|
+
'online.on': ['access', 'Online', 'Online', 'Open PMOS from anywhere through a Cloudflare tunnel, signed in with Cloudflare Access.'],
|
|
101
|
+
'look.theme': ['appearance', 'Look', 'Theme', ''],
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
/* About you: [field, label, help, max length, placeholder, textarea]. Stored in the proactive settings, as the Welcome page stores them. */
|
|
105
|
+
export const ABOUT = Object.freeze([
|
|
106
|
+
['name', 'Your name', 'Shown in your Briefing greeting and in notes you write.', 40, 'For example: Dana', false],
|
|
107
|
+
['role', 'Your role', 'Helps PMOS judge which items are yours to decide.', 80, 'For example: Senior PM, Payments', false],
|
|
108
|
+
['work', 'What you do', 'One line. The Briefing uses it to rank what needs you first.', 200, 'In one line, for example: I run checkout and the payment methods', false],
|
|
109
|
+
['duties', 'Your job description', 'What you own, decide and deliver. Paste it from your job description if you like. It stays on this computer.', 1000, 'What you own, decide and deliver.', true],
|
|
110
|
+
]);
|
|
111
|
+
|
|
112
|
+
/** Every row whose label, help or group matches `q` (lower case), as [key, section]. */
|
|
113
|
+
export function rowsMatching(q) {
|
|
114
|
+
const s = String(q || '').trim().toLowerCase();
|
|
115
|
+
if (!s) return [];
|
|
116
|
+
const out = [];
|
|
117
|
+
for (const [k, [g, l, h]] of Object.entries(LABELS)) if ((g + ' ' + l + ' ' + h).toLowerCase().includes(s)) out.push([k, sectionOfStore(k)]);
|
|
118
|
+
for (const [k, [sec, g, l, h]] of Object.entries(EXT)) if ((g + ' ' + l + ' ' + h).toLowerCase().includes(s)) out.push([k, sec]);
|
|
119
|
+
for (const [f, l, h] of ABOUT) if (('your details ' + l + ' ' + h).toLowerCase().includes(s)) out.push(['you.' + f, 'about']);
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
const STORE_SECTION = { ws: 'workspaces', board: 'boards', conn: 'connectors', rec: 'recording', time: 'time', share: 'sharing', sess: 'sessions', look: 'appearance', upd: 'updates' };
|
|
123
|
+
function sectionOfStore(k) { return STORE_SECTION[k.split('.')[0]] || 'workspaces'; }
|
|
124
|
+
|
|
125
|
+
/** A gear, drawn here: the console's sprite has none. */
|
|
126
|
+
export function gearIcon(cls) {
|
|
127
|
+
const NS = 'http://www.w3.org/2000/svg';
|
|
128
|
+
const s = document.createElementNS(NS, 'svg');
|
|
129
|
+
s.setAttribute('class', 'ic' + (cls ? ' ' + cls : '')); s.setAttribute('aria-hidden', 'true'); s.setAttribute('viewBox', '0 0 24 24');
|
|
130
|
+
s.innerHTML = '<circle cx="12" cy="12" r="3"/><path d="M19 12a7 7 0 0 0-.1-1.2l2-1.6-2-3.4-2.4 1a7 7 0 0 0-2-1.2L14 3h-4l-.5 2.6a7 7 0 0 0-2 1.2l-2.4-1-2 3.4 2 1.6a7 7 0 0 0 0 2.4l-2 1.6 2 3.4 2.4-1a7 7 0 0 0 2 1.2L10 21h4l.5-2.6a7 7 0 0 0 2-1.2l2.4 1 2-3.4-2-1.6c.1-.4.1-.8.1-1.2"/>';
|
|
131
|
+
return s;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function ensureCss() {
|
|
135
|
+
if (typeof document === 'undefined' || document.querySelector('link[data-c3-settings]')) return;
|
|
136
|
+
const l = document.createElement('link'); l.rel = 'stylesheet'; l.href = new URL('./c3-settings.css', import.meta.url).href; l.setAttribute('data-c3-settings', ''); document.head.appendChild(l);
|
|
137
|
+
}
|
|
138
|
+
/* Every control is a primitives.js button(); its children are the console3 icon and text, and its class carries the look (as c3-reports.js does). */
|
|
139
|
+
const bt = (attrs, ...kids) => {
|
|
140
|
+
const { onclick, disabled, class: cls, type, ...rest } = attrs || {};
|
|
141
|
+
const b = button({ label: '', variant: 'ghost', size: 'sm', onClick: onclick, disabled, attrs: { ...rest, class: cx('c3set-b', cls) } });
|
|
142
|
+
b.replaceChildren(...kids.flat().filter((k) => k != null && k !== false));
|
|
143
|
+
return b;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const ZONES = ['Asia/Jakarta', 'Asia/Makassar', 'Asia/Jayapura', 'Asia/Singapore', 'Asia/Kuala_Lumpur', 'Asia/Bangkok', 'Asia/Tokyo', 'Asia/Kolkata', 'Australia/Sydney', 'Europe/London', 'Europe/Berlin', 'America/New_York', 'America/Los_Angeles', 'UTC'];
|
|
147
|
+
/* Choices the mockup draws as a select even though they are short. */
|
|
148
|
+
const AS_SELECT = new Set(['conn.cal.window', 'conn.cal.refresh', 'conn.jira.refresh', 'conn.wispr.wait']);
|
|
149
|
+
const qs = (o) => Object.entries(o).filter(([, v]) => v != null && v !== '').map(([k, v]) => k + '=' + encodeURIComponent(v)).join('&');
|
|
150
|
+
|
|
151
|
+
/** Apply the settings the console itself draws: the start tab is read by the shell; tint and the daylight strip are attributes on the root. */
|
|
152
|
+
export function applyLook(root, settings) {
|
|
153
|
+
if (!root || !settings) return;
|
|
154
|
+
ensureCss();
|
|
155
|
+
const v = (k) => { const s = settings.find((x) => x.key === k); return s ? s.value : undefined; };
|
|
156
|
+
root.setAttribute('data-c3-tint', v('time.tint') === false ? 'off' : 'on');
|
|
157
|
+
root.setAttribute('data-c3-strip', v('time.strip') === false ? 'off' : 'on');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function mountSettings(host, ctx = {}) {
|
|
161
|
+
ensureCss();
|
|
162
|
+
const M = {
|
|
163
|
+
sec: SECTIONS.some((s) => s[0] === ctx.section) ? ctx.section : 'about', q: '', ws: ctx.ws || '', data: null, err: {},
|
|
164
|
+
engine: null, people: null, consent: null, online: null, profiles: null, typesafe: null, kits: {}, about: null, running: null,
|
|
165
|
+
tplEdit: null, wsEdit: null, wsDrop: null, addPath: '',
|
|
166
|
+
};
|
|
167
|
+
const root = el('div', { class: 'brief c3set', 'data-c3set': '' });
|
|
168
|
+
const nav = el('ul', { class: 'list', role: 'list', 'data-c3set-nav': '' });
|
|
169
|
+
const segRow = el('div', { class: 'st-seg', 'data-c3set-seg': '' });
|
|
170
|
+
const body = el('div', { class: 'st-body', 'data-c3set-body': '' });
|
|
171
|
+
const search = el('input', { type: 'text', id: 'c3set-q', placeholder: 'Search settings', 'aria-label': 'Search settings', autocomplete: 'off', 'data-c3set-search': '',
|
|
172
|
+
oninput: () => { M.q = search.value.trim(); draw(); } });
|
|
173
|
+
const wsSel = el('select', { id: 'c3set-ws', 'aria-label': 'Workspace these settings apply to', 'data-c3set-ws': '', onchange: () => { M.ws = wsSel.value; loadWs(); } });
|
|
174
|
+
root.append(el('div', { class: 'calwrap' },
|
|
175
|
+
el('div', { class: 'st-hd' }, el('div', { class: 'dim st-lead' }, 'Each setting has one home here. The pages link to it.'),
|
|
176
|
+
el('div', { class: 'tools' }, search, el('label', { class: 'dim st-help', for: 'c3set-ws' }, 'Workspace'), wsSel)),
|
|
177
|
+
segRow,
|
|
178
|
+
el('div', { class: 'st-wrap' },
|
|
179
|
+
el('section', { class: 'card sect st-nav', 'aria-label': 'Settings sections' },
|
|
180
|
+
el('div', { class: 'sh2' }, el('h2', null, gearIcon('sm'), 'Settings ', el('span', { class: 'bdg g' }, String(SECTIONS.length)))), nav),
|
|
181
|
+
body)));
|
|
182
|
+
host.replaceChildren(root);
|
|
183
|
+
|
|
184
|
+
const wsName = (id) => { const w = (ctx.workspaces || []).find((x) => x.id === id); return w ? (w.label || w.name || w.id) : id || 'no workspace'; };
|
|
185
|
+
const S = (k) => (M.data ? M.data.settings.find((x) => x.key === k) : null);
|
|
186
|
+
const val = (k) => { const s = S(k); return s ? s.value : undefined; };
|
|
187
|
+
const override = (k) => (M.data ? M.data.overrides.find((o) => o.key === k) : null);
|
|
188
|
+
const labelOf = (k) => (LABELS[k] ? LABELS[k][1] : EXT[k] ? EXT[k][2] : k);
|
|
189
|
+
|
|
190
|
+
/* ── reads ── */
|
|
191
|
+
async function loadStore() {
|
|
192
|
+
const r = await pmGet('/api/settings?' + qs({ ws: M.ws }));
|
|
193
|
+
M.data = r && r.ok ? r : { settings: [], overrides: [], categories: ['Discover', 'Plan', 'Do', 'Learn'], error: (r && r.error) || 'The settings could not be read.' };
|
|
194
|
+
applyLook(ctx.root, M.data.settings); draw();
|
|
195
|
+
}
|
|
196
|
+
async function loadEngine() { const r = await pmGet('/api/notes/engine?' + qs({ ws: M.ws })); M.engine = r && r.ok ? r : { error: (r && r.error) || 'not read' }; draw(); }
|
|
197
|
+
async function loadWs() {
|
|
198
|
+
M.people = null; M.consent = null; draw();
|
|
199
|
+
await Promise.all([loadStore(), loadEngine(),
|
|
200
|
+
M.ws ? pmGet('/api/people?' + qs({ ws: M.ws })).then((r) => { M.people = r && r.ok !== false && r.settings ? { on: !!r.settings.calendar_names } : { error: (r && r.error) || 'not read' }; }) : null,
|
|
201
|
+
M.ws ? pmGet('/api/validate/repo?' + qs({ ws: M.ws })).then((r) => { M.consent = r && r.ok !== false ? { on: !!r.consent } : { error: (r && r.error) || 'not read' }; }) : null]);
|
|
202
|
+
draw();
|
|
203
|
+
}
|
|
204
|
+
function loadOnce() {
|
|
205
|
+
pmGet('/api/online').then((r) => { M.online = r && r.ok !== false ? r : { error: (r && r.error) || 'not read' }; draw(); });
|
|
206
|
+
pmGet('/api/profiles').then((r) => { M.profiles = r && r.ok !== false ? r : { error: (r && r.error) || 'not read', profiles: [] }; draw(); });
|
|
207
|
+
pmGet('/api/typesafe').then((r) => { M.typesafe = r && r.ok ? r.key : null; draw(); });
|
|
208
|
+
pmGet('/api/proactive/settings').then((r) => { const s = r && r.ok && r.settings; M.about = s ? { name: s.name || '', role: (s.about || {}).role || '', work: (s.about || {}).work || '', duties: (s.about || {}).duties || '' } : { error: (r && r.error) || 'not read' }; draw(); });
|
|
209
|
+
pmGet('/api/sessions').then((r) => { M.running = r && Array.isArray(r.sessions) ? r.sessions : null; draw(); });
|
|
210
|
+
for (const w of ctx.workspaces || []) pmGet('/api/kit/status?' + qs({ ws: w.id })).then((r) => { M.kits[w.id] = r && r.ok ? r : { error: (r && r.error) || 'not read' }; draw(); });
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/* ── writes: each save says so in a toast, with Undo when the old value can be put back ── */
|
|
214
|
+
async function setStore(key, value, quiet) {
|
|
215
|
+
const s = S(key) || {}; const old = s.value;
|
|
216
|
+
const r = await pmPost('/api/settings', { key, value, ws: s.scope === 'ws' ? M.ws : undefined });
|
|
217
|
+
if (!r || !r.ok) { M.err[key] = (r && r.error) || 'Not saved.'; draw(); return false; }
|
|
218
|
+
delete M.err[key];
|
|
219
|
+
await loadStore();
|
|
220
|
+
if (!quiet) toast('Saved: ' + labelOf(key), old === undefined ? undefined : () => setStore(key, old, true));
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
async function resetStore(key) {
|
|
224
|
+
const s = S(key) || {}; const old = s.value;
|
|
225
|
+
const r = await pmPost('/api/settings', { key, reset: true, ws: s.scope === 'ws' ? M.ws : undefined });
|
|
226
|
+
if (r && r.ok) { await loadStore(); toast('Back to the default: ' + labelOf(key), () => setStore(key, old, true)); }
|
|
227
|
+
}
|
|
228
|
+
async function resetSection(id) {
|
|
229
|
+
const keys = M.data.settings.filter((s) => s.changed && sectionOfStore(s.key) === id);
|
|
230
|
+
const was = keys.map((s) => [s.key, s.value]);
|
|
231
|
+
for (const s of keys) await pmPost('/api/settings', { key: s.key, reset: true, ws: s.scope === 'ws' ? M.ws : undefined });
|
|
232
|
+
await loadStore();
|
|
233
|
+
toast('Section is back to its defaults', async () => { for (const [k, v] of was) await setStore(k, v, true); });
|
|
234
|
+
}
|
|
235
|
+
async function notesSwitch(patch, key, undoPatch) {
|
|
236
|
+
const r = await pmPost('/api/notes/switch', M.ws ? { ws: M.ws, ...patch } : patch);
|
|
237
|
+
if (!r || !r.ok) { M.err[key] = (r && r.error) || 'Not saved.'; draw(); return; }
|
|
238
|
+
delete M.err[key]; M.engine = r; draw();
|
|
239
|
+
toast('Saved: ' + labelOf(key), undoPatch ? () => notesSwitch(undoPatch, key) : undefined);
|
|
240
|
+
}
|
|
241
|
+
async function saveAbout(field, value) {
|
|
242
|
+
const a = M.about; const old = a[field];
|
|
243
|
+
const next = { ...a, [field]: value };
|
|
244
|
+
const r = await pmPost('/api/proactive/settings', { name: next.name.slice(0, 40), about: { role: next.role, work: next.work, duties: next.duties } });
|
|
245
|
+
if (!r || !r.ok) { M.err['you.' + field] = (r && r.error) || 'Not saved.'; draw(); return; }
|
|
246
|
+
delete M.err['you.' + field]; M.about = next; draw();
|
|
247
|
+
toast('Saved: ' + ABOUT.find((f) => f[0] === field)[1], () => saveAbout(field, old));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/* ── building blocks (console3 markup: .card.sect > .sh2, ul.list > li.nrow, .seg, .tag, .chip2, .fold) ── */
|
|
251
|
+
const btn = (label, { variant = 'outline', ic, onclick, attrs, disabled } = {}) => {
|
|
252
|
+
const b = button({ label, variant, size: 'sm', onClick: onclick, disabled, attrs });
|
|
253
|
+
if (ic) b.querySelector(SEL.btnLabel).prepend(ic === 'gear' ? gearIcon('sm') : c3ic(ic, 'sm'));
|
|
254
|
+
return b;
|
|
255
|
+
};
|
|
256
|
+
const tag = (text, tone) => el('span', { class: cx('tag', tone) }, text);
|
|
257
|
+
const chip = (text, cls) => el('span', { class: cx('chip2', cls) }, text);
|
|
258
|
+
const scopeTag = (scope) => el('span', { class: 'tag', title: 'Where this setting applies' }, scope === 'ws' ? 'Workspace · ' + wsName(M.ws) : scope === 'board' ? 'Per board' : 'This computer');
|
|
259
|
+
const fold = (ic, text, tone, ...more) => el('div', { class: cx('fold', tone) }, ic ? c3ic(ic, 'sm') : null, el('span', null, text), ...more);
|
|
260
|
+
const sect = (title, kids, { ic, count, right, attrs } = {}) => el('section', { class: 'card sect', ...(attrs || {}) },
|
|
261
|
+
el('div', { class: 'sh2' }, el('h2', null, ic ? c3ic(ic, 'sm') : null, title, count != null ? el('span', { class: 'bdg g' }, String(count)) : null), right || null),
|
|
262
|
+
...kids.flat().filter(Boolean));
|
|
263
|
+
const list = (rows) => { const r = rows.flat().filter(Boolean); return r.length ? el('ul', { class: 'list', role: 'list' }, ...r) : null; };
|
|
264
|
+
const swBtn = (on, label, onClick, attrs) => bt({ type: 'button', class: 'sw', role: 'switch', 'aria-checked': String(!!on), 'aria-label': label, onclick: onClick, ...(attrs || {}) }, el('i', { class: 'sw-dot' }));
|
|
265
|
+
const seg = (label, opts, cur, onPick, disabled) => el('div', { class: 'seg', role: 'group', 'aria-label': label },
|
|
266
|
+
...opts.map(([v, l]) => bt({ type: 'button', 'aria-pressed': String(String(cur) === String(v)), disabled: !!disabled, 'data-v': String(v), onclick: () => onPick(v) }, l)));
|
|
267
|
+
const select = (id, label, opts, cur, onPick, disabled) => el('select', { id, 'aria-label': label, disabled: !!disabled, onchange: (e) => onPick(e.target.value) },
|
|
268
|
+
...opts.map(([v, l]) => el('option', { value: String(v), selected: String(cur) === String(v) }, l)));
|
|
269
|
+
const locked = (text) => el('span', { class: 'chip2' }, c3ic('lock', 'sm'), text);
|
|
270
|
+
const secLink = (id, label) => bt({ type: 'button', class: 'chip2 lk', onclick: () => goSec(id) }, label);
|
|
271
|
+
const tabLink = (tab, label) => bt({ type: 'button', class: 'chip2 lk', onclick: () => ctx.goTab && ctx.goTab(tab) }, label);
|
|
272
|
+
const help = (...kids) => el('div', { class: 'dim st-help' }, ...kids);
|
|
273
|
+
/** One setting row: label, scope tag and help on the left; the control and Reset on the right. */
|
|
274
|
+
function frame({ key, label, help: h, scope, control, changed, onReset, env, err, wide, note, lock, forId, ic }) {
|
|
275
|
+
return el('li', { class: cx('nrow st-row', wide && 'wide', ic && 'has-ki'), 'data-c3set-row': key },
|
|
276
|
+
ic ? el('span', { class: cx('ki', ic[1]) }, c3ic(ic[0])) : null,
|
|
277
|
+
el('div', { class: 'nb' },
|
|
278
|
+
forId ? el('label', { class: 'tt', for: forId }, label) : el('span', { class: 'tt' }, label),
|
|
279
|
+
scope || changed ? el('div', { class: 'chips' }, scope ? scopeTag(scope) : null, changed ? el('span', { class: 'st-dot', title: 'Changed from the default' }) : null) : null,
|
|
280
|
+
h ? help(h) : null,
|
|
281
|
+
env ? el('div', { class: 'fold w', 'data-c3set-env': env.env }, c3ic('alert', 'sm'), el('span', null, el('b', { class: 'mono' }, env.env + '=' + env.value), ' is set where PMOS starts. ' + env.effect + ' Remove it from your shell profile, then restart PMOS.')) : null,
|
|
282
|
+
lock ? fold('lock', lock) : null,
|
|
283
|
+
note ? help(note) : null,
|
|
284
|
+
err ? el('div', { class: 'fold w', role: 'alert' }, c3ic('alert', 'sm'), el('span', null, err)) : null),
|
|
285
|
+
el('div', { class: 'acts' }, control, changed && onReset ? btn('Reset', { variant: 'ghost', onclick: onReset, attrs: { 'data-c3set-reset': key } }) : null));
|
|
286
|
+
}
|
|
287
|
+
/** A row for a key of the settings store. */
|
|
288
|
+
function storeRow(key, extra = {}) {
|
|
289
|
+
const s = S(key); const L = LABELS[key];
|
|
290
|
+
if (!s || !L) return null;
|
|
291
|
+
const [, label, h, opts] = L;
|
|
292
|
+
const id = 'c3set-' + key;
|
|
293
|
+
const ov = override(key); const dis = !!(extra.disabled || ov);
|
|
294
|
+
const optList = (s.options || []).map((v) => [v, (opts && opts[v]) || v]);
|
|
295
|
+
let control;
|
|
296
|
+
if (s.type === 'bool') control = swBtn(s.value, label, () => setStore(key, !s.value), { id, 'data-c3set-key': key, disabled: dis });
|
|
297
|
+
else if (s.type === 'choice' && !AS_SELECT.has(key) && optList.length <= 3 && optList.every(([, l]) => l.length < 24)) control = seg(label, optList, s.value, (v) => setStore(key, v), dis);
|
|
298
|
+
else if (s.type === 'choice') control = select(id, label, optList, s.value, (v) => setStore(key, v), dis);
|
|
299
|
+
else if (s.type === 'hour') { const hs = []; for (let h2 = s.min; h2 <= s.max; h2++) hs.push([h2, String(h2).padStart(2, '0') + ':00']); control = select(id, label, hs, s.value, (v) => setStore(key, Number(v)), dis); }
|
|
300
|
+
else if (s.type === 'zone') {
|
|
301
|
+
const here = (() => { try { return Intl.DateTimeFormat().resolvedOptions().timeZone; } catch { return ''; } })();
|
|
302
|
+
control = select(id, label, [['', 'This computer\'s' + (here ? ' (' + here + ')' : '')], ...ZONES.map((z) => [z, z.replace(/_/g, ' ')])], s.value, (v) => setStore(key, v), dis);
|
|
303
|
+
} else if (s.type === 'text') control = el('input', { type: 'text', id, value: s.value, maxlength: String(s.max || 400), 'aria-label': label, autocomplete: 'off', disabled: dis, onchange: (e) => setStore(key, e.target.value) });
|
|
304
|
+
return frame({ key, label, help: h, scope: s.scope, control, changed: s.changed, onReset: () => resetStore(key), env: ov, err: M.err[key], note: extra.note, lock: extra.lock,
|
|
305
|
+
forId: s.type === 'choice' && control.tagName !== 'SELECT' ? null : id, wide: s.type === 'text' && (s.max || 0) > 100 });
|
|
306
|
+
}
|
|
307
|
+
function extRow(key, control, extra = {}) {
|
|
308
|
+
const [, , label, h] = EXT[key];
|
|
309
|
+
return frame({ key, label, help: h, scope: extra.scope, control, env: override(key), err: M.err[key], note: extra.note, lock: extra.lock, forId: extra.forId });
|
|
310
|
+
}
|
|
311
|
+
const lockedRow = (key, label, h, text, scope) => frame({ key, label, help: h, scope, control: locked(text) });
|
|
312
|
+
/** A feature PMOS does not have yet (PM decision 9): the row shows, with its control off and the reason. */
|
|
313
|
+
const notYet = (key, label, h, why, scope, control) => frame({ key, label, help: h, scope, lock: why, control: control || btn('Not available yet', { variant: 'ghost', disabled: true }) });
|
|
314
|
+
const group = (title, rows, ic, opts) => { const l = list(rows); return l ? sect(title, [l], { ic, ...(opts || {}) }) : null; };
|
|
315
|
+
const groupOf = (sec, title, ic) => group(title, Object.keys(LABELS).filter((k) => sectionOfStore(k) === sec && LABELS[k][0] === title).map((k) => storeRow(k)), ic);
|
|
316
|
+
const goSec = (id) => { M.sec = id; M.q = ''; search.value = ''; draw(); };
|
|
317
|
+
|
|
318
|
+
/* ── sections ── */
|
|
319
|
+
function aboutSec() {
|
|
320
|
+
const a = M.about;
|
|
321
|
+
if (!a) return [fold(null, 'Reading what you told PMOS.')];
|
|
322
|
+
if (a.error) return [el('div', { class: 'fold w', role: 'alert' }, c3ic('alert', 'sm'), el('span', null, 'What you told PMOS could not be read: ' + a.error + '.'))];
|
|
323
|
+
const rows = ABOUT.map(([f, label, h, max, ph, area]) => {
|
|
324
|
+
const id = 'c3set-you-' + f;
|
|
325
|
+
const input = area
|
|
326
|
+
? el('textarea', { id, rows: '5', maxlength: String(max), placeholder: ph, 'aria-label': label, oninput: (e) => { count.textContent = e.target.value.length + ' of ' + max + ' characters'; }, onchange: (e) => saveAbout(f, e.target.value.trim()) }, a[f])
|
|
327
|
+
: el('input', { type: 'text', id, value: a[f], maxlength: String(max), placeholder: ph, 'aria-label': label, autocomplete: 'off', onchange: (e) => saveAbout(f, e.target.value.trim()) });
|
|
328
|
+
const count = area ? el('span', { class: 'dim mono st-count' }, a[f].length + ' of ' + max + ' characters') : null;
|
|
329
|
+
return frame({ key: 'you.' + f, label, help: h, scope: 'computer', control: area ? [input, count] : input, err: M.err['you.' + f], forId: id, wide: !!area });
|
|
330
|
+
});
|
|
331
|
+
return [group('Your details', rows, 'user'),
|
|
332
|
+
fold('spark', 'The Welcome page asks for these four on first run. Change them here any time.', null, btn('See Welcome', { variant: 'ghost', ic: 'next', onclick: () => { location.href = '/welcome'; } }))];
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function workspacesSec() {
|
|
336
|
+
const wss = ctx.workspaces || [];
|
|
337
|
+
const rows = wss.map((w) => {
|
|
338
|
+
const cur = w.id === M.ws;
|
|
339
|
+
const editing = M.wsEdit === w.id; const dropping = M.wsDrop === w.id;
|
|
340
|
+
const nameIn = editing ? el('input', { type: 'text', value: w.label || w.name || '', 'aria-label': 'New name for ' + (w.name || w.id), id: 'c3set-wsname', maxlength: '60' }) : null;
|
|
341
|
+
let nb; let acts;
|
|
342
|
+
if (editing) {
|
|
343
|
+
nb = [nameIn];
|
|
344
|
+
acts = [btn('Rename', { variant: 'primary', onclick: async () => { const old = w.label; const nm = nameIn.value.trim(); const r = await pmPost('/api/workspaces/rename', { id: w.id, name: nm }); if (r && r.ok !== false) { w.label = nm; M.wsEdit = null; fillWs(); toast('Renamed to ' + nm); } else M.err['ws:' + w.id] = (r && r.error) || 'Not renamed.'; draw(); void old; } }),
|
|
345
|
+
btn('Cancel', { variant: 'ghost', onclick: () => { M.wsEdit = null; draw(); } })];
|
|
346
|
+
} else if (dropping) {
|
|
347
|
+
nb = [el('span', { class: 'tt' }, w.label || w.name || w.id), fold('alert', 'Removing a workspace takes it off this list. Its folder, boards and notes stay where they are.', 'w')];
|
|
348
|
+
acts = [btn('Remove workspace', { variant: 'primary', attrs: { 'data-c3set-forget': w.id }, onclick: async () => { const r = await pmPost('/api/workspaces/forget', { id: w.id }); if (r && r.ok) { ctx.workspaces = ctx.workspaces.filter((x) => x.id !== w.id); M.wsDrop = null; if (M.ws === w.id) { M.ws = (ctx.workspaces[0] || {}).id || ''; loadWs(); } fillWs(); toast('Removed ' + (w.name || w.id)); } else M.err['ws:' + w.id] = (r && (r.error || r.reason)) || 'Not removed.'; draw(); } }),
|
|
349
|
+
btn('Keep it', { variant: 'ghost', onclick: () => { M.wsDrop = null; draw(); } })];
|
|
350
|
+
} else {
|
|
351
|
+
nb = [el('span', { class: 'tt' }, w.label || w.name || w.id),
|
|
352
|
+
el('div', { class: 'chips' }, cur ? tag('Current', 'acc') : null, w.home ? tag('PMOS itself') : null, w.root ? chip(w.root, 'mono') : null, w.branch ? chip('branch ' + w.branch) : null,
|
|
353
|
+
chip((w.sessions || 0) + ((w.sessions || 0) === 1 ? ' session' : ' sessions')))];
|
|
354
|
+
acts = [cur ? null : btn('Use', { onclick: () => { M.ws = w.id; fillWs(); loadWs(); toast('Settings now show ' + (w.label || w.name || w.id)); } }),
|
|
355
|
+
btn('Rename', { variant: 'ghost', onclick: () => { M.wsEdit = w.id; draw(); } }),
|
|
356
|
+
w.home || wss.length < 2 ? null : btn('Remove', { variant: 'ghost', onclick: () => { M.wsDrop = w.id; draw(); } })];
|
|
357
|
+
}
|
|
358
|
+
return el('li', { class: 'nrow', 'data-c3set-ws-row': w.id }, el('span', { class: cx('ki', cur && 'hot') }, c3ic('repo')),
|
|
359
|
+
el('div', { class: 'nb' }, ...nb, M.err['ws:' + w.id] ? fold('alert', M.err['ws:' + w.id], 'w') : null), el('div', { class: 'acts' }, ...acts.filter(Boolean)));
|
|
360
|
+
});
|
|
361
|
+
const pathIn = el('input', { type: 'text', id: 'c3set-addpath', placeholder: '/Users/you/code/my-product', 'aria-label': 'Folder of the workspace to add', value: M.addPath, autocomplete: 'off', oninput: (e) => { M.addPath = e.target.value; } });
|
|
362
|
+
const addRow = frame({ key: 'ws.add', label: 'Add a workspace', forId: 'c3set-addpath', help: 'Give the folder of a repository. PMOS reads what is there and changes nothing until you start work in it. The first-run steps are on the Welcome page.',
|
|
363
|
+
err: M.err.add, wide: true, control: [pathIn, btn('Add', { ic: 'plus', attrs: { 'data-c3set-add': '' }, onclick: async () => {
|
|
364
|
+
const rootPath = M.addPath.trim(); if (!rootPath) { pathIn.focus(); return; }
|
|
365
|
+
const r = await pmPost('/api/workspaces', { root: rootPath, focus: false });
|
|
366
|
+
if (r && r.ok) { (ctx.workspaces = ctx.workspaces || []).push(r.workspace); M.addPath = ''; delete M.err.add; toast('Added ' + (r.workspace.name || rootPath)); fillWs(); } else M.err.add = (r && r.error) || 'Not added.';
|
|
367
|
+
draw();
|
|
368
|
+
} })] });
|
|
369
|
+
return [sect('Your workspaces', [rows.length ? list(rows) : fold(null, 'No workspace is registered yet.'), list([addRow])], { ic: 'repo', count: wss.length,
|
|
370
|
+
right: btn('Add a workspace', { ic: 'plus', onclick: () => { const i = root.querySelector('#c3set-addpath'); if (i) i.focus(); } }) }),
|
|
371
|
+
groupOf('workspaces', 'Start', 'repo')];
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function boardsSec() {
|
|
375
|
+
const s = S('board.templates'); const cats = (M.data && M.data.categories) || ['Discover', 'Plan', 'Do', 'Learn'];
|
|
376
|
+
const tpl = s ? s.value : {};
|
|
377
|
+
const draft = M.tplEdit;
|
|
378
|
+
const flow = (stages) => el('div', { class: 'st-flow' }, ...stages.flatMap((st, i) => [i ? c3ic('next', 'sm') : null, el('span', { class: 'chip2', title: st.cat + ' category' }, el('i', { class: 'st-cat-dot', 'data-cat': st.cat }), st.name)]).filter(Boolean));
|
|
379
|
+
const rows = Object.entries(tpl).map(([id, t]) => {
|
|
380
|
+
const editing = draft && draft.id === id;
|
|
381
|
+
const d = editing ? draft.t : t;
|
|
382
|
+
const editor = editing ? [el('ol', { class: 'st-stages' }, ...d.stages.map((st, i) => el('li', null,
|
|
383
|
+
el('input', { type: 'text', value: st.name, 'aria-label': 'Stage ' + (i + 1) + ' name', maxlength: '40', oninput: (e) => { st.name = e.target.value; } }),
|
|
384
|
+
el('select', { 'aria-label': 'Stage ' + (i + 1) + ' category', onchange: (e) => { st.cat = e.target.value; } }, ...cats.map((c) => el('option', { value: c, selected: c === st.cat }, c))),
|
|
385
|
+
bt({ type: 'button', class: 'ib sm', 'aria-label': 'Remove ' + st.name, disabled: d.stages.length < 2, onclick: () => { d.stages.splice(i, 1); draw(); } }, c3ic('x', 'sm'))))),
|
|
386
|
+
help('Each stage maps to Discover, Plan, Do or Learn, so the portfolio can compare boards with different workflows. Up to 12 stages. Boards that already use this template keep their own copy.'),
|
|
387
|
+
M.err['board.templates'] ? fold('alert', M.err['board.templates'], 'w') : null] : [];
|
|
388
|
+
const acts = editing
|
|
389
|
+
? [d.stages.length < 12 ? btn('Add stage', { ic: 'plus', onclick: () => { d.stages.push({ name: 'New stage', cat: 'Do' }); draw(); } }) : null,
|
|
390
|
+
btn('Save', { variant: 'primary', attrs: { 'data-c3set-tpl-save': id }, onclick: async () => { const next = JSON.parse(JSON.stringify(tpl)); next[id] = d; if (await setStore('board.templates', next)) M.tplEdit = null; draw(); } }),
|
|
391
|
+
btn('Cancel', { variant: 'ghost', onclick: () => { M.tplEdit = null; delete M.err['board.templates']; draw(); } })]
|
|
392
|
+
: [btn('Edit template', { attrs: { 'data-c3set-tpl-edit': id }, onclick: () => { M.tplEdit = { id, t: JSON.parse(JSON.stringify(t)) }; draw(); } })];
|
|
393
|
+
return el('li', { class: 'nrow', 'data-c3set-tpl': id }, el('span', { class: 'ki' }, c3ic('cols')),
|
|
394
|
+
el('div', { class: 'nb' }, el('span', { class: 'tt' }, editing ? 'Editing ' + d.name : t.name), el('div', { class: 'chips' }, val('board.template') === id ? tag('Default for new boards', 'acc') : null), flow(d.stages), ...editor),
|
|
395
|
+
el('div', { class: 'acts' }, ...acts.filter(Boolean)));
|
|
396
|
+
});
|
|
397
|
+
return [groupOf('boards', 'Defaults', 'cols'),
|
|
398
|
+
group('PMOS itself', [lockedRow('board.self', 'PMOS itself', 'The PMOS workspace keeps its derived ladder: the stage follows the artifacts and is never moved by hand.', 'Derived from the work')], 'cols'),
|
|
399
|
+
sect('Workflow templates', [list(rows), help('Change a template and only boards created afterwards use it. Each board keeps its own copy.')], { ic: 'cols', count: rows.length,
|
|
400
|
+
right: s && s.changed ? btn('Reset templates', { variant: 'ghost', onclick: () => resetStore('board.templates') }) : null }),
|
|
401
|
+
sect('Boards in ' + wsName(M.ws), [list([notYet('board.list', 'Each board and its workflow', 'A board\'s workflow belongs to the board. This list will be a shortcut to each one.', 'Boards with their own workflows come with the Initiatives update. Until then a workspace has one board.', 'ws',
|
|
402
|
+
btn('Open Initiatives', { variant: 'ghost', ic: 'next', onclick: () => ctx.goTab && ctx.goTab('init') }))])], { ic: 'cols' })];
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function profileNow() {
|
|
406
|
+
const p = M.profiles; const id = sessionDefaults().profile || (p && p.prefs && p.prefs.claude) || null;
|
|
407
|
+
return { id, x: p && p.profiles ? p.profiles.find((y) => y.id === id) : null };
|
|
408
|
+
}
|
|
409
|
+
async function chooseProfile(id) {
|
|
410
|
+
const before = profileNow().id;
|
|
411
|
+
setSessionDefaults({ profile: id || null });
|
|
412
|
+
const r = await pmPost('/api/profiles/prefer', { agent: 'claude', profile: id || null });
|
|
413
|
+
if (r && r.ok) { if (M.profiles) M.profiles.prefs = r.prefs; delete M.err['sess.profile']; toast('Saved: default profile', () => chooseProfile(before)); } else M.err['sess.profile'] = (r && r.error) || 'Not saved.';
|
|
414
|
+
draw();
|
|
415
|
+
}
|
|
416
|
+
const profileSelect = (id) => {
|
|
417
|
+
const profs = (M.profiles && M.profiles.profiles) || [];
|
|
418
|
+
return select(id, 'Default profile', [['', 'None'], ...profs.map((x) => [x.id, x.label && x.label !== x.id ? x.label + ' (' + x.id + ')' : x.id])], profileNow().id || '', chooseProfile);
|
|
419
|
+
};
|
|
420
|
+
function connectorsSec() {
|
|
421
|
+
const { id, x } = profileNow();
|
|
422
|
+
const mcp = x ? x.mcp : [];
|
|
423
|
+
const has = (re) => mcp.some((m) => re.test(m));
|
|
424
|
+
const reach = (on) => tag(on ? 'In the profile' : 'Not in the profile', on ? 'ok' : 'warn');
|
|
425
|
+
const via = (on, name) => (on ? 'Through profile ' + id + '.' : id ? 'Profile ' + id + ' has no ' + name + ' connector.' : 'No default profile is set.');
|
|
426
|
+
const people = M.people;
|
|
427
|
+
const w = M.engine && M.engine.whisper;
|
|
428
|
+
const sender = val('conn.sender');
|
|
429
|
+
const senderName = (LABELS['conn.sender'][3] || {})[sender] || sender;
|
|
430
|
+
const setSender = (v) => setStore('conn.sender', v);
|
|
431
|
+
return [
|
|
432
|
+
sect('Claude profile', [list([
|
|
433
|
+
frame({ key: 'conn.profile', label: 'Profile that reaches your tools', help: 'PMOS holds no passwords or keys. Your Claude session uses the connectors in this profile.', scope: 'computer', forId: 'c3set-conn-profile', control: profileSelect('c3set-conn-profile'), err: M.err['sess.profile'] }),
|
|
434
|
+
frame({ key: 'conn.list', label: 'Connectors in profile ' + (id || 'none'), control: null,
|
|
435
|
+
help: mcp.length ? null : id ? 'This profile lists no connectors.' : 'No default profile is set, so a session starts with Claude Code\'s own connectors.' })]),
|
|
436
|
+
mcp.length ? el('div', { class: 'chips st-pad' }, ...mcp.map((m) => tag(m))) : null,
|
|
437
|
+
help('Profiles are listed in ', secLink('sessions', 'Claude sessions'), '.')], { ic: 'user', attrs: { 'data-c3set-profile': '' } }),
|
|
438
|
+
sect('Google Calendar', [list([
|
|
439
|
+
notYet('conn.cal.list', 'Calendars to import', 'Pick which of your calendars the import reads.', 'Choosing calendars comes later. Today the import reads the calendars your session can see.', 'computer'),
|
|
440
|
+
storeRow('conn.cal.window'), storeRow('conn.cal.refresh'), storeRow('conn.cal.private'),
|
|
441
|
+
M.ws ? extRow('people.names', people && !people.error ? swBtn(people.on, EXT['people.names'][2], async () => { const r = await pmPost('/api/people/decide', { ws: M.ws, act: 'setting', calendar_names: !people.on }); if (r && r.ok !== false) { people.on = !people.on; toast('Saved: attendee names'); } else M.err['people.names'] = (r && r.error) || 'Not saved.'; draw(); }) : el('span', { class: 'dim' }, people ? people.error : 'Reading'), { scope: 'ws' }) : null]),
|
|
442
|
+
el('div', { class: 'iacts' }, btn('Import now', { ic: 'undo', disabled: !M.ws || !has(/calendar/i), onclick: async () => { const r = await pmPost('/api/sources/refresh', { ws: M.ws, source: 'calendar' }); toast(r && r.ok ? 'Import started in your Claude session. Events appear in Calendar when it finishes.' : ((r && r.error) || 'The import did not start.')); } }),
|
|
443
|
+
el('span', { class: 'dim st-help' }, via(has(/calendar/i), 'calendar') + ' PMOS never edits your calendar.'))], { ic: 'cal', right: reach(has(/calendar/i)) }),
|
|
444
|
+
sect('Jira', [list([notYet('jira.map', 'Jira project for each board', 'A board backed by a Jira project mirrors its sprints and reads ticket status from it, with each Jira status mapped to a board stage.', 'Mapping boards and statuses comes with the Initiatives update.', 'board')]),
|
|
445
|
+
help(via(has(/atlassian|jira/i), 'Jira'))], { ic: 'org', right: reach(has(/atlassian|jira/i)) }),
|
|
446
|
+
group('Jira behaviour', [storeRow('conn.jira.refresh'),
|
|
447
|
+
lockedRow('jira.push', 'Creating or updating tickets', 'PMOS drafts tickets from the PRD. Nothing reaches Jira until you select it (D88).', 'You approve every push', 'ws'),
|
|
448
|
+
lockedRow('jira.sprints', 'Sprints and burndown', 'A board backed by Jira shows its sprints. Native sprints are only for boards with no tracker (D5).', 'Mirrored from Jira, read-only', 'board')], 'org'),
|
|
449
|
+
sect('Wispr Flow', [list([w ? extRow('notes.wispr', swBtn(w.wispr !== false, EXT['notes.wispr'][2], () => notesSwitch({ wispr: w.wispr === false }, 'notes.wispr', { wispr: w.wispr !== false }), { disabled: !!override('notes.wispr') }), { scope: 'computer' }) : null, storeRow('conn.wispr.wait')]),
|
|
450
|
+
help(via(has(/wispr/i), 'Wispr Flow'))], { ic: 'mic', right: reach(has(/wispr/i)) }),
|
|
451
|
+
sect('Sender for shared dashboards', [el('div', { class: 'iacts', 'data-c3set-sender': '' },
|
|
452
|
+
...[['slack', 'Slack'], ['gmail', 'Gmail drafts']].map(([v, l]) => btn(sender === v ? l : 'Use ' + l, { variant: sender === v ? 'primary' : 'outline', ic: sender === v ? 'check' : null, attrs: { 'data-v': v }, onclick: () => setSender(v) })),
|
|
453
|
+
sender !== 'none' ? btn('Disconnect', { variant: 'ghost', onclick: () => setSender('none') }) : null),
|
|
454
|
+
help(sender === 'none' ? 'Not connected. Until you pick one, a scheduled share is a ready draft.' : 'Your session sends through ' + senderName + '. Sharing decides whether scheduled shares go out or wait as drafts.'),
|
|
455
|
+
M.err['conn.sender'] ? fold('alert', M.err['conn.sender'], 'w') : null], { ic: 'mail', right: tag(sender === 'none' ? 'Not connected' : senderName, sender === 'none' ? 'warn' : 'ok') }),
|
|
456
|
+
];
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function recordingSec() {
|
|
460
|
+
const e = M.engine; const w = e && e.whisper; const j = e && e.jev;
|
|
461
|
+
if (!e) return [fold(null, 'Reading the recording setup.')];
|
|
462
|
+
if (e.error || !w) return [fold('alert', 'The recording setup could not be read: ' + (e.error || 'no answer') + '.', 'w')];
|
|
463
|
+
const langOv = override('notes.language');
|
|
464
|
+
const langName = (v) => ((w.languages || []).find((x) => x.value === v) || {}).label || v;
|
|
465
|
+
const engineName = w.live === 'qwen' ? 'Qwen3-ASR' : w.ready ? 'Whisper' : 'Not set up';
|
|
466
|
+
const summary = [w.live === 'qwen' || w.ready ? engineName + ' on this computer' : 'No local transcription yet', langOv ? langName(langOv.value) + ' (set by ' + langOv.env + ')' : w.language === 'auto' ? 'language detected for each line' : langName(w.language) + ', forced',
|
|
467
|
+
val('rec.echo') === false || override('rec.echo') ? 'echo removal off' : 'echo removal on', val('rec.final') === false || override('rec.final') ? 'no pass after Stop' : 'a final pass after Stop'];
|
|
468
|
+
const warn = !!(langOv || w.language === 'auto' || !w.ready);
|
|
469
|
+
const fin = w.final;
|
|
470
|
+
const flip = (k, on) => ({ [k]: on });
|
|
471
|
+
return [
|
|
472
|
+
sect('Recording now uses', [el('div', { class: 'st-now' }, summary.join(' · ')),
|
|
473
|
+
!w.ready ? fold('alert', 'Local transcription is not set up (' + (w.missing || []).join(' and ') + ' missing). Open a note and choose Set up in a terminal.', 'w')
|
|
474
|
+
: warn ? fold('alert', langOv ? 'An environment variable overrides the language you pick below.' : 'Detect can turn accented English into another language. Pick a language if transcripts come back wrong.', 'w') : null,
|
|
475
|
+
help('Wispr Flow notes after a meeting: ' + (w.wispr === false ? 'off' : 'on') + '. That setting is in ', secLink('connectors', 'Connectors'), '.')],
|
|
476
|
+
{ ic: 'mic', right: tag(warn ? (w.ready ? 'Check language' : 'Not set up') : 'Ready', warn ? 'warn' : 'ok'), attrs: { 'data-c3set-recnow': '' } }),
|
|
477
|
+
group('Live transcript', [storeRow('rec.sources'),
|
|
478
|
+
lockedRow('notes.engine', 'Live engine', 'Both run on this computer. PMOS uses Qwen3-ASR when it is set up, because it handles English, Indonesian and Chinese in one model.', w.live === 'qwen' ? 'Qwen3-ASR' : w.ready ? 'Whisper' : 'None set up', 'computer'),
|
|
479
|
+
w.models && w.live !== 'qwen' ? extRow('notes.model', select('c3set-notes-model', EXT['notes.model'][2], w.models.map((m) => [m.value, m.label]), w.model, (v) => notesSwitch({ model: v }, 'notes.model', { model: w.model })), { scope: 'computer', forId: 'c3set-notes-model' }) : null,
|
|
480
|
+
w.accuracies && w.live !== 'qwen' ? extRow('notes.accuracy', seg(EXT['notes.accuracy'][2], w.accuracies.map((a) => [a.value, a.value === 'fast' ? 'Faster' : 'More accurate']), w.accuracy, (v) => notesSwitch({ accuracy: v }, 'notes.accuracy', { accuracy: w.accuracy })), { scope: 'computer' }) : null,
|
|
481
|
+
w.languages ? extRow('notes.language', select('c3set-notes-language', EXT['notes.language'][2], w.languages.map((l) => [l.value, l.label]), w.language, (v) => notesSwitch({ language: v }, 'notes.language', { language: w.language }), !!langOv), { scope: 'computer', forId: 'c3set-notes-language' }) : null,
|
|
482
|
+
w.language !== 'auto' ? storeRow('rec.guard') : null, storeRow('rec.echo'), storeRow('rec.settle'), M.ws ? storeRow('rec.vocab') : null], 'mic'),
|
|
483
|
+
group('After Stop', [storeRow('rec.final'),
|
|
484
|
+
fin && val('rec.final') !== false ? lockedRow('notes.final', 'Final transcriber', 'Picked from what is set up on this computer.', fin.transcriber === 'qwen' ? 'Qwen3-ASR' : fin.transcriber === 'parakeet' ? 'Parakeet' : fin.transcriber === 'whisper' ? 'Whisper, beam search' : 'None', 'computer') : null,
|
|
485
|
+
fin && val('rec.final') !== false ? lockedRow('notes.speakers', 'Separate speakers', 'Labels who said what. It runs when the speaker model is set up on this computer.', fin.speakers ? 'On, the speaker model is set up' : 'Off, no speaker model', 'computer') : null], 'mic'),
|
|
486
|
+
group('Audio', [extRow('notes.keep', swBtn(w.keep_audio !== false, EXT['notes.keep'][2], () => notesSwitch(flip('keep_audio', w.keep_audio === false), 'notes.keep', flip('keep_audio', w.keep_audio !== false))), { scope: 'computer' }),
|
|
487
|
+
w.keep_audio !== false ? extRow('notes.clean', swBtn(w.clean_audio === true, EXT['notes.clean'][2], () => notesSwitch(flip('clean_audio', w.clean_audio !== true), 'notes.clean', flip('clean_audio', w.clean_audio === true))), { scope: 'computer' }) : null,
|
|
488
|
+
w.keep_audio !== false ? storeRow('rec.retain') : null], 'mic'),
|
|
489
|
+
M.ws && j ? group('Jev', [extRow('notes.jev', swBtn(!!j.jev, EXT['notes.jev'][2], () => notesSwitch({ jev: !j.jev }, 'notes.jev', { jev: !!j.jev })), { scope: 'ws', note: j.paused || null }),
|
|
490
|
+
extRow('notes.check', swBtn(!!j.check, EXT['notes.check'][2], () => notesSwitch({ check: !j.check }, 'notes.check', { check: !!j.check })), { scope: 'ws' })], 'mic') : null,
|
|
491
|
+
];
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function timeSec() {
|
|
495
|
+
const now = new Date();
|
|
496
|
+
const part = dayPartOf(now);
|
|
497
|
+
const hh = (h) => String(h).padStart(2, '0') + ':00';
|
|
498
|
+
return [fold('clock', 'Now: ' + part[0].toUpperCase() + part.slice(1) + ' · ' + String(now.getHours()).padStart(2, '0') + ':' + String(now.getMinutes()).padStart(2, '0') + ' · working hours ' + hh(val('time.workStart') ?? 9) + ' to ' + hh(val('time.workEnd') ?? 18) + '. Parts of the day follow your clock; the Briefing never reshuffles while you read.', null),
|
|
499
|
+
group('Where and when', [storeRow('time.zone'),
|
|
500
|
+
notYet('time.place', 'Location for daylight', 'Sunrise and sunset are worked out on this computer from an approximate place. No network (D6).', 'Today the place comes from your time zone. Picking a place comes later.', 'computer'),
|
|
501
|
+
storeRow('time.weekStart')], 'sun'),
|
|
502
|
+
group('Your day', [storeRow('time.workStart'), storeRow('time.workEnd'),
|
|
503
|
+
lockedRow('time.parts', 'Parts of the day', 'Morning until 11:00, midday until 14:00, afternoon until 17:00, evening until 21:00, then night.', 'Follows your clock', 'computer'),
|
|
504
|
+
lockedRow('time.idle', 'When cards re-rank', 'A new part of the day never reshuffles cards while you read.', 'Only when idle or on reload', 'computer'),
|
|
505
|
+
storeRow('time.prep'), storeRow('time.buffer')], 'sun'),
|
|
506
|
+
group('Ambience', [storeRow('time.tint'), storeRow('time.strip'),
|
|
507
|
+
lockedRow('time.motion', 'Motion', 'Fades and slides when cards move. Reduce motion in your system settings to turn them off.', 'Same as system', 'computer'),
|
|
508
|
+
lockedRow('time.weather', 'Weather', 'Weather needs a network request, which PMOS does not make (D6).', 'Off', 'computer')], 'sun')];
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function sharingSec() {
|
|
512
|
+
if (!M.ws) return [fold(null, 'Pick a workspace at the top to set who its dashboards go to.')];
|
|
513
|
+
const s = S('share.audiences'); const aud = s ? s.value : {};
|
|
514
|
+
const sender = val('conn.sender');
|
|
515
|
+
const senderName = (LABELS['conn.sender'][3] || {})[sender] || sender;
|
|
516
|
+
const save = (next, msg, prev) => setStore('share.audiences', next, true).then((ok) => { if (ok) toast(msg, () => setStore('share.audiences', prev, true)); });
|
|
517
|
+
const rows = Object.entries(aud).map(([name, people]) => {
|
|
518
|
+
const inp = el('input', { type: 'text', placeholder: 'Add a person', 'aria-label': 'Add a person to ' + name, maxlength: '60', autocomplete: 'off' });
|
|
519
|
+
const addP = () => { const v = inp.value.trim(); if (!v) return; const next = JSON.parse(JSON.stringify(aud)); next[name] = [...people, v]; save(next, 'Added ' + v + ' to ' + name, aud); };
|
|
520
|
+
inp.addEventListener('keydown', (e) => { if (e.key === 'Enter') addP(); });
|
|
521
|
+
return el('li', { class: 'nrow', 'data-c3set-aud': name }, el('span', { class: 'ki' }, c3ic('user')),
|
|
522
|
+
el('div', { class: 'nb' }, el('span', { class: 'tt' }, name), el('div', { class: 'chips' }, scopeTag('ws')),
|
|
523
|
+
help(name === 'Leadership' ? 'Short: one sentence per report, and what needs a decision.' : name === 'Team' ? 'Fuller: the numbers behind each sentence.' : ''),
|
|
524
|
+
el('div', { class: 'chips' }, ...(people.length ? people.map((p) => bt({ type: 'button', class: 'chip2 lk', 'aria-label': 'Remove ' + p + ' from ' + name, title: 'Remove ' + p + ' from ' + name, onclick: () => { const next = JSON.parse(JSON.stringify(aud)); next[name] = people.filter((x) => x !== p); save(next, 'Removed ' + p, aud); } }, p, c3ic('x', 'sm'))) : [el('span', { class: 'dim' }, 'Nobody yet')]))),
|
|
525
|
+
el('div', { class: 'acts' }, inp, btn('Add', { onclick: addP })));
|
|
526
|
+
});
|
|
527
|
+
return [sect('Sender', [help(sender === 'none' ? 'Nothing is sent until you pick a sender. ' : 'Shares can go out through ' + senderName + '. ', 'Change it in ', secLink('connectors', 'Connectors'), '.')],
|
|
528
|
+
{ ic: 'mail', right: tag(sender === 'none' ? 'Not connected' : senderName, sender === 'none' ? 'warn' : 'ok') }),
|
|
529
|
+
group('Sending', [storeRow('share.mode', { disabled: sender === 'none', lock: sender === 'none' ? 'Connect a sender in Connectors first. Until then every share is a draft.' : null })], 'mail'),
|
|
530
|
+
group('Detail level', [notYet('share.detail', 'Detail for each audience', 'Short is one sentence per report and what needs a decision. Full adds the numbers behind each sentence.', 'The Share sheet sets the detail for each send. A default per audience comes later.', 'ws')], 'mail'),
|
|
531
|
+
sect('Audiences', [list(rows), M.err['share.audiences'] ? fold('alert', M.err['share.audiences'], 'w') : null], { ic: 'user', count: rows.length }),
|
|
532
|
+
sect('Scheduled shares', [fold(null, 'Each schedule is set on its dashboard in Reports. When dashboards can be scheduled, they are listed here so you can see everything that goes out.', null, btn('Open Reports', { variant: 'ghost', ic: 'next', onclick: () => ctx.goTab && ctx.goTab('rep') }))], { ic: 'clock' })];
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function accessSec() {
|
|
536
|
+
const o = M.online;
|
|
537
|
+
const later = [sect('Open on your phone', [list([notYet('access.pair', 'Phone access', 'Scan a one-time code with your phone to pair it.', 'Pairing with a code comes later. Today, switch Online on and sign in from the phone.', 'computer')])], { ic: 'phone' }),
|
|
538
|
+
sect('Signed-in devices', [list([notYet('access.devices', 'Devices', 'The computers and phones signed in to this PMOS, each with Sign out.', 'The device list comes later. Cloudflare Access lists sign-ins today.', 'computer')])], { ic: 'user' })];
|
|
539
|
+
if (!o) return [fold(null, 'Reading the Online switch.'), ...later];
|
|
540
|
+
if (o.error) return [fold('alert', 'The Online switch could not be read: ' + o.error + '.', 'w'), ...later];
|
|
541
|
+
if (o.envHost) return [group('Online', [frame({ key: 'online.on', label: 'Online', help: 'PMOS_PUBLIC_HOST=' + o.envHost + ' was set when PMOS started, so it is already online that way.', control: locked('On, from the environment') })], 'phone'), ...later];
|
|
542
|
+
const F = [['host', 'Public hostname', 'pmos.example.com'], ['tunnel', 'Tunnel name', 'pmos-mac'], ['team', 'Cloudflare team domain', 'myteam.cloudflareaccess.com'], ['audience', 'Access audience tag', '64 letters and digits'], ['email', 'Email allowed in', 'you@example.com']];
|
|
543
|
+
const inputs = {};
|
|
544
|
+
const fieldRows = F.map(([k, l, ph]) => { inputs[k] = el('input', { type: 'text', id: 'c3set-online-' + k, value: o[k] || '', placeholder: ph, disabled: !o.canChange, autocomplete: 'off' }); return frame({ key: 'online.' + k, label: l, forId: 'c3set-online-' + k, control: inputs[k] }); });
|
|
545
|
+
const post = async (patch, msg) => { const r = await pmPost('/api/online', patch); if (r && r.ok !== false) { M.online = r; delete M.err['online.on']; toast(msg || 'Saved: Online'); } else M.err['online.on'] = (r && r.error) || 'Not saved.'; draw(); };
|
|
546
|
+
const values = () => Object.fromEntries(F.map(([k]) => [k, inputs[k].value.trim()]));
|
|
547
|
+
const running = o.on && o.running && o.running.running;
|
|
548
|
+
return [group('Online', [extRow('online.on', swBtn(o.on, 'Online', () => post({ ...values(), on: !o.on }, o.on ? 'Online is off' : 'Online is on'), { disabled: !o.canChange }), { scope: 'computer',
|
|
549
|
+
note: !o.canChange ? 'Only the computer that runs PMOS can change this.' : !o.cloudflared ? 'cloudflared is not installed. Install it with brew install cloudflared, then switch Online on.' : null })], 'phone'),
|
|
550
|
+
running ? fold('check', 'Tunnel running · cloudflared found' + (o.lastRefusal ? '' : ' · no refused sign-in'), 'a') : null,
|
|
551
|
+
sect('Tunnel and sign-in', [list(fieldRows), o.canChange ? el('div', { class: 'iacts' }, btn('Save', { variant: 'primary', onclick: () => post(values()) })) : null,
|
|
552
|
+
o.lastRefusal ? help('Last refused sign-in: ' + o.lastRefusal.reason) : null], { ic: 'phone' }),
|
|
553
|
+
...later];
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function sessionsSec() {
|
|
557
|
+
const d = sessionDefaults(); const p = M.profiles;
|
|
558
|
+
const profs = (p && p.profiles) || [];
|
|
559
|
+
const setDef = (patch, key, undo) => { setSessionDefaults(patch); toast('Saved: ' + EXT[key][2], () => { setSessionDefaults(undo); draw(); }); draw(); };
|
|
560
|
+
const def = profileNow().id;
|
|
561
|
+
const profRows = profs.map((x) => el('li', { class: 'nrow', 'data-c3set-prof': x.id }, el('span', { class: cx('ki', def === x.id && 'hot') }, c3ic('user')),
|
|
562
|
+
el('div', { class: 'nb' }, el('span', { class: 'tt' }, (x.label || x.id) + ' ', el('span', { class: 'dim mono' }, x.id)),
|
|
563
|
+
el('div', { class: 'chips' }, def === x.id ? tag('Default', 'acc') : null, x.detected ? tag('Detected') : null, chip('Connectors: ' + (x.mcp && x.mcp.length ? x.mcp.join(', ') : 'none')))),
|
|
564
|
+
el('div', { class: 'acts' }, def === x.id ? null : btn('Make default', { variant: 'ghost', onclick: () => chooseProfile(x.id) }))));
|
|
565
|
+
const run = M.running || [];
|
|
566
|
+
const max = val('sess.max');
|
|
567
|
+
const runRows = run.map((s) => el('li', { class: 'nrow' }, el('span', { class: 'ki' }, c3ic('term')),
|
|
568
|
+
el('div', { class: 'nb' }, el('span', { class: 'tt' }, s.title || s.name || s.label || s.id),
|
|
569
|
+
el('div', { class: 'chips' }, s.ws ? chip(wsName(s.ws)) : null, s.profile ? chip('profile ' + s.profile) : null, s.state || s.status ? tag(String(s.state || s.status), /idle/i.test(String(s.state || s.status)) ? 'warn' : 'ok') : null))));
|
|
570
|
+
return [group('New sessions', [
|
|
571
|
+
extRow('sess.cli', seg('Start with', [['claude', 'Claude Code'], ['shell', 'A shell']], d.cli, (v) => setDef({ cli: v }, 'sess.cli', { cli: d.cli })), { scope: 'computer', note: 'Kept in this browser, as the dock keeps it.' }),
|
|
572
|
+
extRow('sess.tier', seg('Model', [['auto', 'Auto'], ['fast', 'Fast'], ['balanced', 'Balanced'], ['deep', 'Deep']], d.tier, (v) => setDef({ tier: v }, 'sess.tier', { tier: d.tier })), { scope: 'computer' }),
|
|
573
|
+
extRow('sess.profile', profileSelect('c3set-sess-profile'), { scope: 'computer', forId: 'c3set-sess-profile', note: profs.length ? null : p ? 'No profile was found. Make one in the main app\'s Settings, Profiles.' : 'Reading profiles.' }),
|
|
574
|
+
lockedRow('sess.flags', 'Launch flags', 'Flags for the claude command, such as a permission mode.', 'Set when you start a session', 'computer')], 'term'),
|
|
575
|
+
profs.length ? sect('Profiles', [list(profRows), help('A new profile is made in the main app\'s Settings, Profiles.')], { ic: 'user', count: profs.length }) : null,
|
|
576
|
+
groupOf('sessions', 'Housekeeping', 'term'),
|
|
577
|
+
sect('Running now' + (M.running ? ' · ' + run.length + (max ? ' of ' + max : '') : ''), [M.running ? (run.length ? list(runRows) : fold(null, 'No sessions running.')) : fold(null, 'Reading the sessions.'),
|
|
578
|
+
help('Close a session from the dock at the bottom of the page.')], { ic: 'term' }),
|
|
579
|
+
M.ws ? group('Workspace', [extRow('validate.consent', M.consent && !M.consent.error ? swBtn(M.consent.on, EXT['validate.consent'][2], async () => { const r = await pmPost('/api/validate/repo', { ws: M.ws, consent: !M.consent.on }); if (r && r.ok !== false) { M.consent.on = !!r.consent; toast('Saved: validate consent'); } else M.err['validate.consent'] = (r && r.error) || 'Not saved.'; draw(); }) : el('span', { class: 'dim' }, M.consent ? M.consent.error : 'Reading'), { scope: 'ws' })], 'term') : null];
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function privacySec() {
|
|
583
|
+
const w = M.engine && M.engine.whisper; const j = M.engine && M.engine.jev; const o = M.online;
|
|
584
|
+
const jevOn = !!(j && (j.jev || j.check));
|
|
585
|
+
const L = [
|
|
586
|
+
['Nothing, by default', 'PMOS makes no network request of its own (D69 to D71).', 'Always', 'ok'],
|
|
587
|
+
['Update check', 'Asks the npm registry for the latest version.', val('upd.check') === 'daily' ? 'Once a day' : 'Only when you press Check', val('upd.check') === 'daily' ? 'warn' : ''],
|
|
588
|
+
['Online tunnel', 'Cloudflare carries your pages to your other devices (D89).', o && o.on ? 'On' : 'Off', o && o.on ? 'warn' : ''],
|
|
589
|
+
['Jev (TypeSafe)', 'Note lines and matching passages. Never audio, names or private notes (D79).', M.ws ? (jevOn ? 'On in ' : 'Off in ') + wsName(M.ws) : 'Pick a workspace', jevOn ? 'warn' : ''],
|
|
590
|
+
['Your connected tools', 'Calendar, Jira, Wispr Flow and the sender are reached by your Claude session, not by PMOS.', 'Through profile ' + (profileNow().id || 'none'), ''],
|
|
591
|
+
];
|
|
592
|
+
const leave = L.map(([t, h, s, tone]) => el('li', { class: 'nrow' }, el('span', { class: cx('ki', tone === 'ok' ? 'hot' : tone === 'warn' ? 't-warn' : '') }, c3ic(tone === 'ok' ? 'lock' : 'out')),
|
|
593
|
+
el('div', { class: 'nb' }, el('span', { class: 'tt' }, t), help(h)), el('div', { class: 'acts' }, tag(s, tone))));
|
|
594
|
+
return [sect('What leaves this computer', [list(leave)], { ic: 'shield', attrs: { 'data-c3set-leaves': '' } }),
|
|
595
|
+
sect('Keys and files', [list([
|
|
596
|
+
frame({ key: 'typesafe', label: 'TypeSafe key', help: 'Kept on this computer only. It is never shown, here or anywhere.', scope: 'computer', control: M.typesafe ? tag(M.typesafe.present ? 'Saved' : 'None', M.typesafe.present ? 'ok' : '') : tag('Reading') }),
|
|
597
|
+
frame({ key: 'state', label: 'Data folder', help: 'Notes, audio, settings and caches. Outside every repository, so no commit can carry them.', scope: 'computer', control: chip((M.data && M.data.state_dir) || '', 'mono st-wrap-text') }),
|
|
598
|
+
lockedRow('priv.cards', 'Send card text to Jev for icons', 'Card kinds and icons are chosen by code rules on this computer (D7).', 'Off', 'computer'),
|
|
599
|
+
notYet('priv.export', 'Export or reset every setting', 'Save every setting to a file, or put them all back to their defaults. Your workspaces, notes and audio stay.', 'Export and reset all come later. Reset one setting or one section with its Reset button.', 'computer')]),
|
|
600
|
+
el('div', { class: 'iacts' }, btn('Export settings', { ic: 'out', disabled: true }), btn('Reset all settings', { variant: 'ghost', disabled: true }), w && w.keep_audio !== false ? btn('Recording settings', { variant: 'ghost', ic: 'next', onclick: () => goSec('recording') }) : null)], { ic: 'lock' })];
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function appearanceSec() {
|
|
604
|
+
const pref = ctx.themePref || ctx.theme;
|
|
605
|
+
return [group('Look', [extRow('look.theme', seg('Theme', [['system', 'Same as system'], ['light', 'Light'], ['dark', 'Dark']], pref, (v) => { const before = ctx.themePref || ctx.theme; if (ctx.setTheme) ctx.setTheme(v); ctx.themePref = v; ctx.theme = v; toast('Saved: theme', () => { if (ctx.setTheme) ctx.setTheme(before); ctx.themePref = before; ctx.theme = before; draw(); }); draw(); }), { scope: 'computer', note: 'Kept in this browser.' }),
|
|
606
|
+
storeRow('look.start'),
|
|
607
|
+
lockedRow('look.icons', 'Card icons', 'Chosen from the built-in icon set by the card kind (D7).', 'Chosen by code rules', 'computer')], 'eye')];
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function updatesSec() {
|
|
611
|
+
const rows = (ctx.workspaces || []).map((w) => {
|
|
612
|
+
const k = M.kits[w.id];
|
|
613
|
+
const kit = k && k.kit; const behind = k && k.kitBehind;
|
|
614
|
+
return el('li', { class: 'nrow', 'data-c3set-kit': w.id }, el('span', { class: 'ki' }, c3ic('repo')),
|
|
615
|
+
el('div', { class: 'nb' }, el('span', { class: 'tt' }, w.label || w.name || w.id),
|
|
616
|
+
el('div', { class: 'chips' }, chip(!k ? 'Reading' : k.error ? k.error : kit && kit.present ? 'kit ' + (kit.version || 'vendored by hand') : 'no kit in this workspace', 'mono'),
|
|
617
|
+
k && k.latest ? (behind ? tag(k.latest + ' available', 'warn') : tag('Up to date', 'ok')) : null),
|
|
618
|
+
M.err['kit:' + w.id] ? fold('alert', M.err['kit:' + w.id], 'w') : null),
|
|
619
|
+
el('div', { class: 'acts' }, behind ? btn('Update', { variant: 'primary', onclick: async () => { const r = await pmPost('/api/kit/update', { ws: w.id }); if (r && r.ok) { M.kits[w.id] = { ...M.kits[w.id], ...r, kitBehind: false }; toast('Updated the kit in ' + (w.label || w.name || w.id)); } else M.err['kit:' + w.id] = (r && r.error) || 'Not updated.'; draw(); } }) : null));
|
|
620
|
+
});
|
|
621
|
+
const first = (ctx.workspaces || [])[0];
|
|
622
|
+
const st = first && M.kits[first.id];
|
|
623
|
+
const app = st && st.app; const appBehind = st && st.appBehind;
|
|
624
|
+
const appRow = frame({ key: 'upd.app', label: 'PMOS app ' + ((app && app.version) || ''),
|
|
625
|
+
help: appBehind ? 'The app update restarts PMOS. Run it from the main app\'s Settings, Updates, which waits for the restart.' : 'Press Check now to ask the npm registry for the latest version.',
|
|
626
|
+
control: [st && st.latest ? tag(appBehind ? st.latest + ' available' : 'Up to date', appBehind ? 'acc' : 'ok') : null,
|
|
627
|
+
btn('Check now', { ic: 'undo', disabled: !first, attrs: { 'data-c3set-check': '' }, onclick: async () => {
|
|
628
|
+
for (const w of ctx.workspaces || []) { const r = await pmPost('/api/kit/check', { ws: w.id }); M.kits[w.id] = r && r.ok ? r : { ...(M.kits[w.id] || {}), error: (r && r.error) || 'The check failed.' }; }
|
|
629
|
+
const s2 = first && M.kits[first.id]; toast(s2 && s2.appBehind ? 'PMOS ' + s2.latest + ' is available' : 'Checked. PMOS is up to date'); draw();
|
|
630
|
+
} }),
|
|
631
|
+
appBehind ? btn('Open the main app', { variant: 'primary', onclick: () => { location.href = '/'; } }) : null] });
|
|
632
|
+
return [sect('PMOS app', [list([appRow])], { ic: 'cloud' }), sect('Kit in each workspace', [list(rows)], { ic: 'repo', count: rows.length }), groupOf('updates', 'When', 'cloud')];
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const BUILD = { about: aboutSec, workspaces: workspacesSec, boards: boardsSec, connectors: connectorsSec, recording: recordingSec, time: timeSec, sharing: sharingSec, access: accessSec, sessions: sessionsSec, privacy: privacySec, appearance: appearanceSec, updates: updatesSec };
|
|
636
|
+
|
|
637
|
+
function searchView() {
|
|
638
|
+
const hits = rowsMatching(M.q);
|
|
639
|
+
const secs = SECTIONS.filter(([, t, , i]) => (t + ' ' + i).toLowerCase().includes(M.q.toLowerCase()));
|
|
640
|
+
if (!hits.length && !secs.length) return [el('div', { class: 'fold', 'data-c3set-none': '' }, 'No setting matches "' + M.q + '". Try a word like language, Jira or hours.')];
|
|
641
|
+
const by = new Map();
|
|
642
|
+
for (const [k, sec] of hits) { if (!by.has(sec)) by.set(sec, []); by.get(sec).push(k); }
|
|
643
|
+
return [secs.length ? el('div', { class: 'iacts' }, ...secs.map(([id, t, ic]) => btn(t, { ic, onclick: () => goSec(id) }))) : null,
|
|
644
|
+
...[...by.entries()].map(([sec, keys]) => {
|
|
645
|
+
const meta = SECTIONS.find((s) => s[0] === sec);
|
|
646
|
+
const other = (k) => { const a = k.startsWith('you.') ? ABOUT.find((f) => 'you.' + f[0] === k) : null; return a ? [a[1], a[2]] : [EXT[k][2], EXT[k][3]]; };
|
|
647
|
+
const rows = keys.map((k) => (LABELS[k] ? storeRow(k) : frame({ key: k, label: other(k)[0], help: other(k)[1], control: btn('Open in ' + meta[1], { variant: 'ghost', ic: 'next', onclick: () => goSec(sec) }) })));
|
|
648
|
+
return sect(meta[1], [list(rows)], { ic: meta[2], count: rows.filter(Boolean).length, right: btn('Open section', { variant: 'ghost', ic: 'next', onclick: () => goSec(sec) }) });
|
|
649
|
+
})];
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function fillWs() {
|
|
653
|
+
wsSel.replaceChildren(...(ctx.workspaces || []).map((w) => el('option', { value: w.id, selected: w.id === M.ws }, w.label || w.name || w.id)));
|
|
654
|
+
if (!(ctx.workspaces || []).length) wsSel.append(el('option', { value: '' }, 'No workspace'));
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/* The section list is built once and updated in place: rebuilding it under the pointer would lose the press that moved the focus. */
|
|
658
|
+
const navBtns = SECTIONS.map(([id, t, ic]) => {
|
|
659
|
+
const badge = el('span', { class: 'bdg', hidden: true });
|
|
660
|
+
const b = bt({ type: 'button', class: 'nrow', 'aria-current': 'false', 'data-c3set-sec': id, onclick: () => goSec(id) },
|
|
661
|
+
el('span', { class: 'ki' }, c3ic(ic)), el('span', { class: 'nb' }, el('span', { class: 'tt' }, t)), badge);
|
|
662
|
+
return { id, b, badge };
|
|
663
|
+
});
|
|
664
|
+
nav.replaceChildren(...navBtns.map((x) => el('li', null, x.b)));
|
|
665
|
+
const segBtns = SECTIONS.map(([id, t, ic]) => ({ id, b: bt({ type: 'button', 'aria-pressed': 'false', 'data-c3set-seg-sec': id, onclick: () => goSec(id) }, c3ic(ic, 'sm'), t) }));
|
|
666
|
+
segRow.replaceChildren(el('div', { class: 'seg', role: 'group', 'aria-label': 'Settings sections' }, ...segBtns.map((x) => x.b)));
|
|
667
|
+
function paintNav() {
|
|
668
|
+
for (const x of navBtns) {
|
|
669
|
+
const n = changedIn(x.id);
|
|
670
|
+
x.b.setAttribute('aria-current', String(!M.q && M.sec === x.id));
|
|
671
|
+
x.badge.hidden = !n; x.badge.textContent = n ? String(n) : ''; x.badge.title = n ? n + ' changed from the default' : '';
|
|
672
|
+
}
|
|
673
|
+
for (const x of segBtns) x.b.setAttribute('aria-pressed', String(!M.q && M.sec === x.id));
|
|
674
|
+
}
|
|
675
|
+
/* A press that takes the focus out of a field blurs it before its click lands: redraw after that click, not under it. */
|
|
676
|
+
let pressing = false;
|
|
677
|
+
document.addEventListener('pointerdown', () => { pressing = true; }, true);
|
|
678
|
+
document.addEventListener('pointerup', () => { setTimeout(() => { pressing = false; }, 0); }, true);
|
|
679
|
+
function drawAfterBlur() {
|
|
680
|
+
const go = () => { if (draw.pending) { draw.pending = false; draw(); } };
|
|
681
|
+
if (pressing) document.addEventListener('click', () => setTimeout(go, 0), { capture: true, once: true }); else go();
|
|
682
|
+
}
|
|
683
|
+
const changedIn = (id) => (M.data ? M.data.settings.filter((s) => s.changed && sectionOfStore(s.key) === id).length : 0);
|
|
684
|
+
function draw() {
|
|
685
|
+
// never rebuild under a field the PM is typing in
|
|
686
|
+
const a = document.activeElement;
|
|
687
|
+
if (a && body.contains(a) && ((a.tagName === 'INPUT' && a.type === 'text') || a.tagName === 'TEXTAREA')) { if (!draw.pending) { draw.pending = true; a.addEventListener('blur', drawAfterBlur, { once: true }); } return; }
|
|
688
|
+
paintNav();
|
|
689
|
+
if (M.q) { body.replaceChildren(...searchView().filter(Boolean)); return; }
|
|
690
|
+
const [id, title, ic, intro] = SECTIONS.find((s) => s[0] === M.sec);
|
|
691
|
+
const needsStore = id !== 'about';
|
|
692
|
+
const parts = M.data || !needsStore ? BUILD[id]() : [fold(null, 'Reading the settings.')];
|
|
693
|
+
const n = changedIn(id);
|
|
694
|
+
body.replaceChildren(el('div', { class: 'st-sec', 'data-c3set-section': id, 'aria-labelledby': 'c3set-h-' + id },
|
|
695
|
+
el('div', { class: 'lh' }, el('span', { class: 'ki hot' }, c3ic(ic)), el('div', null, el('b', { id: 'c3set-h-' + id }, title), intro ? el('span', { class: 'dim' }, intro) : null),
|
|
696
|
+
el('div', { class: 'acts' }, n ? btn('Reset section', { variant: 'ghost', onclick: () => resetSection(id), attrs: { 'data-c3set-reset-sec': id } }) : null)),
|
|
697
|
+
M.data && M.data.error && needsStore ? fold('alert', M.data.error, 'w') : null,
|
|
698
|
+
...parts.filter(Boolean)));
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
fillWs(); draw(); loadWs(); loadOnce();
|
|
702
|
+
return { show: (sec) => { if (SECTIONS.some((s) => s[0] === sec)) goSec(sec); }, refresh: () => { loadWs(); pmGet('/api/sessions').then((r) => { M.running = r && Array.isArray(r.sessions) ? r.sessions : null; draw(); }); } };
|
|
703
|
+
}
|