Fix Assistant sidebar navigation round-trip
This commit is contained in:
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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user