mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-22 16:51:28 +02:00
A company with no agent cannot do anything: no runs, no tasks, nothing to show. The dashboard says so in a banner with a link, which asks the customer to notice a problem the product can fix for them. It is worse for a company created by Paperclip Cloud: Cloud creates the company before the tenant boots, so the companyless redirect never runs, and the customer arrives on an empty dashboard straight out of a signup flow that already asked for a mission. The dashboard now opens onboarding when the agent list has loaded and is empty, and onboarding opens on the agent step when the company already has its mission — read from the company-level goal the seed writes, under the query key the launch path already uses, so it shares a cache entry rather than adding a fetch. The step is decided once. `initialStep` is derived from the company list and the goal list, so it changes on any retry, refetch or cache invalidation. An effect that took it as a dependency called `setStep` on every one of those and moved a customer who was already mid-flow. Gating the input only narrowed that window; it could not close it. The step now belongs to the request that opened the wizard: the effect reads it through a ref and is keyed on the wizard opening or the company changing. `createdCompanyIdRef` beside it already used this pattern for the same reason. That exposed a path nothing had ever taken. A company reached the mission step only by creating itself on step 1, so opening an existing company there found code that had never run: `companyName` is only typed on step 1, and both ways forward require it, so the step could not be completed at all; and confirming advanced without writing anything, so the mission the customer typed was discarded. Both fixed, and the write now reconciles against the goal list rather than adding a second company goal, since the mission lookup fails open and can send a company that has one back to that step. Company-scoped state now stays with its company. `clearCompanyScopedState` runs when the route replaces a company and when it withdraws one — the same event, and clearing half of it left a goal id that made the next company skip a mission it had never given. `stillTheSameCompany` guards all five async writes, after the server work rather than before it, so a company switch mid-flight cannot hand the new company the old one's goal, project, issue or agent, and cannot leave a hired agent without its instructions file. The keyboard path honours `loading` like every button already did. `claimOnboardingOffer` makes onboarding an offer that stays declined for the visit. Route ownership is now recorded whenever the route names a company, including one the wizard already holds. This changes a documented rule deliberately: without it a self-created company was never withdrawn, so `/onboarding` would show "create a company" while still holding the previous one and write the customer's new mission into it. Tested at the seam, because every defect on this branch lived between a value and its consumer and the predicate tests passed at every stage. `OnboardingWizard.step.test.tsx` renders the real wizard against the real resolver and the real mission hook across 18 cases, and each was fault-injected against the code it replaces rather than trusted on a green run. That caught a case that passed against the broken code, and a race in one of the guards. ui typecheck clean; full ui suite 3923 pass, with one timezone-dependent IssueProperties failure present on this branch's base in a file this change does not touch. All CI gates green; Greptile 5/5. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
31 lines
1.2 KiB
TypeScript
31 lines
1.2 KiB
TypeScript
/**
|
|
* Which companies have already been offered onboarding automatically.
|
|
*
|
|
* The dashboard opens onboarding for a company that has no agent. That is an
|
|
* offer, and an offer that reappears after it is declined is not one, so each
|
|
* company gets at most one.
|
|
*
|
|
* Module scope rather than state inside the dashboard, on purpose. A ref dies
|
|
* with the component, so navigating away and back would offer again, and it
|
|
* holds one company, so visiting a second agentless company and returning to
|
|
* the first would too. A page reload does offer again, which is the right
|
|
* scope: that is a new visit rather than the same one continuing.
|
|
*/
|
|
const offeredCompanyIds = new Set<string>();
|
|
|
|
/**
|
|
* Records an offer, and reports whether it is the first one for this company.
|
|
* Returns false when onboarding has already been offered, in which case the
|
|
* caller must not open it again.
|
|
*/
|
|
export function claimOnboardingOffer(companyId: string): boolean {
|
|
if (offeredCompanyIds.has(companyId)) return false;
|
|
offeredCompanyIds.add(companyId);
|
|
return true;
|
|
}
|
|
|
|
/** Forget every offer. For tests, which must not inherit each other's state. */
|
|
export function resetOnboardingOffers(): void {
|
|
offeredCompanyIds.clear();
|
|
}
|