Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions .claude/skills/finalize-inbox-sync/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
---
name: finalize-inbox-sync
description: Fifth step of the inbox-to-cloud sync workflow. Runs typecheck and format, runs any inbox-scene tests, and produces the structured final report the user verifies. Assumes `/simplify` has already run on the touched files (orchestrated by the parent skill). Use as part of `/sync-inbox-to-cloud`, or standalone after a hand-rolled sync followed by a manual simplify pass.
---

# Finalize the inbox sync

This is sub-skill 5 of `/sync-inbox-to-cloud`. Re-read the parent skill's hard rules at `/Users/twixes/Developer/code/.claude/skills/sync-inbox-to-cloud/SKILL.md` before starting.

## Goal

Verify the simplified sync compiles cleanly and emit the structured final report.

By the time this sub-skill is invoked, `/implement-inbox-sync` and `/simplify` have already run. The code is integrated and simplified; you just need to validate and report.

## Steps

### 1. Typecheck and format

Run from `~/Developer/posthog/`:

```sh
pnpm --filter=@posthog/frontend typescript:check
pnpm --filter=@posthog/frontend format
```

**Typecheck and format are not optional.** If typecheck takes 5+ minutes, run it anyway — kick it off in the background (`run_in_background: true` on the Bash call) and keep building the report in parallel. Do not report `not run` for typecheck or format under Verification. "Not run" is not an acceptable verification outcome — the user will run them locally and get a wall of errors that you could have surfaced.

If typecheck fails on a generated `*LogicType.ts`, the Kea type generator should regenerate it as part of the pipeline. If it still fails, follow `/Users/twixes/Developer/posthog/.claude/skills/writing-kea-logics/SKILL.md`.

If `/simplify` proposed deletions that broke a wire-up, fix it here before reporting.

**Do not** run the desktop typecheck (`pnpm typecheck` from the code repo) — you didn't touch that side.

### 2. Run any tests for the inbox scene

```sh
hogli test frontend/src/scenes/inbox
```

If tests fail because of behaviour changes in the port (e.g. the default `statusFilter` changed to match desktop), update the test expectations — desktop is the source of truth.

### 3. Produce the final report

This is the artifact the user verifies. Keep it skimmable — bullets, not paragraphs.

- **Synced** — features ported / polished, one bullet each, citing desktop source file → cloud destination file.
- **Stubbed (Coming soon™)** — desktop features intentionally disabled on cloud. Should contain ONLY live-chat affordances (Discuss / chat-with-inbox). If more than 1-2 items, you stubbed too aggressively — go back and revisit.
- **Reused existing cloud surface** — desktop features whose run-log viewing re-uses `products/tasks/frontend/`. The linkage itself belongs under "Synced", not here.
- **Skipped (rare)** — features with no cloud analogue (e.g. OS-only Electron API). More than a couple items means you skipped too much.
- **Open questions** — missing backend endpoints, UX ambiguities, sub-skill ambiguities for the next iteration. The user uses this to refine the skills.
- **Verification** — typecheck pass/fail, format pass/fail, simplify outcome, tests pass/fail-or-N/A. Cite commands and any error excerpts if anything failed.
- **Files modified** — final list of touched cloud files, for the user's diff review.

## Next step

**Do not stop here.** The parent `/sync-inbox-to-cloud` is a single uninterrupted workflow. Immediately invoke `/reflect-on-inbox-sync` using the Skill tool — it will audit the run against the hard rules and append concrete skill-refinement suggestions to your report. Do not finalize the output as "done" until reflection has appended its section.

Make the report accurate — if you skipped something for a reason not covered in the hard rules, say so under "Open questions" so the rules can be tightened next iteration.
71 changes: 71 additions & 0 deletions .claude/skills/implement-inbox-sync/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
---
name: implement-inbox-sync
description: Third step of the inbox-to-cloud sync workflow. Executes the manifest from `/plan-inbox-sync`, dispatching parallel sub-agents per the slicing plan and then integrating their work in the central cloud scene and logic. Owns the desktop→cloud translation (Quill/Radix → LemonUI, Zustand/TanStack Query → Kea, TanStack Router → scenes/urls). Use as part of `/sync-inbox-to-cloud`, or standalone to execute a hand-rolled manifest.
---

# Implement the inbox sync

This is sub-skill 3 of `/sync-inbox-to-cloud`. Re-read the parent skill's hard rules at `/Users/twixes/Developer/code/.claude/skills/sync-inbox-to-cloud/SKILL.md` before starting.

## Goal

Apply the manifest from `/plan-inbox-sync` to the cloud Inbox. Parallelize where the plan says so; integrate centrally.

## Phases

### Phase A — Dispatch parallel sub-agents (if planned)

**Before dispatching sub-agents**, the orchestrator verifies cloud has every non-trivial dependency the desktop side uses. Grep `~/Developer/posthog/frontend/package.json` for each library that appears in desktop imports — typical candidates: `framer-motion`, `tiptap`, `xterm`, `codemirror`, `react-virtualized`. For any missing library, decide upfront how slices should handle it:

- **Add the dep** to cloud's `package.json` (small, well-maintained libraries).
- **Substitute** with a cloud-available alternative — and brief sub-agents on the exact substitution (e.g. `framer-motion <motion.div animate={{x:N}}>` → `<div style={{transform: 'translateX(Npx)', transition: '...'}}>`).
- **Skip the visual** and brief sub-agents to surface it as Open Question.

The wrong move is to let each sub-agent guess. Put the decision in the sub-agent prompt so all slices substitute the same way.

Read `/Users/twixes/Developer/code/.claude/skills/sync-inbox-to-cloud/references/parallelization.md` for the full pattern. Short version:

- Dispatch up to 4 sub-agents in a single message (parallel Agent tool calls).
- Each sub-agent owns a disjoint file scope and creates new files only in their owned subdirectory.
- Sub-agents MUST NOT touch the central scene entry-point file(s) or the central composing logic.
- Each sub-agent's prompt inlines the relevant hard rules and the translation rules from `references/translation.md`.
- Each sub-agent's prompt names the files they own AND lists the files owned by other slices as "do not touch".

**Slice prerequisites.** If one slice produces types, utils, or API wrappers consumed by other slices, treat it as a sequential prerequisite — dispatch it alone, wait for it to return, then dispatch the dependents in parallel. The naive "everything parallel" pattern works only when slices share no contracts; in practice a Foundation slice (types + API + badges + utils) almost always exists. Default order: Foundation → wait → {feature slices in parallel} → orchestrator integration.

If the plan said don't parallelize (small re-sync, just-shifted IA, plan has only 1-2 cohesive areas), implement sequentially. The translation rules in `references/translation.md` apply either way.

### Phase B — Integrate

The orchestrator (you) owns the central integration files. After sub-agents return:

- **Write/rewrite the central cloud scene entry-point file** to compose the new subcomponents from sub-agents. If desktop's IA has shifted (e.g. introduced new tabs), this is where the new top-level shape lands.
- **Write/rewrite the central composing Kea logic** to `connect` to the sibling logics, handle routing (`urlToAction` / `actionToUrl`), and own cross-slice scene state.
- **Add new TS wrappers in `frontend/src/lib/api.ts`** if any slice surfaced an existing backend endpoint without a wrapper. Verify the endpoint exists first (grep `products/signals/backend/views.py`).
- **Update `urls.ts` and `scenes.ts`** if desktop's IA introduced new routes (new tabs, new subroutes).
- **Update `types.ts`** with any new shared types.

### Phase C — Quick smoke-check

Before handing off to `/finalize-inbox-sync`, do a quick visual scan of your work:

- Did any sub-agent return saying it had to touch a file outside its scope? If so, you have an integration mess — re-plan that slice or absorb the file into the orchestrator's scope.
- Did any sub-agent return saying it stubbed Coming soon™ on something that isn't live chat? Revert that — wire it properly.
- Did any sub-agent return saying it skipped work? Re-dispatch with stronger framing — there is no skip.

## Translation

See `references/translation.md` for the desktop→cloud mapping covering:

- Component library (Radix Themes + Quill → LemonUI)
- Icons (Phosphor → `@posthog/icons`)
- State management (Zustand + TanStack Query → Kea)
- Routing (TanStack Router → scenes / urls / sceneTypes)
- API (already shared; just frontend wrappers in `lib/api.ts`)
- Persistence (Zustand `persist()` → Kea `{ persist: true }` per reducer)
- Hedgehogs / empty-state assets (`lib/components/hedgehogs`)
- Analytics events (`posthog.capture(...)` from `posthog-js`; mirror desktop event names verbatim)

## Next step

**Do not stop here.** The parent `/sync-inbox-to-cloud` is a single uninterrupted workflow. Immediately invoke `/simplify` using the Skill tool, passing it the list of cloud files you touched as scope. Do not summarize the work and wait, do not produce a report yet (that's step 5), do not pause — chain straight to the next step.
91 changes: 91 additions & 0 deletions .claude/skills/implement-inbox-sync/references/translation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Desktop → cloud translation

Reference for translating PostHog Code's desktop UI/state idioms into PostHog Cloud's idioms. Used by `/implement-inbox-sync` and inlined into parallel sub-agent prompts.

## Component library

Desktop uses `@radix-ui/themes`, `@posthog/quill`, and `@phosphor-icons/react`. Cloud uses `@posthog/lemon-ui`, `lib/lemon-ui/*`, and `@posthog/icons`. Map each desktop component to its closest LemonUI equivalent:

- `Button` from `@posthog/quill` or `@radix-ui/themes` → `LemonButton` from `@posthog/lemon-ui`
- `IconButton` → `LemonButton` with `icon` prop, no children
- `Text`, `Box`, `Flex` from `@radix-ui/themes` → plain `div` / `span` + Tailwind classes (`flex`, `gap-2`, etc.); cloud does not use Radix Themes
- `Dialog` from `@radix-ui/themes` → `LemonModal` from `@posthog/lemon-ui`
- `Tabs` from `@radix-ui/themes` → `LemonTabs` from `@posthog/lemon-ui`
- `DropdownMenu` from `@radix-ui/themes` → `LemonMenu` from `@posthog/lemon-ui`
- `Checkbox` → `LemonCheckbox`
- `Switch` → `LemonSwitch`
- `Select` → `LemonSelect` (single) / `LemonInputSelect` (multi)
- `TextField`, `Input` → `LemonInput`
- `TextArea` → `LemonTextArea`
- `Badge` from `@radix-ui/themes` → `LemonTag` (colored labels) or `LemonBadge` (counts)
- `Tooltip` from `@radix-ui/themes` or `@posthog/quill` → `Tooltip` from `@posthog/lemon-ui`
- `ScrollArea` from `@radix-ui/themes` → plain `div` with overflow Tailwind, or `ScrollableShadows` from `lib/components/ScrollableShadows`
- `Skeleton` → `LemonSkeleton`
- `Banner` / inline alert → `LemonBanner`
- `Spinner` from `@radix-ui/themes` → `Spinner` from `@posthog/lemon-ui`
- `Link` (any) → `Link` from `@posthog/lemon-ui`
- `Markdown` / desktop's report-summary markdown component → `LemonMarkdown` from `lib/lemon-ui/LemonMarkdown`
- Profile pictures → `ProfilePicture` from `lib/lemon-ui/ProfilePicture/ProfilePicture`
- `Kbd` from `@posthog/quill` → `KeyboardShortcut` from `lib/components/KeyboardShortcut` if cloud has it, otherwise inline `<kbd>` with Tailwind
- `cn` from `@posthog/quill` → `clsx` from `clsx`

For icons, map `@phosphor-icons/react` icons to the closest equivalent in `@posthog/icons` (e.g. `EnvelopeSimpleIcon` → `IconLetter` or `IconNotification`). Do not bring Phosphor or Lucide into cloud.

**Some icons live in `lib/lemon-ui/icons` rather than `@posthog/icons`.** Before deciding an icon doesn't exist, also grep `frontend/src/lib/lemon-ui/icons/icons.tsx`. Common cases of icons in `lib/lemon-ui/icons` (not `@posthog/icons`): `IconOpenInNew`, `IconLink`, `IconArrowDown`, `IconTag`, `IconChevronUp`, `IconKanban`, `IconTicket`. The import path matters — `import { IconOpenInNew } from 'lib/lemon-ui/icons'`, not `from '@posthog/icons'`.

When a desktop component has no direct LemonUI equivalent (a specific animated loader, a custom badge variant, etc.), implement it in plain JSX + Tailwind in the cloud Inbox dir. **Do not add a third component library.**

## Animations

Desktop uses `framer-motion` for entry/exit animations, fan stacks, spring transitions. Cloud does NOT currently have `framer-motion` installed. Substitute as follows:

- `<motion.div initial={{opacity:0}} animate={{opacity:1}} transition={{duration:0.2}}>` → `<div className="animate-fade-in">` if cloud has a Tailwind `fade-in` keyframe; otherwise plain `<div>` with `style={{ transition: 'opacity 0.2s ease' }}`.
- `<motion.div animate={{ x: N, scale: S }}>` → `<div style={{ transform: \`translateX(${N}px) scale(${S})\`, transition: 'transform 0.3s cubic-bezier(0.2, 0.8, 0.2, 1)' }}>`.
- `<AnimatePresence>` exit animations → handle via CSS `@keyframes` + a `transition-group`-style mount/unmount delay, or accept synchronous unmount if the exit visual is minor.
- Spring-physics fan stacks (e.g. desktop's MultiSelectStack) → port via CSS `transition: transform ... cubic-bezier(...)`. The visual will land 90% of the way without `spring` physics.
- Stagger entry (per-row `delay: i * 0.035`) → CSS `animation-delay` on a `@keyframes` rule, or accept synchronous appearance for list rows (typical at >20 rows the stagger barely shows anyway).

If a desktop animation is core UX and CSS substitution would be jarring (e.g. a primary interaction surface), surface it under Open Questions in the final report and recommend adding `framer-motion` to cloud's `package.json` rather than dropping silently.

**Replacing `<motion.div>` with a bare `<div>` and reporting "no functional impact" is a polish drop.** The parent skill's hard rules forbid this — at minimum keep CSS transitions; at maximum surface as Open Question.

## State management

Desktop uses Zustand stores and TanStack Query. Cloud uses Kea. Map each desktop pattern to its Kea equivalent:

- Zustand `create(...)` store with state + actions → Kea logic with `actions` + `reducers`
- Zustand persisted store via `persist()` middleware → Kea reducer with the `{ persist: true }` option per-reducer. Pattern: `reducers({ statusFilter: [DESKTOP_DEFAULT, { persist: true }, { setStatusFilter: (_, { value }) => value }] })`. **If desktop persists a piece of state via Zustand `persist`, the cloud Kea reducer MUST also persist via `{ persist: true }`.** Default values mirror desktop verbatim — copy the literal default from the desktop store file.
- TanStack Query `useQuery` / `useInfiniteQuery` → Kea `loaders` builder. Pagination: a `loadMoreReports` action that appends; do not re-implement infinite scroll from scratch.
- TanStack Query refetch interval / polling → Kea `afterMount` + `cache.disposables.add(...)` registering a `setInterval`. Read the cloud `using-kea-disposables` skill first.
- `useMutation` → a Kea `actions` + `listeners` block that calls `api.signalReports.*`, dispatches success/failure actions, surfaces `lemonToast` for errors.
- tRPC client call from a component → a `listeners` call that hits the appropriate `api.*` REST endpoint in `~/Developer/posthog/frontend/src/lib/api.ts`.
- `useEffect` for subscriptions / window listeners → `afterMount` + `cache.disposables.add(...)`. Never write a bare `addEventListener` in a Kea logic without disposables.
- Cross-store `useOtherStore.getState()` → Kea `connect({ values: [otherLogic, [...]], actions: [otherLogic, [...]] })`.
- `useState` for UI-local toggles → `useState` is fine on cloud too — local component state stays local.
- Custom hook orchestrating multiple queries → a selector or loader on the logic that merges them. Do not write hooks that re-run multiple queries.

The `useDiscussReport` and any chat-with-inbox affordance are agent-chat hooks — stub them Coming soon™. The "Create PR" / `useCreatePrReport` is a **task-kickoff** hook, not a chat hook — port it fully, wiring to cloud's `api.tasks.*` and reusing desktop's prompt-building utils where the prompt content matters.

## Routing

Desktop uses TanStack Router. Cloud uses scenes + urls + sceneTypes. Map each desktop pattern to its cloud equivalent:

- Route registered in `apps/code/src/renderer/router.tsx` → already-registered cloud scene (today: `Scene.Inbox` in `frontend/src/scenes/sceneTypes.ts`, mapped in `frontend/src/scenes/scenes.ts`, URL in `frontend/src/scenes/urls.ts` as `urls.inbox(reportId?)`). If desktop introduces new top-level tabs, add the corresponding cloud routes here.
- `useSearch()` / route params → `urlToAction` / `actionToUrl` in the central inbox scene logic
- Deep-link selection (desktop's `useInboxDeepLink`, `useInboxDeepLinkListSync`) → `setSelectedReportId` action + `actionToUrl` round-trip on the cloud side. Verify deep links survive the polish you port; extend if needed.

For modals and drawers (sources dialog, dismiss dialog, configure-agents drawer), they generally stay in-scene as `LemonModal` / `LemonDrawer`, no new URL — unless desktop deep-links to a specific configuration tab, in which case mirror that with a sub-route.

## API

Both sides talk to the same Django backend. On cloud, REST endpoints are wrapped in `frontend/src/lib/api.ts` under `api.signalReports.*` (list, retrieve, dismiss, snooze, etc.) and adjacent groupings (`api.tasks.*`, etc.). Read those wrappers — they tell you the exact request shape. If desktop reads a field cloud's serializer doesn't expose, that's a backend gap → surface it in the report, don't add a parallel API. If the endpoint exists but cloud lacks a TS wrapper, add the wrapper.

## Empty / loading / setup states

Desktop has rich onboarding (warming-up panes, select-something panes, gated panes, skeleton backdrops, setup panes) plus sources dialogs. Port the **logic** of each (when it shows, what it says, what action it offers) into cloud's equivalent. Don't copy desktop's exact JSX — cloud's layout, hedgehogs, and typography differ.

Hedgehogs come from `lib/components/hedgehogs` (e.g. `GraphsHog`, `PopUpBinocularsHog`). Use these for cloud empty states; do not import desktop assets.

## Analytics

Desktop fires events via `@utils/analytics` with constants from `@shared/types/analytics`. Cloud fires events via `posthog.capture(...)` from `posthog-js` (or the `eventUsageLogic` pattern). **Mirror the event name and properties verbatim** — same `inbox viewed`, same property keys — so we can dashboard both surfaces together. If desktop has a constant, copy the literal string into cloud.
Loading
Loading