create-pmos 0.39.0 → 0.40.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/README.md +9 -1
- package/workspace/bin/pmos.js +8 -1
- package/workspace/guide/index.md +4 -2
- package/workspace/guide/layout.md +15 -1
- package/workspace/guide/proactive-engine.md +75 -0
- package/workspace/guide/validate-with-typesafe.md +36 -3
- package/workspace/lib/brief.js +113 -0
- package/workspace/lib/claims.js +605 -0
- package/workspace/lib/doc-profiles.js +74 -0
- package/workspace/lib/initiative-files.js +11 -3
- package/workspace/lib/preview-kind.js +17 -2
- package/workspace/lib/proactive-cli.js +137 -0
- package/workspace/lib/proactive.js +208 -0
- package/workspace/lib/repo-watch.js +120 -0
- package/workspace/lib/typesafe-settings.js +5 -2
- package/workspace/package-lock.json +1 -1
- package/workspace/package.json +3 -3
- package/workspace/public/pm/app.js +1 -1
- package/workspace/public/pm/data.js +2 -2
- package/workspace/public/pm/modes-live.js +221 -0
- package/workspace/public/pm/modes.js +3 -2
- package/workspace/public/pm/panel-repos.js +2 -2
- package/workspace/public/pm/tab-doc.js +57 -4
- package/workspace/server.js +239 -14
- package/workspace/skills/morning-brief.skill +43 -0
package/workspace/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmos-workspace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.40.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "PMOS Terminal Workspace — local companion hosting real PTYs beside the library and gates (D62)",
|
|
6
6
|
"license": "MIT",
|
|
@@ -10,12 +10,12 @@
|
|
|
10
10
|
"scripts": {
|
|
11
11
|
"start": "node server.js",
|
|
12
12
|
"test": "node test.js && npm run test:modules",
|
|
13
|
-
"test:modules": "node focus.test.js && node handoff.test.js && node repo-scan.test.js && node profiles.test.js && node external-linking.test.js && node story-artifacts.test.js && node no-network.test.js && node typesafe-validation.test.js && node fixture-parity.test.mjs && node markdown.test.mjs && node start-policy.test.js && node state-isolation.test.js && node sync.test.js && node schedule-cli.test.js && node initiative-surface.test.js && node guide.test.js && node next-step-actions.test.js",
|
|
13
|
+
"test:modules": "node proactive.test.js && node focus.test.js && node handoff.test.js && node repo-scan.test.js && node profiles.test.js && node external-linking.test.js && node story-artifacts.test.js && node no-network.test.js && node typesafe-validation.test.js && node claims.test.js && node repo-watch.test.js && node fixture-parity.test.mjs && node markdown.test.mjs && node start-policy.test.js && node state-isolation.test.js && node sync.test.js && node schedule-cli.test.js && node initiative-surface.test.js && node guide.test.js && node next-step-actions.test.js",
|
|
14
14
|
"test:ui": "node ui-test.js",
|
|
15
15
|
"test:parity": "node design-parity.js --states",
|
|
16
16
|
"test:all": "npm test && npm run test:panels && npm run test:ui && npm run test:parity && npm run test:offline",
|
|
17
17
|
"doctor": "node doctor.js",
|
|
18
|
-
"test:panels": "node pm-shell-test.js && node pm-repos-test.js && node pm-initiatives-test.js && node pm-detail-test.js && node pm-tabs-test.js && node pm-term-test.js && node pm-modes-test.js && node pm-r1-test.js && node pm-r2-test.js && node pm-r3-test.js && node pm-r4-test.js && node pm-r5-test.js && node pm-r6-test.js && node pm-r7-test.js && node pm-r8-test.js && node pm-r9-test.js && node pm-r10-test.js && node pm-r11-test.js && node pm-r12-test.js && node pm-r13-test.js && node pm-r14-test.js && node pm-r15-test.js && node pm-r16-test.js && node pm-r17-test.js && node pm-r16-test.js && node pm-r18-test.js && node pm-r19-test.js && node pm-r20-test.js && node pm-r21-test.js",
|
|
18
|
+
"test:panels": "node pm-shell-test.js && node pm-repos-test.js && node pm-initiatives-test.js && node pm-detail-test.js && node pm-tabs-test.js && node pm-term-test.js && node pm-modes-test.js && node pm-r1-test.js && node pm-r2-test.js && node pm-r3-test.js && node pm-r4-test.js && node pm-r5-test.js && node pm-r6-test.js && node pm-r7-test.js && node pm-r8-test.js && node pm-r9-test.js && node pm-r10-test.js && node pm-r11-test.js && node pm-r12-test.js && node pm-r13-test.js && node pm-r14-test.js && node pm-r15-test.js && node pm-r16-test.js && node pm-r17-test.js && node pm-r16-test.js && node pm-r18-test.js && node pm-r19-test.js && node pm-r20-test.js && node pm-r21-test.js && node pm-r22-test.js && node pm-r23-test.js",
|
|
19
19
|
"test:offline": "node offline-tarball.test.js"
|
|
20
20
|
},
|
|
21
21
|
"engines": {
|
|
@@ -33,7 +33,7 @@ import { isMobile, mountMobile, applyMobile } from './mobile.js';
|
|
|
33
33
|
const MODE_TITLE = {
|
|
34
34
|
product: 'initiatives', scheduled: 'scheduled tasks', workflow: 'workflows', skills: 'skills',
|
|
35
35
|
knowledge: 'knowledge', agent: 'agents', memory: 'memory', connect: 'connections', focus: 'focus',
|
|
36
|
-
portfolio: 'portfolio', guide: 'guide', validate: 'validate',
|
|
36
|
+
portfolio: 'portfolio', guide: 'guide', validate: 'validate', today: 'today',
|
|
37
37
|
};
|
|
38
38
|
const DETAIL_TITLE = {
|
|
39
39
|
init: 'initiative', flow: 'workflow', sched: 'scheduled task', skill: 'skill', know: 'decision',
|
|
@@ -1433,9 +1433,9 @@ export async function readDoc(path, ws, ref) {
|
|
|
1433
1433
|
const url = '/api/doc?' + q({ ws: ws || state.wsId || (p ? p.ws : ''), path, ...(ref ? { ref } : {}) });
|
|
1434
1434
|
const res = await fetch(url);
|
|
1435
1435
|
if (!res.ok) {
|
|
1436
|
-
// The server states why a
|
|
1436
|
+
// The server states why a read was refused (binary, too large, not text, outside the workspace); show that reason.
|
|
1437
1437
|
const why = res.status === 404 ? (await res.text().catch(() => '')) : '';
|
|
1438
|
-
throw new Error(res.status === 404 ? (why
|
|
1438
|
+
throw new Error(res.status === 404 ? (why || 'not a readable text document in this workspace') : 'HTTP ' + res.status);
|
|
1439
1439
|
}
|
|
1440
1440
|
return res.text();
|
|
1441
1441
|
}
|
|
@@ -581,12 +581,90 @@ async function vRun() {
|
|
|
581
581
|
vSay(out.aborted ? 'bad' : 'ok', 'asked ' + out.asked + ' of ' + out.requests + ' · ' + out.raised + ' raised · ' + out.local_findings + ' local · ' + out.withheld + ' withheld · ' + out.failed + ' failed' + (out.aborted ? ' — ' + out.aborted : ''));
|
|
582
582
|
}
|
|
583
583
|
|
|
584
|
+
|
|
585
|
+
/* ── claims-ledger — is this repository consistent? Local checks first; nothing here sends anything ───────────────
|
|
586
|
+
* The verdict says what was checked and against what, in plain words. A finding names the documents and puts the
|
|
587
|
+
* truth value beside the claim. "Consistent" means no recorded statement contradicts its source: prose is not read,
|
|
588
|
+
* and the page says so every time. */
|
|
589
|
+
let cOpen = new Set(); // 'undecided' | 'fine' | 'unclassified' — which collapsed groups are open
|
|
590
|
+
let cNote = null;
|
|
591
|
+
const cSay = (text) => { cNote = text; setState({}); };
|
|
592
|
+
async function cCheck() {
|
|
593
|
+
const ws = vWs(); if (!ws) return cSay('select a repository first');
|
|
594
|
+
cSay('checking …');
|
|
595
|
+
const out = await post('/api/claims/check', { ws });
|
|
596
|
+
reload('claims');
|
|
597
|
+
cSay(out && out.ok ? 'checked just now — ' + out.run.reads + ' file(s) read, ' + out.run.affected + ' re-verified, ' + out.run.appended + ' recorded' : ((out && out.error) || 'the check failed'));
|
|
598
|
+
}
|
|
599
|
+
async function cWatch(on) {
|
|
600
|
+
const ws = vWs(); if (!ws) return;
|
|
601
|
+
const out = await post('/api/claims/watch', { ws, watch: on });
|
|
602
|
+
reload('claims');
|
|
603
|
+
cSay(out && out.ok ? (on ? 'now checked automatically whenever its files change' : 'automatic checking is off') : ((out && out.error) || 'not saved'));
|
|
604
|
+
}
|
|
605
|
+
const cToggle = (k) => { if (cOpen.has(k)) cOpen.delete(k); else cOpen.add(k); setState({}); };
|
|
606
|
+
const TYPE_CHIP = 'font-family:var(--mono);font-size:9.5px;padding:1px 6px;border:1px solid var(--border);color:var(--muted-foreground)';
|
|
607
|
+
|
|
608
|
+
function claimsBlock(ws) {
|
|
609
|
+
const kids = [];
|
|
610
|
+
kids.push(el('div', { style: LABEL }, iv('consistency of this repository')));
|
|
611
|
+
if (!ws) return [...kids, el('div', { style: NOTE }, iv('Select a repository first.'))];
|
|
612
|
+
const r = read('claims', '/api/claims?ws=' + encodeURIComponent(ws));
|
|
613
|
+
const st = status(r, 'the consistency checks');
|
|
614
|
+
if (st) return [...kids, st];
|
|
615
|
+
const d = r.data; const c = d.counts;
|
|
616
|
+
const word = d.checking ? 'Checking…' : d.verdict === 'not-checked' ? 'Not checked yet' : d.verdict === 'consistent' ? 'Consistent' : c.problems + (c.problems === 1 ? ' problem' : ' problems');
|
|
617
|
+
const tone = d.verdict === 'problems' ? 'var(--destructive)' : 'var(--foreground)';
|
|
618
|
+
const why = d.verdict === 'not-checked'
|
|
619
|
+
? 'Press “check now” to read this repository’s documents and config files and compare each with what it must agree with.'
|
|
620
|
+
: 'Checked ' + c.documents + ' documents and config files · ' + c.verified + ' statements verified · ' + c.stale + ' stale · ' + c.undecided + ' worth a look. '
|
|
621
|
+
+ c.internal_only + ' are checked by their own structure only; ' + c.unclassified + ' are not classified and are never compared with other files.';
|
|
622
|
+
kids.push(el('div', { 'data-r22': 'verdict', 'data-verdict': d.checking ? 'checking' : d.verdict, style: 'padding:6px 14px;border-left:2px solid ' + tone + ';margin:2px 14px 6px' },
|
|
623
|
+
el('span', { 'data-r22': 'verdict-word', style: 'font-size:15px;font-weight:600;color:' + tone }, iv(word)),
|
|
624
|
+
el('span', { 'data-r22': 'verdict-why', style: 'display:block;font-size:11.5px;color:var(--muted-foreground);line-height:1.5;margin-top:2px' }, iv(why))));
|
|
625
|
+
kids.push(el('div', { 'data-r22': 'limits', style: NOTE }, iv('Checked by structure and the claims record. Prose is not read, so “Consistent” means no recorded statement contradicts its source — not that every sentence was checked. Nothing here is sent anywhere.')));
|
|
626
|
+
kids.push(el('div', { style: 'display:flex;gap:6px;align-items:center;flex-wrap:wrap;padding:2px 14px 6px' },
|
|
627
|
+
el('button', { style: BTN, hover: 'color:var(--foreground)', 'data-r22': 'check', disabled: d.checking ? 'true' : null, onClick: cCheck }, 'check now'),
|
|
628
|
+
el('button', { style: BTN + (d.watching ? ';color:var(--foreground);border-color:var(--foreground)' : ''), hover: 'color:var(--foreground)', 'data-r22': 'watch', onClick: () => cWatch(!d.watching),
|
|
629
|
+
title: 'When on, the workspace notices changes to files in this repository and re-checks the documents they affect. It records problems in the repository\'s claims.jsonl.' },
|
|
630
|
+
iv(d.watching ? 'automatic checking: ON — turn off' : 'automatic checking: OFF — turn on'))));
|
|
631
|
+
if (d.watching && d.last_run) kids.push(el('div', { 'data-r22': 'last', style: NOTE }, iv('Last automatic check ' + String(d.last_run.at).replace('T', ' ').slice(0, 16) + (d.last_run.ok === false ? ' — failed: ' + (d.last_run.error || 'error') : d.last_run.run ? ' — ' + d.last_run.run.affected + ' document(s) re-verified' : ''))));
|
|
632
|
+
if (cNote) kids.push(el('div', { 'data-r22': 'note', style: NOTE }, iv(cNote)));
|
|
633
|
+
if (d.ledger && d.verdict !== 'not-checked') kids.push(el('div', { style: NOTE }, iv('Record of problems: '), el('button', { style: 'text-decoration:underline;color:var(--foreground)', 'data-r22': 'ledger', onClick: () => openDocIn(ws, d.ledger, state.product) }, iv(d.ledger))));
|
|
634
|
+
|
|
635
|
+
const docs = d.docs || [];
|
|
636
|
+
const finding = (f, rel) => el('div', { 'data-r22': 'finding', 'data-status': f.status, style: 'padding:2px 14px 2px 26px;font-size:11.5px;line-height:1.5;color:' + (f.status === 'contradicted' ? 'var(--foreground)' : 'var(--muted-foreground)') }, iv(f.sentence));
|
|
637
|
+
const docRow = (x, showFindings) => el('div', { 'data-r22': 'doc', 'data-rel': x.rel, style: 'padding:4px 0;border-bottom:1px solid oklch(1 0 0 / 5%)' },
|
|
638
|
+
el('button', { style: 'display:flex;gap:8px;align-items:baseline;width:100%;padding:2px 14px;text-align:left', hover: 'background:var(--accent)', onClick: () => openDocIn(ws, x.rel, state.product) },
|
|
639
|
+
el('span', { style: 'font-family:var(--mono);font-size:11.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0' }, iv(x.rel)),
|
|
640
|
+
el('span', { style: TYPE_CHIP + ';margin-left:auto;flex:none' }, iv(x.classified ? x.type : 'not classified'))),
|
|
641
|
+
x.compared_with.length ? el('div', { 'data-r22': 'compared', style: 'padding:0 14px 0 26px;font-size:10.5px;color:var(--muted-foreground)' }, iv('compared with: ' + x.compared_with.join(', '))) : (x.classified ? el('div', { style: 'padding:0 14px 0 26px;font-size:10.5px;color:var(--muted-foreground)' }, iv(x.references.length ? 'no counterpart found to compare with' : 'checked on its own structure only')) : el('div', { style: 'padding:0 14px 0 26px;font-size:10.5px;color:var(--muted-foreground)' }, iv('nothing declares what this is, so it is checked on its own links and dates only'))),
|
|
642
|
+
showFindings ? x.findings.map((f) => finding(f, x.rel)) : null);
|
|
643
|
+
const group = (key, title, list, open, showFindings) => {
|
|
644
|
+
if (!list.length) return null;
|
|
645
|
+
return el('div', { 'data-r22': 'group', 'data-group': key },
|
|
646
|
+
el('button', { style: LABEL + ';display:block;width:100%;text-align:left', 'data-r22': 'group-toggle', onClick: open === 'always' ? null : () => cToggle(key) }, iv((open === 'always' || cOpen.has(key) ? '▾ ' : '▸ ') + title + ' · ' + list.length)),
|
|
647
|
+
open === 'always' || cOpen.has(key) ? list.slice(0, 150).map((x) => docRow(x, showFindings)) : null,
|
|
648
|
+
list.length > 150 && (open === 'always' || cOpen.has(key)) ? el('div', { style: NOTE }, iv('… and ' + (list.length - 150) + ' more')) : null);
|
|
649
|
+
};
|
|
650
|
+
const withStatus = (st) => docs.filter((x) => x.findings.some((f) => f.status === st));
|
|
651
|
+
const only = (st) => (x) => ({ ...x, findings: x.findings.filter((f) => f.status === st) });
|
|
652
|
+
kids.push(group('problems', 'problems — a statement is contradicted by its source', withStatus('contradicted').map(only('contradicted')), 'always', true));
|
|
653
|
+
kids.push(group('stale', 'stale — a source changed after the document did, or its date passed', withStatus('stale').map(only('stale')), 'always', true));
|
|
654
|
+
kids.push(group('undecided', 'worth a look — structure cannot decide', withStatus('undecided').map(only('undecided')), 'toggle', true));
|
|
655
|
+
kids.push(group('fine', 'checked and fine', docs.filter((x) => x.classified && !x.findings.length), 'toggle', false));
|
|
656
|
+
kids.push(group('unclassified', 'not classified — nothing declares what these are', docs.filter((x) => !x.classified), 'toggle', true));
|
|
657
|
+
return kids.filter(Boolean);
|
|
658
|
+
}
|
|
659
|
+
|
|
584
660
|
export function validateList() {
|
|
585
661
|
const kids = [];
|
|
586
662
|
const k = read('typesafe', '/api/typesafe');
|
|
587
663
|
const ws = vWs();
|
|
588
664
|
const rp = ws ? read('vrepo', '/api/validate/repo?ws=' + encodeURIComponent(ws)) : null;
|
|
589
665
|
const fd = ws ? read('vfind', '/api/validate/findings?ws=' + encodeURIComponent(ws)) : null;
|
|
666
|
+
kids.push(...claimsBlock(ws));
|
|
667
|
+
kids.push(el('div', { style: LABEL + ';margin-top:10px' }, iv('typesafe (jev) — optional, sends text only after you turn it on')));
|
|
590
668
|
kids.push(el('div', { style: NOTE, 'data-r19': 'intro' }, iv('Checks this repository\'s documents for reliability, coherence and freshness. Local checks run first and send nothing. Text that needs judgement goes to TypeSafe (Jev), only after you switch it on for this repository and confirm what will be sent. Findings are advice; nothing is edited.')));
|
|
591
669
|
|
|
592
670
|
// 1 · the key
|
|
@@ -660,3 +738,146 @@ export function validateList() {
|
|
|
660
738
|
}
|
|
661
739
|
return [el('div', { style: 'padding:2px 0 8px', 'data-r19': 'validate' }, kids)];
|
|
662
740
|
}
|
|
741
|
+
|
|
742
|
+
/* ── Today (proactive-engine D1): what is waiting, computed from the ledgers, with feedback ─────────────────────── */
|
|
743
|
+
let tNote = null;
|
|
744
|
+
let tMissed = false;
|
|
745
|
+
let tRetry = null;
|
|
746
|
+
const tSay = (tone, text) => { tNote = { tone, text }; setState({}); };
|
|
747
|
+
const OWNER_WORD = { pm: 'yours', agent: 'the agent\'s' };
|
|
748
|
+
async function tFeedback(body, say) {
|
|
749
|
+
const out = await post('/api/proactive/feedback', body);
|
|
750
|
+
if (!out || out.ok === false) return tSay('bad', (out && out.error) || 'feedback was not saved');
|
|
751
|
+
tSay('ok', say);
|
|
752
|
+
}
|
|
753
|
+
async function tOpen(c) {
|
|
754
|
+
tFeedback({ candidate_id: c.id, event: 'opened' }, 'opened — recorded');
|
|
755
|
+
const o = c.open || {};
|
|
756
|
+
if (o.type === 'schedule') { setState({ mode: 'scheduled', col0: true }); return; }
|
|
757
|
+
if (c.repo && c.initiative_id) await land({ repo: c.repo, id: c.initiative_id });
|
|
758
|
+
}
|
|
759
|
+
async function tScan() {
|
|
760
|
+
const out = await post('/api/proactive/scan', {});
|
|
761
|
+
reload('today'); reload('brief');
|
|
762
|
+
tSay(!out || out.ok === false ? 'bad' : 'ok', !out || out.ok === false ? ((out && out.error) || 'the scan failed') : 'scanned — ' + out.found + ' waiting, ' + out.appended + ' new');
|
|
763
|
+
}
|
|
764
|
+
async function tSettings(patch) {
|
|
765
|
+
const out = await post('/api/proactive/settings', patch);
|
|
766
|
+
reload('today'); reload('brief');
|
|
767
|
+
tSay(!out || out.ok === false ? 'bad' : 'ok', !out || out.ok === false ? ((out && out.error) || 'not saved') : 'saved');
|
|
768
|
+
}
|
|
769
|
+
function tCard(c) {
|
|
770
|
+
const b = (label, attr, fn) => el('button', { style: BTN, hover: 'color:var(--foreground)', 'data-r22': attr, onClick: fn }, label);
|
|
771
|
+
return el('div', { 'data-r22': 'card', 'data-id': c.id, 'data-kind': c.kind, 'data-repo': c.repo || '', style: 'padding:8px 14px;border-bottom:1px solid var(--border)' },
|
|
772
|
+
el('span', { style: 'display:flex;gap:8px;align-items:baseline' },
|
|
773
|
+
el('span', { style: 'font-size:12.5px' }, iv(c.title)),
|
|
774
|
+
el('span', { style: 'margin-left:auto;font-family:var(--mono);font-size:10px;color:var(--muted-foreground)' }, iv(c.kind + ' · ' + (OWNER_WORD[c.owner] || c.owner)))),
|
|
775
|
+
el('span', { style: 'display:block;font-size:11px;color:var(--muted-foreground);margin:2px 0 4px' }, iv(c.why + (c.evidence ? ' — ' + c.evidence : ''))),
|
|
776
|
+
el('span', { style: 'display:flex;gap:4px;flex-wrap:wrap' },
|
|
777
|
+
c.open ? b('open', 'open', () => tOpen(c)) : null,
|
|
778
|
+
b('useful', 'useful', () => tFeedback({ candidate_id: c.id, verdict: 'useful' }, 'thanks — recorded as useful')),
|
|
779
|
+
b('not useful', 'not-useful', () => tFeedback({ candidate_id: c.id, verdict: 'not-useful' }, 'recorded as not useful')),
|
|
780
|
+
b('wrong priority', 'wrong-priority', () => tFeedback({ candidate_id: c.id, verdict: 'wrong-priority' }, 'recorded as wrong priority')),
|
|
781
|
+
b('dismiss 7 days', 'dismiss', async () => { await tFeedback({ candidate_id: c.id, event: 'dismissed' }, 'dismissed for 7 days'); reload('today'); setState({}); })));
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/* the morning brief (proactive-engine D2): submitted by a scheduled skill in a session, only checked and stored by the server */
|
|
785
|
+
let bNote = null;
|
|
786
|
+
async function bSchedule() {
|
|
787
|
+
const data = await import('./data.js');
|
|
788
|
+
const ws = state.product ? data.wsOf(state.product) : null;
|
|
789
|
+
if (!ws) { bNote = 'select a repository first'; setState({}); return; }
|
|
790
|
+
const out = await post('/api/schedules/seed', { ws, set: 'brief' });
|
|
791
|
+
bNote = !out || out.ok === false ? ((out && out.error) || 'not scheduled')
|
|
792
|
+
: out.created.length ? 'scheduled: ' + out.created[0].name + ' (' + out.created[0].cadence + ' ' + out.created[0].time + ') — it runs only while the workspace server runs. Made after that time today, it also runs once within a minute.'
|
|
793
|
+
: 'nothing to add — ' + out.kept.map((k) => k.skill + ': ' + k.why).join('; ');
|
|
794
|
+
reload('brief'); setState({});
|
|
795
|
+
}
|
|
796
|
+
function bCard(x, ask) {
|
|
797
|
+
const b = (label, attr, fn) => el('button', { style: BTN, hover: 'color:var(--foreground)', 'data-r22': attr, onClick: fn }, label);
|
|
798
|
+
return el('div', { 'data-r22': ask ? 'ask-card' : 'brief-card', 'data-id': x.id, 'data-source': x.source, style: 'padding:8px 14px;border-bottom:1px solid var(--border)' },
|
|
799
|
+
el('span', { style: 'display:flex;gap:8px;align-items:baseline' },
|
|
800
|
+
el('span', { style: 'font-size:12.5px' }, iv(x.title)),
|
|
801
|
+
el('span', { style: 'margin-left:auto;font-family:var(--mono);font-size:10px;color:var(--muted-foreground)' }, iv(x.source + ' · ' + (OWNER_WORD[x.owner] || x.owner) + ' · ' + x.effort + ' · ' + x.confidence))),
|
|
802
|
+
el('span', { style: 'display:block;font-size:11px;color:var(--muted-foreground);margin:2px 0 4px' }, iv(x.why_now + (x.evidence ? ' — ' + x.evidence : ''))),
|
|
803
|
+
el('span', { style: 'display:flex;gap:4px;flex-wrap:wrap' },
|
|
804
|
+
b('useful', 'useful', () => tFeedback({ candidate_id: x.id, verdict: 'useful' }, 'thanks — recorded as useful')),
|
|
805
|
+
b('not useful', 'not-useful', () => tFeedback({ candidate_id: x.id, verdict: 'not-useful' }, 'recorded as not useful')),
|
|
806
|
+
b('wrong priority', 'wrong-priority', () => tFeedback({ candidate_id: x.id, verdict: 'wrong-priority' }, 'recorded as wrong priority'))));
|
|
807
|
+
}
|
|
808
|
+
function briefSection() {
|
|
809
|
+
const r = read('brief', '/api/proactive/brief');
|
|
810
|
+
const st = status(r, 'the brief');
|
|
811
|
+
if (st) return [st];
|
|
812
|
+
const d = r.data; const br = d.brief;
|
|
813
|
+
const kids = [el('div', { 'data-r22': 'greeting', style: 'padding:10px 14px 2px;font-size:15px;color:var(--foreground)' }, iv(d.greeting))];
|
|
814
|
+
if (!br) {
|
|
815
|
+
kids.push(el('div', { style: NOTE, 'data-r22': 'no-brief' }, iv('There is no brief yet. A scheduled skill in an agent session reads your calendar and other sources and submits it. This server only checks and stores it.' + (d.scheduled.length ? ' It is scheduled for ' + d.scheduled.map((x) => x.time).join(', ') + ' and runs only while the workspace server runs.' : ''))));
|
|
816
|
+
if (!d.scheduled.length) kids.push(el('div', { style: 'padding:0 14px 6px' }, el('button', { style: BTN, hover: 'color:var(--foreground)', 'data-r22': 'schedule-brief', onClick: bSchedule }, 'schedule the morning brief (06:45 daily)')));
|
|
817
|
+
if (bNote) kids.push(el('div', { style: NOTE, 'data-r22': 'brief-note' }, iv(bNote)));
|
|
818
|
+
return kids;
|
|
819
|
+
}
|
|
820
|
+
kids.push(el('div', { style: NOTE, 'data-r22': 'brief-date' }, iv('Brief for ' + br.date + ' · made ' + String(br.generated_at).replace('T', ' ').slice(0, 16) + ' UTC · typed scoring ' + (br.judgment.used ? 'used (the brief says so; the server cannot check it)' : 'not used'))));
|
|
821
|
+
if (d.stale) kids.push(el('div', { style: NOTE + ';color:var(--destructive)', 'data-r22': 'stale' }, iv('This brief is for ' + br.date + ', not today (' + d.today + '). It is out of date.')));
|
|
822
|
+
kids.push(el('div', { style: NOTE, 'data-r22': 'sources' }, iv('Sources: ' + Object.entries(br.sources).map(([k, v]) => k + ' ' + v).join(' · '))));
|
|
823
|
+
kids.push(el('div', { style: LABEL }, iv('calendar')));
|
|
824
|
+
if (!br.calendar.length) kids.push(el('div', { style: NOTE, 'data-r22': 'no-events' }, iv(br.sources.calendar === 'read' ? 'No events today.' : 'The calendar was not read, so this says nothing about your day.')));
|
|
825
|
+
for (const e of br.calendar) kids.push(el('div', { 'data-r22': 'event', style: 'padding:3px 14px;font-size:12px' }, iv(new Date(e.start).toTimeString().slice(0, 5) + ' ' + e.title + (e.with ? ' — ' + e.with : ''))));
|
|
826
|
+
kids.push(el('div', { style: LABEL }, iv('priorities')));
|
|
827
|
+
if (!br.priorities.length) kids.push(el('div', { style: NOTE, 'data-r22': 'no-priorities' }, iv('No priorities in this brief.')));
|
|
828
|
+
for (const x of br.priorities) kids.push(bCard(x, false));
|
|
829
|
+
if (br.worth_asking.length) {
|
|
830
|
+
kids.push(el('div', { style: LABEL }, iv('worth asking · the brief is unsure')));
|
|
831
|
+
for (const x of br.worth_asking) kids.push(bCard(x, true));
|
|
832
|
+
}
|
|
833
|
+
return kids;
|
|
834
|
+
}
|
|
835
|
+
export function todayList() {
|
|
836
|
+
const r = read('today', '/api/proactive/today');
|
|
837
|
+
const kids = [el('div', { style: NOTE, 'data-r22': 'intro' }, iv('What is waiting for you, worked out from the ledgers and git on this computer. It runs only while the workspace server runs, reads no mail or calendar, and changes nothing: your feedback only changes how this list is ordered later.'))];
|
|
838
|
+
kids.push(...briefSection());
|
|
839
|
+
kids.push(el('div', { style: LABEL }, iv('waiting on records')));
|
|
840
|
+
kids.push(el('div', { style: 'display:flex;gap:4px;flex-wrap:wrap;padding:4px 14px' },
|
|
841
|
+
el('button', { style: BTN, hover: 'color:var(--foreground)', 'data-r22': 'scan', onClick: tScan }, 'scan now'),
|
|
842
|
+
el('button', { style: BTN, hover: 'color:var(--foreground)', title: 'read the brief and the list again', 'data-r22': 'refresh', onClick: () => { reload('today'); reload('brief'); setState({}); } }, '↻ refresh'),
|
|
843
|
+
el('button', { style: BTN, hover: 'color:var(--foreground)', 'data-r22': 'missed', onClick: () => { tMissed = !tMissed; setState({}); } }, 'something was missed')));
|
|
844
|
+
if (tMissed) {
|
|
845
|
+
const ta = el('input', { type: 'text', placeholder: 'what should it have shown you?', 'data-r22': 'missed-input', style: 'flex:1;min-width:0;font-size:11.5px;padding:4px 8px;border:1px solid var(--border);background:var(--background);color:var(--foreground);min-height:28px' });
|
|
846
|
+
kids.push(el('div', { style: 'display:flex;gap:4px;padding:2px 14px 6px' }, ta,
|
|
847
|
+
el('button', { style: BTN, hover: 'color:var(--foreground)', 'data-r22': 'missed-save', onClick: async () => { await tFeedback({ verdict: 'missed', note: ta.value }, 'recorded as missed'); if (tNote && tNote.tone === 'ok') tMissed = false; setState({}); } }, 'save')));
|
|
848
|
+
}
|
|
849
|
+
if (tNote) kids.push(el('div', { 'data-r22': 'note', style: NOTE + ';color:' + (tNote.tone === 'bad' ? 'var(--destructive)' : tNote.tone === 'ok' ? 'var(--foreground)' : 'var(--muted-foreground)') }, iv(tNote.text)));
|
|
850
|
+
const st = status(r, 'Today');
|
|
851
|
+
if (st) return [...kids, st];
|
|
852
|
+
const d = r.data;
|
|
853
|
+
const s = d.settings;
|
|
854
|
+
if (d.off) kids.push(el('div', { style: NOTE, 'data-r22': 'off' }, iv('The engine is switched off. Nothing is scanned automatically; "scan now" still works.')));
|
|
855
|
+
if (d.quiet) kids.push(el('div', { style: NOTE, 'data-r22': 'quiet' }, iv('Quiet hours (' + s.quiet.start + '–' + s.quiet.end + '): no automatic scans now.')));
|
|
856
|
+
// a repository still being read is asked again in a few seconds, so the list fills in without a press
|
|
857
|
+
if ((d.unreadable || []).length && !tRetry && typeof setTimeout === 'function') tRetry = setTimeout(() => { tRetry = null; reload('today'); setState({}); }, 4000);
|
|
858
|
+
for (const u of d.unreadable || []) kids.push(el('div', { style: NOTE + ';color:var(--destructive)', 'data-r22': 'unreadable' }, iv('Not read — ' + (u.name || u.repo || 'a repository') + ': ' + (u.error || 'unknown reason') + '. Nothing is shown for it, which is not the same as nothing waiting.')));
|
|
859
|
+
if (d.empty) kids.push(el('div', { style: NOTE, 'data-r22': 'empty' }, iv('Nothing is waiting on any repository that was read.')));
|
|
860
|
+
for (const c of d.candidates || []) kids.push(tCard(c));
|
|
861
|
+
if (d.dismissed) kids.push(el('div', { style: NOTE, 'data-r22': 'dismissed' }, iv(d.dismissed + ' dismissed item(s) hidden for 7 days.')));
|
|
862
|
+
// settings
|
|
863
|
+
kids.push(el('div', { style: LABEL }, iv('settings')));
|
|
864
|
+
const chk = (label, on, attr, patch) => el('label', { style: 'display:flex;gap:8px;align-items:center;padding:2px 14px;font-size:12px' },
|
|
865
|
+
el('input', { type: 'checkbox', checked: on ? 'checked' : null, 'data-r22': attr, onChange: () => tSettings(patch(!on)) }), iv(label));
|
|
866
|
+
kids.push(chk('engine on', s.enabled, 'set-enabled', (v) => ({ enabled: v })));
|
|
867
|
+
for (const t of Object.keys(s.triggers)) kids.push(chk('watch: ' + t, s.triggers[t], 'set-' + t, (v) => ({ triggers: { [t]: v } })));
|
|
868
|
+
kids.push(el('div', { style: LABEL }, iv('morning brief')));
|
|
869
|
+
for (const k of Object.keys(s.briefSources || {})) kids.push(chk('read ' + k + (k === 'mail' ? ' (titles and senders only)' : ''), s.briefSources[k], 'set-src-' + k, (v) => ({ briefSources: { [k]: v } })));
|
|
870
|
+
kids.push(chk('typed scoring (sends titles and metadata to the Typesafe service)', s.briefScoring, 'set-scoring', (v) => ({ briefScoring: v })));
|
|
871
|
+
const nm = el('input', { type: 'text', maxlength: '40', placeholder: 'your name for the greeting', value: s.name || '', 'data-r22': 'set-name', style: 'width:180px;font-size:11.5px;padding:2px 6px;border:1px solid var(--border);background:var(--background);color:var(--foreground)' });
|
|
872
|
+
kids.push(el('div', { style: 'display:flex;gap:6px;align-items:center;padding:4px 14px;font-size:12px' }, nm, el('button', { style: BTN, 'data-r22': 'set-name-save', onClick: () => tSettings({ name: nm.value }) }, 'save')));
|
|
873
|
+
kids.push(el('div', { style: LABEL }, iv('learning')));
|
|
874
|
+
kids.push(chk('pause learning (nothing learns yet)', s.pauseLearning, 'set-pause', (v) => ({ pauseLearning: v })));
|
|
875
|
+
const days = el('input', { type: 'number', min: '1', max: '90', value: String(s.stalledDays), 'data-r22': 'set-stalled-days', style: 'width:60px;font-size:11.5px;padding:2px 6px;border:1px solid var(--border);background:var(--background);color:var(--foreground)' });
|
|
876
|
+
const qs = el('input', { type: 'text', placeholder: '22:00', value: s.quiet ? s.quiet.start : '', 'data-r22': 'set-quiet-start', style: 'width:56px;font-size:11.5px;padding:2px 6px;border:1px solid var(--border);background:var(--background);color:var(--foreground)' });
|
|
877
|
+
const qe = el('input', { type: 'text', placeholder: '07:00', value: s.quiet ? s.quiet.end : '', 'data-r22': 'set-quiet-end', style: 'width:56px;font-size:11.5px;padding:2px 6px;border:1px solid var(--border);background:var(--background);color:var(--foreground)' });
|
|
878
|
+
kids.push(el('div', { style: 'display:flex;gap:6px;align-items:center;flex-wrap:wrap;padding:4px 14px;font-size:12px' }, iv('stalled after'), days, iv('days'),
|
|
879
|
+
el('button', { style: BTN, 'data-r22': 'set-stalled-save', onClick: () => tSettings({ stalledDays: Number(days.value) }) }, 'save')));
|
|
880
|
+
kids.push(el('div', { style: 'display:flex;gap:6px;align-items:center;flex-wrap:wrap;padding:4px 14px 8px;font-size:12px' }, iv('quiet hours'), qs, iv('to'), qe,
|
|
881
|
+
el('button', { style: BTN, 'data-r22': 'set-quiet-save', onClick: () => tSettings({ quiet: qs.value.trim() || qe.value.trim() ? { start: qs.value.trim(), end: qe.value.trim() } : null }) }, 'save')));
|
|
882
|
+
return [el('div', { style: 'padding:2px 0 8px', 'data-r22': 'today' }, kids)];
|
|
883
|
+
}
|
|
@@ -1046,6 +1046,7 @@ const LISTS = {
|
|
|
1046
1046
|
portfolio: () => (FIXTURE ? [] : LIVE.portfolioList()),
|
|
1047
1047
|
guide: () => (FIXTURE ? [] : LIVE.guideList()),
|
|
1048
1048
|
validate: () => (FIXTURE ? [] : LIVE.validateList()),
|
|
1049
|
+
today: () => (FIXTURE ? [] : LIVE.todayList()),
|
|
1049
1050
|
agent: agentsList,
|
|
1050
1051
|
connect: connectList,
|
|
1051
1052
|
memory: () => (FIXTURE ? memoryList() : LIVE.memoryLive()),
|
|
@@ -1068,7 +1069,7 @@ const BODIES = {
|
|
|
1068
1069
|
* a mode or a detail kind that is not in it is a failing test, so a new one has to say which column it opens in. */
|
|
1069
1070
|
export const MODE_PLACE = {
|
|
1070
1071
|
lists: { product: 'navigation', focus: 'navigation', scheduled: 'navigation', workflow: 'navigation', skills: 'navigation',
|
|
1071
|
-
knowledge: 'navigation', portfolio: 'navigation', guide: 'navigation', validate: 'navigation', agent: 'navigation', connect: 'navigation', memory: 'navigation' },
|
|
1072
|
+
knowledge: 'navigation', portfolio: 'navigation', guide: 'navigation', validate: 'navigation', today: 'navigation', agent: 'navigation', connect: 'navigation', memory: 'navigation' },
|
|
1072
1073
|
details: { init: 'detail', flow: 'detail', sched: 'detail', skill: 'detail', know: 'detail', agent: 'detail', mem: 'detail',
|
|
1073
1074
|
prof: 'detail', intg: 'detail', addprod: 'detail', bind: 'detail', new: 'detail', none: 'detail' },
|
|
1074
1075
|
};
|
|
@@ -1078,7 +1079,7 @@ export const MODE_PLACE = {
|
|
|
1078
1079
|
export const ROW_OPENS = {
|
|
1079
1080
|
product: 'detail', portfolio: 'detail', focus: 'detail',
|
|
1080
1081
|
skills: 'tab', knowledge: 'tab', guide: 'tab', validate: 'tab',
|
|
1081
|
-
scheduled: 'list', memory: 'list',
|
|
1082
|
+
scheduled: 'list', memory: 'list', today: 'list',
|
|
1082
1083
|
};
|
|
1083
1084
|
/** The column a list mode opens in: always `navigation`. `null` for a mode this table does not know. */
|
|
1084
1085
|
export const placeOfMode = (mode) => MODE_PLACE.lists[mode] || null;
|
|
@@ -177,8 +177,8 @@ export const UNBACKED_MODES = ['agent', 'connect']; // mode ids (labels: agent
|
|
|
177
177
|
/* overhaul-r4 — the PORTFOLIO mode joins "all repositories" in live mode only (the fixture drawer is the
|
|
178
178
|
* prototype's, and the design comparison reads it). Its meta lives here, not in fixture.js, for the
|
|
179
179
|
* same reason. */
|
|
180
|
-
const META = Object.assign({}, MODE_META, { portfolio: ['▦', 'portfolio'], guide: ['?', 'guide'], validate: ['◉', 'validate'] });
|
|
181
|
-
const LIVE_EXTRA = { 'all repositories': ['portfolio', 'guide', 'validate'] };
|
|
180
|
+
const META = Object.assign({}, MODE_META, { portfolio: ['▦', 'portfolio'], guide: ['?', 'guide'], validate: ['◉', 'validate'], today: ['☀', 'today'] });
|
|
181
|
+
const LIVE_EXTRA = { 'all repositories': ['today', 'portfolio', 'guide', 'validate'] };
|
|
182
182
|
const DRAWER_GROUPS = SETTINGS_GROUPS.map((g) => ({
|
|
183
183
|
label: g.label,
|
|
184
184
|
items: (FIXTURE ? [] : LIVE_EXTRA[g.label] || []).concat(
|
|
@@ -476,17 +476,70 @@ function sheetBody(tab) {
|
|
|
476
476
|
]);
|
|
477
477
|
}
|
|
478
478
|
|
|
479
|
+
/* ── file-viewer-any-text — a document that is not markdown, a page or a grid is SOURCE ──────────────────
|
|
480
|
+
* `previewKind` decides what counts as markdown or HTML, in the one place the server also trusts. Anything else
|
|
481
|
+
* the reader serves is shown as numbered monospace lines, never through the markdown renderer: JSON pretty-printed,
|
|
482
|
+
* JSONL as one record per line. These tabs are read-only, because /api/doc/save refuses everything but markdown and
|
|
483
|
+
* an edit or save button that cannot work is worse than none. If the classifier did not load, nothing here applies
|
|
484
|
+
* and the pane behaves as before. */
|
|
485
|
+
function isSourcePath(p) {
|
|
486
|
+
if (isReference(p)) return false; // an external reference has no file to show
|
|
487
|
+
const PK = typeof window !== 'undefined' ? window.PreviewKind : null;
|
|
488
|
+
if (!PK || typeof PK.previewKind !== 'function') return false;
|
|
489
|
+
const kind = PK.previewKind(String(p || ''));
|
|
490
|
+
if (kind === 'markdown' || kind === 'html') return false;
|
|
491
|
+
const e = extOfPath(p);
|
|
492
|
+
return !(e === 'csv' || e === 'tsv' || e === 'pdf' || e === 'xlsx');
|
|
493
|
+
}
|
|
494
|
+
const SRC_CAP = 5000; // lines drawn; the cap is STATED when it applies
|
|
495
|
+
const JSONL_EXT = new Set(['jsonl', 'ndjson', 'jsonlines', 'ldjson']);
|
|
496
|
+
function sourceBody(tab, text) {
|
|
497
|
+
const ext = extOfPath(tab.path);
|
|
498
|
+
return scrollPane(tab, 'view', 'src\u0000' + text, RAW_ST, () => {
|
|
499
|
+
const head = (t, bad) => el('div', {
|
|
500
|
+
'data-r21': 'source-note',
|
|
501
|
+
style: 'padding:2px 0 8px;font-family:var(--mono);font-size:10.5px;color:' + (bad ? 'var(--destructive)' : 'var(--muted-foreground)'),
|
|
502
|
+
}, iv(t));
|
|
503
|
+
const row = (n, t, bad) => el('div', { style: 'display:flex;gap:10px;min-height:1.2em' },
|
|
504
|
+
el('span', { style: 'flex:none;min-width:4ch;text-align:right;color:var(--muted-foreground);user-select:none' }, iv(String(n))),
|
|
505
|
+
el('div', { style: 'flex:1;min-width:0;white-space:pre-wrap;overflow-wrap:anywhere' + (bad ? ';color:var(--destructive)' : '') }, iv(t)));
|
|
506
|
+
const lines = (arr) => arr.slice(0, SRC_CAP).map((t, i) => row(i + 1, t, false));
|
|
507
|
+
const capNote = (n) => (n > SRC_CAP ? [head('Showing the first ' + SRC_CAP + ' of ' + n + ' lines. The rest is not drawn. Use raw to read it all.', false)] : []);
|
|
508
|
+
const all = text.split('\n');
|
|
509
|
+
if (ext === 'json') {
|
|
510
|
+
let v; let err = null;
|
|
511
|
+
try { v = JSON.parse(text); } catch (e) { err = (e && e.message) || 'parse error'; }
|
|
512
|
+
if (err) return [head('JSON · read-only · NOT valid JSON: ' + err + ' · shown as written', true), ...lines(all), ...capNote(all.length)];
|
|
513
|
+
const pretty = JSON.stringify(v, null, 2).split('\n');
|
|
514
|
+
return [head('JSON · read-only · pretty-printed · ' + pretty.length + ' lines', false), ...lines(pretty), ...capNote(pretty.length)];
|
|
515
|
+
}
|
|
516
|
+
if (JSONL_EXT.has(ext)) {
|
|
517
|
+
const recs = []; let bad = 0; let n = 0;
|
|
518
|
+
all.forEach((t, i) => {
|
|
519
|
+
if (!t.trim()) return;
|
|
520
|
+
n++;
|
|
521
|
+
if (recs.length >= SRC_CAP) return;
|
|
522
|
+
try { recs.push(row(i + 1, JSON.stringify(JSON.parse(t), null, 2), false)); }
|
|
523
|
+
catch (e) { bad++; recs.push(row(i + 1, t + '\n⚠ line ' + (i + 1) + ' is not valid JSON: ' + ((e && e.message) || 'parse error'), true)); }
|
|
524
|
+
});
|
|
525
|
+
return [head('JSONL · read-only · ' + n + ' record' + (n === 1 ? '' : 's') + (bad ? ' · ' + bad + ' not valid JSON (marked)' : '') + (n > SRC_CAP ? ' · first ' + SRC_CAP + ' shown' : ''), bad > 0), ...recs];
|
|
526
|
+
}
|
|
527
|
+
return [head((ext || 'text') + ' · read-only · source', false), ...lines(all), ...capNote(all.length)];
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
|
|
479
531
|
export function mountDoc(host, tab) {
|
|
480
532
|
if (!FIXTURE && extOfPath(tab.path) === 'pdf') { setChildren(host, [pdfBody(tab)]); return; }
|
|
481
533
|
if (!FIXTURE && extOfPath(tab.path) === 'xlsx') { setChildren(host, [sheetBody(tab)]); return; }
|
|
482
534
|
const raw = docText(tab);
|
|
483
535
|
const text = state.docEdits[docKey(tab)] ?? raw;
|
|
484
|
-
const
|
|
536
|
+
const nonMd = !FIXTURE && isSourcePath(tab.path); // file-viewer-any-text: read-only, shown as source
|
|
537
|
+
const dm = ((tab.readonly || nonMd) && (state.docModes[tab.id] === 'edit' || state.docModes[tab.id] === 'diff')) ? 'view' : (state.docModes[tab.id] ?? 'view');
|
|
485
538
|
const dirty = text !== raw;
|
|
486
539
|
const err = saveErr.get(docKey(tab));
|
|
487
540
|
|
|
488
541
|
// A file opened from the initiative's git files is evidence of what changed: it is read, never edited or saved here.
|
|
489
|
-
const readonly = !!tab.readonly;
|
|
542
|
+
const readonly = !!tab.readonly || nonMd;
|
|
490
543
|
const modeBtns = ['view', 'raw', 'edit', 'diff'].filter((mm) => !(readonly && (mm === 'edit' || mm === 'diff'))).map((mm) => el('button', {
|
|
491
544
|
onClick: () => setState((s) => ({ docModes: { ...s.docModes, [tab.id]: mm } })),
|
|
492
545
|
style: 'font-family:var(--mono);font-size:10.5px;padding:4px 10px;min-height:26px;background:'
|
|
@@ -517,7 +570,7 @@ export function mountDoc(host, tab) {
|
|
|
517
570
|
* clean file dirty for nothing is how a tool teaches you to ignore its dirty flag. */
|
|
518
571
|
const norm = typeof window !== 'undefined' ? window.MdNormalize : null;
|
|
519
572
|
const canNormalize = !!(norm && typeof norm.normalize === 'function')
|
|
520
|
-
&& !isHtmlDoc(tab.path) && !isReference(tab.path) && !unread.has(docKey(tab));
|
|
573
|
+
&& !isHtmlDoc(tab.path) && !isReference(tab.path) && !unread.has(docKey(tab)) && !nonMd;
|
|
521
574
|
const normBtn = canNormalize ? el('button', {
|
|
522
575
|
onClick: () => {
|
|
523
576
|
const r = norm.normalize(text);
|
|
@@ -566,7 +619,7 @@ export function mountDoc(host, tab) {
|
|
|
566
619
|
if (body.defaultValue !== text) body.defaultValue = text;
|
|
567
620
|
if (body.value !== text) body.value = text;
|
|
568
621
|
} else if (!FIXTURE && (extOfPath(tab.path) === 'csv' || extOfPath(tab.path) === 'tsv')) body = gridBody(tab, text);
|
|
569
|
-
else body = rendersAsPage(tab) ? htmlFrame(tab, text) : viewBody(tab, text);
|
|
622
|
+
else body = rendersAsPage(tab) ? htmlFrame(tab, text) : (nonMd && !unread.has(docKey(tab)) ? sourceBody(tab, text) : viewBody(tab, text));
|
|
570
623
|
|
|
571
624
|
setChildren(host, [modeBar, body]);
|
|
572
625
|
return host;
|