From 876ec421b4c070a8235bae2a71349674613af08c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 28 Apr 2026 05:57:15 +0000 Subject: [PATCH 01/29] chore: Update appcast for release manual Auto-generated appcast files for Sparkle auto-updates. Co-Authored-By: github-actions[bot] --- Docs/appcast-beta.xml | 123 ++++++++++++++++++++++++++++-------------- Docs/appcast.xml | 83 ++++++++++++++++++++++++++++ 2 files changed, 166 insertions(+), 40 deletions(-) diff --git a/Docs/appcast-beta.xml b/Docs/appcast-beta.xml index e6a89b1a..308b0e11 100644 --- a/Docs/appcast-beta.xml +++ b/Docs/appcast-beta.xml @@ -5,6 +5,89 @@ https://github.com/eyelock/TermQ Most recent updates to TermQ en + + Version 0.9.0 + 0.9.0 + 0.9.0 + Mon, 27 Apr 2026 21:09:42 +0000 + What's New +
    +
  • right-click context menu in terminal with Copy and Paste (#221)
  • +
  • YNH 0.2 support — namespace model, marketplace rename, fixture tests (#218)
  • +
  • sidebar drag-and-drop, branch ops, harness/marketplace ordering (#217)
  • +
  • add in-app diagnostics log viewer (#193)
  • +
  • Protected branches deny-list for Prune Merged Branches (#178)
  • +
  • Pin marketplace and harness registry to a git ref (#171)
  • +
  • Open in editor submenu for worktree and harness context menus (#169)
  • +
  • Generate release notes from conventional commits (#168)
  • +
  • Auto-tags, terminal naming, and tag tooltip (#158)
  • +
  • Harness sidebar quality-of-life improvements (#156)
  • +
  • Harness and Marketplace sidebars adopt tree layout with grouping (#154)
  • +
  • UX polish, marketplace improvements, and terminal enhancements (#151)
  • +
  • Marketplace browser, harness wizard, and install/launch management (#148)
  • +
  • YNH harness sidebar — detect, install, launch, manage (#142)
  • +
  • Git worktree sidebar (#126)
  • +
+

Bug Fixes

+
    +
  • fix upward scroll during text selection against streaming output (#224)
  • +
  • restore Reveal in Terminal across sidebar tabs (#223)
  • +
  • prevent SwiftUI from closing main window on URL events (#222)
  • +
  • github install + YNH 0.2.0 capabilities gate (#210)
  • +
  • expand github source shorthand for ynh include add (#203)
  • +
  • guard NSApp.unhide against calling on visible app to prevent spontaneous window hide (#199)
  • +
  • show spinner in worktree row during deletion (#195)
  • +
  • eliminate all build and lint warnings (#191)
  • +
  • eliminate live file monitoring side effects in BoardViewModelDefaultsTests (#185)
  • +
  • Persist harness-launched cards and deduplicate on re-launch (#179)
  • +
  • Inherit all security settings from global defaults when creating terminals (#177)
  • +
  • Restore tab drag-and-drop by replacing Button with onTapGesture (#176)
  • +
  • resolve relative plugin paths in ynh include add (#175)
  • +
  • fix printf treating triple-dash separator as flag on macOS (#173)
  • +
  • use echo instead of printf for bullet list items in release notes (#172)
  • +
  • Fix text selection and scroll in terminals with mouse tracking (#147)
  • +
  • Correct TUI rendering in tmux control mode terminals (#138) (#141)
  • +
  • Disable Sparkle auto-updater in debug builds (#137)
  • +
  • Use file-based key storage in debug builds (#130)
  • +
  • Debug build shows SHA in About panel; document debug behaviors (#131)
  • +
  • Use async panel.begin() for Browse button in path pickers (#132)
  • +
  • Preserve tmux pane focus across SwiftUI re-renders (#133)
  • +
  • Migrate SecureStorage to Data Protection Keychain (#124) (#129)
  • +
  • Scroll tab bar to reveal selected terminal on sidebar jump (#125) (#128)
  • +
  • Scroll tab bar to reveal selected terminal on sidebar jump (#125)
  • +
  • Migrate SecureStorage to Data Protection Keychain (#124)
  • +
+
+

Installation

+ +
    +
  1. Download TermQ-0.9.0.dmg
  2. +
  3. Open the DMG and drag TermQ.app to your Applications folder
  4. +
  5. Double-click to launch
  6. +
+

Alternative: Zip Archive

+
    +
  1. Download TermQ-0.9.0.zip
  2. +
  3. Unzip and move TermQ.app to your Applications folder
  4. +
+

CLI Tool

+

The termqcli CLI is bundled inside the app. To install it:

+
    +
  1. Open TermQ.app
  2. +
  3. Go to TermQ → Settings (or press ⌘,)
  4. +
  5. Click Install Command Line Tool
  6. +
+

Or manually:

+
sudo cp /Applications/TermQ.app/Contents/Resources/termqcli /usr/local/bin/termqcli
+

Checksums

+

See checksums.txt for SHA-256 checksums of all release artifacts.

]]>
+ + 14.0 +
Version 0.9.0.b8 0.9.0.b8 @@ -1326,46 +1409,6 @@ type="application/octet-stream"/> 14.0 - - Version 0.7.0.b11 - 0.7.0.b11 - 0.7.0.b11 - Sun, 12 Apr 2026 23:00:14 +0000 - Installation - -
    -
  1. Download TermQ-0.7.0-beta.11.dmg
  2. -
  3. Open the DMG and drag TermQ.app to your Applications folder
  4. -
  5. Double-click to launch
  6. -
-

Alternative: Zip Archive

-
    -
  1. Download TermQ-0.7.0-beta.11.zip
  2. -
  3. Unzip and move TermQ.app to your Applications folder
  4. -
-

CLI Tool

-

The termqcli CLI is bundled inside the app. To install it:

-
    -
  1. Open TermQ.app
  2. -
  3. Go to TermQ → Settings (or press ⌘,)
  4. -
  5. Click Install Command Line Tool
  6. -
-

Or manually:

-
sudo cp /Applications/TermQ.app/Contents/Resources/termqcli /usr/local/bin/termqcli
-

Checksums

-

See checksums.txt for SHA-256 checksums of all release artifacts.

-

What's Changed

- -

Full Changelog: https://github.com/eyelock/TermQ/compare/v0.7.0-beta.10...v0.7.0-beta.11

]]>
- - 14.0 -
diff --git a/Docs/appcast.xml b/Docs/appcast.xml index 701d2235..cdb9ceeb 100644 --- a/Docs/appcast.xml +++ b/Docs/appcast.xml @@ -5,6 +5,89 @@ https://github.com/eyelock/TermQ Most recent updates to TermQ en + + Version 0.9.0 + 0.9.0 + 0.9.0 + Mon, 27 Apr 2026 21:09:42 +0000 + What's New +
    +
  • right-click context menu in terminal with Copy and Paste (#221)
  • +
  • YNH 0.2 support — namespace model, marketplace rename, fixture tests (#218)
  • +
  • sidebar drag-and-drop, branch ops, harness/marketplace ordering (#217)
  • +
  • add in-app diagnostics log viewer (#193)
  • +
  • Protected branches deny-list for Prune Merged Branches (#178)
  • +
  • Pin marketplace and harness registry to a git ref (#171)
  • +
  • Open in editor submenu for worktree and harness context menus (#169)
  • +
  • Generate release notes from conventional commits (#168)
  • +
  • Auto-tags, terminal naming, and tag tooltip (#158)
  • +
  • Harness sidebar quality-of-life improvements (#156)
  • +
  • Harness and Marketplace sidebars adopt tree layout with grouping (#154)
  • +
  • UX polish, marketplace improvements, and terminal enhancements (#151)
  • +
  • Marketplace browser, harness wizard, and install/launch management (#148)
  • +
  • YNH harness sidebar — detect, install, launch, manage (#142)
  • +
  • Git worktree sidebar (#126)
  • +
+

Bug Fixes

+
    +
  • fix upward scroll during text selection against streaming output (#224)
  • +
  • restore Reveal in Terminal across sidebar tabs (#223)
  • +
  • prevent SwiftUI from closing main window on URL events (#222)
  • +
  • github install + YNH 0.2.0 capabilities gate (#210)
  • +
  • expand github source shorthand for ynh include add (#203)
  • +
  • guard NSApp.unhide against calling on visible app to prevent spontaneous window hide (#199)
  • +
  • show spinner in worktree row during deletion (#195)
  • +
  • eliminate all build and lint warnings (#191)
  • +
  • eliminate live file monitoring side effects in BoardViewModelDefaultsTests (#185)
  • +
  • Persist harness-launched cards and deduplicate on re-launch (#179)
  • +
  • Inherit all security settings from global defaults when creating terminals (#177)
  • +
  • Restore tab drag-and-drop by replacing Button with onTapGesture (#176)
  • +
  • resolve relative plugin paths in ynh include add (#175)
  • +
  • fix printf treating triple-dash separator as flag on macOS (#173)
  • +
  • use echo instead of printf for bullet list items in release notes (#172)
  • +
  • Fix text selection and scroll in terminals with mouse tracking (#147)
  • +
  • Correct TUI rendering in tmux control mode terminals (#138) (#141)
  • +
  • Disable Sparkle auto-updater in debug builds (#137)
  • +
  • Use file-based key storage in debug builds (#130)
  • +
  • Debug build shows SHA in About panel; document debug behaviors (#131)
  • +
  • Use async panel.begin() for Browse button in path pickers (#132)
  • +
  • Preserve tmux pane focus across SwiftUI re-renders (#133)
  • +
  • Migrate SecureStorage to Data Protection Keychain (#124) (#129)
  • +
  • Scroll tab bar to reveal selected terminal on sidebar jump (#125) (#128)
  • +
  • Scroll tab bar to reveal selected terminal on sidebar jump (#125)
  • +
  • Migrate SecureStorage to Data Protection Keychain (#124)
  • +
+
+

Installation

+ +
    +
  1. Download TermQ-0.9.0.dmg
  2. +
  3. Open the DMG and drag TermQ.app to your Applications folder
  4. +
  5. Double-click to launch
  6. +
+

Alternative: Zip Archive

+
    +
  1. Download TermQ-0.9.0.zip
  2. +
  3. Unzip and move TermQ.app to your Applications folder
  4. +
+

CLI Tool

+

The termqcli CLI is bundled inside the app. To install it:

+
    +
  1. Open TermQ.app
  2. +
  3. Go to TermQ → Settings (or press ⌘,)
  4. +
  5. Click Install Command Line Tool
  6. +
+

Or manually:

+
sudo cp /Applications/TermQ.app/Contents/Resources/termqcli /usr/local/bin/termqcli
+

Checksums

+

See checksums.txt for SHA-256 checksums of all release artifacts.

]]>
+ + 14.0 +
Version 0.8.2 0.8.2 From bfd6342d050500bd32954f94dbef29bb3201c2b2 Mon Sep 17 00:00:00 2001 From: David Collie Date: Tue, 28 Apr 2026 07:06:59 +0100 Subject: [PATCH 02/29] fix(ci): fix appcast not updating on stable release (#233) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Co-authored-by: Claude Sonnet 4.6 --- .github/workflows/release.yml | 8 ---- .github/workflows/update-appcast.yml | 11 +++-- scripts/generate-appcast.sh | 60 ++++++++++++++++++++-------- 3 files changed, 52 insertions(+), 27 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 30ff1c2a..bdc82842 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -453,14 +453,6 @@ jobs: TermQ-${{ steps.version.outputs.VERSION }}.zip.sig checksums.txt - - name: Trigger appcast update - env: - GH_TOKEN: ${{ secrets.APPCAST_TOKEN }} - run: | - echo "Triggering appcast update workflow..." - gh workflow run update-appcast.yml - echo "✅ Appcast update triggered" - - name: Cleanup keychain if: always() run: | diff --git a/.github/workflows/update-appcast.yml b/.github/workflows/update-appcast.yml index ee08c7fe..e20dae3e 100644 --- a/.github/workflows/update-appcast.yml +++ b/.github/workflows/update-appcast.yml @@ -1,11 +1,15 @@ name: Update Appcast on: - release: - types: [published] + workflow_run: + workflows: ["Release"] + types: [completed] workflow_dispatch: # Allow manual trigger to regenerate appcast +# Only update appcast when the Release workflow succeeds +# (workflow_dispatch always proceeds — used for manual regeneration) + permissions: contents: write pull-requests: write @@ -14,6 +18,7 @@ jobs: update-appcast: name: Update Appcast runs-on: ubuntu-latest + if: github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' steps: - name: Checkout @@ -52,7 +57,7 @@ jobs: git config user.email "github-actions[bot]@users.noreply.github.com" BRANCH="hotfix/appcast-update" - RELEASE_TAG="${{ github.event.release.tag_name || 'manual' }}" + RELEASE_TAG="${{ github.event.workflow_run.head_branch || 'manual' }}" git checkout -B "$BRANCH" git add Docs/appcast.xml Docs/appcast-beta.xml diff --git a/scripts/generate-appcast.sh b/scripts/generate-appcast.sh index 39e8d93d..62d2795c 100755 --- a/scripts/generate-appcast.sh +++ b/scripts/generate-appcast.sh @@ -48,26 +48,54 @@ check_dependencies() { fetch_releases() { log_info "Fetching releases from GitHub..." - local api_url="https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/releases" - local tmpfile - tmpfile=$(mktemp) - - # Fetch to temp file and sanitize JSON (remove control characters that break jq) - if ! curl -sS "$api_url" 2>/dev/null | tr -d '\000-\011\013-\037' > "$tmpfile"; then - log_error "Failed to fetch releases from $api_url" - rm -f "$tmpfile" - exit 1 + local base_url="https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/releases" + local all_releases="[]" + local page=1 + + # Use GH_TOKEN if available — authenticated requests bypass the API cache so a + # freshly published release is visible without waiting for cache expiry + local auth_header=() + if [ -n "${GH_TOKEN:-}" ]; then + auth_header=(-H "Authorization: Bearer $GH_TOKEN") fi - # Validate JSON - if ! jq empty "$tmpfile" 2>/dev/null; then - log_error "Invalid JSON response from GitHub API" + while true; do + local tmpfile + tmpfile=$(mktemp) + local url="${base_url}?per_page=100&page=${page}" + + if ! curl -sS "${auth_header[@]}" "$url" 2>/dev/null | tr -d '\000-\011\013-\037' > "$tmpfile"; then + log_error "Failed to fetch releases page $page" + rm -f "$tmpfile" + exit 1 + fi + + if ! jq empty "$tmpfile" 2>/dev/null; then + log_error "Invalid JSON from GitHub API (page $page)" + rm -f "$tmpfile" + exit 1 + fi + + local count + count=$(jq 'length' "$tmpfile") + + if [[ "$count" -eq 0 ]]; then + rm -f "$tmpfile" + break + fi + + local page_data + page_data=$(cat "$tmpfile") rm -f "$tmpfile" - exit 1 - fi - cat "$tmpfile" - rm -f "$tmpfile" + all_releases=$(printf '%s\n%s' "$all_releases" "$page_data" | jq -s 'add') + + [[ "$count" -lt 100 ]] && break + page=$((page + 1)) + done + + log_info "Found $(echo "$all_releases" | jq 'length') release(s)" + echo "$all_releases" } # Convert GitHub release date to RFC 2822 format From 032138e28e4af9f7493d1c893d9c91777890c8db Mon Sep 17 00:00:00 2001 From: David Collie Date: Tue, 28 Apr 2026 07:35:21 +0100 Subject: [PATCH 03/29] fix(harnesses): fix uninstall for local harnesses with no YNH install 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 Co-authored-by: Claude Sonnet 4.6 --- .../Resources/ar.lproj/Localizable.strings | 2 + .../Resources/ca.lproj/Localizable.strings | 2 + .../Resources/cs.lproj/Localizable.strings | 2 + .../Resources/da.lproj/Localizable.strings | 2 + .../Resources/de.lproj/Localizable.strings | 2 + .../Resources/el.lproj/Localizable.strings | 2 + .../Resources/en-AU.lproj/Localizable.strings | 2 + .../Resources/en-GB.lproj/Localizable.strings | 2 + .../Resources/en.lproj/Localizable.strings | 2 + .../es-419.lproj/Localizable.strings | 2 + .../Resources/es.lproj/Localizable.strings | 2 + .../Resources/fi.lproj/Localizable.strings | 2 + .../Resources/fr-CA.lproj/Localizable.strings | 2 + .../Resources/fr.lproj/Localizable.strings | 2 + .../Resources/he.lproj/Localizable.strings | 2 + .../Resources/hi.lproj/Localizable.strings | 2 + .../Resources/hr.lproj/Localizable.strings | 2 + .../Resources/hu.lproj/Localizable.strings | 2 + .../Resources/id.lproj/Localizable.strings | 2 + .../Resources/it.lproj/Localizable.strings | 2 + .../Resources/ja.lproj/Localizable.strings | 2 + .../Resources/ko.lproj/Localizable.strings | 2 + .../Resources/ms.lproj/Localizable.strings | 2 + .../Resources/nl.lproj/Localizable.strings | 2 + .../Resources/no.lproj/Localizable.strings | 2 + .../Resources/pl.lproj/Localizable.strings | 2 + .../Resources/pt-PT.lproj/Localizable.strings | 2 + .../Resources/pt.lproj/Localizable.strings | 2 + .../Resources/ro.lproj/Localizable.strings | 2 + .../Resources/ru.lproj/Localizable.strings | 2 + .../Resources/sk.lproj/Localizable.strings | 2 + .../Resources/sl.lproj/Localizable.strings | 2 + .../Resources/sv.lproj/Localizable.strings | 2 + .../Resources/th.lproj/Localizable.strings | 2 + .../Resources/tr.lproj/Localizable.strings | 2 + .../Resources/uk.lproj/Localizable.strings | 2 + .../Resources/vi.lproj/Localizable.strings | 2 + .../Resources/zh-HK.lproj/Localizable.strings | 2 + .../zh-Hans.lproj/Localizable.strings | 2 + .../zh-Hant.lproj/Localizable.strings | 2 + Sources/TermQ/Utilities/Strings+Sidebar.swift | 12 ++ Sources/TermQ/Views/ContentView.swift | 16 ++- Sources/TermQ/Views/HarnessDetailView.swift | 2 +- .../Views/Sidebar/HarnessesSidebarTab.swift | 3 +- .../TermQSharedTests/HarnessModelTests.swift | 114 ++++++++++++++++++ Tests/TermQTests/HarnessRepositoryTests.swift | 54 +++++++++ 46 files changed, 277 insertions(+), 4 deletions(-) diff --git a/Sources/TermQ/Resources/ar.lproj/Localizable.strings b/Sources/TermQ/Resources/ar.lproj/Localizable.strings index 093561bc..4db83281 100644 --- a/Sources/TermQ/Resources/ar.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/ar.lproj/Localizable.strings @@ -717,6 +717,8 @@ "harnesses.uninstall.help" = "إلغاء تثبيت هذا الـ harness"; "harnesses.uninstall.alert.title %@" = "إلغاء تثبيت \"%@\"؟"; "harnesses.uninstall.alert.message" = "سيؤدي هذا إلى إزالة الـ harness من نظامك."; +"harnesses.uninstall.alert.message.local" = "سيؤدي هذا إلى إزالة harness من YNH. لن يتم حذف ملفاتك المصدر المحلية."; +"harnesses.uninstall.alert.message.untracked" = "لا يوجد لهذا harness أي سجل تثبيت في YNH. سيتم حذفه مباشرةً من القرص."; "harnesses.uninstall.alert.worktrees %ld" = "هذا الـ harness مرتبط بـ %ld مستودع(ات) عمل — ستُمسح تلك الارتباطات."; "harnesses.uninstall.alert.terminals %ld" = "%ld محطة (محطات) طرفية تُشغّل هذا الـ harness حاليًا."; "harnesses.uninstall.alert.confirm" = "إلغاء التثبيت"; diff --git a/Sources/TermQ/Resources/ca.lproj/Localizable.strings b/Sources/TermQ/Resources/ca.lproj/Localizable.strings index 58d742d2..42dfe5a4 100644 --- a/Sources/TermQ/Resources/ca.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/ca.lproj/Localizable.strings @@ -717,6 +717,8 @@ "harnesses.uninstall.help" = "Desinstal·la aquest harness"; "harnesses.uninstall.alert.title %@" = "Desinstal·lar \"%@\"?"; "harnesses.uninstall.alert.message" = "Això eliminarà el harness del vostre sistema."; +"harnesses.uninstall.alert.message.local" = "Això eliminarà el harness de YNH. Els vostres fitxers font locals no s'eliminaran."; +"harnesses.uninstall.alert.message.untracked" = "Aquest harness no té cap registre d'instal·lació a YNH. S'eliminarà directament del disc."; "harnesses.uninstall.alert.worktrees %ld" = "Aquest harness està vinculat a %ld arbre(s) de treball — aquestes associacions s'esboraran."; "harnesses.uninstall.alert.terminals %ld" = "%ld terminal(s) estan executant aquest harness ara mateix."; "harnesses.uninstall.alert.confirm" = "Desinstal·la"; diff --git a/Sources/TermQ/Resources/cs.lproj/Localizable.strings b/Sources/TermQ/Resources/cs.lproj/Localizable.strings index 8f1acf9c..ecc2aa1a 100644 --- a/Sources/TermQ/Resources/cs.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/cs.lproj/Localizable.strings @@ -717,6 +717,8 @@ "harnesses.uninstall.help" = "Odinstalovat tento harness"; "harnesses.uninstall.alert.title %@" = "Odinstalovat \"%@\"?"; "harnesses.uninstall.alert.message" = "Tím se harness odebere z vašeho systému."; +"harnesses.uninstall.alert.message.local" = "Tím se odstraní harness z YNH. Vaše místní zdrojové soubory nebudou smazány."; +"harnesses.uninstall.alert.message.untracked" = "Tento harness nemá žádný záznam o instalaci v YNH. Bude odstraněn přímo z disku."; "harnesses.uninstall.alert.worktrees %ld" = "Tento harness je propojen s %ld pracovním stromem (stromy) — tato propojení budou vymazána."; "harnesses.uninstall.alert.terminals %ld" = "%ld terminál(ů) aktuálně spouští tento harness."; "harnesses.uninstall.alert.confirm" = "Odinstalovat"; diff --git a/Sources/TermQ/Resources/da.lproj/Localizable.strings b/Sources/TermQ/Resources/da.lproj/Localizable.strings index cd998299..13d46067 100644 --- a/Sources/TermQ/Resources/da.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/da.lproj/Localizable.strings @@ -717,6 +717,8 @@ "harnesses.uninstall.help" = "Afinstaller denne harness"; "harnesses.uninstall.alert.title %@" = "Afinstaller \"%@\"?"; "harnesses.uninstall.alert.message" = "Dette fjerner harness fra dit system."; +"harnesses.uninstall.alert.message.local" = "Dette fjerner harness fra YNH. Dine lokale kildefiler slettes ikke."; +"harnesses.uninstall.alert.message.untracked" = "Denne harness har ingen YNH-installationspost. Den slettes direkte fra disken."; "harnesses.uninstall.alert.worktrees %ld" = "Denne harness er knyttet til %ld worktree(s) — disse tilknytninger ryddes."; "harnesses.uninstall.alert.terminals %ld" = "%ld terminal(er) kører i øjeblikket denne harness."; "harnesses.uninstall.alert.confirm" = "Afinstaller"; diff --git a/Sources/TermQ/Resources/de.lproj/Localizable.strings b/Sources/TermQ/Resources/de.lproj/Localizable.strings index 195ccfc4..32b02afc 100644 --- a/Sources/TermQ/Resources/de.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/de.lproj/Localizable.strings @@ -717,6 +717,8 @@ "harnesses.uninstall.help" = "Diesen Harness deinstallieren"; "harnesses.uninstall.alert.title %@" = "\"%@\" deinstallieren?"; "harnesses.uninstall.alert.message" = "Dadurch wird der Harness von Ihrem System entfernt."; +"harnesses.uninstall.alert.message.local" = "Dadurch wird der Harness aus YNH entfernt. Ihre lokalen Quelldateien werden nicht gelöscht."; +"harnesses.uninstall.alert.message.untracked" = "Dieser Harness hat keinen YNH-Installationseintrag. Er wird direkt von der Festplatte gelöscht."; "harnesses.uninstall.alert.worktrees %ld" = "Dieser Harness ist mit %ld Worktree(s) verknüpft — diese Verknüpfungen werden aufgehoben."; "harnesses.uninstall.alert.terminals %ld" = "%ld Terminal(s) führen diesen Harness gerade aus."; "harnesses.uninstall.alert.confirm" = "Deinstallieren"; diff --git a/Sources/TermQ/Resources/el.lproj/Localizable.strings b/Sources/TermQ/Resources/el.lproj/Localizable.strings index 1ccd141c..efcc7d3d 100644 --- a/Sources/TermQ/Resources/el.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/el.lproj/Localizable.strings @@ -717,6 +717,8 @@ "harnesses.uninstall.help" = "Απεγκατάσταση αυτού του harness"; "harnesses.uninstall.alert.title %@" = "Απεγκατάσταση \"%@\";"; "harnesses.uninstall.alert.message" = "Αυτό θα αφαιρέσει το harness από το σύστημά σας."; +"harnesses.uninstall.alert.message.local" = "Αυτό θα αφαιρέσει το harness από το YNH. Τα τοπικά αρχεία πηγαίου κώδικα δεν θα διαγραφούν."; +"harnesses.uninstall.alert.message.untracked" = "Αυτό το harness δεν έχει εγγραφή εγκατάστασης στο YNH. Θα διαγραφεί απευθείας από τον δίσκο."; "harnesses.uninstall.alert.worktrees %ld" = "Αυτό το harness είναι συνδεδεμένο με %ld worktree(s) — αυτές οι συσχετίσεις θα διαγραφούν."; "harnesses.uninstall.alert.terminals %ld" = "%ld τερματικό(ά) εκτελεί αυτήν τη στιγμή αυτό το harness."; "harnesses.uninstall.alert.confirm" = "Απεγκατάσταση"; diff --git a/Sources/TermQ/Resources/en-AU.lproj/Localizable.strings b/Sources/TermQ/Resources/en-AU.lproj/Localizable.strings index 07ca660b..dab62510 100644 --- a/Sources/TermQ/Resources/en-AU.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/en-AU.lproj/Localizable.strings @@ -715,6 +715,8 @@ "harnesses.uninstall.help" = "Uninstall this harness"; "harnesses.uninstall.alert.title %@" = "Uninstall \"%@\"?"; "harnesses.uninstall.alert.message" = "This will remove the harness from your system."; +"harnesses.uninstall.alert.message.local" = "This will remove the harness from YNH. Your local source files will not be deleted."; +"harnesses.uninstall.alert.message.untracked" = "This harness has no YNH install record. It will be deleted directly from disk."; "harnesses.uninstall.alert.worktrees %ld" = "This harness is linked to %ld worktree(s) — those associations will be cleared."; "harnesses.uninstall.alert.terminals %ld" = "%ld terminal(s) are currently running this harness."; "harnesses.uninstall.alert.confirm" = "Uninstall"; diff --git a/Sources/TermQ/Resources/en-GB.lproj/Localizable.strings b/Sources/TermQ/Resources/en-GB.lproj/Localizable.strings index 7cdb177d..ffa1f8e6 100644 --- a/Sources/TermQ/Resources/en-GB.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/en-GB.lproj/Localizable.strings @@ -715,6 +715,8 @@ "harnesses.uninstall.help" = "Uninstall this harness"; "harnesses.uninstall.alert.title %@" = "Uninstall \"%@\"?"; "harnesses.uninstall.alert.message" = "This will remove the harness from your system."; +"harnesses.uninstall.alert.message.local" = "This will remove the harness from YNH. Your local source files will not be deleted."; +"harnesses.uninstall.alert.message.untracked" = "This harness has no YNH install record. It will be deleted directly from disk."; "harnesses.uninstall.alert.worktrees %ld" = "This harness is linked to %ld worktree(s) — those associations will be cleared."; "harnesses.uninstall.alert.terminals %ld" = "%ld terminal(s) are currently running this harness."; "harnesses.uninstall.alert.confirm" = "Uninstall"; diff --git a/Sources/TermQ/Resources/en.lproj/Localizable.strings b/Sources/TermQ/Resources/en.lproj/Localizable.strings index 83a5743e..695bbff7 100644 --- a/Sources/TermQ/Resources/en.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/en.lproj/Localizable.strings @@ -765,6 +765,8 @@ "harnesses.uninstall.help" = "Uninstall this harness"; "harnesses.uninstall.alert.title %@" = "Uninstall \"%@\"?"; "harnesses.uninstall.alert.message" = "This will remove the harness from your system."; +"harnesses.uninstall.alert.message.local" = "This will remove the harness from YNH. Your local source files will not be deleted."; +"harnesses.uninstall.alert.message.untracked" = "This harness has no YNH install record. It will be deleted directly from disk."; "harnesses.uninstall.alert.worktrees %ld" = "This harness is linked to %ld worktree(s) — those associations will be cleared."; "harnesses.uninstall.alert.terminals %ld" = "%ld terminal(s) are currently running this harness."; "harnesses.uninstall.alert.confirm" = "Uninstall"; diff --git a/Sources/TermQ/Resources/es-419.lproj/Localizable.strings b/Sources/TermQ/Resources/es-419.lproj/Localizable.strings index e35acb7e..6592f399 100644 --- a/Sources/TermQ/Resources/es-419.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/es-419.lproj/Localizable.strings @@ -717,6 +717,8 @@ "harnesses.uninstall.help" = "Desinstalar este harness"; "harnesses.uninstall.alert.title %@" = "¿Desinstalar \"%@\"?"; "harnesses.uninstall.alert.message" = "Esto eliminará el harness de tu sistema."; +"harnesses.uninstall.alert.message.local" = "Esto eliminará el harness de YNH. Tus archivos fuente locales no se eliminarán."; +"harnesses.uninstall.alert.message.untracked" = "Este harness no tiene ningún registro de instalación en YNH. Se eliminará directamente del disco."; "harnesses.uninstall.alert.worktrees %ld" = "Este harness está vinculado a %ld árbol(es) de trabajo — esas asociaciones se eliminarán."; "harnesses.uninstall.alert.terminals %ld" = "%ld terminal(es) están ejecutando este harness actualmente."; "harnesses.uninstall.alert.confirm" = "Desinstalar"; diff --git a/Sources/TermQ/Resources/es.lproj/Localizable.strings b/Sources/TermQ/Resources/es.lproj/Localizable.strings index b84227f4..01481cb9 100644 --- a/Sources/TermQ/Resources/es.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/es.lproj/Localizable.strings @@ -717,6 +717,8 @@ "harnesses.uninstall.help" = "Desinstalar este harness"; "harnesses.uninstall.alert.title %@" = "¿Desinstalar \"%@\"?"; "harnesses.uninstall.alert.message" = "Esto eliminará el harness de tu sistema."; +"harnesses.uninstall.alert.message.local" = "Esto eliminará el harness de YNH. Sus archivos fuente locales no se eliminarán."; +"harnesses.uninstall.alert.message.untracked" = "Este harness no tiene ningún registro de instalación en YNH. Se eliminará directamente del disco."; "harnesses.uninstall.alert.worktrees %ld" = "Este harness está vinculado a %ld árbol(es) de trabajo — esas asociaciones se eliminarán."; "harnesses.uninstall.alert.terminals %ld" = "%ld terminal(es) están ejecutando este harness actualmente."; "harnesses.uninstall.alert.confirm" = "Desinstalar"; diff --git a/Sources/TermQ/Resources/fi.lproj/Localizable.strings b/Sources/TermQ/Resources/fi.lproj/Localizable.strings index e02bb906..2307a6e9 100644 --- a/Sources/TermQ/Resources/fi.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/fi.lproj/Localizable.strings @@ -723,6 +723,8 @@ "harnesses.uninstall.help" = "Poista tämän harnessin asennus"; "harnesses.uninstall.alert.title %@" = "Poistetaanko \"%@\"?"; "harnesses.uninstall.alert.message" = "Tämä poistaa harnessin järjestelmästäsi."; +"harnesses.uninstall.alert.message.local" = "Tämä poistaa harnessin YNH:sta. Paikallisia lähdetiedostojasi ei poisteta."; +"harnesses.uninstall.alert.message.untracked" = "Tällä harnessilla ei ole YNH-asennustietuetta. Se poistetaan suoraan levyltä."; "harnesses.uninstall.alert.worktrees %ld" = "Tämä harness on linkitetty %ld työtree(hen/hin) — nämä yhteydet poistetaan."; "harnesses.uninstall.alert.terminals %ld" = "%ld pääte(ttä) käyttää tällä hetkellä tätä harnessia."; "harnesses.uninstall.alert.confirm" = "Poista asennus"; diff --git a/Sources/TermQ/Resources/fr-CA.lproj/Localizable.strings b/Sources/TermQ/Resources/fr-CA.lproj/Localizable.strings index a8ebe335..62c14e67 100644 --- a/Sources/TermQ/Resources/fr-CA.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/fr-CA.lproj/Localizable.strings @@ -723,6 +723,8 @@ "harnesses.uninstall.help" = "Désinstaller ce harnais"; "harnesses.uninstall.alert.title %@" = "Désinstaller \"%@\" ?"; "harnesses.uninstall.alert.message" = "Le harnais sera supprimé de votre système."; +"harnesses.uninstall.alert.message.local" = "Cela supprimera le harness de YNH. Vos fichiers sources locaux ne seront pas supprimés."; +"harnesses.uninstall.alert.message.untracked" = "Ce harness n'a aucun enregistrement d'installation dans YNH. Il sera supprimé directement du disque."; "harnesses.uninstall.alert.worktrees %ld" = "Ce harnais est lié à %ld arbre(s) de travail — ces associations seront supprimées."; "harnesses.uninstall.alert.terminals %ld" = "%ld terminal(s) exécutent actuellement ce harnais."; "harnesses.uninstall.alert.confirm" = "Désinstaller"; diff --git a/Sources/TermQ/Resources/fr.lproj/Localizable.strings b/Sources/TermQ/Resources/fr.lproj/Localizable.strings index 1d6d6208..344af5de 100644 --- a/Sources/TermQ/Resources/fr.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/fr.lproj/Localizable.strings @@ -723,6 +723,8 @@ "harnesses.uninstall.help" = "Désinstaller ce harnais"; "harnesses.uninstall.alert.title %@" = "Désinstaller \"%@\" ?"; "harnesses.uninstall.alert.message" = "Le harnais sera supprimé de votre système."; +"harnesses.uninstall.alert.message.local" = "Cela supprimera le harness de YNH. Vos fichiers sources locaux ne seront pas supprimés."; +"harnesses.uninstall.alert.message.untracked" = "Ce harness n'a aucun enregistrement d'installation dans YNH. Il sera supprimé directement du disque."; "harnesses.uninstall.alert.worktrees %ld" = "Ce harnais est lié à %ld arbre(s) de travail — ces associations seront supprimées."; "harnesses.uninstall.alert.terminals %ld" = "%ld terminal(s) exécutent actuellement ce harnais."; "harnesses.uninstall.alert.confirm" = "Désinstaller"; diff --git a/Sources/TermQ/Resources/he.lproj/Localizable.strings b/Sources/TermQ/Resources/he.lproj/Localizable.strings index 7430ddd1..2a078450 100644 --- a/Sources/TermQ/Resources/he.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/he.lproj/Localizable.strings @@ -723,6 +723,8 @@ "harnesses.uninstall.help" = "הסר את ה-harness הזה"; "harnesses.uninstall.alert.title %@" = "להסיר את \"%@\"?"; "harnesses.uninstall.alert.message" = "פעולה זו תסיר את ה-harness מהמערכת שלך."; +"harnesses.uninstall.alert.message.local" = "פעולה זו תסיר את ה-harness מ-YNH. הקבצים המקומיים שלך לא יימחקו."; +"harnesses.uninstall.alert.message.untracked" = "ל-harness זה אין רשומת התקנה ב-YNH. הוא יימחק ישירות מהדיסק."; "harnesses.uninstall.alert.worktrees %ld" = "ה-harness הזה מקושר ל-%ld worktree(s) — הקשרים הללו יימחקו."; "harnesses.uninstall.alert.terminals %ld" = "%ld מסוף(ים) מריצים כעת את ה-harness הזה."; "harnesses.uninstall.alert.confirm" = "הסר התקנה"; diff --git a/Sources/TermQ/Resources/hi.lproj/Localizable.strings b/Sources/TermQ/Resources/hi.lproj/Localizable.strings index 46bb565d..68e24982 100644 --- a/Sources/TermQ/Resources/hi.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/hi.lproj/Localizable.strings @@ -723,6 +723,8 @@ "harnesses.uninstall.help" = "इस harness को अनइंस्टॉल करें"; "harnesses.uninstall.alert.title %@" = "\"%@\" को अनइंस्टॉल करें?"; "harnesses.uninstall.alert.message" = "यह आपके सिस्टम से harness को हटा देगा।"; +"harnesses.uninstall.alert.message.local" = "यह YNH से harness को हटा देगा। आपकी स्थानीय स्रोत फ़ाइलें हटाई नहीं जाएंगी।"; +"harnesses.uninstall.alert.message.untracked" = "इस harness का YNH में कोई इंस्टॉल रिकॉर्ड नहीं है। इसे सीधे डिस्क से हटाया जाएगा।"; "harnesses.uninstall.alert.worktrees %ld" = "यह harness %ld worktree(s) से जुड़ा है — वे संबद्धताएं साफ़ कर दी जाएंगी।"; "harnesses.uninstall.alert.terminals %ld" = "%ld टर्मिनल वर्तमान में इस harness को चला रहे हैं।"; "harnesses.uninstall.alert.confirm" = "अनइंस्टॉल करें"; diff --git a/Sources/TermQ/Resources/hr.lproj/Localizable.strings b/Sources/TermQ/Resources/hr.lproj/Localizable.strings index bde2b164..9f791af5 100644 --- a/Sources/TermQ/Resources/hr.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/hr.lproj/Localizable.strings @@ -723,6 +723,8 @@ "harnesses.uninstall.help" = "Deinstaliraj ovaj harness"; "harnesses.uninstall.alert.title %@" = "Deinstalirati \"%@\"?"; "harnesses.uninstall.alert.message" = "Time će se harness ukloniti s vašeg sustava."; +"harnesses.uninstall.alert.message.local" = "Ovo će ukloniti harness iz YNH-a. Vaše lokalne izvorne datoteke neće biti izbrisane."; +"harnesses.uninstall.alert.message.untracked" = "Ovaj harness nema zapisa o instalaciji u YNH-u. Bit će izbrisan izravno s diska."; "harnesses.uninstall.alert.worktrees %ld" = "Ovaj harness je povezan s %ld worktree(om/ovima) — te će asocijacije biti obrisane."; "harnesses.uninstall.alert.terminals %ld" = "%ld terminal(a) trenutno izvodi ovaj harness."; "harnesses.uninstall.alert.confirm" = "Deinstaliraj"; diff --git a/Sources/TermQ/Resources/hu.lproj/Localizable.strings b/Sources/TermQ/Resources/hu.lproj/Localizable.strings index ff8abce8..ac4bb4c2 100644 --- a/Sources/TermQ/Resources/hu.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/hu.lproj/Localizable.strings @@ -723,6 +723,8 @@ "harnesses.uninstall.help" = "Ez a harness eltávolítása"; "harnesses.uninstall.alert.title %@" = "Eltávolítja: \"%@\"?"; "harnesses.uninstall.alert.message" = "Ez eltávolítja a harness-t a rendszeréből."; +"harnesses.uninstall.alert.message.local" = "Ez eltávolítja a harness-t a YNH-ból. A helyi forrásfájlok nem törlődnek."; +"harnesses.uninstall.alert.message.untracked" = "Ennek a harness-nek nincs YNH-telepítési bejegyzése. Közvetlenül a lemezről törlődik."; "harnesses.uninstall.alert.worktrees %ld" = "Ez a harness %ld munkafahoz van csatolva — ezek a kapcsolatok törlődnek."; "harnesses.uninstall.alert.terminals %ld" = "%ld terminál jelenleg ezt a harness-t futtatja."; "harnesses.uninstall.alert.confirm" = "Eltávolítás"; diff --git a/Sources/TermQ/Resources/id.lproj/Localizable.strings b/Sources/TermQ/Resources/id.lproj/Localizable.strings index 0d3f39a5..ae47eb60 100644 --- a/Sources/TermQ/Resources/id.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/id.lproj/Localizable.strings @@ -717,6 +717,8 @@ "harnesses.uninstall.help" = "Hapus instalasi harness ini"; "harnesses.uninstall.alert.title %@" = "Hapus instalasi \"%@\"?"; "harnesses.uninstall.alert.message" = "Ini akan menghapus harness dari sistem Anda."; +"harnesses.uninstall.alert.message.local" = "Ini akan menghapus harness dari YNH. File sumber lokal Anda tidak akan dihapus."; +"harnesses.uninstall.alert.message.untracked" = "Harness ini tidak memiliki catatan instalasi YNH. Harness akan dihapus langsung dari disk."; "harnesses.uninstall.alert.worktrees %ld" = "Harness ini terhubung ke %ld worktree — asosiasi tersebut akan dihapus."; "harnesses.uninstall.alert.terminals %ld" = "%ld terminal sedang menjalankan harness ini."; "harnesses.uninstall.alert.confirm" = "Hapus Instalasi"; diff --git a/Sources/TermQ/Resources/it.lproj/Localizable.strings b/Sources/TermQ/Resources/it.lproj/Localizable.strings index 95f26f23..db9bca84 100644 --- a/Sources/TermQ/Resources/it.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/it.lproj/Localizable.strings @@ -717,6 +717,8 @@ "harnesses.uninstall.help" = "Disinstalla questo harness"; "harnesses.uninstall.alert.title %@" = "Disinstallare \"%@\"?"; "harnesses.uninstall.alert.message" = "Questo rimuoverà il harness dal sistema."; +"harnesses.uninstall.alert.message.local" = "Questa operazione rimuoverà il harness da YNH. I file sorgente locali non verranno eliminati."; +"harnesses.uninstall.alert.message.untracked" = "Questo harness non ha alcun record di installazione YNH. Verrà eliminato direttamente dal disco."; "harnesses.uninstall.alert.worktrees %ld" = "Questo harness è collegato a %ld worktree — le relative associazioni verranno cancellate."; "harnesses.uninstall.alert.terminals %ld" = "%ld terminale/i sta attualmente eseguendo questo harness."; "harnesses.uninstall.alert.confirm" = "Disinstalla"; diff --git a/Sources/TermQ/Resources/ja.lproj/Localizable.strings b/Sources/TermQ/Resources/ja.lproj/Localizable.strings index 4934a645..0d6e56ff 100644 --- a/Sources/TermQ/Resources/ja.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/ja.lproj/Localizable.strings @@ -717,6 +717,8 @@ "harnesses.uninstall.help" = "このHarnessをアンインストール"; "harnesses.uninstall.alert.title %@" = "「%@」をアンインストールしますか?"; "harnesses.uninstall.alert.message" = "Harnessがシステムから削除されます。"; +"harnesses.uninstall.alert.message.local" = "これにより YNH から harness が削除されます。ローカルのソースファイルは削除されません。"; +"harnesses.uninstall.alert.message.untracked" = "この harness には YNH のインストール記録がありません。ディスクから直接削除されます。"; "harnesses.uninstall.alert.worktrees %ld" = "このHarnessは%ld個のWorktreeにリンクされています — それらの関連付けが解除されます。"; "harnesses.uninstall.alert.terminals %ld" = "%ld個のターミナルが現在このHarnessを実行中です。"; "harnesses.uninstall.alert.confirm" = "アンインストール"; diff --git a/Sources/TermQ/Resources/ko.lproj/Localizable.strings b/Sources/TermQ/Resources/ko.lproj/Localizable.strings index ae10b68d..f4f69c85 100644 --- a/Sources/TermQ/Resources/ko.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/ko.lproj/Localizable.strings @@ -722,6 +722,8 @@ "harnesses.uninstall.help" = "이 Harness 제거"; "harnesses.uninstall.alert.title %@" = "\"%@\"을(를) 제거하시겠습니까?"; "harnesses.uninstall.alert.message" = "이 Harness가 시스템에서 제거됩니다."; +"harnesses.uninstall.alert.message.local" = "YNH에서 harness가 제거됩니다. 로컬 소스 파일은 삭제되지 않습니다."; +"harnesses.uninstall.alert.message.untracked" = "이 harness는 YNH 설치 기록이 없습니다. 디스크에서 직접 삭제됩니다."; "harnesses.uninstall.alert.worktrees %ld" = "이 Harness는 %ld개의 Worktree에 연결되어 있습니다 — 해당 연결이 해제됩니다."; "harnesses.uninstall.alert.terminals %ld" = "현재 %ld개의 터미널이 이 Harness를 실행 중입니다."; "harnesses.uninstall.alert.confirm" = "제거"; diff --git a/Sources/TermQ/Resources/ms.lproj/Localizable.strings b/Sources/TermQ/Resources/ms.lproj/Localizable.strings index 2441af49..c29852d3 100644 --- a/Sources/TermQ/Resources/ms.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/ms.lproj/Localizable.strings @@ -722,6 +722,8 @@ "harnesses.uninstall.help" = "Nyahpasang harness ini"; "harnesses.uninstall.alert.title %@" = "Nyahpasang \"%@\"?"; "harnesses.uninstall.alert.message" = "Ini akan membuang harness daripada sistem anda."; +"harnesses.uninstall.alert.message.local" = "Ini akan mengalih keluar harness daripada YNH. Fail sumber tempatan anda tidak akan dipadam."; +"harnesses.uninstall.alert.message.untracked" = "Harness ini tidak mempunyai rekod pemasangan YNH. Ia akan dipadam terus dari cakera."; "harnesses.uninstall.alert.worktrees %ld" = "Harness ini dikaitkan dengan %ld worktree — perkaitan tersebut akan dipadamkan."; "harnesses.uninstall.alert.terminals %ld" = "%ld terminal sedang menjalankan harness ini."; "harnesses.uninstall.alert.confirm" = "Nyahpasang"; diff --git a/Sources/TermQ/Resources/nl.lproj/Localizable.strings b/Sources/TermQ/Resources/nl.lproj/Localizable.strings index f7ee4b4b..a0adaddd 100644 --- a/Sources/TermQ/Resources/nl.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/nl.lproj/Localizable.strings @@ -722,6 +722,8 @@ "harnesses.uninstall.help" = "Deze harness verwijderen"; "harnesses.uninstall.alert.title %@" = "\"%@\" verwijderen?"; "harnesses.uninstall.alert.message" = "Hiermee wordt de harness van uw systeem verwijderd."; +"harnesses.uninstall.alert.message.local" = "Hierdoor wordt de harness uit YNH verwijderd. Uw lokale bronbestanden worden niet verwijderd."; +"harnesses.uninstall.alert.message.untracked" = "Deze harness heeft geen YNH-installatierecord. Hij wordt rechtstreeks van de schijf verwijderd."; "harnesses.uninstall.alert.worktrees %ld" = "Deze harness is gekoppeld aan %ld worktree(s) — die koppelingen worden gewist."; "harnesses.uninstall.alert.terminals %ld" = "%ld terminal(s) voeren deze harness momenteel uit."; "harnesses.uninstall.alert.confirm" = "Verwijderen"; diff --git a/Sources/TermQ/Resources/no.lproj/Localizable.strings b/Sources/TermQ/Resources/no.lproj/Localizable.strings index d27007fe..fab19078 100644 --- a/Sources/TermQ/Resources/no.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/no.lproj/Localizable.strings @@ -722,6 +722,8 @@ "harnesses.uninstall.help" = "Avinstaller denne harness"; "harnesses.uninstall.alert.title %@" = "Avinstaller \"%@\"?"; "harnesses.uninstall.alert.message" = "Dette fjerner harness fra systemet ditt."; +"harnesses.uninstall.alert.message.local" = "Dette vil fjerne harness fra YNH. Dine lokale kildefiler vil ikke bli slettet."; +"harnesses.uninstall.alert.message.untracked" = "Denne harness har ingen YNH-installasjonspost. Den slettes direkte fra disken."; "harnesses.uninstall.alert.worktrees %ld" = "Denne harness er koblet til %ld worktree(r) — disse koblingene vil bli fjernet."; "harnesses.uninstall.alert.terminals %ld" = "%ld terminal(er) kjører denne harness for øyeblikket."; "harnesses.uninstall.alert.confirm" = "Avinstaller"; diff --git a/Sources/TermQ/Resources/pl.lproj/Localizable.strings b/Sources/TermQ/Resources/pl.lproj/Localizable.strings index b7f7dfa5..ded51ce2 100644 --- a/Sources/TermQ/Resources/pl.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/pl.lproj/Localizable.strings @@ -722,6 +722,8 @@ "harnesses.uninstall.help" = "Odinstaluj ten harness"; "harnesses.uninstall.alert.title %@" = "Odinstalować \"%@\"?"; "harnesses.uninstall.alert.message" = "Spowoduje to usunięcie harness z systemu."; +"harnesses.uninstall.alert.message.local" = "Spowoduje to usunięcie harness z YNH. Twoje lokalne pliki źródłowe nie zostaną usunięte."; +"harnesses.uninstall.alert.message.untracked" = "Ten harness nie ma rekordu instalacji YNH. Zostanie usunięty bezpośrednio z dysku."; "harnesses.uninstall.alert.worktrees %ld" = "Ten harness jest powiązany z %ld drzewem/drzewami roboczymi — te powiązania zostaną usunięte."; "harnesses.uninstall.alert.terminals %ld" = "%ld terminal(i) aktualnie uruchamia ten harness."; "harnesses.uninstall.alert.confirm" = "Odinstaluj"; diff --git a/Sources/TermQ/Resources/pt-PT.lproj/Localizable.strings b/Sources/TermQ/Resources/pt-PT.lproj/Localizable.strings index 73692c68..40321717 100644 --- a/Sources/TermQ/Resources/pt-PT.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/pt-PT.lproj/Localizable.strings @@ -722,6 +722,8 @@ "harnesses.uninstall.help" = "Desinstalar este harness"; "harnesses.uninstall.alert.title %@" = "Desinstalar \"%@\"?"; "harnesses.uninstall.alert.message" = "Isto irá remover o harness do seu sistema."; +"harnesses.uninstall.alert.message.local" = "Isto irá remover o harness do YNH. Os seus ficheiros de origem locais não serão eliminados."; +"harnesses.uninstall.alert.message.untracked" = "Este harness não tem registo de instalação no YNH. Será eliminado diretamente do disco."; "harnesses.uninstall.alert.worktrees %ld" = "Este harness está ligado a %ld worktree(s) — essas associações serão eliminadas."; "harnesses.uninstall.alert.terminals %ld" = "%ld terminal(is) estão actualmente a executar este harness."; "harnesses.uninstall.alert.confirm" = "Desinstalar"; diff --git a/Sources/TermQ/Resources/pt.lproj/Localizable.strings b/Sources/TermQ/Resources/pt.lproj/Localizable.strings index 2d7042c9..25bc22a5 100644 --- a/Sources/TermQ/Resources/pt.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/pt.lproj/Localizable.strings @@ -722,6 +722,8 @@ "harnesses.uninstall.help" = "Desinstalar este harness"; "harnesses.uninstall.alert.title %@" = "Desinstalar \"%@\"?"; "harnesses.uninstall.alert.message" = "Isso removerá o harness do seu sistema."; +"harnesses.uninstall.alert.message.local" = "Isto irá remover o harness do YNH. Os seus ficheiros de origem locais não serão eliminados."; +"harnesses.uninstall.alert.message.untracked" = "Este harness não tem registro de instalação no YNH. Ele será excluído diretamente do disco."; "harnesses.uninstall.alert.worktrees %ld" = "Este harness está vinculado a %ld worktree(s) — essas associações serão removidas."; "harnesses.uninstall.alert.terminals %ld" = "%ld terminal(is) estão executando este harness no momento."; "harnesses.uninstall.alert.confirm" = "Desinstalar"; diff --git a/Sources/TermQ/Resources/ro.lproj/Localizable.strings b/Sources/TermQ/Resources/ro.lproj/Localizable.strings index 54447317..44a30f10 100644 --- a/Sources/TermQ/Resources/ro.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/ro.lproj/Localizable.strings @@ -717,6 +717,8 @@ "harnesses.uninstall.help" = "Dezinstalați acest harness"; "harnesses.uninstall.alert.title %@" = "Dezinstalați \"%@\"?"; "harnesses.uninstall.alert.message" = "Aceasta va elimina harness-ul din sistemul dvs."; +"harnesses.uninstall.alert.message.local" = "Aceasta va elimina harness din YNH. Fișierele sursă locale nu vor fi șterse."; +"harnesses.uninstall.alert.message.untracked" = "Acest harness nu are niciun înregistrare de instalare YNH. Va fi șters direct de pe disc."; "harnesses.uninstall.alert.worktrees %ld" = "Acest harness este legat de %ld worktree(uri) — aceste asocieri vor fi șterse."; "harnesses.uninstall.alert.terminals %ld" = "%ld terminal(e) rulează în prezent acest harness."; "harnesses.uninstall.alert.confirm" = "Dezinstalare"; diff --git a/Sources/TermQ/Resources/ru.lproj/Localizable.strings b/Sources/TermQ/Resources/ru.lproj/Localizable.strings index 8df75c3f..98703257 100644 --- a/Sources/TermQ/Resources/ru.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/ru.lproj/Localizable.strings @@ -717,6 +717,8 @@ "harnesses.uninstall.help" = "Удалить этот harness"; "harnesses.uninstall.alert.title %@" = "Удалить \"%@\"?"; "harnesses.uninstall.alert.message" = "Это удалит harness с вашей системы."; +"harnesses.uninstall.alert.message.local" = "Это удалит harness из YNH. Ваши локальные исходные файлы удалены не будут."; +"harnesses.uninstall.alert.message.untracked" = "У этого harness нет записи об установке в YNH. Он будет удалён непосредственно с диска."; "harnesses.uninstall.alert.worktrees %ld" = "Этот harness связан с %ld рабочим деревом (деревьями) — эти связи будут удалены."; "harnesses.uninstall.alert.terminals %ld" = "%ld терминал(а/ов) в данный момент запускает этот harness."; "harnesses.uninstall.alert.confirm" = "Удалить"; diff --git a/Sources/TermQ/Resources/sk.lproj/Localizable.strings b/Sources/TermQ/Resources/sk.lproj/Localizable.strings index b3902801..322579f1 100644 --- a/Sources/TermQ/Resources/sk.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/sk.lproj/Localizable.strings @@ -717,6 +717,8 @@ "harnesses.uninstall.help" = "Odinštalovať tento harness"; "harnesses.uninstall.alert.title %@" = "Odinštalovať \"%@\"?"; "harnesses.uninstall.alert.message" = "Týmto sa harness odstráni z vášho systému."; +"harnesses.uninstall.alert.message.local" = "Týmto sa harness odstráni z YNH. Vaše lokálne zdrojové súbory nebudú vymazané."; +"harnesses.uninstall.alert.message.untracked" = "Tento harness nemá žiadny záznam o inštalácii v YNH. Bude odstránený priamo z disku."; "harnesses.uninstall.alert.worktrees %ld" = "Tento harness je prepojený s %ld pracovným stromom (stromami) — tieto prepojenia budú vymazané."; "harnesses.uninstall.alert.terminals %ld" = "%ld terminál(ov) momentálne spúšťa tento harness."; "harnesses.uninstall.alert.confirm" = "Odinštalovať"; diff --git a/Sources/TermQ/Resources/sl.lproj/Localizable.strings b/Sources/TermQ/Resources/sl.lproj/Localizable.strings index 09e03494..e56c6ea9 100644 --- a/Sources/TermQ/Resources/sl.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/sl.lproj/Localizable.strings @@ -724,6 +724,8 @@ "harnesses.uninstall.help" = "Odstrani ta harness"; "harnesses.uninstall.alert.title %@" = "Odstraniti \"%@\"?"; "harnesses.uninstall.alert.message" = "S tem boste odstranili harness iz vašega sistema."; +"harnesses.uninstall.alert.message.local" = "S tem boste odstranili harness iz YNH. Vaše lokalne izvorne datoteke ne bodo izbrisane."; +"harnesses.uninstall.alert.message.untracked" = "Ta harness nima zapisa o namestitvi v YNH. Izbrisal se bo neposredno z diska."; "harnesses.uninstall.alert.worktrees %ld" = "Ta harness je povezan z %ld delovnim drevesom (drevesi) — te povezave bodo počiščene."; "harnesses.uninstall.alert.terminals %ld" = "%ld terminal(ov) trenutno poganja ta harness."; "harnesses.uninstall.alert.confirm" = "Odstrani"; diff --git a/Sources/TermQ/Resources/sv.lproj/Localizable.strings b/Sources/TermQ/Resources/sv.lproj/Localizable.strings index 8ca9e29a..340c81f0 100644 --- a/Sources/TermQ/Resources/sv.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/sv.lproj/Localizable.strings @@ -724,6 +724,8 @@ "harnesses.uninstall.help" = "Avinstallera denna harness"; "harnesses.uninstall.alert.title %@" = "Avinstallera \"%@\"?"; "harnesses.uninstall.alert.message" = "Detta tar bort harness från ditt system."; +"harnesses.uninstall.alert.message.local" = "Det här tar bort harness från YNH. Dina lokala källfiler tas inte bort."; +"harnesses.uninstall.alert.message.untracked" = "Den här harness har inget YNH-installationspost. Den kommer att raderas direkt från disken."; "harnesses.uninstall.alert.worktrees %ld" = "Denna harness är länkad till %ld worktree(s) — dessa kopplingar kommer att rensas."; "harnesses.uninstall.alert.terminals %ld" = "%ld terminal(er) kör för närvarande denna harness."; "harnesses.uninstall.alert.confirm" = "Avinstallera"; diff --git a/Sources/TermQ/Resources/th.lproj/Localizable.strings b/Sources/TermQ/Resources/th.lproj/Localizable.strings index 599c4736..5dbf7aa7 100644 --- a/Sources/TermQ/Resources/th.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/th.lproj/Localizable.strings @@ -724,6 +724,8 @@ "harnesses.uninstall.help" = "ถอนการติดตั้ง harness นี้"; "harnesses.uninstall.alert.title %@" = "ถอนการติดตั้ง \"%@\" หรือไม่?"; "harnesses.uninstall.alert.message" = "การดำเนินการนี้จะลบ harness ออกจากระบบของคุณ"; +"harnesses.uninstall.alert.message.local" = "การดำเนินการนี้จะลบ harness ออกจาก YNH ไฟล์ต้นฉบับในเครื่องของคุณจะไม่ถูกลบ"; +"harnesses.uninstall.alert.message.untracked" = "harness นี้ไม่มีบันทึกการติดตั้งใน YNH จะถูกลบออกจากดิสก์โดยตรง"; "harnesses.uninstall.alert.worktrees %ld" = "harness นี้เชื่อมโยงกับ %ld worktree — การเชื่อมโยงเหล่านั้นจะถูกล้าง"; "harnesses.uninstall.alert.terminals %ld" = "ขณะนี้มี %ld เทอร์มินัลที่กำลังรัน harness นี้"; "harnesses.uninstall.alert.confirm" = "ถอนการติดตั้ง"; diff --git a/Sources/TermQ/Resources/tr.lproj/Localizable.strings b/Sources/TermQ/Resources/tr.lproj/Localizable.strings index 879abeac..33b682c6 100644 --- a/Sources/TermQ/Resources/tr.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/tr.lproj/Localizable.strings @@ -724,6 +724,8 @@ "harnesses.uninstall.help" = "Bu harness'i kaldır"; "harnesses.uninstall.alert.title %@" = "\"%@\" kaldırılsın mı?"; "harnesses.uninstall.alert.message" = "Bu, harness'i sisteminizden kaldıracak."; +"harnesses.uninstall.alert.message.local" = "Bu işlem harness'i YNH'dan kaldıracak. Yerel kaynak dosyalarınız silinmeyecek."; +"harnesses.uninstall.alert.message.untracked" = "Bu harness'in YNH'da kurulum kaydı yok. Doğrudan diskten silinecek."; "harnesses.uninstall.alert.worktrees %ld" = "Bu harness %ld worktree ile bağlantılı — bu ilişkilendirmeler temizlenecek."; "harnesses.uninstall.alert.terminals %ld" = "%ld terminal şu anda bu harness'i çalıştırıyor."; "harnesses.uninstall.alert.confirm" = "Kaldır"; diff --git a/Sources/TermQ/Resources/uk.lproj/Localizable.strings b/Sources/TermQ/Resources/uk.lproj/Localizable.strings index 72f4a7b9..6a9b8a1d 100644 --- a/Sources/TermQ/Resources/uk.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/uk.lproj/Localizable.strings @@ -724,6 +724,8 @@ "harnesses.uninstall.help" = "Видалити цей harness"; "harnesses.uninstall.alert.title %@" = "Видалити \"%@\"?"; "harnesses.uninstall.alert.message" = "Це видалить harness з вашої системи."; +"harnesses.uninstall.alert.message.local" = "Це видалить harness із YNH. Ваші локальні вихідні файли не буде видалено."; +"harnesses.uninstall.alert.message.untracked" = "Цей harness не має запису про встановлення в YNH. Його буде видалено безпосередньо з диска."; "harnesses.uninstall.alert.worktrees %ld" = "Цей harness пов'язаний з %ld робочим деревом (деревами) — ці зв'язки буде очищено."; "harnesses.uninstall.alert.terminals %ld" = "%ld термінал(и) зараз виконують цей harness."; "harnesses.uninstall.alert.confirm" = "Видалити"; diff --git a/Sources/TermQ/Resources/vi.lproj/Localizable.strings b/Sources/TermQ/Resources/vi.lproj/Localizable.strings index 8956cff6..83d3ba4c 100644 --- a/Sources/TermQ/Resources/vi.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/vi.lproj/Localizable.strings @@ -724,6 +724,8 @@ "harnesses.uninstall.help" = "Gỡ cài đặt harness này"; "harnesses.uninstall.alert.title %@" = "Gỡ cài đặt \"%@\"?"; "harnesses.uninstall.alert.message" = "Thao tác này sẽ xóa harness khỏi hệ thống của bạn."; +"harnesses.uninstall.alert.message.local" = "Thao tác này sẽ xóa harness khỏi YNH. Các tệp nguồn cục bộ của bạn sẽ không bị xóa."; +"harnesses.uninstall.alert.message.untracked" = "Harness này không có bản ghi cài đặt trong YNH. Nó sẽ bị xóa trực tiếp khỏi ổ đĩa."; "harnesses.uninstall.alert.worktrees %ld" = "Harness này được liên kết với %ld worktree — các liên kết đó sẽ bị xóa."; "harnesses.uninstall.alert.terminals %ld" = "%ld terminal đang chạy harness này."; "harnesses.uninstall.alert.confirm" = "Gỡ cài đặt"; diff --git a/Sources/TermQ/Resources/zh-HK.lproj/Localizable.strings b/Sources/TermQ/Resources/zh-HK.lproj/Localizable.strings index c9084b84..a07ccbda 100644 --- a/Sources/TermQ/Resources/zh-HK.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/zh-HK.lproj/Localizable.strings @@ -717,6 +717,8 @@ "harnesses.uninstall.help" = "解除安裝此 harness"; "harnesses.uninstall.alert.title %@" = "解除安裝「%@」?"; "harnesses.uninstall.alert.message" = "這將從您的系統中移除該 harness。"; +"harnesses.uninstall.alert.message.local" = "呢個操作會喺 YNH 移除該 harness。您嘅本地原始碼檔案唔會被刪除。"; +"harnesses.uninstall.alert.message.untracked" = "呢個 harness 喺 YNH 冇安裝記錄,將會直接從磁碟刪除。"; "harnesses.uninstall.alert.worktrees %ld" = "此 harness 已連結 %ld 個工作樹——這些關聯將被清除。"; "harnesses.uninstall.alert.terminals %ld" = "目前有 %ld 個終端機正在執行此 harness。"; "harnesses.uninstall.alert.confirm" = "解除安裝"; diff --git a/Sources/TermQ/Resources/zh-Hans.lproj/Localizable.strings b/Sources/TermQ/Resources/zh-Hans.lproj/Localizable.strings index a2269fad..991c0718 100644 --- a/Sources/TermQ/Resources/zh-Hans.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/zh-Hans.lproj/Localizable.strings @@ -717,6 +717,8 @@ "harnesses.uninstall.help" = "卸载此 harness"; "harnesses.uninstall.alert.title %@" = "卸载\"%@\"?"; "harnesses.uninstall.alert.message" = "这将从您的系统中移除该 harness。"; +"harnesses.uninstall.alert.message.local" = "这将从 YNH 中移除该 harness。您的本地源文件不会被删除。"; +"harnesses.uninstall.alert.message.untracked" = "此 harness 在 YNH 中没有安装记录,将直接从磁盘删除。"; "harnesses.uninstall.alert.worktrees %ld" = "此 harness 已关联 %ld 个工作树——这些关联将被清除。"; "harnesses.uninstall.alert.terminals %ld" = "当前有 %ld 个终端正在运行此 harness。"; "harnesses.uninstall.alert.confirm" = "卸载"; diff --git a/Sources/TermQ/Resources/zh-Hant.lproj/Localizable.strings b/Sources/TermQ/Resources/zh-Hant.lproj/Localizable.strings index 9d0af79d..7b5617a7 100644 --- a/Sources/TermQ/Resources/zh-Hant.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/zh-Hant.lproj/Localizable.strings @@ -717,6 +717,8 @@ "harnesses.uninstall.help" = "移除此 harness 的安裝"; "harnesses.uninstall.alert.title %@" = "移除安裝「%@」?"; "harnesses.uninstall.alert.message" = "這將從您的系統中移除該 harness。"; +"harnesses.uninstall.alert.message.local" = "這將從 YNH 中移除該 harness。您的本機原始碼檔案不會被刪除。"; +"harnesses.uninstall.alert.message.untracked" = "此 harness 在 YNH 中沒有安裝記錄,將直接從磁碟中刪除。"; "harnesses.uninstall.alert.worktrees %ld" = "此 harness 已連結 %ld 個工作樹——這些關聯將被清除。"; "harnesses.uninstall.alert.terminals %ld" = "目前有 %ld 個終端機正在執行此 harness。"; "harnesses.uninstall.alert.confirm" = "移除安裝"; diff --git a/Sources/TermQ/Utilities/Strings+Sidebar.swift b/Sources/TermQ/Utilities/Strings+Sidebar.swift index 914d7bdf..f22626f6 100644 --- a/Sources/TermQ/Utilities/Strings+Sidebar.swift +++ b/Sources/TermQ/Utilities/Strings+Sidebar.swift @@ -1,5 +1,6 @@ import Foundation import SwiftUI +import TermQShared // MARK: - Sidebar @@ -272,6 +273,17 @@ extension Strings { String(format: localized("harnesses.uninstall.alert.title %@"), name) } static var uninstallAlertMessage: String { localized("harnesses.uninstall.alert.message") } + static var uninstallAlertMessageLocal: String { localized("harnesses.uninstall.alert.message.local") } + static var uninstallAlertMessageUntracked: String { localized("harnesses.uninstall.alert.message.untracked") } + static func uninstallBaseMessage(for harness: Harness) -> String { + if harness.installedFrom == nil { + return uninstallAlertMessageUntracked + } else if harness.installedFrom?.sourceType == "local" { + return uninstallAlertMessageLocal + } else { + return uninstallAlertMessage + } + } static func uninstallAlertWorktrees(_ count: Int) -> String { String(format: localized("harnesses.uninstall.alert.worktrees %ld"), count) } diff --git a/Sources/TermQ/Views/ContentView.swift b/Sources/TermQ/Views/ContentView.swift index 22dc002d..13007a6b 100644 --- a/Sources/TermQ/Views/ContentView.swift +++ b/Sources/TermQ/Views/ContentView.swift @@ -782,8 +782,22 @@ extension ContentView { viewModel.selectedCard = card } - /// Uninstall a harness in a transient terminal; clears associations when the shell exits. + /// Uninstall a harness. Harnesses with no YNH install record are deleted directly + /// from the filesystem; YNH-managed harnesses use a transient terminal and clear + /// associations when the shell exits. func uninstallHarness(name: String) { + let harness = harnessRepo.harnesses.first(where: { $0.name == name }) + + // Harnesses with no YNH install record can't be uninstalled via `ynh uninstall`. + // Delete them directly and clean up associations. + if let harness, harness.installedFrom == nil { + try? FileManager.default.removeItem(at: URL(fileURLWithPath: harness.path)) + YNHPersistence.shared.removeAllAssociations(for: name) + harnessRepo.selectedHarnessName = nil + Task { await harnessRepo.refresh() } + return + } + guard case .ready(let ynhPath, _, _) = ynhDetector.status else { return } let column: Column if let current = viewModel.selectedCard, diff --git a/Sources/TermQ/Views/HarnessDetailView.swift b/Sources/TermQ/Views/HarnessDetailView.swift index 0233e228..d281e1b0 100644 --- a/Sources/TermQ/Views/HarnessDetailView.swift +++ b/Sources/TermQ/Views/HarnessDetailView.swift @@ -460,7 +460,7 @@ extension HarnessDetailView { } fileprivate var uninstallAlertMessage: String { - var parts = [Strings.Harnesses.uninstallAlertMessage] + var parts = [Strings.Harnesses.uninstallBaseMessage(for: harness)] let linkedCount = ynhPersistence.worktrees(for: harness.name).count if linkedCount > 0 { parts.append(Strings.Harnesses.uninstallAlertWorktrees(linkedCount)) diff --git a/Sources/TermQ/Views/Sidebar/HarnessesSidebarTab.swift b/Sources/TermQ/Views/Sidebar/HarnessesSidebarTab.swift index 3d5b0b4a..5daa6e76 100644 --- a/Sources/TermQ/Views/Sidebar/HarnessesSidebarTab.swift +++ b/Sources/TermQ/Views/Sidebar/HarnessesSidebarTab.swift @@ -194,7 +194,7 @@ struct HarnessesSidebarTab: View { Text( linked > 0 ? Strings.Harnesses.uninstallAlertWorktrees(linked) - : Strings.Harnesses.uninstallAlertMessage + : Strings.Harnesses.uninstallBaseMessage(for: harness) ) } } @@ -574,7 +574,6 @@ extension HarnessesSidebarTab { fileprivate func performDeleteLocalHarness(_ harness: Harness) { onUninstall?(harness.name) - try? FileManager.default.removeItem(at: URL(fileURLWithPath: harness.path)) } private func revealLocalGroupInFinder(_ group: HarnessGroup) { diff --git a/Tests/TermQSharedTests/HarnessModelTests.swift b/Tests/TermQSharedTests/HarnessModelTests.swift index 4e1ae94a..0d9169fc 100644 --- a/Tests/TermQSharedTests/HarnessModelTests.swift +++ b/Tests/TermQSharedTests/HarnessModelTests.swift @@ -199,6 +199,120 @@ final class HarnessModelTests: XCTestCase { XCTAssertNil(h.description) } + // MARK: - Harness uninstall provenance + + /// A harness with no install record (e.g. a locally-authored harness that was never + /// `ynh install`ed) must have `installedFrom == nil`. The UI uses this to skip + /// `ynh uninstall` and delete the directory directly. + func testHarness_noInstallRecord_hasNilProvenance() throws { + let json = """ + { + "name": "github-tester", + "version": "0.1.0", + "default_vendor": "claude", + "path": "/Users/dev/harnesses/github-tester", + "installed_from": null, + "artifacts": {"skills": 0, "agents": 0, "rules": 0, "commands": 0}, + "includes": [], + "delegates_to": [] + } + """ + let h = try JSONDecoder().decode(Harness.self, from: json.data(using: .utf8)!) + XCTAssertNil(h.installedFrom) + } + + /// A harness installed via `ynh install ./local-path` has `source_type == "local"`. + /// The UI routes these through `ynh uninstall`, which succeeds because ynh has an install record. + func testHarness_locallyInstalledViaYNH_hasLocalSourceType() throws { + let json = """ + { + "name": "assistants-dev", + "version": "0.1.0", + "default_vendor": "claude", + "path": "/Users/dev/.ynh/harnesses/assistants-dev", + "installed_from": { + "source_type": "local", + "source": "/Users/dev/harnesses/assistants-dev", + "path": null, + "registry_name": null, + "installed_at": "2026-01-01T00:00:00Z" + }, + "artifacts": {"skills": 1, "agents": 0, "rules": 0, "commands": 0}, + "includes": [], + "delegates_to": [] + } + """ + let h = try JSONDecoder().decode(Harness.self, from: json.data(using: .utf8)!) + XCTAssertNotNil(h.installedFrom) + XCTAssertEqual(h.installedFrom?.sourceType, "local") + } + + /// A harness with no install record uninstalled from HarnessDetailView shows the untracked + /// alert message, not the generic or local-specific one. + func testHarness_noInstallRecord_isDistinctFromLocalSourceType() throws { + let nilProvenanceJSON = """ + { + "name": "untracked", + "version": "0.1.0", + "default_vendor": "claude", + "path": "/Users/dev/harnesses/untracked", + "installed_from": null, + "artifacts": {"skills": 0, "agents": 0, "rules": 0, "commands": 0}, + "includes": [], + "delegates_to": [] + } + """ + let localJSON = """ + { + "name": "assistants-dev", + "version": "0.1.0", + "default_vendor": "claude", + "path": "/Users/dev/.ynh/harnesses/assistants-dev", + "installed_from": { + "source_type": "local", + "source": "/Users/dev/harnesses/assistants-dev", + "path": null, + "registry_name": null, + "installed_at": "2026-01-01T00:00:00Z" + }, + "artifacts": {"skills": 0, "agents": 0, "rules": 0, "commands": 0}, + "includes": [], + "delegates_to": [] + } + """ + let untracked = try JSONDecoder().decode(Harness.self, from: nilProvenanceJSON.data(using: .utf8)!) + let local = try JSONDecoder().decode(Harness.self, from: localJSON.data(using: .utf8)!) + XCTAssertNil(untracked.installedFrom) + XCTAssertNotNil(local.installedFrom) + XCTAssertEqual(local.installedFrom?.sourceType, "local") + XCTAssertNotEqual(untracked.installedFrom?.sourceType, local.installedFrom?.sourceType) + } + + /// A registry harness has `source_type == "registry"` and is always uninstalled via ynh. + func testHarness_registryInstalled_hasRegistrySourceType() throws { + let json = """ + { + "name": "david", + "version": "0.1.0", + "default_vendor": "claude", + "path": "/Users/dev/.ynh/harnesses/david", + "installed_from": { + "source_type": "registry", + "source": "https://github.com/eyelock/assistants", + "path": null, + "registry_name": "eyelock-assistants", + "installed_at": "2026-01-01T00:00:00Z" + }, + "artifacts": {"skills": 2, "agents": 1, "rules": 0, "commands": 0}, + "includes": [], + "delegates_to": [] + } + """ + let h = try JSONDecoder().decode(Harness.self, from: json.data(using: .utf8)!) + XCTAssertNotNil(h.installedFrom) + XCTAssertEqual(h.installedFrom?.sourceType, "registry") + } + // MARK: - JSONFragment func testJSONFragment_decodesObject() throws { diff --git a/Tests/TermQTests/HarnessRepositoryTests.swift b/Tests/TermQTests/HarnessRepositoryTests.swift index c2605e88..838c3578 100644 --- a/Tests/TermQTests/HarnessRepositoryTests.swift +++ b/Tests/TermQTests/HarnessRepositoryTests.swift @@ -173,3 +173,57 @@ final class HarnessRepositoryInvalidationTests: XCTestCase { XCTAssertNil(repo.selectedDetail) } } + +// MARK: - Strings.Harnesses.uninstallBaseMessage + +@MainActor +final class HarnessUninstallMessageTests: XCTestCase { + + private func makeHarness(sourceType: String?) -> Harness { + let installedFrom: HarnessProvenance? = sourceType.map { type in + try! JSONDecoder().decode( + HarnessProvenance.self, + from: """ + {"source_type":"\(type)","source":"/tmp/h","path":null,"registry_name":null,"installed_at":"2026-01-01"} + """.data(using: .utf8)! + ) + } + return Harness( + name: "h", version: "1", defaultVendor: "claude", path: "/tmp/h", + installedFrom: installedFrom, + artifacts: HarnessArtifactCounts(skills: 0, agents: 0, rules: 0, commands: 0) + ) + } + + func test_untrackedHarness_usesUntrackedMessage() { + let harness = makeHarness(sourceType: nil) + XCTAssertEqual( + Strings.Harnesses.uninstallBaseMessage(for: harness), + Strings.Harnesses.uninstallAlertMessageUntracked + ) + } + + func test_localInstalledHarness_usesLocalMessage() { + let harness = makeHarness(sourceType: "local") + XCTAssertEqual( + Strings.Harnesses.uninstallBaseMessage(for: harness), + Strings.Harnesses.uninstallAlertMessageLocal + ) + } + + func test_registryHarness_usesGenericMessage() { + let harness = makeHarness(sourceType: "registry") + XCTAssertEqual( + Strings.Harnesses.uninstallBaseMessage(for: harness), + Strings.Harnesses.uninstallAlertMessage + ) + } + + func test_gitHarness_usesGenericMessage() { + let harness = makeHarness(sourceType: "git") + XCTAssertEqual( + Strings.Harnesses.uninstallBaseMessage(for: harness), + Strings.Harnesses.uninstallAlertMessage + ) + } +} From ee2d325da514429e19b757a62ed0db5172a19f15 Mon Sep 17 00:00:00 2001 From: David Collie Date: Tue, 28 Apr 2026 07:53:07 +0100 Subject: [PATCH 04/29] =?UTF-8?q?docs(skills):=20update=20hotfix=20procedu?= =?UTF-8?q?re=20=E2=80=94=20PR=20before=20CI,=20CHANGELOG,=20appcast=20for?= =?UTF-8?q?ward-port?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Open PR to main before waiting for CI (CI runs on the PR) - Always update CHANGELOG.md on the hotfix branch before tagging - Forward-port PR must include CHANGELOG and appcast changes Co-Authored-By: Claude Sonnet 4.6 --- .claude/skills/release/references/hotfix.md | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/.claude/skills/release/references/hotfix.md b/.claude/skills/release/references/hotfix.md index 34550b02..be85e95a 100644 --- a/.claude/skills/release/references/hotfix.md +++ b/.claude/skills/release/references/hotfix.md @@ -18,21 +18,28 @@ Use for critical production bugs, security vulnerabilities, or data loss issues git checkout -b hotfix/v0.6.4 v0.6.3 ``` -### 2. Implement the Fix +### 2. Implement the Fix and Update CHANGELOG Apply the fix directly on the hotfix branch. Keep it minimal — only the targeted change. +**Always update `CHANGELOG.md`** in the same commit or as a follow-up commit on the hotfix branch — not as a separate PR afterwards. The changelog entry must be present before tagging. + ```bash git add git commit -m "fix: " +# Update CHANGELOG.md, then: +git add CHANGELOG.md +git commit -m "chore: update CHANGELOG for v0.6.4" git push -u origin hotfix/v0.6.4 ``` -### 3. Wait for CI +### 3. Open a PR to Main and Wait for CI -**MANDATORY before tagging.** The CI workflow runs on `hotfix/*` branches. +**Open a PR targeting `main` before tagging.** CI runs on the PR — do not tag until it passes. ```bash +gh pr create --base main --title "fix: hotfix v0.6.4" \ + --body "Hotfix release v0.6.4. Cherry-picks onto v0.6.3." gh run list --branch hotfix/v0.6.4 --workflow=ci.yml --limit 1 gh run watch ``` @@ -75,7 +82,7 @@ gh pr create --base develop --title "fix: forward-port hotfix v0.6.4" \ Merge once CI passes. If the cherry-pick has conflicts (develop has diverged significantly), resolve them before pushing. -**Auto-generated files (appcasts):** The `update-appcast.yml` workflow updates `Docs/appcast.xml` and `Docs/appcast-beta.xml` on main automatically after each release. These changes are never automatically forward-ported. After every release — stable or hotfix — create a forward-port PR that includes the updated appcast files. +**Auto-generated files (appcasts):** The `update-appcast.yml` workflow updates `Docs/appcast.xml` and `Docs/appcast-beta.xml` on main automatically after each release. These changes are never automatically forward-ported. After every release — stable or hotfix — create a forward-port PR that includes the updated appcast files AND any CHANGELOG changes from the hotfix branch. ## What NOT to Do From 10d00b3712ebee31165a3b7f7a312387e5dea009 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 28 Apr 2026 07:09:02 +0000 Subject: [PATCH 05/29] chore: Update appcast for release v0.9.1 Auto-generated appcast files for Sparkle auto-updates. Co-Authored-By: github-actions[bot] --- Docs/appcast-beta.xml | 96 ++++++++++++++++++------------------------- Docs/appcast.xml | 41 ++++++++++++++++++ 2 files changed, 82 insertions(+), 55 deletions(-) diff --git a/Docs/appcast-beta.xml b/Docs/appcast-beta.xml index 308b0e11..434f5c97 100644 --- a/Docs/appcast-beta.xml +++ b/Docs/appcast-beta.xml @@ -5,6 +5,47 @@ https://github.com/eyelock/TermQ Most recent updates to TermQ en + + Version 0.9.1 + 0.9.1 + 0.9.1 + Tue, 28 Apr 2026 07:08:13 +0000 + Bug Fixes +
    +
  • fix uninstall for local harnesses with no YNH install record (#234)
  • +
  • fix appcast not updating on stable release (#233)
  • +
+
+

Installation

+ +
    +
  1. Download TermQ-0.9.1.dmg
  2. +
  3. Open the DMG and drag TermQ.app to your Applications folder
  4. +
  5. Double-click to launch
  6. +
+

Alternative: Zip Archive

+
    +
  1. Download TermQ-0.9.1.zip
  2. +
  3. Unzip and move TermQ.app to your Applications folder
  4. +
+

CLI Tool

+

The termqcli CLI is bundled inside the app. To install it:

+
    +
  1. Open TermQ.app
  2. +
  3. Go to TermQ → Settings (or press ⌘,)
  4. +
  5. Click Install Command Line Tool
  6. +
+

Or manually:

+
sudo cp /Applications/TermQ.app/Contents/Resources/termqcli /usr/local/bin/termqcli
+

Checksums

+

See checksums.txt for SHA-256 checksums of all release artifacts.

]]>
+ + 14.0 +
Version 0.9.0 0.9.0 @@ -1354,61 +1395,6 @@ type="application/octet-stream"/> 14.0 - - Version 0.7.0.b5 - 0.7.0.b5 - 0.7.0.b5 - Sun, 12 Apr 2026 11:08:29 +0000 - BROKEN APPCAST SIGNING See https://github.com/eyelock/TermQ/pull/109 for fix

-

Installation

- -
    -
  1. Download TermQ-0.7.0-beta.5.dmg
  2. -
  3. Open the DMG and drag TermQ.app to your Applications folder
  4. -
  5. Double-click to launch
  6. -
-

Alternative: Zip Archive

-
    -
  1. Download TermQ-0.7.0-beta.5.zip
  2. -
  3. Unzip and move TermQ.app to your Applications folder
  4. -
-

CLI Tool

-

The termqcli CLI is bundled inside the app. To install it:

-
    -
  1. Open TermQ.app
  2. -
  3. Go to TermQ → Settings (or press ⌘,)
  4. -
  5. Click Install Command Line Tool
  6. -
-

Or manually:

-
sudo cp /Applications/TermQ.app/Contents/Resources/termqcli /usr/local/bin/termqcli
-

Checksums

-

See checksums.txt for SHA-256 checksums of all release artifacts.

-

What's Changed

- -

New Contributors

- -

Full Changelog: https://github.com/eyelock/TermQ/compare/v0.7.0-beta.4...v0.7.0-beta.5

]]>
- - 14.0 -
diff --git a/Docs/appcast.xml b/Docs/appcast.xml index cdb9ceeb..af264a82 100644 --- a/Docs/appcast.xml +++ b/Docs/appcast.xml @@ -5,6 +5,47 @@ https://github.com/eyelock/TermQ Most recent updates to TermQ en + + Version 0.9.1 + 0.9.1 + 0.9.1 + Tue, 28 Apr 2026 07:08:13 +0000 + Bug Fixes +
    +
  • fix uninstall for local harnesses with no YNH install record (#234)
  • +
  • fix appcast not updating on stable release (#233)
  • +
+
+

Installation

+ +
    +
  1. Download TermQ-0.9.1.dmg
  2. +
  3. Open the DMG and drag TermQ.app to your Applications folder
  4. +
  5. Double-click to launch
  6. +
+

Alternative: Zip Archive

+
    +
  1. Download TermQ-0.9.1.zip
  2. +
  3. Unzip and move TermQ.app to your Applications folder
  4. +
+

CLI Tool

+

The termqcli CLI is bundled inside the app. To install it:

+
    +
  1. Open TermQ.app
  2. +
  3. Go to TermQ → Settings (or press ⌘,)
  4. +
  5. Click Install Command Line Tool
  6. +
+

Or manually:

+
sudo cp /Applications/TermQ.app/Contents/Resources/termqcli /usr/local/bin/termqcli
+

Checksums

+

See checksums.txt for SHA-256 checksums of all release artifacts.

]]>
+ + 14.0 +
Version 0.9.0 0.9.0 From ffe5a84f527ed31d74ce2168fad4c1809c8222c1 Mon Sep 17 00:00:00 2001 From: David Collie Date: Tue, 28 Apr 2026 18:42:01 +0100 Subject: [PATCH 06/29] fix(ui): re-register URL Apple Event handler after SwiftUI scene setup (#239) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Co-authored-by: Claude Sonnet 4.6 --- Sources/TermQ/TermQApp.swift | 8 -------- Sources/TermQ/TermQAppDelegate.swift | 9 +++++++++ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/Sources/TermQ/TermQApp.swift b/Sources/TermQ/TermQApp.swift index e337ac13..aee38b67 100644 --- a/Sources/TermQ/TermQApp.swift +++ b/Sources/TermQ/TermQApp.swift @@ -209,14 +209,6 @@ struct TermQApp: App { UserDefaults.standard.register(defaults: [ "protectedBranches": "main,master,develop" ]) - - // Register URL handler - NSAppleEventManager.shared().setEventHandler( - URLEventHandler.shared, - andSelector: #selector(URLEventHandler.handleURL(_:replyEvent:)), - forEventClass: AEEventClass(kInternetEventClass), - andEventID: AEEventID(kAEGetURL) - ) } /// Check if we should offer to restore from backup on startup diff --git a/Sources/TermQ/TermQAppDelegate.swift b/Sources/TermQ/TermQAppDelegate.swift index 8244fa0c..e321c9d8 100644 --- a/Sources/TermQ/TermQAppDelegate.swift +++ b/Sources/TermQ/TermQAppDelegate.swift @@ -61,6 +61,15 @@ class TermQAppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate { let desc = "\(type(of: win)) visible=\(win.isVisible) frame=\(win.frame)" TermQLogger.window.notice(" window[\(i)]: \(desc)") } + // Override SwiftUI's kAEGetURL handler, which SwiftUI registers during scene setup + // (after our App.init runs). Registering here ensures our handler wins and prevents + // SwiftUI's AppWindowsController from hiding the main window on every URL open. + NSAppleEventManager.shared().setEventHandler( + URLEventHandler.shared, + andSelector: #selector(URLEventHandler.handleURL(_:replyEvent:)), + forEventClass: AEEventClass(kInternetEventClass), + andEventID: AEEventID(kAEGetURL) + ) // Store reference to the main window and set delegate // In SwiftUI apps, the window might not be created yet, so we poll for it setupMainWindowDelegate() From eb6c2fe0f0b58ac1c20a644915ec8b95fc81c8d3 Mon Sep 17 00:00:00 2001 From: David Collie Date: Tue, 28 Apr 2026 22:30:30 +0100 Subject: [PATCH 07/29] fix(terminal): replace -50 Finder dialog with correct file/URL handling (#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 Co-authored-by: Claude Sonnet 4.6 --- .../Resources/ar.lproj/Localizable.strings | 5 + .../Resources/ca.lproj/Localizable.strings | 5 + .../Resources/cs.lproj/Localizable.strings | 5 + .../Resources/da.lproj/Localizable.strings | 5 + .../Resources/de.lproj/Localizable.strings | 5 + .../Resources/el.lproj/Localizable.strings | 5 + .../Resources/en-AU.lproj/Localizable.strings | 5 + .../Resources/en-GB.lproj/Localizable.strings | 5 + .../Resources/en.lproj/Localizable.strings | 5 + .../es-419.lproj/Localizable.strings | 5 + .../Resources/es.lproj/Localizable.strings | 5 + .../Resources/fi.lproj/Localizable.strings | 5 + .../Resources/fr-CA.lproj/Localizable.strings | 5 + .../Resources/fr.lproj/Localizable.strings | 5 + .../Resources/he.lproj/Localizable.strings | 5 + .../Resources/hi.lproj/Localizable.strings | 5 + .../Resources/hr.lproj/Localizable.strings | 5 + .../Resources/hu.lproj/Localizable.strings | 5 + .../Resources/id.lproj/Localizable.strings | 5 + .../Resources/it.lproj/Localizable.strings | 5 + .../Resources/ja.lproj/Localizable.strings | 5 + .../Resources/ko.lproj/Localizable.strings | 5 + .../Resources/ms.lproj/Localizable.strings | 5 + .../Resources/nl.lproj/Localizable.strings | 5 + .../Resources/no.lproj/Localizable.strings | 5 + .../Resources/pl.lproj/Localizable.strings | 5 + .../Resources/pt-PT.lproj/Localizable.strings | 5 + .../Resources/pt.lproj/Localizable.strings | 5 + .../Resources/ro.lproj/Localizable.strings | 5 + .../Resources/ru.lproj/Localizable.strings | 5 + .../Resources/sk.lproj/Localizable.strings | 5 + .../Resources/sl.lproj/Localizable.strings | 5 + .../Resources/sv.lproj/Localizable.strings | 5 + .../Resources/th.lproj/Localizable.strings | 5 + .../Resources/tr.lproj/Localizable.strings | 5 + .../Resources/uk.lproj/Localizable.strings | 5 + .../Resources/vi.lproj/Localizable.strings | 5 + .../Resources/zh-HK.lproj/Localizable.strings | 5 + .../zh-Hans.lproj/Localizable.strings | 5 + .../zh-Hant.lproj/Localizable.strings | 5 + .../TermQ/Services/TerminalLinkResolver.swift | 166 ++++++++++++++++ Sources/TermQ/Utilities/Strings.swift | 12 ++ .../TerminalSessionManager+ControlMode.swift | 7 + .../ViewModels/TerminalSessionManager.swift | 53 +---- Sources/TermQ/Views/TerminalHostView.swift | 79 ++++++++ .../TerminalLinkResolverTests.swift | 183 ++++++++++++++++++ .../TermQTests/TerminalLinkRoutingTests.swift | 140 ++++++++++++++ 47 files changed, 791 insertions(+), 49 deletions(-) create mode 100644 Sources/TermQ/Services/TerminalLinkResolver.swift create mode 100644 Tests/TermQTests/TerminalLinkResolverTests.swift create mode 100644 Tests/TermQTests/TerminalLinkRoutingTests.swift diff --git a/Sources/TermQ/Resources/ar.lproj/Localizable.strings b/Sources/TermQ/Resources/ar.lproj/Localizable.strings index 4db83281..bdffd42d 100644 --- a/Sources/TermQ/Resources/ar.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/ar.lproj/Localizable.strings @@ -417,6 +417,11 @@ "alert.quit.direct.sessions" = "Running Terminal Sessions"; "alert.quit.direct.sessions.message %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits. Terminals using tmux will persist."; "alert.quit.direct.sessions.message.simple %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits."; +"alert.file.open.no.handler.title" = "لم يتم تعيين أي تطبيق لفتح هذا الملف."; +"alert.file.open.no.handler.message %@" = "لا يوجد في macOS تطبيق افتراضي مسجّل لـ \"%@\".\n\nهل تريد الكشف عن الملف في Finder لاختيار تطبيق؟"; +"alert.file.open.failed.title" = "تعذّر فتح الملف."; +"alert.file.open.failed.message %@ %@" = "أفادت macOS بـ: %@\n\nهل تريد الكشف عن \"%@\" في Finder؟"; +"alert.file.open.reveal.in.finder" = "الكشف في Finder"; "common.quit" = "Quit"; // Environment settings (auto-added) diff --git a/Sources/TermQ/Resources/ca.lproj/Localizable.strings b/Sources/TermQ/Resources/ca.lproj/Localizable.strings index 42dfe5a4..d7f7b9c6 100644 --- a/Sources/TermQ/Resources/ca.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/ca.lproj/Localizable.strings @@ -417,6 +417,11 @@ "alert.quit.direct.sessions" = "Running Terminal Sessions"; "alert.quit.direct.sessions.message %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits. Terminals using tmux will persist."; "alert.quit.direct.sessions.message.simple %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits."; +"alert.file.open.no.handler.title" = "Cap aplicació està configurada per obrir aquest fitxer."; +"alert.file.open.no.handler.message %@" = "macOS no té cap aplicació predeterminada registrada per a \"%@\".\n\nMostrar el fitxer al Finder per triar-ne una?"; +"alert.file.open.failed.title" = "No s’ha pogut obrir el fitxer."; +"alert.file.open.failed.message %@ %@" = "macOS ha informat: %@\n\nMostrar \"%@\" al Finder?"; +"alert.file.open.reveal.in.finder" = "Mostra al Finder"; "common.quit" = "Quit"; // Environment settings (auto-added) diff --git a/Sources/TermQ/Resources/cs.lproj/Localizable.strings b/Sources/TermQ/Resources/cs.lproj/Localizable.strings index ecc2aa1a..13431444 100644 --- a/Sources/TermQ/Resources/cs.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/cs.lproj/Localizable.strings @@ -417,6 +417,11 @@ "alert.quit.direct.sessions" = "Running Terminal Sessions"; "alert.quit.direct.sessions.message %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits. Terminals using tmux will persist."; "alert.quit.direct.sessions.message.simple %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits."; +"alert.file.open.no.handler.title" = "Žádná aplikace není nastavena pro otevření tohoto souboru."; +"alert.file.open.no.handler.message %@" = "macOS nemá zaregistrovanou výchozí aplikaci pro \"%@\".\n\nZobrazit soubor ve Finderu a vybrat aplikaci?"; +"alert.file.open.failed.title" = "Soubor nelze otevřít."; +"alert.file.open.failed.message %@ %@" = "macOS hlásí: %@\n\nZobrazit \"%@\" ve Finderu?"; +"alert.file.open.reveal.in.finder" = "Zobrazit ve Finderu"; "common.quit" = "Quit"; // Environment settings (auto-added) diff --git a/Sources/TermQ/Resources/da.lproj/Localizable.strings b/Sources/TermQ/Resources/da.lproj/Localizable.strings index 13d46067..3e82e456 100644 --- a/Sources/TermQ/Resources/da.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/da.lproj/Localizable.strings @@ -417,6 +417,11 @@ "alert.quit.direct.sessions" = "Running Terminal Sessions"; "alert.quit.direct.sessions.message %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits. Terminals using tmux will persist."; "alert.quit.direct.sessions.message.simple %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits."; +"alert.file.open.no.handler.title" = "Ingen applikation er indstillet til at åbne denne fil."; +"alert.file.open.no.handler.message %@" = "macOS har ingen standardapplikation registreret for \"%@\".\n\nVis filen i Finder for at vælge en?"; +"alert.file.open.failed.title" = "Filen kunne ikke åbnes."; +"alert.file.open.failed.message %@ %@" = "macOS rapporterede: %@\n\nVis \"%@\" i Finder?"; +"alert.file.open.reveal.in.finder" = "Vis i Finder"; "common.quit" = "Quit"; // Environment settings (auto-added) diff --git a/Sources/TermQ/Resources/de.lproj/Localizable.strings b/Sources/TermQ/Resources/de.lproj/Localizable.strings index 32b02afc..e2163916 100644 --- a/Sources/TermQ/Resources/de.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/de.lproj/Localizable.strings @@ -417,6 +417,11 @@ "alert.quit.direct.sessions" = "Running Terminal Sessions"; "alert.quit.direct.sessions.message %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits. Terminals using tmux will persist."; "alert.quit.direct.sessions.message.simple %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits."; +"alert.file.open.no.handler.title" = "Keine Anwendung ist zum Öffnen dieser Datei festgelegt."; +"alert.file.open.no.handler.message %@" = "macOS hat keine Standardanwendung für \"%@\" registriert.\n\nDatei im Finder anzeigen, um eine auszuwählen?"; +"alert.file.open.failed.title" = "Die Datei konnte nicht geöffnet werden."; +"alert.file.open.failed.message %@ %@" = "macOS meldet: %@\n\n\"%@\" im Finder anzeigen?"; +"alert.file.open.reveal.in.finder" = "Im Finder anzeigen"; "common.quit" = "Quit"; // Environment settings (auto-added) diff --git a/Sources/TermQ/Resources/el.lproj/Localizable.strings b/Sources/TermQ/Resources/el.lproj/Localizable.strings index efcc7d3d..d0219361 100644 --- a/Sources/TermQ/Resources/el.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/el.lproj/Localizable.strings @@ -417,6 +417,11 @@ "alert.quit.direct.sessions" = "Running Terminal Sessions"; "alert.quit.direct.sessions.message %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits. Terminals using tmux will persist."; "alert.quit.direct.sessions.message.simple %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits."; +"alert.file.open.no.handler.title" = "Δεν έχει οριστεί εφαρμογή για το άνοιγμα αυτού του αρχείου."; +"alert.file.open.no.handler.message %@" = "Το macOS δεν έχει καταχωρισμένη προεπιλεγμένη εφαρμογή για το \"%@\".\n\nΝα εμφανιστεί το αρχείο στο Finder για να επιλέξετε μία;"; +"alert.file.open.failed.title" = "Δεν ήταν δυνατό το άνοιγμα του αρχείου."; +"alert.file.open.failed.message %@ %@" = "Το macOS ανέφερε: %@\n\nΝα εμφανιστεί το \"%@\" στο Finder;"; +"alert.file.open.reveal.in.finder" = "Εμφάνιση στο Finder"; "common.quit" = "Quit"; // Environment settings (auto-added) diff --git a/Sources/TermQ/Resources/en-AU.lproj/Localizable.strings b/Sources/TermQ/Resources/en-AU.lproj/Localizable.strings index dab62510..eff18bef 100644 --- a/Sources/TermQ/Resources/en-AU.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/en-AU.lproj/Localizable.strings @@ -417,6 +417,11 @@ "alert.quit.direct.sessions" = "Running Terminal Sessions"; "alert.quit.direct.sessions.message %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits. Terminals using tmux will persist."; "alert.quit.direct.sessions.message.simple %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits."; +"alert.file.open.no.handler.title" = "No application is set to open this file."; +"alert.file.open.no.handler.message %@" = "macOS doesn't have a default application registered for \"%@\".\n\nReveal the file in Finder so you can choose one?"; +"alert.file.open.failed.title" = "Couldn't open the file."; +"alert.file.open.failed.message %@ %@" = "macOS reported: %@\n\nReveal \"%@\" in Finder?"; +"alert.file.open.reveal.in.finder" = "Reveal in Finder"; "common.quit" = "Quit"; // Environment settings (auto-added) diff --git a/Sources/TermQ/Resources/en-GB.lproj/Localizable.strings b/Sources/TermQ/Resources/en-GB.lproj/Localizable.strings index ffa1f8e6..9ede4aff 100644 --- a/Sources/TermQ/Resources/en-GB.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/en-GB.lproj/Localizable.strings @@ -417,6 +417,11 @@ "alert.quit.direct.sessions" = "Running Terminal Sessions"; "alert.quit.direct.sessions.message %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits. Terminals using tmux will persist."; "alert.quit.direct.sessions.message.simple %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits."; +"alert.file.open.no.handler.title" = "No application is set to open this file."; +"alert.file.open.no.handler.message %@" = "macOS doesn't have a default application registered for \"%@\".\n\nReveal the file in Finder so you can choose one?"; +"alert.file.open.failed.title" = "Couldn't open the file."; +"alert.file.open.failed.message %@ %@" = "macOS reported: %@\n\nReveal \"%@\" in Finder?"; +"alert.file.open.reveal.in.finder" = "Reveal in Finder"; "common.quit" = "Quit"; // Environment settings (auto-added) diff --git a/Sources/TermQ/Resources/en.lproj/Localizable.strings b/Sources/TermQ/Resources/en.lproj/Localizable.strings index 695bbff7..9a75e236 100644 --- a/Sources/TermQ/Resources/en.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/en.lproj/Localizable.strings @@ -458,6 +458,11 @@ "alert.quit.direct.sessions" = "Running Terminal Sessions"; "alert.quit.direct.sessions.message %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits. Terminals using tmux will persist."; "alert.quit.direct.sessions.message.simple %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits."; +"alert.file.open.no.handler.title" = "No application is set to open this file."; +"alert.file.open.no.handler.message %@" = "macOS doesn't have a default application registered for \"%@\".\n\nReveal the file in Finder so you can choose one?"; +"alert.file.open.failed.title" = "Couldn't open the file."; +"alert.file.open.failed.message %@ %@" = "macOS reported: %@\n\nReveal \"%@\" in Finder?"; +"alert.file.open.reveal.in.finder" = "Reveal in Finder"; // MARK: - Backup "backup.title" = "Data Backup"; diff --git a/Sources/TermQ/Resources/es-419.lproj/Localizable.strings b/Sources/TermQ/Resources/es-419.lproj/Localizable.strings index 6592f399..5789461c 100644 --- a/Sources/TermQ/Resources/es-419.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/es-419.lproj/Localizable.strings @@ -417,6 +417,11 @@ "alert.quit.direct.sessions" = "Running Terminal Sessions"; "alert.quit.direct.sessions.message %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits. Terminals using tmux will persist."; "alert.quit.direct.sessions.message.simple %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits."; +"alert.file.open.no.handler.title" = "Ninguna aplicación está configurada para abrir este archivo."; +"alert.file.open.no.handler.message %@" = "macOS no tiene ninguna aplicación predeterminada registrada para \"%@\".\n\n¿Mostrar el archivo en el Finder para elegir una?"; +"alert.file.open.failed.title" = "No se pudo abrir el archivo."; +"alert.file.open.failed.message %@ %@" = "macOS informó: %@\n\n¿Mostrar \"%@\" en el Finder?"; +"alert.file.open.reveal.in.finder" = "Mostrar en el Finder"; "common.quit" = "Quit"; // Environment settings (auto-added) diff --git a/Sources/TermQ/Resources/es.lproj/Localizable.strings b/Sources/TermQ/Resources/es.lproj/Localizable.strings index 01481cb9..e560d494 100644 --- a/Sources/TermQ/Resources/es.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/es.lproj/Localizable.strings @@ -417,6 +417,11 @@ "alert.quit.direct.sessions" = "Running Terminal Sessions"; "alert.quit.direct.sessions.message %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits. Terminals using tmux will persist."; "alert.quit.direct.sessions.message.simple %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits."; +"alert.file.open.no.handler.title" = "Ninguna aplicación está configurada para abrir este archivo."; +"alert.file.open.no.handler.message %@" = "macOS no tiene ninguna aplicación predeterminada registrada para \"%@\".\n\n¿Mostrar el archivo en el Finder para elegir una?"; +"alert.file.open.failed.title" = "No se pudo abrir el archivo."; +"alert.file.open.failed.message %@ %@" = "macOS informó: %@\n\n¿Mostrar \"%@\" en el Finder?"; +"alert.file.open.reveal.in.finder" = "Mostrar en el Finder"; "common.quit" = "Quit"; // Environment settings (auto-added) diff --git a/Sources/TermQ/Resources/fi.lproj/Localizable.strings b/Sources/TermQ/Resources/fi.lproj/Localizable.strings index 2307a6e9..9ea6b9df 100644 --- a/Sources/TermQ/Resources/fi.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/fi.lproj/Localizable.strings @@ -417,6 +417,11 @@ "alert.quit.direct.sessions" = "Running Terminal Sessions"; "alert.quit.direct.sessions.message %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits. Terminals using tmux will persist."; "alert.quit.direct.sessions.message.simple %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits."; +"alert.file.open.no.handler.title" = "Yhtään sovellusta ei ole asetettu avaamaan tätä tiedostoa."; +"alert.file.open.no.handler.message %@" = "macOS:lla ei ole oletussovellusta rekisteröitynä tiedostolle \"%@\".\n\nNäytetäänkö tiedosto Finderissa, jotta voit valita sovelluksen?"; +"alert.file.open.failed.title" = "Tiedostoa ei voitu avata."; +"alert.file.open.failed.message %@ %@" = "macOS ilmoitti: %@\n\nNäytetäänkö \"%@\" Finderissa?"; +"alert.file.open.reveal.in.finder" = "Näytä Finderissa"; "common.quit" = "Quit"; // Environment settings (auto-added) diff --git a/Sources/TermQ/Resources/fr-CA.lproj/Localizable.strings b/Sources/TermQ/Resources/fr-CA.lproj/Localizable.strings index 62c14e67..01c9c8ee 100644 --- a/Sources/TermQ/Resources/fr-CA.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/fr-CA.lproj/Localizable.strings @@ -417,6 +417,11 @@ "alert.quit.direct.sessions" = "Running Terminal Sessions"; "alert.quit.direct.sessions.message %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits. Terminals using tmux will persist."; "alert.quit.direct.sessions.message.simple %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits."; +"alert.file.open.no.handler.title" = "Aucune application n’est configurée pour ouvrir ce fichier."; +"alert.file.open.no.handler.message %@" = "macOS n’a pas d’application par défaut enregistrée pour \"%@\".\n\nAfficher le fichier dans le Finder pour en choisir une ?"; +"alert.file.open.failed.title" = "Impossible d’ouvrir le fichier."; +"alert.file.open.failed.message %@ %@" = "macOS a signalé : %@\n\nAfficher \"%@\" dans le Finder ?"; +"alert.file.open.reveal.in.finder" = "Afficher dans le Finder"; "common.quit" = "Quit"; // Environment settings (auto-added) diff --git a/Sources/TermQ/Resources/fr.lproj/Localizable.strings b/Sources/TermQ/Resources/fr.lproj/Localizable.strings index 344af5de..d2af1937 100644 --- a/Sources/TermQ/Resources/fr.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/fr.lproj/Localizable.strings @@ -417,6 +417,11 @@ "alert.quit.direct.sessions" = "Running Terminal Sessions"; "alert.quit.direct.sessions.message %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits. Terminals using tmux will persist."; "alert.quit.direct.sessions.message.simple %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits."; +"alert.file.open.no.handler.title" = "Aucune application n’est configurée pour ouvrir ce fichier."; +"alert.file.open.no.handler.message %@" = "macOS n’a pas d’application par défaut enregistrée pour \"%@\".\n\nAfficher le fichier dans le Finder pour en choisir une ?"; +"alert.file.open.failed.title" = "Impossible d’ouvrir le fichier."; +"alert.file.open.failed.message %@ %@" = "macOS a signalé : %@\n\nAfficher \"%@\" dans le Finder ?"; +"alert.file.open.reveal.in.finder" = "Afficher dans le Finder"; "common.quit" = "Quit"; // Environment settings (auto-added) diff --git a/Sources/TermQ/Resources/he.lproj/Localizable.strings b/Sources/TermQ/Resources/he.lproj/Localizable.strings index 2a078450..2d597989 100644 --- a/Sources/TermQ/Resources/he.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/he.lproj/Localizable.strings @@ -417,6 +417,11 @@ "alert.quit.direct.sessions" = "Running Terminal Sessions"; "alert.quit.direct.sessions.message %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits. Terminals using tmux will persist."; "alert.quit.direct.sessions.message.simple %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits."; +"alert.file.open.no.handler.title" = "לא הוגדרה אפליקציה לפתיחת קובץ זה."; +"alert.file.open.no.handler.message %@" = "ל-macOS אין אפליקציית ברירת מחדל רשומה עבור \"%@\".\n\nלחשוף את הקובץ ב-Finder כדי לבחור אחת?"; +"alert.file.open.failed.title" = "לא ניתן לפתוח את הקובץ."; +"alert.file.open.failed.message %@ %@" = "macOS דיווח: %@\n\nלחשוף את \"%@\" ב-Finder?"; +"alert.file.open.reveal.in.finder" = "חשוף ב-Finder"; "common.quit" = "Quit"; // Environment settings (auto-added) diff --git a/Sources/TermQ/Resources/hi.lproj/Localizable.strings b/Sources/TermQ/Resources/hi.lproj/Localizable.strings index 68e24982..c45a7f90 100644 --- a/Sources/TermQ/Resources/hi.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/hi.lproj/Localizable.strings @@ -417,6 +417,11 @@ "alert.quit.direct.sessions" = "Running Terminal Sessions"; "alert.quit.direct.sessions.message %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits. Terminals using tmux will persist."; "alert.quit.direct.sessions.message.simple %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits."; +"alert.file.open.no.handler.title" = "इस फ़ाइल को खोलने के लिए कोई ऐप्लिकेशन सेट नहीं है।"; +"alert.file.open.no.handler.message %@" = "macOS में \"%@\" के लिए कोई डिफ़ॉल्ट ऐप्लिकेशन पंजीकृत नहीं है।\n\nFinder में फ़ाइल दिखाएं ताकि आप एक चुन सकें?"; +"alert.file.open.failed.title" = "फ़ाइल नहीं खोली जा सकी।"; +"alert.file.open.failed.message %@ %@" = "macOS ने बताया: %@\n\nFinder में \"%@\" दिखाएं?"; +"alert.file.open.reveal.in.finder" = "Finder में दिखाएं"; "common.quit" = "Quit"; // Environment settings (auto-added) diff --git a/Sources/TermQ/Resources/hr.lproj/Localizable.strings b/Sources/TermQ/Resources/hr.lproj/Localizable.strings index 9f791af5..59f9cb9c 100644 --- a/Sources/TermQ/Resources/hr.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/hr.lproj/Localizable.strings @@ -417,6 +417,11 @@ "alert.quit.direct.sessions" = "Running Terminal Sessions"; "alert.quit.direct.sessions.message %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits. Terminals using tmux will persist."; "alert.quit.direct.sessions.message.simple %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits."; +"alert.file.open.no.handler.title" = "Nijedna aplikacija nije postavljena za otvaranje ove datoteke."; +"alert.file.open.no.handler.message %@" = "macOS nema registriranu zadanu aplikaciju za \"%@\".\n\nPrikazati datoteku u Finderu radi odabira aplikacije?"; +"alert.file.open.failed.title" = "Datoteka se nije mogla otvoriti."; +"alert.file.open.failed.message %@ %@" = "macOS je prijavio: %@\n\nPrikazati \"%@\" u Finderu?"; +"alert.file.open.reveal.in.finder" = "Prikaži u Finderu"; "common.quit" = "Quit"; // Environment settings (auto-added) diff --git a/Sources/TermQ/Resources/hu.lproj/Localizable.strings b/Sources/TermQ/Resources/hu.lproj/Localizable.strings index ac4bb4c2..94da9ba5 100644 --- a/Sources/TermQ/Resources/hu.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/hu.lproj/Localizable.strings @@ -417,6 +417,11 @@ "alert.quit.direct.sessions" = "Running Terminal Sessions"; "alert.quit.direct.sessions.message %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits. Terminals using tmux will persist."; "alert.quit.direct.sessions.message.simple %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits."; +"alert.file.open.no.handler.title" = "Nincs beállítva alkalmazás a fájl megnyitásához."; +"alert.file.open.no.handler.message %@" = "A macOS-en nincs alapértelmezett alkalmazás regisztrálva a \"%@\" fájlhoz.\n\nMegmutatja a fájlt a Finderben, hogy kiválaszthasson egyet?"; +"alert.file.open.failed.title" = "A fájl nem nyitható meg."; +"alert.file.open.failed.message %@ %@" = "A macOS jelentette: %@\n\nMegmutatja a \"%@\" fájlt a Finderben?"; +"alert.file.open.reveal.in.finder" = "Megjelenítés a Finderben"; "common.quit" = "Quit"; // Environment settings (auto-added) diff --git a/Sources/TermQ/Resources/id.lproj/Localizable.strings b/Sources/TermQ/Resources/id.lproj/Localizable.strings index ae47eb60..19186ca2 100644 --- a/Sources/TermQ/Resources/id.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/id.lproj/Localizable.strings @@ -416,6 +416,11 @@ "alert.quit.direct.sessions" = "Running Terminal Sessions"; "alert.quit.direct.sessions.message %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits. Terminals using tmux will persist."; "alert.quit.direct.sessions.message.simple %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits."; +"alert.file.open.no.handler.title" = "Tidak ada aplikasi yang disetel untuk membuka file ini."; +"alert.file.open.no.handler.message %@" = "macOS tidak memiliki aplikasi default yang terdaftar untuk \"%@\".\n\nTampilkan file di Finder untuk memilih salah satu?"; +"alert.file.open.failed.title" = "File tidak dapat dibuka."; +"alert.file.open.failed.message %@ %@" = "macOS melaporkan: %@\n\nTampilkan \"%@\" di Finder?"; +"alert.file.open.reveal.in.finder" = "Tampilkan di Finder"; "common.quit" = "Quit"; // Environment settings (auto-added) diff --git a/Sources/TermQ/Resources/it.lproj/Localizable.strings b/Sources/TermQ/Resources/it.lproj/Localizable.strings index db9bca84..4229977f 100644 --- a/Sources/TermQ/Resources/it.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/it.lproj/Localizable.strings @@ -416,6 +416,11 @@ "alert.quit.direct.sessions" = "Running Terminal Sessions"; "alert.quit.direct.sessions.message %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits. Terminals using tmux will persist."; "alert.quit.direct.sessions.message.simple %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits."; +"alert.file.open.no.handler.title" = "Nessuna applicazione è impostata per aprire questo file."; +"alert.file.open.no.handler.message %@" = "macOS non ha un’applicazione predefinita registrata per \"%@\".\n\nMostrare il file nel Finder per sceglierne una?"; +"alert.file.open.failed.title" = "Impossibile aprire il file."; +"alert.file.open.failed.message %@ %@" = "macOS ha segnalato: %@\n\nMostrare \"%@\" nel Finder?"; +"alert.file.open.reveal.in.finder" = "Mostra nel Finder"; "common.quit" = "Quit"; // Environment settings (auto-added) diff --git a/Sources/TermQ/Resources/ja.lproj/Localizable.strings b/Sources/TermQ/Resources/ja.lproj/Localizable.strings index 0d6e56ff..27771492 100644 --- a/Sources/TermQ/Resources/ja.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/ja.lproj/Localizable.strings @@ -416,6 +416,11 @@ "alert.quit.direct.sessions" = "Running Terminal Sessions"; "alert.quit.direct.sessions.message %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits. Terminals using tmux will persist."; "alert.quit.direct.sessions.message.simple %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits."; +"alert.file.open.no.handler.title" = "このファイルを開くアプリケーションが設定されていません。"; +"alert.file.open.no.handler.message %@" = "macOS には \"%@\" に対してデフォルトのアプリケーションが登録されていません。\n\nFinder でファイルを表示してアプリケーションを選択しますか?"; +"alert.file.open.failed.title" = "ファイルを開けませんでした。"; +"alert.file.open.failed.message %@ %@" = "macOS が報告しました: %@\n\nFinder で \"%@\" を表示しますか?"; +"alert.file.open.reveal.in.finder" = "Finder で表示"; "common.quit" = "Quit"; // Environment settings (auto-added) diff --git a/Sources/TermQ/Resources/ko.lproj/Localizable.strings b/Sources/TermQ/Resources/ko.lproj/Localizable.strings index f4f69c85..f8bbcbf6 100644 --- a/Sources/TermQ/Resources/ko.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/ko.lproj/Localizable.strings @@ -417,6 +417,11 @@ "alert.quit.direct.sessions" = "Running Terminal Sessions"; "alert.quit.direct.sessions.message %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits. Terminals using tmux will persist."; "alert.quit.direct.sessions.message.simple %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits."; +"alert.file.open.no.handler.title" = "이 파일을 열도록 설정된 앱이 없습니다."; +"alert.file.open.no.handler.message %@" = "macOS에 \"%@\"에 등록된 기본 앱이 없습니다.\n\n앱을 선택할 수 있도록 Finder에서 파일을 표시할까요?"; +"alert.file.open.failed.title" = "파일을 열 수 없습니다."; +"alert.file.open.failed.message %@ %@" = "macOS 보고: %@\n\nFinder에서 \"%@\"을(를) 표시할까요?"; +"alert.file.open.reveal.in.finder" = "Finder에서 표시"; "common.quit" = "Quit"; // Environment settings (auto-added) diff --git a/Sources/TermQ/Resources/ms.lproj/Localizable.strings b/Sources/TermQ/Resources/ms.lproj/Localizable.strings index c29852d3..89980b02 100644 --- a/Sources/TermQ/Resources/ms.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/ms.lproj/Localizable.strings @@ -417,6 +417,11 @@ "alert.quit.direct.sessions" = "Running Terminal Sessions"; "alert.quit.direct.sessions.message %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits. Terminals using tmux will persist."; "alert.quit.direct.sessions.message.simple %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits."; +"alert.file.open.no.handler.title" = "Tiada aplikasi ditetapkan untuk membuka fail ini."; +"alert.file.open.no.handler.message %@" = "macOS tidak mempunyai aplikasi lalai yang berdaftar untuk \"%@\".\n\nDedahkan fail dalam Finder untuk memilih satu?"; +"alert.file.open.failed.title" = "Fail tidak dapat dibuka."; +"alert.file.open.failed.message %@ %@" = "macOS melaporkan: %@\n\nDedahkan \"%@\" dalam Finder?"; +"alert.file.open.reveal.in.finder" = "Dedahkan dalam Finder"; "common.quit" = "Quit"; // Environment settings (auto-added) diff --git a/Sources/TermQ/Resources/nl.lproj/Localizable.strings b/Sources/TermQ/Resources/nl.lproj/Localizable.strings index a0adaddd..51a92df8 100644 --- a/Sources/TermQ/Resources/nl.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/nl.lproj/Localizable.strings @@ -417,6 +417,11 @@ "alert.quit.direct.sessions" = "Running Terminal Sessions"; "alert.quit.direct.sessions.message %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits. Terminals using tmux will persist."; "alert.quit.direct.sessions.message.simple %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits."; +"alert.file.open.no.handler.title" = "Er is geen applicatie ingesteld om dit bestand te openen."; +"alert.file.open.no.handler.message %@" = "macOS heeft geen standaard applicatie geregistreerd voor \"%@\".\n\nBestand tonen in Finder om er een te kiezen?"; +"alert.file.open.failed.title" = "Het bestand kon niet worden geopend."; +"alert.file.open.failed.message %@ %@" = "macOS meldde: %@\n\n\"%@\" tonen in Finder?"; +"alert.file.open.reveal.in.finder" = "Toon in Finder"; "common.quit" = "Quit"; // Environment settings (auto-added) diff --git a/Sources/TermQ/Resources/no.lproj/Localizable.strings b/Sources/TermQ/Resources/no.lproj/Localizable.strings index fab19078..a6d0bcff 100644 --- a/Sources/TermQ/Resources/no.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/no.lproj/Localizable.strings @@ -417,6 +417,11 @@ "alert.quit.direct.sessions" = "Running Terminal Sessions"; "alert.quit.direct.sessions.message %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits. Terminals using tmux will persist."; "alert.quit.direct.sessions.message.simple %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits."; +"alert.file.open.no.handler.title" = "Ingen applikasjon er satt til å åpne denne filen."; +"alert.file.open.no.handler.message %@" = "macOS har ingen standardapplikasjon registrert for \"%@\".\n\nVise filen i Finder for å velge en?"; +"alert.file.open.failed.title" = "Filen kunne ikke åpnes."; +"alert.file.open.failed.message %@ %@" = "macOS rapporterte: %@\n\nVise \"%@\" i Finder?"; +"alert.file.open.reveal.in.finder" = "Vis i Finder"; "common.quit" = "Quit"; // Environment settings (auto-added) diff --git a/Sources/TermQ/Resources/pl.lproj/Localizable.strings b/Sources/TermQ/Resources/pl.lproj/Localizable.strings index ded51ce2..9f9cee83 100644 --- a/Sources/TermQ/Resources/pl.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/pl.lproj/Localizable.strings @@ -417,6 +417,11 @@ "alert.quit.direct.sessions" = "Running Terminal Sessions"; "alert.quit.direct.sessions.message %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits. Terminals using tmux will persist."; "alert.quit.direct.sessions.message.simple %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits."; +"alert.file.open.no.handler.title" = "Żadna aplikacja nie jest ustawiona do otwierania tego pliku."; +"alert.file.open.no.handler.message %@" = "macOS nie ma zarejestrowanej domyślnej aplikacji dla \"%@\".\n\nUjawnić plik w Finderze, aby wybrać aplikację?"; +"alert.file.open.failed.title" = "Nie można otworzyć pliku."; +"alert.file.open.failed.message %@ %@" = "macOS zgłosił: %@\n\nUjawnić \"%@\" w Finderze?"; +"alert.file.open.reveal.in.finder" = "Pokaż w Finderze"; "common.quit" = "Quit"; // Environment settings (auto-added) diff --git a/Sources/TermQ/Resources/pt-PT.lproj/Localizable.strings b/Sources/TermQ/Resources/pt-PT.lproj/Localizable.strings index 40321717..3cfc6892 100644 --- a/Sources/TermQ/Resources/pt-PT.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/pt-PT.lproj/Localizable.strings @@ -417,6 +417,11 @@ "alert.quit.direct.sessions" = "Running Terminal Sessions"; "alert.quit.direct.sessions.message %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits. Terminals using tmux will persist."; "alert.quit.direct.sessions.message.simple %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits."; +"alert.file.open.no.handler.title" = "Nenhuma aplicação está definida para abrir este ficheiro."; +"alert.file.open.no.handler.message %@" = "O macOS não tem uma aplicação predefinida registada para \"%@\".\n\nMostrar o ficheiro no Finder para escolher uma?"; +"alert.file.open.failed.title" = "Não foi possível abrir o ficheiro."; +"alert.file.open.failed.message %@ %@" = "O macOS comunicou: %@\n\nMostrar \"%@\" no Finder?"; +"alert.file.open.reveal.in.finder" = "Mostrar no Finder"; "common.quit" = "Quit"; // Environment settings (auto-added) diff --git a/Sources/TermQ/Resources/pt.lproj/Localizable.strings b/Sources/TermQ/Resources/pt.lproj/Localizable.strings index 25bc22a5..6936efc5 100644 --- a/Sources/TermQ/Resources/pt.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/pt.lproj/Localizable.strings @@ -417,6 +417,11 @@ "alert.quit.direct.sessions" = "Running Terminal Sessions"; "alert.quit.direct.sessions.message %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits. Terminals using tmux will persist."; "alert.quit.direct.sessions.message.simple %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits."; +"alert.file.open.no.handler.title" = "Nenhuma aplicação está definida para abrir este ficheiro."; +"alert.file.open.no.handler.message %@" = "O macOS não tem uma aplicação padrão registada para \"%@\".\n\nMostrar o ficheiro no Finder para escolher uma?"; +"alert.file.open.failed.title" = "Não foi possível abrir o ficheiro."; +"alert.file.open.failed.message %@ %@" = "O macOS comunicou: %@\n\nMostrar \"%@\" no Finder?"; +"alert.file.open.reveal.in.finder" = "Mostrar no Finder"; "common.quit" = "Quit"; // Environment settings (auto-added) diff --git a/Sources/TermQ/Resources/ro.lproj/Localizable.strings b/Sources/TermQ/Resources/ro.lproj/Localizable.strings index 44a30f10..5f6b16a2 100644 --- a/Sources/TermQ/Resources/ro.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/ro.lproj/Localizable.strings @@ -416,6 +416,11 @@ "alert.quit.direct.sessions" = "Running Terminal Sessions"; "alert.quit.direct.sessions.message %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits. Terminals using tmux will persist."; "alert.quit.direct.sessions.message.simple %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits."; +"alert.file.open.no.handler.title" = "Nicio aplicație nu este setată să deschidă acest fișier."; +"alert.file.open.no.handler.message %@" = "macOS nu are o aplicație implicită înregistrată pentru \"%@\".\n\nAfișați fișierul în Finder pentru a alege una?"; +"alert.file.open.failed.title" = "Fișierul nu a putut fi deschis."; +"alert.file.open.failed.message %@ %@" = "macOS a raportat: %@\n\nAfișați \"%@\" în Finder?"; +"alert.file.open.reveal.in.finder" = "Afișați în Finder"; "common.quit" = "Quit"; // Environment settings (auto-added) diff --git a/Sources/TermQ/Resources/ru.lproj/Localizable.strings b/Sources/TermQ/Resources/ru.lproj/Localizable.strings index 98703257..4cb74b50 100644 --- a/Sources/TermQ/Resources/ru.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/ru.lproj/Localizable.strings @@ -416,6 +416,11 @@ "alert.quit.direct.sessions" = "Running Terminal Sessions"; "alert.quit.direct.sessions.message %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits. Terminals using tmux will persist."; "alert.quit.direct.sessions.message.simple %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits."; +"alert.file.open.no.handler.title" = "Ни одно приложение не настроено для открытия этого файла."; +"alert.file.open.no.handler.message %@" = "В macOS нет зарегистрированного приложения по умолчанию для \"%@\".\n\nПоказать файл в Finder, чтобы выбрать приложение?"; +"alert.file.open.failed.title" = "Не удалось открыть файл."; +"alert.file.open.failed.message %@ %@" = "macOS сообщил: %@\n\nПоказать \"%@\" в Finder?"; +"alert.file.open.reveal.in.finder" = "Показать в Finder"; "common.quit" = "Quit"; // Environment settings (auto-added) diff --git a/Sources/TermQ/Resources/sk.lproj/Localizable.strings b/Sources/TermQ/Resources/sk.lproj/Localizable.strings index 322579f1..8c8be230 100644 --- a/Sources/TermQ/Resources/sk.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/sk.lproj/Localizable.strings @@ -416,6 +416,11 @@ "alert.quit.direct.sessions" = "Running Terminal Sessions"; "alert.quit.direct.sessions.message %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits. Terminals using tmux will persist."; "alert.quit.direct.sessions.message.simple %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits."; +"alert.file.open.no.handler.title" = "Na otvorenie tohto súboru nie je nastavená žiadna aplikácia."; +"alert.file.open.no.handler.message %@" = "macOS nemá zaregistrovanú predvolenú aplikáciu pre \"%@\".\n\nZobraziť súbor vo Finderi a vybrať aplikáciu?"; +"alert.file.open.failed.title" = "Súbor sa nepodarilo otvoriť."; +"alert.file.open.failed.message %@ %@" = "macOS hlásil: %@\n\nZobraziť \"%@\" vo Finderi?"; +"alert.file.open.reveal.in.finder" = "Zobraziť vo Finderi"; "common.quit" = "Quit"; // Environment settings (auto-added) diff --git a/Sources/TermQ/Resources/sl.lproj/Localizable.strings b/Sources/TermQ/Resources/sl.lproj/Localizable.strings index e56c6ea9..15ac0f52 100644 --- a/Sources/TermQ/Resources/sl.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/sl.lproj/Localizable.strings @@ -418,6 +418,11 @@ "alert.quit.direct.sessions" = "Running Terminal Sessions"; "alert.quit.direct.sessions.message %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits. Terminals using tmux will persist."; "alert.quit.direct.sessions.message.simple %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits."; +"alert.file.open.no.handler.title" = "Za odpiranje te datoteke ni nastavljena nobena aplikacija."; +"alert.file.open.no.handler.message %@" = "macOS nima registrirane privzete aplikacije za \"%@\".\n\nPrikaži datoteko v Finderju in izberi aplikacijo?"; +"alert.file.open.failed.title" = "Datoteke ni bilo mogoče odpreti."; +"alert.file.open.failed.message %@ %@" = "macOS je poročal: %@\n\nPrikaži \"%@\" v Finderju?"; +"alert.file.open.reveal.in.finder" = "Prikaži v Finderju"; "common.quit" = "Quit"; // Environment settings (auto-added) diff --git a/Sources/TermQ/Resources/sv.lproj/Localizable.strings b/Sources/TermQ/Resources/sv.lproj/Localizable.strings index 340c81f0..f8d2fe4b 100644 --- a/Sources/TermQ/Resources/sv.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/sv.lproj/Localizable.strings @@ -418,6 +418,11 @@ "alert.quit.direct.sessions" = "Running Terminal Sessions"; "alert.quit.direct.sessions.message %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits. Terminals using tmux will persist."; "alert.quit.direct.sessions.message.simple %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits."; +"alert.file.open.no.handler.title" = "Inget program är inställt för att öppna den här filen."; +"alert.file.open.no.handler.message %@" = "macOS har inget standardprogram registrerat för \"%@\".\n\nVisa filen i Finder för att välja ett?"; +"alert.file.open.failed.title" = "Det gick inte att öppna filen."; +"alert.file.open.failed.message %@ %@" = "macOS rapporterade: %@\n\nVisa \"%@\" i Finder?"; +"alert.file.open.reveal.in.finder" = "Visa i Finder"; "common.quit" = "Quit"; // Environment settings (auto-added) diff --git a/Sources/TermQ/Resources/th.lproj/Localizable.strings b/Sources/TermQ/Resources/th.lproj/Localizable.strings index 5dbf7aa7..3c3c574c 100644 --- a/Sources/TermQ/Resources/th.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/th.lproj/Localizable.strings @@ -418,6 +418,11 @@ "alert.quit.direct.sessions" = "Running Terminal Sessions"; "alert.quit.direct.sessions.message %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits. Terminals using tmux will persist."; "alert.quit.direct.sessions.message.simple %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits."; +"alert.file.open.no.handler.title" = "ไม่มีแอปพลิเคชันที่ตั้งค่าให้เปิดไฟล์นี้"; +"alert.file.open.no.handler.message %@" = "macOS ไม่มีแอปพลิเคชันเริ่มต้นที่ลงทะเบียนสำหรับ \"%@\"\n\nเปิดเผยไฟล์ใน Finder เพื่อเลือกแอปพลิเคชัน?"; +"alert.file.open.failed.title" = "ไม่สามารถเปิดไฟล์ได้"; +"alert.file.open.failed.message %@ %@" = "macOS รายงาน: %@\n\nเปิดเผย \"%@\" ใน Finder?"; +"alert.file.open.reveal.in.finder" = "เปิดเผยใน Finder"; "common.quit" = "Quit"; // Environment settings (auto-added) diff --git a/Sources/TermQ/Resources/tr.lproj/Localizable.strings b/Sources/TermQ/Resources/tr.lproj/Localizable.strings index 33b682c6..35f677eb 100644 --- a/Sources/TermQ/Resources/tr.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/tr.lproj/Localizable.strings @@ -418,6 +418,11 @@ "alert.quit.direct.sessions" = "Running Terminal Sessions"; "alert.quit.direct.sessions.message %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits. Terminals using tmux will persist."; "alert.quit.direct.sessions.message.simple %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits."; +"alert.file.open.no.handler.title" = "Bu dosyayı açmak için hiçbir uygulama ayarlanmamış."; +"alert.file.open.no.handler.message %@" = "macOS, \"%@\" için kayıtlı bir varsayılan uygulamaya sahip değil.\n\nBir uygulama seçebilmek için dosyayı Finder’da göster?"; +"alert.file.open.failed.title" = "Dosya açılamadı."; +"alert.file.open.failed.message %@ %@" = "macOS bildirdi: %@\n\n\"%@\" öğesini Finder’da göster?"; +"alert.file.open.reveal.in.finder" = "Finder’da Göster"; "common.quit" = "Quit"; // Environment settings (auto-added) diff --git a/Sources/TermQ/Resources/uk.lproj/Localizable.strings b/Sources/TermQ/Resources/uk.lproj/Localizable.strings index 6a9b8a1d..40197aa8 100644 --- a/Sources/TermQ/Resources/uk.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/uk.lproj/Localizable.strings @@ -418,6 +418,11 @@ "alert.quit.direct.sessions" = "Running Terminal Sessions"; "alert.quit.direct.sessions.message %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits. Terminals using tmux will persist."; "alert.quit.direct.sessions.message.simple %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits."; +"alert.file.open.no.handler.title" = "Жодна програма не встановлена для відкриття цього файлу."; +"alert.file.open.no.handler.message %@" = "macOS не має зареєстрованої програми за замовчуванням для \"%@\".\n\nПоказати файл у Finder, щоб вибрати програму?"; +"alert.file.open.failed.title" = "Не вдалося відкрити файл."; +"alert.file.open.failed.message %@ %@" = "macOS повідомив: %@\n\nПоказати \"%@\" у Finder?"; +"alert.file.open.reveal.in.finder" = "Показати у Finder"; "common.quit" = "Quit"; // Environment settings (auto-added) diff --git a/Sources/TermQ/Resources/vi.lproj/Localizable.strings b/Sources/TermQ/Resources/vi.lproj/Localizable.strings index 83d3ba4c..632c9bed 100644 --- a/Sources/TermQ/Resources/vi.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/vi.lproj/Localizable.strings @@ -418,6 +418,11 @@ "alert.quit.direct.sessions" = "Running Terminal Sessions"; "alert.quit.direct.sessions.message %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits. Terminals using tmux will persist."; "alert.quit.direct.sessions.message.simple %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits."; +"alert.file.open.no.handler.title" = "Không có ứng dụng nào được thiết lập để mở tệp này."; +"alert.file.open.no.handler.message %@" = "macOS không có ứng dụng mặc định nào được đăng ký cho \"%@\".\n\nHiển thị tệp trong Finder để chọn một ứng dụng?"; +"alert.file.open.failed.title" = "Không thể mở tệp."; +"alert.file.open.failed.message %@ %@" = "macOS báo cáo: %@\n\nHiển thị \"%@\" trong Finder?"; +"alert.file.open.reveal.in.finder" = "Hiển thị trong Finder"; "common.quit" = "Quit"; // Environment settings (auto-added) diff --git a/Sources/TermQ/Resources/zh-HK.lproj/Localizable.strings b/Sources/TermQ/Resources/zh-HK.lproj/Localizable.strings index a07ccbda..5cb2e59a 100644 --- a/Sources/TermQ/Resources/zh-HK.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/zh-HK.lproj/Localizable.strings @@ -416,6 +416,11 @@ "alert.quit.direct.sessions" = "Running Terminal Sessions"; "alert.quit.direct.sessions.message %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits. Terminals using tmux will persist."; "alert.quit.direct.sessions.message.simple %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits."; +"alert.file.open.no.handler.title" = "沒有設定用於開啟此檔案的應用程式。"; +"alert.file.open.no.handler.message %@" = "macOS 沒有為「%@」登記預設應用程式。\n\n在 Finder 中顯示檔案以選擇應用程式?"; +"alert.file.open.failed.title" = "無法開啟該檔案。"; +"alert.file.open.failed.message %@ %@" = "macOS 報告:%@\n\n在 Finder 中顯示「%@」?"; +"alert.file.open.reveal.in.finder" = "在 Finder 中顯示"; "common.quit" = "Quit"; // Environment settings (auto-added) diff --git a/Sources/TermQ/Resources/zh-Hans.lproj/Localizable.strings b/Sources/TermQ/Resources/zh-Hans.lproj/Localizable.strings index 991c0718..2a5c17e2 100644 --- a/Sources/TermQ/Resources/zh-Hans.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/zh-Hans.lproj/Localizable.strings @@ -416,6 +416,11 @@ "alert.quit.direct.sessions" = "Running Terminal Sessions"; "alert.quit.direct.sessions.message %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits. Terminals using tmux will persist."; "alert.quit.direct.sessions.message.simple %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits."; +"alert.file.open.no.handler.title" = "没有设置用于打开此文件的应用程序。"; +"alert.file.open.no.handler.message %@" = "macOS 没有为\"%@\"注册默认应用程序。\n\n是否在 Finder 中显示文件以选择一个应用程序?"; +"alert.file.open.failed.title" = "无法打开该文件。"; +"alert.file.open.failed.message %@ %@" = "macOS 报告:%@\n\n是否在 Finder 中显示\"%@\"?"; +"alert.file.open.reveal.in.finder" = "在 Finder 中显示"; "common.quit" = "Quit"; // Environment settings (auto-added) diff --git a/Sources/TermQ/Resources/zh-Hant.lproj/Localizable.strings b/Sources/TermQ/Resources/zh-Hant.lproj/Localizable.strings index 7b5617a7..289eece2 100644 --- a/Sources/TermQ/Resources/zh-Hant.lproj/Localizable.strings +++ b/Sources/TermQ/Resources/zh-Hant.lproj/Localizable.strings @@ -416,6 +416,11 @@ "alert.quit.direct.sessions" = "Running Terminal Sessions"; "alert.quit.direct.sessions.message %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits. Terminals using tmux will persist."; "alert.quit.direct.sessions.message.simple %lld" = "You have %lld running terminal session(s) that will be terminated when TermQ quits."; +"alert.file.open.no.handler.title" = "沒有設定用於開啟此檔案的應用程式。"; +"alert.file.open.no.handler.message %@" = "macOS 沒有為「%@」註冊預設應用程式。\n\n在 Finder 中顯示檔案以選擇一個應用程式?"; +"alert.file.open.failed.title" = "無法開啟該檔案。"; +"alert.file.open.failed.message %@ %@" = "macOS 回報:%@\n\n在 Finder 中顯示「%@」?"; +"alert.file.open.reveal.in.finder" = "在 Finder 中顯示"; "common.quit" = "Quit"; // Environment settings (auto-added) diff --git a/Sources/TermQ/Services/TerminalLinkResolver.swift b/Sources/TermQ/Services/TerminalLinkResolver.swift new file mode 100644 index 00000000..b2321eab --- /dev/null +++ b/Sources/TermQ/Services/TerminalLinkResolver.swift @@ -0,0 +1,166 @@ +import AppKit +import Foundation +import SwiftTerm + +/// Pure resolution of a terminal-detected link string into an actionable +/// outcome. Has no side effects and no UI — call sites in +/// `TerminalSessionManager` apply the result via `NSWorkspace`/`AlertBuilder`. +enum TerminalLinkAction: Equatable { + /// Open a remote URL (http/https) with the default browser. + case openURL(URL) + /// Open a local file with the registered default application. + case openFile(URL) + /// File doesn't exist — reveal it in Finder rooted at the nearest + /// existing parent directory. + case revealInFinder(file: URL, root: URL) + /// Couldn't resolve to a path; hand back to the caller as a plain string + /// for default-handler behaviour. + case fallbackString(String) + /// Nothing to do (empty/whitespace-only payload). + case noop +} + +enum TerminalLinkResolver { + + /// Strip whitespace and trailing punctuation that SwiftTerm's implicit + /// link regex can capture (`)`, `]`, `}`, `:`, `.`, line-wrap whitespace, + /// etc.) so the resolved path matches what's actually on disk. + static func sanitize(_ link: String) -> String { + let trimSet = CharacterSet.whitespacesAndNewlines + .union(CharacterSet(charactersIn: ".,;:!?)]}>\"")) + var result = link.trimmingCharacters(in: .whitespacesAndNewlines) + while let last = result.unicodeScalars.last, trimSet.contains(last) { + result.removeLast() + } + return result + } + + /// Resolve a raw link payload against the optional current working + /// directory and a file-existence predicate. + static func resolve( + link: String, + cwd: String?, + fileExists: (String) -> Bool + ) -> TerminalLinkAction { + let trimmed = sanitize(link) + guard !trimmed.isEmpty else { return .noop } + + if trimmed.hasPrefix("http://") || trimmed.hasPrefix("https://") { + if let url = URL(string: trimmed) { return .openURL(url) } + return .fallbackString(trimmed) + } + + let resolvedPath: String + if trimmed.hasPrefix("/") { + resolvedPath = trimmed + } else if let base = cwd { + resolvedPath = (base as NSString).appendingPathComponent(trimmed) + } else { + return .fallbackString(trimmed) + } + + let url = URL(fileURLWithPath: resolvedPath).standardized + if fileExists(url.path) { + return .openFile(url) + } + + var parent = url.deletingLastPathComponent() + while !fileExists(parent.path) && parent.path != "/" { + parent = parent.deletingLastPathComponent() + } + return .revealInFinder(file: url, root: parent) + } +} + +/// Single entry point for terminal link clicks across all `TerminalViewDelegate` +/// implementations in the project (direct sessions, tmux control-mode panes, +/// and any future surfaces). +/// +/// **Wiring rule:** every `TerminalViewDelegate` we author must implement +/// `requestOpenLink` and route it here. SwiftTerm's protocol-default opens +/// the raw payload via `URL(string:)` + `NSWorkspace.shared.open`, which +/// produces the macOS "-50" Finder dialog for absolute paths. Skipping this +/// step silently regresses to that default — see `TerminalLinkRoutingTests`. +@MainActor +enum TermQTerminalLink { + + /// Resolves a terminal-detected link via `TerminalLinkResolver` and + /// applies the resulting action through `NSWorkspace`. + static func open(link: String, cwd: String?) { + TermQLogger.ui.debug("TermQTerminalLink.open raw=\(link) cwd=\(cwd ?? "")") + let action = TerminalLinkResolver.resolve( + link: link, + cwd: cwd, + fileExists: { FileManager.default.fileExists(atPath: $0) } + ) + switch action { + case .noop: + return + case .openURL(let url): + NSWorkspace.shared.open(url) + case .fallbackString(let string): + if let url = URL(string: string) { NSWorkspace.shared.open(url) } + case .revealInFinder(_, let root): + // file doesn't exist; selectFile silently fails for missing paths. + // Open the nearest existing parent instead so Finder shows something useful. + NSWorkspace.shared.open(root) + case .openFile(let url): + openFileWithDefaultApp(url) + } + } + + /// Opens `url` with its registered default application, surfacing a + /// friendly alert instead of the macOS "-50" Finder dialog when + /// LaunchServices has no handler or the launch attempt fails. + private static func openFileWithDefaultApp(_ url: URL) { + var isDir: ObjCBool = false + if FileManager.default.fileExists(atPath: url.path, isDirectory: &isDir), isDir.boolValue { + NSWorkspace.shared.open(url) + return + } + + guard let handler = NSWorkspace.shared.urlForApplication(toOpen: url) else { + TermQLogger.ui.info("TermQTerminalLink no default handler path=\(url.path)") + presentNoHandlerAlert(for: url) + return + } + + let configuration = NSWorkspace.OpenConfiguration() + NSWorkspace.shared.open([url], withApplicationAt: handler, configuration: configuration) { + _, error in + guard let error else { return } + Task { @MainActor in + TermQLogger.ui.warning( + "TermQTerminalLink launch failed path=\(url.path) error=\(error.localizedDescription)" + ) + presentOpenFailedAlert(for: url, error: error) + } + } + } + + private static func presentNoHandlerAlert(for url: URL) { + let revealed = AlertBuilder.confirm( + title: Strings.Alert.FileOpen.noHandlerTitle, + message: Strings.Alert.FileOpen.noHandlerMessage(url.lastPathComponent), + confirmButton: Strings.Alert.FileOpen.revealInFinder, + style: .informational + ) + if revealed { + NSWorkspace.shared.activateFileViewerSelecting([url]) + } + } + + private static func presentOpenFailedAlert(for url: URL, error: Error) { + let revealed = AlertBuilder.confirm( + title: Strings.Alert.FileOpen.failedTitle, + message: Strings.Alert.FileOpen.failedMessage( + error: error.localizedDescription, filename: url.lastPathComponent + ), + confirmButton: Strings.Alert.FileOpen.revealInFinder, + style: .warning + ) + if revealed { + NSWorkspace.shared.activateFileViewerSelecting([url]) + } + } +} diff --git a/Sources/TermQ/Utilities/Strings.swift b/Sources/TermQ/Utilities/Strings.swift index 1746f2a1..aab8c5ee 100644 --- a/Sources/TermQ/Utilities/Strings.swift +++ b/Sources/TermQ/Utilities/Strings.swift @@ -677,6 +677,18 @@ enum Strings { static func quitWithDirectSessionsMessageWithTmux(_ count: Int) -> String { localized("alert.quit.direct.sessions.message %lld", count) } + + enum FileOpen { + static var noHandlerTitle: String { localized("alert.file.open.no.handler.title") } + static func noHandlerMessage(_ filename: String) -> String { + localized("alert.file.open.no.handler.message %@", filename) + } + static var failedTitle: String { localized("alert.file.open.failed.title") } + static func failedMessage(error: String, filename: String) -> String { + localized("alert.file.open.failed.message %@ %@", error, filename) + } + static var revealInFinder: String { localized("alert.file.open.reveal.in.finder") } + } } // MARK: - Backup diff --git a/Sources/TermQ/ViewModels/TerminalSessionManager+ControlMode.swift b/Sources/TermQ/ViewModels/TerminalSessionManager+ControlMode.swift index b80236ab..159342cd 100644 --- a/Sources/TermQ/ViewModels/TerminalSessionManager+ControlMode.swift +++ b/Sources/TermQ/ViewModels/TerminalSessionManager+ControlMode.swift @@ -135,6 +135,13 @@ class ControlModePaneDelegate: TerminalViewDelegate { func rangeChanged(source: TerminalView, startY: Int, endY: Int) {} func bell(source: TerminalView) {} func hostCurrentDirectoryUpdate(source: TerminalView, directory: String?) {} + + func requestOpenLink(source: TerminalView, link: String, params: [String: String]) { + // Control-mode panes don't track cwd via OSC 7 — pass nil. + Task { @MainActor in + TermQTerminalLink.open(link: link, cwd: nil) + } + } } // MARK: - Control Mode Integration diff --git a/Sources/TermQ/ViewModels/TerminalSessionManager.swift b/Sources/TermQ/ViewModels/TerminalSessionManager.swift index 1cb1e34d..2665b7fc 100644 --- a/Sources/TermQ/ViewModels/TerminalSessionManager.swift +++ b/Sources/TermQ/ViewModels/TerminalSessionManager.swift @@ -773,14 +773,10 @@ class SessionDelegate: NSObject, LocalProcessTerminalViewDelegate { } } - func requestOpenLink(source: TerminalView, link: String, params: [String: String]) { - let cardId = self.cardId - let manager = self.manager - Task { @MainActor in - let cwd = manager?.getCurrentDirectory(for: cardId) - openLink(link, cwd: cwd) - } - } + // Note: `requestOpenLink` is *not* implemented here. SwiftTerm's + // `LocalProcessTerminalView` claims `terminalDelegate = self` in its init, + // so this method on `SessionDelegate` (the `processDelegate`) is never + // invoked by SwiftTerm. The override lives on `TermQTerminalView` instead. func processTerminated(source: TerminalView, exitCode: Int32?) { // Capture values before Task to avoid Swift 6 Sendable issues @@ -814,44 +810,3 @@ class SessionDelegate: NSObject, LocalProcessTerminalViewDelegate { extension Notification.Name { static let termqDirectSessionExited = Notification.Name("termq.directSessionExited") } - -// MARK: - Link Opening - -/// Resolves and opens a link from a terminal Cmd+click. -/// - http/https URLs open in the default browser. -/// - Absolute paths open with their default app (or reveal in Finder if they don't exist). -/// - Relative paths are resolved against `cwd` before opening. -@MainActor -private func openLink(_ link: String, cwd: String?) { - // Explicit http/https — straight to browser - if link.hasPrefix("http://") || link.hasPrefix("https://"), - let url = URL(string: link) - { - NSWorkspace.shared.open(url) - return - } - - // Resolve to an absolute path - let resolvedPath: String - if link.hasPrefix("/") { - resolvedPath = link - } else if let base = cwd { - resolvedPath = (base as NSString).appendingPathComponent(link) - } else { - // No cwd and not absolute — fall back to default SwiftTerm behaviour - if let url = URL(string: link) { NSWorkspace.shared.open(url) } - return - } - - let url = URL(fileURLWithPath: resolvedPath).standardized - if FileManager.default.fileExists(atPath: url.path) { - NSWorkspace.shared.open(url) - } else { - // Path doesn't exist — reveal the nearest parent that does - var parent = url.deletingLastPathComponent() - while !FileManager.default.fileExists(atPath: parent.path) && parent.path != "/" { - parent = parent.deletingLastPathComponent() - } - NSWorkspace.shared.selectFile(url.path, inFileViewerRootedAtPath: parent.path) - } -} diff --git a/Sources/TermQ/Views/TerminalHostView.swift b/Sources/TermQ/Views/TerminalHostView.swift index 8977dfe7..4bbdf10a 100644 --- a/Sources/TermQ/Views/TerminalHostView.swift +++ b/Sources/TermQ/Views/TerminalHostView.swift @@ -10,6 +10,10 @@ class TermQTerminalView: LocalProcessTerminalView { /// The card ID this terminal belongs to var cardId: UUID? + /// Retains the proxy delegate installed in init. `terminalDelegate` is `weak` + /// on `TerminalView`, so this strong reference keeps it alive. + private var linkDelegate: TermQLinkDelegate? + /// Terminal title for notifications var terminalTitle: String = "Terminal" @@ -50,6 +54,32 @@ class TermQTerminalView: LocalProcessTerminalView { /// Persists the intended viewport position across linefeed resets. private var selectionScrollTargetRow: Int? + // MARK: - Init + + /// Install `TermQLinkDelegate` so link clicks route through `TermQTerminalLink.open`. + /// + /// SwiftTerm's `LocalProcessTerminalView.init` sets `terminalDelegate = self`. The + /// `requestOpenLink` witness for that conformance was compiled into the SwiftTerm + /// binary and maps to the protocol-extension default (`URL(string:)` + `NSWorkspace.open` + /// → macOS "-50" dialog). Subclass overrides land in a separate vtable slot that the + /// inherited witness never consults. SwiftTerm's own docs say: "If you must change the + /// delegate make sure that you proxy the values." We follow that guidance here. + override init(frame: CGRect) { + super.init(frame: frame) + installLinkDelegate() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + installLinkDelegate() + } + + private func installLinkDelegate() { + let delegate = TermQLinkDelegate(view: self) + linkDelegate = delegate + terminalDelegate = delegate + } + deinit { // Use MainActor.assumeIsolated since deinit is nonisolated in Swift 6 // but we're always deallocated on the main thread for NSView subclasses @@ -620,6 +650,55 @@ class TermQTerminalView: LocalProcessTerminalView { } } +/// Full-proxy `TerminalViewDelegate` that intercepts `requestOpenLink` and forwards +/// every other method to `LocalProcessTerminalView`'s own implementations. +/// +/// `TerminalViewDelegate` isn't annotated `@MainActor`, but SwiftTerm only ever calls +/// it from `TerminalView` (NSView → `@MainActor`). The class is marked `@MainActor` +/// to reflect that reality; each protocol method is `nonisolated` to satisfy the +/// conformance and uses `assumeIsolated` to re-enter the main actor for forwarded calls. +/// +/// Per SwiftTerm's docs: "If you must change the delegate make sure that you proxy +/// the values in your implementation to the values set after initializing this instance." +/// +/// See `TerminalLinkRoutingTests` for the static guardrail that catches any future +/// `requestOpenLink` definition that doesn't route through `TermQTerminalLink.open`. +@MainActor +private final class TermQLinkDelegate: TerminalViewDelegate { + private weak var view: TermQTerminalView? + + init(view: TermQTerminalView) { self.view = view } + + nonisolated func requestOpenLink(source: TerminalView, link: String, params: [String: String]) { + MainActor.assumeIsolated { + let cwd = view?.cardId.flatMap { TerminalSessionManager.shared.getCurrentDirectory(for: $0) } + TermQTerminalLink.open(link: link, cwd: cwd) + } + } + + nonisolated func sizeChanged(source: TerminalView, newCols: Int, newRows: Int) { + MainActor.assumeIsolated { view?.sizeChanged(source: source, newCols: newCols, newRows: newRows) } + } + nonisolated func setTerminalTitle(source: TerminalView, title: String) { + MainActor.assumeIsolated { view?.setTerminalTitle(source: source, title: title) } + } + nonisolated func hostCurrentDirectoryUpdate(source: TerminalView, directory: String?) { + MainActor.assumeIsolated { view?.hostCurrentDirectoryUpdate(source: source, directory: directory) } + } + nonisolated func send(source: TerminalView, data: ArraySlice) { + MainActor.assumeIsolated { view?.send(source: source, data: data) } + } + nonisolated func scrolled(source: TerminalView, position: Double) { + MainActor.assumeIsolated { view?.scrolled(source: source, position: position) } + } + nonisolated func clipboardCopy(source: TerminalView, content: Data) { + MainActor.assumeIsolated { view?.clipboardCopy(source: source, content: content) } + } + nonisolated func rangeChanged(source: TerminalView, startY: Int, endY: Int) { + MainActor.assumeIsolated { view?.rangeChanged(source: source, startY: startY, endY: endY) } + } +} + /// Container view that adds padding around the terminal and handles alternate scroll mode class TerminalContainerView: NSView { private(set) var terminal: TermQTerminalView diff --git a/Tests/TermQTests/TerminalLinkResolverTests.swift b/Tests/TermQTests/TerminalLinkResolverTests.swift new file mode 100644 index 00000000..916d4e47 --- /dev/null +++ b/Tests/TermQTests/TerminalLinkResolverTests.swift @@ -0,0 +1,183 @@ +import XCTest + +@testable import TermQ + +final class TerminalLinkResolverTests: XCTestCase { + + // MARK: - sanitize + + func testSanitize_trimsLeadingAndTrailingWhitespace() { + XCTAssertEqual(TerminalLinkResolver.sanitize(" /tmp/file.md "), "/tmp/file.md") + } + + func testSanitize_stripsTrailingPunctuationFromPath() { + XCTAssertEqual(TerminalLinkResolver.sanitize("/tmp/file.md."), "/tmp/file.md") + XCTAssertEqual(TerminalLinkResolver.sanitize("/tmp/file.md,"), "/tmp/file.md") + XCTAssertEqual(TerminalLinkResolver.sanitize("/tmp/file.md)"), "/tmp/file.md") + XCTAssertEqual(TerminalLinkResolver.sanitize("/tmp/file.md]"), "/tmp/file.md") + XCTAssertEqual(TerminalLinkResolver.sanitize("/tmp/file.md:"), "/tmp/file.md") + XCTAssertEqual(TerminalLinkResolver.sanitize("/tmp/file.md\""), "/tmp/file.md") + } + + func testSanitize_stripsCombinedTrailingPunctuationAndWhitespace() { + XCTAssertEqual(TerminalLinkResolver.sanitize("/tmp/file.md).\n"), "/tmp/file.md") + } + + func testSanitize_doesNotTouchInteriorPunctuation() { + XCTAssertEqual(TerminalLinkResolver.sanitize("/a.b/c.d.md"), "/a.b/c.d.md") + } + + func testSanitize_emptyAndWhitespaceOnly() { + XCTAssertEqual(TerminalLinkResolver.sanitize(""), "") + XCTAssertEqual(TerminalLinkResolver.sanitize(" \n "), "") + } + + // MARK: - resolve: http/https + + func testResolve_httpURL_returnsOpenURL() { + let action = TerminalLinkResolver.resolve( + link: "https://example.com/x", + cwd: nil, + fileExists: { _ in false } + ) + XCTAssertEqual(action, .openURL(URL(string: "https://example.com/x")!)) + } + + func testResolve_httpURL_withTrailingPunctuation_isStripped() { + let action = TerminalLinkResolver.resolve( + link: "https://example.com/x).", + cwd: nil, + fileExists: { _ in false } + ) + XCTAssertEqual(action, .openURL(URL(string: "https://example.com/x")!)) + } + + // MARK: - resolve: absolute paths + + func testResolve_absolutePath_existing_returnsOpenFile() { + let action = TerminalLinkResolver.resolve( + link: "/tmp/foo.md", + cwd: nil, + fileExists: { $0 == "/tmp/foo.md" } + ) + XCTAssertEqual(action, .openFile(URL(fileURLWithPath: "/tmp/foo.md").standardized)) + } + + func testResolve_absolutePath_missing_revealsNearestExistingParent() { + let action = TerminalLinkResolver.resolve( + link: "/tmp/missing/dir/file.md", + cwd: nil, + fileExists: { $0 == "/tmp" } + ) + guard case .revealInFinder(let file, let root) = action else { + XCTFail("expected revealInFinder, got \(action)") + return + } + XCTAssertEqual(file.path, "/tmp/missing/dir/file.md") + XCTAssertEqual(root.path, "/tmp") + } + + func testResolve_absolutePath_missingAllParents_revealsRoot() { + let action = TerminalLinkResolver.resolve( + link: "/nope/file.md", + cwd: nil, + fileExists: { _ in false } + ) + if case .revealInFinder(_, let root) = action { + XCTAssertEqual(root.path, "/") + } else { + XCTFail("expected revealInFinder, got \(action)") + } + } + + func testResolve_absolutePath_withTrailingPunctuation_existsAfterTrim() { + let action = TerminalLinkResolver.resolve( + link: "/tmp/foo.md.", + cwd: nil, + fileExists: { $0 == "/tmp/foo.md" } + ) + XCTAssertEqual(action, .openFile(URL(fileURLWithPath: "/tmp/foo.md").standardized)) + } + + // MARK: - resolve: relative paths + + func testResolve_relativePath_withCwd_resolvesAndOpens() { + let action = TerminalLinkResolver.resolve( + link: "sub/file.md", + cwd: "/tmp", + fileExists: { $0 == "/tmp/sub/file.md" } + ) + XCTAssertEqual(action, .openFile(URL(fileURLWithPath: "/tmp/sub/file.md").standardized)) + } + + func testResolve_relativePath_withCwd_missing_revealsParent() { + let action = TerminalLinkResolver.resolve( + link: "sub/file.md", + cwd: "/tmp", + fileExists: { $0 == "/tmp" } + ) + if case .revealInFinder(_, let root) = action { + XCTAssertEqual(root.path, "/tmp") + } else { + XCTFail("expected revealInFinder, got \(action)") + } + } + + func testResolve_relativePath_withoutCwd_returnsFallbackString() { + let action = TerminalLinkResolver.resolve( + link: "file.md", + cwd: nil, + fileExists: { _ in false } + ) + XCTAssertEqual(action, .fallbackString("file.md")) + } + + // MARK: - resolve: edge cases + + func testResolve_emptyLink_isNoop() { + let action = TerminalLinkResolver.resolve( + link: "", + cwd: "/tmp", + fileExists: { _ in true } + ) + XCTAssertEqual(action, .noop) + } + + func testResolve_whitespaceOnlyLink_isNoop() { + let action = TerminalLinkResolver.resolve( + link: " \n ", + cwd: "/tmp", + fileExists: { _ in true } + ) + XCTAssertEqual(action, .noop) + } + + func testResolve_directoryPath_existing_returnsOpenFile() { + // Directories take the same .openFile branch — the executor delegates + // to NSWorkspace which opens the folder in Finder. + let action = TerminalLinkResolver.resolve( + link: "/Users/me/Workspace/project", + cwd: nil, + fileExists: { $0 == "/Users/me/Workspace/project" } + ) + XCTAssertEqual( + action, + .openFile(URL(fileURLWithPath: "/Users/me/Workspace/project").standardized) + ) + } + + func testResolve_pathWithSpacesAndTrailingComma_handledCorrectly() { + // Mimics the real-world case: "Plan committed to /Users/.../foo.md, and ..." + // Once SwiftTerm's regex captures the path with a trailing comma, sanitize + // must strip it so fileExists matches. + let action = TerminalLinkResolver.resolve( + link: "/Users/me/Plans/foo.md,", + cwd: nil, + fileExists: { $0 == "/Users/me/Plans/foo.md" } + ) + XCTAssertEqual( + action, + .openFile(URL(fileURLWithPath: "/Users/me/Plans/foo.md").standardized) + ) + } +} diff --git a/Tests/TermQTests/TerminalLinkRoutingTests.swift b/Tests/TermQTests/TerminalLinkRoutingTests.swift new file mode 100644 index 00000000..c9fd48ff --- /dev/null +++ b/Tests/TermQTests/TerminalLinkRoutingTests.swift @@ -0,0 +1,140 @@ +import XCTest + +@testable import TermQ + +/// Static guardrail: every `requestOpenLink` definition in TermQ must route +/// through `TermQTerminalLink.open(link:cwd:)`. +/// +/// SwiftTerm has *two* ways for a URL click to land outside our central +/// handler: +/// 1. A `TerminalViewDelegate` conformer that doesn't override the method +/// (tmux control mode hit this). +/// 2. A `LocalProcessTerminalView` subclass — `LocalProcessTerminalView.init` +/// assigns `terminalDelegate = self`, so the view itself fields +/// `requestOpenLink`, not the configured `processDelegate`. Forgetting +/// to override on the subclass silently regresses to SwiftTerm's broken +/// default (`URL(string:)` + `NSWorkspace.open` → macOS "-50" dialog). +/// +/// This test scans the project for every `requestOpenLink` declaration site +/// and asserts each one calls `TermQTerminalLink.open`. +final class TerminalLinkRoutingTests: XCTestCase { + + func testEveryRequestOpenLinkDefinition_routesThroughTermQTerminalLink() throws { + let sourcesURL = try sourcesDirectory() + let swiftFiles = try collectSwiftFiles(under: sourcesURL) + + var offenders: [String] = [] + + for file in swiftFiles { + let contents = try String(contentsOf: file, encoding: .utf8) + for definition in requestOpenLinkBodies(in: contents) { + if !definition.body.contains("TermQTerminalLink.open") { + offenders.append( + "\(file.lastPathComponent):\(definition.line) — does not call TermQTerminalLink.open" + ) + } + } + } + + XCTAssertTrue( + offenders.isEmpty, + """ + Some `requestOpenLink` definitions don't route through TermQTerminalLink.open. \ + They will fall back to SwiftTerm's broken default and produce the macOS "-50" \ + Finder dialog for absolute paths. + + \(offenders.joined(separator: "\n")) + + Fix: call `TermQTerminalLink.open(link: link, cwd: )` from the body. + """ + ) + } + + func testTermQTerminalView_installsLinkDelegate() throws { + // Regression guard: TermQTerminalView must install TermQLinkDelegate in its + // init. SwiftTerm's requestOpenLink witness table entry is baked into the + // SwiftTerm binary — a subclass override in our module is never consulted. + // Per SwiftTerm's docs, the correct approach is to set a custom terminalDelegate + // and proxy all values. TermQLinkDelegate does this; installLinkDelegate wires it. + let url = try sourcesDirectory().appendingPathComponent("Views/TerminalHostView.swift") + let contents = try String(contentsOf: url, encoding: .utf8) + XCTAssertTrue( + contents.contains("installLinkDelegate"), + """ + TermQTerminalView must call installLinkDelegate() in its init overrides. \ + SwiftTerm's requestOpenLink witness is baked into the SwiftTerm binary — \ + the only reliable fix is to replace terminalDelegate with a proxy (TermQLinkDelegate) \ + per SwiftTerm's own documentation. + """ + ) + XCTAssertTrue( + contents.contains("TermQLinkDelegate"), + "TermQLinkDelegate proxy must be defined in TerminalHostView.swift." + ) + } + + // MARK: - Source scanning + + private struct DefinitionMatch { + let line: Int + let body: String + } + + private func sourcesDirectory() throws -> URL { + var url = URL(fileURLWithPath: #filePath) + .deletingLastPathComponent() // TermQTests + .deletingLastPathComponent() // Tests + .deletingLastPathComponent() // package root + url.appendPathComponent("Sources/TermQ") + var isDir: ObjCBool = false + guard FileManager.default.fileExists(atPath: url.path, isDirectory: &isDir), isDir.boolValue + else { + throw NSError( + domain: "TerminalLinkRoutingTests", code: 1, + userInfo: [NSLocalizedDescriptionKey: "Sources/TermQ not found at \(url.path)"] + ) + } + return url + } + + private func collectSwiftFiles(under root: URL) throws -> [URL] { + guard + let enumerator = FileManager.default.enumerator( + at: root, + includingPropertiesForKeys: [.isRegularFileKey], + options: [.skipsHiddenFiles] + ) + else { return [] } + var result: [URL] = [] + for case let url as URL in enumerator + where url.pathExtension == "swift" { + result.append(url) + } + return result + } + + /// Finds every `func requestOpenLink(...)` declaration in `source` and + /// returns its line number plus the brace-balanced body. + private func requestOpenLinkBodies(in source: String) -> [DefinitionMatch] { + let pattern = #"func\s+requestOpenLink\s*\([^{]*\)\s*\{"# + guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else { return [] } + let ns = source as NSString + let matches = regex.matches(in: source, range: NSRange(location: 0, length: ns.length)) + return matches.compactMap { match -> DefinitionMatch? in + let openBrace = match.range.location + match.range.length - 1 + var depth = 1 + var i = openBrace + 1 + while i < ns.length && depth > 0 { + let ch = ns.character(at: i) + if ch == 0x7B { depth += 1 } + if ch == 0x7D { depth -= 1 } + i += 1 + } + guard depth == 0 else { return nil } + let body = ns.substring(with: NSRange(location: openBrace + 1, length: i - openBrace - 2)) + let prefix = ns.substring(with: NSRange(location: 0, length: match.range.location)) + let line = prefix.reduce(into: 1) { count, ch in if ch == "\n" { count += 1 } } + return DefinitionMatch(line: line, body: body) + } + } +} From 69fd2a458a3b42145af407b4c9c192267851ac84 Mon Sep 17 00:00:00 2001 From: David Collie Date: Tue, 28 Apr 2026 22:38:06 +0100 Subject: [PATCH 08/29] chore: update CHANGELOG for v0.9.2 Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d5eb0bd5..a718cd7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.9.2] — 2026-04-28 + +### Fixed + +- Re-register URL Apple Event handler after SwiftUI scene setup (#239) +- Replace -50 Finder dialog error with correct file/URL open handling (#240) + +## [0.9.1] — 2026-04-28 + +### Fixed + +- Fix appcast not updating on stable release (#233) +- Fix uninstall for local harnesses with no YNH install record (#234) + ## [0.9.0] ### Added From 2682095c7290c50f231d1b4e5699e299b911356b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 28 Apr 2026 22:01:34 +0000 Subject: [PATCH 09/29] chore: Update appcast for release v0.9.2 Auto-generated appcast files for Sparkle auto-updates. Co-Authored-By: github-actions[bot] --- Docs/appcast-beta.xml | 1094 +++++++++++++++++++++++++++++++++++++++++ Docs/appcast.xml | 722 +++++++++++++++++++++++++++ 2 files changed, 1816 insertions(+) diff --git a/Docs/appcast-beta.xml b/Docs/appcast-beta.xml index 434f5c97..d830a539 100644 --- a/Docs/appcast-beta.xml +++ b/Docs/appcast-beta.xml @@ -5,6 +5,49 @@ https://github.com/eyelock/TermQ Most recent updates to TermQ en + + Version 0.9.2 + 0.9.2 + 0.9.2 + Tue, 28 Apr 2026 22:00:42 +0000 + Bug Fixes +
    +
  • replace -50 Finder dialog with correct file/URL handling (#240)
  • +
  • re-register URL Apple Event handler after SwiftUI scene setup (#239)
  • +
  • fix uninstall for local harnesses with no YNH install record (#234)
  • +
  • fix appcast not updating on stable release (#233)
  • +
+
+

Installation

+ +
    +
  1. Download TermQ-0.9.2.dmg
  2. +
  3. Open the DMG and drag TermQ.app to your Applications folder
  4. +
  5. Double-click to launch
  6. +
+

Alternative: Zip Archive

+
    +
  1. Download TermQ-0.9.2.zip
  2. +
  3. Unzip and move TermQ.app to your Applications folder
  4. +
+

CLI Tool

+

The termqcli CLI is bundled inside the app. To install it:

+
    +
  1. Open TermQ.app
  2. +
  3. Go to TermQ → Settings (or press ⌘,)
  4. +
  5. Click Install Command Line Tool
  6. +
+

Or manually:

+
sudo cp /Applications/TermQ.app/Contents/Resources/termqcli /usr/local/bin/termqcli
+

Checksums

+

See checksums.txt for SHA-256 checksums of all release artifacts.

]]>
+ + 14.0 +
Version 0.9.1 0.9.1 @@ -1395,6 +1438,1057 @@ type="application/octet-stream"/> 14.0 + + Version 0.7.0.b5 + 0.7.0.b5 + 0.7.0.b5 + Sun, 12 Apr 2026 11:08:29 +0000 + BROKEN APPCAST SIGNING See https://github.com/eyelock/TermQ/pull/109 for fix

+

Installation

+ +
    +
  1. Download TermQ-0.7.0-beta.5.dmg
  2. +
  3. Open the DMG and drag TermQ.app to your Applications folder
  4. +
  5. Double-click to launch
  6. +
+

Alternative: Zip Archive

+
    +
  1. Download TermQ-0.7.0-beta.5.zip
  2. +
  3. Unzip and move TermQ.app to your Applications folder
  4. +
+

CLI Tool

+

The termqcli CLI is bundled inside the app. To install it:

+
    +
  1. Open TermQ.app
  2. +
  3. Go to TermQ → Settings (or press ⌘,)
  4. +
  5. Click Install Command Line Tool
  6. +
+

Or manually:

+
sudo cp /Applications/TermQ.app/Contents/Resources/termqcli /usr/local/bin/termqcli
+

Checksums

+

See checksums.txt for SHA-256 checksums of all release artifacts.

+

What's Changed

+ +

New Contributors

+ +

Full Changelog: https://github.com/eyelock/TermQ/compare/v0.7.0-beta.4...v0.7.0-beta.5

]]>
+ + 14.0 +
+ + Version 0.7.0.b11 + 0.7.0.b11 + 0.7.0.b11 + Sun, 12 Apr 2026 23:00:14 +0000 + Installation + +
    +
  1. Download TermQ-0.7.0-beta.11.dmg
  2. +
  3. Open the DMG and drag TermQ.app to your Applications folder
  4. +
  5. Double-click to launch
  6. +
+

Alternative: Zip Archive

+
    +
  1. Download TermQ-0.7.0-beta.11.zip
  2. +
  3. Unzip and move TermQ.app to your Applications folder
  4. +
+

CLI Tool

+

The termqcli CLI is bundled inside the app. To install it:

+
    +
  1. Open TermQ.app
  2. +
  3. Go to TermQ → Settings (or press ⌘,)
  4. +
  5. Click Install Command Line Tool
  6. +
+

Or manually:

+
sudo cp /Applications/TermQ.app/Contents/Resources/termqcli /usr/local/bin/termqcli
+

Checksums

+

See checksums.txt for SHA-256 checksums of all release artifacts.

+

What's Changed

+ +

Full Changelog: https://github.com/eyelock/TermQ/compare/v0.7.0-beta.10...v0.7.0-beta.11

]]>
+ + 14.0 +
+ + Version 0.7.0.b10 + 0.7.0.b10 + 0.7.0.b10 + Sun, 12 Apr 2026 21:48:17 +0000 + Installation + +
    +
  1. Download TermQ-0.7.0-beta.10.dmg
  2. +
  3. Open the DMG and drag TermQ.app to your Applications folder
  4. +
  5. Double-click to launch
  6. +
+

Alternative: Zip Archive

+
    +
  1. Download TermQ-0.7.0-beta.10.zip
  2. +
  3. Unzip and move TermQ.app to your Applications folder
  4. +
+

CLI Tool

+

The termqcli CLI is bundled inside the app. To install it:

+
    +
  1. Open TermQ.app
  2. +
  3. Go to TermQ → Settings (or press ⌘,)
  4. +
  5. Click Install Command Line Tool
  6. +
+

Or manually:

+
sudo cp /Applications/TermQ.app/Contents/Resources/termqcli /usr/local/bin/termqcli
+

Checksums

+

See checksums.txt for SHA-256 checksums of all release artifacts.

+

What's Changed

+ +

Full Changelog: https://github.com/eyelock/TermQ/compare/v0.7.0-beta.9...v0.7.0-beta.10

]]>
+ + 14.0 +
+ + Version 0.7.0.b4 + 0.7.0.b4 + 0.7.0.b4 + Wed, 28 Jan 2026 00:56:38 +0000 + Installation + +
    +
  1. Download TermQ-0.7.0-beta.4.dmg
  2. +
  3. Open the DMG and drag TermQ.app to your Applications folder
  4. +
  5. Double-click to launch
  6. +
+

Alternative: Zip Archive

+
    +
  1. Download TermQ-0.7.0-beta.4.zip
  2. +
  3. Unzip and move TermQ.app to your Applications folder
  4. +
+

CLI Tool

+

The termqcli CLI is bundled inside the app. To install it:

+
    +
  1. Open TermQ.app
  2. +
  3. Go to TermQ → Settings (or press ⌘,)
  4. +
  5. Click Install Command Line Tool
  6. +
+

Or manually:

+
sudo cp /Applications/TermQ.app/Contents/Resources/termqcli /usr/local/bin/termqcli
+

Checksums

+

See checksums.txt for SHA-256 checksums of all release artifacts.

+

What's Changed

+ +

Full Changelog: https://github.com/eyelock/TermQ/compare/v0.7.0-beta.3...v0.7.0-beta.4

]]>
+ + 14.0 +
+ + Version 0.7.0.b3 + 0.7.0.b3 + 0.7.0.b3 + Mon, 19 Jan 2026 15:53:41 +0000 + Installation + +
    +
  1. Download TermQ-0.7.0-beta.3.dmg
  2. +
  3. Open the DMG and drag TermQ.app to your Applications folder
  4. +
  5. Double-click to launch
  6. +
+

Alternative: Zip Archive

+
    +
  1. Download TermQ-0.7.0-beta.3.zip
  2. +
  3. Unzip and move TermQ.app to your Applications folder
  4. +
+

CLI Tool

+

The termqcli CLI is bundled inside the app. To install it:

+
    +
  1. Open TermQ.app
  2. +
  3. Go to TermQ → Settings (or press ⌘,)
  4. +
  5. Click Install Command Line Tool
  6. +
+

Or manually:

+
sudo cp /Applications/TermQ.app/Contents/Resources/termqcli /usr/local/bin/termqcli
+

Checksums

+

See checksums.txt for SHA-256 checksums of all release artifacts.

+

What's Changed

+ +

Full Changelog: https://github.com/eyelock/TermQ/compare/v0.7.0-beta.2...v0.7.0-beta.3

]]>
+ + 14.0 +
+ + Version 0.7.0.b2 + 0.7.0.b2 + 0.7.0.b2 + Mon, 19 Jan 2026 08:30:17 +0000 + Installation + +
    +
  1. Download TermQ-0.7.0-beta.2.dmg
  2. +
  3. Open the DMG and drag TermQ.app to your Applications folder
  4. +
  5. Double-click to launch
  6. +
+

Alternative: Zip Archive

+
    +
  1. Download TermQ-0.7.0-beta.2.zip
  2. +
  3. Unzip and move TermQ.app to your Applications folder
  4. +
+

CLI Tool

+

The termqcli CLI is bundled inside the app. To install it:

+
    +
  1. Open TermQ.app
  2. +
  3. Go to TermQ → Settings (or press ⌘,)
  4. +
  5. Click Install Command Line Tool
  6. +
+

Or manually:

+
sudo cp /Applications/TermQ.app/Contents/Resources/termqcli /usr/local/bin/termqcli
+

Checksums

+

See checksums.txt for SHA-256 checksums of all release artifacts.

+

What's Changed

+ +

Full Changelog: https://github.com/eyelock/TermQ/compare/v0.7.0-beta.1...v0.7.0-beta.2

]]>
+ + 14.0 +
+ + Version 0.7.0.b1 + 0.7.0.b1 + 0.7.0.b1 + Mon, 19 Jan 2026 07:44:59 +0000 + Installation + +
    +
  1. Download TermQ-0.7.0-beta.1.dmg
  2. +
  3. Open the DMG and drag TermQ.app to your Applications folder
  4. +
  5. Double-click to launch
  6. +
+

Alternative: Zip Archive

+
    +
  1. Download TermQ-0.7.0-beta.1.zip
  2. +
  3. Unzip and move TermQ.app to your Applications folder
  4. +
+

CLI Tool

+

The termqcli CLI is bundled inside the app. To install it:

+
    +
  1. Open TermQ.app
  2. +
  3. Go to TermQ → Settings (or press ⌘,)
  4. +
  5. Click Install Command Line Tool
  6. +
+

Or manually:

+
sudo cp /Applications/TermQ.app/Contents/Resources/termqcli /usr/local/bin/termqcli
+

Checksums

+

See checksums.txt for SHA-256 checksums of all release artifacts.

+

What's Changed

+ +

Full Changelog: https://github.com/eyelock/TermQ/compare/v0.6.1...v0.7.0-beta.1

]]>
+ + 14.0 +
+ + Version 0.6.4 + 0.6.4 + 0.6.4 + Sat, 17 Jan 2026 17:25:05 +0000 + +

[!WARNING] The 0.6.x builds are quite unstable, I learned a good lesson here about getting a bit too carried away on how easy it is to ship features with Claude. Shipping, features and fixes that on the surface were working, but when you started trying to use TermQ as your base tool showed up as not quite ready yet. Feel free to try the 0.7.0 beta releases which are focussed on stabilising the app's current features!

+ +

Installation

+ +
    +
  1. Download TermQ-0.6.4.dmg
  2. +
  3. Open the DMG and drag TermQ.app to your Applications folder
  4. +
  5. Double-click to launch
  6. +
+

Alternative: Zip Archive

+
    +
  1. Download TermQ-0.6.4.zip
  2. +
  3. Unzip and move TermQ.app to your Applications folder
  4. +
+

CLI Tool

+

The termqcli CLI is bundled inside the app. To install it:

+
    +
  1. Open TermQ.app
  2. +
  3. Go to TermQ → Settings (or press ⌘,)
  4. +
  5. Click Install Command Line Tool
  6. +
+

Or manually:

+
sudo cp /Applications/TermQ.app/Contents/Resources/termqcli /usr/local/bin/termqcli
+

Checksums

+

See checksums.txt for SHA-256 checksums of all release artifacts.

+

Full Changelog: https://github.com/eyelock/TermQ/compare/v0.6.3...v0.6.4

]]>
+ + 14.0 +
+ + Version 0.6.3 + 0.6.3 + 0.6.3 + Sat, 17 Jan 2026 16:01:40 +0000 + Installation + +
    +
  1. Download TermQ-0.6.3.dmg
  2. +
  3. Open the DMG and drag TermQ.app to your Applications folder
  4. +
  5. Double-click to launch
  6. +
+

Alternative: Zip Archive

+
    +
  1. Download TermQ-0.6.3.zip
  2. +
  3. Unzip and move TermQ.app to your Applications folder
  4. +
+

CLI Tool

+

The termqcli CLI is bundled inside the app. To install it:

+
    +
  1. Open TermQ.app
  2. +
  3. Go to TermQ → Settings (or press ⌘,)
  4. +
  5. Click Install Command Line Tool
  6. +
+

Or manually:

+
sudo cp /Applications/TermQ.app/Contents/Resources/termqcli /usr/local/bin/termqcli
+

Checksums

+

See checksums.txt for SHA-256 checksums of all release artifacts.

+

Full Changelog: https://github.com/eyelock/TermQ/compare/v0.6.2...v0.6.3

]]>
+ + 14.0 +
+ + Version 0.6.2 + 0.6.2 + 0.6.2 + Sat, 17 Jan 2026 08:59:59 +0000 + Installation + +
    +
  1. Download TermQ-0.6.2.dmg
  2. +
  3. Open the DMG and drag TermQ.app to your Applications folder
  4. +
  5. Double-click to launch
  6. +
+

Alternative: Zip Archive

+
    +
  1. Download TermQ-0.6.2.zip
  2. +
  3. Unzip and move TermQ.app to your Applications folder
  4. +
+

CLI Tool

+

The termqcli CLI is bundled inside the app. To install it:

+
    +
  1. Open TermQ.app
  2. +
  3. Go to TermQ → Settings (or press ⌘,)
  4. +
  5. Click Install Command Line Tool
  6. +
+

Or manually:

+
sudo cp /Applications/TermQ.app/Contents/Resources/termqcli /usr/local/bin/termqcli
+

Checksums

+

See checksums.txt for SHA-256 checksums of all release artifacts.

+

Full Changelog: https://github.com/eyelock/TermQ/compare/v0.6.1...v0.6.2

]]>
+ + 14.0 +
+ + Version 0.6.2.b1 + 0.6.2.b1 + 0.6.2.b1 + Sat, 17 Jan 2026 01:34:33 +0000 + Installation + +
    +
  1. Download TermQ-0.6.2-beta.1.dmg
  2. +
  3. Open the DMG and drag TermQ.app to your Applications folder
  4. +
  5. Double-click to launch
  6. +
+

Alternative: Zip Archive

+
    +
  1. Download TermQ-0.6.2-beta.1.zip
  2. +
  3. Unzip and move TermQ.app to your Applications folder
  4. +
+

CLI Tool

+

The termqcli CLI is bundled inside the app. To install it:

+
    +
  1. Open TermQ.app
  2. +
  3. Go to TermQ → Settings (or press ⌘,)
  4. +
  5. Click Install Command Line Tool
  6. +
+

Or manually:

+
sudo cp /Applications/TermQ.app/Contents/Resources/termqcli /usr/local/bin/termqcli
+

Checksums

+

See checksums.txt for SHA-256 checksums of all release artifacts.

+

Full Changelog: https://github.com/eyelock/TermQ/compare/v0.6.1...v0.6.2-beta.1

]]>
+ + 14.0 +
+ + Version 0.6.1 + 0.6.1 + 0.6.1 + Fri, 16 Jan 2026 14:16:26 +0000 + Installation + +
    +
  1. Download TermQ-0.6.1.dmg
  2. +
  3. Open the DMG and drag TermQ.app to your Applications folder
  4. +
  5. Right-click and select "Open" on first launch (required for unsigned apps)
  6. +
+

Alternative: Zip Archive

+
    +
  1. Download TermQ-0.6.1.zip
  2. +
  3. Unzip and move TermQ.app to your Applications folder
  4. +
+

CLI Tool

+

The termqcli CLI is bundled inside the app. To install it:

+
    +
  1. Open TermQ.app
  2. +
  3. Go to TermQ → Settings (or press ⌘,)
  4. +
  5. Click Install Command Line Tool
  6. +
+

Or manually:

+
sudo cp /Applications/TermQ.app/Contents/Resources/termqcli /usr/local/bin/termqcli
+

Checksums

+

See checksums.txt for SHA-256 checksums of all release artifacts.

+

What's Changed

+ +

Full Changelog: https://github.com/eyelock/TermQ/compare/v0.6.0...v0.6.1

]]>
+ + 14.0 +
+ + Version 0.6.0 + 0.6.0 + 0.6.0 + Fri, 16 Jan 2026 09:18:53 +0000 + Installation + +
    +
  1. Download TermQ-0.6.0.dmg
  2. +
  3. Open the DMG and drag TermQ.app to your Applications folder
  4. +
  5. Right-click and select "Open" on first launch (required for unsigned apps)
  6. +
+

Alternative: Zip Archive

+
    +
  1. Download TermQ-0.6.0.zip
  2. +
  3. Unzip and move TermQ.app to your Applications folder
  4. +
+

CLI Tool

+

The termqcli CLI is bundled inside the app. To install it:

+
    +
  1. Open TermQ.app
  2. +
  3. Go to TermQ → Settings (or press ⌘,)
  4. +
  5. Click Install Command Line Tool
  6. +
+

Or manually:

+
sudo cp /Applications/TermQ.app/Contents/Resources/termqcli /usr/local/bin/termqcli
+

Checksums

+

See checksums.txt for SHA-256 checksums of all release artifacts.

+

What's Changed

+ +

Full Changelog: https://github.com/eyelock/TermQ/compare/v0.5.2...v0.6.0

]]>
+ + 14.0 +
+ + Version 0.5.2 + 0.5.2 + 0.5.2 + Wed, 14 Jan 2026 16:08:13 +0000 + Installation + +
    +
  1. Download TermQ-0.5.2.dmg
  2. +
  3. Open the DMG and drag TermQ.app to your Applications folder
  4. +
  5. Right-click and select "Open" on first launch (required for unsigned apps)
  6. +
+

Alternative: Zip Archive

+
    +
  1. Download TermQ-0.5.2.zip
  2. +
  3. Unzip and move TermQ.app to your Applications folder
  4. +
+

CLI Tool

+

The termq CLI is bundled inside the app. To install it:

+
    +
  1. Open TermQ.app
  2. +
  3. Go to TermQ → Settings (or press ⌘,)
  4. +
  5. Click Install Command Line Tool
  6. +
+

Or manually:

+
sudo cp /Applications/TermQ.app/Contents/Resources/termq /usr/local/bin/
+

Checksums

+

See checksums.txt for SHA-256 checksums of all release artifacts.

+

What's Changed

+ +

Full Changelog: https://github.com/eyelock/TermQ/compare/v0.5.1...v0.5.2

]]>
+ + 14.0 +
+ + Version 0.5.1 + 0.5.1 + 0.5.1 + Wed, 14 Jan 2026 15:19:45 +0000 + Installation + +
    +
  1. Download TermQ-0.5.1.dmg
  2. +
  3. Open the DMG and drag TermQ.app to your Applications folder
  4. +
  5. Right-click and select "Open" on first launch (required for unsigned apps)
  6. +
+

Alternative: Zip Archive

+
    +
  1. Download TermQ-0.5.1.zip
  2. +
  3. Unzip and move TermQ.app to your Applications folder
  4. +
+

CLI Tool

+

The termq CLI is bundled inside the app. To install it:

+
    +
  1. Open TermQ.app
  2. +
  3. Go to TermQ → Settings (or press ⌘,)
  4. +
  5. Click Install Command Line Tool
  6. +
+

Or manually:

+
sudo cp /Applications/TermQ.app/Contents/Resources/termq /usr/local/bin/
+

Checksums

+

See checksums.txt for SHA-256 checksums of all release artifacts.

+

What's Changed

+ +

Full Changelog: https://github.com/eyelock/TermQ/compare/v0.5.0...v0.5.1

]]>
+ + 14.0 +
+ + Version 0.5.0 + 0.5.0 + 0.5.0 + Wed, 14 Jan 2026 07:50:45 +0000 + Installation + +
    +
  1. Download TermQ-0.5.0.dmg
  2. +
  3. Open the DMG and drag TermQ.app to your Applications folder
  4. +
  5. Right-click and select "Open" on first launch (required for unsigned apps)
  6. +
+

Alternative: Zip Archive

+
    +
  1. Download TermQ-0.5.0.zip
  2. +
  3. Unzip and move TermQ.app to your Applications folder
  4. +
+

CLI Tool

+

The termq CLI is bundled inside the app. To install it:

+
    +
  1. Open TermQ.app
  2. +
  3. Go to TermQ → Settings (or press ⌘,)
  4. +
  5. Click Install Command Line Tool
  6. +
+

Or manually:

+
sudo cp /Applications/TermQ.app/Contents/Resources/termq /usr/local/bin/
+

Checksums

+

See checksums.txt for SHA-256 checksums of all release artifacts.

+

What's Changed

+ +

Full Changelog: https://github.com/eyelock/TermQ/compare/v0.4.4...v0.5.0

]]>
+ + 14.0 +
+ + Version 0.4.4 + 0.4.4 + 0.4.4 + Tue, 13 Jan 2026 20:45:53 +0000 + Installation + +
    +
  1. Download TermQ-0.4.4.dmg
  2. +
  3. Open the DMG and drag TermQ.app to your Applications folder
  4. +
  5. Right-click and select "Open" on first launch (required for unsigned apps)
  6. +
+

Alternative: Zip Archive

+
    +
  1. Download TermQ-0.4.4.zip
  2. +
  3. Unzip and move TermQ.app to your Applications folder
  4. +
+

CLI Tool

+

The termq CLI is bundled inside the app. To install it:

+
    +
  1. Open TermQ.app
  2. +
  3. Go to TermQ → Settings (or press ⌘,)
  4. +
  5. Click Install Command Line Tool
  6. +
+

Or manually:

+
sudo cp /Applications/TermQ.app/Contents/Resources/termq /usr/local/bin/
+

Checksums

+

See checksums.txt for SHA-256 checksums of all release artifacts.

+

What's Changed

+ +

Full Changelog: https://github.com/eyelock/TermQ/compare/v0.4.3...v0.4.4

]]>
+ + 14.0 +
+ + Version 0.4.3 + 0.4.3 + 0.4.3 + Tue, 13 Jan 2026 16:18:17 +0000 + Installation + +
    +
  1. Download TermQ-0.4.3.dmg
  2. +
  3. Open the DMG and drag TermQ.app to your Applications folder
  4. +
  5. Right-click and select "Open" on first launch (required for unsigned apps)
  6. +
+

Alternative: Zip Archive

+
    +
  1. Download TermQ-0.4.3.zip
  2. +
  3. Unzip and move TermQ.app to your Applications folder
  4. +
+

CLI Tool

+

The termq CLI is bundled inside the app. To install it:

+
    +
  1. Open TermQ.app
  2. +
  3. Go to TermQ → Settings (or press ⌘,)
  4. +
  5. Click Install Command Line Tool
  6. +
+

Or manually:

+
sudo cp /Applications/TermQ.app/Contents/Resources/termq /usr/local/bin/
+

Checksums

+

See checksums.txt for SHA-256 checksums of all release artifacts.

+

What's Changed

+ +

Full Changelog: https://github.com/eyelock/TermQ/compare/v0.4.2...v0.4.3

]]>
+ + 14.0 +
+ + Version 0.4.2 + 0.4.2 + 0.4.2 + Mon, 12 Jan 2026 20:33:57 +0000 + Installation + +
    +
  1. Download TermQ-0.4.2.dmg
  2. +
  3. Open the DMG and drag TermQ.app to your Applications folder
  4. +
  5. Right-click and select "Open" on first launch (required for unsigned apps)
  6. +
+

Alternative: Zip Archive

+
    +
  1. Download TermQ-0.4.2.zip
  2. +
  3. Unzip and move TermQ.app to your Applications folder
  4. +
+

CLI Tool

+

The termq CLI is bundled inside the app. To install it:

+
    +
  1. Open TermQ.app
  2. +
  3. Go to TermQ → Settings (or press ⌘,)
  4. +
  5. Click Install Command Line Tool
  6. +
+

Or manually:

+
sudo cp /Applications/TermQ.app/Contents/Resources/termq /usr/local/bin/
+

Checksums

+

See checksums.txt for SHA-256 checksums of all release artifacts.

+

What's Changed

+ +

Full Changelog: https://github.com/eyelock/TermQ/compare/v0.4.1...v0.4.2

]]>
+ + 14.0 +
+ + Version 0.4.1 + 0.4.1 + 0.4.1 + Mon, 12 Jan 2026 20:17:38 +0000 + Installation + +
    +
  1. Download TermQ-0.4.1.dmg
  2. +
  3. Open the DMG and drag TermQ.app to your Applications folder
  4. +
  5. Right-click and select "Open" on first launch (required for unsigned apps)
  6. +
+

Alternative: Zip Archive

+
    +
  1. Download TermQ-0.4.1.zip
  2. +
  3. Unzip and move TermQ.app to your Applications folder
  4. +
+

CLI Tool

+

The termq CLI is bundled inside the app. To install it:

+
    +
  1. Open TermQ.app
  2. +
  3. Go to TermQ → Settings (or press ⌘,)
  4. +
  5. Click Install Command Line Tool
  6. +
+

Or manually:

+
sudo cp /Applications/TermQ.app/Contents/Resources/termq /usr/local/bin/
+

Checksums

+

See checksums.txt for SHA-256 checksums of all release artifacts.

+

What's Changed

+ +

Full Changelog: https://github.com/eyelock/TermQ/compare/v0.4.0...v0.4.1

]]>
+ + 14.0 +
+ + Version 0.4.0 + 0.4.0 + 0.4.0 + Sun, 11 Jan 2026 22:00:56 +0000 + Full Changelog: https://github.com/eyelock/TermQ/compare/v0.3.1...v0.4.0

]]>
+ + 14.0 +
+ + Version 0.3.0 + 0.3.0 + 0.3.0 + Fri, 09 Jan 2026 14:01:41 +0000 + Installation + +
    +
  1. Download TermQ-0.3.0.dmg
  2. +
  3. Open the DMG and drag TermQ.app to your Applications folder
  4. +
  5. Right-click and select "Open" on first launch (required for unsigned apps)
  6. +
+

Alternative: Zip Archive

+
    +
  1. Download TermQ-0.3.0.zip
  2. +
  3. Unzip and move TermQ.app to your Applications folder
  4. +
+

CLI Tool

+

The termq CLI is bundled inside the app. To install it:

+
    +
  1. Open TermQ.app
  2. +
  3. Go to TermQ → Settings (or press ⌘,)
  4. +
  5. Click Install Command Line Tool
  6. +
+

Or manually:

+
sudo cp /Applications/TermQ.app/Contents/Resources/termq /usr/local/bin/
+

Checksums

+

See checksums.txt for SHA-256 checksums of all release artifacts.

+

What's Changed

+ +

Full Changelog: https://github.com/eyelock/TermQ/compare/v0.2.1...v0.3.0

]]>
+ + 14.0 +
+ + Version 0.2.1 + 0.2.1 + 0.2.1 + Fri, 09 Jan 2026 11:23:00 +0000 + Installation + +
    +
  1. Download TermQ-0.2.1.dmg
  2. +
  3. Open the DMG and drag TermQ.app to your Applications folder
  4. +
  5. Right-click and select "Open" on first launch (required for unsigned apps)
  6. +
+

Alternative: Zip Archive

+
    +
  1. Download TermQ-0.2.1.zip
  2. +
  3. Unzip and move TermQ.app to your Applications folder
  4. +
+

CLI Tool

+

The termq CLI is bundled inside the app. To install it:

+
    +
  1. Open TermQ.app
  2. +
  3. Go to TermQ → Settings (or press ⌘,)
  4. +
  5. Click Install Command Line Tool
  6. +
+

Or manually:

+
sudo cp /Applications/TermQ.app/Contents/Resources/termq /usr/local/bin/
+

Checksums

+

See checksums.txt for SHA-256 checksums of all release artifacts.

+

What's Changed

+ +

Full Changelog: https://github.com/eyelock/TermQ/compare/v0.2.0...v0.2.1

]]>
+ + 14.0 +
+ + Version 0.2.0 + 0.2.0 + 0.2.0 + Fri, 09 Jan 2026 11:09:52 +0000 + Installation + +
    +
  1. Download TermQ-0.2.0.dmg
  2. +
  3. Open the DMG and drag TermQ.app to your Applications folder
  4. +
  5. Right-click and select "Open" on first launch (required for unsigned apps)
  6. +
+

Alternative: Zip Archive

+
    +
  1. Download TermQ-0.2.0.zip
  2. +
  3. Unzip and move TermQ.app to your Applications folder
  4. +
+

CLI Tool

+

The termq CLI is bundled inside the app. To install it:

+
    +
  1. Open TermQ.app
  2. +
  3. Go to TermQ → Settings (or press ⌘,)
  4. +
  5. Click Install Command Line Tool
  6. +
+

Or manually:

+
sudo cp /Applications/TermQ.app/Contents/Resources/termq /usr/local/bin/
+

Checksums

+

See checksums.txt for SHA-256 checksums of all release artifacts.

+

What's Changed

+ +

Full Changelog: https://github.com/eyelock/TermQ/compare/v0.1.0...v0.2.0

]]>
+ + 14.0 +
+ + Version 0.1.0 + 0.1.0 + 0.1.0 + Fri, 09 Jan 2026 09:40:05 +0000 + Installation + +
    +
  1. Download TermQ-0.1.0.dmg
  2. +
  3. Open the DMG and drag TermQ.app to your Applications folder
  4. +
  5. Right-click and select "Open" on first launch (required for unsigned apps)
  6. +
+

Alternative: Zip Archive

+
    +
  1. Download TermQ-0.1.0.zip
  2. +
  3. Unzip and move TermQ.app to your Applications folder
  4. +
+

CLI Tool

+

The termq CLI is bundled inside the app. To install it:

+
    +
  1. Open TermQ.app
  2. +
  3. Go to TermQ → Settings (or press ⌘,)
  4. +
  5. Click Install Command Line Tool
  6. +
+

Or manually:

+
sudo cp /Applications/TermQ.app/Contents/Resources/termq /usr/local/bin/
+

Checksums

+

See checksums.txt for SHA-256 checksums of all release artifacts.

+

What's Changed

+ +

New Contributors

+ +

Full Changelog: https://github.com/eyelock/TermQ/commits/v0.1.0

]]>
+ + 14.0 +
diff --git a/Docs/appcast.xml b/Docs/appcast.xml index af264a82..601b48a3 100644 --- a/Docs/appcast.xml +++ b/Docs/appcast.xml @@ -5,6 +5,49 @@ https://github.com/eyelock/TermQ Most recent updates to TermQ en + + Version 0.9.2 + 0.9.2 + 0.9.2 + Tue, 28 Apr 2026 22:00:42 +0000 + Bug Fixes +
    +
  • replace -50 Finder dialog with correct file/URL handling (#240)
  • +
  • re-register URL Apple Event handler after SwiftUI scene setup (#239)
  • +
  • fix uninstall for local harnesses with no YNH install record (#234)
  • +
  • fix appcast not updating on stable release (#233)
  • +
+
+

Installation

+ +
    +
  1. Download TermQ-0.9.2.dmg
  2. +
  3. Open the DMG and drag TermQ.app to your Applications folder
  4. +
  5. Double-click to launch
  6. +
+

Alternative: Zip Archive

+
    +
  1. Download TermQ-0.9.2.zip
  2. +
  3. Unzip and move TermQ.app to your Applications folder
  4. +
+

CLI Tool

+

The termqcli CLI is bundled inside the app. To install it:

+
    +
  1. Open TermQ.app
  2. +
  3. Go to TermQ → Settings (or press ⌘,)
  4. +
  5. Click Install Command Line Tool
  6. +
+

Or manually:

+
sudo cp /Applications/TermQ.app/Contents/Resources/termqcli /usr/local/bin/termqcli
+

Checksums

+

See checksums.txt for SHA-256 checksums of all release artifacts.

]]>
+ + 14.0 +
Version 0.9.1 0.9.1 @@ -587,6 +630,685 @@ type="application/octet-stream"/> 14.0 + + Version 0.6.4 + 0.6.4 + 0.6.4 + Sat, 17 Jan 2026 17:25:05 +0000 + +

[!WARNING] The 0.6.x builds are quite unstable, I learned a good lesson here about getting a bit too carried away on how easy it is to ship features with Claude. Shipping, features and fixes that on the surface were working, but when you started trying to use TermQ as your base tool showed up as not quite ready yet. Feel free to try the 0.7.0 beta releases which are focussed on stabilising the app's current features!

+ +

Installation

+ +
    +
  1. Download TermQ-0.6.4.dmg
  2. +
  3. Open the DMG and drag TermQ.app to your Applications folder
  4. +
  5. Double-click to launch
  6. +
+

Alternative: Zip Archive

+
    +
  1. Download TermQ-0.6.4.zip
  2. +
  3. Unzip and move TermQ.app to your Applications folder
  4. +
+

CLI Tool

+

The termqcli CLI is bundled inside the app. To install it:

+
    +
  1. Open TermQ.app
  2. +
  3. Go to TermQ → Settings (or press ⌘,)
  4. +
  5. Click Install Command Line Tool
  6. +
+

Or manually:

+
sudo cp /Applications/TermQ.app/Contents/Resources/termqcli /usr/local/bin/termqcli
+

Checksums

+

See checksums.txt for SHA-256 checksums of all release artifacts.

+

Full Changelog: https://github.com/eyelock/TermQ/compare/v0.6.3...v0.6.4

]]>
+ + 14.0 +
+ + Version 0.6.3 + 0.6.3 + 0.6.3 + Sat, 17 Jan 2026 16:01:40 +0000 + Installation + +
    +
  1. Download TermQ-0.6.3.dmg
  2. +
  3. Open the DMG and drag TermQ.app to your Applications folder
  4. +
  5. Double-click to launch
  6. +
+

Alternative: Zip Archive

+
    +
  1. Download TermQ-0.6.3.zip
  2. +
  3. Unzip and move TermQ.app to your Applications folder
  4. +
+

CLI Tool

+

The termqcli CLI is bundled inside the app. To install it:

+
    +
  1. Open TermQ.app
  2. +
  3. Go to TermQ → Settings (or press ⌘,)
  4. +
  5. Click Install Command Line Tool
  6. +
+

Or manually:

+
sudo cp /Applications/TermQ.app/Contents/Resources/termqcli /usr/local/bin/termqcli
+

Checksums

+

See checksums.txt for SHA-256 checksums of all release artifacts.

+

Full Changelog: https://github.com/eyelock/TermQ/compare/v0.6.2...v0.6.3

]]>
+ + 14.0 +
+ + Version 0.6.2 + 0.6.2 + 0.6.2 + Sat, 17 Jan 2026 08:59:59 +0000 + Installation + +
    +
  1. Download TermQ-0.6.2.dmg
  2. +
  3. Open the DMG and drag TermQ.app to your Applications folder
  4. +
  5. Double-click to launch
  6. +
+

Alternative: Zip Archive

+
    +
  1. Download TermQ-0.6.2.zip
  2. +
  3. Unzip and move TermQ.app to your Applications folder
  4. +
+

CLI Tool

+

The termqcli CLI is bundled inside the app. To install it:

+
    +
  1. Open TermQ.app
  2. +
  3. Go to TermQ → Settings (or press ⌘,)
  4. +
  5. Click Install Command Line Tool
  6. +
+

Or manually:

+
sudo cp /Applications/TermQ.app/Contents/Resources/termqcli /usr/local/bin/termqcli
+

Checksums

+

See checksums.txt for SHA-256 checksums of all release artifacts.

+

Full Changelog: https://github.com/eyelock/TermQ/compare/v0.6.1...v0.6.2

]]>
+ + 14.0 +
+ + Version 0.6.1 + 0.6.1 + 0.6.1 + Fri, 16 Jan 2026 14:16:26 +0000 + Installation + +
    +
  1. Download TermQ-0.6.1.dmg
  2. +
  3. Open the DMG and drag TermQ.app to your Applications folder
  4. +
  5. Right-click and select "Open" on first launch (required for unsigned apps)
  6. +
+

Alternative: Zip Archive

+
    +
  1. Download TermQ-0.6.1.zip
  2. +
  3. Unzip and move TermQ.app to your Applications folder
  4. +
+

CLI Tool

+

The termqcli CLI is bundled inside the app. To install it:

+
    +
  1. Open TermQ.app
  2. +
  3. Go to TermQ → Settings (or press ⌘,)
  4. +
  5. Click Install Command Line Tool
  6. +
+

Or manually:

+
sudo cp /Applications/TermQ.app/Contents/Resources/termqcli /usr/local/bin/termqcli
+

Checksums

+

See checksums.txt for SHA-256 checksums of all release artifacts.

+

What's Changed

+ +

Full Changelog: https://github.com/eyelock/TermQ/compare/v0.6.0...v0.6.1

]]>
+ + 14.0 +
+ + Version 0.6.0 + 0.6.0 + 0.6.0 + Fri, 16 Jan 2026 09:18:53 +0000 + Installation + +
    +
  1. Download TermQ-0.6.0.dmg
  2. +
  3. Open the DMG and drag TermQ.app to your Applications folder
  4. +
  5. Right-click and select "Open" on first launch (required for unsigned apps)
  6. +
+

Alternative: Zip Archive

+
    +
  1. Download TermQ-0.6.0.zip
  2. +
  3. Unzip and move TermQ.app to your Applications folder
  4. +
+

CLI Tool

+

The termqcli CLI is bundled inside the app. To install it:

+
    +
  1. Open TermQ.app
  2. +
  3. Go to TermQ → Settings (or press ⌘,)
  4. +
  5. Click Install Command Line Tool
  6. +
+

Or manually:

+
sudo cp /Applications/TermQ.app/Contents/Resources/termqcli /usr/local/bin/termqcli
+

Checksums

+

See checksums.txt for SHA-256 checksums of all release artifacts.

+

What's Changed

+ +

Full Changelog: https://github.com/eyelock/TermQ/compare/v0.5.2...v0.6.0

]]>
+ + 14.0 +
+ + Version 0.5.2 + 0.5.2 + 0.5.2 + Wed, 14 Jan 2026 16:08:13 +0000 + Installation + +
    +
  1. Download TermQ-0.5.2.dmg
  2. +
  3. Open the DMG and drag TermQ.app to your Applications folder
  4. +
  5. Right-click and select "Open" on first launch (required for unsigned apps)
  6. +
+

Alternative: Zip Archive

+
    +
  1. Download TermQ-0.5.2.zip
  2. +
  3. Unzip and move TermQ.app to your Applications folder
  4. +
+

CLI Tool

+

The termq CLI is bundled inside the app. To install it:

+
    +
  1. Open TermQ.app
  2. +
  3. Go to TermQ → Settings (or press ⌘,)
  4. +
  5. Click Install Command Line Tool
  6. +
+

Or manually:

+
sudo cp /Applications/TermQ.app/Contents/Resources/termq /usr/local/bin/
+

Checksums

+

See checksums.txt for SHA-256 checksums of all release artifacts.

+

What's Changed

+ +

Full Changelog: https://github.com/eyelock/TermQ/compare/v0.5.1...v0.5.2

]]>
+ + 14.0 +
+ + Version 0.5.1 + 0.5.1 + 0.5.1 + Wed, 14 Jan 2026 15:19:45 +0000 + Installation + +
    +
  1. Download TermQ-0.5.1.dmg
  2. +
  3. Open the DMG and drag TermQ.app to your Applications folder
  4. +
  5. Right-click and select "Open" on first launch (required for unsigned apps)
  6. +
+

Alternative: Zip Archive

+
    +
  1. Download TermQ-0.5.1.zip
  2. +
  3. Unzip and move TermQ.app to your Applications folder
  4. +
+

CLI Tool

+

The termq CLI is bundled inside the app. To install it:

+
    +
  1. Open TermQ.app
  2. +
  3. Go to TermQ → Settings (or press ⌘,)
  4. +
  5. Click Install Command Line Tool
  6. +
+

Or manually:

+
sudo cp /Applications/TermQ.app/Contents/Resources/termq /usr/local/bin/
+

Checksums

+

See checksums.txt for SHA-256 checksums of all release artifacts.

+

What's Changed

+ +

Full Changelog: https://github.com/eyelock/TermQ/compare/v0.5.0...v0.5.1

]]>
+ + 14.0 +
+ + Version 0.5.0 + 0.5.0 + 0.5.0 + Wed, 14 Jan 2026 07:50:45 +0000 + Installation + +
    +
  1. Download TermQ-0.5.0.dmg
  2. +
  3. Open the DMG and drag TermQ.app to your Applications folder
  4. +
  5. Right-click and select "Open" on first launch (required for unsigned apps)
  6. +
+

Alternative: Zip Archive

+
    +
  1. Download TermQ-0.5.0.zip
  2. +
  3. Unzip and move TermQ.app to your Applications folder
  4. +
+

CLI Tool

+

The termq CLI is bundled inside the app. To install it:

+
    +
  1. Open TermQ.app
  2. +
  3. Go to TermQ → Settings (or press ⌘,)
  4. +
  5. Click Install Command Line Tool
  6. +
+

Or manually:

+
sudo cp /Applications/TermQ.app/Contents/Resources/termq /usr/local/bin/
+

Checksums

+

See checksums.txt for SHA-256 checksums of all release artifacts.

+

What's Changed

+ +

Full Changelog: https://github.com/eyelock/TermQ/compare/v0.4.4...v0.5.0

]]>
+ + 14.0 +
+ + Version 0.4.4 + 0.4.4 + 0.4.4 + Tue, 13 Jan 2026 20:45:53 +0000 + Installation + +
    +
  1. Download TermQ-0.4.4.dmg
  2. +
  3. Open the DMG and drag TermQ.app to your Applications folder
  4. +
  5. Right-click and select "Open" on first launch (required for unsigned apps)
  6. +
+

Alternative: Zip Archive

+
    +
  1. Download TermQ-0.4.4.zip
  2. +
  3. Unzip and move TermQ.app to your Applications folder
  4. +
+

CLI Tool

+

The termq CLI is bundled inside the app. To install it:

+
    +
  1. Open TermQ.app
  2. +
  3. Go to TermQ → Settings (or press ⌘,)
  4. +
  5. Click Install Command Line Tool
  6. +
+

Or manually:

+
sudo cp /Applications/TermQ.app/Contents/Resources/termq /usr/local/bin/
+

Checksums

+

See checksums.txt for SHA-256 checksums of all release artifacts.

+

What's Changed

+ +

Full Changelog: https://github.com/eyelock/TermQ/compare/v0.4.3...v0.4.4

]]>
+ + 14.0 +
+ + Version 0.4.3 + 0.4.3 + 0.4.3 + Tue, 13 Jan 2026 16:18:17 +0000 + Installation + +
    +
  1. Download TermQ-0.4.3.dmg
  2. +
  3. Open the DMG and drag TermQ.app to your Applications folder
  4. +
  5. Right-click and select "Open" on first launch (required for unsigned apps)
  6. +
+

Alternative: Zip Archive

+
    +
  1. Download TermQ-0.4.3.zip
  2. +
  3. Unzip and move TermQ.app to your Applications folder
  4. +
+

CLI Tool

+

The termq CLI is bundled inside the app. To install it:

+
    +
  1. Open TermQ.app
  2. +
  3. Go to TermQ → Settings (or press ⌘,)
  4. +
  5. Click Install Command Line Tool
  6. +
+

Or manually:

+
sudo cp /Applications/TermQ.app/Contents/Resources/termq /usr/local/bin/
+

Checksums

+

See checksums.txt for SHA-256 checksums of all release artifacts.

+

What's Changed

+ +

Full Changelog: https://github.com/eyelock/TermQ/compare/v0.4.2...v0.4.3

]]>
+ + 14.0 +
+ + Version 0.4.2 + 0.4.2 + 0.4.2 + Mon, 12 Jan 2026 20:33:57 +0000 + Installation + +
    +
  1. Download TermQ-0.4.2.dmg
  2. +
  3. Open the DMG and drag TermQ.app to your Applications folder
  4. +
  5. Right-click and select "Open" on first launch (required for unsigned apps)
  6. +
+

Alternative: Zip Archive

+
    +
  1. Download TermQ-0.4.2.zip
  2. +
  3. Unzip and move TermQ.app to your Applications folder
  4. +
+

CLI Tool

+

The termq CLI is bundled inside the app. To install it:

+
    +
  1. Open TermQ.app
  2. +
  3. Go to TermQ → Settings (or press ⌘,)
  4. +
  5. Click Install Command Line Tool
  6. +
+

Or manually:

+
sudo cp /Applications/TermQ.app/Contents/Resources/termq /usr/local/bin/
+

Checksums

+

See checksums.txt for SHA-256 checksums of all release artifacts.

+

What's Changed

+ +

Full Changelog: https://github.com/eyelock/TermQ/compare/v0.4.1...v0.4.2

]]>
+ + 14.0 +
+ + Version 0.4.1 + 0.4.1 + 0.4.1 + Mon, 12 Jan 2026 20:17:38 +0000 + Installation + +
    +
  1. Download TermQ-0.4.1.dmg
  2. +
  3. Open the DMG and drag TermQ.app to your Applications folder
  4. +
  5. Right-click and select "Open" on first launch (required for unsigned apps)
  6. +
+

Alternative: Zip Archive

+
    +
  1. Download TermQ-0.4.1.zip
  2. +
  3. Unzip and move TermQ.app to your Applications folder
  4. +
+

CLI Tool

+

The termq CLI is bundled inside the app. To install it:

+
    +
  1. Open TermQ.app
  2. +
  3. Go to TermQ → Settings (or press ⌘,)
  4. +
  5. Click Install Command Line Tool
  6. +
+

Or manually:

+
sudo cp /Applications/TermQ.app/Contents/Resources/termq /usr/local/bin/
+

Checksums

+

See checksums.txt for SHA-256 checksums of all release artifacts.

+

What's Changed

+ +

Full Changelog: https://github.com/eyelock/TermQ/compare/v0.4.0...v0.4.1

]]>
+ + 14.0 +
+ + Version 0.4.0 + 0.4.0 + 0.4.0 + Sun, 11 Jan 2026 22:00:56 +0000 + Full Changelog: https://github.com/eyelock/TermQ/compare/v0.3.1...v0.4.0

]]>
+ + 14.0 +
+ + Version 0.3.0 + 0.3.0 + 0.3.0 + Fri, 09 Jan 2026 14:01:41 +0000 + Installation + +
    +
  1. Download TermQ-0.3.0.dmg
  2. +
  3. Open the DMG and drag TermQ.app to your Applications folder
  4. +
  5. Right-click and select "Open" on first launch (required for unsigned apps)
  6. +
+

Alternative: Zip Archive

+
    +
  1. Download TermQ-0.3.0.zip
  2. +
  3. Unzip and move TermQ.app to your Applications folder
  4. +
+

CLI Tool

+

The termq CLI is bundled inside the app. To install it:

+
    +
  1. Open TermQ.app
  2. +
  3. Go to TermQ → Settings (or press ⌘,)
  4. +
  5. Click Install Command Line Tool
  6. +
+

Or manually:

+
sudo cp /Applications/TermQ.app/Contents/Resources/termq /usr/local/bin/
+

Checksums

+

See checksums.txt for SHA-256 checksums of all release artifacts.

+

What's Changed

+ +

Full Changelog: https://github.com/eyelock/TermQ/compare/v0.2.1...v0.3.0

]]>
+ + 14.0 +
+ + Version 0.2.1 + 0.2.1 + 0.2.1 + Fri, 09 Jan 2026 11:23:00 +0000 + Installation + +
    +
  1. Download TermQ-0.2.1.dmg
  2. +
  3. Open the DMG and drag TermQ.app to your Applications folder
  4. +
  5. Right-click and select "Open" on first launch (required for unsigned apps)
  6. +
+

Alternative: Zip Archive

+
    +
  1. Download TermQ-0.2.1.zip
  2. +
  3. Unzip and move TermQ.app to your Applications folder
  4. +
+

CLI Tool

+

The termq CLI is bundled inside the app. To install it:

+
    +
  1. Open TermQ.app
  2. +
  3. Go to TermQ → Settings (or press ⌘,)
  4. +
  5. Click Install Command Line Tool
  6. +
+

Or manually:

+
sudo cp /Applications/TermQ.app/Contents/Resources/termq /usr/local/bin/
+

Checksums

+

See checksums.txt for SHA-256 checksums of all release artifacts.

+

What's Changed

+ +

Full Changelog: https://github.com/eyelock/TermQ/compare/v0.2.0...v0.2.1

]]>
+ + 14.0 +
+ + Version 0.2.0 + 0.2.0 + 0.2.0 + Fri, 09 Jan 2026 11:09:52 +0000 + Installation + +
    +
  1. Download TermQ-0.2.0.dmg
  2. +
  3. Open the DMG and drag TermQ.app to your Applications folder
  4. +
  5. Right-click and select "Open" on first launch (required for unsigned apps)
  6. +
+

Alternative: Zip Archive

+
    +
  1. Download TermQ-0.2.0.zip
  2. +
  3. Unzip and move TermQ.app to your Applications folder
  4. +
+

CLI Tool

+

The termq CLI is bundled inside the app. To install it:

+
    +
  1. Open TermQ.app
  2. +
  3. Go to TermQ → Settings (or press ⌘,)
  4. +
  5. Click Install Command Line Tool
  6. +
+

Or manually:

+
sudo cp /Applications/TermQ.app/Contents/Resources/termq /usr/local/bin/
+

Checksums

+

See checksums.txt for SHA-256 checksums of all release artifacts.

+

What's Changed

+ +

Full Changelog: https://github.com/eyelock/TermQ/compare/v0.1.0...v0.2.0

]]>
+ + 14.0 +
+ + Version 0.1.0 + 0.1.0 + 0.1.0 + Fri, 09 Jan 2026 09:40:05 +0000 + Installation + +
    +
  1. Download TermQ-0.1.0.dmg
  2. +
  3. Open the DMG and drag TermQ.app to your Applications folder
  4. +
  5. Right-click and select "Open" on first launch (required for unsigned apps)
  6. +
+

Alternative: Zip Archive

+
    +
  1. Download TermQ-0.1.0.zip
  2. +
  3. Unzip and move TermQ.app to your Applications folder
  4. +
+

CLI Tool

+

The termq CLI is bundled inside the app. To install it:

+
    +
  1. Open TermQ.app
  2. +
  3. Go to TermQ → Settings (or press ⌘,)
  4. +
  5. Click Install Command Line Tool
  6. +
+

Or manually:

+
sudo cp /Applications/TermQ.app/Contents/Resources/termq /usr/local/bin/
+

Checksums

+

See checksums.txt for SHA-256 checksums of all release artifacts.

+

What's Changed

+ +

New Contributors

+ +

Full Changelog: https://github.com/eyelock/TermQ/commits/v0.1.0

]]>
+ + 14.0 +
From 1e358ece05302073f22ec36f13b91619025a2d23 Mon Sep 17 00:00:00 2001 From: David Collie Date: Wed, 29 Apr 2026 06:14:55 +0100 Subject: [PATCH 10/29] 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 --- Sources/TermQ/Services/TerminalLinkResolver.swift | 4 ++-- Sources/TermQ/Services/TmuxControlModeSession.swift | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Sources/TermQ/Services/TerminalLinkResolver.swift b/Sources/TermQ/Services/TerminalLinkResolver.swift index b2321eab..de2fdf06 100644 --- a/Sources/TermQ/Services/TerminalLinkResolver.swift +++ b/Sources/TermQ/Services/TerminalLinkResolver.swift @@ -127,13 +127,13 @@ enum TermQTerminalLink { let configuration = NSWorkspace.OpenConfiguration() NSWorkspace.shared.open([url], withApplicationAt: handler, configuration: configuration) { - _, error in + @Sendable [url] _, error in guard let error else { return } Task { @MainActor in TermQLogger.ui.warning( "TermQTerminalLink launch failed path=\(url.path) error=\(error.localizedDescription)" ) - presentOpenFailedAlert(for: url, error: error) + Self.presentOpenFailedAlert(for: url, error: error) } } } diff --git a/Sources/TermQ/Services/TmuxControlModeSession.swift b/Sources/TermQ/Services/TmuxControlModeSession.swift index 59886f7c..92c78433 100644 --- a/Sources/TermQ/Services/TmuxControlModeSession.swift +++ b/Sources/TermQ/Services/TmuxControlModeSession.swift @@ -68,8 +68,10 @@ public class TmuxControlModeSession: ObservableObject { proc.standardInput = input proc.standardError = Pipe() // Suppress stderr - // Handle output asynchronously - output.fileHandleForReading.readabilityHandler = { [weak self] handle in + // Handle output asynchronously. + // @Sendable breaks @MainActor isolation inheritance — FileHandle calls this + // on a background queue, not the main actor. + output.fileHandleForReading.readabilityHandler = { @Sendable [weak self] handle in let data = handle.availableData guard !data.isEmpty else { From a2fe0a4b5767b8c8b3d306f2a4699404ef29e0be Mon Sep 17 00:00:00 2001 From: David Collie Date: Wed, 29 Apr 2026 06:15:17 +0100 Subject: [PATCH 11/29] chore: update CHANGELOG for v0.9.3 Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a718cd7d..2e9d0de3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.9.3] — 2026-04-29 + +### Fixed + +- Fix EXC_BREAKPOINT crash caused by @MainActor isolation inherited by system-dispatched closures in TerminalLinkResolver and TmuxControlModeSession + ## [0.9.2] — 2026-04-28 ### Fixed From d9e3845b339893acee18b69653841a311b9dae0a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 29 Apr 2026 05:58:06 +0000 Subject: [PATCH 12/29] chore: Update appcast for release v0.9.3 Auto-generated appcast files for Sparkle auto-updates. Co-Authored-By: github-actions[bot] --- Docs/appcast-beta.xml | 43 +++++++++++++++++++++++++++++++++++++++++++ Docs/appcast.xml | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+) diff --git a/Docs/appcast-beta.xml b/Docs/appcast-beta.xml index d830a539..09fb6666 100644 --- a/Docs/appcast-beta.xml +++ b/Docs/appcast-beta.xml @@ -5,6 +5,49 @@ https://github.com/eyelock/TermQ Most recent updates to TermQ en + + Version 0.9.3 + 0.9.3 + 0.9.3 + Wed, 29 Apr 2026 05:57:15 +0000 + Bug Fixes +
    +
  • replace -50 Finder dialog with correct file/URL handling (#240)
  • +
  • re-register URL Apple Event handler after SwiftUI scene setup (#239)
  • +
  • fix uninstall for local harnesses with no YNH install record (#234)
  • +
  • fix appcast not updating on stable release (#233)
  • +
+
+

Installation

+ +
    +
  1. Download TermQ-0.9.3.dmg
  2. +
  3. Open the DMG and drag TermQ.app to your Applications folder
  4. +
  5. Double-click to launch
  6. +
+

Alternative: Zip Archive

+
    +
  1. Download TermQ-0.9.3.zip
  2. +
  3. Unzip and move TermQ.app to your Applications folder
  4. +
+

CLI Tool

+

The termqcli CLI is bundled inside the app. To install it:

+
    +
  1. Open TermQ.app
  2. +
  3. Go to TermQ → Settings (or press ⌘,)
  4. +
  5. Click Install Command Line Tool
  6. +
+

Or manually:

+
sudo cp /Applications/TermQ.app/Contents/Resources/termqcli /usr/local/bin/termqcli
+

Checksums

+

See checksums.txt for SHA-256 checksums of all release artifacts.

]]>
+ + 14.0 +
Version 0.9.2 0.9.2 diff --git a/Docs/appcast.xml b/Docs/appcast.xml index 601b48a3..fb3285a2 100644 --- a/Docs/appcast.xml +++ b/Docs/appcast.xml @@ -5,6 +5,49 @@ https://github.com/eyelock/TermQ Most recent updates to TermQ en + + Version 0.9.3 + 0.9.3 + 0.9.3 + Wed, 29 Apr 2026 05:57:15 +0000 + Bug Fixes +
    +
  • replace -50 Finder dialog with correct file/URL handling (#240)
  • +
  • re-register URL Apple Event handler after SwiftUI scene setup (#239)
  • +
  • fix uninstall for local harnesses with no YNH install record (#234)
  • +
  • fix appcast not updating on stable release (#233)
  • +
+
+

Installation

+ +
    +
  1. Download TermQ-0.9.3.dmg
  2. +
  3. Open the DMG and drag TermQ.app to your Applications folder
  4. +
  5. Double-click to launch
  6. +
+

Alternative: Zip Archive

+
    +
  1. Download TermQ-0.9.3.zip
  2. +
  3. Unzip and move TermQ.app to your Applications folder
  4. +
+

CLI Tool

+

The termqcli CLI is bundled inside the app. To install it:

+
    +
  1. Open TermQ.app
  2. +
  3. Go to TermQ → Settings (or press ⌘,)
  4. +
  5. Click Install Command Line Tool
  6. +
+

Or manually:

+
sudo cp /Applications/TermQ.app/Contents/Resources/termqcli /usr/local/bin/termqcli
+

Checksums

+

See checksums.txt for SHA-256 checksums of all release artifacts.

]]>
+ + 14.0 +
Version 0.9.2 0.9.2 From 3d2b6bd7f84462e8fb60b9b54c7e3814d93b041b Mon Sep 17 00:00:00 2001 From: David Collie Date: Sat, 2 May 2026 21:27:11 +0100 Subject: [PATCH 13/29] fix(harness): tolerate name/id mismatch when resolving selected harness (#252) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `Harness.id` is `"namespace/name"` for namespaced installs but `YNHPersistence` keys associations by bare `name`. The Launch 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 --- CHANGELOG.md | 15 +++++++++++++++ Sources/TermQ/Services/HarnessRepository.swift | 18 ++++++++++++------ 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e9d0de3..db9cd439 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.9.4] — 2026-05-02 + +### Fixed + +- **Launch-from-worktree blank sheet** — Launch from a worktree + row (or a repo's default-harness context menu) now resolves the harness + correctly when the install is namespaced. `HarnessRepository` previously + matched `selectedHarnessName` against `Harness.id` only — for namespaced + installs `id` is `"namespace/name"` while `YNHPersistence` keys + associations by bare `name`, so the lookup missed and the launch sheet + rendered with no content (a blank rounded sheet that never populated and + could only be dismissed with Esc). Lookup is now tolerant of either + form, and the stale-selection eviction in `refresh()` matches the same + rule. + ## [0.9.3] — 2026-04-29 ### Fixed diff --git a/Sources/TermQ/Services/HarnessRepository.swift b/Sources/TermQ/Services/HarnessRepository.swift index 7eb32230..8226fb0e 100644 --- a/Sources/TermQ/Services/HarnessRepository.swift +++ b/Sources/TermQ/Services/HarnessRepository.swift @@ -30,10 +30,14 @@ final class HarnessRepository: ObservableObject { private let ynhDetector: any YNHDetectorProtocol - /// The currently selected harness, matched by `Harness.id`. + /// The currently selected harness. Matches by `Harness.id` first, falling + /// back to `Harness.name`. The fallback covers callers that pass a bare + /// name (e.g. values stored by `YNHPersistence`, which keys associations + /// by `name` not `id`) — for namespaced installs `id` is `"namespace/name"` + /// and a name-only key would otherwise miss. var selectedHarness: Harness? { - guard let id = selectedHarnessName else { return nil } - return harnesses.first { $0.id == id } + guard let key = selectedHarnessName else { return nil } + return harnesses.first { $0.id == key } ?? harnesses.first { $0.name == key } } private convenience init() { @@ -70,9 +74,11 @@ final class HarnessRepository: ObservableObject { let decoded = try JSONDecoder().decode([Harness].self, from: Data(json.utf8)) harnesses = decoded - // Clear selection if the selected harness was removed. - if let id = selectedHarnessName, - !decoded.contains(where: { $0.id == id }) + // Clear selection if the selected harness was removed. Match by + // `id` or `name` to mirror `selectedHarness` — `selectedHarnessName` + // can hold either form depending on the caller. + if let key = selectedHarnessName, + !decoded.contains(where: { $0.id == key || $0.name == key }) { selectedHarnessName = nil } From deb4a2f6d4f2d9240217d00020e5e51d85b05370 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 2 May 2026 21:26:46 +0000 Subject: [PATCH 14/29] chore: Update appcast for release v0.9.4 Auto-generated appcast files for Sparkle auto-updates. Co-Authored-By: github-actions[bot] --- Docs/appcast-beta.xml | 50 +++++++++++++++++++++++++++++++++++++++++++ Docs/appcast.xml | 50 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+) diff --git a/Docs/appcast-beta.xml b/Docs/appcast-beta.xml index 09fb6666..5bfa46b1 100644 --- a/Docs/appcast-beta.xml +++ b/Docs/appcast-beta.xml @@ -5,6 +5,56 @@ https://github.com/eyelock/TermQ Most recent updates to TermQ en + + Version 0.9.4 + 0.9.4 + 0.9.4 + Sat, 02 May 2026 21:25:50 +0000 + What's New +
    +
  • per-row delegate Edit and Remove in detail pane (#251)
  • +
  • inline include editing & manifest editor in detail pane (#250)
  • +
  • Phase 1 — management foundation, fork & duplicate, drift detection (#249)
  • +
+

Bug Fixes

+
    +
  • tolerate name/id mismatch when resolving selected harness (#252)
  • +
  • replace -50 Finder dialog with correct file/URL handling (#240)
  • +
  • re-register URL Apple Event handler after SwiftUI scene setup (#239)
  • +
  • fix uninstall for local harnesses with no YNH install record (#234)
  • +
  • fix appcast not updating on stable release (#233)
  • +
+
+

Installation

+ +
    +
  1. Download TermQ-0.9.4.dmg
  2. +
  3. Open the DMG and drag TermQ.app to your Applications folder
  4. +
  5. Double-click to launch
  6. +
+

Alternative: Zip Archive

+
    +
  1. Download TermQ-0.9.4.zip
  2. +
  3. Unzip and move TermQ.app to your Applications folder
  4. +
+

CLI Tool

+

The termqcli CLI is bundled inside the app. To install it:

+
    +
  1. Open TermQ.app
  2. +
  3. Go to TermQ → Settings (or press ⌘,)
  4. +
  5. Click Install Command Line Tool
  6. +
+

Or manually:

+
sudo cp /Applications/TermQ.app/Contents/Resources/termqcli /usr/local/bin/termqcli
+

Checksums

+

See checksums.txt for SHA-256 checksums of all release artifacts.

]]>
+ + 14.0 +
Version 0.9.3 0.9.3 diff --git a/Docs/appcast.xml b/Docs/appcast.xml index fb3285a2..21acbbc0 100644 --- a/Docs/appcast.xml +++ b/Docs/appcast.xml @@ -5,6 +5,56 @@ https://github.com/eyelock/TermQ Most recent updates to TermQ en + + Version 0.9.4 + 0.9.4 + 0.9.4 + Sat, 02 May 2026 21:25:50 +0000 + What's New +
    +
  • per-row delegate Edit and Remove in detail pane (#251)
  • +
  • inline include editing & manifest editor in detail pane (#250)
  • +
  • Phase 1 — management foundation, fork & duplicate, drift detection (#249)
  • +
+

Bug Fixes

+
    +
  • tolerate name/id mismatch when resolving selected harness (#252)
  • +
  • replace -50 Finder dialog with correct file/URL handling (#240)
  • +
  • re-register URL Apple Event handler after SwiftUI scene setup (#239)
  • +
  • fix uninstall for local harnesses with no YNH install record (#234)
  • +
  • fix appcast not updating on stable release (#233)
  • +
+
+

Installation

+ +
    +
  1. Download TermQ-0.9.4.dmg
  2. +
  3. Open the DMG and drag TermQ.app to your Applications folder
  4. +
  5. Double-click to launch
  6. +
+

Alternative: Zip Archive

+
    +
  1. Download TermQ-0.9.4.zip
  2. +
  3. Unzip and move TermQ.app to your Applications folder
  4. +
+

CLI Tool

+

The termqcli CLI is bundled inside the app. To install it:

+
    +
  1. Open TermQ.app
  2. +
  3. Go to TermQ → Settings (or press ⌘,)
  4. +
  5. Click Install Command Line Tool
  6. +
+

Or manually:

+
sudo cp /Applications/TermQ.app/Contents/Resources/termqcli /usr/local/bin/termqcli
+

Checksums

+

See checksums.txt for SHA-256 checksums of all release artifacts.

]]>
+ + 14.0 +
Version 0.9.3 0.9.3 From f399ab7c3d740216e12384b3337dd6ef53c7eedb Mon Sep 17 00:00:00 2001 From: David Collie Date: Sun, 3 May 2026 11:04:34 +0100 Subject: [PATCH 15/29] fix(harness): adapt to ynh 0.3 JSON envelope and version_installed rename 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 --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: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 24 ++++++++++++++++ .../TermQ/Services/HarnessRepository.swift | 18 ++++++++++-- Sources/TermQShared/Harness.swift | 3 +- Sources/TermQShared/HarnessInfo.swift | 3 +- .../TermQSharedTests/HarnessModelTests.swift | 28 +++++++++---------- Tests/TermQTests/YNHDecodingTests.swift | 6 ++-- 6 files changed, 61 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index db9cd439..1b2d36d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.9.5] — 2026-05-03 + +### Fixed + +- **Harness list and detail never load (ynh 0.3+ JSON shape change)** — + Three breaking changes in ynh 0.3.0's structured-output format + combined to leave TermQ with an empty harness list and unreadable + detail responses: + 1. `ynh ls --format json` switched from emitting a bare harness + array to an envelope object `{capabilities, harnesses, + ynh_version}`. + 2. `ynh info --format json` likewise switched to an envelope + `{capabilities, harness, ynh_version}`. + 3. The `version` field on each harness row was renamed to + `version_installed` in both `ynh ls` and `ynh info` payloads. + The combined effect on v0.9.4 was every dependent surface — the + Harnesses sidebar tab, harness detail view, the launch sheet + (rendered as a blank rounded pill or did nothing at all when invoked + from a worktree row), install/uninstall/update flows — silently + no-op'd. Decoders updated to the envelope shapes and the + `version_installed` JSON key. The blank-sheet symptom reported + against v0.9.4 was a downstream consequence; the v0.9.4 identifier + fallback remains in place. + ## [0.9.4] — 2026-05-02 ### Fixed diff --git a/Sources/TermQ/Services/HarnessRepository.swift b/Sources/TermQ/Services/HarnessRepository.swift index 8226fb0e..aa4bdf52 100644 --- a/Sources/TermQ/Services/HarnessRepository.swift +++ b/Sources/TermQ/Services/HarnessRepository.swift @@ -5,6 +5,18 @@ private enum HarnessDetailError: Error { case missingYnd } +/// Envelope shape returned by `ynh ls --format json` (ynh 0.3+). +/// Wraps the harness list alongside capability and version metadata. +private struct YNHListEnvelope: Decodable { + let harnesses: [Harness] +} + +/// Envelope shape returned by `ynh info --format json` (ynh 0.3+). +/// Wraps a single harness payload alongside capability and version metadata. +private struct YNHInfoEnvelope: Decodable { + let harness: HarnessInfo +} + /// Repository for querying installed harnesses via `ynh ls --format json` /// and fetching full detail via `ynh info` + `ynd compose`. /// @@ -71,7 +83,8 @@ final class HarnessRepository: ObservableObject { args: ["ls", "--format", "json"], environment: env ) - let decoded = try JSONDecoder().decode([Harness].self, from: Data(json.utf8)) + let envelope = try JSONDecoder().decode(YNHListEnvelope.self, from: Data(json.utf8)) + let decoded = envelope.harnesses harnesses = decoded // Clear selection if the selected harness was removed. Match by @@ -154,7 +167,8 @@ final class HarnessRepository: ObservableObject { args: ["info", name, "--format", "json"], environment: env ) - let info = try JSONDecoder().decode(HarnessInfo.self, from: Data(infoJSON.utf8)) + let infoEnvelope = try JSONDecoder().decode(YNHInfoEnvelope.self, from: Data(infoJSON.utf8)) + let info = infoEnvelope.harness guard let yndBinary = yndPath else { throw HarnessDetailError.missingYnd diff --git a/Sources/TermQShared/Harness.swift b/Sources/TermQShared/Harness.swift index 330dce4b..09c8d8d5 100644 --- a/Sources/TermQShared/Harness.swift +++ b/Sources/TermQShared/Harness.swift @@ -27,7 +27,8 @@ public struct Harness: Codable, Equatable, Sendable, Identifiable { public let delegatesTo: [HarnessDelegate] enum CodingKeys: String, CodingKey { - case name, version, description, path, artifacts, includes, namespace + case name, description, path, artifacts, includes, namespace + case version = "version_installed" case defaultVendor = "default_vendor" case installedFrom = "installed_from" case delegatesTo = "delegates_to" diff --git a/Sources/TermQShared/HarnessInfo.swift b/Sources/TermQShared/HarnessInfo.swift index 741360fb..59ee9a23 100644 --- a/Sources/TermQShared/HarnessInfo.swift +++ b/Sources/TermQShared/HarnessInfo.swift @@ -19,7 +19,8 @@ public struct HarnessInfo: Codable, Sendable { public let manifest: JSONFragment? enum CodingKeys: String, CodingKey { - case name, version, description, path, manifest + case name, description, path, manifest + case version = "version_installed" case defaultVendor = "default_vendor" case installedFrom = "installed_from" } diff --git a/Tests/TermQSharedTests/HarnessModelTests.swift b/Tests/TermQSharedTests/HarnessModelTests.swift index 0d9169fc..42a43547 100644 --- a/Tests/TermQSharedTests/HarnessModelTests.swift +++ b/Tests/TermQSharedTests/HarnessModelTests.swift @@ -120,7 +120,7 @@ final class HarnessModelTests: XCTestCase { let json = """ { "name": "my-harness", - "version": "1.2.3", + "version_installed": "1.2.3", "description": "A test harness", "default_vendor": "claude", "path": "/home/user/.ynh/harnesses/my-harness", @@ -145,7 +145,7 @@ final class HarnessModelTests: XCTestCase { let json = """ { "name": "test", - "version": "0.1", + "version_installed": "0.1", "default_vendor": "claude", "path": "/path", "installed_from": { @@ -169,7 +169,7 @@ final class HarnessModelTests: XCTestCase { let json = """ { "name": "test", - "version": "0.1", + "version_installed": "0.1", "default_vendor": "claude", "path": "/path", "artifacts": {"skills": 0, "agents": 0, "rules": 0, "commands": 0}, @@ -187,7 +187,7 @@ final class HarnessModelTests: XCTestCase { let json = """ { "name": "x", - "version": "1", + "version_installed": "1", "default_vendor": "c", "path": "/p", "artifacts": {"skills": 0, "agents": 0, "rules": 0, "commands": 0}, @@ -208,7 +208,7 @@ final class HarnessModelTests: XCTestCase { let json = """ { "name": "github-tester", - "version": "0.1.0", + "version_installed": "0.1.0", "default_vendor": "claude", "path": "/Users/dev/harnesses/github-tester", "installed_from": null, @@ -227,7 +227,7 @@ final class HarnessModelTests: XCTestCase { let json = """ { "name": "assistants-dev", - "version": "0.1.0", + "version_installed": "0.1.0", "default_vendor": "claude", "path": "/Users/dev/.ynh/harnesses/assistants-dev", "installed_from": { @@ -253,7 +253,7 @@ final class HarnessModelTests: XCTestCase { let nilProvenanceJSON = """ { "name": "untracked", - "version": "0.1.0", + "version_installed": "0.1.0", "default_vendor": "claude", "path": "/Users/dev/harnesses/untracked", "installed_from": null, @@ -265,7 +265,7 @@ final class HarnessModelTests: XCTestCase { let localJSON = """ { "name": "assistants-dev", - "version": "0.1.0", + "version_installed": "0.1.0", "default_vendor": "claude", "path": "/Users/dev/.ynh/harnesses/assistants-dev", "installed_from": { @@ -293,7 +293,7 @@ final class HarnessModelTests: XCTestCase { let json = """ { "name": "david", - "version": "0.1.0", + "version_installed": "0.1.0", "default_vendor": "claude", "path": "/Users/dev/.ynh/harnesses/david", "installed_from": { @@ -379,7 +379,7 @@ final class HarnessModelTests: XCTestCase { let json = """ { "name": "test-harness", - "version": "1.0", + "version_installed": "1.0", "description": null, "default_vendor": "claude", "path": "/path/to/harness" @@ -398,7 +398,7 @@ final class HarnessModelTests: XCTestCase { let json = """ { "name": "test", - "version": "2.0", + "version_installed": "2.0", "default_vendor": "codex", "path": "/path", "manifest": {"tool": "termq", "version": "1"} @@ -413,7 +413,7 @@ final class HarnessModelTests: XCTestCase { let json = """ { "name": "test", - "version": "1.0", + "version_installed": "1.0", "default_vendor": "claude", "path": "/p", "installed_from": { @@ -432,7 +432,7 @@ final class HarnessModelTests: XCTestCase { func testHarnessInfo_customCodingKeys() throws { let json = """ - {"name":"h","version":"v","default_vendor":"claude","path":"/p"} + {"name":"h","version_installed":"v","default_vendor":"claude","path":"/p"} """ let info = try JSONDecoder().decode(HarnessInfo.self, from: json.data(using: .utf8)!) XCTAssertEqual(info.defaultVendor, "claude") @@ -442,7 +442,7 @@ final class HarnessModelTests: XCTestCase { func testHarnessDetail_init() throws { let infoJSON = """ - {"name":"h","version":"1","default_vendor":"claude","path":"/p"} + {"name":"h","version_installed":"1","default_vendor":"claude","path":"/p"} """ let compositionJSON = """ { diff --git a/Tests/TermQTests/YNHDecodingTests.swift b/Tests/TermQTests/YNHDecodingTests.swift index a1e0e581..9474cb68 100644 --- a/Tests/TermQTests/YNHDecodingTests.swift +++ b/Tests/TermQTests/YNHDecodingTests.swift @@ -17,7 +17,7 @@ final class YNHHarnessDecodingTests: XCTestCase { [ { "name": "assistants-dev", - "version": "0.1.0", + "version_installed": "0.1.0", "description": "A harness for adding to eyelock-assistants", "default_vendor": "claude", "path": "/Users/test/.ynh/harnesses/assistants-dev", @@ -51,7 +51,7 @@ final class YNHHarnessDecodingTests: XCTestCase { [ { "name": "my-harness", - "version": "0.1.0", + "version_installed": "0.1.0", "default_vendor": "claude", "path": "/Users/test/.ynh/harnesses/my-harness", "installed_from": { @@ -83,7 +83,7 @@ final class YNHHarnessDecodingTests: XCTestCase { [ { "name": "tester", - "version": "0.2.0", + "version_installed": "0.2.0", "default_vendor": "claude", "namespace": "eyelock/assistants", "path": "/Users/test/.ynh/harnesses/eyelock--assistants/tester", From 1f81aed99860c33f5c868a83517826d6c74cbe35 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 3 May 2026 10:30:11 +0000 Subject: [PATCH 16/29] chore: Update appcast for release v0.9.5 Auto-generated appcast files for Sparkle auto-updates. Co-Authored-By: github-actions[bot] --- Docs/appcast-beta.xml | 50 +++++++++++++++++++++++++++++++++++++++++++ Docs/appcast.xml | 50 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+) diff --git a/Docs/appcast-beta.xml b/Docs/appcast-beta.xml index 5bfa46b1..937c80fe 100644 --- a/Docs/appcast-beta.xml +++ b/Docs/appcast-beta.xml @@ -5,6 +5,56 @@ https://github.com/eyelock/TermQ Most recent updates to TermQ en + + Version 0.9.5 + 0.9.5 + 0.9.5 + Sun, 03 May 2026 10:29:15 +0000 + What's New +
    +
  • per-row delegate Edit and Remove in detail pane (#251)
  • +
  • inline include editing & manifest editor in detail pane (#250)
  • +
  • Phase 1 — management foundation, fork & duplicate, drift detection (#249)
  • +
+

Bug Fixes

+
    +
  • tolerate name/id mismatch when resolving selected harness (#252)
  • +
  • replace -50 Finder dialog with correct file/URL handling (#240)
  • +
  • re-register URL Apple Event handler after SwiftUI scene setup (#239)
  • +
  • fix uninstall for local harnesses with no YNH install record (#234)
  • +
  • fix appcast not updating on stable release (#233)
  • +
+
+

Installation

+ +
    +
  1. Download TermQ-0.9.5.dmg
  2. +
  3. Open the DMG and drag TermQ.app to your Applications folder
  4. +
  5. Double-click to launch
  6. +
+

Alternative: Zip Archive

+
    +
  1. Download TermQ-0.9.5.zip
  2. +
  3. Unzip and move TermQ.app to your Applications folder
  4. +
+

CLI Tool

+

The termqcli CLI is bundled inside the app. To install it:

+
    +
  1. Open TermQ.app
  2. +
  3. Go to TermQ → Settings (or press ⌘,)
  4. +
  5. Click Install Command Line Tool
  6. +
+

Or manually:

+
sudo cp /Applications/TermQ.app/Contents/Resources/termqcli /usr/local/bin/termqcli
+

Checksums

+

See checksums.txt for SHA-256 checksums of all release artifacts.

]]>
+ + 14.0 +
Version 0.9.4 0.9.4 diff --git a/Docs/appcast.xml b/Docs/appcast.xml index 21acbbc0..85c533f5 100644 --- a/Docs/appcast.xml +++ b/Docs/appcast.xml @@ -5,6 +5,56 @@ https://github.com/eyelock/TermQ Most recent updates to TermQ en + + Version 0.9.5 + 0.9.5 + 0.9.5 + Sun, 03 May 2026 10:29:15 +0000 + What's New +
    +
  • per-row delegate Edit and Remove in detail pane (#251)
  • +
  • inline include editing & manifest editor in detail pane (#250)
  • +
  • Phase 1 — management foundation, fork & duplicate, drift detection (#249)
  • +
+

Bug Fixes

+
    +
  • tolerate name/id mismatch when resolving selected harness (#252)
  • +
  • replace -50 Finder dialog with correct file/URL handling (#240)
  • +
  • re-register URL Apple Event handler after SwiftUI scene setup (#239)
  • +
  • fix uninstall for local harnesses with no YNH install record (#234)
  • +
  • fix appcast not updating on stable release (#233)
  • +
+
+

Installation

+ +
    +
  1. Download TermQ-0.9.5.dmg
  2. +
  3. Open the DMG and drag TermQ.app to your Applications folder
  4. +
  5. Double-click to launch
  6. +
+

Alternative: Zip Archive

+
    +
  1. Download TermQ-0.9.5.zip
  2. +
  3. Unzip and move TermQ.app to your Applications folder
  4. +
+

CLI Tool

+

The termqcli CLI is bundled inside the app. To install it:

+
    +
  1. Open TermQ.app
  2. +
  3. Go to TermQ → Settings (or press ⌘,)
  4. +
  5. Click Install Command Line Tool
  6. +
+

Or manually:

+
sudo cp /Applications/TermQ.app/Contents/Resources/termqcli /usr/local/bin/termqcli
+

Checksums

+

See checksums.txt for SHA-256 checksums of all release artifacts.

]]>
+ + 14.0 +
Version 0.9.4 0.9.4 From 5ec1227b6629823606522aec1dd02d9aff053af7 Mon Sep 17 00:00:00 2001 From: David Collie Date: Tue, 5 May 2026 07:02:28 +0100 Subject: [PATCH 17/29] hotfix(v0.9.6): backport focus, marketplace persistence, and OSC 52 default fixes (#272) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(window): stop stealing focus on AppleEvent reopen (#268) 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 Co-authored-by: Claude Opus 4.7 (1M context) * fix(marketplace): persist removals and harden against silent save failures (#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 * fix(security): align OSC 52 clipboard runtime default with Settings UI The runtime gate in TerminalHostView defaulted to true on 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. Drop the explicit-true sentinel; UserDefaults.bool returns false when the key is absent, matching the Settings UI and every other read site for this key. Surgical version of #270 for the v0.9.6 hotfix line; the develop fix routes the same gate through SettingsStore, which doesn't exist on this branch. Also reflow MarketplaceSidebarTab.swift:168 to satisfy swift-format (carried over from the #264 cherry-pick), and add a 0.9.6 CHANGELOG section covering the three backports. Co-Authored-By: Claude Opus 4.7 (1M context) --------- Co-authored-by: David Collie Co-authored-by: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 27 ++++ .../Marketplace/MarketplaceSidebarTab.swift | 6 +- Sources/TermQ/Services/MarketplaceStore.swift | 95 +++++++++--- Sources/TermQ/TermQAppDelegate.swift | 15 +- .../Settings/SettingsMarketplacesView.swift | 17 ++- Sources/TermQ/Views/TerminalHostView.swift | 8 +- Tests/TermQTests/MarketplaceStoreTests.swift | 142 ++++++++++++++++++ 7 files changed, 271 insertions(+), 39 deletions(-) create mode 100644 Tests/TermQTests/MarketplaceStoreTests.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b2d36d0..888804e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.9.6] — 2026-05-04 + +### Fixed + +- **Focus stealing on MCP-driven URL deliveries** — Background `termq://` + URL deliveries via `NSWorkspace.open(activates: false)` were triggering + AppleEvent Reopen, and `applicationShouldHandleReopen` unconditionally + called `makeKeyAndOrderFront`, stealing focus from whatever app the + user was working in. The handler now only activates the window on + genuine user-initiated reopen (unhide on Cmd+H, deminiaturize on + Cmd+M, or bring forward when no windows are visible). Backport of + #268. +- **Marketplace removals not persisted** — Removing a marketplace from + Settings → External Sources didn't survive relaunch. Three concurrent + issues fixed: the confirmation dialog read state after dismissal + (racy), `save()` swallowed errors with `try?`, and a re-seed could + re-add a default the user had removed. Tombstones now track removed + defaults (`marketplaces.removedDefaultURLs.v1`); Restore Defaults + bypasses tombstones explicitly. Backport of #264. +- **OSC 52 clipboard default mismatched Settings UI** — The runtime gate + defaulted to `true` on unset while Settings → Data & Security + displayed `false`, so a never-touched user saw "Off" but terminal + programs could silently copy to the clipboard. The runtime now + defaults to `false` to match the Settings UI. Behavior change: + existing users who relied on the implicit-on default will need to + enable OSC 52 explicitly. Aligned with #270. + ## [0.9.5] — 2026-05-03 ### Fixed diff --git a/Sources/TermQ/Marketplace/MarketplaceSidebarTab.swift b/Sources/TermQ/Marketplace/MarketplaceSidebarTab.swift index afecc97a..561fe9b9 100644 --- a/Sources/TermQ/Marketplace/MarketplaceSidebarTab.swift +++ b/Sources/TermQ/Marketplace/MarketplaceSidebarTab.swift @@ -115,7 +115,7 @@ struct MarketplaceSidebarTab: View { .foregroundColor(.secondary) .multilineTextAlignment(.center) Button(Strings.Marketplace.restoreDefaults) { - store.restoreDefaults() + store.restoreDefaults(force: true) Task { for marketplace in store.marketplaces { await refresh(marketplace) @@ -165,7 +165,9 @@ struct MarketplaceSidebarTab: View { selectedMarketplace = MarketplaceSelection(id: marketplace.id, marketplace: marketplace) } .contextMenu { - Button { Task { await refresh(marketplace) } } label: { + Button { + Task { await refresh(marketplace) } + } label: { Label(Strings.Marketplace.rowRefresh, systemImage: "arrow.clockwise") } if marketplace.isLocal { diff --git a/Sources/TermQ/Services/MarketplaceStore.swift b/Sources/TermQ/Services/MarketplaceStore.swift index 8c0f5f59..fb951a16 100644 --- a/Sources/TermQ/Services/MarketplaceStore.swift +++ b/Sources/TermQ/Services/MarketplaceStore.swift @@ -10,25 +10,40 @@ final class MarketplaceStore: ObservableObject { @Published private(set) var marketplaces: [Marketplace] = [] + /// Most recent persistence error, if any. Surfaces silent disk failures + /// (write permission, disk full, encoding) to the UI. + @Published private(set) var lastPersistenceError: String? + /// If non-nil, this marketplace is pre-selected in the browser (e.g. after wizard handoff). @Published var preselectedMarketplaceID: UUID? /// If non-nil, the HarnessIncludePicker should open pre-targeted at this harness. @Published var preselectedHarnessTarget: String? private let fileURL: URL + private let defaults: UserDefaults private let encoder = JSONEncoder() private let decoder = JSONDecoder() - private init() { - guard - let appSupport = FileManager.default.urls( - for: .applicationSupportDirectory, - in: .userDomainMask - ).first - else { fatalError("applicationSupportDirectory unavailable") } - let dir = appSupport.appendingPathComponent("TermQ", isDirectory: true) - try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true) - fileURL = dir.appendingPathComponent("marketplaces.json") + private static let defaultsSeedKey = "marketplaces.defaultsSeeded.v1" + private static let removedDefaultsKey = "marketplaces.removedDefaultURLs.v1" + + /// Designated initialiser. Defaults to the production location and standard UserDefaults; + /// tests inject a temp file and an isolated UserDefaults suite. + init(fileURL: URL? = nil, defaults: UserDefaults = .standard) { + if let fileURL { + self.fileURL = fileURL + } else { + guard + let appSupport = FileManager.default.urls( + for: .applicationSupportDirectory, + in: .userDomainMask + ).first + else { fatalError("applicationSupportDirectory unavailable") } + let dir = appSupport.appendingPathComponent("TermQ", isDirectory: true) + try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true) + self.fileURL = dir.appendingPathComponent("marketplaces.json") + } + self.defaults = defaults encoder.outputFormatting = .prettyPrinted encoder.dateEncodingStrategy = .iso8601 @@ -38,17 +53,26 @@ final class MarketplaceStore: ObservableObject { seedDefaultsIfNeeded() } - private static let defaultsSeedKey = "marketplaces.defaultsSeeded.v1" - private func seedDefaultsIfNeeded() { - guard !UserDefaults.standard.bool(forKey: Self.defaultsSeedKey) else { return } - restoreDefaults() - UserDefaults.standard.set(true, forKey: Self.defaultsSeedKey) + guard !defaults.bool(forKey: Self.defaultsSeedKey) else { return } + // Respect tombstones on re-seed (e.g. after a defaults reset). Only the + // explicit "Restore Defaults" button bypasses tombstones via force: true. + restoreDefaults(force: false) + defaults.set(true, forKey: Self.defaultsSeedKey) } /// Add any default marketplaces that are not already present. - func restoreDefaults() { + /// + /// - Parameter force: When `true`, ignores tombstones (URLs the user has explicitly removed) + /// and clears them. Use this for the explicit "Restore Defaults" button. + /// When `false` (default), tombstoned defaults stay removed. + func restoreDefaults(force: Bool = false) { + if force { + defaults.removeObject(forKey: Self.removedDefaultsKey) + } + let tombstones = removedDefaultURLs for seed in KnownMarketplaces.all { + if !force, tombstones.contains(seed.url) { continue } add( Marketplace( id: UUID(), name: seed.name, owner: seed.owner, @@ -67,8 +91,33 @@ final class MarketplaceStore: ObservableObject { } private func save() { - guard let data = try? encoder.encode(marketplaces) else { return } - try? data.write(to: fileURL, options: .atomic) + do { + let data = try encoder.encode(marketplaces) + try data.write(to: fileURL, options: .atomic) + lastPersistenceError = nil + } catch { + let message = "Failed to persist marketplaces: \(error.localizedDescription)" + lastPersistenceError = message + TermQLogger.io.error(message) + } + } + + // MARK: - Tombstones + + private var removedDefaultURLs: Set { + Set(defaults.stringArray(forKey: Self.removedDefaultsKey) ?? []) + } + + private func tombstone(url: String) { + var current = removedDefaultURLs + current.insert(url) + defaults.set(Array(current), forKey: Self.removedDefaultsKey) + } + + private func untombstone(url: String) { + var current = removedDefaultURLs + current.remove(url) + defaults.set(Array(current), forKey: Self.removedDefaultsKey) } // MARK: - Mutations @@ -77,12 +126,22 @@ final class MarketplaceStore: ObservableObject { guard !marketplaces.contains(where: { $0.url == marketplace.url && $0.vendor == marketplace.vendor }) else { return } + // Adding a known-default URL clears its tombstone so future seed checks treat it as present. + if KnownMarketplaces.all.contains(where: { $0.url == marketplace.url }) { + untombstone(url: marketplace.url) + } marketplaces.append(marketplace) save() } func remove(id: UUID) { + guard let removed = marketplaces.first(where: { $0.id == id }) else { return } marketplaces.removeAll { $0.id == id } + // If this URL is in the seed list, tombstone it so we don't re-add it on the next launch + // (defence against any future seed re-run, e.g. after a defaults reset or version bump). + if KnownMarketplaces.all.contains(where: { $0.url == removed.url }) { + tombstone(url: removed.url) + } save() } diff --git a/Sources/TermQ/TermQAppDelegate.swift b/Sources/TermQ/TermQAppDelegate.swift index e321c9d8..422a44b6 100644 --- a/Sources/TermQ/TermQAppDelegate.swift +++ b/Sources/TermQ/TermQAppDelegate.swift @@ -115,14 +115,19 @@ class TermQAppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate { TermQLogger.window.notice(" window[\(i)]: \(desc)") } if let window = mainWindow { - // Only call unhide if the app is actually hidden (e.g. from windowShouldClose → NSApp.hide). - // Calling unhide on a non-hidden app schedules a deferred _doOrderWindow orderOut block that - // fires when the run loop returns to NSDefaultRunLoopMode — which a busy terminal defers for - // seconds or minutes, producing the "spontaneous" window hide. + // Reopen events fire not just on Dock clicks but on every AppleEvent URL + // delivery (e.g. MCP-driven `termq://` opens with activates:false). Activating + // the window unconditionally here causes TermQ to steal focus on every + // background MCP operation. Only bring the window forward when there's + // actually something to surface. if NSApp.isHidden { NSApp.unhide(nil) + window.makeKeyAndOrderFront(nil) + } else if window.isMiniaturized { + window.deminiaturize(nil) + } else if !flag { + window.makeKeyAndOrderFront(nil) } - window.makeKeyAndOrderFront(nil) return false } // No main window tracked yet — allow the system to open one diff --git a/Sources/TermQ/Views/Settings/SettingsMarketplacesView.swift b/Sources/TermQ/Views/Settings/SettingsMarketplacesView.swift index 7b9976dd..965eeafa 100644 --- a/Sources/TermQ/Views/Settings/SettingsMarketplacesView.swift +++ b/Sources/TermQ/Views/Settings/SettingsMarketplacesView.swift @@ -99,16 +99,19 @@ struct SettingsMarketplacesView: View { .confirmationDialog( Strings.Settings.Marketplaces.removeConfirmTitle, isPresented: $showRemoveConfirmation, - titleVisibility: .visible - ) { + titleVisibility: .visible, + presenting: marketplaceToRemove + ) { marketplace in + // Capture by value — dialog dismissal can clear @State before + // the destructive action's body runs, so reading marketplaceToRemove + // after dismissal is racy. See HarnessDetailDependencyView for the + // same pattern. Button(Strings.Marketplace.rowRemove, role: .destructive) { - if let marketplace = marketplaceToRemove { store.remove(id: marketplace.id) } + store.remove(id: marketplace.id) } Button(Strings.Common.cancel, role: .cancel) {} - } message: { - if let marketplace = marketplaceToRemove { - Text(Strings.Settings.Marketplaces.removeConfirmMessage(marketplace.vendor.displayName)) - } + } message: { marketplace in + Text(Strings.Settings.Marketplaces.removeConfirmMessage(marketplace.vendor.displayName)) } .confirmationDialog( Strings.Settings.Marketplaces.removeYNHMarketplaceConfirmTitle, diff --git a/Sources/TermQ/Views/TerminalHostView.swift b/Sources/TermQ/Views/TerminalHostView.swift index 4bbdf10a..5e1d9eee 100644 --- a/Sources/TermQ/Views/TerminalHostView.swift +++ b/Sources/TermQ/Views/TerminalHostView.swift @@ -185,17 +185,11 @@ class TermQTerminalView: LocalProcessTerminalView { } } - /// User preference key for allowing OSC 52 clipboard access private static let allowOscClipboardKey = "allowOscClipboard" - /// Whether OSC 52 clipboard access is allowed (default: true for compatibility) static var allowOscClipboard: Bool { get { - // Default to true for backwards compatibility - if UserDefaults.standard.object(forKey: allowOscClipboardKey) == nil { - return true - } - return UserDefaults.standard.bool(forKey: allowOscClipboardKey) + UserDefaults.standard.bool(forKey: allowOscClipboardKey) } set { UserDefaults.standard.set(newValue, forKey: allowOscClipboardKey) diff --git a/Tests/TermQTests/MarketplaceStoreTests.swift b/Tests/TermQTests/MarketplaceStoreTests.swift new file mode 100644 index 00000000..5515b6a4 --- /dev/null +++ b/Tests/TermQTests/MarketplaceStoreTests.swift @@ -0,0 +1,142 @@ +import XCTest + +@testable import TermQ + +@MainActor +final class MarketplaceStoreTests: XCTestCase { + + private var tempDir: URL! + private var fileURL: URL! + private var defaults: UserDefaults! + private var suiteName: String! + + override func setUp() async throws { + tempDir = FileManager.default.temporaryDirectory.appendingPathComponent( + "MarketplaceStoreTests-\(UUID().uuidString)", isDirectory: true) + try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) + fileURL = tempDir.appendingPathComponent("marketplaces.json") + suiteName = "MarketplaceStoreTests-\(UUID().uuidString)" + defaults = UserDefaults(suiteName: suiteName)! + } + + override func tearDown() async throws { + try? FileManager.default.removeItem(at: tempDir) + defaults.removePersistentDomain(forName: suiteName) + } + + // MARK: - Seeding + + func test_firstLaunch_seedsKnownMarketplaces() { + let store = MarketplaceStore(fileURL: fileURL, defaults: defaults) + let urls = Set(store.marketplaces.map(\.url)) + for seed in KnownMarketplaces.all { + XCTAssertTrue(urls.contains(seed.url), "Seed missing: \(seed.url)") + } + } + + func test_secondLaunch_doesNotReseed() { + _ = MarketplaceStore(fileURL: fileURL, defaults: defaults) + let store2 = MarketplaceStore(fileURL: fileURL, defaults: defaults) + // Should match KnownMarketplaces count exactly — no duplicates from a re-seed + XCTAssertEqual(store2.marketplaces.count, KnownMarketplaces.all.count) + } + + // MARK: - Persistence round-trip + + func test_remove_persistsAcrossRelaunch() { + let store = MarketplaceStore(fileURL: fileURL, defaults: defaults) + let target = store.marketplaces.first { $0.url == "https://github.com/eyelock/assistants" } + let id = try! XCTUnwrap(target?.id) + store.remove(id: id) + XCTAssertFalse(store.marketplaces.contains(where: { $0.id == id })) + + // Simulate relaunch + let reborn = MarketplaceStore(fileURL: fileURL, defaults: defaults) + XCTAssertFalse( + reborn.marketplaces.contains(where: { $0.url == "https://github.com/eyelock/assistants" }), + "Removed default marketplace should not reappear after relaunch" + ) + } + + func test_add_persistsAcrossRelaunch() { + let store = MarketplaceStore(fileURL: fileURL, defaults: defaults) + let userAdded = Marketplace( + id: UUID(), name: "Custom", owner: "user", description: nil, + vendor: .claude, url: "https://example.com/custom", ref: nil, + plugins: [], lastFetched: nil, fetchError: nil + ) + store.add(userAdded) + + let reborn = MarketplaceStore(fileURL: fileURL, defaults: defaults) + XCTAssertTrue(reborn.marketplaces.contains(where: { $0.url == "https://example.com/custom" })) + } + + // MARK: - Tombstones (defence-in-depth against re-seed) + + func test_removedDefault_staysRemoved_evenIfSeedFlagCleared() { + let store = MarketplaceStore(fileURL: fileURL, defaults: defaults) + let target = store.marketplaces.first { $0.url == "https://github.com/eyelock/assistants" } + store.remove(id: try! XCTUnwrap(target?.id)) + + // Simulate "the seed key got cleared somehow" (defaults wipe, version bump, etc.) + defaults.removeObject(forKey: "marketplaces.defaultsSeeded.v1") + + let reborn = MarketplaceStore(fileURL: fileURL, defaults: defaults) + XCTAssertFalse( + reborn.marketplaces.contains(where: { $0.url == "https://github.com/eyelock/assistants" }), + "Tombstone should prevent a re-seed from re-adding a removed default" + ) + } + + func test_restoreDefaults_force_clearsTombstones() { + let store = MarketplaceStore(fileURL: fileURL, defaults: defaults) + let target = store.marketplaces.first { $0.url == "https://github.com/eyelock/assistants" } + store.remove(id: try! XCTUnwrap(target?.id)) + + store.restoreDefaults(force: true) + XCTAssertTrue( + store.marketplaces.contains(where: { $0.url == "https://github.com/eyelock/assistants" }), + "Force-restoring defaults should override tombstones" + ) + + // And the tombstone should be cleared, so a relaunch keeps the entry. + let reborn = MarketplaceStore(fileURL: fileURL, defaults: defaults) + XCTAssertTrue(reborn.marketplaces.contains(where: { $0.url == "https://github.com/eyelock/assistants" })) + } + + func test_addingBackARemovedDefault_clearsItsTombstone() { + let store = MarketplaceStore(fileURL: fileURL, defaults: defaults) + let target = store.marketplaces.first { $0.url == "https://github.com/eyelock/assistants" } + store.remove(id: try! XCTUnwrap(target?.id)) + + // User re-adds the same URL by hand via the Add sheet. + store.add( + Marketplace( + id: UUID(), name: "eyelock assistants", owner: "eyelock", description: nil, + vendor: .claude, url: "https://github.com/eyelock/assistants", ref: nil, + plugins: [], lastFetched: nil, fetchError: nil + ) + ) + + // Simulate re-seed conditions; entry should remain (tombstone cleared by add). + defaults.removeObject(forKey: "marketplaces.defaultsSeeded.v1") + let reborn = MarketplaceStore(fileURL: fileURL, defaults: defaults) + let count = reborn.marketplaces.filter { $0.url == "https://github.com/eyelock/assistants" }.count + XCTAssertEqual(count, 1, "Re-added default should appear exactly once after a re-seed") + } + + // MARK: - Add dedup + + func test_add_deduplicatesByURLAndVendor() { + let store = MarketplaceStore(fileURL: fileURL, defaults: defaults) + let before = store.marketplaces.count + store.add( + Marketplace( + id: UUID(), name: "dup", owner: "x", description: nil, + vendor: .claude, url: "https://github.com/eyelock/assistants", ref: nil, + plugins: [], lastFetched: nil, fetchError: nil + ) + ) + XCTAssertEqual(store.marketplaces.count, before, "Duplicate URL+vendor should not be added") + } +} From a76327340603cc6a59b012aa0c1b17239c06153d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 5 May 2026 06:18:31 +0000 Subject: [PATCH 18/29] chore: Update appcast for release v0.9.6 Auto-generated appcast files for Sparkle auto-updates. Co-Authored-By: github-actions[bot] --- Docs/appcast-beta.xml | 56 +++++++++++++++++++++++++++++++++++++++++++ Docs/appcast.xml | 56 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+) diff --git a/Docs/appcast-beta.xml b/Docs/appcast-beta.xml index 937c80fe..b7cc0bad 100644 --- a/Docs/appcast-beta.xml +++ b/Docs/appcast-beta.xml @@ -5,6 +5,62 @@ https://github.com/eyelock/TermQ Most recent updates to TermQ en + + Version 0.9.6 + 0.9.6 + 0.9.6 + Tue, 05 May 2026 06:17:38 +0000 + What's New +
    +
  • SourcePicker Add Delegate context (Phase 2) (#267)
  • +
  • unified SourcePicker for Install Harness (Phase 1) (#262)
  • +
  • per-row delegate Edit and Remove in detail pane (#251)
  • +
  • inline include editing & manifest editor in detail pane (#250)
  • +
  • Phase 1 — management foundation, fork & duplicate, drift detection (#249)
  • +
+

Bug Fixes

+
    +
  • align OSC 52 clipboard gate with Settings UI default (#270)
  • +
  • stop stealing focus on AppleEvent reopen (#268)
  • +
  • persist removals and harden against silent save failures (#264)
  • +
  • adapt to ynh 0.3 JSON envelope and version_installed rename (#258)
  • +
  • tolerate name/id mismatch when resolving selected harness (#252)
  • +
  • replace -50 Finder dialog with correct file/URL handling (#240)
  • +
  • re-register URL Apple Event handler after SwiftUI scene setup (#239)
  • +
  • fix uninstall for local harnesses with no YNH install record (#234)
  • +
  • fix appcast not updating on stable release (#233)
  • +
+
+

Installation

+ +
    +
  1. Download TermQ-0.9.6.dmg
  2. +
  3. Open the DMG and drag TermQ.app to your Applications folder
  4. +
  5. Double-click to launch
  6. +
+

Alternative: Zip Archive

+
    +
  1. Download TermQ-0.9.6.zip
  2. +
  3. Unzip and move TermQ.app to your Applications folder
  4. +
+

CLI Tool

+

The termqcli CLI is bundled inside the app. To install it:

+
    +
  1. Open TermQ.app
  2. +
  3. Go to TermQ → Settings (or press ⌘,)
  4. +
  5. Click Install Command Line Tool
  6. +
+

Or manually:

+
sudo cp /Applications/TermQ.app/Contents/Resources/termqcli /usr/local/bin/termqcli
+

Checksums

+

See checksums.txt for SHA-256 checksums of all release artifacts.

]]>
+ + 14.0 +
Version 0.9.5 0.9.5 diff --git a/Docs/appcast.xml b/Docs/appcast.xml index 85c533f5..f5ede539 100644 --- a/Docs/appcast.xml +++ b/Docs/appcast.xml @@ -5,6 +5,62 @@ https://github.com/eyelock/TermQ Most recent updates to TermQ en + + Version 0.9.6 + 0.9.6 + 0.9.6 + Tue, 05 May 2026 06:17:38 +0000 + What's New +
    +
  • SourcePicker Add Delegate context (Phase 2) (#267)
  • +
  • unified SourcePicker for Install Harness (Phase 1) (#262)
  • +
  • per-row delegate Edit and Remove in detail pane (#251)
  • +
  • inline include editing & manifest editor in detail pane (#250)
  • +
  • Phase 1 — management foundation, fork & duplicate, drift detection (#249)
  • +
+

Bug Fixes

+
    +
  • align OSC 52 clipboard gate with Settings UI default (#270)
  • +
  • stop stealing focus on AppleEvent reopen (#268)
  • +
  • persist removals and harden against silent save failures (#264)
  • +
  • adapt to ynh 0.3 JSON envelope and version_installed rename (#258)
  • +
  • tolerate name/id mismatch when resolving selected harness (#252)
  • +
  • replace -50 Finder dialog with correct file/URL handling (#240)
  • +
  • re-register URL Apple Event handler after SwiftUI scene setup (#239)
  • +
  • fix uninstall for local harnesses with no YNH install record (#234)
  • +
  • fix appcast not updating on stable release (#233)
  • +
+
+

Installation

+ +
    +
  1. Download TermQ-0.9.6.dmg
  2. +
  3. Open the DMG and drag TermQ.app to your Applications folder
  4. +
  5. Double-click to launch
  6. +
+

Alternative: Zip Archive

+
    +
  1. Download TermQ-0.9.6.zip
  2. +
  3. Unzip and move TermQ.app to your Applications folder
  4. +
+

CLI Tool

+

The termqcli CLI is bundled inside the app. To install it:

+
    +
  1. Open TermQ.app
  2. +
  3. Go to TermQ → Settings (or press ⌘,)
  4. +
  5. Click Install Command Line Tool
  6. +
+

Or manually:

+
sudo cp /Applications/TermQ.app/Contents/Resources/termqcli /usr/local/bin/termqcli
+

Checksums

+

See checksums.txt for SHA-256 checksums of all release artifacts.

]]>
+ + 14.0 +
Version 0.9.5 0.9.5 From 4f18dbf993a786812f248f55cd421a3cf95fe233 Mon Sep 17 00:00:00 2001 From: David Collie Date: Thu, 7 May 2026 20:27:12 +0100 Subject: [PATCH 19/29] 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 Co-authored-by: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 17 +++++ .../TermQ/Services/HarnessRepository.swift | 40 +++++++++-- Sources/TermQShared/Harness.swift | 34 +++++++++ Sources/TermQShared/HarnessInfo.swift | 28 ++++++++ .../TermQSharedTests/HarnessModelTests.swift | 27 ++++++++ Tests/TermQTests/YNHDecodingTests.swift | 69 +++++++++++++++++++ 6 files changed, 209 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 888804e4..a86f599a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.9.7] — 2026-05-07 + +### Fixed + +- **Harness loading broken against YNH 0.2.x** — v0.9.5/0.9.6 hard-coded + the YNH 0.3 structured-output shape (`{harnesses: [...]}` envelope, + `version_installed` field), but YNH 0.3 was never published to the + Homebrew tap. Every user running `brew install ynh` is on YNH 0.2.3, + which emits a bare `[Harness]` array with `version`. The mismatch + left the Harnesses sidebar empty and produced a blank Launch card on + worktree rows that already had a harness associated. Decoding is now + tolerant: `YNHListEnvelope` / `YNHInfoEnvelope` accept either the 0.3 + envelope or the 0.2 bare shape, and `Harness` / `HarnessInfo` accept + either `version_installed` or `version`. The compat layer can be + removed once YNH 0.3 ships to the tap and a capability gate is in + place. + ## [0.9.6] — 2026-05-04 ### Fixed diff --git a/Sources/TermQ/Services/HarnessRepository.swift b/Sources/TermQ/Services/HarnessRepository.swift index aa4bdf52..ea367949 100644 --- a/Sources/TermQ/Services/HarnessRepository.swift +++ b/Sources/TermQ/Services/HarnessRepository.swift @@ -5,16 +5,44 @@ private enum HarnessDetailError: Error { case missingYnd } -/// Envelope shape returned by `ynh ls --format json` (ynh 0.3+). -/// Wraps the harness list alongside capability and version metadata. -private struct YNHListEnvelope: Decodable { +/// Envelope shape returned by `ynh ls --format json`. +/// +/// YNH 0.3+ wraps the harness list as `{harnesses: [...], capabilities, ynh_version}`. +/// YNH 0.2.x emits a bare `[Harness]` array. This decoder accepts either shape. +struct YNHListEnvelope: Decodable { let harnesses: [Harness] + + private enum CodingKeys: String, CodingKey { case harnesses } + + init(from decoder: Decoder) throws { + if let keyed = try? decoder.container(keyedBy: CodingKeys.self), + keyed.contains(.harnesses) + { + harnesses = try keyed.decode([Harness].self, forKey: .harnesses) + } else { + harnesses = try [Harness](from: decoder) + } + } } -/// Envelope shape returned by `ynh info --format json` (ynh 0.3+). -/// Wraps a single harness payload alongside capability and version metadata. -private struct YNHInfoEnvelope: Decodable { +/// Envelope shape returned by `ynh info --format json`. +/// +/// YNH 0.3+ wraps the payload as `{harness: {...}, capabilities, ynh_version}`. +/// YNH 0.2.x emits a bare `HarnessInfo` object. This decoder accepts either shape. +struct YNHInfoEnvelope: Decodable { let harness: HarnessInfo + + private enum CodingKeys: String, CodingKey { case harness } + + init(from decoder: Decoder) throws { + if let keyed = try? decoder.container(keyedBy: CodingKeys.self), + keyed.contains(.harness) + { + harness = try keyed.decode(HarnessInfo.self, forKey: .harness) + } else { + harness = try HarnessInfo(from: decoder) + } + } } /// Repository for querying installed harnesses via `ynh ls --format json` diff --git a/Sources/TermQShared/Harness.swift b/Sources/TermQShared/Harness.swift index 09c8d8d5..1c469c02 100644 --- a/Sources/TermQShared/Harness.swift +++ b/Sources/TermQShared/Harness.swift @@ -29,11 +29,45 @@ public struct Harness: Codable, Equatable, Sendable, Identifiable { enum CodingKeys: String, CodingKey { case name, description, path, artifacts, includes, namespace case version = "version_installed" + case versionLegacy = "version" case defaultVendor = "default_vendor" case installedFrom = "installed_from" case delegatesTo = "delegates_to" } + public init(from decoder: Decoder) throws { + let c = try decoder.container(keyedBy: CodingKeys.self) + name = try c.decode(String.self, forKey: .name) + // YNH 0.3+ emits `version_installed`; 0.2.x emits `version`. Accept either. + if let modern = try c.decodeIfPresent(String.self, forKey: .version) { + version = modern + } else { + version = try c.decode(String.self, forKey: .versionLegacy) + } + description = try c.decodeIfPresent(String.self, forKey: .description) + defaultVendor = try c.decode(String.self, forKey: .defaultVendor) + namespace = try c.decodeIfPresent(String.self, forKey: .namespace) + path = try c.decode(String.self, forKey: .path) + installedFrom = try c.decodeIfPresent(HarnessProvenance.self, forKey: .installedFrom) + artifacts = try c.decode(HarnessArtifactCounts.self, forKey: .artifacts) + includes = try c.decodeIfPresent([HarnessInclude].self, forKey: .includes) ?? [] + delegatesTo = try c.decodeIfPresent([HarnessDelegate].self, forKey: .delegatesTo) ?? [] + } + + public func encode(to encoder: Encoder) throws { + var c = encoder.container(keyedBy: CodingKeys.self) + try c.encode(name, forKey: .name) + try c.encode(version, forKey: .version) + try c.encodeIfPresent(description, forKey: .description) + try c.encode(defaultVendor, forKey: .defaultVendor) + try c.encodeIfPresent(namespace, forKey: .namespace) + try c.encode(path, forKey: .path) + try c.encodeIfPresent(installedFrom, forKey: .installedFrom) + try c.encode(artifacts, forKey: .artifacts) + try c.encode(includes, forKey: .includes) + try c.encode(delegatesTo, forKey: .delegatesTo) + } + public init( name: String, version: String, diff --git a/Sources/TermQShared/HarnessInfo.swift b/Sources/TermQShared/HarnessInfo.swift index 59ee9a23..7e0390a3 100644 --- a/Sources/TermQShared/HarnessInfo.swift +++ b/Sources/TermQShared/HarnessInfo.swift @@ -21,9 +21,37 @@ public struct HarnessInfo: Codable, Sendable { enum CodingKeys: String, CodingKey { case name, description, path, manifest case version = "version_installed" + case versionLegacy = "version" case defaultVendor = "default_vendor" case installedFrom = "installed_from" } + + public init(from decoder: Decoder) throws { + let c = try decoder.container(keyedBy: CodingKeys.self) + name = try c.decode(String.self, forKey: .name) + // YNH 0.3+ emits `version_installed`; 0.2.x emits `version`. Accept either. + if let modern = try c.decodeIfPresent(String.self, forKey: .version) { + version = modern + } else { + version = try c.decode(String.self, forKey: .versionLegacy) + } + description = try c.decodeIfPresent(String.self, forKey: .description) + defaultVendor = try c.decode(String.self, forKey: .defaultVendor) + path = try c.decode(String.self, forKey: .path) + installedFrom = try c.decodeIfPresent(HarnessProvenance.self, forKey: .installedFrom) + manifest = try c.decodeIfPresent(JSONFragment.self, forKey: .manifest) + } + + public func encode(to encoder: Encoder) throws { + var c = encoder.container(keyedBy: CodingKeys.self) + try c.encode(name, forKey: .name) + try c.encode(version, forKey: .version) + try c.encodeIfPresent(description, forKey: .description) + try c.encode(defaultVendor, forKey: .defaultVendor) + try c.encode(path, forKey: .path) + try c.encodeIfPresent(installedFrom, forKey: .installedFrom) + try c.encodeIfPresent(manifest, forKey: .manifest) + } } /// An opaque JSON value stored as its raw string representation. diff --git a/Tests/TermQSharedTests/HarnessModelTests.swift b/Tests/TermQSharedTests/HarnessModelTests.swift index 42a43547..1fc82896 100644 --- a/Tests/TermQSharedTests/HarnessModelTests.swift +++ b/Tests/TermQSharedTests/HarnessModelTests.swift @@ -116,6 +116,33 @@ final class HarnessModelTests: XCTestCase { XCTAssertNotEqual(h1, h2) } + /// YNH 0.2.x emits `version` (not `version_installed`) on `ynh ls`/`ynh info` + /// payloads. TermQ must accept that legacy key so 0.9.7 does not regress users + /// still on Homebrew-shipped YNH 0.2.3. + func testHarness_codable_acceptsLegacyVersionKey() throws { + let json = """ + { + "name": "legacy", + "version": "0.1.0", + "default_vendor": "claude", + "path": "/p", + "artifacts": {"skills": 0, "agents": 0, "rules": 0, "commands": 0}, + "includes": [], + "delegates_to": [] + } + """ + let h = try JSONDecoder().decode(Harness.self, from: json.data(using: .utf8)!) + XCTAssertEqual(h.version, "0.1.0") + } + + func testHarnessInfo_codable_acceptsLegacyVersionKey() throws { + let json = """ + {"name":"h","version":"v","default_vendor":"claude","path":"/p"} + """ + let info = try JSONDecoder().decode(HarnessInfo.self, from: json.data(using: .utf8)!) + XCTAssertEqual(info.version, "v") + } + func testHarness_codable_minimal() throws { let json = """ { diff --git a/Tests/TermQTests/YNHDecodingTests.swift b/Tests/TermQTests/YNHDecodingTests.swift index 9474cb68..4a57cad9 100644 --- a/Tests/TermQTests/YNHDecodingTests.swift +++ b/Tests/TermQTests/YNHDecodingTests.swift @@ -154,6 +154,75 @@ final class YNHHarnessDecodingTests: XCTestCase { XCTAssertNotEqual(a.id, b.id) } + // MARK: - ynh ls envelope tolerance (0.2 bare array vs 0.3+ envelope) + + func test_ynh_ls_envelope_acceptsBareArray_ynh02() throws { + // YNH 0.2.x ships a bare array with `version` (not `version_installed`). + let json = """ + [ + { + "name": "david", + "version": "0.1.0", + "default_vendor": "claude", + "path": "/Users/test/.ynh/harnesses/david", + "artifacts": {"skills": 0, "agents": 0, "rules": 0, "commands": 0}, + "includes": [], + "delegates_to": [] + } + ] + """ + let envelope = try JSONDecoder().decode(YNHListEnvelope.self, from: Data(json.utf8)) + XCTAssertEqual(envelope.harnesses.count, 1) + XCTAssertEqual(envelope.harnesses[0].version, "0.1.0") + } + + func test_ynh_ls_envelope_acceptsEnvelope_ynh03() throws { + let json = """ + { + "capabilities": "0.3.0", + "ynh_version": "0.3.0", + "harnesses": [ + { + "name": "david", + "version_installed": "0.1.0", + "default_vendor": "claude", + "path": "/Users/test/.ynh/harnesses/david", + "artifacts": {"skills": 0, "agents": 0, "rules": 0, "commands": 0}, + "includes": [], + "delegates_to": [] + } + ] + } + """ + let envelope = try JSONDecoder().decode(YNHListEnvelope.self, from: Data(json.utf8)) + XCTAssertEqual(envelope.harnesses.count, 1) + XCTAssertEqual(envelope.harnesses[0].version, "0.1.0") + } + + // MARK: - ynh info envelope tolerance + + func test_ynh_info_envelope_acceptsBareObject_ynh02() throws { + let json = """ + {"name":"h","version":"1","default_vendor":"claude","path":"/p"} + """ + let envelope = try JSONDecoder().decode(YNHInfoEnvelope.self, from: Data(json.utf8)) + XCTAssertEqual(envelope.harness.name, "h") + XCTAssertEqual(envelope.harness.version, "1") + } + + func test_ynh_info_envelope_acceptsEnvelope_ynh03() throws { + let json = """ + { + "capabilities": "0.3.0", + "ynh_version": "0.3.0", + "harness": {"name":"h","version_installed":"1","default_vendor":"claude","path":"/p"} + } + """ + let envelope = try JSONDecoder().decode(YNHInfoEnvelope.self, from: Data(json.utf8)) + XCTAssertEqual(envelope.harness.name, "h") + XCTAssertEqual(envelope.harness.version, "1") + } + // MARK: - ynh search --format json func test_ynh_search_decodesSearchResultArray() throws { From bfa36878708b9a26b7c7bfdc9a84e40e419703a3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 7 May 2026 19:44:14 +0000 Subject: [PATCH 20/29] chore: Update appcast for release v0.9.7 Auto-generated appcast files for Sparkle auto-updates. Co-Authored-By: github-actions[bot] --- Docs/appcast-beta.xml | 52 +++++++++++++++++++++++++++++++++++++++---- Docs/appcast.xml | 52 +++++++++++++++++++++++++++++++++++++++---- 2 files changed, 96 insertions(+), 8 deletions(-) diff --git a/Docs/appcast-beta.xml b/Docs/appcast-beta.xml index b7cc0bad..512f7c9f 100644 --- a/Docs/appcast-beta.xml +++ b/Docs/appcast-beta.xml @@ -6,12 +6,13 @@ Most recent updates to TermQ en - Version 0.9.6 - 0.9.6 - 0.9.6 - Tue, 05 May 2026 06:17:38 +0000 + Version 0.9.7 + 0.9.7 + 0.9.7 + Thu, 07 May 2026 19:43:26 +0000 What's New
    +
  • SourcePicker Add Include context (Phase 3) (#294)
  • SourcePicker Add Delegate context (Phase 2) (#267)
  • unified SourcePicker for Install Harness (Phase 1) (#262)
  • per-row delegate Edit and Remove in detail pane (#251)
  • @@ -20,6 +21,7 @@

Bug Fixes

    +
  • generate release notes from CHANGELOG.md instead of develop range (#283)
  • align OSC 52 clipboard gate with Settings UI default (#270)
  • stop stealing focus on AppleEvent reopen (#268)
  • persist removals and harden against silent save failures (#264)
  • @@ -34,6 +36,48 @@

    Installation

      +
    1. Download TermQ-0.9.7.dmg
    2. +
    3. Open the DMG and drag TermQ.app to your Applications folder
    4. +
    5. Double-click to launch
    6. +
    +

    Alternative: Zip Archive

    +
      +
    1. Download TermQ-0.9.7.zip
    2. +
    3. Unzip and move TermQ.app to your Applications folder
    4. +
    +

    CLI Tool

    +

    The termqcli CLI is bundled inside the app. To install it:

    +
      +
    1. Open TermQ.app
    2. +
    3. Go to TermQ → Settings (or press ⌘,)
    4. +
    5. Click Install Command Line Tool
    6. +
    +

    Or manually:

    +
    sudo cp /Applications/TermQ.app/Contents/Resources/termqcli /usr/local/bin/termqcli
    +

    Checksums

    +

    See checksums.txt for SHA-256 checksums of all release artifacts.

    ]]> + + 14.0 + + + Version 0.9.6 + 0.9.6 + 0.9.6 + Tue, 05 May 2026 06:17:38 +0000 + Bug Fixes +
      +
    • align OSC 52 clipboard gate with Settings UI default — runtime gate previously defaulted to ON while Settings showed OFF; never-touched users could have terminal programs silently copy to the clipboard. Behavior change: existing users who relied on the implicit-on default will need to enable OSC 52 explicitly. (#270 / #272)
    • +
    • stop stealing focus on AppleEvent reopen — MCP-driven termq:// URL deliveries no longer activate the TermQ window when the user is working in another app. (#268)
    • +
    • persist marketplace removals and harden against silent save failures — removed marketplaces survive relaunch; tombstones prevent re-seed from resurrecting them; persistence errors are now surfaced instead of swallowed. (#264)
    • +
    +
    +

    Installation

    + +
    1. Download TermQ-0.9.6.dmg
    2. Open the DMG and drag TermQ.app to your Applications folder
    3. Double-click to launch
    4. diff --git a/Docs/appcast.xml b/Docs/appcast.xml index f5ede539..53d1bdc4 100644 --- a/Docs/appcast.xml +++ b/Docs/appcast.xml @@ -6,12 +6,13 @@ Most recent updates to TermQ en - Version 0.9.6 - 0.9.6 - 0.9.6 - Tue, 05 May 2026 06:17:38 +0000 + Version 0.9.7 + 0.9.7 + 0.9.7 + Thu, 07 May 2026 19:43:26 +0000 What's New
        +
      • SourcePicker Add Include context (Phase 3) (#294)
      • SourcePicker Add Delegate context (Phase 2) (#267)
      • unified SourcePicker for Install Harness (Phase 1) (#262)
      • per-row delegate Edit and Remove in detail pane (#251)
      • @@ -20,6 +21,7 @@

      Bug Fixes

        +
      • generate release notes from CHANGELOG.md instead of develop range (#283)
      • align OSC 52 clipboard gate with Settings UI default (#270)
      • stop stealing focus on AppleEvent reopen (#268)
      • persist removals and harden against silent save failures (#264)
      • @@ -34,6 +36,48 @@

        Installation

          +
        1. Download TermQ-0.9.7.dmg
        2. +
        3. Open the DMG and drag TermQ.app to your Applications folder
        4. +
        5. Double-click to launch
        6. +
        +

        Alternative: Zip Archive

        +
          +
        1. Download TermQ-0.9.7.zip
        2. +
        3. Unzip and move TermQ.app to your Applications folder
        4. +
        +

        CLI Tool

        +

        The termqcli CLI is bundled inside the app. To install it:

        +
          +
        1. Open TermQ.app
        2. +
        3. Go to TermQ → Settings (or press ⌘,)
        4. +
        5. Click Install Command Line Tool
        6. +
        +

        Or manually:

        +
        sudo cp /Applications/TermQ.app/Contents/Resources/termqcli /usr/local/bin/termqcli
        +

        Checksums

        +

        See checksums.txt for SHA-256 checksums of all release artifacts.

        ]]> + + 14.0 + + + Version 0.9.6 + 0.9.6 + 0.9.6 + Tue, 05 May 2026 06:17:38 +0000 + Bug Fixes +
          +
        • align OSC 52 clipboard gate with Settings UI default — runtime gate previously defaulted to ON while Settings showed OFF; never-touched users could have terminal programs silently copy to the clipboard. Behavior change: existing users who relied on the implicit-on default will need to enable OSC 52 explicitly. (#270 / #272)
        • +
        • stop stealing focus on AppleEvent reopen — MCP-driven termq:// URL deliveries no longer activate the TermQ window when the user is working in another app. (#268)
        • +
        • persist marketplace removals and harden against silent save failures — removed marketplaces survive relaunch; tombstones prevent re-seed from resurrecting them; persistence errors are now surfaced instead of swallowed. (#264)
        • +
        +
        +

        Installation

        + +
        1. Download TermQ-0.9.6.dmg
        2. Open the DMG and drag TermQ.app to your Applications folder
        3. Double-click to launch
        4. From 9449878a634519492c899406d449f225bb8b9005 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 9 May 2026 11:48:20 +0000 Subject: [PATCH 21/29] chore: Update appcast for release v0.10.0-beta.1 Auto-generated appcast files for Sparkle auto-updates. Co-Authored-By: github-actions[bot] --- Docs/appcast-beta.xml | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/Docs/appcast-beta.xml b/Docs/appcast-beta.xml index 512f7c9f..eca1c53d 100644 --- a/Docs/appcast-beta.xml +++ b/Docs/appcast-beta.xml @@ -5,6 +5,43 @@ https://github.com/eyelock/TermQ Most recent updates to TermQ en + + Version 0.10.0.b1 + 0.10.0.b1 + 0.10.0.b1 + Sat, 09 May 2026 11:47:27 +0000 + See CHANGELOG.md for changes in this release.

          +
          +

          Installation

          + +
            +
          1. Download TermQ-0.10.0-beta.1.dmg
          2. +
          3. Open the DMG and drag TermQ.app to your Applications folder
          4. +
          5. Double-click to launch
          6. +
          +

          Alternative: Zip Archive

          +
            +
          1. Download TermQ-0.10.0-beta.1.zip
          2. +
          3. Unzip and move TermQ.app to your Applications folder
          4. +
          +

          CLI Tool

          +

          The termqcli CLI is bundled inside the app. To install it:

          +
            +
          1. Open TermQ.app
          2. +
          3. Go to TermQ → Settings (or press ⌘,)
          4. +
          5. Click Install Command Line Tool
          6. +
          +

          Or manually:

          +
          sudo cp /Applications/TermQ.app/Contents/Resources/termqcli /usr/local/bin/termqcli
          +

          Checksums

          +

          See checksums.txt for SHA-256 checksums of all release artifacts.

          ]]>
          + + 14.0 +
          Version 0.9.7 0.9.7 From 299788b5f663957c6444a7712b79ead891464414 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 9 May 2026 16:58:40 +0000 Subject: [PATCH 22/29] chore: Update appcast for release v0.10.0-beta.2 Auto-generated appcast files for Sparkle auto-updates. Co-Authored-By: github-actions[bot] --- Docs/appcast-beta.xml | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/Docs/appcast-beta.xml b/Docs/appcast-beta.xml index eca1c53d..d4df3484 100644 --- a/Docs/appcast-beta.xml +++ b/Docs/appcast-beta.xml @@ -5,6 +5,43 @@ https://github.com/eyelock/TermQ Most recent updates to TermQ en + + Version 0.10.0.b2 + 0.10.0.b2 + 0.10.0.b2 + Sat, 09 May 2026 16:57:48 +0000 + See CHANGELOG.md for changes in this release.

          +
          +

          Installation

          + +
            +
          1. Download TermQ-0.10.0-beta.2.dmg
          2. +
          3. Open the DMG and drag TermQ.app to your Applications folder
          4. +
          5. Double-click to launch
          6. +
          +

          Alternative: Zip Archive

          +
            +
          1. Download TermQ-0.10.0-beta.2.zip
          2. +
          3. Unzip and move TermQ.app to your Applications folder
          4. +
          +

          CLI Tool

          +

          The termqcli CLI is bundled inside the app. To install it:

          +
            +
          1. Open TermQ.app
          2. +
          3. Go to TermQ → Settings (or press ⌘,)
          4. +
          5. Click Install Command Line Tool
          6. +
          +

          Or manually:

          +
          sudo cp /Applications/TermQ.app/Contents/Resources/termqcli /usr/local/bin/termqcli
          +

          Checksums

          +

          See checksums.txt for SHA-256 checksums of all release artifacts.

          ]]>
          + + 14.0 +
          Version 0.10.0.b1 0.10.0.b1 From 3ad343c2c2a240c3fa9924bb389dc89807ac0935 Mon Sep 17 00:00:00 2001 From: David Collie Date: Mon, 11 May 2026 06:59:59 +0100 Subject: [PATCH 23/29] chore: update CHANGELOG for v0.10.0 --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 67656906..e357adef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.10.0] + ### Added - **Remote PR feed** — the Repositories sidebar gains a **Local / Remote** toggle. From bdd9b52299b4c6e769d1a7c87bde8dd486337c1e Mon Sep 17 00:00:00 2001 From: David Collie Date: Mon, 11 May 2026 07:06:09 +0100 Subject: [PATCH 24/29] fix(merge): remove duplicate init/encode from HarnessInfo after merge conflict resolution --- Sources/TermQShared/HarnessInfo.swift | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/Sources/TermQShared/HarnessInfo.swift b/Sources/TermQShared/HarnessInfo.swift index 9deed87a..68ced03e 100644 --- a/Sources/TermQShared/HarnessInfo.swift +++ b/Sources/TermQShared/HarnessInfo.swift @@ -59,33 +59,6 @@ public struct HarnessInfo: Codable, Sendable { try c.encodeIfPresent(isPinned, forKey: .isPinned) try c.encodeIfPresent(manifest, forKey: .manifest) } - - public init(from decoder: Decoder) throws { - let c = try decoder.container(keyedBy: CodingKeys.self) - name = try c.decode(String.self, forKey: .name) - // YNH 0.3+ emits `version_installed`; 0.2.x emits `version`. Accept either. - if let modern = try c.decodeIfPresent(String.self, forKey: .version) { - version = modern - } else { - version = try c.decode(String.self, forKey: .versionLegacy) - } - description = try c.decodeIfPresent(String.self, forKey: .description) - defaultVendor = try c.decode(String.self, forKey: .defaultVendor) - path = try c.decode(String.self, forKey: .path) - installedFrom = try c.decodeIfPresent(HarnessProvenance.self, forKey: .installedFrom) - manifest = try c.decodeIfPresent(JSONFragment.self, forKey: .manifest) - } - - public func encode(to encoder: Encoder) throws { - var c = encoder.container(keyedBy: CodingKeys.self) - try c.encode(name, forKey: .name) - try c.encode(version, forKey: .version) - try c.encodeIfPresent(description, forKey: .description) - try c.encode(defaultVendor, forKey: .defaultVendor) - try c.encode(path, forKey: .path) - try c.encodeIfPresent(installedFrom, forKey: .installedFrom) - try c.encodeIfPresent(manifest, forKey: .manifest) - } } /// An opaque JSON value stored as its raw string representation. From 49a4dc618fd58b2851393ad86130a8f953855555 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 11 May 2026 06:31:27 +0000 Subject: [PATCH 25/29] chore: Update appcast for release v0.10.0 Auto-generated appcast files for Sparkle auto-updates. Co-Authored-By: github-actions[bot] --- Docs/appcast-beta.xml | 101 ++++++++++++++++++++++++++++++++++++++++++ Docs/appcast.xml | 101 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 202 insertions(+) diff --git a/Docs/appcast-beta.xml b/Docs/appcast-beta.xml index d4df3484..aaaf4651 100644 --- a/Docs/appcast-beta.xml +++ b/Docs/appcast-beta.xml @@ -5,6 +5,107 @@ https://github.com/eyelock/TermQ Most recent updates to TermQ en + + Version 0.10.0 + 0.10.0 + 0.10.0 + Mon, 11 May 2026 06:30:22 +0000 + Added +
            +
          • Remote PR feed — the Repositories sidebar gains a Local / Remote toggle. Remote mode lists open pull requests for each registered GitHub repository, fetched via the gh CLI. The feed is priority-ordered: checked-out PRs pin to the top, then review-requested, then open non-draft with no reviewers, then everything else. Within each tier, PRs are sorted by updatedAt descending.

            +
              +
            • Per-host identity — login is resolved per-repository by calling gh api user with the repo's working directory, so orgs on github.com, GHEC, and on-prem GHE each resolve the correct account automatically.
            • +
            • Configurable feed cap — defaults to 20 PRs per repo; adjustable globally in Settings → GitHub or per-repo via YNHPersistence. Tier-1 (checked-out) PRs always appear regardless of the cap.
            • +
            • Overflow indicator — when the full list exceeds the cap, a + N more footer shows the count of hidden PRs.
            • +
            • Priority badges — each PR row carries role badges: you (author), review (review requested), assigned, draft, and checked out (green).
            • +
          • +
          • Run with Focus sheet — right-clicking any PR row with a checked-out worktree exposes Run with Focus…, a sheet for launching a ynh run harness session against that PR's worktree:

            +
              +
            • Harness picker pre-selects the last-used harness for the repo.
            • +
            • Focus picker pre-selects the repo's saved default focus (if set).
            • +
            • Vendor picker lets you override the harness default vendor; shows availability.
            • +
            • Profile picker is interactive in ad-hoc mode, locked to the focus's profile when a focus is selected.
            • +
            • Prompt textarea shows the focus prompt read-only; a Customize button unlocks it for editing.
            • +
            • Stay interactive toggle (gated on vendor capability) appends --interactive so the agent stays open after the initial focus response.
            • +
            • Harness detail caching — focuses and profiles are cached in-process after the first load; re-opening the sheet is instant. A refresh button forces a re-read from disk when the harness YAML has changed.
            • +
            • Terminal cards created from the sheet are titled focus: org/repo#N, with the repo slug middle-truncated if the total would exceed 40 characters.
            • +
          • +
          • Remote PR context menu — the PR row right-click menu now matches the local worktree menu structure (when the PR is checked out):

            +
              +
            • Run with Focus… and Quick Launch Focus ▶ submenu (per-focus quick launch without opening the full sheet; populated automatically when harness detail is cached).
            • +
            • Quick Terminal / Create Terminal… — open a terminal at the worktree path.
            • +
            • Reveal in Finder / Reveal in Terminal / Copy Branch Name.
            • +
            • Open PR on Remote / Copy PR URL / Update from Origin.
            • +
            • Show in Local — jump to Local mode, focused on the worktree.
            • +
            • Set Default Focus ▶ — change or clear the default focus for the repo (used as the pre-selection next time the Run with Focus sheet opens).
            • +
          • +
          • Prune Closed PRs — a ⊘ Prune Closed PRs action appears in Remote mode for repos with checked-out worktrees whose PRs have since been closed or merged. A confirmation sheet lists candidates with dirty/ahead flags; safe-to-prune rows are checked by default.

          • +
          • GitHub Settings tab — new Settings → GitHub tab with a stepper for the global PR feed cap (5–100, step 5).

          • +
          • Convert to Worktree — local branch rows gain a Convert to Worktree context menu action that creates a linked worktree for the branch without switching the main checkout.

          • +
          • Inline include and manifest editing — the detail pane now exposes per-row edit and remove on every include of an editable harness, plus a manifest-level editor for free-form fields. Mutations stream through IncludeMutator / HarnessManifestEditor against YNH and refresh the detail in place.

          • +
          • Per-row delegate management — delegates added by the host harness show as their own rows with edit + remove affordances, parallel to the include UX. Backed by DelegateMutator with the same source-aware apply path.

          • +
          • Unified Source Picker — Install Harness, Add Include, and Add Delegate now flow through a single Library / Git / Path picker surface. Library tab pulls from ynh marketplace results; Git accepts any URL with optional ref/sha pin; Path lets you point at a local source directory. Replaces the previous bespoke install + add-include sheets.

          • +
          • Schema-1 → 2 migration coordinator — on first launch against a YNH binary that has migrated ~/.ynh to canonical-id schema 2, TermQ consumes the migration manifest and rewrites its persisted worktree↔︎harness associations from the old <namespace>/<name> shape to the new host-prefixed canonical id (<host>/<org>/<repo>/<name>). Idempotent — re-applying the same manifest is a no-op.

          • +
          • Quarantine sidebar group — quarantined harnesses (entries YNH could not load due to a broken manifest) appear in a dedicated QUARANTINED group below LOCAL with per-row Restore and Drop actions. Drop is confirmation-gated and permanently removes the entry from ~/.ynh/.quarantine/broken/.

          • +
          • Harness Management Phase 1 — first slice of the harness-as-first-class-citizen rework. Registry harnesses, local harnesses, and forks now have distinct identities, editability rules, and provenance display in TermQ.

          • +
          • Source badges — sidebar rows and detail pane header show a provenance chip: registry name (registry installs), short Git URL (git installs), Local (path installs), or Forked from <registry> (forked-locals).

          • +
          • Read-only indicator — registry harnesses display a Read-only pill; surfaces that direct edits will be overwritten by the next ynh update.

          • +
          • Update detection with three-state drift signal

            +
              +
            • Versioned — manifest version bumped upstream → orange dot, info banner, single-click Update.
            • +
            • Unversioned drift — content changed upstream without a version bump → amber warning triangle, warning banner, confirmation step in the Update sheet listing each drifted include path with installed → available SHAs. Surfaces the supply-chain signal explicitly.
            • +
            • None — clean.
            • +
          • +
          • Fork to local — actions menu offers Fork to local on registry harnesses; single-call flow against pointer-model YNH (ynh fork --to <path>); creates one editable working tree, no copy under ~/.ynh/harnesses/. Detail pane shows ghost origin via installed_from.forked_from.

          • +
          • Duplicate — local-only renamed copy via ynh fork --to <path> --name <newname> single call. Hidden for registry harnesses (Fork covers that intent). Appears in sidebar context menu and detail action menu.

          • +
          • Action menu parity — sidebar context menu and detail action menu share the same canonical layout in five groups (Run, Location, Actions, Help, Destructive). Sidebar drops Help and advanced Actions; detail keeps everything. "Open in…" submenu (VS Code, Cursor, Zed, etc.), Reveal in Terminal, Open in browser (URL sources), Copy as Pathname all consistent.

          • +
          • Editable path resolution — for forked-locals, Reveal/Open/Copy actions target the editable source tree (installed_from.source), not the YNH install slot. Single canonical "where this lives on disk" location.

          • +
          • Sidebar header spinner — global probe in flight shows a spinner next to the "Harnesses" title; per-harness operations show the spinner next to the row.

          • +
          • Vendor override picker — per-harness vendor override (claude / codex / cursor) persists across launches, surfaces as a picker badge in the detail header.

          • +
          • Update menu hidden for forks — YNH explicitly refuses ynh update on forks; menu reflects that rather than offering an action that always errors.

          • +
          +

          Changed

          +
            +
          • Canonical-id at the YNH CLI boundary — every TermQ→YNH command now passes harness.id (the canonical id YNH stamps on each harness) rather than the bare name. Eliminates the duplicate-name bug class where a registry install and a local fork sharing a name could resolve to the wrong target. The id || name fallback in HarnessRepository is gone and Harness.id is sourced verbatim from the YNH envelope (with a namespace + "/" + name fallback for older binaries that don't emit the field).

          • +
          • Fork sheet aligned with ynh fork --name — the fork sheet's free-form Identity field is replaced with an optional Name field. The new fork's canonical id is always local/<name>; submitting with no name keeps the source's name. Matches what the YNH binary actually exposes.

          • +
          • Detail pane refactored — extracted HarnessDetailViewModel from the view; source classification, editability, and update signals live in pure types with their own test coverage. Old feature-flagged badge retired.

          • +
          • YNH command runner is injectableYNHCommandRunner protocol replaces direct CommandRunner.run calls in repository, vendor, search, update-availability, harness author runners, editor registry, and terminal session manager. Production paths use the live runner; tests inject stubs to exercise success and failure branches without spawning real YNH subprocesses.

          • +
          • YNH JSON envelope — TermQ now reads capabilities and ynh_version from the structured-output envelope (YNH 0.3.0+) and gates Phase 1 features behind a version probe.

          • +
          • Tolerant decoderHarness decoder accepts null for includes and delegates_to (which YNH emits for broken installs whose source path is missing). A single bad row no longer collapses the whole sidebar.

          • +
          • Settings layering — introduced SettingsStore as the single owner for user preferences, with explicit defaults → user → per-card override resolution. The four per-terminal fields the audit named — safe paste, font size, theme, and backend — now carry an Optional override on the card rather than being snapshotted from UserDefaults at create time. New cards inherit the global default and track future changes to it; the card editor exposes an "Override default" toggle for each field.

            +

            Upgrade behaviour for existing cards: cards persisted before this release keep their concrete values as explicit overrides. They will not track future changes to the matching global default. To opt back into the global, open the card editor and turn the "Override default" toggle off. This is intentional — it preserves "what users had" through the upgrade.

          • +
          +
          +

          Installation

          + +
            +
          1. Download TermQ-0.10.0.dmg
          2. +
          3. Open the DMG and drag TermQ.app to your Applications folder
          4. +
          5. Double-click to launch
          6. +
          +

          Alternative: Zip Archive

          +
            +
          1. Download TermQ-0.10.0.zip
          2. +
          3. Unzip and move TermQ.app to your Applications folder
          4. +
          +

          CLI Tool

          +

          The termqcli CLI is bundled inside the app. To install it:

          +
            +
          1. Open TermQ.app
          2. +
          3. Go to TermQ → Settings (or press ⌘,)
          4. +
          5. Click Install Command Line Tool
          6. +
          +

          Or manually:

          +
          sudo cp /Applications/TermQ.app/Contents/Resources/termqcli /usr/local/bin/termqcli
          +

          Checksums

          +

          See checksums.txt for SHA-256 checksums of all release artifacts.

          ]]>
          + + 14.0 +
          Version 0.10.0.b2 0.10.0.b2 diff --git a/Docs/appcast.xml b/Docs/appcast.xml index 53d1bdc4..2e4b0761 100644 --- a/Docs/appcast.xml +++ b/Docs/appcast.xml @@ -5,6 +5,107 @@ https://github.com/eyelock/TermQ Most recent updates to TermQ en + + Version 0.10.0 + 0.10.0 + 0.10.0 + Mon, 11 May 2026 06:30:22 +0000 + Added +
            +
          • Remote PR feed — the Repositories sidebar gains a Local / Remote toggle. Remote mode lists open pull requests for each registered GitHub repository, fetched via the gh CLI. The feed is priority-ordered: checked-out PRs pin to the top, then review-requested, then open non-draft with no reviewers, then everything else. Within each tier, PRs are sorted by updatedAt descending.

            +
              +
            • Per-host identity — login is resolved per-repository by calling gh api user with the repo's working directory, so orgs on github.com, GHEC, and on-prem GHE each resolve the correct account automatically.
            • +
            • Configurable feed cap — defaults to 20 PRs per repo; adjustable globally in Settings → GitHub or per-repo via YNHPersistence. Tier-1 (checked-out) PRs always appear regardless of the cap.
            • +
            • Overflow indicator — when the full list exceeds the cap, a + N more footer shows the count of hidden PRs.
            • +
            • Priority badges — each PR row carries role badges: you (author), review (review requested), assigned, draft, and checked out (green).
            • +
          • +
          • Run with Focus sheet — right-clicking any PR row with a checked-out worktree exposes Run with Focus…, a sheet for launching a ynh run harness session against that PR's worktree:

            +
              +
            • Harness picker pre-selects the last-used harness for the repo.
            • +
            • Focus picker pre-selects the repo's saved default focus (if set).
            • +
            • Vendor picker lets you override the harness default vendor; shows availability.
            • +
            • Profile picker is interactive in ad-hoc mode, locked to the focus's profile when a focus is selected.
            • +
            • Prompt textarea shows the focus prompt read-only; a Customize button unlocks it for editing.
            • +
            • Stay interactive toggle (gated on vendor capability) appends --interactive so the agent stays open after the initial focus response.
            • +
            • Harness detail caching — focuses and profiles are cached in-process after the first load; re-opening the sheet is instant. A refresh button forces a re-read from disk when the harness YAML has changed.
            • +
            • Terminal cards created from the sheet are titled focus: org/repo#N, with the repo slug middle-truncated if the total would exceed 40 characters.
            • +
          • +
          • Remote PR context menu — the PR row right-click menu now matches the local worktree menu structure (when the PR is checked out):

            +
              +
            • Run with Focus… and Quick Launch Focus ▶ submenu (per-focus quick launch without opening the full sheet; populated automatically when harness detail is cached).
            • +
            • Quick Terminal / Create Terminal… — open a terminal at the worktree path.
            • +
            • Reveal in Finder / Reveal in Terminal / Copy Branch Name.
            • +
            • Open PR on Remote / Copy PR URL / Update from Origin.
            • +
            • Show in Local — jump to Local mode, focused on the worktree.
            • +
            • Set Default Focus ▶ — change or clear the default focus for the repo (used as the pre-selection next time the Run with Focus sheet opens).
            • +
          • +
          • Prune Closed PRs — a ⊘ Prune Closed PRs action appears in Remote mode for repos with checked-out worktrees whose PRs have since been closed or merged. A confirmation sheet lists candidates with dirty/ahead flags; safe-to-prune rows are checked by default.

          • +
          • GitHub Settings tab — new Settings → GitHub tab with a stepper for the global PR feed cap (5–100, step 5).

          • +
          • Convert to Worktree — local branch rows gain a Convert to Worktree context menu action that creates a linked worktree for the branch without switching the main checkout.

          • +
          • Inline include and manifest editing — the detail pane now exposes per-row edit and remove on every include of an editable harness, plus a manifest-level editor for free-form fields. Mutations stream through IncludeMutator / HarnessManifestEditor against YNH and refresh the detail in place.

          • +
          • Per-row delegate management — delegates added by the host harness show as their own rows with edit + remove affordances, parallel to the include UX. Backed by DelegateMutator with the same source-aware apply path.

          • +
          • Unified Source Picker — Install Harness, Add Include, and Add Delegate now flow through a single Library / Git / Path picker surface. Library tab pulls from ynh marketplace results; Git accepts any URL with optional ref/sha pin; Path lets you point at a local source directory. Replaces the previous bespoke install + add-include sheets.

          • +
          • Schema-1 → 2 migration coordinator — on first launch against a YNH binary that has migrated ~/.ynh to canonical-id schema 2, TermQ consumes the migration manifest and rewrites its persisted worktree↔︎harness associations from the old <namespace>/<name> shape to the new host-prefixed canonical id (<host>/<org>/<repo>/<name>). Idempotent — re-applying the same manifest is a no-op.

          • +
          • Quarantine sidebar group — quarantined harnesses (entries YNH could not load due to a broken manifest) appear in a dedicated QUARANTINED group below LOCAL with per-row Restore and Drop actions. Drop is confirmation-gated and permanently removes the entry from ~/.ynh/.quarantine/broken/.

          • +
          • Harness Management Phase 1 — first slice of the harness-as-first-class-citizen rework. Registry harnesses, local harnesses, and forks now have distinct identities, editability rules, and provenance display in TermQ.

          • +
          • Source badges — sidebar rows and detail pane header show a provenance chip: registry name (registry installs), short Git URL (git installs), Local (path installs), or Forked from <registry> (forked-locals).

          • +
          • Read-only indicator — registry harnesses display a Read-only pill; surfaces that direct edits will be overwritten by the next ynh update.

          • +
          • Update detection with three-state drift signal

            +
              +
            • Versioned — manifest version bumped upstream → orange dot, info banner, single-click Update.
            • +
            • Unversioned drift — content changed upstream without a version bump → amber warning triangle, warning banner, confirmation step in the Update sheet listing each drifted include path with installed → available SHAs. Surfaces the supply-chain signal explicitly.
            • +
            • None — clean.
            • +
          • +
          • Fork to local — actions menu offers Fork to local on registry harnesses; single-call flow against pointer-model YNH (ynh fork --to <path>); creates one editable working tree, no copy under ~/.ynh/harnesses/. Detail pane shows ghost origin via installed_from.forked_from.

          • +
          • Duplicate — local-only renamed copy via ynh fork --to <path> --name <newname> single call. Hidden for registry harnesses (Fork covers that intent). Appears in sidebar context menu and detail action menu.

          • +
          • Action menu parity — sidebar context menu and detail action menu share the same canonical layout in five groups (Run, Location, Actions, Help, Destructive). Sidebar drops Help and advanced Actions; detail keeps everything. "Open in…" submenu (VS Code, Cursor, Zed, etc.), Reveal in Terminal, Open in browser (URL sources), Copy as Pathname all consistent.

          • +
          • Editable path resolution — for forked-locals, Reveal/Open/Copy actions target the editable source tree (installed_from.source), not the YNH install slot. Single canonical "where this lives on disk" location.

          • +
          • Sidebar header spinner — global probe in flight shows a spinner next to the "Harnesses" title; per-harness operations show the spinner next to the row.

          • +
          • Vendor override picker — per-harness vendor override (claude / codex / cursor) persists across launches, surfaces as a picker badge in the detail header.

          • +
          • Update menu hidden for forks — YNH explicitly refuses ynh update on forks; menu reflects that rather than offering an action that always errors.

          • +
          +

          Changed

          +
            +
          • Canonical-id at the YNH CLI boundary — every TermQ→YNH command now passes harness.id (the canonical id YNH stamps on each harness) rather than the bare name. Eliminates the duplicate-name bug class where a registry install and a local fork sharing a name could resolve to the wrong target. The id || name fallback in HarnessRepository is gone and Harness.id is sourced verbatim from the YNH envelope (with a namespace + "/" + name fallback for older binaries that don't emit the field).

          • +
          • Fork sheet aligned with ynh fork --name — the fork sheet's free-form Identity field is replaced with an optional Name field. The new fork's canonical id is always local/<name>; submitting with no name keeps the source's name. Matches what the YNH binary actually exposes.

          • +
          • Detail pane refactored — extracted HarnessDetailViewModel from the view; source classification, editability, and update signals live in pure types with their own test coverage. Old feature-flagged badge retired.

          • +
          • YNH command runner is injectableYNHCommandRunner protocol replaces direct CommandRunner.run calls in repository, vendor, search, update-availability, harness author runners, editor registry, and terminal session manager. Production paths use the live runner; tests inject stubs to exercise success and failure branches without spawning real YNH subprocesses.

          • +
          • YNH JSON envelope — TermQ now reads capabilities and ynh_version from the structured-output envelope (YNH 0.3.0+) and gates Phase 1 features behind a version probe.

          • +
          • Tolerant decoderHarness decoder accepts null for includes and delegates_to (which YNH emits for broken installs whose source path is missing). A single bad row no longer collapses the whole sidebar.

          • +
          • Settings layering — introduced SettingsStore as the single owner for user preferences, with explicit defaults → user → per-card override resolution. The four per-terminal fields the audit named — safe paste, font size, theme, and backend — now carry an Optional override on the card rather than being snapshotted from UserDefaults at create time. New cards inherit the global default and track future changes to it; the card editor exposes an "Override default" toggle for each field.

            +

            Upgrade behaviour for existing cards: cards persisted before this release keep their concrete values as explicit overrides. They will not track future changes to the matching global default. To opt back into the global, open the card editor and turn the "Override default" toggle off. This is intentional — it preserves "what users had" through the upgrade.

          • +
          +
          +

          Installation

          + +
            +
          1. Download TermQ-0.10.0.dmg
          2. +
          3. Open the DMG and drag TermQ.app to your Applications folder
          4. +
          5. Double-click to launch
          6. +
          +

          Alternative: Zip Archive

          +
            +
          1. Download TermQ-0.10.0.zip
          2. +
          3. Unzip and move TermQ.app to your Applications folder
          4. +
          +

          CLI Tool

          +

          The termqcli CLI is bundled inside the app. To install it:

          +
            +
          1. Open TermQ.app
          2. +
          3. Go to TermQ → Settings (or press ⌘,)
          4. +
          5. Click Install Command Line Tool
          6. +
          +

          Or manually:

          +
          sudo cp /Applications/TermQ.app/Contents/Resources/termqcli /usr/local/bin/termqcli
          +

          Checksums

          +

          See checksums.txt for SHA-256 checksums of all release artifacts.

          ]]>
          + + 14.0 +
          Version 0.9.7 0.9.7 From ab08b90c4c30bbcb14924e8006f520f75733e9ea Mon Sep 17 00:00:00 2001 From: David Collie Date: Tue, 12 May 2026 06:32:13 +0100 Subject: [PATCH 26/29] chore: update CHANGELOG for v0.10.1 --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d36446bd..32c83f69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.10.1] + +### Added + +- **Run with Focus on non-checked-out PRs** — the Run with Focus sheet and Quick Launch Focus submenu are now available from the Remote PR context menu even when the PR's branch is not locally checked out. TermQ resolves the correct worktree root automatically and launches the harness session against it. +- **Active terminal sidebar highlight** — the sidebar entry matching the currently active terminal is now displayed in bold, making it easier to track your position across many open sessions. + +### Fixed + +- YNH CLI invocations now use canonical harness IDs instead of bare names, preventing argument-parsing errors when harness names contain characters that the CLI interprets as flags. + ## [0.10.0] ### Added From 4a3a4133d25e857cc561a2c3b0264393ef17d83e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 12 May 2026 05:56:40 +0000 Subject: [PATCH 27/29] chore: Update appcast for release v0.10.1 Auto-generated appcast files for Sparkle auto-updates. Co-Authored-By: github-actions[bot] --- Docs/appcast-beta.xml | 45 +++++++++++++++++++++++++++++++++++++++++++ Docs/appcast.xml | 45 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+) diff --git a/Docs/appcast-beta.xml b/Docs/appcast-beta.xml index aaaf4651..6c5f7081 100644 --- a/Docs/appcast-beta.xml +++ b/Docs/appcast-beta.xml @@ -5,6 +5,51 @@ https://github.com/eyelock/TermQ Most recent updates to TermQ en + + Version 0.10.1 + 0.10.1 + 0.10.1 + Tue, 12 May 2026 05:55:44 +0000 + Added +
            +
          • Run with Focus on non-checked-out PRs — the Run with Focus sheet and Quick Launch Focus submenu are now available from the Remote PR context menu even when the PR's branch is not locally checked out. TermQ resolves the correct worktree root automatically and launches the harness session against it.
          • +
          • Active terminal sidebar highlight — the sidebar entry matching the currently active terminal is now displayed in bold, making it easier to track your position across many open sessions.
          • +
          +

          Fixed

          +
            +
          • YNH CLI invocations now use canonical harness IDs instead of bare names, preventing argument-parsing errors when harness names contain characters that the CLI interprets as flags.
          • +
          +
          +

          Installation

          + +
            +
          1. Download TermQ-0.10.1.dmg
          2. +
          3. Open the DMG and drag TermQ.app to your Applications folder
          4. +
          5. Double-click to launch
          6. +
          +

          Alternative: Zip Archive

          +
            +
          1. Download TermQ-0.10.1.zip
          2. +
          3. Unzip and move TermQ.app to your Applications folder
          4. +
          +

          CLI Tool

          +

          The termqcli CLI is bundled inside the app. To install it:

          +
            +
          1. Open TermQ.app
          2. +
          3. Go to TermQ → Settings (or press ⌘,)
          4. +
          5. Click Install Command Line Tool
          6. +
          +

          Or manually:

          +
          sudo cp /Applications/TermQ.app/Contents/Resources/termqcli /usr/local/bin/termqcli
          +

          Checksums

          +

          See checksums.txt for SHA-256 checksums of all release artifacts.

          ]]>
          + + 14.0 +
          Version 0.10.0 0.10.0 diff --git a/Docs/appcast.xml b/Docs/appcast.xml index 2e4b0761..4f75b817 100644 --- a/Docs/appcast.xml +++ b/Docs/appcast.xml @@ -5,6 +5,51 @@ https://github.com/eyelock/TermQ Most recent updates to TermQ en + + Version 0.10.1 + 0.10.1 + 0.10.1 + Tue, 12 May 2026 05:55:44 +0000 + Added +
            +
          • Run with Focus on non-checked-out PRs — the Run with Focus sheet and Quick Launch Focus submenu are now available from the Remote PR context menu even when the PR's branch is not locally checked out. TermQ resolves the correct worktree root automatically and launches the harness session against it.
          • +
          • Active terminal sidebar highlight — the sidebar entry matching the currently active terminal is now displayed in bold, making it easier to track your position across many open sessions.
          • +
          +

          Fixed

          +
            +
          • YNH CLI invocations now use canonical harness IDs instead of bare names, preventing argument-parsing errors when harness names contain characters that the CLI interprets as flags.
          • +
          +
          +

          Installation

          + +
            +
          1. Download TermQ-0.10.1.dmg
          2. +
          3. Open the DMG and drag TermQ.app to your Applications folder
          4. +
          5. Double-click to launch
          6. +
          +

          Alternative: Zip Archive

          +
            +
          1. Download TermQ-0.10.1.zip
          2. +
          3. Unzip and move TermQ.app to your Applications folder
          4. +
          +

          CLI Tool

          +

          The termqcli CLI is bundled inside the app. To install it:

          +
            +
          1. Open TermQ.app
          2. +
          3. Go to TermQ → Settings (or press ⌘,)
          4. +
          5. Click Install Command Line Tool
          6. +
          +

          Or manually:

          +
          sudo cp /Applications/TermQ.app/Contents/Resources/termqcli /usr/local/bin/termqcli
          +

          Checksums

          +

          See checksums.txt for SHA-256 checksums of all release artifacts.

          ]]>
          + + 14.0 +
          Version 0.10.0 0.10.0 From 6de2ec4b7529ce431c39a3f8f40221fa35dfa41b Mon Sep 17 00:00:00 2001 From: David Collie Date: Wed, 13 May 2026 08:09:13 +0100 Subject: [PATCH 28/29] chore: update CHANGELOG for v0.11.0 --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index de5c2d79..9206387d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.11.0] + ### Added - **Focus and profile editing** — editable harnesses gain full inline editing for focuses and profiles From e22211a38acd792d8405d4c5fa90d6fa4b0fb25f Mon Sep 17 00:00:00 2001 From: David Collie Date: Wed, 13 May 2026 08:47:52 +0100 Subject: [PATCH 29/29] chore: sync appcast entries for v0.11.0 Co-Authored-By: Claude Sonnet 4.6 --- Docs/appcast-beta.xml | 53 +++++++++++++++++++++++++++++++++++++++++++ Docs/appcast.xml | 53 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+) diff --git a/Docs/appcast-beta.xml b/Docs/appcast-beta.xml index 6c5f7081..03574320 100644 --- a/Docs/appcast-beta.xml +++ b/Docs/appcast-beta.xml @@ -5,6 +5,59 @@ https://github.com/eyelock/TermQ Most recent updates to TermQ en + + Version 0.11.0 + 0.11.0 + 0.11.0 + Wed, 13 May 2026 07:34:26 +0000 + Added +
            +
          • Focus and profile editing — editable harnesses gain full inline editing for focuses and profiles directly in the detail pane: +
              +
            • Focuses — add, edit, and remove focuses from the Focuses section. The edit sheet exposes the focus name, prompt, and optional profile binding. Changes round-trip through ynh focus add, ynh focus update, and ynh focus remove.
            • +
            • Profiles — add and remove profiles from the Profiles section. Each profile card carries an menu with Edit (opens the profile sheet) and Remove (with confirmation).
            • +
            • Profile hooks — within the profile edit sheet, add and remove hooks per event via the same plus.circle / minus.circle affordances used at the harness level.
            • +
            • Profile MCP servers — add and remove MCP servers within a profile. The Add MCP sheet supports both command and SSE-URL server types with args, env vars, and HTTP headers.
            • +
            • Profile includes — the profile edit sheet's Includes section uses the unified Source Picker (Library / Git URL) to add includes, and a remove button to drop them.
            • +
          • +
          • Harness-level hook and MCP server editing — the Hooks and MCP Servers sections of the composition view now support inline add and remove for editable harnesses: +
              +
            • Add hooks via ynh hook add, remove individual hook entries by index via ynh hook remove.
            • +
            • Add MCP servers via ynh mcp add, remove them by name via ynh mcp remove.
            • +
            • Remove buttons appear per-entry and are disabled while a mutation is in flight to prevent double-actions.
            • +
          • +
          +
          +

          Installation

          + +
            +
          1. Download TermQ-0.11.0.dmg
          2. +
          3. Open the DMG and drag TermQ.app to your Applications folder
          4. +
          5. Double-click to launch
          6. +
          +

          Alternative: Zip Archive

          +
            +
          1. Download TermQ-0.11.0.zip
          2. +
          3. Unzip and move TermQ.app to your Applications folder
          4. +
          +

          CLI Tool

          +

          The termqcli CLI is bundled inside the app. To install it:

          +
            +
          1. Open TermQ.app
          2. +
          3. Go to TermQ → Settings (or press ⌘,)
          4. +
          5. Click Install Command Line Tool
          6. +
          +

          Or manually:

          +
          sudo cp /Applications/TermQ.app/Contents/Resources/termqcli /usr/local/bin/termqcli
          +

          Checksums

          +

          See checksums.txt for SHA-256 checksums of all release artifacts.

          ]]>
          + + 14.0 +
          Version 0.10.1 0.10.1 diff --git a/Docs/appcast.xml b/Docs/appcast.xml index 4f75b817..e9b8a88f 100644 --- a/Docs/appcast.xml +++ b/Docs/appcast.xml @@ -5,6 +5,59 @@ https://github.com/eyelock/TermQ Most recent updates to TermQ en + + Version 0.11.0 + 0.11.0 + 0.11.0 + Wed, 13 May 2026 07:34:26 +0000 + Added +
            +
          • Focus and profile editing — editable harnesses gain full inline editing for focuses and profiles directly in the detail pane: +
              +
            • Focuses — add, edit, and remove focuses from the Focuses section. The edit sheet exposes the focus name, prompt, and optional profile binding. Changes round-trip through ynh focus add, ynh focus update, and ynh focus remove.
            • +
            • Profiles — add and remove profiles from the Profiles section. Each profile card carries an menu with Edit (opens the profile sheet) and Remove (with confirmation).
            • +
            • Profile hooks — within the profile edit sheet, add and remove hooks per event via the same plus.circle / minus.circle affordances used at the harness level.
            • +
            • Profile MCP servers — add and remove MCP servers within a profile. The Add MCP sheet supports both command and SSE-URL server types with args, env vars, and HTTP headers.
            • +
            • Profile includes — the profile edit sheet's Includes section uses the unified Source Picker (Library / Git URL) to add includes, and a remove button to drop them.
            • +
          • +
          • Harness-level hook and MCP server editing — the Hooks and MCP Servers sections of the composition view now support inline add and remove for editable harnesses: +
              +
            • Add hooks via ynh hook add, remove individual hook entries by index via ynh hook remove.
            • +
            • Add MCP servers via ynh mcp add, remove them by name via ynh mcp remove.
            • +
            • Remove buttons appear per-entry and are disabled while a mutation is in flight to prevent double-actions.
            • +
          • +
          +
          +

          Installation

          + +
            +
          1. Download TermQ-0.11.0.dmg
          2. +
          3. Open the DMG and drag TermQ.app to your Applications folder
          4. +
          5. Double-click to launch
          6. +
          +

          Alternative: Zip Archive

          +
            +
          1. Download TermQ-0.11.0.zip
          2. +
          3. Unzip and move TermQ.app to your Applications folder
          4. +
          +

          CLI Tool

          +

          The termqcli CLI is bundled inside the app. To install it:

          +
            +
          1. Open TermQ.app
          2. +
          3. Go to TermQ → Settings (or press ⌘,)
          4. +
          5. Click Install Command Line Tool
          6. +
          +

          Or manually:

          +
          sudo cp /Applications/TermQ.app/Contents/Resources/termqcli /usr/local/bin/termqcli
          +

          Checksums

          +

          See checksums.txt for SHA-256 checksums of all release artifacts.

          ]]>
          + + 14.0 +
          Version 0.10.1 0.10.1