mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-13 15:55:46 +01:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - Many teams run Paperclip self-hosted, so the project grows when those users tell others about it > - A GitHub star is the cheapest way for a user to do that, but the app never asks > - Asking too early is noise. A user who has finished a few tasks knows whether Paperclip helps them > - This pull request adds a one-time, dismissable modal that asks self-hosted users for a star after their company has 5 done tasks > - The benefit is more visibility for the project, and no repeated prompt for the user ## Linked Issues or Issue Description **Subsystem affected** Board UI (dialogs shown in the main app shell). **Problem or motivation** Self-hosted users have no prompt to star the repository. Users who like Paperclip often do not think of it. A prompt at the right moment helps other teams find the project. **Proposed solution** Show a small modal once the selected company has 5 tasks with status "done". The modal shows the Paperclip mark, the headline "You completed 5 tasks!", a short body, a "Star on GitHub" button and a "Not now" button. It shows only once per browser. Closing it in any way counts as seen. **Alternatives considered** A permanent banner or sidebar link. Both stay on screen after the user decides, so they cost attention every day. A one-time modal costs attention once. **Roadmap alignment** This is a small UI addition. It does not overlap with planned core work in ROADMAP.md. No related public issues or PRs found (searched for "star", "github star"). ## What Changed - Added `GitHubStarPrompt` (`ui/src/components/GitHubStarPrompt.tsx`). It reads the health endpoint and shows only on self-hosted instances (never on Cloud), never during onboarding, and only after 5 done tasks in the selected company. It waits for any other open dialog to close. It stores a "seen" flag in `localStorage` when it opens, so a reload does not show it again. - Mounted the prompt in `BoardDialogs`. - Exported the modal-detection helpers (`MODAL_SELECTOR`, `useModalOpen`) from `AnnouncementWell` so both components use the same rule. - Added `PaperclipMark`, the mark alone from the existing lockup geometry. The lockup and the mark now share one path constant. - Added the `issues.starPromptDone` query key. - Added tests for when the prompt opens and when it stays hidden. ## Verification - `pnpm --filter @paperclipai/ui exec vitest run src/components/GitHubStarPrompt.test.tsx src/components/AnnouncementWell.test.tsx` - `pnpm check:token-gates` (all gates clean) - `pnpm typecheck` - Manual: on a self-hosted dev instance, mark 5 tasks in one company as done. The modal opens. "Star on GitHub" opens github.com/paperclipai/paperclip in a new tab. Reload: the modal does not open again. To see it again, delete the `paperclip.github-star-prompt.seen` key from `localStorage`. ## Risks - Low risk. The change is UI only and additive. - The prompt runs one small issues query (limit 5) while it is still eligible. After it is seen, the query is disabled. - If `localStorage` is not available, the prompt treats itself as already seen and does not show. > 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 - Claude Opus 5.5 (Anthropic), model id `claude-opus-5-5`, run through Claude Code with tool use (shell, file edits). Standard context window; extended thinking at low effort. ## 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 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: squadbot <squadbot@squadbots-Mac-mini.local> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
56 lines
2.6 KiB
TypeScript
56 lines
2.6 KiB
TypeScript
// @vitest-environment jsdom
|
|
import { act } from "react";
|
|
import { flushSync } from "react-dom";
|
|
import { createRoot } from "react-dom/client";
|
|
import { afterEach, beforeEach, expect, it, vi } from "vitest";
|
|
import { BoardDialogs } from "./BoardDialogs";
|
|
|
|
const state = vi.hoisted(() => ({ newIssueOpen: false, newProjectOpen: false, newGoalOpen: false, newAgentOpen: false }));
|
|
vi.mock("../context/DialogContext", () => ({ useDialogState: () => state, useDialogActions: () => ({ closeNewIssue: vi.fn(), closeNewProject: vi.fn(), closeNewGoal: vi.fn(), closeNewAgent: vi.fn() }) }));
|
|
vi.mock("./NewIssueDialog", () => ({ NewIssueDialog: () => <input aria-label="Task draft" /> }));
|
|
vi.mock("./NewProjectDialog", () => ({ NewProjectDialog: () => <div>Project dialog</div> }));
|
|
vi.mock("./NewGoalDialog", () => ({ NewGoalDialog: () => <div>Goal dialog</div> }));
|
|
vi.mock("./NewAgentDialog", () => ({ NewAgentDialog: () => <div>Agent dialog</div> }));
|
|
vi.mock("./GitHubStarPrompt", () => ({ GitHubStarPrompt: () => null }));
|
|
let container: HTMLDivElement;
|
|
let root: ReturnType<typeof createRoot>;
|
|
beforeEach(() => {
|
|
Object.assign(state, { newIssueOpen: false, newProjectOpen: false, newGoalOpen: false, newAgentOpen: false });
|
|
container = document.createElement("div");
|
|
document.body.appendChild(container);
|
|
root = createRoot(container);
|
|
});
|
|
afterEach(async () => {
|
|
await act(async () => root.unmount());
|
|
container.remove();
|
|
});
|
|
async function render() { await act(async () => { root.render(<BoardDialogs />); }); }
|
|
it("renders the task form on the first open without an asynchronous loading frame", () => {
|
|
act(() => flushSync(() => root.render(<BoardDialogs />)));
|
|
state.newIssueOpen = true;
|
|
act(() => {
|
|
flushSync(() => root.render(<BoardDialogs />));
|
|
expect(container.querySelector('[aria-label="Task draft"]')).not.toBeNull();
|
|
expect(document.body.textContent).not.toContain("Loading new task");
|
|
});
|
|
});
|
|
it("leaves unopened dialog trees unmounted", async () => {
|
|
await render();
|
|
expect(container.innerHTML).toBe("");
|
|
});
|
|
it("loads a requested dialog and retains its draft across close and reopen", async () => {
|
|
await render();
|
|
state.newIssueOpen = true;
|
|
await render();
|
|
const draft = container.querySelector("input")!;
|
|
expect(draft).not.toBeNull();
|
|
draft.value = "Keep my draft";
|
|
expect(container.textContent).not.toContain("Project dialog");
|
|
state.newIssueOpen = false;
|
|
await render();
|
|
state.newIssueOpen = true;
|
|
await render();
|
|
expect(container.querySelector("input")).toBe(draft);
|
|
expect(container.querySelector("input")?.value).toBe("Keep my draft");
|
|
});
|