Skip to content

feat(notebooks): show conflict modal on 410 error#58213

Merged
a-lider merged 16 commits into
masterfrom
fix/notebook-stale-conflict-modal
May 18, 2026
Merged

feat(notebooks): show conflict modal on 410 error#58213
a-lider merged 16 commits into
masterfrom
fix/notebook-stale-conflict-modal

Conversation

@a-lider
Copy link
Copy Markdown
Contributor

@a-lider a-lider commented May 8, 2026

Problem

When PM-collab can't rebase a user's local edits onto the latest server state, those edits silently disappear. Two failure modes:

  • 410 stale - Redis stream hit MAXLEN or TTL expired, so the missed steps from the client's last_seen_version are gone and can't be reapplied
  • Rebase throws - server returned 409 with the missed range, but applying them to the local doc throws (corrupted step format, schema mismatch, or other edge case)

Neither has been hit during internal testing, but it's better to surface a fallback so the user can recover their work without digging through notebook history. Also, the modal suggests opening support ticket, it would helps us catch unexpected errors.

Changes

  • Conflict modal with a side-by-side preview of "Last saved version" vs "Your unsaved changes," and two actions: Discard unsaved changes and reload or Copy unsaved version to a new notebook
  • NotebookPreview component - lightweight read-only React renderer for a notebook ProseMirror JSON document. Used by the modal to show both versions without spinning up a full Tiptap editor.
  • Discard does a hard reload so the editor remounts cleanly with the server's content via initialContent (no setContent transaction that would dirty PM-collab's sendable buffer).
  • rebaseFailed action in notebookCollabLogic bubbles up to notebookLogic to open the modal when applyRemoteStep throws during a 409 rebase.

I initially tried implementing "Save anyway" in the modal via the legacy PATCH endpoint that overwrites the entire content. The problem with that approach: if another client is connected, their next collab/save would overwrite the force-saved version. PATCH updates the notebook content but doesn't broadcast steps (i.e. what changed) through the collab stream - and in the 410 case the missed steps can't be reconstructed anyway because the stream was truncated. So instead of force-save, I made the "Copy to new notebook" to preserve the user's unsaved work.

How did you test?

Manually broke the Redis stream to force a 410 on the next save:

XADD "notebook:collab:{<team_id>:<notebook_short_id>}:stream" "9999-0" data '{"step" {"stepType":"replace","from":0,"to":0},"client_id":"fake","user_id":1, "user_name":"fake","cursor_head":0}'

Make sure to replace <team_id>:<notebook_short_id>

Then typed a few characters in the notebook to trigger collab/save → got the modal → tested both Discard and Copy-to-new-notebook paths, plus closing the modal and re-typing.


Discard unsaved changes and reload:

2026-05-10 09 44 42

Copy unsaved version to new Notebook:

2026-05-10 09 48 00

Publish to changelog?

no

Docs update

🤖 Agent context

a-lider and others added 7 commits May 8, 2026 16:25
Replace the force-save PATCH path with the standard 409 rebase. Modal
opens only when PM-collab actually throws (parse / illegal transition)
or on a 410 stream-trim. Recovery actions are discard-and-reload or
copy-unsaved-to-new-notebook — no more PATCH-clobber that silently
overwrites concurrent edits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Long unbroken strings overflowed horizontally because grid columns
default to min-width: auto. Add min-w-0, switch overflow to y-only,
break-words for unbroken text, and bump max-h to 30rem.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(notebooks): use (duplicate) suffix for copied unsaved notebook

Match the convention used by duplicateNotebook elsewhere in the file
instead of the bespoke (unsaved changes) suffix.
setContent on a collab editor adds a 'replace whole doc' step to PM-
collab's sendable buffer, which then triggers another save against the
same trimmed stream and re-opens the modal. A page reload is the only
way to drop the plugin's pending steps alongside localContent. Also
drop the redundant loadNotebook in the copy-to-new-notebook path
since we immediately navigate away.
Copy link
Copy Markdown
Contributor Author

a-lider commented May 8, 2026

This stack of pull requests is managed by Graphite. Learn more about stacking.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 8, 2026

🎭 Playwright didn't run on this PR — your changes touch code that could affect E2E behavior, but Playwright is opt-in via label now to keep CI cost down.

Add the run-playwright label if you want an E2E sweep before merging — CI will pick it up automatically.

Most PRs don't need this. Real regressions still get caught on master and fix-forward.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 8, 2026

Size Change: +12.9 kB (+0.01%)

Total Size: 118 MB

📦 View Changed
Filename Size Change
frontend/dist/CalendarHeatMap 9 kB +3.97 kB (+78.74%) 🆘
frontend/dist/CustomerAnalyticsScene 26.8 kB -1.3 kB (-4.62%)
frontend/dist/ExporterNotebookScene 2.71 MB +4.8 kB (+0.18%)
frontend/dist/render-query.js 26.7 MB +1.43 kB (+0.01%)
frontend/dist/toolbar.js 14.9 MB +1.43 kB (+0.01%)
frontend/dist/TransformationsScene 2.16 kB -4.24 kB (-66.24%) 🏆
frontend/dist/WorkflowScene 109 kB +6.8 kB (+6.65%) 🔍
ℹ️ View Unchanged
Filename Size Change
frontend/dist/368Hedgehogs 5.51 kB 0 B
frontend/dist/abap 14.2 kB 0 B
frontend/dist/AccountConnected 2.99 kB 0 B
frontend/dist/Action 24.9 kB 0 B
frontend/dist/Actions 1.2 kB -63 B (-4.99%)
frontend/dist/AdvancedActivityLogsScene 39.9 kB 0 B
frontend/dist/AgenticAuthorize 5.7 kB 0 B
frontend/dist/apex 3.99 kB 0 B
frontend/dist/ApprovalDetail 16.5 kB 0 B
frontend/dist/architecture-7EHR7CIX 372 B 0 B
frontend/dist/architectureDiagram-3BPJPVTR 151 kB 0 B
frontend/dist/array.full.es5.js 347 kB 0 B
frontend/dist/array.full.js 427 kB 0 B
frontend/dist/array.js 191 kB 0 B
frontend/dist/AsyncMigrations 13.4 kB 0 B
frontend/dist/AuthenticatedShell 173 kB 0 B
frontend/dist/AuthorizationStatus 969 B 0 B
frontend/dist/azcli 885 B 0 B
frontend/dist/bat 1.88 kB 0 B
frontend/dist/BatchExportScene 60.8 kB 0 B
frontend/dist/bicep 2.59 kB 0 B
frontend/dist/Billing 731 B 0 B
frontend/dist/BillingSection 21 kB 0 B
frontend/dist/blockDiagram-GPEHLZMM 72.5 kB 0 B
frontend/dist/BoxPlot 5.25 kB -35 B (-0.66%)
frontend/dist/browserAll-0QZMN1W2 37.4 kB 0 B
frontend/dist/BusinessKnowledgeScene 18.9 kB -35 B (-0.19%)
frontend/dist/ButtonPrimitives 796 B 0 B
frontend/dist/c4Diagram-AAUBKEIU 70.8 kB 0 B
frontend/dist/cameligo 2.23 kB 0 B
frontend/dist/changeRequestsLogic 748 B 0 B
frontend/dist/classDiagram-4FO5ZUOK 1.28 kB 0 B
frontend/dist/classDiagram-v2-Q7XG4LA2 1.28 kB 0 B
frontend/dist/CLIAuthorize 11.6 kB 0 B
frontend/dist/CLILive 4.26 kB 0 B
frontend/dist/clojure 9.68 kB 0 B
frontend/dist/CodeEditorInline 696 B -34 B (-4.66%)
frontend/dist/coffee 3.63 kB 0 B
frontend/dist/Cohort 27.7 kB 0 B
frontend/dist/CohortCalculationHistory 6.47 kB 0 B
frontend/dist/Cohorts 9.64 kB 0 B
frontend/dist/ConfirmOrganization 4.76 kB 0 B
frontend/dist/conversations.js 67.3 kB 0 B
frontend/dist/cose-bilkent-S5V4N54A 82.8 kB 0 B
frontend/dist/Coupons 963 B 0 B
frontend/dist/cpp 5.33 kB 0 B
frontend/dist/Create 1.08 kB +179 B (+19.93%) 🚨
frontend/dist/crisp-chat-integration.js 1.97 kB 0 B
frontend/dist/csharp 4.56 kB 0 B
frontend/dist/csp 1.45 kB 0 B
frontend/dist/css 4.54 kB 0 B
frontend/dist/cssMode 4.2 kB 0 B
frontend/dist/CustomCssScene 3.8 kB 0 B
frontend/dist/CustomerAnalyticsConfigurationScene 2.27 kB 0 B
frontend/dist/CustomerJourneyBuilderScene 2.04 kB -35 B (-1.69%)
frontend/dist/CustomerJourneyTemplatesScene 7.75 kB 0 B
frontend/dist/customizations.full.js 18 kB 0 B
frontend/dist/CyclotronJobInputAssignee 1.54 kB 0 B
frontend/dist/CyclotronJobInputBusinessHours 2.92 kB -35 B (-1.18%)
frontend/dist/CyclotronJobInputTicketTags 920 B -34 B (-3.56%)
frontend/dist/cypher 3.42 kB 0 B
frontend/dist/dagre-BM42HDAG 11.9 kB 0 B
frontend/dist/dart 4.29 kB 0 B
frontend/dist/Dashboard 1.38 kB 0 B
frontend/dist/Dashboards 21.8 kB 0 B
frontend/dist/DashboardTemplateCopyScene 5.95 kB 0 B
frontend/dist/DataManagementScene 884 B 0 B
frontend/dist/DataPipelinesNewScene 2.55 kB 0 B
frontend/dist/DataWarehouseScene 46.5 kB 0 B
frontend/dist/Deactivated 1.37 kB 0 B
frontend/dist/dead-clicks-autocapture.js 14.3 kB 0 B
frontend/dist/DeadLetterQueue 5.63 kB 0 B
frontend/dist/DebugScene 20.2 kB 0 B
frontend/dist/decompressionWorker 2.85 kB 0 B
frontend/dist/decompressionWorker.js 2.85 kB 0 B
frontend/dist/DecompressionWorkerManager 329 B 0 B
frontend/dist/DefinitionEdit 8.82 kB 0 B
frontend/dist/DefinitionView 24.3 kB 0 B
frontend/dist/Deployment 3.94 kB +35 B (+0.9%)
frontend/dist/DeploymentProject 5.47 kB +35 B (+0.64%)
frontend/dist/Deployments 9.17 kB -36 B (-0.39%)
frontend/dist/DestinationsScene 2.92 kB 0 B
frontend/dist/diagram-2AECGRRQ 6.66 kB 0 B
frontend/dist/diagram-5GNKFQAL 3.61 kB 0 B
frontend/dist/diagram-KO2AKTUF 11.5 kB 0 B
frontend/dist/diagram-LMA3HP47 5.02 kB 0 B
frontend/dist/diagram-OG6HWLK6 11.8 kB 0 B
frontend/dist/dist 643 B 0 B
frontend/dist/dockerfile 1.91 kB 0 B
frontend/dist/EarlyAccessFeature 991 B +136 B (+15.91%) ⚠️
frontend/dist/EarlyAccessFeatures 3.05 kB 0 B
frontend/dist/ecl 5.38 kB 0 B
frontend/dist/EditorScene 1.38 kB 0 B
frontend/dist/elixir 10.3 kB 0 B
frontend/dist/elk.bundled 1.44 MB 0 B
frontend/dist/EmailMFAVerify 3.26 kB 0 B
frontend/dist/EndpointScene 39.9 kB 0 B
frontend/dist/EndpointsScene 23.9 kB -6 B (-0.03%)
frontend/dist/erDiagram-TEJ5UH35 27.7 kB 0 B
frontend/dist/ErrorTrackingIssueFingerprintsScene 7.22 kB 0 B
frontend/dist/ErrorTrackingIssueScene 101 kB +979 B (+0.98%)
frontend/dist/ErrorTrackingScene 27.1 kB 0 B
frontend/dist/EvaluationTemplates 779 B -34 B (-4.18%)
frontend/dist/eventmodeling-FCH6USID 375 B 0 B
frontend/dist/EventsScene 2.81 kB 0 B
frontend/dist/exception-autocapture.js 11.8 kB 0 B
frontend/dist/Experiment 210 kB -6 B (0%)
frontend/dist/Experiments 19.3 kB 0 B
frontend/dist/exporter 19 kB 0 B
frontend/dist/exporter.js 19 kB 0 B
frontend/dist/ExporterDashboardScene 1.78 kB 0 B
frontend/dist/ExporterHeatmapScene 19.5 kB 0 B
frontend/dist/ExporterInsightScene 2.88 kB 0 B
frontend/dist/ExporterInterviewScene 309 kB 0 B
frontend/dist/ExporterRecordingScene 995 B 0 B
frontend/dist/exporterSharedChunkAnchors 288 kB 0 B
frontend/dist/exporterSharedChunkAnchors.js 288 kB 0 B
frontend/dist/ExportsScene 4.23 kB 0 B
frontend/dist/FeatureFlag 134 kB 0 B
frontend/dist/FeatureFlags 844 B 0 B
frontend/dist/FeatureFlagTemplatesScene 7.24 kB 0 B
frontend/dist/FlappyHog 6.02 kB +34 B (+0.57%)
frontend/dist/flow9 1.85 kB 0 B
frontend/dist/flowDiagram-I6XJVG4X 61.6 kB 0 B
frontend/dist/freemarker2 16.7 kB 0 B
frontend/dist/fsharp 3.02 kB 0 B
frontend/dist/ganttDiagram-6RSMTGT7 50.9 kB 0 B
frontend/dist/gitGraph-WXDBUCRP 360 B 0 B
frontend/dist/gitGraphDiagram-PVQCEYII 30.2 kB 0 B
frontend/dist/go 2.69 kB 0 B
frontend/dist/graphql 2.3 kB 0 B
frontend/dist/Group 15.1 kB 0 B
frontend/dist/Groups 4.15 kB 0 B
frontend/dist/GroupsNew 7.58 kB 0 B
frontend/dist/handlebars 7.38 kB 0 B
frontend/dist/hcl 3.63 kB 0 B
frontend/dist/HealthCategoryDetailScene 7.48 kB 0 B
frontend/dist/HealthScene 12.4 kB 0 B
frontend/dist/HeatmapNewScene 5.27 kB 0 B
frontend/dist/HeatmapRecordingScene 4.25 kB 0 B
frontend/dist/HeatmapScene 6.8 kB 0 B
frontend/dist/HeatmapsScene 4.13 kB 0 B
frontend/dist/hls 394 kB 0 B
frontend/dist/HogFunctionScene 59.5 kB 0 B
frontend/dist/hogql_parser_wasm_browser 1.53 MB 0 B
frontend/dist/HogRepl 7.61 kB 0 B
frontend/dist/html 5.62 kB 0 B
frontend/dist/htmlMode 4.65 kB 0 B
frontend/dist/image-blob-reduce.esm 49.5 kB 0 B
frontend/dist/InboxScene 59.9 kB 0 B
frontend/dist/index 60.7 kB 0 B
frontend/dist/index.js 60.7 kB 0 B
frontend/dist/info-J43DQDTF 348 B 0 B
frontend/dist/infoDiagram-5YYISTIA 1.32 kB 0 B
frontend/dist/ini 1.14 kB 0 B
frontend/dist/InsightQuickStart 5.67 kB 0 B
frontend/dist/InsightScene 34.6 kB 0 B
frontend/dist/IntegrationsRedirect 976 B 0 B
frontend/dist/intercom-integration.js 2.03 kB 0 B
frontend/dist/InviteSignup 15.2 kB 0 B
frontend/dist/ishikawaDiagram-YF4QCWOH 18 kB 0 B
frontend/dist/java 3.26 kB 0 B
frontend/dist/javascript 1.02 kB 0 B
frontend/dist/journeyDiagram-JHISSGLW 24 kB 0 B
frontend/dist/jsonMode 13.9 kB 0 B
frontend/dist/julia 7.26 kB 0 B
frontend/dist/kanban-definition-UN3LZRKU 21.2 kB 0 B
frontend/dist/katex 266 kB 0 B
frontend/dist/kotlin 3.44 kB 0 B
frontend/dist/lazy 146 kB 0 B
frontend/dist/LegacyPluginScene 20.9 kB 0 B
frontend/dist/LegalDocumentNewScene 59.6 kB 0 B
frontend/dist/LegalDocumentsScene 5.17 kB 0 B
frontend/dist/LemonTextAreaMarkdown 740 B 0 B
frontend/dist/less 3.93 kB 0 B
frontend/dist/lexon 2.47 kB 0 B
frontend/dist/lib 2.25 kB 0 B
frontend/dist/Link 706 B +34 B (+5.06%) 🔍
frontend/dist/LinkScene 25 kB 0 B
frontend/dist/LinksScene 4.4 kB 0 B
frontend/dist/liquid 4.57 kB 0 B
frontend/dist/LiveDebugger 19.4 kB +37 B (+0.19%)
frontend/dist/LiveEventsTable 5.47 kB 0 B
frontend/dist/LLMAnalyticsClusterScene 21.5 kB 0 B
frontend/dist/LLMAnalyticsClustersScene 54.7 kB -38 B (-0.07%)
frontend/dist/LLMAnalyticsDatasetScene 20.7 kB 0 B
frontend/dist/LLMAnalyticsDatasetsScene 3.49 kB 0 B
frontend/dist/LLMAnalyticsEvaluation 59.6 kB 0 B
frontend/dist/LLMAnalyticsEvaluationsScene 28 kB 0 B
frontend/dist/LLMAnalyticsPlaygroundScene 37.6 kB +36 B (+0.1%)
frontend/dist/LLMAnalyticsScene 118 kB +345 B (+0.29%)
frontend/dist/LLMAnalyticsSessionScene 13.6 kB +41 B (+0.3%)
frontend/dist/LLMAnalyticsTag 27.3 kB 0 B
frontend/dist/LLMAnalyticsTagsScene 7.19 kB 0 B
frontend/dist/LLMAnalyticsTraceScene 130 kB -39 B (-0.03%)
frontend/dist/LLMAnalyticsUsers 730 B 0 B
frontend/dist/LLMASessionFeedbackDisplay 5.04 kB -35 B (-0.69%)
frontend/dist/LLMPromptScene 17.8 kB 0 B
frontend/dist/LLMPromptsScene 4.68 kB 0 B
frontend/dist/LLMSkillScene 793 B -34 B (-4.11%)
frontend/dist/LLMSkillsScene 810 B -34 B (-4.03%)
frontend/dist/Login 8.86 kB 0 B
frontend/dist/Login2FA 4.49 kB 0 B
frontend/dist/logs.js 38.9 kB 0 B
frontend/dist/LogsAlertDetailScene 17.2 kB 0 B
frontend/dist/LogsSamplingDetailScene 4.44 kB -36 B (-0.8%)
frontend/dist/LogsSamplingNewScene 1.97 kB -35 B (-1.74%)
frontend/dist/LogsScene 18.1 kB 0 B
frontend/dist/lua 2.16 kB 0 B
frontend/dist/m3 2.85 kB 0 B
frontend/dist/main 819 kB 0 B
frontend/dist/ManagedMigration 14.4 kB 0 B
frontend/dist/markdown 3.83 kB 0 B
frontend/dist/MarketingAnalyticsScene 40.3 kB 0 B
frontend/dist/MaterializedColumns 10.4 kB 0 B
frontend/dist/Max 888 B 0 B
frontend/dist/mdx 5.43 kB 0 B
frontend/dist/memlens.lib.bundle 27.9 kB 0 B
frontend/dist/mermaid.core 28.5 kB 0 B
frontend/dist/MermaidDiagram 2.15 kB 0 B
frontend/dist/MessageTemplate 16.5 kB +70 B (+0.43%)
frontend/dist/MetricsScene 1.04 kB 0 B
frontend/dist/mindmap-definition-RKZ34NQL 24.8 kB 0 B
frontend/dist/mips 2.62 kB 0 B
frontend/dist/ModelsScene 18.9 kB +36 B (+0.19%)
frontend/dist/MonacoDiffEditor 471 B 0 B
frontend/dist/monacoEditorWorker 288 kB 0 B
frontend/dist/monacoEditorWorker.js 288 kB 0 B
frontend/dist/monacoJsonWorker 419 kB 0 B
frontend/dist/monacoJsonWorker.js 419 kB 0 B
frontend/dist/monacoTsWorker 7.02 MB 0 B
frontend/dist/monacoTsWorker.js 7.02 MB 0 B
frontend/dist/MoveToPostHogCloud 4.7 kB 0 B
frontend/dist/msdax 4.95 kB 0 B
frontend/dist/mysql 11.3 kB 0 B
frontend/dist/NavTabChat 7.41 kB 0 B
frontend/dist/NewSourceScene 1.05 kB 0 B
frontend/dist/NewTabScene 1.72 kB 0 B
frontend/dist/NodeDetailScene 16.9 kB -33 B (-0.19%)
frontend/dist/NotebookCanvasScene 3.55 kB 0 B
frontend/dist/NotebookPanel 5.51 kB 0 B
frontend/dist/NotebookScene 8.75 kB 0 B
frontend/dist/NotebooksScene 7.84 kB 0 B
frontend/dist/OAuthAuthorize 844 B 0 B
frontend/dist/objective-c 2.44 kB 0 B
frontend/dist/Onboarding 771 kB 0 B
frontend/dist/OnboardingCouponRedemption 1.44 kB 0 B
frontend/dist/packet-YPE3B663 354 B 0 B
frontend/dist/pascal 3.03 kB 0 B
frontend/dist/pascaligo 2.04 kB 0 B
frontend/dist/passkeyLogic 722 B 0 B
frontend/dist/PasswordReset 4.6 kB 0 B
frontend/dist/PasswordResetComplete 3.23 kB 0 B
frontend/dist/PendingDeletion 2.38 kB 0 B
frontend/dist/perl 8.29 kB 0 B
frontend/dist/PersonScene 18.5 kB 0 B
frontend/dist/PersonsScene 5.91 kB 0 B
frontend/dist/pgsql 13.5 kB 0 B
frontend/dist/php 8.06 kB 0 B
frontend/dist/pie-LRSECV5Y 345 B 0 B
frontend/dist/pieDiagram-4H26LBE5 4.92 kB 0 B
frontend/dist/PipelineStatusScene 9.35 kB 0 B
frontend/dist/pla 1.72 kB 0 B
frontend/dist/posthog 146 kB 0 B
frontend/dist/postiats 7.9 kB 0 B
frontend/dist/powerquery 17 kB 0 B
frontend/dist/powershell 3.31 kB 0 B
frontend/dist/PreflightCheck 5.81 kB 0 B
frontend/dist/product-tours.js 115 kB 0 B
frontend/dist/ProductTour 275 kB 0 B
frontend/dist/ProductTours 4.92 kB 0 B
frontend/dist/ProjectHomepage 19.9 kB 0 B
frontend/dist/protobuf 9.09 kB 0 B
frontend/dist/pug 4.86 kB 0 B
frontend/dist/python 4.8 kB 0 B
frontend/dist/qsharp 3.23 kB 0 B
frontend/dist/quadrantDiagram-W4KKPZXB 34.4 kB 0 B
frontend/dist/QueryPerformance 8.86 kB 0 B
frontend/dist/r 3.16 kB 0 B
frontend/dist/radar-GUYGQ44K 351 B 0 B
frontend/dist/razor 9.38 kB 0 B
frontend/dist/react-json-view 121 kB 0 B
frontend/dist/recorder-v2.js 98.6 kB 0 B
frontend/dist/recorder.js 98.6 kB 0 B
frontend/dist/redis 3.59 kB 0 B
frontend/dist/redshift 11.8 kB 0 B
frontend/dist/RegionMap 29.6 kB 0 B
frontend/dist/render-query 26.7 MB 0 B
frontend/dist/ReplayLens 21.3 kB 0 B
frontend/dist/ReplayLensesScene 12.2 kB +36 B (+0.3%)
frontend/dist/requirementDiagram-4Y6WPE33 31.9 kB 0 B
frontend/dist/ResourceTransfer 9.42 kB 0 B
frontend/dist/restructuredtext 3.94 kB 0 B
frontend/dist/RevenueAnalyticsScene 25.8 kB 0 B
frontend/dist/ruby 8.54 kB 0 B
frontend/dist/rust 4.2 kB 0 B
frontend/dist/sankeyDiagram-5OEKKPKP 24 kB 0 B
frontend/dist/SavedInsights 902 B 0 B
frontend/dist/sb 1.86 kB 0 B
frontend/dist/scala 7.36 kB 0 B
frontend/dist/schema 720 kB 0 B
frontend/dist/SchemaScene 21.2 kB +36 B (+0.17%)
frontend/dist/scheme 1.8 kB 0 B
frontend/dist/scss 6.45 kB 0 B
frontend/dist/SdkDoctorScene 9.65 kB 0 B
frontend/dist/sequenceDiagram-3UESZ5HK 117 kB 0 B
frontend/dist/SessionAttributionExplorerScene 6.87 kB 0 B
frontend/dist/SessionGroupSummariesTable 4.84 kB -35 B (-0.72%)
frontend/dist/SessionGroupSummaryScene 17.2 kB 0 B
frontend/dist/SessionProfileScene 15.3 kB 0 B
frontend/dist/SessionRecordingDetail 2 kB 0 B
frontend/dist/SessionRecordingFilePlaybackScene 4.71 kB 0 B
frontend/dist/SessionRecordings 980 B 0 B
frontend/dist/SessionRecordingsKiosk 10.1 kB 0 B
frontend/dist/SessionRecordingsPlaylistScene 5.28 kB 0 B
frontend/dist/SessionRecordingsSettingsScene 2.14 kB 0 B
frontend/dist/SessionsScene 4.22 kB 0 B
frontend/dist/SettingsScene 3.33 kB 0 B
frontend/dist/sharedChunkAnchors 236 kB 0 B
frontend/dist/sharedChunkAnchors.js 236 kB 0 B
frontend/dist/SharedMetric 6.03 kB 0 B
frontend/dist/SharedMetrics 787 B 0 B
frontend/dist/shell 3.11 kB 0 B
frontend/dist/SignupContainer 28.4 kB 0 B
frontend/dist/Site 1.43 kB 0 B
frontend/dist/solidity 18.6 kB 0 B
frontend/dist/sophia 2.8 kB 0 B
frontend/dist/SourceScene 962 B +34 B (+3.66%)
frontend/dist/SourcesScene 6.2 kB +35 B (+0.57%)
frontend/dist/sparql 2.59 kB 0 B
frontend/dist/sql 10.3 kB 0 B
frontend/dist/SqlVariableEditScene 7.49 kB 0 B
frontend/dist/st 7.44 kB 0 B
frontend/dist/StartupProgram 21.4 kB 0 B
frontend/dist/stateDiagram-AJRCARHV 11.3 kB 0 B
frontend/dist/stateDiagram-v2-BHNVJYJU 1.18 kB 0 B
frontend/dist/StripeConfirmInstall 3.78 kB 0 B
frontend/dist/SubscriptionScene 14.4 kB 0 B
frontend/dist/SubscriptionsScene 5.42 kB 0 B
frontend/dist/SupportSettingsScene 1.68 kB +35 B (+2.13%)
frontend/dist/SupportTicketScene 33.8 kB 0 B
frontend/dist/SupportTicketsScene 937 B -34 B (-3.5%)
frontend/dist/Survey 1.12 kB 0 B
frontend/dist/SurveyFormBuilder 1.78 kB 0 B
frontend/dist/Surveys 26.6 kB 0 B
frontend/dist/surveys.js 94.7 kB 0 B
frontend/dist/SurveyWizard 70.5 kB 0 B
frontend/dist/swift 5.3 kB 0 B
frontend/dist/SystemStatus 17.1 kB 0 B
frontend/dist/systemverilog 7.65 kB 0 B
frontend/dist/TaskDetailScene 23.3 kB 0 B
frontend/dist/TaskTracker 14.4 kB -38 B (-0.26%)
frontend/dist/tcl 3.61 kB 0 B
frontend/dist/TextCardMarkdownEditor 11.2 kB 0 B
frontend/dist/timeline-definition-PNZ67QCA 31.3 kB 0 B
frontend/dist/toolbar 14.9 MB 0 B
frontend/dist/ToolbarLaunch 2.71 kB 0 B
frontend/dist/tracing-headers.js 1.74 kB 0 B
frontend/dist/TracingScene 54.2 kB 0 B
frontend/dist/treemap-LRROVOQU 357 B 0 B
frontend/dist/treeView-BLDUP644 360 B 0 B
frontend/dist/TrendsBarChart 6.74 kB 0 B
frontend/dist/TrendsLineChart 6.8 kB 0 B
frontend/dist/tsMode 24 kB 0 B
frontend/dist/twig 6.01 kB 0 B
frontend/dist/TwoFactorReset 4.27 kB 0 B
frontend/dist/typescript 274 B 0 B
frontend/dist/typespec 2.86 kB 0 B
frontend/dist/Unsubscribe 1.9 kB 0 B
frontend/dist/UserInterview 6.25 kB -35 B (-0.56%)
frontend/dist/UserInterviewResponse 5.54 kB 0 B
frontend/dist/UserInterviews 3.77 kB 0 B
frontend/dist/vb 5.83 kB 0 B
frontend/dist/vennDiagram-CIIHVFJN 41.6 kB 0 B
frontend/dist/VercelConnect 5.23 kB 0 B
frontend/dist/VercelLinkError 2.5 kB 0 B
frontend/dist/VerifyEmail 5.02 kB 0 B
frontend/dist/vimMode 211 kB 0 B
frontend/dist/VisualReviewIndexScene 2.41 kB 0 B
frontend/dist/VisualReviewRunScene 44.3 kB 0 B
frontend/dist/VisualReviewRunsScene 7.21 kB +38 B (+0.53%)
frontend/dist/VisualReviewSettingsScene 11 kB 0 B
frontend/dist/VisualReviewSnapshotHistoryScene 12.4 kB 0 B
frontend/dist/VisualReviewSnapshotOverviewScene 17.5 kB 0 B
frontend/dist/wardley-L42UT6IY 352 B 0 B
frontend/dist/wardleyDiagram-YWT4CUSO 26.2 kB 0 B
frontend/dist/web-vitals-with-attribution.js 11.8 kB 0 B
frontend/dist/web-vitals.js 6.39 kB 0 B
frontend/dist/WebAnalyticsScene 9.77 kB 0 B
frontend/dist/WebGLRenderer-DYjOwNoG 60.4 kB 0 B
frontend/dist/WebGPURenderer-B_wkl_Ja 36.3 kB 0 B
frontend/dist/WebScriptsScene 2.78 kB 0 B
frontend/dist/WebVitals 7.41 kB +35 B (+0.47%)
frontend/dist/WebVitalsPathBreakdown 3.89 kB +35 B (+0.91%)
frontend/dist/webworkerAll-puPV1rBA 397 B 0 B
frontend/dist/wgsl 7.38 kB 0 B
frontend/dist/Wizard 4.7 kB 0 B
frontend/dist/WorkflowsScene 60 kB 0 B
frontend/dist/WorldMap 1.04 MB 0 B
frontend/dist/xml 3.02 kB 0 B
frontend/dist/xychartDiagram-2RQKCTM6 39.6 kB 0 B
frontend/dist/yaml 4.64 kB 0 B

compressed-size-action

…eads

NotebookNodeQuery was calling updateAttributes({ ...attributes, x: y })
in three places. Tiptap merges, so the spread was redundant. It also
dragged the existing query object through useSyncedAttributes' string-
ifier, which then disagreed with the un-stringified previousNodeAttrs
and dispatched a no-op transaction — prosemirror-collab caught that as
a local edit and tried to save, looping on 410 after a Discard+reload.

Discard now just clearLocalContent + window.location.reload to remount
the editor with server content (initialContent path, no setContent
transaction).
@a-lider a-lider force-pushed the fix/notebook-stale-conflict-modal branch from b22dbb1 to 1e94554 Compare May 9, 2026 20:20
a-lider added 2 commits May 10, 2026 16:07
The modal handles both 410 stale and rebase failures, not just stale —
rename for accuracy. NotebookStaleConflictModal -> NotebookCollabConflictModal,
staleConflict -> collabConflict, showStaleConflict -> showCollabConflict,
dismissStaleConflict -> dismissCollabConflict.
Collab notebooks update via SSE, not setContent. Restricting the
polling-refresh setContent to !collabEnabled avoids dirtying PM-collab's
sendable buffer when scheduleNotebookRefresh fires.

if (isDataTableNode(modifiedQuery) && isEventsQuery(modifiedQuery.source)) {
modifiedQuery.source.fixedProperties = canvasFiltersOverride
updateAttributes({ ...attributes, isDefaultFilterApplied: true })
Copy link
Copy Markdown
Contributor Author

@a-lider a-lider May 10, 2026

Choose a reason for hiding this comment

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

I cleaned it up here. Spreading ..attributes was causing a no-op transaction save after reload, which triggered the modal again.

Tiptap’s updateAttributes(partial) always merges into existing attrs, so passing the full set is redundant.

a-lider added 3 commits May 12, 2026 06:38
useSyncedAttributes' hasChanges compared prosemirror state directly to
stringifiedAttrs. When notebook content was loaded from JSON with the
value as an object, the wrapper's stringification falsely registered as
a change and dispatched a no-op transaction that prosemirror-collab
caught as a local edit — looping on 410 after a Discard+reload.
@a-lider a-lider marked this pull request as ready for review May 12, 2026 11:39
@assign-reviewers-posthog assign-reviewers-posthog Bot requested a review from a team May 12, 2026 11:40
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 12, 2026

Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
frontend/src/scenes/notebooks/Notebook/notebookCollabLogic.ts:152-155
**`serverContent` is identical to `localContent` when the step throws**

`applyRemoteStep` throws before `editor.view.dispatch(tr)` is called (e.g. during `Step.fromJSON` or `receiveTransaction`), so the editor state is never mutated. `editor.getJSON()` in the catch block therefore returns the same document as `localContent` — both columns in the conflict modal render the same content, defeating the side-by-side preview.

The 410 path handles this correctly by using `error.data.content` (the actual server payload). The `rebaseFailed` path should do a similar lookup — the `notebookLogic` listener for `rebaseFailed` is already async and could fetch the latest server content there before calling `showCollabConflict`. The same issue exists in the SSE `onMessage` catch block at line 203.

### Issue 2 of 2
frontend/src/scenes/notebooks/Notebook/notebookLogic.ts:472-474
**`collabConflict` check is unreliable here — listeners are async**

`actions.applyRemoteSteps(steps)` only dispatches the action; the `applyRemoteSteps` listener (and the downstream `rebaseFailed``showCollabConflict` chain that sets `collabConflict`) is queued as an async task and won't have run yet when execution reaches this guard. In practice `values.collabConflict` will always be `null` at this point, so `actions.saveNotebook(...)` is unconditionally called even when the rebase failed. The more durable guard is the one at line 427, but it is subject to the same race during the retry (the listener chain may finish in the microtask gap around `await api.create`). Consider using a local flag or restructuring so the rebase result is known synchronously before deciding whether to retry.

Reviews (1): Last reviewed commit: "Simplify comments" | Re-trigger Greptile

Comment on lines +152 to +155
} catch (e) {
posthog.captureException(e as Error, { action: 'notebook collab apply remote step' })
actions.rebaseFailed({ serverContent: editor.getJSON(), localContent, localText })
return
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 serverContent is identical to localContent when the step throws

applyRemoteStep throws before editor.view.dispatch(tr) is called (e.g. during Step.fromJSON or receiveTransaction), so the editor state is never mutated. editor.getJSON() in the catch block therefore returns the same document as localContent — both columns in the conflict modal render the same content, defeating the side-by-side preview.

The 410 path handles this correctly by using error.data.content (the actual server payload). The rebaseFailed path should do a similar lookup — the notebookLogic listener for rebaseFailed is already async and could fetch the latest server content there before calling showCollabConflict. The same issue exists in the SSE onMessage catch block at line 203.

Prompt To Fix With AI
This is a comment left during a code review.
Path: frontend/src/scenes/notebooks/Notebook/notebookCollabLogic.ts
Line: 152-155

Comment:
**`serverContent` is identical to `localContent` when the step throws**

`applyRemoteStep` throws before `editor.view.dispatch(tr)` is called (e.g. during `Step.fromJSON` or `receiveTransaction`), so the editor state is never mutated. `editor.getJSON()` in the catch block therefore returns the same document as `localContent` — both columns in the conflict modal render the same content, defeating the side-by-side preview.

The 410 path handles this correctly by using `error.data.content` (the actual server payload). The `rebaseFailed` path should do a similar lookup — the `notebookLogic` listener for `rebaseFailed` is already async and could fetch the latest server content there before calling `showCollabConflict`. The same issue exists in the SSE `onMessage` catch block at line 203.

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Changed that to fetch fresh notebook content when the conflict modal is opened

Comment on lines +472 to +474
if (values.collabConflict) {
return values.notebook
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 collabConflict check is unreliable here — listeners are async

actions.applyRemoteSteps(steps) only dispatches the action; the applyRemoteSteps listener (and the downstream rebaseFailedshowCollabConflict chain that sets collabConflict) is queued as an async task and won't have run yet when execution reaches this guard. In practice values.collabConflict will always be null at this point, so actions.saveNotebook(...) is unconditionally called even when the rebase failed. The more durable guard is the one at line 427, but it is subject to the same race during the retry (the listener chain may finish in the microtask gap around await api.create). Consider using a local flag or restructuring so the rebase result is known synchronously before deciding whether to retry.

Prompt To Fix With AI
This is a comment left during a code review.
Path: frontend/src/scenes/notebooks/Notebook/notebookLogic.ts
Line: 472-474

Comment:
**`collabConflict` check is unreliable here — listeners are async**

`actions.applyRemoteSteps(steps)` only dispatches the action; the `applyRemoteSteps` listener (and the downstream `rebaseFailed``showCollabConflict` chain that sets `collabConflict`) is queued as an async task and won't have run yet when execution reaches this guard. In practice `values.collabConflict` will always be `null` at this point, so `actions.saveNotebook(...)` is unconditionally called even when the rebase failed. The more durable guard is the one at line 427, but it is subject to the same race during the retry (the listener chain may finish in the microtask gap around `await api.create`). Consider using a local flag or restructuring so the rebase result is known synchronously before deciding whether to retry.

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Not critical, worst case is 1 extra collab/save retry and showCollabConflict firing twice (modal re-renders)

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 12, 2026

Reviews (2): Last reviewed commit: "fix(playwright): satisfy ty for setup he..." | Re-trigger Greptile

@a-lider a-lider force-pushed the fix/notebook-stale-conflict-modal branch from 1ff080d to ba867cf Compare May 12, 2026 12:03
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 12, 2026

Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
frontend/src/scenes/notebooks/Notebook/notebookLogic.ts:1120-1126
**Conflicting toasts when navigation fails after successful copy**

`lemonToast.success`, `actions.dismissCollabConflict()`, and `actions.clearLocalContent()` all run before `await openNotebook(...)`. If `openNotebook` rejects (e.g. a routing error), the single `catch` block fires and shows "Could not copy your changes to a new notebook" — but the notebook *was* already created and the success toast already displayed. The user sees two simultaneous toasts with opposite meanings, with the conflict modal gone and no clear path to the new notebook.

```suggestion
                actions.dismissCollabConflict()
                actions.clearLocalContent()
                try {
                    await openNotebook(created.short_id, NotebookTarget.Scene)
                } catch {
                    lemonToast.success('Saved your unsaved changes to a new notebook. Navigate there manually if needed.')
                    return
                }
                lemonToast.success('Saved your unsaved changes to a new notebook.')
            } catch {
                lemonToast.error('Could not copy your changes to a new notebook.')
            }
```

Reviews (3): Last reviewed commit: "Simplify comments" | Re-trigger Greptile


return (
<LemonModal
isOpen
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think its probably better to pass the open state directly to the modal component instead of conditionally rendering this entire component which will cause mount & remount every time its visibility is toggled.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Makes sense, fixed that


// Icons for the most common embed types.
// Anything not listed here gets the generic IconNotebook fallback.
const NODE_ICONS: Partial<Record<NotebookNodeType, JSX.Element>> = {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do we already have this defined somewhere else? For example in the insert node menu? If yes, then is this necessary. If no, then we should update those other places to use this map.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yeah, thought about that. Ideally this would live on KNOWN_NODES next to titlePlaceholder, but it means touching 30+ NotebookNode*.tsx files just to add an icon: field, and this preview is the only place that needs a node-type → icon map.

The existing slash command icons map to specific subtypes (Trend, Funnel, Retention, Python query…) rather than the parent node type (e.g. ph-query covers a bunch of those), so reusing them didn't quite fit here.

Passing isOpen={!!collabConflict} lets LemonModal play its close
animation and keeps the component's kea subscriptions alive across
open/close, instead of remounting on every toggle.
@a-lider a-lider enabled auto-merge (squash) May 18, 2026 19:47
@posthog
Copy link
Copy Markdown
Contributor

posthog Bot commented May 18, 2026

👋 Visual changes detected for this PR.

Review and approve in PostHog Visual Review

If these changes are unexpected, they may be caused by a flaky test or a broken snapshot on master. Don't approve — rerun the job or wait for a fix.

@a-lider a-lider merged commit 9bf44b3 into master May 18, 2026
343 of 349 checks passed
@a-lider a-lider deleted the fix/notebook-stale-conflict-modal branch May 18, 2026 21:39
@deployment-status-posthog
Copy link
Copy Markdown

deployment-status-posthog Bot commented May 18, 2026

Deploy status

Environment Status Deployed At Workflow
dev ✅ Deployed 2026-05-18 22:18 UTC Run
prod-us ✅ Deployed 2026-05-18 22:35 UTC Run
prod-eu ✅ Deployed 2026-05-18 22:38 UTC Run

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.

2 participants