Skip to content

[SUPERSEDED by ymichael/bb#66] feat(app): sidebar Back/Forward route-history controls#1

Closed
brsbl wants to merge 293 commits into
mainfrom
bb/implement-left-panel-history-navigation-buttons-thr_ysusjqansr
Closed

[SUPERSEDED by ymichael/bb#66] feat(app): sidebar Back/Forward route-history controls#1
brsbl wants to merge 293 commits into
mainfrom
bb/implement-left-panel-history-navigation-buttons-thr_ysusjqansr

Conversation

@brsbl
Copy link
Copy Markdown
Owner

@brsbl brsbl commented Jun 5, 2026

Summary

Adds browser-style Back and Forward navigation buttons to the left sidebar that move through the bb app-shell route history. This is app-route history only — it does not touch the in-thread browser tab history in BrowserTabContent.

The controls live in the sidebar primary-actions area, above New Thread / New Manager (keeping them clear of the macOS titlebar/traffic-light drag region and hidden in the icon-collapsed sidebar).

What changed

  • apps/app/src/lib/app-route-history.ts — new useAppRouteHistoryNavigation hook. Tracks the actual React Router entries visited while mounted, including duplicate same-URL pushes (distinct location.keys):
    • PUSH appends after the current slot and drops the forward stack.
    • REPLACE overwrites the current slot (no extra Back entry).
    • POP reconciles to a known slot by location.key; an unrecorded key is treated as the app-owned history boundary (stack resets to the current route) so the controls never step into unrecorded/off-app history.
    • canGoBack / canGoForward and goBack / goForward skip equal-URL slots and move by a real navigate(delta), so one click lands on the nearest visibly different route (no fake no-op steps) while preserving router/browser state.
  • apps/app/src/components/sidebar/SidebarHistoryNavigationControls.tsx — two accessible icon buttons (Go back / Go forward) built on the Button primitive + sidebar tokens, native disabled, aria-hidden icons, type="button", matching title/aria-label. Exposes an onNavigate callback so the sidebar can close the mobile drawer after an enabled press. Owns its own row spacing.
  • apps/app/src/components/sidebar/AppSidebar.tsx — renders the controls inside app-sidebar-primary-actions above ProjectListActionButtons, passing closeOnMobile as onNavigate. Existing top-reserve / macOS chrome behavior is untouched.

Tests

  • SidebarHistoryNavigationControls.test.tsx (10 tests): initial disabled state, accessibility attributes, push enables Back only, Back→Forward URL + disabled-state updates, duplicate same-URL skipping (A→B→B: one Back lands on A, one Forward returns to B), forward-stack clearing (A→B→C, Back, push D), replace updates the slot, native POP reconciles to a known entry, unknown POP boundary, and the onNavigate (drawer-close) contract.
  • AppLayout.test.tsx: controls render above the primary actions, Back before Forward, with exactly one sidebar toggle (desktop chrome unchanged). Existing chrome/resize tests remain valid.

Validation

  • pnpm exec turbo run typecheck --filter=@bb/app — pass
  • pnpm exec turbo run lint --filter=@bb/app — pass
  • pnpm exec turbo run test --filter=@bb/app --force — 910 passed (139 files), including the new suites

Live browser UI verification was not run: no browser-automation tooling is available in this environment, and the only running server is the installed prod build, which does not include these worktree changes. Behavior is covered by the automated tests above.

🤖 Generated with Claude Code

codex and others added 30 commits May 27, 2026 22:23
Dragging the Threads/Projects sections in the sidebar looked janky from
three separate dnd-kit issues:

- Sections scaled/zoomed because `CSS.Transform.toString` emits the
  scaleX/scaleY dnd-kit computes for differently-sized items. Switch to
  `CSS.Translate.toString` so only translation is applied.
- A dragged section vanished behind the other section's rows: each
  section's sticky header creates its own stacking context
  (`isolation: isolate`), so without an elevated z-index the overlapped
  rows painted on top. Lift the dragged section (and project rows) with
  position: relative + zIndex while dragging.
- Reordering required over-dragging because `closestCenter` keys off the
  dragged element's center, which sits far below the cursor for the tall
  Threads section. Use a pointer-first collision strategy so the swap
  triggers where the cursor actually is.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… pnpm store

electron-builder's npmRebuild rebuilt better-sqlite3 through the workspace
symlink into the shared content-addressed pnpm store, flipping the binary to
Electron's ABI and SIGKILLing every plain-node consumer (the @bb/server test
suite) until it was rebuilt back.

Disable npmRebuild and fetch the Electron prebuild into the packaged copy in the
existing afterPack hook instead, so the shared store keeps the node-ABI binary
and only the packaged app gets the Electron-ABI one (the server runs via
ELECTRON_RUN_AS_NODE, so it needs Electron's ABI).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The runbook only covered the npm package, so releases shipped bb-app to npm
while the desktop app (published by the separate build-desktop.yml workflow)
stayed on the previous version. Document that a release publishes both outputs
from the same commit, add the build-desktop dispatch + verification steps, and
cover the desktop-specific failure modes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…s + status app + bb app CLI

Per-thread Apps system foundation (no frontend yet): apps/<id>/ layout
(manifest.json + served assets/ + file-based data/ + logo.*), daemon-owned
asset serving + data/ watcher, server routes (entry/assets/icon/data CRUD/
message + WS relay on thread:<id>:app:<id>:data), injected window.bb bridge
(capability-gated, advisory for v1), default seeded `status` app, and
bb app new|list|open|rm. STATUS remains in place; teardown is Phase 3.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ch + app tabs

Frontend for the Apps system (additive; STATUS UI left intact for Phase 3):
- App tab kind + pinned `status` app tab in the secondary panel
- "+" launcher listing the thread's apps with resolved icons
- Unified app + file search (Apps + Files sections)
- App tab content branches on entry kind: HTML → iframe + injected window.bb,
  Markdown → static react-markdown
- Live data updates via window.bb.data.onChange (with replay/unsubscribe
  lifecycle guard), shared markdown asset URL transform, centralized STATUS_APP_ID

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Removes the legacy STATUS system end to end (no backwards compat) and makes
the Apps system the only path:
- Delete STATUS server routes/injection/status-data/status-version, the WS hub
  status method, the daemon status commands (HOST_DAEMON_PROTOCOL_VERSION→26),
  the frontend manager-status surface + useThreadStatusVersion, and all
  STATUS-specific tests.
- Rewrite manager/thread system prompts + instructions to the apps model
  (apps/status, data/state.json, window.bb, bb app CLI).
- bb-guide-status-state → bb-guide-app (bb guide app); fold bb guide styling
  into it; regenerate templates. New managers seed only the `status` app.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Guide for migrating an existing manager/thread off the removed STATUS surface
(STATUS.html/.md/STATUS folder + STATUS-data) to a `status` app: target layout,
manifest, state consolidation into data/state.json, the bbStatusState→window.bb
and bbThreadTell→window.bb.message API mapping, agent/maintainer direct-fs
writes, and cleanup. Points to `bb guide app` for the full reference.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… on unmatched

Replace `/api/v1/threads/:id/apps/:appId/assets/*` with the canonical flat
`/api/v1/threads/:id/apps/:appId/*` wildcard → filesystem
`apps/<appId>/assets/<*>`. The `assets/` directory is an internal storage
detail; URLs no longer expose it. Nested paths preserved.

Add an `/api/v1/*` JSON-404 guard so unmatched API URLs return JSON 404
instead of falling through to the bb SPA shell HTML.

Improve the fresh-manager missing-manifest path to return `app_not_provisioned`
with a rebuild hint (root cause was stale binaries, not a real seeding bug).

Update default status template HTML, file-content-urls, migration guide,
bb guide app, tests, and regenerated templates to flat URLs only. No
back-compat alias kept — apps system isn't shipped yet.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
seedManagerThreadStorage previously early-returned after copying from
a user-authored manager template, skipping the bundled apps/status
fallback entirely. Fresh managers with any user template on disk would
404 on apps/status because the bundled manifest never got seeded.

Now the bundled set (apps/status/{manifest.json, assets/index.html,
data/state.json}) is always overlaid on top of any user-template copy,
regardless of which template is active. User-authored files still win
because they are copied first and the bundled overlay uses
writeFile(..., flag: "wx") which refuses to overwrite. Missing
non-default templates still warn but no longer skip the seed.

Tests refactored around expectBundledStatusAppSeeded() helper; added
"user-authored files win over the bundled overlay at the same path"
case asserting both behaviors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Full-width button below the search input in the secondary-panel New
Tab. Clicking it prefills the thread composer's draft via
usePromptDraftStorage.setDraft (text + clears attachments — full
replacement, not just text), focuses the composer with the [NAME]
placeholder selected, and dismisses the New Tab panel.

Replacing a non-empty draft prompts a browser confirm; cancel
preserves the existing draft (text + attachments), accept replaces
both. Reuses the existing prompt-draft store; no parallel state.
Plumbs a stable textareaId from ThreadDetailPromptArea through
FollowUpPromptBox so the button can focus the composer reliably.

Prefill template references `bb guide app`, the apps/<id>/
{manifest.json, assets/index.html, data/state.json} layout, and the
window.bb.data / window.bb.message browser APIs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Visual: bigger app rows with rounded tile-style icon container, name
+ id caption, uppercase tracking-wider section headers ("APPS",
"FILES"), token-driven hover/active/focus states that work in dark
mode. Create App moves from a full-width outline button below the
search to a dotted "+ Create App" tile at the end of the Apps list,
mirroring app rows with a "Describe an idea, the manager builds it"
hint line. The Apps section now always renders so the tile is
reachable immediately on open.

Prompt: prefilled template drops the [NAME] / [DESCRIBE…] placeholders
in favor of an end-of-text cursor landing. All apps-system context
lives at the top (manifest layout, window.bb usage), with a single
line pointing at `bb app new` for default styling, and a trailing
`What I want:` blank line where the user starts typing. Composer
cursor parks at end of text — no placeholder hunt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The blank template now scaffolds a bb-native HTML surface instead of
a barebones one-line demo. New apps get the bb default styling head
verbatim from `bb guide styling` (Google Fonts links for Inter +
Fira Code, the full `:root` oklch token block, dark-mode overrides,
body / .panel defaults), with scaffold-only classes (header chip,
title, timestamp slot, section title, task-list rows, pills) layered
strictly after the guide block. Visual structure mirrors the status
task-list dashboard at a stripped-down plain-HTML scale, with a
placeholder line — "Ask your agent to customize the status app how
you please." — inviting the user to ask their agent to build on top.

Name interpolation goes through a shared escapeHtmlText helper now
extracted into @bb/domain and reused from the previous module-local
copies in apps/desktop (local-view, log-viewer). XSS test covers
`<script>`, `&`, `"`, and `'` in the app name.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three small visual fixes to the secondary-panel New Tab launcher:

1. Unified the icon containers across Pet / Status / Create App tiles
   via shared LAUNCHER_TILE_ICON_CLASS constants. All three now share
   a size-9 raised frame with a size-5 glyph; the only intentional
   variant is the dashed inner border on Create App. The frame stays
   constant, only the asset varies.

2. Hide redundant id captions when the app id matches a slugified
   display name. `pet` / `status` captions disappear; when the id
   genuinely differs from the name, the caption renders in font-mono
   so it visibly reads as a developer identifier.

3. Drop the duplicate `title` tooltip on Create App (body hint stays)
   and the dead `disabled` prop on CreateAppTile.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The default status app shipped to fresh managers was hard-coded to a
PR/worker-review workflow (PULL REQUESTS + WORKERS + "Message the
manager" sections, a `pr` landing-mode chip). That assumed one
specific routine; a brand-new bb user has neither. Replace with a
bb-styled generic starting-point dashboard that matches the
`bb app new --template blank` output and invites customization:

  "Ask your agent to customize the status app how you please."

Mechanism: extract buildBlankAppIndexHtml({ name }) into a new
apps/server/src/services/threads/blank-app-scaffold.ts. Both
`bb app new --template blank` and `--template status` paths plus the
bundled manager-template overlay now generate index.html through one
generator (only the manifest icon differs). Delete the static
default-template/apps/status/{assets/index.html, data/state.json}
that pinned the old PR-review content.

User-authored manager templates still win over the bundled seed via
the existing flag:"wx" overlay semantics.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A single invalid app manifest in a thread's apps/ directory used to
fail the entire GET /api/v1/threads/<id>/apps list response with 422,
taking down the whole secondary-panel launcher.

The list route now shares a single readAppManifest parse step with
the detail and serve routes; zod validation failures on an individual
manifest log a structured warning (appId, manifestPath, summarized
issue list at WARN level, full issues at debug) and that app is
skipped in the list response. Detail and serve routes still surface
the failure as a new invalid_manifest 422 error code (added to
@bb/server-contract errors) with a one-line message — full zod issues
stay in the server log, not on the wire.

Tests cover: partial-success list (valid + invalid → 200 with only
the valid summary + logger warned); detail returns invalid_manifest
422; serve returns invalid_manifest 422 (not the SPA shell
fallthrough).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
SawyerHood and others added 26 commits June 4, 2026 16:13
The thread/manager header sits in an Electron title-bar drag region, so the
"..." ThreadActionsMenu trigger was swallowed as a window drag. Apply the
existing MACOS_WINDOW_NO_DRAG_CLASS to the header trigger only (sidebar usage
unchanged).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The native WebContentsView bounds were driven only by an async renderer
pipeline (ResizeObserver/window resize -> rAF -> IPC -> setBounds), so during
a synchronous OS window resize the view trailed the window edge by >=1 frame +
an IPC hop. Electron 41's WebContentsView has no setAutoResize.

Introduce a resize-invariant layout descriptor ({left, top, rightInset,
bottomInset}) the renderer emits only on layout-shape changes. The main process
caches it per view and, on the host BrowserWindow will-resize/resize events,
re-projects bounds from getContentBounds() and calls setBounds() synchronously
in lockstep with the OS resize. Preserves the prior keep-alive (#94),
panel-resize (#96), and containment (#128) behavior.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Delete manager templates end to end — server route + storage-template
service, host-daemon command handler, the domain/server/host-daemon contract
types and fields (templateName / managerTemplateName / ManagerTemplateName /
GET /manager-templates / host.list_manager_templates), the bb manager CLI
options, the frontend ManagerTemplatePicker + compose-view usage, the
bb-guide-manager-templates chapter (regenerated), and all related tests.

Managers are still created normally; they now start from default/empty thread
storage with no template selection or seeding. Existing on-disk
~/.bb/manager-templates/ user data is left intact and is now unused.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…degen

Replace the static single-file app scaffold with a full Vite + React +
TypeScript Todo app that showcases the window.bb SDK and live data-binding:
- editable source/ + pre-built public/ (served web root, flat refs), copied
  into each new app by the server (excludes dev-only screenshots/report dirs);
- an add-todos skill + README;
- SDK types are GENERATED from @bb/sdk (self-contained ambient window.bb d.ts,
  no imports) via packages/sdk/scripts/generate-app-globals-dts.mjs, with a
  drift-guard test so the vendored template types can't silently diverge.

Polished UI pass (lucide, bb oklch tokens, responsive); Live-pill + bound-data
showcase removed; error notices retained.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
electron-builder's default node_modules .d.ts pruning stripped the
app-scaffold-template's generated source/src/bb-sdk.d.ts from app.asar, so apps
created in the packaged desktop app scaffolded without their window.bb types.
Add a dedicated files FileSet for the template tree so it ships verbatim,
without relaxing .d.ts pruning elsewhere. Regression test added.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a typed realtime subscription API to @bb/sdk: bb.on({event, ...scope,
callback}) returning an idempotent unsubscribe, across thread/project/
environment/host/system (config/apps) / app / app-data (changed+resync) plus a
realtime:connection lifecycle event. One websocket per SDK instance with
ref-counted subscriptions, reconnect + resubscribe, and contract-derived
payloads (no SDK-only mirrors). window.bb.data.onChange now rides the shared
socket with full Phase 1 parity (subscribe-before-replay, buffer, version
dedupe, resync + reconnect replay). Server broadcasts app:changed alongside
system:apps-changed; app-data stays per-application.

Hardened after dual review: reject orphaned socket-ready promise on
close-before-open; connection listeners are observers (no socket ownership);
reset reconnect intent on idle close (no double-replay); reconnect replays
before emitting connected; fail-safe outgoing broadcast validation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Local electron-builder runs previously forced identity=null and
CSC_IDENTITY_AUTO_DISCOVERY=false unless the full CI secret set was
present, so every locally packaged bb.app shipped the prebuilt
Electron's invalidated adhoc linker signature (spctl: "code has no
resources but signature indicates they must be present"). macOS
provenance-tracks such bundles, which forces syspolicyd to evaluate and
journal every exec in the app's process tree — observed pegging
syspolicyd at ~380% CPU and stalling process launches system-wide.

Signing now resolves to one of three modes:
- environment: full CI secrets — sign with the provided cert + notarize
  (published-release path, unchanged)
- keychain: no secrets — sign via keychain auto-discovery, skip
  notarization (locally built apps never carry the quarantine xattr, so
  notarization is unnecessary; a valid Developer ID seal is what lets
  Gatekeeper cache assessments and keeps fresh launches out of the
  provenance sandbox)
- disabled: no secrets and CSC_IDENTITY_AUTO_DISCOVERY=false — explicit
  unsigned build (CI workflow-artifact-only path, unchanged)

Validated: packaged app deep-verifies strict with sealed resources
(6105 files), spawn-helper and better_sqlite3.node carry the Team ID,
and the hardened-runtime app fully boots (bridge/server/daemon all
re-exec the signed binary via ELECTRON_RUN_AS_NODE).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a customModels key to the bb-app managed config so users can register
model ids the provider catalog doesn't offer (e.g. non-public preview
models):

  { "customModels": [{ "providerId": "claude-code", "model": "<id>",
    "displayName": "<label>" }] }

The server appends them to the daemon-reported model list in
/system/execution-options — including when the provider catalog fails to
load — with catalog metadata winning on model-id collision (selected-only
entries are promoted rather than shadowed). Reasoning ladders are
per-provider: claude-code gets the full low-max ladder, while codex and pi
cap at xhigh since both reject "max" provider-wide. providerId is
validated by agentProviderIdSchema at the schema boundary so the launcher
and server agree on validity and reload 422s name the offending field.

The launcher preserves customModels across managed-config writes
(pruneManagedConfig previously dropped unknown keys) and surfaces entries
in `bb-app config list`. Reasoning-effort constants move from
@bb/agent-runtime to @bb/domain so the server can build AvailableModel
entries without depending on the runtime package.

No frontend changes: config-changed notifications already invalidate the
execution-options queries, so open pickers refresh on reload.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Translate the agent SDK's task event family (task_started/task_updated/
task_progress/task_notification) into a new backgroundTask thread item:
dynamic workflows render as a live timeline row with phase groups and
per-agent progress, folded from workflow_progress delta batches and
throttled adapter-side. Progress/completion events are thread-scoped so
tasks that outlive their spawning turn stay pagination-safe; timeline
windows backfill the latest task state for in-window items.

Lifecycle ownership: the adapter settles open tasks on thread/resume and
provider process exit; the server settles dangling items when a daemon
session re-registers with a new instance id; superseded progress rows are
pruned keep-latest-while-pending.

Adds the "ultracode" reasoning level (ranked between xhigh and max,
reconciling down to xhigh on model switch) for xhigh-capable claude-code
models. The server-owned workflowsEnabled policy flows explicitly through
the daemon contract (protocol v31); the adapter decomposes ultracode into
effort "xhigh" plus flag-tier settings {enableWorkflows, ultracode}.

Also: status:null now completes dangling contextCompaction items, and
session_state_changed/tool_progress/tool_use_summary are classified so
they no longer surface as provider/unhandled debug rows. Fixtures are
captured from a real workflow run driven through the agent SDK.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fixes from a 99-agent adversarial review of e0b03c6d7 (41 raw findings →
28 confirmed after 3-lens verification):

Client lifecycle (packages/sdk/src/realtime-client.ts):
- connectSocket constructs the socket before creating the socket-ready
  promise, so a sync throw (bad URL/factory) can no longer orphan a
  pending promise into an unhandled rejection; the reconnect timer body
  also contains sync throws instead of crashing the process
- onclose always records reconnect intent and emits 'disconnected' even
  while a stale backoff timer is pending, and a listener-driven connect
  cancels the pending timer — previously a close during backoff silently
  skipped the reconnect replay (lost app-data events, Phase 1 parity
  regression) and the orphaned timer escalated the delay while connected
- idle close that cancels a pending reconnect now emits a terminal
  disconnected event so observers don't wait forever on a promised retry
- dispatch iterates a listener snapshot: listeners added inside a
  callback no longer receive the in-flight event (preserves
  subscribe-then-replay ordering); replay delivery loops re-check
  listener.active so unsubscribe stops deliveries immediately
- buffered-event flush uses a per-path last-version rule: when the
  replay snapshot already delivered a path's final buffered state, all
  buffered events for that path are skipped — the old exact-match dedupe
  could re-deliver an older value last, leaving consumers permanently
  stale (versions are content hashes, so ordering cannot disambiguate)
- reconnect now notifies app-data:resync subscribers (broadcasts may
  have been missed) before replaying and emitting reconnected
- late realtime:connection observers receive the current state as a
  microtask snapshot instead of observing nothing until the next
  transition

Version-skew tolerance (domain/server-contract):
- changed-message and ThreadChangeMetadata types are now z.infer-derived
  from their schemas — schema/type drift is a compile error instead of a
  silent fail-closed realtime blackout at the hub's outgoing gate
- new lenient inbound schemas (changedMessageLenientSchema,
  serverMessageLenientSchema): clients strip unknown fields and filter
  unknown change kinds instead of dropping whole messages when talking
  to a newer server; SDK and web app now parse inbound traffic with
  them (the strict schemas keep guarding the server's outgoing boundary)
- changes arrays are readonly in the message types; entity events are
  delivered as shared objects with mutation blocked at compile time
- dropped the dead optional id from AppChangedMessage (never emitted,
  never consumed; scaffolded apps could only write filters that never
  match) and documented app:changed as a global app-list signal

Node support (packages/sdk/src/node.ts):
- the node transport ships a default websocket factory: global
  WebSocket on Node 22+, the ws package on supported Node 20 — bb.on
  and bb.data.onChange no longer throw out of the box on Node 20
- BbRealtimeSocket is now a minimal runtime-agnostic shape; default
  factories adapt the environment socket (browser global, node global,
  ws) instead of requiring DOM event types

API surface cleanup:
- *RealtimeOnInput types renamed to *RealtimeOnArgs and on(input) to
  on(args), matching the SDK-wide Args convention (pre-release rename)
- BbSdk and InjectedAppWindowBb derive on() from the shared BbRealtime
  interface instead of triplicating the signature
- CreateCurrentAppDataAreaArgs requires apps/realtime (the only caller
  always passed both); deleted the unreachable onChange runtime throw
- shared cloneJsonValue helper replaces two diverged copies; app-data
  events are cloned once per delivery instead of twice
- realtime URL derivation preserves a path-prefixed baseUrl (mirrors
  the HTTP transport); same-origin browser derivation still uses /ws
- resolveApplicationId reuses requireCurrentApplicationId

Frontend:
- removed the unreachable app:changed cache registry (the SPA never
  subscribes to the app entity; system:apps-changed remains the
  canonical app-list invalidation path) and replaced the bypass test
  with one pinning the no-op
- ws.ts logs dropped inbound messages instead of swallowing them

Tests (+54): buffered-flush deliver/stale-path cases, negative id and
prefix scope filtering, all previously unexercised dispatch paths,
listener exception isolation, unsubscribe-during-dispatch/replay,
backoff growth/cap/reset, close-during-pending-backoff, resync-on-
reconnect ordering, late connection-observer snapshot, lenient parsing,
hub fail-safe validation drop, notifySystem/notifyHost delivery, full
change-kind schema-gate sweep per entity, and notifyGlobalAppsChanged
end-to-end through real websockets.

Known debt (deliberately not addressed here): BbRealtimeClient and the
SPA's WebSocketManager remain two parallel realtime implementations;
consolidating them is a standalone refactor.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ider turn watchdog

The idle watchdog killed healthy workflow turns ("No provider activity
for 906s after item/completed"): item/backgroundTask/progress was added
to the activity list, but those events are thread-scoped (turn_id NULL)
and the candidate query correlated activity strictly on the anchor
row's turn_id, so a streaming workflow was invisible to it.

The anchor is now the newest event that is either scoped to the active
turn (the latest turn/started) or a thread-scoped background task
event. The NULL-turn arm is deliberately restricted to the
backgroundTask family so thread-scoped provider/error noise cannot
defer reaping a wedged turn. item/backgroundTask/completed joins the
activity list — load-bearing, because progress rows are pruned the
moment the completed row lands, which would otherwise false-fire the
watchdog right after a successful workflow.

Guard hardening from adversarial review: active threads with no
turn/started yet are excluded in SQL (a NULL activeTurnId would throw
in row parsing and abort the whole sweep tick); the turn/completed,
pending-interaction, and started-at correlations all key off the
active-turn subquery instead of the anchor's turn_id; OR fragments are
self-parenthesized (drizzle's and() does not wrap raw fragments);
empty-string providerThreadId anchors fall back to the latest real id;
and the persisted lastActivityEventType is a plain string so
activity-list edits never make stored watchdog events unparseable.

The regenerated SDK browser bundle also catches up on the workflow and
reasoning-level domain schemas from a0bd295a7, which shipped without a
bundle regen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…s on disk

Rebuilding an app (or editing public/ files directly) never reloaded the
open app surface. Two breaks upstream of the SPA's existing reloadToken
mechanism: the daemon's apps-root watcher silently dropped every path
under <app>/public/ (no classification branch), and even reported storage
hints were gated behind an app-list signature diff that content edits
never alter.

Add a per-app content-changed signal end to end:

- host-watcher: classify <app>/public/** as application-content-changed,
  deduped to one event per app per flush batch; source/ stays
  unclassified (nothing served changes until a build writes public/).
- host-daemon: dispatch the new observed kind and send
  {type: "application-content-changed", applicationId} over the daemon
  WS, with per-app offline buffering flushed after reconnect.
- domain/server: APP_CHANGE_KINDS gains "content-changed"; app changed
  messages carry an optional id (absent = list-level, present =
  app-scoped); hub.notifyAppContentChanged broadcasts it, and notifyApp
  is narrowed to the new AppListChangeKind subset so an id-less
  content-changed is unrepresentable at the producer.
- SPA: subscribe to the "app" entity and invalidate only the changed
  app's detail + markdown-preview queries via REALTIME_APP_CHANGE_REGISTRY,
  so the detail refetch bumps dataUpdatedAt and busts the iframe
  reloadToken without reloading other open apps.
- sdk: document the new app:changed semantics; regenerate the scaffold
  bb-sdk.d.ts and browser runtime bundle.

Also extract the shared mock hub socket test helper that was previously
copy-pasted across four server test files.

Validated end to end against a live dev instance: touching
public/index.html broadcasts {entity:"app",id,changes:["content-changed"]}
within the watcher debounce; touching source/ broadcasts nothing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ntime already hosts

A thread.start/turn.submit whose freshly scanned injected-skill catalog
hash differed from the loaded runtime's hash always forced a runtime
replacement, which threw whenever the environment had active threads or
open terminals. An agent that installed a skill mid-turn (e.g. building
an app with a skills/ dir) therefore bricked its own thread: every
subsequent message failed with "already has an active runtime with
injected skill catalog X; requested Y" and was dropped, and the thread
flipped to error status while the daemon-side turn kept running.

Thread commands now pass their targetThreadId down to
ensureCompatibleEntry; when the entry already hosts that thread and has
active runtime work, the stale catalog is reused (with a warn log) and
the refresh is deferred to the next launch on an idle environment. Idle
entries are still replaced as before.

The remaining conflict case (environment busy with other threads or
terminals) now throws a typed SkillCatalogConflictError mapped to the
new workspace resolution failure code "skill_catalog_conflict" instead
of surfacing as "unknown".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ersarial review

Follow-up to the stale-catalog deferral fix, addressing review findings:

- Keep the about-to-be-active catalog when pruning staging dirs during a
  runtime replacement. The cleanup keep-list was built only from loaded
  entries after the replaced entry was deleted, so the freshly staged new
  catalog was removed before the replacement runtime bound it — the
  refreshed runtime launched with skill roots pointing at a deleted
  directory (pre-existing, but the deferral fix made idle replacement the
  designed refresh path).

- Defer the catalog swap for any busy runtime a thread command targets,
  not just runtimes already hosting the thread. Keying the guard on
  hosting left the original brick reachable: a terminal-first entry
  (no catalog, open terminal) or a sibling thread resuming after daemon
  restart still failed and dropped the message. SkillCatalogConflictError
  now only guards the no-target invariant.

- Warn once per requested catalog per entry instead of on every command
  while the environment stays busy.

- Stop CreateEntryArgs advertising ignored resolution-time fields
  (injectedSkillSources, targetThreadId).

- Pin the full defer-then-refresh-once-idle sequence and staged-catalog
  survival in tests; the prior reuse test compared an entry's hash with
  itself.

- Regenerate the SDK browser bundle for the skill_catalog_conflict
  contract code added in the previous commit.

Known follow-ups (not in this change): the deferral is invisible to the
server (no catalog hash in command results, no idle-transition
reconciliation owner), and thread commands remain unlaned so a
catalog-bearing command racing an in-flight resume can still replace the
runtime mid-resume.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ture

The unarchive-resume fixture landed on main without the new required
turn.submit option introduced by the workflows commit; align it with the
sibling fixtures.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
plans/claude-code-workflows.md was swept into the manager-templates
removal commit by an over-broad add during conflict resolution. Plans
live in thread storage, not the repo.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…t, honest contracts, d.ts codegen

Adversarial-review fixes for the apps platform surface:
- extract scaffold machinery out of routes/apps.ts into services modules;
  fs.cp filtered copy applies exclusions at every depth; build-time template
  copy reuses it so dist ships no dev artifacts; README $-pattern safe
- serve the window.bb runtime from a content-hashed immutable endpoint
  instead of inlining ~865KB into every app HTML response; bundle is
  regenerated from the real module graph (no regex+Function eval) with a
  drift-guard test against the committed artifact
- delete decorative AppRuntimeBootstrap capabilities/dataUrl/messageUrl;
  appId/applicationId required on the injected window.bb contract
- app d.ts codegen emits BbRealtime (window.bb.on typechecks in new apps),
  self-containment guards actually fire, drift test typechecks generated
  output; template source typechecked, source<->public drift guard, useTodos
  stale-snapshot race fixed, todo helper renamed for accuracy

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e entry

- delete the ~240-line dead CLI HTTP client; port its tests to the SDK
  transport that production actually runs
- typed BbHttpError (status + server code) replaces 'HTTP 404:' message
  sniffing; missing app vs missing data path report correctly again
- importing @bb/sdk/node no longer eagerly loads CLI config — bb --help/
  --version/guide work without BB_SERVER_URL; one canonical package entry
  (package.json '.' and vitest alias agree)
- status area uses domain unions instead of plain strings; dead area
  exports removed; CLI consumers (guide, pending-todos, context-env) stop
  shadowing SDK types and building network clients for local work

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- fix stale 'connected' observer state when the last targeted listener
  unsubscribes while the socket is CLOSING; fix reentrant connect during
  the 'disconnected' emit corrupting reconnectDelayMs; regression tests
  for both plus the synchronous-throw path through on()/activateListener
- collapse four copy-pasted dispatch methods and eight listener-record
  interfaces into one generic keyed implementation; named aliases replace
  inline Extract<> signatures
- hub.notifyApp signature matches reality (single change kind); hub tests
  assert distinct outcomes per kind; integration test reuses the SDK ws
  adapter; lenient/strict server-message schema drift guard; stale
  daemon-protocol comment removed

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…watchdog + reasoning-ladder single source

- lost background tasks now settle on every realistic path: daemon restart
  (regardless of previous-session status), lease expiry, and disconnect
  grace — mirroring pending-interaction reconciliation; settling no longer
  flips already-completed workflows to interrupted
- late thread-scoped backgroundTask events no longer stretch the spawning
  turn's source range (turn-summary expansion 400 fix) with projection
  regression tests; error-only workflow rows are expandable; shared
  settled-state predicate and timeline row types deduped
- backgroundTask progress rows fetched by targeted query instead of
  loading all rows and filtering in JS; timeline window backfill covered
  by tests and reuses mergeStoredEventRowsById
- workflowsEnabled stays required downstream of the server boundary and
  the default policy is tested with true; watchdog thread-scope list
  derived from threadOnlyThreadEventTypes; SQL fragments deduped;
  custom-model reasoning ladders reuse the server policy table; dead
  taskType contract carry removed

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- delete the dead first-boot PREFERENCES.md host read (and its mode
  plumbing through thread-send/queued-messages/nudge sweep) that could
  never find content after seeding removal; drop the dead request field
- historical host.list_manager_templates command rows stay in the
  read-only prune cohort (test on in-memory sqlite)
- inline the pass-through ManagerSlot wrapper; drop the stale
  'may already exist from user templates' welcome-template sentence and
  the test pinning it

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…at can fail

- browser-view layout projected in a consistent coordinate space so
  docked DevTools no longer misprojects the native view; will-resize uses
  the in-flight bounds instead of stale getContentBounds()
- attach/setBounds accept the legacy bounds shape under shell/server
  version skew; one shared collapsed-layout sentinel in the contract;
  open-tab payload uses the contract type; dead clamp export removed
- BrowserTabDeck resize test flushes rAF and fails on revert;
  ThreadActionsMenu test exercises the actual no-drag fix; no-drag class
  gated on usesDesktopChrome

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ering

- remove unreachable skill_catalog_conflict contract code; thread-brick
  regression test drives the real command path (targetThreadId from a
  thread command); staging-survival assertions check the real catalog
  hash instead of masking null with ''; log-throttle state moved off the
  exported RuntimeEntry
- ServerConnection's three parallel recoverable-message buffers unified
  behind one keyed pending buffer (existing tests pin behavior)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
bb previously had no repo-shipped skills: building-bb-apps existed only as
a hand-authored file in one machine's data dir. This adds a built-in skill
source so the app-builder skill ships with every install.

- Contract: new "builtin" injected-skill source variant (applicationId
  null); protocol 31 -> 32; dedicated union test.
- Content: building-bb-apps SKILL.md checked in under
  apps/server/src/services/skills/builtin-skills/, copied to
  dist/builtin-skills by a new build step (mirrors the app scaffold
  template copy), resolved beside the module in both src and dist layouts.
- Discovery: the server scans the builtin root alongside data-dir and app
  roots. A user data-dir/app skill with the same name overrides the
  builtin (logged at info); collisions among user sources keep the
  existing drop-all rule. builtinSkillsRootPath is resolved once at boot
  into ServerRuntimeConfig and passed explicitly.
- Daemon staging is sourceType-agnostic and needs no behavior change; a
  staging test pins the builtin catalog entry.
- Regenerated the SDK app-runtime browser bundle (embeds the contract
  schema).
- TemplateKind cleanup: removed unused "skill_seed", added the
  actually-generated "system-message", replaced the kind cast with
  validation at decode.

Known limitation (documented in resolveInjectedSkillSources): skill source
paths are server-machine paths; daemons on additional hosts skip them with
a staging warning, same as data-dir and app skills today.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add browser-style Back/Forward navigation to the left sidebar that moves
through the app-shell route history.

- New useAppRouteHistoryNavigation hook (app-route-history.ts) tracks the
  actual React Router entries visited while mounted (including duplicate
  same-URL pushes with distinct location.keys). PUSH appends and clears the
  forward stack, REPLACE updates the current slot, POP reconciles to a known
  entry by key and treats an unrecorded key as the app-owned history
  boundary. Back/Forward move by a real navigate(delta) that skips equal-URL
  slots so one click lands on the nearest visibly different route.
- New SidebarHistoryNavigationControls renders two accessible icon buttons
  (Go back / Go forward) using the Button primitive + sidebar tokens, native
  disabled state, aria-hidden icons, and an onNavigate hook for closing the
  mobile drawer.
- Wire the controls into AppSidebar's primary-actions area above New Thread /
  New Manager, passing closeOnMobile so the compact drawer closes after an
  enabled press. Placement keeps the controls out of the macOS titlebar drag
  region and hidden in the icon-collapsed sidebar.

Tests cover initial disabled state, push/back/forward, duplicate same-URL
skipping, forward-stack clearing, replace, native/unknown POP boundaries, the
onNavigate contract, and sidebar placement with unchanged desktop chrome.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace inline types in function signatures with named aliases per AGENTS:
- AppRouteNavigationType for reduceHistory's navigation-kind parameter.
- NavigateToOptions for the test navigateTo helper's options argument.

No behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@brsbl
Copy link
Copy Markdown
Owner Author

brsbl commented Jun 5, 2026

Superseded by ymichael#66, which targets the up-to-date ymichael/bb:main and shows a clean 2-commit diff. Please review there. Leaving this PR open (not closing/deleting) per instruction; it points at the same branch head.

@brsbl brsbl changed the title feat(app): sidebar Back/Forward route-history controls [SUPERSEDED by ymichael/bb#66] feat(app): sidebar Back/Forward route-history controls Jun 5, 2026
…lisions

BrowserRouter labels unkeyed document-history entries "default", so a POP can
share the mounted entry's key while pointing at a different URL. Two stale-state
bugs followed:

- The effect deduped on location.key, so a same-key POP was dropped entirely
  and never reduced. Dedupe on the location object identity instead (React
  Router hands out a fresh location per navigation).
- The POP reducer reconciled by key alone, so it could snap to a recorded slot
  whose URL no longer matches. Require key AND normalized URL to match;
  otherwise treat the popped route as the app-owned history boundary.

Adds a regression test with MemoryRouter entries that intentionally share the
"default" key but have different URLs (verified to fail before this fix).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@brsbl brsbl closed this Jun 5, 2026
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.

3 participants