From 34ed127653f7f8316484dd30ae142cac892d7968 Mon Sep 17 00:00:00 2001 From: Thomas Nilles Date: Sun, 20 Sep 2026 17:50:32 -0400 Subject: [PATCH] Slice 12: inline command-result toasts New overlay: components post 'toast' CustomEvents ({message, kind: success|error|info}) and it shows brief, auto-dismissing, bottom-right toasts (errors linger). (git + coordination actions) and (create/merge) now post friendly success/error toasts instead of alert(); the activity feed still logs everything. Also adds the slice 11 CHANGELOG entry. Verified live: 'Checked for updates' toast shown. Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 17 ++++++++ components/pr-list/pr-list.js | 10 ++++- components/repo-menu/repo-menu.js | 25 ++++++++++- components/toast-host/toast-host.js | 67 +++++++++++++++++++++++++++++ components/toast-host/toast-host.md | 22 ++++++++++ web/templates/index.html | 2 + 6 files changed, 139 insertions(+), 4 deletions(-) create mode 100644 components/toast-host/toast-host.js create mode 100644 components/toast-host/toast-host.md diff --git a/CHANGELOG.md b/CHANGELOG.md index f3d35a5..991b61a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -243,3 +243,20 @@ Append-only running history of all changes (AGENT.md §9.1). Newest last. `cmd/server/main.go`, `components/pr-list`, `web/templates/help.html`, `AGENT.md`. - **Note:** the head branch must already exist on the remote (push first). + +## 2026-09-20 — Slice 11: branch-picker submenu +- **What:** `` "Switch branch" is now a flyout submenu populated from + `GET /api/repo` (the repo's branches; current one disabled), flipping leftward + near the viewport edge; clicking a branch checks it out. "New branch…" still + prompts. No backend change. +- **Affects:** `components/repo-menu`, `web/templates/help.html`. + +## 2026-09-20 — Slice 12: inline command-result toasts +- **What:** New `` overlay — components post `toast` CustomEvents + (`{message, kind}`; success/error/info) and it shows brief, auto-dismissing, + bottom-right toasts. `` (git ops + coordination actions) and + `` (create/merge) now post success/error toasts with friendly labels + instead of `alert()`. Activity feed still logs everything. +- **Why:** Immediate, legible feedback for the non-expert audience (§6 polish). +- **Affects:** `components/toast-host` (new), `components/repo-menu`, + `components/pr-list`, `web/templates/index.html`. diff --git a/components/pr-list/pr-list.js b/components/pr-list/pr-list.js index f89471a..b16c766 100644 --- a/components/pr-list/pr-list.js +++ b/components/pr-list/pr-list.js @@ -59,12 +59,17 @@ class PRList extends HTMLElement { }); const data = await res.json(); if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`); + this.#toast(`Merged & cleaned up PR #${pr.number}`, 'success'); this.#load(this.#path); // refresh the list } catch (err) { - this.#error(`Merge failed: ${err.message}`); + this.#toast(`Merge failed: ${err.message}`, 'error'); } } + #toast(message, kind) { + document.dispatchEvent(new CustomEvent('toast', { detail: { message, kind } })); + } + async #create() { const branch = this.#repo?.branch; if (!branch) return; @@ -78,9 +83,10 @@ class PRList extends HTMLElement { }); const data = await res.json(); if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`); + this.#toast(`Opened PR #${data.number}`, 'success'); this.#load(this.#path); // refresh so the new PR appears } catch (err) { - window.alert(`Create PR failed: ${err.message}`); + this.#toast(`Create PR failed: ${err.message}`, 'error'); } } diff --git a/components/repo-menu/repo-menu.js b/components/repo-menu/repo-menu.js index 8d468e1..8025cc2 100644 --- a/components/repo-menu/repo-menu.js +++ b/components/repo-menu/repo-menu.js @@ -135,12 +135,15 @@ class RepoMenu extends HTMLElement { } case 'active': await this.#post('/api/active-project', { path: repo.path }); + this.#toast(`${name} is now the active project`, 'info'); break; case 'handoff': await this.#post('/api/switch', { target: repo.path }); + this.#toast(`Asked Claude to switch to ${name}`, 'info'); break; case 'copy': - try { await navigator.clipboard.writeText(repo.path); } catch { /* ignore */ } + try { await navigator.clipboard.writeText(repo.path); this.#toast('Path copied', 'info'); } + catch { this.#toast('Could not copy path', 'error'); } break; } } @@ -150,6 +153,19 @@ class RepoMenu extends HTMLElement { await this.#git('checkout', { branch }); } + #okLabel(op, extra) { + switch (op) { + case 'pull': return 'Got the latest'; + case 'push': return 'Published'; + case 'fetch': return 'Checked for updates'; + case 'commit': return 'Saved your work'; + case 'checkout': return `Switched to ${extra.branch}`; + case 'create-branch': return `Created branch ${extra.branch}`; + case 'discard': return 'Discarded changes'; + default: return 'Done'; + } + } + async #git(op, extra = {}) { try { const res = await fetch('/api/repo/git', { @@ -159,11 +175,16 @@ class RepoMenu extends HTMLElement { }); const data = await res.json(); if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`); + this.#toast(this.#okLabel(op, extra), 'success'); } catch (err) { - window.alert(`${op} failed: ${err.message}`); + this.#toast(`${this.#okLabel(op, extra)} failed: ${err.message}`, 'error'); } } + #toast(message, kind) { + document.dispatchEvent(new CustomEvent('toast', { detail: { message, kind } })); + } + async #post(url, body) { try { await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); diff --git a/components/toast-host/toast-host.js b/components/toast-host/toast-host.js new file mode 100644 index 0000000..3c9eadd --- /dev/null +++ b/components/toast-host/toast-host.js @@ -0,0 +1,67 @@ +// — a singleton overlay that shows brief command-result toasts. +// +// A self-contained control (AGENT.md §1.1): shadow DOM, no data of its own. Any +// component posts a toast by dispatching a `toast` CustomEvent on document: +// document.dispatchEvent(new CustomEvent('toast', +// { detail: { message: 'Published', kind: 'success' } })); +// kind ∈ success | error | info. Toasts stack bottom-right, auto-dismiss (errors +// linger longer), and dismiss on click. + +class ToastHost extends HTMLElement { + #onToast = null; + + constructor() { + super(); + this.attachShadow({ mode: 'open' }); + } + + connectedCallback() { + this.#render(); + this.#onToast = (e) => this.#show(e.detail || {}); + document.addEventListener('toast', this.#onToast); + } + + disconnectedCallback() { + document.removeEventListener('toast', this.#onToast); + } + + #show({ message, kind = 'info', timeout }) { + if (!message) return; + const t = document.createElement('div'); + t.className = 'toast ' + (['success', 'error', 'info'].includes(kind) ? kind : 'info'); + t.textContent = String(message); + t.addEventListener('click', () => t.remove()); + this.shadowRoot.getElementById('stack').appendChild(t); + requestAnimationFrame(() => t.classList.add('in')); + const ms = timeout || (kind === 'error' ? 6000 : 3500); + setTimeout(() => { + t.classList.remove('in'); + setTimeout(() => t.remove(), 200); + }, ms); + } + + #render() { + this.shadowRoot.innerHTML = ` + +
`; + } +} + +customElements.define('toast-host', ToastHost); diff --git a/components/toast-host/toast-host.md b/components/toast-host/toast-host.md new file mode 100644 index 0000000..985034b --- /dev/null +++ b/components/toast-host/toast-host.md @@ -0,0 +1,22 @@ +# toast-host + +## Intent +A singleton overlay for brief command-result feedback (AGENT.md §6 polish). It +gives immediate, legible confirmation of what a UI action did — "Published", +"Merged & cleaned up PR #3", or an error — instead of only the activity feed or a +browser `alert()`. Any component can post to it without a reference to it. + +## Public surface +- **Tag:** `` (place once, near the end of the page). +- **Listens:** `toast` on `document` — `detail: { message, kind?, timeout? }`, + where `kind` ∈ `success | error | info` (default `info`). +- **Behavior:** toasts stack bottom-right, animate in, auto-dismiss (errors last + longer — 6s vs 3.5s), and dismiss on click. Renders nothing until posted to. + +## History +- 2026-09-20: created — slice 12; inline toasts for menu/PR command results. + +## Notes / gotchas +- Posters build the `CustomEvent` themselves (components are standalone, no shared + module); keep the detail shape in sync with this contract. +- Messages are set via `textContent` (no HTML injection). diff --git a/web/templates/index.html b/web/templates/index.html index 31c1519..253fd85 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -13,6 +13,7 @@ +