The confirmation dialog was rendered inside the non-modal history popover, so
taking focus dismissed the popover and unmounted the dialog with it. It now
lives in the header as a sibling of the popover, and the panel's Escape handler
only fires for events whose target is inside the panel.
A half-configured watch card, and its client request id, survived a chat switch
and a new chat, so it was submitted against whatever chat happened to be open.
Chat changes now go through one function that both resets the card and
invalidates in-flight opens - including the chat a submitted watch lands in.
Opening a chat subtracted from the unread count twice, and again on every
revisit; the read effect's cleanup also restored the on-screen chat id after the
panel had let go, which hid that chat's later wakes for the rest of the session.
The count is now derived from the chat list, reading a chat settles both its
wake and its work, and both counts re-seed when the environment changes.
Only a deleted chat is swallowed now. A refused close left the card in_progress while the
action reported success, so the panel span forever with nothing to retry it.
The reader always reported a live queue, so a queue_oldest_age watch on a deleted queue sat
pending for its whole window and an engine failure read as a healthy zero wait.
The errors page cites `error_<fingerprint>` and the agent's tools cite the bare one, so
the same error could carry two watches — two wakes and two emails per recurrence — and the
prefixed spelling produced an unresolvable link in the wake.
The queue detail page presents a task queue with its `task/` prefix stripped, but
`TaskQueue.name` keeps it, so every watch the page offered on a task's own queue was
refused as a missing target.
Each test replays the migrations inside its own budget, which overruns vitest's
5s default on a loaded CI host and times out a different test each run.
The block catalog and the watch spec spelled every variant out in full, and the
serialized tool definitions are paid on every call of every chat. Two collapses:
the seven simple evidence kinds become one member with a `kind` enum, and the
watch kinds that take the same fields share a member each. Same payloads accepted,
same payloads refused.
A custom queue's name has nothing to do with any task id, but the agent searched the deployed task list for a task named after the queue, found none, and reported the queue's tasks as deleted or renamed.
get_queue now returns consumerTasks for a custom queue — the deployed task slugs whose queue config names it — and the prompt says an absent task of that name is not evidence about the queue.
DashboardAgent declared turnStarted but nothing set it, so a question asked
and then left with the panel closed raised no dot until the next reload.
The panel now reports turn activity up to DashboardAgent, which latches onto
the chat whose turn is running. Closing the panel reports nothing, so the
latch holds; re-opening the chat with the turn over clears it.
A watch wake was the only thing that raised the dot and the highlight. An answer or a settled card that landed while the panel was closed now does the same — without a toast, which stays a wake's alone.
Most tools spend an environment JWT with a hardcoded scope list; the delegated token's cap only ceilings that exchange. Queues were missing from the list, so the live lookup 403'd and read as a queue that does not exist.
Metrics authorize as a query read; paused, depth and limit live on the queue row, which is a queues read the agent's token did not carry — so a live lookup 403'd and read as a missing queue.
Metrics are a window: they cannot show a pause, and they cannot show a backlog that arrived after the window. get_queue now carries the queue's live row, and the prompt leads with it.
The page chips come from the page registry, not the signals — so hiding the saturation signal left both the investigate and the watch chip on a paused queue.
The metrics route answers an unknown queue with zeroes, so asking for the wrong kind read as an idle queue. get_queue now tries the other kind before believing them.
An investigation card carries its own watch button, and the prompt asked for a watch offer on top of it. The card wins — it is the one with the pre-filled spec — and the prompt now says so too.
"Tell me when this run finishes", "ping me if that error comes back". A watch
checks on its own cadence, reports once, and stops within 24 hours. The user
confirms a pre-filled card, so nothing starts behind their back; the answer
lands in the chat, and by email if they asked for it.
Restores the feature this branch's base PR set aside, unchanged: the card and
chips, the wake banner and toast, the unread badge, the checks for runs, queues,
errors and health, the batch scheduler and its backstops, the alert channel and
its email, and the agent's schedule_watch tool with the prompt that governs it.
Pure move: the watch contracts and queries, the delivery-claim id, the chat
lock, markChatRead and the watch cancellation inside softDeleteChat, plus the
four contract types the watch schema annotates, re-narrowed here.
The transcript perf fix had two halves. The winners half kept its identity because a test
pinned it; the strip cache had none, so rewriting the component into chat primitives quietly
brought back the rebuild-every-render version, and every tool-calling turn re-rendered on each
streamed token again. Restore the cache and pin it the same way.
orderTranscript is not implicated: it returns a fresh array but the same message objects, which
is what the turns memoize on.
Where neither Ask AI nor the agent can open, the redirect left `?aiHelp=` on the
environment page with nothing to read it. Send the question to the docs instead.
- `_app`: `AskAIRoot` is a sibling of the app, not a wrapper. Its Kapa provider
mounts client-only, so wrapping the outlet remounted the whole signed-in tree
once per page load on cloud. Entry points reach it through an open-request
bridge instead of a render prop.
- The agent no longer hands a `trigger://source/...` target to `navigate`: a
resolved GitHub URL opens in a new tab, and only a root-relative path is routed.
- Chat history reloads coalesce without answering a request with data fetched
before it, so a new chat and its title land.
- Help & Feedback and the shortcuts sheet offer each AI surface only where the
reader has it: Ask AI is back for users without agent access.
- Restore the `AskAgentButton` mount on the deploy blank states, dropped in the
PR split.
- `agent-shortcuts.test.ts`: import ⌘I from `ask-ai-channels`.
new URL(environmentPath, origin) ignores origin when the path is absolute, and
the result goes straight into redirect(). Today's only caller passes a
builder-generated internal path, so this closes the gap rather than a hole.
justify-center on a scrolling column overflows equally in both directions, and
nothing can scroll back past the origin, so at the docked panel's narrowest the
heading and composer were unreachable. The child centres with m-auto, which gives
its space up once there is none to spare.
The character counter's live region only entered the DOM at the warning point,
and several screen readers only announce updates for a region that was already
there; it is now always mounted and empty until there is something to say.
The history trigger's aria-label replaced the chat title it shows, so a
speech-input user could not activate it by the words on it. The title now leads
the accessible name.
A request still in flight at unmount rejected afterwards, and the catch scheduled
a retry that fetched again and set state for a component that was gone. The hook
tracks whether it is still mounted and neither records nor reschedules once it is
not.
setSearchParams only starts the navigation that drops the param, so a render
before it commits saw the question again and asked it a second time. The reader
now records what it sent and forgets it once the URL no longer carries it, so a
later visit with the same question still works.
ViewBlocks looked each surviving block's index up with indexOf inside the render
loop: quadratic, and two occurrences of the same block object both answered with
the first index, so they collided on one React key. latestRevisionEntries carries
each survivor's position out instead.
Bar timestamps came from Date.now() during render, so the same bar reported a
different time on every re-render and a server pass disagreed with the client.
They now come from the view model's generatedAt, which the schema already
describes as the timestamp the renderer must not invent.
Moves the arithmetic into report-spark.ts to keep it clock-free and testable,
and drops the unreachable Math.max on the slice end while doing so.
Retry appended the last user message again, so the failed turn stayed in the
transcript and its text was sent twice. It now regenerates once the agent has
started answering, and otherwise re-sends the failed turn under its own id.
A limit of 0 is zero capacity, not saturation: running >= 0 holds for every
queue, so any backlog marked the queue degraded and offered Investigate, while
the agent's own suggested prompt stayed silent. One predicate now decides it
for the queue detail page, the queues list badge and the page mappers.
Radix tooltip content is not the accessible name of its trigger, so the
icon-only ask-agent button and the two deploy docs links announced as
unnamed controls. Name them explicitly and pass asChild so the tooltip
trigger stops wrapping them in a second button.
Adds a source scan that fails on the next SimpleTooltip with an unnamed
or double-wrapped control, with the pre-existing sites baselined.