Skip to content

Lifecycle: make Tray residency optional and restore full windows only on explicit actions #289

Closed

Description

@skyswordw

Parent plan: #286.

Problem and priority

P2. Stow currently stays running after the last window closes unconditionally (applicationShouldTerminateAfterLastWindowClosed returns false). Its optional Hide Dock icon setting changes presentation, not process lifetime. The user wants normal archive-tool behavior plus a clean Tray-only mode, with full UI opened by explicit actions.

Actual ArcZip observation

ArcZip 1.2.1 (336), Box enabled, no archive operation: before close its main process PID was 90367 with activation policy 0 (regular). Closing through File > Close removed its onscreen main window; PID 90367 remained and policy became 1 (accessory). Its archive XPC service PID 90370 also remained at a later snapshot. Reopening the UI used the same main PID. There is no evidence here of a distinct Box-only executable replacing the main process. Bundle inspection found archive XPC, Finder/Quick Look extensions and Sparkle helpers, not a separate Box app. Box-disabled close behavior was not tested.

Proposed Stow behavior

Situation Outcome
Last normal window closes, Tray disabled, no work/prompt/transition pending Terminate through the normal safe termination path. No permanent warm process solely to speed Finder/Services.
Last normal window closes, Tray enabled Remain in accessory mode with Tray availability; no Dock icon, Welcome/Browser window or automatic Tray reveal.
Work, an owned prompt or an in-flight window handover still exists Keep the operation/prompt alive and reachable. Reevaluate after it settles. Never mistake Welcome→Browser, Browser→Glance, Pack presentation or permission recovery for a user closing the last working surface.
Open Tray Show only Tray, without promoting to a full window.
Open Stow / explicit reopen Show the appropriate main window (Welcome when no archive is open), restore regular activation, focus it once.
Create Archive / Tray Pack / explicit open-in-main Create opens the new draft workspace; Tray Pack presents the existing quick-pack flow; explicit open-in-main presents Browser. Each uses a valid window host; no unnecessary Welcome flash or duplicate picker. Preserve chosen Browser/Glance behavior for existing external-open entry points.
Explicit Quit Stow Quit the entire application including Tray, even if enabled, using unsaved-work and active-job protections. Do not silently relaunch it.

A normal Settings window must be reachable and correctly focused from Tray-only mode; closing it returns to the appropriate mode. Glance is an active archive surface, not a reason to auto-quit merely because it is an NSPanel. Hiding Tray is not quitting or clearing files.

Architecture and migration

Implement a coordinated lifecycle state machine in the existing app first. Release closed archive models/security scopes, previews and needless timers; do not claim that accessory mode alone reduces memory or means the main process has exited. Profile idle CPU/wakeups and memory before/after; separate XPC/extension processes from the main process. A separate helper executable is a later measured optimization, not required for this UX.

Reconcile/deprecate HideDockIconWithoutWindows so it does not conflict with deterministic Tray-only mode. Preserve user intent and document migration. Launch-at-login remains independently opt-in and should start quietly only when a useful resident mode is enabled; do not auto-register login items or introduce a daemon. If existing login preference and disabled Tray conflict, show clear settings behavior and do not silently create residency. Finder/Services/Quick Look must still launch the main app on demand and deliver each request once. Preserve signed extension registration.

Acceptance and sources

Test the state matrix with empty and populated Tray, Browser/Glance, Pack/batch/extract/install/open/unlock, unsaved edits (Cancel/Discard), settings/pickers, explicit quit, last-window close and transitions, late completions, and subsequent Finder/Services launch. Verify PID and activation policy, not just Dock disappearance. Use current signed artifacts for system-integration acceptance under #38.

Apple documents that last-window termination can be requested even with panels still open and still goes through the termination delegate: last-window lifecycle. See activation policies and SMAppService for separate presentation and login-registration responsibilities.

Ownership and delivery

Codex / Grok Bot owns functional state, routing, security, cancellation, errors, tests and accessibility semantics. Claude Code owns layout, hierarchy, copy presentation, materials and motion. Functional controls may be implemented before visual review. No collaborator session or visual approval is claimed; no unknown GitHub account is assigned. Preserve basic keyboard/focus access, meaningful names, Reduce Motion/Transparency and Increase Contrast; dedicated VoiceOver acceptance remains deferred under #93. Every new user-facing string needs zh-Hans. Run relevant tests, localization and generated-plist checks for implementation; coordinate installed/registered signed artifacts with the existing release owner (#32/#38).

Dependencies and integration

Depends on the master setting/migration in #288. Integrate #290 menu routes and #293 draft presentation; quick Pack remains supported. Must preserve the #287 fix.

Activity

  1. skyswordw commented on Nov 5, 2026

    @skyswordw
    MemberAuthor

    PR #295 adds the lifecycle coordinator, protected window handovers, optional accessory residency and normal termination when Tray is disabled and work has settled. Jobs, prompts, source scopes, drafts and background failures participate in lifetime; explicit Quit does not kill running operations. Existing Hide Dock UI is retired and login stays separate. Model tests cover the decision matrix and preserving/reopening accepted work. The complete physical last-window/PID/Dock/login and signed integration matrix remains #38; CUA background window/key state made native Settings close input inconclusive, so it is not reported as a pass.

    Implementation: #295
    Functional handoff: https://github.com/Beastry/stow/blob/codex/tray-functional-followups/Handoff/286-functional-followups.md

  2. skyswordw commented on Nov 6, 2026

    @skyswordw
    MemberAuthor

    Review update: #295 implements optional residency, pending-window/work retention, minimized/Settings windows, guarded Quit, draft lifetime and recovery of retained Browser failures. The integrated suite passed, including the lifetime matrix. This session verified a normal explicit Quit after real draft creation, but not the complete last-window/PID/Dock, login or signed external-launch matrix. Keep this issue open for that native acceptance; #38 retains system integration coverage.

  3. skyswordw commented on Nov 6, 2026

    @skyswordw
    MemberAuthor

    Native follow-up verified the core lifetime behavior on main 7c53936 using the isolated ad-hoc app app.stow.PRStackReview without Finder/Quick Look plugins:

    • Tray enabled: closing the last Welcome window preserved PID 21145 and changed activation policy 0 (regular) → 1 (accessory); reopening restored policy 0 in that same PID.
    • Tray disabled: closing the last Welcome window terminated PID 21145; reopening created PID 21297. A minimize action retained the process with regular policy.
    • Explicit Quit terminated the app with Tray enabled. Cancelling a changed draft's quit prompt preserved its name/files.

    Read-only process inspection ran before the next AX snapshot, because the inspection tool itself can reopen or relaunch a windowless app. This explains the earlier inconclusive close observations.

    This pass found one small edge case: a renamed empty draft offered Save in its close/quit alert although it had no contents to save. PR #301 disables that invalid action using canSave. Native AX checks confirmed Save disabled for empty and enabled for populated drafts, with Cancel preserving the draft. 41 tests in five focused suites passed. #301 closes this implementation issue when merged; the broader signed/login/external-launch/operation/performance matrix remains in #38. This is bounded runtime/AX evidence, not VoiceOver or full signed-system acceptance.

  4. skyswordw commented on Nov 6, 2026

    @skyswordw
    MemberAuthor

    Closed by merged #301 (bb38dc9). The tested PR tree exactly matches the resulting main tree. CI passed 687 tests with zero failures/skips, 0/1,261 missing zh-Hans strings, generated-plist checks and Universal 2/packaging; snapshots passed. The native follow-up and its limits are recorded in Handoff/286-functional-followups.md. #287/#290 remain open for their distinct physical-input acceptance; broader signed-system checks remain in #38.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions