Skip to content

chore: merge main into develop ahead of v0.9.0 release#228

Merged
eyelock merged 64 commits into
developfrom
chore/merge-main-into-develop
Apr 27, 2026
Merged

chore: merge main into develop ahead of v0.9.0 release#228
eyelock merged 64 commits into
developfrom
chore/merge-main-into-develop

Conversation

@eyelock
Copy link
Copy Markdown
Owner

@eyelock eyelock commented Apr 25, 2026

Summary

Resolves the divergence between main and develop that accumulated during the 0.9 beta cycle. Without this, the release PR (#226) shows as conflicting on GitHub.

Resolution strategy

  • Swift source files, tests, localization: develop's version wins — it has the complete 0.9.0 implementation including all DI refactoring
  • Docs/appcast.xml, Docs/appcast-beta.xml: main's version wins — it's the authoritative record of what's been shipped

After merging

#226 (develop → main release PR) will become conflict-free and ready to mark as ready for review.

🤖 Generated with Claude Code

eyelock and others added 30 commits April 16, 2026 10:05
Switches from the legacy Login Keychain (binary-hash ACL) to the Data
Protection Keychain (kSecUseDataProtectionKeychain: true), which ties
access to the app's bundle ID + Team ID. This prevents the recurring
"app requires access to a key in your keychain" prompt after a rebuild,
which broke the binary-hash ACL.

One-time migration: on first access the legacy entry is read, migrated
to the new keychain, and the legacy entry is deleted.

Co-authored-by: David Collie <support@eyelock.net>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
TmuxMultiPaneContainerNSView.update() was unconditionally calling
removeFromSuperview() + addSubview() on the active pane view during every
render pass. This clears NSWindow's first responder (AppKit calls
resignFirstResponder when a view leaves the window) and does not restore it,
because the focus-scheduling guard in updateNSView only fires when activePaneId
changes, not on subsequent same-pane renders.

For stable existing terminals this was rarely noticed — the session has no
rapid state changes. For new terminals created from the sidebar, the tmux
control session fires a burst of updates while connecting, causing each
successive render to drop focus. The pane appeared unfocused (and clicking
did nothing because the pane was losing focus again immediately).

Fix: skip the reorder when the active pane is already the topmost pane view
(last subview before the overlay). The z-order is preserved in all cases:
- Active pane changes → pane is no longer last → reorder fires
- New sibling pane added → new pane becomes last → active is no longer last → reorder fires
- Same active pane, no new siblings → pane is already last → no reorder, focus preserved

Co-authored-by: David Collie <support@eyelock.net>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
)

NSOpenPanel.runModal() cannot nest inside a SwiftUI sheet's modal session
on macOS Sequoia — the panel either silently fails or immediately dismisses.

Switch to panel.begin(_:), which presents the panel as a floating window
outside the current modal session. The completion handler runs on the main
thread, so updating the @binding path is safe. All callers of PathInputField
(AddRepositorySheet, EditRepositorySheet, NewWorktreeSheet, CheckoutBranchSheet)
get the fix automatically.

Co-authored-by: David Collie <support@eyelock.net>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
fix: Hotfix v0.7.3 — security, focus, and Sequoia compatibility
Branch protection on main requires status checks and blocks direct pushes.
The workflow now creates a hotfix/appcast-update branch, opens a PR, and
enables auto-merge. The hotfix/* branch name satisfies protect-main.yml,
and appcast-only changes pass All Clear (code checks skip via paths-filter).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Auto-generated appcast files for Sparkle auto-updates.

Co-Authored-By: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Rewrites the control mode parser's byte ingestion to eliminate a class
of UTF-8 corruption that caused TUI apps (Claude Code, htop, etc.) to
render blank or visually corrupted screens.

Root cause: the previous `parse(_ data: Data)` decoded the entire pipe
buffer as a UTF-8 string before splitting on newlines. When a
`%extended-output` data section ended with the lead byte of a multi-byte
sequence (e.g. 0xE2 for U+2500 ─) and the continuation bytes appeared at
the start of the next line's data section, the decode walk-back would
either stall (growing the buffer unbounded until the control client was
killed) or silently drop those bytes. Box-drawing characters in TUI
borders were a frequent trigger; a full-screen render could hit this on
every repaint.

Fix:
- `parse(_ data: Data)` now splits `rawBuffer` on 0x0A at the raw byte
  level. In tmux control mode 0x0A is always a line terminator — data
  newlines are octal-encoded as `\012` — so this split is always safe.
- `processRawOutputLine` + `decodeTmuxOutputBytes` process the data
  section of `%output` and `%extended-output` lines entirely at the byte
  level. SwiftTerm's `feed(byteArray:)` handles multi-byte UTF-8 assembly
  across calls internally, so the bytes arrive intact.
- Enables `refresh-client -f pause-after=1` (the tmux backpressure
  pause/continue protocol) so that output bursts that fall behind trigger
  a graceful `%pause`/`%continue` cycle instead of killing the client
  after five minutes of lag.
- Implements `handlePause`, `handleContinue`, and the `onPausePane`
  callback to respond to `%pause %<pane-id>` with
  `refresh-client -A %<pane-id>:continue`.
- Changes `default-terminal` from `xterm-256color` to `tmux-256color`
  per the tmux FAQ.
- Enables `allow-passthrough` for DCS sequence forwarding.

Co-authored-by: David Collie <support@eyelock.net>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Auto-generated appcast files for Sparkle auto-updates.

Co-Authored-By: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Auto-generated appcast files for Sparkle auto-updates.

Co-Authored-By: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
…tracking

Two bugs that broke selection when terminal apps (e.g. Claude Code) enable
mouse tracking:

1. **Text selection wiped during streaming** — SwiftTerm calls selectNone()
   on every linefeed when allowMouseReporting=true. Fix: set
   allowMouseReporting=false on first drag event (preventing SwiftTerm from
   forwarding drags to the PTY and from clearing selection on each new line).
   Restore on the next mouseDown so subsequent clicks still reach the app.

2. **Cmd+C copies empty string** — SwiftTerm's keyDown sets selection.active=false
   unconditionally before invoking copy: via interpretKeyEvents. Fix: intercept
   Cmd+C in the existing keyDown local monitor, call copy: before keyDown fires
   (while selection is still active), then consume the event.

Also fixes scroll events being converted to arrow keys when the terminal app
has enabled mouse tracking. Previously any alternate-buffer app received arrow
keys on scroll regardless of mouseMode; now the conversion only fires when
mouseMode==.off (i.e. the app has NOT set up its own mouse event handling).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Auto-generated appcast files for Sparkle auto-updates.

Co-Authored-By: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
…tracking

fix(terminal): Fix text selection and scroll in terminals with mouse tracking
Auto-generated appcast files for Sparkle auto-updates.

Co-Authored-By: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
chore: Update appcast for v0.8.0-beta.3
Auto-generated appcast files for Sparkle auto-updates.

Co-Authored-By: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Auto-generated appcast files for Sparkle auto-updates.

Co-Authored-By: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Auto-generated appcast files for Sparkle auto-updates.

Co-Authored-By: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Auto-generated appcast files for Sparkle auto-updates.

Co-Authored-By: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Auto-generated appcast files for Sparkle auto-updates.

Co-Authored-By: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
…orward-port rule (#164)

protect-main.yml: adds release/* as a permitted source alongside develop
and hotfix/* — needed for release resolution branches when develop→main
has conflicts.

Release skill: marks the appcast forward-port step as MANDATORY in both
stable and hotfix procedures, with an explicit note that the update-appcast
workflow commits directly to main and those changes must be synced back.

Co-authored-by: David Collie <support@eyelock.net>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(security): Migrate SecureStorage to Data Protection Keychain (#124)

Switches from the legacy Login Keychain (binary-hash ACL) to the Data
Protection Keychain (kSecUseDataProtectionKeychain: true), which ties
access to the app's bundle ID + Team ID. This prevents the recurring
"app requires access to a key in your keychain" prompt after a rebuild,
which broke the binary-hash ACL.

One-time migration: on first access the legacy entry is read, migrated
to the new keychain, and the legacy entry is deleted.

Co-authored-by: David Collie <support@eyelock.net>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ui): Scroll tab bar to reveal selected terminal on sidebar jump (#125)

When a terminal is selected from the worktree popover in the sidebar,
the tab bar now scrolls to bring that tab into view. Uses ScrollViewReader
with .id(tabCard.id) on each tab item and .onChange(of: card.id) to call
proxy.scrollTo with .center anchor on every card change.

Co-authored-by: David Collie <support@eyelock.net>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* ci: Skip Build Release job on PRs targeting develop (#127)

Build Release validates the release configuration compiles cleanly.
  This is only meaningful before landing on main (stable release path)
  or on hotfix branches. PRs into develop don't need it — the beta
  release workflow in release.yml runs its own full build when tagging.

  Removes the bottleneck that was blocking develop PR merges waiting
  for an ~5 minute macOS release build.

  Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Co-authored-by: David Collie <support@eyelock.net>

* fix(ui): Scroll tab bar to reveal selected terminal on sidebar jump (#125) (#128)

When a terminal is selected from the worktree popover in the sidebar,
the tab bar now scrolls to bring that tab into view. Uses ScrollViewReader
with .id(tabCard.id) on each tab item and .onChange(of: card.id) to call
proxy.scrollTo with .center anchor on every card change.

Co-authored-by: David Collie <support@eyelock.net>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(security): Migrate SecureStorage to Data Protection Keychain (#124) (#129)

Switches from the legacy Login Keychain (binary-hash ACL) to the Data
Protection Keychain (kSecUseDataProtectionKeychain: true), which ties
access to the app's bundle ID + Team ID. This prevents the recurring
"app requires access to a key in your keychain" prompt after a rebuild,
which broke the binary-hash ACL.

One-time migration: on first access the legacy entry is read, migrated
to the new keychain, and the legacy entry is deleted.

Co-authored-by: David Collie <support@eyelock.net>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(focus): Preserve tmux pane focus across SwiftUI re-renders (#133)

TmuxMultiPaneContainerNSView.update() was unconditionally calling
removeFromSuperview() + addSubview() on the active pane view during every
render pass. This clears NSWindow's first responder (AppKit calls
resignFirstResponder when a view leaves the window) and does not restore it,
because the focus-scheduling guard in updateNSView only fires when activePaneId
changes, not on subsequent same-pane renders.

For stable existing terminals this was rarely noticed — the session has no
rapid state changes. For new terminals created from the sidebar, the tmux
control session fires a burst of updates while connecting, causing each
successive render to drop focus. The pane appeared unfocused (and clicking
did nothing because the pane was losing focus again immediately).

Fix: skip the reorder when the active pane is already the topmost pane view
(last subview before the overlay). The z-order is preserved in all cases:
- Active pane changes → pane is no longer last → reorder fires
- New sibling pane added → new pane becomes last → active is no longer last → reorder fires
- Same active pane, no new siblings → pane is already last → no reorder, focus preserved

Co-authored-by: David Collie <support@eyelock.net>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ui): Use async panel.begin() for Browse button in path pickers (#132)

NSOpenPanel.runModal() cannot nest inside a SwiftUI sheet's modal session
on macOS Sequoia — the panel either silently fails or immediately dismisses.

Switch to panel.begin(_:), which presents the panel as a floating window
outside the current modal session. The completion handler runs on the main
thread, so updating the @binding path is safe. All callers of PathInputField
(AddRepositorySheet, EditRepositorySheet, NewWorktreeSheet, CheckoutBranchSheet)
get the fix automatically.

Co-authored-by: David Collie <support@eyelock.net>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(dev): Debug build shows SHA in About panel; document debug behaviors (#131)

Makefile: set CFBundleVersion to "<SHA>-debug" for debug builds instead of
the marketing version string. The standard About panel previously showed
"0.7.2 (0.7.2)" — now shows "0.7.2 (446ee59-debug)", making it
immediately clear which commit is running and that it's a debug build.

CONTRIBUTING.md: add "Debug Build Behavior (TERMQ_DEBUG_BUILD)" subsection
documenting how the debug and release builds differ — ad-hoc signing, file-
based encryption key storage, separate bundle ID and data directory, and
the About panel build number format. Explains why the Data Protection
Keychain is not used in debug builds.

Co-authored-by: David Collie <support@eyelock.net>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(security): Use file-based key storage in debug builds (#130)

The Data Protection Keychain requires a Team ID or keychain-access-groups
entitlement. Ad-hoc signed debug builds have neither, causing a
"net.eyelock.termq.secrets wants to access confidential information" prompt
on every rebuild as the binary hash ACL invalidates.

Guard the keychain code with #if TERMQ_DEBUG_BUILD. In debug builds a 0o600
file (.enc-key in the config directory) holds the 256-bit key. The secrets
file stays AES-GCM encrypted; only the key storage changes. Release builds
continue to use the Data Protection Keychain (kSecUseDataProtectionKeychain)
with one-time migration from the legacy Login Keychain.

Co-authored-by: David Collie <support@eyelock.net>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ui): Git worktree sidebar (#126)

* feat(ui): Add git worktree sidebar

Adds a collapsible sidebar panel showing registered repositories and
their git worktrees, with full lifecycle management.

Repository management:
- Add/edit/remove repositories with name, path, and worktree base path
- Worktree base path auto-populated from {repo}/.worktrees; user can override
- Checkbox to add base path to .gitignore when nested inside the repo
- Base path validation blocks paths equal to or parent of the repo root
- Persist expanded/collapsed state per repo across sessions

Worktree display and navigation:
- List all worktrees per repo with branch name, short commit hash, dirty indicator
- Dual-slot left icon: status badge (house/lock) + terminal count with popover
- Popover lists open terminals for each worktree; tap to jump to terminal
- Dirty state detected via DispatchSource file watcher (.git/HEAD, .git/index)
  plus a 15-second poll for working-tree edits that don't touch git metadata

Worktree actions:
- Create worktree: typeahead branch picker, inferred path, baseBranch selection
- Remove worktree (git worktree remove) and force-delete with confirmation alerts
- Lock/unlock worktree via context menu
- Prune stale worktree refs with dry-run preview sheet
- Reveal in Finder, Open in Terminal (Terminal.app), copy path
- Open remote branch/commit on GitHub/GitLab from commit hash and branch name

Terminal integration:
- New Terminal button per worktree row opens terminal at that path
- Create Terminal (add to board without switching) via context menu

Git state monitoring:
- GitRepositoryMonitor: DispatchSource watches .git/HEAD and .git/index for
  all worktrees; fires refresh on any change from any process
- NSTableView reentrant delegate fix: RepoDisclosureView owns @State for
  expanded/collapsed so ViewModel mutations fire via .onChange, not during render

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ui): Add local branches section to worktree sidebar

Local branches that are not already checked out as a worktree now appear
in a collapsible "Local Branches" disclosure group below each repo's
worktree list. The section is automatically filtered — a branch disappears
from the list as soon as a worktree is created for it.

New entry points to create a worktree from an existing branch:
- Right-click a branch row → New Worktree (pre-selected, confirm path)
- Right-click the repo row → New Worktree from Branch… (picker)
- Right-click the main worktree → New Worktree from Branch… (picker)

Under the hood this runs `git worktree add <path> <branch>` (checks out
the existing branch without creating a new one), distinct from the
existing New Worktree action which always creates a fresh branch with -b.

Also fixes tab bar not scrolling when a new terminal is created from the
sidebar (adds .onAppear to the ScrollViewReader so the initial appearance
case is handled alongside .onChange). Adds focus-event tracing via
TermQLogger.focus for diagnosing the remaining first-responder issue.

Documentation: Tutorial section 12.8 added; sections renumbered.
Localization: 7 new keys, English fallbacks seeded in all 39 non-English locales.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs: Add Tutorial 8 — Git Worktrees (Power Workflows)

Full tutorial covering repo registration, worktree lifecycle (lock/remove/
force-delete/prune), terminal launch, and the Local Branches section.
Placed in Power Workflows as tutorial 8 with sequential numbering.

Includes all 10 screenshots (compressed with pngquant, -82–88%) and adds
a make compress-images target with a /push guard to prevent future
uncompressed images from slipping in.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: David Collie <support@eyelock.net>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* ci: Appcast workflow creates PR instead of pushing directly to main (#135)

Branch protection on main requires status checks and blocks direct pushes.
The workflow now creates a hotfix/appcast-update branch, opens a PR, and
enables auto-merge. The hotfix/* branch name satisfies protect-main.yml,
and appcast-only changes pass All Clear (code checks skip via paths-filter).

Co-authored-by: David Collie <support@eyelock.net>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* fix(dev): Disable Sparkle auto-updater in debug builds (#137)

* fix(dev): Disable Sparkle auto-updater in debug builds

Debug builds were initializing Sparkle with startingUpdater:true against
the production appcast. Since the debug version is always "older" than
the latest release, Sparkle would find an update and could wake the
release app via Launch Services — causing rogue windows during development.

- Pass startingUpdater:false when TERMQ_DEBUG_BUILD is set
- Hide "Check for Updates" menu item in debug builds
- Document the behavior difference in CONTRIBUTING.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style: Auto-format pre-existing #if block indentation

swift-format corrected indentation inside conditional compilation
blocks in SecureStorage.swift and minor comment formatting in tests.
Pre-existing issues surfaced by the format pass — no logic changes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: David Collie <support@eyelock.net>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* fix(tmux): Correct TUI rendering in tmux control mode terminals (#138) (#141)

Rewrites the control mode parser's byte ingestion to eliminate a class
of UTF-8 corruption that caused TUI apps (Claude Code, htop, etc.) to
render blank or visually corrupted screens.

Root cause: the previous `parse(_ data: Data)` decoded the entire pipe
buffer as a UTF-8 string before splitting on newlines. When a
`%extended-output` data section ended with the lead byte of a multi-byte
sequence (e.g. 0xE2 for U+2500 ─) and the continuation bytes appeared at
the start of the next line's data section, the decode walk-back would
either stall (growing the buffer unbounded until the control client was
killed) or silently drop those bytes. Box-drawing characters in TUI
borders were a frequent trigger; a full-screen render could hit this on
every repaint.

Fix:
- `parse(_ data: Data)` now splits `rawBuffer` on 0x0A at the raw byte
  level. In tmux control mode 0x0A is always a line terminator — data
  newlines are octal-encoded as `\012` — so this split is always safe.
- `processRawOutputLine` + `decodeTmuxOutputBytes` process the data
  section of `%output` and `%extended-output` lines entirely at the byte
  level. SwiftTerm's `feed(byteArray:)` handles multi-byte UTF-8 assembly
  across calls internally, so the bytes arrive intact.
- Enables `refresh-client -f pause-after=1` (the tmux backpressure
  pause/continue protocol) so that output bursts that fall behind trigger
  a graceful `%pause`/`%continue` cycle instead of killing the client
  after five minutes of lag.
- Implements `handlePause`, `handleContinue`, and the `onPausePane`
  callback to respond to `%pause %<pane-id>` with
  `refresh-client -A %<pane-id>:continue`.
- Changes `default-terminal` from `xterm-256color` to `tmux-256color`
  per the tmux FAQ.
- Enables `allow-passthrough` for DCS sequence forwarding.

Co-authored-by: David Collie <support@eyelock.net>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(harnesses): YNH harness sidebar — detect, install, launch, manage (#142)

* feat(harnesses): Phase 1 — YNH detection and empty sidebar tab

Add YNH toolchain detection service with binary discovery across
well-known install locations (including ~/.ynh/bin/ for GUI apps that
don't inherit shell $PATH). Introduce three-state YNHStatus enum
(missing/binaryOnly/ready) driven by `ynh paths --format json`.

Wire up a sidebar tab-switcher gated by feature flag + binary detection,
with placeholder content for the harnesses list (Phase 2). Add YNH
settings section with version/path display, resolved paths, and an
Advanced disclosure group for $YNH_HOME override.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(harnesses): Phase 2 — installed harnesses list and detail pane

Add the read-only harness browsing experience: sidebar list populated
from `ynh ls --format json`, rich detail pane with clickable dependency
cards, and mutual exclusivity between harness/terminal selection.

New types (TermQShared):
- Harness, HarnessProvenance, HarnessArtifactCounts, HarnessInclude,
  HarnessDelegate — Codable types shaped by `ynh ls` JSON output

New service:
- HarnessRepository — @mainactor ObservableObject singleton wrapping
  `ynh ls`, with auto-refresh on app focus and detection state changes

New views:
- HarnessRowView — sidebar row with name, version, vendor/artifact/source badges
- HarnessDetailView — header, info, artifact counts, dependency section
  with clickable git source links, pick pills, and Reveal in Finder

Navigation:
- Card selection and harness selection are mutually exclusive
- Close button restores the previously selected terminal card
- Grid button always returns to the board view
- Toolbar title shows harness name and vendor badge when selected

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(harnesses): Phase 3 — full detail pane with composition data

Source the detail pane from two YNH calls: `ynh info <name> --format json`
for identity/provenance/manifest, then `ynd compose <info.path>` for the
composed vendor-neutral view (resolved artifacts, hooks, MCP servers,
profiles, focuses). Merged in Swift into a single HarnessDetail.

New types (TermQShared):
- HarnessInfo — thin identity + provenance + raw manifest (JSONFragment)
- HarnessComposition — composed artifacts, includes with resolved status,
  hooks, MCP servers, profiles, focuses, counts
- HarnessDetail — TermQ-side merge of info + composition

Service:
- HarnessRepository gains fetchDetail(for:) with per-session caching,
  chaining ynh info → ynd compose; invalidation after mutations

Views:
- HarnessDetailView slimmed to header, info, artifacts; delegates to
  HarnessDetailCompositionView and HarnessDetailDependencyView
- All sections always visible with empty state hints for discoverability
- Composed includes show resolved/unresolved badges
- Manifest in collapsible DisclosureGroup at the bottom
- Git helpers extracted to reusable GitSourceLabel, GitActionButtons,
  GitPickPill, GitURLHelper

English strings added; other languages deferred to Phase 9.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(harnesses): Phase 4 — launch flow

Adds vendor picker, focus picker, working directory, and backend
selection to a HarnessLaunchSheet. Confirms creates a transient
TerminalCard running `ynh run <name>` with card tags for harness,
vendor, and focus.

- Vendor: decoded from `ynh vendors --format json` (YNH Phase 8);
  CodingKeys map name/display_name/cli/config_dir → Swift fields
- VendorService: straight JSONDecoder replace tabwriter text parser
- send-keys uses -l flag to suppress tmux key-name expansion; prompt
  is single-quoted to prevent shell metacharacter injection
- tmuxControl backend routed through sendInitCommandViaControlMode
  (polls for pane + resize gate before sending init command)
- Localized in 39 languages; also synced pre-existing harness string
  gaps (settings.ynh.*, common.clear, sidebar prune keys)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(harnesses): Phase 5 — worktree ↔ harness linkage

Persists worktree→harness associations in ynh.json via LocalYNHConfig
and YNHConfigLoader (NSFileCoordinator, mirrors RepoConfig pattern).
YNHPersistence observable singleton loads on startup and saves on mutation.

Sidebar: "Set Harness" context submenu on non-main worktrees; purple
puzzle-piece badge on linked rows navigates to harness detail.

Harness detail: "Linked Worktrees" section with live terminal count
badge and per-row launch button that pre-fills the working directory
in HarnessLaunchSheet.

BackupManager now includes ynh.json in its backup/restore sweep
alongside repos.json.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(harnesses): Phase 5 UX — launch from worktree, clickable terminal badge, harness filter

- Add "Launch [harness]" context menu item to worktree sidebar rows when a
  harness is linked, opening HarnessLaunchSheet pre-filled with the worktree path
- Move linked worktrees section to top of HarnessDetailView (below header)
- Terminal count badge in linked worktrees is now a clickable popover listing
  only harness-tagged terminals (filters by tag key/value, not just directory)
- Fix working directory timing bug: HarnessLaunchSheet now uses a custom init
  with State(initialValue:) so the correct path is set before first render

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(harnesses): Phase 6 — install sheet with search, Git, and sources tabs

Adds a three-tab HarnessInstallSheet (Search/From Git/Sources) backed by
HarnessSearchService and SourcesService. Install runs in a transient direct
terminal with auto-exit, triggering an automatic harness list refresh via
NotificationCenter when the session ends. Also adds a refresh button to the
Repositories sidebar header and auto-refreshes the Harnesses tab on switch.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(harnesses): Phase 7 — uninstall, update, and YNH error surfacing

Adds Uninstall (with confirmation alert showing linked worktree and terminal
warnings) and Update to the harness detail view (⋯ menu) and sidebar context
menu. Both operations run in transient direct terminals that auto-exit, with
NotificationCenter triggering list refresh and cache invalidation on
completion. Harness names are shell-quoted to handle spaces safely. YNH
command stderr is now surfaced in the detail error area instead of the
generic fallback message.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(harnesses): auto-close transient install/update/uninstall tab on success

The YNH subprocess now reports its exit code in the session-exited
notification; ContentView closes the transient tab when exit code is 0
and leaves it open otherwise so failure output remains readable.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs(harnesses): Phase 9 — tutorial, changelog, logging audit

Adds Tutorial 13 (Harnesses) with placeholder screenshots for later
replacement, wires it into the sidebar navigation, and records the
0.8 Harnesses feature work under Unreleased in CHANGELOG. Also gates
one VendorService warning behind fileLoggingEnabled so subprocess
stderr cannot leak to unified logging.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs(harnesses): drop version references in "What's next"; add real screenshots

Reword the forward-looking paragraph so it doesn't tie authoring to a
specific future version. Replace most placeholder PNGs with real
screenshots of the shipped UI; two detection-state images remain as
placeholders pending a broken-YNH environment to screenshot.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs(harnesses): drop binaryOnly detection state; compress tutorial images

Section 13.3 now describes two detection states (missing, ready). The
binaryOnly case is effectively unreachable with current YNH releases,
which auto-create ~/.ynh on first call, and the old advice to "run
ynh init" no longer applies. The corresponding placeholder PNG is
removed.

Screenshots compressed with pngquant at quality 75–90 — 7.5 MB total
down to 1.5 MB (~80% reduction) with no perceptible quality loss at
tutorial display sizes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(i18n): escape inner quotes around %@ and collapse multi-line values

Two mechanical encoding bugs were silently corrupting translations at runtime:

1. Unescaped ASCII double-quotes around %@ placeholders in 13 language
   files (27 lines across sidebar.remove.worktree.message %@,
   sidebar.worktree.delete.message %@, and a few zh-Hans keys). The
   .strings parser treated the inner " as a value terminator, truncating
   everything from %@ onward.
2. Multi-line expansion of \n\n in security.external.modification.message
   across all 39 non-English files. Real newlines terminated the value
   at the first fragment, losing both %@ substitutions.

No translation content changed — only encoding.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: David Collie <support@eyelock.net>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* fix(terminal): Fix text selection and scroll in terminals with mouse tracking (#147)

Two bugs that broke selection when terminal apps (e.g. Claude Code) enable
mouse tracking:

1. **Text selection wiped during streaming** — SwiftTerm calls selectNone()
   on every linefeed when allowMouseReporting=true. Fix: set
   allowMouseReporting=false on first drag event (preventing SwiftTerm from
   forwarding drags to the PTY and from clearing selection on each new line).
   Restore on the next mouseDown so subsequent clicks still reach the app.

2. **Cmd+C copies empty string** — SwiftTerm's keyDown sets selection.active=false
   unconditionally before invoking copy: via interpretKeyEvents. Fix: intercept
   Cmd+C in the existing keyDown local monitor, call copy: before keyDown fires
   (while selection is still active), then consume the event.

Also fixes scroll events being converted to arrow keys when the terminal app
has enabled mouse tracking. Previously any alternate-buffer app received arrow
keys on scroll regardless of mouseMode; now the conversion only fires when
mouseMode==.off (i.e. the app has NOT set up its own mouse event handling).

Co-authored-by: David Collie <support@eyelock.net>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(harnesses): Marketplace browser, harness wizard, and install/launch management (#148)

* fix(tmux): Fix recovery loop for full-stack harness sessions and deleted card badges

- TmuxManager: add renameSession(from:to:) to permanently bind a tmux session
  to its card UUID after recovery, preventing duplicate-card accumulation on
  restart
- BoardViewModel+TmuxRecovery: use session.cardIdPrefix (UUID) for title
  matching instead of session.name; call renameSession after recovery so
  future restarts match by UUID automatically
- WorktreeSidebarView: exclude deleted/binned cards from worktree terminal
  badge count

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(harnesses): Marketplace browser, harness wizard, and install/launch management

- Vendor marketplace browser: browse, filter, and install harness plugins
  from configured vendor marketplaces
- Harness wizard: guided multi-step flow for creating new harnesses with
  artifact selection via HarnessIncludePicker (auto-advances past empty steps)
- Harness detail view: full composition, dependencies, artifacts, hooks,
  MCP servers, profiles, and focuses; Configure from Marketplaces CTA
- Three-tab install sheet: Search (registry), From Git (URL+subpath), Sources
  (local); pre-populated with installed harnesses before search term entered
- External Sources settings tab (renamed from Marketplaces): manage vendor
  marketplaces and YNH registries side-by-side
- HarnessDetailView ··· menu: Copy Run Command, YNH Documentation link
- Sidebar row context menu: Copy Run Command
- Sidebar tab resets to Repositories on first launch only (not every appear)
- tmux: fix recovery loop for full-stack harness sessions and deleted card badges

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: David Collie <support@eyelock.net>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ui): UX polish, marketplace improvements, and terminal enhancements (#151)

* fix(terminal): Fix text selection and scroll in terminals with mouse tracking

- Intercept mouse events to allow drag-selection even when terminal
  mouse reporting is active
- Restore allowMouseReporting on next click after a drag-selection
  so reporting works normally again for TUI apps

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(harnesses): Marketplace browser, harness wizard, and install/launch management

Marketplace:
- Marketplace sidebar tab with index browser, search, and plugin rows
- Add Marketplace sheet for registering custom Git-backed indexes
- Known defaults: claude-plugins-official and eyelock/assistants
- MarketplaceFetcher fetches and caches indexes, lazy-loads skill details
- MarketplaceStore holds the live list; persisted in ~/.termq/marketplaces.json

Harness wizard:
- HarnessWizardSheet guides creation of a new harness from scratch
- HarnessIncludePicker selects and includes marketplace plugins into a harness,
  either standalone or pre-targeted to a specific harness (wizard mode)
- HarnessAuthor service handles file writes and ynd include invocations

Install and launch:
- HarnessInstallSheet and HarnessLaunchSheet drive ynh install and run flows
- BinView shows deleted (soft-removed) cards with restore and permanent-delete

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ui): UX polish — harness sidebar, marketplace, board navigation, and toolbar

Repositories sidebar:
- Primary click on worktree row launches linked harness (auto) or opens terminal
- Repo header and main worktree row gain Set Harness in context menu
- Jigsaw badge is three-state: green (repo default), orange (worktree override), dim (inherited)
- Launch <harness> is first in context menu when a harness applies

Marketplace sidebar:
- Refresh-all button in header; eyelock/assistants added to known defaults
- Button order matches Repositories and Harnesses tabs (+ then ↺)
- Remove premature "Configure from Marketplaces" button from HarnessDetailView

YNH improvements:
- Settings > Tools shows "YNH Documentation →" link when ynh is not installed
- "Export as Marketplace…" in harness context menu runs ynd export in a transient terminal

Window title and toolbar:
- navigationTitle always "TermQ" — no flicker when switching between board and terminal view
- Navigation icon images fixed to frame(width: 16) so title position is stable
- Terminal button always visible in board view (disabled when no tabs)

Help view:
- Strip non-http link attributes in parseInlineMarkdown (prevents error -50 on internal links)

Settings — Safe Paste default:
- New "Enable Safe Paste for New Terminals" toggle in Data & Security
- All four card-creation paths (addTerminal, quickNewTerminal, newTerminal(at:), duplicate)
  read defaultSafePaste from UserDefaults; Board.addCard gains safePasteEnabled parameter

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(harness): Separate repo default harness from worktree override storage

Root cause: repo default and main worktree override shared the same key
(repo.path == main worktree path) in worktreeHarness, so clearing one
cleared the other.

Fix: LocalYNHConfig gains a repoHarness dictionary. Repo defaults are stored
there; worktree overrides (including the main worktree) stay in worktreeHarness.
The two maps never collide.

- LocalYNHConfig: repoHarness field with backward-compatible decoder default
- YNHPersistence: repoDefaultHarness(for:) and setRepoDefaultHarness(_:for:)
- removeAllAssociations clears both maps

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(terminal): Cmd+click to open links and file paths from the terminal

Implements requestOpenLink on SessionDelegate, replacing SwiftTerm's default
which fails on relative paths (NSWorkspace error -50).

- http/https URLs → NSWorkspace.shared.open (default browser)
- Absolute paths that exist → NSWorkspace.shared.open (default app / Finder)
- Relative paths → resolved against the terminal's tracked CWD, then opened
- Non-existent paths → nearest existing parent revealed in Finder

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs: Help index, tutorials, registry walkthrough, and image updates

Help index:
- index.json restructured to mirror _sidebar.md exactly: Introduction (Why TermQ),
  Tutorials, Reference, Further Info sections with correct file paths
- "Why TermQ" moved to Introduction at the top; duplicate removed from Further Info

Tutorials:
- 12-worktree-sidebar: document three-state jigsaw badge, Set Harness on repo
  header and main worktree, context menu ordering, precedence rules
- 13-harness-sidebar: document repo default vs worktree override, auto-launch
  behaviour, Export as Marketplace; add Step 0 registry walkthrough (add
  eyelock/assistants via globe button, then install from Search)
- 14-marketplace: new tutorial covering auto-seeded defaults, Restore Defaults,
  header buttons, and plugin installation flow

Images:
- harness-add-registry.png: new screenshot for registry walkthrough
- harness-install-sheet-search.png: re-compressed (~85% size reduction)

Tests:
- HelpContentLoaderTests: verify flat, reference/, and tutorials/ topic ID paths

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(localization): Remove duplicate marketplace keys from en.lproj

Merge with origin/develop introduced a second copy of the
settings.marketplaces.* block. Removed the duplicate section.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: David Collie <support@eyelock.net>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs(workflow): Fix worktree post-merge cleanup sequencing (#152)

Three issues caused churn after merge in worktree sessions:
- Running `git worktree remove` from inside the worktree invalidated CWD
- `git push origin --delete` failed when GitHub had already auto-deleted the branch
- `gh pr merge --auto` was being used (forbidden — only user may set auto-merge)

Fix: always cd to main repo before removing worktree, guard remote deletion
with `git ls-remote --exit-code`, and explicitly forbid `--auto` in merge rules.

Co-authored-by: David Collie <support@eyelock.net>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ui): Harness and Marketplace sidebars adopt tree layout with grouping (#154)

Reorganises both sidebars to match the Repositories disclosure-group
pattern the user identified as the gold standard.

Harnesses are now grouped by provenance: Default (known names:
assistants, ynh-dev, termq-dev — removable), GitHub org, registry, and
local. Marketplaces are grouped into Default (KnownMarketplaces URLs)
and dynamic GitHub org sections. Both use collapsible DisclosureGroups
with the tab icon in the group heading label.

Also fixes the New Worktree sheet defaulting to the wrong base branch:
the repo refresh button now runs `git remote set-head origin --auto`
before refreshing, so defaultBranch() always reflects the remote's
actual default rather than the stale clone-time value.

GitURLHelper is extracted from HarnessDetailDependencyView into a
shared Utilities file, gains a repoOwner() method, and shortURL() is
fixed to correctly strip https:// schemes.

Co-authored-by: David Collie <support@eyelock.net>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ui): Harness sidebar quality-of-life improvements (#156)

- Empty state with "Add sample eyelock/assistants" and "Add Registry" shortcuts
- Prune Merged Branches from Local Branches context menu header
- Duplicate Harness action creates a local copy with ynh install
- New Folder support in all directory picker dialogs
- Configurable terminal scrollback buffer (default 5,000, range 500–100,000)
- Right-click context menus on harness group headers (Settings, Reveal in Finder,
  Open in Browser) and harness rows (Reveal in Finder, Open in Browser)
- Harness row menu reordered: Launch | Copy + Reveal + Browser |
  Update + Duplicate + Export | Uninstall
- Install sheet opens in browse mode: registry harnesses load on appear,
  split into Installed / Available Locally / Available from Registries sections

Co-authored-by: David Collie <support@eyelock.net>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ui): Auto-tags, terminal naming, and tag tooltip (#158)

* feat(ui): Auto-tags, branch naming, and tag tooltip for terminals

Terminals now receive a richer set of automatically applied tags at
creation time, giving developers and the CLI immediate context about
how a terminal was opened and what worktree it belongs to.

Auto-tags by creation path:
- source: worktree | quick | card | harness
- backend: pty | tmux-attach | tmux-control
- shell: zsh / bash / fish (from $SHELL)
- branch, repository (org/repo): populated when a worktree is known
- session, window: populated for tmux-backed harness terminals
- vendor: only when the harness config auto-launches a vendor

Terminal tab title now defaults to the branch name (numbered on
collision) instead of "Terminal N" or the harness name, making
worktree-sourced tabs immediately meaningful.

Tab hover tooltip gains a second section below "title in column"
listing all key: value tag pairs, so the full context is one hover away.

CLI tag value filter updated to substring match so `repository=TermQ`
finds `eyelock/TermQ` without requiring the full path.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

feat(ui): Sort tags alphabetically in tab tooltip

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ui): Log call stack on window miniaturize for diagnosis

windowWillMiniaturize fires in release builds where the spontaneous
minimize occurs. The captured stack will reveal whether the trigger
is Cmd+M leaking through a focus gap, an external tool, or macOS itself.

Investigation plan at .claude/plans/fix-window-spontaneous-minimize.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs: Update README and help screenshots

Rewrites the README to reflect current feature set — board, worktrees,
AI integration, harnesses, and marketplace. Updated board-view.png and
terminal-tabs.png screenshots.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

style: Fix 31 SwiftLint warnings — identifier names, force unwrap, trailing closures

Renames single-letter variables to descriptive names across 18 files
(q→query, m→marketplace, v→value, s→str, p→ynhPath/proc/path, n→count,
w→boundsWidth, h→boundsHeight, j→otherIndex, d→yndPath).
Adds swiftlint:disable:next for a static URL literal force-unwrap.
Converts two multiple-closures-with-trailing-closure violations to
explicit labeled form.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: David Collie <support@eyelock.net>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore: ignore .worktrees/ directory (#160)

Co-authored-by: David Collie <support@eyelock.net>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* ci: Trigger fresh workflow check after protect-main update

---------

Co-authored-by: David Collie <support@eyelock.net>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
eyelock and others added 26 commits April 20, 2026 23:54
Relative-source plugins (e.g. ./skills/infra) store a path that is
meaningful within the marketplace repo but is not a valid git URL.
Passing it directly to `ynh include add` caused a fatal git error
because YNH tried to clone it as a local repository.

Fix: PluginSourceSpec.resolved(marketplaceURL:) maps relative sources
to (marketplace-git-url, --path <subdir>), which is the form YNH
expects. Also fixes a Codable round-trip bug where the synthesized
encoder wrote key "type" but the custom decoder only read "source",
silently corrupting type to .unknown after MarketplaceStore persistence
and preventing the relative-source guard from ever firing.

Co-authored-by: David Collie <support@eyelock.net>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…ure (#176)

Button captures mouse-down events on macOS, preventing .draggable() from
initiating a drag session. Switching to HStack + .onTapGesture matches the
pattern used by TerminalCardView (Kanban), where drag already works.
Accessibility traits restored explicitly via .accessibilityAddTraits/.accessibilityLabel.

Co-authored-by: David Collie <support@eyelock.net>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…ting terminals (#177)

New terminals were only reading defaultSafePaste and defaultBackend from
UserDefaults — allowOscClipboard, enableTerminalAutorun, and
confirmExternalLLMModifications were ignored, so per-card values always
started at the hardcoded TerminalCard init defaults regardless of what the
user had configured in Settings.

Fixes all card creation paths: addTerminal, newTerminal(at:), quickNewTerminal,
launchHarness, and the install/uninstall/update/export harness operations.
Centralises the reads into a NewTerminalDefaults struct and newTerminalDefaults()
extension so the logic lives in one place.

Co-authored-by: David Collie <support@eyelock.net>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…179)

Harness-launched terminals were created as transient (tab-only) cards
and never written to the board. They are now added directly to
board.cards so they survive session restarts.

Clicking a harness-enabled worktree now switches to the existing card
instead of spawning a duplicate. Launching via the "Launch <harness>"
context menu bypasses dedup and always creates a new card, preserving
the ability to run multiple instances intentionally.

Adds HarnessCardTests covering the dedup matching predicate. Adds
testing.md to the termq-dev skill documenting the test target boundary
and the rule that tests are not optional.

Co-authored-by: David Collie <support@eyelock.net>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Auto-generated appcast files for Sparkle auto-updates.

Co-Authored-By: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Auto-generated appcast files for Sparkle auto-updates.

Co-Authored-By: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Auto-generated appcast files for Sparkle auto-updates.

Co-Authored-By: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Auto-generated appcast files for Sparkle auto-updates.

Co-Authored-By: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Auto-generated appcast files for Sparkle auto-updates.

Co-Authored-By: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
…er, and github shorthand expansion (#213)

Three fixes for v0.8.2:

- Guard NSApp.unhide(nil) in applicationShouldHandleReopen with an
  isHidden check. Unconditional unhide on macOS Sequoia scheduled a
  deferred _doOrderWindow orderOut block that fired when the run loop
  returned to NSDefaultRunLoopMode — which a busy terminal defers for
  seconds or minutes, causing the spontaneous window hide.
- Show ProgressView in the worktree left-slot icon while deletion is in
  flight so the sidebar gives feedback during slow operations.
- Expand bare owner/repo github shorthand to github.com/owner/repo in
  resolved() to match the format ynh include add expects, consistent
  with MarketplaceFetcher which already expands shorthand for cloning.

Co-authored-by: David Collie <support@eyelock.net>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Auto-generated appcast files for Sparkle auto-updates.

Co-Authored-By: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Auto-generated appcast files for Sparkle auto-updates.

Co-Authored-By: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Auto-generated appcast files for Sparkle auto-updates.

Co-Authored-By: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Resolves divergence accumulated during beta cycle. Source files take
develop's version (full 0.9.0 implementation); appcast files take main's
version (authoritative release history).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CheckoutBranchSheet.swift entered the merge from main's hotfix/#213 but
referenced checkoutBranchAsWorktree and Strings.Sidebar.checkoutBranch*
which were not yet in develop's codebase.

Adds the method through the full stack (GitServiceShared → GitService →
GitServiceProtocol → WorktreeSidebarViewModel), the five string constants
to Strings+Sidebar.swift, the en.lproj keys, and a no-op stub to
MockGitService in tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@eyelock eyelock merged commit 80f1fdb into develop Apr 27, 2026
7 checks passed
@eyelock eyelock deleted the chore/merge-main-into-develop branch April 27, 2026 18:54
eyelock pushed a commit that referenced this pull request Apr 27, 2026
Sources and skill files take the release branch (develop) version.
Appcasts auto-merged cleanly — already synced via #228.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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