Skip to content

Open workspace drafts optimistically and dedupe draft writes#29

Merged
Ffinnis merged 4 commits intomainfrom
workspace-mo6my31f
Apr 20, 2026
Merged

Open workspace drafts optimistically and dedupe draft writes#29
Ffinnis merged 4 commits intomainfrom
workspace-mo6my31f

Conversation

@Ffinnis
Copy link
Copy Markdown
Owner

@Ffinnis Ffinnis commented Apr 20, 2026

Summary

  • Opened workspace-backed new threads optimistically before waiting for server workspace sync, with rollback and error toast on setActive failure.
  • Added a shared logical-project indexing helper and used it to simplify sidebar grouping for threads, workspaces, and workspace sections.
  • Consolidated composer draft creation into an atomic upsert path to avoid duplicate writes and preserve sticky composer state.
  • Added new-thread latency tracking and a dev-only debug surface, plus tests for optimistic workspace behavior and single-write draft creation.

Testing

  • Not run in this branch summary.
  • Added/updated Vitest coverage for optimistic workspace open, rollback on sync failure, stale failure race handling, and draft write deduplication.
  • Existing project checks should still be run before merge: bun fmt, bun lint, bun typecheck.

Summary by CodeRabbit

  • Bug Fixes

    • Improved workspace activation resilience: failed syncs roll back safely, keep the UI on the draft route, and show a “Workspace sync failed” notification; race conditions guarded.
  • New Features

    • Optimistic workspace switching for new-thread creation with deferred server persistence.
    • Dev-only new-thread latency tracker and debug panel; keyboard shortcut flow now records latency.
  • Tests

    • Added coverage for workspace-activation races, draft deduplication, and new-thread latency.
  • Refactor

    • Sidebar project indexing and composer-draft handling reorganized for atomic updates and fewer redundant store writes.

- Start workspace drafts before setActive ack and roll back on failure
- Consolidate composer draft upsert and sticky state into one write
- Add sidebar grouping helper and new-thread latency diagnostics
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 20, 2026

📝 Walkthrough

Walkthrough

Adds dev-only new-thread latency tracking and debug UI; implements optimistic workspace activation with token-guarded async persistence and rollback on failure; introduces logical project indexing for sidebar rendering; adds atomic create-or-reuse composer-draft semantics with sticky-state application; and includes extensive tests for races, dedupe, and persistence behaviors. (36 words)

Changes

Cohort / File(s) Summary
Latency tracking & dev UI
apps/web/src/perf/newThreadLatency.ts, apps/web/src/perf/newThreadLatency.test.ts, apps/web/src/components/WebSocketConnectionSurface.tsx
Adds NewThreadLatency module (tracker, bounded samples, hooks, test helpers) and tests. Adds a DEV-only debug panel showing p95 and recent samples.
Keyboard-shortcut & ChatView tests
apps/web/src/routes/_chat.tsx, apps/web/src/components/ChatView.browser.tsx
Shortcut flow now starts a latency tracker and passes it into handler. ChatView browser tests updated to repeatedly wait-for-layout after dispatch, assert draft-route vs snapshot ordering, workspace setActive race/error handling, and draft-write deduplication.
New-thread handling & hook integration
apps/web/src/hooks/useHandleNewThread.ts
handleNewThread accepts optional latencyTracker, replaces several draft APIs with createOrReuseProjectDraft, uses memoized projectsByScopedKey, and navigates via a runTransition helper.
Optimistic workspace activation & store API
apps/web/src/components/Sidebar.tsx, apps/web/src/store.ts, apps/web/src/store.test.ts
Sidebar new-thread flow performs optimistic per-project workspace activation, snapshots previous active workspace, guards async persistence with a latest-token, reverts on failures (toast on error). Store adds setActiveWorkspaceForProject and tests optimistic/no-op behavior.
Composer draft atomic ops & sticky state
apps/web/src/composerDraftStore.ts, apps/web/src/composerDraftStore.test.ts
Adds createOrReuseProjectDraft, upsertLogicalProjectDraftThreadState, assignLogicalProjectDraftMapping, and applyStickyStateToThreadKey to perform atomic logical→draft mapping, optional sticky-state application, and preserve promotedTo. Tests verify single-write semantics, remapping rules, and promotion preservation.
Sidebar logical grouping & rendering
apps/web/src/components/Sidebar.logic.ts, apps/web/src/components/Sidebar.logic.test.ts, apps/web/src/components/Sidebar.tsx
Adds buildLogicalProjectEntryIndex to group entries by logical project key and preserve order. Sidebar now uses indexed maps for threads/workspaces/sections and precomputes visible thread keys. Tests validate grouping and ordering.
Misc / test harness updates
apps/web/src/components/ChatView.browser.tsx, apps/web/src/composerDraftStore.test.ts
Test harness changes for race/failure cases around workspacesSetActive, assertions on draft open timing vs snapshot refresh, and composer draft write dedupe for keyboard shortcuts.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Sidebar as Sidebar UI
    participant Store as AppStore
    participant Router as Router/Handler
    participant API as Workspace API
    participant Latency as NewThreadLatency

    User->>Sidebar: Click "New Thread" / press shortcut
    Sidebar->>Latency: startNewThreadLatency(flow)
    Latency-->>Sidebar: tracker

    Sidebar->>Store: setActiveWorkspaceForProject (optimistic)
    Store-->>Sidebar: optimistic state update

    Sidebar->>Store: createOrReuseProjectDraft(...)
    Store-->>Sidebar: draft created/reused

    Sidebar->>Router: router.navigate(newDraft) (via startTransition)
    Router->>Latency: markRouteReady()
    Latency-->>Latency: append sample (partial)

    Note right of API: async persistence (token-guarded)
    Sidebar->>API: workspaces.setActive (async)

    alt server ack success (token matches)
        API-->>Sidebar: success
        Sidebar->>Latency: markWorkspaceAck("success")
    else server ack error (token matches)
        API-->>Sidebar: error
        Sidebar->>Store: revert active workspace (if token matches)
        Sidebar->>Sidebar: show "Workspace sync failed" toast
        Sidebar->>Latency: markWorkspaceAck("error")
    else stale response (token mismatch)
        API-->>Sidebar: ignored (no-op)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I hopped a timed and tiny track,

Drafts were reused, then written back;
Tokens keep the freshest click,
Latency hums, the samples tick;
New threads bloom — optimistic trick!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically summarizes the main changes: optimistic workspace opening and draft write deduplication.
Description check ✅ Passed The description addresses all required template sections: clearly describes what changed and why, explains the problem being solved and approach, includes relevant testing notes, and indicates checklist items were considered.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch workspace-mo6my31f

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XL labels Apr 20, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
apps/web/src/perf/newThreadLatency.test.ts (1)

64-78: Strengthen the eviction test with distinct sample durations.

Line 76 and Line 77 both asserting 1 doesn’t prove oldest samples were evicted in order. Make per-sample durations unique and assert boundary values.

Suggested test hardening
   for (let index = 0; index < 55; index += 1) {
     const tracker = startNewThreadLatency("shortcut");
-    nowMs += 1;
+    nowMs += index + 1;
     tracker.markRouteReady();
   }

   const samples = getNewThreadLatencySamples();
   expect(samples).toHaveLength(50);
-  expect(samples[0]?.durationMs).toBe(1);
-  expect(samples[49]?.durationMs).toBe(1);
+  expect(samples[0]?.durationMs).toBe(6);
+  expect(samples[49]?.durationMs).toBe(55);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/perf/newThreadLatency.test.ts` around lines 64 - 78, The test
currently uses identical durations so eviction order isn’t verified; update the
loop in the "keeps only the latest 50 samples" test to produce unique durations
per sample by advancing nowMs differently between creating the tracker
(startNewThreadLatency) and calling tracker.markRouteReady(), using
__setNewThreadLatencyNowProviderForTests to control time; after pushing 55
samples, use getNewThreadLatencySamples() and assert samples.length === 50 and
that samples[0].durationMs equals the duration of the 6th-created sample and
samples[49].durationMs equals the duration of the 55th-created sample (reference
functions: __setNewThreadLatencyNowProviderForTests, startNewThreadLatency,
tracker.markRouteReady, getNewThreadLatencySamples).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/web/src/components/Sidebar.tsx`:
- Around line 2494-2504: The optimistic workspace switch via
setActiveWorkspaceForProject is done before awaiting handleNewThread, but
there's no rollback if handleNewThread rejects; modify the flow to capture the
current active workspace (previous environmentId/projectId/workspaceId) before
calling setActiveWorkspaceForProject, then call setActiveWorkspaceForProject and
await handleNewThread inside a try block, and in the catch for handleNewThread
revert by calling setActiveWorkspaceForProject with the saved previous ids (and
rethrow or return the error) so the sidebar reflects the original workspace;
keep the existing api.workspaces.setActive(...).catch(...) logic intact for the
server update.

In `@apps/web/src/composerDraftStore.ts`:
- Around line 1158-1170: The current cleanup block deletes the previous thread's
entries even if that draft is mid-promotion; modify the conditional so we do not
evict a draft whose state has promotedTo set. Specifically, when checking
previousThreadKeyForLogicalProject before deleting nextDraftThreadsByThreadKey
and nextDraftsByThreadKey, also check
state.draftsByThreadKey[previousThreadKeyForLogicalProject]?.promotedTo ===
undefined (or falsy); if promotedTo exists, skip the delete logic for that
previousThreadKeyForLogicalProject to preserve in-flight promotion data in
nextLogicalProjectDraftThreadKeyByLogicalProject, nextDraftThreadsByThreadKey,
and nextDraftsByThreadKey.

In `@apps/web/src/perf/newThreadLatency.ts`:
- Around line 158-172: getNewThreadLatencySamples currently builds a new array
on every call which breaks useSyncExternalStore's snapshot stability; change it
to return a cached snapshot reference and only recreate that cached array when
samplesWithId changes. Concretely, add a module-level cachedSnapshot variable
(e.g., cachedNewThreadLatencySamples) and a function to refresh it when
samplesWithId is mutated (or compute it lazily and update on writes), update
getNewThreadLatencySamples to return cachedNewThreadLatencySamples instead of
mapping each time, and ensure any code that mutates samplesWithId also updates
the cache so useNewThreadLatencySamples (which passes getNewThreadLatencySamples
as the snapshot getter) will receive a stable reference between real updates.

---

Nitpick comments:
In `@apps/web/src/perf/newThreadLatency.test.ts`:
- Around line 64-78: The test currently uses identical durations so eviction
order isn’t verified; update the loop in the "keeps only the latest 50 samples"
test to produce unique durations per sample by advancing nowMs differently
between creating the tracker (startNewThreadLatency) and calling
tracker.markRouteReady(), using __setNewThreadLatencyNowProviderForTests to
control time; after pushing 55 samples, use getNewThreadLatencySamples() and
assert samples.length === 50 and that samples[0].durationMs equals the duration
of the 6th-created sample and samples[49].durationMs equals the duration of the
55th-created sample (reference functions:
__setNewThreadLatencyNowProviderForTests, startNewThreadLatency,
tracker.markRouteReady, getNewThreadLatencySamples).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a77f48b1-62f2-40ec-a896-64caf7f956a7

📥 Commits

Reviewing files that changed from the base of the PR and between 9b16324 and 03951c5.

📒 Files selected for processing (13)
  • apps/web/src/components/ChatView.browser.tsx
  • apps/web/src/components/Sidebar.logic.test.ts
  • apps/web/src/components/Sidebar.logic.ts
  • apps/web/src/components/Sidebar.tsx
  • apps/web/src/components/WebSocketConnectionSurface.tsx
  • apps/web/src/composerDraftStore.test.ts
  • apps/web/src/composerDraftStore.ts
  • apps/web/src/hooks/useHandleNewThread.ts
  • apps/web/src/perf/newThreadLatency.test.ts
  • apps/web/src/perf/newThreadLatency.ts
  • apps/web/src/routes/_chat.tsx
  • apps/web/src/store.test.ts
  • apps/web/src/store.ts

Comment thread apps/web/src/components/Sidebar.tsx Outdated
Comment thread apps/web/src/composerDraftStore.ts
Comment thread apps/web/src/perf/newThreadLatency.ts
- Avoid evicting promoted drafts during project thread remaps
- Cache new-thread latency snapshots for stable references
- Restore the previous active workspace if new thread creation fails
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/web/src/composerDraftStore.ts`:
- Around line 1131-1146: The early-return path rebuilds the thread with
createDraftThreadState (assigned to nextDraftThread) which clears promotedTo,
causing promotion state to be lost when draftThreadsEqual triggers; modify the
logic so that before comparing/returning you copy existingThread.promotedTo into
the rebuilt thread (i.e. set nextDraftThread.promotedTo =
existingThread.promotedTo) or preserve promotedTo when constructing
nextDraftThread, ensuring callers like setLogicalProjectDraftThreadId,
createOrReuseProjectDraft, getDraftSessionByLogicalProjectKey, and
finalizePromotedDraftThreadByRef still see the promotion marker.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c2407804-b395-4419-be2b-f4c39acfd83c

📥 Commits

Reviewing files that changed from the base of the PR and between 03951c5 and c3a1a2d.

📒 Files selected for processing (5)
  • apps/web/src/components/Sidebar.tsx
  • apps/web/src/composerDraftStore.test.ts
  • apps/web/src/composerDraftStore.ts
  • apps/web/src/perf/newThreadLatency.test.ts
  • apps/web/src/perf/newThreadLatency.ts
✅ Files skipped from review due to trivial changes (1)
  • apps/web/src/perf/newThreadLatency.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/src/composerDraftStore.test.ts

Comment thread apps/web/src/composerDraftStore.ts
- keep promoted thread metadata when reusing an existing logical draft mapping
- wait for workspace-set-active requests in chat view tests
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/web/src/composerDraftStore.ts`:
- Around line 1149-1152: The current merge into
nextLogicalProjectDraftThreadKeyByLogicalProjectKey blindly adds
[input.normalizedLogicalProjectKey]: input.draftId without removing any existing
entries that point to the same draftId, leaving stale aliases; update the
construction to first copy state.logicalProjectDraftThreadKeyByLogicalProjectKey
then remove any keys whose value === input.draftId before assigning the new
mapping for input.normalizedLogicalProjectKey so old logicalProjectKey ->
draftId aliases are cleared; apply the same change in the other occurrence (the
block around lines referencing the same pattern) and keep references to
nextLogicalProjectDraftThreadKeyByLogicalProjectKey,
state.logicalProjectDraftThreadKeyByLogicalProjectKey,
input.normalizedLogicalProjectKey, input.draftId,
getDraftSessionByLogicalProjectKey and isComposerThreadKeyInUse when locating
the spots to change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 43ff07c9-c7d2-4c08-bef2-aadbf9944010

📥 Commits

Reviewing files that changed from the base of the PR and between c3a1a2d and 651155e.

📒 Files selected for processing (3)
  • apps/web/src/components/ChatView.browser.tsx
  • apps/web/src/composerDraftStore.test.ts
  • apps/web/src/composerDraftStore.ts
✅ Files skipped from review due to trivial changes (1)
  • apps/web/src/composerDraftStore.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/src/components/ChatView.browser.tsx

Comment thread apps/web/src/composerDraftStore.ts Outdated
- Keep only the latest logical-project mapping for a reused draft id
- Normalize persisted mappings to avoid stale aliases
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
apps/web/src/composerDraftStore.ts (1)

291-306: Extract the repeated draft-upsert options shape into a shared type.

That inline options object now appears in the public store interface and again in the helper signatures below. Pulling it into a single DraftThreadUpsertOptions alias will make future field additions much harder to miss.

♻️ Example cleanup
+type DraftThreadUpsertOptions = {
+  threadId?: ThreadId;
+  branch?: string | null;
+  worktreePath?: string | null;
+  createdAt?: string;
+  envMode?: DraftThreadEnvMode;
+  runtimeMode?: RuntimeMode;
+  interactionMode?: ProviderInteractionMode;
+};
+
 interface ComposerDraftStoreState {
   ...
   setLogicalProjectDraftThreadId: (
     logicalProjectKey: string,
     projectRef: ScopedProjectRef,
     draftId: DraftId,
-    options?: {
-      threadId?: ThreadId;
-      branch?: string | null;
-      worktreePath?: string | null;
-      createdAt?: string;
-      envMode?: DraftThreadEnvMode;
-      runtimeMode?: RuntimeMode;
-      interactionMode?: ProviderInteractionMode;
-    },
+    options?: DraftThreadUpsertOptions,
   ) => void;
   ...
   createOrReuseProjectDraft: (input: {
     logicalProjectKey: string;
     projectRef: ScopedProjectRef;
     draftId: DraftId;
-    options?: {
-      threadId?: ThreadId;
-      branch?: string | null;
-      worktreePath?: string | null;
-      createdAt?: string;
-      envMode?: DraftThreadEnvMode;
-      runtimeMode?: RuntimeMode;
-      interactionMode?: ProviderInteractionMode;
-    };
+    options?: DraftThreadUpsertOptions;
     applyStickyState?: boolean;
   }) => void;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/composerDraftStore.ts` around lines 291 - 306, The public
interface method createOrReuseProjectDraft repeats the inline options shape;
extract that options object into a shared type alias (e.g.,
DraftThreadUpsertOptions) and replace all inline occurrences with that alias
(including the createOrReuseProjectDraft signature and any helper functions that
accept the same options), update imports/exports as needed, and run type checks
to ensure fields like threadId, branch, worktreePath, createdAt, envMode,
runtimeMode, and interactionMode are preserved exactly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@apps/web/src/composerDraftStore.ts`:
- Around line 291-306: The public interface method createOrReuseProjectDraft
repeats the inline options shape; extract that options object into a shared type
alias (e.g., DraftThreadUpsertOptions) and replace all inline occurrences with
that alias (including the createOrReuseProjectDraft signature and any helper
functions that accept the same options), update imports/exports as needed, and
run type checks to ensure fields like threadId, branch, worktreePath, createdAt,
envMode, runtimeMode, and interactionMode are preserved exactly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 14a1a8e3-f18e-427e-9a89-75e2e2c2de9a

📥 Commits

Reviewing files that changed from the base of the PR and between 651155e and e55dcc5.

📒 Files selected for processing (2)
  • apps/web/src/composerDraftStore.test.ts
  • apps/web/src/composerDraftStore.ts
✅ Files skipped from review due to trivial changes (1)
  • apps/web/src/composerDraftStore.test.ts

@Ffinnis Ffinnis merged commit 4f2bd20 into main Apr 20, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant