Skip to content

Add folder (non-git) repository support + harden removal pipeline#257

Merged
sbertix merged 10 commits intomainfrom
sbertix/folder-clean
Apr 19, 2026
Merged

Add folder (non-git) repository support + harden removal pipeline#257
sbertix merged 10 commits intomainfrom
sbertix/folder-clean

Conversation

@sbertix
Copy link
Copy Markdown
Collaborator

@sbertix sbertix commented Apr 19, 2026

Summary

  • Adds non-git folder repositories: open any directory through the repo picker and it renders as its own sidebar section with a single synthetic main worktree. Terminal tabs, hotkeys, scripts, command palette, and deeplinks all reuse the existing SidebarSelection / WorktreeTerminalManager plumbing. Git-only actions (archive, pin, unpin, new worktree) surface a folder-incompatible alert instead of silently no-op-ing.
  • Refactors the sidebar UI into shared SidebarItem* views (WorktreeRowSidebarItemView, WorktreeRowsViewSidebarItemsView) so git and folder rows share rendering.
  • Rationalizes the delete pipeline around a typed batch aggregator (ActiveRemovalBatch, RepositoryRemovalRecord, DeleteDisposition). Bulk deletes now fire one terminal .repositoriesRemoved per batch, coalesce parallel trash failures into a single alert, and can't clobber concurrent git-section and folder batches.
  • Splits RemovalOutcome.failure into .failureSilent (caller owns the alert — blocking-script, cancel, kind-flip) and .failureWithMessage(String) (aggregator owns the alert — trashItem errors). Makes the alert-clobber scenario unrepresentable: a script-failure alert sharing a batch with a trash failure is preserved; the trash errors are logged via repositoriesLogger.warning instead of overwriting the UI.
  • Prunes removingRepositoryIDs + activeRemovalBatches in applyRepositories symmetric with the seven other reload-intersected trackers.
  • Surfaces missing-on-disk folder roots through the same LoadFailureSidebarFailedRepositoryRow pipeline git repos already use, via a new gitClient.rootDirectoryExists dependency. A folder deleted from Finder while Supacode was running now shows the error row instead of an empty folder.
  • Logs ambiguous damaged-bare-clone paths (lastComponent ends in .git but HEAD/objects/refs trio is incomplete) in Repository.isGitRepository(at:) so the otherwise-silent misclassification is observable in telemetry without widening the classifier and creating false positives.
  • Dedups the folderIncompatibleAlert copy across the reducer helper and the AppFeature deeplink handler via FolderIncompatibleAction.alertCopy.
  • Moves the seedRemovalBatch test helper out of #if DEBUG in the main module and into the test target where @testable import supacode already exposes the internal state fields.
  • Adds .help() to the Folder Settings context-menu button to match CLAUDE.md's tooltip mandate.
  • Prunes settingsFile.repositories entries when a repository is removed so the script / run-config / open-action dicts don't leak forever.

Test plan

  • make check (swift-format + swiftlint, strict mode)
  • make test — 849 tests pass, 12 known issues unchanged from the branch baseline
  • Open a non-git folder via the repo picker; confirm it renders a section with one selectable row
  • Hit Archive / Pin / Unpin on a folder row via hotkey and via deeplink — both show the folder-incompatible alert with identical copy
  • Bulk-delete two folders where one script fails and the other's trash throws — script-failure alert stays visible; trash failure is logged
  • Delete a folder from Finder while Supacode is running — refresh the sidebar — folder row shows the failed-repository error row with a "may have been moved or deleted" message
  • Rapid double-tap Delete on a folder with an empty delete script — second tap is a no-op, no duplicate analytics
  • Trigger a reload while a folder trash is in-flight — batch still drains; state stays consistent

Closes #252

sbertix added 10 commits April 19, 2026 14:26
Classify repositories as git or folder at load time via
`Repository.isGitRepository(at:)` (checks `.git` dir/file and the
`.bare` / `.git` root-name conventions). Classification runs through
`GitClientDependency.isGitRepository` so tests can override without
touching the filesystem.

A folder-kind repository has exactly one synthesized "main"
`Worktree` with `id = "folder:" + path` (see
`Repository.folderWorktreeID(for:)`), `workingDirectory == rootURL`.
Selection and terminal binding reuse the standard
`SidebarSelection.worktree(id)` machinery — nothing git-specific
runs for folders.

Folder-remove alert offers three buttons: "Remove from Supacode"
(stop managing, disk untouched), "Delete from disk" (routes through
`FileManager.trashItem`), and "Cancel". Both routes flow through
the existing `.requestDeleteWorktree` →
`.deleteWorktreeConfirmed` → `.deleteScriptCompleted` pipeline;
handlers branch on intent so `gitClient.removeWorktree` is never
called for folders, and `removingRepositoryIDs: [Repository.ID:
RemovalIntent]` carries the routing intent even if a `git init`
flips `isGitRepository` mid-delete (the kind flip surfaces a
user-visible error).

Settings hides Setup and Archive script sections for folders;
Delete Script and user-defined scripts stay. `openRepositorySettings`
(context menu + deeplink) routes folders to `.repositoryScripts`
since there is no general pane for them.

Guards added where git-only behavior would be wrong:
`worktreesForInfoWatcher()` filters out folders; command palette
dedupes folder rows to `Foo` instead of `Foo / Foo`; worktree
deeplinks (`.archive`, `.unarchive`, `.pin`, `.unpin`) reject folder
targets with an explanatory alert;
`createRandomWorktreeInRepository` /
`createWorktreeInRepository` / `.repoWorktreeNew` deeplink reject
folders up front. `repository_removed` analytics is tagged with
`kind`. UI copy flipped to "Add Repository or Folder" across menus,
buttons, tooltips, and shortcut help.

Tests cover classification, immediate appearance after
`.openRepositories`, mixed git+folder load, watcher filter, both
deletion paths (`.requestRemoveRepository` and
`.requestDeleteWorktree`), blocking-script success + failure +
cancellation, the kind-flip race, the folder-guard on
new-worktree creation, the unified removal intent, and the
Delete-from-disk path. `AGENTS.md` documents the invariants.
Route folder repositories through the same `SidebarItemView` /
`SidebarItemsView` / `SidebarItemContextMenu` stack as git
worktrees by introducing `SidebarItemModel.Kind` (.git / .folder).
Folders inherit the standard shimmer, running-script ping dot,
notification badge, and archiving/deleting icon flips for free;
`SidebarItemView` short-circuits on `.folder` to render an SF
`folder` icon at `.fontWeight(.semibold)` and skip branch-name /
PR / check-badge computation.

Rename `WorktreeRowModel` / `WorktreeRow` / `WorktreeRowsView` /
`WorktreeRowGroupView` / `WorktreeRowContainer` /
`WorktreeContextMenu` / `WorktreeRowSections` to
`SidebarItemModel` / `SidebarItemView` / `SidebarItemsView` /
`SidebarItemGroupView` / `SidebarItemContainer` /
`SidebarItemContextMenu` / `SidebarItemSections`, and promote
`SidebarRepositorySectionView` to `SidebarSectionView` behind a
new `SidebarRootView` dispatcher. The context menu renders
folder-specific copy inline ("Remove Folder…", "Archive Folder…"
disabled, "Folder Settings…") and drops "Copy as Branch Name".

Folder sidebar sections wrap `SidebarItemsView` in a `Section { … }
header: { EmptyView() }` so the sidebar applies the same
inter-group padding used between git sections; the explicit
`header:` form keeps NSOutlineView's row counts consistent in
sidebar-style Lists (headerless `Section { … }` overload crashed
during the AutoLayout commit).
Before this commit, deleting a single item mostly worked but bulk
deletes — especially folder-row bulk unlink and section-level
"Remove Repository" on a git repo — were a patchwork of
partially-overlapping actions (`.requestDeleteWorktree`,
`.confirmDeleteSidebarItem`, `.confirmRemoveRepository`,
`.repositoryRemoved`, `folderRemovalEffect`). Each per-target
removal fired its own `.repositoryRemoved(id)` which triggered a
full async reload with `.cancellable(cancelInFlight: true)`, so
siblings cancelled each other mid-flight and only one repo
actually disappeared from the sidebar.

This commit redesigns the pipeline around four things:

1. **One closed-sum disposition**. `DeleteDisposition`
   (`.gitWorktreeDelete | .gitRepositoryUnlink | .folderUnlink |
   .folderTrash`) replaces the former 2×2 split between user
   `DeleteAction` and recorded `RemovalIntent`, which could encode
   impossible combinations like `(git worktree, .unlink)` and gave
   `.delete` two different meanings depending on target kind.

2. **Batch aggregator keyed by id**.
   `activeRemovalBatches: [BatchID: ActiveRemovalBatch]` lets
   overlapping flows (e.g. a folder-trash still awaiting
   `FileManager.trashItem` while the user confirms a git-section
   remove on a different repo) each complete independently.
   `removingRepositoryIDs: [Repository.ID: RepositoryRemovalRecord]`
   couples the per-repo disposition with its owning batch id so
   lookups and draining stay in lockstep. Bulk and single both go
   through the same plural entry points (single = batch-of-1);
   section-level removes seed a batch-of-1 too.

3. **One verb, one terminal**. The public surface collapses to
   `.requestDeleteSidebarItems` → `.confirmDeleteSidebarItems(_,
   disposition:)` → per-target `.deleteSidebarItemConfirmed` →
   `.repositoryRemovalCompleted` (aggregator drain) → a single
   `.repositoriesRemoved([ids])` terminal per batch. Partial
   failures drain the batch without producing a terminal for the
   failed targets; the aggregator clears `removingRepositoryIDs`
   for failed targets so the sidebar row becomes clickable again.
   `signalFolderRemovalFailure` helper collapses the three
   duplicated failure tails in `.deleteScriptCompleted` (cancel /
   non-zero exit / success-kind-flip) into one site.

4. **Loud invariants**. Orphan `.repositoryRemovalCompleted`
   arrivals (no matching record) call `reportIssue` and
   defensively clear per-worktree trackers so a future regression
   can't silently leak state. Kind / disposition mismatches
   (e.g. `.unlink` against a git target) also `reportIssue`
   instead of dropping silently.

UX improvements:
- Single-target main-worktree delete (palette, hotkey,
  context-menu) now surfaces the same "Delete not allowed" alert
  the deeplink path already shows instead of silently no-opping.
  Bulk selections that mix main + other worktrees keep silently
  filtering so the rest of the batch proceeds.
- Folder-removal confirmation copy fixed: singular branch now reads
  "…to stop managing the folder (it stays on disk)…" matching the
  plural branch; previous build shipped a grammatically broken
  "…to stop the folder…".

Tests (840 passing, 11 new):
- `concurrentFolderAndSectionBatchesEachCompleteIndependently`
  pins the overlap invariant — folder batch in-flight + concurrent
  section remove on a different repo must each produce their own
  `.repositoriesRemoved`.
- `orphanCompletionReportsIssueAndFiresSoloTerminal` verifies
  `reportIssue` fires and worktree trackers get defensively
  cleared.
- `requestDeleteMainWorktreeShowsNotAllowedAlertForSingleTarget`
  and `requestDeleteMainWorktreeInBulkRemainsSilentlyFiltered`
  split the old "silently filtered" assertion into the two
  behaviors we actually want.
- `bulkFolderUnlinkTerminatesWithEmptyState` (renamed from the
  misnamed original — it asserts termination, not the race).
- Exhaustivity-off tests restored key
  `store.receive(\.repositoriesRemoved)` /
  `\.delegate.selectedWorktreeChanged` assertions so future drops
  of the delegate fan-out don't pass silently.
- `State.seedRemovalBatch(pending:)` helper wires record + batch
  for tests that drop straight into `.deleteSidebarItemConfirmed`.

Also refreshed `AGENTS.md` folder-pipeline notes and stale
`.repositoryRemoved` references in tests after the verb rename.
Removing a repo from Supacode used to leave its per-repo config
block (scripts, run command, open action) orphaned in
`settings.json` under `repositories[<path>]` — only the
`repositoryRoots` array got pruned. Users who added and removed
folders while exploring saw the dict accumulate dead entries
forever.

Add `RepositoryPersistenceClient.pruneRepositoryConfigs(_ ids:)`
that drops the given ids from `settingsFile.repositories`, and
call it from both removal paths: `.repositoriesRemoved` (normal
removal) and `.removeFailedRepository` (failed-to-load cleanup).
The two side-effects in the aggregator terminal are chained
inside the same `.run` so they share the `CancelID.persistRoots`
cancellation scope.

Regression test: `folderRemovalPrunesRootsAndConfigsFromSettings`
asserts both `saveRoots` (pruned root list) AND
`pruneRepositoryConfigs` (removed repo id) fire on folder removal.
Tightens places where the new folder kind meets `isMainWorktree`
geometry or async invariant boundaries.

- Folder deeplink delete used to route into
  `deeplinkDeleteWorktreeEffect`'s `isMainWorktree` gate because
  folders have a synthetic main-worktree
  (`workingDirectory == rootURL`). CLI users got a misleading
  "main worktree not allowed" alert and couldn't remove folders
  via deeplink at all. Route folder targets through
  `.requestDeleteSidebarItems([target])` so the 3-button folder
  alert handles the confirmation.

- `folderRemovalEffect` swallowed `FileManager.trashItem` errors
  and still reported `succeeded: true`, so a failed "Delete from
  disk" silently removed the folder from Supacode while leaving
  the on-disk content in place. On catch, dispatch
  `succeeded: false` and present a "Delete from disk failed"
  alert with the error description.

- `.deleteScriptCompleted` derived `owningRepo` from
  `state.repositories.first(...)`; under a reload /
  `.removeFailedRepository` race the live repo could be gone and
  `signalFolderRemovalFailure` would bail out, orphaning the
  batch record forever. Rewrite the helper to resolve the repo id
  from `state.removingRepositoryIDs` (authoritative across
  reloads) using the `"folder:" + path` id convention, and drop
  the `owningRepo` parameter from call sites.

- `.deleteSidebarItemConfirmed` now `reportIssue`s when a folder
  target arrives without a seeded `RepositoryRemovalRecord` —
  turns the accidental gate the deeplink's isMainWorktree check
  used to provide into a load-bearing guard that survives the
  deeplink fix above.

- Folder archive / pin / unpin hotkeys used to silently no-op
  because the synthetic main-worktree satisfied `isMainWorktree`
  geometrically. Surface the same "Action not available" alert
  the deeplink layer already shows for these actions so hotkeys
  and the deeplink match.

- Git section-removal confirmation message no longer says
  "Worktrees and the main repository folder stay on disk" — the
  word "folder" is now overloaded with the folder-repo concept.
  Rewrote to "The repository and its worktrees stay on disk."

- Auto-delete of expired archived worktrees now `reportIssue`s
  and skips folder-synthetic worktree ids (`"folder:" + path`).
  Folders can't be archived by any user-reachable path today, but
  an archived-entry for a folder would hit the git delete path
  and fail confusingly — flag the invariant breach loudly.

Tests (845 passing, 4 new):
- `deleteFolderDeeplinkRoutesToFolderAlertPipeline`
- `requestArchiveWorktreeForFolderShowsActionNotAvailable`
  (covers archive / pin / unpin)
- `folderTrashFailureSurfacesAlertAndKeepsRepo`
- `orphanCompletionSucceededFiresSoloTerminalAndRemovesRepo`
  (companion to the existing succeeded=false test)
Two real bugs plus three polish items in the folder-removal
pipeline. Everything here lives in the delete flow that this
branch introduces; nothing touches repositories outside that
path.

- `.repositoryRemovalCompleted(succeeded: false)` cleared
  `removingRepositoryIDs` but left the per-worktree trackers
  (`deletingWorktreeIDs` / `deleteScriptWorktreeIDs`) populated.
  The empty-script folder-trash branch at
  RepositoriesFeature.swift:1855 seeds `deletingWorktreeIDs`
  before the effect fires; on trash failure the row rendered
  `.deleting(inTerminal: false)` forever. Aggregator now mirrors
  the orphan-path cleanup in its hot failure branch.

- `.deleteScriptCompleted` looked up `owningRepo` via
  `state.repositories.first(...)` — a concurrent reload /
  `.removeFailedRepository` pruning mid-script left the batch
  record orphaned and sibling folder targets hung forever. The
  exit=0 no-`owningRepo` branch now probes
  `removingRepositoryIDs[String(worktreeID.dropFirst("folder:".count))]`
  and routes through `signalFolderRemovalFailure` when a folder
  record is still there, so the batch drains even when the repo
  vanished from `state.repositories`.

- `.autoDeleteExpiredArchivedWorktrees` used to `reportIssue` on
  folder-prefixed archived ids and `continue` without purging,
  so every `.repositoriesLoaded` re-fired the same issue forever.
  Now collects stray folder-prefixed entries upfront, reports
  once, and purges them via `$sidebar.withLock` so the invariant
  self-heals on first encounter.

- Bulk folder-trash failures used to each dispatch
  `.presentAlert` and clobber `state.alert` in a last-write-wins
  race. `.repositoryRemovalCompleted` now carries an optional
  `failureMessage`; the aggregator collects them in
  `ActiveRemovalBatch.failureMessagesByRepositoryID` and surfaces
  one consolidated alert when the batch drains — single-target
  keeps the existing UX, multi-target lists every failed folder
  by name so users can see which stayed on disk.

- Four inlined "Action not available" alert constructions
  (RepositoriesFeature.swift archive/pin/unpin + AppFeature.swift
  deeplink) collapsed into one `folderIncompatibleAlert(action:)`
  helper on the reducer, plus a `FolderIncompatibleAction` enum
  that drives per-action copy (`"Archive not available"` /
  `"Archive only applies to git repositories."`) so the user knows
  which action they just tried.

- Removal-pipeline types + helpers moved to a new
  `RepositoriesFeature+Removal.swift` extension —
  `DeleteDisposition`, `RepositoryRemovalRecord`,
  `ActiveRemovalBatch`, `BatchID`, `FolderIncompatibleAction`,
  `seedRemovalBatch`, `folderRemovalEffect`,
  `signalFolderRemovalFailure`, `folderIncompatibleAlert`,
  `consolidatedTrashFailureAlert`,
  `confirmationAlertForRepositoryRemoval`, and `messageAlert`.
  The reducer body stays in the main file (Swift reducers can't
  split), but the main file drops from 4,613 to 4,488 lines and
  the removal domain is now self-contained in a 293-line file.

Tests (847 passing, +2):
- `bulkFolderTrashFailuresCoalesceIntoSingleAlert` — asserts the
  consolidated alert names both failed folders.
- `deleteScriptCompletedDrainsBatchWhenOwningRepoVanished` —
  exercises the repo-eviction race by seeding a batch for a repo
  that doesn't live in `state.repositories`.
- `folderTrashFailureSurfacesAlertAndKeepsRepo` strengthened to
  assert `deletingWorktreeIDs` / `deleteScriptWorktreeIDs` clear
  on failure.
- `requestArchiveWorktreeForFolderShowsActionNotAvailable`
  updated to match the new per-action copy.
… helpers

A consolidated trash-failure alert could silently vanish on the
same tick because downstream reducer actions unconditionally
cleared `state.alert`. Rooting that out turned up two
synergistic clear sites; both now leave the alert alone and let
confirmation-style alerts be cleared by their own confirm
handlers.

- Alert clobber: dropped the unconditional `state.alert = nil`
  from `.deleteSidebarItemConfirmed` and `.reloadRepositories`.
  `.confirmDeleteSidebarItems` already clears its own confirm
  alert at entry, and `.reloadRepositories` is a data-layer
  refresh that has no business wiping whatever alert a parallel
  flow just set. Regression test
  `deleteSidebarItemConfirmedDoesNotClobberTerminalAlert` pins
  the new contract: a programmatic `.deleteSidebarItemConfirmed`
  (the shape `.autoDeleteExpiredArchivedWorktrees` uses) must
  leave a pre-existing terminal alert intact.

- Cross-feature alert duplication: `FolderIncompatibleAction`
  grew a `.unarchive` case and the deeplink path at
  `AppFeature.swift:1029-1048` now builds its alert title/body
  from the shared `displayName`, so the inline switch that used
  to duplicate the copy word-for-word is gone.

- Prefix coupling: `Repository.folderWorktreeIDPrefix` +
  `repositoryID(fromFolderWorktreeID:)` + `isFolderWorktreeID(_:)`
  on `Repository` replace four hand-parsed `"folder:"` prefix
  tests scattered across the reducer and `+Removal.swift`.

- Typed outcome: `(succeeded: Bool, failureMessage: String?)` on
  `.repositoryRemovalCompleted` collapsed into a single
  `RemovalOutcome` sum (`.success | .failure(message: String?)`).
  The action can no longer express "success + failure message"
  and the aggregator reads `outcome.succeeded` /
  `outcome.failureMessage` directly.

- Alert fallback + fragile lookup: `consolidatedTrashFailureAlert`
  now takes a pre-resolved `namesByRepositoryID` map that the
  aggregator snapshots from `state.repositories` at drain time
  (before `.repositoriesRemoved` prunes anyone). Single- and
  multi-target copy share one `displayName(for:)` helper that
  falls back to `URL(fileURLWithPath: id).lastPathComponent`
  instead of the inconsistent "the folder" / raw-path split.

- Orphan cleanup scope: the orphan-path and hot-failure cleanups
  no longer iterate every worktree in `repository.worktrees` —
  they clear only the folder-synthetic worktree id derived from
  the repo id. The old wider sweep was safe today (only folder
  dispositions reach `.failure`) but would have quietly clobbered
  sibling git worktree trackers if a future caller ever fed a
  git repo id through this path.

- Test hygiene: `State.seedRemovalBatch(pending:id:)` is now
  `#if DEBUG`-only so production callers can't accidentally
  corrupt the removal state machine.

Tests (848 passing, +1). Build green.
- Split RemovalOutcome.failure into .failureSilent / .failureWithMessage(String)
  so the aggregator can't clobber a caller-owned alert (script failure, user
  cancel, kind-flip) with the consolidated trash-failure alert. When the
  batch drains with both a silent failure and trash messages, preserve the
  caller's alert and log the trash errors via repositoriesLogger.warning
  instead of overwriting.
- Use switch outcome at the aggregator and orphan-completion call sites
  instead of if outcome.succeeded / if let outcome.failureMessage, restoring
  exhaustiveness at the critical drain path.
- Drain the delete re-entry guard as a silent failure when the guard trips
  for a folder that has already been seeded into the batch aggregator,
  preventing the batch from hanging forever on a pending target.
- Prune removingRepositoryIDs + activeRemovalBatches in applyRepositories,
  symmetric with the seven other trackers intersected against the live
  roster; report the invariant break via reportIssue so tests catch future
  regressions.
- Log ambiguous .git-suffixed directories (missing HEAD / objects / refs)
  in Repository.isGitRepository(at:) so damaged bare clones are observable
  in telemetry without widening folder classification.
- Dedup the folder-incompatible alert copy via
  FolderIncompatibleAction.alertCopy so AppFeature's deeplink handler and
  the reducer's folderIncompatibleAlert share one source of truth.
- Move the seedRemovalBatch test helper out of #if DEBUG in the main module
  and into the test target, where @testable import supacode already gives
  it access to the internal state fields.
- Add .help() tooltip to the Folder Settings context-menu button.
- Revert the delete re-entry guard drain introduced in the previous
  commit. The guard correctly short-circuits rapid second taps whose
  first-tap `.repositoryRemovalCompleted` is already going to drain
  the batch; emitting `.failureSilent` here double-drained pending
  and orphaned the first completion into reportIssue, breaking the
  idempotency regression test.
- Silence `prunedRemovalTrackers` in `applyRepositories`. Firing
  reportIssue on every reload-during-removal flow also caught the
  synchronous `.gitRepositoryUnlink` path that tests seed
  deliberately, producing false failures. The symmetric prune still
  keeps state consistent with the seven other trackers intersected
  against the live roster; the orphan-completion branch in
  `.repositoryRemovalCompleted` already handles real liveness.
- Extract the prune into `prunedRemovalTrackers` so applyRepositories
  stays under the 100-line function-body limit enforced by swiftlint.
- Wrap the delete-confirmation subject line into a local binding so
  it fits within the 120-character line limit.
- Delete the unused `makeFolderFixture` test helper; its tuple return
  tripped the large-tuple lint rule and nothing actually called it.
- Rewrite the damaged-bare-clone HEAD write in
  `damagedBareCloneClassifiesAsFolderAndWarns` to use the
  non-optional `Data(_:)` initializer per the
  `non_optional_string_data_conversion` lint rule.
A folder-kind root that got deleted / moved / unmounted while Supacode
was running silently became an empty folder repository in the sidebar:
gitClient.isGitRepository returned false for the missing path (the
FileManager checks inside it all miss), loadRepositoriesData took the
non-git branch, and synthesized a folder row with no indication that
the directory was gone. Git repos already surface this via
loadFailuresByID → SidebarFailedRepositoryRow; folders did not.

Add a `rootDirectoryExists` closure to GitClientDependency so the
loader can distinguish "directory is gone" (failure row) from
"directory exists but isn't a git repo" (folder row). The live
implementation uses FileManager.fileExists on the standardized path;
testValue defaults to `true` so fixtures with fake /tmp paths keep
exercising the classification branches they were written for.

Route the missing-directory case through the same LoadFailure
pipeline git failures use, so the sidebar renders the familiar error
row with a "this directory may have been moved or deleted" message.

Covered by a new regression test that stubs rootDirectoryExists to
return false and asserts the loader emits a loadFailuresByID entry
instead of a synthesized folder repository.
@sbertix sbertix enabled auto-merge (squash) April 19, 2026 21:17
@tuist
Copy link
Copy Markdown

tuist Bot commented Apr 19, 2026

🛠️ Tuist Run Report 🛠️

Builds 🔨

Scheme Status Duration Commit
supacode 34.7s 3b2af3a1e

@sbertix sbertix merged commit 68e4496 into main Apr 19, 2026
2 checks passed
@sbertix sbertix deleted the sbertix/folder-clean branch April 19, 2026 21:19
@bjufre
Copy link
Copy Markdown

bjufre commented Apr 21, 2026

After this update Supacode stopped detecting my bare repo as such and just shows a folder, and after the restart I lost the layout and panes I was working and stuck back to Zellij and old plain Ghostty 🙏 😢

@sbertix
Copy link
Copy Markdown
Collaborator Author

sbertix commented Apr 21, 2026

After this update Supacode stopped detecting my bare repo as such and just shows a folder

Sorry to hear that. Could you give me some more info on the structure of the repo so I can add some more regression testing? @bjufre

and after the restart I lost the layout and panes

When repos are rendered as folders they don't lose their metadata, they just hide all info which is no longer relevant, so in theory if you haven't made any other changes it should be back once it's fixed.

@sbertix
Copy link
Copy Markdown
Collaborator Author

sbertix commented Apr 21, 2026

I've started a new release extending the name convention checks we used before, to rely more on the folder structure. 0.8.3 is being released rn. If you can try it to see if it fixes the issue, it would be greatly appreciated 🙇‍♂️ @bjufre

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.

Feature Request: Possibility to use outside of a git repo

2 participants