From e36f47091701f3a7db0c9952db1ca2edc8c75f2a Mon Sep 17 00:00:00 2001 From: Kaede Akino Date: Thu, 27 Aug 2026 22:35:15 +0800 Subject: [PATCH] Preserve Assistant navigation state --- Termind.xcodeproj/project.pbxproj | 4 +- Termind/App/TermindCommands.swift | 2 +- .../Commands/AppCommandRegistry.swift | 2 +- .../Features/Assistant/AssistantView.swift | 4 +- Termind/Features/Tabs/TabBarView.swift | 2 +- Termind/Features/Tabs/TabContentView.swift | 2 +- Termind/Features/Tabs/WindowModel.swift | 16 ++++--- TermindTests/WindowStateTests.swift | 46 ++++++++++++++++++- 8 files changed, 61 insertions(+), 17 deletions(-) diff --git a/Termind.xcodeproj/project.pbxproj b/Termind.xcodeproj/project.pbxproj index 4ce168ad..0dd85002 100644 --- a/Termind.xcodeproj/project.pbxproj +++ b/Termind.xcodeproj/project.pbxproj @@ -418,7 +418,7 @@ "@executable_path/../Frameworks", ); LM_SKIP_METADATA_EXTRACTION = YES; - MARKETING_VERSION = 0.1.1; + MARKETING_VERSION = 0.1.2; PRODUCT_BUNDLE_IDENTIFIER = com.akinokaede.termind; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; @@ -449,7 +449,7 @@ "@executable_path/../Frameworks", ); LM_SKIP_METADATA_EXTRACTION = YES; - MARKETING_VERSION = 0.1.1; + MARKETING_VERSION = 0.1.2; PRODUCT_BUNDLE_IDENTIFIER = com.akinokaede.termind; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; diff --git a/Termind/App/TermindCommands.swift b/Termind/App/TermindCommands.swift index 06245c53..c4f64dfa 100644 --- a/Termind/App/TermindCommands.swift +++ b/Termind/App/TermindCommands.swift @@ -39,7 +39,7 @@ struct TermindCommands: Commands { app.connect(in: window) } .keyboardShortcut("t") - Button("New Chat") { window?.openAssistant() } + Button("New Chat") { window?.openNewAssistantChat() } .keyboardShortcut("t", modifiers: [.command, .option]) Divider() diff --git a/Termind/Application/Commands/AppCommandRegistry.swift b/Termind/Application/Commands/AppCommandRegistry.swift index 7b525bf6..5c7ed502 100644 --- a/Termind/Application/Commands/AppCommandRegistry.swift +++ b/Termind/Application/Commands/AppCommandRegistry.swift @@ -59,7 +59,7 @@ final class AppCommandRegistry { guard let host = app.hostLibrary.host(id) else { return } app.open(host, as: target, in: window) case .newChat: - window.openAssistant() + window.openNewAssistantChat() case .newHost: window.openHostEditor(host: nil) case .showAssistant: diff --git a/Termind/Features/Assistant/AssistantView.swift b/Termind/Features/Assistant/AssistantView.swift index afa25fe0..5f0b16e2 100644 --- a/Termind/Features/Assistant/AssistantView.swift +++ b/Termind/Features/Assistant/AssistantView.swift @@ -11,7 +11,7 @@ struct AssistantSidebarView: View { Divider() HStack(spacing: Metrics.Space.s) { Button { - window.openAssistant() + window.openNewAssistantChat() } label: { Label("New Chat", systemImage: "plus.bubble") .frame( @@ -532,7 +532,7 @@ struct AssistantView: View { } else if let conversation = window.assistantLibrary.selectedConversation { ConversationView( conversation: conversation, - onNewChat: { window.selectAssistant() } + onNewChat: { window.openNewAssistantChat() } ) .background(.background) } else { diff --git a/Termind/Features/Tabs/TabBarView.swift b/Termind/Features/Tabs/TabBarView.swift index 5d3a5c33..a434bd2e 100644 --- a/Termind/Features/Tabs/TabBarView.swift +++ b/Termind/Features/Tabs/TabBarView.swift @@ -368,7 +368,7 @@ struct TabBarView: View { // Same order as the File menu, so the two lists of the same four // verbs cannot be read as two different lists. Button("Connect") { app.connect(in: window) } - Button("New Chat") { window.openAssistant() } + Button("New Chat") { window.openNewAssistantChat() } Button("New Host") { window.openHostEditor(host: nil) } let recents = app.hostLibrary.recents(limit: 5) diff --git a/Termind/Features/Tabs/TabContentView.swift b/Termind/Features/Tabs/TabContentView.swift index 2e46d2cb..0301ed87 100644 --- a/Termind/Features/Tabs/TabContentView.swift +++ b/Termind/Features/Tabs/TabContentView.swift @@ -765,7 +765,7 @@ struct WelcomeView: View { HStack(spacing: Metrics.Space.l) { Button("Connect") { app.connect(in: window) } Button("Batch Startup") { app.batchStartup(in: window) } - Button("New Chat") { window.openAssistant() } + Button("New Chat") { window.openNewAssistantChat() } } } diff --git a/Termind/Features/Tabs/WindowModel.swift b/Termind/Features/Tabs/WindowModel.swift index 17d2cc21..cf1f7d33 100644 --- a/Termind/Features/Tabs/WindowModel.swift +++ b/Termind/Features/Tabs/WindowModel.swift @@ -393,13 +393,15 @@ final class WindowModel { selectedTopLevelTab = .vault } - func selectAssistant(_ id: ConversationModel.ID? = nil) { + /// Enters the Assistant without changing its in-memory navigation state: + /// the current conversation, New Chat, and Archive are all left untouched. + func selectAssistant() { + selectedTopLevelTab = .assistant + } + + func selectAssistant(_ id: ConversationModel.ID) { isAssistantArchivePresented = false - if let id { - assistantLibrary.select(id) - } else { - assistantLibrary.selectNewConversation() - } + assistantLibrary.select(id) selectedTopLevelTab = .assistant } @@ -652,7 +654,7 @@ final class WindowModel { } @discardableResult - func openAssistant() -> ConversationModel { + func openNewAssistantChat() -> ConversationModel { isAssistantArchivePresented = false let conversation = assistantLibrary.selectNewConversation() selectedTopLevelTab = .assistant diff --git a/TermindTests/WindowStateTests.swift b/TermindTests/WindowStateTests.swift index 8ad38ad4..588df95a 100644 --- a/TermindTests/WindowStateTests.swift +++ b/TermindTests/WindowStateTests.swift @@ -202,7 +202,49 @@ struct WindowStateTests { } @Test - func assistantDefaultsToNewChatEvenWhenHistoryExists() { + func enteringAssistantKeepsTheVolatileConversationSelection() { + let conversation = ConversationModel() + let library = AssistantLibraryModel(store: AssistantConversationStore( + conversations: [conversation] + )) + let window = WindowModel(assistantLibrary: library) + + window.selectAssistant(conversation.id) + window.selectVault() + window.selectAssistant() + + #expect(window.selectedTopLevelTab == .assistant) + #expect(window.assistantLibrary.selectedConversation === conversation) + + let draft = window.openNewAssistantChat() + #expect(window.assistantLibrary.selectedConversation == nil) + #expect(draft === window.assistantLibrary.draftConversation) + + window.selectVault() + window.selectAssistant() + #expect(window.assistantLibrary.selectedConversation == nil) + #expect(draft === window.assistantLibrary.draftConversation) + } + + @Test + func enteringAssistantKeepsTheVolatileArchiveDestination() { + let conversation = ConversationModel() + let library = AssistantLibraryModel(store: AssistantConversationStore( + conversations: [conversation] + )) + let window = WindowModel(assistantLibrary: library) + + window.selectAssistant(conversation.id) + window.showAssistantArchive() + window.selectVault() + window.selectAssistant() + + #expect(window.isAssistantArchivePresented) + #expect(window.assistantLibrary.selectedConversation === conversation) + } + + @Test + func assistantStartsOnNewChatWhenNoVolatileSelectionExists() { let previousID = UUID() let library = AssistantLibraryModel() library.restore([ @@ -225,7 +267,7 @@ struct WindowStateTests { @Test func newChatJoinsHistoryOnlyAfterItsFirstSend() { let window = WindowModel() - let draft = window.openAssistant() + let draft = window.openNewAssistantChat() #expect(window.assistantLibrary.selectedConversation == nil) #expect(window.assistantLibrary.conversations.isEmpty)