mirror of
https://github.com/abue-ammar/tinycast.git
synced 2026-10-03 00:28:16 +08:00
Retire the compatibility machinery, behind scheduled migrations (#198)
Phase 35 of docs/refactor/. The legacy KeyboardShortcuts_ key namespace and HotKeyBinding's hand-written Codable seam are gone: bindings persist under hotkey.<action> with the synthesised conformance, and HotKeyAction.defaultsKey stays the one place a key is computed. POLICY.md assumed there are no existing users. v0.7.5 is a shipped stable release, and both of the phase's deletions would have destroyed real user data on update: every hotkey silently unbound, and the entire clipboard history discarded, since v0.7.5 predates pinned_at and the store deletes a database it cannot open. Each cleanup therefore ships behind a one-time migration instead of a plain deletion. - LegacyHotKeyRecords adopts the shipped records once, reading both old shapes and never overwriting a key the user has already rebound. - ClipboardStore's two ALTER TABLE guards stay exactly as they are. Both are recorded in POLICY.md as scheduled deletions, to come out two stable releases after this one ships. Neither persists a flag, so each removal is a pure deletion. Net line count is positive as a result, which the phase document forbids; that is the deliberate cost of the migrations. AGENTS.md's refactor banner is removed and its hotkey clause amended. Raycast import and the snippet Markdown serializer are untouched: another application's format, not our legacy.
This commit is contained in:
@@ -1,21 +1,3 @@
|
||||
> ## ⚠ An approved architecture refactor is in progress
|
||||
>
|
||||
> Some **structural** rules in this file — file layout, type ownership, which type does what — are being
|
||||
> changed by it. That is deliberate, and a phase contradicting one of them is **not** an error.
|
||||
>
|
||||
> **If you are executing a phase from [`docs/refactor/`](docs/refactor/), the phase document overrides
|
||||
> the architectural guidance here.** Read
|
||||
> [`docs/refactor/prompts/system-prompt.md`](docs/refactor/prompts/system-prompt.md) for the full
|
||||
> precedence ladder, and [`docs/refactor/POLICY.md`](docs/refactor/POLICY.md) for the migration and
|
||||
> compatibility rules.
|
||||
>
|
||||
> **Behavioral invariants below always hold** — UI, keyboard behaviour, accessibility, permission and
|
||||
> consent flows, Swift 6 data-race safety, and the explicitly off-limits files. A phase that contradicts
|
||||
> one of *those* is wrong: stop and say so.
|
||||
>
|
||||
> **If you are not working from a `docs/refactor/` phase**, ignore this box entirely and follow this
|
||||
> file as written.
|
||||
|
||||
## Project
|
||||
|
||||
Tinycast is a native macOS menu-bar launcher (a minimal Raycast): fuzzy app launcher, global +
|
||||
@@ -220,13 +202,16 @@ Never break these without an explicit task to do so.
|
||||
block observers, `isolated deinit` for `ClipboardStore`'s SQLite teardown, decode raw Carbon / C
|
||||
pointers to plain values before crossing into actor code.
|
||||
- **Clipboard writes stamp a private `internalType` marker** so the poller skips Tinycast's own writes.
|
||||
- **Hotkeys persist under legacy `KeyboardShortcuts_<name>` UserDefaults keys** (from the removed
|
||||
KeyboardShortcuts package) so old bindings survive. `HotKeyBinding` is the one thing an action is
|
||||
- **Hotkeys persist as JSON strings under `hotkey.<action>` UserDefaults keys**, and
|
||||
`HotKeyAction.defaultsKey` is the one place that computes a key — it is also the `HotKeyCenter`
|
||||
registration id, so the two can never drift. `HotKeyBinding` is the one thing an action is
|
||||
bound to and it has two cases with two engines: a `.combo` is a Carbon registration, a `.doubleTap`
|
||||
is recognized by `DoubleTapMonitor` (Carbon cannot see a lone modifier at all). Its `Codable`
|
||||
conformance is the compatibility seam — a `.combo` must keep encoding as the bare
|
||||
`{"carbonKeyCode":N,"carbonModifiers":N}` record and decoding must keep trying that shape first, or
|
||||
every existing binding and backup breaks. `HotKeys/Model/DoubleTapModifier.swift` and
|
||||
is recognized by `DoubleTapMonitor` (Carbon cannot see a lone modifier at all). Its `Codable` is the
|
||||
synthesised one; `KeyShortcut`'s hand-written `init(from:)` is not a format seam but a correctness
|
||||
one, routing every decode through the initializer that masks device modifier bits off.
|
||||
`LegacyHotKeyRecords` adopts the shipped `KeyboardShortcuts_` records once and is **scheduled for
|
||||
deletion** — see [POLICY.md](docs/refactor/POLICY.md); nothing new may depend on it.
|
||||
`HotKeys/Model/DoubleTapModifier.swift` and
|
||||
`DoubleTapDetector.swift` stay Foundation-only and pure with the clock injected as a parameter, for
|
||||
`Tools/hotkey-test.swift`; every `CGEvent` call lives in `DoubleTapMonitor.swift`, which is
|
||||
listen-only, installs *only* while something is bound to a double-tap, and never prompts for
|
||||
|
||||
@@ -181,6 +181,7 @@
|
||||
E4B18EBFFD009B2CAFF4BA9B /* PanelTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73E25B87C692D5FEE1FBF994 /* PanelTransition.swift */; };
|
||||
E58AB83C608F8BBF99EAEB7E /* QuicklinkListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 856F65245B10433A230194E6 /* QuicklinkListView.swift */; };
|
||||
E63AA3E7A400CB81FBFC3214 /* RaycastFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = A09A0608AC763EBC26216118 /* RaycastFormat.swift */; };
|
||||
E7DCBF503DDEF223C6BE71E5 /* LegacyHotKeyRecords.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F375AF988B0DA844E2473DA /* LegacyHotKeyRecords.swift */; };
|
||||
E8CB7624CCA5905AF1A4FAD9 /* AppCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BECF24B674F9458E4A876DA /* AppCore.swift */; };
|
||||
EA2587800F2CA347788C6F8C /* UninstallScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0508EBDFD6E59370AB2E5B7 /* UninstallScreen.swift */; };
|
||||
EA89498DA03BDFA3858144BF /* AppActionsMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C045C66BB654E4CB407A673 /* AppActionsMenu.swift */; };
|
||||
@@ -276,6 +277,7 @@
|
||||
5D3044106BC42824C79E1803 /* QuicklinkArgumentsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuicklinkArgumentsView.swift; sourceTree = "<group>"; };
|
||||
5DADE9F769787D44F72A2071 /* ShortcutRecorderPopover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutRecorderPopover.swift; sourceTree = "<group>"; };
|
||||
5F0A68B2040582996EFBA877 /* CommandCatalog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandCatalog.swift; sourceTree = "<group>"; };
|
||||
5F375AF988B0DA844E2473DA /* LegacyHotKeyRecords.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyHotKeyRecords.swift; sourceTree = "<group>"; };
|
||||
60CCB1A47E0022BAEDD1EE96 /* CalculatorHistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalculatorHistoryView.swift; sourceTree = "<group>"; };
|
||||
61FFFE3901140714D3A7BB4C /* QuicklinkDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuicklinkDestination.swift; sourceTree = "<group>"; };
|
||||
622E1F1C01B954DCA2199BCD /* ScrollIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollIntent.swift; sourceTree = "<group>"; };
|
||||
@@ -887,6 +889,7 @@
|
||||
F0311153A4A3557C6FFEAAAB /* HotKeyManager.swift */,
|
||||
1868F87E31E20A4775C84E6A /* HyperKeyTap.swift */,
|
||||
3E16AEF81320A101E384098E /* KeyShortcut.swift */,
|
||||
5F375AF988B0DA844E2473DA /* LegacyHotKeyRecords.swift */,
|
||||
B235DFF451FF4D52A8E44D00 /* ShortcutCaptureSession.swift */,
|
||||
);
|
||||
path = Service;
|
||||
@@ -1313,6 +1316,7 @@
|
||||
D1F9C28CA4048480DB818EE9 /* LauncherList.swift in Sources */,
|
||||
633DCA2F00A274C375EB4423 /* LauncherRankingStore.swift in Sources */,
|
||||
7FF6A0A4702E1C1365F2D76A /* LauncherScreen.swift in Sources */,
|
||||
E7DCBF503DDEF223C6BE71E5 /* LegacyHotKeyRecords.swift in Sources */,
|
||||
AAFD2C9276D38719E85B675F /* Memo.swift in Sources */,
|
||||
43DBCE7B5A84D0D6322124F5 /* MessageHUDController.swift in Sources */,
|
||||
8D529C8F3AB579941067FE98 /* MessageHUDView.swift in Sources */,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Foundation
|
||||
|
||||
/// What an action is bound to: two kinds, two engines. See docs/hotkeys.md.
|
||||
enum HotKeyBinding: Hashable, Sendable {
|
||||
enum HotKeyBinding: Hashable, Sendable, Codable {
|
||||
case combo(KeyShortcut)
|
||||
case doubleTap(DoubleTapModifier)
|
||||
|
||||
@@ -23,29 +23,3 @@ enum HotKeyBinding: Hashable, Sendable {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// The compatibility seam for both shapes. See docs/hotkeys.md#persistence.
|
||||
extension HotKeyBinding: Codable {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case doubleTapModifier
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
if let shortcut = try? KeyShortcut(from: decoder) {
|
||||
self = .combo(shortcut)
|
||||
return
|
||||
}
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
self = .doubleTap(try container.decode(DoubleTapModifier.self, forKey: .doubleTapModifier))
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
switch self {
|
||||
case .combo(let shortcut):
|
||||
try shortcut.encode(to: encoder)
|
||||
case .doubleTap(let modifier):
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(modifier, forKey: .doubleTapModifier)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ final class HotKeyManager {
|
||||
private let boundQuicklinkKey = "boundQuicklinkIDs"
|
||||
|
||||
func start(customCommandIDs: Set<UUID>, quicklinkIDs: Set<UUID>) {
|
||||
LegacyHotKeyRecords.adopt(candidateActions, decoder: decoder, encoder: encoder)
|
||||
prune(key: boundCustomCommandKey, live: customCommandIDs) { .customCommand(id: $0) }
|
||||
prune(key: boundQuicklinkKey, live: quicklinkIDs) { .quicklink(id: $0) }
|
||||
// After the prunes, so a dropped record can't survive in memory this session.
|
||||
|
||||
@@ -170,20 +170,18 @@ enum HotKeyAction: Hashable, Sendable {
|
||||
case windowCommand(id: WindowCommand.ID)
|
||||
case quicklink(id: UUID)
|
||||
|
||||
/// The defaults key; the `KeyboardShortcuts_` prefix is a fossil, kept verbatim.
|
||||
/// The UserDefaults key, and the `HotKeyCenter` registration id: one per action.
|
||||
var defaultsKey: String {
|
||||
switch self {
|
||||
case .togglePalette: "KeyboardShortcuts_togglePalette"
|
||||
case .toggleClipboard: "KeyboardShortcuts_toggleClipboard"
|
||||
case .toggleEmoji: "KeyboardShortcuts_toggleEmoji"
|
||||
case .app(let bundleID): "KeyboardShortcuts_appHotkey." + bundleID
|
||||
case .settingsPane(let bundleID): "KeyboardShortcuts_paneHotkey." + bundleID
|
||||
case .customCommand(let id):
|
||||
"KeyboardShortcuts_customCommandHotkey." + id.uuidString.lowercased()
|
||||
case .systemAction(let id): "KeyboardShortcuts_systemActionHotkey." + id.rawValue
|
||||
case .windowCommand(let id): "KeyboardShortcuts_windowCommandHotkey." + id.rawValue
|
||||
case .quicklink(let id):
|
||||
"KeyboardShortcuts_quicklinkHotkey." + id.uuidString.lowercased()
|
||||
case .togglePalette: "hotkey.togglePalette"
|
||||
case .toggleClipboard: "hotkey.toggleClipboard"
|
||||
case .toggleEmoji: "hotkey.toggleEmoji"
|
||||
case .app(let bundleID): "hotkey.app." + bundleID
|
||||
case .settingsPane(let bundleID): "hotkey.pane." + bundleID
|
||||
case .customCommand(let id): "hotkey.customCommand." + id.uuidString.lowercased()
|
||||
case .systemAction(let id): "hotkey.systemAction." + id.rawValue
|
||||
case .windowCommand(let id): "hotkey.windowCommand." + id.rawValue
|
||||
case .quicklink(let id): "hotkey.quicklink." + id.uuidString.lowercased()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import Foundation
|
||||
|
||||
/// Temporary: adopts the pre-`hotkey.<action>` records. Delete per docs/refactor/POLICY.md.
|
||||
enum LegacyHotKeyRecords {
|
||||
private struct Combo: Decodable {
|
||||
let carbonKeyCode: Int
|
||||
let carbonModifiers: Int
|
||||
}
|
||||
|
||||
private struct DoubleTap: Decodable {
|
||||
let doubleTapModifier: DoubleTapModifier
|
||||
}
|
||||
|
||||
/// Consumes each old record, so the second launch finds nothing and this becomes a no-op.
|
||||
static func adopt(_ actions: [HotKeyAction], decoder: JSONDecoder, encoder: JSONEncoder) {
|
||||
let defaults = UserDefaults.standard
|
||||
for action in actions {
|
||||
let legacy = legacyKey(for: action)
|
||||
guard let json = defaults.string(forKey: legacy), let data = json.data(using: .utf8)
|
||||
else { continue }
|
||||
defaults.removeObject(forKey: legacy)
|
||||
// Never clobber a binding already set under the new key: that one is the user's intent.
|
||||
guard defaults.string(forKey: action.defaultsKey) == nil,
|
||||
let binding = binding(from: data, decoder),
|
||||
let encoded = try? encoder.encode(binding),
|
||||
let rewritten = String(data: encoded, encoding: .utf8)
|
||||
else { continue }
|
||||
defaults.set(rewritten, forKey: action.defaultsKey)
|
||||
}
|
||||
}
|
||||
|
||||
/// The two shapes `HotKeyBinding` used to write, flat combo first as that is what 0.7.5 stored.
|
||||
private static func binding(from data: Data, _ decoder: JSONDecoder) -> HotKeyBinding? {
|
||||
if let combo = try? decoder.decode(Combo.self, from: data) {
|
||||
return .combo(
|
||||
KeyShortcut(
|
||||
carbonKeyCode: combo.carbonKeyCode, carbonModifiers: combo.carbonModifiers))
|
||||
}
|
||||
if let tap = try? decoder.decode(DoubleTap.self, from: data) {
|
||||
return .doubleTap(tap.doubleTapModifier)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private static func legacyKey(for action: HotKeyAction) -> String {
|
||||
switch action {
|
||||
case .togglePalette: "KeyboardShortcuts_togglePalette"
|
||||
case .toggleClipboard: "KeyboardShortcuts_toggleClipboard"
|
||||
case .toggleEmoji: "KeyboardShortcuts_toggleEmoji"
|
||||
case .app(let bundleID): "KeyboardShortcuts_appHotkey." + bundleID
|
||||
case .settingsPane(let bundleID): "KeyboardShortcuts_paneHotkey." + bundleID
|
||||
case .customCommand(let id):
|
||||
"KeyboardShortcuts_customCommandHotkey." + id.uuidString.lowercased()
|
||||
case .systemAction(let id): "KeyboardShortcuts_systemActionHotkey." + id.rawValue
|
||||
case .windowCommand(let id): "KeyboardShortcuts_windowCommandHotkey." + id.rawValue
|
||||
case .quicklink(let id):
|
||||
"KeyboardShortcuts_quicklinkHotkey." + id.uuidString.lowercased()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ without re-registering. "Show in launcher" only hides the section; shortcuts kee
|
||||
`CustomCommandStore` is owned by `AppCore` and persists the ordered command array as JSON in
|
||||
bundle-scoped `UserDefaults`. Each command has a stable UUID. Its launcher entry id is
|
||||
`custom-command:<uuid>`, and its hotkey uses
|
||||
`KeyboardShortcuts_customCommandHotkey.<uuid>` plus the `boundCustomCommandIDs` index.
|
||||
`hotkey.customCommand.<uuid>` plus the `boundCustomCommandIDs` index.
|
||||
|
||||
Editing preserves the UUID and therefore its favorite, visibility, and hotkey references. Deleting
|
||||
goes through `AppCore`, which unregisters the hotkey and clears those references before removing the
|
||||
|
||||
+14
-9
@@ -14,8 +14,8 @@ the keycap rendering — only the _engine_ differs.
|
||||
|
||||
## Persistence
|
||||
|
||||
Bindings persist as JSON strings under `KeyboardShortcuts_<name>` UserDefaults keys — a **legacy
|
||||
format** from the removed KeyboardShortcuts package, kept so old bindings survive. The set of bound
|
||||
Bindings persist as JSON strings under `hotkey.<action>` UserDefaults keys, computed in one place —
|
||||
`HotKeyAction.defaultsKey`, which doubles as the `HotKeyCenter` registration id. The set of bound
|
||||
bundle IDs lives in `boundAppBundleIDs` and is re-registered on launch. System Settings panes use
|
||||
`boundPaneBundleIDs`; custom commands and quicklinks use their stable UUIDs in
|
||||
`boundCustomCommandIDs` and `boundQuicklinkIDs`. Those two are the per-item case — unlike a fixed
|
||||
@@ -24,15 +24,20 @@ and to prune bindings whose record was deleted while Tinycast wasn't running. Th
|
||||
`QuicklinkStore` loads at launch even when the feature is off
|
||||
(see [quicklinks.md](quicklinks.md#hotkeys)).
|
||||
|
||||
A `.combo` writes the original `{"carbonKeyCode":N,"carbonModifiers":N}` record and
|
||||
`HotKeyBinding.init(from:)` tries that shape first, so nothing needs migrating. A `.doubleTap` writes
|
||||
`{"doubleTapModifier":"command"}` — a shape a pre-double-tap build fails to decode and therefore reads
|
||||
as _unbound_, which is the intended degradation. The same wrapper is what `SettingsBackup.HotkeyBackup`
|
||||
stores, so old backup files import unchanged; a backup containing a double-tap cannot be read by an
|
||||
older build, which is what the `version` 3 bump records (`version` 4 adds the quicklinks array).
|
||||
`HotKeyBinding` takes the synthesised `Codable`, so a `.combo` writes
|
||||
`{"combo":{"_0":{"carbonKeyCode":N,"carbonModifiers":N}}}` and a `.doubleTap` writes
|
||||
`{"doubleTap":{"_0":"command"}}`. `KeyShortcut` keeps a hand-written `init(from:)` — not a format seam,
|
||||
but the guarantee that every decode runs through the initializer that masks device modifier bits off.
|
||||
`SettingsBackup.HotkeyBackup` stores the same values, so the backup file carries this shape too; only
|
||||
export → import within one build is guaranteed to round-trip.
|
||||
|
||||
`LegacyHotKeyRecords` adopts the records shipped before this namespace existed — `v0.7.5` wrote a bare
|
||||
`{"carbonKeyCode":N,"carbonModifiers":N}` under `KeyboardShortcuts_<name>`. It runs once from
|
||||
`start()`, consumes each old record, and never overwrites a key the user has already rebound. It is
|
||||
scheduled for deletion; see [refactor/POLICY.md](refactor/POLICY.md).
|
||||
|
||||
System actions and window commands are the fixed-catalog case: they persist under
|
||||
`KeyboardShortcuts_systemActionHotkey.<raw-id>` and `KeyboardShortcuts_windowCommandHotkey.<raw-id>`
|
||||
`hotkey.systemAction.<raw-id>` and `hotkey.windowCommand.<raw-id>`
|
||||
and need **no** bound-ID index, because `start()` and `conflictOwner` can just iterate `allCases` and
|
||||
`register` no-ops on an unbound item. A registered window-command shortcut still runs nothing while the
|
||||
feature switch is off — `AppCore.runWindowCommand` re-checks it (see
|
||||
|
||||
+1
-1
@@ -145,7 +145,7 @@ Duplicating takes a **new** identity, so the copy can't inherit the original's s
|
||||
|
||||
## Hotkeys
|
||||
|
||||
`HotKeyAction.quicklink(id:)` persists under `KeyboardShortcuts_quicklinkHotkey.<uuid>` with a
|
||||
`HotKeyAction.quicklink(id:)` persists under `hotkey.quicklink.<uuid>` with a
|
||||
`boundQuicklinkIDs` index, the same shape custom commands use — both are per-item rather than
|
||||
per-catalog-entry, so both need an index for `start()` to re-register from. The store therefore loads
|
||||
**even while the feature is off** and before `hotKeys.start`: the stale-binding prune reads that
|
||||
|
||||
+27
-11
@@ -147,29 +147,45 @@ settings take their intended defaults, nothing crashes on an absent key or an ab
|
||||
## Conflicts with `AGENTS.md`
|
||||
|
||||
`AGENTS.md` is loaded into every Claude Code session **before** anything the operator pastes, and it
|
||||
describes the codebase as it is today. Conflicts with this policy are therefore guaranteed, not
|
||||
accidental. Three are known and listed here so they are recorded rather than discovered mid-phase:
|
||||
describes the codebase as it is today. Conflicts with this policy were therefore guaranteed, not
|
||||
accidental. Three were known, and phase 35 has closed all three:
|
||||
|
||||
1. > _"Hotkeys persist under legacy `KeyboardShortcuts_<name>` UserDefaults keys (from the removed
|
||||
> KeyboardShortcuts package) so old bindings survive."_
|
||||
|
||||
**Superseded.** Old bindings need not survive.
|
||||
**Closed by phase 35.** The namespace is now `hotkey.<action>`. Old bindings do survive, but via a
|
||||
one-time adoption scheduled for deletion — not by keeping the namespace. See below.
|
||||
|
||||
2. > _"Its `Codable` conformance is the compatibility seam — a `.combo` must keep encoding as the bare
|
||||
> `{"carbonKeyCode":N,"carbonModifiers":N}` record … or every existing binding and backup breaks."_
|
||||
|
||||
**Superseded.** See phase 35.
|
||||
**Closed by phase 35.** `HotKeyBinding` uses the synthesised conformance; the backup format changed
|
||||
with it, and only export → import within one build is required.
|
||||
|
||||
3. > _"`SettingsBackup` … Every field is optional so an import applies only the keys actually present."_
|
||||
|
||||
**Retained** — but for a different reason. Optionality supports **partial** files and Raycast
|
||||
imports, not old Tinycast versions.
|
||||
|
||||
`AGENTS.md` now carries a banner at the top pointing here and stating that structural rules are being
|
||||
changed by the refactor — so the correction arrives in the first thing an agent reads, rather than
|
||||
depending on the operator pasting the standing prompt.
|
||||
The refactor banner that stood at the top of `AGENTS.md` is gone: phase 35 removed it, because the
|
||||
refactor ends there and a banner announcing one in progress would be false.
|
||||
|
||||
The three clauses above are still **superseded but not yet deleted**. Phase 35 deletes them, amends the
|
||||
surrounding text, and **removes the banner** — the refactor is over at that point and the banner would
|
||||
become a lie. Until then, this document wins and a phase that trips over one of them should proceed and
|
||||
note it.
|
||||
## The two scheduled deletions
|
||||
|
||||
Phase 35 found the policy's core assumption — "there are no existing users" — to be **false**:
|
||||
`v0.7.5` is a shipped stable release, and both objectives would have destroyed real user data on
|
||||
update. By operator decision the cleanups shipped anyway, each behind a one-time migration, and each
|
||||
migration is scheduled for removal rather than kept.
|
||||
|
||||
| What | Migration | Why it was needed |
|
||||
| ---- | --------- | ----------------- |
|
||||
| `hotkey.<action>` keys and the synthesised `HotKeyBinding.Codable` | `LegacyHotKeyRecords.adopt`, called once from `HotKeyManager.start()` | `v0.7.5` stores a bare `{"carbonKeyCode":N,"carbonModifiers":N}` under `KeyboardShortcuts_<name>`; both the key and the shape changed, so every shipped binding would have read as unbound |
|
||||
| `ClipboardStore`'s `source_app` / `pinned_at` columns | the two existing `ALTER TABLE` guards and `columnExists` | `v0.7.5` has no `pinned_at`, so the prepared statements would fail, and the store **deletes a database it cannot open** — the whole clipboard history, silently |
|
||||
|
||||
**Delete both** once the release carrying them has been superseded by two further stable releases, so
|
||||
no supported upgrade path still starts from `v0.7.5`. Removal is a pure deletion in each case: delete
|
||||
`LegacyHotKeyRecords.swift` plus its one call site, and delete the two guards plus `columnExists`.
|
||||
Neither carries a version flag or persisted state, so nothing is left behind.
|
||||
|
||||
Until then, `grep -rn "ALTER TABLE\|columnExists" Tinycast` returning three hits is **expected**, and
|
||||
phase 35's acceptance criterion 4 is knowingly unmet.
|
||||
|
||||
@@ -163,7 +163,7 @@ stock Electron app tiles correctly without it, delete the helper rather than kee
|
||||
`LauncherView.rows` mirrors that position with a "Window Management" section; the slice order is the
|
||||
flat-selection invariant, so the two must move together.
|
||||
- **`HotKeyAction.windowCommand(id:)`** — persisted under
|
||||
`KeyboardShortcuts_windowCommandHotkey.<raw-id>`, matching the legacy prefix convention. Unlike
|
||||
`hotkey.windowCommand.<raw-id>`, matching the shared `HotKeyAction.defaultsKey` 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.
|
||||
- **`WindowCommandCoordinator.runWindowCommand(id:)`** is the one funnel for both palette activation and the global
|
||||
|
||||
Reference in New Issue
Block a user