Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
fafc4e5
Share workspace inventory across windows
wesm Sep 1, 2026
4abd92e
Keep shared inventory stable across scene changes
wesm Sep 1, 2026
334876e
Keep shared inventory consistent during mutations
wesm Sep 1, 2026
a5ec3bb
Keep removals hidden across fence refreshes
wesm Sep 1, 2026
81ed1f7
Make shared refreshes replace stale inventory
wesm Sep 2, 2026
7a4215b
Refresh shared inventory after inactive periods
wesm Sep 2, 2026
ab39f2c
Keep shared inventory authoritative after recovery
wesm Sep 2, 2026
6e8e033
Merge remote-tracking branch 'origin/main' into kenn-forge/issue-186-…
wesm Sep 2, 2026
45d7455
Let the inventory store own mutation-end refreshes
wesm Sep 2, 2026
5f7df2d
Pin the cadence in mutation-end tmux store tests
wesm Sep 2, 2026
a34c7a9
Document store-owned mutation refreshes and probe epochs
wesm Sep 2, 2026
a8cf7bc
Fence mutation publications by host mutation epoch
wesm Sep 2, 2026
c39e37d
Keep removal preflight inventory scene-local
wesm Sep 2, 2026
6110e37
Document scene-local preflight inventory
wesm Sep 2, 2026
1df507a
Drop the deferred refresh after project removal
wesm Sep 2, 2026
8bc8145
Tighten fence satisfaction, probe yielding, and tombstone clearing
wesm Sep 2, 2026
9cf020f
Clear worktree tombstones when a project is re-registered
wesm Sep 2, 2026
55b1a41
Treat stale mutation results as provisional
wesm Sep 2, 2026
ae7d550
Clear removal tombstones in every window on re-registration
wesm Sep 3, 2026
b5916ea
Force the fence-end reload when a project registers mid-mutation
wesm Sep 3, 2026
a65822a
Keep scene tombstones behind the shared cache and fence by endpoint
wesm Sep 3, 2026
80b8e68
Drop scene tombstone retention against the shared cache
wesm Sep 3, 2026
b7013ee
Apply shared removal tombstones to scene-local inventory loads
wesm Sep 3, 2026
f93e41e
Document tombstones on scene-local inventory loads
wesm Sep 3, 2026
a1c8b80
Match project removal tombstones under a legacy-empty identity
wesm Sep 3, 2026
39df919
Identify legacy project tombstones by path on both ends
wesm Sep 3, 2026
1e9357a
Carry the project path through quarantine resolution
wesm Sep 3, 2026
d5ea309
Let a mutation ended during publication reload and keep its filter
wesm Sep 3, 2026
7e9aa0b
Revoke provisional freshness, reload on registration, remove by path
wesm Sep 3, 2026
c2bc674
Align legacy project removal and release legacy worktree tombstones
wesm Sep 3, 2026
ae336af
Compare project tombstone paths after normalization
wesm Sep 3, 2026
e05ea9f
Key legacy worktree tombstones by project path
wesm Sep 3, 2026
15efe28
Skip stale loads, clear earlier-identity tombstones, keep provisional…
wesm Sep 4, 2026
935fcf7
Load stale cache entries for new subscribers and scope tombstones to …
wesm Sep 4, 2026
64a27de
Mark path tombstone keys and scope registration clearing to the path
wesm Sep 4, 2026
f1b778f
Key post-mutation exclusions like removal tombstones
wesm Sep 4, 2026
d39f24b
Load only for hosts a subscriber newly registers
wesm Sep 4, 2026
bc60049
Key worktree tombstones by project path and remove projects by path
wesm Sep 4, 2026
74a2523
Match the removed project by path in tombstones and quarantine recovery
wesm Sep 4, 2026
093b50c
Resolve a quarantine only against the project at its own path
wesm Sep 4, 2026
2738057
Make duplicate-registration tests observe their fixes
wesm Sep 4, 2026
51d67a1
Observe quarantine completion through the coordinator event
wesm Sep 4, 2026
124ad66
Announce a registration once per command host
wesm Sep 4, 2026
56e404a
Start the tmux lane in the aliased registration test
wesm Sep 4, 2026
fb4e7c6
Exclude parked quarantines from every mutation fence check
wesm Sep 4, 2026
b75565d
Observe the fence-end reload behind a parked quarantine
wesm Sep 4, 2026
acee486
Refresh registered projects from unsaved SSH drafts
wesm Sep 5, 2026
c051f83
Release unknown project tombstones after authoritative registration
wesm Sep 5, 2026
32488f6
Keep worktree inventory current across registration and host changes
wesm Sep 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Sources/App/App.swift
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ struct GhosthubApp: App {
)
}
WorkspaceSceneBootstrap.ensureBootstrapped()
WorkspaceInventoryStore.shared.startApplicationActivityMonitoring()
}

var body: some Scene {
Expand Down
69 changes: 56 additions & 13 deletions Sources/App/KwtInventoryClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,19 @@ struct KwtDirectoryWorkspaceRecord: Codable, Equatable, Sendable {
}
}

extension KwtDirectoryWorkspaceRecord {
init(_ workspace: DirectoryWorkspaceSummary) {
self.init(
name: workspace.name,
path: workspace.path,
sessionName: workspace.tmuxSessionName,
sessionLive: workspace.sessionLive,
tmuxSocketName: workspace.tmuxSocketName,
tmuxAttachMode: workspace.tmuxAttachMode
)
}
}

struct KwtProjectInventory: Equatable, Sendable {
var project: KwtProjectRecord
var worktrees: [KwtWorktreeRecord]
Expand Down Expand Up @@ -166,8 +179,22 @@ struct KwtHostInventory: Equatable, Sendable {
}
})
}
let exclusions =
excludingWorktrees[item.project.repository] ?? []
// Removals from a legacy-empty project are keyed by its path.
// A legacy-empty record cannot name its repository, so every
// repository-keyed removal applies to it as well.
var exclusions = excludingWorktrees[
KwtSnapshotMerger.removalPathKey(item.project.path)
] ?? []
if item.project.repository.isEmpty {
for (key, identities) in excludingWorktrees
where !KwtSnapshotMerger.isRemovalPathKey(key) {
exclusions.formUnion(identities)
}
} else {
exclusions.formUnion(
excludingWorktrees[item.project.repository] ?? []
)
}
retained.worktrees.removeAll { worktree in
exclusions.contains {
$0.matches(
Expand Down Expand Up @@ -751,16 +778,7 @@ enum KwtSnapshotMerger {
)
let directoryRecords = inventory.directoryWorkspaceWarning != nil
&& inventory.directoryWorkspaces.isEmpty
? existingDirectoryWorkspaces.map {
KwtDirectoryWorkspaceRecord(
name: $0.name,
path: $0.path,
sessionName: $0.tmuxSessionName,
sessionLive: $0.sessionLive,
tmuxSocketName: $0.tmuxSocketName,
tmuxAttachMode: $0.tmuxAttachMode
)
}
? existingDirectoryWorkspaces.map(KwtDirectoryWorkspaceRecord.init)
: inventory.directoryWorkspaces
let directoryWorkspaces = directoryRecords.map { record in
let recordPath = normalizePath(record.path)
Expand Down Expand Up @@ -915,7 +933,32 @@ enum KwtSnapshotMerger {
return updated
}

private static func normalizedPath(_ path: String) -> String {
/// The key worktree removal tombstones live under: the project's
/// normalized path when known, since a worktree belongs to exactly one
/// registration, else the repository identity.
static func removalTombstoneKey(
repository: String,
path: String?
) -> String {
guard let path else { return repository }
return removalPathKey(path)
}

/// Path keys carry an explicit marker so no repository identity, on any
/// platform's path syntax, can be mistaken for one.
static func removalPathKey(_ path: String) -> String {
removalPathKeyMarker + normalizedPath(path)
}

static func isRemovalPathKey(_ key: String) -> Bool {
key.hasPrefix(removalPathKeyMarker)
}

private static let removalPathKeyMarker = "path\u{0}"

/// Lexically normalizes a host path so equivalent spellings compare
/// equal without touching any filesystem.
static func normalizedPath(_ path: String) -> String {
guard path.contains("/") else { return path }
let isAbsolute = path.hasPrefix("/")
var components: [Substring] = []
Expand Down
Loading