create-pmos 0.523.0 → 0.524.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 +44 -16
- 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/changelog.json +43 -0
- package/workspace/guide/agent-capabilities.md +6 -3
- package/workspace/guide/home.md +1 -1
- package/workspace/guide/horizon-scan.md +2 -2
- package/workspace/guide/idle-lab.md +46 -0
- package/workspace/guide/index.md +2 -0
- package/workspace/guide/moments.md +43 -0
- package/workspace/guide/welcome.md +37 -28
- package/workspace/lib/bg-queue.js +226 -0
- package/workspace/lib/driver-run.js +8 -1
- package/workspace/lib/first-reveal.js +311 -0
- package/workspace/lib/idle-lab.js +275 -0
- package/workspace/lib/moments.js +241 -0
- package/workspace/lib/morning-goals.js +191 -0
- package/workspace/lib/onboard-fundamentals.js +205 -0
- package/workspace/lib/onboarding-progress.js +13 -1
- package/workspace/lib/orchestrator-spawn.js +9 -2
- package/workspace/lib/project-onboard.js +86 -6
- package/workspace/lib/proposals.js +9 -5
- package/workspace/lib/route-modules.js +4 -0
- package/workspace/lib/routes-lab.js +137 -0
- package/workspace/lib/routes-loose.js +1 -0
- package/workspace/lib/routes-moments.js +42 -0
- package/workspace/lib/routes-morning-goals.js +109 -0
- package/workspace/lib/routes-onboard.js +28 -8
- package/workspace/lib/routes-proposals.js +12 -2
- package/workspace/lib/routes-reveal.js +73 -0
- package/workspace/lib/scan-run.js +4 -3
- package/workspace/lib/scan.js +5 -3
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/project-onboard-flow-test.js +70 -57
- package/workspace/public/pm/cozy-ev.css +3 -0
- package/workspace/public/pm/cozy-lab-ins.js +87 -0
- package/workspace/public/pm/cozy-lab.css +86 -0
- package/workspace/public/pm/cozy-lab.js +138 -0
- package/workspace/public/pm/cozy-nav.js +22 -8
- package/workspace/public/pm/cozy-onboard.css +31 -0
- package/workspace/public/pm/cozy-onboard.js +217 -111
- package/workspace/public/pm/cozy-reveal.css +52 -0
- package/workspace/public/pm/cozy-reveal.js +130 -0
- package/workspace/public/pm/home-app.js +38 -3
- package/workspace/public/pm/home-impact.css +12 -0
- package/workspace/public/pm/home-moment.js +41 -0
- package/workspace/public/pm/ini-props.js +15 -6
- package/workspace/public/pm/premium-board.js +2 -0
- package/workspace/public/pm/premium-first.js +1 -1
- package/workspace/public/pm/premium-insights.js +6 -1
- package/workspace/public/pm/quiet-place.js +31 -0
- package/workspace/public/premium-intelligence.html +3 -0
- package/workspace/replaced-tests.json +14 -0
- package/workspace/server.js +46 -5
- package/workspace/test-files.json +2 -0
- package/workspace/test-lists/first-reveal.json +9 -0
- package/workspace/test-lists/idle-lab.json +9 -0
- package/workspace/test-lists/morning-goals.json +9 -0
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
/* pm/cozy-onboard.js — set up a project as one conversation in the Home chat (initiative claude-onboarding-build, direction B on the
|
|
2
2
|
* onboarding design canvas, In development; its link is in planning/prd/claude-onboarding-build-prd.md).
|
|
3
3
|
*
|
|
4
|
-
* One page: it draws in the Home chat's own place (same hm-a / hm-u turns
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* One page: it draws in the Home chat's own place (same hm-a / hm-u turns and the normal composer) and leaves the list where it is.
|
|
5
|
+
* Onboarding build B1 (frames ObA1Setup, ObA3Fundamentals): one line about the login, one question, two ✦, one fundamentals card.
|
|
6
|
+
* 1. The login is checked silently on first open (claude-readiness.js). A pass is one line; only a failure asks, as one ✦ "Sign in".
|
|
7
|
+
* 2. The first question is the product. The composer is open from the first screen; an answer typed before the check passes waits for it.
|
|
8
|
+
* 3. ✦ one proposal for the folder, the read-only search and what needs a sign-in. ✓ runs it; ✕ asks what to change, in the composer.
|
|
9
|
+
* 4. The findings as up to three source tiles, then ✦ "Set up PMOS in <folder> with this context". A missing line says so and has edit.
|
|
10
|
+
* 5. After init: the fundamentals card (lib/onboard-fundamentals.js), each line with its source and one change; then ✦ the first initiative.
|
|
11
|
+
* The calls to action are ✓ and ✕ only. Order is enforced by the server. Every press is the PM's (pmPost). Text from connectors is escaped. */
|
|
8
12
|
import { pmGet, pmPost } from './http.js';
|
|
9
13
|
import { adler } from './cozy-adler.js';
|
|
10
14
|
import { signInTerminal } from './cozy-claude-setup.js';
|
|
@@ -12,7 +16,26 @@ import { signInTerminal } from './cozy-claude-setup.js';
|
|
|
12
16
|
const esc = (s) => String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
13
17
|
const CHECK = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg>';
|
|
14
18
|
const UP = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 19V5M5 12l7-7 7 7"/></svg>';
|
|
15
|
-
const FIELD_LABELS = [['name', 'Name'], ['purpose', 'Purpose'], ['team', 'Team'], ['
|
|
19
|
+
const FIELD_LABELS = [['name', 'Name'], ['purpose', 'Purpose'], ['team', 'Team'], ['timeline', 'Timeline'], ['weekly', 'Weekly'], ['owner', 'Owner'], ['goals', 'Goals']];
|
|
20
|
+
const YES = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg>';
|
|
21
|
+
const NO = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg>';
|
|
22
|
+
const PEN = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 20h4L19 9l-4-4L4 16v4z"/></svg>';
|
|
23
|
+
|
|
24
|
+
/** What the PM asks to change in the first ✦, in words: a folder path, sources to leave out, or no search. PURE. */
|
|
25
|
+
export function readChange(text, labels = []) {
|
|
26
|
+
const t = String(text || '').trim();
|
|
27
|
+
const folder = (/(?:^|\s)((?:~|\/|[A-Za-z]:\\)[^\s,;]+)/.exec(t) || [])[1] || '';
|
|
28
|
+
const skip = /\b(no|skip|don'?t|do not)\s+(search|look)/i.test(t) || /^skip\b/i.test(t);
|
|
29
|
+
const out = labels.filter((l) => new RegExp('\\b' + l.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b', 'i').test(t));
|
|
30
|
+
return { folder, skip, leaveOut: out };
|
|
31
|
+
}
|
|
32
|
+
/** A line about the context, in words: "team is Payments", "owner: Dana", "the weekly is Tue 10:00". Otherwise the words are the purpose. PURE. */
|
|
33
|
+
export function readField(text) {
|
|
34
|
+
const t = String(text || '').trim();
|
|
35
|
+
const m = /^(?:the\s+)?(name|purpose|team|owner|timeline|weekly|goals?)\s*(?:is|are|:|=)\s*(.+)$/i.exec(t);
|
|
36
|
+
if (m) return { [m[1].toLowerCase().replace(/^goal$/, 'goals')]: m[2].trim() };
|
|
37
|
+
return t ? { purpose: t } : {};
|
|
38
|
+
}
|
|
16
39
|
|
|
17
40
|
/** The words of the project answer: the name is the first clause; a Jira key, a team and a folder path are picked up when written plainly. */
|
|
18
41
|
export function readAnswer(text) {
|
|
@@ -31,95 +54,124 @@ export function mountOnboard(host, { ws = '', done = false } = {}) {
|
|
|
31
54
|
sec.innerHTML = '<div class="hm-log ob-log" id="obLog" role="log" aria-live="polite"></div><div class="hm-foot" id="obFoot"></div>';
|
|
32
55
|
host.after(sec); host.hidden = true;
|
|
33
56
|
const log = sec.querySelector('#obLog'), foot = sec.querySelector('#obFoot');
|
|
34
|
-
const S = { data: null, busy: '', note: '', profile: null, said: '', session: null, folderInput: false,
|
|
57
|
+
const S = { data: null, busy: '', note: '', profile: null, said: '', session: null, folderInput: false, stopTerm: null,
|
|
58
|
+
autoChecked: false, chooser: false, queued: '', asking: '', excluded: new Set(), editing: new Set(), changes: [] };
|
|
35
59
|
const post = async (verb, body = {}) => { const r = await pmPost('/api/onboard/' + verb, body); if (r && r.progress) S.data = { ...S.data, ...r }; return r || { ok: false, error: 'PMOS did not answer. Try again.' }; };
|
|
36
60
|
|
|
37
|
-
async function load() { const r = await pmGet('/api/onboard/state'); if (r && r.ok) S.data = r; draw(); }
|
|
61
|
+
async function load() { const r = await pmGet('/api/onboard/state'); if (r && r.ok) S.data = r; draw(); auto(); }
|
|
38
62
|
const P = () => (S.data && S.data.progress) || {};
|
|
39
63
|
const claudeTurn = (body, mood, latest) => `<div class="hm-a ob-turn">${latest ? `<span class="hm-av hm-adler" aria-hidden="true">${adler(mood, 36, { label: false, id: 'ob', dress: true })}</span>` : '<span class="hm-av t-C s28" aria-hidden="true">C</span>'}<div class="hm-ab"><p class="hm-who">Claude</p>${body}</div></div>`;
|
|
40
64
|
const youTurn = (text) => `<div class="hm-u"><div class="hm-bub"><p>${esc(text)}</p></div><span class="hm-you">You</span></div>`;
|
|
41
65
|
const btn = (act, label, cls = '', extra = '') => `<button type="button" class="ut-btn ${cls}" data-ob="${act}"${S.busy ? ' disabled' : ''}${extra}>${esc(S.busy === act ? 'Working…' : label)}</button>`;
|
|
66
|
+
/** ✓ and ✕, icon-only (the PM's rule). */
|
|
67
|
+
const yesNo = (yes, no, what) => `<div class="ob-yn"><button type="button" class="ut-ib lg solid" data-ob="${yes}" aria-label="Yes: ${esc(what)}" title="Yes"${S.busy ? ' disabled' : ''}>${S.busy === yes ? '…' : YES}</button>`
|
|
68
|
+
+ `<button type="button" class="ut-ib lg ob-no" data-ob="${no}" aria-label="No, change something" title="Change something"${S.busy ? ' disabled' : ''}>${NO}</button></div>`;
|
|
69
|
+
const prop = (title, rows, foot2, yes, no, what) => `<div class="ut-card ob-prop" role="group" aria-label="${esc(what)}"><p class="ob-pt"><span class="ob-spark" aria-hidden="true">✦</span><span>${title}</span></p>`
|
|
70
|
+
+ (rows.length ? `<ul class="ob-pl">${rows.map((r) => `<li>${r}</li>`).join('')}</ul>` : '') + (foot2 ? `<p class="ob-note">${foot2}</p>` : '') + yesNo(yes, no, what) + '</div>';
|
|
71
|
+
const link = (act, label, extra = '') => `<button type="button" class="ob-lk" data-ob="${act}"${extra}>${esc(label)}</button>`;
|
|
72
|
+
const profiles = () => ((S.data && S.data.claude) || {}).profiles || [];
|
|
42
73
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
// One row per login that really differs: a pinned copy of the login on this computer shows as that login, and PMOS's own
|
|
47
|
-
// profile is the "separate login" row (bug 2026-11-03: "My usual Claude login" and "PMOS" repeated the other two rows).
|
|
48
|
-
const profiles = c.profiles || [], ownId = c.own || '__own__';
|
|
74
|
+
/** The old explicit chooser, kept for "use another login": one row per login that really differs. */
|
|
75
|
+
function chooserTurn(p) {
|
|
76
|
+
const c = (S.data && S.data.claude) || { profiles: [] }, ownId = c.own || '__own__';
|
|
49
77
|
const row = (id) => (id === '__own__' || (id && id === c.own) ? ownId : (c.alias && c.alias[id]) || id);
|
|
50
|
-
const pick = row(S.profile || p.profile || c.selected || (profiles[0] && profiles[0].id));
|
|
78
|
+
const pick = row(S.profile || p.profile || c.selected || (profiles()[0] && profiles()[0].id));
|
|
79
|
+
const opt = (id, label, note) => `<label class="ob-opt${id === pick ? ' on' : ''}"><input type="radio" name="ob-login" value="${esc(id)}"${id === pick ? ' checked' : ''}><span><b>${esc(label)}</b>${note ? `<em>${esc(note)}</em>` : ''}</span></label>`;
|
|
80
|
+
const opts = profiles().map((x) => opt(x.id, x.id === '__default__' ? 'The Claude login on this computer' : x.label, x.id === '__default__' ? 'Whatever account Claude Code uses in your usual terminal' : '')).join('')
|
|
81
|
+
+ opt(ownId, 'A separate login just for PMOS', 'Keeps PMOS on its own account. You sign in once.');
|
|
82
|
+
const failed = p.checked && p.checked.state && p.checked.state !== 'ready' && S.busy !== 'check' && row(p.profile) === pick;
|
|
83
|
+
return (failed ? `<div class="ob-bad" role="alert"><b>${esc(p.checked.message || 'This login did not pass the check.')}</b><span>I won’t switch to a different account on my own. Sign this one in, or pick another login.</span></div>` : '')
|
|
84
|
+
+ `<p>Which Claude login should PMOS use? I test exactly that one.</p><div class="ut-card ob-card" role="radiogroup" aria-label="Claude login">${opts}`
|
|
85
|
+
+ `<div class="ob-row">${btn('check', 'Test this login', 'pri')}${btn('login', 'Sign in to it', '')}</div>`
|
|
86
|
+
+ `<p class="ob-note">The test sends one short message to Claude with no tools. It uses a little of this account’s usage.</p></div>`;
|
|
87
|
+
}
|
|
88
|
+
const loginLabel = (p) => { const c = (S.data && S.data.claude) || {}; const id = p.profile; if (!id || id === '__default__' || (c.alias && c.alias[id] === '__default__')) return 'the login on this computer'; if (id === c.own) return 'PMOS’s own login'; return ((profiles().find((x) => x.id === id) || {}).label) || id; };
|
|
89
|
+
|
|
90
|
+
function turns() {
|
|
91
|
+
const p = P(), out = [];
|
|
92
|
+
// 1. the login, checked silently
|
|
51
93
|
if (!p.verified) {
|
|
52
|
-
const failed = p.checked && p.checked.state && p.checked.state !== 'ready' &&
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
94
|
+
const failed = p.checked && p.checked.state && p.checked.state !== 'ready' && S.busy !== 'check' && S.autoChecked;
|
|
95
|
+
if (S.chooser) out.push({ mood: 'needs-you', body: chooserTurn(p) + (S.session ? signInNote() : '') });
|
|
96
|
+
else if (!failed) out.push({ mood: 'thinking', body: '<p class="ob-sub">Checking Claude Code on this computer…</p>' });
|
|
97
|
+
else out.push({ mood: 'honest', body: prop('Sign in to Claude Code', [esc(p.checked.message || 'This login did not pass the check.') + ' I won’t switch to a different account on my own.'],
|
|
98
|
+
`${link('chooser', 'Use another login')}`, 'login', 'chooser', 'Sign in to Claude Code') + (S.session ? signInNote() : '') });
|
|
99
|
+
} else {
|
|
100
|
+
out.push({ mood: 'handled', body: `<div class="ob-good">${CHECK}<b>Claude Code is signed in on this computer, and I just tested it</b><span>Using ${esc(loginLabel(p))} · ${link('settings-claude', 'change in Settings')}</span></div>` });
|
|
101
|
+
}
|
|
102
|
+
// 2. the product question, from the first screen
|
|
103
|
+
if (!p.project) {
|
|
104
|
+
out.push({ mood: 'needs-you', body: '<p>What product or project is this for? A name is enough.</p><p class="ob-sub">The team, a Jira key or a link help me find the right sources.</p>' });
|
|
105
|
+
if (S.queued) out.push({ you: S.queued }, { mood: 'thinking', body: '<p class="ob-sub">I start as soon as the Claude check passes.</p>' });
|
|
63
106
|
return out;
|
|
64
107
|
}
|
|
65
|
-
const label = row(p.profile) === ownId ? 'A separate login just for PMOS' : (profiles.find((x) => x.id === row(p.profile)) || {}).label || p.profile;
|
|
66
|
-
out.push({ mood: 'handled', body: `<div class="ob-good">${CHECK}<b>Login works · ${esc(label)}</b><span>Tested ${p.checked && p.checked.at ? new Date(p.checked.at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : 'just now'} · one test message, no tools</span></div>`
|
|
67
|
-
+ (p.project ? '' : `<p>What product or project is this for? Its name is enough to start.</p><p class="ob-sub">If you have them, add the team, a Jira key, a link or the folder where it lives. They help me find the right sources, not a same-name one.</p><div class="ob-row"><button type="button" class="hm-chip loose" data-ob="relogin">Use another login</button></div>`) });
|
|
68
|
-
if (!p.project) return out;
|
|
69
|
-
// 2. the project, the folder, the sources
|
|
70
108
|
out.push({ you: S.said || p.project.description || p.project.name });
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const fa = f.facts || {};
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
+ `<div class="ob-row">${btn('search', 'Search, read-only', 'pri')}${btn('skip', 'Skip, use my description', 'ghost')}</div><p class="ob-note">Read-only. I won’t post, comment or change anything. Searching uses some of this account’s usage.</p></div>`;
|
|
91
|
-
}
|
|
109
|
+
for (const c of S.changes.filter((x) => x.at === 'plan')) out.push({ you: c.text });
|
|
110
|
+
// 3. one ✦: folder + sources + search
|
|
111
|
+
const f = p.folder, disc = p.discovered && p.discovered.sources;
|
|
112
|
+
if (!p.found && !p.reviewed) {
|
|
113
|
+
if (!f || !disc) { out.push({ mood: S.busy ? 'thinking' : 'honest', body: S.busy ? '<p class="ob-sub">Looking at where it can live and what this login can read…</p>' : `<p class="ob-sub">I couldn’t list what this login can read. ${link('relist', 'Try again')}</p>` }); return out; }
|
|
114
|
+
const ex = f.mode === 'existing', fa = f.facts || {};
|
|
115
|
+
const search = disc.filter((s) => s.status === 'connected' && !S.excluded.has(s.id));
|
|
116
|
+
const sign = disc.filter((s) => s.status === 'needs_auth' && !S.excluded.has(s.id));
|
|
117
|
+
const rows = [`<b>Folder</b> <code>${esc(f.root)}</code> · ${ex ? (fa.pmos ? 'PMOS is already here; I reuse it' : 'I add only what is missing; files and history stay') : 'a new folder with Git and PMOS'} · ${link('pick-existing', 'Use another folder', ' aria-label="Choose an existing folder"')}`];
|
|
118
|
+
if (search.length || ex) rows.push(`<b>Search, read-only</b> ${esc([...search.map((s) => s.label), ...(ex ? ['the folder README and Git history'] : [])].join(', '))} for “${esc(p.project.name)}”${p.project.jira ? ' and ' + esc(p.project.jira) : ''}`);
|
|
119
|
+
else rows.push('<b>Search</b> no connected tool to search. I use what you wrote.');
|
|
120
|
+
for (const s of sign) rows.push(`${esc(s.label)} needs signing in inside Claude; I’ll leave it out · <a href="https://claude.ai/settings/connectors" target="_blank" rel="noopener noreferrer">sign in</a>`);
|
|
121
|
+
if (S.excluded.size) rows.push(`<span class="ob-sub">Left out: ${esc(disc.filter((s) => S.excluded.has(s.id)).map((s) => s.label).join(', '))}</span>`);
|
|
122
|
+
let body = `<p><b>${esc(p.project.name)}</b>, got it.</p>` + prop(`Set up ${esc(p.project.name)} here, and look for what already exists`, rows,
|
|
123
|
+
search.length ? 'Read-only: I won’t post, comment or change anything. The search uses a little of this account’s usage.' : 'Nothing is created until you say yes.', 'plan-yes', 'plan-no', 'Set up here and search');
|
|
124
|
+
if (S.folderInput) body += `<form class="ob-path" data-ob-form="path"><label class="ut-field"><span>Folder path</span><input name="path" autocomplete="off" placeholder="${S.data.platform === 'win32' ? 'C:\\Users\\you\\Projects\\checkout' : '~/code/checkout'}"></label></form>`;
|
|
125
|
+
out.push({ mood: 'needs-you', body });
|
|
126
|
+
if (S.asking === 'plan') out.push({ mood: 'needs-you', body: '<p>What should I change? Write a folder path, name a source to leave out, or say “no search”.</p>' });
|
|
127
|
+
return out;
|
|
92
128
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const found = p.found || { sources: [], jira: [], findings: [] };
|
|
97
|
-
if (p.found) out.push({ you: 'Search these sources' });
|
|
98
|
-
const rv = p.review || { fields: {} };
|
|
129
|
+
// 4. the findings as source tiles, then ✦ set up with this context
|
|
130
|
+
const found = p.found || { sources: [], jira: [], findings: [], highlights: [] };
|
|
131
|
+
const tiles = (found.highlights && found.highlights.length ? found.highlights : firstPerSource(found.findings)).slice(0, 3);
|
|
99
132
|
let rb = '';
|
|
100
|
-
if (
|
|
101
|
-
|
|
102
|
-
|
|
133
|
+
if (p.found) {
|
|
134
|
+
rb += tiles.length ? `<p>I found ${tiles.length === 1 ? 'one thing' : ['', '', 'two', 'three'][tiles.length] + ' things'} about <b>${esc(p.project.name)}</b>.</p><div class="ob-tiles">${tiles.map((t) => `<div class="ut-card ob-tile"><span class="ob-ts">${esc(t.source_label)}</span><b>${t.url ? `<a href="${esc(t.url)}" target="_blank" rel="noopener noreferrer">${esc(t.headline)}</a>` : esc(t.headline)}</b>${t.detail ? `<span>${esc(t.detail)}</span>` : ''}</div>`).join('')}</div>`
|
|
135
|
+
: `<p>I didn’t find anything about <b>${esc(p.project.name)}</b>. Tell me in a sentence what it is for, or leave it blank.</p>`;
|
|
136
|
+
const failed = found.sources.filter((s) => s.state === 'failed');
|
|
137
|
+
if (failed.length) rb += `<p class="ob-sub">${failed.map((s) => `${esc(s.label)}: ${esc(s.error || 'It could not be read.')}`).join(' ')} I used nothing from ${failed.length === 1 ? 'it' : 'them'}. ${link('retry', 'Try again')}</p>`;
|
|
138
|
+
}
|
|
139
|
+
const rv0 = p.review || { fields: {} }, rv = S.jira ? { ...rv0, jira: S.jira } : rv0; // a choice made on the page survives a redraw
|
|
103
140
|
if (found.jira && found.jira.length > 1) {
|
|
104
141
|
rb += `<p><b>${found.jira.length} Jira projects could be this one.</b> Which is it?</p><div class="ut-card ob-card" role="radiogroup" aria-label="Jira project">${found.jira.map((j) => `<label class="ob-opt${rv.jira === j.key ? ' on' : ''}"><input type="radio" name="ob-jira" value="${esc(j.key)}"${rv.jira === j.key ? ' checked' : ''}><span><b>${esc(j.key)}${j.name ? ' · ' + esc(j.name) : ''}</b></span></label>`).join('')}<label class="ob-opt${rv.jira === 'none' ? ' on' : ''}"><input type="radio" name="ob-jira" value="none"${rv.jira === 'none' ? ' checked' : ''}><span>Neither. Don’t link Jira</span></label></div>`;
|
|
105
142
|
}
|
|
106
|
-
if (!found.findings.length && p.found && !failed.length) rb += '<p>I didn’t find much. Tell me in a sentence or two what this project is for, or fill in the fields below.</p>';
|
|
107
143
|
const fields = rv.fields || {};
|
|
108
|
-
rb += `<form class="ut-card ob-card ob-ctx" data-ob-form="review"><p><b>Project context</b> <span class="ob-sub">· saved to the project when you set it up. Edit anything.</span></p>`
|
|
109
|
-
+ FIELD_LABELS.map(([k, l]) => { const v = fields[k]; const tag = !v ? '<span class="ob-tag miss">Missing</span>' : v.source === 'finding' ? `<span class="ob-tag">${v.url ? `<a href="${esc(v.url)}" target="_blank" rel="noopener noreferrer">${esc(v.source_label)}</a>` : esc(v.source_label)}</span>` : `<span class="ob-tag you">${v.source === 'folder' ? 'Folder' : 'You'}</span>`;
|
|
110
|
-
return `<label class="ob-fld"><b>${l}</b><input name="${k}" value="${esc(v ? v.value : '')}" placeholder="${!v ? 'Not found. Add it or leave blank' : ''}">${tag}</label>`; }).join('')
|
|
111
|
-
+ (fields.code ? `<p class="ob-sub">Code: ${esc(fields.code.value)}</p>` : '')
|
|
112
|
-
+ `</form>`;
|
|
113
144
|
const where = f && f.root ? f.root : 'the new folder';
|
|
114
|
-
const
|
|
115
|
-
|
|
145
|
+
const jk = rv.jira && rv.jira !== 'none' ? rv.jira : (found.jira && found.jira.length === 1 ? found.jira[0].key : '');
|
|
146
|
+
const rows = FIELD_LABELS.filter(([k]) => k !== 'name').map(([k, l]) => {
|
|
147
|
+
const v = fields[k], editing = S.editing.has(k) || S.editing.has('all');
|
|
148
|
+
const src = !v ? '' : v.source === 'finding' ? (v.url ? `<a href="${esc(v.url)}" target="_blank" rel="noopener noreferrer">${esc(v.source_label)}</a>` : esc(v.source_label)) : v.source === 'folder' ? 'the folder' : 'you';
|
|
149
|
+
if (editing) return `<label class="ob-fld"><b>${l}</b><input name="${k}" value="${esc(v ? v.value : '')}" placeholder="Leave blank if not known"></label>`;
|
|
150
|
+
return `<span class="ob-fl"><b>${l}</b>${v ? `<span>${esc(v.value)}</span><em>${src}</em>` : '<span class="ob-miss">Not found. Tell me, or leave blank</span>'}<button type="button" class="ut-ib ob-pen" data-ob-edit="${k}" aria-label="Edit ${esc(l.toLowerCase())}" title="Edit">${PEN}</button></span>`;
|
|
151
|
+
});
|
|
152
|
+
rb += `<form data-ob-form="review" class="ob-rv">` + prop(`Set up PMOS in <code>${esc(where)}</code> with this context${jk ? ', and link Jira ' + esc(jk) : ''}`, rows,
|
|
153
|
+
'Nothing changes in your tools, and no tasks are created. Next I settle the basics, and you can change each one.', 'init', 'ctx-no', 'Set up PMOS with this context') + '</form>';
|
|
116
154
|
if (p.init && !p.init.ok) {
|
|
117
155
|
const steps = p.init.steps || {};
|
|
118
|
-
rb += `<div class="ob-bad" role="alert"><b>Setup stopped at “${esc(p.init.step || 'a step')}”.</b><span>${esc(p.init.error || '')} Nothing you had was removed. ${Object.keys(steps).filter((k) => steps[k].state === 'done').length} steps are done
|
|
156
|
+
rb += `<div class="ob-bad" role="alert"><b>Setup stopped at “${esc(p.init.step || 'a step')}”.</b><span>${esc(p.init.error || '')} Nothing you had was removed. ${Object.keys(steps).filter((k) => steps[k].state === 'done').length} steps are done. Setting up again does only the rest.</span></div>`;
|
|
119
157
|
}
|
|
120
158
|
out.push({ mood: p.init && !p.init.ok ? 'honest' : 'needs-you', body: rb });
|
|
159
|
+
for (const c of S.changes.filter((x) => x.at === 'context')) out.push({ you: c.text });
|
|
160
|
+
if (S.asking === 'context') out.push({ mood: 'needs-you', body: '<p>What should I change? Edit a line above, or tell me, for example “owner is Dana”.</p>' });
|
|
121
161
|
return out;
|
|
122
162
|
}
|
|
163
|
+
const signInNote = () => '<p class="ob-note">Finish signing in in the browser window Claude opened. If it asks for a code, paste it below. I test the login as soon as you are done.</p><div class="cs-term ob-term" aria-label="Claude sign-in terminal"></div>';
|
|
164
|
+
const firstPerSource = (findings) => { const seen = new Set(), out = []; for (const x of findings || []) { if (seen.has(x.source)) continue; seen.add(x.source); out.push({ source_label: x.source_label, headline: x.title, detail: x.summary, url: x.url }); } return out; };
|
|
165
|
+
|
|
166
|
+
/** 5. After init: what PMOS settled, each with its source and one change; then ✦ the first initiative (B2 takes it from there). */
|
|
167
|
+
function settledTurns() {
|
|
168
|
+
const p = P(), d = p.done, fd = p.fundamentals || { lines: [] };
|
|
169
|
+
const rows = fd.lines.map((l) => { const meta = [l.source ? esc(l.source) : '', l.hint ? link('wf-new', 'draft a ' + l.hint.word + ' workflow') : '', l.change ? link('f:' + l.key, l.change.label) : ''].filter(Boolean).join(' · ');
|
|
170
|
+
return `<li data-key="${esc(l.key)}">${CHECK}<span><span>${esc(l.text)}</span>${meta ? `<em>${meta}</em>` : ''}</span></li>`; }).join('');
|
|
171
|
+
return [{ mood: 'handled', body: `<p><b>${esc(d.name)}</b> is set up in <code>${esc(d.root)}</code>. I settled the basics for you:</p>`
|
|
172
|
+
+ `<ul class="ut-card ob-done ob-fund">${rows}</ul>${fd.error ? `<p class="ob-sub">${esc(fd.error)}</p>` : ''}`
|
|
173
|
+
+ prop('Draft the first initiative from the purpose, with its expected outcome', [], 'Nothing is final until you keep it.', 'first-yes', 'first-no', 'Draft the first initiative') }];
|
|
174
|
+
}
|
|
123
175
|
|
|
124
176
|
function doneTurns() {
|
|
125
177
|
const d = P().done;
|
|
@@ -134,30 +186,56 @@ export function mountOnboard(host, { ws = '', done = false } = {}) {
|
|
|
134
186
|
|
|
135
187
|
function draw() {
|
|
136
188
|
if (!S.data) { log.innerHTML = `<div class="hm-empty hm-empty-ad">${adler('thinking', 'l', { id: 'obload' })}<p>Getting setup ready…</p></div>`; return; }
|
|
137
|
-
const
|
|
189
|
+
const p0 = P();
|
|
190
|
+
const list = done ? doneTurns() : p0.done && p0.fundamentals ? turns().slice(0, 1).concat(settledTurns()) : turns();
|
|
138
191
|
const lastClaude = list.map((t, i) => (t.you ? -1 : i)).filter((i) => i >= 0).pop();
|
|
139
192
|
// No big Adler here: the latest reply carries his face, and the list may already show him (one Adler per screen, rule 21).
|
|
140
|
-
const intro = !done && !P().
|
|
193
|
+
const intro = !done && !P().project ? '<div class="hm-empty ob-hello"><p><b>Let’s set up your first project</b></p><p>It takes a few minutes. Nothing is created until you say so.</p></div>' : '';
|
|
141
194
|
if (S.stopTerm) { S.stopTerm(); S.stopTerm = null; }
|
|
142
195
|
log.innerHTML = intro + list.map((t, i) => (t.you ? youTurn(t.you) : claudeTurn(t.body, t.mood, i === lastClaude))).join('') + (S.note ? `<p class="ob-note ob-live" role="status">${esc(S.note)}</p>` : '');
|
|
143
196
|
const term = log.querySelector('.ob-term');
|
|
144
197
|
// the sign-in ends on its own once Claude has the login; a clean end runs the test at once
|
|
145
198
|
if (term && S.session) signInTerminal(term, S.session, { onEnd: (code) => { S.session = null; if (!code) act('check'); else { S.note = 'The sign-in stopped before it finished. Sign in again.'; draw(); } } }).then((stop) => { S.stopTerm = stop; }).catch((e) => { S.note = e.message; S.session = null; draw(); });
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
199
|
+
// the composer is open throughout (B1): it answers the product question, the ✕ "what should I change", and after setup goes to Home
|
|
200
|
+
const p = P(), asks = !done && !p.project && !S.queued;
|
|
201
|
+
const kept = foot.querySelector('textarea'); const typed = kept ? kept.value : '';
|
|
202
|
+
foot.innerHTML = `<form class="hm-box" data-ob-form="say"><textarea rows="1" aria-label="Message Claude" placeholder="${asks ? 'e.g. Checkout redesign, payments team, Jira PAY' : 'Message Claude'}"${S.busy ? ' disabled' : ''}></textarea><div class="hm-tb"><span class="sp"></span><span class="hm-key">Enter sends · Shift+Enter for a new line</span><button type="submit" class="hm-send" aria-label="Send"${S.busy ? ' disabled' : ''}>${UP}</button></div></form>`;
|
|
203
|
+
const ta = foot.querySelector('textarea'); ta.value = typed;
|
|
204
|
+
if (asks || S.asking) ta.focus({ preventScroll: true });
|
|
149
205
|
log.scrollTop = log.scrollHeight;
|
|
150
206
|
}
|
|
151
207
|
|
|
208
|
+
/** Run what the page can on its own: the silent login check on first open, and the folder + connector listing after the answer. */
|
|
209
|
+
async function auto() {
|
|
210
|
+
const p = P();
|
|
211
|
+
if (done || S.busy) return;
|
|
212
|
+
if (!p.verified && !S.autoChecked && !S.chooser) { S.autoChecked = true; return act('check'); }
|
|
213
|
+
if (p.verified && S.queued && !p.project) { const q = S.queued; S.queued = ''; return answer(q); }
|
|
214
|
+
if (p.project && !p.found && !p.reviewed && (!p.folder || !p.discovered) && !S.autoListed) {
|
|
215
|
+
S.autoListed = true; S.busy = 'list'; draw();
|
|
216
|
+
if (!p.folder) await post('folder', { mode: 'new' });
|
|
217
|
+
if (!P().discovered) { const r = await post('discover'); if (!r.ok) S.note = r.error; }
|
|
218
|
+
S.busy = ''; draw();
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
async function answer(v) {
|
|
222
|
+
const a = readAnswer(v); S.said = v; S.busy = 'say'; draw();
|
|
223
|
+
const r = await post('project', { name: a.name, team: a.team, jira: a.jira, link: a.link, description: a.description });
|
|
224
|
+
if (!r.ok) S.note = r.error;
|
|
225
|
+
else if (a.folder) { const f = await post('folder', { mode: 'existing', root: a.folder }); if (!f.ok) S.note = 'I couldn’t use ' + a.folder + ': ' + (f.error || 'not found') + ' I propose a folder instead.'; }
|
|
226
|
+
S.busy = ''; S.autoListed = false; draw(); auto();
|
|
227
|
+
}
|
|
228
|
+
const go = (q = '') => location.assign('/premium.html?ws=' + encodeURIComponent(P().done.ws) + '&onboarded=1' + q);
|
|
229
|
+
const home = (say) => location.assign('/premium.html?ws=' + encodeURIComponent(P().done.ws) + '&say=' + encodeURIComponent(say));
|
|
230
|
+
|
|
152
231
|
async function act(a, b) {
|
|
153
232
|
const p = P();
|
|
154
|
-
const typed = readReview(); // before any redraw, so what the PM typed
|
|
155
|
-
const ticked = [...log.querySelectorAll('input[name="ob-src"]:checked')].map((x) => x.value), localTicked = !!log.querySelector('input[name="ob-local"]:checked');
|
|
233
|
+
const typed = readReview(); // before any redraw, so what the PM typed or chose is never lost
|
|
156
234
|
S.busy = a; S.note = ''; draw();
|
|
157
235
|
try {
|
|
158
236
|
if (a === 'check' || a === 'login') {
|
|
159
237
|
const cl = S.data.claude || {};
|
|
160
|
-
let profile = S.profile || p.profile || cl.selected;
|
|
238
|
+
let profile = S.chooser ? (S.profile || p.profile || cl.selected) : (p.profile || cl.selected || '__default__');
|
|
161
239
|
if (cl.alias && cl.alias[profile]) profile = cl.alias[profile]; // a pinned copy of the usual login is tested as that login
|
|
162
240
|
if (profile === '__own__') { const own = await post('own'); if (!own.ok) throw new Error(own.error || 'The PMOS login could not be made.'); profile = own.id; S.profile = profile; await load(); }
|
|
163
241
|
const r = await post(a, { profile });
|
|
@@ -166,32 +244,41 @@ export function mountOnboard(host, { ws = '', done = false } = {}) {
|
|
|
166
244
|
else if (r.signedIn) { S.session = null; S.busy = ''; return act('check'); } // already signed in: test it, no terminal
|
|
167
245
|
else S.session = r.session;
|
|
168
246
|
}
|
|
169
|
-
else { S.session = null; if (
|
|
170
|
-
} else if (a === '
|
|
171
|
-
else if (a === '
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
247
|
+
else { S.session = null; S.autoChecked = true; if (r.ok || (r.progress && r.progress.verified)) S.chooser = false; await load(); S.busy = ''; draw(); return auto(); }
|
|
248
|
+
} else if (a === 'chooser') { S.chooser = true; }
|
|
249
|
+
else if (a === 'relist') { S.autoListed = false; S.busy = ''; return auto(); }
|
|
250
|
+
else if (a === 'settings-claude' || a === 'settings-sources') { import('./cozy-settings.js').then((M) => M.openSettings(a === 'settings-claude' ? 'claude' : 'connectors')); }
|
|
251
|
+
else if (a === 'pick-existing') {
|
|
252
|
+
const r = await post('folder', { mode: 'existing', pick: true, root: p.folder && p.folder.mode === 'existing' ? p.folder.root : undefined });
|
|
253
|
+
if (r.unsupported) { S.folderInput = 'existing'; S.note = r.error; } else if (!r.ok && !r.cancelled) S.note = r.error;
|
|
254
|
+
} else if (a === 'plan-yes') {
|
|
255
|
+
const disc = (p.discovered && p.discovered.sources) || [];
|
|
256
|
+
const ids = disc.filter((s) => s.status === 'connected' && !S.excluded.has(s.id)).map((s) => s.id);
|
|
257
|
+
const local = p.folder && p.folder.mode === 'existing';
|
|
258
|
+
S.asking = '';
|
|
259
|
+
if (!ids.length && !local) { const r = await post('review', {}); if (!r.ok) S.note = r.error; }
|
|
260
|
+
else { S.note = 'Searching, read-only. This can take a minute or two…'; draw(); const r = await post('search', { sources: ids, local }); S.note = r.ok ? '' : r.error; }
|
|
261
|
+
} else if (a === 'plan-no') { S.asking = 'plan'; }
|
|
262
|
+
else if (a === 'retry') {
|
|
263
|
+
const ids = ((p.found && p.found.sources) || []).filter((s) => s.id !== 'folder').map((s) => s.id);
|
|
264
|
+
const local = (p.found && p.found.sources || []).some((s) => s.id === 'folder');
|
|
182
265
|
S.note = 'Searching, read-only. This can take a minute or two…'; draw();
|
|
183
|
-
const r = await post('search', { sources: ids, local });
|
|
184
|
-
|
|
185
|
-
} else if (a === 'skip') { const r = await post('review', {}); if (!r.ok) S.note = r.error; }
|
|
186
|
-
else if (a === 'again') { await saveReview(typed); const r = await post('discover'); if (!r.ok) S.note = r.error; }
|
|
266
|
+
const r = await post('search', { sources: ids, local }); S.note = r.ok ? '' : r.error;
|
|
267
|
+
} else if (a === 'ctx-no') { S.asking = 'context'; S.editing = new Set(['all']); }
|
|
187
268
|
else if (a === 'init') {
|
|
188
269
|
await saveReview(typed);
|
|
189
|
-
S.note = 'Setting up the project…'; draw();
|
|
270
|
+
S.asking = ''; S.note = 'Setting up the project…'; draw();
|
|
190
271
|
const r = await post('init');
|
|
191
|
-
if (r.ok && r.progress && r.progress.done) {
|
|
192
|
-
S.note = r.error || 'Setup did not finish.';
|
|
193
|
-
|
|
194
|
-
|
|
272
|
+
if (r.ok && r.progress && r.progress.done) { S.editing = new Set(); S.note = ''; S.jira = null; }
|
|
273
|
+
else { S.note = r.error || 'Setup did not finish.'; await load(); }
|
|
274
|
+
} else if (a.startsWith('f:')) {
|
|
275
|
+
const r = await post('fundamental', { key: a.slice(2) });
|
|
276
|
+
if (!r.ok) S.note = r.error;
|
|
277
|
+
else if (r.follow === 'settings') location.assign('/premium-workflows.html?ws=' + encodeURIComponent(p.done.ws));
|
|
278
|
+
else if (r.follow === 'context') { home('Walk me through the project context you saved, and change what I say'); return; }
|
|
279
|
+
} else if (a === 'wf-new') { location.assign('/premium-workflows.html?new=1&ws=' + encodeURIComponent(p.done.ws)); return; }
|
|
280
|
+
else if (a === 'first-yes') { go('&first=draft'); return; }
|
|
281
|
+
else if (a === 'first-no') { go('&first=none'); return; } // B2: the reveal with no draft, and nothing written
|
|
195
282
|
} catch (e) { S.note = e.message || 'That did not finish. Try again.'; }
|
|
196
283
|
S.busy = ''; draw();
|
|
197
284
|
}
|
|
@@ -199,38 +286,57 @@ export function mountOnboard(host, { ws = '', done = false } = {}) {
|
|
|
199
286
|
const form = log.querySelector('form[data-ob-form="review"]'); if (!form) return null;
|
|
200
287
|
const fields = {}; for (const [k] of FIELD_LABELS) { const i = form.querySelector(`input[name="${k}"]`); if (i) fields[k] = i.value; }
|
|
201
288
|
const j = log.querySelector('input[name="ob-jira"]:checked');
|
|
202
|
-
return j ? { fields, jira: j.value } : { fields };
|
|
289
|
+
return j ? { fields, jira: j.value } : (Object.keys(fields).length ? { fields } : null);
|
|
203
290
|
}
|
|
204
291
|
async function saveReview(typed) { if (typed) await post('review', typed); }
|
|
205
292
|
|
|
206
293
|
sec.addEventListener('click', (e) => {
|
|
294
|
+
const ed = e.target.closest('[data-ob-edit]');
|
|
295
|
+
if (ed) { S.editing.add(ed.dataset.obEdit); const keep = readReview(); draw(); if (keep) for (const [k, v] of Object.entries(keep.fields || {})) { const i = log.querySelector(`input[name="${k}"]`); if (i) i.value = v; } const i = log.querySelector(`input[name="${ed.dataset.obEdit}"]`); if (i) i.focus(); return; }
|
|
207
296
|
const b = e.target.closest('[data-ob]'); if (!b || b.disabled || S.busy) return;
|
|
208
|
-
act(b.dataset.ob);
|
|
297
|
+
e.preventDefault(); act(b.dataset.ob);
|
|
209
298
|
});
|
|
210
299
|
sec.addEventListener('change', (e) => {
|
|
211
300
|
const t = e.target;
|
|
212
301
|
if (t.name === 'ob-login') { S.profile = t.value; S.session = null; draw(); }
|
|
213
|
-
else if (t.name === 'ob-
|
|
214
|
-
else if (t.name === 'ob-jira') log.querySelectorAll('input[name="ob-jira"]').forEach((x) => x.closest('.ob-opt').classList.toggle('on', x.checked)); // saved with the review, so typing is never redrawn away
|
|
302
|
+
else if (t.name === 'ob-jira') S.jira = t.value, log.querySelectorAll('input[name="ob-jira"]').forEach((x) => x.closest('.ob-opt').classList.toggle('on', x.checked)); // saved with the review, so typing is never redrawn away
|
|
215
303
|
});
|
|
216
304
|
sec.addEventListener('submit', async (e) => {
|
|
217
305
|
e.preventDefault();
|
|
218
306
|
const form = e.target.closest('form'); if (!form || S.busy) return;
|
|
219
307
|
if (form.dataset.obForm === 'say') {
|
|
220
308
|
const ta = form.querySelector('textarea'); const v = ta.value.trim(); if (!v) return;
|
|
221
|
-
|
|
222
|
-
const
|
|
223
|
-
if (
|
|
224
|
-
|
|
225
|
-
|
|
309
|
+
ta.value = '';
|
|
310
|
+
const p = P();
|
|
311
|
+
if (p.done && p.fundamentals) { home(v); return; }
|
|
312
|
+
if (!p.project) { if (!p.verified) { S.queued = v; draw(); return; } return answer(v); }
|
|
313
|
+
if (!p.found && !p.reviewed) { // ✕ on the first ✦: a folder path, sources to leave out, or no search
|
|
314
|
+
S.changes.push({ at: 'plan', text: v });
|
|
315
|
+
const disc = (p.discovered && p.discovered.sources) || [];
|
|
316
|
+
const c = readChange(v, disc.map((s) => s.label));
|
|
317
|
+
for (const l of c.leaveOut) { const s = disc.find((x) => x.label === l); if (s) S.excluded.add(s.id); }
|
|
318
|
+
S.asking = '';
|
|
319
|
+
if (c.folder) { const r = await post('folder', { mode: 'existing', root: c.folder }); if (!r.ok) { S.note = r.error; S.asking = 'plan'; } }
|
|
320
|
+
if (c.skip) { const r = await post('review', {}); if (!r.ok) S.note = r.error; }
|
|
321
|
+
else if (!c.folder && !c.leaveOut.length) { S.note = 'I didn’t catch a folder or a source in that. Write a folder path, or the name of a source to leave out.'; S.asking = 'plan'; }
|
|
322
|
+
draw(); return;
|
|
323
|
+
}
|
|
324
|
+
// ✕ on the second ✦, or a word about the context at any time before setup
|
|
325
|
+
S.changes.push({ at: 'context', text: v });
|
|
326
|
+
const typed = readReview() || { fields: {} };
|
|
327
|
+
const fl = readField(v);
|
|
328
|
+
const cur = (p.review && p.review.fields) || {};
|
|
329
|
+
if (fl.purpose && !Object.keys(fl).some((k) => k !== 'purpose') && cur.purpose && cur.purpose.value) { S.note = 'Say which line it is, for example “owner is Dana”, or edit the line above.'; draw(); return; }
|
|
330
|
+
S.busy = 'say'; draw();
|
|
331
|
+
const r = await post('review', { ...typed, fields: { ...(typed.fields || {}), ...fl } });
|
|
332
|
+
if (!r.ok) S.note = r.error; else { S.asking = ''; S.editing = new Set(); }
|
|
226
333
|
S.busy = ''; draw();
|
|
227
334
|
} else if (form.dataset.obForm === 'path') {
|
|
228
335
|
const v = form.querySelector('input').value.trim(); if (!v) return;
|
|
229
|
-
const
|
|
230
|
-
const r = await post('folder', mode === 'new' ? { mode, parent: v } : { mode, root: v });
|
|
336
|
+
const r = await post('folder', { mode: 'existing', root: v });
|
|
231
337
|
if (!r.ok) S.note = r.error; else { S.folderInput = false; S.note = ''; }
|
|
232
338
|
draw();
|
|
233
|
-
}
|
|
339
|
+
} else if (form.dataset.obForm === 'review') { act('init'); }
|
|
234
340
|
});
|
|
235
341
|
sec.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey && e.target.matches('textarea')) { e.preventDefault(); e.target.closest('form').requestSubmit(); } });
|
|
236
342
|
load();
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/* cozy-reveal.css — Home right after setup (onboarding build B2; pm/cozy-reveal.js). Cozy tokens only; the turns, cards, buttons and
|
|
2
|
+
* composer are the Home chat's own (cozy-home.css, cozy-today.css, cozy-onboard.css). */
|
|
3
|
+
.rv-ticks { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; max-width: 720px; }
|
|
4
|
+
.rv-ticks li { display: flex; gap: 8px; align-items: baseline; color: var(--cz-ink2); font-size: var(--fs-body); line-height: var(--lh-body); animation: rv-in .28s ease-out both; animation-delay: calc(var(--rv-i, 0) * 120ms); }
|
|
5
|
+
.rv-ticks li svg { color: var(--cz-ok); flex: none; transform: translateY(2px); }
|
|
6
|
+
.rv-ticks li span { flex: 1; min-width: 0; overflow-wrap: anywhere; }
|
|
7
|
+
.rv-ticks li em { font-style: normal; color: var(--cz-ink3); font-size: var(--fs-meta); font-variant-numeric: tabular-nums; }
|
|
8
|
+
@keyframes rv-in { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
|
|
9
|
+
|
|
10
|
+
.rv-portrait, .rv-draft { max-width: 720px; padding: 14px 16px; display: flex; flex-direction: column; gap: 12px; box-sizing: border-box; }
|
|
11
|
+
.rv-head { display: flex; gap: 12px; align-items: center; }
|
|
12
|
+
.rv-head b { color: var(--cz-ink); font-size: var(--fs-section); }
|
|
13
|
+
.rv-head p { margin: 2px 0 0; color: var(--cz-ink2); font-size: var(--fs-meta); line-height: var(--lh-meta); }
|
|
14
|
+
.rv-av { width: 40px; height: 40px; border-radius: 12px; background: var(--cz-bluebg); color: var(--cz-blueink); display: inline-flex; align-items: center; justify-content: center; font-weight: var(--fw-strong); flex: none; }
|
|
15
|
+
.rv-cells { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 10px; }
|
|
16
|
+
.rv-cell { background: var(--cz-well); border-radius: 12px; padding: 10px 12px; display: flex; flex-direction: column; gap: 4px; min-width: 0; }
|
|
17
|
+
.rv-lab { margin: 0; font-size: var(--fs-badge); letter-spacing: .06em; color: var(--cz-ink3); font-weight: var(--fw-strong); }
|
|
18
|
+
.rv-big { color: var(--cz-ink); font-size: var(--fs-section); font-variant-numeric: tabular-nums; }
|
|
19
|
+
.rv-big i { font-style: normal; color: var(--cz-ink2); font-weight: var(--fw-regular); font-size: var(--fs-meta); }
|
|
20
|
+
.rv-txt { margin: 0; color: var(--cz-ink2); font-size: var(--fs-meta); line-height: var(--lh-meta); overflow-wrap: anywhere; }
|
|
21
|
+
.rv-src { margin: 0; color: var(--cz-ink3); font-size: var(--fs-badge); line-height: var(--lh-badge); overflow-wrap: anywhere; }
|
|
22
|
+
.rv-src a { color: inherit; }
|
|
23
|
+
.rv-bar { height: 6px; border-radius: 999px; background: var(--cz-hair); overflow: hidden; }
|
|
24
|
+
.rv-bar i { display: block; height: 100%; background: var(--cz-blue); border-radius: inherit; }
|
|
25
|
+
.rv-track { position: relative; height: 34px; margin: 2px 6px 0; border-bottom: 2px solid var(--cz-hair); }
|
|
26
|
+
.rv-mon { position: absolute; top: 0; font-size: var(--fs-badge); color: var(--cz-ink3); transform: translateX(-2px); }
|
|
27
|
+
.rv-dot { position: absolute; bottom: -5px; width: 8px; height: 8px; margin-left: -4px; border-radius: 50%; background: var(--cz-blue); }
|
|
28
|
+
.rv-today { position: absolute; bottom: -8px; width: 2px; height: 16px; margin-left: -1px; background: var(--cz-ink); }
|
|
29
|
+
.rv-chk { position: absolute; bottom: -8px; width: 16px; height: 16px; margin-left: -8px; border-radius: 50%; background: var(--cz-okbg); color: var(--cz-ok); display: inline-flex; align-items: center; justify-content: center; }
|
|
30
|
+
.rv-foot { display: flex; align-items: center; justify-content: space-between; gap: 8px; color: var(--cz-ink3); font-size: var(--fs-meta); }
|
|
31
|
+
|
|
32
|
+
.rv-kind { margin: 0; display: flex; gap: 6px; align-items: center; color: var(--cz-blueink); font-size: var(--fs-meta); font-weight: var(--fw-strong); }
|
|
33
|
+
.rv-title { color: var(--cz-ink); font-size: var(--fs-section); line-height: 1.3; }
|
|
34
|
+
.rv-draft dl { margin: 0; display: flex; flex-direction: column; gap: 6px; }
|
|
35
|
+
.rv-dl { display: grid; grid-template-columns: 130px 1fr; gap: 8px; align-items: baseline; }
|
|
36
|
+
.rv-dl dt { color: var(--cz-ink3); font-size: var(--fs-meta); }
|
|
37
|
+
.rv-dl dd { margin: 0; color: var(--cz-ink); font-size: var(--fs-body); line-height: var(--lh-body); display: flex; flex-direction: column; gap: 2px; min-width: 0; overflow-wrap: anywhere; }
|
|
38
|
+
.rv-dl dd em { font-style: normal; color: var(--cz-ink3); font-size: var(--fs-badge); }
|
|
39
|
+
.rv-dl dd em a { color: inherit; }
|
|
40
|
+
.rv-in { font: inherit; width: 100%; box-sizing: border-box; border: 1px solid var(--cz-hair); border-radius: 10px; padding: 6px 10px; background: var(--cz-card); color: var(--cz-ink); }
|
|
41
|
+
.rv-next { margin: 0; color: var(--cz-ink2); font-size: var(--fs-meta); line-height: var(--lh-meta); }
|
|
42
|
+
.rv-acts { display: flex; gap: 8px; }
|
|
43
|
+
@media (max-width: 560px) { .rv-dl { grid-template-columns: 1fr; gap: 2px; } }
|
|
44
|
+
|
|
45
|
+
.rv-left { display: flex; flex-direction: column; gap: 4px; padding: 6px 4px 10px; }
|
|
46
|
+
.rv-lrow { display: flex; flex-direction: column; gap: 2px; padding: 8px 10px; border-radius: 12px; background: var(--cz-card); border: 0; text-align: left; font: inherit; color: inherit; }
|
|
47
|
+
.rv-lrow b { color: var(--cz-ink); font-size: var(--fs-row); display: inline-flex; gap: 6px; align-items: center; }
|
|
48
|
+
.rv-lrow span { color: var(--cz-ink2); font-size: var(--fs-meta); line-height: var(--lh-meta); }
|
|
49
|
+
.rv-ldraft { cursor: pointer; }
|
|
50
|
+
.rv-ldraft b svg { color: var(--cz-blueink); }
|
|
51
|
+
.rv-lnote { margin: 4px 10px 0; color: var(--cz-ink3); font-size: var(--fs-badge); line-height: var(--lh-badge); }
|
|
52
|
+
@media (prefers-reduced-motion: reduce) { .rv-ticks li { animation: none; } }
|