149 lines
4.5 KiB
Swift
149 lines
4.5 KiB
Swift
// SPDX-FileCopyrightText: 2026 Kaede Akino
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
import SwiftUI
|
|
import TermindApplication
|
|
import TermindDomain
|
|
import TermindPresentation
|
|
|
|
/// Window-scoped ownership for the dynamic workspace collection.
|
|
/// Navigation remains in `WindowModel`; recoverable close history belongs to the app.
|
|
@Observable
|
|
final class WorkspaceDeckModel {
|
|
var workspaces: [Workspace]
|
|
|
|
init(workspaces: [Workspace] = []) {
|
|
self.workspaces = workspaces
|
|
}
|
|
}
|
|
|
|
/// The one left sidebar's window-lifetime state.
|
|
///
|
|
/// Navigation and contextual tools share a physical column. The selected mode
|
|
/// belongs to the window and remains unchanged as its destination changes.
|
|
@Observable
|
|
final class WindowSidebarState {
|
|
nonisolated enum Mode: String, Hashable, Sendable, CaseIterable, Identifiable {
|
|
case navigation, assistant, snippets, history
|
|
|
|
nonisolated var id: String { rawValue }
|
|
|
|
var label: LocalizedStringResource {
|
|
switch self {
|
|
case .navigation: AppStrings.Workspace.navigator
|
|
case .assistant: AppStrings.Navigation.assistant
|
|
case .snippets: AppStrings.Navigation.snippets
|
|
case .history: AppStrings.Common.history
|
|
}
|
|
}
|
|
|
|
var symbol: String {
|
|
switch self {
|
|
case .navigation: "list.bullet"
|
|
case .assistant: "sparkles"
|
|
case .snippets: "curlybraces"
|
|
case .history: "clock.arrow.circlepath"
|
|
}
|
|
}
|
|
}
|
|
|
|
var isPresented = true
|
|
/// The last usable width survives hiding the column and is the initial
|
|
/// width for a window created from this one.
|
|
var preferredWidth: CGFloat = Metrics.Sidebar.ideal
|
|
var observedWidth: CGFloat?
|
|
var measuredWidth: CGFloat?
|
|
/// A late scene request rebuilds only the new window's split view so its
|
|
/// first column can take the requested width.
|
|
var initialLayoutGeneration = 0
|
|
|
|
/// The contextual Sidebar keeps its own draft, but its open Chat/Archive
|
|
/// route is shared with the full Assistant through `WindowModel`.
|
|
var assistantDraft = ConversationModel()
|
|
|
|
/// What the focused pane is, for whichever composer this column is showing.
|
|
///
|
|
/// Here rather than on a conversation, because it describes the window's
|
|
/// current view of a host — not the chat. `WindowModel.refreshSessionContext`
|
|
/// is the only writer, and it clears this to `.none` when no Workspace is
|
|
/// selected so a stale host and directory cannot be left on screen.
|
|
var sessionContext: SessionContext = .none
|
|
|
|
var mode: Mode = .navigation
|
|
}
|
|
|
|
@Observable
|
|
final class WindowPresentationState {
|
|
enum PresentationRoute: Identifiable {
|
|
case hostChooser(HostChooserPurpose)
|
|
case quickOpen
|
|
case workspaceTemplateName(WorkspaceTemplateNamePresentation)
|
|
|
|
var id: String {
|
|
switch self {
|
|
case .hostChooser(let purpose): "host-chooser-\(purpose.id)"
|
|
case .quickOpen: "quick-open"
|
|
case .workspaceTemplateName(let presentation): presentation.id.uuidString
|
|
}
|
|
}
|
|
}
|
|
|
|
var route: PresentationRoute?
|
|
|
|
var hostChooser: HostChooserPurpose? {
|
|
get {
|
|
guard case .hostChooser(let purpose) = route else { return nil }
|
|
return purpose
|
|
}
|
|
set {
|
|
route = newValue.map(PresentationRoute.hostChooser)
|
|
}
|
|
}
|
|
|
|
var isQuickOpenPresented: Bool {
|
|
get {
|
|
guard case .quickOpen = route else { return false }
|
|
return true
|
|
}
|
|
set {
|
|
if newValue {
|
|
route = .quickOpen
|
|
} else if case .quickOpen = route {
|
|
route = nil
|
|
}
|
|
}
|
|
}
|
|
|
|
var workspaceTemplateName: WorkspaceTemplateNamePresentation? {
|
|
get {
|
|
guard case .workspaceTemplateName(let presentation) = route else { return nil }
|
|
return presentation
|
|
}
|
|
set {
|
|
if let newValue {
|
|
route = .workspaceTemplateName(newValue)
|
|
} else if case .workspaceTemplateName = route {
|
|
route = nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Observable
|
|
final class WorkspaceTemplateNamePresentation: Identifiable {
|
|
let id = UUID()
|
|
let workspaceID: Workspace.ID
|
|
var name: String
|
|
var error: String?
|
|
|
|
init(workspaceID: Workspace.ID, name: String) {
|
|
self.workspaceID = workspaceID
|
|
self.name = name
|
|
}
|
|
}
|
|
|
|
@Observable
|
|
final class WorkspaceDragState {
|
|
var payload: DragPayload?
|
|
}
|