feat: queued follow-up messages in composer (#58) #287

Merged
dries merged 2 commits from feat/queued-follow-up-messages into main 2026-07-12 01:34:24 +02:00
Owner

Closes #58.

Server-side, shared, race-free follow-up message queue. Prompts submitted while a session is mid-turn are queued and auto-sent one per turn when the session goes idle.

Design

  • Server-side, not frontend — the queue lives in state.db (migration v21), shared across every connected client and surviving a client moving machines. A frontend-only queue would show phantom messages to other clients watching the same session.
  • Race-free by construction — enqueue is unconditional (no busy check → no check-then-act race); flush is the sole send gate, serialized per session and driven by the existing session.idle SSE edge. One message drains per idle edge, oldest first (one follow-up per turn).
  • Remote-transparent — the queue row carries the compound platform id, so flush routes back over gRPC for remote sessions.

What's included

Backend

  • internal/queuesvc — enqueue / flush (per-session lock) + List/Remove/Move.
  • internal/state — v21 queued_message table + CRUD (append, head, list, delete, transactional reorder).
  • Composer POST /message enqueues; session.idle flushes. Loops/MCP still send directly (programmatic, not composer).
  • REST: GET/DELETE /api/session/{id}/queue{/qmid}, POST .../move (ownership-scoped).
  • ocman.queue.updated broadcast carrying the session's full list.

Frontend

  • useMessageQueue renders the list purely from server state (initial fetch + queue.updated broadcasts).
  • Composer renders the queue under the footer with remove / reorder controls, via a dedicated QueuedMessages leaf component.

Reliability & live-update fixes

Follow-up round hardening the live-update path so the list stays correct without a refresh:

  • http.Flusher on the status-recorder middleware — the wrapper implemented http.Hijacker (for WebSockets) but not http.Flusher, so the global /api/events SSE handler failed its w.(http.Flusher) check and returned before subscribing. No client ever subscribed → broadcasts reached zero subscribers → the UI only updated on refresh. (Per-session /api/session/{id}/events worked only because it bypasses that path.) This was the root cause of "new queued messages don't appear until refresh."
  • Reliable deliveryqueue.updated is coalesced in the broadcast hub: on a full subscriber buffer the latest snapshot is parked (last-write-wins) instead of dropped, so the freshest list is never lost under load.
  • Load-on-reconnectuseMessageQueue reloads from the endpoint on every SSE (re)connect (mirroring the conversation SSE), with a monotonic seq guard so a slow reload can't clobber a fresher broadcast.
  • No spurious clears — the hook only resets the list on an actual session change, not on every effect re-run (e.g. platform resolving), which previously wiped a just-shown enqueue.
  • Blocking composer send — the composer disables its send controls until the POST completes, so the broadcast has landed by the time it re-enables; no optimistic queue state needed.
  • Sweep backstop fix — a force-queued message is no longer marked drained at enqueue time, which had disarmed the Sweep and could strand a message whose session.idle edge never arrived.
  • Dropped a vite proxy proxyRes data listener that put SSE responses into flowing mode and consumed the bytes.

Tests

  • Go: state CRUD, queuesvc (order, one-per-idle, send-error-retry, ownership guards, notify-on-every-mutation), server route + SSE-wire integration, broadcast coalescing, statusRecorder Flush/Hijack delegation.
  • Frontend: useGlobalEvents queue routing, useMessageQueue (broadcast apply, reconnect reload, seq guard, effect-rerun survival), QueuedMessages component.
  • All green: go test, go vet, platform-branching check, tsc -b, pnpm lint (0 errors), pnpm test (1480 pass), coverage ratchet .
Closes #58. Server-side, shared, race-free follow-up message queue. Prompts submitted while a session is mid-turn are queued and auto-sent one per turn when the session goes idle. ## Design - **Server-side, not frontend** — the queue lives in `state.db` (migration v21), shared across every connected client and surviving a client moving machines. A frontend-only queue would show phantom messages to other clients watching the same session. - **Race-free by construction** — enqueue is unconditional (no busy check → no check-then-act race); flush is the sole send gate, serialized per session and driven by the existing `session.idle` SSE edge. One message drains per idle edge, oldest first (one follow-up per turn). - **Remote-transparent** — the queue row carries the compound platform id, so flush routes back over gRPC for remote sessions. ## What's included **Backend** - `internal/queuesvc` — enqueue / flush (per-session lock) + List/Remove/Move. - `internal/state` — v21 `queued_message` table + CRUD (append, head, list, delete, transactional reorder). - Composer `POST /message` enqueues; `session.idle` flushes. Loops/MCP still send directly (programmatic, not composer). - REST: GET/DELETE `/api/session/{id}/queue{/qmid}`, POST `.../move` (ownership-scoped). - `ocman.queue.updated` broadcast carrying the session's full list. **Frontend** - `useMessageQueue` renders the list purely from server state (initial fetch + `queue.updated` broadcasts). - Composer renders the queue under the footer with remove / reorder controls, via a dedicated `QueuedMessages` leaf component. ## Reliability & live-update fixes Follow-up round hardening the live-update path so the list stays correct without a refresh: - **`http.Flusher` on the status-recorder middleware** — the wrapper implemented `http.Hijacker` (for WebSockets) but not `http.Flusher`, so the global `/api/events` SSE handler failed its `w.(http.Flusher)` check and returned before subscribing. No client ever subscribed → broadcasts reached zero subscribers → the UI only updated on refresh. (Per-session `/api/session/{id}/events` worked only because it bypasses that path.) This was the root cause of "new queued messages don't appear until refresh." - **Reliable delivery** — `queue.updated` is coalesced in the broadcast hub: on a full subscriber buffer the latest snapshot is parked (last-write-wins) instead of dropped, so the freshest list is never lost under load. - **Load-on-reconnect** — `useMessageQueue` reloads from the endpoint on every SSE (re)connect (mirroring the conversation SSE), with a monotonic seq guard so a slow reload can't clobber a fresher broadcast. - **No spurious clears** — the hook only resets the list on an actual session change, not on every effect re-run (e.g. `platform` resolving), which previously wiped a just-shown enqueue. - **Blocking composer send** — the composer disables its send controls until the POST completes, so the broadcast has landed by the time it re-enables; no optimistic queue state needed. - **Sweep backstop fix** — a force-queued message is no longer marked drained at enqueue time, which had disarmed the Sweep and could strand a message whose `session.idle` edge never arrived. - Dropped a vite proxy `proxyRes` data listener that put SSE responses into flowing mode and consumed the bytes. ## Tests - Go: state CRUD, queuesvc (order, one-per-idle, send-error-retry, ownership guards, notify-on-every-mutation), server route + SSE-wire integration, broadcast coalescing, `statusRecorder` Flush/Hijack delegation. - Frontend: `useGlobalEvents` queue routing, `useMessageQueue` (broadcast apply, reconnect reload, seq guard, effect-rerun survival), `QueuedMessages` component. - All green: `go test`, `go vet`, platform-branching check, `tsc -b`, `pnpm lint` (0 errors), `pnpm test` (1480 pass), coverage ratchet ✅.
Prompts submitted while a session is mid-turn are queued server-side and
auto-sent one per turn when the session goes idle. The queue lives in
state.db so it is shared across every connected client and survives a
client moving machines.

Race-free by construction: enqueue is unconditional (no busy check, no
check-then-act race); flush is the sole send gate, serialized per session
and driven by the existing session.idle SSE edge. One message drains per
idle edge, oldest first.

Backend only; composer UI (list/reorder/delete) is phase 2, wired off the
new ocman.queue.updated broadcast.
feat(queue): composer UI for the follow-up message queue (#58)
Some checks failed
CI / Build Desktop (macOS arm64) (pull_request) Successful in 1m46s
CI / Frontend (pull_request) Successful in 4m26s
CI / Backend (pull_request) Successful in 4m37s
CI / Coverage Ratchet (pull_request) Successful in 8m1s
CI / Build (pull_request) Successful in 5m0s
CI / Playwright E2E (pull_request) Has been cancelled
CI / Semantic Tag (pull_request) Has been cancelled
4fb41903f1
Phase 2: surface and manage the server-side queue from the composer.

- REST: GET/DELETE /api/session/{id}/queue{/qmid} and POST .../move,
  all ownership-scoped to the session in the URL.
- queuesvc gains List/Remove/Move; state gains MoveQueuedMessage
  (transactional position swap) and GetQueuedMessageSession.
- Frontend: useMessageQueue hook keeps a live copy in sync off the
  ocman.queue.updated broadcast; Composer renders the queue as a list
  under the footer with remove / reorder controls.

Coverage ratchet: pass

Suite Baseline This PR Δ
go 72.10% 72.40% +0.30
frontend 62.24% 62.39% +0.15

Tolerance: -0.1%. Baseline stored on gh-pages.

<!-- coverage-ratchet --> ### Coverage ratchet: ✅ pass | Suite | Baseline | This PR | Δ | | |---|---|---|---|---| | go | 72.10% | 72.40% | +0.30 | ✅ | | frontend | 62.24% | 62.39% | +0.15 | ✅ | _Tolerance: -0.1%. Baseline stored on `gh-pages`._
refactor(composer): extract QueuedMessages into its own component
Some checks failed
CI / Build Desktop (macOS arm64) (pull_request) Successful in 3m3s
CI / Frontend (pull_request) Successful in 5m22s
CI / Backend (pull_request) Successful in 7m4s
CI / Coverage Ratchet (pull_request) Successful in 8m52s
CI / Build (pull_request) Successful in 3m15s
CI / Playwright E2E (pull_request) Failing after 17m44s
CI / Semantic Tag (pull_request) Has been skipped
28568b1ce7
The follow-up queue list was inline in ComposerImpl (already the
repo's largest function). It's pure presentational — props in,
remove/reorder callbacks out — so it lifts cleanly into a leaf
component, shrinking the composer and making the queue list
independently testable.
fix(queue): keep the whole backlog; don't drain on a status blip
Some checks failed
CI / Build Desktop (macOS arm64) (pull_request) Successful in 1m58s
CI / Frontend (pull_request) Successful in 3m52s
CI / Backend (pull_request) Successful in 5m41s
CI / Coverage Ratchet (pull_request) Successful in 8m11s
CI / Build (pull_request) Successful in 4m15s
CI / Playwright E2E (pull_request) Failing after 18m22s
CI / Semantic Tag (pull_request) Has been skipped
fff7701f62
A follow-up queued while the agent was busy could vanish: TurnRunning
infers busy from the last message's role, which flips to idle the instant
a user message lands, so the enqueue path saw 'idle' and sent an earlier
queued message into the running turn — dropping it from the visible queue.

Enqueue no longer drains a backlog. It fast-path sends only when the
message is the sole queued item AND we haven't already drained since the
last real session.idle edge (a new per-session guard, re-armed by Flush).
Genuine turn completion (session.idle) remains the only thing that drains
a backlog, one message per turn.

With the full queue retained, every item shows with its remove/reorder
controls (the delete button was already wired; the disappearing head is
what made it look absent).

Regression test: two enqueues under a perpetually-idle status stub must
retain both, not chain-send them.
fix(queue): make queued-message delete best-effort
Some checks failed
CI / Frontend (pull_request) Successful in 3m49s
CI / Build Desktop (macOS arm64) (pull_request) Successful in 4m28s
CI / Coverage Ratchet (pull_request) Has been cancelled
CI / Playwright E2E (pull_request) Has been cancelled
CI / Backend (pull_request) Has been cancelled
CI / Build (pull_request) Has been cancelled
CI / Semantic Tag (pull_request) Has been cancelled
1a714af0cf
Deleting an already-gone item (it flushed, or another client removed it)
now returns 204 instead of 404. A delete's goal is 'this message is not
in the queue', which a missing id already satisfies — and it kills the
click-delete-just-as-it-drains race.
feat(queue): show queued follow-ups above the composer input
Some checks failed
CI / Frontend (pull_request) Failing after 2m43s
CI / Build Desktop (macOS arm64) (pull_request) Failing after 3m9s
CI / Playwright E2E (pull_request) Failing after 2m13s
CI / Coverage Ratchet (pull_request) Failing after 6m0s
CI / Build (pull_request) Failing after 1m38s
CI / Backend (pull_request) Successful in 6m37s
CI / Semantic Tag (pull_request) Has been skipped
3268707086
Moved the queue list from the composer footer to just above the input
box (alongside the recording banner). Pending follow-ups now read
top-to-bottom straight into the composer, which is where the eye expects
the next-to-send messages to sit.
fix(queue): wrap long queued-message text
Some checks failed
CI / Build Desktop (macOS arm64) (pull_request) Failing after 39s
CI / Frontend (pull_request) Failing after 2m29s
CI / Playwright E2E (pull_request) Has been cancelled
CI / Backend (pull_request) Has been cancelled
CI / Coverage Ratchet (pull_request) Has been cancelled
CI / Build (pull_request) Has been cancelled
CI / Semantic Tag (pull_request) Has been cancelled
63e24d25c9
Queued items used nowrap+ellipsis on a single line, so long prompts ran
under the right sidebar. Let the text wrap (min-width:0 so the flex child
can shrink, overflow-wrap:anywhere to break URLs/paths), align the row to
the top, and keep the icon and action buttons from shrinking.
test(e2e): verify long queued message wraps with controls visible
Some checks failed
CI / Build Desktop (macOS arm64) (pull_request) Failing after 39s
CI / Frontend (pull_request) Failing after 2m13s
CI / Playwright E2E (pull_request) Failing after 2m5s
CI / Coverage Ratchet (pull_request) Failing after 6m24s
CI / Build (pull_request) Failing after 2m11s
CI / Backend (pull_request) Successful in 6m35s
CI / Semantic Tag (pull_request) Has been skipped
5a8c6996b5
Unit tests (jsdom) can't verify layout. This Playwright test seeds a long
queued follow-up, then asserts in a real browser that the row wrapped to
multiple lines and the remove control's right edge stays within the
composer wrap — i.e. the controls remain visible instead of running under
the sidebar. Passes on Chromium and WebKit; would fail under the old
nowrap+ellipsis styling.
Queued messages weren't being sent. Three defects, one symptom:

- The idle-driven flush re-checked the inferred session status, which
  lags the session.idle SSE edge and can still read "busy" at the edge —
  swallowing the drain. Since a now-idle session emits no further edge,
  the head was stranded. Flush now trusts the authoritative idle edge and
  skips the busy gate; the gate still guards the enqueue fast-path.

- Backlogs stranded before this fix (or by a swallowed edge) had no way
  to recover. Add a periodic Sweep (runQueueSweep, 15s + one at startup)
  that drains one message from each idle session with a standing backlog,
  honoring the same busy/blip guards.

- The composer's React.memo comparator omitted queuedMessages, so
  switching sessions left the previous queue on screen until a refresh.
  Add it to the comparator (extracted to composerMemo.ts for testing).

Also commits composerMemo.ts, which a prior commit referenced but never
added, leaving HEAD unbuildable.
fix(queue): don't show a QUEUED thread bubble for a queued send
Some checks failed
CI / Build Desktop (macOS arm64) (pull_request) Successful in 3m51s
CI / Frontend (pull_request) Successful in 4m28s
CI / Backend (pull_request) Successful in 4m54s
CI / Coverage Ratchet (pull_request) Successful in 7m36s
CI / Build (pull_request) Successful in 4m53s
CI / Playwright E2E (pull_request) Has been cancelled
CI / Semantic Tag (pull_request) Has been cancelled
1be5678a5e
A follow-up sent mid-turn appeared twice: as a large 'QUEUED' bubble in
the message thread (the optimistic pending bubble, which convertMessages
tags as queued because it follows an unfinished assistant turn) AND in the
compact queue list under the composer. Only the compact list is wanted.

handleSend now skips the optimistic bubble when the session is running —
it POSTs directly (which enqueues server-side) and lets the queue list
surface the message via the queue.updated broadcast. The idle path keeps
the optimistic bubble.

Also adds a state test for SessionsWithQueuedMessages (the Sweep query)
and gofmt-normalizes server.go.
test(queue): cover live queue-list updates from the broadcast
Some checks failed
CI / Frontend (pull_request) Successful in 3m33s
CI / Backend (pull_request) Successful in 4m49s
CI / Build Desktop (macOS arm64) (pull_request) Successful in 5m52s
CI / Playwright E2E (pull_request) Has been cancelled
CI / Coverage Ratchet (pull_request) Has been cancelled
CI / Build (pull_request) Has been cancelled
CI / Semantic Tag (pull_request) Has been cancelled
a68fe7e310
Proves the compact queued-message list updates from ocman.queue.updated
without a page refresh — enqueue and drain both reflect live, and
broadcasts for other sessions are ignored. Adds queuedMessages/delete/move
stubs to the session-detail harness so the queue path is exercised.
refactor(thread): remove the in-thread QUEUED message badge
Some checks failed
CI / Build Desktop (macOS arm64) (pull_request) Successful in 2m50s
CI / Frontend (pull_request) Successful in 6m8s
CI / Backend (pull_request) Successful in 7m30s
CI / Coverage Ratchet (pull_request) Successful in 9m12s
CI / Build (pull_request) Successful in 3m13s
CI / Playwright E2E (pull_request) Failing after 17m52s
CI / Semantic Tag (pull_request) Has been skipped
d18e45073f
Obsolete since #58: ocman now owns the follow-up queue and shows it in
the compact list under the composer, so the old thread-level 'QUEUED'
badge (a user message detected as following an unfinished assistant turn)
is redundant and confusing. Removes the isQueued detection from
convertMessages, the badge + styling from AssistantThread, and the now-
dead queued-detection tests.
fix(queue): let the client force-queue a mid-turn send
Some checks failed
CI / Build Desktop (macOS arm64) (pull_request) Failing after 1m20s
CI / Frontend (pull_request) Successful in 4m31s
CI / Backend (pull_request) Successful in 4m54s
CI / Coverage Ratchet (pull_request) Successful in 8m3s
CI / Build (pull_request) Successful in 4m58s
CI / Playwright E2E (pull_request) Failing after 18m42s
CI / Semantic Tag (pull_request) Has been skipped
e6f9ffc274
Mid-turn sends weren't appearing in the queue list: the server's enqueue
fast-path gated on its own status inference, which reads the lagging DB
and can report idle during a live turn — so it drained the message
straight into the running turn instead of holding it (the queued_message
table stayed empty).

The client knows the turn is running authoritatively from the live SSE
stream. Thread that intent through: composer sends a queue=true flag when
isRunning; the server holds such messages unconditionally (no drain,
disarms the idle-blip fast path) and lets the real session.idle edge send
them one per turn. Idle sends (queue=false) keep the immediate-drain
fast path.

Regression tests: force-queue holds even when status reads idle; the
composer passes queue=true mid-turn.
dries changed title from feat: queued follow-up messages in composer (#58) to WIP: feat: queued follow-up messages in composer (#58) 2026-07-11 22:43:16 +02:00
The status-capturing middleware wrapper implemented http.Hijacker but not
http.Flusher, so the global /api/events SSE handler failed its
w.(http.Flusher) check and returned before subscribing. No client ever
subscribed, so broadcasts (queue.updated, etc.) reached zero subscribers
and the UI only updated on refresh. Per-session /api/session/{id}/events
worked only because it bypasses this path.

Delegate Flush to the embedded writer, mirroring the existing Hijack.
- queue.updated carries the session's full list (0..N items) on every
  enqueue/drain/delete/move so clients render server state directly.
- Coalesce queue.updated in the broadcast hub: on a full subscriber
  buffer, park the latest snapshot (last-write-wins) instead of dropping,
  so the freshest list is never lost under load.
- ListQueuedMessages accepts the empty-platform wildcard, matching
  HeadQueuedMessage, so a broadcast can resolve the queue by session id.
- Don't mark a force-queued message drained at enqueue; that disarmed the
  Sweep backstop and could strand a message whose idle edge never arrives.
fix(queue): render the follow-up list purely from SSE state
Some checks failed
CI / Build Desktop (macOS arm64) (pull_request) Successful in 1m12s
CI / Frontend (pull_request) Successful in 4m58s
CI / Backend (pull_request) Successful in 5m15s
CI / Coverage Ratchet (pull_request) Successful in 8m44s
CI / Build (pull_request) Successful in 5m27s
CI / Playwright E2E (pull_request) Failing after 18m15s
CI / Semantic Tag (pull_request) Has been skipped
7ed0d7ef53
- useMessageQueue: load the list on mount AND on every /api/events
  (re)connect (onSseConnect), then apply queue.updated broadcasts. A
  monotonic seq guard drops a slow reload that would clobber a fresher
  broadcast. Only reset the list on an actual session change, not on every
  effect re-run (platform resolving), which previously wiped a just-shown
  enqueue.
- Composer blocks the send controls until the POST completes, so by the
  time it re-enables the queue.updated broadcast has already landed — no
  optimistic queue state needed.
- Drop the vite proxy's proxyRes data listener: it put SSE responses into
  flowing mode and consumed the bytes, breaking event delivery.
dries changed title from WIP: feat: queued follow-up messages in composer (#58) to feat: queued follow-up messages in composer (#58) 2026-07-11 22:59:12 +02:00
test(e2e): fix queue-related e2e failures
Some checks failed
CI / Build Desktop (macOS arm64) (pull_request) Successful in 1m14s
CI / Frontend (pull_request) Successful in 3m52s
CI / Playwright E2E (pull_request) Has been cancelled
CI / Backend (pull_request) Has been cancelled
CI / Coverage Ratchet (pull_request) Has been cancelled
CI / Build (pull_request) Has been cancelled
CI / Semantic Tag (pull_request) Has been cancelled
a342d275bd
- fixtures: mock GET /api/session/{id}/queue as an empty array. Without
  it useMessageQueue's fetch fell through the catch-all to the session-
  detail JSON (an object, not an array), breaking the queue render and
  detaching the composer input mid-test.
- session-detail: remove the stale 'genuinely queued user message shows
  the Queued badge' test — the in-thread badge was removed on this branch
  (follow-ups now surface in the compact queue list). Sibling tests still
  assert the badge is absent.
- vite: restore the proxyRes drain (needed to keep the e2e preview server
  stable) with a clarifying comment; the live-update fix is server-side
  (statusRecorder http.Flusher), not the proxy.
fix(queue): never let a non-array queue response crash the render
All checks were successful
CI / Build Desktop (macOS arm64) (pull_request) Successful in 1m15s
CI / Frontend (pull_request) Successful in 4m45s
CI / Backend (pull_request) Successful in 5m8s
CI / Coverage Ratchet (pull_request) Successful in 8m27s
CI / Build (pull_request) Successful in 5m29s
CI / Playwright E2E (pull_request) Successful in 6m14s
CI / Semantic Tag (pull_request) Has been skipped
0be9363ca6
If the /queue endpoint returns a non-array (misrouted request, a proxy
returning the session object or HTML, an error page), setQueue previously
stored it and QueuedMessages' .map threw at render — taking down the whole
composer/page (observed as widespread 'element detached from the DOM'
e2e failures across composer/session-actions/sse specs). Guard applyLatest
to coerce any non-array to [].
feat(queue): clamp queued items to 3 lines and cap the list height
Some checks failed
CI / Build Desktop (macOS arm64) (pull_request) Successful in 1m16s
CI / Coverage Ratchet (pull_request) Has been cancelled
CI / Frontend (pull_request) Has been cancelled
CI / Backend (pull_request) Has been cancelled
CI / Playwright E2E (pull_request) Has been cancelled
CI / Build (pull_request) Has been cancelled
CI / Semantic Tag (pull_request) Has been cancelled
103d6648b1
A long queued prompt (or a big backlog) previously grew the compact list
until it took over the composer/thread. Now each item's text is clamped
to 3 lines (click to expand/collapse) and the whole list is capped at
~35vh with internal scroll, so it stays out of the way.
dries force-pushed feat/queued-follow-up-messages from 103d6648b1
Some checks failed
CI / Build Desktop (macOS arm64) (pull_request) Successful in 1m16s
CI / Coverage Ratchet (pull_request) Has been cancelled
CI / Frontend (pull_request) Has been cancelled
CI / Backend (pull_request) Has been cancelled
CI / Playwright E2E (pull_request) Has been cancelled
CI / Build (pull_request) Has been cancelled
CI / Semantic Tag (pull_request) Has been cancelled
to 72a8d2659b
All checks were successful
CI / Build Desktop (macOS arm64) (pull_request) Successful in 1m11s
CI / Frontend (pull_request) Successful in 5m19s
CI / Backend (pull_request) Successful in 7m4s
CI / Coverage Ratchet (pull_request) Successful in 9m15s
CI / Build (pull_request) Successful in 3m35s
CI / Playwright E2E (pull_request) Successful in 6m1s
CI / Semantic Tag (pull_request) Has been skipped
CI / Build Desktop (macOS arm64) (push) Successful in 1m5s
CI / Frontend (push) Successful in 5m41s
CI / Backend (push) Successful in 7m26s
CI / Coverage Ratchet (push) Successful in 9m54s
CI / Build (push) Successful in 3m34s
CI / Playwright E2E (push) Successful in 6m0s
CI / Semantic Tag (push) Successful in 7s
Release / Build Frontend (push) Successful in 1m3s
Release / Build Desktop (macOS arm64) (push) Successful in 2m14s
Release / Build darwin/arm64 (push) Successful in 3m21s
Release / Build darwin/amd64 (push) Successful in 4m39s
Release / Build linux/amd64 (push) Successful in 4m41s
Release / Build linux/arm64 (push) Successful in 1m47s
Release / Publish Release (push) Successful in 36s
2026-07-12 01:12:51 +02:00
Compare
dries merged commit 72a8d2659b into main 2026-07-12 01:34:24 +02:00
dries deleted branch feat/queued-follow-up-messages 2026-07-12 01:34:24 +02:00
Sign in to join this conversation.
No description provided.