FN-8037 extracted pending-image previews into PendingImagePreviews,
which renders the thumbnail as a decorative alt="" image inside a
button labelled 'Open image <name>'. Point the four stale
getByAltText assertions at that accessible open button instead.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Switch the mobile row alignment from centered to space-between and
widen the icons+Save group to the full row, so the workflow/steps and
option-chip rows share the exact same left/right edges as the bottom
row. Group gaps remain as minimum spacing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Restore the isolated built-core barrel regression guard to the default CLI test lane.
- Hoist recompilation and PostgreSQL fixture setup outside timed test bodies.
- Inject the fixture store into the dynamic extension and retain text-budget assertions.
- Skip cleanly per test when a transitive dist artifact is unavailable.
- Remove the matching CLI quarantine exclusion and ledger entry.
Files changed:
.../src/__tests__/extension-dist-barrel.test.ts | 234 +++++++++++----------
packages/cli/vitest.config.ts | 8 +-
scripts/lib/test-quarantine.json | 5 -
3 files changed, 121 insertions(+), 126 deletions(-)
Fusion-Task-Id: FN-8093
Fusion-Task-Lineage: 352b3675-0579-43bc-acd9-6a11919ed646
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Move the CLI plugin-install persistence coverage to the shared PostgreSQL test harness.
- Replace SQLite central and local database assertions with PostgreSQL schema queries.
- Assert global installation and project-scoped state persistence without a local plugin store.
- Clean up temporary plugin fixtures after the PostgreSQL test.
Files changed:
packages/cli/src/commands/__tests__/plugin.test.ts | 120 +++++++++++----------
1 file changed, 64 insertions(+), 56 deletions(-)
Fusion-Task-Id: FN-8091
Fusion-Task-Lineage: 99b8d1da-54b7-4cd3-a086-9e4461a8aa06
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Align the responsive TaskDetailModal tab-order assertion with the rendered tab set.
- Include Terminal and Cost between Comments and Artifacts in the responsive tab list.
- Document why the regression expectation includes the always-available tabs.
Files changed:
.../__tests__/TaskDetailModal.responsive-and-dependencies.test.tsx | 3 +++
1 file changed, 3 insertions(+)
Fusion-Task-Id: FN-8039
Fusion-Task-Lineage: 86f347a7-ca5a-4a83-8616-4ca303cc6115
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Keep GitLab import details consistent with the shared padded FloatingWindow layout.\n\n- Move the GitLab preview into the shared heading and scrolling-content structure\n- Place the GitLab import control in the shared bottom action bar for mobile reachability\n- Cover cross-provider detail layout and shared mobile-sheet invariants\n\nFiles changed:\n .../dashboard/app/components/GitHubImportModal.tsx | 20 ++++++++++----\n .../__tests__/GitHubImportModal.test.tsx | 32 +++++++++++++++++++---\n 2 files changed, 42 insertions(+), 10 deletions(-)
Fusion-Task-Id: FN-8032
Fusion-Task-Lineage: 150b992a-d5fa-4001-b818-7ece031a6cb5
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Update ChatView header contracts to enter the direct-thread pane before assertions.
- Add a shared mobile direct-thread helper for context-window tests
- Select the active session before mobile and floating header assertions
- Preserve rename coverage for the direct-thread session switcher
Files changed:
.../components/__tests__/ChatView.context-window.test.tsx | 14 ++++++++++++++
.../components/__tests__/ChatView.core-contracts.test.tsx | 9 +++++++++
2 files changed, 23 insertions(+)
Fusion-Task-Id: FN-8053
Fusion-Task-Lineage: a491db31-04b1-4cb5-ac58-c3be297bfe45
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
- One settings-scoped checkbox rule (accent, size, focus ring) so the Advanced-settings
toggle, SettingsToggleRow, ntfy/webhook card headers, and MCP toggle stop falling
back to the browser-default accent.
- Fix the empty/off-screen help bubble on mobile: .notification-provider-header and
.settings-field-label-row are now positioned ancestors for SettingsHelpTip.
- Migrate every remaining inline <small> description across settings sections to the
shared SettingsHelpTip "?" affordance (validation errors and live status stay inline).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Center each wrapped row below the quick-add textarea (workflow/steps,
option chips, icons+Save) so all rows share equal left/right insets on
mobile, instead of options hugging the left edge and Save hugging the
right. Desktop/tablet keep the left-options / right-Save toolbar layout.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Tighten the mobile primary-group gap to --space-xs and the icon-only
controls' min-width floor to 32px so the Save button no longer wraps to
its own line in the board quick-add composer. Touch-target height is
unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
## Problem
Migration 0006 made `project_id` the RLS isolation partition on every
`project`-schema table — stamped by a BEFORE INSERT trigger from the
`fusion.project_id` session GUC, with every PK/unique/FK rewritten to
composite `(project_id, …)`. Eleven tables **also** carried a
caller-supplied domain `projectId` on their TS types and wrote that
domain value into the same physical column.
When the domain value differs from the session GUC, the parent row lands
in the domain partition while child rows (`research_run_events`,
`experiment_session_records`, `eval_task_results`, …) land in the
session partition — and the composite FK fails with SQLSTATE 23503.
Appending an event to a project-owned research run could not persist.
## Fix
**Decision (operator): separate domain column; `project_id` stays the
partition.**
- **Migration `0011_owner_project_id.sql`** adds a nullable
`owner_project_id` domain column to the 11 conflated tables
(`research_runs`, `experiment_sessions`, `todo_lists`, `eval_runs`,
`chat_sessions`, `chat_rooms`, `ai_sessions`, `chat_token_usage`,
`project_insights`, `project_insight_runs`, `cli_sessions`), backfills
it from `project_id` (identical in production, so exact; the
`__legacy_unscoped__` sentinel backfills to NULL), and indexes it.
Idempotent, `to_regclass`-guarded per the 0007 pattern.
- **Stores** (`async-research-store`, `async-experiment-session-store`,
`async-todo-store`, `async-chat-store`, `async-ai-session-store`,
`async-eval-store`, `async-insight-store`, `cli-session-store`, …) stop
writing `project_id` entirely — the trigger/GUC owns the partition — and
map their domain `projectId` field to `owner_project_id` for both reads
and filters. TS types unchanged.
- **Applier** registers `OWNER_PROJECT_ID_SPLIT_VERSION = "0011"` and
advances `SCHEMA_BASELINE_VERSION`.
## Verification (re-run independently of the implementing agent)
- Core `tsc --noEmit`: exit 0 · `pnpm lint`: exit 0 · `pnpm
check:changesets`: exit 0 · `pnpm test:gate`: 185/185
- Full postgres suite: **5 failed / 807 passed** vs a **7 / 804**
baseline — the two conflation round-trips
(`satellite-db-injected-stores` ResearchStore + ExperimentSessionStore)
go green, zero new failures. The remaining 5 are pre-existing
unbound-harness `__meta`/identity failures, unrelated to this change.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* Corrected project-scoped persistence and queries across AI sessions,
chats (rooms + token usage), evaluations/experiments, insights,
research, and todos by separating domain ownership from RLS
partitioning.
* Prevented foreign-key and row-level security violations when storing
or retrieving project-scoped data, including legacy records.
* **Database / New Features**
* Added migration 0011 introducing `owner_project_id` and backfilling
existing rows to preserve ownership while improving isolation.
* **Tests**
* Updated migration-parity coverage to include the new baseline step.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
## Summary
Follow-up on **#2127** (Quality plugin already on `main`). This PR only
lands the remaining Quality deltas that were not merged:
- **Experimental gate fix** — `TaskStore.getSettings()` is async; the
gate now awaits merged settings so enabling
`experimentalFeatures.qualityPlugin` actually works, and status-bearing
errors return structured `{ status, body }` instead of collapsing to
hard failures
- **Done-task QA worktrees** — when a task has no live worktree (typical
after land), preview/task runs create a disposable checkout under
`.fusion/quality-qa/` at the task branch or merge commit so processes
run the **done task’s code**, not project root
- **Hub layout** — shared `ViewHeader` + dashboard spacing/typography so
Quality matches Insights / Compound Engineering / Goals
Scoped to `plugins/fusion-plugin-quality/**` only (rebased onto current
`main`; duplicate plugin-landing commits dropped).
## Test plan
- [ ] Enable **Settings → Experimental → Quality Plugin**, restart if
routes were cold
- [ ] Quality hub: header matches other views; refresh + presets work
- [ ] Done task → QA tab → Start preview uses QA worktree at
branch/merge commit (not project root)
- [ ] Active task with live worktree still uses that worktree
- [ ] Flag off: clear experimental-disabled error (not generic empty
failure)
- [ ] `pnpm --filter @fusion-plugin-examples/quality test` (32 tests)
Mobile operator report: the import screen spent ~4 rows on chrome — a
provider row, a tab row, and a boxed ORIGIN/filter/Load stack — leaving
only **~9 issues visible**. Import also sat *above* the preview it acts
on, and a second Import in the list footer let you import an issue whose
body you had never opened.
## Before → After (measured at 412px)
| | before | after |
|---|---|---|
| control chrome | ~4 stacked bands (93px) | **one wrapping row (70px)**
|
| toolbar | boxed band, origin on its own line | **single 36px row** |
| issues visible | ~9 | **~13** |
| Import | list footer **and** preview header | **detail bottom bar
only** |
## Layout
- **Provider, type tabs, origin, filter and Load share one row.** It
wraps rather than clipping at the narrowest widths.
- **Load is icon-only** — the label survives as `aria-label`/`title`, so
the accessible name is unchanged (the pre-existing role+name query still
finds it, which is what proves nothing was lost for screen readers).
- **The labels filter is a popover.** Its trigger doubles as the readout
— it renders the active labels and takes an `is-active` cue — so
collapsing never hides applied state. Dismisses on outside pointerdown
or Escape; Escape `stopPropagation` keeps the modal from closing along
with it.
- **Origin stays visible as an inline chip** (per your call): it's
context for what you're importing, so it flattens from a stacked
ORIGIN/repo block rather than hiding in the popover.
- Removed the toolbar's `flex: 1 1 100%` mobile stacking — it dated from
when the toolbar was a full-width band and was forcing origin to claim
an entire line.
- Also neutralised `[data-theme="light"] .github-import-toolbar`, which
re-applied the band background later in the cascade at equal specificity
(light mode only).
## Actions — one place to import
Import + Close issue move from the preview header to a **bottom action
bar**: commit actions belong below the content they act on and within
thumb reach, matching the modal's own Cancel bar. The list footer's
duplicate Import is removed (Cancel stays — the modal still needs a
dismiss), so **an issue can no longer be imported sight-unseen**.
## Note for review — interaction with #551a2a3c1
`551a2a3c1` ("align import detail header") landed mid-work and
conflicted. Its panel-padding work is preserved untouched. Its header
rules were kept **as-is** because they remain correct for a lone label,
but its stated rationale — dropping `space-between` so "Close issue and
Import stay grouped as a pair at the end" — is moot now that those
actions live in the bottom bar. The comment is marked superseded rather
than left asserting something untrue.
## Verification
Measured in a real browser at 412px (jsdom has no layout, so the tests
pin structure and behaviour, not geometry): chrome 93→70px, toolbar a
single 36px row, ~13 issues visible, filter popover clamped inside the
viewport and autofocused, detail bar below the content with an unclipped
Import label. Re-verified after the rebase.
**90 tests pass** · gate green (294/122/63) · lint clean · typecheck
clean.
Seven existing tests encoded the old UI (labelled Load, always-open
filter input). They were **updated to the new contract rather than
appeased** — the filter assertions now exercise the whole affordance
(collapsed → open → filters), and FN-7657 persistence is asserted via
the *collapsed trigger*, proving both that state survives remount
**and** that a restored filter stays visible, which is the real risk
when a control collapses.
Two things worth knowing:
- The footer guard was **confirmed non-vacuous**: reintroducing the list
Import fails it.
- One new CSS rule is deliberately a child selector
(`.github-import-controls > *`) rather than naming
`.github-import-tabs`, because `GitHubImportModal.test.tsx` extracts
base rules with a naive first-match regex
(`/\.github-import-tabs\s*\{[^}]*\}/`) that a rule mentioning them
*above* the originals would silently hijack.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Added a more compact, responsive mobile layout for the GitHub import
screen.
* Moved Import and Close issue actions to the preview’s bottom action
bar.
* Added a collapsible labels filter popover with keyboard and
outside-click dismissal.
* Consolidated mobile controls into a single compact row.
* Simplified the Load action to an accessible icon-only button.
* **Bug Fixes**
* Removed duplicate Import controls from the list footer.
* Improved control sizing, wrapping, and preview action positioning on
narrow screens.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow-up to #2158. That PR was squash-merged while this last commit was
still in flight, so these four fixes never landed on main.
### Source Control had no icon in the nav
A section's own `icon` now wins over the generic scope glyph, which
becomes the fallback. `icon` previously only rendered when `scope` was
`undefined`, so **any** scoped section was forced to the globe/folder
pair and could not identify itself — Source Control was
indistinguishable from MCP Servers and Scheduling. Both Source Control
entries now carry a git-branch icon. Nothing is lost: these sections
spell their tier out in the label (`· Global` / `· Project`), and the
aria-label and tooltip still announce it.
### Settings opens on Authentication
Previously "General · Global". Nothing else in the product works until a
provider is connected — the dashboard's own empty state sends operators
to Settings for exactly this. Authentication leads the **AI & Models**
group rather than sitting under Integrations: connecting a provider and
picking the models it gates are one task, in that order. A test asserts
the landing section is never one the Advanced switch hides, so a
default-configured operator cannot land on a section their own nav does
not list.
### The `fn` CLI binary panel moved behind Advanced, at the bottom
It used to render *first* inside "General · Global" — an
install/version/path panel was the first thing an operator saw on
opening Settings. It is machine plumbing touched once, or when an
install breaks, so it belongs with the other specialist surfaces.
### Changing section starts you at the top of it
Sections keep no scroll of their own, so the container's offset carried
over: leaving a long section scrolled halfway and picking a short one
landed mid-content — on mobile, often past everything, on an apparently
blank screen. Guarded by a ref rather than by reading the highlight key,
because a search jump also changes `activeSection` and that key
self-clears ~1.6s later, which would re-run the effect and yank the
operator off the row they just jumped to.
## Verification
- Typecheck clean (`tsconfig.app.json` — the one that covers `app/`).
- Settings suites run against this branch and against plain main:
**identical 67-failure set, 0 new**. Those 67 are pre-existing on main
(51 scheduling-merge + 16 remote-notifications) and are untouched here.
- Browser-verified: lands on Authentication; Source Control renders
`lucide-git-branch`; CLI Binary is last and hidden with Advanced off;
section change resets scroll 1200 → 0 while a search jump still holds
its row.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
## Evidence
Both Source Control rows now carry a git-branch icon, sized and aligned
like their neighbours; Models keeps globe/folder.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Settings now opens to **Authentication** by default.
* Added a dedicated advanced-only **CLI Binary** settings section.
* Improved navigation icons for Authentication and Source Control
sections.
* **Improvements**
* Settings sections now reliably scroll to the top when switching
sections.
* Search-result navigation preserves the selected result’s position.
* **Tests**
* Updated settings and mobile navigation coverage for the new default
section and labels.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The board composer's Save button rendered its label cut off on mobile
("Sav|").
## Root cause (measured, not guessed)
Reproduced in Chromium at a 412px viewport and measured the live layout:
- The primary action group needs **~275px** inside a **260px** column —
a 15px deficit.
- All five icon controls carry an explicit `min-width: 36px`
touch-target floor, so they **cannot** absorb it.
- Save's own automatic minimum size — which would normally floor a flex
item at its min-content width — is **zeroed by `overflow: hidden`**. Per
the flexbox spec, automatic minimum size applies only when `overflow` is
`visible`.
That `overflow: hidden` exists for a **vertical** reason
(FN-7680/FN-7683 height equalization). So a height fix silently made
Save the only horizontally-shrinkable control in the row: it absorbed
the entire deficit (**40px actual vs 55px needed**) and clipped its own
label.
## Fix
- Save is pinned to its content width (`flex: 0 0 auto; min-width:
max-content`) so it can never be squeezed. `overflow: hidden` stays — it
still owns the vertical clamp.
- The group may `flex-wrap: wrap` with `justify-content: flex-end`, so a
genuine deficit reflows to a second right-aligned line instead of
clipping.
**Not breakpoint-scoped** (FN-5751): the mechanism is width-driven, not
media-driven — mobile only trips it first because its 36px touch targets
are wider than the desktop chips. Where the row already fits, both rules
are inert.
This revises the older "wraps as one unit, never splitting Save from its
neighbors" intent: at widths where the row genuinely cannot fit, Save
wrapping to its own right-aligned line is strictly better than a clipped
label.
## Verification (in-browser, both breakpoints)
| | 412px (mobile) | 1400px (desktop) |
|---|---|---|
| label clipped | **no** (scrollWidth 67 === clientWidth 67) | no |
| Save width | 69px (full) | 69px |
| Save height | 36px — equal to icon siblings | unchanged |
| overflow | none (right edge 292 === container 292) | none |
| layout | wraps to a second right-aligned line | **no-op** — still one
line (group height 28px) |
| icon touch targets | all five still ≥36px | unchanged |
Gate green · lint clean · 288 dashboard composer tests pass.
## On the test
The CSS guard is a **string-match, not a layout proof** — jsdom has no
flex layout and cannot observe clipping, so the real proof is the
browser measurement above. The test exists so the invariant-bearing
declarations can't be silently dropped or re-scoped into a media query.
It was **confirmed non-vacuous**: 3 of its 4 cases fail against the
pre-fix CSS.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* Fixed the Quick Entry Save button label being clipped on narrow mobile
screens.
* Improved action layout wrapping while preserving icon touch-target
sizing.
* **Tests**
* Added regression coverage to verify the Save button remains fully
visible across narrow layouts.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>