diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 64e02490..87468c02 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1253,762 +1253,16 @@ entry. See `CONTRIBUTING.md` § Releases & changelog. translates through `llmProviderSeam`, including streaming final-event usage telemetry and provider-based retry classification. -### Fixed - -- **web-ui/chat**: provider errors (quota, rate-limit, billing) are now surfaced - as the provider's human-readable sentence across all chat surfaces — the main - chat bubble, the builder chat, the preview chat, and the default simple - builder intake — with a translated generic fallback, instead of the raw HTTP - status and JSON envelope. On the primary path the orchestrator failure arrives - as an in-band error event on an already-streaming 200 response; the background - stream toast now finishes that turn as a failure showing the same humanized - sentence, instead of reporting it as 'done' (a successful turn) as it did - before (#403). - ---- - -## [0.54.0] - 2026-07-06 - -### Added - -- **web-ui/chat**: collapsible debug-chat intro banner (#428) - ---- - -## [0.53.0] - 2026-07-06 - -### Added - -- **web-ui**: restore Days One face for the omadia wordmark (#427) - ---- - -## [0.52.3] - 2026-07-06 - -### Fixed - -- **channels**: rebind inbound route handler on hot-reinstall (#395) (#407) - ---- - -## [0.52.2] - 2026-07-06 - -### Changed - -- move Orchestrators/Conductor into Admin cluster, enlarge chevron (#424) - -### Fixed - -- **web-ui**: stop chat auto-scroll from yanking user back to bottom (#404) (#425) - ---- - -## [0.52.1] - 2026-07-06 - -### Fixed - -- **web-ui**: allow changing or removing an LLM provider's API key (#402) (#423) - ---- - -## [0.52.0] - 2026-07-03 - -### Added - -- **builder**: wire type:oauth UI + gate provider/scopes -- **builder**: add oauth_providers descriptor + type:oauth wiring for AgentSpec (#371) - ---- - -## [0.51.0] - 2026-07-03 - -### Added - -- **skills**: skill lifecycle — import, edit, safety guard, multi-source adapters, bundles, and direct-answer persona skills (#411) - ---- - -## [0.50.1] - 2026-07-03 - -### Fixed - -- **store**: portal install drawer above global header - ---- - -## [0.50.0] - 2026-07-02 - -### Added - -- **orchestrator**: per-Agent LLM model selection - -### Fixed - -- **orchestrator**: address per-Agent model selection review - ---- - -## [0.49.0] - 2026-07-02 - -### Added - -- **ui-prefs**: persist Lume palette/appearance server-side per user (#287) - -### Fixed - -- **ui-prefs**: avoid 401 bounce; clear prefs cookie on logout - ---- - -## [0.48.0] - 2026-07-01 - -### Added - -- **store**: dynamic post-install setup options for plugin fields (#393) - ---- - -## [0.47.0] - 2026-07-01 - -### Added - -- **conductor**: guided designer UX — dropdowns + builders replace raw ISO/cron/JSON inputs (#398) - ---- - -## [0.46.1] - 2026-06-30 - -### Fixed - -- **ui**: update table rendering behavior (#366) - ---- - -## [0.46.0] - 2026-06-30 - -### Added - -- **conductor**: approval-card reminder contract + holder-authorized await resolution (#394) - ---- - -## [0.45.0] - 2026-06-30 - -### Added - -- **conductor**: principalRef identity-bridge for channel-binding delivery (P2a) (#389) - ---- - -## [0.44.0] - 2026-06-30 - -### Added - -- Omadia Conductor — deterministic workflow engine (Spec 005, US1–US9 + waves 1–6 + channel event-emit) (#388) - ---- - -## [0.43.1] - 2026-06-29 - -### Fixed - -- implement pr feedback -- **ui**: update dropdown font + bg color - ---- - -## [0.43.0] - 2026-06-29 - -### Added - -- **platform**: plugin egress primitives — ctx.net (raw TCP) + $config.* in network.outbound (#370) - ---- - -## [0.42.0] - 2026-06-29 - -### Added - -- implement pr feedback - -### Fixed - -- **auth**: redirect /login to dashboard if already logged in - ---- - -## [0.41.0] - 2026-06-24 - -### Added - -- **#309**: run agents on LLM subscriptions via the official CLIs (#367) - ---- - -## [0.40.0] - 2026-06-24 - -### Added - -- in-app "Create Issue" button (operator GitHub device flow) (#363) - ---- - -## [0.39.0] - 2026-06-23 - -### Added - -- **builder**: run codegen + preview on any configured LLM provider (#297) (#320) - ---- - -## [0.38.0] - 2026-06-22 - -### Added - -- **platform**: declarative kernel OAuth broker (descriptor engine) — spec 005 core (#325) - ---- - -## [0.37.3] - 2026-06-22 - -### Fixed - -- **web-ui**: lowercase the omadia brand name in user-facing text (#359) - ---- - -## [0.37.2] - 2026-06-22 - -### Fixed - -- **desktop**: rename wizard bridge const to avoid global name collision (#358) - ---- - -## [0.37.1] - 2026-06-22 - -### Fixed - -- **desktop**: bundle preload so the onboarding wizard works (+ install verbosity) (#357) - ---- - -## [0.37.0] - 2026-06-22 - -### Added - -- **desktop**: native one-click installer with bundled PostgreSQL 17 + pgvector (macOS/Linux/Windows) (#355) - ---- - -## [0.36.0] - 2026-06-19 - -### Added - -- **desktop**: native one-click installer (Electron + embedded PGlite) + signing CI (#341) - ---- - -## [0.35.1] - 2026-06-19 - -### Fixed - -- **ci**: publish versioned + latest images on auto-release (#340) - ---- - -## [0.35.0] - 2026-06-19 - -### Added - -- minimal-core onboarding stack (prebuilt images + opt-in overlays) (#339) - ---- - -## [0.34.0] - 2026-06-18 - -### Added - -- **orchestrator**: agent transparency + Direct Line + forced delegation (#332) (#335) - ---- - -## [0.33.2] - 2026-06-18 - -### Fixed - -- **builder**: persist preview test-credentials on apply + host-backed preview ctx.llm (#334) - ---- - -## [0.33.1] - 2026-06-18 - -### Fixed - -- **builder**: provide ctx.jobs + ctx.status stubs in preview harness (#328) - ---- - -## [0.33.0] - 2026-06-17 - -### Added - -- **privacy-guard**: render V4 results as a structured, guard-flagged canvas table (#324) - ---- - -## [0.32.0] - 2026-06-17 - -### Added - -- **llm**: contract-only SDK-free core + wire-format adapter packages (#298) (#323) - ---- - -## [0.31.0] - 2026-06-16 - -### Added - -- **kg**: automatic self-curation — durable coverage grows + duplicates auto-merge (#322) - ---- - -## [0.30.0] - 2026-06-16 - -### Added - -- **platform**: runtime credentials + flow toolkit + plugin status (spec 004) (#318) - ---- - -## [0.29.0] - 2026-06-16 - -### Added - -- Lumens (Live Interactivity) 1.1 — canvas-core + Tier-2 producer (server) (#315) - ---- - -## [0.28.0] - 2026-06-16 - -### Added - -- **orchestrator**: durable long-term knowledge tier + auto-promotion (#317) - ---- - -## [0.27.1] - 2026-06-16 - -### Fixed - -- **web-ui**: widen markdown table cell spacing to Lume density (#316) - ---- - -## [0.27.0] - 2026-06-15 - -### Added - -- **orchestrator-extras**: relevance-gate + LLM-agnostic judge for cross-session recall (#310) - ---- - -## [0.26.0] - 2026-06-15 - -### Added - -- **llm-provider**: support keyless local providers (e.g. Ollama) (#308) - ---- - -## [0.25.2] - 2026-06-15 - -### Fixed - -- **ui-orchestrator**: canvas composition uses model classes + mirror provider keys (fixes stuck "Working on it…") (#307) - ---- - -## [0.25.1] - 2026-06-15 - -### Fixed - -- **llm**: register provider plugins on hot-install, not just at boot (#306) - ---- - -## [0.25.0] - 2026-06-15 - -### Added - -- **install**: multiline setup fields for string/secret values (#305) - ---- - -## [0.24.1] - 2026-06-15 - -### Fixed - -- **llm**: preserve server tools through the provider seam (live 400 hotfix) (#304) - ---- - -## [0.24.0] - 2026-06-15 - -### Added - -- **pairing**: friction-free Omadia UI ↔ host pairing — server side (#293) (#303) - ---- - -## [0.23.0] - 2026-06-15 - -### Added - -- **admin**: data-driven provider compliance flags (requiresAvvDisclosure/euHosted) (#302) - ---- - -## [0.22.0] - 2026-06-15 - -### Added - -- **llm**: everything-is-a-plugin — pluggable provider seam + empty core (Anthropic/OpenAI/Mistral/MiniMax plugins) (#300) - ---- - -## [0.21.0] - 2026-06-14 - -### Added - -- **llm**: Mistral as a first-class admin-selectable provider (#299) - ---- - -## [0.20.0] - 2026-06-14 - -### Added - -- **llm**: pluggable LLM provider — OpenAI (GPT-5.x) as admin-selectable provider (#292) - ---- - -## [0.19.0] - 2026-06-14 - -### Added - -- **canvas**: publish privacy-shield datasets — canvas_publish_rows accepts datasetId - -### Fixed - -- **canvas**: carry the sentinel sink through the STREAMING turn scope too -- **canvas**: carry the sentinel sink into the turn scope — the tap never fired -- **canvas**: tap raw sentinels before privacy interning — guarded servers never rendered - ---- - -## [0.18.0] - 2026-06-12 - -### Added - -- **omadia-ui**: Tier-2 canvas pipeline — skeleton fix, producer tools (rows/charts/choice), typed UI actions, per-user canvas registry (#277) - ---- - -## [0.17.1] - 2026-06-12 - -### Fixed - -- **builder**: resolve Anthropic client per turn so vault-seeded keys reach the Builder (#281) - ---- - -## [0.17.0] - 2026-06-10 - -### Added - -- **builder**: one-click agent export from dashboard cards (#270) (#279) - ---- - -## [0.16.2] - 2026-06-10 - -### Changed - -- **plan-runner**: reuse stored processes + batch plan-step reads, cache overlay (#276) - -### Fixed - -- **memory**: stop logging expected memory-tool errors as crashes (#278) - ---- - -## [0.16.1] - 2026-06-10 - -### Fixed - -- **builder-preview**: wire ctx.http into the preview runtime (#275) - ---- - -## [0.16.0] - 2026-06-09 - -### Added - -- **ui-orchestrator**: skeleton composition + requirement handoff (#273) - ---- - -## [0.15.0] - 2026-06-09 - -### Added - -- **ui-channel**: thread localOperations + turn action into metadata (#272) - ---- - -## [0.14.0] - 2026-06-08 - -### Added - -- **admin**: de-duplicate per-plugin settings out of the .env admin page (#265) - ---- - -## [0.13.2] - 2026-06-08 - -### Fixed - -- **agent-builder**: propagate runtime agent installs to fallback even when boot was chat-disabled (#266) - ---- - -## [0.13.1] - 2026-06-08 - -### Fixed - -- **orchestrator**: forward modelRouting to per-Agent orchestrators (#263) - ---- - -## [0.13.0] - 2026-06-08 - -### Added - -- **chat**: show the Haiku-triage decision inline in the turn card (#261) - ---- - -## [0.12.1] - 2026-06-08 - -### Fixed - -- **web-ui**: dismiss stream toasts visually + explicit abort with confirm (#260) - ---- - -## [0.12.0] - 2026-06-08 - -### Added - -- **admin**: .env-based settings overview with live auto-apply + model-routing env wiring (#259) - ---- - -## [0.11.1] - 2026-06-07 - -### Fixed - -- **web-ui**: usage dashboard 404 + show per-turn model & tokens in chat (#258) - ---- - -## [0.11.0] - 2026-06-07 - -### Added - -- **plugins**: auto-author self-extension + standalone-plugin SDK (#255) - ---- - -## [0.10.0] - 2026-06-07 - -### Added - -- LLM cost telemetry, dashboard & per-turn Sonnet/Opus routing (#253) - ---- - -## [0.9.0] - 2026-06-07 - -### Added - -- **routines**: cold-start delivery-target model for proactive 1:1 outreach (#252) - ---- - -## [0.8.2] - 2026-06-07 - -### Fixed - -- **middleware**: propagate runtime plugin (de)activation to per-Agent orchestrators (#257) - ---- - -## [0.8.1] - 2026-06-07 - -### Fixed - -- **dynamic-runtime**: late-resolve vault-armed Anthropic client for sub-agents (#256) - ---- - -## [0.8.0] - 2026-06-07 - -### Added - -- **plugins**: operator-gated, non-escalating plugin self-extension (#254) - ---- - -## [0.7.0] - 2026-06-07 - -### Added - -- **plan-runner**: GC semantically-duplicate plans on materialise (#241) - ---- - -## [0.6.1] - 2026-06-06 - -### Fixed - -- **orchestrator**: raise tool-loop cap 25→100 with round-loop guard + best-effort finalize (#240) - ---- - -## [0.6.0] - 2026-06-06 - -### Added - -- **orchestrator**: live mid-turn steering of a running chat turn (#239) - ---- - -## [0.5.2] - 2026-06-06 - -### Fixed - -- **orchestrator**: raise tool-loop cap 12→25 with floor on stale configs (#237) - ---- - -## [0.5.1] - 2026-06-06 - -### Fixed - -- **config**: treat empty optional diagram/S3 env vars as unset, not a boot-crash (#238) - ---- - -## [0.5.0] - 2026-06-06 - -### Added - -- **ui-orchestrator**: Tier-2 surface synthesis in canvasChatAgent (PR-9b-1) (#235) - ---- - -## [0.4.0] - 2026-06-06 - -### Added - -- **builder**: codegen/build/runtime observability tools for the Builder agent (#227) (#236) - ---- - -## [0.3.8] - 2026-06-05 - -### Fixed - -- **middleware**: arm host-LLM plugins on vault key-entry so plan-runner works on fresh installs (#234) - ---- - -## [0.3.7] - 2026-06-05 - -### Fixed - -- **builder**: author plugins from spec.author, not hardcoded "byte5 GmbH" (#225) (#233) - ---- - -## [0.3.6] - 2026-06-05 - -### Fixed - -- **builder**: prevent message loss when toggling simple/extended view (#224) (#231) - ---- - -## [0.3.5] - 2026-06-05 - -### Fixed - -- **web-ui**: install drawer overlays render above global header (#232) - ---- - -## [0.3.4] - 2026-06-05 - -### Fixed - -- **web-ui**: survive stale/foreign chat-session shapes instead of a blank crash (#230) - ---- - -## [0.3.3] - 2026-06-05 - -### Fixed - -- **builder**: raise report_platform_issue summary cap 280→500 (#229) - ---- - -## [0.3.2] - 2026-06-05 - -### Fixed - -- **orchestrator**: boot gracefully without ANTHROPIC_API_KEY (Setup-Wizard key entry) (#228) - ---- - -## [0.3.1] - 2026-06-05 - -### Fixed - -- **knowledge-graph**: survive first-boot Postgres race instead of crash-looping (#226) - ---- - -## [0.3.0] - 2026-06-05 - -### Added - -- **builder**: native core-bug reporting — GitHub App direct-create + UI (#223) - ---- - -## [0.2.1] - 2026-06-05 - -### Changed - -- **builder**: user-facing 'Veröffentlichen' → 'Bereitstellen' (i18n de, redo of #208) (#217) - -### Fixed - -- **ci**: set git identity before annotated release tag (#218) -- **builder**: ctx.memory in preview runtime, accessor permission lint, and setup_fields rename (#207) +### Changed — background chat streams surface in-context, not as toasts + +- **Removed `StreamToasts`** (the bottom-right floating cards for background + chat turns). Per the Lume visual spec §7.6, toasts / floating notifications + are a ship-blocking anti-pattern; §7.4 makes the chat the surface of record. +- **Background-stream state now lives on the chat tab**: a running background + turn shows a pulsing accent dot, a finished one an accent dot, an errored one + a danger dot — each carrying an aria-label + title so colour is never the sole + signal (§8). Selecting the tab clears its unread marker; active-session errors + continue to render inline on the turn. See [ADR-0006](adr/0006-in-context-background-stream-surfacing.md). --- diff --git a/docs/adr/0006-in-context-background-stream-surfacing.md b/docs/adr/0006-in-context-background-stream-surfacing.md new file mode 100644 index 00000000..03c588e7 --- /dev/null +++ b/docs/adr/0006-in-context-background-stream-surfacing.md @@ -0,0 +1,83 @@ +# 0006 — In-context surfacing for background chat streams (no toasts) + +## Status + +Accepted + +- **Date:** 2026-07-02 +- **Deciders:** Operator-UI maintainers +- **Supersedes:** — + +## Context and Problem Statement + +The operator web UI runs chat turns per session and lets the user switch tabs +or menu routes while a turn is still streaming (the runner is headless). The +first Lume integration (#284) shipped `StreamToasts` — bottom-right floating +cards — to tell the user a background turn had progressed or finished. But the +Lume visual spec (`byte5ai/omadia-ui` `docs/visual-spec.md`) forbids toasts: +§7.4 makes the canvas the *surface of record* ("errors live in the tree, in +context") and §7.6 lists "toasts / floating notifications" as a ship-blocking +anti-pattern. #284 restyled the toasts in Lume material but deliberately left +the mechanism in place, deferring the architecture call to #286. How should +background-stream state reach the user without violating §7.6? + +## Decision Drivers + +- §7.6 anti-patterns are ship-blockers (spec §10); reintroducing them must not ship. +- §7.4 intent: state lives in context, on the surface of record — here, the chat. +- The needed data already exists per-session in `streamStore` (`phase`, + `previewTail`, `error`); no new persistence should be required. +- §8 accessibility floor: colour is never the sole signal. + +## Considered Options + +- **A — In-context surfacing.** Remove `StreamToasts`; surface background-stream + state on the session's chat tab (a dot) and keep errors inline in the turn. +- **B — Sanctioned deviation.** Keep the toasts; document that the operator UI + is not the canvas renderer the spec primarily governs (§9 scope), so the + §7.6 ban does not bind it. + +## Decision Outcome + +Chosen option: **A**, because it honours the spec's intent rather than carving +an exception, and it is cheap — `streamStore` already holds everything the tab +needs, so the change is a relocation of existing state, not new machinery. + +### Consequences + +- 🟢 **Good:** No §7.6 violation; the tab is the surface of record for its session. +- 🟢 **Good:** No new store surface, persistence, or notification centre — + `useStreamRecord` + the existing `dismiss()` cover display and clear-on-select. +- 🟢 **Good:** Active-session errors already render inline on the message, so no + new error UI was needed. +- 🔴 **Bad:** Stopping a *background* stream now takes one extra click — open the + tab, then use the existing in-chat stop button. The toast's inline abort is gone. +- ⚪ **Neutral:** The unread dot clears on tab select only. Select forgets a + `done` record; `error` / `aborted` / running records are kept — the + agent_unavailable recovery banner and inline error read them off the store, + and dropping a running record would flip `isActive` off (stop button, + composer lock). An unresolved background error therefore re-flags its dot if + the user views then leaves the tab again, which is intended. + +## Pros and Cons of the Options + +### A — In-context surfacing + +- 🟢 Matches §7.4/§7.6; no exception to defend or re-litigate later. +- 🟢 Reuses existing state; small, reviewable diff. +- 🔴 A dot is quieter than a floating card — the user must glance at the tab strip. + +### B — Sanctioned deviation + +- 🟢 Zero code change; toasts already Lume-styled. +- 🔴 Leans on a scope loophole the issue author themselves argued against; erodes + the anti-pattern list and invites the next deviation. + +## More Information + +- Issue #286; base Lume integration #284; adoption tracking #282. +- Lume visual spec §7.4 (Errors / surface of record), §7.6 (anti-pattern list), + §8 (accessibility floor) — `byte5ai/omadia-ui` `docs/visual-spec.md`. +- Implementation: `web-ui/app/_components/ChatTabs.tsx` (tab dot), + `web-ui/app/chat/page.tsx` (`handleSelect` clear-on-select), + `web-ui/app/layout.tsx` (toast mount removed). diff --git a/docs/adr/README.md b/docs/adr/README.md index 79e0e0b6..0d686e1c 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -33,6 +33,7 @@ decision, write a new ADR and mark the old one **Superseded by …**. | 0003 | [Capability-based, multi-provider middleware](0003-capability-based-multi-provider-middleware.md) | Accepted | 2026-06-03 | | 0004 | [Knowledge graph as the agent memory substrate](0004-knowledge-graph-as-memory-substrate.md) | Accepted | 2026-06-03 | | 0005 | [Two-phase confirmation for write-capable connectors](0005-two-phase-confirmation-for-writes.md) | Accepted | 2026-06-03 | +| 0006 | [In-context surfacing for background chat streams (no toasts)](0006-in-context-background-stream-surfacing.md) | Accepted | 2026-07-02 | > These first records are written *retroactively* — they document decisions that > were already implemented and proven in the product. New decisions should be diff --git a/web-ui/app/_components/ChatTabs.tsx b/web-ui/app/_components/ChatTabs.tsx index 568e26a0..13ff94cd 100644 --- a/web-ui/app/_components/ChatTabs.tsx +++ b/web-ui/app/_components/ChatTabs.tsx @@ -9,6 +9,11 @@ import { } from 'react'; import { useTranslations } from 'next-intl'; import type { ChatSession } from '../_lib/chatSessions'; +import { + isStreamActive, + useStreamRecord, + type StreamRecord, +} from '../_lib/streamStore'; interface ChatTabsProps { sessions: ChatSession[]; @@ -71,6 +76,39 @@ export function ChatTabs({ ); } +/** Background-stream state a tab surfaces as a dot. `null` = nothing to show + * (active tab, no record, or a user-aborted turn). */ +type TabStreamState = 'running' | 'done' | 'error'; + +function tabStreamState( + active: boolean, + rec: StreamRecord | undefined, +): TabStreamState | null { + if (active || !rec) return null; + if (isStreamActive(rec)) return 'running'; + if (rec.phase === 'error') return 'error'; + if (rec.phase === 'done') return 'done'; + return null; +} + +function streamAriaKey(state: TabStreamState): string { + return state === 'running' + ? 'streamRunningAria' + : state === 'error' + ? 'streamErrorAria' + : 'streamDoneAria'; +} + +/** A dot, never a status pill (§7.6). Colour is never the sole signal — + * each dot carries an aria-label + title (§8). */ +function streamDotClass(state: TabStreamState): string { + const base = 'ml-1 inline-block size-1.5 shrink-0 rounded-full'; + if (state === 'error') return `${base} bg-[color:var(--danger)]`; + if (state === 'running') + return `${base} bg-[color:var(--accent)] animate-pulse`; + return `${base} bg-[color:var(--accent)]`; +} + interface TabProps { session: ChatSession; active: boolean; @@ -94,6 +132,11 @@ function Tab({ const [editing, setEditing] = useState(false); const [draft, setDraft] = useState(session.title); const inputRef = useRef(null); + // In-context surfacing (issue #286): a background session's stream state + // lives on its tab, not in a floating toast. The active tab shows nothing — + // its stream is already visible inline. `aborted` gets no marker: the user + // stopped it themselves, so there's nothing unread to flag. + const streamState = tabStreamState(active, useStreamRecord(session.id)); useEffect(() => { if (editing) { @@ -166,6 +209,14 @@ function Tab({ ) : ( {session.title} )} + {streamState && !editing && ( + + )} {canClose && !editing && ( - - {record.previewTail && ( -
- {record.previewTail} -
- )} -
- {t('elapsedSec', { sec: elapsedSec })} - {typeof record.tokensIn === 'number' && record.tokensIn > 0 ? ( - - · ↓ {formatTokenCount(record.tokensIn)} - - ) : null} - {typeof record.tokensOut === 'number' && record.tokensOut > 0 ? ( - - ↑ {formatTokenCount(record.tokensOut)} - - ) : null} - {typeof record.cacheTokens === 'number' && record.cacheTokens > 0 ? ( - - · 🟢 {formatTokenCount(record.cacheTokens)} - - ) : null} - {record.error ? ( - - · {record.error} - - ) : null} -
- - - {/* Real-stop affordance — only while the stream is still running. - Clicking opens a confirm modal; confirming calls onAbort(), which - aborts the underlying fetch. Separate from the top-right X, which - only hides the toast. */} - {!isTerminal ? ( - - ) : null} - - {confirming ? ( - { - setConfirming(false); - onAbort(); - }} - onCancel={() => { - setConfirming(false); - }} - /> - ) : null} - - - ); -} - -interface AbortConfirmModalProps { - t: ReturnType; - onConfirm: () => void; - onCancel: () => void; -} - -/** - * Lightweight custom confirmation dialog (no native window.confirm). Rendered - * through a portal to so its fixed positioning is immune to the - * transformed motion ancestors of the toast stack. Closes on Escape or - * backdrop click; the safe "keep running" action is auto-focused. - */ -function AbortConfirmModal({ - t, - onConfirm, - onCancel, -}: AbortConfirmModalProps): React.ReactPortal | null { - useEffect(() => { - const onKey = (e: KeyboardEvent): void => { - if (e.key === 'Escape') onCancel(); - }; - window.addEventListener('keydown', onKey); - return () => { - window.removeEventListener('keydown', onKey); - }; - }, [onCancel]); - - if (typeof document === 'undefined') return null; - - return createPortal( - { - e.stopPropagation(); - onCancel(); - }} - role="presentation" - > - { - e.stopPropagation(); - }} - > -

- {t('abortConfirmTitle')} -

-

- {t('abortConfirmBody')} -

-
- - -
-
-
, - document.body, - ); -} - -/** "1.2k" / "340" / "1.45M" — keeps the toast row compact. */ -function formatTokenCount(n: number): string { - if (n < 1000) return String(n); - if (n < 10000) return (n / 1000).toFixed(2).replace(/\.?0+$/, '') + 'k'; - if (n < 1000000) return Math.round(n / 1000) + 'k'; - return (n / 1000000).toFixed(2).replace(/\.?0+$/, '') + 'M'; -} - -/** Re-renders the caller every second when `active` is true. Returns the - * current wall-clock at the last tick. */ -function useTickingNow(active: boolean): number { - const [now, setNow] = useState(() => Date.now()); - useEffect(() => { - if (!active) return; - const id = setInterval(() => { - setNow(Date.now()); - }, 1000); - return () => { - clearInterval(id); - }; - }, [active]); - return now; -} - -function phaseLabelFor( - phase: StreamPhase, - t: ReturnType, -): string { - switch (phase) { - case 'pending': - case 'thinking': - return t('phaseThinking'); - case 'streaming': - return t('phaseStreaming'); - case 'tool_running': - return t('phaseToolRunning'); - case 'done': - return t('phaseDone'); - case 'error': - return t('phaseError'); - case 'aborted': - return t('phaseAborted'); - } -} - -function paletteFor(phase: StreamPhase): { - border: string; - bg: string; - icon: string; - symbol: string; -} { - switch (phase) { - case 'done': - return { - border: 'border-[color:var(--success)]', - bg: 'bg-[color:var(--success)]/10', - icon: 'text-[color:var(--success)]', - symbol: '✓', - }; - case 'error': - return { - border: 'border-[color:var(--danger-edge)]', - bg: 'bg-[color:var(--danger)]/8', - icon: 'text-[color:var(--danger)]', - symbol: '✗', - }; - case 'aborted': - return { - border: 'border-[color:var(--border)]', - bg: 'bg-[color:var(--bg-soft)]', - icon: 'text-[color:var(--fg-muted)]', - symbol: '⏹', - }; - default: - return { - border: 'border-[color:var(--accent)]', - bg: 'bg-[color:var(--bg-elevated)]', - icon: 'text-[color:var(--accent)]', - symbol: '…', - }; - } -} diff --git a/web-ui/app/chat/page.tsx b/web-ui/app/chat/page.tsx index af5e18df..5a4de69a 100644 --- a/web-ui/app/chat/page.tsx +++ b/web-ui/app/chat/page.tsx @@ -443,6 +443,20 @@ export default function ChatPage(): React.ReactElement { void deleteSession(id); }; + // Selecting a tab marks its background answer as read (issue #286). We only + // forget a `done` record: the dot itself already hides once the tab is active + // (it renders for non-active tabs only), so dismiss exists purely to stop a + // finished answer re-flagging after the user switches away again. `error` / + // `aborted` records are deliberately kept — the agent_unavailable recovery + // banner and the inline error read them off the store (page.tsx §Phase A). + // A running record is likewise kept so `isActive` (stop button / composer + // lock) stays correct once the tab is active. + const handleSelect = (id: string): void => { + const rec = streamStore.get(id); + if (rec?.phase === 'done') streamStore.dismiss(id); + setActive(id); + }; + const canReset = !hydrating && !resetPending && activeSession.messages.length > 0; @@ -451,7 +465,7 @@ export default function ChatPage(): React.ReactElement { { diff --git a/web-ui/app/layout.tsx b/web-ui/app/layout.tsx index 58c79aa6..f2bfb013 100644 --- a/web-ui/app/layout.tsx +++ b/web-ui/app/layout.tsx @@ -12,7 +12,6 @@ import { Nav } from './_components/Nav'; import { ThemeControls } from './_components/ThemeControls'; import { SessionWatcher } from './_components/SessionWatcher'; import { StreamRunner } from './_components/StreamRunner'; -import { StreamToasts } from './_components/StreamToasts'; import { ChatSessionsProvider } from './_lib/chatSessionsContext'; import { StreamStoreProvider } from './_lib/streamStore'; import { UI_PREFS_COOKIE, parseUiPrefsCookie } from './_lib/uiPrefs'; @@ -137,12 +136,11 @@ export default async function RootLayout({
{children}
- {/* Background-stream toasts (only render for chats that - aren't currently in view). The runner is headless — it - owns the fetch + NDJSON-parse loop so that switching to - another menu route doesn't kill an in-flight turn. */} + {/* Headless stream runner — owns the fetch + NDJSON-parse loop + so switching menu route doesn't kill an in-flight turn. + Background-stream state surfaces in-context on the chat tab + (issue #286, Lume §7.4/§7.6), not in a floating toast. */} - diff --git a/web-ui/messages/de.json b/web-ui/messages/de.json index 0a3d487f..b293f52e 100644 --- a/web-ui/messages/de.json +++ b/web-ui/messages/de.json @@ -787,7 +787,10 @@ "tabTitleSuffix": "ID: {id}\nDoppelklick zum Umbenennen", "closeAriaLabel": "Tab schließen: {title}", "closeWhileBusyTitle": "Warten bis der Turn durch ist", - "closeTitle": "Tab schließen" + "closeTitle": "Tab schließen", + "streamRunningAria": "{title}: Antwort wird erstellt", + "streamDoneAria": "{title}: Antwort fertig", + "streamErrorAria": "{title}: Antwort fehlgeschlagen" }, "authBadge": { "loginLink": "Login", @@ -1261,25 +1264,6 @@ "orphanExplain": "Plugin-IDs aus früheren Seeds, die im aktuellen Katalog nicht mehr existieren (typisch: Umbenennung de.byte5.agent.* → @omadia/*). Werden beim nächsten „Speichern\" verworfen, außer du markierst sie als „Behalten\".", "orphanKeep": "Behalten" }, - "streamToasts": { - "phaseThinking": "Agent denkt", - "phaseStreaming": "Antwort läuft", - "phaseToolRunning": "Tool läuft", - "phaseDone": "Antwort fertig", - "phaseError": "Fehler", - "phaseAborted": "Abgebrochen", - "elapsedSec": "{sec}s", - "tokensInTitle": "{n} Input-Tokens (kumuliert über Iterationen)", - "tokensOutTitle": "{n} Output-Tokens (kumuliert über Iterationen)", - "cacheHitTitle": "{n} Cache-Read-Tokens", - "dismissAriaLabel": "Benachrichtigung schließen (Stream läuft weiter)", - "dismissTitle": "Schließen", - "abortButton": "Stream stoppen", - "abortConfirmTitle": "Stream wirklich stoppen?", - "abortConfirmBody": "Die laufende Antwort wird sofort abgebrochen und kann nicht fortgesetzt werden.", - "abortConfirmKeep": "Weiter laufen lassen", - "abortConfirmStop": "Ja, stoppen" - }, "builder": { "issueReport": { "kicker": "Core-Bug an omadia melden", diff --git a/web-ui/messages/en.json b/web-ui/messages/en.json index 59d7229a..4d665684 100644 --- a/web-ui/messages/en.json +++ b/web-ui/messages/en.json @@ -787,7 +787,10 @@ "tabTitleSuffix": "ID: {id}\nDouble-click to rename", "closeAriaLabel": "Close tab: {title}", "closeWhileBusyTitle": "Wait until the turn is done", - "closeTitle": "Close tab" + "closeTitle": "Close tab", + "streamRunningAria": "{title}: response in progress", + "streamDoneAria": "{title}: response ready", + "streamErrorAria": "{title}: response failed" }, "authBadge": { "loginLink": "Login", @@ -1261,25 +1264,6 @@ "orphanExplain": "Plugin ids from earlier seeds that no longer exist in the current catalog (typically a rename like de.byte5.agent.* → @omadia/*). Dropped on next Save unless you tick \"Keep\".", "orphanKeep": "Keep" }, - "streamToasts": { - "phaseThinking": "Agent is thinking", - "phaseStreaming": "Answer streaming", - "phaseToolRunning": "Tool running", - "phaseDone": "Answer ready", - "phaseError": "Error", - "phaseAborted": "Aborted", - "elapsedSec": "{sec}s", - "tokensInTitle": "{n} input tokens (cumulative across iterations)", - "tokensOutTitle": "{n} output tokens (cumulative across iterations)", - "cacheHitTitle": "{n} cache-read tokens", - "dismissAriaLabel": "Dismiss notification (stream keeps running)", - "dismissTitle": "Dismiss", - "abortButton": "Stop stream", - "abortConfirmTitle": "Stop this stream?", - "abortConfirmBody": "The running response will be aborted immediately and can't be resumed.", - "abortConfirmKeep": "Keep running", - "abortConfirmStop": "Yes, stop" - }, "builder": { "issueReport": { "kicker": "Report core bug to omadia",