//! Remote diff state model.//!//! Client-side model for a single remote repository diff state subscription//! received from the remote server. Presents the same read API as//! `LocalDiffStateModel` and emits the same `DiffStateModelEvent` variants.//!//! The active [`DiffMode`] can change; the model handles this by unsubscribing//! from the old `(repo_path, mode)` subscription and re-subscribing with the//! new mode. use std::sync::Arc; use instant::Instant;use remote_server::manager::{RemoteServerManager, RemoteServerManagerEvent};use warp_core::{HostId, SessionId, send_telemetry_from_ctx};use warp_util::remote_path::RemotePath;use warp_util::standardized_path::StandardizedPath;use warpui::{ModelContext, SingletonEntity}; use super::{ BackendOrigin, CommitChainMode, DiffMetadata, DiffMode, DiffOperation, DiffState, DiffStateError, DiffStateModelEvent, DiffStats, FileDiffAndContent, GitDiffData, GitDiffWithBaseContent,};use crate::code_review::telemetry_event::CodeReviewTelemetryEvent;use crate::remote_server::diff_state_proto::{try_decode_file_delta, try_decode_snapshot};use crate::remote_server::proto;use crate::util::git::{BranchEntry, Commit, FileChangeEntry, PrInfo}; // ── Internal state ──────────────────────────────────────────────── #[derive(Default)]enum InternalRemoteDiffState { #[default] Loading, NotInRepository, Loaded(GitDiffData), Error(String), /// The remote connection was lost. Preserves stale data until the model /// can re-establish the server-side subscription. Disconnected,} // ── Model ──────────────────────────────────────────────────────────────────── pub struct RemoteDiffStateModel { remote_path: RemotePath, mode: DiffMode, state: InternalRemoteDiffState, metadata: Option<DiffMetadata>, /// Start time for the latest caller-tracked full diff snapshot request. tracked_diff_load_start_time: Option<Instant>,} impl warpui::Entity for RemoteDiffStateModel { type Event = DiffStateModelEvent;} impl RemoteDiffStateModel { /// Creates a new remote diff state model. /// /// Identity is `(host_id, repo_path, mode)`. The model is session-agnostic: /// the manager resolves a connected session for the host on every outbound /// RPC, and host-level connect/disconnect events drive subscription /// lifecycle. /// /// `preferred_session` is the session that opened this review (the /// triggering callsite). It is used only for the *initial* `GetDiffState` /// dispatch and is deliberately not stored: a shared, long-lived model /// must not pin a session, and later re-triggers supply their own session /// (or `None`) rather than reusing a stale one. /// /// A session for this host is required at construction time. The model starts in `Loading` and /// issues the initial `GetDiffState` request. Runtime disconnects transition the model through /// `mark_disconnected`; subsequent reconnects re-subscribe via the `HostConnected` event handler. pub fn new( remote_path: RemotePath, mode: DiffMode, preferred_session: Option<SessionId>, ctx: &mut ModelContext<Self>, ) -> Self { // Subscribe to RemoteServerManager push events and filter by remote_path and diff_mode let mgr_handle = RemoteServerManager::handle(ctx); ctx.subscribe_to_model(&mgr_handle, |me, _, event, ctx| { me.handle_manager_event(event, ctx) }); let host_id = remote_path.host_id.clone(); let repo_path = remote_path.path.clone(); let mode_clone = mode.clone(); mgr_handle.update(ctx, |mgr, ctx| { mgr.get_diff_state( host_id, repo_path, proto::DiffMode::from(&mode_clone), preferred_session, ctx, ); }); Self { remote_path, mode, state: InternalRemoteDiffState::Loading, metadata: None, tracked_diff_load_start_time: None, } } // ── Event handler ─────────────────────────────────────────── fn matches_remote_path_and_mode( &self, host_id: &HostId, repo_path: &StandardizedPath, mode: &proto::DiffMode, ) -> bool { let remote_mode = proto::DiffMode::from(&self.mode); self.remote_path.matches(host_id, repo_path) && mode == &remote_mode } fn handle_manager_event( &mut self, event: &RemoteServerManagerEvent, ctx: &mut ModelContext<Self>, ) { match event { RemoteServerManagerEvent::DiffStateSnapshotReceived { host_id, repo_path, mode, snapshot, } => { if !self.matches_remote_path_and_mode(host_id, repo_path, mode) { return; } self.handle_snapshot_received(snapshot, ctx); } RemoteServerManagerEvent::DiffStateMetadataUpdateReceived { host_id, repo_path, mode, update, } => { if !self.matches_remote_path_and_mode(host_id, repo_path, mode) { return; } self.handle_metadata_update_received(update, ctx); } RemoteServerManagerEvent::DiffStateFileDeltaReceived { host_id, repo_path, mode, delta, } => { if !self.matches_remote_path_and_mode(host_id, repo_path, mode) { return; } self.handle_file_delta_received(delta, ctx); } RemoteServerManagerEvent::GetBranchesResponse { repo_path, result, .. } if repo_path == &self.remote_path.path => { let branches = match result { Ok(branch_infos) => branch_infos .iter() .map(|info| BranchEntry { name: info.name.clone(), is_main: info.is_main, }) .collect(), Err(err) => { log::warn!("RemoteDiffStateModel: GetBranches failed: {err}"); vec![] } }; ctx.emit(DiffStateModelEvent::BranchesReceived(branches)); } RemoteServerManagerEvent::CommitChainResponse { host_id, repo_path, result, } if self.remote_path.matches(host_id, repo_path) => { self.handle_git_commit_chain_response(result, ctx); } RemoteServerManagerEvent::GitPushResponse { host_id, repo_path, result, } if self.remote_path.matches(host_id, repo_path) => { self.handle_git_push_response(result, ctx); } RemoteServerManagerEvent::CreatePrResponse { host_id, repo_path, result, } if self.remote_path.matches(host_id, repo_path) => { self.handle_create_pr_response(result, ctx); } RemoteServerManagerEvent::GenerateCommitMessageResponse { host_id, repo_path, result, } if self.remote_path.matches(host_id, repo_path) => { // AI ran on the daemon; just relay the result to the dialog. ctx.emit(DiffStateModelEvent::CommitMessageGenerated(result.clone())); } RemoteServerManagerEvent::GetCommittedBranchFilesResponse { host_id, repo_path, result, } if self.remote_path.matches(host_id, repo_path) => { self.handle_get_committed_branch_files_response(result, ctx); } RemoteServerManagerEvent::HostDisconnected { host_id } if host_id == &self.remote_path.host_id => { self.mark_disconnected(ctx); } RemoteServerManagerEvent::HostConnected { host_id } if host_id == &self.remote_path.host_id && matches!(self.state, InternalRemoteDiffState::Disconnected) => { // Reconnect is event-driven with no viewing-session in scope // (and the prior session may be gone), so re-subscribe over // any connected session for the host. self.resubscribe(false, None, ctx); } _ => {} } } /// Marks the model as disconnected, preserving any stale data and /// emitting `ConnectionLost`. fn mark_disconnected(&mut self, ctx: &mut ModelContext<Self>) { if matches!(self.state, InternalRemoteDiffState::Disconnected) { return; } self.tracked_diff_load_start_time = None; self.state = InternalRemoteDiffState::Disconnected; ctx.emit(DiffStateModelEvent::ConnectionLost); } /// Re-sends `GetDiffState` for this model's `(host_id, repo, mode)` and /// transitions to `Loading` while waiting for a fresh snapshot. /// /// `preferred_session` is supplied by the triggering callsite (the /// session-scoped view) so the request rides the connection that needs the /// result; `None` (e.g. reconnect) falls back to any connected session. fn resubscribe( &mut self, track_load_duration: bool, preferred_session: Option<SessionId>, ctx: &mut ModelContext<Self>, ) { // Always overwrite to avoid carrying a stale `Instant` from a prior // tracked load that was interrupted by a session blip. self.tracked_diff_load_start_time = track_load_duration.then(Instant::now); let host_id = self.remote_path.host_id.clone(); let repo_path = self.remote_path.path.clone(); let mode = self.mode.clone(); RemoteServerManager::handle(ctx).update(ctx, |mgr, ctx| { mgr.get_diff_state( host_id, repo_path, proto::DiffMode::from(&mode), preferred_session, ctx, ); }); self.state = InternalRemoteDiffState::Loading; ctx.emit(DiffStateModelEvent::NewDiffsComputed { diffs: None, load_duration: None, }); } // ── Proto → state conversion helpers ──────────────────────────────────────────────── fn handle_snapshot_received( &mut self, snapshot: &proto::DiffStateSnapshot, ctx: &mut ModelContext<Self>, ) { match try_decode_snapshot(snapshot) { Ok((metadata, state, diffs)) => self.apply_snapshot(metadata, state, diffs, ctx), Err(error) => { self.tracked_diff_load_start_time = None; warp_core::safe_error!( safe: ("RemoteDiffStateModel: failed to decode diff state snapshot"), full: ("RemoteDiffStateModel: failed to decode diff state snapshot: {error}") ); } } } fn handle_metadata_update_received( &mut self, update: &proto::DiffStateMetadataUpdate, ctx: &mut ModelContext<Self>, ) { match update .metadata .as_ref() .map(DiffMetadata::try_from) .transpose() { Ok(Some(metadata)) => self.apply_metadata_update(&metadata, ctx), Ok(None) => {} Err(error) => { warp_core::safe_error!( safe: ("RemoteDiffStateModel: failed to decode diff state metadata update"), full: ("RemoteDiffStateModel: failed to decode diff state metadata update: {error}") ); } } } fn handle_file_delta_received( &mut self, delta: &proto::DiffStateFileDelta, ctx: &mut ModelContext<Self>, ) { match try_decode_file_delta(delta) { Ok((file_path, diff, metadata)) => { self.apply_file_delta(file_path, diff, metadata, ctx) } Err(error) => { warp_core::safe_error!( safe: ("RemoteDiffStateModel: failed to decode diff state file delta"), full: ("RemoteDiffStateModel: failed to decode diff state file delta: {error}") ); } } } // ── Apply methods ────────────────────────────────────────────────────── /// Requests a fresh diff snapshot from the remote server, including file /// content. Unlike the former `replay_latest_diffs` (which reconstructed /// data from cached `GitDiffData` and lost `content_at_head`), this sends /// an actual `GetDiffState` RPC so the server can reload content from disk. /// /// Does NOT transition to `Loading` or emit `NewDiffsComputed(None)` first, /// so existing views subscribed to this model won't flash a loading state. /// The server response arrives as a `DiffStateSnapshotReceived` event and /// flows through `apply_snapshot` normally. pub(crate) fn fetch_fresh_snapshot( &mut self, track_load_duration: bool, preferred_session: Option<SessionId>, ctx: &mut ModelContext<Self>, ) { if track_load_duration { self.tracked_diff_load_start_time = Some(Instant::now()); } let host_id = self.remote_path.host_id.clone(); let repo_path = self.remote_path.path.clone(); let mode = self.mode.clone(); // `preferred_session` is supplied per-call by the triggering view (the // session showing the review); `None` falls back to any connected // session for the host. Never cached on this shared model. RemoteServerManager::handle(ctx).update(ctx, |mgr, ctx| { mgr.get_diff_state( host_id, repo_path, proto::DiffMode::from(&mode), preferred_session, ctx, ); }); } fn apply_snapshot( &mut self, metadata: Option<DiffMetadata>, state: DiffState, diffs: Option<GitDiffWithBaseContent>, ctx: &mut ModelContext<Self>, ) { // Update metadata, detecting branch changes. if let Some(metadata) = &metadata { self.apply_metadata_update(metadata, ctx); } // Update state. match state { // Disconnected is never produced by proto deserialization. DiffState::Disconnected => {} DiffState::NotInRepository => { self.tracked_diff_load_start_time = None; self.state = InternalRemoteDiffState::NotInRepository; ctx.emit(DiffStateModelEvent::NewDiffsComputed { diffs: None, load_duration: None, }); } DiffState::Loading => { self.state = InternalRemoteDiffState::Loading; ctx.emit(DiffStateModelEvent::NewDiffsComputed { diffs: None, load_duration: None, }); } DiffState::Error(msg) => { let load_duration = self .tracked_diff_load_start_time .take() .map(|start| start.elapsed()); let err = DiffStateError::from_message(&msg); err.report_and_log(); send_telemetry_from_ctx!( CodeReviewTelemetryEvent::LoadDiffFailed { backend_origin: BackendOrigin::ClientRemote, operation: DiffOperation::RemoteDiff, mode: self.mode.clone(), error: err.to_string(), load_duration, }, ctx ); self.state = InternalRemoteDiffState::Error(msg); ctx.emit(DiffStateModelEvent::NewDiffsComputed { diffs: None, load_duration: None, }); } DiffState::Loaded => { let Some(base_content) = diffs else { let load_duration = self .tracked_diff_load_start_time .take() .map(|start| start.elapsed()); let err = DiffStateError::empty_diff_data(); err.report_and_log(); send_telemetry_from_ctx!( CodeReviewTelemetryEvent::LoadDiffFailed { backend_origin: BackendOrigin::ClientRemote, operation: DiffOperation::RemoteDiff, mode: self.mode.clone(), error: err.to_string(), load_duration, }, ctx ); self.state = InternalRemoteDiffState::Error(err.to_string()); ctx.emit(DiffStateModelEvent::NewDiffsComputed { diffs: None, load_duration: None, }); return; }; let diffs = GitDiffData::from(&base_content); let load_duration = self .tracked_diff_load_start_time .take() .map(|start| start.elapsed()); self.state = InternalRemoteDiffState::Loaded(diffs); ctx.emit(DiffStateModelEvent::NewDiffsComputed { diffs: Some(Arc::new(base_content)), load_duration, }); } } } fn apply_metadata_update(&mut self, metadata: &DiffMetadata, ctx: &mut ModelContext<Self>) { let previous_branch = self .metadata .as_ref() .map(|m| m.current_branch_name.as_str()); let branch_changed = previous_branch.is_some_and(|prev| prev != metadata.current_branch_name.as_str()); let metadata = metadata.clone(); self.metadata = Some(metadata.clone()); // Only emit CurrentBranchChanged when there was a previous branch to // compare against. On the first metadata update (initial snapshot) // previous_branch is None — that's initial population, not a switch. if branch_changed { ctx.emit(DiffStateModelEvent::CurrentBranchChanged); } ctx.emit(DiffStateModelEvent::MetadataRefreshed(Box::new(metadata))); } fn apply_file_delta( &mut self, file_path: String, diff: Option<FileDiffAndContent>, metadata: Option<DiffMetadata>, ctx: &mut ModelContext<Self>, ) { if let Some(metadata) = &metadata { self.apply_metadata_update(metadata, ctx); } let InternalRemoteDiffState::Loaded(ref mut diffs) = self.state else { // Ignore file deltas until the initial snapshot has loaded. return;Lines 1–500 of 878. Load more
//! Remote diff state model. //! //! Client-side model for a single remote repository diff state subscription //! received from the remote server. Presents the same read API as //! `LocalDiffStateModel` and emits the same `DiffStateModelEvent` variants. //! //! The active [`DiffMode`] can change; the model handles this by unsubscribing //! from the old `(repo_path, mode)` subscription and re-subscribing with the //! new mode. use std::sync::Arc; use instant::Instant; use remote_server::manager::{RemoteServerManager, RemoteServerManagerEvent}; use warp_core::{HostId, SessionId, send_telemetry_from_ctx}; use warp_util::remote_path::RemotePath; use warp_util::standardized_path::StandardizedPath; use warpui::{ModelContext, SingletonEntity}; use super::{ BackendOrigin, CommitChainMode, DiffMetadata, DiffMode, DiffOperation, DiffState, DiffStateError, DiffStateModelEvent, DiffStats, FileDiffAndContent, GitDiffData, GitDiffWithBaseContent, }; use crate::code_review::telemetry_event::CodeReviewTelemetryEvent; use crate::remote_server::diff_state_proto::{try_decode_file_delta, try_decode_snapshot}; use crate::remote_server::proto; use crate::util::git::{BranchEntry, Commit, FileChangeEntry, PrInfo}; // ── Internal state ──────────────────────────────────────────────── #[derive(Default)] enum InternalRemoteDiffState { #[default] Loading, NotInRepository, Loaded(GitDiffData), Error(String), /// The remote connection was lost. Preserves stale data until the model /// can re-establish the server-side subscription. Disconnected, } // ── Model ──────────────────────────────────────────────────────────────────── pub struct RemoteDiffStateModel { remote_path: RemotePath, mode: DiffMode, state: InternalRemoteDiffState, metadata: Option<DiffMetadata>, /// Start time for the latest caller-tracked full diff snapshot request. tracked_diff_load_start_time: Option<Instant>, } impl warpui::Entity for RemoteDiffStateModel { type Event = DiffStateModelEvent; } impl RemoteDiffStateModel { /// Creates a new remote diff state model. /// /// Identity is `(host_id, repo_path, mode)`. The model is session-agnostic: /// the manager resolves a connected session for the host on every outbound /// RPC, and host-level connect/disconnect events drive subscription /// lifecycle. /// /// `preferred_session` is the session that opened this review (the /// triggering callsite). It is used only for the *initial* `GetDiffState` /// dispatch and is deliberately not stored: a shared, long-lived model /// must not pin a session, and later re-triggers supply their own session /// (or `None`) rather than reusing a stale one. /// /// A session for this host is required at construction time. The model starts in `Loading` and /// issues the initial `GetDiffState` request. Runtime disconnects transition the model through /// `mark_disconnected`; subsequent reconnects re-subscribe via the `HostConnected` event handler. pub fn new( remote_path: RemotePath, mode: DiffMode, preferred_session: Option<SessionId>, ctx: &mut ModelContext<Self>, ) -> Self { // Subscribe to RemoteServerManager push events and filter by remote_path and diff_mode let mgr_handle = RemoteServerManager::handle(ctx); ctx.subscribe_to_model(&mgr_handle, |me, _, event, ctx| { me.handle_manager_event(event, ctx) }); let host_id = remote_path.host_id.clone(); let repo_path = remote_path.path.clone(); let mode_clone = mode.clone(); mgr_handle.update(ctx, |mgr, ctx| { mgr.get_diff_state( host_id, repo_path, proto::DiffMode::from(&mode_clone), preferred_session, ctx, ); }); Self { remote_path, mode, state: InternalRemoteDiffState::Loading, metadata: None, tracked_diff_load_start_time: None, } } // ── Event handler ─────────────────────────────────────────── fn matches_remote_path_and_mode( &self, host_id: &HostId, repo_path: &StandardizedPath, mode: &proto::DiffMode, ) -> bool { let remote_mode = proto::DiffMode::from(&self.mode); self.remote_path.matches(host_id, repo_path) && mode == &remote_mode } fn handle_manager_event( &mut self, event: &RemoteServerManagerEvent, ctx: &mut ModelContext<Self>, ) { match event { RemoteServerManagerEvent::DiffStateSnapshotReceived { host_id, repo_path, mode, snapshot, } => { if !self.matches_remote_path_and_mode(host_id, repo_path, mode) { return; } self.handle_snapshot_received(snapshot, ctx); } RemoteServerManagerEvent::DiffStateMetadataUpdateReceived { host_id, repo_path, mode, update, } => { if !self.matches_remote_path_and_mode(host_id, repo_path, mode) { return; } self.handle_metadata_update_received(update, ctx); } RemoteServerManagerEvent::DiffStateFileDeltaReceived { host_id, repo_path, mode, delta, } => { if !self.matches_remote_path_and_mode(host_id, repo_path, mode) { return; } self.handle_file_delta_received(delta, ctx); } RemoteServerManagerEvent::GetBranchesResponse { repo_path, result, .. } if repo_path == &self.remote_path.path => { let branches = match result { Ok(branch_infos) => branch_infos .iter() .map(|info| BranchEntry { name: info.name.clone(), is_main: info.is_main, }) .collect(), Err(err) => { log::warn!("RemoteDiffStateModel: GetBranches failed: {err}"); vec![] } }; ctx.emit(DiffStateModelEvent::BranchesReceived(branches)); } RemoteServerManagerEvent::CommitChainResponse { host_id, repo_path, result, } if self.remote_path.matches(host_id, repo_path) => { self.handle_git_commit_chain_response(result, ctx); } RemoteServerManagerEvent::GitPushResponse { host_id, repo_path, result, } if self.remote_path.matches(host_id, repo_path) => { self.handle_git_push_response(result, ctx); } RemoteServerManagerEvent::CreatePrResponse { host_id, repo_path, result, } if self.remote_path.matches(host_id, repo_path) => { self.handle_create_pr_response(result, ctx); } RemoteServerManagerEvent::GenerateCommitMessageResponse { host_id, repo_path, result, } if self.remote_path.matches(host_id, repo_path) => { // AI ran on the daemon; just relay the result to the dialog. ctx.emit(DiffStateModelEvent::CommitMessageGenerated(result.clone())); } RemoteServerManagerEvent::GetCommittedBranchFilesResponse { host_id, repo_path, result, } if self.remote_path.matches(host_id, repo_path) => { self.handle_get_committed_branch_files_response(result, ctx); } RemoteServerManagerEvent::HostDisconnected { host_id } if host_id == &self.remote_path.host_id => { self.mark_disconnected(ctx); } RemoteServerManagerEvent::HostConnected { host_id } if host_id == &self.remote_path.host_id && matches!(self.state, InternalRemoteDiffState::Disconnected) => { // Reconnect is event-driven with no viewing-session in scope // (and the prior session may be gone), so re-subscribe over // any connected session for the host. self.resubscribe(false, None, ctx); } _ => {} } } /// Marks the model as disconnected, preserving any stale data and /// emitting `ConnectionLost`. fn mark_disconnected(&mut self, ctx: &mut ModelContext<Self>) { if matches!(self.state, InternalRemoteDiffState::Disconnected) { return; } self.tracked_diff_load_start_time = None; self.state = InternalRemoteDiffState::Disconnected; ctx.emit(DiffStateModelEvent::ConnectionLost); } /// Re-sends `GetDiffState` for this model's `(host_id, repo, mode)` and /// transitions to `Loading` while waiting for a fresh snapshot. /// /// `preferred_session` is supplied by the triggering callsite (the /// session-scoped view) so the request rides the connection that needs the /// result; `None` (e.g. reconnect) falls back to any connected session. fn resubscribe( &mut self, track_load_duration: bool, preferred_session: Option<SessionId>, ctx: &mut ModelContext<Self>, ) { // Always overwrite to avoid carrying a stale `Instant` from a prior // tracked load that was interrupted by a session blip. self.tracked_diff_load_start_time = track_load_duration.then(Instant::now); let host_id = self.remote_path.host_id.clone(); let repo_path = self.remote_path.path.clone(); let mode = self.mode.clone(); RemoteServerManager::handle(ctx).update(ctx, |mgr, ctx| { mgr.get_diff_state( host_id, repo_path, proto::DiffMode::from(&mode), preferred_session, ctx, ); }); self.state = InternalRemoteDiffState::Loading; ctx.emit(DiffStateModelEvent::NewDiffsComputed { diffs: None, load_duration: None, }); } // ── Proto → state conversion helpers ──────────────────────────────────────────────── fn handle_snapshot_received( &mut self, snapshot: &proto::DiffStateSnapshot, ctx: &mut ModelContext<Self>, ) { match try_decode_snapshot(snapshot) { Ok((metadata, state, diffs)) => self.apply_snapshot(metadata, state, diffs, ctx), Err(error) => { self.tracked_diff_load_start_time = None; warp_core::safe_error!( safe: ("RemoteDiffStateModel: failed to decode diff state snapshot"), full: ("RemoteDiffStateModel: failed to decode diff state snapshot: {error}") ); } } } fn handle_metadata_update_received( &mut self, update: &proto::DiffStateMetadataUpdate, ctx: &mut ModelContext<Self>, ) { match update .metadata .as_ref() .map(DiffMetadata::try_from) .transpose() { Ok(Some(metadata)) => self.apply_metadata_update(&metadata, ctx), Ok(None) => {} Err(error) => { warp_core::safe_error!( safe: ("RemoteDiffStateModel: failed to decode diff state metadata update"), full: ("RemoteDiffStateModel: failed to decode diff state metadata update: {error}") ); } } } fn handle_file_delta_received( &mut self, delta: &proto::DiffStateFileDelta, ctx: &mut ModelContext<Self>, ) { match try_decode_file_delta(delta) { Ok((file_path, diff, metadata)) => { self.apply_file_delta(file_path, diff, metadata, ctx) } Err(error) => { warp_core::safe_error!( safe: ("RemoteDiffStateModel: failed to decode diff state file delta"), full: ("RemoteDiffStateModel: failed to decode diff state file delta: {error}") ); } } } // ── Apply methods ────────────────────────────────────────────────────── /// Requests a fresh diff snapshot from the remote server, including file /// content. Unlike the former `replay_latest_diffs` (which reconstructed /// data from cached `GitDiffData` and lost `content_at_head`), this sends /// an actual `GetDiffState` RPC so the server can reload content from disk. /// /// Does NOT transition to `Loading` or emit `NewDiffsComputed(None)` first, /// so existing views subscribed to this model won't flash a loading state. /// The server response arrives as a `DiffStateSnapshotReceived` event and /// flows through `apply_snapshot` normally. pub(crate) fn fetch_fresh_snapshot( &mut self, track_load_duration: bool, preferred_session: Option<SessionId>, ctx: &mut ModelContext<Self>, ) { if track_load_duration { self.tracked_diff_load_start_time = Some(Instant::now()); } let host_id = self.remote_path.host_id.clone(); let repo_path = self.remote_path.path.clone(); let mode = self.mode.clone(); // `preferred_session` is supplied per-call by the triggering view (the // session showing the review); `None` falls back to any connected // session for the host. Never cached on this shared model. RemoteServerManager::handle(ctx).update(ctx, |mgr, ctx| { mgr.get_diff_state( host_id, repo_path, proto::DiffMode::from(&mode), preferred_session, ctx, ); }); } fn apply_snapshot( &mut self, metadata: Option<DiffMetadata>, state: DiffState, diffs: Option<GitDiffWithBaseContent>, ctx: &mut ModelContext<Self>, ) { // Update metadata, detecting branch changes. if let Some(metadata) = &metadata { self.apply_metadata_update(metadata, ctx); } // Update state. match state { // Disconnected is never produced by proto deserialization. DiffState::Disconnected => {} DiffState::NotInRepository => { self.tracked_diff_load_start_time = None; self.state = InternalRemoteDiffState::NotInRepository; ctx.emit(DiffStateModelEvent::NewDiffsComputed { diffs: None, load_duration: None, }); } DiffState::Loading => { self.state = InternalRemoteDiffState::Loading; ctx.emit(DiffStateModelEvent::NewDiffsComputed { diffs: None, load_duration: None, }); } DiffState::Error(msg) => { let load_duration = self .tracked_diff_load_start_time .take() .map(|start| start.elapsed()); let err = DiffStateError::from_message(&msg); err.report_and_log(); send_telemetry_from_ctx!( CodeReviewTelemetryEvent::LoadDiffFailed { backend_origin: BackendOrigin::ClientRemote, operation: DiffOperation::RemoteDiff, mode: self.mode.clone(), error: err.to_string(), load_duration, }, ctx ); self.state = InternalRemoteDiffState::Error(msg); ctx.emit(DiffStateModelEvent::NewDiffsComputed { diffs: None, load_duration: None, }); } DiffState::Loaded => { let Some(base_content) = diffs else { let load_duration = self .tracked_diff_load_start_time .take() .map(|start| start.elapsed()); let err = DiffStateError::empty_diff_data(); err.report_and_log(); send_telemetry_from_ctx!( CodeReviewTelemetryEvent::LoadDiffFailed { backend_origin: BackendOrigin::ClientRemote, operation: DiffOperation::RemoteDiff, mode: self.mode.clone(), error: err.to_string(), load_duration, }, ctx ); self.state = InternalRemoteDiffState::Error(err.to_string()); ctx.emit(DiffStateModelEvent::NewDiffsComputed { diffs: None, load_duration: None, }); return; }; let diffs = GitDiffData::from(&base_content); let load_duration = self .tracked_diff_load_start_time .take() .map(|start| start.elapsed()); self.state = InternalRemoteDiffState::Loaded(diffs); ctx.emit(DiffStateModelEvent::NewDiffsComputed { diffs: Some(Arc::new(base_content)), load_duration, }); } } } fn apply_metadata_update(&mut self, metadata: &DiffMetadata, ctx: &mut ModelContext<Self>) { let previous_branch = self .metadata .as_ref() .map(|m| m.current_branch_name.as_str()); let branch_changed = previous_branch.is_some_and(|prev| prev != metadata.current_branch_name.as_str()); let metadata = metadata.clone(); self.metadata = Some(metadata.clone()); // Only emit CurrentBranchChanged when there was a previous branch to // compare against. On the first metadata update (initial snapshot) // previous_branch is None — that's initial population, not a switch. if branch_changed { ctx.emit(DiffStateModelEvent::CurrentBranchChanged); } ctx.emit(DiffStateModelEvent::MetadataRefreshed(Box::new(metadata))); } fn apply_file_delta( &mut self, file_path: String, diff: Option<FileDiffAndContent>, metadata: Option<DiffMetadata>, ctx: &mut ModelContext<Self>, ) { if let Some(metadata) = &metadata { self.apply_metadata_update(metadata, ctx); } let InternalRemoteDiffState::Loaded(ref mut diffs) = self.state else { // Ignore file deltas until the initial snapshot has loaded. return;