// Shared runtime for the five mockups: theme, hash router, demo states, simulated actions. // Every action here is simulated. Nothing is requested from a server and nothing outside this browser changes. (() => { 'use strict'; const B = window.Brand, V = window.MosaicViews, D = window.MosaicData, L = window.DesignLayout || {}; const $ = sel => document.querySelector(sel); const STORE = 'mosaic-wui-mockup-v1'; const modes = ['light', 'dim', 'dark']; const systemMode = () => matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; const ctx = { theme: {palette: 'harbor', mode: systemMode(), chosen: false}, demoState: 'normal', route: '#/', path: '/', params: {}, local: {observing: new Set(), queue: {}, handled: new Set(), collapsed: false}, design: L.id || 'shared', compose: L, }; let storageOk = true; try { const saved = JSON.parse(localStorage.getItem(STORE) || '{}'); if (B.palettes.some(p => p.id === saved.palette)) ctx.theme.palette = saved.palette; if (modes.includes(saved.mode)) { ctx.theme.mode = saved.mode; ctx.theme.chosen = true; } if (['normal', 'loading', 'empty', 'error'].includes(saved.demoState)) ctx.demoState = saved.demoState; if (typeof saved.collapsed === 'boolean') ctx.local.collapsed = saved.collapsed; } catch { storageOk = false; } // URL overrides let the index page and the verifier open a design in a given palette, mode, or state. const q = new URLSearchParams(location.search); if (B.palettes.some(p => p.id === q.get('palette'))) ctx.theme.palette = q.get('palette'); if (modes.includes(q.get('mode'))) { ctx.theme.mode = q.get('mode'); ctx.theme.chosen = true; } if (['normal', 'loading', 'empty', 'error'].includes(q.get('state'))) ctx.demoState = q.get('state'); function persist() { if (!storageOk) return; try { localStorage.setItem(STORE, JSON.stringify({palette: ctx.theme.palette, mode: ctx.theme.chosen ? ctx.theme.mode : undefined, demoState: ctx.demoState, collapsed: ctx.local.collapsed})); } catch { storageOk = false; } } function applyTheme() { const p = B.palettes.find(x => x.id === ctx.theme.palette); const t = B.tokens(p, ctx.theme.mode); for (const [k, v] of Object.entries(t)) document.documentElement.style.setProperty('--' + k, v); document.documentElement.dataset.palette = p.id; document.documentElement.dataset.mode = ctx.theme.mode; document.documentElement.style.colorScheme = ctx.theme.mode === 'light' ? 'light' : 'dark'; document.querySelectorAll('[data-theme="palette"]').forEach(el => { el.value = p.id; }); document.querySelectorAll('[data-theme="mode"]').forEach(el => { el.checked = el.value === ctx.theme.mode; }); document.querySelectorAll('[data-theme-summary]').forEach(el => { el.textContent = `${p.name} · ${ctx.theme.mode[0].toUpperCase() + ctx.theme.mode.slice(1)}`; }); } matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { if (!ctx.theme.chosen) { ctx.theme.mode = systemMode(); applyTheme(); } }); function announce(text) { const live = $('#announce'); if (live) { live.textContent = ''; setTimeout(() => { live.textContent = text; }, 30); } } let toastTimer; function toast(text, tone = 'info') { const host = $('#toasts'); if (!host) return; const el = document.createElement('div'); el.className = `toast toast-${tone}`; el.setAttribute('role', 'status'); el.innerHTML = `${V.esc(text)}`; host.appendChild(el); el.querySelector('.toast-close').addEventListener('click', () => el.remove()); clearTimeout(toastTimer); toastTimer = setTimeout(() => el.remove(), 9000); announce(text); } // Router const ROUTES = [ [/^\/$/, 'home', 'Control board'], [/^\/sessions$/, 'sessions', 'Sessions'], [/^\/sessions\/([^/]+)$/, 'session', 'Session'], [/^\/projects$/, 'projects', 'Projects'], [/^\/projects\/([^/]+)$/, 'project', 'Project'], [/^\/agents$/, 'agents', 'Agents'], [/^\/work$/, 'work', 'Work'], [/^\/runs$/, 'runs', 'Runs and evidence'], [/^\/messages$/, 'messages', 'Messages'], [/^\/library$/, 'library', 'Library'], [/^\/settings$/, 'settings', 'Settings'], [/^\/launch$/, 'launch', 'Launch'], ]; function parse() { const raw = location.hash || '#/'; const [path, qs] = raw.slice(1).split('?'); ctx.route = raw; ctx.path = path || '/'; ctx.params = Object.fromEntries(new URLSearchParams(qs || '')); for (const [re, name, title] of ROUTES) { const m = ctx.path.match(re); if (m) { if (m[1]) ctx.params.id = decodeURIComponent(m[1]); return {name, title}; } } return {name: 'notFound', title: 'Page not found'}; } let firstRender = true; function render() { const {name, title} = parse(); ctx.view = name; const main = $('#main'); if (!main) return; const html = name === 'home' && L.home ? L.home(ctx, V) : V.views[name](ctx); main.innerHTML = html; document.title = `${title} · Mosaic Stack (${L.name || 'mockup'})`; document.querySelectorAll('[data-nav]').forEach(a => { const target = a.getAttribute('href').slice(1).split('?')[0]; const current = target === '/' ? ctx.path === '/' : ctx.path.startsWith(target); if (current) a.setAttribute('aria-current', 'page'); else a.removeAttribute('aria-current'); }); if (L.afterRender) L.afterRender(ctx, V); if (!firstRender) { main.focus({preventScroll: false}); window.scrollTo(0, 0); announce(`${title} loaded.`); } firstRender = false; wireForms(main); document.documentElement.dataset.route = name; } window.addEventListener('hashchange', render); // Simulated actions const simulate = (what, tone = 'info') => toast(`Simulated: ${what}. No live effect; this is a design mockup.`, tone); const actions = { retry: () => { ctx.demoState = 'normal'; persist(); render(); toast('Reloaded. This is a demonstration state change, not a request.', 'ok'); }, stop: id => simulate(`stop requested for session ${id}. The service would finish the current tool call and record the stop`), 'config-check': id => simulate(`configuration check on ${id}. Result would be matching, changed, or unknown, and never blocks a running session`), observe: id => { ctx.local.observing.add(id); render(); toast('Simulated: you are observing this session. Observers cannot send control input.', 'ok'); }, control: id => simulate(`control transfer requested on ${id}. The current controller would be released and the generation would increment`), handled: id => { ctx.local.handled.add(id); simulate(`marked ${id} handled. Waiting items never expire on their own`, 'ok'); }, 'goal-stop': id => simulate(`/goal stop on ${id}. The goal stays visible as stopped until cleared`), 'goal-clear': id => simulate(`/goal clear on ${id}`), 'queue-after': id => simulate(`message queued after the current turn on ${id}`), cancel: (id, q) => { const list = ctx.local.queue[id] || [{id: 'q1', text: 'Please summarize what changed since yesterday.', state: 'started'}, {id: 'q2', text: 'Then list any open questions for me.', state: 'queued'}]; ctx.local.queue[id] = list.filter(x => x.id !== q); render(); toast(`Simulated: ${q === 'q1' ? 'interrupt requested; the running tool call may finish first' : 'queued item removed immediately'}.`, 'ok'); }, 'new-project': () => simulate('new project. The create path is a backend gap listed in FEATURES.md section 12'), 'new-agent': () => simulate('new agent definition (one canonical SOUL, registrations added separately)'), 'edit-soul': id => simulate(`open SOUL for ${id}. Edits require a reviewed commit`), retire: id => simulate(`workspace retire ${id}. Refuses while active sessions exist`), reopen: id => simulate(`workspace reopen ${id}`), assign: id => simulate(`assign ${id} to an agent within the mission's ceiling`), move: id => simulate(`move ${id}. Column changes are records, not drag state`), accept: id => simulate(`accept ${id} with evidence reference`, 'ok'), 'show-run': id => simulate(`open run record ${id} (write-once; result.json and stderr.txt)`), activate: id => simulate(`release activate ${id}. Appends to activation-log.jsonl`), rollback: id => simulate(`release rollback to ${id}`), deactivate: id => simulate(`deactivate skill ${id}`), 'activate-skill': id => simulate(`activate skill ${id}`), uninstall: id => simulate(`uninstall ${id}`), 'add-account': () => simulate('add account. The credential is written by the auth shim to a runtime path and never displayed'), 'auth-status': () => simulate('auth status refresh (pi auth check driver)'), reconfigure: () => simulate('reopen setup wizard'), 'add-user': () => simulate('add user (multi-user is proposed, not built)'), }; function wireForms(root) { root.querySelectorAll('form[data-form]').forEach(form => { form.addEventListener('submit', e => { e.preventDefault(); const out = form.querySelector('.form-result'); const kind = form.dataset.form; const data = Object.fromEntries(new FormData(form)); if (kind === 'launch' || kind === 'quick-launch') { const clash = D.sessions.find(s => s.agent === data.agent && s.project === data.project && s.workspace === data.workspace && !['settled', 'stopped'].includes(s.state)); const ws = V.projectOf(data.project).workspaces.find(w => w.id === data.workspace); if (ws && ws.status === 'retired') { out.innerHTML = `Refused: workspace ${V.esc(ws.name)} is retired. Reopen it from the project page or pick another workspace. Nothing was launched.`; announce('Launch refused: workspace retired.'); return; } if (clash && data.mode !== 'fresh') { out.innerHTML = `Already running: ${V.esc(V.agentOf(clash.agent).name)} in ${V.esc(clash.project)}/${V.esc(clash.workspace)} (${V.esc(clash.controller)}, gen ${clash.generation}). Connect and observe or . Nothing was launched twice.`; announce('Launch conflict: session already running.'); return; } out.innerHTML = `Simulated launch of ${V.esc(V.agentOf(data.agent).name)} in ${V.esc(data.project)}/${V.esc(data.workspace)} (${V.esc(data.mode || 'resume')}${data.work ? ', work ' + V.esc(data.work) : ''}). In the real service you would land in the session view.`; announce('Simulated launch recorded in this page only.'); return; } if (kind === 'search') { const term = (data.q || '').trim(); const hits = term ? [...D.sessions.filter(s => (V.agentOf(s.agent).name + s.project + s.workspace + s.goal.text).toLowerCase().includes(term.toLowerCase())).map(s => `Session: ${V.agentOf(s.agent).name} in ${s.project}/${s.workspace}`), ...D.projects.filter(p => p.name.toLowerCase().includes(term.toLowerCase())).map(p => `Project: ${p.name}`)] : []; toast(term ? (hits.length ? `Simulated search: ${hits.length} match${hits.length === 1 ? '' : 'es'}. First: ${hits[0]}.` : `Simulated search: nothing matched "${term}".`) : 'Type something to search.'); return; } const ta = form.querySelector('textarea'); if (ta && !ta.value.trim()) { out.innerHTML = 'Write a message first. Nothing was sent.'; ta.focus(); return; } out.innerHTML = `Simulated send of ${ta.value.trim().length} characters. Your text is kept below until the real service confirms delivery.`; announce('Simulated send; text preserved.'); }); // Fresh-only fieldset toggle and project → workspace dependency form.querySelectorAll('[name=mode]').forEach(r => r.addEventListener('change', () => { const fs = form.querySelector('[data-fresh-only]'); if (fs) fs.hidden = form.querySelector('[name=mode]:checked').value !== 'fresh'; })); const proj = form.querySelector('[name=project]'), wsSel = form.querySelector('[name=workspace]'); if (proj && wsSel) proj.addEventListener('change', () => { wsSel.innerHTML = V.projectOf(proj.value).workspaces.map(w => ``).join(''); }); }); } document.addEventListener('click', e => { const b = e.target.closest('[data-action]'); if (!b || b.disabled) return; const [name, ...args] = b.dataset.action.split(':'); if (actions[name]) actions[name](...args); else simulate(name); }); document.addEventListener('change', e => { const el = e.target; if (el.dataset.theme === 'palette') { ctx.theme.palette = el.value; applyTheme(); persist(); announce(`Palette ${B.palettes.find(p => p.id === el.value).name}.`); } if (el.dataset.theme === 'mode') { ctx.theme.mode = el.value; ctx.theme.chosen = true; applyTheme(); persist(); announce(`${el.value} mode.`); } if (el.hasAttribute('data-demo')) { ctx.demoState = el.value; persist(); render(); toast(`Demonstration state: ${el.value}. Lists now render that state; nothing was requested.`); } }); document.addEventListener('keydown', e => { if (e.key === 'Escape') { document.querySelectorAll('[data-overlay][open], dialog[open]').forEach(el => { if (el.close) el.close(); else el.removeAttribute('open'); }); $('#toasts')?.replaceChildren(); } if (L.keydown) L.keydown(e, ctx, V, {render, toast, announce}); }); // Public surface for layouts and the verifier window.MosaicApp = {ctx, render, toast, announce, applyTheme, persist, setTheme(p, m) { if (p) ctx.theme.palette = p; if (m) { ctx.theme.mode = m; ctx.theme.chosen = true; } applyTheme(); persist(); }, setState(s) { ctx.demoState = s; persist(); render(); }}; applyTheme(); if (L.shell) $('#shell').innerHTML = L.shell(ctx, V); document.querySelectorAll('[data-icon]').forEach(el => { el.innerHTML = B.icon(el.dataset.icon); }); render(); if (!storageOk) toast('Browser storage is unavailable. Appearance still works but will not be remembered.', 'danger'); })();