Skip to content

feat: document tabs and recent documents welcome window - #10

Merged
maxmilian merged 11 commits into
mainfrom
feat/tabs-and-recents
Sep 10, 2026
Merged

maxmilian merged 11 commits into
mainfrom
feat/tabs-and-recents

Conversation

@maxmilian

Copy link
Copy Markdown
Owner

Implements the tabs-and-recents spec/plan (docs/superpowers, 2026-09-10).

What

  • Native window tabs: every document window is tagged with the tabbing identifier tech.ankey.Markout.document and joins an existing tab group via addTabbedWindow (setting tabbingMode alone is too late — see spike findings in the spec).
  • Welcome window: launching with nothing to open closes the blank document DocumentGroup creates and shows an AppKit-hosted welcome window listing recent documents; also available from Window ▸ Welcome to Markout and on Dock-icon reopen.
  • Own recents store: the system recent-documents list never persists for this ad-hoc-signed app, so recents live in ~/Library/Application Support/Markout/recents.json (capped at 10, newest first, missing files dimmed and unclickable).

Verification

  • 16 new unit tests (pure list/display/store logic); full suite 145 tests green.
  • Manual, on macOS 26.6.2 debug build: two files open as one tabbed window; launch with nothing shows only the welcome window; launch with files shows no welcome window; recents.json written correctly.
  • Not verified in this session (no Accessibility/Screen Recording permission in the sandbox): clicking a recents row (NSDocumentController.openDocument(withContentsOf:) path — fallbacks in the plan's Fallback Notes), focusedSceneValue behavior with the welcome window key, drag-out / ⌘⇧[ / ⌘N interactions, dimmed missing-file rows, and the Prefer tabs: Always setting. Also not re-verified on the macOS 14 deployment floor.

maxmilian and others added 10 commits September 10, 2026 16:12
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UeFJc3unfjgUNUjZa6LYUH
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UeFJc3unfjgUNUjZa6LYUH
Two fresh-context reviews; the plan's code was built and its tests run.
Behavioral fixes: define which tab group a new document joins, re-read the
recents list whenever the welcome window is shown, open documents before
closing that window, spare autosave-restored untitled documents, and skip the
launch handling during hosted test runs. Also handles Dock reopen with no
windows, and records the macOS 14 verification gap.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UeFJc3unfjgUNUjZa6LYUH
Verified on macOS 26.6.2 (debug build, ad-hoc signed):
- Launch with no documents: only the welcome window appears; no blank
  untitled document, no Open panel (the registered
  NSShowAppCentricOpenPanelInsteadOfUntitledFile default suppressed it).
- Launch with two files: both join one tabbed document window; no welcome.
- Recents persist across quit/relaunch in Application Support.

Not verifiable in this session (the sandbox lacks Accessibility and Screen
Recording permission, so rows could not be clicked and menus could not be
inspected): the NSDocumentController.openDocument(withContentsOf:) row-click
path, the focusedSceneValue behavior with the welcome window key, drag-out /
⌘⇧[ / ⌘N interactions, and dimmed missing-file rows.
Two fresh-context reviews of the implementation surfaced five issues:

- applicationShouldHandleReopen returned false after showing the welcome
  window, letting AppKit also run its default reopen handling (a new
  untitled document on top of the welcome window). Now returns true.
- WindowTabbingAccessor rescanned for a tab host on every SwiftUI
  re-render (i.e. every keystroke) and could re-merge a tab the user had
  deliberately dragged out into its own window. A coordinator flag now
  joins at most once per window. Measured on macOS 26.6.2: a fresh window
  can already carry a non-nil tabGroup, so 'has joined' cannot be inferred
  from tabGroup — gating on tabGroup == nil broke initial joining and was
  replaced by the flag.
- The test-host guard only checked XCTestConfigurationFilePath, which
  Swift Testing runs do not reliably set; now also probes for the
  XCTestCase class and XCTestBundlePath.
- Reopening an already-open document from the welcome window did not
  refresh its openedAt (the spec requires it); the openDocument
  completion now records it.
- decode() now deduplicates and re-caps a hand-edited recents.json at
  read time; duplicate paths would have broken ForEach(id: \.path).
  Covered by a new unit test (146 total).

Regression-verified by launch: two files open as one tabbed window
(3/3 runs), no-document launch shows only the welcome window.
@maxmilian

Copy link
Copy Markdown
Owner Author

Two fresh-context reviews (one REQUEST CHANGES, one APPROVE-with-fixes) led to 4d81405, which addresses their consolidated findings:

  • applicationShouldHandleReopen now returns true after showing the welcome window (returning false let AppKit also run default reopen handling → untitled document on top of the welcome window).
  • WindowTabbingAccessor joins at most once per window via a coordinator flag. This stops per-keystroke host rescans and prevents re-merging a deliberately dragged-out tab. Measured on macOS 26.6.2: a fresh window can already carry a non-nil tabGroup, so "has joined" cannot be inferred from tabGroup — gating on tabGroup == nil broke initial joining (two files opened as two windows) and was replaced by the flag. Regression: two files → one tabbed window, 3/3 runs.
  • Test-host guard now also probes XCTestCase / XCTestBundlePath (Swift Testing does not reliably set XCTestConfigurationFilePath).
  • Reopening an already-open document from the welcome window now refreshes openedAt (spec requirement) via the openDocument completion.
  • decode deduplicates and re-caps a hand-edited recents.json (duplicate paths would break ForEach(id: \.path)); new unit test, suite now 146.

Still requiring manual verification before merge (no Accessibility/Screen Recording in the dev sandbox): recents row click (openDocument path), focusedSceneValue behavior with the welcome window key, ⌘⇧[ / drag-out / ⌘N interactions, dimmed missing-file rows, Prefer tabs: Always, and the Dock-reopen no-untitled-document check added above.

- applicationShouldHandleReopen returns false again after showing the
  welcome window. AppKit documents false as 'the app handled the reopen';
  returning true would run the default handling, which for a document app
  with no visible windows is opening a new untitled document. The prior
  commit had inverted this based on a misreading. The reopen path also
  skips the test-host guard now, like the launch path.
- Welcome rows format timestamps with a Gregorian calendar and
  en_US_POSIX locale regardless of system locale, matching the app's
  English-only copy (a non-Gregorian or non-Latin locale previously
  produced localized or wrong-script output from the fixed formats).
- Two misleading comments in WindowTabbing reworded: the tabHost fallback
  is a deliberate last resort, and tabbingMode = .preferred overrides the
  system 'prefer tabs' setting on purpose.

Regression-verified by launch: two files open as one tabbed window (2/2
runs), no-document launch shows only the welcome window. 146 tests green.
@maxmilian

Copy link
Copy Markdown
Owner Author

Second dual fresh-context review round (of 896d447's parent state) — one APPROVE, one REQUEST CHANGES with a single P2, fixed in 896d447:

  • P2, fixed: applicationShouldHandleReopen must return false after showing the welcome window (AppKit docs: false = "the app handled it"; true runs the default, which opens an untitled document for a doc-based app with no visible windows). The previous round's change to true was a misreading — this also restores the plan's original code. The reopen path now uses the same test-host guard as the launch path.
  • P3, fixed: welcome rows now format timestamps with a Gregorian calendar + en_US_POSIX locale (fixed "MMM d"/"HH:mm" formats under a non-Gregorian/non-Latin system locale previously produced localized output, breaking the English-only copy). Two misleading comments in WindowTabbing reworded.
  • P3, accepted as-is: the 0.3 s launch sweep could in theory close a ⌘N document created within 0.3 s of launch (snapshot-at-launch can't distinguish it, since the launch document itself appears only after didFinishLaunching); NSShowAppCentricOpenPanelInsteadOfUntitledFile registration stays — the spike measured an Open panel at launch without it; per-row DateFormatter/fileExists cost is trivial at ≤10 rows; onOpenPanel leaves the welcome window open (no completion hook on the panel).

Regression: 146 tests green; two-file launch → one tabbed window (2/2), no-document launch → only the welcome window.

@maxmilian
maxmilian merged commit 78f5fab into main Sep 10, 2026
1 check passed
@maxmilian
maxmilian deleted the feat/tabs-and-recents branch September 10, 2026 09:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant