mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-04 02:32:07 +01:00
04afdcdee5e4e8e9519cff7488251d5aa5c7eaa2
10
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
bc1a21564f |
Remove the company brand color and per-company attachment limit (#12291)
## Thinking Path
> - Paperclip is the open source app people use to manage AI agents for
work
> - A company is the top-level container, and the company General page
holds its settings
> - Two of those settings did almost nothing: the brand color only
tinted the generated company icon, and the attachment size limit sat
under the deployment-level `PAPERCLIP_ATTACHMENT_MAX_BYTES` cap that
already bounded every upload
> - A setting that changes one icon hue, and a setting that can only
lower a limit the operator already set, are not worth the page space or
the code that carries them
> - This pull request deletes both settings from the UI, the validators,
the API contract, the server, and the database
> - With the deployment cap as the only limit left, the message a person
sees when an upload is rejected has to name that limit in terms they can
act on, so the raw byte count becomes a human-readable size
> - The benefit is a shorter company General page for every deployment,
one attachment limit instead of two, and less code between an upload and
its ceiling
## Linked Issues or Issue Description
No existing issue. The description below follows
`.github/ISSUE_TEMPLATE/enhancement.yml`.
**What existing behavior does this improve?**
The company General page (`/company/settings`), the `PATCH
/api/companies/{companyId}` and `PATCH
/api/companies/{companyId}/branding` request contracts, and the
attachment upload limit on task, case, and company-import uploads.
**Subsystem affected**
Cross-cutting: `ui/`, `server/`, `packages/shared`, `packages/db`.
**Current behavior**
The company General page shows an "Appearance" section with three
controls: Logo, Brand color, and Attachment size limit. The brand color
is a hex value that feeds one thing — the hue of the generated company
pattern icon. Companies that never set one already get a hue derived
from the company name. The attachment size limit is a per-company byte
count stored on `companies.attachment_max_bytes`. Every upload path
clamps it against the deployment-level `PAPERCLIP_ATTACHMENT_MAX_BYTES`
cap, so the per-company value can only lower a limit the operator
already chose.
**Proposed behavior**
The Appearance section keeps the Logo control only. The company pattern
icon always derives its hue from the company name. Every attachment path
reads the deployment cap directly, so `PAPERCLIP_ATTACHMENT_MAX_BYTES`
is the single limit. An upload rejected by that limit says so in human
units — "File is larger than the 10 MB limit" rather than a raw byte
count. The `companies.brand_color` and `companies.attachment_max_bytes`
columns are dropped, and both fields leave the company API contract.
**Reason and benefit**
Both settings ask an operator to make a decision that changes almost
nothing. The brand color moves one icon hue on a page that also lets you
upload a real logo, which overrides the icon entirely. The attachment
limit reads as a real control but cannot raise anything, so it is a
second place to look when an upload is rejected. Removing both shortens
the page every deployment sees, removes a company-scoped read from the
task attachment upload path, and leaves one attachment limit to reason
about instead of two.
**Breaking changes**
The company API responses no longer include `brandColor` or
`attachmentMaxBytes`, and `GET /api/invites/{token}` no longer includes
`companyBrandColor`. `PATCH /api/companies/{companyId}/branding` is
strict, so a request that sends `brandColor` now returns 400; the
non-strict `PATCH /api/companies/{companyId}` schema strips it. Company
packages exported by older versions still import: the portability
company manifest schema is non-strict, so the retired keys are stripped
and ignored rather than rejected. Companies that stored a brand color
lose it — their icon reverts to the name-derived hue that every company
without a color already used.
## What Changed
- Removed the "Brand color" and "Attachment size limit" fields from the
company General page, along with their state, dirty checks, save
payload, and Save-button gating.
- Removed `brandColor` and `attachmentMaxBytes` from
`createCompanySchema`, `updateCompanySchema`, and
`updateCompanyBrandingSchema`, and deleted the now-orphaned
`DEFAULT_COMPANY_ATTACHMENT_MAX_BYTES` and
`MAX_COMPANY_ATTACHMENT_MAX_BYTES` constants.
- Removed both fields from the `Company` type, the portability manifest
type and schema, and the `companiesApi.update` payload allowlist.
- Dropped `brandColor` from `CompanyPatternIcon` and its callers, so the
icon hue always comes from the company name. Deleted the now-unused
`hexToHue` helper and the now-unused `pickTextColorForSolidBg` export.
- Stopped emitting `brandColor` from the company service selection and
from the invite-summary and invite-branding payloads in
`server/src/routes/access.ts`.
- Replaced `normalizeIssueAttachmentMaxBytes` with the deployment cap:
task attachments, case attachments, and company import now use
`MAX_ATTACHMENT_BYTES` directly. The helper is deleted.
- Added `formatAttachmentSize()` next to `MAX_ATTACHMENT_BYTES` and
routed every over-limit message through it, so a rejected upload names
the limit in human units instead of raw bytes: `Image exceeds 10485760
bytes` becomes `Image is larger than the 10 MB limit`. Enforcement is
unchanged — the same single cap, the same multer limits, the same status
codes and response shapes.
- Added migration
`0229_drop_company_brand_color_and_attachment_max_bytes.sql` and removed
both columns from the Drizzle `companies` schema.
- Kept legacy imports working: the portability company manifest schema
is non-strict, so older packages carrying the retired keys still import
with the keys ignored.
- Updated the skill API reference and the implementation spec, and
pruned the token-extraction allowlist entries that the removed code made
stale.
## Verification
Commands run from the repository root:
- `pnpm --filter @paperclipai/shared typecheck` — pass
- `pnpm --filter @paperclipai/db typecheck` — pass (includes
`check:migrations`, which validates the new migration number and journal
entry)
- `pnpm --filter @paperclipai/ui typecheck` — pass
- server typecheck via `node_modules/.bin/tsc --noEmit` in `server/` —
pass. `pnpm --filter @paperclipai/server typecheck` could not run
locally because it builds the Rust runner first and `cargo` is not
installed on this machine; the TypeScript step it wraps is the command
above.
- `npx vitest run packages/shared/src/validators/company.test.ts` — 6
passed
- `npx vitest run server/src/__tests__/company-portability.test.ts` — 90
passed
- `npx vitest run server/src/__tests__/attachment-types.test.ts
server/src/__tests__/assets.test.ts
server/src/__tests__/issue-attachment-routes.test.ts
server/src/__tests__/company-portability.test.ts
server/src/__tests__/cases-routes.test.ts` — 165 passed (the
human-readable limit messages)
- `npx vitest run server/src/__tests__/company-branding-route.test.ts
server/src/__tests__/issue-attachment-routes.test.ts
server/src/__tests__/invite-summary-route.test.ts
server/src/__tests__/openclaw-invite-prompt-route.test.ts
server/src/__tests__/companies-route-cross-company-authz.test.ts` — all
passed
- `npx vitest run cli/src/__tests__/company.test.ts
cli/src/__tests__/company-delete.test.ts` — 27 passed
- `npx vitest run` in `ui/` — 4425 passed, 1 pre-existing failure
unrelated to this change (`OnboardingWizard.test.tsx` "renders instead
of throwing when the browser denies storage access", which also fails on
`master`)
- `npx vitest run` in `server/` — see the note below
- `node scripts/check-token-gates.mjs` — no new violations; the only
reported violations are the pre-existing `PillGuy.tsx` ones present on
`master`
New tests added:
- `packages/shared/src/validators/company.test.ts` — the create and
update schemas strip the retired keys, the strict branding schema
rejects `brandColor`, and the portability manifest schema accepts a
legacy entry carrying both keys and drops them.
- `server/src/__tests__/company-branding-route.test.ts` — `PATCH
/api/companies/{companyId}/branding` returns 400 for `brandColor` and
does not call the company service.
- `server/src/__tests__/company-portability.test.ts` — a legacy package
that declares `brandColor` and `attachmentMaxBytes` imports
successfully, and neither key reaches `companies.create`.
- `server/src/__tests__/issue-attachment-routes.test.ts` — the effective
task attachment limit is the deployment cap, and the route no longer
loads the company to size an upload.
- `server/src/__tests__/attachment-types.test.ts` —
`formatAttachmentSize()` renders the default cap as `10 MB`, keeps one
decimal place for fractional sizes and drops a trailing `.0`, falls back
to KB and bytes for small caps, steps up to GB, and never emits `NaN`
for a degenerate input.
- `server/src/__tests__/assets.test.ts` — the asset-image and
company-logo routes both return the human-readable limit message on an
over-cap upload.
## Merge with master
`master` moved while this was open, and the merge needed two
resolutions:
- **`ui/src/pages/CompanySettings.tsx`.** #12243 reworded the
user-facing
copy from "company" to "organization", and that rewording landed inside
the "Brand color" and "Attachment size limit" hints — the two fields
this change deletes. Both fields are removed, so the conflicted block is
dropped whole. The Logo field and every other copy change from #12243
are
kept.
- **Migration renumbered 0228 -> 0229.** #12307 landed
`0228_nasty_grim_reaper`, so this migration is now
`0229_drop_company_brand_color_and_attachment_max_bytes`. Its snapshot
is
rebuilt from master's `0228_snapshot.json` with only the two `companies`
columns removed, and `meta/_journal.json` is master's journal plus a
single `idx: 229` entry. `pnpm --filter @paperclipai/db
check:migrations`
passes.
The snapshot was rebuilt by hand rather than taken from `drizzle-kit
generate`, because master's `0228_snapshot.json` has drifted from
master's
own schema: `issue_question_response_deliveries.error_count` is created
by
master's 0228 SQL but missing from its snapshot, and the snapshot still
carries `decision_archive_notification_outbox.error_count`. Regenerating
folds both into this migration, and the resulting `ADD COLUMN
error_count`
would fail on a fresh database where master's 0228 already created that
column. Rebuilding from master's snapshot leaves that drift exactly
where
it is and keeps this migration to the two column drops. The drift is
pre-existing on master and is not addressed here.
## Risks
- **The migration is a destructive column drop.**
`0229_drop_company_brand_color_and_attachment_max_bytes.sql` removes
`companies.brand_color` and `companies.attachment_max_bytes`. It is safe
because both features are removed in the same change and nothing reads
either column after it. The statements use `DROP COLUMN IF EXISTS`,
matching the convention of the recent drop migrations in this
repository. The drop is not reversible: a downgrade after this migration
loses any stored values.
- **Stored brand colors are lost.** A company that had set a color now
renders the name-derived icon hue that every company without a color
already used. No other surface changes, and an uploaded logo still
overrides the icon.
- **API response shape narrows.** `brandColor` and `attachmentMaxBytes`
leave the company payloads, and `companyBrandColor` leaves the invite
summary payload. A client reading those fields now sees `undefined`. The
bundled UI and CLI are updated in this change.
- **Legacy imports are covered.** Packages exported by older versions
still carry both keys. The manifest schema is non-strict, so the keys
are stripped rather than rejected, and a test locks that in.
- **The over-limit message strings changed.** Anything matching on the
old `... exceeds N bytes` text — a test, a script, or a client that
string-matches `body.error` — needs updating. The status codes (422) and
response shapes are unchanged, so structured clients are unaffected.
- **Attachment limits can only widen.** A deployment that had lowered a
company below the deployment cap now allows uploads up to the cap for
that company. Lower `PAPERCLIP_ATTACHMENT_MAX_BYTES` if a smaller
ceiling is needed.
- **Storybook visual baselines shift** for the `CompanyPatternIcon`
matrix story, because those fixtures had brand colors. That workflow
runs only on a PR labeled `storybook-visual`, so it does not gate this
PR; regenerate the baselines if the label is added.
## Model Used
Claude (Anthropic), Claude Opus, agentic tool use via Claude Code.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge
|
||
|
|
327f59cac2 |
refactor(ui): key the company list by account (#11488)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - Which companies a person belongs to is an authorization fact the server owns, and the UI caches the answer for speed > - It cached that answer under one `["companies"]` key with no account attached, while `main.tsx` sets `staleTime: 30_000` for every query > - So for thirty seconds after an account change, one person's list answered questions asked about another, arriving with no loading state and no error > - Three separate consumers each grew their own defense against this, and each was a place to forget one > - This pull request keys the entry by account, so a list belonging to someone else is not distrusted but unreachable > - The benefit is that the protection stops depending on every future consumer remembering to defend itself ## Linked Issues or Issue Description No public issue exists. Refs #11380, #11382, #11417, #11430. The problem follows. **What happened?** The company list lived in a single cache entry, `["companies"]`, carrying no record of which account it was fetched for. Combined with the app-wide 30s `staleTime`, any read within that window after an account change returned the previous account's list — from cache, with no request, no loading state and no error. Every consumer that treats the list as an authorization fact had to know this and defend itself: - `InviteLanding` reads it to decide whether you already belong to the inviting company (#11417). - `OnboardingWizard` reads it to decide whether a saved draft belongs to you (#11382, merged). - `CompanyProvider` reads it to pick and persist your active company (#11430). All three defenses are correct. The problem is structural: the fourth consumer has to invent a fourth one. **Expected behavior** A cached company list can only answer questions about the account it was fetched for. **Steps to reproduce** 1. On a self-hosted instance in `authenticated` mode, sign in as account A, which belongs to company X. 2. Within thirty seconds, have account B become the session in that tab — a second tab signing in, or A's session lapsing server-side. 3. Any consumer reading the company list receives A's list, and nothing in the query result indicates it is not B's. **Paperclip version or commit** `master` at `ac91b7f3b`, which includes #11430. ## What Changed - `ui/src/lib/queryKeys.ts` — `companies.list(userId)` replaces `companies.all` as the list's entry. `companies.all` remains the prefix, so it still matches for invalidation. - `ui/src/api/companies-query.ts` — `companyListQueryOptions(userId)` builds the keyed options; `useCompanyListQuery()` is the only observer entry point and holds until the session settles, because the key cannot be built before then; `fetchCompanyListForCurrentAccount(queryClient)` covers imperative paths; `useAccountIdentity()` exposes the session identity the key is built from. - `ui/src/api/companies-query.ts` — the `/companies` detach moved into the query function. - `ui/src/context/CompanyContext.tsx` — drops the session-watching refetch machinery the key now makes unnecessary (`removeQueries`, the explicit replacement fetch, the awaiting gate). It still clears the live selection on an account change, because that is component state and does not change key with the query. - `ui/src/pages/InviteLanding.tsx`, `ui/src/components/OnboardingWizard.tsx` — read through the account-aware API. - Tests — the account-keyed guarantee, prefix invalidation still reaching the list, the detach inside the query function, and updates where suites seeded the old shared key. ### The existing defenses are deliberately left in place The per-consumer gates in #11382, #11417 and #11430 are now belt and braces. They are also what will catch this refactor if it is wrong somewhere, so removing them in the same change that moves the foundation would be the wrong order. Simplifying them is a follow-up, once this has proven itself. ### Why `retry: 1` appears in CompanyProvider An earlier measurement on #11430 found a retry on the replacement fetch changed no outcome, because `removeQueries` made the observer rebind and issue a second request for free. Keying by account removes that mechanism and the free attempt with it. The retry now carries the property the incidental refetch used to — a single blip during an account change should not leave the customer with no companies until they find "Try again". #11430's test for that property is unchanged and still passes, which is how the gap was caught. ### A regression this went through, kept for the record Gating the query on the session settling meant that while the account was unknown the query was *disabled*, and a disabled query reports `isLoading: false` with no data — which the provider defaults to an empty list and reads as "asked, and owns nothing". That is the destructive branch #11477 had just fixed, reached through a different door: it would have cleared the customer's stored company on every cold boot. #11477's test caught it during the rebase. `useCompanyListQuery` now reports the wait for the account as part of the wait for the list. ### What this does not do It does not scope the rest of the per-account cache. `["companies", id]`, stats, and every other account-scoped entry still survive an account change; that is the cache-lifetime work in #11380. ## Verification - `pnpm vitest run` in `ui`: **4018 passed, 1 failed**. - `pnpm tsc -b` in `ui`: clean. - `companies-query.test.ts`: 6 passed. `CompanyContext.test.tsx`: 17 passed. `OnboardingWizard.test.tsx`: 13 passed. `InviteLanding.test.tsx`: 13 passed. The failure is the pre-existing timezone-dependent `IssueProperties.test.tsx`, fixed by #11478. Two behaviours are asserted rather than assumed, because the refactor is only safe if they hold: that invalidating the `companies` prefix still marks the account-keyed list stale (19 call sites depend on it), and that the query function detaches the in-flight `/companies` request before fetching. **Not done:** no manual two-account run in a browser. The path needs two accounts on an `authenticated` instance, which a local dev instance cannot exercise. ## Risks Moderate, and worth reading before approving. **It touches `InviteLanding.tsx`, which #11417 also modifies**, so one of the two will need a rebase — the conflict is mechanical (both change how the same query is read). This was #11481, which GitHub closed automatically when its base branch (#11430's) was deleted on merge; reopening a pull request whose base branch is gone is not permitted, so it continues here against `master` with the same head and the same review already recorded on #11481. **The list now waits for the session query.** The key cannot be built before the account is known. In the app the session is already fetched at boot by many components, so this is a dependency rather than an extra request, but it does serialize: on a cold boot the list waits for the session to land. Every test that renders a company-list consumer now needs a session in the cache, which is why several suites gained a seed. **A missing mock surfaces as a passing gate rather than an error.** The detach inside the query function meant suites whose `companiesApi` mock lacked `detachInflightList` had their query function throw, which read as "decided" in the onboarding gate and mounted the wizard early. Fixed in the affected suites; worth knowing as a failure mode. ## Model Used Claude Opus 5 (`claude-opus-5`), through Claude Code. Extended thinking enabled. Tool use enabled: file read and edit, shell for typecheck and test runs. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [ ] All Paperclip CI gates are green - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
ac91b7f3b2 |
fix(ui): scope the company selection to the signed-in account (#11430)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - Every company-scoped screen reads the active company from `CompanyProvider`, which picks one from the `["companies"]` list and remembers it in localStorage > - That cache entry is shared app-wide and carries no account identity, so it survives a change of account in the tab > - The provider therefore auto-selects from whatever list is cached, which can belong to the account that just went away > - This pull request makes the provider watch the account and refuse to derive a selection from a list fetched for a different one > - The benefit is that the app stops pointing at a company the signed-in account may not be able to see ## Linked Issues or Issue Description No public issue exists. Refs #11380, #11382, #11417. The problem follows. **What happened?** `CompanyProvider` auto-selects a company from the shared `["companies"]` cache entry and writes that id to `localStorage`. Nothing ties that entry to an account. When the account changes in the tab, the previous account's list is still served, so the provider can select — and persist — a company belonging to the account that just went away. Company-scoped screens then render against a company the current account may not be able to see. Signing in through `Auth.tsx` invalidates the entry, so the in-app sign-in path is covered. Two paths are not: a session that lapses server-side, and a second account signing in on another tab. The sign-out sweep in #11380 does not cover them either, because neither presses the sign-out button. **Expected behavior** The company selection is derived only from a company list fetched for the account that is signed in now. **Steps to reproduce** 1. On a self-hosted instance in `authenticated` mode, sign in as account A, which belongs to company X. 2. In a second tab, sign in as account B, which does not belong to company X. 3. Return to the first tab. The session query refetches and reports account B, while the company list is still account A's. 4. The provider keeps company X selected and leaves its id in `localStorage`. **Paperclip version or commit** `master` at `6542ad1f4`. ## What Changed - `ui/src/context/CompanyContext.tsx` — the provider observes `queryKeys.auth.session`. On a change of session user it clears the live selection, removes the shared company list, and holds auto-select until a list fetched for the new account lands. The stored id is left alone on purpose: `resolveBootstrapCompanySelection` re-validates it, so an account signing back in keeps its company while an unrelated account cannot inherit it. - `ui/src/context/CompanyContext.tsx` — an errored list is treated as undecided rather than as "no companies". With `retry: false` a single network blip sticks, and the empty-list branch read it as proof the account owns nothing and cleared the stored selection. - `ui/src/api/client.ts` — new `detachInflightGet(path)`. GET coalescing keys on the request path alone, so a `/companies` request issued under the previous session could be joined by the replacement fetch and answer it with the previous account's companies. Detaching leaves that request to settle for its own callers and makes the next call issue a fresh one. - `ui/src/api/companies.ts` — `companiesApi.detachInflightList()` wraps that for the list path. - `ui/src/context/CompanyContext.tsx` — `companyListUnavailable` separates "no usable list because a request failed" from "this account owns nothing", and `retryCompanies` gives a recovery action that fetches. Both are derived from the query rather than tracked beside it; a second copy of "did the last attempt succeed" drifted out of step during review, reporting a failure over a later empty list that was simply the truth. - `ui/src/components/SidebarCompanyMenu.tsx` — renders "Couldn't load companies" and a Try again item in place of "No companies", which is a claim about the account that a failed request cannot support. This is the menu `Sidebar` mounts, so it is the only place a customer can act on the failure. - `ui/src/components/CompanySwitcher.tsx` — the same treatment. The application does not render this component (its only mount is a Storybook story), so it is kept in step rather than relied on. - `ui/src/context/CompanyContext.test.tsx`, `ui/src/components/SidebarCompanyMenu.test.tsx`, `ui/src/api/client.test.ts` — coverage for the account switch, a same-account re-observation not churning, the detached GET, the failed replacement and its recovery, a single blip self-healing, unavailability not outliving the failure, and the sidebar rendering the recovery action for a failure but plain "No companies" for an account that owns nothing. ### No `retry` override on the replacement fetch The obvious fix for a failed replacement is a retry, and it is not load-bearing here. A transient failure already gets a second attempt: the observer rebinds to a fresh query on the render those state updates schedule, and issues its own request — measured as two attempts with or without the option. Retries would only add failed round trips before a real outage is reported, and the outage is what needs a way out, which is what `companyListUnavailable` and `retryCompanies` provide. ### Why `removeQueries` here, and why that does not generalise Removal notifies no observer. What rebinds them at this call site is the render the surrounding state updates schedule; every observer re-binds to a fresh query on the next render. A caller without that guarantee would leave mounted observers serving the previous account's value, so this is not a pattern to lift elsewhere — the sign-out sweep in #11380 must use `resetQueries` instead, and its measurements are at [#11380](https://github.com/paperclipai/paperclip/pull/11380#issuecomment-5300984911). The inverse caveat holds for a local reset under an observer that stays mounted, which is why #11417 and #11382 avoid `resetQueries`. ## Verification - `pnpm vitest run` in `ui`: **4014 passed, 1 failed**. - `pnpm tsc -b` in `ui`: clean. - `CompanyContext.test.tsx`: 16 passed. `SidebarCompanyMenu.test.tsx`: 15 passed. `client.test.ts`: 9 passed. The failure is pre-existing and unrelated: `IssueProperties.test.tsx` expects `4:08 PM` and gets `9:08 AM`, a timezone-dependent assertion. It reproduces on a tree without this change, and #11478 fixes it. Each new test was confirmed to fail against the implementation it covers, by reverting that change and re-running rather than by assuming. The account-switch test fails without the fix (the selection stays on the previous account's company and no refetch is issued); the flag-clearing test fails without its clause (an empty list keeps reading as "couldn't load"). **Not done:** no manual two-account run in a browser. The path needs two accounts on an `authenticated` instance, which a local dev instance cannot exercise. ## Risks Low. The failure direction is a company selection withheld for one extra round trip, which resolves when the list arrives. The direction it removes is one account's company selected and persisted for another. It adds one company-list request per account change, on a query key the app already uses. It adds no request at boot: the session query it observes is already fetched app-wide. **This does not close the class.** Company-scoped entries other than the list — `["companies", id]`, stats, and the rest of the per-account cache — still survive an account change. That is the cache-lifetime work in #11380, not this provider's. ## Model Used Claude Opus 5 (`claude-opus-5`), through Claude Code. Extended thinking enabled. Tool use enabled: file read and edit, shell for typecheck and test runs, and a scratch vitest harness to measure `removeQueries` and `resetQueries` notification behaviour against the installed `@tanstack/query-core` 5.101.4. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
e384d0a2bd |
fix(ui): keep the stored company when the company request fails (#11477)
An error is not an answer, and this branch is destructive.
`companiesListQueryOptions` sets `retry: false`, so a request that fails before
ever succeeding leaves `data` undefined - which `CompanyProvider` defaults to
`{ companies: [], unauthorized: false }`. That is indistinguishable from "this
account was asked, and owns nothing", so `shouldClearStoredCompanySelection`
returned true and the effect removed the customer's stored company. With
`refetchOnWindowFocus: true`, a blip on focus during a cold load was enough,
and the next visit drops them onto whichever company sorts first.
The predicate now takes `errored` and refuses to clear on it. Required rather
than optional, so the compiler made both existing call sites state their
answer instead of inheriting a default.
Not clearing costs nothing: a stored id that no longer resolves is ignored by
`resolveBootstrapCompanySelection`, which checks it against the current list
before using it. Clearing wrongly costs the customer's selection, which cannot
be recovered.
Scoped deliberately. This file had been described as carrying the same defect
as the onboarding draft gate and the sign-out sweep, and that was overstated.
Those two *trusted* a stale list to answer "does this account own this
company?". This one validates membership against the current list and only
picks a default, so a stale list here self-corrects rather than leaking. The
failed-request branch is the part that is genuinely wrong, and it is the only
part changed. The transient re-decision during a background refetch is real,
self-correcting, and left alone.
Tested at both levels, because the predicate alone would not have caught it:
the provider is what defaults a failed request to an empty list, so the wiring
is where the decision goes wrong. Removing the guard fails both.
ui typecheck clean; full ui suite 4016 pass, with only the timezone-dependent
IssueProperties failure already present on master.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
61a5b7c6f9 |
fix(ui): stop the selection ping-pong on archived company URLs (#11300)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - The UI keeps a selected company in `CompanyProvider` with two writers: a bootstrap effect that repairs invalid selections, and a Layout route-sync effect that selects the company the URL prefix names > - The route-sync matches the URL against the full company list (archived included), while the bootstrap resolver only accepted companies from the sidebar-filtered non-archived list > - On any archived company's URL the two effects overwrite each other's selection in a synchronous loop until React throws error #185 ("Maximum update depth exceeded") and unmounts the root to a blank page — armed by remembered last-visited paths, back/forward navigation, or bookmarks, on first load and client navigation alike > - This pull request makes an already-selected company only need to exist, keeping the sidebar filter for fresh-boot resolution where no explicit selection exists > - The benefit is that archived company URLs render instead of blanking the entire app ## Linked Issues or Issue Description No existing issue. Description follows the bug template: **What happened?** Opening (or back-navigating to) a URL whose company prefix belongs to an archived company blanked the whole app with `Minified React error #185`. Console in dev mode: "Maximum update depth exceeded. This can happen when a component calls setState inside useEffect…". A workspace whose first/seeded company was archived hit this on every load of its remembered URL. **Expected behavior** An archived company's URL renders its pages (the company still exists and its API routes serve data). The sidebar simply does not feature archived companies, and fresh boots still land on a non-archived company. **Steps to reproduce** 1. Create two companies; archive one (`PATCH /api/companies/:id` with `status: "archived"`). 2. Navigate to `/{archivedPrefix}/dashboard` — direct load or client-side back-navigation. 3. Before this fix: React #185 and an unmounted blank page (reproduced deterministically by the new e2e test). ## What Changed - `ui/src/context/CompanyContext.tsx`: `resolveBootstrapCompanySelection` keeps an explicitly selected company that exists in the full company list; stored-id and default resolution still prefer sidebar (non-archived) companies. - `ui/src/context/CompanyContext.test.tsx`: resolver keeps an archived-but-existing selection; a truly deleted selection is still replaced. - `tests/e2e/archived-company-url.spec.ts`: end-to-end regression driving both field shapes (direct load and back-navigation onto an archived company URL); it failed with the exact #185 console errors before the fix and passes after. ## Verification - `pnpm vitest run src/context …` in `ui/` — 122 tests pass (includes the new resolver cases). - `npx playwright test --config tests/e2e/playwright.config.ts archived-company-url` — fails before the fix (captured "Maximum update depth exceeded" console errors), passes after. - `pnpm typecheck` in `ui/` — clean. ## Risks Low risk. The only behavioral change is that a selection naming an archived-but-existing company survives the bootstrap repair — previously that state was unreachable without crashing. Boots with no valid selection behave exactly as before (non-archived preferred), covered by the existing and new resolver tests. ## Model Used - Claude (Anthropic), Claude Fable 5 (`claude-fable-5`), via Claude Code CLI with extended thinking and tool use (code search, edit, test execution, Playwright-driven crash reproduction). ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [ ] All Paperclip CI gates are green - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge |
||
|
|
ded813ad6f |
feat(interactions): add governed agent addressees (#10252)
## Thinking Path > - Paperclip is the control plane that lets humans govern companies of AI agents. > - Issue-thread interactions are the structured handoff point for confirmations, questions, suggested tasks, and other governed decisions. > - Those interactions previously assumed that only board users could resolve them, preventing one agent from explicitly addressing another agent for a response. > - Agent resolution needs company-level governance, auditable resolver identity, safe terminal-state handling, and attention routing so authorization is enforced server-side rather than inferred from UI behavior. > - This pull request adds governed agent resolution, withdrawal and terminal expiry semantics, explicit agent addressees, lifecycle reconciliation, and attention-feed filtering. > - The benefit is that agents can participate in structured decisions without weakening board control, company isolation, wake behavior, or audit invariants. ## Linked Issues or Issue Description ### Subsystem affected Issue-thread interactions across database, shared contracts, server authorization/services, adapter callbacks, agent skill guidance, API docs, and UI governance surfaces. ### Problem or motivation Structured interactions were board-only, had no explicit agent addressee, and lacked durable withdrawal/terminal-expiry semantics. That made peer-agent decisions impossible to authorize and audit safely. ### Proposed solution Persist requested/effective resolver policy and addressee identity, enforce company governance and eligible agent resolution, reconcile addressee lifecycle changes, expose withdrawal and terminal expiry, and route attention to the intended active agent with board fallback. ### Alternatives considered Implicitly authorizing the issue assignee or mentioned agents was rejected as ambiguous and difficult to audit. Using comments alone was rejected because it loses structured outcomes and continuation behavior. ### Roadmap alignment Supports the ROADMAP direction for lightweight leadership-agent communication that still resolves into governed decisions and work objects. ### Additional context Public GitHub issue/PR search found no duplicate implementation; open PR search for interaction resolver governance and agent addressees only returned this PR. ## What Changed - Add company-scoped interaction resolver governance contracts and persistence. - Add requested/effective resolver policy, resolver identity, withdrawal, and terminal-expiry behavior. - Add explicit `addresseeAgentId` validation, authorization, persistence, lifecycle reconciliation, API documentation, and skill guidance. - Route pending addressed interactions to the intended invokable agent and fall back to board attention when that agent becomes ineligible or is deleted. - Preserve sandbox callback identity fields required by governed resolution paths. - Add migrations `0193` and `0194` plus route, service, attention, adapter, CLI, and UI coverage. - Add governance state and company settings UI, including responsive mobile behavior and distinct withdrawn/expired audit presentation. ## Verification - `pnpm check:token-gates` — passed. - `pnpm -r typecheck` — passed, including migration numbering and safety checks. - `pnpm test:run` — feature/server and UI workspace suites passed; one unrelated CLI AWS doctor test observed injected static AWS credentials and warned instead of passing. - `env -u AWS_ACCESS_KEY_ID -u AWS_SECRET_ACCESS_KEY pnpm exec vitest run cli/src/__tests__/secrets.test.ts --project paperclipai` — 8 tests passed, confirming the failure was environment-sensitive. - `pnpm build` — passed. - Latest rebased head `e24cece6be9f1877bdbac7691bcb44fd583c0161` completed all GitHub CI jobs successfully. ## Risks - Migrations add interaction and company-governance fields; numbering is conflict-free on current `master`, additive statements are idempotent, and migration safety checks pass. - Agent authorization behavior expands beyond board-only resolution, but defaults remain board-only and coverage exercises company boundaries, resolver eligibility, lifecycle invalidation, wake behavior, withdrawal, expiry, and attention fallback. - Attention routing depends on current agent invokability; reconciliation and read-time filtering prevent stale addressees from retaining visibility or resolution authority. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used - OpenAI Codex using `gpt-5.6-sol` with reasoning, terminal tool use, code execution, Git/GitHub integration, and Paperclip control-plane tools. Context-window metadata was not reported by the runtime. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
ad961227f5 |
feat(secrets): add user-specific runtime secrets (#8825)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - Agent runs often need provider credentials, API tokens, and other environment-bound secrets. > - Company-level secrets work for shared credentials, but they do not model values that should differ by human operator. > - Without a user-scoped model, a run can dispatch without knowing whether the responsible human has supplied the needed value. > - Paperclip also needs run attribution to make those user-scoped runtime checks deterministic and auditable. > - This pull request adds user-specific secret definitions, per-user values, environment bindings, responsible-user attribution, and runtime resolution gates. > - The benefit is that teams can define the secret once, let each user provide their own value, and block runs before dispatch when required user secrets or active definitions are unavailable. ## Linked Issues or Issue Description Refs #224 Refs #6057 This PR implements user-specific secret support as a core secret-management capability rather than a one-off adapter setting. It is related to existing public work on company secrets UI and runtime secret refs, but is distinct because the value is owned by the responsible user and resolved at run dispatch time. Related PR search before opening found existing secrets work such as #1550, #8256, #8614, #8634, and #8647; none of those add the full user-secret definition/value/runtime gate covered here. ## What Changed - Added user-secret definitions and per-user "My secrets" values, keeping stored values out of access metadata. - Added `user_secret_ref` environment bindings and UI affordances to pick them alongside existing secret refs. - Added responsible-user runtime resolution so user-secret refs resolve against the human responsible for the run. - Added pre-dispatch missing-secret gates so runs fail before adapter dispatch when required user values are absent or definitions are inactive. - Added low-trust allowlist hardening for user-secret runtime access. - Added issue, routine, run, and agent API key responsible-user attribution and fail-closed dispatch behavior when attribution cannot be resolved. - Added denial-copy mapping so responsible-user authorization failures surface as actionable run outcomes instead of opaque setup failures. - Added OpenAPI documentation for the user-secret routes. - Rebases cleanly on current `master`; migrations were renumbered incrementally as `0128_user_specific_secrets`, `0129_agent_api_key_responsible_user`, and `0130_run_responsible_user_invariant` after upstream `0126`/`0127` migrations. - Removed previously committed local design screenshots so the PR contains code/docs/tests only. ## Verification - PASS: PR head `2527febd106bcf3ca264ca0da7fca491084192d6` is based on `paperclipai/paperclip:master`. - PASS: `git diff --check` - PASS: `git diff --name-only public/master...HEAD | rg '^(pnpm-lock\\.yaml|\\.github/workflows/|screenshots/)' || true` produced no files. - PASS: migration journal audit confirmed unique indexes through `130` with tail entries `0126_issue_comment_derived_attribution`, `0127_environment_custom_images_instance_scoped`, `0128_user_specific_secrets`, `0129_agent_api_key_responsible_user`, and `0130_run_responsible_user_invariant`. - PASS: `pnpm --filter @paperclipai/ui typecheck` - PASS: `pnpm --filter @paperclipai/server typecheck` - PASS: `pnpm --filter @paperclipai/server exec vitest run src/__tests__/heartbeat-responsible-user-invariant.test.ts` - PASS: `pnpm --filter @paperclipai/server exec vitest run src/__tests__/heartbeat-active-run-output-watchdog.test.ts src/__tests__/heartbeat-stale-queue-invalidation.test.ts src/__tests__/heartbeat-workspace-finalize-branch.test.ts src/__tests__/issue-monitor-scheduler.test.ts` - PASS: `pnpm --filter @paperclipai/server exec vitest run src/__tests__/heartbeat-comment-wake-batching.test.ts src/__tests__/heartbeat-retry-scheduling.test.ts src/__tests__/heartbeat-accepted-plan-workspace-refresh.test.ts src/__tests__/heartbeat-plugin-environment.test.ts` - PASS: `pnpm --filter @paperclipai/server exec vitest run src/__tests__/low-trust-red-team-routes.test.ts` - PASS: `pnpm --filter @paperclipai/server exec vitest run src/__tests__/secrets-service.test.ts` (55 tests) - PASS: `pnpm vitest run server/src/__tests__/secrets-routes.test.ts server/src/__tests__/secrets-service.test.ts` (89 tests after final Greptile cleanup fixes) - PASS: `pnpm --filter @paperclipai/server exec vitest run src/__tests__/heartbeat-issue-liveness-escalation.test.ts` (17 tests after the final rebase CI fix) - PASS: focused server Vitest batches covering heartbeat recovery, project env, plugin env, routines, low-trust, pipelines, monitors, watchdog, and stale queue paths. - PASS: GitHub checks are green on `2527febd106bcf3ca264ca0da7fca491084192d6`, including Typecheck + Release Registry, Build, General tests, serialized server suites, e2e, Canary Dry Run, verify, security checks, and Greptile Review. - PASS: Greptile Review completed successfully on `2527febd106bcf3ca264ca0da7fca491084192d6` with Confidence Score 5/5, and GraphQL review-thread audit returned zero unresolved non-outdated threads. ## Risks - Runtime behavior now depends on a run having a correct responsible user; missing or incorrect responsibility assignment can block runs before adapter dispatch. - `user_secret_ref` bindings intentionally expose metadata without values, but UI/API callers may need to handle the new binding kind explicitly. - External secret providers and IAM policies are not automatically provisioned by this PR; operators still need to configure provider-side access for non-local vaults. - The PR is broad across db/shared/server/UI/runtime paths, so release validation should include both API and UI secret workflows before merge. - The migration renumbering is intentionally incremental after upstream migrations; the branch migrations use guarded column/table/index/constraint creation so users who tested the older draft numbering should not hit duplicate DDL for the existing objects. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used OpenAI Codex, GPT-5-based coding agent (`gpt-5`), Codex local adapter with shell/tool use and code execution. Context window and internal reasoning mode are not exposed by the runtime. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
1991ec9d6f |
[codex] Split backend control-plane QoL slice (#4700)
## Thinking Path > - Paperclip is the control plane for autonomous AI companies, so backend task ownership, recovery, review visibility, and company-scoped limits need to stay enforceable without UI-only coupling. > - Closed PR #4692 bundled those backend changes with UI workflow, docs, skills, workflow, and lockfile churn. > - PAP-2694 asks for a clean backend/control-plane slice from that closed branch. > - This branch starts from current `master` and mines only the `cli`, `packages/db`, `packages/shared`, and `server` contracts/tests needed for the backend behavior. > - It explicitly excludes UI workflow/performance work, `.github/workflows/pr.yml`, `pnpm-lock.yaml`, docs, skills, package-script, adapter UI build-config, and perf fixture script changes; the only UI files are fixture/test updates required by the tightened shared `Company` contract. > - The benefit is a smaller reviewable PR that preserves the control-plane fixes while staying under Greptile s 100-file review limit. ## What Changed - Added company-scoped attachment-size limits through DB schema/migrations, shared company portability contracts, CLI import/export coverage, and server attachment upload enforcement. - Added productivity review service/API behavior for no-comment streak, long-active, and high-churn review issues, including request-depth clamping and issue summary exposure. - Hardened issue ownership and recovery/control-plane paths: peer-agent mutation denial, issue tree pause/resume behavior, stranded recovery origins, and related activity/test coverage. - Preserved related backend contract updates for routine timestamp variables and managed agent instruction bundles because they live in shared/server contracts from the source branch. - Addressed Greptile feedback by making `Company.attachmentMaxBytes` non-optional, simplifying review request-depth clamping, fixing the migration final newline, and enforcing the process-level attachment cap as the final ceiling for uploads. - Added minimal company fixtures needed for repo-wide typecheck/build and kept the PR to 66 changed files with forbidden/non-slice paths excluded. ## Verification - `pnpm install --frozen-lockfile` - `git diff --check origin/master..HEAD` - `git diff --name-only origin/master..HEAD | wc -l` -> 66 files - `git diff --name-only origin/master..HEAD -- .github/workflows/pr.yml pnpm-lock.yaml package.json doc skills .agents scripts packages/adapters` -> no output - `pnpm exec vitest run --config vitest.config.ts packages/shared/src/validators/issue.test.ts packages/shared/src/routine-variables.test.ts packages/shared/src/adapter-types.test.ts cli/src/__tests__/company-import-export-e2e.test.ts cli/src/__tests__/company.test.ts server/src/__tests__/productivity-review-service.test.ts server/src/__tests__/issue-tree-control-service.test.ts server/src/__tests__/issue-tree-control-routes.test.ts server/src/__tests__/issue-agent-mutation-ownership-routes.test.ts server/src/__tests__/issue-attachment-routes.test.ts server/src/__tests__/heartbeat-process-recovery.test.ts server/src/__tests__/issues-service.test.ts` -> 12 files, 147 tests passed - `pnpm exec vitest run --config vitest.config.ts cli/src/__tests__/company-delete.test.ts cli/src/__tests__/company-import-export-e2e.test.ts server/src/__tests__/productivity-review-service.test.ts` -> 3 files, 18 tests passed - `pnpm exec vitest run --config vitest.config.ts server/src/__tests__/issue-attachment-routes.test.ts` -> 1 file, 6 tests passed - `pnpm --filter @paperclipai/db typecheck && pnpm --filter @paperclipai/shared typecheck && pnpm --filter @paperclipai/server typecheck && pnpm --filter paperclipai typecheck` - `pnpm --filter @paperclipai/server typecheck` - `pnpm --filter @paperclipai/ui typecheck && pnpm --filter @paperclipai/ui build` ## Risks - Includes migrations `0073_shiny_salo.sql` and `0074_striped_genesis.sql`; merge ordering matters if another PR adds migrations first. - This is intentionally backend-only apart from fixture/test updates forced by shared type correctness; UI affordances from PR #4692 are not present here and should land in separate UI slices. - The worktree install emitted plugin SDK bin-link warnings for unbuilt plugin packages, but the targeted tests and package typechecks completed successfully. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected; check the roadmap first. See `CONTRIBUTING.md`. ## Model Used - OpenAI Codex, GPT-5 coding agent, tool-enabled terminal/GitHub workflow. Exact runtime context window was not exposed by the harness. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] If this change affects the UI, I have included before/after screenshots - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing> |
||
|
|
d0bdbe11a9 |
Stabilize inline selector keyboard handling (#4617)
## Thinking Path > - Paperclip's board UI relies on compact selectors for frequent issue and agent edits. > - Inline selectors often live inside larger keyboard-aware surfaces such as composers and popovers. > - Arrow, enter, tab, and escape keys handled by the selector should not leak to parent document shortcuts. > - Stale company selection should also stay hidden until the company list confirms it is valid. > - This pull request tightens inline selector keyboard handling and adds regression coverage for stale company bootstrap behavior. > - The benefit is fewer accidental parent interactions and safer company-scoped UI initialization. ## What Changed - Added a stable empty `recentOptionIds` default so selector filtering does not get a new array every render. - Mirrored highlighted option state into a ref so Enter/Tab commits the current highlighted option reliably after keyboard navigation. - Stopped propagation for selector-owned navigation/commit/escape keys. - Added jsdom regressions for inline selector keyboard handling and CompanyProvider stale selection behavior. ## Verification - `pnpm exec vitest run ui/src/components/InlineEntitySelector.test.tsx ui/src/context/CompanyContext.test.tsx` - Targeted selector and CompanyProvider tests pass cleanly without React `act(...)` warnings. - Screenshots not attached: this is keyboard/state behavior covered by component tests. ## Risks - Low risk: changes are scoped to inline selector key handling and tests. The main behavior shift is intentionally preventing handled selector keys from reaching parent listeners. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used - OpenAI Codex coding agent based on GPT-5, tool-enabled local repository and shell access, Paperclip heartbeat context. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [ ] If this change affects the UI, I have included before/after screenshots - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing> |
||
|
|
d95968a9f8 |
[codex] Ignore stale stored company selections (#4602)
## Thinking Path > - Paperclip orchestrates AI agents for zero-human companies > - The board UI is the operator’s control surface for selecting the active company > - A company id stored in localStorage can become stale across resets, imports, or deleted companies > - Exposing that stale id before companies load can briefly put downstream UI in an invalid company scope > - This pull request defers selected-company exposure until the loaded company list validates the stored id > - The benefit is a cleaner company-selection bootstrap path and fewer transient invalid API requests ## What Changed - Initialized `CompanyProvider` selection as `null` until companies finish loading. - Reused a stored company id only when it exists in the loaded selectable company list. - Cleared storage and selected state when no companies are available. - Added jsdom regression coverage for stale stored ids before and after company loading. ## Verification - `pnpm exec vitest run --project @paperclipai/ui ui/src/context/CompanyContext.test.tsx` ## Risks - Low risk. The change only affects selection bootstrap and keeps valid stored selections intact. - There may be a slightly longer initial `null` selected-company state while the company list is loading. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used - OpenAI Codex, GPT-5 coding agent, tool-enabled terminal/GitHub workflow, reasoning mode active. Context window not exposed in this environment. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] If this change affects the UI, I have included before/after screenshots - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing> |