Sloth-ninja gives firm controls a pilot-ready polish

This fork turns early firm feedback into cleaner administration, fewer dead ends, and tighter guardrails around organisation data.

workflowsecurity

The team has removed connector options that were never actually available and cleared out stale entries from the admin experience. That matters because a legal team should not have to guess whether a promised integration is real.

  • Firm-managed AI choices now behave more consistently for members.
  • Firm names, account controls, chat history and long labels are easier to use in the interface.
  • Members are blocked from continuing to write to outdated organisation fields, helping keep firm data on the current path.

These are not headline-grabbing features, but they are the work that makes a pilot feel trustworthy rather than improvised.

So what Managing partners and legal-ops leads testing a shared AI workspace should care because this is the unglamorous control and usability layer that determines whether people adopt it.

View this fork on GitHub →

Spotted something wrong? Or know the PR text has fresher detail than the writeup above?

Commits in this thread

16 commits from Sloth-ninja/JessicaOSS, oldest first. Source extracted verbatim from the harvested git log.

SHA Subject Author Date
bb479ec9 fix(sidebar): portal the account menu so it stops covering chat history sloth-ninja 2026-08-13 ↗ GitHub
commit body
The bottom profile block's dropdown was a hand-rolled `absolute
bottom-full z-50` box rendered in flow above its trigger, so it painted
over the last visible Assistant History rows (pilot feedback 13/08).

Swap it for the shared Radix wrapper already used one file away in
SidebarChatItem: portal-mounted, collision-aware, with native dismiss on
outside click and Escape. Positioned side="top" align="start"; the
existing glass styling is passed through to DropdownMenuContent, and both
sidebar states are preserved - expanded matches the trigger width (via
--radix-dropdown-menu-trigger-width, the house pattern from
account/models and TREditColumnMenu), collapsed stays w-56.

Net deletion: the isDropdownOpen state and the manual document
click-outside listener are gone; the trigger's open highlight now comes
from data-[state=open].

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ab9f7269 fix(chat): hide the model picker when the firm manages model preferences sloth-ninja 2026-08-13 ↗ GitHub
commit body
Firm members whose firm has memberModelPrefs OFF were still shown an
interactive model picker in both composers, even though the server
already clamps the chat model to the firm default. The control looked
live and did nothing (pilot feedback 13/08).

Both ChatInput and TRChatPanel now consult personalModelPrefsBlocked -
the same helper account/models/page.tsx uses - and render a quiet inline
"Provided by <firm>" label in place of the picker. Absence, not a
disabled-looking-but-clickable control, matching the established WS8
absence-not-disabled treatment.

The offeredProviders filtering is untouched for the policy-ON case.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
04b78e20 fix: remove Canva and Apollo from the connector registry (never built) sloth-ninja 2026-08-13 ↗ GitHub
commit body
Owner decision 13/08/2026: neither was part of the UK-legal shortlist
rationale, and both were marked availability:"oauth" with "VERIFIED"
comments that nothing ever verified - the gallery rendered a live
Connect button that dead-ended for pilot users. Delete both entries
outright.

Removing a previously-valid registry id exposes a real gap: the admin
connector-gallery curation PATCH validated every incoming id against
the live registry and 400'd the whole write on an unknown one. Since
the admin UI round-trips its full tick-list on every toggle (with no
control to un-tick a row the registry no longer renders), a firm that
had curated canva/apollo would be permanently blocked from saving any
future connector-gallery change. parseConnectorCuration now silently
drops unknown string ids (still 400s on genuinely malformed,
non-string entries), matching the read-side tolerance
filterRegistryByOrgCuration already has.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
05571808 fix: server-side belt drops firm members' legacy organisation writes sloth-ninja 2026-08-13 ↗ GitHub
commit body
PATCH /user/profile accepted and unconditionally wrote the legacy
free-text `organisation` field. A sibling frontend PR hides the input
for firm members; this is the server-side belt.

When the caller has a non-null organisation membership, the
organisation key is now silently dropped from the update before it
reaches the DB - not rejected with a 4xx, since the field is cosmetic
(absence-not-disabled precedent, WS8 PR B). Other fields in the same
PATCH still apply. Fails OPEN on a membership-lookup error: this is a
non-destructive cosmetic write, so availability wins (WS8 PR B
precedent, not the deletion fail-SAFE direction). Orgless callers are
unaffected. The membership lookup only runs when the payload actually
touches `organisation`, avoiding an extra query on unrelated PATCHes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
c183f0df fix(sidebar): stop the nav column painting over the profile block sloth-ninja 2026-08-13 ↗ GitHub
commit body
The account-menu swap only covered the popup case. A fresh owner
screenshot reproduces the overlap with no popup open: on a short viewport
the "Assistant History" header renders behind the bottom profile row.

Root cause is the flex chain, not the dropdown. The middle region is
flex-1 min-h-0, so it shrinks to nothing when the fixed nav sections
above it (this pilot user is a firm admin, so Firm *and* Firm admin are
both present) eat the height. Its children then keep their intrinsic
height - only the row list is overflow-y-auto, the Recent Matters block
and the History header are not - and with no clipping ancestor they paint
straight through onto the mt-auto profile block.

Wrap the nav sections and the middle region in one `flex-1 min-h-0
overflow-y-auto` column. The profile block is now a SIBLING of a scroll
container, so nothing above it can paint over it at any height - an
invariant of the structure rather than of the arithmetic. Add a min-h-56
floor on the middle region so History degrades to a shorter scrollable
list instead of a half-rendered header, and shrink-0 on the profile.

Verified by headless-Chrome renders of a raw-CSS harness translating this
class chain, before vs after, with the full firm-admin nav:
  - 820px viewport (the reproducing config): before overlaps, after clear
  - 700px viewport: before spills outside the card entirely, after clear
  - tall viewport: before/after pixel-identical, no scrollbar, no
    regression to the pinned Recent Matters + inner history scroll

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
a648cc3b fix(account): show firm members their firm name read-only sloth-ninja 2026-08-13 ↗ GitHub
commit body
The Organisation field edited user_profiles.organisation - a legacy
free-text column that predates firms and is shown nowhere else. A firm
member could type anything into it and nothing would change, while the
real firm name sat unused in the same payload as profile.firm.name
(pilot feedback 13/08).

Firm members now see profile.firm.name read-only with a quiet "Managed by
your firm." line, matching how this page already treats the Danger Zone
for firm members. Orgless self-hosters keep the editable field exactly as
before, so the legacy column stays reachable where it is still the only
source of an organisation name.

The seeding effect now skips the firm path, since only the orgless branch
has a field to populate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
eedbb795 fix: GET /admin/connector-gallery filters stale ids from enabledConnectorIds sloth-ninja 2026-08-13 ↗ GitHub
commit body
Review finding (Minor 1): the stored enabledConnectorIds were returned
unfiltered, so a firm that had curated a since-removed registry entry
(e.g. canva/apollo, removed 13/08/2026) would see it echoed back. This
inflates the admin card's tick count - its "all visible" footer
compares visible.size === registry.length, so a phantom stale id makes
that comparison false while a real entry stays effectively hidden from
the count - and a curation stored as ONLY stale ids would let an
untick compute a payload the server canonicalises to [] ("all
visible"), inverting the admin's intent.

Filters the stored list against connectorRegistryIds() before
returning, mirroring the write-side tolerance already applied to
PATCH /admin/connector-gallery.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
8832de30 test: make resolveUserOrganisation a spy so call-count claims are real sloth-ninja 2026-08-13 ↗ GitHub
commit body
Review finding (Minor 2): the "does not resolve membership when payload
has no organisation key" test used a bare closure for the
resolveUserOrganisation mock, so it recorded nothing and the test would
pass under any implementation - the claim was never actually checked.

Replaces the closure with a vi.fn() spy (call-through pattern matching
user.connectorGallery.test.ts) and asserts exact call counts: a
displayName-only PATCH resolves membership exactly once (loadProfile's
own resolve for the response's `firm` field), an organisation-touching
PATCH exactly twice (the belt's resolve + loadProfile's).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
e337f539 docs: BUILD_LOG entry for the 13/08 pilot-feedback UI fix wave sloth-ninja 2026-08-13 ↗ GitHub
Covers all three items, the harness-render evidence for the sidebar
layout fix, and the honest lint gate (count identical to main).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
5bbe0a09 docs: BUILD_LOG entry for the pilot-feedback fix train PR B sloth-ninja 2026-08-13 ↗ GitHub
commit body
Connector-registry honesty (Canva/Apollo removed, owner decision
13/08) + firm-member organisation-write belt, including the two
review-round fixes (PATCH silent-drop, GET read-side stale-id filter).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
e07984c4 Merge pull request #85 from Sloth-ninja/pilot-feedback-connectors Ezana.H 2026-08-13 ↗ GitHub
Pilot feedback 13/08: connector-registry honesty + organisation-write belt
87598c0f fix(sidebar): move the height floor onto the History section (review I1) sloth-ninja 2026-08-13 ↗ GitHub
commit body
Review caught that min-h-56 on the middle wrapper did not deliver what its
comment claimed. The floor is shared with the Recent Matters block, which
cannot shrink - with a full 5-matter list Recent Matters (~220px) eats the
entire 224px floor and the History section resolves to ZERO height: empty
list, header flush against the bottom. That is the exact configuration the
pilot firm will QA.

Drop the floor from the wrapper (it now sizes to content inside the
scrollable column) and put min-h-48 on the History section, where it
actually protects the list. The row container is itself a scroll
container, so its automatic minimum size is 0 and the section still
resolves to a bounded height.

Re-measured in a corrected harness - hard config this time: full 5-matter
Recent Matters, firm-admin nav, and the logo row OUTSIDE the scroll
column as in the real code (the first harness wrongly nested it):

  viewport   main            min-h-56 wrapper   min-h-48 History
  796px      history 0px,    history 0px        history 192px,
             escapes card    (no escape)        rows 174px
  676px      history 0px,    history 0px        history 192px,
             escapes card    (no escape)        rows 174px
  1006px     history 60px,   history 60px       history 192px,
             rows 42px       rows 42px          rows 174px

Matches the reviewer's independent measurement (192/168; the 6px delta is
header line-height modelling). Accepted trade-off, as flagged: at ~1030px
with 5 recent matters the outer column becomes scrollable where today it
is not - scroll is acceptable, an empty history list is not, and the fix
buys 174px of rows against main's 42px at that height.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
17444d9e fix(chat): truncate the firm label on the text, not the flex box (review M1) sloth-ninja 2026-08-13 ↗ GitHub
commit body
`truncate` sets overflow/text-overflow/white-space, but it was on the
flex container, where text-overflow has no line box to apply to - a long
firm name would hard-clip at max-w rather than ellipsing. Move it to an
inner span wrapping the text. The title attribute already carries the
full name and is unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fc4b4dcb docs: state the harness configuration and the floor move (review I2) sloth-ninja 2026-08-13 ↗ GitHub
commit body
The entry's 820px claim came from a harness that modelled the easy case -
empty Recent Matters, and the logo row nested inside the scroll column,
which the real code does not do. The conclusions held under a corrected
higher-fidelity harness, but the entry must say which configuration was
rendered rather than implying the hard one.

Records the round-1 vs round-2 harness configurations, the measurement
method (elementFromPoint sampling, since getBoundingClientRect ignores
clipping and false-positives on scrolled-out content), the I1 floor move
to the History section, and the accepted 1030px scroll trade-off.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
148af688 Merge pull request #86 from Sloth-ninja/pilot-feedback-ui Ezana.H 2026-08-13 ↗ GitHub
Pilot feedback 13/08: sidebar overlap, firm model-picker gating, Organisation read-only
b728db31 docs: BUILD_LOG entry for the close-out of the two 12-14/08 trains sloth-ninja 2026-08-14 ↗ GitHub
commit body
Records the docs-only close-out: what changed in CLAUDE.md, HANDOVER §0
and DURABLE_LESSONS, and how each fact was cross-checked against git log
and the existing entries. Notes one correction to the brief this task was
given - PR #84 merged on 12/08, not 13/08, per merge commit d3bf3eb and
the #84 entry itself.

Decision recorded: the proposed stop-check.js hardening is deliberately
excluded and documented as awaiting owner sign-off.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Capture this thread into my fork

Download a single Markdown prompt that tells Claude how to port every commit above into your working tree — adapting paths and structure to match your repo. Run it via claude -p < capture-thread-1006.md from inside the repo you want the changes in.

⬇ Download capture-thread-1006.md