chore: merge main into develop ahead of v0.9.0 release#228
Merged
Conversation
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>
chore: Update appcast for manual
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>
chore: Update appcast for manual
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 manual
…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>
chore: Update appcast for manual
…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>
chore: Update appcast for manual
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 manual
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 manual
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 manual
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 manual
…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>
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>
release: TermQ v0.8.1
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 manual
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 manual
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 manual
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 manual
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 manual
…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>
chore: Update CHANGELOG for v0.8.2
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 manual
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 manual
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 manual
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
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Resolves the divergence between
mainanddevelopthat accumulated during the 0.9 beta cycle. Without this, the release PR (#226) shows as conflicting on GitHub.Resolution strategy
Docs/appcast.xml,Docs/appcast-beta.xml: main's version wins — it's the authoritative record of what's been shippedAfter merging
#226 (develop → main release PR) will become conflict-free and ready to mark as ready for review.
🤖 Generated with Claude Code