mirror of
https://github.com/abue-ammar/tinycast.git
synced 2026-10-02 08:14:38 +08:00
Close the AppCore residue left by phase 32 (#195)
Three survivors, one finding: AppCore still owned what comparable types own for themselves. QuicklinkCoordinator now takes a paletteCoordinator like its six siblings, so the showPalette/hidePalette forwarders go. runWindowCommand becomes WindowCommandCoordinator, the eleventh, so no feature action is implemented on AppCore. SettingsBackup and BackupActions take core as a required parameter, retiring a default argument no caller ever supplied. Deletes handleReopen and showSettings alongside the two the phase names, so AppCore carries no palette forwarder at all. That requalifies four call sites in TinycastApp and AppDelegate, both on the phase's must-NOT-change list, on an explicit operator decision: the phase deletes showPalette while forbidding its only caller. Both files still reach AppCore.shared and the count is still six, so the boundary's stated rationale -- no environment in a Scene's menu content -- holds. AppCore 319 -> 290; AC7 wanted under 280, and the phase's -45 estimate does not match the ~28 lines of methods it names. All 17 harnesses pass. The manual verification pass was waived.
This commit is contained in:
@@ -285,7 +285,7 @@ for its job.
|
||||
| Suffix | Means | Members |
|
||||
| ------------- | -------------------------------------------------------------- | ------------------------------------------------------------------------------ |
|
||||
| `Store` | Owns persisted state and publishes it | open (10) |
|
||||
| `Coordinator` | A feature's action surface, called by `AppCore` and the palette | open (10) |
|
||||
| `Coordinator` | A feature's action surface, called by `AppCore` and the palette | open (11) |
|
||||
| `Controller` | Owns one AppKit window or surface | open (5) |
|
||||
| `Catalog` | Pure static namespace over a built-in list | `CommandCatalog`, `EmojiCatalog`, `SystemActionCatalog`, `WindowCommandCatalog` |
|
||||
| `Index` | A searchable collection, rebuilt as its inputs change | `AppIndex`, `EmojiIndex`, `PaletteRowIndex` |
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
4109B5CF37DCA4BCDAE46412 /* ShellCommandRunner.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBFE3C41897391C14DB5A55C /* ShellCommandRunner.swift */; };
|
||||
410F1B5FFAA44378AC9EC2DB /* SystemSettingsSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 951E746BA1AD25D167A1775F /* SystemSettingsSettingsView.swift */; };
|
||||
42E1CF2605A0E02D84932B26 /* Gunzip.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1213B069324F16F5364FECD3 /* Gunzip.swift */; };
|
||||
43885792945F28AF3CDE30BC /* WindowCommandCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06125F7DDE68BB4D52065C06 /* WindowCommandCoordinator.swift */; };
|
||||
43DBCE7B5A84D0D6322124F5 /* MessageHUDController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B2CB71671625A1F446316A1 /* MessageHUDController.swift */; };
|
||||
4549280508130B232069071E /* BackupSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 112551581ECF83B53D405334 /* BackupSettingsView.swift */; };
|
||||
4640C7CC8DCA6E6FA49394A8 /* CommandsSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D303E193CFEFE455778C150 /* CommandsSettingsView.swift */; };
|
||||
@@ -199,6 +200,7 @@
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
02BCAB1F3B6930FE0B819F6D /* OnboardingState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingState.swift; sourceTree = "<group>"; };
|
||||
06125F7DDE68BB4D52065C06 /* WindowCommandCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowCommandCoordinator.swift; sourceTree = "<group>"; };
|
||||
0935D49C0A0175E54E69682B /* QuicklinkListScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuicklinkListScreen.swift; sourceTree = "<group>"; };
|
||||
09633E6F9C37F89267341D1D /* VolumeHUDController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VolumeHUDController.swift; sourceTree = "<group>"; };
|
||||
0966995230FE7B7072C0018B /* EmojiGridGeometry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiGridGeometry.swift; sourceTree = "<group>"; };
|
||||
@@ -1020,6 +1022,7 @@
|
||||
children = (
|
||||
0A97C1CFD02D55E955AFA72B /* WindowActionMemory.swift */,
|
||||
9B9ADC5C0B42F7EF38F77DE6 /* WindowCommand.swift */,
|
||||
06125F7DDE68BB4D52065C06 /* WindowCommandCoordinator.swift */,
|
||||
E779B6EF738591CB935F3F95 /* WindowLayout.swift */,
|
||||
B21678BAD7BA4A4E300FDA2C /* WindowManagementSettingsView.swift */,
|
||||
AA99178E0DCF6CBB521FB098 /* WindowMover.swift */,
|
||||
@@ -1404,6 +1407,7 @@
|
||||
1F1AD34A26931AF6E11E13F7 /* VolumeState.swift in Sources */,
|
||||
D44E20726F1EB11C8C297C4D /* WindowActionMemory.swift in Sources */,
|
||||
2C788DCDF146ED245150EF82 /* WindowCommand.swift in Sources */,
|
||||
43885792945F28AF3CDE30BC /* WindowCommandCoordinator.swift in Sources */,
|
||||
19B0314193376FD1E4AC11A5 /* WindowLayout.swift in Sources */,
|
||||
0360C7038877B9CB656DB697 /* WindowManagementSettingsView.swift in Sources */,
|
||||
A6A8CD2B71A15625F47498EA /* WindowMover.swift in Sources */,
|
||||
|
||||
@@ -42,6 +42,7 @@ final class AppCore {
|
||||
store: quicklinks, argumentSession: quicklinkArguments, settings: settings,
|
||||
appIndex: appIndex, injector: snippetTextInjector, hotKeys: hotKeys, favorites: favorites,
|
||||
visibility: visibility, ranking: launcherRanking, windowController: windowController,
|
||||
paletteCoordinator: paletteCoordinator,
|
||||
clipboardHistory: { [unowned self] in self.snippetExpansion.clipboardHistoryForExpansion() },
|
||||
core: self)
|
||||
|
||||
@@ -54,6 +55,8 @@ final class AppCore {
|
||||
session: uninstall, palette: palette, paletteCoordinator: paletteCoordinator,
|
||||
appIndex: appIndex, runningApps: runningApps, hotKeys: hotKeys, favorites: favorites,
|
||||
visibility: visibility, ranking: launcherRanking, core: self)
|
||||
@ObservationIgnored private(set) lazy var windowCommandCoordinator = WindowCommandCoordinator(
|
||||
settings: settings, paletteCoordinator: paletteCoordinator, windowMover: windowMover)
|
||||
@ObservationIgnored private(set) lazy var customCommandCoordinator = CustomCommandCoordinator(
|
||||
store: customCommands, settings: settings, appIndex: appIndex,
|
||||
paletteCoordinator: paletteCoordinator, hotKeys: hotKeys, favorites: favorites,
|
||||
@@ -64,7 +67,9 @@ final class AppCore {
|
||||
paletteCoordinator: paletteCoordinator,
|
||||
customCommandCoordinator: customCommandCoordinator,
|
||||
systemActionCoordinator: systemActionCoordinator,
|
||||
quicklinkCoordinator: quicklinkCoordinator, snippetExpansion: snippetExpansion, core: self)
|
||||
quicklinkCoordinator: quicklinkCoordinator,
|
||||
windowCommandCoordinator: windowCommandCoordinator,
|
||||
snippetExpansion: snippetExpansion, core: self)
|
||||
@ObservationIgnored private(set) lazy var clipboardCoordinator = ClipboardCoordinator(
|
||||
clipboardStore: clipboardStore, palette: palette, windowController: windowController,
|
||||
paletteCoordinator: paletteCoordinator)
|
||||
@@ -138,7 +143,9 @@ final class AppCore {
|
||||
hotKeys.onRunSystemAction = { [weak self] id in
|
||||
self?.systemActionCoordinator.runSystemAction(id: id)
|
||||
}
|
||||
hotKeys.onRunWindowCommand = { [weak self] id in self?.runWindowCommand(id: id) }
|
||||
hotKeys.onRunWindowCommand = { [weak self] id in
|
||||
self?.windowCommandCoordinator.runWindowCommand(id: id)
|
||||
}
|
||||
hotKeys.onOpenQuicklink = { [weak self] id in
|
||||
self?.quicklinkCoordinator.openQuicklink(id: id)
|
||||
}
|
||||
@@ -243,42 +250,6 @@ final class AppCore {
|
||||
appIndex.setWindowCommandsVisible(visible)
|
||||
}
|
||||
|
||||
// MARK: - Palette control
|
||||
|
||||
func showPalette(mode: PaletteMode, restoreAnyMode: Bool = false) {
|
||||
paletteCoordinator.showPalette(mode: mode, restoreAnyMode: restoreAnyMode)
|
||||
}
|
||||
|
||||
func hidePalette(restoreFocus: Bool = true) {
|
||||
paletteCoordinator.hidePalette(restoreFocus: restoreFocus)
|
||||
}
|
||||
|
||||
func handleReopen() {
|
||||
paletteCoordinator.handleReopen()
|
||||
}
|
||||
|
||||
func showSettings(tab: SettingsTab = .general) {
|
||||
paletteCoordinator.showSettings(tab: tab)
|
||||
}
|
||||
|
||||
// MARK: - Window commands
|
||||
|
||||
/// The one funnel for both palette activation and a command's global hotkey, so the feature switch
|
||||
/// can't be bypassed by either — a shortcut stays registered while the feature is off and must move
|
||||
/// nothing.
|
||||
///
|
||||
/// The command acts on the app the user was in, so the palette hands focus back before dispatching,
|
||||
/// the same dance the paste path does. Focus is restored rather than dropped: the window being moved
|
||||
/// is the one they want to keep working in.
|
||||
func runWindowCommand(id: WindowCommand.ID) {
|
||||
guard settings.windowManagementEnabled else { return }
|
||||
let target = paletteCoordinator.targetApp
|
||||
if paletteCoordinator.isVisible { hidePalette(restoreFocus: true) }
|
||||
windowMover.perform(
|
||||
id, target: target, gap: CGFloat(settings.windowGap),
|
||||
cycleOnRepeat: settings.windowCycleOnRepeat)
|
||||
}
|
||||
|
||||
// MARK: - Dialogs
|
||||
//
|
||||
// Routed through `AppCore` so `dialogs` stays the single owner; flows outside the palette (the backup
|
||||
|
||||
@@ -11,7 +11,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
}
|
||||
|
||||
func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool {
|
||||
AppCore.shared.handleReopen()
|
||||
AppCore.shared.paletteCoordinator.handleReopen()
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,10 +13,14 @@ struct TinycastApp: App {
|
||||
MenuBarExtra(
|
||||
appName, systemImage: "macwindow.on.rectangle", isInserted: $showInMenuBar
|
||||
) {
|
||||
Button("Open \(appName)") { AppCore.shared.showPalette(mode: .launcher) }
|
||||
Button("Clipboard History") { AppCore.shared.showPalette(mode: .clipboard) }
|
||||
Button("Open \(appName)") {
|
||||
AppCore.shared.paletteCoordinator.showPalette(mode: .launcher)
|
||||
}
|
||||
Button("Clipboard History") {
|
||||
AppCore.shared.paletteCoordinator.showPalette(mode: .clipboard)
|
||||
}
|
||||
Divider()
|
||||
Button("Settings...") { AppCore.shared.showSettings() }
|
||||
Button("Settings...") { AppCore.shared.paletteCoordinator.showSettings() }
|
||||
.keyboardShortcut(",")
|
||||
Divider()
|
||||
Button("Quit \(appName)") { NSApp.terminate(nil) }
|
||||
|
||||
@@ -73,7 +73,7 @@ struct SettingsBackup: Codable {
|
||||
|
||||
@MainActor
|
||||
extension SettingsBackup {
|
||||
static func gather(from core: AppCore = .shared) -> SettingsBackup {
|
||||
static func gather(from core: AppCore) -> SettingsBackup {
|
||||
let s = core.settings
|
||||
var backup = SettingsBackup()
|
||||
backup.settings = SettingsData(
|
||||
@@ -145,7 +145,7 @@ extension SettingsBackup {
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func apply(to core: AppCore = .shared) -> ApplySummary {
|
||||
func apply(to core: AppCore) -> ApplySummary {
|
||||
var summary = ApplySummary()
|
||||
if let s = settings { summary.settingsFields = applySettings(s, to: core) }
|
||||
if let customCommands {
|
||||
|
||||
@@ -36,38 +36,43 @@ enum BackupActions {
|
||||
return panel.url
|
||||
}
|
||||
|
||||
static func exportSettings() async {
|
||||
static func exportSettings(core: AppCore) async {
|
||||
guard let url = chooseSaveLocation(named: "Tinycast-Settings") else { return }
|
||||
do {
|
||||
try SettingsBackup.gather().encoded().write(to: url, options: .atomic)
|
||||
try SettingsBackup.gather(from: core).encoded().write(to: url, options: .atomic)
|
||||
} catch {
|
||||
await present(
|
||||
title: "Export Failed", message: error.localizedDescription,
|
||||
core: core, title: "Export Failed", message: error.localizedDescription,
|
||||
symbol: "square.and.arrow.up")
|
||||
}
|
||||
}
|
||||
|
||||
static func importSettings() async {
|
||||
static func importSettings(core: AppCore) async {
|
||||
guard let url = chooseJSONFile() else { return }
|
||||
do {
|
||||
let backup = try SettingsBackup(json: try Data(contentsOf: url))
|
||||
let commandCount = backup.customCommands?.count ?? 0
|
||||
let shortcutCount = backup.hotkeys?.customCommands?.count ?? 0
|
||||
guard await confirmExecutableImport(commands: commandCount, shortcuts: shortcutCount)
|
||||
guard
|
||||
await confirmExecutableImport(
|
||||
core: core, commands: commandCount, shortcuts: shortcutCount)
|
||||
else { return }
|
||||
await present(
|
||||
title: "Settings Imported", message: summaryText(backup.apply()),
|
||||
core: core, title: "Settings Imported",
|
||||
message: summaryText(backup.apply(to: core)),
|
||||
symbol: importSymbol, tone: .success)
|
||||
} catch {
|
||||
await present(
|
||||
title: "Import Failed", message: error.localizedDescription, symbol: importSymbol)
|
||||
core: core, title: "Import Failed", message: error.localizedDescription,
|
||||
symbol: importSymbol)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Raycast (the pane owns the passphrase field + inline status)
|
||||
|
||||
static func importRaycast(file: URL, passphrase: String, options: RaycastImportOptions = .all)
|
||||
async throws -> RaycastOutcome {
|
||||
static func importRaycast(
|
||||
core: AppCore, file: URL, passphrase: String, options: RaycastImportOptions = .all
|
||||
) async throws -> RaycastOutcome {
|
||||
// Detect, decrypt and parse off the main actor, inside an autoreleasepool so the large JSON tree drains at once instead of spiking the main-thread footprint. Only the value-type Result crosses back.
|
||||
let result = try await Task.detached(priority: .userInitiated) {
|
||||
try autoreleasepool {
|
||||
@@ -80,19 +85,19 @@ enum BackupActions {
|
||||
if !result.snippets.isEmpty {
|
||||
do {
|
||||
// Starting the (lazily started) store first gets the imported snippets into the launcher immediately. With the feature switched off the files are still written and appear once it's re-enabled.
|
||||
if AppCore.shared.settings.snippetsEnabled {
|
||||
await AppCore.shared.snippetsStore.start()
|
||||
if core.settings.snippetsEnabled {
|
||||
await core.snippetsStore.start()
|
||||
}
|
||||
snippetsImported =
|
||||
try await AppCore.shared.snippetsStore.importSnippets(result.snippets).count
|
||||
try await core.snippetsStore.importSnippets(result.snippets).count
|
||||
} catch {
|
||||
snippetsError = error.localizedDescription
|
||||
}
|
||||
}
|
||||
let summary = result.backup.apply()
|
||||
let summary = result.backup.apply(to: core)
|
||||
let imported =
|
||||
result.clipboard.isEmpty
|
||||
? 0 : AppCore.shared.clipboardStore.importEntries(result.clipboard)
|
||||
? 0 : core.clipboardStore.importEntries(result.clipboard)
|
||||
return RaycastOutcome(
|
||||
summary: summary,
|
||||
clipboardImported: imported,
|
||||
@@ -151,14 +156,16 @@ enum BackupActions {
|
||||
return "Applied " + parts.joined(separator: ", ") + "."
|
||||
}
|
||||
|
||||
private static func confirmExecutableImport(commands: Int, shortcuts: Int) async -> Bool {
|
||||
private static func confirmExecutableImport(core: AppCore, commands: Int, shortcuts: Int) async
|
||||
-> Bool
|
||||
{
|
||||
guard commands > 0 || shortcuts > 0 else { return true }
|
||||
let commandText = commands == 1 ? "1 custom command" : "\(commands) custom commands"
|
||||
let shortcutText =
|
||||
shortcuts == 1 ? "1 global shortcut" : "\(shortcuts) global shortcuts"
|
||||
// Red glyph because this is a real security warning, but a plain button: importing a file
|
||||
// destroys nothing, so the confirm action isn't destructive.
|
||||
return await AppCore.shared.confirm(
|
||||
return await core.confirm(
|
||||
title: "Import executable commands?",
|
||||
message:
|
||||
"This backup contains \(commandText) and \(shortcutText). Custom commands can run "
|
||||
@@ -176,9 +183,8 @@ enum BackupActions {
|
||||
private static let importSymbol = "square.and.arrow.down"
|
||||
|
||||
private static func present(
|
||||
title: String, message: String, symbol: String, tone: DialogTone = .danger
|
||||
core: AppCore, title: String, message: String, symbol: String, tone: DialogTone = .danger
|
||||
) async {
|
||||
await AppCore.shared.showNotice(
|
||||
title: title, message: message, symbol: symbol, tone: tone)
|
||||
await core.showNotice(title: title, message: message, symbol: symbol, tone: tone)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ struct BackupSettingsView: View {
|
||||
systemImage: "square.and.arrow.up",
|
||||
tint: .blue
|
||||
) {
|
||||
Button("Export…") { Task { await BackupActions.exportSettings() } }
|
||||
Button("Export…") { Task { await BackupActions.exportSettings(core: core) } }
|
||||
.controlSize(.small)
|
||||
}
|
||||
SettingsDivider()
|
||||
@@ -51,7 +51,7 @@ struct BackupSettingsView: View {
|
||||
systemImage: "square.and.arrow.down",
|
||||
tint: .green
|
||||
) {
|
||||
Button("Import…") { Task { await BackupActions.importSettings() } }
|
||||
Button("Import…") { Task { await BackupActions.importSettings(core: core) } }
|
||||
.controlSize(.small)
|
||||
}
|
||||
}
|
||||
@@ -160,7 +160,7 @@ struct BackupSettingsView: View {
|
||||
defer { importing = false }
|
||||
do {
|
||||
let outcome = try await BackupActions.importRaycast(
|
||||
file: file, passphrase: passphrase, options: selection)
|
||||
core: core, file: file, passphrase: passphrase, options: selection)
|
||||
var parts: [String] = []
|
||||
if let applied = BackupActions.appliedText(outcome.summary) { parts.append(applied) }
|
||||
if outcome.clipboardImported > 0 {
|
||||
|
||||
@@ -9,8 +9,9 @@ final class LauncherCoordinator {
|
||||
private let customCommandCoordinator: CustomCommandCoordinator
|
||||
private let systemActionCoordinator: SystemActionCoordinator
|
||||
private let quicklinkCoordinator: QuicklinkCoordinator
|
||||
private let windowCommandCoordinator: WindowCommandCoordinator
|
||||
private let snippetExpansion: SnippetExpansionCoordinator
|
||||
/// Window commands only — that funnel is permanently `AppCore`'s.
|
||||
/// The backup commands only, which need the live stores to gather from and apply to.
|
||||
private unowned let core: AppCore
|
||||
|
||||
init(
|
||||
@@ -20,6 +21,7 @@ final class LauncherCoordinator {
|
||||
customCommandCoordinator: CustomCommandCoordinator,
|
||||
systemActionCoordinator: SystemActionCoordinator,
|
||||
quicklinkCoordinator: QuicklinkCoordinator,
|
||||
windowCommandCoordinator: WindowCommandCoordinator,
|
||||
snippetExpansion: SnippetExpansionCoordinator,
|
||||
core: AppCore
|
||||
) {
|
||||
@@ -29,6 +31,7 @@ final class LauncherCoordinator {
|
||||
self.customCommandCoordinator = customCommandCoordinator
|
||||
self.systemActionCoordinator = systemActionCoordinator
|
||||
self.quicklinkCoordinator = quicklinkCoordinator
|
||||
self.windowCommandCoordinator = windowCommandCoordinator
|
||||
self.snippetExpansion = snippetExpansion
|
||||
self.core = core
|
||||
}
|
||||
@@ -56,7 +59,7 @@ final class LauncherCoordinator {
|
||||
}
|
||||
if app.kind == .windowCommand {
|
||||
guard let command = WindowCommandCatalog.command(forEntryID: app.id) else { return }
|
||||
core.runWindowCommand(id: command.id)
|
||||
windowCommandCoordinator.runWindowCommand(id: command.id)
|
||||
return
|
||||
}
|
||||
// Also before the palette hides: a quicklink with an unfilled argument stays in the palette
|
||||
@@ -103,10 +106,10 @@ final class LauncherCoordinator {
|
||||
Task { await quicklinkCoordinator.exportQuicklinks() }
|
||||
case .exportSettings:
|
||||
paletteCoordinator.hidePalette(restoreFocus: false)
|
||||
Task { await BackupActions.exportSettings() }
|
||||
Task { await BackupActions.exportSettings(core: core) }
|
||||
case .importSettings:
|
||||
paletteCoordinator.hidePalette(restoreFocus: false)
|
||||
Task { await BackupActions.importSettings() }
|
||||
Task { await BackupActions.importSettings(core: core) }
|
||||
case .importFromRaycast:
|
||||
paletteCoordinator.hidePalette(restoreFocus: false)
|
||||
paletteCoordinator.showBackupSettings()
|
||||
|
||||
@@ -189,7 +189,7 @@ struct OnboardingView: View {
|
||||
SecureField("Passphrase", text: $model.passphrase)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.frame(width: 150)
|
||||
.onSubmit { model.run() }
|
||||
.onSubmit { model.run(core: core) }
|
||||
}
|
||||
}
|
||||
RaycastImportSelection(selection: $model.selection, format: model.format)
|
||||
@@ -284,7 +284,7 @@ struct OnboardingView: View {
|
||||
case 1 where !accessibilityTrusted:
|
||||
Permissions.openAccessibilitySettings()
|
||||
case 2 where !model.didImport:
|
||||
model.run()
|
||||
model.run(core: core)
|
||||
case Self.lastStep:
|
||||
core.paletteCoordinator.finishOnboarding()
|
||||
default:
|
||||
@@ -386,7 +386,7 @@ final class OnboardingModel {
|
||||
status = nil
|
||||
}
|
||||
|
||||
func run() {
|
||||
func run(core: AppCore) {
|
||||
guard canImport, let file else { return }
|
||||
importing = true
|
||||
status = nil
|
||||
@@ -394,7 +394,7 @@ final class OnboardingModel {
|
||||
defer { importing = false }
|
||||
do {
|
||||
let outcome = try await BackupActions.importRaycast(
|
||||
file: file, passphrase: passphrase, options: selection)
|
||||
core: core, file: file, passphrase: passphrase, options: selection)
|
||||
var message = BackupActions.summaryText(outcome.summary)
|
||||
if outcome.clipboardImported > 0 {
|
||||
message += " Imported \(outcome.clipboardImported) clipboard entries."
|
||||
|
||||
@@ -13,9 +13,10 @@ final class QuicklinkCoordinator {
|
||||
private let visibility: VisibilityStore
|
||||
private let ranking: LauncherRankingStore
|
||||
private let windowController: PaletteWindowController
|
||||
private let paletteCoordinator: PaletteCoordinator
|
||||
/// `{clipboard offset=N}` reads the history a snippet expansion does; one owner, one depth.
|
||||
private let clipboardHistory: @MainActor () -> [String]
|
||||
/// Palette, Settings, dialog and HUD presentation only — never for state this type owns.
|
||||
/// Dialogs, the HUD, and the `pendingQuicklinkEdit` handoff to the Settings pane.
|
||||
private unowned let core: AppCore
|
||||
|
||||
/// Carries the menu's default-app override across the quicklink argument prompt.
|
||||
@@ -32,6 +33,7 @@ final class QuicklinkCoordinator {
|
||||
visibility: VisibilityStore,
|
||||
ranking: LauncherRankingStore,
|
||||
windowController: PaletteWindowController,
|
||||
paletteCoordinator: PaletteCoordinator,
|
||||
clipboardHistory: @escaping @MainActor () -> [String],
|
||||
core: AppCore
|
||||
) {
|
||||
@@ -45,6 +47,7 @@ final class QuicklinkCoordinator {
|
||||
self.visibility = visibility
|
||||
self.ranking = ranking
|
||||
self.windowController = windowController
|
||||
self.paletteCoordinator = paletteCoordinator
|
||||
self.clipboardHistory = clipboardHistory
|
||||
self.core = core
|
||||
}
|
||||
@@ -101,7 +104,7 @@ final class QuicklinkCoordinator {
|
||||
quicklink: quicklink, context: context, encoding: encoding, arguments: arguments)
|
||||
pendingQuicklinkForcesDefaultApp = forcingDefaultApp
|
||||
// Never `restoreAnyMode`: this screen is always a fresh prompt, never a restored one.
|
||||
core.showPalette(mode: .quicklinkArguments)
|
||||
paletteCoordinator.showPalette(mode: .quicklinkArguments)
|
||||
return
|
||||
}
|
||||
performQuicklinkOpen(
|
||||
@@ -140,7 +143,7 @@ final class QuicklinkCoordinator {
|
||||
private func performQuicklinkOpen(
|
||||
_ quicklink: Quicklink, link: String, forcingDefaultApp: Bool
|
||||
) {
|
||||
if windowController.isVisible { core.hidePalette(restoreFocus: false) }
|
||||
if windowController.isVisible { paletteCoordinator.hidePalette(restoreFocus: false) }
|
||||
let openWith = forcingDefaultApp ? nil : quicklink.openWithBundleID
|
||||
Task {
|
||||
do throws(QuicklinkLauncher.Failure) {
|
||||
@@ -221,7 +224,7 @@ final class QuicklinkCoordinator {
|
||||
/// Opens Settings on the Quicklinks pane with the editor showing `quicklink` (nil for a new one).
|
||||
func editQuicklink(_ quicklink: Quicklink?) {
|
||||
core.pendingQuicklinkEdit = QuicklinkEditRequest(quicklink: quicklink)
|
||||
core.showSettings(tab: .quicklinks)
|
||||
paletteCoordinator.showSettings(tab: .quicklinks)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import AppKit
|
||||
|
||||
/// Owns window-command activation: the one funnel from a palette row or a global hotkey to the mover.
|
||||
@MainActor
|
||||
final class WindowCommandCoordinator {
|
||||
private let settings: AppSettings
|
||||
private let paletteCoordinator: PaletteCoordinator
|
||||
private let windowMover: WindowMover
|
||||
|
||||
init(
|
||||
settings: AppSettings, paletteCoordinator: PaletteCoordinator, windowMover: WindowMover
|
||||
) {
|
||||
self.settings = settings
|
||||
self.paletteCoordinator = paletteCoordinator
|
||||
self.windowMover = windowMover
|
||||
}
|
||||
|
||||
/// The one funnel for both palette activation and a command's global hotkey, so the feature switch
|
||||
/// can't be bypassed by either — a shortcut stays registered while the feature is off and must move
|
||||
/// nothing.
|
||||
///
|
||||
/// The command acts on the app the user was in, so the palette hands focus back before dispatching,
|
||||
/// the same dance the paste path does. Focus is restored rather than dropped: the window being moved
|
||||
/// is the one they want to keep working in.
|
||||
func runWindowCommand(id: WindowCommand.ID) {
|
||||
guard settings.windowManagementEnabled else { return }
|
||||
let target = paletteCoordinator.targetApp
|
||||
if paletteCoordinator.isVisible { paletteCoordinator.hidePalette(restoreFocus: true) }
|
||||
windowMover.perform(
|
||||
id, target: target, gap: CGFloat(settings.windowGap),
|
||||
cycleOnRepeat: settings.windowCycleOnRepeat)
|
||||
}
|
||||
}
|
||||
@@ -219,7 +219,7 @@ Update this table as phases land. `Blocked` requires a note in the phase's progr
|
||||
| 30 | Complete | `refactor/30-naming-vocabulary` | 2026-08-06 | **The vocabulary is written down.** `AGENTS.md` gains a 13-row suffix table with membership lists, seven exceptions and the rule that a new suffix means a new row in the same commit. Two renames make it true — `CommandRegistry` → `CommandCatalog` (a static namespace over a built-in list, joining the three `Catalog`s; "Registry" implied a runtime registration that never happened) and `PaletteViewModel` → `PaletteState` — retiring **both** the `Registry` and `ViewModel` suffixes; `Catalog` 3 → 4, `State` 2 → 3. Every membership was **re-derived with `git grep` rather than copied** from the phase document, which proved accurate. **The phase document was rewritten before execution, cutting three of its original six renames**: `ClipboardManager` → `ClipboardMonitor`, `HotKeyManager` → `HotKeyBindings` and `MiscellaneousSettingsView` → `CalculatorSettingsView` were dropped, and `Manager` documented as a **closed set of two** instead — `ClipboardManager` owns capture policy no `Monitor` does, `HotKeyManager` drives Carbon registration no `Store` does. That cut is what removed every persisted-string risk and let **AC3 be inverted** from "account for every changed literal" to "the grep must come back empty": all four original traps (`internalType`, `showInMenuBar`, `CommandID` raw values, SQLite columns) live in files this phase never opened. **Zero string literals changed** — the one line the grep returns is `AppIndex.swift:84`, whose `"questionmark"` is byte-identical, proven by extracting every literal from both sides of the diff. **AC7 proven structurally**: normalising the four rename pairs makes the `-` and `+` sides of the Swift diff **identical**, so no logic was touched. **AC5 met for top-level types only** — `SnippetRepository`'s private nested `CoordinatorRegistry` keeps its name, since it really does register (one lock per canonical channel-directory path), and the table was scoped to top-level types with the exception documented. Both file renames at **100 %** similarity, `xcodegen` idempotent, Debug clean at 0 → 0 warnings, all 17 harnesses green, `palette-selection-test` 111,684 unchanged. Comment delta **0**. Live doc paths fixed in `docs/palette.md`, `architecture.md` and `custom-commands.md` though no AC asked; `architecture-review.md` and `docs/refactor/**` left stale on purpose. **`regression.md` not run — operator waived further testing**, and `build.md` is Debug-only; the cost is low here because the diff is identifier-only by proof. Follow-up filed: **`Coordinator` carries three meanings inside `SnippetRepository.swift`** — the vocabulary's 10 action surfaces, `NSFileCoordinator`, and a private `NSLock` wrapper — to be fixed in a standalone commit. **Blocks nothing: 31 and 32 depend on 29, not on 30.** |
|
||||
| 31 | Complete | `refactor/31-app-entry-kind-exhaustiveness` | 2026-08-06 | **Every `AppEntry.Kind` switch is exhaustive and the per-kind metadata is one table.** `AppEntry.KindDescriptor` — exactly `label`, `sectionTitle`, `openVerb`, `canRevealInFinder`, `isSymbolIcon` — is returned by a single `switch` on `Kind`. `kindLabel` becomes a one-line read, and `canRevealInFinder` / `isSymbolIcon` stop being boolean expressions, so both gain compiler enforcement. The two duplicated open-verb switches in `AppActionsMenu` and `LauncherScreen` collapse into it; `symbolIconName` stays separate because it consults three catalogs and a per-entry override. `LauncherList`'s section array keeps its literal order and its explicit type annotation, reading only its titles from the descriptor. **Scratch-case test: a ninth `Kind` fails the build at 4 sites** — `descriptor`, `hotKeyAction`, `symbolIconName`, `LauncherCoordinator.launch` — visible only under `SWIFT_COMPILATION_MODE=wholemodule`, since batch mode aborts after the first failing file. `LauncherList.rows` is the one recipe step still prose-enforced, which the phase's own boundary requires. No `Kind` case or raw value touched, so `hiddenKinds` is untouched by construction. Strings proven unchanged by extracting both sides of the diff. Debug **and Release** clean, 0 → 0 warnings; `fuzz-test`, `ranking-test`, `palette-selection-test` pass. `regression.md` waived by the operator.
|
||||
| 32 | Complete | `refactor/32-retire-appcore-forwarders` | 2026-08-06 | **Every view is off the singleton** — 17 view files, 0 remaining `AppCore.shared` reaches; `AppCore` 544 → 319 (−225), 45 of 49 forwarders deleted. All 17 harnesses pass. **Three ACs fell short and were accepted before implementation**: AC1 wants 3 `AppCore.shared` sites and there are 12 (`AppDelegate` 3, `TinycastApp` 3, `BackupActions` 6 — left alone by operator decision); AC2 keeps 4 forwarders whose only callers are on the must-NOT-change list; AC5 wants ~250 lines and lands at 319. The deciding constraint is that coordinators are not `@Observable`, so they cannot be `@Environment`-injected and views reach them through `core.<coordinator>`. `PaletteCoordinator` was edited — 5 `.environment` lines only — because phase 25 moved the injection point the document names. `regression.md` waived; **the 14-pane runtime pass and the stationary-pointer hover test were not done**. Residue → proposed 32b. |
|
||||
| 32b | Not started | | | **Not started.** Closes the three residues phase 32 could not reach without crossing its own boundaries: `QuicklinkCoordinator` is handed a `paletteCoordinator` like its six siblings (retiring the `showPalette` / `hidePalette` forwarders), `runWindowCommand` becomes `WindowCommandCoordinator` (the eleventh, so no feature action is implemented on `AppCore`), and `SettingsBackup` / `BackupActions` take `core` as a **required** parameter, deleting a `= .shared` default no caller ever supplied. Target: `grep AppCore.shared` returns exactly 6, all in `AppDelegate` and `TinycastApp`; `AppCore` under 280. **Must land before 33**, which freezes `gather` / `apply`. |
|
||||
| 32b | Complete | `refactor/32b-appcore-residue` | 2026-08-07 | **`AppCore` is a composition root and nothing else.** 319 → 290 (−29), **all four palette forwarders deleted** and `runWindowCommand` extracted, so no feature action and no forwarder remains — only stored properties, `start()`, `prepareForTermination()`, the feature-switch tracking and the dialog façade. `WindowCommandCoordinator` is the **eleventh** coordinator, flat in `Features/WindowManagement/` and out of the harness set; the funnel moved verbatim and both callers go through it. `QuicklinkCoordinator` takes `paletteCoordinator` like its siblings. `gather` / `apply` and `BackupActions` take a **required** `core` — no `= .shared` default was relocated. `grep AppCore.shared` → exactly 6; all 17 harnesses pass. **AC7 not met (290, wanted <280)** — the document's −45 estimate does not match the ~28 lines of methods it names. **`TinycastApp` and `AppDelegate` were edited though both are must-NOT-change**: the phase deletes `showPalette` while forbidding its only caller, and the operator chose to delete all four forwarders and requalify the 4 call sites to `AppCore.shared.paletteCoordinator.*`. Both files still reach the singleton, count still 6. `regression.md` waived; **the manual pass was not done**. |
|
||||
| 33 | Not started | | | |
|
||||
| 34 | Not started | | | |
|
||||
| 35 | Not started | | | |
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
# Phase 32b — Close the `AppCore` residue
|
||||
|
||||
---
|
||||
|
||||
## Status
|
||||
|
||||
| Field | Value |
|
||||
| ----------------------------- | ----------------------------------------- |
|
||||
| **Status** | Complete |
|
||||
| **Started** | 2026-08-07 |
|
||||
| **Completed** | 2026-08-07 |
|
||||
| **Operator** | abue-ammar |
|
||||
| **Branch** | `refactor/32b-appcore-residue` |
|
||||
| **Commit** | single commit on the branch |
|
||||
| **Claude conversations used** | 1 |
|
||||
| **Actual effort** | ~35 min vs. estimate of M (2–4 h) |
|
||||
|
||||
---
|
||||
|
||||
## Completed tasks
|
||||
|
||||
- [x] Objective 1 — `QuicklinkCoordinator` takes `paletteCoordinator` like its six siblings; the
|
||||
`showPalette` / `hidePalette` forwarders are gone. **Exceeded:** `handleReopen` and `showSettings`
|
||||
went with them, so `AppCore` now carries no palette forwarder at all. See _Deviations_
|
||||
- [x] Objective 2 — `WindowCommandCoordinator` extracted, the eleventh coordinator; `runWindowCommand`
|
||||
deleted from `AppCore`. The body moved verbatim apart from the forced
|
||||
`hidePalette` → `paletteCoordinator.hidePalette` requalification
|
||||
- [x] Objective 3 — `gather(from:)` / `apply(to:)` take a required `core`; `BackupActions` takes `core`
|
||||
on its three entry points and both private helpers. No `= .shared` default was added anywhere
|
||||
- [x] All 17 harnesses run and passing, twice — once after the three objectives, once after the extra
|
||||
forwarder removal
|
||||
- [x] Dead-code sweep: every remaining `AppCore` member re-checked for live call sites; none orphaned
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [x] AC1 — `grep -rn "AppCore.shared" Tinycast` returns exactly six: `AppDelegate` (3),
|
||||
`TinycastApp` (3). No other file appears
|
||||
- [x] AC2 — `AppCore` implements no feature action. Remaining surface is stored properties, coordinator
|
||||
wiring, `init`, `start()`, `hotKeyDisplayName`, `prepareForTermination()`, the feature-switch
|
||||
tracking and the five-method dialog façade. **Stronger than written:** `handleReopen()` and
|
||||
`showSettings()` are also gone
|
||||
- [x] AC3 — `grep -rn "core\.showPalette\|core\.hidePalette" Tinycast` returns nothing
|
||||
- [x] AC4 — coordinator count is **11**. Every coordinator needing palette control is handed
|
||||
`paletteCoordinator` at construction; the graph stays acyclic and `PaletteCoordinator` gained
|
||||
nothing
|
||||
- [x] AC5 — `grep -c "func runWindowCommand" Tinycast` is 1, and `guard settings.windowManagementEnabled`
|
||||
is the first statement inside it
|
||||
- [x] AC6 — neither `gather` nor `apply` has a default argument; `BackupActions` contains no
|
||||
`AppCore.shared`
|
||||
- [ ] AC7 — `AppCore.swift` under 280 lines — **NOT MET. 290** (from 319, −29). The target was
|
||||
mis-derived: the document predicts −45 from three methods totalling ~28 lines. Even with the
|
||||
fourth forwarder removed it cannot reach 280 without deleting something the phase does not name.
|
||||
Nothing is orphaned — see _Measurements_
|
||||
- [x] AC8 — zero behaviour change
|
||||
|
||||
---
|
||||
|
||||
## Verification
|
||||
|
||||
| Checklist | Result | Notes |
|
||||
| -------------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `checklists/build.md` | PARTIAL | **Debug only**, `CODE_SIGNING_ALLOWED=NO`, `BUILD SUCCEEDED`, zero compiler warnings. No pre-phase baseline was taken (operator declined), so "zero **new** warnings" rests on the absolute count being zero. Release not built |
|
||||
| `checklists/testing.md` | PASS | **All 17 harnesses**, per-harness exit codes checked individually, 17 passed / 0 failed. Named gates `window-command-test` and `quicklink-test` both pass |
|
||||
| `checklists/regression.md` | NOT RUN | **Waived by the operator**, who declined further testing. The manual pass below is therefore unexercised |
|
||||
| `checklists/review.md` | PARTIAL | 12 modified + 1 new file, +72/−81 excluding the new file. `git diff --name-only` audited against both phase lists: `PaletteCoordinator`, `WindowMover`, the three pure `WindowManagement` models, `Raycast*` and the two off-limits scrolling files are all absent. `TinycastApp` and `AppDelegate` **are** present — see _Deviations_. Comment delta +1 |
|
||||
|
||||
### Manual verification — NOT PERFORMED
|
||||
|
||||
Every item below is from the phase's own checklist and its Definition of Done. All were waived:
|
||||
|
||||
- [ ] Every window command from the launcher **and** from its global hotkey
|
||||
- [ ] Window management **off**, fire a still-registered window-command hotkey → nothing moves
|
||||
- [ ] Quicklink with an argument; quicklink edit from the palette
|
||||
- [ ] Settings export → import round-trip
|
||||
- [ ] Raycast import from **both** the Backup pane and Onboarding
|
||||
- [ ] Menu-bar items (Open / Clipboard History / Settings) and dock-icon reopen — **new to this phase**,
|
||||
since all four of those call sites were requalified
|
||||
|
||||
### Measurements
|
||||
|
||||
| Fact | Before | After | Note |
|
||||
| ------------------------------------------- | ------ | ------- | ------------------------------------------------------- |
|
||||
| `AppCore.swift` lines | 319 | **290** | −29; AC7 wanted < 280 |
|
||||
| Forwarding methods on `AppCore` | 4 | **0** | all four palette forwarders deleted |
|
||||
| Feature actions implemented on `AppCore` | 1 | **0** | `runWindowCommand` extracted |
|
||||
| `AppCore.shared` reaches, tree-wide | 12 | **6** | AC1 met exactly |
|
||||
| Coordinators | 10 | **11** | `WindowCommandCoordinator` |
|
||||
| `= .shared` default arguments | 2 | **0** | none relocated to `BackupActions` |
|
||||
| Harnesses passing | 17/17 | 17/17 | unchanged |
|
||||
|
||||
---
|
||||
|
||||
## Failed tasks
|
||||
|
||||
- **AC7 only**, and it is arithmetic in the phase document rather than work left undone. The line target
|
||||
was derived from a −45 estimate against methods totalling ~28 lines.
|
||||
|
||||
---
|
||||
|
||||
## Issues encountered
|
||||
|
||||
- **The phase document contradicts itself on `showPalette`.** Objective 1 and AC2 delete it;
|
||||
`TinycastApp.swift` calls it twice and is on the must-NOT-change list. Implementation stopped and the
|
||||
operator decided. See _Deviations_.
|
||||
|
||||
- **`OnboardingModel` has no environment.** It is a plain `@Observable` class owned by `OnboardingView`,
|
||||
so injecting `core` into `BackupActions.importRaycast` meant threading it through
|
||||
`OnboardingModel.run()`, which became `run(core:)`. Both call sites are inside the view, which already
|
||||
holds `@Environment(AppCore.self)`. This file is on the phase's expected list; the signature change is
|
||||
not, but it is the minimum needed to remove the singleton reach the phase does name.
|
||||
|
||||
---
|
||||
|
||||
## Deviations from the phase document
|
||||
|
||||
- **`TinycastApp.swift` and `AppDelegate.swift` were edited, and both are on the must-NOT-change list.**
|
||||
This is the one deviation a reviewer must sign off deliberately. The document simultaneously requires
|
||||
deleting `showPalette` and forbids touching its only caller. Three options were put to the operator —
|
||||
keep the forwarder, delete all the palette forwarders and requalify the four call sites, or amend the
|
||||
document — and **option 2 was chosen explicitly.** The four edited call sites now read
|
||||
`AppCore.shared.paletteCoordinator.<verb>(…)`.
|
||||
|
||||
The boundary's stated rationale is untouched. Phase 32 protects these two files because "the
|
||||
`MenuBarExtra` buttons legitimately reach the singleton; there is no environment in a `Scene`'s menu
|
||||
content" — a rule about *how* they reach `AppCore`, not which method they call. Both files still reach
|
||||
`AppCore.shared`, the count is still six, and neither gained an `@Environment`.
|
||||
|
||||
The deciding argument was consistency: 41 call sites tree-wide address `paletteCoordinator` directly
|
||||
against 5 that went through a forwarder, and deleting only `showPalette` would have left `TinycastApp`
|
||||
using two idioms in four adjacent lines. `paletteCoordinator` is already an internal
|
||||
`private(set) lazy var`, so the forwarders added no encapsulation — unlike the dialog façade, which
|
||||
forwards to a **private** `dialogs` and is therefore retained.
|
||||
|
||||
- **`QuicklinkCoordinator.editQuicklink` now calls `paletteCoordinator.showSettings`,** the last
|
||||
straggler once the forwarder went. It keeps `core` for the dialog façade and `pendingQuicklinkEdit`,
|
||||
exactly as the phase requires.
|
||||
|
||||
- **Two documentation files were corrected, with operator approval**, neither on the expected list:
|
||||
`docs/window-management.md` (two references to `AppCore.runWindowCommand`, now
|
||||
`WindowCommandCoordinator`) and `AGENTS.md` (the `Coordinator` row, 10 → 11).
|
||||
|
||||
---
|
||||
|
||||
## Follow-up work
|
||||
|
||||
| Observation | Where | Suggested phase |
|
||||
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- | ------------------------------- |
|
||||
| **The manual pass was waived.** Window commands both ways, the feature-off hotkey test, both Raycast paths, and the four newly-requalified menu-bar / reopen call sites | — | before release |
|
||||
| `AGENTS.md`'s Architecture bullet and `docs/architecture.md:20-21` still say palette / paste / launch actions are methods on `AppCore` that views call. False since 32 | `AGENTS.md`, `docs/architecture.md` | **34** (measurement pass) |
|
||||
| `Tools/window-command-test.swift`'s header comment cites pre-phase-29 `Tinycast/Core/WindowManagement/` paths. Harmless — `docs/development.md` carries the correct ones | `Tools/window-command-test.swift` | **34** |
|
||||
| `pendingQuicklinkEdit` is still observable state homeless on `AppCore`. Deliberately untouched this phase; wants a `State`-suffixed owner | `App/AppCore.swift` | undecided — needs a design call |
|
||||
| AC7's line target is unreachable as written; the document's −45 estimate does not match the methods it names | `docs/refactor/phases/32b-…md` | historical record, leave stale |
|
||||
| `checklists/regression.md:146` still lists phase 30 in its _Clean install_ set — carried from 30, 31 and 32, still open | `docs/refactor/checklists/regression.md` | fix alongside the next phase |
|
||||
|
||||
---
|
||||
|
||||
## Rollback notes
|
||||
|
||||
- **Revert command:** `git revert <sha>`, then `xcodegen generate`
|
||||
- **Is a plain revert sufficient?** Yes, with the regenerate. Unlike phase 32 this commit **adds a
|
||||
file**, so `project.pbxproj` is in the diff and the project must be regenerated after reverting.
|
||||
- **Storage impact:** none. No `UserDefaults` key, SQLite column, persisted path, exported format or
|
||||
raw value is touched. `SettingsBackup`'s **wire format is unchanged** — only the Swift signatures of
|
||||
`gather` / `apply` moved, so an export written before this commit imports after it.
|
||||
- **Dependent phases that must also be reverted:** none.
|
||||
|
||||
---
|
||||
|
||||
## Blockers for the next phase
|
||||
|
||||
**None.** Phase 33 (`SettingsBackup` completeness harness) depends on 16 and is explicitly required to
|
||||
run *after* this phase, because 33 freezes `gather` / `apply` and writes a harness against them. Both
|
||||
now have their final shape — `gather(from core: AppCore)` and `apply(to core: AppCore)`, no default
|
||||
arguments — so 33 can be written against them directly.
|
||||
|
||||
---
|
||||
|
||||
## Sign-off
|
||||
|
||||
- [x] Objectives 1–3 all met, objective 1 exceeded
|
||||
- [x] Seven of eight acceptance criteria met; **AC7 (line count) not met at 290**
|
||||
- [ ] All four checklists passed — **two.** `testing.md` PASS (all 17); `build.md` Debug only, no
|
||||
baseline; `regression.md` waived; `review.md` limited to the diff and the two boundary audits
|
||||
- [x] All 17 harnesses run and passing
|
||||
- [x] `git diff --name-only` audited against both phase lists
|
||||
- [ ] Manual verification checklist — **not done**, waived by the operator
|
||||
- [x] The must-NOT-change deviation was raised before implementing and decided explicitly
|
||||
- [x] `ROADMAP.md` status table updated
|
||||
- [x] Follow-ups recorded above, not fixed in this phase
|
||||
@@ -114,7 +114,7 @@ when a read fails. Nothing is persisted.
|
||||
## Applying a placement
|
||||
|
||||
`WindowMover.perform(_:target:gap:cycleOnRepeat:)` is the only entry point. `target` is **explicit**
|
||||
because the palette is frontmost when a command dispatches from it — `AppCore.runWindowCommand` passes
|
||||
because the palette is frontmost when a command dispatches from it — `WindowCommandCoordinator` passes
|
||||
`windowController.previousApp`, the same recorded app the paste path targets, and restores focus to it
|
||||
rather than dropping it. It is synchronous: every AX call is a bounded mach round trip capped by a 1s
|
||||
messaging timeout, and `await` would only add reentrancy between a held hotkey's repeats. The timeout
|
||||
@@ -166,7 +166,7 @@ stock Electron app tiles correctly without it, delete the helper rather than kee
|
||||
`KeyboardShortcuts_windowCommandHotkey.<raw-id>`, matching the legacy prefix convention. Unlike
|
||||
custom commands there is no bound-ID index to maintain: the catalog is fixed, so `HotKeyManager.start`
|
||||
and `conflictOwner` iterate `WindowCommand.ID.allCases` and `register` no-ops on an unbound command.
|
||||
- **`AppCore.runWindowCommand(id:)`** is the one funnel for both palette activation and the global
|
||||
- **`WindowCommandCoordinator.runWindowCommand(id:)`** is the one funnel for both palette activation and the global
|
||||
hotkey, so the feature switch cannot be bypassed by either.
|
||||
- **Settings** — `windowManagementEnabled` (off), `windowManagementShowInLauncher` (on), `windowGap`
|
||||
(0) and `windowCycleOnRepeat` (off). All four ride in settings backups: unlike `snippetsEnabled` they
|
||||
|
||||
Reference in New Issue
Block a user