---
phase: 22-add-from-catalog-thread-integration
plan: 02
type: execute
wave: 2
depends_on: ["22-01"]
files_modified:
  - src/client/components/AddToThreadModal.tsx
  - src/client/routes/__root.tsx
autonomous: false
requirements: [CATFLOW-05, CATFLOW-06]

must_haves:
  truths:
    - "Clicking Add on a catalog search card in thread mode opens the AddToThreadModal with a thread picker"
    - "User can select an existing active thread and the catalog item is added as a candidate"
    - "User can choose New Thread which shows thread name + category fields and creates thread + candidate in one step"
    - "After creating a new thread, subsequent adds in same session default to that thread"
    - "Clicking Add to Thread on the global item detail page opens the same thread picker modal"
  artifacts:
    - path: "src/client/components/AddToThreadModal.tsx"
      provides: "Thread picker modal with new thread creation flow"
      min_lines: 120
    - path: "src/client/routes/__root.tsx"
      provides: "AddToThreadModal rendered at root level"
      contains: "AddToThreadModal"
  key_links:
    - from: "src/client/components/AddToThreadModal.tsx"
      to: "/api/threads"
      via: "useCreateThread for new thread creation"
      pattern: "useCreateThread"
    - from: "src/client/components/AddToThreadModal.tsx"
      to: "/api/threads/:threadId/candidates"
      via: "apiPost for candidate creation after thread selection/creation"
      pattern: "apiPost.*candidates"
    - from: "src/client/components/AddToThreadModal.tsx"
      to: "src/client/stores/uiStore.ts"
      via: "setCatalogSessionThreadId to remember thread selection"
      pattern: "setCatalogSessionThreadId"
---

<objective>
Build the AddToThreadModal: thread picker with existing active threads, "New Thread..." option for combined thread+candidate creation, and session thread memory. Wire to root layout. Verify CATFLOW-06 regression (thread resolution with catalog-linked candidates) via existing service tests.

Purpose: CATFLOW-05 -- users can add catalog items as thread candidates from search. CATFLOW-06 -- resolution already works, confirmed by existing tests.
Output: Working add-to-thread flow, complete Phase 22 catalog integration.
</objective>

<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/22-add-from-catalog-thread-integration/22-CONTEXT.md
@.planning/phases/22-add-from-catalog-thread-integration/22-RESEARCH.md
@.planning/phases/22-add-from-catalog-thread-integration/22-01-SUMMARY.md

<interfaces>
<!-- Types and contracts from Plan 01 and existing code -->

From src/client/stores/uiStore.ts (after Plan 01):
```typescript
addToThreadModal: { open: boolean; globalItemId: number | null; globalItemName: string | null };
openAddToThread: (globalItemId: number, globalItemName: string) => void;
closeAddToThread: () => void;
catalogSessionThreadId: number | null;
setCatalogSessionThreadId: (id: number | null) => void;
```

From src/client/hooks/useThreads.ts:
```typescript
export function useThreads(includeResolved = false)
// Returns ThreadListItem[] with: id, name, status, categoryId, categoryName, candidateCount

export function useCreateThread()
// mutationFn: (data: { name: string; categoryId: number }) => apiPost<ThreadListItem>("/api/threads", data)
// onSuccess invalidates ["threads"]
```

From src/client/hooks/useCandidates.ts:
```typescript
export function useCreateCandidate(threadId: number)
// mutationFn: (data: CreateCandidate & { imageFilename?: string }) => apiPost(...)
// NOTE: threadId is a hook parameter, not in mutation payload
// For dynamic threadId (new thread flow), use apiPost directly instead
```

From src/shared/schemas.ts:
```typescript
export const createCandidateSchema = z.object({
  name: z.string().min(1, "Name is required"),
  weightGrams: z.number().nonnegative().optional(),
  priceCents: z.number().int().nonnegative().optional(),
  categoryId: z.number().int().positive(),
  notes: z.string().optional(),
  productUrl: z.string().url().optional().or(z.literal("")),
  imageFilename: z.string().optional(),
  imageSourceUrl: z.string().url().optional().or(z.literal("")),
  status: candidateStatusSchema.optional(),
  pros: z.string().optional(),
  cons: z.string().optional(),
  globalItemId: z.number().int().positive().optional(),
});
```

From src/client/lib/api.ts:
```typescript
export async function apiPost<T>(url: string, body: unknown): Promise<T>
```

From src/client/hooks/useGlobalItems.ts:
```typescript
export function useGlobalItem(id: number | null)
// Returns globalItem with: id, brand, model, category, weightGrams, priceCents, imageUrl, description, ownerCount
// NOTE: Already has `enabled: id != null` guard built in -- safe to call with null globalItemId
```
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Build AddToThreadModal with thread picker and new thread flow</name>
  <files>
    src/client/components/AddToThreadModal.tsx
    src/client/routes/__root.tsx
  </files>
  <read_first>
    src/client/stores/uiStore.ts
    src/client/components/CreateThreadModal.tsx
    src/client/components/AddToCollectionModal.tsx
    src/client/hooks/useThreads.ts
    src/client/hooks/useCandidates.ts
    src/client/hooks/useGlobalItems.ts
    src/client/hooks/useCategories.ts
    src/shared/schemas.ts
    src/client/lib/api.ts
    src/client/routes/__root.tsx
  </read_first>
  <action>
**Create `src/client/components/AddToThreadModal.tsx`**

This modal has two modes: "pick" (select existing thread) and "create" (new thread + candidate).

**Component structure:**

```typescript
import { useState, useEffect } from "react";
import { toast } from "sonner";
import { useCategories } from "../hooks/useCategories";
import { useGlobalItem } from "../hooks/useGlobalItems";
import { useCreateThread, useThreads } from "../hooks/useThreads";
import { apiPost } from "../lib/api";
import { useUIStore } from "../stores/uiStore";
import { useQueryClient } from "@tanstack/react-query";
```

**State management:**
- Read from UIStore: `addToThreadModal` (open, globalItemId, globalItemName), `closeAddToThread`, `catalogSessionThreadId`, `setCatalogSessionThreadId`
- Local state: `mode` ("pick" | "create"), `selectedThreadId` (number | null), `newThreadName` (string), `newThreadCategoryId` (number | null), `isSubmitting` (boolean), `error` (string | null)
- Fetch: `useThreads()` for thread list, `useCategories()` for new thread category dropdown, `useGlobalItem(globalItemId)` for the global item data needed to create the candidate. NOTE: `useGlobalItem` already has `enabled: id != null` guard built in, so passing `globalItemId` (which may be null when modal is closed) is safe -- no additional enabled guard needed.
- `useCreateThread()` for thread creation, `useQueryClient()` for manual invalidation

**Initialization logic (per D-12, D-19):**
- When modal opens (`open` becomes true), if `catalogSessionThreadId` is set, pre-select it in the thread picker (`selectedThreadId = catalogSessionThreadId`)
- Filter threads to active only: `threads?.filter((t) => t.status === "active") ?? []`
- If no active threads exist, auto-switch to "create" mode (per D-09 empty state)

**"Pick" mode UI (per D-06, D-07):**
- Header: "Add to Thread"
- Subheading: show `globalItemName` in `<p className="text-sm text-gray-500 mb-4">`
- Thread dropdown `<select>`:
  - Each active thread as `<option value={t.id}>{t.name} ({t.categoryName})</option>` -- show category alongside name per discretion
  - Last option: `<option value="new">+ New Thread...</option>`
  - When "new" is selected, switch `mode` to "create"
- Submit button: "Add as Candidate", disabled during `isSubmitting`
- Cancel button: calls `closeAddToThread()`

**"Create" mode UI (per D-11, D-13):**
- Header: "New Thread + Candidate"
- Subheading: show `globalItemName`
- Thread name input: same as CreateThreadModal
- Category dropdown: same as CreateThreadModal (from `useCategories()`)
- Submit button: "Create & Add", disabled during `isSubmitting`
- Cancel button: goes back to "pick" mode if active threads exist, otherwise calls `closeAddToThread()`
- Back link: "Back to thread picker" if active threads exist

**Submit handler for "pick" mode:**
```typescript
async function handleAddToExistingThread() {
  if (!selectedThreadId || !globalItemId) return;
  setIsSubmitting(true);
  setError(null);
  try {
    const thread = activeThreads.find((t) => t.id === selectedThreadId);
    await apiPost(`/api/threads/${selectedThreadId}/candidates`, {
      name: globalItemName ?? "Unknown Item",
      globalItemId,
      categoryId: thread?.categoryId ?? categories?.[0]?.id ?? 1,
      weightGrams: globalItem?.weightGrams ?? undefined,
      priceCents: globalItem?.priceCents ?? undefined,
    });
    queryClient.invalidateQueries({ queryKey: ["threads"] });
    queryClient.invalidateQueries({ queryKey: ["threads", selectedThreadId] });
    setCatalogSessionThreadId(selectedThreadId);
    toast.success(`Added to "${thread?.name ?? "thread"}"`);
    closeAddToThread();
  } catch (err) {
    setError(err instanceof Error ? err.message : "Failed to add candidate");
  } finally {
    setIsSubmitting(false);
  }
}
```

Use `apiPost` directly instead of `useCreateCandidate` hook because `useCreateCandidate(threadId)` requires threadId at hook initialization time. For the "pick" flow the threadId changes with selection. Using `apiPost` directly avoids this pitfall (per RESEARCH.md Pitfall 1).

**Submit handler for "create" mode (per D-11):**
```typescript
async function handleCreateThreadAndAdd() {
  const trimmedName = newThreadName.trim();
  if (!trimmedName || !newThreadCategoryId || !globalItemId) return;
  setIsSubmitting(true);
  setError(null);
  try {
    const thread = await createThread.mutateAsync({ name: trimmedName, categoryId: newThreadCategoryId });
    await apiPost(`/api/threads/${thread.id}/candidates`, {
      name: globalItemName ?? "Unknown Item",
      globalItemId,
      categoryId: newThreadCategoryId,
      weightGrams: globalItem?.weightGrams ?? undefined,
      priceCents: globalItem?.priceCents ?? undefined,
    });
    queryClient.invalidateQueries({ queryKey: ["threads"] });
    setCatalogSessionThreadId(thread.id);
    toast.success(`Created "${trimmedName}" with first candidate`);
    closeAddToThread();
  } catch (err) {
    setError(err instanceof Error ? err.message : "Failed to create thread");
  } finally {
    setIsSubmitting(false);
  }
}
```

**Form reset:** `useEffect` watching `open` -- when it becomes false, reset all local state (mode back to "pick", clear name, selectedThreadId, error).

**Modal rendering:** Follow CreateThreadModal pattern:
- `fixed inset-0 z-50 flex items-center justify-center bg-black/50`
- onClick backdrop = close
- Escape key = close
- Inner: `w-full max-w-md bg-white rounded-xl shadow-xl p-6`, `e.stopPropagation()`

**Add to root layout** (`src/client/routes/__root.tsx`):
Add import:
```typescript
import { AddToThreadModal } from "../components/AddToThreadModal";
```
Add `<AddToThreadModal />` right after `<AddToCollectionModal />` in the JSX.
  </action>
  <verify>
    <automated>cd /home/jean-luc-makiola/Development/projects/GearBox && bun run build 2>&1 | tail -5 && bun test tests/services/thread.service.test.ts 2>&1 | tail -10</automated>
  </verify>
  <acceptance_criteria>
    - `src/client/components/AddToThreadModal.tsx` exists with `export function AddToThreadModal`
    - `AddToThreadModal.tsx` contains `useThreads` import
    - `AddToThreadModal.tsx` contains `useCreateThread` import
    - `AddToThreadModal.tsx` contains `useGlobalItem` import
    - `AddToThreadModal.tsx` contains `apiPost` import from `../lib/api`
    - `AddToThreadModal.tsx` contains `setCatalogSessionThreadId`
    - `AddToThreadModal.tsx` contains `catalogSessionThreadId`
    - `AddToThreadModal.tsx` contains `toast.success`
    - `AddToThreadModal.tsx` contains `globalItemId` in the apiPost call body
    - `AddToThreadModal.tsx` contains `mode` state with "pick" and "create" values
    - `AddToThreadModal.tsx` contains `+ New Thread...` option text
    - `src/client/routes/__root.tsx` contains `import { AddToThreadModal }`
    - `src/client/routes/__root.tsx` contains `<AddToThreadModal />`
    - `bun run build` exits with code 0
    - `bun test tests/services/thread.service.test.ts` passes -- confirms CATFLOW-06 regression coverage (resolveThread with globalItemId candidate creates reference item, test at line 704)
  </acceptance_criteria>
  <done>AddToThreadModal supports picking an existing active thread and creating a new thread with first candidate. Session thread tracking persists across adds. Build passes. Thread service tests pass confirming CATFLOW-06 (resolveThread with catalog-linked candidate) is covered.</done>
</task>

<task type="checkpoint:human-verify" gate="blocking">
  <name>Task 2: Verify complete add-from-catalog flows</name>
  <what-built>
    Complete add-from-catalog and add-to-thread flows wired through catalog search overlay and global item detail pages. Two modal components (AddToCollectionModal, AddToThreadModal) with toast notifications.
  </what-built>
  <how-to-verify>
    Run `bun run dev` and test in browser at http://localhost:5173:

    **Flow 1: Add to Collection from catalog search**
    1. Click the FAB (floating action button)
    2. Select "Add to Collection"
    3. Catalog search overlay opens with "Adding to Collection" context
    4. Search for an item and click "Add" on a card
    5. Verify: AddToCollectionModal opens with category dropdown, notes field, purchase price field
    6. Select a category, optionally fill notes/price, click "Add to Collection"
    7. Verify: toast "Added to Collection" appears, modal closes, overlay stays open
    8. Verify: item appears in collection (navigate to /collection to check)

    **Flow 2: Add to Collection from global item detail page**
    1. Click a catalog card to navigate to its detail page (`/global-items/:id`)
    2. Verify: both "Add to Collection" and "Add to Thread" buttons are visible
    3. Click "Add to Collection"
    4. Verify: same modal opens, submit works, toast appears

    **Flow 3: Add to Thread from catalog search (existing thread)**
    1. Ensure at least one active thread exists (create one via Planning tab if needed)
    2. Click FAB > "Start Thread"
    3. Search for an item and click "Add"
    4. Verify: thread picker modal opens listing active threads with category names
    5. Select a thread and click "Add as Candidate"
    6. Verify: toast "Added to [Thread Name]" appears
    7. Click "Add" on another item -- verify the previously selected thread is pre-selected

    **Flow 4: New Thread creation from thread picker**
    1. In thread mode, click "Add" on a catalog card
    2. In the thread picker dropdown, select "+ New Thread..."
    3. Verify: form switches to show thread name + category fields
    4. Fill in thread name, select category, click "Create & Add"
    5. Verify: toast "Created [name] with first candidate" appears
    6. Click "Add" on another card -- verify the new thread is pre-selected (session memory)

    **Flow 5: Add to Thread from global item detail page**
    1. Navigate to a global item detail page
    2. Click "Add to Thread"
    3. Verify: thread picker modal opens, same as Flow 3

    **Flow 6: Thread resolution regression (CATFLOW-06)**
    1. Go to a thread that has a catalog-linked candidate (from Flow 3/4)
    2. Resolve the thread by selecting that candidate
    3. Verify: a new item appears in collection with the global item data
  </how-to-verify>
  <resume-signal>Type "approved" or describe any issues found</resume-signal>
</task>

</tasks>

<verification>
1. `bun run build` passes with no type errors
2. `bun test tests/services/thread.service.test.ts` passes (CATFLOW-06 regression -- resolveThread with globalItemId)
3. Add-to-collection flow works from both entry points
4. Add-to-thread flow works with existing threads and new thread creation
5. Session thread memory works within a search session
6. Thread resolution with catalog-linked candidate creates reference item
</verification>

<success_criteria>
- CATFLOW-05 is functional: user can add catalog items as thread candidates from search
- CATFLOW-06 is verified: resolving a catalog-linked candidate creates a reference item (confirmed by `bun test tests/services/thread.service.test.ts` and manual Flow 6)
- AddToThreadModal supports existing thread selection AND new thread + candidate creation
- Session thread tracking remembers selection within a catalog search session
- All flows accessible from both catalog search overlay and global item detail page
</success_criteria>

<output>
After completion, create `.planning/phases/22-add-from-catalog-thread-integration/22-02-SUMMARY.md`
</output>
