Skip to content

Add Window Projects panel for archiving and restoring terminal windows#620

Draft
YSaxon wants to merge 61 commits into
gnachman:masterfrom
YSaxon:claude/archives-per-window-projects-xtfmL
Draft

Add Window Projects panel for archiving and restoring terminal windows#620
YSaxon wants to merge 61 commits into
gnachman:masterfrom
YSaxon:claude/archives-per-window-projects-xtfmL

Conversation

@YSaxon

@YSaxon YSaxon commented Mar 17, 2026

Copy link
Copy Markdown

Summary

Introduces a new "Window Projects" panel UI that allows users to organize, archive, and restore iTerm2 terminal windows into named project hierarchies. This feature enables saving window arrangements for later restoration while keeping projects organized in a tree structure.

Key Changes

  • New Panel UI (iTermProjectsPanelController.swift):

    • Floating panel window with split-view layout
    • Left side: hierarchical project tree (NSOutlineView) with project management controls
    • Right side: list of currently open windows (NSTableView) with archive options
    • Hover previews for both archived windows and open windows
  • Data Model (iTermWindowProjectsModel.swift):

    • iTermWindowProject: Hierarchical project nodes supporting nested sub-projects
    • iTermArchivedWindow: Stores window metadata and binary-encoded arrangement data
    • Singleton model with JSON persistence to Application Support directory
    • Full CRUD operations for projects and window archiving/restoration
  • Project Management Features:

    • Create root projects and nested sub-projects
    • Rename projects via context menu
    • Delete projects with confirmation dialog
    • Display total archived window count per project
  • Window Archiving:

    • Archive open windows to selected project (with option to keep open or close)
    • Remove archived windows from projects
    • Restore individual archived windows or all windows in a project
    • Preserve window arrangements using iTerm's existing arrangement serialization
  • UI Polish:

    • Context menus for projects and archived windows
    • Hover preview popovers (0.5s delay) showing window thumbnails
    • Status label showing open window count
    • Responsive button states based on selection
    • Section headers and visual separators
  • Integration:

    • Added "Window Projects…" menu item to Window menu in iTermApplicationDelegate.m
    • Notification-based model change propagation
    • Proper memory management with weak references in closures

Implementation Details

  • Window arrangements are serialized as binary plists and base64-encoded for JSON storage
  • Preview images for open windows use CGWindowListCreateImage for live thumbnails
  • Archived window previews use iTerm's ArrangementPreviewView component
  • Split view maintains 42% width preference for projects panel with configurable min/max constraints
  • All UI built with Auto Layout for responsive resizing

https://claude.ai/code/session_011nQdWSvNudhnXQN7vu16YW

claude added 2 commits March 17, 2026 20:52
Introduces a side-by-side panel (Window > Window Projects…) that lets
users organise iTerm2 windows into arbitrarily-nested projects:

• iTermWindowProjectsModel – data layer (projects, sub-projects, archived
  windows). Persisted as JSON in ~/Library/Application Support/iTerm2/
  WindowProjects.json.

• iTermProjectsPanelController – floating panel with an NSSplitView:
  - Left: NSOutlineView showing the project tree. Each project can hold
    sub-projects and archived (closed) windows. Hover preview shows an
    ArrangementPreviewView for any archived window.
  - Right: NSTableView listing every currently-open window. Hover preview
    captures a live screenshot via CGWindowListCreateImage.

  Actions: create/rename/delete projects & sub-projects; archive a
  selected open window to a selected project (optionally closing it);
  restore individual archived windows or all windows in a project.
  Right-click context menus on both panes.

• Window > Window Projects… menu item wired to iTermApplicationDelegate.

https://claude.ai/code/session_011nQdWSvNudhnXQN7vu16YW
- Add Window > Window Projects… menu item (awakeFromNib, programmatic)
- Add -showWindowProjectsPanel: IBAction to iTermApplicationDelegate
- Register both new Swift files in iTerm2SharedARC target

https://claude.ai/code/session_011nQdWSvNudhnXQN7vu16YW
@YSaxon
YSaxon marked this pull request as draft March 17, 2026 21:03
claude and others added 3 commits March 18, 2026 14:55
- iTermWindowProject gains a lastUsed: Date field (backward-compatible;
  defaults to distantPast on decode so older JSON sorts to the bottom)
- archiveWindow and restoreWindow/restoreAllWindows update lastUsed
- Projects pane header now has a two-segment control ("Name" / "Recent")
  that re-sorts root and sub-project lists in the outline view on the fly;
  "Recent" is the default

https://claude.ai/code/session_011nQdWSvNudhnXQN7vu16YW
Model (iTermWindowProjectsModel):
- Add runtime liveAssociations ([windowNumber → projectID])
- associateWindow/disassociateWindow: mark open windows as belonging
  to a project without closing them
- Auto-archive on close: NSWindow.willCloseNotification observer saves
  the window arrangement to its associated project when the window closes
- closeProject: archive + close all live windows for a project
- archivedWindow(id:) and project(id:) lookup helpers

Panel UI (iTermProjectsPanelController):
Left pane: now shows both live (bold, terminal.fill icon) and archived
  (grey, terminal icon) windows as leaves under each project. Double-click
  an open window → bring to front; double-click archived → restore.
  New "Close All" button + context menu item closes the selected project.

Right pane: converted from NSTableView to NSOutlineView grouped by
  project. Each project that has open windows appears as an expandable
  group header. Unassociated open windows appear in an "Unassociated"
  section at the bottom. Bottom bar "Associate with Project" button
  (replaces old "Archive to Project") records the live association
  without closing the window.

Drag-and-drop (all 6 semantic paths):
  right window  → left project       archive + close
  right window  → right group        reassign association (keep open)
  right window  → Unassociated/root  disassociate (keep open)
  right group   → left pane          close-all for that project
  left archived → right pane         restore, remove archive entry
  left project  → right pane         restore all windows in project

https://claude.ai/code/session_011nQdWSvNudhnXQN7vu16YW
@gnachman

Copy link
Copy Markdown
Owner

This seems like a useful idea - basically a way of organizing arrangements. I think it would be better to start with a spec; the code as written is pretty far from being mergable. Do you have a vision for where this could go? It feels like a bigger idea is trapped inside waiting to get out.

YSaxon added 23 commits June 17, 2026 14:38
…ing application shutdown, and add complete unit tests
…GraphEncodingTests.swift to allow building and running unit tests successfully
…nsible global fallbacks when nothing is selected
…ree using live process checking and custom styling
…metadata, and fix Association button responder targets
YSaxon and others added 26 commits June 22, 2026 14:14
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…gnostics

The Freeze All button routes through closeProject, a duplicate freeze path that
still closed the fd (closeFileDescriptorAndDeregisterIfPossible) instead of
parking — so the parking fix in archiveWindow never ran for the main UI path,
and the fd was closed (killing in-process reattach). Centralize into
parkSessionsForReattachment and call it from both archiveWindow and closeProject.

Also fix the thaw/pathfinder diagnostics: getForSocketNumber's callback delivers
an iTermResult<iTermMultiServerConnection>, not the connection directly, so the
unattachedChildren probe always read -1. Unwrap the result.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Across a quit/relaunch, the startup orphan-server adopter swept detached
windows' processes into a generic recovered window (since project archives
aren't part of macOS window restoration), consuming the child from
unattachedChildren so a later project restore fell back to a fresh shell + left
a stray tab.

Add a claimedChildPIDsProvider hook on iTermOrphanServerAdopter; the app
delegate wires it to iTermWindowProjectsModel.claimedMultiserverChildPIDs (all
Child PIDs across archived arrangements). The adopter leaves claimed children
parked in unattachedChildren so on-demand project restore re-attaches them.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…open windows on quit

Re-key liveAssociations from window-number to the stable PseudoTerminal
terminalGuid and persist it (separate WindowProjectAssociations.json, projects
JSON untouched). An open associated window brought back by native window
restoration after quit/crash now re-joins its project automatically — the
restored window keeps its guid, so project(for:) resolves it with no fragile
reconcile step.

applicationWillTerminate no longer archives open windows (that created a live
restored window + a stale archive duplicate); it just sets isTerminating so the
willClose notifications fired during teardown are ignored (preserving
associations). windowWillClose still archives a user-initiated close (the closed
state), keyed by guid.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…s helper

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…parsing

Add coverage for the recently-built data layer (DesignNotes §8 headless list):
- serverDict(in:) / allServerChildPIDs(in:) extraction, incl. nested split-pane
  Subviews and NSNumber-valued plists
- claimedMultiserverChildPIDs() over a nested project tree
- totalWindowCount recursion
- guid→project association persistence round-trip (save/load), multi-entry, and
  dangling-association tolerance

Remove testSocketAdoptionExplorer: an exploratory probe that asserted a live
multiserver daemon connection to a hardcoded socket — unrunnable headlessly
(hangs 5s then fails) and copy-paste-buggy.

Also fix DesignNotes gnachman#10 (empty 42-byte arrangement bug): add isArchivable()
guard and wire it into archiveWindow/closeProject/windowWillClose so an empty
capture never produces an unrestorable archive entry (window stays open
instead), with a unit test for the guard.

Add test-only accessors (testOnlyAssociations, testOnlyReloadAssociationsFromDisk,
testOnlyIsTerminating) mirroring the existing testOnlySetRootProjects pattern;
setUp/tearDown now back up and restore associations for inter-test isolation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ver occurs

Not yet convinced DesignNotes gnachman#10 (empty ~42-byte capture) is a real current
bug vs. residue of an older build. Replace the terse guard log with a temporary
dev reporter that, if it ever fires, dumps the arrangement bytes/keys, window
title/number, live session count, and a 25-frame call stack to /tmp/iterm_wp.log
and pops a warning dialog so we can catch it in the act.

Clearly marked STRIP BEFORE SHIPPING — remove together with the isArchivable
guard wiring once the question is settled.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…multi-select

GUI session for the Window Projects panel:

- Drop-overlay drag model: drag origin + payload decide the action via labeled
  drop zones. Associated open window(s) / project group dragged right→left show
  an Archive (75%) | Detach (25%) overlay; saved window(s) / project dragged
  left→right show a Restore overlay. Unassociated windows still associate by
  dropping on a project row. New iTermProjectsDropOverlay drives zones off the
  live NSDraggingInfo.
- Naming: open/detached/closed vocabulary. Freeze→Detach, Close&Archive→Archive
  across buttons/menus; project rows read (N open, M detached, Q closed); saved
  leaf rows read Detached/Closed.
- Multi-select on both panes; actions and context menus apply to the selection.
- Left pane: single Restore button with a count label (Restore / Restore N /
  Restore All… + confirm when nothing selected). Disabled buttons now dim
  obviously. Drops always target the whole project (no between-rows lines).
- Right pane: removed Pathfinder/Manual-Adopt; added Archive/Detach buttons. The
  Unassociated group is always present and pinned on top and is the only
  disassociate drop target (no accidental disassociation in empty space).
- Previews are pinned to their own side (left→left, right→right) so they never
  cover the other pane, with an arrow pointing back at the hovered row; saved
  thumbnails render at 2× for crispness.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
So the cursor shows the same redirect-arrow badge as the right pane when
dragging a window/archive onto a project. Cosmetic only — the drop behavior
(associate / move between projects) is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…r Server Dict

isOrphanedAndRunning only checked the monoserver "Server PID" key, which doesn't
exist in multiserver arrangements, so every saved window classified as closed.
Now also probe Server Dict→Child PID (via allServerChildPIDs) with kill(pid,0),
keeping the monoserver path as a fallback. This is what lets the UI distinguish
detached from closed windows.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Route WindowProjects.json, associations, and thumbnails through iTerm's
suite-aware applicationSupportDirectory (was hardcoding .../iTerm2/), so
launching with `-suite <name>` isolates Window Projects storage along with
the prefs and daemon socket it already isolated. With no -suite the path is
byte-identical to before, so prod is unchanged and no migration is needed.

This lets manual/E2E test runs (-suite test) avoid clobbering the Window
Projects accumulated from real dogfooding, and keeps a test instance's parked/
reattached processes on a separate daemon from real running shells.

Adds tools/claude_script_0003_dogfood_suite.sh to launch an isolated test
instance (optionally seeding its prefs from prod).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Closing (red button / Cmd-W) a window associated with a project now prompts
with Save & Detach / Save & Close / Remove from Project / Cancel instead of
silently archiving. iTermWarning's "Remember my choice" persists the selection
(Cancel is never remembered, so a window can't become unclosable).

The dialog hooks PseudoTerminal.windowShouldClose: — the cancelable point —
since the model's willClose observer fires after the close is committed. The
chosen action runs on the next runloop tick to avoid re-entering close from
within windowShouldClose:, and removes the association first so the willClose
fallback no-ops (no double archive). That fallback still handles non-user
closes (last session exits, programmatic close), where a prompt would be wrong.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
gemini added spurious/mismatched nullable annotations while building
Window Projects; these stayed dormant as warnings until origin/master's
-Werror flip broke the PCH build. Revert the spurious iTermController.h
nullable (impl never returns nil), wrap iTermOrphanServerAdopter.h in
NS_ASSUME_NONNULL to support our claimedChildPIDsProvider addition, and
drop now-redundant optional-handling in iTermWindowProjectsModel.swift
now that the real types are correctly non-optional.
These declarations predate Window Projects and aren't ours to redesign.
The real trigger isn't -Werror alone: this branch newly imports
iTermMultiServerConnection.h and iTermOrphanServerAdopter.h into
iTerm2SharedARC-Bridging-Header.h so Swift can call them directly.
SwiftGeneratePch enforces a much stricter nullability-completeness audit
on headers exposed to Swift than plain ObjC compilation ever did, so
latent partial-audit gaps that were always harmless there (argv/
newEnviron char**, a fully-unaudited file) became hard errors the moment
these files got bridged. Annotate in place to match actual behavior;
revert in favor of an upstream fix if one lands.
ModernTests is a PBXFileSystemSynchronizedRootGroup, so files physically
under ModernTests/ are auto-discovered — no manual PBXFileReference/
PBXBuildFile/Sources-phase entry needed. tools/add_file_to_xcodeproj.rb
used the old manual-add style when this file was added, creating a
duplicate reference resolved against the project root instead of
ModernTests/, which failed with "Build input file cannot be found."
@gnachman gnachman closed this Jul 23, 2026
@YSaxon

YSaxon commented Jul 23, 2026

Copy link
Copy Markdown
Author

Funny you should close it now. I'm actually getting much closer to something stable and coherent. I was planning to wait to ping you about my more recent work on it until I had it ready - it needs bug fixes and efficiency refactors, but I think it's actually going in a positive direction

@gnachman gnachman reopened this Jul 23, 2026
@gnachman

Copy link
Copy Markdown
Owner

I do think that a spec would be good to create - this is a pretty big change and it needs to be thought through holisitically. Reopening it for now

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.

3 participants