Remove the App Group wholesale — one sandbox, one bookmark, one flag
Phase 2 of the one-app pivot (DESIGN 12 ▸ App-side state, re-ruled
2026-07-30; reworks 566deab). AppGroup retires; what remains is
AppStateHome — ordinary sandbox Application Support as the one home for
the registry, clipboard staging and template stores, keeping the
unit-test-host redirect (the test host is the app and would sweep real
state). Scalar defaults return to UserDefaults.standard.
BoardRecord's per-edition grant slots and openNow flags collapse to one
bookmark + one isOpenNow; the legacy-key decode and adopt-in-memory
paths go (nothing shipped with group-era records), while the founding
four-keys-required / defaults-for-everything-since decode policy stays —
a bookmarkless record decodes as the born-orphan row rather than
quarantining the list. needsReopen and the pre-anchored re-grant panel
are removed whole: the only state that flow served — a record granted by
a sibling sandbox — is unrepresentable now, and a dead bookmark of our
own was already the orphan case by explicit comment. The
indexOfRecord path fallback dies with it; path is never a key again.
The cross-process freshness stamp (mtime+size re-read) and
BoardEditionPresence with its popover "Also open in…" line retire; the
clipboard prune keeps its atomic .sweeping/ claim-then-delete, reframed
for crash residue and open -n copies rather than sibling editions. The
application-groups entitlement key is gone.
1880 tests in 317 suites green (13 cross-edition tests retired with
their subject).
Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
@@ -68,15 +68,6 @@ struct BoardInfoWidget: View {
|
||||
let store: BoardStore
|
||||
let recents: StyleRecents
|
||||
|
||||
/// The cross-edition awareness line's *fact*, asked afresh each time the popover is built — see
|
||||
/// `BoardEditionPresence` for why it is a closure rather than a value: both halves of it (the
|
||||
/// registry's flags and whether the other app is alive) can change while a board window sits
|
||||
/// there, and neither is a thing to observe.
|
||||
///
|
||||
/// Defaulted to "no line" so the two surfaces that build this widget without a registry — the
|
||||
/// titlebar tests — keep saying nothing rather than needing one.
|
||||
var otherEditionNote: () -> String? = { nil }
|
||||
|
||||
@Bindable var presentation: BoardInfoPresentation
|
||||
|
||||
var body: some View {
|
||||
@@ -96,7 +87,7 @@ struct BoardInfoWidget: View {
|
||||
.help("Board Info")
|
||||
.accessibilityLabel("Board Info")
|
||||
.popover(isPresented: $presentation.isPresented, arrowEdge: .bottom) {
|
||||
BoardInfoView(store: store, recents: recents, otherEditionNote: otherEditionNote())
|
||||
BoardInfoView(store: store, recents: recents)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -112,14 +103,12 @@ struct BoardInfoWidget: View {
|
||||
func boardInfoTitlebarAccessory(
|
||||
store: BoardStore,
|
||||
recents: StyleRecents,
|
||||
presentation: BoardInfoPresentation,
|
||||
otherEditionNote: @escaping () -> String? = { nil }
|
||||
presentation: BoardInfoPresentation
|
||||
) -> NSTitlebarAccessoryViewController {
|
||||
let hosting = NSHostingView(
|
||||
rootView: BoardInfoWidget(
|
||||
store: store,
|
||||
recents: recents,
|
||||
otherEditionNote: otherEditionNote,
|
||||
presentation: presentation
|
||||
)
|
||||
)
|
||||
@@ -152,10 +141,6 @@ struct BoardInfoView: View {
|
||||
/// See `BoardGitNote.hasGitDirectory(at:)` for why a live-updating fact isn't needed here.
|
||||
private let hasGitDirectory: Bool
|
||||
|
||||
/// The cross-edition awareness line, or `nil` for no line — resolved once when the view is built,
|
||||
/// on `hasGitDirectory`'s terms and for its reason (`BoardEditionPresence`).
|
||||
private let otherEditionNote: String?
|
||||
|
||||
/// The style editor brings its own padding, so the sections around it carry the same number by
|
||||
/// hand instead of an outer padding that would double up on it — **the editor's own figure**
|
||||
/// (`StyleEditorLayout.sectionSpacing`), which is font-derived, so the popover's chrome scales
|
||||
@@ -164,11 +149,10 @@ struct BoardInfoView: View {
|
||||
StyleEditorLayout.sectionSpacing(bodyPointSize: CardWindowMetrics.bodyPointSize)
|
||||
}
|
||||
|
||||
init(store: BoardStore, recents: StyleRecents, otherEditionNote: String? = nil) {
|
||||
init(store: BoardStore, recents: StyleRecents) {
|
||||
self.store = store
|
||||
self.recents = recents
|
||||
self.hasGitDirectory = BoardGitNote.hasGitDirectory(at: store.rootURL)
|
||||
self.otherEditionNote = otherEditionNote
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@@ -196,21 +180,10 @@ struct BoardInfoView: View {
|
||||
// nothing here at all — no header, no divider, no placeholder — and the popover ends at
|
||||
// Styling, complete in itself. Only a board that actually carries an inert `.git` earns
|
||||
// this closing note.
|
||||
//
|
||||
// The awareness line is its neighbour on the same terms, and the two stack in one section
|
||||
// when both are true — a `.git`-bearing board open in Pro is exactly the household where
|
||||
// both sentences apply, and each answers a different question.
|
||||
if hasGitDirectory || otherEditionNote != nil {
|
||||
if hasGitDirectory {
|
||||
Divider()
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
if hasGitDirectory {
|
||||
BoardGitNote()
|
||||
}
|
||||
if let otherEditionNote {
|
||||
BoardEditionPresenceNote(text: otherEditionNote)
|
||||
}
|
||||
}
|
||||
.padding(inset)
|
||||
BoardGitNote()
|
||||
.padding(inset)
|
||||
}
|
||||
}
|
||||
// The style editor's popover width, taken from the editor rather than restated: the embed
|
||||
@@ -344,77 +317,3 @@ struct BoardGitNote: View {
|
||||
FileManager.default.fileExists(atPath: boardRoot.appendingPathComponent(".git").path)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - The other edition
|
||||
|
||||
/// **⚠ Retired by the one-app collapse — one-app collapse phase 2.** There is no other edition to be
|
||||
/// aware of: 12-editions.md ▸ App-side state (re-ruled 2026-07-30) removes the App Group and with it
|
||||
/// the cross-edition flags this line reads, and 12 ▸ Distribution retires the second app outright.
|
||||
/// This type, `BoardInfoView.otherEditionNote` and `BoardRecord.openNow`'s keying go together in that
|
||||
/// phase; until then the line is simply never non-`nil` in practice, since no sibling exists to set a
|
||||
/// flag. Kept functioning, and documented as it was built, rather than half-unwound here.
|
||||
///
|
||||
/// **The cross-edition awareness line** — "Also open in Lanework Pro" (12-editions.md ▸ Both editions
|
||||
/// installed, ruled 2026-07-29).
|
||||
///
|
||||
/// > Two conveniences ride the shared registry: open-now flags are per-edition …, and the board
|
||||
/// > popover carries a contextual awareness line ("Also open in Lanework Pro") read from the other
|
||||
/// > edition's flag, **pid-liveness-checked so crash residue never lies** — a line, never a gate.
|
||||
///
|
||||
/// It is `BoardGitNote`'s posture applied to a different fact: contextual, absent when untrue, and
|
||||
/// never a control. Nothing about it gates anything — the same board open in both apps is the designed
|
||||
/// foreign-writer story (12), so this line exists to *explain* what the user is looking at, not to
|
||||
/// warn them off it.
|
||||
///
|
||||
/// ### Why liveness matters, and why it is a parameter
|
||||
///
|
||||
/// The flag is a live open marker that a crash deliberately leaves standing (02-architecture.md
|
||||
/// § Launch and window lifecycle — that residue is what makes crash recovery free). So a flag alone
|
||||
/// would claim "also open in Lanework Pro" about an app that died last Tuesday. `NSRunningApplication`
|
||||
/// settles it, and the check is injected so `note(otherEditions:isRunning:)` is a pure function the
|
||||
/// tests pin directly — the same seam `BoardGitNote.hasGitDirectory(at:)` is.
|
||||
enum BoardEditionPresence {
|
||||
|
||||
/// Whether an edition is running right now, by bundle id — the pid-liveness half.
|
||||
///
|
||||
/// `NSRunningApplication.runningApplications(withBundleIdentifier:)` rather than a stored pid: the
|
||||
/// registry records *which* edition had the board open, never a process id, and it should not
|
||||
/// start — a pid is a fact with a shelf life of milliseconds, and the bundle id is the question
|
||||
/// actually being asked.
|
||||
@MainActor
|
||||
static func isRunning(_ bundleID: String) -> Bool {
|
||||
!NSRunningApplication.runningApplications(withBundleIdentifier: bundleID).isEmpty
|
||||
}
|
||||
|
||||
/// The line, or `nil` for no line at all.
|
||||
///
|
||||
/// Three ways to get `nil`, and each is the honest answer: no other edition has the board flagged;
|
||||
/// the flagged edition is not running (crash residue); or the flagged bundle id is one this build
|
||||
/// cannot name (`AppGroup.editionDisplayName`) — a future edition, where inventing a name would be
|
||||
/// worse than saying nothing.
|
||||
///
|
||||
/// **One line even when several editions qualify**, taking the first by the sorted order the
|
||||
/// registry hands over: the popover has room for a sentence, not a roster, and with Teams
|
||||
/// deferred there is no case today where two others are open at once.
|
||||
static func note(otherEditions: [String], isRunning: (String) -> Bool) -> String? {
|
||||
for bundleID in otherEditions {
|
||||
guard isRunning(bundleID), let name = AppGroup.editionDisplayName(bundleID) else { continue }
|
||||
return "Also open in \(name)"
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/// The awareness line as a view, beside `BoardGitNote` and styled identically — the two contextual
|
||||
/// notes are one register, so a popover carrying both reads as one surface.
|
||||
struct BoardEditionPresenceNote: View {
|
||||
|
||||
let text: String
|
||||
|
||||
var body: some View {
|
||||
Text(text)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user