{
let initials = initials.into();
let title = title.into();
@@ -1307,33 +1258,15 @@ fn profile_info_row(
),
);
- if let Some(on_logout) = on_logout {
- row = row.child(logout_button(cx, on_logout));
- }
-
- row
-}
-
-fn logout_button(
- cx: &App,
- on_press: impl Fn(&PressEvent, &mut Window, &mut App) + 'static,
-) -> Stateful
{
- div()
- .id("settings-delta-logout")
- .flex_none()
- .size(px(16.0))
- .flex()
- .items_center()
- .justify_center()
- .cursor_pointer()
- .hit_slop(px(14.0))
- .on_press(on_press)
- .child(
+ row = row.cursor_pointer().on_press(on_press).child(
+ div().pl(px(8.0)).child(
svg()
- .path("icons/log-out.svg")
- .size(px(12.0))
- .text_color(rgb(theme::accent_red(cx))),
- )
+ .path("icons/chevron-right.svg")
+ .size(px(theme::ICON_SM))
+ .text_color(rgb(theme::text_muted(cx))),
+ ),
+ );
+ row
}
fn short_id(id: uuid::Uuid) -> String {
diff --git a/crates/zedra/src/workspace.rs b/crates/zedra/src/workspace.rs
index dc0467d7..cb1d9b48 100644
--- a/crates/zedra/src/workspace.rs
+++ b/crates/zedra/src/workspace.rs
@@ -20,7 +20,7 @@ use crate::agent_detail::AgentDetail;
use crate::agent_manage::AgentManage;
use crate::agent_picker::AgentPicker;
use crate::agent_sessions::AgentSessions;
-use crate::delta::{ClientDeltaInfo, DeltaState};
+use crate::delta::{ClientDeltaInfo, DeltaPatch, DeltaState};
use crate::editor::git_sidebar::GitFileSection;
use crate::file_search::{FileSearchEvent, FileSearchPanel};
use crate::pending::{SharedPendingSlot, shared_pending_slot, spawn_periodic_task};
@@ -1442,7 +1442,7 @@ impl Workspace {
Ok((Some(result), next)) => {
let applied = delta_state.update(cx, |state, cx| {
// Skip stale host registration results if Delta auth state changed mid-flight.
- state.apply_if_current(&snapshot, next, cx)
+ state.merge(DeltaPatch::between(&snapshot, &next), cx)
});
let _ = workspace.update(cx, |ws, cx| {
ws.delta_host_reconciling = false;
diff --git a/crates/zedra/src/workspace_state.rs b/crates/zedra/src/workspace_state.rs
index b09fd393..2380e542 100644
--- a/crates/zedra/src/workspace_state.rs
+++ b/crates/zedra/src/workspace_state.rs
@@ -622,6 +622,18 @@ impl WorkspaceState {
Ok(())
}
+ /// Forget Delta's server-side host mappings without removing paired hosts.
+ pub fn clear_delta_bindings() -> Result<(), String> {
+ let _guard = workspace_store_lock()
+ .lock()
+ .map_err(|e| format!("Failed to lock workspace store: {e}"))?;
+ let mut store = WorkspaceStore::load()?;
+ if store.clear_delta_bindings() {
+ store.save()?;
+ }
+ Ok(())
+ }
+
pub fn now_u64() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
@@ -709,6 +721,19 @@ impl WorkspaceStore {
self.workspaces.retain(|w| w.endpoint_addr != endpoint_addr);
self.workspaces.len() != before
}
+
+ fn clear_delta_bindings(&mut self) -> bool {
+ let mut changed = false;
+ for workspace in &mut self.workspaces {
+ if workspace.delta_host_pubkey.take().is_some() {
+ changed = true;
+ }
+ if workspace.delta_host_node_id.take().is_some() {
+ changed = true;
+ }
+ }
+ changed
+ }
}
impl EventEmitter for WorkspaceState {}
@@ -1158,6 +1183,30 @@ mod tests {
assert_eq!(loaded[0].delta_host_node_id, Some(delta_host_node_id));
}
+ #[test]
+ fn clear_delta_bindings_preserves_paired_workspace() {
+ let _guard = set_test_data_directory("clear-delta-bindings");
+ WorkspaceState::upsert(WorkspaceState {
+ endpoint_addr: "endpoint-a".into(),
+ custom_name: Some("Paired Mac".into()),
+ delta_host_pubkey: Some([7u8; 32]),
+ delta_host_node_id: Some(
+ Uuid::parse_str("33333333-3333-3333-3333-333333333333").unwrap(),
+ ),
+ ..Default::default()
+ })
+ .unwrap();
+
+ WorkspaceState::clear_delta_bindings().unwrap();
+
+ let loaded = WorkspaceState::load().unwrap();
+ assert_eq!(loaded.len(), 1);
+ assert_eq!(loaded[0].endpoint_addr, "endpoint-a");
+ assert_eq!(loaded[0].custom_name.as_deref(), Some("Paired Mac"));
+ assert_eq!(loaded[0].delta_host_pubkey, None);
+ assert_eq!(loaded[0].delta_host_node_id, None);
+ }
+
#[test]
fn remove_by_endpoint_addr_persists_removal() {
let _guard = set_test_data_directory("remove-persists-removal");
diff --git a/docs/MANUAL_TEST.md b/docs/MANUAL_TEST.md
index 5e8a05df..f2fde1ce 100644
--- a/docs/MANUAL_TEST.md
+++ b/docs/MANUAL_TEST.md
@@ -96,8 +96,36 @@ Verifies a `zedra/rpc/3` host still serves a pre-bump app.
2. Expected: `Profile` and `Notifications` appear above `Appearance`
3. Start sign-in or notification registration
4. Expected: progress or error status replaces the relevant row description, with no separate `Status` row and no combined status/description text
-5. Sign in to Delta, then tap the logout icon in the profile row
-6. Expected: a native confirmation alert appears; cancelling keeps the profile signed in, and confirming returns the profile row to `Sign In`
+5. Sign in to Delta, then tap the profile row
+6. Expected: the Account screen opens, showing the signed-in email, notification state, and the stack and device identifiers
+
+### Account screen
+
+1. Sign in to Delta, then open Settings and tap the profile row
+2. Expected: the header title matches the Settings title size, and no content sits under the status bar
+3. Tap the `Stack`, device, and any `Devices` row
+4. Expected: each copies its full identifier, the icon turns into a green check for about a second, and pasting elsewhere yields the whole id with no trailing characters
+5. Compare the `Devices` list against `zedra stack list` on the paired host
+6. Expected: the same nodes appear, this device is marked, and each row shows its kind and joined date
+7. Leave the screen and reopen it within five minutes
+8. Expected: the list paints immediately with no refetch; relaunching the app also shows it before any network call
+9. With notifications not yet enabled, tap the `Notifications` row
+10. Expected: the native permission prompt appears, and the row settles on `Enabled` with the provider once registration finishes
+11. Tap `Log Out`
+12. Expected: the app signs out immediately and returns to Settings, with the profile row back to `Sign In`
+
+### Account deletion
+
+1. Sign in with a throwaway account, then open Account and tap `Delete Account`
+2. Expected: a native destructive alert appears; cancelling leaves the account signed in and unchanged
+3. Confirm the deletion
+4. Expected: the app returns to Settings signed out, and `delta.json` no longer holds tokens, stack, node, or cached nodes
+5. Run `zedra stack list` against the deleted stack
+6. Expected: the stack is gone and the CLI reports the account no longer exists
+7. Confirm saved workspaces still appear on Home
+8. Expected: pairings survive — only the Delta host binding fields were cleared
+9. Sign in again with the same provider
+10. Expected: a fresh stack and node are created, and the email still populates the profile row
### Mobile node metadata reconciliation
diff --git a/vendor/zed b/vendor/zed
index 8bcfdccc..fbf1db19 160000
--- a/vendor/zed
+++ b/vendor/zed
@@ -1 +1 @@
-Subproject commit 8bcfdccca639421b5de505e6acf839e2c332755d
+Subproject commit fbf1db1954821f057cb187ba1c4ef4335d6f7495