Skip to content

release: TermQ v0.11.0#320

Merged
eyelock merged 8 commits into
mainfrom
release/v0.11.0
May 13, 2026
Merged

release: TermQ v0.11.0#320
eyelock merged 8 commits into
mainfrom
release/v0.11.0

Conversation

@eyelock
Copy link
Copy Markdown
Owner

@eyelock eyelock commented May 13, 2026

Promotes release/v0.11.0 to main for stable release.

eyelock and others added 8 commits May 12, 2026 07:02
* chore: Update appcast for release manual

Auto-generated appcast files for Sparkle auto-updates.

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

* fix(ci): fix appcast not updating on stable release (#233)

Three bugs prevented v0.9.0 from appearing in appcast.xml:

1. Race condition: the `release: published` and explicit `workflow_dispatch`
   triggers both fired within 2 seconds of the release being published, before
   the GitHub API had indexed the new release. The appcast workflow got the
   pre-release cached list of exactly 30 items and silently skipped v0.9.0
   (no zip asset visible yet → jq returned empty → no warning logged → diff
   showed no changes → no PR created).

2. Pagination: generate-appcast.sh fetched a single page with GitHub's default
   of 30 releases. With 50+ releases in the repo, any stable release beyond
   page 1 would be invisible to the generator.

Fixes:
- Replace `release: published` + explicit `workflow_dispatch` trigger with
  `workflow_run` on Release workflow completion. The Release workflow takes
  30-60 minutes to build/sign/notarize; by the time it completes the API
  has long indexed the new release.
- Add job-level guard to skip appcast update when the Release workflow failed.
- Implement page-looping in fetch_releases() (per_page=100&page=N until empty)
  so all releases are fetched regardless of count.
- Add GH_TOKEN auth header to bypass the API response cache.
- Fix stale github.event.release.tag_name reference (removed with the
  release: published trigger) → github.event.workflow_run.head_branch.

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

* 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 <support@eyelock.net>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs(skills): update hotfix procedure — PR before CI, CHANGELOG, appcast forward-port

- 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 <noreply@anthropic.com>

* chore: Update appcast for release v0.9.1

Auto-generated appcast files for Sparkle auto-updates.

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

* fix(ui): re-register URL Apple Event handler after SwiftUI scene setup (#239)

SwiftUI registers its own kAEGetURL handler during scene initialisation,
which runs after App.init — overriding the registration we placed there.
This caused SwiftUI's AppWindowsController.activateWindowForExternalEvent
to close the main window on every URL open.

Moving the NSAppleEventManager registration to applicationDidFinishLaunching
ensures TermQ's handler is set last and wins, so SwiftUI never sees the URL
Apple Event and cannot hide the window.

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

* 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 <support@eyelock.net>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore: update CHANGELOG for v0.9.2

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

* chore: Update appcast for release v0.9.2

Auto-generated appcast files for Sparkle auto-updates.

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

* fix(concurrency): add @sendable to system-dispatched closures to prevent MainActor isolation crash

TerminalLinkResolver and TmuxControlModeSession both had closures passed to
system APIs (LaunchServices completion handler and FileHandle.readabilityHandler)
that were inheriting @mainactor isolation from their enclosing context. When
called on a background queue by the system this triggers EXC_BREAKPOINT via
_swift_task_checkIsolatedSwift. Mark both closures @sendable to opt them out
of actor isolation inheritance.

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

* chore: update CHANGELOG for v0.9.3

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

* chore: Update appcast for release v0.9.3

Auto-generated appcast files for Sparkle auto-updates.

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

* fix(harness): tolerate name/id mismatch when resolving selected harness (#252)

`Harness.id` is `"namespace/name"` for namespaced installs but
`YNHPersistence` keys associations by bare `name`. The Launch <harness>
flow on worktree rows passes the persisted name through and sets
`selectedHarnessName = name`, but `HarnessRepository.selectedHarness`
matched on `id` only — for any namespaced install the lookup missed,
the launch sheet's content closure returned nothing, and the sheet
rendered as a blank rounded rectangle that never populated and could
only be dismissed with Esc.

Make `selectedHarness` match by `id` then fall back to `name`. Apply
the same rule to the stale-selection eviction inside `refresh()` so
the next list refresh doesn't immediately clear a name-keyed selection.

Affects v0.9.3 — also forward-ported via hotfix release.

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

* chore: Update appcast for release v0.9.4

Auto-generated appcast files for Sparkle auto-updates.

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

* 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 <name> --format json` likewise wraps in
     `{capabilities, harness, ynh_version}`.
  3. The harness `version` field was renamed to `version_installed`
     in both `ynh ls` and `ynh info` payloads.

Update HarnessRepository to decode through YNHListEnvelope /
YNHInfoEnvelope wrappers, and remap the `version` CodingKey on
Harness and HarnessInfo to `version_installed`. ynd compose still
emits `version` so HarnessComposition is unchanged.

User-visible symptom on v0.9.4: the Harnesses sidebar tab was empty,
harness detail showed nothing, and Launch from a worktree row
presented a blank rounded sheet (or did nothing at all). The v0.9.4
identifier-fallback fix at HarnessRepository:40 was a downstream
patch on the same bug class but couldn't help while the list itself
was empty.

YNH-side schema changes are documented separately in a YNH bug
report (envelope shape, version rename, Harness.namespace not
populated for registry installs). All three are 0.x churn and
explicitly not backward-compatible.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: Update appcast for release v0.9.5

Auto-generated appcast files for Sparkle auto-updates.

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

* hotfix(v0.9.6): backport focus, marketplace persistence, and OSC 52 default fixes (#272)

* 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 <support@eyelock.net>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 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 <support@eyelock.net>

* 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) <noreply@anthropic.com>

---------

Co-authored-by: David Collie <support@eyelock.net>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: Update appcast for release v0.9.6

Auto-generated appcast files for Sparkle auto-updates.

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

* hotfix(v0.9.7): tolerate YNH 0.2.x list/info shapes (#296)

v0.9.5/0.9.6 hard-coded the YNH 0.3 structured-output shape, but
YNH 0.3 was never published to the Homebrew tap. Every user on
`brew install ynh` is on 0.2.3, so harness loading failed entirely:
empty Harnesses sidebar, blank Launch card on worktree rows that
already had a harness associated.

Decoding is now tolerant of both shapes:

- `YNHListEnvelope` / `YNHInfoEnvelope` accept either the 0.3
  envelope (`{harnesses: [...]}` / `{harness: {...}}`) or a bare
  `[Harness]` / `HarnessInfo` payload.
- `Harness` / `HarnessInfo` accept either `version_installed` (0.3)
  or `version` (0.2.x).

Tests cover both shapes for both call sites.

The compat layer is intentional and sticks around past 0.10. Removal
plan: once YNH 0.3 ships to the tap, gate behavior on
`YNHDetector.capabilityMeets("0.3.0")` for one release, then delete
the legacy branches.

Co-authored-by: David Collie <support@eyelock.net>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: Update appcast for release v0.9.7

Auto-generated appcast files for Sparkle auto-updates.

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

* chore: Update appcast for release v0.10.0-beta.1

Auto-generated appcast files for Sparkle auto-updates.

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

* chore: Update appcast for release v0.10.0-beta.2

Auto-generated appcast files for Sparkle auto-updates.

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

* chore: update CHANGELOG for v0.10.0

* fix(merge): remove duplicate init/encode from HarnessInfo after merge conflict resolution

* chore: Update appcast for release v0.10.0

Auto-generated appcast files for Sparkle auto-updates.

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

* chore: update CHANGELOG for v0.10.1

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: David Collie <support@eyelock.net>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: David Collie <support@eyelock.net>
…#317)

Subscribe to every NSApp and NSWindow transition (becomeActive,
resignActive, hide, unhide, becomeKey, resignKey, becomeMain,
resignMain, miniaturize, deminiaturize) and route them through a
single logLifecycle helper. Each entry records a state snapshot
(active/hidden/visible/key/main/min/frontmost-bundle-id) plus a
24-frame stack so the diagnostics report shows who pulled focus
and from which app, without needing new instrumentation each
time a regression appears.

Co-authored-by: David Collie <support@eyelock.net>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
#318)

In tmux attach mode, `set-option mouse off` caused tmux to swallow the
inner app's mouse-mode-enable escapes. SwiftTerm then saw an alt screen
with mouseMode == .off and converted scroll wheel events to arrow keys,
which mouse-aware apps like Claude Code interpret as keystrokes.

Re-enable `mouse on`. Native text selection still works thanks to the
allowMouseReporting toggle added in #147, which disables SwiftTerm's
mouse reporting on drag. Add WheelDownPane copy-mode bindings that
auto-cancel when scroll_position reaches the bottom so the wheel
doesn't leave the user parked in copy-mode.

Co-authored-by: David Collie <support@eyelock.net>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ervers (#319)

* feat(harness): inline editing for focuses, profiles, hooks, and MCP servers

Adds full editing support for the Composition section of editable harnesses
(local, git-cloned, and forked). All mutations round-trip through `ynh`
subcommands and reload the detail pane in place — no dismiss/reopen required.

Focuses:
- Add, edit (prompt + profile binding), and remove via ynh focus add/update/remove
- FocusMutator drives all three operations; HarnessFocusEditor owns the sheet lifecycle

Profiles:
- Add and remove at the harness level via ynh profile add/remove
- Full profile edit sheet: hooks, MCP servers, and includes editable inline
- HarnessProfileEditor drives mutations; reloadAndRefresh updates the open
  sheet in place so edits appear without closing

Harness-level hooks:
- Add via ynh hook add (event, command, optional matcher)
- Remove by event+index via ynh hook remove
- HarnessHookMutator + HarnessHookEditor wire the UI

Harness-level MCP servers:
- Add via ynh mcp add (command or URL type, args, env, headers)
- Remove by name via ynh mcp remove
- Reuses AddMCPSheet across harness and profile surfaces

Profile hooks, MCP servers, and includes:
- Add/remove hooks via ynh profile hook add/remove
- Add/remove/update MCP servers via ynh profile mcp add/remove/update
  (null flag supported to suppress inherited harness-level servers)
- Add/remove includes via ynh profile include add/remove using the
  unified Source Picker (Library / Git URL tabs)
- ProfileMutator handles all ynh profile subcommands

UI conventions throughout:
- plus.circle / minus.circle borderless buttons below section content
- Sheets presented at scroll-view level, not on individual buttons
- isMutating disables remove buttons during in-flight mutations
- Error surfaces via alert on each editor's errorMessage

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

* test(harness): mutator arg builders and run-state coverage

54 tests across FocusMutator, HarnessHookMutator, and ProfileMutator.

Arg builder tests (nonisolated static, no subprocess): every flag combination
including optional matcher, empty-string omission, clearProfile/clearArgs/
clearEnv/clearHeaders, null flag short-circuit, env key sort order, and
index-as-string for hook/hook-remove.

Run-state tests (MutatorStubRunner injected): success sets succeeded + clears
errorMessage, failure sets errorMessage from stderr, throwing runner sets
errorMessage, second run clears prior state, correct args forwarded to runner.

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

* docs(harness): tutorial and changelog for inline focus/profile/hook/MCP editing

Extends tutorials/harnesses.md §12 with dedicated sections for inline hook,
MCP server, profile, and focus editing, plus a new §12a covering the profile
edit sheet in detail. Updates §19 (authoring loop) to reflect the full
scaffold → includes → hooks → MCP → profiles → focuses → launch workflow.
Updates §20 (what's next) to enumerate the new editing capabilities.

Adds 7 screenshots (compressed ~5x from originals via pngquant):
harness-composition-edit-overview, harness-add-hook-sheet,
harness-add-mcp-sheet, harness-profile-card-menu, harness-focus-card-menu,
harness-focus-edit-sheet, harness-profile-edit-sheet.

CHANGELOG [Unreleased] entry covers focuses, profiles, harness-level hooks
and MCP servers, and the remove affordances for all four surfaces.

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

* fix(harness): guard empty-string profile/command/url in update arg builders

FocusMutator.buildUpdateArgs emitted --profile "" when profile was set to
an empty string with clearProfile: false — no !isEmpty guard on that branch
unlike the prompt guard directly above it.

ProfileMutator.buildMCPUpdateArgs had the same issue for both command and
url: the add builder guards !isEmpty on both, but the update builder did not,
so empty strings would emit --command "" or --url "" to the CLI.

Adds three regression tests that confirm the fixed paths omit the flags.
Fixes forEach → for-in loop in MutatorStubRunner (swift-format violation).

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

---------

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

Replaces transient terminal cards for ynh install, ynh uninstall, and
ynd export with CommandRunnerSheet-backed progress sheets. The old
approach spawned a real terminal card that auto-ran a shell command —
sheets give us structured success/failure state, a retry button on
failure, and cleaner lifecycle without the tab management overhead.

HarnessLifecycleCoordinator drops installCardIDs, uninstallCardIDs, and
handleTransientSessionExit entirely. ContentView drops the
termqDirectSessionExited notification handler and gains three .sheet
modifiers for the new flow.

Co-authored-by: David Collie <support@eyelock.net>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
@eyelock eyelock merged commit 451346a into main May 13, 2026
8 checks passed
@eyelock eyelock deleted the release/v0.11.0 branch May 13, 2026 07:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant