Fix Assistant sidebar navigation round-trip

This commit is contained in:
Kaede Akino committed 2026-08-30 13:34:04 +08:00
1 parent aceab8260f
commit cac60df1a8
4 files changed
+162 -28

No files matched your search

@@ -70,8 +70,8 @@ struct WindowSidebarView: View {
let isEnabled = window.isSidebarModeEnabled(mode)
return Button {
guard mode == .navigation || isEnabled else { return }
window.sidebarMode = mode
guard isEnabled else { return }
window.activateSidebarMode(mode)
} label: {
Group {
if #available(macOS 26.0, *), usesSystemGlass, isSelected {
+55 -1
View File
@@ -20,7 +20,17 @@ final class WindowModel {
}
/// The top-level shell has two pinned entries followed by dynamic SSH/SFTP
/// Workspaces. This is the single source of truth for workspace selection.
var selectedDestination: WindowDestination = .vault
var selectedDestination: WindowDestination = .vault {
didSet {
if selectedDestination != .assistant {
assistantReturnDestination = selectedDestination
}
}
}
/// Where Navigator returns after the Assistant temporarily replaces the
/// detail surface. Assistant has no row inside the Navigator that could
/// retain the previous selection for us.
private var assistantReturnDestination: WindowDestination = .vault
var vaultDestination: VaultDestination = .welcome
var vaultWorkRoute: VaultWorkRoute?
let assistantLibrary: AssistantLibraryModel
@@ -158,6 +168,48 @@ final class WindowModel {
sidebar.reconcileMode()
}
/// Applies a Sidebar mode chosen by the user, including the two mode-rail
/// buttons that also act as top-level navigation affordances.
///
/// Direct `sidebarMode` assignment deliberately remains presentation-only
/// for restoration, screenshots, and other programmatic configuration.
func activateSidebarMode(_ mode: SidebarMode) {
guard sidebar.isModeEnabled(mode) else { return }
sidebarMode = mode
switch mode {
case .assistant:
// Inside a Workspace this is its compact, context-aware Assistant.
// Everywhere else the same control opens the full Assistant route.
if selectedWorkspace == nil {
selectAssistant()
}
case .navigation:
if selectedDestination == .assistant {
restoreAssistantReturnDestination()
}
case .snippets, .history:
break
}
}
private func restoreAssistantReturnDestination() {
switch assistantReturnDestination {
case .workspace(let id):
if workspaces.contains(where: { $0.id == id }) {
select(id)
} else {
selectVault()
}
case .vault:
selectVault()
case .assistant:
// Defensive only: the remembered destination is updated exclusively
// from non-Assistant routes.
selectVault()
}
}
/// Opens the shared column and selects the current Workspace's assistant.
/// Repeating the command is intentionally idempotent; hiding the column is
/// the native Toggle Sidebar command's job.
@@ -195,6 +247,8 @@ final class WindowModel {
self.vaultDestination = vaultDestination
self.assistantLibrary = assistantLibrary
selectedDestination = workspaces.first.map { .workspace($0.id) } ?? .vault
// Property observers do not run while an instance is being initialized.
assistantReturnDestination = selectedDestination
for workspace in workspaces {
workspace.lifecycleRouter = self
}
+26 -21
View File
@@ -2365,6 +2365,10 @@
}
}
},
"Choose" : {
"comment" : "A button that opens a file picker to choose a private key.",
"isCommentAutoGenerated" : true
},
"Choose a private key or OpenSSH user certificate file." : {
"localizations" : {
"zh-Hans" : {
@@ -2386,6 +2390,7 @@
}
},
"Choose File…" : {
"extractionState" : "stale",
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
@@ -4197,16 +4202,6 @@
}
}
},
"Files and images" : {
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "文件与图片"
}
}
}
},
"Dropped image" : {
"localizations" : {
"zh-Hans" : {
@@ -4948,6 +4943,16 @@
}
}
},
"Files and images" : {
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "文件与图片"
}
}
}
},
"Files are identical" : {
"localizations" : {
"zh-Hans" : {
@@ -5842,16 +5847,6 @@
}
}
},
"Import Private Key" : {
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "导入私钥"
}
}
}
},
"Import or generate a key, then create an identity with a username. Private keys stay in the Keychain." : {
"localizations" : {
"zh-Hans" : {
@@ -5862,6 +5857,16 @@
}
}
},
"Import Private Key" : {
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "导入私钥"
}
}
}
},
"In Current Workspace" : {
"localizations" : {
"zh-Hans" : {
@@ -14535,4 +14540,4 @@
}
},
"version" : "1.2"
}
}
+79 -4
View File
@@ -321,13 +321,88 @@ struct WindowStateTests {
func selectingAssistantDoesNotCreateAConversation() {
let window = WindowModel()
window.selectAssistant()
window.activateSidebarMode(.assistant)
#expect(window.selectedDestination == .assistant)
#expect(window.assistantLibrary.conversations.isEmpty)
#expect(window.assistantLibrary.selectedConversation == nil)
}
@Test
func assistantAndNavigatorModeRailRoundTripsToThePreviousVaultRoute() {
let conversation = ConversationModel()
let library = AssistantLibraryModel(store: AssistantConversationStore(
conversations: [conversation]
))
let window = WindowModel(assistantLibrary: library)
window.selectAssistant(conversation.id)
window.openVaultWork(.hostEditor(nil), on: .hosts)
// The Assistant mode can already be selected after navigation elsewhere;
// pressing its selected button must still activate the Assistant route.
window.sidebarMode = .assistant
window.activateSidebarMode(.assistant)
#expect(window.selectedDestination == .assistant)
#expect(window.assistantLibrary.selectedConversation === conversation)
window.activateSidebarMode(.navigation)
#expect(window.selectedDestination == .vault)
#expect(window.vaultDestination == .hosts)
#expect(window.vaultWorkRoute == .hostEditor(nil))
}
@Test
func assistantModeRailStaysCompactInWorkspaceAndNavigatorReturnsThereFromFullScreen() {
let workspace = Workspace.sshSession(
host: Host(name: "Server", host: "server.example")
)
let window = WindowModel(workspaces: [workspace])
window.activateSidebarMode(.assistant)
#expect(window.sidebarMode == .assistant)
#expect(window.selectedDestination == .workspace(workspace.id))
let conversation = window.assistantLibrary.createConversation()
window.openAssistantFullScreen(conversation.id)
window.activateSidebarMode(.navigation)
#expect(window.sidebarMode == .navigation)
#expect(window.selectedDestination == .workspace(workspace.id))
}
@Test
func navigatorFallsBackToVaultWhenTheAssistantReturnWorkspaceWasClosed() {
let workspace = Workspace.sshSession(
host: Host(name: "Server", host: "server.example")
)
let window = WindowModel(workspaces: [workspace])
window.openAssistantFullScreen()
window.close(workspace.id)
window.activateSidebarMode(.navigation)
#expect(window.workspaces.isEmpty)
#expect(window.selectedDestination == .vault)
}
@Test
func nonNavigationToolModesAndDirectModeAssignmentDoNotNavigate() {
let window = WindowModel()
window.openPage(.hosts)
window.sidebarMode = .assistant
#expect(window.selectedDestination == .vault)
window.activateSidebarMode(.snippets)
#expect(window.selectedDestination == .vault)
window.activateSidebarMode(.history)
#expect(window.selectedDestination == .vault)
}
@Test
func enteringAssistantKeepsTheVolatileConversationSelection() {
let conversation = ConversationModel()
@@ -338,7 +413,7 @@ struct WindowStateTests {
window.selectAssistant(conversation.id)
window.selectVault()
window.selectAssistant()
window.activateSidebarMode(.assistant)
#expect(window.selectedDestination == .assistant)
#expect(window.assistantLibrary.selectedConversation === conversation)
@@ -348,7 +423,7 @@ struct WindowStateTests {
#expect(draft === window.assistantLibrary.draftConversation)
window.selectVault()
window.selectAssistant()
window.activateSidebarMode(.assistant)
#expect(window.assistantLibrary.selectedConversation == nil)
#expect(draft === window.assistantLibrary.draftConversation)
}
@@ -364,7 +439,7 @@ struct WindowStateTests {
window.selectAssistant(conversation.id)
window.showAssistantArchive()
window.selectVault()
window.selectAssistant()
window.activateSidebarMode(.assistant)
#expect(window.isAssistantArchivePresented)
#expect(window.assistantLibrary.selectedConversation === conversation)