import { html, nothing } from 'lit'; import { LightElement } from '../../lib/base.js'; /** * Mobile project list. Lists projects from `GET /api/projects`; tapping one opens * (or resumes) its coordinator session via `POST /api/projects/{id}/session` and * emits a `project-open` event so the shell can re-point the chat to that source. * * The session machinery is shared with the desktop copilot: the same `project-{id}` * source, the same provisioning (`project-coordinator` + project RunContext), so a * project chat is continuous across desktop, mobile browser, and the native shell. */ export class ProjectsPage extends LightElement { static properties = { visible: { type: Boolean }, _data: { state: true }, _error: { state: true }, _loading: { state: true }, }; constructor() { super(); this.visible = false; this._data = null; this._error = null; this._loading = false; } updated(changed) { if (changed.has('visible') && this.visible) this._load(); } async _load() { this._loading = true; try { const res = await fetch('/api/projects'); if (!res.ok) throw new Error(`HTTP ${res.status}`); this._data = await res.json(); this._error = null; } catch (e) { this._error = e.message; } finally { this._loading = false; } } async _open(project) { try { const res = await fetch(`/api/projects/${project.id}/session`, { method: 'POST' }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const { source } = await res.json(); this.dispatchEvent(new CustomEvent('project-open', { bubbles: true, composed: true, detail: { source, label: project.name }, })); } catch (e) { this._error = e.message; } } render() { if (!this.visible) return nothing; const projects = this._data ?? []; return html`
Projects
${this._error ? html`
${this._error}
` : nothing} ${projects.length === 0 ? html`

${this._loading ? 'Loading…' : 'No projects yet'}

` : html`
${projects.map(p => html`
this._open(p)}>
${p.name}
${p.description ? html`
${p.description}
` : nothing}
`)}
`}
`; } } customElements.define('projects-page', ProjectsPage);