Skip to content

fix(tui): load the catalog without waiting for the event stream - #353

Merged
shuv1337 merged 1 commit into
integration-v2from
catalog-stream-decouple
Aug 10, 2026
Merged

fix(tui): load the catalog without waiting for the event stream#353
shuv1337 merged 1 commit into
integration-v2from
catalog-stream-decouple

Conversation

@shuv1337

Copy link
Copy Markdown
Collaborator

Fixes #351.

Summary

The TUI's provider and model lists only synced after the SSE stream reported server.connected. That handshake is bounded by a 2s connectTimeout armed before ~2s of blocking startup work on the same JS thread, so on a compiled build it almost always aborted a connection that was never unhealthy; the model switcher showed No items available and the footer No provider selected until the retry landed. Measured 4.6–4.9s to first model on a static transport and 5.4–12.4s on a managed one, against a server that had published catalog.updated at 450ms.

Changes

  • Decouple the catalog from the stream. Fetch on location set — those reads are plain HTTP and never depended on the stream. server.connected still resyncs, and DataProvider already drops the cached completion whenever the stream is down, so a healthy start costs exactly one fetch.
  • Retry an unestablished handshake against the endpoint already resolved rather than re-resolving the managed service first (ServiceLifecycle.ensure() was the source of the multi-second variance). A stream that established and then dropped still re-resolves, since a restarted server may have moved.
  • Distinguish an unfetched model list from an empty one so the dialog shows Loading models… instead of No items available.

Validation

  • bun typecheck clean in packages/tui
  • 600 tests pass, including two regression tests that fail on pristine source:
    • loads the catalog before the event stream connects (test/cli/tui/data.test.tsx)
    • retries an unestablished handshake before re-resolving the server (test/cli/tui/use-event.test.tsx)

Out of scope (tracked in #351)

The 2s connectTimeout still measures wall-clock across a potentially blocked event loop; arming it after dispatch is a separate repair. Upstream v2 carries the same defects, so expect a conflict at the next sync.

The provider and model lists only synced once the SSE stream reported
server.connected. That handshake is bounded by a 2s connectTimeout armed
before roughly 2s of blocking startup work, so on a compiled build it
almost always aborted spuriously: 7 of 8 cold launches burned the full
timeout, then paid reconnect backoff before any catalog request went out.
Measured time-to-model was 4.6-4.9s on a static transport and 5.4-12.4s on
a managed one, against a server that had published catalog.updated at
450ms.

Fetch the catalog on location set, since those reads are plain HTTP and
never depended on the stream; server.connected still resyncs, and
DataProvider already drops the cached completion whenever the stream is
down, so a healthy start costs one fetch.

Retry an opening handshake that never established against the endpoint
just resolved instead of re-resolving the managed service first, which
was the source of the multi-second variance. A stream that established
and then dropped still re-resolves, since a restarted server may have
moved.

Distinguish an unfetched model list from an empty one so the dialog shows
'Loading models…' rather than 'No items available'.
@shuv1337
shuv1337 merged commit 49116c8 into integration-v2 Aug 10, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shuvbot found no summary-only findings.

@shuv1337
shuv1337 deleted the catalog-stream-decouple branch August 10, 2026 21:52
shuv1337 added a commit that referenced this pull request Aug 13, 2026
Merge anomalyco/opencode upstream/v2 (283258e, 187 commits) into
integration-v2 (00e7c54), aligning with upstream everywhere except
fork-critical features, per PLAN-upstream-v2-merge.md.

Preserved fork features: identity/publish/updater/launcher, managed
service lifecycle (ServiceLifecycle.ensure everywhere), Anthropic Claude
Pro/Max subscription provider, structured output, session tool policy,
strict prompt retry identity (durable admitted-event reconciliation),
mobile pairing removal, and prior fork fixes (#350/#353/#354).

Adopted from upstream: session_v2 rename, skills prompt attachments,
plugin hook rename request -> http.request/http.response, new CLI
commands (models/export/import/debug config), workspace_domain,
legacy-credential import, docs/app/desktop changes.

Database bridge (M2): DatabaseMigration.apply now detects a fork-tip
session table (fork_boundary column, tool-policy migration present, no
session_v2) and renames it to session_v2 in one transaction before
upstream migrations run, rebuilding session_v2_* indexes and marking
the upstream squash migration as already applied so its destructive SQL
never executes. Validated against a real dev DB copy (12 sessions/16
messages/86 events preserved) and via a fork-tip fixture test.

Data-safety corrections (M3): V1Migration no longer globally wipes
events when V2 sessions already exist (gated on empty session_v2);
imported legacy Anthropic OAuth credentials now map to methodID
claude-pro-max.

CLI/service wiring (M4): new upstream handlers registered without pair;
every server-start path (auth login, debug config, server-connection)
routes through ServiceLifecycle.ensure.

Regeneration & docs (M5): protocol/client/www regenerated; publish.yml
gains a github.ref_name == integration-v2 guard; merged docs rebranded
to Shuvcode while preserving SDK/API symbols; migrate-v1.mdx documents
the DB bridge, scoped event deletion, and the hook rename; AGENTS.md
records the session_v2/bridge and hook-name invariants.

Upstream-bug follow-ups (M6): symlinked mutation targets now resolve
through the nearest existing ancestor's real path before authorization,
closing an external-directory approval bypass; desktop background-cli
prefers the canonical managed state root over the first running
candidate; failed server replacement no longer removes existing tabs.

Validated: full per-package typecheck+test pass (schema, protocol,
core, server, client, cli, tui, app) plus a live smoke test against an
isolated standalone server pointed at a scratch copy of a real dev
database, confirming existing sessions/transcripts render and that a
live prompt round-trips both structured output and a skill attachment.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

TUI: provider/model list blocked behind the SSE handshake (5-12s to first model)

1 participant