/* node:coverage disable */ (function (root, factory) { 'use strict'; const contract = typeof module === 'object' && module.exports ? require('./wave_b_contract.js') : root.HermesHuxWaveBContract; const api = factory(contract); if (typeof module === 'object' && module.exports) module.exports = api; else root.HermesHuxWaveBProjectsModes = api; }(typeof globalThis === 'object' ? globalThis : this, function (contract) { /* node:coverage enable */ 'use strict'; function el(doc, tag, attrs, value) { const node = doc.createElement(tag); Object.entries(attrs || {}).forEach(([key, item]) => node.setAttribute(key, item)); if (value !== undefined) node.textContent = String(value); return node; } function status(doc, message, alert) { return el(doc, 'p', {class: 'hux-wave-b__status', role: alert ? 'alert' : 'status', 'aria-live': 'polite'}, message); } function heading(doc, title, description) { const head = el(doc, 'header', {class: 'hux-wave-b__heading'}); head.appendChild(el(doc, 'h3', {}, title)); head.appendChild(el(doc, 'p', {}, description)); return head; } function exactEtag(result, revision) { if (String(result.etag || '').replaceAll('"', '') !== String(revision)) { throw new contract.WaveBContractError('ETag does not match the record revision'); } } function key(action, target) { return `webui:${action}:${target}`.slice(0, 120); } function object(value) { return value && typeof value === 'object' && !Array.isArray(value) ? value : null; } function modeContract(raw) { if (!object(raw) || raw.schema !== 'hux.mode.v1' || !contract.MODE_IDS.has(raw.mode) || typeof raw.label !== 'string' || !raw.label || raw.label.length > 40 || typeof raw.intent !== 'string' || !raw.intent || raw.intent.length > 400 || !object(raw.constraints) || !object(raw.switchyard) || typeof raw.switchyard.route_id !== 'string' || !/^atlas\/(auto|manual|fallback|worker)\/[a-z0-9/-]+$/.test(raw.switchyard.route_id) || (raw.mode !== 'private' && raw.switchyard.route_id.startsWith('atlas/manual/'))) { throw new contract.WaveBContractError('Invalid friendly mode contract'); } if (/[\u0000-\u001f\u007f]/.test(raw.label + raw.intent)) { throw new contract.WaveBContractError('Friendly mode text is invalid'); } return Object.freeze({id: raw.mode, label: raw.label, description: raw.intent}); } function normalizeModes(raw) { if (!object(raw) || !Array.isArray(raw.items) || raw.items.length !== contract.MODES.length || raw.next !== null) { throw new contract.WaveBContractError('Invalid friendly mode page'); } const modes = raw.items.map(modeContract); if (new Set(modes.map((mode) => mode.id)).size !== contract.MODES.length) { throw new contract.WaveBContractError('Friendly modes are incomplete'); } return Object.freeze(modes); } function normalizeSelection(raw, identity, projectId, conversationId) { if (!object(raw) || raw.schema !== 'hux.mode_selection.v1' || !contract.isId(raw.id, 'mode') || raw.owner !== identity.userRef || raw.project_id !== projectId || raw.conversation_id !== conversationId || !Number.isSafeInteger(raw.revision) || raw.revision < 1) { throw new contract.WaveBContractError('Mode selection crossed its scope'); } return Object.freeze({id: raw.id, mode: modeContract(raw.mode), revision: raw.revision}); } function createProjectsExtension(options) { const settings = options || {}; const doc = settings.document; const projectId = settings.projectId; const conversationId = settings.conversationId; const branchPoint = settings.branchPointMessageId; if (!doc || !contract.MESSAGE.test(branchPoint || 'message-default') || !contract.isId(projectId, 'prj') || !contract.isId(conversationId, 'conv')) { throw new TypeError('Projects runtime context is invalid'); } function render(context) { const root = el(doc, 'div', {class: 'hux-wave-b hux-wave-b--projects'}); context.container.replaceChildren(root); let project = null; let conversations = []; let lineage = null; let generation = 0; async function mutate(path, method, body, revision, idempotencyKey) { const headers = {}; if (revision !== null) headers['If-Match'] = String(revision); if (idempotencyKey) headers['Idempotency-Key'] = idempotencyKey; await context.client.request(path, {method, headers, body}); await load(); } function conversationCard(item) { const card = el(doc, 'article', {class: 'hux-wave-b__card'}); const title = el(doc, 'h5', {}, item.title); if (item.id === conversationId) title.appendChild(el(doc, 'span', {class: 'hux-wave-b__current'}, ' Current')); card.appendChild(title); card.appendChild(el(doc, 'p', {}, `${item.mode || 'automatic'} · ${item.artifactIds.length} artifacts`)); const tags = el(doc, 'ul', {class: 'hux-wave-b__tags', 'aria-label': 'Conversation tags'}); item.tags.forEach((tag) => tags.appendChild(el(doc, 'li', {}, tag))); card.appendChild(tags); const pin = el(doc, 'button', {type: 'button'}, item.pinned ? 'Unpin conversation' : 'Pin conversation'); pin.addEventListener('click', () => mutate(`/conversations/${item.id}`, 'PATCH', {pinned: !item.pinned}, item.revision).catch(showError)); card.appendChild(pin); return card; } function showError() { root.replaceChildren(heading(doc, 'Projects and conversations', 'Organize the current project without crossing its tenant boundary.'), status(doc, 'This project workspace is temporarily unavailable.', true)); } function draw() { root.replaceChildren(heading(doc, 'Projects and conversations', 'Organize, search, pin, and inspect branches inside the current project.')); const projectCard = el(doc, 'section', {class: 'hux-wave-b__card', 'aria-label': 'Current project'}); projectCard.appendChild(el(doc, 'h4', {}, project.name)); projectCard.appendChild(el(doc, 'p', {}, project.description || 'No project description.')); const pin = el(doc, 'button', {type: 'button'}, project.pinned ? 'Unpin project' : 'Pin project'); pin.addEventListener('click', () => mutate(`/projects/${project.id}`, 'PATCH', {pinned: !project.pinned}, project.revision).catch(showError)); projectCard.appendChild(pin); root.appendChild(projectCard); const search = el(doc, 'form', {class: 'hux-wave-b__form', role: 'search'}); const label = el(doc, 'label', {}, 'Search this project'); const input = el(doc, 'input', {name: 'q', type: 'search', maxlength: '200'}); const submit = el(doc, 'button', {type: 'submit'}, 'Search'); label.appendChild(input); search.appendChild(label); search.appendChild(submit); search.addEventListener('submit', async (event) => { event.preventDefault(); const query = input.value.trim(); if (!query) return; submit.disabled = true; try { const result = await context.client.request(`/search?q=${encodeURIComponent(query)}&project_id=${projectId}`); conversations = contract.normalizeSearch(result.body, context.client.identity, projectId).items; draw(); } catch (_) { showError(); } }); root.appendChild(search); root.appendChild(el(doc, 'p', {class: 'hux-wave-b__gap'}, 'Search covers titles, tags, project name, and artifact titles. Message text is not indexed by this backend.')); const list = el(doc, 'section', {'aria-label': 'Conversations'}); list.appendChild(el(doc, 'h4', {}, 'Conversations')); if (!conversations.length) list.appendChild(status(doc, 'No matching conversations in this project.')); conversations.forEach((item) => list.appendChild(conversationCard(item))); root.appendChild(list); const tree = el(doc, 'section', {class: 'hux-wave-b__lineage', 'aria-label': 'Conversation branches'}); tree.appendChild(el(doc, 'h4', {}, 'Branch lineage')); const chain = [...lineage.ancestors, lineage.conversation]; const path = el(doc, 'ol'); chain.forEach((item) => path.appendChild(el(doc, 'li', {}, item.title))); tree.appendChild(path); const children = el(doc, 'ul', {'aria-label': 'Direct child branches'}); lineage.children.forEach((item) => children.appendChild(el(doc, 'li', {}, item.title))); if (!lineage.children.length) children.appendChild(el(doc, 'li', {}, 'No direct child branches.')); tree.appendChild(children); if (branchPoint) { const branch = el(doc, 'button', {type: 'button'}, 'Branch from current message'); branch.addEventListener('click', () => mutate(`/conversations/${conversationId}/branch`, 'POST', {branch_point_message_id: branchPoint}, null, key('branch', `${conversationId}:${branchPoint}`)) .catch(showError)); tree.appendChild(branch); } else { tree.appendChild(el(doc, 'p', {class: 'hux-wave-b__gap'}, 'Choose a message before creating a branch; the backend requires an exact branch point.')); } root.appendChild(tree); } async function load() { const current = ++generation; root.replaceChildren(heading(doc, 'Projects and conversations', 'Organize the current project without crossing its tenant boundary.'), status(doc, 'Loading project…')); try { const [projectResult, conversationsResult, lineageResult] = await Promise.all([ context.client.request(`/projects/${projectId}`), context.client.request(`/conversations?project_id=${projectId}`), context.client.request(`/conversations/${conversationId}/lineage`), ]); if (current !== generation) return; project = contract.normalizeProject(projectResult.body, context.client.identity, projectId); exactEtag(projectResult, project.revision); conversations = contract.normalizeConversationPage(conversationsResult.body, context.client.identity, projectId); lineage = contract.normalizeLineage(lineageResult.body, context.client.identity, projectId, conversationId); draw(); } catch (_) { if (current === generation) showError(); } } load(); } return Object.freeze({id: 'projects', flag: 'hux.projects', label: 'Projects', order: 30, render}); } function createModesExtension(options) { const settings = options || {}; const doc = settings.document; const projectId = settings.projectId; const conversationId = settings.conversationId; if (!doc || !contract.isId(projectId, 'prj') || !contract.isId(conversationId, 'conv')) { throw new TypeError('Modes runtime context is invalid'); } function render(context) { const root = el(doc, 'div', {class: 'hux-wave-b hux-wave-b--modes'}); context.container.replaceChildren(root); let selection = null; let modes = []; function draw(message) { root.replaceChildren(heading(doc, 'Conversation mode', 'Choose an intent; Switchyard remains responsible for the provider and exact route.')); root.appendChild(el(doc, 'p', {class: 'hux-wave-b__gap'}, 'Automatic choices express intent; Switchyard selects from the available provider pool.')); if (message) root.appendChild(status(doc, message)); const choices = el(doc, 'div', {class: 'hux-wave-b__choices', role: 'radiogroup', 'aria-label': 'Friendly modes'}); modes.forEach((mode) => { const button = el(doc, 'button', {type: 'button', role: 'radio', 'aria-checked': selection && selection.mode.id === mode.id ? 'true' : 'false'}, mode.label); button.appendChild(el(doc, 'span', {}, mode.description)); button.addEventListener('click', async () => { button.disabled = true; try { const path = `/projects/${projectId}/conversations/${conversationId}/mode`; const result = await context.client.request(path, {method: 'PUT', headers: { 'If-Match': String(selection ? selection.revision : 0), 'Idempotency-Key': key('mode', `${conversationId}:${mode.id}`)}, body: {project_id: projectId, mode: mode.id, advanced: false}}); selection = normalizeSelection(result.body, context.client.identity, projectId, conversationId); exactEtag(result, selection.revision); draw(`${mode.label} mode selected.`); } catch (_) { draw('The mode could not be changed. Refresh and try again.'); } }); choices.appendChild(button); }); root.appendChild(choices); } draw('Loading friendly modes…'); context.client.request('/modes').then((result) => { modes = normalizeModes(result.body); return context.client.request(`/projects/${projectId}/conversations/${conversationId}/mode`) .then((current) => { selection = normalizeSelection(current.body, context.client.identity, projectId, conversationId); exactEtag(current, selection.revision); }) .catch(() => { selection = null; }); }).then(() => draw(selection ? 'Current mode loaded.' : 'Choose a mode for this conversation.')) .catch(() => draw('The friendly mode catalog could not be verified.')); } return Object.freeze({id: 'friendly-modes', flag: 'hux.friendly_modes', label: 'Mode', order: 60, render}); } return Object.freeze({createModesExtension, createProjectsExtension, exactEtag, key, modeContract, normalizeModes, normalizeSelection}); }));