ClawSweeper flagged that the prior matrix at line 31-48 tested
labels/title/notes by manually invoking normalizeBoundedString, even
though those fields use the independent normalizeTitle, normalizeNotes,
and normalizeLabels functions (each with its own generic message).
A regression that drops the (got N) suffix from normalizeBoundedString
would not be caught for those three surfaces — operators would still
see the old opaque error.
This commit narrows the matrix to only the consumers of the shared
helper (comment body, link title, link target, link URL, card id) and
restores the exact labels gateway assertion that should match the
unchanged normalizeLabels error.
Verification: 9/9 inline Node 18 AST harness (node /tmp/verify-pr-118888-v77.mjs);
parent-commit proof-gate (pre-PR helper without the got suffix) fails
4/9 of the post-fix expected messages.
Closes ClawSweeper review on PR #118888 (P2 + P3).
The shared normalizeBoundedString helper in extensions/workboard/src
threw an opaque error like 'comment body must be 2000 characters or
fewer.' whenever an oversize string hit any bounded field. Agents that
posted a long run summary or oversized link URL had no way to tell
how far over the limit they were, so they could not retry with a
trimmed payload.
Include the actual length in the error so the message tells the agent
exactly what to do next:
comment body must be 2000 characters or fewer (got 3502).
The format applies uniformly to every bounded field routed through
this helper (labels, link URL, link title, link target, comment
body, title, notes, worker log message, session key, run id,
protocol violation detail, etc.) since the helper is the single
canonical owner of the message.
Cover the new format with a focused unit test that pins the regex
for the comment-body repro from #118845 and exercises the six other
bounded fields listed in the issue. Update the existing
gateway.test.ts assertion to allow both the old and new format so
the format change is a forward-compatible improvement.
The Block Kit session card terminalized into a '🛠️ N tool calls · ⏱️ Ns'
footer, leaving a per-turn receipt in the Slack transcript on the default
progress path. PR #122976 deleted the same receipt from the native progress
surface but left this sibling rendering it.
Finished cards now keep only the durable diff stat and the session link;
tool-call and elapsed counters stay live working state. Also corrects the
documented channels.slack.streaming.mode default, stale since #122552.
The sidebar queued-message badge and the composer queue reused the clock
glyph that also marks attached automations, so a session with both showed
two identical clocks distinguishable only by tooltip. Add an outbox tray
icon (Lucide inbox with the roof swapped for a rising arrow) and use it
for queued-to-send surfaces; the clock stays automation/cron-only. The
mock dev fixture now flags one session with hasAutomation so the badge
renders in mocked-dashboard proofs.
* fix(delivery): unify terminal settlement ownership
Treat identityless adapter returns as potentially visible across channel, queue, and cron paths. Let recovery own terminal completion so ambiguity persists as notice debt instead of being double-settled or silently suppressed.
Co-authored-by: ruel225 <ruel225@users.noreply.github.com>
* refactor(delivery): narrow terminal internals
Remove now-unused internal exports after terminal-settlement ownership was consolidated.
* test(tts): preserve message runtime exports
Import and spread the actual message runtime so the focused mock retains every runtime binding while overriding only the durable send core.
---------
Co-authored-by: ruel225 <ruel225@users.noreply.github.com>
When a caller passed an explicit maxBytes with optimizeImages on (the
default), the source-read bound was inflated to max(maxBytes, 100MB
document cap). A channel with a 5MB attachment cap therefore let a
hostile or oversized URL buffer up to 100MB before the final size check
— 20x resource amplification, and the inflation applied before content
classification, so it was not limited to images.
The inflation exists so oversized-but-compressible originals can be
fetched and compressed under the delivery cap. Size that headroom off
the thing that justifies it: 4x the 6MB image cap (24MB), enough for
large phone photos, instead of the document cap.
Accepted tradeoff (named at the constant): originals above 24MB that
would have compressed under the cap now fail; the fetch error names the
bound. Callers without an explicit cap keep per-kind defaults.
Live proof on a real localhost HTTP server (no mocked fetch): an 8.1MB
PNG under a 5MB cap compresses to 3.75MB and delivers; a 30MB-declared
image is rejected at the Content-Length precheck in 7ms with 'content
length 31457280 exceeds maxBytes 25165824'.
Regression: headroom-bound test fails pre-fix (30MB passes the old
100MB bound); companion test proves compression headroom still works.
* fix(browser): support Chrome Web Store native bootstrap
* chore: keep browser release note in PR body
* docs(browser): document Store identity trust boundary
* docs(browser): correct Store recovery guidance
Worktree sessions now derive their worktree/branch name from the same
concise generated title the sidebar shows (bounded 8s wait, raw-prompt
slug then crustacean fallback), persisted once as displayName so the
chat-send pass never duplicates the model call. Dashboard title
scheduling moves from the dispatch chain's finally (after the whole
first turn) back to dispatch start, so long or interrupted first turns
no longer leave sessions silently untitled (regression from #122471).
slugifyWorktreeTitle truncates at word boundaries; dead
generatedDisplayName plumbing removed.
* refactor(webui): centralize chat metadata caching
* test(webui): cover shared chat metadata store
* refactor(webui): move ChatMetadataResult type to its store owner
* fix(webui): let the metadata store own revalidation lifetime
* fix(webui): satisfy no-promise-executor-return in retry delay
* refactor(ui): align approval page with redesigned approval card
Follow up #124767 by carrying severity accents and identity chips onto the standalone approval page.
Remove the unused execApproval.labels severity, plugin, and agent keys.
* refactor(ui): keep approval-page helpers in repo idiom
Reverts drive-by rewrites from the build pass: restore the exhaustive
decisionLabel switch (the ternary lost the never check), the early-return
appliedDecisionMatches, explicit gatewayCode comparisons, and align the
severity mapping shape with the sibling exec-approval-card.ts.
* test(ui): type approval severity fixtures
* test(scripts): wait for a parseable descendant pid in Bun smoke cleanup
The readiness gate only required the descendant pid file to exist, but
existsSync can observe it between open() and write(); a loaded runner
then reads an empty file and the integer assertion flakes (seen on PR
#124816 CI run 31971191374). Readiness now requires parseable content.
* fix(ui): drop the transcript keyboard-focus ring
The .chat-thread:focus-visible 2px inset outline (added in #120693) boxed
the entire main content whenever the transcript took keyboard focus
(Shift+Tab, closing transcript search) and read as a broken border that
stopped above the composer. The transcript stays keyboard-focusable for
scroll keys; it just paints no focus box. Accepted tradeoff: no visible
focus indicator on the scroll container.
* feat(ui): single primary composer button with Enter-steer and Esc-stop
The composer now renders exactly one primary action, always rightmost:
- draft or attachments present: one send-shaped button whose label follows
the session follow-up mode (Send / Queue / Steer); the stop button no
longer renders beside it
- empty while a run is active: Stop (tooltip "Stop (Esc)"); Escape in the
composer aborts the run, deferring to reply-clear and menu precedence
- empty while idle: disabled send labeled "Write a message to send."
Enter on an empty composer while a run is active steers the oldest
steerable queued message — Enter queues, Enter again steers — using the
same eligibility predicate as the queue chip's Steer action (shared
steerableQueuedMessage helper). The mic/dictation control is now always a
compact secondary control left of the primary instead of morphing into
the primary when the composer is empty. Dead ChatRunControlsProps fields
(hasMessages, onExport, onNewSession, showPrimary, showSecondary) and the
composer exportMarkdown wrapper are removed; the assertion-safety
baseline entry for chat-composer.ts shrinks 8 -> 7.
* refactor(ui): quiet the idle composer mic
The idle mic was an accent-tinted two-segment pill (mic + permanently
visible device-picker chevron) that outweighed the primary action. It now
matches the attach button's ghost weight: transparent idle, subtle hover,
and the accent treatment is reserved for the recording and talk-active
states so the idle -> recording escalation reads stronger. The device
picker chevron collapses at idle and reveals on hover/focus-within (kept
always visible on coarse pointers, where dictation matters most; reveal
transition disabled under prefers-reduced-motion). The idle tooltip now
teaches the gesture split — "Tap to talk · Hold to dictate" — while the
aria-label stays "Start voice input".
* refactor(ui): split composer keydown and oversized composer test files
chat-composer.ts crossed the 700-line oxlint budget after the keyboard
additions; the keydown handling (menu routing, history keys, Escape-stop,
Enter steer/send) moves to chat-composer-keydown.ts behind a typed deps
factory. The composer unit tests split their primary-action/keyboard
coverage into chat-composer-actions.test.ts over a shared
chat-composer.test-support.ts, and the catalog/model-discovery e2e cases
move to chat-composer-catalog.e2e.test.ts. Pure moves, no behavior
change; no max-lines suppressions or baseline entries added.
* chore(ui): keep composer test-support ComposerProps internal
The split exported a type nothing imports; the dead-export scan is a
merge gate.
* fix(ui): narrow composer keydown target and adapt keyboard tab-order proof
The keydown split moved two bare textarea assertions into a zero-baseline
file; a single instanceof narrowing at the handler entry replaces both
(the handler only binds to the composer textarea). chat-composer.ts's own
assertion baseline ratchets 7 -> 5. The pointer-activation e2e now walks
the real post-redesign tab order (mic, focus-revealed picker trigger,
then primary send) instead of assuming one Tab.
* fix(ui): gate Enter-steer on the queue chip's visible availability
ClawSweeper P1: the Enter path only required an abortable run, so
offline Enter swallowed the keypress and invoked a steer lifecycle that
returns at !connected with no visible outcome, while the queue chip's
Steer button was correctly hidden behind connected && canCompose. The
keydown path now mirrors the chip's exact boundary (connected, canSend,
abortable, visible queue surface including sessionKey-less pending
rows).