// Route views and building blocks shared by the five mockups. Each design composes these differently. (() => { 'use strict'; const D = window.MosaicData, B = window.Brand; const esc = s => String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); const icon = (id, label) => `${label ? `${esc(label)}` : ''}`; const ago = iso => { const ms = D.now - new Date(iso); const m = Math.round(ms / 60000); if (m < 1) return 'just now'; if (m < 60) return `${m} min ago`; const h = Math.floor(m / 60); if (h < 24) return `${h} h ${m % 60} min ago`; const d = Math.floor(h / 24); return `${d} d ${h % 24} h ago`; }; const dur = iso => { const m = Math.round((D.now - new Date(iso)) / 60000); const h = Math.floor(m / 60); return h ? `${h} h ${m % 60} min` : `${m} min`; }; const stamp = iso => iso.replace('T', ' ').replace(':00Z', ' UTC'); const STATES = {running:['Running','ok'], waiting:['Waiting on you','attn'], quiet:['Quiet wait','muted'], blocked:['Blocked','attn'], stalled:['Stalled','danger'], settled:['Settled','ok'], stopped:['Stopped','muted']}; const badge = state => { const [label, tone] = STATES[state] || [state, 'muted']; return `${label}`; }; const feat = status => `${status}`; const agentOf = id => D.agents.find(a => a.id === id) || {id, name: id, type: ''}; const projectOf = id => D.projects.find(p => p.id === id) || {id, name: id, workspaces: []}; const sessionOf = id => D.sessions.find(s => s.id === id); const scope = s => `${esc(agentOf(s.agent).name)} in ${esc(s.project)}/${esc(s.workspace)}`; const avatar = id => ``; const link = (href, text, cls = '') => `${text}`; const btn = (label, action, opts = {}) => ``; const actionLink = ([label, target], cls = 'btn') => target.startsWith('action:') ? btn(esc(label), target.slice(7), {cls}) : link(target, esc(label), cls); const kindIcon = {acceptance:'check', blocked:'warning', decision:'board', stalled:'error', config:'extensions', 'quiet':'dim'}; // Demo state wrapper: loading, empty, error, or the real content. function stateWrap(ctx, what, content, opts = {}) { const s = ctx.demoState; if (s === 'loading') return `

Loading ${esc(what)}. Nothing is being requested; this is a demonstration state.

${opts.skeleton ? `` : ''}
`; if (s === 'empty') return `

No ${esc(what)} yet.

${esc(opts.emptyHelp || 'When there is something here you will see it listed with its agent, project, and workspace.')}

${opts.emptyAction ? actionLink(opts.emptyAction, 'btn primary') : ''}
`; if (s === 'error') return ``; return content; } const legend = () => `
Legend${['implemented','planned','proposed','mockup'].map(feat).join('')}FEATURES.md
`; // Building blocks function waitingList(ctx, variant = 'list') { const items = D.waiting.map(w => { const s = sessionOf(w.session); return `
  • ${icon(kindIcon[w.kind] || 'conversation')}

    ${esc(w.title)}

    ${ago(w.since)}

    ${scope(s)} ${badge(s.state)}

    ${esc(w.detail)}

    ${w.actions.map((a, i) => actionLink(a, i === 0 ? 'btn primary' : 'btn')).join('')}
  • `; }).join(''); return stateWrap(ctx, 'items waiting on you', `
      ${items}
    `, {skeleton: 3, emptyHelp: 'Nothing is waiting on you. Sessions that need a decision, an acceptance, or a check-in appear here first.'}); } function sessionsTable(ctx, list = D.sessions, opts = {}) { const rows = list.map(s => ` ${avatar(s.agent)}${esc(agentOf(s.agent).name)} ${esc(s.project)}/${esc(s.workspace)} ${badge(s.state)} ${esc(s.goal.text)}${esc(s.goal.report)} ${esc(s.controller)}gen ${s.generation} ${opts.compact ? '' : `${{matching:'Matches', changed:'Changed', unknown:'Unknown'}[s.config]}`} `).join(''); return stateWrap(ctx, 'sessions', `
    ${opts.compact ? '' : ''}${rows}
    Agent sessions across projects
    AgentProject / workspaceStatusGoalControllerSinceConfig
    `, {skeleton: 5, emptyAction: ['Launch an agent', '#/launch']}); } function sessionCard(s) { return `
    ${avatar(s.agent)}

    ${esc(agentOf(s.agent).name)}

    ${esc(s.project)}/${esc(s.workspace)}

    ${badge(s.state)}

    ${esc(s.goal.text)}

    ${esc(s.goal.report)}

    `; } function sessionCards(ctx, list = D.sessions) { return stateWrap(ctx, 'sessions', `
    ${list.map(sessionCard).join('')}
    `, {skeleton: 6, emptyAction: ['Launch an agent', '#/launch']}); } function sessionColumns(ctx, groups) { const cols = groups.map(([title, states, tone]) => { const list = D.sessions.filter(s => states.includes(s.state)); return `

    ${esc(title)} ${list.length}

    ${list.length ? list.map(sessionCard).join('') : '

    None

    '}
    `; }).join(''); return stateWrap(ctx, 'sessions', `
    ${cols}
    `, {skeleton: 4, emptyAction: ['Launch an agent', '#/launch']}); } function eventStream(ctx, limit = 12) { const items = D.events.slice(0, limit).map(e => { const s = e.session && sessionOf(e.session); return `
  • ${icon({start:'plus', turn:'conversation', tool:'extensions', waiting:'warning', quiet:'dim', config:'extensions', blocked:'warning', stalled:'error', settled:'check', release:'projects', stop:'error'}[e.kind] || 'conversation')}

    ${s ? `${esc(e.text)}` : esc(e.text)}

  • `; }).join(''); return stateWrap(ctx, 'activity', `
      ${items}
    `, {skeleton: 6}); } function quickLaunch() { return `

    Launch

    Resume is the default. First use creates the conversation and says so.

    ${link('#/launch', 'More options', 'btn')}
    `; } const running = () => D.sessions.filter(s => ['running','waiting','quiet','blocked','stalled'].includes(s.state)); const recent = () => D.sessions.filter(s => ['settled','stopped'].includes(s.state)); // Route views const views = { home(ctx) { if (ctx.compose?.home) return ctx.compose.home(ctx, api); return `

    Control board

    ${running().length} sessions active across ${D.projects.length} projects. ${D.waiting.length} waiting on you. ${feat('proposed')}

    Waiting on you ${D.waiting.length}

    ${waitingList(ctx)}

    Running now ${running().length}

    ${sessionsTable(ctx, running())}

    Recently settled

    ${sessionsTable(ctx, recent(), {compact: true})}
    `; }, sessions(ctx) { const f = ctx.params.filter || 'all'; const list = f === 'all' ? D.sessions : D.sessions.filter(s => s.state === f || s.project === f); const chips = [['all','All'], ...Object.entries(STATES).map(([k, [l]]) => [k, l]), ...D.projects.map(p => [p.id, p.name])]; return `

    Sessions

    Every conversation is bound to one agent, project, and workspace. ${feat('planned')}

    ${sessionsTable(ctx, list)}

    ${list.length} of ${D.sessions.length} sessions shown. Legacy sessions are not listed until adopted by a reviewed assignment.

    `; }, session(ctx) { const s = sessionOf(ctx.params.id); if (!s) return views.notFound(ctx); const t = D.transcript[s.id] || D.defaultTranscript; const youControl = s.controller.startsWith('you'); const observing = s.observers.includes('you') || ctx.local.observing.has(s.id); const queue = ctx.local.queue[s.id] || [{id: 'q1', text: 'Please summarize what changed since yesterday.', state: 'started'}, {id: 'q2', text: 'Then list any open questions for me.', state: 'queued'}]; const w = D.waiting.find(x => x.session === s.id); return `
    ${avatar(s.agent)}

    ${esc(agentOf(s.agent).name)} in ${esc(s.project)}/${esc(s.workspace)}

    ${badge(s.state)} · started ${stamp(s.started)} · ${s.turns} turns · session ${esc(s.id)}

    ${btn(icon('people', observing ? 'Observing' : 'Observe'), 'observe:' + s.id, {disabled: observing || youControl, attrs: `aria-pressed="${observing}"`})}${btn(icon('arrow', youControl ? 'You control' : 'Request control'), 'control:' + s.id, {disabled: youControl})}${btn(icon('error', 'Stop'), 'stop:' + s.id, {disabled: ['settled','stopped'].includes(s.state)})}${link('#/launch?agent=' + s.agent + '&project=' + s.project + '&workspace=' + s.workspace + '&mode=fresh', icon('plus', 'Fresh'), 'btn')}
    ${w ? `

    Waiting on you: ${esc(w.title)}

    ${esc(w.detail)}

    ${w.actions.map((a, i) => actionLink(a, i === 0 ? 'btn primary' : 'btn')).join('')}${btn('Mark handled (simulated)', 'handled:' + w.id)}
    ` : ''}

    Conversation

    ${stateWrap(ctx, 'transcript', `
      ${t.map(m => `
    1. ${esc(m.who === 'agent' ? agentOf(s.agent).name : m.who)}

      ${esc(m.text)}

      ${m.tool ? `

      ${icon('extensions')}${esc(m.tool)}

      ` : ''}
    2. `).join('')}
    `, {skeleton: 3})}
    `; }, projects(ctx) { return `

    Projects

    A project registers agents and owns its workspaces. Membership in a project does not open every workspace. ${feat('planned')}

    ${btn(icon('plus', 'New project'), 'new-project', {cls: 'primary'})}
    ${stateWrap(ctx, 'projects', `
    ${D.projects.map(p => `

    ${esc(p.name)}

    ${esc(p.status)}

    Workspaces

    Agents

    `).join('')}
    `, {skeleton: 3, emptyAction: ['Create a project', 'action:new-project']})}`; }, project(ctx) { const p = D.projects.find(x => x.id === ctx.params.id); if (!p) return views.notFound(ctx); const regs = D.agents.flatMap(a => a.registrations.filter(r => r[0] === p.id).map(r => ({agent: a, workspace: r[1], role: r[2]}))); return `

    ${esc(p.name)}

    Policy: ${esc(p.policy)} · ${p.workspaces.length} workspaces · ${p.agents.length} registered agents

    Workspaces ${feat('planned')}

    ${p.workspaces.map(w => ``).join('')}
    WorkspaceStatusActive sessionsActions
    ${esc(w.name)}${w.status === 'retired' ? 'Retired' : 'Active'}${D.sessions.filter(s => s.project === p.id && s.workspace === w.id && !['settled','stopped'].includes(s.state)).length}${w.status === 'retired' ? btn('Reopen', 'reopen:' + w.id) : btn('Retire', 'retire:' + w.id)}${link('#/launch?project=' + p.id + '&workspace=' + w.id, 'Launch', 'btn')}

    Registrations ${feat('planned')}

    ${regs.map(r => ``).join('')}
    AgentWorkspaceRoleStatus
    ${avatar(r.agent.id)}${esc(r.agent.name)}${esc(r.workspace)}${esc(r.role)}Active

    Roles are ceilings narrowed by registration. A role label never grants access to another workspace.

    Missions ${feat('implemented')}

    Decisions ${feat('planned')}

    `; }, agents(ctx) { return `

    Agents

    One reusable definition per agent. Registrations place it in projects and workspaces without creating a second identity. ${feat('planned')}

    ${btn(icon('plus', 'New agent'), 'new-agent', {cls: 'primary'})}
    ${stateWrap(ctx, 'agents', `
    ${D.agents.map(a => `
    ${avatar(a.id)}

    ${esc(a.name)}

    ${esc(a.type)} · ${esc(a.harness)} · ${esc(a.model)}

    SOUL
    ${esc(a.soul)} (one canonical file)
    Skills
    ${a.skills.join(', ') || 'none'}
    Registered
      ${a.registrations.map(r => `
    • ${esc(r[0])}/${esc(r[1])} ${esc(r[2])}
    • `).join('')}
    Sessions
    ${D.sessions.filter(s => s.agent === a.id && !['settled','stopped'].includes(s.state)).length} active
    `).join('')}
    `, {skeleton: 4})}`; }, work(ctx) { const f = ctx.params.project || 'all'; const list = D.tasks.filter(t => f === 'all' || (t.mission ? D.missions.find(m => m.id === t.mission)?.project === f : t.project === f)); const cols = [['backlog','Backlog'],['ready','Ready'],['doing','Doing'],['review','Review'],['waiting','Waiting on owner'],['done','Done']]; return `

    Work

    Missions govern tasks. Assignments bind a task to one agent. Kanban columns are a view of the same records, not a second list. ${feat('implemented')} ${feat('proposed')}

    ${stateWrap(ctx, 'tasks', `
    ${cols.map(([k, label]) => { const ts = list.filter(t => t.column === k); return `

    ${label} ${ts.length}

    ${ts.map(t => `
    ${esc(t.title)}${t.assignee ? avatar(t.assignee) + esc(agentOf(t.assignee).name === t.assignee ? t.assignee : agentOf(t.assignee).name) : 'unassigned'}
    Mission
    ${t.mission ? esc(D.missions.find(m => m.id === t.mission)?.title) : 'ad hoc (still needs purpose and criteria)'}
    Depends on
    ${t.deps.map(d => `${esc(d)}`).join(', ') || 'nothing'}
    Evidence
    ${esc(t.evidence) || 'none yet'}
    ${btn('Assign', 'assign:' + t.id)}${btn('Move', 'move:' + t.id)}${k === 'review' ? btn('Accept', 'accept:' + t.id, {cls: 'primary'}) : ''}
    `).join('') || '

    None

    '}
    `; }).join('')}
    `, {skeleton: 4})}`; }, runs(ctx) { return `

    Runs and evidence

    Run records are write-once. Prune leaves a receipt. Nothing here can be edited. ${feat('implemented')}

    ${stateWrap(ctx, 'runs', `
    ${D.runs.map(r => ``).join('')}
    RunTaskAgent / workspaceOutcomeDurationToolsEvidence
    ${esc(r.id)}${esc(r.task)}${esc(agentOf(r.agent).name)} · ${esc(r.workspace)}${esc(r.outcome)} (exit ${r.exit})${esc(r.duration)}${esc(r.tools)}${esc(r.evidence)} ${btn('Show', 'show-run:' + r.id)}
    `, {skeleton: 5})}

    Releases ${feat('implemented')}

    Command evidence ${feat('planned')}

    Each managed command records actor, scope, assignment, the authorized command, enforced limits, start and end, outcome, and evidence references. Internal file and network calls are not separately enumerated (owner ruling Q27 A).

    Actor
    rocko · execution s-00a7d195
    Command
    npm test (allowlisted)
    Limits
    workspace acme-ops/ops, no network
    Outcome
    exit 0, 48 s
    `; }, messages(ctx) { const sel = ctx.params.id || D.messages[0].id; const m = D.messages.find(x => x.id === sel) || D.messages[0]; return `

    Messages

    Every message addresses a workspace. ${D.messages.filter(x => x.unread).length} unread. ${feat('proposed')}

    ${esc(agentOf(m.from).name === m.from ? m.from : agentOf(m.from).name)} to ${esc(m.to)} ${esc(m.project)}/${esc(m.workspace)}

    ${esc(m.text)}

    ${link('#/sessions', 'Open session', 'btn')}

    Transport: mosaic comms (proposed). Today's transport is the tmux agent-send wrapper. Delivery never grants reassignment authority.

    `; }, library(ctx) { return `

    Library

    Skills, extensions, and packages with their lifecycle and role ceilings. ${feat('planned')}

    ${stateWrap(ctx, 'skills', `
    ${D.skills.map(s => ``).join('')}
    NameVersionStateCeilingNoteActions
    ${esc(s.id)}${esc(s.version)}${esc(s.state)}${esc(s.ceiling)}${esc(s.note)}${s.state === 'active' ? btn('Deactivate', 'deactivate:' + s.id) : btn('Activate', 'activate-skill:' + s.id)}${btn('Uninstall', 'uninstall:' + s.id, {disabled: s.ceiling === 'extension'})}
    `, {skeleton: 5})}

    Packages ${feat('planned')}

    packages/mosaic (registry schemas, validate and list), packages/config, packages/agent, packages/auth. Extensions remain the canonical source until M20 packaging is chartered.

    `; }, settings(ctx) { const palettes = B.palettes.map(p => ``).join(''); return `

    Settings

    Appearance is yours. Accounts show metadata only; credentials never appear in the interface.

    Appearance ${feat('mockup')}

    Mode
    ${['light','dim','dark'].map(m => ``).join('')}

    Follows your system preference until you choose. Stored in this browser only.

    Accounts ${feat('implemented')} ${feat('planned')}

    ${D.accounts.map(a => ``).join('')}
    AccountProviderKindStatusDefault forCredential
    ${esc(a.id)}${esc(a.provider)}${esc(a.kind)}${esc(a.status)}${a.expires ? ` until ${esc(a.expires)}` : ''}${a.defaultFor.map(x => esc(agentOf(x).name)).join(', ') || 'none'}${esc(a.credential)}
    ${btn('Add account', 'add-account')}${btn('Refresh status', 'auth-status')}

    Roles ${feat('planned')}

    Not a rank hierarchy. Role files change only through reviewed commits.

    Setup and presets ${feat('proposed')}

    Site homelab · instance main · system auth internal · break-glass admin set. Presets: Software factory Personal assistant Journal

    ${btn('Reconfigure', 'reconfigure')}${btn('Add user', 'add-user')}

    Prototype controls ${feat('mockup')}

    Changes how lists render so reviewers can see loading, empty, and error handling. Nothing is requested or saved.

    ${legend()}
    `; }, launch(ctx) { const p = ctx.params; const wsOptions = pid => (projectOf(pid).workspaces || []).map(w => ``).join(''); return `

    Launch

    Name the agent, project, and workspace. Resume reopens the existing conversation; Fresh starts a new one without importing old chat. ${feat('planned')}

    Launch form

    Conversation
    Work decision for Fresh
    ${link('#/', 'Cancel', 'btn')}

    What the service will do

    `; }, notFound(ctx) { return `

    Page not found

    Nothing lives at ${esc(ctx.route)}. Your work is unaffected.

    `; }, }; const api = {views, esc, icon, ago, dur, stamp, badge, feat, avatar, scope, link, btn, actionLink, legend, stateWrap, waitingList, sessionsTable, sessionCard, sessionCards, sessionColumns, eventStream, quickLaunch, running, recent, agentOf, projectOf, sessionOf, STATES}; window.MosaicViews = api; })();