Files
stack/agents/dewey/work/wui/mockups/shared/app.js
T
jason.woltjeandClaude Opus 5.5 1f4882d2b4 docs(wui): Dewey's design work: brand board, five mockups, slice 1 S5 views and checks
Brand board revisions, the five dashboard mockups with their verify
script and evidence, the slice 1 S5 design note (SLICE1-VIEWS.md, rev 3
plus the S2c section 10 update), the Console mockup of inbox, tasks,
agents and trail (mockups/slice1/) and checks/slice1-verify.mjs (16
checks; check 16 pinned to bus at d27042fa). Also the CHAT-03 progress
log and the 2026-09-26 dirty-tree manifest. Design and evidence only;
no package code. Row 40 (#1522), committed on its own at Sage's
direction, 2026-10-09.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-09 17:54:50 -05:00

186 lines
14 KiB
JavaScript

// 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 = `<span class="ic" aria-hidden="true">${B.icon(tone === 'danger' ? 'error' : tone === 'ok' ? 'check' : 'conversation')}</span><span>${V.esc(text)}</span><button type="button" class="toast-close" aria-label="Dismiss notice">&times;</button>`;
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 = `<span class="result result-danger">Refused: workspace ${V.esc(ws.name)} is retired. Reopen it from the project page or pick another workspace. Nothing was launched.</span>`; announce('Launch refused: workspace retired.'); return; }
if (clash && data.mode !== 'fresh') { out.innerHTML = `<span class="result result-attn">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}). <a href="#/sessions/${clash.id}">Connect and observe</a> or <button type="button" class="link" data-action="control:${clash.id}">request control</button>. Nothing was launched twice.</span>`; announce('Launch conflict: session already running.'); return; }
out.innerHTML = `<span class="result result-ok">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.</span>`;
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 = '<span class="result result-danger">Write a message first. Nothing was sent.</span>'; ta.focus(); return; }
out.innerHTML = `<span class="result result-ok">Simulated send of ${ta.value.trim().length} characters. Your text is kept below until the real service confirms delivery.</span>`;
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 => `<option value="${w.id}">${V.esc(w.name)}${w.status === 'retired' ? ' (retired)' : ''}</option>`).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');
})();