Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
948ce0e
cmux-next: the typeahead GUI gate in the repo (cx-wb5.78): typeahead-…
lawrencecchen Oct 14, 2026
b1e08b9
test(control): a deleted control socket file comes back while the app…
teamleaderleo Oct 14, 2026
f2546c8
fix(control): put the control socket back when its file is deleted un…
teamleaderleo Oct 14, 2026
fd614cb
test: a stop during a socket restore closes the new socket
teamleaderleo Oct 14, 2026
8945660
fix: restore the control socket on its accept queue
teamleaderleo Oct 14, 2026
2f692a8
fix: set SO_NOSIGPIPE on the stale-socket probe
teamleaderleo Oct 14, 2026
b400ce2
Merge feat-cmux-next into continuity-soak-socket-rebind
teamleaderleo Oct 14, 2026
1268cf7
test: take the restore path with a plain listener
teamleaderleo Oct 14, 2026
1e04356
test: failing socket restores back off and still come back
teamleaderleo Oct 14, 2026
8dd3953
fix: back off failing socket restores instead of giving up
teamleaderleo Oct 14, 2026
a4c31da
chore(next): PR autofix at 8dd3953bf70f
Oct 14, 2026
4b36532
acpmux: behavior test, a new chat starts in full access; the person's…
lawrencecchen Oct 14, 2026
8a96bfa
acpmux: new chats start in the harness's full-access mode; the person…
lawrencecchen Oct 14, 2026
d076b0d
feat(cmux-next): Page Info on the shared native popup host; its rows …
lawrencecchen Oct 14, 2026
9d8071b
fix(cmux-next): a new bookmark's name follows the page title that arr…
lawrencecchen Oct 14, 2026
0c017de
test: bookmark bubble live check covers the late page title, Page Inf…
lawrencecchen Oct 14, 2026
ec99080
review: Page Info rows keep their inset before the accessory (cx-x9o52)
lawrencecchen Oct 14, 2026
f949002
Home: the home workspace with hq-ec's page in a left chat dock; Home …
lawrencecchen Oct 14, 2026
da8aba8
cmux-next: pin CEF cef-154.0.28-cmux.25 (cmux.24 + WebAuthn hybrid fi…
lawrencecchen Oct 14, 2026
0cefb1d
cmux-next: remote browser host linked against CEF cmux.25 (step beeef…
lawrencecchen Oct 14, 2026
f2c65d6
passkeys-roaming-e2e: --typing smoke with trusted key events (cx-d0d.97)
lawrencecchen Oct 14, 2026
9bfc94e
passkeys-matrix-e2e: a timed-out addVirtualAuthenticator reply counts…
lawrencecchen Oct 14, 2026
90407b9
chrome-promos-live: ignore Chromium's status bubble (<= 30 pt at the …
lawrencecchen Oct 14, 2026
44b7544
passkeys matrix + promo check review fixes: unconfirmed authenticator…
lawrencecchen Oct 14, 2026
7ddd45d
Home dock: review fixes; classic Home reads DesignSettings.classicHom…
lawrencecchen Oct 14, 2026
b8fc637
Merge remote-tracking branch 'origin/feat-cmux-next' into feat-cmux-n…
lawrencecchen Oct 14, 2026
efe494d
free tier: a Keychain error never mints a new App Attest key; no key-…
lawrencecchen Oct 14, 2026
4a0157d
acpmux start mode: review fixes (cx-wx93i)
lawrencecchen Oct 14, 2026
51ae142
cmux-next: Ctrl and Option holds show their shortcut hints too; hint …
lawrencecchen Oct 14, 2026
5352aea
cmux-next: hold hints review fixes: only Cmd/Ctrl chords get a chip, …
lawrencecchen Oct 14, 2026
4fa5ccd
review: Page Info keeps its side on a new page; any NativePopupPanel …
lawrencecchen Oct 14, 2026
89e3a1a
free tier e2e runner: never kill; the app quits through its control s…
lawrencecchen Oct 14, 2026
5d741e1
cmux-next groups: a right-click on a group header or tab group chip o…
lawrencecchen Oct 14, 2026
30c2e49
Channels view in the app: the page may play video from the daemon's l…
lawrencecchen Oct 14, 2026
baed838
group-editor-live: find the group by its member row, the tab chip by …
lawrencecchen Oct 14, 2026
6747c2a
group-editor-live: real window-server right-click first, the in-app e…
lawrencecchen Oct 14, 2026
b2af26c
cmux-next tab groups: the group editor's More row shows the group's f…
lawrencecchen Oct 14, 2026
dc6f7c1
cmux-tui: behavior test, a large legacy registry gets no full VACUUM …
lawrencecchen Oct 14, 2026
a6ea277
Merge remote-tracking branch 'origin/feat-cmux-next' into feat-cmux-n…
lawrencecchen Oct 14, 2026
7d62c41
Merge remote-tracking branch 'origin/feat-cmux-next' into feat-cmux-n…
lawrencecchen Oct 14, 2026
69c779a
Merge origin/feat-cmux-next (7419c1717c8f) into feat-cmux-next
lawrencecchen Oct 14, 2026
26629b1
Merge origin/feat-cmux-next (7419c1717c8f) into feat-cmux-next
lawrencecchen Oct 14, 2026
f75f4b2
Merge origin/feat-cmux-next (7419c1717c8f) into feat-cmux-next
lawrencecchen Oct 14, 2026
93afcf0
Merge origin/feat-cmux-next (7419c1717c8f) into feat-cmux-next
lawrencecchen Oct 14, 2026
8d1d599
Merge origin/feat-cmux-next (7419c1717c8f) into feat-cmux-next
lawrencecchen Oct 14, 2026
a1f9f9d
cmux-tui: no full VACUUM of a large legacy registry while its owner s…
lawrencecchen Oct 14, 2026
86adffb
Merge remote-tracking branch 'origin/feat-cmux-next' into feat-cmux-n…
lawrencecchen Oct 14, 2026
f842189
Merge origin/feat-cmux-next (c5b55f8acd47) into feat-cmux-next
lawrencecchen Oct 14, 2026
e00b9d1
Merge remote-tracking branch 'origin/feat-cmux-next' into feat-cmux-n…
lawrencecchen Oct 14, 2026
3d3d16f
safe-push train: merge 69c779add1c1 (feat-cmux-next-ff-s3)
Oct 14, 2026
bda608b
safe-push train: merge 8d1d59909f99 (nx-passkeys-hw)
Oct 14, 2026
0a2b48e
safe-push train: merge f75f4b2aded4 (feat-cmux-next-groups-restore)
Oct 14, 2026
4f1bb0f
Merge origin/feat-cmux-next (3d3d16fc3ac0) into feat-cmux-next
lawrencecchen Oct 14, 2026
d527baf
Merge origin/feat-cmux-next (0a2b48e498b1) into feat-cmux-next
lawrencecchen Oct 14, 2026
c720dec
safe-push train: merge 26629b172245 (feat-cmux-next-hqec-media-csp)
Oct 14, 2026
493f3d7
safe-push train: merge 93afcf04da32 (mhh-modifier-hold-hints)
Oct 14, 2026
ef43a52
safe-push train: merge f842189a6c45 (hq39-attest-keystore)
Oct 14, 2026
9300251
cmux-next: a local merge commit reuses the fetched same-tree acpmux (…
teamleaderleo Oct 14, 2026
82014cd
safe-push train: merge 4f1bb0f03779 (feat-cmux-next-notconn-p0)
Oct 14, 2026
f4bc46b
safe-push train: merge d527bafe710b (feat-cmux-next-popup-pageinfo)
Oct 14, 2026
93d4ef2
webviews: popups draw one hairline, not two (#20461)
teamleaderleo Oct 14, 2026
720fa08
Merge origin/feat-cmux-next (93d4ef26af52) into feat-cmux-next
lawrencecchen Oct 14, 2026
4c55c90
cmux-next: name the owner of ControlWorkspaceInfoCache's publish hop …
teamleaderleo Oct 14, 2026
eddae17
safe-push train: merge 720fa088c73a (feat-cmux-next-fullaccess)
Oct 14, 2026
2834e65
CmuxNext: one-line sidebar rows by default; notification text in the …
teamleaderleo Oct 14, 2026
705b5d7
Merge origin/feat-cmux-next (2834e655ab03) into feat-cmux-next
lawrencecchen Oct 14, 2026
140aa1f
Merge feat-cmux-next into continuity-soak-socket-rebind
teamleaderleo Oct 14, 2026
2358d24
test(cmux-next): a failed LineClient connect closes its descriptor once
teamleaderleo Oct 14, 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
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,32 @@ import Security

/// Where an app keeps the id of its App Attest key (the key itself stays in
/// the Secure Enclave; the id is a handle, not a secret).
///
/// Every call reports a store error: a caller that cannot read or write the
/// key id must not make a new App Attest key (a new key spends the device's
/// monthly grant).
public protocol AttestKeyStore: Sendable {
/// The stored key id, or nil.
func load() -> String?
/// The stored key id, or nil when none is stored.
/// - Throws: ``AttestKeyStoreError`` when the store cannot be read (for example a denied
/// Keychain access); the caller must then not make a new key.
func load() throws -> String?
/// Stores `keyID`, replacing any earlier one.
func save(_ keyID: String)
/// Forgets the stored key id.
func delete()
/// - Throws: ``AttestKeyStoreError`` when the store cannot be written.
func save(_ keyID: String) throws
/// Forgets the stored key id (no error when none is stored).
/// - Throws: ``AttestKeyStoreError`` when the store cannot be changed.
func delete() throws
}

/// A key store read or write that failed, with the system status.
public struct AttestKeyStoreError: Error, Sendable, Equatable {
/// The `OSStatus` of the failed call.
public let status: Int32

/// - Parameter status: the `OSStatus` of the failed call.
public init(status: Int32) {
self.status = status
}
}

/// ``AttestKeyStore`` in one generic-password item of the login keychain
Expand All @@ -34,25 +53,33 @@ public struct KeychainAttestKeyStore: AttestKeyStore {
[kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, kSecAttrAccount as String: account]
}

public func load() -> String? {
public func load() throws -> String? {
var item: CFTypeRef?
var search = query
search[kSecReturnData as String] = true
search[kSecMatchLimit as String] = kSecMatchLimitOne
guard SecItemCopyMatching(search as CFDictionary, &item) == errSecSuccess, let data = item as? Data else { return nil }
return String(data: data, encoding: .utf8)
let status = SecItemCopyMatching(search as CFDictionary, &item)
if status == errSecItemNotFound { return nil }
guard status == errSecSuccess else { throw AttestKeyStoreError(status: status) }
guard let data = item as? Data, let keyID = String(data: data, encoding: .utf8), !keyID.isEmpty else {
throw AttestKeyStoreError(status: errSecDecode)
}
return keyID
}

public func save(_ keyID: String) {
public func save(_ keyID: String) throws {
let data = Data(keyID.utf8)
if SecItemUpdate(query as CFDictionary, [kSecValueData as String: data] as CFDictionary) == errSecItemNotFound {
var status = SecItemUpdate(query as CFDictionary, [kSecValueData as String: data] as CFDictionary)
if status == errSecItemNotFound {
var add = query
add[kSecValueData as String] = data
SecItemAdd(add as CFDictionary, nil)
status = SecItemAdd(add as CFDictionary, nil)
}
guard status == errSecSuccess else { throw AttestKeyStoreError(status: status) }
}

public func delete() {
SecItemDelete(query as CFDictionary)
public func delete() throws {
let status = SecItemDelete(query as CFDictionary)
guard status == errSecSuccess || status == errSecItemNotFound else { throw AttestKeyStoreError(status: status) }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,10 @@ public actor FreeTierAttestProvider {
generation += 1
let register = Self.encode([
"op": "attest_provider", "version": 1, "platform": "mac", "origin": origin,
"enabled": enabled, "managed": managed, "has_key": keys.load() != nil, "signed_in": signedIn,
// No Keychain read where App Attest cannot work (no prompt in an ad-hoc DEV build);
// a read error says no key here, and the attest request then refuses (no new key).
"enabled": enabled, "managed": managed,
"has_key": attest.isSupported && ((try? keys.load()) ?? nil) != nil, "signed_in": signedIn,
])
let (lines, sink) = AsyncStream.makeStream(of: Data?.self, bufferingPolicy: .bufferingNewest(64))
let connection = AcpmuxCheckedLine(environment: routerEnvironment)
Expand Down Expand Up @@ -200,22 +203,24 @@ public actor FreeTierAttestProvider {
switch request["req"] as? String {
case "attest":
let started = generation
if request["fresh_key"] as? Bool == true { keys.delete() }
if request["fresh_key"] as? Bool == true { try keys.delete() }
let keyID: String
if let known = keys.load() {
// A store error throws: no new key is made when the old id cannot be read.
if let known = try keys.load() {
keyID = known
} else {
keyID = try await attest.generateKey()
// A reconnect while the key was made: a newer request owns the key id now.
guard generation == started else { return ["error": "failed"] }
keys.save(keyID)
// Saved before the attestation: a key whose id cannot be kept is never attested.
try keys.save(keyID)
}
let attestation = try await attest.attestKey(keyID, clientDataHash: hash)
let token = try await attest.deviceCheckToken()
return ["key_id": keyID, "attestation": attestation.base64EncodedString(),
"device_token": token.base64EncodedString()]
case "assert":
guard let keyID = keys.load() else { return ["error": "invalid_key"] }
guard let keyID = try keys.load() else { return ["error": "invalid_key"] }
let assertion = try await attest.generateAssertion(keyID, clientDataHash: hash)
return ["key_id": keyID, "assertion": assertion.base64EncodedString()]
default:
Expand All @@ -229,6 +234,10 @@ public actor FreeTierAttestProvider {
logger.info("free tier: an App Attest call failed")
return ["error": "failed"]
}
} catch let error as AttestKeyStoreError {
// The key id cannot be read or kept (a denied Keychain access): no new key, no proof.
logger.error("free tier: the App Attest key id store failed (status \(error.status, privacy: .public)); no new key")
return ["error": "failed"]
} catch {
return ["error": "failed"]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,26 +28,24 @@ extension AppActions {
guard let number = invocation["index"]?.intValue else { return }
SidebarNavigation.select(number: number, services)
})
// The native Home as a pane tab (HomePageTab), next to any other tab. Registered at
// launch so a restored Home tab finds its provider.
// The native Home tab (HomePageTab), the channels Home tab (HomeChannelsPageTab) and in DEBUG the Home
// fixture: registered at launch so a restored tab finds its provider (not blank).
services.pages.register(HomePageTab(services: services))
registry.bind("home.tab", invoke: { _ = HomePageTab.open(services, focus: $0.allowsViewChange) })
// Home is a top page (TOP-SECTION-ITEMS-ARE-PAGES): the active
// window shows it, from any origin (a focus action). With no window,
// the store's home workspace opens one; else refused with why.
// The channels Home tab (HomeChannelsPageTab), next to the native Home, and in DEBUG the Home fixture
// (debug.home_native_fixture.open): registered at launch so a restored tab finds its provider (not blank).
services.pages.register(HomeChannelsPageTab(services: services))
#if DEBUG
services.pages.register(DebugHomeNativeFixture())
#endif
registry.bind("home.channels") { _ = HomeChannelsPageTab.open(services) }
// Home = the home workspace and its left dock (cx-ebm.59.5), in the active window or the one that has it.
registry.bind("home.show") {
if TopPages.show(.home, services: services) != nil { return }
guard services.windows.active == nil, let home = services.home.homeWorkspace else {
services.registry.refuse(RefusalStrings.homeNotReady)
let classic = services.home.dock.isClassic
if classic, TopPages.show(.home, services: services) != nil { return }
guard !classic || services.windows.active == nil, classic || ActionRunScope.viewChangeAllowed(), let home = services.home.homeWorkspace else {
services.registry.refuse(classic || ActionRunScope.viewChangeAllowed() ? RefusalStrings.homeNotReady : RefusalStrings.homeNeedsFocus)
return
}
if !classic, let active = services.windows.active { return services.windows.show(workspaceID: home.id, in: active.state) }
services.windows.reveal(workspaceID: home.id)
}
// The composer's attach button as an action (home.attachFiles): a path
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ extension BookmarkService {
func showBubble(for node: BookmarkNode, profile: String, isNew: Bool, anchor: NSView, tab key: String?) {
let folders = BookmarkFolderChoice.all(in: tree(profile), barTitle: BookmarkStrings.barTitle, otherTitle: BookmarkStrings.otherBookmarks)
let bubble = BookmarkEditBubble(
isNew: isNew, title: node.title, folder: node.parent, folders: folders,
isNew: isNew, title: node.title, folder: node.parent, folders: folders, pageTitle: { [weak self] in key.flatMap { self?.services.cache.existingBrowser($0)?.tab.state }.flatMap { $0.url == node.url ? $0.title : nil } },
onSave: { [weak self] result in
guard let self, let current = tree(profile).node(node.id) else { return }
if result.title != current.title { try? apply(.update(id: node.id, title: result.title), profile: profile) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,11 @@ struct DebugShortcutHintControl {
let hints = controller.shortcutHints else {
return .object(["error": .string("no active window hint monitor")])
}
let flags: NSEvent.ModifierFlags
switch params["modifier"]?.stringValue {
case "cmd": flags = .command
case "ctrl": flags = .control
case "release": flags = []
default: return .object(["error": .string("modifier must be cmd, ctrl, or release")])
}
// "cmd", "ctrl", "opt", "shift" joined by "+" (for example "ctrl+opt"), or "release".
let names: [String: NSEvent.ModifierFlags] = ["cmd": .command, "ctrl": .control, "opt": .option, "shift": .shift, "release": []]
let parts = (params["modifier"]?.stringValue ?? "").split(separator: "+").map { names[String($0)] }
guard !parts.isEmpty, !parts.contains(nil), parts.count == 1 || !parts.contains(where: { $0?.isEmpty == true }) else { return .object(["error": .string("modifier must be cmd, ctrl, opt, shift joined by +, or release")]) }
let flags = parts.reduce(into: NSEvent.ModifierFlags()) { $0.formUnion($1 ?? []) }
let ready = await hints.debugHold(flags)
return .object(["ready": .bool(ready), "released": .bool(flags.isEmpty),
"source": .string("modifier held via debug event injection")])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,17 @@ struct ShortcutHintModifierPolicy {
static let intentionalHoldDelay: Duration = .milliseconds(300)
private var suppressed = false
private var held = false

mutating func keyDown() { suppressed = held }

mutating func update(flags: NSEvent.ModifierFlags, eligible: Bool, elapsed: Duration) -> Bool {
/// The held set a hold may show hints for: Cmd, Ctrl or Option, alone or together, with or without Shift.
static func armed(_ flags: NSEvent.ModifierFlags) -> NSEvent.ModifierFlags? {
let modifiers = flags.intersection([.command, .control, .shift, .option])
held = !modifiers.isEmpty
if modifiers.isEmpty { suppressed = false }
return eligible && !suppressed && (modifiers == .command || modifiers == .control)
&& elapsed >= Self.intentionalHoldDelay
return modifiers.isDisjoint(with: [.command, .control, .option]) ? nil : modifiers
}

mutating func update(flags: NSEvent.ModifierFlags, eligible: Bool, elapsed: Duration) -> Bool {
held = !flags.isDisjoint(with: [.command, .control, .shift, .option])
if !held { suppressed = false }
return eligible && !suppressed && Self.armed(flags) != nil && elapsed >= Self.intentionalHoldDelay
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ extension WindowScopedShortcutHintModifierMonitor {
NSEvent.keyEvent(with: .flagsChanged, location: .zero, modifierFlags: flags,
timestamp: ProcessInfo.processInfo.systemUptime, windowNumber: window.windowNumber,
context: nil, characters: "", charactersIgnoringModifiers: "", isARepeat: false,
keyCode: flags.contains(.control) ? 59 : 55)
keyCode: flags.contains(.command) ? 55 : flags.contains(.control) ? 59 : flags.contains(.option) ? 58 : 56)
}
}
#endif
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import AppKit

/// Watches only the host window's intentional Command/Control holds. It never consumes input.
/// Watches only the host window's intentional Command/Control/Option holds; a key or a click (Option-click) hides and suppresses. It never consumes input.
@MainActor
final class WindowScopedShortcutHintModifierMonitor {
private weak var window: NSWindow?
Expand Down Expand Up @@ -37,7 +37,7 @@ final class WindowScopedShortcutHintModifierMonitor {
self?.receive(event)
return event
}
keyMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] event in
keyMonitor = NSEvent.addLocalMonitorForEvents(matching: [.keyDown, .leftMouseDown, .rightMouseDown, .otherMouseDown]) { [weak self] event in
guard let self, eligible else { return event }
keyDown()
return event
Expand All @@ -55,18 +55,17 @@ final class WindowScopedShortcutHintModifierMonitor {
let flags = event.modifierFlags
currentFlags = flags
hide()
// Reset suppression when the modifier is released; only bare Cmd/Ctrl arms the delay.
// Reset suppression when the modifier is released; a Cmd, Ctrl or Option set arms the delay.
_ = policy.update(flags: flags, eligible: eligible, elapsed: .zero)
let modifiers = flags.intersection([.command, .control, .shift, .option])
guard eligible, modifiers == .command || modifiers == .control else { return }
guard eligible, ShortcutHintModifierPolicy.armed(flags) != nil else { return }
pending = Task { [weak self, clock] in
// wakeup-allow: one cancellable, injected-clock 0.30s intentional hold deadline; never polling.
do { try await clock.sleep(for: ShortcutHintModifierPolicy.intentionalHoldDelay) } catch { return }
guard !Task.isCancelled, let self else { return }
pending = nil
let current = currentFlags
if policy.update(flags: current, eligible: eligible, elapsed: ShortcutHintModifierPolicy.intentionalHoldDelay) {
changed(current.intersection([.command, .control]))
changed(ShortcutHintModifierPolicy.armed(current))
#if DEBUG
capture.finish(true)
#endif
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ final class WindowShortcutHints {
guard KeyRouter.allows(registry.keyTier(for: action), id: action, focus: controller.focus.state) else { return nil }
// Use the resolved table, including keybindings.json removals and rebindings.
for entry in table.entries.reversed() where entry.command == action && entry.keys.count == 1 {
guard let shortcut = entry.keys.first, shortcut.modifiers.contains(held),
guard let shortcut = entry.keys.first, shortcut.modifiers.contains(held), KeyRouter.isChord(shortcut.modifiers),
digit.map({ entry.argument == String($0) }) ?? (entry.argument == nil),
table.resolve(entry.keys, in: context, isRunnable: { bindings.canPerform($0, in: context.bits) }).winner == entry else { continue }
return shortcut.displayString
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ extension HomeService {
guard let self, !Task.isCancelled else { return }
homeWorkspaceID = home
settleHomeEnsure(generation)
homeWorkspaceStep = "ensured \(home)"
homeWorkspaceStep = await (HomeDock.classic ? "ensured \(home)" : dock.ensure(connection, store: services.machines.local.store, home: home) {
TabMoves.toNewDockColumn($0, anchor: $1, edge: .left, role: .agentChat, respawn: $2, services: services, completion: $3) })
try await ensureChiefTab(connection, home: home)
} catch is CancellationError {
} catch {
Expand Down Expand Up @@ -141,7 +142,7 @@ extension HomeService {
// The local Chief is looked up (never created) once a chief is placed.
let localChief = if placed == nil, let owner { try await chiefConversation(owner) } else { listed?.id }
let localHasHistory = placed == nil || (listed?.lastSeq ?? 0) > 0
guard let chief = HomeChiefSource.choose(local: localChief, localHasHistory: localHasHistory, placed: placed) else { return }
guard let chief = HomeChiefSource.choose(local: localChief, localHasHistory: localHasHistory, placed: placed), HomeDock.classic else { return }
homeWorkspaceStep = "waiting for the home workspace in the tree"
// The tree reports a just-created home after its event; wait for it
// (this task is cancelled by the next connection).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ final class HomeService {
@ObservationIgnored var homeWorkspaceStep = "not started"
/// The chief tab creation's idempotency key (one per creation).
@ObservationIgnored let chiefTabKey = HomeChiefTabKey()
@ObservationIgnored let dock = HomeDock() // the Home dock's page tab and links (cx-ebm.59.5, cx-ebm.59.6)
/// The signed-in user's chief placed on a paired server (G6), as last
/// read on a connect or sign-in; its main conversation is the Chief tab.
private(set) var cloudChief: CloudChief?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import CmuxNextDesign

/// What a notification does to the sidebar, as classic did (hq#1870 P1 #11):
/// its workspace moves to the top of the unpinned rows
/// (`notifications.attention.moveToTop`), and its row shows the text while it
/// is unread (`sidebar.workspaceRow.notification`; the text comes from the
/// daemon store, seeded here with the notifications posted before launch).
/// (`notifications.attention.moveToTop`), and its hover card shows the text
/// while it is unread (its row too with `sidebar.workspaceRow.notification`
/// on; the text comes from the daemon store, seeded here with the
/// notifications posted before launch).
extension NotificationCenterService {
/// Moves a notified workspace first in its group, else first among the
/// loose rows, as Move Workspace to Top does. A pinned workspace, a muted
Expand Down
Loading
Loading