Ants Projects Hub
← Ants Terminal

Changelog

Ants Terminal

Every release, newest first — 100 in total.

v0.7.98-rc1Pre-release

0.7.98 RC1 — Patron preview

0.7.98 RC1 — Patron preview (public ship target: 2026-07-09)

A large Ants-MCP / Claude-integration release. Highlights:

  • Master on/off switch for the Ants MCP integration (ANTS-1901)
  • project_query — Claude runs read-only Lua sandboxed over your project, returning just the result (ANTS-2093)
  • Screen-reader support for the terminal area (AT-SPI / Orca) (ANTS-1078)
  • dry_run preview across all 9 mutating MCP verbs (ANTS-2227)
  • audit_run async:true + audit_poll for slow sweeps past the transport timeout (ANTS-3396)
  • Flatpak installs can now reach the Ants MCP server from Claude sessions (ANTS-1900)
  • roadmap / changelog / feedback verb ergonomics + verbatim MCP arg-forwarding

📄 Full changelog (all 132 changes): https://github.com/milnet01/ants-terminal/blob/v0.7.98-rc1/CHANGELOG.md

v0.7.97

0.7.97 — MCP integration briefs, project auto-detect, work-bundles

A large MCP / Claude-integration milestone. Ants now answers "where do I hook into this pipeline?" (read_region call_sequence) and "show me the canonical idiom" (similar_code include_bodies) in a single call, auto-detects projects whose code isn't under src/ and offers to write their settings file, groups related to-dos into thematic work-bundles, and keeps its most-used tools instantly callable so long sessions don't drift back to raw grep.

Highlights

  • Integration briefsread_region call_sequence:true returns the ordered steps inside a function with line anchors + the accessors a new step needs; similar_code include_bodies:true returns full matching definitions so you copy a canonical idiom in one call. (ANTS-2157 / ANTS-2156)
  • Project auto-detect — Ants notices when a project's code isn't under src/ and offers to write its .ants/project.json; new project_settings verb creates/updates it. (ANTS-2160 / ANTS-2161)
  • Work-bundlesroadmap_query mode:"bundles" groups active to-dos by theme, flagging blocked items and shipped siblings. (ANTS-1922)
  • Eager-loaded tools — the most-used MCP verbs are always loaded in Claude Code, callable instantly without a discovery step. (ANTS-2158)
  • Crash-class locks — two audit/review use-after-free crash classes are regression-locked; workspace-search no longer times out under concurrent load. (ANTS-2102 / ANTS-2144)
  • Code-navigation accuracyfile_outline stops mislabelling locals and case labels as functions and now finds old-C-style definitions; plus a sweep of related fixes. (ANTS-2159 et al.)
  • Security — block credential-bearing AI endpoint URLs and refuse cleartext-HTTP Bearer egress across the review family. (ANTS-2109 / ANTS-2108)

Full release notes: CHANGELOG.md at this tag — https://github.com/milnet01/ants-terminal/blob/v0.7.97/CHANGELOG.md

v0.7.97-rc1Pre-release

0.7.97 RC1 — Patron preview

Theme: Rolling Patron preview of the next release. Fixes and features land in this section as they ship during the freeze window; the section gets its public date when 0.7.97 is promoted to a stable release.

Full release notes: CHANGELOG.md at this tag — https://github.com/milnet01/ants-terminal/blob/v0.7.97-rc1/CHANGELOG.md

v0.7.96

0.7.96 — MCP token-frugality, teardown-crash sweep, security hardening

Theme: Ants-MCP token-frugality and roadmap / read-verb expansion, a sweep of teardown / use-after-free crash fixes, and indie-review security hardening.

Highlights

Added

  • Token-frugal MCP read verbs — read_region, apply_edits, project-wide codebase_index (ANTS-2021 / 2022 / 1637)
  • Lean-by-default MCP replies + opt-in compact:true (ANTS-2085 / 2091)
  • roadmap_log dry-run / custom stable IDs / near-duplicate advisory; changelog_log batch op; server build-identity in session_orient (ANTS-2077 / 2078 / 2043 / 2044 / 2073)

Fixed — crash & stability sweep

  • Teardown crashes/hangs on terminal-tab close, AI-review dialog close, and Lua plugin unload (ANTS-2110 / 2111 / 2117)
  • MCP/IPC idle-timer use-after-free under concurrent requests (ANTS-2026)
  • Crash when changing theme over a permission prompt (ANTS-2024)
  • Locale-dependent roadmap bundle-row sort (ANTS-2120)

Security — indie-review hardening

  • Hook-routing + MCP-wrap hardening (ANTS-1996); remote-control symlink/UID/DoS hardening (ANTS-1995); permission-prompt spoof gate (ANTS-1993); private cache dirs at 0700 (ANTS-1988); AI error-body secret scrub

See CHANGELOG.md for the full list (~85 entries). The x86_64 AppImage attaches automatically once the release workflow finishes building.

v0.7.93Pre-release

0.7.93

Theme: Auto-switcher visibility — the silent /model injection finally announces itself + the operator-trust ledger (ANTS-1893 surfacing + ANTS-1894 near-miss telemetry + ANTS-1891 honest headline + ANTS-1897 MCP cheat-sheet).

Full release notes: CHANGELOG.md at this tag — https://github.com/milnet01/ants-terminal/blob/v0.7.93/CHANGELOG.md

v0.7.93-rc3Pre-release

0.7.93 RC3 — Patron preview

Theme: Auto-switcher visibility — the silent /model injection finally announces itself + the operator-trust ledger (ANTS-1893 surfacing + ANTS-1894 near-miss telemetry + ANTS-1891 honest headline + ANTS-1897 MCP cheat-sheet).

Full release notes: CHANGELOG.md at this tag — https://github.com/milnet01/ants-terminal/blob/v0.7.93-rc3/CHANGELOG.md

v0.7.93-rc2Pre-release

0.7.93 RC2 — Patron preview

Respin of v0.7.93-rc1 with cherry-picked fixes: c6cc905

v0.7.93-rc1Pre-release

0.7.93 RC1 — Patron preview

Theme: Auto-switcher visibility — the silent /model injection finally announces itself + the operator-trust ledger (ANTS-1893 surfacing + ANTS-1894 near-miss telemetry + ANTS-1891 honest headline + ANTS-1897 MCP cheat-sheet).

Full release notes: CHANGELOG.md at this tag — https://github.com/milnet01/ants-terminal/blob/v0.7.93-rc1/CHANGELOG.md

v0.7.92Pre-release

0.7.92

Theme: MCP token-saver depth + frozen-RC release pipeline. Wires the 0.7.92 milestone (pulls 34–42 MCP/test-audit/indie-review fold-ins) and bootstraps the weekly-Wednesday + Patron-RC cadence. Public ship target 2026-05-27; the inaugural v0.7.92-rc1 is cut from these bits.

Full release notes: CHANGELOG.md at this tag — https://github.com/milnet01/ants-terminal/blob/v0.7.92/CHANGELOG.md

v0.7.92-rc1Pre-release

0.7.92 RC1 — Patron preview

Theme: MCP token-saver depth + frozen-RC release pipeline. Wires the 0.7.92 milestone (pulls 34–42 MCP/test-audit/indie-review fold-ins) and bootstraps the weekly-Wednesday + Patron-RC cadence. Public ship target 2026-05-27; the inaugural v0.7.92-rc1 is cut from these bits.

Full release notes: CHANGELOG.md at this tag — https://github.com/milnet01/ants-terminal/blob/v0.7.92-rc1/CHANGELOG.md

v0.7.91

0.7.91 — indie-review fold-in (3 CRIT + 18 HIGH inline; 30 follow-ups roadmapped)

Theme: indie-review fold-in. The 2026-05-13 multi-agent /audit

  • /indie-review sweep across 14 subsystems surfaced 3 CRITICAL, 53 HIGH, 64 MEDIUM, ~75 LOW findings. This release lands the mechanical, security, and correctness items inline; 30 substantial follow-ups (refactors, multi-file API changes, planning-required items) are folded into ROADMAP as ANTS-1260…ANTS-1317. All 422 tests passing.

Security

  • vtparser — 8-bit C1 controls now routed. Raw 0x80-0x9F bytes in the PTY stream were being silently mis-decoded as malformed UTF-8 → U+FFFD, which made the ST (0x9C) terminator branches in OscString / DcsString / ApcString / IgnoreString dead code. ECMA-48 § 5.3 / Williams VT500 state machine treat C1 as first-class controls. Fixed at vtparser.cpp:feedByte — bytes in [0x80, 0x9F] route through processChar(byte) directly, plus a Ground-state dispatch table for 0x9B (CSI), 0x9D (OSC), 0x90 (DCS), 0x9F (APC), 0x98/0x9E (Ignore). Other C1 emit Execute.

  • lua-plugins — heap cap silently bypassed. Lua 5.4 manual: when ptr == NULL, the allocator's osize parameter encodes the object type (0..8 small int), not byte count. The custom allocator treated it as a byte count, drifting m_luaMemUsage downward unboundedly on every fresh allocation — a plugin could silently exceed the documented 10 MB cap. Fixed at luaengine.cpp:luaAlloc (zero osize when ptr == nullptr).

  • roadmapdialog — HTML injection (CWE-79). rec.kind was emitted unescaped into the rendered card HTML; rxKind admits <, >, &, ", so a hostile ROADMAP.md bullet like Kind: <img src=x onerror=…> rendered as raw HTML in QTextBrowser. Fixed at roadmapdialog.cpp:1337htmlEscape(rec.kind) before emission.

  • claudeintegration — cold-start hook gate tightened. During the 1–3 s window after a tab switch (while m_transcriptPath was empty), foreign-tab PreToolUse / PostToolUse / Stop events passed the focus gate and mutated singleton state — exactly the ANTS-1161 symptom returning transiently. Now only SessionStart

    • PermissionRequest land while the path is empty; other state-mutating hooks drop with a diagnostic log (DebugLog::Claude).
  • ptyhandler — child signal mask + dispositions reset before exec. Qt / glib / dbus install handlers for SIGCHLD / SIGPIPE / SIGUSR2 etc. POSIX § 2.4 keeps SIGPIPE=SIG_IGN across exec, so child shells inherited the parent's mask and pipelines silently hung. ~Pty also now sends SIGCONT alongside SIGTERM so a stopped child doesn't burn the full 500 ms escalation budget.

  • remotecontrol — cwd validation on cmdLaunch + cmdNewTab (CWE-22 parity). Other path-bearing verbs (workspace-search, file-outline, git_state) validated their path parameter against control-bytes / backslashes; launch and new-tab did not. Same-UID model holds today, but the verb is the seam Claude Code (and any future MCP) crosses with user-supplied input — validate at the boundary, not the kernel.

  • remotecontrol — getsockopt(SO_PEERCRED) len check. On getsockopt failure with a zero-initialised cred, the disconnect log read "peer UID 0" — a phantom "root tried to connect" alarm. Now checks len == sizeof(cred) before reading cred.uid.

  • antshelper — --repo-root traversal hardening. Rejects .. substrings + NUL bytes; canonicalises before script lookup so a planted packaging/check-version-drift.sh at an attacker- controlled path can't pose as a project root.

  • lua-plugins — init.lua symlink rejection. Plugin directory was already canonicalised; the init.lua file inside wasn't symlink-checked. A symlink to /etc/passwd or ~/.ssh/id_rsa would surface the file's first line in lua_tostring(err) when the parser failed. Rejected at pluginmanager.cpp:202 with a qWarning.

  • terminalgrid — Kitty chunk buffer flush on alt-screen + resize. An attacker m=1 chunked image transfer + alt-screen flip → stale bytes prepended to the next legitimate APC m=0 from a different sender → poisoned image attributed to new sender. Fixed at the alt-enter site + at the top of TerminalGrid::resize.

  • terminalgrid — OSC 8 id= length cap (256 bytes). URI was capped at MAX_OSC8_URI_BYTES = 2048; the id was uncapped. With the parser's 10 MiB per-OSC cap, a hostile id=<10MB> sequence scaled to ~800 MiB across 80 rows of scrollback.

  • terminalgrid — Sixel first-pass payload cap (4 MiB). The vt parser caps DCS bodies at 10 MiB, but Sixel's first-pass iterator walks every byte to compute width/height with no cycle budget. A 10 MiB payload of valid sixel bytes pinned the parse thread for ~10 M iterations.

Added

  • claudebgtasks::poll() — watch-loss recovery mirror of claudetasklist::poll(). Re-adds the QFileSystemWatcher when the transcript path (re-)appears + mtime-shortcircuits to skip reparse when nothing changed. Closes the documented CLAUDE.md claim that both trackers had poll() parity (only foreground did pre-0.7.91). Wired into ClaudeStatusBarController::refreshBgTasksButton.

  • claudebgtasks::parseTranscriptisSidechain + isCompactSummary filters. Foreground tracker already filtered these (ANTS-1158 sidechain rationale + ANTS-1224 compact-summary state reset); bg-tasks did not, so subagent-launched background tasks silently inflated the parent session's running count, and post-/compact resume left pre-compact bg entries lingering.

  • m_errorHideTimer owned member on ClaudeStatusBarController — replaces the prior QTimer::singleShot pattern in setError. Re-entry now cancels the prior auto-hide instead of inheriting it (rapid back-to-back errors no longer flash and disappear). Sticky errors via autoHideMs <= 0.

  • Accessibility — setAccessibleName on three previously-silent status-bar chips. m_bgTasksBtn, m_tasksBtn, m_errorLabel now expose Orca-readable labels per docs/standards/documentation.md § 7. The remaining a11y gaps (keyboard-nav to expand cards, aria-expanded on collapse anchors) are tracked as ANTS-1277.

  • vtparser — kSafeMinSigned / kSafeMaxSigned named constants for the SSE2 safe-ASCII scan boundary (-96 / -2 were magic numbers).

  • Q_ASSERT in handleAsciiPrintRun verifying the [0x20, 0x7E] precondition the SIMD scanner establishes.

Changed

  • CLAUDE.md — three doc-drift fixes. terminalwidget is QWidget (has been since 0.7.4), not QOpenGLWidget. featurecoverage lists 2 in-process lanes + 1 shell-based (test_health runs via QProcess, not inProcessRunner). claudetasklist / claudebgtasks bullet now reflects ANTS-1246 done/total chip semantics + the new bg-tasks poll().

  • SARIF partialFingerprints key bumped to primaryLocationLineHash/v1 (SARIF v2.1.0 § 3.27.13 versioning convention).

  • computeDedup chained single-arg .arg() — closes a %n-placeholder collision vector if a file path contains a literal "%3" / "%4" substring (legal on filesystems).

  • terminalwidget — dead #include <QSurfaceFormat> removed (no QSurfaceFormat reference left in the file).

  • terminalwidget — dead duplicate loop in invalidateSpanCaches removed (lines 3472-3478 re-erased the exact key set the previous loop had just erased; was a no-op).

  • terminalwidget — unused m_perfLastPaintUs member + the overlay line that read it dropped. The member was never written; the overlay always showed Paint: 0 us.

  • terminalwidget — isCellSearchMatch early-returns on empty m_searchMatches. Called twice per cell per frame; ~16,000 wasted comparator invocations/frame at 60 fps when no search active.

  • luaengine::lua_ants_on — per-event handler cap of 64. A plugin calling ants.on(event, fn) in a loop would otherwise grow the handler list until the heap cap fired.

  • CI — build-asan job timeout 20 → 30 min. Mirrors the earlier 421e32d build-test 15 → 25 bump shape; ASan is 2-3× slower than the release build, and the 0.7.91 sweep pushed it past the prior cap.

  • ROADMAP.md — 30 follow-up cards allocated ANTS-1260… ANTS-1317 across five themes: indie-review deferred items (1260…1277), MCP renderer bug (1278), MCP orchestration consolidation (1279…1288), skill displacement + context discipline + security hardening + namespacing (1289…1298), general Claude Code workflow MCPs (1299…1312), and visibility / Anthropic-discoverability (1313…1317). .roadmap-counter: 1259 → 1317.

Fixed

  • antshelper — silent false-clean on hung script. proc.waitForFinished(60000) return value was ignored; Qt's exitStatus() returns NormalExit + exitCode() returns 0 by default on a still-running process. A hung drift script was reported as {ok:true, clean:true} exit 0. Now branches on the timeout: kills the process and returns {ok:false, error:"drift script timed out after 60s", code:"script_timeout"} with exit 1.

  • antshelper — isatty(0) short-circuit on readStdin. Interactive runs no longer block until Ctrl-D when stdin is a TTY.

  • claudeintegration — m_planModeByPid PID-reuse leak. Cache was only pruned on explicit closeTab. Linux PID-reuse is fast; an abnormally-exiting shell left a stale entry that poisoned the next tab assigned the same PID. Now also pruned on the NotRunning transition in pollClaudeProcess.

  • claudetasklist + claudebgtasks — m_lastRescanMtimeMs reset on path change. A re-bind to the same path shortly after a clear could short-circuit poll() on a matched mtime, skipping the legitimate rescan.

  • claudestatuswidgets — m_promptActive now cleared in resetForTabSwitch (was the only render-state flag left out).

  • claudestatuswidgets — empty initial label on m_tasksBtn. The prior "☰ 0/0" literal contradicted the ANTS-1246 hide predicate (total <= 0 || done >= total). The chip is hidden until first refresh anyway, but the latent inconsistency is gone.

  • claudeintegration — qWarning on the 100 MiB transcript cap. Previously returned an empty QJsonArray silently; "transcript empty / status frozen" reports now have a logging breadcrumb.

  • tests — remote_control_launch test window 3000 → 3500 chars to admit the new cwd validation block in cmdLaunch.

v0.7.90

0.7.90 — ANTS-1113 v1: /debt-sweep fold (engine + 4 MCP tools)

Theme: ANTS-1113 v1 — fold /debt-sweep mechanical scan into Ants. The four canonical post-feature drift categories (code drift, test coverage, doc drift, packaging drift) lift out of the file-reading subagent into 4 new MCP tools backed by a DebtSweepEngine namespace; per-finding triage continues to live in optional Claude calls. Estimated saving per /debt-sweep run: ~15-40 K orchestrator tokens (subagent never opens 95% of the project files; findings come back as pre-classified JSON instead of free-form prose). Qt "Debt Sweep" tab deferred to ANTS-1259 v2.

Added

  • ANTS-1113 — DebtSweepEngine helper (src/debtsweepengine.{h,cpp}, Qt::Core only). Per-detector pure functions:

    • detectStaleTypeComments(projectPath, opt) — code drift (a): walks git diff <since>..HEAD --name-only for *.cpp/h/py/js/ts/tsx, extracts comments, flags leading-cap CamelCase tokens (≥4 chars, \b([A-Z][A-Za-z0-9_]{3,})\b) absent from the project source blob.
    • detectAddedTodos(projectPath, opt) — code drift (c): unified=0 diff parser flags TODO / FIXME / XXX / HACK markers added in scope.
    • detectOrphanQUnused(projectPath, opt) — code drift (d): per-file pass over git ls-files '*.cpp' '*.h' finds Q_UNUSED(x) / (void)x; markers wrapping a variable not declared anywhere in the same file. The only v1 detector that sets autoFixable=true.
    • detectMissingInvariantTests(projectPath, opt) — test coverage: parses \bINV-([0-9][0-9a-zA-Z]*)\b from each tests/features/*/spec.md (covers INV-7 and INV-8b); reports invariants not mentioned in any sibling test_*.{cpp,py,js,go,rs}.
    • detectRoadmapShippedWithoutCommit(projectPath, opt) — doc drift (a): reads ROADMAP, runs git log --all --format=%s once, flags ✅ items whose stable ID is unmentioned.
    • detectChangelogStaleBullets(projectPath, opt) — doc drift (b): parses CHANGELOG [Unreleased] block, flags bullets citing files not in git diff <since>..HEAD.
    • runPackagingDrift(projectPath, opt) — packaging drift: wraps the existing packaging/check-version-drift.sh; parses stdout into Finding structs.
    • scanAll(projectPath, opt) — convenience: runs every enabled detector in canonical category order.
    • applyMechanicalFix(projectPath, finding) — applies one mechanical edit; returns ApplyVerdict {applied, errorCode, errorMessage} so MCP handlers can disambiguate file_changed / not_fixable / io_error no-ops without inspecting QFile errno state.
    • templateDebtSweepFoldInBlock(deferred, ids, dateIso)### 🧹 Debt-sweep fold-in (DATE) block per roadmap-format.md § 3.8 + § 3.5.3. <dateIso> byte-identical between heading and per-bullet Source: line (locked by INV-10).
    • triagePrompt(llmShaped) — pure string templating of the LLM triage prompt for the judgment-required subset. Locked by tests/features/debt_sweep_engine/ (13 tests).
  • ANTS-1113 — 4 new MCP tools registered via the consolidated registerToolProvider registry from ANTS-1253:

    • debt_sweep_scan — runs the four-category scan; returns {findings, total_findings, by_category, since_resolved} (input: optional since, optional categories subset).
    • debt_sweep_apply_fix — applies one mechanical fix in-place; ok=true even on recognised no-ops (file_changed / not_fixable); ok=false only on io_error. Re-grep guard in applyMechanicalFix § 3.9 makes the operation idempotent against re-application (locked by INV-13a).
    • debt_sweep_defer — allocates IDs via RoadmapFoldIn::allocateIds, renders the fold-in block, and atomically inserts it into ROADMAP.md when findActiveReleaseHeading succeeds. Eager ID allocation (no rollback on written:false) — same trade-off as ANTS-1111 § 2.5; envelope returns the allocated IDs so the caller can splice manually.
    • debt_sweep_triage_prompt — emits the LLM triage prompt for a caller-filtered subset of findings. All 4 follow the existing UID-scoped 0700-perms IPC trust model; schemas use additionalProperties: false. Locked by tests/features/mcp_debt_sweep_tools/ (5 tests).
  • ANTS-1259 (new ROADMAP item) — ANTS-1113 v2: AuditDialog "Debt Sweep" tab + per-finding Fix / Defer / Allow buttons + Triage with AI button (aidialog dispatch). Adds README CLI flag drift detector (needs binary execution). Spec § 1.1 of docs/specs/ANTS-1113.md documents the v1/v2 split rationale.

Changed

  • featurecoverage.{h,cpp} — three internal helpers lifted to FeatureCoverage:: public surface so DebtSweepEngine can reuse them without copying the extension list, skip-dir set, or containment-check fallback chain:
    • buildProjectSourceBlob(projectPath) — concatenates every source/ config/doc file in the project tree (per the canonical extension list) into one UTF-8 blob; skips build dirs + spec.md files.
    • existsInSource(blob, token) — substring containment + :: and . tail-fallbacks (with the same identifier-shape guards runSpecDriftCheck had inline).
    • specStopwords() — public accessor for the existing internal kSpecStopwords set. runSpecDriftCheck is rewired to call them; behaviour unchanged.

Tests

  • tests/features/debt_sweep_engine/ (13 tests) — engine pure-fn invariants (INV-3, INV-4, INV-9, INV-10, INV-11, INV-13a, INV-13b
    • scanAll include-flag honouring).
  • tests/features/mcp_debt_sweep_tools/ (5 tests) — source-grep verification of the wiring layers (tools/list, registerToolProvider, remotecontrol.h declarations, remotecontrol.cpp definitions, schema additionalProperties:false).
  • tests/features/mcp_indie_review_tools/test_mcp_indie_review_tools.cppAllSchemasUseAdditionalPropertiesFalse region boundary tightened so it doesn't drift when new tool blocks land after it.

v0.7.89

0.7.89 — ANTS-1112 v1: /indie-review fold (engine + 5 MCP tools)

[0.7.89] — 2026-05-13

Theme: ANTS-1112 v1 — fold /indie-review orchestration into Ants. The mechanical halves of the multi-agent independent review (partition, brief assembly, cross-lane corroboration, synthesis-prompt templating, ROADMAP fold-in) lift out of orchestrator context as 5 new MCP tools backed by a IndieReviewEngine namespace; per-lane review judgment + dispatch continue to live in Claude subagent calls. Estimated saving per /indie-review run: ~20-50 K orchestrator tokens. Qt dialog deferred to ANTS-1258 v2.

Added

  • ANTS-1112 — IndieReviewEngine helper (src/indiereviewengine.{h,cpp}, Qt::Core only). Six pure functions:

    • derivePartition(projectPath) — reads CLAUDE.md ## Module map (src/) via the existing SubsystemMap::cachedLanes helper, walks src/ to compute per-lane source-file lists; honours <projectPath>/.indie-review/partition.json override when present.
    • assembleBrief(projectPath, lane) — verbatim brief text for one lane: header + source bodies + ROADMAP slice + standards links. Pure file IO bounded to projectPath.
    • extractFileLineCitations(projectPath, report) — regex pass over a single review report; rejects paths that escape projectPath (defense against fabricated cites).
    • corroboratedFindings(projectPath, reports, minLanes=2) — cross-lane corroboration; (file, -1) (file-level) and (file, 42) (line-level) are distinct keys (intentional).
    • synthesisPrompt(reports, threatModelExtras) — pure string templating of the optional cross-cutting synthesis prompt.
    • templateIndieReviewFoldInBlock(actionable, ids, dateIso)### 🔍 Indie-review fold-in (DATE) block per roadmap-format.md § 3.8 + § 3.5.3.
    • assembleThreatModelExtras(projectPath) — MCP-handler helper that concatenates CLAUDE.md / SECURITY.md / .semgrep.yml under === <header> === markers. Locked by tests/features/indie_review_engine/ (13 tests).
  • ANTS-1112 — 5 new MCP tools registered via the consolidated registerToolProvider registry from ANTS-1253:

    • indie_review_partition — returns lane list (no input).
    • indie_review_brief — returns the assembled brief for one lane (input: lane).
    • indie_review_corroborate — returns cross-lane corroborated findings (input: reports map, optional min_lanes).
    • indie_review_synthesis_prompt — returns the rendered synthesis prompt (input: reports, optional include_threat_model_extras).
    • indie_review_fold_in — allocates IDs via RoadmapFoldIn::allocateIds, renders the ### 🔍 Indie-review fold-in (DATE) block, and atomically inserts it into ROADMAP.md when findActiveReleaseHeading succeeds (input: actionable array, optional date_iso, optional release_block_heading). All 5 follow the existing UID-scoped 0700-perms IPC trust model; schemas use additionalProperties: false. Locked by tests/features/mcp_indie_review_tools/ (5 tests).
  • ANTS-1258 (new ROADMAP item) — ANTS-1112 v2: Qt IndieReviewDialog that wraps the v1 engine for users who want in-app indie review without Claude orchestration. Spec § 1.1 of docs/specs/ANTS-1112.md documents the v1/v2 split rationale.

v0.7.88

0.7.88 — ANTS-1111 v1: /audit triage fold (engine layer)

[0.7.88] — 2026-05-13

Theme: ANTS-1111 v1 — fold /audit triage into the Project Audit tool's engine layer. Five mechanical pieces lift out of the LLM round-trip: cross-tool corroboration severity-tier shift, framework auto-detect, ROADMAP fold-in helper, the // audit: drop[=rule] inline-suppress alias, and the RuleQualityTracker::noisyRuleIds accessor that feeds the shift. UI affordances (Fold-into-ROADMAP button, per-finding Allow button, Since-baseline pill) deferred to ANTS-1257 v2. Cumulative payload: ~750 LoC engine + ~33 new test cases, all green. Foundation ships ahead of ANTS-1112 / 1113 (which reuse RoadmapFoldIn).

Added

  • ANTS-1111 — RoadmapFoldIn helper (src/roadmapfoldin.{h,cpp}, Qt::Core only). Three operations: allocateIds(projectPath, n) reserves N consecutive IDs from .roadmap-counter under ::flock(LOCK_EX|LOCK_NB) (5 s budget; adopts the configbackup.h pattern); insertBlock(projectPath, heading, block) performs an atomic insert immediately after a named ## heading via QSaveFile, preserving original file permissions; findActiveReleaseHeading(projectPath) returns the first (target: …)-marked heading, falling back to the first shipped release block. Returns false on heading-not-found — caller is responsible for creating the heading first. Locked by tests/features/roadmap_fold_in/ (12 tests).

  • ANTS-1111 — AuditEngine::applyCorroborationShift (src/auditengine.cpp). Severity-tier promotion when ≥ 2 distinct CheckIds cite the same (file, line) (clamped to Blocker); demotion when a single-tool finding's checkId is in noisyRules (clamped to Info). Wired into AuditDialog::renderResults after the enrichment pass; the noisyRules set comes from the new RuleQualityTracker::noisyRuleIds(fpThreshold=50, minSamples=5) accessor (src/auditrulequality.{h,cpp}). Locked by tests/features/audit_corroboration_shift/ (8 tests).

  • ANTS-1111 — AuditEngine::templateRoadmapFoldInBlock (src/auditengine.cpp). Pure-string templating of a ### 🔍 Audit fold-in (DATE) subsection per roadmap-format.md § 3.8 (subsection shape) + § 3.5 (per-bullet fields). Caller pre-allocates IDs via RoadmapFoldIn::allocateIds. Empty input → empty string. Covered by tests/features/roadmap_fold_in/Inv5TemplateShape.

  • ANTS-1111 — AuditHygiene::detectProjectFrameworks (src/audithygiene.{h,cpp}). Probes the project root for 7 framework markers (flask, django, react, vue, qt6, rust, go) by reading requirements.txt / pyproject.toml / package.json / CMakeLists.txt / Cargo.toml / go.mod / manage.py. Companion semgrepRulePacks(QStringList) maps recognised frameworks to {"--config", "p/<fw>"} argv pairs. Pure-IO, bounded to projectPath. Wiring into the live runNextCheck semgrep invocation deferred to ANTS-1257. Locked by tests/features/audit_framework_detect/ (10 tests).

  • ANTS-1111 — // audit: drop[=rule] inline-suppress alias. Shorter ergonomic form of the existing // ants-audit: disable token. Same parser code (auditdialog.cpp:2055); same semantics (bare form suppresses every rule on the line; =rule suffix targets one). Both forms coexist indefinitely. The generic audit: prefix is confined by the verb constraint (\s*drop) plus the existing rule-list parser (auditdialog.cpp:2083-2107); collision-prevention covered in spec § 2.6 + INV-12. Locked by tests/features/audit_drop_alias/ (2 tests).

Changed

  • ANTS-1111 — Widened-allowlist documentation. The pre-existing .audit_allowlist.json filter (AuditDialog::allowlisted(), auditdialog.cpp:3991) was already cross-detector — the call site lives in the main per-finding loop and matches by exact checkId equality, so any detector's rule (clazy-X, cppcheck-Y, etc.) can be allowlisted. The doc-comment at auditdialog.h:101-110 now documents this widened scope (was previously labelled grep-rule only). No behaviour change.

  • ADR-0003 (docs/decisions/0003-cc-fold-relax-gate-and-draw-boundary.md) — relaxes the ADR-0002 dec 8 gate (ANTS-1120 measurement no longer pre-requisite for the CC-fold bullets), draws the ANTS-1108 ↔ ANTS-1111 / 1113 per-surface boundary, and enumerates pre-existing scaffolding so the subsequent ANTS-1112 / 1113 specs don't re-invent it.

  • ANTS-1257 (new ROADMAP item) — ANTS-1111 v2: UI affordances (Fold-into-ROADMAP button, per-finding Allow button, Since-baseline pill, semgrep wiring). Spec § 12 of docs/specs/ANTS-1111.md documents the v1/v2 split rationale.

v0.7.87

0.7.87 — MCP token-reduction pack

[0.7.87] — 2026-05-13

Theme: MCP token-reduction pack — five new MCP tools (workspace_search, file_outline, git_state, subsystem, last_audit_summary) collapse common Bash/Read patterns into structured envelopes, plus a token-saving hook pack and a provider-registry consolidation that makes the next tool a one-line add. Cumulative session saving on a typical /indie-review + /audit workflow: ~50-100 K tokens.

Changed

  • ANTS-1253 — Consolidate MCP-tool provider registry. Replaces the 12 per-tool setXProvider/m_xProvider setter+member pairs in ClaudeIntegration (each added by ANTS-1244 / 1247-1251) with a single registerToolProvider(QString name, ToolHandler handler) surface backed by std::map<QString, ToolHandler> m_toolProviders, where ToolHandler = std::function<QString(const QJsonObject&)>. The 92-line tools/call else if (toolName == "X" && m_XProvider) chain in claudeintegration.cpp collapses to one inline branch for get_session_info (the documented carve-out — it reads ClaudeIntegration's own state, not an external delegate) plus a single m_toolProviders.find(toolName) lookup. MainWindow::setupClaudeMcpProviders now makes 12 registerToolProvider("name", lambda) calls; each lambda absorbs the dispatcher-side argument extraction + result formatting that previously lived in the per-tool dispatch case. No behaviour change. Source delta: −157 LoC across claudeintegration.{h,cpp} + mainwindow.cpp. Spec: docs/specs/ANTS-1253.md (5-loop cold-eyes pass, ship-ready). New regression test: tests/features/mcp_provider_registry/ (10 invariants source-greped, pre-fix red verified — 9/10 fail against the unrefactored tree; INV-10 carve-out preservation passes both sides).

Added

  • ANTS-1254 — last_audit_summary MCP tool. New read-only tool that opens the latest .audit_cache/audit-*.sarif and returns a compact summary: counts (error/warning/note/suppressed) plus top_findings[] sorted by SARIF level desc → confidence desc → file asc → line asc. Default top_n=5, severity_floor="warning"; server-clamps top_n to [0, 50]. Saves ~5-15 K tokens per audit consultation vs reading the HTML report (which today's flow uses). Backed by the new AuditEngine::summariseSarif(path, topN, levelFloor) pure parser + RemoteControl::cmdLastAuditSummary with a single-entry mtime-keyed cache ((path, mtime, topN, floor) 4-tuple). Latest-SARIF discovery uses lex-max filename (audit-YYYYMMDD-HHmmss.sarif is sortable at second granularity); html_path derives via extension swap, falling back to lex-max audit-*.html within ±60 s of the SARIF timestamp (the SARIF and HTML export buttons each call QDateTime::currentDateTime() independently). Severity resolution: rule-index lookup (runs[0].tool.driver.rules[].properties.severity) with fallback for foreign SARIF (error→CRITICAL, warning→MAJOR, note→INFO). Per-tab gate inherits from resolveRootCanonical(MainWindow*). Lands on the post-1253 registry (one registerToolProvider call in setupClaudeMcpProviders, no per-tool setter). Spec: docs/specs/ANTS-1254.md (5-loop cold-eyes pass, ship-ready). New regression test: tests/features/mcp_last_audit_summary/ (10 invariants — 6 parser-side against committed fixture_min.sarif + fixture_empty.sarif; 4 wiring-side via source-grep). Pre-fix red verified — summariseSarif symbol absent from stashed engine fails build.

  • ANTS-1252 — Token-saving hook pack. Five bash hooks plus tools/install-hooks.sh that nudge Claude Code toward cheaper MCP tool calls. SessionStart preamble emits a ≤ 500 B branch/ahead/last-commit summary (cap enforced via head -c). PreToolUse(Bash) veto blocks grep -r src/, git status, cat ROADMAP.md | grep etc. with reasons capped at 200 bytes, redirecting to mcp__ants__workspace_search / mcp__ants__get_git_status / mcp__ants__roadmap_query. Per- command escape hatch: trailing # ants-bypass comment, stripped before pattern match (INV-12 — never appears in the emitted reason text). PreToolUse(Read) veto blocks full reads of ROADMAP.md > 50 KiB; bypassed cleanly via offset/limit args. Stop hook backgrounds an ants-helper drift-check under flock (INV-11) with a sane-toplevel marker write (INV-9). PreCompact hook walks the transcript JSONL and writes the most recent TodoWrite snapshot to ~/.cache/ants-terminal/precompact_<sessionId>.json only if sessionId matches ^[a-zA-Z0-9_-]{1,64}$ (INV-2). Per-project gate via committed .ants-project marker — non-ants sessions exit silently in sub-millisecond stat ascent (no git rev-parse fork). Install-hooks hardening: lstat symlink abort (INV-5), cp --no-dereference backup, sentinel-key fence ants_hooks_pack_v1 rather than text-fence comments (INV-6 — jq strips JSON comments), tmpfile + jq empty validate- before-rename (INV-8), idempotent re-install, --dry-run and --uninstall flags. Token saving: ~30-100 K/week depending on which siblings shipped first (calibrated in spec § 5). Conformance harness at tests/features/hook_pack/test_hooks.sh (shell-driven per audit_self_test.sh pattern, no C++ link) covers INV-1/2/3/4/7/9/10/12 + bash-veto behaviour + read- roadmap-veto behaviour + install round-trip + symlink abort — 36 assertions, all green; INV-6/8/11 deferred to manual smoke with documented rationale. Spec: docs/specs/ANTS-1252.md. Cold-eyes review on tests/features/hook_pack/spec.md returned 7 findings (1 HIGH on source-vs-runtime semantics for INV-12, 2 MEDIUM on doc-code drift, 4 LOW); all fixed inline.

  • ANTS-1251 — subsystem MCP tool (consolidated; map / files / recent_changes via op discriminator). Pre-parses the project's CLAUDE.md ## Module map (src/) H2 into a lanes[] array and serves per-lane chunks so /indie-review reviewers don't each re-read the file. Three ops: map returns [{name, summary}, …], files returns the lexicographically-sorted src/<lane>* glob, recent_changes returns merged-by-sha git log entries across every file in the lane. Cache is mtime-only on CLAUDE.md (no wall-clock TTL — concurrent reviewers share warm cache). Defensive parser drops bullets that don't match the `name` — summary shape and splits multi-name bullets like `a` / `b` into one Lane each. Hardening: lane membership check precedes any filesystem call (closes cold-eyes S1251-1 path-traversal vector); per-result canonical-startsWith re-check on resolved files (defence in depth against malicious symlinks inside src/). Composes cmdGitState({op:"log", path:<file>}) per lane file for the recent_changes op — no duplicated git plumbing. Distinct error codes bad_op (input enum) and unknown_lane (with lanes:[…] echoed in the response so the caller can recover). Single setter / member / provider-lambda triple on ClaudeIntegration + MainWindow. Locked by feature test mcp_subsystem/ (12 invariants — decl, INV anchors, IPC dispatch, MCP tools/list schema with op enum + op in required[], MCP tools/call dispatch, header surface, mainwindow lambda, op-switch literals, error-code surfacing, cmdGitState composition, parser surface, CMake wiring, and the ≥ 15-lane CLAUDE.md parser floor). Token math: ~24 K saved per /indie-review run (6 reviewers × ~3.5 K → ~250 each); permanent schema cost ~115 tokens. Side-effect: mcp_workspace_search test INV-3c now uses a word-boundary regex (\bsystem\() instead of substring match so cmdSubsystem( is not false-flagged as a shell escape.

  • ANTS-1250 — git_state MCP tool (consolidated; status / log / diff via op discriminator). Replaces multiple Bash calls to git status, git log, git diff with a single structured tool. Cold-eyes pass 2 collapsed three originally-proposed verbs into one to save ~240 permanent schema tokens per session start; per- call savings are ~14–300 tokens depending on op. Status returns {branch, upstream, ahead, behind, files:[{path,index,worktree}], untracked[]} parsed from --porcelain=v1 -b. Log returns {commits:[{sha,subject,date,body?}], truncated} parsed from unit-separator-framed --pretty=format:, with n+1 probe to detect truncation and per-body 1 KiB cap when body:true. Diff returns {files:[{path,added,removed}], totals} parsed from --numstat, with binary files surfaced as null added/removed. Hardening: shell-less QProcess::start("git", QStringList...) via the new gitwrap.{h,cpp} synchronous helper (5 s terminate

    • 200 ms grace → kill, 4 KiB stderr cap); strict regex on range excludes leading - from any rev-component (closes cold-eyes S1250-1 flag-injection); -- argv separator before every user-derived positional arg, with ./ prefix on --leading paths; canonical-startsWith path-escape guard for path mirroring ANTS-1248's lane check; distinct error codes bad_op, bad_range, bad_path, git_failed, git_missing, not_git_repo. Locked by feature test mcp_git_state/ (13 invariants spanning decl, INV anchors, shell-lessness in gitwrap.cpp, IPC route, tools/list schema with op enum + required:["op"], tools/call dispatch, header decl + member, provider lambda, op-dispatch chain, the stricter regex literal, the 2-tier kill constants, and CMake wiring). Spec: docs/specs/ANTS-1250.md.
  • ANTS-1249 — file_outline MCP tool. Returns a compact {header_doc, symbols:[{line, kind, name, signature}], total_lines, total_bytes} envelope for a single file instead of a full Read. ~13-39× compression on typical C++ source (e.g. auditdialog.cpp 67 K tokens → ~2 K). New src/fileoutline.{h,cpp} translation unit hosts a 6-regex scanner (C++ member / type / free-func / Qt-marker + Python + Markdown headings). Each regex is a static const QRegularExpression with .optimize() invoked at first use, so the JIT compiles once per process. Possessive quantifiers

    • per-line 1024-byte cap bound the worst case against catastrophic backtracking. Header-doc capped at 2 KiB. path argument is canonicalised + NFC-normalised + checked against the project root via startsWith (same security posture as ANTS-1248's lane). Locked by feature test mcp_file_outline/ (10 invariants: wiring + runtime floor of ≥ 8 symbols against the in-tree auditdialog.cpp to catch regex-set regressions, plus not-found path). Spec: docs/specs/ANTS-1249.md.
  • ANTS-1248 — workspace_search MCP tool (ripgrep wrapper). Replaces typical Bash grep -r ... src/ patterns with a structured {ok, matches:[{file,line,text}], truncated, elapsed_ms} envelope. Shell-less argv (QProcess::start("rg", QStringList...) — never bash -c, never a single-string overload). Server clamps max_results at 500 (default 50); lane and glob NFC-normalised and rejected if they contain control chars / backslash / .. segments / parent-traversal past the project root (canonical-startsWith check via QFileInfo::canonicalFilePath()); 4 KiB stderr cap surfaced only on ok:false; 2-tier hard kill (2 s terminate() then 200 ms grace → kill()) so a catastrophic regex can't outlive the wall budget. Estimated saving ~6–15 K tokens per typical bug-investigation session at ~150 permanent schema tokens. Locked by feature test mcp_workspace_search/ whose 10 invariants cover decl, INV anchors, shell-lessness, IPC route, tools/list schema (with required: ["pattern"]), tools/call dispatch, header decl + member, provider lambda, the literal ripgrep flags (--json, --no-heading, --line-number, --max-columns, --threads), and the 2-tier kill wiring. Spec: docs/specs/ANTS-1248.md.

v0.7.86

0.7.86 — MCP integration end-to-end + inputSchema compliance

Theme: MCP integration end-to-end — Claude Code sessions inside Ants tabs can now call the in-process MCP server. Three internal remote-control verbs (roadmap_query, tab_list, get_text) are promoted to MCP tools; a Python stdio bridge (tools/mcp-bridge.py) connects Claude Code's stdio MCP client to the Ants QLocalServer Unix socket; and a Zod-compliance fix repairs the silent zero-tools failure that the bridge surfaced on its first probe. Plus a Tasks chip semantics tidy-up and an optional status filter on roadmap_query that cuts typical "what's next" queries by ~7×.

Added

  • ANTS-1244 — roadmap_query / tab_list / get_text as MCP tools. Wires three existing RemoteControl IPC verbs as MCP tools so Claude Code sessions inside Ants tabs can query terminal state via tool-call rather than Bash / Read. The handlers (cmdRoadmapQuery, cmdTabList, cmdGetText) are promoted to public on RemoteControl; MainWindow lambdas in setupClaudeMcpProviders delegate directly, sharing the existing roadmap-query mtime cache. Token saving per session-with-roadmap-need: ~110 K tokens (a 120 K Read of the 482 KiB ROADMAP.md → ~13 K of structured JSON for 397 status-emoji bullets at ~133 B/bullet measured). Per-call saving on tab_list / get_text: 30–100 tokens of bash-glue Claude no longer composes. Locked by 7-invariant source-grep test mcp_extra_tools/. Spec converged across 7 cold-eyes loops.

  • ANTS-1247 — status filter on roadmap_query MCP tool / IPC verb. Optional status argument ("all" default, "active" = 📋+🚧, "shipped" = ✅; case-insensitive) on the roadmap_query MCP tool. On this repo at ship time: "active" returns 57 of 399 bullets (~1.75 K tokens vs ~12 K), a ~7× saving per "what's next" query. Provider lambda widened to thread the filter through; cache continues to hold the full unfiltered array (filter runs post-cache). Pack-mate with ANTS-1248..1252 (spec-only) and ANTS-1253/1254 (planned follow-ups). Cold-eyes pass 2 converged across 4 lanes (performance / token reduction / security / optimisation) over 4 loops — final pass clean.

  • ANTS-1255 — MCP stdio bridge (tools/mcp-bridge.py). Unblocks the entire MCP pack on the consumer side. The Ants in-process MCP server exposes JSON-RPC over a QLocalServer Unix socket; Claude Code's MCP client speaks stdio. Until now, every server-side tool registered since ANTS-1244 was unreachable from a Claude Code session. ~100-line Python 3 stdlib script reads line-delimited JSON-RPC from stdin, opens one AF_UNIX connection per request (server is one-shot by design), forwards the request, writes the reply to stdout. Notifications (no id) are forwarded without awaiting a response. Socket selection: $ANTS_MCP_SOCKET override or newest-mtime /tmp/ants-terminal-mcp-*. Register with claude mcp add ants -- /path/to/tools/mcp-bridge.py once per machine. End-to-end verified: initialize handshake → 9 tools listed → roadmap_query status="active"bad_status error path → clean exit on stdin close.

Fixed

  • ANTS-1246 — Tasks chip progress semantics + Mode B batch reset. Two coordinated fixes for the same user-visible symptom (the bottom-right Tasks chip showing wrong or no info during an active Claude task list). (1) Chip now reads ☰ <completed>/<total> and stays visible iff 0 < done < total, closing the visibility hole left by ANTS-1221's pending-only predicate — visible end-to-end through every active run, hides cleanly at 100 %. (2) The TaskCreate path in ClaudeTaskListTracker::parseTranscript (Mode B) piled up completed tasks forever; now, when a new TaskCreate arrives and every task in out is completed, the prior batch is cleared before appending. Mode A (TodoWrite snapshot) was already correct. Partial batches (any pending/in_progress) are preserved. Locked by 4-INV tasks_chip_done_over_total/ and 3 new behavioural cases in claude_task_list/. Spec: docs/specs/ANTS-1246.md (2 cold-eyes loops, clean pass).

  • ANTS-1256 — MCP tools/list inputSchema compliance. The six zero-arg MCP tools (get_cwd, get_session_info, get_last_command, get_git_status, get_environment, tab_list) were emitted without an inputSchema field. Claude Code's MCP client validates tools/list with Zod and rejects the entire response when any entry omits the field — the connection reports "Connected" but registers zero tools. Surfaced 2026-05-12 on the first probe through the ANTS-1255 bridge: the log read tools[1].inputSchema expected object, received undefined for indices 2/3/4/5/7 as well. Fix: a shared QJsonObject emptySchema; emptySchema["type"] = "object"; sentinel, assigned to each zero-arg tool's inputSchema. Locked by feature test mcp_tools_list_schema/ whose third invariant pins tools.append() == ["inputSchema"] = count parity inside the tools/list block — so any future tool addition that omits the field breaks CI before it reaches a user.

v0.7.85

0.7.85 — Roadmap density toggle + Qt::Popup blocker fix

Theme: Roadmap dialog density toggle (compact / cozy / comfortable) — Material 3 / Linear / GitHub-style density selector that persists across sessions. Plus a regression fix that surfaces while building it: Qt::Popup widgets (combo dropdowns, menus, color pickers) opened from inside a dialog were being eaten by the ANTS-1051 pseudo-modal blocker. The fix is one short-circuit in dialogfocus::shouldSuppressEvent ForDialog and benefits every dialog that hosts a popup, not just the new density combo.

Added

  • ANTS-1238 — Roadmap dialog density toggle (compact / cozy / comfortable). Material 3 / Linear / GitHub all expose a density selector; the Roadmap dialog gains one to match. A new QComboBox lives at the trailing edge of the filterRow (after the existing addStretch(1)) with three options: compact (9/10/11/14 px text tier, proportional padding), cozy (the pre-1238 baseline — 10/11/12/13/16 px), and comfortable (12/13/14/15/18 px). Selection drives a CSS class on renderCardsHtml's root container; per-tier sizes/padding are defined in STYLE_COMPACT / STYLE_COZY / STYLE_COMFORTABLE. Persists via a new roadmap_density Config key, with graceful fallback to "cozy" on missing/invalid value and silent best-effort on persistence-write failure (matches the existing Config write convention). The default is cozy, byte-equal to the pre-1238 baseline render — INV-1 in the spec's test bundle locks that. Regression-locked by ANTS-1238-INV-1..9 in tests/features/roadmap_density/spec.md (tier-unique sentinels 9/16/18 px scoped to renderCardsHtml; combo present and wired; Config getter/setter round-trip; persistence-failure contract). No keyboard shortcut in v1 — the ?-cheatsheet (ANTS-1236) doesn't gain a row. Spec: docs/specs/ANTS-1238.md (cold-eyes-clean after 4 loops + ~50 verified findings fixed; full audit trail at ROADMAP.md ### 📝 Cold-eyes 2026-05-12 (ANTS-1238 spec)).

Fixed

  • ANTS-1051 — Qt::Popup events were swallowed by the pseudo-modal blocker. Surfaced by the ANTS-1238 density combo: clicking an option in the dropdown popup did nothing — the popup didn't even close. Root cause: a Qt::Popup window (QComboBox dropdown, QMenu context menu, color-picker popup, …) is its own top-level widget, and QWidget::isAncestorOf stops walking at window boundaries — so popup descendants opened FROM inside a dialog read as "outside the dialog's tree" and the pseudo-modal blocker (ANTS-1051) ate every mouse event on them. Fix is a one-block short-circuit at the top of dialogfocus::shouldSuppressEventForDialog: if QApplication::activePopupWidget() is non-null and target is either that popup or a descendant of it, return false immediately. Regression-locked by a new INV-2i in tests/features/dialog_pseudo_modal/spec.md — source-grep-asserted rather than behaviourally driven because the offscreen QPA platform doesn't promote Qt::Popup widgets to activePopupWidget(). Affects every dialog that hosts a combo / menu / color picker, not just the Roadmap density combo — Settings, AuditDialog, etc. all benefit transparently.

v0.7.84

0.7.84 — RoadmapDialog v2 finishing pass

Theme: RoadmapDialog v2 finishing pass — keyboard ergonomics (/-focus, Esc-clear, ?-cheatsheet), accessibility (status and theme glyph labels for screen readers), and a freshness signal ("Updated N days ago" on in-progress cards). Plus theme-aware frameless title bars + dark-theme link fixes across every dialog.

Added

  • ANTS-1237 — "Updated N days ago" line on 🚧 cards in the Roadmap dialog. Surfaces stall signal at-a-glance: every 🚧 in-progress card now shows when its bullet block was last touched, mirroring GitHub Projects / Linear / Jira's default "last activity" affordance. Derived from one git blame --line-porcelain call against ROADMAP.md (cached by mtime via the same pattern as parseShippedDates), then MAX(author-time) over each - 🚧 [ANTS-NNNN] bullet's line + its 2-space-indented continuation lines. Renders as <span class="rm-date">· Updated <X></span> inline with the ID chip, where <X> is one of: today / yesterday / Nd ago (2-13d) / Nw ago (2-8w) / Nmo ago (2-12mo) / Ny ago (≥1y). Reuses the existing .rm-date CSS class — no new style rule. Status-gated (✅ already shows shipped date; 📋/💭 don't need it). Graceful degradation on non-git checkouts: git blame failure → empty hash → no "Updated" line, mirroring the ✅-card path when shipped date is absent. ID mentions outside the bullet block (e.g. audit-trail prose in another card's body) do NOT contribute to MAX — the walker only enters block-mode at lines matching ^- 🚧 \[ANTS-NNNN\]. Cost: ~175 ms blame call on first dialog open per session against the current 8723-line ROADMAP, cached thereafter; ~4 MB transient peak for the porcelain output (verified by wc -c); ~1 KiB persistent per dialog at realistic 5-15 🚧 bullet counts. Regression- locked by ANTS-1237-INV-1..8 in tests/features/roadmap_inprogress_age/spec.md (renderer- layer + parser-layer test split; parser tests early-skip with GTEST_SKIP() when git is not on PATH). Spec: docs/specs/ANTS-1237.md (cold-eyes-clean after 4 loops + ~40 verified findings fixed; full audit trail at ROADMAP.md ### 📝 Cold-eyes 2026-05-11 (ANTS-1237 spec)).

  • ANTS-1234 — Roadmap dialog /-focus + Esc-clear + body-only auto-expand. Three keyboard-first ergonomics additions to the Roadmap dialog's existing substring search box. (1) Pressing / anywhere in the dialog focuses the search box and select-alls any existing predicate so the next keystroke replaces it (Linear / GitHub / Notion / VS Code / Slack convention). Layout-robust via event->text() == "/"; gated on !m_searchBox->hasFocus() so the user can still type / into the predicate (URLs, path tokens). (2) Pressing Esc while the search box has focus clears the predicate and removes focus — one press undoes a search, a second press closes the dialog as before. Implemented via installEventFilter(this) on the search box + a dialog-side eventFilter override that consumes the Escape KeyPress before it can reach QDialog::reject. For every non-Escape key, the filter falls through so the QLineEdit receives PgUp / F5 / typing characters normally. (3) Cards whose match lives only in the body continuation prose (not in ID / headline / Layman) auto-expand for that render so the matched substring is visible in context. Render-time only; m_expandedItems is never mutated, so clearing the search reverts every auto-expanded card to its prior user-driven state. The id:NNNN jump shortcut also auto-expands the matched card. The cheatsheet (ANTS-1236) gains a / row in lockstep — kRoadmapShortcuts[] bumps 9 → 10 rows and the static_assert + test assertions all move 9 → 10 in the same commit. Cost: +4 case-insensitive QString::contains calls per card per render (~0.3 ms on 200 cards, well below the 120 ms search debounce). Regression-locked by ANTS-1234-INV-1..9 in tests/features/roadmap_search_keybinds/spec.md. Spec: docs/specs/ANTS-1234.md (cold-eyes-clean after 7 loops + ~25 findings fixed).

  • ANTS-1236 — Keyboard-shortcut cheatsheet in the Roadmap dialog. Press ? inside the Roadmap dialog to open a sub-dialog listing every keyboard shortcut it ships today. Mechanism: a file-scope kRoadmapShortcuts[] data table in src/roadmapdialog.cpp is the single source of truth; roadmapShortcutRows() exports it to RoadmapShortcutsDialog (src/roadmapshortcutsdialog.{h,cpp}), which renders a two-column QTableWidget (Shortcut / Action). 10 shortcuts at ship after ANTS-1234 added /: ?, /, Esc, F5, Ctrl+C, Ctrl+A, ↑ ↓, PgUp PgDn, Home End, Tab Shift+Tab. The trigger uses event->text() == "?" (layout-robust — AltGr / dead-key paths on non-US layouts still hit it) and gates on !m_searchBox->hasFocus() so the user can still type ? into the substring filter. The overlay is lazy + reused (one instance via QPointer), inherits the active terminal theme through DialogChrome, and announces via setWindowTitle(tr("Roadmap Keyboard Shortcuts")). The static_assert(std::size(kRoadmapShortcuts) == 10, …) guard locks the row count to the test's exact-10 assertion so adding a future shortcut must bump the test in lockstep. Regression-locked by ANTS-1236-INV-1..8 in tests/features/roadmap_shortcuts_cheatsheet/spec.md. Spec: docs/specs/ANTS-1236.md (cold-eyes-clean after 7 loops + ~27 findings fixed).

  • ANTS-1235 — Accessible status / theme glyph labels in the Roadmap dialog. Screen readers (Orca / NVDA / VoiceOver) announce "✅" as "white heavy check mark" by default — useless as a scan cue on a several-hundred-bullet roadmap. The fix emits a short, lowercase text label inline alongside each status emoji on every card: <span class="rm-state">✅</span> <span class="rm-state-label">shipped</span> and friends. Section-header count chips gain trailing words too — ✅ 47 shipped · 🚧 2 in progress · 📋 3 planned · 💭 8 considered in place of the prior bare ✅ 47 🚧 2 …. Filter checkboxes get setAccessibleName() setters ("Show shipped items" etc.) so Orca speaks the verb-led form instead of the visible label. The visible ✅ Done checkbox label is renamed ✅ Shipped for vocabulary consistency with the rest of the roadmap-format standards (§3.3 accepts both terms). Theme glyphs (🎨/⚡/🔌/🖥/🔒/🧰/📚/📦/🐛/🔍/🧹) stay unlabelled because the heading text they prefix already labels them. Verified mechanism: HTML aria-label is dead on arrival in Qt 6 QTextBrowser (parser strips unknown attributes; QAccessibleTextInterface only reads toPlainText()), so any a11y solution must put the label in the rendered text. docs/standards/documentation.md gains a new Accessibility section documenting the Qt 6 a11y path so future contributors don't repeat the aria-label investigation. Regression-locked by INV-18..24 in tests/features/roadmap_dialog_cards/spec.md (302/302 tests pass). Spec: docs/specs/ANTS-1235.md. Cold- eyes audit trail: ROADMAP.md ### 📝 Cold-eyes 2026-05-11.

Fixed

  • ANTS-1242 — Theme-aware frameless title bar on every dialog. Fourth-pass user feedback after ANTS-1241 (2026-05-11): the Roadmap dialog's window-manager-drawn title bar still showed as the system grey, not the active terminal theme — because KWin (and GNOME / Wayland) paints server-side window decorations from the system colour scheme and ignores the application's QPalette. The main terminal window already worked around this by going frameless (Qt::FramelessWindowHint) and drawing its own TitleBar widget; dialogs did not. Fixed by introducing a shared DialogChrome helper (src/dialogchrome.{h,cpp}) that any dialog ctor can invoke with one line: auto chrome = DialogChrome::install(this, themeName); — it sets the frameless flag, prepends a themed TitleBar widget, wires close / minimize / maximize signals, and returns a content QWidget to use as the layout parent for the rest of the ctor. Applied to all ten QDialog subclasses: RoadmapDialog, AuditDialog, SettingsDialog, SshDialog, AiDialog, ClaudeAllowlistDialog, ClaudeTranscriptDialog, ClaudeProjectsDialog, ClaudeBgTasksDialog, and ClaudeTaskListDialog. MainWindow::applyTheme now also calls DialogChrome::setActiveTheme(name) so dialogs that don't receive a theme name still pick up the current one. Live-theme refresh is preserved on the dialogs that previously supported it (Roadmap, bg tasks, task list) — the helper's applyTheme(dlg, bar, name) re-styles both the dialog palette and the title bar.
  • ANTS-1241 — RoadmapDialog v2: inline #NNNN on summary row + larger ID font. Third-pass user feedback after ANTS-1240 (2026-05-11): the card's #NNNN (and shipped date) were emitted on a separate <div class="rm-meta"> row below the summary line, which (a) hit the same Qt nested-block QPalette::Base frame issue that ANTS-1240 fixed on the expanded body — painting a darker band under the ID on dark themes — and (b) was visually noisy. Fixed by emitting the hashed ID and shipped date as inline <span> children of the card div, on the summary row immediately before the rm-toggle anchor. CSS .rm-id font bumped from the old 10 px meta size to 12 px so the number is scannable at a glance. The .rm-meta CSS rule is gone. Regression-locked by spec INV-17 in tests/features/roadmap_dialog_cards/spec.md.
  • ANTS-1240 — RoadmapDialog v2: theme palette + expanded-body background frame. Second-pass user feedback after the ANTS-1239 link-colour fix (2026-05-11):
    • Dialog doesn't pick up the active theme. Only the QTextBrowser's QPalette::Link / LinkVisited were themed in ANTS-1239; the QDialog window, the QListWidget TOC sidebar, and the viewer's Base / Text roles all fell through to Qt's default dark palette. So the dialog looked greyer than the rest of the app (e.g. against the Tokyo Night / Dracula / One Dark navy backgrounds). Fixed by applying bgPrimary to QPalette::Window / Base on all three widgets, textPrimary to WindowText/Text, and accent to the TOC's Highlight so the selection indicator stays visible.
    • Expanded body has a different background colour from the card. Qt's text engine renders nested <div> block elements with their own QPalette::Base background frame — it does not visually inherit from the parent <div>'s background CSS. So the renderer's <div class="rm-body"> wrapper inside each <div class="rm-card"> painted bgPrimary over the card's bgSecondary background, creating a visible colour break at the divider. Fixed by emitting body <p> paragraphs directly as children of the card (no wrapping div) — <p> doesn't create a separate background frame, so each paragraph paints over the card's bgSecondary and the visual continuity is restored. First body paragraph carries class="rm-body-first" (dotted divider + extra padding-top); subsequent lines carry class="rm-body-line" (indent only). Regression-locked by spec INV-15 + INV-16 in tests/features/roadmap_dialog_cards/spec.md.
  • ANTS-1239 — RoadmapDialog v2: duplicate heading slugs + black <a> text on dark themes. Two bugs surfaced by user test-drive of 0.7.83's card renderer:
    • Slug collision. Three ### Performance h3s (under 0.7.0, 0.8.0, and Beyond 1.0) all slugged to performance, so expanding one expanded all three, and bySection["performance"] pooled bullets from every Performance section into a single bucket. Fixed by tracking a seen set across each walk and appending -2, -3, … to repeat slugs (uniqueSlug helper). Both parseBullets and renderCardsHtml walk the same sourceText in the same order, so their slug sequences agree and bySection[slug] keys match the URLs emitted into the click anchors. Same fix covers other duplicate h3s in the file (Platform, Cross-cutting themes, Security, Tier 2 — hardening sweep, …).
    • Black section titles on dark themes. Qt's text engine paints <a> foreground using the widget's QPalette::Link role, ignoring the inline <style> block's a{color:…} rule and not propagating through color:inherit. On Qt 6's default palette that role renders as near-black on most dark themes, so the chevron + heading text in each section header disappeared. Fixed by setting QPalette::Link / QPalette::LinkVisited on the QTextBrowser to the active theme's textPrimary, plus emitting explicit color:<textPrimary> on .rm-section-toggle / .rm-section-title (replacing the prior color:inherit) as belt-and-braces. Regression-locked by spec INV-13/14 in tests/features/roadmap_dialog_cards/spec.md.

v0.7.81

0.7.81 — tasks-chip semantics polish + About-dialog build context

Theme: small follow-ups to the 0.7.80 user-feedback batch — a two-fix bundle that finishes the Tasks-chip semantics arc started in ANTS-1216, plus a long-requested About-dialog enhancement that surfaces enough build context to triage bug reports without a "what build are you on?" round-trip.

Added

  • ANTS-1222 — Help → About Ants Terminal… now shows a Build line. The About box used to list only the version and the Qt runtime; bug reports often needed a follow-up "what build are you on?" round-trip. The dialog now also displays a Build: line with the build date, build type (Release/Debug/RelWithDebInfo), short git commit (or unknown for tarball builds), and the compiler used (GCC or Clang with version). Implementation: a CMake configure_file() block at the top of CMakeLists.txt writes build/generated/build_info.h at configure time; the compiler ID is detected at compile time inside aboutdialogs.cpp so it reflects the actual toolchain (env-var overrides, distcc, distro alternative chains) rather than what CMake found. Reconfigure (cmake build/) to refresh the SHA between commits.

Changed

  • ANTS-1218 + ANTS-1221 — Tasks chip now shows progress count and hides at 100%. The bottom-right ☰ X/Y chip used to mean "X tasks left out of Y" (counted down) and stayed lit while a single in-flight Claude task remained. It now means "X done out of Y" (counts up, like every other progress display in the app) and disappears as soon as nothing is waiting on you. Two bundled changes: (1) ClaudeTaskListTracker::unfinishedCount() now counts only pending tasks — a task Claude is actively working on is Claude's problem, not yours, so it no longer keeps the chip visible. (2) The chip's numerator is now total - unfinished instead of unfinished, so X/Y reads like a GitHub-PR-checks progress bar. Pre-existing unfinished <= 0 hide branch (from ANTS-1216) still fires — combined effect: chip cleanly hides at 100%, monotonically increases as tasks complete, and never surfaces a single in-flight task as actionable.

CI is now building the AppImage and other release artifacts via release.yml; they'll attach to this release page once the workflow completes (typically a few minutes on the public-repo Linux runners).

v0.7.75

0.7.75 — DEC mode 2026 sync output atomicity fix (ANTS-1148)

[0.7.75] — 2026-05-02

Theme: ANTS-1148 — DEC private mode 2026 (Synchronized Output / BSU/ESU) atomicity fix. Pre-fix, onVtBatch suppressed only its own update() during BSU but left the live grid mutated by processAction; any other paint trigger (blinkCursor, focusIn/Out, hover, selection, visual-bell flash) would call paintEvent which read the live half-applied state and leak mid- sync content to screen. The fix unifies sync output onto the existing 0.6.33 frozen-screen snapshot machinery: pre-scan a VtBatch for CSI ?2026h, capture the snapshot before the processAction loop, route paintEvent's cursor reads through new effectiveCursorRow/Col() accessors, and clear on ESU or via the 500 ms safety timer. Single TU touched (terminalwidget.{cpp,h}), behavioural-correctness only — no LoC delta on mainwindow.cpp. 132/132 ctests pass.

Fixed

  • ANTS-1148 — Synchronized Output (DEC mode 2026) is now atomic against non-batch paint triggers. Pre-fix, BSU (CSI ?2026h) suppressed onVtBatch's trailing update() but did nothing about the live grid mutation underneath: any paintEvent driven by blinkCursor (550 ms cadence), focusInEvent / focusOutEvent, mouse-hover OSC 8 hyperlink rollover, selection drag, or visual-bell flash would re-render from the half-applied grid mid-BSU and leak partial state — exactly the tearing the protocol exists to prevent. Fix folds sync output onto the existing 0.6.33 frozen-screen snapshot path: a new file-scope helper batchEntersSyncOutput pre-scans the VtBatch for CSI ?2026h (CsiDispatch + intermediate ? + finalChar h + 2026 in params), and onVtBatch captures the snapshot under (m_syncOutputActive || batchEntersSyncOutput(*batch)) && m_frozenScreenRows.empty() before processAction runs. The left disjunct handles eviction-recovery — resize / RIS / alt- screen toggle clear the snapshot mid-sync, and we need to re-capture for the rest of the block. Cleanup runs at end-of- batch when !m_syncOutputActive && m_scrollOffset == 0 && !m_frozenScreenRows.empty() (drops the prior wasSync && gate that stranded same-batch BSU+ESU snapshots — cold-eyes C2); the 500 ms safety timer also clears on force-end. paintEvent's three cursor-render sites (cursor draw, under-cursor glyph, autocomplete ghost) route through new effectiveCursorRow/Col() inline accessors that read m_frozenCursorRow/Col when m_frozenScreenRows is non-empty and live otherwise; the eight typing / IME / semantic-output cursor reads (keyPressEvent, inputMethodQuery, clickToMoveCursor, findMatchingBracket, toggleFoldAtCursor, toggleBookmark, updateSuggestion, lastCommandOutput) intentionally keep direct m_grid->cursorRow/Col() reads — those paths want the real cursor, not the rendered one. Spec at docs/specs/ANTS-1148.md cold-eyes-reviewed before code (10 findings folded — 2 CRITICAL, 3 HIGH, 3 MEDIUM, 2 LOW); new feature test at tests/features/sync_output_snapshot/ locks 9 INVs by source-grep. Out-of-snapshot scope (intentional, matches xterm/foot): cursor visibility / shape / blink (DECSCUSR, DECTCEM) and inline images (Sixel / Kitty / iTerm2) read live mid-BSU.

  • ANTS-1148 — updateScrollBar's frozen-snapshot predicate extended to cover sync output. Indie-review HIGH on commit 9674e5a caught a missed call site: updateScrollBar at terminalwidget.cpp:2793 still used the pre-1148 predicate wantFrozen = (m_scrollOffset > 0). Closes spec transition row 7 — scrolled-back-during-sync, user scrolls back to bottom while BSU is still active: without the sync clause, the snapshot is evicted and the next paint reads the half-applied grid. Fix extends to (m_scrollOffset > 0) || m_syncOutputActive, matching the unified-predicate pattern already in use at the other three call sites (onVtBatch pre-scan, end-of-loop cleanup, safety-timer slot). Test gains INV-4b locking the disjunction at this third site (74395a6).

Changed

  • blinkCursor partial-update rect routes through effectiveCursorRow/Col(). Pre-fix the blink invalidation rect was computed from m_grid->cursorRow/Col() directly; under sync the live cursor position can have advanced past the frozen-rendered cell, so the partial-update rect wouldn't cover the cell paintEvent actually draws and the cursor would visually stall mid-BSU. Routing through the centralised accessor locks the invalidation rect to the rendered cell — a small consistency win that drops out of the snapshot unification rather than a separate fix.

v0.7.74

0.7.74 — Bundle G Tier 3 closeout (mainwindow.cpp -785 LoC)

[0.7.74] — 2026-05-02

Theme: Bundle G Tier 3 carve-out — second and third of three L6 LoC decompositions of src/mainwindow.cpp. Ships the ClaudeStatusBarController extraction (ANTS-1146) and the themedstylesheet helper extraction (ANTS-1147), together cutting mainwindow.cpp from 6249 → 5464 LoC (-785, -12.6 %) — the largest single-bundle reduction in the file's recent history. Adds three roadmap items (ANTS-1155 / 1156 / 1157) covering the in-app self-update gap, a roadmap-system audit, and the Project Audit tool flesh-out. Closes with a post-bundle debt sweep (5 trivials + 3 behaviorals). 131/131 ctests pass.

Changed

  • ANTS-1146 — ClaudeStatusBarController extracted from mainwindow.cpp. L6 LoC2 carve-out. Three contiguous Claude-status sections — applyClaudeStatusLabel, updateClaudeThemeColors, setupClaudeIntegration (~680 LoC combined, including the ~120-LoC permission-button factory) — moved to a top-level ClaudeStatusBarController : QObject in new translation unit src/claudestatuswidgets.{cpp,h}. State booleans (m_claudePromptActive, m_claudePlanMode, m_claudeAuditing, m_claudeLastState, m_claudeLastDetail) and the six widgets they drive (m_claudeStatusLabel, m_claudeContextBar, m_claudeReviewBtn, m_claudeErrorLabel, m_claudeBgTasks, m_claudeBgTasksBtn) all migrate; service objects (m_claudeIntegration, m_claudeTabTracker) stay on MainWindow and are observed via attach(). Coupling shape: six signals out (reviewClicked, bgTasksClicked, allowlistRequested, reviewButtonShouldRefresh, statusMessageRequested, statusMessageCleared) plus four std::function providers in (current/focused/at-tab terminal

    • tab-indicator-enabled toggle). Three orphans that lived in setupClaudeIntegration for historical convenience but aren't Claude chrome (Roadmap button, update-available QAction, 5 s startup update-check singleShot) stay on MainWindow under the renamed setupStatusBarChrome host. mainwindow.cpp: 6249 → 5656 LoC (-593). Spec at docs/specs/ANTS-1146.md cold-eyes-reviewed before implementation; new feature test at tests/features/claude_statusbar_extraction/ locks 9 INVs; three pre-existing tests (claude_bg_tasks_button, claude_state_dot_palette, allowlist_add) re-pointed at claudestatuswidgets.cpp per the spec's INV-9 table.
  • ANTS-1147 — themedstylesheet helpers extracted from mainwindow.cpp + cache-and-compare branch chip. L6 LoC3 carve-out. applyTheme's ~178 LoC inline QSS body and four per-widget restyle templates moved to pure-function builders in new translation unit src/themedstylesheet.{cpp,h}: six public helpers (buildAppStylesheet, buildMenuBarStylesheet, buildStatusMessageStylesheet, buildStatusProcessStylesheet, buildGitSeparatorStylesheet, buildChipStylesheet). The chip QSS template that was inlined three times (applyTheme, updateStatusBar, refreshRepoVisibility) collapses onto one parameterised helper; margin asymmetry preserved byte-for-byte (unit-free 0 vs <N>px). mainwindow.cpp: 5656 → 5464 LoC (-192); themedstylesheet.cpp: 272 LoC. New feature test at tests/features/themedstylesheet_extraction/ locks 8 INVs; three pre-existing tests (menubar_hover_stylesheet, tab_close_button_visible, review_changes_clickable) re-pointed at themedstylesheet.cpp.

Performance

  • ANTS-1147 — branch-chip restyle is now cache-and-compare. updateStatusBar runs every 2 s on the status timer; pre-fix code rebuilt and re-applied the branch-chip QSS unconditionally on every tick even when nothing changed. Two new private members on MainWindow (m_lastBranchChipQss + m_lastBranchChipValid) cache the last-applied stylesheet; the tick now computes the new QSS via themedstylesheet::buildChipStylesheet and only calls setStyleSheet when it differs. applyTheme invalidates the cache on theme change so the next tick re-applies. User-visible only as a small CPU reduction in the steady-state status-tick path; the optimisation was the roadmap entry's stated motivation alongside the extraction.

Documentation

  • ANTS-1155 added to 0.8.0 — true in-app self-update (no AppImageUpdate dep). Today's "Update" click in handleUpdateClicked is in-place auto-update only when the user already has AppImageUpdate (GUI) or appimageupdatetool (CLI) on $PATH AND is running the AppImage build; every other path falls through to QDesktopServices::openUrl — a glorified browser link. User feedback 2026-05-02: "the terminal must download and apply the update directly, no link." Plan: download full AppImage via QNetworkAccessManager, SHA-256 verify against the *.sha256 artefact already published by release.yml, atomic rename(2) over $APPIMAGE, restart-with-session-preservation. Three-bucket distribution-channel contract (ANTS_BUILD_CHANNEL = appimage / distro / source): distro-packaged builds suppress the notifier structurally — Flatpak / RPM / .deb / Arch users update via their package manager, not via the app. Placed in 📦 Distribution readiness between H7 (website) and H13 (outreach launch) — auto-update lands before outreach so the wave of new users actually stays on latest.

  • ANTS-1156 added to 0.8.0 — Roadmap-system audit (split / tag / integrate / display / number / write). User ask 2026-05-02: "We need to iron out how the roadmap is going to work" — six concrete questions. Three already covered (defer to ANTS-1154 tagging, ANTS-1139 summary-table renderer, and docs/standards/roadmap-format.md § 3.5.1 numbering); three genuinely open (file-splitting strategy as ROADMAP.md approaches the § 3.9 archive-rotation threshold; Ants Terminal ↔ roadmap integration via Help-menu access + cross-roadmap navigation + two-pane reading mode; Claude Code ↔ roadmap integration via auto-load, fold-in of /audit and /debt-sweep findings, and a roadmap MCP capability). Six child deliverables (1156-A through 1156-F) enumerated to spin out once decisions land. Sequence dependency: ANTS-1154 (format v2 tagging) lands first since 1156's child items consume v2 tags. Placed at the lead of 🧰 Dev experience.

  • ANTS-1157 added to 0.8.0 — Project Audit tool flesh-out (cross-skill / cross-project history). User ask 2026-05-02: flesh out the Project Audit concept by analysing every /audit, /indie-review, and /debt-sweep run across all projects. Six capabilities sequenced smallest-first: (1) run- history persistence as dated SARIF v2.1.0 files in ~/.local/share/ants-terminal/audit-history/<slug>/; (2) per-finding fingerprint + recurrence detection via SARIF partialFingerprints (§ 3.36); (3) MTTR / open-finding-age tracking + a new History tab in AuditDialog; (4) cross-skill correlation lane for findings flagged by both static analysis and cold-eyes review; (5) Projects tab with per-project summary metrics modeled on Codacy/SonarQube; (6) roadmap- fold-in audit trail (raw → ANTS-N → status flip → CHANGELOG) integrating with ANTS-1117 IPC verbs and ANTS-1154 tagged-text. Online research synthesised from SARIF 2.1.0 (OASIS), DefectDojo v2.55.1, Microsoft sarif-tools, SARIF Visualizer, Codacy, and SonarQube. Placed at the lead of 🧰 Dev experience above ANTS-1156.

  • Post-bundle debt sweep — 5 trivials + 3 behaviorals. Trivials: stale comment in mainwindow.h:4 referencing the pre-1146 applyClaudeStatusLabel rewritten to point at the controller; refreshStatusBarForActiveTab doc-comment at mainwindow.h:404 updated similarly; unused <QApplication> and <QDir> includes dropped from claudestatuswidgets.cpp; tests/features/claude_bg_tasks_button/spec.md and tests/features/claude_state_dot_palette/spec.md INV prose updated to match the post-1146 controller-routed call shapes. Behaviorals (user sign-off 2026-05-02): deleted write-only m_lastBranchChipPrimary + m_lastBranchChipTheme cache members from ANTS-1147 (the QSS string itself encodes the theme × primary × margin triple, so per-flag fields were YAGNI redundancy); added "verified externally via git diff --stat; this in-process test asserts only the floor on the new TU" disclaimer to the two-sided LoC anchors in both docs/specs/ANTS-1146.md (INV-8) and docs/specs/ANTS-1147.md (INV-7) so spec text matches what the source-grep harnesses can actually enforce.

v0.7.56

0.7.56 — App-Build suite alignment

Theme: App-Build suite alignment — confirm-on-close for tabs running non-shell processes, four-doc shareable standards bundle synced to the user-level /start-app template, CI-blocking metainfo escape fix, and the strategic roadmap for incorporating the App-Build workflow natively into Ants Terminal so users can run as much of it as possible without spending Claude tokens.

Added

  • Confirm-on-close for tabs running non-shell processes (ANTS-1102). Closing a tab whose shell has any non-shell descendant (vim, top, claude, tail -f, ...) shows a Wayland-correct confirmation dialog naming the process. "Don't ask again" checkbox flips Config::confirmCloseWithProcesses to false for subsequent closes. Default on. Settings UI in the Terminal tab. Default safe-shell allowlist: bash, zsh, fish, sh, ksh, dash, ash, tcsh, csh, mksh, yash. Probe walks /proc/<pid>/task/<pid>/children transitively (cap 256 visited PIDs) — Linux-only, mirrors the existing ClaudeIntegration descendant probe. Locked by tests/features/confirm_close_with_processes/ (11 invariants).
  • Reopen-closed-tab via Ctrl+Shift+Z — already shipped pre-request, surfaced as ANTS-1101 ✅ during the 1102 implementation pass. Existing m_closedTabs deque (cap 10)
    • File → Undo Close Tab now dual-references the original closeTab refactor (push lives in performTabClose).
  • Four-document shareable standards bundle at docs/standards/ (coding · documentation · testing · commits) plus an index README, with the detailed ROADMAP and CHANGELOG format spec carried as a sub-spec at docs/standards/roadmap-format.md. Commits standard mandates <ID>: <description> subjects so every commit links back to a ROADMAP item; testing standard mandates TDD by default (ANTS-1055, ANTS-1104).
  • ADR scaffolding at docs/decisions/ with 0001-record-architecture-decisions.md (Michael Nygard's format) plus per-folder README.
  • docs/specs/ and docs/journal/ placeholder folders for per-feature spec drafts and per-phase outcomes.
  • New Project standards section in CLAUDE.md pointing at the five standards files and the /start-app, /app-workflow, /close-phase skills (ANTS-1104).
  • Roadmap for incorporating the App-Build workflow natively into Ants Terminal (ANTS-1108) so the mechanical 70 % of the per-phase 9-step loop (ID allocation, /audit, ctest, drift checks, /debt-sweep, /bump, /release, fold-in templating, atomic CHANGELOG/ROADMAP edits) runs in C++ with zero LLM round-trips. Three supporting roadmap items: ANTS-1106 (mandatory Kind: + viewer faceted categorisation), ANTS-1107 (adopt App-Build documentation folder structure: glossary.md, known-issues.md, audit-allowlist.md, ideas.md, design.md, .claude/workflow.md), ANTS-1109 (status-bar git-branch chip restyle to match the Public/Private repo pill).

Changed

  • ROADMAP masthead now points to docs/standards/roadmap-format.md for the format spec (was docs/ROADMAP_FORMAT.md originally; briefly lived inline in documentation.md § 3 before being extracted as a sub-spec for token efficiency under ANTS-1104).
  • docs/standards/ now byte-identical to the user-level /start-app template at ~/.claude/skills/app-workflow/templates/docs/standards/, with per-language idiom examples and push-policy details delegated to the global ~/.claude/CLAUDE.md rather than duplicated in the project standards (ANTS-1104).
  • README.md project-structure tree adds docs/standards/, docs/decisions/, docs/specs/, docs/journal/; flags STANDARDS.md / RULES.md as deprecated (retire under ANTS-1105 with explicit user confirmation).
  • ROADMAP dialog redesign refined per user feedback — tabs now Full / History / Current / Next / Far Future (rename of All/Completed/Outstanding), search field above the TOC, default size bumped to ~1200x800 with persisted geometry (ANTS-1100).

Fixed

  • CHANGELOG.md was missing a top-level [Unreleased] block that the Roadmap dialog reads for current-work signaling (roadmap-format.md § 4.1 mandates one always, even empty).
  • Unescaped & in the 0.7.55 metainfo <release> body broke appstreamcli validate and turned CI red on every commit since the release. Now &amp;. (ANTS-1099)

v0.7.55

0.7.55 — VT parser correctness + audit hardening

Theme: VT parser correctness + audit-dialog hardening from the 2026-04-27 indie-review. Six fixes — multi-row OSC 8 hyperlink span emission, ITU/ECMA-48 colon-RGB form parsing, audit comment-suppress hyphen handling, trend-snapshot dedup, bg-tasks liveness sweep split from full reparse, and a hot-path returnByReference fix.

Fixed

  • Multi-row OSC 8 hyperlink spans miscoded. Wrapped hyperlinks stored only a single mis-shaped span on the start row; now emits per-row spans on close. Pre-0.7.55 shape preserved for single-row spans + the resize-pushed-text-to-scrollback edge case.
  • ITU/ECMA-48 colon-RGB form 38:2::r:g:b dropped a channel. Parser now detects the 4-slot colon group (colorspace + R + G + B) and reads R/G/B from the right offsets. Legacy 38;2;r;g;b and 3-slot 38:2:r:g:b continue unchanged.
  • Audit comment-suppress regex mis-handled hyphenated rule IDs. // nosemgrep: bash-c-non-literal matched only bash because the terminator class included -. Now [)\]]|$.
  • Trend snapshot duplicated on every UI filter click. Was inside renderResults which fires on every severity-pill toggle. Now gated by m_snapshotPersisted, reset in runAudit().
  • bg-tasks: split liveness sweep from full transcript reparse. New sweepLiveness() walks m_tasks via stat()+mtime check; the 2 s status-timer drives it instead of the 16 MiB transcript reparse. File watcher continues to drive full rescan() on transcript-changed.
  • ClaudeBgTaskTracker::tasks() returned by value on hot path. cppcheck returnByReference. Now const &.

Tests

  • ctest 112/112 green.

v0.7.54

0.7.54 — A11y + UX bundle (indie-review fold-in)

Theme: A11y + UX bundle from the 2026-04-27 indie-review. Six fixes — accessibility plumbing on the status bar + ToggleSwitch, plan-mode persistence across tab switches, an SSE iteration cap to prevent UI freeze on misbehaving AI endpoints, and IPv6 support in the SSH Quick Connect parser.

Changed

  • A11y — status-bar QLabels gain setAccessibleName. Branch chip, repo visibility chip, foreground-process label, status-message slot, Claude session label, Claude context bar, Review Changes button. Screen readers (Orca / NVDA / VoiceOver) now announce each chrome widget with semantic text instead of raw tab content or Powerline glyph codepoints. The Claude session label additionally updates accessibleDescription on every state transition so the screen reader announces the current state.
  • A11y — ToggleSwitch accessibility plumbing. Default accessibleName + QAccessibleStateChangeEvent on every check-state flip (checkStateSet / nextCheckState). Keyboard users now get immediate AT-SPI / UIAutomation confirmation when their toggle lands.
  • Plan-mode survives tab switch. Caches per-pid plan-mode state in m_planModeByPid and restores it on tab switch back, so the indicator no longer flickers off→on when the transcript-tail window doesn't include the latched permission-mode event.
  • AI SSE parser caps per-tick iterations + re-arms via singleShot(0). Caps at 256 lines per tick (covers ~8 seconds of legitimate streaming output before yielding); pathological bursts yield to the event loop. Prevents UI freeze on misbehaving endpoints.
  • SSH Quick Connect parses IPv6 host literals. [2001:db8::1]:2222 no longer mis-parses as host=[2001 port=0. Matches RFC 3986 §3.2.2 / OpenSSH command-line convention.

Tests

  • ctest 112/112 green.

v0.7.53

0.7.53 — Tier-1 remainders + VT/paste/plugin hardening

Theme: Tier-1 remainders + VT/paste/plugin hardening from the 2026-04-27 indie-review. Six security-class fixes — three completing the Tier-1 ship-this-week list, three from Tier-2 paste/image hardening.

Fixed

  • HIGH — VT parser dispatched OSC's trailing ESC byte as a real ESC sequence. A crafted OSC ending in ESC c triggered RIS (full terminal reset); ESC D triggered IND; ESC 7 / ESC 8 triggered DECSC/DECRC. RCE-adjacent because OSC payloads can arrive from a hostile remote shell, trigger-rule expansion, or pasted content. Fixed at the C0 pre-handler — string-state ESC no longer transitions to Escape; new OscStringEsc / DcsStringEsc / ApcStringEsc / IgnoreStringEsc peek-states consume the trailing byte and return to Ground. Locked by new tests/features/vt_osc_esc_discard/ (5 invariants × 11 sub-cases).
  • HIGH — X10 mouse byte > 0xDF corrupted UTF-8 stream. Coordinates encoded as col + 32 produced bytes ≥ 0xE0 when col exceeded 223; apps reading the emit stream mis-framed subsequent click bytes as UTF-8 continuations. Both wheel-event + mousePressEvent SGR fallback now clamp col/row to 223 in X10 mode.
  • HIGH — Lua plugin permission allow-list + intersect missing. Manifest entries are now filtered against knownPermissions() (clipboard.write, settings); prompt-result is intersected with requested set before reaching the engine.
  • HIGH — Image-paste filename injection + path escape. m_imagePasteDir canonicalised + required under $HOME; UUID4 suffix on filename to prevent millisecond-collision clobbers.
  • HIGH — Paste preview split on LF only — bare \r terminator spoofed dialog. Preview now normalises \r\n and bare \r to \n for display; actual paste still writes original bytes verbatim.
  • HIGH — Bracketed-paste 8-bit C1 form \x9B[200~ not stripped. Now strips 7-bit (\x1B[), raw 8-bit (\x9B), and UTF-8-encoded 8-bit (\xC2\x9B) forms.

Tests

  • New tests/features/vt_osc_esc_discard/ — 5 invariants × 11 sub-cases.
  • ctest 112/112 green.

v0.7.52

0.7.52 — indie-review tier-1 sweep (data-loss + Wayland + secret-leak)

Theme: First Tier-1 sweep of the 2026-04-27 indie-review findings. Eight fixes covering CRITICAL data-loss / security regressions plus HIGH security-class hardening — SessionManager silent scrollback loss, the same Wayland modal-grab pattern from 0.7.50 lurking in the update-confirmation dialog, SARIF/HTML reports leaking secrets at 0644, and five smaller HIGH items. The user-reported GitHub repo-type chip regression is also resolved (verified visible).

Fixed

  • CRITICAL — SessionManager silent data loss. saveSession + saveTabOrder used QFile::rename, which on POSIX refuses to overwrite an existing destination. Every session save AFTER the first silently failed: the .dat file held the original snapshot, .dat.tmp accumulated each new write, user scrollback never updated past the first save. Switched both to std::rename (POSIX rename(2) — atomic replace), mirroring the 0.7.12 Config fix. Errno logged on failure; orphaned .tmp removed.
  • CRITICAL — Update-confirmation dialog same Wayland modal-grab regression as 0.7.50 About fix. Was QMessageBox box(this); box.exec() — exactly the QTBUG-79126 / QTBUG-90005 click-drop pattern. Converted to the proven non-modal QDialog + plain QPushButton + clicked()→close() shape.
  • CRITICAL — SARIF / HTML export not atomic + 0644 perms. The reports embed every finding the audit ran, including any leaked secret surfaced by gitleaks / secrets_scan rules. Switched to QSaveFile + commit() + setOwnerOnlyPerms (0600).
  • HIGH — new-tab / launch IPC commands bypassed the send-text C0 filter. Both now route through filterControlChars by default; raw: true opt-out preserved.
  • HIGH — OSC 8 file:// and ftp:// schemes removed from the hyperlink allowlist. xdg-open file:///foo.desktop autoexecutes via the desktop's .desktop handler — RCE-adjacent. Allowlist now covers only http, https, mailto.
  • HIGH — SSH extraArgs quote-bypass on the dangerous--o allowlist. Switched to QProcess::splitCommand for proper POSIX shell quoting.
  • HIGH — extractCwdFromTranscript unbounded readLine. Caps at 64 KiB.
  • HIGH — AI endpoint scheme allowlist (http/https only). Rejects file:// / gopher:// / bare-host schemes up-front.
  • HIGH — openFileAtPath argv-injection via attacker-controlled paths starting with -. Now prepends ./ for combined-path branches and inserts -- for separate-path branches.

Resolved (user reports, 2026-04-27)

  • GitHub Public/Private repo-type chip not showing. Confirmed visible per user screenshot 2026-04-28.

Tests

  • tests/features/persistence_post_rename_chmod/ INV-2 revised to accept either QFile::rename or std::rename + rc==0 gating.
  • tests/features/github_status_bar/ INV-17 revised to lock the Wayland-correct dialog shape (forbid QMessageBox box(this), require new QDialog(this) + &QPushButton::clicked + &QDialog::close).
  • tests/features/remote_control_new_tab/ INV-3c/3d added.
  • tests/features/remote_control_launch/ INV-4b/4c added.
  • ctest 111/111 green.

v0.7.51

0.7.51 — config-reload inotify-loop fix

Theme: Hot-reload doesn't loop any more. Same-day follow-up to 0.7.50 — three reported symptoms (status bar permanently sticking at "Config reloaded from disk", Help → Check for Updates appearing to do nothing, Settings → Preferences not opening) all traced to a single root cause: MainWindow::onConfigFileChanged re-entering itself in an infinite inotify loop. The 0.7.31 attempt at fixing the same loop (m_configWatcher->blockSignals(true/false) bracketing) never worked because Qt reads inotify events from the event loop after the slot returns — outliving the blockSignals window. Fix is at the source: make the setters idempotent so the slot has nothing to re-trigger.

Fixed

  • Config reload re-entered itself in an inotify loop. Config::setTheme unconditionally called save() even when the value matched, so any applyTheme(m_config.theme()) from inside onConfigFileChanged rewrote the watched file → kernel inotify event → next event-loop tick re-emits fileChanged → slot re-enters → loop. setTheme now early-returns when the value matches, breaking the loop at its source. onConfigFileChanged additionally skips the no-op applyTheme call when the theme didn't change, and carries an m_inConfigReload re-entrancy flag (cleared via QTimer::singleShot(0, ...) so a save inside the slot is dropped but a subsequent genuine external edit is honored). The failed 0.7.31 blockSignals calls are removed.

    User-visible cascade now resolves:

    • Status bar stops sticking at "Config reloaded from disk" — the 3 s dismissal timer can actually expire.
    • Help → Check for Updates produces a visible "Checking for updates…" then "Up to date — running v0.7.51 (latest)" toast (or the actual update notice when behind).
    • Settings → Preferences opens the dialog instead of having it deleted-on-reload faster than the user can click.

    Locked down by tests/features/config_reload_loop_safety/ — INV-1 idempotent setter (call shape + functional mtime check), INV-2 no-op skip at the call site, INV-3 re-entrancy guard, INV-4 failed blockSignals attempt removed.

v0.7.50

0.7.50 — Wayland-correct dialog dismissal

Theme: Round four on the same dialog-button bug — and the matching Roadmap-dialog Close button. Three prior fix attempts (0.7.22, 0.7.35, 0.7.49) each diagnosed a downstream symptom rather than the actual root cause; this release identifies it from upstream Qt bug reports and applies the fix that the bg-tasks dialog has been quietly using all along.

Fixed

  • Both Help → About OK buttons (still) didn't dismiss the dialog in 0.7.49, and the Roadmap dialog's Close button hadn't worked since 0.7.43 either. User report 2026-04-28: "OK buttons still do nothing on the 2 About dialogs… Close button does nothing on the Roadmap either."

    Real root cause: both upstream Qt bugs QTBUG-79126 ("Dialogs behavior on Wayland is wrong") and QTBUG-90005 ("global modality on Wayland") document that Wayland's xdg-shell protocol has no equivalent of Qt::ApplicationModal. Calling setModal(true) (which 0.7.49 added) is a no-op on Wayland except for an aggravating side effect: KWin/Wayland routes click events into the modal-grab handler which then drops them, instead of delivering them to the dialog's button. The role-based dispatch path inside QDialogButtonBox is a known second aggravator on the same bug.

    The Background Tasks dialog has been working all along on the same parent-window flags — by being non-modal, with a plain QPushButton whose clicked() is wired directly to QDialog::close. Both About dialogs and the Roadmap dialog now follow that proven shape: non-modal, plain QPushButton, direct clicked()close(). No more setModal(true), no more QDialogButtonBox in these handlers. The OK / Close click reaches its slot and the dialog dismisses on the first try.

    Spec coverage extended: tests/features/help_about_menu/ INV-4 now asserts a plain QPushButton (negative grep on new QDialogButtonBox); INV-7f flips from "must call setModal(true)" to "must NOT call dlg->setModal(". tests/features/roadmap_viewer/ INV-14 mirrors the same. The regression history in help_about_menu/spec.md records the Wayland-modal root cause so the next contributor doesn't repeat the cycle.


🤖 AppImage builds via the existing release workflow. Wait for the workflow to attach the AppImage to this release.

v0.7.42

0.7.42 — Command Palette ghost-completion (Tab to commit /slash-style suggestions)

Theme: Inline ghost-text completion in the Command Palette — Claude Code's /slash-command UX, ported to Ctrl+Shift+P. As you type, the unmatched suffix of the top fuzzy-match is rendered in a dimmed colour right after the cursor; Tab commits the suggestion into the input. Retires ROADMAP § "Command Palette ghost-completion (near-term, small scope)".

Added

  • GhostLineEdit subclass of QLineEdit. Lives in src/commandpalette.h. Carries a m_ghost string set via setGhostSuffix(); paintEvent calls QLineEdit::paintEvent(event) first, then opens a fresh QPainter and draws the ghost at cursorRect().right() + 1 using palette().color(QPalette::Text) with setAlphaF(0.45). No layout changes — the suffix overlays the existing line-edit content area, so palette geometry is unchanged.

  • CommandPalette::updateGhostCompletion(filter). Invoked from populateList after the result list is rebuilt. Looks at m_list->item(0), recovers the underlying QAction, strips & accelerators, and:

    • if the filter is empty, list is empty, or the action name does not start with the filter (case-insensitive) → setGhostSuffix("");
    • otherwise → setGhostSuffix(name.mid(filter.length())), which preserves the action name's original casing in the ghost.

    contains()-only matches (where the filter appears mid-string in the top match) get an empty ghost — the visual contract is that the suffix appears flush after the user's typed input, which only makes sense for prefix matches.

  • Tab-key commit handler. eventFilter adds Qt::Key_TabcommitGhost(), which appends the ghost suffix to the input via setText. The follow-up textChanged → filterActions → populateList → updateGhostCompletion cycle clears the ghost in the same dispatch since the new filter equals the action name's tail exactly. The post-commit text equals the visible composition (user-typed prefix + ghost suffix), so user-typed casing is preserved — same as shell-completion semantics. Tab is always consumed by the palette (even when the ghost is empty) so focus cannot leak out of the input while the palette is open. Tab does not also execute the action — the user presses Enter to run, matching Claude Code's /slash-completion contract.

Tests

  • tests/features/command_palette_ghost_completion/ — ten invariants spanning the contract: I1 the input is a GhostLineEdit findable by objectName; I2 empty filter → empty ghost; I3 prefix "ind" → ghost "ex Review" (first item in setActions order is "Index Review"); I4 uppercase "INDEX" → ghost " Review" (case-insensitive prefix, casing preserved from the action name); I5 contains-only match → empty ghost; I6 no-match → empty ghost; I7 Tab commit → text = filter + ghost ("index Review") and ghost cleared; I8 Tab with empty ghost is consumed (text unchanged, focus retained on input); I9 source-grep on commandpalette.cpp (exactly one setAlphaF(0.45 literal, ≥1 cursorRect( reference); I10 Esc still hides the palette and emits closed() exactly once (regression guard on existing dismiss behaviour). Drives CommandPalette directly under QT_QPA_PLATFORM=offscreen — no MainWindow link, no PTY. Pre-fix verification: with commandpalette.{h,cpp} stashed, the test fails to even compile (missing GhostLineEdit symbol); restored, test passes.

Notes

  • Out of scope. The in-terminal shell ghost-suggestion (fish-style, OSC 133 prompt-detection + history scraping) is a separate ROADMAP item marked 💭 with deferral past 1.0. It would touch terminalgrid.cpp paint path and vtparser.cpp rather than commandpalette.cpp.
  • Frequency-ranked top match. The palette today picks the top match by setActions iteration order. A frequency-ranked source (most-used action first) is a noted future refinement; the contract above stays stable across that change because the test uses a fixed action ordering.

v0.7.41

0.7.41 — A11y for chrome (accessible names + AT-SPI introspection lane)

[0.7.41] — 2026-04-27

Theme: Accessibility — explicit setAccessibleName / setAccessibleDescription on every glyph-only chrome control so Orca, Speakup, and other AT-SPI screen readers announce them by purpose ("Close window") instead of by codepoint name ("eight-spoked asterisk", "multiplication sign x"). Two ROADMAP items retired in one bundle: § "Accessibility pass on chrome" (mainwindow / palette / title-bar control labels) and § "AT-SPI introspection lane" (an automated check that every user-visible chrome control carries a name).

Added

  • Accessible names on TitleBar window controls. centerBtn ("Center window" / "Center this window on the active screen"), minimizeBtn ("Minimize window"), maximizeBtn ("Maximize window" / "Maximize or restore this window"), and closeBtn ("Close window") each carry an explicit setAccessibleName and setAccessibleDescription set immediately after setText in titlebar.cpp:54-97. Each button also gains a stable objectName (the previously-set closeBtn is joined by the matching three siblings) so the introspection test can find them deterministically. Visual rendering is unchanged — the glyphs (✥ – □ ✕) still drive the on-screen pixels; only the AT-SPI tree changed.

  • Accessible names on the Command Palette. m_input (objectName commandPaletteInput) gains Command palette search / Type to filter actions; Tab to commit; Esc to dismiss. m_list (objectName commandPaletteList) gains Command palette results / Available actions matching the current filter. Placeholder text and visible behaviour are unchanged. The placeholder is a typing hint for sighted users; the accessible name is what screen readers announce on focus — separating the two unblocks the keyboard-only workflow.

Tests

  • tests/features/a11y_chrome_names/ — eight invariants spanning the contract: T1–T4 each TitleBar button has the right accessibleName + non-empty accessibleDescription; T5/T6 the CommandPalette input + list have theirs; T7 the AT-SPI introspection lane (every reachable QAbstractButton carries either a non-empty accessibleName() or a non-empty text(), every reachable QLineEdit has an explicit accessibleName), T8/T9 source-grep counts on titlebar.cpp and commandpalette.cpp so a future refactor that drops the setAccessibleName calls fails the build, not just AT-SPI usage at runtime. The test constructs the TitleBar and CommandPalette directly under QT_QPA_PLATFORM=offscreen so it runs in CI without a display.

Notes

  • tr() translation hooks for the new strings are deferred to the 0.9.0 H10 i18n bundle so the .qm files cover both UI text and accessibility strings in one pass — pulling them in now would mean re-touching every setAccessibleName(...) call later.
  • A custom QAccessibleInterface adapter for TerminalWidget itself (the text-changed event lane behind the H9 a11y bundle in 0.9.0) is a separate, larger design cycle and is out of scope for this bundle.

v0.7.40

0.7.40 — Perf bundle — scroll-region rotate + VtBatch zero-copy

Theme: Performance — scroll-region rotate + VtBatch zero-copy across the worker→GUI thread hop. Two Tier 3 perf items from ROADMAP.md § 0.7.x ⚡ / 🏗 Tier 3 — structural retired in one bundle. Both are observably equivalent at the API level — the existing tests/features/scroll_region_rotate/ and tests/features/threaded_parse_equivalence/ regressions pass unchanged — so the win is throughput / cost, not visible behaviour.

Changed

  • TerminalGrid::scrollUp / scrollDown use std::rotate. Replaced the per-iteration m_screenLines.erase + insert loop with a single rotate over [scrollTop, scrollBottom], gated by a pool-salvage / scrollback-push pre-pass and a fresh-blank post-pass. The CSI 2J doubling-guard window check now runs once per batch (elapsed time across a batch is microseconds, so the per-iteration window-extend was redundant). m_scrollback cap enforcement collapses from N pop_front passes to one. On CSI 100 S against an 80-row screen, the row-shift cost goes from O(count × rows) memmoves (8000) to O(rows) (80).

  • VtStream::batchReady carries VtBatchPtr instead of const VtBatch &. using VtBatchPtr = std::shared_ptr<const VtBatch>; is the new alias in vtstream.h. Qt's queued- connection plumbing must own the parameters it dispatches, so the prior const T & signal forced a deep copy of the entire batch (the actions vector + rawBytes QByteArray) on every worker→GUI hop, regardless of any move-from-pending shaping the emitter performed. The shared_ptr wrap reduces the cross-thread payload to a 16-byte atomic refcount bump; the underlying VtBatch lives on the heap and is not duplicated. Both emit sites (flushBatch, onPtyFinished) build via std::make_shared<VtBatch>(). TerminalWidget::onVtBatch signature changed to void onVtBatch(VtBatchPtr batch); field access is now via batch->… instead of batch.….

Tests

  • tests/features/vtbatch_zero_copy/ — 5 source-grep invariants locking the cross-thread signal shape so a future refactor can't silently revert to the deep-copy form. Behavioural equivalence on the action stream is already covered by tests/features/threaded_parse_equivalence/.

  • tests/features/scroll_region_rotate/ — pre-existing 8 invariants on rotation correctness (I1–I8 in spec.md) still pass. The spec was written algorithm-agnostic, anticipating this swap.

v0.7.39

0.7.39 — Claude state-dot palette + status-bar Roadmap viewer

Theme: Claude Code UX bundle + status-bar Roadmap viewer. Two user requests landed in quick succession.

User request 1 (Claude state-dot palette):

"Let's have a round dot on each tab that has a Claude Code session running (no icons or anything else other than the tab label). The dot will change colour with the various states that Claude Code is in. Each state has its own colour (grey for idle). Then extend those colours to the status bar Claude Code status too."

User request 2 (Roadmap viewer):

"Add a button on the status bar to view the roadmap. So, it brings up a dialog showing the roadmap. It should have filters as well to show what is outstanding and what is completed. If at all possible, it should also highlight what item is being done currently. The roadmap button should only show if there is roadmap documentation — let's simplify that to requiring a roadmap.md file only." Plus a clarification adding a fourth emoji toggle and elevating "Currently being tackled" to a peer filter.

Added

  • Unified Claude state-dot palette across tabs and status bar. New static helper ClaudeTabIndicator::color(Glyph) in coloredtabbar.h is the single source of truth for an eight-state palette: Idle grey #888888, Thinking blue #5BA0E5, ToolUse yellow #E5C24A, Bash green #6FCF50, Planning cyan #5DCFCF, Auditing magenta #C76DC7, Compacting violet #A87FE0, AwaitingInput orange #F08A4B. Red is intentionally absent — AwaitingInput is a normal interaction state, not an error. ColoredTabBar::paintEvent calls the helper for fill colour and uses a single kDotRadius = 4 for every state; the prior AwaitingInput "outline + radius 5" treatment is removed (per "no icons or anything else"). The bottom status-bar Claude Code label was rewired to map current state → Glyph → helper colour, replacing the prior Theme::ansi[N] mappings that drifted from the tab dot's colour and varied across themes.

  • Auditing now lights the per-tab dot. Previously surfaced only on the active-tab status bar (m_claudeAuditing). Plumbed into ClaudeTabTracker::ShellState::auditing (mirrored from the existing ClaudeTranscriptSnapshot.auditing field). Tab provider routes it to Glyph::Auditing magenta; hover tooltip reads "Claude: auditing". Precedence chain unchanged across both surfaces: AwaitingInput → Planning → Auditing → state-derived.

  • Status-bar Roadmap viewer button (roadmapdialog.{h,cpp}, new files). Visible iff the active tab's shellCwd() contains a ROADMAP.md (case-insensitive — accepts Roadmap.md, roadmap.md too). MainWindow:: refreshRoadmapButton is called from the central refreshStatusBarForActiveTab tick. Click opens RoadmapDialog: a non-modal QDialog with a top row of five peer category checkboxes (✅ Done · 📋 Planned · 🚧 In progress · 💭 Considered · Currently being tackled), all default-checked. A bullet renders iff any of its enabled category memberships matches; plain narration bullets (no status emoji) always render. The static RoadmapDialog::renderHtml helper is pure so tests can drive it without spinning a Qt widget.

  • "Currently being tackled" highlight. A bullet matches the current-work signal if its first-80-character payload (status- emoji-stripped, normalised — lowercase, hyphens-and-underscores as spaces, punctuation removed) contains a phrase from a signal set built from (a) the local CHANGELOG.md [Unreleased] block bullets, (b) the last 5 non-release/non-merge/non-revert git commit subjects. Matched bullets get a yellow left-border CSS highlight (border-left: 4px solid #E5C24A — the same ToolUse yellow from the dot palette; the consistency is intentional). When the signal set is empty, no highlight renders — the feature is silent when there's nothing to point at.

  • Live-tail dialog mechanics. QFileSystemWatcher on ROADMAP.md and CHANGELOG.md, 200 ms debounce timer, and the same scroll-preservation triple shipped with 0.7.37 / 0.7.38: m_lastHtml shared_ptr cache + skip-identical-HTML guard, capture vbar before setHtml, restore with qMin(saved, vbar->maximum()) clamp, was-at-bottom pin so live appends stay visible.

Changed

  • ClaudeTabIndicator::Glyph enum gains Auditing. Inserted between Planning and Compacting so the palette ordering reads Done-side → Action-side → InFlight-side → BlockingUser. ClaudeTabTracker::ShellState gains a bool auditing field and a new comparison branch in maybeEmit so changes to the audit latch trigger shellStateChanged like every other field.

  • MainWindow::applyClaudeStatusLabel no longer reads Theme::ansi[] for Claude-label colours. All state → colour mappings now route through the unified helper. The Theme &th = Themes::byName(...) local was removed (unused).

Removed

  • AwaitingInput-specific dot decoration (radius = 5 and white outline). Colour alone is the differentiator now, per the "no icons or anything else" user spec. The outline.alpha() branch was removed from paintEvent.

Tests

  • tests/features/claude_state_dot_palette/ — 8 source-grep invariants asserting helper signature, full palette, paintEvent helper-call, uniform geometry, mainwindow status-bar wiring, Auditing plumbing through ShellState, and double-use of the Auditing glyph (provider closure + status applier).

  • tests/features/roadmap_viewer/ — 10 hybrid invariants (source-grep + behavioral). Links the dialog source so the static renderHtml helper can be driven against synthetic markdown to verify five-bit filter semantics, the highlight CSS marker on signal match, the marker's absence on empty signal sets, the case-insensitive cwd probe, the wire-up shape (m_roadmapBtn construction + click connection), and the refreshRoadmapButton call from refreshStatusBarForActiveTab.

Documentation

  • ROADMAP user-request entries added for both 0.7.39 features with full citation of the user asks. Two stale 📋 entries in the cross-cutting themes section flipped to ✅ with shipped-release citations (the /tmp/*.js TOCTOU and the no-auth local-IPC chain — both shipped in the 0.7.12 Tier 1 batch but the umbrella narrative entries weren't updated at the time).

v0.7.35

0.7.35 — About dialog OK button fix — QDialog + connected QDialogButtonBox

Theme: UX bug fix. Single user-reported regression in the Help → About Ants Terminal dialog (the GUI version indicator added in 0.7.22).

Fixed

  • About Ants Terminal dialog OK button silently no-op'd (mainwindow.cpp). Was: Help → About Ants Terminal… opened the version dialog correctly, but clicking its OK button did nothing — the dialog could only be dismissed via the window-manager close (X) button. The 0.7.22 implementation used QMessageBox::Ok with Qt::TextBrowserInteraction (which pulls in Qt::TextSelectableByMouse); under the combination of our frameless MainWindow, Qt::WA_TranslucentBackground, KDE Plasma + KWin, and Qt 6.11 this caused the OK click to be silently dropped. Now: the About handler builds a custom QDialog with a QDialogButtonBox(QDialogButtonBox::Ok) whose accepted signal is explicitly connected to QDialog::accept, giving us a click path that's standard, testable, and independent of QMessageBox's internal standard-button dispatch.

  • About dialog GitHub link was a visual-only no-op (mainwindow.cpp). Was: the link in the About body was rendered as clickable (cursor changed on hover) because Qt::TextBrowserInteraction enables Qt::LinksAccessibleByMouse, but setOpenExternalLinks(true) was never called on the label — so clicking the link emitted linkActivated() into the void. Now: the body QLabel has setOpenExternalLinks(true), so clicking the GitHub URL opens it in the user's browser.

Changed

  • tests/features/help_about_menu/spec.md + test (8 invariants). Spec rewritten to lock the QDialog + QDialogButtonBox shape, document the 2026-04-25 regression in the History section, and assert via source-grep that the QMessageBox::Ok + Qt::TextBrowserInteraction pattern cannot reappear without the test failing. Pre-fix source fails 6 of the 8 new I4 invariants; post-fix all 8 pass.

v0.7.38

0.7.38 — Background-tasks status-bar button — Claude Code live tail

Theme: Feature. Background-tasks status-bar surface for Claude Code. User request 2026-04-25:

"a button on the status bar when there are background tasks being run. We then click the button to view what Claude Code shows for the background tasks. The button opens a dialog showing the live update info on the background tasks."

Claude Code can spawn long-running tasks via Bash or Task with run_in_background: true. The TUI shows a sidebar listing them with their tail output; users dropping into a different tab to keep working had no way to see those tasks from Ants Terminal — they had to flip back to the TUI and parse the sidebar by hand. The new status-bar button surfaces the live count and the dialog mirrors the 0.7.37 Review Changes update model (debounced live tail with scroll-preservation).

Added

  • Background-tasks tracker (new claudebgtasks.{h,cpp}). A pure parser walks the active session's transcript JSONL, recognizing tool_use blocks whose input.run_in_background == true (start) and tool_result blocks whose toolUseResult.backgroundTaskId is set (confirmation, with the on-disk output path extracted from the result body). Completion / kill state is derived from subsequent BashOutput results carrying status: "completed" | "killed" | "failed" and from KillShell tool calls. The ClaudeBgTaskTracker class wraps the parser with a QFileSystemWatcher on the transcript and emits tasksChanged() when the running-count or shape changes. Static parseTranscript(path) is testable without a watcher.

  • Status-bar button (mainwindow.{h,cpp}). Sibling to the Review Changes button; same fixed sizePolicy contract. Hidden by default; visible only while runningCount() > 0 for the active tab's session. Label re-renders as "Background Tasks (N)"; tooltip discloses running + total. Re-targeted on tab switch via refreshBgTasksButton() (called from refreshStatusBarForActiveTab), so each tab's session drives its own count independently.

  • Live-tail dialog (new claudebgtasksdialog.{h,cpp}). Mirrors the Review Changes live-update model that 0.7.37 stabilized. QFileSystemWatcher covers each task's .output file plus the transcript path, so new starts and completions appear without manual refresh. A 200 ms debounce timer collapses bursts of fileChanged signals (common when the backgrounded process is noisy) into one render. Skip-identical-HTML guard via a per- dialog m_lastHtml (std::shared_ptr<QString>) preserves selection and scroll position when the render is unchanged. Capture-vbar/hbar before setHtml, restore after with qMin(..., maximum()) clamp keeps absolute scroll position when content changes — same shape as the 0.7.37 Review Changes fix. Bonus "was at bottom" detection: when the user is tailing the bottom, pin them to the bottom across appends so live output stays visible without manual scrolling.

Changed

  • tests/features/claude_bg_tasks_button/spec.md + test (10 invariants). New regression test pinning the parser shape (run_in_background and backgroundTaskId keys), the tracker's QFileSystemWatcher + tasksChanged() signal, the button's hide-when-empty contract, the connect to showBgTasksDialog, the dialog's reuse of the 0.7.37 scroll-preservation pattern, the outputPath watch enumeration in rewatch(), the debounce timer interval (≤ 500 ms, single-shot), and the CMake source list. Source-grep harness, no Qt link.

v0.7.37

0.7.37 — Review Changes dialog — scroll preserved across live refreshes

Theme: UX bug fix. Live-update regression in the Review Changes dialog (the QFileSystemWatcher + 300ms debounce wiring added in 0.7.32). User report 2026-04-25:

"When using the Review Changes dialog, the constant resetting of the text means that if I scroll, it resets to the beginning every refresh. That means I can't scroll basically."

The 0.7.32 finalize lambda called viewerGuard->setHtml(html) unconditionally on every probe completion — every git change, every debounce tic, every Refresh click. QTextEdit::setHtml re-parses the document and snaps the vertical scroll bar back to the top, also discarding selection and cursor position. On a long diff with active live updates the dialog became unscrollable.

Fixed

  • Review Changes dialog: scroll position preserved across live refreshes (mainwindow.cpp). Two-layer guard around the setHtml call inside MainWindow::showDiffViewer's finalize lambda. (1) An auto lastHtml = std::make_shared<QString>() cache threaded through runProbes and finalize lets finalize early- return when the new render is byte-identical to the previous one — the common case during idle live-update tics, where branch metadata refreshes don't change anything visible. Selection, cursor, and scroll are byte-perfectly preserved. (2) When content does change, vertical and horizontal scroll-bar values are captured before setHtml and restored after, clamped to bar->maximum() so a shorter render after a commit doesn't over- scroll. First-render carve-out (isFirstRender = lastHtml->isEmpty()) keeps the initial paint at the top.

Changed

  • tests/features/review_changes_scroll_preserve/spec.md + test (6 invariants). New regression test pinning the lastHtml cache, the skip-identical guard, the capture-scroll-before-setHtml + restore-after pattern, and the first-render carve-out. Source-grep harness scoped to the MainWindow::showDiffViewer body so other QScrollBar / setHtml call-sites in mainwindow.cpp don't cause false positives.

v0.7.36

0.7.36 — Tab bar + status bar opaque under translucent parent

Theme: UX bug fix. Same translucent-parent failure mode that bit the menubar in 0.7.25/0.7.26 caught in two more places (user report, post-0.7.32):

"The tabs themselves are fine but the rest of the tab bar across the window is transparent as well as the background for the status bar."

The 0.7.32 close-button SVG drew the user's eye to the empty-area fill that had been mis-painted all along; the bug was not introduced by 0.7.32 but became visible because the new tab look re-balanced what the user noticed.

Fixed

  • Tab bar empty area paints opaque under WA_TranslucentBackground (coloredtabbar.{h,cpp}). Was: the strip to the right of the last tab rendered the desktop wallpaper through, with the QSS QTabBar { background-color: ... } rule silently dropped by Qt's stylesheet engine on KWin + Breeze + Qt 6 once WA_OpaquePaintEvent was set on the widget. Same root cause documented in opaquemenubar.h. Now: ColoredTabBar exposes setBackgroundFill(QColor) and prepends a CompositionMode_Source fillRect to its existing paintEvent, before the base class draws tabs and before the colour-group gradient overlay. applyTheme() pushes theme.bgSecondary into the override.

  • Status bar background paints opaque under WA_TranslucentBackground (new opaquestatusbar.h, mainwindow.{h,cpp}). Was: the entire status bar strip rendered the desktop wallpaper through — same root cause as the tab bar above. Now: a header-only OpaqueStatusBar mirrors OpaqueMenuBar (paintEvent fillRect → delegate). MainWindow constructs an OpaqueStatusBar, installs it via setStatusBar(...) before the first statusBar() call (Qt's lazy-creation would otherwise install a plain QStatusBar that paints transparent), and pushes theme.bgSecondary via setBackgroundFill from applyTheme().

Changed

  • tests/features/tabbar_statusbar_opaque/spec.md + test (5 invariants). New regression test pinning the setBackgroundFill
    • fillRect mechanism on both bars, the setStatusBar install order, the WA_OpaquePaintEvent attribute on the tab bar, and the paint order inside ColoredTabBar::paintEvent (fill before the base class draws tabs). Source-grep harness, no display required.

v0.7.34

0.7.34 — Terminal correctness — DECOM CUP/HVP/VPA + DECSC saves origin

Theme: Terminal correctness. One ROADMAP item retiring the last known DECOM (origin mode) bug — tmux/screen save-restore round-trips were silently dropping the origin-mode flag and ignoring scroll-region-relative coordinates on absolute cursor moves.

Fixed

  • CUP / HVP / VPA translate to scroll-region origin under DECOM (terminalgrid.cpp). Was: with origin mode (DECOM, CSI ?6h) active and a scroll region set via DECSTBM (CSI top;bottom r), CSI 1;1H jumped to absolute (0, 0) and CSI 99;1H clamped to the bottom of the entire screen instead of the bottom of the scroll region. Programs that depend on origin-mode-relative positioning (vim split-window cursor restoration, screen's caption line, tmux status redraws) ended up writing to the wrong row. Now: the 'H', 'f', and 'd' cases inside processCSI add m_scrollTop to the requested row when m_originMode is set, then clamp to [m_scrollTop, m_scrollBottom]. Behaviour matches xterm.

  • DECSC saves DECOM + DECAWM, DECRC restores them (terminalgrid.cpp, terminalgrid.h). Was: saveCursor() stored only (row, col, attrs), so a TUI that flipped origin mode or auto-wrap, did CSI s / ESC 7, ran code that re-toggled the flag, then CSI u / ESC 8'd ended up with the flag in whatever state the inner code left it in — silent coordinate-space corruption. Now: m_savedOriginMode and m_savedAutoWrap members capture the flags on save and restore them on restore, matching the VT420 spec for DECSC / DECRC.

  • DECSTBM home position respects DECOM (terminalgrid.cpp). Was: setScrollRegion() always homed the cursor to absolute (0, 0) after setting the region. Now: when m_originMode is on at the time DECSTBM lands, the cursor moves to (m_scrollTop, 0) — the top-left of the origin-mode coordinate space — as xterm does.

Locked by tests/features/origin_mode_correctness/ — 12 behavioural invariants (CUP/HVP/VPA translation, scroll-region clamping, DECSTBM home, DECSC/DECRC save-restore in both CSI s/u and ESC 7/8 forms, DECAWM round-trip via the wrap-or-not at last-column probe) plus 4 source-grep checks anchoring the fix's shape (std::clamp(row, m_scrollTop, m_scrollBottom) near case 'H', m_savedOriginMode = m_originMode in saveCursor, the mirror line in restoreCursor, and the m_originMode ? m_scrollTop : 0 ternary in setScrollRegion).

v0.7.33

0.7.33 — Lifecycle/cleanup — PTY dtor off-thread + portal session close + plugin manifest safety

Theme: Lifecycle / cleanup. Three ROADMAP items addressing GUI-thread blocking on shutdown, an xdg-desktop-portal session leak, and two latent issues (OOM surface + symlink escape) in the Lua plugin loader.

Changed

  • PTY destructor escalation runs off the main thread (Pty::~Pty). Was: SIGTERM-then-busy-wait-then-SIGKILL ran on the GUI thread; N split panes closing together blocked the window close N × 500 ms. KWin would throw a "window not responding" hint at four splits. Now: the destructor still does the cheap pre-escalation reap (SIGHUP + close master fd + waitpid(WNOHANG)) inline — most shells exit on SIGHUP in microseconds. If the cheap reap doesn't take, the SIGTERM/SIGKILL escalation moves to a detached std::thread capturing the pid by value (no this reference can outlive the destructor). The thread creation is wrapped in a try { ... }.detach() } catch (const std::system_error &) that falls back to the synchronous escalation when thread creation fails (rare — ulimit -u pressure at exit). Locked by tests/features/pty_dtor_off_main_thread/ — 11 invariants on <thread> include, lambda capture list (rejects [this] and [&]), .detach() call, fallback retention, and the pre-escalation cheap reap remaining inline.

  • GlobalShortcutsPortal closes its session on destruction (GlobalShortcutsPortal::~GlobalShortcutsPortal). Was: no destructor at all — the session handle returned by CreateSession leaked for the lifetime of the D-Bus client. xdg-desktop-portal accumulated one orphan session per Ants invocation that crashed or was SIGKILLed before the QObject parent-tree cleanup could implicitly close the bus connection; visible via busctl --user introspect org.freedesktop.portal.Desktop .... Now: the destructor issues an asynchronous org.freedesktop.portal.Session.Close call against m_sessionHandle when non-empty (early-returns on empty handle so X11 / GNOME / no-portal paths don't crash Qt's D-Bus marshaller). New kSessionIface constant in the anonymous namespace alongside the existing service/path/interface constants. Locked by tests/features/portal_session_close/ — 8 invariants on header dtor declaration with override, kSessionIface constant, empty-handle early return, and the asyncCall(createMethodCall(..., kSessionIface, "Close")) dispatch.

  • Plugin manifest cap + canonical plugin path (PluginManager::scanAndLoad). Two latent issues addressed together. Manifest cap: f.readAll() was unbounded — a multi-GB manifest.json (corrupt disk, malicious tarball) would allocate that much RAM before QJsonDocument::fromJson got a chance to reject it. Now: f.read(kMaxManifestBytes) with kMaxManifestBytes = 1024 * 1024 (1 MiB ≈ 250 plugins worth of permission/description text — real manifests are <10 KiB, so the cap never bites legitimate content). Files larger than the cap log a warning and skip without parsing. **Canonical plugin path:** the scan now anchors on QFileInfo(m_pluginDir).canonicalFilePath(), passes QDir::NoSymLinks to entryList (cheap first-pass filter), and per-entry verifies the resolved path is anchored inside the canonical root via startsWith(canonicalRoot + "/"). Closes the symlink-escape shape where a hostile plugin tarball containing evil -> /etc/cron.daily could trick the loader into attempting init.lua from outside the user's plugin tree. Locked by tests/features/plugin_manifest_safety/ — 12 invariants on the cap (named constant + value + bounded read

    • no readAll-feeding-fromJson regression), NoSymLinks flag, canonical anchor, per-entry containment check, and the reject-warning message text.

v0.7.32

0.7.32 — Dialog UX — Settings gating/rollback/defaults + Review Changes branches+live + tab × always visible

Theme: Dialog UX. Three bundle items from the Settings dialog ROADMAP list (dependency-UI gating, Cancel rollback for Profiles, Restore Defaults per-tab) plus one user feedback item from 2026-04-25 — the Review Changes dialog only surfaced current- branch state and missed work living on other branches.

Added

  • Restore Defaults button per primary Settings tab (SettingsDialog::setupGeneralTab, setupAppearanceTab, setupTerminalTab, setupAiTab). Was: only Keybindings had a defaults-reset button. A user who tweaked the dialog and wanted to start over had to either remember every default or delete ~/.config/ants-terminal/config.json (which lost unrelated settings: highlight rules, profiles, plugin grants). Now: each substantive tab has its own button with stable objectName (restoreDefaultsGeneral, restoreDefaultsAppearance, restoreDefaultsTerminal, restoreDefaultsAi). Reset slots mutate widget state only — applySettings still commits, Cancel still rolls back. Schema defaults match the second argument of each Config::xxx() getter so the dialog and config layer can't drift. Locked by tests/features/settings_restore_defaults/ — 22 invariants on objectNames, reset-value coverage per tab, and the "no m_config-> writes from within reset slots" rule.

  • Review Changes dialog: live updates via QFileSystemWatcher + manual Refresh button (MainWindow::showDiffViewer). User feedback 2026-04-25: dialog should show live or near-live updates. Was: probes ran once on dialog open and never again — the user had to close and re-open to see changes from a commit / fetch / branch operation done in the terminal underneath. Now: a QFileSystemWatcher watches cwd, .git, .git/HEAD, .git/index, .git/refs/heads, .git/refs/remotes, .git/logs/HEAD. fileChanged and directoryChanged signals feed a 300 ms single-shot QTimer (debounce — git pull / git fetch fire fileChanged O(refs) times in milliseconds; the debounce coalesces them into one re-probe). The probe-spawning logic was refactored into a runProbes lambda that constructs a fresh ProbeState per call, so an in-flight probe whose finalize outlives the next refresh can't decrement the new pending counter and render a half-populated mix. Atomic-rename safe: the fs-event handler re-adds paths that exist but are no longer watched (Qt loses the watch on rename(2), which git uses for HEAD/index/logs/HEAD updates). A live-status label (reviewLiveStatus objectName) shows "● refreshing…" / "● live — auto-refresh on git changes" so the user can confirm the watcher is wired. A manual Refresh button (reviewRefreshBtn objectName) bypasses the debounce for cases where state changed outside the watched paths (a build script in another shell, a different terminal). Locked by tests/features/review_changes_branches/ (extended) — now 33 invariants total covering ProbeState fields, runAsync targets, finalizer rendering, copy-handler payload, empty-state guard, runProbes lambda, watcher armament, debounce timing, atomic- rename re-watch, refresh-button bypass, and live-status label states.

  • Review Changes dialog: per-branch summary + cross-branch unpushed commits (MainWindow::showDiffViewer, MainWindow::ProbeState). Was: three async git probes (status, diff HEAD, log @{u}..HEAD) all scoped to the current branch's working tree and HEAD lineage. A user with five feature branches each holding unpushed work saw "no unpushed" if they happened to be on a clean branch. Branches without upstreams or with diverged ahead/behind state were invisible. User feedback 2026-04-25: "the Review Changes dialog doesn't consider changes in other branches of the project." Now: two additional probes drop in alongside the existing three — git for-each-ref refs/heads --format='%(refname:short) \t%(upstream:short)\t%(upstream:track)\t%(subject)' for the per-branch summary (with ahead/behind/gone/no-upstream colour cues), and git log --branches --not --remotes --oneline --decorate for the cross-branch unpushed log (every commit reachable from any local branch but not from any remote- tracking branch). Both are O(refs) and finish in milliseconds. Copy Diff includes both new sections. Locked by tests/features/review_changes_branches/ — 15 invariants on ProbeState fields, runAsync targets, finalizer rendering, copy-handler payload, and empty-state guards.

Changed

  • Tab close button (×) is always visible, not hover-only (MainWindow::applyTheme stylesheet — QTabBar::close-button). User feedback 2026-04-25: "the tabs still don't have a visible marker per tab that shows where to click to close the tab. The mouseover works but we need to also see it when onmouseout." The 0.6.27 fix removed image: none to let Qt fall back to the platform's standard close icon — that worked on Breeze/Adwaita but failed on Fusion / qt6ct / certain Plasma colour schemes where the platform style still rendered the × hover-only. Now: explicit data-URI SVG image: url("data:image/svg+xml;...") rules in both the default and :hover QTabBar::close-button variants. Glyph re-tints with the active theme via textSecondary (default) / textPrimary (hover); hover keeps the ansi-red background-color will-click cue. URL-encoded %23 is spliced into the arg list via QStringLiteral("%23") + theme.<color>.name().mid(1) rather than the format string — prevents Qt's CSS parser from truncating the data URI at the fragment delimiter and also avoids the QString::arg() placeholder-numbering collision. Locked by tests/features/tab_close_button_visible/ — 11 invariants on data-URI presence, two-line × shape, arg-side splice, and image-rule presence in BOTH state variants.

  • Dependency-UI enable gating (SettingsDialog::setupAppearanceTab, setupTerminalTab, setupAiTab). Was: master checkboxes (m_aiEnabled, m_autoColorScheme, m_quakeMode) gated logic but not UI — typing an API key into a feature-disabled AI tab, or selecting "Solarized" as the light-mode theme while auto-switch was off, both produced silent no-ops. Now: QCheckBox::toggled is wired to setEnabled on every dependent sibling, with a one-shot sync call at construction so the initial state matches the loaded config without relying on setChecked() always emitting toggled (it only emits when state actually changes). Disabled controls keep their current values, so toggling the master back on restores the user's prior selection rather than zeroing it out. Locked by tests/features/settings_dependency_gating/ — 16 invariants on the three sync lambdas, dependent setEnabled calls, toggled-connect wiring, and initial-sync call sites.

  • Profiles tab honors Cancel/OK semantics (SettingsDialog::setupProfilesTab, loadSettings, applySettings, m_pendingProfiles, m_pendingActiveProfile). Was: profile Save/Delete/Load buttons mutated m_config immediately via setProfiles() / setActiveProfile(). Cancel could not roll those edits back — they had already been persisted to config.json before the user's intent was known. Every other Settings tab follows the standard "stage in widgets, commit on applySettings, discard on reject" pattern; Profiles broke that contract. Now: the three buttons mutate a pending-state pair (m_pendingProfiles + m_pendingActiveProfile), loadSettings re-initializes the pair from m_config, and applySettings is the single commit point that calls setProfiles / setActiveProfile. Cancel skips applySettings, so dialog close leaves m_config unchanged. Locked by tests/features/settings_profile_cancel_rollback/ — 11 invariants including a global "exactly one m_config->setProfiles call site" check that catches a regression where a button callback starts writing to m_config directly again.

v0.7.31

0.7.31 — Persistence integrity — concurrent-writer guard + post-rename chmod + secureio.h split

Theme: Persistence integrity (cross-file). Four items from the post-0.7.30 grouping plan, addressing the silent-data-loss / permission-drift / concurrent-writer surfaces that span Config, ClaudeAllowlist, SessionManager, and SettingsDialog. Splits the growing secureio.h into secureio.h (perms) + configbackup.h (rotation + cooperative write lock) before a third helper landed.

Added

  • ConfigWriteLock cooperative write lock for shared config files (src/configbackup.h). Was: two simultaneously-running Ants instances saving the same ~/.config/ants-terminal/config.json raced over <path>.tmp + rename(2) — both writers truncated the shared tmp, partial bytes interleaved on the same inode, and last-rename-wins silently dropped one process's keystrokes / settings / profile / AI key. Same shape for ~/.claude/settings.json where Allowlist + Install-hooks + git-context-installer all read-modify-write with no serialization. Now: an RAII guard wraps POSIX flock(2) on a sibling <path>.lock file with a 5-second poll deadline (100 × 50 ms), advisory so cooperating callers serialize while non-cooperating editors (vim, jq) bypass by design. Config::save, ClaudeAllowlistDialog::saveSettings, SettingsDialog::installClaudeHooks, and SettingsDialog::installClaudeGitContextHook now construct ConfigWriteLock writeLock(path) with an acquired() guard before the write block; failure to acquire logs and returns rather than risking the data race. Locked by tests/features/concurrent_writer_lock/ (5 runtime invariants via fork(2) so flock semantics are honestly tested across processes, plus 7 source-grep invariants on each save site). Pre-fix source fails 7; post-fix all 12 pass.

  • Belt-and-suspenders post-rename setOwnerOnlyPerms at every persistence site (Config::save, SessionManager::saveSession, SessionManager::saveTabOrder, SettingsDialog::installClaudeHooks, SettingsDialog::installClaudeGitContextHook). Was: each site set 0600 on the temp fd before write but relied on rename(2) preserving perms across the swap. ext4/xfs/btrfs honor that, but FAT/exFAT on removable media (no POSIX bits at all), some SMB/NFS servers (server-side rename applies server umask), and Qt's copy+unlink fallback (cross-device rename, exotic mounts — copy creates the destination with the process umask) do not. Files containing ai_api_key (config.json), Claude Code bearer tokens (settings.json), or paste-buffer scrollback content (session_*.dat) could land 0644 on those filesystems and leak to every UID on the host. Now: each site re-chmods the final inode after rename/commit() returns success — idempotent on POSIX filesystems, essential elsewhere. Locked by tests/features/persistence_post_rename_chmod/ (10 source-grep invariants spanning config.cpp / sessionmanager.cpp / settingsdialog.cpp). Pre-fix source fails 6; post-fix all 10 pass.

  • secureio.h / configbackup.h split. secureio.h is now perms-only — setOwnerOnlyPerms(QFileDevice&) and setOwnerOnlyPerms(const QString&), the original 0600-bitmask helpers that ~12 callers reach for. configbackup.h is the new home for rotateCorruptFileAside (silent-data-loss recovery, added 0.7.12) and the new ConfigWriteLock (concurrent-writer guard, added this release). The split preempts a third-helper cliff: the file was straddling perms + recovery + lock concerns and a fourth would have made the include cost-of-business across the codebase. Each downstream caller now picks the include it actually needs (config.cpp, claudeallowlist.cpp, settingsdialog.cpp pull both; sites that only set perms keep their secureio.h include). Locked by tests/features/secureio_configbackup_split/ (13 invariants: file-content boundaries, non-copyable lock, every caller's include set). Pre-fix source fails 4; post-fix all 13 pass.

Changed

  • Behavioural test coverage extended to the parse-failure mirror sites in ClaudeAllowlist + SettingsDialog. The 0.7.12 silent-data-loss-on-parse-failure refuse pattern shipped in three sites — Config::load, ClaudeAllowlistDialog::saveSettings, and SettingsDialog::install{ClaudeHooks,ClaudeGitContextHook} — but only Config had a regression test (tests/features/config_parse_failure_guard/). The other two were locked only by adjacent grep-style asserts inside other feature tests. Now they have their own dedicated test: tests/features/settings_parse_failure_mirror/ (8 invariants: rotation call site, return-false-after-rotation gating, open-failure branch distinct from parse-failure branch, comment anchors explaining the clobber-risk reasoning). Closes the ROADMAP "Allowlist + settings-dialog feature-test analogs" item. Pre-fix source already passes — these tests lock previously shipped behaviour against future regressions, not catch a new bug.

v0.7.30

0.7.30 — Session-file integrity — SHA-256 envelope + qUncompress pre-flight + cell-loop status checks

Theme: Session-file integrity. Three ROADMAP § 0.7.12 Tier 2 items shipped together from the post-0.7.27 grouping plan, all in sessionmanager.cpp: a SHA-256 envelope around the qCompress payload, a pre-flight on qCompress's 4-byte uncompressed-length prefix, and per-cell QDataStream::status() checks inside the decode loops.

Added

  • V4 SHA-256 envelope around session-file payload (SessionManager::serialize, SessionManager::restore, SessionManager::ENVELOPE_MAGIC, SessionManager::ENVELOPE_VERSION). Was: session files at $XDG_DATA_HOME/ants-terminal/sessions/session_<tabId>.dat were raw qCompress output with no payload integrity. Anyone with write access to that directory (the user's own UID, a compromised local process, a runaway pip install post-exec, an npm dependency) could plant a crafted session that fed arbitrary codepoints, fg/bg colors, and attribute flags into the grid on next restore — a render surface, not a sandbox. Now: serialize wraps the qCompress output in a V4 envelope [SHEC magic (0x53484543)][envelope version=1][SHA-256 of payload (32 bytes)][payload length (uint32)][compressed payload]; restore peeks the magic, verifies the hash, and refuses to restore on version mismatch, length disagreement, or hash mismatch. Inner QDataStream format unchanged (still V3) — the integrity layer is framing-only. Legacy V1-V3 files (no envelope) continue to load via the magic-peek fall-through; their next save writes them out as V4 organically. Regression test tests/features/session_sha256_checksum locks four invariants — serialize emits the envelope (INV-1), restore peeks the magic and verifies the hash with a return-false on mismatch (INV-2), ENVELOPE_MAGIC/ENVELOPE_VERSION declared on SessionManager with the agreed-upon magic value (INV-3), envelope version remains 1 at this milestone (INV-4). Pre-fix source fails eight invariant assertions; post-fix all four pass. ROADMAP § 0.7.12 Tier 2 entry retired.

  • qUncompress length-prefix pre-flight (SessionManager::restore, SessionManager::MAX_UNCOMPRESSED). Was: a crafted file claiming 500 MB uncompressed in qCompress's 4-byte big-endian prefix triggered a 500 MB allocation that the post-hoc raw.size() > 500MB cap could only catch after the damage was done — and the on-disk payload could be tiny, so the allocation pressure showed up with no concomitant disk-space anomaly. Now: restore reads the first 4 bytes of the compressed payload, reconstructs the big-endian uint32, and rejects any claim above MAX_UNCOMPRESSED (500 MB) BEFORE qUncompress runs — constant-time, no allocator pressure. Short-payload guard (compressed.size() < 4) keeps the same path safe against truncated inputs that can't carry a length prefix at all. The post-decompression cap remains as a defense-in-depth backstop against payloads that under-claim and over-deliver. Regression test tests/features/session_qcompress_length_guard locks four invariants — pre-flight reconstruction precedes qUncompress (INV-1, INV-3), MAX_UNCOMPRESSED is the named constant (INV-2), short-payload guard exists (INV-4). Pre-fix source fails four invariant assertions; post-fix all four pass. ROADMAP § 0.7.12 Tier 2 entry retired.

Changed

  • QDataStream::status() checks inside cell-decode loops (SessionManager::restore readCell / readCombining). Was: the readCell lambda was void; a stream truncated mid-cell still flowed default-constructed QRgb and uint8_t values through QColor::fromRgba etc. into the cell, silently writing uninitialized fg/bg/flags into the grid. The surrounding loops didn't check per-iteration status either, so the grid kept accepting cells from a stream already at ReadPastEnd. Now: readCell returns bool and short-circuits on in.status() != QDataStream::Ok; every call site (scrollback cells, screen cells in range, screen cells skipped on width shrink, screen cells skipped on height shrink) is guarded by if (!readCell(...)) return false. The combining-character helper checks status after each codepoint read, so a stream truncated mid-codepoint can't push default-constructed 0 into the combining map either. Pre- fix, a partial save (kernel crash mid-fsync, disk-full mid-write, hostile sender truncating the envelope payload) could materialize as garbage cells in the next restore — not a crash, but a corrupted scrollback. Regression test tests/features/session_cell_loop_stream_status locks three invariants — readCell returns bool with status check (INV-1), every call site uses if (!readCell(...)) and at least four sites exist (INV-2), readCombining inner codepoint loop checks status (INV-3). Pre-fix source fails three invariant assertions; post-fix all three pass. ROADMAP § 0.7.12 Tier 2 entry retired.

v0.7.29

0.7.29 — Audit pipeline II (SARIF suppressions[] + regex-DoS watchdog + 96-bit dedup)

Theme: Audit pipeline II — output quality. Three ROADMAP § 0.7.12 items shipped together from the post-0.7.27 grouping plan, all in auditdialog.cpp: SARIF v2.1.0 result.suppressions[] array, a regex-DoS watchdog on user-supplied patterns, and a widened 96-bit dedup key.

Added

  • SARIF v2.1.0 result.suppressions[] array (AuditDialog::exportSarif, AuditDialog::loadSuppressions, AuditDialog::saveSuppression, m_suppressionReasons map, Finding::suppressed). Was: SARIF export silently dropped suppressed findings (those whose dedup key matched ~/.audit_suppress), producing a falsely-clean report that defeated the suppression-trend telemetry already computed for the in-app dashboard. Now: a parallel QHash<QString, QString> m_suppressionReasons map populated alongside m_suppressedKeys carries the user's free-text reason from the JSONL file into memory; the parse pipeline marks suppressed findings with Finding::suppressed = true instead of dropping them; render paths (UI, HTML, plain-text) continue to filter via isSuppressed; the SARIF export iterates ALL findings and attaches a suppressions[] block (kind: external, state: accepted, justification: the user's reason) per SARIF v2.1.0 §3.34. GitHub Code Scanning / SonarQube / VSCode SARIF Viewer now see the full audit picture and can compute fires-vs-suppressions ratios across export boundaries. Regression test tests/features/audit_sarif_suppressions locks five invariants — map declaration (INV-1), loadSuppressions populates + clears (INV-2), saveSuppression mirrors (INV-3), exportSarif emits the suppressions JSON property with external/accepted fields (INV-4), exportSarif no longer drops on isSuppressed (INV-5). Pre-fix source fails seven invariant assertions; post-fix all five pass. ROADMAP § 0.7.12 Tier 2 entry retired.

  • Regex-DoS watchdog on user-supplied audit patterns (AuditDialog::isCatastrophicRegex, AuditDialog::hardenUserRegex, applyFilter, loadAllowlist). Was: user patterns from audit_rules.json (OutputFilter::dropIfMatches) and .audit_allowlist.json (AllowlistEntry::lineRegex) flowed straight into QRegularExpression with no shape check and no match-time bound. A pathological pattern committed in either file could pin the GUI thread for seconds with classic catastrophic backtracking on adversarial scanner output — (.+)+, (\w*)*, (.*)+ against long lines. Now: a static isCatastrophicRegex heuristic rejects nested-quantifier shapes (a quantified group whose body itself contains a quantifier) at compile time with a qWarning naming the offending pattern; the rule continues to run without the filter rather than refusing to load. Patterns that pass the shape check are wrapped in PCRE2's (*LIMIT_MATCH=100000) inline option via hardenUserRegex so even catastrophic shapes that slip past the heuristic have a bounded match-step budget — PCRE2 returns "no match" on overrun (fail-safe). 100 k steps handles every sane pattern (typical match completes in < 1 k) and aborts adversarial patterns within milliseconds. Regression test tests/features/audit_regex_dos_watchdog locks four invariants — the helper exists and is invoked at both user-pattern entry points (INV-1), it recognizes nested-quantifier shapes (INV-2), the LIMIT_MATCH=N cap is in the [1k, 1M] sane range (INV-3), loadAllowlist emits a qWarning on rejection (INV-4). Pre-fix source fails four invariants; post-fix all four pass. ROADMAP § 0.7.12 Tier 2 entry retired.

Changed

  • Widen computeDedup from 64 to 96 bits (computeDedup, AuditDialog::isSuppressed, Finding::dedupKey). Was: SHA-256 truncated to 16 hex chars (64 bits) — same key serves as the SARIF partialFingerprint, the suppression-JSONL key, the rule-quality bucket, and the in-app "Suppress" anchor URL. The 64-bit collision threshold (~2³² entries at 50 % collision probability) was comfortable but tight given the multi-role usage; a single false-collision cost a wrong-finding suppression. Now: .left(24) (96 bits) raises the birthday threshold to ~2⁴⁸ for 8 extra bytes per stored key — well past any plausible project's lifetime collection. A new bool AuditDialog::isSuppressed(const QString &dedupKey) const helper encapsulates a backward-compat lookup: match either the full 24-char key OR the leading 16-char prefix, so existing pre-0.7.29 user ~/.audit_suppress entries continue to suppress new 24-char findings without forcing a migration. Six render-pipeline call sites that previously read m_suppressedKeys.contains(f.dedupKey) now route through the helper. Regression test tests/features/audit_dedup_96bit locks four invariants — width ≥ 24 hex chars (INV-1), isSuppressed exists with .left(16) legacy path (INV-2), zero raw m_suppressedKeys.contains(f.dedupKey) sites remain (INV-3), saveSuppression mirrors into m_suppressionReasons (INV-4). Pre-fix source fails five invariant assertions; post-fix all four pass.

85/85 ctest green; drift check clean.

v0.7.28

0.7.28 — Audit pipeline I (process-side robustness)

Theme: Audit pipeline I — process-side robustness. Three ROADMAP § 0.7.12 Tier 2 items shipped together as bundle 0.7.28 from the post-0.7.27 grouping plan: per-tool timeout overrides, incremental QProcess output drain with a 64 MiB cap, and a distinct tool-crash warning that no longer hides as "0 findings."

Changed

  • AuditCheck::timeoutMs field — per-check QProcess timeout (AuditCheck, AuditDialog::runNextCheck, ctor lambda). Was: m_timeout->start(30000) hard-coded inside runNextCheck, with the timeout-handler lambda printing a literal "Timed out (30s)" warning. Slow tools (cppcheck on a 500k-line tree, semgrep with rule-pack compile, osv-scanner rate-limited by OSV.dev, trufflehog over the full git history, clang-tidy / clazy on Qt-heavy code) routinely exceeded the 30 s cap and got demoted to tool-health warnings instead of producing findings. Now: a new int timeoutMs = 30000; trailing field on the AuditCheck aggregate (default preserves the pre-fix global so positional call sites stay correct), m_timeout->start(check.timeoutMs) at the use site, and the timeout-warning string formatted from the actual cap. populateChecks ends with a calibration loop that bumps known-slow tool IDs to 60 s (cppcheck, cppcheck_unused, clang_tidy, clazy), 90 s (semgrep), or 120 s (osv_scanner, trufflehog). Regression test tests/features/audit_per_tool_timeout locks four invariants — the field declaration (INV-1), the per-check use site with no hard-coded 30000 (INV-2), the parameterised warning message (INV-3), at least one calibration override above 30 s (INV-4). Pre-fix source fails all four; post-fix all four pass. ROADMAP § 0.7.12 Tier 2 entry retired.

  • Incremental QProcess output drain with 64 MiB cap (AuditDialog::onCheckOutputReady, AuditDialog::onCheckErrorReady, AuditDialog::onCheckFinished). Was: m_process->readAllStandardOutput() called exactly once inside onCheckFinished. Until that moment, QProcess accumulated internal buffers without bound — a runaway semgrep against generated code or a buggy plugin emitting a tight printf loop could buffer hundreds of megabytes before the timeout fired, with the audit dialog showing a frozen progress bar the whole time. Now: the constructor wires readyReadStandardOutput and readyReadStandardError to new onCheckOutputReady / onCheckErrorReady slots that append incrementally to m_currentOutput / m_currentError; if the combined size exceeds MAX_TOOL_OUTPUT_BYTES = 64 * 1024 * 1024, the process is killed and m_outputOverflowed is flagged. On finished() the runner drains any tail data (Qt may buffer between the last readyRead and finished()), reads from the member buffers instead of the live process, and surfaces a distinct "Output exceeded N MiB cap" warning when overflow occurred. Buffers reset before each check so output never concatenates across checks. A small connectProcessSignals() helper centralises the three connections so the timeout-kill / cancel-kill / reconnect cycles never lose a drain slot. Regression test tests/features/audit_incremental_output_drain locks six invariants — both readyRead* connections (INV-1), buffer members declared (INV-2), MAX_TOOL_OUTPUT_BYTES cap in the 4 MiB ≤ cap < 1 GiB defensible range (INV-3), per-check reset (INV-4), onCheckFinished reads buffers not the live process (INV-5), overflow path kills the process (INV-6). Pre-fix source fails on the connect-grep alone; post-fix all six pass. ROADMAP § 0.7.12 Tier 2 entry retired.

Fixed

  • Tool-crash distinct from "no findings" (AuditDialog::onCheckFinished). Was: signature void AuditDialog::onCheckFinished(int /*exitCode*/, QProcess::ExitStatus /*status*/) — both parameters discarded via comments. A tool that segfaulted with empty stdout was indistinguishable from a clean run with zero findings, silently hiding both a real bug in the tool AND any findings the tool would have reported. Same went for non-zero-exit-with-stderr-only patterns (clang-tidy missing compile_commands.json, semgrep failing to parse a rule). Now: parameters named (exitCode, exitStatus); function branches on QProcess::CrashExit to emit a "Tool crashed (signal exit)" warning; on exitCode != 0 && stdout empty && stderr non-empty to emit a "Tool exited N with no findings on stdout" warning. Both warnings demoted to Severity::Info so they don't sort to the top of the report next to real findings. The four exit modes (timeout, overflow, crash, non-zero-with-stderr-only) all funnel through a single small file-scope makeToolHealthWarning() helper that centralises the row shape (Info severity, warning flag, distinct message prefix). Regression test tests/features/audit_tool_crash_distinct locks four invariants — named parameters (INV-1), CrashExit branch (INV-2), warning emission with crash/exit-message (INV-3), Severity::Info demotion (INV-4). Pre-fix source fails all four; post-fix all four pass. ROADMAP § 0.7.12 Tier 2 entry retired.

81/81 ctest green; drift check clean.

v0.7.27

0.7.27 — PTY-handler hardening (close_range + EAGAIN write queue)

Theme: PTY-handler hardening sweep. Two ROADMAP § 0.7.12 Tier 2 items shipped together — child-side FD closure no longer relies on a hard-coded fd<1024 cap, and master-side writes no longer drop data on EAGAIN.

Security

  • PTY child uses close_range(2) with RLIMIT_NOFILE-bounded fallback (Pty::start). Was: for (int fd = 3; fd < 1024; ++fd) ::close(fd) — silently leaked any inherited FD with index ≥ 1024 into the user's shell on systemd-service / container / hardened-server profiles where rlim_cur sits above the hard-coded ceiling. Qt's display socket, D-Bus session connection, plugin HTTP sockets, Lua VM eventfds, and the remote-control IPC socket are all valid leak candidates; a leaked AI HTTP socket is a credentials-exfiltration vector, a leaked D-Bus socket lets the shell impersonate the user's desktop session, a leaked remote-control socket is a UID-scope RCE-by-proxy vector. Now: post-fork child branch issues ::syscall(SYS_close_range, 3, ~0U, 0) first — single signal-safe syscall on Linux 5.9+, atomic over the whole range, ignores the soft cap. If the syscall returns non-zero (kernel < 5.9, missing build-time SYS_close_range), the fallback path consults getrlimit(RLIMIT_NOFILE) and iterates up to the runtime soft cap, capped at 65536 to bound the worst-case syscall storm on profiles where rlim_cur is in the hundreds of thousands. Two new headers (<sys/resource.h>, <sys/syscall.h>) added to ptyhandler.cpp. Regression test tests/features/pty_closefrom locks five invariants — SYS_close_range referenced inside the post-fork child branch (INV-1), the hard-coded fd<1024 loop is gone (INV-2), fallback consults RLIMIT_NOFILE (INV-3), required headers included (INV-4), fallback bound is capped to 65536 to avoid the unbounded-loop pathology (INV-5). Pre-fix source fails 4 of 5; post-fix all 5 pass. CWE-403 reference; ROADMAP § 0.7.12 Tier 2 entry retired.

Fixed

  • Pty::write queues on EAGAIN instead of dropping bytes (Pty::write, new Pty::onWriteReady). Was: the master FD is non-blocking, so a slow consumer on the slave side could fill the kernel PTY buffer; on EAGAIN the write loop's else-clause broke out with the comment // EAGAIN or fatal error, silently dropping the unwritten remainder. Behaviourally invisible during normal interactive use (the kernel buffer drains within microseconds) but provoked by realistic bursts — large pastes, AI-dialog command insertions, plugin-driven keystroke floods, or any write into a slave whose reader is suspended. Now: a new m_pendingWrite byte buffer and a QSocketNotifier( QSocketNotifier::Write) on m_masterFd (initially disabled because PTY masters are writable nearly continuously and a hot notifier would burn CPU). On EAGAIN the unwritten remainder is moved to the queue and the notifier is enabled; when the kernel signals writability, onWriteReady drains the queue and disables the notifier on completion. Fresh write() calls arriving while the queue is non-empty append rather than bypass, preserving FIFO ordering so newer keystrokes never race ahead of older ones. Queue capped at 4 MiB (MAX_PENDING_WRITE_BYTES) — large enough for realistic bursts, small enough that a permanently-stuck slave cannot OOM the GUI process. Regression test tests/features/pty_write_eagain_queue locks seven invariants — write-side notifier creation (INV-1), queue member declared (INV-2), notifier pointer member declared (INV-3), onWriteReady slot declared and connected (INV-4), EAGAIN handled distinctly inside Pty::write (INV-5), 4 MiB cap present (INV-6), direct-write path checks queue first for FIFO ordering (INV-7). Pre-fix source fails 5 of 7; post-fix all 7 pass. ROADMAP § 0.7.12 Tier 2 entry retired.

78/78 ctest green; drift check clean.

v0.7.26

0.7.26 — OpaqueMenuBar (root-cause fix for menubar transparency)

Theme: menubar opacity — the actual root-cause fix. 0.7.25's palette + widget-local-QSS belt-and-suspenders did not in fact suspend any belt: the user reported the desktop wallpaper still showing through the menubar strip on KWin + Breeze + Qt 6.

Fixed

  • Menubar background now actually paints opaquely under WA_TranslucentBackground (OpaqueMenuBar, MainWindow::applyTheme). User report 2026-04-25: "I can clearly see my desktop background behind it." Root cause: under WA_TranslucentBackground parent + WA_OpaquePaintEvent on the menubar, none of the conventional opaque-paint paths runs reliably on every WM/style stack. Specifically: autoFillBackground is suppressed by WA_OpaquePaintEvent (the contract is "the widget paints all pixels"), QPalette::Window is only consulted by autoFillBackground and inherits the suppression, and QSS QMenuBar { background-color: … } — which is supposed to draw via QStyleSheetStyle::drawControl(CE_MenuBarEmptyArea) — is silently skipped on KWin + Breeze + Qt 6 when WA_OpaquePaintEvent is set, because the QSS engine assumes the widget owns those pixels. Net effect: every safeguard 0.7.25 added was correctly installed and not painting anything; the menubar surface stayed cleared-to-transparent and the compositor showed the wallpaper through. New file src/opaquemenubar.h defines OpaqueMenuBar, a QMenuBar subclass whose paintEvent runs QPainter::fillRect(rect(), m_bg) with CompositionMode_Source before delegating to QMenuBar::paintEvent. That is the only path that actually keeps the WA_OpaquePaintEvent contract honest under translucent parents. m_menuBar is now an OpaqueMenuBar and applyTheme sets the fill colour via setBackgroundFill(theme.bgSecondary). The 0.7.25 palette + widget-local QSS calls are kept, no longer load- bearing for the strip's opacity but useful for child-widget theme propagation and for scoping the ::item :hover/:selected/:pressed rules on the menubar itself rather than relying on the top-level cascade. Regression test tests/features/menubar_hover_stylesheet extended with INV-8 — three new assertions that pin the OpaqueMenuBar construction site, the setBackgroundFill call in applyTheme, and the presence of paintEvent + fillRect inside src/opaquemenubar.h. The full spec is rewritten with the per-iteration history (0.6.42 → 0.7.26) and an explicit "manual verification" recipe (bright wallpaper + dark bgSecondary + ~0.85 opacity on KWin) so any future drift is reproducible by hand.

77/77 ctest green; drift check clean.

v0.7.25

0.7.25

Theme: memory-DoS hardening on OSC 8 hyperlinks and Kitty APC graphics chunks, plus a menubar-opacity fix reported mid-session.

Security

  • OSC 8 URI cap + Kitty APC chunk-buffer cap (TerminalGrid). Two attacker-controlled accumulators were previously bounded only by the VT parser's per-envelope 10 MB ceiling, with no downstream ceiling of their own. A hostile program could emit one \x1b]8;;<10MB-URI>\x07 per scrollback line and wedge tens of GB of URI bytes into per-row HyperlinkSpan copies + scrollback, or keep sending \x1b_G...,m=1,...\x07 frames without ever closing with m=0, growing m_kittyChunkBuffer unbounded. src/terminalgrid.h now declares MAX_OSC8_URI_BYTES = 2048 (real URLs are <2 KiB) and MAX_KITTY_CHUNK_BYTES = 32 MiB (larger than any realistic chunked image upload). Oversized OSC 8 URIs take the invalid-scheme drop path; following text prints unlinked. The APC chunk path clears and shrink_to_fits the staging buffer on cap breach so a subsequent m=0 sees an empty buffer rather than attacker-prepended garbage. Regression test tests/features/osc8_apc_memory_caps locks 5 invariants — pre-fix fails INV-OSC8-B and INV-APC-B.

Fixed

  • Menubar renders opaque under WA_TranslucentBackground (MainWindow::applyTheme). User report 2026-04-24. The top-level QSS cascade could race with the compositor damage rect under WA_TranslucentBackground, so on frames where the compositor invalidated the chrome region before QSS polish produced a paint, the menubar showed through. applyTheme now installs a belt-and-suspenders opaque fill on m_menuBar: QPalette::Window = theme.bgSecondary (so autoFillBackground paints before QSS) + a widget-local setStyleSheet(QMenuBar ...) block. Mirrors the pattern already used on the custom title bar and status bar. tests/features/menubar_hover_stylesheet extended with INV-7.

77/77 ctest green; drift check clean.

v0.7.24

v0.7.24 — wide-char overwrite zeroes the mate

Tier 2 hardening: wide-char (CJK / wide emoji) overwrite no longer strands its mate.

Fixed

  • Wide-char overwrite now zeroes the stranded mate (TerminalGrid). A wide character occupies two adjacent cells — a first half (isWideChar=true, codepoint=CP) and a continuation (isWideCont=true, codepoint=0). Before this release, three write paths in src/terminalgrid.cpp left half-pairs in an inconsistent state when a new write landed on only one half:
    • handlePrint narrow write over a continuation left the mate at col-1 still claiming isWideChar=true with no neighbor (rendered with a gap);
    • handlePrint narrow write over a first half left the old continuation at col+1 claiming isWideCont=true with no mate (blocked selection / copy);
    • handlePrint wide write shifted by one from an existing wide pair orphaned the old continuation at col+2;
    • the SIMD-coalesced fast path handleAsciiPrintRun had the same left/right edge issues on its write span.
  • Consolidated into a single breakWidePairsAround(row, startCol, endCol) helper that runs before every write site: it blanks the stranded first half on the left edge and clears the stranded continuation on the right edge.

Regression test tests/features/wide_char_overwrite_mate locks five subcases — pre-fix source fails four of them (INV-1, INV-2, INV-3, INV-4), post-fix all five pass.

See CHANGELOG.md for the full entry.

v0.7.23

0.7.23 — Background Color Erase on scroll + erase paths

Theme: xterm-compatible Background Color Erase (BCE) across every scroll and erase path. 0.7.12 Tier 2 hardening item.

Fixed

  • BCE on scroll and insert/delete paths (TerminalGrid). Apps that paint with a non-default SGR background (\e[44m etc.) and then scroll, insert lines, delete lines, delete chars, or insert blanks expect the newly-exposed cells to inherit the current bg — this is the xterm convention vim/less/tmux/mc/htop rely on for full-screen painted backgrounds. Previously takeBlankedCellsRow() hardcoded m_defaultBg, so CSI L / CSI M / CSI S / CSI T / LF-past-scroll-bottom all produced default-bg gaps through any painted region. deleteChars / insertBlanks used raw m_currentAttrs.bg without the .isValid() fallback that clearRow already had. Consolidated the policy into one TerminalGrid::eraseBg() helper; every erase/scroll callsite now reads from it. Regression test: tests/features/bce_scroll_erase/spec.md — 10 behavioral subcases (IL, DL, SU, SD, DCH, ICH, ED2, EL0, LF-scroll, SGR-reset-before-erase). Pre-fix source fails 5 subcases (IL/DL/SU/SD/LF-scroll); post-fix all 10 pass.

v0.7.22

0.7.22 — Help → About menu (GUI version indicator)

Theme: user-visible About menu (user ask 2026-04-24 — "How do I see what version of Ants Terminal I am running? Can you please add a GUI version?").

Added

  • Help → About Ants Terminal… menu. New rightmost menu on the menubar (matching Linux desktop HIG: Help is always last). Two actions:

    • About Ants Terminal… — shows a rich-text dialog with the running ANTS_VERSION (read directly from CMake's project-wide single source of truth, no hardcoded literal), the Qt runtime version (via qVersion()), the Lua engine version (when compiled with ANTS_LUA_PLUGINS), a one-line summary, and a clickable GitHub homepage link (TextBrowserInteraction enabled so URLs in the dialog open in the user's browser).
    • About Qt… — the stock QMessageBox::aboutQt dialog. Inherits future Qt-version bumps automatically.

    Pre-fix, ants-terminal --version on the command line was the only path to read the running version. Regression test: tests/features/help_about_menu/spec.md — 6 invariants (Help-menu-last, About-action present, ANTS_VERSION referenced not hardcoded, Qt::RichText + Qt::TextBrowserInteraction set, About-Qt action routed to QMessageBox::aboutQt, no "0.7." literal inside the handler body).

v0.7.21

0.7.21 — Lua sandbox hardening: string.dump strip + t-mode load + hook clear

Theme: Lua sandbox hardening trio from the 0.7.12 /indie-review. Three small defense-in-depth fixes in LuaEngine, behavioral + source-grep regression test locked to all three.

Security

  • string.dump removed from the plugin sandbox. string.dump(f) returns the bytecode serialization of a Lua function. Lua 5.4 has no bytecode verifier — the loader parses any byte sequence beginning with \x1b as a binary chunk, and crafted bytecode can corrupt Lua's internal state and escape the sandbox. load/loadstring/loadfile are already nilled at init, so there is no supported round-trip from string.dump back to executing bytecode, but a future C API added to ants.* that wraps luaL_loadbuffer with plugin- supplied data would reopen the attack surface. Closing the primitive at the sandbox layer — lua_setfield(m_state, -2, "dump") scoped to the string table, not a blanket lua_setglobal — is cheaper than auditing every future C API for the same rule.
  • LuaEngine::loadScript forces "t" (text-only) load mode. The pre-fix path luaL_dofileluaL_loadfileluaL_loadfilex(L, path, nullptr) accepted both text and binary chunks at the loader level. The 0x1b-first-byte peek in loadScript was the first gate; the loader call is now the second gate. A future refactor that drops the peek still gets rejection at the Lua level.
  • Instruction-count hook cleared before lua_close in shutdown. lua_close runs every pending __gc metamethod in dependency order. Metamethods can execute arbitrary Lua code which the count hook observes. If the hook fires mid- close and walks back into registry data or the dying engine pointer via __ants_engine, we get a UAF window. Clearing the hook first with lua_sethook(m_state, nullptr, 0, 0) removes that window; the C-side cleanup proceeds without any Lua-level observer.

Regression test: tests/features/lua_sandbox_hardening/spec.md — 6 invariants, behavioral (string.dump is nil, valid-text loads, 0x1b-first-byte rejected) plus source-grep on the three fix tokens (luaL_loadfilex(..., "t"), lua_sethook(m_state, nullptr, 0, 0) before lua_close, lua_setfield(m_state, -2, "dump") scoped to the string table). Verified to fail against pre-fix source via git stash — 5 of 9 invariants flip red on the regression, all green after the fix lands.

v0.7.20

0.7.20 — Tier 2 hardening: debug.log perms + audit path-traversal + settings-dialog reload

Theme: Tier 2 hardening sweep — three open 📋 items from the 0.7.12 /indie-review landed as one release. Each has a behavioral regression test, source-grep on the fix's load-bearing tokens, and was verified to fail against pre-fix source via git stash before locking.

Security

  • debug.log lands 0600 regardless of umask. ~/.local/share/ants-terminal/debug.log was being created with the process umask (typically 0644 under 0022). The log can include PTY keystrokes (via the input / pty categories), AI endpoint request+response bodies (network), OSC 133 HMAC digest material (shell), and Claude transcript parse state (claude) — every one of those is material that must not be world-readable. DebugLog::setActive now calls setOwnerOnlyPerms twice after the file opens: once on the QFileDevice to cover the just-opened fd, and once on the path string to narrow any pre-existing 0644 file that append reused from a prior (pre-fix) run. Fix uses the project-standard secureio.h helper, not a raw QFile::setPermissions bitmask. Regression test: tests/features/debuglog_perms/spec.md (4 invariants: fresh-open perms, clear-then-reopen, pre-existing 0644 narrowed to 0600, source uses the helper).
  • Audit path-traversal guard on findings' file field. User- supplied audit rules, audit_rules.json in a cloned project, and external scanner regex outputs can all produce findings whose file field is e.g. ../../etc/passwd. Pre-fix, six call sites across AuditDialog naively concatenated m_projectPath + "/" + f.file and passed the result to readSnippet / lineIsCode / comment scans / AI-triage POST bodies — a textbook CWE-22 + OWASP LLM06 (sensitive-information disclosure via LLM) chain, since the AI-triage surface exfiltrates snippet contents to the configured /v1/chat/completions endpoint. New AuditDialog::resolveProjectPath helper canonicalizes the candidate path (resolves .. and dereferences symlinks in a single QFileInfo::canonicalFilePath call), requires the result to be anchored under the canonical project root (with a trailing-slash sentinel so /proj-foo can't escape from /proj), and returns QString() on any rejection. All six call sites migrated: dropFindingsInCommentsOrStrings, inlineSuppressed, the enrichment pass, single-finding AI-triage snippet fallback, batch AI-triage snippet fallback, and the dropIfContextContains regex-captured-relPath read. Regression test: tests/features/audit_path_traversal/spec.md — 5 invariants behavioral (via a byte-faithful reference reimpl — production helper is private on the heavy QDialog subclass) plus source-grep on the production code to confirm migration + helper structure (canonicalFilePath + anchored startsWith).

Fixed

  • Settings dialog discarded on external config.json reload. MainWindow caches the SettingsDialog across Preferences... opens; the dialog was constructed with &m_config and populated its widgets from the then-current values at construction. When QFileSystemWatcher fired onConfigFileChanged on an external edit, m_config = Config() reloaded from disk but the cached dialog still held pre-reload widget state. Next Preferences... open would show stale values, and clicking OK would replay them over the fresh Config — silently undoing the external edit. onConfigFileChanged now closes the dialog if visible, calls deleteLater(), and nulls the pointer, so the next open rebuilds from the freshly reloaded Config. Regression test: tests/features/settings_dialog_config_reload/spec.md — 4 source-grep invariants (cache nulled, visible-close gate, deleteLater-not-delete, invalidation scoped to onConfigFileChanged).

v0.7.19

0.7.19 — tab-rename persistence + AppStream CI un-break

Theme: CI un-break + tab-rename persistence. CI had been red since 0.7.17 on the AppStream metainfo validation step — the 0.7.17 release description embedded git clone https://ghp_…@github.com/… as an example of what the new secret-redactor scrubs, and appstreamcli correctly rejects plaintext URLs in <description> bodies. Rewrote the example to use an inline non-URL form; local appstreamcli validate --explain now exits 0. Separately, user asked whether manual tab renames (right-click → "Rename Tab…") survive Ants restart. They didn't — the pin map lived only in MainWindow memory and SessionManager didn't serialize it. Fixed by bumping the session-file schema to V3 with a trailing pinnedTitle field; V2 files still load with the out-param defaulting to empty.

Added

  • Manual tab renames persist across Ants Terminal sessions. User ask 2026-04-24. The right-click "Rename Tab…" pin (m_tabTitlePins) is now written to and read from each per-tab session file, so a tab renamed to "Deploy" or "Prod DB" or "Claude #3" keeps that label after the app exits and relaunches. SessionManager schema bumped to V3: a trailing QString pinnedTitle field is appended after the V2 cwd. V2 files continue to load via the existing in.atEnd() gate — Ants 0.7.18 and earlier can't read V3 files, but since session files are a per-user cache (not an interchange format) that's by design. MainWindow::saveAllSessions threads m_tabTitlePins.value(w) (keyed by the outer tab widget, which may be a QSplitter for split tabs) into the save; restoreSessions pulls the pin back, populates the in-memory m_tabTitlePins map, and sets the tab label directly (pin takes precedence over the shell-derived window title from the saved grid's windowTitle()). Contract locked by tests/features/tab_rename_persist/spec.md — 20 invariants covering the V3 round-trip (three pin lengths including empty), V2 backward compat (hand-crafted V2 stream → restore leaves pinnedTitle out-param empty), and MainWindow source-grep that both save-side and restore-side wiring remain threaded. Verified to fail against pre-fix source before locking.

Fixed

  • AppStream metainfo validation passes. appstreamcli validate --explain failed on the 0.7.17 / 0.7.18 release descriptions because the 0.7.17 block contained git clone https://ghp_…@github.com/… as a scrubber example, and AppStream's description-has-plaintext-url rule rejects raw URLs in description bodies. CI's "Validate AppStream metainfo" step exited 3 on every push since 0.7.17. Rewrote the example as <code>git clone</code> with an embedded <code>ghp_</code> token in the URL. CI build-test step should be green again on this release.

Changed

  • SessionManager::restore initializes optional out-params before reading. Previously, callers that passed a pre-populated QString *cwd or QString *pinnedTitle would see their sentinel survive an older-format load (V2 files leaving pinnedTitle untouched, V1 files leaving cwd untouched). Fixed by clearing both out-params at function entry regardless of the stream's version. The version-gated read blocks still populate them only when the on-disk format actually has the field; the clear-first guarantees they read as empty rather than as whatever the caller happened to pre-fill. Locked by tab_rename_persist I3.