---
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"
- "Resolving a thread with a catalog-linked candidate creates a reference item (already works)"
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"
---
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 (thread resolution with catalog-linked candidates).
Purpose: CATFLOW-05 -- users can add catalog items as thread candidates from search. CATFLOW-06 -- resolution already works, verify it.
Output: Working add-to-thread flow, complete Phase 22 catalog integration.
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
@.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
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("/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(url: string, body: unknown): Promise
```
From src/client/hooks/useGlobalItems.ts:
```typescript
export function useGlobalItem(id: number)
// Returns globalItem with: id, brand, model, category, weightGrams, priceCents, imageUrl, description, ownerCount
```
Task 1: Build AddToThreadModal with thread picker and new thread flow
src/client/components/AddToThreadModal.tsx
src/client/routes/__root.tsx
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
**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
- `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 `
`
- Thread dropdown `
cd /home/jean-luc-makiola/Development/projects/GearBox && bun run build 2>&1 | tail -5
- `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 ``
- `bun run build` exits with code 0
AddToThreadModal supports picking an existing active thread and creating a new thread with first candidate. Session thread tracking persists across adds. Both catalog search overlay and global item detail page wire to this modal via UIStore. Build passes.Task 2: Verify complete add-from-catalog flows
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.
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 (CATFLOW-06 -- existing behavior)**
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
Type "approved" or describe any issues found
1. `bun run build` passes with no type errors
2. `bun test` passes (no backend changes, existing tests should remain green)
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
- 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
- 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