Activity: one stream at a time, none while hidden; the header by the chat's width
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:
4 files changed
+35
-9
No files matched your search
+13
@@ -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
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -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
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user