Refine Snippets & History UI

This commit is contained in:
Kaede Akino committed 2026-09-21 16:48:51 +08:00
1 parent cf87058672
commit 4cb443ccfc
9 files changed
+368 -132

No files matched your search

+53 -49
View File
@@ -3439,22 +3439,6 @@
}
}
},
"Ask Assistant" : {
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "询问助手"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "詢問助手"
}
}
}
},
"Ask anything…" : {
"localizations" : {
"zh-Hans" : {
@@ -3471,6 +3455,22 @@
}
}
},
"Ask Assistant" : {
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "询问助手"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "詢問助手"
}
}
}
},
"Assigned roles will return to their defaults." : {
"comment" : "A message displayed in a confirmation dialog that warns of the consequences of deleting a provider.",
"isCommentAutoGenerated" : true,
@@ -6070,6 +6070,7 @@
}
},
"Connection error" : {
"extractionState" : "stale",
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
@@ -11657,6 +11658,7 @@
}
},
"Hide" : {
"extractionState" : "stale",
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
@@ -11738,22 +11740,6 @@
}
}
},
"Hide Session Details" : {
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "隐藏会话详情"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "隱藏工作階段詳細資訊"
}
}
}
},
"Hide Secret" : {
"localizations" : {
"zh-Hans" : {
@@ -11770,6 +11756,22 @@
}
}
},
"Hide Session Details" : {
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "隐藏会话详情"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "隱藏工作階段詳細資訊"
}
}
}
},
"Hide tag suggestions" : {
"localizations" : {
"zh-Hans" : {
@@ -22443,6 +22445,7 @@
}
},
"Show" : {
"extractionState" : "stale",
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
@@ -22640,22 +22643,6 @@
}
}
},
"Show Session Details" : {
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "显示会话详情"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "顯示工作階段詳細資訊"
}
}
}
},
"Show Secret" : {
"localizations" : {
"zh-Hans" : {
@@ -22672,6 +22659,22 @@
}
}
},
"Show Session Details" : {
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "显示会话详情"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "顯示工作階段詳細資訊"
}
}
}
},
"Show tag suggestions" : {
"localizations" : {
"zh-Hans" : {
@@ -23774,6 +23777,7 @@
}
},
"Startup Snippet" : {
"extractionState" : "stale",
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
@@ -30405,4 +30409,4 @@
}
},
"version" : "1.2"
}
}
@@ -1,14 +1,26 @@
import SwiftUI
import TermindApplication
import TermindDomain
import TermindPresentation
import UIKit
nonisolated enum MobileCommandHistorySnippetFactory {
static func make(name: String, command: String) -> Snippet? {
let name = name.trimmingCharacters(in: .whitespacesAndNewlines)
guard !name.isEmpty else { return nil }
return Snippet(title: name, body: command)
}
}
struct MobileCommandHistoryView: View {
let library: HostLibraryModel
let workspace: MobileWorkspace
@Environment(\.dismiss) private var dismiss
@Environment(\.locale) private var locale
@State private var query = ""
@State private var clearing = false
@State private var snippetEntry: CommandHistoryEntry?
@State private var snippetName = ""
private var entries: [CommandHistoryEntry] {
_ = workspace.commandHistory.revision
@@ -38,6 +50,10 @@ struct MobileCommandHistoryView: View {
}
Button("Insert", systemImage: "text.insert") { insert(entry) }
.disabled(!workspace.connected)
Button("Save as Snippet", systemImage: "curlybraces") {
beginSavingSnippet(entry)
}
Divider()
Button("Delete", systemImage: "trash", role: .destructive) {
workspace.commandHistory.delete(entry.id)
}
@@ -76,6 +92,24 @@ struct MobileCommandHistoryView: View {
.confirmationDialog("Clear History?", isPresented: $clearing, titleVisibility: .visible) {
Button("Clear", role: .destructive) { workspace.commandHistory.clear(for: workspace.descriptor.hostID) }
}
.alert(
"Save as Snippet",
isPresented: Binding(
get: { snippetEntry != nil },
set: { if !$0 { cancelSavingSnippet() } }
)
) {
TextField("Snippet Name", text: $snippetName.singleLineText, axis: .horizontal)
.singleLinePaste()
.lineLimit(1)
Button("Cancel", role: .cancel, action: cancelSavingSnippet)
Button("Save", action: saveSnippet)
.disabled(snippetName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
} message: {
if let snippetEntry {
Text(CommandHistoryPresentation.title(for: snippetEntry))
}
}
}.mobileSheetPresentation(compact: true)
.onDisappear { workspace.terminal.requestFocus() }
}
@@ -104,4 +138,24 @@ struct MobileCommandHistoryView: View {
dismiss()
workspace.insertTerminalText(entry.command)
}
private func beginSavingSnippet(_ entry: CommandHistoryEntry) {
snippetName = ""
snippetEntry = entry
}
private func cancelSavingSnippet() {
snippetEntry = nil
snippetName = ""
}
private func saveSnippet() {
guard let snippetEntry,
let snippet = MobileCommandHistorySnippetFactory.make(
name: snippetName, command: snippetEntry.command
)
else { return }
library.upsert(snippet)
cancelSavingSnippet()
}
}
@@ -231,7 +231,7 @@ struct MobileWorkspaceView: View {
case .snippets:
MobileTerminalSnippetsView(library: library, workspace: workspace)
case .commandHistory:
MobileCommandHistoryView(workspace: workspace)
MobileCommandHistoryView(library: library, workspace: workspace)
case .sftpHistory:
MobileSFTPHistoryView(workspace: workspace, open: reopenHistory)
}
+13
View File
@@ -11,6 +11,19 @@ import struct TermindDomain.Host
@Suite @MainActor
struct MobileSnippetTests {
@Test func historySnippetFactoryTrimsTheNameAndPreservesCommandDefaults() throws {
let command = "printf first\nprintf second"
let snippet = try #require(
MobileCommandHistorySnippetFactory.make(name: " Deploy ", command: command)
)
#expect(snippet.title == "Deploy")
#expect(snippet.body == command)
#expect(snippet.packageID == nil)
#expect(snippet.targetHostIDs.isEmpty)
#expect(MobileCommandHistorySnippetFactory.make(name: " \n ", command: command) == nil)
}
@Test func legacyTimestampFallbackIsStableUntilARealEditExists() throws {
let suiteName = "termind.snippet-timestamp-tests.\(UUID().uuidString)"
let defaults = try #require(UserDefaults(suiteName: suiteName))
+2 -2
View File
@@ -19,7 +19,7 @@ struct MobileTerminalListTests {
hosts: app.library,
commandHistory: app.commandHistory)
try await withHostedView(MobileCommandHistoryView(workspace: workspace)) { root in
try await withHostedView(MobileCommandHistoryView(library: app.library, workspace: workspace)) { root in
#expect(nativeVisibleCells(in: root.view).count == 2)
let newest = try #require(app.commandHistory.entries(for: host.id).first)
@@ -45,7 +45,7 @@ struct MobileTerminalListTests {
hosts: app.library,
commandHistory: history)
try await withHostedView(MobileCommandHistoryView(workspace: workspace)) { root in
try await withHostedView(MobileCommandHistoryView(library: app.library, workspace: workspace)) { root in
#expect(history.entries(for: host.id).count == CommandHistoryModel.perHostLimit)
let cells = nativeVisibleCells(in: root.view)
#expect(!cells.isEmpty)
+2 -1
View File
@@ -69,6 +69,7 @@ nonisolated enum MenuSymbol {
static let resetName = "arrow.uturn.backward"
static let duplicate = "plus.square.on.square"
static let copy = "doc.on.doc"
static let insert = "text.insert"
static let paste = "doc.on.clipboard"
static let execute = "play"
static let executeOnHosts = "server.rack"
@@ -124,7 +125,7 @@ nonisolated enum MenuSymbol {
newChat, newHost, newGroup, newPackage, newFolder, newSnippet, saveAsSnippet,
newIdentity, generateKey, generateCommand,
importItem, download, export, upload,
details, pause, selectDevices, open, openFullScreen, edit, rename, resetName, duplicate, copy, paste,
details, pause, selectDevices, open, openFullScreen, edit, rename, resetName, duplicate, copy, insert, paste,
execute, executeOnHosts, start, stop, refresh, showHiddenFiles, hideHiddenFiles, editPath,
editPermissions, test,
pin, unpin, archive, unarchive, delete,
+176 -79
View File
@@ -454,10 +454,93 @@ final class SidebarListContainer: NSView {
}
}
@MainActor
enum TerminalSidebarMenuBuilder {
static func snippets(
snippet: Snippet?, canInsert: Bool, locale: Locale,
copy: @escaping (Snippet) -> Void,
insert: @escaping (Snippet) -> Void
) -> [NativeMenuEntry] {
[
NativeMenuEntry(
title: localized("Copy", locale: locale),
symbol: MenuSymbol.copy,
isEnabled: snippet != nil
) {
if let snippet { copy(snippet) }
},
NativeMenuEntry(
title: localized("Insert", locale: locale),
symbol: MenuSymbol.insert,
isEnabled: snippet != nil && canInsert
) {
if let snippet { insert(snippet) }
},
]
}
static func history(
entry: CommandHistoryEntry?, canInsert: Bool, canClear: Bool, locale: Locale,
copy: @escaping (CommandHistoryEntry) -> Void,
insert: @escaping (CommandHistoryEntry) -> Void,
saveAsSnippet: @escaping (CommandHistoryEntry) -> Void,
remove: @escaping (CommandHistoryEntry) -> Void,
clear: @escaping () -> Void
) -> [NativeMenuEntry] {
var entries = [
NativeMenuEntry(
title: localized("Copy", locale: locale),
symbol: MenuSymbol.copy,
isEnabled: entry != nil
) {
if let entry { copy(entry) }
},
NativeMenuEntry(
title: localized("Insert", locale: locale),
symbol: MenuSymbol.insert,
isEnabled: entry != nil && canInsert
) {
if let entry { insert(entry) }
},
NativeMenuEntry(
title: localized("Save as Snippet", locale: locale),
symbol: MenuSymbol.saveAsSnippet,
isEnabled: entry != nil
) {
if let entry { saveAsSnippet(entry) }
},
.separator,
]
if let entry {
entries.append(
NativeMenuEntry(
title: localized("Remove this History", locale: locale),
symbol: MenuSymbol.removeHistory
) {
remove(entry)
})
}
entries.append(
NativeMenuEntry(
title: localized("Clear History", locale: locale),
symbol: MenuSymbol.clearHistory,
isEnabled: canClear,
action: clear
))
return entries
}
private static func localized(
_ key: String.LocalizationValue, locale: Locale
) -> String {
AppLocalization.string(key, locale: locale)
}
}
// MARK: - Session Snippets
/// The global Snippet shelf. Execution always respects the Snippet's saved
/// Targets; the focused terminal is never an implicit destination.
/// The global Snippet shelf inserts into the focused terminal. The full
/// Snippets page remains the place for executing a Snippet on its saved Targets.
private struct SnippetsSidebarView: View {
@Environment(AppModel.self) private var app
@Environment(WindowModel.self) private var window
@@ -545,17 +628,14 @@ private struct SnippetsSidebarView: View {
)
}
/// This shelf only runs what is already saved; snippets are made on the
/// Snippets page. With nothing under the pointer both verbs grey out rather
/// than the menu not appearing, so the shelf answers a right-click the same
/// way wherever it lands.
/// With nothing under the pointer both verbs grey out rather than the menu
/// not appearing, so the shelf answers a right-click the same way wherever
/// it lands.
private var blankAreaMenu: [NativeMenuEntry] {
[
NativeMenuEntry(title: localized("Copy"), symbol: MenuSymbol.copy, isEnabled: false),
NativeMenuEntry(
title: localized("Execute"), symbol: MenuSymbol.execute, isEnabled: false
),
]
TerminalSidebarMenuBuilder.snippets(
snippet: nil, canInsert: canInsert, locale: locale,
copy: copy, insert: insert
)
}
private func snippetRow(_ snippet: Snippet) -> some View {
@@ -563,17 +643,16 @@ private struct SnippetsSidebarView: View {
snippet: snippet, isSelected: selectedSnippetID == snippet.id
)
.termindSidebarRow(isSelected: selectedSnippetID == snippet.id) {
[
NativeMenuEntry(title: localized("Copy"), symbol: MenuSymbol.copy) {
copyToPasteboard(snippet.body)
},
NativeMenuEntry(title: localized("Execute"), symbol: MenuSymbol.execute) {
requestExecution(snippet)
},
]
TerminalSidebarMenuBuilder.snippets(
snippet: snippet, canInsert: canInsert, locale: locale,
copy: copy, insert: insert
)
}
.onTapGesture(count: 2) { execute(snippet) }
.onTapGesture {
selectedSnippetID = snippet.id
insert(snippet)
}
.onTapGesture(count: 2) { requestExecution(snippet) }
.onTapGesture { selectedSnippetID = snippet.id }
}
private var displayedSnippets: [Snippet] {
@@ -603,14 +682,24 @@ private struct SnippetsSidebarView: View {
}
}
private func requestExecution(_ snippet: Snippet) {
if snippet.targetHostIDs.isEmpty {
window.hostChooser = .snippet(
SnippetHostChooserRequest(
snippetIDs: [snippet.id], strategy: .fillMissingTargets
))
return
}
private var canInsert: Bool {
window.selectedWorkspace?.focusedPane.sshSession?.status.isLive == true
}
private func copy(_ snippet: Snippet) {
copyToPasteboard(snippet.body)
}
private func insert(_ snippet: Snippet) {
guard let workspace = window.selectedWorkspace,
let sshSession = workspace.focusedPane.sshSession,
sshSession.status.isLive
else { return }
workspace.deliver(snippet.body, from: sshSession)
}
private func execute(_ snippet: Snippet) {
guard canInsert else { return }
if CommandRiskClassifier.assess(snippet.body).requiresSecondConfirmation {
destructiveSnippet = snippet
} else {
@@ -619,7 +708,14 @@ private struct SnippetsSidebarView: View {
}
private func executeNow(_ snippet: Snippet) {
app.execute([snippet], in: window)
guard let workspace = window.selectedWorkspace,
let sshSession = workspace.focusedPane.sshSession,
sshSession.status.isLive
else {
destructiveSnippet = nil
return
}
workspace.deliver(snippet.body, from: sshSession, submit: true)
destructiveSnippet = nil
}
}
@@ -707,6 +803,7 @@ private struct HistorySidebarView: View {
private struct SSHCommandHistorySidebar: View {
@Environment(AppModel.self) private var app
@Environment(WindowModel.self) private var window
@Environment(\.locale) private var locale
@State private var query = ""
@State private var destructiveEntry: CommandHistoryEntry?
@@ -816,22 +913,7 @@ private struct SSHCommandHistorySidebar: View {
/// needs a row greyed out. Clear History is not one of those — it is about
/// the list, so it stays live where there is nothing under the pointer.
private var globalMenu: [NativeMenuEntry] {
[
NativeMenuEntry(
title: String(localized: "Copy"), symbol: MenuSymbol.copy, isEnabled: false
),
NativeMenuEntry(
title: String(localized: "Save as Snippet"),
symbol: MenuSymbol.saveAsSnippet,
isEnabled: false
),
.separator,
NativeMenuEntry(
title: String(localized: "Clear History"),
symbol: MenuSymbol.clearHistory,
isEnabled: !allEntries.isEmpty,
) { confirmsClear = true },
]
historyMenu(for: nil)
}
@ViewBuilder
@@ -847,10 +929,6 @@ private struct SSHCommandHistorySidebar: View {
.frame(height: 18)
.padding(.horizontal, Metrics.Space.s)
.padding(.vertical, 4)
.background(
Color.primary.opacity(0.055),
in: .rect(cornerRadius: Metrics.Radius.small)
)
.overlay {
RoundedRectangle(cornerRadius: Metrics.Radius.small)
.strokeBorder(Color.accentColor.opacity(0.55))
@@ -862,6 +940,7 @@ private struct SSHCommandHistorySidebar: View {
}
.padding(.vertical, 2)
.padding(.horizontal, Metrics.Space.m)
.termindSidebarRowBackground(isSelected: false)
} else {
CommandHistoryCard(
entry: entry,
@@ -877,32 +956,25 @@ private struct SSHCommandHistorySidebar: View {
insert(entry)
}
.termindSidebarRow(isSelected: selectedID == entry.id) {
[
NativeMenuEntry(
title: String(localized: "Copy"), symbol: MenuSymbol.copy
) {
copyToPasteboard(entry.command)
},
NativeMenuEntry(
title: String(localized: "Save as Snippet"),
symbol: MenuSymbol.saveAsSnippet
) {
beginSnippetNaming(entry)
},
.separator,
NativeMenuEntry(
title: String(localized: "Remove this History"),
symbol: MenuSymbol.removeHistory,
) { delete(entry) },
NativeMenuEntry(
title: String(localized: "Clear History"),
symbol: MenuSymbol.clearHistory,
) { confirmsClear = true },
]
historyMenu(for: entry)
}
}
}
private func historyMenu(for entry: CommandHistoryEntry?) -> [NativeMenuEntry] {
TerminalSidebarMenuBuilder.history(
entry: entry,
canInsert: canInsert,
canClear: !allEntries.isEmpty,
locale: locale,
copy: { copyToPasteboard($0.command) },
insert: insert,
saveAsSnippet: beginSnippetNaming,
remove: delete,
clear: { confirmsClear = true }
)
}
private var historyListItems: [SidebarListItem] {
let entries = allEntries.map {
SidebarListItem.history($0, isNaming: namingEntryID == $0.id)
@@ -924,13 +996,15 @@ private struct SSHCommandHistorySidebar: View {
}
private func insert(_ entry: CommandHistoryEntry) {
guard let workspace = window.selectedWorkspace, let sshSession = workspace.focusedPane.sshSession else {
return
}
guard let workspace = window.selectedWorkspace,
let sshSession = workspace.focusedPane.sshSession,
sshSession.status.isLive
else { return }
workspace.deliver(entry.command, from: sshSession)
}
private func execute(_ entry: CommandHistoryEntry) {
guard canInsert else { return }
if CommandRiskClassifier.assess(entry.command).requiresSecondConfirmation {
destructiveEntry = entry
} else {
@@ -939,7 +1013,11 @@ private struct SSHCommandHistorySidebar: View {
}
private func executeNow(_ entry: CommandHistoryEntry) {
guard let workspace = window.selectedWorkspace, let sshSession = workspace.focusedPane.sshSession else {
guard let workspace = window.selectedWorkspace,
let sshSession = workspace.focusedPane.sshSession,
sshSession.status.isLive
else {
destructiveEntry = nil
return
}
workspace.deliver(entry.command, from: sshSession, submit: true)
@@ -968,6 +1046,10 @@ private struct SSHCommandHistorySidebar: View {
namingEntryID = entry.id
}
private var canInsert: Bool {
sshSession?.status.isLive == true
}
private func cancelSnippetNaming() {
namingEntryID = nil
snippetName = ""
@@ -1336,14 +1418,14 @@ private struct SidebarInlineTextField: NSViewRepresentable {
field.target = context.coordinator
field.action = #selector(Coordinator.submit)
field.stringValue = text
field.placeholderString = prompt
field.isBezeled = false
field.isBordered = false
field.drawsBackground = false
field.focusRingType = .none
field.font = NSFont.preferredFont(forTextStyle: .body)
field.lineBreakMode = .byTruncatingTail
field.cell?.usesSingleLineMode = true
field.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
configure(field)
context.coordinator.requestFocus(for: field)
return field
}
@@ -1352,6 +1434,19 @@ private struct SidebarInlineTextField: NSViewRepresentable {
if field.stringValue != text {
field.stringValue = text
}
configure(field)
}
private func configure(_ field: NSTextField) {
let font = NSFont.preferredFont(forTextStyle: .body)
field.font = font
field.textColor = .labelColor
field.placeholderAttributedString = NSAttributedString(
string: prompt,
attributes: [.font: font, .foregroundColor: NSColor.secondaryLabelColor]
)
field.currentEditor()?.textColor = .labelColor
field.currentEditor()?.drawsBackground = false
}
final class Coordinator: NSObject, NSTextFieldDelegate {
@@ -1375,6 +1470,8 @@ private struct SidebarInlineTextField: NSViewRepresentable {
didRequestFocus = true
DispatchQueue.main.async {
field.window?.makeFirstResponder(field)
field.currentEditor()?.textColor = .labelColor
field.currentEditor()?.drawsBackground = false
}
}
+17
View File
@@ -100,6 +100,23 @@ struct BroadcastInputTests {
#expect(secondWrites == [input])
}
@Test
func programmaticInsertAndSubmitUseTheSameBroadcastPath() {
let (workspace, firstSSHSession, secondSSHSession) = makeTwoSSHSessionWorkspace()
var firstWrites: [Data] = []
var secondWrites: [Data] = []
firstSSHSession.inputSink = { firstWrites.append($0) }
secondSSHSession.inputSink = { secondWrites.append($0) }
workspace.broadcastsInput = true
workspace.deliver("uptime", from: firstSSHSession)
workspace.deliver("whoami", from: firstSSHSession, submit: true)
let expected = [Data("uptime".utf8), Data("whoami\r".utf8)]
#expect(firstWrites == expected)
#expect(secondWrites == expected)
}
@Test
func nonInteractiveTerminalWritesNeverBroadcast() {
let (workspace, firstSSHSession, secondSSHSession) = makeTwoSSHSessionWorkspace()
+50
View File
@@ -300,6 +300,56 @@ struct ListViewMigrationTests {
]
)
}
@Test @MainActor
func terminalSidebarMenusKeepCopyInsertAndHistoryGroupingAligned() {
let locale = Locale(identifier: "en")
let snippet = Snippet(title: "Status", body: "uptime")
var insertedSnippet: Snippet.ID?
let snippetEntries = TerminalSidebarMenuBuilder.snippets(
snippet: snippet, canInsert: true, locale: locale,
copy: { _ in }, insert: { insertedSnippet = $0.id }
)
#expect(snippetEntries.map(\.title) == ["Copy", "Insert"])
#expect(snippetEntries.allSatisfy { $0.isEnabled })
snippetEntries[1].action()
#expect(insertedSnippet == snippet.id)
let disabledSnippetEntries = TerminalSidebarMenuBuilder.snippets(
snippet: nil, canInsert: false, locale: locale,
copy: { _ in }, insert: { _ in }
)
#expect(disabledSnippetEntries.map(\.title) == ["Copy", "Insert"])
#expect(disabledSnippetEntries.allSatisfy { !$0.isEnabled })
let history = CommandHistoryEntry(hostID: UUID(), command: "whoami")
let historyEntries = TerminalSidebarMenuBuilder.history(
entry: history, canInsert: true, canClear: true, locale: locale,
copy: { _ in }, insert: { _ in }, saveAsSnippet: { _ in },
remove: { _ in }, clear: {}
)
#expect(
historyEntries.map(\.title)
== [
"Copy", "Insert", "Save as Snippet", "", "Remove this History",
"Clear History",
]
)
#expect(historyEntries[3].isSeparator)
let blankHistoryEntries = TerminalSidebarMenuBuilder.history(
entry: nil, canInsert: false, canClear: true, locale: locale,
copy: { _ in }, insert: { _ in }, saveAsSnippet: { _ in },
remove: { _ in }, clear: {}
)
#expect(
blankHistoryEntries.map(\.title)
== ["Copy", "Insert", "Save as Snippet", "", "Clear History"]
)
#expect(blankHistoryEntries.prefix(3).allSatisfy { !$0.isEnabled })
#expect(blankHistoryEntries.last?.isEnabled == true)
}
}
@Test