Activity: one stream at a time, none while hidden; the header by the chat's width
ci / check (push) Failing after 3s
ci / start (push) Failing after 2s

The events stream is held while it opens, so a reconnect and the window
showing again cannot open two; while the window is hidden in the tray it is
not reopened (the server polls for each), and showing the window opens it
again. The Thinking switch says 'Think before answering' in the header too:
'Thinking' is the catalogues' word for a reply in progress. The chat's header
restacks by the chat column's own width (a container query at --bp-narrow),
so a chat beside the activity panel keeps its title.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
HomerandClaude Opus 5.5 committed 2026-11-09 10:53:40 +00:00
1 parent b6d69575f7
commit de914f4b16
4 files changed
+35 -9

No files matched your search

+13
View File
@@ -1053,6 +1053,19 @@ label, .label { font: var(--text-sm) / var(--leading-tight) var(--font-ui); colo
.activity { grid-area: main; z-index: var(--z-panel); border-left: 0; }
}
/* The chat's header by the chat's own width, not the window's: with the activity panel or the
viewer beside it, a wide window still leaves a narrow chat, and its controls get a row of their
own there as on a narrow window. 48rem is --bp-narrow again. */
.main { container: chat / inline-size; }
@container chat (max-width: 48rem) {
.head { padding: var(--sp-2) var(--sp-3) 0; grid-template-columns: minmax(0, 1fr) auto; }
.head__place { grid-column: 1 / -1; grid-row: 2; padding-bottom: var(--sp-2); }
.head__place:empty { display: none; }
.head__tools { grid-column: 1 / -1; grid-row: 3; flex-wrap: wrap; padding-bottom: var(--sp-2); }
.head__place:empty + .head__tools { grid-row: 2; }
.head__tools:empty { display: none; }
}
/* 48rem (--bp-narrow): the sidebar is a drawer, opened from the title bar. */
@media (max-width: 48rem) {
:root { --side-btn-w: var(--titlebar-h); }
+21 -5
View File
@@ -169,6 +169,11 @@ function stop() {
}
async function listenTo(id) {
// Held while the stream opens, so nothing opens a second one meanwhile.
const pending = { close() {} }
stream = pending
let ended = false
let handleId = null
try {
const handle = await follow(`/chats/${enc(id)}/activity/events`, {
onFrame: (event, frame) => {
@@ -179,10 +184,14 @@ async function listenTo(id) {
// The server closes the stream after a while (`close`): it is opened again at once. A broken
// one is opened again later, more slowly each time -- polling, in effect, while it fails.
onEnd: (err) => {
ended = true
if (chat?.id !== id) return
stream = null
if (stream === pending || stream?.id === handleId) stream = null
// Not while the window is hidden (in the tray): it is opened again when it shows.
if (document.visibilityState === "hidden") return
failures = err ? failures + 1 : 0
retry = setTimeout(() => {
retry = 0
if (chat?.id === id && !stream) {
if (err && panelOpen()) read(false)
listenTo(id)
@@ -190,12 +199,17 @@ async function listenTo(id) {
}, err ? Math.min(60000, 5000 * failures) : 500)
},
})
if (chat?.id !== id) handle.close()
else stream = handle
handleId = handle.id
if (chat?.id !== id || stream !== pending) handle.close()
else if (!ended) stream = handle
} catch {
if (stream === pending) stream = null
if (chat?.id !== id) return
failures += 1
retry = setTimeout(() => chat?.id === id && listenTo(id), Math.min(60000, 5000 * failures))
retry = setTimeout(() => {
retry = 0
if (chat?.id === id && !stream) listenTo(id)
}, Math.min(60000, 5000 * failures))
}
}
@@ -662,6 +676,8 @@ export function initActivity({ onLayout }) {
true,
)
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "visible" && chat && panelOpen()) read(false)
if (document.visibilityState !== "visible" || !chat) return
if (!stream && !retry) listenTo(chat.id)
if (panelOpen()) read(false)
})
}
-2
View File
@@ -832,8 +832,6 @@ export function newChat(kind) {
const showLevels = (id) => {
const model = models.find((m) => m.id === id)
const control = effortControl({ model, value: levelsOf(model).start, id: "new-effort" })
const switchOnly = levelsOf(model).control === "switch"
effortField.children[0].textContent = switchOnly ? t("Thinking") : t("Reasoning effort")
effortField.children[0].htmlFor = "new-effort"
effortField.children[1].replaceWith(control || h("span", { id: "new-effort" }))
effortField.hidden = !control
+1 -2
View File
@@ -165,9 +165,8 @@ export function effortControl({ model, value, onChange, id, compact }) {
"label",
{ class: `toggle${compact ? " toggle--compact" : ""}`, title: t("Think before answering") },
box,
h("span", { class: "toggle__label", text: compact ? t("Thinking") : t("Think before answering") }),
h("span", { class: "toggle__label", text: t("Think before answering") }),
)
if (compact) box.setAttribute("aria-label", t("Think before answering"))
box.addEventListener("change", () => {
current = box.checked ? on : "off"
onChange?.(current)