release: TermQ v0.10.0#306
Merged
Merged
Conversation
chore: sync main into develop post v0.9.0
Three bugs prevented v0.9.0 from appearing in appcast.xml: 1. Race condition: the `release: published` and explicit `workflow_dispatch` triggers both fired within 2 seconds of the release being published, before the GitHub API had indexed the new release. The appcast workflow got the pre-release cached list of exactly 30 items and silently skipped v0.9.0 (no zip asset visible yet → jq returned empty → no warning logged → diff showed no changes → no PR created). 2. Pagination: generate-appcast.sh fetched a single page with GitHub's default of 30 releases. With 50+ releases in the repo, any stable release beyond page 1 would be invisible to the generator. Fixes: - Replace `release: published` + explicit `workflow_dispatch` trigger with `workflow_run` on Release workflow completion. The Release workflow takes 30-60 minutes to build/sign/notarize; by the time it completes the API has long indexed the new release. - Add job-level guard to skip appcast update when the Release workflow failed. - Implement page-looping in fetch_releases() (per_page=100&page=N until empty) so all releases are fetched regardless of count. - Add GH_TOKEN auth header to bypass the API response cache. - Fix stale github.event.release.tag_name reference (removed with the release: published trigger) → github.event.workflow_run.head_branch. Co-authored-by: David Collie <support@eyelock.net> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
… record (#234) Harnesses that exist on disk but were never registered via `ynh install` caused `ynh uninstall` to fail with "harness not installed". The fix: - `uninstallHarness(name:)` now detects `installedFrom == nil` and deletes the harness directory via FileManager directly, bypassing the ynh terminal entirely, then cleans associations and refreshes the list - Removed a redundant `FileManager.removeItem` from `performDeleteLocalHarness` in the sidebar (now owned by `uninstallHarness`) - Uninstall confirmation dialogs now show context-aware messages for all three harness provenance states (untracked / ynh-local / registry+git), with the selection logic extracted into a single `Strings.Harnesses.uninstallBaseMessage(for:)` function used by both HarnessDetailView and HarnessesSidebarTab Co-authored-by: David Collie <support@eyelock.net> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs(skills): harden hotfix procedure post v0.9.1 learnings - Merge PR to main before tagging (tag must point to merge commit on main) - Skill/docs updates belong on the hotfix branch for clean cherry-picks - Consolidate forward-port into a single PR to reduce public PR noise - Add "NEVER open multiple forward-port PRs" to What NOT to Do Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: Update appcast for release v0.9.1 Auto-generated appcast files for Sparkle auto-updates. Co-Authored-By: github-actions[bot] <github-actions[bot]@users.noreply.github.com> --------- Co-authored-by: David Collie <support@eyelock.net> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
#239) SwiftUI registers its own kAEGetURL handler during scene initialisation, which runs after App.init — overriding the registration we placed there. This caused SwiftUI's AppWindowsController.activateWindowForExternalEvent to close the main window on every URL open. Moving the NSAppleEventManager registration to applicationDidFinishLaunching ensures TermQ's handler is set last and wins, so SwiftUI never sees the URL Apple Event and cannot hide the window. Co-authored-by: David Collie <support@eyelock.net> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…ng (#240) Cmd+clicking a path in the terminal produced the macOS "-50" dialog because SwiftTerm's default requestOpenLink implementation calls URL(string:) on bare paths, which produces a schemeless URL that LaunchServices rejects. Root cause: LocalProcessTerminalView satisfies requestOpenLink via a protocol extension default baked into the SwiftTerm binary. Subclass overrides in our module land in a separate vtable slot that the inherited witness table never consults. Per SwiftTerm's own docs, the fix is to replace terminalDelegate with a proxy and forward all values. - Add TermQLinkDelegate: full-proxy TerminalViewDelegate installed in TermQTerminalView.init; intercepts requestOpenLink, forwards everything else to LocalProcessTerminalView's concrete implementations - Add TerminalLinkResolver: pure resolution of a link string into .openURL / .openFile / .revealInFinder / .fallbackString / .noop - Add TermQTerminalLink.open: single entry point for all link clicks; pre-flight checks for registered handler, surfaces friendly alert instead of -50 dialog, opens directories directly in Finder - Add TerminalLinkRoutingTests: static guardrail that scans all requestOpenLink definitions and asserts each routes through TermQTerminalLink.open - Add TerminalLinkResolverTests: 20 unit tests for sanitize + resolve - Wire ControlModePaneDelegate.requestOpenLink through the same entry point - Localize two new alert strings (no-handler, launch-failed) into 40 languages Co-authored-by: David Collie <support@eyelock.net> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: update CHANGELOG for v0.9.2 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: Update appcast for release v0.9.2 Auto-generated appcast files for Sparkle auto-updates. Co-Authored-By: github-actions[bot] <github-actions[bot]@users.noreply.github.com> --------- Co-authored-by: David Collie <support@eyelock.net> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* fix(concurrency): add @sendable to system-dispatched closures to prevent MainActor isolation crash TerminalLinkResolver and TmuxControlModeSession both had closures passed to system APIs (LaunchServices completion handler and FileHandle.readabilityHandler) that were inheriting @mainactor isolation from their enclosing context. When called on a background queue by the system this triggers EXC_BREAKPOINT via _swift_task_checkIsolatedSwift. Mark both closures @sendable to opt them out of actor isolation inheritance. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: update CHANGELOG for v0.9.3 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: Update appcast for release v0.9.3 Auto-generated appcast files for Sparkle auto-updates. Co-Authored-By: github-actions[bot] <github-actions[bot]@users.noreply.github.com> --------- Co-authored-by: David Collie <support@eyelock.net> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
…ft detection (#249) * feat(harness): Phase 1 management — foundation, detail pane, fork & duplicate, drift detection Scope: CommandRunner foundation. Streamed-output shell-out with cancellation; migrate ynh callers off ad-hoc Process/Pipe to a single seam. YNH 0.3+ envelope decoding. HarnessListResponse / HarnessInfoResponse parse the structured-output envelope with capabilities and ynh_version; YnhVersionProbe gates Phase 1 affordances on capabilities ≥ 0.3.0. Detail pane refactor. Extract HarnessDetailViewModel from the view; pure formatters for source classification (HarnessSourceBadgeViewModel), editability (HarnessEditabilityResolver), and update signal (HarnessUpdateBadgeStore). Old feature-flagged badge retired. Source badges and read-only model. Sidebar rows and detail header show provenance: registry name, short Git URL, "Local", or "Forked from <X>". Registry harnesses display a Read-only pill — surfaces that direct edits will be overwritten by the next ynh update. Update detection. UpdateAvailabilityService seam + LiveUpdateAvailabilityService impl wraps ynh ls --check-updates and ynh info --check-updates. Three-state HarnessUpdateSignal classifies as versioned (manifest version bumped — orange dot, info banner), unversionedDrift (content drift without a version bump — amber warning triangle, warning banner, confirmation step listing each drifted include SHA), or none. Sidebar dots, header global-probe spinner, dependency-view warning triangle render the signal. Fork to local (single-call). ForkHarnessSheet runs ynh fork --to <path> against the pointer-model YNH; one editable working tree, no copy under ~/.ynh/harnesses/. installed_from.forked_from preserved; ghost origin shown in detail pane. Duplicate (single-call with --name). DuplicateHarnessSheet runs ynh fork --to <path> --name <newname> for local-only renamed copies. Hidden for registry harnesses (Fork covers that intent). Action menu parity. Sidebar context menu and detail action menu share a canonical five-group layout (Run, Location, Actions, Help, Destructive). "Open in…" submenu, Open in browser (URL sources), Reveal in Terminal, Copy as Pathname now consistent across both surfaces. Update menu hidden for forks (YNH refuses). Editable-path resolution. For forked-locals, Reveal/Open/Copy actions target installed_from.source (the editable working tree), not the YNH install slot. Single canonical "where this lives on disk" location. Tolerant Harness decoder. Custom init(from:) accepts null for includes and delegates_to (which YNH emits for broken-install error rows). One bad row no longer collapses the whole sidebar. Sheet first-paint fix. Fork, Update, Duplicate, Add Marketplace, Session Recovery, and Install sheets apply their .frame at the .sheet content closure rather than inside the sheet body. Eliminates the rounded-rect placeholder flash before content resolves. Vendor override picker. Per-harness vendor override (claude/codex/cursor) persists via YNHPersistence; surfaces as a picker badge in the detail header. Tutorial 13 updated for fork/duplicate/menu reorganization. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(localization): Translate Phase 1 harness management strings (40 languages) Adds and syncs localized strings for the Phase 1 harness management surface across all 40 supported locales: - Source badges (Local, Forked from <X>, Read-only pill) - Update banner copy (versioned vs unversioned-drift wording) - Update sheet phases (confirm, running, succeeded, failed) - Unversioned-drift warning banner and confirmation step - Fork sheet (title, explanation, fork button, progress) - Duplicate sheet (title, name field, duplicate button) - Action menu items new in this PR (Reveal in Terminal, Open in…, Open in browser, Copy as Pathname, Delete Harness) - Sidebar tooltips for the global-probe spinner and dependency-view warning triangle - Common error/loading copy used by CommandRunnerSheet No source-only NEEDS TRANSLATION markers remain. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(harness): Coverage for Phase 1 — fork, badges, decoder, drift signals Adds and updates test coverage to match the Phase 1 implementation: - HarnessUpdateBadgeStoreTests — full coverage of the three-state signal classification (none / versioned / unversionedDrift), include-level drift, harness-source drift (self-contained plugins), and version-bump precedence over drift. Regression test for nil-ref include data shape. - HarnessSourceBadgeViewModelTests — registry / git / local / forked classification from installed_from variants. - HarnessEditabilityResolverTests — editability decision per source kind and capability gate. - ForkServiceTests — single-call ynh fork --to flow, error mapping, invalidate-on-success. - YnhVersionProbeTests — semver comparison, capability gating. - YNHDecodingTests — extended for tolerant Harness decoder (null includes/delegates_to from broken-install error rows), envelope shape, --check-updates fields (version_available, ref_available, sha_available), forked_from provenance. - UpdateCheckStateTests — lifecycle transitions. - YNHPersistenceProtocolTests — vendor override persistence contract. - HarnessRepositoryTests — capability surface + selectedDetail wiring. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: David Collie <support@eyelock.net> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
#250) * feat(harness): inline include editing in detail pane Adds inline add/edit/remove of includes directly from the harness detail pane, gated to non-registry harnesses. Builds on Phase 1's foundation; no changes to the existing marketplace-driven flow. - IncludeMutator service wraps ynh include remove/update with the same streaming pattern as IncludeApplier - HarnessIncludeEditor coordinator drives the per-include affordances (edit sheet, remove confirmation) and the inline add panel state - AddIncludeFlow embeds the source picker (marketplace browse or git URL), reusable picks selector, and review-and-apply step inline below the dependencies section - EditIncludeSheet edits ref/path/picks with a command preview and honors ynh's "empty picks = include all" semantics - Already-installed plugins in the source picker route to Edit instead of attempting a duplicate add Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(harness): manifest field editor for description, vendor, version Adds an "Edit Manifest…" item to the detail pane action menu (gated to fully-editable harnesses) that opens a sheet for the safe subset of plugin.json fields. Includes/delegates remain managed by ynh commands. - HarnessManifestEditor service reads/writes .ynh-plugin/plugin.json, preserving every key (\$schema, name, includes, etc.) by round-tripping through a JSON dictionary - SemverValidator gives the version field a shape check with inline hint - VendorService drives the default-vendor picker - Save invalidates detail and re-fetches so the header reflects the change immediately Also picks up a swift-format whitespace fix on AddIncludeFlow. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(harness): polish manifest editor and drift indicator - Manifest save now refreshes the harness list so the sidebar reflects new version and description (was only re-fetching detail) - "Default Vendor" label disambiguates the manifest field from the per-user override badge in the header - Drift triangle on dependency rows now matches by git URL when an include has no subpath, so includes spanning the whole repo light up - Drift tooltip names the SHA delta ("Upstream commit changed: abc1234 → def5678") instead of the generic banner copy Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(localization): translate harness detail editing strings Brings 39 non-English locales up to 852/852 keys. Covers the 50 new strings added by this branch (include add/edit/remove + manifest editor + drift tooltip) plus 12 pre-existing gaps that the localizer audit surfaced (sidebar checkout-branch sheet, harness loading help, unversioned drift help/banner/confirmation copy). Validated with zero NEEDS TRANSLATION markers, zero missing keys, and zero format-specifier mismatches across all 40 languages. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: David Collie <support@eyelock.net> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the row-level affordances for delegates that mirror what we just shipped for includes — ••• menu with Edit / Remove on each delegate card, gated to non-registry harnesses, plus the underlying YNH wrapper. The "Add Delegate" entry point is intentionally absent. The first attempt landed an inline embed that didn't fit how users actually think about picking a delegate target (a harness already in their library, not a path to type or browse to). That flow will be reintroduced once we agree on a unified source-picker pattern that covers Add Include, Add Delegate, Add Marketplace, and Install Harness consistently. What's in: - DelegateMutator service (ynh delegate add / remove / update) - HarnessDelegateEditor coordinator for per-row Edit and Remove - EditDelegateSheet (ref + path text fields + command preview) - Per-row ••• menu on delegate cards in HarnessDetailDependencyView - Remove confirmation dialog - Strings translated across all 40 supported languages What's deliberately missing (deferred): - "Add Delegate" entry point — depends on unified picker design Co-authored-by: David Collie <support@eyelock.net> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ss (#252) `Harness.id` is `"namespace/name"` for namespaced installs but `YNHPersistence` keys associations by bare `name`. The Launch <harness> flow on worktree rows passes the persisted name through and sets `selectedHarnessName = name`, but `HarnessRepository.selectedHarness` matched on `id` only — for any namespaced install the lookup missed, the launch sheet's content closure returned nothing, and the sheet rendered as a blank rounded rectangle that never populated and could only be dismissed with Esc. Make `selectedHarness` match by `id` then fall back to `name`. Apply the same rule to the stale-selection eviction inside `refresh()` so the next list refresh doesn't immediately clear a name-keyed selection. Affects v0.9.3 — also forward-ported via hotfix release. Co-authored-by: David Collie <support@eyelock.net>
…name (#258) Three breaking changes in ynh 0.3.0's structured-output format combined to leave TermQ unable to load any harness data against the new YNH: 1. `ynh ls --format json` now returns an envelope object `{capabilities, harnesses, ynh_version}` instead of a bare array. 2. `ynh info <name> --format json` likewise wraps in `{capabilities, harness, ynh_version}`. 3. The harness `version` field was renamed to `version_installed` in both `ynh ls` and `ynh info` payloads. Update HarnessRepository to decode through YNHListEnvelope / YNHInfoEnvelope wrappers, and remap the `version` CodingKey on Harness and HarnessInfo to `version_installed`. ynd compose still emits `version` so HarnessComposition is unchanged. User-visible symptom on v0.9.4: the Harnesses sidebar tab was empty, harness detail showed nothing, and Launch from a worktree row presented a blank rounded sheet (or did nothing at all). The v0.9.4 identifier-fallback fix at HarnessRepository:40 was a downstream patch on the same bug class but couldn't help while the list itself was empty. YNH-side schema changes are documented separately in a YNH bug report (envelope shape, version rename, Harness.namespace not populated for registry installs). All three are 0.x churn and explicitly not backward-compatible. Co-authored-by: David Collie <support@eyelock.net> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…#255) Land the three Lane A structural fixes from the post-0.9.4 audit. All three are pattern adoptions, not rewrites, but together they close the load-bearing failure modes that produced the white-pill launch bug and the 0.9.3 actor-isolation crash class. 1. LoadState readiness primitive on HarnessRepository - New `LoadState<Value>` enum (.idle/.loading/.loaded(value)/.error) - HarnessRepository exposes `listState` as the canonical source; the legacy `harnesses`/`isLoading` are computed accessors over it. - Launch sheet migrated from `.sheet(isPresented:)` with a conditionally empty content closure (the SwiftUI footgun behind the white-pill bug) to `.sheet(item: $launchSheetTarget)` against an Identifiable target that is set only when both `listState.isLoaded` AND the requested harness id resolves to a `Harness`. Cold-start clicks against an unloaded list are queued in `pendingLaunch` and resolve via `.onChange(of: listState)` once data lands. 2. Canonical Harness identity = `Harness.id` - Renamed `selectedHarnessName` → `selectedHarnessId` (every site). - Sidebar `.tag(harness.name)` → `.tag(harness.id)`; both sidebars write `harness.id` into YNHPersistence rather than `harness.name`. - YNHPersistence stores ids; new `migrateLegacyHarnessKeys(using:)` pass rewrites pre-existing bare-name values to canonical ids on first successful refresh (idempotent, gated per session). - `removeAllAssociations` accepts an id and tolerates legacy bare-name/`*/<name>` matches as a transition safety net. - The id||name fallback inside `selectedHarness` is gone; refresh normalises a legacy bare-name selection to its canonical id rather than dropping it. 3. -strict-concurrency=complete enforced explicitly - Applied as a swiftSetting to every target in Package.swift so the gate survives any future relaxation of the package language mode. - TmuxControlModeSession's process/pipe `nonisolated(unsafe)` storage replaced with a `TmuxProcessHolder: @unchecked Sendable` indirection. The session is now @MainActor-clean; cleanup runs from the holder's deinit on whichever queue ARC releases on, not from the session. Verification: - `swift package clean && make check` clean (1658 tests pass, no warnings). - White-pill repro requires interactive cold-launch testing on the user's machine — the structural fix prevents the bug class by construction but should be confirmed against the four launch entry points. Co-authored-by: David Collie <support@eyelock.net> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…vider, UpdaterProvider) (#259) * refactor: introduce YNHCommandRunner seam for YNH-touching services Protocol-and-DI extraction around CommandRunner.run for HarnessRepository, VendorService, SourcesService, HarnessSearchService, and the LiveUpdateAvailabilityService.production wiring. Lets tests stub the subprocess layer instead of shelling out to a real ynh/ynd binary, unblocking coverage of the success and error branches in each service. Production callers are unchanged — LiveYNHCommandRunner is the default init argument and is a thin pass-through to CommandRunner.run. * refactor: introduce WorkspaceProvider seam for NSWorkspace consumers Protocol-and-DI extraction around NSWorkspace.shared for EditorRegistry and TermQTerminalLink. Lets tests stub LaunchServices queries, URL opens, and Finder reveal calls without launching real apps. TermQTerminalLink moves from a stateless enum to a struct with an injectable workspace + filesystem predicates; the static `open(link:cwd:)` facade is preserved unchanged for the existing TerminalViewDelegate call sites. Production callers are unchanged — LiveWorkspaceProvider is the default. * refactor: introduce UpdaterProviding seam for Sparkle integration Protocol-and-DI extraction around SPUUpdater for UpdaterViewModel. Tests can now inject a stub that records checkForUpdates() calls and emits canCheckForUpdates changes via a PassthroughSubject — letting the view model's state plumbing be exercised without Sparkle reaching the network or talking to LaunchServices. UpdaterViewModel keeps its existing init(updater:controller:) as a convenience that wraps LiveUpdaterProvider, so production wiring in TermQAppDelegate is unchanged. --------- Co-authored-by: David Collie <support@eyelock.net>
Extracts a generic two-tab picker (Library | Git URL) from the prior HarnessInstallSheet, driven by a SourcePickerContext protocol. Install Harness becomes the first context; Add Delegate and Add Include will follow in later phases per .claude/plans/feat-unified-source-picker.md. UX changes - Sources tab folds into Library as a gear-button affordance opening a nested Manage Sources sheet (popover would crash inside macOS sheets). - Library search filters Installed / Available Locally / Available from Registries in place; no mode switch. - Empty state: centered "No results" overlay when a filter clears every section. Registry section hides entirely when filtered to nothing. - "No marketplaces configured" inline row for the genuine empty state. Implementation - Sources/TermQ/Views/SourcePicker/SourcePickerContext.swift — protocol. - Sources/TermQ/Views/SourcePicker/SourcePicker.swift — generic shell. - Sources/TermQ/Views/SourcePicker/HarnessInstallContext.swift — context + Library, Git URL, and Manage Sources views. - HarnessInstallSheet reduced to a thin host preserving its public init and onInstall callback so call sites (ContentView.swift:257) need no edits. Localization - Replaces tab keys (.search, .sources removed; .library added). - Adds .manage.sources, .manage.sources.help, .manage.sources.done, .section.available.empty across all 40 locales. Co-authored-by: David Collie <support@eyelock.net>
…lures (#264) Removing a marketplace from Settings → External Sources didn't survive relaunch. Three concurrent issues: - Confirmation dialog read marketplaceToRemove after dismissal (racy); switched to the presenting: form so the action captures by value, matching the pattern already used in HarnessDetailDependencyView. - save() swallowed all errors with try?; now logs via TermQLogger.io and exposes lastPersistenceError on the store. - A re-seed (after a defaults reset or version bump) could re-add a default the user had explicitly removed. Added tombstone tracking (marketplaces.removedDefaultURLs.v1) honoured on seed; the explicit Restore Defaults button bypasses tombstones via force: true. MarketplaceStore.init now accepts optional fileURL and UserDefaults for isolated tests; new MarketplaceStoreTests.swift covers seeding, removal persistence, tombstone behaviour, and dedup. Fixes #260 Co-authored-by: David Collie <support@eyelock.net>
#265) Models the layering the codebase has been implementing informally (built-in defaults → user prefs → per-card overrides) as an explicit @observable owner. Migrates the four audit-named drift fields (safePaste, fontSize, themeId, backend) from snapshot-on-create to Optional per-card overrides that resolve through SettingsStore at use time, plus the seven other globals it now owns (enableTerminalAutorun, allowOscClipboard, confirmExternalLLMModifications, tmuxEnabled, tmuxAutoReattach, binRetentionDays, terminalScrollbackLines). Drift fix - TerminalCard.{safePasteEnabled, fontSize, themeId, backend} are now Optional. Custom Codable round-trips concrete pre-upgrade values as explicit overrides so existing cards keep their behavior; missing or sentinel ("", 0) values decode to nil (inherit). - Card-create sites (BoardViewModel.newTerminal/quickNewTerminal, ContentView.handlePendingTerminal/install/uninstall/export/launchHarness) no longer snapshot the four fields from UserDefaults — they pass nil. - TerminalSessionManager resolves at session-create time via settings.effective{SafePaste,FontSize,ThemeId,Backend}(card:). - CardEditorView gains an "Override default" toggle per field; the inherited value is shown read-only when the toggle is off. Tier A/B migration - SettingsView writes the seven Tier B globals through @bindable SettingsStore. Theme picker stays bound to TerminalSessionManager to preserve live theme application on running terminals. - BinView, ToolsTabContent, CardEditorView (read-only globals), HarnessLaunchSheet, URLHandler, BoardViewModel+TmuxRecovery, BoardViewModel+HeadlessMode, BoardViewModel.purgeExpiredCards, TerminalSessionManager (scrollback + autorun gate) all route through SettingsStore. External-write bridge - SettingsStore observes UserDefaults.didChangeNotification and reconciles its in-memory state on external writes (CLI, MCP, Sparkle, future code). The didSet write-back is suppressed during sync to avoid loops. Upgrade behaviour - Existing cards retain their concrete values as explicit overrides and will not track future changes to the matching global default until the user toggles "Override default" off. CHANGELOG documents this. New cards inherit by default. Tests - SettingsStoreTests (19) cover defaults, layering, override resolution, write-through, external-sync, and the propagation contract (inheriting cards see global changes; overriding cards don't). - TerminalCardTests, CardEditorViewModelTests, BoardTests, BoardViewModelDefaultsTests, TmuxMetadataTests updated to assert the new Optional contract. Follow-up audit filed as #263 — disposition for the remaining ~70 @AppStorage / UserDefaults sites (16 distinct keys), classified as migrate-to-store (3), move-to-other-owner (4), or stays (12). Co-authored-by: David Collie <support@eyelock.net> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…#266) Closes the three "candidate-for-store" items from #263: - copyOnSelect: SettingsView writes through `$settings.copyOnSelect`; TerminalHostView reads `SettingsStore.shared.copyOnSelect`. - defaultWorkingDirectory: SettingsView binds to `$settings.defaultWorkingDirectory`; BoardViewModel.newTerminalDefaults reads through the store. Empty-string decode falls back to `NSHomeDirectory()`. - diagnosticsVerboseMode: TermQLogBuffer's UI-bound setter routes through `SettingsStore.shared` (with a MainActor hop). The init read and the per-message gate in `append()` keep the nonisolated UserDefaults read, since `append()` is called from arbitrary actor contexts and `SettingsStore.shared` is `@MainActor`. Both reads share the same persisted key so they stay consistent. Adds six SettingsStoreTests covering defaults, persisted reads, write-through, and the empty-string-string fallback for `defaultWorkingDirectory`. After this lands, 16 of #263's bullets remain (4 move-to-other-owner + 12 stays-as-is). The OSC 52 default-mismatch is still a separate followup. Co-authored-by: David Collie <support@eyelock.net> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
applicationShouldHandleReopen fired on every MCP-driven termq:// URL delivery (NSWorkspace.open with activates:false does not suppress the underlying AE Reopen). The handler unconditionally called makeKeyAndOrderFront, so each background MCP op stole focus from whatever app the user was working in. Gate the activation: unhide on Cmd+H, deminiaturize on Cmd+M, bring the window forward only when no windows are visible. When the window is already visible and the app is not hidden, no-op — AppKit handles genuine Dock-click activation independently of this delegate method. Co-authored-by: David Collie <support@eyelock.net> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…270) `TerminalHostView`'s runtime gate for OSC 52 clipboard access defaulted to `true` when the preference was unset, while `SettingsView` displayed `false`. So a never-touched user saw "Off" in Settings → Data & Security but terminal programs could silently copy to the clipboard. Aligning to `SettingsStore.shared.allowOscClipboard` (default `false`) closes the mismatch and matches what users see in Settings. Behavior change: existing users who had relied on the implicit-on default will need to enable OSC 52 explicitly. CHANGELOG documents this. Filed as the OSC 52 follow-up from #263 (now closed). Co-authored-by: David Collie <support@eyelock.net> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds Add Delegate as the second SourcePickerContext. Users can attach a delegate to an editable harness from the detail pane via a new "+ Add Delegate…" entry point, choosing from installed harnesses or pasting a git URL — same shape as Install Harness. Library / Git URL contexts - Library lists installed harnesses (excluding self) with search; pick a row to enter a Configure stage with optional ref + path inputs and a command preview, then Apply runs `ynh delegate add` via DelegateMutator. - Git URL tab takes URL + ref + path with the same Apply path. - sourceURL for installed-harness picks comes from `Harness.installedFrom.source` — the registry URL, git URL, or local path the harness was originally installed from. Works across all three install types without assuming a specific source kind. Cross-context unification - Both Install Harness and Add Delegate now share the same Library shape: search field + gear icon (Manage Sources) at the top, "Browse Local…" button at the bottom. Same icon, same label, same position. - New `SourcePickerManageSourcesSheet` is the shared view behind the gear in both contexts; it lists registered ynh sources with Remove. - Install Harness's "Add Source…" (previously buried in the gear popup) is now the visible "Browse Local…" button at the bottom of Library; Add Delegate's bottom button is identical, with context-specific follow-up (registers as ynh source vs. uses the path as ad-hoc delegate sourceURL). Editor lifecycle - HarnessDelegateEditor gains `requestAdd()`, `isAddingDelegate` flag, and `reloadAfterAdd(harnessName:)`. The unified picker owns the mutation; the editor owns the post-apply detail reload. - HarnessDetailDependencyView renders the "+ Add Delegate…" button below the delegates list (mirrors the existing Add Include affordance) and presents the SourcePicker sheet via a thin `AddDelegateSheetHost`. Localization - Adds `harnesses.add.delegate.*` and `source.picker.browse.local*` keys; renames the old `harnesses.add.delegate.browse.local*` keys to the shared `source.picker.*` namespace. - All 40 locales synced. Co-authored-by: David Collie <support@eyelock.net>
…rs (#271) Splits the launch and lifecycle flows out of ContentView (974 lines before, the highest-churn file in the audit's god-object list at §2G) into two @observable coordinators held via @State. - HarnessLaunchCoordinator owns: pendingLaunch, launchSheetTarget, launchWorkingDirectory, launchWorktreeBranch, cardBeforeHarness state, the PendingLaunch + LaunchSheetTarget types, and the requestLaunch / tryResolvePendingLaunch / launchHarness / dismiss / clearAllSelection / captureCardBeforeHarness functions. - HarnessLifecycleCoordinator owns: installCardIDs, uninstallCardNames, harnessNameToFork/Update, showForkSheet/UpdateSheet/InstallSheet state, and the installHarness / uninstallHarness / updateHarness / exportHarness / forkHarness / handleTransientSessionExit / handleForkCompleted functions. Both take their service dependencies via constructor injection (default to the existing .shared instances) so tests can drive them directly. Pattern follows the SettingsStore + Lane A approach: @observable + @State, not new singletons. The audit (§2A) flagged singleton-style services as load-bearing bug class #1; adding more would regress. ContentView is now 792 lines (-182). Every reference to the extracted state goes through a coordinator — no half-migration. Sub-views participate via the @State-held coordinator instances. The ContentView+HarnessLaunch.swift and ContentView+HarnessFork.swift extension files are deleted; their content moved into the coordinators or inlined into ContentView's harness sub-view extension. handlePendingTerminal (URL-driven) stays on ContentView — different entry point from the coordinator-driven flows. Adds 13 tests covering both coordinators (initial state, fork/update sheet state, transient-session-exit dispatch, dismiss clears state, captureCardBeforeHarness, clearAllSelection). Tests: 1705 (was 1692). Lint, format, build all clean. Co-authored-by: David Collie <support@eyelock.net> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…a injected runner (#280) `HarnessSearchService` already accepts a `YNHCommandRunner` and `YNHDetectorProtocol` via init, but the existing tests all fall through the `.missing` detector guard — leaving the entire `search()` body (arg construction, env override, command result decode, error paths) uncovered. Adds a `StubYNHCommandRunner` test double and nine tests covering: - result decoding from canned JSON - empty-query "browse all" arg shape - whitespace-only query treated as browse - non-zero exit code → error - malformed JSON → error - runner throws → error - `ynhHomeOverride` propagates to environment - subsequent search call cancels the previous (debounce) Coverage moves from "init + reset + .missing-guard only" to the full success path. Closes the easy-win line item from `.claude/plans/2026-04-22-test-integration-initial-handover.md` (plan-2 table at line 172). Tests: 1705 → 1713 (+8 net new; the existing 9 still pass). Co-authored-by: David Collie <support@eyelock.net> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: Update appcast for release v0.9.6 Auto-generated appcast files for Sparkle auto-updates. Co-Authored-By: github-actions[bot] <github-actions[bot]@users.noreply.github.com> * chore: forward-port v0.9.6 hotfix CHANGELOG The three fix commits backported on the v0.9.6 hotfix line are already present on develop (#264, #268 directly; #270 via the SettingsStore route). This commit folds the [Unreleased] OSC 52 entry into a new [0.9.6] section so develop's CHANGELOG reflects what shipped, alongside the appcast files cherry-picked from main. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: David Collie <support@eyelock.net> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ceTests flake (#279, #285) (#284) * test(coverage): backfill HeadlessWriter and CLI+Mutations branches (#279) Add targeted tests for previously uncovered branches: - HeadlessWriter: tag-merge replacement when key already exists, replaceTags with nil to clear all tags, and permanent delete paths (exact name, partial name, unknown identifier). - CLI+Mutations: --init-command warning in headless mode, --replace-tags clearing existing tags, and invalid column surfaces a failure. Coverage: - HeadlessWriter: 87% -> 98.8% lines - CLI+Mutations: 82% -> 86% lines, 100% regions RepoConfigLoader unchanged at 94% lines / 100% regions; remaining gaps are NSFileCoordinator and encoder-failure paths that aren't reachable without a mockable seam. Closes #279 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(harness-search): replace fixed-sleep wait with deterministic poll (#285) The 450ms fixed sleep in waitForDebouncedSearch left only ~100ms of slack after the 350ms debounce — not enough on loaded CI runners where parallel test bundles compete for scheduling. This produced non-deterministic failures across multiple test cases in the file (test_search_withResults_decodesAndPublishesThem, test_search_commandNonZeroExit_setsError, etc.). Replace the helper with a generic poll-until-predicate awaiter (20ms tick, 3s ceiling). Each callsite now waits on the actual terminal state it asserts on, so timing variance no longer matters. Closes #285 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: David Collie <support@eyelock.net> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…range (#283) The previous generator ranged over `${PREV_TAG}..origin/develop` and filtered by `feat:`/`fix:` commit prefixes, on the assumption that all features land on develop. That assumption breaks for hotfixes: a hotfix is tagged off `main`, and `develop` contains every commit that wasn't backported. v0.9.6's release notes ended up listing SourcePicker, harness management, ynh 0.3 envelope, and other develop-only commits that aren't actually in the v0.9.6 tag. Switch to extracting the section header `## [$VERSION]` from CHANGELOG.md. CHANGELOG is already the human-curated source of truth for what shipped — using it directly is correct for every release type (stable, beta, hotfix) and gives release-author control over wording. If the section is empty (CHANGELOG not maintained for that version), fall back to a stub pointer. Co-authored-by: David Collie <support@eyelock.net> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ction (#276) (#286) BackupManager previously hard-coded its primary (Application Support) and backup directories at static-property level, which made every file-based test either skip when a real file existed on disk or operate on the user's actual data. Introduce a `BackupRoots` Sendable struct with `primaryDir` + `backupDir`, and a single static `rootsOverride` hook (nonisolated(unsafe), test-only) that flips the resolution. All path getters now route through `roots`, so call sites remain unchanged. Add a dedicated test suite (`BackupManagerRootsInjectionTests`) covering backup/restore/backupInfo/hasBackup/checkAndOfferRestore against fresh temp directories per test — no more skips. `MarketplaceStore` was already DI-ready (`init(fileURL:defaults:)`) with isolated tests, so this PR scopes to BackupManager only. SecureStorage interaction in backup/restoreSecrets() left as-is — that's the Keychain abstraction tracked by #274. Closes #276 Co-authored-by: David Collie <support@eyelock.net> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… (#287) SecureStorage previously hardcoded its dependencies — Keychain (or a file-based debug fallback) for the AES key, and DataDirectoryManager for the secrets file location. Tests had no way in, leaving 0% coverage on launch-path code that resolves env vars on every terminal open. Extract an `EncryptionKeyStore` protocol with three implementations: - `LiveEncryptionKeyStore` — encapsulates the existing Data Protection Keychain + legacy migration in RELEASE, file-based key in TERMQ_DEBUG_BUILD. Same behaviour as before. - `InMemoryEncryptionKeyStore` — test-only, holds the key in memory. `SecureStorage` now takes `keyStore` + `configDirectory` at init; production code keeps using `.shared` (defaults preserved). Tests construct fresh actors over temp directories. `GlobalEnvironmentManager` gets matching DI: `init(secureStorage: userDefaults: autoLoad:)` lets tests pass an isolated storage actor and a UserDefaults suite, with `autoLoad: false` to skip the init-time Task that previously made deterministic tests impossible. Two new test suites: - `SecureStorageTests` (15 tests) — round-trip, persistence, hasEncryptionKey, reset, export/import, clearCache. - `InMemoryEncryptionKeyStoreTests` (4 tests) — load/store/delete semantics for the test double. - `GlobalEnvironmentManagerTests` (16 tests) — add/update/delete, secret/non-secret transitions, persistence round-trip, resolution, duplicate-key detection. Out of scope per #274: key rotation; migrating other Keychain consumers. Closes #274 Co-authored-by: David Collie <support@eyelock.net> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…wners (#269) (#288) Each move replaces a @AppStorage key duplicated across several Views (or read raw via UserDefaults) with a small @published owner that persists to UserDefaults itself. Same on-disk shape — existing values keep loading. - `marketplaceAutoRefresh` → `MarketplaceStore.autoRefresh`. Settings binds via `$store.autoRefresh`. One reader. - `defaultHarnessAuthorDirectory` → new `HarnessAuthorPreferences` ObservableObject. Replaces five sites: HarnessWizardSheet, ForkHarnessSheet, DuplicateHarnessSheet, SettingsMarketplacesView, HarnessesSidebarTab (the latter previously read raw UserDefaults). - `sidebar.selectedTab` → new `SidebarState` ObservableObject. The `SidebarTab` enum is promoted out of `SidebarView` to module scope so cross-view writers (HarnessWizardSheet, HarnessDetailView) can set tabs by enum case instead of fragile raw strings like "marketplaces". - `protectedBranches` → new `GitConfigStore`. SettingsView binds via `$gitConfig.globalProtectedBranches`; WorktreeSidebarViewModel reads from the store instead of raw UserDefaults at line 392. 10 new tests in `AppStorageOwnerTests` exercise each owner's default, persistence, and (for SidebarState) malformed-value fallback against isolated UserDefaults suites. Closes #269 Co-authored-by: David Collie <support@eyelock.net> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…inalSessionManager (#273) (#290) Two of the three direct `Process()` sites flagged in #273 fit the existing `YNHCommandRunner` shape — single-shot fire-and-forget subprocesses with no streaming, lifecycle, or pipe semantics: - `EditorRegistry.which(_:)` — `/usr/bin/which <bin>`. `start()` and `detect()` become async; `start()` schedules the work on a Task so the AppDelegate caller stays sync. - `TerminalSessionManager.sendTmuxCommand(_:to:)` — fire-and-forget `tmux <command> -t <session>`. Both sites now take `commandRunner: any YNHCommandRunner` at init, defaulting to `LiveYNHCommandRunner`. The protocol is mildly misnamed for non-YNH callers but reusing it avoids cosmetic churn across six existing files; renaming can come later if it grates. The third site, `TmuxControlModeSession.connect()` (line 96), is explicitly out of scope here — it needs the full Process lifecycle plus Pipe/FileHandle streaming abstraction that #273 itself flagged as a separate follow-on. Filed as #289. Tests: - New `EditorRegistryTests` (5 tests): bundle-id hit short-circuits `which`, success/empty/failure paths for the runner, exact args asserted on the captured invocations. - `TerminalSessionManagerTests`: smoke-test that `sendTmuxCommand` no-ops cleanly when no session exists for the card. End-to-end happy-path for the runner is hard to drive without real session state and is left for #289's follow-up. Closes #273 Co-authored-by: David Collie <support@eyelock.net> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…277) (#291) `HarnessAuthor.swift` carried six classes (HarnessAuthor, MarketplaceAddRunner, IncludeMutator, DelegateMutator, YNHMarketplaceService, IncludeApplier) each with its own ~60-line copy-pasted streamProcess implementation spawning Process directly. ~300 lines of duplication, near-zero coverage, launch-path adjacent. Each class now takes `commandRunner: any YNHCommandRunner = LiveYNHCommandRunner()` at init and delegates streaming to `CommandRunner.run`'s onStdoutLine/onStderrLine callbacks. Stdout and stderr are merged into outputLines (matching prior behaviour where both pipes were combined). The empty-line filter is preserved by guarding the line callback. The `LineBuffer` helper at the top of the file is removed; CommandRunner has its own line splitter. `YNHMarketplaceService.fetch` and `runSilent` use the runner's buffered Result.stdout / didSucceed surface — no streaming needed for the JSON-listing and silent-remove paths. File shrinks from 796 to 616 lines (-180). All six classes are now testable without spawning real ynh/ynd subprocesses. 19 new tests in `HarnessAuthorRunnerTests`: - HarnessAuthor: success, create-fail-skips-install, arg-passing. - MarketplaceAddRunner: success and failure surfaces. - IncludeMutator: pure arg builders + remove/update success/fail. - DelegateMutator: pure arg builders + add success. - YNHMarketplaceService: JSON refresh decode, non-zero exit leaves state empty, remove invokes remove+refresh in order. - IncludeApplier: pure arg builders + apply success/fail. Scope notes against the original audit (#277): - The audit assumed manifest editing, fork/duplicate orchestration, filesystem operations, and persistence updates were all in HarnessAuthor. They aren't — those live in HarnessManifestEditor, Authoring/{Fork,Duplicate}HarnessSheet.swift, and HarnessRepository respectively. The actual problem was streamProcess duplication. - A `HarnessAuthorPreferences` type already shipped in #269. The `HarnessFsOperations` extraction the audit suggested has no caller in this file. - File-split per class (one file each) is good hygiene but cosmetic; filing as a separate follow-up so this PR stays focused on the testability win. Closes #277 Co-authored-by: David Collie <support@eyelock.net> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…les (#292) (#293) Pure cosmetic reorg following #277. The six classes that previously shared HarnessAuthor.swift have no shared state, no inheritance, and no helper code (since #277 collapsed the duplicated streamProcess into YNHCommandRunner calls). One file per class makes each findable by name. | File | Class | |-------------------------------|------------------------| | HarnessAuthor.swift (slimmed) | HarnessAuthor | | MarketplaceAddRunner.swift | MarketplaceAddRunner | | IncludeMutator.swift | IncludeMutator | | DelegateMutator.swift | DelegateMutator | | YNHMarketplaceService.swift | YNHMarketplaceService | | IncludeApplier.swift | IncludeApplier | Option structs collocate with the class that consumes them (IncludeRemove/UpdateOptions move with IncludeMutator, DelegateAdd/Remove/UpdateOptions with DelegateMutator, IncludeApplicationOptions with IncludeApplier, YNHMarketplace with YNHMarketplaceService). Shared types still consumed by HarnessAuthor itself (AuthorStepStatus, AuthorStep, HarnessCreationOptions, YNHBinaries) stay in HarnessAuthor.swift. No behaviour change. All 19 HarnessAuthorRunnerTests still pass. Closes #292 Co-authored-by: David Collie <support@eyelock.net> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Migrates the bespoke Add Include flow onto the unified SourcePicker, completing the picker plan for the three "add a thing" surfaces: Install Harness (Phase 1), Add Delegate (Phase 2), and now Add Include. Layout - Library tab lists plugins from configured marketplaces with search and "already installed" recognition. Picking a fresh plugin enters a Configure stage with the picks tree (IncludePicksSelector) and a command preview; clicking a row matching an existing include routes through to the editor's Edit sheet via switchToEditing(). - Git URL tab takes URL + ref + path with command preview (no picks — raw URLs aren't introspected; user can narrow with Edit afterwards). - Apply runs `ynh include add` via IncludeApplier; on success the editor's didFinishAddingInclude() dismisses the sheet and reloads detail. Sheet replaces the inline expansion. The "+ Add Include…" button now toggles editor.isAddingInclude which the IncludeEditorOverlay presents as a sheet (mirroring AddDelegateSheetHost). File layout - New: Sources/TermQ/Views/SourcePicker/AddIncludeContext.swift - New: Sources/TermQ/Views/SourcePicker/IncludePicksSelector.swift (extracted from the old flow file; reusable view) - New: Sources/TermQ/Marketplace/IncludeMatching.swift (IncludeKey + IncludePluginLookup; non-View utilities) - Deleted: Sources/TermQ/Views/AddIncludeFlow.swift (790 lines) - HarnessDetailDependencyView: drops AddIncludeSectionView; entry-point button stays; sheet host modifier added. Tests - AddIncludeStoreTests → AddIncludeContextTests, rewritten against the new context API. Covers library-stage resolution, picks helpers, and command-preview behaviour for both Library and Git URL paths. Localization - Retires step-title and source-mode strings (no longer needed — picker shell handles tab labels and step dots are dropped). - Adds harnesses.include.add.edit.existing. - All 40 locales synced. Co-authored-by: David Collie <support@eyelock.net>
* hotfix(v0.9.7): tolerate YNH 0.2.x list/info shapes (#296) v0.9.5/0.9.6 hard-coded the YNH 0.3 structured-output shape, but YNH 0.3 was never published to the Homebrew tap. Every user on `brew install ynh` is on 0.2.3, so harness loading failed entirely: empty Harnesses sidebar, blank Launch card on worktree rows that already had a harness associated. Decoding is now tolerant of both shapes: - `YNHListEnvelope` / `YNHInfoEnvelope` accept either the 0.3 envelope (`{harnesses: [...]}` / `{harness: {...}}`) or a bare `[Harness]` / `HarnessInfo` payload. - `Harness` / `HarnessInfo` accept either `version_installed` (0.3) or `version` (0.2.x). Tests cover both shapes for both call sites. The compat layer is intentional and sticks around past 0.10. Removal plan: once YNH 0.3 ships to the tap, gate behavior on `YNHDetector.capabilityMeets("0.3.0")` for one release, then delete the legacy branches. Co-authored-by: David Collie <support@eyelock.net> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: Update appcast for release v0.9.7 Auto-generated appcast files for Sparkle auto-updates. Co-Authored-By: github-actions[bot] <github-actions[bot]@users.noreply.github.com> * fix: complete forward-port of v0.9.7 hotfix The hotfix added isPinned to HarnessInfo but the custom init(from:) and encode(to:) didn't handle the new property — develop's build broke on merge. Decode/encode it like the other optional fields. Tests for YNH 0.2.x/0.3 envelope tolerance referenced YNHListEnvelope/ YNHInfoEnvelope, but the actual types are HarnessListResponse/ HarnessInfoResponse. Updated test references. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: David Collie <support@eyelock.net> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
The local-branch context menu previously offered only "New Worktree from Branch", which creates a new branch off the selected one. The more common operation is converting an existing branch into a worktree without creating a new branch — this adds that as a distinct action. The Convert sheet exposes an editable Branch Name (default = current) so a loose name can be normalised to the feature-branch convention as part of the conversion; if changed, git branch -m runs before git worktree add. Co-authored-by: David Collie <support@eyelock.net> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(harness): canonical-id cutover at the YNH CLI boundary
Treat each harness's canonical id (`<host>/<org>/<repo>/<name>` or
`local/<name>`) as the only identifier across every TermQ→YNH
interaction. Eliminates the duplicate-name bug class where a registry
install and a local fork sharing a name could resolve to the wrong
target.
Steps consolidated into this commit:
- **Drop bare-name fallbacks in HarnessRepository.** The `id || name`
mixed-key fallback was the original source of duplicate-name bugs;
every read path now keys exclusively on `Harness.id`.
- **Canonical-id at the YNH CLI boundary.** Every shell-out (`ynh
install`, `ynh info`, `ynh include`, `ynh delegate`, `ynh fork`,
`ynh uninstall`, `ynh update`, `ynh export`) passes `harness.id`
rather than `harness.name`.
- **Fork UX, migration coordinator, and quarantine surface** (steps
4–8 of the plan):
- Fork sheet: `--name <new-name>` flow matching what YNH actually
exposes; canonical id is always `local/<name>`.
- `HarnessMigrationCoordinator` reads `~/.ynh/.migration-manifest.json`
on startup and rewrites TermQ-side persisted ids in `YNHPersistence`
from old shape to canonical. Idempotent via
`TermQ.harnessMigration.lastAppliedSchema` UserDefault. Includes a
leaf-name fallback so `<old-namespace>/<name>` values rewrite
against bare-name manifest entries (the persistence-shape mismatch
we hit on first cutover migration). Always applies the on-disk
manifest before deciding whether to call `ynh migrate`, so a fresh
TermQ against an already-migrated YNH home still rewrites
persistence cleanly.
- Quarantine sidebar group surfaces entries from
`~/.ynh/.quarantine/broken/` with Restore + Drop actions.
Robustness fixes folded in:
- `Harness.id` is decoded verbatim from the YNH 0.4 envelope's `id`
field, with a `namespace + "/" + name` fallback for older envelopes.
- `HarnessListResponse` initializes `schemaVersion` in both decoder
branches.
- Fork-completion lookup matches by `path` rather than synthesized id,
robust against future fork id stamping changes.
Tests:
- Envelope `id` decode primary path (was previously only fallback path).
- `HarnessListResponse.schemaVersion` round-trip.
- `MigrationManifest` and `QuarantineEntry` decode contracts.
Docs:
- CHANGELOG sweep covering Phase 1 follow-ups: include/manifest editing,
delegate management, unified Source Picker, schema-1→2 migration,
quarantine sidebar, canonical-id at YNH CLI, fork sheet alignment,
YNHCommandRunner injectable seam.
- Tutorial 13 expanded with sections on inline editing, quarantine UX,
and automatic schema migration; fork section rewritten to describe
the `--name` flow.
- Three 4×4 placeholder PNGs for the tutorial — real screenshots TBD.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(harness): stale-while-revalidate refresh, no flash on focus
`HarnessRepository.refresh()` set `listState = .loading` at the top of
every refresh, which made `harnesses` (which reads `listState.value`)
return `[]` until the new `ynh ls` response arrived. The
`didBecomeActiveNotification` handler in SidebarView calls refresh on
every focus, so each refocus produced a ~100ms window where the harness
list was empty:
- The detail pane unmounted (its survival check sees no harness)
- Open Add Include / Add Delegate sheets dismissed (parent unmounted →
@State reset)
- Layout flashed to the default Kanban view, then back to detail when
the load completed
Stale-while-revalidate semantics: keep `listState` at `.loaded` across
refreshes once we have data. A separate `isRefreshing` @published flag
exposes the in-flight signal to consumers that want a spinner. The
header spinner and refresh-button-disabled gate continue to work via
`isLoading`, which is now `isRefreshing || listState.isLoading`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(harness-source-picker): delegate add — pre-fill, SHA pin toggle, local restriction
Aligns Add Delegate's Library tab with YNH's marketplace pinning model
and YNH's actual `delegate add` surface. Final state collapses several
in-flight iterations as the YNH-side semantics were pinned down.
Form behaviour:
- Pre-fill `path` from the picked harness's `installedFrom.path` so
YNH receives a fully-qualified delegate target. Without this,
`ynh delegate add <target> <source-url>` with no `--path` produces
a silently-ambiguous entry (the bug class YNH PR #134's validator
now rejects).
- Pre-fill `ref` from `installedFrom.ref` (the user's stated install-
time intent — could be a tag, branch, or SHA). Per YNH's marketplace
model (docs/marketplace.md#pinning-refs-and-shas), ref is the primary
identifier and SHA is an optional integrity pin layered on top.
Defaulting to the symbolic ref preserves the user's tracking intent
("give me 1.0, including future patches").
- Surface the resolved SHA below the ref field as a caption, and add
a "Pin to exact commit (integrity check)" toggle. When ticked, the
form submits the SHA as `--ref` (YNH's resolver supports SHA-as-ref).
- Configure form's `@State` initializers seed from the picked source
so the pre-filled ref + path actually appear in the UI rather than
being silently overridden by empty defaults.
Local-as-delegate restriction:
- Local-only harnesses cannot be valid delegate targets — there's no
shareable identity (no remote URL or canonical id YNH can resolve
across machines), and persisting a filesystem path in plugin.json
bakes one user's directory layout into the harness manifest.
Filter local-sourceType harnesses out of the Add Delegate library
list and remove the Browse-Local affordance entirely.
- Tutorial 13's Delegates section gets a callout describing the
restriction and pointing users at the Git URL alternative.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(harness-source-picker): include — ref plumbing, picks step, URL normalization
Three independent gaps in Add Include surfaced during canonical-id
testing. All collapse here:
**`--ref` was silently dropped on every include add.**
`IncludeApplicationOptions` had no `ref` field and `buildIncludeAddArgs`
never emitted `--ref`. Every include floated to HEAD regardless of
the user's pinning intent — Library tab dropped `marketplace.ref`
that was already fetched as `libraryResolvedRef`; Git URL tab dropped
the user-typed `gitRef`. Plumb `ref: String?` through
`IncludeApplicationOptions`, `buildIncludeAddArgs`, `applyLibrary()`,
`applyGitURL()`, and `commandPreview()`. The Library Configure view
surfaces the inherited marketplace ref as a "Pinned to X (inherited
from marketplace)" / "Tracks marketplace HEAD (unpinned)" caption.
**Picks step buried in the configure view.**
The unification work (Library / Git / Path tabs under one SourcePicker)
didn't carry over the multi-step Artifacts → Apply UX from the older
HarnessIncludePicker. Result: picks selector compressed alongside the
plugin header card, command preview, and Apply button on a single
screen — easy to miss and dismiss with the all-pre-selected default.
Restructure `AddIncludeLibraryConfigureView` as a two-step wizard
mirroring the marketplace picker: numbered step indicator, Artifacts
step with full-bleed picks selector + Select All / None affordances,
Apply step with command preview and Back/Apply controls.
**`IncludePluginLookup` only normalized `.git` and case.**
YNH stores include source URLs as `github.com/eyelock/assistants`
(host-prefixed, no scheme) while marketplaces typically store the
canonical `https://github.com/eyelock/assistants` form. They never
matched, so Edit Include's "Picks" section showed "0 of 0 selected —
No artifacts enumerated for this plugin — This source isn't in your
marketplaces" even when the plugin clearly was. Extract a shared
`GitURLNormalizer` that strips scheme, well-known host prefix,
trailing `.git`, trailing slash, and SSH alt form. The same helper
now backs both `IncludeKey.matches` (already-installed detection in
Add Include) and `IncludePluginLookup.find` (picks discovery in Edit
Include). Nine unit tests cover the normalization paths.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(harness): install search spinner + settings ref pin display
Two small UX polish items surfaced during canonical-id testing.
**Install Harness's "Available from Registries" flicker.**
The section flashed "No marketplaces configured. Add one in
Settings → External Sources." before the initial search task fired,
then switched to the spinner, then to the actual list. The empty-
state branch was reached during the brief window where `isSearching`
was still false (search hadn't been kicked off yet) and `results`
was empty. Add a `hasSearched` flag on `HarnessSearchService` that
flips true after the first search task runs. The registry-rows view
now treats "never searched" the same as "actively searching" — the
spinner sticks until we have a real result or error to render.
**Settings → External Sources ref pin display.**
Surface the configured git ref (or "latest (unpinned)") on each
marketplace row. Useful debug context for verifying which version
of a registry the app is reading from when investigating
include/delegate resolution issues. For TermQ-side `Marketplace`
rows the ref comes from the user-supplied pin. For YNH-side
`YNHMarketplace` rows the ref is decoded from `YNHMarketplace.ref`;
displays "latest (unpinned)" until YNH starts emitting that field
in `registry list --format json`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(harness-sidebar): grouping, fork badge, broken-fork display, safe Delete
Four small but visible sidebar polish items, each fixing a UX gap that
canonical-id testing surfaced.
**DEFAULT/LOCAL grouping respects source type.**
The DEFAULT group filter matched solely on `harness.name` against
`KnownHarnesses.defaultNames`. Once a user forked a default-named
harness (e.g. `ynh-dev`), both the original registry install and the
fork share `name == "ynh-dev"`, so both filtered into DEFAULT and the
fork never appeared in LOCAL. Add a source-type gate: a harness only
counts as DEFAULT when its `installedFrom.sourceType` is not "local".
Forks and other locally-sourced harnesses now reach LOCAL even when
they share a name with a known default.
**Forked-to-local rows render distinctly.**
The LOCAL group rendered every entry with a generic "Local" source
badge, so a hand-built local harness and a fork of a registry harness
were visually identical. Forks now render with an `arrow.triangle.branch`
icon and a "Fork of <upstream>" label using the upstream's registry
name (or short git URL). Tooltip carries the full origin description
— registry name, short URL, and version when available.
**Broken-fork state surfaced from the YNH envelope.**
YNH tags pointer-installed forks whose on-disk `.ynh-plugin/plugin.json`
is missing or unreadable with `kind: "local-fork-broken"` and a
`broken_reason`. Decode both fields on `Harness` and surface broken
entries with a red `exclamationmark.octagon.fill` indicator next to
the name, a red "Broken" badge in the row footer, and the
`broken_reason` rendered inline as the caption. Tooltip on the
indicator falls back to a help message pointing the user at right-
click → Uninstall.
**Delete actually deletes.**
The destructive "Delete Harness" action called the same `onUninstall`
path as plain Uninstall, which leaves the source tree in place per
`ynh uninstall`'s contract. The dialog copy explicitly promised
"permanently delete its files. This action cannot be undone." — but
no file deletion ever happened.
Wire a separate `deleteLocalHarness(id:)` lifecycle method that chains
`ynh uninstall <id> && rm -rf <editable-path> && exit` in the
transient terminal. Three layers of defence:
- The sidebar context menu only shows Delete for local-source harnesses
- The coordinator independently refuses if `sourceType != "local"`
- `shellQuote` survives paths with spaces or single quotes
Pure command-builder is exposed for testing — chain order, quoting,
and exit semantics are unit-covered.
Localized to all 39 non-English locales.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* style(harness-source-picker): swiftlint for-where + identifier_name fixes
Trivial style cleanup surfaced by the quality gate after squashing:
- `GitURLNormalizer.normalize` now uses `for x in xs where ...` form
instead of `for x in xs { if ... { break } }`. Same semantics, one
less indent level.
- Identifier `s` renamed to `normalized` (swiftlint identifier_name
minimum-length rule).
- swift-format whitespace fix in AddIncludeContext.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(harness): gate ynh ls error description behind fileLoggingEnabled
The catch arm at HarnessRepository.swift:174 logs the full error
description via `String(describing: error)`. When the error is
`YNHDetectionError.commandFailed`, the description includes stderr —
which is terminal output, classified as user data per logging-rules.
Sending it unconditionally to os.Logger violates the privacy boundary.
Default to type-only logging (no stderr leakage) and gate the full
description behind `TermQLogger.fileLoggingEnabled`, mirroring the
pattern already used in `fetchDetail` at lines 210-214 and 225-229
of the same file.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(harness-migration): refresh quarantine list even when migration applied
ContentView's `runMigrationIfNeeded` short-circuited when the
migration flag was already set, returning before delegating to the
coordinator. The coordinator's own "already applied → just refresh
quarantine" branch was therefore unreachable — the QUARANTINED
sidebar group only updated on the first launch that triggered a
migration, and then went stale forever.
Drop the outer hasApplied check; let the coordinator decide. Its
`runIfNeeded` does the right thing in all branches (calls
`refreshQuarantineList` either standalone when migration is current,
or as the final step after running migrate).
Surface symptom: planting a quarantined entry while TermQ was
running never made the QUARANTINED group appear, even after relaunch
— because the migration flag was already at schema 2 from prior
sessions and the coordinator was never given a chance to re-probe
`ynh quarantine list`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: David Collie <support@eyelock.net>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…umeric filename prefixes, real screenshots (#302) * docs(help): real screenshots replace placeholders, drop orphans Replaces the 4×4 placeholder PNGs that shipped with the canonical-id cutover with real screenshots, and adds the screenshots that tutorials 12 and 14 referenced but never had files for. **Replaced** (placeholder → real, all compressed via pngquant): - harness-delegate-editor, harness-fork-button, harness-fork-sheet, harness-include-editor, harness-quarantine-group, harness-update-dot **Added** (was referenced in markdown, no file existed): - harness-install-browse, harness-install-search-results - harness-wizard-identity, harness-wizard-progress - harnesses-author-directory - marketplace-browser, marketplace-include-picker - settings-protected-branches **Removed** (existed but never referenced): - harness-add-registry, harness-install-sheet-search - harness-source-badges, harness-worktree-context-menu All new and replaced PNGs run through `make compress-images` so every file in `Docs/Help/Images/` is now under 300KB (was up to ~1MB raw). Image audit after this commit: 71 referenced, 71 on disk, zero broken, zero orphans. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(help): drop numeric prefixes from tutorial filenames Tutorial display order has lived in `_sidebar.md` for a while — sidebar already shows "8. Git Worktrees" mapped to `12-worktree-sidebar.md`, "9. Harnesses" mapped to `13-harness-sidebar.md`, etc. The filename numbers carried no semantic value beyond confusion when reordering. Drop the `NN-` prefix from every tutorial filename. Display order continues to live in `_sidebar.md` (and `index.json`), making future reorganisation a one-file edit instead of a filesystem rename pass plus 60+ cross-reference updates. Updated all references across `_sidebar.md`, `index.json`, `README.md`, `why.md`, `reference/*.md`, and inter-tutorial links. Bundled help in `Sources/TermQ/Resources/Help/` will sync via `make copy-help` in a later commit. No content changes — pure rename + reference update. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(help): split Marketplace Browser and Harnesses tutorials, with Marketplace as prerequisite Reorganises the help tutorials so each one focuses on a single mental model. **Old shape**: tutorial 13 was "Harnesses" (install / lifecycle / fork) and tutorial 14 was "Marketplaces & Authoring" — bundling marketplace browsing with the harness-creation wizard, the include-picker, and the typical authoring loop. Half of T14 was actually harness-side content. The narrative had to bounce between two concepts and the order put the dependent (harnesses) before its dependency (marketplaces). **New shape**: - T9 (was T14, slimmed): **Marketplace Browser** — what marketplaces are, default and custom marketplaces, browsing the catalogue, marketplaces sidebar context menus. - T10 (was T13, expanded): **Harnesses** — the entire harness lifecycle, including the harness-authoring sections that used to live under the marketplace tutorial: adding marketplace plugins to a harness, default author directory, the wizard, and the typical authoring loop. Also folded into this commit: - **Stale Picker text** in the old §13.4 (Installing a harness): "three tabs: Search, From Git, Sources" rewritten as the unified two-tab Source Picker (Library + Git URL). The Sources subsection is removed entirely — sources management moved to Settings → External Sources, accessible from the Library tab's gear icon. - **Library / Git / Path** in §12 corrected to **Library / Git URL**. - **Settings → Marketplaces** corrected to **Settings → External Sources** throughout. - Section numbering inside each tutorial dropped the leading tutorial number (§13.X → §X) to decouple body refs from tutorial position. - Tutorial titles drop the "Tutorial NN:" prefix — display order lives in `_sidebar.md` and `index.json`, and self-contained titles survive future reordering without a body-edit pass. Other touched files: - `_sidebar.md` and `index.json`: swap order, update display titles (Harnesses → Marketplace Browser → Harnesses). - `README.md`: the Power Workflows section now lists Marketplace Browser (T9) and Harnesses (T10) — they were missing entirely. `Sources/TermQ/Resources/Help/` will sync via `make copy-help` in the next commit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: David Collie <support@eyelock.net> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…detail, Run with Focus polish (#304) * feat(sidebar): Remote PRs panel with Local/Remote toggle and Run with Focus Adds a Remote tab to the Repositories sidebar panel. When the `gh` CLI is present and authenticated, the tab lists open GitHub PRs for each repo with role badges (author / review-requested / assigned / draft / checked-out). Key capabilities: - GhCliProbe detects gh presence + auth on startup and app-foreground - GitHubPRService fetches open PRs via `gh pr list --json` per repo - SHA-primary + branch-name-fallback PR↔worktree matching - Checkout PR as new worktree from the Remote tab (gh pr checkout) - Local tab gains PR-link badge on matched worktrees - Worktree context menu adds "Run with Focus…", "Open PR on Remote", and "Show in Remote" when the worktree is PR-linked - Remote PR row context menu: Open/Copy URL, Run with Focus (checked out), Show in Local, Checkout as Worktree (not checked out) - Force-push indicator + re-routed "Update from Origin" for PR-linked worktrees - ReviewWithFocusSheet with harness/profile/focus pickers and command preview - PruneClosedPRsSheet for worktrees whose PR number is no longer open - WorktreeSidebarView split into +RemotePRs.swift (file_length limit) Note: the focus sheet end-to-end depends on feat/harness-detail-editing landing first (canonical ID fix for ynh run / ynh info). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(sidebar): rename ReviewWithFocus → RunWithFocus throughout The sheet label was already "Run with Focus…" but the code still used Review* naming. Aligns all type names, protocol methods, string keys, persistence fields, and the file name with the UI copy: - ReviewWithFocusSheet → RunWithFocusSheet (file renamed) - ReviewWithFocusContext → RunWithFocusContext - onReviewWithFocus → onRunWithFocus (callback props) - reviewWithFocusContext → runWithFocusContext (state var) - reviewHarness/setReviewHarness → runHarness/setRunHarness - setReviewFocus → setRunFocus, reviewFocus → runFocus - repoReviewHarness/repoReviewFocus → repoRunHarness/repoRunFocus - remote.prs.review.* string keys → remote.prs.run.* Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(run-with-focus): always show Profile row; rename Prompt → Focus prompt Profile picker now appears even when the harness declares no profiles, showing a single disabled "Default" entry as a signal that YNH supports the --profile flag. When profiles exist, the picker shows them (read-only, driven by the selected focus). Also renames the prompt section header from "Prompt" to "Focus prompt" for clarity. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(run-with-focus): always invalidate detail cache on sheet open HarnessRepository caches ynd compose results per session. The focus sheet was hitting a stale cache entry when the harness's plugin.json had been edited since the last fetch — focuses and profiles would not appear until the app restarted. Invalidate the specific harness entry before each fetchDetail call in RunWithFocusSheet so the sheet always reflects the current on-disk composition. The detail pane's existing cache-on-mutation contract is unaffected. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(run-with-focus): interactive Profile picker in ad-hoc mode When no focus is selected the Profile row is now a live picker — the user can choose any profile declared by the harness, or leave it on "(harness defaults)". The chosen profile is passed as --profile to ynh run and shown in the command preview. When a focus is selected the profile is still locked (derived from the focus definition) because --focus and --profile are mutually exclusive in YNH. Also adds profile: String? to HarnessLaunchConfig and updates command() to emit --profile when set (and no focus flag is present). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(run-with-focus): show loading spinner while fetching harness detail Focus and Profile pickers are replaced by a spinner + label while ynd compose runs, preventing the sheet from appearing in a bare default state before the data arrives. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(remote-prs): priority feed, per-host login, cap setting, sidebar polish - Add `updatedAt` to `GitHubPR` for recency ordering - `GitHubPRService`: per-repo login via `gh api user` (scoped to remote host via currentDirectory, cached per repo); `prioritisedFeed` method orders PRs in four tiers: checked-out → review-requested → unreviewed non-draft → rest, `updatedAt` desc within each tier - `SettingsStore`: `remotePRFeedCap` (default 20); persisted via UserDefaults - `LocalYNHConfig`: `repoRemotePRFeedCap` per-repo override - `YNHPersistence`: `remotePRFeedCap(for:)` / `setRemotePRFeedCap(_:for:)` - New GitHub Settings tab (`SettingsGitHubView`) with stepper for feed cap - Remote sidebar: applies priority feed + cap, shows "+N more" footer when truncated, hides "Prune Closed PRs" when no closed-PR worktrees exist - Local sidebar: PR badge moves to trailing edge, is now a Button that opens Run with Focus sheet; shows PR title in tooltip - Branch name button gets `.help()` tooltip showing full branch name Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(run-with-focus): vendor picker + --interactive toggle - `Vendor`: add `supportsInitialPrompt` decoded from `supports_initial_prompt` in `ynh vendors --format json` (defaults false for older YNH) - `HarnessLaunchConfig`: add `interactive: Bool`; `command()` appends `--interactive` when true - `RunWithFocusSheet`: add vendor picker (same defaultVendorTag pattern as HarnessLaunchSheet); add "Stay interactive after run" toggle, shown only when the resolved vendor reports `supportsInitialPrompt` and there is a prompt or focus to send; toggle auto-clears when switching to a vendor that doesn't support it Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(run-with-focus): refresh VendorService on sheet open if vendors not yet loaded Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(remote-prs): context menu restructure, card titles, cache-first detail loading Restructures the Remote PRs context menu to match the Local panel layout: Run with Focus at top, Quick Launch Focus submenu pre-populated from cached harness detail, then terminal/reveal/copy actions, then PR-specific actions, then Set Default Focus submenu. Adds card title override (focus: org/repo#N) with 40-char middle-truncation for long org/repo slugs, built at launch time and surfaced as the terminal card header rather than the bare branch name. Harness detail is now cache-first: RunWithFocusSheet reads the in-memory cache immediately (no fetch), with a manual ⟳ button to force-invalidate. PR rows pre-warm the cache via .task(id:) so the sheet opens instantly in the common case. Default focus restore defers via Task { @mainactor } to survive the onChange(of: selectedHarnessId) clear. Adds 33 unit tests covering prioritised PR feed ordering/overflow, card title truncation, and HarnessLaunchConfig command composition. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(remote-prs): Tutorial 15 with compressed screenshots Adds Help tutorial covering the full Remote PRs feature set: feed overview, priority ordering, PR rows, context menu, Run with Focus sheet, Quick Launch, Set Default Focus, and GitHub Settings. Twelve pngquant-compressed screenshots (~78% size reduction each). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(remote-prs): align tutorial with new slug naming convention Renames 15-remote-prs.md → remote-prs.md to match the slug-first convention introduced in #302. Registers the tutorial in _sidebar.md (Power Workflows section, numbered 11) and index.json. 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>
# Conflicts: # .claude/skills/release/references/hotfix.md # CHANGELOG.md # Docs/appcast-beta.xml # Sources/TermQ/Services/HarnessRepository.swift # Sources/TermQ/Services/MarketplaceStore.swift # Sources/TermQ/Services/TerminalLinkResolver.swift # Sources/TermQ/Views/ContentView.swift # Sources/TermQ/Views/HarnessDetailView.swift # Sources/TermQ/Views/Sidebar/HarnessesSidebarTab.swift # Sources/TermQ/Views/TerminalHostView.swift # Sources/TermQShared/Harness.swift # Tests/TermQTests/HarnessRepositoryTests.swift # Tests/TermQTests/YNHDecodingTests.swift
… conflict resolution
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.
Promotes release/v0.10.0 to main for stable release.