mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-11 14:10:50 +02:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - The sidebar identifies the current organization. > - An optional plugin can replace this navigation surface. > - The built-in title appears before discovery and module loading finish. > - This pull request reserves the trigger until its owner is known. > - The organization name appears once, while failures retain built-in navigation. ## Linked Issues or Issue Description Refs #13832. Searched related pull requests and issues; no duplicate fix found. **What happened?** The organization switcher renders a provisional built-in title before an installed replacement loads. Unrelated plugin imports can also affect its loading state. **Expected behavior** Reserve the trigger with a neutral placeholder, then show the resolved navigation surface. Keep the built-in menu on failed or absent contributions. **Steps to reproduce** Install an organization-switcher contribution. Delay session, company, contribution, and module responses. Reload the page and watch the trigger through each stage. ## What Changed - Reserve the trigger through account, company selection, slot discovery, and module loading. - Distinguish failed session lookup from pending lookup so errors retain usable navigation. - Load and await only contributions matching the requested slots. Observe completion of imports started by another consumer. - Document loading behavior and add regression coverage for loading, failures, unrelated modules, and identity transitions. ## Verification - `pnpm -r typecheck` passed, including Rust checks. - `pnpm build` passed. - All 629 UI test files passed: 6,593 tests. The 42 focused UI/API/plugin tests also passed. - `pnpm check:token-gates` and `git diff --check` passed. - `pnpm test:run` was also attempted. The broad local server run was stopped after recording skill-cache/channel fixture failures outside this diff (for example, runtime skill source status `missing` instead of `available`). The original cause is not established. All latest-head Linux CI gates pass; the complete UI suite and affected local checks pass. - Desktop (1440px) and mobile (390px) Chromium checks passed with real host components, dynamic module loading, and the built Account bundle. Delayed fixture responses produced exactly two title states: empty placeholder, then the resolved name. A slow refresh preserved the title and trigger dimensions; absent/failed plugin fallback and Escape dismissal passed, with zero uncaught browser errors. This is browser component integration, not a live signed-in tenant test. ## Risks A cold load displays a neutral placeholder until discovery completes. Absent, ambiguous, failed, and invalid contributions still use the built-in menu. No migrations or authorization changes. Scoped module loading changes when an unrelated contribution is imported; each surface loads its own matching modules. ## Model Used OpenAI Codex, GPT-6, with reasoning, code execution, and browser verification. The exact deployment ID and context window are not exposed in this session. ## 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>
56 lines
2.7 KiB
TypeScript
56 lines
2.7 KiB
TypeScript
// @vitest-environment jsdom
|
|
import { act } from "react";
|
|
import { createRoot, type Root } from "react-dom/client";
|
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
import { afterEach, beforeEach, expect, it, vi } from "vitest";
|
|
import type { PluginUiContribution } from "@/api/plugins";
|
|
import { queryKeys } from "@/lib/queryKeys";
|
|
import { _resetPluginModuleLoader, ensurePluginContributionLoaded, usePluginSlots } from "./slots";
|
|
|
|
const contribution: PluginUiContribution = {
|
|
pluginId: "navigation", pluginKey: "fixture.navigation", displayName: "Navigation", version: "1",
|
|
uiEntryFile: "index.js", launchers: [],
|
|
slots: [{ type: "organizationSwitcher", id: "navigation", displayName: "Organizations", exportName: "Switcher" }],
|
|
};
|
|
let root: Root;
|
|
let container: HTMLDivElement;
|
|
let client: QueryClient;
|
|
function Consumer() {
|
|
const { isLoading } = usePluginSlots({ slotTypes: ["organizationSwitcher"] });
|
|
return <span>{isLoading ? "Loading" : "Ready"}</span>;
|
|
}
|
|
async function render() {
|
|
await act(async () => root.render(<QueryClientProvider client={client}><Consumer /></QueryClientProvider>));
|
|
}
|
|
beforeEach(() => {
|
|
Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true });
|
|
vi.stubGlobal("__paperclipPluginBridge__", {});
|
|
client = new QueryClient({ defaultOptions: { queries: { retry: false, staleTime: Infinity } } });
|
|
container = document.createElement("div"); document.body.append(container); root = createRoot(container);
|
|
});
|
|
afterEach(async () => {
|
|
await act(async () => root.unmount()); container.remove(); client.clear(); _resetPluginModuleLoader();
|
|
vi.unstubAllGlobals(); vi.restoreAllMocks();
|
|
});
|
|
it("does not wait for or load modules for unrelated slots", async () => {
|
|
const fetch = vi.fn(); vi.stubGlobal("fetch", fetch);
|
|
client.setQueryData(queryKeys.plugins.uiContributions, [{ ...contribution,
|
|
slots: [{ type: "page", id: "other", displayName: "Other", exportName: "Page", routePath: "other" }],
|
|
}]);
|
|
await render();
|
|
expect(container.textContent).toBe("Ready");
|
|
expect(fetch).not.toHaveBeenCalled();
|
|
});
|
|
it("settles when a module import started by another consumer fails", async () => {
|
|
let reject!: (error: Error) => void;
|
|
const fetch = vi.fn(() => new Promise<Response>((_resolve, rejectPromise) => { reject = rejectPromise; }));
|
|
vi.stubGlobal("fetch", fetch); vi.spyOn(console, "error").mockImplementation(() => {});
|
|
client.setQueryData(queryKeys.plugins.uiContributions, [contribution]);
|
|
const loading = ensurePluginContributionLoaded(contribution);
|
|
await render();
|
|
expect(container.textContent).toBe("Loading");
|
|
await act(async () => { reject(new Error("unavailable")); await loading; });
|
|
expect(container.textContent).toBe("Ready");
|
|
expect(fetch).toHaveBeenCalledOnce();
|
|
});
|