Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions STARTER.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,19 @@ The example GitHub identity and `example.invalid` address are placeholders,
not repositories or a deployed service. This example body map is a fixture,
not a complete Mind installation.

## Navigate before connecting organs

The instance preview includes the familiar Scientist/organ icon bar above the
overview. Scientist returns to the overview; each connected organ opens its
configured board inside the page, with an external-link fallback. Paused organs
remain navigable without resuming their feed observation.

An unconnected organ opens its existing Dashboards description and setup prompt,
with a **Copy setup prompt** button. Setup-incomplete organs use the same resume
prompt as their overview card. These views share their text with Dashboards;
selecting one does not connect an organ or request its feed. Links such as
`#organ-brain` can be bookmarked, and Back/Forward switch the selected view.

## Initialize a user-owned configuration directory

If you already have a canonical Mind `repos.yaml`, the offline initializer saves
Expand Down
42 changes: 42 additions & 0 deletions scripts/instance_navigation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// The shell keeps its navigation while selecting an instance-owned destination.
(() => {
const nav = document.querySelector('.instance-nav');
if (!nav) return;
const overview = document.getElementById('scientist');
const views = [...document.querySelectorAll('.instance-view')];
const links = [...nav.querySelectorAll('a')];
function select(focus = false) {
const view = views.find(v => '#' + v.id === location.hash);
overview.hidden = !!view;
for (const panel of views) panel.hidden = panel !== view;
const selected = view ? '#' + view.id : '#scientist';
for (const link of links) {
if (link.hash === selected) link.setAttribute('aria-current', 'page');
else link.removeAttribute('aria-current');
}
const frame = view && view.querySelector('iframe');
if (frame && !frame.dataset.loaded) {
// Replace the child entry so Back/Forward belongs to the shell routes.
frame.contentWindow.location.replace(frame.dataset.boardSrc);
frame.dataset.loaded = 'true';
}
if (focus) {
const heading = view ? view.querySelector('h1') : overview.querySelector('h1');
if (heading) { heading.setAttribute('tabindex', '-1'); heading.focus({preventScroll: true}); }
window.scrollTo(0, 0);
}
nav.querySelector('[aria-current="page"]').scrollIntoView({block: 'nearest', inline: 'nearest'});
}
history.scrollRestoration = 'manual';
window.addEventListener('hashchange', () => select(true));
window.addEventListener('popstate', () => select(true));
nav.addEventListener('click', event => {
const link = event.target.closest('a');
if (!link || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
event.preventDefault();
event.stopPropagation(); // The shared section-link handler must not scroll behind the shell.
if (link.hash !== location.hash) history.pushState(null, '', link.hash);
select(true);
});
select();
})();
82 changes: 82 additions & 0 deletions scripts/instance_navigation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
"""Instance-owned cockpit navigation; optional organs never gain implicit URLs."""
from html import escape

# Match the existing cockpit's icon/label order (including Scientist first).
# This is presentation only: membership and destinations come from the profile.
ICONS = {
'scientist': '🔭', 'brain': '🧠', 'mind': '💭', 'cortex': '🔬',
'broca': '🗣️', 'memory': '📚', 'eyes': '👁️', 'ears': '👂',
'heart': '❤️', 'hands': '🛠️', 'pulse': '⏱️', 'insight': '💡',
'dna': '🧬', 'nerves': '⚡', 'gut': '♻️',
}


def inactive_content(organ, *, pending=False):
"""One verbatim description/prompt for both the overview and organ page."""
key = organ['key']
if pending:
prompt = (f"Resume setup for {organ['name']} in my existing Mind task. "
"Read my profile and canonical map first. Validate the owner setup and published feed, "
"then preview connect_organ.py before applying. Preserve existing records.")
label = f'Resume {key} setup'
else:
prompt = (f"Help me add {organ['name']} ({organ['role']}) to this Scientist. "
"Read my existing profile, canonical map and Mind task. Confirm the purpose, repository "
"identity and public board location; preview organ_lifecycle.py setup before applying. "
"Keep it setup incomplete until connect_organ.py validates its published feed.")
label = f'Add {key}'
note = '<p>Not connected. No feed requested.</p>' if pending else ''
return (f'{note}<p>{escape(organ["role"])}</p>'
f'<textarea readonly aria-label="{escape(label, quote=True)}">{escape(prompt)}</textarea>'
f'<button type="button" class="copy organ-setup-copy" data-cmd="{escape(prompt, quote=True)}" '
f'aria-label="Copy {escape(label, quote=True)} prompt">Copy setup prompt</button>')


def render(instance):
"""Return navigation and local destinations without contacting any board."""
entries = {o['key']: (o, state) for state in ('boards', 'setup', 'available')
for o in instance.get(state, [])}
links = ['<li><a class="board-link" href="#scientist" aria-current="page">'
'<span class="nav-icon" aria-hidden="true">🔭</span><span>Scientist</span></a></li>']
panels = []
for key in dict.fromkeys([*ICONS, *entries]):
if key not in entries:
continue
organ, state = entries[key]
name = escape(organ['name'])
status = ('Paused' if organ.get('paused') else 'Connected') if state == 'boards' else (
'Setup incomplete' if state == 'setup' else 'Available to add')
links.append(f'<li><a class="board-link" href="#organ-{key}" title="{name} · {status}">'
f'<span class="nav-icon" aria-hidden="true">{ICONS.get(key, "◉")}</span>'
f'<span>{name}</span></a></li>')
if state == 'boards':
url = escape(organ['url'], quote=True)
content = (f'<p><a href="{url}" target="_blank" rel="noopener">Open {name} in a new tab →</a></p>'
f'<iframe title="{name} board" data-board-src="{url}" loading="lazy"></iframe>')
else:
content = inactive_content(organ, pending=state == 'setup')
panels.append(f'<section class="instance-view" id="organ-{key}" hidden '
f'aria-labelledby="organ-{key}-heading"><h1 id="organ-{key}-heading" tabindex="-1">'
f'{name}</h1><p class="organ-state">{status}</p>{content}</section>')
nav = ('<nav class="instance-nav" aria-label="Organ boards"><div class="nav-scroll"><ul>'
+ ''.join(links) + '</ul></div></nav>')
return nav, ''.join(panels)


CSS = """
.instance-nav{position:sticky;top:0;z-index:20;background:var(--bg);border-bottom:1px solid var(--line);margin-bottom:1rem}
.instance-nav .nav-scroll{overflow-x:auto;overscroll-behavior-x:contain;scrollbar-width:thin;padding:.45rem .5rem}
.instance-nav ul{display:flex;gap:.3rem;list-style:none;width:max-content;margin:0 auto;padding:0}
.instance-nav .board-link{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.15rem;min-width:76px;min-height:64px;padding:.4rem .65rem;border:1px solid transparent;border-radius:12px;color:var(--muted);font-size:.8rem;font-weight:650;text-decoration:none;box-sizing:border-box}
.instance-nav .board-link:hover{background:var(--panel,var(--bg));color:var(--fg)}
.instance-nav .board-link[aria-current=page]{color:var(--accent);border-color:var(--accent)}
.instance-nav .nav-icon{font-size:1.55rem;line-height:1.25}
.instance-nav :focus-visible,.instance-view :focus-visible{outline:3px solid var(--accent);outline-offset:3px}
.instance-view[hidden],#scientist[hidden]{display:none!important}
.instance-view,#scientist{scroll-margin-top:7rem}
.instance-view h1{font-size:1.8rem;margin-bottom:.3rem}
.organ-state{color:var(--muted)}
.instance-view textarea{width:100%;box-sizing:border-box;min-height:10rem;font:inherit;padding:.75rem;background:var(--bg);color:var(--fg);border:1px solid var(--line);border-radius:6px;resize:vertical}
button.organ-setup-copy{width:auto;height:auto;min-height:44px;padding:.5rem .8rem;margin-top:.5rem;font:inherit}
.instance-view iframe{display:block;width:100%;height:75vh;height:75dvh;min-height:360px;border:1px solid var(--line);background:var(--bg)}
"""
28 changes: 12 additions & 16 deletions scripts/organism_board.py
Original file line number Diff line number Diff line change
Expand Up @@ -332,27 +332,21 @@ def _render_html(snapshot: dict) -> str:
<div class="organ-content"><dl><div class="summary-row"><dt>Published status</dt><dd>{head}</dd></div></dl>
<div class="organ-foot"><span data-freshness>{_html.escape(note)}</span><a href="{url}">Open {organ_name} →</a></div></div></details>""")
catalogue = ""
navigation = views = navigation_css = navigation_script = ""
if instance is not None:
from instance_navigation import inactive_content, render as render_navigation, CSS
navigation, views = render_navigation(dict(instance, boards=snapshot.get("boards", [])))
navigation_css = CSS
navigation_script = (HOME / "scripts" / "instance_navigation.js").read_text(encoding="utf-8")
for pending in instance.get("setup", []):
key = pending["key"]
prompt_text = (f"Resume setup for {pending['name']} in my existing Mind task. "
"Read my profile and canonical map first. Validate the owner setup and published feed, "
"then preview connect_organ.py before applying. Preserve existing records.")
rows.append(f'<details class="organ-card" id="setup-{key}"><summary>'
f'{_html.escape(pending["name"])} · Setup incomplete</summary>'
f'<div class="organ-content"><p>Not connected. No feed requested.</p>'
f'<p>{_html.escape(pending["role"])}</p><textarea readonly aria-label="Resume {key} setup">'
f'{_html.escape(prompt_text)}</textarea></div></details>')
f'<div class="organ-content">{inactive_content(pending, pending=True)}</div></details>')
entries = []
for available in instance.get("available", []):
key = available["key"]
prompt_text = (f"Help me add {available['name']} ({available['role']}) to this Scientist. "
"Read my existing profile, canonical map and Mind task. Confirm the purpose, repository "
"identity and public board location; preview organ_lifecycle.py setup before applying. "
"Keep it setup incomplete until connect_organ.py validates its published feed.")
entries.append(f'<details><summary>{_html.escape(available["name"])} · Available to add</summary>'
f'<p>{_html.escape(available["role"])}</p><textarea readonly aria-label="Add {key}">'
f'{_html.escape(prompt_text)}</textarea></details>')
f'{inactive_content(available)}</details>')
if entries:
catalogue = '<details class="organ-catalogue"><summary>Available organs</summary>' + ''.join(entries) + '</details>'
data = json.dumps(snapshot, ensure_ascii=False).replace("<", "\\u003c").replace(">", "\\u003e").replace("&", "\\u0026")
Expand All @@ -363,17 +357,19 @@ def _render_html(snapshot: dict) -> str:
# whole grid inside section_layout's extra collapsed H2 group.
return f"""<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>{_html.escape(name)} Dashboard</title><style>{t.css(BOARD_KEY)}{_EXTRA_CSS}</style></head>
<body>{banner}{panel}
<title>{_html.escape(name)} Dashboard</title><style>{t.css(BOARD_KEY)}{_EXTRA_CSS}{navigation_css}</style></head>
<body>{navigation}{'<main id="scientist">' if instance is not None else ""}{banner}{panel}
{projects_html}
<h2 id="dashboards-heading">Dashboards</h2>
{'<p>No organs connected. Add a connection when your project needs it.</p>' if instance is not None and not snapshot.get("boards") else ''}
<div id="dashboards" class="organ-grid" aria-label="Organ summaries">{''.join(rows)}</div>
{catalogue}
<noscript><p>Publication snapshot. Open an organ for its latest evidence.</p></noscript>
{footer}
{'</main>' if instance is not None else ""}{views}
{'<noscript><style>.instance-view[hidden]{display:block!important}</style></noscript>' if instance is not None else ""}
<script type="application/json" id="scientist-snapshot">{data}</script>
<script>{t.JS}</script><script>{script}</script></body></html>"""
<script>{t.JS}</script><script>{script}</script><script>{navigation_script}</script></body></html>"""


def badge_endpoint(snapshot: dict) -> dict:
Expand Down
84 changes: 84 additions & 0 deletions tests/navigation_browser.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// NODE_PATH=<playwright modules> node tests/navigation_browser.cjs <fixtures> <evidence>
// Fixtures: CLI-rendered empty.html, connected.html and examples/lifecycle as lifecycle.html.
const {chromium} = require('playwright');
const fs = require('node:fs');
const path = require('node:path');
const http = require('node:http');
const assert = require('node:assert/strict');
(async () => {
const fixtures = path.resolve(process.argv[2]), evidence = path.resolve(process.argv[3]);
fs.mkdirSync(evidence,{recursive:true});
const server = http.createServer((req,res)=>{
const name = new URL(req.url,'http://localhost').pathname.slice(1);
if (!['empty.html','connected.html','lifecycle.html'].includes(name)) {res.writeHead(404);res.end();return;}
res.setHeader('Content-Type','text/html; charset=utf-8');res.end(fs.readFileSync(path.join(fixtures,name)));
});
await new Promise(resolve=>server.listen(0,'127.0.0.1',resolve));
let browser;
try {
browser=await chromium.launch({headless:true,...(process.env.PLAYWRIGHT_EXECUTABLE_PATH ? {executablePath:process.env.PLAYWRIGHT_EXECUTABLE_PATH} : {channel:'chromium'})});
const base=`http://127.0.0.1:${server.address().port}/`, results=[];
for(const width of [390,768,820,1024,1440]) for(const colorScheme of ['light','dark']) {
const ctx=await browser.newContext({viewport:{width,height:1000},colorScheme,permissions:['clipboard-read','clipboard-write']});
const requests=[];
await ctx.route('https://**/*',route=>{requests.push(route.request().url());return route.fulfill({status:200,contentType:'text/html',body:'<h1>Configured board</h1>'});});
const page=await ctx.newPage(),errors=[];page.on('pageerror',e=>errors.push(e.message));
for(const variant of ['empty','connected']) {
await page.goto(base+variant+'.html');
assert.equal(await page.locator('.instance-nav a').count(),15);
assert.equal(await page.locator('#scientist').isVisible(),true);
await page.locator('.organ-catalogue > summary').click();
const original=await page.locator('.organ-catalogue textarea[aria-label="Add brain"]').inputValue();
// Native keyboard link activation is the same navigation route as a pointer.
await page.locator('.instance-nav a[href="#organ-brain"]').focus();await page.keyboard.press('Enter');
await page.waitForFunction(()=>!document.getElementById('organ-brain').hidden);
assert.equal(await page.locator('#scientist').isVisible(),false);
assert.equal(await page.locator('#organ-brain textarea').inputValue(),original);
assert.equal(await page.locator('#organ-brain h1').evaluate(el=>el===document.activeElement),true);
await page.locator('#organ-brain button.copy').click();
assert.equal(await page.evaluate(()=>navigator.clipboard.readText()),original);
assert.equal(await page.locator('.instance-nav a[aria-current="page"]').getAttribute('href'),'#organ-brain');
assert.equal(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth),true);
if(variant==='connected') await page.screenshot({path:path.join(evidence,`brain-${width}-${colorScheme}.png`),fullPage:true});
for(const href of await page.locator('.instance-nav a').evaluateAll(as=>as.map(a=>a.getAttribute('href')))) {
await page.locator(`.instance-nav a[href="${href}"]`).click();
await page.waitForFunction(href=>document.querySelector('.instance-nav [aria-current="page"]').getAttribute('href')===href,href);
}
await page.goto(base+variant+'.html#organ-brain');
assert.equal(await page.locator('#organ-brain').isVisible(),true);
await page.locator('.instance-nav a[href="#organ-mind"]').click();
await page.goBack();await page.waitForFunction(()=>!document.getElementById('organ-brain').hidden);
await page.goForward();await page.waitForFunction(()=>!document.getElementById('organ-mind').hidden);
await page.locator('.instance-nav a[href="#scientist"]').click();
await page.waitForFunction(()=>!document.getElementById('scientist').hidden);
await page.evaluate(()=>new Promise(resolve=>requestAnimationFrame(()=>requestAnimationFrame(resolve))));
assert(await page.evaluate(()=>document.querySelector('#scientist .hero').getBoundingClientRect().top >= document.querySelector('.instance-nav').getBoundingClientRect().bottom),'navigation must not cover the overview banner');
if(variant==='connected') await page.screenshot({path:path.join(evidence,`overview-${width}-${colorScheme}.png`),fullPage:true});
}
assert.deepEqual(requests,[],'empty and disconnected project must not request any external URL');
await page.goto(base+'lifecycle.html');await page.waitForFunction(()=>!refreshing);
assert(requests.every(url=>url.startsWith('https://example.invalid/checks/state.json')));
await page.locator('.instance-nav a[href="#organ-broca"]').click();
assert.equal(await page.locator('#organ-broca textarea').inputValue(),await page.locator('#setup-broca textarea').inputValue());
await page.locator('.instance-nav a[href="#organ-brain"]').click();
await page.locator('.instance-nav a[href="#organ-mind"]').click();
await page.frameLocator('#organ-mind iframe').locator('h1').waitFor();
assert.equal(await page.locator('#organ-mind .organ-state').textContent(),'Paused');
assert(requests.includes('https://example.invalid/plans/'));
assert(!requests.some(url=>url.startsWith('https://example.invalid/plans/state.json')));
await page.goBack();await page.waitForFunction(()=>!document.getElementById('organ-brain').hidden);
await page.goForward();await page.waitForFunction(()=>!document.getElementById('organ-mind').hidden);
await page.locator('.instance-nav a[href="#organ-heart"]').click();
await page.frameLocator('#organ-heart iframe').locator('h1').waitFor();
assert(requests.includes('https://example.invalid/checks/'));
assert.deepEqual(errors,[]);
results.push({width,colorScheme,variants:3,passed:true});await ctx.close();
}
const ctx=await browser.newContext({javaScriptEnabled:false});
const page=await ctx.newPage();await page.goto(base+'empty.html#organ-brain');
assert.equal(await page.locator('#organ-brain textarea').isVisible(),true);
await ctx.close();
fs.writeFileSync(path.join(evidence,'validation.json'),JSON.stringify(results,null,2));
console.log('PASS: 30 browser cases, keyboard/copy/history, inactive network isolation, connected/paused routing, no-JS text.');
} finally {if(browser)await browser.close();await new Promise(resolve=>server.close(resolve));}
})().catch(e=>{console.error(e);process.exitCode=1;});
Loading
Loading