diff --git a/ui/app.css b/ui/app.css index 47c899a..cd5c3b1 100644 --- a/ui/app.css +++ b/ui/app.css @@ -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); } diff --git a/ui/js/activity.js b/ui/js/activity.js index 1846f9c..a00142b 100644 --- a/ui/js/activity.js +++ b/ui/js/activity.js @@ -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) }) } diff --git a/ui/js/chat.js b/ui/js/chat.js index f711aa6..3090ec1 100644 --- a/ui/js/chat.js +++ b/ui/js/chat.js @@ -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 diff --git a/ui/js/models.js b/ui/js/models.js index 4e552be..6919b28 100644 --- a/ui/js/models.js +++ b/ui/js/models.js @@ -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)